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 <name> 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
This commit is contained in:
2026-10-04 13:30:50 +00:00
parent d1aebd9919
commit 843495e0d0
3 changed files with 290 additions and 25 deletions
+33 -5
View File
@@ -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 <name> db [pg名 [--db X] | --clear] [--no-restart]`;
缺省查看当前绑定与可用实例。绑定 = DSN 写入持久化 exec= + dbref=<pg>/<库> 登记,实例运行中
缺库自动建,服务运行中自动重启生效;--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 @@
## 结果与验证(完成后填写)
- 结果:
- 验证:
- 影响与剩余项:
- 结果:已实现(上列目标全部落地;`<name> 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` 路径(此前被静默丢弃)。
剩余项无。
+38 -10
View File
@@ -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@<sha> \
--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 <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` 目录,切回即见旧数据)。
## 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 服务当前绑定的 `<pg名>/<库名>`(见 §4.5;外部 DSN 显示 `外部@host:port`,
未绑定显示 `-`)。
STATE 的判定:pidfile 存在且 `/proc/<pid>/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/<name>/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 数据库绑定 `<pg名>/<库名>`(`<name> 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 → <pg>/<库>` / `db unbind`) |
| `backup` | ok / fail | 备份(文件、大小) |
| `restore` | ok / fail | 恢复(库、文件、表数) |
| `clone` | ok / fail | 克隆(物理/在线) |
+219 -10
View File
@@ -5,6 +5,7 @@
#
# ════════════════════ 命令一览 ════════════════════
# scripts/svc.sh <name> [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 <name> [--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 运行参数
# <name> db <pg名> [--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 <dump> --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 <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 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 <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> 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 +319,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 全线)。
@@ -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 <agent名> 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 服务的数据库绑定(<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 +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 档;绑定:<name> db <pg名>)}" ;;
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 <agent名> 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 绑定子命令:自带参数解析(位置参数 <pg名> 不走 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 动作"