Files
dsh_ai1net_server/.workbuddy/memory/2026-09-20.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

589 lines
96 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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-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 断言(非本人则拒绝释放)。