dsh-astrbot-gateway
v0.6.1
Published
DSH cordis plugin: task dispatch, session wake-up and completion reporting through the AstrBot dsh gateway.
Maintainers
Readme
dsh-astrbot-gateway
安装方式见仓库的
docs/install-dsh.md,那份是写给 AI 的,照着做就能挂进 profile。
dsh 侧的桥接插件:把闸门筛选后转达的指令从 cache/inbox 取来执行,结果写进
cache/outbox 交付给闸门 —— 由闸门转述给用户,dsh 不直连用户。
对应任务卡:../../docs/task-for-dsh.md;消息规范:../../docs/message-rules.md(中转规则 v2)。
它做什么
| 方向 | 行为 |
| --- | --- |
| 下行 | 每 pollMs(默认 4s)扫一次 cache/inbox/*.json;发现 pending 指令就自动拉起一个 DSH 会话去处理(autoDispatch,默认开),并把「收到 / 已拉起 / 拉起失败」写成 cache/outbox/<id>.notice.json 通知文件,同时刷新队列快照 cache/bridge_status.json。 |
| 交付 | 只落盘,不直发用户(uplinkMode 默认 off):结果写 cache/outbox/<id>.json,字段按 v2 规范齐全;落盘后闸门侧会被叫醒,由闸门用自己的话讲给用户。 |
| 接单 | 提供工具 bridge_inbox / bridge_claim / bridge_complete,由 agent 认领并执行指令,完成后写 outbox 交付。 |
| 收尾 | runningTimeoutMs(默认 30 分钟)超时回收:认领后一直不回报的(DSH 重启、会话被停止、agent 被杀)会被标成 failed 并落一条失败结果给闸门,避免永久挂在队列里冒充「在跑」。写 0 可关闭。 |
| 上行 | 保留但默认关闭:/send 到达的是闸门的 QQ 账号(= 用户的私聊),用它发就等于绕过闸门。只有显式写 uplinkMode: 'on'(救火)才会走;启动时仍会 GET /ping 做自检,结果记在 bridge_status.json.uplink。 |
v2 与 v1 的差别(一句话)
v1:完成任务后用上行接口直接给用户发消息,用户在 QQ 里看到的是 dsh 的话。
v2:完成任务后只写 outbox,用户通过闸门得知结果,且闸门会注明「来自dsh」。
差别落在三处:outbox/<id>.json 的字段(多了 source,to 从 用户 改成 gateway)、
通知改落 outbox/<id>.notice.json、以及默认零上行。
自动拉起:扫到 pending 就自己干活(autoDispatch)
轮询发现新指令后,插件会自己叫醒一个会话去处理,不用用户手动喊:
sessionController.create({}) // 建会话:sessionId / cwd / 预设都可省略,走宿主默认
sessionController.prompt({ // 投一条用户消息(按 requestId 幂等,重复投喂会被去重)
sessionId, requestId: `bridge-<id>`, mode: 'queue',
content: [{ type: 'text', text: '【桥接指令 <id>】… 请 bridge_claim → 执行 → bridge_complete' }],
})机制照抄部署里现成的 @agents-anywhere/dsh-bridge-next(AA 手机端 → DSH 会话,就是
「外部事件拉起会话」的官方实现),两个服务都用 ctx.get 取(可选取)。
容错设计(都很重要):
sessionController缺失 /create抛错 /prompt抛错 → 只降级成「只通知」,插件本身照常跑, 并且在通知里如实写明失败原因;最多重试3次,两次之间至少隔dispatchRetryMs(默认 60s,避免轮询 4s 时十几秒就把重试次数烧完),之后放弃并写明「需要你叫我处理」。- 已拉起的指令写
cache/.dispatched-<id>标记,重启后也不会重复拉起; 即使标记写失败,prompt的requestId幂等也能兜住重复投喂。 - 仍然不擅自把任务标成
running:认领仍由 agent 显式调bridge_claim完成, 这样没人干活时任务不会被悄悄吞掉。 capabilities.dispatch四种取值:disabled(关掉了)/ready/lazy/failed。lazy是正常现象:插件的行可能比session-controller那一行先激活,而 cordis 对 尚未运行的服务ctx.get会返回 undefined。真正的判定发生在轮询拉起时(会重新ctx.get), 所以lazy不影响自动拉起。
桥接会话跑哪种「模式」(agentPreset)
sessionController.create() 不传 agentPreset 时用的是宿主默认预设,而本机
$DSH_HOME/settings.yaml 里 agent-presets.default 是 teyvat-hoi4(提瓦特黎明 HOI4
项目专用 persona,每回合强制先读 E:\teyvatdaybreak\PROJECT_RULES.md + SKILL 索引)。
不管它的话,每条桥接指令拉起的会话都被 HOI4 模式接管 —— 跟桥接任务毫无关系,白烧几轮
读规则。实测三条桥接会话(group-182442、rule-v2-…、selfcheck-v2-1)的 header 全是
"agentPreset":"teyvat-hoi4"。
规矩(用户定的):桥接默认走「标准模式」(standard);确实要 HOI4 模式的单条指令由闸门点名。
优先级(高 → 低):
| 来源 | 写法 | 说明 |
| --- | --- | --- |
| 单条指令 | inbox JSON 里 "preset": "teyvat-hoi4"(也认 "agentPreset") | 闸门按这条指令的性质选模式 |
| 插件配置 | 行配置 dispatchPreset: standard | 本机 desktop profile 的补丁层已这么设 |
| 宿主默认 | 不传 agentPreset | 兜底,等于 settings.yaml 的 agent-presets.default |
点名一个不存在的预设不会让指令失败:记一行日志(…点名的预设 "x" 不在花名册(可用: …),回退到 …)
后按「插件配置 → 宿主默认」回退。花名册(ctx.agentPresets)读不到时也不拦,照传,
交给宿主 create() 用 agent-preset/not-found 说话 —— 「读不到花名册」和「预设不存在」是两回事。
拉起后的通知里会写明这次用的模式:已自动拉起会话 <sessionId> 处理(模式 standard)。
部署侧那个默认值写在
$DSH_HOME/profiles/desktop/cordis.patch.yml(desktop profile 是patchReload: live,改完不重启就会重新组合)。改插件代码则必须重启 dsh: base 里hmr那一行是disabled,只有配置会被热重载。
为什么轮询不自动认领
autoClaim 默认 false:把 status 改成 running 由 bridge_claim 显式完成。
这样重复轮询、以及用户不在场/没人干活时,指令都不会被悄悄标成 running 而永久卡住。
需要自动认领就把 autoClaim 打开。
已通知过的指令会在 cache/.notified-<id> 留标记,所以插件重启后不会重复落通知文件。
目录约定
cache/
bridge_access.json # 闸门侧签发(含 token),本插件只读
inbox/<id>.json # 闸门筛选后转达的指令,本插件读 + 回写 status
outbox/<id>.json # 本插件写的结果(v2:to=gateway,带 source)
outbox/<id>.notice.json# 本插件写的巡检通知(收到指令 / 已拉起 / 拉起失败)
bridge_status.json # 本插件写的自检/队列快照,便于排查(含 dispatch.cwd/preset、accessError、hint)
.notified-<id> # 本插件的「已通知」标记
.dispatched-<id> # 本插件的「已自动拉起」标记(含 session id)bridge_status.json 里的 hint 是给人看的:上行自检通过时它是 null;不通时它会直接写明
「AstrBot 侧还没装好,去面板装哪个插件、填哪一项、然后重启」。桥断掉的时候先看这一行,
bridge_inbox 的返回里也会带上同一句。
inbox/<id>.json 结构(闸门落盘,from 应为 gateway —— v2 起 dsh 不直收用户原文):
{"id":"...","from":"gateway","to":"dsh","time":"ISO8601","type":"task","content":"文本","status":"pending"}可选字段 preset(或 agentPreset):这条指令用哪个 agent 预设跑。不写就用插件配置的
dispatchPreset(本机 = standard),再不写才落到宿主默认。见上面「桥接会话跑哪种模式」。
outbox/<id>.json 由本插件写(v2 规范,docs/message-rules.md 第二节):
{
"id": "任务ID", "from": "dsh", "to": "gateway", "source": "dsh",
"time": "ISO8601", "type": "result", "ref": "对应的任务ID",
"status": "done", "summary": "一句话摘要", "content": "完整内容"
}字段规矩:source(信息来源)与 status(当前状态)缺一不可 —— 少了闸门侧就无法
判断这条该不该转、转的是谁的话;content 必须是完整结果(不能拿摘要顶替),
超长时截断处会留一行显式说明。status 取 done 或 failed。
outbox/<id>.notice.json 是本插件的巡检通知(type: "notice"、notice: true,字段与上面同构)。
它存在的理由:v2 下 dsh 连「我收到了,正在干」这种即时告知也不能直发用户,
只能落盘,等闸门被叫醒时一并转述。
写 outbox 一律用「临时文件 + rename」,读者不会看到半截文件。
配置
插件按 cordis 配置项读取,全部可选(默认值即可跑):
| 键 | 默认 | 说明 |
| --- | --- | --- |
| root | <项目目录>\cache | 桥接缓存根目录 |
| pollMs | 4000 | 轮询间隔;0 关闭轮询(只用工具手动处理) |
| autoDispatch | true | 扫到 pending 就自动拉起会话处理;设 false 退回「只通知」 |
| dispatchPreset | 空 | 自动拉起用哪个 agent 预设(= 会话以哪种「模式」跑);空 = 宿主默认预设。本机 desktop profile 的补丁层设成 standard,免得每条桥接指令都继承 settings.yaml 里那个 HOI4 项目预设;单条指令可用 inbox JSON 的 preset 覆盖 |
| dispatchCwd | 桥接目录的上一级 | 自动拉起的会话归属哪个工作区组别;目录不存在则落「未分组」 |
| autoClaim | false | 轮询发现新指令时是否自动标 running |
| uplinkMode | off | off=只落盘交付闸门(v2 默认,不直发用户);on=恢复 v1 直发(仅救火用) |
| defaultTarget | 空 | 上行目标;空则用 bridge_access.json 的 default_target(只对 uplinkMode: 'on' 有意义) |
| targetType | PrivateMessage | AstrBot 侧只接受 PrivateMessage / GroupMessage |
安装(已执行)
本插件是 profile bundle 插件(宿主组合里的一行),装好后长期生效、对所有会话可用,
但需要重启 dsh 才会加载。已经用 tools/install.ps1 -Apply 装好,重启即生效。
若要在别的机器/环境重装,脚本做两件事:
- 在 profile 里把本包登记为本地依赖,并把
"dsh-astrbot-gateway"追加进 profilepackage.json的dsh.profile.bundles。本包自带cordis.patch.yml(insert一行dsh-astrbot-gateway),所以只要它在 bundles 列表里就会自动挂载。 - 改
package.json前自动备份,可重复执行(幂等)。
# 只看会改什么
powershell -ExecutionPolicy Bypass -File tools\install.ps1
# 真正执行
powershell -ExecutionPolicy Bypass -File tools\install.ps1 -Apply重启后确认:
cache/bridge_status.json出现且"uplink": true;capabilities字段里tools/timer/interval都是true;- 工具列表里能看到
bridge_inbox/bridge_claim/bridge_complete。
两个已经踩过的坑(改这个插件前务必先看)
1. pnpm 对 file: 依赖是「复制」,不是软链
改完 lib/index.js 后直接把插件加进 profile,pnpm 会在 node_modules 里留一份
旧副本,并且重跑 pnpm add 只会回一句 Already up to date —— dsh 加载的
始终是那份旧代码。表现是「改了没反应」,非常容易误判成 inject 或加载问题。
tools/install.ps1 现在会对比 node_modules/<pkg>/lib/index.js 与源码的哈希,
不一致就删掉副本再 pnpm install,装完还会再验一次哈希。改完插件请用它重装。
2. cordis 是严格注入,且 ESM 有缓存
ctx.<name>属性访问用到的服务必须在inject里声明(数组形式), 否则 apply 开头就抛cannot get property "..." without inject,整个插件不注册。 本插件声明的是['tools', 'timer']。ctx.get(name)不需要 inject:它只是查表,取不到返回undefined、不会抛 (get(name, strict)的 strict 只影响「存在但未启动」的服务)。 所以可选能力一律用ctx.get+ 判空,绝不能写进 inject —— 否则宿主没这个服务时连插件都加载不起来。本插件的sessionController就是可选取服务(自动拉起用,见上面「自动拉起」一节)。- 改完源码后 disable/enable、重装依赖都不会生效:ESM 模块按 URL 永久缓存, 只能重启 web。
- 反过来,纯 cordis 插件(不声明
dsh.bundle)走 profile 的cordis.patch.ymlinsert 行是配置 HMR、可零重启挂载;bundle 插件必须重启。
tests/selftest.mjs 里的假 ctx 复刻了这两条语义:属性访问未声明服务就抛错并计入
undeclared,而 get('未知服务') 返回 undefined,所以这两类错误在离线自测阶段
就会暴露,不用等重启。
3. 同一个 id 只能有一层:进了 bundles 就别再写 profile patch
DSH Desktop 启动时这样组 profile 组合:
dsh.profile.bundles里每个 bundle 各一层:读它的package.json→dsh.bundle.patch指向的 patch 文件;- 再叠加 profile 自己的
cordis.patch.yml(又一层); - 把各层
insert行拼成 loader 行,最后统一检查 id 唯一性。
所以本包已经在 bundles 里时,profile 的 cordis.patch.yml 里再手写一行
- insert: { id: dsh-astrbot-gateway },同一个 id 就被插了两次,桌面端启动直接抛:
dsh-plugin-desktop: duplicate loader entry id "dsh-astrbot-gateway" in the composed profile
at assertUniqueEntryIds (.../lib/profile-*.js)
at prepareDesktopProfile (.../lib/main.js)随后进恢复模式:内部执行 dsh plugin --profile desktop remove dsh-astrbot-gateway。
而 remove 要跑 pnpm,profile 里只要有一个拉不动的私有 git 依赖(例如 gal-view 连不上
GitHub),连恢复都会失败,只剩人工回滚 profile —— 桌面端起不来。这个坑真踩过。
本包是 bundle 插件(package.json 里有 dsh.bundle),挂载方式就是「进 bundles」,
不需要、也不要再动 profile 的 cordis.patch.yml。
重启前先离线预检(复刻上面的分层组合 + id 唯一性检查,不起 DSH 也不会崩):
node ..\..\tools\check-profile.mjs --profile desktop退出码 0 = 无重复 id;1 = 有冲突(会打印是哪两层撞了)。install.ps1 也加了同样的
守卫:发现 profile patch 里已有同名 insert 行就直接拒绝执行。
另外:
dsh plugin add/remove会跑 pnpm。profile 里有file:依赖时,直接改 profile 的package.json+ 保证node_modules\<pkg>是最新副本,比走dsh plugin更省事, 也不会因为网络问题把恢复流程拖失败。
4. 不要导出 Config(除非用 schemastery)
cordis 解析插件配置走 Standard Schema 协议,实现是:
entry.plugin.Config['~standard'].validate(rawConfig) // @deepseek-ai/cordis 的 resolveConfig所以 Config 必须是 schemastery 的 z.object({...}) 实例,不是描述字段的普通对象。
写成普通对象时 Config['~standard'] 是 undefined,读 .validate 立刻抛:
dsh-plugin-desktop: plugin tree failed to load: failed to apply loader entry dsh-astrbot-gateway
(dsh-astrbot-gateway): Cannot read properties of undefined (reading 'validate')
at resolveConfig (…/@deepseek-ai/cordis/lib/index.js:957:45)整个插件树加载失败(不是只有本插件不生效),桌面端同样进恢复模式回滚 profile —— 和坑 3 是同一套恢复流程,也一样会因为 gal-view 连不上 GitHub 而恢复失败。这个坑也真踩过。
本插件刻意零依赖(只 import node: 内置模块),而 desktop profile 的 node_modules
里没有 @deepseek-ai/schemastery(装它要走 pnpm / 联网),所以干脆不导出 Config:
配置全靠 apply 里的默认值;需要覆盖时在组合的行里写 config,apply 照常读到(无校验)。
同一个 profile 里能跑的 gal-view 也是这个形态——只导出 name / inject / apply。
两道回归守卫:
tests/selftest.mjs:Config要么不存在,要么必须带'~standard'.validate;tools/check-profile.mjs(仓库根tools\):重启前import每个 bundle 的入口, 把name/inject/apply/Config契约一起查。
5. ctx.tools.register() 不编译任何东西——传进去的必须是编译后的原始 JSON Schema
这是最绕、踩得最久的一条。dsh-tools 有两个层级:
| 层 | 角色 | 对 parameters / output.schema 做什么 |
| --- | --- | --- |
| defineTool({...}) | 作者向包装器(内置工具都走它) | 把描述符 DSL / 作者值 schema 编译成原始 JSON Schema,再交给 register |
| ctx.tools.register(definition) | 注册表本身 | 什么都不编译:只要求 output.render 是函数、assertSupportedJsonSchema(output.schema),然后把 definition 原样收下 |
也就是说 register 认为你给的就是编译后的成品:
definition.parameters 会被原样发给模型,output.schema 会被当成已经是原始 JSON Schema 来断言。
本插件刻意零依赖(拿不到 @deepseek-ai/dsh-tools 的 defineTool),所以直接手写编译后的形态。
写错就会连踩两个坑,两个都真踩过:
坑 5a —— 把作者 spec 直接当原始 schema(逐字段 required: true):
dsh-plugin-desktop: plugin tree failed to load: failed to apply loader entry dsh-astrbot-gateway
(dsh-astrbot-gateway): unsupported JSON schema: schema.properties.summary.properties.total.required
is not supported on type "number"; schema.properties.tasks.required is not supported on type "array"; …
at assertSupportedJsonSchema (…/@deepseek-ai/dsh-tools/lib/index.js)→ 整个插件树加载失败,桌面端回滚 profile。
坑 5b —— parameters 用了描述符 DSL(根节点没有 type):
本轮运行失败 Invalid schema for function 'bridge_claim':
schema must be a JSON Schema of 'type: "object"', got 'type: null'.→ 插件挂得上、日志一切正常(上行自检: OK、轮询已启动),但每一轮对话都失败,而且同样触发回滚。这个坑最难查,因为它伪装成"运行期偶发"。
所以本项目里的规矩是:
parameters手写成{ type:'object', properties:{...}, required:[...] }(没有必填参数时省略required键,与defineTool产物一致);output.schema手写成 object 根、落在受支持子集内的原始 schema;- 两者的写法必须逐字节等于
defineTool会编译出来的东西 (cache/下跑一次保真度对比即可确认)。
受支持子集(取自 dsh-tools 源码):关键字只有
type / oneOf / properties / required / additionalProperties / items / enum / const
加注解(description、title 等),并且:
properties/required/additionalProperties只能挂在type: "object"上;items只能挂在type: "array"上;enum/const只能挂在标量上;type必须是单个字符串(不支持 type 数组);同一节点不能同时写type与oneOf;additionalProperties必须是 boolean;required必须是字符串数组且只能挂在 object 上;parameters必须是 object 根(否则模型 API 直接拒收)。
两道离线门禁(都不用重启):
node tests\selftest.mjs # 自测:内置同规则校验 parameters(object 根) + output.schema
node ..\..\tools\validate-tool-schemas.mjs # 真门禁:抽出 app.asar 里的真 dsh-tools 来断言validate-tool-schemas.mjs 不是"复刻规则",而是把 asar 里的 node_modules/@deepseek-ai
抽到 cache/_dsh-asar/ 后直接调用官方函数(assertObjectJsonSchema(parameters) +
assertSupportedJsonSchema(output.schema),正是 register 与模型 API 的要求),
所以结论和真启动一致。tools\install.ps1 -Apply 也会先跑这两道门禁,不通过就拒绝写 profile。
6. 工具返回值要过两道输出校验(都是活测才暴露的)
dsh-tools 对每个工具的返回值做两件事,任何一条不过,整个工具调用就报
tool "<name>" returned invalid output: …:
6a. 必须是 lossless JSON(isJsonValue)——带 undefined 的对象不是合法 JSON 值
(JSON.stringify 会静默丢掉它):
tool "bridge_inbox" returned invalid output: value is not lossless JSON真实触发条件:任务 JSON 里没有 ref 字段时 t.ref 是 undefined。
队列为空时恰好没有 undefined,所以只在「真有任务」时才现形。
修法是返回前统一过一遍 jsonSafe()(递归丢 undefined)。
6b. 必须严格符合自己的 output.schema(含 additionalProperties: false):
tool "bridge_complete" returned invalid output:
"value.uplink.result" is not a declared property (additionalProperties: false)真实触发条件:sendUplink 成功时返回 { ok, status, result },而 schema 里 uplink
只声明了 ok / status / message。这是只有成功路径才有的字段,失败路径测不出来。
修法是把返回值归一化成 schema 里声明过的形状。
6a / 6b 都发生在
execute()之后:报错时副作用(写 outbox、回写 inbox、 上行回报 QQ)其实都已经做完了,只是这一轮的「工具结果」被判非法。
这两条现在由 tools\validate-tool-schemas.mjs 的行为门禁覆盖:它会在沙箱里
(stub 掉 fetch,绝不发真请求)真跑一遍 bridge_inbox → bridge_claim → bridge_complete,
再用官方的 validateJsonSchemaValue 校验每个返回值。
7. 自动拉起的三处宿主契约(都真踩过)
自动拉起用的宿主 API(照抄 @agents-anywhere/dsh-bridge-next):
await sessionController.create({ cwd }) // 1) cwd 决定会话归到哪个工作区
await workspaceRegistry.create(cwd).attachSession(sessionId) // 2) 挂进该工作区组别
await sessionController.prompt({ ... }, signal) // 3) signal 是必需的第二参数7a. prompt(request, signal) 的 signal 是必需的
同一个类里两个方法签名不一样,prompt 第一行就是 signal.throwIfAborted()
(create 只收 1 个参数):
create(request) // 只收 1 个参数
prompt(request, signal) { // ← 第二参数必需
signal.throwIfAborted();
return this.commands.prompt(request);
}漏传会得到:
自动拉起失败(Cannot read properties of undefined (reading 'throwIfAborted')),稍后重试。宿主不关心"谁取消",但会先检查它存在,所以传 new AbortController().signal 即可。
7b. 不传 cwd 会拿到宿主的 process.cwd()(= DSH 安装目录)
那个路径不属于任何工作区,会话就落到侧边栏的**「未分组」**里(能看到,但不在你的项目组下)。
7c. 光有 cwd 还不够,必须 attachSession()
GUI 侧边栏读的是工作区的 sessionIds,而 attachSession 会校验会话 header 的 cwd
必须等于工作区路径 —— 所以 create 的 cwd 与 attachSession 的工作区必须一致。
本项目的行为(用户定的规矩):
- 能挂上工作区就带组别:
dispatchCwd默认取桥接目录的上一级(<项目目录>), 那是个真实存在、你也在用的工作区; - 挂不上就落「未分组」,但绝不因此失败:目录不存在 → 不传
cwd;attachSession抛错 → 只记日志。会话照样建、照样干活,只是显示在未分组里。
tests/selftest.mjs 里的假 sessionController / workspaceRegistry 刻意与真 API 同签名
(prompt(request, signal) 第一行 throwIfAborted();attachSession 记录调用),
所以漏 signal、漏 cwd、漏挂载这三类问题都会在离线自测里暴露。
自测
node tests/selftest.mjs # 默认:不发消息,只报告「本来会发什么」
node tests/selftest.mjs --live # 真发到用户的 QQ(会打扰人,慎用)以假的 cordis ctx 直接驱动插件本体,覆盖:读访问文件、上行 ping、轮询发现、去重通知、
认领(含拒绝路径穿越)、完成、outbox 结构与 v2 字段清单、状态快照、严格注入合规、
全程零上行(v2 守门断言)、以及救火开关 uplinkMode:'on' 仍可用。
使用沙箱目录 cache/_selftest,不碰真实的 inbox/outbox。
默认模式会拦截 /send(ping 仍放行),只把请求内容记下来做断言,所以反复跑也不会
打扰用户——这一点是踩过坑之后改的:早先版本每跑一轮就真发两条到 QQ,连跑几轮把用户
的消息列表刷了一串测试消息。v2 之后更强的保证是:默认一次 /send 都不该发生,
自测里那条「全程零上行」就是拦这件事的;真发生会打印每一条越权上行的内容。
已知边界
- 自动拉起会新建一个会话(不是往当前会话注入消息——dsh 没有那种公开接口)。
所以每条桥接指令对应会话列表里的一个新会话,你能在 GUI 里看到它跑了什么。
工作目录/预设用
dispatchCwd/dispatchPreset控制:本机dispatchCwd已指向<项目目录>、dispatchPreset已设成standard(见上面「桥接会话跑哪种模式」)。 - 被拉起的会话仍受宿主的审批与权限策略约束。如果它的动作触发了审批,会停在 等审批那一步——这不是桥接的问题,按需在宿主侧调权限预设即可。
- 新会话需要有可用的模型路由:宿主没给这个会话配到能用的 provider 时,
prompt会抛session/model-unavailable,此时插件降级成「只通知」,通知里会写明原因。 bridge_complete里「写 outbox / 回写 inbox」与「交付」是分开报告的:交付失败 不会让「结果已落盘」变成失败,返回值里delivery.ok会如实为false。 v1 的uplink字段仍在返回值里(兼容老读者),它现在反映的是交付结果。- token 只在本机使用,不下发到任何外部端点;插件只连
bridge_access.json里的127.0.0.1地址。 - 改完本插件必须重启 dsh(bundle 插件 + ESM 缓存,见上面第 2 条)。 重启前跑的仍是旧代码 —— 包括「还在直发用户」的 v1 行为。
- 改 profile 补丁层(
cordis.patch.yml里的配置覆盖)也要重启:实测 2026-09-12 改完$DSH_HOME/profiles/desktop/cordis.patch.yml(dispatchPreset: standard)后, 34 秒后自动拉起的会话仍是"agentPreset":"teyvat-hoi4",bridge_status.json也没变成 新结构 —— 即补丁没有被热重载。原因在宿主侧:热重载那条路watchUserPatches()要求ctx.get('hmr')存在,而 base 里hmr那一行是disabled: true(dsh-app-boot的 注释说 CLI launcher 有自己的 watch-only 回退,Desktop 这条路径没有)。所以 desktop profile 上的patchReload: live实际等于「下次启动才生效」。 判定方法:重启后看cache/bridge_status.json有没有新的dispatch块、dispatch.preset是不是standard—— 有就是新代码+新配置都生效了。
