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 <same file>' 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 <name> 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
This commit is contained in:
2026-10-04 13:42:51 +00:00
parent 73bca5e749
commit ecfed7be31
3 changed files with 241 additions and 34 deletions
+36 -21
View File
@@ -6,25 +6,33 @@
- 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 3836ea88e3954878f257b7708ae5ccef947ccd79 - 权威仓: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 - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-db-bind(worktree);分支:feature/svc-db-bind
- 授权与交付终点:实现"服务↔数据库绑定"(svc.sh 服务级 db 子命令 + info/list 显示绑定)并修复 - 授权与交付终点:实现"服务↔数据库绑定"(svc.sh 服务级 db 子命令 + info/list 显示绑定)与
adjust_args_for_bin 单横线误判;终点 = PR 合入 main(附 precheck / task-sync / 测试证据) "服务配置文件更新/即时生效"(config 子命令),并修复 adjust_args_for_bin 单横线误判;
终点 = PR 合入 main(用户指示完成后一并合入;附 precheck / task-sync / 测试证据)
## 目标与非目标 ## 目标与非目标
目标: 目标:
- agent 服务可绑定/切换/解除数据库:`svc.sh <name> db [pg名 [--db X] | --clear] [--no-restart]`; - **数据库绑定**:agent 服务可绑定/切换/解除数据库:`svc.sh <name> db [pg名 [--db X] | --clear]
缺省查看当前绑定与可用实例。绑定 = DSN 写入持久化 exec= + dbref=<pg>/<库> 登记,实例运行中 [--no-restart]`;缺省查看当前绑定与可用实例。绑定 = DSN 写入持久化 exec= + dbref=<pg>/<库>
缺库自动建,服务运行中自动重启生效;--clear 回内嵌 SQLite 档。 登记,实例运行中缺库自动建,服务运行中自动重启生效;--clear 回内嵌 SQLite 档。
- 显示层全链路:list 表新增 DB 列;status 详情新增"数据库"行;info 数据库节新增"绑定服务" - **配置文件更新**:`svc.sh <name> config [file.json |--clear] [--no-restart]`;缺省查看当前
反向映射;无 dbref 的旧服务按 DSN host:port 反查匹配实例(外部 DSN 显示 外部@host:port)。 配置文件(掩码内容 + `agent-system config explain -config` 视角的生效值)。切换前经二进制
- 修复既有缺陷:adjust_args_for_bin 以 grep '--dsn' 检测旗标支持,Go flag 帮助打印单横线 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 重建参数时被误删——改为"去横线旗标名 + `-dsn`,导致现役二进制的 --dsn/--auth 在 deploy 重建参数时被误删——改为"去横线旗标名 +
1-2 横线"匹配。 1-2 横线"匹配。
- record_state 跨重启保留 dbref;svc_del_key 辅助;db create 尾部提示绑定命令; - record_state 跨重启保留 dbref/conffile;svc_del_key 辅助;db create 尾部提示绑定命令;
usage/usage_short/help(lifecycle/db)/docs/svc.md 同步。 usage/usage_short/help(lifecycle/config/db)/docs/svc.md(§2/§4.5/§4.6/§6/§10.1)同步。
非目标:不改 agent 二进制与部署/生命周期语义;不做 static/postgres 的绑定;不做运行中热切换 非目标:不改 agent 二进制与部署/生命周期语义;不做 static/postgres 的绑定;不做运行中热切换
(切换经重启生效)。 (切换经重启生效);不做配置文件的编辑器(内容仍由用户维护,svc.sh 负责校验/切换/生效)。
## 同步记录 ## 同步记录
@@ -34,19 +42,26 @@
## 结果与验证(完成后填写) ## 结果与验证(完成后填写)
- 结果:已实现(上列目标全部落地;`<name> db` 服务级动作与顶层 `db` 命令并存无冲突)。 - 结果:已实现(数据库绑定与配置文件更新两块目标全部落地;`<name> db`/`<name> config`
服务级动作与顶层 `db` 命令、`--config` 旗标并存无冲突)。
- 验证(worktree runtime 实测,真实 agent 二进制 + PostgreSQL 18,2026-10-04): - 验证(worktree runtime 实测,真实 agent 二进制 + PostgreSQL 18,2026-10-04):
- `bash -n` 通过; - `bash -n` 通过;
- 绑定:`t-app db bpg` → exec= 写入 --dsn、dbref=bpg/agent、自动重启生效;**record_state - 数据库绑定:`t-app db bpg` → exec= 写入 --dsn、dbref=bpg/agent、自动重启生效;
重写 svc.env 后 dbref 保留**;PG 的 agent 库出现 **21 张表**(服务真实落库,非仅配置); **record_state 重写 svc.env 后 dbref 保留**;PG 的 agent 库出现 **21 张表**(服务真实
- 切换:`t-app db bpg --db t2` → 自动建库 t2、重启、t2 库 21 张表; 落库,非仅配置);`--db t2` 切换自动建库(t2 库 21 张表);`--clear` 干净回内嵌档;
- 解绑:`db --clear` → dbref/--dsn 移除、重启回内嵌档; `--no-restart` 只改登记;错误路径(未登记实例/多余参数/postgres 服务)均 exit 2;
- `--no-restart` 只改登记;错误路径(未登记实例/多余参数/postgres 服务)均 exit 2 且提示正确; - 配置文件更新:坏文件(未知键 bogus)被 schema 校验拒绝且不动 exec、不重启;合法文件
- 显示层:list DB 列(t-app→bpg/agent)、`t-app db` 查看(掩码 DSN+实例列表)、info 数据库节 绑定后 cmdline 含 `--config <abs>`、conffile= 跨 record_state 保留;**配置文件内的 dsn
"绑定服务 t-app(agent)";手删 dbref 模拟旧部署 → list 按 DSN host:port 反查仍显示 bpg/agent; 真实生效**(错误密码版本在日志中报 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 判不支持)均正确; - adjust 修复:正例(帮助含 `-dsn` 判支持)与负例(无 dsn 判不支持)均正确;
- 回归:db dsn(掩码)/ db ls / info / history(新增 `set ok db bind → bpg/agent` 事件)/ - 回归:db dsn(掩码)/ db ls / info / history(新增 `set ok db bind →` / `config →` /
--help 正常;测试现场(t-app/bpg)已 rm --purge 清除; `config unbind` 事件)/ --help 正常;测试现场(t-app/bpg、临时 JSON)已清除。
- 正式验收:`make test` exit 0(test-boundaries/pins/libs/core/backend/frontend/ui/agentcmd 全过)。 - 正式验收:`make test` exit 0(test-boundaries/pins/libs/core/backend/frontend/ui/agentcmd 全过)。
- 影响与剩余项:list 表加宽一列;老脚本无兼容性问题(dbref 缺省视为未绑定); - 影响与剩余项:list 表加宽一列;老脚本无兼容性问题(dbref 缺省视为未绑定);
发现并修复的 adjust_args_for_bin 缺陷影响既有 `deploy --dsn/--auth` 路径(此前被静默丢弃)。 发现并修复的 adjust_args_for_bin 缺陷影响既有 `deploy --dsn/--auth` 路径(此前被静默丢弃)。
+22
View File
@@ -65,6 +65,7 @@ scripts/svc.sh pg1 start # pg_ctl 启动 + 自动建库
scripts/svc.sh prod deploy --from dev@<sha> \ scripts/svc.sh prod deploy --from dev@<sha> \
--dsn "$(scripts/svc.sh db dsn pg1 --reveal)" # DSN 直接组合进部署 --dsn "$(scripts/svc.sh db dsn pg1 --reveal)" # DSN 直接组合进部署
scripts/svc.sh prod db pg1 --db prod # 或事后绑定/切换(缺库自动建,重启生效) scripts/svc.sh prod db pg1 --db prod # 或事后绑定/切换(缺库自动建,重启生效)
scripts/svc.sh prod config .svc/prod.json # 指定/更换配置文件(校验+重启即时生效)
# 日常操作 # 日常操作
scripts/svc.sh list # 全服务总览(含 DB 绑定列) scripts/svc.sh list # 全服务总览(含 DB 绑定列)
@@ -240,6 +241,26 @@ scripts/svc.sh <agent名> db --clear # 解除绑定(回内嵌 SQL
登记实例(显示 `<pg名>/<库>`;匹配不上显示 `外部@host:port`); 登记实例(显示 `<pg名>/<库>`;匹配不上显示 `外部@host:port`);
- `--clear` 后服务回到内嵌 SQLite 档(数据在原 `--data` 目录,切回即见旧数据)。 - `--clear` 后服务回到内嵌 SQLite 档(数据在原 `--data` 目录,切回即见旧数据)。
### 4.6 config(agent 服务的配置文件更新/生效)
```bash
scripts/svc.sh <agent名> config # 查看当前配置文件(掩码内容 + agent 视角生效值)
scripts/svc.sh <agent名> config <file.json> # 更换使用的配置文件(校验通过才切换)
scripts/svc.sh <agent名> config <file> --no-restart # 只改登记,下次 start 生效
scripts/svc.sh <agent名> config --clear # 移除(回命令行参数形态)
```
配置文件是 agent 二进制的 `--config`(JSON,schema 严格校验;可用
`agent-system config explain -config <file>` 预览每个键的来源与生效值)。
- **切换前强校验**:文件必须存在且通过二进制的 schema 校验(未知键/非法值直接拒绝,
不会把服务重启进坏配置);旧产物不识别 `--config` 时警告;
- **即时生效**:运行中的服务自动重启应用新配置(agent 无 SIGHUP 热重载,重启秒级);
绑定后日常改配置 = 编辑文件 + `restart`(或重跑 `config <同一文件>` 再校验一遍);
- 持久化:`exec=` 的 `--config <绝对路径>` + `conffile=` 键(跨重启保留,600);
- 展示:`status`/`info <name>` 的"配置文件"行、`info` 配置节的文件内容快照
(敏感值掩码,`info --reveal` 显示明文);文件丢失时明确告警并提示恢复或 `--clear`。
## 5. 命令参考:部署来源 ## 5. 命令参考:部署来源
`deploy --from` 支持三种来源(**缺省 = `dev` 远端最新**),agent 与 static 共用: `deploy --from` 支持三种来源(**缺省 = `dev` 远端最新**),agent 与 static 共用:
@@ -578,6 +599,7 @@ scripts/svc.sh prod deploy --env .env.prod # 等价
| `pidcheck` | 进程判定标记(见 §6.1) | | `pidcheck` | 进程判定标记(见 §6.1) |
| `exec` | agent 上次完整启动参数(含 `--dsn`,密码在其中) | | `exec` | agent 上次完整启动参数(含 `--dsn`,密码在其中) |
| `dbref` | agent 数据库绑定 `<pg名>/<库名>`(`<name> db` 命令写入,跨重启保留;见 §4.5) | | `dbref` | agent 数据库绑定 `<pg名>/<库名>`(`<name> db` 命令写入,跨重启保留;见 §4.5) |
| `conffile` | agent 配置文件绝对路径(`<name> config` 命令写入,跨重启保留;见 §4.6) |
| `user` / `password` / `dbname` / `authmode` / `pgdata` | postgres 专属 | | `user` / `password` / `dbname` / `authmode` / `pgdata` | postgres 专属 |
| `htdocs` | static 专属 | | `htdocs` | static 专属 |
+183 -13
View File
@@ -6,6 +6,7 @@
# ════════════════════ 命令一览 ════════════════════ # ════════════════════ 命令一览 ════════════════════
# scripts/svc.sh <name> [action] [参数...] 服务级:deploy(默认)|start|stop|restart|status|logs [N] # scripts/svc.sh <name> [action] [参数...] 服务级:deploy(默认)|start|stop|restart|status|logs [N]
# |db [pg名|--clear](agent 数据库绑定/切换) # |db [pg名|--clear](agent 数据库绑定/切换)
# |config [file|--clear](agent 配置文件更新/生效)
# scripts/svc.sh list [--tag T] 服务总览(status 等价) # scripts/svc.sh list [--tag T] 服务总览(status 等价)
# scripts/svc.sh info [name] [--tag T] [--reveal] 综合报告(只读):服务/数据库/配置快照/最近事件 # scripts/svc.sh info [name] [--tag T] [--reveal] 综合报告(只读):服务/数据库/配置快照/最近事件
# scripts/svc.sh set <name> [--port N --apply] [--tag T] 变更登记(换端口/标签) # scripts/svc.sh set <name> [--port N --apply] [--tag T] 变更登记(换端口/标签)
@@ -42,6 +43,8 @@
# --plugins/--dsn/--auth/--mock/--data agent 运行参数 # --plugins/--dsn/--auth/--mock/--data agent 运行参数
# <name> db <pg名> [--db X] [--no-restart] agent 绑定/切换数据库(实例运行中缺库自动建; # <name> db <pg名> [--db X] [--no-restart] agent 绑定/切换数据库(实例运行中缺库自动建;
# --clear 解绑回内嵌 SQLite;无参查看当前绑定与可用实例) # --clear 解绑回内嵌 SQLite;无参查看当前绑定与可用实例)
# <name> config <file.json> [--no-restart] agent 更换使用的配置文件(schema 校验通过才切换,
# 运行中自动重启即时生效;--clear 移除;无参查看当前配置与生效值)
# --model-base/--model-key/--model-name 与 --light-*/--tavily-keys 启动后播种设置 # --model-base/--model-key/--model-name 与 --light-*/--tavily-keys 启动后播种设置
# -- <剩余参数> 原样透传给 agent-system serve(escape hatch) # -- <剩余参数> 原样透传给 agent-system serve(escape hatch)
# db create:--user U(默认 agent)--password P(缺省自动生成)--db NAME --auth-mode scram-sha-256|md5 # 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 pg1 --db dev # 把 dev 服务绑定到 pg1/dev 库(自动重启)
# scripts/svc.sh dev db pg2 --no-restart # 切换但不重启(下次 start 生效) # scripts/svc.sh dev db pg2 --no-restart # 切换但不重启(下次 start 生效)
# scripts/svc.sh dev db --clear # 解绑回内嵌 SQLite 档 # 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 copy pg1 pg2 --db app # 把 pg1 的 app 库搬到 pg2
# scripts/svc.sh db restore pg2 --file <dump> --db app --force # scripts/svc.sh db restore pg2 --file <dump> --db app --force
# scripts/svc.sh list --tag core && scripts/svc.sh history # scripts/svc.sh list --tag core && scripts/svc.sh history
@@ -130,6 +135,7 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po
scripts/svc.sh <name> start|stop|restart|logs [N] 生命周期与日志 scripts/svc.sh <name> start|stop|restart|logs [N] 生命周期与日志
scripts/svc.sh <name> status 服务详情(版本/来源/DSN 等) scripts/svc.sh <name> status 服务详情(版本/来源/DSN 等)
scripts/svc.sh <name> db [pg名|--clear] agent 数据库绑定/切换(缺省查看) scripts/svc.sh <name> db [pg名|--clear] agent 数据库绑定/切换(缺省查看)
scripts/svc.sh <name> config [file|--clear] agent 配置文件更新/即时生效(缺省查看)
scripts/svc.sh db create pg1 --port 8890 建数据库 scripts/svc.sh db create pg1 --port 8890 建数据库
scripts/svc.sh db dsn|backup|backups|restore|clone|copy|ls|set pg1 ... 数据库运维 scripts/svc.sh db dsn|backup|backups|restore|clone|copy|ls|set pg1 ... 数据库运维
scripts/svc.sh set pg1 --port 8895 --apply 换端口(--tag 改标签) scripts/svc.sh set pg1 --port 8895 --apply 换端口(--tag 改标签)
@@ -264,6 +270,10 @@ EOF
绑定与可用实例;给 pg名=绑定/切换(DSN 写入 exec=,dbref= 登记, 绑定与可用实例;给 pg名=绑定/切换(DSN 写入 exec=,dbref= 登记,
实例运行中缺库自动建,服务运行中自动重启生效);--clear 解绑回内嵌 实例运行中缺库自动建,服务运行中自动重启生效);--clear 解绑回内嵌
SQLite 档;--no-restart 只改登记 SQLite 档;--no-restart 只改登记
scripts/svc.sh <name> config [file|--clear] [--no-restart] agent 配置文件:缺省查看当前
文件与生效值;给 file=更换(agent 二进制 --config,JSON schema 校验
通过才切换,运行中自动重启即时生效);--clear 移除回命令行参数形态。
绑定后改文件内容 + restart 即生效(agent 无热重载,重启秒级)
scripts/svc.sh <name> logs [N] 跟随日志(默认 50 行起;如 svc.sh pg1 logs 200) scripts/svc.sh <name> logs [N] 跟随日志(默认 50 行起;如 svc.sh pg1 logs 200)
scripts/svc.sh <name> status 单服务详情(kind/状态/地址/来源/版本/标签/DSN/目录) scripts/svc.sh <name> status 单服务详情(kind/状态/地址/来源/版本/标签/DSN/目录)
scripts/svc.sh list [--tag T] 全服务表格:NAME KIND STATE PID ADDR SOURCE TAGS 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 文件、 - 裸 start/restart 复用 exec=(上次完整参数);任何显式参数(或 --config 文件、
SVC_* 变量)出现即整体重建参数 SVC_* 变量)出现即整体重建参数
- tags 随每次登记合并去重,跨重启保留 - tags 随每次登记合并去重,跨重启保留
服务级配置文件(与上述 svc.sh dotenv 层不同——这是 agent 二进制自己的 JSON 配置):
scripts/svc.sh <name> config [file|--clear] 更换/移除/查看(见 help lifecycle);
切换前 schema 校验(agent-system config explain -config <file>),运行中自动重启
即时生效;登记于 svc.env conffile=,info/status 可见。
EOF EOF
return 0 ;; return 0 ;;
examples) examples)
@@ -644,15 +659,17 @@ svc_merge_tags() {
} }
# record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600)。 # record_state EXTRA_KEY=VALUE...:把当前态+持久化配置写入 svc.env(600)。
# 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref(跨重启 # 基本键:kind/env/pid/addr/tags/source/version/started/pidcheck/exec + dbref/
# 保留的数据库绑定);kind 特有键(user/password/dbname/authmode/pgdata/htdocs...)经参数追加。 # conffile(跨重启保留的数据库绑定与配置文件);kind 特有键(user/password/dbname/
# authmode/pgdata/htdocs...)经参数追加。
record_state() { record_state() {
# 先读完旧文件再开重定向(块内 {} > svc.env 会先截断文件,读旧值必须在其之前)。 # 先读完旧文件再开重定向(块内 {} > svc.env 会先截断文件,读旧值必须在其之前)。
local pid prev_src prev_dbref merged_tags local pid prev_src prev_dbref prev_conffile merged_tags
pid="$(read_pid)" pid="$(read_pid)"
prev_src="" prev_src=""
[ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)" [ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)"
prev_dbref="$(svc_get dbref)" prev_dbref="$(svc_get dbref)"
prev_conffile="$(svc_get conffile)"
SVC_SOURCE="${SRC_LABEL:-${prev_src:-}}" SVC_SOURCE="${SRC_LABEL:-${prev_src:-}}"
merged_tags="$(svc_merge_tags)" merged_tags="$(svc_merge_tags)"
mkdir -p "$ENV_DIR" mkdir -p "$ENV_DIR"
@@ -668,6 +685,7 @@ record_state() {
printf 'pidcheck=%s\n' "${SVC_PIDCHECK:-}" printf 'pidcheck=%s\n' "${SVC_PIDCHECK:-}"
printf 'exec=%s\n' "${SVC_EXEC:-}" printf 'exec=%s\n' "${SVC_EXEC:-}"
[ -n "$prev_dbref" ] && printf 'dbref=%s\n' "$prev_dbref" [ -n "$prev_dbref" ] && printf 'dbref=%s\n' "$prev_dbref"
[ -n "$prev_conffile" ] && printf 'conffile=%s\n' "$prev_conffile"
[ $# -gt 0 ] && printf '%s\n' "$@" [ $# -gt 0 ] && printf '%s\n' "$@"
} > "$ENV_DIR/svc.env" } > "$ENV_DIR/svc.env"
chmod 600 "$ENV_DIR/svc.env" chmod 600 "$ENV_DIR/svc.env"
@@ -1895,6 +1913,127 @@ db_ls() {
return 0 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 服务的配置文件(<name> 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 <<EOF
[$ENV_NAME] 配置文件:
scripts/svc.sh $ENV_NAME config 查看当前配置文件与生效值
scripts/svc.sh $ENV_NAME config <file.json> [--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 <file.json>)\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 服务的数据库绑定(<name> db ...)---- # ---- agent 服务的数据库绑定(<name> db ...)----
# svc_db_bind [pg名] [--db X] [--clear] [--no-restart]:查看 / 绑定切换 / 解除 # svc_db_bind [pg名] [--db X] [--clear] [--no-restart]:查看 / 绑定切换 / 解除
@@ -2183,13 +2322,21 @@ registered_names() {
} }
# db_ref_of NAME:agent 服务的数据库绑定标识(list/status/info 共用)。 # db_ref_of NAME:agent 服务的数据库绑定标识(list/status/info 共用)。
# dbref= 优先;否则从 exec= 的 --dsn 提取 host:port 匹配已登记 postgres 实例 # dbref= 优先;否则从 exec= 的 --dsn 或配置文件(conffile=)的 dsn 键提取 host:port
# (兼容直接 --dsn 部署、尚未登记 dbref 的服务);都匹配不上则回 外部@host:port。 # 匹配已登记 postgres 实例(兼容直接 --dsn/--config 部署、尚未登记 dbref 的服务);
# 都匹配不上则回 外部@host:port。
db_ref_of() { 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)" ref="$(env_kv_of "$name" dbref)"
if [ -n "$ref" ]; then printf '%s' "$ref"; return 0; fi if [ -n "$ref" ]; then printf '%s' "$ref"; return 0; fi
dsn="$(printf '%s' "$(env_kv_of "$name" exec)" | sed -n 's/.*--dsn \([^ ]*\).*/\1/p')" 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 [ -n "$dsn" ] || return 0
hostport="$(printf '%s' "$dsn" | sed -n 's#.*@\([^/?]*\).*#\1#p')" hostport="$(printf '%s' "$dsn" | sed -n 's#.*@\([^/?]*\).*#\1#p')"
while IFS= read -r pg; do while IFS= read -r pg; do
@@ -2250,8 +2397,12 @@ env_status_detail() {
[ -n "$tags" ] && printf ' 标签 %s\n' "$tags" [ -n "$tags" ] && printf ' 标签 %s\n' "$tags"
case "$kind" in case "$kind" in
agent) agent)
local dbref; dbref="$(db_ref_of "$ENV_NAME")" local dbref cfile
printf ' 数据库 %s\n' "${dbref:-无(内嵌 SQLite 档;绑定:<name> db <pg名>)}" ;; dbref="$(db_ref_of "$ENV_NAME")"
printf ' 数据库 %s\n' "${dbref:-无(内嵌 SQLite 档;绑定:<name> db <pg名>)}"
cfile="$(svc_get conffile)"
[ -z "$cfile" ] && cfile="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
printf ' 配置文件 %s\n' "${cfile:-无(命令行参数形态;<name> config <file> 绑定)}" ;;
postgres) postgres)
printf ' 库 %s 用户 %s\n' "$(svc_get dbname)" "$(svc_get user)" printf ' 库 %s 用户 %s\n' "$(svc_get dbname)" "$(svc_get user)"
printf ' DSN postgresql://%s:***@%s:%s/%s(db dsn %s --reveal 看密码)\n' \ printf ' DSN postgresql://%s:***@%s:%s/%s(db dsn %s --reveal 看密码)\n' \
@@ -2419,11 +2570,27 @@ info_pg_block() {
return 0 return 0
} }
# info_config_block NAME:单服务 svc.env 内容快照(文件路径+权限+逐行内容)。 # info_config_block NAME:单服务 svc.env 内容快照(文件路径+权限+逐行内容),
# agent 服务另附其配置文件(conffile)的掩码内容快照。
info_config_block() { info_config_block() {
local name="$1" f="$RUNTIME/$1/svc.env" local name="$1" f="$RUNTIME/$1/svc.env"
printf ' [%s] %s(权限 %s,kind=%s)\n' "$name" "$f" "$(info_file_mode "$f")" "$(svc_kind "$name")" printf ' [%s] %s(权限 %s,kind=%s)\n' "$name" "$f" "$(info_file_mode "$f")" "$(svc_kind "$name")"
info_snapshot_lines "$f" 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 return 0
} }
@@ -2769,15 +2936,18 @@ main() {
# 动作词可省略(省略 = deploy);flags 可直接跟随服务名。 # 动作词可省略(省略 = deploy);flags 可直接跟随服务名。
local action="deploy" local action="deploy"
case "${1:-}" in 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 esac
if [ "$action" = logs ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then if [ "$action" = logs ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
CLI_LOGS_N="$1"; shift CLI_LOGS_N="$1"; shift
fi fi
# db 绑定子命令:自带参数解析(位置参数 <pg名> 不走 parse_args;未知 flags 由其自行拒绝)。 # db / config 子命令:自带参数解析(位置参数不走 parse_args;未知 flags 由其自行拒绝)。
if [ "$action" = db ]; then if [ "$action" = db ] || [ "$action" = config ]; then
env_config "$ENV_NAME" env_config "$ENV_NAME"
svc_db_bind "$@" || exit $? case "$action" in
db) svc_db_bind "$@" || exit $? ;;
config) svc_config "$@" || exit $? ;;
esac
exit 0 exit 0
fi fi
parse_args "$@" parse_args "$@"