diff --git a/.gitignore b/.gitignore index afad76fc..09762bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ /data/ /data-test/ .zcode/plans/ +runtime/ diff --git a/Makefile b/Makefile index 0d1212b8..5776a98b 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/scripts/svc.sh b/scripts/svc.sh new file mode 100755 index 00000000..8ec59f60 --- /dev/null +++ b/scripts/svc.sh @@ -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//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 :进程存在且 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 "$@"