dsh-connect-trae 4.5.2 → 4.6.0

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 CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.6.0 (2026-10-26)
4
+
5
+ > **文生图先只在国内版面世,并且必须由用户亲手勾选。** 企业版的租户网关是另一条
6
+ > 计费面,国际版按另一套套餐计费,两者的生图行为都没有测过——所有取证都在
7
+ > `trae-api-cn.mchost.guru` 上做的。
8
+
9
+ ### 新增
10
+
11
+ - **文生图接进会话:`trae_draw_image`**(`feat(tools)`)。
12
+ - 用登录账号画一张图,返回 Trae 的**图片对象 key**(形如 `tos-cn-i-…/cn/<uid>/gen_image/…`,
13
+ 图在 Trae 客户端的历史里能看到;字节插件拿不到,见下)。默认关闭、**按面板单独开**:
14
+ 卡片模型栏勾上「允许模型自主生图」(`drawImageEnabled`)该面板才注册这个工具,
15
+ 取消勾选立即撤下。
16
+ - **协议是实测出来的,不是从二进制里猜的**:`POST /api/ide/v1/tool_text_to_image`
17
+ (另有 SSE 变体),请求体**只认** `{prompt, image_size?}`——多带 `mode_type`/`session_id`/
18
+ `image_urls` 之类的已知字段直接 400 空 body,未知字段反被忽略;成功响应是**扁平**的
19
+ `{"message":"success","uri":"tos-cn-i-…"}`,没有 `{code,msg,data}` 信封。模型 `seedream_5`。
20
+ - **尺寸边界**:总像素必须落在 **3686400 ~ 16777216**(`1024x1024` 报
21
+ `image size must be at least 3686400 pixels`、`4608x4608` 报 at most,两者都**不扣费**);
22
+ 预设 `2k`/`3k`/`4k` 可用,缺省 `1920x1920`。
23
+ - **计费口径写进工具文案**:实测 `1920x1920` 一张约 **8.8 积分**(国内版价),上游不回传
24
+ 扣费数字。失败后加 **30 秒冷却**(按张计费,不过冷却的重试等于再买一张)。
25
+ - **不取图,只回 key**:同路径的 GET 不是取图,**是又一次生成**。决定性探针(CN 个人版):
26
+ POST 拿到 `…/1792954795741_44f94116_jpeg_1920x1920` 之后,用同一 `(prompt, size)` 带
27
+ 全套网关头 + `x-ide-sign` 打 GET,回来的是**另一个** uri
28
+ (`…/1792954810505_b107ac11_jpeg_1920x1920`),耗时 27 秒、**一个像素都没有**——等于又画
29
+ 一张、又扣一次(两次共约 17.6)。非法尺寸探针(`100x100`)同样印证:报错直接来自 Ark 的
30
+ `[callArkImageGeneration]`。所以发版代码**不再发这条 GET**:`generate` 只发一次 POST,
31
+ 返回 `{url, credit?}`,渲染层把 key 显示成**行内代码**并注明「不能直接打开」,绝不拼进
32
+ `![](...)`(key 不是路径,拼进去只会得到以当前目录为基准的碎图标)。要真正把 uri 变成
33
+ 字节,需要另一条通道(iCube material 的 `get_url` + `x-icube-token`,基址来自运行时
34
+ boot config),目前**没有打通**,代码里也不留假装能取的入口。
35
+ 两例 `tests/image-tool-render.spec.ts` 与两例 `tests/image-tool-download.spec.ts`
36
+ (现已改名口径)用 stub fetch 断言**每次生成只发一次请求**。
37
+ - **只有国内版面板提供**(`DRAW_IMAGE_PANELS = ['cn']`,见 `src/status-paths.ts`)。
38
+ 企业版走租户网关、国际版按另一套套餐计费,两者的生图行为都**没有测过**:企业版原先
39
+ 就因为这一点被拒绝,国际版没有理由凭「面板是消费者版」放行——而且工具文案里的
40
+ 8.8 积分是国内价,放到国际版就是错的。两处的勾选框都**置灰并写明原因**(不隐藏:
41
+ 隐藏会让人以为插件漏了这个功能),理由与「加进白名单必须先补实测证据」写在同一处注释里。
42
+ - **注册开关是一个可测的纯函数**:`drawImagePanelsOf()` 三道门——面板在白名单内、
43
+ 该面板的 provider 开着(opt-out 语义,旧配置仍算开)、用户显式勾选(**只认字面量
44
+ `true`**,`'yes'`、`1` 之类的手改值不算同意)。企业版即使被勾上也**结构性**拒绝,
45
+ 不靠白名单列表的书写顺序。首版把这条规则写在 `apply()` 的闭包里,只能靠读代码确认,
46
+ 现在由 `tests/image-tool-optin.spec.ts` 的 12 例钉住(缺省/非法形状/非布尔真值/关掉的
47
+ provider/企业版/国际版/多面板顺序/整段 section 与裸 `regions` map 同答)。
48
+ - **开关就在卡片里,不是一个只有 schema 的字段**:真值是 `regions.<panel>.drawImageEnabled`
49
+ (Host 的工具解析按面板读它,所以写的是 `regions` 而不是顶层设置键)。勾选即写、不经过
50
+ 「保存」,写后**回读文档**核对——一个谎报授权的勾选框会让「工具没出现」看起来像 bug。
51
+ 4 例卡片测试 `tests/card-draw-image-switch.spec.tsx` 断言写入字段是 `regions`、该槽位的
52
+ 模型目录/图片模型/上下文预算/账号池一个都不能丢。
53
+ - **范围**:改图(需先走 TOS 上传预签名)与文生视频(异步素材轮询)**不做**,也不留半截入口。
54
+
55
+ ### 文档
56
+
57
+ - `README.md` / `README.en.md` 新增「文生图」一节与功能列表条目:开关语义(含只有国内版
58
+ 提供)、实测协议表、计费,以及为什么那条「取图」GET **是又一次生成**、插件只回 key。
59
+
3
60
  ## 4.5.2 (2026-10-25)
4
61
 
5
62
  > **思考档位不再被吞,也不再被谎报。** 插件原先只认识三档,未知档位一律按 `high`
package/README.en.md CHANGED
@@ -42,6 +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 hands back the Trae object key (the image itself is visible in the Trae client; the bytes are not reachable from the plugin). **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 and why the "fetch" GET is really a second generation are in "Text-to-image" below.
45
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.
46
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.
47
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.
@@ -115,6 +116,27 @@ Notes:
115
116
  - **An automatically enabled row is shown ticked and read-only.** Under the union, unticking it would be re-applied immediately, so offering a clickable box would be a lie. There is currently no per-model "force off" escape hatch; if you want one, say so on issue #16.
116
117
  - Manual ticks still work for models Trae marks `false` (or does not mark), which is the escape hatch for "upstream does not say so, but it actually works".
117
118
 
119
+ ## Text-to-image (`trae_draw_image`)
120
+
121
+ The text-to-image service behind TraeWork / SOLO is now reachable from a DSH conversation: the model **draws one image** and the result is written to a local file.
122
+
123
+ **Off by default, switched on per panel.** Generation **really spends credits** (measured: ~8.8 for one `1920x1920` on CN personal), so this is not the image-input kind of declaration — it is an explicit spend gate. Tick "Allow the model to generate images" (`drawImageEnabled`) in the card's model column and that panel's `trae_draw_image` gets registered with DSH; unticking removes it immediately. Each panel stores its own switch; the enterprise and international panels do not offer one — the former's tenant gateway is a separate billing surface, the latter bills under another plan, and neither has been measured, so the plugin does not pretend otherwise.
124
+
125
+ **The protocol (all measured, none of it guessed from the binary):**
126
+
127
+ | Item | Fact |
128
+ |---|---|
129
+ | Endpoint | `POST /api/ide/v1/tool_text_to_image` (plus an SSE variant `..._stream`), model `seedream_5` |
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
+ | Success | Flat `{"message":"success","uri":"tos-cn-i-…/cn/<uid>/gen_image/<ts>_<hex>_jpeg_1920x1920"}` — **no** `{code,msg,data}` envelope |
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 | **There is no working fetch channel.** The `uri` is an object key, not an openable URL, and the same-path GET is not a fetch but another generation (next row). So the tool returns the key and nothing else; the picture is visible in the Trae client's history |
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
+
136
+ **Key only, no fetch.** On success the tool returns `url` (the Trae object key, shaped `tos-cn-i-…/cn/<uid>/gen_image/…`), `model`, `prompt`, `size` and a `hint`; the renderer shows the key as **inline code** labelled "cannot be opened directly" and never splices it into `![](...)` — a key is not a path, and pasting it there only yields a broken icon resolved against the cwd. Missing bytes are a capability boundary here, not an error: turning a uri into pixels needs another channel (iCube material `get_url` plus `x-icube-token`, base URL from the runtime boot config) that is not wired up yet. Failures still start a 30-second cooldown — generation is billed per image, so a retry buys another one.
137
+
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
+
118
140
  ## Model coverage
119
141
 
120
142
  This plugin serves models from Trae's **SOLO channel** (`DeepSeek-V4-Flash-Official`, `DeepSeek-V4-Pro-Official`, `GLM-5.3`, `GLM-5.2`, `Kimi-K3`, `MiniMax-M3`, `Qwen3.8-Max`, `Doubao-Seed-*`, …), for both the domestic and international sides.
package/README.md CHANGED
@@ -46,6 +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 文生图能力接进会话:模型画一张图,插件回你 Trae 的图片对象 key(图在 Trae 客户端里看,字节插件拿不到)。**默认关闭、按面板单独开**——生图真实消耗积分(实测 CN 个人版 `1920x1920` 一张约 8.8),所以它和识图那种「让不让看图」的开关不同,只能由你点头。协议、尺寸边界,以及为什么「取图」那条 GET 其实是又一次生成,见下方「[文生图](#文生图trae_draw_image)」。
49
50
  - **国内国际双区域自动识别** —— 自动发现 Trae CN / TRAE SOLO CN / Trae / TRAE SOLO 四个本地安装的登录账号;区域由凭证自带的 `userRegion` 声明自动判定(host 后缀与 edition 标签兜底),无需手动指定。
50
51
  - **目录与勾选按面板隔离** —— 三个面板各一套模型目录、勾选、图片开关与上下文预算,各自读各自的槽位。名录从**该面板所钉的那条网关**读取——原本钉在企业池的客户端去读消费池名录,会永远发现不了自己池的独占模型。
51
52
  - **识图能力跟随 Trae 自身的标注** —— Trae 把某个模型标为 `multimodal` 时,插件直接按多模态对外声明,无需你在卡片上逐一手勾(issue #16)。判据是「**你手动勾选的 ∪ Trae 标注多模态的**」:Trae 明确标 `false` 或未标注的模型保持纯文本,你可以照旧手动为它们打开图片输入。卡片上被自动开启的那一行显示为已勾选且只读,并说明原因。详见下方「图片输入(识图)」。
@@ -120,6 +121,27 @@ DSH PiAiAdapter(每个 provider 一套)
120
121
  - **被自动开启的行在卡片上显示为已勾选且只读。** 因为并集语义下取消勾选会被立刻重新加上,给它一个可点的框是在撒谎。目前不为单个模型提供「强制关闭」的出口;如果你需要这个开关,可以在 issue #16 下提出。
121
122
  - 手动勾选对 Trae 标注 `false`(或未标注)的模型依然有效,这是为那些「上游没标但实际可用」的情况留的口子。
122
123
 
124
+ ## 文生图(`trae_draw_image`)
125
+
126
+ TraeWork / SOLO 侧那套文生图服务带出来了:在会话里让模型**画一张图**。插件拿得到的是 Trae 的图片对象 key(图在 Trae 客户端里看),拿不到图片字节——原因见下。
127
+
128
+ **默认关闭,按面板单独开。** 生图**真实消耗积分**(实测 CN 个人版 `1920x1920` 一张约 8.8 积分),所以它不是识图那种「让不让看图」的声明,而是一个要你点头的支出口:卡片模型栏勾上「允许模型自主生图」(`drawImageEnabled`),该面板的 `trae_draw_image` 才注册进 DSH;取消勾选立即撤下。面板各存各的开关;企业版与国际版面板不提供——企业版租户网关是另一条计费面,国际版按另一套套餐计费,两者的生图行为都没有测过,不在这里假装支持。
129
+
130
+ **口径(全部实测,不是从二进制里猜的):**
131
+
132
+ | 项目 | 事实 |
133
+ |---|---|
134
+ | 端点 | `POST /api/ide/v1/tool_text_to_image`(另有 SSE 变体 `..._stream`),模型 `seedream_5` |
135
+ | 请求体 | 只有 `{prompt, image_size?}`。多带已知字段(`mode_type` / `session_id` / `image_urls` / `watermark`)网关直接回 400 空 body——未知字段反而被忽略 |
136
+ | 成功响应 | 扁平结构 `{"message":"success","uri":"tos-cn-i-…/cn/<uid>/gen_image/<ts>_<hex>_jpeg_1920x1920"}`,**没有** `{code,msg,data}` 信封 |
137
+ | 尺寸 | 总像素必须落在 **3686400 ~ 16777216**:`1024x1024` 太小被拒(`code 3003`,不扣费)、`4608x4608` 太大被拒;预设 `2k`/`3k`/`4k` 可用,缺省 `1920x1920` |
138
+ | 取图 | **没有可用的取图通道。** `uri` 是对象 key,不是可直接打开的 URL;同路径的 GET 不是取图,是又一次生成(见下一行),所以工具**只回 key、不再请求**,把 key 原样交给用户,图在 Trae 客户端的历史里能看到 |
139
+ | 那条 GET 的真实身份 | 决定性探针:POST 拿到 `…/1792954795741_44f94116_jpeg_1920x1920` 后,用同一 `(prompt, size)` 打 GET,回来的是**另一个** uri(`…/1792954810505_b107ac11_jpeg_1920x1920`),耗时 27 秒、一个像素都没有——等于又画一张、再扣一次(两次共约 17.6)。非法尺寸探针(`100x100`)也印证:报错直接来自 Ark 的 `[callArkImageGeneration]` |
140
+
141
+ **只回 key,不取图。** 工具成功时返回 `url`(Trae 对象 key,形如 `tos-cn-i-…/cn/<uid>/gen_image/…`)、`model`、`prompt`、`size` 和一句 `hint`;渲染层把 key 显示成**行内代码**并注明「不能直接打开」,绝不拼进 `![](...)`——key 不是路径,拼进去只会得到以当前目录为基准的碎图标。取不到字节这件事在这里不是错误,是能力边界:真的取字节要另一条通道(iCube material 的 `get_url` + `x-icube-token`,基址来自运行时 boot config),还没打通。失败仍有 30 秒冷却——生图按张计费,重试等于再买一张。
142
+
143
+ **改图与文生视频没有放开。** 改图要先走 TOS 上传预签名(本插件没有这条链路);视频要轮询素材结果,是另一条异步管线。两者都不做,也不留半截入口。
144
+
123
145
  ## 模型覆盖范围
124
146
 
125
147
  插件提供的是 Trae **SOLO 通道**的模型(`DeepSeek-V4-Flash-Official`、`DeepSeek-V4-Pro-Official`、`GLM-5.3`、`GLM-5.2`、`Kimi-K3`、`MiniMax-M3`、`Qwen3.8-Max`、`Doubao-Seed-*` 等),三个面板均走这条通道。企业池另有一份名录(例如 `DeepSeek-V4.1-Flash` 只有企业池列),所以两边勾选互不覆盖。
package/lib/client.js CHANGED
@@ -369,6 +369,48 @@ window.__ModuleLoader__.load({
369
369
  });
370
370
  }
371
371
  /**
372
+ * Panels whose text-to-image path this plugin has actually MEASURED, in
373
+ * `TRAE_PANELS` tab order.
374
+ *
375
+ * ONE entry today. The other two are refused for the SAME reason — no evidence
376
+ * — which is exactly the asymmetry the review of #62 held the PR on:
377
+ *
378
+ * - `enterprise` bills through the tenant gateway, a second billing surface
379
+ * whose image behaviour has never been probed;
380
+ * - `ai` answers on `coresg-normal.trae.ai`, where no `tool_text_to_image`
381
+ * request has ever run either — every measurement behind this tool was
382
+ * taken against `trae-api-cn.mchost.guru`. Refusing one unverified gateway
383
+ * while serving another is not a rule, it is an accident.
384
+ *
385
+ * Adding a panel means adding its evidence in the same change (the precedent
386
+ * for international claims is `docs/INTL_SG_EVIDENCE.md`). Order is
387
+ * significant: the tool's resolver walks `TRAE_PANELS` order and takes the
388
+ * FIRST consenting panel, so `cn` — the only measured one — always answers
389
+ * first.
390
+ */
391
+ const DRAW_IMAGE_PANELS = ["cn"];
392
+ /**
393
+ * Build the next `regions` settings value for one panel's 「允许模型自主生图」
394
+ * switch (`drawImageEnabled`, `trae_draw_image`).
395
+ *
396
+ * Same merge rule as every sibling region write: only the addressed panel's
397
+ * `drawImageEnabled` changes, so flipping the switch carries that slot's model
398
+ * directory, selection, budgets, pool and credit seat through, and leaves the
399
+ * other panels alone. The flag is a PANEL's consent, not the account's: cn and
400
+ * enterprise bill the same Trae CN account but route through different gateways,
401
+ * so consent must not cross the boundary (the tool resolver reads it per panel).
402
+ *
403
+ * `value` may be the whole settings section or the `regions` map; the RETURN
404
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
405
+ * 'regions', ...)` needs.
406
+ */
407
+ function nextRegionDrawImage(value, panel, enabled) {
408
+ return nextRegionSlots(regionsMapOf(value), panel, {
409
+ ...regionSlotOf(value, panel),
410
+ drawImageEnabled: enabled
411
+ });
412
+ }
413
+ /**
372
414
  * Build the next `regions` settings value for one panel's MODEL DIRECTORY.
373
415
  *
374
416
  * The model-directory save REPLACES the whole region slot, so every field of
@@ -1195,6 +1237,14 @@ window.__ModuleLoader__.load({
1195
1237
  capability note. Wraps on narrow cards; keeps the two buttons visually a
1196
1238
  pair and distinct from the save footer below. */
1197
1239
  .dsm-trae-context-bulk{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
1240
+ /* Per-panel 「自主生图」 consent (trae_draw_image). Sits under the bulk-context
1241
+ row, above the capability note, because it is a per-REGION runtime choice like
1242
+ the model picks above it — not a global preference. The input stays a real
1243
+ checkbox so the card's tests can find it by its accessible name. */
1244
+ .dsm-trae-draw-image{display:flex;align-items:flex-start;gap:8px;margin-top:10px;font-size:11px;line-height:16px;cursor:pointer}
1245
+ .dsm-trae-draw-image input{flex:none;margin:2px 0 0}
1246
+ .dsm-trae-draw-image[data-disabled="true"]{cursor:not-allowed;opacity:.6}
1247
+ .dsm-trae-draw-image-hint{color:var(--dsw-alias-label-tertiary,#999)}
1198
1248
  /* Verdict badge. Only a real refusal is coloured as a warning; an unknown
1199
1249
  verdict stays neutral because an inconclusive probe is not evidence against
1200
1250
  the model. */
@@ -4133,6 +4183,8 @@ window.__ModuleLoader__.load({
4133
4183
  const optimisticSettings = (0, react.useRef)({});
4134
4184
  /** True while a preference is crossing the wire; serializes both writers. */
4135
4185
  const [savingSettings, setSavingSettings] = (0, react.useState)(false);
4186
+ /** The in-flight flip of the per-panel 「自主生图」 switch (undefined = read the document). */
4187
+ const [drawImageDraft, setDrawImageDraft] = (0, react.useState)(void 0);
4136
4188
  /**
4137
4189
  * Per-account status (check-in / balances / subscription) shown in each row of
4138
4190
  * the account table, keyed by region then account id. Loaded per account via
@@ -4250,6 +4302,37 @@ window.__ModuleLoader__.load({
4250
4302
  if (mounted.current) setSavingSettings(false);
4251
4303
  }
4252
4304
  };
4305
+ /**
4306
+ * Flip ONE panel's 「允许模型自主生图」 switch (`drawImageEnabled`).
4307
+ *
4308
+ * Not a top-level preference like the two above: the flag lives INSIDE
4309
+ * `regions.<panel>`, because it is per-panel consent and the Host's tool
4310
+ * resolver reads it per panel. `nextRegionDrawImage` carries the rest of that
4311
+ * slot through, so flipping this cannot drop the model directory, budgets,
4312
+ * pool or credit seat — the failure #44 fixed for the pool.
4313
+ *
4314
+ * Serialized by `savingSettings` with the sibling writers: they all write the
4315
+ * same settings document. The read-back re-reads the document, so a write that
4316
+ * silently reverted cannot leave the box claiming a grant the Host never got —
4317
+ * the one failure that matters here, because a box that lies about consent
4318
+ * makes the tool's absence look like a bug.
4319
+ */
4320
+ const writeDrawImage = async (value) => {
4321
+ if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
4322
+ setSavingSettings(true);
4323
+ setWriteError(void 0);
4324
+ setDrawImageDraft(value);
4325
+ try {
4326
+ await writeSettingsField(settingsScope, "regions", nextRegionDrawImage(settingsScope.getSnapshot().value, activeRegion, value), (readBack) => regionSlotsOf(readBack)[activeRegion]?.drawImageEnabled === value);
4327
+ } catch (error) {
4328
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
4329
+ } finally {
4330
+ if (mounted.current) {
4331
+ setDrawImageDraft(void 0);
4332
+ setSavingSettings(false);
4333
+ }
4334
+ }
4335
+ };
4253
4336
  (0, react.useEffect)(() => settingsScope?.subscribe(() => {
4254
4337
  setSettingsRevision((value) => value + 1);
4255
4338
  }), [settingsScope]);
@@ -4854,6 +4937,26 @@ window.__ModuleLoader__.load({
4854
4937
  const slotImageIds = Array.isArray(regionSlot?.imageModelIds) ? regionSlot.imageModelIds.filter((id) => typeof id === "string") : void 0;
4855
4938
  const legacyImageIds = legacySlot && typeof settingsValue === "object" && settingsValue !== null && Array.isArray(settingsValue.imageModelIds) ? settingsValue.imageModelIds.filter((id) => typeof id === "string") : [];
4856
4939
  const savedImageIds = new Set(slotImageIds ?? legacyImageIds);
4940
+ /**
4941
+ * The active panel's 「自主生图」 consent, read from the SAME place the Host
4942
+ * reads it (`regions.<panel>.drawImageEnabled`, see regionStateOf) with any
4943
+ * in-flight flip laid over it. There is deliberately no legacy flat fallback:
4944
+ * this flag was born after the region split, and reading a top-level key here
4945
+ * would show consent the Host's per-panel resolver never grants.
4946
+ */
4947
+ const committedDrawImage = regionSlot?.drawImageEnabled === true;
4948
+ const drawImageOn = drawImageDraft ?? committedDrawImage;
4949
+ /**
4950
+ * Whether the active panel can serve text-to-image at all. This is the SAME
4951
+ * list the Host registers from (`DRAW_IMAGE_PANELS`, imported rather than
4952
+ * restated), so the box can never offer a grant the Host will then refuse:
4953
+ * a switch that ticks and does nothing is the exact failure this whole
4954
+ * opt-in must not have. Two panels are out — `enterprise` bills through a
4955
+ * tenant gateway, and `ai` has never been probed at all (every measurement
4956
+ * behind the credit figures is CN), so neither may spend credits.
4957
+ */
4958
+ const drawImageServable = DRAW_IMAGE_PANELS.includes(activeRegion);
4959
+ const drawImageUnavailableHint = activeRegion === "ai" ? t("row.drawImageHintAi") : t("row.drawImageHintEnterprise");
4857
4960
  const draft = drafts[activeRegion];
4858
4961
  const visibleModels = draft?.models ?? (status.status === "signed-in" ? status.models : []);
4859
4962
  /**
@@ -5731,6 +5834,22 @@ window.__ModuleLoader__.load({
5731
5834
  className: "dsm-trae-model-capability-note",
5732
5835
  children: t("row.contextBudgetHint")
5733
5836
  }),
5837
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
5838
+ className: "dsm-trae-draw-image",
5839
+ "data-disabled": drawImageServable ? void 0 : "true",
5840
+ title: drawImageServable ? t("row.drawImageHint") : drawImageUnavailableHint,
5841
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
5842
+ type: "checkbox",
5843
+ checked: drawImageOn,
5844
+ disabled: !drawImageServable || busy || saving || savingSettings || settingsScope?.getSnapshot().writable !== true,
5845
+ onChange: (event) => {
5846
+ writeDrawImage(event.target.checked);
5847
+ }
5848
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [t("row.drawImage"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5849
+ className: "dsm-trae-draw-image-hint",
5850
+ children: [" ", drawImageServable ? t("row.drawImageHint") : drawImageUnavailableHint]
5851
+ })] })]
5852
+ }),
5734
5853
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5735
5854
  className: "dsm-trae-model-capability-note",
5736
5855
  children: t("row.modelCapabilityPending")
@@ -6838,6 +6957,10 @@ window.__ModuleLoader__.load({
6838
6957
  "row.modelsTestAllHint": "The「test all」button probes every model in the catalog, enabled or not, so it spends noticeably more quota than the enabled-only run.",
6839
6958
  "row.showPointsInMainUi": "Show credits in the composer row",
6840
6959
  "row.showPointsInMainUiHint": "A clickable readout in the composer tool row while a model from this plugin is selected: Trae CN · <balance> on the CN side, Trae Global · <plan> on the international one.",
6960
+ "row.drawImage": "Let the model draw images (trae_draw_image)",
6961
+ "row.drawImageHint": "Adds a trae_draw_image tool to this panel: the model can generate one image when you ask for one, at this account's real credit cost (measured ~8.8 for 1920x1920 on Trae CN). Off by default — while this is unticked the tool does not exist, so the model cannot spend credits on its own.",
6962
+ "row.drawImageHintEnterprise": "Not available on the enterprise panel: that path bills through the tenant gateway and has not been verified.",
6963
+ "row.drawImageHintAi": "Not available on the international panel: image generation has only been measured on Trae CN, and this path bills on a different plan, so the credit figures shown here would be wrong. Switch to the CN panel to use it.",
6841
6964
  "row.showPointsInMainUiHintAi": "A clickable readout in the composer tool row while a model from this plugin is selected: Trae Global · <plan>.",
6842
6965
  "row.creditPositionLabel": "position",
6843
6966
  "row.creditPositionComposer": "composer toolbar",
@@ -7219,6 +7342,10 @@ window.__ModuleLoader__.load({
7219
7342
  "row.showPointsInMainUi": "在输入框工具栏显示积分读数",
7220
7343
  "row.showPointsInMainUiHint": "在输入框工具栏显示可点击读数(选中本插件模型时出现):国内版为「Trae CN · 余额」,国际版为「Trae 国际版 · 套餐」。",
7221
7344
  "row.showPointsInMainUiHintAi": "在输入框工具栏显示可点击读数(选中本插件模型时出现):国际版为「Trae 国际版 · 套餐」。",
7345
+ "row.drawImage": "允许模型自主生图(trae_draw_image)",
7346
+ "row.drawImageHint": "为本面板挂上 trae_draw_image 工具:你要求时模型能生成一张图,按本账号真实扣积分(国内版实测 1920x1920 约 8.8)。默认关闭——不勾选时工具根本不存在,模型无法自己花额度。",
7347
+ "row.drawImageHintEnterprise": "企业面板不提供:那条路径走租户网关计费,未经验证。",
7348
+ "row.drawImageHintAi": "国际版面板不提供:生图只在 Trae CN 实测过,国际版按另一套套餐计费,这里标注的积分价格会对不上。要用请切到国内版面板。",
7222
7349
  "row.creditPositionLabel": "显示位置",
7223
7350
  "row.creditPositionComposer": "输入框工具栏",
7224
7351
  "row.creditPositionStats": "输入框下方",
package/lib/index.d.ts CHANGED
@@ -4427,6 +4427,13 @@ interface TraeRegionState {
4427
4427
  enabledModelIds?: string[];
4428
4428
  /** Models the user explicitly enabled for image input in this region. */
4429
4429
  imageModelIds?: string[];
4430
+ /**
4431
+ * Whether the model may call trae_draw_image for this panel. Opt-IN:
4432
+ * image generation spends real credits, so an absent value is never consent
4433
+ * (the same rule workbuddy's imageToolEnabled follows). Declared in the
4434
+ * schema below — a field missing from the schema is dropped on every save.
4435
+ */
4436
+ drawImageEnabled?: boolean;
4430
4437
  /** Local DSH context budget per model in this region. */
4431
4438
  contextBudgets?: Record<string, number>;
4432
4439
  /**
package/lib/index.js CHANGED
@@ -1560,6 +1560,64 @@ function nextRegionCreditPosition(value, panel, position) {
1560
1560
  creditPosition: position
1561
1561
  });
1562
1562
  }
1563
+ /**
1564
+ * Panels whose text-to-image path this plugin has actually MEASURED, in
1565
+ * `TRAE_PANELS` tab order.
1566
+ *
1567
+ * ONE entry today. The other two are refused for the SAME reason — no evidence
1568
+ * — which is exactly the asymmetry the review of #62 held the PR on:
1569
+ *
1570
+ * - `enterprise` bills through the tenant gateway, a second billing surface
1571
+ * whose image behaviour has never been probed;
1572
+ * - `ai` answers on `coresg-normal.trae.ai`, where no `tool_text_to_image`
1573
+ * request has ever run either — every measurement behind this tool was
1574
+ * taken against `trae-api-cn.mchost.guru`. Refusing one unverified gateway
1575
+ * while serving another is not a rule, it is an accident.
1576
+ *
1577
+ * Adding a panel means adding its evidence in the same change (the precedent
1578
+ * for international claims is `docs/INTL_SG_EVIDENCE.md`). Order is
1579
+ * significant: the tool's resolver walks `TRAE_PANELS` order and takes the
1580
+ * FIRST consenting panel, so `cn` — the only measured one — always answers
1581
+ * first.
1582
+ */
1583
+ const DRAW_IMAGE_PANELS = ["cn"];
1584
+ /**
1585
+ * Whether ONE panel's 「允许模型自主生图」 grant is in force, i.e. its box was
1586
+ * ticked. ONLY the literal `true` counts: a hand-edited `'yes'` or a `1` is a
1587
+ * typo, not consent to spend credits. (A `{ get(): true }` live reference — the
1588
+ * DSH 0.1.7 slot shape — IS decoded to the true it holds first, see
1589
+ * {@link unwrapVolatileDeep}: that is the Host's own document, not a hand edit.)
1590
+ *
1591
+ * `value` may be the whole settings section or the `regions` map (see
1592
+ * {@link regionsMapOf}).
1593
+ */
1594
+ function drawImageEnabledOf(value, panel) {
1595
+ return regionSlotOf(value, panel)["drawImageEnabled"] === true;
1596
+ }
1597
+ /**
1598
+ * The panels that may register `trae_draw_image` RIGHT NOW — the single
1599
+ * judgment behind both the tool's registration and its account resolver, and
1600
+ * therefore the only place a user's credits can be exposed by accident.
1601
+ *
1602
+ * Pure and exported on purpose (review of #62, item 3): the first version kept
1603
+ * this rule inside `apply()`'s closure, so "does ticking the box actually
1604
+ * grant anything" could only be code-reviewed, never tested.
1605
+ *
1606
+ * THREE gates, all required: the panel must be measured
1607
+ * ({@link DRAW_IMAGE_PANELS}), its provider must be switched ON (opt-out rule,
1608
+ * so a config written before the switch existed still counts), and the panel
1609
+ * must carry an explicit grant. A tick on a switched-off provider is STORED
1610
+ * consent, not a live grant: flipping the provider back on restores the tool
1611
+ * without asking again, which is why both gates are read here rather than at
1612
+ * the checkbox.
1613
+ *
1614
+ * `enterprise` is refused structurally, even if someone later adds it to
1615
+ * {@link DRAW_IMAGE_PANELS}: consent must never cross into the tenant billing
1616
+ * surface by editing a list.
1617
+ */
1618
+ function drawImagePanelsOf(value) {
1619
+ return TRAE_PANELS.filter((panel) => panel !== "enterprise" && DRAW_IMAGE_PANELS.includes(panel) && regionEnabledOf(value, panel) && drawImageEnabledOf(value, panel));
1620
+ }
1563
1621
  //#endregion
1564
1622
  //#region src/decrypt.ts
1565
1623
  const TRAE_AUTH_STORAGE_KEY = "iCubeAuthInfo://icube.cloudide";
@@ -9209,7 +9267,7 @@ async function checkinOne(target, deps) {
9209
9267
  return {
9210
9268
  ...base,
9211
9269
  status: "failed",
9212
- message: messageOf(error)
9270
+ message: messageOf$1(error)
9213
9271
  };
9214
9272
  }
9215
9273
  if (credential === void 0) return {
@@ -9224,7 +9282,7 @@ async function checkinOne(target, deps) {
9224
9282
  return {
9225
9283
  ...base,
9226
9284
  status: "failed",
9227
- message: messageOf(error)
9285
+ message: messageOf$1(error)
9228
9286
  };
9229
9287
  }
9230
9288
  if (!status.enabled) return {
@@ -9260,7 +9318,7 @@ async function checkinOne(target, deps) {
9260
9318
  return {
9261
9319
  ...base,
9262
9320
  status: "failed",
9263
- message: messageOf(error)
9321
+ message: messageOf$1(error)
9264
9322
  };
9265
9323
  }
9266
9324
  }
@@ -9312,7 +9370,7 @@ async function testOne(target, modelId, probe, deps) {
9312
9370
  return {
9313
9371
  ...base,
9314
9372
  outcome: "auth",
9315
- detail: messageOf(error)
9373
+ detail: messageOf$1(error)
9316
9374
  };
9317
9375
  }
9318
9376
  if (credential === void 0) return {
@@ -9380,7 +9438,7 @@ function defaultWait(ms) {
9380
9438
  return new Promise((resolve) => setTimeout(resolve, ms));
9381
9439
  }
9382
9440
  /** A safe message from an unknown throwable, bounded in length. */
9383
- function messageOf(error) {
9441
+ function messageOf$1(error) {
9384
9442
  return (error instanceof Error ? error.message : String(error)).slice(0, 300);
9385
9443
  }
9386
9444
  //#endregion
@@ -9847,6 +9905,316 @@ var UnconfiguredTraeUpstreamClient = class {
9847
9905
  }
9848
9906
  };
9849
9907
  //#endregion
9908
+ //#region src/image-gen-trae.ts
9909
+ /** 文生图(同步):一次 POST 拿回结果。Trae 客户端的 ToolText2Image 用的就是它。 */
9910
+ const TRAE_TOOL_TEXT_TO_IMAGE_PATH = "/api/ide/v1/tool_text_to_image";
9911
+ /** 上游为图像工具固定的模型 id;请求体里没有 model 字段,服务端自选。 */
9912
+ const TRAE_IMAGE_MODEL = "seedream_5";
9913
+ /** 尺寸预设:活体确认 '2k','3k','4k' 被 Ark 接受,square/square_hd 等
9914
+ * 客户端预设名服务端也会翻译(square_hd 实测出 1920x1920)。 */
9915
+ const TRAE_IMAGE_SIZE_PRESETS = [
9916
+ "2k",
9917
+ "3k",
9918
+ "4k",
9919
+ "square",
9920
+ "square_hd",
9921
+ "landscape",
9922
+ "portrait"
9923
+ ];
9924
+ var TraeImageError = class extends Error {
9925
+ code;
9926
+ /** 上游业务码(响应里的 code 字段),只在 UPSTREAM 类失败上有值。 */
9927
+ serverCode;
9928
+ constructor(message, code, serverCode) {
9929
+ super(message);
9930
+ this.name = "TraeImageError";
9931
+ this.code = code;
9932
+ this.serverCode = typeof serverCode === "number" ? serverCode : void 0;
9933
+ }
9934
+ };
9935
+ /** 尺寸合法性:预设名,或 宽x高 且总像素在 Ark 的 [下限,上限] 区间。 */
9936
+ function isTraeImageSize(value) {
9937
+ if (TRAE_IMAGE_SIZE_PRESETS.includes(value)) return true;
9938
+ const m = /^([0-9]{3,5})x([0-9]{3,5})$/.exec(value);
9939
+ if (m === null) return false;
9940
+ const pixels = Number(m[1]) * Number(m[2]);
9941
+ return pixels >= 3686400 && pixels <= 16777216;
9942
+ }
9943
+ /** 组一次文生图请求体;参数毛病全在这里拦下(INVALID_ARGS,不花积分)。 */
9944
+ function buildTraeImageBody(input) {
9945
+ const prompt = input.prompt.trim();
9946
+ if (prompt === "") throw new TraeImageError("trae image: prompt 必须是非空字符串。", "INVALID_ARGS");
9947
+ const out = { prompt };
9948
+ if (input.size !== void 0 && input.size !== "") {
9949
+ const size = input.size.trim().toLowerCase();
9950
+ if (!isTraeImageSize(size)) throw new TraeImageError("trae image: 尺寸「" + size + "」不被接受(需 宽x高 且总像素 ∈ [3686400,16777216],或预设 " + TRAE_IMAGE_SIZE_PRESETS.join("/") + ")。", "INVALID_ARGS");
9951
+ out.image_size = size;
9952
+ }
9953
+ return out;
9954
+ }
9955
+ /** 应答里图片地址的字段名。活体看到的就是 uri;其余字段是 Rust 端
9956
+ * ToolcallResultImage 的字段池(tos_uri/display_url/…),留给未来形态。 */
9957
+ const TRAE_IMAGE_URL_FIELDS = [
9958
+ "uri",
9959
+ "tos_uri",
9960
+ "display_url",
9961
+ "image_url",
9962
+ "original_url",
9963
+ "preview_url",
9964
+ "internal_url"
9965
+ ];
9966
+ /** 深度不超过 4 地扫第一个可用 URL 字段;再深的嵌套不是这个 API 的形态。 */
9967
+ function findImageUrl(node, depth = 0) {
9968
+ if (depth > 4 || node === null || typeof node !== "object") return void 0;
9969
+ const record = node;
9970
+ for (const field of TRAE_IMAGE_URL_FIELDS) {
9971
+ const value = record[field];
9972
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
9973
+ }
9974
+ for (const value of Object.values(record)) if (value !== null && typeof value === "object") {
9975
+ const found = findImageUrl(value, depth + 1);
9976
+ if (found !== void 0) return found;
9977
+ }
9978
+ }
9979
+ function findCredit(node, depth = 0) {
9980
+ if (depth > 4 || node === null || typeof node !== "object") return void 0;
9981
+ const record = node;
9982
+ const credit = record.credit;
9983
+ if (typeof credit === "number" && Number.isFinite(credit)) return credit;
9984
+ for (const value of Object.values(record)) if (value !== null && typeof value === "object") {
9985
+ const found = findCredit(value, depth + 1);
9986
+ if (found !== void 0) return found;
9987
+ }
9988
+ }
9989
+ /**
9990
+ * 解析 tool_text_to_image 的扁平 JSON 应答。
9991
+ *
9992
+ * 实测形态:成功 {message:"success", uri:"…"};失败 {code:3003, message:"…"}。
9993
+ * code 存在且非 0 → UPSTREAM(原样抛出 code/message——余额不足、风控、
9994
+ * Ark 参数错误全靠它浮出);无 code 但 uri 缺失 → EMPTY_RESULT。
9995
+ */
9996
+ function parseTraeImageResponse(json) {
9997
+ const record = json !== null && typeof json === "object" ? json : {};
9998
+ const code = typeof record.code === "number" ? record.code : void 0;
9999
+ const message = typeof record.message === "string" ? record.message : void 0;
10000
+ if (code !== void 0 && code !== 0) throw new TraeImageError("trae image: 上游业务错误 code=" + code + (message === void 0 || message === "" ? "" : ": " + message), "UPSTREAM", code);
10001
+ const url = findImageUrl(record);
10002
+ if (url === void 0) throw new TraeImageError("trae image: 应答里没有图片地址" + (message === void 0 ? "" : "(message=" + message + ")"), "EMPTY_RESULT", code);
10003
+ const credit = findCredit(record);
10004
+ return credit === void 0 ? { url } : {
10005
+ url,
10006
+ credit
10007
+ };
10008
+ }
10009
+ //#endregion
10010
+ //#region src/image-tool.ts
10011
+ /** 工具名。与其它 connect 插件的命名习惯一致。 */
10012
+ const TRAE_IMAGE_TOOL_NAME = "trae_draw_image";
10013
+ /** 单次生图上限。Trae 客户端里一张图 15-45 秒,留足余量。 */
10014
+ const IMAGE_TOOL_TIMEOUT_MS = 18e4;
10015
+ /** 失败后的冷却:生图按张扣积分,连续重试会白白烧积分。 */
10016
+ const FAILURE_COOLDOWN_MS = 3e4;
10017
+ function messageOf(error) {
10018
+ return error instanceof Error ? error.message : String(error);
10019
+ }
10020
+ /** HTTP 层的状态码映射。语义与 workbuddy 侧一致。 */
10021
+ function mapHttpToImageError(status, detail) {
10022
+ const tail = detail === "" ? "" : ":" + detail.slice(0, 200);
10023
+ if (status === 401 || status === 403) return new TraeImageError("trae image: 上游拒绝了凭据(HTTP " + status + ")" + tail, "AUTH");
10024
+ if (status === 429) return new TraeImageError("trae image: 上游限流(HTTP 429)" + tail, "RATE_LIMIT");
10025
+ return new TraeImageError("trae image: 上游 HTTP " + status + tail, "UPSTREAM");
10026
+ }
10027
+ /**
10028
+ * 发一次真机文生图请求。所有可预期的失败都归一成 TraeImageError,
10029
+ * 让上层的冷却判据只看 code(见 createTraeDrawImageTool 的注释)。
10030
+ */
10031
+ var TraeImageClient = class {
10032
+ options;
10033
+ constructor(options = {}) {
10034
+ this.options = options;
10035
+ }
10036
+ async generate(account, input, signal) {
10037
+ const body = buildTraeImageBody(input);
10038
+ const endpoint = traeEndpoint(account.baseUrl, TRAE_TOOL_TEXT_TO_IMAGE_PATH);
10039
+ const headers = buildTraeHeaders(account.credential, account.identity, { profile: "model-detail" });
10040
+ const fetchImpl = this.options.fetchImpl ?? globalThis.fetch;
10041
+ const timeoutMs = this.options.timeoutMs ?? IMAGE_TOOL_TIMEOUT_MS;
10042
+ const requestSignal = signal ?? AbortSignal.timeout(timeoutMs);
10043
+ let response;
10044
+ try {
10045
+ response = await fetchImpl(endpoint, {
10046
+ method: "POST",
10047
+ headers,
10048
+ body: JSON.stringify(body),
10049
+ signal: requestSignal
10050
+ });
10051
+ } catch (error) {
10052
+ throw new TraeImageError("trae image: 生图请求发出失败:" + messageOf(error), "NO_ROUTE");
10053
+ }
10054
+ let payload;
10055
+ try {
10056
+ payload = await response.text();
10057
+ } catch (error) {
10058
+ throw new TraeImageError("trae image: 读取应答失败:" + messageOf(error), "UPSTREAM");
10059
+ }
10060
+ if (!response.ok) throw mapHttpToImageError(response.status, payload);
10061
+ let json;
10062
+ try {
10063
+ json = JSON.parse(payload);
10064
+ } catch {
10065
+ throw new TraeImageError("trae image: 应答不是 JSON(HTTP 200):" + payload.slice(0, 200), "UPSTREAM");
10066
+ }
10067
+ const outcome = parseTraeImageResponse(json);
10068
+ const result = { url: outcome.url };
10069
+ if (outcome.credit !== void 0) result.credit = outcome.credit;
10070
+ return result;
10071
+ }
10072
+ };
10073
+ function asRecord(value) {
10074
+ return typeof value === "object" && value !== null ? value : {};
10075
+ }
10076
+ function asNonEmptyString(value) {
10077
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
10078
+ }
10079
+ /**
10080
+ * 这些失败码都发生在「生图请求已经发出去」之后或尝试发出它——上游可能已经
10081
+ * 扣了积分或排了队,连续重试只会把它们再花一遍,所以进冷却。
10082
+ * INVALID_ARGS / NOT_LOGGED_IN 一分钱没花,不冷却(判据同 workbuddy)。
10083
+ */
10084
+ const BILLABLE_FAILURE_CODES = /* @__PURE__ */ new Set([
10085
+ "AUTH",
10086
+ "RATE_LIMIT",
10087
+ "NO_ROUTE",
10088
+ "UPSTREAM",
10089
+ "EMPTY_RESULT"
10090
+ ]);
10091
+ function isBillableFailure(error) {
10092
+ if (error instanceof TraeImageError) return BILLABLE_FAILURE_CODES.has(error.code);
10093
+ return true;
10094
+ }
10095
+ /** 造出工具定义。每次 apply() 只造一个实例,冷却状态挂在闭包里。 */
10096
+ function createTraeDrawImageTool(deps) {
10097
+ let cooldownUntil = 0;
10098
+ return {
10099
+ name: TRAE_IMAGE_TOOL_NAME,
10100
+ description: "用当前登录的 Trae 账号文生图(TraeWork/SOLO 同款服务,模型 seedream_5)。只在用户明确要一张图时调用:每张真实扣该账号积分,实测 CN 个人版 1920x1920 一张约 8.8 积分(尺寸越大越贵),而上游不回传扣费数字,以 Trae 额度面板为准。不是常开能力:只有插件设置里为该区打开「允许模型自主生图」(drawImageEnabled) 时本工具才存在,没打开就没得调——不要用报错去试探它开没开。取图口径:生成只回一个图片对象 key(tos uri,形如 tos-cn-i-xxx/cn/<uid>/gen_image/…,打不开),本工具不会再发取图请求——实测对同一 (prompt, size) 再 GET 一次不是缓存查询,而是又一次生成,只会再扣一张的钱并回一个**新的** uri。图片字节目前插件拿不到,图只在 Trae 客户端的历史里能看到;把 key 原样告诉用户即可。不要为了\"取图\"或\"换张\"重跑同一条 prompt。失败后有 30 秒冷却,冷却内重试不会打到上游。尺寸:默认 1920x1920,也可给 宽x高(总像素需在 3686400~16777216 之间,如 2048x2048)或预设 2k/3k/4k;过小(如 1024x1024)会被上游直接拒绝。(改图与文生视频暂未开放:改图需要先走 TOS 上传预签名,视频要轮询素材结果。)",
10101
+ parameters: {
10102
+ type: "object",
10103
+ properties: {
10104
+ prompt: {
10105
+ type: "string",
10106
+ description: "画面描述(中文或英文都可以)。写清楚主体、风格、构图、光线。"
10107
+ },
10108
+ size: {
10109
+ type: "string",
10110
+ description: "图片尺寸:默认 1920x1920;也可给 宽x高(总像素 3686400~16777216,如 2048x2048)或预设 2k/3k/4k。过小(如 1024x1024)会被上游拒绝。"
10111
+ }
10112
+ },
10113
+ required: ["prompt"],
10114
+ additionalProperties: false
10115
+ },
10116
+ output: {
10117
+ schema: {
10118
+ type: "object",
10119
+ properties: {
10120
+ url: {
10121
+ type: "string",
10122
+ description: "Trae 图片对象 key(tos uri),不是可直接打开的 URL;图片字节插件拿不到。"
10123
+ },
10124
+ model: {
10125
+ type: "string",
10126
+ description: "实际使用的生图模型 id。"
10127
+ },
10128
+ prompt: {
10129
+ type: "string",
10130
+ description: "实际提交的画面描述。"
10131
+ },
10132
+ size: {
10133
+ type: "string",
10134
+ description: "实际提交的尺寸。"
10135
+ },
10136
+ hint: {
10137
+ type: "string",
10138
+ description: "给模型的后续提示(如何展示、扣费口径)。"
10139
+ },
10140
+ credit: {
10141
+ type: "number",
10142
+ description: "上游若回传扣费数字,这里给出积分数。"
10143
+ }
10144
+ },
10145
+ required: [
10146
+ "url",
10147
+ "model",
10148
+ "prompt",
10149
+ "size",
10150
+ "hint"
10151
+ ],
10152
+ additionalProperties: false
10153
+ },
10154
+ render(_args, value) {
10155
+ const record = asRecord(value);
10156
+ const url = asNonEmptyString(record["url"]) ?? "";
10157
+ const model = asNonEmptyString(record["model"]) ?? "unknown";
10158
+ const size = asNonEmptyString(record["size"]) ?? "";
10159
+ const credit = typeof record["credit"] === "number" ? record["credit"] : void 0;
10160
+ const hint = asNonEmptyString(record["hint"]) ?? "";
10161
+ const lines = [
10162
+ "图片对象 key(不能直接打开):`" + url + "`",
10163
+ "",
10164
+ "文生图 · 模型:" + model + (size === "" ? "" : " · 尺寸:" + size) + (credit === void 0 ? "" : " · 消耗 " + credit + " 积分")
10165
+ ];
10166
+ if (hint !== "") lines.push("", hint);
10167
+ return [{
10168
+ type: "text",
10169
+ text: lines.join("\n")
10170
+ }];
10171
+ }
10172
+ },
10173
+ timeoutMs: IMAGE_TOOL_TIMEOUT_MS,
10174
+ async execute(args, exec) {
10175
+ const record = asRecord(args);
10176
+ const prompt = asNonEmptyString(record["prompt"]);
10177
+ if (prompt === void 0) throw new TraeImageError("trae_draw_image: 参数 prompt 必填,且必须是非空字符串。", "INVALID_ARGS");
10178
+ const size = asNonEmptyString(record["size"]) ?? "1920x1920";
10179
+ const now = Date.now();
10180
+ if (now < cooldownUntil) throw new TraeImageError("trae image: 上一次生图刚失败过,请 " + Math.ceil((cooldownUntil - now) / 1e3) + " 秒后再试(失败冷却是为了避免连续重试白白烧积分)。", "COOLDOWN");
10181
+ const account = await deps.resolveAccount();
10182
+ if (account === void 0) throw new TraeImageError("trae image: 没有可用的 Trae 账号——要么相关区都没登录,要么已登录的那个区没有打开「允许模型自主生图」(drawImageEnabled)。请先登录 Trae 并在插件设置里打开那个开关。", "NOT_LOGGED_IN");
10183
+ const signal = exec.signal === void 0 ? AbortSignal.timeout(IMAGE_TOOL_TIMEOUT_MS) : AbortSignal.any([exec.signal, AbortSignal.timeout(IMAGE_TOOL_TIMEOUT_MS)]);
10184
+ try {
10185
+ const outcome = await deps.generate(account, {
10186
+ prompt,
10187
+ size
10188
+ }, signal);
10189
+ const value = {
10190
+ url: outcome.url,
10191
+ model: TRAE_IMAGE_MODEL,
10192
+ prompt,
10193
+ size,
10194
+ hint: "这张图来自 Trae 文生图(模型 seedream_5),按张计入该账号积分(实测 1920x1920 约 8.8,上游不回传数字,以 Trae 额度面板为准)。url 是 Trae 的图片对象 key,不能直接打开,图片字节插件也拿不到——这张图能在 Trae 客户端的历史/作品里看到。不要把 key 拼进 markdown 图片语法;也不要为了取图或换张重跑同一条 prompt(取图那条请求本身就是又一次生成,会再扣一张)。"
10195
+ };
10196
+ if (outcome.credit !== void 0) value.credit = outcome.credit;
10197
+ return value;
10198
+ } catch (error) {
10199
+ if (isBillableFailure(error)) cooldownUntil = Date.now() + FAILURE_COOLDOWN_MS;
10200
+ throw error;
10201
+ }
10202
+ }
10203
+ };
10204
+ }
10205
+ /**
10206
+ * 把工具注册进宿主。返回 disposer;调用方负责在插件卸载时调用它。
10207
+ * 注册失败只记警告——生图工具缺失不该让整个插件起不来(同 workbuddy)。
10208
+ */
10209
+ function registerTraeDrawImageTool(tools, deps) {
10210
+ try {
10211
+ return tools.register(createTraeDrawImageTool(deps));
10212
+ } catch (error) {
10213
+ deps.logger.warn("dsh-connect-trae: 注册 trae_draw_image 失败: " + messageOf(error));
10214
+ return;
10215
+ }
10216
+ }
10217
+ //#endregion
9850
10218
  //#region src/index.ts
9851
10219
  const name = "dsh-connect-trae";
9852
10220
  const inject = ["llm"];
@@ -9978,6 +10346,7 @@ const regionStateConfig = z.object({
9978
10346
  lastCatalog: z.array(modelConfig).default([]),
9979
10347
  enabledModelIds: z.array(z.string()).default([]),
9980
10348
  imageModelIds: z.array(z.string()).default([]),
10349
+ drawImageEnabled: z.boolean().default(false).description("Allow the model to generate images with trae_draw_image on this panel (consumes credits; default off)"),
9981
10350
  contextBudgets: z.dict(z.number().step(1).min(1)).default({}),
9982
10351
  creditPosition: z.union([z.const("composer"), z.const("stats")]).default("composer").description("积分读数显示位置:\"composer\" = 输入框工具栏(默认),\"stats\" = 输入框下方(与「轮次 / 步数 / token」统计同一行)。装了别的插件后读数不显示时改成 stats。"),
9983
10352
  pool: z.object({
@@ -11120,8 +11489,48 @@ function apply(ctx, config) {
11120
11489
  const settings = settingsCtx.settings;
11121
11490
  ctx.effect(() => settings.configure({ auto: true }, ctx.fiber));
11122
11491
  });
11492
+ const drawImageClient = new TraeImageClient({ logger: ctx.logger });
11493
+ let drawImageTools;
11494
+ let disposeDrawImageTool;
11495
+ const syncTraeDrawImageTool = (value) => {
11496
+ if (drawImagePanelsOf(value).length === 0) {
11497
+ disposeDrawImageTool?.();
11498
+ disposeDrawImageTool = void 0;
11499
+ return;
11500
+ }
11501
+ if (disposeDrawImageTool !== void 0 || drawImageTools === void 0) return;
11502
+ disposeDrawImageTool = registerTraeDrawImageTool(drawImageTools, {
11503
+ resolveAccount: async () => {
11504
+ for (const panel of drawImagePanelsOf(current())) {
11505
+ const stack = stacks[panel];
11506
+ try {
11507
+ return {
11508
+ credential: await stack.store.resolve(),
11509
+ identity: await stack.identity(),
11510
+ baseUrl: REGION_GATEWAYS[stack.region].chat
11511
+ };
11512
+ } catch {
11513
+ continue;
11514
+ }
11515
+ }
11516
+ },
11517
+ generate: (account, request, signal) => drawImageClient.generate(account, request, signal),
11518
+ logger: ctx.logger
11519
+ });
11520
+ };
11521
+ ctx.inject(["tools"], (toolsCtx) => {
11522
+ drawImageTools = toolsCtx.tools;
11523
+ if (drawImageTools === void 0) return;
11524
+ syncTraeDrawImageTool(current());
11525
+ toolsCtx.effect(() => () => {
11526
+ disposeDrawImageTool?.();
11527
+ disposeDrawImageTool = void 0;
11528
+ drawImageTools = void 0;
11529
+ });
11530
+ });
11123
11531
  ctx.on("loader/volatile-update", () => {
11124
11532
  applySelection(current());
11533
+ syncTraeDrawImageTool(current());
11125
11534
  });
11126
11535
  let stopped = false;
11127
11536
  ctx.effect(() => () => {
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.5.2",
5
+ "version": "4.6.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",