dsh-vision-router 1.5.0 → 1.5.2
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 +9 -8
- package/README.zh.md +8 -7
- package/index.js +60 -8
- package/lib/client.js +84 -18
- package/lib/update-check.js +68 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center"><strong>Paste an image and it just works — eyes for text-only agents on DeepSeek Harness. Free out of the box, no key, no Python, one command.</strong></p>
|
|
8
8
|
|
|
9
|
-
<p align="center">DeepSeek keeps thinking; the built-in free vision chain and
|
|
9
|
+
<p align="center">DeepSeek keeps thinking; the built-in free vision chain and fourteen deep tools do the seeing. Image turns behave like ordinary tool-calling turns — grounded, measurable, repeatable.</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome · DSH plugin" /></a>
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.5.
|
|
19
|
-
<a href="tests"><img src="https://img.shields.io/badge/verified-
|
|
18
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.5.2"><img src="https://img.shields.io/badge/release-v1.5.2-5B4CF0?style=flat-square" alt="Release v1.5.2" /></a>
|
|
19
|
+
<a href="tests"><img src="https://img.shields.io/badge/verified-355%20tests-2EA44F?style=flat-square" alt="Verified: 355 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>
|
|
22
22
|
<img src="https://img.shields.io/badge/runtime-no%20Python-8A2BE2?style=flat-square" alt="No Python" />
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **Announcement (v1.5.
|
|
31
|
+
> 📌 **Announcement (v1.5.2)**
|
|
32
32
|
>
|
|
33
|
-
> **v1.5.
|
|
33
|
+
> **v1.5.2:** Fixes DSH Settings keyed-slot loading.
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<img src="assets/vision-demo.gif" width="640" alt="Demo: paste an image, the agent locates the send button with vision_ground / vision_crop / vision_pixel_diff and answers with coordinates" />
|
|
@@ -190,7 +190,7 @@ Any of these channels can join the vision chain as an `httpProviders` entry (key
|
|
|
190
190
|
- **Automatic failover with classified errors.** Region blocks, ToS filtering, 402 quota, 429 rate limits, context overflow, network failures — the chain walks providers one by one and only reports after all of them failed, with actionable advice. A 429 immediately advances to the next backend and opens a Retry-After-aware cooldown instead of sleeping inside the request.
|
|
191
191
|
- **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.
|
|
192
192
|
- **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.
|
|
193
|
-
- **Stable tool schema.** All
|
|
193
|
+
- **Stable tool schema.** All fourteen 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).
|
|
194
194
|
- **Selective proxy.** Only the configured vision provider hosts go through your local proxy; DeepSeek stays direct.
|
|
195
195
|
|
|
196
196
|
### Pixel loop in practice
|
|
@@ -211,18 +211,19 @@ The vision model is **only the eyes**; DeepSeek is **always the brain**. An imag
|
|
|
211
211
|
|
|
212
212
|
## Tools
|
|
213
213
|
|
|
214
|
-
Default `progressiveTools: false`: all
|
|
214
|
+
Default `progressiveTools: false`: all fourteen 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:
|
|
215
215
|
|
|
216
216
|
<p align="center">
|
|
217
217
|
<img src="assets/vision-tools.svg" width="100%" alt="Eleven image-processing tools available in DSH Vision Router." />
|
|
218
218
|
</p>
|
|
219
219
|
|
|
220
|
-
The diagram covers the eleven image-processing tools. `vision_present` (durable image delivery) and `vision_bootstrap` (the optional 1+x structured first pass) bring the default deep-tool set to
|
|
220
|
+
The diagram covers the eleven image-processing tools. `vision_present` (durable image delivery) and `vision_bootstrap` (the optional 1+x structured first pass) bring the default deep-tool set to fourteen. Enabling the privacy-gated `vision_screenshot` at boot adds an optional fifteenth tool.
|
|
221
221
|
|
|
222
222
|
| Tool | What it does | Artifact |
|
|
223
223
|
|---|---|---|
|
|
224
224
|
| `vision_bootstrap` | Optional 1+x structured first visual pass; establishes task-independent evidence before at least one follow-up vision call | — |
|
|
225
225
|
| `vision_describe` | Image Q&A / multi-image compare / structured-evidence JSON mode (summary + layout regions + entity inventory + verbatim transcription) | — |
|
|
226
|
+
| `vision_materialize` | Copy an authorized attachment into the session workspace and return a filesystem path for local OCR/parser fallbacks; no vision/network call | image copy |
|
|
226
227
|
| `vision_ground` | Locate a target → **original-pixel box x1/y1/x2/y2** | annotated PNG (optional) |
|
|
227
228
|
| `vision_detect` | Numbered inventory of every element of a kind (buttons/inputs/links…) with original-pixel boxes | annotated PNG with numbered boxes |
|
|
228
229
|
| `vision_crop` | Crop and zoom into a pixel box | PNG |
|
package/README.zh.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center"><strong>图片粘贴即用:给 DeepSeek Harness 的纯文本 Agent 装上“眼睛”——开箱免费、免 Key、无 Python、一条命令安装。</strong></p>
|
|
8
8
|
|
|
9
|
-
<p align="center">DeepSeek 只负责思考,内置免费视觉链 +
|
|
9
|
+
<p align="center">DeepSeek 只负责思考,内置免费视觉链 + 14 个深看工具负责“看”;图片轮次就像普通工具调用一样自然、可定位、可验证。</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://awesome-dsh-plugin.com"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="awesome · DSH plugin" /></a>
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.5.
|
|
19
|
-
<a href="tests"><img src="https://img.shields.io/badge/verified-
|
|
18
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v1.5.2"><img src="https://img.shields.io/badge/release-v1.5.2-5B4CF0?style=flat-square" alt="Release v1.5.2" /></a>
|
|
19
|
+
<a href="tests"><img src="https://img.shields.io/badge/verified-355%20tests-2EA44F?style=flat-square" alt="Verified: 355 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>
|
|
22
22
|
<img src="https://img.shields.io/badge/runtime-no%20Python-8A2BE2?style=flat-square" alt="No Python" />
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **公告(v1.5.
|
|
31
|
+
> 📌 **公告(v1.5.2)**
|
|
32
32
|
>
|
|
33
|
-
> **v1.5.
|
|
33
|
+
> **v1.5.2:修复 DSH 设置页 keyed-slot 加载失败。**
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
36
36
|
<img src="assets/vision-demo.gif" width="640" alt="演示:粘贴图片,Agent 用 vision_ground / vision_crop / vision_pixel_diff 定位发送按钮并给出坐标" />
|
|
@@ -189,7 +189,7 @@ opencode-go + 自动识图 ← 发图片时选这个
|
|
|
189
189
|
- **自动降级 + 分类报错。** 地区限制、ToS 风控、402 额度、429 限流、上下文超长、网络故障——链路逐供应商尝试,全部失败才报错并给出可操作的建议。遇到 429 会立即尝试下一后端,并按 Retry-After 开启冷却,不会在单次请求内睡眠等待。
|
|
190
190
|
- **图片记忆。** 视觉答案按附件内容哈希缓存;后续文字轮用记录的描述替换历史图片(标注为不可信证据),DeepSeek 真正“记得”之前发过的图,且不重复消耗视觉调用。
|
|
191
191
|
- **可验证的像素闭环。** 参照图 → `vision_html_screenshot` → `vision_pixel_diff`(差异率 + 红色热力图 + 最差区域排行)→ 修复 → 再对比,直到差异收敛。UI 还原从“目测”变成“实测”。
|
|
192
|
-
- **稳定工具 schema。** 默认从会话开始就注册完整
|
|
192
|
+
- **稳定工具 schema。** 默认从会话开始就注册完整 14 个深看工具,避免图片轮中途扩展工具列表导致长上下文的 KV / prefix cache 失效。仍保留 `progressiveTools: true` 作为高级启动期 opt-in;开启后才使用 `vision_activate` 按需挂载。详见 [`docs/progressive-tools-cache.md`](docs/progressive-tools-cache.md)。
|
|
193
193
|
- **选择性代理。** 只有配置的视觉供应商域名走本地代理;DeepSeek 保持直连。
|
|
194
194
|
|
|
195
195
|
### 像素闭环实测
|
|
@@ -210,7 +210,7 @@ Agent 仅根据参考图复刻 UI,再用 `vision_pixel_diff` 验证最终结
|
|
|
210
210
|
|
|
211
211
|
## 工具
|
|
212
212
|
|
|
213
|
-
默认 `progressiveTools: false`:
|
|
213
|
+
默认 `progressiveTools: false`:14 个深看工具从插件启动时就保持常驻,文本轮和图片轮都可直接调用。若你在 profile / composition 的 `cordis.patch.yml` 中显式开启 `progressiveTools: true`,才会恢复渐进模式:初始只暴露 `vision_activate`,首次需要时再挂载完整工具,并注册 `vision-tools` 技能。该开关是启动期配置,修改后需重启 DSH。全部工具基于 sharp / potrace / tesseract / 系统 Chrome——无 Python:
|
|
214
214
|
|
|
215
215
|
<p align="center">
|
|
216
216
|
<img src="assets/vision-tools-zh.svg" width="100%" alt="DSH Vision Router 的 11 个图像处理工具。" />
|
|
@@ -222,6 +222,7 @@ Agent 仅根据参考图复刻 UI,再用 `vision_pixel_diff` 验证最终结
|
|
|
222
222
|
|---|---|---|
|
|
223
223
|
| `vision_bootstrap` | 可选 1+x 结构化首遍视觉识别;先建立任务无关证据底图,再至少进行 1 次后续视觉调用 | — |
|
|
224
224
|
| `vision_describe` | 看图问答 / 多图对比 / 结构化证据 JSON 模式(摘要 + 布局区域 + 实体清单 + 原文转写) | — |
|
|
225
|
+
| `vision_materialize` | 把已授权附件复制到会话工作区并返回真实文件路径,供本地 OCR/解析器降级使用;不调用视觉模型或网络 | image copy |
|
|
225
226
|
| `vision_ground` | 定位目标 → **原图像素框 x1/y1/x2/y2** | 标注 PNG(可选) |
|
|
226
227
|
| `vision_detect` | 盘点某类元素(按钮/输入框/链接…)→ 编号清单 + 原图像素框 | 编号标注 PNG |
|
|
227
228
|
| `vision_crop` | 按像素框裁剪放大 | PNG |
|
package/index.js
CHANGED
|
@@ -4947,7 +4947,8 @@ export function apply(ctx, config = {}) {
|
|
|
4947
4947
|
'question — rephrasing cannot fix an auth, rate-limit or outage problem. Answer from the information ' +
|
|
4948
4948
|
'you already have and continue the text task, telling the user vision is temporarily unavailable. ' +
|
|
4949
4949
|
'Only content-level uncertainty in a SUCCESSFUL answer justifies a second look (vision_crop, ' +
|
|
4950
|
-
'vision_ground or another vision_describe).'
|
|
4950
|
+
'vision_ground or another vision_describe). If infrastructure failure leaves a file_path-only OCR/parser as the fallback, ' +
|
|
4951
|
+
'use vision_materialize on the uploaded attachment id; never guess a same-named local file or private attachment-store path.',
|
|
4951
4952
|
parameters: {
|
|
4952
4953
|
type: 'object',
|
|
4953
4954
|
properties: {
|
|
@@ -4997,6 +4998,12 @@ export function apply(ctx, config = {}) {
|
|
|
4997
4998
|
if (paths.length + attachmentIds.length === 0 || paths.length + attachmentIds.length > 4) {
|
|
4998
4999
|
throw new Error('vision_describe: provide 1-4 images via paths and/or attachmentIds')
|
|
4999
5000
|
}
|
|
5001
|
+
// Preserve only durable upload ids for a deterministic offline fallback.
|
|
5002
|
+
// Never expose or guess the attachment store's private filesystem path.
|
|
5003
|
+
const materializableAttachmentIds = [...new Set([
|
|
5004
|
+
...attachmentIds.map((id) => String(id)).filter((id) => isAttachmentIdInput(id)),
|
|
5005
|
+
...paths.map((item) => String(item)).filter((item) => isAttachmentIdInput(item)),
|
|
5006
|
+
])]
|
|
5000
5007
|
|
|
5001
5008
|
for (const path of paths) {
|
|
5002
5009
|
let bytes
|
|
@@ -5363,11 +5370,18 @@ export function apply(ctx, config = {}) {
|
|
|
5363
5370
|
attempted.length > 0 ? attempted : errors.map((text) => ({ backend: 'configured', kind: 'NO_ADAPTER', error: text })),
|
|
5364
5371
|
reason,
|
|
5365
5372
|
)
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5373
|
+
const baseFailure = attempted.length > 0
|
|
5374
|
+
? failure
|
|
5375
|
+
: { ...failure, code: VISION_RESULT_CODES.UNSUPPORTED_BACKEND }
|
|
5376
|
+
if (materializableAttachmentIds.length > 0) {
|
|
5377
|
+
baseFailure.degradedAccess = {
|
|
5378
|
+
tool: 'vision_materialize',
|
|
5379
|
+
attachmentIds: materializableAttachmentIds,
|
|
5380
|
+
advice:
|
|
5381
|
+
'If another local/OCR tool requires a filesystem path, call vision_materialize for the uploaded attachment id. Do not guess a filename or the attachment store path.',
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
return JSON.stringify(baseFailure)
|
|
5371
5385
|
},
|
|
5372
5386
|
}
|
|
5373
5387
|
deepToolDefs.push(visionDescribeTool)
|
|
@@ -5539,6 +5553,44 @@ export function apply(ctx, config = {}) {
|
|
|
5539
5553
|
render: (_args, value) => [{ type: 'text', text: value }],
|
|
5540
5554
|
}
|
|
5541
5555
|
|
|
5556
|
+
// issue #153: materialize an authorized attachment into the workspace for
|
|
5557
|
+
// file_path-only local parsers without coupling them to DSH storage internals.
|
|
5558
|
+
deepToolDefs.push({
|
|
5559
|
+
name: 'vision_materialize',
|
|
5560
|
+
description:
|
|
5561
|
+
'Copy an uploaded image attachment (sha256:...) or readable local image into the session workspace and return a real filesystem path. ' +
|
|
5562
|
+
'This tool performs NO vision model/network call. Use it after vision_describe/vision_bootstrap returns ok:false when a local OCR/parser accepts only file_path. ' +
|
|
5563
|
+
'Never guess the attachment store path or search for a same-named file.',
|
|
5564
|
+
parameters: {
|
|
5565
|
+
type: 'object',
|
|
5566
|
+
properties: {
|
|
5567
|
+
image: { type: 'string', description: 'Uploaded image attachment id (recommended, e.g. sha256:...) or a readable local image path' },
|
|
5568
|
+
},
|
|
5569
|
+
required: ['image'],
|
|
5570
|
+
additionalProperties: false,
|
|
5571
|
+
},
|
|
5572
|
+
output: stringOutput,
|
|
5573
|
+
async execute(args, exec) {
|
|
5574
|
+
const source = String(args.image ?? '')
|
|
5575
|
+
const { bytes, mediaType } = await readImageBytes(exec, source)
|
|
5576
|
+
const extension = mediaType === 'image/jpeg'
|
|
5577
|
+
? 'jpg'
|
|
5578
|
+
: mediaType === 'image/webp'
|
|
5579
|
+
? 'webp'
|
|
5580
|
+
: mediaType === 'image/gif'
|
|
5581
|
+
? 'gif'
|
|
5582
|
+
: 'png'
|
|
5583
|
+
const target = await saveArtifact(exec, `${artifactStem(source, 'materialized')}.${extension}`, bytes)
|
|
5584
|
+
return JSON.stringify({
|
|
5585
|
+
path: target,
|
|
5586
|
+
mediaType,
|
|
5587
|
+
bytes: bytes.length,
|
|
5588
|
+
...(isAttachmentIdInput(source) ? { source } : {}),
|
|
5589
|
+
safeWorkspaceCopy: true,
|
|
5590
|
+
})
|
|
5591
|
+
},
|
|
5592
|
+
})
|
|
5593
|
+
|
|
5542
5594
|
const visionPresentOutput = {
|
|
5543
5595
|
schema: {
|
|
5544
5596
|
type: 'object',
|
|
@@ -6763,7 +6815,7 @@ export function apply(ctx, config = {}) {
|
|
|
6763
6815
|
}
|
|
6764
6816
|
return (
|
|
6765
6817
|
'视觉深看工具已挂载:vision_bootstrap(结构化预识别)、vision_describe(看图问答)、vision_ground(像素定位)、vision_detect(元素清单)、' +
|
|
6766
|
-
'vision_crop(裁剪放大)、vision_pixel_diff(像素对比验证)、vision_colors(取色)、' +
|
|
6818
|
+
'vision_materialize(附件落盘)、vision_crop(裁剪放大)、vision_pixel_diff(像素对比验证)、vision_colors(取色)、' +
|
|
6767
6819
|
'vision_ocr(文字识别)、vision_trace(SVG 矢量化)、vision_extract_foreground(抠图)、' +
|
|
6768
6820
|
'vision_html_screenshot(页面截图)。现在可以直接调用已启用的工具。' +
|
|
6769
6821
|
'注意:vision_ocr 只用于读取图片文字;视觉工具返回 ok:false 后端不可用结果时,不要改问法重复调用,继续文本任务。'
|
|
@@ -6773,7 +6825,7 @@ export function apply(ctx, config = {}) {
|
|
|
6773
6825
|
ctx.tools.register({
|
|
6774
6826
|
name: 'vision_activate',
|
|
6775
6827
|
description:
|
|
6776
|
-
'Mount the deep vision tools (vision_bootstrap / vision_describe / vision_ground / vision_detect / vision_crop / ' +
|
|
6828
|
+
'Mount the deep vision tools (vision_bootstrap / vision_describe / vision_ground / vision_detect / vision_materialize / vision_crop / ' +
|
|
6777
6829
|
'vision_pixel_diff / vision_colors / vision_ocr / vision_trace / ' +
|
|
6778
6830
|
'vision_extract_foreground / vision_present / vision_html_screenshot) for this session. Desktop screenshot remains disabled until the user explicitly opts in through Vision Router settings. They mount ' +
|
|
6779
6831
|
'automatically on image turns; call this only when you need them on a text-only turn.',
|
package/lib/client.js
CHANGED
|
@@ -163,6 +163,8 @@ window.__ModuleLoader__.load({
|
|
|
163
163
|
updateCurrent: '已是最新版本 v{current}',
|
|
164
164
|
updateAhead: '当前 v{current} 高于 registry 最新 v{latest};可能是源码或预发布构建,不会建议降级。',
|
|
165
165
|
updateFailed: '更新检查失败:{error}',
|
|
166
|
+
updateNoDiagnostic: '更新检查接口未返回可诊断的错误详情',
|
|
167
|
+
updateInvalidResponse: '更新检查接口返回了无效响应',
|
|
166
168
|
updateInstallHint: '已安全识别当前 DSH CLI,可直接用这套 DSH 更新插件;完成后需要重启 DSH 才会加载新版本。',
|
|
167
169
|
updateAutoUnavailable: '当前 DSH CLI 无法被安全识别,因此不执行自动更新。请沿用你原来安装 DSH / 插件的方式手动更新。',
|
|
168
170
|
updateNow: '一键更新到 v{latest}',
|
|
@@ -173,10 +175,12 @@ window.__ModuleLoader__.load({
|
|
|
173
175
|
updateActionFailed: '一键更新失败:{error}',
|
|
174
176
|
updateReleaseNotes: '查看更新说明',
|
|
175
177
|
updateRegistryFallback: '当前配置的 registry 不可用,已自动改用 npm 官方源完成检查。',
|
|
178
|
+
updateReleaseFallback: 'npm registry 检查失败,已通过 GitHub Releases 获取最新版本号;安装仍需 npm registry 可访问。',
|
|
179
|
+
updateManualUnknownTarget: '无法确认最新版本,因此不会生成 @latest 或普通 update 命令。请先打开 Releases 确认最新版本号,再把下方 <version> 替换为该版本。',
|
|
176
180
|
updateManualTitle: '手动更新',
|
|
177
181
|
updateManualSource: '源码仓库 / pnpm DSH:',
|
|
178
182
|
updateManualNpx: '普通 npm / npx DSH:',
|
|
179
|
-
updateManualAgeHint: '
|
|
183
|
+
updateManualAgeHint: '上方只会使用已确认的具体版本号;若无法确认版本,则只提供 <version> 模板。pnpm 11 默认会在新版本发布 24 小时内静默拦截 @latest / 普通 update(minimumReleaseAge 策略,但命令仍可能显示成功),所以这里不再推荐这两种模糊更新方式。',
|
|
180
184
|
updateProject: '项目主页',
|
|
181
185
|
updateReleases: 'Releases',
|
|
182
186
|
save: '保存',
|
|
@@ -398,6 +402,8 @@ window.__ModuleLoader__.load({
|
|
|
398
402
|
updateCurrent: 'Up to date: v{current}',
|
|
399
403
|
updateAhead: 'Current v{current} is ahead of registry v{latest}; this may be a source or prerelease build, so no downgrade is suggested.',
|
|
400
404
|
updateFailed: 'Update check failed: {error}',
|
|
405
|
+
updateNoDiagnostic: 'The update-check endpoint returned no diagnostic error details',
|
|
406
|
+
updateInvalidResponse: 'The update-check endpoint returned an invalid response',
|
|
401
407
|
updateInstallHint: 'The current DSH CLI was verified, so Vision Router can update through this same DSH installation. Restart DSH after the update to load the new plugin bundle.',
|
|
402
408
|
updateAutoUnavailable: 'The current DSH CLI could not be verified safely, so automatic update is disabled. Update through the same DSH/plugin installation path you originally used.',
|
|
403
409
|
updateNow: 'Update to v{latest}',
|
|
@@ -408,10 +414,12 @@ window.__ModuleLoader__.load({
|
|
|
408
414
|
updateActionFailed: 'One-click update failed: {error}',
|
|
409
415
|
updateReleaseNotes: 'View release notes',
|
|
410
416
|
updateRegistryFallback: 'The configured registry failed; the check succeeded through the official npm registry.',
|
|
417
|
+
updateReleaseFallback: 'npm registry checks failed, so the latest version was resolved from GitHub Releases; installation still requires an accessible npm registry.',
|
|
418
|
+
updateManualUnknownTarget: 'The latest version could not be confirmed, so no @latest or plain update command is generated. Open Releases first, confirm the newest version, then replace <version> in the template below.',
|
|
411
419
|
updateManualTitle: 'Manual update',
|
|
412
420
|
updateManualSource: 'DSH source checkout / pnpm:',
|
|
413
421
|
updateManualNpx: 'Normal npm / npx DSH:',
|
|
414
|
-
updateManualAgeHint: '
|
|
422
|
+
updateManualAgeHint: 'The command above only uses a confirmed exact version; if no version can be confirmed, the UI shows a <version> template instead. pnpm 11 can silently withhold @latest / plain update for releases younger than 24h (minimumReleaseAge) even while reporting success, so those ambiguous update forms are no longer recommended here.',
|
|
415
423
|
updateProject: 'Project',
|
|
416
424
|
updateReleases: 'Releases',
|
|
417
425
|
save: 'Save',
|
|
@@ -1026,6 +1034,12 @@ window.__ModuleLoader__.load({
|
|
|
1026
1034
|
let settingsPersistence
|
|
1027
1035
|
function installSettingsPersistence(scope) {
|
|
1028
1036
|
const pending = new Map()
|
|
1037
|
+
// Browser-side hidden settings (onboardingSeen / visionGuideStep) are
|
|
1038
|
+
// best-effort durability hints, not a retry queue. Remember the last
|
|
1039
|
+
// mutation issued for each field for this page lifetime so a rejected
|
|
1040
|
+
// Host write or a stale/oscillating snapshot can never create an
|
|
1041
|
+
// endless /api/settings.mutate loop (issue #155).
|
|
1042
|
+
const issued = new Map()
|
|
1029
1043
|
let flushing = false
|
|
1030
1044
|
const readSnapshot = () => {
|
|
1031
1045
|
try {
|
|
@@ -1038,14 +1052,32 @@ window.__ModuleLoader__.load({
|
|
|
1038
1052
|
const snapshot = readSnapshot()
|
|
1039
1053
|
return snapshot && snapshot.value ? snapshot.value : undefined
|
|
1040
1054
|
}
|
|
1055
|
+
const sameMutation = (left, operation, value) =>
|
|
1056
|
+
!!left && left.operation === operation &&
|
|
1057
|
+
(operation === 'unset' || jsonValueEqual(left.value, value))
|
|
1058
|
+
const alreadyStored = (field, operation, value) => {
|
|
1059
|
+
const snapshot = readSnapshot()
|
|
1060
|
+
const user = snapshot && snapshot.user
|
|
1061
|
+
const stored = !!user && typeof user === 'object' && Object.prototype.hasOwnProperty.call(user, field)
|
|
1062
|
+
return operation === 'unset'
|
|
1063
|
+
? !stored
|
|
1064
|
+
: stored && jsonValueEqual(user[field], value)
|
|
1065
|
+
}
|
|
1041
1066
|
const queue = (field, operation, value) => {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1067
|
+
// No network mutation when the raw user layer already has the exact
|
|
1068
|
+
// requested state. This also makes repeated finish/dismiss handlers
|
|
1069
|
+
// idempotent after a successful write.
|
|
1070
|
+
if (alreadyStored(field, operation, value)) {
|
|
1071
|
+
pending.delete(field)
|
|
1072
|
+
issued.set(field, { operation, value })
|
|
1047
1073
|
return
|
|
1048
1074
|
}
|
|
1075
|
+
const previous = pending.get(field)
|
|
1076
|
+
if (sameMutation(previous, operation, value)) return
|
|
1077
|
+
// Once an identical hidden-state mutation was attempted in this page,
|
|
1078
|
+
// never auto-retry it merely because subscribe/readback fired again.
|
|
1079
|
+
// A genuinely different value/operation is still allowed through.
|
|
1080
|
+
if (sameMutation(issued.get(field), operation, value)) return
|
|
1049
1081
|
pending.set(field, { operation, value, attempted: false })
|
|
1050
1082
|
void flush()
|
|
1051
1083
|
}
|
|
@@ -1068,6 +1100,7 @@ window.__ModuleLoader__.load({
|
|
|
1068
1100
|
for (const [field, entry] of work) {
|
|
1069
1101
|
if (pending.get(field) !== entry) continue
|
|
1070
1102
|
entry.attempted = true
|
|
1103
|
+
issued.set(field, { operation: entry.operation, value: entry.value })
|
|
1071
1104
|
try {
|
|
1072
1105
|
if (entry.operation === 'unset') {
|
|
1073
1106
|
if (typeof scope.unset !== 'function') continue
|
|
@@ -1083,6 +1116,10 @@ window.__ModuleLoader__.load({
|
|
|
1083
1116
|
}
|
|
1084
1117
|
} finally {
|
|
1085
1118
|
flushing = false
|
|
1119
|
+
// A different value may have been queued while the previous write
|
|
1120
|
+
// was in flight. Drain it once; identical mutations were filtered by
|
|
1121
|
+
// `issued`, so this cannot become a subscribe-driven busy loop.
|
|
1122
|
+
if ([...pending.values()].some((entry) => !entry.attempted)) void flush()
|
|
1086
1123
|
}
|
|
1087
1124
|
}
|
|
1088
1125
|
settingsPersistence = {
|
|
@@ -1109,6 +1146,7 @@ window.__ModuleLoader__.load({
|
|
|
1109
1146
|
},
|
|
1110
1147
|
}
|
|
1111
1148
|
void flush()
|
|
1149
|
+
return settingsPersistence
|
|
1112
1150
|
}
|
|
1113
1151
|
|
|
1114
1152
|
function readOnboardingSeen() {
|
|
@@ -2344,6 +2382,14 @@ window.__ModuleLoader__.load({
|
|
|
2344
2382
|
}).catch(() => {})
|
|
2345
2383
|
}
|
|
2346
2384
|
|
|
2385
|
+
const diagnosticError = (value, fallback) => {
|
|
2386
|
+
if (typeof value === 'string' && value.trim() !== '') return value.trim()
|
|
2387
|
+
if (value && typeof value.message === 'string' && value.message.trim() !== '') {
|
|
2388
|
+
return value.message.trim()
|
|
2389
|
+
}
|
|
2390
|
+
return fallback
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2347
2393
|
const runUpdateCheck = async (force = false) => {
|
|
2348
2394
|
if (updateState.status === 'running') return
|
|
2349
2395
|
setUpdateState({ status: 'running', result: updateState.result })
|
|
@@ -2354,7 +2400,10 @@ window.__ModuleLoader__.load({
|
|
|
2354
2400
|
)
|
|
2355
2401
|
const result = await response.json().catch(() => undefined)
|
|
2356
2402
|
if (!response.ok) {
|
|
2357
|
-
throw new Error(result && result.error
|
|
2403
|
+
throw new Error(diagnosticError(result && result.error, `HTTP ${response.status}`))
|
|
2404
|
+
}
|
|
2405
|
+
if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
|
|
2406
|
+
throw new Error(t('updateInvalidResponse'))
|
|
2358
2407
|
}
|
|
2359
2408
|
setUpdateState({ status: 'done', result })
|
|
2360
2409
|
} catch (error) {
|
|
@@ -2383,7 +2432,10 @@ window.__ModuleLoader__.load({
|
|
|
2383
2432
|
})
|
|
2384
2433
|
const result = await response.json().catch(() => undefined)
|
|
2385
2434
|
if (!response.ok) {
|
|
2386
|
-
throw new Error(result && result.error
|
|
2435
|
+
throw new Error(diagnosticError(result && result.error, `HTTP ${response.status}`))
|
|
2436
|
+
}
|
|
2437
|
+
if (!result || typeof result !== 'object' || typeof result.ok !== 'boolean') {
|
|
2438
|
+
throw new Error(t('updateInvalidResponse'))
|
|
2387
2439
|
}
|
|
2388
2440
|
setSelfUpdateState({ status: 'done', result })
|
|
2389
2441
|
} catch (error) {
|
|
@@ -2955,13 +3007,18 @@ window.__ModuleLoader__.load({
|
|
|
2955
3007
|
result && typeof result.releasesUrl === 'string' && result.releasesUrl
|
|
2956
3008
|
? result.releasesUrl
|
|
2957
3009
|
: projectUrl + '/releases/latest'
|
|
2958
|
-
//
|
|
2959
|
-
//
|
|
2960
|
-
//
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3010
|
+
// Manual recovery is executable only when an exact target was
|
|
3011
|
+
// confirmed by npm metadata or GitHub Releases. If every version source
|
|
3012
|
+
// is unavailable, show a non-executable <version> template instead of
|
|
3013
|
+
// falling back to @latest / plain update, both of which pnpm 11 may
|
|
3014
|
+
// silently withhold while still reporting success.
|
|
3015
|
+
const manualVersion =
|
|
3016
|
+
result && typeof result.latestVersion === 'string' && result.latestVersion.trim()
|
|
3017
|
+
? result.latestVersion.trim()
|
|
3018
|
+
: ''
|
|
3019
|
+
const manualTargetKnown = manualVersion !== ''
|
|
3020
|
+
const manualPackageSpec = 'dsh-vision-router@' + (manualTargetKnown ? manualVersion : '<version>')
|
|
3021
|
+
const manualAction = 'add ' + manualPackageSpec
|
|
2965
3022
|
const pnpmCommand = 'pnpm dsh plugin --profile ' + profile + ' ' + manualAction
|
|
2966
3023
|
const npxCommand = 'npx @deepseek-ai/dsh plugin --profile ' + profile + ' ' + manualAction
|
|
2967
3024
|
let status
|
|
@@ -2978,7 +3035,7 @@ window.__ModuleLoader__.load({
|
|
|
2978
3035
|
}
|
|
2979
3036
|
} else if (updateState.status === 'done') {
|
|
2980
3037
|
failedUpdate = true
|
|
2981
|
-
status = t('updateFailed', { error: result && result.error
|
|
3038
|
+
status = t('updateFailed', { error: diagnosticError(result && result.error, t('updateNoDiagnostic')) })
|
|
2982
3039
|
}
|
|
2983
3040
|
let selfUpdateStatus
|
|
2984
3041
|
let selfUpdateFailed = false
|
|
@@ -2998,7 +3055,7 @@ window.__ModuleLoader__.load({
|
|
|
2998
3055
|
} else if (selfUpdateState.status === 'error') {
|
|
2999
3056
|
selfUpdateFailed = true
|
|
3000
3057
|
selfUpdateStatus = t('updateActionFailed', {
|
|
3001
|
-
error: selfUpdateState.result && selfUpdateState.result.error
|
|
3058
|
+
error: diagnosticError(selfUpdateState.result && selfUpdateState.result.error, t('updateNoDiagnostic')),
|
|
3002
3059
|
})
|
|
3003
3060
|
}
|
|
3004
3061
|
const showManualHelp =
|
|
@@ -3013,6 +3070,9 @@ window.__ModuleLoader__.load({
|
|
|
3013
3070
|
const manualHelp = showManualHelp
|
|
3014
3071
|
? h('div', { className: 'vr-update-manual' },
|
|
3015
3072
|
h('div', { className: 'vr-update-manual-title' }, t('updateManualTitle')),
|
|
3073
|
+
!manualTargetKnown
|
|
3074
|
+
? h('p', { className: 'vr-update-note' }, t('updateManualUnknownTarget'))
|
|
3075
|
+
: null,
|
|
3016
3076
|
auto && auto.reason === 'source-cli-needs-loader'
|
|
3017
3077
|
? commandBlock(t('updateManualSource'), pnpmCommand)
|
|
3018
3078
|
: null,
|
|
@@ -3043,6 +3103,9 @@ window.__ModuleLoader__.load({
|
|
|
3043
3103
|
result && result.ok === true && result.registryFallbackFrom
|
|
3044
3104
|
? h('p', { className: 'vr-hint' }, t('updateRegistryFallback'))
|
|
3045
3105
|
: null,
|
|
3106
|
+
result && result.ok === true && result.releaseFallback === true
|
|
3107
|
+
? h('p', { className: 'vr-hint' }, t('updateReleaseFallback'))
|
|
3108
|
+
: null,
|
|
3046
3109
|
result && result.ok === true && result.updateAvailable === true
|
|
3047
3110
|
? h('div', { className: 'vr-catalog-error' },
|
|
3048
3111
|
h('p', { className: 'vr-hint' }, auto && auto.supported === true ? t('updateInstallHint') : t('updateAutoUnavailable')),
|
|
@@ -3305,6 +3368,7 @@ window.__ModuleLoader__.load({
|
|
|
3305
3368
|
typeof React.memo === 'function' ? React.memo(VisionRouterCard) : VisionRouterCard
|
|
3306
3369
|
|
|
3307
3370
|
const ARTIFACT_TOOL_KEYS = [
|
|
3371
|
+
'vision_materialize',
|
|
3308
3372
|
'vision_crop',
|
|
3309
3373
|
'vision_pixel_diff',
|
|
3310
3374
|
'vision_trace',
|
|
@@ -3507,6 +3571,7 @@ window.__ModuleLoader__.load({
|
|
|
3507
3571
|
yield ctx.slots.register(
|
|
3508
3572
|
{
|
|
3509
3573
|
name: 'settings.plugin.item',
|
|
3574
|
+
key: 'vision-router',
|
|
3510
3575
|
id: 'vision-router',
|
|
3511
3576
|
order: 30,
|
|
3512
3577
|
label: () => t('nav'),
|
|
@@ -3546,6 +3611,7 @@ window.__ModuleLoader__.load({
|
|
|
3546
3611
|
exports.parseLocalProviderDraft = parseLocalProviderDraft
|
|
3547
3612
|
exports.jsonValueEqual = jsonValueEqual
|
|
3548
3613
|
exports.commitSettingsPlan = commitSettingsPlan
|
|
3614
|
+
exports.installSettingsPersistence = installSettingsPersistence
|
|
3549
3615
|
return module.exports
|
|
3550
3616
|
},
|
|
3551
3617
|
})
|
package/lib/update-check.js
CHANGED
|
@@ -7,6 +7,7 @@ export const PACKAGE_NAME = 'dsh-vision-router'
|
|
|
7
7
|
export const CURRENT_VERSION = String(packageJson.version ?? '')
|
|
8
8
|
export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
9
9
|
export const RELEASES_URL = 'https://github.com/ysr666/dsh-vision-router/releases/latest'
|
|
10
|
+
export const GITHUB_LATEST_RELEASE_API = 'https://api.github.com/repos/ysr666/dsh-vision-router/releases/latest'
|
|
10
11
|
|
|
11
12
|
function parseIdentifier(value) {
|
|
12
13
|
return /^\d+$/.test(value) ? { numeric: true, value: Number(value) } : { numeric: false, value }
|
|
@@ -96,6 +97,25 @@ async function fetchLatestVersion({ fetchImpl, registryBase, signal, timeoutMs }
|
|
|
96
97
|
return latestVersion
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
|
|
101
|
+
async function fetchLatestReleaseVersion({ fetchImpl, releaseApi, signal, timeoutMs }) {
|
|
102
|
+
const requestSignal = signal ?? AbortSignal.timeout(timeoutMs)
|
|
103
|
+
const response = await fetchImpl(releaseApi, {
|
|
104
|
+
method: 'GET',
|
|
105
|
+
headers: {
|
|
106
|
+
accept: 'application/vnd.github+json',
|
|
107
|
+
'user-agent': 'dsh-vision-router-update-check',
|
|
108
|
+
},
|
|
109
|
+
signal: requestSignal,
|
|
110
|
+
})
|
|
111
|
+
if (!response.ok) throw new Error(`GitHub releases API returned HTTP ${response.status}`)
|
|
112
|
+
const body = await response.json().catch(() => undefined)
|
|
113
|
+
const tag = body && typeof body.tag_name === 'string' ? body.tag_name.trim() : ''
|
|
114
|
+
const latestVersion = tag.replace(/^v/i, '')
|
|
115
|
+
if (!parseSemver(latestVersion)) throw new Error('GitHub latest release returned an invalid tag')
|
|
116
|
+
return latestVersion
|
|
117
|
+
}
|
|
118
|
+
|
|
99
119
|
/**
|
|
100
120
|
* Check only; never installs or mutates anything. This deliberately stays
|
|
101
121
|
* independent of whether DSH/plugin was installed via npx, a global CLI,
|
|
@@ -111,6 +131,7 @@ export async function checkPackageUpdate({
|
|
|
111
131
|
currentVersion = CURRENT_VERSION,
|
|
112
132
|
registry = registryBaseFromEnv(),
|
|
113
133
|
fallbackRegistry = DEFAULT_NPM_REGISTRY,
|
|
134
|
+
releaseApi = GITHUB_LATEST_RELEASE_API,
|
|
114
135
|
signal,
|
|
115
136
|
timeoutMs = 10_000,
|
|
116
137
|
} = {}) {
|
|
@@ -155,10 +176,48 @@ export async function checkPackageUpdate({
|
|
|
155
176
|
}
|
|
156
177
|
}
|
|
157
178
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
// Registry metadata is the source of truth for installability, but a user's
|
|
180
|
+
// registry path can be blocked while GitHub remains reachable. Use the
|
|
181
|
+
// project's latest release only as a read-only version fallback so the UI
|
|
182
|
+
// can still show an exact `@<version>` recovery command instead of falling
|
|
183
|
+
// back to a bare `update` (which pnpm 11 may silently withhold for 24h).
|
|
184
|
+
if (signal?.aborted) throw signal.reason ?? new Error('update check aborted')
|
|
185
|
+
try {
|
|
186
|
+
const latestVersion = await fetchLatestReleaseVersion({
|
|
187
|
+
fetchImpl,
|
|
188
|
+
releaseApi,
|
|
189
|
+
signal,
|
|
190
|
+
timeoutMs,
|
|
191
|
+
})
|
|
192
|
+
const precedence = compareSemver(currentVersion, latestVersion)
|
|
193
|
+
return {
|
|
194
|
+
ok: true,
|
|
195
|
+
packageName: PACKAGE_NAME,
|
|
196
|
+
currentVersion,
|
|
197
|
+
latestVersion,
|
|
198
|
+
updateAvailable: precedence === -1,
|
|
199
|
+
aheadOfRegistry: precedence === 1,
|
|
200
|
+
checkedAt: Date.now(),
|
|
201
|
+
latestSource: 'github-release',
|
|
202
|
+
releaseFallback: true,
|
|
203
|
+
registryFailures: failures,
|
|
204
|
+
releasesUrl: RELEASES_URL,
|
|
205
|
+
// Exact on purpose: `@latest` can still be filtered by pnpm 11's
|
|
206
|
+
// minimumReleaseAge policy, while an explicit version is exempted.
|
|
207
|
+
packageSpec: `${PACKAGE_NAME}@${latestVersion}`,
|
|
208
|
+
installMethodAgnostic: true,
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (signal?.aborted) throw error
|
|
212
|
+
const releaseFailure = errorMessage(error)
|
|
213
|
+
throw new Error(
|
|
214
|
+
'update check failed: ' +
|
|
215
|
+
[
|
|
216
|
+
...failures.map((item) => `${item.registry} (${item.error})`),
|
|
217
|
+
`${releaseApi} (${releaseFailure})`,
|
|
218
|
+
].join(' -> '),
|
|
219
|
+
)
|
|
220
|
+
}
|
|
162
221
|
}
|
|
163
222
|
|
|
164
223
|
/**
|
|
@@ -170,6 +229,7 @@ export function createCachedUpdateChecker({
|
|
|
170
229
|
currentVersion = CURRENT_VERSION,
|
|
171
230
|
registry = registryBaseFromEnv(),
|
|
172
231
|
fallbackRegistry = DEFAULT_NPM_REGISTRY,
|
|
232
|
+
releaseApi = GITHUB_LATEST_RELEASE_API,
|
|
173
233
|
successTtlMs = 6 * 60 * 60 * 1000,
|
|
174
234
|
failureTtlMs = 5 * 60 * 1000,
|
|
175
235
|
timeoutMs = 10_000,
|
|
@@ -191,6 +251,7 @@ export function createCachedUpdateChecker({
|
|
|
191
251
|
currentVersion,
|
|
192
252
|
registry,
|
|
193
253
|
fallbackRegistry,
|
|
254
|
+
releaseApi,
|
|
194
255
|
timeoutMs,
|
|
195
256
|
}).catch((error) => ({
|
|
196
257
|
ok: false,
|
|
@@ -198,7 +259,9 @@ export function createCachedUpdateChecker({
|
|
|
198
259
|
checkedAt: Date.now(),
|
|
199
260
|
registry: normalizeRegistryBase(registry),
|
|
200
261
|
releasesUrl: RELEASES_URL,
|
|
201
|
-
|
|
262
|
+
// No ambiguous package target when every version source failed. The UI
|
|
263
|
+
// must require the user to confirm an exact release before constructing
|
|
264
|
+
// a recovery command.
|
|
202
265
|
installMethodAgnostic: true,
|
|
203
266
|
error: errorMessage(error),
|
|
204
267
|
}))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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": {
|