dsh-connect-trae 4.5.1 → 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 +98 -0
- package/README.en.md +22 -0
- package/README.md +22 -0
- package/lib/client.js +134 -0
- package/lib/index.d.ts +24 -1
- package/lib/index.js +522 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
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
|
+
|
|
60
|
+
## 4.5.2 (2026-10-25)
|
|
61
|
+
|
|
62
|
+
> **思考档位不再被吞,也不再被谎报。** 插件原先只认识三档,未知档位一律按 `high`
|
|
63
|
+
> 发出去——用户选 Max、实际收到 high,无报错也无日志。这一版把词表与 Trae 客户端
|
|
64
|
+
> 对齐(`low / medium / high / extra_high / max`),并把「不认识的档位」从谎报改成
|
|
65
|
+
> 丢弃,让上游用它自己的默认值。
|
|
66
|
+
>
|
|
67
|
+
> 词表变宽,**声称没变**:能选哪些档位仍然只取决于上游为该模型实际下发的
|
|
68
|
+
> `options`,所以今天只发三档的模型不会凭空多出 Max。
|
|
69
|
+
|
|
70
|
+
### 修复
|
|
71
|
+
|
|
72
|
+
- **思考档位词表配平,未知档位不再静默降档成 high**(#60)。
|
|
73
|
+
- **三处吞档位**:解析侧 `EFFORT_MAP` / `TRAE_REASONING_EFFORTS` 缺 `medium` 与
|
|
74
|
+
`max`;`toPiModel` 的 `thinkingLevelMap` 把 `minimal / medium / max` 硬编码成
|
|
75
|
+
`null`;反向映射写成三元链
|
|
76
|
+
`effort === 'low' ? 'light' : effort === 'xhigh' ? 'extra_high' : 'high'`。
|
|
77
|
+
- **最坏的是第三处**:任何未预期档位都被映射成 `high` 发给上游。Trae 客户端自己
|
|
78
|
+
认识五档(`reasoning_effort.{low,medium,high,extra_high,max}`),并按 agent model
|
|
79
|
+
灰度下发,所以上游哪天发 `max`,用户选 Max 就会被静默降成 high。**静默降档比
|
|
80
|
+
不给选择器更坏**——它给出一个便宜了、但用户没要的答案。
|
|
81
|
+
- **改法**:反向映射抽成 `WIRE_EFFORT_MAP` 查表 + `wireEffortsOf()`,三处调用点
|
|
82
|
+
统一走它(报告人点到两处,第三处在 wire 行提升的分支里,一起改才不会有同一份
|
|
83
|
+
目录两种行为)。查不到即**丢弃**,而不是猜一个默认值。
|
|
84
|
+
- **实测**(2026-10-25,本机 CN 凭据,98 行目录):当前 `options` 只有
|
|
85
|
+
`["light","high"]`、`["high","extra_high"]`、`["light","high","extra_high"]`
|
|
86
|
+
三种,`max` 尚未下发——所以这是「今天就备好、明天才用上」,不是伪造档位。
|
|
87
|
+
- 测试:新增 10 例(词表、丢弃未知档位、既有三档逐字不变、`discoveredCatalog`
|
|
88
|
+
同步、不伪造档位、`thinkingLevelMap` 按模型而非硬编码)。三处均做变异验证:
|
|
89
|
+
恢复旧映射 → 5 例红;恢复硬编码 `null` → 1 例红;去掉解析侧新键 → 1 例红。
|
|
90
|
+
- 贡献者:[@ahian-lee](https://github.com/ahian-lee)(issue #60,附诊断与补丁)。
|
|
91
|
+
|
|
92
|
+
### 文档
|
|
93
|
+
|
|
94
|
+
- **卡片模型栏写明「上下文预算只影响 DSH 侧」**(#60 的顺带问题)。
|
|
95
|
+
- 每行显示的是 Trae 自己公布的最大窗口,而下面的预算单选只写 DSH 的本地预算;
|
|
96
|
+
请求体里既没有窗口字段、也没有 `max_mode`,所以把预算提到 1M 并不会让上游按
|
|
97
|
+
1M 服务。原先的标签读起来像「已经放开到 1M」。
|
|
98
|
+
- 上游是否接受 `v2_max_mode_enabled` 这类字段**尚未取证**,所以这次不动请求体
|
|
99
|
+
(乱加字段可能让每次请求 400),只把事实说出来:常驻说明,不用悬停提示。
|
|
100
|
+
|
|
3
101
|
## 4.5.1 (2026-10-25)
|
|
4
102
|
|
|
5
103
|
> **只在租户池上架的模型不再隐形。** 插件的模型目录只问了 `solo.trae.cn` 一处,
|
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
|
/**
|
|
@@ -5659,6 +5762,7 @@ window.__ModuleLoader__.load({
|
|
|
5659
5762
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
|
|
5660
5763
|
className: "dsm-trae-context-budget",
|
|
5661
5764
|
"aria-label": t("row.contextBudget"),
|
|
5765
|
+
title: t("row.contextBudgetHint"),
|
|
5662
5766
|
children: contextBudgetOptions(model).map((option) => {
|
|
5663
5767
|
const selectable = model.maxContextWindow !== void 0;
|
|
5664
5768
|
const checked = (activeContextBudgets[model.id] ?? model.contextWindow) === option;
|
|
@@ -5726,6 +5830,26 @@ window.__ModuleLoader__.load({
|
|
|
5726
5830
|
})
|
|
5727
5831
|
]
|
|
5728
5832
|
}),
|
|
5833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5834
|
+
className: "dsm-trae-model-capability-note",
|
|
5835
|
+
children: t("row.contextBudgetHint")
|
|
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
|
+
}),
|
|
5729
5853
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5730
5854
|
className: "dsm-trae-model-capability-note",
|
|
5731
5855
|
children: t("row.modelCapabilityPending")
|
|
@@ -6833,6 +6957,10 @@ window.__ModuleLoader__.load({
|
|
|
6833
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.",
|
|
6834
6958
|
"row.showPointsInMainUi": "Show credits in the composer row",
|
|
6835
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.",
|
|
6836
6964
|
"row.showPointsInMainUiHintAi": "A clickable readout in the composer tool row while a model from this plugin is selected: Trae Global · <plan>.",
|
|
6837
6965
|
"row.creditPositionLabel": "position",
|
|
6838
6966
|
"row.creditPositionComposer": "composer toolbar",
|
|
@@ -6898,6 +7026,7 @@ window.__ModuleLoader__.load({
|
|
|
6898
7026
|
"row.veilSaving": "Saving changes…",
|
|
6899
7027
|
"row.modelContext": "Max context {context}",
|
|
6900
7028
|
"row.contextBudget": "DSH context budget",
|
|
7029
|
+
"row.contextBudgetHint": "Affects the DSH-side context budget only. The plugin does not send a window or Max-mode field upstream, so this does not change the window Trae serves the request with.",
|
|
6901
7030
|
"row.modelOutput": "Output {output}",
|
|
6902
7031
|
"row.modelImage": "Image",
|
|
6903
7032
|
"row.modelImageAuto": "Enabled automatically: Trae marks this model multimodal.",
|
|
@@ -7213,6 +7342,10 @@ window.__ModuleLoader__.load({
|
|
|
7213
7342
|
"row.showPointsInMainUi": "在输入框工具栏显示积分读数",
|
|
7214
7343
|
"row.showPointsInMainUiHint": "在输入框工具栏显示可点击读数(选中本插件模型时出现):国内版为「Trae CN · 余额」,国际版为「Trae 国际版 · 套餐」。",
|
|
7215
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 实测过,国际版按另一套套餐计费,这里标注的积分价格会对不上。要用请切到国内版面板。",
|
|
7216
7349
|
"row.creditPositionLabel": "显示位置",
|
|
7217
7350
|
"row.creditPositionComposer": "输入框工具栏",
|
|
7218
7351
|
"row.creditPositionStats": "输入框下方",
|
|
@@ -7277,6 +7410,7 @@ window.__ModuleLoader__.load({
|
|
|
7277
7410
|
"row.veilSaving": "正在保存更改…",
|
|
7278
7411
|
"row.modelContext": "最大上下文 {context}",
|
|
7279
7412
|
"row.contextBudget": "DSH 上下文预算",
|
|
7413
|
+
"row.contextBudgetHint": "只影响 DSH 侧的上下文预算。插件不会向上游发送窗口或 Max 模式字段,所以这不会改变 Trae 实际服务这次请求所用的窗口。",
|
|
7280
7414
|
"row.modelOutput": "最大输出 {output}",
|
|
7281
7415
|
"row.modelImage": "图片",
|
|
7282
7416
|
"row.modelImageAuto": "已自动开启:Trae 把该模型标为多模态。",
|
package/lib/index.d.ts
CHANGED
|
@@ -44,7 +44,23 @@ interface TraeActivityDiscount {
|
|
|
44
44
|
}
|
|
45
45
|
//#endregion
|
|
46
46
|
//#region src/reasoning.d.ts
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The DSH thinking levels this plugin can carry, in ascending order.
|
|
49
|
+
*
|
|
50
|
+
* `max` is the fifth level Trae's own client advertises — its bundle ships the
|
|
51
|
+
* i18n keys `trae-chat-core.model.reasoning_effort.{low,medium,high,extra_high,max}`
|
|
52
|
+
* and `ai.model.reasoning_effort.{low,medium,high,xhigh,max}`, and publishes the
|
|
53
|
+
* per-model list from a remote config (`AI.agent.model.reasoning_effort_level_by_agent_model_v2`).
|
|
54
|
+
* Leaving it out of this list meant a level the vendor could start serving was
|
|
55
|
+
* filtered away at PARSE time, before anything downstream could decide (issue
|
|
56
|
+
* #60). Listing it does not fabricate the level: `supported` is still built
|
|
57
|
+
* only from what the upstream response actually contains, so a model that does
|
|
58
|
+
* not advertise `max` never offers it.
|
|
59
|
+
*
|
|
60
|
+
* `minimal` is kept for the same reason in reverse — it is a DSH level, so the
|
|
61
|
+
* type must be able to represent it even though no CN model has advertised it.
|
|
62
|
+
*/
|
|
63
|
+
declare const TRAE_REASONING_EFFORTS: readonly ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
48
64
|
type TraeReasoningEffort = typeof TRAE_REASONING_EFFORTS[number];
|
|
49
65
|
interface TraeReasoningCapability {
|
|
50
66
|
supported: readonly TraeReasoningEffort[];
|
|
@@ -4411,6 +4427,13 @@ interface TraeRegionState {
|
|
|
4411
4427
|
enabledModelIds?: string[];
|
|
4412
4428
|
/** Models the user explicitly enabled for image input in this region. */
|
|
4413
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;
|
|
4414
4437
|
/** Local DSH context budget per model in this region. */
|
|
4415
4438
|
contextBudgets?: Record<string, number>;
|
|
4416
4439
|
/**
|
package/lib/index.js
CHANGED
|
@@ -282,6 +282,53 @@ function displayKey(name) {
|
|
|
282
282
|
return name.trim().toLowerCase();
|
|
283
283
|
}
|
|
284
284
|
/**
|
|
285
|
+
* DSH thinking level -> the `reasoning_effort` name Trae's wire accepts.
|
|
286
|
+
*
|
|
287
|
+
* The INVERSE of `EFFORT_MAP` in `model-metadata.ts`, and it must stay an exact
|
|
288
|
+
* mirror of it: a level this table does not name has no wire spelling, so it is
|
|
289
|
+
* DROPPED rather than guessed (issue #60).
|
|
290
|
+
*
|
|
291
|
+
* The previous form was a ternary chain ending in a bare `'high'`:
|
|
292
|
+
*
|
|
293
|
+
* effort === 'low' ? 'light' : effort === 'xhigh' ? 'extra_high' : 'high'
|
|
294
|
+
*
|
|
295
|
+
* so EVERY unnamed level was sent as `high`. That is not a harmless default: it
|
|
296
|
+
* is a silent downgrade. Trae's own vocabulary carries five levels
|
|
297
|
+
* (`low / medium / high / extra_high / max`, verified in the client bundle's
|
|
298
|
+
* `reasoning_effort` i18n keys), and its remote config publishes the list per
|
|
299
|
+
* model (`AI.agent.model.reasoning_effort_level_by_agent_model_v2`). The day
|
|
300
|
+
* upstream starts advertising `max`, a user picking Max would have sent `high`
|
|
301
|
+
* — no error, no log, and a cheaper answer than the one they asked for. A
|
|
302
|
+
* dropped field lets the upstream apply its own default, which is at least the
|
|
303
|
+
* model's real behaviour instead of a wrong claim about it.
|
|
304
|
+
*
|
|
305
|
+
* Measured 2026-10-25 on a live CN credential (98 directory rows): options are
|
|
306
|
+
* `["light","high"]`, `["high","extra_high"]`, `["light","high","extra_high"]`
|
|
307
|
+
* or absent — `max` is not published YET, which is exactly why this must not
|
|
308
|
+
* wait for it to be.
|
|
309
|
+
*/
|
|
310
|
+
const WIRE_EFFORT_MAP = {
|
|
311
|
+
low: "light",
|
|
312
|
+
medium: "medium",
|
|
313
|
+
high: "high",
|
|
314
|
+
xhigh: "extra_high",
|
|
315
|
+
max: "max"
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Map a model's advertised levels to their wire names, dropping unnamed ones.
|
|
319
|
+
*
|
|
320
|
+
* `flatMap` rather than `map` + a placeholder: an unmappable level must not
|
|
321
|
+
* produce an entry at all, because every consumer reads "is there a value here"
|
|
322
|
+
* as "may this level be sent" (see `thinkingLevelMap` in `adapter.ts` and the
|
|
323
|
+
* `delete input['reasoning_effort']` fallback in `solo-bridge.ts`).
|
|
324
|
+
*/
|
|
325
|
+
function wireEffortsOf(supported) {
|
|
326
|
+
return Object.fromEntries(supported.flatMap((effort) => {
|
|
327
|
+
const wire = WIRE_EFFORT_MAP[effort];
|
|
328
|
+
return wire === void 0 ? [] : [[effort, wire]];
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
285
332
|
* Merge the two Trae model sources into one authoritative catalog.
|
|
286
333
|
*
|
|
287
334
|
* `remote` (the solo.trae.cn `/models` directory) is the authoritative model
|
|
@@ -360,7 +407,7 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
360
407
|
reasoningSupported: model.reasoningSupported,
|
|
361
408
|
...model.reasoning === void 0 ? {} : {
|
|
362
409
|
reasoning: model.reasoning,
|
|
363
|
-
reasoningEfforts:
|
|
410
|
+
reasoningEfforts: wireEffortsOf(model.reasoning.supported)
|
|
364
411
|
},
|
|
365
412
|
...wireModel !== void 0 && wireModel.id !== "" && wireModel.id !== model.id ? { wireConfigName: wireModel.id } : {},
|
|
366
413
|
...wireFunction === void 0 ? {} : { wireFunction }
|
|
@@ -433,7 +480,7 @@ function mergeTraeModelSources(remote, wire) {
|
|
|
433
480
|
supported: [...wireModel.reasoning.supported],
|
|
434
481
|
...wireModel.reasoning.defaultEffort === void 0 ? {} : { defaultEffort: wireModel.reasoning.defaultEffort }
|
|
435
482
|
},
|
|
436
|
-
reasoningEfforts:
|
|
483
|
+
reasoningEfforts: wireEffortsOf(wireModel.reasoning.supported)
|
|
437
484
|
},
|
|
438
485
|
...wireModel.function === void 0 ? {} : { wireFunction: wireModel.function }
|
|
439
486
|
});
|
|
@@ -507,7 +554,7 @@ function discoveredCatalog(models) {
|
|
|
507
554
|
reasoningSupported: model.reasoningSupported,
|
|
508
555
|
...model.reasoning === void 0 ? {} : {
|
|
509
556
|
reasoning: model.reasoning,
|
|
510
|
-
reasoningEfforts:
|
|
557
|
+
reasoningEfforts: wireEffortsOf(model.reasoning.supported)
|
|
511
558
|
}
|
|
512
559
|
});
|
|
513
560
|
return result;
|
|
@@ -836,14 +883,28 @@ function toPiModel(info, baseUrl, providerId, withRate = true) {
|
|
|
836
883
|
cacheWrite: 0
|
|
837
884
|
},
|
|
838
885
|
reasoning: info.reasoningEfforts !== void 0,
|
|
839
|
-
...info.reasoningEfforts === void 0 ? {} : {
|
|
886
|
+
...info.reasoningEfforts === void 0 ? {} : {
|
|
887
|
+
/**
|
|
888
|
+
* Which DSH levels this model may be asked for, and under which wire name.
|
|
889
|
+
*
|
|
890
|
+
* Every level is read from the model's OWN advertised capability, never
|
|
891
|
+
* hardcoded. `minimal / medium / max` used to be pinned to `null`, so a
|
|
892
|
+
* level the upstream published could not be selected even after it was
|
|
893
|
+
* parsed — the second of the three places issue #60 reported. `null`
|
|
894
|
+
* still means "not offered", which is what a model that does not
|
|
895
|
+
* advertise the level reports: nothing here fabricates a level.
|
|
896
|
+
*
|
|
897
|
+
* `off` stays `null` deliberately: Trae's wire has no "no reasoning"
|
|
898
|
+
* spelling, so an explicit off is not something this provider can send.
|
|
899
|
+
*/
|
|
900
|
+
thinkingLevelMap: {
|
|
840
901
|
off: null,
|
|
841
|
-
minimal: null,
|
|
902
|
+
minimal: info.reasoningEfforts.minimal ?? null,
|
|
842
903
|
low: info.reasoningEfforts.low ?? null,
|
|
843
|
-
medium: null,
|
|
904
|
+
medium: info.reasoningEfforts.medium ?? null,
|
|
844
905
|
high: info.reasoningEfforts.high ?? null,
|
|
845
906
|
xhigh: info.reasoningEfforts.xhigh ?? null,
|
|
846
|
-
max: null
|
|
907
|
+
max: info.reasoningEfforts.max ?? null
|
|
847
908
|
} },
|
|
848
909
|
...info.contextWindow === void 0 ? {} : { contextWindow: info.contextWindow },
|
|
849
910
|
...info.maxTokens === void 0 ? {} : { maxTokens: info.maxTokens },
|
|
@@ -1499,6 +1560,64 @@ function nextRegionCreditPosition(value, panel, position) {
|
|
|
1499
1560
|
creditPosition: position
|
|
1500
1561
|
});
|
|
1501
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
|
+
}
|
|
1502
1621
|
//#endregion
|
|
1503
1622
|
//#region src/decrypt.ts
|
|
1504
1623
|
const TRAE_AUTH_STORAGE_KEY = "iCubeAuthInfo://icube.cloudide";
|
|
@@ -4752,12 +4871,29 @@ function parseTraeActivityDiscount(value) {
|
|
|
4752
4871
|
}
|
|
4753
4872
|
//#endregion
|
|
4754
4873
|
//#region src/reasoning.ts
|
|
4874
|
+
/**
|
|
4875
|
+
* The DSH thinking levels this plugin can carry, in ascending order.
|
|
4876
|
+
*
|
|
4877
|
+
* `max` is the fifth level Trae's own client advertises — its bundle ships the
|
|
4878
|
+
* i18n keys `trae-chat-core.model.reasoning_effort.{low,medium,high,extra_high,max}`
|
|
4879
|
+
* and `ai.model.reasoning_effort.{low,medium,high,xhigh,max}`, and publishes the
|
|
4880
|
+
* per-model list from a remote config (`AI.agent.model.reasoning_effort_level_by_agent_model_v2`).
|
|
4881
|
+
* Leaving it out of this list meant a level the vendor could start serving was
|
|
4882
|
+
* filtered away at PARSE time, before anything downstream could decide (issue
|
|
4883
|
+
* #60). Listing it does not fabricate the level: `supported` is still built
|
|
4884
|
+
* only from what the upstream response actually contains, so a model that does
|
|
4885
|
+
* not advertise `max` never offers it.
|
|
4886
|
+
*
|
|
4887
|
+
* `minimal` is kept for the same reason in reverse — it is a DSH level, so the
|
|
4888
|
+
* type must be able to represent it even though no CN model has advertised it.
|
|
4889
|
+
*/
|
|
4755
4890
|
const TRAE_REASONING_EFFORTS = [
|
|
4756
4891
|
"minimal",
|
|
4757
4892
|
"low",
|
|
4758
4893
|
"medium",
|
|
4759
4894
|
"high",
|
|
4760
|
-
"xhigh"
|
|
4895
|
+
"xhigh",
|
|
4896
|
+
"max"
|
|
4761
4897
|
];
|
|
4762
4898
|
function parseReasoningCapability(value) {
|
|
4763
4899
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
@@ -5863,10 +5999,31 @@ function parseFeatures(value) {
|
|
|
5863
5999
|
return;
|
|
5864
6000
|
}
|
|
5865
6001
|
}
|
|
6002
|
+
/**
|
|
6003
|
+
* Trae's wire effort name -> the DSH thinking level.
|
|
6004
|
+
*
|
|
6005
|
+
* The FORWARD half of the mapping; `WIRE_EFFORT_MAP` in `catalog.ts` is its
|
|
6006
|
+
* exact inverse. Both halves must name the same levels, or a level survives one
|
|
6007
|
+
* direction and vanishes in the other.
|
|
6008
|
+
*
|
|
6009
|
+
* `medium` and `max` are here because Trae's own client advertises them
|
|
6010
|
+
* (`reasoning_effort.{low,medium,high,extra_high,max}` in its i18n bundle) and
|
|
6011
|
+
* publishes the per-model list remotely — so the day upstream starts sending
|
|
6012
|
+
* either, the parser must not filter it away before anything can use it (issue
|
|
6013
|
+
* #60). Measured 2026-10-25 on a live CN credential, the published options were
|
|
6014
|
+
* only `light / high / extra_high`; these entries are therefore inert today and
|
|
6015
|
+
* ready tomorrow, not a claim that the levels are served now.
|
|
6016
|
+
*
|
|
6017
|
+
* An unknown name is deliberately ABSENT rather than defaulted: `parse` drops
|
|
6018
|
+
* it, and the reverse map drops it too, so an unrecognised level can never be
|
|
6019
|
+
* reported to the user or sent upstream under a name the vendor never said.
|
|
6020
|
+
*/
|
|
5866
6021
|
const EFFORT_MAP = {
|
|
5867
6022
|
light: "low",
|
|
6023
|
+
medium: "medium",
|
|
5868
6024
|
high: "high",
|
|
5869
|
-
extra_high: "xhigh"
|
|
6025
|
+
extra_high: "xhigh",
|
|
6026
|
+
max: "max"
|
|
5870
6027
|
};
|
|
5871
6028
|
/**
|
|
5872
6029
|
* Parse only capabilities explicitly advertised by Trae's remote model API.
|
|
@@ -9110,7 +9267,7 @@ async function checkinOne(target, deps) {
|
|
|
9110
9267
|
return {
|
|
9111
9268
|
...base,
|
|
9112
9269
|
status: "failed",
|
|
9113
|
-
message: messageOf(error)
|
|
9270
|
+
message: messageOf$1(error)
|
|
9114
9271
|
};
|
|
9115
9272
|
}
|
|
9116
9273
|
if (credential === void 0) return {
|
|
@@ -9125,7 +9282,7 @@ async function checkinOne(target, deps) {
|
|
|
9125
9282
|
return {
|
|
9126
9283
|
...base,
|
|
9127
9284
|
status: "failed",
|
|
9128
|
-
message: messageOf(error)
|
|
9285
|
+
message: messageOf$1(error)
|
|
9129
9286
|
};
|
|
9130
9287
|
}
|
|
9131
9288
|
if (!status.enabled) return {
|
|
@@ -9161,7 +9318,7 @@ async function checkinOne(target, deps) {
|
|
|
9161
9318
|
return {
|
|
9162
9319
|
...base,
|
|
9163
9320
|
status: "failed",
|
|
9164
|
-
message: messageOf(error)
|
|
9321
|
+
message: messageOf$1(error)
|
|
9165
9322
|
};
|
|
9166
9323
|
}
|
|
9167
9324
|
}
|
|
@@ -9213,7 +9370,7 @@ async function testOne(target, modelId, probe, deps) {
|
|
|
9213
9370
|
return {
|
|
9214
9371
|
...base,
|
|
9215
9372
|
outcome: "auth",
|
|
9216
|
-
detail: messageOf(error)
|
|
9373
|
+
detail: messageOf$1(error)
|
|
9217
9374
|
};
|
|
9218
9375
|
}
|
|
9219
9376
|
if (credential === void 0) return {
|
|
@@ -9281,7 +9438,7 @@ function defaultWait(ms) {
|
|
|
9281
9438
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9282
9439
|
}
|
|
9283
9440
|
/** A safe message from an unknown throwable, bounded in length. */
|
|
9284
|
-
function messageOf(error) {
|
|
9441
|
+
function messageOf$1(error) {
|
|
9285
9442
|
return (error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
9286
9443
|
}
|
|
9287
9444
|
//#endregion
|
|
@@ -9748,6 +9905,316 @@ var UnconfiguredTraeUpstreamClient = class {
|
|
|
9748
9905
|
}
|
|
9749
9906
|
};
|
|
9750
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
|
|
9751
10218
|
//#region src/index.ts
|
|
9752
10219
|
const name = "dsh-connect-trae";
|
|
9753
10220
|
const inject = ["llm"];
|
|
@@ -9879,6 +10346,7 @@ const regionStateConfig = z.object({
|
|
|
9879
10346
|
lastCatalog: z.array(modelConfig).default([]),
|
|
9880
10347
|
enabledModelIds: z.array(z.string()).default([]),
|
|
9881
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)"),
|
|
9882
10350
|
contextBudgets: z.dict(z.number().step(1).min(1)).default({}),
|
|
9883
10351
|
creditPosition: z.union([z.const("composer"), z.const("stats")]).default("composer").description("积分读数显示位置:\"composer\" = 输入框工具栏(默认),\"stats\" = 输入框下方(与「轮次 / 步数 / token」统计同一行)。装了别的插件后读数不显示时改成 stats。"),
|
|
9884
10352
|
pool: z.object({
|
|
@@ -11021,8 +11489,48 @@ function apply(ctx, config) {
|
|
|
11021
11489
|
const settings = settingsCtx.settings;
|
|
11022
11490
|
ctx.effect(() => settings.configure({ auto: true }, ctx.fiber));
|
|
11023
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
|
+
});
|
|
11024
11531
|
ctx.on("loader/volatile-update", () => {
|
|
11025
11532
|
applySelection(current());
|
|
11533
|
+
syncTraeDrawImageTool(current());
|
|
11026
11534
|
});
|
|
11027
11535
|
let stopped = false;
|
|
11028
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
|
+
"version": "4.6.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|