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

278 lines
48 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工作日志 · 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"` → 已释放。