Merge pull request 'Add commit-picked deploys and a service registry to svc.sh' (#55) from feature/svc-deploy-source-registry into main
ci / test (push) Failing after 30s

This commit was merged in pull request #55.
This commit is contained in:
2026-09-30 13:21:43 +00:00
4 changed files with 661 additions and 61 deletions
@@ -37,3 +37,4 @@
| 同步前验收 | f832f0413e0baa358e7422db5edacb180188635a | 5c2fa4562b3010a94bbaa4731299223e70953ee4 | 2026-09-30T11:56:42Z |
| 同步前验收 | 251bd94847595dcb01c5571448b28ee3d75da956 | 52d0e45c81edd4e8ea2226439766301610d8faa3 | 2026-09-30T12:00:26Z |
| 同步前验收 | 1fd4fade4a882f11240a39256354fd04c8101ff1 | 599a6448d4e5b13e89753eaddc168292689b1367 | 2026-09-30T12:41:46Z |
| 同步前验收 | 8c4fd019475e57aa70646bed9f2ee1a334b72c45 | 8ba1b0e71570aed2ef277d11763a89ffb99f1b56 | 2026-09-30T13:20:32Z |
@@ -0,0 +1,78 @@
# feature/svc-deploy-source-registry 任务卡
任务主记录:https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/55;状态:已完成(待合入);owner:weichen
## 基线与授权
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 8c4fd019475e57aa70646bed9f2ee1a334b72c45
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-deploy-source-registry(worktree);分支:feature/svc-deploy-source-registry
- 授权与交付终点:用户授权实施 svc.sh 增强(提交选择部署 + 服务登记);终点 = 实现合入 main 并回收工作区。仅改 Dev 仓 `scripts/svc.sh` 与顶层 `Makefile` 帮助文案,不动模块 gitlink。
## 目标与非目标
目标(scripts/svc.sh):
1. **按提交部署**:可列出 Dev 仓(本仓,每提交 = 一组固定 gitlink 组合)与 Releases 仓
(`../Agent-System-Releases`,每提交 = 一个发布版本)的提交,并选定提交部署:
- 新子命令 `svc.sh commits [dev|release] [N]`:编号列表(短 SHA、日期、主题;release 附
版本号与 devCommit;dev 标注 HEAD 与已部署环境)。
- `svc.sh <env> deploy --from SOURCE`:`local`(默认,保持现行为:构建当前工作区)|
`dev[<@sha>]`(隔离 worktree 物化 gitlink → make build → 复制制品)|
`release[<@version|sha>]`(取 Releases `build/<v>/` 制品,sha256 对 manifest 校验)。
- `--pick [dev|release]` 交互式编号选择(仅 TTY);`--dry-run` 只解析打印计划。
2. **服务登记不限 dev/prod**:环境名泛化(`^[a-z][a-z0-9-]{0,15}$`,保留字除外),
`SVC_<ENV>_ADDR/DATA/DSN/AUTH/MOCK` 对任意环境生效;自定义环境无显式地址时端口由
环境名确定性派生(8800+hash,冲突扫描递增)。启动登记落盘:
- `runtime/<env>/svc.env`(当前态:env/pid/addr/data/bin/source/version/started)
- `runtime/<env>/history.jsonl`(追加式:deploy/start/stop 事件,含来源与结果)
- 环境自包含:`runtime/<env>/{bin/agent-system,plugins/}`,跨环境互不串版本。
- `svc.sh status` 扫描 runtime/* 枚举全部环境(不再硬编码 dev/prod);
新子命令 `svc.sh history [env] [N]` 跨环境合并查看。
非目标:不改 agent-system serve 的接口与二进制行为;不做容器/多机;不做 git fetch 拉远端新
提交(commits 只列本地已有对象,缺失时提示先 fetch);不改 Releases 仓脚本。
## 设计要点
- 兼容:dev/prod 默认地址、SVC_DEV_*/SVC_PROD_* 变量、stop 只停自己 pidfile 的安全约束、
端口占用预检、播种逻辑全部保留。`start` 改用环境目录内二进制;旧环境(无环境产物)首次
需重新 deploy(文档注明)。
- dev@sha 构建用 `git worktree add --detach runtime/.worktrees/<sha>` + 递归 submodule
update,不触碰主检出工作区(保护他人现场);成功后回收 worktree,失败保留供排障。
- release 制品校验失败即中止部署(事实与声明一致);无 build/<v> 时提示在 Releases 仓跑
`scripts/build-release.sh <v>`。
- Releases 仓定位:`SVC_RELEASES_DIR` > `../Agent-System-Releases`(相对本仓根)。
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | 8c4fd019475e57aa70646bed9f2ee1a334b72c45 | 8c4fd019475e57aa70646bed9f2ee1a334b72c45 | 2026-09-30T13:03:31Z |
## 结果与验证(完成后填写)
- 结果:`scripts/svc.sh` 重写完成(348 → ~790 行),Makefile 增加 svc-commits/svc-history 目标。
- 提交选择部署:`commits [dev|release] [N]` 列举(dev 标注 HEAD/已部署环境;release 附版本
与 devCommit);`deploy --from local|dev[@sha]|release[@版本|sha]`;`--pick` 交互选择;
`--dry-run` 打印计划。
- 服务登记:环境名泛化 + 派生端口;环境自包含(bin/plugins/data/记录);svc.env 当前态 +
history.jsonl 事件流;`status` 扫描发现全部环境;`history [env] [N]` 汇总。
- 附带修复:旧制品二进制不识别 --dsn/--auth 时按 serve 用法探测自动省略(0.1.0 制品实测)。
- 验证(全部在任务工作区 runtime 内执行,未触碰主检出与 Releases 仓):
- `bash -n` 通过;shellcheck 本机未安装(缺项)。
- commits dev/release 列举正确(无 versions/ 的骨架提交显示 `-`)。
- dry-run 三种来源计划正确;非法 sha/版本/来源/环境名/保留字均报错 exit 2。
- stub 二进制全链路:start → status 发现 → svc.env(pid/addr/bin/version)→ HTTP 200 →
stop → history 登记 start/stop ok。
- 真实 release@0.1.0 部署:sha256 校验通过 → 产物复制 → 启动(HTTP 200)→ status 显示
来源 → commits release 标注 [已部署:testrel]。
- 失败路径:sha256 不符(伪造 manifest)拒绝部署并登记 deploy fail;启动即退登记 start fail
且 restart 传播 exit 1;--pick 非 TTY 拒绝。
- dev@d26187e 真实部署:隔离 worktree 递归物化(子模块精确落位该提交 gitlink)→ make
build → 版本串 d26187e+core=b6d428b+backend=9a545b1+frontend=9fc1387+ui=3f692d2(与该
提交组合一致)→ 产物复制 → 启动 HTTP 200 → status 来源 dev@d26187e → 构建 worktree 已回收
(git worktree list 无残留);该提交产物同样不识别 --auth,自动省略逻辑生效。
- 兼容:旧格式 svc.env(仅 addr/data)在 status 正常显示;SVC_STAGING_ADDR/--env 文件/
--port 覆盖生效;make svc-commits/svc-history 可用。
- 影响与剩余项:存量环境(旧脚本部署)无环境产物,升级后首次需重新 deploy(已在头注释注明);
commits 只列本地对象,远端新提交需先 git fetch(设计内);dev@sha 构建需 npm/go 网络。
+10 -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 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
.PHONY: help sync status build frontend svc svc-commits svc-history 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)"
@@ -46,6 +46,8 @@ help:
@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 svc-commits 列出可部署提交:make svc-commits SRC=dev|release [N=20]"
@echo "make svc-history 启动/部署历史:make svc-history [ENV] [N=20]"
@echo "make dev 开发模式说明(前后端分离热更新)"
@echo "make pin 推进 submodule 指针到各仓远端 main(提交前须 make test 通过)"
@echo "make status submodule 状态"
@@ -97,6 +99,13 @@ svc:
@test -n "$(ENV)" || (echo "用法:make svc ENV=dev|prod [ACTION=deploy|stop|start|restart|status|logs] [ARGS='--port 9000 --mock ...']"; exit 2)
scripts/svc.sh $(ENV) $(ACTION) $(ARGS)
svc-commits:
@test -n "$(SRC)" || (echo "用法:make svc-commits SRC=dev|release [N=20]"; exit 2)
scripts/svc.sh commits $(SRC) $(N)
svc-history:
scripts/svc.sh history $(ENV) $(N)
demo: build
$(BIN) serve --addr 127.0.0.1:8787 --data ./data --plugins $(PLUGINS) --mock
+572 -60
View File
@@ -1,25 +1,41 @@
#!/usr/bin/env bash
# svc.sh —— 多环境服务管理:按环境隔离地 停止旧服务 → 构建最新 → 启动。
# svc.sh —— 多环境服务管理:按环境隔离地 停止旧服务 → 取源(本地 / Dev 提交 / Release
# 版本)→ 构建 / 校验 → 启动,并登记启动记录。
#
# 用法:
# scripts/svc.sh <dev|prod> [action] [参数...]
# scripts/svc.sh <env> [action] [参数...]
# env :任意环境名(^[a-z][a-z0-9-]{0,15}$;dev/prod 有默认配置,其余自动派生)
# action:deploy(默认)| stop | start | restart | status | logs [行数]
# scripts/svc.sh status # 全环境总览
# scripts/svc.sh status # 全环境总览(扫描 runtime/* 下已登记环境)
# scripts/svc.sh commits [dev|release] [N] # 列出可部署提交(默认 dev,N 默认 20)
# scripts/svc.sh history [env] [N] # 启动/部署历史(默认全部环境,最近 20 条)
#
# 参数(优先级:命令行 > --env 配置文件 > 环境变量/.svc.env > 默认值):
# 部署来源(仅 deploy;默认 local):
# --from local 构建当前工作区(make build),复制产物到环境目录
# --from dev 构建 Dev 仓(本仓)当前 HEAD 的 gitlink 组合
# --from dev@<sha> 在隔离 worktree(runtime/.worktrees/<sha>)物化该提交
# 的子模块组合并构建——不触碰当前工作区;子模块对象缺失时
# 需网络拉取
# --from release Releases 仓最新版本制品(定位:SVC_RELEASES_DIR >
# ../Agent-System-Releases)
# --from release@<版本|sha> 指定 Releases 版本;制品取 build/<v>/,sha256 对
# versions/<v>/manifest.json 校验,不符即中止
# --pick [dev|release] 交互式编号选择提交后部署(仅 TTY;非交互请用 --from)
# --dry-run 只解析并打印部署计划,不执行
#
# 其余参数(优先级:命令行 > --env 配置文件 > 环境变量/.svc.env > 默认值):
# --env FILE 环境配置文件(dotenv:KEY=VALUE,支持 # 注释、成对引号、
# export 前缀;只识别合法变量名,不执行文件内容)。
# 文件里写 SVC_DEV_ADDR/SVC_MODEL_* 等,CLI 参数可覆盖其中任意项
# 任意环境 X 的变量名为 SVC_<X 大写,- → _>_ADDR/DATA/DSN/AUTH/MOCK
# (如环境 staging → SVC_STAGING_ADDR)
# --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 历史
# --dsn DSN PostgreSQL 档 DSN(dev 外环境;含密码时建议用环境变量传递)
# --auth on|off|auto API key 认证(默认 auto:PG 档开、嵌入式关)
# --mock 演示模式(dev 常用)
# --plugins dir 插件目录(默认 <repo>/plugins)
# --plugins dir 插件目录(默认 <env>/plugins,随 deploy 复制)
# --model-base URL 启动后自动播种主模型配置(settings.models.chat.baseUrl)
# --model-key KEY 主模型 API Key(播种用;勿写入脚本/仓库)
# --model-name NAME 主模型名称
@@ -27,9 +43,16 @@
# --tavily-keys K1,K2 网络搜索密钥池(播种 tools.tavily.apiKeys)
# -- <剩余参数> 原样透传给 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 可覆盖
# 环境与登记:
# - dev 默认 127.0.0.1:8788、prod 默认 127.0.0.1:8787;自定义环境默认端口按环境名
# 确定性派生(8800 起,与已登记端口/监听端口冲突时自动递增),可 SVC_<ENV>_ADDR 覆盖
# - 每个环境自包含:runtime/<env>/{bin/agent-system, plugins/, data/, svc.pid,
# svc.log, svc.env, history.jsonl}——跨环境互不串版本,回退 = 重新 deploy 旧来源
# - 启动登记:svc.env(当前态:pid/addr/data/bin/source/version/started)+
# history.jsonl(追加式事件流:deploy/start/stop);status 与 history 从这些文件
# 汇总,不依赖脚本内存状态
# - start/restart 使用环境目录内产物;环境无产物时先 deploy(旧版脚本部署过的环境
# 无环境产物,同样需要重新 deploy 一次)
#
# 一次性配置:.svc.env(自动加载)或 --env 指定文件(如 .env.dev/.env.prod,
# 均建议 gitignore)。文件示例(.env.dev):
@@ -39,14 +62,21 @@
# SVC_MODEL_NAME=glm-4.7
#
# 示例:
# scripts/svc.sh dev --env ./.env.dev # 用配置文件启动
# scripts/svc.sh dev --env ./.env.dev --port 9000 # CLI 覆盖文件里的端口
# scripts/svc.sh prod --host 0.0.0.0 --dsn postgres://u:p@127.0.0.1:5432/as
# scripts/svc.sh commits dev # 看 Dev 仓可部署提交
# scripts/svc.sh commits release # 看 Releases 仓版本
# scripts/svc.sh staging deploy --pick # 交互选提交部署到新环境 staging
# scripts/svc.sh prod deploy --from dev@73b2802 # 部署 Dev 仓指定提交
# scripts/svc.sh prod deploy --from release@0.1.0 # 部署(回退到)发布版本
# scripts/svc.sh dev --env ./.env.dev # 用配置文件启动
# scripts/svc.sh dev --env ./.env.dev --port 9000
# scripts/svc.sh status && scripts/svc.sh history
# make svc ENV=dev ARGS="--env .env.dev --port 9000"
#
# 安全约束:
# - 只停自己启动过的进程(pidfile + /proc/<pid>/cmdline 双重校验),绝不动他人现场;
# - 端口被非本脚本进程占用时明确报错并给出占用者信息,不抢不杀。
# - 端口被非本脚本进程占用时明确报错并给出占用者信息,不抢不杀;
# - dev@<sha> 构建在隔离 worktree 进行,不改写当前工作区与子模块检出。
# <<usage-end>>
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -60,7 +90,7 @@ 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,46p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,/^# <<usage-end>>$/p' "${BASH_SOURCE[0]}" | sed '$d;s/^# \{0,1\}//'
}
# load_env_file FILE:dotenv 安全解析——逐行 KEY=VALUE 赋值(剥 export 前缀与成对
@@ -88,6 +118,7 @@ CLI_ADDR=""; CLI_HOST=""; CLI_PORT=""; CLI_DATA=""; CLI_DSN=""
CLI_AUTH=""; CLI_MOCK=0; CLI_PLUGINS=""; CLI_EXTRA=""; CLI_LOGS_N=""
CLI_MBASE=""; CLI_MKEY=""; CLI_MNAME=""
CLI_LBASE=""; CLI_LKEY=""; CLI_LNAME=""; CLI_TAVILY=""
CLI_FROM=""; CLI_PICK=""; CLI_DRYRUN=0
need_value() { [ $# -ge 2 ] || { c_err "参数 $1 需要值"; exit 2; }; }
@@ -109,6 +140,10 @@ parse_args() {
--light-key) need_value "$@"; CLI_LKEY="$2"; shift 2 ;;
--light-name) need_value "$@"; CLI_LNAME="$2"; shift 2 ;;
--tavily-keys) need_value "$@"; CLI_TAVILY="$2"; shift 2 ;;
--from) need_value "$@"; CLI_FROM="$2"; shift 2 ;;
--pick) CLI_PICK="dev"
case "${2:-}" in dev|release) CLI_PICK="$2"; shift 2 ;; *) shift ;; esac ;;
--dry-run) CLI_DRYRUN=1; shift ;;
--) shift; CLI_EXTRA="$*"; break ;;
-h|--help) usage; exit 0 ;;
*) c_err "未知参数:$1"; usage; exit 2 ;;
@@ -126,34 +161,103 @@ addr_compose() {
echo "$host:$port"
}
# ---- 环境泛化 ----
RESERVED_ENVS=" status commits history deploy stop start restart logs help version "
# valid_env NAME:环境名合法性(小写字母开头、a-z0-9-、≤16 位;保留字不可用作环境名)。
valid_env() {
case "$RESERVED_ENVS" in *" $1 "*) return 1 ;; esac
[[ "$1" =~ ^[a-z][a-z0-9-]{0,15}$ ]]
}
# env_var ENV SUFFIX:取 SVC_<ENV 大写,- → _>_<SUFFIX> 变量值(无则空)。
env_var() {
local v="SVC_$(printf '%s' "$1" | tr 'a-z-' 'A-Z_')_$2"
printf '%s' "${!v:-}"
}
# ports_recorded PORT SKIP_ENV:除 SKIP_ENV 外,是否有环境登记了该端口(svc.env addr)。
ports_recorded() {
local p="$1" skip="$2" d e a
for d in "$RUNTIME"/*/; do
[ -f "${d}svc.env" ] || continue
e="$(basename "$d")"
[ "$e" = "$skip" ] && continue
a="$(sed -n 's/^addr=.*:\([0-9]\+\)$/\1/p' "${d}svc.env")"
[ "$a" = "$p" ] && return 0
done
return 1
}
# port_listening PORT:端口是否处于监听(ss 可用时)。
port_listening() {
local p="$1"
command -v ss >/dev/null 2>&1 || return 1
ss -ltn 2>/dev/null | awk -v p=":$p" '$4 ~ p"$" {f=1} END {exit !f}'
}
# derive_port NAME:由环境名确定性派生端口(8800+hash%300),与已登记/监听端口冲突
# 时在 8800..9099 内递增扫描;找不到空闲端口则失败。
derive_port() {
local name="$1" p i=0
p=$(( 8800 + ($(printf '%s' "$name" | cksum | cut -d' ' -f1) % 300) ))
while [ "$i" -le 300 ]; do
if ! ports_recorded "$p" "$name" && ! port_listening "$p"; then
echo "$p"; return 0
fi
p=$(( 8800 + (p - 8800 + 1) % 300 ))
i=$((i + 1))
done
return 1
}
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
local name="$1"
valid_env "$name" || {
c_err "非法环境名:$name(^[a-z][a-z0-9-]{0,15}$;保留字 status/commits/history 等不可用)"
exit 2
}
ENV_NAME="$name"
ENV_DIR="$RUNTIME/$name"
ENV_PID="$ENV_DIR/svc.pid"
ENV_LOG="$ENV_DIR/svc.log"
ENV_PLUGINS="${CLI_PLUGINS:-$ROOT/plugins}"
ENV_BIN_DIR="$ENV_DIR/bin"
ENV_BIN="$ENV_BIN_DIR/agent-system"
ENV_PLUGINS="${CLI_PLUGINS:-$ENV_DIR/plugins}"
# 运行参数:mock/DSN/auth(CLI 与环境变量取并集,CLI 优先)。
local base_addr base_data
base_addr="$(env_var "$name" ADDR)"
base_data="$(env_var "$name" DATA)"
case "$name" in
dev) [ -z "$base_addr" ] && base_addr="127.0.0.1:8788"
[ -z "$base_data" ] && base_data="$RUNTIME/dev/data" ;;
prod) [ -z "$base_addr" ] && base_addr="127.0.0.1:8787"
[ -z "$base_data" ] && base_data="$RUNTIME/prod/data" ;;
*)
if [ -z "$base_addr" ]; then
local p
p="$(derive_port "$name")" || {
c_err "[$name] 无空闲派生端口(8800-9099),请用 --port/--addr 或 SVC_$(printf '%s' "$name" | tr 'a-z-' 'A-Z_')_ADDR 显式指定"
exit 1
}
base_addr="127.0.0.1:$p"
fi
[ -z "$base_data" ] && base_data="$RUNTIME/$name/data"
;;
esac
ENV_ADDR="$(addr_compose "$base_addr")"
ENV_DATA="${CLI_DATA:-$base_data}"
# 运行参数:mock/DSN/auth(CLI 与环境变量取并集,CLI 优先;dev 保持只认 mock)。
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 [ "$CLI_MOCK" = 1 ] || [ "$(env_var "$name" MOCK)" = "1" ]; then
ENV_ARGS="--mock"
elif [ "$name" != dev ]; then
local dsn="${CLI_DSN:-$(env_var "$name" DSN)}"
if [ -n "$dsn" ]; then ENV_ARGS="$ENV_ARGS --dsn $dsn"; fi
ENV_ARGS="$ENV_ARGS --auth ${CLI_AUTH:-${SVC_PROD_AUTH:-auto}}"
local auth="${CLI_AUTH:-$(env_var "$name" AUTH)}"
[ -z "$auth" ] && auth=auto
ENV_ARGS="$ENV_ARGS --auth $auth"
fi
}
@@ -171,6 +275,34 @@ pid_alive() {
read_pid() { [ -f "$ENV_PID" ] && cat "$ENV_PID" 2>/dev/null || true; }
# ---- 启动登记(svc.env 当前态 + history.jsonl 事件流)----
# jclean S:剥 JSON 字符串里不能出现的字符(本脚本只写简单 ASCII 值)。
jclean() { printf '%s' "$1" | tr -d '"\001-\037'; }
# hist_event EVENT RESULT PID DETAIL:向 runtime/<env>/history.jsonl 追加一条事件。
hist_event() {
mkdir -p "$ENV_DIR" 2>/dev/null || return 0
printf '{"ts":"%s","event":"%s","env":"%s","source":"%s","result":"%s","pid":"%s","addr":"%s","detail":"%s"}\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
"$(jclean "$1")" "$(jclean "$ENV_NAME")" "$(jclean "${SRC_LABEL:-}")" \
"$(jclean "$2")" "$(jclean "$3")" "$(jclean "$ENV_ADDR")" "$(jclean "$4")" \
>> "$ENV_DIR/history.jsonl" 2>/dev/null || true
}
# record_state:成功启动后把当前态写入 svc.env(status/commits 标注从此读取)。
record_state() {
local pid prev_src
pid="$(read_pid)"
prev_src=""
[ -f "$ENV_DIR/svc.env" ] && prev_src="$(sed -n 's/^source=//p' "$ENV_DIR/svc.env")"
SVC_SOURCE="${SRC_LABEL:-${prev_src:-}}"
mkdir -p "$ENV_DIR"
printf 'env=%s\npid=%s\naddr=%s\ndata=%s\nbin=%s\nsource=%s\nversion=%s\nstarted=%s\n' \
"$ENV_NAME" "${pid:-}" "$ENV_ADDR" "$ENV_DATA" "$ENV_BIN" \
"${SVC_SOURCE:-}" "${SVC_VERSION:-}" "$(date -Is)" > "$ENV_DIR/svc.env"
}
do_stop() {
local pid; pid="$(read_pid)"
if [ -z "$pid" ]; then
@@ -185,13 +317,14 @@ do_stop() {
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; }
pid_alive "$pid" || { c_ok "[$ENV_NAME] 已停止"; rm -f "$ENV_PID"; hist_event stop ok "$pid" "SIGTERM 优雅退出"; 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] 已强制停止"
hist_event stop ok "$pid" "SIGKILL 强制停止"
}
port_owner() {
@@ -205,7 +338,7 @@ do_build() {
c_info "[$ENV_NAME] 构建最新组合(make build:前端 + 内嵌后端)"
if ! make -C "$ROOT" build; then
c_err "[$ENV_NAME] 构建失败——旧服务已停止,本次部署中止(修复后重跑)"
exit 1
return 1
fi
}
@@ -251,6 +384,30 @@ PY
fi
}
# adjust_args_for_bin BIN ARGS:按目标二进制的 serve 用法过滤参数——旧版本产物可能
# 不认识 --dsn/--auth(探测 serve -h 输出);不支持的 flag 连同其值一并省略。
adjust_args_for_bin() {
local bin="$1" args="$2" out
out="$("$bin" serve -h 2>&1 || true)"
local filtered="" w drop_next=0
for w in $args; do
case "$w" in
--dsn|--auth)
if printf '%s\n' "$out" | grep -q -- "$w"; then
filtered="$filtered $w"
else
drop_next=1
fi
;;
*)
if [ "$drop_next" = 1 ]; then drop_next=0; continue; fi
filtered="$filtered $w"
;;
esac
done
printf '%s' "${filtered# }"
}
do_start() {
if pid_alive "$(read_pid)"; then
c_warn "[$ENV_NAME] 已在运行 pid=$(read_pid)(如需换参数/新版本请用 deploy/restart)"
@@ -262,46 +419,391 @@ do_start() {
if [ -n "$owner" ]; then
c_err "[$ENV_NAME] 端口被占用(非本脚本管理):$owner"
c_err "处理:停掉该进程,或换端口(--port 8799 / --addr host:port)"
exit 1
return 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"
if [ ! -x "$ENV_BIN" ]; then
c_err "[$ENV_NAME] 环境产物缺失:$ENV_BIN"
c_err "请先部署:scripts/svc.sh $ENV_NAME deploy [--from local|dev[@<sha>]|release[@<版本>]]"
return 1
fi
SVC_VERSION="$("$ENV_BIN" version 2>/dev/null || true)"
if [ -n "$ENV_ARGS" ]; then
local adjusted; adjusted="$(adjust_args_for_bin "$ENV_BIN" "$ENV_ARGS")"
if [ "$adjusted" != "$ENV_ARGS" ]; then
c_warn "[$ENV_NAME] 该产物不支持 --dsn/--auth(旧版本二进制),已自动省略:$ENV_ARGS → ${adjusted:-<无>}"
fi
ENV_ARGS="$adjusted"
fi
c_info "[$ENV_NAME] 启动:$ENV_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 \
nohup "$ENV_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"
record_state
# 健康等待:最多 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)"
c_ok "[$ENV_NAME] 已启动 pid=$(cat "$ENV_PID") → http://$host:$port(来源 ${SVC_SOURCE:-?},日志:$ENV_LOG)"
hist_event start ok "$(read_pid)" "http://$host:$port"
seed_settings "$host" "$port"
return 0
fi
pid_alive "$(read_pid)" || { c_err "[$ENV_NAME] 进程启动后即退出,查看日志:tail -40 $ENV_LOG"; exit 1; }
pid_alive "$(read_pid)" || {
c_err "[$ENV_NAME] 进程启动后即退出,查看日志:tail -40 $ENV_LOG"
hist_event start fail "$(read_pid)" "进程即退(日志 $ENV_LOG)"
return 1
}
sleep 0.2
done
c_err "[$ENV_NAME] 20s 健康检查未通过(进程存活但未响应),查看日志:tail -40 $ENV_LOG"
exit 1
hist_event start fail "$(read_pid)" "健康检查超时"
return 1
}
# ---- 提交列举与选择 ----
# releases_root:定位 Releases 仓(SVC_RELEASES_DIR > ../Agent-System-Releases)。
releases_root() {
[ -n "${REL_ROOT:-}" ] && return 0
REL_ROOT="${SVC_RELEASES_DIR:-$ROOT/../Agent-System-Releases}"
if [ ! -d "$REL_ROOT/.git" ]; then
c_err "Releases 仓不存在:$REL_ROOT(可用 SVC_RELEASES_DIR 指定路径)"
exit 2
fi
}
# deployed_envs KEY:source=KEY 的环境列表("a,b"),从各环境 svc.env 汇总。
deployed_envs() {
local key="$1" d e s out=""
for d in "$RUNTIME"/*/; do
[ -f "${d}svc.env" ] || continue
e="$(basename "$d")"
valid_env "$e" || continue
s="$(sed -n 's/^source=//p' "${d}svc.env")"
[ "$s" = "$key" ] && out="${out:+$out,}$e"
done
printf '%s' "$out"
}
# print_commit_list SRC N PICK:列出提交表;PICK=1 时同时填充 PICK_OPTS(供 --pick)。
print_commit_list() {
local src="$1" count="$2" pick="$3"
local sep=$'\037'
if [ "$src" = dev ]; then
c_info "Dev 仓提交($ROOT;倒序取 $count 条)——部署:scripts/svc.sh <env> deploy --from dev@<sha>"
printf ' %3s %-9s %-10s %s\n' '#' 'sha' '日期' '主题'
local head_short; head_short="$(git -C "$ROOT" rev-parse --short HEAD)"
local n=0 h date subject marks envs
while IFS="$sep" read -r h date subject; do
[ -z "$h" ] && continue
n=$((n + 1))
marks=""
[ "$h" = "$head_short" ] && marks="${marks}[HEAD]"
envs="$(deployed_envs "dev@$h")"
[ -n "$envs" ] && marks="${marks}[已部署:$envs]"
printf ' %3d %-9s %-10s %s %s\n' "$n" "$h" "$date" "$subject" "$marks"
[ "$pick" = 1 ] && PICK_OPTS+=("$h")
done < <(git -C "$ROOT" log -n "$count" --format="%h${sep}%ad${sep}%s" --date=short)
else
c_info "Releases 仓提交($REL_ROOT;倒序取 $count 条)——部署:scripts/svc.sh <env> deploy --from release@<版本>"
printf ' %3s %-9s %-10s %-8s %-9s %s\n' '#' 'sha' '日期' '版本' 'dev提交' '主题'
local head_full; head_full="$(git -C "$REL_ROOT" rev-parse HEAD)"
local n=0 full h date subject ver dc marks envs
while IFS="$sep" read -r full h date subject; do
[ -z "$full" ] && continue
n=$((n + 1))
ver="$(git -C "$REL_ROOT" ls-tree --name-only "$full:versions" 2>/dev/null | sort -V | tail -1 || true)"
dc=""
[ -n "$ver" ] && dc="$(git -C "$REL_ROOT" show "$full:versions/$ver/manifest.json" 2>/dev/null | sed -n 's/.*"devCommit": *"\([0-9a-f]\{8,\}\)".*/\1/p' || true)"
[ -z "$ver" ] && ver="-"
marks=""
[ "$full" = "$head_full" ] && marks="${marks}[HEAD]"
envs="$(deployed_envs "release@$ver")"
[ -n "$envs" ] && marks="${marks}[已部署:$envs]"
printf ' %3d %-9s %-10s %-8s %-9s %s %s\n' "$n" "$h" "$date" "$ver" "${dc:0:8}" "$subject" "$marks"
[ "$pick" = 1 ] && PICK_OPTS+=("$ver")
done < <(git -C "$REL_ROOT" log -n "$count" --format="%H${sep}%h${sep}%ad${sep}%s" --date=short)
fi
return 0
}
cmd_commits() {
local src="${1:-dev}" count="${2:-20}"
case "$src" in
dev|release) ;;
*) c_err "用法:scripts/svc.sh commits [dev|release] [N]"; exit 2 ;;
esac
case "$count" in ''|*[!0-9]*|0) c_err "N 须为正整数"; exit 2 ;; esac
[ "$src" = release ] && releases_root
print_commit_list "$src" "$count" 0
}
# pick_commit SRC:交互式编号选择,stdout 只回显 "dev@<sha>" / "release@<版本>"
# (列表与提示走 stderr,避免被调用方的命令替换捕获);取消则退出 0。
pick_commit() {
local src="$1"
[ -t 0 ] || { c_err "--pick 需要交互终端;非交互环境请用 --from $src@<sha|版本>"; exit 2; }
PICK_OPTS=()
print_commit_list "$src" 20 1 >&2
local sel
printf '\n输入序号选择要部署的提交(1-%s,回车取消):' "${#PICK_OPTS[@]}" >&2
read -r sel || { printf '\n' >&2; c_info "已取消"; exit 0; }
case "$sel" in
''|*[!0-9]*) c_info "已取消(未选择)"; exit 0 ;;
*) [ "$sel" -ge 1 ] && [ "$sel" -le "${#PICK_OPTS[@]}" ] || { c_err "序号超范围:$sel"; exit 2; } ;;
esac
echo "$src@${PICK_OPTS[$((sel - 1))]}"
}
# ---- 部署来源解析与物化 ----
# resolve_release REF:解析 Releases 引用(空 = 最新;版本号 / 提交号 / tag),
# 设 REL_COMMIT / REL_VERSION,并顺手取 manifest 里的 devCommit 到 SRC_DEVCOMMIT。
resolve_release() {
local ref="${1:-}" commit ver
if [ -z "$ref" ]; then
commit="$(git -C "$REL_ROOT" rev-parse --verify --quiet HEAD)"
[ -n "$commit" ] || { c_err "Releases 仓无法解析 HEAD:$REL_ROOT"; exit 2; }
elif commit="$(git -C "$REL_ROOT" rev-parse --verify --quiet "${ref}^{commit}")"; then
:
elif git -C "$REL_ROOT" cat-file -e "HEAD:versions/$ref/manifest.json" 2>/dev/null; then
commit="$(git -C "$REL_ROOT" rev-parse HEAD)"
else
c_err "Releases 仓无此引用:$ref(可用版本号或提交号;先看可用项:scripts/svc.sh commits release)"
exit 2
fi
ver="$(git -C "$REL_ROOT" ls-tree --name-only "$commit:versions" 2>/dev/null | sort -V | tail -1 || true)"
[ -n "$ver" ] || { c_err "提交 $(git -C "$REL_ROOT" rev-parse --short "$commit") 下无 versions/ 快照,无法确定版本号"; exit 2; }
REL_COMMIT="$commit"; REL_VERSION="$ver"
SRC_DEVCOMMIT="$(git -C "$REL_ROOT" show "$commit:versions/$ver/manifest.json" 2>/dev/null | sed -n 's/.*"devCommit": *"\([0-9a-f]\{8,\}\)".*/\1/p' || true)"
}
# resolve_source:解析 --from / --pick → SRC_KIND(local|dev|release) 与统一标签
# SRC_LABEL(local@<sha> | dev@<sha> | release@<版本>,进 svc.env/history)。
resolve_source() {
local spec="${CLI_FROM:-local}"
if [ -n "$CLI_PICK" ]; then
[ "$CLI_PICK" = release ] && releases_root
spec="$(pick_commit "$CLI_PICK")"
fi
case "$spec" in
local)
SRC_KIND=local
SRC_LABEL="local@$(git -C "$ROOT" rev-parse --short HEAD)"
;;
dev|dev@*)
local ref="HEAD"
case "$spec" in dev@*) ref="${spec#dev@}" ;; esac
local full
full="$(git -C "$ROOT" rev-parse --verify --quiet "${ref}^{commit}")" || {
c_err "Dev 仓无此提交:$ref(若是远端新提交,先 git -C $ROOT fetch;可用项:scripts/svc.sh commits dev)"
exit 2
}
SRC_KIND=dev
SRC_FULL="$full"
SRC_SHA="$(git -C "$ROOT" rev-parse --short "$full")"
SRC_LABEL="dev@$SRC_SHA"
;;
release|release@*)
releases_root
local ref=""
case "$spec" in release@*) ref="${spec#release@}" ;; esac
resolve_release "$ref"
SRC_KIND=release
SRC_LABEL="release@$REL_VERSION"
;;
*)
c_err "无法解析 --from $spec(可用:local | dev[@<sha>] | release[@<版本|sha>])"
exit 2
;;
esac
}
# install_artifacts SRCBIN SRCPLUGINS:把构建产物复制进环境目录(环境自包含)。
install_artifacts() {
local srcbin="$1" srcplug="$2"
mkdir -p "$ENV_BIN_DIR"
rm -rf "$ENV_DIR/plugins"
cp -f "$srcbin" "$ENV_BIN" || return 1
if [ -d "$srcplug" ]; then
cp -r "$srcplug" "$ENV_DIR/plugins" || return 1
else
c_warn "[$ENV_NAME] 源插件目录缺失:$srcplug(按空插件集部署)"
mkdir -p "$ENV_DIR/plugins"
fi
c_ok "[$ENV_NAME] 产物就位:$ENV_BIN(plugins:$ENV_DIR/plugins)"
}
materialize_local() {
c_info "[$ENV_NAME] 构建当前工作区(make build)并复制到环境目录"
do_build || return 1
install_artifacts "$BIN" "$ROOT/plugins"
}
materialize_dev_commit() {
local wt="$RUNTIME/.worktrees/$SRC_SHA"
if [ -d "$wt" ]; then
c_info "[$ENV_NAME] 复用已有构建 worktree:$wt"
else
c_info "[$ENV_NAME] 物化 Dev 提交 $SRC_SHA → 隔离 worktree(不触碰当前工作区)"
git -C "$ROOT" worktree add --detach "$wt" "$SRC_FULL" >/dev/null || return 1
fi
c_info "[$ENV_NAME] 物化子模块组合(git submodule update --init --recursive;缺对象时需网络)"
git -C "$wt" submodule update --init --recursive || {
c_err "[$ENV_NAME] 子模块物化失败(对象缺失时先 git -C $ROOT fetch origin)"
return 1
}
c_info "[$ENV_NAME] 构建(make build:npm ci + go build,需网络)"
make -C "$wt" build || return 1
install_artifacts "$wt/bin/agent-system" "$wt/plugins" || return 1
c_info "[$ENV_NAME] 回收构建 worktree"
git -C "$ROOT" worktree remove --force "$wt" >/dev/null 2>&1 \
|| c_warn "[$ENV_NAME] worktree 回收失败,可手动:git -C $ROOT worktree remove --force $wt"
}
materialize_release() {
local art="$REL_ROOT/build/$REL_VERSION" bin="$REL_ROOT/build/$REL_VERSION/agent-system"
local mf="$REL_ROOT/versions/$REL_VERSION/manifest.json"
if [ ! -f "$bin" ]; then
c_err "[$ENV_NAME] Releases 仓无制品:$bin"
c_err "先在 Releases 仓构建:scripts/build-release.sh $REL_VERSION(或换版本:scripts/svc.sh commits release)"
return 1
fi
if [ -f "$mf" ]; then
local want have
want="$(sed -n 's/.*"agent-system": *"\([0-9a-f]\{64\}\)".*/\1/p' "$mf" | head -1)"
have="$(sha256sum "$bin" | cut -d' ' -f1)"
if [ -n "$want" ] && [ "$want" != "$have" ]; then
c_err "[$ENV_NAME] sha256 校验失败:制品与 manifest 不符($bin)——拒绝部署"
return 1
fi
[ -n "$want" ] && c_ok "[$ENV_NAME] sha256 校验通过(manifest:$REL_VERSION)"
else
c_warn "[$ENV_NAME] 无 manifest 可校验:$mf"
fi
[ -n "${SRC_DEVCOMMIT:-}" ] && c_info "[$ENV_NAME] 版本对应 Dev 提交:$SRC_DEVCOMMIT"
install_artifacts "$bin" "$art/plugins" || return 1
}
materialize_source() {
case "$SRC_KIND" in
local) materialize_local ;;
dev) materialize_dev_commit ;;
release) materialize_release ;;
esac
}
print_deploy_plan() {
c_info "[$ENV_NAME] 部署计划(dry-run,不执行)"
printf ' 环境:%s → %s(addr %s,data %s)\n' "$ENV_NAME" "$ENV_DIR" "$ENV_ADDR" "$ENV_DATA"
case "$SRC_KIND" in
local)
printf ' 来源:%s(构建当前工作区 make build)\n' "$SRC_LABEL"
printf ' 产物:%s ← %s + %s/plugins\n' "$ENV_BIN" "$BIN" "$ROOT" ;;
dev)
printf ' 来源:%s(完整提交 %s)\n' "$SRC_LABEL" "$SRC_FULL"
printf ' 物化:worktree %s(submodule update --init --recursive + make build)\n' "$RUNTIME/.worktrees/$SRC_SHA"
printf ' 产物:%s ← 该 worktree 构建输出\n' "$ENV_BIN" ;;
release)
printf ' 来源:%s(Releases 提交 %s)\n' "$SRC_LABEL" "$(git -C "$REL_ROOT" rev-parse --short "$REL_COMMIT")"
[ -n "${SRC_DEVCOMMIT:-}" ] && printf ' 对应 Dev 提交:%s\n' "$SRC_DEVCOMMIT"
local art="$REL_ROOT/build/$REL_VERSION/agent-system"
if [ -f "$art" ]; then
printf ' 制品:%s(存在,将做 sha256 校验)\n' "$art"
else
printf ' 制品:%s(缺失!需先在 Releases 仓构建,或换版本)\n' "$art"
fi
printf ' 产物:%s ← Releases 制品\n' "$ENV_BIN" ;;
esac
printf ' 启动:%s serve --addr %s --data %s --plugins %s%s%s\n' \
"$ENV_BIN" "$ENV_ADDR" "$ENV_DATA" "$ENV_PLUGINS" "${ENV_ARGS:+ $ENV_ARGS}" "${CLI_EXTRA:+ $CLI_EXTRA}"
}
do_deploy() {
resolve_source
if [ "$CLI_DRYRUN" = 1 ]; then
print_deploy_plan
exit 0
fi
hist_event deploy begin "$(read_pid)" "from=$SRC_LABEL"
do_stop
if ! materialize_source; then
hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"
c_err "[$ENV_NAME] 部署失败:来源物化未完成(上方日志)"
exit 1
fi
do_start || { hist_event deploy fail "$(read_pid)" "启动失败($SRC_LABEL)"; exit 1; }
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL"
}
# ---- 状态与历史 ----
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")"
local addr="$ENV_ADDR" data="$ENV_DATA" src="" a
if [ -f "$ENV_DIR/svc.env" ]; then
a="$(sed -n 's/^addr=//p' "$ENV_DIR/svc.env")"; [ -n "$a" ] && addr="$a"
a="$(sed -n 's/^data=//p' "$ENV_DIR/svc.env")"; [ -n "$a" ] && data="$a"
src="$(sed -n 's/^source=//p' "$ENV_DIR/svc.env")"
fi
if pid_alive "$pid"; then
printf ' %-5s 运行中 pid=%-7s %s 数据 %s\n' "$ENV_NAME" "$pid" "$addr" "$data"
printf ' %-10s 运行中 pid=%-7s %s 数据 %s%s\n' "$ENV_NAME" "$pid" "$addr" "$data" "${src:+ 来源 $src}"
else
printf ' %-5s 未运行 %s 数据 %s\n' "$ENV_NAME" "$addr" "$data"
printf ' %-10s 未运行 %s 数据 %s%s\n' "$ENV_NAME" "$addr" "$data" "${src:+ 上次 $src}"
fi
}
global_status() {
c_info "服务总览(root: $ROOT)"
local envs=(dev prod) e d
for d in "$RUNTIME"/*/; do
[ -d "$d" ] || continue
e="$(basename "$d")"
[ "$e" = ".worktrees" ] && continue
valid_env "$e" || continue
case " ${envs[*]} " in *" $e "*) continue ;; esac
envs+=("$e")
done
for e in "${envs[@]}"; do
ENV_NAME="$e"; env_config "$e"; env_status_line
done
}
# jfield KEY LINE:从 history.jsonl 一行里取字符串字段值。
jfield() { printf '%s' "$2" | sed -n "s/.*\"$1\": *\"\([^\"]*\)\".*/\1/p"; }
cmd_history() {
local env="${1:-}" n="${2:-20}" files=() f
case "$n" in ''|*[!0-9]*|0) c_err "N 须为正整数"; exit 2 ;; esac
if [ -n "$env" ]; then
valid_env "$env" || { c_err "非法环境名:$env"; exit 2; }
if [ -f "$RUNTIME/$env/history.jsonl" ]; then
files=("$RUNTIME/$env/history.jsonl")
else
c_info "[$env] 暂无历史(未登记过任何事件)"
exit 0
fi
else
for f in "$RUNTIME"/*/history.jsonl; do
[ -f "$f" ] && files+=("$f")
done
if [ "${#files[@]}" -eq 0 ]; then
c_info "暂无历史(任何环境都未登记过事件)"
exit 0
fi
fi
c_info "启动/部署历史($([ -n "$env" ] && echo "环境 $env" || echo "全部环境"),最近 $n 条)"
cat "${files[@]}" | sort | tail -n "$n" | while IFS= read -r l; do
[ -n "$l" ] || continue
printf ' %s %-10s %-8s %-6s %-22s %-6s %s\n' \
"$(jfield ts "$l")" "$(jfield env "$l")" "$(jfield event "$l")" \
"$(jfield result "$l")" "$(jfield source "$l")" "$(jfield pid "$l")" "$(jfield detail "$l")"
done
}
main() {
[ $# -ge 1 ] || { usage; exit 2; }
# 预扫 --env(支持 --env FILE 与 --env=FILE,可出现在任意位置):
@@ -318,11 +820,17 @@ main() {
set -- ${pre[@]+"${pre[@]}"}
[ -f "$ROOT/.svc.env" ] && load_env_file "$ROOT/.svc.env"
[ -n "$svc_envfile" ] && load_env_file "$svc_envfile"
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
case "$1" in
-h|--help|help) usage; exit 0 ;;
esac
if [ "$1" = status ] && [ $# -eq 1 ]; then
global_status
exit 0
fi
case "$1" in
commits) shift; cmd_commits "$@"; exit 0 ;;
history) shift; cmd_history "$@"; exit 0 ;;
esac
ENV_NAME="$1"; shift
# 动作词可省略(省略 = deploy);flags 可直接跟随环境名。
local action="deploy"
@@ -330,13 +838,17 @@ main() {
deploy|stop|start|restart|status|logs) action="$1"; shift ;;
esac
# logs 的行数(兼容 svc.sh dev logs 100)。
if [ "$action" = "logs" ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
if [ "$action" = logs ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
CLI_LOGS_N="$1"; shift
fi
parse_args "$@"
if { [ -n "$CLI_FROM" ] || [ -n "$CLI_PICK" ] || [ "$CLI_DRYRUN" = 1 ]; } && [ "$action" != deploy ]; then
c_err "--from/--pick/--dry-run 仅用于 deploy 动作"
exit 2
fi
env_config "$ENV_NAME"
case "$action" in
deploy) do_stop; do_build; do_start ;;
deploy) do_deploy ;;
stop) do_stop ;;
start) do_start ;;
restart) do_stop; do_start ;;