dsh-codex-subscription 2.3.0 → 2.4.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/README.en.md CHANGED
@@ -16,21 +16,12 @@ No OpenAI API key or Codex CLI. Models, search, quota, and image generation stay
16
16
  [![MIT](https://img.shields.io/badge/license-MIT-111111.svg)](LICENSE)
17
17
  [![Star](https://img.shields.io/github/stars/WSL043/dsh-codex-subscription?style=flat&logo=github&label=Star)](https://github.com/WSL043/dsh-codex-subscription/stargazers)
18
18
 
19
- [What's new](#whats-new-in-230) · [Features](#feature-overview) · [Install](#install) · [Daily use](#inside-the-plugin) · [Examples](#from-sketch-to-image) · [User guide](docs/GUIDE.en.md) · [Update and uninstall](#update-and-uninstall)
19
+ [Features](#feature-overview) · [Install](#install) · [Daily use](#inside-the-plugin) · [Examples](#from-sketch-to-image) · [User guide](docs/GUIDE.en.md) · [Update and uninstall](#update-and-uninstall)
20
20
 
21
21
  </div>
22
22
 
23
23
  <p align="center"><img src="docs/assets/product-account-demo-en.png" width="1100" alt="Codex subscription account and quota interface with model choices, quota windows and alerts; demonstration data"></p>
24
24
 
25
- ## What's new in 2.3.0
26
-
27
- - **The official desktop app works.** The official DeepSeek Harness desktop app (0.2.0-rc.2) passed a hands-on acceptance run: plugin install, sign-in, model list, hiding models, a chat with a tool call, and image generation. See [Install](#install).
28
- - **Models come only from your account.** The model list is read entirely from the account catalog; before sign-in or when the read fails no model is shown, and there is no built-in fallback. Under Models & runtime you can show or hide each model in the composer.
29
- - **Settings reorganised.** Models and search are separate cards, switches use DSH's own control, and dead buttons no longer appear while signed out.
30
- - **Ultrafast speed tier (Beta).** The new tier from OpenAI's developer event. It appears in the model menu only when your account catalog lists it for that model (today Pro 500 with GPT-6 Astra); other accounts never see it and it is never silently swapped for another tier. It is Beta because it has not been run on a Pro 500 account yet.
31
- - **Shorter support diagnostics.** "Copy report" now copies the conclusions, recent failures and per-feature status (about 1 KB); the full report can still be copied separately from the details.
32
- - **Subtasks and stability.** Codex subtasks now really take effect on 0.2.0 and no longer expose the apps connected to your account; image generation being rejected by the host on the desktop app is fixed (2.2.11).
33
-
34
25
  ## Feature overview
35
26
 
36
27
  Subscription access, model controls, quota management, and image creation in one DSH workflow. Each capability is listed below; model access and quota depend on your account.
@@ -79,7 +70,7 @@ Subscription access, model controls, quota management, and image creation in one
79
70
  | **Smoothing and navigation** | Smooth complete strokes after release; pan, zoom, undo, and redo | Shortcuts can be customized or disabled |
80
71
  | **Multiple drafts** | Save and reopen up to 20 local drafts | Stored in the current browser |
81
72
  | **Import and export** | Export PNG, layered PSD, or native drafts that preserve editable strokes | PSD import supports a limited set of ordinary pixel layers |
82
- | **Agent drawing** | Ask the agent to draw with `@sketch`, follow progress, and stop it | Beta; off by default, requires the canvas |
73
+ | **Agent drawing** | Ask the agent to draw with `@sketch`, follow progress, and stop it; the agent can draw whole stages in SVG, imported as editable native strokes | Beta; off by default, requires the canvas |
83
74
  | **Result preview for the model** | Return a canvas image after drawing for subsequent review | Beta; off by default, adds image input usage |
84
75
 
85
76
  ### Advanced capabilities and maintenance
@@ -113,7 +104,7 @@ This plugin supports the latest DeepSeek Harness release recorded in its package
113
104
  2. Paste this package name, with its version, into the **Package name or address** field:
114
105
 
115
106
  ```text
116
- dsh-codex-subscription@2.3.0
107
+ dsh-codex-subscription@2.4.1
117
108
  ```
118
109
 
119
110
  3. Click **Install** and wait for completion. Follow the page instructions; save your work before restarting if requested.
@@ -183,59 +174,54 @@ Image generation, editing, and the sketch canvas are **Beta**. The canvas and ag
183
174
  Draw by hand with the canvas button, or ask the agent with `@sketch` and your drawing request.
184
175
 
185
176
  <table>
186
- <tr><th width="50%">Original sketch</th><th width="50%">Actual plugin output</th></tr>
187
- <tr><td><img src="docs/assets/sketch-demo-source.png" alt="Mountains and cabin sketch" width="480"></td><td><img src="docs/assets/sketch-demo-result.png" alt="Watercolor mountain cabin generated from the sketch" width="480"></td></tr>
177
+ <tr><th width="50%">Native sketch drawn by the agent</th><th width="50%">Image generated with the sketch as the only reference</th></tr>
178
+ <tr><td><img src="docs/assets/sketch-lighthouse-sketch.webp" alt="Lighthouse sketch drawn by GPT-6.1-Sol" width="480"></td><td><img src="docs/assets/sketch-lighthouse-result.webp" alt="Watercolor lighthouse poster generated from the sketch" width="480"></td></tr>
188
179
  </table>
189
180
 
190
- The request keeps the mountain and cabin composition, turning it into a warm watercolor illustration with green peaks, an orange roof, a meadow stream, and soft morning light, without the blue outlines.
181
+ **GPT-6.1-Sol · Xhigh** on the official desktop app 0.2.0-rc.2: one `@sketch` message produced 6 layers and 229 editable native strokes, then called GPT Image 2 (quality: high) once with the sketch as the only reference. About 13 minutes end to end.
182
+
183
+ The prompt (sent in Chinese): draw "Lighthouse and Homecoming" on a 4:3 canvas — a red-and-white lighthouse on a rocky cape casting a fan of light, an orange-sailed fishing boat heading home with a V-shaped wake, a half-risen sun, three cloud bands, three gulls and surf around the rocks, in six named layers; then turn the sketch into a warm watercolor travel poster with no text, keeping the composition.
191
184
 
192
185
  <details>
193
- <summary>Advanced examples · Layered illustration and Mona Lisa</summary>
186
+ <summary>Advanced examples · Mechanical cutaway, blue-green landscape and a reference-copy challenge</summary>
194
187
 
195
- **Someone Behind the Canvas**
188
+ The first two subjects were chosen to expose quality differences: a mechanical cutaway tests structure and detail (meshing gears, pendulum, chains, tiny figures on a staircase), and a Song-dynasty blue-green landscape tests composition and taste (the "three distances", empty space, texture strokes and mineral color). The same prompt went to **GPT-6.1-Sol · Xhigh** and **GPT-6-Astra · Low**, one run each, on the official desktop app, with GPT Image 2 (quality: high).
196
189
 
197
- **Astra draws the sketch; GPT Image 2 generates the illustration.** The editable original has 427 strokes across six layers. The image request retains its composition and adds paper-art and hand-painted detail. The requested model is `gpt-image-2`; the server does not report the executing model.
190
+ **Clock Tower Cutaway**
198
191
 
199
192
  <table>
200
- <tr><th width="50%">Native sketch</th><th width="50%">Generated result</th></tr>
201
- <tr><td><img src="docs/assets/sketch-advanced-source.png" alt="Sketch" width="480"></td><td><img src="docs/assets/sketch-advanced-result.png" alt="Result" width="480"></td></tr>
193
+ <tr><th></th><th width="45%">Native sketch</th><th width="45%">Generated image</th></tr>
194
+ <tr><td><b>Sol<br>Xhigh</b></td><td><img src="docs/assets/sketch-clocktower-sol-sketch.webp" alt="Clock tower sketch by Sol" width="400"></td><td><img src="docs/assets/sketch-clocktower-sol-result.webp" alt="Clock tower illustration from Sol's sketch" width="400"></td></tr>
195
+ <tr><td><b>Astra<br>Low</b></td><td><img src="docs/assets/sketch-clocktower-astra-sketch.webp" alt="Clock tower sketch by Astra" width="400"></td><td><img src="docs/assets/sketch-clocktower-astra-result.webp" alt="Clock tower illustration from Astra's sketch" width="400"></td></tr>
202
196
  </table>
203
197
 
204
- <details>
205
- <summary>Show the reproduction prompt and original image request</summary>
206
-
207
- **Sketch reproduction prompt (reconstructed from the artwork, not the original conversation)**
208
-
209
- Astra drew the original in stages. This Chinese prompt provides a starting point for the same concept, not a guarantee of an identical result.
210
-
211
- ```text
212
- @sketch 用 4:3 横版画板绘制《画布背面有人》:中央偏上是一处撕开的纸洞,洞内是深蓝星空和一位拿颜料桶的小画师;蓝色颜料从桶中流出,形成 S 形河流,流向下方城市。左侧城市保持未上色线稿,右侧城市被暖色点亮,加入纸船与飞鸟。按纸面、洞内世界、颜料河流、城市、画师和细节分层绘制,保留原生可编辑笔画。
213
- ```
198
+ Sol first computed tooth counts (48, 38, 27, 46, 36, 24, 34, 30, 22) and pitch circles for the nine gears with a script, then placed them in mesh; drawing took about an hour. Its first image request failed; it asked, was allowed one retry, and succeeded. Astra Low finished sketch and image in about 5 minutes, with a simpler structure and gears that are not individually calibrated.
214
199
 
215
- **Actual image-generation prompt (original Chinese)**
200
+ **A Thousand Green Peaks**
216
201
 
217
- ```text
218
- 请基于本条附加草图实际调用订阅图片工具一次,生成成品插画。quality=low,模型使用当前默认,不切换型号,不额外生成。主题《画布背面有人》:保留4:3横构图、中央偏上的撕纸洞口、洞内拿颜料桶的小画师、流出成为S形河流的蓝色颜料、下方左侧未上色城市与右侧被点亮城市、纸船飞鸟。精修为惊艳的立体纸艺与精细手绘结合的编辑插画,纸张纤维、真实撕边及柔和投影,深靛蓝洞内星月,丰富青蓝颜料层次和流动质感,赭橙画师与暖色建筑,微小清晰的叙事细节。不重构为风景,不添加文字水印。必须使用本条参考图片编辑,不能仅凭文字生成。生成后简短说明完成即可。
219
- ```
220
-
221
- </details>
222
-
223
- Original example released in: [Beta v2.1.0-beta.2](https://github.com/WSL043/dsh-codex-subscription/releases/tag/v2.1.0-beta.2)
202
+ <table>
203
+ <tr><th></th><th width="45%">Native sketch</th><th width="45%">Generated image</th></tr>
204
+ <tr><td><b>Sol<br>Xhigh</b></td><td><img src="docs/assets/sketch-shanshui-sol-sketch.webp" alt="Landscape sketch by Sol" width="400"></td><td><img src="docs/assets/sketch-shanshui-sol-result.webp" alt="Blue-green landscape from Sol's sketch" width="400"></td></tr>
205
+ <tr><td><b>Astra<br>Low</b></td><td><img src="docs/assets/sketch-shanshui-astra-sketch.webp" alt="Landscape sketch by Astra" width="400"></td><td><img src="docs/assets/sketch-shanshui-astra-result.webp" alt="Blue-green landscape from Astra's sketch" width="400"></td></tr>
206
+ </table>
224
207
 
225
- **Mona Lisa: Astra sketch → GPT image generation**
208
+ Sol Xhigh took about 28 minutes and Astra Low about 5. Both kept the main peak, waterfall, plank path and river in place; Sol's receding ranges and mist bands are more complete.
226
209
 
227
- Example version: [2.1.0-beta.5](https://github.com/WSL043/dsh-codex-subscription/releases/tag/v2.1.0-beta.5)
210
+ **"DeepSeek-chan" · copy a reference image (draw only, no image generation)**
228
211
 
229
- Actual results supplied by the user from another computer: Astra draws on a portrait canvas, then GPT image generation turns the sketch into an oil painting.
212
+ Here the reference image is attached to the message and the model copies it on the canvas, which tests proportions, gradient long hair, lace trim, hand lettering and small motifs. The reference is a community sticker of the DeepSeek mascot girl. Each cell is one run on the official desktop app with no image generation; the native-stroke runs had "return preview when finished" enabled, and the SVG runs added one sentence to the same prompt asking for the svg command on every layer.
230
213
 
231
214
  <table>
232
- <tr><th width="50%">Native Astra sketch</th><th width="50%">GPT-generated oil painting</th></tr>
233
- <tr><td><img src="docs/assets/sketch-mona-lisa-source.png" alt="Mona Lisa sketch drawn by Astra" width="480"></td><td><img src="docs/assets/sketch-mona-lisa-result.png" alt="Mona Lisa oil painting generated from the sketch" width="480"></td></tr>
215
+ <tr><th width="20%">Reference</th><th width="40%">Native strokes</th><th width="40%">SVG</th></tr>
216
+ <tr><td rowspan="2"><img src="docs/assets/sketch-niang-reference.webp" alt="DeepSeek-chan sticker reference" width="220"></td><td><img src="docs/assets/sketch-niang-sol-native.webp" alt="Sol native strokes" width="330"><br><sub><b>Sol Xhigh</b> · 40 min 50 s · 526 strokes</sub></td><td><img src="docs/assets/sketch-niang-sol-svg.webp" alt="Sol SVG" width="330"><br><sub><b>Sol Xhigh</b> · 31 min 48 s · 463 strokes</sub></td></tr>
217
+ <tr><td><img src="docs/assets/sketch-niang-astra-native.webp" alt="Astra native strokes" width="330"><br><sub><b>Astra Medium</b> · about 7 min · 332 strokes</sub></td><td><img src="docs/assets/sketch-niang-astra-svg.webp" alt="Astra SVG" width="330"><br><sub><b>Astra Medium</b> · 10 min 5 s</sub></td></tr>
234
218
  </table>
235
219
 
236
- Original sketch prompt: `@sketch 用竖版画板画一幅《蒙娜丽莎》` (Draw the Mona Lisa on a portrait canvas.)
220
+ The SVG runs have finer hair highlights, lace trim and lettering, and Sol was almost 9 minutes faster with SVG than stroke by stroke; Astra spent about 3 extra minutes on SVG for clearly richer detail. Neither model got the face shape right (too long and flat) or the small whale motif on the apron, though Sol's whale looks more like a whale.
221
+
222
+ The full Chinese prompts for all three subjects are in the [Chinese README](README.md#从草图到作品).
237
223
 
238
- Original image prompt: `帮我变成油画` (Turn it into an oil painting.)
224
+ Each example is a single run, not a measure of consistent quality. Sol Xhigh is much slower; Astra or a lower reasoning level is enough for everyday sketches.
239
225
 
240
226
  </details>
241
227
 
package/README.md CHANGED
@@ -16,21 +16,12 @@
16
16
  [![MIT](https://img.shields.io/badge/license-MIT-111111.svg)](LICENSE)
17
17
  [![Star](https://img.shields.io/github/stars/WSL043/dsh-codex-subscription?style=flat&logo=github&label=Star)](https://github.com/WSL043/dsh-codex-subscription/stargazers)
18
18
 
19
- [更新亮点](#230-更新亮点) · [功能](#功能一览) · [安装](#安装) · [日常使用](#界面一览) · [作品案例](#从草图到作品) · [使用指南](docs/GUIDE.zh-CN.md) · [更新与卸载](#更新与卸载)
19
+ [功能](#功能一览) · [安装](#安装) · [日常使用](#界面一览) · [作品案例](#从草图到作品) · [使用指南](docs/GUIDE.zh-CN.md) · [更新与卸载](#更新与卸载)
20
20
 
21
21
  </div>
22
22
 
23
23
  <p align="center"><img src="docs/assets/product-account-demo.png" width="1100" alt="Codex 订阅账号与额度界面:模型选择、双周期额度与提醒;使用演示数据"></p>
24
24
 
25
- ## 2.3.0 更新亮点
26
-
27
- - **官方桌面端可用**:DeepSeek Harness 官方桌面版(0.2.0-rc.2)已完成实机验收——安装插件、登录、模型列表、隐藏模型、带工具调用的对话、图片生成均通过;安装方式见[安装](#安装)。
28
- - **模型只来自你的账号**:模型列表完全读取账号目录,登录前或读取失败时不显示任何模型,也没有内置兜底;“模型与运行”里可逐个显示 / 隐藏输入框中的模型。
29
- - **设置页重新整理**:模型与搜索分成两个卡片,开关统一使用 DSH 自带控件,未登录时不再出现无效按钮。
30
- - **极速档 Ultrafast(Beta)**:OpenAI 发布会新增的速度档。只有账号模型目录明确给出该档位(目前为 Pro 500 的 GPT-6 Astra)才会在模型菜单里出现,其他账号不显示,也不会被悄悄降级到别的档位;由于暂无 Pro 500 账号做实机验证,标为 Beta。
31
- - **更短的支持诊断**:“复制诊断”现在只复制结论、近期失败和各能力状态(约 1 KB),完整报告仍可在详情里单独复制。
32
- - **子任务与稳定性**:Codex 子任务在 0.2.0 上真正生效,并且不再暴露账号里连接的应用;修复了图片生成在桌面版上被宿主拒绝的问题(2.2.11)。
33
-
34
25
  ## 功能一览
35
26
 
36
27
  把订阅接入、模型控制、额度管理和图片创作放在同一个 DSH 工作流里。下面逐项列出插件提供的能力;模型权限和额度以账号实际返回为准。
@@ -79,7 +70,7 @@
79
70
  | **笔画平滑与导航** | 抬笔后平滑整条路径;拖动、缩放、撤销重做 | 快捷键可自定义和关闭 |
80
71
  | **多份草稿** | 保存并重新编辑最多 20 份本地草稿 | 保存在当前浏览器 |
81
72
  | **导入与导出** | 导出 PNG、分层 PSD,或交换保留原生笔画的草稿文件 | PSD 导入仅支持有限的普通像素图层 |
82
- | **Agent 绘图** | 用 `@sketch` 让 Agent 绘制,查看进度并随时停止 | Beta;默认关闭,需同时开启画板 |
73
+ | **Agent 绘图** | 用 `@sketch` 让 Agent 绘制,查看进度并随时停止;Agent 可用 SVG 整段作画,导入为可编辑原生笔画 | Beta;默认关闭,需同时开启画板 |
83
74
  | **绘图结果回看** | 完成后向模型返回画布预览,帮助后续检查 | Beta;默认关闭,会增加图片输入用量 |
84
75
 
85
76
  ### 进阶能力与维护
@@ -113,7 +104,7 @@
113
104
  2. 在 **包名或地址** 输入框中粘贴下面的包名(已带版本号):
114
105
 
115
106
  ```text
116
- dsh-codex-subscription@2.3.0
107
+ dsh-codex-subscription@2.4.1
117
108
  ```
118
109
 
119
110
  3. 点击 **安装**,等待安装完成;按页面提示操作,需要重启时先保存工作。
@@ -183,59 +174,77 @@ dsh --profile headless "只回复:ok"
183
174
  手动画图时点击画板按钮;让 Agent 绘图时发送 `@sketch` 加绘图要求。
184
175
 
185
176
  <table>
186
- <tr><th width="50%">画板原草图</th><th width="50%">插件实际生成结果</th></tr>
187
- <tr><td><img src="docs/assets/sketch-demo-source.png" alt="山峰与小屋草图" width="480"></td><td><img src="docs/assets/sketch-demo-result.png" alt="根据草图生成的水彩山间小屋" width="480"></td></tr>
177
+ <tr><th width="50%">Agent 绘制的原生草图</th><th width="50%">以草图为唯一参考的生图结果</th></tr>
178
+ <tr><td><img src="docs/assets/sketch-lighthouse-sketch.webp" alt="GPT-6.1-Sol 绘制的灯塔与归航草图" width="480"></td><td><img src="docs/assets/sketch-lighthouse-result.webp" alt="根据草图生成的灯塔水彩海报" width="480"></td></tr>
188
179
  </table>
189
180
 
190
- 示例要求:保留山峰与小屋的构图,生成温暖的水彩旅行插画,青绿山峰、橙色屋顶、草地小溪与柔和晨光,不保留蓝色线条。
181
+ **GPT-6.1-Sol · Xhigh**,官方桌面版 0.2.0-rc.2 实测:一条 `@sketch` 消息完成 6 个图层、229 笔可编辑原生笔画,再以这张草图为唯一参考调用一次 GPT Image 2(质量:高),全程约 13 分钟。
182
+
183
+ ```text
184
+ @sketch 用 4:3 横版画板绘制《灯塔与归航》:左侧礁石岬角上立一座红白条纹灯塔,塔顶灯室射出扇形光束;右侧海面上一艘挂橙色小帆的渔船正驶回港湾,船后拖出 V 形水痕;地平线上是半个初升的太阳,天空有三条横向云带和三只海鸥,前景礁石间有一圈白色浪花。按天空、太阳与云、海面、礁石与灯塔、渔船、海鸥与浪花分层绘制,保留原生可编辑笔画。画完后,把这张草图作为唯一参考图,调用订阅图片工具一次生成成品:保持构图不变,做成温暖的水彩旅行海报,晨光金橙配靛青海面,有纸纹和柔和晕染,不添加文字。
185
+ ```
191
186
 
192
187
  <details>
193
- <summary>进阶展示 · 分层插画与蒙娜丽莎</summary>
188
+ <summary>进阶展示 · 机械剖面、青绿山水与 DeepSeek 娘临摹</summary>
194
189
 
195
- **《画布背面有人》**
190
+ 前两道题都选了容易暴露差距的题材:机械剖面考结构与细节(齿轮咬合、摆锤、链条、楼梯上的人物),青绿山水考构图与审美(三远法、留白、皴法与设色)。同一条提示词分别交给 **GPT-6.1-Sol · Xhigh** 和 **GPT-6-Astra · Low**,各跑一次,均在官方桌面版上完成,生图统一为 GPT Image 2(质量:高)。
196
191
 
197
- **Astra 绘制草图,GPT Image 2 生成成图。** 原生草图共 6 层、427 笔,可继续编辑;生图请求保留构图,精修纸艺与手绘质感。请求型号为 `gpt-image-2`,服务端未报告实际执行型号。
192
+ **《钟塔剖面图》**
198
193
 
199
194
  <table>
200
- <tr><th width="50%">原生草图</th><th width="50%">实际生成结果</th></tr>
201
- <tr><td><img src="docs/assets/sketch-advanced-source.png" alt="Sketch" width="480"></td><td><img src="docs/assets/sketch-advanced-result.png" alt="Result" width="480"></td></tr>
195
+ <tr><th></th><th width="45%">原生草图</th><th width="45%">生图结果</th></tr>
196
+ <tr><td><b>Sol<br>Xhigh</b></td><td><img src="docs/assets/sketch-clocktower-sol-sketch.webp" alt="Sol 绘制的钟塔剖面草图" width="400"></td><td><img src="docs/assets/sketch-clocktower-sol-result.webp" alt="Sol 草图生成的钟塔剖面插画" width="400"></td></tr>
197
+ <tr><td><b>Astra<br>Low</b></td><td><img src="docs/assets/sketch-clocktower-astra-sketch.webp" alt="Astra 绘制的钟塔剖面草图" width="400"></td><td><img src="docs/assets/sketch-clocktower-astra-result.webp" alt="Astra 草图生成的钟塔剖面插画" width="400"></td></tr>
202
198
  </table>
203
199
 
204
- <details>
205
- <summary>展开复现提示词与原始生图请求</summary>
200
+ Sol 先用脚本算出 9 个齿轮的齿数(48、38、27、46、36、24、34、30、22)与节圆,再按咬合位置排布,绘图约 1 小时;第一次生图请求失败,按它的询问同意重试一次后成功。Astra Low 约 5 分钟完成全部草图与生图,结构更简化,齿轮没有逐一校准。
206
201
 
207
- **草图复现提示词(按原画面整理,非完整原始对话)**
202
+ **《千峰翠色》**
208
203
 
209
- 原案例由 Astra 分阶段绘制,下面提供同主题的复现起点,不保证得到完全相同的画面。
204
+ <table>
205
+ <tr><th></th><th width="45%">原生草图</th><th width="45%">生图结果</th></tr>
206
+ <tr><td><b>Sol<br>Xhigh</b></td><td><img src="docs/assets/sketch-shanshui-sol-sketch.webp" alt="Sol 绘制的青绿山水草图" width="400"></td><td><img src="docs/assets/sketch-shanshui-sol-result.webp" alt="Sol 草图生成的青绿山水" width="400"></td></tr>
207
+ <tr><td><b>Astra<br>Low</b></td><td><img src="docs/assets/sketch-shanshui-astra-sketch.webp" alt="Astra 绘制的青绿山水草图" width="400"></td><td><img src="docs/assets/sketch-shanshui-astra-result.webp" alt="Astra 草图生成的青绿山水" width="400"></td></tr>
208
+ </table>
210
209
 
211
- ```text
212
- @sketch 用 4:3 横版画板绘制《画布背面有人》:中央偏上是一处撕开的纸洞,洞内是深蓝星空和一位拿颜料桶的小画师;蓝色颜料从桶中流出,形成 S 形河流,流向下方城市。左侧城市保持未上色线稿,右侧城市被暖色点亮,加入纸船与飞鸟。按纸面、洞内世界、颜料河流、城市、画师和细节分层绘制,保留原生可编辑笔画。
213
- ```
210
+ Sol Xhigh 约 28 分钟,Astra Low 约 5 分钟。两者都保留了主峰、瀑布、栈道与江面的位置;Sol 的远山层次和雾带留白更完整。
214
211
 
215
- **实际生图提示词**
212
+ **《DeepSeek 娘》· 照着参考图临摹(只画不生图)**
216
213
 
217
- ```text
218
- 请基于本条附加草图实际调用订阅图片工具一次,生成成品插画。quality=low,模型使用当前默认,不切换型号,不额外生成。主题《画布背面有人》:保留4:3横构图、中央偏上的撕纸洞口、洞内拿颜料桶的小画师、流出成为S形河流的蓝色颜料、下方左侧未上色城市与右侧被点亮城市、纸船飞鸟。精修为惊艳的立体纸艺与精细手绘结合的编辑插画,纸张纤维、真实撕边及柔和投影,深靛蓝洞内星月,丰富青蓝颜料层次和流动质感,赭橙画师与暖色建筑,微小清晰的叙事细节。不重构为风景,不添加文字水印。必须使用本条参考图片编辑,不能仅凭文字生成。生成后简短说明完成即可。
219
- ```
214
+ 这道题把一张参考图作为附件发给模型,让它在画板上临摹,考的是比例、渐变长发、蕾丝褶边、手写字和小图案能不能对得上。参考图是社区流传的 DeepSeek 娘表情包。每格都在官方桌面版上跑一次,没有调用生图;原生笔画版开启了“完成后返回预览”,SVG 版在同一条提示词后加了一句“每层用 svg 命令”。
220
215
 
221
- </details>
216
+ <table>
217
+ <tr><th width="20%">参考图</th><th width="40%">原生笔画</th><th width="40%">SVG</th></tr>
218
+ <tr><td rowspan="2"><img src="docs/assets/sketch-niang-reference.webp" alt="DeepSeek 娘表情包参考图" width="220"></td><td><img src="docs/assets/sketch-niang-sol-native.webp" alt="Sol 原生笔画版" width="330"><br><sub><b>Sol Xhigh</b> · 40 分 50 秒 · 526 笔</sub></td><td><img src="docs/assets/sketch-niang-sol-svg.webp" alt="Sol SVG 版" width="330"><br><sub><b>Sol Xhigh</b> · 31 分 48 秒 · 463 笔</sub></td></tr>
219
+ <tr><td><img src="docs/assets/sketch-niang-astra-native.webp" alt="Astra 原生笔画版" width="330"><br><sub><b>Astra Medium</b> · 约 7 分钟 · 332 笔</sub></td><td><img src="docs/assets/sketch-niang-astra-svg.webp" alt="Astra SVG 版" width="330"><br><sub><b>Astra Medium</b> · 10 分 5 秒</sub></td></tr>
220
+ </table>
222
221
 
223
- 原案例首发:[Beta v2.1.0-beta.2](https://github.com/WSL043/dsh-codex-subscription/releases/tag/v2.1.0-beta.2)
222
+ SVG 版的发丝高光、蕾丝褶边和手写字更精细,Sol 用 SVG 还比逐笔绘制快了近 9 分钟;Astra 用 SVG 多花约 3 分钟,换来明显更丰富的细节。两个模型都没能画准脸型(偏长偏平)和围裙上的小鲸鱼图案,Sol 的鲸鱼更像鲸。
224
223
 
225
- **《蒙娜丽莎》:Astra 草图 → GPT 生图**
224
+ <details>
225
+ <summary>展开三道题的完整提示词</summary>
226
226
 
227
- 案例版本:[2.1.0-beta.5](https://github.com/WSL043/dsh-codex-subscription/releases/tag/v2.1.0-beta.5)
227
+ ```text
228
+ @sketch 用 3:4 竖版画板绘制《钟塔剖面图》:蒸汽朋克风格的钟塔纵向剖面技术插图。最上方是圆形大钟面(十二个刻度、两根指针)和一口铜钟;钟面下一层是齿轮机房:至少 9 个大小不同、齿数正确、彼此咬合的齿轮(相邻齿轮转向相反,齿廓要画出锯齿),另有一个锚形擒纵机构;中段是一根贯穿的长摆杆,末端为铜制圆盘摆锤,旁边两条挂着配重铁块的链条;塔壁一侧是沿墙盘旋而上的螺旋楼梯,梯上有三个微小的工匠人物;底部是石砌地基与拱门。剖面用厚实的暗色砖石墙体框出,内部露出黄铜与铁质结构,加入铆钉、管道,以及从小窗透进来的光柱。按背景砖墙、结构框架、齿轮组、摆锤与链条、楼梯与人物、光影与细节分层绘制。画完后,把这张草图作为唯一参考图,调用订阅图片工具一次生成成品:保持每个部件的位置、比例与咬合关系不变,做成精密的蒸汽朋克剖面图插画——黄铜与旧钢的金属质感、细致的机械倒影和铆钉、羊皮纸底色配墨线渲染、暖色灯光,不添加文字。
229
+ ```
228
230
 
229
- 用户在另一台电脑上的实际效果:先让 Astra 在竖版画板上绘制,再通过 GPT 生图转成油画。
231
+ ```text
232
+ @sketch 用 3:4 竖版画板绘制《千峰翠色》:宋代青绿山水的构图,严格运用「三远法」。高远:右侧一座主峰拔地而起,占画面上半部,山体用层叠的块面表现,山腰有横向的云雾留白;深远:主峰后方还有两重淡青色远山,越远越淡,之间隔着大片留白的雾带;平远:下方是舒展的江面,一叶渔舟和两只飞鸟。左侧一挂瀑布从中段峭壁倾泻而下,落入山涧,涧边有三株松树,松针成簇;山腰有一座小亭和一条曲折栈道,栈道上有两个极小的行者;前景是坡石与芦苇,用勾勒加披麻皴式的笔触表现。色彩层次为石青、石绿、赭石,远山用淡青。按远山与天空、雾带留白、主峰、瀑布与松树、亭台栈道与行者、江面舟鸟与前景分层绘制。画完后,把这张草图作为唯一参考图,调用订阅图片工具一次生成成品:保持山体、瀑布、栈道、江面的位置和三远法的层次不变,做成宋代青绿山水绢本设色——石青石绿矿物颜料的沉着厚重、细腻的皴擦与勾勒、绢面纹理与微微泛黄的古意、云雾留白,不添加文字或印章。
233
+ ```
230
234
 
231
- <table>
232
- <tr><th width="50%">Astra 原生草图</th><th width="50%">GPT 生图:油画效果</th></tr>
233
- <tr><td><img src="docs/assets/sketch-mona-lisa-source.png" alt="Astra 绘制的蒙娜丽莎草图" width="480"></td><td><img src="docs/assets/sketch-mona-lisa-result.png" alt="草图转换后的蒙娜丽莎油画" width="480"></td></tr>
234
- </table>
235
+ ```text
236
+ @sketch 用 1:1 方形画板,尽可能忠实地临摹我附上的这张参考图(一张 Q 版二次元表情包)。画面:白色底;Q 版二头身的蓝发鲸鱼娘跪坐在画面右侧偏中,蓝色渐变长发披肩(发根深蓝、发梢浅蓝),头顶一根呆毛,戴白色蕾丝女仆头饰和蓝色蝴蝶结,头两侧是深蓝色带白色内里的鲸鱼耳鳍,蓝色大眼睛带高光,猫嘴嘟嘴的“ω”表情,脸颊泛红,双手叉腰;穿深蓝色女仆装,白色蕾丝围裙(围裙上有一只蓝色小鲸鱼图案),黑色领结,袖口和裙摆有白色蕾丝褶边;身后露出一条深蓝色的鲸鱼尾巴。左上方是白色对话气泡(黑色描边,尖角指向她),气泡里用黑色粗体写“你这吃白饭的蓝色大肥鱼。”;左侧从画面外伸进来一只黑袖子的手,食指指向她;她的左下方用蓝色描边字写“没吃饱喵!”;她脚边的地上放着一只空的白瓷饭碗,碗内壁印着一只蓝色小鲸鱼。请分层绘制:白色背景、头发与鲸鱼耳鳍与尾巴、身体与服装、脸部五官、饭碗、手臂与对话气泡文字,保留原生可编辑笔画,并特别注意角色比例、头发的渐变与发丝走向、蕾丝褶边和脸部表情的细节。这次只在草图画布上画,不要调用任何生图或图片编辑工具;画完后检查一遍与参考图的差异并修正,最后保存并完成。
237
+ ```
235
238
 
236
- 草图提示词:`@sketch 用竖版画板画一幅《蒙娜丽莎》`
239
+ SVG 版在“这次只在草图画布上画”之前多加了一句:
240
+
241
+ ```text
242
+ 作画方式:每个图层都用 svg 命令整层写成 SVG(路径用贝塞尔曲线,配色手动分层做出渐变感),不要逐点使用原生笔画命令。
243
+ ```
244
+
245
+ </details>
237
246
 
238
- 生图提示词:`帮我变成油画`
247
+ 每个例子只跑一次,不代表稳定水平;Sol Xhigh 耗时明显更长,日常草图用 Astra 或较低推理档即可。
239
248
 
240
249
  </details>
241
250
 
package/lib/client.js CHANGED
@@ -2321,6 +2321,799 @@ window.__ModuleLoader__.load({
2321
2321
  }))
2322
2322
  };
2323
2323
  }
2324
+ //#endregion
2325
+ //#region src/sketch-svg.js
2326
+ const MAX_SVG_CHARACTERS = 2e5;
2327
+ const MAX_BEZIER_SEGMENTS = 64;
2328
+ const FLATTEN_STEPS = 10;
2329
+ const NAMED_COLORS = {
2330
+ black: "#000000",
2331
+ white: "#ffffff",
2332
+ red: "#ff0000",
2333
+ green: "#008000",
2334
+ blue: "#0000ff",
2335
+ yellow: "#ffff00",
2336
+ orange: "#ffa500",
2337
+ purple: "#800080",
2338
+ pink: "#ffc0cb",
2339
+ brown: "#a52a2a",
2340
+ gray: "#808080",
2341
+ grey: "#808080",
2342
+ silver: "#c0c0c0",
2343
+ gold: "#ffd700",
2344
+ navy: "#000080",
2345
+ teal: "#008080",
2346
+ maroon: "#800000",
2347
+ olive: "#808000",
2348
+ lime: "#00ff00",
2349
+ aqua: "#00ffff",
2350
+ cyan: "#00ffff",
2351
+ magenta: "#ff00ff",
2352
+ fuchsia: "#ff00ff",
2353
+ ivory: "#fffff0",
2354
+ beige: "#f5f5dc",
2355
+ tan: "#d2b48c",
2356
+ khaki: "#f0e68c",
2357
+ coral: "#ff7f50",
2358
+ salmon: "#fa8072",
2359
+ crimson: "#dc143c",
2360
+ indigo: "#4b0082",
2361
+ violet: "#ee82ee",
2362
+ turquoise: "#40e0d0",
2363
+ skyblue: "#87ceeb",
2364
+ darkgray: "#a9a9a9",
2365
+ darkgrey: "#a9a9a9",
2366
+ lightgray: "#d3d3d3",
2367
+ lightgrey: "#d3d3d3",
2368
+ darkred: "#8b0000",
2369
+ darkgreen: "#006400",
2370
+ darkblue: "#00008b",
2371
+ transparent: "none"
2372
+ };
2373
+ const UNSUPPORTED = /* @__PURE__ */ new Set([
2374
+ "text",
2375
+ "tspan",
2376
+ "image",
2377
+ "foreignObject",
2378
+ "filter",
2379
+ "clipPath",
2380
+ "mask",
2381
+ "pattern",
2382
+ "style",
2383
+ "script",
2384
+ "switch",
2385
+ "marker",
2386
+ "symbol"
2387
+ ]);
2388
+ const IGNORED = /* @__PURE__ */ new Set([
2389
+ "title",
2390
+ "desc",
2391
+ "metadata",
2392
+ "defs",
2393
+ "linearGradient",
2394
+ "radialGradient",
2395
+ "stop",
2396
+ "namedview",
2397
+ "sodipodi:namedview"
2398
+ ]);
2399
+ const decode = (value) => value.split("&quot;").join("\"").split("&apos;").join("'").split("&lt;").join("<").split("&gt;").join(">").split("&amp;").join("&");
2400
+ function parseXml(text) {
2401
+ const tag = /<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<!\[CDATA\[[\s\S]*?\]\]>|<!DOCTYPE[^>]*>|<(\/?)([A-Za-z_][\w:.-]*)((?:\s+[^\s=/>]+\s*=\s*(?:"[^"]*"|'[^']*'))*)\s*(\/?)>/g;
2402
+ const attribute = /([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
2403
+ const root = {
2404
+ name: "#root",
2405
+ attrs: {},
2406
+ children: []
2407
+ };
2408
+ const stack = [root];
2409
+ for (let match = tag.exec(text); match; match = tag.exec(text)) {
2410
+ if (match[2] === void 0) continue;
2411
+ if (match[1] === "/") {
2412
+ if (stack.length > 1 && stack.at(-1).name === match[2]) stack.pop();
2413
+ else throw Error(`SVG is malformed: unexpected </${match[2]}>`);
2414
+ continue;
2415
+ }
2416
+ const attrs = {};
2417
+ for (let item = attribute.exec(match[3]); item; item = attribute.exec(match[3])) attrs[item[1]] = decode(item[2] ?? item[3]);
2418
+ const node = {
2419
+ name: match[2],
2420
+ attrs,
2421
+ children: []
2422
+ };
2423
+ stack.at(-1).children.push(node);
2424
+ if (match[4] !== "/") stack.push(node);
2425
+ }
2426
+ if (stack.length !== 1) throw Error(`SVG is malformed: <${stack.at(-1).name}> is not closed`);
2427
+ return root;
2428
+ }
2429
+ const IDENTITY = [
2430
+ 1,
2431
+ 0,
2432
+ 0,
2433
+ 1,
2434
+ 0,
2435
+ 0
2436
+ ];
2437
+ const multiply = (m, n) => [
2438
+ m[0] * n[0] + m[2] * n[1],
2439
+ m[1] * n[0] + m[3] * n[1],
2440
+ m[0] * n[2] + m[2] * n[3],
2441
+ m[1] * n[2] + m[3] * n[3],
2442
+ m[0] * n[4] + m[2] * n[5] + m[4],
2443
+ m[1] * n[4] + m[3] * n[5] + m[5]
2444
+ ];
2445
+ const apply$1 = (m, x, y) => [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
2446
+ const NUMBER = /[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/g;
2447
+ const numbers = (text) => (String(text ?? "").match(NUMBER) ?? []).map(Number);
2448
+ function parseTransform(text) {
2449
+ let matrix = IDENTITY;
2450
+ const source = String(text ?? "");
2451
+ const item = /(matrix|translate|scale|rotate|skewX|skewY)\s*\(([^)]*)\)/g;
2452
+ if (source.replace(item, "").replace(/[\s,]/g, "")) throw Error(`Unsupported SVG transform: ${source}`);
2453
+ for (let match = item.exec(source); match; match = item.exec(source)) {
2454
+ const v = numbers(match[2]);
2455
+ let next;
2456
+ if (match[1] === "matrix" && v.length === 6) next = v;
2457
+ else if (match[1] === "translate" && v.length >= 1) next = [
2458
+ 1,
2459
+ 0,
2460
+ 0,
2461
+ 1,
2462
+ v[0],
2463
+ v[1] ?? 0
2464
+ ];
2465
+ else if (match[1] === "scale" && v.length >= 1) next = [
2466
+ v[0],
2467
+ 0,
2468
+ 0,
2469
+ v[1] ?? v[0],
2470
+ 0,
2471
+ 0
2472
+ ];
2473
+ else if (match[1] === "rotate" && v.length >= 1) {
2474
+ const a = v[0] * Math.PI / 180, c = Math.cos(a), s = Math.sin(a);
2475
+ const turn = [
2476
+ c,
2477
+ s,
2478
+ -s,
2479
+ c,
2480
+ 0,
2481
+ 0
2482
+ ];
2483
+ next = v.length >= 3 ? multiply(multiply([
2484
+ 1,
2485
+ 0,
2486
+ 0,
2487
+ 1,
2488
+ v[1],
2489
+ v[2]
2490
+ ], turn), [
2491
+ 1,
2492
+ 0,
2493
+ 0,
2494
+ 1,
2495
+ -v[1],
2496
+ -v[2]
2497
+ ]) : turn;
2498
+ } else if (match[1] === "skewX" && v.length === 1) next = [
2499
+ 1,
2500
+ 0,
2501
+ Math.tan(v[0] * Math.PI / 180),
2502
+ 1,
2503
+ 0,
2504
+ 0
2505
+ ];
2506
+ else if (match[1] === "skewY" && v.length === 1) next = [
2507
+ 1,
2508
+ Math.tan(v[0] * Math.PI / 180),
2509
+ 0,
2510
+ 1,
2511
+ 0,
2512
+ 0
2513
+ ];
2514
+ else throw Error(`Unsupported SVG transform: ${match[0]}`);
2515
+ matrix = multiply(matrix, next);
2516
+ }
2517
+ return matrix;
2518
+ }
2519
+ function parseColor(value) {
2520
+ const text = String(value ?? "").trim().toLowerCase();
2521
+ if (!text || text === "none") return "none";
2522
+ if (text === "currentcolor") return "#000000";
2523
+ if (Object.hasOwn(NAMED_COLORS, text)) return NAMED_COLORS[text];
2524
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(text);
2525
+ if (hex) return hex[1].length === 3 ? `#${[...hex[1]].map((ch) => ch + ch).join("")}` : text;
2526
+ const rgb = /^rgba?\(([^)]*)\)$/.exec(text);
2527
+ if (rgb) {
2528
+ const parts = rgb[1].split(/[\s,/]+/).filter(Boolean);
2529
+ const channel = (part) => Math.max(0, Math.min(255, Math.round(part.endsWith("%") ? parseFloat(part) * 2.55 : parseFloat(part))));
2530
+ if (parts.length >= 3) return `#${parts.slice(0, 3).map((part) => channel(part).toString(16).padStart(2, "0")).join("")}`;
2531
+ }
2532
+ throw Error(`Unsupported SVG color: ${value}. Use #rrggbb, #rgb, rgb(), a basic color name, none, or a gradient id.`);
2533
+ }
2534
+ function styleOf(node, parent) {
2535
+ const own = { ...node.attrs };
2536
+ if (own.style) for (const rule of own.style.split(";")) {
2537
+ const at = rule.indexOf(":");
2538
+ if (at > 0) own[rule.slice(0, at).trim()] = rule.slice(at + 1).trim();
2539
+ }
2540
+ const pick = (name, fallback) => own[name] === void 0 || own[name] === "inherit" ? fallback : own[name];
2541
+ const number = (name, fallback) => {
2542
+ const value = pick(name, void 0);
2543
+ return value === void 0 ? fallback : Number.parseFloat(value);
2544
+ };
2545
+ return {
2546
+ fill: pick("fill", parent.fill),
2547
+ stroke: pick("stroke", parent.stroke),
2548
+ strokeWidth: number("stroke-width", parent.strokeWidth),
2549
+ fillOpacity: number("fill-opacity", parent.fillOpacity),
2550
+ strokeOpacity: number("stroke-opacity", parent.strokeOpacity),
2551
+ opacity: parent.opacity * number("opacity", 1),
2552
+ display: own.display === "none" || own.visibility === "hidden" ? "none" : "inline"
2553
+ };
2554
+ }
2555
+ function arcToCubics(x1, y1, rx, ry, rotation, large, sweep, x2, y2) {
2556
+ if (x1 === x2 && y1 === y2) return [];
2557
+ rx = Math.abs(rx);
2558
+ ry = Math.abs(ry);
2559
+ if (!rx || !ry) return [[
2560
+ x1,
2561
+ y1,
2562
+ x2,
2563
+ y2,
2564
+ x2,
2565
+ y2
2566
+ ]];
2567
+ const phi = rotation * Math.PI / 180, cos = Math.cos(phi), sin = Math.sin(phi);
2568
+ const dx = (x1 - x2) / 2, dy = (y1 - y2) / 2;
2569
+ const x1p = cos * dx + sin * dy, y1p = -sin * dx + cos * dy;
2570
+ const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
2571
+ if (lambda > 1) {
2572
+ rx *= Math.sqrt(lambda);
2573
+ ry *= Math.sqrt(lambda);
2574
+ }
2575
+ const sign = large === sweep ? -1 : 1;
2576
+ const numerator = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
2577
+ const denominator = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
2578
+ const factor = sign * Math.sqrt(Math.max(0, numerator / denominator));
2579
+ const cxp = factor * rx * y1p / ry, cyp = -factor * ry * x1p / rx;
2580
+ const cx = cos * cxp - sin * cyp + (x1 + x2) / 2, cy = sin * cxp + cos * cyp + (y1 + y2) / 2;
2581
+ const angle = (ux, uy, vx, vy) => Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
2582
+ const theta = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
2583
+ let delta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
2584
+ if (!sweep && delta > 0) delta -= 2 * Math.PI;
2585
+ if (sweep && delta < 0) delta += 2 * Math.PI;
2586
+ const count = Math.max(1, Math.ceil(Math.abs(delta) / (Math.PI / 2) - 1e-9));
2587
+ const step = delta / count, k = 4 / 3 * Math.tan(step / 4);
2588
+ const out = [];
2589
+ for (let index = 0; index < count; index++) {
2590
+ const a1 = theta + index * step, a2 = a1 + step;
2591
+ const point = (a, scale) => {
2592
+ const px = rx * Math.cos(a), py = ry * Math.sin(a);
2593
+ const tx = -rx * Math.sin(a) * scale, ty = ry * Math.cos(a) * scale;
2594
+ return [
2595
+ cos * px - sin * py + cx,
2596
+ sin * px + cos * py + cy,
2597
+ cos * tx - sin * ty,
2598
+ sin * tx + cos * ty
2599
+ ];
2600
+ };
2601
+ const p1 = point(a1, k), p2 = point(a2, k);
2602
+ out.push([
2603
+ p1[0] + p1[2],
2604
+ p1[1] + p1[3],
2605
+ p2[0] - p2[2],
2606
+ p2[1] - p2[3],
2607
+ p2[0],
2608
+ p2[1]
2609
+ ]);
2610
+ }
2611
+ return out;
2612
+ }
2613
+ function parsePathData(data) {
2614
+ const source = String(data ?? "");
2615
+ const subpaths = [];
2616
+ let index = 0, command = "", current, x = 0, y = 0, startX = 0, startY = 0, lastControl, lastCommand = "";
2617
+ const skip = () => {
2618
+ while (index < source.length && /[\s,]/.test(source[index])) index++;
2619
+ };
2620
+ const readNumber = () => {
2621
+ skip();
2622
+ NUMBER.lastIndex = index;
2623
+ const match = NUMBER.exec(source);
2624
+ if (!match || match.index !== index) throw Error(`Malformed SVG path data near "${source.slice(index, index + 12)}"`);
2625
+ index = NUMBER.lastIndex;
2626
+ return Number(match[0]);
2627
+ };
2628
+ const readFlag = () => {
2629
+ skip();
2630
+ const ch = source[index];
2631
+ if (ch !== "0" && ch !== "1") throw Error("Malformed SVG arc flag in path data");
2632
+ index++;
2633
+ return ch === "1";
2634
+ };
2635
+ const begin = (px, py) => {
2636
+ current = {
2637
+ start: [px, py],
2638
+ curves: [],
2639
+ closed: false,
2640
+ straight: true
2641
+ };
2642
+ subpaths.push(current);
2643
+ };
2644
+ const line = (px, py) => {
2645
+ current.curves.push([
2646
+ x,
2647
+ y,
2648
+ px,
2649
+ py,
2650
+ px,
2651
+ py
2652
+ ]);
2653
+ x = px;
2654
+ y = py;
2655
+ };
2656
+ const cubic = (c1x, c1y, c2x, c2y, px, py) => {
2657
+ current.curves.push([
2658
+ c1x,
2659
+ c1y,
2660
+ c2x,
2661
+ c2y,
2662
+ px,
2663
+ py
2664
+ ]);
2665
+ current.straight = false;
2666
+ lastControl = [c2x, c2y];
2667
+ x = px;
2668
+ y = py;
2669
+ };
2670
+ for (;;) {
2671
+ skip();
2672
+ if (index >= source.length) break;
2673
+ if (/[MmLlHhVvCcSsQqTtAaZz]/.test(source[index])) command = source[index++];
2674
+ else if (!command) throw Error("SVG path data must start with a command");
2675
+ else if (command === "M") command = "L";
2676
+ else if (command === "m") command = "l";
2677
+ else if (command === "Z" || command === "z") throw Error("Unexpected number after Z in SVG path data");
2678
+ const relative = command === command.toLowerCase();
2679
+ const upper = command.toUpperCase();
2680
+ const ox = relative ? x : 0, oy = relative ? y : 0;
2681
+ if (upper === "M") {
2682
+ const px = readNumber() + ox, py = readNumber() + oy;
2683
+ x = startX = px;
2684
+ y = startY = py;
2685
+ begin(px, py);
2686
+ lastControl = void 0;
2687
+ } else if (upper === "Z") {
2688
+ if (current && !current.closed) {
2689
+ if (x !== startX || y !== startY) line(startX, startY);
2690
+ current.closed = true;
2691
+ }
2692
+ x = startX;
2693
+ y = startY;
2694
+ lastControl = void 0;
2695
+ current = void 0;
2696
+ } else {
2697
+ if (!current) {
2698
+ begin(x, y);
2699
+ startX = x;
2700
+ startY = y;
2701
+ }
2702
+ if (upper === "L") line(readNumber() + ox, readNumber() + oy);
2703
+ else if (upper === "H") line(readNumber() + ox, y);
2704
+ else if (upper === "V") line(x, readNumber() + oy);
2705
+ else if (upper === "C") cubic(readNumber() + ox, readNumber() + oy, readNumber() + ox, readNumber() + oy, readNumber() + ox, readNumber() + oy);
2706
+ else if (upper === "S") {
2707
+ const [rx, ry] = lastControl && lastCommand && "CScs".includes(lastCommand) ? [2 * x - lastControl[0], 2 * y - lastControl[1]] : [x, y];
2708
+ cubic(rx, ry, readNumber() + ox, readNumber() + oy, readNumber() + ox, readNumber() + oy);
2709
+ } else if (upper === "Q" || upper === "T") {
2710
+ let qx, qy;
2711
+ if (upper === "Q") {
2712
+ qx = readNumber() + ox;
2713
+ qy = readNumber() + oy;
2714
+ } else [qx, qy] = lastControl && lastCommand && "QTqt".includes(lastCommand) ? [2 * x - lastControl[0], 2 * y - lastControl[1]] : [x, y];
2715
+ const px = readNumber() + ox, py = readNumber() + oy;
2716
+ const from = [x, y];
2717
+ cubic(from[0] + 2 / 3 * (qx - from[0]), from[1] + 2 / 3 * (qy - from[1]), px + 2 / 3 * (qx - px), py + 2 / 3 * (qy - py), px, py);
2718
+ lastControl = [qx, qy];
2719
+ } else if (upper === "A") {
2720
+ const rx = readNumber(), ry = readNumber(), rotation = readNumber(), large = readFlag(), sweep = readFlag();
2721
+ const px = readNumber() + ox, py = readNumber() + oy;
2722
+ const pieces = arcToCubics(x, y, rx, ry, rotation, large, sweep, px, py);
2723
+ for (const piece of pieces) cubic(...piece);
2724
+ if (!pieces.length) {
2725
+ x = px;
2726
+ y = py;
2727
+ }
2728
+ lastControl = void 0;
2729
+ } else throw Error(`Unsupported SVG path command: ${command}`);
2730
+ }
2731
+ lastCommand = command;
2732
+ }
2733
+ return subpaths.filter((subpath) => subpath.curves.length > 0);
2734
+ }
2735
+ const ellipsePath = (cx, cy, rx, ry) => {
2736
+ const k = .5522847498;
2737
+ return {
2738
+ start: [cx + rx, cy],
2739
+ closed: true,
2740
+ straight: false,
2741
+ curves: [
2742
+ [
2743
+ cx + rx,
2744
+ cy + ry * k,
2745
+ cx + rx * k,
2746
+ cy + ry,
2747
+ cx,
2748
+ cy + ry
2749
+ ],
2750
+ [
2751
+ cx - rx * k,
2752
+ cy + ry,
2753
+ cx - rx,
2754
+ cy + ry * k,
2755
+ cx - rx,
2756
+ cy
2757
+ ],
2758
+ [
2759
+ cx - rx,
2760
+ cy - ry * k,
2761
+ cx - rx * k,
2762
+ cy - ry,
2763
+ cx,
2764
+ cy - ry
2765
+ ],
2766
+ [
2767
+ cx + rx * k,
2768
+ cy - ry,
2769
+ cx + rx,
2770
+ cy - ry * k,
2771
+ cx + rx,
2772
+ cy
2773
+ ]
2774
+ ]
2775
+ };
2776
+ };
2777
+ const polylinePath = (points, closed) => {
2778
+ const path = {
2779
+ start: points[0],
2780
+ curves: [],
2781
+ closed,
2782
+ straight: true
2783
+ };
2784
+ let [x, y] = points[0];
2785
+ for (const [px, py] of points.slice(1)) {
2786
+ path.curves.push([
2787
+ x,
2788
+ y,
2789
+ px,
2790
+ py,
2791
+ px,
2792
+ py
2793
+ ]);
2794
+ x = px;
2795
+ y = py;
2796
+ }
2797
+ if (closed && (x !== points[0][0] || y !== points[0][1])) path.curves.push([
2798
+ x,
2799
+ y,
2800
+ points[0][0],
2801
+ points[0][1],
2802
+ points[0][0],
2803
+ points[0][1]
2804
+ ]);
2805
+ return path;
2806
+ };
2807
+ const roundedRectPath = (x, y, w, h, rx, ry) => {
2808
+ rx = Math.min(rx, w / 2);
2809
+ ry = Math.min(ry, h / 2);
2810
+ const k = .5522847498;
2811
+ const curves = [];
2812
+ let cx = x + rx, cy = y;
2813
+ const go = (...piece) => {
2814
+ curves.push(piece);
2815
+ cx = piece[4];
2816
+ cy = piece[5];
2817
+ };
2818
+ go(cx, cy, x + w - rx, y, x + w - rx, y);
2819
+ go(x + w - rx + rx * k, y, x + w, y + ry - ry * k, x + w, y + ry);
2820
+ go(x + w, y + ry, x + w, y + h - ry, x + w, y + h - ry);
2821
+ go(x + w, y + h - ry + ry * k, x + w - rx + rx * k, y + h, x + w - rx, y + h);
2822
+ go(x + w - rx, y + h, x + rx, y + h, x + rx, y + h);
2823
+ go(x + rx - rx * k, y + h, x, y + h - ry + ry * k, x, y + h - ry);
2824
+ go(x, y + h - ry, x, y + ry, x, y + ry);
2825
+ go(x, y + ry - ry * k, x + rx - rx * k, y, x + rx, y);
2826
+ return {
2827
+ start: [x + rx, y],
2828
+ curves,
2829
+ closed: true,
2830
+ straight: false
2831
+ };
2832
+ };
2833
+ function svgToSketchCommands(text, { width, height, layer, existingIds = [] }) {
2834
+ if (typeof text !== "string" || !text.trim()) throw Error("svg must be a non-empty SVG document");
2835
+ if (text.length > 2e5) throw Error(`svg is too large: ${text.length} characters, limit ${MAX_SVG_CHARACTERS}. Split it across several svg commands.`);
2836
+ const root = parseXml(text).children.find((node) => node.name === "svg");
2837
+ if (!root) throw Error("svg must contain an <svg> element");
2838
+ const box = numbers(root.attrs.viewBox);
2839
+ const viewWidth = box.length === 4 ? box[2] : Number.parseFloat(root.attrs.width);
2840
+ const viewHeight = box.length === 4 ? box[3] : Number.parseFloat(root.attrs.height);
2841
+ const originX = box.length === 4 ? box[0] : 0, originY = box.length === 4 ? box[1] : 0;
2842
+ if (!(viewWidth > 0 && viewHeight > 0)) throw Error("<svg> needs a viewBox=\"0 0 W H\" (or width and height)");
2843
+ if (Math.abs(viewWidth / viewHeight - width / height) > .01 * (width / height)) throw Error(`SVG viewBox ${viewWidth}x${viewHeight} does not match the ${width}x${height} sketch ratio. Use viewBox="0 0 ${width} ${height}".`);
2844
+ const byId = /* @__PURE__ */ new Map();
2845
+ const index = (node) => {
2846
+ if (node.attrs.id) byId.set(node.attrs.id, node);
2847
+ node.children.forEach(index);
2848
+ };
2849
+ index(root);
2850
+ const warnings = /* @__PURE__ */ new Set();
2851
+ const used = new Set(existingIds);
2852
+ let counter = 0, clamped = 0;
2853
+ const nextId = (suffix) => {
2854
+ for (;;) {
2855
+ counter++;
2856
+ const id = `svg-${counter}${suffix}`;
2857
+ if (!used.has(id)) {
2858
+ used.add(id);
2859
+ return id;
2860
+ }
2861
+ }
2862
+ };
2863
+ const scale = width / viewWidth;
2864
+ const commands = [];
2865
+ const clamp = (value) => {
2866
+ if (value < 0 || value > 1) clamped++;
2867
+ return Math.max(0, Math.min(1, value));
2868
+ };
2869
+ const norm = (matrix, px, py) => {
2870
+ const [tx, ty] = apply$1(matrix, px, py);
2871
+ return {
2872
+ x: clamp((tx - originX) / viewWidth),
2873
+ y: clamp((ty - originY) / viewHeight)
2874
+ };
2875
+ };
2876
+ const gradientColor = (reference) => {
2877
+ const target = byId.get(/^url\(\s*['"]?#([^)'"]+)['"]?\s*\)/.exec(reference)?.[1] ?? "");
2878
+ if (!target || !/Gradient$/.test(target.name)) throw Error(`SVG paint ${reference} does not reference a gradient defined in the same SVG`);
2879
+ const stops = target.children.filter((child) => child.name === "stop");
2880
+ if (!stops.length) throw Error(`Gradient ${target.attrs.id} has no stops`);
2881
+ const channels = stops.map((stop) => parseColor(stop.attrs["stop-color"] ?? /stop-color\s*:\s*([^;]+)/.exec(stop.attrs.style ?? "")?.[1] ?? "#000000")).filter((color) => color !== "none").map((color) => [
2882
+ 1,
2883
+ 3,
2884
+ 5
2885
+ ].map((offset) => Number.parseInt(color.slice(offset, offset + 2), 16)));
2886
+ const mean = [
2887
+ 0,
2888
+ 1,
2889
+ 2
2890
+ ].map((channel) => Math.round(channels.reduce((sum, item) => sum + item[channel], 0) / channels.length));
2891
+ warnings.add(`Gradient ${target.attrs.id} was flattened to its average color; stack several bands for a real gradient.`);
2892
+ return `#${mean.map((value) => value.toString(16).padStart(2, "0")).join("")}`;
2893
+ };
2894
+ const paint = (value) => String(value).startsWith("url(") ? gradientColor(value) : parseColor(value);
2895
+ const emit = (subpath, matrix, style, id) => {
2896
+ [subpath.start, ...subpath.curves.flatMap((curve) => [
2897
+ [curve[0], curve[1]],
2898
+ [curve[2], curve[3]],
2899
+ [curve[4], curve[5]]
2900
+ ])];
2901
+ const fill = style.fill === "none" ? "none" : paint(style.fill);
2902
+ const stroke = style.stroke === "none" ? "none" : paint(style.stroke);
2903
+ const common = layer === void 0 ? {} : { layer };
2904
+ const strokeWidth = Math.max(1, Math.min(256, (style.strokeWidth || 1) * scale * Math.hypot(matrix[0], matrix[1])));
2905
+ const segments = subpath.curves.length;
2906
+ const closed = subpath.closed;
2907
+ const straightPoints = [subpath.start, ...subpath.curves.map((curve) => [curve[4], curve[5]])];
2908
+ const build = (color, opacity, filled, suffix) => {
2909
+ if (subpath.straight && straightPoints.length >= 2) {
2910
+ const vertices = closed ? straightPoints.slice(0, -1) : straightPoints;
2911
+ if (filled || closed) {
2912
+ if (vertices.length >= 3) return [{
2913
+ op: "stroke",
2914
+ ...common,
2915
+ id: nextId(suffix),
2916
+ shape: "polygon",
2917
+ color,
2918
+ opacity,
2919
+ fill: filled,
2920
+ width: strokeWidth,
2921
+ points: vertices.map(([px, py]) => norm(matrix, px, py))
2922
+ }];
2923
+ }
2924
+ if (vertices.length === 2 && !filled) return [{
2925
+ op: "stroke",
2926
+ ...common,
2927
+ id: nextId(suffix),
2928
+ shape: "line",
2929
+ color,
2930
+ opacity,
2931
+ fill: false,
2932
+ width: strokeWidth,
2933
+ points: vertices.map(([px, py]) => norm(matrix, px, py))
2934
+ }];
2935
+ if (!filled) {
2936
+ const pieces = [];
2937
+ for (let from = 0; from < vertices.length - 1; from += 1900) pieces.push({
2938
+ op: "stroke",
2939
+ ...common,
2940
+ id: nextId(suffix),
2941
+ shape: "pen",
2942
+ color,
2943
+ opacity,
2944
+ fill: false,
2945
+ width: strokeWidth,
2946
+ points: vertices.slice(from, from + 1901).map(([px, py]) => norm(matrix, px, py))
2947
+ });
2948
+ return pieces;
2949
+ }
2950
+ return [];
2951
+ }
2952
+ if (filled && segments > MAX_BEZIER_SEGMENTS) {
2953
+ warnings.add(`A filled path with ${segments} curve segments was flattened to a polygon (more than ${MAX_BEZIER_SEGMENTS} segments cannot stay a Bezier).`);
2954
+ const flat = [subpath.start];
2955
+ let [cx, cy] = subpath.start;
2956
+ for (const [a, b, c1, d, e, f] of subpath.curves) {
2957
+ for (let step = 1; step <= FLATTEN_STEPS; step++) {
2958
+ const t = step / FLATTEN_STEPS, u = 1 - t;
2959
+ flat.push([u * u * u * cx + 3 * u * u * t * a + 3 * u * t * t * c1 + t * t * t * e, u * u * u * cy + 3 * u * u * t * b + 3 * u * t * t * d + t * t * t * f]);
2960
+ }
2961
+ cx = e;
2962
+ cy = f;
2963
+ }
2964
+ if (flat.length > 2e3) throw Error("A filled path is too complex to import; simplify it or split it into several paths.");
2965
+ return [{
2966
+ op: "stroke",
2967
+ ...common,
2968
+ id: nextId(suffix),
2969
+ shape: "polygon",
2970
+ color,
2971
+ opacity,
2972
+ fill: true,
2973
+ width: strokeWidth,
2974
+ points: flat.map(([px, py]) => norm(matrix, px, py))
2975
+ }];
2976
+ }
2977
+ const pieces = [];
2978
+ for (let from = 0; from < segments; from += MAX_BEZIER_SEGMENTS) {
2979
+ const chunk = subpath.curves.slice(from, from + MAX_BEZIER_SEGMENTS);
2980
+ const chunkPoints = [from === 0 ? subpath.start : [subpath.curves[from - 1][4], subpath.curves[from - 1][5]], ...chunk.flatMap((curve) => [
2981
+ [curve[0], curve[1]],
2982
+ [curve[2], curve[3]],
2983
+ [curve[4], curve[5]]
2984
+ ])];
2985
+ pieces.push({
2986
+ op: "stroke",
2987
+ ...common,
2988
+ id: nextId(suffix),
2989
+ shape: "bezier",
2990
+ color,
2991
+ opacity,
2992
+ fill: filled,
2993
+ width: strokeWidth,
2994
+ points: chunkPoints.map(([px, py]) => norm(matrix, px, py))
2995
+ });
2996
+ }
2997
+ return pieces;
2998
+ };
2999
+ if (fill !== "none" && style.fillOpacity * style.opacity > 0) commands.push(...build(fill, style.fillOpacity * style.opacity, true, ""));
3000
+ if (stroke !== "none" && style.strokeOpacity * style.opacity > 0 && style.strokeWidth > 0) commands.push(...build(stroke, style.strokeOpacity * style.opacity, false, "-line"));
3001
+ };
3002
+ const walk = (node, matrix, parentStyle, depth) => {
3003
+ if (depth > 24) throw Error("SVG is nested too deeply");
3004
+ if (UNSUPPORTED.has(node.name)) throw Error(`Unsupported SVG element <${node.name}>. Draw with path, rect, circle, ellipse, line, polyline, polygon, g and use; add text with a native text command.`);
3005
+ if (IGNORED.has(node.name)) return;
3006
+ for (const name of [
3007
+ "clip-path",
3008
+ "mask",
3009
+ "filter"
3010
+ ]) if (node.attrs[name] && node.attrs[name] !== "none") throw Error(`Unsupported SVG attribute ${name}. Bake it into the shapes instead.`);
3011
+ const style = styleOf(node, parentStyle);
3012
+ if (style.display === "none") return;
3013
+ const local = multiply(matrix, parseTransform(node.attrs.transform));
3014
+ const n = (name) => Number.parseFloat(node.attrs[name] ?? 0);
3015
+ const finish = (subpaths) => subpaths.forEach((subpath) => emit(subpath, local, style, node.attrs.id));
3016
+ switch (node.name) {
3017
+ case "svg":
3018
+ case "g":
3019
+ case "a":
3020
+ for (const child of node.children) walk(child, local, style, depth + 1);
3021
+ return;
3022
+ case "use": {
3023
+ const href = node.attrs.href ?? node.attrs["xlink:href"];
3024
+ const target = href?.startsWith("#") ? byId.get(href.slice(1)) : void 0;
3025
+ if (!target) throw Error(`<use> must reference an element in the same SVG (${href ?? "missing href"})`);
3026
+ walk(target, multiply(local, [
3027
+ 1,
3028
+ 0,
3029
+ 0,
3030
+ 1,
3031
+ n("x"),
3032
+ n("y")
3033
+ ]), { ...style }, depth + 1);
3034
+ return;
3035
+ }
3036
+ case "path":
3037
+ finish(parsePathData(node.attrs.d));
3038
+ return;
3039
+ case "rect": {
3040
+ const w = n("width"), h = n("height");
3041
+ if (!(w > 0 && h > 0)) return;
3042
+ const rx = node.attrs.rx !== void 0 ? n("rx") : node.attrs.ry !== void 0 ? n("ry") : 0;
3043
+ const ry = node.attrs.ry !== void 0 ? n("ry") : rx;
3044
+ finish([rx > 0 || ry > 0 ? roundedRectPath(n("x"), n("y"), w, h, rx, ry) : polylinePath([
3045
+ [n("x"), n("y")],
3046
+ [n("x") + w, n("y")],
3047
+ [n("x") + w, n("y") + h],
3048
+ [n("x"), n("y") + h]
3049
+ ], true)]);
3050
+ return;
3051
+ }
3052
+ case "circle":
3053
+ if (n("r") > 0) finish([ellipsePath(n("cx"), n("cy"), n("r"), n("r"))]);
3054
+ return;
3055
+ case "ellipse":
3056
+ if (n("rx") > 0 && n("ry") > 0) finish([ellipsePath(n("cx"), n("cy"), n("rx"), n("ry"))]);
3057
+ return;
3058
+ case "line":
3059
+ finish([polylinePath([[n("x1"), n("y1")], [n("x2"), n("y2")]], false)]);
3060
+ return;
3061
+ case "polyline":
3062
+ case "polygon": {
3063
+ const values = numbers(node.attrs.points);
3064
+ if (values.length < 4 || values.length % 2) throw Error(`<${node.name}> needs an even number of coordinates, at least two points`);
3065
+ const pts = [];
3066
+ for (let at = 0; at < values.length; at += 2) pts.push([values[at], values[at + 1]]);
3067
+ finish([polylinePath(pts, node.name === "polygon")]);
3068
+ return;
3069
+ }
3070
+ default: throw Error(`Unsupported SVG element <${node.name}>`);
3071
+ }
3072
+ };
3073
+ walk(root, IDENTITY, {
3074
+ fill: "#000000",
3075
+ stroke: "none",
3076
+ strokeWidth: 1,
3077
+ fillOpacity: 1,
3078
+ strokeOpacity: 1,
3079
+ opacity: 1
3080
+ }, 0);
3081
+ if (!commands.length) throw Error("The SVG contained no drawable shapes");
3082
+ return {
3083
+ commands,
3084
+ warnings: [...warnings],
3085
+ clamped
3086
+ };
3087
+ }
3088
+ function expandSvgCommands(commands, doc, report) {
3089
+ if (!commands.some((command) => command?.op === "svg")) return commands;
3090
+ const width = doc.width ?? 1024, height = doc.height ?? 1024;
3091
+ const ids = new Set(doc.layers.flatMap((item) => item.strokes.map((stroke) => stroke.id).filter(Boolean)));
3092
+ const out = [];
3093
+ let clamped = 0;
3094
+ for (const command of commands) {
3095
+ if (command?.op !== "svg") {
3096
+ out.push(command);
3097
+ if (command?.id) ids.add(command.id);
3098
+ continue;
3099
+ }
3100
+ const result = svgToSketchCommands(command.svg, {
3101
+ width,
3102
+ height,
3103
+ layer: command.layer,
3104
+ existingIds: ids
3105
+ });
3106
+ result.commands.forEach((item) => ids.add(item.id));
3107
+ out.push(...result.commands);
3108
+ clamped += result.clamped;
3109
+ if (report) report.warnings = [.../* @__PURE__ */ new Set([...report.warnings ?? [], ...result.warnings])];
3110
+ }
3111
+ if (report && clamped) report.clampedPoints = (report.clampedPoints ?? 0) + clamped;
3112
+ return out;
3113
+ }
3114
+ //#endregion
3115
+ //#region src/sketch-commands.js
3116
+ const MAX_SKETCH_POINTS = 2e5;
2324
3117
  const SKETCH_COMMAND_HELP = {
2325
3118
  coordinates: "Assign short meaningful stroke id values for later edits. Text uses two opposite box corners and text content; width is font size in pixels, automatic fitting within the box. Arrow uses two endpoints. Normalized x/y in [0,1]; width is canvas pixels. Read documentId and revision before editing.",
2326
3119
  shapes: "line: exactly two endpoints; rectangle/circle (ellipse alias accepted): exactly two opposite bounding-box corners (circle draws an ellipse within that box); polygon: three or more vertices, closed automatically; pen: ordered path points. bezier: start point, then groups of control1/control2/end; use 4 points for one cubic curve, max 64 segments. Prefer bezier for smooth designed curves instead of many pen samples. fill:true closes and fills the curve. fill:true fills rectangle/circle/polygon. Layers and strokes paint in list order, later ones on top. All commands needed for drawing are described here; no source-code search is required.",
@@ -2330,19 +3123,22 @@ window.__ModuleLoader__.load({
2330
3123
  layer: "Add: {op:\"layer\",action:\"add\",value:\"name\"}; optional id is the NEW unique integer ID, otherwise allocated automatically. after is the existing insertion anchor, defaults to active layer. Other actions: {op:\"layer\",action:\"select|rename|visible|duplicate|up|down|delete|clear\",id:1,value:\"name\"}; id targets an existing layer.",
2331
3124
  curve: "Prefer {op:\"stroke\",shape:\"bezier\",start:{x:0,y:0},segments:[{control1:{x:0.2,y:0},control2:{x:0.8,y:1},end:{x:1,y:1}}],color:\"#123456\"}. Each segment has exactly two controls and an endpoint; no point counting required. Legacy points arrays still accepted. Do not provide both forms.",
2332
3125
  object: "{op:\"object\",layer:1,id:\"title\",action:\"update|duplicate|delete\",patch:{color:\"#0088ff\",text:\"Title\"},transform:{dx:0.05,dy:0,scaleX:1,scaleY:1}}. All patch and transform fields optional. Inspect returns object IDs and bounds. Prefer targeted edits over redrawing layers.",
2333
- resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}"
3126
+ resize: "{op:\"resize\",ratio:\"1:1|4:3|3:4|16:9|9:16\"}",
3127
+ svg: "{op:\"svg\",svg:\"<svg viewBox=\\\"0 0 1024 768\\\">...</svg>\",layer:1}. Fastest way to draw: send a whole scene as SVG (path with M L H V C S Q T A Z, rect, circle, ellipse, line, polyline, polygon, g, use, transform, fill/stroke/opacity, style=\"...\"); it becomes native editable strokes. The viewBox must match the canvas size (use the width and height from inspect; 4:3 is 1024x768). Gradients are flattened to their average color, so build glows and skies from stacked bands; text, image, filter, clipPath, mask, pattern and CSS classes are refused. Coordinates outside the canvas are clamped. One svg command may hold up to 200000 characters."
2334
3128
  },
2335
3129
  limits: {
2336
3130
  strokes: MAX_SKETCH_STROKES,
2337
3131
  pointsPerStroke: MAX_STROKE_POINTS,
2338
- pointsTotal: 2e5,
2339
- commandsPerBatch: 256
3132
+ pointsTotal: MAX_SKETCH_POINTS,
3133
+ commandsPerBatch: 256,
3134
+ svgCharacters: MAX_SVG_CHARACTERS
2340
3135
  }
2341
3136
  };
2342
3137
  const finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
2343
- function applySketchCommands(source, commands) {
3138
+ function applySketchCommands(source, commands, report) {
2344
3139
  if (!Array.isArray(commands) || !commands.length || commands.length > 256) throw Error("Expected 1–256 commands");
2345
3140
  let doc = identifyObjects(source);
3141
+ commands = expandSvgCommands(commands, doc, report);
2346
3142
  for (let command of commands) {
2347
3143
  if (!command || typeof command !== "object") throw Error("Invalid command");
2348
3144
  if (command.op === "stroke") command = expandSketchPreset(command);
@@ -2501,7 +3297,9 @@ window.__ModuleLoader__.load({
2501
3297
  } : item)
2502
3298
  };
2503
3299
  }
2504
- if (strokeCount(doc) > 2e3 || doc.layers.reduce((n, l) => n + l.strokes.reduce((m, s) => m + s.points.length, 0), 0) > 2e5) throw Error("Sketch resource budget exceeded");
3300
+ const strokes = strokeCount(doc), points = doc.layers.reduce((n, l) => n + l.strokes.reduce((m, s) => m + s.points.length, 0), 0);
3301
+ if (strokes > 2e3) throw Error(`Sketch would hold ${strokes} strokes; the limit is ${MAX_SKETCH_STROKES}. Merge shapes or drop small details, then send this batch again`);
3302
+ if (points > 2e5) throw Error(`Sketch would hold ${points} points; the limit is ${MAX_SKETCH_POINTS}. Use fewer or simpler paths, then send this batch again`);
2505
3303
  return doc;
2506
3304
  }
2507
3305
  function createSketchCommandSession(adapter) {
@@ -2544,11 +3342,12 @@ window.__ModuleLoader__.load({
2544
3342
  }
2545
3343
  if (request.revision !== current.revision || adapter.busy()) throw Error("Sketch changed or is being edited; inspect again");
2546
3344
  let changedObjects;
3345
+ const report = {};
2547
3346
  if (request.action === "apply") {
2548
3347
  const before = adapter.document();
2549
3348
  let next;
2550
3349
  try {
2551
- next = applySketchCommands(before, request.commands);
3350
+ next = applySketchCommands(before, request.commands, report);
2552
3351
  } catch (cause) {
2553
3352
  const error = new Error(`${cause.message} Correct the batch and retry with the same runId and revision; nothing was applied.`, { cause });
2554
3353
  error.code = "SKETCH_INVALID_BATCH";
@@ -2574,7 +3373,9 @@ window.__ModuleLoader__.load({
2574
3373
  ...changedObjects ? {
2575
3374
  changedObjects: changedObjects.slice(0, 100),
2576
3375
  changedObjectCount: changedObjects.length
2577
- } : {}
3376
+ } : {},
3377
+ ...report?.warnings?.length ? { warnings: report.warnings } : {},
3378
+ ...report?.clampedPoints ? { clampedPoints: report.clampedPoints } : {}
2578
3379
  };
2579
3380
  completed.set(key, {
2580
3381
  fingerprint,
@@ -5847,6 +6648,10 @@ window.__ModuleLoader__.load({
5847
6648
  .codexSubscriptionRecover .codexSubscriptionError{flex:1}
5848
6649
  .codexSubscriptionRecover button{flex:0 0 auto}
5849
6650
  .codexSubscriptionDiagnostics{color:var(--dsw-alias-label-secondary)}
6651
+ .codexSubscriptionDiagnostics ul{margin:0;padding-left:18px;font-size:13px;line-height:20px}
6652
+ .codexSubscriptionDiagnostics li+li{margin-top:6px}
6653
+ .codexSubscriptionDiagnostics li strong{font-weight:600;color:var(--dsw-alias-label-primary)}
6654
+ .codexSubscriptionDiagnostics [role=status]>p{margin:0;font-size:13px}
5850
6655
  .codexSubscriptionDiagnostics pre{max-height:240px;margin:0;padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;font:11px/17px ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--dsw-alias-label-secondary)}
5851
6656
  .codexSubscriptionLink{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:32px;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;text-decoration:none;white-space:nowrap}
5852
6657
  .codexSubscriptionLink:hover{background:var(--dsw-alias-bg-module-platform)}
@@ -7676,7 +8481,7 @@ window.__ModuleLoader__.load({
7676
8481
  }
7677
8482
  //#endregion
7678
8483
  //#region src/version.js
7679
- const PACKAGE_VERSION = "2.3.0";
8484
+ const PACKAGE_VERSION = "2.4.1";
7680
8485
  //#endregion
7681
8486
  //#region src/client-recovery.js
7682
8487
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -9955,7 +10760,9 @@ window.__ModuleLoader__.load({
9955
10760
  return {
9956
10761
  format: "compact-1",
9957
10762
  plugin: report.version,
9958
- dsh: report.client?.version,
10763
+ dsh: report.runtime?.dsh,
10764
+ ...report.client?.version && report.client.version !== report.version ? { pluginClient: report.client.version } : {},
10765
+ platform: report.runtime ? `${report.runtime.platform}-${report.runtime.arch}` : void 0,
9959
10766
  generatedAt: report.generatedAt,
9960
10767
  account: report.account?.status,
9961
10768
  catalog: report.catalog ? {
package/lib/index.js CHANGED
@@ -1205,13 +1205,18 @@ const SUPPORTED_RUNTIME_VERSIONS = Object.freeze([
1205
1205
  ]);
1206
1206
  const require = createRequire(import.meta.url);
1207
1207
  const execute = promisify(execFile);
1208
+ /** The DSH release this plugin is loaded into, read from the host's own LLM package. */
1209
+ function hostDshVersion(resolve = require.resolve, entry = process.argv[1]) {
1210
+ const attempts = [() => resolve("@deepseek-ai/dsh-llm/package.json")];
1211
+ if (typeof entry === "string" && entry) attempts.push(() => require.resolve("@deepseek-ai/dsh-llm/package.json", { paths: [dirname(entry)] }));
1212
+ for (const attempt of attempts) try {
1213
+ const { version } = JSON.parse(readFileSync(attempt(), "utf8"));
1214
+ if (typeof version === "string" && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version)) return version;
1215
+ } catch {}
1216
+ }
1208
1217
  function matchingSubagentRuntimeVersion(resolve = require.resolve) {
1209
- try {
1210
- const host = JSON.parse(readFileSync(resolve("@deepseek-ai/dsh-llm/package.json"), "utf8"));
1211
- return SUPPORTED_RUNTIME_VERSIONS.includes(host.version) ? host.version : void 0;
1212
- } catch {
1213
- return;
1214
- }
1218
+ const version = hostDshVersion(resolve, resolve === require.resolve ? process.argv[1] : "");
1219
+ return SUPPORTED_RUNTIME_VERSIONS.includes(version) ? version : void 0;
1215
1220
  }
1216
1221
  function inspectSubagentRuntime(resolve = require.resolve) {
1217
1222
  try {
@@ -1799,7 +1804,8 @@ const sketchCommandArray = {
1799
1804
  "stroke",
1800
1805
  "object",
1801
1806
  "layer",
1802
- "resize"
1807
+ "resize",
1808
+ "svg"
1803
1809
  ]
1804
1810
  },
1805
1811
  id: {
@@ -1863,6 +1869,10 @@ const sketchCommandArray = {
1863
1869
  "clear"
1864
1870
  ]
1865
1871
  },
1872
+ svg: {
1873
+ type: "string",
1874
+ description: "op svg only: a complete <svg viewBox=\"0 0 W H\"> document (W and H equal to the canvas width and height from inspect). Imported as native editable strokes; the fastest way to draw a whole scene. Optional layer sets the target layer."
1875
+ },
1866
1876
  value: string,
1867
1877
  ratio: {
1868
1878
  type: "string",
@@ -1888,7 +1898,7 @@ const sketchCommandArray = {
1888
1898
  function createSketchAgentTool(bridge, attachments) {
1889
1899
  return defineTool({
1890
1900
  name: "codex_sketch",
1891
- description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
1901
+ description: "Edit the sketch board in this session using native editable strokes and layers. Use for @sketch requests and explicit follow-up edits to that drawing. The toolbar pen button is for manual drawing; do not require the user to open it. Start with inspect for the runId, documentId, revision and command reference. Draw with the svg op whenever possible: one SVG document per stage (paths, shapes, groups, transforms; see the reference from inspect) is far faster and richer than hand-placed strokes, and still becomes native editable strokes. Draw the shapes, hair, clothing and lettering outlines of every layer with svg; use stroke commands only for small corrections and native text. Apply atomic batches, preview between stages, and call finish to save the finished draft and release the editing lock. save is only a checkpoint. finish returns an image only when the user enables experimental preview feedback. Closing the board does not stop drawing. If the user stops drawing, do not retry. Never generates AI images, sends messages or attaches images automatically. Inspect automatically opens the board in the currently viewed session. Do not ask the user to open it first. If the session is not visible in DSH, ask them to switch to it. On timeout inspect before retrying; reuse the exact requestId only for the same request.",
1892
1902
  parameters: {
1893
1903
  action: {
1894
1904
  type: "string",
@@ -3058,7 +3068,7 @@ async function readSubscriptionCredentials(getAuth, readCredential, signal) {
3058
3068
  }
3059
3069
  //#endregion
3060
3070
  //#region src/version.js
3061
- const PACKAGE_VERSION = "2.3.0";
3071
+ const PACKAGE_VERSION = "2.4.1";
3062
3072
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
3063
3073
  //#endregion
3064
3074
  //#region src/model-catalog.js
@@ -3551,6 +3561,23 @@ const PATH_LIKE_ATTACHMENT_ID = /[\\/]/u;
3551
3561
  const FILE_NAME_ATTACHMENT_ID = /\.[A-Za-z0-9]{1,16}$/u;
3552
3562
  const record$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
3553
3563
  const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
3564
+ const NETWORK_REASONS = {
3565
+ UND_ERR_HEADERS_TIMEOUT: "the image service sent no response within 5 minutes",
3566
+ UND_ERR_BODY_TIMEOUT: "the image response stalled",
3567
+ UND_ERR_CONNECT_TIMEOUT: "the connection to the image service timed out",
3568
+ UND_ERR_SOCKET: "the connection closed unexpectedly",
3569
+ ECONNRESET: "the connection was reset",
3570
+ ETIMEDOUT: "the connection timed out",
3571
+ ENOTFOUND: "DNS lookup failed",
3572
+ ECONNREFUSED: "the connection was refused",
3573
+ EAI_AGAIN: "DNS lookup failed"
3574
+ };
3575
+ function requestFailureReason(error) {
3576
+ const code = [(error?.cause ?? error)?.code, error?.code].find((value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{2,39}$/u.test(value));
3577
+ if (code !== void 0) return `: ${NETWORK_REASONS[code] ?? "network error"} (${code})`;
3578
+ if (error?.name === "TimeoutError") return ": the image request timed out";
3579
+ return "";
3580
+ }
3554
3581
  function normalizeImageOptions(args) {
3555
3582
  const quality = nonEmpty(args?.quality) ?? "auto";
3556
3583
  const background = nonEmpty(args?.background) ?? "auto";
@@ -3955,7 +3982,7 @@ function createCodexImageTool(options) {
3955
3982
  });
3956
3983
  } catch (error) {
3957
3984
  if (exec.signal.aborted) throw exec.signal.reason;
3958
- throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed`, { cause: error });
3985
+ throw new Error(`Codex image ${editing ? "edit" : "generation"} request failed${requestFailureReason(error)}`, { cause: error });
3959
3986
  }
3960
3987
  if (!response.ok) {
3961
3988
  if (response.status === 401 || response.status === 403) throw new Error("ChatGPT sign-in needs to be renewed");
@@ -4642,7 +4669,8 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
4642
4669
  runtime: {
4643
4670
  node: process.version,
4644
4671
  platform: process.platform,
4645
- arch: process.arch
4672
+ arch: process.arch,
4673
+ ...hostDshVersion() ? { dsh: hostDshVersion() } : {}
4646
4674
  },
4647
4675
  account,
4648
4676
  login,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",