dsh-vision-router 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/README.zh.md +16 -6
- package/assets/vision-tools-zh.svg +3 -3
- package/assets/vision-tools.svg +3 -3
- package/index.js +478 -25
- 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,11 @@
|
|
|
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.2)**
|
|
32
32
|
>
|
|
33
|
-
> v1.2.
|
|
33
|
+
> v1.2.2 closes 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) — stops `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 warns loudly when a stale sharp left over from a pre-v1.2 upgrade would break the pixel tools with `colourspace: parameter space not set` (issue #75).
|
|
34
|
+
>
|
|
35
|
+
> 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
36
|
|
|
35
37
|
<p align="center">
|
|
36
38
|
<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 +127,7 @@ The built-in anonymous OVH vision fallback is already configured, so normal imag
|
|
|
125
127
|
- **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
128
|
- **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
129
|
- **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
|
|
130
|
+
- **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
131
|
- **Selective proxy.** Only the configured vision provider hosts go through your local proxy; DeepSeek stays direct.
|
|
130
132
|
|
|
131
133
|
### Pixel loop in practice
|
|
@@ -146,10 +148,10 @@ The vision model is **only the eyes**; DeepSeek is **always the brain**. An imag
|
|
|
146
148
|
|
|
147
149
|
## Tools
|
|
148
150
|
|
|
149
|
-
All
|
|
151
|
+
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
152
|
|
|
151
153
|
<p align="center">
|
|
152
|
-
<img src="assets/vision-tools.svg" width="100%" alt="
|
|
154
|
+
<img src="assets/vision-tools.svg" width="100%" alt="Eleven vision tools available in DSH Vision Router." />
|
|
153
155
|
</p>
|
|
154
156
|
|
|
155
157
|
| Tool | What it does | Artifact |
|
|
@@ -340,6 +342,15 @@ Settings live in the profile's settings provider and survive upgrades.
|
|
|
340
342
|
> # your overrides …
|
|
341
343
|
> ```
|
|
342
344
|
|
|
345
|
+
> **After upgrading from v1.1.x, pixel tools fail with
|
|
346
|
+
> `colourspace: parameter space not set`:** a stale sharp 0.34.0 from the
|
|
347
|
+
> v1.1.0 era still sits in the profile and its libvips DLL conflicts with the
|
|
348
|
+
> host's sharp 0.35.3 in the same process (issues #42 / #75). Delete
|
|
349
|
+
> `~/.dsh/profiles/<profile>/node_modules/sharp` and
|
|
350
|
+
> `~/.dsh/profiles/<profile>/node_modules/@img` and restart, or run
|
|
351
|
+
> `pnpm install` inside the profile. Since v1.2.2 the plugin detects the
|
|
352
|
+
> stale version at runtime and prints the same guidance itself.
|
|
353
|
+
|
|
343
354
|
### Uninstall
|
|
344
355
|
|
|
345
356
|
```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,11 @@
|
|
|
28
28
|
<p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
|
|
29
29
|
|
|
30
30
|
> [!WARNING]
|
|
31
|
-
> 📌 **公告(v1.2.
|
|
31
|
+
> 📌 **公告(v1.2.2)**
|
|
32
32
|
>
|
|
33
|
-
> v1.2.
|
|
33
|
+
> v1.2.2 现已支持:补上最后一处附件 ID 缺口——宿主 `read_image` 回挂图片公布的 `sha256:…` ID 现在可被 `vision_describe` 与全部像素工具解析(issue #72);`vision_present` 等工具结果里的图像块不再把文本模型会话锁死在 `UNSUPPORTED_CONTENT`(issue #74,已锁死的历史会话升级后自动修复);检测到 v1.1.x 升级残留的旧版 sharp 时明确告警,把玄学的 `colourspace` 报错变成一眼可见的修复指引(issue #75)。
|
|
34
|
+
>
|
|
35
|
+
> v1.2.1 加固像素闭环:十一个像素工具可直接接受上传图片的附件 ID(告别 `cannot read …/sha256:…` 的绕路),产物文件名带指纹不再互相覆盖,`vision_ground` 对退化框自动重试,模型引导支持从第 1 步完整重放(先退出设置页),设置卡片在数百模型目录下滚动依然流畅。
|
|
34
36
|
|
|
35
37
|
<p align="center">
|
|
36
38
|
<img src="assets/vision-demo.gif" width="640" alt="演示:粘贴图片,Agent 用 vision_ground / vision_crop / vision_pixel_diff 定位发送按钮并给出坐标" />
|
|
@@ -125,7 +127,7 @@ opencode-go + 自动识图 ← 发图片时选这个
|
|
|
125
127
|
- **自动降级 + 分类报错。** 地区限制、ToS 风控、402 额度、429 限流(尊重 Retry-After 退避重试)、上下文超长、网络故障——链路逐供应商尝试,全部失败才报错并给出可操作的建议。
|
|
126
128
|
- **图片记忆。** 视觉答案按附件内容哈希缓存;后续文字轮用记录的描述替换历史图片(标注为不可信证据),DeepSeek 真正“记得”之前发过的图,且不重复消耗视觉调用。
|
|
127
129
|
- **可验证的像素闭环。** 参照图 → `vision_html_screenshot` → `vision_pixel_diff`(差异率 + 红色热力图 + 最差区域排行)→ 修复 → 再对比,直到差异收敛。UI 还原从“目测”变成“实测”。
|
|
128
|
-
- **渐进式 schema 暴露。** 平时只有一个零参引导工具 `vision_activate`;图片轮自动挂载全部
|
|
130
|
+
- **渐进式 schema 暴露。** 平时只有一个零参引导工具 `vision_activate`;图片轮自动挂载全部 11 个深看工具(附一次性使用提示),并为纯文字轮注册 `vision-tools` 技能。
|
|
129
131
|
- **选择性代理。** 只有配置的视觉供应商域名走本地代理;DeepSeek 保持直连。
|
|
130
132
|
|
|
131
133
|
### 像素闭环实测
|
|
@@ -146,10 +148,10 @@ Agent 仅根据参考图复刻 UI,再用 `vision_pixel_diff` 验证最终结
|
|
|
146
148
|
|
|
147
149
|
## 工具
|
|
148
150
|
|
|
149
|
-
|
|
151
|
+
11 个深看工具在图片轮自动挂载(`autoActivateOnImage`);文字轮可通过 `vision_activate` 或 `/vision-tools` 技能挂载。全部基于 sharp / potrace / tesseract / 系统 Chrome——无 Python:
|
|
150
152
|
|
|
151
153
|
<p align="center">
|
|
152
|
-
<img src="assets/vision-tools-zh.svg" width="100%" alt="DSH Vision Router 的
|
|
154
|
+
<img src="assets/vision-tools-zh.svg" width="100%" alt="DSH Vision Router 的 11 个视觉工具。" />
|
|
153
155
|
</p>
|
|
154
156
|
|
|
155
157
|
| 工具 | 作用 | 产物 |
|
|
@@ -338,6 +340,14 @@ pnpm dsh plugin --profile web update dsh-vision-router
|
|
|
338
340
|
> # 你的配置…
|
|
339
341
|
> ```
|
|
340
342
|
|
|
343
|
+
> **从 v1.1.x 升级后像素工具报 `colourspace: parameter space not set`:**
|
|
344
|
+
> 这是 v1.1.0 时代自带的 sharp 0.34.0 残留在 profile 里、与宿主 sharp 0.35.3
|
|
345
|
+
> 同进程 DLL 冲突所致(issue #42 / #75)。删除
|
|
346
|
+
> `~/.dsh/profiles/<profile>/node_modules/sharp` 与
|
|
347
|
+
> `~/.dsh/profiles/<profile>/node_modules/@img` 后重启,或在 profile 目录执行
|
|
348
|
+
> `pnpm install` 重装依赖即可。v1.2.2 起插件会在检测到残留版本时直接告警并
|
|
349
|
+
> 给出同样的指引。
|
|
350
|
+
|
|
341
351
|
### 卸载
|
|
342
352
|
|
|
343
353
|
```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(
|
|
@@ -397,29 +523,100 @@ export function renderVisionPresent(value) {
|
|
|
397
523
|
]
|
|
398
524
|
}
|
|
399
525
|
|
|
526
|
+
/** Text marker replacing a tool-produced image block (shared by the pre-step
|
|
527
|
+
* inbox sanitizer and the session-surface shadow sanitizer). */
|
|
528
|
+
export function toolImageMarker(block) {
|
|
529
|
+
const attachment = block && block.attachment ? block.attachment : {}
|
|
530
|
+
const id = attachment.attachmentId || attachment.id || 'unknown'
|
|
531
|
+
const name = attachment.name || 'tool image'
|
|
532
|
+
return {
|
|
533
|
+
type: 'text',
|
|
534
|
+
text:
|
|
535
|
+
`[tool result produced image "${name}", attachment id "${id}". ` +
|
|
536
|
+
`The image was kept out of the text-model request to prevent session corruption. ` +
|
|
537
|
+
`To inspect it, call vision_describe with attachmentIds: ["${id}"] when available, ` +
|
|
538
|
+
'or use a path-based vision tool. To show a generated image to the user, use vision_present instead of read_image.]',
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
400
542
|
export function sanitizeToolResultImages(messages) {
|
|
401
543
|
let anyChanged = false
|
|
402
544
|
const rewritten = (messages ?? []).map((message) => {
|
|
403
545
|
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
|
-
})
|
|
546
|
+
const result = rewriteToolResultImages(message.content, toolImageMarker)
|
|
417
547
|
if (result.changed) anyChanged = true
|
|
418
548
|
return result.changed ? { ...message, content: result.content } : message
|
|
419
549
|
})
|
|
420
550
|
return { messages: anyChanged ? rewritten : (messages ?? []), changed: anyChanged }
|
|
421
551
|
}
|
|
422
552
|
|
|
553
|
+
/** Recursively freeze a plain structured-clone tree (the session log keeps its
|
|
554
|
+
* messages deep-frozen; replacements must match). */
|
|
555
|
+
export function deepFreezeLocal(value) {
|
|
556
|
+
if (value !== null && typeof value === 'object') {
|
|
557
|
+
for (const key of Object.keys(value)) deepFreezeLocal(value[key])
|
|
558
|
+
Object.freeze(value)
|
|
559
|
+
}
|
|
560
|
+
return value
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Build the sanitized, deep-frozen copy of a tool-result message: identical
|
|
565
|
+
* to the original except that every image block (top-level or nested inside
|
|
566
|
+
* tool-result content) is replaced with a text marker. Returns the original
|
|
567
|
+
* message object unchanged when it contains no image.
|
|
568
|
+
*/
|
|
569
|
+
export function sanitizeToolResultMessage(message) {
|
|
570
|
+
if (!message || !Array.isArray(message.content)) return message
|
|
571
|
+
const result = rewriteImagesDeep(message.content, toolImageMarker)
|
|
572
|
+
if (!result.changed) return message
|
|
573
|
+
const clone = structuredClone(message)
|
|
574
|
+
clone.content = result.content
|
|
575
|
+
return deepFreezeLocal(clone)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Plan the shadow replacements that keep tool-produced image blocks out of
|
|
580
|
+
* the model-visible session surface.
|
|
581
|
+
*
|
|
582
|
+
* A tool result (e.g. vision_present, or the host read_image) is persisted as
|
|
583
|
+
* a durable `tool/result` event whose message nests an image block. The agent
|
|
584
|
+
* pre-step only sees the inbox claim — never the historical surface — so no
|
|
585
|
+
* pre-step rewrite can catch these blocks before `Session.deriveMessages()`
|
|
586
|
+
* feeds them to the adapter, and a text-only adapter then rejects every
|
|
587
|
+
* subsequent request (issue #74: UNSUPPORTED_CONTENT session lock).
|
|
588
|
+
*
|
|
589
|
+
* The harness supports shadowing a surface node with a replacement event that
|
|
590
|
+
* carries `surfaceOp: {op:'replace', start, end}` + `sourceEventSeqs: [seq]`:
|
|
591
|
+
* the human transcript keeps rendering the append-origin original (the user
|
|
592
|
+
* still sees the image), while every later `deriveMessages()` projection sees
|
|
593
|
+
* the sanitized replacement. This is the same mechanism the host compaction
|
|
594
|
+
* pruner uses, so it is durable, replayable, and survives session resume.
|
|
595
|
+
*
|
|
596
|
+
* This function is pure: it returns the replacement events to append. The
|
|
597
|
+
* apply() side decides which events to strip (route-aware: an image-capable
|
|
598
|
+
* route legitimately uses read_image's result image) and performs the append.
|
|
599
|
+
*
|
|
600
|
+
* @param events - the session event log array (`session.events`).
|
|
601
|
+
* @param surfaceNodes - the ordered seqs of the current surface (`session.surface.nodes`).
|
|
602
|
+
* @param shouldStrip - (seq, event) => boolean; true to plan a replacement.
|
|
603
|
+
* @returns [{ seq, event, message }] where message is the sanitized frozen
|
|
604
|
+
* replacement message for the append at `seq`.
|
|
605
|
+
*/
|
|
606
|
+
export function planToolResultImageShadows(events, surfaceNodes, shouldStrip) {
|
|
607
|
+
const plans = []
|
|
608
|
+
for (const seq of surfaceNodes ?? []) {
|
|
609
|
+
const event = events && events[seq]
|
|
610
|
+
if (!event || event.type !== 'tool/result') continue
|
|
611
|
+
const message = event.data && event.data.message
|
|
612
|
+
if (!message || !Array.isArray(message.content) || !blocksHaveImage(message.content)) continue
|
|
613
|
+
if (typeof shouldStrip !== 'function' || shouldStrip(seq, event) !== true) continue
|
|
614
|
+
const sanitized = sanitizeToolResultMessage(message)
|
|
615
|
+
if (sanitized !== message) plans.push({ seq, event, message: sanitized })
|
|
616
|
+
}
|
|
617
|
+
return plans
|
|
618
|
+
}
|
|
619
|
+
|
|
423
620
|
/** Marker text for an image the text-only model cannot see (see vision_describe). */
|
|
424
621
|
function imageMarker(id) {
|
|
425
622
|
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 +644,51 @@ export function rewriteImageBlocks(messages) {
|
|
|
447
644
|
return { messages: anyChanged ? rewritten : (messages ?? []), attachments }
|
|
448
645
|
}
|
|
449
646
|
|
|
647
|
+
/**
|
|
648
|
+
* Collect distinct durable attachment refs from a session event log.
|
|
649
|
+
*
|
|
650
|
+
* The event log is the only place that sees every image that entered the
|
|
651
|
+
* conversation, including host-produced ones such as `read_image` re-uploads,
|
|
652
|
+
* which are persisted as `tool/result` events and never pass through the
|
|
653
|
+
* inbox-claim message stream a plugin sees on `agent/pre-step` (issue #72).
|
|
654
|
+
* Extracting refs here — with full metadata, so `attachments.readImage` can
|
|
655
|
+
* verify the bytes — is what lets `vision_describe` / the pixel tools resolve
|
|
656
|
+
* ids the harness announced but the plugin never indexed.
|
|
657
|
+
*
|
|
658
|
+
* Handles the same message-producing event types the host surface derives
|
|
659
|
+
* (`user/message` carries the message directly; `assistant/message` and
|
|
660
|
+
* `tool/result` nest it under `data.message`) and descends into nested
|
|
661
|
+
* `tool-result` content exactly like `rewriteImageBlocks`.
|
|
662
|
+
*
|
|
663
|
+
* @param events - the session event log (`session.events`), or any array shaped like it.
|
|
664
|
+
* @returns distinct attachment refs in first-seen order.
|
|
665
|
+
*/
|
|
666
|
+
export function collectEventAttachmentRefs(events) {
|
|
667
|
+
const refs = []
|
|
668
|
+
const seen = new Set()
|
|
669
|
+
for (const event of events ?? []) {
|
|
670
|
+
if (!event || !event.data) continue
|
|
671
|
+
let message
|
|
672
|
+
if (event.type === 'user/message') {
|
|
673
|
+
message = event.data
|
|
674
|
+
} else if (event.type === 'assistant/message' || event.type === 'tool/result') {
|
|
675
|
+
message = event.data.message
|
|
676
|
+
} else {
|
|
677
|
+
continue
|
|
678
|
+
}
|
|
679
|
+
if (!message || !Array.isArray(message.content)) continue
|
|
680
|
+
rewriteImagesDeep(message.content, (block) => {
|
|
681
|
+
const attachment = block && block.attachment
|
|
682
|
+
if (attachment && attachment.attachmentId && !seen.has(String(attachment.attachmentId))) {
|
|
683
|
+
seen.add(String(attachment.attachmentId))
|
|
684
|
+
refs.push(attachment)
|
|
685
|
+
}
|
|
686
|
+
return block
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
return refs
|
|
690
|
+
}
|
|
691
|
+
|
|
450
692
|
/** Extract a JSON object/array from model output (tolerates fences and prose). */
|
|
451
693
|
export function extractJson(text) {
|
|
452
694
|
const source = String(text ?? '')
|
|
@@ -1755,6 +1997,11 @@ export function modelInfoAcceptsImages(info) {
|
|
|
1755
1997
|
}
|
|
1756
1998
|
|
|
1757
1999
|
export function apply(ctx, config = {}) {
|
|
2000
|
+
// Route sharp version diagnostics (issue #75) through the harness logger
|
|
2001
|
+
// instead of console.warn, so the warning lands in the server log.
|
|
2002
|
+
registerSharpWarningHook((message) => {
|
|
2003
|
+
ctx.logger?.warn(message)
|
|
2004
|
+
})
|
|
1758
2005
|
// Live configuration: composition entry at boot, then the resolved settings
|
|
1759
2006
|
// section once the settings service mounts (installSettingsSection below).
|
|
1760
2007
|
let current = () => config
|
|
@@ -2710,6 +2957,39 @@ export function apply(ctx, config = {}) {
|
|
|
2710
2957
|
}
|
|
2711
2958
|
}
|
|
2712
2959
|
|
|
2960
|
+
// session id -> event-log length already scanned for attachment refs.
|
|
2961
|
+
// Mirrors sessionAttachmentsById: sessions are long-lived objects, and only
|
|
2962
|
+
// the id string survives a process resume, so the index is keyed by id.
|
|
2963
|
+
const scannedSessionEventSeqs = new Map()
|
|
2964
|
+
|
|
2965
|
+
/**
|
|
2966
|
+
* Index image attachments recorded anywhere in the session event log, not
|
|
2967
|
+
* just in the inbox-claim message stream `agent/pre-step` hands us
|
|
2968
|
+
* (issue #72). Host-produced images (the built-in `read_image` tool's
|
|
2969
|
+
* re-uploads, persisted as `tool/result` events) never enter that stream,
|
|
2970
|
+
* so the harness-announced attachment id stayed unresolvable even though
|
|
2971
|
+
* the UI showed the image and the bytes are durably stored. `session.events`
|
|
2972
|
+
* is the complete append-only log (seeded from storage on resume), so
|
|
2973
|
+
* scanning it — incrementally, per session id — finds every ref with full
|
|
2974
|
+
* metadata for a later `attachments.readImage(ref)`.
|
|
2975
|
+
*/
|
|
2976
|
+
const scanSessionEventLog = (session) => {
|
|
2977
|
+
if (!session) return
|
|
2978
|
+
let events
|
|
2979
|
+
try {
|
|
2980
|
+
events = session.events
|
|
2981
|
+
} catch {
|
|
2982
|
+
return // not a host Session (or the getter is unavailable): nothing to scan
|
|
2983
|
+
}
|
|
2984
|
+
if (!Array.isArray(events) || events.length === 0) return
|
|
2985
|
+
const key = session.id !== undefined ? String(session.id) : undefined
|
|
2986
|
+
const last = key !== undefined ? (scannedSessionEventSeqs.get(key) ?? 0) : 0
|
|
2987
|
+
if (last >= events.length) return
|
|
2988
|
+
const refs = collectEventAttachmentRefs(events.slice(last))
|
|
2989
|
+
if (key !== undefined) scannedSessionEventSeqs.set(key, events.length)
|
|
2990
|
+
if (refs.length > 0) recordUploadedAttachments(session, refs)
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2713
2993
|
const lookupAttachment = (session, id) => {
|
|
2714
2994
|
const byId = session && session.id !== undefined
|
|
2715
2995
|
? sessionAttachmentsById.get(String(session.id))
|
|
@@ -2719,7 +2999,163 @@ export function apply(ctx, config = {}) {
|
|
|
2719
2999
|
if (hit !== undefined) return hit
|
|
2720
3000
|
}
|
|
2721
3001
|
const map = session ? sessionAttachments.get(session) : undefined
|
|
2722
|
-
|
|
3002
|
+
const hit = map ? map.get(String(id)) : undefined
|
|
3003
|
+
if (hit !== undefined) return hit
|
|
3004
|
+
// Miss: fall back to the session event log. Ids announced by the harness
|
|
3005
|
+
// for images it persisted itself (read_image re-uploads) live there even
|
|
3006
|
+
// though they never crossed the inbox-claim stream, so this resolves them
|
|
3007
|
+
// exactly like user-uploaded ids (issue #72). Refs from the log carry
|
|
3008
|
+
// full metadata, so a later attachments.readImage(ref) verifies and
|
|
3009
|
+
// returns the bytes.
|
|
3010
|
+
if (session !== undefined) {
|
|
3011
|
+
scanSessionEventLog(session)
|
|
3012
|
+
const afterById = session.id !== undefined
|
|
3013
|
+
? sessionAttachmentsById.get(String(session.id))
|
|
3014
|
+
: undefined
|
|
3015
|
+
if (afterById !== undefined) {
|
|
3016
|
+
const after = afterById.get(String(id))
|
|
3017
|
+
if (after !== undefined) return after
|
|
3018
|
+
}
|
|
3019
|
+
const afterMap = sessionAttachments.get(session)
|
|
3020
|
+
const afterHit = afterMap ? afterMap.get(String(id)) : undefined
|
|
3021
|
+
if (afterHit !== undefined) return afterHit
|
|
3022
|
+
}
|
|
3023
|
+
return undefined
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// ── issue #74: shadow-sanitize tool-result image blocks on the surface ────
|
|
3027
|
+
//
|
|
3028
|
+
// A tool result that renders an image block (vision_present, or the host
|
|
3029
|
+
// read_image on image-capable routes) is persisted as a durable
|
|
3030
|
+
// `tool/result` event and then flows into EVERY later request through
|
|
3031
|
+
// `Session.deriveMessages()`. A text-only adapter (DeepSeek native, pi-ai
|
|
3032
|
+
// text routes) rejects nested images with UNSUPPORTED_CONTENT and the
|
|
3033
|
+
// session is locked forever. The pre-step inbox sanitizer cannot see these
|
|
3034
|
+
// historical events, so the surface itself is rewritten instead: each
|
|
3035
|
+
// offending event is shadowed by a sanitized replacement event
|
|
3036
|
+
// (`surfaceOp: {op:'replace'}`, the same mechanism the host compaction
|
|
3037
|
+
// pruner uses). The Web UI transcript renders append-origin events, so the
|
|
3038
|
+
// user still sees the image; only the model-visible surface is sanitized.
|
|
3039
|
+
// The decision is route-aware: an image-capable route legitimately consumes
|
|
3040
|
+
// read_image's result image, mirroring the host's own gate
|
|
3041
|
+
// (`assertImageCapableRoute` in dsh-tool-fs).
|
|
3042
|
+
|
|
3043
|
+
// session -> { count: nodes examined, done: Set<seqs already decided> }
|
|
3044
|
+
const sessionSurfaceScans = new WeakMap()
|
|
3045
|
+
|
|
3046
|
+
const sessionRouteHandlesImages = async (session) => {
|
|
3047
|
+
let provider
|
|
3048
|
+
let model
|
|
3049
|
+
try {
|
|
3050
|
+
const header = typeof session.requestHeader === 'function' ? session.requestHeader() : undefined
|
|
3051
|
+
provider = header && header.config ? header.config.provider : undefined
|
|
3052
|
+
model = header && header.config ? header.config.model : undefined
|
|
3053
|
+
} catch {
|
|
3054
|
+
return false
|
|
3055
|
+
}
|
|
3056
|
+
if (typeof provider !== 'string' || provider === '' || typeof model !== 'string' || model === '') {
|
|
3057
|
+
return false
|
|
3058
|
+
}
|
|
3059
|
+
// Plugin-owned routes handle image blocks at their stream boundary: the
|
|
3060
|
+
// wrapper and the provider twins rewrite them into markers/descriptions,
|
|
3061
|
+
// the stealth adapter does the same, and the vision-chain route serves
|
|
3062
|
+
// image-capable models.
|
|
3063
|
+
if (provider === wrapperRoute() || provider === chainRoute() || provider.endsWith('-vision')) {
|
|
3064
|
+
return true
|
|
3065
|
+
}
|
|
3066
|
+
if (stealthActive && provider === 'deepseek-official') return true
|
|
3067
|
+
// Routing mode reverse-routes text-only turns back to the text provider;
|
|
3068
|
+
// when neither the wrapper nor the stealth adapter is there to rewrite
|
|
3069
|
+
// images, the reverse target is the bare text adapter, which rejects
|
|
3070
|
+
// tool-result images. Sanitize so text turns stay usable.
|
|
3071
|
+
if (routingEnabled() && reverseRoutingEnabled() && !wrapperRegistered && !stealthActive) {
|
|
3072
|
+
return false
|
|
3073
|
+
}
|
|
3074
|
+
// Same probe the host uses to gate read_image: only routes whose models
|
|
3075
|
+
// declare image input may keep tool-result images.
|
|
3076
|
+
try {
|
|
3077
|
+
const info = await ctx.llm.resolveModelInfo(provider, model)
|
|
3078
|
+
return Array.isArray(info && info.inputModalities) && info.inputModalities.includes('image')
|
|
3079
|
+
} catch {
|
|
3080
|
+
return false // unknown route: fail safe and sanitize
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
const sanitizeSessionToolResults = async (session) => {
|
|
3085
|
+
if (!session) return
|
|
3086
|
+
let events
|
|
3087
|
+
let nodes
|
|
3088
|
+
try {
|
|
3089
|
+
events = session.events
|
|
3090
|
+
nodes = session.surface && session.surface.nodes
|
|
3091
|
+
} catch {
|
|
3092
|
+
return // not a host Session: nothing to sanitize
|
|
3093
|
+
}
|
|
3094
|
+
if (!Array.isArray(events) || !Array.isArray(nodes) || nodes.length === 0) return
|
|
3095
|
+
let scan = sessionSurfaceScans.get(session)
|
|
3096
|
+
if (!scan) {
|
|
3097
|
+
scan = { count: 0, done: new Set() }
|
|
3098
|
+
sessionSurfaceScans.set(session, scan)
|
|
3099
|
+
}
|
|
3100
|
+
// Compaction replaces the surface wholesale; a shrunk node list means the
|
|
3101
|
+
// positional cursor is stale, so restart from the head. Kept decisions are
|
|
3102
|
+
// memoized in `done`, so a restart is a cheap no-op for examined events.
|
|
3103
|
+
if (nodes.length < scan.count) {
|
|
3104
|
+
scan.count = 0
|
|
3105
|
+
scan.done = new Set()
|
|
3106
|
+
}
|
|
3107
|
+
if (nodes.length === scan.count) return
|
|
3108
|
+
let routeHandlesImages
|
|
3109
|
+
const newSeqs = nodes.slice(scan.count)
|
|
3110
|
+
for (const seq of newSeqs) {
|
|
3111
|
+
const event = events[seq]
|
|
3112
|
+
if (!event || event.type !== 'tool/result' || scan.done.has(seq)) continue
|
|
3113
|
+
const message = event.data && event.data.message
|
|
3114
|
+
if (!message || !Array.isArray(message.content) || !blocksHaveImage(message.content)) {
|
|
3115
|
+
scan.done.add(seq)
|
|
3116
|
+
continue
|
|
3117
|
+
}
|
|
3118
|
+
if (routeHandlesImages === undefined) {
|
|
3119
|
+
routeHandlesImages = await sessionRouteHandlesImages(session)
|
|
3120
|
+
}
|
|
3121
|
+
if (routeHandlesImages) {
|
|
3122
|
+
// Image-capable route: keep the image (read_image's result is the
|
|
3123
|
+
// model's view of the file). The wrapper/twin/stealth routes rewrite
|
|
3124
|
+
// it at stream time anyway.
|
|
3125
|
+
scan.done.add(seq)
|
|
3126
|
+
continue
|
|
3127
|
+
}
|
|
3128
|
+
const sanitized = sanitizeToolResultMessage(message)
|
|
3129
|
+
if (sanitized === message) {
|
|
3130
|
+
scan.done.add(seq)
|
|
3131
|
+
continue
|
|
3132
|
+
}
|
|
3133
|
+
try {
|
|
3134
|
+
session.append(
|
|
3135
|
+
'tool/result',
|
|
3136
|
+
{ ...event.data, message: sanitized },
|
|
3137
|
+
{
|
|
3138
|
+
surfaceOp: { op: 'replace', start: seq, end: seq },
|
|
3139
|
+
sourceEventSeqs: [seq],
|
|
3140
|
+
},
|
|
3141
|
+
)
|
|
3142
|
+
scan.done.add(seq)
|
|
3143
|
+
ctx.logger?.info(
|
|
3144
|
+
'vision-router: sanitized a tool-result image block out of the model surface (event seq %s)',
|
|
3145
|
+
seq,
|
|
3146
|
+
)
|
|
3147
|
+
} catch (error) {
|
|
3148
|
+
// A failed shadow leaves the original event on the surface: the
|
|
3149
|
+
// session stays usable (today's behavior) instead of crashing the
|
|
3150
|
+
// pre-step.
|
|
3151
|
+
ctx.logger?.warn(
|
|
3152
|
+
'vision-router: could not sanitize tool-result image at event seq %s (%s)',
|
|
3153
|
+
seq,
|
|
3154
|
+
error && error.message ? error.message : String(error),
|
|
3155
|
+
)
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
scan.count += newSeqs.length
|
|
2723
3159
|
}
|
|
2724
3160
|
|
|
2725
3161
|
// session -> { turn, startIndex, hasImage, routed, failures, lastError }
|
|
@@ -2735,12 +3171,37 @@ export function apply(ctx, config = {}) {
|
|
|
2735
3171
|
// sanitized. This preserves attachment lookup for a later vision_describe.
|
|
2736
3172
|
const rawImageRefs = rewriteImageBlocks(rawMessages)
|
|
2737
3173
|
recordUploadedAttachments(session, rawImageRefs.attachments)
|
|
3174
|
+
// Also index image attachments that live only in the session event log
|
|
3175
|
+
// (read_image re-uploads never cross the inbox-claim message stream).
|
|
3176
|
+
// Incremental: scans only new events (issue #72).
|
|
3177
|
+
scanSessionEventLog(session)
|
|
2738
3178
|
// Hard invariant: tool-produced image blocks never reach a model request.
|
|
2739
|
-
//
|
|
2740
|
-
//
|
|
3179
|
+
// Two layers: (1) sanitize tool-result images in the inbox claim, and
|
|
3180
|
+
// (2) shadow-sanitize historical tool/result events on the session
|
|
3181
|
+
// surface (issue #74) — the only lever that can rewrite durable history.
|
|
2741
3182
|
const sanitizedToolResults = sanitizeToolResultImages(rawMessages)
|
|
2742
3183
|
const messages = sanitizedToolResults.messages
|
|
2743
3184
|
const hasImage = messages.some((message) => blocksHaveImage(message && message.content))
|
|
3185
|
+
try {
|
|
3186
|
+
await sanitizeSessionToolResults(session)
|
|
3187
|
+
} catch (error) {
|
|
3188
|
+
ctx.logger?.warn(
|
|
3189
|
+
'vision-router: session-surface sanitization failed (%s)',
|
|
3190
|
+
error && error.message ? error.message : String(error),
|
|
3191
|
+
)
|
|
3192
|
+
}
|
|
3193
|
+
// Register the turn state BEFORE the image-turn branches below: those
|
|
3194
|
+
// branches return early (auto-mount reminder, history rewrite), and the
|
|
3195
|
+
// agent/request hook must still see the state, otherwise an image turn is
|
|
3196
|
+
// served by the text provider and rejected (issue #74, second root cause).
|
|
3197
|
+
if (routingEnabled()) {
|
|
3198
|
+
const events = session.events ?? []
|
|
3199
|
+
turnState.set(session, {
|
|
3200
|
+
turn: payload.turn,
|
|
3201
|
+
startIndex: events.length,
|
|
3202
|
+
hasImage,
|
|
3203
|
+
})
|
|
3204
|
+
}
|
|
2744
3205
|
if (hasImage) {
|
|
2745
3206
|
// Auto-mount the deep vision tools on image turns: the model can use
|
|
2746
3207
|
// them from its very first step without the user asking for them.
|
|
@@ -2805,14 +3266,6 @@ export function apply(ctx, config = {}) {
|
|
|
2805
3266
|
return { ...decision, messages: cleaned.messages }
|
|
2806
3267
|
}
|
|
2807
3268
|
}
|
|
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
3269
|
return sanitizedToolResults.changed ? { ...decision, messages } : decision
|
|
2817
3270
|
})
|
|
2818
3271
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|