dsh-vision-router 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -6
- package/README.zh.md +18 -6
- package/assets/vision-tools-zh.svg +3 -3
- package/assets/vision-tools.svg +3 -3
- package/index.js +487 -25
- package/lib/client.js +138 -16
- 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 eleven pixel-level 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>
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **Announcement (v1.2.
|
|
31
|
+
> 📌 **Announcement (v1.2.3)**
|
|
32
32
|
>
|
|
33
|
-
> v1.2.
|
|
33
|
+
> 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
|
+
>
|
|
35
|
+
> 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).
|
|
36
|
+
>
|
|
37
|
+
> 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.
|
|
34
38
|
|
|
35
39
|
<p align="center">
|
|
36
40
|
<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" />
|
|
@@ -125,7 +129,7 @@ The built-in anonymous OVH vision fallback is already configured, so normal imag
|
|
|
125
129
|
- **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.
|
|
126
130
|
- **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.
|
|
127
131
|
- **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.
|
|
128
|
-
- **Progressive schema exposure.** Only a zero-arg `vision_activate` bootstrap is always visible; image turns auto-mount all
|
|
132
|
+
- **Progressive schema exposure.** Only a zero-arg `vision_activate` bootstrap is always visible; image turns auto-mount all eleven deep tools with a one-time usage note, and a `vision-tools` skill is registered for text-only turns.
|
|
129
133
|
- **Selective proxy.** Only the configured vision provider hosts go through your local proxy; DeepSeek stays direct.
|
|
130
134
|
|
|
131
135
|
### Pixel loop in practice
|
|
@@ -146,10 +150,10 @@ The vision model is **only the eyes**; DeepSeek is **always the brain**. An imag
|
|
|
146
150
|
|
|
147
151
|
## Tools
|
|
148
152
|
|
|
149
|
-
All
|
|
153
|
+
All eleven deep tools mount automatically on image turns (`autoActivateOnImage`); text turns can mount them via `vision_activate` or the `/vision-tools` skill. Built on sharp / potrace / tesseract / system Chrome — no Python:
|
|
150
154
|
|
|
151
155
|
<p align="center">
|
|
152
|
-
<img src="assets/vision-tools.svg" width="100%" alt="
|
|
156
|
+
<img src="assets/vision-tools.svg" width="100%" alt="Eleven vision tools available in DSH Vision Router." />
|
|
153
157
|
</p>
|
|
154
158
|
|
|
155
159
|
| Tool | What it does | Artifact |
|
|
@@ -340,6 +344,15 @@ Settings live in the profile's settings provider and survive upgrades.
|
|
|
340
344
|
> # your overrides …
|
|
341
345
|
> ```
|
|
342
346
|
|
|
347
|
+
> **After upgrading from v1.1.x, pixel tools fail with
|
|
348
|
+
> `colourspace: parameter space not set`:** a stale sharp 0.34.0 from the
|
|
349
|
+
> v1.1.0 era still sits in the profile and its libvips DLL conflicts with the
|
|
350
|
+
> host's sharp 0.35.3 in the same process (issues #42 / #75). Delete
|
|
351
|
+
> `~/.dsh/profiles/<profile>/node_modules/sharp` and
|
|
352
|
+
> `~/.dsh/profiles/<profile>/node_modules/@img` and restart, or run
|
|
353
|
+
> `pnpm install` inside the profile. Since v1.2.2 the plugin detects the
|
|
354
|
+
> stale version at runtime and prints the same guidance itself.
|
|
355
|
+
|
|
343
356
|
### Uninstall
|
|
344
357
|
|
|
345
358
|
```sh
|
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 只负责思考,内置免费视觉链 + 11 个像素级工具负责“看”;图片轮次就像普通工具调用一样自然、可定位、可验证。</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>
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **公告(v1.2.
|
|
31
|
+
> 📌 **公告(v1.2.3)**
|
|
32
32
|
>
|
|
33
|
-
> v1.2.
|
|
33
|
+
> v1.2.3 现已支持:修复 DSH Desktop 每次启动都重复弹出首次引导的问题——引导「已读」标记与模型引导步骤改存 profile 设置文件(不再依赖按 origin 隔离、每次随机端口重启即清零的 `localStorage`)(issue #78)。
|
|
34
|
+
>
|
|
35
|
+
> 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)。
|
|
36
|
+
>
|
|
37
|
+
> v1.2.1 加固像素闭环:十一个像素工具可直接接受上传图片的附件 ID(告别 `cannot read …/sha256:…` 的绕路),产物文件名带指纹不再互相覆盖,`vision_ground` 对退化框自动重试,模型引导支持从第 1 步完整重放(先退出设置页),设置卡片在数百模型目录下滚动依然流畅。
|
|
34
38
|
|
|
35
39
|
<p align="center">
|
|
36
40
|
<img src="assets/vision-demo.gif" width="640" alt="演示:粘贴图片,Agent 用 vision_ground / vision_crop / vision_pixel_diff 定位发送按钮并给出坐标" />
|
|
@@ -125,7 +129,7 @@ opencode-go + 自动识图 ← 发图片时选这个
|
|
|
125
129
|
- **自动降级 + 分类报错。** 地区限制、ToS 风控、402 额度、429 限流(尊重 Retry-After 退避重试)、上下文超长、网络故障——链路逐供应商尝试,全部失败才报错并给出可操作的建议。
|
|
126
130
|
- **图片记忆。** 视觉答案按附件内容哈希缓存;后续文字轮用记录的描述替换历史图片(标注为不可信证据),DeepSeek 真正“记得”之前发过的图,且不重复消耗视觉调用。
|
|
127
131
|
- **可验证的像素闭环。** 参照图 → `vision_html_screenshot` → `vision_pixel_diff`(差异率 + 红色热力图 + 最差区域排行)→ 修复 → 再对比,直到差异收敛。UI 还原从“目测”变成“实测”。
|
|
128
|
-
- **渐进式 schema 暴露。** 平时只有一个零参引导工具 `vision_activate`;图片轮自动挂载全部
|
|
132
|
+
- **渐进式 schema 暴露。** 平时只有一个零参引导工具 `vision_activate`;图片轮自动挂载全部 11 个深看工具(附一次性使用提示),并为纯文字轮注册 `vision-tools` 技能。
|
|
129
133
|
- **选择性代理。** 只有配置的视觉供应商域名走本地代理;DeepSeek 保持直连。
|
|
130
134
|
|
|
131
135
|
### 像素闭环实测
|
|
@@ -146,10 +150,10 @@ Agent 仅根据参考图复刻 UI,再用 `vision_pixel_diff` 验证最终结
|
|
|
146
150
|
|
|
147
151
|
## 工具
|
|
148
152
|
|
|
149
|
-
|
|
153
|
+
11 个深看工具在图片轮自动挂载(`autoActivateOnImage`);文字轮可通过 `vision_activate` 或 `/vision-tools` 技能挂载。全部基于 sharp / potrace / tesseract / 系统 Chrome——无 Python:
|
|
150
154
|
|
|
151
155
|
<p align="center">
|
|
152
|
-
<img src="assets/vision-tools-zh.svg" width="100%" alt="DSH Vision Router 的
|
|
156
|
+
<img src="assets/vision-tools-zh.svg" width="100%" alt="DSH Vision Router 的 11 个视觉工具。" />
|
|
153
157
|
</p>
|
|
154
158
|
|
|
155
159
|
| 工具 | 作用 | 产物 |
|
|
@@ -338,6 +342,14 @@ pnpm dsh plugin --profile web update dsh-vision-router
|
|
|
338
342
|
> # 你的配置…
|
|
339
343
|
> ```
|
|
340
344
|
|
|
345
|
+
> **从 v1.1.x 升级后像素工具报 `colourspace: parameter space not set`:**
|
|
346
|
+
> 这是 v1.1.0 时代自带的 sharp 0.34.0 残留在 profile 里、与宿主 sharp 0.35.3
|
|
347
|
+
> 同进程 DLL 冲突所致(issue #42 / #75)。删除
|
|
348
|
+
> `~/.dsh/profiles/<profile>/node_modules/sharp` 与
|
|
349
|
+
> `~/.dsh/profiles/<profile>/node_modules/@img` 后重启,或在 profile 目录执行
|
|
350
|
+
> `pnpm install` 重装依赖即可。v1.2.2 起插件会在检测到残留版本时直接告警并
|
|
351
|
+
> 给出同样的指引。
|
|
352
|
+
|
|
341
353
|
### 卸载
|
|
342
354
|
|
|
343
355
|
```sh
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1140" viewBox="0 0 1440 1140">
|
|
2
2
|
<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#070914"/><stop offset="1" stop-color="#09111a"/></linearGradient><linearGradient id="a" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#8a68ff"/><stop offset="1" stop-color="#39d6ff"/></linearGradient><pattern id="d" width="28" height="28" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="1" fill="#73809a" opacity=".1"/></pattern></defs>
|
|
3
|
-
<rect width="1440" height="
|
|
4
|
-
<g font-family="system-ui,Segoe UI,Microsoft YaHei,Arial,sans-serif"><text x="72" y="75" fill="#f5f7fc" font-size="42" font-weight="800">DSH <tspan fill="#8768ff">Vision</tspan> <tspan fill="#4acfff">Router</tspan></text><text x="72" y="108" fill="#818ba0" font-size="15">github.com/ysr666/dsh-vision-router</text><text x="720" y="170" text-anchor="middle" fill="url(#a)" font-size="52" font-weight="800">10 个视觉工具</text><text x="720" y="205" text-anchor="middle" fill="#8d97aa" font-size="17">基于 sharp · potrace · tesseract · system Chrome</text><rect x="50" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">1</text><text x="118" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_describe</text><rect x="505" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">2</text><text x="573" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_ground</text><rect x="960" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">3</text><text x="1028" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_crop</text><rect x="50" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">4</text><text x="118" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_pixel_diff</text><rect x="505" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">5</text><text x="573" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_colors</text><rect x="960" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">6</text><text x="1028" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_ocr</text><rect x="50" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">7</text><text x="118" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_trace</text><rect x="505" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">8</text><text x="573" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_extract_foreground</text><rect x="960" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">9</text><text x="1028" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_html_screenshot</text><rect x="75" y="315" width="205" height="140" rx="10" fill="#102437"/><circle cx="225" cy="347" r="22" fill="#f0d38b"/><path d="M75 431l70-66 45 39 31-30 59 57" fill="#1d5264"/><path d="M75 445l77-62 35 30 42-38 51 70" fill="#18344d"/><rect x="300" y="320" width="150" height="50" rx="10" fill="#151525" stroke="#42366e"/><text x="314" y="342" fill="#a98cff" font-size="12">问 这张图片里</text><text x="314" y="359" fill="#c8cede" font-size="12"> 有什么?</text><rect x="300" y="382" width="150" height="68" rx="10" fill="#111925" stroke="#334054"/><text x="314" y="405" fill="#7fe0bd" font-size="12">答 山间湖泊</text><text x="314" y="423" fill="#bfc7d6" font-size="12"> 松树与云</text><rect x="535" y="315" width="370" height="140" rx="10" fill="#101620" stroke="#293244"/><rect x="550" y="330" width="100" height="110" rx="7" fill="#151b27"/><text x="565" y="354" fill="#7f899c" font-size="11">仪表盘</text><rect x="558" y="368" width="80" height="27" rx="6" fill="#251d45"/><text x="570" y="386" fill="#b99cff" font-size="11">项目</text><text x="565" y="414" fill="#7f899c" font-size="11">分析</text><rect x="670" y="337" width="205" height="94" rx="8" fill="#111824"/><text x="686" y="360" fill="#e5e9f1" font-size="15" font-weight="700">项目</text><rect x="720" y="374" width="115" height="40" rx="6" fill="#161d29" stroke="#48dc8e" stroke-width="2"/><circle cx="720" cy="374" r="4" fill="#48dc8e"/><circle cx="835" cy="374" r="4" fill="#48dc8e"/><circle cx="720" cy="414" r="4" fill="#48dc8e"/><circle cx="835" cy="414" r="4" fill="#48dc8e"/><text x="746" y="399" fill="#c7a6ff" font-size="11">新建项目</text><rect x="990" y="315" width="175" height="140" rx="10" fill="#161c2b"/><path d="M1000 435l55-87 45 68 55-45" fill="none" stroke="#d37e46" stroke-width="5"/><rect x="1030" y="335" width="90" height="95" fill="none" stroke="#a987ff" stroke-width="3" stroke-dasharray="8 6"/><circle cx="1030" cy="335" r="5" fill="#b293ff"/><circle cx="1120" cy="335" r="5" fill="#b293ff"/><circle cx="1030" cy="430" r="5" fill="#b293ff"/><circle cx="1120" cy="430" r="5" fill="#b293ff"/><text x="1182" y="392" fill="#886cff" font-size="28">→</text><rect x="1220" y="315" width="145" height="140" rx="10" fill="#201d25"/><path d="M1230 440l45-100 50 83 30-50" fill="none" stroke="#d37e46" stroke-width="6"/><rect x="75" y="575" width="125" height="135" rx="9" fill="#121824" stroke="#344052"/><rect x="215" y="575" width="125" height="135" rx="9" fill="#121824" stroke="#344052"/><text x="92" y="603" fill="#d8deea" font-size="12">基线图像</text><text x="92" y="635" fill="#fff" font-size="23">$29/月</text><rect x="91" y="662" width="92" height="26" rx="6" fill="#226bd1"/><text x="115" y="680" fill="#fff" font-size="10">开始使用</text><text x="232" y="603" fill="#d8deea" font-size="12">当前图像</text><text x="232" y="635" fill="#fff" font-size="23">$29/月</text><rect x="231" y="662" width="92" height="26" rx="6" fill="#3eaa67"/><text x="255" y="680" fill="#fff" font-size="10">开始使用</text><text x="355" y="649" fill="#886cff" font-size="28">→</text><rect x="390" y="575" width="65" height="135" rx="8" fill="#0f1118" stroke="#553445"/><g fill="#f0444f"><rect x="400" y="590" width="14" height="18"/><rect x="430" y="612" width="17" height="20"/><rect x="408" y="645" width="22" height="22"/><rect x="435" y="670" width="12" height="27"/></g><rect x="535" y="575" width="370" height="75" rx="10" fill="#35203c"/><circle cx="840" cy="610" r="30" fill="#f2a34e"/><path d="M535 640l85-48 72 40 74-55 139 63" fill="#1e2a44"/><g><rect x="545" y="668" width="62" height="42" rx="6" fill="#1e2a44"/><rect x="616" y="668" width="62" height="42" rx="6" fill="#3b4d7a"/><rect x="687" y="668" width="62" height="42" rx="6" fill="#6b7fcb"/><rect x="758" y="668" width="62" height="42" rx="6" fill="#f29d49"/><rect x="829" y="668" width="62" height="42" rx="6" fill="#ffd6a6"/></g><g fill="#9da6b8" font-family="monospace" font-size="9"><text x="548" y="724">#1E2A44</text><text x="619" y="724">#3B4D7A</text><text x="690" y="724">#6B7FCB</text><text x="761" y="724">#F29D49</text><text x="832" y="724">#FFD6A6</text></g><rect x="990" y="575" width="175" height="135" rx="10" fill="#e8e8e5" stroke="#5f6670"/><text x="1077" y="607" text-anchor="middle" fill="#111827" font-size="18" font-weight="800">PARKING</text><text x="1077" y="633" text-anchor="middle" fill="#111827" font-size="18" font-weight="800">RESERVED</text><text x="1077" y="659" text-anchor="middle" fill="#111827" font-size="20" font-weight="800">24/7</text><text x="1182" y="646" fill="#886cff" font-size="28">→</text><rect x="1220" y="575" width="145" height="135" rx="10" fill="#0d1518" stroke="#28483a"/><text x="1235" y="610" fill="#58d99a" font-family="monospace" font-size="14">PARKING</text><text x="1235" y="635" fill="#58d99a" font-family="monospace" font-size="14">RESERVED</text><text x="1235" y="660" fill="#58d99a" font-family="monospace" font-size="14">24/7</text><rect x="75" y="835" width="165" height="140" rx="10" fill="#111827" stroke="#2d3b55"/><path d="M101 940l45-82 59 40-58 61z" fill="#d8e1f2"/><circle cx="165" cy="890" r="12" fill="#3d87c7"/><path d="M118 949l-13 19 21-10" fill="#ff633f"/><text x="255" y="920" fill="#886cff" font-size="28">→</text><rect x="300" y="835" width="150" height="140" rx="10" fill="#0f1420" stroke="#2d3b55"/><path d="M322 948l45-91 57 42-62 63z" fill="none" stroke="url(#a)" stroke-width="3"/><circle cx="386" cy="889" r="13" fill="none" stroke="#43d6ff" stroke-width="3"/><rect x="535" y="835" width="170" height="140" rx="10" fill="#262a36"/><path d="M590 890h62l13 52h-90z" fill="#606672"/><path d="M580 942v25M658 942v25" stroke="#7b533a" stroke-width="8"/><text x="720" y="920" fill="#886cff" font-size="28">→</text><rect x="760" y="835" width="145" height="140" rx="10" fill="#ddd"/><g fill="#bfc3c8"><rect x="760" y="835" width="29" height="28"/><rect x="818" y="835" width="29" height="28"/><rect x="876" y="835" width="29" height="28"/><rect x="789" y="863" width="29" height="28"/><rect x="847" y="863" width="29" height="28"/></g><path d="M803 890h60l12 52h-86z" fill="#555b67"/><path d="M793 942v25M868 942v25" stroke="#7b533a" stroke-width="8"/><rect x="990" y="835" width="375" height="140" rx="10" fill="#101621" stroke="#344054"/><rect x="990" y="835" width="375" height="25" rx="10" fill="#171d2a"/><circle cx="1005" cy="847" r="4" fill="#ff665a"/><circle cx="1018" cy="847" r="4" fill="#f0b85b"/><circle cx="1031" cy="847" r="4" fill="#50d890"/><text x="1060" y="851" fill="#8c96a9" font-family="monospace" font-size="10">http://localhost:8080</text><rect x="1004" y="873" width="95" height="86" rx="6" fill="#17132b"/><text x="1018" y="894" fill="#9e8cff" font-size="10">概览</text><text x="1018" y="918" fill="#8d96a8" font-size="10">报表</text><text x="1018" y="942" fill="#8d96a8" font-size="10">设置</text><rect x="1113" y="873" width="105" height="62" rx="6" fill="#151b28"/><rect x="1231" y="873" width="118" height="62" rx="6" fill="#151b28"/><text x="1125" y="895" fill="#7f899d" font-size="9">用户数</text><text x="1125" y="920" fill="#f4f7fb" font-size="18">12,540</text><path d="M1120 956c50-23 82 6 126-22 34-21 56 4 96-36" fill="none" stroke="url(#a)" stroke-width="3"/><rect x="50" y="1018" width="1340" height="52" rx="14" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="1044" r="16" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="1044" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="15" font-weight="800">10</text><text x="108" y="1050" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="16" font-weight="700">vision_detect</text><text x="420" y="1050" fill="#818ba0" font-size="14">元素清单:按类盘点页面元素,编号 + 原图像素框</text></g></svg>
|
|
3
|
+
<rect width="1440" height="1140" rx="28" fill="url(#bg)"/><rect width="1440" height="1140" rx="28" fill="url(#d)"/><rect x="1" y="1" width="1438" height="1138" rx="27" fill="none" stroke="#6554ff" opacity=".32"/>
|
|
4
|
+
<g font-family="system-ui,Segoe UI,Microsoft YaHei,Arial,sans-serif"><text x="72" y="75" fill="#f5f7fc" font-size="42" font-weight="800">DSH <tspan fill="#8768ff">Vision</tspan> <tspan fill="#4acfff">Router</tspan></text><text x="72" y="108" fill="#818ba0" font-size="15">github.com/ysr666/dsh-vision-router</text><text x="720" y="170" text-anchor="middle" fill="url(#a)" font-size="52" font-weight="800">11 个视觉工具</text><text x="720" y="205" text-anchor="middle" fill="#8d97aa" font-size="17">基于 sharp · potrace · tesseract · system Chrome</text><rect x="50" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">1</text><text x="118" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_describe</text><rect x="505" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">2</text><text x="573" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_ground</text><rect x="960" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">3</text><text x="1028" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_crop</text><rect x="50" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">4</text><text x="118" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_pixel_diff</text><rect x="505" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">5</text><text x="573" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_colors</text><rect x="960" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">6</text><text x="1028" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_ocr</text><rect x="50" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">7</text><text x="118" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_trace</text><rect x="505" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">8</text><text x="573" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_extract_foreground</text><rect x="960" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="17" font-weight="800">9</text><text x="1028" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_html_screenshot</text><rect x="75" y="315" width="205" height="140" rx="10" fill="#102437"/><circle cx="225" cy="347" r="22" fill="#f0d38b"/><path d="M75 431l70-66 45 39 31-30 59 57" fill="#1d5264"/><path d="M75 445l77-62 35 30 42-38 51 70" fill="#18344d"/><rect x="300" y="320" width="150" height="50" rx="10" fill="#151525" stroke="#42366e"/><text x="314" y="342" fill="#a98cff" font-size="12">问 这张图片里</text><text x="314" y="359" fill="#c8cede" font-size="12"> 有什么?</text><rect x="300" y="382" width="150" height="68" rx="10" fill="#111925" stroke="#334054"/><text x="314" y="405" fill="#7fe0bd" font-size="12">答 山间湖泊</text><text x="314" y="423" fill="#bfc7d6" font-size="12"> 松树与云</text><rect x="535" y="315" width="370" height="140" rx="10" fill="#101620" stroke="#293244"/><rect x="550" y="330" width="100" height="110" rx="7" fill="#151b27"/><text x="565" y="354" fill="#7f899c" font-size="11">仪表盘</text><rect x="558" y="368" width="80" height="27" rx="6" fill="#251d45"/><text x="570" y="386" fill="#b99cff" font-size="11">项目</text><text x="565" y="414" fill="#7f899c" font-size="11">分析</text><rect x="670" y="337" width="205" height="94" rx="8" fill="#111824"/><text x="686" y="360" fill="#e5e9f1" font-size="15" font-weight="700">项目</text><rect x="720" y="374" width="115" height="40" rx="6" fill="#161d29" stroke="#48dc8e" stroke-width="2"/><circle cx="720" cy="374" r="4" fill="#48dc8e"/><circle cx="835" cy="374" r="4" fill="#48dc8e"/><circle cx="720" cy="414" r="4" fill="#48dc8e"/><circle cx="835" cy="414" r="4" fill="#48dc8e"/><text x="746" y="399" fill="#c7a6ff" font-size="11">新建项目</text><rect x="990" y="315" width="175" height="140" rx="10" fill="#161c2b"/><path d="M1000 435l55-87 45 68 55-45" fill="none" stroke="#d37e46" stroke-width="5"/><rect x="1030" y="335" width="90" height="95" fill="none" stroke="#a987ff" stroke-width="3" stroke-dasharray="8 6"/><circle cx="1030" cy="335" r="5" fill="#b293ff"/><circle cx="1120" cy="335" r="5" fill="#b293ff"/><circle cx="1030" cy="430" r="5" fill="#b293ff"/><circle cx="1120" cy="430" r="5" fill="#b293ff"/><text x="1182" y="392" fill="#886cff" font-size="28">→</text><rect x="1220" y="315" width="145" height="140" rx="10" fill="#201d25"/><path d="M1230 440l45-100 50 83 30-50" fill="none" stroke="#d37e46" stroke-width="6"/><rect x="75" y="575" width="125" height="135" rx="9" fill="#121824" stroke="#344052"/><rect x="215" y="575" width="125" height="135" rx="9" fill="#121824" stroke="#344052"/><text x="92" y="603" fill="#d8deea" font-size="12">基线图像</text><text x="92" y="635" fill="#fff" font-size="23">$29/月</text><rect x="91" y="662" width="92" height="26" rx="6" fill="#226bd1"/><text x="115" y="680" fill="#fff" font-size="10">开始使用</text><text x="232" y="603" fill="#d8deea" font-size="12">当前图像</text><text x="232" y="635" fill="#fff" font-size="23">$29/月</text><rect x="231" y="662" width="92" height="26" rx="6" fill="#3eaa67"/><text x="255" y="680" fill="#fff" font-size="10">开始使用</text><text x="355" y="649" fill="#886cff" font-size="28">→</text><rect x="390" y="575" width="65" height="135" rx="8" fill="#0f1118" stroke="#553445"/><g fill="#f0444f"><rect x="400" y="590" width="14" height="18"/><rect x="430" y="612" width="17" height="20"/><rect x="408" y="645" width="22" height="22"/><rect x="435" y="670" width="12" height="27"/></g><rect x="535" y="575" width="370" height="75" rx="10" fill="#35203c"/><circle cx="840" cy="610" r="30" fill="#f2a34e"/><path d="M535 640l85-48 72 40 74-55 139 63" fill="#1e2a44"/><g><rect x="545" y="668" width="62" height="42" rx="6" fill="#1e2a44"/><rect x="616" y="668" width="62" height="42" rx="6" fill="#3b4d7a"/><rect x="687" y="668" width="62" height="42" rx="6" fill="#6b7fcb"/><rect x="758" y="668" width="62" height="42" rx="6" fill="#f29d49"/><rect x="829" y="668" width="62" height="42" rx="6" fill="#ffd6a6"/></g><g fill="#9da6b8" font-family="monospace" font-size="9"><text x="548" y="724">#1E2A44</text><text x="619" y="724">#3B4D7A</text><text x="690" y="724">#6B7FCB</text><text x="761" y="724">#F29D49</text><text x="832" y="724">#FFD6A6</text></g><rect x="990" y="575" width="175" height="135" rx="10" fill="#e8e8e5" stroke="#5f6670"/><text x="1077" y="607" text-anchor="middle" fill="#111827" font-size="18" font-weight="800">PARKING</text><text x="1077" y="633" text-anchor="middle" fill="#111827" font-size="18" font-weight="800">RESERVED</text><text x="1077" y="659" text-anchor="middle" fill="#111827" font-size="20" font-weight="800">24/7</text><text x="1182" y="646" fill="#886cff" font-size="28">→</text><rect x="1220" y="575" width="145" height="135" rx="10" fill="#0d1518" stroke="#28483a"/><text x="1235" y="610" fill="#58d99a" font-family="monospace" font-size="14">PARKING</text><text x="1235" y="635" fill="#58d99a" font-family="monospace" font-size="14">RESERVED</text><text x="1235" y="660" fill="#58d99a" font-family="monospace" font-size="14">24/7</text><rect x="75" y="835" width="165" height="140" rx="10" fill="#111827" stroke="#2d3b55"/><path d="M101 940l45-82 59 40-58 61z" fill="#d8e1f2"/><circle cx="165" cy="890" r="12" fill="#3d87c7"/><path d="M118 949l-13 19 21-10" fill="#ff633f"/><text x="255" y="920" fill="#886cff" font-size="28">→</text><rect x="300" y="835" width="150" height="140" rx="10" fill="#0f1420" stroke="#2d3b55"/><path d="M322 948l45-91 57 42-62 63z" fill="none" stroke="url(#a)" stroke-width="3"/><circle cx="386" cy="889" r="13" fill="none" stroke="#43d6ff" stroke-width="3"/><rect x="535" y="835" width="170" height="140" rx="10" fill="#262a36"/><path d="M590 890h62l13 52h-90z" fill="#606672"/><path d="M580 942v25M658 942v25" stroke="#7b533a" stroke-width="8"/><text x="720" y="920" fill="#886cff" font-size="28">→</text><rect x="760" y="835" width="145" height="140" rx="10" fill="#ddd"/><g fill="#bfc3c8"><rect x="760" y="835" width="29" height="28"/><rect x="818" y="835" width="29" height="28"/><rect x="876" y="835" width="29" height="28"/><rect x="789" y="863" width="29" height="28"/><rect x="847" y="863" width="29" height="28"/></g><path d="M803 890h60l12 52h-86z" fill="#555b67"/><path d="M793 942v25M868 942v25" stroke="#7b533a" stroke-width="8"/><rect x="990" y="835" width="375" height="140" rx="10" fill="#101621" stroke="#344054"/><rect x="990" y="835" width="375" height="25" rx="10" fill="#171d2a"/><circle cx="1005" cy="847" r="4" fill="#ff665a"/><circle cx="1018" cy="847" r="4" fill="#f0b85b"/><circle cx="1031" cy="847" r="4" fill="#50d890"/><text x="1060" y="851" fill="#8c96a9" font-family="monospace" font-size="10">http://localhost:8080</text><rect x="1004" y="873" width="95" height="86" rx="6" fill="#17132b"/><text x="1018" y="894" fill="#9e8cff" font-size="10">概览</text><text x="1018" y="918" fill="#8d96a8" font-size="10">报表</text><text x="1018" y="942" fill="#8d96a8" font-size="10">设置</text><rect x="1113" y="873" width="105" height="62" rx="6" fill="#151b28"/><rect x="1231" y="873" width="118" height="62" rx="6" fill="#151b28"/><text x="1125" y="895" fill="#7f899d" font-size="9">用户数</text><text x="1125" y="920" fill="#f4f7fb" font-size="18">12,540</text><path d="M1120 956c50-23 82 6 126-22 34-21 56 4 96-36" fill="none" stroke="url(#a)" stroke-width="3"/><rect x="50" y="1018" width="1340" height="52" rx="14" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="1044" r="16" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="1044" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="15" font-weight="800">10</text><text x="108" y="1050" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="16" font-weight="700">vision_detect</text><text x="420" y="1050" fill="#818ba0" font-size="14">元素清单:按类盘点页面元素,编号 + 原图像素框</text></g><rect x="50" y="1082" width="1340" height="52" rx="14" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="1108" r="16" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="1108" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="15" font-weight="800">11</text><text x="108" y="1114" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="16" font-weight="700">vision_long_screenshot_ocr</text><text x="420" y="1114" fill="#818ba0" font-size="14">长截图转写:分块重叠识别,拼接输出 Markdown</text></svg>
|
package/assets/vision-tools.svg
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1140" viewBox="0 0 1440 1140">
|
|
2
2
|
<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#070914"/><stop offset="1" stop-color="#09111a"/></linearGradient><linearGradient id="a" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#8a68ff"/><stop offset="1" stop-color="#39d6ff"/></linearGradient><pattern id="d" width="28" height="28" patternUnits="userSpaceOnUse"><circle cx="2" cy="2" r="1" fill="#73809a" opacity=".1"/></pattern></defs>
|
|
3
|
-
<rect width="1440" height="
|
|
4
|
-
<g font-family="system-ui,Segoe UI,Arial,sans-serif"><text x="72" y="75" fill="#f5f7fc" font-size="42" font-weight="800">DSH <tspan fill="#8768ff">Vision</tspan> <tspan fill="#4acfff">Router</tspan></text><text x="72" y="108" fill="#818ba0" font-size="15">github.com/ysr666/dsh-vision-router</text><text x="720" y="170" text-anchor="middle" fill="url(#a)" font-size="52" font-weight="800">10 Vision Tools</text><text x="720" y="205" text-anchor="middle" fill="#8d97aa" font-size="17">Built on sharp · potrace · tesseract · system Chrome</text><rect x="50" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">1</text><text x="118" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_describe</text><rect x="505" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">2</text><text x="573" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_ground</text><rect x="960" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">3</text><text x="1028" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_crop</text><rect x="50" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">4</text><text x="118" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_pixel_diff</text><rect x="505" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">5</text><text x="573" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_colors</text><rect x="960" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">6</text><text x="1028" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_ocr</text><rect x="50" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">7</text><text x="118" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_trace</text><rect x="505" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">8</text><text x="573" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_extract_foreground</text><rect x="960" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">9</text><text x="1028" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_html_screenshot</text><rect x="75" y="315" width="205" height="140" rx="10" fill="#102437"/><circle cx="225" cy="347" r="22" fill="#f0d38b"/><path d="M75 431l70-66 45 39 31-30 59 57" fill="#1d5264"/><path d="M75 445l77-62 35 30 42-38 51 70" fill="#18344d"/><rect x="300" y="320" width="150" height="50" rx="10" fill="#151525" stroke="#42366e"/><text x="314" y="342" fill="#a98cff" font-size="12">Q What's in</text><text x="314" y="359" fill="#c8cede" font-size="12"> this image?</text><rect x="300" y="382" width="150" height="68" rx="10" fill="#111925" stroke="#334054"/><text x="314" y="405" fill="#7fe0bd" font-size="12">A Mountain lake</text><text x="314" y="423" fill="#bfc7d6" font-size="12"> with trees</text><text x="314" y="441" fill="#bfc7d6" font-size="12"> and clouds.</text><rect x="535" y="315" width="370" height="140" rx="10" fill="#101620" stroke="#293244"/><rect x="550" y="330" width="100" height="110" rx="7" fill="#151b27"/><text x="565" y="354" fill="#7f899c" font-size="11">Dashboard</text><rect x="558" y="368" width="80" height="27" rx="6" fill="#251d45"/><text x="570" y="386" fill="#b99cff" font-size="11">Projects</text><text x="565" y="414" fill="#7f899c" font-size="11">Analytics</text><rect x="670" y="337" width="205" height="94" rx="8" fill="#111824"/><text x="686" y="360" fill="#e5e9f1" font-size="15" font-weight="700">Projects</text><rect x="720" y="374" width="115" height="40" rx="6" fill="#161d29" stroke="#48dc8e" stroke-width="2"/><circle cx="720" cy="374" r="4" fill="#48dc8e"/><circle cx="835" cy="374" r="4" fill="#48dc8e"/><circle cx="720" cy="414" r="4" fill="#48dc8e"/><circle cx="835" cy="414" r="4" fill="#48dc8e"/><text x="742" y="399" fill="#c7a6ff" font-size="11">New Project</text><rect x="990" y="315" width="175" height="140" rx="10" fill="#161c2b"/><path d="M1000 435l55-87 45 68 55-45" fill="none" stroke="#d37e46" stroke-width="5"/><rect x="1030" y="335" width="90" height="95" fill="none" stroke="#a987ff" stroke-width="3" stroke-dasharray="8 6"/><circle cx="1030" cy="335" r="5" fill="#b293ff"/><circle cx="1120" cy="335" r="5" fill="#b293ff"/><circle cx="1030" cy="430" r="5" fill="#b293ff"/><circle cx="1120" cy="430" r="5" fill="#b293ff"/><text x="1182" y="392" fill="#886cff" font-size="28">→</text><rect x="1220" y="315" width="145" height="140" rx="10" fill="#201d25"/><path d="M1230 440l45-100 50 83 30-50" fill="none" stroke="#d37e46" stroke-width="6"/><rect x="75" y="575" width="125" height="135" rx="9" fill="#121824" stroke="#344052"/><rect x="215" y="575" width="125" height="135" rx="9" fill="#121824" stroke="#344052"/><text x="92" y="603" fill="#d8deea" font-size="12">Pro Plan</text><text x="92" y="635" fill="#fff" font-size="23">$29</text><rect x="91" y="662" width="92" height="26" rx="6" fill="#226bd1"/><text x="109" y="680" fill="#fff" font-size="10">Get Started</text><text x="232" y="603" fill="#d8deea" font-size="12">Pro Plan</text><text x="232" y="635" fill="#fff" font-size="23">$29</text><rect x="231" y="662" width="92" height="26" rx="6" fill="#3eaa67"/><text x="249" y="680" fill="#fff" font-size="10">Get Started</text><text x="355" y="649" fill="#886cff" font-size="28">→</text><rect x="390" y="575" width="65" height="135" rx="8" fill="#0f1118" stroke="#553445"/><g fill="#f0444f"><rect x="400" y="590" width="14" height="18"/><rect x="430" y="612" width="17" height="20"/><rect x="408" y="645" width="22" height="22"/><rect x="435" y="670" width="12" height="27"/></g><rect x="535" y="575" width="370" height="75" rx="10" fill="#35203c"/><circle cx="840" cy="610" r="30" fill="#f2a34e"/><path d="M535 640l85-48 72 40 74-55 139 63" fill="#1e2a44"/><g><rect x="545" y="668" width="62" height="42" rx="6" fill="#1e2a44"/><rect x="616" y="668" width="62" height="42" rx="6" fill="#3b4d7a"/><rect x="687" y="668" width="62" height="42" rx="6" fill="#6b7fcb"/><rect x="758" y="668" width="62" height="42" rx="6" fill="#f29d49"/><rect x="829" y="668" width="62" height="42" rx="6" fill="#ffd6a6"/></g><g fill="#9da6b8" font-family="monospace" font-size="9"><text x="548" y="724">#1E2A44</text><text x="619" y="724">#3B4D7A</text><text x="690" y="724">#6B7FCB</text><text x="761" y="724">#F29D49</text><text x="832" y="724">#FFD6A6</text></g><rect x="990" y="575" width="175" height="135" rx="10" fill="#e8e8e5" stroke="#5f6670"/><text x="1077" y="607" text-anchor="middle" fill="#111827" font-size="18" font-weight="800">PARKING</text><text x="1077" y="633" text-anchor="middle" fill="#111827" font-size="18" font-weight="800">RESERVED</text><text x="1077" y="659" text-anchor="middle" fill="#111827" font-size="20" font-weight="800">24/7</text><text x="1182" y="646" fill="#886cff" font-size="28">→</text><rect x="1220" y="575" width="145" height="135" rx="10" fill="#0d1518" stroke="#28483a"/><text x="1235" y="610" fill="#58d99a" font-family="monospace" font-size="14">PARKING</text><text x="1235" y="635" fill="#58d99a" font-family="monospace" font-size="14">RESERVED</text><text x="1235" y="660" fill="#58d99a" font-family="monospace" font-size="14">24/7</text><rect x="75" y="835" width="165" height="140" rx="10" fill="#111827" stroke="#2d3b55"/><path d="M101 940l45-82 59 40-58 61z" fill="#d8e1f2"/><circle cx="165" cy="890" r="12" fill="#3d87c7"/><path d="M118 949l-13 19 21-10" fill="#ff633f"/><text x="255" y="920" fill="#886cff" font-size="28">→</text><rect x="300" y="835" width="150" height="140" rx="10" fill="#0f1420" stroke="#2d3b55"/><path d="M322 948l45-91 57 42-62 63z" fill="none" stroke="url(#a)" stroke-width="3"/><circle cx="386" cy="889" r="13" fill="none" stroke="#43d6ff" stroke-width="3"/><rect x="535" y="835" width="170" height="140" rx="10" fill="#262a36"/><path d="M590 890h62l13 52h-90z" fill="#606672"/><path d="M580 942v25M658 942v25" stroke="#7b533a" stroke-width="8"/><text x="720" y="920" fill="#886cff" font-size="28">→</text><rect x="760" y="835" width="145" height="140" rx="10" fill="#ddd"/><g fill="#bfc3c8"><rect x="760" y="835" width="29" height="28"/><rect x="818" y="835" width="29" height="28"/><rect x="876" y="835" width="29" height="28"/><rect x="789" y="863" width="29" height="28"/><rect x="847" y="863" width="29" height="28"/></g><path d="M803 890h60l12 52h-86z" fill="#555b67"/><path d="M793 942v25M868 942v25" stroke="#7b533a" stroke-width="8"/><rect x="990" y="835" width="375" height="140" rx="10" fill="#101621" stroke="#344054"/><rect x="990" y="835" width="375" height="25" rx="10" fill="#171d2a"/><circle cx="1005" cy="847" r="4" fill="#ff665a"/><circle cx="1018" cy="847" r="4" fill="#f0b85b"/><circle cx="1031" cy="847" r="4" fill="#50d890"/><text x="1060" y="851" fill="#8c96a9" font-family="monospace" font-size="10">http://localhost:8080</text><rect x="1004" y="873" width="95" height="86" rx="6" fill="#17132b"/><text x="1018" y="894" fill="#9e8cff" font-size="10">Overview</text><text x="1018" y="918" fill="#8d96a8" font-size="10">Reports</text><text x="1018" y="942" fill="#8d96a8" font-size="10">Settings</text><rect x="1113" y="873" width="105" height="62" rx="6" fill="#151b28"/><rect x="1231" y="873" width="118" height="62" rx="6" fill="#151b28"/><text x="1125" y="895" fill="#7f899d" font-size="9">Users</text><text x="1125" y="920" fill="#f4f7fb" font-size="18">12,540</text><path d="M1120 956c50-23 82 6 126-22 34-21 56 4 96-36" fill="none" stroke="url(#a)" stroke-width="3"/><rect x="50" y="1018" width="1340" height="52" rx="14" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="1044" r="16" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="1044" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="15" font-weight="800">10</text><text x="108" y="1050" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="16" font-weight="700">vision_detect</text><text x="420" y="1050" fill="#818ba0" font-size="14">numbered inventory of every element of a kind — original-pixel boxes</text></g></svg>
|
|
3
|
+
<rect width="1440" height="1140" rx="28" fill="url(#bg)"/><rect width="1440" height="1140" rx="28" fill="url(#d)"/><rect x="1" y="1" width="1438" height="1138" rx="27" fill="none" stroke="#6554ff" opacity=".32"/>
|
|
4
|
+
<g font-family="system-ui,Segoe UI,Arial,sans-serif"><text x="72" y="75" fill="#f5f7fc" font-size="42" font-weight="800">DSH <tspan fill="#8768ff">Vision</tspan> <tspan fill="#4acfff">Router</tspan></text><text x="72" y="108" fill="#818ba0" font-size="15">github.com/ysr666/dsh-vision-router</text><text x="720" y="170" text-anchor="middle" fill="url(#a)" font-size="52" font-weight="800">11 Vision Tools</text><text x="720" y="205" text-anchor="middle" fill="#8d97aa" font-size="17">Built on sharp · potrace · tesseract · system Chrome</text><rect x="50" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">1</text><text x="118" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_describe</text><rect x="505" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">2</text><text x="573" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_ground</text><rect x="960" y="245" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="279" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="279" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">3</text><text x="1028" y="286" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_crop</text><rect x="50" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">4</text><text x="118" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_pixel_diff</text><rect x="505" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">5</text><text x="573" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_colors</text><rect x="960" y="505" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="539" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="539" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">6</text><text x="1028" y="546" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_ocr</text><rect x="50" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">7</text><text x="118" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_trace</text><rect x="505" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="527" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="527" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">8</text><text x="573" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_extract_foreground</text><rect x="960" y="765" width="430" height="240" rx="20" fill="#0c111c" stroke="#265b73"/><circle cx="982" cy="799" r="18" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="982" y="799" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-family="system-ui,Segoe UI,Arial,sans-serif" font-size="17" font-weight="800">9</text><text x="1028" y="806" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="18" font-weight="700">vision_html_screenshot</text><rect x="75" y="315" width="205" height="140" rx="10" fill="#102437"/><circle cx="225" cy="347" r="22" fill="#f0d38b"/><path d="M75 431l70-66 45 39 31-30 59 57" fill="#1d5264"/><path d="M75 445l77-62 35 30 42-38 51 70" fill="#18344d"/><rect x="300" y="320" width="150" height="50" rx="10" fill="#151525" stroke="#42366e"/><text x="314" y="342" fill="#a98cff" font-size="12">Q What's in</text><text x="314" y="359" fill="#c8cede" font-size="12"> this image?</text><rect x="300" y="382" width="150" height="68" rx="10" fill="#111925" stroke="#334054"/><text x="314" y="405" fill="#7fe0bd" font-size="12">A Mountain lake</text><text x="314" y="423" fill="#bfc7d6" font-size="12"> with trees</text><text x="314" y="441" fill="#bfc7d6" font-size="12"> and clouds.</text><rect x="535" y="315" width="370" height="140" rx="10" fill="#101620" stroke="#293244"/><rect x="550" y="330" width="100" height="110" rx="7" fill="#151b27"/><text x="565" y="354" fill="#7f899c" font-size="11">Dashboard</text><rect x="558" y="368" width="80" height="27" rx="6" fill="#251d45"/><text x="570" y="386" fill="#b99cff" font-size="11">Projects</text><text x="565" y="414" fill="#7f899c" font-size="11">Analytics</text><rect x="670" y="337" width="205" height="94" rx="8" fill="#111824"/><text x="686" y="360" fill="#e5e9f1" font-size="15" font-weight="700">Projects</text><rect x="720" y="374" width="115" height="40" rx="6" fill="#161d29" stroke="#48dc8e" stroke-width="2"/><circle cx="720" cy="374" r="4" fill="#48dc8e"/><circle cx="835" cy="374" r="4" fill="#48dc8e"/><circle cx="720" cy="414" r="4" fill="#48dc8e"/><circle cx="835" cy="414" r="4" fill="#48dc8e"/><text x="742" y="399" fill="#c7a6ff" font-size="11">New Project</text><rect x="990" y="315" width="175" height="140" rx="10" fill="#161c2b"/><path d="M1000 435l55-87 45 68 55-45" fill="none" stroke="#d37e46" stroke-width="5"/><rect x="1030" y="335" width="90" height="95" fill="none" stroke="#a987ff" stroke-width="3" stroke-dasharray="8 6"/><circle cx="1030" cy="335" r="5" fill="#b293ff"/><circle cx="1120" cy="335" r="5" fill="#b293ff"/><circle cx="1030" cy="430" r="5" fill="#b293ff"/><circle cx="1120" cy="430" r="5" fill="#b293ff"/><text x="1182" y="392" fill="#886cff" font-size="28">→</text><rect x="1220" y="315" width="145" height="140" rx="10" fill="#201d25"/><path d="M1230 440l45-100 50 83 30-50" fill="none" stroke="#d37e46" stroke-width="6"/><rect x="75" y="575" width="125" height="135" rx="9" fill="#121824" stroke="#344052"/><rect x="215" y="575" width="125" height="135" rx="9" fill="#121824" stroke="#344052"/><text x="92" y="603" fill="#d8deea" font-size="12">Pro Plan</text><text x="92" y="635" fill="#fff" font-size="23">$29</text><rect x="91" y="662" width="92" height="26" rx="6" fill="#226bd1"/><text x="109" y="680" fill="#fff" font-size="10">Get Started</text><text x="232" y="603" fill="#d8deea" font-size="12">Pro Plan</text><text x="232" y="635" fill="#fff" font-size="23">$29</text><rect x="231" y="662" width="92" height="26" rx="6" fill="#3eaa67"/><text x="249" y="680" fill="#fff" font-size="10">Get Started</text><text x="355" y="649" fill="#886cff" font-size="28">→</text><rect x="390" y="575" width="65" height="135" rx="8" fill="#0f1118" stroke="#553445"/><g fill="#f0444f"><rect x="400" y="590" width="14" height="18"/><rect x="430" y="612" width="17" height="20"/><rect x="408" y="645" width="22" height="22"/><rect x="435" y="670" width="12" height="27"/></g><rect x="535" y="575" width="370" height="75" rx="10" fill="#35203c"/><circle cx="840" cy="610" r="30" fill="#f2a34e"/><path d="M535 640l85-48 72 40 74-55 139 63" fill="#1e2a44"/><g><rect x="545" y="668" width="62" height="42" rx="6" fill="#1e2a44"/><rect x="616" y="668" width="62" height="42" rx="6" fill="#3b4d7a"/><rect x="687" y="668" width="62" height="42" rx="6" fill="#6b7fcb"/><rect x="758" y="668" width="62" height="42" rx="6" fill="#f29d49"/><rect x="829" y="668" width="62" height="42" rx="6" fill="#ffd6a6"/></g><g fill="#9da6b8" font-family="monospace" font-size="9"><text x="548" y="724">#1E2A44</text><text x="619" y="724">#3B4D7A</text><text x="690" y="724">#6B7FCB</text><text x="761" y="724">#F29D49</text><text x="832" y="724">#FFD6A6</text></g><rect x="990" y="575" width="175" height="135" rx="10" fill="#e8e8e5" stroke="#5f6670"/><text x="1077" y="607" text-anchor="middle" fill="#111827" font-size="18" font-weight="800">PARKING</text><text x="1077" y="633" text-anchor="middle" fill="#111827" font-size="18" font-weight="800">RESERVED</text><text x="1077" y="659" text-anchor="middle" fill="#111827" font-size="20" font-weight="800">24/7</text><text x="1182" y="646" fill="#886cff" font-size="28">→</text><rect x="1220" y="575" width="145" height="135" rx="10" fill="#0d1518" stroke="#28483a"/><text x="1235" y="610" fill="#58d99a" font-family="monospace" font-size="14">PARKING</text><text x="1235" y="635" fill="#58d99a" font-family="monospace" font-size="14">RESERVED</text><text x="1235" y="660" fill="#58d99a" font-family="monospace" font-size="14">24/7</text><rect x="75" y="835" width="165" height="140" rx="10" fill="#111827" stroke="#2d3b55"/><path d="M101 940l45-82 59 40-58 61z" fill="#d8e1f2"/><circle cx="165" cy="890" r="12" fill="#3d87c7"/><path d="M118 949l-13 19 21-10" fill="#ff633f"/><text x="255" y="920" fill="#886cff" font-size="28">→</text><rect x="300" y="835" width="150" height="140" rx="10" fill="#0f1420" stroke="#2d3b55"/><path d="M322 948l45-91 57 42-62 63z" fill="none" stroke="url(#a)" stroke-width="3"/><circle cx="386" cy="889" r="13" fill="none" stroke="#43d6ff" stroke-width="3"/><rect x="535" y="835" width="170" height="140" rx="10" fill="#262a36"/><path d="M590 890h62l13 52h-90z" fill="#606672"/><path d="M580 942v25M658 942v25" stroke="#7b533a" stroke-width="8"/><text x="720" y="920" fill="#886cff" font-size="28">→</text><rect x="760" y="835" width="145" height="140" rx="10" fill="#ddd"/><g fill="#bfc3c8"><rect x="760" y="835" width="29" height="28"/><rect x="818" y="835" width="29" height="28"/><rect x="876" y="835" width="29" height="28"/><rect x="789" y="863" width="29" height="28"/><rect x="847" y="863" width="29" height="28"/></g><path d="M803 890h60l12 52h-86z" fill="#555b67"/><path d="M793 942v25M868 942v25" stroke="#7b533a" stroke-width="8"/><rect x="990" y="835" width="375" height="140" rx="10" fill="#101621" stroke="#344054"/><rect x="990" y="835" width="375" height="25" rx="10" fill="#171d2a"/><circle cx="1005" cy="847" r="4" fill="#ff665a"/><circle cx="1018" cy="847" r="4" fill="#f0b85b"/><circle cx="1031" cy="847" r="4" fill="#50d890"/><text x="1060" y="851" fill="#8c96a9" font-family="monospace" font-size="10">http://localhost:8080</text><rect x="1004" y="873" width="95" height="86" rx="6" fill="#17132b"/><text x="1018" y="894" fill="#9e8cff" font-size="10">Overview</text><text x="1018" y="918" fill="#8d96a8" font-size="10">Reports</text><text x="1018" y="942" fill="#8d96a8" font-size="10">Settings</text><rect x="1113" y="873" width="105" height="62" rx="6" fill="#151b28"/><rect x="1231" y="873" width="118" height="62" rx="6" fill="#151b28"/><text x="1125" y="895" fill="#7f899d" font-size="9">Users</text><text x="1125" y="920" fill="#f4f7fb" font-size="18">12,540</text><path d="M1120 956c50-23 82 6 126-22 34-21 56 4 96-36" fill="none" stroke="url(#a)" stroke-width="3"/><rect x="50" y="1018" width="1340" height="52" rx="14" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="1044" r="16" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="1044" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="15" font-weight="800">10</text><text x="108" y="1050" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="16" font-weight="700">vision_detect</text><text x="420" y="1050" fill="#818ba0" font-size="14">numbered inventory of every element of a kind — original-pixel boxes</text></g><rect x="50" y="1082" width="1340" height="52" rx="14" fill="#0c111c" stroke="#44356f"/><circle cx="72" cy="1108" r="16" fill="#151329" stroke="#7765ff" stroke-width="2"/><text x="72" y="1108" text-anchor="middle" dominant-baseline="central" fill="#f0f3f9" font-size="15" font-weight="800">11</text><text x="108" y="1114" fill="#f0f3f9" font-family="ui-monospace,SFMono-Regular,Menlo,monospace" font-size="16" font-weight="700">vision_long_screenshot_ocr</text><text x="420" y="1114" fill="#818ba0" font-size="14">long-screenshot transcription: overlapping chunks, stitched Markdown</text></svg>
|
package/index.js
CHANGED
|
@@ -40,10 +40,136 @@ import { createHash, randomBytes } from 'node:crypto'
|
|
|
40
40
|
// lazily and cache the resolved factory so a sharp failure degrades only the
|
|
41
41
|
// pixel-level tools — the routing chain and text tools keep working.
|
|
42
42
|
let sharpPromise
|
|
43
|
+
// Module-level warning sink installed by apply(): the plugin routes runtime
|
|
44
|
+
// diagnostics through ctx.logger instead of console.warn. Kept as a plain
|
|
45
|
+
// function slot so loadSharp() stays usable outside a Cordis context (tests,
|
|
46
|
+
// the doctor CLI).
|
|
47
|
+
let sharpWarningHook
|
|
48
|
+
export function registerSharpWarningHook(hook) {
|
|
49
|
+
sharpWarningHook = typeof hook === 'function' ? hook : undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function warnSharp(message) {
|
|
53
|
+
if (sharpWarningHook !== undefined) {
|
|
54
|
+
try {
|
|
55
|
+
sharpWarningHook(message)
|
|
56
|
+
return
|
|
57
|
+
} catch {
|
|
58
|
+
/* fall through to console */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (typeof console !== 'undefined' && typeof console.warn === 'function') console.warn(message)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Split "1.2.3" / "1.2" / "1" / "1.2.3-beta.4" into comparable parts
|
|
65
|
+
* (missing minor/patch default to 0, like semver). */
|
|
66
|
+
export function parseVersionParts(version) {
|
|
67
|
+
const match = String(version ?? '').trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/)
|
|
68
|
+
if (!match) return undefined
|
|
69
|
+
return {
|
|
70
|
+
major: Number(match[1]),
|
|
71
|
+
minor: Number(match[2] ?? 0),
|
|
72
|
+
patch: Number(match[3] ?? 0),
|
|
73
|
+
pre: match[4],
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compareVersionParts(a, b) {
|
|
78
|
+
if (a.major !== b.major) return a.major < b.major ? -1 : 1
|
|
79
|
+
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1
|
|
80
|
+
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1
|
|
81
|
+
// A prerelease sorts below its release: 0.35.3-beta < 0.35.3.
|
|
82
|
+
if (a.pre === undefined && b.pre === undefined) return 0
|
|
83
|
+
if (a.pre === undefined) return 1
|
|
84
|
+
if (b.pre === undefined) return -1
|
|
85
|
+
return a.pre < b.pre ? -1 : a.pre > b.pre ? 1 : 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Minimal semver range check for the comparator shapes the plugin itself
|
|
90
|
+
* declares (`>=0.35.3 <1`, space-separated clauses, `||` alternatives).
|
|
91
|
+
* @returns true when `version` satisfies `range`, false otherwise (also for
|
|
92
|
+
* malformed inputs, so an unparsable range fails safe and loud).
|
|
93
|
+
*/
|
|
94
|
+
export function versionSatisfies(version, range) {
|
|
95
|
+
const parts = parseVersionParts(version)
|
|
96
|
+
if (parts === undefined) return false
|
|
97
|
+
const alternatives = String(range ?? '')
|
|
98
|
+
.split('||')
|
|
99
|
+
.map((alt) => alt.trim())
|
|
100
|
+
.filter((alt) => alt !== '')
|
|
101
|
+
if (alternatives.length === 0) return false
|
|
102
|
+
return alternatives.some((alternative) => {
|
|
103
|
+
const clauses = alternative.split(/\s+/)
|
|
104
|
+
if (clauses.length === 0) return false
|
|
105
|
+
return clauses.every((clause) => {
|
|
106
|
+
const match = clause.match(/^(>=|<=|>|<|=)?\s*(.+)$/)
|
|
107
|
+
if (!match) return false
|
|
108
|
+
const op = match[1] ?? '='
|
|
109
|
+
const other = parseVersionParts(match[2])
|
|
110
|
+
if (other === undefined) return false
|
|
111
|
+
const cmp = compareVersionParts(parts, other)
|
|
112
|
+
switch (op) {
|
|
113
|
+
case '>=': return cmp >= 0
|
|
114
|
+
case '<=': return cmp <= 0
|
|
115
|
+
case '>': return cmp > 0
|
|
116
|
+
case '<': return cmp < 0
|
|
117
|
+
default: return cmp === 0
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Read the plugin's own peerDependencies.sharp range from the installed
|
|
124
|
+
// package.json (createRequire resolves it relative to this file, so the value
|
|
125
|
+
// is never hardcoded and follows package.json through releases).
|
|
126
|
+
let sharpPeerRangeCache
|
|
127
|
+
function sharpPeerRange() {
|
|
128
|
+
if (sharpPeerRangeCache === undefined) {
|
|
129
|
+
try {
|
|
130
|
+
const requireLocal = createRequire(import.meta.url)
|
|
131
|
+
const pkg = requireLocal('./package.json')
|
|
132
|
+
sharpPeerRangeCache =
|
|
133
|
+
pkg && pkg.peerDependencies && typeof pkg.peerDependencies.sharp === 'string'
|
|
134
|
+
? pkg.peerDependencies.sharp
|
|
135
|
+
: undefined
|
|
136
|
+
} catch {
|
|
137
|
+
sharpPeerRangeCache = undefined
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return sharpPeerRangeCache
|
|
141
|
+
}
|
|
142
|
+
|
|
43
143
|
function loadSharp() {
|
|
44
144
|
if (!sharpPromise) {
|
|
45
145
|
sharpPromise = import('sharp')
|
|
46
|
-
.then((mod) =>
|
|
146
|
+
.then((mod) => {
|
|
147
|
+
const sharp = mod.default ?? mod
|
|
148
|
+
// issue #75: an upgrade from v1.1.x can leave a stale sharp 0.34.0 in
|
|
149
|
+
// the profile's node_modules; pnpm does not physically remove orphaned
|
|
150
|
+
// peer copies on upgrade. On Windows the stale copy's libvips DLL and
|
|
151
|
+
// the host's coexist in one process and every pixel tool then dies
|
|
152
|
+
// with the cryptic "colourspace: parameter space not set". Detect the
|
|
153
|
+
// violation up front and turn it into an actionable warning.
|
|
154
|
+
try {
|
|
155
|
+
const version = sharp && sharp.versions && typeof sharp.versions.sharp === 'string'
|
|
156
|
+
? sharp.versions.sharp
|
|
157
|
+
: undefined
|
|
158
|
+
const range = sharpPeerRange()
|
|
159
|
+
if (version !== undefined && range !== undefined && !versionSatisfies(version, range)) {
|
|
160
|
+
warnSharp(
|
|
161
|
+
`dsh-vision-router: the resolved sharp ${version} does not satisfy the plugin peer range "${range}". ` +
|
|
162
|
+
'This is usually a stale sharp left in the profile from a pre-v1.2 upgrade: remove ' +
|
|
163
|
+
'`<profile>/node_modules/sharp` and `<profile>/node_modules/@img` (or run `pnpm install` in the profile) ' +
|
|
164
|
+
'and restart, so the plugin falls through to the host sharp. Until then, pixel tools may fail with ' +
|
|
165
|
+
'"colourspace: parameter space not set".',
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
/* diagnostics must never break the pixel tools */
|
|
170
|
+
}
|
|
171
|
+
return sharp
|
|
172
|
+
})
|
|
47
173
|
.catch((cause) => {
|
|
48
174
|
sharpPromise = undefined // allow a retry after the environment is repaired
|
|
49
175
|
const error = new Error(
|
|
@@ -107,6 +233,15 @@ export const Config = z.object({
|
|
|
107
233
|
tool: z.boolean().default(true),
|
|
108
234
|
progressiveTools: z.boolean().default(true),
|
|
109
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(''),
|
|
110
245
|
artifactsDir: z.string().default('.dsh-vision-router/artifacts'),
|
|
111
246
|
rewriteImages: z.boolean().default(true),
|
|
112
247
|
downscale: z.boolean().default(true),
|
|
@@ -397,29 +532,100 @@ export function renderVisionPresent(value) {
|
|
|
397
532
|
]
|
|
398
533
|
}
|
|
399
534
|
|
|
535
|
+
/** Text marker replacing a tool-produced image block (shared by the pre-step
|
|
536
|
+
* inbox sanitizer and the session-surface shadow sanitizer). */
|
|
537
|
+
export function toolImageMarker(block) {
|
|
538
|
+
const attachment = block && block.attachment ? block.attachment : {}
|
|
539
|
+
const id = attachment.attachmentId || attachment.id || 'unknown'
|
|
540
|
+
const name = attachment.name || 'tool image'
|
|
541
|
+
return {
|
|
542
|
+
type: 'text',
|
|
543
|
+
text:
|
|
544
|
+
`[tool result produced image "${name}", attachment id "${id}". ` +
|
|
545
|
+
`The image was kept out of the text-model request to prevent session corruption. ` +
|
|
546
|
+
`To inspect it, call vision_describe with attachmentIds: ["${id}"] when available, ` +
|
|
547
|
+
'or use a path-based vision tool. To show a generated image to the user, use vision_present instead of read_image.]',
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
400
551
|
export function sanitizeToolResultImages(messages) {
|
|
401
552
|
let anyChanged = false
|
|
402
553
|
const rewritten = (messages ?? []).map((message) => {
|
|
403
554
|
if (!message || !Array.isArray(message.content)) return message
|
|
404
|
-
const result = rewriteToolResultImages(message.content,
|
|
405
|
-
const attachment = block.attachment || {}
|
|
406
|
-
const id = attachment.attachmentId || attachment.id || 'unknown'
|
|
407
|
-
const name = attachment.name || 'tool image'
|
|
408
|
-
return {
|
|
409
|
-
type: 'text',
|
|
410
|
-
text:
|
|
411
|
-
`[tool result produced image "${name}", attachment id "${id}". ` +
|
|
412
|
-
`The image was kept out of the text-model request to prevent session corruption. ` +
|
|
413
|
-
`To inspect it, call vision_describe with attachmentIds: ["${id}"] when available, ` +
|
|
414
|
-
'or use a path-based vision tool. To show a generated image to the user, use vision_present instead of read_image.]',
|
|
415
|
-
}
|
|
416
|
-
})
|
|
555
|
+
const result = rewriteToolResultImages(message.content, toolImageMarker)
|
|
417
556
|
if (result.changed) anyChanged = true
|
|
418
557
|
return result.changed ? { ...message, content: result.content } : message
|
|
419
558
|
})
|
|
420
559
|
return { messages: anyChanged ? rewritten : (messages ?? []), changed: anyChanged }
|
|
421
560
|
}
|
|
422
561
|
|
|
562
|
+
/** Recursively freeze a plain structured-clone tree (the session log keeps its
|
|
563
|
+
* messages deep-frozen; replacements must match). */
|
|
564
|
+
export function deepFreezeLocal(value) {
|
|
565
|
+
if (value !== null && typeof value === 'object') {
|
|
566
|
+
for (const key of Object.keys(value)) deepFreezeLocal(value[key])
|
|
567
|
+
Object.freeze(value)
|
|
568
|
+
}
|
|
569
|
+
return value
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Build the sanitized, deep-frozen copy of a tool-result message: identical
|
|
574
|
+
* to the original except that every image block (top-level or nested inside
|
|
575
|
+
* tool-result content) is replaced with a text marker. Returns the original
|
|
576
|
+
* message object unchanged when it contains no image.
|
|
577
|
+
*/
|
|
578
|
+
export function sanitizeToolResultMessage(message) {
|
|
579
|
+
if (!message || !Array.isArray(message.content)) return message
|
|
580
|
+
const result = rewriteImagesDeep(message.content, toolImageMarker)
|
|
581
|
+
if (!result.changed) return message
|
|
582
|
+
const clone = structuredClone(message)
|
|
583
|
+
clone.content = result.content
|
|
584
|
+
return deepFreezeLocal(clone)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Plan the shadow replacements that keep tool-produced image blocks out of
|
|
589
|
+
* the model-visible session surface.
|
|
590
|
+
*
|
|
591
|
+
* A tool result (e.g. vision_present, or the host read_image) is persisted as
|
|
592
|
+
* a durable `tool/result` event whose message nests an image block. The agent
|
|
593
|
+
* pre-step only sees the inbox claim — never the historical surface — so no
|
|
594
|
+
* pre-step rewrite can catch these blocks before `Session.deriveMessages()`
|
|
595
|
+
* feeds them to the adapter, and a text-only adapter then rejects every
|
|
596
|
+
* subsequent request (issue #74: UNSUPPORTED_CONTENT session lock).
|
|
597
|
+
*
|
|
598
|
+
* The harness supports shadowing a surface node with a replacement event that
|
|
599
|
+
* carries `surfaceOp: {op:'replace', start, end}` + `sourceEventSeqs: [seq]`:
|
|
600
|
+
* the human transcript keeps rendering the append-origin original (the user
|
|
601
|
+
* still sees the image), while every later `deriveMessages()` projection sees
|
|
602
|
+
* the sanitized replacement. This is the same mechanism the host compaction
|
|
603
|
+
* pruner uses, so it is durable, replayable, and survives session resume.
|
|
604
|
+
*
|
|
605
|
+
* This function is pure: it returns the replacement events to append. The
|
|
606
|
+
* apply() side decides which events to strip (route-aware: an image-capable
|
|
607
|
+
* route legitimately uses read_image's result image) and performs the append.
|
|
608
|
+
*
|
|
609
|
+
* @param events - the session event log array (`session.events`).
|
|
610
|
+
* @param surfaceNodes - the ordered seqs of the current surface (`session.surface.nodes`).
|
|
611
|
+
* @param shouldStrip - (seq, event) => boolean; true to plan a replacement.
|
|
612
|
+
* @returns [{ seq, event, message }] where message is the sanitized frozen
|
|
613
|
+
* replacement message for the append at `seq`.
|
|
614
|
+
*/
|
|
615
|
+
export function planToolResultImageShadows(events, surfaceNodes, shouldStrip) {
|
|
616
|
+
const plans = []
|
|
617
|
+
for (const seq of surfaceNodes ?? []) {
|
|
618
|
+
const event = events && events[seq]
|
|
619
|
+
if (!event || event.type !== 'tool/result') continue
|
|
620
|
+
const message = event.data && event.data.message
|
|
621
|
+
if (!message || !Array.isArray(message.content) || !blocksHaveImage(message.content)) continue
|
|
622
|
+
if (typeof shouldStrip !== 'function' || shouldStrip(seq, event) !== true) continue
|
|
623
|
+
const sanitized = sanitizeToolResultMessage(message)
|
|
624
|
+
if (sanitized !== message) plans.push({ seq, event, message: sanitized })
|
|
625
|
+
}
|
|
626
|
+
return plans
|
|
627
|
+
}
|
|
628
|
+
|
|
423
629
|
/** Marker text for an image the text-only model cannot see (see vision_describe). */
|
|
424
630
|
function imageMarker(id) {
|
|
425
631
|
return `[attached image: ${id}] The current model cannot see images. To examine it, call vision_describe with attachmentIds: ["${id}"] and a specific question.`
|
|
@@ -447,6 +653,51 @@ export function rewriteImageBlocks(messages) {
|
|
|
447
653
|
return { messages: anyChanged ? rewritten : (messages ?? []), attachments }
|
|
448
654
|
}
|
|
449
655
|
|
|
656
|
+
/**
|
|
657
|
+
* Collect distinct durable attachment refs from a session event log.
|
|
658
|
+
*
|
|
659
|
+
* The event log is the only place that sees every image that entered the
|
|
660
|
+
* conversation, including host-produced ones such as `read_image` re-uploads,
|
|
661
|
+
* which are persisted as `tool/result` events and never pass through the
|
|
662
|
+
* inbox-claim message stream a plugin sees on `agent/pre-step` (issue #72).
|
|
663
|
+
* Extracting refs here — with full metadata, so `attachments.readImage` can
|
|
664
|
+
* verify the bytes — is what lets `vision_describe` / the pixel tools resolve
|
|
665
|
+
* ids the harness announced but the plugin never indexed.
|
|
666
|
+
*
|
|
667
|
+
* Handles the same message-producing event types the host surface derives
|
|
668
|
+
* (`user/message` carries the message directly; `assistant/message` and
|
|
669
|
+
* `tool/result` nest it under `data.message`) and descends into nested
|
|
670
|
+
* `tool-result` content exactly like `rewriteImageBlocks`.
|
|
671
|
+
*
|
|
672
|
+
* @param events - the session event log (`session.events`), or any array shaped like it.
|
|
673
|
+
* @returns distinct attachment refs in first-seen order.
|
|
674
|
+
*/
|
|
675
|
+
export function collectEventAttachmentRefs(events) {
|
|
676
|
+
const refs = []
|
|
677
|
+
const seen = new Set()
|
|
678
|
+
for (const event of events ?? []) {
|
|
679
|
+
if (!event || !event.data) continue
|
|
680
|
+
let message
|
|
681
|
+
if (event.type === 'user/message') {
|
|
682
|
+
message = event.data
|
|
683
|
+
} else if (event.type === 'assistant/message' || event.type === 'tool/result') {
|
|
684
|
+
message = event.data.message
|
|
685
|
+
} else {
|
|
686
|
+
continue
|
|
687
|
+
}
|
|
688
|
+
if (!message || !Array.isArray(message.content)) continue
|
|
689
|
+
rewriteImagesDeep(message.content, (block) => {
|
|
690
|
+
const attachment = block && block.attachment
|
|
691
|
+
if (attachment && attachment.attachmentId && !seen.has(String(attachment.attachmentId))) {
|
|
692
|
+
seen.add(String(attachment.attachmentId))
|
|
693
|
+
refs.push(attachment)
|
|
694
|
+
}
|
|
695
|
+
return block
|
|
696
|
+
})
|
|
697
|
+
}
|
|
698
|
+
return refs
|
|
699
|
+
}
|
|
700
|
+
|
|
450
701
|
/** Extract a JSON object/array from model output (tolerates fences and prose). */
|
|
451
702
|
export function extractJson(text) {
|
|
452
703
|
const source = String(text ?? '')
|
|
@@ -1755,6 +2006,11 @@ export function modelInfoAcceptsImages(info) {
|
|
|
1755
2006
|
}
|
|
1756
2007
|
|
|
1757
2008
|
export function apply(ctx, config = {}) {
|
|
2009
|
+
// Route sharp version diagnostics (issue #75) through the harness logger
|
|
2010
|
+
// instead of console.warn, so the warning lands in the server log.
|
|
2011
|
+
registerSharpWarningHook((message) => {
|
|
2012
|
+
ctx.logger?.warn(message)
|
|
2013
|
+
})
|
|
1758
2014
|
// Live configuration: composition entry at boot, then the resolved settings
|
|
1759
2015
|
// section once the settings service mounts (installSettingsSection below).
|
|
1760
2016
|
let current = () => config
|
|
@@ -2710,6 +2966,39 @@ export function apply(ctx, config = {}) {
|
|
|
2710
2966
|
}
|
|
2711
2967
|
}
|
|
2712
2968
|
|
|
2969
|
+
// session id -> event-log length already scanned for attachment refs.
|
|
2970
|
+
// Mirrors sessionAttachmentsById: sessions are long-lived objects, and only
|
|
2971
|
+
// the id string survives a process resume, so the index is keyed by id.
|
|
2972
|
+
const scannedSessionEventSeqs = new Map()
|
|
2973
|
+
|
|
2974
|
+
/**
|
|
2975
|
+
* Index image attachments recorded anywhere in the session event log, not
|
|
2976
|
+
* just in the inbox-claim message stream `agent/pre-step` hands us
|
|
2977
|
+
* (issue #72). Host-produced images (the built-in `read_image` tool's
|
|
2978
|
+
* re-uploads, persisted as `tool/result` events) never enter that stream,
|
|
2979
|
+
* so the harness-announced attachment id stayed unresolvable even though
|
|
2980
|
+
* the UI showed the image and the bytes are durably stored. `session.events`
|
|
2981
|
+
* is the complete append-only log (seeded from storage on resume), so
|
|
2982
|
+
* scanning it — incrementally, per session id — finds every ref with full
|
|
2983
|
+
* metadata for a later `attachments.readImage(ref)`.
|
|
2984
|
+
*/
|
|
2985
|
+
const scanSessionEventLog = (session) => {
|
|
2986
|
+
if (!session) return
|
|
2987
|
+
let events
|
|
2988
|
+
try {
|
|
2989
|
+
events = session.events
|
|
2990
|
+
} catch {
|
|
2991
|
+
return // not a host Session (or the getter is unavailable): nothing to scan
|
|
2992
|
+
}
|
|
2993
|
+
if (!Array.isArray(events) || events.length === 0) return
|
|
2994
|
+
const key = session.id !== undefined ? String(session.id) : undefined
|
|
2995
|
+
const last = key !== undefined ? (scannedSessionEventSeqs.get(key) ?? 0) : 0
|
|
2996
|
+
if (last >= events.length) return
|
|
2997
|
+
const refs = collectEventAttachmentRefs(events.slice(last))
|
|
2998
|
+
if (key !== undefined) scannedSessionEventSeqs.set(key, events.length)
|
|
2999
|
+
if (refs.length > 0) recordUploadedAttachments(session, refs)
|
|
3000
|
+
}
|
|
3001
|
+
|
|
2713
3002
|
const lookupAttachment = (session, id) => {
|
|
2714
3003
|
const byId = session && session.id !== undefined
|
|
2715
3004
|
? sessionAttachmentsById.get(String(session.id))
|
|
@@ -2719,7 +3008,163 @@ export function apply(ctx, config = {}) {
|
|
|
2719
3008
|
if (hit !== undefined) return hit
|
|
2720
3009
|
}
|
|
2721
3010
|
const map = session ? sessionAttachments.get(session) : undefined
|
|
2722
|
-
|
|
3011
|
+
const hit = map ? map.get(String(id)) : undefined
|
|
3012
|
+
if (hit !== undefined) return hit
|
|
3013
|
+
// Miss: fall back to the session event log. Ids announced by the harness
|
|
3014
|
+
// for images it persisted itself (read_image re-uploads) live there even
|
|
3015
|
+
// though they never crossed the inbox-claim stream, so this resolves them
|
|
3016
|
+
// exactly like user-uploaded ids (issue #72). Refs from the log carry
|
|
3017
|
+
// full metadata, so a later attachments.readImage(ref) verifies and
|
|
3018
|
+
// returns the bytes.
|
|
3019
|
+
if (session !== undefined) {
|
|
3020
|
+
scanSessionEventLog(session)
|
|
3021
|
+
const afterById = session.id !== undefined
|
|
3022
|
+
? sessionAttachmentsById.get(String(session.id))
|
|
3023
|
+
: undefined
|
|
3024
|
+
if (afterById !== undefined) {
|
|
3025
|
+
const after = afterById.get(String(id))
|
|
3026
|
+
if (after !== undefined) return after
|
|
3027
|
+
}
|
|
3028
|
+
const afterMap = sessionAttachments.get(session)
|
|
3029
|
+
const afterHit = afterMap ? afterMap.get(String(id)) : undefined
|
|
3030
|
+
if (afterHit !== undefined) return afterHit
|
|
3031
|
+
}
|
|
3032
|
+
return undefined
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// ── issue #74: shadow-sanitize tool-result image blocks on the surface ────
|
|
3036
|
+
//
|
|
3037
|
+
// A tool result that renders an image block (vision_present, or the host
|
|
3038
|
+
// read_image on image-capable routes) is persisted as a durable
|
|
3039
|
+
// `tool/result` event and then flows into EVERY later request through
|
|
3040
|
+
// `Session.deriveMessages()`. A text-only adapter (DeepSeek native, pi-ai
|
|
3041
|
+
// text routes) rejects nested images with UNSUPPORTED_CONTENT and the
|
|
3042
|
+
// session is locked forever. The pre-step inbox sanitizer cannot see these
|
|
3043
|
+
// historical events, so the surface itself is rewritten instead: each
|
|
3044
|
+
// offending event is shadowed by a sanitized replacement event
|
|
3045
|
+
// (`surfaceOp: {op:'replace'}`, the same mechanism the host compaction
|
|
3046
|
+
// pruner uses). The Web UI transcript renders append-origin events, so the
|
|
3047
|
+
// user still sees the image; only the model-visible surface is sanitized.
|
|
3048
|
+
// The decision is route-aware: an image-capable route legitimately consumes
|
|
3049
|
+
// read_image's result image, mirroring the host's own gate
|
|
3050
|
+
// (`assertImageCapableRoute` in dsh-tool-fs).
|
|
3051
|
+
|
|
3052
|
+
// session -> { count: nodes examined, done: Set<seqs already decided> }
|
|
3053
|
+
const sessionSurfaceScans = new WeakMap()
|
|
3054
|
+
|
|
3055
|
+
const sessionRouteHandlesImages = async (session) => {
|
|
3056
|
+
let provider
|
|
3057
|
+
let model
|
|
3058
|
+
try {
|
|
3059
|
+
const header = typeof session.requestHeader === 'function' ? session.requestHeader() : undefined
|
|
3060
|
+
provider = header && header.config ? header.config.provider : undefined
|
|
3061
|
+
model = header && header.config ? header.config.model : undefined
|
|
3062
|
+
} catch {
|
|
3063
|
+
return false
|
|
3064
|
+
}
|
|
3065
|
+
if (typeof provider !== 'string' || provider === '' || typeof model !== 'string' || model === '') {
|
|
3066
|
+
return false
|
|
3067
|
+
}
|
|
3068
|
+
// Plugin-owned routes handle image blocks at their stream boundary: the
|
|
3069
|
+
// wrapper and the provider twins rewrite them into markers/descriptions,
|
|
3070
|
+
// the stealth adapter does the same, and the vision-chain route serves
|
|
3071
|
+
// image-capable models.
|
|
3072
|
+
if (provider === wrapperRoute() || provider === chainRoute() || provider.endsWith('-vision')) {
|
|
3073
|
+
return true
|
|
3074
|
+
}
|
|
3075
|
+
if (stealthActive && provider === 'deepseek-official') return true
|
|
3076
|
+
// Routing mode reverse-routes text-only turns back to the text provider;
|
|
3077
|
+
// when neither the wrapper nor the stealth adapter is there to rewrite
|
|
3078
|
+
// images, the reverse target is the bare text adapter, which rejects
|
|
3079
|
+
// tool-result images. Sanitize so text turns stay usable.
|
|
3080
|
+
if (routingEnabled() && reverseRoutingEnabled() && !wrapperRegistered && !stealthActive) {
|
|
3081
|
+
return false
|
|
3082
|
+
}
|
|
3083
|
+
// Same probe the host uses to gate read_image: only routes whose models
|
|
3084
|
+
// declare image input may keep tool-result images.
|
|
3085
|
+
try {
|
|
3086
|
+
const info = await ctx.llm.resolveModelInfo(provider, model)
|
|
3087
|
+
return Array.isArray(info && info.inputModalities) && info.inputModalities.includes('image')
|
|
3088
|
+
} catch {
|
|
3089
|
+
return false // unknown route: fail safe and sanitize
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
const sanitizeSessionToolResults = async (session) => {
|
|
3094
|
+
if (!session) return
|
|
3095
|
+
let events
|
|
3096
|
+
let nodes
|
|
3097
|
+
try {
|
|
3098
|
+
events = session.events
|
|
3099
|
+
nodes = session.surface && session.surface.nodes
|
|
3100
|
+
} catch {
|
|
3101
|
+
return // not a host Session: nothing to sanitize
|
|
3102
|
+
}
|
|
3103
|
+
if (!Array.isArray(events) || !Array.isArray(nodes) || nodes.length === 0) return
|
|
3104
|
+
let scan = sessionSurfaceScans.get(session)
|
|
3105
|
+
if (!scan) {
|
|
3106
|
+
scan = { count: 0, done: new Set() }
|
|
3107
|
+
sessionSurfaceScans.set(session, scan)
|
|
3108
|
+
}
|
|
3109
|
+
// Compaction replaces the surface wholesale; a shrunk node list means the
|
|
3110
|
+
// positional cursor is stale, so restart from the head. Kept decisions are
|
|
3111
|
+
// memoized in `done`, so a restart is a cheap no-op for examined events.
|
|
3112
|
+
if (nodes.length < scan.count) {
|
|
3113
|
+
scan.count = 0
|
|
3114
|
+
scan.done = new Set()
|
|
3115
|
+
}
|
|
3116
|
+
if (nodes.length === scan.count) return
|
|
3117
|
+
let routeHandlesImages
|
|
3118
|
+
const newSeqs = nodes.slice(scan.count)
|
|
3119
|
+
for (const seq of newSeqs) {
|
|
3120
|
+
const event = events[seq]
|
|
3121
|
+
if (!event || event.type !== 'tool/result' || scan.done.has(seq)) continue
|
|
3122
|
+
const message = event.data && event.data.message
|
|
3123
|
+
if (!message || !Array.isArray(message.content) || !blocksHaveImage(message.content)) {
|
|
3124
|
+
scan.done.add(seq)
|
|
3125
|
+
continue
|
|
3126
|
+
}
|
|
3127
|
+
if (routeHandlesImages === undefined) {
|
|
3128
|
+
routeHandlesImages = await sessionRouteHandlesImages(session)
|
|
3129
|
+
}
|
|
3130
|
+
if (routeHandlesImages) {
|
|
3131
|
+
// Image-capable route: keep the image (read_image's result is the
|
|
3132
|
+
// model's view of the file). The wrapper/twin/stealth routes rewrite
|
|
3133
|
+
// it at stream time anyway.
|
|
3134
|
+
scan.done.add(seq)
|
|
3135
|
+
continue
|
|
3136
|
+
}
|
|
3137
|
+
const sanitized = sanitizeToolResultMessage(message)
|
|
3138
|
+
if (sanitized === message) {
|
|
3139
|
+
scan.done.add(seq)
|
|
3140
|
+
continue
|
|
3141
|
+
}
|
|
3142
|
+
try {
|
|
3143
|
+
session.append(
|
|
3144
|
+
'tool/result',
|
|
3145
|
+
{ ...event.data, message: sanitized },
|
|
3146
|
+
{
|
|
3147
|
+
surfaceOp: { op: 'replace', start: seq, end: seq },
|
|
3148
|
+
sourceEventSeqs: [seq],
|
|
3149
|
+
},
|
|
3150
|
+
)
|
|
3151
|
+
scan.done.add(seq)
|
|
3152
|
+
ctx.logger?.info(
|
|
3153
|
+
'vision-router: sanitized a tool-result image block out of the model surface (event seq %s)',
|
|
3154
|
+
seq,
|
|
3155
|
+
)
|
|
3156
|
+
} catch (error) {
|
|
3157
|
+
// A failed shadow leaves the original event on the surface: the
|
|
3158
|
+
// session stays usable (today's behavior) instead of crashing the
|
|
3159
|
+
// pre-step.
|
|
3160
|
+
ctx.logger?.warn(
|
|
3161
|
+
'vision-router: could not sanitize tool-result image at event seq %s (%s)',
|
|
3162
|
+
seq,
|
|
3163
|
+
error && error.message ? error.message : String(error),
|
|
3164
|
+
)
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
scan.count += newSeqs.length
|
|
2723
3168
|
}
|
|
2724
3169
|
|
|
2725
3170
|
// session -> { turn, startIndex, hasImage, routed, failures, lastError }
|
|
@@ -2735,12 +3180,37 @@ export function apply(ctx, config = {}) {
|
|
|
2735
3180
|
// sanitized. This preserves attachment lookup for a later vision_describe.
|
|
2736
3181
|
const rawImageRefs = rewriteImageBlocks(rawMessages)
|
|
2737
3182
|
recordUploadedAttachments(session, rawImageRefs.attachments)
|
|
3183
|
+
// Also index image attachments that live only in the session event log
|
|
3184
|
+
// (read_image re-uploads never cross the inbox-claim message stream).
|
|
3185
|
+
// Incremental: scans only new events (issue #72).
|
|
3186
|
+
scanSessionEventLog(session)
|
|
2738
3187
|
// Hard invariant: tool-produced image blocks never reach a model request.
|
|
2739
|
-
//
|
|
2740
|
-
//
|
|
3188
|
+
// Two layers: (1) sanitize tool-result images in the inbox claim, and
|
|
3189
|
+
// (2) shadow-sanitize historical tool/result events on the session
|
|
3190
|
+
// surface (issue #74) — the only lever that can rewrite durable history.
|
|
2741
3191
|
const sanitizedToolResults = sanitizeToolResultImages(rawMessages)
|
|
2742
3192
|
const messages = sanitizedToolResults.messages
|
|
2743
3193
|
const hasImage = messages.some((message) => blocksHaveImage(message && message.content))
|
|
3194
|
+
try {
|
|
3195
|
+
await sanitizeSessionToolResults(session)
|
|
3196
|
+
} catch (error) {
|
|
3197
|
+
ctx.logger?.warn(
|
|
3198
|
+
'vision-router: session-surface sanitization failed (%s)',
|
|
3199
|
+
error && error.message ? error.message : String(error),
|
|
3200
|
+
)
|
|
3201
|
+
}
|
|
3202
|
+
// Register the turn state BEFORE the image-turn branches below: those
|
|
3203
|
+
// branches return early (auto-mount reminder, history rewrite), and the
|
|
3204
|
+
// agent/request hook must still see the state, otherwise an image turn is
|
|
3205
|
+
// served by the text provider and rejected (issue #74, second root cause).
|
|
3206
|
+
if (routingEnabled()) {
|
|
3207
|
+
const events = session.events ?? []
|
|
3208
|
+
turnState.set(session, {
|
|
3209
|
+
turn: payload.turn,
|
|
3210
|
+
startIndex: events.length,
|
|
3211
|
+
hasImage,
|
|
3212
|
+
})
|
|
3213
|
+
}
|
|
2744
3214
|
if (hasImage) {
|
|
2745
3215
|
// Auto-mount the deep vision tools on image turns: the model can use
|
|
2746
3216
|
// them from its very first step without the user asking for them.
|
|
@@ -2805,14 +3275,6 @@ export function apply(ctx, config = {}) {
|
|
|
2805
3275
|
return { ...decision, messages: cleaned.messages }
|
|
2806
3276
|
}
|
|
2807
3277
|
}
|
|
2808
|
-
if (routingEnabled()) {
|
|
2809
|
-
const events = session.events ?? []
|
|
2810
|
-
turnState.set(session, {
|
|
2811
|
-
turn: payload.turn,
|
|
2812
|
-
startIndex: events.length,
|
|
2813
|
-
hasImage,
|
|
2814
|
-
})
|
|
2815
|
-
}
|
|
2816
3278
|
return sanitizedToolResults.changed ? { ...decision, messages } : decision
|
|
2817
3279
|
})
|
|
2818
3280
|
|
package/lib/client.js
CHANGED
|
@@ -554,20 +554,116 @@ window.__ModuleLoader__.load({
|
|
|
554
554
|
}
|
|
555
555
|
|
|
556
556
|
const ONBOARDING_STORAGE_KEY = 'dsh-vision-router:onboarding:model-guide-v2'
|
|
557
|
+
// issue #78: DSH Desktop serves the Web UI from a random port on every
|
|
558
|
+
// launch (--port 0), so origin-scoped localStorage forgets everything
|
|
559
|
+
// between restarts and the first-run dialog re-appeared on every boot.
|
|
560
|
+
// The durable source of truth is now the `vision-router` settings section
|
|
561
|
+
// (the profile settings file, synced through ctx.settingsScope);
|
|
562
|
+
// localStorage survives only as a legacy/migration fallback for
|
|
563
|
+
// pre-v1.2.3 browsers and as a best-effort downgrade channel.
|
|
564
|
+
const ONBOARDING_SETTINGS_KEY = 'onboardingSeen'
|
|
557
565
|
// The walkthrough has three steps: step1 (the session/text model selector
|
|
558
566
|
// on the chat page), step2 (open Settings → Plugins → Vision Router), and
|
|
559
567
|
// step3 (the highlighted vision chain, rendered by the settings card as a
|
|
560
568
|
// callout). Only step1/step2 need persistence; step3 re-derives from the
|
|
561
569
|
// card being on screen.
|
|
562
570
|
const VISION_GUIDE_STORAGE_KEY = 'dsh-vision-router:guide:vision-backend-v2'
|
|
571
|
+
const VISION_GUIDE_SETTINGS_KEY = 'visionGuideStep'
|
|
563
572
|
const VISION_GUIDE_EVENT = 'dsh-vision-router:vision-settings-guide'
|
|
564
573
|
let visionGuidePrompt
|
|
565
574
|
let visionGuideStepMemory
|
|
575
|
+
// Installed by apply(): a narrow, defensive view of the settings scope.
|
|
576
|
+
// { get, set, unset } never throw, and subscribe returns a disposer (or
|
|
577
|
+
// undefined when the scope is unavailable — the legacy paths take over).
|
|
578
|
+
let settingsPersistence
|
|
579
|
+
function installSettingsPersistence(scope) {
|
|
580
|
+
const readSection = () => {
|
|
581
|
+
try {
|
|
582
|
+
const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
|
|
583
|
+
return snapshot && snapshot.value ? snapshot.value : undefined
|
|
584
|
+
} catch {
|
|
585
|
+
return undefined
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
settingsPersistence = {
|
|
589
|
+
get(field) {
|
|
590
|
+
const section = readSection()
|
|
591
|
+
return section ? section[field] : undefined
|
|
592
|
+
},
|
|
593
|
+
set(field, value) {
|
|
594
|
+
try {
|
|
595
|
+
const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
|
|
596
|
+
if (!snapshot || !snapshot.writable || typeof scope.set !== 'function') return
|
|
597
|
+
void scope.set(field, value)
|
|
598
|
+
} catch {
|
|
599
|
+
// Read-only provider or unavailable scope: the localStorage
|
|
600
|
+
// fallback keeps this load functional.
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
unset(field) {
|
|
604
|
+
try {
|
|
605
|
+
const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
|
|
606
|
+
if (!snapshot || !snapshot.writable || typeof scope.unset !== 'function') return
|
|
607
|
+
void scope.unset(field)
|
|
608
|
+
} catch {
|
|
609
|
+
// Same fallback reasoning as set().
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
subscribe(listener) {
|
|
613
|
+
try {
|
|
614
|
+
return scope && typeof scope.subscribe === 'function' ? scope.subscribe(listener) : undefined
|
|
615
|
+
} catch {
|
|
616
|
+
return undefined
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function readOnboardingSeen() {
|
|
623
|
+
try {
|
|
624
|
+
if (settingsPersistence && settingsPersistence.get(ONBOARDING_SETTINGS_KEY) === true) return true
|
|
625
|
+
} catch {
|
|
626
|
+
// Fall through to the legacy origin-scoped marker.
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
if (window.localStorage && window.localStorage.getItem(ONBOARDING_STORAGE_KEY) === 'seen') {
|
|
630
|
+
// Legacy value from before the settings-backed flag: migrate it so
|
|
631
|
+
// it survives desktop restarts, then honor it for this load.
|
|
632
|
+
if (settingsPersistence) settingsPersistence.set(ONBOARDING_SETTINGS_KEY, true)
|
|
633
|
+
return true
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
// Best effort only.
|
|
637
|
+
}
|
|
638
|
+
return false
|
|
639
|
+
}
|
|
640
|
+
function rememberOnboardingSeen() {
|
|
641
|
+
try {
|
|
642
|
+
if (settingsPersistence) settingsPersistence.set(ONBOARDING_SETTINGS_KEY, true)
|
|
643
|
+
} catch {
|
|
644
|
+
// The dialog can still be dismissed for this page load.
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
if (window.localStorage) window.localStorage.setItem(ONBOARDING_STORAGE_KEY, 'seen')
|
|
648
|
+
} catch {
|
|
649
|
+
// Best effort: the dialog can still be dismissed for this page load.
|
|
650
|
+
}
|
|
651
|
+
}
|
|
566
652
|
|
|
567
653
|
function readVisionGuideStep() {
|
|
568
654
|
try {
|
|
569
|
-
const value =
|
|
655
|
+
const value = settingsPersistence && settingsPersistence.get(VISION_GUIDE_SETTINGS_KEY)
|
|
570
656
|
if (value === 'step1' || value === 'step2') return value
|
|
657
|
+
} catch {
|
|
658
|
+
// Fall through to the legacy origin-scoped marker.
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const value = window.localStorage && window.localStorage.getItem(VISION_GUIDE_STORAGE_KEY)
|
|
662
|
+
if (value === 'step1' || value === 'step2') {
|
|
663
|
+
// Legacy value: migrate into settings for desktop durability.
|
|
664
|
+
if (settingsPersistence) settingsPersistence.set(VISION_GUIDE_SETTINGS_KEY, value)
|
|
665
|
+
return value
|
|
666
|
+
}
|
|
571
667
|
} catch {
|
|
572
668
|
// Fall through to page-memory state when storage access is blocked.
|
|
573
669
|
}
|
|
@@ -575,6 +671,14 @@ window.__ModuleLoader__.load({
|
|
|
575
671
|
}
|
|
576
672
|
function writeVisionGuideStep(step) {
|
|
577
673
|
visionGuideStepMemory = step === 'step1' || step === 'step2' ? step : undefined
|
|
674
|
+
try {
|
|
675
|
+
if (settingsPersistence) {
|
|
676
|
+
if (visionGuideStepMemory) settingsPersistence.set(VISION_GUIDE_SETTINGS_KEY, visionGuideStepMemory)
|
|
677
|
+
else settingsPersistence.unset(VISION_GUIDE_SETTINGS_KEY)
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
// Page-memory state still keeps the guide functional for this load.
|
|
681
|
+
}
|
|
578
682
|
try {
|
|
579
683
|
if (!window.localStorage) return
|
|
580
684
|
if (visionGuideStepMemory) window.localStorage.setItem(VISION_GUIDE_STORAGE_KEY, visionGuideStepMemory)
|
|
@@ -677,9 +781,13 @@ window.__ModuleLoader__.load({
|
|
|
677
781
|
syncVisionGuidePrompt(t)
|
|
678
782
|
window.addEventListener(VISION_GUIDE_EVENT, sync)
|
|
679
783
|
window.addEventListener('popstate', sync)
|
|
784
|
+
// The durable step now lives in the settings section; re-sync when its
|
|
785
|
+
// snapshot changes (e.g. the first read resolves after page load).
|
|
786
|
+
const unsubscribe = settingsPersistence && settingsPersistence.subscribe(sync)
|
|
680
787
|
return () => {
|
|
681
788
|
window.removeEventListener(VISION_GUIDE_EVENT, sync)
|
|
682
789
|
window.removeEventListener('popstate', sync)
|
|
790
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
683
791
|
removeVisionGuidePrompt()
|
|
684
792
|
}
|
|
685
793
|
}
|
|
@@ -687,13 +795,6 @@ window.__ModuleLoader__.load({
|
|
|
687
795
|
let onboardingOverlay
|
|
688
796
|
let onboardingKeyDown
|
|
689
797
|
|
|
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
798
|
function dismissOnboarding(remember = true) {
|
|
698
799
|
if (remember) rememberOnboardingSeen()
|
|
699
800
|
if (typeof document !== 'undefined' && onboardingKeyDown) {
|
|
@@ -779,16 +880,33 @@ window.__ModuleLoader__.load({
|
|
|
779
880
|
}
|
|
780
881
|
function installOnboarding(t) {
|
|
781
882
|
if (typeof document === 'undefined' || typeof window === 'undefined') return
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
883
|
+
let timer
|
|
884
|
+
const clearTimer = () => {
|
|
885
|
+
if (timer !== undefined) window.clearTimeout(timer)
|
|
886
|
+
timer = undefined
|
|
786
887
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
888
|
+
// Reactive against the settings scope: the settings snapshot arrives
|
|
889
|
+
// asynchronously on a cold page load, so a one-shot localStorage check
|
|
890
|
+
// could show the dialog before the durable "seen" flag lands. Re-sync
|
|
891
|
+
// on every snapshot change and dismiss the overlay if the flag appears.
|
|
892
|
+
const sync = () => {
|
|
893
|
+
if (readOnboardingSeen()) {
|
|
894
|
+
clearTimer()
|
|
895
|
+
if (onboardingOverlay) dismissOnboarding(false)
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
if (timer === undefined && !onboardingOverlay) {
|
|
899
|
+
timer = window.setTimeout(() => {
|
|
900
|
+
timer = undefined
|
|
901
|
+
if (!readOnboardingSeen()) showOnboarding(t)
|
|
902
|
+
}, 650)
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
sync()
|
|
906
|
+
const unsubscribe = settingsPersistence && settingsPersistence.subscribe(sync)
|
|
790
907
|
return () => {
|
|
791
|
-
|
|
908
|
+
clearTimer()
|
|
909
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
792
910
|
// Unmounting is not a user choice: drop the dialog without marking
|
|
793
911
|
// the guide seen, so the first-run overview can still auto-open.
|
|
794
912
|
if (onboardingOverlay) dismissOnboarding(false)
|
|
@@ -1894,6 +2012,10 @@ window.__ModuleLoader__.load({
|
|
|
1894
2012
|
|
|
1895
2013
|
function apply(ctx) {
|
|
1896
2014
|
const scope = ctx.settingsScope.bind({ namespace: 'vision-router' })
|
|
2015
|
+
// issue #78: route the onboarding/guide durability through the settings
|
|
2016
|
+
// section (profile settings file) so it survives DSH Desktop's random
|
|
2017
|
+
// per-launch port. Installed before the effects below run.
|
|
2018
|
+
installSettingsPersistence(scope)
|
|
1897
2019
|
const presentedImageUrls = new Map()
|
|
1898
2020
|
const createdImageUrls = new Set()
|
|
1899
2021
|
const loadPresentedImage = (sessionId, attachment) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|