dsh-vision-router 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/README.zh.md +10 -6
- package/cordis.patch.yml +6 -2
- package/docs/progressive-tools-cache.md +57 -0
- package/entry.js +51 -0
- package/index.js +9 -0
- package/lib/client.js +258 -25
- package/lib/file-logger.js +273 -0
- package/package.json +10 -7
- package/presets/README.md +3 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.
|
|
18
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.3.0"><img src="https://img.shields.io/badge/release-v1.3.0-5B4CF0?style=flat-square" alt="Release v1.3.0" /></a>
|
|
19
19
|
<a href="tests"><img src="https://img.shields.io/badge/verified-149%20tests-2EA44F?style=flat-square" alt="Verified: 149 tests" /></a>
|
|
20
20
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
|
|
21
21
|
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >=22" /></a>
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **Announcement (v1.
|
|
31
|
+
> 📌 **Announcement (v1.3.0)**
|
|
32
32
|
>
|
|
33
|
-
> v1.
|
|
33
|
+
> **v1.3.0** makes long sessions and mixed-plugin setups safer: the vision-tool schema is stable by default to protect prefix/KV caches, diagnostics can be persisted and opened from Settings, custom vision-model filtering explains missing image metadata and supports re-detection, host-provided DSH capability packages use the correct peer contract, and Vision Router tool cards can coexist with other vision plugins while keeping Vision Router rendering precedence.
|
|
34
|
+
>
|
|
35
|
+
> v1.2.3 fixes DSH Desktop's re-appearing first-run dialog: the onboarding "seen" flag and the model-guide step now persist in the profile settings file instead of origin-scoped `localStorage`, which a random per-launch port (`--port 0`) wiped on every boot (issue #78).
|
|
36
|
+
>
|
|
37
|
+
> v1.2.2 closed the last attachment-id gap — ids announced for images the host persisted itself (e.g. `read_image` re-uploads, `sha256:…`) now resolve in `vision_describe` and every pixel tool (issue #72) — stopped `vision_present` and other tool-result image blocks from ever locking a text-model session with `UNSUPPORTED_CONTENT` (issue #74; already-locked sessions heal after upgrading), and warned loudly when a stale sharp left over from a pre-v1.2 upgrade would break the pixel tools with `colourspace: parameter space not set` (issue #75).
|
|
34
38
|
>
|
|
35
39
|
> v1.2.1 hardened the pixel loop: all eleven pixel tools now accept uploaded-image attachment ids directly (no more `cannot read …/sha256:…` round trips), artifact filenames carry collision-free fingerprints, `vision_ground` retries degenerate boxes, the model guide replays fully from step 1 (leaving the settings first), and the settings card scrolls smoothly even with hundreds of models per provider.
|
|
36
40
|
|
|
@@ -108,7 +112,7 @@ The auto-vision group follows the live DSH model catalog. Adding models or chang
|
|
|
108
112
|
|
|
109
113
|
### 3. Paste or upload the image
|
|
110
114
|
|
|
111
|
-
After choosing the “+ Auto Vision” group, paste or upload an image normally.
|
|
115
|
+
After choosing the “+ Auto Vision” group, paste or upload an image normally. By default the complete vision tool schema is stable from session start, so the agent can immediately use `vision_describe`, `vision_ground`, `vision_crop`, and the rest across multiple steps when needed.
|
|
112
116
|
|
|
113
117
|
The built-in anonymous OVH vision fallback is already configured, so normal image use needs no signup or API key. **The lower-right chat picker selects only the brain/conversation model**; vision backends do not belong there. Advanced options live under **Settings → Plugins → Plugin config → 视觉路由(自动识图)**: each vision-backend row selects one image-capable user model already configured under **Settings → Models**. Leaving every user row empty is valid; the OVH chain remains the final fallback. `Vision HTTP` is an internal transport route, not a model group users should select.
|
|
114
118
|
|
|
@@ -127,7 +131,7 @@ The built-in anonymous OVH vision fallback is already configured, so normal imag
|
|
|
127
131
|
- **Automatic failover with classified errors.** Region blocks, ToS filtering, 402 quota, 429 rate limits (with Retry-After backoff), context overflow, network failures — the chain walks providers one by one and only reports after all of them failed, with actionable advice.
|
|
128
132
|
- **Image memory.** Vision answers are cached by attachment content hash; later text turns substitute the recorded description (marked as untrusted evidence), so DeepSeek genuinely remembers earlier images without re-spending vision calls.
|
|
129
133
|
- **A verifiable pixel loop.** Reference → `vision_html_screenshot` → `vision_pixel_diff` (ratio + red heatmap + worst-region ranking) → fix → repeat until the mismatch converges. UI restoration becomes measurable instead of eyeballed.
|
|
130
|
-
- **
|
|
134
|
+
- **Stable tool schema.** All eleven deep tools are registered from session start by default, avoiding a mid-conversation tool-list expansion that can invalidate long-context KV/prefix caches. `progressiveTools: true` remains an advanced boot-time opt-in; only then does `vision_activate` mount the tools on demand. See [`docs/progressive-tools-cache.md`](docs/progressive-tools-cache.md).
|
|
131
135
|
- **Selective proxy.** Only the configured vision provider hosts go through your local proxy; DeepSeek stays direct.
|
|
132
136
|
|
|
133
137
|
### Pixel loop in practice
|
|
@@ -148,7 +152,7 @@ The vision model is **only the eyes**; DeepSeek is **always the brain**. An imag
|
|
|
148
152
|
|
|
149
153
|
## Tools
|
|
150
154
|
|
|
151
|
-
|
|
155
|
+
Default `progressiveTools: false`: all eleven deep tools stay registered from plugin startup, so text and image turns can call them immediately. If you explicitly set `progressiveTools: true` in the profile/composition `cordis.patch.yml`, progressive mode is restored: only `vision_activate` is exposed initially, the full tool set mounts on first use, and the `vision-tools` skill is registered. This is a boot-time switch; restart DSH after changing it. Built on sharp / potrace / tesseract / system Chrome — no Python:
|
|
152
156
|
|
|
153
157
|
<p align="center">
|
|
154
158
|
<img src="assets/vision-tools.svg" width="100%" alt="Eleven vision tools available in DSH Vision Router." />
|
|
@@ -264,7 +268,7 @@ Everything is optional; defaults work out of the box. Edit via the Web card or a
|
|
|
264
268
|
| `wrapperRoute` / `chainRoute` | `deepseek-vision` / `vision-chain` | admission wrapper route name / fallback chain route name (empty disables) |
|
|
265
269
|
| `stealth` | `false` | take over the official `deepseek-official` route (official row only; custom routes are auto-wrapped by default) |
|
|
266
270
|
| `textProvider` | `deepseek-official` / `deepseek-v4-pro` | the model that reasons (your daily model) |
|
|
267
|
-
| `tool` / `progressiveTools` / `autoActivateOnImage` | `true`
|
|
271
|
+
| `tool` / `progressiveTools` / `autoActivateOnImage` | `true` / `false` / `true` | vision tools on / progressive mounting (off by default for a stable tool schema) / image-turn auto-mount when progressive mode is enabled; `progressiveTools` is boot-time config |
|
|
268
272
|
| `rewriteImages` | `true` | rewrite image blocks in the model input (cached description or tool-hint marker); the UI log keeps images |
|
|
269
273
|
| `downscale` / `downscaleMaxPixels` | `true` / `4000000` | pre-call downscale and its pixel budget (latency guard) |
|
|
270
274
|
| `cache` / `cacheTtlSeconds` / `cacheMaxEntries` | `true` / `3600` / `200` | vision answer cache |
|
package/README.zh.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.
|
|
18
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.3.0"><img src="https://img.shields.io/badge/release-v1.3.0-5B4CF0?style=flat-square" alt="Release v1.3.0" /></a>
|
|
19
19
|
<a href="tests"><img src="https://img.shields.io/badge/verified-149%20tests-2EA44F?style=flat-square" alt="Verified: 149 tests" /></a>
|
|
20
20
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
|
|
21
21
|
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >=22" /></a>
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **公告(v1.
|
|
31
|
+
> 📌 **公告(v1.3.0)**
|
|
32
|
+
>
|
|
33
|
+
> **v1.3.0** 重点加固长会话稳定性与插件共存:视觉工具 schema 默认从会话开始保持稳定,减少 prefix/KV cache 失效;诊断日志可持久化并从设置页直接打开;自定义视觉模型缺失 image 元数据时会明确解释并支持重新检测;宿主提供的 DSH capability 包使用正确的 peer 依赖契约;与其他视觉插件共装时,同名工具卡不再导致启动失败,同时保留 Vision Router 自己的渲染优先级。
|
|
34
|
+
>
|
|
35
|
+
> v1.2.3 现已支持:修复 DSH Desktop 每次启动都重复弹出首次引导的问题——引导「已读」标记与模型引导步骤改存 profile 设置文件(不再依赖按 origin 隔离、每次随机端口重启即清零的 `localStorage`)(issue #78)。
|
|
32
36
|
>
|
|
33
37
|
> v1.2.2 现已支持:补上最后一处附件 ID 缺口——宿主 `read_image` 回挂图片公布的 `sha256:…` ID 现在可被 `vision_describe` 与全部像素工具解析(issue #72);`vision_present` 等工具结果里的图像块不再把文本模型会话锁死在 `UNSUPPORTED_CONTENT`(issue #74,已锁死的历史会话升级后自动修复);检测到 v1.1.x 升级残留的旧版 sharp 时明确告警,把玄学的 `colourspace` 报错变成一眼可见的修复指引(issue #75)。
|
|
34
38
|
>
|
|
@@ -108,7 +112,7 @@ opencode-go + 自动识图 ← 发图片时选这个
|
|
|
108
112
|
|
|
109
113
|
### 3. 直接粘贴或上传图片
|
|
110
114
|
|
|
111
|
-
选好「+
|
|
115
|
+
选好「+ 自动识图」模型组后,直接往对话里贴图即可。默认情况下完整视觉工具表从会话开始就保持稳定,Agent 可直接调用 `vision_describe`、`vision_ground`、`vision_crop` 等工具看图,需要时连续多步操作。
|
|
112
116
|
|
|
113
117
|
默认已经有内置 OVH 匿名视觉兜底,无需注册、无需 Key。**聊天页右下角只选择“脑子/会话模型”**;视觉模型不要在那里选。高级配置在 **设置 → 插件 → 插件配置 → 视觉路由(自动识图)**:视觉后端链每一行只选择一个你在 **设置 → 模型** 中已经配置且支持图片输入的用户模型;一行都不填也可以,OVH 免费链会固定在最后兜底。插件内部的 `Vision HTTP` 只是传输实现,不是用户需要选择的模型组。
|
|
114
118
|
|
|
@@ -127,7 +131,7 @@ opencode-go + 自动识图 ← 发图片时选这个
|
|
|
127
131
|
- **自动降级 + 分类报错。** 地区限制、ToS 风控、402 额度、429 限流(尊重 Retry-After 退避重试)、上下文超长、网络故障——链路逐供应商尝试,全部失败才报错并给出可操作的建议。
|
|
128
132
|
- **图片记忆。** 视觉答案按附件内容哈希缓存;后续文字轮用记录的描述替换历史图片(标注为不可信证据),DeepSeek 真正“记得”之前发过的图,且不重复消耗视觉调用。
|
|
129
133
|
- **可验证的像素闭环。** 参照图 → `vision_html_screenshot` → `vision_pixel_diff`(差异率 + 红色热力图 + 最差区域排行)→ 修复 → 再对比,直到差异收敛。UI 还原从“目测”变成“实测”。
|
|
130
|
-
-
|
|
134
|
+
- **稳定工具 schema。** 默认从会话开始就注册完整 11 个深看工具,避免图片轮中途扩展工具列表导致长上下文的 KV / prefix cache 失效。仍保留 `progressiveTools: true` 作为高级启动期 opt-in;开启后才使用 `vision_activate` 按需挂载。详见 [`docs/progressive-tools-cache.md`](docs/progressive-tools-cache.md)。
|
|
131
135
|
- **选择性代理。** 只有配置的视觉供应商域名走本地代理;DeepSeek 保持直连。
|
|
132
136
|
|
|
133
137
|
### 像素闭环实测
|
|
@@ -148,7 +152,7 @@ Agent 仅根据参考图复刻 UI,再用 `vision_pixel_diff` 验证最终结
|
|
|
148
152
|
|
|
149
153
|
## 工具
|
|
150
154
|
|
|
151
|
-
11
|
|
155
|
+
默认 `progressiveTools: false`:11 个深看工具从插件启动时就保持常驻,文本轮和图片轮都可直接调用。若你在 profile / composition 的 `cordis.patch.yml` 中显式开启 `progressiveTools: true`,才会恢复渐进模式:初始只暴露 `vision_activate`,首次需要时再挂载完整工具,并注册 `vision-tools` 技能。该开关是启动期配置,修改后需重启 DSH。全部工具基于 sharp / potrace / tesseract / 系统 Chrome——无 Python:
|
|
152
156
|
|
|
153
157
|
<p align="center">
|
|
154
158
|
<img src="assets/vision-tools-zh.svg" width="100%" alt="DSH Vision Router 的 11 个视觉工具。" />
|
|
@@ -264,7 +268,7 @@ Web 配置页在 **设置 → 插件 → 插件配置** 下注册「视觉路由
|
|
|
264
268
|
| `wrapperRoute` / `chainRoute` | `deepseek-vision` / `vision-chain` | 准入包装路由名 / 降级链路由名(置空关闭) |
|
|
265
269
|
| `stealth` | `false` | 接管官方 `deepseek-official` 路由(仅官方行;自定义路由默认由自动包装处理) |
|
|
266
270
|
| `textProvider` | `deepseek-official` / `deepseek-v4-pro` | 负责思考的模型(你的日常模型) |
|
|
267
|
-
| `tool` / `progressiveTools` / `autoActivateOnImage` | `true`
|
|
271
|
+
| `tool` / `progressiveTools` / `autoActivateOnImage` | `true` / `false` / `true` | 视觉工具总开关 / 渐进式挂载(默认关闭以稳定工具 schema)/ 渐进模式下图片轮自动挂载;`progressiveTools` 为启动期配置 |
|
|
268
272
|
| `rewriteImages` | `true` | 模型输入层改写图片块(缓存描述或工具提示标记);界面日志保留图片 |
|
|
269
273
|
| `downscale` / `downscaleMaxPixels` | `true` / `4000000` | 调用前压缩及其像素预算(延迟保护) |
|
|
270
274
|
| `cache` / `cacheTtlSeconds` / `cacheMaxEntries` | `true` / `3600` / `200` | 视觉答案缓存 |
|
package/cordis.patch.yml
CHANGED
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
# 挂载后是否接管官方 deepseek-official 路由由设置里的「隐身模式」开关决定
|
|
9
9
|
# (默认关;开启后需自行在 profile 补丁层禁用 llm-deepseek 行)。
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# 挂载插件行。默认保持完整视觉工具表常驻(issue #81):虽然渐进挂载可以
|
|
12
|
+
# 少发一小段工具 schema,但图片轮首次扩展工具列表会改变请求前缀,可能让
|
|
13
|
+
# 长会话的 provider KV/prefix cache 整体失效。高级用户仍可在 profile /
|
|
14
|
+
# composition 补丁中显式把 progressiveTools 改回 true(启动期配置)。
|
|
13
15
|
- insert:
|
|
14
16
|
- id: vision-router
|
|
15
17
|
name: dsh-vision-router
|
|
18
|
+
config:
|
|
19
|
+
progressiveTools: false
|
|
16
20
|
|
|
17
21
|
# 放宽附件图片限制(部署默认 5MB / 4000 万像素 → 20MB / 1 亿像素),
|
|
18
22
|
# 大尺寸设计稿/扫描图可过审。字段可选,不需要可在 profile 补丁层覆写。
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Progressive tools and prefix cache
|
|
2
|
+
|
|
3
|
+
中文 | English
|
|
4
|
+
|
|
5
|
+
## 中文
|
|
6
|
+
|
|
7
|
+
Vision Router 默认让完整视觉工具表从会话开始就保持稳定。正常通过 DSH bundle 安装时,组合层会提供:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
- id: vision-router
|
|
11
|
+
name: dsh-vision-router
|
|
12
|
+
config:
|
|
13
|
+
progressiveTools: false
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
插件入口本身也把 `progressiveTools` 的 schema 默认值设为 `false`,因此即使后续 profile patch 整块覆盖了 bundle 的 `config`、但没有重述这个字段,也不会意外回到渐进模式。
|
|
17
|
+
|
|
18
|
+
原因是很多模型服务的 KV / prefix cache 会把工具 schema 视为请求前缀的一部分。若长会话一开始只暴露 `vision_activate`,第一次图片轮再挂载完整视觉工具,工具列表发生变化后可能导致此前的大段会话前缀无法命中缓存。
|
|
19
|
+
|
|
20
|
+
渐进式暴露仍然保留为高级 opt-in。如果你更在意平时请求中少携带工具 schema,而不依赖长会话前缀缓存,请在 **profile / composition 的 `cordis.patch.yml`** 中显式给 `vision-router` 设置:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
- id: vision-router
|
|
24
|
+
config:
|
|
25
|
+
progressiveTools: true
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`progressiveTools` 决定插件启动时注册哪组工具,因此它是**启动期配置**;不要依赖运行中的 Web 设置热切换这个字段。修改 profile patch 后重启 DSH。
|
|
29
|
+
|
|
30
|
+
开启后,平时只暴露 `vision_activate`,首次需要时再挂载完整视觉工具;默认关闭时,完整视觉工具从一开始就常驻。视觉工具本身的能力不变。
|
|
31
|
+
|
|
32
|
+
## English
|
|
33
|
+
|
|
34
|
+
Vision Router keeps the complete vision tool schema stable from the beginning of a session by default. Normal DSH bundle installs provide this composition layer:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
- id: vision-router
|
|
38
|
+
name: dsh-vision-router
|
|
39
|
+
config:
|
|
40
|
+
progressiveTools: false
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The public plugin entrypoint also makes `false` the schema default. Therefore a later profile patch that replaces the bundle `config` without restating this field cannot accidentally fall back to progressive mode.
|
|
44
|
+
|
|
45
|
+
Many model providers include tool schemas in the KV/prefix-cacheable request prefix. If a long conversation starts with only `vision_activate` and the first image turn later mounts the complete vision tool set, that prefix change can invalidate a large cached history.
|
|
46
|
+
|
|
47
|
+
Progressive exposure remains available as an advanced opt-in. If minimizing the always-present tool schema matters more than long-context prefix-cache stability, explicitly set it in the **profile/composition `cordis.patch.yml`**:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
- id: vision-router
|
|
51
|
+
config:
|
|
52
|
+
progressiveTools: true
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`progressiveTools` determines which tools are registered at plugin startup, so it is a **boot-time setting**; do not rely on changing this field through live Web settings. Restart DSH after changing the profile patch.
|
|
56
|
+
|
|
57
|
+
When enabled, only `vision_activate` is exposed initially and the complete vision tool set mounts on first use. With the default disabled mode, the complete vision tool set is present from the start. The vision capabilities themselves are unchanged.
|
package/entry.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Public plugin entrypoint.
|
|
2
|
+
//
|
|
3
|
+
// Keep the large implementation in index.js, but normalize the progressive
|
|
4
|
+
// tools switch here before Cordis reads the exported Config. Issue #81 showed
|
|
5
|
+
// that changing the tool schema mid-session can invalidate provider prefix/KV
|
|
6
|
+
// caches for very long conversations, so progressive exposure is now an
|
|
7
|
+
// explicit opt-in rather than the implicit fallback.
|
|
8
|
+
|
|
9
|
+
import z from '@deepseek-ai/schemastery'
|
|
10
|
+
import * as core from './index.js'
|
|
11
|
+
import { installVisionRouterFileLogging } from './lib/file-logger.js'
|
|
12
|
+
|
|
13
|
+
// Schemastery object schemas expose set() as the supported way to replace a
|
|
14
|
+
// field schema. This mutates the Config object that index.js itself later uses
|
|
15
|
+
// for the settings namespace, so composition config and settings validation
|
|
16
|
+
// agree on the same default.
|
|
17
|
+
core.Config.set('progressiveTools', z.boolean().default(false))
|
|
18
|
+
|
|
19
|
+
export * from './index.js'
|
|
20
|
+
export const Config = core.Config
|
|
21
|
+
|
|
22
|
+
// Defense in depth for direct/programmatic callers that invoke apply() without
|
|
23
|
+
// first running the Cordis Config resolver: only an explicit true enables the
|
|
24
|
+
// schema-changing progressive mode. The wrapped context changes only logger:
|
|
25
|
+
// every existing vision-router diagnostic still reaches the host logger and is
|
|
26
|
+
// also persisted to ~/.dsh/logs/vision-router/vision-router.log.
|
|
27
|
+
export function apply(ctx, config = {}) {
|
|
28
|
+
const logging = installVisionRouterFileLogging(ctx)
|
|
29
|
+
try {
|
|
30
|
+
const result = core.apply(logging.ctx, {
|
|
31
|
+
...config,
|
|
32
|
+
progressiveTools: config.progressiveTools === true,
|
|
33
|
+
})
|
|
34
|
+
if (result && typeof result.then === 'function') {
|
|
35
|
+
return result.catch((error) => {
|
|
36
|
+
logging.logger.error(
|
|
37
|
+
'vision-router: plugin apply failed: %s',
|
|
38
|
+
error && error.stack ? error.stack : error && error.message ? error.message : String(error),
|
|
39
|
+
)
|
|
40
|
+
throw error
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
return result
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logging.logger.error(
|
|
46
|
+
'vision-router: plugin apply failed: %s',
|
|
47
|
+
error && error.stack ? error.stack : error && error.message ? error.message : String(error),
|
|
48
|
+
)
|
|
49
|
+
throw error
|
|
50
|
+
}
|
|
51
|
+
}
|
package/index.js
CHANGED
|
@@ -233,6 +233,15 @@ export const Config = z.object({
|
|
|
233
233
|
tool: z.boolean().default(true),
|
|
234
234
|
progressiveTools: z.boolean().default(true),
|
|
235
235
|
autoActivateOnImage: z.boolean().default(true),
|
|
236
|
+
// Client-persisted UI state (issue #78): DSH Desktop serves the Web UI from
|
|
237
|
+
// a random port on every launch, so origin-scoped localStorage forgets the
|
|
238
|
+
// first-run onboarding dialog and it re-appeared on every boot. These keys
|
|
239
|
+
// live in the settings section (the profile settings file) instead; the
|
|
240
|
+
// client still mirrors best-effort copies into localStorage for downgrades.
|
|
241
|
+
// Both are internal to the client bundle — the settings card renders them
|
|
242
|
+
// nowhere, and the server half never reads them.
|
|
243
|
+
onboardingSeen: z.boolean().default(false),
|
|
244
|
+
visionGuideStep: z.string().default(''),
|
|
236
245
|
artifactsDir: z.string().default('.dsh-vision-router/artifacts'),
|
|
237
246
|
rewriteImages: z.boolean().default(true),
|
|
238
247
|
downscale: z.boolean().default(true),
|
package/lib/client.js
CHANGED
|
@@ -77,6 +77,14 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
visionCapsLoading: '正在验证哪些模型真正支持图片输入…',
|
|
78
78
|
visionCapsError: '视觉能力元数据暂时不可用;为防止误选,暂不提供用户视觉模型下拉。内置 OVH 免费兜底仍可用。',
|
|
79
79
|
visionCapsFiltered: '视觉后端下拉只显示明确声明 image 输入的模型。',
|
|
80
|
+
visionCapsEmptyTitle: '没有可选的用户视觉模型',
|
|
81
|
+
visionCapsEmptyBody: '检测到 {count} 个用户模型,但它们都没有被 DSH 明确标记为支持图片,因此已被安全隐藏。',
|
|
82
|
+
visionCapsHiddenPrefix: '被隐藏的模型:',
|
|
83
|
+
visionCapsReasonMissingImage: '未声明 image',
|
|
84
|
+
visionCapsReasonUnverified: '无法验证图片能力',
|
|
85
|
+
visionCapsHiddenMore: '另有 {count} 个模型未显示',
|
|
86
|
+
visionCapsMissingImageHint: '如果这里有你刚在「设置 → 模型 → 添加自定义提供方」中添加的视觉模型,请在 $DSH_HOME/settings.yaml 为该模型补上 input: [text, image],或为整个提供方补上 defaultInput: [text, image]。DSH 当前 Web 表单不会写入这个字段。',
|
|
87
|
+
visionCapsRetry: '重新检测模型',
|
|
80
88
|
chainInvalidCurrent: '当前保存的视觉后端不支持图片或无法验证,已从下拉列表隐藏,运行时也会跳过:',
|
|
81
89
|
retryCatalog: '重试加载目录',
|
|
82
90
|
advanced: '高级设置',
|
|
@@ -98,6 +106,8 @@ window.__ModuleLoader__.load({
|
|
|
98
106
|
testConnecting: '测试中…',
|
|
99
107
|
testOk: '连接正常',
|
|
100
108
|
testFailed: '连接失败',
|
|
109
|
+
openLogFolder: '打开日志文件夹',
|
|
110
|
+
openLogFolderFailed: '无法打开日志文件夹',
|
|
101
111
|
updateTitle: '版本更新',
|
|
102
112
|
checkUpdate: '检查更新',
|
|
103
113
|
updateChecking: '检查中…',
|
|
@@ -248,6 +258,14 @@ window.__ModuleLoader__.load({
|
|
|
248
258
|
visionCapsLoading: 'Checking which models genuinely accept image input…',
|
|
249
259
|
visionCapsError: 'Vision capability metadata is unavailable; user vision-model choices are hidden to prevent bad selections. The built-in OVH fallback still works.',
|
|
250
260
|
visionCapsFiltered: 'The vision-backend dropdown only shows models that explicitly declare image input.',
|
|
261
|
+
visionCapsEmptyTitle: 'No selectable user vision models',
|
|
262
|
+
visionCapsEmptyBody: 'DSH reported {count} user models, but none are explicitly marked as accepting images, so Vision Router hid them safely.',
|
|
263
|
+
visionCapsHiddenPrefix: 'Hidden models:',
|
|
264
|
+
visionCapsReasonMissingImage: 'image input not declared',
|
|
265
|
+
visionCapsReasonUnverified: 'image capability could not be verified',
|
|
266
|
+
visionCapsHiddenMore: '{count} more models not shown',
|
|
267
|
+
visionCapsMissingImageHint: 'If one of these is a vision model you just added through Settings → Models → Add custom provider, add input: [text, image] to that model in $DSH_HOME/settings.yaml, or defaultInput: [text, image] to the provider. The current DSH Web form does not write this field.',
|
|
268
|
+
visionCapsRetry: 'Re-detect models',
|
|
251
269
|
chainInvalidCurrent: 'This saved vision backend does not support images or could not be verified. It is hidden from the dropdown and skipped at runtime: ',
|
|
252
270
|
retryCatalog: 'Retry catalog',
|
|
253
271
|
advanced: 'Advanced settings',
|
|
@@ -269,6 +287,8 @@ window.__ModuleLoader__.load({
|
|
|
269
287
|
testConnecting: 'Testing…',
|
|
270
288
|
testOk: 'Connected',
|
|
271
289
|
testFailed: 'Connection failed',
|
|
290
|
+
openLogFolder: 'Open logs folder',
|
|
291
|
+
openLogFolderFailed: 'Could not open logs folder',
|
|
272
292
|
updateTitle: 'Updates',
|
|
273
293
|
checkUpdate: 'Check for updates',
|
|
274
294
|
updateChecking: 'Checking…',
|
|
@@ -385,9 +405,6 @@ window.__ModuleLoader__.load({
|
|
|
385
405
|
.map((group) => {
|
|
386
406
|
const models = (group && Array.isArray(group.models) ? group.models : []).filter((model) => {
|
|
387
407
|
if (!model || typeof model.id !== 'string') return false
|
|
388
|
-
// The built-in backend is defined by this plugin and always
|
|
389
|
-
// declares image input. Keeping it visible while the capability
|
|
390
|
-
// request is still loading avoids a blank default editor.
|
|
391
408
|
if (group.id === 'vision-http') return false
|
|
392
409
|
return !!(caps[group.id] && caps[group.id][model.id] && caps[group.id][model.id].image === true)
|
|
393
410
|
})
|
|
@@ -396,6 +413,33 @@ window.__ModuleLoader__.load({
|
|
|
396
413
|
.filter((group) => group && group.models.length > 0)
|
|
397
414
|
}
|
|
398
415
|
|
|
416
|
+
function collectFilteredVisionBackends(groups, capabilities) {
|
|
417
|
+
const caps = capabilities && typeof capabilities === 'object' ? capabilities : {}
|
|
418
|
+
const hidden = []
|
|
419
|
+
for (const group of Array.isArray(groups) ? groups : []) {
|
|
420
|
+
if (
|
|
421
|
+
!group ||
|
|
422
|
+
typeof group.id !== 'string' ||
|
|
423
|
+
group.id === 'vision-http' ||
|
|
424
|
+
group.id === 'vision-chain' ||
|
|
425
|
+
group.id.endsWith('-vision')
|
|
426
|
+
) continue
|
|
427
|
+
for (const model of Array.isArray(group.models) ? group.models : []) {
|
|
428
|
+
if (!model || typeof model.id !== 'string' || model.id === '') continue
|
|
429
|
+
const capability = caps[group.id] && caps[group.id][model.id]
|
|
430
|
+
if (capability && capability.image === true) continue
|
|
431
|
+
hidden.push({
|
|
432
|
+
provider: group.id,
|
|
433
|
+
model: model.id,
|
|
434
|
+
reason: capability && typeof capability.reason === 'string' ? capability.reason : undefined,
|
|
435
|
+
missingImageDeclaration:
|
|
436
|
+
!!capability && capability.reason === 'model metadata does not declare image input',
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return hidden
|
|
441
|
+
}
|
|
442
|
+
|
|
399
443
|
// ── field specs ──────────────────────────────────────────────────────────
|
|
400
444
|
const TOGGLE_KEYS = ['routing', 'tool', 'autoWrapProviders', 'stealth']
|
|
401
445
|
const ADVANCED_TOGGLE_KEYS = ['reverseRouting', 'rewriteImages', 'downscale', 'cache', 'freeFallback']
|
|
@@ -495,6 +539,10 @@ window.__ModuleLoader__.load({
|
|
|
495
539
|
'.vr-chain-row{display:flex;align-items:center;gap:8px;margin:6px 0}' +
|
|
496
540
|
'.vr-stealth-notice{color:var(--dsw-alias-label-warning,var(--dsw-alias-label-secondary))}' +
|
|
497
541
|
'.vr-catalog-error{display:flex;align-items:center;gap:10px;flex-wrap:wrap}' +
|
|
542
|
+
'.vr-vision-empty{margin:8px 0 2px;padding:11px 12px;border:1px solid var(--dsw-alias-label-warning,var(--dsw-alias-border-l2));border-radius:9px;background:var(--dsw-alias-bg-module-platform);display:flex;flex-direction:column;gap:7px}' +
|
|
543
|
+
'.vr-vision-empty-title{font-size:12px;font-weight:650;color:var(--dsw-alias-label-primary);margin:0}' +
|
|
544
|
+
'.vr-vision-empty-list{margin:0;padding-left:18px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;word-break:break-word}' +
|
|
545
|
+
'.vr-vision-empty-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}' +
|
|
498
546
|
'.vr-subheader{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:8px 0;border:none;background:none;cursor:pointer;font:inherit;color:var(--dsw-alias-label-primary);text-align:left}' +
|
|
499
547
|
'.vr-group{content-visibility:auto;contain-intrinsic-size:auto 96px;border-top:1px solid var(--dsw-alias-border-l2);padding:10px 0 2px;display:flex;flex-direction:column;gap:8px}' +
|
|
500
548
|
'.vr-group-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary);margin:0}' +
|
|
@@ -554,20 +602,116 @@ window.__ModuleLoader__.load({
|
|
|
554
602
|
}
|
|
555
603
|
|
|
556
604
|
const ONBOARDING_STORAGE_KEY = 'dsh-vision-router:onboarding:model-guide-v2'
|
|
605
|
+
// issue #78: DSH Desktop serves the Web UI from a random port on every
|
|
606
|
+
// launch (--port 0), so origin-scoped localStorage forgets everything
|
|
607
|
+
// between restarts and the first-run dialog re-appeared on every boot.
|
|
608
|
+
// The durable source of truth is now the `vision-router` settings section
|
|
609
|
+
// (the profile settings file, synced through ctx.settingsScope);
|
|
610
|
+
// localStorage survives only as a legacy/migration fallback for
|
|
611
|
+
// pre-v1.2.3 browsers and as a best-effort downgrade channel.
|
|
612
|
+
const ONBOARDING_SETTINGS_KEY = 'onboardingSeen'
|
|
557
613
|
// The walkthrough has three steps: step1 (the session/text model selector
|
|
558
614
|
// on the chat page), step2 (open Settings → Plugins → Vision Router), and
|
|
559
615
|
// step3 (the highlighted vision chain, rendered by the settings card as a
|
|
560
616
|
// callout). Only step1/step2 need persistence; step3 re-derives from the
|
|
561
617
|
// card being on screen.
|
|
562
618
|
const VISION_GUIDE_STORAGE_KEY = 'dsh-vision-router:guide:vision-backend-v2'
|
|
619
|
+
const VISION_GUIDE_SETTINGS_KEY = 'visionGuideStep'
|
|
563
620
|
const VISION_GUIDE_EVENT = 'dsh-vision-router:vision-settings-guide'
|
|
564
621
|
let visionGuidePrompt
|
|
565
622
|
let visionGuideStepMemory
|
|
623
|
+
// Installed by apply(): a narrow, defensive view of the settings scope.
|
|
624
|
+
// { get, set, unset } never throw, and subscribe returns a disposer (or
|
|
625
|
+
// undefined when the scope is unavailable — the legacy paths take over).
|
|
626
|
+
let settingsPersistence
|
|
627
|
+
function installSettingsPersistence(scope) {
|
|
628
|
+
const readSection = () => {
|
|
629
|
+
try {
|
|
630
|
+
const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
|
|
631
|
+
return snapshot && snapshot.value ? snapshot.value : undefined
|
|
632
|
+
} catch {
|
|
633
|
+
return undefined
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
settingsPersistence = {
|
|
637
|
+
get(field) {
|
|
638
|
+
const section = readSection()
|
|
639
|
+
return section ? section[field] : undefined
|
|
640
|
+
},
|
|
641
|
+
set(field, value) {
|
|
642
|
+
try {
|
|
643
|
+
const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
|
|
644
|
+
if (!snapshot || !snapshot.writable || typeof scope.set !== 'function') return
|
|
645
|
+
void scope.set(field, value)
|
|
646
|
+
} catch {
|
|
647
|
+
// Read-only provider or unavailable scope: the localStorage
|
|
648
|
+
// fallback keeps this load functional.
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
unset(field) {
|
|
652
|
+
try {
|
|
653
|
+
const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
|
|
654
|
+
if (!snapshot || !snapshot.writable || typeof scope.unset !== 'function') return
|
|
655
|
+
void scope.unset(field)
|
|
656
|
+
} catch {
|
|
657
|
+
// Same fallback reasoning as set().
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
subscribe(listener) {
|
|
661
|
+
try {
|
|
662
|
+
return scope && typeof scope.subscribe === 'function' ? scope.subscribe(listener) : undefined
|
|
663
|
+
} catch {
|
|
664
|
+
return undefined
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function readOnboardingSeen() {
|
|
671
|
+
try {
|
|
672
|
+
if (settingsPersistence && settingsPersistence.get(ONBOARDING_SETTINGS_KEY) === true) return true
|
|
673
|
+
} catch {
|
|
674
|
+
// Fall through to the legacy origin-scoped marker.
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
if (window.localStorage && window.localStorage.getItem(ONBOARDING_STORAGE_KEY) === 'seen') {
|
|
678
|
+
// Legacy value from before the settings-backed flag: migrate it so
|
|
679
|
+
// it survives desktop restarts, then honor it for this load.
|
|
680
|
+
if (settingsPersistence) settingsPersistence.set(ONBOARDING_SETTINGS_KEY, true)
|
|
681
|
+
return true
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
// Best effort only.
|
|
685
|
+
}
|
|
686
|
+
return false
|
|
687
|
+
}
|
|
688
|
+
function rememberOnboardingSeen() {
|
|
689
|
+
try {
|
|
690
|
+
if (settingsPersistence) settingsPersistence.set(ONBOARDING_SETTINGS_KEY, true)
|
|
691
|
+
} catch {
|
|
692
|
+
// The dialog can still be dismissed for this page load.
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
if (window.localStorage) window.localStorage.setItem(ONBOARDING_STORAGE_KEY, 'seen')
|
|
696
|
+
} catch {
|
|
697
|
+
// Best effort: the dialog can still be dismissed for this page load.
|
|
698
|
+
}
|
|
699
|
+
}
|
|
566
700
|
|
|
567
701
|
function readVisionGuideStep() {
|
|
568
702
|
try {
|
|
569
|
-
const value =
|
|
703
|
+
const value = settingsPersistence && settingsPersistence.get(VISION_GUIDE_SETTINGS_KEY)
|
|
570
704
|
if (value === 'step1' || value === 'step2') return value
|
|
705
|
+
} catch {
|
|
706
|
+
// Fall through to the legacy origin-scoped marker.
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
const value = window.localStorage && window.localStorage.getItem(VISION_GUIDE_STORAGE_KEY)
|
|
710
|
+
if (value === 'step1' || value === 'step2') {
|
|
711
|
+
// Legacy value: migrate into settings for desktop durability.
|
|
712
|
+
if (settingsPersistence) settingsPersistence.set(VISION_GUIDE_SETTINGS_KEY, value)
|
|
713
|
+
return value
|
|
714
|
+
}
|
|
571
715
|
} catch {
|
|
572
716
|
// Fall through to page-memory state when storage access is blocked.
|
|
573
717
|
}
|
|
@@ -575,6 +719,14 @@ window.__ModuleLoader__.load({
|
|
|
575
719
|
}
|
|
576
720
|
function writeVisionGuideStep(step) {
|
|
577
721
|
visionGuideStepMemory = step === 'step1' || step === 'step2' ? step : undefined
|
|
722
|
+
try {
|
|
723
|
+
if (settingsPersistence) {
|
|
724
|
+
if (visionGuideStepMemory) settingsPersistence.set(VISION_GUIDE_SETTINGS_KEY, visionGuideStepMemory)
|
|
725
|
+
else settingsPersistence.unset(VISION_GUIDE_SETTINGS_KEY)
|
|
726
|
+
}
|
|
727
|
+
} catch {
|
|
728
|
+
// Page-memory state still keeps the guide functional for this load.
|
|
729
|
+
}
|
|
578
730
|
try {
|
|
579
731
|
if (!window.localStorage) return
|
|
580
732
|
if (visionGuideStepMemory) window.localStorage.setItem(VISION_GUIDE_STORAGE_KEY, visionGuideStepMemory)
|
|
@@ -677,9 +829,13 @@ window.__ModuleLoader__.load({
|
|
|
677
829
|
syncVisionGuidePrompt(t)
|
|
678
830
|
window.addEventListener(VISION_GUIDE_EVENT, sync)
|
|
679
831
|
window.addEventListener('popstate', sync)
|
|
832
|
+
// The durable step now lives in the settings section; re-sync when its
|
|
833
|
+
// snapshot changes (e.g. the first read resolves after page load).
|
|
834
|
+
const unsubscribe = settingsPersistence && settingsPersistence.subscribe(sync)
|
|
680
835
|
return () => {
|
|
681
836
|
window.removeEventListener(VISION_GUIDE_EVENT, sync)
|
|
682
837
|
window.removeEventListener('popstate', sync)
|
|
838
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
683
839
|
removeVisionGuidePrompt()
|
|
684
840
|
}
|
|
685
841
|
}
|
|
@@ -687,13 +843,6 @@ window.__ModuleLoader__.load({
|
|
|
687
843
|
let onboardingOverlay
|
|
688
844
|
let onboardingKeyDown
|
|
689
845
|
|
|
690
|
-
function rememberOnboardingSeen() {
|
|
691
|
-
try {
|
|
692
|
-
if (window.localStorage) window.localStorage.setItem(ONBOARDING_STORAGE_KEY, 'seen')
|
|
693
|
-
} catch {
|
|
694
|
-
// Best effort: the dialog can still be dismissed for this page load.
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
846
|
function dismissOnboarding(remember = true) {
|
|
698
847
|
if (remember) rememberOnboardingSeen()
|
|
699
848
|
if (typeof document !== 'undefined' && onboardingKeyDown) {
|
|
@@ -779,16 +928,33 @@ window.__ModuleLoader__.load({
|
|
|
779
928
|
}
|
|
780
929
|
function installOnboarding(t) {
|
|
781
930
|
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
931
|
+
let timer
|
|
932
|
+
const clearTimer = () => {
|
|
933
|
+
if (timer !== undefined) window.clearTimeout(timer)
|
|
934
|
+
timer = undefined
|
|
786
935
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
936
|
+
// Reactive against the settings scope: the settings snapshot arrives
|
|
937
|
+
// asynchronously on a cold page load, so a one-shot localStorage check
|
|
938
|
+
// could show the dialog before the durable "seen" flag lands. Re-sync
|
|
939
|
+
// on every snapshot change and dismiss the overlay if the flag appears.
|
|
940
|
+
const sync = () => {
|
|
941
|
+
if (readOnboardingSeen()) {
|
|
942
|
+
clearTimer()
|
|
943
|
+
if (onboardingOverlay) dismissOnboarding(false)
|
|
944
|
+
return
|
|
945
|
+
}
|
|
946
|
+
if (timer === undefined && !onboardingOverlay) {
|
|
947
|
+
timer = window.setTimeout(() => {
|
|
948
|
+
timer = undefined
|
|
949
|
+
if (!readOnboardingSeen()) showOnboarding(t)
|
|
950
|
+
}, 650)
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
sync()
|
|
954
|
+
const unsubscribe = settingsPersistence && settingsPersistence.subscribe(sync)
|
|
790
955
|
return () => {
|
|
791
|
-
|
|
956
|
+
clearTimer()
|
|
957
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
792
958
|
// Unmounting is not a user choice: drop the dialog without marking
|
|
793
959
|
// the guide seen, so the first-run overview can still auto-open.
|
|
794
960
|
if (onboardingOverlay) dismissOnboarding(false)
|
|
@@ -867,6 +1033,8 @@ window.__ModuleLoader__.load({
|
|
|
867
1033
|
const [visionCaps, setVisionCaps] = useState({ status: 'idle', capabilities: {}, builtinFallback: [], anonymousRpmPerModel: 2, error: undefined })
|
|
868
1034
|
const catalogReady = catalog.status === 'ready' && catalog.groups.length > 0
|
|
869
1035
|
const visionGroups = filterVisionBackendGroups(catalog.groups, visionCaps.capabilities)
|
|
1036
|
+
const hiddenVisionBackends =
|
|
1037
|
+
visionCaps.status === 'ready' ? collectFilteredVisionBackends(catalog.groups, visionCaps.capabilities) : []
|
|
870
1038
|
const visionModelsFor = (providerId) => {
|
|
871
1039
|
const group = visionGroups.find((entry) => entry.id === providerId)
|
|
872
1040
|
return group && Array.isArray(group.models) ? group.models : []
|
|
@@ -895,8 +1063,8 @@ window.__ModuleLoader__.load({
|
|
|
895
1063
|
}, 220)
|
|
896
1064
|
return () => window.clearTimeout(timer)
|
|
897
1065
|
}, [guideActive, open, catalog.status, visionCaps.status])
|
|
898
|
-
const loadCatalog = () => {
|
|
899
|
-
if (catalog.status === 'loading' || catalog.status === 'ready') return
|
|
1066
|
+
const loadCatalog = (force = false) => {
|
|
1067
|
+
if (!force && (catalog.status === 'loading' || catalog.status === 'ready')) return
|
|
900
1068
|
setCatalog({ status: 'loading', groups: [], error: undefined })
|
|
901
1069
|
try {
|
|
902
1070
|
const connection = props.getConnection ? props.getConnection() : undefined
|
|
@@ -943,8 +1111,8 @@ window.__ModuleLoader__.load({
|
|
|
943
1111
|
})
|
|
944
1112
|
}
|
|
945
1113
|
}
|
|
946
|
-
const loadVisionCapabilities = () => {
|
|
947
|
-
if (visionCaps.status === 'loading' || visionCaps.status === 'ready') return
|
|
1114
|
+
const loadVisionCapabilities = (force = false) => {
|
|
1115
|
+
if (!force && (visionCaps.status === 'loading' || visionCaps.status === 'ready')) return
|
|
948
1116
|
setVisionCaps({ status: 'loading', capabilities: {}, builtinFallback: [], anonymousRpmPerModel: 2, error: undefined })
|
|
949
1117
|
fetch('/_dsh/vision-router/model-capabilities')
|
|
950
1118
|
.then(async (response) => {
|
|
@@ -973,6 +1141,12 @@ window.__ModuleLoader__.load({
|
|
|
973
1141
|
}),
|
|
974
1142
|
)
|
|
975
1143
|
}
|
|
1144
|
+
const retryVisionModels = () => {
|
|
1145
|
+
setCatalog({ status: 'idle', groups: [], error: undefined })
|
|
1146
|
+
setVisionCaps({ status: 'idle', capabilities: {}, builtinFallback: [], anonymousRpmPerModel: 2, error: undefined })
|
|
1147
|
+
loadCatalog(true)
|
|
1148
|
+
loadVisionCapabilities(true)
|
|
1149
|
+
}
|
|
976
1150
|
let snapshot
|
|
977
1151
|
let renderError
|
|
978
1152
|
try {
|
|
@@ -1413,6 +1587,37 @@ window.__ModuleLoader__.load({
|
|
|
1413
1587
|
h('p', { className: 'vr-hint' }, t('chainHint')),
|
|
1414
1588
|
)
|
|
1415
1589
|
}
|
|
1590
|
+
const emptyVisionModelsPanel = () => {
|
|
1591
|
+
if (!(catalogReady && visionCaps.status === 'ready' && visionGroups.length === 0 && hiddenVisionBackends.length > 0)) {
|
|
1592
|
+
return null
|
|
1593
|
+
}
|
|
1594
|
+
const preview = hiddenVisionBackends.slice(0, 8)
|
|
1595
|
+
const remaining = hiddenVisionBackends.length - preview.length
|
|
1596
|
+
const hasMissingDeclaration = hiddenVisionBackends.some((entry) => entry.missingImageDeclaration)
|
|
1597
|
+
return h('div', { className: 'vr-vision-empty' },
|
|
1598
|
+
h('p', { className: 'vr-vision-empty-title' }, t('visionCapsEmptyTitle')),
|
|
1599
|
+
h('p', { className: 'vr-hint' }, t('visionCapsEmptyBody', { count: hiddenVisionBackends.length })),
|
|
1600
|
+
h('p', { className: 'vr-hint' }, t('visionCapsHiddenPrefix')),
|
|
1601
|
+
h('ul', { className: 'vr-vision-empty-list' },
|
|
1602
|
+
preview.map((entry) =>
|
|
1603
|
+
h('li', { key: entry.provider + '/' + entry.model },
|
|
1604
|
+
entry.provider + '/' + entry.model + ' — ' +
|
|
1605
|
+
t(entry.missingImageDeclaration ? 'visionCapsReasonMissingImage' : 'visionCapsReasonUnverified')),
|
|
1606
|
+
),
|
|
1607
|
+
remaining > 0 ? h('li', { key: 'more' }, t('visionCapsHiddenMore', { count: remaining })) : null,
|
|
1608
|
+
),
|
|
1609
|
+
hasMissingDeclaration
|
|
1610
|
+
? h('p', { className: 'vr-hint vr-stealth-notice' }, t('visionCapsMissingImageHint'))
|
|
1611
|
+
: null,
|
|
1612
|
+
h('div', { className: 'vr-vision-empty-actions' },
|
|
1613
|
+
h('button', {
|
|
1614
|
+
type: 'button', className: 'vr-btn',
|
|
1615
|
+
disabled: catalog.status === 'loading' || visionCaps.status === 'loading',
|
|
1616
|
+
onClick: retryVisionModels,
|
|
1617
|
+
}, t('visionCapsRetry')),
|
|
1618
|
+
),
|
|
1619
|
+
)
|
|
1620
|
+
}
|
|
1416
1621
|
const builtinFallbackPanel = () => {
|
|
1417
1622
|
const list = Array.isArray(visionCaps.builtinFallback) ? visionCaps.builtinFallback : []
|
|
1418
1623
|
const enabled = format('freeFallback') !== false
|
|
@@ -1708,6 +1913,7 @@ window.__ModuleLoader__.load({
|
|
|
1708
1913
|
: visionCaps.status === 'ready'
|
|
1709
1914
|
? h('p', { className: 'vr-hint' }, t('visionCapsFiltered'))
|
|
1710
1915
|
: null,
|
|
1916
|
+
emptyVisionModelsPanel(),
|
|
1711
1917
|
catalogReady
|
|
1712
1918
|
? chainEditor()
|
|
1713
1919
|
: h('div', {
|
|
@@ -1781,6 +1987,28 @@ window.__ModuleLoader__.load({
|
|
|
1781
1987
|
? `${t('testOk')}(${typeof testState.result.latencyMs === 'number' ? testState.result.latencyMs + 'ms' : 'ok'})`
|
|
1782
1988
|
: `${t('testFailed')}:${testState.result && testState.result.error ? testState.result.error : 'unknown'}`)
|
|
1783
1989
|
: null,
|
|
1990
|
+
h('button', {
|
|
1991
|
+
type: 'button', className: 'vr-btn',
|
|
1992
|
+
onClick: async () => {
|
|
1993
|
+
try {
|
|
1994
|
+
const response = await fetch('/_dsh/vision-router/logs', {
|
|
1995
|
+
method: 'POST',
|
|
1996
|
+
cache: 'no-store',
|
|
1997
|
+
})
|
|
1998
|
+
const result = await response.json().catch(() => undefined)
|
|
1999
|
+
if (!response.ok || !result || result.ok !== true) {
|
|
2000
|
+
throw new Error(result && result.error ? result.error : `HTTP ${response.status}`)
|
|
2001
|
+
}
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
if (typeof window.alert === 'function') {
|
|
2004
|
+
window.alert(
|
|
2005
|
+
t('openLogFolderFailed') + ':' +
|
|
2006
|
+
(error && error.message ? error.message : String(error)),
|
|
2007
|
+
)
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
},
|
|
2011
|
+
}, t('openLogFolder')),
|
|
1784
2012
|
h('button', {
|
|
1785
2013
|
type: 'button', className: 'vr-btn', disabled: testState.status === 'running',
|
|
1786
2014
|
onClick: runTestConnection,
|
|
@@ -1894,6 +2122,10 @@ window.__ModuleLoader__.load({
|
|
|
1894
2122
|
|
|
1895
2123
|
function apply(ctx) {
|
|
1896
2124
|
const scope = ctx.settingsScope.bind({ namespace: 'vision-router' })
|
|
2125
|
+
// issue #78: route the onboarding/guide durability through the settings
|
|
2126
|
+
// section (profile settings file) so it survives DSH Desktop's random
|
|
2127
|
+
// per-launch port. Installed before the effects below run.
|
|
2128
|
+
installSettingsPersistence(scope)
|
|
1897
2129
|
const presentedImageUrls = new Map()
|
|
1898
2130
|
const createdImageUrls = new Set()
|
|
1899
2131
|
const loadPresentedImage = (sessionId, attachment) => {
|
|
@@ -2019,12 +2251,12 @@ window.__ModuleLoader__.load({
|
|
|
2019
2251
|
() =>
|
|
2020
2252
|
ctx.slots.inject('tool.call.toolview', function* () {
|
|
2021
2253
|
yield ctx.slots.register(
|
|
2022
|
-
{ name: 'tool.call.toolview', key: 'vision_present', inject: () => ({}) },
|
|
2254
|
+
{ name: 'tool.call.toolview', key: 'vision_present', priority: -10, inject: () => ({}) },
|
|
2023
2255
|
VisionPresentCard,
|
|
2024
2256
|
)
|
|
2025
2257
|
for (const key of ARTIFACT_TOOL_KEYS) {
|
|
2026
2258
|
yield ctx.slots.register(
|
|
2027
|
-
{ name: 'tool.call.toolview', key, inject: () => ({}) },
|
|
2259
|
+
{ name: 'tool.call.toolview', key, priority: -10, inject: () => ({}) },
|
|
2028
2260
|
ArtifactCard,
|
|
2029
2261
|
)
|
|
2030
2262
|
}
|
|
@@ -2037,6 +2269,7 @@ window.__ModuleLoader__.load({
|
|
|
2037
2269
|
exports.inject = ['settingsScope', 'slots', 'locale', 'sessions']
|
|
2038
2270
|
exports.unwrapModelsResult = unwrapModelsResult
|
|
2039
2271
|
exports.filterVisionBackendGroups = filterVisionBackendGroups
|
|
2272
|
+
exports.collectFilteredVisionBackends = collectFilteredVisionBackends
|
|
2040
2273
|
return module.exports
|
|
2041
2274
|
},
|
|
2042
2275
|
})
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { appendFile, mkdir, rename, rm, stat } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { execFile } from 'node:child_process'
|
|
5
|
+
import { format as formatArgs, promisify } from 'node:util'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { resolveDshHome } from './doctor.js'
|
|
8
|
+
|
|
9
|
+
export const LOG_FILE_NAME = 'vision-router.log'
|
|
10
|
+
export const LOG_BACKUP_FILE_NAME = 'vision-router.1.log'
|
|
11
|
+
export const DEFAULT_LOG_MAX_BYTES = 2 * 1024 * 1024
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile)
|
|
14
|
+
const installs = new WeakMap()
|
|
15
|
+
|
|
16
|
+
export function resolveVisionRouterLogPaths(dshHome = resolveDshHome()) {
|
|
17
|
+
const directory = path.join(dshHome, 'logs', 'vision-router')
|
|
18
|
+
return {
|
|
19
|
+
directory,
|
|
20
|
+
file: path.join(directory, LOG_FILE_NAME),
|
|
21
|
+
backup: path.join(directory, LOG_BACKUP_FILE_NAME),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Diagnostics are intended to be shareable in bug reports. Existing runtime
|
|
27
|
+
* messages should not contain secrets, but redact common credential shapes as
|
|
28
|
+
* defense in depth before anything reaches disk.
|
|
29
|
+
*/
|
|
30
|
+
export function sanitizeLogText(value) {
|
|
31
|
+
let text = String(value ?? '')
|
|
32
|
+
text = text.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, 'Bearer [REDACTED]')
|
|
33
|
+
text = text.replace(/\bsk-(?:proj-)?[A-Za-z0-9_-]{8,}\b/g, '[REDACTED_KEY]')
|
|
34
|
+
text = text.replace(
|
|
35
|
+
/([?&](?:api[_-]?key|access[_-]?token|token|key|auth)=)[^&\s]+/gi,
|
|
36
|
+
'$1[REDACTED]',
|
|
37
|
+
)
|
|
38
|
+
text = text.replace(
|
|
39
|
+
/\b(authorization|api[_-]?key|access[_-]?token)\s*[:=]\s*["']?[^\s"',}]+/gi,
|
|
40
|
+
'$1=[REDACTED]',
|
|
41
|
+
)
|
|
42
|
+
return text
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createFileLogSink({
|
|
46
|
+
file,
|
|
47
|
+
backup,
|
|
48
|
+
maxBytes = DEFAULT_LOG_MAX_BYTES,
|
|
49
|
+
onError = () => {},
|
|
50
|
+
} = {}) {
|
|
51
|
+
let queue = Promise.resolve()
|
|
52
|
+
let initialized = false
|
|
53
|
+
let size = 0
|
|
54
|
+
let disabled = false
|
|
55
|
+
let reportedError = false
|
|
56
|
+
|
|
57
|
+
const reportError = (error) => {
|
|
58
|
+
if (reportedError) return
|
|
59
|
+
reportedError = true
|
|
60
|
+
try {
|
|
61
|
+
onError(error)
|
|
62
|
+
} catch {
|
|
63
|
+
// A diagnostics failure must never affect the plugin runtime.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const prepare = async () => {
|
|
68
|
+
if (initialized || disabled) return
|
|
69
|
+
await mkdir(path.dirname(file), { recursive: true })
|
|
70
|
+
try {
|
|
71
|
+
size = (await stat(file)).size
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error && error.code !== 'ENOENT') throw error
|
|
74
|
+
size = 0
|
|
75
|
+
}
|
|
76
|
+
initialized = true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rotate = async () => {
|
|
80
|
+
try {
|
|
81
|
+
await rm(backup, { force: true })
|
|
82
|
+
await rename(file, backup)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error && error.code !== 'ENOENT') throw error
|
|
85
|
+
}
|
|
86
|
+
size = 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const write = (level, args) => {
|
|
90
|
+
if (disabled) return queue
|
|
91
|
+
const rendered = sanitizeLogText(formatArgs(...args))
|
|
92
|
+
const line = `[${new Date().toISOString()}] [${String(level).toUpperCase()}] ${rendered}\n`
|
|
93
|
+
const bytes = Buffer.byteLength(line)
|
|
94
|
+
queue = queue.then(async () => {
|
|
95
|
+
try {
|
|
96
|
+
await prepare()
|
|
97
|
+
if (size > 0 && size + bytes > maxBytes) await rotate()
|
|
98
|
+
await appendFile(file, line, { encoding: 'utf8', mode: 0o600 })
|
|
99
|
+
size += bytes
|
|
100
|
+
} catch (error) {
|
|
101
|
+
disabled = true
|
|
102
|
+
reportError(error)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
return queue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
write,
|
|
110
|
+
flush: () => queue,
|
|
111
|
+
get disabled() {
|
|
112
|
+
return disabled
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function packageVersion() {
|
|
118
|
+
try {
|
|
119
|
+
const require = createRequire(import.meta.url)
|
|
120
|
+
const manifest = require('../package.json')
|
|
121
|
+
return typeof manifest?.version === 'string' ? manifest.version : 'unknown'
|
|
122
|
+
} catch {
|
|
123
|
+
return 'unknown'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function teeLogger(baseLogger, sink) {
|
|
128
|
+
const levels = new Set(['debug', 'info', 'warn', 'error'])
|
|
129
|
+
return new Proxy(baseLogger ?? {}, {
|
|
130
|
+
get(target, property) {
|
|
131
|
+
if (levels.has(property)) {
|
|
132
|
+
return (...args) => {
|
|
133
|
+
const method = target && typeof target[property] === 'function' ? target[property] : undefined
|
|
134
|
+
if (method) {
|
|
135
|
+
try {
|
|
136
|
+
method.apply(target, args)
|
|
137
|
+
} catch {
|
|
138
|
+
// Keep the file logger from changing host logger semantics.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
void sink.write(property, args)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const value = Reflect.get(target, property, target)
|
|
145
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function contextWithLogger(ctx, logger) {
|
|
151
|
+
return new Proxy(ctx, {
|
|
152
|
+
get(target, property) {
|
|
153
|
+
if (property === 'logger') return logger
|
|
154
|
+
const value = Reflect.get(target, property, target)
|
|
155
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function openLogDirectory(directory, {
|
|
161
|
+
platform = process.platform,
|
|
162
|
+
exec = execFileAsync,
|
|
163
|
+
} = {}) {
|
|
164
|
+
await mkdir(directory, { recursive: true })
|
|
165
|
+
if (platform === 'darwin') {
|
|
166
|
+
await exec('open', [directory], { timeout: 10_000, windowsHide: true })
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
if (platform === 'win32') {
|
|
170
|
+
await exec('explorer.exe', [directory], { timeout: 10_000, windowsHide: true })
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
await exec('xdg-open', [directory], { timeout: 10_000, windowsHide: true })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sameOrigin(req) {
|
|
177
|
+
const origin = req?.headers?.origin
|
|
178
|
+
const host = req?.headers?.host
|
|
179
|
+
const fetchSite = req?.headers?.['sec-fetch-site']
|
|
180
|
+
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'none') return false
|
|
181
|
+
if (!origin) return true
|
|
182
|
+
if (!host) return false
|
|
183
|
+
try {
|
|
184
|
+
return new URL(origin).host === host
|
|
185
|
+
} catch {
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function sendJson(res, status, body) {
|
|
191
|
+
res.writeHead(status, {
|
|
192
|
+
'content-type': 'application/json; charset=utf-8',
|
|
193
|
+
'cache-control': 'no-store',
|
|
194
|
+
})
|
|
195
|
+
res.end(JSON.stringify(body))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function installLogRoute(ctx, paths) {
|
|
199
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
200
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
201
|
+
kind: 'exact',
|
|
202
|
+
path: '/_dsh/vision-router/logs',
|
|
203
|
+
handler: async (req, res) => {
|
|
204
|
+
if (req.method === 'GET') {
|
|
205
|
+
sendJson(res, 200, { ok: true, directory: paths.directory, file: paths.file })
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
if (req.method !== 'POST') {
|
|
209
|
+
res.setHeader('Allow', 'GET, POST')
|
|
210
|
+
sendJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
if (!sameOrigin(req)) {
|
|
214
|
+
sendJson(res, 403, { ok: false, error: 'cross-origin request rejected' })
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await openLogDirectory(paths.directory)
|
|
219
|
+
sendJson(res, 200, { ok: true, directory: paths.directory, file: paths.file })
|
|
220
|
+
} catch (error) {
|
|
221
|
+
sendJson(res, 500, {
|
|
222
|
+
ok: false,
|
|
223
|
+
directory: paths.directory,
|
|
224
|
+
file: paths.file,
|
|
225
|
+
error: error && error.message ? error.message : String(error),
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
}), 'vision-router: diagnostics log route')
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Return a Cordis context whose logger tees this plugin's existing diagnostics
|
|
235
|
+
* to a small rotating file while preserving the host logger. The original
|
|
236
|
+
* context is otherwise untouched.
|
|
237
|
+
*/
|
|
238
|
+
export function installVisionRouterFileLogging(ctx, options = {}) {
|
|
239
|
+
if (ctx && typeof ctx === 'object' && installs.has(ctx)) return installs.get(ctx)
|
|
240
|
+
|
|
241
|
+
const paths = resolveVisionRouterLogPaths(options.dshHome)
|
|
242
|
+
const baseLogger = ctx?.logger
|
|
243
|
+
const sink = createFileLogSink({
|
|
244
|
+
file: paths.file,
|
|
245
|
+
backup: paths.backup,
|
|
246
|
+
maxBytes: options.maxBytes ?? DEFAULT_LOG_MAX_BYTES,
|
|
247
|
+
onError: (error) => {
|
|
248
|
+
try {
|
|
249
|
+
baseLogger?.warn?.(
|
|
250
|
+
'vision-router: diagnostics file logging disabled: %s',
|
|
251
|
+
error && error.message ? error.message : String(error),
|
|
252
|
+
)
|
|
253
|
+
} catch {
|
|
254
|
+
// Logging failure remains non-fatal.
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
const logger = teeLogger(baseLogger, sink)
|
|
259
|
+
const wrappedCtx = contextWithLogger(ctx, logger)
|
|
260
|
+
const installed = { ctx: wrappedCtx, logger, sink, ...paths }
|
|
261
|
+
if (ctx && typeof ctx === 'object') installs.set(ctx, installed)
|
|
262
|
+
|
|
263
|
+
installLogRoute(ctx, paths)
|
|
264
|
+
logger.info(
|
|
265
|
+
'vision-router: diagnostics log enabled at %s (plugin=%s node=%s platform=%s/%s)',
|
|
266
|
+
paths.file,
|
|
267
|
+
packageVersion(),
|
|
268
|
+
process.version,
|
|
269
|
+
process.platform,
|
|
270
|
+
process.arch,
|
|
271
|
+
)
|
|
272
|
+
return installed
|
|
273
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/ysr666/dsh-vision-router",
|
|
11
11
|
"type": "module",
|
|
12
|
-
"main": "
|
|
12
|
+
"main": "entry.js",
|
|
13
13
|
"bin": {
|
|
14
14
|
"dsh-vision-router": "./lib/doctor-cli.js"
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
|
-
".": "./
|
|
17
|
+
".": "./entry.js",
|
|
18
18
|
"./client": "./lib/client.js",
|
|
19
19
|
"./package.json": "./package.json",
|
|
20
20
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"assets",
|
|
27
27
|
"cordis.patch.yml",
|
|
28
28
|
"docs",
|
|
29
|
+
"entry.js",
|
|
29
30
|
"index.js",
|
|
30
31
|
"lib",
|
|
31
32
|
"presets"
|
|
@@ -50,18 +51,20 @@
|
|
|
50
51
|
"@deepseek-ai/schemastery": ">=3.18.0",
|
|
51
52
|
"potrace": "^2.1.8",
|
|
52
53
|
"puppeteer-core": "^25.6.0",
|
|
53
|
-
"undici": ">=6.15.0"
|
|
54
|
-
"@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.6",
|
|
55
|
-
"@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.6"
|
|
54
|
+
"undici": ">=6.15.0"
|
|
56
55
|
},
|
|
57
56
|
"peerDependencies": {
|
|
57
|
+
"@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.6",
|
|
58
59
|
"sharp": ">=0.35.3 <1"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
62
|
+
"@deepseek-ai/dsh-anonymous-user-id": "^0.1.0-rc.6",
|
|
63
|
+
"@deepseek-ai/dsh-llm-deepseek": "^0.1.0-rc.6",
|
|
61
64
|
"sharp": "^0.35.3"
|
|
62
65
|
},
|
|
63
66
|
"scripts": {
|
|
64
|
-
"test": "node --test tests/core.test.js tests/client.test.js tests/http-compat.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js"
|
|
67
|
+
"test": "node --test tests/core.test.js tests/client.test.js tests/http-compat.test.js tests/update-check.test.js tests/self-update.test.js tests/doctor.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js"
|
|
65
68
|
},
|
|
66
69
|
"pnpm": {
|
|
67
70
|
"onlyBuiltDependencies": [
|
package/presets/README.md
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
把它合并进你自己的 `settings.yaml`(或复制整个段),填一个 `apiKeyEnv` 对应的 Key 即可。
|
|
5
5
|
**仓库绝不内置任何第三方 Key**。
|
|
6
6
|
|
|
7
|
+
> [!IMPORTANT]
|
|
8
|
+
> DSH 当前的 Web「设置 → 模型 → 添加自定义提供方」表单不会写入图片输入能力元数据。即使端点实际是智谱 GLM-4.6V、Qwen-VL 等视觉模型,如果模型没有显式的 `input: [text, image]`(或提供方没有 `defaultInput: [text, image]`),DSH 仍会把它报告为纯文本模型,Vision Router 会为安全起见从视觉后端下拉中隐藏它。下面这些 preset 已经带好 `input` 声明;如果你是通过 Web 手动添加的自定义视觉提供方,请在 `$DSH_HOME/settings.yaml` 补上该字段。
|
|
9
|
+
|
|
7
10
|
- `dashscope.yaml` —— 阿里云百炼,大陆直连,新用户每系列 100 万 token/90 天(推荐首选)
|
|
8
11
|
- `zhipu.yaml` —— 智谱 bigmodel.cn,`glm-4.6v-flash` 永久免费(provider key 用 `zhipu`,避免与 pi-ai 内置目录里的 `zai` 编码端点冲突)
|
|
9
12
|
- `siliconflow.yaml` —— 硅基流动,¥14 赠金覆盖 Qwen2.5-VL
|