Files
Agent-System-Dev/scripts/svc.sh
T
weichen 8a494e87c7 Add CLI flags to svc.sh (addr/host/port/data/dsn/auth/mock/passthrough)
Why: the manager only took configuration from SVC_* environment
variables; binding a public interface or pointing at a database meant
exporting vars first, which is awkward for one-off deploys and
invisible in shell history review.

Evidence: live matrix on the worktree - --port (8801), --host 0.0.0.0
(bind verified via ss, health probe auto-maps to 127.0.0.1), --addr
whole-form, CLI-over-env precedence (--port beats SVC_DEV_ADDR),
--mock reaching the server cmdline, --dsn passthrough proven by the
postgres connect attempt in the log, -- passthrough (--artifacts), and
make svc ARGS passthrough. A set -e silent-exit trap in env_config was
caught by the first run and fixed (condition-as-last-statement).

Impact: scripts/svc.sh <env> [action] [--addr|--host|--port|--data|
--dsn|--auth|--mock|--plugins| -- <extra>]; precedence CLI > env vars >
defaults; help rewritten with examples; make svc gains ARGS; logs keeps
the positional line-count form. The real-world port guard held: a dev
instance started from the main checkout was refused with owner info,
untouched.

Refs: feature/svc-cli-args task card.
2026-09-30 11:50:24 +00:00

247 lines
9.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# svc.sh —— 多环境服务管理:按环境隔离地 停止旧服务 → 构建最新 → 启动。
#
# 用法:
# scripts/svc.sh <dev|prod> [action] [参数...]
# action:deploy(默认)| stop | start | restart | status | logs [行数]
# scripts/svc.sh status # 全环境总览
#
# 参数(优先级:命令行 > 环境变量 > 默认值):
# --addr host:port 监听地址(整体覆盖)
# --host host 只改主机部分(如 0.0.0.0 对外开放)
# --port port 只改端口部分
# --data dir 数据目录(SQLite 与工件)
# --dsn DSN PostgreSQL 档 DSN(数据库地址/端口/库名都在 DSN 内,如
# postgres://user:pass@dbhost:5432/agentdb)——含密码时建议
# 改用环境变量 SVC_PROD_DSN 传递,避免进 shell 历史
# --auth on|off|auto API key 认证(默认 auto:PG 档开、嵌入式关)
# --mock 演示模式(dev 常用)
# --plugins dir 插件目录(默认 <repo>/plugins)
# -- <剩余参数> 原样透传给 agent-system serve(escape hatch)
#
# 环境变量默认值:
# dev: 127.0.0.1:8788 数据 runtime/dev/data;SVC_DEV_ADDR/DATA/MOCK=1 可覆盖
# prod: 127.0.0.1:8787 数据 runtime/prod/data;SVC_PROD_ADDR/DATA/DSN/AUTH 可覆盖
#
# 示例:
# scripts/svc.sh dev --port 9000 --mock
# scripts/svc.sh prod --host 0.0.0.0 --dsn postgres://u:p@127.0.0.1:5432/as
# scripts/svc.sh dev deploy --addr 0.0.0.0:8788
# make svc ENV=dev ARGS="--port 9000"
#
# 安全约束:
# - 只停自己启动过的进程(pidfile + /proc/<pid>/cmdline 双重校验),绝不动他人现场;
# - 端口被非本脚本进程占用时明确报错并给出占用者信息,不抢不杀。
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME="$ROOT/runtime"
BIN="$ROOT/bin/agent-system"
c_info() { printf '\033[1;34m[svc]\033[0m %s\n' "$*"; }
c_ok() { printf '\033[1;32m[svc]\033[0m %s\n' "$*"; }
c_warn() { printf '\033[1;33m[svc]\033[0m %s\n' "$*"; }
c_err() { printf '\033[1;31m[svc]\033[0m %s\n' "$*" >&2; }
usage() {
sed -n '2,36p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}
# ---- 命令行参数(覆盖层)----
CLI_ADDR=""; CLI_HOST=""; CLI_PORT=""; CLI_DATA=""; CLI_DSN=""
CLI_AUTH=""; CLI_MOCK=0; CLI_PLUGINS=""; CLI_EXTRA=""; CLI_LOGS_N=""
need_value() { [ $# -ge 2 ] || { c_err "参数 $1 需要值"; exit 2; }; }
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--addr) need_value "$@"; CLI_ADDR="$2"; shift 2 ;;
--host) need_value "$@"; CLI_HOST="$2"; shift 2 ;;
--port) need_value "$@"; CLI_PORT="$2"; shift 2 ;;
--data) need_value "$@"; CLI_DATA="$2"; shift 2 ;;
--dsn) need_value "$@"; CLI_DSN="$2"; shift 2 ;;
--auth) need_value "$@"; CLI_AUTH="$2"; shift 2 ;;
--plugins) need_value "$@"; CLI_PLUGINS="$2"; shift 2 ;;
--mock) CLI_MOCK=1; shift ;;
--) shift; CLI_EXTRA="$*"; break ;;
-h|--help) usage; exit 0 ;;
*) c_err "未知参数:$1"; usage; exit 2 ;;
esac
done
}
# addr_compose 基础值(host:port)按 CLI 的 --addr/--host/--port 组合出最终地址。
addr_compose() {
local base="$1" host port
host="${base%:*}"; port="${base##*:}"
[ -n "$CLI_ADDR" ] && { echo "$CLI_ADDR"; return 0; }
[ -n "$CLI_HOST" ] && host="$CLI_HOST"
[ -n "$CLI_PORT" ] && port="$CLI_PORT"
echo "$host:$port"
}
env_config() {
case "$1" in
dev)
ENV_ADDR="$(addr_compose "${SVC_DEV_ADDR:-127.0.0.1:8788}")"
ENV_DATA="${CLI_DATA:-${SVC_DEV_DATA:-$RUNTIME/dev/data}}"
ENV_DIR="$RUNTIME/dev"
;;
prod)
ENV_ADDR="$(addr_compose "${SVC_PROD_ADDR:-127.0.0.1:8787}")"
ENV_DATA="${CLI_DATA:-${SVC_PROD_DATA:-$RUNTIME/prod/data}}"
ENV_DIR="$RUNTIME/prod"
;;
*) c_err "未知环境:$1(可用:dev | prod)"; usage; exit 2 ;;
esac
ENV_PID="$ENV_DIR/svc.pid"
ENV_LOG="$ENV_DIR/svc.log"
ENV_PLUGINS="${CLI_PLUGINS:-$ROOT/plugins}"
# 运行参数:mock/DSN/auth(CLI 与环境变量取并集,CLI 优先)。
ENV_ARGS=""
if [ "$ENV_NAME" = dev ]; then
if [ "${SVC_DEV_MOCK:-0}" = "1" ] || [ "$CLI_MOCK" = 1 ]; then
ENV_ARGS="--mock"
fi
else
local dsn="${CLI_DSN:-${SVC_PROD_DSN:-}}"
if [ -n "$dsn" ]; then ENV_ARGS="$ENV_ARGS --dsn $dsn"; fi
ENV_ARGS="$ENV_ARGS --auth ${CLI_AUTH:-${SVC_PROD_AUTH:-auto}}"
fi
}
# pid_alive <pid>:进程存在且 cmdline 是我们的 serve(防 PID 复用误杀)。
pid_alive() {
local pid="$1"
[ -n "$pid" ] && [ -r "/proc/$pid/cmdline" ] || return 1
local cmd
cmd="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)" || return 1
case "$cmd" in
*agent-system*serve*) return 0 ;;
*) return 1 ;;
esac
}
read_pid() { [ -f "$ENV_PID" ] && cat "$ENV_PID" 2>/dev/null || true; }
do_stop() {
local pid; pid="$(read_pid)"
if [ -z "$pid" ]; then
c_info "[$ENV_NAME] 无 pidfile——没有由本脚本启动的实例"
return 0
fi
if ! pid_alive "$pid"; then
c_warn "[$ENV_NAME] pidfile 指向的进程已不存在(pid=$pid),清理 pidfile"
rm -f "$ENV_PID"
return 0
fi
c_info "[$ENV_NAME] 停止旧服务 pid=$pid(SIGTERM,等待优雅停机)"
kill -TERM "$pid"
for _ in $(seq 1 100); do
pid_alive "$pid" || { c_ok "[$ENV_NAME] 已停止"; rm -f "$ENV_PID"; return 0; }
sleep 0.1
done
c_warn "[$ENV_NAME] 10s 未退出,SIGKILL 兜底"
kill -KILL "$pid" 2>/dev/null || true
rm -f "$ENV_PID"
c_ok "[$ENV_NAME] 已强制停止"
}
port_owner() {
local port="${ENV_ADDR##*:}"
if command -v ss >/dev/null 2>&1; then
ss -ltnp 2>/dev/null | awk -v p=":$port" '$4 ~ p"$" {print $0; exit}'
fi
}
do_build() {
c_info "[$ENV_NAME] 构建最新组合(make build:前端 + 内嵌后端)"
if ! make -C "$ROOT" build; then
c_err "[$ENV_NAME] 构建失败——旧服务已停止,本次部署中止(修复后重跑)"
exit 1
fi
}
do_start() {
if pid_alive "$(read_pid)"; then
c_warn "[$ENV_NAME] 已在运行 pid=$(read_pid)(如需换参数/新版本请用 deploy/restart)"
return 0
fi
mkdir -p "$ENV_DATA" "$ENV_DIR"
# 端口占用预检:他人现场不动,只报告。
local owner; owner="$(port_owner)"
if [ -n "$owner" ]; then
c_err "[$ENV_NAME] 端口被占用(非本脚本管理):$owner"
c_err "处理:停掉该进程,或换端口(--port 8799 / --addr host:port)"
exit 1
fi
[ -x "$BIN" ] || { c_err "[$ENV_NAME] 缺少二进制 $BIN——先构建(deploy 会自动构建)"; exit 1; }
c_info "[$ENV_NAME] 启动:$BIN serve --addr $ENV_ADDR --data $ENV_DATA --plugins $ENV_PLUGINS $ENV_ARGS $CLI_EXTRA"
# 直接 nohup 简单命令(不经 cd 复合后台作业):$! 即服务进程本身,pidfile 语义才成立。
nohup "$BIN" serve --addr "$ENV_ADDR" --data "$ENV_DATA" --plugins "$ENV_PLUGINS" $ENV_ARGS $CLI_EXTRA \
>> "$ENV_LOG" 2>&1 &
echo $! > "$ENV_PID"
printf 'addr=%s\ndata=%s\n' "$ENV_ADDR" "$ENV_DATA" > "$ENV_DIR/svc.env"
# 健康等待:最多 20s;探测主机取绑定主机(0.0.0.0/:: → 127.0.0.1)。
local host="${ENV_ADDR%:*}" port="${ENV_ADDR##*:}"
if [ "$host" = "0.0.0.0" ] || [ "$host" = "::" ]; then host="127.0.0.1"; fi
for _ in $(seq 1 100); do
if curl -s -o /dev/null -m 2 "http://$host:$port/" 2>/dev/null; then
c_ok "[$ENV_NAME] 已启动 pid=$(cat "$ENV_PID") → http://$host:$port(日志:$ENV_LOG)"
return 0
fi
pid_alive "$(read_pid)" || { c_err "[$ENV_NAME] 进程启动后即退出,查看日志:tail -40 $ENV_LOG"; exit 1; }
sleep 0.2
done
c_err "[$ENV_NAME] 20s 健康检查未通过(进程存活但未响应),查看日志:tail -40 $ENV_LOG"
exit 1
}
env_status_line() {
local pid; pid="$(read_pid)"
# 运行态显示实际启动参数(svc.env 由 start 落盘);未运行显示当前配置默认值。
local addr="$ENV_ADDR" data="$ENV_DATA"
if pid_alive "$pid" && [ -f "$ENV_DIR/svc.env" ]; then
addr="$(sed -n 's/^addr=//p' "$ENV_DIR/svc.env")"
data="$(sed -n 's/^data=//p' "$ENV_DIR/svc.env")"
fi
if pid_alive "$pid"; then
printf ' %-5s 运行中 pid=%-7s %s 数据 %s\n' "$ENV_NAME" "$pid" "$addr" "$data"
else
printf ' %-5s 未运行 %s 数据 %s\n' "$ENV_NAME" "$addr" "$data"
fi
}
main() {
[ $# -ge 1 ] || { usage; exit 2; }
if [ "$1" = "status" ] && [ $# -eq 1 ]; then
c_info "服务总览(root: $ROOT)"
for e in dev prod; do ENV_NAME="$e"; env_config "$e"; env_status_line; done
exit 0
fi
ENV_NAME="$1"; shift
# 动作词可省略(省略 = deploy);flags 可直接跟随环境名。
local action="deploy"
case "${1:-}" in
deploy|stop|start|restart|status|logs) action="$1"; shift ;;
esac
# logs 的行数(兼容 svc.sh dev logs 100)。
if [ "$action" = "logs" ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
CLI_LOGS_N="$1"; shift
fi
parse_args "$@"
env_config "$ENV_NAME"
case "$action" in
deploy) do_stop; do_build; do_start ;;
stop) do_stop ;;
start) do_start ;;
restart) do_stop; do_start ;;
status) env_status_line ;;
logs) [ -f "$ENV_LOG" ] && tail -n "${CLI_LOGS_N:-50}" -f "$ENV_LOG" || c_warn "[$ENV_NAME] 暂无日志" ;;
esac
}
main "$@"