dsh-connect-trae 4.6.0 → 4.6.2
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/CHANGELOG.md +132 -0
- package/README.en.md +3 -3
- package/README.md +4 -4
- package/lib/index.js +283 -14
- package/package.json +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,137 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.6.2 (2026-10-27)
|
|
4
|
+
|
|
5
|
+
> **从插件市场装完,重启后插件不会再消失。** 安装其实失败了,但失败发生在
|
|
6
|
+
> 「已提升到 live」之后的一行——用户看到的是「装好了」,下一次启动才发现插件没了。
|
|
7
|
+
|
|
8
|
+
### 修复
|
|
9
|
+
|
|
10
|
+
- **`@earendil-works/pi-ai` 改标为可选 peer,安装不再被 generation 校验拦下**(#65)。
|
|
11
|
+
- **现象**:从 DSH 插件市场安装后,日志先报
|
|
12
|
+
`generation-install: promoted to dsh-connect-trae+4.6.1+…`,紧接着报
|
|
13
|
+
`generation-install: generation peer validation failed: @earendil-works/pi-ai
|
|
14
|
+
does not resolve from the generation or installation closure`。这次安装因此
|
|
15
|
+
`exit=1`、**不登记进持久化清单**(`.generations/desired.json`、
|
|
16
|
+
`desktop-workbenches/market-installs.json`);DSH 下次启动按清单重建
|
|
17
|
+
`.generations/live`,未登记的 generation 被清掉(`logs/harness.log`:
|
|
18
|
+
`swept 3 unreferenced plugin generation(s)`)——表现为「装完没问题,重启后消失」,
|
|
19
|
+
重装必然复现。
|
|
20
|
+
- **根因不是 pi-ai 特殊,而是宿主核心包的判定方式是包名前缀。** 插件市场
|
|
21
|
+
(`dshmarket` 的 `corePackageNames()`)从 DSH 安装目录的 `node_modules` 反推核心包集合,
|
|
22
|
+
规则是 `/^(?:dsh|cordis)/` 匹配于 `@deepseek-ai/` 之下;命中者的 peer 允许从**宿主锚点**
|
|
23
|
+
解析,未命中者必须出现在插件自己的安装闭包里。`@earendil-works/pi-ai` 两个前缀都不匹配,
|
|
24
|
+
而 DSH 只在运行时注入它,闭包里从来没有——于是必然失败。
|
|
25
|
+
- **修法**:进 `peerDependenciesMeta` 标 `optional`(同作者的 `dsh-connect-workbuddy`
|
|
26
|
+
早就是这个写法,本仓库是漏了,不是设计如此)。**peer 范围保留不动**
|
|
27
|
+
(`>=0.85.1 <2.0.0`):它编码的是适配器能吃哪几代 pi-ai(见 `src/pi-ai-runtime.ts`
|
|
28
|
+
与 issue #27),与「装不装」是两件事,删范围会丢掉代际判据。
|
|
29
|
+
- **为什么这次专门加测试**:修复是纯声明式的,构建期没有任何东西会察觉——`tsc` 只看到
|
|
30
|
+
字符串;`devDependencies` 里有实体 pi-ai,本地每次测试都能解析成功;CI 照常绿。
|
|
31
|
+
故障只在用户机器上、重启之后才出现。故新增 `tests/peer-optional.spec.ts`(6 例)
|
|
32
|
+
直接读随包 manifest 断言该标记,并把规则推广为「任何不在 `@deepseek-ai/{dsh,cordis}`
|
|
33
|
+
前缀下、却由宿主提供的 peer 都必须 optional」,避免下一个宿主依赖重开同一个洞。
|
|
34
|
+
- 贡献者:[@674830790](https://github.com/674830790)(issue #65,附完整日志与因果链)。
|
|
35
|
+
|
|
36
|
+
### 测试
|
|
37
|
+
|
|
38
|
+
- `tests/peer-optional.spec.ts` 6 例:pi-ai 必须 `optional: true`;非核心前缀的宿主提供 peer
|
|
39
|
+
一律 optional;`optional` 必须是字面量 `true`(`"yes"`/`1` 之类 npm 不认,改了看着像修好
|
|
40
|
+
其实没修);`peerDependenciesMeta` 不得出现未声明的 peer;pi-ai 仍须被 `devDependencies`
|
|
41
|
+
钉住。**变异验证**:删掉 `peerDependenciesMeta` 后该文件 2 例转红,确认守卫不是恰好绿。
|
|
42
|
+
- 全量 **93 files / 1186 tests** 通过(原 1180 + 新增 6)。
|
|
43
|
+
|
|
44
|
+
### 说明
|
|
45
|
+
|
|
46
|
+
- 本次修复的判据来自插件市场的源码规则与同类插件的既有写法,**未在真机上复现那条
|
|
47
|
+
generation 校验**(开发机为 0.2.1-rc.3、报告人用 rc.2,该校验器的报错串不在本机应用包内)。
|
|
48
|
+
若 4.6.2 在报告人机器上仍复现,则说明该校验器不认 `optional` 标记而要求闭包里存在实体
|
|
49
|
+
文件,届时应改走「把 pi-ai 移出 peer 名单、改为运行时软探测」。
|
|
50
|
+
|
|
51
|
+
## 4.6.1 (2026-10-26)
|
|
52
|
+
|
|
53
|
+
> **4.6.0 说「图片字节插件拿不到」,这一版把这句话纠正过来。** 那条按
|
|
54
|
+
> `?prompt=&image_size=` 再打一次 GET 的路确实不是取图、而是**又一次生成**,但
|
|
55
|
+
> 「uri → 字节」不是没有通道,只是请求体形态特殊。现在图片会被取回来、落成会话文件、
|
|
56
|
+
> 直接显示在聊天里。
|
|
57
|
+
|
|
58
|
+
### 修复
|
|
59
|
+
|
|
60
|
+
- **文生图把图片真的取回来了**。
|
|
61
|
+
- **取图通道**:`POST /api/ide/v1/get_resource_url`,body 必须正是
|
|
62
|
+
`{"uri_list":["<tos key>"]}`——`{uri}`/`{path}`/`{resource_uri}`/`{uris:[…]}`/
|
|
63
|
+
`{resources:[…]}`/`{scene:'report',uri}` 以及 GET `?uri=` 统统只回 200 + 空
|
|
64
|
+
`url_map`(不报错、也不给地址)。应答是
|
|
65
|
+
`{"url_map":{"<key>":"<byteimg 签名 URL>"}}`,再直接 GET 那个签名地址(**不带任何
|
|
66
|
+
Trae 头**,签名在 query 里)即得字节——实测 `image/jpeg`、1,155,578 字节、头
|
|
67
|
+
`ffd8ffe0`。对已有 key 这一步**零成本**,不会重新生成。
|
|
68
|
+
- **图片落成会话文件并直接显示**:`generate` 返回 `{url, credit?, bytes, mimeType}`,
|
|
69
|
+
`execute` 把字节写进 `<session cwd>/trae-images/trae-YYYYMMDD-HHMMSS-<hex>.<ext>`
|
|
70
|
+
(扩展名跟 mime 走),`output.schema` 恢复 `path` 并进 `required`,`render` 输出
|
|
71
|
+
``。DSH 的聊天按 session cwd 解析相对路径、再过宿主的
|
|
72
|
+
`api/file` 鉴权路由取字节,所以图真的显示出来,不需要用户去 Trae 客户端翻历史。
|
|
73
|
+
- **失败语义**:图已生成、积分已扣、但字节取不回来(`url_map` 里没有这个 key / CDN 非
|
|
74
|
+
2xx / 0 字节 / 落盘失败)→ `IMAGE_UNAVAILABLE`,进 30 秒冷却,文案明说「积分已经扣了」
|
|
75
|
+
并给出对象 key 供排障,要求模型**不要重试**。
|
|
76
|
+
- 改图与文生视频仍然**不做**,也不留半截入口。
|
|
77
|
+
- **图片现在落在会话目录,不再落到宿主进程的 cwd**。
|
|
78
|
+
- 用户在实际 web 会话里跑了一次:工具返回 `trae-images/trae-20261026-034146-1c0a91.jpg`,
|
|
79
|
+
聊天里却显示「图片无法预览」。文件确实生成了(789,453 字节真 JPEG),但落在**启动 DSH
|
|
80
|
+
的那个目录**里,而不是会话目录。
|
|
81
|
+
- 根因:宿主的 `ToolExecution` 不带会话目录。`cwd` 字段 4.15.x 仍标 `Reserved`(不填),
|
|
82
|
+
`agent.session.cwd` 这个字段不存在,`agent.session.header.cwd` 又只是**创建时**目录
|
|
83
|
+
(`/cd` 之后就不对)。插件此前只能回退到 `process.cwd()`。
|
|
84
|
+
- 现在照宿主自己的 `dsh-tool-*` 取法:`ctx.inject(['workingDirectory'])` 后调
|
|
85
|
+
`workingDirectory.ensure(exec.agent, exec.signal)`(它读 session projection
|
|
86
|
+
`workingDirectory`,即实时目录,并确认目录还在)。阶梯:宿主 service →
|
|
87
|
+
`agent.session.header.cwd` → `process.cwd()`。`TraeImageToolDeps` 新增可选
|
|
88
|
+
`directoryOf`,service 读取放在 `src/index.ts` 注入,`image-tool` 保持纯函数可单测。
|
|
89
|
+
- **取图那一跳的 HTTP 错误不再谎报成因**。
|
|
90
|
+
- 此前它复用生图那套状态码映射,取图接口回 403 会报 `AUTH`(「上游拒绝了凭据」)、回
|
|
91
|
+
429 会报 `RATE_LIMIT`——可这时图早已生成、积分早已扣掉,用户照这句话去重新登录,
|
|
92
|
+
钱花了图还是拿不到。
|
|
93
|
+
- 现在取图阶段(解析地址、下载字节)的 HTTP 失败一律归 `IMAGE_UNAVAILABLE`,文案保留
|
|
94
|
+
状态码与上游原文、附上对象 key。冷却判据只看 code,`IMAGE_UNAVAILABLE` 仍在冷却集合里,
|
|
95
|
+
不会因此变成可重试。
|
|
96
|
+
- **下载回来的东西必须是图片**。签名地址过期时 CDN 常拿 `200` 回一页 HTML,而
|
|
97
|
+
`content-type` 可能仍写着 `image/jpeg`。现在按字节魔数判定
|
|
98
|
+
(JPEG/PNG/GIF/WebP/BMP),认不出就如实报 `IMAGE_UNAVAILABLE`(带前 4 字节),
|
|
99
|
+
不再落一个打不开的 `.jpg`。
|
|
100
|
+
|
|
101
|
+
### 变更
|
|
102
|
+
|
|
103
|
+
- **删掉已证明是「又一次生成」的配套代码**:`src/image-gen-trae.ts` 里的
|
|
104
|
+
`traeImageFetchUrl()`、`computeTraeIdeSign()`、`traeImageSignFields()` 与
|
|
105
|
+
`TRAE_IMAGE_SIGN_SALT` 全部移除(连同它们的测试)。取图跟 prompt 没有任何关系,
|
|
106
|
+
留着只会诱导后人再打一次那条会再扣费的 GET。
|
|
107
|
+
|
|
108
|
+
### 测试
|
|
109
|
+
|
|
110
|
+
- `tests/image-tool-download.spec.ts` 5 例:stub fetch 断言**三跳**(POST 生图 →
|
|
111
|
+
POST `get_resource_url` → GET 签名地址),且**没有任何**带 `image_size=` 的请求;
|
|
112
|
+
`uri_list` 请求体;CDN 请求不带 `Authorization`、只带 `Accept: image/*`;
|
|
113
|
+
空 `url_map` → `IMAGE_UNAVAILABLE`;取图跳回 401/403/429/500/502 一律
|
|
114
|
+
`IMAGE_UNAVAILABLE`(断言文案含状态码与对象 key,且**不含**「拒绝了凭据」「限流」);
|
|
115
|
+
取图跳回 `200` 但字节不是图片(HTML 错误页)→ `IMAGE_UNAVAILABLE`。
|
|
116
|
+
- `tests/image-gen-trae.spec.ts` 21 例:新增 `buildTraeResourceUrlBody`(只产出
|
|
117
|
+
`uri_list`、去空白、空 uri → `INVALID_ARGS`)、`parseTraeResourceUrlMap`(去空白、
|
|
118
|
+
坏形状回空表、丢弃非字符串/空串)、`signedTraeResourceUrl`(取到地址、缺 key 即
|
|
119
|
+
`IMAGE_UNAVAILABLE` 并带 key)三组纯函数单测。
|
|
120
|
+
- `tests/image-tool-render.spec.ts` 12 例:字节真落盘、文件名与后缀、markdown 用相对路径
|
|
121
|
+
且不含 key、credit 回显、写盘失败即 `IMAGE_UNAVAILABLE`、`IMAGE_UNAVAILABLE` 进冷却而
|
|
122
|
+
`INVALID_ARGS` 不进;以及会话目录阶梯三条——只有 `agent.session.header.cwd` 时写进它、
|
|
123
|
+
实时 `workingDirectory` 优先于创建时目录、`directoryOf` 注入(`index.ts` 接 projection
|
|
124
|
+
的方式)优先于两者。
|
|
125
|
+
- 全量 **92 files / 1180 tests** 通过(16 个 live 探针被 `TRAE_LIVE` 跳过且未提交,
|
|
126
|
+
跑一次烧一次积分);CI 在 `ubuntu-latest` 与 `windows-latest` 双平台通过。
|
|
127
|
+
- 真机(零成本,复用既有 key):签名 → 1,155,578 字节 JPEG → 落盘同样字节 →
|
|
128
|
+
render 输出 markdown 图片,整条链路跑通。除 POST 之外整条链路都在真账号上验过。
|
|
129
|
+
|
|
130
|
+
### 文档
|
|
131
|
+
|
|
132
|
+
- `README.md` / `README.en.md` 的「文生图」条目与协议表改成「两步取图 + 落盘 + 相对
|
|
133
|
+
路径显示」,并保留「那条 GET 是又一次生成」的实测记录作为反面证据。
|
|
134
|
+
|
|
3
135
|
## 4.6.0 (2026-10-26)
|
|
4
136
|
|
|
5
137
|
> **文生图先只在国内版面世,并且必须由用户亲手勾选。** 企业版的租户网关是另一条
|
package/README.en.md
CHANGED
|
@@ -42,7 +42,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
42
42
|
- **Multiplier in the model name** — model names show the credit multiplier in Trae's own menu format (e.g. `GLM-5.2 · x0.79`), updated with each directory refresh.
|
|
43
43
|
- **Promotion badge** — Trae publishes its discount rules in the model directory (`activity_discount`: pay ratio, list price, off-peak windows with their time zone). The plugin judges them against **the clock at render time** and paints one capsule beside the model name (`5 折` / `off-peak 5 折` / `limited 5 折`), with the old price, the new price and the window on hover. Judging at render time is what keeps a night-time discount from still hanging there the next morning; when upstream says nothing is discounted right now no badge is painted, the multiplier is never used to guess a promotion, and no campaign without an end time is labelled "limited". It is information only: no request and no billing changes, and the international region (subscription) has no such data and therefore shows no badge. See [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md).
|
|
44
44
|
- **DSH local tool loop** — gets pending structured `tool_calls` from Trae `llm_utils_chat`, lets DSH execute its own local tools, then returns tool results to the model.
|
|
45
|
-
- **Text-to-image (`trae_draw_image`)** — brings the signed-in account's Trae text-to-image service into the conversation: the model draws one image and the plugin
|
|
45
|
+
- **Text-to-image (`trae_draw_image`)** — brings the signed-in account's Trae text-to-image service into the conversation: the model draws one image and the plugin **fetches the bytes too**, writes them under the session cwd and shows the picture inline. **Off by default, enabled per panel** — generation really spends credits (measured: ~8.8 for one `1920x1920` on CN personal), so unlike the image-input declaration it only happens when you say so. Protocol, size bounds, the two-step fetch and why the old "fetch" GET is really a second generation are in "Text-to-image" below.
|
|
46
46
|
- **Dual-region detection (CN / international)** — detects local sign-ins across Trae CN, TRAE SOLO CN, Trae, and TRAE SOLO installs. The region is derived from the credential's own `userRegion` claim (host suffix and edition label as fallbacks), with no manual switch.
|
|
47
47
|
- **Region-isolated directories and selections** — CN and international each keep their own model directory, enabled picks, image opt-ins, and context budgets, and each provider reads only its own slot.
|
|
48
48
|
- **Vision follows Trae's own declaration** — when Trae marks a model `multimodal`, the plugin advertises it as such with no manual tick per model (issue #16). The rule is "**models you tick ∪ models Trae marks multimodal**": models Trae marks `false`, or does not mark at all, stay text-only and can still be turned on by hand. A row enabled automatically is shown ticked and read-only, with the reason. See "Image input" below.
|
|
@@ -130,10 +130,10 @@ The text-to-image service behind TraeWork / SOLO is now reachable from a DSH con
|
|
|
130
130
|
| Request body | Only `{prompt, image_size?}`. Adding known fields (`mode_type` / `session_id` / `image_urls` / `watermark`) earns a 400 with an empty body; unknown fields are ignored |
|
|
131
131
|
| Success | Flat `{"message":"success","uri":"tos-cn-i-…/cn/<uid>/gen_image/<ts>_<hex>_jpeg_1920x1920"}` — **no** `{code,msg,data}` envelope |
|
|
132
132
|
| Sizes | Total pixels must land in **3686400 ~ 16777216**: `1024x1024` is rejected as too small (`code 3003`, no charge) and `4608x4608` as too large; presets `2k`/`3k`/`4k` work, default is `1920x1920` |
|
|
133
|
-
| Fetch | **
|
|
133
|
+
| Fetch | **Two steps, no extra charge**: `POST /api/ide/v1/get_resource_url` whose body must be exactly `{"uri_list":["<key>"]}` (`{uri}`/`{path}`/`{resource_uri}`/`{uris:[…]}` and GET `?uri=` all answer 200 with an empty `url_map`), replying `{"url_map":{"<key>":"<byteimg signed URL>"}}`; then a plain GET of that signed URL (**no Trae headers** — the signature is in the query) returns the bytes: measured `image/jpeg`, 1,155,578 bytes, header `ffd8ffe0` |
|
|
134
134
|
| What that GET really is | Decisive probe: after the POST returned `…/1792954795741_44f94116_jpeg_1920x1920`, the GET with the same `(prompt, size)` came back with a **different** uri (`…/1792954810505_b107ac11_jpeg_1920x1920`) after 27 s and zero pixels — a second image and a second charge (~17.6 for the pair). The illegal-size probe agreed: the error is Ark's own `[callArkImageGeneration]` |
|
|
135
135
|
|
|
136
|
-
**
|
|
136
|
+
**The bytes really do come back.** On success the tool returns `url` (the Trae object key, kept for diagnostics), `path` (a local file relative to the session cwd, e.g. `trae-images/trae-20261026-042015-ab12cd.jpg`), `model`, `prompt`, `size` and a `hint`; the renderer emits `` — DSH's chat resolves that relative path against the session cwd and reads the bytes through the host's authenticated `api/file` route, so the picture shows up inline. Only when the bytes genuinely could not be fetched (`IMAGE_UNAVAILABLE`: the image was generated and charged) does it fall back to inline-code "key only", and such a failure still starts the 30-second cooldown — generation is billed per image, so a retry buys another one.
|
|
137
137
|
|
|
138
138
|
**Image editing and text-to-video are not opened up.** Editing needs a TOS upload presign first (this plugin has no such path); video polls a material result — a separate async pipeline. Neither ships, and neither leaves half an entry point behind.
|
|
139
139
|
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
- **倍率内嵌模型名** —— 模型名称按 Trae 自身菜单的格式显示积分倍率(如 `GLM-5.2 · x0.79`),倍率随目录刷新更新。
|
|
47
47
|
- **活动优惠徽标** —— Trae 把促销规则发在目录里(`activity_discount`:折扣比例、原价、闲时时段与时区),插件按**当前时钟**判定后,在模型名旁边画一枚胶囊:`5 折` / `闲时 5 折` / `限时 5 折`,悬停写明原价、现价与时段。判定放在**渲染时刻**而不是解析时刻,所以夜间折扣不会在第二天早上继续挂着;上游说「此刻没打折」就不画,**不从倍率反推**,也不给没有结束时间的活动编「限时」。这是纯提示:不改请求、不改计费,国际区(订阅制)没有这项数据因此不显示。详见 [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md)。**同一枚胶囊也画在 DSH 的模型选择器里**(宿主 `conversation.input.model.label` 插槽),挑模型时就能看见——毕竟徽标画在设置卡片里时,你已经选完了。选择器里**倍率只出现一次**:名字里已有的目录倍率会被拆出来,有折扣时换成**折后价**(与卡片同一套时钟判定,同一份数据),打开选择器不会额外触发上游读。
|
|
48
48
|
- **DSH 本地工具循环** —— 通过 Trae `llm_utils_chat` 获取待执行的结构化 `tool_calls`,交由 DSH 自带的本地工具执行,再将工具结果回传模型。
|
|
49
|
-
- **文生图(`trae_draw_image`)** —— 把登录账号的 Trae
|
|
49
|
+
- **文生图(`trae_draw_image`)** —— 把登录账号的 Trae 文生图能力接进会话:模型画一张图,插件把**图片字节一起取回来**、写进会话目录再显示给你。**默认关闭、按面板单独开**——生图真实消耗积分(实测 CN 个人版 `1920x1920` 一张约 8.8),所以它和识图那种「让不让看图」的开关不同,只能由你点头。协议、尺寸边界、取图那两步,以及为什么「取图」那条 GET 其实是又一次生成,见下方「[文生图](#文生图trae_draw_image)」。
|
|
50
50
|
- **国内国际双区域自动识别** —— 自动发现 Trae CN / TRAE SOLO CN / Trae / TRAE SOLO 四个本地安装的登录账号;区域由凭证自带的 `userRegion` 声明自动判定(host 后缀与 edition 标签兜底),无需手动指定。
|
|
51
51
|
- **目录与勾选按面板隔离** —— 三个面板各一套模型目录、勾选、图片开关与上下文预算,各自读各自的槽位。名录从**该面板所钉的那条网关**读取——原本钉在企业池的客户端去读消费池名录,会永远发现不了自己池的独占模型。
|
|
52
52
|
- **识图能力跟随 Trae 自身的标注** —— Trae 把某个模型标为 `multimodal` 时,插件直接按多模态对外声明,无需你在卡片上逐一手勾(issue #16)。判据是「**你手动勾选的 ∪ Trae 标注多模态的**」:Trae 明确标 `false` 或未标注的模型保持纯文本,你可以照旧手动为它们打开图片输入。卡片上被自动开启的那一行显示为已勾选且只读,并说明原因。详见下方「图片输入(识图)」。
|
|
@@ -123,7 +123,7 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
123
123
|
|
|
124
124
|
## 文生图(`trae_draw_image`)
|
|
125
125
|
|
|
126
|
-
TraeWork / SOLO
|
|
126
|
+
TraeWork / SOLO 侧那套文生图服务带出来了:在会话里让模型**画一张图**,插件把图片字节取回来、落成会话目录里的文件,再按相对路径显示在聊天里。取图走 `get_resource_url` 解析签名地址,不会重新生成、不额外计费——见下表。
|
|
127
127
|
|
|
128
128
|
**默认关闭,按面板单独开。** 生图**真实消耗积分**(实测 CN 个人版 `1920x1920` 一张约 8.8 积分),所以它不是识图那种「让不让看图」的声明,而是一个要你点头的支出口:卡片模型栏勾上「允许模型自主生图」(`drawImageEnabled`),该面板的 `trae_draw_image` 才注册进 DSH;取消勾选立即撤下。面板各存各的开关;企业版与国际版面板不提供——企业版租户网关是另一条计费面,国际版按另一套套餐计费,两者的生图行为都没有测过,不在这里假装支持。
|
|
129
129
|
|
|
@@ -135,10 +135,10 @@ TraeWork / SOLO 侧那套文生图服务带出来了:在会话里让模型**
|
|
|
135
135
|
| 请求体 | 只有 `{prompt, image_size?}`。多带已知字段(`mode_type` / `session_id` / `image_urls` / `watermark`)网关直接回 400 空 body——未知字段反而被忽略 |
|
|
136
136
|
| 成功响应 | 扁平结构 `{"message":"success","uri":"tos-cn-i-…/cn/<uid>/gen_image/<ts>_<hex>_jpeg_1920x1920"}`,**没有** `{code,msg,data}` 信封 |
|
|
137
137
|
| 尺寸 | 总像素必须落在 **3686400 ~ 16777216**:`1024x1024` 太小被拒(`code 3003`,不扣费)、`4608x4608` 太大被拒;预设 `2k`/`3k`/`4k` 可用,缺省 `1920x1920` |
|
|
138
|
-
| 取图 |
|
|
138
|
+
| 取图 | **两步、零额外计费**:`POST /api/ide/v1/get_resource_url`,body 只能是 `{"uri_list":["<key>"]}`(`{uri}`/`{path}`/`{resource_uri}`/`{uris:[…]}`、GET `?uri=` 都只回 200 + 空 `url_map`),应答 `{"url_map":{"<key>":"<byteimg 签名 URL>"}}`;再直接 GET 那个签名地址(**不带任何 Trae 头**,签名在 query 里)拿字节——实测 `image/jpeg`、1,155,578 字节、头 `ffd8ffe0` |
|
|
139
139
|
| 那条 GET 的真实身份 | 决定性探针:POST 拿到 `…/1792954795741_44f94116_jpeg_1920x1920` 后,用同一 `(prompt, size)` 打 GET,回来的是**另一个** uri(`…/1792954810505_b107ac11_jpeg_1920x1920`),耗时 27 秒、一个像素都没有——等于又画一张、再扣一次(两次共约 17.6)。非法尺寸探针(`100x100`)也印证:报错直接来自 Ark 的 `[callArkImageGeneration]` |
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
**图是真的取回来了。** 工具成功时返回 `url`(Trae 对象 key,留作排障)、`path`(相对会话目录的本地文件,如 `trae-images/trae-20261026-042015-ab12cd.jpg`)、`model`、`prompt`、`size` 和一句 `hint`;渲染层输出 ``——DSH 的聊天按 session cwd 解析这个相对路径,再过宿主的 `api/file` 鉴权路由取字节,所以图直接显示出来。只有字节真的没拿到时(`IMAGE_UNAVAILABLE`:图已生成、积分已扣)才退化成「只有对象 key」的行内代码;那种失败照样进 30 秒冷却——生图按张计费,重试等于再买一张。
|
|
142
142
|
|
|
143
143
|
**改图与文生视频没有放开。** 改图要先走 TOS 上传预签名(本插件没有这条链路);视频要轮询素材结果,是另一条异步管线。两者都不做,也不留半截入口。
|
|
144
144
|
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { basename, dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { copyFile, readFile, readdir, rename, rm, stat } from "node:fs/promises";
|
|
9
|
+
import { copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
10
10
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
|
|
11
11
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
12
12
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -9906,6 +9906,40 @@ var UnconfiguredTraeUpstreamClient = class {
|
|
|
9906
9906
|
};
|
|
9907
9907
|
//#endregion
|
|
9908
9908
|
//#region src/image-gen-trae.ts
|
|
9909
|
+
/**
|
|
9910
|
+
* Trae 文生图的纯逻辑层:请求体、响应解析、尺寸校验、取图地址解析、SSE 帧。
|
|
9911
|
+
*
|
|
9912
|
+
* 与 dsh-connect-workbuddy 的 image-gen.ts 同一分工——本文件只做纯函数与
|
|
9913
|
+
* 常量,不发网络请求;网络与工具注册在 image-tool.ts。
|
|
9914
|
+
*
|
|
9915
|
+
* 协议事实来自 2026-10-25 对 Trae CN 账号的活体探测(tests/live-probe*.spec.ts,
|
|
9916
|
+
* 结果存 probe2/3/4/5-results.jsonl):
|
|
9917
|
+
* - POST {chat}/api/ide/v1/tool_text_to_image,请求体只有 {prompt, image_size?}
|
|
9918
|
+
* 被接受;多发 mode_type/request_type/chat_mode/session_id/access_type/
|
|
9919
|
+
* image_urls/watermark 等任何已知字段都会被网关 400 拒绝(空 body)。
|
|
9920
|
+
* 未知字段被忽略(camelCase 字段能过)。
|
|
9921
|
+
* - 成功应答是扁平 JSON {message: "success", uri: "<tos 对象 key>"};
|
|
9922
|
+
* 业务失败是扁平 {code: 3003, message: "…"}。没有 {code:0,data} envelope。
|
|
9923
|
+
* - image_size 校验在 Ark 侧:格式 'WIDTHxHEIGHT' 或预设 2k/3k/4k,
|
|
9924
|
+
* 总像素 ∈ [3686400, 16777216](512x512 与 1024x1024 会被拒);
|
|
9925
|
+
* 缺省时输出 1920x1920。
|
|
9926
|
+
* - 同一路径的 GET(?prompt=<P>&image_size=<S> + x-ide-sign 头)**不是取图**,
|
|
9927
|
+
* 而是又一次生成:2026-10-26 决定性实测,同一 (prompt, size) 紧接着 GET,
|
|
9928
|
+
* 回来的是另一个 uri、耗时 27 秒、零像素(等于多扣一张)。曾为它写下的
|
|
9929
|
+
* traeImageFetchUrl / computeTraeIdeSign / traeImageSignFields 与签名盐已删除——
|
|
9930
|
+
* 取图跟 prompt 没有任何关系。
|
|
9931
|
+
* - 真正的取图是两步、不再生成(同日实测,零成本):POST
|
|
9932
|
+
* /api/ide/v1/get_resource_url { uri_list: [<tos key>] } →
|
|
9933
|
+
* { url_map: { "<key>": "<byteimg 签名 URL>" } },再直接 GET 那个签名 URL
|
|
9934
|
+
* (不带任何 Trae 头)即得图片字节(实测 image/jpeg、1,155,578 字节、
|
|
9935
|
+
* 头 ffd8ffe0)。签名 URL 带 x-expires,过期重新解析即可。
|
|
9936
|
+
* - SSE 端点 tool_text_to_image_stream 的事件名是小写:heartbeat(data:
|
|
9937
|
+
* {timestamp_ms, status})、result(data: {uri})、done(data:
|
|
9938
|
+
* {finish_reason})。
|
|
9939
|
+
* - 计费锚点:1920x1920 一张 ≈ 8.8 积分。
|
|
9940
|
+
*
|
|
9941
|
+
* @module dsh-connect-trae/image-gen-trae
|
|
9942
|
+
*/
|
|
9909
9943
|
/** 文生图(同步):一次 POST 拿回结果。Trae 客户端的 ToolText2Image 用的就是它。 */
|
|
9910
9944
|
const TRAE_TOOL_TEXT_TO_IMAGE_PATH = "/api/ide/v1/tool_text_to_image";
|
|
9911
9945
|
/** 上游为图像工具固定的模型 id;请求体里没有 model 字段,服务端自选。 */
|
|
@@ -9952,6 +9986,40 @@ function buildTraeImageBody(input) {
|
|
|
9952
9986
|
}
|
|
9953
9987
|
return out;
|
|
9954
9988
|
}
|
|
9989
|
+
/**
|
|
9990
|
+
* uri → 可下载 URL 的解析端点。2026-10-26 活体确认:body 必须正是
|
|
9991
|
+
* `{ uri_list: [<tos key>] }`;其它所有形态({uri}、{path}、{resource_uri}、
|
|
9992
|
+
* {uris:[…]}、{resources:[…]}、{scene:'report',uri}、GET ?uri=)都回
|
|
9993
|
+
* HTTP 200 + 空 url_map——不报错,只是什么都不给。
|
|
9994
|
+
*/
|
|
9995
|
+
const TRAE_RESOURCE_URL_PATH = "/api/ide/v1/get_resource_url";
|
|
9996
|
+
/** 组一次解析请求体;空 uri 在这里拦下(INVALID_ARGS,一分钱没花)。 */
|
|
9997
|
+
function buildTraeResourceUrlBody(uri) {
|
|
9998
|
+
const key = typeof uri === "string" ? uri.trim() : "";
|
|
9999
|
+
if (key === "") throw new TraeImageError("trae image: 解析图片地址需要非空的 uri。", "INVALID_ARGS");
|
|
10000
|
+
return { uri_list: [key] };
|
|
10001
|
+
}
|
|
10002
|
+
/**
|
|
10003
|
+
* 解析应答里的 url_map。服务端对认不出的 key 就回空表(不报错),
|
|
10004
|
+
* 所以非字符串/空串条目一律丢弃,由调用方按"缺地址"处理。
|
|
10005
|
+
*/
|
|
10006
|
+
function parseTraeResourceUrlMap(json) {
|
|
10007
|
+
const raw = (json !== null && typeof json === "object" ? json : {})["url_map"];
|
|
10008
|
+
const map = {};
|
|
10009
|
+
if (raw === null || typeof raw !== "object") return map;
|
|
10010
|
+
for (const [key, value] of Object.entries(raw)) if (typeof value === "string" && value.trim() !== "") map[key] = value.trim();
|
|
10011
|
+
return map;
|
|
10012
|
+
}
|
|
10013
|
+
/**
|
|
10014
|
+
* 取该 uri 的签名下载地址。空表 = 服务端认不出这个 key,此时图片已经
|
|
10015
|
+
* 生成并计费,却拿不到字节——如实抛 IMAGE_UNAVAILABLE,别谎报成功。
|
|
10016
|
+
*/
|
|
10017
|
+
function signedTraeResourceUrl(json, uri) {
|
|
10018
|
+
const map = parseTraeResourceUrlMap(json);
|
|
10019
|
+
const signed = map[uri] ?? map[uri.trim()];
|
|
10020
|
+
if (signed === void 0) throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但服务端没给出可下载地址(url_map 里没有这个 key):" + uri, "IMAGE_UNAVAILABLE");
|
|
10021
|
+
return signed;
|
|
10022
|
+
}
|
|
9955
10023
|
/** 应答里图片地址的字段名。活体看到的就是 uri;其余字段是 Rust 端
|
|
9956
10024
|
* ToolcallResultImage 的字段池(tos_uri/display_url/…),留给未来形态。 */
|
|
9957
10025
|
const TRAE_IMAGE_URL_FIELDS = [
|
|
@@ -10008,12 +10076,32 @@ function parseTraeImageResponse(json) {
|
|
|
10008
10076
|
}
|
|
10009
10077
|
//#endregion
|
|
10010
10078
|
//#region src/image-tool.ts
|
|
10079
|
+
/**
|
|
10080
|
+
* trae_draw_image:把 Trae 的文生图能力(客户端与 SOLO 共用的
|
|
10081
|
+
* /api/ide/v1/tool_text_to_image 端点)暴露成一个 DSH agent 工具。
|
|
10082
|
+
*
|
|
10083
|
+
* 与 dsh-connect-workbuddy 的 image-tool.ts 同一注册形态:手写 ToolDefinition、
|
|
10084
|
+
* 不 import 宿主类型(宿主 Tools.register 只校验 output.render 是函数、
|
|
10085
|
+
* output.schema 是受支持的 JSON Schema;parameters 原样投影给模型)。
|
|
10086
|
+
* 这样 bin 可达图不新增任何非 node: 裸说明符(tests/host-peers.spec.ts 的约束)。
|
|
10087
|
+
*
|
|
10088
|
+
* 作用域(v1,有意为之):
|
|
10089
|
+
* - 只做文生图。改图(image_urls 传参考图)需要先把本地文件经
|
|
10090
|
+
* get_resource_upload_url 预签名上传到 TOS,那是另一条流水线,未接。
|
|
10091
|
+
* - 只做文生图同步端点。SSE 端点与 video 端点的常量在 image-gen-trae.ts
|
|
10092
|
+
* 留档(tool_text_to_image_stream / tool_text_to_video_stream),
|
|
10093
|
+
* Trae 的视频结果还要轮询 query_material_result,等真机探针确认后再加。
|
|
10094
|
+
*
|
|
10095
|
+
* @module dsh-connect-trae/image-tool
|
|
10096
|
+
*/
|
|
10011
10097
|
/** 工具名。与其它 connect 插件的命名习惯一致。 */
|
|
10012
10098
|
const TRAE_IMAGE_TOOL_NAME = "trae_draw_image";
|
|
10013
10099
|
/** 单次生图上限。Trae 客户端里一张图 15-45 秒,留足余量。 */
|
|
10014
10100
|
const IMAGE_TOOL_TIMEOUT_MS = 18e4;
|
|
10015
10101
|
/** 失败后的冷却:生图按张扣积分,连续重试会白白烧积分。 */
|
|
10016
10102
|
const FAILURE_COOLDOWN_MS = 3e4;
|
|
10103
|
+
/** 图片落地目录名,相对会话工作目录。 */
|
|
10104
|
+
const TRAE_IMAGE_DIR = "trae-images";
|
|
10017
10105
|
function messageOf(error) {
|
|
10018
10106
|
return error instanceof Error ? error.message : String(error);
|
|
10019
10107
|
}
|
|
@@ -10025,8 +10113,22 @@ function mapHttpToImageError(status, detail) {
|
|
|
10025
10113
|
return new TraeImageError("trae image: 上游 HTTP " + status + tail, "UPSTREAM");
|
|
10026
10114
|
}
|
|
10027
10115
|
/**
|
|
10028
|
-
*
|
|
10029
|
-
*
|
|
10116
|
+
* 取图阶段(解析签名地址 / 下载字节)的 HTTP 失败。
|
|
10117
|
+
*
|
|
10118
|
+
* 这里**不能**复用 mapHttpToImageError:那套映射是给「生图那一跳」用的,401/403 报
|
|
10119
|
+
* AUTH、429 报 RATE_LIMIT 都还有意义。到了取图这一跳,图早已生成、积分早已扣掉,
|
|
10120
|
+
* 再把 403 说成「上游拒绝了凭据」就是谎报——用户会去重新登录,而钱已经花了、图还是
|
|
10121
|
+
* 拿不到。所以一律归成 IMAGE_UNAVAILABLE,文案保留状态码与原文便于排障,并附上对象
|
|
10122
|
+
* key。冷却判据只看 code,IMAGE_UNAVAILABLE 同样在冷却集合里,不会因为换了 code 就
|
|
10123
|
+
* 变成可重试(重试等于再买一张)。
|
|
10124
|
+
*/
|
|
10125
|
+
function mapRetrievalHttpError(status, detail, uri) {
|
|
10126
|
+
const tail = detail === "" ? "" : ":" + detail.slice(0, 200);
|
|
10127
|
+
return new TraeImageError("trae image: 图片已经生成、积分已经扣了,但取图接口回了 HTTP " + status + tail + "\n图片对象 key:" + uri, "IMAGE_UNAVAILABLE");
|
|
10128
|
+
}
|
|
10129
|
+
/**
|
|
10130
|
+
* 发一次真机文生图请求,并把图片字节一起取回来。所有可预期的失败都归一成
|
|
10131
|
+
* TraeImageError,让上层的冷却判据只看 code(见 createTraeDrawImageTool 的注释)。
|
|
10030
10132
|
*/
|
|
10031
10133
|
var TraeImageClient = class {
|
|
10032
10134
|
options;
|
|
@@ -10065,14 +10167,109 @@ var TraeImageClient = class {
|
|
|
10065
10167
|
throw new TraeImageError("trae image: 应答不是 JSON(HTTP 200):" + payload.slice(0, 200), "UPSTREAM");
|
|
10066
10168
|
}
|
|
10067
10169
|
const outcome = parseTraeImageResponse(json);
|
|
10068
|
-
const
|
|
10170
|
+
const signed = await this.resolveSignedUrl(account, outcome.url, requestSignal);
|
|
10171
|
+
const image = await this.download(signed, requestSignal, outcome.url);
|
|
10172
|
+
const result = {
|
|
10173
|
+
url: outcome.url,
|
|
10174
|
+
bytes: image.bytes,
|
|
10175
|
+
mimeType: image.mimeType
|
|
10176
|
+
};
|
|
10069
10177
|
if (outcome.credit !== void 0) result.credit = outcome.credit;
|
|
10070
10178
|
return result;
|
|
10071
10179
|
}
|
|
10180
|
+
/**
|
|
10181
|
+
* 把 tos key 解析成一个可直接 GET 的签名地址(byteimg CDN,带 x-expires)。
|
|
10182
|
+
* 这一步只是向服务端要地址,不会重新画图。
|
|
10183
|
+
*/
|
|
10184
|
+
async resolveSignedUrl(account, uri, signal) {
|
|
10185
|
+
const body = buildTraeResourceUrlBody(uri);
|
|
10186
|
+
const endpoint = traeEndpoint(account.baseUrl, TRAE_RESOURCE_URL_PATH);
|
|
10187
|
+
const headers = buildTraeHeaders(account.credential, account.identity, { profile: "model-detail" });
|
|
10188
|
+
const fetchImpl = this.options.fetchImpl ?? globalThis.fetch;
|
|
10189
|
+
const requestSignal = signal ?? AbortSignal.timeout(IMAGE_TOOL_TIMEOUT_MS);
|
|
10190
|
+
let response;
|
|
10191
|
+
try {
|
|
10192
|
+
response = await fetchImpl(endpoint, {
|
|
10193
|
+
method: "POST",
|
|
10194
|
+
headers,
|
|
10195
|
+
body: JSON.stringify(body),
|
|
10196
|
+
signal: requestSignal
|
|
10197
|
+
});
|
|
10198
|
+
} catch (error) {
|
|
10199
|
+
throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但解析下载地址的请求发不出去:" + messageOf(error), "IMAGE_UNAVAILABLE");
|
|
10200
|
+
}
|
|
10201
|
+
let payload;
|
|
10202
|
+
try {
|
|
10203
|
+
payload = await response.text();
|
|
10204
|
+
} catch (error) {
|
|
10205
|
+
throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但读取解析应答失败:" + messageOf(error), "IMAGE_UNAVAILABLE");
|
|
10206
|
+
}
|
|
10207
|
+
if (!response.ok) throw mapRetrievalHttpError(response.status, payload, uri);
|
|
10208
|
+
let json;
|
|
10209
|
+
try {
|
|
10210
|
+
json = JSON.parse(payload);
|
|
10211
|
+
} catch {
|
|
10212
|
+
throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但解析应答不是 JSON(HTTP 200):" + payload.slice(0, 200), "IMAGE_UNAVAILABLE");
|
|
10213
|
+
}
|
|
10214
|
+
if (json === null || typeof json !== "object") throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但解析应答里没有 url_map:" + payload.slice(0, 200), "IMAGE_UNAVAILABLE");
|
|
10215
|
+
return signedTraeResourceUrl(json, uri);
|
|
10216
|
+
}
|
|
10217
|
+
/** 下载签名地址上的图片字节。CDN 不需要任何 Trae 头——签名就在 query 里。 */
|
|
10218
|
+
async download(signed, signal, uri) {
|
|
10219
|
+
const fetchImpl = this.options.fetchImpl ?? globalThis.fetch;
|
|
10220
|
+
const requestSignal = signal ?? AbortSignal.timeout(IMAGE_TOOL_TIMEOUT_MS);
|
|
10221
|
+
let response;
|
|
10222
|
+
try {
|
|
10223
|
+
response = await fetchImpl(signed, {
|
|
10224
|
+
method: "GET",
|
|
10225
|
+
headers: { Accept: "image/*" },
|
|
10226
|
+
signal: requestSignal
|
|
10227
|
+
});
|
|
10228
|
+
} catch (error) {
|
|
10229
|
+
throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但下载字节失败:" + messageOf(error), "IMAGE_UNAVAILABLE");
|
|
10230
|
+
}
|
|
10231
|
+
if (!response.ok) {
|
|
10232
|
+
const key = uri === void 0 || uri === "" ? "" : "\n图片对象 key:" + uri;
|
|
10233
|
+
throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但下载地址回了 HTTP " + response.status + "。" + key, "IMAGE_UNAVAILABLE");
|
|
10234
|
+
}
|
|
10235
|
+
let buffer;
|
|
10236
|
+
try {
|
|
10237
|
+
buffer = await response.arrayBuffer();
|
|
10238
|
+
} catch (error) {
|
|
10239
|
+
throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但读取图片字节失败:" + messageOf(error), "IMAGE_UNAVAILABLE");
|
|
10240
|
+
}
|
|
10241
|
+
if (buffer.byteLength === 0) throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但下载回来是 0 字节。", "IMAGE_UNAVAILABLE");
|
|
10242
|
+
const mimeType = (response.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
|
|
10243
|
+
const sniffed = imageMimeTypeOf(new Uint8Array(buffer));
|
|
10244
|
+
if (sniffed === void 0) throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但下载回来的不是图片(content-type " + (mimeType === "" ? "缺失" : mimeType) + ",前 4 字节 " + hexPrefix(new Uint8Array(buffer)) + ")。" + (uri === void 0 || uri === "" ? "" : "\n图片对象 key:" + uri), "IMAGE_UNAVAILABLE");
|
|
10245
|
+
return {
|
|
10246
|
+
bytes: new Uint8Array(buffer),
|
|
10247
|
+
mimeType: sniffed
|
|
10248
|
+
};
|
|
10249
|
+
}
|
|
10072
10250
|
};
|
|
10073
10251
|
function asRecord(value) {
|
|
10074
10252
|
return typeof value === "object" && value !== null ? value : {};
|
|
10075
10253
|
}
|
|
10254
|
+
/**
|
|
10255
|
+
* 从字节魔数判断图片类型(JPEG / PNG / GIF / WebP / BMP)。
|
|
10256
|
+
*
|
|
10257
|
+
* 为什么不信 content-type:签名地址过期或被劫持时,CDN 常拿 200 回一页 HTML。
|
|
10258
|
+
* 那样写下去的文件扩展名是 .jpg 却打不开,用户只看到碎图标,而积分已经扣了。
|
|
10259
|
+
* 认不出就返回 undefined,由调用方如实报 IMAGE_UNAVAILABLE。
|
|
10260
|
+
*/
|
|
10261
|
+
function imageMimeTypeOf(bytes) {
|
|
10262
|
+
const at = (index) => bytes[index] ?? -1;
|
|
10263
|
+
if (at(0) === 255 && at(1) === 216 && at(2) === 255) return "image/jpeg";
|
|
10264
|
+
if (at(0) === 137 && at(1) === 80 && at(2) === 78 && at(3) === 71) return "image/png";
|
|
10265
|
+
if (at(0) === 71 && at(1) === 73 && at(2) === 70) return "image/gif";
|
|
10266
|
+
if (at(0) === 82 && at(1) === 73 && at(2) === 70 && at(3) === 70 && at(8) === 87 && at(9) === 69 && at(10) === 66 && at(11) === 80) return "image/webp";
|
|
10267
|
+
if (at(0) === 66 && at(1) === 77) return "image/bmp";
|
|
10268
|
+
}
|
|
10269
|
+
/** 前若干字节的十六进制,只用于排障文案。 */
|
|
10270
|
+
function hexPrefix(bytes) {
|
|
10271
|
+
return [...bytes.slice(0, 4)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
10272
|
+
}
|
|
10076
10273
|
function asNonEmptyString(value) {
|
|
10077
10274
|
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
10078
10275
|
}
|
|
@@ -10086,7 +10283,8 @@ const BILLABLE_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
|
10086
10283
|
"RATE_LIMIT",
|
|
10087
10284
|
"NO_ROUTE",
|
|
10088
10285
|
"UPSTREAM",
|
|
10089
|
-
"EMPTY_RESULT"
|
|
10286
|
+
"EMPTY_RESULT",
|
|
10287
|
+
"IMAGE_UNAVAILABLE"
|
|
10090
10288
|
]);
|
|
10091
10289
|
function isBillableFailure(error) {
|
|
10092
10290
|
if (error instanceof TraeImageError) return BILLABLE_FAILURE_CODES.has(error.code);
|
|
@@ -10097,7 +10295,7 @@ function createTraeDrawImageTool(deps) {
|
|
|
10097
10295
|
let cooldownUntil = 0;
|
|
10098
10296
|
return {
|
|
10099
10297
|
name: TRAE_IMAGE_TOOL_NAME,
|
|
10100
|
-
description: "用当前登录的 Trae 账号文生图(TraeWork/SOLO 同款服务,模型 seedream_5)。只在用户明确要一张图时调用:每张真实扣该账号积分,实测 CN 个人版 1920x1920 一张约 8.8 积分(尺寸越大越贵),而上游不回传扣费数字,以 Trae 额度面板为准。不是常开能力:只有插件设置里为该区打开「允许模型自主生图」(drawImageEnabled)
|
|
10298
|
+
description: "用当前登录的 Trae 账号文生图(TraeWork/SOLO 同款服务,模型 seedream_5)。只在用户明确要一张图时调用:每张真实扣该账号积分,实测 CN 个人版 1920x1920 一张约 8.8 积分(尺寸越大越贵),而上游不回传扣费数字,以 Trae 额度面板为准。不是常开能力:只有插件设置里为该区打开「允许模型自主生图」(drawImageEnabled) 时本工具才存在,没打开就没得调——不要用报错去试探它开没开。取图口径:生成成功后本工具会把图片字节取回来、写进会话工作目录的 trae-images/ 下,结果里给相对路径 path,渲染层已经据此把图显示给用户了——不需要你再写 markdown。取图走 get_resource_url 解析签名地址(只是要地址,不会重新生成、不额外计费)。不要为了\"取图\"或\"换张\"重跑同一条 prompt(那会再扣一张)。失败码 IMAGE_UNAVAILABLE 表示图已生成、积分已扣、但字节没落地:同样不要重跑。失败后有 30 秒冷却,冷却内重试不会打到上游。尺寸:默认 1920x1920,也可给 宽x高(总像素需在 3686400~16777216 之间,如 2048x2048)或预设 2k/3k/4k;过小(如 1024x1024)会被上游直接拒绝。(改图与文生视频暂未开放:改图需要先走 TOS 上传预签名,视频要轮询素材结果。)",
|
|
10101
10299
|
parameters: {
|
|
10102
10300
|
type: "object",
|
|
10103
10301
|
properties: {
|
|
@@ -10119,7 +10317,11 @@ function createTraeDrawImageTool(deps) {
|
|
|
10119
10317
|
properties: {
|
|
10120
10318
|
url: {
|
|
10121
10319
|
type: "string",
|
|
10122
|
-
description: "Trae 图片对象 key(tos uri
|
|
10320
|
+
description: "Trae 图片对象 key(tos uri),留作记录与排障,不是可直接打开的 URL。"
|
|
10321
|
+
},
|
|
10322
|
+
path: {
|
|
10323
|
+
type: "string",
|
|
10324
|
+
description: "图片文件相对会话工作目录的路径(如 trae-images/trae-20261026-041500-ab12cd.jpg);渲染层已据此显示图片。"
|
|
10123
10325
|
},
|
|
10124
10326
|
model: {
|
|
10125
10327
|
type: "string",
|
|
@@ -10135,7 +10337,7 @@ function createTraeDrawImageTool(deps) {
|
|
|
10135
10337
|
},
|
|
10136
10338
|
hint: {
|
|
10137
10339
|
type: "string",
|
|
10138
|
-
description: "
|
|
10340
|
+
description: "给模型的后续提示(图片位置、扣费口径)。"
|
|
10139
10341
|
},
|
|
10140
10342
|
credit: {
|
|
10141
10343
|
type: "number",
|
|
@@ -10144,6 +10346,7 @@ function createTraeDrawImageTool(deps) {
|
|
|
10144
10346
|
},
|
|
10145
10347
|
required: [
|
|
10146
10348
|
"url",
|
|
10349
|
+
"path",
|
|
10147
10350
|
"model",
|
|
10148
10351
|
"prompt",
|
|
10149
10352
|
"size",
|
|
@@ -10154,15 +10357,13 @@ function createTraeDrawImageTool(deps) {
|
|
|
10154
10357
|
render(_args, value) {
|
|
10155
10358
|
const record = asRecord(value);
|
|
10156
10359
|
const url = asNonEmptyString(record["url"]) ?? "";
|
|
10360
|
+
const path = asNonEmptyString(record["path"]) ?? "";
|
|
10157
10361
|
const model = asNonEmptyString(record["model"]) ?? "unknown";
|
|
10158
10362
|
const size = asNonEmptyString(record["size"]) ?? "";
|
|
10159
10363
|
const credit = typeof record["credit"] === "number" ? record["credit"] : void 0;
|
|
10160
10364
|
const hint = asNonEmptyString(record["hint"]) ?? "";
|
|
10161
|
-
const lines = [
|
|
10162
|
-
|
|
10163
|
-
"",
|
|
10164
|
-
"文生图 · 模型:" + model + (size === "" ? "" : " · 尺寸:" + size) + (credit === void 0 ? "" : " · 消耗 " + credit + " 积分")
|
|
10165
|
-
];
|
|
10365
|
+
const lines = path === "" ? ["图片已生成,但字节没取回来(只有对象 key):`" + url + "`"] : [""];
|
|
10366
|
+
lines.push("", "文生图 · 模型:" + model + (size === "" ? "" : " · 尺寸:" + size) + (credit === void 0 ? "" : " · 消耗 " + credit + " 积分"));
|
|
10166
10367
|
if (hint !== "") lines.push("", hint);
|
|
10167
10368
|
return [{
|
|
10168
10369
|
type: "text",
|
|
@@ -10186,12 +10387,14 @@ function createTraeDrawImageTool(deps) {
|
|
|
10186
10387
|
prompt,
|
|
10187
10388
|
size
|
|
10188
10389
|
}, signal);
|
|
10390
|
+
const saved = await saveTraeImage(await deps.directoryOf?.(exec) ?? sessionCwdOf(exec) ?? process.cwd(), outcome);
|
|
10189
10391
|
const value = {
|
|
10190
10392
|
url: outcome.url,
|
|
10393
|
+
path: saved.relativePath,
|
|
10191
10394
|
model: TRAE_IMAGE_MODEL,
|
|
10192
10395
|
prompt,
|
|
10193
10396
|
size,
|
|
10194
|
-
hint: "这张图来自 Trae 文生图(模型 seedream_5),按张计入该账号积分(实测 1920x1920 约 8.8,上游不回传数字,以 Trae
|
|
10397
|
+
hint: "这张图来自 Trae 文生图(模型 seedream_5),按张计入该账号积分(实测 1920x1920 约 8.8,上游不回传数字,以 Trae 额度面板为准)。图片已保存到会话目录下的 " + saved.relativePath + "(绝对路径 " + saved.absolutePath + "),渲染层已经把它显示给用户了,不需要你再写 markdown。不要为了取图或换张重跑同一条 prompt——那会再扣一张。"
|
|
10195
10398
|
};
|
|
10196
10399
|
if (outcome.credit !== void 0) value.credit = outcome.credit;
|
|
10197
10400
|
return value;
|
|
@@ -10202,6 +10405,52 @@ function createTraeDrawImageTool(deps) {
|
|
|
10202
10405
|
}
|
|
10203
10406
|
};
|
|
10204
10407
|
}
|
|
10408
|
+
/** 从 exec 里读会话工作目录(宿主没给就返回 undefined)。 */
|
|
10409
|
+
function sessionCwdOf(exec) {
|
|
10410
|
+
const session = exec.agent?.session;
|
|
10411
|
+
return nonEmptyPath(session?.workingDirectory) ?? nonEmptyPath(session?.header?.cwd) ?? nonEmptyPath(session?.cwd) ?? nonEmptyPath(exec.cwd);
|
|
10412
|
+
}
|
|
10413
|
+
/** 只在真拿到非空字符串时返回它(空串/空白/null 一律当「不知道」)。 */
|
|
10414
|
+
function nonEmptyPath(value) {
|
|
10415
|
+
if (typeof value !== "string") return void 0;
|
|
10416
|
+
return value.trim() === "" ? void 0 : value;
|
|
10417
|
+
}
|
|
10418
|
+
/** mime → 文件后缀(上游目前只回 image/jpeg)。 */
|
|
10419
|
+
function imageExtensionFor(mimeType) {
|
|
10420
|
+
const type = mimeType.toLowerCase();
|
|
10421
|
+
if (type.includes("png")) return "png";
|
|
10422
|
+
if (type.includes("webp")) return "webp";
|
|
10423
|
+
if (type.includes("gif")) return "gif";
|
|
10424
|
+
return "jpg";
|
|
10425
|
+
}
|
|
10426
|
+
/** 文件名:本地时间的可排序时间戳 + 随机后缀,同一秒两张图也不会互相覆盖。 */
|
|
10427
|
+
function imageFileName(mimeType, now) {
|
|
10428
|
+
const date = new Date(now);
|
|
10429
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
10430
|
+
const stamp = String(date.getFullYear()) + pad(date.getMonth() + 1) + pad(date.getDate()) + "-" + pad(date.getHours()) + pad(date.getMinutes()) + pad(date.getSeconds());
|
|
10431
|
+
const rand = Math.random().toString(16).slice(2, 8);
|
|
10432
|
+
return "trae-" + stamp + "-" + rand + "." + imageExtensionFor(mimeType);
|
|
10433
|
+
}
|
|
10434
|
+
/**
|
|
10435
|
+
* 把图片字节写成 <cwd>/trae-images/<name>,返回绝对路径与相对路径。
|
|
10436
|
+
* 相对路径给 render 用:DSH 的聊天按 session cwd 解析 markdown 图片地址,
|
|
10437
|
+
* 再过 api/file 鉴权路由取字节,所以相对路径就能显示。
|
|
10438
|
+
*/
|
|
10439
|
+
async function saveTraeImage(cwd, outcome) {
|
|
10440
|
+
const name = imageFileName(outcome.mimeType, Date.now());
|
|
10441
|
+
const directory = join(cwd, TRAE_IMAGE_DIR);
|
|
10442
|
+
try {
|
|
10443
|
+
await mkdir(directory, { recursive: true });
|
|
10444
|
+
const absolutePath = join(directory, name);
|
|
10445
|
+
await writeFile(absolutePath, outcome.bytes);
|
|
10446
|
+
return {
|
|
10447
|
+
absolutePath,
|
|
10448
|
+
relativePath: "trae-images/" + name
|
|
10449
|
+
};
|
|
10450
|
+
} catch (error) {
|
|
10451
|
+
throw new TraeImageError("trae image: 图片已经生成、积分已经扣了,但写入本地文件失败(目录 " + directory + "):" + messageOf(error) + "\n图片对象 key:" + outcome.url, "IMAGE_UNAVAILABLE");
|
|
10452
|
+
}
|
|
10453
|
+
}
|
|
10205
10454
|
/**
|
|
10206
10455
|
* 把工具注册进宿主。返回 disposer;调用方负责在插件卸载时调用它。
|
|
10207
10456
|
* 注册失败只记警告——生图工具缺失不该让整个插件起不来(同 workbuddy)。
|
|
@@ -11515,9 +11764,29 @@ function apply(ctx, config) {
|
|
|
11515
11764
|
}
|
|
11516
11765
|
},
|
|
11517
11766
|
generate: (account, request, signal) => drawImageClient.generate(account, request, signal),
|
|
11767
|
+
directoryOf: (exec) => ensureSessionDirectory === void 0 ? void 0 : ensureSessionDirectory(exec),
|
|
11518
11768
|
logger: ctx.logger
|
|
11519
11769
|
});
|
|
11520
11770
|
};
|
|
11771
|
+
let ensureSessionDirectory;
|
|
11772
|
+
ctx.inject(["workingDirectory"], (directoryCtx) => {
|
|
11773
|
+
const service = directoryCtx.workingDirectory;
|
|
11774
|
+
if (service === void 0 || typeof service.ensure !== "function") return;
|
|
11775
|
+
const ensure = service.ensure.bind(service);
|
|
11776
|
+
ensureSessionDirectory = async (exec) => {
|
|
11777
|
+
const agent = exec.agent;
|
|
11778
|
+
if (agent === void 0) return void 0;
|
|
11779
|
+
try {
|
|
11780
|
+
const directory = await ensure(agent, exec.signal);
|
|
11781
|
+
return typeof directory === "string" && directory.trim() !== "" ? directory : void 0;
|
|
11782
|
+
} catch {
|
|
11783
|
+
return;
|
|
11784
|
+
}
|
|
11785
|
+
};
|
|
11786
|
+
directoryCtx.effect(() => () => {
|
|
11787
|
+
ensureSessionDirectory = void 0;
|
|
11788
|
+
});
|
|
11789
|
+
});
|
|
11521
11790
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
11522
11791
|
drawImageTools = toolsCtx.tools;
|
|
11523
11792
|
if (drawImageTools === void 0) return;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-trae",
|
|
3
3
|
"displayName": "DSH Connect Trae",
|
|
4
4
|
"description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
|
|
5
|
-
"version": "4.6.
|
|
5
|
+
"version": "4.6.2",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -93,6 +93,11 @@
|
|
|
93
93
|
"@earendil-works/pi-ai": ">=0.85.1 <2.0.0",
|
|
94
94
|
"react": "^18.2.0"
|
|
95
95
|
},
|
|
96
|
+
"peerDependenciesMeta": {
|
|
97
|
+
"@earendil-works/pi-ai": {
|
|
98
|
+
"optional": true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
96
101
|
"devDependencies": {
|
|
97
102
|
"@deepseek-ai/cordis": "^4.0.5",
|
|
98
103
|
"@deepseek-ai/dsh-atomic-write": "0.2.1-rc.3",
|