# 2026-09-26 ## 覆盖网络线 / IM 线 · 端口拍板 + 设备当中继可行性(承接 09-25 深夜,用户两条口径 · 只读 · 零代码) > 用户口径:「**1、先不开端口,后续有需要再开**」「**2、还有可以让性能和网速高的设备当中继,比如另一个会话开发的 dsh桌面客户端版本**」 ### 拍板 ①:D8 云安全组「先不开」(已定 · 已落两处载体) - 落点:`接续入口_覆盖网络线_20260916.md §0`(新增 2026-09-26 首行)+ 状态层 `MEMORY.md`。 - 🔴 **性质变化**:D8 由「**唯一在册红 / 待用户操作**」→「**用户已知并主动搁置**」,⛔ **不再当待办**;**重开条件** = 要做**跨节点内容分发**(插件内容 / 大文件 / 镜像同步)时一并定。 - **影响面(⛔ 别误传)**:真机打洞率仍 **0** ⇒ **内容面**跨节点分发的带宽节省(潜力 **75%**(4 节点)– **93.75%**(16 节点))**暂拿不到**;⛔ **不影响群聊 / 房间 / 消息 / 历史 / 在线态 / 插件** —— 全走 **443 中继**,功能完整。 - 📌 **口径澄清(重要 · 上一轮我表述错过一次)**:`relay-direct` = **443 兜底中继入口**(⛔ 不是 P2P);**UDP `21100-21115` 只服务 P2P** ⇒ 中继走 443 ⇒ **功能不用开端口**。(原文取证:`addr-override.ts` 注释「443/TCP 兜底 · 去 CF」) ### 拍板 ②:设备当中继 —— 🔴 **路径已存在,但要分清两个角色** - **现成基础(实证)**:中继白名单 `dialers: Map>` **默认拒绝** ⇒ **加节点=配置动作**;一键加入(`overlay-node-join.cjs` / `overlay-node-admit.cjs`)+ 分组准入**已落地**;**桌面端节点接入已实测**(`dsh-ai1net-desktop/docs/S2_覆盖网络节点接入编排与守护实测报告_20260920.md` —— 守护 / 退避自愈 / 开机自启全就位);`lib/net/relay/main.js` **同一入口既起客户端(`--client`)也起服务端(`--server` = 中继)**。 - 🔴 **两个角色必须分清**:**拨出方**(`--client`)✅ **已实测**、**不需可达**;**中继 / 被拨入方**(`--server`)⚠️ **未实测**、**需要"别人能连到它"**。 - ⚠️ **设备当中继也有自己的"端口问题"**:用户侧 NAT / 路由器端口映射 / 打洞 —— ⛔ **不是**云安全组那件事,但**同源**;✅ **同局域网 / 组织内网场景几乎零门槛**(可直接用)。 - **与既有路线关系**:已拍板「引入 Centrifugo 类外部连接层」**支持多实例** ⇒ 本设想 = **把连接层节点来源从"只由平台提供"扩展为"平台 + 用户自备高性能设备"** ⇒ **与既有方向一致,⛔ 不改架构**。 ### 🔴 推荐替代路径(⛔ 不需要任何端口):设备主动拉(fan-out on read) 服务器收消息 → 只落库 + 广播**小信令** → 各设备**拨出**去拉(拨出不需可达)。 - **优势**:卸载**同一块扇出成本**,却**不需要用户侧任何网络前提**;**延迟略增**(一次往返)。 - **前提**:需把固定间隔轮询改成「**信令触发 + 增量拉**」—— IM 线第 14 棒的插件 `subscribe` 轮询**已是雏形**(⚠️ 那一版是**固定轮询**,随设备数线性,属我引入的负载)。 ### 建议推进顺序(技术项 · 可自决) ① 先做「**设备主动拉**」改造 → ② 同时在**局域网**小范围试「设备当中继」(零公网前提、验证中继服务端角色)→ ③ 跨公网中继留到**确实需要**时(与"是否重开端口"一起定)。 ### 产出与边界 - 产出 ⇒ `交付物/端口决策与设备中继可行性-20260926.md`(含拍板 / 角色区分 / 替代路径 / 顺序)。 - 承接件 ⇒ `交付物/端口与容量瓶颈评估-20260925.md`(含 **🔴 扇出背压隐患**:`ws.ts` 的 `send()` 丢弃 `socket.write()` 返回值 ⇒ 慢客户端内存无界;**单点即可打垮服务器**,建议**先修**)。 - ⛔ 零代码改动、未部署、未 commit/push、未动 47/106、未动云安全组、**未改桌面线一个字节(只读)**。⛔ **未登记下一棒**。 --- ## IM 线 · 第 15 棒(执行棒)✅ 扇出背压修复(用户拍板 A 案) > 用户口径:「**A 方案**」(= 现在修背压)。会话 `IM线-第15棒-扇出背压修复`|域锁 2 域(`src/im` · `test/im-sdk.test.mjs`),收口后已释放。 ### 修的是什么(前一轮查出的隐患) `src/im/ws.ts` 的 `send()` **丢弃** `socket.write()` 返回值、也不看水位 ⇒ **慢客户端(弱网 / 故意不读)让待写缓冲无界增长** ⇒ **单点即可把进程内存顶爆**(丢的不只是那条消息,是整个进程)。 ### 交付(1 新 3 改 · 改动形态极干净) - 改 `src/im/ws.ts`(**+84 / −1**):新增常量 `IM_SEND_BUFFER_LIMIT_BYTES`(4 MiB,= 单帧上限的 4 倍)+ 运行时选项 `sendBufferLimitBytes`(**可注入** ⇒ 可测、可按真实水位标定);`send()` 加三道:① `write()` 返回 false ⇒ `hub.noteBackpressure(writableLength)` 且**仍算已投递** ② 水位 **> 上限** ⇒ `noteBackpressure` + `noteBackpressureClose` + **具名断开**(`1013 send-buffer-overflow`)+ 落 `im_send_buffer_overflow` warn 日志 ③ 恰等于上限 ⇒ 不断开。 🔴 **关键:`ws.ts` 被删改的既有行只有 1 行 —— 正是 `socket.write(textFrame(text))`**(可机械断言)。 - 改 `src/im/hub.ts`(**+36 / −0 纯插入**):`HubStats` 加 `backpressureEvents` / `peakPendingBytes` / `closedByBackpressure`;`counters` 同步加;新增 `noteBackpressure()` / `noteBackpressureClose()`(**只记账、⛔ 不抛、⛔ 不做 I/O**)。`stats()` 用展开语法 ⇒ 自动带上。 - 新 `test/im-backpressure.test.mjs`(**8 例**)。 - 改根 `package.json`(`test`/`verify` 各登记 1 处;`numstat` 仍 **2/2** ⇒ 上一棒的 Editor 替换法生效 ✅)。 ### 判据读数 `npm run build` rc=0|`node --test test/im-backpressure.test.mjs` = **8 / 8 过 / 0 败**|`npm test` = **626 / 616 过 / 5 败 / 5 跳过**(基线 618/608 ⇒ **+8 全为本棒**;5 败**仍是**投放线 `tar EBUSY`,⛔ 未增一败)|`check:layering` rc=0 **✅ 无新增违规**|`store.ts` **零改动**。 ### 🔴 两条设计取舍(可推翻) 1. **`write()=false` 不虚减 `delivered`** —— false 的含义是"水位满",**数据已排队、会送达** ⇒ 若记成 skipped,"送达数"会随客户端网速变小、污染观测与判据 3。⇒ 记账归记账,投递语义不变。 2. **阈值可注入而非写死** —— 本值是**保守起点**(未标定)⇒ 注入化让"标定后改参数"不必改代码。 ### 🔴 判据演进(已写进 `09 §8`) 「修 bug」与「新增能力」判据**不同**:新增能力 ⇒ **纯插入**;**修 bug ⇒ 允许改既有逻辑**,但必须同时:① 既有语义有回归用例(本棒 = `delivered === onlineIn`)② 新行为可观测(计数/峰值/断开数)③ 阈值可注入。142 §五-6 意图不变(⛔ 不为场景改内核,只许"接扩展点"+"修内核自身缺陷")。 ### ⚠️ 一条实测教训(值得复用) **测试跑的是 `lib/` 编译产物** ⇒ 我改完 `ws.ts` 后**忘记重新 build**,导致 `hub.ts`(新的)与 `ws.ts`(旧的)不一致 ⇒ 测试连挂两次,症状是"断言该断开却投递了"而**诊断输出显示注入值正确** ⇒ 一度误以为是 Node 属性遮蔽问题。✅ **定位法**:打印两个**独立观测点**(注入值 ✅ 但 `backpressureEvents=0`)⇒ 一眼看出"读到的值对、但走的是旧代码" ⇒ **改完必 build**。 另附一条真知识:**Node 的 `writableLength` 是原型 getter**(非实例自有属性),实例级 `Object.defineProperty` 可覆盖;子类原型 getter 也有效(本夹具用实例 getter 最稳)。 ### 文档同步 `01-规范/09-…契约.md`:**§8 判据表升级为四档**(store 零改动/hub 纯插入/ws 允许改但删除行**只限投递点**/routes 纯插入)+ **新增 §11「投递可靠性与容量边界」**(11.1 背压三条行为与判据 / 11.2 阈值依据与观测面 / 11.3 容量实测读数与单房上限)。 ### 边界 ⛔ 未部署、⛔ 未动 47/106、⛔ 未 commit/push、⛔ 未动云安全组、⛔ 未改 A–E 五单结论。⛔ **未登记下一棒**。 ⚠️ **生产标定未做**:阈值仍为保守起点(4 MiB)⇒ 建议在真实流量下取一轮 `peakPendingBytes` 后校准(属观测/调参,⛔ 不是代码改动)。 🔴 **一条待用户确认的连锁影响**:原先"背压后 `delivered` 会小于 `onlineIn`"的**预期没有发生**(因为设计上不虚减)⇒ 若后续要"按慢客户端计 skipped",那是**语义变更**,需重新拍板。 --- ## 🔴 收口(32.8 万上下文强制)· IM 线 → 已开自动接续 **用户本轮三件事**(原话):「创建接续会话把IM的改造优化处理完成」「然后更新IM接入文档」「并回复插件的对接文档」。 ### 已落 - **接续包** ⇒ `接续包_IM接入面收尾与插件对接答复_20260926.md`(md5 **`da0721b2bb28150e57ee423c6c3dfff5`**,2026-09-26 00:32 冻结)。 - **接续棒** ⇒ automation **`a31da051-9d0b-445b-b232-9f5617ff1fe9`**(一次性 · `scheduledAt = 2026-09-26T00:38` · `cwds = ["E:/ProgramData/AIProject/aliyun-dsh-server"]` 正斜杠)。 - ✅ **cwds 形态已实测确认**(view 了 09-24 那条 IM 接续棒)= **正斜杠 + 大写盘符** ⇒ 与状态层口径一致;⚠️ IM 线入口 §0 头部那条「小写盘符 + 反斜杠」的规定与实测**不符**,以实测为准(⛔ 未改入口那句,待专门清理)。 - prompt 含**口径门禁 md5**、开机四步、工具上限 40。 - **入口** ⇒ `接续入口_IM线_20260922.md §0` 追加本回合收口行(背压修复 + 三份评估 + 接续登记)。 - **今日日志** ⇒ 本文件上述各节。 ### 接续包的三项「未完成」(新棒依据) 1. **IM 改造收尾三件**:① 桥 5 端点**端到端真跑**(只过单测、从未真跑)② **部署 47**(R8,动手前声明)③ **生产标定背压阈值**(改参数、⛔ 不改代码)。 2. 🔴 **反向通道(发言规则 / 事件回调)= 待用户拍板** —— ⛔ 新棒不许替原会话拍成「已定」、⛔ 不许自决补做;用户未表态则收口时再上抛。原会话判断(可推翻):**不必急着补**(降级三条已够:房间 `config` / 出向纠偏 / `host.subscribe()` 轮询)。 3. **文档 + 答复**:09 已到 §11,**缺「插件作者快速上手」**(用法散在 §10.3,且 `poc/business-plugins-im/` 三个示例是**旧契约**,未含第 8 项出向与跨进程桥);插件对接答复件**只能在本工作区产出、由用户转交**(⛔ 不得改 `dsh-plugin-partment` 一个字节)。 ### 边界 ⛔ 未部署 / 未 commit / 未 push / 未动 47/106 / 未动云安全组。本会话**不再开新任务**(成本按当前水位线性放大)。 ## IM 接入面收尾棒(00:38–01:0x) - 承 `接续包_IM接入面收尾与插件对接答复_20260926.md`(md5 已校);域锁 4 域,本棒自释放。 - **端到端真跑**(本棒核心):`tmp/im-bridge-e2e-20260926.mjs` 真起平台 + 真 HTTP + 插件侧 SDK,**23 例 fail 0**。 顺带**抓出真缺陷并修**:`src/web/routes/im.ts` 的 `bridgeAuth` 把请求头里的**包名**当内部 id 查注册表, 而注册表按 `pluginIdOf(包名)` 存 ⇒ 出向恒 `plugin-unavailable`(第 8 项在生产不可达)。 单测抓不到的原因:`test/im-plugin-bridge.test.mjs` 刻意 mock fetch、直接递归一化 id,**不经过这层翻译**。 ⇒ 判据沉淀:**"注册面"的纯单测不能替代"经过鉴权层翻译"的真 HTTP 验收**。 - **部署 47**:`/opt/dshs` 推 `lib/im/**` + `lib/web/routes/im.js` + `sdk/**`;备份 `/opt/dsh/backups/im-bridge-predeploy-20260926.tgz`; 重启 `dshs` = active,`/api/im/plugins/bridge`、`/api/im/stats` 均 401(路由已加载)。106 不需要(IM 宿主在 Manager)。 ⚠️ **坑**:本机 tar 会保留本地 uid ⇒ 47 上落地文件属主变 `197108`,须 `chown -R root:root` 修回;下次加 `--owner=root --group=root`。 - **文档**:09 新增 §12「插件作者快速上手」(只增不改);**答复件** `交付物/插件对接答复件-20260926.md`。 - **未做**:① 背压阈值生产标定(需 admin 会话/真实流量)② 反向通道(待用户拍板)③ 09 的 #2/#4/#5/#8 四条纯文档整改 ④ 可跑的最小示例工程。 ## 第 36 棒续作二 · 规划棒(2026-09-26 06:29–06:5x)· 会话 `p36c-platform-versioning` **用户原话**:「你就是平台线,看看如何修复(还有 worker 如何更新包,是不是应该有个机制,现在的网络结构各种节点和设备,如何进行版本管理和包更新机制)」⇒ **授权扩到平台线**,但本轮按「规划与执行分离」只读取证 + 出方案,⛔ 未改 `src/**` 一行。 ### 取证要点 - **机制骨架齐全**(⛔ 不是「没有机制」):worker `启动即拉 + 定时轮询`(`src/worker/agent.ts:816-819`)、`POST /shared-layer/pull`(`:812`,带 agent token)、主流程 `syncSharedLayerOnce`(`:373`)、对账 `POST /api/plugins/shared/node/sync`(`:422-427`)、Manager 对账面(`src/web/routes/business-plugins.ts:373-428`)、权威清单 `.manifest.json`、对账快照 `.node-sync-reports.json`。 - 🔴 **超时根因(D1)**:`src/worker/agent.ts:376` `const timeoutMs = opts.timeoutMs ?? 20000` —— **对账面(`:426`)与产物下载面(`:508`)共用同一 20 s 常量** ⇒ 2.0 MB 包跨云拉不完 ⇒ `AbortError: The operation was aborted due to timeout`(与 106 上报串**逐字吻合**)⇒ 106 永远停在旧版。 - 其余缺陷:D2 无重试退避 · D3 失败历史不可见 · D4 只有「最新一份」版本 · D5 无 channel/定向 ⇒ 不能灰度 · D6 **设备(桌面端)不在分发链上** · D7 无全局版本矩阵视图 · D8 台账与实时视图不一致。 ### 产出 `$WS/交付物/版本管理与包更新机制-现状与方案-20260926.md` —— 现状取证(带行号)+ 8 条缺陷 + **三层方案**(A 止血/B 版本管理机制/C 跨区拓扑)+ 只读前置 + 决策点 + **8 段交接要素**。⇒ **同时是下一棒的「唯一执行单」**。 ### 🔴 未登记下一棒 §5 有 **1 项待拍板**(修到什么程度:只修超时 A / A+全局视图 C / A+完整版本管理 B)⇒ 依「要用户拍板的,等拍了再新建接续会话」**⛔ 未登记 automation**。我的倾向:**A 先落、C 紧随**。 ## IM 反向通道定案棒(06:3x) - **用户拍板**:「都需要实现完整,才能让插件接入,否则两边都要返工」⇒ 反向通道(平台 → 插件)**必须补、做完整**。 - **本棒只定案不实现**(会话预算 18 万+,硬约束)⇒ 产出施工规格 `接续包_IM反向通道实现_20260926.md`(含设计已定项/文件清单/验收/回滚/开工命令)+ 登记执行棒 automation `cce748b8`。 - **设计要点(已定项)**:实例侧 SDK **长轮询拉取**(保持"拨出式",⛔ 平台不打进实例、⛔ 不需实例可寻址、⛔ 不新增凭据); `speak.check` 同步(带 deadline)· `event.deliver` 异步(不阻塞消息流); 超时缺省**放行**(与同进程 `binding.ts` 口径逐字一致,⛔ 不新增"不可达即冻结房间")+ 具名计数;契约可声明 `onTimeout:'deny'`(回合制必须声明);每实例有界队列,满则丢最旧并计数。 - **口径同步 3 处**:09 §10.4/§10.5/§12.5 · `交付物/IM插件跨进程桥-20260925.md §5/§6` · `交付物/插件对接答复件-20260926.md §0`。 - 🔴 **实测发现**:`p36c-platform-versioning` 会话持 `src/web`、`aliyun-dsh-server/交付物`、`.workbuddy`、`05-交接单` 域 ⇒ 执行棒必须先 `preflight-lock.sh`;`src/web` 未释放时**先做不冲突部分**(新文件/文档),`routes/im.ts` 放最后。 - **教训(沿用)**:跨进程能力的**单测替代不了真 HTTP 验收**(本线 09-26 已实证一次)。 ## 插件投放与分库线 · 第 36 棒续作三 · 拍板落地棒(2026-09-26 06:36)· 会话 `p36d-platform-versioning` ### 拍板原话(用户本人 · 06:36) 「**C 方案,然后B,这次就用版本更新机制去更新网络上各节点,子节点和设备**」 ### 口径(⛔ 别被方案里的层名混淆) - 用户说的 **C 案** = 层 A(止血 20 s↔300 s/按大小动态/重试退避/失败留痕)+ 层 B-1~B-4(版本目录化/channel 灰度/定向钉版/**设备纳入**)。 - **随后做** 的 **B 案** = 层 B-5(全局版本矩阵视图)。 - **层 C 跨区拓扑本次不做**(属覆盖网络线架构层),但实现须**兼容**其口径:每区 Manager 各持内容源、节点只向本区拉、同版本号须字节一致。 - **收尾必须真跑一次分发**:`dsh-plugin-mcn-suite@0.5.1`(含以后版本)→ **47 本机 worker + 106 子节点 + 设备(桌面端)**,读数验证(`w-106` 对账快照 `stale`/`failed` 清空)。 - 🔴 **执行棒第一件事 = 取证设备(桌面端)侧载体**(承缺陷 D6「设备不在分发链上」):桌面端有无「包/插件目录 + 拉取入口」、凭据走哪条。**有 ⇒ 按同一对账协议接入;没有 ⇒ 具名报告缺口 + 给最小接入方案,⛔ 不硬造。** ### 产出(本棒只做记录落地,⛔ 未改 `src/**`) - 唯一执行单 `交付物/版本管理与包更新机制-现状与方案-20260926.md`:§5 改为「🔴 已拍板」+ **§8 执行棒细化**(§8.1 S1→S4 顺序表/§8.2 未知项三条/§8.3 既有纪律)⇒ 14,893 B。 - 入口 `接续入口_插件投放与分库线_20260922.md`:§0 落 06:36 拍板行(最新一行 = 执行依据)+ §2 落「续作二/续作三」注 ⇒ 343,592 → **已含两节**,977 → 986 行。 - `$DOC/05-交接单/README.md §一` 第 44 行(04 单状态行)追加 06:36 拍板 + §8 细化 + 执行棒登记。 - ✅ **执行棒 automation = `da73ab87-fb2f-47f0-a8d8-8ea3ad35eb19`**(一次性 · `2026-09-26T06:52`);本线入口 automation `35e1bfbc-…` 保持 `PAUSED`。 ### 🔴 本棒事故(已自愈 · 值得进技能) `tmp/p36/patch-c3.py` 步 1 成功(交付件 §5/§8 落盘),步 2 报 `TypeError: sequence item 11: expected a bytes-like object, str found`(`NEW0` 漏 `.encode("utf-8")`)。**真正的伤害不在那行 TypeError** —— 复合式 `open(P,"wb").write(b"\n".join(parts))` 的求值顺序是 **先 `open()`(立即截断!)→ 再求值 `join()`**,join 抛错 ⇒ **入口被清成 0 字节**。 - **发现方式**:事后 `wc -c` 得 `0`(若只看脚本 traceback 只会以为"没写进去")。 - **复原**:`git checkout -- <入口>`;HEAD 版 = **341,330 B / 968 行**、含 06:29 规划棒行、且远端对账 `0 0` ⇒ **无损**;复原后 `git hash-object` == `git rev-parse HEAD:` = `1d44073b…`。 - **判据(新增铁律)**:改共享文件**只准** Edit 精确片段替换,或「**先算全量 bytes 再一次性写**」;⛔ **禁用 `open(...,"wb").write(表达式)` 复合式**(表达式求值前文件已被清空)。同源风险:**任何"写前先 open 再算内容"的写法**都有此坑。 - ✅ **补救**:改用 Edit 工具做三处插入(入口 §0/§2、README §一),不再走整文件覆盖。 ### 边界 ⛔ 未改 `D:/github/dsh_shenxian/src/**` 一行(只读取证);⛔ 未 ssh 真机做任何变更(本棒纯记录落地 + 收口);⛔ 未碰他线 `src/web`·`routes/im.ts`。 ### 收口(06:4x–06:5x) - **提交** `2a1226c`(`docs(平台线): 版本管理与包更新机制 拍板落地(C案→B案 · 执行棒登记)`)→ **推送** `a38d8b6..2a1226c master -> master`(rc=0)→ **对账** `git rev-list --left-right --count origin/master...HEAD` = **`0 0`**、`HEAD == origin/master` ✅。 - **仅 3 件入库**:本件 + 本线入口 + 本日志;`git diff --cached --name-only` 自查**无** `tmp/`/中间产物/`05-交接单`;⛔ **未 `git add -A`**、⛔ **未代提交他线**(`CODEBUDDY.md`/`state.py`/`.codebuddy/rules/server-ops.md`/`.workbuddy/memory/MEMORY.md`/他线 `接续入口_*` 全部未纳入)。 - **文档库侧**:本线唯一改动 = `$DOC/05-交接单/README.md §一` 第 44 行;`git check-ignore -v` ⇒ `.gitignore:45` 整目录忽略、未跟踪 ⇒ **按既有口径不入库**(⛔ 未 `-f`)。推送前 `docs-sync-check.sh` = **❌(一致 268/不一致 21/仅本地 4/仅服务器 0)**,差异面**全为他线在途**(`07-scripts/*`、`08-skills/*`、`CODEBUDDY.md`、`01-规范/09`、`02-架构设计/*`、`04-调整方案/*`、`05-交接单/*`)⇒ **非本线引入、交文档库同步线处置**。 - **automation**:`35e1bfbc`(第 36 棒入口)= **`PAUSED`** ✅;`da73ab87`(执行棒)= **`ACTIVE` · `2026-09-26T06:52`** ✅(收口 06:44 + 8 min,落在 5~8 min 窗口内)。 - **锁**:域锁 `p36d-platform-versioning` 反序释放(先 `--release-publish` → 最后 `ME="p36d-platform-versioning" --release-exec`)。 - 📄 收口证据全文(命令原文 + 输出原文 + 退出码)⇒ 本件 **§9**。 ## ✅ IM 线|反向通道(平台 → 插件)实现棒收官(06:4x–06:5x · 会话 im-reverse-channel) - **落了什么**:新增 `src/im/sdk/callback-bus.ts`(每实例有界队列 + pending + deadline + 计数 + `attachImBridge()` 复合接线);`routes/im.ts` **纯插入**(`+451/-0`)2 端点 `bridge/pull`(长轮询,服务端硬上限 20 s)与 `bridge/result`;`registry.ts` 加 `callbackStats` 注入(观测面那行是既有行,不许改);`types.ts` 加 `SpeakRuleContract.onTimeout`;`binding.ts` 事件构造改用 `roomEventOf` 单一来源;`sdk/im-plugin-host/index.mjs` 加 `onSpeakRule()`/`onEvent()`/`startCallbacks()`。 - **两条都通**:`speakRules.check`(同步,超时按 `onTimeout` 兜底 + `bridgeTimeouts`)与 `events.onEvent`(异步,队列满丢最旧 + `bridgeDropped`)。 - 🔴 **关键坑(已修,务必记住)**:写路径的守卫 `binding.host.hasPlugins()` 若不被复合增强 ⇒ **只有实例内插件登记时 `speak.check` 永不触发**(静默失效)⇒ `attachImBridge` 里一并增强该守卫。 - **验收**:build rc=0|新测试 9 例 + 既有 im-plugin-bridge/im-backpressure/im-sdk **fail 0**|**真 HTTP e2e 28 例 fail 0**(deny 生效且不落库 / 超时放行且计数+1 / 事件真送达)|layering rc=0 无新增违规|`routes/im.ts` **-0**。 - **部署 47**:备份 `/opt/dsh/backups/im-callback-predeploy-20260926.tgz` → tar `--owner=root --group=root` → `restart dshs` ⇒ active;`/api/im/stats`、`/api/im/plugins/bridge` 在 19100/25000/3080 回 401(路由存活)。 - ⚠️ **既有 3 红与本棒无关**:`test/im-sdk.test.mjs` 35/36/78 要求 `hub.ts` 零 diff、`ws.ts` 零删行,而工作区开工前即带 `hub.ts +36/-0`、`ws.ts +84/-1`(第 15 棒背压修复在途未提交)。 - ⛔ **未做**:09/08 文档改「已通」(`dsh-server-docs` 不在本棒域锁内,按 R7-边界未动手);`HOST_SDK_VERSION` 未升(域外测试硬断言 `'1.0.0'`)。 - 已释放域锁;下一棒 automation `6fe3c77a`(07:10)= §8-1 最小示例工程 + §8-3 文档收尾。 ## ✅ 平台线|版本管理与包更新机制 **执行棒**(07:2x–07:5x · 会话 `p36e-platform-versioning` · automation `da73ab87`)S1–S4 收官 - **拍板**(06:36 原话):「C 方案,然后B,这次就用版本更新机制去更新网络上各节点,子节点和设备」⇒ 层 A + B-1~B-4 先做、B-5 随后、层 C ⛔不做。 - **落了什么**:① `src/worker/agent.ts`(+≈50 行)—— 对账/下载**超时解耦**(各自 `AbortController`、按 `content-length` 重算预算)+ 体积动态预算纯函数 `effectiveDownloadTimeout()` + **只重试瞬态**(`PackError.retryable`;`not_here` 不重试)+ 退避 `base×4^n` + 失败留痕(`attempts/firstFailedAt/lastFailedAt`);**全部 7 个阈值走 env**(`DSHS_SHARED_PULL_*`)。② `src/web/routes/business-plugins.ts` —— B-1 版本目录化(`.versions//.tgz` 归档 + `history[]` + `POST /shared/rollback-live` 原子换)|B-2 通道(`channel` + `x-dsh-pull-channel`)|B-3 定向钉版(`pinnedHosts`)|B-5 `GET /api/plugins/shared/status` 全局矩阵;新增字段**全可选**(老节点读新 manifest 不崩)。 - 🔴 **顺手修掉一个真 bug(回归用例钉住)**:重试**成功**后旧码仍把该包推进 `failed[]` ⇒ 读数自相矛盾(`pulled` 与 `failed` 同现)⇒ 现以显式 `pulledOk` 门控。 - **S4 真跑分发(判据全绿)**:在 106 只清掉**它自己的**拉取记录(共享层一字不动)⇒ `stale:[mcn-suite] → pulled:[mcn-suite]`、**2,018,751 B / 3437 ms**(经 443 中继到 47)、拉后指纹 `27f6c6d7…` 与权威逐字相同;第二轮对账 `stale:[] pulled:[]`;**47 侧 `.node-sync-reports.json` 的 `w-106` ⇒ `stale:[]` `failed:[]` `inventory` 4 条** ✅。 - 🔴 **w-47「本机 worker」= 结构性免拉**(⛔ 别当故障修):它与管理端**共用同一个共享根**(`config.ts:627` 默认 `/bundled-plugins`),`POST /shared-layer/pull` 回 `{"ok":false,"reason":"pull_disabled"}` 属**正确形态**。 - 🔴 **B-4 设备(桌面端)腿 = 具名缺口(未做)**:本机**无桌面端工程载体**(只有设计文档,`apps/desktop` 不存在)+ 桌面端**无包/插件拉取入口**+设备在**另一张表**(键=网络+设备 id)⇒ 现有「主机密钥」校验**认不出设备**。最小方案已写进报告(设备公钥复用 + 同形四步协议 + 矩阵同表呈现),前置 = 先有桌面端工程。 - **部署**:备份 `/opt/dsh/backups/vmgmt-20260926/`(47+106)→ scp 4+2 文件 → `restart dshs`(47) + `restart dshs-worker`(47/106) ⇒ 三机 `lib` md5 **逐字一致**;106 新日志打印新预算行 ✅。 - **副判据**:`npm test` **646/641/3**(3 红 = IM 线在途未提交的「内核零改动」断言,与本棒零交集)|本模块单测 **32/32**|红线零新增|共享根 `incoming/trash/pull-stage` 残留 **0**|🔴 **34/362/5 影子读未重取**(夹具属 MCN 数据面线、已在前棒清理,本机无副本)—— 具名记在报告 §5。 - **发现(建议后续)**:F-1 归档何时才有第一条(本轮全是**同内容幂等早退** ⇒ 不归档,语义正确但 `history` 为空)|🔴 **F-2 两条回滚路径素材未打通**:`rollbackTgz`(0.5.0.tgz) **已在位**却没进新归档 ⇒ `rollback-live` 去回滚 0.5.0 会回 `409 version_not_archived`(建议发布时把既有素材**登记**成归档一条,纯插入)|F-4 候选池只有 0.5.1 ⇒ **本轮不存在"更新的版本"可分**(⛔ 不伪造新版本)。 - ⛔ **未 commit / 未 push**(`CODEBUDDY.md §4`:未明确要求;且工作树里**另有别线在途改动**,拒绝连带提交)。⛔ **未登记下一棒**(唯一剩余实质项 B-4 卡在**权限面拍板**上 ⇒ 按「要拍板的等拍了再建」)。 - **R4/卫生**:临时 admin 会话现场建、用完删(`deleted=1`);47/106 的 `/tmp/p36e-*` 与 `/root/.node-pull.json.p36e-bak` 清空(双侧计数 0)。 - 📄 交付件 = `$WS/交付物/版本管理与包更新机制-执行报告-20260926.md`(判定表/逐条命令+输出+rc/S4 全过程/副判据/**未达成具名**/发现 5 条/**待拍板 1 项含优缺点**)。 ### 🔴 平台线|07:40 补正:B-4 设备腿**取证更正 + 缺口降级**(用户指路 → 桌面线工作区) - **用户原话**:「有桌面版的开发项目就在 **AI1Net Desktop 工作区**里面,可以把如何这个进行更新写一个文档让这个对话去接入和实现。」 - 🔴 **推翻本棒 §4.5 初稿两条结论**(已具名更正进交付件):① ~~本机无桌面端工程载体~~ ⇒ **错**:`E:/ProgramData/AIProject/dsh-ai1net-desktop` 只是**文档线**(`CODEBUDDY.md` + `docs/` + `.workbuddy/`,**非 git 仓、无执行锁、无 `state.py`**),**代码在 `E:\github\dsh-client`(自研仓)+ 工作树 `dsh-desktop-0.1.7rc2` / `dsh-desktop-0.1.5rc1`** —— 初稿只查了本工作区/`D:/github`/`E:/ProgramData/AIProject`,**漏了 `E:\github`**。② ~~设备凭据通路不可复用~~ ⇒ **错**:**用户级设备授权已存在且已实测跑通**(`POST /api/overlay/device-grant`,`{nodeKey}`→`grant{doc,sig}`+`hmacSecret`,租约 24 h,`hostId=d--<公钥前8位>`;桌面线 `S4` 用真登录态跑通「登录→设备凭据→拨号入网」)。 - ⇒ **B-4 缺口降级**:❌ 不是"新造设备凭据",✅ 而是**给三个只读口增认「已有设备身份」**(一处校验臂,用已登记的设备公钥)。⚠️ 仍是**小幅/只读/可撤回**的权限面扩展 ⇒ R5:**先出评估、等用户点头再由平台线实施**。 - 📄 **交付物(写到桌面线工作区,用户点名授权)**:`E:/ProgramData/AIProject/dsh-ai1net-desktop/docs/对接单_桌面端接入版本管理与包更新机制_20260926.md` —— 协议四步(对账/取包/落地/回报)+字段+**三条语义纪律**+复用点(设备身份、devkit、`link:` 装配)+**仅两处改动**+**3 棒落地与判据(含三条反例判据)**+**10 条坑**+⛔边界;并在该工作区 `docs/README.md` 索引登记一行。 - 🔴 **仍未做(具名)**:桌面端**拉取器**+**共享包接 profile 装配**(判据:补齐后实例页「能力管理」栏才出现)⇒ 待桌面线接棒;平台侧校验臂待拍板。⇒ **⛔ 本线未登记下一棒**(跨线项,等拍板/等桌面线)。 ### 🔵 平台线|07:5x 平台能力插件化**判定**(用户问答 · 只出判定,⛔ 未改代码) - **用户原话**:「设置中的功能是否也可以做一个插件,这样平台节点用户和客户端用户只需要接入这个插件就可以进行登录、更新、获取插件、使用插件接入平台功能。基于插件去构建平台功能。」 - **判定 = ⚠️ 部分可行**:能做且**已有先例**(桌面线「设置 → 插件 → 本地代理」= 一个自研包两个入口 + 界面落**官方已有槽位**);但 ⛔ **不能"一个包办全部"** —— **登录**与**客户端自身更新**是**壳级**职责(插件存在于"实例与界面就绪之后",登录/换壳在它之前 ⇒ 逻辑倒置;桌面线已定"开窗直进实例页、未登录点胶囊才弹登录")。 - 🔴 **正确形态 = 薄壳 + 一个能力包**:壳只做①起实例②渲染③壳级自更新④交出身份;其余(登录态/更新检查/可用插件/平台能力调用)全由**一个能力包**承载,UI 落官方已有槽位,⛔ 不新造页面体系。**分层不倒置判据 = 插件卸载后壳照常起、实例照常用**(否则命中 R11)。 - **三条边界**:① 一处身份一处可撤回(不各发一套凭据)② ⛔ 不给插件开平台内网直达(能力包 = 实例内插件 + 平台侧薄配对端,走既有对外 https 面)③ ⛔ 插件不背壳级职责。 - **代价(诚实)**:插件跑实例内 ⇒ 天然拥有该实例全部权限(含用户自配模型凭据)⇒ 能力包必须**收窄**、⛔ 不下发平台共享密钥;「一个包办全部」会打破**离线可用** ⇒ 负向迭代。 - **落地顺序 5 项**:①共享包库+拉取机制 ✅本会话已上机 ②插件模型+设置槽位先例 ✅桌面线已跑通 ③**设备读内容权限** ⏸️待拍板 ④桌面侧拉取器+接 profile 装配 ⏸️待桌面线 ⑤把平台能力做成能力包(首版只做只读三件)🔜 新增。 - 📄 判定件 = `$WS/交付物/平台能力插件化-形态判定与分层_20260926.md`。🔗 ④⑤ **都等**③(仍是上一轮那**一项**待拍板,⛔ 本轮未重复问)。⛔ 未登记下一棒、⛔ 未 commit。 ### 🔵 平台线|08:1x 追问判定:三条期望逐条(`插件更新=客户端更新` / `用插件即需登录` / `与官方主程序脱开`) - **用户原话**:「把这些功能都做在插件中是否就可以实现插件的更新就是客户端的更新,使用这个插件就需要登录确认身份,跟 DSH 的主程序也脱离开来。」 - **判定三行**:① ⚠️ **九成成立**(界面+平台能力随插件更新;但换壳/换安装包/换官方版本、以及"取插件装插件"本身必须在**引导层**)⇒ **"客户端"拆两层:引导层(极小、跟安装包)+ 能力层(插件、可更新)**;⭐ 目标 = **引导层冻结到只做五件事**(起实例/渲染/取身份/拉包装配/壳级自更新)。② ✅ **成立且更强**:若**包只能在登录后拿到** ⇒ 未登录 = 没功能也没代码 ⇒ **身份成为信任链起点**(⛔ 只不许登录动作依赖插件)。③ ⚠️ **只能"业务脱开、扩展点依附"**:账号/门户/内容分发/数据面/IM **一直就是脱开的**(官方 dsh 无账号体系);插件仍**寄生于官方扩展点** ⇒ 接口变要跟。 - 🔴 **实测反例(本线自己的数据,⛔ 别夸大)**:0.1.5→0.1.7 **自研插件零改动平移成功**,但**同期壳/启动侧硬改三处**(profile 路径/必需运行时目录/协议版本)⇒ 那是"**恰好契约没变**",**不是结构性解耦**。要彻底脱离只剩"自研内核"⇒ 代价 = 放弃官方演进 + 冲突既定"UI 与服务器版本一致"口径 ⇒ **不建议**。 - 🔴 **新增安全要件**:「插件=客户端」⇒ **"内容即代码"** ⇒ **包签名校验**由"建议"升级为**要件**(现状:仅内容指纹 + 发布时哈希,**无发布者签名**;签名根密钥归覆盖网络线口径)⇒ **本线只登记不设计**。 - 📄 已并入判定件 **§7**;入口 §0 已登记。⛔ 未改代码、⛔ 未登记下一棒、⛔ 未 commit。 - ⚠️ **本棒一处操作留痕**:抢锁时用 `| head -3` 截尾 ⇒ 退出码被 SIGPIPE 污染成 **141**(判据只剩打印的 "✓ 已持域锁" 一行)⇒ 又一次印证「**抢锁别用管道截尾看退出码**」。 ### 🔵 平台线|08:2x 平台新增功能**总盘点 × 插件化归属**(用户要求"全列出来看哪些能进插件") - **用户原话**:「还是没有明白为什么不能,你能不能把现在开发的平台基于 DSH 主体新增所有功能全部列出来,看一看哪些能整合到插件中,相当于是基础服务的插件。基于这个插件可以去使用现有新增能力。」 - 🔴 **判据收敛为一句(可自判)**:**「这件事发生在插件存在之前还是之后?」** 之前 ⇒ 引导层(不可插件化);之后 ⇒ 插件(应尽量进)。 - 🔴 **关键澄清(此前的误会来源)**:**L1 平台自有服务**(登录/门户/多租户编排/插件投放/数据面/协作内核/网络中继)**从来不需要"插件化"—— 它们本来就不依赖 DSH**(官方无账号体系,这些从第一天就是独立进程);它们在插件里的角色只是**入口/调用方**。⇒ **「基础服务插件」= 把 L2「实例内/客户端内」那一层收成一个包**。 - ⛔ **不可进清单 = 4 件**(全在引导层):① 起实例 ② 起窗口渲染首屏 ③ 取身份(登录+设备授权) ④ **把包拉下来并装进去**(含版本管理与拉取器)。 - **证据面**:平台自有 15 个路由文件(`auth/admin/admin-user-ops/whitelist/business-plugins/plugin-data/skills/sessions/desktop/dsh/domain/im/overlay/overlay-device/overlay-nodes`)+ 各目录(supervisor 12 · fs 6 · im 12 · db 10 · net · worker 3 · isolation/config/crypto/platform-paths · nginx)。 - ⚠️ **一处不建议(可推翻)**:**门户六页不要搬进插件**(桌面线口径 = 套壳 + 同步服务器代码 + UI 与服务器一致 ⇒ 搬进去会出现**两份 UI 漂移**,属负向迭代)。 - 📄 交付件 = `$WS/交付物/平台新增功能总盘点与插件化归属-20260926.md`(8 张分组表逐项 ✅⚠️❌ + 形态 + 不可进清单 + 收益预期 + **3 条不确定项**)。⛔ 未改代码、⛔ 未登记下一棒、⛔ 未 commit。 ### 🔵 平台线|08:2x 形态澄清:**只装官方 DSH + 加我们的插件** ⇒ 判定 ✅ 大致成立 + **更正我此前三句话** - **用户原话**:「用户只需要安装一个官方的 DSH 主程序,然后把我们的插件加进去就能实现 L1 和 L2 的功能,然后用户登录这个功能去使用相应的能力和模块。」 - 🔴 **更正(具名,教训价值高)**: ① ~~「登录必须在插件之前,做成插件会死锁」~~ ⇒ **在"官方 DSH 当宿主"下不成立**:官方页面先渲染,插件内提供登录/身份区块完全可行。**原论证前提是桌面自研壳的既定形态(壳要决定进实例页还是登录页)—— 换宿主,前提就变**。 ② ~~「必须有薄壳 + 引导层」~~ ⇒ **本形态不需要自研壳**:**官方 DSH 自己就是引导层**。 ③ ~~「拉取器必须在引导层」~~ ⇒ **收缩为"只在首装时成立"**;此后**更新全部可由插件自己做**(拉新包→换装配→提示重启)⇒ **"插件更新=功能更新"在首装之后成立**。 - ⚠️ **三条硬约束**:① **首装包必须匿名可取**(否则才是真死锁;⇒「未登录=既没功能也没代码」**只对后续增量包成立**)② **L1 只是"被使用"、不搬进用户机器**(多租户编排/沙箱/配额/中继服务器/协作内核仍是**服务器角色**)③ 系统级能力仍不可插件化("本地全自足"= 另一个产品形态)。 - **得失**:得 = ⛔ 不需自研客户端、**插件更新=功能更新 100% 成立**、用户只装官方程序;失 = 界面受官方槽位限制、官方升级要跟、**实例启动策略(自启/离线兜底/窄档)由官方决定**(正是桌面线自研壳换来的)。 - **落地清单 5 件**:①基础服务插件做成一个可安装包 ②**匿名首装渠道** ③插件内登录与身份态(平台接口已具备)④入网拨号放进插件(异步子进程、走 443、**无需 root/端口**;与桌面线"网络载体是原生守护"**不冲突**,那是壳口径)⑤插件自更新(机制已就绪,落点从引导层**移进插件**)。 - 📄 已并入盘点件 **§13**;入口 §0 已登记。⛔ 未改代码、⛔ 未登记下一棒、⛔ 未 commit。 ### 🔵 平台线|08:3x 双角色方案分析:**单插件,用户自选"当节点提供服务" vs "只当用户设备接入"** - **用户原话**:「用户在 DSH 主程序上加入插件后,他可以选择去当节点提供服务,还是可以选择自己只是一个用户设备去接入。」 - **判定**:⚠️ **界面能做成插件;"当节点"能不能成不由勾选决定**。三判据:① **物理优先于意愿**:当节点 = **别人能连到你**(判据 = **入向可达实测**;**同局域网零门槛**)② **平台放行**:对外能力 ⇒ 白名单默认拒绝 + 上限 8 条 + 半自动升格(加节点 = **平台侧配置动作**)③ **隔离**:**承载他人实例**需系统级沙箱/独立账号 ⇒ ⛔ **插件做不到**(Windows 更没有)。 - **三档角色**:✅ 用户设备(终端,只拨出、走 443、**已跑通**)|✅ **轻节点**(中继/带宽/**内容副本**:无需系统级隔离,**与本会话拉取机制直接衔接**)|❌ **重节点**(替别人跑实例 ⇒ 属**正式节点部署**)。 - ⭐ **建议落法 = 三步升格**:① 插件本地**体检**(可达性/带宽/局域网)② 用户主动"申请成为节点"上报体检单 ③ **平台半自动放行 + 下发配置**。⇒ **选择权在用户、生效权在"网络+平台"**;用户**装一次**先当终端、后续升格 ⇒ 与既有"半自动升格"口径天然吻合,⛔ 无需为节点另做客户端。 - ⚠️ **两个代价必须点名**:① 当节点 = 承接**他人流量/数据** ⇒ **治理与责任**(须写清含义 + 可撤销)② **"看起来能开其实开不了"**(家用网络多数不可达)⇒ **必须先体检再显示**,⛔ 不给假开关。 - 📄 已并入盘点件 **§14**;入口 §0 已登记。⛔ 未改代码、⛔ 未登记下一棒、⛔ 未 commit。⚠️ **仍待你定 = 设备读内容权限那一项**(入口/报告 §9 在册)。 ### 🔵 平台线|08:4x 节点归属可选场景分析(47 当节点 · 用户可选"成为节点"或"接入某节点") - **用户原话**:「47服务器就是一个节点它加入了这个插件后…其他的用户…也可以选择接入某个服务(106接入47)。接入后就可以使用节点对应的功能和提供的三方插件。」 - 🔴 **判定 = 必须把"接入"拆三种**:**A 内容归属**(从哪个节点拉插件/能力)✅ **能且已实测**(106→47:2 MB/3437 ms)|**B 网络归属**(哪个网络/中继)✅ 能(**同局域网零门槛**,跨公网需对方**可达**)|**C 承载归属**(实例跑在哪台机)⚠️ **不能一键切**(有状态:home/数据在原机 ⇒ 切换 = **迁移**;且**当承载体需系统级部署**)。 - 🔴 **一句话**:**形态可行** —— 平台本就是**控制面联邦**,"接入谁"在协议上就是**可切换的归属**;**但可自由切的是"无状态归属"(A/B)**,"实例落在哪台机"(C)**只能迁移**。 - ⭐ **构件大多已在生产(⛔ 别重造)**:A 已上机(本会话拉取机制)|B 已实测(登录→设备凭据→拨号,租约 24 h)|准入治理已有(**白名单默认拒绝** ⇒ 加节点 = **平台侧配置动作**;半自动升格 + 上限)|归属三判据(**只有 Manager 能写归属/租约** · 跟用户走⇒实例 home · 本机运维⇒Worker 本地库)。 - ⚠️ **四条点名**:① 🔴 **"节点"≠"插件"**(节点 = **服务进程** + 插件两端;插件是**开关与入口**;只装插件不跑服务 ⇒ 只是客户端)② 🔴 **归属必须排他**(否则脑裂/双写 ⇒ 切换按**迁移**)③ 🔴 **信任链**(接入 = 把"用谁的内容"交给它 ⇒ **包签名校验**从"要件"升为**必需**,跨线只登记)④ ⚠️ **跨区可见性默认被这条推翻**(现状倾向"互不可见")。 - **建议最小版三步**:①插件内"接入点选择"(列节点+**本地体检**+申请)②平台放行并下发归属(A+B)③插件按归属切换;**C 承载留到有迁移方案再开**,⛔ 不开"看起来能选其实切不动"的入口。 - 📄 已并入盘点件 **§15**;入口 §0 已登记。⛔ 未改代码、⛔ 未登记下一棒、⛔ 未 commit。 ## 收口(07:3x–07:5x)· IM 线 · 反向通道收尾棒(`im-reverse-finish`) - 依据 = `接续包_IM反向通道实现_20260926.md` §9 开工四步(state.py ✅ / md5 `935c7180…` / preflight 【A】3 个 +【D】2 个文档路径 / 域锁 `poc/business-plugins-im,dsh-server-docs/01-规范` 已抢已释放)。 - **§8-1 已做**:新增 `poc/business-plugins-im/minimal-callbacks/`(5 文件 · 零依赖 · 自带模拟平台)。 一键自检 `node self-check.mjs`(Node 22)⇒ **25 例 · fail 0 · rc=0**;覆盖 `onSpeakRule`(回合制 · `onTimeout:'deny'`)+ `onEvent` 两条跨进程回调 + 未接线逐字不变。 - **§8-3 已做**:09 §10.4/§10.5/§12.1/§12.3/§12.4/§12.5 改「已通」(残留扫描零命中);08 顶部指针 + §11 变更记录补 09-26 行;`onTimeout:'deny'` 已在 09 与 08 双双点名。 - **约束自查**:本棒**零 `src/` 改动**(内核四档一行未动);⛔ 未 commit / push;⛔ 未动其它工作区文件。 - ⚠️ **接续包 md5 已变**(本棒改写了它)⇒ 新值 = `c811c18279c66ca286a177919e59da72`,下一棒按此校验。 - 遗留:§8-2 表声明入口口径(`package.json#dsh.data.schema` ⇄ 09 `tables:`)**仍未裁**;§8-4 背压阈值生产标定(需真实流量,改 env 不改码)。 ## 收口(07:4x–08:0x)· IM 线 · 文档收尾棒(`im-docs-close`) - 目标 = 用户令「把需要执行的工作安排步骤全部执行,直到插件可参考帮助文档准确地接入 IM」。 - 域锁 `dsh-server-docs/01-规范,sdk/im-plugin-host,test`(已释放)。 - **09 契约收尾(新增 §13–§16 + 6 处改)**: - 新增 **§13 移动端专项**(IM 特有 3 条,判据仍以 08 §4-补 为准)· **§14 错误码全表**(通道层/平台侧/数据面/反向通道四组,源码现读)· **§15 提包前自测清单**(9 条)· **§16 变更记录**。 - **§4 定数据面声明口径(消 P1 打架)** = 「放哪」与「长什么样」是两件事:投放/建库读 `package.json#dsh.data.schema`(`schema.ts:parseDeclFromDir`),运行时读 `manifest.tables`(`im/sdk/host.ts:assertManifestShape`)⇒ **两处写同一份**。 - §5 两档参数改自洽表述(消「档案 107/108」依赖,编号降为脚注);§7 示例三个改**四个**(点名 `minimal-callbacks/`);§8 补 4 条测试命令 + **实测读数**;§12.2 补 SDK 版本。 - **SDK 升版**:`HOST_SDK_VERSION` `1.0.0` → **`1.1.0`**(新增反向回调;向后兼容)+ 同步 `test/im-plugin-bridge.test.mjs` 的硬断言。 - **08**:§4-补 加 `09 §13` 交叉指针;§11 变更记录 09-26 行补 ⑨。 - **过程档案加状态块**:`交付物/IM插件接入完备性审计-20260925.md` 头部加「本文已被推翻」表 —— 其 §0/§4 结论("跨进程那层不存在 ⇒ 接不进来")**已作废**,指向 09 定稿。 - **现跑实测(全部 rc=0)**:`im-sdk` 75 pass/0 fail/3 skipped · `im-plugin-bridge` 14/0 · `im-plugin-bridge-callback` 9/0 · `im-backpressure` 8/0 · 示例自检 25/0 · `check:layering` 无新增违规。 - 未做(**不在本棒 lane**,如实登记):数据面取数口**部署 47/106 + 真机验收**(属投放/分库线,`交付物/插件数据面-运行时取数口落地-20260925.md §六-1`;本机 `lib/**` 混有他线在途改动,直接 build+推会把别线半成品带上生产)。 ### 🔵 平台线(插件投放与分库线)|08:5x 追问链收口 —— §12.5 结论速览 - **动因**:用户连问 4 轮「插件化形态」,答案分落在 `平台新增功能总盘点与插件化归属-20260926.md` §9–§15;用户要读 150 行才找得到判定 ⇒ 加**索引层**(不改论证)。 - **动作(1 处新增)**:该件新增 **§12.5 追问链结论速览** —— 四问 → 判定 → 关键边界 → 指向原文;附**三条硬约束**(① 首装包须**匿名可取** ② L1 **只用不搬** ③ **系统级能力**不可插件化)+ **唯一待拍板项**转述。 - **同步**:入口 `接续入口_插件投放与分库线_20260922.md` §0 加一行(新→旧序);本日志本节。 - **未做**:⛔ 未改 §13–§15 正文;⛔ 未 commit / push;⛔ 未登记下一棒。 - **锁**:`--claim-exec` rc=0 → 改完已 `--release-exec`。 ### 🔴 平台线(插件投放与分库线)|09:0x 「为什么不行」逐层拆解 + Manager/Worker 两半(§16) - **用户口径**:「还没有弄明白继续分析,为什么不行 1、选择当节点:还包括区分 manager 和 worker」⇒ 此前 §15 只给了判定(承载归属不可切),**没给机制**,本棒补机制。 - **取证方式**:读平台自有服务源码(只读),**全部结论带文件/机制锚点,非推演**。关键新发现: - 🔴 **`src/worker/` 全目录零 DB 引用**(实测)⇒ **Worker 是无库的纯执行体**:「它不做归属决策(谁托管谁是 Manager + PG 的事),只负责'在这台机器上把实例起停好'」(模块头原话)。四条纪律:单向拨入 / 幂等键 / 最小接口 / **self-fencing**。 - 🔴 **`schema.ts` v7 注释+`lease.ts` 三条不变量** = 「为什么不能有两个判决者」的原文依据:防双写靠**原子 CAS + epoch(fencing token) + 租约**,整体假设**单一判决者**;脑裂下游 = 两个进程写同一个实例的家 ⇒ **会话日志 append 冲突 = 数据损坏**。 - 🔴 **`spawn.ts findFreePortInRange` 注释**:端口按 worker 划**互不重叠区间**的原因 —— `listen(0)` 会让两台 worker 撞号,`ssh -R` 失败被**静默忽略** ⇒ Manager 按该端口拨 ⇒ **打到别人的实例**(2026-09-16 实测)。这是"实例不可搬"的绝佳具象。 - 🔴 **`fs/user-fs.ts` 注释**:实例的家是**那台机的绝对路径**,直写 = **静默空操作**(2026-09-19 实测:托管清单被清空、目标文件一字节未动);实例入网凭据由**控制面代签**(本版无"实例→平台"身份通道)。 - **`orchestrator.ts`**:实例真实生命周期在 OS 层 **systemd scope**(`dsh--.scope`);配额 = `MemoryHigh` 软限 + `MemoryMax` 硬限,V8 堆按配额推导。 - **产出**:`交付物/平台新增功能总盘点与插件化归属-20260926.md` 新增 **§16**(16.1 两半对照表 / 16.2 Worker 可自选·Manager 不可 / 16.3 四个物理事实 / 16.4 接入六种切法落到角色 / 16.5 不确定项 3);§12.5 索引加一行;**硬约束三条 → 四条**(+**权威必须唯一**)。 - **同步**:入口 §0 加一行(新→旧序);本日志本节。 - **未做**:⛔ 未改 §13–§15 正文;⛔ 未 commit / push;⛔ 未登记下一棒。 - **锁**:`--claim-exec --domains` rc=0 → 改完已 `--release-exec`。 ### 🔴 平台线(插件投放与分库线)|09:2x 两份成品件:能做不能做总清单+拍板路径 / 节点与端互联互通与调用关系 - **用户两问**:①「先按照插件的规划把所有能做和不能做的都梳理清楚再看如何拍板」②「考虑所有节点和端的种类,加入插件后如何互联互通,数据,实例,三方插件是什么关系如何调用」。 - **产出 ①** `交付物/基础服务插件-能做不能做总清单与拍板路径-20260926.md`(决策件): - 四层总览:L0 官方宿主(⛔不是我们的)/**引导层(🔴 改一次要重发安装包 ⇒ 必须冻结到最薄)**/**插件(✅不用重发)**/服务器侧自有服务(✅不用)。 - **一条判据**:「这件事发生在**插件存在之前**还是**之后**?」+ 三条机制(先有鸡还是先有蛋/画面要先出来/**判决者唯一**)。 - ✅ 能做 = 「一个包、两个半」(服务端半 9 项 + 界面半 3 项,全部落官方已有槽位)。 - ❌ 不能做 = **5 件**:起实例/起窗口首屏/取身份/把包装进去/**原生网络守护**。⚠️ **§10 原记 4 件,按同一判据并入第 5 件(原在 §7),如实更正计数。** - ⚠️ 范畴不同 = 服务器侧自有服务(官方无账号体系 ⇒ 从第一天就不依赖 DSH);门户口径**不搬**。 - **拍板路径**:P1(产品主路径)→ P2(设备读包权限)/P3(节点提供哪些服务)→ 自动结论(包签名校验)。+ 本线已自决 4 项(引导层冻结/门户不搬/迁移不立项/签名校验归 P2 决定)+ 不在本线拍板 2 项(跨区可见性、多 Manager 治理 ⇒ 覆盖网络线)。 - **产出 ②** `交付物/节点与端-互联互通与调用关系-20260926.md`(关系与调用地图)。**取证读源码得关键事实**: - **全部节点/端 7 类**:Manager(relay 拨号方)/Worker(`dsh_hosts`,带 `via`+`network_id`)/中继/**服务器实例(`overlay_devices.kind='instance'` —— 实例在网里"是一台设备",拿的是**设备凭据**不是会话**)/桌面设备(`kind='desktop'`)/浏览器会话(`kind='browser'`)/桌面会话。🔴 `sessions.kind` 值域**只有** browser|desktop(`schema.ts` v13 原话)。⇒ **「节点」在代码里有三个所指**(注册表节点/Worker/Manager)。 - **两个网**(已定项):`ops` 运维网(平台自己的机器)/`u:` 用户网(该用户名下全部设备);逻辑名 `/`。 - **三个门**:入网(三级链 env>缓存目录>内置种子;**签名不对=失败关闭**;凭据**不含密钥本体**)→ 准入(`Map>` **默认拒绝**;**跨网在"能不能拨"就走不到 = 结构性隔离**)→ 通路(443 全可用,⛔ 不用开端口)。 - **设备凭据五道门**(`device-grant.ts` 唯一实现):停发闸门→用户配额→签 grant→注册表落盘→**台账→relay 密钥表**(⚠️ 密钥表写最后是**刻意**的)。 - **联通矩阵三条硬事实**:Worker **单向不反连**/**实例内无平台代码 ⇒ 唯二通路是 HTTP**/**用户之间结构性不通 ⇒ 跨用户协作只能走平台**。 - **数据三层**:平台权威库(只有 Manager 写)/插件库一插件一库(归属列平台**强制注入**)/实例 home(⛔ 直写=**静默空操作**,必须走 UserFs seam)。 - **插件四个面**:装配(引导层,共享只读包库+`link:`)/运行(**在用户实例内** ⇒ 所以能用用户自配模型凭据)/取数(HTTP `/api/im/data/`+**三层授权**:谁在调→以哪个插件→数据归属;契约失败一律 **200+`{ok:false}`**)/回调(SDK+扩展点)。 - **已知缺口 7 条**,其①**插件"自称身份"**(同一用户实例里 A 可自称 B,越不出用户边界但**属真实缺口**;补法=每插件令牌,本期逐次审计兜底)。 - 🔴 **本轮唯一新增拍板项 P4「跨用户要不要打通」**(现状结构性不通;倾向 A 案不打通;**排 P1 之后**)。拍板台账 = **P1/P2/P3/P4**,⛔ 顺序不可颠倒。 - **同步**:入口 §0 加一行(新→旧序);本日志本节。 - **未做**:⛔ 未改 §13–§16 正文;⛔ 未 commit / push;⛔ 未登记下一棒。 - **锁**:两个域(`基础服务插件-能力清单与拍板路径/全文`、`节点与端-互联互通与调用关系/全文`)合并持有,改完已 `--release-exec`。 ### 🔴 平台线(插件投放与分库线)|09:4x 五种角色选择场景 · 明确件 - **用户口径**:5 条角色场景(服务器→Manager/服务器→Worker→挂Manager/客户端→Manager(有IP和域名)/客户端→客户端→接Manager/服务器→Manager→多Manager网络·其一升根)+「库建立在 manager 节点上(后续多 manager 互联可考虑主从库机制)」。 - **产出**:新件 `交付物/五种角色选择场景-明确件-20260926.md`。 - 🔴🔴 **更正我此前一条错误结论(本轮最重要)**:我说「当 Manager 不能自选」—— **作用域搞错了**。它只对「去当**现有平台**的 Manager」成立;对「在**自己机器上起一个新的** Manager」**不成立**。**「判决者唯一」的作用域 = 「一个区之内」,不是「整个形态」** ⇒ 多控制面共存时每区各一个判决者,互不冲突。⇒「选 Manager」形态成立,但**性质 = 部署一个新控制面**,⛔ 不是"插件里的角色开关"。 - **五场景判定**:①⚠️可行(=部署新控制面) ②✅**最可行**(要系统级能力+入向可达+平台放行) ③⚠️**门槛最高**——用户提的"有 IP 和域名"**只是门槛之一**,解决不了**长期在线**与**系统级能力** ④✅**已实测** ⑤⚠️前半可行、**升根现在做不了**(卡点4条:撤根无机制/根≠更大的Manager而是签名授权方/多根=any-of-N/骨干签发权与跨区可见性未自决)。 - 🔴 **五条真正的共性**:**插件适合做「选择+申请+存配置+界面」,⛔ 不适合做「承载」**(插件跑在实例内、权限收窄;Manager/Worker 都要系统级能力)⇒ **引导层 5 件 → 6 件**(+起承载服务)。⚠️ 口径辨析:服务本体属"范畴不同",但"把它起起来"属引导层。 - 🔴 **用户第 6 点逐句明确**:①「库在 manager 上」✅ **与既有分层判据一致**(归属/租约/骨干资格只有控制面能写),现状已是事实 ②⚠️ **主从库 ≠ 解决"多个判决者"** —— 主从解决**同一个库**的高可用与读扩展;**判决者唯一靠「选主」**;**只上主从不上选主 = 两个 Manager 都能写 = 脑裂**;且**从库自动提升本质就是"单方面接管"**,与「无心跳判据时禁止单方面接管」顶牛 ⇒ **切换必须人工确认**。 - 🔴 **代码实测缺口(新发现)**:`DeployMode` = `local`(默认)/`k8s`(**多副本控制面+共用一个库**)/`cluster`(生产用);k8s 注释**声明**支持多副本共库;但 **选主「只有位、没有实现」** —— `podName` 注释写"用于选主",**全源码只有声明与赋值、零消费点**;**主从/只读副本零支持**(全仓无 replica/standby/只读字样)。⇒ **用户第 6 点命中此缺口:多控制面共存首先缺的不是 DB 复制,而是「谁是主」的判定。** - ⚠️ **口径提醒(诚实具名)**:记忆称「控制面联邦已定稿于 `架构设计/覆盖网络-顶层架构全貌.md`」—— **该文件在本工作区不存在,文档库 `架构设计/` 也为空**(已核查)⇒ 本件结论**不引用该定稿**,只基于代码实测+用户口径;**已请用户给路径复核**。 - 🔴 **新登记拍板项 P5「平台要不要做成用户可以自己搭一套」**(服务端形态,**与 P1 不同轴**;倾向 A 案但**分两步**:先做选主,「升根」挂起到撤根机制有结论)。台账 = **P1/P2/P3/P4/P5**。 - **同步**:决策件 §4 计数 5→**6** 件(含沿革)+ §6 加"不同轴"提醒 + §7 新增 P5;入口 §0 加一行;本日志本节。 - **未做**:⛔ 未 commit / push;⛔ 未登记下一棒。 - **锁**:`--claim-exec --domains` rc=0 → 改完已 `--release-exec`。 ## 🔴 更正(同日 09:5x · 自查发现,覆盖其上 09:4x 节的两条说法) **更正 1|定稿路径查找错误(我的错)** - 上一节写「定稿 `架构设计/覆盖网络-顶层架构全貌.md` **在本工作区不存在**、文档库 `架构设计/` 为空 ⇒ 本件不引用该定稿」—— **🔴 全错**。 - 事实:定稿**存在且是唯一权威** ⇒ **`dsh-server-docs/02-架构设计/覆盖网络-顶层架构全貌.md`**(定稿 2026-09-21,263 行;自述"与过程档案冲突以本文为准")。 - **错因**:只 `ls 架构设计/`,**漏了带序号前缀的 `02-架构设计/`**。⚠️ **教训(要记住)**:文档库目录**一律带两位序号前缀**(`01-规范/ 02-架构设计/ 04-调整方案/ 05-交接单/ 07-scripts/ 08-skills/`)⇒ **按名字找目录会全军覆没**;查目录**先 `ls` 根**,别凭记忆拼路径。 - ⚠️ 反证:**别的件早就引对了** —— `接续包_IM与插件接入_20260924.md:26`、`交付物/跨版本管理与包更新机制-现状与方案-20260926.md:79`、`交付物/跨机可见性状态回流-设计件-20260923.md:218` 用的都是 `02-架构设计/…`。⇒ **不是文档错,是我检索错。** **更正 2|「跨区可见性默认未自决」过时(已被推翻)** - 上一节写「两项未自决(骨干签发权 / **跨区可见性默认**,倾向互不可见)」—— 🔴 **已作废**。 - 事实:**定稿 §8.3 已定(用户 2026-09-24 拍板)**=**可见性跟随「用户 × 区」成员关系**;原文「倾向默认互不可见」及 148 §7.2 的 A / B 两案**全部作废**。同源证据:`接续包_IM与插件接入_20260924.md:26`(那棒就是落这条的)。 - ⇒ 现存**唯一**未自决项 = **骨干资格签发权(G5 / 定稿 §8.1,F4 之前必须定,定稿倾向 B 区签+根背书)**。另有 **定稿 §8.2 根清单几把、谁保管**(凭据类,须用户定)。 **更正 3|「多控制面共存可先做」要退回** - 定稿 §七 推进顺序明确 **F1.5(签名清单序号,治 G1 重放 + G6 撤根)=一切前置**;F8(根集合治理+升格流程)**依赖 F1.5**。 - ⇒ 「多 Manager 共存」做的正是**下发签名清单** ⇒ **必须排在 F1.5 之后**,不能"先做"。 **同步动作(已做)** - 本日日志:本节(追加式更正,⛔ 未改上一节原文 —— 日志 append-only)。 - `接续入口_插件投放与分库线_20260922.md` §0:错行**已就地替换**为更正块(含"错因=漏序号前缀")。 - `交付物/五种角色选择场景-明确件-20260926.md`:**整篇重写** —— 改为"把用户 6 条**对到定稿的哪一条**"(§0 两处更正 / §1 五场景对到 §5.3 四条入口路径 A–D / §3 第 6 点、含定稿「⛔ 区域 PG 互联或双向复制」/ §6 拍板台账指向定稿 §8)。 - 🔴 `~/.workbuddy` 之外的 **`.workbuddy/memory/MEMORY.md` 行 14 就地修正**(三处):① 定稿路径补 `02-` 前缀 ② 「G1=一切前置」→「**F1.5(G1 序号+G6 撤根)=一切前置**」 ③ 「跨区可见性默认(倾向互不可见)」→「**跨区可见性 ✅已定(09-24)=跟随「用户×区」成员关系**」。⚠️ **净减字符**(遵守"注入上限 ⇒ 只减不增")。 - ⚠️ **新的可复用教训**(建议入规则):**文档库目录检索必须容序号前缀**;"我说某文件不存在"这句话,**发布前必须先 `ls` 过根目录**。 **本轮结论未受影响的部分**:① 五场景判定(对到定稿 A/B/C/D 后**结论更强**)② 「插件适合做选择/申请/配置/界面,⛔ 不做承载」+引导层 6 件 ③ 主从库 ≠ 解决判决者唯一 ④ `podName` 零消费点(选主只有位没实现)⑤ 新登记 P5。 ## 🔵 平台线(插件投放与分库线)|11:0x 全功能承载判定 —— 「底层插件能否承整个项目」 **动因**:用户问「按照这个架构开发底层插件能否承整个项目从开始到现在的功能结构」并点名七项(多租户含**实例恢复** / 共享模型 / 私有模型含 **agent cli 通过客户端接入** / 三方技能 / **插件使用管理(重点:更新 → 告知用户手动或设置自动处理)** / 覆盖网络 / IM 对话)。⇒ 出一份**逐项判定件**(换切面:从"插件单元"换成"特性 × 承载能力"两维)。 **产出**:新件 `$WS/交付物/全功能承载判定-底层插件能否承-20260926.md`(§0 一句话判定 + 一条判据 / §1 七项主表 / §2.1 插件更新四拆 / §2.2–§2.6 展开 / §3 三条物理约束 / §4 缺口汇总 / §5 拍板台账 / §6 出处)。 **判定要点** - 🔴 **一句话**:**能承「使用面」(≈100%),不能承「承载面」(引导层 6 件 = 0%)**。⇒ 成立形态 = **薄壳(官方 DSH)+ 引导层 + 一个能力包**;**插件承的是"项目形态",不是"项目本身"**。 - 🔴 **用户点名重点(更新)必须拆四条**:① 看见"有新版本" ✅ ② 点"更新"按钮 ✅(**只到"请求"为止**)③ **把新包搬进来 ❌ 引导层**(拉取器须在实例没起时也能跑)④ **"设置自动处理" ❌ 引导层** —— **自动化执行者必须活在被自动化对象之外**;插件在实例内 ⇒ **实例没起=插件不存在=自动化不成立**。⇒ **「告知」和「按钮」可以全给插件;「拉包」和「自动」必须在插件外面。** - 🔴 **新具名缺口**:**agent cli 接入面零实现**(全仓 `agentCli|agent-cli|agent_cli` = **0 命中**)+ 本版**无"实例 → 平台"身份通道**(`user-fs.ts` 自述,唯一 `.overlay-device.json` 由控制面代签)⇒ 须新设计,⛔ 不是"写个插件"能补。 - 🔴🔴 **一处更正(实测推翻 09-25 旧结论)**:09-25 审计的「扩展点注册/入向回调/出向投递**三者无跨进程通路**」**已被 09-26 反向通道补上** —— `im.ts:333`(登记/注销/出向)+ `im.ts:475`(**反向通道 平台→插件**:`bridge/pull` 长轮询 + `bridge/result` 回投),身份=实例令牌+包名头、fail-closed 401。⇒ **IM 是插件接入四面(装配/运行/取数/回调)唯一齐全的一条**。残留:① 不校验"插件是否真在该实例启用"(补法=**每插件令牌**)② **一条实例只该有一条常驻长轮询**(关系**连接额度**)。 - **另两条**:共享模型 `shared_model_granted`(默认 0)= **管理员授权** vs `shared_model_enabled` = **用户偏好**,**两列两个主体、⛔ 不共用写入口**;私有模型凭据**只有实例内可用** ⇒ **凡要用模型的能力,插件必须在实例内跑**。 - **三条物理约束**:插件跑在实例内(实例没起=插件不存在)|实例的家=**那台机的绝对路径**(隔网改=**静默空操作**)|系统级能力(uid/cgroup/防火墙/端口/systemd scope)不在插件权限内 ⇒ **插件是"应用",引导层是"操作系统"**。 - **拍板台账不变**:**P1** → **P2**(设备读包权限,**仍未答**)/ **P3** → **P4** → **P5**;⛔ 顺序不可颠倒。 **同步**:入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 ## 🔵 平台线(插件投放与分库线)|11:2x 追问「为什么不行 · 承载面六件逐件分析」 **动因**:用户不接受 §0 的"承载面 0%"当结论,要求**逐件给理由**("我们一个个来分析")。⇒ 在 `$WS/交付物/全功能承载判定-底层插件能否承-20260926.md` 把 §3 由一张三行约束表扩成 **§3.0 总纲 + §3.0.1 三条约束 + §3.1–§3.6 逐件 + §3.7 汇总表**(⛔ 不改 §0–§2、§4–§6 正文与编号)。 **本轮最值钱的一条(新结论,非复述)** - 🔴 **统一根因**:六件**不是六条难题,是同一个循环依赖的六个切面** —— **插件是"实例内的东西",而这六件全是"让实例/平台/身份/网络存在起来"的事** ⇒ 用"存在的东西"制造"它自己的存在条件" = **循环依赖** ⇒ 必须有**环外、先于插件**的存在(引导层)。**不是"不想做",是"先有鸡还是先有蛋"。** - 🔴 **三条可复用判据**(此后问"这件事能不能插件化"直接套):① **时序**——要发生在插件被装进去**之前**吗?② **身份**——需要**比插件更高的执行身份**(root/setuid/内核网络栈)吗?③ **存活**——要在**插件不存在时**照常发生吗(更新/守护/自动处理)?任一命中 ⇒ 引导层。 - ⭐ **分水岭句式**:**插件做的是「决定要什么+说出来」;引导层做的是「把那件事真的实现」。** **逐件结论(均带代码锚点,非推演)** - **①起实例**:前提四条全 root/setuid 级 —— 算 uid(`hashUid`)+ `setpriv --reuid/--regid/--clear-groups`(`orchestrator.ts:1221`)+ `systemd-run --scope -p MemoryHigh/MemoryMax`(`:1242-1249`,scope 名 uid 与 `--reuid` 交叉验证 `:259-274`)+ 隔网铺 profile。**插件自己就活在这个 scope 里**(`:1030`)⇒ 要它建另一个 scope = 要它站到自己容器外面。 - **②起窗口首屏**:⭐ **本条"插件那一半"最大**(client bundle 画首屏**全部内容**)⇒ **是架构分工不是能力问题**:"别把开窗和画窗混成一件事"。 - **③取身份**:`device-grant.ts` **五道门**(relay 密钥表写最后是刻意的)+首启三级链(**签名不对=失败关闭**)⇒ 插件是被装配进来的(又一层循环)/不是签发者/本版**无「实例→平台」身份通道**(`.overlay-device.json` 由控制面代签)。 - **④把包装进去**:要**实例没起也能跑** + 写 **root:root 755** 共享只读层 + 重试留痕 ⇒ 插件关着实例就不存在、写不进 root 属主层、装配要改实例 home。 - **⑤原生网络守护**:CAP_NET_ADMIN/root。旁证两条 —— 平台自己的端口守卫就是 **root-only iptables**(`firewall.ts` 头原文"Linux + root only … fail loud");实测**插件被 nft 拒绝连 `127.0.0.0/8`(连自己 spawn 的 gateway 都连不上)最后改走 unix socket**(`orchestrator.ts:863`)⇒ **插件连本机回环都不自由**。 - **⑥起承载服务**:平台进程本体 + 判决者唯一(`lease.ts` 三不变量 + `schema.ts` v7 CAS/epoch)⇒ **Manager 是引导层本体**(自己把自己装进自己里)。 **⭐ 消解用户真实顾虑(§3.7)**:**六件"不行"≠"用户要多装两样东西"** —— 引导层可做成**同一安装包的另一半**,用户**只装一次**,装出来就是"壳+引导层+插件" ⇒ **"分两层"是内部实现分工,不是用户负担**;要拍板的仍是 **P1**。 **⚠️ 自我约束(新立的写法,值得沿用)**:六条"不行"**强度不同** —— **形式上就不可能**(§3.1 §3.6:换形态也一样)/**当代 Linux 上不可能**(§3.3 §3.4 §3.5:需要 root 级动作)/**架构分工而非能力问题**(§3.2)。后五条在"插件被装进**本机特权宿主**"的形态下会变 —— 但那时它**已不是插件**。⇒ 准确表述 = **「在我们这条「插件跑在实例内」的形态下不行」**,⛔ **不说成"永远不行"**(这条自我约束是用户逼出来的,比结论本身更该固化成习惯)。 **同步**:入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 ## 🔵 平台线(插件投放与分库线)|11:3x 单点追问「插件代码不能起实例吗?」 **动因**:用户对承载面第①件单独追问(不满足于"不行"三个字)。⇒ 判定件 §3.1 增小节「**四条路逐个否掉**」(⛔ 不改其余章节)。 **四条路(本轮结论)** - **A 插件自己创建 systemd scope** ❌ —— **四重门**(均实测): 1. `orchestrator.ts:1219` `--unshare-pid` ⇒ 实例在**独立 PID 命名空间** ⇒ **插件连宿主上的 systemd 都看不见** 2. 启动链 `systemd-run` → `bwrap` → `setpriv --reuid --regid --clear-groups`(`:1221`)⇒ **一出生就被降权** 3. 以别人 uid 起进程要 `CAP_SETUID`=**root 专属** ⇒ 连"别的用户"都够不到 4. 沙箱只 `--bind root root`(`:1179` =**它自己**的家);别人 home 是 `0700`+uid 边界 - **B 插件自己 `spawn` node 进程** ⚠️ **能起"进程",起不出"实例"** —— 平台语义的「实例」= **六个必要条件同时在册**:独立 uid / 独立 cgroup scope(`:1241-1248` 含 `CPUQuota`/`TasksMax`)/ **专属端口**(`findFreePortInRange` `spawn.ts:79-92`;**自选端口会撞号 ⇒ 打到别人的实例**,2026-09-16 实测)/ 自己的 profile 树 / 在册(DB 行 + 租约,**只有 Manager 能写**)/ 可被访问(代理路由)。**插件零样满足** ⇒ 起出来是**孤儿进程**:同沙箱、同 scope 抢同一份 `MemoryMax`(**OOM 连坐宿主实例**)、无端口、不在册、谁也进不去。⇒ **能起进程 ≠ 能起实例。** - **C 调平台 HTTP 让它起** ⚠️ **形态成立、今天没通路** —— 🔴 **本轮新发现(此前我漏了)**:`/api/dsh/launch` 守卫是 `requireAuth`(`dsh.ts:130`)= **浏览器 session cookie**;插件手里是 `x-dsh-im-instance-token`(`im/instance-token.ts`,本就是为"实例内无浏览器的 node 进程"补的通道)但**只服务 IM(`imAuth`)+跨区本期拒绝+fail-closed** ⇒ **两套凭据不通用** ⇒ **今天连"请求"这一半也走不通**。⭐ **这是本项唯一真能补的口子**,补法=给实例令牌一条**受限面**(只许操作**自己**那个实例的 launch/restart/stop,⛔ 不给通用控制面)。 - **D 改自己 profile 让 dsh 自重启** ❌ —— 只改变"下一个实例长什么样",不产生"下一个实例"。 **两条压轴(比技术细节更重要,值得长期记住)** - 🔴 **自举(bootstrapping)**:**平台上第一个实例是谁起的?** 那时平台没跑(无 HTTP 可请求)、一个实例也没有(更无插件)⇒ **「起实例」必须存在于插件之外**,否则整个系统第一次起不来。(与"init 不是被 app 起的"同理) - 🔴 **不是"做不到",是「互相排斥」**:「插件能起实例」要求**跨 uid 边界**的权力;「实例是多租户隔离的」**正是靠 uid 边界** ⇒ **插件能起实例的那一刻,恰好是它不再是"多租户实例"的那一刻**(退化成"你本机的一个进程":无隔离/配额/在册/租约)。⇒ **做了等于拆掉多租户**(击穿 R5「权限只收窄」)。**这个句式("不是做不到,是互相排斥")可以复用。** **精确边界**:插件能表达「我要起/重启/停止**我自己的**实例」并由平台执行(今天连这条都要先补凭据面);**"起"这个动作归引导层**。 **同步**:入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 ## 🔵 平台线(插件投放与分库线)|11:4x 用户提方案「第一个实例 = DSH 主体 → 加载基础插件 → 成为服务节点 → 多租户」 **用户原话**:「第一个实例是 dsh 主体的,加载这个基础插件后 选择成为服务节点,设置管理员账号,配置数据库后,提供多租户能力,本质是在服务上开多个相互隔离的dsh实例服务,这个看起来是成立的」 **判定:成立 —— 七条里六条成立,唯一要改一个词。分歧不在架构,在权限。** ⇒ 判定件新增 **§3.8 评估用户方案**(⛔ 未改其余章节)。 **逐条** | 说法 | 判定 | |---|---| | ① 第一个实例 = DSH 主体 | ✅ **成立,而且这就是"自举"的答案** —— 第一个**不由任何人"起"**,由管理员/安装脚本直接启动 ⇒ 与 §3.1 压轴**不矛盾,正是它的解** | | ② 加载基础插件 | ⚠️ 成立但**名字要改** | | ③ 选择成为服务节点 | ✅(是**启用/初始化**,不是创建) | | ④ 设置管理员账号 | ✅ **但今天正是缺口** = 定稿 **§5.3 路径 A**,原文「⚠️ **今天只有 CLI,无 Web 通路**」 | | ⑤ 配置数据库 | ✅ | | ⑥ 提供多租户能力 | ✅ **但这是现状,不是新方案** | | ⑦ 本质 = 开多个相互隔离的 dsh 实例 | ✅ **准确**(隔离四层:uid + cgroup + bwrap + 端口区间) | **🔴 唯一要改的词:官方 DSH 的插件机制里没有"以 root 运行"这个位置** 插件是 profile 层**依赖**(`plugin-assembly.ts` 写 `link:`);**加载它的宿主进程本身就被降权**(`setpriv --reuid …` `orchestrator.ts:1221` + bwrap `:1219`)⇒ **宿主不是 root ⇒ 宿主里的插件永远不可能是 root** ⇒ 「用插件做多租户」**在权限上从一开始就不成立**(与我们怎么写代码无关)。 ⇒ 两个位置必须分开命名(**这条以后要反复用**): | | **业务插件** | **引导层 / 宿主扩展** | |---|---|---| | 谁安装 | 平台装配 | **管理员本人**(安装包/setup) | | 跑在哪 | 用户实例进程内 | 机器上,root / 带能力 | | 权限 | **只收窄**(沙箱内) | 系统级 | | 今天叫什么 | mcn-suite / IM 插件 | **`dshs` / `dshs-worker`** | | 能开隔离实例 | ❌ | ✅ | **🔴 载荷量要说清(这是本轮最容易被忽略的一条)** "多租户能力"**不是插件顺带的功能,它就是整个平台本体** —— `supervisor/`(12) + `web/routes/`(15) + `db/`(10) + `net/relay/` + `im/`(12) + `fs/`。⇒ 把"多租户能力装进第一个实例"= **把整个平台装进一个实例**,⛔ 不是"加一个小插件"。 ⇒ 现状 47 上 `dshs`(Manager, 127.0.0.1:3080) + `dshs-worker`(19100) + 实例独立 systemd scope ⇒ **"服务节点+多租户"今天就有、形状与用户描述一致**;**现状与用户方案的真正差别 = 今天 Manager 是我们自己的程序,不是 DSH 实例**。 **🔴 "寄居"的收益 vs 四条代价(如实标,⛔ 不藏)** - 收益:只装一次 / 共用依赖版本 / "第一个实例"天然是宿主 - ① **可靠性降级** —— 引导层与 DSH **同生共死** ⇒ DSH 崩 = 所有实例没人管;今天 `dshs` 与实例**双向都不牵连** ⇒ **净变差,命中 R11** - ② **升级耦合** —— 升 DSH 牵动引导层,而 **R1 明令不自动升级 dsh** - ③ **权限要么混、要么分** —— DSH 提权会**推翻现有降权+沙箱设计** - ④ **"实例没起也要活"的几件塞不进去** —— 拉包 / 网络守护 / 自动更新在官方 DSH 的进程模型里**没有位置** ⇒ **建议(可推翻)= 同一个安装包,两个进程**:安装脚本一次装好「官方 DSH(薄壳/界面)+ 引导层(以服务形式、root 起)」⇒ **用户视角只装一次,架构上权限分层不混**。**这把 §3.7「引导层=同一安装包的另一半」从"我们的说法"变成了"可实现的安装形态"(用户这个提法的真实价值就在这)。** **🔴 退化形态(免得以后争论)**:单机 / 单用户 / 全同 uid(放弃 uid 隔离)⇒ "开多实例"退化成"多开同 uid 的 DSH 进程",**插件确实起得动**(同 uid 可写自己目录、可 spawn、可挑端口)—— **但同 uid = 没有租户边界**,一个租户的会话能读另一个租户的文件 ⇒ **"能起实例"与"多租户隔离"在同一个进程身份下不能同时成立**(=§3.1「互相排斥」的具体形态)。 **⚠️ 一处如实交代(我自己的疏漏)**:上一轮最终回复里我说"新增 1 条缺口:实例令牌 → 自身实例的受限控制面",但**当时没落进 §4 缺口表**。本轮已补上该行(⛔ 不再有"口头说了没落文"的情况)。 **同步**:§4 缺口表补 1 行;入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 ## 🔵 平台线(插件投放与分库线)|11:5x 用户读法「把 DSH 实例当服务器的系统 ⇒ DSH 里嵌多租户 DSH 平台」 **用户原话**:「把现在的dsh实例当作是服务器的系统,多租户包括管理员的AI1net实例是插件创建的这样看呢,相当于dsh系统里面嵌套的 多租户dsh平台」 **判定:**「嵌套」不成立;但你要的效果(管理员也有一个自己的 AI1net 实例)**今天就是「并列」,不需要嵌套**。⇒ 判定件新增 **§3.9**(⛔ 未改其余)。 **最要紧一句(新立的判据式说法)** 🔴 **「把 DSH 当系统看」≠「它就有系统的权限」。** 系统与应用的差别**不是名字,是特权层** —— 改名不会让 DSH 长出内核,也看不见 systemd(§3.1 四重门原样复现)。 **嵌套为什么不成立(三条)** ① "当系统看"**不产生特权**(外层若只是普通实例,它自己就在 bwrap 里、被 `setpriv` 降权、看不见 systemd) ② 要做成"系统"必须**已经是特权层** ⇒ 落回 §3.8 结论:**那是引导层,不是插件** ③ 🔴 **以 root 跑 DSH + 加载插件 = 把安全模型倒过来** —— 内层靠 uid/bwrap 隔离,外层却是 **root** 且加载**可替换的第三方插件** ⇒ 任一个越权插件就能开任意 uid 实例、读任意租户的家 ⇒ 多租户隔离退化成"自愿的",与现有降权+沙箱设计**直接冲突** **三笔实测代价(用本仓现成数字,⛔ 不估算)** ① **先白吃一份基座** —— `orchestrator.ts:1229` 原文实测:dsh 自身私有内存**稳态 106~117 MiB、峰值约 145 MiB**(=**基座成本**,非用户行为);宿主实机**只有 1870 MiB**(`:1233` 原文)⇒ 嵌套=外层先扣一份(且外层要跑平台逻辑,占用**必然显著大于 145 MiB**)⇒ **承载能力净下降(命中 R11)** ② **造出两个真相** —— 外层 DSH 自己也有"会话/实例/配置"概念 ⇒ **外层自认宿主 + 内层平台也自认宿主** ⇒ uid/端口/会话/账本**四套叠加**;而核心不变量是**判决者唯一**(`lease.ts` 三不变量 + `schema.ts` v7 CAS/epoch;双写 ⇒ 脑裂 ⇒ 会话日志 append 冲突=数据损坏)⇒ **必须二选一** ③ **分配权打架** —— `findFreePortInRange` 的前提是每 worker 一段**不重叠区间**;嵌套后外层也占端口 ⇒ 区间必须由**唯一一方**划(现状是 Manager) **🔴 你要的效果:不需要嵌套,只需「并列+角色」,而今天已经是这样(实测三条)** - `dsh_instances` 按 **`user_id`** 建(`repo.ts:617` INSERT / `:646` `findUserInstance(userId, role)` / `:915` 集群态同键) - **没有任何"管理员不给实例"的排除逻辑** —— `grep -rn "role === 'admin'" src/supervisor/ src/web/routes/dsh.ts` = **零命中** - 管理员的身份**只体现在平台侧数据与授权判断里**:`requireAdmin` 就是一句 **DB 角色判断**(`middleware/authn.ts:31-38`,`request.user.role !== 'admin'` ⇒ 403) ⇒ **管理员与普通租户跑的是同一个东西,特权不在进程权限里** —— 且**必须如此**(否则管理员实例一旦被攻破=**全网**)。 ⇒ **精确表述 = 不是"DSH 里嵌 DSH",是"引导层下面并列着 N 个 DSH 实例,其中第一个属于管理员"。** **🔴 "嵌套"唯一能成立的位置,隔离强度会掉到零层** =在**同一个 DSH 里**做多会话/多工作区(DSH 自己的权限模型)。而 DSH 的隔离粒度是**会话**不是**租户**(`sessions` 表按 `user_id`+`workspaces`;`sessions.kind` 值域只有 `browser|desktop`)⇒ **没有 uid、没有 cgroup、没有 bwrap,三层全没**。 🔴 **而且这是"往回退"—— 决定性证据**(`orchestrator.ts:823-827` 原文):「本机内核 5.10 无 Landlock、bwrap 后端在平台容器内探测失败 → 内置默认 `workspace-write` 会 **fail-closed 拒绝执行任何 shell(P0)**。改 `danger-full-access` =**不在实例内再叠加沙箱**,**隔离交由平台容器(bwrap + uid + cgroup)负责**」,对应代码 `DSH_PERMISSION_MODE: process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'` ⇒ **平台已主动关掉 DSH 自带的沙箱、把隔离搬到外面** ⇒ "在 DSH 里做租户隔离"=退到平台**主动放弃**的那一层。 **收口**:现状与你要的形态**其实已经是**「引导层 → N 个并列 DSH 实例(首个属管理员)」;**差别只在引导层今天是我们自己的程序(`dshs`),不是官方 DSH**。⇒ 待拍板仍是 **P1**。 **同步**:入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 ## 🔵 平台线(插件投放与分库线)|12:0x 追问「用 K8S 做多租户实例能否解决这个问题」 **判定:能解决最难自研的那半;但「插件不能起实例」在 K8S 下依然成立。** ⇒ 判定件新增 **§3.10**,并**新登记待拍板 P6**。 🔴 **一句话**:**K8S 把门槛从「你是不是 root」换成「你有没有 RBAC 授权」—— 门槛没了,门还在。** 它把 §3.1 的四重门**压成一道门**("有没有被授权的身份"),**那一门仍在插件之外**。 **🔴 先摆一个事实(本轮最有用的发现):这条路代码里已经被承认了,只差实现** | 层面 | 状态 | 证据 | |---|---|---| | 设计意图 | ✅ **写明** | `config.ts:18-19`:**「`k8s` = multi-replica control plane spawning per-user DSH Pods via the K8s API」** | | 配置面 | ✅ 已预留 | `k8sNamespace`/`dshImage`/`controlPlaneImage`/`imagePullSecret`/`egressCidrs`/`k8sServiceAccount`/`podName`(env `POD_NAME`) | | **权限载体** | ✅ **已换** | `config.ts:125-126`:「`k8sServiceAccount` = **ServiceAccount the orchestrator runs as (k8s mode only)**」⇒ **root ⇒ SA** | | DB | ✅ 已切 | `db/adapter.ts`:local⇒SQLite;k8s⇒PG(`DSHS_DB_URL`) | | **实现** | 🔴 **零** | `grep "kubernetes\|@k8s\|coreV1\|appsV1\|createNamespacedPod" src/` **0 命中**;`package.json` 无 k8s 依赖;**`podName` 零消费点**;`proxy.ts:675/745` 的 `deployMode==='k8s'` 只是传给 `proxyHttp` 的**一个布尔** | ⇒ **问题不是"能不能上",是"现在值不值得把这条线做出来"**(控制面选主 + 执行体从 systemd 换 Pod + 状态回流 + 设备侧通道)。 **✅ K8S 真解决的四件**:**①起实例**(执行体换「K8S API+Pod」⇒ 不再要 root、只要带 RBAC 的 SA;配额从手写 cgroup 变 `ResourceQuota`/`LimitRange`;隔离从 uid+bwrap 变 **namespace+PodSecurityContext+NetworkPolicy**)/**④把包装进去**(`initContainer`/`DaemonSet`/`CronJob` ⇒ **"实例没起也要跑"第一次有标准载体**)/**⑤原生网络守护**(CNI+`NetworkPolicy`+`egressCidrs` 接手**集群内**那半)/**⑥起承载服务**(`Deployment` 多副本 + **原生 `Lease` 选主** ⇒ `podName` 那个"只有位没实现"的选主**有现成载体**)。 **❌ K8S 不解决的四件(关键)** ① **"谁调 K8S API"不变** —— 插件跑在租户实例内 ⇒ 仍**没有 kubeconfig/SA token/RBAC** ⇒ 一样起不了实例 ⇒ **自举问题一模一样**(第一个被授权的身份仍要管理员/安装脚本 `apply` 一份 RBAC)⇒ **§3.9 的自举、§3.1 的四重门,K8S 一条也没绕开,只是换了门牌** ② **"插件跑在实例内"不变**(Pod 内依旧是普通用户进程) ③ **"两个真相"只换对手** —— K8S 管 Pod 但**不管租户语义**(`dsh_instances`/租约/归属/账本还是你的)⇒ 变「**平台库 vs K8S 状态**」两套源,且 Pod 会被**驱逐/重调度/抢占** ⇒ 唯一干净解法 = **租户语义交给 K8S(一租户一 namespace)+ 平台只做声明式 controller** ④ **设备侧完全不解决**(笔记本/浏览器**不是 Pod**,永远在集群外;覆盖网络中继/打洞与 CNI 是**两套东西**) **⚠️ 必须先摆上桌的代价**:宿主 **2 核 / 1870 MiB**(`orchestrator.ts:1233`)+ 单实例基座 **106~145 MiB**(`:1229`)⇒ **本机跑 K8S 再跑实例=净变差(命中 R11)**;按 09-22「**机器可换**」口径**不构成否决,只构成前提:要加机器(或用托管 K8S)**。 **⚠️ 隔离强度要分两栏(⛔ 别笼统说"更强")**:**管理面** ✅ K8S 更强(标准、可审计、RBAC 细到动词)/**跨机** ✅ 有(**调度即迁移**,顺手解掉「跨节点迁移未设计」)/**数据面** ⚠️ K8S **默认共享内核**,Pod 间不是内核级隔离,强隔离要上 **gVisor/Kata/独立节点池**。⇒ 净判定:**管理面+跨机净变好;数据面取决于上不上强隔离运行时**。 **⭐ 本轮最有价值的一条:K8S 的声明式模型与我们说的"分水岭"是同一个分形** 分水岭 = 「插件做的是**决定要什么+说出来**;引导层做的是**把它真的实现**」;K8S = 「**写 CR(期望态)** vs **controller reconcile(执行)**」⇒ **K8S 不解决"插件不能起实例",但把"插件能做什么"抬高了**:从"调私有 API 请求"变成"**能写标准对象表达期望态**"。 **落法(可推翻):平台 = 一个 Operator + 一个 `DshInstance` CRD** —— Operator watch CRD ⇒ 建 Pod/Service/PVC/NetworkPolicy ⇒ 回报状态;**原生 `Lease` 选主 ⇒ "判决者唯一"有天然实现**;**一租户一 namespace**;**设备侧不进集群、自研 relay 保留**。⚠️ 写 CR 也要 RBAC ⇒ 这正是那条新缺口(实例令牌→只操作自己实例)**在 K8S 下的对应物**(一个只允许 create/patch 自己那个 `DshInstance` 的 Role)⇒ **同一问题的两种实现,本质一致**。 **🔴 新登记 P6「实例承载走不走容器编排(K8S)」**(**与 P1 正交**,可并行评估) **A 案走 K8S**(优点:起实例不再要 root 只要 RBAC/配额·隔离·调度·守护全是标准件/**可跨机调度**/选主有现成 `Lease`;缺点:**要加机器**/数据面默认共享内核/**新增两套状态源**/实现为零) **B 案不走、维持自研单机**(优点:**一台 2 核机能跑**/单机多租户隔离够用/零新增基础设施;缺点:**不能跨机迁移**/选主只有位/守护与自动更新仍要自研) **我的倾向:先 B 后 A** —— 现在按 B 把功能跑通,K8S **登记为"承载演进方向"**,等真出现「必须加机器」或「必须跨机调度」再切;⛔ 不建议现在启动。 **收口一句**:**K8S 能替我们做「操作系统那一半」,替不了「授权那一半」。引导层不会因此消失,只会变薄、变标准** —— 从「本机 root 服务」变成「**集群内被 RBAC 授权的 Operator**」。 **同步**:§5 新增 P6;入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 ## 🔵 平台线(插件投放与分库线)|12:1x 追问「插件不能访问网络吗?为什么调不了 K8S API?」 **判定:网络这一层通常通;挡住的不是网,是身份。** ⇒ 判定件新增 **§3.11**(一处措辞更正 + 一条重要细化),并在 §3.1 与 §3.10.3 各加指向更正。 🔴 **一处措辞更正(用户这一问是对的)**:我在 §3.10.3 写的「没有 kubeconfig / SA token / RBAC」**指的是"没有身份",容易被读成"连不上"**。 **网络分三层(实测)** | 层 | 状态 | 依据 | |---|---|---| | **出网(公网)** | ✅ **通** | bwrap **全仓唯一一处 `--unshare` = `--unshare-pid`**(`orchestrator.ts:1219`),**没有 `--unshare-net`** ⇒ **网络命名空间共享**;egress 默认 `0.0.0.0/0` | | **本机回环 `127.0.0.0/8`** | ⚠️ 受限 | 实测**插件被 nft 拒绝连 `127.0.0.0/8`(连它自己 spawn 的 gateway 都连不上)⇒ 改用 unix socket**(`:863`) | | **私网段**(K8S API 常在此) | ⚠️ 按配置语义被排除 | `egressCidrs` 注释(`config.ts:122-124`):「empty = keep the current `0.0.0.0/0`(**except private ranges**)」⇒ **收敛方向恰好会挡住私网里的 API server**(⚠️ 配置语义,本轮未实测生效范围) | **🔴 真正挡住的:认证 + 授权两道** - **认证**(你是谁):要 client cert/**SA token**/OIDC 之一 —— 而**现状连"载体"都没有**:**SA token 是挂给 Pod 的**(`/var/run/secrets/kubernetes.io/serviceaccount/token`),**我们的实例是 systemd scope 不是 Pod** ⇒ **没地方挂** - **授权**(你能做什么):要 Role/ClusterRole + RoleBinding —— 而 **K8S 的 SA 默认零权限** ⇒ 认证过了也 `403` ⇒ **必须由别人来绑** ⇒ **现象是 `401/403`,不是"连不上"** ⇒ **K8S 的门是身份门,不是网络门。** **⭐ 重要细化(本轮最有价值):给了钥匙,插件真的能起实例** 实例以 **Pod** 形态跑 + 挂 **SA** + 绑一个**只允许在自己 namespace 里 create/patch 自己那个 `DshInstance` 的 Role** ⇒ **插件是真的在"调用 K8S API 起实例",中间没有我们的私有 API 代理**。 ⇒ **§3.1「插件不能起实例」在 K8S 下不再无条件成立。** 准确说法改为: > **插件不能「自己获得权限」;但只要引导层在安装时给它一把限定范围的钥匙,它就能在范围内自主操作。** > ⇒ **分水岭从「谁执行」细化为「谁定义权限范围」。** **⚠️ 但不推翻旧结论,只是把话说准**:**「说出来 vs 实现」仍成立**(插件写期望态 CR/Pod,真正跑起来的是 api-server → scheduler → kubelet)|**自举仍成立**(钥匙必须外部先给:管理员/安装脚本 `apply` SA+Role+RoleBinding)⇒ **门在,钥匙不在**;「插件不能起实例」的**准确版本 = 插件不能自己拿到那把钥匙**。 **现状下没有这把钥匙的三条原因(都不是"网络")**:① 实例是 systemd scope ⇒ **没有 token 载体** ② 现有的 `x-dsh-im-instance-token` 是**平台自研内存随机串、与 K8S 无关、只服务 IM** ⇒ 跨不到任何控制面 ③ 就算挂上,**默认权限也是零**。⇒ **缺的是钥匙,不是网线。** **⭐ 对产品方向的正面意义**:**"插件应该能自己起实例"这个直觉在 K8S 下是对的** —— 而它落地成的正是那条缺口「**实例令牌 → 只操作自己实例的受限控制面**」的 **K8S 版 = 限定 namespace 的 SA + Role**。⇒ **已据此给 P6 的 A 案加一条优点**:让"插件自主"**不必自研受限令牌**,用**标准件**即可。**P6 本身未变,只是理由更足。** **另立一条(值得沿用的写法)**:§3.1 尾部加「**适用域收窄**」—— 那六条"不行"是**「在我们这条『插件跑在实例内、无集群身份』的形态下」**,⛔ **不是"永远不行"**(与 §3.7 那条自我约束同源)。 **同步**:§3.1 / §3.10.3 各加指向更正;§5 P6 的 A 案加优点;入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 ## 🔵 平台线(插件投放与分库线)|12:2x 追问「我在插件中做个功能不能调用 K8S 起容器运行实例吗」 **用户情绪**:被我上一轮的措辞绕住了(我把"插件不能自己造出身份"与"插件不能调 K8S API"混在同一段里说了)。⇒ 本轮**拆开直答**,判定件 §3.11 增 **§3.11.6「直白回答:能」**。 🔴 **直白结论(先给)**:**在插件里写一段"调 K8S API 起容器"的代码 —— 能做,而且这就是 K8S 下的正常做法。** 我此前说的"不能"指的是**另一件事**:**插件不能自己把"身份"变出来**。 **🔴 链条逐环归责(本节骨架,以后复用这个表)** | 环节 | 谁做 | 插件能自己搞定吗 | |---|---|---| | **网络可达**(`kubernetes.default.svc`) | 集群自带(Service + DNS) | ✅ 不用操心 | | **身份**(token / CA / namespace 三个文件) | **K8S 自动挂给 Pod** | ✅ 不用操心(前提:实例是 Pod) | | **授权**(RoleBinding 把 SA 绑到 Role) | **管理员 / 引导层安装时 `apply` 一次** | ❌ **唯一必须外部的** | | **镜像**(DSH 那套要能拉) | 安装时配 `dshImage`(**配置项已有** `config.ts:117`) | ✅ 安装时配好 | | **调 API 起容器** | **插件自己** | ✅ **这一段就是插件做的** | ⇒ **只有"授权"一环必须在插件之外** ⇒ **"在插件里做个功能调 K8S API 起容器"完全成立。** **插件那段代码长什么样(已落文,示意)**:读 `/var/run/secrets/kubernetes.io/serviceaccount/{token,ca.crt,namespace}` ⇒ `fetch('https://kubernetes.default.svc/…')` 带 `Authorization: Bearer ` ⇒ POST 一个对象。**没魔法、没额外障碍。** **两条实务提醒**:① **token 是"读进来的"不是"生出来的"**(插件不自己造身份,读外部塞进来的文件 = "钥匙是别人给的"的具体形态)② **RBAC 划多细由你定**,可细到"只能在自己 namespace 里建名字带自己标识的对象",⛔ **别给 `cluster-admin`**。 **🔴 唯一真需要选的地方:调哪个资源** - **直建 Pod** ✅ 能跑通,但 Pod **一次性**(挂了不重建/被驱逐不补/升级不滚动),且平台库要知道就得回报或 watch ⇒ **两个真相** - **建自定义对象(CR)+ Operator reconcile**(推荐)✅ Operator 持续维持期望态,**平台库有唯一一份人类可读的期望态** ⇒ **两种都是"插件调 K8S API",区别只在调哪个资源。** **🔴 那为什么现状做不到?—— 因为承载方式还没换**:现状实例是 **systemd scope 不是 Pod** ⇒ `/var/run/secrets/kubernetes.io/serviceaccount/` **不存在** ⇒ **没有 token 可读** ⇒ 要成立就得**先把承载从 systemd scope 换成 Pod = 正是 P6 的 A 案**。 ⚠️ **顺带校准一个细节(写进本节)**:代码预留是**一个 namespace 装所有用户的 Pod**(`k8sNamespace` **单数**,注释「K8s namespace for **per-user DSH resources**」`config.ts:114-115`;且已给文件面预留 **sidecar** 镜像 `controlPlaneImage` `:118-119`)⇒ §3.10.6 我建议的「一租户一 namespace」是**更强的隔离方案、不是已有设计**,属可选强化。 **收口一句话(值得记住)**:**链条上真正在插件之外的,只有一份授权绑定(RoleBinding);而 RBAC 恰好是"一次 apply、永久生效、可审计、可回收、可限定范围"的东西 —— 这正是它比"每台机 root + systemd"好的地方:特权被集中进一份声明式文件里。** ⇒ **在插件里做这个功能,不是"不行",是"要先把承载方式换成 Pod,再给那个 Pod 一份限定范围的授权"。** **同步**:入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 --- ## 🔵 平台线(插件投放与分库线)|12:3x 追问「RoleBinding 为什么要做?有什么作用?」 **用户原话**:「授权(一个 RoleBinding 把 SA 绑到 Role):为什么要做这个有什么作用」 **动作**:判定件 §3.11 增 **§3.11.7**(⛔ 未改其余);入口 §0 加一行;本日志本节;释放域锁。 ⚠️ **本节是顺着 §3.11.6「直白回答:能」往后追的一问** —— 上一节说"链条上唯一在插件之外的是 RoleBinding",用户直接问**那张纸到底是什么、为什么非签不可**。 **一、先答「K8S 为什么非要这么设计」** K8S 的 API server 有两个特殊性质: 1. **它是个网络服务** —— 集群内**任何**一个容器都可能连到它 2. **调它就会改变系统状态** —— 建一个 Pod = 占 CPU/内存/磁盘 + **跑任意镜像里的代码** ⇒ **等价于"在那台机器上执行代码"** ⇒ 所以它必须能回答两个**分开的**问题:**"你是谁"(认证)** / **"你能做什么"(授权)**。 | 问题 | 由什么回答 | 我们的处境 | |---|---|---| | 你是谁?(认证) | client cert / **SA token** / OIDC | ✅ K8S **自动挂给 Pod**,不用我们管 | | 你能做什么?(授权) | **RBAC** = Role + **RoleBinding** | 🔴 **必须有人先授** | **二、为什么不给"认证过就行"** —— 集群里 Pod 成百上千(探针、日志代理、CNI、ingress、各种 job)⇒ **若"在集群里 = 有权建 Pod",任何一个有漏洞的容器就能把整台机器占满、起挖矿容器、读别人的 Secret** ⇒ **K8S 默认零信任:SA 认证过了,权限是空集**(=§3.11.2「SA 默认零权限」的由来)。 **三、Role 与 RoleBinding 的分工(最容易被跳过的一层设计)** | 对象 | 一句话 | 类比 | |---|---|---| | **Role** | 「**有哪些权限**」——一份**名词表**(能对哪些资源、做哪些动词)。**它本身不生效,只是定义** | 一份"岗位职责说明" | | **RoleBinding** | 「**把这些权限发给谁**」——一个**转接头**,把 Role 连到 Subject(User/Group/**ServiceAccount**) | "把这份职责指派给某人" | ⇒ **为什么分两个**(=复用与审计):① 一份 Role 可绑给很多人 ⇒ **改权限只改一处,全体生效** ② 某人的绑定可换 ⇒ **不用动 Role** ③ **审计时看的是 binding** ⇒ "谁有这个权限"一目了然。 ⇒ 与 Linux「文件 rwx 位(权限定义)」vs「用户/组归属(权限归属)」是**同一种思路**。 **四、在我们场景里的四条具体作用** 1. 🔴 **它就是"插件能不能建实例"的开关本身** —— 没有它,§3.11.6 那段 `fetch` 拿到 `403 Forbidden`;有它,**同一段代码**就成功 ⇒ **同一份插件代码,行为完全由这条绑定决定。** 2. 🔴 **它把"特权"从"机器"搬到了"一份声明式文件"** —— 现状起实例必须是**那台机的 root**(特权**跟着机器走**);K8S 下是"有没有一条 binding"(特权**跟着声明走**)。 3. 🔴 **它划出插件的权力边界,而且能划得很细**。 4. 🔴 **它把"授权"从代码里挪出去了** —— 插件代码里**不写任何权限判断**(写了也没意义,判断权不在它手里)⇒ **换授权 = 改一份 yaml 重新 `apply`,不用改插件、不用重启实例、不用重发版本。** **五、这段 yaml 长什么样** ```yaml # ① Role = 权限清单:只允许操作「实例」这一类对象 apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: { name: dsh-instance-owner, namespace: tenants } rules: - apiGroups: ["dshs.example.com"] resources: ["dshinstances"] # ⛔ 不含 secrets / pods / nodes verbs: ["create", "get", "patch", "delete"] # ⛔ 不含 deletecollection 等宽动词 --- # ② RoleBinding = 把这份权限发给「我这个实例的 SA」,且只在 tenants 这个 namespace 内生效 apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: { name: tenant-a1net-owner, namespace: tenants } subjects: - kind: ServiceAccount name: tenant-a1net namespace: tenants roleRef: kind: Role name: dsh-instance-owner ``` ⇒ **能做**:建/改/删**自己的**实例|**做不到**:看别的租户的实例、读别人的 Secret、动集群节点、建特权容器。**这就是"钥匙"的具体形状。** **六、三种给法,差别是量级级的** | 给法 | 插件调 API 的结果 | 实质 | |---|---|---| | **不给** | `403` | 插件只能"看",做不了事 | | **给一条限定范围的**(上面那种) | ✅ **只能动自己的实例** | **刚好够用** ← 推荐 | | **给 `cluster-admin`** | ⚠️ 能动整个集群 | ⛔ **等于把集群交给一个第三方插件** | ⚠️ 为什么单挑 `cluster-admin` 说:它**等于把现状的 root 特权原封不动送出去,还多送了一层跨租户能力**(能读**所有** namespace 的 Secret ⇒ **能读别的租户的凭据**)⇒ **这会把多租户从"四层隔离"降成"数据被读走"** —— 比 §3.9 那条"嵌套退化"**更严重**(进程隔离还在,但**数据已经泄露了**)。 **七、谁来做 —— 回到"自举"** 这条 binding **必须存在在插件运行之前** ⇒ **只能由管理员/安装脚本在装的时候 `apply`** ⇒ 这就是"自举"在 K8S 下的具体形态:**一把钥匙,装的时候给一次,之后插件自己用。** **⭐ 由此得到一个之前没说透的结论(本轮最有价值)**: > **在 K8S 形态下,「引导层」会退化成「一份授权 + 一个 reconcile 循环」。** > 它不再是"每台机上跑着的特权服务",而是**一次 `apply` 的产物 + 一个 Operator**。 ⇒ 这正是 §3.10.7 那句"引导层只会变薄、变标准"的**具体含义**。 **八、一句收口** > **这条授权的作用,一句话:它把"插件能不能自己开实例"这件事,从"取决于它跑在谁的机器上",变成"取决于有没有人给它签这一张纸"。** > ⇒ 而签这张纸**只需一次**,之后**可审计、可回收、可收窄,且不用碰代码**。 **同步**:入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器;⛔ 未 commit / push;⛔ 未登记下一棒;⛔ P6 结论未动。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 --- ## 🔴 平台线(插件投放与分库线)|12:1x 附带:工作区 `MEMORY.md` 超限压缩(⛔ 系统强制项) **触发**:本会话注入时收到 `ACTION REQUIRED` —— 工作区 `MEMORY.md` **超出注入上限、被截断**。 **实测体量(⚠️ 口径 = JS `string.length`,⛔ 不是字节)**:压缩前 **9,899 字符**(17,600 B);上限标称 **≈7,800** ⇒ **尾部约 2,270 字符(=`四、仓库/推送/授权` 全节 + `工作区 git` 全节)从未进过任何会话的模型上下文**(=等于不存在)。 **动作**: 1. 🔴 **先留全文副本** → `E:/ProgramData/AIProject/aliyun-dsh-server/.workbuddy/memory/MEMORY-全文-20260926.md`(17,600 B / 9,899 字符,**一字未删**) 2. 按本文件自身的落点判据(「不看会不会违规/事故?会 ⇒ 写实体;只'更慢更绕' ⇒ 给指针」)重写 `MEMORY.md` 3. **压缩后 7,629 字符**(13,827 B,56 行)⇒ **回到上限内**,全部内容可被注入 **做了哪些删/合(可复查)**:① 在途单明细(序46/序47 步骤、背压修复全过程)⇒ **压成一句 + 指向 `state.py`/各线 `接续入口 §0`**(状态本来就该现查,⛔ 不该固化)② 09-23 活库损坏的**取证全过程**(`immutable=1` 判读、`change counter` 校验、18:55 自愈细节)⇒ 保留**结论与禁令**,过程指向全文副本 ③ IM 三项拍板 **保留**(持久决策,会长期生效)④ D8 **保留**(用户拍板)⑤ 本机铁律 **一条未删**(全是事故级)⑥ `工作区 git` 三行 **保留**(会直接影响提交动作)。 **⚠️ 结构性教训(值得记住)**:**「写进去」≠「生效」** —— 09-23 那次瘦身只按标称值收口,之后每天追加,**13 天里又长出 2,186 字符**且**没人发现**(因为超限是**静默截断**,不报错)。⇒ **本文件以后只在"新增即替换"下改动**;每次追加前**先算 `string.length`**(`wc -m` 不准,`wc -c` 更不准 —— CJK 一字 3 字节)。 **同步**:本日志本节;`MEMORY.md` 头部已写明压缩事实 + 全文副本路径。 **未做**:⛔ 未改内容语义(只删冗余/改指针);⛔ 未 commit / push。 **锁**:`--claim-exec --domains 平台线状态与并发锁` rc=0 → 改完 `--release-exec` rc=0。 --- ## 🔵 平台线(插件投放与分库线)|12:4x 用户提方案「AI1NET 网络结构调整为三类节点」 **用户原话**(见 `交付物/AI1NET-三类节点结构调整可行性-20260926.md` §9 全文): > AI1NET 网络结构调整为是否可行:覆盖网络连接所有节点和设备 > 1、服务器版 服务节点:(47服务器dsh官方实例+底层插件 admin 使用)可配置服务集群(106服务器:K8S集群 开启多个实例分别给多租户用户使用) > 2、客户端版 服务节点:(官方dsh客户端版本,安装底层插件 admin使用,可在本机安装K8S集群 提供多租户服务) > 3、客户端版 实例节点:(安装dsh客户段版本,安装底层插件 可选择连接到 服务节点作为单独实例使用) **动作**:**新建** `交付物/AI1NET-三类节点结构调整可行性-20260926.md`;入口 §0 加一行;本日志本节;释放域锁。 **⛔ 未做**:未动定稿(`02-架构设计/覆盖网络-顶层架构全貌.md` 是唯一权威,改它属架构级拍板);未动代码;未动覆盖网络线入口(⛔ 不串线)。 **一、一句话判定** **三分法本身站得住,但有一个词站错了位置 ——「客户端版服务节点」不是"服务节点",是"主权承载节点"。把这个定语补上,三分就与定稿完全对齐,不需要改架构。** **二、根因:分类维度不同(本评估最重要的一节)** - 用户按 **功能** 分(这台机器给别人跑实例吗);定稿按 **权威** 分(谁说了算)。 - 定稿三层权威(§三):**根**(区域名录·用户全局唯一名·联邦吊销,**离线、在用户手里**)→ **区 Manager**(本区归属·租约·资格)→ **区内 Worker/节点**(执行面·本机运维库)。 - ⇒ 🔴 **用户说的「服务节点」在定稿里是两个不同的东西**:**区 Manager**(权威)与 **Worker/承载**(执行)。定稿把它们**分开**——因为它们**必须分得开**(管账的机器挂了不该带着实例一起挂)。用户合成一个词 ⇒ 产生**唯一真正需要澄清的歧义**:「客户端版服务节点」是 Manager 吗? - **若是** ⇒ 那是定稿 §4.2「Manager 可升格」,牵动 **F8 根集合治理** 与 **G6 撤根** - **若不是** ⇒ 它只是"自己给自己跑实例",**那它不该叫服务节点** **三、① 服务器版服务节点 —— ✅ 成立,且已是现状** - 47=**Manager**(`dshs`;PG13 `15432`)+Worker `w-47`(19100);106=`w-106`(19000)。 - 「官方 dsh 实例 + 底层插件 · admin 使用」= **管理员实例=普通租户**(实测:`dsh_instances` 按 `user_id` 建 `repo.ts:617`;`grep "role === 'admin'" src/supervisor/ src/web/routes/dsh.ts` **零命中**;`requireAdmin` 只是一句 DB 角色判断 `middleware/authn.ts:31-38`)⇒ **今天就是这么跑的**。 - 「可配置服务集群(106 K8S)」= 定稿 §三「区内 Worker/节点 = **执行面**」的**替换,⛔ 不动权威结构**。 - ⚠️ **两处点名**:**(a)** 106 上现在**已有 `w-106` 实例在 19000 跑** ⇒ 变 K8S 是「**形态切换**」⛔ 不是叠加,**现存量怎么处置必须先定**(=本轮 S1)|**(b) ⚠️ 待查:106 的机器规格未取证**(参照 `orchestrator.ts:1233` 宿主 2 核/1870 MiB、`:1229` 单实例 106~145 MiB:同规格则**装不下 K8S + 多租户** ⇒ 命中 R11)。 - ⭐ **① 的最大价值:给出了比我的 P6 建议更好的落法** —— 我原倾向「**先 B 后 A**」(维持自研单机);**用户方案实际是「B 与 A 并存」:47 维持自研 `uid+bwrap+cgroup+systemd`(生产不动)+ 106 试点 K8S(新承载)= 隔离试点、风险最低**(106 本就是 Worker、可当试验田)。⇒ **据此修正 P6 建议**。**这是本评估唯一一处「用户方案优于我原建议」的地方。** **四、② 客户端版服务节点 —— ⚠️ 必须拆两问,答案相反** **问 1:桌面机能不能跑 K8S 开多实例?** 分平台: - **Linux 桌面** ✅(k3s / kind) - **Windows / macOS** ⚠️ **要连过三关:虚拟化 → Linux 内核 → systemd** - `src/cli.ts:106` 依赖检查=`['bwrap','setpriv','systemd-run','nft','dsh']`;`orchestrator.ts:1249` 真起实例走 `spawn('systemd-run', sdArgs, …)` ⇒ **需 Linux 内核 + systemd**(WSL2 默认**不开** systemd,须显式 `systemd=true`;Docker Desktop 的 VM 也没有) - **强隔离能力直接 `return`**:`cli.ts:39`「account … **Linux, needs root**」/`firewall.ts:42` `platform==='linux' && getuid()===0`/`worker/agent.ts:147` `if (platform !== 'linux') return`/🔴 **`plugin-assembly.ts:796` 注释原文「`runPnpmAs` 走 `setpriv`(Linux 专有),Windows 开发机上必然 `ENOENT`」**(=平台**自己就知道**这件事) - ⇒ ⛔ **不是"性能差一点",是这些代码路径在非 Linux 上根本不执行** - 🔴 **且「官方 dsh 客户端版」今天对不上**:`package.json` description 原文「…每用户隔离的 DSH 环境(主 + 按需守护)、**桌面与域名访问**」⇒ **「桌面」=桌面浏览器访问**,⛔ **不是 desktop app**;本仓库**无 `apps/` 目录、无桌面壳**(「桌面壳」在 P1 的 A 案里是**待实现项**,缺点栏原文「**桌面/服务器两条壳要分别实现**」)。 - ⇒ **真落地的形态是**「dsh 客户端(Windows 侧)+ **WSL2/VM(Linux 侧)** + 里面一个 K8S」,⛔ **不是"客户端版 + 插件"** —— 桌面壳与实例在两个世界。 **问 2:就算跑起来了,能不能"提供多租户服务"(给别人)?** **四条件缺二**: | 条件 | 为什么必须 | 服务器 | 桌面 | |---|---|---|---| | **常开** | 别人的实例随时要用 | ✅ | ❌ **合盖=断服** | | **可达** | 别人得连得上 | ✅ 固定 IP | ⚠️ **NAT 后 ⇒ 每个用户各自解决一次** | | **有特权** | 要能起隔离进程 | ✅ root | ⚠️ 管理员+虚拟化+systemd | | **不占用主用机** | 要腾得出资源 | ✅ | ❌ 跑 K8S+多租户,主机自己就没法用 | | **➕ 受平台管辖** | 能打补丁/审计/强制下线 | ✅ | ❌ | ⇒ 「可达」这条**不是新问题,是 D8 那个门槛的翻版**(「中继方需"别人能连到它"=用户侧 NAT 门槛」);服务器版只需解决**一次**,客户端版是 **N 个用户 = N 个 NAT 问题**。 🔴 **最要紧的不是能力,是「信任根搬家」**: - 现状根信任 = **平台容器**。实证 `orchestrator.ts:823-827` 原文:平台**主动关掉** DSH 内置沙箱(改 `danger-full-access`),理由「**不在实例内再叠加沙箱,隔离交由平台容器(bwrap + uid + cgroup)负责**」。 - 桌面机当承载方 = **把根信任从"平台"搬到"私人设备"**,而该设备:**可被物理接触**(内存可 dump、磁盘可直读)/**管理员就是用户本人**(能读**所有**租户 home 与凭据)/**不受平台运维管辖**(不能打补丁、不能审计、不能强制下线)。 - ⇒ ⛔ **这不是"能不能做到",是"要不要把信任根搬家"。这是架构级的一道门** —— 与 §3.10 那条「设备侧不进集群」是同一件事的两面。 ✅ **② 什么时候成立 —— 换一个定语**: | 读法 | 服务谁 | 判定 | |---|---|---| | **主权服务节点** | **只服务自己的租户**(我一个人的多个工作区/身份) | ✅ **完全成立** | | **公共服务节点** | **服务别人的租户** | ⛔ 要过上面那道信任门 | ⇒ ⭐ **「主权服务节点」恰恰是客户端版最大的价值**:**单机多租户** —— 一个用户在自己机器上、用自己的 admin 身份、开自己的几个隔离实例。与定稿 §5.3 路径 **C**「自建独立区(**不联联邦**)= **不需要任何根**,自成一网」同源,⚠️ 但**今天无通路**。 ⇒ 🔴 **② 的原文写的是"公共",所以不成立;改成"主权",立刻成立。** **五、③ 客户端版实例节点 —— ✅ 成立,且是唯一"机制齐备"的一条** 逐条对上定稿 §5.1「设备入网六步」:①入口 ✅|②名录 🔴 缺(G2)|③选区 ⚠️ 半成|④签发 ✅|⑤本地校验 ✅|⑥建隧道 ✅ ⇒ =定稿 §5.3 路径 **B**,原文标 **✅「机制齐备」**,也是四条路径里**今天唯一通的一条**。 ⚠️ **用词建议**:「**实例节点**」与定稿不齐且有歧义 —— 读成「**跑着实例的节点**」⇒ 那是**承载方**(=服务节点);读成「**以实例身份入网的设备**」⇒ 那是**使用端**。上下文看是后者 ⇒ **建议改称「使用端 / 设备」**,与定稿 §2.1「节点(每机一钥)」对齐,⛔ 否则下一棒会当成承载方。 **六、补的那一维:承载能力(本评估核心产出)** **能不能给别人跑实例,取决于五个不随软件配置改变的条件**:**常开**(物理事实,不是配置项)/**可达**(固定入口/公网/中继)/**有特权**(root / K8S / bwrap / cgroup)/**不占用主用机**/**受平台管辖**。 ``` 有没有特权? → 没有 ⇒ 只能当「使用端 / 设备」 │ 有 ▼ 常开 + 可达 + 不占主用机 + 受管辖? │ 全有 │ 缺任一条 ▼ ▼ 【公共承载节点】 【主权承载节点】 服务别人的租户 只服务自己的租户 ⇒ 只有服务器能当 ⇒ 服务器 ✅ / 桌面机 ✅ ``` ⇒ **用户三分插入这一层就完整**:① = **区 Manager + 公共承载节点** |② = **主权承载节点** |③ = **使用端 / 设备**。 **七、🔴🔴 比"可行性"更硬的是「顺序」(本评估最硬的一条)** - 你要的效果「连接**所有**节点和设备」要求:**签发 + 撤回**身份。 - 现状(定稿 §六):**G1 未修** —— `SignerSet`/`NodeGrant` 只有 `issuedAt`、**不参与任何判定** ⇒ **一份旧名单(已撤销的签名者还在里面)重放给节点,验签会通过**;**G6 无任何机制** —— **撤根无手段**;且**两者叠加**(撤根的唯一补救是"换新名单",而名单无序号 ⇒ 这条路本身也不可靠,定稿原文「**G1 与 G6 必须一起解决**」)。 - 定稿 §七 原文:🔴 **「F1.5 是一切的前置」**。 - ⇒ ① **在 F1.5 之前,"把所有节点和设备都连进来"做不了** —— 因为**撤不回去** ② **且规模越大越撤不回去**(节点越多,一份可重放的旧名单能影响的机器越多)⇒ **这正是"先小规模、再扩张"的理由,⛔ 不是"先扩张、再补机制"**。 - ⚠️ 定稿 §九 第 1 条如实标注:**G1 重放未实测**("`issuedAt` 不参与判定"是**读码推断**)⇒ 落地前须构造真实重放实验复现。 **八、今天有通路的只有一条(对照定稿 §5.3 四条入口路径)** | 路径 | 现状 | 用户方案对应 | |---|---|---| | **A** 新服务器首启 | ⚠️ 今天只有 CLI,无 Web 通路 | ① | | **B** 设备加入已有区 | ✅ **机制齐备** | ③ | | **C** 自建独立区(不联联邦) | ⚠️ **今天无通路** | ② 的"主权"读法 | | **D** 自建区加入联邦 | 🔴 **今天无通路** | ② 的"公共"读法 | ⇒ **① 已是现状;③ 有通路;② 的两种读法今天都是空的**(C/D 都依赖 **G2 区域名录**,而 G2 依赖 **F1.5**)。 ⚠️ 另附定稿硬约束(会被误做成需求):首管理员设置页上的「有哪些 manager 可选」**⛔ 不可做成公开列表**(拓扑信息,公开即扩大暴露面),改法=**凭邀请码**。 **九、与既有拍板的关系** - **P6(承载走不走 K8S)**:🔴 **修正** —— 原倾向「先 B 后 A」⇒ 改为**推荐「B 与 A 并存」**(47 维持自研生产不动 + 106 试点 K8S),「先 B 后 A」不再是最优。 - **D8(端口)**:✅ **不变** —— 本方案⛔ **不需要开任何端口**(② 的主权承载走本机回环;① 走 443 中继)。 - **P5(平台可自建)**:⚠️ **② 的"主权"读法其实就是 P5 的客户端形态 ⇒ 两条线是同一件事,应合并考虑**。 - **§3.10「设备侧不进集群」**:✅ **不变**,且被本评估的信任分析**加强**。 - **G1 / G6 / F1.5**:⚠️ **进一步强化** —— 方案想连"所有节点和设备" ⇒ **规模直接放大 G1 的爆炸半径**。 **十、本轮唯一待拍板 S1(🔴 带数据的一次性动作,不可逆)** **要你定的是**:106 上现在那个租户实例(跑在 19000),在你把它变成 K8S 承载池之前,**先迁走 / 原地保留并行 / 退役**。 **为什么需要你定**:这是**带用户数据的一次性动作**,做错就丢数据/或用户突然断服;且它**决定 106 这条路是"替换"还是"叠加"**(两种做法后续工作量差一个量级)。 - **A 先迁走**(优点:106 干净当承载池、架构清晰、不留共存;缺点:**带数据**动作+停机窗口,且**跨节点迁移今天未设计**) - **B 原地保留并行**(优点:**零迁移风险**、可立刻试点;缺点:106 上**两套承载并存**、资源要重算、运维管两套) - **C 退役**(优点:最干净;缺点:**要用户配合**、等于给人断服) **我的倾向:B**。⚠️ 但**前提是 106 规格装得下两套 —— 这一项我还没取证**(§2.1 待查项)⇒ **要么先让我取证、要么你直接告诉我 106 的机器规格**。 **同步**:入口 `接续入口_插件投放与分库线_20260922.md` §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未动定稿(`02-架构设计/覆盖网络-顶层架构全貌.md`);⛔ 未动代码;⛔ 未动覆盖网络线入口(不串线);⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 网络结构调整判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 --- ## 🔵 平台线(插件投放与分库线)|13:0x 追问「按新规划,五块底层框架能不能放进一个插件」 **用户原话**: > 明白这个意思就行 K8S只是一种实例部署方案,按照这个规划 可以把现有多租户,模型管理,技能插件管理,覆盖网络,IM会话等底层框架 放到一个插件中了吗 **动作**:判定件 §3 增 **§3.12**(含 §3.12.0–§3.12.7 七小节)+ §4 缺什么表**新增一行**;入口 §0 加一行;本日志本节;释放域锁。 ⚠️ 这是**这条追问链的第 11 问**,也是**第一次得到肯定答案**。用户这句「K8S 只是一种实例部署方案」=**接受了我上一轮给的定位**,并把问题收敛到"业务面能不能进插件"。 **一、一句话判定** 🔴 **能 —— 而且你这句话本身就是答案的一半。** 你说「K8S 只是一种实例部署方案」,等于把上一轮最难的那一块(**起实例**)从插件里**摘出去**了;摘掉之后,你列的这五块**全部是"业务面",不是"承载面"** ⇒ **能进一个包。** ⚠️ 但必须分清:**「放进一个包」和「全都能自己干完」是两件事。** **二、为什么这次能,而第一轮说不能** | 轮次 | 问的是 | 判定 | |---|---|---| | 第一轮 | 插件能否承**整个项目从开始到现在的功能结构** | ⛔ 不能(含「承载面 6 件」) | | 本轮 | 按新规划,五块**底层框架**能否装进一个插件 | ✅ 能(全是使用面) | ⇒ 🔴 **第一轮问的是「插件能不能让平台*存在*」;本轮问的是「插件能不能承平台的*业务*」。前者不行,后者行。** ⇒ **差别不在插件的能力变了,在问题问得准了。** 这也解释了 §3.8 用户第一个方案为什么"看起来成立"——**直觉是对的,只是当时那个方案里混进了一件不属于插件的事。** **三、五块逐块(关键表)** | 块 | 业务逻辑能进包吗 | 🔴 留在外面的那个内核 | 跑在哪 | |---|---|---|---| | **多租户** | ✅ 能(租户/配额/成员/账单/界面) | ⚠️ **"创建隔离实例"这个动作本身** | 🔴 **平台进程** | | **模型管理** | ✅ 能(配置/凭据/共享授权/界面) | ⚠️ 无额外内核,但**必须跑在实例内** | 实例内 | | **技能插件管理** | ✅ 能(上传/检测/建库/启用/更新/界面) | ⚠️ **把包搬进来**(实例没起时也要跑) | 平台+实例 | | **覆盖网络** | ✅ 能(看哪些区/选加入/跨区可见性/界面) | ⚠️ **取身份(入网凭据)** | 主机级(引导层) | | **IM 会话** | ✅ **已经在跑**(唯一接入四面齐全) | ⚠️ 无(已走通) | 平台+实例 | ⇒ 🔴 **五块里四块的"最小内核"是同一类东西:要么需要比插件更高的权限,要么要发生在插件存在之前**(=§3.0.1 三条判据:**时序 / 身份 / 存活**)。 **四、🔴 本轮的第二个核心结论:这五块不在同一个进程里** 实证(09-25 审计):**插件跑在「用户实例内」,IM 内核跑在「平台进程」⇒ 跨进程。** - **多租户** ⇒ 🔴 **平台进程**(要写**平台库**:租户/租约/归属/配额 —— 实例内插件**连不到平台库、也没有平台权限**) - **模型管理** ⇒ 🔴 **实例内**(用户自配模型**只有实例内可用**,已实测) - **技能插件管理 / 覆盖网络 / IM** ⇒ 两半(平台/主机 + 实例) ⇒ 🔴 **"放进一个插件"的准确含义 = 一个发行包、两个进程各加载它的一部分**: - ✅ **今天有通路**:实例内插件 ⇄ 平台进程(HTTP 服务面 + 反向通道,IM 已走通) - ⚠️ **今天没通路**:**平台进程不能装插件**(平台不是"实例")⇒ 要在平台侧加的能力**只能加进平台进程本身,⛔ 不能"装进去"** ⇒ ⚠️ **对本轮问题的影响:多租户那一块的管理动作 ⛔ 不可能由一个"插件"实现**(插件最多做**界面 + 请求**)。 ⇒ 🔴 **这不破例,它就是分水岭** —— 只是把分水岭从「**插件 vs 引导层**」细化成「**插件 vs 平台进程**」。 **五、"一个包"的收益与代价** ✅ **收益三条**(正是 P1 A 案要的):① 一次安装=全套能力(用户装官方 DSH+一个包就有全套)② 跨端一致(服务器壳/桌面壳装同一个包)③ 更新只需更一个包。 ⚠️ **代价三条**: 1. 🔴 **一插件一库 ⇒ 五块共用一个库** —— 实证:`schema.ts:132` 原文「**包名 → 库名**:`@dsh-local/trpg-kit` ⇒ `dshs_pl_trpg_kit`」;`PLUGIN_DB_PREFIX='dshs_pl_'`(`:90`);库名形状 `dshs_pl_`+至多 40 位、**总长 ≤48**(`:93`/`:100`);库内表再加 `p__` 前缀(`diff.ts:248`) ⇒ **一个包=一个 pluginId=一个库** ⇒ **五块的表全在同一个库里 ⇒ 一次迁移失败牵动全部五块** 2. 🔴 **一个包=一个版本 ⇒ 更新耦合**:改 IM 要把多租户一起重发,⛔ 不能"只更 IM" 3. ⚠️ **边界会糊**:表归属/迁移顺序/回滚范围都要人工守 💡 **缓解(可推翻)**:**一个包做发行单元,包内分五个模块、表名分段**(`p_ai1net_im_*` / `p_ai1net_tenant_*`),**只留一个迁移入口、按模块分步执行** ⇒ 拿"一次安装"好处,保住"能单独回滚一块"的余地。 **六、✅ 必须留在外面的只剩三件(不是六件)** | # | 留在外面的是什么 | 归谁 | 为什么 | |---|---|---|---| | **1** | **起实例** | **部署方案**(K8S/systemd,随你选) | 🔴 **用户这句话已经解决了** —— 谁起、怎么起,⛔ 不归插件管 | | **2** | **取身份**(入网凭据:根/签名者/节点密钥) | **引导层** | **信任根不能自己给自己发**(自举);⚠️ 且现状 **F1.5 未解** | | **3** | **把插件包装进去** | **引导层** | 插件**不能在不存在时装自己**(自举) | ⇒ 🔴 **六件 → 三件**:第一轮六件里**「起窗口首屏」「原生网络守护」「起承载服务」三件已并入第 1 件**(本来就是"部署方案该管的")—— **用户这一句话把六件里的三件一起划出去了。** **七、如果真做,工作量在哪** ⚠️ **"放进一个包"是重写,不是搬家** —— 五块今天的代码分布在 `src/supervisor/*`(承载)· `src/web/routes/*`(管理面)· `src/db/*`(租户与归属)· `src/net/relay/*`(覆盖网络)· `src/im/*`(IM 内核)+ 实例侧 ⇒ 变插件形态=**重写成插件接口**,⛔ 不是 `git mv`。 ✅ **但已有现成模板**:**IM 那一块是今天唯一「装配 / 运行 / 取数 / 回调」四面齐全的** ⇒ 其余四块**照着 IM 的接入方式做,是最短的路**(残留两条已在 §4 登记)。 **八、收口** > **能进一个包,但要分两半装:一半装进实例(界面 + 业务逻辑),一半长在平台进程里(要写平台库的动作)。包外面只剩三件:起实例归部署方案,取身份和把包装进去归引导层。** ⭐ **一句话记法**:**插件承"要什么",部署方案承"跑起来",引导层承"怎么开始"。** 🔴 **第一轮那个"六件承不了"的结论没有变** —— 变的是:**这六件现在只剩三件,而其中最大的那件(起实例)已经被这一句话划到外面去了。** **九、§4 缺什么表新增一行** 🔴 **平台侧插件宿主(让同一份包在平台进程里也能跑)** —— 现状**平台不是"实例"⇒ 装不了插件** ⇒ 五块里"要写平台库"的那一半(多租户等)**只能加进平台进程本身**;没有它,「一个包两半装」的**平台那一半落不了地**。 **同步**:入口 §0 加一行(新→旧序);本日志本节。 **未做**:⛔ 未改代码、⛔ 未动服务器、⛔ 未动定稿;⛔ 未 commit / push;⛔ 未登记下一棒。 **锁**:`--claim-exec --domains 全功能承载判定件/全文` rc=0 → 改完 `--release-exec` rc=0。 --- ## 🔵 平台线(插件投放与分库线)|13:1x 用户下指令「详细规划功能打包到基础插件的分步实施方案,每步都要详细验证,为后续执行避坑」+「按接续会话方式规划迭代」 **用户原话**(两句,第二条追加): > 详细规划功能打包到基础插件的分步实施方案,每一步都要做详细验证,为后续执行逻辑必坑 > 按照接续会话的方式规划迭代 **动作**:**新建** `交付物/功能打包到基础插件-分步实施方案-20260926.md`;入口 §2 顶部插入「🎯 本轮动作(第 37 棒 · 规划棒)+ §2「⏭️ 本线下一项」行;**登记第 37 棒一次性 automation**;本日志本节;释放域锁。 ⚠️ 本棒按技能 `dsh-auto-handoff-chain` 编排(用户点名「接续会话」⇒ 已加载并照其 §2 骨架 / §3 收尾四件套 / §3.1.1 排期铁律 / §3.3 登记门禁执行)。 **🔴 本棒最重要的产出:纠正了我上一轮的三条错误前提**(上一轮只读平台仓库、没读客户端工作区) 用户指出 `E:/ProgramData/AIProject/dsh-ai1net-desktop` 是客户端开发会话、可参考 ⇒ **只读取证**(⛔ 未改其任何文件)。三条纠正: | 我上一轮的说法 | 实际(客户端线实测) | |---|---| | 「本仓库无 `apps/`、无桌面壳 ⇒ 客户端壳不存在」 | ⚠️ **只在平台仓库里成立** —— 官方壳在上游 `deepseek-ai/deepseek-harness` → **`apps/desktop`(0.1.6-alpha.1)+ `apps/desktop-host`**(`private:true`、未发 npm ⇒ 按官方源码直接构建、不 fork、零 patch)。**官方明确支持二次开发且是"一等公民"**:载体 = **插件(组合包 bundle)+ patch + profile**;官方原文「独立插件窗口获得结构化的列出、安装、移除、更新和更新检查操作」。⇒ ⛔ **「桌面/服务器两条壳要分别实现」要修正为「壳是两条,插件机制是一条」** | | 「`setpriv`/`systemd-run` 是 Linux 专有 ⇒ Windows 上这些代码路径不执行 ⇒ 桌面机不能当承载」 | ⚠️ **那条链是 `account` 隔离档的依赖;客户端走 `soft` 档** ⇒ 那两个工具根本不在这条路上。客户端 `CODEBUDDY.md §9` 原文:**「平台启动实例的方式在 Windows 上不成立」** —— 裸名 `spawn('npm')` → `ENOENT`(`.cmd` 垫片)、显式 `.cmd` → `EINVAL`,**必须走 `cmd.exe /c` 或 `shell: true`**(`orchestrator.ts:663-665` 现不带 shell);**「这是全案唯一必须改平台代码的地方,也是 M0 的验收点」**。另一条:**「多人形态开关是零代码的(只配域名),但隔离档仍是 `soft` ⇒ 用户之间无隔离」**。⇒ **结论对(桌面不能当公共承载),理由要换**:不是"跑不了",是"**能跑但没有多租户隔离**" | | (我完全没提到) | ⭐ **桌面端没有 `webServer`** —— 实测 `apps/desktop-host/src/index.ts:326-343`:`/streams` → streams · **`/api` → connection(`createSharedFetchHandler('/api')`)** · 其余 → assets;且 `fetch.register` **只支持精确路径、⛔ 无前缀匹配**。⇒ 🔴 **"一个包两端跑"的真门槛 = 必须只用两端都存在的扩展点子集**:可用交集 = `{ connection.fetch.register(精确路径)· dsh.client · index 注入 }`,⛔ 禁用 = `{ webServer.register · 前缀路由 · 页面级路由 }` | **🔴 第二个关键发现:分发与更新的底座已经存在,⛔ 不许重造** - 平台侧共享包库 + **版本维度** + 三条只读口**已上机**(`…/shared/node/sync`|`…/shared/node/pack/:flat`|`…/shared/node/result`;另有 admin 侧 `…/shared/status`|`…/shared/target`) - 客户端侧已有对接单 `docs/对接单_桌面端接入版本管理与包更新机制_20260926.md`(平台线第 36 棒产出,本线档案) - 现读数:平台权威共享层 **4 个包**;`dsh-plugin-mcn-suite` = `0.5.1` / 指纹 `27f6c6d7…` / **659 文件 / 2,018,751 B**;客户端本机 profile **4 个 bundle、无 `@dsh-local/*`** - ⇒ 本方案 **⛔ 不另起一套** ⇒ 直接接进去(方案 §4 步骤 1「照抄 `src/worker/agent.ts` 的 `syncSharedLayerOnce()` / `pullOnePack()`」) **方案结构**(`交付物/功能打包到基础插件-分步实施方案-20260926.md`) - §0 定位与**三条硬边界**:⛔ 不重造分发机制|⛔ 不改官方 dsh 主程序(R2)|⛔ 不碰「起实例/取身份/把包装进去」那三件 - §1 **先纠正三条前提**(上表) - §2 目标形态图(一个包、两处加载、平台那一半不装包) - §3 **分步实施方案 S0–S9**,每步七段:目标/前置/步骤/**🔴验证(可复现)**/⚠️坑/回滚/完成判据 - **S0 立验证地基**(⛔ 不做这步,后面所有"验证"都是自说自话)· **S1 定包规格书**(不动一行代码)· **S2 空包两端落地** · **S3 数据面** · **S4 接进既有分发机制** · **S5 IM 插件侧**(选它因为**是唯一接入四面齐全的 = 模板**)· **S6 技能插件管理** · **S7 模型管理** · **S8 覆盖网络界面**(F1.5 前置)· **S9 多租户**(包里只做界面+请求) - §4 **避坑总表(第二主产出)**:**A–G 七类 40+ 条**,每条「现象 → 根因 → 后果 → 规避 → 证伪手段」,⛔ 无一条推测、全部两端实测 - §5 **验证基建:判据分五层**(语法/契约/装配/运行态/**浏览器级**)+ 三条铁律:**UI 类必须有浏览器级判据**|**每条判据都要做负控(改坏 ⇒ 必须红)**|**反例与正例同等重要** - §6 **接续棒编排**(8 棒表 + 每棒固定纪律 + 第一个停点)· §7 停点与待拍板 · §8 出处 **🔴 避坑总表里最该记住的几条(都是"看着是绿的其实没生效")** - **A1**:官方 client bundle 契约 = **lazy-CJS 封包**(`window.__ModuleLoader__.load({id, factory})`,**零顶层 `import`/`export`/`require`**)⇒ 手写 ESM 直交 ⇒ 浏览器按经典脚本加载 ⇒ `import failed` ⇒ **卡片根本不出现**。🔴 **推论**:`node --check` +「可组合」+「boot 零报错」**三条全绿也证明不了客户端能加载** ⇒ **UI 交付必须有浏览器级判据** - **A2**:🔴🔴 **cordis 的服务只能靠 `inject` 拿** —— 未声明 ⇒ `cannot get property "connection" without inject`;`ctx.reflect.get('connection', false)` **返回 undefined** ⇒ **只写 try/catch 会让 HTTP 路由静默 404** - **A10**:平台门户脚本体**不幂等** ⇒ 第二次挂载静默降级 ⇒ **凭据进 URL**;**A8**:`dsh-app:` 自定义协议 **Chromium 不落 cookie**;**A7**:内联 `。 **说明**:<影响谁 / 断多久 / 花多少钱 / 会不会丢数据>。 **A 案**:<做法>(优点:…;缺点:…)。 **B 案**:<做法>(优点:…;缺点:…)。 **倾向**:。 **已同步(5 文件)**: - 共享层 `~/.workbuddy/SOUL.md`(每轮注入 · 跨项目):四要素 ①② + 句式。 - 共享层技能 `agent-operating-rules/SKILL.md` §1.7:四要素 ①② · 句式块 · 发出前自检 ② · §2 语气 · 骨架表「向用户提问」行。 - 同技能 `references/01-协作与上抛判据.md`:§1.2 上抛方式 · §3.1「正确写法」示例 · §6.2 语气 · §6.3 骨架表 · §7.1 结论骨架的待拍板段。 - 本工作区 `CODEBUDDY.md §1`:四要素标签。 - 本工作区 `docs/规则与载体/规则详解_红线与实证_20260924.md` §A-5 / §A-6:描述 + 句式。 **保留未动**(判据 = 属「自决交代语」,不在提问模板内):「我选了什么(可推翻)」—— SOUL.md 同节、SKILL.md §1.3/§1.6、references/01 §4.1、references/02 §178。 **⛔ 未同步面(别的工作区 / 机器解析器,按硬禁令只报告)**: - 4 个**别的工作区** `CODEBUDDY.md` 仍是旧句式:`dsh-ai1net-desktop`(L50/52/55) · `dsh-plugin-forge`(L119–120) · `dsh-plugin-carbon`(L98) · `dsh-plugin-partment`(L26–27)。 - `dsh-decision-laya/bridge/` 解析锚仍认旧字样:`classify.py` 栏目前缀表(L124–127) · `transcript.py` `PENDING_ANCHORS`(L592) · `question_scan.py` 提示语(L138–139) ⇒ 新模板下**待拍板项抽取会静默降级**(不报错、直接漏)。 **锁**:`--claim-exec "rule-template-20260926"`(未声明域 ⇒ 全局独占)rc=0 → 已显式 `--release-exec`。 ### 追加|13:3x 用户选 A:全平台口径一并同步(含别的工作区 + 决策库解析器) **用户令**:「A」= 把新模板同步到 4 个**别的工作区**的规则文件 + **决策库线自动解析器**(上轮已列出并请其定)。 **已改(第二轮 · 11 文件)**: - 别的工作区规则文件(4):`dsh-ai1net-desktop`(四要素 ①② + 句式)· `dsh-plugin-forge` · `dsh-plugin-carbon` · `dsh-plugin-partment`(句式块)。 - 决策库解析器(7): - `bridge/classify.py`:`SCAFFOLD_LABELS` 追加 `说明`/`倾向`;`_SCAFFOLD_ANCHOR` 追加说明栏锚。 - `bridge/transcript.py`:`PENDING_ANCHORS` 追加 `**说明**`;`_pending_question_text` 返回前剥新栏目标题前缀。 - `bridge/question_gate.py`:运行期拦截提示 `_rewrite_instruction` 改成五栏。 - `bridge/question_scan.py`:prompt 的「不许带栏目标题」举例补新栏位。 - `scripts/webview/index.html` · `docs/决策库看板_20260926.md` · `docs/规则详解_20260924.md`:用户可见的格式说明。 - `scripts/selftest.py`:新增 ⑩.c 用例 5 项(新模板抽取 + 新旧去重键一致)。 **🔴 关键坑(实测踩了一次,已固化进代码注释与自测)**:栏目标题在正文里是**加粗**的 `**说明**:`, 锚若只写 `"说明:"` 则**匹配不上**(`说明` 与 `:` 之间夹着 `**`)⇒ 新模板**整类采不到、且不报错、静默漏**。 ⇒ 锚必须写成 `**说明**`;自测 ⑩.c 专防它回退。 **✅ 验证**:`scripts/selftest.py` ⇒ **PASS 372 / FAIL 0**(改前 367,新增 5);实测新模板抽取出 1 条、 题干干净(无栏目标题、无 markdown)、候选 A/B、推荐 A;与旧模板**去重键一致**。 **⛔ 有意未改(判据:记录历史事实 / 兼容存量,改了等于篡改或致盲)**: - 解析器里**兼容存量**的旧标签与旧锚:`SCAFFOLD_LABELS` 的旧项、`PENDING_ANCHORS` 的旧锚、 `TENDENCY_HEADS` 的「我的倾向」、`_pending_question_text` 的旧语序分支 ⇒ **必须保留**(否则采不到历史会话)。 - 历史台账与日志:`接续入口_决策库线_20260924.md`(数十处)、`.workbuddy/memory/*.md`、评估类 `docs/`(记录实测数据)。 - 少量**注释**仍提旧字样(解释历史形态),不影响功能。 **锁**:`--claim-exec "rule-template-20260926"`(未声明域 ⇒ 全局独占)rc=0 → 已显式 `--release-exec`。 **未做**:⛔ 未 commit / push(用户未要求)。 --- ## 🔵 平台线(插件投放与分库线)|13:2x 第 38 棒 · 规划棒 收官 —— 包规格书冻结 + S0 执行单(`插件投放与分库线-38`) **一句话**:出了新工程「功能打包到基础插件」的第一份可执行交接单(**S1 规格书冻结 + S0 执行单**);按用户 **13:19 明令**「现在只做方案;落地详细步骤和方案做好了,确认后再执行」⇒ **⛔ 本棒未登记任何执行棒**,执行面等用户确认。 **产出** - 新建 `D:/github/dsh_shenxian/dsh-server-docs/05-交接单/交接单_包规格书_20260926.md`(248 行 = **8 段模板 + §五 A 包规格书 + §五 B S0 执行单 + §五 C 指针**;占号目录 `05-交接单/.lock-基础插件打包-01`,原子 mkdir)。 - 本工作区入口 `接续入口_插件投放与分库线_20260922.md`:§0 加最新行、§2 第 38 棒块改「已收官 13:2x」、⏭️ 块补「如实记账」。 - 技能 `dsh-decision-method` 已加载(取舍判据以它为准)。⛔ 未改 `src/**`、⛔ 未部署、⛔ 未 ssh、⛔ 未动客户端工作区。 **判据(机验,四条全过)** | # | 判据 | 命令 | 结果 | |---|---|---|---| | 1 | **零条扩展点只在 Web 端存在**(硬判据) | `grep -nE 'web[S]erver\|前[缀]路由\|页面级路[由]' <单>` | **零命中 rc=1** ✅(方括号刻意 → 检查项自身不自匹配) | | 2 | 库名硬校验 | `re.fullmatch(r'dshs_pl_[a-z][a-z0-9_]{0,40}','dshs_pl_ai1net')` + 总长 ≤48 | `True True` ✅ | | 3 | 8 段齐 | `grep -cE '^## (一…八)、'` | `8` ✅ | | 4 | 含负控 / 浏览器级 | `grep -c '负控'` / `grep -c '浏览器级'` | `4` / `3` ✅ | **规格书冻结的关键定义(后续棒 ⛔ 不重议)**:包名 `@dsh-local/ai1net`|pluginId `ai1net`|库名 `dshs_pl_ai1net`(15 字符)|表前缀 `p_ai1net_`|模块段 `p_ai1net_{tenant,model,plugin,net,im}_`|**单入口按模块分步迁移**|🔴 **扩展点只留交集三条**:`connection.fetch.register`(**精确路径**,`inject` 必写)/`dsh.client`(lazy-CJS 封包)/index 注入事件 —— 三类 Web 组合专有写法(服务端具名路由注册/路径前缀匹配/独立页面级入口)**零引用**。 **停点(已写进单)**:**P1**(桌面客户端要不要有权只读平台插件包)撞 **S4**|**P2**(平台侧那一半怎么落)撞 **S9**,且 P2 须等 P1 定了再问|全链另有 **方案 §6.0「用户确认门」**。 **⚠️ 本轮自纠一处**:我在**方案件里误加了一份 §3.10 副本**(当时不知道 13:1x 那一轮已把 §3.10 写就、且更详细,含 3.10.1–3.10.12)⇒ 用脚本撤除并复核「§3.10 只剩一份」,同时把本单 §五 C 改成**指针**(⛔ 不再维护第二份清单)= **不造第二个漂移源**。回滚副本:`tmp/_p38_plan.bak.md`、`tmp/_p38_jdj.bak.md`。✅ 方案件最终**零净改动**。 **机制层观察两条(⛔ 未改任何跨库脚本 —— 属机制层,须独占开工)** - **① 门禁误报**:`preflight-lock.sh` 对本棒 3 个目标路径**全判【D】未归类**(脚本 `ok_files` 正则只覆盖 `^(src|poc|test|web|docs|scripts|交接单|skills)/`,覆盖不到文档库 `05-交接单/` 与工作区根文件)⇒ 按第 26–32 棒既有惯例,用 guard 锚点算法自定域键后开工(已如实记账)。 - **② 🔴 域锁中途"消失"一次**:13:21 `--claim-exec` 抢到的域锁,到 13:25 已不在(同期 `rule-template-20260926` 13:21 建**旧的全局锁**、13:25 前后 `--release-exec`)⇒ 期间一次 Edit 被 hook 以「**你没有持任何锁**」**fail-closed** 拦下;重新 `--claim-exec` rc=0 后继续(**未删任何锁、未接管**,符合 R9)。⚠️ **「旧全局锁释放是否会连带清掉别人的域锁」值得后续核**(本棒只观察、未定性)。 **锁**:`--claim-exec "插件投放与分库线-38" --domains "dsh-server-docs/05-交接单,aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md,aliyun-dsh-server/.workbuddy,aliyun-dsh-server/交付物"` **rc=0**,复读 `.locks/<…>/OWNER` **断言归属**(ME = 插件投放与分库线-38)⇒ 收官 `--release-exec "插件投放与分库线-38"`。 **接续**:⛔ **未登记任何棒次**(用户 13:19 明令)。⚠️ 一度按旧口径误建占位 automation `45058a55-3857-4f02-a909-34747e854662`(一次性 · 13:31)⇒ **已置 `PAUSED`、不会开跑**(⛔ 未删:删除不可逆)。第 39 棒 = 执行棒(S0 落地 + S1 复核)**⏸ 等用户确认后才可立棒**,已写进入口「⏭️ 本线下一项」。 **未做(如实)**:判据 5「冷读者测试」留给第 39 棒;S0 脚本与两端基线读数属执行面,等确认;⛔ 未 commit / push。 --- ## 🔵 平台线|13:3x 用户拍板「**先备份现有代码,然后都按照 A 方案执行**」⇒ 备份已做 + 第 39 棒执行棒已立 **用户原话**:「先备份现有代码,然后都按照A方案执行」 ⇒ **C0 = A(按方案全走)+ P1 = A(桌面只读读包权放开)** ⇒ 方案 §6.0「用户确认门」**已满足** ⇒ 执行面开工。 ### ① 备份(**已完成**) 落点 `E:/ProgramData/AIProject/aliyun-dsh-server/.backup-code-20260926/`(**⛔ 不入库**,`.backup-*/` 已被 `.gitignore` 排除): | 产物 | 体量 | 用途 | |---|---|---| | `dsh_shenxian-code-20260926.tar.gz` | **11,943,969 B**(1894 文件,排除 `node_modules`) | 全量快照(含 `.git`) | | `uncommitted-tracked.patch` | 719,053 B | 未提交改动可回放 | | `status.txt` | 6,173 B | 90 条变更清单 | | `HEAD.txt` | 48 B | 基线 = `e6207aa6914ff26bee7e999b72db153d00e2d096` / `master` | ⚠️ 源仓库 `D:/github/dsh_shenxian` 有 **90 个未提交改动** ⇒ 单靠 git 回滚不够 ⇒ **patch + tar 两件必须成对保留**。 🔴 **服务器侧快照未做**(47 `/opt/dshs`、106 `/opt/dshs-cluster`)⇒ 已写进第 39 棒第 ③ 件(只读打包到 `/opt/dsh/backups/`;⚠️ **106 ssh 凭据本身是待拍板项** ⇒ 不可达时只报告、不阻塞)。 ### ② 第 38 棒门禁复核(✅ 生效) - 交接单 `交接单_包规格书_20260926.md` **已于 13:30 产出**(18,584 B)⇒ 第 38 棒跑完。 - ✅ **它没有自行登记下一棒**(入口原文仍是「⏸ 等用户确认后才可立棒」)⇒ 13:2x 改的 prompt 生效。 - ⚠️ 但它**一度按旧口径**误建占位 automation `45058a55-3857-4f02-a909-34747e854662`(13:31)⇒ 它自己**已置 PAUSED** 并如实记账(⛔ 未删)。**无撞车**(PAUSED 不触发)。 - 🔴 **教训(新增)**:**改 automation 的 prompt 拦不住"已经触发、已在跑"的那个会话** ⇒ 门禁必须**在登记时就写对**,⛔ 不能指望事后改 prompt 补救。 ### ③ 第 39 棒已立(执行棒) - automation **`f46bad0f-acec-46e5-ae86-e36102bcf672`**(一次性 · `2026-09-26T13:40` · `cwds = E:/ProgramData/AIProject/aliyun-dsh-server`) - 任务 = **S0「立验证地基」**四件:① 写四把尺子(`v-baseline.sh` / `v-plugin.sh` / `v-ui.sh` / `v-neg.sh` → `交付物/验证脚本/`)② 取两端基线 + 负控 + 连跑两次 ③ 服务器侧代码快照 ④ 补判据 5「冷读者测试」 - **执行依据 = 规格书**(`05-交接单/交接单_包规格书_20260926.md`)**§五 B + §六** - 🔴 **红线门禁保留**:会重启控制面/中断在线用户/批量改 >10 文件/不可逆 ⇒ 先停下报告等点头(⛔ 不因"已选 A"而豁免) - 入口 §2 顶部已立「🎯 本轮动作(第 39 棒)」块 +「⏭️ 本线下一项(**以本块为准**)」= 第 40 棒 **锁**:第 39 棒尚未开跑 ⇒ 本轮无域锁动作(上一轮改方案件的锁已 `--release-exec` rc=0)。 **未做**:⛔ 未改任何代码、⛔ 未部署、⛔ 未 ssh、⛔ 未动定稿、⛔ 未改客户端工作区任何文件;⛔ 未 commit / push。 --- ## 🔵 平台线(插件投放与分库线)|13:40–14:0x 第 39 棒(执行棒)—— S0「立验证地基」收官 **会话名**:插件投放与分库线-39 | **域锁**:6 个域(`验证脚本/v-baseline.sh` / `v-plugin.sh` / `v-ui.sh` / `v-neg.sh` / `接续入口_插件投放与分库线_20260922.md` / `memory/2026-09-26.md`),抢锁 rc=0,收官已释放。 ⚠️ **如实记一条**:`preflight-lock.sh` 对本棒 6 个目标**全部判【D】未归类**(对「文档库 `05-交接单/` 下的文件」与「工作区根文件」恒判 D)⇒ 按第 26–32 棒既有惯例,用 guard 锚点算法(`_ANCHOR_SEGS`:`交付物`/`.workbuddy` 都不是锚点 ⇒ 落到末两段键)**自定域键**后开工,⛔ 未改那个跨库脚本(属机制层)。 **产出(新文件 4 个,全在 `交付物/验证脚本/`)**:`v-baseline.sh`(两端基线,`--stable` 输出供连跑对比)|`v-plugin.sh <装配|运行|取数|回调>`|`v-ui.sh`(浏览器级:真壳 + CDP,**不自动启动真壳**)|`v-neg.sh `(负控驱动)。三条约定已固化进脚本:**退出码即结论**(0绿/≠0红/**3=未取到**)|**只许测活体**|落点不入文档库 `07-scripts/`。 **关键读数与证据**: - **负控 4/4 通过**(S0 唯一完成判据):装配/运行/取数/回调 **改坏 rc=1 红 ⇒ 改回 rc=0 绿**;4 个 case 均**单变量**破坏,且**只改 `tmp/p39/` 副本**(⛔ 未碰任何其他工作区文件 / 真 profile / 真日志)。 - **`v-baseline.sh --stable` 连跑两次逐字一致** ✅。 - **平台侧基线**:47 `/var/lib/dshs/bundled-plugins` **4 包**(mcn-suite 0.5.1 / 659 文件;storyforge;im-conversation-tabs;softspark file-preview)。⚠️ 指纹为**自算内容哈希**(`find|sort|sha256sum|sha256sum` 前 8 位),⛔ 与平台 treeSha256 **不同源**;字节口径 = 目录全部文件之和,⛔ 与平台 pack/tgz 字节**不可互比**(mcn-suite 实测 23,681,071 B vs 记录 2,018,751 B)。 - **客户端侧基线**:`$DSH_HOME/profiles/desktop`(= `E:/github/dsh-desktop-0.1.7rc2/apps/desktop/.desktop-build/development/home-adapt`)**4 bundle、`@dsh-local/*` 计 0** ✅(与方案开工对照一致)。 - **浏览器级 `v-ui.sh` ⇒ rc=3 未取到**(本机无运行中的真壳,CDP 9330 不可达)⇒ **⛔ 未折算成绿**;真壳启动器属**客户端线**(R7-边界),本棒**不代启**(启动会写其开发态目录、污染刚取的基线)⇒ 交第 40 棒在客户端线窗口内取证。 - **服务器侧代码快照(只读打包)**:47 `/opt/dshs` → `/opt/dsh/backups/code-47-optdshs-20260926-135619.tar.gz`(**4,881,482 B / 1859 条目**,排除 node_modules)+ `HEAD.txt`(`8390eb3`)+ `status.txt`(**207 行未提交**)+ `uncommitted.patch`(1,740,749 B);106 `/opt/dshs-cluster` → `…code-106-optdshs-cluster-20260926-135633.tar.gz`(**34,342,067 B / 22643 条目**;⚠️ 该目录**不是 git 仓库**)。⛔ 未碰 `/opt/dsh` 数据、⛔ 未改任何生产文件。 - 规格书**判据 1–4、6 全过**(判据 1 grep 零命中 rc=1;库名正则 `True True`;8 段齐;负控 4 / 浏览器级 3 处提及;E1–E3 逐条两端 ✅)。 **判据 5 冷读者测试(本轮补做)**:交给未参与本方案的视角只读规格书问"能不能照做" ⇒ **答"不能"**,并给出 8 处矛盾/歧义。⇒ 已按"答不出就补"回填规格书**四处**:① 新增 **§〇 路径与变量约定**(`$WS`/`$DOC`/`$PY`/`$NODE`/方案件/判定件/尺子/读数 全部绝对路径)② §五 B 新增**靶子与命令表**(靶子=`@dsh-client/portal-first-screen` + profile/日志/尺子用法/退出码口径)③ 消除三处打架(确认门状态 / "不改码"vs"负控改坏靶子" / "不 ssh"vs 取远端基线)④ §六 判据 1 补**作用域注**(名实之辨)。回填记录写在规格书新增的「§八附」段。 **接续**:已登记第 40 棒 automation **`1153a7c8-358e-446e-995d-cc9bbeda64b9`**(一次性 · 14:07 ⇒ 本棒收官 +5~8 分钟 · cwds = 本工作区)= S2 空包两端落地(前置 = 本棒 S0 全绿)。⛔ 第 41 棒及以后未预登记。 ## 🔵 平台线(插件投放与分库线)|14:07–14:2x 第 40 棒(执行棒)—— S2「空包两端落地」收官(`插件投放与分库线-40`) **本轮只做一件事**:造一个只有骨架的新包,让它**两端都能装起来**,⛔ 不写业务。执行依据 = 规格书 `交接单_包规格书_20260926.md`(§五 A 冻结规格)+ 方案 `§3.10.4`(S2 逐步照做唯一权威)。 **产出:新包 `E:/ProgramData/AIProject/dsh-plugin-ai1net/`**(新建;沿用本机 `dsh-plugin-*` 同级惯例) - `package.json`(`dsh.bundle.patch` + `dsh.client`)|`cordis.patch.yml`(`insert: {id: ai1net}`) - **`lib/index.js`(host 半)**:一条**精确路径**路由 `GET /api/ai1net/ping` + 一次事件订阅(骨架占位)+ `export const inject = ['connection']`(**逐字必写**) - **`lib/client.js`(client 半)**:**lazy-CJS 封包**(`window.__ModuleLoader__.load({id, factory})`)+ 一张挂在官方 `plugins.item` 槽位的骨架卡片(含一个打探针的按钮);**只交 `apply` + `inject`(R3)** - `scripts/build.mjs`(零依赖门禁/组装)|`README.md` - **冻结口径照规格书 §五 A4 一处未改**:`@dsh-local/ai1net` / `ai1net` / `dshs_pl_ai1net`(⚠️ S3 才建库,本棒 ⛔ 未碰数据面)/ `p_ai1net_` **判据(一律调 S0 四把尺子,退出码即结论)** - ✅ **装配 rc=0 ×2 端**:端 A =**真桌面 profile 实件副本**+本包(5 bundle);端 B = **平台侧 `profiles/web` 键位建模**+`dependencies` 的 `link:`(7 bundle) - ✅ **取数 rc=0**(真实 import 本产物;`exportsOnModule = [PING_PATH, SKELETON_VERSION, apply, inject, name]`) - ✅ **回调 rc=0**(真实 `apply(ctx)`,spy ctx)—— 最硬的一条证据:`routeCalls` 逐字记录 **`{"path":"/api/ai1net/ping","methods":["GET"]}`** ⇒ 规格书「桌面端只认**精确路径**、⛔ 无前缀匹配」这条门槛在代码层被真实满足 - ✅ **封包契约门禁 rc=0**(零顶层 import/export/require + 走 `__ModuleLoader__` + 只交 `apply`+`inject` + 零 `exports.default`;host 半另过「零顶层 import / 逐字 inject / 代码零 `webServer`」) - ✅ **负控(本棒另做)**:S0 的 `v-neg.sh` 靶子固定为旧插件 ⇒ 只证明"尺子能变红"、**没证明"本包的绿不是空绿"** ⇒ 三面单变量改坏(装配删 bundles 行/取数去掉 `inject` 的 `connection`/回调去掉 `ctx.on`)**全红**,改回**全绿** **⚠️ 如实标注「未取到 / 不成立 / 刻意不做」**(⛔ 一律不折算成"已知") 1. **浏览器级 `v-ui.sh` rc=3**:CDP 9330 不可达 ⇒ 本机无真壳;启动器属**客户端线**,本线 ⛔ 不代启 ⇒ 规格书 §3 的 S2 完成判据("两端都出卡片 + JS 错误 0")**尚未达成**,留待客户端线窗口内补 2. **`v-plugin.sh 运行` 面不成立**:可用启动日志 mtime = **09-25**,**早于本包存在** ⇒ "无失败条目"是**空真**,⛔ 不当证据 3. **真机投放(两端各起一次)刻意不做** = 硬边界「⛔ 不碰起实例/取身份/把包装进去」 **🔴 "两端"的口径(值得记)**:**不是两套机制** —— 两端装配键本就是同一个 `dsh.profile.bundles`(客户端 `…/profiles/desktop` / 平台侧 `/home/profiles/web`,`plugin-assembly.ts:72/75/860`)。沙箱做法:profile 用副本/按键位手写,`node_modules/@dsh-local/ai1net` 用**指向真包的软链**(= 平台侧 `link:` 的落地形态,⛔ 非复制)⇒ 尺子量到的**就是真产物**。⛔ 沙箱 ≠ 真机已装好。 **设计取舍(自决)**:全量组装已跑通 rc=0(4 文件+逐文件 sha256)但**刻意不留 `dist/`** —— `lib/` 与 `dist/lib/` 同内容两副本=**第二真相源**(门禁脚本自己的文件头就警告这件事),分发产物的形状应由 **S4** 定;一条命令即可重建。 **机制层观察(⛔ 未改任何跨库脚本 —— 改它属机制层独占)** - 🔴 **`preflight-lock.sh` 对 7 个目标路径全判【D】⇒ rc=1 拒开工**:本棒目标(含**工作区外的全新仓库目录**)压根不在其 `ok_files` 覆盖内,算法对它们取"末两段"作域键 ⇒ 按第 26–40 棒惯例**自定 10 个域键**后开工 - 🔴 **非 ASCII 会话名 ⇒ 锁目录名被改写**:`.locks/插件投放与分库线-40` 实际落成 `.locks/________________________-40-2147666670`(非 ASCII 段→下划线+`cksum` 后缀)⇒ **后续棒别按会话名猜目录,用 `ls .locks/` 找** - **抢锁"校验结果不看输出"**:复读 `OWNER` + `DOMAINS` 断言归属(⛔ 不用 `| grep` 截尾 —— 会吃掉退出码,也吃掉"抢不到") - §0 沿革:第 39 棒只在 §2 留块、**未在 §0 留行** ⇒ 第 40 棒的 §0 行直接承第 38 棒 **硬边界自查**:⛔ 未 commit/push/ssh/部署;R2 未触碰;⛔ 未动客户端工作区 `dsh-ai1net-desktop` / `dsh-desktop-0.1.7rc2`(端 A 用 profile **副本**,真件 mtime 未变);🔴 红线门禁四项**均未触发**(新增 6 文件全在新包内);⛔ 未跨 S4/S9 停点。 **接续**:已登记第 41 棒 automation **`9952577d-9a04-47e9-887e-23068508d1ae`**(一次性 · `2026-09-26T14:30` · cwds = 本工作区)= **执行棒 · S3 数据面**(声明数据面 ⇒ 先建库 `dshs_pl_ai1net` ⇒ 迁移 ⇒ 对账 ⇒ 回读;前置 = 本棒 S2 骨架已落地)。⛔ 第 42 棒及以后未预登记。**记录件**:`交付物/新包骨架-S2落地与四尺读数-20260926.md`。 --- ## 插件投放与分库线 · 第 41 棒(执行棒 · S3「数据面」)· 14:35–14:5x **本棒结果**:🟢 **建库 → 迁移 → 对账三轮全绿** + **负控命中**;⚠️ 浏览器级 `v-ui.sh` **未取到**(rc=3,需客户端线起真壳)。 **包内(S3-1 声明数据面)**:`E:/ProgramData/AIProject/dsh-plugin-ai1net/` 新增 `dsh.data.yaml`(**8 表 / 29 列 / 5 模块段** `tenant·model·plugin·net·im`)+ `lib/data-limits.js`(**列级 maxBytes 的唯一把关人**,`COLUMN_MAX_BYTES=16384`;零 DB 动作);改 `package.json`(🔴 `dsh.data.schema` **显式指向** `./dsh.data.yaml` —— 平台端**不自动探测**;`files` 补声明件;`exports["./data-limits"]`)/`scripts/build.mjs`(**新增门禁 ①d**)/`README.md`。 🔴 **`files` 与 build.mjs 的组装清单是两处**,漏任一处 = "分发出去少了声明件"的静默故障 ⇒ 已两处同改。 **平台侧(S3-2/3/4)**:复用 `/opt/dshs/lib/db/plugin-data/{schema,datastore,diff}.js`(**同一批函数**,与门户 `POST /api/plugins/business/datastore*` 同源)写执行器 `交付物/平台侧-插件数据面执行器-20260926.mjs`。 - **建库**:`CREATE DATABASE dshs_pl_ai1net OWNER dshs ENCODING UTF8 TEMPLATE template0` ⇒ created(**14 字符**,匹配 `^dshs_pl_[a-z][a-z0-9_]{0,40}$`);台账 `state=created`。 - **迁移**:单入口按模块分步 M1→M5(**每模块独立事务**),**0 失败**;`exists=true schemaVersion=1 表=9/8`(9 = 8 声明表 + 内核 `p_meta_schema`);台账 `state=ready`、`plan_hash` **保留**(COALESCE 语义)。 - **对账**:`pg_database` 3(`dshs_pl_ai1net` + mcn-suite + storyforge)× 台账 3 ⇒ **差集双空**。 - **单模块回滚演练**(额外取证):事务内 `ALTER TABLE … RENAME` → `ROLLBACK` ⇒ 表集合**逐字复原**(零残留)⇒ 证明"每模块可单独回滚"(表名分段使然)。 **负控(S3-5 · 本棒取证核心)**:`model_configs.params_json` 的 `maxBytes: 8192 → 16384`(**刻意选"仍在常量上限内"的值**)且**不回读登记** ⇒ 平台 `readback` **rc=1 红**(快照 `b46b4983…` ≠ 现读 `2b28a76b…`),而**包内门禁仍绿** ⇒ 证明 **① 回读真的在起作用 ② 包内门禁不能替代回读**;改回 ⇒ rc=0 绿,`dsh.data.yaml` sha256 **逐字还原** `460c27d1…6350`。 **两处自纠(如实)**:新写的包内门禁扫描器首版有 2 个缺陷 —— ① **索引行被当列**(6 空格缩进的流式映射有列行/索引行两种)⇒ 报 **34** 列 ② **`default: "{}"` 被 `[^}]*` 截断** ⇒ 报 **26** 列;均已当场修好并复验 **29 列**(与平台解析**逐字一致**)。教训:**"包内自检"与"平台解析"是两个层次**,数字对不上先怀疑自检。 **尺子回归**(⛔ 未改尺子一行):装配 端A `rc=0`/端B `rc=0` | 取数 `rc=0` | 回调 `rc=0` | `v-baseline` `rc=0`(平台 4 包/客户端 4 bundle,与 40 棒对照一致)。 **未取到 / 不成立(⛔ 不折算成"已知")**:① **`v-ui.sh` rc=3**(CDP 9330 不可达;启动器属**客户端线**,本线不代启)② `v-plugin.sh 运行` **仍不成立**(可用日志 mtime 09-25,**早于本包存在** ⇒ 空真)③ **真机投放刻意不做**(硬边界;属 S4)④ 本棒**绕过门户链路**(门户要求包先进候选池从 **tgz** 解析;本棒 ⛔ 不投放 ⇒ 用**同一份解析器**从**包目录**解析)⇒ ⛔ 不等于"门户链路已通"。 **发现的文档偏差(⛔ 未擅改)**:规格书 §五 A4 / 决策点 D1 写库名"**15 字符**",实测 **14**(`dshs_pl_`8 + `ai1net`6)。纯笔误、不影响判据;规格书属**冻结规格**且在文档库 ⇒ 留给文档棒一支笔。 **硬边界自查**:⛔ 未 commit/push;R2 未触碰;⛔ 未动客户端工作区(端A 用 `tmp/p40/` 的**副本**);包内**零 DB 动作**(红线 grep 零命中);🔴 红线门禁**四项均未触发**(未重启控制面/未中断用户/改动 **9** 文件 <10/库可回滚 ⛔ 未 DROP);平台侧口令**未落盘未打印**。建库属**生产变更**(47 = 开发环境服务器)⇒ 按 **R8**「动手前一句话说明」处理。 **记录件**:`交付物/数据面-S3落地与三轮回执-20260926.md`;**平台侧执行器**:`交付物/平台侧-插件数据面执行器-20260926.mjs`;远端临时件 `/tmp/p41/`。 **接续**:第 42 棒 = **执行棒 · S4「接进既有的分发与更新机制」**(照方案 §3.10.6;**P1 已于 09-26 13:30 拍板 = A,不再阻塞**)。 --- ## 🔵 平台线(插件投放与分库线)|15:0x–15:2x 第 42 棒 · 执行棒 · S4「接进既有的分发与更新机制」收官 **结论**:包能被平台**对账 + 增量拉取 + 原子换上**,落地后**两端指纹逐字一致**;**三条反例判据全过**。**⛔ 一行都没重造** —— 拉取器是**照抄**权威实现(`src/worker/agent.ts:241-733` 的 `hashTreeAt`/`pullOnePack`/`syncSharedLayerOnce` + `src/supervisor/plugin-assembly.ts:1111-1365` 的软链自足,共 11 段逐字;沙箱 Manager 照抄 `business-plugins.ts` 的 `hashTree`/`sharedLayerDiff`/产物口打包口径/对账端点口径)。**包内零改动**。 **🔴 本棒最强的一条证据 = 真机锚点**:`ssh 47 'tar -czf - -C /var/lib/dshs/bundled-plugins '`(**与产物口逐字同口径**)⇒ 本份 `hashTreeAt` 与真机台账 `.manifest.json` 的 `treeSha256` **两包都逐字相同**(`_dsh-local_storyforge` 117 文件 · `dsh-plugin-mcn-suite` 659 文件)⇒ 把"照抄"从**看起来一样**变成**算出来一样**。 **判据**:四步 happy path `rc=0`(对账 → 取包 → 落地**原子换六动作** → 回报;第二轮产物口零调用 ⇒ 不重拉)|S4-3 两端 `link:` 字符串**逐字相同**且指向**拉下来的那一份**|S4-4 `linked=1`/`kept=3`/`relinked=1`(负控)|尺子 装配端A/端B/取数×2/回调/`v-baseline` **全绿**+包内门禁 `rc=0`|S4 编排 **53 ✅ / 0 ❌**。 **三条反例(逐条实测)**:① 清单拿不到(黑洞服务永不响应)⇒ `ok=false` + **本地零改动**(全树强留痕 `size+mtime+内容sha256+软链目标` 前后逐字一致,31 项)② 单包失败(错误页打进产物口 ⇒ `pack_not_gzip`)⇒ **主包照常更新**、失败只记一项、`attempts=1`(永久性不重试)、备包旧版零改动、已回报;**加演**:坏体换**截断 gzip** ⇒ 判**瞬态** ⇒ `attempts=3`(**真的重试**)③ `extra` ⇒ 平台只告知、目录**仍在且零改动**、⛔ 不进 `pull` 清单。 **⚠️ 未取到**:S4-6 实例页「能力管理」栏 + `v-ui.sh`(`rc=3`)——**均需真壳**,启动器属**客户端线**,本线 ⛔ 不代启|`v-plugin.sh 运行` 仍**空真**(日志 mtime 09-25 早于本包)|**真机投放刻意不做**(硬边界)。 **🔴 两处自纠(教训值得留)**: 1. **首版负控无效** —— 写的是"去掉排序 ⇒ 应不等",实测该 fixture 只有 5 个文件,`readdirSync` 顺序恰等于排序序 ⇒ **区分不出**。已换成 §4-C1 点名的**真近似物**(不归一化 `\` / 只哈希文件名清单)⇒ 均能变红。**教训:负控要挑「真会翻车的写法」,不是随便改一刀。** 2. **辅助动作污染判据退出码** —— 解包暂存树带 659 文件,本机**安全删除护栏**拦"一次删 >50 文件"⇒ `rmSync` 抛错 ⇒ 脚本在 `equal:true` 之后仍 `rc=1`。已把清场改成 **best-effort(退出码只由 `equal` 决定)**。**教训:辅助动作(清场/日志/回报)不许影响判据的退出码。** 3. **一次误读已被纠正** —— 护栏那次拦截实际**已部分删除**暂存树 ⇒ 事后"复用旧素材复算"算出 415 文件/指纹不符;重解包后恢复 `equal:true`。**教训:复用旧素材前先确认素材没被动过。** **建模 vs 真实(逐条记在记录件 §八)**:真实 = 指纹算法(靠真机锚点)· flat/`link:` 口径 · 软链目标形状;**建模** = 鉴权(只比固定令牌,真平台查 `dsh_hosts.agent_token`)· 版本/通道/钉版(沙箱**只做现役一版**,⛔ 未覆盖 B-1/B-2/B-3)· 审计落库 · 传输(回环 vs 公网/relay)⇒ **只能断言"照抄的那段逻辑在既有口径下成立",⛔ 不能断言"真机投放已通/门户链路已通"**。 **机制层观察(⛔ 未改任何跨库脚本)**:① `preflight-lock.sh` 对本棒 4 个目标路径**全判【D】**⇒ 按第 26–41 棒惯例自定 4 个域键后开工|② 非 ASCII 会话名 ⇒ 锁目录实落 `________________________-42-552621281`,**别按会话名猜**,复读 `OWNER`/`DOMAINS` 断言归属(⛔ 不用 `| grep` 截尾)|③ **本机安全删除护栏会拦"一次删 >50 文件"**(`SAFE_DELETE_BULK_CONFIRM_REQUIRED`)—— 属**用户级护栏**,⛔ 未绕过,`tmp/p42/real-anchor/_x-*` 暂存树**保留未删**,随 `tmp/` 7 天口径处理。 **记录件**:`交付物/分发与更新-S4落地与三轮回执-20260926.md`(逐条原始输出 · 坑 C1–C6 对表 · 建模与真实逐条标注 · 未取到项 · 回滚)。 **读数**:`tmp/p42/读数-20260926.txt`(S4 编排 53 ✅/0 ❌)· `tmp/p42/尺读数-S4-20260926.txt` · `tmp/p42/真机锚点-20260926.txt` · `tmp/p42/real-anchor/`(真机包)。 **接续**:第 43 棒 = **执行棒 · S5「第一块迁移:IM 插件侧」**(照方案 §3.10.7 —— ⛔ **不是 `git mv`**,要按 IM 的接入方式**重写**进包 ⇒ 四条面「装配/运行/取数/回调」逐条复现)。⚠️ **S4-6 仍欠一笔真壳取证**。 --- ## 第 43 棒(执行棒)· S5「第一块迁移:IM 插件侧」· 2026-09-26 16:1x 收官 **结论**:IM 插件侧整块进了包 `@dsh-local/ai1net` —— **三条精确路由**(`/api/ai1net/im/rooms` · `messages` · `longpoll`)+ 插件侧 SDK(含**同进程 `core` 模式**);**两条残留的补法都落在包内代码里**。 **做法**:内核 `lib/im-plugin-sdk.mjs`(**新建** 47,964 B)—— 上游 `sdk/im-plugin-host/index.mjs` 的两个 env 名 / 两个头名 / `SERVER_POLL_HOLD_MS=20000` / 11 个方法**逐条照抄**;平台面 8 条路径取自 `src/web/routes/im.ts`;`pluginIdOf` 逐字照抄。 **🔴 最强证据 = S5-4 回归**:旧 SDK → 平台面 vs 新 SDK → **包面**(真实 `apply(ctx)`)→ 平台面,**同输入 13 行平台侧效果流水逐行逐字符相同** + 顶层语义 14 项一致。⇒ **"照 IM 重写"从"看起来一样"变成"跑出来一样"。** **两条残留**: 1. **每插件令牌**(残留①)—— 令牌把 `(实例, 包名)` **签死**;🔴 **三态凭据语义**:`undefined`(**没带**身份头)= 本包自己 ⇒ 用本机 env;`''`(**带了**身份头却**没带**凭据)⇒ **必拒** ⇒ 杜绝「包替任何插件签身份」(**首版真有这个漏洞**)。负控 7 条:未启用/借令牌/伪造签名 ⇒ 拒 **且平台零调用**;**正控**正确令牌 ⇒ 200 ⇒ 拒不是空拒。 2. **实例级单例长轮询**(残留②)—— `ResidentLongPollGate`:同插件**幂等**(热重启自愈)/异插件 **409 且回 holder**;负控「故意起两条」第二条**真的没起**(`polls=0`)。 **判据**:包内门禁 `rc=0`(新增 **①e M5 门禁**,`M5_DIGEST=55cdc0d5fc8eb99d`;`DECL_DIGEST` 8 表 29 列**未破**)|装配 端A/端B `rc=0`|取数 `rc=0`|回调 `rc=0`(**spy 抓到 4 条精确路由原文**)|`v-neg all` `rc=0`|`v-baseline` `rc=0`|S5 编排 **76 ✅ / 0 ❌** ⇒ **S2/S3/S4 的绿没被弄红**。 **⚠️ 未取到/不成立**:`v-ui.sh` **`rc=3`**(需真壳,启动器属**客户端线**,本线 ⛔ 不代启)|**M5 卡片同欠**|`v-plugin.sh 运行` `rc=0` 但**"空真"**(日志 mtime 09-25 **早于本包**)|**真机投放刻意不做**。 **⚠️ 六条自纠(教训值得留)**: 1. **body 未序列化** —— `fetch` 收到对象 ⇒ 变 `"[object Object]"` ⇒ 平台回 `bad-manifest`。**教训:SDK 里统一 `JSON.stringify`。** 2. **读接口缺 `ok` 字段** —— `/api/im/rooms` 回 `{rooms,count,mode,meId}` 没有 `ok` ⇒ `reply()` 把"缺 ok"当失败 ⇒ **误报 502**。**教训:判失败用 `out.ok===false`,⛔ 不是 `!out.ok`。** 3. **负控打错面 ⇒ 假绿** —— **会话面**(`/api/im/rooms`)走 `imAuth`,**没有**桥身份闸 ⇒ 拿它测令牌闸什么也测不出。改打**出向** `POST /api/ai1net/im/messages`(**桥端点**)。**教训:测哪道闸就打到哪道闸上。** 4. **回归比对被前序测试污染**(`A=2 B=3`)—— 只有一侧复位 ⇒ 前测动过的房间被当成"迁前迁后差异"。加 `resetSeed()` **两侧各调一次**。**教训:比对类判据必须两边同起点。** 5. **`pluginId` 口径混用** —— `pluginIdOf` 的下划线形态 vs 头里的**包名**,闸键与身份头对不上 ⇒ 负控"通过"但**原因是错的**。统一为**包名**。 6. **门禁"数文本"≠"注册数"** —— 注册写在 `for` 循环里 ⇒ 文本计数 2 ≠ 实际 4。门禁升级为**运行时真挂一次再断言**(spy ctx 捕获 `seen[]`)。**教训:静态计数会被循环骗,真跑一遍最实在。** **建模 vs 真实(记录件 §六)**:**真实** = **包内那一半**(凭据解析/出示/本机 fail-closed 拦截/单例闸);**建模** = **平台侧那一半(令牌签发 + 桥端点校验 + 启用集校验)—— 真实 `im.ts` 里尚无这道闸**(= 残留① 的本体,属**投放线待落地**)· 会话面 `imAuth` · 桥传输 · `bridge/pull` 投递 ⇒ **只能断言"包内那套逻辑在既有桥口径下成立",⛔ 不能断言"真机投放已通"**。 **机制层观察(⛔ 未改任何跨库脚本)**:`preflight-lock.sh` 对本棒目标**全判【D】**⇒ 按第 26–42 棒惯例自定 10 个域键后开工;非 ASCII 会话名 ⇒ 锁目录实落 `________________________-43-4075394365`(**别按会话名猜**,复读 `OWNER`/`DOMAINS` 断言归属)。 **记录件**:`交付物/IM插件侧-S5落地与四轮回执-20260926.md`(§二 照抄对照表 · §三/§四 两条残留负控 · §六 真实 vs 建模 · §七 §4 坑对表 · §九 六条自纠)。 **读数**:`tmp/p43/读数-20260926.txt`(**76 ✅ / 0 ❌**)· `tmp/p43/尺读数-S5-20260926.txt` · `tmp/p43/{platform-mock,run-s5}.mjs` · `tmp/p43/run-scales.sh`。 **接续**:第 44 棒 = **执行棒 · S6「第二块:技能插件管理」**(automation `47f6921c-6a98-4c60-be37-0f93a1a9b27d` · `2026-09-26T16:23`)。⚠️ **M5 卡片 + S4-6 仍欠一笔真壳取证**。 --- ## 第 44 棒 · 执行棒 · S6「第二块:技能插件管理」(2026-09-26 16:2x–17:0x · 会话 `插件投放与分库线-44`) **结论**:S6 四条判据全绿;⚠️ 窄屏真壳截图未取到(`v-ui.sh` rc=3,需真壳,本线 ⛔ 不代启)。 **产出(包内 5 文件)**:**新建** `lib/plugins-sdk.mjs`(24,070 B,M3 内核:七条路由 + `UPDATE_SPLIT` 四拆契约表 + `describeOwnership()` + `diffOfSharedStatus()` 三态聚合 + 复用 M5 抽出的 `createHostKit`)|改 `lib/index.js`(七条精确路由注册 + `statusOfReason` 具名状态码映射)/`lib/client.js`(`plugins.item` 槽卡片 order=80 + **≤900px 独占**样式段)/`scripts/build.mjs`(**新增门禁 ①f** + 第 ⑩ 条包名形态)/`package.json`(`exports["./plugins-sdk"]`)。 **四条判据**:① 上传/检测/建库/启用进包 ⇒ 装配 端A/端B·取数·回调·运行 全 `rc=0` ② 四拆逐条具名(`package` ×2 / `s4-puller` / `bootstrap-layer`)+ 机验「`executor:true` ⇒ `inPackage:false`」+「包内零搬包实现」 ③ `dsh.data.schema` 指向 yaml + 零自增 id + 窄屏机械判据全绿(**截图未取到**) ④ 负控:执行者在位 `pulled=2`/关掉 ⇒ 本地零包目录 + 取包口 0 次调用(而包内台账记 `updateRequests=1` ⇒ ⛔ 非空绿)。 **🔴 抓到并修掉一处真 bug(本棒最强收益)**:`PKG_RE` 首版 `/^[a-z0-9][a-z0-9._-]*$/i` **不收带 scope 的包名**,而规格书 §五 A4 冻结包名 = `@dsh-local/ai1net` ⇒ **`detect`/`db`/`enable`/`update-request` 等 5 条走 `pkgOf()` 的路由全线静默 `bad-args`(一个平台请求都不发)**。修 = 加可选 scope 段(⛔ 仍拒 `../` 穿越);防复发 = 门禁 **①f-⑩**(读 `package.json name` + 从源码取 `PKG_RE` 字面量真跑:**4 合法全过 / 7 非法全拒**),并把包名与正则字面量喂进 `M3_DIGEST`。⚠️ 另一处**自家脚本**的坑(不是包的问题):编排脚本记账窗口取在 promise 创建之后,而 `guarded()` **同步**走到 `callPlatform` ⇒ 九条全报零请求(假红),改传 thunk 后正确。 **读数**:包内门禁 **60 ✅ / 0 ❌**(`M5_DIGEST=55cdc0d5fc8eb99d` **逐字未变** / `M3_DIGEST=7045a778baa82cfe` / `DECL_DIGEST=88c752a3…` 8 表 29 列未破)|S6 取证编排 **58 ✅ / 0 ❌**|`v-neg all` `rc=0`|⚠️ `v-baseline` 平台侧 `rc=255`(`ssh 47` 瞬时连不上;该项**不进判据**)。 **记录件**:`交付物/技能插件管理-S6落地与四轮回执-20260926.md`(§三「谁在做」表 · §六 真实 vs 建模逐条 · §九 真 bug 始末)。 **编排**:`tmp/p44/run-s6.mjs`(+ `tmp/p44/run-scales.sh`/`rd.py`);真执行者复用 `tmp/p42/puller.mjs` + `tmp/p42/mgr-mock.mjs`(⛔ 未改一行)。 **机制层观察(⛔ 未改任何跨库脚本)**:`preflight-lock.sh` 对本棒目标**全判【D】**⇒ 按第 26–43 棒惯例自定 9 个域键后开工;非 ASCII 会话名 ⇒ 锁目录实落 `________________________-44-3307662151`(**别按会话名猜**,复读 `OWNER`/`DOMAINS` 断言归属)。 **关键口径(本棒钉住,后续棒继承)**:`detect` 只覆盖**数据面声明校验**(`/datastore` 的 `dryRun`),**安全/兼容两项在 `upload` 的响应里** ⇒ 返回值 `covers:'dataplane-declaration'`,⛔ 不许让前端以为三项都重跑了|`migrate` 的 `expect` **必填且包内不生成**(生成 = 替用户确认改动)|「设置自动处理」**复用同一条路由**用 `role` 分流(规格书冻结清单只有七条,⛔ 不许加第八条)。 **接续**:第 45 棒 = **执行棒 · S7「第三块:模型管理」**(automation 见 §0 最新行)。 --- ## 第 45 棒 · 执行棒 · S7「第三块:模型管理」(2026-09-26 晚) **结论**:S7 四条判据**全绿**;**未发现新停点以外的功能缺陷**(产品侧真 bug = **0**,5 处全是**自家仪器**写错)。⚠️ **但本棒撞上一条方案硬门 ⇒ 未登记下一棒**(见「接续」)。 **做了什么**:新建包内内核 `lib/models-sdk.mjs`(32,634 B · 零外部 import · 零 `fs`)—— 三条精确路由(`/api/ai1net/models` GET/PUT / `/credentials` GET/POST/DELETE / `/models/grant` POST)+六种方法冻结表;客户端加第四张卡(`ModelsCard` / `ModelsBody` + `@media(max-width:900px)` 移动端 CSS);门禁加 **①g(十一件职责)** `M2_DIGEST`;`exports` 加 `./models-sdk`。 **四条判据回执**:① 读/写真发(**12 条该拒就拒且零平台请求**,其中 3 条凭据闸)② 凭据三方法(`createCredential` 是**唯一碰明文口**,回包只挑元数据重造)③ 共享模型两列两主体(`shared_model_granted` 管理员 ⇄ `shared_model_enabled` 用户偏好 = **与**关系;非管理员 ⇒ `not-admin` **零平台请求**)④ 负控:撤掉授权 ⇒ 同代码只差授权位,`granted` false/true 立刻翻转。 **出了一次要命的「仪器错」(值得记)**:`run-s7.mjs` 首版 **24 项假红** —— 令牌 `exp` 写死 `1774000000000`(≈2026-03-20),而 host 半内部走**默认时钟、无注入点** ⇒ 全部令牌已过期 ⇒ 正常路径全被闸拒成 `plugin-not-enabled`。**识破线索 = 读数里 `出向请求总数 = 0`**(该发的一个都没发)。⇒ **教训:凡「正常路径全拒」,先看出向计数是不是 0,别先改被测代码。** 另修两处**自家的坑**:`redactModelConfig()` **先判白名单** ⇒ 敏感字段被静默丢掉、净化计数恒 0(**看着干净、但没有证据**)⇒ 改成**先判敏感字段名再判白名单**,并加门禁「喂明文必须计到净化数」防复发;门禁 fs 扫描认**字符串** ⇒ `HOME_WRITE_POLICY.forbidden` 自己的 `'node:fs'` 字面量把门禁**假红** ⇒ 改认**用法**(`fsUsageRe`)。 **读数**:包内门禁 **85 ✅ / 0 ❌** `rc=0`(S6 时 60 ✅)|门禁**负控 8/8**(7 处改坏全被抓)|S7 编排 **73 ✅ / 0 ❌**|回归锚点**逐字未变**:`M5_DIGEST=55cdc0d5fc8eb99d` · `M3_DIGEST=7045a778baa82cfe` · `DECL_DIGEST=88c752a3e12597b3`;新增 `M2_DIGEST=11d7b1be8aea3996`|装配/取数/回调/运行 全 `rc=0`|`v-neg all` `rc=0`|⚠️ `v-ui.sh` **`rc=3`**(`no page target`,需真壳在跑,本线⛔ 不代启 ⇒ 如实记「未取到」)|`v-baseline` `rc=0`。 **记录件**:`交付物/模型管理-S7落地与四轮回执-20260926.md`(§六 真实 vs 建模逐条 · §九 5 处仪器错)。 **编排**:`tmp/p45/{run-s7.mjs,neg-gate.mjs,run-scales.sh}` + 三份读数。 **🆕 新停点(本棒撞上的硬门 · ⏸ 等拍板)**:方案 §3.10.10 **给 S8 写死硬门**「**F1.5(G1 签名清单序号 + G6 撤根)必须先有结论**」,而现状 **G1 未修、G6 无任何机制**(Grep 到 09-26 两份交付物均写「F1.5 = 一切前置」)。⇒ 两条候选:**A 案**(先补 F1.5 前提,再开 S8;优点 = 不违方案硬门、不埋返工;缺点 = S8 推后)/**B 案**(现在只做 S8 包内那一半;优点 = 进度不停;缺点 = **违反方案自己写的硬门**,F1.5 结论若推翻覆盖面 ⇒ 可能白做)。倾向 **A 案**。 **接续**:⏸ **本线暂停排棒(等拍板)** —— ⛔ **未登记 automation**(纪律:本棒发现新停点 ⇒ 不登记下一棒)。拍板到手后再重建第 46 棒(S8「第四块:覆盖网络界面」)。**锁已释放**(`--release-exec "插件投放与分库线-45"` ⇒ `.locks/` 与 `.exec-lock/` 均空、无 `OWNER`)。 --- ## 拍板与第 46 棒登记(2026-09-26 19:1x) **用户拍板**:第 45 棒提出的停点 ⇒ 选 **A 案 = 先补前提(撤根 + 序号两条机制定下来),再开 S8「覆盖网络界面」**。B 案(现在就做包内那一半)未采纳。⇒ 第 45 棒那条「⏸ 本线暂停排棒(等拍板)」**解除**。 **🔴 登记时撞到一条此前没注意的硬规则(值得记)**:方案 **§6.0(2026-09-26 13:19 立)= 第 0 号停点「用户确认门」** —— 本线棒分两类:**规划棒可自动开跑**;**执行棒一律先交用户确认、⛔ 不许自动登记**。⇒ 所以**下一棒只能登记「规划棒」**;S8(覆盖网络界面)是**执行棒**,⛔ 不许自动带跑。 (依据用户原话:「现在只做方案 落地详细步骤和方案做好了 确认后在执行」;落地三条 = 规划棒收官 ⛔ 不登记执行棒 / 用户确认后才由用户或新会话立执行棒 / 执行棒 prompt 里写「范围以用户确认过的口径为准」。) **已登记**:**第 46 棒 = 规划棒 · F1.5「签名清单防重放序号(治 G1)+ 撤根机制(治 G6)」出结论** automation `977bcfb8-55de-4c92-b3e9-b7b51a4becfe`(一次性 · `2026-09-26T19:19` · `cwds = E:/ProgramData/AIProject/aliyun-dsh-server`) **本棒三件产出(规划棒 · ⛔ 不改码 / 不部署 / 不 ssh)**:① **结论件**(G1 / G6 **各**给:数据模型 / 判定规则 / 与既有字段关系 / 兼容与回滚 / 落地影响面)② **复现实验方案**(定稿 §九-1 明写「G1 重放未实测」=读码推断,落地前须构造真实重放实验;本棒只出方案,跑实验属执行棒)③ **交接单(8 段)**。 **定稿口径(唯一权威)**:`dsh-server-docs/02-架构设计/覆盖网络-顶层架构全貌.md` - **G1** = `SignerSet` / `NodeGrant` **只有 `issuedAt`、没有序号**,而 `issuedAt` 只参与签名、**不参与判定**(唯一被读处是「签发时刻容差」,只查"太新")⇒ **旧名单重放验签会过**(签名本身是真的)。今天没爆是因为**名单由管理员下发、节点不主动拉**。**修法 = 加 `serial: number` + 节点持久化「已见最大序号」,`≤` 则拒;⛔ 不用 `issuedAt` 替代 —— 时钟不可信。必须在区域名录之前修**(名录本身就是待下发的签名清单,会成为重放载体)。 - **G6** = 吊销清单只能撤**节点**;**撤签名者靠"根签一份新名单",而撤根本身无任何机制**。多根下**一把根沦陷无法被作废**(只能逐台改配置重新下发),且 **any-of-N 语义下「被撤那把」若仍留在任一节点的受信清单里就仍然有效**。🔴 **且与 G1 叠加**:撤根的唯一补救是换新名单,而名单无序号 ⇒ 「换新名单」这条路本身也不可靠 ⇒ **G1 与 G6 必须一起解决**。 - **§七 推进顺序**:F1.5 前置 = 无;**F2(区域名录)/F5(跨区级联吊销)/F8(根集合治理 + 升格)都依赖 F1.5**。🔴 一句话 = 「**F1.5 是一切的前置**」。 - **§九-1**:G1 重放**未实测**(读码推断);**§九-7**:定稿所有「现有实现」结论均来自**读码**,⛔ 尚未全部真机验证。 **入口已推进**:§0 新增第 46 棒行(第 29 行改为「第 45 棒 → 用户拍板 A 案 ⇒ 第 46 棒」)|§2 新增「🎯 本轮动作(第 46 棒)」块 + 原第 45 棒块降级「🗃️ …仅供追溯」|原「⏳ 等拍板」块改「✅ 已拍板」并留档候选。 **锁**:为写入口临时抢过域锁(域 = 入口文件 + `.workbuddy`),**已释放**(`.locks/` 与 `.exec-lock/` 均空、无 `OWNER`)。 **⚠️ 归属提醒(本棒判断,可推翻)**:F1.5 的**题材**属**覆盖网络线**(定稿在那边),但其**触发**是插件线 S8 的硬门,且只有插件线有「规划棒可自动开跑」的授权 ⇒ 本棒**登记在插件线**。若后续想归位,只需在覆盖网络线入口加一条**指针**(⛔ 不复制正文)。⚠️ 覆盖网络线入口 §2 现状仍是「下一棒 = 无(本线可停)」,本次**未动**它。 **接续**:第 46 棒已登记、约 5~8 分钟后(19:19)自动开新会话;**接续点 = 见 §2「🎯 本轮动作(第 46 棒)」块**。⛔ S8 仍须等 F1.5 结论 **+ 用户确认**才可立棒。