zhiwuli0228/dsh-image-router0

dsh-image-router

DeepSeek Harness plugin: analyse images out of band with a vision model — pasted images are digested into text before admission and a describe_image tool covers image paths, all without ever changing the session's model.

包名
dsh-image-router
版本
0.3.4
许可证
MIT
最近更新
2026年9月12日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:zhiwuli0228/dsh-image-router

配置视觉路由:两条路,都不需要手写 YAML

图片发给谁,由 vision 决定。它有两档,装好后可以在 设置 → 插件 → 插件配置 的卡片里直接切换:

① 用这个部署里已经配好的模型

卡片会列出支持图片输入的模型供点选,不用记 provider id:

  • 优先读 $DSH_HOME/settings.yamlllm-pi-ai 段已经声明了 models 的路由(这正是设置 → 模型页写的那些);
  • 否则逐个 provider 调用 remote.llm.discoverModels('image-router', { provider }),由宿主侧的图片能力 oracle 回答(见下);
  • 下拉里找不到就手填 provider / model(目录型的路由可以服务卡片枚举不到的模型)。

图片能力 oracle 是怎么来的:浏览器唯一的模型类接口是 remote.llm.discoverModels(settingsNs, request),且只能按命名空间提问。本插件因此在自己的命名空间 image-router 上注册了一个 discovery handler,用 ctx.llm.listModels(provider) 读每条路由的模型,再按 inputModalities 过滤:

  • 模型显式声明了模态 → 声明说了算(声明 ['text'] 的即使 id 里有 vision 也剔除);
  • 路由什么都没披露 → 退回到 id 里的视觉家族特征(vision / vl / omni / 4o / gemini …),因为目录型路由未必公开这个字段;
  • 未知/不可达路由 → 空列表,不抛错。

实测:对 deepseek-official(4 个模型)只返回 2 个图片模型(deepseek-flashdeepseek-v4-flash-vision-exp);审计写 discover provider=… models=4 imageCapable=2

对应的配置就是:

- id: image-router
  config:
    vision: { provider: qwen-token-plan-cn, model: qwen3.8-flash }

② 自定义端点:填 baseURL + 模型 + API Key

没有现成的视觉模型?卡片里切到「自定义端点」,只填三样东西:地址、模型名、API Key

插件不自己实现协议,也不自己存密钥 —— 它把这三样翻译成上游本来就有的东西:

你填的落到哪里
baseURL / 协议 / 模型 / 显示名$DSH_HOME/settings.yamlllm-pi-ai.providers.image-router-vision只写这一条路径,你原有的 provider 一个字节都不动)
API Key凭据库 $DSH_HOME/.credentials.yamlrefs.IMAGE_ROUTER_VISION_API_KEY;路由里只留引用名
图片能力声明模型条目上的 input: [text, image]

也就是说:线协议、模型发现、图片投影、重试全都由 @deepseek-ai/dsh-llm-pi-ai 负责,本插件只是"把卡片上的三个字段写成 DSH 的配置"。因此 dsh web 不需要重启,下一次判定就用新路由。

- id: image-router
  config:
    vision:
      endpoint:
        baseURL: https://gateway.example/v1
        model: gpt-4o-mini
        api: openai-completions   # 可选:openai-completions / openai-responses / anthropic-messages
        apiKey: sk-…              # 可选:写入凭据库后即从配置里消失(只写不读)

几个值得知道的细节:

  • endpoint 优先于 provider/model;两者都在时用显式的 provider/model,所以老配置不会被动改变行为。
  • API Key 是只写字段:Host 收到后写进凭据库;组合 base 与卡片草稿都会剥掉它,所以它既不回显、也不写进本插件的配置、也不进 trace。这条是实测修出来的 —— 早先的实现让 describe() 返回的已解析值带着密钥,明文于是落进了 $DSH_HOME/settings.yamlimage-router.vision.endpoint.apiKey。现在有两道独立防线:configBase 剥掉 endpoint.apiKey(设置节本身不可能持有密钥),卡片编辑器用 withoutKey(...) 播种(旧版本写下的密钥不会被回填进草稿,也就不会被下一次保存写回去)。
  • 凭据存成引用名$DSH_HOME/.credentials.yamlrefs.IMAGE_ROUTER_VISION_API_KEY(可用 apiKeyEnv 改名)。选引用名而不是泛型 key,是因为 pi-ai 解析 apiKeyEnv 走的正是这一层 —— credentialRef(name)ctx.credentials.resolve(...),与你原有的 QWEN_TOKEN_PLAN_CN_API_KEY 同一种形态;泛型 key 在这一层解析不到。
  • 别关掉「声明支持图片输入」:pi-ai 对自定义路由的默认模态是 ["text"],关掉之后发过去的图片会被投影成占位文字,端点收不到图。
  • 想在设置 → 模型里管理这条路由也行:它就是一条普通的 llm-pi-ai 路由,displayName、超时、协议都能在那边继续改。
  • 卡片会在审计文件里留一行 endpoint-route-live … modalities=text+image,用来回答"我配的端点到底生效了吗"。

在图形界面里配置

插件注册了一个设置命名空间 image-router,并自带浏览器一半:装上后 设置 → 插件 → 插件配置 里会出现一张 image-router · 图片旁路识别 卡片。

卡片里的 vision 路由有两档(详见上面「配置视觉路由」):

  • ① 已配置模型:从 llm-pi-ai 设置段 + 宿主侧图片能力 oracle(remote.llm.discoverModels('image-router', …))汇总出的下拉,只列支持图片的模型,点选即可,另有 provider / model 手填兜底。
  • ② 自定义端点:填 baseURL / model / apiKey(+ 可选协议、显示名、凭据引用名)。保存后由宿主写进 llm-pi-ai 与凭据库,不重启即可用。
卡片字段生效时机
modevision(两档都算)、instructionmaxTokenstimeoutMslabel保存后立即生效 —— 下一次提示词、下一次旁路调用、下一次工具调用就用新值,不需要重启
tool(是否注册 describe_image)、traceFile(审计文件路径)挂载时确定,改动需要重启 dsh web(卡片里没有这两项,免得承诺做不到的事)
  • 卡片显示的是当前生效值:profile 补丁层的配置作为该设置节的 base,卡片里的保存只是叠在它之上的一层覆盖(落在 $DSH_HOME/settings.yamlimage-router: 段)。
  • 写坏不会破坏正在工作的插件:校验失败的覆盖会被忽略,继续用上一份好配置,并在审计文件里记一行 settings-invalid …
  • 端点档只写自己那一条路径:用 settings.mutate('llm-pi-ai', [{op:'set', path:['providers','image-router-vision'], value}]),你原有的 provider 不会被整体覆盖;撤掉端点档时发的是对应的 unset,凭据保留(可能还想复用)。这条路径上的 set替换整个 profile 对象,所以写入前先 settings.get('llm-pi-ai') 读回现值做合并 —— 你在「设置 → 模型」给这条路由加过的 headerscompattimeoutMs 不会被一次保存抹掉(实测:预置的 x-tenant 头与 5000ms 超时在保存后仍在,而 models 被重建为本轮的值)。
  • 两半必须用同一个 namespace:宿主 lib/settings.jsSETTINGS_NAMESPACE 与浏览器 client/client.jsNAMESPACE
  • 宿主侧只走 ctx.inject(['settings']) + settings.register(ns, schema, { base })绝不 import @deepseek-ai/dsh-settings 的命名导出 —— 上游删过 installSettingsSection,而缺失的命名导出是模块求值期 SyntaxError,会让宿主启动失败退出 1(dshmarket 踩过这个坑)。
  • 浏览器一半靠 ctx.remote(来自 dsh-api-remotes,已写进本包 dsh.client.inject)读取模型清单;没有远端服务的部署会退化成纯手填,卡片照常渲染。
  • 浏览器一半的 inject 必须是空数组,两个服务都靠可选注入拿:ctx.inject(['slots','settingsScope'], …)slots 不是包名、也不在任何客户端清单里(它是隐式提供的服务),而声明了加载器满足不了的依赖会让 entry 永久 pending 且毫无提示 —— apply 从不执行、卡片从不注册、页面什么都不显示、控制台一片干净。这正是本插件卡片长期不出现的两个原因之一(另一个是下面这条)。宿主半边用的是同一手法,所以薄部署也能激活。
  • 只有注册了命名空间有浏览器一半注册 settings.plugin.item 卡片的插件才会出现在那个页面:两者缺一都不会渲染任何东西。该 tab 的实现是 namespaces.map(ns => renderSlot('settings.plugin.item', {}, { entryKey: ns })) —— 所以"宿主 serve 的命名空间"与"卡片 claim 的 key"必须完全一致。
  • 激活状态可观测:浏览器里读 window.__imageRouter 会得到 {applied, slots, scope, slotDispatched, registered, error},逐步说明走到了哪一步。这条路径上的失败方式全是静默的,所以状态必须可读、不能靠猜。

3. 在 profile 的 cordis.patch.yml 里写配置覆盖(完整示例见 examples/cordis.patch.yml)

没有现成的视觉模型也可以只给端点,见下面「配置视觉路由」的 ②。


### B. 从 GitHub 安装(等价形态)

```powershell

# 3. 在 profile 的 cordis.patch.yml 里写配置覆盖(完整示例见 examples/cordis.patch.yml)

# 之后同样加进 dsh.profile.bundles,并在 profile 的 cordis.patch.yml 写同 id 的配置覆盖

装好后这些配置项也能在 设置 → 插件 → 插件配置 的卡片里改(见下),保存即时生效。

然后重启 dsh web

C. 本地开发 / 不想装依赖:按路径挂载

零安装,只要把仓库放在磁盘上:


## 配置项

| 键 | 默认 | 说明 |
|---|---|---|
| `mode` | `digest` | `digest`=旁路分析并替换文字(不改模型);`switch`=临时借用视觉路由 |
| `vision` | 必填(二选一) | 旁路调用的路由。**①** `provider` / `model` / 可选 `reasoningEffort`;**②** `endpoint: { baseURL, model, api?, name?, apiKey?, apiKeyEnv?, images? }` —— 由宿主翻译成上游 `llm-pi-ai` 的一条路由。两者同时存在时 ① 优先 |
| `instruction` | 内置(提取文字 + 描述画面) | 给视觉模型的指令 |
| `maxTokens` | `900` | 分析结果上限 |
| `timeoutMs` | `120000` | 单次旁路调用的超时 |
| `label` | `true` | 在替换文本前加 `[图片分析 · provider/model]` 标记 |
| `tool` | `true` | 是否注册 `describe_image`(按路径按需分析图片的模型工具) |
| `traceFile` | 无 | 审计文件:挂载、每次图片分析、工具调用、失败原因 |
| `dryRun` | `false` | 只写审计、不改提示词、不调视觉模型 |
| `text` / `holdTurns` / `sticky` | — | **仅 switch 模式**:归还兜底路由、图片后再保持视觉模型的轮数、永不归还 |
| `imageExtensions` / `hint` | — | **仅 switch 模式**:文本里的图片扩展名/正则判定 |