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