dsh-vision-router 2.2.2 → 2.2.3

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
@@ -253,7 +253,7 @@ The diagram covers the eleven image-processing tools. `vision_present` (durable
253
253
  | `vision_present` | Publish a generated or edited local image as a durable chat attachment so the user can see it | image attachment |
254
254
  | `vision_pixel_diff` | Per-pixel comparison: diff ratio + worst 8×8-grid regions | red heatmap PNG + JSON report |
255
255
  | `vision_colors` | Dominant colors (hex + share) | — |
256
- | `vision_ocr` | Text transcription: local tesseract (chi_sim+eng) first, vision model fallback | — |
256
+ | `vision_ocr` | Text transcription: configurable default engine (`auto` / local Tesseract / vision model); an explicit per-call engine still wins | — |
257
257
  | `vision_trace` | SVG vectorization (potrace posterization; icons/logos) | SVG |
258
258
  | `vision_extract_foreground` | Cutout via border flood fill (uniform backgrounds) | transparent PNG |
259
259
  | `vision_html_screenshot` | Screenshot a local HTML file (headless system Chrome); `fullPage: true` captures the whole page and reports `pageHeight` | PNG |
@@ -357,8 +357,8 @@ Everything is optional; defaults work out of the box. Prefer **Settings → Visi
357
357
  | `rewriteImages` | `true` | rewrite image blocks in the model input (cached description or tool-hint marker); the UI log keeps images |
358
358
  | `desktopScreenshot` | `false` | privacy opt-in for the model-callable `vision_screenshot` desktop-capture tool; checked live before every capture |
359
359
  | `freeFallback` | `true` | append the anonymous OVH models after explicit local/custom HTTP backends; turning this off never disables an explicitly configured local backend |
360
- | `localOllama` | `{ enabled: false, baseURL: 'http://127.0.0.1:11434/v1', model: 'qwen2.5vl', format: 'openai' }` | local vision backend; when enabled, `local-ollama` leads the HTTP vision chain, is skipped automatically when down, and supports OpenAI or Anthropic wire format |
361
- | `localLmStudio` | `{ enabled: false, baseURL: 'http://localhost:1234/v1', model: '', format: 'openai' }` | local LM Studio backend after Ollama; enter the exact model identifier from LM Studio Developer or `/v1/models` |
360
+ | `localOllama` | `{ enabled: false, baseURL: 'http://127.0.0.1:11434/v1', model: 'qwen2.5vl', format: 'openai', maxTokens: 4096, reasoningEffort: 'none' }` | local vision backend; OpenAI mode disables supported model reasoning by default so the output budget is spent on answer text |
361
+ | `localLmStudio` | `{ enabled: false, baseURL: 'http://localhost:1234/v1', model: '', format: 'openai', maxTokens: 4096, reasoningEffort: 'none' }` | local LM Studio backend after Ollama; LM Studio 0.4+ can use `format: 'lmstudio'` for documented native reasoning control |
362
362
  | `visionTurnBudgetMs` | `0` | whole-turn vision wall-clock budget; `0` means unlimited. Concrete provider calls/tools still keep their own hard deadlines |
363
363
  | `downscale` / `downscaleMaxPixels` | `true` / `4000000` | pre-call downscale and its pixel budget (latency guard) |
364
364
  | `cache` / `cacheTtlSeconds` / `cacheMaxEntries` | `true` / `3600` / `200` | vision answer cache |
@@ -409,19 +409,19 @@ ollama pull qwen2.5vl
409
409
  - When enabled, `local-ollama` heads the HTTP vision chain. For a strict local-only setup, remove cloud vision rows/custom HTTP endpoints and turn off `freeFallback`.
410
410
  - The selected loopback Ollama model is prewarmed through Ollama's native API and kept resident for 30 minutes. If it is cold when Ollama is the primary image backend, loading completes before the normal vision-task budget starts; a short `/api/ps` probe keeps a dead service on the fast fallback path. Remote Ollama URLs are never auto-warmed.
411
411
  - **LM Studio works the same way** — enable `localLmStudio` with its OpenAI-compatible endpoint (default `http://localhost:1234/v1`) and enter the exact model identifier shown in Developer or `/v1/models`. It sits after `local-ollama` and before custom/cloud HTTP backends.
412
- - Each local backend can speak **OpenAI or Anthropic format** via `format` (default `openai`). Anthropic mode routes to `/v1/messages` with `anthropic-version` and base64 image sources; `x-api-key` is sent only when a key is configured. LM Studio needs version 0.4.1 or newer for this endpoint.
412
+ - Local backends keep **OpenAI** as the compatibility default and can also use **Anthropic**. LM Studio additionally offers **LM Studio native** mode (`format: 'lmstudio'`, LM Studio 0.4+) at `/api/v1/chat`; use it when you need documented reasoning control (`reasoningEffort: 'none'` maps to `reasoning: off`). `maxTokens` is configurable and defaults to 4096.
413
413
  - If a local backend is down or the call times out, its entry is skipped automatically and the chain falls through to the cloud backends — no call breaks.
414
414
  - `vision_screenshot` is disabled by default. After the separate Desktop screenshot opt-in, `identify=true` uses the same Ollama → LM Studio fallback.
415
415
 
416
416
  ## Requirements
417
417
 
418
418
  - DeepSeek Harness Web profile. Normal installs can use `npx @deepseek-ai/dsh ...`; source checkouts use `pnpm dsh ...`. A bare `dsh ...` command only works when the CLI is already on your shell `PATH`.
419
- - **DSH Host support policy:** DVR 2.1.x keeps DSH `0.1.0-rc.8` as the public minimum and currently supports the released stable channel through `0.1.5-rc.2`. Exact `0.1.6-alpha.1` coverage is **verification evidence only**, not a preview support promise; scheduled `latest`/`alpha` canaries monitor drift without changing the support policy. DVR 2.0.x was the final train with public support for rc.6/rc.7. See [DSH Host support window](docs/architecture/dsh-support-window.md).
419
+ - **DSH Host support policy:** DVR 2.2.x keeps DSH `0.1.0-rc.8` as the public minimum and currently supports the released stable channel through `0.1.5-rc.3`. Exact `0.1.7-rc.2` (`next`) coverage is **verification evidence only**, not a preview support promise; scheduled `latest`/`alpha` canaries monitor drift without changing the support policy. DVR 2.0.x was the final train with public support for rc.6/rc.7. See [DSH Host support window](docs/architecture/dsh-support-window.md).
420
420
  - Node ≥ 22 (host side).
421
421
  - No API key for the default free chain; a credential reference (`apiKeyEnv`) only for paid `httpProviders`.
422
422
  - Chrome / Chromium / Edge is needed only for `vision_html_screenshot`; every other tool works without a browser.
423
423
  - Desktop capture is opt-in. Windows and macOS use OS-provided capture facilities; Linux needs ImageMagick `import` or `scrot` and a capturable desktop session (Wayland support depends on the environment).
424
- - Tesseract is optional: `vision_ocr` falls back to the vision model when the local engine is absent.
424
+ - Tesseract is optional. `vision_ocr` defaults to `ocrEngine: auto` (local Tesseract first, vision fallback), and Settings → Advanced → **Default OCR engine** can force Tesseract-only or vision-model-only behavior without uninstalling Tesseract. An explicit per-call `engine=tesseract|vision` always overrides the default.
425
425
 
426
426
  ## Install and lifecycle
427
427
 
package/README.zh.md CHANGED
@@ -251,7 +251,7 @@ Agent 仅根据参考图复刻 UI,再用 `vision_pixel_diff` 验证最终结
251
251
  | `vision_present` | 把生成或编辑后的本地图片发布为持久聊天附件,供用户查看 | 图片附件 |
252
252
  | `vision_pixel_diff` | 逐像素对比:差异率 + 最差 8×8 网格区域 | 红色热力图 PNG + JSON 报告 |
253
253
  | `vision_colors` | 主色提取(十六进制 + 占比) | — |
254
- | `vision_ocr` | 文字转写:本地 tesseract(中英)优先,视觉模型兜底 | — |
254
+ | `vision_ocr` | 文字转写:默认引擎可配置为 `auto` / 本地 Tesseract / 视觉模型;单次显式 engine 仍优先 | — |
255
255
  | `vision_trace` | SVG 矢量化(potrace 分色;图标/logo) | SVG |
256
256
  | `vision_extract_foreground` | 边界洪泛抠图(纯色背景) | 透明 PNG |
257
257
  | `vision_html_screenshot` | 给本地 HTML 文件截图(无头系统 Chrome);`fullPage: true` 截整页并返回 `pageHeight` | PNG |
@@ -355,8 +355,8 @@ Web profile 现在提供一级 **设置 → Vision Router** 页面。常规页
355
355
  | `rewriteImages` | `true` | 模型输入层改写图片块(缓存描述或工具提示标记);界面日志保留图片 |
356
356
  | `desktopScreenshot` | `false` | 模型可调用的 `vision_screenshot` 桌面截屏隐私开关;每次截屏前实时检查 |
357
357
  | `freeFallback` | `true` | 在显式本地/自定义 HTTP 后端之后追加匿名 OVH 模型;关闭它不会停用用户明确配置的本地后端 |
358
- | `localOllama` | `{ enabled: false, baseURL: 'http://127.0.0.1:11434/v1', model: 'qwen2.5vl', format: 'openai' }` | 本地视觉后端;开启后排在 HTTP 视觉链前部,服务未运行会自动跳过,支持 OpenAI / Anthropic 协议 |
359
- | `localLmStudio` | `{ enabled: false, baseURL: 'http://localhost:1234/v1', model: '', format: 'openai' }` | Ollama 之后的本地 LM Studio 后端;填写 Developer 页或 `/v1/models` 返回的真实模型 ID |
358
+ | `localOllama` | `{ enabled: false, baseURL: 'http://127.0.0.1:11434/v1', model: 'qwen2.5vl', format: 'openai', maxTokens: 4096, reasoningEffort: 'none' }` | 本地视觉后端;OpenAI 模式默认关闭受支持模型的推理,把输出预算留给正文 |
359
+ | `localLmStudio` | `{ enabled: false, baseURL: 'http://localhost:1234/v1', model: '', format: 'openai', maxTokens: 4096, reasoningEffort: 'none' }` | Ollama 之后的本地 LM Studio 后端;LM Studio 0.4+ 可选 `format: 'lmstudio'` 使用官方原生推理控制 |
360
360
  | `visionTurnBudgetMs` | `0` | 整轮视觉总墙钟预算;`0` = 不设整轮上限。具体 provider调用/工具仍有自己的硬超时 |
361
361
  | `downscale` / `downscaleMaxPixels` | `true` / `4000000` | 调用前压缩及其像素预算(延迟保护) |
362
362
  | `cache` / `cacheTtlSeconds` / `cacheMaxEntries` | `true` / `3600` / `200` | 视觉答案缓存 |
@@ -407,19 +407,19 @@ ollama pull qwen2.5vl
407
407
  - 开启后 `local-ollama` 排在 HTTP 视觉链前部。若要严格纯本地,请移除云视觉行/自定义 HTTP 端点,并关闭 `freeFallback`。
408
408
  - 选中的本机 loopback Ollama 模型会通过原生 API 预热并保持 30 分钟驻留。如果模型在 Ollama 作为首个图片后端时已经冷却,加载会在正常视觉任务预算开始之前完成;短 `/api/ps` 探测保证服务未运行/挂死时仍快速进入 fallback。远程 Ollama URL 不会自动预热。
409
409
  - **LM Studio 同理**——开启 `localLmStudio`,填 OpenAI 兼容端点(默认 `http://localhost:1234/v1`),并使用 Developer 页或 `/v1/models` 返回的真实模型标识。它排在 `local-ollama` 之后、自定义/云 HTTP 后端之前。
410
- - 每个本地后端可通过 `format` 选择 **OpenAI 或 Anthropic 格式**(默认 `openai`)。Anthropic 模式走 `/v1/messages`,带 `anthropic-version` 并把图片转为 base64 source;只有配置了 Key 才发送 `x-api-key`。LM Studio 需 0.4.1 或更高版本才提供该端点。
410
+ - 本地后端继续以 **OpenAI** 为兼容默认,也可选 **Anthropic**。LM Studio 额外提供 **LM Studio 原生**模式(`format: 'lmstudio'`,需 LM Studio 0.4+),走 `/api/v1/chat`;需要稳定关闭推理时推荐该模式,因为官方 API 明确支持 `reasoning: off`。`maxTokens` 可配置,默认 4096。
411
411
  - 任一本地后端未运行或调用超时时自动跳过,继续降级到云链。
412
412
  - `vision_screenshot` 默认关闭。单独开启「桌面截屏」隐私开关后,`identify=true` 使用同样的 Ollama → LM Studio 降级顺序。
413
413
 
414
414
  ## 环境要求
415
415
 
416
416
  - DeepSeek Harness 的 Web profile。普通安装可用 `npx @deepseek-ai/dsh ...`;从源码仓库运行时用 `pnpm dsh ...`。只有 CLI 已经进入系统 `PATH` 时才能直接写 `dsh ...`。
417
- - **DSH Host 支持策略:** DVR 2.1.x 的公开最低 Host 仍为 DSH `0.1.0-rc.8`,当前正式发布通道已验证并支持到 `0.1.5-rc.2`。对 `0.1.6-alpha.1` 的精确覆盖**只属于验证证据**,不代表对 preview 的公开支持承诺;定时 `latest`/`alpha` canary 只负责发现上游漂移,也不会自动改变支持策略。DVR 2.0.x 是最后公开支持 rc.6/rc.7 的版本线。详见 [DSH Host 支持窗口](docs/architecture/dsh-support-window.md)。
417
+ - **DSH Host 支持策略:** DVR 2.2.x 的公开最低 Host 仍为 DSH `0.1.0-rc.8`,当前正式发布通道已验证并支持到 `0.1.5-rc.3`。对 `0.1.7-rc.2`(`next`)的精确覆盖**只属于验证证据**,不代表对 preview 的公开支持承诺;定时 `latest`/`alpha` canary 只负责发现上游漂移,也不会自动改变支持策略。DVR 2.0.x 是最后公开支持 rc.6/rc.7 的版本线。详见 [DSH Host 支持窗口](docs/architecture/dsh-support-window.md)。
418
418
  - Node ≥ 22(宿主侧)。
419
419
  - 默认免费链路无需 API Key;付费 `httpProviders` 只需一个凭据引用(`apiKeyEnv`)。
420
420
  - 只有 `vision_html_screenshot` 需要 Chrome / Chromium / Edge;其余工具无浏览器也能用。
421
421
  - 桌面截屏必须显式开启。Windows/macOS 使用系统截屏能力;Linux 需安装 ImageMagick `import` 或 `scrot`,且必须处于可截取的桌面会话(Wayland 支持取决于环境)。
422
- - tesseract 可选:本地引擎缺失时 `vision_ocr` 自动退回视觉模型。
422
+ - tesseract 可选。`vision_ocr` 默认使用 `ocrEngine: auto`(本地 Tesseract 优先、失败或空结果再回退视觉模型);也可在「设置 → 高级 → OCR 默认引擎」强制仅 Tesseract 或直接使用视觉模型,无需卸载 Tesseract。单次调用显式 `engine=tesseract|vision` 始终覆盖默认设置。
423
423
 
424
424
  ## 安装与生命周期
425
425
 
package/cordis.patch.yml CHANGED
@@ -43,6 +43,17 @@
43
43
  config:
44
44
  progressiveTools: false
45
45
 
46
+ # DSH 的 live patch/HMR 旧实现会把 loader dispose 的 disabled=true 就地写回
47
+ # bundle insert 对象,随后 recompose 又复用同一个对象(upstream #2854)。
48
+ # 0.1.7-rc.2 仍保留该共享对象边界;Desktop supervisor + 多 bundle 场景可因此
49
+ # 反复卸载/重挂 DVR(issue #547)。在 insert 之后再按 id 显式恢复 enabled,
50
+ # 让每次 recompose 都覆盖被污染的 disabled 值。新 Include 会立即命中上面的
51
+ # insert;旧 Include 若尚不能在同一 patch list 中索引新 insert,则只会跳过这条
52
+ # overlay,保持旧 Host 原有启动行为。上游 clone insert 后可审计删除。
53
+ - id: vision-router
54
+ name: dsh-vision-router
55
+ disabled: false
56
+
46
57
  # Vision Router 的附件产品契约:Host attachment store 是 durable 图片的唯一
47
58
  # owner,视觉工具只读取 Host 已持久化、校验后的 canonical pixels。DSH rc.8
48
59
  # 以前 Host 会把已准入的源编码直接持久化;0.1.2-alpha.1 开始又增加独立的
@@ -8,11 +8,11 @@ The matrix is capability-based. Runtime code must feature-detect the seam it nee
8
8
 
9
9
  | CI fixture | DSH package line | Role |
10
10
  | --- | --- | --- |
11
- | `minimum-contract` | `0.1.0-rc.6` | Historical compatibility-retention fixture. The name is legacy; DVR 2.1.x public support starts at rc.8. |
11
+ | `minimum-contract` | `0.1.0-rc.6` | Historical compatibility-retention fixture. The name is legacy; DVR 2.1.x and later 2.x public support starts at rc.8. |
12
12
  | `legacy-contract` | `0.1.0-rc.8` | Public support-floor fixture carrying batch attachments and dimension policy. |
13
- | `current-contract` | `0.1.5-rc.2` | Current stable Host contract baseline. Must remain green. |
14
- | `preview-contract` / exact preview gates | `0.1.6-alpha.1` | Required verification evidence only; not a public preview-support claim. |
15
- | retained preview regression | `0.1.5-alpha.2` | Older preview regression fixture retained while the 0.1.6 preview line is qualified. |
13
+ | `current-contract` | `0.1.5-rc.3` | Current stable Host contract baseline. Must remain green. |
14
+ | `preview-contract` / exact preview gates | `0.1.7-rc.2` (`next`) | Required verification evidence only; not a public preview-support claim. |
15
+ | retained preview regression | `0.1.5-alpha.2` | Older preview regression fixture retained as an older preview regression while the 0.1.7 next line is qualified. |
16
16
  | `latest-dsh` / alpha canaries | resolved dynamically from npm dist-tags | Scheduled drift surveillance only. Never changes support policy by itself. |
17
17
 
18
18
  Node 22 and Node 24 remain the general required runtime matrix. The Host contract jobs are additive; they do not replace the normal test matrix.
@@ -21,7 +21,7 @@ Node 22 and Node 24 remain the general required runtime matrix. The Host contrac
21
21
 
22
22
  `yes` means the fixture has a direct positive test or feature probe. `no` means a direct negative probe exists. `compat` means the fixture proves Vision Router can safely carry the newer input/config through that Host, but does **not** claim the Host owns that capability. `probe` means the capability is intentionally not inferred from the version label and is verified at runtime/contract-test time.
23
23
 
24
- | Capability | minimum-contract rc.6 | legacy-contract rc.8 | current-contract rc.2 (0.1.5) | Evidence / detection |
24
+ | Capability | minimum-contract rc.6 | legacy-contract rc.8 | current-contract rc.3 (0.1.5) | Evidence / detection |
25
25
  | --- | --- | --- | --- | --- |
26
26
  | Batch attachment save | no | yes | yes | `hasBatchAttachmentContract()` checks the released `attachments.saveImages` prototype; `tests/rc6-rc7-compat.test.js`; contract CI. |
27
27
  | Max image dimension policy | compat | yes | yes | All fixtures parse the complete attachment-local row; rc.8/current positively retain the field and the established admission tests exercise the 10000/10001 boundary. Older Schemastery passthrough is not treated as ownership evidence. |
@@ -38,7 +38,7 @@ Node 22 and Node 24 remain the general required runtime matrix. The Host contrac
38
38
  | Public entry boot | yes | yes | yes | packed plugin public entry import in each Host contract fixture. |
39
39
  | Packaged tarball install | yes | yes | yes | each Host contract fixture packs the plugin then installs the tarball into an isolated Host package. |
40
40
 
41
- Preview-only evidence: the `0.1.6-alpha.1` contract fixture additionally mounts the Host-owned `@deepseek-ai/dsh-compaction-image-offload` projection and proves that one offloaded image occurrence stays offloaded across live requests, `SessionStore.fork()`, JSONL process restart, and cold resume, while a new occurrence of the same durable attachment remains a retained image. This is verification evidence for the preview contract only; it does not expand the public support window.
41
+ Preview-only evidence: the `0.1.7-rc.2` (`next`) contract fixture additionally mounts the Host-owned `@deepseek-ai/dsh-compaction-image-offload` projection and proves that one offloaded image occurrence stays offloaded across live requests, `SessionStore.fork()`, JSONL process restart, and cold resume, while a new occurrence of the same durable attachment remains a retained image. This is verification evidence for the preview contract only; it does not expand the public support window.
42
42
 
43
43
  ## Compatibility inventory and exit criteria
44
44
 
@@ -8,12 +8,12 @@ The public support policy contains only released Host semantics. Preview/canary
8
8
 
9
9
  | Role | DSH train | Meaning |
10
10
  |---|---|---|
11
- | Minimum Supported Host | `0.1.0-rc.8` | Oldest Host generation that DVR 2.1.x publicly supports. |
12
- | Current Stable Host | `0.1.5-rc.2` | Current npm stable-channel release covered by required exact Host and browser evidence. |
11
+ | Minimum Supported Host | `0.1.0-rc.8` | Oldest Host generation that DVR 2.2.x publicly supports. |
12
+ | Current Stable Host | `0.1.5-rc.3` | Current npm stable-channel release covered by required exact Host and browser evidence. |
13
13
 
14
- DVR `2.1.x` therefore keeps `0.1.0-rc.8` as its public floor and supports released Host trains through the current stable channel. Runtime branching remains capability-based rather than version-string-driven.
14
+ DVR `2.2.x` therefore keeps `0.1.0-rc.8` as its public floor and supports released Host trains through the current stable channel. Runtime branching remains capability-based rather than version-string-driven.
15
15
 
16
- DSH `0.1.5-rc.1` remains explicitly admitted for existing installations; current exact stable evidence has advanced to `0.1.5-rc.2`. Advancing this evidence does not raise the public minimum.
16
+ DSH `0.1.5-rc.1` / `0.1.5-rc.2` remain admitted for existing installations; current exact stable evidence has advanced to `0.1.5-rc.3`. Advancing this evidence does not raise the public minimum.
17
17
 
18
18
  No later support-floor increase is currently announced.
19
19
 
@@ -23,8 +23,8 @@ Compatibility evidence answers a different question: what exact upstream release
23
23
 
24
24
  | Evidence role | DSH source | Meaning |
25
25
  |---|---|---|
26
- | Exact stable evidence | `0.1.5-rc.2` | Required Host/wire and real Host + Chromium coverage for the current stable release. |
27
- | Exact preview evidence | `0.1.6-alpha.1` | Required preview Host/wire/lifecycle/browser evidence. This is not a preview support promise. |
26
+ | Exact stable evidence | `0.1.5-rc.3` | Required Host/wire and real Host + Chromium coverage for the current stable release. |
27
+ | Exact preview evidence | `0.1.7-rc.2` (`next`) | Required preview Host/wire/lifecycle/browser evidence. This is not a preview support promise. |
28
28
  | Stable drift canary | npm dist-tag `latest` | Scheduled, dynamically resolved surveillance. A failure starts compatibility investigation; it does not rewrite support policy. |
29
29
  | Preview drift canary | npm dist-tag `alpha` | Scheduled, dynamically resolved surveillance with preview-specific lifecycle coverage. A failure does not rewrite support policy. |
30
30
 
@@ -43,7 +43,7 @@ DVR 2.0.x minimum: DSH 0.1.0-rc.6
43
43
  DVR 2.1.x minimum: DSH 0.1.0-rc.8
44
44
  ```
45
45
 
46
- Users still on rc.6/rc.7 should upgrade DSH before upgrading to DVR 2.1.x.
46
+ Users still on rc.6/rc.7 should upgrade DSH before upgrading to DVR 2.1.x or any later 2.x train. DVR 2.2.x inherits the same rc.8 floor; this maintenance update does not raise it.
47
47
 
48
48
  This support-floor transition does **not** require deleting every rc.6-era compatibility seam in the same release. Compatibility code is retired only after a separate proof shows it is unreachable or unnecessary on every supported Host and durable-history path.
49
49
 
@@ -50,7 +50,7 @@ H1 is complete as an egress contract, not as a new production dependency. Exact
50
50
  4. The same contract runs against the exact current stable Host and exact preview evidence on Linux, macOS, and Windows through the existing source-contract matrix.
51
51
  5. Production code contains no `@deepseek-ai/dsh-http-proxy` dependency or import. `proxyRouteFor()` is used only by the exact-source test as an oracle for the Host decision.
52
52
 
53
- The current stable evidence advances to DSH `0.1.5-rc.2`; `0.1.5-rc.1` remains explicitly peer-admitted, and the public minimum remains `0.1.0-rc.8`.
53
+ The current stable evidence advances to DSH `0.1.5-rc.3`; `0.1.5-rc.1` / `0.1.5-rc.2` remain explicitly peer-admitted, exact next-channel evidence is `0.1.7-rc.2`, and the public minimum remains `0.1.0-rc.8`.
54
54
 
55
55
  ## H2 — Scope Host-owned override authority
56
56
 
@@ -0,0 +1,22 @@
1
+ # v2.2.3
2
+
3
+ DSH Vision Router 2.2.3 is a compatibility and stability hotfix focused on DSH 0.1.7-rc.2, long-running browser Hosts, local vision backends, and release recovery.
4
+
5
+ ## Highlights
6
+
7
+ - Qualifies exact DSH `0.1.7-rc.2` support without raising the public Host floor. Session format v4 and the new Settings Forms + ConfigEditor composition are explicitly recognized while unknown future contracts still fail closed.
8
+ - Fixes the UOS/Desktop multi-bundle HMR/recompose failure class reported in #547: the Vision Router bundle now restores its inserted row after Loader `disabled=true` mutation instead of inheriting a disabled row across recomposition.
9
+ - Makes local reasoning/output budgets configurable so Ollama/LM Studio-compatible local vision backends are not forced into one fixed response budget.
10
+ - Adds a configurable default OCR engine policy and hardens structured-answer fallback, long-OCR blank retry handling, and one-time Session event-feed activation backfill.
11
+ - Makes npm publication recovery idempotent: public-registry propagation is classified as missing/transient/exact/mismatch, delayed visibility is tolerated, and mismatched or fatal identities fail closed.
12
+
13
+ ## DSH 0.1.7-rc.2 validation
14
+
15
+ - Exact DSH source: `0.1.7-rc.2` at `477b4f420553e8a52c2fbccc464d7561b239c443`.
16
+ - Node 22/24 compatibility gates and Windows/macOS/Linux exact-source checks pass.
17
+ - Real `dsh web` + Chromium coverage passes for startup, Vision mode, mixed attachments, Settings mount/save/reload/readback, and live bundle recomposition.
18
+ - The public minimum supported DSH Host remains `0.1.0-rc.8`; existing 2.2.x settings require no migration.
19
+
20
+ ## Upgrade
21
+
22
+ Users on DSH 0.1.7-rc.2, especially Desktop/multi-bundle or local-model setups, should upgrade to 2.2.3 after release. No settings migration is required.
package/index.js CHANGED
@@ -249,6 +249,8 @@ export const Config = z.object({
249
249
  // (its own cap) and the vision-model fallback only the rest — never two
250
250
  // full timeouts added together.
251
251
  ocrTimeoutMs: z.number().step(1).min(1000).max(120000).default(30000),
252
+ // Default engine for vision_ocr. Per-call engine=tesseract|vision remains authoritative.
253
+ ocrEngine: z.union(['auto', 'tesseract', 'vision']).default('auto'),
252
254
  proxy: z.string().default(''),
253
255
  proxyHosts: z.array(z.string()).default([...DEFAULT_PROXY_HOSTS]),
254
256
  // Remote browsers are intentionally unable to use DSH's broad settings.*
@@ -312,6 +314,8 @@ export const Config = z.object({
312
314
  // 设置卡用 placeholder 提示识别任务常用的建议值。
313
315
  temperature: z.number().min(0).max(2),
314
316
  top_p: z.number().min(0).max(1),
317
+ maxTokens: z.number().step(1).min(256).max(32768).default(4096),
318
+ reasoningEffort: z.union(['provider_default', 'none', 'low', 'medium', 'high', 'max']).default('none'),
315
319
  })
316
320
  .default({}),
317
321
  // ── dsh-vision 并入:本地 LM Studio 视觉后端(与 Ollama 同层级)───────────
@@ -326,10 +330,14 @@ export const Config = z.object({
326
330
  model: z.string().default(''),
327
331
  // 请求格式:'openai'(/chat/completions,默认)| 'anthropic'
328
332
  // (/messages,LM Studio 的 OpenAI 兼容服务同样提供)。
329
- format: z.union(['openai', 'anthropic']).default('openai'),
333
+ // LM Studio 0.4+ 的 native /api/v1/chat 明确支持 reasoning=off;
334
+ // OpenAI Chat Completions 仍保留给旧版/兼容服务。
335
+ format: z.union(['openai', 'anthropic', 'lmstudio']).default('openai'),
330
336
  // 与 localOllama 相同:显式设置才透传,留空尊重服务端默认。
331
337
  temperature: z.number().min(0).max(2),
332
338
  top_p: z.number().min(0).max(1),
339
+ maxTokens: z.number().step(1).min(256).max(32768).default(4096),
340
+ reasoningEffort: z.union(['provider_default', 'none', 'low', 'medium', 'high', 'max']).default('none'),
333
341
  })
334
342
  .default({}),
335
343
  // Legacy compatibility only: older profiles may still contain these two
@@ -2545,7 +2553,7 @@ export function apply(ctx, config = {}, runtime = {}) {
2545
2553
  const ocrPolicy =
2546
2554
  '不要默认把 OCR 当第二步;仅在需要逐字保真时用 vision_ocr,并把结果当作需要结合上下文验证的证据。' +
2547
2555
  'UI/截图语义通常用 vision_describe 或 vision_detect,精确定位用 vision_ground。' +
2548
- 'vision_ocr 的 engine=auto 始终先尝试本地 Tesseract,失败或空结果时再回退视觉模型;结构化模式不会改变这一顺序。' +
2556
+ 'vision_ocr 未显式指定 engine 时遵循设置中的 OCR 默认引擎;单次显式 engine=tesseract/vision 始终优先。' +
2549
2557
  '完成至少 1 次后续证据调用后,证据充分就直接作答,不要为了流程继续调用。'
2550
2558
  bootstrapReminder = {
2551
2559
  role: 'user',
@@ -3062,9 +3070,9 @@ ctx.logger?.info(
3062
3070
 
3063
3071
  }
3064
3072
  }
3065
- const fallback = `vision_describe: the model did not produce valid JSON. Raw output:\n${text.slice(0, 2000)}`
3066
- if (cacheEnabled()) cache.set(key, fallback)
3067
- return fallback
3073
+ const invalidJson = new Error('vision_describe backend did not produce valid JSON after one correction retry')
3074
+ invalidJson.code = 'INVALID_REQUEST'
3075
+ throw invalidJson
3068
3076
  }
3069
3077
  if (text !== '') {
3070
3078
  if (cacheEnabled()) cache.set(key, text)
@@ -3188,9 +3196,9 @@ ctx.logger?.info(
3188
3196
 
3189
3197
  }
3190
3198
  }
3191
- const fallback = `vision_describe: the model did not produce valid JSON. Raw output:\n${text.slice(0, 2000)}`
3192
- if (cacheEnabled()) cache.set(key, fallback)
3193
- return fallback
3199
+ const invalidJson = new Error('vision_describe backend did not produce valid JSON after one correction retry')
3200
+ invalidJson.code = 'INVALID_REQUEST'
3201
+ throw invalidJson
3194
3202
  }
3195
3203
  if (text !== '') {
3196
3204
  if (cacheEnabled()) cache.set(key, text)
@@ -4195,10 +4203,11 @@ ctx.logger?.info(
4195
4203
  deepToolDefs.push({
4196
4204
  name: 'vision_ocr',
4197
4205
  description:
4198
- 'Transcribe TEXT from an image. ENGINE POLICY: omitted engine / engine=auto always tries local ' +
4199
- 'Tesseract (chi_sim+eng) first — fast, free, offline — then falls back to a vision model if local ' +
4200
- 'OCR fails or returns no text. Structured 1+x follow-up does not change this order. Explicit ' +
4201
- 'engine=tesseract or engine=vision is always honored. Returns the text and which engine produced it. ' +
4206
+ 'Transcribe TEXT from an image. ENGINE POLICY: explicit engine=tesseract or engine=vision always wins. ' +
4207
+ 'Otherwise the configured OCR engine policy applies. The default auto policy tries local Tesseract ' +
4208
+ '(chi_sim+eng) first — fast, free, offline — then falls back to a vision model if local OCR fails or ' +
4209
+ 'returns no text. Structured 1+x follow-up does not change the selected policy. Returns the text and ' +
4210
+ 'which engine produced it. ' +
4202
4211
  'SCOPE: vision_ocr reads letters, it does NOT recognize people, objects or scenes. Never use it ' +
4203
4212
  'as a fallback when vision_describe fails to identify who/what is in a picture ("这是谁" / ' +
4204
4213
  '"这是什么东西" questions are answered by vision_describe, not OCR). If vision_describe returns ' +
@@ -4227,7 +4236,7 @@ ctx.logger?.info(
4227
4236
  },
4228
4237
  engine: {
4229
4238
  type: 'string',
4230
- description: '"auto" (default): always try local Tesseract first, then fall back to the vision model if local OCR fails or returns no text. Structured 1+x does not change this order; use explicit "tesseract"/"vision" to force an engine.',
4239
+ description: '"auto": use the configured OCR engine policy (default policy is local Tesseract first, then vision fallback); explicit "tesseract"/"vision" always overrides the configured default for this call.',
4231
4240
  },
4232
4241
  },
4233
4242
  additionalProperties: false,
@@ -4236,7 +4245,7 @@ ctx.logger?.info(
4236
4245
  async execute(args, exec) {
4237
4246
  const imageInput = resolveOcrImageInput(args)
4238
4247
  const session = exec?.agent?.session
4239
- const engine = resolveVisionOcrEngine(args.engine)
4248
+ const engine = resolveVisionOcrEngine(args.engine, current().ocrEngine)
4240
4249
  const degraded = degradedLocalState(session, imageInput)
4241
4250
  if (
4242
4251
  engine !== 'vision' &&
@@ -4465,8 +4474,11 @@ ctx.logger?.info(
4465
4474
  used = 'failed'
4466
4475
  text = ''
4467
4476
  } else {
4468
- const retryText = retry.text.trim()
4469
- if (retryText !== '') text = retryText
4477
+ // An ok retry that came back blank means the stricter prompt
4478
+ // found no visible text. The first answer was already judged
4479
+ // a hallucination (12k+ chars) — keeping it here would
4480
+ // publish it as engine-verified. Same contract as EMPTY below.
4481
+ text = retry.text.trim()
4470
4482
  used = 'vision'
4471
4483
  }
4472
4484
  } else {
package/lib/client.js CHANGED
@@ -71,9 +71,14 @@ window.__ModuleLoader__.load({
71
71
  localOllamaModel: 'Ollama 模型名',
72
72
  localOllamaTemperature: '温度 temperature',
73
73
  localOllamaTopP: 'top_p',
74
+ localMaxTokens: '最大输出 token',
75
+ localReasoningEffort: '推理强度',
76
+ localReasoningDefault: '服务端默认',
77
+ localReasoningNone: '关闭推理',
74
78
  localRequestFormat: '请求协议',
75
79
  localFormatOpenAI: 'OpenAI(/chat/completions)',
76
80
  localFormatAnthropic: 'Anthropic(/messages)',
81
+ localFormatLmStudio: 'LM Studio 原生(/api/v1/chat)',
77
82
  localTemperaturePlaceholder: '留空=服务端默认;建议 0.5',
78
83
  localTopPPlaceholder: '留空=服务端默认;建议 0.8',
79
84
  localLmStudioModelPlaceholder: '填写 Developer 页或 /v1/models 中的模型标识',
@@ -219,7 +224,7 @@ window.__ModuleLoader__.load({
219
224
  '用于兼容旧版行为,一般无需开启;开启后只使用上方识图模型链中的后端。',
220
225
  hintReverseRouting: '仅在「整轮交给视觉模型」开启时生效;纯文字消息继续交给聊天模型处理。',
221
226
  hintTool: '允许聊天模型按需查看、定位、裁剪和比较图片。推荐保持开启。',
222
- hintStructuredVisionBootstrap: '默认关闭。开启后,每个图片任务会先做一次不读取具体任务目标的结构化预识别,再由聊天模型根据原问题至少追加一次验证或深挖识图调用(1+x,x≥1)。启用的 Ollama / LM Studio 会和其他视觉后端一样参与这条识图链,不需要另开「即时识图」。准确性更高,但会增加至少一次视觉调用;若后续需要 OCR,engine=auto 仍会先尝试本地 Tesseract,失败或空结果再回退视觉模型;结构化模式不会改变这一顺序。需保持「识图工具」开启。',
227
+ hintStructuredVisionBootstrap: '默认关闭。开启后,每个图片任务会先做一次不读取具体任务目标的结构化预识别,再由聊天模型根据原问题至少追加一次验证或深挖识图调用(1+x,x≥1)。启用的 Ollama / LM Studio 会和其他视觉后端一样参与这条识图链,不需要另开「即时识图」。准确性更高,但会增加至少一次视觉调用;若后续需要 OCR,未显式指定 engine 时会遵循「OCR 默认引擎」设置;单次显式 engine=tesseract/vision 始终优先。需保持「识图工具」开启。',
223
228
  hintAutoWrapProviders: '自动为已启用的聊天模型创建带「+ 自动识图」的版本。原模型不受影响,推荐保持开启。',
224
229
  hintRewriteImages: '避免把无法读取的原始图片直接发送给纯文字模型。推荐保持开启。',
225
230
  hintDownscale: '超过像素预算的图片先缩放再送视觉模型,降低延迟与成本;默认开启。',
@@ -243,6 +248,11 @@ window.__ModuleLoader__.load({
243
248
  numTimeoutMs: '单次识图请求超时(毫秒)',
244
249
  numVisionTaskTimeoutMs: '识图任务最长时间(毫秒)',
245
250
  numOcrTimeoutMs: 'OCR 最长时间(毫秒)',
251
+ selectOcrEngine: 'OCR 默认引擎',
252
+ hintOcrEngine: '决定 vision_ocr 未显式指定 engine 时的默认策略。自动:先本地 Tesseract,失败/空结果再用视觉模型;Tesseract:只用本地 OCR;视觉模型:直接跳过 Tesseract。单次工具调用显式 engine=tesseract/vision 始终优先。',
253
+ ocrEngineAuto: '自动(Tesseract → 视觉模型)',
254
+ ocrEngineTesseract: '仅 Tesseract',
255
+ ocrEngineVision: '仅视觉模型',
246
256
  numDownscaleMaxPixels: '图片像素上限',
247
257
  numCacheTtlSeconds: '缓存有效期(秒)',
248
258
  numCacheMaxEntries: '最大缓存数量',
@@ -347,9 +357,14 @@ window.__ModuleLoader__.load({
347
357
  localOllamaModel: 'Ollama model name',
348
358
  localOllamaTemperature: 'Temperature',
349
359
  localOllamaTopP: 'Top-p',
360
+ localMaxTokens: 'Max output tokens',
361
+ localReasoningEffort: 'Reasoning effort',
362
+ localReasoningDefault: 'Provider default',
363
+ localReasoningNone: 'Disable reasoning',
350
364
  localRequestFormat: 'Request protocol',
351
365
  localFormatOpenAI: 'OpenAI (/chat/completions)',
352
366
  localFormatAnthropic: 'Anthropic (/messages)',
367
+ localFormatLmStudio: 'LM Studio native (/api/v1/chat)',
353
368
  localTemperaturePlaceholder: 'Blank = server default; suggested 0.5',
354
369
  localTopPPlaceholder: 'Blank = server default; suggested 0.8',
355
370
  localLmStudioModelPlaceholder: 'Model identifier from Developer or /v1/models',
@@ -496,7 +511,7 @@ window.__ModuleLoader__.load({
496
511
  'only the vision models configured above participate.',
497
512
  hintReverseRouting: 'Only applies when whole-turn vision routing is enabled; plain text messages continue to use the chat model.',
498
513
  hintTool: 'Lets the chat model inspect, locate, crop, and compare images as needed. Recommended on.',
499
- hintStructuredVisionBootstrap: 'Off by default. Each image task first gets one task-independent structured visual baseline with no task goal passed into the pre-scan, then the chat model must make at least one evidence or deepening vision call for the original request (1+x, x>=1). Enabled Ollama / LM Studio backends participate in this same vision chain; there is no separate instant-recognition switch. This improves evidence quality but adds at least one visual call. If OCR is needed, engine=auto still tries local Tesseract first and falls back to the vision model only when local OCR fails or returns no text; structured mode does not change this order. Keep Vision tools enabled.',
514
+ hintStructuredVisionBootstrap: 'Off by default. Each image task first gets one task-independent structured visual baseline with no task goal passed into the pre-scan, then the chat model must make at least one evidence or deepening vision call for the original request (1+x, x>=1). Enabled Ollama / LM Studio backends participate in this same vision chain; there is no separate instant-recognition switch. This improves evidence quality but adds at least one visual call. If OCR is needed, calls without an explicit engine follow the “Default OCR engine” setting; an explicit engine=tesseract/vision always wins for that call. Keep Vision tools enabled.',
500
515
  hintAutoWrapProviders: 'Automatically creates a “+ Auto Vision” version of enabled chat models. Original model groups stay unchanged. Recommended on.',
501
516
  hintRewriteImages: 'Prevents raw image content from being sent to a text-only model that cannot read it. Recommended on.',
502
517
  hintDownscale: 'Images beyond the pixel budget are resized before the vision call, cutting latency and cost; on by default.',
@@ -521,6 +536,11 @@ window.__ModuleLoader__.load({
521
536
  numTimeoutMs: 'Single vision request timeout (ms)',
522
537
  numVisionTaskTimeoutMs: 'Vision task maximum time (ms)',
523
538
  numOcrTimeoutMs: 'OCR maximum time (ms)',
539
+ selectOcrEngine: 'Default OCR engine',
540
+ hintOcrEngine: 'Default policy when vision_ocr does not explicitly choose an engine. Auto tries local Tesseract first and falls back to the vision model; Tesseract uses local OCR only; Vision skips Tesseract. An explicit engine=tesseract/vision on a tool call always wins.',
541
+ ocrEngineAuto: 'Auto (Tesseract → vision)',
542
+ ocrEngineTesseract: 'Tesseract only',
543
+ ocrEngineVision: 'Vision model only',
524
544
  numDownscaleMaxPixels: 'Image pixel limit',
525
545
  numCacheTtlSeconds: 'Cache TTL (seconds)',
526
546
  numCacheMaxEntries: 'Maximum cached answers',
@@ -719,10 +739,14 @@ window.__ModuleLoader__.load({
719
739
  localOllama: {
720
740
  baseURL: 'http://127.0.0.1:11434/v1',
721
741
  model: 'qwen2.5vl',
742
+ maxTokens: 4096,
743
+ reasoningEffort: 'none',
722
744
  },
723
745
  localLmStudio: {
724
746
  baseURL: 'http://localhost:1234/v1',
725
747
  model: '',
748
+ maxTokens: 4096,
749
+ reasoningEffort: 'none',
726
750
  },
727
751
  }
728
752
 
@@ -739,7 +763,17 @@ window.__ModuleLoader__.load({
739
763
  typeof input.model === 'string' && input.model !== ''
740
764
  ? input.model
741
765
  : defaults.model,
742
- format: input.format === 'anthropic' ? 'anthropic' : 'openai',
766
+ format: input.format === 'anthropic' ? 'anthropic' : input.format === 'lmstudio' ? 'lmstudio' : 'openai',
767
+ ...((Number.isInteger(input.maxTokens) ? input.maxTokens : defaults.maxTokens) === undefined
768
+ ? {}
769
+ : { maxTokens: Number.isInteger(input.maxTokens) ? input.maxTokens : defaults.maxTokens }),
770
+ ...((['provider_default', 'none', 'low', 'medium', 'high', 'max'].includes(input.reasoningEffort)
771
+ ? input.reasoningEffort
772
+ : defaults.reasoningEffort) === undefined
773
+ ? {}
774
+ : { reasoningEffort: ['provider_default', 'none', 'low', 'medium', 'high', 'max'].includes(input.reasoningEffort)
775
+ ? input.reasoningEffort
776
+ : defaults.reasoningEffort }),
743
777
  temperature:
744
778
  typeof input.temperature === 'number' && Number.isFinite(input.temperature)
745
779
  ? input.temperature
@@ -761,6 +795,12 @@ window.__ModuleLoader__.load({
761
795
  typeof input.top_p === 'number' && Number.isFinite(input.top_p)
762
796
  ? Math.min(1, Math.max(0, input.top_p))
763
797
  : undefined
798
+ const maxTokens = Number.isInteger(input.maxTokens)
799
+ ? Math.min(32768, Math.max(256, input.maxTokens))
800
+ : defaults.maxTokens
801
+ const reasoningEffort = ['provider_default', 'none', 'low', 'medium', 'high', 'max'].includes(input.reasoningEffort)
802
+ ? input.reasoningEffort
803
+ : defaults.reasoningEffort
764
804
  return {
765
805
  enabled: input.enabled === true,
766
806
  baseURL:
@@ -771,7 +811,9 @@ window.__ModuleLoader__.load({
771
811
  typeof input.model === 'string' && input.model.trim() !== ''
772
812
  ? input.model.trim()
773
813
  : defaults.model,
774
- format: input.format === 'anthropic' ? 'anthropic' : 'openai',
814
+ format: input.format === 'anthropic' ? 'anthropic' : input.format === 'lmstudio' ? 'lmstudio' : 'openai',
815
+ ...(maxTokens === undefined ? {} : { maxTokens }),
816
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
775
817
  ...(temperature === undefined ? {} : { temperature }),
776
818
  ...(topP === undefined ? {} : { top_p: topP }),
777
819
  }
@@ -2371,6 +2413,7 @@ window.__ModuleLoader__.load({
2371
2413
  timeoutMs: 'numTimeoutMs',
2372
2414
  visionTaskTimeoutMs: 'numVisionTaskTimeoutMs',
2373
2415
  ocrTimeoutMs: 'numOcrTimeoutMs',
2416
+ ocrEngine: 'selectOcrEngine',
2374
2417
  downscaleMaxPixels: 'numDownscaleMaxPixels',
2375
2418
  cacheTtlSeconds: 'numCacheTtlSeconds',
2376
2419
  cacheMaxEntries: 'numCacheMaxEntries',
@@ -2399,6 +2442,7 @@ window.__ModuleLoader__.load({
2399
2442
  timeoutMs: 'numHintTimeoutMs',
2400
2443
  visionTaskTimeoutMs: 'numHintVisionTaskTimeoutMs',
2401
2444
  ocrTimeoutMs: 'numHintOcrTimeoutMs',
2445
+ ocrEngine: 'hintOcrEngine',
2402
2446
  downscaleMaxPixels: 'numHintDownscaleMaxPixels',
2403
2447
  cacheTtlSeconds: 'numHintCacheTtlSeconds',
2404
2448
  cacheMaxEntries: 'numHintCacheMaxEntries',
@@ -2796,6 +2840,7 @@ window.__ModuleLoader__.load({
2796
2840
  return normalizeLocalProviderDraft(value, LOCAL_PROVIDER_DEFAULTS.localLmStudio)
2797
2841
  }
2798
2842
  if (key === 'localDescribeStyle') return value === 'structured' ? 'structured' : 'plain'
2843
+ if (key === 'ocrEngine') return value === 'tesseract' || value === 'vision' ? value : 'auto'
2799
2844
  if (SELECT_KEYS.includes(key)) {
2800
2845
  if (value === 'custom') return 'standard'
2801
2846
  return value === 'fast' || value === 'standard' || value === 'deep' ? value : 'standard'
@@ -2856,6 +2901,9 @@ window.__ModuleLoader__.load({
2856
2901
  if (key === 'localDescribeStyle') {
2857
2902
  return text === 'structured' || text === 'plain' ? { value: text } : undefined
2858
2903
  }
2904
+ if (key === 'ocrEngine') {
2905
+ return text === 'auto' || text === 'tesseract' || text === 'vision' ? { value: text } : undefined
2906
+ }
2859
2907
  if (SELECT_KEYS.includes(key)) {
2860
2908
  return text === 'fast' || text === 'standard' || text === 'deep' ? { value: text } : undefined
2861
2909
  }
@@ -3354,6 +3402,26 @@ window.__ModuleLoader__.load({
3354
3402
  h('option', { value: 'anthropic' }, t('localFormatAnthropic')),
3355
3403
  ),
3356
3404
  ),
3405
+ h('div', { className: 'vr-local-row vr-local-row-pair' },
3406
+ h('label', { className: 'vr-label vr-local-label' }, t('localMaxTokens')),
3407
+ h('input', {
3408
+ className: 'vr-input', value: String(value.maxTokens ?? 4096), disabled: editBlocked,
3409
+ type: 'number', step: '256', min: '256', max: '32768',
3410
+ onChange: (event) => update({ maxTokens: Number(event.target.value) }),
3411
+ }),
3412
+ h('label', { className: 'vr-label vr-local-label' }, t('localReasoningEffort')),
3413
+ h('select', {
3414
+ className: 'vr-input vr-select', value: value.reasoningEffort, disabled: editBlocked || value.format === 'anthropic',
3415
+ onChange: (event) => update({ reasoningEffort: event.target.value }),
3416
+ },
3417
+ h('option', { value: 'provider_default' }, t('localReasoningDefault')),
3418
+ h('option', { value: 'none' }, t('localReasoningNone')),
3419
+ h('option', { value: 'low' }, 'low'),
3420
+ h('option', { value: 'medium' }, 'medium'),
3421
+ h('option', { value: 'high' }, 'high'),
3422
+ h('option', { value: 'max' }, 'max'),
3423
+ ),
3424
+ ),
3357
3425
  h('div', { className: 'vr-local-row vr-local-row-pair' },
3358
3426
  h('label', { className: 'vr-label vr-local-label' }, t('localOllamaTemperature')),
3359
3427
  h('input', {
@@ -3418,6 +3486,27 @@ window.__ModuleLoader__.load({
3418
3486
  },
3419
3487
  h('option', { value: 'openai' }, t('localFormatOpenAI')),
3420
3488
  h('option', { value: 'anthropic' }, t('localFormatAnthropic')),
3489
+ h('option', { value: 'lmstudio' }, t('localFormatLmStudio')),
3490
+ ),
3491
+ ),
3492
+ h('div', { className: 'vr-local-row vr-local-row-pair' },
3493
+ h('label', { className: 'vr-label vr-local-label' }, t('localMaxTokens')),
3494
+ h('input', {
3495
+ className: 'vr-input', value: String(value.maxTokens ?? 4096), disabled: editBlocked,
3496
+ type: 'number', step: '256', min: '256', max: '32768',
3497
+ onChange: (event) => update({ maxTokens: Number(event.target.value) }),
3498
+ }),
3499
+ h('label', { className: 'vr-label vr-local-label' }, t('localReasoningEffort')),
3500
+ h('select', {
3501
+ className: 'vr-input vr-select', value: value.reasoningEffort, disabled: editBlocked || value.format !== 'lmstudio',
3502
+ onChange: (event) => update({ reasoningEffort: event.target.value }),
3503
+ },
3504
+ h('option', { value: 'provider_default' }, t('localReasoningDefault')),
3505
+ h('option', { value: 'none' }, t('localReasoningNone')),
3506
+ h('option', { value: 'low' }, 'low'),
3507
+ h('option', { value: 'medium' }, 'medium'),
3508
+ h('option', { value: 'high' }, 'high'),
3509
+ h('option', { value: 'max' }, 'max'),
3421
3510
  ),
3422
3511
  ),
3423
3512
  h('div', { className: 'vr-local-row vr-local-row-pair' },
@@ -4123,6 +4212,11 @@ window.__ModuleLoader__.load({
4123
4212
  h('p', { className: 'vr-group-title' }, t('groupPerformance')),
4124
4213
  PERFORMANCE_TOGGLE_KEYS.map((key) => toggleField(key)),
4125
4214
  NUMBER_KEYS.map((key) => textField(key, t(LABEL_KEY[key]), t(HINT_KEY[key]), false)),
4215
+ selectField('ocrEngine', t(LABEL_KEY.ocrEngine), t(HINT_KEY.ocrEngine), [
4216
+ { value: 'auto', label: t('ocrEngineAuto') },
4217
+ { value: 'tesseract', label: t('ocrEngineTesseract') },
4218
+ { value: 'vision', label: t('ocrEngineVision') },
4219
+ ]),
4126
4220
  ),
4127
4221
  h('div', { className: 'vr-group' },
4128
4222
  h('p', { className: 'vr-group-title' }, t('groupCompatibility')),
@@ -1439,8 +1439,9 @@ export function posterizeSvgColor(data, info, palette, timeoutMs = 60000) {
1439
1439
  }
1440
1440
 
1441
1441
  /** Resolve the effective vision_ocr engine without hiding explicit user/model intent. */
1442
- export function resolveVisionOcrEngine(requestedEngine) {
1442
+ export function resolveVisionOcrEngine(requestedEngine, configuredEngine = 'auto') {
1443
1443
  if (requestedEngine === 'tesseract' || requestedEngine === 'vision') return requestedEngine
1444
+ if (configuredEngine === 'tesseract' || configuredEngine === 'vision') return configuredEngine
1444
1445
  return 'auto'
1445
1446
  }
1446
1447
 
@@ -1857,11 +1858,15 @@ export function localOllamaProvidersOf(config) {
1857
1858
  baseURL,
1858
1859
  model,
1859
1860
  apiKeyEnv: '',
1860
- maxTokens: 2048,
1861
+ maxTokens: Number.isInteger(local.maxTokens) ? local.maxTokens : 4096,
1861
1862
  // Ollama's OpenAI-compatible Chat Completions endpoint supports
1862
1863
  // reasoning_effort='none'. Vision extraction should spend the bounded
1863
1864
  // completion budget on observable answer text rather than hidden thought.
1864
- reasoningEffort: 'none',
1865
+ ...(local.reasoningEffort === 'provider_default'
1866
+ ? {}
1867
+ : { reasoningEffort: ['none', 'low', 'medium', 'high', 'max'].includes(local.reasoningEffort)
1868
+ ? local.reasoningEffort
1869
+ : 'none' }),
1865
1870
  // 仅显式选择 anthropic 格式时携带(默认 openai 路径保持字节不变)。
1866
1871
  ...(local.format === 'anthropic' ? { format: 'anthropic' } : {}),
1867
1872
  // 建议值透传:温度/top_p 只在显式配置时携带(callOpenAICompatible
@@ -1889,8 +1894,12 @@ export function localLmStudioProvidersOf(config) {
1889
1894
  baseURL,
1890
1895
  model,
1891
1896
  apiKeyEnv: '',
1892
- maxTokens: 2048,
1897
+ maxTokens: Number.isInteger(local.maxTokens) ? local.maxTokens : 4096,
1898
+ ...(local.format === 'lmstudio' && ['none', 'low', 'medium', 'high', 'max'].includes(local.reasoningEffort)
1899
+ ? { reasoningEffort: local.reasoningEffort }
1900
+ : {}),
1893
1901
  ...(local.format === 'anthropic' ? { format: 'anthropic' } : {}),
1902
+ ...(local.format === 'lmstudio' ? { format: 'lmstudio' } : {}),
1894
1903
  ...(typeof local.temperature === 'number' ? { temperature: local.temperature } : {}),
1895
1904
  ...(typeof local.top_p === 'number' ? { top_p: local.top_p } : {}),
1896
1905
  },
@@ -1917,12 +1926,108 @@ export function localProvidersOf(config) {
1917
1926
  * temperature/top_p 仅显式配置时透传(两个 transport 的显式可选参数,
1918
1927
  * 现有调用不传,wire 保持 main 原样)。
1919
1928
  */
1929
+ function lmStudioNativeContent(messages) {
1930
+ const system = []
1931
+ const input = []
1932
+ for (const message of messages ?? []) {
1933
+ if (!message) continue
1934
+ const blocks = Array.isArray(message.content)
1935
+ ? message.content
1936
+ : typeof message.content === 'string'
1937
+ ? [{ type: 'text', text: message.content }]
1938
+ : []
1939
+ if (message.role === 'system') {
1940
+ for (const block of blocks) {
1941
+ if (block?.type === 'text' && typeof block.text === 'string' && block.text.trim() !== '') {
1942
+ system.push(block.text)
1943
+ }
1944
+ }
1945
+ continue
1946
+ }
1947
+ for (const block of blocks) {
1948
+ if (block?.type === 'text' && typeof block.text === 'string') {
1949
+ input.push({ type: 'text', content: block.text })
1950
+ } else if (block?.type === 'image_url' && typeof block.image_url?.url === 'string') {
1951
+ input.push({ type: 'image', data_url: block.image_url.url })
1952
+ }
1953
+ }
1954
+ }
1955
+ return { systemPrompt: system.join('\n').trim(), input }
1956
+ }
1957
+
1958
+ async function callLmStudioNative(provider, messages, options = {}) {
1959
+ const normalizedBaseURL = stripTrailingSlashes(String(provider.baseURL ?? 'http://localhost:1234/v1'))
1960
+ const apiRoot = normalizedBaseURL.endsWith('/v1') ? normalizedBaseURL.slice(0, -3) : normalizedBaseURL
1961
+ const { systemPrompt, input } = lmStudioNativeContent(messages)
1962
+ const reasoningEffort = options.reasoningEffort ?? provider.reasoningEffort
1963
+ const reasoning = reasoningEffort === 'none'
1964
+ ? 'off'
1965
+ : ['low', 'medium', 'high'].includes(reasoningEffort)
1966
+ ? reasoningEffort
1967
+ : reasoningEffort === 'max'
1968
+ ? 'high'
1969
+ : undefined
1970
+ const body = {
1971
+ model: provider.model,
1972
+ input,
1973
+ store: false,
1974
+ max_output_tokens: options.maxTokens ?? provider.maxTokens ?? 4096,
1975
+ ...(systemPrompt === '' ? {} : { system_prompt: systemPrompt }),
1976
+ ...(reasoning === undefined ? {} : { reasoning }),
1977
+ ...(typeof options.temperature === 'number' ? { temperature: options.temperature } : {}),
1978
+ ...(typeof options.top_p === 'number' ? { top_p: options.top_p } : {}),
1979
+ }
1980
+ const response = await fetch(`${apiRoot}/api/v1/chat`, {
1981
+ method: 'POST',
1982
+ headers: { 'content-type': 'application/json' },
1983
+ body: JSON.stringify(body),
1984
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
1985
+ })
1986
+ if (!response.ok) {
1987
+ const detail = (await readResponseTextBounded(
1988
+ response,
1989
+ ERROR_RESPONSE_MAX_BYTES,
1990
+ { label: 'LM Studio native error response' },
1991
+ ).catch(() => '')).slice(0, 300)
1992
+ const error = new Error(`http provider "${provider.name}": ${response.status} ${detail}`)
1993
+ error.status = response.status
1994
+ error.code = kindForHttpStatus(response.status) ?? 'HTTP_PROVIDER_FAILED'
1995
+ throw error
1996
+ }
1997
+ const data = await readResponseJsonBounded(
1998
+ response,
1999
+ MODEL_RESPONSE_MAX_BYTES,
2000
+ { label: 'LM Studio native response' },
2001
+ )
2002
+ const text = Array.isArray(data?.output)
2003
+ ? data.output
2004
+ .filter((item) => item?.type === 'message' && typeof item.content === 'string')
2005
+ .map((item) => item.content)
2006
+ .join('\n')
2007
+ .trim()
2008
+ : ''
2009
+ if (text === '' && Number(data?.stats?.total_output_tokens) >= body.max_output_tokens) {
2010
+ const error = new Error(`http provider "${provider.name}": empty answer after exhausting the completion budget`)
2011
+ error.code = 'VISION_EMPTY_RESPONSE'
2012
+ throw error
2013
+ }
2014
+ return text
2015
+ }
2016
+
1920
2017
  export async function callLocalBackend(provider, messages, options = {}) {
1921
2018
  const maxTokens = options.maxTokens ?? provider.maxTokens ?? 2048
1922
2019
  const sampling = {
1923
2020
  ...(typeof provider.temperature === 'number' ? { temperature: provider.temperature } : {}),
1924
2021
  ...(typeof provider.top_p === 'number' ? { top_p: provider.top_p } : {}),
1925
2022
  }
2023
+ if (provider.format === 'lmstudio') {
2024
+ return callLmStudioNative(provider, messages, {
2025
+ maxTokens,
2026
+ signal: options.signal,
2027
+ ...(typeof provider.reasoningEffort === 'string' ? { reasoningEffort: provider.reasoningEffort } : {}),
2028
+ ...sampling,
2029
+ })
2030
+ }
1926
2031
  if (provider.format === 'anthropic') {
1927
2032
  const system = []
1928
2033
  const wire = []
@@ -2182,11 +2287,16 @@ export async function callOpenAICompatible(provider, messages, options = {}) {
2182
2287
  MODEL_RESPONSE_MAX_BYTES,
2183
2288
  { label: `http provider \"${provider.name}\" response` },
2184
2289
  )
2185
- const content = data && data.choices && data.choices[0] && data.choices[0].message
2186
- ? data.choices[0].message.content
2187
- : undefined
2290
+ const choice = data && data.choices && data.choices[0] ? data.choices[0] : undefined
2291
+ const content = choice && choice.message ? choice.message.content : undefined
2188
2292
  if (typeof content !== 'string') throw new Error(`http provider "${provider.name}": unexpected response shape`)
2189
- return content.trim()
2293
+ const text = content.trim()
2294
+ if (text === '' && choice?.finish_reason === 'length' && (provider.name === 'local-ollama' || provider.name === 'local-lmstudio')) {
2295
+ const error = new Error(`http provider "${provider.name}": empty answer after exhausting the completion budget`)
2296
+ error.code = 'VISION_EMPTY_RESPONSE'
2297
+ throw error
2298
+ }
2299
+ return text
2190
2300
  }
2191
2301
 
2192
2302
  /**
@@ -43,6 +43,7 @@ export function inspectDshHostCapabilities(ctx) {
43
43
  const llm = serviceOf(ctx, 'llm')
44
44
  const jobs = serviceOf(ctx, 'jobs')
45
45
  const settings = serviceOf(ctx, 'settings')
46
+ const configEditor = serviceOf(ctx, 'configEditor')
46
47
  const tools = serviceOf(ctx, 'tools')
47
48
 
48
49
  let maxImageDimension = false
@@ -57,6 +58,10 @@ export function inspectDshHostCapabilities(ctx) {
57
58
  const prepareCall = hasFunction(llm, 'prepareCall')
58
59
  const toolRegistration = hasFunction(tools, 'register') || hasFunction(tools, 'registerTool')
59
60
  const toolExecution = hasFunction(tools, 'call') || hasFunction(tools, 'execute') || hasFunction(tools, 'invoke')
61
+ const settingsLiveNamespace = hasFunction(settings, 'register')
62
+ || (hasFunction(settings, 'describe')
63
+ && hasFunction(configEditor, 'configuration')
64
+ && hasFunction(configEditor, 'edit'))
60
65
 
61
66
  return Object.freeze({
62
67
  batchAttachments: hasFunction(attachments, 'saveImages'),
@@ -70,10 +75,11 @@ export function inspectDshHostCapabilities(ctx) {
70
75
  // No stable read-only client-surface capability flag exists across the
71
76
  // supported Host window yet.
72
77
  surfaceReplacement: UNKNOWN,
73
- // DSH SettingsProvider.register() is the public live-namespace seam; get()
74
- // and watch() belong to the returned SettingsScope rather than the service.
75
- // Detect the stable service method and do not manufacture a registration.
76
- settingsLiveNamespace: hasFunction(settings, 'register'),
78
+ // Older Hosts expose SettingsProvider.register() directly. DSH 0.1.7's
79
+ // reviewed replacement is SettingsForms.describe() plus ConfigEditor
80
+ // configuration()/edit(), which DVR adapts into the same live namespace.
81
+ // Probe only readable method presence; never manufacture a registration or write.
82
+ settingsLiveNamespace,
77
83
  // Browser exposure is a presentation capability and is not safely implied
78
84
  // by the Host settings service alone.
79
85
  settingsWebExposure: UNKNOWN,
@@ -1,12 +1,12 @@
1
1
  export const DSH_SUPPORT_WINDOW = Object.freeze({
2
- dvrTrain: '2.1.x',
2
+ dvrTrain: '2.2.x',
3
3
  minimum: '0.1.0-rc.8',
4
- currentStable: '0.1.5-rc.2',
4
+ currentStable: '0.1.5-rc.3',
5
5
  })
6
6
 
7
7
  export const DSH_VERIFICATION_EVIDENCE = Object.freeze({
8
- exactStable: '0.1.5-rc.2',
9
- exactPreview: '0.1.6-alpha.1',
8
+ exactStable: '0.1.5-rc.3',
9
+ exactPreview: '0.1.7-rc.2',
10
10
  stableCanaryDistTag: 'latest',
11
11
  previewCanaryDistTag: 'alpha',
12
12
  })
@@ -40,6 +40,7 @@ export const REMOTE_SETTINGS_READABLE_FIELDS = Object.freeze([
40
40
  'visionTaskTimeoutMs',
41
41
  'visionTurnBudgetMs',
42
42
  'ocrTimeoutMs',
43
+ 'ocrEngine',
43
44
  'freeFallback',
44
45
  'freeCloudFirst',
45
46
  'autoWrapProviders',
@@ -183,6 +183,10 @@ function translateLegacyRuntimeText(value, i18n) {
183
183
  'In structured mode, vision_ocr with an omitted engine or engine=auto uses vision-model OCR (engine=vision) directly instead of accepting the first non-empty local Tesseract result; explicit engine=tesseract or engine=vision is always preserved.',
184
184
  'For vision_ocr, engine=auto always tries local Tesseract first and falls back to the vision model only when local OCR fails or returns no text; structured mode does not change this order. Use explicit engine=vision to force vision-model OCR.',
185
185
  )
186
+ .replace(
187
+ '不要默认把 OCR 当第二步;仅在需要逐字保真时用 vision_ocr,并把结果当作需要结合上下文验证的证据。UI/截图语义通常用 vision_describe 或 vision_detect,精确定位用 vision_ground。vision_ocr 未显式指定 engine 时遵循设置中的 OCR 默认引擎;单次显式 engine=tesseract/vision 始终优先。完成至少 1 次后续证据调用后,证据充分就直接作答,不要为了流程继续调用。',
188
+ 'Do not default to OCR as the second step. Use vision_ocr only for verbatim evidence. If it returns uncertain:true, verify the ambiguous text against context when possible; if uncertain:false and the text is sufficient, answer without redundant verification. For UI/screenshot semantics use vision_describe or vision_detect; use vision_ground for precise localization. When vision_ocr has no explicit engine, it follows the configured Default OCR engine; per-call engine=tesseract/vision always wins. After at least 1 follow-up evidence call, answer once the evidence is sufficient instead of calling more tools just for the workflow.',
189
+ )
186
190
  .replace(
187
191
  '不要默认把 OCR 当第二步;仅在需要逐字保真时用 vision_ocr,并把结果当作需要结合上下文验证的证据。UI/截图语义通常用 vision_describe 或 vision_detect,精确定位用 vision_ground。vision_ocr 的 engine=auto 始终先尝试本地 Tesseract,失败或空结果时再回退视觉模型;结构化模式不会改变这一顺序。完成至少 1 次后续证据调用后,证据充分就直接作答,不要为了流程继续调用。',
188
192
  'Do not default to OCR as the second step. Use vision_ocr only for verbatim evidence. If it returns uncertain:true, verify the ambiguous text against context when possible; if uncertain:false and the text is sufficient, answer without redundant verification. For UI/screenshot semantics use vision_describe or vision_detect; use vision_ground for precise localization. For vision_ocr, engine=auto always tries local Tesseract first and falls back to the vision model only when local OCR fails or returns no text; structured mode does not change this order. After at least 1 follow-up evidence call, answer once the evidence is sufficient instead of calling more tools just for the workflow.',
@@ -6,7 +6,8 @@ function nonNegativeSafeInteger(value) {
6
6
  * Build the Host-owned Session surface replacement intent for one exact node.
7
7
  *
8
8
  * DSH Session format v3 renamed replacement endpoints from start/end to
9
- * startSeq/endSeq. The field change belongs to the Session format contract,
9
+ * startSeq/endSeq, and reviewed format v4 keeps that replacement shape. The
10
+ * field change belongs to the Session format contract,
10
11
  * not to a DSH package-version heuristic, so dispatch on session.header.version
11
12
  * and refuse unknown future formats instead of guessing forward compatibility.
12
13
  *
@@ -29,7 +30,7 @@ export function sessionSurfaceReplacementIntent(session, seq) {
29
30
  }
30
31
  }
31
32
 
32
- if (version === 3) {
33
+ if (version === 3 || version === 4) {
33
34
  return {
34
35
  surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq },
35
36
  sourceEventSeqs: [seq],
@@ -91,6 +91,7 @@ export function createSessionVisionIndex({
91
91
  const repairReadWarnings = new WeakMap()
92
92
  const repairFeedOverflowWarnings = new WeakSet()
93
93
  const repairFeedObserverWarnings = new WeakMap()
94
+ const surfaceFeedBackfills = new WeakMap()
94
95
  const attachmentRecoveryWarnings = new WeakMap()
95
96
  let surfaceEventFeedActive = false
96
97
 
@@ -357,6 +358,54 @@ export function createSessionVisionIndex({
357
358
  surfaceEventFeedActive = true
358
359
  }
359
360
 
361
+ const backfillSurfaceEventFeed = async (session) => {
362
+ let events
363
+ if (typeof readSessionLog === 'function') {
364
+ let result
365
+ try {
366
+ result = await readSessionLog(session)
367
+ } catch (error) {
368
+ warnRepairReadFailure(session, surfaceNodes(session)?.[0] ?? 0, error)
369
+ return
370
+ }
371
+ if (result?.supported === true) {
372
+ if (!Array.isArray(result.events)) {
373
+ warnRepairReadFailure(session, surfaceNodes(session)?.[0] ?? 0, new Error('Session log reader returned no event log'))
374
+ return
375
+ }
376
+ events = result.events
377
+ } else if (result?.supported !== false) {
378
+ warnRepairReadFailure(session, surfaceNodes(session)?.[0] ?? 0, new Error('Session log reader returned an invalid capability result'))
379
+ return
380
+ }
381
+ }
382
+
383
+ // Event-feed activation can happen after a live Session already contains
384
+ // committed events (enable/re-enable/HMR). Backfill exactly once from one
385
+ // snapshot, then return to the O(new events) feed path. Older partial Hosts
386
+ // without SessionQuery retain the Session-local compatibility snapshot.
387
+ if (events === undefined) events = legacySessionEvents(session)
388
+ if (!Array.isArray(events)) return
389
+
390
+ const nodes = surfaceNodes(session)
391
+ if (!nodes || nodes.length === 0) return
392
+ for (const seq of nodes) {
393
+ const event = events[seq]
394
+ if (!event || typeof event !== 'object' || (event.seq !== undefined && event.seq !== seq)) continue
395
+ recordSessionEvent(session, event)
396
+ }
397
+ }
398
+
399
+ const ensureSurfaceEventFeedBackfill = async (session) => {
400
+ if (!surfaceEventFeedActive || !session) return
401
+ let task = surfaceFeedBackfills.get(session)
402
+ if (!task) {
403
+ task = backfillSurfaceEventFeed(session)
404
+ surfaceFeedBackfills.set(session, task)
405
+ }
406
+ await task
407
+ }
408
+
360
409
  const consumePendingRepairEvents = (pendingBySession, session) => {
361
410
  const pending = pendingBySession.get(session)
362
411
  if (!pending || pending.size === 0) return []
@@ -532,6 +581,7 @@ export function createSessionVisionIndex({
532
581
  store.recordAttachments(session, found.attachments)
533
582
  }
534
583
  }
584
+ await ensureSurfaceEventFeedBackfill(session)
535
585
  await repairToolResultSurface(session)
536
586
  await repairGuardStopSurface(session)
537
587
  return decision
@@ -73,7 +73,7 @@ export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
73
73
  function makeComponent(React,helpers){
74
74
  helpers=helpers&&typeof helpers==='object'?helpers:{};
75
75
  var commitPlan=typeof helpers.commitSettingsPlan==='function'?function(scope,plan,drafts){return helpers.commitSettingsPlan(scope,plan,drafts);}:function(scope,plan){return fallbackCommit(scope,plan);};
76
- var parseLocal=typeof helpers.parseLocalProviderDraft==='function'?helpers.parseLocalProviderDraft:function(value,defaults){var input=obj(value),temperature=typeof input.temperature==='number'&&Number.isFinite(input.temperature)?Math.min(2,Math.max(0,input.temperature)):undefined,topP=typeof input.top_p==='number'&&Number.isFinite(input.top_p)?Math.min(1,Math.max(0,input.top_p)):undefined;return Object.assign({enabled:input.enabled===true,baseURL:typeof input.baseURL==='string'&&input.baseURL.trim()?input.baseURL.trim():defaults.baseURL,model:typeof input.model==='string'&&input.model.trim()?input.model.trim():defaults.model,format:input.format==='anthropic'?'anthropic':'openai'},temperature===undefined?{}:{temperature:temperature},topP===undefined?{}:{top_p:topP});};
76
+ var parseLocal=typeof helpers.parseLocalProviderDraft==='function'?helpers.parseLocalProviderDraft:function(value,defaults){var input=obj(value),temperature=typeof input.temperature==='number'&&Number.isFinite(input.temperature)?Math.min(2,Math.max(0,input.temperature)):undefined,topP=typeof input.top_p==='number'&&Number.isFinite(input.top_p)?Math.min(1,Math.max(0,input.top_p)):undefined,maxTokens=Number.isInteger(input.maxTokens)?Math.min(32768,Math.max(256,input.maxTokens)):defaults.maxTokens,reasoningEffort=['provider_default','none','low','medium','high','max'].includes(input.reasoningEffort)?input.reasoningEffort:defaults.reasoningEffort;return Object.assign({enabled:input.enabled===true,baseURL:typeof input.baseURL==='string'&&input.baseURL.trim()?input.baseURL.trim():defaults.baseURL,model:typeof input.model==='string'&&input.model.trim()?input.model.trim():defaults.model,format:input.format==='anthropic'?'anthropic':input.format==='lmstudio'?'lmstudio':'openai',maxTokens:maxTokens,reasoningEffort:reasoningEffort},temperature===undefined?{}:{temperature:temperature},topP===undefined?{}:{top_p:topP});};
77
77
  return function VisionRouterSettingsIA(props){
78
78
  var scope=props&&props.scope;
79
79
  var subscribe=React.useMemo(function(){return scope&&typeof scope.subscribe==='function'?scope.subscribe.bind(scope):function(){return function(){};};},[scope]);
@@ -142,9 +142,9 @@ export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
142
142
  function draftRun(key,raw){
143
143
  if(boolKeys.has(key)){var next=bool(raw);return {value:key==='allowRemoteSettings'&&snapshot.mode==='host'?(next?'true':'false'):next};}
144
144
  if(numberMeta[key])return numberRun(key,raw);
145
- if(key==='visionDepth')return raw==='fast'||raw==='standard'||raw==='deep'?{value:raw}:undefined;
145
+ if(key==='visionDepth')return raw==='fast'||raw==='standard'||raw==='deep'?{value:raw}:undefined;if(key==='ocrEngine')return raw==='auto'||raw==='tesseract'||raw==='vision'?{value:raw}:undefined;
146
146
  if(key==='textProvider'){var pair=obj(raw),p=String(pair.provider||'').trim(),m=String(pair.model||'').trim();if(!p&&!m)return {clear:true};return p&&m?{value:{provider:p,model:m}}:undefined;}
147
- if(key==='localOllama'||key==='localLmStudio'){var defaults=key==='localOllama'?{baseURL:'http://127.0.0.1:11434/v1',model:'qwen2.5vl'}:{baseURL:'http://localhost:1234/v1',model:''};var parsed=parseLocal(raw,defaults);if(key==='localLmStudio'&&parsed.enabled===true&&String(parsed.model||'').trim()==='')return undefined;return {value:parsed};}
147
+ if(key==='localOllama'||key==='localLmStudio'){var defaults=key==='localOllama'?{baseURL:'http://127.0.0.1:11434/v1',model:'qwen2.5vl',maxTokens:4096,reasoningEffort:'none'}:{baseURL:'http://localhost:1234/v1',model:'',maxTokens:4096,reasoningEffort:'none'};var parsed=parseLocal(raw,defaults);if(key==='localLmStudio'&&parsed.enabled===true&&String(parsed.model||'').trim()==='')return undefined;return {value:parsed};}
148
148
  if(key==='proxyHosts'||key==='extraVisionModels'){var list=arr(raw).map(function(x){return String(x||'').trim();}).filter(Boolean);return list.length?{value:list}:{clear:true};}
149
149
  if(key==='proxy'||key==='wrapperRoute'||key==='chainRoute'){var text=String(raw||'').trim();return text?{value:text}:{clear:true};}
150
150
  return {value:raw};
@@ -254,11 +254,11 @@ export const SETTINGS_IA_CLIENT_PRELUDE = String.raw`(function(){
254
254
  ])
255
255
  );
256
256
  }
257
- function localProviderCard(key,label,defaults,which){var current=Object.assign({},defaults,obj(value(key,{}))),opened=which==='ollama'?open.ollama:open.lmstudio;function update(patch){setValue(key,Object.assign({},current,patch));}function sample(name,min,max){return h('div',{className:'vr-field'},h('span',{className:'vr-label'},name),h('input',{className:'vr-input',type:'number',step:'0.1',min:min,max:max,value:current[name]===undefined?'':current[name],disabled:!writable||saving,onChange:function(event){var patch={};patch[name]=event.target.value===''?undefined:Number(event.target.value);update(patch);}}));}return card([h('div',{className:'vr-ia-backend-head',key:'head'},h('div',null,h('div',{className:'vr-ia-field-head'},h('strong',null,label),override(key)),h('p',{className:'vr-hint'},current.enabled?tx('已启用','Enabled'):tx('未启用','Disabled'))),h('input',{type:'checkbox',checked:current.enabled===true,disabled:!writable||saving,onChange:function(event){update({enabled:event.target.checked});}})),h('div',{className:'vr-field',key:'model'},h('span',{className:'vr-label'},tx('模型','Model')),h('input',{className:'vr-input',value:String(current.model||''),disabled:!writable||saving,onChange:function(event){update({model:event.target.value});}}),key==='localLmStudio'&&invalidKeys.includes(key)?h('p',{className:'vr-failed'},tx('启用 LM Studio 时必须填写真实模型标识。','A real model id is required when LM Studio is enabled.')):null),h('div',{className:'vr-field',key:'url'},h('span',{className:'vr-label'},tx('服务地址','Service URL')),h('input',{className:'vr-input',value:String(current.baseURL||''),disabled:!writable||saving,onChange:function(event){update({baseURL:event.target.value});}})),h('button',{type:'button',className:'vr-btn',onClick:function(){setOpen(Object.assign({},open,which==='ollama'?{ollama:!opened}:{lmstudio:!opened}));}},opened?tx('收起高级连接设置','Hide advanced connection settings'):tx('高级连接设置','Advanced connection settings')),opened?h('div',{className:'vr-ia-subgrid',key:'advanced'},h('div',{className:'vr-field'},h('span',{className:'vr-label'},tx('请求协议','Request protocol')),h('select',{className:'vr-input',value:current.format==='anthropic'?'anthropic':'openai',disabled:!writable||saving,onChange:function(event){update({format:event.target.value});}},h('option',{value:'openai'},'OpenAI'),h('option',{value:'anthropic'},'Anthropic'))),sample('temperature',0,2),sample('top_p',0,1)):null]);}
258
- function localPageContent(){if(!local)return h(React.Fragment,null,title(tx('本地与设备','Local & device')),card([h('p',{className:'vr-hint',key:'remote'},tx('本地视觉后端和桌面截图只能在运行 DSH 的机器上配置。','Local vision backends and desktop capture can only be configured on the DSH machine.'))]));return h(React.Fragment,null,title(tx('本地与设备','Local & device'),tx('本地运行视觉模型可减少 API 费用和图片上传。','Run vision locally to reduce API cost and image uploads.')),localProviderCard('localOllama','Ollama',{enabled:false,baseURL:'http://127.0.0.1:11434/v1',model:'qwen2.5vl',format:'openai'},'ollama'),localProviderCard('localLmStudio','LM Studio',{enabled:false,baseURL:'http://localhost:1234/v1',model:'',format:'openai'},'lmstudio'),card([toggle('desktopScreenshot',tx('允许 Agent 读取桌面截图','Allow the agent to capture the desktop'),tx('这是独立的隐私权限;macOS 上保存为开启后会立即触发屏幕录制权限检查。','This is a separate privacy permission; on macOS saving it enabled immediately triggers the screen-recording permission check.'))]));}
257
+ function localProviderCard(key,label,defaults,which){var current=Object.assign({},defaults,obj(value(key,{}))),opened=which==='ollama'?open.ollama:open.lmstudio;function update(patch){setValue(key,Object.assign({},current,patch));}function sample(name,min,max){return h('div',{className:'vr-field'},h('span',{className:'vr-label'},name),h('input',{className:'vr-input',type:'number',step:'0.1',min:min,max:max,value:current[name]===undefined?'':current[name],disabled:!writable||saving,onChange:function(event){var patch={};patch[name]=event.target.value===''?undefined:Number(event.target.value);update(patch);}}));}return card([h('div',{className:'vr-ia-backend-head',key:'head'},h('div',null,h('div',{className:'vr-ia-field-head'},h('strong',null,label),override(key)),h('p',{className:'vr-hint'},current.enabled?tx('已启用','Enabled'):tx('未启用','Disabled'))),h('input',{type:'checkbox',checked:current.enabled===true,disabled:!writable||saving,onChange:function(event){update({enabled:event.target.checked});}})),h('div',{className:'vr-field',key:'model'},h('span',{className:'vr-label'},tx('模型','Model')),h('input',{className:'vr-input',value:String(current.model||''),disabled:!writable||saving,onChange:function(event){update({model:event.target.value});}}),key==='localLmStudio'&&invalidKeys.includes(key)?h('p',{className:'vr-failed'},tx('启用 LM Studio 时必须填写真实模型标识。','A real model id is required when LM Studio is enabled.')):null),h('div',{className:'vr-field',key:'url'},h('span',{className:'vr-label'},tx('服务地址','Service URL')),h('input',{className:'vr-input',value:String(current.baseURL||''),disabled:!writable||saving,onChange:function(event){update({baseURL:event.target.value});}})),h('button',{type:'button',className:'vr-btn',onClick:function(){setOpen(Object.assign({},open,which==='ollama'?{ollama:!opened}:{lmstudio:!opened}));}},opened?tx('收起高级连接设置','Hide advanced connection settings'):tx('高级连接设置','Advanced connection settings')),opened?h('div',{className:'vr-ia-subgrid',key:'advanced'},h('div',{className:'vr-field'},h('span',{className:'vr-label'},tx('请求协议','Request protocol')),h('select',{className:'vr-input',value:current.format==='anthropic'?'anthropic':current.format==='lmstudio'?'lmstudio':'openai',disabled:!writable||saving,onChange:function(event){update({format:event.target.value});}},h('option',{value:'openai'},'OpenAI'),h('option',{value:'anthropic'},'Anthropic'),which==='lmstudio'?h('option',{value:'lmstudio'},tx('LM Studio 原生','LM Studio native')):null)),h('div',{className:'vr-field'},h('span',{className:'vr-label'},tx('最大输出 token','Max output tokens')),h('input',{className:'vr-input',type:'number',step:'256',min:'256',max:'32768',value:String(current.maxTokens||4096),disabled:!writable||saving,onChange:function(event){update({maxTokens:Number(event.target.value)});}})),h('div',{className:'vr-field'},h('span',{className:'vr-label'},tx('推理强度','Reasoning effort')),h('select',{className:'vr-input',value:String(current.reasoningEffort||defaults.reasoningEffort||'provider_default'),disabled:!writable||saving||(which==='lmstudio'?current.format!=='lmstudio':current.format==='anthropic'),onChange:function(event){update({reasoningEffort:event.target.value});}},h('option',{value:'provider_default'},tx('服务端默认','Provider default')),h('option',{value:'none'},tx('关闭推理','Disable reasoning')),h('option',{value:'low'},'low'),h('option',{value:'medium'},'medium'),h('option',{value:'high'},'high'),h('option',{value:'max'},'max'))),sample('temperature',0,2),sample('top_p',0,1)):null]);}
258
+ function localPageContent(){if(!local)return h(React.Fragment,null,title(tx('本地与设备','Local & device')),card([h('p',{className:'vr-hint',key:'remote'},tx('本地视觉后端和桌面截图只能在运行 DSH 的机器上配置。','Local vision backends and desktop capture can only be configured on the DSH machine.'))]));return h(React.Fragment,null,title(tx('本地与设备','Local & device'),tx('本地运行视觉模型可减少 API 费用和图片上传。','Run vision locally to reduce API cost and image uploads.')),localProviderCard('localOllama','Ollama',{enabled:false,baseURL:'http://127.0.0.1:11434/v1',model:'qwen2.5vl',format:'openai',maxTokens:4096,reasoningEffort:'none'},'ollama'),localProviderCard('localLmStudio','LM Studio',{enabled:false,baseURL:'http://localhost:1234/v1',model:'',format:'openai',maxTokens:4096,reasoningEffort:'none'},'lmstudio'),card([toggle('desktopScreenshot',tx('允许 Agent 读取桌面截图','Allow the agent to capture the desktop'),tx('这是独立的隐私权限;macOS 上保存为开启后会立即触发屏幕录制权限检查。','This is a separate privacy permission; on macOS saving it enabled immediately triggers the screen-recording permission check.'))]));}
259
259
  function wrappersEditor(){var rows=wrapperRows();return h('div',{className:'vr-field'},fieldHead('wrappedProviders',tx('哪些聊天模型可以开启识图','Which chat models can use Vision mode')),h('p',{className:'vr-hint'},tx('通常无需修改;模型留空表示整个 Provider。','Usually leave this alone; an empty model means the whole provider.')),rows.map(function(row,index){return modelRow(row,index,rows,setWrapperDraft,true);}),h('button',{type:'button',className:'vr-btn',disabled:!writable||saving,onClick:function(){setWrapperDraft(rows.concat([{provider:'',model:''}]))}},tx('+ 添加范围','+ Add scope')));}
260
260
  function textProviderEditor(){var current=Object.assign({provider:'',model:''},obj(value('textProvider',{}))),ready=groups.length>0;return h('div',{className:'vr-field'},fieldHead('textProvider',tx('文字回退模型','Text fallback model')),h('div',{className:'vr-chain-row'},ready?h('select',{className:'vr-input',value:current.provider||'',disabled:!writable||saving,onChange:function(event){setValue('textProvider',{provider:event.target.value,model:''});}},providerOptions(current.provider||'')):h('input',{className:'vr-input',value:current.provider||'',disabled:!writable||saving,onChange:function(event){setValue('textProvider',{provider:event.target.value,model:current.model||''});}}),ready?h('select',{className:'vr-input',value:current.model||'',disabled:!writable||saving||!current.provider,onChange:function(event){setValue('textProvider',{provider:current.provider||'',model:event.target.value});}},modelOptions(current.provider||'',current.model||'',false)):h('input',{className:'vr-input',value:current.model||'',disabled:!writable||saving,onChange:function(event){setValue('textProvider',{provider:current.provider||'',model:event.target.value});}})),invalidKeys.includes('textProvider')?h('p',{className:'vr-failed'},tx('Provider 和 model 必须同时填写,或同时留空恢复默认。','Provider and model must both be filled, or both left empty to restore the default.')):null);}
261
- function advancedPage(){var routing=toggleValue('routing',false),turnBudget=Number(value('visionTurnBudgetMs',0))||0,customBudget=turnBudget>0;return h(React.Fragment,null,title(tx('高级','Advanced'),tx('这些默认值对大多数用户已经合适。','Defaults are suitable for most users.')),card([h('h4',{className:'vr-ia-subtitle',key:'p'},tx('性能与稳定性','Performance & stability')),toggle('downscale',tx('自动缩放','Auto downscale')),numberField('downscaleMaxPixels',tx('图片像素上限','Image pixel limit'),null,1000,100000000),toggle('cache',tx('识图缓存','Vision answer cache')),numberField('cacheTtlSeconds',tx('缓存有效期(秒)','Cache TTL (seconds)'),null,0,31536000),numberField('cacheMaxEntries',tx('最大缓存数量','Maximum cached answers'),null,1,100000),h('h4',{className:'vr-ia-subtitle',key:'t'},tx('超时','Timeouts')),numberField('timeoutMs',tx('单次模型请求','Single model request'),tx('毫秒。','Milliseconds.'),1000,600000),numberField('visionTaskTimeoutMs',tx('单个视觉任务','Single visual task'),tx('包含该任务内部的重试和备用模型;不是每个后端各自一份。','Includes retries and fallbacks inside the task; it is not a fresh budget per backend.'),1000,180000),numberField('ocrTimeoutMs',tx('OCR 任务','OCR task'),null,1000,120000),h('div',{className:'vr-field',key:'budget'},fieldHead('visionTurnBudgetMs',tx('整轮视觉工具上限','Whole-turn vision-tool limit')),h('select',{className:'vr-input',value:customBudget?'custom':'unlimited',disabled:!writable||saving,onChange:function(event){setValue('visionTurnBudgetMs',event.target.value==='unlimited'?0:(turnBudget>0?turnBudget:180000));}},h('option',{value:'unlimited'},tx('不限制(推荐)','Unlimited (recommended)')),h('option',{value:'custom'},tx('自定义','Custom'))),customBudget?numberField('visionTurnBudgetMs',tx('上限(毫秒)','Limit (ms)'),null,10000,600000):null)]),card([h('h4',{className:'vr-ia-subtitle',key:'cost'},tx('模型顺序与成本','Model order & cost')),toggle('freeCloudFirst',tx('免费云模型优先','Try free cloud models first'))]),card([h('h4',{className:'vr-ia-subtitle',key:'scope'},tx('识图模式范围','Vision mode scope')),toggle('autoWrapProviders',tx('自动允许已启用模型使用识图','Automatically allow enabled models to use Vision mode')),wrappersEditor()]),local?card([h('h4',{className:'vr-ia-subtitle',key:'network'},tx('网络与远程','Network & remote')),h('p',{className:'vr-hint',key:'network-authority'},tx('默认沿用 DSH/Host 的网络出口;只有需要让视觉请求走不同出口时才填写下方覆盖。','By default Vision Router follows the DSH/Host network path. Use the override below only when vision requests need a different egress route.')),toggle('allowRemoteSettings',tx('允许可信 Host 远程修改设置','Allow trusted-host remote settings'),tx('默认关闭;trustedHosts 不是身份认证。','Off by default; trustedHosts is not authentication.')),textField('proxy',tx('视觉专用代理覆盖','Vision-only proxy override'),tx('留空即沿用 DSH/Host;支持 http(s)://、socks5://,并兼容旧 socks5h://。','Leave empty to follow DSH/Host; supports http(s):// and socks5://, with legacy socks5h:// compatibility.')),textareaArray('proxyHosts',tx('视觉代理覆盖域名','Vision proxy override hosts'),tx('仅在上方覆盖非空时生效;每行一个,其余请求继续沿用 DSH/Host。','Used only when the override above is non-empty; one host per line. All other requests keep the DSH/Host path.'))]):null,card([h('h4',{className:'vr-ia-subtitle',key:'compat'},tx('兼容模式','Compatibility')),toggle('rewriteImages',tx('保护纯文本模型','Protect text-only models')),toggle('routing',tx('整轮视觉路由(旧工作流)','Whole-turn vision routing (legacy workflow)')),routing?toggle('reverseRouting',tx('纯文字消息继续使用聊天模型','Keep text-only messages on the chat model')):null,routing?textProviderEditor():null]),card([h('button',{type:'button',className:'vr-btn',key:'dev',onClick:function(){setOpen(Object.assign({},open,{developer:!open.developer}));}},open.developer?tx('隐藏开发者设置','Hide developer settings'):tx('显示开发者设置','Show developer settings')),open.developer?h('div',{className:'vr-ia-dev',key:'body'},toggle('progressiveTools',tx('渐进式工具暴露','Progressive tool exposure')),h('p',{className:'vr-hint',key:'progressive-restart'},tx('保存后需重启 DSH 才生效。','Restart DSH after saving for this change to take effect.')),local?toggle('stealth','Stealth'):null,local?textField('wrapperRoute',tx('包装路由名','Wrapper route name')):null,local?textField('chainRoute',tx('视觉链路由名','Vision chain route name')):null,textareaArray('extraVisionModels',tx('额外视觉能力标记','Extra vision capability labels'),tx('只有诊断发现模型未声明图片能力时才需要。','Needed only when diagnostics show missing image-capability metadata.'))):null]));}
261
+ function advancedPage(){var routing=toggleValue('routing',false),turnBudget=Number(value('visionTurnBudgetMs',0))||0,customBudget=turnBudget>0;return h(React.Fragment,null,title(tx('高级','Advanced'),tx('这些默认值对大多数用户已经合适。','Defaults are suitable for most users.')),card([h('h4',{className:'vr-ia-subtitle',key:'p'},tx('性能与稳定性','Performance & stability')),toggle('downscale',tx('自动缩放','Auto downscale')),numberField('downscaleMaxPixels',tx('图片像素上限','Image pixel limit'),null,1000,100000000),toggle('cache',tx('识图缓存','Vision answer cache')),numberField('cacheTtlSeconds',tx('缓存有效期(秒)','Cache TTL (seconds)'),null,0,31536000),numberField('cacheMaxEntries',tx('最大缓存数量','Maximum cached answers'),null,1,100000),h('h4',{className:'vr-ia-subtitle',key:'t'},tx('超时','Timeouts')),numberField('timeoutMs',tx('单次模型请求','Single model request'),tx('毫秒。','Milliseconds.'),1000,600000),numberField('visionTaskTimeoutMs',tx('单个视觉任务','Single visual task'),tx('包含该任务内部的重试和备用模型;不是每个后端各自一份。','Includes retries and fallbacks inside the task; it is not a fresh budget per backend.'),1000,180000),numberField('ocrTimeoutMs',tx('OCR 任务','OCR task'),null,1000,120000),h('div',{className:'vr-field',key:'ocr-engine'},fieldHead('ocrEngine',tx('OCR 默认引擎','Default OCR engine')),h('select',{className:'vr-input',value:String(value('ocrEngine','auto')||'auto'),disabled:!writable||saving,onChange:function(event){setValue('ocrEngine',event.target.value);}},h('option',{value:'auto'},tx('自动(Tesseract → 视觉模型)','Auto (Tesseract → vision)')),h('option',{value:'tesseract'},tx('仅 Tesseract','Tesseract only')),h('option',{value:'vision'},tx('仅视觉模型','Vision model only'))),h('p',{className:'vr-hint'},tx('vision_ocr 未显式指定 engine 时使用;单次调用的 engine=tesseract/vision 始终优先。','Used when vision_ocr does not explicitly choose an engine; per-call engine=tesseract/vision always wins.'))),h('div',{className:'vr-field',key:'budget'},fieldHead('visionTurnBudgetMs',tx('整轮视觉工具上限','Whole-turn vision-tool limit')),h('select',{className:'vr-input',value:customBudget?'custom':'unlimited',disabled:!writable||saving,onChange:function(event){setValue('visionTurnBudgetMs',event.target.value==='unlimited'?0:(turnBudget>0?turnBudget:180000));}},h('option',{value:'unlimited'},tx('不限制(推荐)','Unlimited (recommended)')),h('option',{value:'custom'},tx('自定义','Custom'))),customBudget?numberField('visionTurnBudgetMs',tx('上限(毫秒)','Limit (ms)'),null,10000,600000):null)]),card([h('h4',{className:'vr-ia-subtitle',key:'cost'},tx('模型顺序与成本','Model order & cost')),toggle('freeCloudFirst',tx('免费云模型优先','Try free cloud models first'))]),card([h('h4',{className:'vr-ia-subtitle',key:'scope'},tx('识图模式范围','Vision mode scope')),toggle('autoWrapProviders',tx('自动允许已启用模型使用识图','Automatically allow enabled models to use Vision mode')),wrappersEditor()]),local?card([h('h4',{className:'vr-ia-subtitle',key:'network'},tx('网络与远程','Network & remote')),h('p',{className:'vr-hint',key:'network-authority'},tx('默认沿用 DSH/Host 的网络出口;只有需要让视觉请求走不同出口时才填写下方覆盖。','By default Vision Router follows the DSH/Host network path. Use the override below only when vision requests need a different egress route.')),toggle('allowRemoteSettings',tx('允许可信 Host 远程修改设置','Allow trusted-host remote settings'),tx('默认关闭;trustedHosts 不是身份认证。','Off by default; trustedHosts is not authentication.')),textField('proxy',tx('视觉专用代理覆盖','Vision-only proxy override'),tx('留空即沿用 DSH/Host;支持 http(s)://、socks5://,并兼容旧 socks5h://。','Leave empty to follow DSH/Host; supports http(s):// and socks5://, with legacy socks5h:// compatibility.')),textareaArray('proxyHosts',tx('视觉代理覆盖域名','Vision proxy override hosts'),tx('仅在上方覆盖非空时生效;每行一个,其余请求继续沿用 DSH/Host。','Used only when the override above is non-empty; one host per line. All other requests keep the DSH/Host path.'))]):null,card([h('h4',{className:'vr-ia-subtitle',key:'compat'},tx('兼容模式','Compatibility')),toggle('rewriteImages',tx('保护纯文本模型','Protect text-only models')),toggle('routing',tx('整轮视觉路由(旧工作流)','Whole-turn vision routing (legacy workflow)')),routing?toggle('reverseRouting',tx('纯文字消息继续使用聊天模型','Keep text-only messages on the chat model')):null,routing?textProviderEditor():null]),card([h('button',{type:'button',className:'vr-btn',key:'dev',onClick:function(){setOpen(Object.assign({},open,{developer:!open.developer}));}},open.developer?tx('隐藏开发者设置','Hide developer settings'):tx('显示开发者设置','Show developer settings')),open.developer?h('div',{className:'vr-ia-dev',key:'body'},toggle('progressiveTools',tx('渐进式工具暴露','Progressive tool exposure')),h('p',{className:'vr-hint',key:'progressive-restart'},tx('保存后需重启 DSH 才生效。','Restart DSH after saving for this change to take effect.')),local?toggle('stealth','Stealth'):null,local?textField('wrapperRoute',tx('包装路由名','Wrapper route name')):null,local?textField('chainRoute',tx('视觉链路由名','Vision chain route name')):null,textareaArray('extraVisionModels',tx('额外视觉能力标记','Extra vision capability labels'),tx('只有诊断发现模型未声明图片能力时才需要。','Needed only when diagnostics show missing image-capability metadata.'))):null]));}
262
262
  function diagnosticValue(label,valueText){return h('div',{className:'vr-ia-diag-row',key:label},h('span',null,label),h('strong',null,valueText));}
263
263
  function updatePanel(){if(!local)return null;var result=updateState.result,auto=result&&result.autoUpdate,current=result&&result.currentVersion?result.currentVersion:tx('检测中','Checking'),latest=result&&result.latestVersion?result.latestVersion:'—',available=result&&result.ok===true&&result.updateAvailable===true,profile=auto&&auto.profile?auto.profile:'web',manualVersion=result&&result.latestVersion?result.latestVersion:'<version>',spec='dsh-vision-router@'+manualVersion,pnpm='pnpm dsh plugin --profile '+profile+' add '+spec,npx='npx @deepseek-ai/dsh plugin --profile '+profile+' add '+spec;return card([h('h4',{className:'vr-ia-subtitle',key:'title'},tx('版本更新','Updates')),h('p',{className:'vr-hint',key:'status'},updateState.status==='running'?tx('正在检查更新…','Checking for updates…'):result&&result.ok===false?tx('更新检查失败:','Update check failed: ')+String(result.error||'unknown'):available?tx('发现新版本:v','Update available: v')+latest+tx('(当前 v',' (current v')+current+')':result&&result.ok===true?tx('已是最新版本 v','Up to date: v')+current:tx('尚未检查','Not checked yet')),h('div',{className:'vr-ia-actions',key:'actions'},h('button',{type:'button',className:'vr-btn',disabled:updateState.status==='running',onClick:function(){void runUpdateCheck(true);}},tx('检查更新','Check for updates')),available&&auto&&auto.supported===true&&auto.token?h('button',{type:'button',className:'vr-btn vr-btn-save',disabled:selfUpdateState.status==='running',onClick:function(){void runSelfUpdate();}},selfUpdateState.status==='running'?tx('更新中…','Updating…'):tx('一键更新','Update now')):null),selfUpdateState.status==='done'&&selfUpdateState.result?h('p',{className:'vr-hint',key:'updated'},tx('更新完成,请重启 DSH。','Update complete. Restart DSH.')):selfUpdateState.error?h('p',{className:'vr-failed',key:'uerr'},String(selfUpdateState.error)):null,available&&(!auto||auto.supported!==true)?h('div',{className:'vr-ia-manual',key:'manual'},h('p',{className:'vr-hint'},tx('当前安装方式不支持安全的一键更新,请使用与你当前 DSH 安装方式一致的命令:','This install method cannot be safely auto-updated. Use the command matching your DSH installation:')),h('code',{className:'vr-ia-code'},pnpm),h('code',{className:'vr-ia-code'},npx)):null]);}
264
264
  function diagnosticsPage(){var providerCount=groups.length,modelCount=groups.reduce(function(total,group){return total+arr(group&&group.models).length;},0),configured=chainRows().filter(function(row){return row&&row.provider&&row.model;}).length,ollama=obj(value('localOllama',{})),lm=obj(value('localLmStudio',{})),testText=!local?tx('仅本机可见','Local only'):testState.status==='running'?tx('检测中','Checking'):testState.status==='done'&&testState.result&&testState.result.ok===true?tx('连接正常','Connected'):testState.status==='done'?tx('连接失败','Failed'):tx('未检测','Not checked'),capsText=caps.status==='ready'?tx('正常','Ready'):caps.status==='loading'?tx('检测中','Checking'):caps.status==='error'?tx('不可用','Unavailable'):tx('未检测','Not checked');var rows=[[tx('设置协议','Settings contract'),String(value('settingsContractRevision','—'))],[tx('模型目录','Model catalog'),catalog.status==='ready'?tx('正常','Ready'):catalog.status==='loading'?tx('检测中','Checking'):tx('不可用','Unavailable')],[tx('图片能力元数据','Image capability metadata'),capsText],[tx('可选 Provider','Selectable providers'),String(providerCount)],[tx('可选模型','Selectable models'),String(modelCount)],[tx('已配置识图模型','Configured vision models'),String(configured)],[tx('内置免费兜底','Built-in free fallback'),toggleValue('freeFallback',true)?(caps.builtinFallback.length?tx('已启用,','Enabled, ')+caps.builtinFallback.length+tx(' 个模型',' models'):tx('已启用','Enabled')):tx('已关闭','Disabled')],[tx('后端连接','Backend connection'),testText],[tx('Ollama','Ollama'),local?(ollama.enabled===true?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('LM Studio','LM Studio'),local?(lm.enabled===true?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('桌面截图','Desktop capture'),local?(toggleValue('desktopScreenshot',false)?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('仅本机可见','Local only')],[tx('网络出口','Network egress'),local?(String(value('proxy','')).trim()?tx('视觉专用覆盖','Vision-only override'):tx('DSH/Host(默认)','DSH/Host (default)')):tx('仅本机可见','Local only')],[tx('远程设置','Remote settings'),local?(toggleValue('allowRemoteSettings',false)?tx('已启用','Enabled'):tx('未启用','Disabled')):tx('当前为远程安全视图','Remote safe view')]];function reportText(){return ['Vision Router diagnostics'].concat(rows.map(function(row){return row[0]+': '+row[1];}),['doctor: dsh-vision-router doctor']).join('\n');}async function copyReport(){try{if(typeof navigator!=='undefined'&&navigator.clipboard&&typeof navigator.clipboard.writeText==='function')await navigator.clipboard.writeText(reportText());setActionState({status:'copied'});}catch(error){setActionState({status:'error',error:error&&error.message?error.message:String(error)});}}return h(React.Fragment,null,title(tx('诊断','Diagnostics'),tx('状态页不会修改你的模型/路由设置;连接测试和更新操作会明确由按钮触发。','The status page does not change model or routing settings; connection tests and updates are explicit actions.')),card(rows.map(function(row){return diagnosticValue(row[0],row[1]);})),card([h('div',{className:'vr-ia-actions',key:'actions'},local?h('button',{type:'button',className:'vr-btn',disabled:testState.status==='running',onClick:function(){void runTestConnection();}},tx('测试连接','Test connection')):null,h('button',{type:'button',className:'vr-btn',onClick:function(){invalidateCatalog();}},tx('重新检测模型','Re-detect models')),local?h('button',{type:'button',className:'vr-btn',onClick:function(){void openLogs();}},tx('打开日志文件夹','Open logs folder')):null,h('button',{type:'button',className:'vr-btn',onClick:function(){void copyReport();}},tx('复制诊断信息','Copy diagnostics'))),h('p',{className:'vr-hint',key:'doctor'},tx('需要完整 DSH 版本、安装、Profile、运行时路由和会话诊断时执行:dsh-vision-router doctor','For the full DSH version, installation, profile, runtime-route, and session report, run: dsh-vision-router doctor')),testState.status==='done'&&testState.result&&testState.result.ok!==true?h('p',{className:'vr-failed',key:'testerr'},String(testState.result.error||tx('连接测试失败','Connection test failed'))):null,actionState.status==='error'?h('p',{className:'vr-failed',key:'aerr'},String(actionState.error||'unknown')):null]),updatePanel());}
@@ -83,7 +83,7 @@ export function transformSettingsIaToNativeCards(source = SETTINGS_IA_CLIENT_PRE
83
83
  general:new Set(['providers','freeFallback']),
84
84
  strategy:new Set(['tool','structuredVisionBootstrap','visionDepth','visionDepthMaxCalls','guidanceOverrides']),
85
85
  local:new Set(['localOllama','localLmStudio','desktopScreenshot']),
86
- advanced:new Set(['wrappedProviders','downscale','downscaleMaxPixels','cache','cacheTtlSeconds','cacheMaxEntries','timeoutMs','visionTaskTimeoutMs','ocrTimeoutMs','visionTurnBudgetMs','freeCloudFirst','autoWrapProviders','allowRemoteSettings','proxy','proxyHosts','rewriteImages','routing','reverseRouting','textProvider','progressiveTools','stealth','wrapperRoute','chainRoute','extraVisionModels']),
86
+ advanced:new Set(['wrappedProviders','downscale','downscaleMaxPixels','cache','cacheTtlSeconds','cacheMaxEntries','timeoutMs','visionTaskTimeoutMs','ocrTimeoutMs','ocrEngine','visionTurnBudgetMs','freeCloudFirst','autoWrapProviders','allowRemoteSettings','proxy','proxyHosts','rewriteImages','routing','reverseRouting','textProvider','progressiveTools','stealth','wrapperRoute','chainRoute','extraVisionModels']),
87
87
  diagnostics:new Set([])
88
88
  };
89
89
  var invalidChainDraft=chainDraft!==undefined&&!validChain(chainRows());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
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": {
@@ -54,8 +54,8 @@
54
54
  "undici": "^7.29.0"
55
55
  },
56
56
  "peerDependencies": {
57
- "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
58
- "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
57
+ "@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.2",
58
+ "@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.8 || ^0.1.1-rc.1 || ^0.1.3-alpha.2 || 0.1.5-alpha.1 || 0.1.5-alpha.2 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.2",
59
59
  "sharp": ">=0.35.3 <1"
60
60
  },
61
61
  "peerDependenciesMeta": {
@@ -70,7 +70,7 @@
70
70
  "sharp": "^0.35.4"
71
71
  },
72
72
  "scripts": {
73
- "test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/degraded-local-evidence.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/issue-512-tool-restriction.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-event-feed.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
73
+ "test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/degraded-local-evidence.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/release-registry-identity.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/issue-512-tool-restriction.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/long-ocr-empty-retry.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-event-feed.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
74
74
  "test:core": "node --test tests/core.test.js tests/degraded-local-evidence.test.js tests/runtime-e2e.test.js tests/structured-bootstrap.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-execution-policy.test.js tests/vision-backend-runtime-policy.test.js",
75
75
  "test:routing": "node --test tests/issue-374-delegated-call-config.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/vision-routing-product.test.js tests/vision-capability-router.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow-health.test.js tests/vision-runtime-performance.test.js tests/vision-background-benchmark.test.js tests/vision-image-input-verdict.test.js",
76
76
  "test:session": "node --test tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/replay-delegation.test.js tests/legacy-session-repair.test.js tests/session-repair-v2.test.js tests/issue-276-session-image-ownership.test.js",