Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下22.md
T
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

48 KiB
Raw Blame History

工作日志 · 2026-09(第 23 片)

⚠️ 本目录日志已按【月】分片(2026-09-15 用户定,单文件 ≤50 KB):同月续片 2026-09.md / 2026-09-下.md / 2026-09-下2.md / 2026-09-下3.md / 2026-09-下4.md / 2026-09-下5.md / 2026-09-下6.md / 2026-09-下7.md / 2026-09-下8.md / 2026-09-下9.md / 2026-09-下10.md / 2026-09-下11.md / 2026-09-下12.md / 2026-09-下13.md / 2026-09-下14.md / 2026-09-下15.md / 2026-09-下16.md / 2026-09-下17.md / 2026-09-下18.md / 2026-09-下19.md / 2026-09-下20.md / 2026-09-下21.md / 2026-09-下22.md / 2026-09-下23.md 写入约定:一律 append 到当月最后一片;该片超 50 KB ⇒ 新建 2026-09-下N.md;⛔ 不要再按日新建 2026-09-DD.md。 覆盖来源:2026-09-15.md 上一片:2026-09-下21.md 下一片:2026-09-下23.md


11:1x–12:0x — T08 执行:S0 ✅ + S1 ✅(用户「确认执行」)

  • 锁纪律:--claim-exec "exec-cluster-1a" → mkdir 交接单/.doing-T08 + OWNER → 台账行改 🔄 执行中(同一动作完成)。中途停 ⇒ 已按纪律释放全局锁(.doing-T08 + OWNER 保留作续做凭证)。
  • S0 基线冻结:47 = /opt/dshs(无 .git ⇒ 用文件 hash:lib/=8423ed1c…、web/=7e001f0a…、package.json=878f7e9a…、unit=545ebe09…、DB 131072 B、用户 6);106 = /root/dsh-users-platform(另起 /opt/dshs-cluster 隔离)。
  • S0 三处修正:① 不在 47 跑 smoke(47 的 /opt/dshs/scripts/ 无 fake-dsh.mjs,且不必生产留痕)② 不建分支(工作树与其他会话共享 + 已有 11 个别人的未提交改动,建分支会把它们带走)③ 106 测试环境独立(端口 13080/13081)。
  • 查清 smoke 性质:ci.sh 明说不纳入;实测自包含(进程内 dbPath:':memory:' + stand-in fake-dsh.mjs);DSHS_DB_URL 优先于 dbPath ⇒ 同一组 smoke 可跑 PG(S1.4 的可行基础)。
  • S1 完成:106 装 PG 15.19(独立数据目录 + pg_ctl 手动起 + 端口 15432 + 只绑 127.0.0.1,未建 systemd 单元,回滚=rm -rf)。
    • S1.4 结论:SQLite 6/8 == PG 6/8,失败项完全相同(smoke-domain/smoke-isolation)⇒ 两后端行为一致。
    • 假阳性已排除:PG 侧 dshs_smoke 有 10 张平台表 + schema_migrations 有记录 + smoke 数据真的落库。
    • S1.5 回滚演练 ✅:加/去 DSHS_DB_URL 各起一次服务,login.html 均 200。
    • S1.2 迁移脚本:新增 scripts/migrate-sqlite-to-pg.mjs(md5 15fb990a68aaae918128faf0801e9464)。设计=列清单不写死(PG information_schema ∩ SQLite PRAGMA)+ PG 结构由平台自己的迁移建 + identity 列 OVERRIDING SYSTEM VALUE + 搬完 setval。实测:逐表行数一致|uid 原样(100001/100002)|audit id 原样|迁移后新用户得 100003 不撞主键。踩坑 1:schema_migrations 不能再搬(与平台迁移冲突,事务已回滚)⇒ 已加 SKIP。
  • 验收三关:① smoke 两后端一致 6/8 ② 单机形态可跑 ✅ ③ 47 四个 hash 逐字一致 ✅(DB 字节一致;用户 6→7 是平台在正常服务,非本单动作)。
  • 🔴 新发现(P0 阻塞,影响 S3/S6):106 上 account 隔离模式实例起不来 —— node: OpenSSL configuration error: Permission denied fopen(/etc/ssl/openssl.cnf) exitCode 13。根因(已复现+硬证据):bwrap 为绑定 /etc/pki/tls/certs 自建的中间目录 /etc/pki、/etc/pki/tls 权限是 drwx------(0700) ⇒ uid 无法穿越 ⇒ 而 106 的 /etc/ssl/openssl.cnf 是指向 /etc/pki/tls/openssl.cnf 的符号链接 ⇒ EACCES(非 ENOENT)。47 无此文件故不触发 ⇒ 设计 §14.3「机器基线不能跟着迁移」的首个真实实例。
    • 已验证修法 D(采纳):绑定前加 --perms 0755 --dir /etc/pki + --perms 0755 --dir /etc/pki/tls —— 零权限扩大。
    • E 被否决:整绑 /etc/pki 也能过,但 /etc/pki/tls/private/postfix.key 会暴露给所有实例(违反 R5)。
    • C 不可行:bind 不能覆盖悬空符号链接(Can't create file at /etc/ssl/openssl.cnf)。
    • ⚠️ 该问题普适:任何"宿主配置文件放 X、用符号链接从 Y 指过去"的发行版都会踩。
  • 发现 2(待定性):smoke-domain 打印 proxy redirect -> /somewhere,断言期望 /u/u1/dsh/somewhere ⇒ legacy 子路径 Location 重写未生效;两侧一致 + 我只读未改码 ⇒ 既有行为,需单独立项。
  • 载荷清单教训:第一版漏 assets/ ⇒ proxy.js 因缺 assets/inject/recovery.js 硬失败(档案 81 R1)。S6 的 join 脚本必须列全:lib scripts package.json assets web poc cordis.patch.yml ensure-role-profile-patch.cjs mksess.cjs README.md STANDARD.md。
  • 改动文件:scripts/migrate-sqlite-to-pg.mjs(本机代码仓,未 commit,§4 提交边界);交接单/T08-*.md §9 执行记录;.doing-T08/OWNER;台账 T08 行。
  • 下一步:S1.6 先修 bwrap 中间目录 0700(涉及 orchestrator.ts 隔离层,需 47 回归 + 106 上 smoke-isolation 转绿)→ 再回 S2(schema v7 + lease.ts)。

12:1x–13:2x — S1.6 完成:bwrap 中间挂载点 0700 已修(双机验证)

  • 用户「确定执行」 ⇒ 抢锁 → 改 src/supervisor/orchestrator.ts(只增不减):
    • 新增模块级 mountParentDirList(dest, stopAt) + mountParentDirArgs(dest, stopAt)
    • 调用点 2 处:① /etc 白名单装配 ② --bind root root 与共享技能层绑定之前(同因)
  • 🔴 救命的一条(回归测试抓到的):第一版用 --perms 0755 --dir 在 106(bubblewrap 0.11.0)通过,但 47 是 bubblewrap 0.4.0 ⇒ bwrap: Unknown option --perms ⇒ 沙箱起不来 = 所有实例全挂(47 上 /etc 可见条目 78 → 0,一眼可见)。 ⇒ 改用 --tmpfs:0.4.0 就支持,且自带 0755(本文件注释早已写明)。47 实测 /etc 条目 78 == 78 零变化。 📌 通用结论:改 bwrap 参数前必须先看目标机的 bubblewrap 版本(47 = 0.4.0 这个基线很旧,很多新选项都没有)。
  • 修复效果(三机证据):① OpenSSL EACCES 出现次数 1 → 0 ② /etc 可见条目 495==495(106)/ 78==78(47) ③ bwrap 接受参数 rc=0 ④ 中间目录 0700 → 0755 ⑤ 私钥 /etc/pki/tls/private 仍不可达 ⑥ 沙箱内 /opt 只有该用户自己的树、平台目录不可见。
  • 忠实链探针(systemd-run --scope → bwrap → setpriv → node,cwd 在用户工作区):cwd=…/ws/proj uid=50001,相对路径写成功 ⇒ account 启动链本身是通的。
  • ⚠️ smoke-isolation 不能作为 account 模式验收(夹具结构性缺陷,非平台缺陷):① 夹具路径必须在沙箱绑定集内(fake-dsh.mjs 放 /opt/... ⇒ Cannot find module;放 /usr/local/... 就好)② 数据根用 mkdtemp(/tmp) 会与沙箱的 --bind <userRoot>/tmp /tmp 冲突 ⇒ bwrap: Can't chdir to /tmp/dsh-smoke-iso-XXXX/...: No such file or directory(生产 dataRoot=/var/lib/dshs 不撞)。⇒ 遗留小项:修夹具或另写 account 验收脚本。
  • 47 未部署(本步只在 106 部署;47 只做同参数实测,在临时目录跑 bwrap,不碰生产进程)。criterion ③ 终检通过:lib//web//unit/DB 四项与开工基线逐字一致,dshs active。
  • 产出:交接单/T08-*.md §10(S1.6 执行记录);orchestrator.ts md5 记入 OWNER;docs-audit rc=0。
  • 下一步:S2(schema v7 + lease.ts)。

13:3x–14:1x — S2 ✅ + S3 ✅(用户「一直执行完毕」)

S2 · 数据模型 v7 + 租约服务

  • 改动:db/schema.ts(v7:dsh_hosts + dsh_instances 加 host_id/epoch/heartbeat_at/lease_until + 两索引,SQLite 与 PG 两方言同写)|db/types.ts(DshHost/ClaimResult/clusterInstanceId())|db/adapter.ts|db/repo.ts + db/sqlite.ts(SQLite 侧)|db/pg.ts(UPDATE … RETURNING 原子抢占)|supervisor/lease.ts(新):InstanceLease + stillHolder() + ttl > 2×renew 硬校验|test/lease.test.mjs(新,10 例)。
  • 验收:SQLite 10/10 == PG 10/10(PG 侧在 106 上用 LEASETEST_PG_URL 跑)。覆盖单写者 / fencing(旧持有者续租必失败) / 过期可抢占且 epoch 递增 / epoch 不复用 / 对账视图 / renewAll 失权清单。
  • 🔴 踩坑(已修):INSTANCE_COLS 漏 v7 的 4 个新列 ⇒ 行能匹配但 hostId 恒为 null(3 个用例挂住)⇒ 两方言的列清单必须同步。

S3 · Worker agent + RemoteSpawner(端到端通过)

  • 改动:worker/agent.ts(新)(/healthz /instances /launch /stop /status /endpoint /fence /restart-probe /watchdog /touch;bearer 鉴权 timingSafeEqual + 白名单动作 + 幂等缓存 + epoch 跟踪)|supervisor/remote-spawner.ts(新)(实现 Spawner 全接口;重试 200ms/1s/3s 且全程复用同一 operationId)|orchestrator.ts 只加两个 additive 方法(listUserInstances()、launchTokenOf())|config.ts(DeployMode 加 'cluster' + 4 个配置)|web/server.ts(cluster 分支 + 缺地址 fail-loud)|cli.ts(dshs worker)|scripts/verify-cluster-agent.mjs(新)。
  • 关键设计:Worker 不碰 DB —— apiKey/uid 由 Manager 在 /launch 时投递、只存内存(与 k8s per-user Secret 同思路),uid 兜底用纯函数 hashUid。
  • 验收(106):verify-cluster-agent.mjs → OK,四项保证全绿:路由/代理层零改动 ✅|launch token 回传(P0-6)✅|幂等键 ✅|self-fencing(epoch 1→2 触发)✅;另证实例确实在 worker 上、代理经远端协议命中 fake-dsh。
  • 🔴 两处踩坑(已修):
    1. 夹具 fake-dsh.mjs 不吐 launch token(真实 dsh 会打 dsh web: http://127.0.0.1:<port>/?token=…)⇒ 平台只能等 10 s 超时、launchToken 恒 undefined ⇒ 「登录直达会话」在本机测试里根本测不到。补上后 launch 从 10.2 s 变即时。连带暴露 smoke-subdomain:78 的陈旧断言(把 URL 写死成不带 token,是"夹具不吐 token"的意外产物)⇒ 已改为 startsWith(子域) + 断言不回环端口;复跑冒烟回到 6/8、失败项与基线完全相同。
    2. agent 在"实例已在跑"的路径上没记 epoch ⇒ /fence 拿不到 epoch、self-fencing 永不触发 ⇒ 修法 = epoch 必须在尝试 spawn 之前落(记的是「Manager 的意图」)。
  • ⚠️ 影响后续的重要发现:今早 06:26 另一会话把 k8s 后端连同 src/tcp-bridge.ts / src/web/file-service.ts / src/fs/k8s-user-fs.ts 一起删了(当时按"k8s 专用"处理)⇒ 设计里 S5/S6"复用跨机原语"的假设失效。两条出路(S4 后再定):(a) 按 cluster 语义自建 bridge + 每用户 file 服务;(b) 让 agent 直接隧道(只暴露 1 个端口、portGuard 语义完整,需实现 WS 隧道)——(b) 更优。S3 的同机形态两者都不需要,不影响已完成部分。
  • 三关:① 冒烟 6/8(失败项与基线相同 = 无回归)② 单机形态可跑 ✅ ③ 47 三个 hash 逐字一致 ✅
  • 下一步 S4:1a 形态(2 台 Manager,一台 capacity=0 + Worker-01 同机)+ 单活退化 + dsh_hosts 注册与心跳。⚠️ 跨机代理需先定 11.3 的 (a)/(b);1a 同机不受影响,可先做。

13:5x–14:2x — 设计纠正(数据分层)+ S4 ✅

🔴 设计纠正(用户口径):Worker 不是"不许有数据库"

  • 用户原话:「不一定 worker 不连数据库,有些插件有可能有大量数据需要保存和管理、长期使用」 ⇒ 我在 §1.2 写的「Worker ❌ 不碰 DB」过窄:本意只是控制面数据(尤其归属)不能由 Worker 写;插件长期数据根本不在同一层。
  • 已改:设计文档新增 §1.3「数据分层」(四条判据 + 插件三种落点);并同步修正 agent.ts/remote-spawner.ts/cli.ts 三处注释(注释是后来人读代码的唯一线索,必须一致)。
  • 三条判据(已写进 MEMORY.md 常驻):① 是归属/租约吗 ⇒ 只有 Manager 能写 ② 跟着用户走吗 ⇒ 放实例 home(插件库实测 home/.dsh/mcn-plugin.db),别塞控制面 PG ③ 是本机运维用的吗 ⇒ 放 Worker 本地库。
  • 插件三种落点:per-user SQLite 在 home(现状,天然跟迁移)|量大要真 DBMS ⇒ 每用户一库 + 数据目录放位置无关存储|跨用户聚合 ⇒ Manager 侧离线汇总,不在 Worker 上做。

S4 · 归属租约接进启动路径 ✅(端到端五项全绿)

  • 改动:spawner.ts(launch opts 加 epoch)|remote-spawner.ts(透传 epoch)|supervisor/leased-spawner.ts(新)(launch 前 claim、失败抛 LeaseBusyError=退让;心跳 renewAll、失权即向 agent 下发更高 epoch;stop 时 release;启动即注册 host + 起心跳)|web/server.ts(cluster 分支 = LeasedSpawner(RemoteSpawner),租约参数走 env)|scripts/verify-cluster-lease.mjs(新)。
  • 验收(106,两 Manager 共享 PG + 同一 agent,短 TTL 秒级制造过期):dsh_hosts = m-1:up, m-2:up|① 归属落库 host_id=m-1 epoch=1|② 单写者:m-2 抛 LeaseBusyError(holder=m-1)、归属未被改动|③ 释放即接手 epoch=2(单调递增,不用等 TTL)|④ 失权即 self-fence:worker 实例数 1 → 0、且归属未被误清。
  • 回归:冒烟 6/8(失败项与基线相同)|S3 端到端 OK|lease 单测 SQLite 10/10 == PG 10/10。
  • 编译期两坑(已修):① quotaInfo 签名被别的会话扩过(多 baseMb)⇒ 委托要按当前接口对齐 ② 我把心跳停止器命名 stop() 与 Spawner.stop(userId) 重名 ⇒ 改 stopHeartbeat()。
  • 47 未变 ✅(三个 hash 逐字一致)。
  • 下一步 S5/S6:按 11.3/(b) 让 agent 直接隧道(只暴露 1 个端口,portGuard 语义完整;需实现 HTTP/WS 转发),再做 join 脚本与计划内迁移。

14:0x–15:0x — S5 ✅ + S6 ✅ + S7(部分),用户要求「执行到任务完成」

S5 · 跨机文件面

  • 改动:agent 加 /fs/*(init/list/mkdir/create/upload/read/isdir/plugins/handoff/root,复用同一个 LocalUserFs)|src/fs/remote-user-fs.ts(新)(路径安全复用 fs-guard.resolveWithinRoot,不重写;resolvePath 按 worker 的 dataRoot 做纯数学)|provider.ts 按模式选实现 + DSHS_CLUSTER_WORKER_DATA_ROOT + 启动探测不一致就报 |scripts/verify-cluster-fs.mjs。
  • 验收(关键设计:Manager 的 dataRoot 故意与 worker 不同 ⇒ 才能证明走远端):门户 tree/mkdir/upload 全经远端 ✓|文件落在 worker、不在 manager ✓|bad_path 与本地同源 ✓|下载回读逐字一致 ✓。
  • 🔴 踩坑(已修):agent 的 /fs/create、/fs/upload 直接 return 字符串 ⇒ Fastify 发 text/plain ⇒ 调用方 JSON.parse 失败 = 静默 500 ⇒ 必须包 {name}。
  • 基线约定:cluster 下所有 worker 的 dataRoot 必须同路径(同镜像可满足),不一致启动即报。

S6 · 多 worker + 容量准入 + 计划内迁移(方案落点)

  • 改动:InstanceLease.held 记 {epoch, hostId}|Spawner.launch/stop 支持显式 hostId|RemoteSpawner 多 host(hosts/hostsProvider 懒加载+30s TTL ⇒ 新增 worker 不用重启 Manager、hostIdFor 按归属路由、未知 host 直接报错拒绝静默改投)|LeasedSpawner 选机(selectHost 容量准入 + agentFor 让 fence 发给实例所在那台;先选机再认领)|管理面 GET/POST /api/admin/hosts(绝不下发 agentToken)+ POST /api/admin/users/:id/dsh/migrate|scripts/verify-cluster-migrate.mjs。
  • 验收(两 worker 共享 dataRoot):① 容量准入把高水位机排除 ⇒ 落 w-a ✓ ② 迁移 w-a→w-b(drain→拉起→epoch+1)✓ ③ 迁移后代理 200 + 文件可读(数据不搬家)✓ ④ 边界 already_there/unknown_host 明确报错 ✓。
  • 容量语义(已定):>0 声明并参与准入(已用+预留 ≤ 容量,DSHS_CLUSTER_RESERVE_MB 默认 512)|≤0 未声明(不设限)|-1 显式禁用承载(专用 Manager)。新增 DSHS_CLUSTER_REGISTER_SELF=0:一个 agent 只应有一条 host 记录。
  • 🔴 三条教训(都已修):① verify 脚本挂死 —— 测试没收尾实例 ⇒ fake-dsh 子进程继承 stdout ⇒ 管道不关 ⇒ ssh 一直等;顺带发现生产问题:agent 的 stop() 只关 HTTP 不收实例 ⇒ worker 停机留孤儿 ⇒ 已改「先 teardown、再关 HTTP」,四个脚本收尾也统一走 agent.stop() ② selectHost 改了语义(不再"谁拉起就归谁")⇒ S4 接管步骤须显式指定目标机 ③ holdings() 结构变了 ⇒ 单测断言同步改(非回归)。

S7 · 观测面(部分完成)

  • 新增 dshs doctor [--json](node/cgroup/swap/bwrap 版本(<0.5 提醒 --perms 不可用)/setpriv 降权/dataRoot/DB;退出码非 0 = 硬失败,可当 join 门禁)|dshs cluster status [--json](worker 目录+水位+实例数+过期租约,并明写"需人工确认才可接管 · R9")。
  • 自动接管按设计保持关闭(expiredAll() 只报不做;人工路径 = migrate API),与 R9 一致。
  • 仍待做:join-worker.sh 一键装机(现在 join = 起 agent + 注册两步手工)|集中日志/metrics|smoke-domain 定性。

总验收(S0–S7 全量复跑,15:0x)

冒烟 6/8(失败项与开工基线完全相同,全程无回归)|四个端到端全 OK|lease 单测 SQLite 10/10 == PG 10/10|残留进程 0|47 三个 hash + unit + dshs active 逐字一致。

15:2x–16:0x — 真实部署形态功能确认(用户「按最佳决策执行…执行完毕后检查确认功能」)

为什么补这一步(缺口)

此前所有验证都是"同一进程内起两个 Fastify" ⇒ 部署形态从未验过(独立进程 + 真 HTTP + 真 PG + 真 dsh 子进程)。新增 scripts/verify-cluster-live.mjs:真起 2 个 agent 进程 + 1 个 Manager 进程。

结果:全绿(①–⑪)

注册→审批→登录 ✓|mkdir 落 worker ✓|拉起真 dsh(URL 带 token)✓|running=true(account 沙箱内) ✓|/enter 登录直达 ✓|实例页面 200(经代理、跟随 303)✓|停止 ✓|w-2 注册(列表不含 agentToken)✓|迁移 w-1→w-2(epoch=3) ✓|迁移后 enter 给新 token URL、页面再 200 ✓|doctor rc=0 + cluster status(w-1:0 / w-2:1 实例)✓。

🔴 真实部署抓到 3 个真 bug(单进程测试根本测不出)

  1. 我 S1.6 引入的 --tmpfs 遮蔽 bug:bwrap: Can't chdir to <userRoot>/ws/proj: No such file or directory —— 用户根与共享技能层嵌套同前缀时,技能层的中间目录 --tmpfs 插在 --bind root root 之后 ⇒ 后挂的 tmpfs 把已绑好的用户根遮掉 ⇒ 实例崩溃循环。修:所有挂载点的中间目录统一前置 + 去重 + 由外到内(一处生成,禁止"就近创建")。
  2. claimInstance 没记 folder/patch:集群模式实例行由它创建(local 模式不写库)⇒ folder 恒 NULL ⇒ 迁移复现不了启动参数(bwrap: Can't chdir to : 空路径)⇒ 崩溃循环。修:认领时一并落库 + 迁移无 folder 就 409 no_folder_recorded(fail-loud)。
  3. 检查脚本自身的健壮性:启动窗口内代理会中途断连(other side closed,需 try/catch 重试);/api/dsh/status 的实例视图不含 launchToken(token 要用 /enter 判定);dsh 首页 303 需跟随重定向。 ⇒ 这三条不修会把真实部署误判为失败 —— 印证设计 §13「组件级测试不能替代部署级验收」。

47 侧说明(R11:不隐瞒)

本单在 47 上零写操作(find /opt/dshs -newermt 11:00 为空);实例 scope 由开工 1 → 0,最可能是平台自身 idle-reap(档案 08)或该实例自行退出,非本单动作(从未在 47 上 stop/kill)。

16:1x–16:4x — 真跨机演练通过(用户:"重点就是验证跨机,47 当 Manager / 106 当 Worker")

关键障碍与绕法(实测)

  • 🔴 106 公网入方向被腾讯云安全组挡住:106 开 19100,47 与本机都连不上;放通只能在控制台点 ⇒ 绕法 = 让 Worker 主动拨 Manager 的 ssh -R 反向隧道(走已开放的 SSH 端口,两端都不新增监听面)。
  • 🔴 实例只监听 127.0.0.1(portGuard 设计前提)+ 端口是动态的(findFreePort())⇒ 用 ControlMaster + ssh -O forward/cancel 在同一条长连接上动态加减转发(新增 src/worker/tunnel.ts)。
  • 安全:106 生成专用密钥,47 的 authorized_keys 加 restrict,port-forwarding(禁 shell/pty/ptys,只允许转发)。

现场

47 Manager(127.0.0.1:13080,不公网暴露) ←── 127.0.0.1:19000/19001 ──ssh -R──▶ 106 agent(w-106/w-106b);控制面 PG 也在 106(经隧道 15432)。

结果:scripts/verify-cluster-cross.mjs(在 47 上跑)九步全绿

⓪ worker 可达(隧道 ready)|① 注册 w-106(不含 agentToken)|② 用户流程|③ 文件面 mkdir 落 106|④ 拉起 running、worker /instances=1|⑤ 跨机页面 200|⑥ 第二台(106 模拟)已注册|⑦ 跨 worker 迁移 w-106→w-106b(epoch=2)|⑧ 迁移后新 URL 页面 200|⑨ 停止。 三方交叉取证:106 有该用户家目录与 ws/proj、两 agent 在跑;47 cluster status 显示 w-106/w-106b 均 up;47 生产逐字未变(三 hash 一致、/opt/dshs 0 改动、13080 无公网监听)。 顺带照出机器基线差异:47 = cgroup v1 + dsh 在 /usr/local/bin/;106 = cgroup v2 + /usr/bin/dsh。

边界(不夸大)

用的是演练级传输(SSH 反向隧道),不是设计 §2.3 的最终拓扑(受控网段白名单/正式隧道服务)⇒ 证明的是"软件在跨机下正确",不是"生产网络已就绪"。仍待做:① 云控制台放通受控网段(或隧道服务化)② 生产切换演练(drain→切→回滚,需窗口)③ join-worker.sh 一键装机。 现场保留待查验(拆除三步见 交接单/T08-§15.5):47 只新增 /opt/dshs-cluster/(新目录)+ authorized_keys 一行。

16:4x–17:1x — 云安全组取证 + 域名形态访问验证 ✅

云端限制取证(用户问"是端口限制还是什么")

  • 是腾讯云安全组,不是 106 主机防火墙:106 上 firewalld inactive、iptables 只有腾讯云「云镜」ipset 黑名单(只 DROP 已标恶意的源)、nft 0 条 ⇒ 主机层面 19100 是允许的;而"106 自测 200 / 47 与本机都连不上"⇒ 拦在云平台层。
  • 47 的出口 IP = 47.77.182.89(106 侧实测对端地址)⇒ 若走 B 方案,安全组写 源 47.77.182.89/32。
  • 实例端口取自内核临时端口段 32768-60999 ⇒ 要放通必须先收窄到可声明的端口池(我建议加 DSHS_PORT_MIN/MAX),否则等于摊开 2.7 万端口。

域名形态访问(演练环境,不动生产/DNS/证书)

scripts/verify-cluster-domain.mjs(在 47 上跑)全绿:https://<user>.test.alotbuy.com/?token=… → 真 dsh 实例页(<base href="/">)、越权(A 的 cookie 打 root 子域)403、实例确实在 106。 🔴 两次假阳性,都要记住:① 判据只用状态码 ⇒ 门户/登录页也是 200;② fetch 会静默丢弃 Host 头(Fetch 规范禁止头,undici 忽略)⇒ 落到"无租户"门户路由、假通过。⇒ 多租户路由验证必须 curl -H Host:(或 node:http),判据要能区分"实例页 vs 门户页"(MEMORY 里早有这条,本次仍踩了)。 🔴 另一个重复踩的坑:pkill -f ".../lib/cli.js ..." 匹配不到实际 argv(进程是相对路径 node lib/cli.js …)⇒ 旧进程没死、新进程绑定失败,看起来"重启了"其实没有(在 47 的 Manager 与 106 的 agent 上各踩一次)⇒ 一律按端口定位 PID 再 kill(ss -lntpH 'sport = :PORT' | grep -oP 'pid=\K[0-9]+')。 ✅ 顺带验证:按端口 kill agent 后 teardown 确实把实例全收了(残留 dsh --profile = 0)。

生产仍未切(明确)

alotbuy.com 仍由 nginx(443) → 生产 dshs(127.0.0.1:3080,deployMode 未设 = local) 服务,7 个用户 home 在 47。要"alotbuy.com 登录 → 实例在 106"需要一次生产切换:① 用户数据面到 106/共享存储 ② 生产库 SQLite→PG(脚本已验证)③ unit 加 cluster env + 重启 ④ 最后一跳(隧道或放通+端口池)⑤ 通配证书/DNS(Nginx 侧)。演练 Manager 在 127.0.0.1:13080、公网不可达、库是独立的 dshs_cross ⇒ 与生产互不影响。

10:2x — 产出交接单 T08(集群化落地 · 全程兼容单例模式)

  • 用户要求:梳理落地方案 → 拆分实现步骤 → 核对无误后逐步执行 → 要兼容现在的单例模式运行。
  • 交付:dsh-server-docs/交接单/T08-集群化落地-兼容单例模式.md(8 段模板齐备 + 步骤 S0–S7);交接单/README.md §一 已加 T08 行(🔵 待执行)。
  • 硬约束(用户口径落地成机械判据):每步结束必须过三关 —— ① smoke 全绿 ② 单机形态(deployMode=local)仍可完整跑通 ③ 47 生产 commit 一行不变。第 ③ 条是"兼容单例模式"的硬证据(验收 §6 第 2 条)。
  • 步骤概览(每步自带验证 + 回滚):
    • S0 前置与基线冻结(抢锁 / 记录 47 与 106 基线 / 47 跑一遍 smoke 存对照基线 / 开分支 feature/cluster-1a / 106 建独立测试目录 /opt/dshs-cluster 端口 13080)
    • S1 SQLite→PG(不改业务逻辑;迁移脚本;同一组 smoke 两侧对比;回滚 = 去掉 DSHS_DB_URL)
    • S2 数据模型 v7(dsh_hosts + 归属/租约列,SQLite 与 PG 两份同步;lease.ts 纯逻辑 + 单测)
    • S3 Worker agent + RemoteSpawner(同机自连验证,拓扑不变;含 launchToken 回传;回滚 = 切回 LocalSpawner)
    • S4 1a 形态(2 台 Manager,其中一台 capacity=0;单活退化验证)
    • S5 文件面跨机(RemoteUserFs 复用 file-service)
    • S6 第二台 Worker + join-worker.sh(必须建号 —— §17.3 实测 uid 无 passwd 条目则 os.userInfo() 失败)+ 计划内迁移
    • S7 自愈与观测(心跳用 HTTP 不用 ping;自动接管开关默认关)
  • 决策:D1–D5 已定(双活+支持单活 / 存储可插拔 / 自建 PG / 跨云只当测试床 / 测试环境用 106 另起一套);⏳ 仅 D6「生产拓扑最终落点」需用户拍板,且不阻塞 S0–S5;D7(MemorySwapMax)明确不在本单,只做只读确认。
  • 范围纪律:⛔ 不动 47 生产;⛔ 不动 106 既有 dsh-users-platform;⛔ 不动 orchestrator.ts 的隔离/配额;⛔ 不加自动化任务。
  • 文档库检查(改完必跑):docs-audit.py rc=0(无 P0、无悬空引用)|docs-manifest.py rc=0(已刷新)|docs-consistency.py rc=0。 docs-sync-check.sh 报 5 内容不一致 + 1 仅本地 —— 其中 属于我的是 3 个(新增 T08、交接单/README.md、docs-manifest.json);另 3 个是别的会话的未提交改动(BRIEF.md / DEPLOY-本部署.md / skills/dsh-opensource-release/SKILL.md)⇒ 我只报不动、不推(§三.0.⑥ 幽灵文件纪律)。规划会话不 scp(§四),服务器镜像留给执行会话。
  • 锁:--claim-exec "集群化落地规划(09-15)" → 检查 → --release-exec 已释放并复核(交接单/.exec-lock 不存在、无 .doing-*)。理由:要等用户核对,不能挂锁空转(§6「持锁期间若要等用户拍板 → 先释放再等」)。

06:3x — 源仓下线 K8s 后端 + 删除 @kubernetes/client-node(路线 C)

  • 用户指令:「又不是 dsh 主程序的,也不是插件需要的 ⇒ 可以备份起来,从项目代码中删除」⇒ 走影响说明 §9 的路线 C。
  • ⚠️ 动手前查到的关键事实(已向用户点明):源仓仍保留 K8s 后端,src/supervisor/{k8s-spawner,leader,reconcile}.ts 三文件 import 该依赖 ⇒ 「回源仓直接 npm uninstall」会打断源仓 tsc(我上一轮的表述有误,已更正)。 另:《集群化改造方案 Manager/Worker》把这些文件列为要照抄/泛化的模板(leader.ts 租约 / k8s-spawner.ts stampFencing / k8s-user-fs.ts + file-service.ts → RemoteUserFs / tcp-bridge.ts)⇒ 备份按「模板」组织,不丢。
  • 执行(源仓 D:/github/dsh_shenxian,基线 HEAD ed0c3c0,工作树干净):
    1. 9 个文件 mv 到 D:/github/_dsh_shenxian_K8s后端备份_20260915/(含 README.md:操作 / 还原命令 / 模板清单 / 刻意保留项);
    2. 脚本 _retire_k8s_from_source.py(CRLF 自适应,预演→落盘→复跑幂等)改写 4 个文件 14 处:provider.ts 整份重写、server.ts 换 fail-loud 守卫 + LocalSpawner、cli.ts 摘 5 import + 选主块 + 2 子命令 + dispatch + HELP、package.json 摘 2 测试入口 + smoke:file-service;
    3. npm uninstall @kubernetes/client-node --package-lock-only(用 --package-lock-only 避开原生依赖重编译;不手改 lock);
    4. 收尾 3 处悬空 JSDoc(user-fs.ts / spawner.ts ×2,文案逐字取自导出侧规则 ⇒ 导出物保持逐字节不变);
    5. 文档防坑:docs/k8s-deploy.md / k8s-deployment.md 加状态横幅(代码已下线,dshs file-service/tcp-bridge 已不存在);README.md 那句失实的「模式 B 已完整落地」改为「已于 2026-09-15 下线」。
  • 验证(最终态):tsc --noEmit exit 0|npm test 36 测试 / 35 通过 / 0 失败 / 1 跳过 + 注入校验全绿|npm run verify exit 0|依赖 0 命中|被删符号 0 命中|node lib/cli.js --help 只剩 server/bootstrap-admin|源仓差异 19 文件 / +29 −2690。
  • ⚠️ 刻意保留(非遗漏):deploy/·poc/01–04·Dockerfile.dsh·docs/k8s*.md·config.ts 的 7 个 K8s 配置字段与 DeployMode 联合类型(方案列为未来可选;server.ts 仍 fail-loud,填 k8s 启动即报错)。
  • 导出侧同步:_build_export.py 的 EXCLUDE_FILES / REGEX_RULES ⑦ 段现为幂等空转(命中 0)→ 注释标明「防御性保留」;台账/清单/影响说明/MEMORY.md 均已更新为「批 3 ✅ 已清,至此清单全部清零」。
  • ⚠️ 未提交:源仓 19 个文件改动未 commit / 未 push(等你发话)。

06:3x — 源仓 K8s 下线改动已提交(未推送)

  • 提交前取证「不影响当前项目」:生产 deployMode=local(DEFAULT_DEPLOY_MODE='local',生产无覆盖)⇒ 被删的 deployMode === 'k8s' 分支本来就不可达;file-service / tcp-bridge 子命令只在 K8s Pod 内调用;DeployMode 联合类型与配置字段未动(env/配置兼容)。唯一行为变化:真设 DEPLOY_MODE=k8s 时启动即报错(原会 new 一个已不存在的类)—— fail-loud,不静默降级。
  • 提交:cf8b7b1 chore(k8s): 下线 K8s 后端形态,移除依赖 @kubernetes/client-node —— 19 文件 / +29 −2690(含 package-lock.json −716 行,由 npm 自行重算)。提交后工作树干净。
  • 只 git add 了自己改的 19 个路径(未用 git add -A,按 R7)。本地 node_modules 里该包仍残留(无害;服务器 npm ci 会按新 lock 装出干净树)。
  • ⏳ 未 push(项目纪律 §4:push 需用户明确说「推送」)。

06:4x — 源仓 K8s 下线改动已推送到远端

  • git push -u origin master 成功:ed0c3c0..cf8b7b1 master -> master(快进,未 force),并顺手补上了 master 的上游跟踪。
  • 推送前先 fetch 对账(没盲推):FETCH_HEAD 显示远端 master = ed0c3c0 = 本次提交的直接父提交 ⇒ 确认是干净快进。
  • 独立复核:git ls-remote origin refs/heads/master = cf8b7b1f5c42c6fb0d17afb025a9d8fddea925c0 = 本地 HEAD ✅
  • ⚠️ 新发现的本机怪癖(已写进 MEMORY.md §三):该仓 refs/remotes/** 写不进去 —— git fetch origin 与 git update-ref refs/remotes/origin/master <sha> 都报成功,但 git show-ref/for-each-ref 里没有该引用 ⇒ git status 显示 ## master...origin/master [gone]。hooks 目录只有 .sample(不是 hook 干的)。 推送本身不受影响;与本机的历史结论「源仓 remote-tracking 一直读不到」一致。⇒ 与远端对账改用 git ls-remote origin refs/heads/master。
  • ⚠️ 本机 git 走 SSH 需加非交互参数(GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new"),否则可能挂在交互提示。
  • 锁:--claim-exec "源仓推送K8s下线-0639" → 已反序释放(释放前验过 OWNER)。

06:5x — 补删 K8s 命名为的死接口 LivePod(用户发现)+ 修正首轮两个副作用

  • 用户指出:src/supervisor/spawner.ts 的 export interface LivePod —— 注释已中性但名字来自 K8s 的 Pod,0 引用死代码,cf8b7b1 漏了。核实属实(源仓 src 里只有声明那一处)。
  • 🔑 提炼判据(已写进 MEMORY §一):K8s 残留要按三类扫 —— ① 字面量 k8s ② 语义措辞(Pod/sidecar/Headless…)③ 标识符名(LivePod/K8sSpawner/stampFencing…)。第三类最易漏(注释改了、名字还在)。扫法:用 K8s 词汇表捞非注释行里的 interface/type/class/函数名。
  • 顺带发现自己首轮留下的两个副作用并修掉:
    1. docs/k8s-deploy.md / k8s-deployment.md 的状态横幅被重复插入(cf8b7b1 里每份 2 条、挤在同一行;工作树又变 3 条)—— 根因:横幅规则不幂等,每跑一次多插一条。⇒ 补「H1 后已有横幅就不再插」的负向断言 + 一条把重复收敛成 1 条的自愈规则。
    2. lib/(构建产物,gitignore)里留着已删模块的编译结果(k8s-spawner.*/leader.*/reconcile.*/k8s-user-fs.*/tcp-bridge.*/file-service.*,共 18 个)⇒ 会让「全仓 grep k8s」出现假阳性,已移入备份目录 _stale_lib/。(tsc 不会自动清已删源文件的产物。)
  • 验证:tsc --noEmit exit 0|npm test 36 / 35 通过 / 0 失败 / 1 跳过|LivePod 全仓 0 命中(含 lib/)|两份文档横幅各 1 条。
  • 提交并推送:68c0a32 refactor(k8s): 删除 K8s 命名的死接口 LivePod;修正 K8s 文档横幅重复(3 文件 / +2 −10)→ cf8b7b1..68c0a32;远端 git ls-remote 复核 = 本地 HEAD;工作树干净。
  • 导出侧同步:_build_export.py 的 K8S_SEMANTIC_RULES 新增第 ⑫ 条(LivePod 整块删除,作兜底);导出物已无 LivePod;台账/影响说明/备份 README 均已更新(新增「K8s 残留三类扫」判据与脚本重跑注意事项)。
  • 锁:--claim-exec "删LivePod死代码-0649" → 已反序释放。

07:0x — 多语言(i18n)现状核查 + R5 口径更新(默认英语)

  • 用户问「项目支持多语言吗(英文/中文)」⇒ 三层核查(全部实测,非推断):
    • 官方 dsh 实例内 UI:✅ 语言开关(Settings → General);0.1.5-rc.1 覆盖已相当完整(见下)。
    • 自研插件(business-plugins「功能管理」):✅ 已中英双语,走官方 locale。
    • 平台自己的门户 6 页 + 注入浮层:❌ 仍是纯中文(<html lang="zh-CN"> + 硬编码,无 i18n 运行时、无切换入口)—— 属 R5 未做项。
    • 文档:开源仓 ✅ 英文为主 + 中文副本;源仓 docs/ 中文。
  • 🔴 推翻文献旧结论:档案 81 §10.7 ⑤ 记「官方只迁了 2 个包、20+ 包硬编码中文」—— 只适用 0.1.2-rc.1。实测 0.1.5-rc.1:客户端包 55 个中 36 个已走官方 locale API(locale.register/bind/addLanguage/LOCALE_IDS),其中 35 个含中文(那是它自带的 zh 词条,不是硬编码),不用 API 却仍含中文只剩 1 个(dsh-client-connection)⇒ 官方在后续版本已大面积补齐。 复现:对 <dsh 包根>/node_modules/@deepseek-ai/*/lib/client.js 逐包 grep -cE "locale\.(register|bind)|addLanguage|LOCALE_IDS" 与 grep -c '[一-龥]'。
  • 按「档案只增不改」处理旧结论:⑤ 处挂同源提示 + 文末追加 §10.8 修正(2026-09-15);§10.5 那句「切英文后官方包仍显示中文」挂一行指针。
  • 用户新口径(2026-09-15):「开发的插件需要沿用 dsh 官方项目的多语言方案,这个项目是全球化的项目,默认选中英语」⇒ 记入 §10.9 追加口径:
    • 自研插件现状已合规(poc/business-plugins/lib/client.js:locale.register(NS,{zh,en}) + locale.bind(NS) + inject=["slots","locale"])⇒ 无需改造。纪律:后续插件一律走官方扩展点,不自造词条运行时;register 要求 zh/en 同时提供。
    • 官方兜底即英语:FALLBACK_LOCALE="en" · LOCALE_IDS=["zh","en"] · 偏好存 settings.yaml 的 locale.preference(NS locale / field preference)⇒ 平台无需改官方代码或打补丁。
    • ⚠️ 唯一例外:navigator.language 仍参与解析(注释原文 “navigator.language trails the ordered …”)⇒ 中文浏览器且无偏好时可能自动落 zh。要「一律默认英语、忽略浏览器」⇒ 铺实例时把 locale.preference: en 写进该用户 settings.yaml(平台侧配置,不触官方代码;用户之后仍可在 Settings 自选)。
    • 平台 6 页的 R5 兜底语言由 zh-CN 改为 en;是否保留 navigator 跟随一步 = 我的取值(已在档案里标注可推翻)。
  • 文档库流程:抢锁 → 改档案 81 → docs-audit / docs-manifest / docs-consistency 全绿 → scp 同步镜像(600)→ 对账确认 81 已一致。
  • ⚠️ 两个旁证(不是我的改动,需留意):
    1. docs-shrink-guard.py 报 skills/dsh-decision-method/SKILL.md 491 → 296 行(-40%) —— 疑似被整文件重写抹行(本库规矩:共享文件只用 Edit 精确替换)。
    2. docs-sync-check.sh 报 5 个文件「内容不一致」:BRIEF.md · docs-manifest.json · DEPLOY-本部署.md · CODEBUDDY.md · skills/dsh-opensource-release/SKILL.md —— 均为其他 lane 的在途改动,镜像未同步(不在我本次清单里)。
  • 锁:--claim-exec "更正R5官方locale覆盖率-0703" → 已反序释放。

07:2x 会话 —— 「用户自传个人技能」支持度核查

  • 用户问:「记得之前提过用户可以自己上传、使用、管理个人 skills,看看项目支持吗」。只读核查(读文档 + 读源码 + 读源码仓 git ls-files + ssh 读 /opt/dshs 现状;未抢锁、未改任何库内文件)。
  • 结论:支持到「后端 + 落盘」两层,用户侧没有界面。 证据:
    • 后端 src/web/routes/skills.ts(686 行)6 个路由全 requireAuth:GET/POST /api/skills/mine、POST /api/skills/mine/apply、POST /:name/{enable,disable}、DELETE /:name;服务器 /opt/dshs 实测同一文件存在(grep -c = 7 处)。
    • 落盘:启用位 $DSH_HOME/skills/<name> / 禁用位 home/skills-library/<name>(dsh 无 disable 机制的绕法);dsh watch 即时生效,不需重启实例。安全加固(拒 symlink/设备/FIFO、解压 ≤2000 文件 / ≤200MB、lchown 跳过 symlink)已在(档案 41)。
    • 界面缺失:门户 #/skills 在 portal.html 标注 admin: true,只调 /api/skills/shared(共享层);web/ 已无 skills.html(档案 16 阶段 1 移除「我的技能」+ 加 role 守卫);实例内 poc/business-plugins/lib/client.js 只注册 4 个 settings.section(preferences / model-settings / business-plugins / platform-admin),没有「我的技能」。档案 41 文末自己也写「用户目前无界面可用,只有 API」。
    • ⇒ 缺口 = 待执行单 T01(档案 16 阶段 3/4;决策点 1 已于 09-12 拍板 = 扩展 business-plugins 加第二个 section,不新建包)。另:档案 11 「待办」里「dsh 会话内 /技能名 调用」的 UI 端到端验收至今未做(仅源码级确认 watch 即时性)。
  • 记忆库维护(系统提示 MEMORY.md 超注入上限被截断):MEMORY.md 19,979 → 12,054 字节(-40%);删的是重复与叙事,判据零丢失 —— 被压缩的详版(K8s 清理全量判据 / 兼容收口 / 圈选细节)原样搬到 PLAYBOOK-实例与插件坑.md 新增的 §7·§8(PLAYBOOK 6,682 → 10,842 字节),MEMORY 顶部加了详版索引。
    • ⚠️ 顺带纠正两处过期状态:① 「归档编号 至 93」→ 至 98(§一/§四 早已引用 96/97/98,登记行没跟上)② §二 新增「个人技能」行(本次核查结论)。
    • ⚠️ 未跑 docs-shrink-guard.py(本文件不在受保护根、不属文档库;该脚本按行数骤降报警,本次 MEMORY.md 行数 -40% 属有意的等价搬迁,非抹行)。

07:26–07:35 会话 —— 「技能与插件统一管理」口径适用性分析(已落 T01)

  • 用户问:「之前提的把 skill 和插件都看成功能(能力),统一管理,适用于用户管理个人 skills 吗」。
  • 原文定位(旧工作区会话目录 E:\ProgramData\.workbuddy\projects\d-AI技能-aliyun-dsh-server,用 extract-user-voice.py --needle 技能 --full 捞出):
    • 2026-09-12 09:18(会话 ca58e09f)原话:「…!业务技能能否打包到插件中 一起安装和使用,不要分开管理!…」→ 落地 = archive/交接单-已完成/T03 §4.1「技能随插件包投放」,判据内核 = 素材库 U4。
    • 同会话 09:35 追问:「是否需要把技能装到 $DSH_HOME/skills/ 还是就在插件内目录使用,如果不懂(不同)插件有相同名称技能如何处理」。
  • 结论(分两层,方向相反):机制/投放层 ❌ 不适用(个人技能不是投放物,没有第二条链路可合并;且 T03 §4.1 第 4 条「不覆盖用户自建同名技能」正是刻意把两向隔离)|入口/心智层 ✅ 适用(实例内那一栏已由档案 60 命名为「功能管理」,再另立「我的技能」section = 造反平行概念)。
  • 落地(本会话自决,可推翻):把 T01 的形态从「新增 settings.section」改为并入既有「功能管理」section 内分组。
    • 改 交接单/T01-档案16阶段3-4-实例内我的技能.md:§一 目标 + 完成判定加形态修正横幅;§三 范围表 client bundle 行;§四 新增决策 6(含依据与"仅入口层合并"的边界);§六 验收 B 措辞;新增 §九「口径依据与适用范围」(原话 / 两层结论表 / 8 维度机制差异对照表 / 相关档案)。
    • 改 03-路线图与待办.md:待执行行补「形态修正(2026-09-15)」+ 指向 T01 决策 6 与 §九。
    • 校验:docs-audit.py 无 P0 / exit 0(交叉引用无悬空);docs-consistency.py 全绿;scp 两文件到 /opt/dsh/docs(同相对目录、600)→ docs-sync-check.sh 本文件两项已从"内容不一致"消失(余下 5 项 = 其他 lane 的在途改动,不在我清单,按纪律不动)。
    • ⛔ 未跑 docs-manifest.py / docs-archive-index.py --write:前者会重写 docs-manifest.json(此刻正是别人的在途改动,R7 + 幽灵文件纪律);后者会把 INDEX.md 的 CR 翻倍(既有实测坑)。未 commit(用户未要求)。
    • 锁:--claim-exec "技能插件统一口径分析-0726" → 已反序释放。

08:25–09:1x 会话 —— T01 落地:「我的技能」并入「功能管理」分组(档案 100)

  • 用户:「规划一个产品方案(界面布局美观 方便操作)然后实现这个功能」→ 完整走完 方案 → 实现 → 投放 → 验收 → 归档。
  • 方案(档案 04-调整方案/100-实例内我的技能-并入功能管理分组.md):并入既有「功能管理」section 内分组(不新开 section);分组头(竖条标题 + 计数 + 右侧主按钮)/ 上传面板(虚线拖拽 + label 包隐藏 input)/ 技能行(名称 + 来源徽章 + 状态点 + 事实 tabular-nums + 动作,官方卡片行语汇 ⇒ 结构上不换行)/ 两个页内确认弹窗(替换 · 删除)/ zh+en 46 条词条。⛔ 仅入口层合并,机制层(两条 API、两套落盘、即时生效 vs 重启)一律独立。
  • 实现:poc/business-plugins/lib/client.js(+ 词典 + 状态机 loadSkills/submitUpload/applyReplace/toggleSkill/removeSkill + 参数化弹窗 skillDialog() + 渲染 + .bp-* CSS);package.json 0.3.20 → 0.3.21;新增 scripts/verify-my-skills.mjs(34 条断言:结构 / 危险操作二次确认 / 锁定行无按钮 / 版式不换行 / 措辞),并挂进 npm run verify。
  • 投放:npm pack(70,758 B / sha256 77429d2c…,与服务器一致)→ scp /opt/dsh/artifacts/ → ensure-biz-plugins.cjs --all --restart → 两实例 0.3.21,bundles 7 / 6 均含 @dsh-local/business-plugins: true(未被 pruneBrokenFileDeps 摘掉)。
  • 验收全绿:① npm run verify(词典 zh/en 各 366 键、引用键 343 全声明);② 06 §7 三段式(磁盘命中 / 壳页 combo 含该包 + rev=533537bbc02b / bundle 200 · 11,363,655 B 含新特征);③ 端到端 19/19(含共享技能 409、非 zip 400);④ agent-browser 全链路零缺陷(设置→功能管理→展开→选文件→上传 200→删除确认弹窗→删除 200)。
  • 🔑 本轮最可复用的收获 = 实例侧取数四坑(已写入 PLAYBOOK §9):① 抓实例页要带门户 sid(否则拿到登录页)② /plugins/ 只在实例子域且仍要 sid(否则假 404)③ 新用户 profile 首访才生成 ⇒ 铺 bundle 必须"先 enter → 再 ensure-biz-plugins → 再 restart";且直改 DB 置 active 会绕过"审批时铺 bundle"钩子 ④ POST /api/dsh/restart 要 {"command":""}。另:role 取值是 active 不是 approved。
  • ⚠️ 踩坑(文档侧):Edit 工具改 INDEX.md 会把整个文件判为改动(该文件是历史 \r\r\n 混合行尾)⇒ 已按兜底法恢复(镜像 == HEAD 字节)+ 改用字节级单行插入(_tmp_t01/fix_index.py),最终 diff = 1 增 1 删。同类改 INDEX 一律走字节级。
  • 收尾:T01 填回报 → git mv 到 archive/交接单-已完成/ → 镜像同步(旧路径已删,对账 0 仅本地 / 0 仅服务器)→ 03-路线图 / 交接单/README / INDEX §二 指针同步;docs-audit 无 P0、docs-consistency 一致。临时用户与 /tmp 脚本全部清理(pocui.txt / probe.json 是 09-11 别人的,按 R7 不动)。未 commit(用户未要求)。
  • 锁:--claim-exec "T01我的技能-0825" → 已释放。