快速了解
它能做什么
通过独立视觉模型分析粘贴或本地图片,同时保持 DeepSeek Harness 会话模型不变。
本站提供的是中文说明,不代表该项目或 Plugin 自身提供中文界面;语言支持请以上游文档为准。
Web Profile
Not declared in supplied evidence
证据已验证
核对日期 2026/9/12 UTC 14:12
选择前先看
DSH Image Router 是一个 DeepSeek Harness bundle 插件。它会先让视觉模型把图片转成文字分析,再交给主会话模型处理。默认 digest 模式会用分析文字替换粘贴或上传的图片,因此即使当前会话使用纯文本模型也能处理带图提示;它还提供 describe_image,用于按需分析本地图片文件。
适合谁
希望在不切换当前聊天模型的情况下使用图片理解能力的 DeepSeek Harness Web profile 用户。
常见任务
- 在纯文本模型会话中解读粘贴的截图、报错图片或上传图片。
- 让模型调用 describe_image 分析本地 PNG、JPEG、WebP 或 GIF 文件路径。
- 在插件设置中选择已有的图片模型路由,或配置自定义视觉端点。
权限与数据
通过已配置的 DSH 服务分析图片,并可选地配置自定义视觉路由。
权限- 调用 describe_image 时读取受支持的本地图片文件。
- 在 digest 模式下于提示词准入前处理包含图片的提示。
- 可能通过 DSH settings 服务写入插件设置和上游路由设置。
- 粘贴图片会被发送到配置的视觉路由做一次性分析;digest 模式会在提示历史中保留返回文字,而非图片块。
- 自定义端点 API Key 被描述为只写,并作为凭据引用保存,而非保存在插件配置中。
- 可选审计输出可能记录挂载、分析、工具调用和失败信息。
- 已配置的视觉模型服务商或自定义端点。
- 已有视觉路由使用其现有凭据。
- 自定义端点可能需要 API Key,并将其存入 DSH 凭据库。
局限
- 必须配置可用的视觉路由或自定义视觉端点;否则插件会以警告方式停用。
- digest 模式会在会话历史中以文字分析替换图片,而不会保留图片块。
- 发送提示词会等待旁路视觉分析完成,并受配置超时限制。
- README 将提示词包装描述为非官方扩展点;宿主内部实现变化时,插件可能退化为不处理图片。
- 尚未通过 GUI 评审验证设置卡片的实际视觉效果。
DSHub 已核对
- 提供的 README 报告了 57 个通过的测试、隔离 web profile 启动、digest 与 describe_image 端到端探针、设置注册和自定义端点路由测试。
- 包清单声明了 DSH bundle patch、Web 客户端注入、Node.js >=20,以及可选的 @deepseek-ai/schemastery peer dependency。
- 提供的记录已验证不可变 Git bundle 结构。
DSHub 未核对
- 未在当前用户的 DSH 环境中执行安装。
- 未独立验证具体 Harness 版本的运行兼容性、服务商可用性、凭据、额度或图片分析质量。
固定版本安装
安装 DSH Image Router
这个Plugin Bundle没有 DSH Plugin 安装操作,请根据源码文档使用真实交付方式。
维护者原文
项目 README
dsh-image-router
给 DeepSeek Harness 用的图片旁路识别插件:图片交给视觉模型分析一次,把结果作为文字放回提示词 —— 你的会话模型自始至终不变。
你贴一张截图,问「这个报错怎么修」
↓ 插件在「提示词准入」之前拦下这次提示词
qwen-token-plan-cn / qwen3.8-flash ← 一次旁路调用(不改会话路由、不写 model/selection)
↓ 分析结果替换掉图片,成为提示词里的一段文字
你原本选定的模型(GUI 里显示的那个)继续作答
这是 Codex 侧 image-router 的同一套语义:用完即走的一次调用,不是把会话切成视觉模型。
覆盖两条路径:
| 场景 | 机制 |
|---|---|
| 提示词里自带图片(贴图 / 上传) | digest:准入前旁路分析 → 图片块替换为文字(自动,模型无感) |
| 图片文件路径(「读一下 x.png」) | describe_image 工具:模型按需调用 → 返回文字分析(任何模型都能用,包括纯文本模型) |
两种模式
mode: digest(默认) |
mode: switch(可选) |
|
|---|---|---|
| 会话模型 | 永不改动 | 图片轮次临时借给视觉模型,之后归还 |
| 图片去向 | 旁路分析后替换为文字 | 原样发给视觉模型 |
| 会话历史里 | 留下文字分析(图片块被替换) | 留下图片 |
模型选择器 / [model changed] 提示 |
不出现 | 会出现 |
| 适用 | 想一直用自己的主力模型 | 想让视觉模型亲自看整轮上下文 |
auto / sticky 是旧的模式名,仍被接受并映射到 switch。
配置视觉路由:两条路,都不需要手写 YAML
图片发给谁,由 vision 决定。它有两档,装好后可以在 设置 → 插件 → 插件配置 的卡片里直接切换:
① 用这个部署里已经配好的模型
卡片会列出支持图片输入的模型供点选,不用记 provider id:
- 优先读
$DSH_HOME/settings.yaml里llm-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-flash、deepseek-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.yaml → llm-pi-ai.providers.image-router-vision(只写这一条路径,你原有的 provider 一个字节都不动) |
| API Key | 凭据库 $DSH_HOME/.credentials.yaml → refs.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 收到后写进凭据库,不回显、不写进本插件的配置,也不进 trace。
- 凭据存成引用名:
$DSH_HOME/.credentials.yaml的refs.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,用来回答"我配的端点到底生效了吗"。
按需工具 describe_image
插件注册一个模型可调的工具(tool: true,默认开启):
describe_image(file_path: string, question?: string) -> string
- 读文件走部署自己的文件系统服务(同一套沙箱与 cwd 解析),按字节签名识别 PNG/JPEG/WebP/GIF(无扩展名也行),落盘成附件,然后做一次旁路调用,返回文字分析。
question可选:填了就追加到分析指令里(例如「图里的报错是什么?」)。- 它在纯文本模型上也能用:图片从不进入会话,
read_image的「当前路由必须声明 image」那道门槛对它不适用。 - 失败会抛出可读错误:
not found/not a regular file/not a supported PNG/JPEG/WebP/GIF image/the vision model produced no analysis。
在图形界面里配置
插件注册了一个设置命名空间 image-router,并自带浏览器一半:装上后 设置 → 插件 → 插件配置 里会出现一张 image-router · 图片旁路识别 卡片。
卡片里的 vision 路由有两档(详见上面「配置视觉路由」):
- ① 已配置模型:从
llm-pi-ai设置段 + 宿主侧图片能力 oracle(remote.llm.discoverModels('image-router', …))汇总出的下拉,只列支持图片的模型,点选即可,另有provider/model手填兜底。 - ② 自定义端点:填
baseURL/model/apiKey(+ 可选协议、显示名、凭据引用名)。保存后由宿主写进llm-pi-ai与凭据库,不重启即可用。
| 卡片字段 | 生效时机 |
|---|---|
mode、vision(两档都算)、instruction、maxTokens、timeoutMs、label |
保存后立即生效 —— 下一次提示词、下一次旁路调用、下一次工具调用就用新值,不需要重启 |
tool(是否注册 describe_image)、traceFile(审计文件路径) |
挂载时确定,改动需要重启 dsh web(卡片里没有这两项,免得承诺做不到的事) |
- 卡片显示的是当前生效值:profile 补丁层的配置作为该设置节的 base,卡片里的保存只是叠在它之上的一层覆盖(落在
$DSH_HOME/settings.yaml的image-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')读回现值做合并 —— 你在「设置 → 模型」给这条路由加过的headers、compat、timeoutMs不会被一次保存抹掉(实测:预置的x-tenant头与 5000ms 超时在保存后仍在,而models被重建为本轮的值)。 - 两半必须用同一个 namespace:宿主
lib/settings.js的SETTINGS_NAMESPACE与浏览器client/client.js的NAMESPACE。 - 宿主侧只走
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},逐步说明走到了哪一步。这条路径上的失败方式全是静默的,所以状态必须可读、不能靠猜。
为什么需要它
DSH 内置行为是「图来了但模型不收图」时静默降级或直接拒绝:
| 位置 | 内置行为 |
|---|---|
dsh-llm/lib/index.js |
路由模型不声明 image 而消息里有图 → projectImagesForTextModel() 把图片换成文本占位符 |
dsh-api-session-controller/lib/index.js |
Web 提示词准入时校验当前会话模型,不声明 image 直接抛 MODEL_DOES_NOT_SUPPORT_IMAGES |
dsh-tool-fs/lib/index.js |
read_image 在当前步骤路由不声明 image 时拒绝执行 |
digest 模式在准入之前就把图片换成文字,所以它同时绕过了上面第一、二道门槛:连纯文本模型也能收到带图的提示词(收到的是文字分析)。
实现要点(全部来自实测)
- 准入前替换:包装
sessionController.prompt,在调用原方法之前改写request.content。图片块被移除、替换成一段文字 —— 因此准入校验看不到图片,不会拒绝、也不需要改模型。 - 图片内容的两种形态:wire 形态
{ type:'image', mediaType, data(base64), name? };准入后形态{ type:'image', attachment: ImageAttachmentRef }。插件用ctx.attachments.admitPromptContent(images)把前者落盘成后者(dsh-attachment/lib/types/types.d.ts:89-113)。 - 旁路调用是标准一次性请求:
ctx.llm.stream({ provider, model, messages, maxTokens, sessionId, signal }),消息为{ id, role:'user', source:{kind:'plugin', plugin:'image-router'}, content:[{type:'text',…},{type:'image',attachment:ref}] }。端点、凭证、重试策略、附件解析全部复用部署自己的配置。- 不传
purpose:它的类型是封闭枚举'compaction' | 'session-title'(dsh-llm/lib/types/types.d.ts:443),自定义值不合法。 signal是AbortSignal.any([调用方 signal, AbortSignal.timeout(timeoutMs)]),取消与超时都生效。- 这次调用不写入会话日志(对比
dsh-session-title-llm会 append 一条session/title-llm-request)。
- 不传
- 失败就保留图片:视觉调用失败(配额、超时、报错)时不替换,把原请求交给原路径,让它给出真实的错误(例如 provider 的 429 或模态拒绝),而不是被插件掩盖。
- 替换文本里不放文件名:
shot.png这种名字会被模型当成可读路径去调read_image,在纯文本模型上换来一条没必要的报错。文件名只留在审计文件里。 - 必须用可选注入:
sessionController只由dsh-web-app提供;顶层inject会让 headless/sdk/acp profile 启动失败(1 entry did not activate)。因此用ctx.inject(['sessionController'], …)。 - 端点档要等三个服务一起就绪:
ctx.inject(['llm', 'settings', 'credentials'], …)。只等llm是个真实的坑 —— 那个回调在 entry 激活时就跑,此时设置节还没注册,get('settings')返回undefined,端点同步被静默跳过,于是在"有 settings 服务的部署"里也永远写不进去(实测踩到,审计里那行endpoint-services settings=no就是它)。 - "跳过"不能算"已完成":同步只在真正落盘(
endpoint-route-ok)后才记下签名,否则一次过早的跳过会让签名永久命中缓存,端点再也不会重试。 - 密钥只写不读:
apiKey从配置里取走后立即从内存副本上剥掉(不改共享的已解析配置对象),并且不参与签名比对 —— 否则同一个端点会不断被判定为"变了"而重复写凭据。 - 自定义路由必须声明图片模态:pi-ai 的
defaultInput是["text"],所以路由的模型条目不写input: [text, image]就会被当成纯文本模型,图片会被投影成占位文字而不是发到端点。卡片默认开这个开关。 - 解析探测留痕:写完后做一次只读的
llm.resolveModelInfo(provider, model),把结果写进审计(endpoint-route-live … modalities=text+image)—— 这是"我配的端点到底生效没有"唯一可读的答案。 - 图片能力 oracle 注册在自己的命名空间上:
ctx.llm.registerModelDiscovery('image-router', …)。浏览器唯一能发的模型类请求是remote.llm.discoverModels(settingsNs, request),而settingsNs只能填它知道的命名空间 —— 填自己的,才能让这次调用回答"这条路由里哪些模型真收图",而不是"这条路由有哪些模型"。过滤必须留在宿主:inputModalities从不过河到浏览器。 - 写入必须"读-改-写":
settings.mutate的路径操作在['providers','image-router-vision']上是整体替换(applyPathOp里是{...section, [head]: op.value}),而这条路由同时也是「设置 → 模型」页能编辑的路由 —— 直接写会把用户在那边加的headers/compat/timeoutMs抹掉。所以先settings.get('llm-pi-ai')读回现有 profile,把本轮拥有的字段(displayName/api/baseURL/apiKeyEnv/models)覆盖上去、其余原样保留。读服务用的是settings.get(ns)(返回解析后的值),不是臆造的方法名 —— 这一点正是靠真机验证才发现的(settings.namespace is not a function)。
已验证
| 环节 | 结论 |
|---|---|
| 单测(digest 替换 / 失败保留 / 多图合并 / dryRun / 工具调用 / 设置覆盖 / 开关模式状态机 / 安全降级 / 审计 / 自定义端点档 / 图片能力 oracle / 上游 profile 合并 / schema 三方消费者契约 / 浏览器半边激活契约) | ✅ 57 个用例通过 |
| 挂载进真实 web-profile 树 | ✅ 隔离实例冷启动,审计写 mounted mode=digest … |
标准 bundle 形态可装载(包名进 dsh.profile.bundles → 包内 dsh.bundle.patch → 部署层 config 覆盖 → Config 补默认值) |
✅ 隔离实例冷启动实测,schemastery 与回退 Standard Schema 两条路径都跑过 |
无 sessionController 的 profile 仍能启动 |
✅ headless 冷启动 exit=0,审计只有 apply-entered |
| digest 端到端:会话模型不变 + 图片被正确识别 | ✅ 隔离实例实测(下方输出) |
describe_image 注册 + 执行全链路 |
✅ 隔离实例实测(下方输出) |
| switch 端到端(借出→保持→归还) | ✅ 隔离实例实测(router-sequence-probe) |
| 设置命名空间注册 | ✅ 隔离实例实测(审计写 settings-registered ns=image-router) |
| 自定义端点档写进上游(真实 web profile,非 mock 服务) | ✅ 隔离实例实测:settings.yaml 出现 providers.image-router-vision(含 apiKeyEnv 引用与 input: [text, image]),.credentials.yaml 的 refs 出现 IMAGE_ROUTER_VISION_API_KEY,原有 provider 未变 |
| 端点档写完后路由真的可用 | ✅ 同一实例审计写 endpoint-route-live provider=image-router-vision model=mock-vision modalities=text+image |
ctx.llm.stream 经该路由打到 mock 端点(图片随行) |
✅ 隔离实例内探针实测:listProviders() 出现 image-router-vision(未重启)、resolveModelInfo 报 ["text","image"]、admitPromptContent 得到 image/png 1x1 70B 引用、stream 收齐 block-start → 3×text-delta → block-end → usage → finish:stop,文本 MOCK STREAM ANSWER;mock 侧线上记录 POST /v1/chat/completions + authorization: Bearer sk-ve… + sawImage: true |
| 图片能力 oracle 真的按模态过滤 | ✅ 同一实例实测:discoverModels('image-router', {provider:'deepseek-official'}) 从 4 个模型里筛出 2 个图片模型;自定义端点路由返回 mock-vision;未知路由返回 [] 而不抛错 |
| 保存不会毁掉用户在模型页对该路由的编辑 | ✅ 同一实例实测:预置 headers: {x-tenant: acme} 与 timeoutMs: 5000 的派生路由,保存后两者仍在,models 被重建为 [{id: mock-vision, input: [text, image]}],过期模型列表消失,其它 provider 未变 |
回退 schema 能过 settings.describe() |
✅ 隔离实例实测(审计记 schema=fallback 以证明走的是回退形态):describe({redactSecrets:true}) 无错返回 8 个命名空间,image-router 条目带 schemaType: object、13 个 valueKeys、applies: live。此前真机因缺 toJSON() 报 加载提供方目录失败,导致所有插件卡片都不渲染 |
| 浏览器半被发现并提供 | ✅ 隔离实例实测(boot manifest 的 combo 清单含 dsh-image-router/client.js,取回 200 且内容含本插件模块);卡片两档的渲染用 React 替身跑过(下拉/端点字段/控件数),视觉外观需你在 GUI 里看一眼 |
digest 端到端实测输出(tools/digest-probe.mjs):
session model before prompt: deepseek-official/deepseek-flash
prompt accepted (digest ran before admission)
session model after prompt: deepseek-official/deepseek-flash ← 模型完全没动
digest session=probe-digest images=1 chars=147 names=probe.png ← 旁路分析产出 147 字
assistant/message source={provider:'deepseek-official', model:'deepseek-flash'}
→ 「图片上的文字是深蓝背景左上角的两行英文——橙黄色的 "VISION ROUTER" 和其正下方白色的 "PROBE 42"。」
describe_image 实测输出(tools/tool-probe.mjs,直接从活的工具注册表取定义后执行):
describe_image in registry: found
execute ok, 113 chars:
1. 文字内容:VISION ROUTER / PROBE 42
2. 画面描述:画面背景为深蓝色。左上角包含两行左对齐的粗体无衬线英文字符……
审计:tool session=probe-tool-session path=…\probe.png mediaType=image/png bytes=2322 chars=113
安装
这是一个标准 DSH bundle 插件包:package.json 声明 dsh.bundle.patch,包内 cordis.patch.yml 只插入一条 loader 条目(部署无关,默认值全在 Config 里)。
A. 从 npm 安装(推荐)
已发布:dsh-image-router(MIT)。
# 1. 装进 profile
dsh plugin --profile web add dsh-image-router
# 2. 把包名加进 profile 的 bundles 列表
# $DSH_HOME/profiles/web/package.json → dsh.profile.bundles: [..., "dsh-image-router"]
# 3. 在 profile 的 cordis.patch.yml 里写配置覆盖(完整示例见 examples/cordis.patch.yml)
# - id: image-router
# config:
# vision: { provider: <provider>, model: <vision-model> }
# 没有现成的视觉模型也可以只给端点,见下面「配置视觉路由」的 ②。
B. 从 GitHub 安装(等价形态)
# 1. 装进 profile(pnpm 转发;也能换成本地 link:)
dsh plugin --profile web add github:zhiwuli0228/dsh-image-router
# 2. 把包名加进 profile 的 bundles 列表
# $DSH_HOME/profiles/web/package.json → dsh.profile.bundles: [..., "dsh-image-router"]
# 3. 在 profile 的 cordis.patch.yml 里写配置覆盖(完整示例见 examples/cordis.patch.yml)
# - id: image-router
# config:
# vision: { provider: <provider>, model: <vision-model> }
没有 GitHub 网络时用本地 checkout(等价形态,pnpm 会把包拷进 profile 的 node_modules):
dsh plugin --profile web add file:E:\path\to\dsh-image-router
# 之后同样加进 dsh.profile.bundles,并在 profile 的 cordis.patch.yml 写同 id 的配置覆盖
装好后这些配置项也能在 设置 → 插件 → 插件配置 的卡片里改(见下),保存即时生效。
然后重启 dsh web。
C. 本地开发 / 不想装依赖:按路径挂载
零安装,只要把仓库放在磁盘上:
# $DSH_HOME/profiles/web/cordis.patch.yml
- insert:
- id: image-router
name: '<repo>\lib\index.js' # 绝对路径或 ./-相对路径都接受
config:
mode: digest
vision:
provider: qwen-token-plan-cn
model: qwen3.8-flash
traceFile: '<repo>\trace.log'
改完必须重启 dsh web。 profile 补丁层确有 live reload(patchReload: live)——实测配置改动会重新 apply,但它不会重新 import 已经加载过的宿主模块,所以代码变更只有重启才可靠生效。
为什么包内不需要 node_modules
Config 采用双形态:能解析 @deepseek-ai/schemastery 时用它;解析不到则退回一个零依赖的等价实现。而后者才是实际发布出去的那一份 —— 这个 import 从本文件自己的位置解析,宿主的 node_modules 在 DSH 安装目录里、不在 profile 往上的解析路径上;实测在真实 profile 中(插件以 file: 装进 profile)schemaForm 恒为 'fallback'。
所以回退形态必须同时满足三个消费者,缺任何一个都会在真机上炸:
| 消费者 | 要什么 | 缺了的后果 |
|---|---|---|
| Cordis(loader) | ~standard.validate(Standard Schema 接口,cordis/lib/index.js:runtime.Config['~standard'].validate(config)) |
启动失败 |
settings 服务 resolve() |
schema 可调用 —— 它直接 schema(mergeLayers(base, section)) 来叠默认值 |
设置节解析抛 TypeError: schema is not a function |
settings 服务 describe() |
schema.toJSON() |
整个提供方/设置目录加载失败,于是所有插件卡片都不渲染(实测报错:加载提供方目录失败: registration.schema.toJSON is not a function) |
redactSecrets 的遍历是防御式的(node.type 不认识就原样返回值),所以回退形态不需要 schemastery 的元数据即可安全通过。
插件会在审计的 apply-entered 行打出 schema=fallback|schemastery —— 这两种形态从外面看不出区别,而"我这儿跑的到底是哪一份"曾是定位真机故障时最难的一环。@deepseek-ai/schemastery 因此声明为可选 peerDependency(不拉取、用宿主自带),见 package.json。
配置项
| 键 | 默认 | 说明 |
|---|---|---|
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 模式:文本里的图片扩展名/正则判定 |
审计与排查
mounted mode=digest vision=qwen-token-plan-cn/qwen3.8-flash maxTokens=900 timeoutMs=120000 label=true dryRun=false
tool-registered name=describe_image vision=qwen-token-plan-cn/qwen3.8-flash
digest session=<id> images=1 chars=147 names=probe.png # 一次成功的旁路分析
tool session=<id> path=<路径> mediaType=image/png bytes=2322 chars=113 # 一次 describe_image 调用
digest-dryRun session=<id> images=2 # dryRun 下的判定
digest-failed reason=error images=1 # 视觉调用失败 → 保留图片
digest-empty images=1 # 视觉模型没输出文本 → 保留图片
digest-admit-failed <原因> # 图片落盘失败 → 不替换
digest-skipped no attachment service # 没有 attachments 服务
tool-failed <原因> # 工具定义没注册成功
session=<id> error=<消息> at=<栈帧> # 插件内部异常(已兜住,提示词照常发出)
排查顺序:有没有 mounted mode=digest(没有 → 没挂上,检查补丁与重启)→ 贴图那一刻有没有 digest 行(没有 → 图片没进到插件,看是不是 file 而非 image 块)→ 有没有 digest-failed / reason=(旁路调用被拒,通常是额度或该路由不可用)→ 工具是否可用看有没有 tool-registered。
行为细节与限制
- 会话历史里留下的是文字分析,不是图片:这是「不切模型」的必然代价(图片块被替换)。附件本身仍持久保存在
$DSH_HOME/attachments/下,审计行也记了文件名。 read_image与describe_image的分工:read_image是 DSH 自带工具,受「当前步骤路由必须声明image」限制;describe_image是本插件提供的,走旁路调用,任何模型都能用。主力换成纯文本模型后,让模型改用describe_image即可(工具 description 已写明适用场景)。- 一次提示词一次调用:同一条提示词里的多张图合并成一次视觉调用,产出一个文字块;
describe_image每次调用只处理一个文件。 - 准入会等旁路调用:发送时
session/prompt会等到分析完成才返回(通常几秒到十几秒,受timeoutMs约束),GUI 的发送按钮在此期间保持等待。 - 旁路调用计入你的额度:走的是
vision路由的 provider 与凭证(和正常请求同一套配额),但不进会话日志。 - 包装
sessionController.prompt是非官方扩展点:未来版本若改名/换实现,插件会退化为「不处理 + 记 warning」,不会崩。工具注册走官方ctx.tools.register(),只要 JSON Schema 合法就稳。 - 失败即放行:任何异常都只写审计 + 一条 warning,提示词照常准入。
测试
cd <repo>
node --test # 标准写法(Node 自带发现,20/22/24 通用)
node test/routing.test.js # 单进程直跑:没有管道支持的沙箱里用这个
node --test test/不可移植:Node 20 会扫描目录,Node 22+ 把test/当成单个入口文件去加载而报MODULE_NOT_FOUND(CI 就是靠多版本矩阵抓到这个的)。
57 个用例:配置归一化与校验(含旧模式名映射、schema 物化出的空对象/空数组、端点档补齐 vision 路由与两档优先级)、图片信号判定、digest 的替换/多图合并/失败保留/无图直通/dryRun/落盘失败、describe_image 的注册/读文件/问题透传/缺文件与目录与非图片的拒绝、设置节的注册与「保存后下一轮即生效」、校验失败的覆盖被忽略、自定义端点写入上游路由与凭据(含"跳过/失败不得记为已同步"这一可重试契约、以及"合并而非覆盖上游 profile")、图片能力 oracle(声明优先于 id、按命名空间注册、过滤、未知路由不抛错、无 llm 服务时退化)、schema 三方消费者契约(~standard / 可调用 / toJSON+safeParse)、浏览器半边激活契约(inject 必须为空、以自己命名空间 claim card、服务缺席时干净降级)、switch 的借出‑归还‑放弃状态机、手选模型不被覆盖、holdTurns、sticky、路由校验与缓存、冷会话不路由也不告警、门面两种布局、审计可写与不可写、无 sessionController 时不包装、卸载恢复。
CI(.github/workflows/ci.yml)在 Ubuntu + Windows × Node 20/22/24 上跑同一套用例,并校验「dsh.bundle.patch 指向的文件存在、入口导出 name/Config/apply」这条打包契约。
tools/ 下五个开发用探针(都可作为一条 insert 挂进 profile;路径均按自身位置推导,任意 checkout 可用):
| 探针 | 用途 |
|---|---|
digest-probe.mjs |
在真实树里跑一次带图提示词,验证「模型不变 + 图片被识别」 |
tool-probe.mjs |
从活注册表取出 describe_image 并执行一次,验证工具全链路 |
router-sequence-probe.mjs |
switch 模式:借出 → 保持 → 归还 的完整序列 |
service-probe.mjs |
打印各服务的方法形状 |
router-call-probe.mjs |
复刻插件调用链,记录 {agent}/{error} 形状与栈 |
参考实现
- kanchengw/image-router — Codex++ 的 HTTP 代理:拦截
image_url,调 VL API 生成[IMAGE ANALYSIS]文字后替换图片再转发(本插件 digest 模式即此语义) - lll888666/codex-deepseek-vision-fallback
- rongyaozhixing/codex-vision-plugin
- ningyougan/deepseek-vision-mcp
有意识地管理
安装与管理
前置条件与目标 Profile
目标: Web Profile
交付方式: Git Bundle — zhiwuli0228/dsh-image-router#4b500f91ad4e2947652d522a94a38ab9ff20d0e9。
验证、更新与移除
显示生命周期命令
dsh plugin --profile web list兼容性与访问范围
DSH bundle; Node.js 20 or later declared: Not declared in supplied evidence。
风险事实
Sends images to the configured vision-model provider for analysis.
证据 ↗A custom endpoint API key is written to the DSH credential store as a named reference.
证据 ↗The describe_image tool reads local image files through the deployment file-system service.
证据 ↗Each image analysis uses the configured vision route and may consume that provider's quota.
证据 ↗证据与编辑审查Manifest、Bundle patch、分发与新鲜度
不可变证据
审查状态与源码活动
请只向你信任的视觉路由提交图片。启用前应检查插件配置,尤其是自定义端点设置和可选审计文件位置。
AI 审查于 2026/9/12 UTC 14:13。GitHub 事实核对日期: 2026/9/12 UTC 14:13。
自当前证据基线以来,没有记录到重要源码变化。