点开图片 → 圈出问题 → 写一行说明 → 拖到合适位置 → 加入输入框 → 接着打字发送
🎯 它解决的是什么
「这个按钮往左挪 8px。」—— 对面问:哪个按钮?
「小屏下这里会挤在一起。」—— 只好再截一张图,再画个箭头。
把截图丢给 AI 说「这里有问题」—— 它开始认真猜「这里」到底是哪。
问题从来不是表达,而是 「这里」需要用手指出来的。 这个插件把这件事压缩成两秒:点开图片 → 圈出来 → 打字 → 加入输入框 → 发送。
左边是原图;右边是点「加入输入框」后 模型真正收到的那张 PNG(原分辨率,圈和字都在上面)
✨ 亮点一览
| 🖊 七种工具 | 移动 · 画笔 · 箭头 · 方框 · 圆圈 · 文字 · 橡皮擦;圆圈和方框按整条轨迹的外接框成形,手绘一圈也成 |
| 🖐 画完还能改 | 每一笔都是活对象:选中、拖动、拖角缩放、双击改字、删除、方向键微调,全部可撤销 |
| 🔤 文字是对象 | 双击直接改内容(不会多出一层);文字工具下也能直接拖走已确定的文字框 |
| 🎨 原生外观 | 工具条、按钮、提示、选中框全部用 DSH 设计 token 拼,浅色深色自动跟随 |
| 📨 一键进输入框 | 合成原分辨率的 PNG,走的是 composer 自己的粘贴通道;失败还有拖放 / 剪贴板 / 下载三级兜底 |
| 🪶 零依赖 · 无构建 | 不 import 任何 @deepseek-ai/*,不需要 dsh-loader,也没有打包产物 —— npm 包里就 4 个文件 |
| 🖼 支持多种来源 | 消息里的截图、输入框里的草稿图、工具返回的图片,都是同一个预览,同一个画笔 |
🚀 30 秒上手
- 在会话里点开任意图片;
- 预览右下角出现圆形 标注 按钮(和右上角关闭按钮同款),点它;
- 用 圆圈 / 方框 / 箭头 圈出问题,或用 文字 在图上写一句;
- 位置不满意?直接拖(见下一节);
- 点 加入输入框 —— 标注图进到输入框,预览自动关闭,你接着打字发送。
| 快捷键 | 作用 |
|---|---|
V |
选择 / 移动 |
1 – 7 |
移动 · 画笔 · 箭头 · 方框 · 圆圈 · 文字 · 擦除 |
E |
橡皮擦(点中哪笔擦哪笔) |
Ctrl/⌘ + Z · Ctrl/⌘ + Shift + Z |
撤销 · 重做 |
Delete |
删除选中 |
方向键(Shift 加速) |
微调 1px / 10px |
| 文字工具 | 图上点一下 → 打字 → 回车(Esc 取消) |
🖐 画完还能改,这才是重点
截图工具画上去的线是烧死的像素,这里不是 —— 每一笔都可以再编辑:
| 想做什么 | 怎么做 |
|---|---|
| 选中 | 切到「移动」(V) 点任意一笔;刚画完的那笔会自动选中 |
| 移动 | 直接拖;方向键微调 1px,Shift + 方向键 10px |
| 缩放 | 拖选中框四角的手柄等比缩放(文字会跟着改字号) |
| 改字 | 双击任意文字直接改(改完还是同一笔) |
| 删除 | Delete,或工具条上的垃圾桶(有选中时它就变成"删除选中") |
| 后悔 | 上面每一步都在撤销栈里,Ctrl+Z 一路退回 |
圈和文字都换过位置了 —— 虚线选中框与四角手柄用的是 DSH 原生主色
🎨 它看起来就像 DSH 自己长出来的
整支工具栏由 DSH 的设计 token 拼成,不引入第二套视觉语言:
| 部位 | 用到的原生 token |
|---|---|
| 工具条容器 | --dsw-menu-surface-fill + --dsw-menu-backdrop-filter + --dsw-elevation-panel + --dsw-radius-lg |
| 工具按钮 | --dsw-alias-button-tool-bar-fill / -hover · --dsw-alias-interactive-bg-hover |
| 主按钮 | --dsw-alias-button-primary-fill · --dsw-alias-label-primary-foreground |
| 右下角入口 | 与预览关闭按钮同款 36px 圆形(--dsw-specific-input-major + 0.5px 细边) |
| 提示条 | --dsw-alias-toast-bg / -label + --dsw-shadow-lv3(顶部居中,同原生 Toast) |
| 选中态 | --dsw-alias-state-business-primary |
| 色板 | 原生 static 色:红 · 琥珀 · 黄 · 绿 · 蓝 · 品牌蓝 deepseek-450 · 黑 · 白 |
🛠 给开发者
三个「不」,让它在 DSH 升级时不容易坏| 做法 | 好处 | |
|---|---|---|
| 不改宿主 | 不注册 slot、不动 React 树,只在预览 DOM 上叠一层自己的 canvas + 工具条,预览一关整层消失 | 不和官方 UI 抢状态,也不怕重渲染 |
| 不依赖内部包 | 不 import 任何 @deepseek-ai/*、不注入 dsh 服务、不需要 dsh-loader |
官方改内部 API / 改包名都影响不到它 |
| 不需要构建 | 浏览器半区就是一个手写的 window.__ModuleLoader__.load 工厂(lib/client.js),没有 tsdown / rollup 产物 |
改完即生效(client-modules 按 mtime 做 HMR),包里只有 4 个文件 |
- 预览识别:只认一个稳定特征 —— body 之下、
div[role="dialog"][aria-modal="true"]且直接子元素里有<img>的节点(即dsh-client-ui-primitives的ImageLightbox)。用节流后的MutationObserver扫描:预览出现即挂载,移除即销毁。 - 坐标系统:所有笔迹以原图像素存储。叠加显示时按显示尺寸缩放,导出时按原图尺寸绘制 —— 1909×1231 的截图在 1427×920 的预览里怎么画、怎么拖,导出的 PNG 就是 1909×1231 的同一位置。
- 编辑模型:笔迹是普通对象(
{tool, color, points[], x, y, fontSize, textWidth…})。选中 / 移动 / 缩放 / 改字都是对对象求值后整帧重绘;拖动时每帧从「按下瞬间的快照 + 位移」重算而不是累加,避免漂移。 - 投递到输入框:把「原图 + 笔迹」合成为 PNG(
File),再向 Lexical 输入框([data-composer-input])派发一个带该文件的合成paste事件 —— 也就是用户自己粘贴截图走的那条路。失败依次退回:document 级drop→ 剪贴板(提示Ctrl+V)→ 下载 PNG。 - 导出不怕污染:先用
fetch(src)把blob:原图取回再解码(createImageBitmap),避开跨源drawImage导致的toBlobSecurityError。
验证到什么程度一个有意思的坑:
position: fixed+left: 50%的浮层,宽度按「left 右侧剩余空间」收缩,工具条会莫名换行 —— 补width: max-content才对(原生 Toast 的 CSS 注释里也记着同一个坑)。
| 测试 | 覆盖 | 结果 |
|---|---|---|
test/test_annotate.py |
画布贴合、笔迹像素、撤销、圆圈成形、导出尺寸、paste 投递、自动关闭 | 17 / 17 |
test/test_edit.py |
选中、拖动、角手柄缩放、文字拖动与双击改字、方向键微调、删除与撤销 | 19 / 19 |
test/e2e_dsh.py |
真实 dsh 实例(隔离 profile + 独立端口):打开预览 → 圈选 → 拖动 → 加字 → 投放 → 断言附件为原分辨率 1909×1231 | 21 / 21 |
前两套跑在自带 fixture(test/lightbox-fixture.html,复刻 DSH 预览的真实 DOM 形状)上;第三套需要一个隔离实例,跑法见脚本头部注释与 PUBLISHING.md。CI(.github/workflows/ci.yml)每次 push 都跑语法检查 + 两套 fixture。
__dshImageAnnotate.annotators[0].state // tool / strokes / selected / natural / rect …
localStorage.setItem('@dsh-plugin/dsh-image-annotate.debug', '1') // 打开详细日志
📦 安装 / 卸载
# 从 GitHub 直接装(现在就能用)
dsh plugin --profile <PROFILE> add github:zhishengplus/dsh-image-annotate
# 发布到 npm 之后也可以按包名装
dsh plugin --profile <PROFILE> add @<scope>/dsh-image-annotate
# 本地开发(link 一个 checkout)
dsh plugin --profile <PROFILE> add link:/path/to/dsh-image-annotate
装完确认它在该 profile 的 package.json 里:
"dsh": { "profile": { "bundles": [ /* …, */ "<上面用的包名>" ] } }
首次需要重启一次 DSH(profile 的 bundle 组合发生在启动时);之后改 lib/client.js 会被 client-modules 的 HMR 按 mtime 感知,刷新页面即可。
卸载:删掉 profile node_modules 里的目录,并去掉 dsh.profile.bundles 里那一行。
🧭 兼容性
| 实测 dsh 版本 | 0.2.0-rc.2(插件只依赖预览的 DOM 契约,理论上对后续版本也较稳) |
| Node | ≥ 20(仅仓库脚本需要;插件本身跑在浏览器里) |
| 依赖 | 零:无 @deepseek-ai/*、无 dsh-loader、无第三方运行时 |
🗺 路线图
- 圈选 / 箭头 / 方框 / 文字 / 擦除,画完可选中、拖动、缩放、改字、删除
- 原分辨率导出 + 一键进输入框 + 三级兜底
- 全量原生 token 外观
- 高亮荧光笔 / 马赛克打码
- 右侧栏「文件浏览」打开图片也能标注
- 自动编号的「标注清单」随图一起发给模型
📄 协议
MIT · 与 DeepSeek 官方无关,未获其许可或背书
如果它帮你省下过一次「我说的是这里」的解释,欢迎点个 ⭐
No comments yet. Be the first to write one.