# dsh-image-annotate [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的 Web 插件:**输入框里待发的图片,点一下就能画标注,标注结果作为新附件插回原图旁边**,原图保留。 [English](README.md) | **中文** 给代理发截图之前总要先在图上圈个重点。可输入框里的待发图片原来只能看不能改,只能先去别的软件里画好、再重新拖进来。这个插件把这一步搬进了输入框。 **最小改动**:DSH 自带 UI 一律不改——附件栏、拖放遮罩、文件卡片、原图灯箱都还是官方那套,插件只接管「点缩略图」这一个动作。只有一个浏览器端文件 + 一个空的 Host 半边;不加 Host 路由、不引入依赖、不写配置文件,你不点图片时它什么都不做。 ![标注弹窗](assets/screenshot-1-editor.png) ## 功能 - **点缩略图**打开的不是普通原图灯箱,而是标注弹窗:既能看大图,也能直接在上面画。 - **四种工具**:画笔(自由涂)、方框、箭头、文字。 - **文字写在可以移动、缩放的框里**:选文字工具后在图上**拖一个框**(只点一下会给一个默认大小的框),框内直接打字。文字按框宽自动折行、字号由算法解出来,所以长段落是变小而不是溢出图片——**永远不会被截断**。落好之后:**拖框体移动**、**拖八个手柄缩放**(文字实时重排,且框不会小到装不下文字的最小可读字号);**双击框内改文字**、选中后 **Delete 删除**,这些操作都能 Ctrl/Cmd+Z 撤销。框可选**形状**(矩形/圆角/椭圆)、跟随当前颜色、并有**底纹**(透明/淡色/实心);选透明就只留边框和文字。Shift+回车换行。 - **文字框与「方框」工具互不影响**:方框工具仍然只是画一个边框;文字框只在文字工具下可选中、可编辑,所以"圈重点"和"写标注"两件事不会互相干扰。 - **6 色**(红黄绿蓝白黑)× **3 档粗细**(细/中/粗,按图片短边比例存),所以在手机截图和 4K 截图上观感一致。 - **撤销**(按钮或 Ctrl/Cmd+Z)、**清空**、Esc 或点遮罩关闭。 - **作为新附件插回**:结果以 `<原名>-<后缀>.png` 追加到附件栏,紧挨着原图。标注图本身就是普通附件,可以再点开继续叠加。 - **设置页**(设置 → 图片标注),默认值都在那里;见下。 - 亮色/深色主题都适配,并且不改动 DSH 自带 UI 的任何样式。 ![结果:标注图追加在原图旁边](assets/screenshot-2-result.png) ## 安装 需要 DSH `0.1.5-rc.1` 或兼容版本(`dsh web`,profile `web`),该版本上验证通过。 ### 从 GitHub 装(推荐) ```sh dsh plugin --profile web add github:janewas/dsh-image-annotate ``` 这条命令会把本包加进 profile 的 bundle 栈,插件自带的 `cordis.patch.yml` 随之把它挂上。bundle 列表是**启动时**读取的,所以装完要**重启 `dsh web`**,再刷新页面。 ### 从 Release 压缩包装(不需要 pnpm,也不用重启) 到 [Releases](https://github.com/janewas/dsh-image-annotate/releases/latest) 下载 `dsh-image-annotate.tgz`: ```sh tar -xzf dsh-image-annotate.tgz cd package bash install.sh # 装到别的 profile 就加参数,例如:bash install.sh headless ``` `install.sh` 是幂等的:把目录软链到 `~/.dsh/profiles/web/node_modules/`,并往该 profile 的 `cordis.patch.yml` 追加挂载行(**先备份**原文件、写临时文件后原子替换)。profile 补丁是 live 生效的,所以**刷新页面即可**,不用重启。解压出来的目录别删也别移动——profile 是软链到它的。 两种方式最终挂的是同一个插件;`install.sh` 写的那行带守卫,一旦检测到本包已经作为 bundle 挂上就会自动让位,所以两边都做也不会挂载两份。 ## 用法 1. 照常往输入框里加图(拖入或点回形针)。 2. **点一下缩略图**,标注弹窗打开,图片按整幅显示。 3. 画:选工具、选颜色、选粗细;文字工具下点位置直接打字。 4. 点「插入到输入框」→ 标注图出现在附件栏末尾,原图仍在。 | 工具 | 操作 | |---|---| | 画笔 | 按住拖动画线 | | 方框 | 从一个角拖到另一个角 | | 箭头 | 从尾拖到头 | | 文字 | 拖一个框 → 框内打字;回车确认(Esc 取消,点别处也算确认,Shift+回车换行) | | 移动 / 缩放文字框 | 文字工具下:拖框体移动;拖八个手柄缩放。文字实时重排,缩到"最小可读字号都放不下"时框就不再变小 | | 改字 / 删除 | 双击框内改文字(清空文字即删掉这个框);选中后按 Delete。移动、缩放、改字、删除都能 Ctrl/Cmd+Z 撤销 | 撤销/清空在工具条上(Ctrl/Cmd+Z 也能撤销)。正在框内打字时这两个键同样可用:**撤销**=放弃这段还没落下的文字,**清空**=放弃这段文字并清掉已画的标注;只有确实没东西可撤/可清时才置灰。Esc、右上角「关闭」或点遮罩关闭弹窗。 ## 设置 设置 → 图片标注(排在官方分页后面)。 ![设置页](assets/screenshot-3-settings.png) | 设置项 | 默认 | 作用 | |---|---|---| | 启用点击编辑 | 开 | 总开关。关掉后点缩略图恢复官方「查看原图」灯箱,插件完全不插手 | | 默认工具 | 画笔 | 弹窗打开时先选中的工具 | | 默认颜色 | 红 | 画笔、方框、箭头、文字的初始颜色 | | 默认线宽 | 中 | 画笔、方框、箭头的线条粗细。文字大小不在这里设——由你画的框决定 | | 文字框形状 | 圆角 | 新文字框的默认形状 | | 文字框底纹 | 淡色 | 透明 = 只留边框和文字;淡色/实心 = 框里填一层底色,字更跳 | | 插入方式 | 追加(保留原图) | 「替换原图」= 标注图插进去后自动摘掉原来那张 | | 标注图文件名 | 标注 | `<原名>-<后缀>.png`;留空则只改扩展名 | 「恢复默认」一键还原。设置存在**浏览器本地**(`localStorage`,键 `dsh-image-annotate.settings.v1`):只对该浏览器 + 该访问地址有效,换个地址(例如从 `127.0.0.1` 换成局域网/Tailscale 地址)就是默认值。插件不写任何 Host 配置。 ![深色主题](assets/screenshot-4-dark.png) ## 实现要点 两个半边,都在 `lib/`: - `lib/index.js` —— Host 半边,故意留空。它的作用是让 loader 有一条入口,从而让 `dsh-client-modules` 扫到本包的 `dsh.client` 声明,把 `lib/client.js` 放进浏览器插件图。 - `lib/client.js` —— 浏览器半边,其余全部在这里。 要改代码的话,这几处是关键: - **挂载点**:`conversation.input.overlay`(会话级 list 槽位)。它的标准 props 提供 `sessionId` 和 `inputActions`;组件平时渲染 `null`,只装一个捕获阶段的 click 监听。编辑器用 `createPortal` 挂到 `document.body`——composer 卡片会为 `position: fixed` 建立包含块,不 portal 会被困在卡片里。 - **命中判定**:`click` 的目标是 `[data-composer-card]` 里的 ``,且它的 `src` 等于某张草稿附件的 `previewUrl`。草稿缩略图用的就是它自己的 object URL,所以匹配精确,绝不会误伤聊天记录里的图片。命中后 `stopPropagation()`,只压掉这一次的官方灯箱。 - **插回草稿**:`conversation.createDrafts(sessionId, [file])` 注册文件,再 `inputActions.addAttachments(ids)` 追加——和回形针按钮走同一条准入路径;被拒(正在提交)就回滚并在弹窗里报错。 - **画布**:底图 canvas 按原图尺寸只画一次,覆盖层 canvas 装标注、每次全量重绘。标注坐标存图片像素,线宽/字号存短边比例;导出时把两层合成到一张 canvas 再 `toBlob('image/png')`。显示尺寸只靠 CSS,所以导出的位图始终是原图分辨率。 - **文字**:文字工具下拖出一个框,输入框盖在框上、样式贴近成品。折行与字号由**预览和导出共用**的同一个排版器算:`fitTextInBox` 先按词元折行(CJK 逐字断行,拉丁单词整体保留,单个词元超宽才硬切),再**二分查找**能完整放进框里的最大字号;连最小字号都放不下时把框往下长高,而不是截断文字。回车/失焦时把 `{type:'textbox', x, y, w, h, text, color, shape, fill}` 压进标注栈;渲染时先填底纹、再描边框,文字居中——实心底纹用反差色,其余情况用标注色 + 反差描边。 - **设置**:注册进 `settings.section`,用 `localStorage` + `useSyncExternalStore` 做一个小 store,弹窗每次打开时读一次。 - **零依赖**:浏览器半边只 require 平台种子模块(`react` / `react-dom`),Host 半边不 import 任何东西,所以 `dsh.client.inject` 为空,没有加载顺序约束。 ## 已知边界 - 只管输入框里的待发图片;聊天记录里的图片仍然打开官方灯箱。 - 导出统一是 PNG:GIF 会丢动画,超大图(几千万像素级)比较吃内存。 - 框太小而文字很长时,字会一直变小;连最小可读字号都放不下时,框会往下长高而不是截断文字。 - 没有裁剪和旋转。 - 一次只能编辑一张。 ## 卸载 从 `~/.dsh/profiles/web/cordis.patch.yml` 删掉挂载行(bundle 方式是 `id: image-annotate`,`install.sh` 方式是 `id: image-annotate-manual`), `install.sh` 建的 `node_modules/dsh-image-annotate` 软链一并删掉,刷新页面。 用 `dsh plugin` 装的则执行 `dsh plugin --profile web remove dsh-image-annotate` 后重启。 ## 许可证 MIT