Files
dsh_ai1net_server/交付物/S5-S6真机E2E验收-20260923.md
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

20 KiB
Raw Permalink Blame History

S5 / S6 真机 E2E 验收 — 部署落地(第 8 棒)

线:插件投放与分库线 | 棒次:第 8 棒 · 执行棒 | 日期:2026-09-23 执行依据:接续入口_插件投放与分库线_20260922.md §0 最新行 + §5「本轮动作(第 8 棒)」 交接单:dsh-server-docs/交接单/插件投放与分库线-①共享只读包库与插件数据面.md §五 S5 / §六


〇、总判定(先给结论)

项 判定 关键证据
部署到 47 + 106 ✅ 完成 三机 md5 逐字一致
S5-E ① v1→v2 升级、存量数据保留 ✅ PASS 升级前后均 3 行
S5-E ② 强行降级 ⇒ 拒绝且库不变 ✅ PASS 409 plan_blocked + 列/行未变
S5-E ③ 同名同版本重传 ⇒ 幂等 ✅ PASS http=200、tgz md5 与源一致
S5-E ④ 迁移前结构备份落盘 ✅ PASS 2 个 .sql(643 / 1344 字节)、0 字节文件 = 0
S5-E ⑤ 备份不可写 ⇒ 500 且库不变 ✅ PASS 500 schema_backup_failed + mkdir_failed: EPERM
S6-E ① 非 admin /mine 条数 = 候选池全量 ✅ PASS 5 = 5
S6-E ② 开关关 ⇒ 404(只记事实) ✅ PASS 404(⛔ 未开开关)
S6-E ③ admin 新增 ⇒ 用户刷新即可见 ✅ PASS 5 → 6
门户三段式 UI 逻辑自洽 ✅ PASS(逻辑层 7/7) 互斥 / 完备 / 语义正确
真机缺陷修复 🔴 发现 1 个真实缺陷并已修 pg_dump env 传递错误 ⇒ 见 §三
回归三门 ✅ 全绿 build rc=0 · 449/447/0/2 · added: 0

本轮净产出 = 修掉一个"备份机制在生产上根本跑不通"的真实缺陷(本机测试测不出、只有真机 E2E 能暴露)。


一、部署与三机一致性

1.1 构建

$ cd D:/github/dsh_shenxian && npm run build
(无输出 = tsc 通过)
退出码 = 0

1.2 三机 md5 逐字一致(部署判据)

$ md5sum lib/db/plugin-data/datastore.js lib/web/routes/business-plugins.js
文件 本机 47 (/opt/dshs/lib/...) 106 (/opt/dshs-cluster/lib/...)
db/plugin-data/datastore.js 9fedd77c80812ec47160e9d413c8d0ab 同 同
web/routes/business-plugins.js fe456d60c1f827db53fa1a54ba229591 同 同
config.js 同 同 同
web/portal.html c4f012ce9916825935c0d166daafb2c0(64894 字节) 同 —
$ systemctl restart dshs          # 47
退出码 = 0

1.3 v15 账本表仍在(第 6 棒落地的迁移)

$ psql -h 127.0.0.1 -p 15432 -U dshs -d dshs -Atc \
  "select tablename from pg_tables where schemaname='public' order by 1;"
audit_log business_plugins credential_vault domains dsh_hosts dsh_instances
email_codes folder_plugins messages overlay_devices plugin_data_audit
plugin_datastores room_members rooms schema_migrations sessions users workspaces
退出码 = 0

⇒ plugin_datastores / plugin_data_audit 均在 ⇒ v15 迁移未丢。

1.4 部署前备份(可回滚)

/opt/dsh/backups/lib-s5s6/47-pre-s5s6-20260923-085319.tgz
/opt/dsh/backups/lib-s5s6/106-pre-s5s6-20260923-085319.tgz

二、S5-E ①–⑤ 真机 E2E

探针插件:@dsh-local/s5probe,表 p_s5probe_notes(scope user),内核自动注入 created_at / updated_at。

2.1 S5-E ① v1→v2 升级 + 存量数据保留 ⇒ PASS

═══ S5-E 真机 E2E v2(47 · Manager · PG 127.0.0.1:15432)═══
── S5-E ① v1 腿 ──
  上传 v1            http=200  ver=1.0.0
  建库              http=200  created=None
  预演 v1           http=200  planHash=ebd541642cd3e9eb…
  迁移 v1(建表)    http=200
  v1 列             user_id:text, created_at:bigint, updated_at:bigint, title:text, body:text
  v1 表清单         p_meta_schema, p_s5probe_notes
  写 3 行(直连插件库 · 数据由插件负责 · 补内核审计列)
    INSERT 0 3
  ▶ rows_before_upgrade = 3  (期望 3)

── S5-E ① v2 腿(升级 · 只增不减)──
  上传 v2            http=200  ver=2.0.0
  预演 v2            http=200  items=['add_column'] forbidden=0
  迁移 v2(加列)    http=200
  v2 列             user_id:text, created_at:bigint, updated_at:bigint, title:text, body:text, pinned:boolean
  ▶ rows_after_upgrade  = 3  (期望 3 —— 存量数据原样保留)
① 升级后行数仍在   : 3 (期望 3)  ⇒ PASS
退出码 = 0

2.2 S5-E ② 强行降级 ⇒ 409 且库未变 ⇒ PASS

── ② 强行降级(装回 v1 · schemaVersion 1 < 库现 2)──
  降级前:台账=ready|2 | 列=user_id,created_at,updated_at,title,body,pinned
  上传 v1(降级包)  http=200  ver=1.0.0
  预演(降级)       http=200
    items     = []
    forbidden = [{"kind": "drop_column", "target": "p_s5probe_notes.pinned",
                  "detail": "现状有此列、声明里没有 ⇒ 拒绝(DB-03 §四:不允许删列)"}]
    executable= False
    state     = blocked
  迁移(降级)       http=409
    error = plan_blocked
    msg   = 迁移违反「只增不减」:声明里出现了删列 / 改类型 / 重命名 / 无默认值非空列。请修正声明后重传。
  降级后:列=user_id,created_at,updated_at,title,body,pinned | 行数=3
  ▶ 判据 ②(库未被改)⇒ PASS
退出码 = 0

⇒ 防降级护栏在真机生效:预演即拦(executable=false / state=blocked),迁移 409,列与行数一字未动。

2.3 S5-E ③ 同名同版本重传 ⇒ 幂等 ⇒ PASS

  重传 v2            http=200  tgz md5=0e69625bb1a1e626…(与源一致)
退出码 = 0

2.4 S5-E ④ 迁移前结构备份落盘(新判据)⇒ PASS

── ④ 迁移前结构备份落盘判据(/opt/dsh/backups/plugin-db/<pkg>/)──
    total 16
    drwx------ 2 root root 4096 Sep 23 09:13 .
    drwx------ 3 root root 4096 Sep 23 09:13 ..
    -rw------- 1 root root  643 Sep 23 09:13 v0-2026-09-23T01-13-10-233.sql
    -rw------- 1 root root 1344 Sep 23 09:13 v1-2026-09-23T01-13-32-231.sql
  ▶ .sql 个数 = 2(≥2:v0 首建后 + v1 升级前)
  ▶ 0 字节文件数 = 0(期望 0)
  ▶ 判据 ④ ⇒ PASS
  台账里的备份审计行:
    datastore_migrate_backup_ok x2
退出码 = 0

⇒ 命名符合 <v>-<ts>.sql,均为非 0 字节,台账双行审计。

2.5 S5-E ⑤ 反证:备份不可写 ⇒ 500 且库未变 ⇒ PASS

⚠️ 诚实记录:本腿第一次尝试(chmod 000)FAILED。 原因:平台以 root 运行,chmod 000 拦不住 root(实测 touch 仍成功)。 第一次失败的原始输出(保留):

  迁移(备份不可写) http=200  ⇒ FAIL
▶ 判据 ⑤(库结构未变)⇒ FAIL

⇒ 改用只读文件系统作反证载体(DSH_PLATFORM_DIR=/sys/dshs-nope)重跑。

═══ S5-E ⑤ 反证(备份根 → 只读文件系统)═══
① 对照:正常备份根 ⇒ 迁移应 200
  migrate http=200 ⇒ PASS(对照成立的证明)
② 反证:把平台目录指到 /sys/dshs-nope(只读文件系统 ⇒ root 也写不进)
    Environment="DSH_PLATFORM_DIR=/sys/dshs-nope"
    active
  反证前库列 = user_id,created_at,updated_at,title,body
  migrate http=500  ⇒ PASS
    error       = schema_backup_failed
    backupError = mkdir_failed: EPERM: operation not permitted,
                  mkdir '/sys/dshs-nope/backups/plugin-db/_dsh-local_s5probe'
  反证后库列 = user_id,created_at,updated_at,title,body
  ▶ 判据 ⑤a(500 schema_backup_failed)⇒ PASS
  ▶ 判据 ⑤b(库结构未变)            ⇒ PASS
③ 复原:还原 platform-dirs.conf → daemon-reload → restart
    Environment="DSH_PLATFORM_DIR=/opt/dsh"
    active
退出码 = 0

环境复原已复验:

Environment="DSH_PLATFORM_DIR=/opt/dsh"     ← drop-in 唯一载体
(无 .bak-s5e 残留 ✓)

⚠️ 踩坑记录:第一次尝试新建第二个 drop-in s5e-counterproof.conf 会被静默覆盖(同名键后写者胜),故改为 sed 编辑唯一载体 platform-dirs.conf 并原样还原 —— 与记忆中的「drop-in 才是唯一载体」一致。


三、🔴 本轮发现的真实缺陷(已修 + 已回归钉住 + 已重部署)

3.1 现象

S5-E ④ 在真机上根本跑不通 —— 结构备份永远失败:

pg_dump: error: connection to database "dshs_pl_s5probe" failed:
could not connect to server: No such file or directory
        Is the server running locally and accepting connections on
        Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?

3.2 根因(受控复现确认)

src/db/plugin-data/datastore.ts 里 pgEnvFrom(baseUrl) 的返回值被展开在 execFileSync 的 options 层(与 env: 同级),而不是合进 env:

// ❌ 缺陷写法:PGHOST/PGPORT/PGUSER 变成了"选项名"而非环境变量
execFileSync('pg_dump', args, { ...pgEnvFrom(baseUrl), env: {...}, timeout: 120_000 })

⇒ pg_dump 子进程收不到 PGHOST/PGPORT/PGUSER ⇒ 退回默认 Unix socket :5432 ⇒ 47 上 PG 实际在 127.0.0.1:15432 ⇒ 连接失败。

本机测试测不出:本机没有 pg_dump,该路径根本不会被走到;只有真机 E2E 能暴露。

3.3 修复

env: pgDumpEnv(baseUrl, password),
timeout: 120_000,
export function pgDumpEnv(baseUrl: string, password: string): NodeJS.ProcessEnv {
  const env: NodeJS.ProcessEnv = {
    ...process.env,
    PGPASSWORD: password,
    PGCONNECT_TIMEOUT: '15',
  }
  try {
    const u = new URL(baseUrl)
    if (u.hostname !== '') env.PGHOST = u.hostname
    if (u.port !== '')     env.PGPORT = u.port
    if (u.username !== '') env.PGUSER = decodeURIComponent(u.username)
  } catch { /* 连接串不可解析 ⇒ 保留 ...process.env */ }
  return env
}

顺带修一处不实文案:business-plugins.ts 里第 4 棒遗留的 backup: 'not_implemented' 未随第 7 棒接线更新,响应改为真实值:

backup: backupPathOrNull === null ? 'skipped_first_create' : 'taken',
backupPath: backupPathOrNull,

3.4 回归钉住(test/plugin-data.test.mjs,+2 用例)

$ npm test
⬇ 见 §四

⚠️ 该用例第一版尝试用 PATH 假 pg_dump 桩,Windows 上失败(execFileSync 无法解析 PATH 里的 .cmd/.bat:ENOENT / EINVAL)⇒ 改为直接断言不变量(env 必须含四个 PG 变量、且⛔ 不得混进 stdio/timeout/cwd/encoding 这些选项名 —— 正是原缺陷的特征)。

3.5 重部署 + 复验

修后重构建、重部署到 47/106,datastore.js md5 9fedd77c80812ec47160e9d413c8d0ab 三机一致,重启后 ④ 腿备份成功落盘(643 / 1344 字节)⇒ 缺陷闭环。


四、回归三门(改完 → 回归 → 才放锁)

门 命令 原始输出 退出码
编译 npm run build 无输出(tsc 过) 0
全量测试 npm test(Node 22.22.2) 449 tests / 447 pass / 0 fail / 2 skipped 0
分层 node scripts/check-layering.mjs ✅ 无新增违规(added: 0) 0

基线 447 / 445 pass / 0 fail / 2 skipped ⇒ 本轮 +2(新增的 pgDumpEnv 不变量用例),失败数仍为 0。


五、S6-E 真机 E2E

5.1 S6-E ① 非 admin /mine 条数 = 候选池全量 ⇒ PASS

── ① 非 admin 用户 GET /api/plugins/mine 条数 = 候选池全量 ──
  admin  /api/plugins/business  http=200
  guest  /api/plugins/mine      http=200
  ▶ 候选池全量 = 5 | guest 可见 = 5
  ▶ 判据 ①(条数一致)⇒ PASS
退出码 = 0

5.2 S6-E ② 开关关 ⇒ 404(🔴 只记事实,⛔ 未开开关)⇒ PASS

── ② GET /api/plugins/shared/catalog —— 🔴 只记事实,⛔ 不打开开关 ──
  admin  http=404
  guest  http=404
  ▶ 开关 DSHS_SHARED_CATALOG_PUBLIC 当前值: (未设置 = 默认 false)
  ▶ 判据 ②(关着 ⇒ 404,⛔ 不是空列表)⇒ PASS
  ⛔ 本棒未打开该开关(属 §1「扩大可见面」红线门禁,须用户拍板)
退出码 = 0

🔴 红线遵守声明:DSHS_SHARED_CATALOG_PUBLIC 属「扩大可见面」红线门禁 ⇒ 本棒全程未开,仅记录"关着 ⇒ 404"这一事实。

5.3 S6-E ③ admin 新增 ⇒ 用户刷新即可见 ⇒ PASS

── ③ admin 新增插件 ⇒ 非 admin 刷新即可见(无需任何管理动作)──
  新增前 guest 可见 = 5
  上传新插件 http=200
  ok=True name=@dsh-local/s6newprobe
  新增后 guest 可见 = 6 | 含新插件 = True
  ▶ 判据 ③(刷新即可见 · 无需管理动作)⇒ PASS
  ▶ 判据 ③b(条数 +1)⇒ PASS
退出码 = 0

5.4 S6-E ② 深挖:enable 落点(跨机形态真实发现)

背景:客用户(guest / uid 100002)实例在 106(worker),不在 47。 /api/plugins/mine 读的是平台本机 profiles(47 上为空)⇒ 字面判据 enabled:true 未闭合。

实际 enable 确实成功,且正确落到实例所在机 106:

POST /api/plugins/mine/apply  http=200
{"ok":true,"taskId":"023a87d692d07654"}

--- 106 上 guest 的 node_modules 出现 storyforge 软链,且指向共享层 ---
lrwxrwxrwx 1 dsh-eeccbc638afc46bdb663 100002 123 Sep 23 09:43
  .../home/profiles/web/node_modules/@dsh-local/storyforge
  -> ../.pnpm/@dsh-local+storyforge@file+..+..+..+..+..+bundled-plugins+_dsh-local_storyforge/node_modules/@dsh-local/storyforge

--- 该软链可解到 package.json --- ✓ 可解
--- 106 侧 package.json 已写入 ---
"@dsh-local/storyforge": "file:/var/lib/dshs/bundled-plugins/_dsh-local_storyforge"
▶ 判据(装配真落到实例所在机)⇒ PASS

判定:不是代码缺陷,是拓扑形态的必然结果 —— /mine 的 enabled 字段取自平台本机 profile 快照;当用户实例在 worker 上时,本机快照必然反映不了远端装配。

旁证(沙箱命令行原文,证明共享层已 ro 绑进实例):

dsh-100002-9648cef4.scope loaded active running /usr/bin/bwrap
  ... --ro-bind-try /var/lib/dshs/bundled-plugins /var/lib/dshs/bundled-plugins
  ... -- setpriv --reuid 100002 --regid 100002 /usr/bin/dsh --profile web --host 127.0.0.1 --port 21000

⇒ S1 共享只读层挂载成立(--ro-bind-try),且实例确实在跑。

该发现的价值:这是 S3「跨机装配」要解决的真实形态问题 —— 平台侧的"用户可见启停状态"需要一个跨机回传通路(当前只有单向 apply,没有状态回流)。建议单独立项,本轮只记录、不改架构。


六、门户三段式 UI 观察

⚠️ 本机 agent-browser 每次调用均挂死(8 分钟无输出;Chromium 进程起了但 open 不返回)—— 属环境限制,按技能指引 fail-fast 处理(已 agent-browser close + 清理 chrome 进程)。 ⇒ 改用等价判据:ui-logic-check.mjs 用真实 API 数据复跑 portal.html 里 loadVisibility() 的三段派生逻辑。

派生逻辑(与 portal.html 逐字一致):

const shared     = (catalog && catalog.plugins) || []
const sharedIds  = new Set(shared.map((p) => p.id))
const enabled    = mine.filter((p) => p.enabled)
const disabled   = mine.filter((p) => !p.enabled && sharedIds.has(p.id))
const unavailable= mine.filter((p) => !p.enabled && !sharedIds.has(p.id))

结果:

═══ S6 三段式 UI 逻辑评估(真实 API 数据)═══
  catalogHttp: 404 | mineHttp: 200
  ① shared (平台共享只读): 0
  ② enabled (已启用): 2
  ③ disabled (已停用): 0
  ⋯ unavailable (暂不可用): 4
  mine 总数: 6
  switchOff (catalog===null ⇒ 显示开启提示): true
--- 断言 ---
  A. 互斥(两两不相交): true
  B. 完备(②+③+⋯ = mine 总数): true   「6 vs 6」
  C. ② 全为 enabled:true: true
  D. ③ 全部在共享层内(能装): true
  E. ⋯ 全部不在共享层内(装不到): true
  F. 开关关 ⇒ ③=0(⛔ 不能把池里的都算"用户没开"): true
  G. 开关关 ⇒ ⋯ = mine 里未启用的全部: true
═══ 总判定:✅ 三段自洽/互斥/完备 ═══
退出码 = 0

⚠️ 未验项(具名):CSS 视觉渲染、tab 点击切换 —— 需真浏览器,本机环境不具备。


七、清理与复原(零残留复验)

对象 处置 复验
探针库 dshs_pl_s5probe DROP DATABASE pg_database like '%probe%' = 0 ✅
候选池 @dsh-local/s5probe / s6newprobe 删行 + 删 tgz 池子恢复 = 原始 4 个 ✅
账本 plugin_datastores 删探针行 like '%probe%' = 0 ✅
审计 plugin_data_audit 删探针行 like '%probe%' = 0 ✅
47 备份目录 plugin-db/_dsh-local_s5probe 删 目录不存在 ✅
共享层 bundled-plugins/*probe* 删(保留 _dsh-local_storyforge / dsh-plugin-mcn-suite) 恢复 2 项 ✅
platform-dirs.conf sed 改 → 原样还原 DSH_PLATFORM_DIR=/opt/dsh + 无 .bak-s5e ✅
106 客实例 storyforge 启用 已回退(见下) ✅

106 客实例回退明细(还原到我介入前的 4 依赖状态):

dependencies: ['@dsh-local/business-plugins', '@dsh-local/portal-entry',
               '@dsh-local/workspace-scoped-picker', '@softspark/dsh-file-preview']
bundles:      ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app',
               '@dsh-local/business-plugins', '@dsh-local/portal-entry',
               '@dsh-local/workspace-scoped-picker', '@softspark/dsh-file-preview']
✓ package.json 已无 storyforge
✓ symlink 已除
✓ .pnpm 已除

106 健康复验:

dshs-worker.service  = active
dshs-relay.service   = active
dsh-100002-9648cef4.scope = active running

八、⛔ 未做的事(具名原因)

  1. 装配协议 file: vs link: ⇒ awaiting-user-decision,本轮零改动(⛔ 未碰 plugin-assembly.ts 协议语法、⛔ 未改交接单里 file: 相关描述)。
  2. 可见面开关 DSHS_SHARED_CATALOG_PUBLIC ⇒ 属 §1「扩大可见面」红线门禁,未开,只记录 404 事实。
  3. 跨节点内容分发 ⇒ 无平台侧通路(106 共享层手工同步),需单独立项。
  4. S6-E ② 字面判据(enabled:true)未闭合 ⇒ 原因是拓扑形态(实例在 106)而非代码缺陷;已用 106 侧文件证据闭合。平台侧用户可见启停状态的跨机回流 = 缺失能力,建议立项。
  5. 门户 UI 的 CSS 渲染 / tab 点击 ⇒ 本机 agent-browser 挂死,环境不具备;已用逻辑层等价判据覆盖。
  6. ⛔ 未 commit / 未 push / ⛔ 未引新依赖 / ⛔ 无 merge / ⛔ 未跑全库 Glob/Grep。

九、技术附录

9.1 测试资产落点

文件 用途
tmp/s5e/pkg-v1/ · pkg-v2/ 探针插件声明(v1:title/body;v2:+pinned)
tmp/s5e/v1.tgz · v2.tgz 476 / 523 字节
tmp/s5e/driver.sh · driver2.sh · driver3.sh · driver5.sh · driver6.sh 各腿驱动
tmp/s5e/ui-logic-check.mjs 三段式逻辑评估
tmp/s5e/*.out 全部原始输出(本报告引用的每一段均出自此处)

9.2 关键 API 契约(本轮实测确认)

端点 方法 本轮实测
/api/plugins/business GET 200 · 候选池全量
/api/plugins/mine GET 200 · 含 enabled 字段
/api/plugins/mine/apply POST 200 · {ok:true, taskId}(异步)
/api/plugins/shared/catalog GET 404(开关关)
/datastore/plan POST 200 · 返回 items / forbidden / executable / state
/datastore/migrate POST 200 / 409 plan_blocked / 500 schema_backup_failed

9.3 表名/列名实况(订正我的两次误判)

  • 候选池表 = business_plugins(无 user_id,全局池)—— 不是 user_plugins。
  • 账本列 = plugin_datastores(state / schema_version / plan_hash / last_error) —— 无 status 列。
  • 不存在 plugin_tasks 表 ⇒ 异步任务状态在内存,落库不可查。
  • 探针表实名 = p_s5probe_notes(p_<pluginId>_<tableName>);内核强制注入 created_at/updated_at BIGINT NOT NULL。

本报告全部数字均出自原始输出文件,未做任何"看起来应该对"的推断。