Add an rdm deploy source to svc (downstream composite binary)

Why: RDM capabilities (two-trees-one-net, rdm.* trace-audit command face)
only exist in the downstream Agent-System-RDM composite binary; svc's three
existing sources all produce the Dev binary, so none of it could be deployed
or lifecycle-managed. This adds --from rdm[@<sha>] with the same isolated
source-repo semantics as the dev source.

Evidence: make test-governance 138/138 (two new stub cases: deploy E2E and
commits/update track); real deployment rdm@2aef3fc recorded in the task card
(pins materialized, pure-Go build, web workbench to htdocs, serve health OK,
rdm.ping/rdm.inspect.capabilities committed, update --check reports latest);
make test full suite exit=0. All segments written by make evidence.

Impact: scripts/svc.sh (rdm source resolution/materialization/deploy/update/
commits, probe timeouts, rdm seed skip, ELF env-DSN evidence fallback),
docs/svc.md 5.4, tests/test_svc.py. RDM-side CLI alignment landed as RDM
PR #228; no base submodules touched.

Refs: task card .task/feature-svc-rdm-source.md; RDM PR #228/#229.
This commit is contained in:
2026-10-11 20:46:03 +00:00
parent effb4edfcd
commit 419380187d
4 changed files with 398 additions and 32 deletions
File diff suppressed because one or more lines are too long
+41 -7
View File
@@ -168,7 +168,7 @@ scripts/svc.sh ui deploy --kind static --dir ../dist # 直接服务既有
| `SVC_<NAME>_ALLOWED_HOSTS` | 逗号分隔的额外主机名,传给 `AGENT_SYSTEM_ALLOWED_HOSTS`,供反向代理的 Host / Origin 校验 |
| `--model-base/--model-key/--model-name`、`--light-*`、`--tavily-keys` | 启动健康后播种 |
| `--dry-run` | 只解析并打印部署计划(来源、物化路径、产物去向、启动参数) |
| `--pick [dev\|release]` | 交互式编号选提交(需 TTY) |
| `--pick [dev\|release\|rdm]` | 交互式编号选提交(需 TTY) |
| `--config FILE` / `--env FILE` | 配置文件(见 §9) |
| `-- <剩余参数>` | 原样透传给 `agent-system serve` |
@@ -268,7 +268,8 @@ scripts/svc.sh <agent名> config --clear # 移除(回命令行参数
## 5. 命令参考:部署来源
`deploy --from` 支持三种来源(**缺省 = `dev` 远端最新**),agent 与 static 共用:
`deploy --from` 支持四种来源(**缺省 = `dev` 远端最新**):dev/local/release 通用于
agent 与 static,rdm 仅限 agent。
### 5.1 dev(缺省)—— 远端最新 / 指定提交
@@ -315,12 +316,44 @@ dev 源的提交解析、`commits` 列举与构建 worktree 物化全部基于
仓库定位:`SVC_RELEASES_DIR` > `../Agent-System-Releases`。制品缺失时提示先在
Releases 仓跑 `scripts/build-release.sh <v>`。
### 5.4 辅助
### 5.4 rdm —— 下游组合仓 Agent-System-RDM(agent 专用)
```
--from rdm # RDM 源仓 origin/main 最新
--from rdm@<sha> # 指定提交
```
部署**下游组合二进制**(基座 + RDM 领域插件:两树一网与 `rdm.*` 追溯审计命令面)。
与 dev 源同构的独立源仓机制:默认 `~/.cache/agent-system-svc/rdm-src`
(`SVC_RDM_SRC_DIR` 覆盖路径;克隆来源 `SVC_RDM_ORIGIN` 覆盖,缺省为 RDM 远端 ssh
地址),流程 = fetch → `worktree add --detach runtime/.worktrees/rdm-<sha>` →
`git submodule update --init --recursive`(物化 `pins/` 基座钉入组合)→ `make build`
(纯 Go,无需 npm)→ `bin/agent-system-rdm` 复制为环境二进制 → worktree 回收。
与基座部署的差异:
- **插件目录按空集安装**:RDM 组合插件编译进二进制(`extension.RegisterCompiled`),
无声明式插件资产;管理页「插件管理」列表只反映目录插件,组合插件经
`/api/registry` 与命令面可见;
- **web/ 工作台**:RDM 自带零构建静态工作台(直连后端 API),deploy 时复制到
`runtime/<name>/htdocs/`,用伴生 static 服务伺服
(`scripts/svc.sh <name>web deploy --kind static --dir runtime/<name>/htdocs`;
注意该目录随本环境 `rm --purge` 一并删除);
- **启动兼容**:组合二进制容忍基座风格的首位 `serve` 子命令并提供 `version`
子命令;svc 的 `version`/`serve -h` 探测带 timeout 护栏;
- **不支持** `--mock` 与 `--config`(组合 CLI 无此旗标);模型设置播种
(`--model-*`/`--tavily-keys`)仅基座产物支持,rdm 源跳过(登记态
`source=rdm@*` 即识别形态,跨裸 start/restart 稳定);
- **PG 档**:`db <pg实例>` 绑定同样走 `AGENT_SYSTEM_DSN` 环境交付;RDM CLI 在
`-dsn` 为空时回退读该环境变量(无 `config explain` 子命令时,svc 以 ELF 内静态
引用作能力证据)。
### 5.5 辅助
```bash
scripts/svc.sh commits [dev|release] [N] # 可部署提交表(默认 20 条)
scripts/svc.sh <name> deploy --pick # 交互式编号选择
scripts/svc.sh <name> deploy --dry-run # 只打印计划
scripts/svc.sh commits [dev|release|rdm] [N] # 可部署提交表(默认 20 条)
scripts/svc.sh <name> deploy --pick # 交互式编号选择
scripts/svc.sh <name> deploy --dry-run # 只打印计划
```
`commits` 表格:dev 源标注 `[最新]`(源仓 origin/main 顶)与 `[已部署:<服务名>]`
@@ -422,6 +455,7 @@ scripts/svc.sh update [name...] [--check] [--all] [--no-fetch]
|---|---|
| `local@<sha>` / `dev@<sha>` | 一律 `deploy --from dev@<远端 main 最新>`(独立部署源仓 + 隔离 worktree 构建;不 pull 主检出、不看工作树状态) |
| `release@<版本>` | `deploy --from release@<Releases 最新版本>`(sha256 校验) |
| `rdm@<sha>` | `deploy --from rdm@<RDM 远端 main 最新>`(组合二进制;见 §5.4) |
任一服务失败不中断其余服务;最终汇总 `成功/失败/跳过`。退出码:0 全部成功或最新;
1 有失败;2 用法错误。
@@ -615,7 +649,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价
| `pid` | 最近启动 pid |
| `addr` | 监听地址(持久化,下次 start 复用) |
| `tags` | 标签(逗号分隔) |
| `source` | 部署来源:`local@<sha>` / `dev@<sha>` / `release@<版本>` / `dir@<路径>` / `postgres@local` / `clone@<名>` |
| `source` | 部署来源:`local@<sha>` / `dev@<sha>` / `release@<版本>` / `rdm@<sha>` / `dir@<路径>` / `postgres@local` / `clone@<名>` |
| `version` | 产物版本串(`agent-system version` 输出) |
| `started` | 最近启动时间 |
| `pidcheck` | 进程判定标记(见 §6.1) |
+186 -17
View File
@@ -20,7 +20,7 @@
# scripts/svc.sh db copy <src> <dst> [--db X] [--force] 跨实例搬库(双方须运行)
# scripts/svc.sh db ls <name> 实例内数据库列表
# scripts/svc.sh db set <name> [...] 等价于 set
# scripts/svc.sh commits [dev|release] [N] 可部署提交列表(默认 dev,N=20)
# scripts/svc.sh commits [dev|release|rdm] [N] 可部署提交列表(默认 dev,N=20)
# scripts/svc.sh history [name] [N] 事件历史(默认全部,最近 20 条)
# scripts/svc.sh update [name] [--check] [--all] 检测远端最新并更新过时服务(与当前工作区状态无关)
# scripts/svc.sh help [主题] 分主题详细帮助
@@ -32,7 +32,10 @@
# 隔离 worktree(runtime/.worktrees/<sha>)进行,不触碰当前工作区
# --from local 构建当前工作区(唯一写主检出构建产物的路径,均 gitignore)
# --from release[@<版本|sha>] Releases 仓制品(sha256 对 versions/<v>/manifest.json 校验)
# --pick [dev|release] 交互式编号选提交;--dry-run 只打印部署计划
# --from rdm[@<sha>] 下游组合仓 Agent-System-RDM(独立 RDM 源仓物化 pins/ 后
# make build 出 agent-system-rdm 组合二进制;web/ 工作台静态
# 资产随环境落 htdocs/,供伴生 static 服务 --dir 使用)
# --pick [dev|release|rdm] 交互式编号选提交;--dry-run 只打印部署计划
#
# ════════════════════ 常用参数(服务级)════════════════════
# --config FILE / --env FILE dotenv 配置文件(KEY=VALUE;--env 为兼容别名)
@@ -119,6 +122,10 @@ SVC_ENVFILE=""
# dev 源的解析/列举/worktree 物化全部基于它——更新与部署因此不依赖当前工作区状态
# (脏树、任意分支均可);目录可随时删除,下次使用自动重建。
SRC_DIR="${SVC_SRC_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/agent-system-svc/src}"
# 独立 RDM 源仓(下游组合仓 Agent-System-RDM 的干净克隆;SVC_RDM_SRC_DIR 覆盖路径,
# SVC_RDM_ORIGIN 覆盖克隆来源)。rdm 源的解析/列举/物化全部基于它,语义与 SRC_DIR 一致。
RDM_SRC_DIR="${SVC_RDM_SRC_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/agent-system-svc/rdm-src}"
RDM_ORIGIN_DEFAULT="ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-RDM.git"
# 提示信息走 stderr:stdout 只留给数据(提交表/服务表/DSN),便于 $( ) 捕获组合。
c_info() { printf '\033[1;34m[svc]\033[0m %s\n' "$*" >&2; }
@@ -252,7 +259,16 @@ EOF
依赖当前工作区状态的来源。
release[@<版本|sha>] 取 Releases 仓 build/<v>/agent-system(+plugins),
sha256 对 versions/<v>/manifest.json 校验,不符即拒绝。
--pick [dev|release] 交互式编号选择(需 TTY;列表与提示走 stderr)
rdm[@<sha>] 部署下游组合仓 Agent-System-RDM:独立 RDM 源仓
(SVC_RDM_SRC_DIR,默认 ~/.cache/agent-system-svc/rdm-src,
首次按 SVC_RDM_ORIGIN——缺省 RDM 远端——克隆)worktree 物化
pins/ 子模块后 make build(纯 Go,无需 npm),产物
bin/agent-system-rdm 复制为环境二进制;插件目录按空集安装
(组合插件编译进二进制),web/ 工作台静态资产随环境落
htdocs/(伴生 static 服务用 --dir 指向该目录)。
注意:rdm 源不支持 --mock 与 --config;模型设置播种
(--model-*/--tavily-keys)仅基座产物支持。
--pick [dev|release|rdm] 交互式编号选择(需 TTY;列表与提示走 stderr)
--dry-run 只打印部署计划不执行
行为细节:
@@ -566,7 +582,7 @@ parse_args() {
--seed-owner) need_value "$@"; CLI_SEED_OWNER="$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 ;;
case "${2:-}" in dev|release|rdm) CLI_PICK="$2"; shift 2 ;; *) shift ;; esac ;;
--dry-run) CLI_DRYRUN=1; shift ;;
--kind) need_value "$@"; CLI_KIND="$2"; shift 2 ;;
--dir) need_value "$@"; CLI_DIR="$2"; shift 2 ;;
@@ -867,7 +883,8 @@ agent_has_explicit_cfg() {
# 旧实现 grep -- '--dsn' 永不命中,把现役二进制的 --dsn/--auth 误当不支持而丢弃)。
adjust_args_for_bin() {
local bin="$1" args="$2" out
out="$("$bin" serve -h 2>&1 || true)"
# timeout 护栏:组合二进制若把 serve 当位置参数忽略而直接进入服务态,探测会挂死。
out="$(timeout 5 "$bin" serve -h 2>&1 || true)"
local filtered="" w drop_next=0 flag
for w in $args; do
case "$w" in
@@ -924,6 +941,29 @@ svc_src_ready() {
return 0
}
# rdm_src_ready:确保独立 RDM 源仓就绪(缺省从 RDM 远端克隆,SVC_RDM_ORIGIN 覆盖来源、
# SVC_RDM_SRC_DIR 覆盖路径)并 fetch;--no-fetch 跳过网络。与 svc_src_ready 同构,
# 面向下游组合仓 Agent-System-RDM(构建组合二进制 agent-system-rdm)。
RDM_SRC_READY_DONE=0
rdm_src_ready() {
[ "$RDM_SRC_READY_DONE" = 1 ] && return 0
if [ ! -d "$RDM_SRC_DIR/.git" ]; then
local url="${SVC_RDM_ORIGIN:-$RDM_ORIGIN_DEFAULT}"
c_info "初始化独立 RDM 源仓:$RDM_SRC_DIR(← $url;与当前工作区无关,可随时删除重建)"
mkdir -p "$(dirname "$RDM_SRC_DIR")"
if ! git clone --quiet "$url" "$RDM_SRC_DIR" >&2; then
c_err "RDM 源仓克隆失败:$url → $RDM_SRC_DIR(可用 SVC_RDM_ORIGIN 指定克隆来源)"
return 1
fi
fi
if [ "${1:-}" != "--no-fetch" ]; then
c_info "git fetch(独立 RDM 源仓:$RDM_SRC_DIR)"
git -C "$RDM_SRC_DIR" fetch origin --quiet 2>/dev/null || c_warn "RDM 源仓 fetch 失败——基于本地缓存($RDM_SRC_DIR)"
fi
RDM_SRC_READY_DONE=1
return 0
}
deployed_envs() {
local key="$1" d e s out=""
for d in "$RUNTIME"/*/; do
@@ -955,6 +995,22 @@ print_commit_list() { # SRC N PICK:提交表到 stdout;PICK=1 时填充 PICK
printf ' %3d %-9s %-10s %s %s\n' "$n" "$h" "$date" "$subject" "$marks"
[ "$pick" = 1 ] && PICK_OPTS+=("$h")
done < <(git -C "$SRC_DIR" log -n "$count" --format="%h${sep}%ad${sep}%s" --date=short origin/main)
elif [ "$src" = rdm ]; then
rdm_src_ready || { c_err "RDM 源仓不可用,无法列出可部署提交"; return 1; }
c_info "RDM 仓提交(独立 RDM 源仓 $RDM_SRC_DIR 的 origin/main;倒序取 $count 条)——部署:scripts/svc.sh <name> deploy --from rdm@<sha>"
printf ' %3s %-9s %-10s %s\n' '#' 'sha' '日期' '主题'
local latest_short; latest_short="$(git -C "$RDM_SRC_DIR" rev-parse --short origin/main 2>/dev/null || true)"
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" = "$latest_short" ] && marks="${marks}[最新]"
envs="$(deployed_envs "rdm@$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 "$RDM_SRC_DIR" log -n "$count" --format="%h${sep}%ad${sep}%s" --date=short origin/main)
else
c_info "Releases 仓提交($REL_ROOT;倒序取 $count 条)——部署:scripts/svc.sh <name> deploy --from release@<版本>"
printf ' %3s %-9s %-10s %-8s %-9s %s\n' '#' 'sha' '日期' '版本' 'dev提交' '主题'
@@ -981,8 +1037,8 @@ print_commit_list() { # SRC N PICK:提交表到 stdout;PICK=1 时填充 PICK
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 ;;
dev|release|rdm) ;;
*) c_err "用法:scripts/svc.sh commits [dev|release|rdm] [N]"; exit 2 ;;
esac
case "$count" in ''|*[!0-9]*|0) c_err "N 须为正整数"; exit 2 ;; esac
[ "$src" = release ] && releases_root
@@ -1059,8 +1115,23 @@ resolve_source() { # → SRC_KINDSRC(local|dev|release) + SRC_LABEL(+SRC_FULL/
SRC_KINDSRC=release
SRC_LABEL="release@$REL_VERSION"
;;
rdm|rdm@*)
# 缺省 ref=origin/main(独立 RDM 源仓远端最新);显式 @<sha> 亦可。
local ref="origin/main"
case "$spec" in rdm@*) ref="${spec#rdm@}" ;; esac
rdm_src_ready || { c_err "独立 RDM 源仓不可用——rdm 源依赖它(可用 SVC_RDM_ORIGIN 指定克隆来源、SVC_RDM_SRC_DIR 指定路径)"; exit 2; }
local full
full="$(git -C "$RDM_SRC_DIR" rev-parse --verify --quiet "${ref}^{commit}")" || {
c_err "RDM 源仓无此提交:$ref(已自动 fetch;可用项:scripts/svc.sh commits rdm)"
exit 2
}
SRC_KINDSRC=rdm
SRC_FULL="$full"
SRC_SHA="$(git -C "$RDM_SRC_DIR" rev-parse --short "$full")"
SRC_LABEL="rdm@$SRC_SHA"
;;
*)
c_err "无法解析 --from $spec(可用:local | dev[@<sha>] | release[@<版本|sha>])"
c_err "无法解析 --from $spec(可用:local | dev[@<sha>] | release[@<版本|sha>] | rdm[@<sha>])"
exit 2
;;
esac
@@ -1102,12 +1173,39 @@ materialize_dev_commit() {
echo "$wt"
}
# recycle_worktree SHA:回收构建 worktree(尽力而为)。
# recycle_worktree SHA [REPO]:回收构建 worktree(尽力而为)。REPO 缺省为 Dev 源仓。
recycle_worktree() {
local repo="${2:-$SRC_DIR}"
local wt="$RUNTIME/.worktrees/$1"
c_info "[$ENV_NAME] 回收构建 worktree"
git -C "$SRC_DIR" worktree remove --force "$wt" >/dev/null 2>&1 \
|| c_warn "[$ENV_NAME] worktree 回收失败,可手动:git -C $SRC_DIR worktree remove --force $wt"
git -C "$repo" worktree remove --force "$wt" >/dev/null 2>&1 \
|| c_warn "[$ENV_NAME] worktree 回收失败,可手动:git -C $repo worktree remove --force $wt"
}
# materialize_rdm_commit FULL SHA:RDM 源的隔离 worktree 物化+构建;stdout 只回显
# worktree 路径。构建 = make build(纯 Go:pins/ 子模块经 go.mod replace 就位后无需
# npm 与网络);产物固定为 bin/agent-system-rdm(缺失即失败,绝不装半成品)。
materialize_rdm_commit() {
local full="$1" sha="$2"
local wt="$RUNTIME/.worktrees/rdm-$sha"
if [ -d "$wt" ]; then
c_info "[$ENV_NAME] 复用已有构建 worktree:$wt"
else
c_info "[$ENV_NAME] 物化 RDM 提交 $sha → 隔离 worktree(源:独立 RDM 源仓 $RDM_SRC_DIR,不触碰当前工作区)"
git -C "$RDM_SRC_DIR" worktree add --detach "$wt" "$full" >/dev/null || return 1
fi
c_info "[$ENV_NAME] 物化基座 pins(git submodule update --init --recursive;缺对象时需网络)"
git -C "$wt" submodule update --init --recursive >&2 || {
c_err "[$ENV_NAME] pins 子模块物化失败(网络或对象缺失;重试会自动 fetch RDM 源仓)"
return 1
}
c_info "[$ENV_NAME] 构建(make build:纯 Go,无需 npm)"
make -C "$wt" build >&2 || return 1
[ -x "$wt/bin/agent-system-rdm" ] || {
c_err "[$ENV_NAME] 构建产物缺失:$wt/bin/agent-system-rdm(RDM Makefile 的 build 目标应产出该名称)"
return 1
}
echo "$wt"
}
materialize_release() {
@@ -1165,6 +1263,25 @@ agent_do_deploy() {
release)
materialize_release || { hist_event deploy fail "$(read_pid)" "物化/校验失败($SRC_LABEL)"; return 1; }
;;
rdm)
wt="$(materialize_rdm_commit "$SRC_FULL" "$SRC_SHA")" \
|| { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; }
# RDM 组合插件编译进二进制(provides 无声明式资产)——插件目录按空集安装。
mkdir -p "$wt/.svc-empty-plugins"
install_artifacts "$wt/bin/agent-system-rdm" "$wt/.svc-empty-plugins" \
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
# RDM 自带零构建工作台(web/,直连后端 API 的静态资产)随环境落 htdocs/,
# 供伴生 static 服务直接 --dir 使用(注意:随本环境 rm --purge 一并删除)。
if [ -d "$wt/web" ]; then
rm -rf "$ENV_DIR/htdocs"
if cp -r "$wt/web" "$ENV_DIR/htdocs"; then
c_ok "[$ENV_NAME] RDM 工作台就位:$ENV_DIR/htdocs(伴生服务:scripts/svc.sh ${ENV_NAME}web deploy --kind static --dir $ENV_DIR/htdocs)"
else
c_warn "[$ENV_NAME] RDM web/ 工作台复制失败(不影响 API 服务启动)"
fi
fi
recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR"
;;
esac
# 新产物承接能力复核(环境 DSN 交付):不支持即失败——绑定保持可恢复,不降级启动。
if [ "$DB_MODE" = pg ] && [ "$DB_SOURCE" != conffile ] && ! bin_env_dsn_supported "$ENV_BIN"; then
@@ -1194,6 +1311,11 @@ print_agent_plan() {
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 ;;
rdm)
printf ' 来源:%s(完整提交 %s;独立 RDM 源仓 %s)\n' "$SRC_LABEL" "$SRC_FULL" "$RDM_SRC_DIR"
printf ' 物化:worktree %s(pins submodule update --init --recursive + make build)\n' "$RUNTIME/.worktrees/rdm-$SRC_SHA"
printf ' 产物:%s ← bin/agent-system-rdm + 空插件目录(组合插件编译进二进制)\n' "$ENV_BIN"
printf ' 工作台:web/ → %s/htdocs(伴生 static:--kind static --dir 该目录)\n' "$ENV_DIR" ;;
esac
return 0
}
@@ -1535,6 +1657,13 @@ bin_env_dsn_supported() {
local out
out="$(AGENT_SYSTEM_DSN='postgresql://svcprobe:svcprobe@127.0.0.1:9/svcprobe' "$1" config explain 2>/dev/null || true)"
case "$out" in *svcprobe*) return 0 ;; esac
# 无 config explain 子命令的组合二进制(下游仓产物):静态证据兜底——ELF 二进制内
# 引用 AGENT_SYSTEM_DSN 即视为消费环境层 DSN(下游经基座 extension.Main 的 env
# 回退生效)。仅对 ELF 生效:脚本文本必然含该字符串字样,会把「不支持」误判为
# 「支持」(回归教训:test_svc 的 legacy 桩即脚本形态)。
[ "$(head -c 4 "$1" 2>/dev/null)" = $'\x7fELF' ] || return 1
command -v strings >/dev/null 2>&1 || return 1
strings "$1" 2>/dev/null | grep -q 'AGENT_SYSTEM_DSN' && return 0
return 1
}
@@ -1563,10 +1692,11 @@ agent_do_start() {
check_port_free || return 1
if [ ! -x "$ENV_BIN" ]; then
c_err "[$ENV_NAME] 环境产物缺失:$ENV_BIN"
c_err "请先部署:scripts/svc.sh $ENV_NAME deploy [--from local|dev[@<sha>]|release[@<版本>]]"
c_err "请先部署:scripts/svc.sh $ENV_NAME deploy [--from local|dev[@<sha>]|release[@<版本>]|rdm[@<sha>]]"
return 1
fi
SVC_VERSION="$("$ENV_BIN" version 2>/dev/null || true)"
# timeout 护栏:组合二进制若不识别子命令可能直接进入服务态阻塞探测(旧产物形态)。
SVC_VERSION="$(timeout 5 "$ENV_BIN" version 2>/dev/null || true)"
# 统一解析数据库配置(复用与重建两路共用;失败拒绝启动,绝不静默回退 SQLite)。
# 2026-10-11 修复:复用 exec= 分支此前不查 dbref——G1-7 迁移把 --dsn 剥入环境后
# record_state 写回不含 DSN 的 exec=,重新绑定后的第二次裸 restart 即丢库。
@@ -1661,7 +1791,12 @@ agent_do_start() {
record_state
c_ok "[$ENV_NAME] 已启动 pid=$(read_pid) → http://$phost:${probe##*:}(来源 ${SVC_SOURCE:-?},日志:$ENV_DIR/svc.log)"
hist_event start ok "$(read_pid)" "http://$phost:${probe##*:}"
seed_settings "$phost" "${probe##*:}"
# rdm 组合产物无 user/apikey CLI 子命令:设置播种(--model-*/--tavily-keys)仅
# 基座产物支持;记录态 svc.env 的 source=rdm@* 跨裸 start/restart 稳定标识形态。
case "${SVC_SOURCE:-}" in
rdm@*) ;;
*) seed_settings "$phost" "${probe##*:}" ;;
esac
return 0
fi
pid_alive "$(read_pid)" || {
@@ -1709,6 +1844,12 @@ static_do_deploy() {
hist_event deploy fail "$(read_pid)" "release 源不支持 static"
return 1
;;
rdm)
c_err "[$ENV_NAME] rdm 源不支持 static 构建(RDM 工作台为 agent 环境内 htdocs/ 的零构建资产)"
c_err " 用法:先 <agent环境> deploy --from rdm,再 scripts/svc.sh <name> deploy --kind static --dir <agent环境>/htdocs"
hist_event deploy fail "$(read_pid)" "rdm 源不支持 static"
return 1
;;
esac
[ -d "$src_dist" ] || { c_err "[$ENV_NAME] 前端产物缺失:$src_dist"; hist_event deploy fail "$(read_pid)" "dist 缺失"; return 1; }
mkdir -p "$ENV_DIR"
@@ -3231,11 +3372,12 @@ cmd_update() {
fi
[ "${#targets[@]}" -gt 0 ] || { c_info "暂无已登记服务(db create / deploy 会登记)"; return 0; }
# 按目标来源决定访问哪些仓:local@/dev@ 走独立部署源仓;release@ 走 Releases 仓。
local latest_sha="" latest_rel="" need_rel=0 need_dev=0 e_src
# 按目标来源决定访问哪些仓:local@/dev@ 走独立部署源仓;release@ 走 Releases 仓;
# rdm@ 走独立 RDM 源仓。
local latest_sha="" latest_rel="" latest_rdm="" need_rel=0 need_dev=0 need_rdm=0 e_src
for e in ${targets[@]+"${targets[@]}"}; do
e_src="$(sed -n 's/^source=//p' "$RUNTIME/$e/svc.env" | head -1)"
case "$e_src" in release@*) need_rel=1 ;; local@*|dev@*) need_dev=1 ;; esac
case "$e_src" in release@*) need_rel=1 ;; local@*|dev@*) need_dev=1 ;; rdm@*) need_rdm=1 ;; esac
done
local fetch_note="" src_args=""
[ "$CLI_NOFETCH" = 1 ] && { fetch_note="--no-fetch,基于本地缓存引用"; src_args="--no-fetch"; }
@@ -3257,6 +3399,13 @@ cmd_update() {
latest_rel="$(git -C "$REL_ROOT" ls-tree --name-only "$rel_head:versions" 2>/dev/null | sort -V | tail -1 || true)"
fi
fi
if [ "$need_rdm" = 1 ]; then
if rdm_src_ready $src_args; then
latest_rdm="$(git -C "$RDM_SRC_DIR" rev-parse --verify --quiet origin/main || true)"
else
c_warn "独立 RDM 源仓不可用——rdm 源服务本轮无法检测与更新"
fi
fi
# 逐服务分析:状态判定 + 收集可更新动作。
c_info "更新检测($fetch_note;root: $ROOT)"
@@ -3313,6 +3462,25 @@ cmd_update() {
fi
fi
;;
rdm@*)
cur="${src##*@}"
if [ -z "$latest_rdm" ]; then
state="RDM 远端 main 未知"; latest_disp="-"
elif ! git -C "$RDM_SRC_DIR" cat-file -e "${cur}^{commit}" 2>/dev/null; then
state="提交不在 RDM 远端(不自动更新;重新 deploy 可纳入更新轨道)"
latest_disp="$(git -C "$RDM_SRC_DIR" rev-parse --short "$latest_rdm" 2>/dev/null || echo "-")"
elif [ "${latest_rdm:0:${#cur}}" = "$cur" ]; then
state="✓ 最新"; latest_disp="$(git -C "$RDM_SRC_DIR" rev-parse --short "$latest_rdm" 2>/dev/null || echo "-")"
elif git -C "$RDM_SRC_DIR" merge-base --is-ancestor "$cur" "$latest_rdm" 2>/dev/null; then
to="$(git -C "$RDM_SRC_DIR" rev-parse --short "$latest_rdm" 2>/dev/null || true)"
latest_disp="$to"
state="可更新"
elif git -C "$RDM_SRC_DIR" merge-base --is-ancestor "$latest_rdm" "$cur" 2>/dev/null; then
state="领先 RDM 远端 main(不动作)"; latest_disp="$(git -C "$RDM_SRC_DIR" rev-parse --short "$latest_rdm" 2>/dev/null || echo "-")"
else
state="与 RDM 远端 main 分叉(不自动更新)"; latest_disp="-"
fi
;;
*)
state="来源未知(重新 deploy 后可参与更新)" ;;
esac
@@ -3346,6 +3514,7 @@ cmd_update() {
case "$from" in
local@*|dev@*) deploy_from="dev@$tov" ;;
release@*) deploy_from="release@$tov" ;;
rdm@*) deploy_from="rdm@$tov" ;;
esac
c_info "[$name] 更新:$from → $tov($deploy_from)"
hist_event update begin "$(read_pid)" "from=$from to=$tov"
+73
View File
@@ -669,5 +669,78 @@ class SvcDbRecoveryTests(unittest.TestCase):
self.assertNotIn("apikey", self.cli_record()) # 认证关闭不铸 key
# ---- 13. rdm 部署源(下游组合仓 Agent-System-RDM):物化/产物/工作台/播种跳过 ----
def make_rdm_repo(self):
"""构造下游组合仓夹具:Makefile build 产出桩 agent-system-rdm + web/ 工作台。"""
remote = self.root / "rdmremote.git"
work = self.root / "rdmwork"
run = lambda cwd, *args: subprocess.run(
["git", *args], cwd=cwd, env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
check=True)
run(self.root, "init", "--bare", "--initial-branch=main", str(remote))
run(self.root, "init", "--initial-branch=main", str(work))
(work / "Makefile").write_text(
"build:\n\tmkdir -p bin\n\tcp %s bin/agent-system-rdm\n" % self.stub)
(work / "web").mkdir()
(work / "web" / "index.html").write_text("<html>rdm workbench</html>\n")
run(work, "add", ".")
run(work, "commit", "-m", "rdm v1")
run(work, "remote", "add", "origin", str(remote))
run(work, "push", "-u", "origin", "main")
return remote, work
def rdm_env(self, remote):
env = dict(self.env)
env.update({
"SVC_RDM_SRC_DIR": str(self.root / "rdmcache"),
"SVC_RDM_ORIGIN": str(remote),
"SVC_MODEL_BASE": "https://stub.example/v1",
"SVC_MODEL_KEY": "sk-stub-model",
"SVC_MODEL_NAME": "stub-model",
})
return env
def test_rdm_source_deploy_end_to_end(self):
remote, work = self.make_rdm_repo()
env = self.rdm_env(remote)
result = self.svc("rdm1", "deploy", "--from", "rdm", "--port", "18841", env=env)
d = self.runtime("rdm1")
# 组合二进制复制改名落位、插件目录空集、工作台静态资产落 htdocs/。
self.assertTrue((d / "bin" / "agent-system").exists())
self.assertTrue((d / "bin" / "agent-system").stat().st_mode & 0o111)
self.assertEqual(self.kv("rdm1", "kind"), "agent")
self.assertTrue(self.kv("rdm1", "source").startswith("rdm@"))
self.assertEqual(list((d / "plugins").iterdir()), [])
self.assertIn("rdm workbench", (d / "htdocs" / "index.html").read_text())
self.assertIn("args:", self.serve_record()) # 健康通过即登记启动参数
# rdm 形态跳过设置播种(无 user/apikey 子命令):给了模型键也不产生 PUT/CLI。
self.assertFalse((self.trace / "put.txt").exists())
self.assertNotIn("user list", self.cli_record())
def test_rdm_source_commits_and_update_track(self):
remote, work = self.make_rdm_repo()
env = self.rdm_env(remote)
# commits rdm 列出来自独立 RDM 源仓的可部署提交。
result = self.svc("commits", "rdm", env=env)
self.assertIn("RDM 仓提交", result.stdout)
self.assertIn("rdm v1", result.stdout)
# 部署 v1 → 远端推进 v2 → update 检测可更新并真更新(source 保持 rdm@ 轨道)。
self.svc("rdm1", "deploy", "--from", "rdm", "--port", "18842", env=env)
old = self.kv("rdm1", "source")
(work / "marker").write_text("v2\n")
subprocess.run(["git", "add", "."], cwd=work, env=self.env, timeout=60, check=True)
subprocess.run(["git", "commit", "-m", "rdm v2"], cwd=work, env=self.env,
timeout=60, check=True)
subprocess.run(["git", "push"], cwd=work, env=self.env, timeout=60, check=True)
self.svc("update", "rdm1", "--check", env=env)
self.svc("update", "rdm1", env=env)
new = self.kv("rdm1", "source")
self.assertTrue(new.startswith("rdm@"))
self.assertNotEqual(new, old)
self.assertIn("args:", self.serve_record()) # 更新后健康在跑
if __name__ == "__main__":
unittest.main()