# 兼容性 ## 基线 | 项 | 值 | | --- | --- | | 宿主 | DSH Desktop `0.2.0-rc.2` | | 宿主源码 tag | `dsh-v0.2.0-rc.2` | | 插件版本 | `dsh-question-bridge@0.1.1` | | 验证过的平台 | 无(见下) | 功能设计对齐了 `dsh-v0.2.0-rc.2` 的源码,但**没有在真实宿主里跑过**:本机没有可用的 TypeScript/esbuild 构建链,也没有把插件装进用户的 profile 做端到端联调。所以这里的"兼容"含义是"按该版本源码的 API 形状实现并对接口做了防御",不是"已跑通"。 ## 用到的宿主 API 宿主端(`lib/index.js`): | API | 用途 | 缺失时的降级 | | --- | --- | --- | | `ctx.agents.roots()` / `agents.get(id)` / `agent/created` | 只对 live root 绑定工具与提示词 | 不绑定,插件静默无功能 | | `agent.ctx.tools.register()` | 注册 `ask_interactive_question` | 工具不存在 | | `agent.ctx.inject(['systemPrompt']).section()` | 会话级动态提示词(open 批次列表) | 无动态段 | | `ctx.systemPrompt.section()` | 全局静态提示词(pending ≠ 授权) | 无静态段 | | `ctx.userQuestions.ask({agent, questions, signal})` | 展示原生问题卡片 | 只走面板;工具仍返回 pending | | `ctx.sessions.get()` / `flush()` | 会话句柄与持久化检查点 | 投递降级为不 flush(答案可能晚一拍落盘) | | `session.snapshotEvents()` | 回合结束时取助手文本、判定答案是否入日志 | 该会话不自动识别/无法确认 delivered | | `session.requestHeader()?.config` / `agent.options` | 取 classifier 的 provider/model | classifier 直接跳过并写诊断日志 | | `ctx.llm.stream()` | 可选模型判定 | 只跑规则 | | `ctx.webServer.register({kind:'exact'})` + `connection.requestRejection()` | 面板数据与动作路由 | 面板拿不到数据 | | `agent/turn-stopping` | 自动识别挂点 | 不识别普通文本 | | `session/event` | 判定答案消息是否真的入日志 | delivered 只能靠 steer 之后的状态推断 | | `agent.steer()` | 把答案投回会话 | 答案停在 queued,等人重试 | 浏览器端(`lib/client.js`):`ctx.slots.inject('shell.overlay', …)`、`ctx.uiWorkspace.openSession(sessionId)`、`window.__ModuleLoader__`、Notification API、`localStorage`。 ## 刻意偏离原方案的地方 原方案假定的工具链(pnpm workspace + tsdown + `@deepseek-ai/dsh-typert-generator` 生成 `./typert` / `./remote`)在这台机器上跑不起来:没有 TypeScript、没有 esbuild、生成器也不在应用包里。与其交付一堆跑不起来的配置,下面几处改了实现方式,理由都是"目标机能否运行": 1. **不用 Typert RPC,改用 HTTP 路由。** 插件的 RPC `open/submit/cancel/retryDelivery` 走 `/plugins/question-bridge/action`,面板轮询 `/plugins/question-bridge/state`。代价是没有生成式类型校验和流式接口;好处是不依赖宿主内部包、可以在任何 0.2.0-rc.2 上跑。 2. **不写自定义 session 事件。** 0.2.0-rc.2 的持久化读取方对"未知且非 ignorable"的事件会拒绝解释整份日志,而 `Session.append()` 没有 ignorable 参数。所以批次状态写在插件自己的 JSON 文件里,日志只用来读真实发生过的 `user/message`。 3. **手写可运行的 JS,不提供构建配置。** 源文件就是产物:`lib/index.js` 是 ESM,`lib/client.js` 是 `window.__ModuleLoader__.load` 的 CJS 工厂。因此没有 `.d.ts`,也没有 sourcemap。 4. **不导出 schemastery `Config`。** 插件里 import 不到 `@deepseek-ai/schemastery`(profile 安装的插件只有自己的依赖,宿主内部包不解析),所以配置在 `apply(ctx, rawConfig)` 里手写归一化。后果是 DSH 的设置界面不会按 schema 渲染本插件配置,需要手写。 5. **classifier 默认关闭。** 规则识别默认开;模型判定要显式 `classifier: "model"` 才启用,避免未验证的模型调用和额度消耗。 6. **通知不做多窗口 leader 选举。** 用 Notification 的 `tag` 归并同一批次、`localStorage` 记录已弹过的 key,多窗口下可能同一时刻各弹一条,但点击任意一条都能回到正确会话。 ## 实现所依赖的接口假设 以下假设来自 0.2.0-rc.2 源码,但没在运行中的宿主上验证过。升级宿主时优先检查这几条: - `ctx.slots.inject('shell.overlay', fn)` 接受 `{name, id, order, label}` 这样的注册选项,且 `shell.overlay` 处于"允许第三方注册"的白名单里。 - `ctx.webServer.register({kind:'exact', path, handler})` 的 handler 形如 `(req, res)`,且 `connection.requestRejection(req)` 返回 `undefined` 表示放行。 - `ToolDefinition` 是结构类型,不经过 `defineTool()` 的普通对象也能注册;`parameters` 的 JSON-Schema 方言接受 `{type:'object', additionalProperties:false, properties:{…}}`。 - `userQuestions.ask({agent, questions, signal})` 在没有 `wait.callId` 时仍会渲染卡片,用户提交后 Promise 以 `{answers:[{id, selected:string[], custom?}]}` 结案;`selected` 是选项 **label**,不是 id。 - `session/event` 的 `event.data` 对 `user/message` 就是消息本体(带 `id`/`source`)。 - `agent/turn-stopping` 在 turn 边界提交前被 await,监听器可以把工作丢到异步队列而不阻塞边界。 ## 已知限制 - **不会把窗口从托盘拉出来。** `window.focus()` 在 Electron 托盘态下不保证生效,这是平台行为。 - **一次只展示一张插件卡片。** 同一会话的其它批次在面板里排队;宿主原生问答/plan review 优先时,插件不会抢占。 - **面板文案只有中文。** 没接 `ctx.locale`,避免和宿主 locale 字典互相覆盖。 - **不支持编辑答案。** 提交后批次进入 `answer-committed`,再答会得到 `ALREADY_ANSWERED`;要改只能让 Agent 重新提问。 - **状态文件按会话分片,没有全局锁。** 同一个会话在两个窗口同时答题,后提交者会收到冲突错误,不会双投。 - **自动识别只覆盖"明显在提问"的段落**:问句结尾、常见中文请求句式、`Which one`/`Please confirm` 之类的英文信号。它不会替你猜隐含问题。 ## 与其它版本的关系 - `0.2.0-rc.1`:Typert 生成器与 `userQuestions` 形状大体相同,但未验证。 - `0.1.5-rc.2`(全局 npm 里那份 CLI):工具与 session API 差异较多,`ask_interactive_question` 的注册方式与 projection 都不同,**不保证兼容**。 升级宿主后如果插件加载失败,先看宿主日志里有没有 `plugin "dsh-question-bridge" failed`,再把 `lib/index.js` 顶部 import 的 node 内置模块排除掉(只用了 `node:crypto`、`node:fs/promises`、`node:os`、`node:path`),剩下的就是 API 形状变了。