Add per-service database binding and fix single-dash flag detection in svc.sh #182

Merged
weichen merged 6 commits from feature/svc-db-bind into main 2026-10-04 13:44:02 +00:00
3 changed files with 522 additions and 21 deletions
+72
View File
@@ -0,0 +1,72 @@
# feature/svc-db-bind 任务卡
任务主记录:<PR 链接,task-finish 时填写>;状态:进行中;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
- 授权与交付终点:实现"服务↔数据库绑定"(svc.sh 服务级 db 子命令 + info/list 显示绑定)与
"服务配置文件更新/即时生效"(config 子命令),并修复 adjust_args_for_bin 单横线误判;
终点 = PR 合入 main(用户指示完成后一并合入;附 precheck / task-sync / 测试证据)
## 目标与非目标
目标:
- **数据库绑定**:agent 服务可绑定/切换/解除数据库:`svc.sh <name> db [pg名 [--db X] | --clear]
[--no-restart]`;缺省查看当前绑定与可用实例。绑定 = DSN 写入持久化 exec= + dbref=<pg>/<库>
登记,实例运行中缺库自动建,服务运行中自动重启生效;--clear 回内嵌 SQLite 档。
- **配置文件更新**:`svc.sh <name> 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/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 负责校验/切换/生效)。
## 同步记录
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|---|---|---|---|
| 启动 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | 2026-10-04T13:23:33Z |
## 结果与验证(完成后填写)
- 结果:已实现(数据库绑定与配置文件更新两块目标全部落地;`<name> db`/`<name> 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 张表**(服务真实
落库,非仅配置);`--db t2` 切换自动建库(t2 库 21 张表);`--clear` 干净回内嵌档;
`--no-restart` 只改登记;错误路径(未登记实例/多余参数/postgres 服务)均 exit 2;
- 配置文件更新:坏文件(未知键 bogus)被 schema 校验拒绝且不动 exec、不重启;合法文件
绑定后 cmdline 含 `--config <abs>`、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 →` / `config →` /
`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` 路径(此前被静默丢弃)。
剩余项无。
| 同步前验收 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | 843495e0d0618b900d727e38570ff61451068c9b | 2026-10-04T13:30:59Z |
| 同步前验收 | 3836ea88e3954878f257b7708ae5ccef947ccd79 | dc7a255f314b1afa35c25a2d0086845945520170 | 2026-10-04T13:43:45Z |
+60 -10
View File
@@ -64,10 +64,13 @@ 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@<sha> \
--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 # 全服务总览
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 +219,48 @@ scripts/svc.sh rm <name> --purge # 连 runtime/<name>/ 整个目录一起删
只管理本脚本登记过的服务;未登记名字会明确报错。
### 4.5 db(agent 服务的数据库绑定/切换)
```bash
scripts/svc.sh <agent名> db # 查看当前绑定 + 可用实例
scripts/svc.sh <agent名> db <pg名> [--db X] # 绑定/切换数据库
scripts/svc.sh <agent名> db <pg名> --no-restart # 只改登记,下次 start 生效
scripts/svc.sh <agent名> db --clear # 解除绑定(回内嵌 SQLite 档)
```
每个 agent 服务都可以对应一个数据库实例中的库;绑定后 DSN 持久化在 `svc.env`
(`exec=` 的 `--dsn` + `dbref=<pg名>/<库名>`,权限 600),跨重启保留。
- **缺库自动建**:实例运行中且目标库不存在时自动 `CREATE DATABASE`(幂等);
实例未运行且目标库非其默认库 → 提示先 `start` 实例;
- **运行中自动重启生效**(复用新 `exec=`);`--no-restart` 只写登记;
- **旧产物预检**:当前二进制不识别 `--dsn` 时警告(绑定已登记但重启按内嵌档跑);
- 展示:`list` 的 **DB 列**、`status` 的"数据库"行、`info` 数据库节的**绑定服务**行
(反向映射:哪些服务指向该实例)都能看到服务↔数据库对应关系;
- 未用本命令、直接 `deploy --dsn` 的服务,显示层会按 DSN 的 host:port 反查匹配已
登记实例(显示 `<pg名>/<库>`;匹配不上显示 `外部@host:port`);
- `--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. 命令参考:部署来源
`deploy --from` 支持三种来源(**缺省 = `dev` 远端最新**),agent 与 static 共用:
@@ -287,12 +332,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 服务当前绑定的 `<pg名>/<库名>`(见 §4.5;外部 DSN 显示 `外部@host:port`,
未绑定显示 `-`)。
STATE 的判定:pidfile 存在且 `/proc/<pid>/cmdline` 匹配登记的 `pidcheck` 标记
(agent=二进制路径、postgres=pgdata 路径、static=`--directory <目录>`)。进程被人手动
杀掉后 STATE 自动落回"未运行",不会误报。
@@ -315,10 +363,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/<name>/svc.env` 的文件路径、权限与逐行内容快照,加上
全局层:`.svc.env`(自动加载层)、本次 `--config/--env` 文件、shell 已导出的
`SVC_*` 变量;
@@ -549,7 +597,9 @@ scripts/svc.sh prod deploy --env .env.prod # 等价
| `version` | 产物版本串(`agent-system version` 输出) |
| `started` | 最近启动时间 |
| `pidcheck` | 进程判定标记(见 §6.1) |
| `exec` | agent 上次完整启动参数 |
| `exec` | agent 上次完整启动参数(含 `--dsn`,密码在其中) |
| `dbref` | agent 数据库绑定 `<pg名>/<库名>`(`<name> db` 命令写入,跨重启保留;见 §4.5) |
| `conffile` | agent 配置文件绝对路径(`<name> config` 命令写入,跨重启保留;见 §4.6) |
| `user` / `password` / `dbname` / `authmode` / `pgdata` | postgres 专属 |
| `htdocs` | static 专属 |
@@ -563,7 +613,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 → <pg>/<库>` / `db unbind`) |
| `backup` | ok / fail | 备份(文件、大小) |
| `restore` | ok / fail | 恢复(库、文件、表数) |
| `clone` | ok / fail | 克隆(物理/在线) |
+390 -11
View File
@@ -5,6 +5,8 @@
#
# ════════════════════ 命令一览 ════════════════════
# scripts/svc.sh <name> [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 <name> [--port N --apply] [--tag T] 变更登记(换端口/标签)
@@ -39,6 +41,10 @@
# --kind agent|static deploy 时指定类型(postgres 用 db create 建)
# --dir PATH static 直接服务既有目录(不构建)
# --plugins/--dsn/--auth/--mock/--data agent 运行参数
# <name> db <pg名> [--db X] [--no-restart] agent 绑定/切换数据库(实例运行中缺库自动建;
# --clear 解绑回内嵌 SQLite;无参查看当前绑定与可用实例)
# <name> config <file.json> [--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
@@ -69,6 +75,11 @@
# 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 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 <dump> --db app --force
# scripts/svc.sh list --tag core && scripts/svc.sh history
@@ -123,6 +134,8 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po
scripts/svc.sh <name> deploy [--from dev@<sha>] 部署并启动(缺省=远端 main 最新)
scripts/svc.sh <name> start|stop|restart|logs [N] 生命周期与日志
scripts/svc.sh <name> status 服务详情(版本/来源/DSN 等)
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 dsn|backup|backups|restore|clone|copy|ls|set pg1 ... 数据库运维
scripts/svc.sh set pg1 --port 8895 --apply 换端口(--tag 改标签)
@@ -253,6 +266,14 @@ EOF
scripts/svc.sh <name> start 启动(agent/static/postgres 分别分派;已在运行则提示)
scripts/svc.sh <name> stop 停止(agent/static: TERM→KILL;postgres: pg_ctl -m fast)
scripts/svc.sh <name> restart 停止 + 启动(裸 restart 复用上次配置)
scripts/svc.sh <name> db [pg名|--clear] [--db X] agent 数据库绑定:缺省查看当前
绑定与可用实例;给 pg名=绑定/切换(DSN 写入 exec=,dbref= 登记,
实例运行中缺库自动建,服务运行中自动重启生效);--clear 解绑回内嵌
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> status 单服务详情(kind/状态/地址/来源/版本/标签/DSN/目录)
scripts/svc.sh list [--tag T] 全服务表格:NAME KIND STATE PID ADDR SOURCE TAGS
@@ -308,6 +329,14 @@ EOF
连接其他服务:DSN 可直接组合进 agent 部署——
scripts/svc.sh prod deploy --from dev@<sha> --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/<name>/svc.env(600);--password 未给时自动生成
(head -c 18 /dev/urandom)。auth-mode 默认 scram-sha-256(pg_hba 全线)。
@@ -375,6 +404,11 @@ dotenv 格式:KEY=VALUE,支持 # 注释、成对引号、export 前缀;只
- 裸 start/restart 复用 exec=(上次完整参数);任何显式参数(或 --config 文件、
SVC_* 变量)出现即整体重建参数
- 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
return 0 ;;
examples)
@@ -625,14 +659,17 @@ 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/
# conffile(跨重启保留的数据库绑定与配置文件);kind 特有键(user/password/dbname/
# authmode/pgdata/htdocs...)经参数追加。
record_state() {
# 先读完旧文件再开重定向(块内 {} > svc.env 会先截断文件,读旧值必须在其之前)。
local pid prev_src 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"
@@ -647,6 +684,8 @@ 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"
[ -n "$prev_conffile" ] && printf 'conffile=%s\n' "$prev_conffile"
[ $# -gt 0 ] && printf '%s\n' "$@"
} > "$ENV_DIR/svc.env"
chmod 600 "$ENV_DIR/svc.env"
@@ -769,14 +808,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 +1454,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 <agent名> db $ENV_NAME [--db X](运行中自动重启生效)"
return 0
}
@@ -1541,6 +1584,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 +1913,259 @@ 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 服务的配置文件(<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 ...)----
# svc_db_bind [pg名] [--db X] [--clear] [--no-restart]:查看 / 绑定切换 / 解除
# agent 服务的数据库。绑定 = 目标实例的 DSN 写入持久化 exec=(--dsn,密码 600 存放)
# 并登记 dbref=<pg>/<库>;实例运行中时确保目标库存在(缺则建);服务运行中则自动
# 重启生效(--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 <<EOF
[$ENV_NAME] 数据库绑定:
scripts/svc.sh $ENV_NAME db 查看当前绑定与可用实例
scripts/svc.sh $ENV_NAME db <pg名> [--db X] [--no-restart] 绑定/切换(运行中自动重启生效)
scripts/svc.sh $ENV_NAME db --clear [--no-restart] 解除绑定(回内嵌 SQLite 档)
EOF
return 0 ;;
-*) c_err "[$ENV_NAME] db 未知参数:$1(可用:<pg名> | --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 <name> 创建)\n'
printf ' 切换:scripts/svc.sh %s db <pg名> [--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 <name> [--port N] [--host H] [--apply] [--tag T...]"; exit 2; }
@@ -2015,10 +2321,40 @@ registered_names() {
return 0
}
# db_ref_of NAME:agent 服务的数据库绑定标识(list/status/info 共用)。
# dbref= 优先;否则从 exec= 的 --dsn 或配置文件(conffile=)的 dsn 键提取 host:port
# 匹配已登记 postgres 实例(兼容直接 --dsn/--config 部署、尚未登记 dbref 的服务);
# 都匹配不上则回 外部@host:port。
db_ref_of() {
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
[ "$(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 +2366,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 +2396,13 @@ env_status_detail() {
[ -n "$src" ] && printf ' 来源 %s 版本 %s\n' "$src" "${ver:--}"
[ -n "$tags" ] && printf ' 标签 %s\n' "$tags"
case "$kind" in
agent)
local dbref cfile
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)
printf ' 库 %s 用户 %s\n' "$(svc_get dbname)" "$(svc_get user)"
printf ' DSN postgresql://%s:***@%s:%s/%s(db dsn %s --reveal 看密码)\n' \
@@ -2185,6 +2531,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 <agent名> db $name)}"
[ -n "$pgdata" ] && printf ' 数据目录 %s(%s)\n' "$pgdata" "$(info_du "$pgdata")"
local nb=0 bsize="" latest="" f
if [ -d "$d/backups" ]; then
@@ -2216,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
}
@@ -2566,11 +2936,20 @@ 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|config) action="$1"; shift ;;
esac
if [ "$action" = logs ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
CLI_LOGS_N="$1"; shift
fi
# db / config 子命令:自带参数解析(位置参数不走 parse_args;未知 flags 由其自行拒绝)。
if [ "$action" = db ] || [ "$action" = config ]; then
env_config "$ENV_NAME"
case "$action" in
db) svc_db_bind "$@" || exit $? ;;
config) svc_config "$@" || exit $? ;;
esac
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 动作"