@anionex/dsh-vision-toolkit 0.1.9 → 0.1.11

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.zh.md CHANGED
@@ -1,38 +1,66 @@
1
- ![DSH Vision Toolkit——面向纯文本 DeepSeek Harness Agent 的原生视觉工程能力](assets/hero.png)
1
+ <p align="center">
2
+ <img src="assets/hero.png" alt="DSH Vision Toolkit——面向纯文本 DeepSeek Harness Agent 的原生视觉工程能力" />
3
+ </p>
4
+
5
+ <h1 align="center">DSH Vision Toolkit</h1>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/Anionex/dsh-vision-toolkit/blob/main/README.md">English</a> | 中文
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://dshfind.com/zh/plugins/Anionex/dsh-vision-toolkit"><img src="https://img.shields.io/badge/%E7%94%B1%20dshfind-%E6%8E%A8%E8%8D%90-FFD700?style=flat-square" alt="由 dshfind 推荐" /></a>
13
+ <a href="https://dshfind.com/zh/plugins/Anionex/dsh-vision-toolkit"><img src="https://img.shields.io/badge/dshfind%20%E8%AF%84%E5%88%86-94%20%7C%20%E6%9C%80%E9%AB%98%E5%88%86%E6%8F%92%E4%BB%B6-5B4CF0?style=flat-square" alt="dshfind 评分:94——最高分插件" /></a>
14
+ <a href="https://x.com/anion_ex"><img src="https://img.shields.io/badge/-@anion__ex-000000?style=flat-square&amp;logo=x&amp;logoColor=white" alt="X:@anion_ex" /></a>
15
+ <a href="https://github.com/Anionex/dsh-vision-toolkit/releases/tag/v0.1.11"><img src="https://img.shields.io/badge/release-v0.1.11-5B4CF0?style=flat-square" alt="Release v0.1.11" /></a>
16
+ <a href="tests"><img src="https://img.shields.io/badge/verified-233%20tests-2EA44F?style=flat-square" alt="已验证:233 项测试" /></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-0B7285?style=flat-square" alt="许可证:MIT" /></a>
21
+ <a href="package.json"><img src="https://img.shields.io/badge/Node.js-%5E22.19%20%7C%20%3E%3D24-339933?style=flat-square&amp;logo=nodedotjs&amp;logoColor=white" alt="Node.js ^22.19 或 >=24" /></a>
22
+ <a href="runtime/requirements.lock"><img src="https://img.shields.io/badge/Python-3.11%2B-3776AB?style=flat-square&amp;logo=python&amp;logoColor=white" alt="Python 3.11+" /></a>
23
+ <a href="cordis.patch.yml"><img src="https://img.shields.io/badge/DSH-Web%20%2B%20Headless-5B4CF0?style=flat-square" alt="DSH Web 与 Headless Profile" /></a>
24
+ </p>
2
25
 
3
- # DSH Vision Toolkit
26
+ ## 让 DSH Agent 真正看见
4
27
 
5
- [![由 dshfind 推荐](https://img.shields.io/badge/%E7%94%B1%20dshfind-%E6%8E%A8%E8%8D%90-FFD700?style=flat-square)](https://dshfind.com/zh/plugins/Anionex/dsh-vision-toolkit)
6
- [![dshfind 评分:94——最高分插件](https://img.shields.io/badge/dshfind%20%E8%AF%84%E5%88%86-94%20%7C%20%E6%9C%80%E9%AB%98%E5%88%86%E6%8F%92%E4%BB%B6-5B4CF0?style=flat-square)](https://dshfind.com/zh/plugins/Anionex/dsh-vision-toolkit)
7
- [![X (Twitter)](https://img.shields.io/badge/-@anion__ex-000000?style=flat-square&logo=x&logoColor=white)](https://x.com/anion_ex)
8
- [![Release v0.1.9](https://img.shields.io/badge/release-v0.1.9-5B4CF0?style=flat-square)](https://github.com/Anionex/dsh-vision-toolkit/releases/tag/v0.1.9)
9
- [![Verified: 230 tests](https://img.shields.io/badge/verified-230%20tests-2EA44F?style=flat-square)](tests)
10
- [![License: MIT](https://img.shields.io/badge/license-MIT-0B7285?style=flat-square)](LICENSE)
11
- [![Node.js](https://img.shields.io/badge/Node.js-%5E22.19%20%7C%20%3E%3D24-339933?style=flat-square&logo=nodedotjs&logoColor=white)](package.json)
12
- [![Python](https://img.shields.io/badge/Python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white)](runtime/requirements.lock)
13
- [![DSH profiles](https://img.shields.io/badge/DSH-Web%20%2B%20Headless-5B4CF0?style=flat-square)](cordis.patch.yml)
28
+ 把截图交给纯文本 DeepSeek Harness Agent,它就能看懂画面、读取文字、定位元素、提取素材、还原界面,并用像素差异检查结果是否准确。
14
29
 
15
- **安装:** `dsh plugin --profile web add @anionex/dsh-vision-toolkit`
30
+ DSH Vision Toolkit 将 [`agent-vision-toolkit`](https://github.com/Anionex/agent-vision-toolkit) 打包成原生 DSH 插件。安装后即可使用图片问答、OCR、原图像素坐标、UI 还原、像素对比、可下载产物和 Web 设置,无需自己拼接脚本。
16
31
 
17
- **DSH Vision Toolkit 将 [`agent-vision-toolkit`](https://github.com/Anionex/agent-vision-toolkit) 作为原生 Profile Bundle 带入 DeepSeek Harness。**
32
+ ```sh
33
+ dsh plugin --profile web add @anionex/dsh-vision-toolkit
34
+ ```
18
35
 
19
- 让纯文本 DSH Agent 真正看见,并知道当前任务应该看哪里:通过带意图的图片问答、OCR、原图像素定位、UI 还原、像素验证、托管产物和 Web Settings 完成视觉闭环。10 个独立工具以结构化 schema 和 Agent 级渐进暴露取代 Shell 拼接。
36
+ npm 包已经包含视觉工具快照,并默认使用托管运行时。**普通安装不需要下载源码,也不需要填写 `agentVisionToolkitPath`。**
20
37
 
21
38
  **上游工具箱:** [Anionex/agent-vision-toolkit](https://github.com/Anionex/agent-vision-toolkit) · **项目网站:** [agent-vision.anionex.me](https://agent-vision.anionex.me)
22
39
 
23
- [English](README.md) | 中文
40
+ ## 你可以用它做什么
24
41
 
25
- ## 为什么需要它
42
+ | 目标 | Agent 可以交付什么 |
43
+ |---|---|
44
+ | 看懂截图 | 针对问题作答、描述画面、比较多张图片或执行 OCR |
45
+ | 找到界面元素 | 返回原图像素坐标,并可生成带框预览图 |
46
+ | 按参考图还原页面 | 渲染网页截图、逐区域诊断差异并持续迭代 |
47
+ | 提取可复用素材 | 裁剪图片、透明前景、主色板或可编辑 SVG |
48
+ | 读取长截图 | 可审计分块、Markdown、manifest 和可恢复 OCR 运行目录 |
49
+ | 验证视觉结果 | 差异百分比、主要偏差区域、热力图和 JSON 报告 |
50
+
51
+ 只有需要理解图片内容时才调用远程视觉模型。裁剪、描摹、像素对比、颜色分析、前景提取和 HTML 截图都在本地运行。
26
52
 
27
- `agent-vision-toolkit` 把视觉视为 Agent 可调用的能力,而不是基础模型自带的天赋。它会把“为什么要看这张图”带入视觉请求,从全局逐步收敛到目标区域,并用专用工具验证坐标、颜色、轮廓和差异,不把泛化描述直接当成证据。
53
+ ## 看看实际效果
28
54
 
29
- DSH Vision Toolkit 保留这套方法,并用原生 schema、DSH Credentials、受生命周期管理的运行时准备、可从 Session 日志重建的结构化结果、可预览产物、专用 Web 卡片和 Settings 取代 CLI 安装与 Bash 参数拼接。Agent 加载一个带版本的 Skill,只有当前任务需要视觉时才会获得 10 个工具 schema。
55
+ 第一个示例是 DSH Web 的真实界面视图;接下来两组示例来自本插件所打包的同一条 `agent-vision-toolkit` 代码线;最后一组展示 DeepSeek Harness Web 中的真实使用流程。素材来源见[素材溯源记录](assets/upstream/README.md)。
30
56
 
31
- 本包完整交付已承诺的 P0 与 P1 产品范围。P2 的稳定 `ctx.visionToolkit` 服务会等到独立插件成为真实消费方后再发布;内部运行时不会把未经验证的生态接口伪装为稳定契约。
57
+ ### DSH 视图示例
32
58
 
33
- ## agent-vision-toolkit 已验证的真实用例
59
+ <p align="center">
60
+ <img src="assets/dsh-view-example.png" width="80%" alt="DSH Web 会话视图:纯文本的 DeepSeek-V4-Flash (Vision Toolkit) 模型回答用户粘贴的品牌横幅图内容。" />
61
+ </p>
34
62
 
35
- 前两张图是本 Bundle 所打包 `agent-vision-toolkit` 固定版本同一代码线上的官方实跑结果;图片问答与截图辅助排障这一张则是在 DeepSeek Harness Web 中实际运行的会话,展示 DSH 中的同一套工作流。上游图片来源见[素材溯源记录](assets/upstream/README.md)。
63
+ *真实的 DSH Web 视图:用户粘贴一张品牌横幅截图,纯文本模型通过 `DeepSeek-V4-Flash (Vision Toolkit)` 图片输入变体回答图片内容。*
36
64
 
37
65
  ### 信息图还原:从截图到可编辑 HTML/CSS
38
66
 
@@ -61,47 +89,61 @@ DSH Vision Toolkit 保留这套方法,并用原生 schema、DSH Credentials、
61
89
 
62
90
  *左:DSH Web 中带意图的图片问答;右:DSH Web 中通过截图对比定位 UI 字段差异,并继续向 `vision_pixel_diff` 推进。上游工作流来源仍为 [`agent-vision-toolkit` 官方实跑](https://github.com/Anionex/agent-vision-toolkit/blob/c27d1a300962b553c0884993c575cd3e819465ce/README.md#real-world-effects)。*
63
91
 
64
- DSH Vision Toolkit 在这些上游能力之外增加原生工具 schema、版本化生命周期、Credentials、结构化 Session 结果、产物、Web 展示、Settings 和渐进暴露。下一节展示由本 DSH 仓库实际执行并提交的可复现实证。
92
+ DSH Vision Toolkit 把这套工作流带进 DSH,让结果可以直接成为文件、坐标、可度量的对比报告,或同一会话中的下一步行动。
65
93
 
66
- ## DSH 原生实证:从参考图到像素级一致
94
+ ## 从大致相似到像素级一致
67
95
 
68
- 仓库中的 UI 还原流程会渲染一个故意不准确的 HTML 实现,测得 `6.04%` 像素差异和 6 个非零差异区域;经过迭代后,在 `1200 × 720` 下达到相对参考图精确 `0%` 的差异。
96
+ 仓库内的 UI 还原流程从一个故意不准确的 HTML 实现开始。Vision Toolkit 测得 `6.04%` 差异,指出最需要修正的区域,并推动下一轮迭代。最终渲染结果在 `1200 × 720` 下达到精确 `0%` 差异。
69
97
 
70
98
  <p>
71
99
  <img src="examples/ui-restoration/assets/initial.png" width="49%" alt="Vision Toolkit 迭代前的 UI 还原候选,与参考图仍有可测量的布局和样式差异。" />
72
100
  <img src="examples/ui-restoration/assets/implementation.png" width="49%" alt="仓库内可复现流程生成的最终 UI 还原结果,与参考图达到零像素差异。" />
73
101
  </p>
74
102
 
75
- | 已验证范围 | 证据 |
103
+ | 起点 | 结果 |
76
104
  |---|---|
77
- | 产品范围 | 10 个独立视觉工具、匹配的 `vision-tools` Skill、产物、专用 Web 卡片和实时 Settings |
78
- | 自动化覆盖 | 17 个 Vitest 文件 / 136 项通过测试,以及不依赖 DSH 开发树的可移植包检查 |
79
- | 真实 Profile | 干净临时 Web 与 Headless 安装、激活、禁用、重新启用和卸载 |
80
- | 视觉验收 | 可复现的 HTML 截图 → 像素对比示例,最终差异为 `0%` |
105
+ | 参考图 | 可打开、可编辑的 HTML 实现 |
106
+ | 第一次对比 | 主要问题区域的差异为 `6.04%` |
107
+ | 最终对比 | 在 `1200 × 720` 下达到 `0%` 差异 |
81
108
 
82
- ## 亮点
109
+ ## 它为什么好用
83
110
 
84
- - **看图但不让每轮提示词膨胀:** 初始只暴露 `vision_toolkit_activate`;加载 `vision-tools` 后,10 个独立 schema 才挂到当前 Agent,版本和健康管理始终不进入模型上下文。
85
- - **直接使用坐标,而不是解析自然语言:** 定位和元素盘点返回原图像素框,所有模型可见结果保持为结构化文字或 JSON。
86
- - **交付正式文件,而不是临时输出:** 裁剪、SVG 恢复、OCR、像素对比、前景提取和 HTML 渲染会生成带描述的产物,Web 客户端可预览、下载或在本地打开。
87
- - **受控管理运行时与凭据:** API Key 由 DSH Credentials 保管;managed 模式准备精确隔离的 Python 环境;失败的 Settings 候选不会替换当前服务 generation。
88
- - **闭合视觉验证循环:** 本地 HTML 渲染和像素差异排序支持参考图 → 实现 → 截图 → 度量迭代,不依赖模型原生图片通道。
89
- - **同一 Bundle 同时服务 Web 与 Headless:** Web 增加卡片、预览、Settings 和健康操作;Headless 保持相同工具语义和完整结构化结果。
111
+ - **直接提出你要解决的问题。** “提交按钮在哪里?”和“这张截图为什么和参考图不一样?”都会进入针对性的视觉分析,而不是得到一段泛泛的描述。
112
+ - **拿到可以继续使用的证据。** Agent 会返回坐标、OCR、测量结果、JSON 和文件,方便打开或交给下一步处理。
113
+ - **工作流留在 DSH 里。** Credential、Settings、产物、Web 卡片和 Headless 结果都与会话的其他内容放在一起。
114
+ - **能本地处理就不调用远程模型。** 裁剪、描摹、像素对比、颜色分析、前景提取和 HTML 截图都不消耗视觉 API 请求。
115
+ - **让迭代有终点。** 参考图 → 实现 → 截图 → 像素对比,把 UI 还原变成有数据支撑的闭环。
90
116
 
91
- ## 快速开始
117
+ ## 三步开始使用
92
118
 
93
- 前置条件:DeepSeek Harness `0.1.0-rc.6` 或兼容的后续 `0.1.x` 版本、Python 3.11+,并确保 `dsh plugin` 可以使用 `pnpm`。从 npm 安装已发布的 Bundle,将其加入所需 Profile,并确认 Bundle 行已经挂载:
119
+ 使用 DeepSeek Harness `0.1.0-rc.6` 或兼容的后续 `0.1.x` 版本即可。插件会在第一次使用时准备托管运行时。
94
120
 
95
121
  ```sh
96
122
  dsh plugin --profile web add @anionex/dsh-vision-toolkit
97
123
  dsh plugin --profile headless add @anionex/dsh-vision-toolkit
98
- dsh --profile web --dump-config | grep vision-toolkit
99
- dsh --profile headless --dump-config | grep vision-toolkit
100
124
  ```
101
125
 
102
- 旧 Profile 的 `pnpm-workspace.yaml` 必须使用 `nodeLinker: hoisted` 和 `autoInstallPeers: false`。更新后的 DSH launcher 会在 `dsh plugin` 运行前修复这两个自有设置;使用旧 launcher 时,应在安装前手动设置,避免 pnpm 在 Profile 内组装第二套 Harness 依赖图。
126
+ 1. 重启 Web Profile,打开 **设置 → 视觉工具**。
127
+ 2. 如果要使用远程视觉模型,选择 DSH Credential,然后执行**测试 API 连接**和**测试视觉模型**。
128
+ 3. 在会话中粘贴图片或把图片放进工作区,调用 `/vision-tools`,提出一个明确的视觉任务。
103
129
 
104
- 安装后重启正在运行的 Web Profile,打开 **设置 → 视觉工具**,为远程工具选择 DSH Credential,先执行**测试 API 连接**,再执行**测试视觉模型**以验证一次真实图片请求。在会话中把图片放进工作区路径,调用 `/vision-tools`,再让 Agent 使用明确的 `vision_*` 工具。本地裁剪、SVG、像素、颜色、前景和 HTML 操作不需要视觉 API Credential。
130
+ 如果使用较旧的 DSH launcher,安装前可能需要在 Profile 中设置 `nodeLinker: hoisted` 和 `autoInstallPeers: false`。当前 launcher 会自动修复这些设置。
131
+
132
+ 安装后重启正在运行的 Web Profile,打开 **设置 → 视觉工具**,先执行**测试 API 连接**,再执行**测试视觉模型**。新安装会自动使用内置免费 Gemma 4 提供方,不需要 API Key 或 DSH Credential。若要使用其他提供方,请修改端点、模型或协议,并配置对应的 DSH Credential。
133
+
134
+ ## 加入交流群
135
+
136
+ 欢迎加入 `agent-vision-toolkit` 项目交流群,交流使用经验、反馈问题并提出建议。
137
+
138
+ <p align="center">
139
+ <img src="assets/community-group-qr.png" alt="agent-vision-toolkit 项目交流群二维码" width="260">
140
+ </p>
141
+ 本地裁剪、SVG、像素、颜色、前景和 HTML 操作不需要视觉 API Credential。
142
+
143
+ > **不需要本地路径。** 普通 npm 安装保持默认的 `runtime.mode: managed` 即可。`runtime.agentVisionToolkitPath` 只面向明确需要外部固定 checkout 的开发者或受控部署。
144
+
145
+ <details>
146
+ <summary><strong>技术架构</strong></summary>
105
147
 
106
148
  ## 工作原理
107
149
 
@@ -124,6 +166,8 @@ flowchart LR
124
166
 
125
167
  所有工具定义都调用同一个 Runtime;Runtime 在分发到固定上游快照或已配置的视觉提供方端点前,统一验证路径、限制、Credential、取消和超时。Web 展示读取相同的结构化结果与产物描述,因此不会改变 Headless 语义。健康、连接测试和版本检查只留在 Settings,不进入模型工具 schema。
126
168
 
169
+ </details>
170
+
127
171
  ## 工具
128
172
 
129
173
  | 工具 | 执行方式 | 结构化结果 | 产物交付 |
@@ -141,6 +185,9 @@ flowchart LR
141
185
 
142
186
  插件不重新实现视觉算法。DSH 侧只负责验证路径与限制、解析 Credential、用 argv 向量调用固定上游脚本、解析精确输出契约、分类失败、描述文件,并把结果投影给模型和 Web 客户端。
143
187
 
188
+ <details>
189
+ <summary><strong>高级模型行为</strong></summary>
190
+
144
191
  ## 渐进式模型暴露
145
192
 
146
193
  运行时就绪状态属于整个 Profile,但 10 个视觉执行工具的 schema 属于具体 Agent。Agent 加载 `vision-tools` 前,插件只贡献很小的 `vision_toolkit_activate` 引导工具;该 Agent 的请求 schema 中没有视觉执行工具。标准 `skill` 工具以 `name="vision-tools"` 成功加载后,会为下一模型步骤自动挂载全部 10 个工具并隐藏引导工具。直接调用 `/vision-tools` 会注入 skill 指令;如果此时视觉工具仍不可见,这些指令要求调用一次 `vision_toolkit_activate`。激活只影响当前 Agent;Session 中存在与打包 skill 版本匹配的持久证据时可以恢复,并持续到 Agent 或插件被释放。
@@ -149,18 +196,20 @@ flowchart LR
149
196
 
150
197
  ## 纯文本模型的图片输入变体
151
198
 
152
- 纯文本模型路由会获得同名的兄弟模型条目:`<模型名> (Vision Toolkit)`,挂在对应的提供方分组下。变体声明支持图片输入,因此粘贴的图片走原生附件流程——输入框缩略图、会话持久化图片与历史渲染全部保留——插件只在发往模型的请求链路上把每个图片块改写成 Vision Toolkit 描述文本,再转交上游路由。会话日志不被改动;回放与 UI 看到的始终是真实图片。
199
+ 纯文本模型路由仍会获得同名的兄弟模型条目:`<模型名> (Vision Toolkit)`,挂在对应的提供方分组下。但 DSH 原生图片块不会把粘贴附件的本地路径传给模型,因此默认粘贴流程会先把每张图片复制进会话工作区,再把绝对路径插入模型可见的消息。DSH 模型随后可以把这个路径传给 `vision_glance` 或其他视觉工具,并使用与 `agent-vision-toolkit` 对齐的 focus hint 和 `[vision model description]` 通道标记。会话日志保存可复用的路径引用,UI 保留粘贴记录。
153
200
 
154
201
  插件会自动为宿主明确声明为纯文本的每个模型注册变体(例如 DeepSeek 对话家族)。粘贴处理是全自动的:当当前模型被确认为纯文本、且它的变体已注册时,浏览器端集成会自动把会话切换到变体(会有一条简短提示说明新模型名),随后粘贴走原生流程,无需手动切换模型。宿主依据浏览器从实时模型目录读到的精确模型路由来裁决,模型选择器标签作为兜底;无法确认或支持图片的路由一律保持原生流程,而"纯文本但没有变体"的模型(例如变体被关闭时)继续走"粘贴转路径":图片被复制进会话工作区,输入框里插入的是它的路径文本。
155
202
 
156
- 描述转换需要已配置的视觉提供方及其 Credential;当运行时未就绪或读取失败时,请求链路上的图片块降级为说明文本,而不是让整轮失败。用 `imageInputVariants.enabled: false` 关闭变体,用 `imageInputVariants.providers` 限制被包装的路由,或用 `imageInputVariants.autoSwitch: false` 让纯文本模型继续走"粘贴转路径"。
203
+ 启用图片输入变体并将 `imageInputVariants.autoSwitch` 设为 `true` 时,描述转换需要已配置的视觉提供方及其 Credential;当运行时未就绪或读取失败时,请求链路上的图片块降级为与上游兼容的 `[vision unavailable: ...]` 提示,而不是让整轮失败。默认路径流程不需要先调用这层自动描述桥接,模型可直接使用收到的图片路径调用视觉工具。桥接不会把注入的上下文文件当作当前用户意图;如果图片来自工具调用,则使用最新的助手段落作为关注提示。用 `imageInputVariants.enabled: false` 关闭变体,用 `imageInputVariants.providers` 限制被包装的路由,或用 `imageInputVariants.autoSwitch: true` 显式启用原生附件自动切换。
204
+
205
+ </details>
157
206
 
158
207
  ## 运行要求
159
208
 
160
209
  - 启用 Web 或 Headless Profile 的 DeepSeek Harness,并确保 `dsh plugin` 可以使用 `pnpm`。
161
210
  - Python 3.11 或更高版本。Managed 模式会创建隔离环境,用户无需手工安装上游 CLI(命令行界面)或 Python 包。
162
211
  - 首次启用 managed 运行时需要联网;如果配置的软件包缓存已有 `runtime/requirements.lock` 中的精确版本,则无需联网。
163
- - `vision_glance`、`vision_ground`、`vision_detect` 和非仅切分长截图 OCR 需要 OpenAI 兼容或 Anthropic 视觉端点及 DSH Credential。本地工具无需该 Credential 也可使用。
212
+ - 内置免费 Gemma 4 提供方可直接用于 `vision_glance`、`vision_ground`、`vision_detect` 和非仅切分长截图 OCR。只有改用自定义 OpenAI 兼容或 Anthropic 端点时才需要 DSH Credential;本地工具不依赖任何远程提供方。
164
213
  - 只有 `vision_html_screenshot` 需要 Chrome、Chromium 或 Edge;未安装受支持浏览器时,其他工具保持可用。
165
214
  - 输入必须是会话工作区或显式 `allowedDirs` 根目录内的 PNG、JPEG、GIF 或 WebP。
166
215
 
@@ -201,7 +250,7 @@ dsh plugin --profile web remove @dsh-external/dsh-vision-toolkit
201
250
  dsh plugin --profile web add @anionex/dsh-vision-toolkit
202
251
  ```
203
252
 
204
- 重启后,Settings → 视觉工具 应显示插件版本 **0.1.9**。
253
+ 重启后,Settings → 视觉工具 应显示插件版本 **0.1.11**。内置免费提供方会自动选中;自定义提供方仍使用配置的 DSH Credential。
205
254
 
206
255
  通过注册表安装时,使用 Profile 的包管理命令更新依赖:
207
256
 
@@ -229,16 +278,16 @@ Bundle 默认使用 managed 运行时。Profile patch 可以覆盖提供方与
229
278
  - id: vision-toolkit
230
279
  config:
231
280
  provider:
232
- baseUrl: https://api.inferera.com/v1
233
- credential: VISION_API_KEY
234
- model: gemini-3.6-flash
281
+ baseUrl: https://vision.anionex.me/v1
282
+ credential: ANIONEX_FREE_VISION
283
+ model: gemma-4-26b-a4b-it
235
284
  protocol: openai
236
285
  anthropicThinking: omit
237
286
  userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36
238
287
  language: zh
239
288
  timeoutMs: 60000
240
- maxImageBytes: 10485760
241
- maxImagePixels: 40000000
289
+ maxImageBytes: 4194304
290
+ maxImagePixels: 20000000
242
291
  concurrency: 4
243
292
  runtime:
244
293
  mode: managed
@@ -246,23 +295,23 @@ Bundle 默认使用 managed 运行时。Profile patch 可以覆盖提供方与
246
295
  imageInputVariants:
247
296
  enabled: true
248
297
  providers: []
249
- autoSwitch: true
298
+ autoSwitch: false
250
299
  ```
251
300
 
252
301
  ### 配置字段
253
302
 
254
303
  | 字段 | 默认值 | 契约 |
255
304
  |---|---|---|
256
- | `provider.baseUrl` | `https://api.inferera.com/v1` | 提供方 API 基础 URL;去除结尾斜杠后使用。Anthropic 应填写以 `/v1` 结尾的基础 URL,不要填写完整 `/messages` URL |
257
- | `provider.credential` | `VISION_API_KEY` | DSH Credential 引用,不是密钥值 |
258
- | `provider.model` | `gemini-3.6-flash` | 远程工具使用的多模态模型名 |
305
+ | `provider.baseUrl` | `https://vision.anionex.me/v1` | 内置免费 OpenAI 兼容端点;自定义提供方可改用其他基础 URL,使用时会去除结尾斜杠 |
306
+ | `provider.credential` | `ANIONEX_FREE_VISION` | 免费服务的只读内置引用;自定义提供方使用 DSH Credential 引用,而不是密钥值 |
307
+ | `provider.model` | `gemma-4-26b-a4b-it` | 远程工具使用的多模态模型名 |
259
308
  | `provider.protocol` | `openai` | `openai` 发送 Chat Completions 请求;`anthropic` 发送原生 Messages 请求 |
260
309
  | `provider.anthropicThinking` | `omit` | Anthropic thinking 字段。`omit` 不发送 thinking 字段,兼容性最好;仅当所选模型明确支持时使用 `disabled` 或 `adaptive`,提供方返回 HTTP 400 时应先恢复 `omit`。 |
261
310
  | `provider.userAgent` | 浏览器兼容默认值 | 视觉请求和显式连接测试发送的 User-Agent;可为提供方或代理兼容性覆盖 |
262
311
  | `language` | `zh` | 视觉输出语言:`zh` 或 `en` |
263
312
  | `timeoutMs` | `60000` | 完整操作截止时间,1000-600000 毫秒;每个工具可请求更窄的覆盖值 |
264
- | `maxImageBytes` | `10485760` | 每张输入图片的编码字节上限 |
265
- | `maxImagePixels` | `40000000` | 每张输入图片的解码像素上限 |
313
+ | `maxImageBytes` | `4194304` | 每张输入图片的编码字节上限;内置免费服务最多接受 4 MiB |
314
+ | `maxImagePixels` | `20000000` | 每张输入图片的解码像素上限;内置免费服务最多接受 20,000,000 像素 |
266
315
  | `concurrency` | `4` | 每个会话内的并发操作数,1-16 |
267
316
  | `runtime.mode` | `managed` | `managed` 使用打包快照;`external` 只接受精确固定版本 |
268
317
  | `runtime.agentVisionToolkitPath` | 未设置 | `external` 模式必填;必须是精确导出快照或固定 commit 的干净 Git checkout |
@@ -270,19 +319,34 @@ Bundle 默认使用 managed 运行时。Profile patch 可以覆盖提供方与
270
319
  | `allowedDirs` | `[]` | 额外的 realpath 解析输入根目录;会话工作区始终允许 |
271
320
  | `imageInputVariants.enabled` | `true` | 为纯文本模型路由在模型选择器中注册图片输入变体条目 |
272
321
  | `imageInputVariants.providers` | `[]` | 按提供方 id 限制被包装的上游路由;为空时包装所有符合条件的路由 |
273
- | `imageInputVariants.autoSwitch` | `true` | 粘贴时自动把纯文本会话切换到其图片输入变体,让图片保持原生流程;`false` 时纯文本模型继续走"粘贴转路径" |
322
+ | `imageInputVariants.autoSwitch` | `false` | 是否在粘贴时自动把纯文本会话切换到图片输入变体;默认关闭,纯文本模型继续走 DSH 兼容的"粘贴转路径"流程 |
274
323
 
275
324
  ### Credential
276
325
 
277
- Web 设置页的只写 **API 密钥** 输入框直接接收真实密钥。留空表示保留现有密钥;填写后保存,会把密钥写入高级设置中的 **凭据名称**,默认名称是 `VISION_API_KEY`。Headless 部署可以在 `$DSH_HOME/.credentials.yaml` 中预置同名引用。
326
+ 内置免费提供方使用固定的 `ANIONEX_FREE_VISION` 引用,不接受也不会保存用户 API Key。修改端点、模型或协议切换到自定义提供方后,只写的 **API 密钥** 输入框会自动解锁;填写后保存,会把密钥写入高级设置中的 **凭据名称** 引用。Headless 部署可以在 `$DSH_HOME/.credentials.yaml` 中预置该自定义引用。
278
327
 
279
328
  Settings 只保存引用,不保存值。浏览器不会读取已保存的密钥,保存成功后输入框也会立即清空而不是回显。每次远程操作都会重新解析引用,并只把值注入对应子进程环境。插件排除用户 `.env`、checkout `.env`、`PYTHONPATH`、`PYTHONHOME`、`VIRTUAL_ENV` 和用户 site-packages,避免环境中的 Python 或上游配置覆盖选定的 DSH 提供方。日志、错误、工具结果、产物元数据和 Settings 响应都不包含密钥。
280
329
 
281
- ### Managed 与 external 运行时
330
+ ### 内置免费服务限制
331
+
332
+ 公开服务是共享的零配置默认入口,不是无限量私有端点。限制由代理执行,并以 OpenAI 风格错误返回明确的原因代码和可读提示;限流响应还会携带 `Retry-After` 与请求额度响应头。
333
+
334
+ | 限制 | 当前值 |
335
+ |---|---:|
336
+ | 单客户端 | 每个 UTC 日 100 次 |
337
+ | 全局服务 | 每个 UTC 日 400 次 |
338
+ | 突发 | 60 秒内 20 次 |
339
+ | 图片字节 | 每张最多 4 MiB |
340
+ | 解码像素 | 每张最多 20,000,000 像素 |
341
+ | 输出 | 最多 512 tokens |
342
+
343
+ ### Managed 运行时与可选 external 运行时
282
344
 
283
345
  Managed 模式会验证 `vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json`,优先使用 `uv`,回退到 `venv` 加 pip,按 `runtime/requirements.lock` 安装精确版本,通过 heartbeat 锁协调并发准备,并只在全部探针通过后发布 staging 环境。
284
346
 
285
- External 模式用于开发或受控部署:
347
+ 大多数用户只需要 managed 模式。它已经包含在 npm 包中,会自动为你准备固定版本的 Python 环境。
348
+
349
+ 可选的 external 模式面向插件开发或受控部署,适用于已经维护好精确上游 checkout 的场景:
286
350
 
287
351
  ```yaml
288
352
  - id: vision-toolkit
@@ -299,6 +363,8 @@ External 模式用于开发或受控部署:
299
363
 
300
364
  Web Profile 会注册 Vision Toolkit Settings 分区,可配置提供方 URL、Credential 引用、模型、OpenAI/Anthropic 协议、Anthropic thinking 模式、User-Agent、语言、超时、字节/像素限制、并发数、运行时模式、Python 覆盖值、external 源码路径和允许目录。该页面还会显示插件/上游版本、当前运行时 generation、不含密钥的 Credential configured/source/writable 状态、运行时路径、健康检查结果和产物路由可用性。
301
365
 
366
+ **插件更新**卡片会通过当前 Profile 配置的 npm registry 检查 `@anionex/dsh-vision-toolkit` 新版本。点击**自动更新并重启**后,插件只安装用户刚刚确认的准确版本,校验安装结果,启动独立重启辅助进程,再优雅重启 DSH Web;当前页面会等待新进程恢复,并在新插件版本开始服务后自动刷新。该操作要求同源请求,只能更新这个固定包,并且会串行执行;`link:`、`file:`、workspace、git、URL、传递依赖、无法唯一识别、只读或缺少 `pnpm` 的安装方式不会开放自动更新,避免覆盖本地开发源码。重启可能中断正在运行的任务,因此界面会要求再次确认。
367
+
302
368
  “保存并应用”会验证完整配置,准备候选 Python/上游运行时,提交 Settings revision,最后才原子切换 generation。候选被拒绝时,之前的 generation 继续服务,页面也会把这种状态与运行时确实不可用区分开来。“重新加载”始终恢复后端已保存的权威值,即使 revision 没有变化也会丢弃被拒绝的浏览器草稿。初始启动无法准备运行时时,Settings 路由仍可用于提交有效配置并激活首个 generation。陈旧浏览器 revision 不会覆盖较新的保存结果,而是返回冲突;刷新后再重试。只读 Settings 提供方允许查看和健康检查,但禁用保存。
303
369
 
304
370
  “运行健康检查”只执行本地检查。“测试 API 连接”是显式操作,会把已配置 Credential 发送到 `GET /models`;OpenAI 使用 Bearer 认证,Anthropic 使用 `x-api-key` 与 `anthropic-version`。这个轻量测试不会上传图片,也不会创建 completion。“测试视觉模型”会另行把插件自带的 `assets/vision-model-test.png` 通过与 `vision_glance` 相同的多模态运行路径发送出去;它会创建一次真实 completion,并用于权威确认所选端点、Credential、模型、协议和上游账户确实能够处理图片。视觉模型检查卡会单独显示“已实测”“未测试”或“测试失败”Tag,避免把 `/models` 返回 HTTP 200 误认为图片调用成功。插件加载和普通 Settings 读取不会发送这两类请求。
@@ -390,7 +456,14 @@ pnpm pack --dry-run
390
456
 
391
457
  ## 项目状态与范围
392
458
 
393
- 版本 `0.1.9` 是当前公开 npm 发布。P0 和 P1 是本包的产品承诺。P2 是设计门槛:至少一个独立插件消费内部能力形态前,不发布稳定 `ctx.visionToolkit` 服务、能力发现 API 或提供方生态。Web 上传、拖拽、摄像头/视频/音频/文档输入、交互式标注框编辑、GUI 自动点击、远程服务集群、模型路由、模型投票和跨会话视觉缓存不属于当前产品范围。
459
+ 版本 `0.1.11` 是当前公开 npm 版本。产品重点是截图理解、视觉定位、OCR、素材提取、UI 还原和 DSH Web/Headless Profile 中的像素级验证。Web 上传、拖拽、摄像头/视频/音频/文档输入、交互式标注框编辑、GUI 自动点击、远程服务集群、模型路由、模型投票和跨会话视觉缓存不属于当前产品范围。
460
+
461
+ <details>
462
+ <summary><strong>维护者范围说明</strong></summary>
463
+
464
+ 稳定的 `ctx.visionToolkit` 服务和能力发现 API 会等到独立插件成为真实消费方后再发布。这样可以让公开集成面建立在已验证的使用场景上,而不是未经验证的生态契约上。
465
+
466
+ </details>
394
467
 
395
468
  ## 社区与关于
396
469
 
@@ -409,11 +482,3 @@ pnpm pack --dry-run
409
482
  ## 许可证
410
483
 
411
484
  插件采用 MIT 许可。打包的 `agent-vision-toolkit` 快照在 `vendor/agent-vision-toolkit/LICENSE` 保留上游 MIT 许可证,并继续作为视觉算法的唯一实现。
412
-
413
- ## 加入交流群
414
-
415
- 欢迎加入 `agent-vision-toolkit` 项目交流群,交流使用经验、反馈问题并提出建议。请扫描下方二维码入群。
416
-
417
- <p align="center">
418
- <img src="assets/community-group-qr.png" alt="agent-vision-toolkit 项目交流群二维码" width="320">
419
- </p>
Binary file
package/lib/client.js CHANGED
@@ -18,6 +18,10 @@ const NS = 'vision-toolkit';
18
18
  const SETTINGS_ROUTE = '/_dsh/vision-toolkit/settings';
19
19
  const PRESENTATION_META_KEY = '$dshVisionToolkit';
20
20
  const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
21
+ // Keep these browser defaults aligned with src/defaults.ts without importing server-side config.
22
+ const BUILT_IN_FREE_VISION_BASE_URL = 'https://vision.anionex.me/v1';
23
+ const BUILT_IN_FREE_VISION_CREDENTIAL = 'ANIONEX_FREE_VISION';
24
+ const BUILT_IN_FREE_VISION_MODEL = 'gemma-4-26b-a4b-it';
21
25
  const en = {
22
26
  nav: 'Vision',
23
27
  settingsTitle: 'Vision Toolkit',
@@ -34,7 +38,7 @@ const en = {
34
38
  apiKeyBlank: 'The API key cannot contain only spaces.',
35
39
  apiKeyInvalid: 'Paste only the key, without a variable name, quotes, spaces, or line breaks.',
36
40
  credential: 'Credential name',
37
- credentialHint: 'Advanced: the API key is stored under this name. Keep VISION_API_KEY unless another plugin configuration requires a different reference.',
41
+ credentialHint: 'The built-in free provider needs no user key. For a custom provider, this is the DSH credential reference used to store its key.',
38
42
  model: 'Model',
39
43
  protocol: 'API protocol',
40
44
  anthropicThinking: 'Anthropic thinking',
@@ -181,7 +185,7 @@ const zh = {
181
185
  apiKeyBlank: 'API 密钥不能只包含空格。',
182
186
  apiKeyInvalid: '请只粘贴密钥本身,不要包含变量名、引号、空格或换行。',
183
187
  credential: '凭据名称',
184
- credentialHint: '高级用法:API 密钥会按此名称保存。除非其他插件配置要求不同名称,否则保持 VISION_API_KEY。',
188
+ credentialHint: '内置免费视觉服务无需用户密钥;切换到自定义服务时,此处是保存其密钥的 DSH 凭据名称。',
185
189
  model: '模型名称',
186
190
  protocol: 'API 协议',
187
191
  anthropicThinking: 'Anthropic thinking',
@@ -641,16 +645,16 @@ class VisionSettingsController {
641
645
  exports.VisionSettingsController = VisionSettingsController;
642
646
  function draftOf(value) {
643
647
  return {
644
- baseUrl: value.provider?.baseUrl ?? 'https://api.inferera.com/v1',
645
- credential: value.provider?.credential ?? 'VISION_API_KEY',
646
- model: value.provider?.model ?? 'gemini-3.6-flash',
648
+ baseUrl: value.provider?.baseUrl ?? BUILT_IN_FREE_VISION_BASE_URL,
649
+ credential: value.provider?.credential ?? BUILT_IN_FREE_VISION_CREDENTIAL,
650
+ model: value.provider?.model ?? BUILT_IN_FREE_VISION_MODEL,
647
651
  protocol: value.provider?.protocol ?? 'openai',
648
652
  anthropicThinking: value.provider?.anthropicThinking ?? 'omit',
649
653
  userAgent: value.provider?.userAgent ?? DEFAULT_USER_AGENT,
650
654
  language: value.language ?? 'zh',
651
655
  timeoutMs: String(value.timeoutMs ?? 60000),
652
- maxImageBytes: String(value.maxImageBytes ?? 10485760),
653
- maxImagePixels: String(value.maxImagePixels ?? 40000000),
656
+ maxImageBytes: String(value.maxImageBytes ?? 4194304),
657
+ maxImagePixels: String(value.maxImagePixels ?? 20000000),
654
658
  concurrency: String(value.concurrency ?? 4),
655
659
  runtimeMode: value.runtime?.mode ?? 'managed',
656
660
  toolkitPath: value.runtime?.agentVisionToolkitPath ?? '',
@@ -699,6 +703,12 @@ function valueOf(draft, t) {
699
703
  allowedDirs: draft.allowedDirs.split(/\r?\n/).map(entry => entry.trim()).filter(Boolean),
700
704
  };
701
705
  }
706
+ function isBuiltInFreeVisionDraft(draft) {
707
+ return draft.baseUrl.trim().replace(/\/+$/, '') === BUILT_IN_FREE_VISION_BASE_URL
708
+ && draft.credential.trim() === BUILT_IN_FREE_VISION_CREDENTIAL
709
+ && draft.model.trim() === BUILT_IN_FREE_VISION_MODEL
710
+ && draft.protocol === 'openai';
711
+ }
702
712
  function Field({ label, children, hint }) {
703
713
  return (0, jsx_runtime_1.jsxs)("label", { className: "dvt-field", children: [(0, jsx_runtime_1.jsx)("span", { children: label }), children, hint === undefined ? null : (0, jsx_runtime_1.jsx)("small", { children: hint })] });
704
714
  }
@@ -840,7 +850,11 @@ function LoadedSettings({ controller, t }) {
840
850
  };
841
851
  const busy = state.action !== undefined;
842
852
  const credentialMatchesSnapshot = draft.credential.trim() === snapshot.credential.ref;
843
- const keyLocked = credentialMatchesSnapshot && !snapshot.credential.writable;
853
+ const builtInCredentialChangedProvider = snapshot.credential.source === 'built-in-free'
854
+ && !isBuiltInFreeVisionDraft(draft);
855
+ const keyLocked = credentialMatchesSnapshot
856
+ && !snapshot.credential.writable
857
+ && !builtInCredentialChangedProvider;
844
858
  const canSave = snapshot.writable || (apiKey.length > 0 && !keyLocked);
845
859
  const runtimeErrorTitle = snapshot.runtime.ready ? t('runtimeCandidateRejected') : t('runtimeUnavailable');
846
860
  return ((0, jsx_runtime_1.jsxs)("div", { className: "dvt-settings", children: [(0, jsx_runtime_1.jsx)("div", { className: "dvt-alert notice", children: t('externalNotice') }), !snapshot.writable ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert warning", children: t('readOnly') }) : null, draftError === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: draftError }), state.error === undefined ? null : (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert error", children: state.error }), state.message === 'saved' ? (0, jsx_runtime_1.jsx)("div", { className: "dvt-alert success", children: t('saved') }) : null, snapshot.runtime.lastError === undefined ? null : (0, jsx_runtime_1.jsxs)("div", { className: "dvt-alert error", children: [(0, jsx_runtime_1.jsx)("strong", { children: runtimeErrorTitle }), (0, jsx_runtime_1.jsx)("span", { children: snapshot.runtime.lastError })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel dvt-essential", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('provider') }), (0, jsx_runtime_1.jsx)("p", { children: t('providerHint') })] }), (0, jsx_runtime_1.jsx)("span", { className: `dvt-badge ${snapshot.credential.configured ? 'ok' : 'error'}`, children: snapshot.credential.configured ? t('configured') : t('missing') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-form-grid", children: [(0, jsx_runtime_1.jsx)(Field, { label: t('protocol'), children: (0, jsx_runtime_1.jsxs)("select", { disabled: !snapshot.writable || busy, value: draft.protocol, onChange: (event) => { update('protocol', event.target.value); }, children: [(0, jsx_runtime_1.jsx)("option", { value: "openai", children: "OpenAI Chat Completions" }), (0, jsx_runtime_1.jsx)("option", { value: "anthropic", children: "Anthropic Messages" })] }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('baseUrl'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { disabled: !snapshot.writable || busy, value: draft.baseUrl, onChange: (event) => { update('baseUrl', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('model'), children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { disabled: !snapshot.writable || busy, value: draft.model, onChange: (event) => { update('model', event.target.value); } }) }), (0, jsx_runtime_1.jsx)(Field, { label: t('apiKey'), hint: keyLocked ? t('apiKeyLocked') : snapshot.credential.source === undefined ? t('apiKeyHint') : `${t('apiKeyHint')} ${t('sourceHint', { source: t('source'), value: credentialSource(snapshot.credential.source, t) })}`, children: (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Input, { "aria-label": t('apiKey'), type: "password", autoComplete: "new-password", disabled: busy || keyLocked, placeholder: snapshot.credential.configured ? t('apiKeyPlaceholderConfigured') : t('apiKeyPlaceholderMissing'), value: apiKey, onChange: (event) => { setApiKey(event.target.value); setDraftError(undefined); } }) })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-save-row", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "primary", disabled: !canSave || busy, onClick: save, children: state.action === 'save' ? t('saving') : t('save') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { variant: "outline", disabled: busy, onClick: () => { void controller.load(); }, children: t('reload') })] }), (0, jsx_runtime_1.jsxs)("section", { className: "dvt-panel", children: [(0, jsx_runtime_1.jsxs)("div", { className: "dvt-panel-title", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h3", { children: t('health') }), (0, jsx_runtime_1.jsx)("p", { children: t('connectionHint') })] }), (0, jsx_runtime_1.jsxs)("div", { className: "dvt-actions", children: [(0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('health'); }, children: state.action === 'health' ? t('testing') : t('runHealth') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "outline", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('connection'); }, children: state.action === 'connection' ? t('testing') : t('testConnection') }), (0, jsx_runtime_1.jsx)(dsh_client_ui_primitives_1.Button, { size: "sm", variant: "primary", disabled: busy || !snapshot.runtime.ready, onClick: () => { void controller.runHealth('model'); }, children: state.action === 'model' ? t('testingModel') : t('testModel') })] })] }), (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('saveBeforeTesting') }), state.health === undefined ? (0, jsx_runtime_1.jsx)("p", { className: "dvt-muted", children: t('notTested') }) : (0, jsx_runtime_1.jsx)("div", { className: "dvt-health-grid", children: Object.entries(state.health.checks).map(([name, check]) => {