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)、记忆修复前备份。
This commit is contained in:
admin committed 2026-09-24 07:51:03 +08:00
commit ce8e6ceed9
396 files changed
+66045

No files matched your search

+588
View File
@@ -0,0 +1,588 @@
# 2026-09-20
## 04:3x · IM 群组对话 —— 需求拍板 + 规划档案 142 落盘
- **用户拍板 B 方案**(人 + Agent **一次做齐**),并给出**两条决定形态的语义**:
① **群聊消息 = agent 的上下文**(不是只看到 @ 它那一条);
② **agent 两种形态** —— **能力机器人**(挂功能区、**占独立成员位**、被 @ 才答)/ **组员代理**(组员开「待应答」后 @ 组员 = 其 agent 代答;**组员与 agent 是一个整体 ⇒ 不占独立成员位**)。
- **追加硬要求(平台化)**:IM 群组**不是单一场景功能** —— 原话「不同插件都要能接入,比如**协作办公插件群**、**mud游戏群**、**跑团群**等」
⇒ 形成**第二个关键判断**:内核只做通用地基,**七个扩展点**(房间类型 / 消息载荷 / 成员角色 / **发言规则** / 能力机器人 / 事件订阅 / 房间内 UI)下沉到插件;
配 **三条平台保护**(插件不直连 DB · 插件故障不拖垮房间 · 能力吃预算)+ **两档传输**(**聊天档** 按 107 口径 / **实时档** 按 108 游戏口径 —— 两档不是同一套参数)。
⚠️ 跑团要**回合制发言**、MUD 要**低延迟**,均与自由并发聊天不同 ⇒ **发言规则必须是插件可定义的**,否则第一类新场景进来就要改内核。
- **产出**:`04-调整方案/142-IM群组对话-需求基线与方案.md`(规划态)+ `INDEX.md` 登记行;`docs-audit.py` **RC=0 / 无 P0**;新档 CR=0(纯 LF)。
- **取证结论(跨会话可复用,勿重查)**:`src/` 应用层 `room|chat` **零命中**;`schema.ts` 11 个迁移**无消息 / 房间 / 成员表**;
用户间通道**故意隔离**(`src/net/relay/server.ts:462`);可复用 = relay ×2 / `DIAL` / `PRESENCE` / 一机一钥 / 网抽象 `ops`·`u:<id>`。
- ⏳ **未拍板**(档案 §六):**首批场景优先级**(办公群 / MUD / 跑团)· UI 落点(门户面板 vs 独立页)· 房间上限与发言预算数值(待压测标定)· 插件扩展开放边界 · E2EE 是否本期。
- ⛔ 本轮**零代码改动、未动服务器、未 commit / push**;全局执行锁**已释放**。⏭ 下一棒 = 规划细化(出可执行交接单),automation 一次性排 04:41。
## 04:5x · IM 群组对话 —— 规划细化:五份可执行交接单落盘
- **产出(全部在 `dsh-server-docs/交接单/`,纯 LF)**:
`IM群组-A-房间内核与DB.md`(10,024 B)· `IM群组-B-平台API与用户端IM-WS通道.md`(8,409 B)·
`IM群组-C-Agent接入插件.md`(9,283 B)· `IM群组-D-插件SDK与扩展点契约.md`(10,052 B)· `IM群组-E-会话界面UI.md`(8,033 B);
`交接单/README.md §一` 增补 5 行 + 1 段串行建议(`git diff --numstat` = **8 / 0**,未触碰既有行)。
- **验收**:`docs-audit.py` **RC=0 / 无 P0**(改前基线同为 RC=0;对比无新增问题);新档 `CR=0`。
- **串行建议**:**A → B →(C / D 可并行)→ E**;除 A/B 前置于后三者外,C/D/E 文件不重叠,但**并行度仍受全局执行锁约束**。
- **本轮新取到的锚点(跨会话可复用,勿重查)**:
① `src/db/schema.ts` 迁移**到 v11 为止**(`MIGRATIONS` 第 476 行起)⇒ IM 新表 = **v12**,且**双后端**(`sqlite` + `pg` 各一份 SQL);
② 平台路由 = `src/web/server.ts:58–68` 集中 import **11 个** `routes/*.ts`,**无 `im.ts`**;
③ **全仓只有 1 处 WS upgrade 处理点** = `src/supervisor/proxy.ts:718`(per-user 子域隧道)⇒ B 单必须**在同一 upgrade 回调里按路径分流**,⛔ 不改隧道语义;
④ 🔴 **前置缺口(C 单第一件事)**:`poc/business-plugins/lib/index.js` **仅 21 行**、server 侧**不触达平台**;能力全在 `lib/client.js`(**4,001 行**)且靠**浏览器 session cookie** 跨子域 ⇒ 实例内「**服务端拨出**」**缺一条凭据通道**(已在单里定为「平台生成 per-instance token → orchestrator 注入 env `DSH_IM_INSTANCE_TOKEN` + `DSH_PLATFORM_IM_URL`」,实例重启即换);
⑤ 实例 env 注入块 = `src/supervisor/orchestrator.ts` 第 **760–780** 行一带;测试布局 = `test/*.test.mjs` + `node --test`(构建产物 `lib/`)。
- **未决项(⛔ 未替用户决定,已逐单显式标注)**:**UI 落点**(门户新面板 vs 独立页,档案 142 §六-1)· **首批场景优先级**(办公群 / MUD / 跑团,§六-5)·
**E2EE 是否本期做**(§六-3,会改协议形态;我在 D 单给了倾向 A=本期不做并列了优缺点)· 房间上限与发言预算**数值待压测标定**(§六-2,单内先给保守初值:房 2000 / 每 agent 每 10 分钟 10 条 / 静默 30 s)。
- ⛔ 本轮**零代码改动、未动 47/106、未 commit / push**;全局执行锁**已释放**。⏭ next = 用户拍板(UI 落点 / 首批场景)后按 A 单开工。
## 08:1x · IM 数据面追加裁决(DB 选型 · 缓存队列 · 对象存储)→ 已写进 A 单 §九
- 用户追加四问(多插件接入表名隔离 / 多领域·项目·会话组 / 主从与负载均衡 / 缓存与队列预留),随后再补一问(**IM 会话产出各类文件 ⇒ 考虑接对象存储桶**)。
- **实测取数(本轮新取,跨会话可复用)**:`package.json` 依赖里与数据相关的**只有 `better-sqlite3` + `pg`**;`src/` 里 **0 处** `redis` / `ioredis` / `bullmq` / `amqplib`/ cache 抽象 ⇒ **缓存与队列是空地,不是"已有待改造"**。
- **裁决(全为技术项,自决 · 已落 `交接单/IM群组-A-房间内核与DB.md §九`,标注为「设计约束,冲突时以本节为准」)**:
① 插件表隔离 = **命名空间前缀 `p_<pluginId>_*` + 内核 API**,⛔ 不给插件直连 DB(前缀只解决物理隔离与归属);
② 多领域/项目/会话组 = **同一套房间/消息模型 + 维度字段**(`owner_scope` · `room_type` · `thread_id` · `project_id`),⛔ **不按领域分表**(否则新场景就要改内核);
③ 主从/负载均衡 = **保留 PostgreSQL**(既有 PG13 + `src/db/` 双后端可切)> MySQL > SQLite(**不支持主从**);⚠️ **读写分离按数据分类**:元信息读可走只读副本,**消息读必须走主/sticky**(否则"刚发的消息看不见",违反游标补拉语义);
④ 缓存与队列 = **预留端口、不预置进程**:队列先用 PG(`SKIP LOCKED` + `LISTEN/NOTIFY`),Redis 仅在「跨机扇出/presence 广播」实测成热点后引入(**单实例 ≤128–256 MiB** 硬前提,宿主内存本就紧:实例硬顶 1024 MiB / 宿主 1870 MiB);端口 = `src/im/cache.ts` / `src/im/queue.ts`;
⑤ **文件与对象存储** = **桶存内容、PG 存元数据**(`files` 表带 `sha256`/`bucket_key`,⛔ 大对象不进 PG);**S3 兼容**(MinIO 自建或 OSS/COS 兼容端点)不锁厂商,端点/密钥走档案 140 的 `config/platform.env`(不入库、默认中性);**上传 = 预签名直传桶(平台不经中转)**,**下载 = 短 TTL 预签名(≤5 min)**、需审计时走 `/api/im/files/:id` 代理;消息 payload 只存 `file_id`(保住扇出与两档传输口径);生命周期 = 内容寻址去重 + 桶 lifecycle(⛔ 不做同步删);权限**复用房间 ACL**;插件产出物**走同一 files 面**(⛔ 不各自建桶/凭据);转码抽帧**不在内核**(走后处理队列);端口 = `src/im/files.ts`,首版单桶多前缀 `rooms/<roomId>/<yyyymm>/<sha256>`。
- **立单时机**:文件面依赖 A 的 ACL 与 B 的接口面 ⇒ **等 IM 主链 A→B 跑通后另立 F 单**;本轮只把约束写死(⛔ 未在 A 单里顺手实现,未改五单范围)。
- 验收:`docs-audit.py` **RC=0 / 无 P0**;A 单 15,828 B / CR=0。⛔ 仍**零代码改动、未动服务器、未 commit**。
## 08:2x · IM 数据面追加(插件如何建表与管数据)→ 已写进 **D 单 §九**
- 用户追问「**插件如何建表以及访问和管理数据**」⇒ 契约落在 **`交接单/IM群组-D-插件SDK与扩展点契约.md` 新增 §九「插件数据面契约」**(A 单 §九-1 加了指针互指;D 单那行台账也补了说明)。
- **裁决(技术项,自决)**:
① **先判落点再建表**(沿用集群化分层判据):跟房间走的业务数据 ⇒ **平台库** `p_<pluginId>_*`;只属于某用户自己的 ⇒ **实例 home(走 `UserFs`)**;本机运维 ⇒ Worker 本地库。⛔ **同一份数据不双写**(双写=脑裂)。
② **建表 = 声明式,⛔ 零裸 SQL**:中性类型集合(`text/integer/bigint/real/boolean/json/timestamp/uuid`)由内核翻译成 **sqlite + pg 两套 DDL** ⇒ 同时保住「双后端可切」与「插件不直连 DB」;内核建表时自动补前缀/`room_id`/审计列。⛔ 禁自定义类型、触发器、存储过程、跨前缀 JOIN。
③ **迁移只增不减**:只允许加列(带默认值)/加表/加索引;在**启用插件时**执行,任一步失败 ⇒ **拒绝启用**(不做半迁移)。
④ **访问走内核数据 API**(`table().insert/update/delete/find/count` + 游标分页):room-scoped 表**自动注入房间 ACL**(复用 `canSee`);跨前缀/内核表**只能经内核 API**;事务仅限同前缀多表;写与"刚写就读"**走主库**。
⑤ **管理**:三重配额(总行数/单行字节/每房行数,超限给明确错误码,⛔ 不静默丢/截断)· 插件写吃**与 agent 同族预算** · DDL 与超阈值写记入内核表 `plugin_data_audit` · **卸载默认保留 30 天**,`DROP TABLE` 属不可逆 ⇒ 必须 **admin 显式确认 + 先出清单**(⛔ 不做"卸载即删")。
⑥ 数据面异常(超时/超配额/迁移失败)⇒ **降级为普通聊天**,房间消息流不受影响(142 §3.6 保护 2)。
- **D 单新增验收断言 8–13**:零裸 SQL / 双后端可建 / 越权拒绝 / 房间 ACL 为 0 行 / 迁移拒绝删列改类型 / 卸载保留与 DROP 确认。
- 验收:`docs-audit.py` **RC=0 / 无 P0**;A 单 16,017 B、D 单 14,468 B、README 164 CR(**均为纯 LF 新档 / 既有 CRLF 未变**)。⛔ 仍零代码改动、未动 47/106、未 commit / push。
## 21:5x · 「数据库专区」成稿(**抢锁失败 ⇒ 未落文档库,全部暂存工作区**)
- 🔴 **抢锁失败(R9 停手,未碰文档库任何文件)**:`交接单/.exec-lock/OWNER` = **`序47执行棒2`**,开始 **09-20 21:27**、`doing` 位为空(与记忆里那支 automation `90049d4c…` 一致)。双证:`handoff-guard.sh --claim-exec` **退出码 1** + Python 直读锁目录。
⚠️ **环境坑(新增)**:本轮 `bash …/handoff-guard.sh` 的**中文输出变乱码**(UTF-8/GBK),且 **coreutils 间歇缺失**(`head`/`tail`/`grep` 报 `command not found`,PATH 被重置 ⇒ 会误触 WSL 阻断)⇒ **判锁改用 Python 直读 `.exec-lock/OWNER`**(`ls`/`python` 正常,管道到 `head` 不可靠)。
- 🔴🔴 **本轮最重要的实测发现(会误导后续执行者,必须优先修正)**:`src/db/schema.ts` 迁移**已到 v13** ——
**v12 = `overlay device ledger (序㊻ 步骤6 / S3)`**(新表 `overlay_devices`)、**v13 = `session/device kind + session cap index (序㊼ S0)`**。
⇒ **IM 房间三表必须取 v14**;而 `交接单/IM群组-A-房间内核与DB.md` 现写「新增 **v12**」,且其「只读前置 #1」原写「**若已 >11 ⇒ 说明别人先做了 ⇒ 停手回报**」——**这句现在会误判**(v12/v13 是覆盖网络线占的,不是 IM)。
- **成稿(暂存,未入文档库)**:`E:/ProgramData/AI技能/aliyun-dsh-server/tmp/数据库专区_草案_20260920/`
`00-专区入口.md`(入口 + 五条判据 + **迁入清单**)· `01-接入指南-各种情形.md`(**六种接入情形**:内核表 / 插件表 / 实例 home / Worker 本地库 / 大对象桶 / 缓存队列)·
`02-表结构台账与迭代机制.md`(**v1–v13 台账 + 内核表全集 + 三条硬纪律 + 四步跨版本 + 评审门禁 + 台账维护规则**);
另 `tmp/插件数据面规范_草案_20260920.md`(= 专区 `03-插件数据面规范.md` 的成稿)。
- **迁入清单(锁释放后一次做完,见 `00-专区入口.md` §🔴)**:抢锁 → `mkdir dsh-server-docs/数据库/` 落 4 档(**新目录不需占号**)→ `INDEX.md` 登记 →
**修正 A 单(v12→v14 + 改掉"停手回报"那句误判判据 + §五-1 的 `SQLITE_V12/PG_V12`→`V14`)** → D 单 §九 加指针 → **删 tmp 草案(不留双源)** → `docs-audit.py` RC=0 + 纯 LF。
- ⛔ 本轮仍**零代码改动、未动 47/106、未 commit / push**;**未登记后续 automation**(锁在别人手里 + 排 automation 属花钱项,等用户口径)。
- 🔑 **跨会话可复用取数**:`grep -nE "^ \{ version:" src/db/schema.ts` = 版本清单(**现有 13 条**);`grep -noE "CREATE TABLE( IF NOT EXISTS)? [a-z_]+" src/db/schema.ts | sort -u` = 表全集(内核保留名 13 张 + `schema_migrations`)。
## 22:4x · 迁入做成「一条命令」(锁仍未释放,文档库零改动)
- 🔴 **锁竞争第 3 次**:`MCN线0.3.13棒` **自 22:10 持锁至今**(22:39 仍在,已 30 分钟)⇒ 两轮 `--claim-exec` 与一次实跑均**抢不到 ⇒ 按 R9 停手**,文档库**零改动**。
- ✅ **本轮产出 = 可一键执行的迁入脚本**:`tmp/迁入数据库专区.py`(**已三次 `--dry` 全绿**,实测抢不到锁时**零改动退出**并打印 OWNER)。
它一次做完 7 件事:① `mkdir 数据库/` 落 4 档(去掉草案头、加「已落文档库」状态行、LF)② 把 A 单 **v12→v14**(含**改掉「若已 >11 ⇒ 停手回报」那句误判**,改为「**v14 被占 ⇒ 取下一个空号继续,不要停手**」,并写明 v12/v13 = 序㊻/序㊼ 的 `overlay_devices` 等、与 IM 无关)③ D 单 §九 加专区指针 ④ **INDEX.md 二、全量清单 字节级插入一行** ⑤ 打印各档字节数与 CR 数 ⑥ 跑 `docs-audit.py` 打印 RC ⑦ `finally` 释放锁。
- **两个"防坑"设计(写脚本时踩到/主动做的)**:① 版本号**现算**(`schema.ts` 里 max+1)而不是写死 —— 序47 正在改代码,写死必然过时;② INDEX.md 用**字节级插入**(该文件 CR=5/LF=269 混排)而不是整文件 Write 覆盖 —— 遵循 README §三「共享文件禁整文件覆盖」。
- **⚠️ 脚本首个版本曾断言失败**:`\bV12\b` 匹配不到 `SQLITE_V12`(`_` 是 word 字符 ⇒ 无词边界)⇒ 改为无边界 `V12`。**教训:跨 `_` 的标识符不能用 `\b`**。
- ⏭ **下一步 = 等窗口跑那一条命令**:`"<py>" "E:/ProgramData/AI技能/aliyun-dsh-server/tmp/迁入数据库专区.py"`(跑前可先 `--dry`)。最佳窗口 = MCN 棒收口后 **5~8 分钟**(其下一棒按纪律才排)。⛔ 仍零代码改动、未动 47/106、未 commit。
## 22:5x · ✅ 数据库专区**已迁入文档库**(锁一释放就执行完毕)
- **锁窗口**:22:46 复查发现 `MCN线0.3.13棒` 已释放 ⇒ 一键脚本抢到锁 → 落库 → 释放。
- **产物(文档库新目录 `dsh-server-docs/数据库/`,全部纯 LF)**:
`DB-00-专区入口.md`(4,835 B) · `DB-01-接入指南.md`(6,588 B) · `DB-02-表结构台账与迭代.md`(6,737 B) · `DB-03-插件数据面规范.md`(9,625 B);
A 单已改 **v12 → v14**(含改掉会误判的"停手回报"判据、写明 v12/v13 = 序㊻/序㊼ 的 `overlay_devices` 等);D 单 §九 已加专区指针;`INDEX.md` 二、全量清单 **字节级 +1 行**。
- **最终验收**:`docs-audit.py` **RC=0 / 无 P0**;`INDEX.md` **CR=5 未变**(行尾零扰动);专区四档 CR=0;全局锁**已释放**。
- 🔴 **两条新踩坑(跨会话必记)**:
① **新目录下的文档不能用 `NN-` 文件名前缀** —— `docs-audit.py` 的正则是 `^(\d+[a-z]?)-`(**只看文件名**),`01-/02-/03-` 会被当**档案编号** ⇒ 与 `04-调整方案/01..03` 撞号 ⇒ **RC=1**。正解 = 用**非数字前缀**(本次改 `DB-00…DB-03`),标题里可继续保留序号(`标题号 ≠ 文件名号` 只在**两边都解析出编号时**才比较)。
② **`\b` 匹配不到 `SQLITE_V12`**(`_` 是 word 字符 ⇒ 无词边界)⇒ 跨下划线的标识符替换**不要用 `\b`**。
- **清双源**:`tmp/数据库专区_草案_20260920/` 与 `tmp/插件数据面规范_草案_20260920.md` 已**移入** `D:/github/dsh_shenxian/_中间产物_待清理/`(⛔ 未直接删,可回溯);工作区 `tmp/` 只留两个脚本:`迁入数据库专区.py`(⚠️ 一次性,重跑会在 A 单断言处安全中止)、`修正数据库专区命名.py`。
- ⏳ **残留小项(未做,等用户口径)**:① `scripts/docs-manifest.py` 机读清单未刷新;② 未 `scp` 到服务器镜像 `/opt/dsh/docs`;③ **未 commit / push**(红线:未明确要求不提交)。⛔ 全程未动 47/106 服务、未改一行代码。
- 📌 **技能待补(下一步做)**:把上述坑①(新目录文档命名与 audit 编号规则)补进技能 `dsh-knowledge-upkeep`(本轮上下文已近上限,未做)。
## 22:5x · 收尾收官(清单 / 技能 / 服务器镜像)
- ✅ **机读清单**:`scripts/docs-manifest.py` **RC=0** —— 文档 **213 份 / 2,234,077 字符**(档案 142 份;分层 hot 6 / warm 61 / cold 50;根级文档 71)⇒ 已含新 `数据库/` 目录。
- ✅ **技能补坑(三处 md5 一致 = `53d73df7`)**:`dsh-knowledge-upkeep` 追加「新增目录的文档命名 vs docs-audit 编号」一节 —— 本机 `.workbuddy/skills/` + 文档库 `skills/` + **服务器镜像 `/opt/dsh/docs/skills/`**(第三处本轮已 scp 并对账)。
- ✅ **服务器镜像**:`/opt/dsh/docs/数据库/`(**新建目录 700**)落 `DB-00~DB-03` 四档 + `交接单/IM群组-A-房间内核与DB.md`、`IM群组-D-插件SDK与扩展点契约.md` 共 **6 档 scp 全部 ✓**;权限 = 文档 600 / `DB-00-专区入口.md` 644(README 类)。
- ⚠️ **`INDEX.md` 未推(主动跳过)**:远端 md5 `8350cbc3` ≠ 本地 `b9c7b056` ⇒ 按 README §三-0-⑤「**防迟到的推送覆盖别人的新版本**」只报不动,**留人工对账**(这是本轮唯一残留动作项)。
- ⏳ 仍**未 commit / push**(红线:未明确要求不提交)。⛔ 全程未动 47 的生产服务(只写了 `/opt/dsh/docs` 镜像目录)、未改一行代码。
## 08:0x–08:5x · MCN 工作台「点了没反应」+ 用户设置切语言「没反应」→ 已修复并真机验收(档案 143)
- 用户:「admin账号dsh会话 点击左侧 MCN工作台入口 没有反应,找到原因修复后把MCN任务会话入口的位置改到MCN工作台 header区域(用图标即可)」+「设置选项中 用户设置里面切换 语言还是不起做用」。
- **两条同源:dsh 0.1.5-rc.1 改了插件依赖的老契约**(不是用户操作、也不是"改不生效"):
① 对话区槽位名 `conversation` → **`main.conversation`** ⇒ 插件 `querySelector('[data-slot="conversation"]')` 实测恒 null ⇒ 面板宿主 `pageHost` 一直 null、portal 不渲染 = **点了没反应**。
② `ctx.locale.getLocale()` 由「返回 id 字符串」变成「返回**快照对象** `{active,locales,revision}`」⇒ `String(obj).split("-")[0]` = `"[object Object]"` ⇒ 受控 `<select>` 匹配不上任何 option(浏览器显示第一项 English,而界面本来就是中文)⇒ 选「中文」看着无变化。
- **改动**:插件源 `dsh-plugin-mcn/lib/client.js`(槽位双名自适应 + 配套 CSS;「MCN任务会话」从左侧栏搬进工作台 header 图标、`V1_ICON_INNER` 加 `chat`、面包屑补名)⇒ 重打包 **0.3.11**(md5 `0b84b3bc…`);`poc/portal-entry/lib/client.js` 的 `currentLocale()` 兼容两形态 ⇒ **0.5.6**。
- **顺手修掉构建链三处「一构建就负向回归」**:`CLIENT_SOURCES`/`HOST_SYNC` 仍列着已摘除的 `dsh-plugin-mcn-schedule`(会把计划任务装回来)|源文件里 v0.3.10 的「自动任务」菜单摘除编辑丢了(死链)|`pack` 未排除 tgz(1.9MB 历史包会被打进新包)。⇒ 重打包后与**线上旧产物** diff = **9 hunk / 51 行**,全部为本次改动。
- **真机验收(agent-browser + mksess 临时会话)**:点工作台 ⇒ `#/mcn` + `[data-mcn-split]`/`[data-mcn-panel]` 出现;header 会话图标 ⇒ 切到 MCN任务会话 视图;左侧栏已无独立会话行;语言行初始 `zh`(修前显示 en)、往返切换 `lang` 跟随、`settings.yaml` 落 `locale: preference: zh`。平台侧 `verify-portal-entry.mjs` 28 条全绿(新增「快照对象取 .active」断言)。
- **未覆盖**:guest 实例在 w-106 ⇒ 其 `portal-entry` 仍 0.5.5、mcn-suite 未装(47 侧 `NO_PROFILE` 跳过;档案 120/138 的集群化通用缺口)。
- 收尾:测试会话已删、服务器 /tmp 暂存件已清、档案 143 + INDEX 登记 + 镜像 md5 对账一致;**未 commit / push**。
- 工具坑(已记进档案):`agent-browser` 前台会被沙箱 SIGTERM(`run_in_background` 起一次守护后,后续加 `> 文件 2>&1` 不走管道即可前台);`set headers` 注 Cookie 无效 ⇒ 用 `eval document.cookie` 注入;手动跑平台脚本须带 `/etc/dshs.env` + `dshs.service.d/*.conf` 的 `Environment=`。
## 09:1x–09:5x · guest 补齐 + 规模化方案(档案 144)
- 用户:「guest的也处理,后续几百上千个用户的时候,遇到种类问题如何处理,有没有更好的方案」。
- **guest 已补齐**:106 上 `@dsh-local/portal-entry` 0.5.5 → **0.5.6**(pnpm add 成功、`dsh.profile.bundles` 含本包、包内 `client.js` 命中 `v = v.active`),
实例停过后由平台按需拉起;**浏览器实测**:初始态 `lang=zh-CN` 且下拉 `zh`(**修前恒显示 en**)、切 English ⇒ `lang=en` + 下拉 `en`(一致)。
⚠️ `dsh-plugin-mcn-suite` 在 106 仍**未装**(本来只有 admin 装,非遗漏)。
- **新基元**:`scripts/install-plugin-for-user.cjs` —— 机器无关 / 幂等 / 包名与版本**从 tgz 里读** / `--expect` 不符退 **2** / 失败**不静默**;
已用于 guest。它是「几百上千用户」编排的**原子动作**(Manager 侧按 Worker 分组后逐机跑同一条命令)。
- **方案(档案 144,待拍板选档)**:真实瓶颈**不是耗时** —— 单用户 `pnpm add` 秒级 ⇒ 1000 用户串行约 1 小时级、按 Worker 分组并行是分钟级;
真正的问题是四条:① 编排脚本**在本机枚举用户** ⇒ 迁到别的 Worker 的用户被**静默跳过**(guest 就是这么被漏的)② **无版本台账** ③ 每用户一份拷贝 + 无版本协商
④ **契约失效零报错**(档案 143 两条真因都不抛异常)。
**推荐**:必装插件从「每用户安装」→「**Worker 共享只读插件层 + 每用户只留开关**」(与 `bundled-skills` 同族,沙箱 scope 里已有 `--ro-bind-try` 先例)⇒ 升级从 O(N) 降到 **O(1) 份内容**;
顺序 = P0 台账(PG)+按 Worker 分组编排+三态输出(半天~1 天,立刻止血)→ P1 契约哨兵 + 失效可见化 → P2 共享只读层(**需先 PoC**:dsh 能否解析 `node_modules/<pkg>` 软链 / ro-bind 白名单 / 多版本策略)→ P3。
**未建接续棒**(要用户选档,选完再出孪生交接单)。
- 收尾:测试会话已删;106 与 47 的临时件已清;档案 **143/144** + INDEX + 镜像 md5 对账一致;**未 commit / push**。
## 09:3x–09:5x · 用户追问「插件共享是否让不同用户互访数据、安全性下降」→ 已答并写入档案 144 §五
- **结论:共享的是「不可变代码」,不是数据;隔离靠 uid + bwrap,不靠"每人一份拷贝"。**
实测(47):`users/<uuid>` = `drwx------` + 属主=该用户 uid(`/var/lib/dshs` 与 `users/` = `711 root`)⇒ 用户 A 的进程**连 B 的目录都进不去**;
实例 scope 里 `--tmpfs /var/lib/dshs` + `--tmpfs /var/lib/dshs/users`(别人的路径在沙箱内**根本不存在**)+ `--bind <uuid>/tmp /tmp`(/tmp 每用户私有)
+ `--ro-bind-try /var/lib/dshs/bundled-skills`(**已有"共享只读层"先例,就是 ro 挂的**)。
插件自有数据(MCN 的 `home/.dsh/mcn-plugin.db`)属主即该 uid ⇒ 共享代码后**仍每用户一份**。
另:**不是"一个共享进程服务所有用户"** —— 每用户仍是各自 uid 的独立实例进程,只是**同一份文件被多个进程只读加载**。
- **共享化真正的利弊**:更好 = 今天插件副本在用户 home 里**用户可写**(能改自己的插件代码),共享层 `root:root 0755 + ro-bind` ⇒ 插件代码**用户不可篡改**(增强);
更差 = **爆炸半径 1 → N**(共享层被投毒/误投会同时影响所有用户)——这是唯一真实新增风险。
- **四条红线(写进 144 §五)**:① 只放不可变代码/静态资源,⛔ 绝不放 DB/缓存/日志/上传物;② 沙箱内**必须 ro-bind** + 宿主 0755 root、用户不可写(rw 绑定比互读数据更严重);
③ 版本目录不可变 + 只走 admin 投放 + sha256 清单 + 定期完整性校验;④ 插件数据落点仍按既有判据(跟用户走 ⇒ 实例 home 且 host 半边走 `UserFs`;平台级 ⇒ 平台库带前缀+ACL),⛔ 禁写宿主机固定路径。
附带:共享后**灰度/回滚更容易**(改指向即可,不必再跑 N 次 pnpm)。
- 同步:144 §五 已入文档库 + 镜像(md5 一致);原「待拍板」顺延为 §六。
## 11:0x–11:2x · 用户追问「共享插件用起来产生的数据 / 生成文件 / 日志 / 临时文件放哪」→ 写入档案 144 §六
- **一句话**:共享层只放**永不变的东西(代码 + 随包静态资源)**;一切运行期可变状态按判据落到**每用户自己的目录**或**平台库**,⛔ 不进共享层。
- **先分"三个半边"**(决定能写什么):client(浏览器,只有 localStorage,要落盘必须调 host)/host(实例进程,uid=该用户 ⇒ 能写自己 home / ws / /tmp;⛔ 写不进共享层与别人 home)/平台(dshs 进程 ⇒ 写平台库与 /opt/dsh/{state,backups},⛔ **直写用户 home 必须走 `UserFs`**)。
- **五问判据**:① 跟用户走 ⇒ `<userRoot>/home`(权威)② 用户要看/下载 ⇒ `<userRoot>/ws`(可见;⚠️ 可见面≠权威面,不双写)③ 全平台一份 ⇒ 平台库 PG(插件前缀 + ACL)④ 可丢的中间物 ⇒ 实例 `/tmp`(**每用户私有**)或 home 下 cache ⑤ 运维/审计 ⇒ `/opt/dsh/{state,backups}`。
- **实测(47 admin 实例)**:`home/.dsh/mcn-plugin.db`(正确落点=DSH_HOME)· `home/mcn-plugin.db`(**旧写法残留**,正是"落点没按判据"的历史垃圾)· `home/.dsh-biz-plugins.log` + `home/.dsh-poc-portal-entry.log`(平台 ensure 脚本的 per-user 日志,同族做法)· `home/.node-compile-cache/`(可丢缓存)· `ws/{mcntimo,mcnworkspace}`(产物)· 沙箱 `/tmp` = `--bind <userRoot>/tmp /tmp`(每用户私有)· 实例进程 stdout/stderr 归 **systemd/journal** 按 unit 采集。
- **两条铁律**:① 可变状态一律不进共享层(ro 绑定写不进,设计也不允许);② ⛔ 不写宿主机**固定路径**(`/var/lib/dshs/logs/x.log`、`/tmp/plugin-y`)——多用户写同一路径 ⇒ 互见 + 冲突 + 无法按用户清理。
- **迁移/备份连带**:权威数据(home)+产物(ws) 都在 `<userRoot>` ⇒ 只搬它(档案 120 流程不变);缓存与 tmp 不参与语义;共享层只是"代码",用户数据一个字不用碰。
- 同步:144 §六 已入文档库 + 镜像(md5 `b48c97b5…` 本地/远端一致);原「待拍板」顺延为 §七。
---
## 14:3x 桌面线对接单核对(覆盖网络线 · 平台侧只读取证,未改任何文件)
**来单**:`E:\ProgramData\AI技能\dsh-ai1net-desktop\docs\对接单_桌面端接入覆盖网络_给平台会话_20260920.md`(桌面线第 5 棒卡"凭据签发")。
**问的事**:加入覆盖网络是否必须凭证、为什么。**答**:必须,且绕不过;但"凭据 ≠ 账号/注册/登录"。
**实测取证(47 · 只读)**:
- 🔴 **纠正来单 §2.3 ①**:47 的 relay/dshs env **都没有** `DSHS_OVERLAY_SIGNER_PUBKEYS`;现网走的是 **`DSHS_OVERLAY_SIGNER_SET_FILE=/etc/dshs/overlay-signers.json`**(SignerSet,由 `DSHS_OVERLAY_ROOT_PUBKEYS` 验)。
- 签名者公钥 hex = `bad464dfd53048efe7b8531029b3030eda49bc12930703d3a2a8f60e3a7daddf`(来源 `/etc/dshs/overlay-signer-key.pem.pub`,64 hex)= SignerSet 里那一把。
- relay 运行参数:`DSHS_OVERLAY_REQUIRE_IDENTITY=1`(**强制身份**,配不全就起动即抛)· `DSHS_RELAY_DIALERS=ops:manager`(**白名单只有 manager**)· 吊销清单 `/etc/dshs/revocations.json`。
- 注册表 `/var/lib/dshs/overlay/nodes.json` = `{"version":1,"nodes":{}}` **空** ⇒ invite/join 通道**从未被走过**,现网 manager 走的是老的手工 grant(`/etc/dshs/node-manager.grant.json` + dialer drop-in)。
- 网名 = `ops`(`DSHS_OVERLAY_NETWORK_ID=ops`);目录键与签名者键是**两把**,⛔ 不合并。
- 平台仓 HEAD `1242d07`,`lib/net/relay/` 与 `scripts/overlay-node-*.cjs` **干净**(未提交改动全在 `dsh-server-docs/` 与 `poc/portal-entry/`,与本次无关)⇒ 桌面线可直接用。
**为什么必须凭证(三条技术判据)**:① 接入面在公网 ⇒ 默认拒绝,否则中继可被白用/当跳板;② 节点无账号体系(一机一钥、私钥不出机)⇒ "谁被允许"只能由控制面在签发时写进可离线验证的凭据;③ 短时效 30min + nonce 原子占位(`O_CREAT|O_EXCL`)⇒ 一张邀请恰一台机器可用。
**待办**:桌面线喊跑即签 `issue-invite`(签方私钥 `0600` 在 47);`derive --apply` + relay 重启属平台侧窗口动作。
## 15:0x 追问「50w 客户端怎么发凭证 / 登录注册留着干啥」(同一条线)
**三条新实测(都影响结论)**:
- 🔴 **invite/join 通道不产 grant**:`grep grant scripts/overlay-node-admit.cjs` = **空**;registry 只产 `DSHS_RELAY_DIALERS` 白名单(hostId 粒度)。而 47 relay 是 `REQUIRE_IDENTITY=1`,服务端走 `verifyPeerGrant(grant, grantSig)` ⇒ **invite 通道的节点即使进了白名单也会被拒**,必须再补一张 `overlay-keyring.cjs issue-grant` 签的 grant。桌面线对接单缺这一步。
- 🔴 **`/dshs-overlay/join` HTTP 端点未实现**:`grep "overlay/join" src/` = 空。`GET /dshs-overlay/bootstrap` 是**地址目录**(可轮换种子),不是签发入口;overlay 管理端点全 `requireAdmin`。⇒ 现状只有"离线申请单 + 人工 CLI"。
- 🔴 **两条机制不可扩展到 50w**:① 白名单落点是**单个 systemd drop-in 的 `Environment=DSHS_RELAY_DIALERS=网/host,…`**(`deriveDropIn`)⇒ 逐设备列名字不可行;② 注册表 `nodes.json` 是**单 JSON 全量读写**(`loadRegistry/saveRegistry`)。
**结论(口径)**:invite 是"几十台服务器节点"的**运维通道**;每设备的真实凭据是 **grant**(`{network,hostId,nodeKey,issuedAt,expiresAt}`,可设过期、可按 hostId/nodeKey 吊销)⇒ 规模化形态 = **设备首次上线带用户登录态调平台签发接口 → 在线签名者当场签 grant**;50w 张凭证全靠自动签发,无人工。**登录注册正是这条链的鉴权与授权来源**(哪台设备属于谁、进哪张网 `ops`/`u:<租户>`/具名网、额度),所以要留,且是规模化前提。真正瓶颈不在签发(一次 Ed25519 签名),而在 **relay 连接承载**(50w 长连接 ⇒ 必须集群分片,量级估算待实测)。
## 15:1x 拍板落地:中继路线 C+A + 设备登录接入(序46 已立单)
**用户拍板原话**:「**c + a 后续再考虑b** 沉淀文档,改造功能支持设备登录接入网络,然后再对接文档中写明接入方式」⇒ 路线 C(直连打洞优先、中继兜底)+ A(relay 集群横扩);**B 分层中继后续再考虑**。
**三份产物(本轮已落)**:
1. 决策与改造方案 = 工作区 `docs/覆盖网络/覆盖网络_设备登录接入_决策与改造方案_20260920.md`(§2 现状取证表 + S0–S4 改造项 + 容量估算 + §6 风险)。
2. 交接单(执行载体)= `dsh-server-docs/交接单/覆盖网络-序46-设备登录接入网络.md`(8 段:目标/只读前置/范围/决策点/步骤/回滚/回报/依赖)+ 已登记 `交接单/README.md §一`。
3. 对接文档 = 桌面线 `对接单_桌面端接入覆盖网络_给平台会话_20260920.md` —— §8 五项回填 + 追加 §10(10.1 签名者来源纠正 · 10.2 **补 grant 步骤** · 10.3 设备登录接入方式 · 10.4 路线口径)。
**本轮新增硬事实(都写进方案 §2,⛔ 别凭记忆复用)**:
- 🔴 relay 准入 = **两道独立证明**:`grant+grantSig`(受信签名者签)**且** `nodeSig`(证明握有私钥);`REQUIRE_IDENTITY=1` 时缺任一即具名拒。
- 🔴 **invite 通道不产 grant**(`grep grant scripts/overlay-node-admit.cjs` 空)⇒ 桌面线第 5 棒必须补 `overlay-keyring.cjs issue-grant`,否则只进白名单也会被拒。
- 🔴 **`/dshs-overlay/join` 端点未实现**(grep 空)⇒ 只能走离线 `--out` 申请单;`GET /dshs-overlay/bootstrap` 是地址目录、不是签发入口。
- 🔴 **不可扩展的两处**:`dialers` 逐 hostId 名单(构造时定型、落 drop-in 的单个 `Environment=`)+ `nodes.json` 单 JSON 全量读写 ⇒ 设备**不进** registry,台账进 PG;准入改「网级放行 + 逐设备 grant」。⛔ **不把 dialers 改成运行期可变**(那是刻意的安全判据)。
- 短租约(默认 24h,键 `DESKTOP_GRANT_TTL_HOURS`)+ **停发即失效** ⇒ 吊销清单只装紧急封禁。
**接续**:已按「收口 + 5~8 分钟、同一时刻只挂一个」登记 序46 执行棒(一次性)。⛔ 本轮未动 47 任何配置、未签 invite、未跑改造。
---
## 序46 执行棒 · 步骤 1 收口(15:18–15:37 · 验收**通过**)
**做了**:把 `u:<tenant>` 租户网端到端跑通(离线通道)+ 原始读数落盘 `tmp/seq46/readings/01–18*.txt`。租户取 `users.id` = `u:2ade6411-927c-49cb-b811-391356263dcd`,设备 `hostId = d-<userId>-<pubkey 前 8 位>` = `d-2ade6411-…-a59404a2`(§④ 规则)。
**改了 47 的三处**(均有备份/可回滚):① relay 手写 drop-in `dialers.conf` 改成「`ops:manager` + `u:<tid>/<hostId>`」并 `restart dshs-relay`(备份 `dialers.conf.bak-seq46-20260920-152837`)② `/etc/dshs/relay-keys.json` 加一条(3→4)③ 控制面注册表 `nodes.json` 加 1 条 approved(原为空表)。⛔ 未动 relay 的 `REQUIRE_IDENTITY`/签名者/吊销,⛔ 未动官方 dsh 主程序。
**验收**:`derive` 打印含该网桶 ✅|relay 日志 `AUTH OK host=u:…/d-… session=24b0eab0741edaeb ports=[]` ✅|ops 网零影响(重启后 manager/w-106 自动重连,refused=0)✅。
**三条硬发现(⛔ 后续棒直接照用,全写在 单 §⑨)**
1. 🔴 **设备形态 = 不声明端口的拨号方**(relay 判据互斥:`dialer-must-not-declare-ports` / `no-ports`)。`main.js --client` **强制 `--ports`** ⇒ 表达不了设备形态(实测被具名拒 9 次)⇒ S1 桌面线必须用 `RelayClient({ports: [], dialer: true, identity})`(Manager 拨号通道同款装配)。
2. 🔴 **`dialers` 桶 = 拨号方名单** ⇒ S2 若用网级通配 `u:<tenant>/*`,整张租户网都被判为拨号方 ⇒ 该网内不能有声明端口的节点。
3. 🔴 **`derive --apply` 只表达一张网**,且落点 `50-overlay-dialers.conf` 与手写 `dialers.conf` 抢同一个 env 键、字典序上还被压掉 ⇒ 真跑会把 `ops:manager` 丢掉且**连租户网也不生效**(静默失效)。本轮**没用 `--apply`**,改合并写手写 drop-in。⇒ S2 的前置:先解决"多网合并派生"。
4. ⚠️ 47 上 `/opt/dshs/scripts/overlay-node-admit.cjs`、`overlay-keyring.cjs` 与仓库 HEAD **md5 不一致** ⇒ 本轮用「HEAD 脚本 + 临时根 `/tmp/seq46/cp`(lib 软链 `/opt/dshs/lib`)」跑,⛔ 未改 `/opt/dshs`;S0 上线前建议同步。
**停手位置**:步骤 2–7 未做(本轮明令"做完即停")⇒ **§① 终态未达成**(现网租户网 dialers 仍是逐设备一条,"不依赖逐设备名单"未验)。测试拨号器已停(不留常驻进程);重拨命令在 单 §⑨。
### 15:46 用户令「继续任务直到功能完成」+ 上下文预算告警(184k token)⇒ 按钩子处置:开新会话接续
**没在本会话继续改代码**(理由:本会话历史已 184k、每轮全量重发;此刻开**新会话**起点约 5 万 ⇒ 单轮成本降到约 1/12;且半成品留在工作树会给下一棒制造脏基线)。⇒ 按 `dsh-auto-handoff-chain` **§3.1.1** 登记**唯一**一个接续棒:
- **执行棒 2**(一次性,id `8be929a7-24d8-4b6f-ae97-63f13766a024`,`scheduledAt 2026-09-20T15:52` = 收口 + 6 分钟):**只做 §⑤ 步骤 2(S2 网级放行)**,可顺带跑步骤 3/4 反证;⛔ 不碰步骤 5/6/7。prompt 已自带四条既成事实 + 三条硬约束 + 单内 §⑦ 收口要求,并写明"收官再登记下一棒"(同一时刻只挂一个)。
- 登记前已 `automation_update list` 确认**无待跑棒**(全部旧棒 `scheduledAt` 均已过 ⇒ 按判据不算待跑)。
- 链的剩余:步骤 2(S2)→ 3/4(反证)→ 5(S0 端点)→ 6(S3 台账)→ 7(真机零介入)。
---
### 16:2x 序46 执行棒 2 收口 —— §⑤ **步骤 2(S2 网级放行)+ 反证** 验收**通过**
**所选实现**:§④ **首选 = dialers 网级通配 `u:<tenant>/*`**(退路"新增整机开关"未采用)。判据 = 改动面最小 + 判据显式。
**生效范围**:只对写 `/*` 的那张网;`ops` 与未列网络照旧默认拒绝;通配**只**放宽"进来后算哪种身份",**准入仍靠 `REQUIRE_IDENTITY=1` + 逐设备 grant**(身份闸门 `server.ts:1146` 在注册闸门 `:1187` **之前**);⛔ 通配只准用在租户网(`ops/*` ⇒ 装载时抛)。
**核心判据成立**:全新 hostId `d-2ade6411-…-78c24c49` + 有效 grant ⇒ `AUTH OK … ports=[]`(拨号器 `state:up`)。A/B 唯一变量 = 白名单:旧配置 ⇒ 拒 `no-ports`(证"确实不在名单里"+**新代码本身不放宽**);新配置 ⇒ 通。`/status` 租户网桶 = `["*"]`,**无任何逐设备条目**;`refused=0` `dialDenied=0`,`manager`/`w-106` 两次重启均自动重连。
**反证四条全成立**:无 grant ⇒ `identity-incomplete`;真签名绑别的 hostId ⇒ `identity-host-mismatch`;绑别的 nodeKey ⇒ `identity-key-mismatch`;改 grant 文档 ⇒ `identity-signature-mismatch`。
⚠️ **教训**:改**文档**只能拿到 `signature-mismatch`;要 `host-/key-mismatch` 必须造**真签过但与对端出示物不一致**的 grant(顺序:签名→网络→host→key)。
**改动 4 文件**:`network.ts`(`DIALER_WILDCARD` + `splitEntry` 抽成唯一一份 + `parseDialerEntry` + `isAllowedDialer` 通配分支)|`server.ts`(两处闸门由内联 `dialers.get(...)?.has(...)` 改为**调用唯一出口**)|`index.ts`(导出)|两个 test(通配用例 + **T4 守卫升级**:等价性从"逐字相同"变"同一份代码",并**反证** server.ts 里不再有内联形态)。交付门禁:`npm test` **226/225 通过 0 失败**;`check:layering` **无新增违规**;单已同步 `/opt/dsh/docs` 镜像(md5 一致);锁已释放。
**本轮两条硬发现(后续棒必须处理)**
1. 🔴 **relay 的 HMAC 密钥表在进程启动时装载一次** ⇒ 新增设备必须重启 relay 才能拨(实测 `unknown-host`)。**这是 §① 终态"人工零介入"的真障碍**(与网级放行无关,在准入链更前面)。已排除"放开 REQUIRE_IDENTITY / 租户共享 secret"(都放宽权限);最优解 = **relay 热加载 `relay-keys.json`** ⇒ 归入步骤 5/6。
2. 🔴 **`derive` 口径仍未定**(步骤1 发现3 延续):`deriveDialers` **本就支持多网并集**(单网限制只在 `deriveDropIn`);且通配一旦启用,"派生该产出 `/*` 还是逐设备枚举"会牵动 `auditDerivation` 三判据(现有实现会把两者判为不等 ⇒ 自审红)。⇒ 动 derive 前先定这两条。
**停手位置**:⛔ 步骤 5(S0 端点)/ 6(S3 台账)/ 7(真机端到端)未做(本轮明令不碰)。**下一棒 = 步骤 5 + 遗留2(密钥表热加载)**,已登记一次性自动化 `99936fdd-f597-4669-a823-a052e07abcaf`(`scheduledAt 2026-09-20T16:28` = 收口 + 7 分钟);登记前已 `list` 确认无待跑棒。原始读数(20 文件)落盘 `tmp/seq46/readings-s2/`。
---
## 收口 · 16:28–17:1x 序46 执行棒 3 —— §⑤ **步骤 5(S0 用户态签发端点)+ 遗留2(密钥表热加载)** 验收**通过**
**基线** HEAD `1242d07`(与步骤 1/2 同);**改动 7 文件**(代码 5:`src/web/routes/overlay-device.ts`🆕|`src/net/relay/server.ts`|`src/net/relay/main.ts`|`src/web/server.ts`|`test/overlay-device.test.mjs`🆕;配置 3:`config/*`)。§④ 补齐项实测:**签发导出名 = `signNodeGrant(grant, signerPrivateKeyPem)`**(`identity.ts:396`);`u:<tenant>` 网已被 relay 服务。
**真跑读数(全部成立)**
- **S0**:不带会话 `401`|tenant 会话 `200`(`network=u:2ade6411-…`|`hostId=d-…-faf3784f`|`grant={doc,sig}`|`expiresAt=+24h`|密钥表 `7→8`)|admin 会话 `200` 但落在**自己的网** `u:cce6d1cd-…`(配额各自算)|连发到上限第 9 发 **`429 device-quota-exceeded`**(`used=10 max=10`)。
- **遗留2 热加载**:启动日志 `密钥表:5 条(**热加载已启用**)`;relay `ActiveEnterTimestamp=16:57:30` **未变**,17:05:20 `🔄 密钥表热加载:7 → 8 条(新增 1:…-faf3784f)` → `AUTH OK … ports=[]`,拨号器退出码 **0**;清理后 17:08:39 `8 → 6 条(移除 2)`;`keyReloads=3` `keyReloadFails=0` `refused=0`。
- **单测** 7/7;**全量回归 233 项 0 失败**。交付门禁:4/4 md5 一致(`/opt/dsh-relay/lib/net/relay/{server,main}.js` + `/opt/dshs/lib/web/{server.js,routes/overlay-device.js}`)|文档镜像 md5 一致 `2960626c…`|锁已释放。
**🔴 本轮事故(已完全修复,必记)**:清理探针时我按**归一化名**(`ops/w-47`)匹配 `/etc/dshs/relay-keys.json` 的**原始键**(文件里是**裸 hostId** `w-47`)⇒ **误删 ops 三把密钥**(17→3 条)。处置 = 停手 → 从自建备份整份恢复(17 条,装载后 `ops/w-47,w-106,manager` 俱在)→ **按原始键名**重删 → 补**三条硬断言**(保留名单含 ops 三把/删除名单⛔ 不得含/装载后用**产品自己的装载器**验三条仍在)。复验 `online=w-106,manager` `refused=0` `authFailed=0` ⇒ **现网无残留影响**。
⇒ **纪律**:删/改**共享表文件**,判据必须是**文件里的原始键**;改完**立刻用产品装载器自校验 + 断言关键条目仍在** —— `md5`/`count` 都不够。
**本轮其他具名发现**
1. 🔴 **新用户"零介入"的下一个真障碍**:`DSHS_RELAY_DIALERS` 是**手写 drop-in** ⇒ **新租户网无人加白名单**,新用户即使有有效 grant + 密钥仍会在**身份闸门**被拒(`dialer` 身份拿不到 ⇒ `no-ports`)。与"derive 多网并集口径"同源 ⇒ 步骤 6/7 硬前置。
2. ⚠️ **`/opt/dshs/scripts/overlay-relaykey-add.cjs` 在 47 上不存在**(`MODULE_NOT_FOUND`)⇒ 按该工具文档删密钥会失败;调用方吞 stderr 时 = **静默失败**(我踩了一次)。与"远端 CLI 与 HEAD 不一致"同族(不止 md5 不同,还有**缺失**)。
3. ⚠️ **取证脚本假信号(已修)**:复用拨号脚本 `--ticks N` 只在 `N=0` 时排周期 ⇒ `--ticks 2` 永不退出 ⇒ 外层 `timeout` 收尾给**退出码 124**,而 `STATUS {"state":"up"}` 明明成功。已改为"真排 N 次"。
4. ⚠️ **scp 路由文件后必须重启 `dshs`**:本轮首次漏重启 ⇒ 跑的是旧模块,现象 = 回执里 `grant.doc=undefined`(形状仍旧)⇒ 这类"改了没生效"在**回执形状**上可直接辨认。
5. ℹ️ 平台侧 `/opt/dshs/lib/net/relay/server.js` **落后两代且是死代码**(`src/` 无人 `import`)⇒ 本轮刻意不动它(避免夹带步骤 2 改动进无关文件)。
6. ℹ️ 47 上 `DSHS_OVERLAY_SIGNER_KEY_FILE`/`DSHS_RELAY_KEYS_FILE` **未显式配**,走代码内中性缺省,且缺省值恰好 = 生产路径(实测签名者私钥读到、密钥表写对)⇒ 无需改 drop-in。
7. ℹ️ 清理后密钥表 **6 条**(ops 三把 + tenant 三台),台账 approved = 3(全 tenant);探针会话已删。真跑成功的 `-faf3784f` **留册**供步骤 6/7 复用(密钥/grant 在 47 `/tmp/seq46/s0/`)。
**停手位置**:⛔ 步骤 6(S3 台账)/ 7(真机端到端)未做(本轮明令一棒一步)。**下一棒 = 步骤 6(S3 台账落 PG + 续签语义)**,并把**遗留 1(新租户网的白名单来源)**纳入(步骤 7 终态硬前置)。原始读数(17 文件)落盘 `tmp/seq46/readings-s0/`(47 侧 `/tmp/seq46/s0/readings/`);复用脚本 `tmp/seq46/s0-*.sh`。
---
## 收口 · 17:20–18:05 序46 执行棒 4 —— §⑤ **步骤 6(S3 设备台账落 PG + 续签语义)** + §⑪ **遗留 1(新租户网白名单来源)** 验收**通过**
**基线**:`1242d07`(09-19 16:18)|**锁**:`序46-棒4-设备台账落PG`(17:20 起持有)→ 收官已 `handoff-guard.sh --release-exec`
**做了什么(15 文件 · 代码 14 + 文档 1)**
- **S3 台账(v12 · 双方言)**:`overlay_devices` 落 47 的 PG13(PK `(network,host_id)`、FK `users(id) ON DELETE CASCADE`、CK `status∈{active,revoked}` 默认 `active`、`grant_renewals` 默认 1、索引 `idx_overlay_devices_user`)。续签语义 = `grant_renewals = overlay_devices.grant_renewals + 1`(**引旧行**,⛔ 不用 `excluded.`);**`status` 刻意不进 `DO UPDATE SET`** ⇒ 停发黏性、续签不复活。
- **端点接线**:`src/web/routes/overlay-device.ts` 新增 ① 停发闸门(`status='revoked'` ⇒ **403 `device-revoked`**)② 台账写入(失败 ⇒ 500 `device-ledger-write-failed`,且**密钥表未写**);回执加 `renewals` + `ledger{…}`。管理面新增 `GET /api/admin/overlay-devices`(只回 `nodeKeyFingerprint`)+ `POST …/revoke`(非法 400 / 不存在 404)。
- **遗留 1**:`TENANT_NETWORK_WILDCARD='u:*'` + `isAllowedDialer` 三分支取并集;**只放宽身份面、⛔ 不放宽准入面**;**只对 `networkKindOf()==='tenant'` 生效**;半通配 `u:*/<hostId>` 与非租户网通配**装载即抛**;`status()` 跳过 `u:*`(⛔ 不产幽灵网)。
**核心读数(全部真跑)**
- **到期拒**(短租约 +25 s):客户端 `HELLO rejected reason=identity-expired` + relay `AUTH DENY … why=identity-expired retryable=false`,退出码 1
- **续签通**:端点 `renewals=2`、租约后移一天;PG `grant_renewals=2 | lease=2026-09-21 17:56:10+08`;同设备再拨 `AUTH OK`/`STATUS state=up`/退出码 0
- **停发/恢复**:`revoke` 200 ⇒ 端点 **403 `device-revoked`**,`grant_renewals` 停在 2(⛔ 不复活);传 `status=active` ⇒ 200、`renewals=3`
- **遗留 1 A/B/C**(同设备同 grant,唯一变量 = 白名单):A(只列 `u:2ade6411…/*`)`DENY why=no-ports` / B(`u:*/*`)`AUTH OK session=d5f508f2483ee4bd` 退出码 0 / C(不带 grant)`DENY why=identity-incomplete`;ops `refused=0`、`online=[manager,w-106]`
- **部署对账**:10 个产物 scp 后 **md5 10/10 与 47 逐字一致**(平台侧 8 + relay 侧 2);文档两处(序46 交接单 + 117 参数表)**双端 md5 一致且 CR=0(纯 LF)**
**本轮具名发现(要带走的)**
1. 🔴 `u:*/*` 是「新用户零介入」的**唯一静态表达**(租户 id 运行时才生成);代价 = 租户网内**身份面整网放宽**(准入面一字未动)。
2. ⚠️ 47 上**没有** `/etc/dshs/overlay-signer-public.pem`;受信公钥要从 `/etc/dshs/overlay-signers.json` 的 `doc.signers[0]` 取(`bad464dfd53048ef…`)。⛔ 别按文件名推。
3. ⚠️ `schema_migrations` 只有 `(version, applied_at)` 两列,⛔ 无 `name`。
4. ⚠️ `npm run verify` 在 `scripts/verify-platform-admin-section.mjs` 崩(`require()` + top-level `await` 混用)—— **仓库既有缺陷**,本轮**未修**(不在范围);`npm test` 全绿。
5. ℹ️ 平台侧 `/opt/dshs/lib/net/relay/server.js` 落后 relay 侧(死代码),本轮**刻意不动**(同 §⑪ 发现 6)。
**清场**:探针会话 `user_agent='seq46-s3-probe'` **2 → 0 已删**;测试设备 `d-cce6d1cd-…-2836b9b8` **留册**(registry approved 4 台、密钥表 7 条);本机归档副本已**脱敏**(删 `sid.txt`/`secret-1/2.txt`/`device.key` 四个凭据文件 + 3 处 `hmacSecret` 置占位),原文只在 47 `/tmp/seq46/s3/`。
**停手位置**:⛔ 步骤 7(真机端到端 · 零介入)未做(本轮一棒一步)。**下一棒 = §⑤ 步骤 7**(automation `9f29374c-4cc0-4a04-99e9-462c2491afca`,定 **18:12**)。⚠️ 步骤 7 依赖**桌面线「第 5 棒」客户端脚本**,若未就绪须按 §⑤「失败一律具名」停在步。原始读数 **41 文件**落盘 `tmp/seq46/readings-s3/`;脚本 `tmp/seq46/s3-01-prep-ab.sh` · `s3-02-expiry-renew.sh` · `s3-dialers.sh` · `redact-s3-readings.py`。
---
## 18:12–18:2x 序46 执行棒 5 —— §⑤ 步骤 7 **受阻 · 具名停在步**(原因码 `client-artifact-missing`)
**基线**:HEAD `1242d07`|**锁**:`序46-步骤7-真机端到端-20260920-1812` → 收官已 `--release-exec`(复核空闲)
**判定**:§⑤ 步骤 7 **未执行** —— 缺的是**载体**(真机客户端脚本),不是"网络不通"。⛔ 本轮**未**拿测试拨号脚本(`tmp/seq46-s2-dialer.cjs`)冒充「真机客户端」(上一棒明令禁止的形态)。
**四条具名依据(桌面侧产物不存在)**
1. 桌面线 automation 台账 **10 条,无「第 5 棒」**;第 4 棒(`0142e285-…`)memory 原文:「⛔ **未登记下一棒** —— 第 5 棒(接入覆盖网络)需 `--invite` + `--signer-pub`,「需用户提供凭据」⇒ 边界外 ⇒ **链条暂停等拍板**」。
2. 对接单 §4 指定落点 `E:/dsh-worker-dev/overlay/` **不存在**。
3. 桌面线工作区 **13 个可执行脚本全是** S1/S1′ 起实例/起 worker 探针(`_devkit/s1-*.ps1` · `s1-*.mjs` · `s1p-verify-instance.sh`),**零**覆盖网络拨入代码。
4. 全工作区命中 `RelayClient`/`dialer`/`nodeSig`/`dshs-relay` 的 = **3 份,全是文档**(对账/实现方案/对接单),**无代码**。
**反向结论(重要)**:桌面线第 5 棒"自停"的两条前置 —— ① 需控制面签 `--invite`+`--signer-pub` ② 只进白名单会被拒(还缺 grant)—— **已被序46 步骤 5/6 全部解除**(S0 登录态换 grant + `u:*/*` 全域 + 密钥表热加载)⇒ 桌面线**现在具备自恢复条件**。其恢复属桌面线 lane:本线⛔ 不代其登记棒次、⛔ 不写其工作区(对接单头部明文「平台会话只读本单」;跨线通知需用户指令)。
**平台半边就绪核对(只读)**:relay `active`/`running`(`ActiveEnterTimestamp=17:52:31`)· `DSHS_RELAY_DIALERS=ops:manager,u:*/*` · `DSHS_OVERLAY_REQUIRE_IDENTITY=1` · `online=[manager(ports=[]), w-106(ports=19000/21000)]` · `refused=0` · `keyReloads=1` `keyReloadFails=0` · 密钥表 **7 条** · 租户网 `sessions=[]`(无遗留拨号)· S0 端点在线(不带会话 `401`)。**`authFailed=2` 两条都具名**:17:52:39 `why=identity-incomplete` + 17:56:07 `why=identity-expired` = 步骤 6 的**故意反证** ⇒ ⛔ 无未解释增量(曾误判为异常,journal 原文已澄清)。
**本轮具名发现(写进单 §⑬ ④,供桌面线第 5 棒直接用)**
1. 🔴 **「登录态」= `sid` cookie,无 token / 设备码通道**:`src/web/middleware/authn.ts:16-17` 只解析 `sid` cookie;`POST /api/auth/login`(`src/web/routes/auth.ts:282`)用 `username+password` 登录并 `set-cookie` ⇒ 桌面客户端必须**自维持 cookie jar**。
2. 🔴 **`/api/auth/login` 是「单活跃会话(last-wins)」**(`auth.ts:294-296`:注释 + `deleteUserSessions(user.id)`)⇒ 设备登录取会话**会顶掉用户浏览器会话**;反之用户再登录 ⇒ 设备会话失效 ⇒ **无法续签**、租约到期掉线。**触发条件**:真机**常驻**形态落地前必须定「设备会话 / 浏览器会话」并存口径。
3. ⚠️ §⑧ 风险 3 的 **`nodeSig` 时钟窗口本轮未实测** ⇒ 已登记为下一棒事项。
**交付 / 收口**:单新增 **§⑬**(受阻具名 + 平台就绪 + 发现 + 遗留 + 接续)|原始读数 3 文件落 `tmp/seq46/readings-s7/`(`00-platform-readiness.txt` · `01-authfail-forensics.txt` · `02-desktop-side-evidence.txt`);脚本 `tmp/seq46/s7-0{0,1,2}-*.sh`|单双端 md5 **`8cf8bfedcdb47ccc9ab6f83fd82c4ff6`** 一致(镜像 `600 root:root`、CR=0)|全库对账 **一致 243 / 不一致 1 / 仅本地 7 / 仅服务器 5** ⇒ 与上棒同、**无新增差异**|`交接单/README.md` §一 序46 行本机已回填(⚠️ 未推镜像,同 §⑫ 先例避免夹带)|**零改动面**(未改代码/配置/凭据/服务)⇒ 无需回滚。
**下一棒**:`5abcac0b-c91a-415c-ae1f-246ac0819807`(一次性 · **18:28** = 收口 + ~7 分钟)= **步骤 7 前置口径 + 存在性门禁**:门禁先查桌面线客户端脚本(到 ⇒ 直接跑步骤 7)⇒ 未到则 ① 实测 `nodeSig` 的 `ts|nonce` 容忍窗口 ② 出「设备会话 vs 浏览器会话」并存口径草案(⛔ 不动代码、⛔ 不放权限)。
**⚠️ 待用户口径(陈述句,非征询)**:桌面线第 5 棒是其链条自停的棒次,原阻塞已由序46解除 ⇒ 恢复它只需在桌面线工作区开一棒(属桌面线 lane);本线不代其登记。另:若要本线把「凭据阻塞已解除 + 客户端所需全部口径」正式通知桌面线,需用户指令 —— 其对接单头部写明「平台会话只读本单」,上次追加(§10)是用户指令授权的。
---
## 收口 · 18:28–18:45 序46 执行棒 6 —— §⑤ **步骤 7 前置**(门禁复现 + 时钟窗口实测 + 会话并存草案)|验收:前置**已补齐**,步骤 7 **仍未过**(无载体)
**① 门禁(§⑬ 遗留 1 复现)= `client-artifact-missing` 未变**:落点 `E:/dsh-worker-dev/overlay/` 不存在;桌面线工作区顶层**只剩** `.workbuddy/`+`CODEBUDDY.md`+`docs/`(§⑬ 清点出的 13 个脚本**已不在**);关键词 `.cjs/.js/.ts/.mjs/.sh/.py` 全量命中 **0**。⇒ 仍**不用**自造拨号脚本冒充真机客户端。
**② 时钟窗口 —— 真跑实测(本棒核心)**:探针 `tmp/seq46/s7-10-window-probe.cjs`,**只读零凭据** —— 靠「`clock-skew` 判在 `bad-mac` **之前**」这一**校验顺序**,用 dummy MAC 量边界(36 次真实 WS 握手)。
- 窗口 `windowMs=60000`(取自 `HELLO_ERR`,⛔ 非读常量)
- 正向(客户端快):内 `+60009ms` | 超 `+60010ms`;负向(客户端慢):内 `-59991ms` | 超 `-59992ms`
- 两侧不对称 ≈ ±10ms **可解释**:服务端取**收帧时刻** ⇒ 反推 47 回环单向时延 δ ≈ 8–10ms
- 超窗原因码 = **`clock-skew`**(`retryable=true`)|窗口内 = `bad-mac`(`retryable=false`)
- 自愈实测:`ts=+7200000ms` → `clock-skew`(帧内带 `serverTime`)⇒ 按 `client.ts:1248` 公式校正后第 2 次**已越过 `clock-skew`**(终点 `bad-mac`,因 dummy MAC)⇒ **漂移只决定"多不多一次握手",不决定"连不连得上"**
**③ 🔴 口径更正(重要,供后续直接引用)**:§⑧ 风险 3 说"时钟漂移会导致 `identity-bad-proof`" —— **与实现不符**。判据顺序(`src/net/relay/server.ts:1249-1257`)= `unknown-host → network-mismatch → bad-nonce → bad-mac-length → **clock-skew** → nonce-replay → bad-mac → 身份两关`;`clock-skew` 早于 MAC、更早于身份 ⇒ 超窗**必然**拿 `clock-skew`。**真机上见 `identity-bad-proof` 一律不是对时问题**,排查方向 = **nodeSig 挑战串 / 密钥 / grant 三者不一致**。(§①–⑧ 一字未改,只在单 §⑭ §⑤ 留记录。)
**④ 会话并存草案(§⑬ 遗留 3)—— 已出 · 落待拍板**:真读代码得事实表(`sessions` 无 `kind` 列 · `auth.ts:294-296` 无条件 `deleteUserSessions` · **无续签端点** · 会话 TTL 7 天 vs grant 24h · `role` 无细粒度 · 续签要 sid)。**量化更正**:设备被迫重 login 的频率 = **7 天一次**,⛔ 不是 24h 一次。候选 **A**(独立账号·零改码)/ **B**(`sessions.kind` 分桶·我倾向)/ **C**(独立续签通道)/ **D**(维持现状=降级)—— B 落"放宽权限"、C 落"新增长期凭据面" ⇒ **列待拍板、停手**,⛔ 未改一行代码、⛔ 未放开任何权限。
**⑤ 具名发现(增量零未解释)**:`authFailed` 2→**40**(+38 = 探针 36 + 补测 2),journal `AUTH DENY` 38 条 = **20 `clock-skew` + 18 `bad-mac`** 逐条对上;`authed`(5)/`identityOk`(5) **零增量**;`used=2/7515`、`ops` 在线面、`dialers` 前后一致 ⇒ **对 ops 零影响**。⚠️ **热加载是「懒」的**:`keyReloads` 1→2 唯一来源 = 密钥表 mtime `17:56:13` 后**一直没有新 `HELLO`**,直到我探针首连才触发(`7 → 7 条 新增0移除0`,内容零变化)⇒ 设备首次拨入本身就是那个 `HELLO`,**不会因表没跟上被拒**。
**⑥ 落盘 / 对账**:读数 6 文件 ⇒ `tmp/seq46/readings-s7b/`(含 `05-index.md`);脚本 `s7-10/11/12`;47 侧探针 `/tmp/`(md5 与本机一致 `5b9a3f0d0df61fe3eac93bea94b23fd4`,⛔ 无持久写)。单 §⑭ 已回填;scp 镜像后 **md5 一致 `c5d1f5d9963760a95f0d93238916ef6c`**、**CR=0 / 683 LF / 73331 B**、`600 root:root`。**零回滚**(未改码 / 未改配置 / 未签凭据 / 未重启单元)。全局执行锁**已释放**。
**⑦ 未登记下一棒(决定 + 理由)**:下一棒实质内容被三件**全部边界外**的事卡住 —— ① 载体属**桌面线 lane**(且其工作区脚本已消失)② 会话路线属**待拍板** ③ 跨线通知**需用户指令**。⇒ 按用户铁律「**要用户拍板的,等拍了再新建接续会话**」**不登记**(登记 = 8 分钟后空转一小时复述同一结论)。**拍板或载体一到,立即登记。**
---
## 拍板落档 · 20:03——20:1x · 会话并存方向**已定**(用户口径)
**用户原话**:「应该使用方案A 一个账号可以支持多种设备登录包括服务器实例」。
🔴 **语义对齐(必须记牢,否则会做反)**:草案 §③ **候选 A 的字面** = 「每台设备绑一个**独立平台账号**」= **多账号**;**用户口径** = 「**一个账号**支持**多种设备**登录」= **多设备·单账号** ⇒ 两者在"账号数"这一维上**方向相反**。用户口径与候选 **B 的内核**(解除 `last-wins`、允许多会话并存)**一致**。⇒ **一律以用户原话那句为执行判据**,字母只作称谓。
**需求判据**:① 同账号可**同时**持 ≥2 会话、**互不顶替** ② 覆盖**浏览器 / 桌面设备 / 服务器实例**三类来源 ③ **保留主动"登出全部"**(把原"登录即隐式驱逐"改成显式操作)④ ⛔ 不引入子账号。
**已定路径(我定·可推翻)**:真多会话 + 每用户会话上限 + 显式「登出全部」入口;`sessions` 加**来源维度**列。⚠️ 涉 `sessions` 表迁移(schema **v12**)⇒ 必须带回滚。⚠️ **待取证(下一棒第一步)**:「**服务器实例**」到底怎么以用户身份登录 —— 复用 `/api/auth/login` 拿 cookie,还是需独立设备码/令牌?(§⑬ 发现1 已证本版**只有 cookie 会话**)。⛔ 本轮**未改代码 / 未动 schema / 未放权限**。
**落档**:单 §⑮(拍板记录 + 需求判据 + 已定路径 + 待取证 + 接续)。scp 镜像后 **md5 一致 `a9bf350c90e8e9722c1a772f3643aa82`**、**CR=0 / 726 LF / 76088 B**、`600 root:root`。下一棒 = **规划棒**(出改造单,含取证 + 档案占号 + 步骤/验收/回滚)。
**🔴 本机环境新事实(会误导判断,务必记住)**:本轮**bash 的 shim 环境坏了** —— `shell-runtime-bash-env.sh: line 3: dirname: command not found` / `cd: null directory` ⇒ 凡走 shell 的脚本(含 `handoff-guard.sh`)**会假报"已有执行会话在跑"**(实为脚本第 33 行 `dirname` 找不到 ⇒ 抢锁逻辑崩)。
**⚠️ 差点因此误判"有会话占用、停手"** —— 实测 `.exec-lock` **根本不存在**(锁是空的)。
**✅ 绕过写法 = 在命令最前面显式给 PATH**:
`PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin:/c/Windows/System32"; export PATH; …`
⇒ 加完 `dirname` 有解析、抢锁即成功。⚠️ 另:`head` 等同理不可用;**PowerShell 工具本轮的 stdout 也采不到**(且重定向出 UTF-16)⇒ **大输出一律先落盘再用 python 读**。(判据:**报"抢锁失败"时先查 `.exec-lock` 是否真存在**,⛔ 别直接信脚本的退出话术。)
---
## 20:19–20:3x · 序47 **规划棒**(单账号多设备会话并存 · 出改造单)
**本棒 = 用户 19:5x 拍板(序46 §⑮)后的第一棒,只出单、⛔ 零代码零生产改动。**
会话 `plan-session-order47-multidevice-0920`;抢锁 → 取证 → 出单 → 对账 → 释放锁 → 登记下一棒,全程一次走完。
**🔴 第一步取证结论(本棒核心,⛔ 决定了整单的实现边界)**:
**本版平台不存在任何「实例 → 平台」身份通道** ⇒ 「服务器实例」**不能**复用 `POST /api/auth/login`,必须走**平台侧代签**。三条证据:
① `src/web/middleware/authn.ts:16-28` —— `requireAuth` **只**从 `sid` cookie 取会话,⛔ 无任何 header/token/设备码分支;
② `src/web/routes/auth.ts:281-300` —— login 只收 `username+password` 并 `set-cookie sid` ⇒ 实例要登录**必须持有用户口令**,而平台**不存明文口令、也从不把口令投给实例**;
③ `src/supervisor/orchestrator.ts:821-827` + `:776` + `remote-spawner.ts:263` —— 实例 spawn 只投 `DSHS_ROLE`/`DSHS_HANDOFF_PATH`/`DSHS_PORT`(+`DEEPSEEK_API_KEY`),远程 payload = `{userId, folder, patch, apiKey, uid, epoch}`
⇒ **平台侧已知实例归属的 userId,却从不给实例任何平台身份凭据**。
**产出**:`交接单/覆盖网络-序47-单账号多设备会话并存.md`(8 段模板 + §⑪ 规划棒记录)
+ `交接单/README.md §一` 新增一行(只加自己那行)。编号 = **47**(`交接单/` 既有最大序 46 ⇒ +1;`mkdir .lock-47` 原子占号**首次成功**)。
**⚠️ 编号撞名(已在单头部写消歧)**:主覆盖网络线的**棒**编号里「序㊼ 部署棒」已存在(记在 `交接单/覆盖网络-序45-….md §14`)⇒ 那是**棒编号**、不是本目录序号。⛔ 引用本单**只用文件名**。
**🔴 单的核心设计(可推翻项已写明优缺点)**:
- v13 迁移**一次两列**:`sessions.kind`(`browser|desktop`)+ `overlay_devices.kind`(`desktop|instance`);
⛔ **不给 `sessions` 加 `instance` 值** —— 取证的直接后果:实例拿的是**设备凭据**、不是平台会话(⛔ 不留废值);⛔ 不加 DB CHECK(SQLite ALTER 不支持,双后端 DDL 必须同形)。
- 实例凭据 = **平台侧代签**(复用 `signNodeGrant` + relay 密钥表 + `overlay_devices` 台账),私钥**由平台生成、只写实例 home(走 `UserFs`)、平台不留副本**;⛔ 不新增公开 token/设备码通道。
- 续签 = **平台侧定时重签**,读台账 `node_key`(⛔ 不读实例文件);⛔ 不用长租约(与"停发即拒"冲突)。
- 保留 E2「登出全部」+ E4「按来源单条吊销」+ E3「每用户上限」(`DSHS_MAX_SESSIONS_PER_USER` 中性默认 20)作为**放宽权限的补偿控制**(写明"本单确实放宽了会话语义")。
**🔴 本轮新增具名发现(会误导判断)**:`交接单/README.md` **本身是 CRLF** —— `HEAD` blob 即含 **156 个 CR**(工作副本 166),而同目录 `序45`/`序46`/`序47` 单均为 **CR=0** ⇒ **同目录两种行尾**。
⚠️ 本棒用 `Edit` 精确插入(工具自动沿用该文件既有 CRLF ⇒ 文件内部未混合),⛔ **未做全文件 LF 转换**(属批量换行转换,须先出受影响清单并取得确认)。
**收官四件套读数**:① 读数落盘 `tmp/seq47/readings-plan/`(`00-code-facts.txt` / `01-docs-audit.txt` / `02-numbering.txt` / `03-final-sync.txt`)
② 回填 = 单 §⑪ + README 行 ③ 镜像对账 = 两文件 md5 **与本机逐字节一致**(单 `CR=0` / `600 root:root`;README `644 root:root`);`docs-audit.py` 退出码 **0**
④ `--release-exec` 已释放(已核 `.exec-lock` 不存在)。⛔ 未 commit / 未 push(按提交边界)。
**遗留(带触发条件)**:① `交接单/README.md` 行尾统一 ⇒ 等用户点头(须先出清单);② 技能 `dsh-change-workflow` R4 的"last-wins 会踢掉用户会话"理由 ⇒ **序47 落地后**必须同步改(`SKILL.md:146` 与 `:395`;落地前仍正确,⛔ 本棒未改)。
**下一棒已登记** = **序47 执行棒 1**,automation **`5e881487-4a68-4d87-b9fd-06334c9f1e5c`**(一次性 · 2026-09-20 **20:36** = 收口 +~6 min,落在用户明令的 5~8 分钟窗口内)。
⚠️ 登记前已 `automation_update list`:列表里 status 全为 `ACTIVE`,但**全部 `scheduledAt` 均已过** ⇒ 无待跑棒(判据 = 看 `scheduledAt` + 编号,⛔ 不看 status)。
## 20:20–20:50 · MCN 数据同步落位 + 站点图标
- **MCN 库迁到权威落点**:源 `C:/Users/Administrator/.dsh/mcn-plugin.db`(444 行/10 表)→ 47 的 admin(`cce6d1cd…`, uid 114801)`<userRoot>/home/.dsh/mcn-plugin.db`(2,383,872 B)。**保留远端权威 schema**(按列名合并;两侧列序不同 ⇒ 丢列 0)。备份 `/opt/dsh/backups/mcn-plugin.db.admin-20260920-2029`。原子改名(`.new` → `mv -f`)+属主 114801 +重启 scope。
- **暴露真因**:插件 `lib/host/mcn/config.js:6` 用 `join(homedir(), ".dsh", "mcn-plugin.db")`,而实例 `HOME=<userRoot>/ws` ⇒ 指向不存在的 `ws/.dsh/` ⇒ `unable to open database file`(**先于本次改动即存在**)。档案 144 §6.3 明定落点 = `<userRoot>/home/.dsh/<plugin>.db`(由 `DSH_HOME` 决定)。
- **已出补丁(未部署)**:`dsh-plugin-mcn/lib/config.js` 新增 `DSH_DIR = process.env.DSH_HOME ? join(DSH_HOME,".dsh") : join(homedir(),".dsh")`,`DB_PATH` / `RANK_META_FILE` 改用它并导出。⚠️ **权威源是 `dsh-plugin-mcn/lib/`** —— suite 的 `lib/host/mcn/` 由 `scripts/build-mcn-suite.cjs` 的 `HOST_SYNC` **覆盖同步**(先改副本会被冲掉)。
- **产物**:`plugin_package/dist/dsh-plugin-mcn-suite-0.3.12.tgz`(436 条目 / 1.90 MB / md5 `f1d5da720bd9fedf1da64fe54d67d189`);逐成员比对 0.3.11 ⇒ **仅 3 处差异** = `config.js`、`package.json`(版本)、`skills/.manifest.json`(仅 `generatedAt`)。
- **站点图标**:剪贴板图 748×748(AI 六边形网络)⇒ `tmp/mcn-sync/icon-out/` 出 `favicon.{png,ico}` + 180/192。4 个 `web/*.html` 的 `<link rel="icon">` **待改**。
- **⛔ 未执行(R9 锁被占)**:全局执行锁被 **序47执行棒(automation `5e881487`)** 于 20:36 起占用,三次 `--claim-exec` 均失败 ⇒ 平台仓 `web/*.html` 改动、图标 scp 到 `/opt/dshs/web/`、插件 0.3.12 投放 **全部挂起**(远端步骤已固化在 `tmp/mcn-sync/deploy-remaining.sh`)。
- **顺带发现(按"先报告后动手"未改)**:`dsh-plugin-mcn/lib/index.js` 有 13 处 `join(homedir(), ".dsh", "sessions")`(应为 `$DSH_HOME/sessions` = `<userRoot>/home/sessions`)—— 同一根因的另一半。
## 20:36–21:12 · 序47 **执行棒 1**(单账号多设备会话并存 · 步 1–5 落地 + 真跑验收)
- **本棒 = 自动化 `5e881487`**,按单 `交接单/覆盖网络-序47-单账号多设备会话并存.md` 从 §⑤ 步骤 1 起,**步 1–5 全部落地并真跑验收**;**步 6–9 未做**(实例凭据代签与投递 / 续签 / 门户 UI / 端到端)。
- **代码改动**(`D:\github\dsh_shenxian`,基线 HEAD `1242d07`,⛔ 未提交):`schema.ts` v13 迁移(`sessions.kind` + `overlay_devices.kind` + `idx_sessions_user_created`)|`types.ts`/`repo.ts`/`pg.ts`/`adapter.ts`/`sqlite.ts` 加 `kind` + 新增 `listUserSessions`(升序是判据的一部分)|`config.ts` 加 `maxSessionsPerUser`(默认 20)|`web/auth.ts` 加 `sessionPublicId()` = `sha256(token_hash)` 前 16 hex|`web/routes/auth.ts` 删 `deleteUserSessions` 改「并存 + 上限具名淘汰 + 审计 `session_evicted`」|**新增** `web/routes/sessions.ts`(4 端点)+ `net/relay/device-grant.ts`(共用签发,五道门)|`overlay-device.ts` 降为薄路由(具名导出原样再导出,⛔ 不留两处判据)。
- **关键实测**:PG `max(version)=13`;迁移前旧 sid 迁移后 `/api/auth/me` 仍 **200**;E1 两客户端各 200(旧行为必 401);`client=instance`/非法 ⇒ **400 bad-client**;26 次登录 ⇒ 会话恒 20 + `session_evicted` 6 行;`revoke` 命中条 401 / 另一条 200 / 不存在 404;`logout-all` ⇒ `{ok:true,revoked:19}` 全 401;会话列表 token 字样 0 行;`test/overlay-device.test.mjs` 33 pass;`npm test` **241 项 / 240 过 / 0 败 / 1 skip**。
- **部署**:`tsc` → scp **38 个 lib 文件**(⛔ 不整目录覆盖)→ 备份 `/opt/dshs/lib-bak-seq47-20260920-205248` → `restart dshs` = active;本机/远端 38 文件 md5 全一致。
- **收口**:读数落 `tmp/seq47/readings-exec/`(18 个文件);回填单 §⑫ + 交接单 README + INDEX + **档案 145**(原子占号 `.lock-145`);文档 6 文件同步 `/opt/dsh/docs`(md5 逐条一致 · 600 root:root);测试账号/审计已清;**全局执行锁已释放**(21:1x 复核 ✓ 无锁)。技能 `dsh-change-workflow` R4 已改写(last-wins 理由失效 + 专用测试账号省事法 + `sid` cookie 坑),三处 md5 一致 `6a88c7daa0b1ce482d8e206e2cd02c25`。
- **下一棒已登记** = **序47 执行棒 2**,automation **`90049d4c-8080-4a73-a1d8-908e06f91ecc`**(一次性 · 21:18)。
- **🔴 本轮新固化坑(会重犯)**:① `sid` cookie 带 `Secure` + `Domain=.ai1net.com` ⇒ **curl 不会为 127.0.0.1 存这条 cookie**(jar 不生成)⇒ 验证必须从 `set-cookie` 响应头取值再 `-b "sid=$VAL"`(曾致首版全部 401 的"功能没生效"假象)。② 按 `kind` 找会话会命中残留行 ⇒ 用 `-A` 打唯一 userAgent 反查 `sid_public`。③ scp **中文目的路径不落地** ⇒ 先 ASCII 暂存名,远端再 `install`。④ 本机 `md5sum` 出 `hash *path`、远端 `hash path` ⇒ 直接 diff = 假"全不同"。
- **顺带观测(与本单无关)**:`systemctl restart dshs` 停机走 stop-sigterm 超 90s 被 SIGKILL(既有停机路径残留 relay 流)。
---
## 21:14–21:3x · MCN 线收尾棒 —— 站点图标上线 + mcn-suite 0.3.12 投放(本轮)
- **锁**:21:14 抢到全局执行锁(`--claim-exec "MCN线收尾棒"`)。⚠️ **简报里的 PATH 法在本机有害**:前置 `/c/Windows/System32` 会让 `bash` 命中 WSL 启动器 ⇒ 输出只剩乱码(`用于 Linux 的 Windows 子系统没有已安装的分发`)、退出码 1,**看起来像"抢锁失败 / 脚本静默错判"**(旧提示里的 PortableGit 段路径本机已不存在)⇒ **直接跑 `bash …` 即可**。已改写 `MEMORY.md §二`。
- **A 站点图标(已上线)**:`web/{admin,login,portal,register}.html` 的 `<link rel="icon" href="/favicon.svg" />` → 三行(`favicon.ico sizes=any` + `favicon.png` + `apple-touch-icon=favicon-180.png`);⛔ `portal.html:130` 顶栏 `<img class="logo" src="/favicon.svg">` **未动**。scp 4 图标(ico/png/180/192)+ 4 个 html 到 `/opt/dshs/web/`(`chmod 644`)。**行尾按服务器既有内容逐文件对齐**(实测服务器侧 admin/portal 是 CRLF、login/register 是 LF ⇒ admin.html 上传前转 CRLF,其余原样)。
- **A 线上复验**:`/favicon.ico`、`/favicon.png`、`/favicon-180.png`、`/favicon-192.png` 全 **200**(Content-Type 分别 `image/vnd.microsoft.icon`、`image/png` ×3);四页线上 HTML 均带三条新 tag,portal 顶栏仍指 `/favicon.svg` ✅。
- **B 备份**:`/opt/dsh/backups/dsh-plugin-mcn-suite-0.3.11-20260920-211901.tgz`(md5 `0b84b3bc485426c40307fd076b922a44`,与源逐字节一致)。
- **B 投放**:池内 `dsh-plugin-mcn-suite.tgz` → 0.3.12(md5 `f1d5da720bd9fedf1da64fe54d67d189` · 436 条目 · 1,989,494 B · `chmod 644`)。`scripts/install-plugin-for-user.cjs` → `/opt/dsh/scripts/`(两端 md5 `1200eb75d0fd6537ac90f3c687dbbdc2` 一致);执行 `--home …/cce6d1cd…/home --uid 114801 --profile web --expect 0.3.12` ⇒ **⑤ 实装版本 = 0.3.12 / OK**(脚本内部 `setpriv` 降权,⛔ 全程未用 root 装)。
- ⚠️ **脚本 ④ 报「已停 0 个实例 scope」**,但实例在 **21:20:53** 被 Manager 的 `crash-restart`(`restartsInWindow:1`)以新 scope `dsh-114801-6d982d03.scope` 拉起 ⇒ 新包被加载。**结论有效,但"停实例"本轮不是脚本完成的**(待观察:④ 的 scope 匹配在 47 上是否可靠 —— 若不可靠,未来运行时新 bundle 不会生效)。
- **根因实证**:0.3.11 `lib/host/mcn/config.js` = `const DB_PATH = join(homedir(), ".dsh", "mcn-plugin.db")`;实例 `HOME=<userRoot>/ws` ⇒ 落 `ws/.dsh/`(**实测该目录不存在**)⇒ `unable to open database file`。0.3.12 = `const DSH_DIR = process.env.DSH_HOME ? join(DSH_HOME, ".dsh") : join(homedir(), ".dsh")` ⇒ `home/.dsh/mcn-plugin.db` ✅。
- **B ④ 校验(全部通过)**:实装 `package.json` = **0.3.12**;实装 `config.js:10` 含 `DSH_DIR` 修正;以 uid 114801 读 `home/.dsh/mcn-plugin.db` = **10 表 / 444 行**(`account_videos` 362 · `account_video_source` 39 · `rewrite_log` 16 · `account_video_analysis` 12 · `hot_accounts` 5 · `account_persona` 4 · `storyboard_log` 3 · `script_review` 2 · `account_analysis` 1 · `creative_log` 0);`bundles` 含 `dsh-plugin-mcn-suite`;日志侧 21:19 起 `unable to open database file` 计数 **0**,改为 `[dsh-plugin-mcn] 数据就绪: cached=true count=5`(21:21:05)。
- **🔴 判版本别信日志**:`lib/index.js:92` 的 `loaded v${VERSION}` 取自常量,**0.3.12 仍打印 `loaded v0.3.10`**(本轮一度据此误判成"没装上新版")⇒ **判版本只看 `node_modules/<pkg>/package.json`**(已写入 `MEMORY.md`)。
- **回滚**:`cp /opt/dsh/backups/dsh-plugin-mcn-suite-0.3.11-20260920-211901.tgz /var/lib/dshs/business-plugins/dsh-plugin-mcn-suite.tgz` → 重跑同一条安装命令(**不带 `--expect`**)。
- **新发现**:`home/mcn-plugin.db`(77,824 B · 09-12)= 同样 10 表的历史残留,与权威库 `home/.dsh/mcn-plugin.db` 并存 ⇒ **是否清理需拍板**(破坏性)。MCN 线剩余尾巴 = 插件 `VERSION` 常量滞后(要出 0.3.13)。
- **权限/边界**:本轮只改 `web/*.html`(4 个)、池内 tgz、`/opt/dsh/{web,scripts,backups}`;⛔ 未 commit/push、⛔ 未动官方 dsh、⛔ 未碰覆盖网络线 relay/nginx/控制面、⛔ 未动云安全组。
---
## 21:26–22:1x · 序47 执行棒 2(步 6–7 落地并真跑验收 + 收口)
- **步 6(S4 实例凭据平台侧代签)✅**:新增 `src/net/relay/instance-credential.ts`(投递+续签**唯一实现**)+
`onInstanceStart` 钩子(`LocalSpawner` / `RemoteSpawner` **两路都挂**,⛔ 失败只记日志不阻断起实例);
真跑 `issued hostId=d-<uid>-c5b31dd9`,文件 `/var/lib/dshs/users/<uid>/home/.overlay-device.json`
= `0600` 属主=**实例 uid**(⛔ 非 root),relay 密钥表 **7→8**、registry **4→5**,
`verifyPeerGrant` / `verifyProof` **双 ok**。
- **步 7(S5 续签)✅**:把凭据 `expiresAt` 人为置过期 ⇒ 续签表 tick `21:54:20` `扫=1 续签=1 失败=0`;
台账 `grant_renewals` **1→2**;`nodeKey` / `hmacSecret` **复用不变**(不轮换 ⇒ 不制造 MAC 拒收窗口);
密钥表 **8→8 键零写**(`*.bak-*` 仍 29)⇒ 复验"可再次准入"通过。
- 🔴 **新坑 1**:`src/fs/user-fs.ts` 的 `HOME_FILE_NAMES` 是**控制面 + worker agent 两端**共用白名单 ⇒
**改它必须两个单元都重启**(只重启 `dshs` 时真跑报 `reason=credential-file-unreadable … bad_path`)。
- 🔴 **新坑 2**:PG `users` 表的**身份键是 `id`**(text uuid),同表另有一个 `uid`(**bigint**)——
按"优先 user_id"的自动选列会误选 `uid`(报 `operator does not exist: bigint = text`);
`audit_log` 的用户身份列名是 **`actor`**(⛔ 不是 `user_id`)。
- 质量门:`tsc` 0 错 | `npm test` **248 / 247 过 / 0 败 / 1 skip**(较棒1 基线 241/240 增 7)|
`check-layering.mjs` 无新增违规(现存 5 条在基线内)。
- 部署:6 个 lib 文件 scp(1 新增 + 5 覆盖)⇒ 备份 **`/opt/dshs/lib-bak-seq47e2-20260920-214248`**;
`dshs` MainPID → 989967、`dshs-worker` MainPID → 990718。
- 收口四件套全过:① 读数落盘 `tmp/seq47/readings-exec2/`(含 `30/31/32-*` 清理读数)② 回填
序47 单 **§⑬ 执行棒 2 记录** + `交接单/README.md §一` 本行 + 档案 **145**(新增 §3.4 / §4.3,改 §五 回滚点 · §六 未做表 · §七 遗留)
③ md5 三方一致、**CR=0 纯 LF**(README 166 为既有 CRLF)、镜像 **600/644 root:root**
④ `--release-exec` 已释放(复查无全局锁)。
- 测试账号 `seq47inst2` 用完即删:`users` / `sessions` / `audit_log` / `overlay_devices` / `dsh_instances`
五表对该 uid 复查**全 0**(⛔ 实例 home 内凭据文件按 §⑦.6 **保留**)。
- **下一棒**:序47 执行棒 3(步 8–9)automation **`4ef4fc56-1f37-4d57-8980-d00ccadffae9`** @ 2026-09-20 **22:15**。
---
## 22:07–22:2x · MCN 线 0.3.13 棒 —— 消除「日志版本号不可信」+ 核实安装脚本 ④
- **锁**:22:07 / 22:09 / 22:10 三次 `--claim-exec` 被「序47执行棒2补记」(覆盖网络线,22:07 起)占住 ⇒ **按 R9 不接管**,先做只读取证(读码 + 47 上只读实测);**22:10:56 抢到**(对方已释放)。收口后释放。
- **A 版本真源统一(三处,全部改为从 `package.json` 读)**:
① `dsh-plugin-mcn-suite/lib/index.js:26` 的 `VERSION = "0.3.10"` → IIFE 读 `../package.json`(**这是 `:92` 日志打印源,本轮主目标**);
② 同包 `lib/skills-installer.js:30` 的 `SUITE_VERSION = "0.3.0"` → 读 `PKG_ROOT/package.json`(仅当 `skills/.manifest.json` 未给 version 时的兜底);
③ `scripts/build-mcn-suite.cjs` 生成 manifest 处**原为硬编码** `suite: "…@0.3.0"` / `version: "0.3.0"` → 改用 `pkg.version`。
`package.json` 0.3.12 → **0.3.13**(description 补 0.3.13 段)。⚠️ `lib/host/*` 由 build 覆盖同步,与本次改动无关。
- **构建**:`node scripts/build-mcn-suite.cjs all`(⚠️ **默认 mode = `client` ⇒ 不打 tgz、不生成 manifest**,必须显式带 `all`)。
产出 `dist/dsh-plugin-mcn-suite-0.3.13.tgz` **436 条目 / 1.90 MB / md5 `f47a07b9a2a889770e35315e22156e90`**;
包内自检:`package.json` 0.3.13、`skills/.manifest.json` `@0.3.13`、`index.js` 与 `skills-installer.js` 两处版本段均为读 `package.json` 写法。
- **投放**:备份 `/opt/dsh/backups/dsh-plugin-mcn-suite-0.3.12-20260920-221319.tgz`(md5 `f1d5da72…` · 与池内逐字节一致)→ 池内换新(`chmod 644` · md5 `f47a07b9…` 两端一致)→ `install-plugin-for-user.cjs --home …/cce6d1cd…/home --uid 114801 --profile web --tgz … --expect 0.3.13` ⇒ **⑤ 实装 = 0.3.13 / OK**(脚本自身 md5 仍 `1200eb75…`,⛔ 未改)。
- **🎯 核心验收(目标达成)**:实例日志(22:00 起)`[mcn-suite] loaded v` **只出现 `v0.3.13`**(1 次)—— 打印版本**首次与实装一致**,误判源消除。scope `dsh-114801-75139b0a` **22:14:11** 起 active;`unable to open database file` 计数 **0**;`数据就绪: cached=true count=5`。
- **其它校验**:实装 `node_modules/dsh-plugin-mcn-suite/package.json` = **0.3.13**;以 uid 114801 读 `home/.dsh/mcn-plugin.db`(属主 114801)= **10 表 / 444 行**(逐表与上一棒一致:account_videos 362 · account_video_source 39 · rewrite_log 16 · account_video_analysis 12 · hot_accounts 5 · account_persona 4 · storyboard_log 3 · script_review 2 · account_analysis 1 · creative_log 0);`bundles` 含该包。
- **B 安装脚本 ④ 判「非缺陷」—— 已核实**:47 上以 **root** 与 **`setpriv --reuid 114801`** 两种身份跑 `systemctl list-units --type=scope --all --no-legend --plain`,**均**列出 live `dsh-114801-*.scope`(rc=0),unit 名与 `install-plugin-for-user.cjs:183` 判据(`indexOf('dsh-'+UID_+'-')===0` + `slice(-6)==='.scope'`)**吻合 ⇒ 匹配逻辑在 47 上能命中**。⚠️ 脚本 `setpriv` **只包裹 `pnpm add`**(`:141`),④ 由 **root** 执行 ⇒ 权限不是障碍。本轮 ④ 报「已停 0 个」=**时序**:④ 执行瞬间旧 scope 已停、新 scope(22:14:11)未起;**新包最终仍生效**(Manager 重启路径)。⛔ **未改脚本**(判据要求"能命中即非缺陷")。
- **回滚**:`cp /opt/dsh/backups/dsh-plugin-mcn-suite-0.3.12-20260920-221319.tgz /var/lib/dshs/business-plugins/dsh-plugin-mcn-suite.tgz` → 重跑同一条安装命令(**不带 `--expect`**)。
- **待拍板(未动手 · 破坏性)**:`home/mcn-plugin.db`(77,824 B · Sep 12 · 与权威库同 10 表)是否清理 —— 与本轮无关,仍挂。
- **收口**:MCN 线本轮**收官**(VERSION 尾巴已清)⇒ ⛔ **不排下一棒**(唯一剩余 = 上述待拍板项);为"抢不到锁"预排的 **22:18 接续棒已撤销**。
- **边界**:改动 = `dsh-plugin-mcn-suite`(3 文件)+ `scripts/build-mcn-suite.cjs` + 池内 tgz + `/opt/dsh/backups/`。⛔ 未 commit/push、⛔ 未动官方 dsh、⛔ 未碰云安全组与覆盖网络线 relay/nginx/控制面。
- **两条小教训**:① `build-mcn-suite.cjs` **默认只跑 client 段** ⇒ 要 tgz 必须带 `all`。② `mktemp -d` 给的是 Git Bash 虚拟 `/tmp`,**原生 `node.exe` 不认**(解析成 `D:\tmp\…` ⇒ MODULE_NOT_FOUND)⇒ 传给原生程序的临时路径必须用真实 Windows 路径。
- 🔴 **收尾新坑(环境,下次必再遇)**:会话中途**裸 `bash` 会被解析到 WSL 启动器**(`C:\Windows\System32\bash.exe`)⇒ 被安全策略拦、退出码 1、输出乱码(`襜輣繈顣…`)⇒ **看起来像"脚本没执行 / 锁还在"**;本机可行路径 = **绝对路径** `E:/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin/bash.exe`(本次的 `--status` 与 `--release-exec` 都是用它跑通的)。⚠️ 该 bash 的 PATH 极简(`head`/`sed`/`dirname` 均缺)⇒ **调用时⛔ 别加管道**(本次 `| head` 就把命令整条搞挂,`rm` 也走了 safe-delete shim 而失败)。
- **锁**:**22:4x 已 `--release-exec`** ⇒ 脚本回显「✓ 已释放全局执行锁」,复查通过;**22:5x 为清理项 B 二次抢锁**(`--claim-exec "MCN线待清理项B"`)。
---
## 22:5x · MCN 线待清理项 B —— 孤儿库按拍板移出(用户选 **B**)
- **拍板**:用户明确选 **B(先移出,可逆)**。
- **取证(先只读,再动手)**:`<home>/mcn-plugin.db`(77,824 B · Sep 12 23:20 · 属主 114801 · md5 `eeda97f443c3e3ab802588dadd50dea8`)—— `lsof` **无持有**;已装插件里对该文件名的**唯一**引用是 `lib/host/mcn/config.js:11` = `join(DSH_DIR, "mcn-plugin.db")`(指向 `$DSH_HOME/.dsh/`),**没有任何代码指向裸 `home/` 根** ⇒ 判为 0.3.11 之前 `homedir()` 口径遗留的**孤儿库**。
- **动作**:`mv <home>/mcn-plugin.db → /opt/dsh/backups/mcn-plugin.db.bak-20260920`(属主 114801 / mtime Sep 12 23:20 **均保留**,md5 一致);原位置**已空**;权威库 `home/.dsh/mcn-plugin.db`(2,383,872 B)**未受影响**;`home/` 根已无 `.db`。
- **回滚**:`mv /opt/dsh/backups/mcn-plugin.db.bak-20260920 <home>/mcn-plugin.db`(无读者 ⇒ 回滚不影响运行)。
- 🔴 **本机环境坑(补充上一段)**:shim 环境里**本地管道工具(`grep` / `head` / `sed`)同样不可用** —— 不只是 `bash` 解析错。本次一条 `ssh … | grep -v "post-quantum…"` 因**本地 grep 不存在**而整条命令失败(退出码 127、`ssh` 根本没执行),**看着像"取证成功但无输出"** ⇒ **本机命令⛔ 一律不加管道**;远端管道(Linux 侧 `grep`)不受影响,可正常用。
- 🗣 **拍板(22:0x)**:`交接单/README.md` 行尾 CRLF 整理 → 用户选 **B = 不做**,继续挂遗留、等该文件下次有实质改动时顺带做(⛔ 后续棒不再上抛)。已回填 序47 单 §⑬④.4 + 档案 145 §七.3;两份文档重同步,md5 本机=镜像(`cb7226dc…` / `a3b8ee91…`)、CR=0、镜像 600 `root:root`。
## 22:45–23:0x · 读「数据库专区」规范 + 自审落点合规
- **新规范已立**:文档库新增 `dsh-server-docs/数据库/`(DB-00 入口 / DB-01 接入指南 / DB-02 表结构台账与迭代 / DB-03 插件数据面规范,2026-09-20 22:46–22:49 落,起草会话 `IM插件数据面-专门文档`;`INDEX.md` 同步已改;草案双源已清)。
- **判据五条**:① 先判落点(平台/房间 ⇒ 平台库;某用户自己的 ⇒ 实例 home;本机运维 ⇒ Worker 库;大对象 ⇒ 桶)② 表名即归属(无前缀 = 内核;`p_<pluginId>_*` = 插件)③ 不直连(**房间维度**插件不持连接、不写 SQL,走 `im.data`)④ 双后端(sqlite + pg 都要建得起来)⑤ 只增不减(加表/加列带默认值/加索引;改类型删列走四步跨版本)。
- **DB-01 情形 3(实例侧)原文**:落点 = `<userRoot>/home`(由 `DSH_HOME` 决定);**平台侧若要写它必须走 `UserFs`——直接 `fs` 写 = 静默空操作**;**实例内插件可直接用实例本地文件 / 本地 SQLite**(不进平台库);坑 = ⛔ `homedir()` 拼路径必错,**正解 = `process.env.DSH_HOME ? join(DSH_HOME,'.dsh') : join(homedir(),'.dsh')`**。
- **自审今日 MCN 工作**:✅ 落点与路径公式**与规范正解逐字一致**(`<userRoot>/home/.dsh/mcn-plugin.db`,未进平台库);✅ 插件属情形 3,用本地 SQLite **不违规**(DB-03 红线针对房间维度)。❌ **方式违规**:平台侧写实例 home 我是以 root 直接 `cp`/`mv`(**未走 `UserFs`**);❌ **双写风险未收口**:本机 `C:/Users/Administrator/.dsh/mcn-plugin.db` 与服务器那份是同一逻辑数据(规范:⛔ 同一份数据不双写 = 脑裂);❌ **流程违规**:未先对该专区判据即动手。
- **DB-02 权威**:`src/db/schema.ts` **已到 v13**(v12 = 序㊻ `overlay_devices`;v13 = 序㊼ 会话/设备类型 + 上限索引);**v14(规划)= IM 房间内核 `rooms`/`room_members`/`messages`**,只需"双后端 + 只增不减 + 幂等 + 台账同一提交加行"。
- **专区自身遗留**:🔴 `DB-03` 头部三行**过时未改** —— 仍自称"草案 / 暂存工作区 / 待落点 `04-调整方案/143-…`",实际已落 `数据库/DB-03-…`;`D` 单 §九 顶部指针未核;`python scripts/docs-audit.py`(RC=0 / 无 P0)与四档纯 LF 校验**均未跑**;`数据库/` 与 `INDEX.md` **未提交**(git `?? 数据库/` + ` M INDEX.md`)。
---
## 规划棒① · carbon插件线(23:32–23:4x)—— 立项 + 出 M1a 交接单 + 建入口 + 登记执行棒①
**这是什么**:按用户要求「使用接续会话的方式,按照决策方法,把 carbon 项目改造为 dsh 插件」,把既有的可行性方案落成一条**自动接力链**。本棒 = 规划棒(只出单,不改码、不部署)。
**决策方法自决项(§4.4 逐条走完,均未上抛)**:
- **D1 第一棒不部署 Carbon,只用 stub MCP server 验证契约** —— 依据「更小改动达同一效果」+「失败代价对称」:先起 Supabase 全栈要 Docker + 数 GB,而它**并不回答本棒的问题**。失败代价从「一整套栈」降到「两个临时文件」。
- **D2 stub 用托管 Node(22.22.2)单文件零依赖**;**D3 若 dsh 只支持 stdio ⇒ 自研 stdio↔HTTP 桥**(约 50 行,不引第三方包,避免新增供应链面);**D4 测试用 mksess.cjs 一次性用户(role=active)**,R4 口径。
- 拒绝上抛:方案里那三个待拍板项(是否长期投入 / Carbon 部署归属 / 是否开放普通用户)**本棒不需要** ⇒ 条件式非阻塞,照常接力;门禁写在入口 §2.1,走到「部署 Carbon」那一步才停。
**关键设计洞察**:M1 的第一步**不是**「把 Carbon 跑起来」,而是「用 stub 回答 dsh 的 MCP 挂载契约」。MCN 插件的 lib/mcp.js 是 **stdio** 形态(npx myai-mcp),而 Carbon 的 /api/mcp 是 **HTTP streamable** —— 两者传输不同 ⇒ 若 dsh 只支持 stdio,需要一个极薄的桥,**形态 A 仍成立**。
**核实到的路径事实(省掉下一棒探索)**:平台仓根 = D:/github/dsh_shenxian;poc/ 在该仓根下(poc/business-plugins/lib/index.js **恰 21 行**、client.js **4001 行** —— 与 C 单取数一致,交叉验证通过);MCP 先例实际在 `D:/dshworkspace/plugin_package/dsh-plugin-mcn/lib/mcp.js`(另有整包版 `dsh-plugin-mcn-suite/lib/host/mcn/mcp.js`)。
**本棒产出**:
- 交接单 `dsh-server-docs/交接单/交接单_carbon插件-M1a-MCP挂载契约验证_20260920.md`(8 段;md5 `27f9f7ae40577116ea21c8e5c3213ed2`)
- 入口 `aliyun-dsh-server/接续入口_carbon插件线_20260920.md`(md5 `bb172619974bcb51b0616e7fba3554b7`)
- automation `770f10d1-b227-458f-ac4f-9d8c7e2ba743` @ 2026-09-20 23:47(执行棒①)
**环境坑(本棒新踩,别再犯)**:用 `bash -c "python -c \"...\""` 双引号包**含反引号**的文本时,bash 会把反引号里的内容当**命令替换**执行 —— 本次日志里 4 处路径/md5 被替换成空,且 `mcp.js` 被 bash 当脚本执行了一遍(报 `//: Is a directory`)。⇒ 写含反引号/`$` 的长文本**一律改用 Edit/Write 工具**,或 heredoc 用**单引号定界符**(`<<'PY'`)。
**链路状态**:规划棒① ✅ → **执行棒① ⏳ 已登记(23:47)** → 规划棒②(按 A/A′ 出 Carbon 对接单)→ …
## StoryForge 插件线 · 序1 执行棒(2026-09-20 23:45 → 09-21 00:05)
**线名**:StoryForge 插件线 | 入口 = `接续入口_StoryForge插件线_20260920.md`(§0 最新行 + §2 首行 = 唯一执行依据)| 方案 v2 = `E:\ProgramData\AI技能\dsh-plugin-forge\StoryForge改造为dsh插件_可行性与落地方案_20260920.md`
**做了什么**:把 StoryForge v3.9.1 上游改造成「不依赖 dev server 代理、可同源静态托管」的产物 + 产出 dsh 插件三件套骨架(形态 = 候选 A 同源 iframe 内嵌,不 fork 上游)。
**关键事实(省掉下一棒探索)**:
- 上游副本 `E:\github\storyforge` @ `v3.9.1` / sha `7286497e`;`9 改 1 删 2 新增`(`+103 / −245`)。
- 🔴 **单子口径被实测推翻**:本机 coreutils **不缺失** —— `dirname`/`ls`/`mkdir`/`md5sum`/`readlink`/`realpath`/`basename`/`sed`/`awk` 全在 `/bin`,`bash scripts/handoff-guard.sh` **可正常运行**(其 `【1c】` 能直接告诉你全局锁在谁手上)。序1 单子 §二 前置 5 的「全部缺失」**已就地证伪并修正**;⛔ 别再据此写新单。
- 🔴 **dev 代理实测 12 条**(deepseek / openai / kimi / claude / nvidia / doubao / agnes / longcat / opencode / siliconflow / qwen / glm),不是单子写的 15 条。
- 🔴 **PWA 不是「只删 vite 插件」就完**:SW 是应用**主动注册**的(`src/lib/pwa/register-service-worker.ts` 注册 `/storyforge/sw.js`)⇒ 必须同时改注册器;`index.html` 里「注销 SW + 清 Cache Storage」原本**只在 localhost 执行**,托管形态必须改成全主机执行(否则旧 SW 跨部署继续喂旧缓存)。⚠️ 同文件有回归测试 `tests/regression/R-CF20260702-local-pwa.test.ts` 断言 `shouldRegisterStoryForgeServiceWorker` 的返回值 ⇒ 改它要**保语义**。
- 🔴 **同源取址口径**:新增**零依赖**模块 `src/lib/ai/same-origin-llm.ts`(`/storyforge/api/llm/<provider>/<sub>` + `/storyforge/api/gist`),`src/lib/types/ai.ts` 的 `PROVIDER_PRESETS` **17 条** baseUrl 全收敛(含 ollama 的 `http://localhost:11434/v1`);`src/lib/ai/proxy-endpoints.ts` **整文件删除**;provider 收敛 = 18 条中仅 `deepseek` 启用、其余「待启用」(白名单模块 `src/lib/ai/provider-allowlist.ts`)。
- 🔴 **假阴性坑**:`grep "/storyforge/api/llm/" dist/` = **0 命中不代表常量没进产物** —— 压缩后是 `prefix + '/' + provider` 三段拼接,**带尾斜杠的整串本就不存在**;要 grep **不带尾斜杠**的前缀。
- 上游 `package.json` 一个字未动(`npm ci` 在 Node **22.22.2** 直接成功,未触发 engine 报错,降级路径没用上)。
- ⚠️ **本机行尾/编码坑(再次踩到)**:Python 里写 `b'含中文'` = **语法错**,脚本**编译期**就失败(一步都不会执行,且 stdout 只有一行 `rc=1`,极易被读成"跑过了")⇒ 二进制模式下判子串一律用 `'…'.encode('utf-8')`。
**验收六条(全绿)**:tag `v3.9.1` | `dist/` 内 `https?://api\.` + `127.0.0.1:` **0 命中** | `dist/index.html` OK | 静态冒烟 `/storyforge/`=**200** / `sw.js`=**404** | `node --check lib/client.js` rc=0 |脱管导出 **0 命中**。
⚠️ **未做浏览器渲染验证**(本机无 Chromium)⇒ 验收只到 **L2(命令)+ L3(对账)**,不得说成「页面正常渲染」。
**本棒产出**:
- 单子 `dsh-server-docs/交接单/StoryForge插件线-序1-上游改造与插件骨架.md`(已回填 §八 + 新增 §九「交给序2/序3 的输入」;30,573 B/纯 LF);同步副本 = `E:\ProgramData\AI技能\dsh-plugin-forge\交接单_StoryForge插件线-序1_上游改造与插件骨架_20260920.md`
- 插件包 `E:\ProgramData\AI技能\dsh-plugin-forge\dsh-plugin-storyforge\`(**6,510,823 B**;version `0.1.0`;`inject=[]`;`dshCompat=">=0.1.5 <0.2"`;host `apply()` 只写 `.dsh/.dsh-storyforge.log`;主 bundle md5 `9dd5ab9855f8cdd8fdabefa901c1e09d`)
- 入口 `接续入口_StoryForge插件线_20260920.md`(§2 已推进到序2)
**链路状态**:规划棒 ✅ → **序1 执行棒 ✅** → 序2 规划棒(部署与实例验收立单,automation `4e42c296-ab86-47b7-aa79-74b520460565`)→ …|🔴 **未触发拍板项**(模型 KEY 来源 / 每用户配额 / 平台是否承担调用成本 —— 三项全属序3)。
**下一步**:序2 规划棒 —— 内容 = 「实现 host 半区 `/storyforge/*` 静态 + SPA 回退 → 打包 → 候选池 → 用户实例启用 → 真机验收」,⚠️ **含 host 半区代码实现**,不是纯部署。
**🔴 锁协议缺陷(本棒实测 · 值得单独立项修)**:本棒持有的全局执行锁在 **00:0x 被外部释放**(我并未释放;随后 `rmtree` 抛 `FileNotFoundError` ⇒ 那时锁已不在)。**取证结论(事实,非推测)**:`dsh-server-docs/scripts/handoff-guard.sh:56-58` 的 `--release-exec` 分支是**裸 `rm -rf "$LOCKEXEC"`,不校验 OWNER 归属** ⇒ **任何会话只要跑一次收尾,就会无条件删掉别人的锁**。本棒期间确有并行会话(carbon 插件线执行棒①,automation 注册于 23:47),其收尾极可能命中该路径。⚠️ **影响**:R9「不得接管 / 不得删锁」在**脚本层没有任何强制**,全局互斥可被无声破坏。✅ **本棒未观察到内容冲突**(`交接单/README.md` 与序1 单的 mtime 均 = 本棒写入时刻,无第三方痕迹),但机制缺陷成立。⇒ **建议**:给 `--release-exec` 加 OWNER 校验(非本人 ⇒ 拒绝并打印占用者)。⚠️ 本棒**未改**该脚本(不在序1 范围);本棒释放锁前已自行加 OWNER 断言(非本人则拒绝释放)。