回收 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)、记忆修复前备份。
20 KiB
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/ uid100002)实例在 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
八、⛔ 未做的事(具名原因)
- 装配协议
file:vslink:⇒awaiting-user-decision,本轮零改动(⛔ 未碰plugin-assembly.ts协议语法、⛔ 未改交接单里file:相关描述)。 - 可见面开关
DSHS_SHARED_CATALOG_PUBLIC⇒ 属 §1「扩大可见面」红线门禁,未开,只记录 404 事实。 - 跨节点内容分发 ⇒ 无平台侧通路(106 共享层手工同步),需单独立项。
- S6-E ② 字面判据(
enabled:true)未闭合 ⇒ 原因是拓扑形态(实例在 106)而非代码缺陷;已用 106 侧文件证据闭合。平台侧用户可见启停状态的跨机回流 = 缺失能力,建议立项。 - 门户 UI 的 CSS 渲染 / tab 点击 ⇒ 本机
agent-browser挂死,环境不具备;已用逻辑层等价判据覆盖。 - ⛔ 未 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。
本报告全部数字均出自原始输出文件,未做任何"看起来应该对"的推断。