dsh-vision-router 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  </p>
16
16
 
17
17
  <p align="center">
18
- <a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.2.1"><img src="https://img.shields.io/badge/release-v1.2.1-5B4CF0?style=flat-square" alt="Release v1.2.1" /></a>
18
+ <a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.3.0"><img src="https://img.shields.io/badge/release-v1.3.0-5B4CF0?style=flat-square" alt="Release v1.3.0" /></a>
19
19
  <a href="tests"><img src="https://img.shields.io/badge/verified-149%20tests-2EA44F?style=flat-square" alt="Verified: 149 tests" /></a>
20
20
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
21
21
  <a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&amp;logo=nodedotjs&amp;logoColor=white" alt="Node.js >=22" /></a>
@@ -28,7 +28,9 @@
28
28
  <p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
29
29
 
30
30
  > [!WARNING]
31
- > 📌 **Announcement (v1.2.3)**
31
+ > 📌 **Announcement (v1.3.0)**
32
+ >
33
+ > **v1.3.0** makes long sessions and mixed-plugin setups safer: the vision-tool schema is stable by default to protect prefix/KV caches, diagnostics can be persisted and opened from Settings, custom vision-model filtering explains missing image metadata and supports re-detection, host-provided DSH capability packages use the correct peer contract, and Vision Router tool cards can coexist with other vision plugins while keeping Vision Router rendering precedence.
32
34
  >
33
35
  > v1.2.3 fixes DSH Desktop's re-appearing first-run dialog: the onboarding "seen" flag and the model-guide step now persist in the profile settings file instead of origin-scoped `localStorage`, which a random per-launch port (`--port 0`) wiped on every boot (issue #78).
34
36
  >
@@ -110,7 +112,7 @@ The auto-vision group follows the live DSH model catalog. Adding models or chang
110
112
 
111
113
  ### 3. Paste or upload the image
112
114
 
113
- After choosing the “+ Auto Vision” group, paste or upload an image normally. The agent auto-mounts the vision tools and can use `vision_describe`, `vision_ground`, `vision_crop`, and the rest across multiple steps when needed.
115
+ After choosing the “+ Auto Vision” group, paste or upload an image normally. By default the complete vision tool schema is stable from session start, so the agent can immediately use `vision_describe`, `vision_ground`, `vision_crop`, and the rest across multiple steps when needed.
114
116
 
115
117
  The built-in anonymous OVH vision fallback is already configured, so normal image use needs no signup or API key. **The lower-right chat picker selects only the brain/conversation model**; vision backends do not belong there. Advanced options live under **Settings → Plugins → Plugin config → 视觉路由(自动识图)**: each vision-backend row selects one image-capable user model already configured under **Settings → Models**. Leaving every user row empty is valid; the OVH chain remains the final fallback. `Vision HTTP` is an internal transport route, not a model group users should select.
116
118
 
@@ -129,7 +131,7 @@ The built-in anonymous OVH vision fallback is already configured, so normal imag
129
131
  - **Automatic failover with classified errors.** Region blocks, ToS filtering, 402 quota, 429 rate limits (with Retry-After backoff), context overflow, network failures — the chain walks providers one by one and only reports after all of them failed, with actionable advice.
130
132
  - **Image memory.** Vision answers are cached by attachment content hash; later text turns substitute the recorded description (marked as untrusted evidence), so DeepSeek genuinely remembers earlier images without re-spending vision calls.
131
133
  - **A verifiable pixel loop.** Reference → `vision_html_screenshot` → `vision_pixel_diff` (ratio + red heatmap + worst-region ranking) → fix → repeat until the mismatch converges. UI restoration becomes measurable instead of eyeballed.
132
- - **Progressive schema exposure.** Only a zero-arg `vision_activate` bootstrap is always visible; image turns auto-mount all eleven deep tools with a one-time usage note, and a `vision-tools` skill is registered for text-only turns.
134
+ - **Stable tool schema.** All eleven deep tools are registered from session start by default, avoiding a mid-conversation tool-list expansion that can invalidate long-context KV/prefix caches. `progressiveTools: true` remains an advanced boot-time opt-in; only then does `vision_activate` mount the tools on demand. See [`docs/progressive-tools-cache.md`](docs/progressive-tools-cache.md).
133
135
  - **Selective proxy.** Only the configured vision provider hosts go through your local proxy; DeepSeek stays direct.
134
136
 
135
137
  ### Pixel loop in practice
@@ -150,7 +152,7 @@ The vision model is **only the eyes**; DeepSeek is **always the brain**. An imag
150
152
 
151
153
  ## Tools
152
154
 
153
- All eleven deep tools mount automatically on image turns (`autoActivateOnImage`); text turns can mount them via `vision_activate` or the `/vision-tools` skill. Built on sharp / potrace / tesseract / system Chrome — no Python:
155
+ Default `progressiveTools: false`: all eleven deep tools stay registered from plugin startup, so text and image turns can call them immediately. If you explicitly set `progressiveTools: true` in the profile/composition `cordis.patch.yml`, progressive mode is restored: only `vision_activate` is exposed initially, the full tool set mounts on first use, and the `vision-tools` skill is registered. This is a boot-time switch; restart DSH after changing it. Built on sharp / potrace / tesseract / system Chrome — no Python:
154
156
 
155
157
  <p align="center">
156
158
  <img src="assets/vision-tools.svg" width="100%" alt="Eleven vision tools available in DSH Vision Router." />
@@ -266,7 +268,7 @@ Everything is optional; defaults work out of the box. Edit via the Web card or a
266
268
  | `wrapperRoute` / `chainRoute` | `deepseek-vision` / `vision-chain` | admission wrapper route name / fallback chain route name (empty disables) |
267
269
  | `stealth` | `false` | take over the official `deepseek-official` route (official row only; custom routes are auto-wrapped by default) |
268
270
  | `textProvider` | `deepseek-official` / `deepseek-v4-pro` | the model that reasons (your daily model) |
269
- | `tool` / `progressiveTools` / `autoActivateOnImage` | `true` ×3 | vision tools on / progressive mounting / auto-mount on image turns |
271
+ | `tool` / `progressiveTools` / `autoActivateOnImage` | `true` / `false` / `true` | vision tools on / progressive mounting (off by default for a stable tool schema) / image-turn auto-mount when progressive mode is enabled; `progressiveTools` is boot-time config |
270
272
  | `rewriteImages` | `true` | rewrite image blocks in the model input (cached description or tool-hint marker); the UI log keeps images |
271
273
  | `downscale` / `downscaleMaxPixels` | `true` / `4000000` | pre-call downscale and its pixel budget (latency guard) |
272
274
  | `cache` / `cacheTtlSeconds` / `cacheMaxEntries` | `true` / `3600` / `200` | vision answer cache |
package/README.zh.md CHANGED
@@ -15,7 +15,7 @@
15
15
  </p>
16
16
 
17
17
  <p align="center">
18
- <a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.2.1"><img src="https://img.shields.io/badge/release-v1.2.1-5B4CF0?style=flat-square" alt="Release v1.2.1" /></a>
18
+ <a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.3.0"><img src="https://img.shields.io/badge/release-v1.3.0-5B4CF0?style=flat-square" alt="Release v1.3.0" /></a>
19
19
  <a href="tests"><img src="https://img.shields.io/badge/verified-149%20tests-2EA44F?style=flat-square" alt="Verified: 149 tests" /></a>
20
20
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
21
21
  <a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&amp;logo=nodedotjs&amp;logoColor=white" alt="Node.js >=22" /></a>
@@ -28,7 +28,9 @@
28
28
  <p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
29
29
 
30
30
  > [!WARNING]
31
- > 📌 **公告(v1.2.3)**
31
+ > 📌 **公告(v1.3.0)**
32
+ >
33
+ > **v1.3.0** 重点加固长会话稳定性与插件共存:视觉工具 schema 默认从会话开始保持稳定,减少 prefix/KV cache 失效;诊断日志可持久化并从设置页直接打开;自定义视觉模型缺失 image 元数据时会明确解释并支持重新检测;宿主提供的 DSH capability 包使用正确的 peer 依赖契约;与其他视觉插件共装时,同名工具卡不再导致启动失败,同时保留 Vision Router 自己的渲染优先级。
32
34
  >
33
35
  > v1.2.3 现已支持:修复 DSH Desktop 每次启动都重复弹出首次引导的问题——引导「已读」标记与模型引导步骤改存 profile 设置文件(不再依赖按 origin 隔离、每次随机端口重启即清零的 `localStorage`)(issue #78)。
34
36
  >
@@ -110,7 +112,7 @@ opencode-go + 自动识图 ← 发图片时选这个
110
112
 
111
113
  ### 3. 直接粘贴或上传图片
112
114
 
113
- 选好「+ 自动识图」模型组后,直接往对话里贴图即可。Agent 会自动挂载视觉工具,通过 `vision_describe`、`vision_ground`、`vision_crop` 等工具看图,需要时连续多步操作。
115
+ 选好「+ 自动识图」模型组后,直接往对话里贴图即可。默认情况下完整视觉工具表从会话开始就保持稳定,Agent 可直接调用 `vision_describe`、`vision_ground`、`vision_crop` 等工具看图,需要时连续多步操作。
114
116
 
115
117
  默认已经有内置 OVH 匿名视觉兜底,无需注册、无需 Key。**聊天页右下角只选择“脑子/会话模型”**;视觉模型不要在那里选。高级配置在 **设置 → 插件 → 插件配置 → 视觉路由(自动识图)**:视觉后端链每一行只选择一个你在 **设置 → 模型** 中已经配置且支持图片输入的用户模型;一行都不填也可以,OVH 免费链会固定在最后兜底。插件内部的 `Vision HTTP` 只是传输实现,不是用户需要选择的模型组。
116
118
 
@@ -129,7 +131,7 @@ opencode-go + 自动识图 ← 发图片时选这个
129
131
  - **自动降级 + 分类报错。** 地区限制、ToS 风控、402 额度、429 限流(尊重 Retry-After 退避重试)、上下文超长、网络故障——链路逐供应商尝试,全部失败才报错并给出可操作的建议。
130
132
  - **图片记忆。** 视觉答案按附件内容哈希缓存;后续文字轮用记录的描述替换历史图片(标注为不可信证据),DeepSeek 真正“记得”之前发过的图,且不重复消耗视觉调用。
131
133
  - **可验证的像素闭环。** 参照图 → `vision_html_screenshot` → `vision_pixel_diff`(差异率 + 红色热力图 + 最差区域排行)→ 修复 → 再对比,直到差异收敛。UI 还原从“目测”变成“实测”。
132
- - **渐进式 schema 暴露。** 平时只有一个零参引导工具 `vision_activate`;图片轮自动挂载全部 11 个深看工具(附一次性使用提示),并为纯文字轮注册 `vision-tools` 技能。
134
+ - **稳定工具 schema。** 默认从会话开始就注册完整 11 个深看工具,避免图片轮中途扩展工具列表导致长上下文的 KV / prefix cache 失效。仍保留 `progressiveTools: true` 作为高级启动期 opt-in;开启后才使用 `vision_activate` 按需挂载。详见 [`docs/progressive-tools-cache.md`](docs/progressive-tools-cache.md)。
133
135
  - **选择性代理。** 只有配置的视觉供应商域名走本地代理;DeepSeek 保持直连。
134
136
 
135
137
  ### 像素闭环实测
@@ -150,7 +152,7 @@ Agent 仅根据参考图复刻 UI,再用 `vision_pixel_diff` 验证最终结
150
152
 
151
153
  ## 工具
152
154
 
153
- 11 个深看工具在图片轮自动挂载(`autoActivateOnImage`);文字轮可通过 `vision_activate` 或 `/vision-tools` 技能挂载。全部基于 sharp / potrace / tesseract / 系统 Chrome——无 Python:
155
+ 默认 `progressiveTools: false`:11 个深看工具从插件启动时就保持常驻,文本轮和图片轮都可直接调用。若你在 profile / composition 的 `cordis.patch.yml` 中显式开启 `progressiveTools: true`,才会恢复渐进模式:初始只暴露 `vision_activate`,首次需要时再挂载完整工具,并注册 `vision-tools` 技能。该开关是启动期配置,修改后需重启 DSH。全部工具基于 sharp / potrace / tesseract / 系统 Chrome——无 Python:
154
156
 
155
157
  <p align="center">
156
158
  <img src="assets/vision-tools-zh.svg" width="100%" alt="DSH Vision Router 的 11 个视觉工具。" />
@@ -266,7 +268,7 @@ Web 配置页在 **设置 → 插件 → 插件配置** 下注册「视觉路由
266
268
  | `wrapperRoute` / `chainRoute` | `deepseek-vision` / `vision-chain` | 准入包装路由名 / 降级链路由名(置空关闭) |
267
269
  | `stealth` | `false` | 接管官方 `deepseek-official` 路由(仅官方行;自定义路由默认由自动包装处理) |
268
270
  | `textProvider` | `deepseek-official` / `deepseek-v4-pro` | 负责思考的模型(你的日常模型) |
269
- | `tool` / `progressiveTools` / `autoActivateOnImage` | `true` ×3 | 视觉工具开关 / 渐进式挂载 / 图片轮自动挂载 |
271
+ | `tool` / `progressiveTools` / `autoActivateOnImage` | `true` / `false` / `true` | 视觉工具总开关 / 渐进式挂载(默认关闭以稳定工具 schema)/ 渐进模式下图片轮自动挂载;`progressiveTools` 为启动期配置 |
270
272
  | `rewriteImages` | `true` | 模型输入层改写图片块(缓存描述或工具提示标记);界面日志保留图片 |
271
273
  | `downscale` / `downscaleMaxPixels` | `true` / `4000000` | 调用前压缩及其像素预算(延迟保护) |
272
274
  | `cache` / `cacheTtlSeconds` / `cacheMaxEntries` | `true` / `3600` / `200` | 视觉答案缓存 |
package/cordis.patch.yml CHANGED
@@ -8,11 +8,15 @@
8
8
  # 挂载后是否接管官方 deepseek-official 路由由设置里的「隐身模式」开关决定
9
9
  # (默认关;开启后需自行在 profile 补丁层禁用 llm-deepseek 行)。
10
10
 
11
- # 挂载插件行。配置全部可选(默认即内置免费视觉端点 + 工具优先的图片流程),
12
- # 可在 Web 设置 > 插件 > 插件配置 的「视觉路由」卡片里实时修改。
11
+ # 挂载插件行。默认保持完整视觉工具表常驻(issue #81):虽然渐进挂载可以
12
+ # 少发一小段工具 schema,但图片轮首次扩展工具列表会改变请求前缀,可能让
13
+ # 长会话的 provider KV/prefix cache 整体失效。高级用户仍可在 profile /
14
+ # composition 补丁中显式把 progressiveTools 改回 true(启动期配置)。
13
15
  - insert:
14
16
  - id: vision-router
15
17
  name: dsh-vision-router
18
+ config:
19
+ progressiveTools: false
16
20
 
17
21
  # 放宽附件图片限制(部署默认 5MB / 4000 万像素 → 20MB / 1 亿像素),
18
22
  # 大尺寸设计稿/扫描图可过审。字段可选,不需要可在 profile 补丁层覆写。
@@ -0,0 +1,57 @@
1
+ # Progressive tools and prefix cache
2
+
3
+ 中文 | English
4
+
5
+ ## 中文
6
+
7
+ Vision Router 默认让完整视觉工具表从会话开始就保持稳定。正常通过 DSH bundle 安装时,组合层会提供:
8
+
9
+ ```yaml
10
+ - id: vision-router
11
+ name: dsh-vision-router
12
+ config:
13
+ progressiveTools: false
14
+ ```
15
+
16
+ 插件入口本身也把 `progressiveTools` 的 schema 默认值设为 `false`,因此即使后续 profile patch 整块覆盖了 bundle 的 `config`、但没有重述这个字段,也不会意外回到渐进模式。
17
+
18
+ 原因是很多模型服务的 KV / prefix cache 会把工具 schema 视为请求前缀的一部分。若长会话一开始只暴露 `vision_activate`,第一次图片轮再挂载完整视觉工具,工具列表发生变化后可能导致此前的大段会话前缀无法命中缓存。
19
+
20
+ 渐进式暴露仍然保留为高级 opt-in。如果你更在意平时请求中少携带工具 schema,而不依赖长会话前缀缓存,请在 **profile / composition 的 `cordis.patch.yml`** 中显式给 `vision-router` 设置:
21
+
22
+ ```yaml
23
+ - id: vision-router
24
+ config:
25
+ progressiveTools: true
26
+ ```
27
+
28
+ `progressiveTools` 决定插件启动时注册哪组工具,因此它是**启动期配置**;不要依赖运行中的 Web 设置热切换这个字段。修改 profile patch 后重启 DSH。
29
+
30
+ 开启后,平时只暴露 `vision_activate`,首次需要时再挂载完整视觉工具;默认关闭时,完整视觉工具从一开始就常驻。视觉工具本身的能力不变。
31
+
32
+ ## English
33
+
34
+ Vision Router keeps the complete vision tool schema stable from the beginning of a session by default. Normal DSH bundle installs provide this composition layer:
35
+
36
+ ```yaml
37
+ - id: vision-router
38
+ name: dsh-vision-router
39
+ config:
40
+ progressiveTools: false
41
+ ```
42
+
43
+ The public plugin entrypoint also makes `false` the schema default. Therefore a later profile patch that replaces the bundle `config` without restating this field cannot accidentally fall back to progressive mode.
44
+
45
+ Many model providers include tool schemas in the KV/prefix-cacheable request prefix. If a long conversation starts with only `vision_activate` and the first image turn later mounts the complete vision tool set, that prefix change can invalidate a large cached history.
46
+
47
+ Progressive exposure remains available as an advanced opt-in. If minimizing the always-present tool schema matters more than long-context prefix-cache stability, explicitly set it in the **profile/composition `cordis.patch.yml`**:
48
+
49
+ ```yaml
50
+ - id: vision-router
51
+ config:
52
+ progressiveTools: true
53
+ ```
54
+
55
+ `progressiveTools` determines which tools are registered at plugin startup, so it is a **boot-time setting**; do not rely on changing this field through live Web settings. Restart DSH after changing the profile patch.
56
+
57
+ When enabled, only `vision_activate` is exposed initially and the complete vision tool set mounts on first use. With the default disabled mode, the complete vision tool set is present from the start. The vision capabilities themselves are unchanged.
package/entry.js ADDED
@@ -0,0 +1,51 @@
1
+ // Public plugin entrypoint.
2
+ //
3
+ // Keep the large implementation in index.js, but normalize the progressive
4
+ // tools switch here before Cordis reads the exported Config. Issue #81 showed
5
+ // that changing the tool schema mid-session can invalidate provider prefix/KV
6
+ // caches for very long conversations, so progressive exposure is now an
7
+ // explicit opt-in rather than the implicit fallback.
8
+
9
+ import z from '@deepseek-ai/schemastery'
10
+ import * as core from './index.js'
11
+ import { installVisionRouterFileLogging } from './lib/file-logger.js'
12
+
13
+ // Schemastery object schemas expose set() as the supported way to replace a
14
+ // field schema. This mutates the Config object that index.js itself later uses
15
+ // for the settings namespace, so composition config and settings validation
16
+ // agree on the same default.
17
+ core.Config.set('progressiveTools', z.boolean().default(false))
18
+
19
+ export * from './index.js'
20
+ export const Config = core.Config
21
+
22
+ // Defense in depth for direct/programmatic callers that invoke apply() without
23
+ // first running the Cordis Config resolver: only an explicit true enables the
24
+ // schema-changing progressive mode. The wrapped context changes only logger:
25
+ // every existing vision-router diagnostic still reaches the host logger and is
26
+ // also persisted to ~/.dsh/logs/vision-router/vision-router.log.
27
+ export function apply(ctx, config = {}) {
28
+ const logging = installVisionRouterFileLogging(ctx)
29
+ try {
30
+ const result = core.apply(logging.ctx, {
31
+ ...config,
32
+ progressiveTools: config.progressiveTools === true,
33
+ })
34
+ if (result && typeof result.then === 'function') {
35
+ return result.catch((error) => {
36
+ logging.logger.error(
37
+ 'vision-router: plugin apply failed: %s',
38
+ error && error.stack ? error.stack : error && error.message ? error.message : String(error),
39
+ )
40
+ throw error
41
+ })
42
+ }
43
+ return result
44
+ } catch (error) {
45
+ logging.logger.error(
46
+ 'vision-router: plugin apply failed: %s',
47
+ error && error.stack ? error.stack : error && error.message ? error.message : String(error),
48
+ )
49
+ throw error
50
+ }
51
+ }
package/lib/client.js CHANGED
@@ -77,6 +77,14 @@ window.__ModuleLoader__.load({
77
77
  visionCapsLoading: '正在验证哪些模型真正支持图片输入…',
78
78
  visionCapsError: '视觉能力元数据暂时不可用;为防止误选,暂不提供用户视觉模型下拉。内置 OVH 免费兜底仍可用。',
79
79
  visionCapsFiltered: '视觉后端下拉只显示明确声明 image 输入的模型。',
80
+ visionCapsEmptyTitle: '没有可选的用户视觉模型',
81
+ visionCapsEmptyBody: '检测到 {count} 个用户模型,但它们都没有被 DSH 明确标记为支持图片,因此已被安全隐藏。',
82
+ visionCapsHiddenPrefix: '被隐藏的模型:',
83
+ visionCapsReasonMissingImage: '未声明 image',
84
+ visionCapsReasonUnverified: '无法验证图片能力',
85
+ visionCapsHiddenMore: '另有 {count} 个模型未显示',
86
+ visionCapsMissingImageHint: '如果这里有你刚在「设置 → 模型 → 添加自定义提供方」中添加的视觉模型,请在 $DSH_HOME/settings.yaml 为该模型补上 input: [text, image],或为整个提供方补上 defaultInput: [text, image]。DSH 当前 Web 表单不会写入这个字段。',
87
+ visionCapsRetry: '重新检测模型',
80
88
  chainInvalidCurrent: '当前保存的视觉后端不支持图片或无法验证,已从下拉列表隐藏,运行时也会跳过:',
81
89
  retryCatalog: '重试加载目录',
82
90
  advanced: '高级设置',
@@ -98,6 +106,8 @@ window.__ModuleLoader__.load({
98
106
  testConnecting: '测试中…',
99
107
  testOk: '连接正常',
100
108
  testFailed: '连接失败',
109
+ openLogFolder: '打开日志文件夹',
110
+ openLogFolderFailed: '无法打开日志文件夹',
101
111
  updateTitle: '版本更新',
102
112
  checkUpdate: '检查更新',
103
113
  updateChecking: '检查中…',
@@ -248,6 +258,14 @@ window.__ModuleLoader__.load({
248
258
  visionCapsLoading: 'Checking which models genuinely accept image input…',
249
259
  visionCapsError: 'Vision capability metadata is unavailable; user vision-model choices are hidden to prevent bad selections. The built-in OVH fallback still works.',
250
260
  visionCapsFiltered: 'The vision-backend dropdown only shows models that explicitly declare image input.',
261
+ visionCapsEmptyTitle: 'No selectable user vision models',
262
+ visionCapsEmptyBody: 'DSH reported {count} user models, but none are explicitly marked as accepting images, so Vision Router hid them safely.',
263
+ visionCapsHiddenPrefix: 'Hidden models:',
264
+ visionCapsReasonMissingImage: 'image input not declared',
265
+ visionCapsReasonUnverified: 'image capability could not be verified',
266
+ visionCapsHiddenMore: '{count} more models not shown',
267
+ visionCapsMissingImageHint: 'If one of these is a vision model you just added through Settings → Models → Add custom provider, add input: [text, image] to that model in $DSH_HOME/settings.yaml, or defaultInput: [text, image] to the provider. The current DSH Web form does not write this field.',
268
+ visionCapsRetry: 'Re-detect models',
251
269
  chainInvalidCurrent: 'This saved vision backend does not support images or could not be verified. It is hidden from the dropdown and skipped at runtime: ',
252
270
  retryCatalog: 'Retry catalog',
253
271
  advanced: 'Advanced settings',
@@ -269,6 +287,8 @@ window.__ModuleLoader__.load({
269
287
  testConnecting: 'Testing…',
270
288
  testOk: 'Connected',
271
289
  testFailed: 'Connection failed',
290
+ openLogFolder: 'Open logs folder',
291
+ openLogFolderFailed: 'Could not open logs folder',
272
292
  updateTitle: 'Updates',
273
293
  checkUpdate: 'Check for updates',
274
294
  updateChecking: 'Checking…',
@@ -385,9 +405,6 @@ window.__ModuleLoader__.load({
385
405
  .map((group) => {
386
406
  const models = (group && Array.isArray(group.models) ? group.models : []).filter((model) => {
387
407
  if (!model || typeof model.id !== 'string') return false
388
- // The built-in backend is defined by this plugin and always
389
- // declares image input. Keeping it visible while the capability
390
- // request is still loading avoids a blank default editor.
391
408
  if (group.id === 'vision-http') return false
392
409
  return !!(caps[group.id] && caps[group.id][model.id] && caps[group.id][model.id].image === true)
393
410
  })
@@ -396,6 +413,33 @@ window.__ModuleLoader__.load({
396
413
  .filter((group) => group && group.models.length > 0)
397
414
  }
398
415
 
416
+ function collectFilteredVisionBackends(groups, capabilities) {
417
+ const caps = capabilities && typeof capabilities === 'object' ? capabilities : {}
418
+ const hidden = []
419
+ for (const group of Array.isArray(groups) ? groups : []) {
420
+ if (
421
+ !group ||
422
+ typeof group.id !== 'string' ||
423
+ group.id === 'vision-http' ||
424
+ group.id === 'vision-chain' ||
425
+ group.id.endsWith('-vision')
426
+ ) continue
427
+ for (const model of Array.isArray(group.models) ? group.models : []) {
428
+ if (!model || typeof model.id !== 'string' || model.id === '') continue
429
+ const capability = caps[group.id] && caps[group.id][model.id]
430
+ if (capability && capability.image === true) continue
431
+ hidden.push({
432
+ provider: group.id,
433
+ model: model.id,
434
+ reason: capability && typeof capability.reason === 'string' ? capability.reason : undefined,
435
+ missingImageDeclaration:
436
+ !!capability && capability.reason === 'model metadata does not declare image input',
437
+ })
438
+ }
439
+ }
440
+ return hidden
441
+ }
442
+
399
443
  // ── field specs ──────────────────────────────────────────────────────────
400
444
  const TOGGLE_KEYS = ['routing', 'tool', 'autoWrapProviders', 'stealth']
401
445
  const ADVANCED_TOGGLE_KEYS = ['reverseRouting', 'rewriteImages', 'downscale', 'cache', 'freeFallback']
@@ -495,6 +539,10 @@ window.__ModuleLoader__.load({
495
539
  '.vr-chain-row{display:flex;align-items:center;gap:8px;margin:6px 0}' +
496
540
  '.vr-stealth-notice{color:var(--dsw-alias-label-warning,var(--dsw-alias-label-secondary))}' +
497
541
  '.vr-catalog-error{display:flex;align-items:center;gap:10px;flex-wrap:wrap}' +
542
+ '.vr-vision-empty{margin:8px 0 2px;padding:11px 12px;border:1px solid var(--dsw-alias-label-warning,var(--dsw-alias-border-l2));border-radius:9px;background:var(--dsw-alias-bg-module-platform);display:flex;flex-direction:column;gap:7px}' +
543
+ '.vr-vision-empty-title{font-size:12px;font-weight:650;color:var(--dsw-alias-label-primary);margin:0}' +
544
+ '.vr-vision-empty-list{margin:0;padding-left:18px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;word-break:break-word}' +
545
+ '.vr-vision-empty-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}' +
498
546
  '.vr-subheader{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:8px 0;border:none;background:none;cursor:pointer;font:inherit;color:var(--dsw-alias-label-primary);text-align:left}' +
499
547
  '.vr-group{content-visibility:auto;contain-intrinsic-size:auto 96px;border-top:1px solid var(--dsw-alias-border-l2);padding:10px 0 2px;display:flex;flex-direction:column;gap:8px}' +
500
548
  '.vr-group-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary);margin:0}' +
@@ -985,6 +1033,8 @@ window.__ModuleLoader__.load({
985
1033
  const [visionCaps, setVisionCaps] = useState({ status: 'idle', capabilities: {}, builtinFallback: [], anonymousRpmPerModel: 2, error: undefined })
986
1034
  const catalogReady = catalog.status === 'ready' && catalog.groups.length > 0
987
1035
  const visionGroups = filterVisionBackendGroups(catalog.groups, visionCaps.capabilities)
1036
+ const hiddenVisionBackends =
1037
+ visionCaps.status === 'ready' ? collectFilteredVisionBackends(catalog.groups, visionCaps.capabilities) : []
988
1038
  const visionModelsFor = (providerId) => {
989
1039
  const group = visionGroups.find((entry) => entry.id === providerId)
990
1040
  return group && Array.isArray(group.models) ? group.models : []
@@ -1013,8 +1063,8 @@ window.__ModuleLoader__.load({
1013
1063
  }, 220)
1014
1064
  return () => window.clearTimeout(timer)
1015
1065
  }, [guideActive, open, catalog.status, visionCaps.status])
1016
- const loadCatalog = () => {
1017
- if (catalog.status === 'loading' || catalog.status === 'ready') return
1066
+ const loadCatalog = (force = false) => {
1067
+ if (!force && (catalog.status === 'loading' || catalog.status === 'ready')) return
1018
1068
  setCatalog({ status: 'loading', groups: [], error: undefined })
1019
1069
  try {
1020
1070
  const connection = props.getConnection ? props.getConnection() : undefined
@@ -1061,8 +1111,8 @@ window.__ModuleLoader__.load({
1061
1111
  })
1062
1112
  }
1063
1113
  }
1064
- const loadVisionCapabilities = () => {
1065
- if (visionCaps.status === 'loading' || visionCaps.status === 'ready') return
1114
+ const loadVisionCapabilities = (force = false) => {
1115
+ if (!force && (visionCaps.status === 'loading' || visionCaps.status === 'ready')) return
1066
1116
  setVisionCaps({ status: 'loading', capabilities: {}, builtinFallback: [], anonymousRpmPerModel: 2, error: undefined })
1067
1117
  fetch('/_dsh/vision-router/model-capabilities')
1068
1118
  .then(async (response) => {
@@ -1091,6 +1141,12 @@ window.__ModuleLoader__.load({
1091
1141
  }),
1092
1142
  )
1093
1143
  }
1144
+ const retryVisionModels = () => {
1145
+ setCatalog({ status: 'idle', groups: [], error: undefined })
1146
+ setVisionCaps({ status: 'idle', capabilities: {}, builtinFallback: [], anonymousRpmPerModel: 2, error: undefined })
1147
+ loadCatalog(true)
1148
+ loadVisionCapabilities(true)
1149
+ }
1094
1150
  let snapshot
1095
1151
  let renderError
1096
1152
  try {
@@ -1531,6 +1587,37 @@ window.__ModuleLoader__.load({
1531
1587
  h('p', { className: 'vr-hint' }, t('chainHint')),
1532
1588
  )
1533
1589
  }
1590
+ const emptyVisionModelsPanel = () => {
1591
+ if (!(catalogReady && visionCaps.status === 'ready' && visionGroups.length === 0 && hiddenVisionBackends.length > 0)) {
1592
+ return null
1593
+ }
1594
+ const preview = hiddenVisionBackends.slice(0, 8)
1595
+ const remaining = hiddenVisionBackends.length - preview.length
1596
+ const hasMissingDeclaration = hiddenVisionBackends.some((entry) => entry.missingImageDeclaration)
1597
+ return h('div', { className: 'vr-vision-empty' },
1598
+ h('p', { className: 'vr-vision-empty-title' }, t('visionCapsEmptyTitle')),
1599
+ h('p', { className: 'vr-hint' }, t('visionCapsEmptyBody', { count: hiddenVisionBackends.length })),
1600
+ h('p', { className: 'vr-hint' }, t('visionCapsHiddenPrefix')),
1601
+ h('ul', { className: 'vr-vision-empty-list' },
1602
+ preview.map((entry) =>
1603
+ h('li', { key: entry.provider + '/' + entry.model },
1604
+ entry.provider + '/' + entry.model + ' — ' +
1605
+ t(entry.missingImageDeclaration ? 'visionCapsReasonMissingImage' : 'visionCapsReasonUnverified')),
1606
+ ),
1607
+ remaining > 0 ? h('li', { key: 'more' }, t('visionCapsHiddenMore', { count: remaining })) : null,
1608
+ ),
1609
+ hasMissingDeclaration
1610
+ ? h('p', { className: 'vr-hint vr-stealth-notice' }, t('visionCapsMissingImageHint'))
1611
+ : null,
1612
+ h('div', { className: 'vr-vision-empty-actions' },
1613
+ h('button', {
1614
+ type: 'button', className: 'vr-btn',
1615
+ disabled: catalog.status === 'loading' || visionCaps.status === 'loading',
1616
+ onClick: retryVisionModels,
1617
+ }, t('visionCapsRetry')),
1618
+ ),
1619
+ )
1620
+ }
1534
1621
  const builtinFallbackPanel = () => {
1535
1622
  const list = Array.isArray(visionCaps.builtinFallback) ? visionCaps.builtinFallback : []
1536
1623
  const enabled = format('freeFallback') !== false
@@ -1826,6 +1913,7 @@ window.__ModuleLoader__.load({
1826
1913
  : visionCaps.status === 'ready'
1827
1914
  ? h('p', { className: 'vr-hint' }, t('visionCapsFiltered'))
1828
1915
  : null,
1916
+ emptyVisionModelsPanel(),
1829
1917
  catalogReady
1830
1918
  ? chainEditor()
1831
1919
  : h('div', {
@@ -1899,6 +1987,28 @@ window.__ModuleLoader__.load({
1899
1987
  ? `${t('testOk')}(${typeof testState.result.latencyMs === 'number' ? testState.result.latencyMs + 'ms' : 'ok'})`
1900
1988
  : `${t('testFailed')}:${testState.result && testState.result.error ? testState.result.error : 'unknown'}`)
1901
1989
  : null,
1990
+ h('button', {
1991
+ type: 'button', className: 'vr-btn',
1992
+ onClick: async () => {
1993
+ try {
1994
+ const response = await fetch('/_dsh/vision-router/logs', {
1995
+ method: 'POST',
1996
+ cache: 'no-store',
1997
+ })
1998
+ const result = await response.json().catch(() => undefined)
1999
+ if (!response.ok || !result || result.ok !== true) {
2000
+ throw new Error(result && result.error ? result.error : `HTTP ${response.status}`)
2001
+ }
2002
+ } catch (error) {
2003
+ if (typeof window.alert === 'function') {
2004
+ window.alert(
2005
+ t('openLogFolderFailed') + ':' +
2006
+ (error && error.message ? error.message : String(error)),
2007
+ )
2008
+ }
2009
+ }
2010
+ },
2011
+ }, t('openLogFolder')),
1902
2012
  h('button', {
1903
2013
  type: 'button', className: 'vr-btn', disabled: testState.status === 'running',
1904
2014
  onClick: runTestConnection,
@@ -2141,12 +2251,12 @@ window.__ModuleLoader__.load({
2141
2251
  () =>
2142
2252
  ctx.slots.inject('tool.call.toolview', function* () {
2143
2253
  yield ctx.slots.register(
2144
- { name: 'tool.call.toolview', key: 'vision_present', inject: () => ({}) },
2254
+ { name: 'tool.call.toolview', key: 'vision_present', priority: -10, inject: () => ({}) },
2145
2255
  VisionPresentCard,
2146
2256
  )
2147
2257
  for (const key of ARTIFACT_TOOL_KEYS) {
2148
2258
  yield ctx.slots.register(
2149
- { name: 'tool.call.toolview', key, inject: () => ({}) },
2259
+ { name: 'tool.call.toolview', key, priority: -10, inject: () => ({}) },
2150
2260
  ArtifactCard,
2151
2261
  )
2152
2262
  }
@@ -2159,6 +2269,7 @@ window.__ModuleLoader__.load({
2159
2269
  exports.inject = ['settingsScope', 'slots', 'locale', 'sessions']
2160
2270
  exports.unwrapModelsResult = unwrapModelsResult
2161
2271
  exports.filterVisionBackendGroups = filterVisionBackendGroups
2272
+ exports.collectFilteredVisionBackends = collectFilteredVisionBackends
2162
2273
  return module.exports
2163
2274
  },
2164
2275
  })
@@ -0,0 +1,273 @@
1
+ import { appendFile, mkdir, rename, rm, stat } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { execFile } from 'node:child_process'
5
+ import { format as formatArgs, promisify } from 'node:util'
6
+ import { createRequire } from 'node:module'
7
+ import { resolveDshHome } from './doctor.js'
8
+
9
+ export const LOG_FILE_NAME = 'vision-router.log'
10
+ export const LOG_BACKUP_FILE_NAME = 'vision-router.1.log'
11
+ export const DEFAULT_LOG_MAX_BYTES = 2 * 1024 * 1024
12
+
13
+ const execFileAsync = promisify(execFile)
14
+ const installs = new WeakMap()
15
+
16
+ export function resolveVisionRouterLogPaths(dshHome = resolveDshHome()) {
17
+ const directory = path.join(dshHome, 'logs', 'vision-router')
18
+ return {
19
+ directory,
20
+ file: path.join(directory, LOG_FILE_NAME),
21
+ backup: path.join(directory, LOG_BACKUP_FILE_NAME),
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Diagnostics are intended to be shareable in bug reports. Existing runtime
27
+ * messages should not contain secrets, but redact common credential shapes as
28
+ * defense in depth before anything reaches disk.
29
+ */
30
+ export function sanitizeLogText(value) {
31
+ let text = String(value ?? '')
32
+ text = text.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, 'Bearer [REDACTED]')
33
+ text = text.replace(/\bsk-(?:proj-)?[A-Za-z0-9_-]{8,}\b/g, '[REDACTED_KEY]')
34
+ text = text.replace(
35
+ /([?&](?:api[_-]?key|access[_-]?token|token|key|auth)=)[^&\s]+/gi,
36
+ '$1[REDACTED]',
37
+ )
38
+ text = text.replace(
39
+ /\b(authorization|api[_-]?key|access[_-]?token)\s*[:=]\s*["']?[^\s"',}]+/gi,
40
+ '$1=[REDACTED]',
41
+ )
42
+ return text
43
+ }
44
+
45
+ export function createFileLogSink({
46
+ file,
47
+ backup,
48
+ maxBytes = DEFAULT_LOG_MAX_BYTES,
49
+ onError = () => {},
50
+ } = {}) {
51
+ let queue = Promise.resolve()
52
+ let initialized = false
53
+ let size = 0
54
+ let disabled = false
55
+ let reportedError = false
56
+
57
+ const reportError = (error) => {
58
+ if (reportedError) return
59
+ reportedError = true
60
+ try {
61
+ onError(error)
62
+ } catch {
63
+ // A diagnostics failure must never affect the plugin runtime.
64
+ }
65
+ }
66
+
67
+ const prepare = async () => {
68
+ if (initialized || disabled) return
69
+ await mkdir(path.dirname(file), { recursive: true })
70
+ try {
71
+ size = (await stat(file)).size
72
+ } catch (error) {
73
+ if (error && error.code !== 'ENOENT') throw error
74
+ size = 0
75
+ }
76
+ initialized = true
77
+ }
78
+
79
+ const rotate = async () => {
80
+ try {
81
+ await rm(backup, { force: true })
82
+ await rename(file, backup)
83
+ } catch (error) {
84
+ if (error && error.code !== 'ENOENT') throw error
85
+ }
86
+ size = 0
87
+ }
88
+
89
+ const write = (level, args) => {
90
+ if (disabled) return queue
91
+ const rendered = sanitizeLogText(formatArgs(...args))
92
+ const line = `[${new Date().toISOString()}] [${String(level).toUpperCase()}] ${rendered}\n`
93
+ const bytes = Buffer.byteLength(line)
94
+ queue = queue.then(async () => {
95
+ try {
96
+ await prepare()
97
+ if (size > 0 && size + bytes > maxBytes) await rotate()
98
+ await appendFile(file, line, { encoding: 'utf8', mode: 0o600 })
99
+ size += bytes
100
+ } catch (error) {
101
+ disabled = true
102
+ reportError(error)
103
+ }
104
+ })
105
+ return queue
106
+ }
107
+
108
+ return {
109
+ write,
110
+ flush: () => queue,
111
+ get disabled() {
112
+ return disabled
113
+ },
114
+ }
115
+ }
116
+
117
+ function packageVersion() {
118
+ try {
119
+ const require = createRequire(import.meta.url)
120
+ const manifest = require('../package.json')
121
+ return typeof manifest?.version === 'string' ? manifest.version : 'unknown'
122
+ } catch {
123
+ return 'unknown'
124
+ }
125
+ }
126
+
127
+ function teeLogger(baseLogger, sink) {
128
+ const levels = new Set(['debug', 'info', 'warn', 'error'])
129
+ return new Proxy(baseLogger ?? {}, {
130
+ get(target, property) {
131
+ if (levels.has(property)) {
132
+ return (...args) => {
133
+ const method = target && typeof target[property] === 'function' ? target[property] : undefined
134
+ if (method) {
135
+ try {
136
+ method.apply(target, args)
137
+ } catch {
138
+ // Keep the file logger from changing host logger semantics.
139
+ }
140
+ }
141
+ void sink.write(property, args)
142
+ }
143
+ }
144
+ const value = Reflect.get(target, property, target)
145
+ return typeof value === 'function' ? value.bind(target) : value
146
+ },
147
+ })
148
+ }
149
+
150
+ function contextWithLogger(ctx, logger) {
151
+ return new Proxy(ctx, {
152
+ get(target, property) {
153
+ if (property === 'logger') return logger
154
+ const value = Reflect.get(target, property, target)
155
+ return typeof value === 'function' ? value.bind(target) : value
156
+ },
157
+ })
158
+ }
159
+
160
+ export async function openLogDirectory(directory, {
161
+ platform = process.platform,
162
+ exec = execFileAsync,
163
+ } = {}) {
164
+ await mkdir(directory, { recursive: true })
165
+ if (platform === 'darwin') {
166
+ await exec('open', [directory], { timeout: 10_000, windowsHide: true })
167
+ return
168
+ }
169
+ if (platform === 'win32') {
170
+ await exec('explorer.exe', [directory], { timeout: 10_000, windowsHide: true })
171
+ return
172
+ }
173
+ await exec('xdg-open', [directory], { timeout: 10_000, windowsHide: true })
174
+ }
175
+
176
+ function sameOrigin(req) {
177
+ const origin = req?.headers?.origin
178
+ const host = req?.headers?.host
179
+ const fetchSite = req?.headers?.['sec-fetch-site']
180
+ if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') return false
181
+ if (!origin) return true
182
+ if (!host) return false
183
+ try {
184
+ return new URL(origin).host === host
185
+ } catch {
186
+ return false
187
+ }
188
+ }
189
+
190
+ function sendJson(res, status, body) {
191
+ res.writeHead(status, {
192
+ 'content-type': 'application/json; charset=utf-8',
193
+ 'cache-control': 'no-store',
194
+ })
195
+ res.end(JSON.stringify(body))
196
+ }
197
+
198
+ function installLogRoute(ctx, paths) {
199
+ ctx.inject(['webServer'], (webCtx) => {
200
+ webCtx.effect(() => webCtx.webServer.register({
201
+ kind: 'exact',
202
+ path: '/_dsh/vision-router/logs',
203
+ handler: async (req, res) => {
204
+ if (req.method === 'GET') {
205
+ sendJson(res, 200, { ok: true, directory: paths.directory, file: paths.file })
206
+ return
207
+ }
208
+ if (req.method !== 'POST') {
209
+ res.setHeader('Allow', 'GET, POST')
210
+ sendJson(res, 405, { ok: false, error: 'method not allowed' })
211
+ return
212
+ }
213
+ if (!sameOrigin(req)) {
214
+ sendJson(res, 403, { ok: false, error: 'cross-origin request rejected' })
215
+ return
216
+ }
217
+ try {
218
+ await openLogDirectory(paths.directory)
219
+ sendJson(res, 200, { ok: true, directory: paths.directory, file: paths.file })
220
+ } catch (error) {
221
+ sendJson(res, 500, {
222
+ ok: false,
223
+ directory: paths.directory,
224
+ file: paths.file,
225
+ error: error && error.message ? error.message : String(error),
226
+ })
227
+ }
228
+ },
229
+ }), 'vision-router: diagnostics log route')
230
+ })
231
+ }
232
+
233
+ /**
234
+ * Return a Cordis context whose logger tees this plugin's existing diagnostics
235
+ * to a small rotating file while preserving the host logger. The original
236
+ * context is otherwise untouched.
237
+ */
238
+ export function installVisionRouterFileLogging(ctx, options = {}) {
239
+ if (ctx && typeof ctx === 'object' && installs.has(ctx)) return installs.get(ctx)
240
+
241
+ const paths = resolveVisionRouterLogPaths(options.dshHome)
242
+ const baseLogger = ctx?.logger
243
+ const sink = createFileLogSink({
244
+ file: paths.file,
245
+ backup: paths.backup,
246
+ maxBytes: options.maxBytes ?? DEFAULT_LOG_MAX_BYTES,
247
+ onError: (error) => {
248
+ try {
249
+ baseLogger?.warn?.(
250
+ 'vision-router: diagnostics file logging disabled: %s',
251
+ error && error.message ? error.message : String(error),
252
+ )
253
+ } catch {
254
+ // Logging failure remains non-fatal.
255
+ }
256
+ },
257
+ })
258
+ const logger = teeLogger(baseLogger, sink)
259
+ const wrappedCtx = contextWithLogger(ctx, logger)
260
+ const installed = { ctx: wrappedCtx, logger, sink, ...paths }
261
+ if (ctx && typeof ctx === 'object') installs.set(ctx, installed)
262
+
263
+ installLogRoute(ctx, paths)
264
+ logger.info(
265
+ 'vision-router: diagnostics log enabled at %s (plugin=%s node=%s platform=%s/%s)',
266
+ paths.file,
267
+ packageVersion(),
268
+ process.version,
269
+ process.platform,
270
+ process.arch,
271
+ )
272
+ return installed
273
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,12 +9,12 @@
9
9
  },
10
10
  "homepage": "https://github.com/ysr666/dsh-vision-router",
11
11
  "type": "module",
12
- "main": "index.js",
12
+ "main": "entry.js",
13
13
  "bin": {
14
14
  "dsh-vision-router": "./lib/doctor-cli.js"
15
15
  },
16
16
  "exports": {
17
- ".": "./index.js",
17
+ ".": "./entry.js",
18
18
  "./client": "./lib/client.js",
19
19
  "./package.json": "./package.json",
20
20
  "./cordis.patch.yml": "./cordis.patch.yml"
@@ -26,6 +26,7 @@
26
26
  "assets",
27
27
  "cordis.patch.yml",
28
28
  "docs",
29
+ "entry.js",
29
30
  "index.js",
30
31
  "lib",
31
32
  "presets"
@@ -50,18 +51,20 @@
50
51
  "@deepseek-ai/schemastery": ">=3.18.0",
51
52
  "potrace": "^2.1.8",
52
53
  "puppeteer-core": "^25.6.0",
53
- "undici": ">=6.15.0",
54
- "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.6",
55
- "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.6"
54
+ "undici": ">=6.15.0"
56
55
  },
57
56
  "peerDependencies": {
57
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.6",
58
+ "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.6",
58
59
  "sharp": ">=0.35.3 <1"
59
60
  },
60
61
  "devDependencies": {
62
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.6",
63
+ "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.6",
61
64
  "sharp": "^0.35.3"
62
65
  },
63
66
  "scripts": {
64
- "test": "node --test tests/core.test.js tests/client.test.js tests/http-compat.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js"
67
+ "test": "node --test tests/core.test.js tests/client.test.js tests/http-compat.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js"
65
68
  },
66
69
  "pnpm": {
67
70
  "onlyBuiltDependencies": [
package/presets/README.md CHANGED
@@ -4,6 +4,9 @@
4
4
  把它合并进你自己的 `settings.yaml`(或复制整个段),填一个 `apiKeyEnv` 对应的 Key 即可。
5
5
  **仓库绝不内置任何第三方 Key**。
6
6
 
7
+ > [!IMPORTANT]
8
+ > DSH 当前的 Web「设置 → 模型 → 添加自定义提供方」表单不会写入图片输入能力元数据。即使端点实际是智谱 GLM-4.6V、Qwen-VL 等视觉模型,如果模型没有显式的 `input: [text, image]`(或提供方没有 `defaultInput: [text, image]`),DSH 仍会把它报告为纯文本模型,Vision Router 会为安全起见从视觉后端下拉中隐藏它。下面这些 preset 已经带好 `input` 声明;如果你是通过 Web 手动添加的自定义视觉提供方,请在 `$DSH_HOME/settings.yaml` 补上该字段。
9
+
7
10
  - `dashscope.yaml` —— 阿里云百炼,大陆直连,新用户每系列 100 万 token/90 天(推荐首选)
8
11
  - `zhipu.yaml` —— 智谱 bigmodel.cn,`glm-4.6v-flash` 永久免费(provider key 用 `zhipu`,避免与 pi-ai 内置目录里的 `zai` 编码端点冲突)
9
12
  - `siliconflow.yaml` —— 硅基流动,¥14 赠金覆盖 Qwen2.5-VL