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 +31 -45
- package/README.md +51 -42
- package/lib/client.js +816 -9
- package/lib/index.js +39 -11
- package/package.json +1 -1
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
|
[](LICENSE)
|
|
17
17
|
[](https://github.com/WSL043/dsh-codex-subscription/stargazers)
|
|
18
18
|
|
|
19
|
-
[
|
|
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.
|
|
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%">
|
|
187
|
-
<tr><td><img src="docs/assets/sketch-
|
|
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
|
-
|
|
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 ·
|
|
186
|
+
<summary>Advanced examples · Mechanical cutaway, blue-green landscape and a reference-copy challenge</summary>
|
|
194
187
|
|
|
195
|
-
**
|
|
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
|
-
**
|
|
190
|
+
**Clock Tower Cutaway**
|
|
198
191
|
|
|
199
192
|
<table>
|
|
200
|
-
<tr><th width="
|
|
201
|
-
<tr><td><img src="docs/assets/sketch-
|
|
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
|
-
|
|
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
|
-
**
|
|
200
|
+
**A Thousand Green Peaks**
|
|
216
201
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
210
|
+
**"DeepSeek-chan" · copy a reference image (draw only, no image generation)**
|
|
228
211
|
|
|
229
|
-
|
|
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="
|
|
233
|
-
<tr><td><img src="docs/assets/sketch-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
[](LICENSE)
|
|
17
17
|
[](https://github.com/WSL043/dsh-codex-subscription/stargazers)
|
|
18
18
|
|
|
19
|
-
[
|
|
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
|
|
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.
|
|
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%"
|
|
187
|
-
<tr><td><img src="docs/assets/sketch-
|
|
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>进阶展示 ·
|
|
188
|
+
<summary>进阶展示 · 机械剖面、青绿山水与 DeepSeek 娘临摹</summary>
|
|
194
189
|
|
|
195
|
-
|
|
190
|
+
前两道题都选了容易暴露差距的题材:机械剖面考结构与细节(齿轮咬合、摆锤、链条、楼梯上的人物),青绿山水考构图与审美(三远法、留白、皴法与设色)。同一条提示词分别交给 **GPT-6.1-Sol · Xhigh** 和 **GPT-6-Astra · Low**,各跑一次,均在官方桌面版上完成,生图统一为 GPT Image 2(质量:高)。
|
|
196
191
|
|
|
197
|
-
|
|
192
|
+
**《钟塔剖面图》**
|
|
198
193
|
|
|
199
194
|
<table>
|
|
200
|
-
<tr><th width="
|
|
201
|
-
<tr><td><img src="docs/assets/sketch-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
@sketch 用 4:3 横版画板绘制《画布背面有人》:中央偏上是一处撕开的纸洞,洞内是深蓝星空和一位拿颜料桶的小画师;蓝色颜料从桶中流出,形成 S 形河流,流向下方城市。左侧城市保持未上色线稿,右侧城市被暖色点亮,加入纸船与飞鸟。按纸面、洞内世界、颜料河流、城市、画师和细节分层绘制,保留原生可编辑笔画。
|
|
213
|
-
```
|
|
210
|
+
Sol Xhigh 约 28 分钟,Astra Low 约 5 分钟。两者都保留了主峰、瀑布、栈道与江面的位置;Sol 的远山层次和雾带留白更完整。
|
|
214
211
|
|
|
215
|
-
|
|
212
|
+
**《DeepSeek 娘》· 照着参考图临摹(只画不生图)**
|
|
216
213
|
|
|
217
|
-
|
|
218
|
-
请基于本条附加草图实际调用订阅图片工具一次,生成成品插画。quality=low,模型使用当前默认,不切换型号,不额外生成。主题《画布背面有人》:保留4:3横构图、中央偏上的撕纸洞口、洞内拿颜料桶的小画师、流出成为S形河流的蓝色颜料、下方左侧未上色城市与右侧被点亮城市、纸船飞鸟。精修为惊艳的立体纸艺与精细手绘结合的编辑插画,纸张纤维、真实撕边及柔和投影,深靛蓝洞内星月,丰富青蓝颜料层次和流动质感,赭橙画师与暖色建筑,微小清晰的叙事细节。不重构为风景,不添加文字水印。必须使用本条参考图片编辑,不能仅凭文字生成。生成后简短说明完成即可。
|
|
219
|
-
```
|
|
214
|
+
这道题把一张参考图作为附件发给模型,让它在画板上临摹,考的是比例、渐变长发、蕾丝褶边、手写字和小图案能不能对得上。参考图是社区流传的 DeepSeek 娘表情包。每格都在官方桌面版上跑一次,没有调用生图;原生笔画版开启了“完成后返回预览”,SVG 版在同一条提示词后加了一句“每层用 svg 命令”。
|
|
220
215
|
|
|
221
|
-
|
|
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
|
-
|
|
222
|
+
SVG 版的发丝高光、蕾丝褶边和手写字更精细,Sol 用 SVG 还比逐笔绘制快了近 9 分钟;Astra 用 SVG 多花约 3 分钟,换来明显更丰富的细节。两个模型都没能画准脸型(偏长偏平)和围裙上的小鲸鱼图案,Sol 的鲸鱼更像鲸。
|
|
224
223
|
|
|
225
|
-
|
|
224
|
+
<details>
|
|
225
|
+
<summary>展开三道题的完整提示词</summary>
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
```text
|
|
228
|
+
@sketch 用 3:4 竖版画板绘制《钟塔剖面图》:蒸汽朋克风格的钟塔纵向剖面技术插图。最上方是圆形大钟面(十二个刻度、两根指针)和一口铜钟;钟面下一层是齿轮机房:至少 9 个大小不同、齿数正确、彼此咬合的齿轮(相邻齿轮转向相反,齿廓要画出锯齿),另有一个锚形擒纵机构;中段是一根贯穿的长摆杆,末端为铜制圆盘摆锤,旁边两条挂着配重铁块的链条;塔壁一侧是沿墙盘旋而上的螺旋楼梯,梯上有三个微小的工匠人物;底部是石砌地基与拱门。剖面用厚实的暗色砖石墙体框出,内部露出黄铜与铁质结构,加入铆钉、管道,以及从小窗透进来的光柱。按背景砖墙、结构框架、齿轮组、摆锤与链条、楼梯与人物、光影与细节分层绘制。画完后,把这张草图作为唯一参考图,调用订阅图片工具一次生成成品:保持每个部件的位置、比例与咬合关系不变,做成精密的蒸汽朋克剖面图插画——黄铜与旧钢的金属质感、细致的机械倒影和铆钉、羊皮纸底色配墨线渲染、暖色灯光,不添加文字。
|
|
229
|
+
```
|
|
228
230
|
|
|
229
|
-
|
|
231
|
+
```text
|
|
232
|
+
@sketch 用 3:4 竖版画板绘制《千峰翠色》:宋代青绿山水的构图,严格运用「三远法」。高远:右侧一座主峰拔地而起,占画面上半部,山体用层叠的块面表现,山腰有横向的云雾留白;深远:主峰后方还有两重淡青色远山,越远越淡,之间隔着大片留白的雾带;平远:下方是舒展的江面,一叶渔舟和两只飞鸟。左侧一挂瀑布从中段峭壁倾泻而下,落入山涧,涧边有三株松树,松针成簇;山腰有一座小亭和一条曲折栈道,栈道上有两个极小的行者;前景是坡石与芦苇,用勾勒加披麻皴式的笔触表现。色彩层次为石青、石绿、赭石,远山用淡青。按远山与天空、雾带留白、主峰、瀑布与松树、亭台栈道与行者、江面舟鸟与前景分层绘制。画完后,把这张草图作为唯一参考图,调用订阅图片工具一次生成成品:保持山体、瀑布、栈道、江面的位置和三远法的层次不变,做成宋代青绿山水绢本设色——石青石绿矿物颜料的沉着厚重、细腻的皴擦与勾勒、绢面纹理与微微泛黄的古意、云雾留白,不添加文字或印章。
|
|
233
|
+
```
|
|
230
234
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</table>
|
|
235
|
+
```text
|
|
236
|
+
@sketch 用 1:1 方形画板,尽可能忠实地临摹我附上的这张参考图(一张 Q 版二次元表情包)。画面:白色底;Q 版二头身的蓝发鲸鱼娘跪坐在画面右侧偏中,蓝色渐变长发披肩(发根深蓝、发梢浅蓝),头顶一根呆毛,戴白色蕾丝女仆头饰和蓝色蝴蝶结,头两侧是深蓝色带白色内里的鲸鱼耳鳍,蓝色大眼睛带高光,猫嘴嘟嘴的“ω”表情,脸颊泛红,双手叉腰;穿深蓝色女仆装,白色蕾丝围裙(围裙上有一只蓝色小鲸鱼图案),黑色领结,袖口和裙摆有白色蕾丝褶边;身后露出一条深蓝色的鲸鱼尾巴。左上方是白色对话气泡(黑色描边,尖角指向她),气泡里用黑色粗体写“你这吃白饭的蓝色大肥鱼。”;左侧从画面外伸进来一只黑袖子的手,食指指向她;她的左下方用蓝色描边字写“没吃饱喵!”;她脚边的地上放着一只空的白瓷饭碗,碗内壁印着一只蓝色小鲸鱼。请分层绘制:白色背景、头发与鲸鱼耳鳍与尾巴、身体与服装、脸部五官、饭碗、手臂与对话气泡文字,保留原生可编辑笔画,并特别注意角色比例、头发的渐变与发丝走向、蕾丝褶边和脸部表情的细节。这次只在草图画布上画,不要调用任何生图或图片编辑工具;画完后检查一遍与参考图的差异并修正,最后保存并完成。
|
|
237
|
+
```
|
|
235
238
|
|
|
236
|
-
|
|
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(""").join("\"").split("'").join("'").split("<").join("<").split(">").join(">").split("&").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:
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1210
|
-
|
|
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.
|
|
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
|
+
"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",
|