dsh-plugin-read-image-preview 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +129 -33
  3. package/client.js +2594 -718
  4. package/index.js +584 -119
  5. package/package.json +11 -2
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dsh-plugin contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-plugin contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,57 +1,153 @@
1
1
  # dsh-plugin-read-image-preview
2
2
 
3
- 把**任何工具产出的图片直接画在会话里**——`read_image` 读的文件图,和 computer-use 截图这类**结果附件图**,共用同一张卡片与同一套交互。
3
+ 会话里的图片(read_image 读到的图、computer-use 截图、你自己贴进来的图)共用一个全屏查看器:放大、平移、圈红框、加字、裁剪,然后**回车直接放进输入框**。输入框旁的 `✂ 截图` 抓屏后直接框选,选完回车同样进输入框——给模型看一个前端问题,不用再开外部截图器。
4
4
 
5
- ## 通用模式(v1.4)
5
+ ## 接在哪里
6
6
 
7
- 卡片按数据源分两轨,注册时一次认领所有图像工具(`tool.call.toolview` 的对应键,priority `-1`):
7
+ | 位置 | 做法 |
8
+ | --- | --- |
9
+ | read_image 行、用户消息里的图、助手消息里的图、轨迹里的图 | 只替换官方的**图库**位置(`tool.call.images`、`conversation.chat.user.images` / `conversation.chat.message.images`,0.2.0 叫 `conversation.message.images`;`conversation.trajectory.images`),priority `-1`。官方行自己的标题、路径、尺寸、Inspect 原样保留 |
10
+ | computer-use 各工具、generate_image | 这些工具没有官方图片行,继续整格认领 `tool.call.toolview`(priority `-1`),渲染同一个图库加一行信息(路径、尺寸、体积、格式、复制路径、在编辑器中打开) |
11
+ | 输入框右侧 | `✂ 截图` 按钮 |
12
+ | 设置 → 截图与标注 | 截图目录管理与按期清理 |
8
13
 
9
- | 轨道 | 触发条件 | 字节来源 | 元数据 |
10
- | --- | --- | --- | --- |
11
- | `file` | 调用参数里有 `file_path`(read 家族) | Host 半区回环路由 `/read-image-preview/resolve`(调用一开始就能出骨架/预览) | resolve 响应 |
12
- | `attachment` | 结果 content 里有 `{type:'image', attachment}`(computer-use 截图及未来任何图像工具) | 会话授权的 `props.loadImage`(先 `peek` 缓存) | **全在附件引用上,零额外请求** |
14
+ > **v1.6 起 read_image 不再整行接管。** 官方 read_image 行从 0.2.0 起把图片交给 `tool.call.images` 子 slot 渲染,并自带路径与尺寸信息;本插件只填这个图库,和官方重合的部分全部交还。代价是官方行默认折叠,点开才看到图。两版 dsh 的消息图片 slot 名不同,全部注册是安全的:`slots.inject` 会一直等到 slot 被声明,没有的名字永远不会激活。
13
15
 
14
- 没有图也没有报错的调用渲染一行结果摘要(失败标红),执行中渲染按真实宽高比撑开的骨架——认领的行永远不会是空壳。
16
+ 图库:单张按真实宽高比显示缩略图(`min(420px, 100%)` × `max-height: 300px`),`<img>` 在字节到达前就带好 `width/height`,晚到的图片不会引起布局跳变(v1.4.1 修过的聊天吸底问题);多张一律平铺成小图,全部可点开,查看器里 `←`/`→` 切换;透明 PNG/WebP/GIF 自动铺棋盘格;加载失败点一下重试。
15
17
 
16
- **v1.4.1:图片加载零位移。** 两种模式下图片像素尺寸都在字节到达之前就已知(resolve 响应 / 附件引用),因此 `<img>` 直接带 `width/height` 渲染、信息行宽度也按已知比例预先定死。晚到的图片(上一张截图的 fetch/解码在新内容压上来之后才完成)落地时不再产生布局跳变——正是这个跳变会让 DSH 聊天流的吸底逻辑把浏览器自发的位移误读成「用户上滚」,从而停止自动跟随最新记录。
18
+ ## 查看器(v1.6 重写)
17
19
 
18
- **dsh 0.1.7-rc.1 起工具调用分三阶段。** 官方把 `RunningToolCall` 拆成 `preparing`(参数还在流式生成,**没有 `argsRaw`**)与 `start`,并且参数开始流动时就把视图渲染出来(`tool.call.toolview` 的 owner 多了 `phase`)。因此判定「执行中」不能再只看 `argsRaw` 是否存在,否则我们认领的行在整个 preparing 阶段会是空壳——`isRunningBlock()` 现在把 `phase: 'preparing' | 'start'` 都算执行中,形状判断继续兼容更早的版本。`tests/phases.test.mjs` 用官方三种形状锁住这条约束(设 `READ_PREVIEW_CLIENT` 指向未修复的 `client.js` 可以验证它确实会失败)。
20
+ ### 一个模式,工具常驻
19
21
 
20
- **接入新工具 = 在 `client.js` 的 `TOOL_LABELS` 加一行**,交互(缩略图、点击放大、Ctrl+滚轮、拖动、双击适应、骨架、透明棋盘格)全部自动继承。
22
+ 不再区分「查看」和「标注」:底栏一直摆着工具,默认是**移动**。
21
23
 
22
- ## 它解决什么
24
+ | 组 | 内容 |
25
+ | --- | --- |
26
+ | 工具 | 移动 `V` · 矩形 `R` · 椭圆 `E` · 箭头 `A` · 画笔 `P` · 文字 `T` · 裁剪 `C` |
27
+ | 样式 | 6 色(红打头)· 细 / 中 / 粗(同时决定文字字号) |
28
+ | 编辑 | 撤销 `Ctrl+Z` · 重做 `Ctrl+Shift+Z`/`Ctrl+Y` · 删除选中 `Delete` · 清空 |
29
+ | 缩放 | − / 百分比 / + · 适应 `F` · 1:1 `0` |
30
+ | 输出 | **发到输入框** `Enter` · 复制 `Ctrl+C` · 保存 `Ctrl+S` · 关闭 `Esc` |
23
31
 
24
- DSH 自带的 `read_image` 卡片(`dsh-client-ui-tool` 的 `read-image-toolview`)有三个让人看不到图的地方:
32
+ - **已画的标记可以改**:移动工具下点线条选中,拖动移位,拉角改大小(箭头拉两端),点颜色 / 粗细直接改选中的那个,`Delete` 删掉;文字双击改字。刚画完的框和箭头自动选中,可以马上调。
33
+ - 框和椭圆只在**线上**能抓住,框内照样拖动平移,圈起来的区域不会挡住手势。
34
+ - 裁剪是一块状态区域:外面压暗,四角可拖,框内可拖动整体移位;导出时只出这一块。
35
+ - 标记坐标一律是**源图像素**,放大多少倍导出都与所见一致;放大到 200% 以上切成像素级渲染,方便看 UI 截图的细节。
25
36
 
26
- 1. 图片是 `ToolRow` 展开区里的内容,**默认折叠**,不点开那一行就没有画面;
27
- 2. 官方派生逻辑明确写着运行期的调用没有 content,所以 **AI 正在看图的那几秒卡片里是空的**;
28
- 3. 折叠状态只有一行路径摘要。
37
+ ### 三个出口
29
38
 
30
- 这个插件接管这些工具的展示位(`tool.call.toolview` 的对应键,priority `-1`,同时遮蔽内置行与动态插件的兜底卡片),把图片提到最外层:
39
+ - **发到输入框**:把当前图片(含标注,按裁剪区域)以附件形式放进输入框。走的是官方输入框的**粘贴**通路:往编辑器根节点派发一个带 PNG 文件的 `paste` 事件,官方的粘贴命令收下文件并取消事件——这和你按 Ctrl+V、拖文件进来是同一个入口,不依赖任何私有接口。找输入框的顺序:离发起位置最近的编辑器 → 最近一次获得焦点的编辑器 → 页面上最后一个可见的编辑器;找不到就改为复制到剪贴板并提示。
40
+ - **复制**:PNG 进剪贴板。
41
+ - **保存**:存进截图目录,提示条里给出路径和「复制路径」。
31
42
 
32
- - **调用一开始就出图**,不等 tool result;
33
- - 加载中是按**真实宽高比**撑开的骨架(扫光动画,`prefers-reduced-motion` 下停用);
34
- - 图下一行信息:状态点(蓝闪=正在看 / 绿=看完 / 红=失败)、路径、像素尺寸、体积、格式;
35
- - 默认是克制的缩略图(`min(420px, 100%)` × `max-height: 300px`),不占聊天流;
36
- - **点图进入缩放视图**:`Ctrl + 滚轮` 连续缩放(0.2×–8×,锚定光标位置;滚轮只在按住 Ctrl 时被消费,所以不与页面滚动打架)、**按住拖动平移**、双击复位、`Esc` 或点背景关闭;底部有当前缩放百分比与 `−` / `+` / `100%`;
37
- - 透明容器(PNG/WebP/GIF)自动铺棋盘格;
38
- - 操作:复制路径、在编辑器中打开。
43
+ 有没复制、保存或发出的标注时,关闭、点暗处、切换图片都会先停一下:「继续编辑 / 复制后离开 / 放弃(Esc)」,连按两次 Esc 即放弃。
39
44
 
40
- ## 两半的分工
45
+ ### 手势
46
+
47
+ | 手势 | 行为 |
48
+ | --- | --- |
49
+ | 滚轮 | 以光标为锚缩放,每格约 1.2×,带缓动(0.05×–16×) |
50
+ | Shift + 滚轮 | 上下平移 |
51
+ | Ctrl + 滚轮 | 左右平移 |
52
+ | 触控板捏合 | 缩放(与 Ctrl+滚轮区分:捏合的 delta 很小或带小数,鼠标一格是整的 ~100) |
53
+ | 触控板双指滑动 | 双向平移(带横向 delta 却没按 Shift,只有触控板会这样) |
54
+ | 拖动空白处 | 平移(移动工具下) |
55
+ | 空格 / 中键按住拖动 | 任何工具下都能平移 |
56
+ | 双击 | 适应窗口 |
57
+ | 点图外暗处 / Esc / `✕ 关闭` / `关闭 (Esc)` | 关闭 |
58
+
59
+ 点暗处只在「静止的点击、落在图外、移动工具、没有裁剪、没有未导出的标注、不是刚抓的屏」时才关闭,正在做的活儿不会被随手一点丢掉。
60
+
61
+ ### 性能
62
+
63
+ 拖动卡顿的三个来源都去掉了:
64
+
65
+ 1. 平移、缩放不再走 React:视图放在 ref 里,每帧由一个 `requestAnimationFrame` 写图片的 `transform`、缩放读数和标注层;React 只在工具、标记这类离散变化时重渲染。
66
+ 2. 标注层按**舞台(视口)**大小分配位图,而不是「原图 × 缩放倍数」——3200×2000 的截图放大到 300% 时,旧做法是一张约 9600×6000 的画布,每次移动都要重新光栅化。
67
+ 3. 去掉全屏 `backdrop-filter: blur`,它会让每一帧平移都重新模糊整张背景。
68
+
69
+ 实测(Playwright 驱动真实 dsh web,alpha.4):120 次指针移动的平移 0 帧超过 50ms。
70
+
71
+ ### 窗口按钮的避让(沿用 v1.5)
72
+
73
+ 桌面端的三个窗口按钮是操作系统画在网页之上的(Electron WCO),任何 `z-index` 都盖不住。所以查看器顶部留出宿主公布的净空(`--dsh-frame-overlay-top`,Windows 60 / macOS 68 / 全屏 20),右上角不放任何本插件的控件,`✕ 关闭` 放在顶部居中;根节点 `-webkit-app-region: no-drag`。
74
+
75
+ 查看器渲染在 `document.body` 的 portal 里,但 React 事件仍会沿声明它的组件树冒泡(工具行、消息、输入框)。根节点把指针、点击、键盘事件全部截停,避免例如工具行头部被误点折叠、连带把查看器卸载掉。键盘监听挂在捕获阶段,Esc 和工具字母不会漏到底下的会话里(Esc 在那里可能会中断正在运行的回合)。
76
+
77
+ ## 截图
78
+
79
+ 左键 `✂ 截图` 立刻抓全屏,**右键选延时**(立即 / 1 / 3 / 5 秒,选择会记住)——延时是为了那些「一点输入框就消失」的画面。抓到的是一张冻结的位图,查看器直接进入框选:拖出区域 → 区域旁浮出「发到输入框 ⏎ · 复制 · 保存 · 取消框选」→ 回车。要标注就先切工具再画,最后回车。
80
+
81
+ - Host 半区用 PowerShell 抓屏:先 `Application::SetHighDpiMode(PerMonitorV2)` 再 `CopyFromScreen`,拿到真实像素(本机 3200×2000,约 0.6 秒)而不是 DPI 虚拟化后的糊图;优先 `pwsh`,失败回落 `WindowsPowerShell 5.1`;macOS 用 `screencapture`,Linux 依次试 `grim` / `gnome-screenshot` / `spectacle` / `scrot` / `import`;
82
+ - 抓屏结果以 `image/png` 直接回给页面,不做 base64 往返;
83
+ - **没有全局热键**:页面拿不到全局快捷键,抢本地组合键又容易和宿主、输入框打架。
84
+
85
+ ### 截图管理与定期清理
86
+
87
+ 设置 → **截图与标注**:缩略图网格(懒加载)、复制路径、删除、打开目录、立即清理、加载更多,点缩略图进查看器(可左右切换)。
88
+
89
+ 落盘位置默认 `~/.dsh/screenshots/`。清理策略:
41
90
 
42
- Slot 视图没有文件权限,所以字节要走页面自己的传输:
91
+ - **按时间 + 按数量 + 按体积**三重上限,谁先到听谁的,从最旧的开始删;
92
+ - 只在启动后 8 秒与按 `cleanupIntervalMinutes`(默认 360 分钟)的心跳上运行,两个定时器都 `unref()`;
93
+ - **只删本插件按 `shot-YYYYMMDD-HHMMSSmmm-xx.png` 命名的文件**——把目录指到存了别的东西的文件夹,也不会被策略清掉。
94
+
95
+ ## 设置项
96
+
97
+ 设置页里本插件自己那一节(字段都会即时生效):
98
+
99
+ | 设置项 | 默认 | 说明 |
100
+ | --- | --- | --- |
101
+ | `screenshotDir` | 空 = `~/.dsh/screenshots` | 截图落盘目录(支持 `~` 与相对路径,相对路径挂在 `~/.dsh` 下) |
102
+ | `retentionDays` | `7` | 保留天数,`0` = 不按时间清 |
103
+ | `retentionMaxFiles` | `300` | 目录里最多留多少个 |
104
+ | `retentionMaxMb` | `500` | 目录最多占多少 MB |
105
+ | `cleanupIntervalMinutes` | `360` | 定期清理间隔(最小 5) |
106
+ | `captureDelaySeconds` | `0` | 抓屏默认延时,页面里的延时菜单可以临时覆盖 |
107
+
108
+ ## 两半的分工
43
109
 
44
110
  | 半边 | 文件 | 职责 |
45
111
  | --- | --- | --- |
46
- | Host | `index.js` | 用组合的 `fs` 服务解析路径、按魔数校验容器、解析像素尺寸,签发能力 token,并在插件私有前缀上提供两个回环路由 |
47
- | Client | `client.js` | 接管 `read_image` 展示位并渲染卡片;通过 `GET /read-image-preview/resolve` 拿描述与 URL |
112
+ | Host | `index.js` | 截图目录的能力 token;抓屏、落盘、列表、删除、按期清理;服务下列回环路由(不读取任何工具传入的路径) |
113
+ | Client | `client.js` | 图库(官方图片位置)、工具卡片、全屏查看器与标注、抓屏入口、截图管理页 |
114
+
115
+ 工具结果与消息里的图片都由会话授权的 `loadImage`(先 `peek` 缓存)取字节,像素尺寸、体积、格式、降采样前的原图尺寸都在附件引用上,零额外请求。
116
+
117
+ 路由(全部在插件私有前缀上,每条都先过 `connection.requestRejection`,取不到该服务时一律 503):
118
+
119
+ - `GET /read-image-preview/<token>` → 截图字节,供管理页缩略图与查看器使用(token 只由 list / save 签发)
120
+ - `GET /read-image-preview/state` → 目录、平台、保留策略、抓屏可用性
121
+ - `GET /read-image-preview/list?limit=&offset=` → 分页的截图清单
122
+ - `POST /read-image-preview/save`(请求体是 PNG 字节)→ 落盘结果与路径
123
+ - `POST /read-image-preview/delete?name=…` · `POST /read-image-preview/cleanup` · `POST /read-image-preview/open`
124
+ - `POST /read-image-preview/capture?delay=` → `image/png` 字节
125
+
126
+ 安全边界:只放行 PNG / JPEG / WebP / GIF 四种位图;URL 里带的是不透明 token 而非路径,token 只指向截图目录里的文件;`save` 只写 PNG 魔数、只写截图目录、文件名由宿主生成;`delete` 只接收单段文件名。**不向 Cordis 服务命名空间发布任何东西**。
127
+
128
+ ## 测试
129
+
130
+ ```sh
131
+ cd dsh-plugin-read-image-preview
132
+ npm test # node --test tests/*.test.mjs
133
+ ```
134
+
135
+ - `tests/phases.test.mjs`:工具调用三阶段形状;认领范围(不含 read_image);多图结果全部交给图库。
136
+ - `tests/gestures.test.mjs`:
137
+ - 滚轮三档映射与触控板识别、以光标为锚的缩放与适应;
138
+ - 标记绘制、命中、移动、改尺寸,裁剪角拖动与夹紧;
139
+ - 关闭方式与右上角几何约束、portal 事件截停;
140
+ - 性能约束:无全屏模糊、标注层按视口分配;
141
+ - 发到输入框(粘贴事件、找输入框的顺序)、图库渲染、slot 注册。
142
+ - `tests/shots.test.mjs`:保留策略、抓屏脚本(先提 DPI 精度再取屏幕)、逐平台命令序列、存/查/删/清路由与鉴权围栏。
143
+
144
+ 单测用的是记录元素树的假 React,覆盖不到真实浏览器里的行为。v1.6 另用 Playwright 驱动真实的 dsh web(0.2.1-alpha.4)跑了一遍:抓屏、框选、画框、平移、滚轮、回车。结果是图片进入输入框附件栏,焦点留在输入框,导出图按源图像素裁剪且带标注。
48
145
 
49
- 路由:
146
+ **没在真实页面验证的**:消息与 read_image 行里的图库,本机没有带图片的历史会话。
50
147
 
51
- - `GET /read-image-preview/resolve?path=…&cwd=…` → JSON 描述 + 能力 URL
52
- - `GET /read-image-preview/<token>` → 图片字节,供 `<img src>` 使用
148
+ ## 已知取向
53
149
 
54
- 安全边界:只放行 PNG / JPEG / WebP / GIF 四种位图,且 `Content-Type` 用的是**嗅探结果而非扩展名**,所以 SVG / HTML 永远不会被协商成脚本;URL 里带的是不透明 token 而非路径,页面无法让宿主读取任意路径。两条路由都是插件私有前缀上的普通 HTTP,**不向 Cordis 服务命名空间发布任何东西**,不会和别的插件撞名。
150
+ 文案目前是硬编码中文(和这个仓库里的同类插件一致),没有接 `ctx.locale`;要出英文得先把 i18n 一次性补齐,而不是在这里掺一半。
55
151
 
56
152
  ## 安装
57
153
 
@@ -59,6 +155,6 @@ Slot 视图没有文件权限,所以字节要走页面自己的传输:
59
155
  dsh plugin --profile web add <本包路径或 tgz>
60
156
  ```
61
157
 
62
- 包内的 `cordis.patch.yml` 通过 `dsh.bundle.patch` 自 wiring(安装即注册),不需要手改 profile 的 `cordis.patch.yml`。装完**重启 profile**,然后随便让模型 `read_image` 一张图即可看到。
158
+ 包内的 `cordis.patch.yml` 通过 `dsh.bundle.patch` 自 wiring(安装即注册),不需要手改 profile 的 `cordis.patch.yml`。装完**重启 profile**。
63
159
 
64
160
  卸载:`dsh plugin --profile web remove dsh-plugin-read-image-preview`。