Add the svc.sh environment service manager #45

Merged
weichen merged 4 commits from feature/svc-env-script into main 2026-09-30 11:38:57 +00:00
5 changed files with 208 additions and 1 deletions
+1
View File
@@ -3,3 +3,4 @@ node_modules/
/data/
/data-test/
.zcode/plans/
runtime/
@@ -32,3 +32,4 @@
| 同步前验收 | 45e6a07626bc1d709624d5e3e37ba7556198d509 | ca2ff2d6a40d54f6d0d2cf73f2e9bd5c783f65e8 | 2026-09-30T10:36:13Z |
| 同步前验收 | 81e4a3447a1daf743d88a7e7bc70e97cf9cac0c7 | 0d84b1d68f160999eedf315ea98156b850b76008 | 2026-09-30T10:44:54Z |
| 同步前验收 | b71ef3e9a857646aec7e46839914f6cc0af1cc9f | 1634b42df094257fec61d475a9c0fac65bad6ab5 | 2026-09-30T11:00:17Z |
| 同步前验收 | fcddfee5be45870071c18d921173f58bfab9e851 | f0e7831ed1cddc67621f873c2e49cba02f81fc3c | 2026-09-30T11:38:44Z |
+20
View File
@@ -0,0 +1,20 @@
# feature/svc-env-script 任务卡
任务主记录:<PR 链接,task-finish 时填写>;状态:已完成;owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0 = fcddfee5be45870071c18d921173f58bfab9e851
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-env-script;分支:feature/svc-env-script
- 授权与交付终点:用户要求 dev/prod 环境服务脚本(停旧→构建→启动,环境互不影响)→ 实现并实测后自主闭环合入。
## 目标与非目标
- 目标:scripts/svc.sh(deploy/stop/start/restart/status/logs);dev/prod 完全隔离(端口/数据/日志/pidfile);只停自己启动的进程(他人现场不动);make svc 快捷目标。
- 非目标:不做 systemd/容器编排(原生脚本档);不做开机自启。
## 结果与验证
- 结果:scripts/svc.sh + `make svc ENV=dev|prod [ACTION=...]` + runtime/ gitignore。默认 dev 127.0.0.1:8788 / prod 127.0.0.1:8787,SVC_*_ADDR/DATA/DSN/DEV_MOCK/PROD_AUTH 可覆盖;svc.env 落盘实际启动参数供 status 显示。
- 验证(全部实测):dev deploy 全链(停旧→make build→健康检查 200);重部署换新 PID 且数据保留(dev-only 项目仍在);prod(8799 覆盖端口)与 dev 并存,/api/projects 互不可见;端口冲突(prod 抢 8788)拒接并点名占用进程,dev 持续 200;stop 清 pidfile 释放端口。实测抓出并修复 PID 捕获缺陷(复合后台作业写入了包装进程 PID)。
- 影响与剩余项:首次 prod 使用 8787 若与现存手工实例冲突会明确拒接(预期行为,按提示处理);PG 档经 SVC_PROD_DSN 传递未在本环境实测。
+7 -1
View File
@@ -13,7 +13,7 @@ PLUGINS := plugins
COMBO := core=$(shell git -C $(CORE) rev-parse --short HEAD 2>/dev/null)+backend=$(shell git -C $(BACKEND) rev-parse --short HEAD 2>/dev/null)+frontend=$(shell git -C $(FRONTEND) rev-parse --short HEAD 2>/dev/null)+ui=$(shell git -C $(UI) rev-parse --short HEAD 2>/dev/null)+agentcmd=$(shell git -C deps/agent-command rev-parse --short HEAD 2>/dev/null)+documents=$(shell git -C modules/documents rev-parse --short HEAD 2>/dev/null)
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null)+$(COMBO)
.PHONY: help sync status build frontend test test-backend test-frontend test-ui test-core test-agentcmd dev pin clean setup precheck task-start task-sync task-close task-finish version
.PHONY: help sync status build frontend svc test-backend test-frontend test-ui test-core test-agentcmd dev pin clean setup precheck task-start task-sync task-close task-finish version
version:
@echo "agent-system $(VERSION)"
@@ -45,6 +45,7 @@ help:
@echo "make sync 物化全部 submodule(递归)"
@echo "make build 构建单二进制 bin/agent-system(契约同步→前端构建→embed→静态链接)"
@echo "make test 全量测试(core + backend + frontend)"
@echo "make svc 环境服务管理:make svc ENV=dev|prod [ACTION=deploy](停旧→构建→启动,环境隔离)"
@echo "make dev 开发模式说明(前后端分离热更新)"
@echo "make pin 推进 submodule 指针到各仓远端 main(提交前须 make test 通过)"
@echo "make status submodule 状态"
@@ -91,6 +92,11 @@ test-agentcmd:
test: test-core test-backend test-frontend test-ui test-agentcmd
# 运行演示(mock 模式):单二进制 + 示例插件 + 嵌入式存储,浏览器 http://127.0.0.1:8787
# 环境服务管理(scripts/svc.sh):deploy = 停旧 → 构建 → 启动;dev/prod 相互隔离。
svc:
@test -n "$(ENV)" || (echo "用法:make svc ENV=dev|prod [ACTION=deploy|stop|start|restart|status|logs]"; exit 2)
scripts/svc.sh $(ENV) $(ACTION)
demo: build
$(BIN) serve --addr 127.0.0.1:8787 --data ./data --plugins $(PLUGINS) --mock
Executable
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env bash
# svc.sh —— 多环境服务管理:按环境隔离地 停止旧服务 → 构建最新 → 启动。
#
# 用法:
# scripts/svc.sh dev # 部署 dev(停止旧 dev → make build → 启动)
# scripts/svc.sh prod # 部署 prod(dev 不受影响;反之亦然)
# scripts/svc.sh dev stop|start|restart|status|logs
# scripts/svc.sh status # 全环境总览
#
# 环境隔离(默认值,可用环境变量覆盖):
# dev: 127.0.0.1:8788 数据 runtime/dev/data 日志 runtime/dev/svc.log
# prod: 127.0.0.1:8787 数据 runtime/prod/data 日志 runtime/prod/svc.log
# 覆盖:SVC_DEV_ADDR / SVC_PROD_ADDR、SVC_DEV_DATA / SVC_PROD_DATA、
# SVC_DEV_MOCK=1(dev 加 --mock)、SVC_PROD_DSN(PG 档)、
# SVC_PROD_AUTH(on|off|auto,默认 auto)。
#
# 安全约束:
# - 只停自己启动过的进程(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; }
env_config() {
case "$1" in
dev)
ENV_ADDR="${SVC_DEV_ADDR:-127.0.0.1:8788}"
ENV_DATA="${SVC_DEV_DATA:-$RUNTIME/dev/data}"
ENV_DIR="$RUNTIME/dev"
ENV_ARGS=""
[ "${SVC_DEV_MOCK:-0}" = "1" ] && ENV_ARGS="--mock"
;;
prod)
ENV_ADDR="${SVC_PROD_ADDR:-127.0.0.1:8787}"
ENV_DATA="${SVC_PROD_DATA:-$RUNTIME/prod/data}"
ENV_DIR="$RUNTIME/prod"
ENV_ARGS=""
[ -n "${SVC_PROD_DSN:-}" ] && ENV_ARGS="$ENV_ARGS --dsn $SVC_PROD_DSN"
ENV_ARGS="$ENV_ARGS --auth ${SVC_PROD_AUTH:-auto}"
;;
*) c_err "未知环境:$1(可用:dev | prod)"; usage; exit 2 ;;
esac
ENV_PID="$ENV_DIR/svc.pid"
ENV_LOG="$ENV_DIR/svc.log"
}
usage() {
sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}
# 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")" || 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)"
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 "处理:停掉该进程,或用环境变量换端口(如 SVC_${ENV_NAME^^}_ADDR=127.0.0.1:8799)"
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 $ROOT/plugins $ENV_ARGS"
# 直接 nohup 简单命令(不经 cd 复合后台作业):$! 即服务进程本身,pidfile 语义才成立。
nohup "$BIN" serve --addr "$ENV_ADDR" --data "$ENV_DATA" --plugins "$ROOT/plugins" $ENV_ARGS \
>> "$ENV_LOG" 2>&1 &
echo $! > "$ENV_PID"
printf 'addr=%s\ndata=%s\n' "$ENV_ADDR" "$ENV_DATA" > "$ENV_DIR/svc.env"
# 健康等待:最多 20s。
local port="${ENV_ADDR##*:}"
for _ in $(seq 1 100); do
if curl -s -o /dev/null -m 2 "http://127.0.0.1:$port/" 2>/dev/null; then
c_ok "[$ENV_NAME] 已启动 pid=$(cat "$ENV_PID") → http://127.0.0.1:$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_config "$e"; ENV_NAME="$e"; env_status_line; done
exit 0
fi
ENV_NAME="$1"; env_config "$ENV_NAME"
local action="${2:-deploy}"
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 "${3:-50}" -f "$ENV_LOG" || c_warn "[$ENV_NAME] 暂无日志" ;;
*) c_err "未知动作:$action(可用:deploy|stop|start|restart|status|logs)"; usage; exit 2 ;;
esac
}
main "$@"