From d1aebd991906106384a8e508e051586395b10a92 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 4 Oct 2026 13:23:33 +0000 Subject: [PATCH 1/6] Record the task card for feature-svc-db-bind --- .task/feature-svc-db-bind.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .task/feature-svc-db-bind.md diff --git a/.task/feature-svc-db-bind.md b/.task/feature-svc-db-bind.md new file mode 100644 index 00000000..43f46546 --- /dev/null +++ b/.task/feature-svc-db-bind.md @@ -0,0 +1,25 @@ +# feature/svc-db-bind 任务卡 + +任务主记录:;状态:进行中;owner:weichen + +## 基线与授权 + +- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 3836ea88e3954878f257b7708ae5ccef947ccd79 +- 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-db-bind(worktree);分支:feature/svc-db-bind +- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> + +## 目标与非目标 + +<预期结果、受影响入口/模块/契约;明确不做什么。> + +## 同步记录 + +| 节点 | sync_base_sha | tested_head_sha | checked_at | +|---|---|---|---| +| 启动 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | 2026-10-04T13:23:33Z | + +## 结果与验证(完成后填写) + +- 结果: +- 验证: +- 影响与剩余项: From 843495e0d0618b900d727e38570ff61451068c9b Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 4 Oct 2026 13:30:50 +0000 Subject: [PATCH 2/6] Add per-service database binding and fix single-dash flag detection in svc.sh Why: info showed services and postgres instances but no service-to-database correspondence, and switching an agent service to another database required redeploying with a hand-built DSN. While wiring this up, adjust_args_for_bin was found matching Go flag help with '--dsn' although Go prints '-dsn', so deploy-built --dsn/--auth args were silently dropped on current binaries. Evidence: full E2E in the task worktree with a real agent binary and a real PostgreSQL 18 instance -- 't-app db bpg' persisted exec=/dbref, auto-restarted (dbref survives record_state rewrites), and the agent created 21 tables in the bound database; 'db bpg --db t2' auto-created and switched databases (21 tables in t2); '--clear' returned the service to embedded SQLite; --no-restart and error paths behave; list gains a DB column, status a database line, info a reverse bound-services line; legacy deployments without dbref resolve by DSN host:port; single-dash detection proven with positive and negative cases; regressions list/db dsn/db ls/info/history/--help pass; make test exit 0. Full log in the task card. Impact: new service-level action 'svc.sh db [pg [--db X] | --clear] [--no-restart]' for agent services (bind/switch/unbind/view), DB column in list, binding lines in status/info, dbref persisted in svc.env, auto database creation when the instance runs, and the adjust_args_for_bin fix restoring deploy --dsn/--auth on current binaries. No agent binary changes. Refs: task card .task/feature-svc-db-bind.md (same doc as the PR); docs/svc.md sections 4.5/6.1/6.2/10.1 --- .task/feature-svc-db-bind.md | 38 +++++- docs/svc.md | 48 ++++++-- scripts/svc.sh | 229 +++++++++++++++++++++++++++++++++-- 3 files changed, 290 insertions(+), 25 deletions(-) diff --git a/.task/feature-svc-db-bind.md b/.task/feature-svc-db-bind.md index 43f46546..44c6d853 100644 --- a/.task/feature-svc-db-bind.md +++ b/.task/feature-svc-db-bind.md @@ -6,11 +6,25 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 3836ea88e3954878f257b7708ae5ccef947ccd79 - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-db-bind(worktree);分支:feature/svc-db-bind -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:实现"服务↔数据库绑定"(svc.sh 服务级 db 子命令 + info/list 显示绑定)并修复 + adjust_args_for_bin 单横线误判;终点 = PR 合入 main(附 precheck / task-sync / 测试证据) ## 目标与非目标 -<预期结果、受影响入口/模块/契约;明确不做什么。> +目标: +- agent 服务可绑定/切换/解除数据库:`svc.sh db [pg名 [--db X] | --clear] [--no-restart]`; + 缺省查看当前绑定与可用实例。绑定 = DSN 写入持久化 exec= + dbref=/<库> 登记,实例运行中 + 缺库自动建,服务运行中自动重启生效;--clear 回内嵌 SQLite 档。 +- 显示层全链路:list 表新增 DB 列;status 详情新增"数据库"行;info 数据库节新增"绑定服务" + 反向映射;无 dbref 的旧服务按 DSN host:port 反查匹配实例(外部 DSN 显示 外部@host:port)。 +- 修复既有缺陷:adjust_args_for_bin 以 grep '--dsn' 检测旗标支持,Go flag 帮助打印单横线 + `-dsn`,导致现役二进制的 --dsn/--auth 在 deploy 重建参数时被误删——改为"去横线旗标名 + + 1-2 横线"匹配。 +- record_state 跨重启保留 dbref;svc_del_key 辅助;db create 尾部提示绑定命令; + usage/usage_short/help(lifecycle/db)/docs/svc.md 同步。 + +非目标:不改 agent 二进制与部署/生命周期语义;不做 static/postgres 的绑定;不做运行中热切换 +(切换经重启生效)。 ## 同步记录 @@ -20,6 +34,20 @@ ## 结果与验证(完成后填写) -- 结果: -- 验证: -- 影响与剩余项: +- 结果:已实现(上列目标全部落地;` db` 服务级动作与顶层 `db` 命令并存无冲突)。 +- 验证(worktree runtime 实测,真实 agent 二进制 + PostgreSQL 18,2026-10-04): + - `bash -n` 通过; + - 绑定:`t-app db bpg` → exec= 写入 --dsn、dbref=bpg/agent、自动重启生效;**record_state + 重写 svc.env 后 dbref 保留**;PG 的 agent 库出现 **21 张表**(服务真实落库,非仅配置); + - 切换:`t-app db bpg --db t2` → 自动建库 t2、重启、t2 库 21 张表; + - 解绑:`db --clear` → dbref/--dsn 移除、重启回内嵌档; + - `--no-restart` 只改登记;错误路径(未登记实例/多余参数/postgres 服务)均 exit 2 且提示正确; + - 显示层:list DB 列(t-app→bpg/agent)、`t-app db` 查看(掩码 DSN+实例列表)、info 数据库节 + "绑定服务 t-app(agent)";手删 dbref 模拟旧部署 → list 按 DSN host:port 反查仍显示 bpg/agent; + - adjust 修复:正例(帮助含 `-dsn` 判支持)与负例(无 dsn 判不支持)均正确; + - 回归:db dsn(掩码)/ db ls / info / history(新增 `set ok db bind → bpg/agent` 事件)/ + --help 正常;测试现场(t-app/bpg)已 rm --purge 清除; + - 正式验收:`make test` exit 0(test-boundaries/pins/libs/core/backend/frontend/ui/agentcmd 全过)。 +- 影响与剩余项:list 表加宽一列;老脚本无兼容性问题(dbref 缺省视为未绑定); + 发现并修复的 adjust_args_for_bin 缺陷影响既有 `deploy --dsn/--auth` 路径(此前被静默丢弃)。 + 剩余项无。 diff --git a/docs/svc.md b/docs/svc.md index 3e63f87d..c6f4394d 100644 --- a/docs/svc.md +++ b/docs/svc.md @@ -64,10 +64,12 @@ scripts/svc.sh db create pg1 --port 8890 # initdb,只登记不启动 scripts/svc.sh pg1 start # pg_ctl 启动 + 自动建库 + pg_isready scripts/svc.sh prod deploy --from dev@ \ --dsn "$(scripts/svc.sh db dsn pg1 --reveal)" # DSN 直接组合进部署 +scripts/svc.sh prod db pg1 --db prod # 或事后绑定/切换(缺库自动建,重启生效) # 日常操作 -scripts/svc.sh list # 全服务总览 +scripts/svc.sh list # 全服务总览(含 DB 绑定列) scripts/svc.sh info # 综合报告:运行服务/数据库/配置快照 +scripts/svc.sh prod db # 查看 prod 的数据库绑定与可用实例 scripts/svc.sh pg1 restart # 重启(复用上次配置) scripts/svc.sh prod logs 100 # 跟日志 scripts/svc.sh history # 事件追溯 @@ -216,6 +218,28 @@ scripts/svc.sh rm --purge # 连 runtime// 整个目录一起删 只管理本脚本登记过的服务;未登记名字会明确报错。 +### 4.5 db(agent 服务的数据库绑定/切换) + +```bash +scripts/svc.sh db # 查看当前绑定 + 可用实例 +scripts/svc.sh db [--db X] # 绑定/切换数据库 +scripts/svc.sh db --no-restart # 只改登记,下次 start 生效 +scripts/svc.sh db --clear # 解除绑定(回内嵌 SQLite 档) +``` + +每个 agent 服务都可以对应一个数据库实例中的库;绑定后 DSN 持久化在 `svc.env` +(`exec=` 的 `--dsn` + `dbref=/<库名>`,权限 600),跨重启保留。 + +- **缺库自动建**:实例运行中且目标库不存在时自动 `CREATE DATABASE`(幂等); + 实例未运行且目标库非其默认库 → 提示先 `start` 实例; +- **运行中自动重启生效**(复用新 `exec=`);`--no-restart` 只写登记; +- **旧产物预检**:当前二进制不识别 `--dsn` 时警告(绑定已登记但重启按内嵌档跑); +- 展示:`list` 的 **DB 列**、`status` 的"数据库"行、`info` 数据库节的**绑定服务**行 + (反向映射:哪些服务指向该实例)都能看到服务↔数据库对应关系; +- 未用本命令、直接 `deploy --dsn` 的服务,显示层会按 DSN 的 host:port 反查匹配已 + 登记实例(显示 `/<库>`;匹配不上显示 `外部@host:port`); +- `--clear` 后服务回到内嵌 SQLite 档(数据在原 `--data` 目录,切回即见旧数据)。 + ## 5. 命令参考:部署来源 `deploy --from` 支持三种来源(**缺省 = `dev` 远端最新**),agent 与 static 共用: @@ -287,12 +311,15 @@ scripts/svc.sh list [--tag T] # status 等价 扫描 `runtime/*/svc.env` 枚举全部已登记服务: ``` - NAME KIND STATE PID ADDR SOURCE TAGS - pg1 postgres 运行中 3115294 127.0.0.1:8890 postgres@local core,local - prod agent 运行中 3117059 127.0.0.1:8791 release@0.1.0 stable - ui-qa static 未运行 - 127.0.0.1:9054 dir@/tmp/static-demo ui + NAME KIND STATE PID ADDR DB SOURCE TAGS + pg1 postgres 运行中 3115294 127.0.0.1:8890 - postgres@local core,local + prod agent 运行中 3117059 127.0.0.1:8791 pg1/prod release@0.1.0 stable + ui-qa static 未运行 - 127.0.0.1:9054 - dir@/tmp/static-demo ui ``` +DB 列:agent 服务当前绑定的 `/<库名>`(见 §4.5;外部 DSN 显示 `外部@host:port`, +未绑定显示 `-`)。 + STATE 的判定:pidfile 存在且 `/proc//cmdline` 匹配登记的 `pidcheck` 标记 (agent=二进制路径、postgres=pgdata 路径、static=`--directory <目录>`)。进程被人手动 杀掉后 STATE 自动落回"未运行",不会误报。 @@ -315,10 +342,10 @@ scripts/svc.sh info [name] [--tag T] [--reveal] 报告分节(整体走 stdout,可直接 `> snapshot.txt` 整页保存): -1. **服务**——`list` 同款表格 + 汇总行(`N 已登记 / M 运行中(agent a、postgres p、static s)`); +1. **服务**——`list` 同款表格(含 **DB 绑定列**)+ 汇总行(`N 已登记 / M 运行中(agent a、postgres p、static s)`); 2. **数据库**——每个 postgres 实例一屏:运行状态/地址、版本/用户/认证/默认库/来源、 - 数据目录与体量、备份份数与最近一份;**运行中的实例直接列出内部数据库及大小** - (未运行则注明内部库不可见); + **绑定服务**(哪些 agent 服务指向该实例,见 §4.5)、数据目录与体量、备份份数与 + 最近一份;**运行中的实例直接列出内部数据库及大小**(未运行则注明内部库不可见); 3. **配置**——逐服务 `runtime//svc.env` 的文件路径、权限与逐行内容快照,加上 全局层:`.svc.env`(自动加载层)、本次 `--config/--env` 文件、shell 已导出的 `SVC_*` 变量; @@ -549,7 +576,8 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 | `version` | 产物版本串(`agent-system version` 输出) | | `started` | 最近启动时间 | | `pidcheck` | 进程判定标记(见 §6.1) | -| `exec` | agent 上次完整启动参数 | +| `exec` | agent 上次完整启动参数(含 `--dsn`,密码在其中) | +| `dbref` | agent 数据库绑定 `/<库名>`(` db` 命令写入,跨重启保留;见 §4.5) | | `user` / `password` / `dbname` / `authmode` / `pgdata` | postgres 专属 | | `htdocs` | static 专属 | @@ -563,7 +591,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 | `deploy` | begin / ok / fail | agent/static 部署全程 | | `start` | ok / fail / warn | 启动(fail 含"进程即退/健康检查超时";warn 为进程起但探活未过) | | `stop` | ok / fail | 停止(含 pg_ctl fast / SIGTERM 兜底说明) | -| `set` | ok | 登记变更(`addr 旧 → 新`) | +| `set` | ok | 登记变更(`addr 旧 → 新`;`db bind → /<库>` / `db unbind`) | | `backup` | ok / fail | 备份(文件、大小) | | `restore` | ok / fail | 恢复(库、文件、表数) | | `clone` | ok / fail | 克隆(物理/在线) | diff --git a/scripts/svc.sh b/scripts/svc.sh index 2c730f5f..ce78c600 100755 --- a/scripts/svc.sh +++ b/scripts/svc.sh @@ -5,6 +5,7 @@ # # ════════════════════ 命令一览 ════════════════════ # scripts/svc.sh [action] [参数...] 服务级:deploy(默认)|start|stop|restart|status|logs [N] +# |db [pg名|--clear](agent 数据库绑定/切换) # scripts/svc.sh list [--tag T] 服务总览(status 等价) # scripts/svc.sh info [name] [--tag T] [--reveal] 综合报告(只读):服务/数据库/配置快照/最近事件 # scripts/svc.sh set [--port N --apply] [--tag T] 变更登记(换端口/标签) @@ -39,6 +40,8 @@ # --kind agent|static deploy 时指定类型(postgres 用 db create 建) # --dir PATH static 直接服务既有目录(不构建) # --plugins/--dsn/--auth/--mock/--data agent 运行参数 +# db [--db X] [--no-restart] agent 绑定/切换数据库(实例运行中缺库自动建; +# --clear 解绑回内嵌 SQLite;无参查看当前绑定与可用实例) # --model-base/--model-key/--model-name 与 --light-*/--tavily-keys 启动后播种设置 # -- <剩余参数> 原样透传给 agent-system serve(escape hatch) # db create:--user U(默认 agent)--password P(缺省自动生成)--db NAME --auth-mode scram-sha-256|md5 @@ -69,6 +72,9 @@ # scripts/svc.sh set pg1 --port 8895 --apply # 换端口并重启生效 # scripts/svc.sh db backup pg1 && scripts/svc.sh db backups pg1 # scripts/svc.sh db clone pg1 pg2 --port 8896 # 物理克隆一套到新端口 +# scripts/svc.sh dev db pg1 --db dev # 把 dev 服务绑定到 pg1/dev 库(自动重启) +# scripts/svc.sh dev db pg2 --no-restart # 切换但不重启(下次 start 生效) +# scripts/svc.sh dev db --clear # 解绑回内嵌 SQLite 档 # scripts/svc.sh db copy pg1 pg2 --db app # 把 pg1 的 app 库搬到 pg2 # scripts/svc.sh db restore pg2 --file --db app --force # scripts/svc.sh list --tag core && scripts/svc.sh history @@ -123,6 +129,7 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po scripts/svc.sh deploy [--from dev@] 部署并启动(缺省=远端 main 最新) scripts/svc.sh start|stop|restart|logs [N] 生命周期与日志 scripts/svc.sh status 服务详情(版本/来源/DSN 等) + scripts/svc.sh db [pg名|--clear] agent 数据库绑定/切换(缺省查看) scripts/svc.sh db create pg1 --port 8890 建数据库 scripts/svc.sh db dsn|backup|backups|restore|clone|copy|ls|set pg1 ... 数据库运维 scripts/svc.sh set pg1 --port 8895 --apply 换端口(--tag 改标签) @@ -253,6 +260,10 @@ EOF scripts/svc.sh start 启动(agent/static/postgres 分别分派;已在运行则提示) scripts/svc.sh stop 停止(agent/static: TERM→KILL;postgres: pg_ctl -m fast) scripts/svc.sh restart 停止 + 启动(裸 restart 复用上次配置) + scripts/svc.sh db [pg名|--clear] [--db X] agent 数据库绑定:缺省查看当前 + 绑定与可用实例;给 pg名=绑定/切换(DSN 写入 exec=,dbref= 登记, + 实例运行中缺库自动建,服务运行中自动重启生效);--clear 解绑回内嵌 + SQLite 档;--no-restart 只改登记 scripts/svc.sh logs [N] 跟随日志(默认 50 行起;如 svc.sh pg1 logs 200) scripts/svc.sh status 单服务详情(kind/状态/地址/来源/版本/标签/DSN/目录) scripts/svc.sh list [--tag T] 全服务表格:NAME KIND STATE PID ADDR SOURCE TAGS @@ -308,6 +319,14 @@ EOF 连接其他服务:DSN 可直接组合进 agent 部署—— scripts/svc.sh prod deploy --from dev@ --dsn "$(scripts/svc.sh db dsn pg1 --reveal)" (旧版本产物不识别 --dsn 时会自动省略并提示。) +更常用:让已部署的 agent 服务绑定/切换数据库(不用重新 deploy)—— + scripts/svc.sh prod db pg1 # 绑定 pg1 默认库(运行中自动重启生效) + scripts/svc.sh prod db pg1 --db prod # 绑定 pg1/prod 库(缺库自动建) + scripts/svc.sh prod db pg2 --no-restart # 切换实例但暂不重启(下次 start 生效) + scripts/svc.sh prod db # 查看当前绑定 + 可用实例 + scripts/svc.sh prod db --clear # 解绑回内嵌 SQLite 档 + 绑定写入 svc.env(exec= 的 --dsn + dbref=,600);info / list 的 DB 列与数据库节 + "绑定服务"行展示服务↔实例对应关系。 密码管理:密码明文存 runtime//svc.env(600);--password 未给时自动生成 (head -c 18 /dev/urandom)。auth-mode 默认 scram-sha-256(pg_hba 全线)。 @@ -625,14 +644,15 @@ svc_merge_tags() { } # record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600)。 -# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec; -# kind 特有键(user/password/dbname/authmode/pgdata/htdocs...)经参数追加。 +# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref(跨重启 +# 保留的数据库绑定);kind 特有键(user/password/dbname/authmode/pgdata/htdocs...)经参数追加。 record_state() { # 先读完旧文件再开重定向(块内 {} > svc.env 会先截断文件,读旧值必须在其之前)。 - local pid prev_src merged_tags + local pid prev_src prev_dbref merged_tags pid="$(read_pid)" prev_src="" [ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)" + prev_dbref="$(svc_get dbref)" SVC_SOURCE="${SRC_LABEL:-${prev_src:-}}" merged_tags="$(svc_merge_tags)" mkdir -p "$ENV_DIR" @@ -647,6 +667,7 @@ record_state() { printf 'started=%s\n' "$(date -Is)" printf 'pidcheck=%s\n' "${SVC_PIDCHECK:-}" printf 'exec=%s\n' "${SVC_EXEC:-}" + [ -n "$prev_dbref" ] && printf 'dbref=%s\n' "$prev_dbref" [ $# -gt 0 ] && printf '%s\n' "$@" } > "$ENV_DIR/svc.env" chmod 600 "$ENV_DIR/svc.env" @@ -769,14 +790,17 @@ agent_has_explicit_cfg() { } # adjust_args_for_bin BIN ARGS:按二进制 serve 用法过滤旧产物不认识的 --dsn/--auth。 +# 匹配按"去横线的旗标名 + 1-2 个横线"(Go flag 帮助打印单横线 -dsn,2026-10-04 实测 +# 旧实现 grep -- '--dsn' 永不命中,把现役二进制的 --dsn/--auth 误当不支持而丢弃)。 adjust_args_for_bin() { local bin="$1" args="$2" out out="$("$bin" serve -h 2>&1 || true)" - local filtered="" w drop_next=0 + local filtered="" w drop_next=0 flag for w in $args; do case "$w" in --dsn|--auth) - if printf '%s\n' "$out" | grep -q -- "$w"; then + flag="${w#--}" + if printf '%s\n' "$out" | grep -Eq -- "(^|[[:space:]])-{1,2}${flag}([[:space:]]|=|$)"; then filtered="$filtered $w" else drop_next=1 @@ -1412,6 +1436,7 @@ db_create() { hist_event create ok "" "user=$user db=$dbname authmode=$authmode port=${ENV_ADDR##*:}" c_ok "[$ENV_NAME] 数据库已创建(未启动):scripts/svc.sh $ENV_NAME start" c_info "[$ENV_NAME] 连接串:scripts/svc.sh db dsn $ENV_NAME --reveal(密码存 svc.env,权限 600)" + c_info "[$ENV_NAME] 服务绑定:scripts/svc.sh db $ENV_NAME [--db X](运行中自动重启生效)" return 0 } @@ -1541,6 +1566,16 @@ svc_set_key() { return 0 } +# svc_del_key KEY:从当前服务 svc.env 删除单键(键不存在则无操作)。 +svc_del_key() { + local key="$1" + [ -f "$ENV_DIR/svc.env" ] || return 0 + { grep -v "^$key=" "$ENV_DIR/svc.env" || true; } > "$ENV_DIR/svc.env.tmp" + mv "$ENV_DIR/svc.env.tmp" "$ENV_DIR/svc.env" + chmod 600 "$ENV_DIR/svc.env" + return 0 +} + # 显式参数版 psql/dump/restore:clone/copy 需要同时操作两个实例,不依赖全局 ENV_*。 pg_run() { # HOST PORT USER PASS DB SQL [tA] local host="$1" port="$2" user="$3" pass="$4" db="$5" sql="$6" ta="${7:-}" pgbin @@ -1860,6 +1895,138 @@ db_ls() { return 0 } +# ---- agent 服务的数据库绑定( db ...)---- + +# svc_db_bind [pg名] [--db X] [--clear] [--no-restart]:查看 / 绑定切换 / 解除 +# agent 服务的数据库。绑定 = 目标实例的 DSN 写入持久化 exec=(--dsn,密码 600 存放) +# 并登记 dbref=/<库>;实例运行中时确保目标库存在(缺则建);服务运行中则自动 +# 重启生效(--no-restart 只改登记)。--clear 解除绑定(回内嵌 SQLite 档)。 +svc_db_bind() { + [ "$SVC_KIND" = agent ] || { c_err "[$ENV_NAME] kind=$SVC_KIND 无数据库绑定(仅 agent 服务)"; return 2; } + [ -f "$ENV_DIR/svc.env" ] || { c_err "[$ENV_NAME] 无登记(先 deploy)"; return 2; } + local target="" clear=0 restart=1 + while [ $# -gt 0 ]; do + case "$1" in + --clear) clear=1; shift ;; + --no-restart) restart=0; shift ;; + --db) need_value "$@"; CLI_DB_NAME="$2"; shift 2 ;; + -h|--help) + cat >&2 < [--db X] [--no-restart] 绑定/切换(运行中自动重启生效) + scripts/svc.sh $ENV_NAME db --clear [--no-restart] 解除绑定(回内嵌 SQLite 档) +EOF + return 0 ;; + -*) c_err "[$ENV_NAME] db 未知参数:$1(可用: | --clear,--db X,--no-restart)"; return 2 ;; + *) [ -z "$target" ] || { c_err "[$ENV_NAME] db 只接受一个 postgres 服务名"; return 2; } + target="$1"; shift ;; + esac + done + + # 无参:查看当前绑定 + 可用实例。 + if [ -z "$target" ] && [ "$clear" = 0 ]; then + local ref dsn pg st + ref="$(db_ref_of "$ENV_NAME")" + if [ -n "$ref" ]; then + printf ' [%s] 数据库绑定:%s\n' "$ENV_NAME" "$ref" + dsn="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')" + [ -n "$dsn" ] && printf ' DSN %s\n' "$(mask_dsn "$dsn")" + else + printf ' [%s] 数据库绑定:无(内嵌 SQLite 档,data=%s)\n' "$ENV_NAME" "$ENV_DATA" + fi + printf ' 可用实例:\n' + local found=0 + while IFS= read -r pg; do + st="$(svc_pid_state "$pg")" + printf ' %-14s %-7s %s\n' "$pg" \ + "$(if [ "${st%% *}" = alive ]; then printf 运行中; else printf 未运行; fi)" \ + "$(env_kv_of "$pg" addr)" + found=1 + done < <(registered_names | while IFS= read -r n; do [ "$(svc_kind "$n")" = postgres ] && printf '%s\n' "$n"; done) + [ "$found" = 0 ] && printf ' (无;scripts/svc.sh db create 创建)\n' + printf ' 切换:scripts/svc.sh %s db [--db X];解除:--clear\n' "$ENV_NAME" + return 0 + fi + + # 解除绑定。 + if [ "$clear" = 1 ]; then + [ -n "$target" ] && { c_err "--clear 不接受服务名参数"; return 2; } + local exec_old new_exec + exec_old="$(svc_get exec)" + if printf '%s' "$exec_old" | grep -q -- '--dsn '; then + new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--dsn [^ ]+//; s/ +/ /g; s/^ //; s/ $//')" + svc_set_key exec "$new_exec" + fi + svc_del_key dbref + hist_event set ok "$(read_pid)" "db unbind" + c_ok "[$ENV_NAME] 已解除数据库绑定(内嵌 SQLite 档;数据目录 $ENV_DATA)" + if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then + do_stop; do_start || { c_err "[$ENV_NAME] 重启失败——解绑已登记,排查后 start"; return 1; } + c_ok "[$ENV_NAME] 已重启(内嵌 SQLite 档)" + else + c_info "[$ENV_NAME] 未运行或 --no-restart——下次 start 生效" + fi + return 0 + fi + + # 绑定/切换。 + valid_env "$target" || { c_err "非法服务名:$target"; return 2; } + [ -f "$RUNTIME/$target/svc.env" ] || { c_err "[$target] 未登记(scripts/svc.sh db create $target)"; return 2; } + [ "$(svc_kind "$target")" = postgres ] || { c_err "[$target] kind=$(svc_kind "$target") 不是 postgres 实例"; return 2; } + local taddr thost tport tuser tpass regdb tdb dsn st + taddr="$(env_kv_of "$target" addr)" + thost="${taddr%:*}"; tport="${taddr##*:}" + tuser="$(env_kv_of "$target" user)" + tpass="$(env_kv_of "$target" password)" + regdb="$(env_kv_of "$target" dbname)" + tdb="${CLI_DB_NAME:-$regdb}" + { [ -n "$tuser" ] && [ -n "$tpass" ]; } || { c_err "[$target] 登记缺 user/password(重新 db create)"; return 2; } + # 实例运行中:确保目标库存在(缺则建,幂等)。 + st="$(svc_pid_state "$target")" + if [ "${st%% *}" = alive ]; then + if ! pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "SELECT 1 FROM pg_database WHERE datname='$tdb'" tA | grep -q 1; then + if pg_run "$thost" "$tport" "$tuser" "$tpass" postgres "CREATE DATABASE \"$tdb\"" >/dev/null; then + c_ok "[$target] 已建库:$tdb" + else + c_warn "[$target] 建库 $tdb 未成功——绑定继续,请手动建库后重启服务" + fi + fi + elif [ "$tdb" != "$regdb" ]; then + c_warn "[$target] 未运行——库 $tdb 暂未创建(先 start $target 再重跑,或绑定默认库 $regdb)" + fi + dsn="postgresql://$tuser:$tpass@$thost:$tport/$tdb" + # 产物支持预检(旧二进制不识别 --dsn 时提示;重启复用 exec 不会自动过滤)。 + if [ -x "$ENV_BIN" ]; then + local adj + adj="$(adjust_args_for_bin "$ENV_BIN" "--dsn $dsn")" + [ "$adj" != "--dsn $dsn" ] && \ + c_warn "[$ENV_NAME] 当前产物不识别 --dsn(旧版本二进制)——绑定已登记,但重启后将按内嵌档运行" + fi + # 写持久化配置:exec= 的 --dsn 替换或追加;dbref= 登记。 + local exec_old new_exec + exec_old="$(svc_get exec)" + if printf '%s' "$exec_old" | grep -q -- '--dsn '; then + new_exec="$(printf '%s' "$exec_old" | sed -E "s|--dsn [^ ]+|--dsn $dsn|")" + else + new_exec="${exec_old:+$exec_old }--dsn $dsn" + fi + svc_set_key exec "$new_exec" + svc_set_key dbref "$target/$tdb" + hist_event set ok "$(read_pid)" "db bind → $target/$tdb" + c_ok "[$ENV_NAME] 数据库绑定:$target/$tdb(DSN $(mask_dsn "$dsn");svc.env 600 保存,info 可查)" + # 运行中:重启生效(start 复用新 exec=)。 + if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then + c_info "[$ENV_NAME] 重启以切换数据库……" + do_stop + do_start || { c_err "[$ENV_NAME] 重启失败——绑定已登记,排查日志后 scripts/svc.sh $ENV_NAME start"; return 1; } + c_ok "[$ENV_NAME] 已切换数据库:$target/$tdb(生效中)" + else + c_info "[$ENV_NAME] 未运行或 --no-restart——下次 start 生效" + fi + return 0 +} + cmd_set() { local name="${1:-}" [ -n "$name" ] || { c_err "用法:scripts/svc.sh set [--port N] [--host H] [--apply] [--tag T...]"; exit 2; } @@ -2015,10 +2182,32 @@ registered_names() { return 0 } +# db_ref_of NAME:agent 服务的数据库绑定标识(list/status/info 共用)。 +# dbref= 优先;否则从 exec= 的 --dsn 提取 host:port 匹配已登记 postgres 实例 +# (兼容直接 --dsn 部署、尚未登记 dbref 的服务);都匹配不上则回 外部@host:port。 +db_ref_of() { + local name="$1" ref dsn hostport pg addr + ref="$(env_kv_of "$name" dbref)" + if [ -n "$ref" ]; then printf '%s' "$ref"; return 0; fi + dsn="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')" + [ -n "$dsn" ] || return 0 + hostport="$(printf '%s' "$dsn" | sed -n 's#.*@\([^/?]*\).*#\1#p')" + while IFS= read -r pg; do + [ "$(svc_kind "$pg")" = postgres ] || continue + addr="$(env_kv_of "$pg" addr)" + if [ "$addr" = "$hostport" ]; then + printf '%s/%s' "$pg" "${dsn##*/}" + return 0 + fi + done < <(registered_names) + printf '外部@%s' "$hostport" + return 0 +} + global_list() { c_info "服务总览(root: $ROOT)" - printf ' %-12s %-9s %-7s %-8s %-20s %-24s %s\n' NAME KIND STATE PID ADDR SOURCE TAGS - local names=() d name kind addr src tags st + printf ' %-12s %-9s %-7s %-8s %-20s %-16s %-24s %s\n' NAME KIND STATE PID ADDR DB SOURCE TAGS + local names=() d name kind addr src tags st dbref while IFS= read -r name; do names+=("$name"); done < <(registered_names) for name in ${names[@]+"${names[@]}"}; do d="$RUNTIME/$name" @@ -2030,10 +2219,13 @@ global_list() { case ",$tags," in *",${LIST_TAG},"*) ;; *) continue ;; esac fi st="$(svc_pid_state "$name")" + dbref="-" + [ "$kind" = agent ] && dbref="$(db_ref_of "$name")" + [ -n "$dbref" ] || dbref="-" if [ "${st%% *}" = alive ]; then - printf ' %-12s %-9s %-7s %-8s %-20s %-24s %s\n' "$name" "$kind" "运行中" "${st#alive }" "$addr" "${src:--}" "${tags:--}" + printf ' %-12s %-9s %-7s %-8s %-20s %-16s %-24s %s\n' "$name" "$kind" "运行中" "${st#alive }" "$addr" "$dbref" "${src:--}" "${tags:--}" else - printf ' %-12s %-9s %-7s %-8s %-20s %-24s %s\n' "$name" "$kind" "未运行" "-" "$addr" "${src:--}" "${tags:--}" + printf ' %-12s %-9s %-7s %-8s %-20s %-16s %-24s %s\n' "$name" "$kind" "未运行" "-" "$addr" "$dbref" "${src:--}" "${tags:--}" fi done [ "${#names[@]}" -eq 0 ] && c_info "(暂无已登记服务;db create / deploy 会登记)" @@ -2057,6 +2249,9 @@ env_status_detail() { [ -n "$src" ] && printf ' 来源 %s 版本 %s\n' "$src" "${ver:--}" [ -n "$tags" ] && printf ' 标签 %s\n' "$tags" case "$kind" in + agent) + local dbref; dbref="$(db_ref_of "$ENV_NAME")" + printf ' 数据库 %s\n' "${dbref:-无(内嵌 SQLite 档;绑定: db )}" ;; postgres) printf ' 库 %s 用户 %s\n' "$(svc_get dbname)" "$(svc_get user)" printf ' DSN postgresql://%s:***@%s:%s/%s(db dsn %s --reveal 看密码)\n' \ @@ -2185,6 +2380,14 @@ info_pg_block() { fi printf ' 版本 %s 用户 %s 认证 %s 默认库 %s 来源 %s\n' \ "${ver:--}" "${user:--}" "${auth:--}" "${db:--}" "${src:--}" + # 反向绑定:哪些 agent 服务当前指向本实例。 + local bound="" bn bref + while IFS= read -r bn; do + [ "$(svc_kind "$bn")" = agent ] || continue + bref="$(db_ref_of "$bn")" + case "$bref" in "$name/"*) bound="${bound:+$bound、}$bn(${bref#*/})" ;; esac + done < <(registered_names) + printf ' 绑定服务 %s\n' "${bound:-无(绑定:scripts/svc.sh db $name)}" [ -n "$pgdata" ] && printf ' 数据目录 %s(%s)\n' "$pgdata" "$(info_du "$pgdata")" local nb=0 bsize="" latest="" f if [ -d "$d/backups" ]; then @@ -2566,11 +2769,17 @@ main() { # 动作词可省略(省略 = deploy);flags 可直接跟随服务名。 local action="deploy" case "${1:-}" in - deploy|stop|start|restart|status|logs) action="$1"; shift ;; + deploy|stop|start|restart|status|logs|db) action="$1"; shift ;; esac if [ "$action" = logs ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then CLI_LOGS_N="$1"; shift fi + # db 绑定子命令:自带参数解析(位置参数 不走 parse_args;未知 flags 由其自行拒绝)。 + if [ "$action" = db ]; then + env_config "$ENV_NAME" + svc_db_bind "$@" || exit $? + exit 0 + fi parse_args "$@" if { [ -n "$CLI_FROM" ] || [ -n "$CLI_PICK" ] || [ "$CLI_DRYRUN" = 1 ] || [ -n "$CLI_DIR" ]; } && [ "$action" != deploy ]; then c_err "--from/--pick/--dry-run/--dir 仅用于 deploy 动作" From 73bca5e7497883768454c9cd362c0968003ab51a Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 4 Oct 2026 13:30:59 +0000 Subject: [PATCH 3/6] Record the sync checkpoint --- .task/feature-svc-db-bind.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.task/feature-svc-db-bind.md b/.task/feature-svc-db-bind.md index 44c6d853..c76af9e3 100644 --- a/.task/feature-svc-db-bind.md +++ b/.task/feature-svc-db-bind.md @@ -51,3 +51,4 @@ - 影响与剩余项:list 表加宽一列;老脚本无兼容性问题(dbref 缺省视为未绑定); 发现并修复的 adjust_args_for_bin 缺陷影响既有 `deploy --dsn/--auth` 路径(此前被静默丢弃)。 剩余项无。 +| 同步前验收 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | 843495e0d0618b900d727e38570ff61451068c9b | 2026-10-04T13:30:59Z | From ecfed7be314b806f17a84c992cedf300ec2b9997 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 4 Oct 2026 13:42:51 +0000 Subject: [PATCH 4/6] Add service config-file switching with schema validation to svc.sh Why: beyond database binding, services needed a way to switch the JSON config file they run with and have the new config take effect promptly; hand-editing exec= risks restarting a service into an invalid config. Evidence: in the task worktree with a real agent binary -- a config with an unknown key is rejected by the binary's strict schema check before any state change; a valid file binds via --config in exec= + conffile= (survives record_state rewrites), restarts the running service, and the config's dsn verifiably drives the process (wrong-password variant failed PG auth in the log; correct variant connected and created 21 tables); editing the file and re-running 'config ' re-validates and restarts (pid changes); '--clear' removes both the flag and conffile; db and config coexist in exec=; view mode shows masked JSON content plus 'config explain' effective values with masked dsn; list DB column also resolves dsns that live only inside the config file. Full log in the task card. Impact: new service-level action 'svc.sh config [file | --clear] [--no-restart]' for agent services; config-file content snapshots (masked, --reveal to show) in info; a config-file line in status; conffile persisted in svc.env. No agent binary changes. Refs: task card .task/feature-svc-db-bind.md (same doc as PR #182); docs/svc.md sections 2/4.5/4.6/6/10.1 --- .task/feature-svc-db-bind.md | 57 ++++++---- docs/svc.md | 22 ++++ scripts/svc.sh | 196 ++++++++++++++++++++++++++++++++--- 3 files changed, 241 insertions(+), 34 deletions(-) diff --git a/.task/feature-svc-db-bind.md b/.task/feature-svc-db-bind.md index c76af9e3..5d245896 100644 --- a/.task/feature-svc-db-bind.md +++ b/.task/feature-svc-db-bind.md @@ -6,25 +6,33 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 3836ea88e3954878f257b7708ae5ccef947ccd79 - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-db-bind(worktree);分支:feature/svc-db-bind -- 授权与交付终点:实现"服务↔数据库绑定"(svc.sh 服务级 db 子命令 + info/list 显示绑定)并修复 - adjust_args_for_bin 单横线误判;终点 = PR 合入 main(附 precheck / task-sync / 测试证据) +- 授权与交付终点:实现"服务↔数据库绑定"(svc.sh 服务级 db 子命令 + info/list 显示绑定)与 + "服务配置文件更新/即时生效"(config 子命令),并修复 adjust_args_for_bin 单横线误判; + 终点 = PR 合入 main(用户指示完成后一并合入;附 precheck / task-sync / 测试证据) ## 目标与非目标 目标: -- agent 服务可绑定/切换/解除数据库:`svc.sh db [pg名 [--db X] | --clear] [--no-restart]`; - 缺省查看当前绑定与可用实例。绑定 = DSN 写入持久化 exec= + dbref=/<库> 登记,实例运行中 - 缺库自动建,服务运行中自动重启生效;--clear 回内嵌 SQLite 档。 -- 显示层全链路:list 表新增 DB 列;status 详情新增"数据库"行;info 数据库节新增"绑定服务" - 反向映射;无 dbref 的旧服务按 DSN host:port 反查匹配实例(外部 DSN 显示 外部@host:port)。 -- 修复既有缺陷:adjust_args_for_bin 以 grep '--dsn' 检测旗标支持,Go flag 帮助打印单横线 +- **数据库绑定**:agent 服务可绑定/切换/解除数据库:`svc.sh db [pg名 [--db X] | --clear] + [--no-restart]`;缺省查看当前绑定与可用实例。绑定 = DSN 写入持久化 exec= + dbref=/<库> + 登记,实例运行中缺库自动建,服务运行中自动重启生效;--clear 回内嵌 SQLite 档。 +- **配置文件更新**:`svc.sh config [file.json |--clear] [--no-restart]`;缺省查看当前 + 配置文件(掩码内容 + `agent-system config explain -config` 视角的生效值)。切换前经二进制 + schema 强校验(未知键/非法值拒绝,不把服务重启进坏配置),通过后把 --config <绝对路径> + 写入 exec= + conffile= 登记,运行中自动重启即时生效(agent 无 SIGHUP 热重载,重启秒级); + --clear 移除回命令行参数形态。绑定后改文件内容 + restart(或重跑 config 同文件)即生效。 +- **显示层全链路**:list 表新增 DB 列(含配置文件 dsn 反查);status 详情新增"数据库""配置 + 文件"行;info 数据库节新增"绑定服务"反向映射、配置节附配置文件内容快照(敏感值掩码, + --reveal 明文);无 dbref 的旧服务按 exec= --dsn 或配置文件 dsn 的 host:port 反查匹配实例 + (外部 DSN 显示 外部@host:port)。 +- **修复既有缺陷**:adjust_args_for_bin 以 grep '--dsn' 检测旗标支持,Go flag 帮助打印单横线 `-dsn`,导致现役二进制的 --dsn/--auth 在 deploy 重建参数时被误删——改为"去横线旗标名 + 1-2 横线"匹配。 -- record_state 跨重启保留 dbref;svc_del_key 辅助;db create 尾部提示绑定命令; - usage/usage_short/help(lifecycle/db)/docs/svc.md 同步。 +- record_state 跨重启保留 dbref/conffile;svc_del_key 辅助;db create 尾部提示绑定命令; + usage/usage_short/help(lifecycle/config/db)/docs/svc.md(§2/§4.5/§4.6/§6/§10.1)同步。 非目标:不改 agent 二进制与部署/生命周期语义;不做 static/postgres 的绑定;不做运行中热切换 -(切换经重启生效)。 +(切换经重启生效);不做配置文件的编辑器(内容仍由用户维护,svc.sh 负责校验/切换/生效)。 ## 同步记录 @@ -34,19 +42,26 @@ ## 结果与验证(完成后填写) -- 结果:已实现(上列目标全部落地;` db` 服务级动作与顶层 `db` 命令并存无冲突)。 +- 结果:已实现(数据库绑定与配置文件更新两块目标全部落地;` db`/` config` + 服务级动作与顶层 `db` 命令、`--config` 旗标并存无冲突)。 - 验证(worktree runtime 实测,真实 agent 二进制 + PostgreSQL 18,2026-10-04): - `bash -n` 通过; - - 绑定:`t-app db bpg` → exec= 写入 --dsn、dbref=bpg/agent、自动重启生效;**record_state - 重写 svc.env 后 dbref 保留**;PG 的 agent 库出现 **21 张表**(服务真实落库,非仅配置); - - 切换:`t-app db bpg --db t2` → 自动建库 t2、重启、t2 库 21 张表; - - 解绑:`db --clear` → dbref/--dsn 移除、重启回内嵌档; - - `--no-restart` 只改登记;错误路径(未登记实例/多余参数/postgres 服务)均 exit 2 且提示正确; - - 显示层:list DB 列(t-app→bpg/agent)、`t-app db` 查看(掩码 DSN+实例列表)、info 数据库节 - "绑定服务 t-app(agent)";手删 dbref 模拟旧部署 → list 按 DSN host:port 反查仍显示 bpg/agent; + - 数据库绑定:`t-app db bpg` → exec= 写入 --dsn、dbref=bpg/agent、自动重启生效; + **record_state 重写 svc.env 后 dbref 保留**;PG 的 agent 库出现 **21 张表**(服务真实 + 落库,非仅配置);`--db t2` 切换自动建库(t2 库 21 张表);`--clear` 干净回内嵌档; + `--no-restart` 只改登记;错误路径(未登记实例/多余参数/postgres 服务)均 exit 2; + - 配置文件更新:坏文件(未知键 bogus)被 schema 校验拒绝且不动 exec、不重启;合法文件 + 绑定后 cmdline 含 `--config `、conffile= 跨 record_state 保留;**配置文件内的 dsn + 真实生效**(错误密码版本在日志中报 PG 认证失败=配置确已应用;正确密码版连上 PG、 + agent 库 21 张表);内容修改后重跑 `config <同一文件>` 再校验并重启(pid 更新); + `--clear` 移除 --config/conffile 并重启;错误路径(文件不存在/双文件)exit 2; + - db + config 共存:exec= 同时带 --dsn 与 --config,重启健康; + - 显示层:list DB 列(含"仅配置文件带 dsn"场景的反查 bpg/agent)、`config` 查看视图 + (JSON 掩码 `"dsn": "***"` + explain 生效值 dsn 掩码)、status"数据库/配置文件"行、 + info 配置节文件内容快照、info 数据库节"绑定服务";手删 dbref 模拟旧部署 → 反查仍显示; - adjust 修复:正例(帮助含 `-dsn` 判支持)与负例(无 dsn 判不支持)均正确; - - 回归:db dsn(掩码)/ db ls / info / history(新增 `set ok db bind → bpg/agent` 事件)/ - --help 正常;测试现场(t-app/bpg)已 rm --purge 清除; + - 回归:db dsn(掩码)/ db ls / info / history(新增 `set ok db bind →` / `config →` / + `config unbind` 事件)/ --help 正常;测试现场(t-app/bpg、临时 JSON)已清除。 - 正式验收:`make test` exit 0(test-boundaries/pins/libs/core/backend/frontend/ui/agentcmd 全过)。 - 影响与剩余项:list 表加宽一列;老脚本无兼容性问题(dbref 缺省视为未绑定); 发现并修复的 adjust_args_for_bin 缺陷影响既有 `deploy --dsn/--auth` 路径(此前被静默丢弃)。 diff --git a/docs/svc.md b/docs/svc.md index c6f4394d..aabfe710 100644 --- a/docs/svc.md +++ b/docs/svc.md @@ -65,6 +65,7 @@ scripts/svc.sh pg1 start # pg_ctl 启动 + 自动建库 scripts/svc.sh prod deploy --from dev@ \ --dsn "$(scripts/svc.sh db dsn pg1 --reveal)" # DSN 直接组合进部署 scripts/svc.sh prod db pg1 --db prod # 或事后绑定/切换(缺库自动建,重启生效) +scripts/svc.sh prod config .svc/prod.json # 指定/更换配置文件(校验+重启即时生效) # 日常操作 scripts/svc.sh list # 全服务总览(含 DB 绑定列) @@ -240,6 +241,26 @@ scripts/svc.sh db --clear # 解除绑定(回内嵌 SQL 登记实例(显示 `/<库>`;匹配不上显示 `外部@host:port`); - `--clear` 后服务回到内嵌 SQLite 档(数据在原 `--data` 目录,切回即见旧数据)。 +### 4.6 config(agent 服务的配置文件更新/生效) + +```bash +scripts/svc.sh config # 查看当前配置文件(掩码内容 + agent 视角生效值) +scripts/svc.sh config # 更换使用的配置文件(校验通过才切换) +scripts/svc.sh config --no-restart # 只改登记,下次 start 生效 +scripts/svc.sh config --clear # 移除(回命令行参数形态) +``` + +配置文件是 agent 二进制的 `--config`(JSON,schema 严格校验;可用 +`agent-system config explain -config ` 预览每个键的来源与生效值)。 + +- **切换前强校验**:文件必须存在且通过二进制的 schema 校验(未知键/非法值直接拒绝, + 不会把服务重启进坏配置);旧产物不识别 `--config` 时警告; +- **即时生效**:运行中的服务自动重启应用新配置(agent 无 SIGHUP 热重载,重启秒级); + 绑定后日常改配置 = 编辑文件 + `restart`(或重跑 `config <同一文件>` 再校验一遍); +- 持久化:`exec=` 的 `--config <绝对路径>` + `conffile=` 键(跨重启保留,600); +- 展示:`status`/`info ` 的"配置文件"行、`info` 配置节的文件内容快照 + (敏感值掩码,`info --reveal` 显示明文);文件丢失时明确告警并提示恢复或 `--clear`。 + ## 5. 命令参考:部署来源 `deploy --from` 支持三种来源(**缺省 = `dev` 远端最新**),agent 与 static 共用: @@ -578,6 +599,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价 | `pidcheck` | 进程判定标记(见 §6.1) | | `exec` | agent 上次完整启动参数(含 `--dsn`,密码在其中) | | `dbref` | agent 数据库绑定 `/<库名>`(` db` 命令写入,跨重启保留;见 §4.5) | +| `conffile` | agent 配置文件绝对路径(` config` 命令写入,跨重启保留;见 §4.6) | | `user` / `password` / `dbname` / `authmode` / `pgdata` | postgres 专属 | | `htdocs` | static 专属 | diff --git a/scripts/svc.sh b/scripts/svc.sh index ce78c600..574bbf99 100755 --- a/scripts/svc.sh +++ b/scripts/svc.sh @@ -6,6 +6,7 @@ # ════════════════════ 命令一览 ════════════════════ # scripts/svc.sh [action] [参数...] 服务级:deploy(默认)|start|stop|restart|status|logs [N] # |db [pg名|--clear](agent 数据库绑定/切换) +# |config [file|--clear](agent 配置文件更新/生效) # scripts/svc.sh list [--tag T] 服务总览(status 等价) # scripts/svc.sh info [name] [--tag T] [--reveal] 综合报告(只读):服务/数据库/配置快照/最近事件 # scripts/svc.sh set [--port N --apply] [--tag T] 变更登记(换端口/标签) @@ -42,6 +43,8 @@ # --plugins/--dsn/--auth/--mock/--data agent 运行参数 # db [--db X] [--no-restart] agent 绑定/切换数据库(实例运行中缺库自动建; # --clear 解绑回内嵌 SQLite;无参查看当前绑定与可用实例) +# config [--no-restart] agent 更换使用的配置文件(schema 校验通过才切换, +# 运行中自动重启即时生效;--clear 移除;无参查看当前配置与生效值) # --model-base/--model-key/--model-name 与 --light-*/--tavily-keys 启动后播种设置 # -- <剩余参数> 原样透传给 agent-system serve(escape hatch) # db create:--user U(默认 agent)--password P(缺省自动生成)--db NAME --auth-mode scram-sha-256|md5 @@ -75,6 +78,8 @@ # scripts/svc.sh dev db pg1 --db dev # 把 dev 服务绑定到 pg1/dev 库(自动重启) # scripts/svc.sh dev db pg2 --no-restart # 切换但不重启(下次 start 生效) # scripts/svc.sh dev db --clear # 解绑回内嵌 SQLite 档 +# scripts/svc.sh dev config .svc/dev.json # 更换配置文件(校验+重启即时生效) +# scripts/svc.sh dev config # 查看当前配置文件与生效值 # scripts/svc.sh db copy pg1 pg2 --db app # 把 pg1 的 app 库搬到 pg2 # scripts/svc.sh db restore pg2 --file --db app --force # scripts/svc.sh list --tag core && scripts/svc.sh history @@ -130,6 +135,7 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po scripts/svc.sh start|stop|restart|logs [N] 生命周期与日志 scripts/svc.sh status 服务详情(版本/来源/DSN 等) scripts/svc.sh db [pg名|--clear] agent 数据库绑定/切换(缺省查看) + scripts/svc.sh config [file|--clear] agent 配置文件更新/即时生效(缺省查看) scripts/svc.sh db create pg1 --port 8890 建数据库 scripts/svc.sh db dsn|backup|backups|restore|clone|copy|ls|set pg1 ... 数据库运维 scripts/svc.sh set pg1 --port 8895 --apply 换端口(--tag 改标签) @@ -264,6 +270,10 @@ EOF 绑定与可用实例;给 pg名=绑定/切换(DSN 写入 exec=,dbref= 登记, 实例运行中缺库自动建,服务运行中自动重启生效);--clear 解绑回内嵌 SQLite 档;--no-restart 只改登记 + scripts/svc.sh config [file|--clear] [--no-restart] agent 配置文件:缺省查看当前 + 文件与生效值;给 file=更换(agent 二进制 --config,JSON schema 校验 + 通过才切换,运行中自动重启即时生效);--clear 移除回命令行参数形态。 + 绑定后改文件内容 + restart 即生效(agent 无热重载,重启秒级) scripts/svc.sh logs [N] 跟随日志(默认 50 行起;如 svc.sh pg1 logs 200) scripts/svc.sh status 单服务详情(kind/状态/地址/来源/版本/标签/DSN/目录) scripts/svc.sh list [--tag T] 全服务表格:NAME KIND STATE PID ADDR SOURCE TAGS @@ -394,6 +404,11 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只 - 裸 start/restart 复用 exec=(上次完整参数);任何显式参数(或 --config 文件、 SVC_* 变量)出现即整体重建参数 - tags 随每次登记合并去重,跨重启保留 + +服务级配置文件(与上述 svc.sh dotenv 层不同——这是 agent 二进制自己的 JSON 配置): + scripts/svc.sh config [file|--clear] 更换/移除/查看(见 help lifecycle); + 切换前 schema 校验(agent-system config explain -config ),运行中自动重启 + 即时生效;登记于 svc.env conffile=,info/status 可见。 EOF return 0 ;; examples) @@ -644,15 +659,17 @@ svc_merge_tags() { } # record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600)。 -# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref(跨重启 -# 保留的数据库绑定);kind 特有键(user/password/dbname/authmode/pgdata/htdocs...)经参数追加。 +# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref/ +# conffile(跨重启保留的数据库绑定与配置文件);kind 特有键(user/password/dbname/ +# authmode/pgdata/htdocs...)经参数追加。 record_state() { # 先读完旧文件再开重定向(块内 {} > svc.env 会先截断文件,读旧值必须在其之前)。 - local pid prev_src prev_dbref merged_tags + local pid prev_src prev_dbref prev_conffile merged_tags pid="$(read_pid)" prev_src="" [ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)" prev_dbref="$(svc_get dbref)" + prev_conffile="$(svc_get conffile)" SVC_SOURCE="${SRC_LABEL:-${prev_src:-}}" merged_tags="$(svc_merge_tags)" mkdir -p "$ENV_DIR" @@ -668,6 +685,7 @@ record_state() { printf 'pidcheck=%s\n' "${SVC_PIDCHECK:-}" printf 'exec=%s\n' "${SVC_EXEC:-}" [ -n "$prev_dbref" ] && printf 'dbref=%s\n' "$prev_dbref" + [ -n "$prev_conffile" ] && printf 'conffile=%s\n' "$prev_conffile" [ $# -gt 0 ] && printf '%s\n' "$@" } > "$ENV_DIR/svc.env" chmod 600 "$ENV_DIR/svc.env" @@ -1895,6 +1913,127 @@ db_ls() { return 0 } +# mask_json TEXT:JSON 文本里键名含 key/pass/secret/token/dsn(不区分大小写)的 +# 字符串值掩为 ***(配置文件内容快照用)。输出带尾换行。 +mask_json() { + printf '%s\n' "$1" | sed -E 's/("[^"]*(key|pass|secret|token|dsn)[^"]*"[[:space:]]*:[[:space:]]*")[^"]*"/\1***"/Ig' +} + +# ---- agent 服务的配置文件( config ...)---- + +# svc_config [FILE|--clear] [--no-restart]:查看 / 更新 / 移除 agent 服务使用的 +# 配置文件(agent 二进制的 --config,JSON,schema 严格校验)。更新 = 校验通过后 +# 把 --config <绝对路径> 写入持久化 exec= 并登记 conffile=,运行中自动重启生效 +# (agent 无 SIGHUP 热重载,重启即"及时生效",秒级);--clear 移除回命令行参数形态。 +svc_config() { + [ "$SVC_KIND" = agent ] || { c_err "[$ENV_NAME] kind=$SVC_KIND 无配置文件绑定(仅 agent 服务)"; return 2; } + [ -f "$ENV_DIR/svc.env" ] || { c_err "[$ENV_NAME] 无登记(先 deploy)"; return 2; } + local target="" clear=0 restart=1 + while [ $# -gt 0 ]; do + case "$1" in + --clear) clear=1; shift ;; + --no-restart) restart=0; shift ;; + -h|--help) + cat >&2 < [--no-restart] 更新使用的配置文件 + (schema 校验通过才切换;运行中自动重启生效) + scripts/svc.sh $ENV_NAME config --clear [--no-restart] 移除(回命令行参数形态) +提示:配置文件绑定后,改文件内容 + restart 即生效;config <同一文件> 也会校验并重启。 +EOF + return 0 ;; + -*) c_err "[$ENV_NAME] config 未知参数:$1(可用:<文件> | --clear,--no-restart)"; return 2 ;; + *) [ -z "$target" ] || { c_err "[$ENV_NAME] config 只接受一个文件路径"; return 2; } + target="$1"; shift ;; + esac + done + + # 当前配置文件(conffile= 优先;旧服务回退 exec= 的 --config)。 + local cur="" + cur="$(svc_get conffile)" + [ -z "$cur" ] && cur="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')" + + # 无参:查看(文件状态 + 掩码内容 + 二进制视角的生效值)。 + if [ -z "$target" ] && [ "$clear" = 0 ]; then + if [ -n "$cur" ]; then + printf ' [%s] 配置文件:%s\n' "$ENV_NAME" "$cur" + if [ -f "$cur" ]; then + printf ' 权限 %s;内容(敏感值掩码):\n' "$(info_file_mode "$cur")" + mask_json "$(cat "$cur")" | sed 's/^/ /' + if [ -x "$ENV_BIN" ] && "$ENV_BIN" config explain -config "$cur" >/dev/null 2>&1; then + printf ' 生效值(agent 视角,敏感值掩码):\n' + "$ENV_BIN" config explain -config "$cur" 2>/dev/null \ + | sed -E 's#(postgres(ql)?://[^:/@[:space:]]+:)[^@[:space:]]+@#\1***@#g; s/^/ /' + else + printf ' (当前产物无法校验该文件:config explain 不可用或文件不合法)\n' + fi + else + printf ' 文件不存在!重启将失败——请恢复文件或 config --clear\n' + fi + else + printf ' [%s] 配置文件:无(命令行参数形态;绑定:config )\n' "$ENV_NAME" + fi + return 0 + fi + + # 移除。 + if [ "$clear" = 1 ]; then + [ -n "$target" ] && { c_err "--clear 不接受文件参数"; return 2; } + local exec_old new_exec + exec_old="$(svc_get exec)" + if printf '%s' "$exec_old" | grep -q -- '--config '; then + new_exec="$(printf '%s' "$exec_old" | sed -E 's/(^| )--config [^ ]+//; s/ +/ /g; s/^ //; s/ $//')" + svc_set_key exec "$new_exec" + fi + svc_del_key conffile + hist_event set ok "$(read_pid)" "config unbind" + c_ok "[$ENV_NAME] 已移除配置文件(回命令行参数形态)" + if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then + do_stop; do_start || { c_err "[$ENV_NAME] 重启失败——移除已登记,排查后 start"; return 1; } + else + c_info "[$ENV_NAME] 未运行或 --no-restart——下次 start 生效" + fi + return 0 + fi + + # 更新。 + [ -f "$target" ] || { c_err "配置文件不存在:$target"; return 2; } + local abs + abs="$(cd "$(dirname "$target")" && pwd)/$(basename "$target")" + if [ -x "$ENV_BIN" ]; then + if ! "$ENV_BIN" config explain -config "$abs" >/dev/null 2>&1; then + c_err "[$ENV_NAME] 配置文件未通过 schema 校验:$abs" + "$ENV_BIN" config explain -config "$abs" >&2 || true + c_err "(键与取值以 config explain 为准;先修正文件再切换)" + return 2 + fi + if ! "$ENV_BIN" serve -h 2>&1 | grep -Eq -- "(^|[[:space:]])-{1,2}config([[:space:]]|=|$)"; then + c_warn "[$ENV_NAME] 当前产物不识别 --config(旧版本二进制)——绑定已登记,但重启将忽略该参数" + fi + fi + local exec_old new_exec + exec_old="$(svc_get exec)" + if printf '%s' "$exec_old" | grep -q -- '--config '; then + new_exec="$(printf '%s' "$exec_old" | sed -E "s|--config [^ ]+|--config $abs|")" + else + new_exec="${exec_old:+$exec_old }--config $abs" + fi + svc_set_key exec "$new_exec" + svc_set_key conffile "$abs" + hist_event set ok "$(read_pid)" "config → $abs" + c_ok "[$ENV_NAME] 配置文件已更新:$abs(schema 校验通过;svc.env 600 保存)" + if [ "$restart" = 1 ] && [ -n "$(read_pid)" ] && pid_alive "$(read_pid)"; then + c_info "[$ENV_NAME] 重启以应用新配置……" + do_stop + do_start || { c_err "[$ENV_NAME] 重启失败——绑定已登记,排查日志后 scripts/svc.sh $ENV_NAME start"; return 1; } + c_ok "[$ENV_NAME] 新配置已生效:$abs" + else + c_info "[$ENV_NAME] 未运行或 --no-restart——下次 start 生效" + fi + return 0 +} + # ---- agent 服务的数据库绑定( db ...)---- # svc_db_bind [pg名] [--db X] [--clear] [--no-restart]:查看 / 绑定切换 / 解除 @@ -2183,13 +2322,21 @@ registered_names() { } # db_ref_of NAME:agent 服务的数据库绑定标识(list/status/info 共用)。 -# dbref= 优先;否则从 exec= 的 --dsn 提取 host:port 匹配已登记 postgres 实例 -# (兼容直接 --dsn 部署、尚未登记 dbref 的服务);都匹配不上则回 外部@host:port。 +# dbref= 优先;否则从 exec= 的 --dsn 或配置文件(conffile=)的 dsn 键提取 host:port +# 匹配已登记 postgres 实例(兼容直接 --dsn/--config 部署、尚未登记 dbref 的服务); +# 都匹配不上则回 外部@host:port。 db_ref_of() { - local name="$1" ref dsn hostport pg addr + local name="$1" ref dsn hostport pg addr cf ref="$(env_kv_of "$name" dbref)" if [ -n "$ref" ]; then printf '%s' "$ref"; return 0; fi dsn="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')" + if [ -z "$dsn" ]; then + cf="$(env_kv_of "$name" conffile)" + [ -z "$cf" ] && cf="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')" + if [ -n "$cf" ] && [ -f "$cf" ]; then + dsn="$(grep -oE '"dsn"[[:space:]]*:[[:space:]]*"[^"]*"' "$cf" 2>/dev/null | head -1 | sed -E 's/.*"([^"]*)"$/\1/')" + fi + fi [ -n "$dsn" ] || return 0 hostport="$(printf '%s' "$dsn" | sed -n 's#.*@\([^/?]*\).*#\1#p')" while IFS= read -r pg; do @@ -2250,8 +2397,12 @@ env_status_detail() { [ -n "$tags" ] && printf ' 标签 %s\n' "$tags" case "$kind" in agent) - local dbref; dbref="$(db_ref_of "$ENV_NAME")" - printf ' 数据库 %s\n' "${dbref:-无(内嵌 SQLite 档;绑定: db )}" ;; + local dbref cfile + dbref="$(db_ref_of "$ENV_NAME")" + printf ' 数据库 %s\n' "${dbref:-无(内嵌 SQLite 档;绑定: db )}" + cfile="$(svc_get conffile)" + [ -z "$cfile" ] && cfile="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')" + printf ' 配置文件 %s\n' "${cfile:-无(命令行参数形态; config 绑定)}" ;; postgres) printf ' 库 %s 用户 %s\n' "$(svc_get dbname)" "$(svc_get user)" printf ' DSN postgresql://%s:***@%s:%s/%s(db dsn %s --reveal 看密码)\n' \ @@ -2419,11 +2570,27 @@ info_pg_block() { return 0 } -# info_config_block NAME:单服务 svc.env 内容快照(文件路径+权限+逐行内容)。 +# info_config_block NAME:单服务 svc.env 内容快照(文件路径+权限+逐行内容), +# agent 服务另附其配置文件(conffile)的掩码内容快照。 info_config_block() { local name="$1" f="$RUNTIME/$1/svc.env" printf ' [%s] %s(权限 %s,kind=%s)\n' "$name" "$f" "$(info_file_mode "$f")" "$(svc_kind "$name")" info_snapshot_lines "$f" + local cf + cf="$(env_kv_of "$name" conffile)" + [ -z "$cf" ] && cf="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')" + if [ -n "$cf" ]; then + if [ -f "$cf" ]; then + printf ' ── 配置文件 %s(权限 %s)──\n' "$cf" "$(info_file_mode "$cf")" + if [ "$CLI_REVEAL" = 1 ]; then + cat "$cf" | sed 's/^/ /' + else + mask_json "$(cat "$cf")" | sed 's/^/ /' + fi + else + printf ' ── 配置文件 %s:文件不存在(重启将失败,恢复或 %s config --clear)──\n' "$cf" "$name" + fi + fi return 0 } @@ -2769,15 +2936,18 @@ main() { # 动作词可省略(省略 = deploy);flags 可直接跟随服务名。 local action="deploy" case "${1:-}" in - deploy|stop|start|restart|status|logs|db) action="$1"; shift ;; + deploy|stop|start|restart|status|logs|db|config) action="$1"; shift ;; esac if [ "$action" = logs ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then CLI_LOGS_N="$1"; shift fi - # db 绑定子命令:自带参数解析(位置参数 不走 parse_args;未知 flags 由其自行拒绝)。 - if [ "$action" = db ]; then + # db / config 子命令:自带参数解析(位置参数不走 parse_args;未知 flags 由其自行拒绝)。 + if [ "$action" = db ] || [ "$action" = config ]; then env_config "$ENV_NAME" - svc_db_bind "$@" || exit $? + case "$action" in + db) svc_db_bind "$@" || exit $? ;; + config) svc_config "$@" || exit $? ;; + esac exit 0 fi parse_args "$@" From dc7a255f314b1afa35c25a2d0086845945520170 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 4 Oct 2026 13:43:44 +0000 Subject: [PATCH 5/6] Record the config-switching acceptance evidence in the svc-db-bind task card Why: completion statement carries precheck/task-sync/test evidence. Evidence: make test exit 0 at head ecfed7b; functional evidence in the card. Impact: card-only; PR description mirrors this card. Refs: .task/feature-svc-db-bind.md --- .task/feature-svc-db-bind.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.task/feature-svc-db-bind.md b/.task/feature-svc-db-bind.md index 5d245896..a333a49a 100644 --- a/.task/feature-svc-db-bind.md +++ b/.task/feature-svc-db-bind.md @@ -61,7 +61,9 @@ info 配置节文件内容快照、info 数据库节"绑定服务";手删 dbref 模拟旧部署 → 反查仍显示; - adjust 修复:正例(帮助含 `-dsn` 判支持)与负例(无 dsn 判不支持)均正确; - 回归:db dsn(掩码)/ db ls / info / history(新增 `set ok db bind →` / `config →` / - `config unbind` 事件)/ --help 正常;测试现场(t-app/bpg、临时 JSON)已清除。 + `config unbind` 事件)/ --help 正常;测试现场(t-app/bpg、临时 JSON)已清除; + - 正式验收(含配置文件更新提交):`make test` 于 head ecfed7b exit 0 + (boundaries/pins/libs/core/backend/frontend/ui/agentcmd 全过);另有前次 843495e 全过记录。 - 正式验收:`make test` exit 0(test-boundaries/pins/libs/core/backend/frontend/ui/agentcmd 全过)。 - 影响与剩余项:list 表加宽一列;老脚本无兼容性问题(dbref 缺省视为未绑定); 发现并修复的 adjust_args_for_bin 缺陷影响既有 `deploy --dsn/--auth` 路径(此前被静默丢弃)。 From 83fd6f5f35767497ed4cba066a915f7386fa6758 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 4 Oct 2026 13:43:45 +0000 Subject: [PATCH 6/6] Record the sync checkpoint --- .task/feature-svc-db-bind.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.task/feature-svc-db-bind.md b/.task/feature-svc-db-bind.md index a333a49a..389385e6 100644 --- a/.task/feature-svc-db-bind.md +++ b/.task/feature-svc-db-bind.md @@ -69,3 +69,4 @@ 发现并修复的 adjust_args_for_bin 缺陷影响既有 `deploy --dsn/--auth` 路径(此前被静默丢弃)。 剩余项无。 | 同步前验收 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | 843495e0d0618b900d727e38570ff61451068c9b | 2026-10-04T13:30:59Z | +| 同步前验收 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | dc7a255f314b1afa35c25a2d0086845945520170 | 2026-10-04T13:43:45Z |