@dsh-plugin/dsh-image-annotate 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.en.md +203 -0
- package/README.md +202 -2
- package/cordis.patch.yml +8 -0
- package/lib/client.js +1965 -0
- package/lib/index.js +14 -0
- package/package.json +64 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-image-annotate 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.en.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="docs/banner.png" alt="dsh-image-annotate — an annotation pen for the DeepSeek Harness image lightbox" width="100%">
|
|
3
|
+
|
|
4
|
+
<h3>Circle the problem right on the preview, keep nudging every mark,<br>and drop the annotated image straight into the composer.</h3>
|
|
5
|
+
|
|
6
|
+
[](https://github.com/topics/dsh-plugin)
|
|
7
|
+
[](#-compatibility)
|
|
8
|
+
[](./package.json)
|
|
9
|
+
[](./test)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](https://github.com/zhishengplus/dsh-image-annotate)
|
|
12
|
+
|
|
13
|
+
<a href="README.md">简体中文</a> · <b>English</b>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="docs/demo.gif" alt="Circling, labelling and dragging marks inside the DSH image preview, then sending the result to the composer" width="880">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center"><sub>Open an image → circle the problem → type a note → drag both into place → Add to composer → send</sub></p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<h3 align="center">🎯 The problem it solves</h3>
|
|
25
|
+
|
|
26
|
+
> "Move that button 8px to the left." — Which button?
|
|
27
|
+
>
|
|
28
|
+
> "This overlaps on narrow screens." — So you take another screenshot and draw an arrow on it.
|
|
29
|
+
>
|
|
30
|
+
> You hand a screenshot to an AI and say "this is broken here" — and it starts guessing where "here" is.
|
|
31
|
+
|
|
32
|
+
The wording was never the issue. **"Here" is something you have to point at.**
|
|
33
|
+
This plugin turns that into two seconds: **open the image → circle it → type → Add to composer → send.**
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="docs/before-after.png" alt="Left: the original screenshot. Right: the exact PNG the model receives after Add to composer." width="920">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p align="center"><sub>Left: the original. Right: <b>the exact PNG the model receives</b> after “Add to composer” — original resolution, marks included.</sub></p>
|
|
40
|
+
|
|
41
|
+
<h3 align="center">✨ Highlights</h3>
|
|
42
|
+
|
|
43
|
+
| | |
|
|
44
|
+
|---|---|
|
|
45
|
+
| 🖊 **Seven tools** | Move · Pen · Arrow · Box · Circle · Text · Eraser. Box and Circle form from the whole traced path, so a freehand loop works too |
|
|
46
|
+
| 🖐 **Editable after drawing** | Every mark is a live object: select, drag, scale by a corner handle, double-click to re-edit text, delete, nudge with arrow keys — all undoable |
|
|
47
|
+
| 🔤 **Text is an object** | Double-click to rewrite it (no duplicate layer); the Text tool can drag a placed label directly |
|
|
48
|
+
| 🎨 **Native look** | Toolbar, buttons, toasts and selection chrome are built from DSH design tokens; light and dark themes just work |
|
|
49
|
+
| 📨 **One click into the composer** | Flattened at original resolution and handed to the composer's own paste channel; falls back to drop → clipboard → download |
|
|
50
|
+
| 🪶 **Zero deps, no build** | No `@deepseek-ai/*` import, no `dsh-loader`, no bundler output — the npm tarball is 4 files |
|
|
51
|
+
| 🖼 **Works on every preview** | Screenshots in the conversation, draft attachments, tool-result images — same preview, same pen |
|
|
52
|
+
|
|
53
|
+
<h3 align="center">🚀 Quick start</h3>
|
|
54
|
+
|
|
55
|
+
1. **Click any image** in a session;
|
|
56
|
+
2. a round **Annotate** button appears in the preview's bottom-right (same shape as the native close button) — click it;
|
|
57
|
+
3. use **Circle / Box / Arrow** to mark the spot, or **Text** to write a note on the image;
|
|
58
|
+
4. not happy with the position? **Just drag it** (next section);
|
|
59
|
+
5. hit **Add to composer** — the image is attached to your input box, the preview closes, and you type your message as usual.
|
|
60
|
+
|
|
61
|
+
| Shortcut | Action |
|
|
62
|
+
| :-- | :-- |
|
|
63
|
+
| `V` | Select / move |
|
|
64
|
+
| `1` – `7` | Move · Pen · Arrow · Box · Circle · Text · Eraser |
|
|
65
|
+
| `E` | Eraser (removes the mark you click) |
|
|
66
|
+
| `Ctrl/⌘ + Z` · `Ctrl/⌘ + Shift + Z` | Undo · redo |
|
|
67
|
+
| `Delete` | Remove the selection |
|
|
68
|
+
| Arrow keys (`Shift` to accelerate) | Nudge 1px / 10px |
|
|
69
|
+
| Text tool | Click on the image → type → `Enter` (`Esc` cancels) |
|
|
70
|
+
|
|
71
|
+
<h3 align="center">🖐 Editing after the fact is the point</h3>
|
|
72
|
+
|
|
73
|
+
A paint tool burns pixels; this doesn't — **every mark stays editable**:
|
|
74
|
+
|
|
75
|
+
| Goal | How |
|
|
76
|
+
| :-- | :-- |
|
|
77
|
+
| **Select** | Switch to Move (`V`) and click any mark — the one you just drew is selected automatically |
|
|
78
|
+
| **Move** | Drag it; arrow keys nudge 1px, `Shift + arrows` 10px |
|
|
79
|
+
| **Scale** | Drag a corner handle of the selection box (text scales its font size too) |
|
|
80
|
+
| **Re-edit text** | **Double-click** any label and rewrite it — same mark, no duplicate |
|
|
81
|
+
| **Delete** | `Delete`, or the trash button (it becomes "delete selection" whenever something is selected) |
|
|
82
|
+
| **Regret** | Every step above is one undo away — `Ctrl+Z` all the way back |
|
|
83
|
+
|
|
84
|
+
<p align="center">
|
|
85
|
+
<img src="docs/demo-move-text.png" alt="The circle and the text label both moved; the dashed selection box and corner handles use DSH's native accent colour" width="880">
|
|
86
|
+
</p>
|
|
87
|
+
|
|
88
|
+
<p align="center"><sub>Both the circle and the label have been moved — the dashed box and handles use DSH's native accent colour</sub></p>
|
|
89
|
+
|
|
90
|
+
<h3 align="center">🎨 It looks like DSH grew it itself</h3>
|
|
91
|
+
|
|
92
|
+
The whole toolbar is assembled from DSH design tokens — no second visual language:
|
|
93
|
+
|
|
94
|
+
| Part | Native tokens |
|
|
95
|
+
| :-- | :-- |
|
|
96
|
+
| Toolbar surface | `--dsw-menu-surface-fill` + `--dsw-menu-backdrop-filter` + `--dsw-elevation-panel` + `--dsw-radius-lg` |
|
|
97
|
+
| Tool buttons | `--dsw-alias-button-tool-bar-fill` / `-hover` · `--dsw-alias-interactive-bg-hover` |
|
|
98
|
+
| Primary action | `--dsw-alias-button-primary-fill` · `--dsw-alias-label-primary-foreground` |
|
|
99
|
+
| Entry button | The **same 36px round button** as the preview's close control (`--dsw-specific-input-major` + 0.5px hairline) |
|
|
100
|
+
| Toasts | `--dsw-alias-toast-bg` / `-label` + `--dsw-shadow-lv3` (top-centred, like the native toast) |
|
|
101
|
+
| Selection | `--dsw-alias-state-business-primary` |
|
|
102
|
+
| Palette | Native static colours: red · amber · yellow · green · blue · brand `deepseek-450` · black · white |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
<h3 align="center">🛠 For developers</h3>
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary><b>Three “nots” that keep it working across DSH upgrades</b></summary>
|
|
110
|
+
|
|
111
|
+
| | What it does | Why it helps |
|
|
112
|
+
| :-- | :-- | :-- |
|
|
113
|
+
| **Doesn't touch the host** | No slot registration, no React tree surgery — it overlays its own canvas + toolbar on the preview DOM and removes both when the preview closes | No state fights with the official UI, immune to re-renders |
|
|
114
|
+
| **Doesn't depend on internals** | No `@deepseek-ai/*` import, no injected dsh service, no `dsh-loader` needed | Internal renames or API changes can't break it |
|
|
115
|
+
| **Doesn't need a build** | The browser half is a hand-written `window.__ModuleLoader__.load` factory (`lib/client.js`) — no tsdown/rollup output | Edit and reload (client-modules HMR keys on mtime); the tarball is 4 files |
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
<details>
|
|
120
|
+
<summary><b>How it works</b></summary>
|
|
121
|
+
|
|
122
|
+
- **Preview detection** — one stable trait: a `div[role="dialog"][aria-modal="true"]` directly under `body` whose **direct child is an `<img>`** (that's `dsh-client-ui-primitives`' `ImageLightbox`). A throttled `MutationObserver` mounts on appear and destroys on removal.
|
|
123
|
+
- **Coordinate system** — all marks are stored in **original-image pixels**. The overlay scales them to the displayed size; the export draws them at natural size. A 1909×1231 screenshot shown at 1427×920 therefore exports as a 1909×1231 PNG with every mark exactly where you put it.
|
|
124
|
+
- **Edit model** — strokes are plain objects (`{tool, color, points[], x, y, fontSize, textWidth…}`). Select / move / scale / re-edit evaluate the object and repaint the frame; a drag recomputes from the "snapshot at pointer-down + delta" each frame instead of accumulating, so nothing drifts.
|
|
125
|
+
- **Delivery** — image and marks are flattened into a PNG (`File`), then a **synthetic `paste` event carrying that file** is dispatched to the Lexical input (`[data-composer-input]`) — the same path a user-pasted screenshot takes. Fallbacks in order: document-level `drop` → clipboard (with a `Ctrl+V` hint) → PNG download.
|
|
126
|
+
- **Taint-proof export** — the original is re-fetched as a blob and decoded via `createImageBitmap` before compositing, avoiding `toBlob` SecurityErrors on cross-origin previews.
|
|
127
|
+
|
|
128
|
+
> One fun trap: a `position: fixed` overlay with `left: 50%` sizes itself against the space *right of* `left`, so the toolbar wrapped for no visible reason — `width: max-content` is the fix (the native Toast CSS documents the same trap).
|
|
129
|
+
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary><b>How far it's verified</b></summary>
|
|
134
|
+
|
|
135
|
+
| Suite | Covers | Result |
|
|
136
|
+
| :-- | :-- | :-- |
|
|
137
|
+
| [`test/test_annotate.py`](test/test_annotate.py) | canvas alignment, stroke pixels, undo, circle forming, export size, paste delivery, auto-close | **17 / 17** |
|
|
138
|
+
| [`test/test_edit.py`](test/test_edit.py) | select, drag, corner-handle scaling, text drag + double-click edit, arrow-key nudge, delete and undo | **19 / 19** |
|
|
139
|
+
| [`test/e2e_dsh.py`](test/e2e_dsh.py) | a **real dsh instance** (isolated profile + own port): open preview → circle → drag → label → deliver → assert the attachment is the original 1909×1231 | **21 / 21** |
|
|
140
|
+
|
|
141
|
+
The first two run against the bundled fixture ([`test/lightbox-fixture.html`](test/lightbox-fixture.html), which mirrors the real preview DOM shape); the third needs a disposable instance — see the script header and [PUBLISHING.md](PUBLISHING.md). CI ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs the syntax check plus both fixture suites on every push.
|
|
142
|
+
|
|
143
|
+
</details>
|
|
144
|
+
|
|
145
|
+
<details>
|
|
146
|
+
<summary><b>Debug</b></summary>
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
__dshImageAnnotate.annotators[0].state // tool / strokes / selected / natural / rect …
|
|
150
|
+
localStorage.setItem('@dsh-plugin/dsh-image-annotate.debug', '1') // verbose logs
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
<h3 align="center">📦 Install / uninstall</h3>
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
# from GitHub (works right now)
|
|
159
|
+
dsh plugin --profile <PROFILE> add github:zhishengplus/dsh-image-annotate
|
|
160
|
+
|
|
161
|
+
# from npm, once published
|
|
162
|
+
dsh plugin --profile <PROFILE> add @<scope>/dsh-image-annotate
|
|
163
|
+
|
|
164
|
+
# local development (link a checkout)
|
|
165
|
+
dsh plugin --profile <PROFILE> add link:/path/to/dsh-image-annotate
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Then make sure it is listed in the profile's `package.json`:
|
|
169
|
+
|
|
170
|
+
```jsonc
|
|
171
|
+
"dsh": { "profile": { "bundles": [ /* …, */ "<the package name you installed>" ] } }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**One DSH restart is required the first time** (profile bundles are composed at boot). After that, edits to `lib/client.js` are picked up by client-modules HMR (keyed on mtime) with a page refresh.
|
|
175
|
+
|
|
176
|
+
Uninstall: remove the folder from the profile's `node_modules` and drop the line from `dsh.profile.bundles`.
|
|
177
|
+
|
|
178
|
+
<h3 align="center">🧭 Compatibility</h3>
|
|
179
|
+
|
|
180
|
+
| | |
|
|
181
|
+
| :-- | :-- |
|
|
182
|
+
| Verified against | dsh `0.2.0-rc.2` (the plugin only relies on the preview's DOM contract, so it should survive most upgrades) |
|
|
183
|
+
| Node | ≥ 20 (repository scripts only — the plugin itself runs in the browser) |
|
|
184
|
+
| Dependencies | **none**: no `@deepseek-ai/*`, no `dsh-loader`, no third-party runtime |
|
|
185
|
+
|
|
186
|
+
<h3 align="center">🗺 Roadmap</h3>
|
|
187
|
+
|
|
188
|
+
- [x] Circle / arrow / box / text / eraser, with select, drag, scale, re-edit and delete after drawing
|
|
189
|
+
- [x] Original-resolution export + one-click delivery with three fallbacks
|
|
190
|
+
- [x] Fully native-token appearance
|
|
191
|
+
- [ ] Highlighter / mosaic redaction
|
|
192
|
+
- [ ] Annotation for images opened from the right-hand file browser
|
|
193
|
+
- [ ] Auto-numbered "annotation list" sent alongside the image
|
|
194
|
+
|
|
195
|
+
<h3 align="center">📄 License</h3>
|
|
196
|
+
|
|
197
|
+
<div align="center">
|
|
198
|
+
|
|
199
|
+
[MIT](LICENSE) · Not affiliated with or endorsed by DeepSeek
|
|
200
|
+
|
|
201
|
+
<sub>If it ever saves you one “no, <i>this</i> one” reply, a ⭐ is appreciated.</sub>
|
|
202
|
+
|
|
203
|
+
</div>
|
package/README.md
CHANGED
|
@@ -1,3 +1,203 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="docs/banner.png" alt="dsh-image-annotate(图片标注笔)—— 给 DeepSeek Harness 的图片预览加一支画笔" width="100%">
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
<h3>在 DSH 的图片预览上圈出问题,画完还能随手拖动调整,<br>一键丢进输入框发给模型。</h3>
|
|
5
|
+
|
|
6
|
+
[](https://github.com/topics/dsh-plugin)
|
|
7
|
+
[](#-兼容性)
|
|
8
|
+
[](./package.json)
|
|
9
|
+
[](./test)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](https://github.com/zhishengplus/dsh-image-annotate)
|
|
12
|
+
|
|
13
|
+
<b>简体中文</b> · <a href="README.en.md">English</a>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="docs/demo.gif" alt="在 DSH 图片预览里圈选、打字、拖动,然后把标注图加入输入框" width="880">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center"><sub>点开图片 → 圈出问题 → 写一行说明 → 拖到合适位置 → 加入输入框 → 接着打字发送</sub></p>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<h3 align="center">🎯 它解决的是什么</h3>
|
|
25
|
+
|
|
26
|
+
> 「这个按钮往左挪 8px。」—— 对面问:哪个按钮?
|
|
27
|
+
>
|
|
28
|
+
> 「小屏下这里会挤在一起。」—— 只好再截一张图,再画个箭头。
|
|
29
|
+
>
|
|
30
|
+
> 把截图丢给 AI 说「这里有问题」—— 它开始认真猜「这里」到底是哪。
|
|
31
|
+
|
|
32
|
+
问题从来不是表达,而是 **「这里」需要用手指出来的**。
|
|
33
|
+
这个插件把这件事压缩成两秒:**点开图片 → 圈出来 → 打字 → 加入输入框 → 发送**。
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="docs/before-after.png" alt="左边是原始截图,右边是加入输入框后模型真正收到的那张 PNG" width="920">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p align="center"><sub>左边是原图;右边是点「加入输入框」后 <b>模型真正收到的那张 PNG</b>(原分辨率,圈和字都在上面)</sub></p>
|
|
40
|
+
|
|
41
|
+
<h3 align="center">✨ 亮点一览</h3>
|
|
42
|
+
|
|
43
|
+
| | |
|
|
44
|
+
|---|---|
|
|
45
|
+
| 🖊 **七种工具** | 移动 · 画笔 · 箭头 · 方框 · 圆圈 · 文字 · 橡皮擦;圆圈和方框按整条轨迹的外接框成形,手绘一圈也成 |
|
|
46
|
+
| 🖐 **画完还能改** | 每一笔都是活对象:选中、拖动、拖角缩放、双击改字、删除、方向键微调,全部可撤销 |
|
|
47
|
+
| 🔤 **文字是对象** | 双击直接改内容(不会多出一层);文字工具下也能直接拖走已确定的文字框 |
|
|
48
|
+
| 🎨 **原生外观** | 工具条、按钮、提示、选中框全部用 DSH 设计 token 拼,浅色深色自动跟随 |
|
|
49
|
+
| 📨 **一键进输入框** | 合成原分辨率的 PNG,走的是 composer 自己的粘贴通道;失败还有拖放 / 剪贴板 / 下载三级兜底 |
|
|
50
|
+
| 🪶 **零依赖 · 无构建** | 不 import 任何 `@deepseek-ai/*`,不需要 dsh-loader,也没有打包产物 —— npm 包里就 4 个文件 |
|
|
51
|
+
| 🖼 **支持多种来源** | 消息里的截图、输入框里的草稿图、工具返回的图片,都是同一个预览,同一个画笔 |
|
|
52
|
+
|
|
53
|
+
<h3 align="center">🚀 30 秒上手</h3>
|
|
54
|
+
|
|
55
|
+
1. 在会话里**点开任意图片**;
|
|
56
|
+
2. 预览右下角出现圆形 **标注** 按钮(和右上角关闭按钮同款),点它;
|
|
57
|
+
3. 用 **圆圈 / 方框 / 箭头** 圈出问题,或用 **文字** 在图上写一句;
|
|
58
|
+
4. 位置不满意?**直接拖**(见下一节);
|
|
59
|
+
5. 点 **加入输入框** —— 标注图进到输入框,预览自动关闭,你接着打字发送。
|
|
60
|
+
|
|
61
|
+
| 快捷键 | 作用 |
|
|
62
|
+
| :-- | :-- |
|
|
63
|
+
| `V` | 选择 / 移动 |
|
|
64
|
+
| `1` – `7` | 移动 · 画笔 · 箭头 · 方框 · 圆圈 · 文字 · 擦除 |
|
|
65
|
+
| `E` | 橡皮擦(点中哪笔擦哪笔) |
|
|
66
|
+
| `Ctrl/⌘ + Z` · `Ctrl/⌘ + Shift + Z` | 撤销 · 重做 |
|
|
67
|
+
| `Delete` | 删除选中 |
|
|
68
|
+
| 方向键(`Shift` 加速) | 微调 1px / 10px |
|
|
69
|
+
| 文字工具 | 图上点一下 → 打字 → 回车(`Esc` 取消) |
|
|
70
|
+
|
|
71
|
+
<h3 align="center">🖐 画完还能改,这才是重点</h3>
|
|
72
|
+
|
|
73
|
+
截图工具画上去的线是烧死的像素,这里不是 —— **每一笔都可以再编辑**:
|
|
74
|
+
|
|
75
|
+
| 想做什么 | 怎么做 |
|
|
76
|
+
| :-- | :-- |
|
|
77
|
+
| **选中** | 切到「移动」(`V`) 点任意一笔;刚画完的那笔会自动选中 |
|
|
78
|
+
| **移动** | 直接拖;方向键微调 1px,`Shift + 方向键` 10px |
|
|
79
|
+
| **缩放** | 拖选中框四角的手柄等比缩放(文字会跟着改字号) |
|
|
80
|
+
| **改字** | **双击**任意文字直接改(改完还是同一笔) |
|
|
81
|
+
| **删除** | `Delete`,或工具条上的垃圾桶(有选中时它就变成"删除选中") |
|
|
82
|
+
| **后悔** | 上面每一步都在撤销栈里,`Ctrl+Z` 一路退回 |
|
|
83
|
+
|
|
84
|
+
<p align="center">
|
|
85
|
+
<img src="docs/demo-move-text.png" alt="圆圈被拖到新位置,文字框也被拖动,虚线选中框与四角手柄使用原生主色" width="880">
|
|
86
|
+
</p>
|
|
87
|
+
|
|
88
|
+
<p align="center"><sub>圈和文字都换过位置了 —— 虚线选中框与四角手柄用的是 DSH 原生主色</sub></p>
|
|
89
|
+
|
|
90
|
+
<h3 align="center">🎨 它看起来就像 DSH 自己长出来的</h3>
|
|
91
|
+
|
|
92
|
+
整支工具栏由 DSH 的设计 token 拼成,不引入第二套视觉语言:
|
|
93
|
+
|
|
94
|
+
| 部位 | 用到的原生 token |
|
|
95
|
+
| :-- | :-- |
|
|
96
|
+
| 工具条容器 | `--dsw-menu-surface-fill` + `--dsw-menu-backdrop-filter` + `--dsw-elevation-panel` + `--dsw-radius-lg` |
|
|
97
|
+
| 工具按钮 | `--dsw-alias-button-tool-bar-fill` / `-hover` · `--dsw-alias-interactive-bg-hover` |
|
|
98
|
+
| 主按钮 | `--dsw-alias-button-primary-fill` · `--dsw-alias-label-primary-foreground` |
|
|
99
|
+
| 右下角入口 | 与预览关闭按钮**同款** 36px 圆形(`--dsw-specific-input-major` + 0.5px 细边) |
|
|
100
|
+
| 提示条 | `--dsw-alias-toast-bg` / `-label` + `--dsw-shadow-lv3`(顶部居中,同原生 Toast) |
|
|
101
|
+
| 选中态 | `--dsw-alias-state-business-primary` |
|
|
102
|
+
| 色板 | 原生 static 色:红 · 琥珀 · 黄 · 绿 · 蓝 · 品牌蓝 `deepseek-450` · 黑 · 白 |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
<h3 align="center">🛠 给开发者</h3>
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary><b>三个「不」,让它在 DSH 升级时不容易坏</b></summary>
|
|
110
|
+
|
|
111
|
+
| | 做法 | 好处 |
|
|
112
|
+
| :-- | :-- | :-- |
|
|
113
|
+
| **不改宿主** | 不注册 slot、不动 React 树,只在预览 DOM 上叠一层自己的 canvas + 工具条,预览一关整层消失 | 不和官方 UI 抢状态,也不怕重渲染 |
|
|
114
|
+
| **不依赖内部包** | 不 `import` 任何 `@deepseek-ai/*`、不注入 dsh 服务、不需要 `dsh-loader` | 官方改内部 API / 改包名都影响不到它 |
|
|
115
|
+
| **不需要构建** | 浏览器半区就是一个手写的 `window.__ModuleLoader__.load` 工厂(`lib/client.js`),没有 tsdown / rollup 产物 | 改完即生效(client-modules 按 mtime 做 HMR),包里只有 4 个文件 |
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
<details>
|
|
120
|
+
<summary><b>它是怎么工作的</b></summary>
|
|
121
|
+
|
|
122
|
+
- **预览识别**:只认一个稳定特征 —— body 之下、`div[role="dialog"][aria-modal="true"]` 且**直接子元素里有 `<img>`** 的节点(即 `dsh-client-ui-primitives` 的 `ImageLightbox`)。用节流后的 `MutationObserver` 扫描:预览出现即挂载,移除即销毁。
|
|
123
|
+
- **坐标系统**:所有笔迹以**原图像素**存储。叠加显示时按显示尺寸缩放,导出时按原图尺寸绘制 —— 1909×1231 的截图在 1427×920 的预览里怎么画、怎么拖,导出的 PNG 就是 1909×1231 的同一位置。
|
|
124
|
+
- **编辑模型**:笔迹是普通对象(`{tool, color, points[], x, y, fontSize, textWidth…}`)。选中 / 移动 / 缩放 / 改字都是对对象求值后整帧重绘;拖动时每帧从「按下瞬间的快照 + 位移」重算而不是累加,避免漂移。
|
|
125
|
+
- **投递到输入框**:把「原图 + 笔迹」合成为 PNG(`File`),再向 Lexical 输入框(`[data-composer-input]`)派发一个**带该文件的合成 `paste` 事件** —— 也就是用户自己粘贴截图走的那条路。失败依次退回:document 级 `drop` → 剪贴板(提示 `Ctrl+V`)→ 下载 PNG。
|
|
126
|
+
- **导出不怕污染**:先用 `fetch(src)` 把 `blob:` 原图取回再解码(`createImageBitmap`),避开跨源 `drawImage` 导致的 `toBlob` SecurityError。
|
|
127
|
+
|
|
128
|
+
> 一个有意思的坑:`position: fixed` + `left: 50%` 的浮层,宽度按「left 右侧剩余空间」收缩,工具条会莫名换行 —— 补 `width: max-content` 才对(原生 Toast 的 CSS 注释里也记着同一个坑)。
|
|
129
|
+
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary><b>验证到什么程度</b></summary>
|
|
134
|
+
|
|
135
|
+
| 测试 | 覆盖 | 结果 |
|
|
136
|
+
| :-- | :-- | :-- |
|
|
137
|
+
| [`test/test_annotate.py`](test/test_annotate.py) | 画布贴合、笔迹像素、撤销、圆圈成形、导出尺寸、paste 投递、自动关闭 | **17 / 17** |
|
|
138
|
+
| [`test/test_edit.py`](test/test_edit.py) | 选中、拖动、角手柄缩放、文字拖动与双击改字、方向键微调、删除与撤销 | **19 / 19** |
|
|
139
|
+
| [`test/e2e_dsh.py`](test/e2e_dsh.py) | **真实 dsh 实例**(隔离 profile + 独立端口):打开预览 → 圈选 → 拖动 → 加字 → 投放 → 断言附件为原分辨率 1909×1231 | **21 / 21** |
|
|
140
|
+
|
|
141
|
+
前两套跑在自带 fixture([`test/lightbox-fixture.html`](test/lightbox-fixture.html),复刻 DSH 预览的真实 DOM 形状)上;第三套需要一个隔离实例,跑法见脚本头部注释与 [PUBLISHING.md](PUBLISHING.md)。CI([`.github/workflows/ci.yml`](.github/workflows/ci.yml))每次 push 都跑语法检查 + 两套 fixture。
|
|
142
|
+
|
|
143
|
+
</details>
|
|
144
|
+
|
|
145
|
+
<details>
|
|
146
|
+
<summary><b>调试</b></summary>
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
__dshImageAnnotate.annotators[0].state // tool / strokes / selected / natural / rect …
|
|
150
|
+
localStorage.setItem('@dsh-plugin/dsh-image-annotate.debug', '1') // 打开详细日志
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
<h3 align="center">📦 安装 / 卸载</h3>
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
# 从 GitHub 直接装(现在就能用)
|
|
159
|
+
dsh plugin --profile <PROFILE> add github:zhishengplus/dsh-image-annotate
|
|
160
|
+
|
|
161
|
+
# 发布到 npm 之后也可以按包名装
|
|
162
|
+
dsh plugin --profile <PROFILE> add @<scope>/dsh-image-annotate
|
|
163
|
+
|
|
164
|
+
# 本地开发(link 一个 checkout)
|
|
165
|
+
dsh plugin --profile <PROFILE> add link:/path/to/dsh-image-annotate
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
装完确认它在该 profile 的 `package.json` 里:
|
|
169
|
+
|
|
170
|
+
```jsonc
|
|
171
|
+
"dsh": { "profile": { "bundles": [ /* …, */ "<上面用的包名>" ] } }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**首次需要重启一次 DSH**(profile 的 bundle 组合发生在启动时);之后改 `lib/client.js` 会被 client-modules 的 HMR 按 mtime 感知,刷新页面即可。
|
|
175
|
+
|
|
176
|
+
卸载:删掉 profile `node_modules` 里的目录,并去掉 `dsh.profile.bundles` 里那一行。
|
|
177
|
+
|
|
178
|
+
<h3 align="center">🧭 兼容性</h3>
|
|
179
|
+
|
|
180
|
+
| | |
|
|
181
|
+
| :-- | :-- |
|
|
182
|
+
| 实测 dsh 版本 | `0.2.0-rc.2`(插件只依赖预览的 DOM 契约,理论上对后续版本也较稳) |
|
|
183
|
+
| Node | ≥ 20(仅仓库脚本需要;插件本身跑在浏览器里) |
|
|
184
|
+
| 依赖 | **零**:无 `@deepseek-ai/*`、无 `dsh-loader`、无第三方运行时 |
|
|
185
|
+
|
|
186
|
+
<h3 align="center">🗺 路线图</h3>
|
|
187
|
+
|
|
188
|
+
- [x] 圈选 / 箭头 / 方框 / 文字 / 擦除,画完可选中、拖动、缩放、改字、删除
|
|
189
|
+
- [x] 原分辨率导出 + 一键进输入框 + 三级兜底
|
|
190
|
+
- [x] 全量原生 token 外观
|
|
191
|
+
- [ ] 高亮荧光笔 / 马赛克打码
|
|
192
|
+
- [ ] 右侧栏「文件浏览」打开图片也能标注
|
|
193
|
+
- [ ] 自动编号的「标注清单」随图一起发给模型
|
|
194
|
+
|
|
195
|
+
<h3 align="center">📄 协议</h3>
|
|
196
|
+
|
|
197
|
+
<div align="center">
|
|
198
|
+
|
|
199
|
+
[MIT](LICENSE) · 与 DeepSeek 官方无关,未获其许可或背书
|
|
200
|
+
|
|
201
|
+
<sub>如果它帮你省下过一次「我说的是这里」的解释,欢迎点个 ⭐</sub>
|
|
202
|
+
|
|
203
|
+
</div>
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# dsh-image-annotate bundle patch — 插入插件行。
|
|
2
|
+
#
|
|
3
|
+
# 行内 name 必须是完整包名:client-modules 以 loader entry name 作为包名解析
|
|
4
|
+
# dsh.client 声明与 exports["./client"],浏览器半区随之以
|
|
5
|
+
# /plugins/@dsh-plugin/dsh-image-annotate/client.js 加载。
|
|
6
|
+
- insert:
|
|
7
|
+
- id: image-annotate
|
|
8
|
+
name: '@dsh-plugin/dsh-image-annotate'
|