Files
dsh_ai1net_server/交付物/IM插件侧-S5落地与四轮回执-20260926.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

20 KiB
Raw Permalink Blame History

S5「第一块迁移:IM 插件侧」· 落地与回执(2026-09-26 · 第 43 棒)

执行依据:本线入口 接续入口_插件投放与分库线_20260922.md §2 第 42 棒块「本线下一项」|方案 交付物/功能打包到基础插件-分步实施方案-20260926.md §3.10.7 + §4 避坑总表|规格书 D:/github/dsh_shenxian/dsh-server-docs/05-交接单/交接单_包规格书_20260926.md §〇/§五 A/§五 C。 会话:插件投放与分库线-43(域锁持有者已复读断言)。 读数:tmp/p43/读数-20260926.txt(76 ✅ / 0 ❌)· tmp/p43/尺读数-S5-20260926.txt。


一、一句话结论

IM 插件侧已按「照 IM 的接入方式重写」的方式整块进了包 —— 包 @dsh-local/ai1net 现在自带三条精确路由(/api/ai1net/im/rooms · messages · longpoll)+ IM 插件侧 SDK(含同进程 core 模式),并把两条残留补在了代码里:每插件令牌(fail-closed) 与 实例级单例长轮询闸。

🔴 最强的一条证据 = 迁移前后「同一组输入 ⇒ 平台侧效果流水逐行完全相同」(A=13 行 / B=13 行,逐行逐字符相同)—— 旧 SDK 直连平台面、新 SDK 走包面转平台面,两条路对同一个平台产生了字面一样的行为。

⚠️ 但必须与「已通」严格分开(详见 §六):包内那一半是真实代码(凭据解析 / 出示 / 本机 fail-closed 拦截 / 单例闸);平台侧那一半(令牌签发 + 桥端点校验 + 启用集校验)本棒是沙箱建模,真实 im.ts 里尚未实现 ⇒ 属投放线待落地。⛔ 不能据此断言「真机投放已通」。


二、S5-1「照抄」逐段对照表(⛔ 判据 = 与上游对得上)

段 上游权威(只读照抄来源) 本包落点 是否逐字/同口径
环境变量名 sdk/im-plugin-host/index.mjs:DSH_PLATFORM_IM_URL / DSH_IM_INSTANCE_TOKEN lib/im-plugin-sdk.mjs ENV_BASE_URL / ENV_INSTANCE_TOKEN ✅ 逐字(机验 ⑦)
插件身份头 src/web/routes/im.ts:291:x-dsh-im-plugin-id(值 = 包名) PLUGIN_ID_HEADER ✅ 逐字(机验 ⑦)
实例令牌头 同上:x-dsh-im-instance-token INSTANCE_TOKEN_HEADER ✅ 逐字(机验 ⑦)
长轮询硬上限 SDK: SERVER_POLL_HOLD_MS = 20_000 SERVER_POLL_HOLD_MS = 20_000 ✅ 同值(机验 ⑦)
平台面 8 条路径 plugins/register(341) · unregister(368) · out/frame(382) · out/message(424) · bridge(465) · bridge/pull(484) · bridge/result(504) + im/rooms/rooms/:id/messages PLATFORM_ROUTES(八条,全在 /api/im/ 下) ✅ 同口径(机验 ⑦)
方法面 ready/unregister/onSpeakRule/onEvent/startCallbacks/frame/say/rooms/messages/subscribe/close createAi1netImHost() 同名同形 ✅ 逐条
pluginIdOf 口径 src/im/sdk/host.ts:112(包名 → 下划线形态) 沙箱 pluginIdOf 逐字照抄 ✅ 逐字(仅沙箱侧用;⚠️ 与令牌绑定的"包名"是两个口径,见下)

四条面(装配 / 运行 / 取数 / 回调)逐条复现

面 判据 结果 原始输出位置
装配 端A 客户端形态 + 端B 平台形态 各三条前提成立 ✅ rc=0 ×2 尺读数 §S5-1
运行 靶子未出现于启动失败面 ⚠️ rc=0(空真,见 §五) 尺读数 §运行
取数 真实 import + 真实模块求值 ✅ rc=0 尺读数 §S5-2
回调 真实 apply(ctx),spy ctx 逐字记录 4 条路由 ✅ rc=0 尺读数 §S5-3

🔴 回调面抓到的四条路由(原文):{"path":"/api/ai1net/ping","methods":["GET"]} · {"path":"/api/ai1net/im/rooms","methods":["GET"]} · {"path":"/api/ai1net/im/messages","methods":["GET","POST"]} · {"path":"/api/ai1net/im/longpoll","methods":["GET"]} —— 全部精确路径(⛔ 无通配 / 无参数 / 无尾斜杠),对应 §4-B2「桌面端只支持精确路径」。

M5 功能面(经包内真实路由转发到平台面)

POST kind=register ⇒ 200(出向前提)|GET rooms ⇒ count=2 且只含自己参与的 r1/r2(⛔ 不含别人的 r3)|GET messages?roomId=r1 ⇒ count=2 · cursor=2|POST kind=frame ⇒ 200 且帧不落库(消息数 2→2)|POST kind=message ⇒ seq=3 且真落库|say(r2 非成员) ⇒ outbound-denied|payload 非对象 ⇒ 400 bad-payload(包内先拦,⛔ 不打平台)|kind 非法 ⇒ 400 bad-kind。


三、S5-2 残留①:每插件令牌("插件是否真在该实例启用")

残留原文(交付物/全功能承载判定-底层插件能否承-20260926.md §2.6):「⚠️ 不校验"该插件是否真在该实例启用"(im.ts:305-307 自述:与 /api/im/data/* 同源,现靠逐次审计兜底)⇒ 补法 = 每插件令牌,属投放线。」

落点与语义

  • 令牌形如 p1.<base64url(pluginId\0instanceKey\0exp)>.<hmacSha256(secret, body).base64url.slice(0,32)> ⇒ 🔴 把 (实例, 包名) 签死 ⇒ 借别人的令牌改个身份头不可能(机验:token-plugin-mismatch)。
  • 🔴 三态凭据语义(本棒实测踩出来的关键一条):undefined(没带身份头)= 本包自己 ⇒ 允许用本机 env;''(带了身份头却没带凭据)⇒ 必须拒。⇒ 杜绝「包替任何插件签身份」这条漏洞(首版有,已修,见 §七)。
  • 拒绝口径:plugin-not-enabled → 403;instance-token-rejected / plugin-token-rejected → 401;longpoll-* → 409;platform-not-configured → 503;bad-* → 400;其余 502。

负控(⛔ 判据 = 未启用必须拒,且平台零调用)

# 输入 期望 实测
① 不带令牌 拒 ✅ 403 plugin-not-enabled + 平台侧一次都没被调(calls 保持 8)
② 借别人的令牌 拒 ✅ 403 + 借令牌那次平台侧也零调用
③ 签名伪造 拒 ✅ 包内闸拒 + 平台侧零调用(包内先拦)
④ 令牌合法但不在启用集 拒(由平台侧拒) ✅ 403 plugin-not-enabled,且这条确实打到了平台 ⇒ 证明平台侧那道闸真在起作用(⛔ 不是包内假绿)
⑤ 正控:正确令牌 过 ✅ 200 ⇒ 证明"拒"不是空拒
⑥ 直测平台桥端点(绕开包) — ✅ 实例令牌错 ⇒ 401 instance-token-rejected;无每插件令牌 ⇒ 401 plugin-token-rejected
⑦ 调用方带错的实例令牌 仍 200 ✅ 因为实例令牌由包注入(heads() 始终覆盖),插件不必也不该持它

🔴 一处口径澄清(本棒纠正了一个易混点):每插件令牌闸只作用于桥端点(plugins/* · bridge/* · out/*)。会话面(/api/im/rooms、/rooms/:id/messages)上游走 imAuth,没有这道闸 ⇒ ⛔ 拿会话面测令牌闸会假绿(首版就这么打错过,已改打 POST /api/ai1net/im/messages)。


四、S5-3 残留②:一个实例只允许一条常驻长轮询

残留原文:「⚠️ 一条实例只该有一条常驻长轮询(im.ts:482 原文:⛔ 别每插件一条)—— 直接关系连接额度。」

落点

  • ResidentLongPollGate(实例级单例闸):claim(pluginId, at) ⇒ 同插件幂等(热重启自愈)/异插件拒且回 holder(便于改走同进程分发);release(仅持有者);isOwner;status{holder, since, refusals, idempotentReclaims}。
  • 包内导出单例 instanceLongPollGate ⇒ 同一实例内所有插件共用一把闸(这是"单例"的落点)。

负控(⛔ 判据 = 故意起两条 ⇒ 第二条必须被检出,且真的没起)

# 输入 实测
① 第一条 claim ✅ 200 · holder=@dsh-local/im-plugin-demo
② 第二条(另一插件)claim ✅ 409 longpoll-already-running,且回"是谁持有"
③ 非持有者 poll ✅ 409 longpoll-not-owner(第二条真的没起)
④ 同插件重入 claim ✅ 200 且 idempotent=true(热重启自愈)
⑤ owner poll ⇒ 取工作项 ⇒ 回投 ✅ 1 条 + 200;同 callId 二次回投 ⇒ unknown-call-id(幂等,⛔ 不重复 resolve)
⑥ 持有者 release ⇒ 别的插件 claim ✅ 200(⛔ 不是永久占用)
⑦ 同进程 core 模式复跑 ✅ 第二条 startCallbacks 乐观置位后回滚为 false、一次 poll 都没发(polls=0)、闸记 refusals=1、持有者没被顶掉

五、S5-4 回归:迁移前后同输入 ⇒ 同行为

做法:scenario(host) 刻意不用定时器(消除时序噪声)⇒ 同一组输入跑两遍 —— Run A = 上游原物 SDK 直连平台面(迁移前口径:只认实例令牌,无令牌闸);Run B = 包内新 SDK → 包面(真实 apply(ctx)) → 平台面(迁移后口径:令牌闸 + 启用集)。比对平台侧效果流水(确定性文本,⛔ 无时间 / 无随机数)与顶层语义。

判据 结果
两边都真的产生了平台侧效果 ✅ A=13 · B=13
🔴 效果流水逐行完全相同 ✅ 逐行逐字符相同(原文见 tmp/p43/读数-20260926.txt ⑤ 段)
顶层语义 14 项逐项一致 ✅ 全过(ready / rooms.count / messages.count·cursor / frame / say(r1).seq=3 / say(r2)=outbound-denied / 增量拉取 1 与 0 / unregister / subscribe)

🔴 两边同起点是这条判据成立的前提 —— platformPre/platformPost.resetSeed() 必须各调一次;首版只在一侧复位 ⇒ 前序测试动过的房间被当成"迁移前后的行为差异"(实测症状 A=2 B=3),已修(见 §七-④)。


六、🔴 真实 vs 建模(逐条标注 · ⛔ 不许把建模当"已通")

项 真实行为(读码 / 只读取证) 本棒做法 是否 = 真实
包内那一半(凭据解析/出示/本机 fail-closed 拦截/单例闸) 新增代码,就在包里跑(真实 apply + 真实 HTTP 分派) 真实代码 ✅ 真实
平台侧那一半(令牌签发 + 桥端点校验 + 启用集校验) 真实 im.ts 里目前没有这道闸(残留① 的存在就是因为它没有) 沙箱 platform-mock 里建模(requirePluginToken 开关:false=迁移前口径/true=迁移后口径) ⚠️ 建模
桥端点 8 条路径 / 身份头 / 长轮询硬上限 真端点存在且在跑(im.ts 逐条实读) 逐字照抄 + 沙箱本机 http ⚠️ 口径真实、传输建模
pluginIdOf 形态映射 上游 SDK 里逐字实现 逐字照抄 ✅ 真实(照抄)
会话面鉴权 真平台走 imAuth(实例令牌 → 用户身份) 沙箱同口径建模(u1) ⚠️ 建模
长轮询"只许一条"的强制点 上游只有自述(im.ts:482 原文),平台侧无强制点 强制点落在包内闸(这正是残留② 的补法) ✅ 真实(补法落在包内)
bridge/pull 工作项投递 真平台按实例排队 沙箱同口径(pushWork) ⚠️ 建模
部署形态(包怎么进实例 / 谁签发令牌) 属投放线,本棒 ⛔ 不碰 ⛔ 不做 ⛔ 未覆盖

🔴 结论纪律:本棒能断言的只有 —— 「照 IM 接入方式重写的那套插件侧逻辑,在既有桥口径下四条面齐全、两条残留的补法在包内成立、迁移前后行为逐行一致」。⛔ 不能据此断言「真机投放已通」「平台侧令牌闸已上线」「跨机 IM 已通」—— 平台侧那一半与部署形态都还在沙箱外。


七、§4 避坑总表对表(本棒相关类目逐条)

# 现象 本棒怎么处理 证据
B1 桌面端 pending (waiting for service: webServer) 只用 connection 一处扩展点;⛔ 零 webServer 引用 门禁 ①c「零 webServer 引用」✅
B2 API 注册了却 404(桌面端只认精确路径) 三条 M5 路由逐条列精确路径,并运行时真挂一次断言 门禁 ①e「四条路径互不重复 / 每条有 methods」+ 回调面 4 条原文
B3 「放进一个包」落了地一半 包内零直连平台库路径(正则红线扫描)+ 平台侧动作全部走桥 门禁 ①e「零直连平台库路径」✅
A2 HTTP 路由静默 404(inject 未声明) inject = ['connection'] 逐字声明 门禁 ①c「逐字声明」✅ + 取数面
A6 一个可选插件把壳搞得起不来 全部装配失败路径降级 warn,⛔ 不抛 包内 8 条 warn 原文已打印(全部是预期拒绝,无一条装配失败)
A5/E2 改了却没生效(UI 侧) ⚠️ 本棒未取到浏览器级,故不断言 UI 生效 §八「未取到」
E1 测试跑的是构建产物 门禁 --check-only rc=0;尺子直接打 lib/ 实件 尺读数 ⓪ ✅
E6/R3 exports.default 禁用 门禁 ①b「零 exports.default」✅(S2 基线未破) 尺读数 ⓪ ✅
D3 列限声明改了却没生效 门禁 ①d 回读 DECL_DIGEST=88c752a3e12597b3…(8 表 / 29 列,S3 基线未破) 尺读数 ⓪ ✅

八、尺子回归(⛔ 未改尺子一行)/ 未取到(如实)

面 靶子 / 入口 rc 说明
包内门禁 node scripts/build.mjs --check-only(包目录) 0 「✅ 门禁通过」;新增 ①e M5 门禁 全绿,M5_DIGEST=55cdc0d5fc8eb99d
装配 · 端A 客户端形态 --profile-json tmp/p43/端A-客户端形态/package.json 0 解析到 dsh-plugin-ai1net/lib/index.js 20798 B
装配 · 端B 平台形态 --profile-json tmp/p43/端B-平台形态/package.json 0 同上(两端同一份产物)
取数 --target @dsh-local/ai1net --entry lib/index.js 0 真实 import;inject=['connection'] ✅
回调 同上 0 真实 apply(ctx),routeCalls 逐字 4 条
v-neg.sh all 尺子自身(⛔ 靶子非本包) 0 装配/运行/取数/回调 四条红/绿双态均成立 ⇒ 尺子仍能判红
v-baseline.sh 47 共享层 + 本机 desktop profile 0 平台侧 4 包;客户端侧 4 bundle
v-ui.sh 浏览器级 3 ⚠️ 未取到(见下)

⇒ S2 / S3 / S4 的绿没被本棒弄红。

⚠️ 未取到 / 不成立(如实)

项 结果 原因
v-ui.sh(浏览器级) ⚠️ rc=3 未取到 CDP 127.0.0.1:9330 不可达 ⇒ 本机无运行中的真壳;启动器属客户端线,本线 ⛔ 不代启
v-plugin.sh 运行 面 ⚠️ rc=0 但属"空真" 可用启动日志 mtime = 2026-09-25T09:19:43,早于本包存在 ⇒ "无失败条目"不构成证据
M5 UI 卡片(plugins.item 槽 + 三条路由的界面) ⚠️ 未取到 同上,需真壳(同 S2/S3/S4 同一笔欠账)
真机投放 / 平台侧令牌闸上线 ⛔ 刻意不做 属硬边界「⛔ 不代用户起实例 / 不碰把包装进去」+ 属投放线
实机 requirePluginToken 开关 ⚠️ 仅沙箱 真实 im.ts 尚无该闸(= 残留① 的本体)

⇒ ⛔ 以上五项都不折算成"已知 / 通过"。


九、两处自纠 + 六条本棒踩出来的坑(如实)

# 症状 根因 处置
① 平台回 bad-manifest / bad-args callPlatform(path,{body: obj}) 直接给 fetch ⇒ body 变 "[object Object]" 在 SDK platform() 内统一 JSON.stringify
② 读接口被误判 502 /api/im/rooms 回 {rooms,count,mode,meId} 无 ok 字段 ⇒ reply() 把"缺 ok"当失败 改成 failed = out==null || out.ok===false(缺 ok 不算失败)
③ 负控假绿(拿会话面测令牌闸) 会话面走 imAuth,没有桥身份闸 ⇒ 打它测不出东西 改打出向 POST /api/ai1net/im/messages(桥端点,有闸)
④ 回归比对"迁前迁后不一致"(A=2 B=3) 只有一侧复位 ⇒ 前序测试动过的房间被当成行为差异 加 resetSeed(),两侧各调一次(同起点)
⑤ 「包替任何插件签身份」漏洞 host 侧从本机 env 回退取令牌 ⇒ 调用方不带凭据也能被"代表" 引入三态凭据语义(undefined / '' / 值)⇒ '' 必拒
⑥ 门禁 ①e「→ 应为 4 处…得到 2」 注册写在 for 循环里 ⇒ 文本计数 ≠ 注册数 门禁从"数文本"升级为「运行时真挂一次再断言」(spy ctx 捕获 seen[])

⚠️ 首版另有一处无效负控:pluginId 口径混用(pluginIdOf 下划线形态 vs 头里的包名)⇒ 闸键与身份头对不上,此时负控会"通过"但原因是错的。已统一为包名(callerOf / 闸键 / 令牌载荷)。


十、硬边界自查 / 遗留 / 回滚

  • ⛔ 未改官方 dsh 主程序与缓存(R2):只读 D:/github/dsh_shenxian/src/web/routes/im.ts、sdk/im-plugin-host/index.mjs、src/im/sdk/host.ts(照抄来源),零写。
  • ⛔ 未重造分发与更新机制:M5 只做「插件侧路由 + SDK」,分发仍走 S4 的拉取器。
  • ⛔ 未代用户起实例;⛔ 未把包装进任何 profile(装配用的是 tmp/p43/ 下的沙箱 profile)。
  • ⛔ 未改客户端工作区 dsh-ai1net-desktop / dsh-desktop-0.1.7rc2 任何文件(只读引用;端A 是 profile 副本)。
  • ⛔ 包内零直连平台库路径(正则红线扫描 ✅);⛔ 未 commit / 未 push。
  • ⚠️ 红线门禁:本棒未触发任何一项 —— 未重启控制面、未中断在线用户、改动文件 6 个(< 10)、无不可逆操作(沙箱可整体 mv 归档)。
  • 🔴 停点:S4 的 P1 已解除;S9 的 P2 仍待拍板,⛔ 本棒未跨过。本棒未发现新停点。
  • 本棒改动的包内文件(6 个):lib/im-plugin-sdk.mjs(新建 47,964 B)|lib/index.js(20,798 B)|lib/client.js(17,210 B)|scripts/build.mjs(23,007 B,新增 ①e 门禁 + ①c 改为"零裸标识符 import"+ 语法门禁扩到 .mjs)|package.json(exports["./im-plugin-sdk"])|README.md。
  • 遗留(如实):① 浏览器级 / M5 卡片仍欠一笔真壳取证(客户端线窗口)② 平台侧令牌闸与"启用集"校验尚未进真实 im.ts(投放线)③ M5 界面只到"能渲染一个只读面板",交互面(发消息 / 开关回调)留后续块 ④ 沙箱 platform-mock 的 requirePluginToken 只是开关,不是真平台实现。
  • 回滚:本棒无生产副作用 ⇒ 回滚 = mv tmp/p43/ 归档/(⛔ 不用 rm);包内改动可用 git 逆转(若包目录未入 git ⇒ 以 lib/im-plugin-sdk.mjs 删除 + 其余 5 文件回退到 S4 版本为准)。

十一、出处

  • 规格书:D:/github/dsh_shenxian/dsh-server-docs/05-交接单/交接单_包规格书_20260926.md(§〇 路径约定 / §五 A 冻结规格 / §五 C 指针)
  • 方案:交付物/功能打包到基础插件-分步实施方案-20260926.md §3.10.7(S5-1…S5-4)+ §4 避坑总表(A/B/D/E 类)
  • 残留两条原文:交付物/全功能承载判定-底层插件能否承-20260926.md §2.6
  • 上游权威(只读):D:/github/dsh_shenxian/src/web/routes/im.ts|D:/github/dsh_shenxian/sdk/im-plugin-host/index.mjs|src/im/sdk/host.ts:112
  • 前置回执:交付物/分发与更新-S4落地与三轮回执-20260926.md(§八 真实 vs 建模已逐条继承)
  • 本棒产物:包内 E:/ProgramData/AIProject/dsh-plugin-ai1net/{lib/im-plugin-sdk.mjs,lib/index.js,lib/client.js,scripts/build.mjs,package.json,README.md}
  • 本棒读数(⛔ 不入库):tmp/p43/platform-mock.mjs|tmp/p43/run-s5.mjs|tmp/p43/run-scales.sh|tmp/p43/读数-20260926.txt|tmp/p43/尺读数-S5-20260926.txt|tmp/p43/端A-客户端形态/|tmp/p43/端B-平台形态/