dsh-connect-trae 4.6.2 → 4.6.4
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 +118 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +56 -5
- package/lib/index.d.ts +133 -6
- package/lib/index.js +286 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,123 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.6.4 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
> **缓存命中恒为 0% 不是插件丢了数字,是上游没有分配缓存池。** Trae 的
|
|
6
|
+
> `metadata` 事件里 `prompt_cache_pool_record` 非空才代表这次请求挂了前缀
|
|
7
|
+
> 缓存池;实测 CN/AI 双区、4 个账号、7 个模型、4 种 function,该字段
|
|
8
|
+
> **100% 为 `null`**。没有池,`cache_read_input_tokens` 只能是 0 —— 0% 是
|
|
9
|
+
> 正确读数。本版把这个事实变成可观测的,用户不必再怀疑插件吞了数据。
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
|
|
13
|
+
- **上游的缓存池事实与管线名不再被丢弃**(#67)。
|
|
14
|
+
- **现象**:无论简单还是复杂提问,缓存命中恒为 0%,十个会话一致;
|
|
15
|
+
同时 tps 在 60–80 之间浮动。
|
|
16
|
+
- **根因**:不是插件。issue #10 的转发修复仍在(`lib/index.js` 含
|
|
17
|
+
`cached_tokens`,同版本下 `workbuddy`/`dsh-opencode-go` 命中显示正常),
|
|
18
|
+
是上游 `llm_utils_chat` 改由 `llm_raw_chat_v2` 管线、火山方舟 `ep-…`
|
|
19
|
+
端点承载后,不再为这些请求分配前缀缓存池。当初产出 5248/9216 的
|
|
20
|
+
`scripts/probe-cache-convention.mjs` 原样重跑,现在三轮全是 0。
|
|
21
|
+
- **修法**:`sse.ts` 新增 `diagnostic` 事件类型,解码 `metadata`
|
|
22
|
+
(`prompt_cache_pool_record`)与 `timing_cost`(`name` 管线名、
|
|
23
|
+
`provider_model_name`);`solo-bridge.ts` 通过可选 `log` 上报
|
|
24
|
+
`promptCachePoolRecord` / `pipeline` / `providerModelName`;
|
|
25
|
+
`index.ts` 四个 `TraeSoloBridge` 构造点接入 `ctx.logger.warn`。
|
|
26
|
+
诊断事件不进答案文本、不改 usage 映射,**对外行为不变**。
|
|
27
|
+
- **为什么区分 `null` 与「字段不存在」**:`null` 是上游明确表示「没挂池」,
|
|
28
|
+
字段缺失只是没这条信息;两者混为一谈就又回到「看不出是谁的问题」。
|
|
29
|
+
|
|
30
|
+
### 修复(顺带守住一个回归)
|
|
31
|
+
|
|
32
|
+
- **`metadata` 不能被 peek 当成「上游已应答」**。它现在是被认识的事件类型,
|
|
33
|
+
若据此停止 peek,紧跟其后的拒绝(如 `1005` 未订阅)会退化成截断的 200 流
|
|
34
|
+
—— 正是 issue #19 的故障形态。`openTraeSource` 显式把 `diagnostic` 排除在
|
|
35
|
+
「已应答」之外。
|
|
36
|
+
|
|
37
|
+
### 测试
|
|
38
|
+
|
|
39
|
+
- `tests/sse.spec.ts`:`metadata` / `timing_cost` 解码为 `diagnostic`,
|
|
40
|
+
保留 `prompt_cache_pool_record: null`,且区分字段缺失。
|
|
41
|
+
- `tests/solo-bridge.spec.ts`:诊断事实上报(含 `null` 原样保留、事件不泄漏进
|
|
42
|
+
答案);`metadata` 之后紧跟的 `1005` 仍转成 402 `hard_credit`。
|
|
43
|
+
|
|
44
|
+
### 说明
|
|
45
|
+
|
|
46
|
+
- 诊断细节与完整证据链见 `docs/ISSUE67_DIAGNOSIS.md`(不随 npm 包发布)。
|
|
47
|
+
- 上游若开始分配缓存池,`cached_tokens` 会自动恢复显示,插件侧无需再改。
|
|
48
|
+
|
|
49
|
+
## 4.6.3 (2026-10-27)
|
|
50
|
+
|
|
51
|
+
> **国际版美国区账号不会再被当成新加坡区。** 它的凭据明明写着
|
|
52
|
+
> `host = https://grow-normal.traeapi.us`、`userRegion = "US"`,插件却因为
|
|
53
|
+
> 「不认识 US」回落到 edition 标签(`sg → ai`),把 US token 发去 `*.trae.ai`
|
|
54
|
+
> ——那边只会回 401;刷新模型拿到的 401 又被路由包成 500。用户看到的是
|
|
55
|
+
> 「拉模型失败,请重新登录」,而重登拿到的还是同一份 US 凭据,于是无限循环。
|
|
56
|
+
|
|
57
|
+
### 修复
|
|
58
|
+
|
|
59
|
+
- **美国区成为显式区域,不再静默回退成新加坡区**(#66)。
|
|
60
|
+
- **现象**:`pay_status` 401;「刷新模型」500
|
|
61
|
+
`{"error":"SOLO remote models returned HTTP 401"}`;宿主日志
|
|
62
|
+
`live ai model directory unavailable; serving the configured catalog`。
|
|
63
|
+
- **根因**:`regionOfUserRegion` 只认 CN/SG、`regionOfHost` 只认
|
|
64
|
+
`.trae.ai`/`.trae.cn`,两级都落空后由 `regionOfEdition("sg")` 决定——
|
|
65
|
+
US 被判定为 `ai`。而 `REGION_GATEWAYS` 只有 cn/ai 两个键,于是「查不到」
|
|
66
|
+
变成了「拿另一个区的网关」。
|
|
67
|
+
- **修法**:新增 `TraeServiceRegion = 'cn' | 'ai' | 'us'`;`TraeRegion`
|
|
68
|
+
(存储/路由桶)仍是 `cn | ai`。`assertSupportedRegion()` 是唯一从区域
|
|
69
|
+
身份进到路由桶的入口,US 在**选择网关之前**抛
|
|
70
|
+
`TraeUnsupportedRegionError`。所有读网关的客户端(用量、SOLO wire、远程
|
|
71
|
+
目录、Raw Chat、IDE 名册)都走这一道,注入 `baseUrl` 的测试/诊断路径也
|
|
72
|
+
不例外。
|
|
73
|
+
- **为什么不是一行 `us → ai` 映射**:`ai` 的网关/目录/chat 契约都是按新加坡
|
|
74
|
+
实测出来的,而报告里的 US 实测恰好相反(远程目录 404、`llm_utils_chat`
|
|
75
|
+
403 `40006`、`create_agent_task` 的 config 绑定未知)。映射过去只是换一种
|
|
76
|
+
方式发错请求。**美国区本身仍未接入**,本版只保证它不再被误路由。
|
|
77
|
+
|
|
78
|
+
- **上游 401/403 不再被包成 500**(#66)。
|
|
79
|
+
- 新增 `src/failure.ts`:`TraeHttpError` 携带上游状态,`httpFailureOf()` 把
|
|
80
|
+
401/403 映射为原状态 + `code: authentication`,本地「区域不支持」映射为
|
|
81
|
+
501 + `code: unsupported-region`,真正没有状态可言的失败仍 500。状态、
|
|
82
|
+
额度、账号表、模型刷新、模型测速、签到与账号池签到/测速这些路由全部改用
|
|
83
|
+
它。
|
|
84
|
+
- 卡片改为显示**服务端原文**(账号刷新、模型刷新、状态读取),不再只打印
|
|
85
|
+
`HTTP 401`——「网关拒绝了你」和「插件坏了」从此可以分辨。
|
|
86
|
+
|
|
87
|
+
- **US 凭据不可选中、不计费,但会被点名**(#66)。
|
|
88
|
+
- `readAll()` 过滤掉本版本不服务的区域(作用域与无作用域 store 都过滤),
|
|
89
|
+
因此它不会被 refresh、不会进账号表、不会发出任何请求;
|
|
90
|
+
`unsupportedCredentials()` 单独把它读出来给卡片点名。
|
|
91
|
+
- 未登录态显示「美国区(US)暂不支持」并列出账号名,写明请求不会发往新加坡
|
|
92
|
+
网关、重新登录没有变化;诊断面板多一行「未支持的区域」。
|
|
93
|
+
- `resolve()` 在「只有 US 凭据」时抛明确错误,而不是笼统的
|
|
94
|
+
`no signed-in account found`;导入的区域门控同步把 `us` 当外区跳过。
|
|
95
|
+
|
|
96
|
+
### 测试
|
|
97
|
+
|
|
98
|
+
- `tests/issue66-us-region.spec.ts`(23 例):US 的三条解析路径(claim / host /
|
|
99
|
+
完整凭据)都得到 `us` 而不是 `ai`;`payStatus` / 远程目录 / wire 名册 /
|
|
100
|
+
Raw Chat / chat 面对 US 凭据**零 `fetch` 调用**(含注入 `baseUrl` 的两例);
|
|
101
|
+
作用域与无作用域 store 都不解析它、但都能点名;401/403/404/US/普通异常的
|
|
102
|
+
状态映射;模型刷新路由逐条断言 401、501、500;状态文档携带通告且不重复英文
|
|
103
|
+
异常文本。
|
|
104
|
+
- `tests/issue66-card-us-notice.spec.tsx`(2 例):未登录文档带
|
|
105
|
+
`unsupportedRegion` 时卡片点名账号;失败请求显示服务端 `error` 原文而非
|
|
106
|
+
`HTTP 401`。
|
|
107
|
+
- **变异验证**:去掉 `regionOfUserRegion` 的 US 分支 → 2 例转红;刷新路由的
|
|
108
|
+
`failJson()` 改回 500 → 2 例转红;删掉 `readAll()` 的区域守卫 → 1 例转红。
|
|
109
|
+
- 全量 **95 files / 1211 tests** 通过(原 93 / 1186 + 新增 2 files / 25 例)。
|
|
110
|
+
|
|
111
|
+
### 说明
|
|
112
|
+
|
|
113
|
+
- **没有对 US 网关发过真机请求**:开发机没有美国区凭据。结论来自报告人给出的
|
|
114
|
+
实测对照(同一 token 打 US 200、打 SG 401)与代码路径;测试断言的是「不会
|
|
115
|
+
发出请求」,不是模拟 US 响应。
|
|
116
|
+
- 美国区的目录与 chat 仍待接入:目录需要 `get_detail_param` 的 `function`
|
|
117
|
+
单数 + `agent_type: null` 形态,chat 需要一次 IDE 侧的 `/api/agent/*`
|
|
118
|
+
实抓。拿到报文后即可按现有 `REGION_GATEWAYS` 的形状补齐。
|
|
119
|
+
- 诊断与取舍全文见仓库内 `docs/ISSUE66_DIAGNOSIS.md`(该文档不随包发布)。
|
|
120
|
+
|
|
3
121
|
## 4.6.2 (2026-10-27)
|
|
4
122
|
|
|
5
123
|
> **从插件市场装完,重启后插件不会再消失。** 安装其实失败了,但失败发生在
|
package/README.en.md
CHANGED
|
@@ -43,7 +43,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
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
45
|
- **Text-to-image (`trae_draw_image`)** — brings the signed-in account's Trae text-to-image service into the conversation: the model draws one image and the plugin **fetches the bytes too**, writes them under the session cwd and shows the picture inline. **Off by default, enabled per panel** — generation really spends credits (measured: ~8.8 for one `1920x1920` on CN personal), so unlike the image-input declaration it only happens when you say so. Protocol, size bounds, the two-step fetch and why the old "fetch" GET is really a second generation are in "Text-to-image" below.
|
|
46
|
-
- **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
|
+
- **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. **The US cluster (`userRegion: "US"` / `*.traeapi.us`) is recognized but not served yet**: a US credential is never routed to the Singapore gateways; the account panel names the account and says the region is unsupported, and that signing in again changes nothing (issue #66).
|
|
47
47
|
- **Region-isolated directories and selections** — CN and international each keep their own model directory, enabled picks, image opt-ins, and context budgets, and each provider reads only its own slot.
|
|
48
48
|
- **Vision follows Trae's own declaration** — when Trae marks a model `multimodal`, the plugin advertises it as such with no manual tick per model (issue #16). The rule is "**models you tick ∪ models Trae marks multimodal**": models Trae marks `false`, or does not mark at all, stay text-only and can still be turned on by hand. A row enabled automatically is shown ticked and read-only, with the reason. See "Image input" below.
|
|
49
49
|
- **Account switching** — refreshes the token list and lets users select an account without storing tokens in DSH settings. **Your choice is remembered and stays in effect**: the plugin will never switch accounts on its own just because the bound one ran out of credit or another usable one was found (switching bills a different person). So when the bound account is exhausted the card **says so explicitly** and lists the other accounts on this machine that still have credit, with their balances, for you to switch to in one click — instead of showing an empty panel or quietly rebinding you. "Refresh accounts & tokens" now always reports what happened (how many accounts were found, who is still bound) instead of appearing to do nothing. The dropdown names the product (`Trae` / `TRAE SOLO` / international) so a credential left behind by an uninstalled app is recognisable.
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
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
49
|
- **文生图(`trae_draw_image`)** —— 把登录账号的 Trae 文生图能力接进会话:模型画一张图,插件把**图片字节一起取回来**、写进会话目录再显示给你。**默认关闭、按面板单独开**——生图真实消耗积分(实测 CN 个人版 `1920x1920` 一张约 8.8),所以它和识图那种「让不让看图」的开关不同,只能由你点头。协议、尺寸边界、取图那两步,以及为什么「取图」那条 GET 其实是又一次生成,见下方「[文生图](#文生图trae_draw_image)」。
|
|
50
|
-
- **国内国际双区域自动识别** —— 自动发现 Trae CN / TRAE SOLO CN / Trae / TRAE SOLO 四个本地安装的登录账号;区域由凭证自带的 `userRegion` 声明自动判定(host 后缀与 edition
|
|
50
|
+
- **国内国际双区域自动识别** —— 自动发现 Trae CN / TRAE SOLO CN / Trae / TRAE SOLO 四个本地安装的登录账号;区域由凭证自带的 `userRegion` 声明自动判定(host 后缀与 edition 标签兜底),无需手动指定。**美国区(`userRegion: "US"` / `*.traeapi.us`)目前「识别但不接入」**:插件不会把 US 凭据当成新加坡区发出去,而会在账号面板明确写「美国区(US)暂不支持」并点名账号,同时说明重新登录不会改变结果(issue #66)。
|
|
51
51
|
- **目录与勾选按面板隔离** —— 三个面板各一套模型目录、勾选、图片开关与上下文预算,各自读各自的槽位。名录从**该面板所钉的那条网关**读取——原本钉在企业池的客户端去读消费池名录,会永远发现不了自己池的独占模型。
|
|
52
52
|
- **识图能力跟随 Trae 自身的标注** —— Trae 把某个模型标为 `multimodal` 时,插件直接按多模态对外声明,无需你在卡片上逐一手勾(issue #16)。判据是「**你手动勾选的 ∪ Trae 标注多模态的**」:Trae 明确标 `false` 或未标注的模型保持纯文本,你可以照旧手动为它们打开图片输入。卡片上被自动开启的那一行显示为已勾选且只读,并说明原因。详见下方「图片输入(识图)」。
|
|
53
53
|
- **多账号切换** —— 支持重新读取 Token 列表并选择账号;Token 不写入 DSH 设置。**你的选择会被记住并长期生效**:插件不会因为某个账号额度用尽、或检测到另一个可用账号就自作主张替你切换(换账号意味着换人计费)——唯一的例外是你**亲自打开**的账号池(见下一条),那是一个显式选择,而不是插件的自作主张。所以当绑定的账号额度耗尽时,卡片会**明确告诉你**,并把本机其他还有额度的账号连同其可用积分数列出来,由你一键切换——而不是让面板一直空着、也不自动把你切走。「刷新账号与 Token」每次都会回报结果(找到几个账号、当前仍绑定谁),不会再「点了没反应」。下拉里显示的是产品名(`Trae` / `TRAE SOLO` / 国际版),便于分辨凭据来自哪个安装。
|
package/lib/client.js
CHANGED
|
@@ -3905,6 +3905,22 @@ window.__ModuleLoader__.load({
|
|
|
3905
3905
|
* Structure, CSS classes, and button primitives mirror `dsh-subagent-default-model`'s
|
|
3906
3906
|
* `SubagentModelCard` so the two plugins share one external-presentation language.
|
|
3907
3907
|
*/
|
|
3908
|
+
/**
|
|
3909
|
+
* Failure text of a route: the server's own message when it sent one, else the
|
|
3910
|
+
* bare status.
|
|
3911
|
+
*
|
|
3912
|
+
* Issue #66: the card used to throw `HTTP ${status}` and drop the body, so the
|
|
3913
|
+
* Host's explanation ("SOLO remote models returned HTTP 401", or the US-region
|
|
3914
|
+
* verdict) never reached the user. A 401 from the wrong gateway and a plugin
|
|
3915
|
+
* crash therefore read identically.
|
|
3916
|
+
*/
|
|
3917
|
+
function routeErrorText(body, status) {
|
|
3918
|
+
if (typeof body === "object" && body !== null) {
|
|
3919
|
+
const error = body.error;
|
|
3920
|
+
if (typeof error === "string" && error.trim() !== "") return error;
|
|
3921
|
+
}
|
|
3922
|
+
return `HTTP ${status}`;
|
|
3923
|
+
}
|
|
3908
3924
|
const POLL_INTERVAL_MS = 6e4;
|
|
3909
3925
|
const TRAE_GITHUB_URL = "https://github.com/dingminhua/dsh-connect-trae";
|
|
3910
3926
|
/**
|
|
@@ -4412,7 +4428,7 @@ window.__ModuleLoader__.load({
|
|
|
4412
4428
|
...signal === void 0 ? {} : { signal }
|
|
4413
4429
|
});
|
|
4414
4430
|
const value = await response.json().catch(() => void 0);
|
|
4415
|
-
if (!response.ok) throw new Error(
|
|
4431
|
+
if (!response.ok) throw new Error(routeErrorText(value, response.status));
|
|
4416
4432
|
const usage = value;
|
|
4417
4433
|
if (mounted.current && signal?.aborted !== true) setStatusByRegion((prev) => ({
|
|
4418
4434
|
...prev,
|
|
@@ -4545,8 +4561,8 @@ window.__ModuleLoader__.load({
|
|
|
4545
4561
|
headers: { accept: "application/json" },
|
|
4546
4562
|
credentials: "same-origin"
|
|
4547
4563
|
});
|
|
4548
|
-
const body = await response.json();
|
|
4549
|
-
if (!response.ok || !Array.isArray(body.accounts)) throw new Error(
|
|
4564
|
+
const body = await response.json().catch(() => void 0);
|
|
4565
|
+
if (!response.ok || body === void 0 || !Array.isArray(body.accounts)) throw new Error(routeErrorText(body, response.status));
|
|
4550
4566
|
/**
|
|
4551
4567
|
* What this press must persist, and why it is NOT `accounts.find(selected)`.
|
|
4552
4568
|
*
|
|
@@ -4891,8 +4907,8 @@ window.__ModuleLoader__.load({
|
|
|
4891
4907
|
headers: { accept: "application/json" },
|
|
4892
4908
|
credentials: "same-origin"
|
|
4893
4909
|
});
|
|
4894
|
-
const body = await response.json();
|
|
4895
|
-
if (!response.ok || !Array.isArray(body.models)) throw new Error(
|
|
4910
|
+
const body = await response.json().catch(() => void 0);
|
|
4911
|
+
if (!response.ok || body === void 0 || !Array.isArray(body.models)) throw new Error(routeErrorText(body, response.status));
|
|
4896
4912
|
const fresh = body.models;
|
|
4897
4913
|
const freshIds = new Set(fresh.map((model) => model.id));
|
|
4898
4914
|
const stillEnabled = [...activeEnabledIds].filter((id) => freshIds.has(id));
|
|
@@ -5907,6 +5923,20 @@ window.__ModuleLoader__.load({
|
|
|
5907
5923
|
children: activeRegion === "cn" ? t("row.tabCn") : activeRegion === "enterprise" ? t("row.tabEnterprise") : t("row.tabAi")
|
|
5908
5924
|
})]
|
|
5909
5925
|
}),
|
|
5926
|
+
status.unsupportedRegion === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5927
|
+
className: "dsm-trae-diag-row",
|
|
5928
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5929
|
+
className: "dsm-trae-diag-label",
|
|
5930
|
+
children: t("row.diagUnsupportedRegion")
|
|
5931
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5932
|
+
className: "dsm-trae-diag-value dsm-trae-diag-bad",
|
|
5933
|
+
children: [
|
|
5934
|
+
status.unsupportedRegion.region.toUpperCase(),
|
|
5935
|
+
" · ",
|
|
5936
|
+
status.unsupportedRegion.accounts.join(", ")
|
|
5937
|
+
]
|
|
5938
|
+
})]
|
|
5939
|
+
}),
|
|
5910
5940
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5911
5941
|
className: "dsm-trae-diag-row",
|
|
5912
5942
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -6046,6 +6076,21 @@ window.__ModuleLoader__.load({
|
|
|
6046
6076
|
className: "dsm-trae-usage-text",
|
|
6047
6077
|
children: status.message ?? t("row.signedOutHint")
|
|
6048
6078
|
}),
|
|
6079
|
+
status.unsupportedRegion === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
6080
|
+
className: "dsm-trae-quarantine",
|
|
6081
|
+
open: true,
|
|
6082
|
+
children: [
|
|
6083
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("row.unsupportedRegionTitle") }),
|
|
6084
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6085
|
+
className: "dsm-trae-quarantine-hint",
|
|
6086
|
+
children: t("row.unsupportedRegionHint")
|
|
6087
|
+
}),
|
|
6088
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
6089
|
+
className: "dsm-trae-quarantine-list",
|
|
6090
|
+
children: status.unsupportedRegion.accounts.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: name }) }, name))
|
|
6091
|
+
})
|
|
6092
|
+
]
|
|
6093
|
+
}),
|
|
6049
6094
|
status.quarantined && status.quarantined.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
6050
6095
|
className: "dsm-trae-quarantine",
|
|
6051
6096
|
open: true,
|
|
@@ -6843,6 +6888,9 @@ window.__ModuleLoader__.load({
|
|
|
6843
6888
|
"row.settingsWriteFailed": "The setting was not saved: {message}",
|
|
6844
6889
|
"row.signedOut": "Not signed in",
|
|
6845
6890
|
"row.signedOutHint": "Sign in once in the Trae desktop app; this plugin follows that sign-in automatically.",
|
|
6891
|
+
"row.unsupportedRegionTitle": "US region is not supported yet",
|
|
6892
|
+
"row.unsupportedRegionHint": "A Trae account on this machine belongs to the United States region (userRegion \"US\" / host *.traeapi.us). This build does not serve that region, so the account is NOT used and no request is sent to the Singapore gateway — signing in again changes nothing. Use a CN or Singapore account (or the Trae CN build) in this panel.",
|
|
6893
|
+
"row.diagUnsupportedRegion": "Unsupported region",
|
|
6846
6894
|
"row.searchedTitle": "Paths checked",
|
|
6847
6895
|
"row.searchedHint": "No sign-in was found. These locations were probed; if your Trae stores its login elsewhere, please report the path.",
|
|
6848
6896
|
"row.searchedMore": "Show {count} more",
|
|
@@ -7227,6 +7275,9 @@ window.__ModuleLoader__.load({
|
|
|
7227
7275
|
"row.settingsWriteFailed": "设置未能保存:{message}",
|
|
7228
7276
|
"row.signedOut": "未登录",
|
|
7229
7277
|
"row.signedOutHint": "在 Trae 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
|
|
7278
|
+
"row.unsupportedRegionTitle": "美国区(US)暂不支持",
|
|
7279
|
+
"row.unsupportedRegionHint": "本机有一个 Trae 账号属于美国区(userRegion \"US\" / 主机 *.traeapi.us)。本版本尚未支持该区,因此不会使用这个账号,也不会把请求发往新加坡网关——重新登录不会有任何变化。请在该面板改用国内版或新加坡区账号(或使用 Trae 国内版客户端)。",
|
|
7280
|
+
"row.diagUnsupportedRegion": "未支持的区域",
|
|
7230
7281
|
"row.searchedTitle": "已检查的路径",
|
|
7231
7282
|
"row.searchedHint": "未找到登录信息。以下是插件探测过的位置;如果你的 Trae 把登录信息存在别处,请把该路径反馈给我们。",
|
|
7232
7283
|
"row.searchedMore": "展开其余 {count} 条",
|
package/lib/index.d.ts
CHANGED
|
@@ -142,6 +142,23 @@ declare function traeStorageCandidates(platform?: NodeJS.Platform, home?: string
|
|
|
142
142
|
//#region src/region.d.ts
|
|
143
143
|
/** Storage/routing bucket: the CN service or the international (ai) service. */
|
|
144
144
|
type TraeRegion = 'cn' | 'ai';
|
|
145
|
+
/**
|
|
146
|
+
* The service a credential's own claims name.
|
|
147
|
+
*
|
|
148
|
+
* WIDER than {@link TraeRegion} on purpose. The US cluster is a real,
|
|
149
|
+
* distinguishable Trae service — `userRegion: "US"` and hosts under
|
|
150
|
+
* `*.traeapi.us` (issue #66, measured on an international 3.6.4 install:
|
|
151
|
+
* `grow-normal.traeapi.us` answers the pay endpoint with 200 where the SG
|
|
152
|
+
* gateway answers 401) — but this plugin does not serve it yet.
|
|
153
|
+
*
|
|
154
|
+
* Naming it as its own value is the whole fix: while the US claim was
|
|
155
|
+
* unrecognised, `regionOfCredential` fell through to the edition label (`sg` →
|
|
156
|
+
* `ai`) and shipped a US token to `*.trae.ai`, where every read answered 401
|
|
157
|
+
* and the card reported it as a generic "refresh failed". A value the type
|
|
158
|
+
* system carries forces every gateway site to say out loud what it does with a
|
|
159
|
+
* region it cannot serve, instead of silently inheriting the SG routing.
|
|
160
|
+
*/
|
|
161
|
+
type TraeServiceRegion = 'cn' | 'ai' | 'us';
|
|
145
162
|
/**
|
|
146
163
|
* One section of the plugin card: a provider the user can switch, bind an
|
|
147
164
|
* account to, and pick models for, INDEPENDENTLY.
|
|
@@ -188,20 +205,36 @@ declare function regionOfEdition(edition: TraeEdition): TraeRegion;
|
|
|
188
205
|
* Region from the credential's `userRegion` claim. The desktop storage spells
|
|
189
206
|
* it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
|
|
190
207
|
* spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
|
|
208
|
+
*
|
|
209
|
+
* `"US"` is recognized as its own region (issue #66). It used to fall through
|
|
210
|
+
* this function unrecognised, which is what let a US credential inherit the SG
|
|
211
|
+
* routing from the edition label.
|
|
212
|
+
*/
|
|
213
|
+
declare function regionOfUserRegion(value: unknown): TraeServiceRegion | undefined;
|
|
214
|
+
/**
|
|
215
|
+
* Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` →
|
|
216
|
+
* cn, `.traeapi.us` → us).
|
|
217
|
+
*
|
|
218
|
+
* The US suffix is checked explicitly. It is NOT a special case of
|
|
219
|
+
* `.trae.ai`: `grow-normal.traeapi.us` is a different cluster with its own
|
|
220
|
+
* pay/pay-status endpoints, and the plugin's SG gateways answer a US token
|
|
221
|
+
* with 401 (issue #66).
|
|
191
222
|
*/
|
|
192
|
-
declare function
|
|
193
|
-
/** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
|
|
194
|
-
declare function regionOfHost(host: string | undefined): TraeRegion | undefined;
|
|
223
|
+
declare function regionOfHost(host: string | undefined): TraeServiceRegion | undefined;
|
|
195
224
|
/**
|
|
196
225
|
* Region of a credential: the `userRegion` claim wins, the host suffix is the
|
|
197
226
|
* fallback, and the edition label is the last resort. Every level is derived
|
|
198
227
|
* from data the credential itself carries, so no user configuration is needed.
|
|
228
|
+
*
|
|
229
|
+
* The edition fallback is only ever reached by a credential that carries
|
|
230
|
+
* NEITHER a claim NOR a resolvable host (a CLI token). That matters: it is the
|
|
231
|
+
* reason a US credential no longer becomes `ai` — its own claim resolves first.
|
|
199
232
|
*/
|
|
200
233
|
declare function regionOfCredential(credential: {
|
|
201
234
|
edition: TraeEdition;
|
|
202
235
|
host?: string;
|
|
203
236
|
userRegion?: string;
|
|
204
|
-
}):
|
|
237
|
+
}): TraeServiceRegion;
|
|
205
238
|
//#endregion
|
|
206
239
|
//#region src/catalog.d.ts
|
|
207
240
|
type TraeInputModality = 'text' | 'image';
|
|
@@ -1266,6 +1299,20 @@ interface TraeWebPayStatus {
|
|
|
1266
1299
|
maxUsage: number;
|
|
1267
1300
|
};
|
|
1268
1301
|
}
|
|
1302
|
+
/**
|
|
1303
|
+
* A credential on this machine that names a region this build cannot serve yet
|
|
1304
|
+
* (issue #66: the US cluster).
|
|
1305
|
+
*
|
|
1306
|
+
* Carried on BOTH shapes: the panel that shows it is normally signed OUT —
|
|
1307
|
+
* the credential is deliberately excluded from billing — so the signed-out
|
|
1308
|
+
* document is the only place the user can learn that a real sign-in is sitting
|
|
1309
|
+
* on disk and why it is not being used.
|
|
1310
|
+
*/
|
|
1311
|
+
interface TraeWebUnsupportedRegion {
|
|
1312
|
+
region: 'us';
|
|
1313
|
+
/** Display names of the credentials carrying that claim; never token material. */
|
|
1314
|
+
accounts: readonly string[];
|
|
1315
|
+
}
|
|
1269
1316
|
/** The JSON document the plugin card renders. */
|
|
1270
1317
|
type TraeWebUsage = {
|
|
1271
1318
|
status: 'signed-out';
|
|
@@ -1278,6 +1325,8 @@ type TraeWebUsage = {
|
|
|
1278
1325
|
* after a lost copy, so that is where the notice has to be.
|
|
1279
1326
|
*/
|
|
1280
1327
|
quarantined?: readonly TraeWebQuarantine[];
|
|
1328
|
+
/** A recognized-but-unserved region on this machine (issue #66). */
|
|
1329
|
+
unsupportedRegion?: TraeWebUnsupportedRegion;
|
|
1281
1330
|
enabled?: boolean;
|
|
1282
1331
|
} | {
|
|
1283
1332
|
status: 'signed-in';
|
|
@@ -1338,6 +1387,12 @@ type TraeWebUsage = {
|
|
|
1338
1387
|
/** Subscription status of an international account (region ai only). */
|
|
1339
1388
|
payStatus?: TraeWebPayStatus;
|
|
1340
1389
|
payStatusError?: string;
|
|
1390
|
+
/**
|
|
1391
|
+
* A recognized-but-unserved region on this machine (issue #66). Present
|
|
1392
|
+
* alongside a working account when the machine also holds e.g. a US
|
|
1393
|
+
* login — that account is not billed, but the panel still says so.
|
|
1394
|
+
*/
|
|
1395
|
+
unsupportedRegion?: TraeWebUnsupportedRegion;
|
|
1341
1396
|
/**
|
|
1342
1397
|
* This panel's account-pool state, when the Host build supports it.
|
|
1343
1398
|
*
|
|
@@ -1830,6 +1885,19 @@ interface TraeAccountChoice {
|
|
|
1830
1885
|
refreshExpiresAtMs?: number;
|
|
1831
1886
|
selected: boolean;
|
|
1832
1887
|
}
|
|
1888
|
+
/**
|
|
1889
|
+
* A credential on this machine that names a region this build cannot serve yet
|
|
1890
|
+
* (issue #66: the US cluster).
|
|
1891
|
+
*
|
|
1892
|
+
* Reported so the card can explain a signed-out panel that a user knows is
|
|
1893
|
+
* signed in. It is never selectable and never billed: `readAll()` skips these,
|
|
1894
|
+
* so they cannot reach a gateway at all.
|
|
1895
|
+
*/
|
|
1896
|
+
interface TraeUnsupportedCredential {
|
|
1897
|
+
region: Exclude<TraeServiceRegion, TraeRegion>;
|
|
1898
|
+
/** Display identity of the credential — never token material. */
|
|
1899
|
+
accountName: string;
|
|
1900
|
+
}
|
|
1833
1901
|
/**
|
|
1834
1902
|
* Why one candidate path did not yield an account. These are safe to surface:
|
|
1835
1903
|
* they carry paths and error text, never token material.
|
|
@@ -2186,6 +2254,22 @@ declare class TraeCredentialStore {
|
|
|
2186
2254
|
expiresAtMs?: number;
|
|
2187
2255
|
source?: TraeCredential['source'];
|
|
2188
2256
|
}>;
|
|
2257
|
+
/**
|
|
2258
|
+
* Credentials this machine holds for a region this build cannot serve yet.
|
|
2259
|
+
*
|
|
2260
|
+
* Issue #66: a US credential used to resolve to `ai`, so the panel billed it
|
|
2261
|
+
* against the SG gateway and every read answered 401. Now it is excluded from
|
|
2262
|
+
* resolution (see `readAll`), which would leave the panel saying "not signed
|
|
2263
|
+
* in" on a machine the user knows IS signed in — so the exclusion has to be
|
|
2264
|
+
* explainable. This reads exactly the credentials `readAll` refuses to
|
|
2265
|
+
* RESOLVE and reports their display identity; nothing here is selectable,
|
|
2266
|
+
* billable, or sent anywhere.
|
|
2267
|
+
*
|
|
2268
|
+
* Scoped to the international bucket: a US install is an international
|
|
2269
|
+
* install, so only the `ai` panel (and an unscoped diagnostic store) can be
|
|
2270
|
+
* the one missing an account because of it.
|
|
2271
|
+
*/
|
|
2272
|
+
unsupportedCredentials(): Promise<TraeUnsupportedCredential[]>;
|
|
2189
2273
|
desktopFilePresent(): Promise<boolean>;
|
|
2190
2274
|
/**
|
|
2191
2275
|
* Remove every plugin-owned copy this store could read (per-region file,
|
|
@@ -3342,6 +3426,29 @@ type TraeStreamEvent = {
|
|
|
3342
3426
|
*/
|
|
3343
3427
|
cacheReadTokens?: number;
|
|
3344
3428
|
cacheWriteTokens?: number;
|
|
3429
|
+
} | {
|
|
3430
|
+
/**
|
|
3431
|
+
* The upstream's own account of how the request was served: `metadata`
|
|
3432
|
+
* (acceptance, plus `prompt_cache_pool_record`) and `timing_cost` (the
|
|
3433
|
+
* pipeline and the backing provider model).
|
|
3434
|
+
*
|
|
3435
|
+
* These events were previously folded into `unknown` and dropped. That
|
|
3436
|
+
* mattered for issue #67: `prompt_cache_pool_record` is the ONLY upstream
|
|
3437
|
+
* signal that says whether a prompt-cache pool was even attached to the
|
|
3438
|
+
* request, so with it dropped a "0% cache" report could not be told apart
|
|
3439
|
+
* from a plugin that lost the numbers. See docs/ISSUE67_DIAGNOSIS.md.
|
|
3440
|
+
*/
|
|
3441
|
+
type: 'diagnostic';
|
|
3442
|
+
/**
|
|
3443
|
+
* `metadata.prompt_cache_pool_record`. `null` is a real, observed value
|
|
3444
|
+
* (100% of measured requests on 2026-10-28) and means the upstream attached
|
|
3445
|
+
* no cache pool; it is kept distinct from "the field was absent".
|
|
3446
|
+
*/
|
|
3447
|
+
promptCachePoolRecord?: unknown;
|
|
3448
|
+
/** `timing_cost.name`, e.g. `llm_raw_chat_v2`. */
|
|
3449
|
+
pipeline?: string;
|
|
3450
|
+
/** `timing_cost.provider_model_name` — the model that actually answered. */
|
|
3451
|
+
providerModelName?: string;
|
|
3345
3452
|
} | {
|
|
3346
3453
|
type: 'done';
|
|
3347
3454
|
finishReason: string;
|
|
@@ -3485,7 +3592,21 @@ declare class TraeSoloBridge implements TraeUpstreamClient {
|
|
|
3485
3592
|
private readonly upstream;
|
|
3486
3593
|
private readonly catalog?;
|
|
3487
3594
|
private readonly wireResolver?;
|
|
3488
|
-
|
|
3595
|
+
/**
|
|
3596
|
+
* Optional sink for the upstream's own diagnostics (`metadata` /
|
|
3597
|
+
* `timing_cost`). Issue #67: without it, `prompt_cache_pool_record: null`
|
|
3598
|
+
* was invisible, so "the upstream attached no cache pool" and "the plugin
|
|
3599
|
+
* lost the numbers" looked identical to a user reading a 0% cache rate.
|
|
3600
|
+
*/
|
|
3601
|
+
private readonly log?;
|
|
3602
|
+
constructor(upstream: TraeUpstreamClient, catalog?: Pick<TraeCatalog, "current"> | undefined, wireResolver?: TraeWireResolver | undefined,
|
|
3603
|
+
/**
|
|
3604
|
+
* Optional sink for the upstream's own diagnostics (`metadata` /
|
|
3605
|
+
* `timing_cost`). Issue #67: without it, `prompt_cache_pool_record: null`
|
|
3606
|
+
* was invisible, so "the upstream attached no cache pool" and "the plugin
|
|
3607
|
+
* lost the numbers" looked identical to a user reading a 0% cache rate.
|
|
3608
|
+
*/
|
|
3609
|
+
log?: ((message: string, detail?: unknown) => void) | undefined);
|
|
3489
3610
|
chatStream(bodyJson: string, signal?: AbortSignal): Promise<TraeChatResult>;
|
|
3490
3611
|
}
|
|
3491
3612
|
//#endregion
|
|
@@ -3875,7 +3996,13 @@ declare class TraeUsageClient {
|
|
|
3875
3996
|
private readonly baseUrl;
|
|
3876
3997
|
private readonly timeoutMs;
|
|
3877
3998
|
constructor(options: TraeUsageOptions);
|
|
3878
|
-
/**
|
|
3999
|
+
/**
|
|
4000
|
+
* Region of the current credential; `cn` when unresolvable.
|
|
4001
|
+
*
|
|
4002
|
+
* A US credential throws {@link TraeUnsupportedRegionError} instead of being
|
|
4003
|
+
* rounded to the international bucket — issue #66: the rounding is what sent
|
|
4004
|
+
* a US token to the SG pay gateway, where the answer is 401.
|
|
4005
|
+
*/
|
|
3879
4006
|
private currentRegion;
|
|
3880
4007
|
/**
|
|
3881
4008
|
* Pay base for the current credential's region. An explicit baseUrl (tests,
|
package/lib/index.js
CHANGED
|
@@ -1154,6 +1154,31 @@ function createTraeAdapter(options) {
|
|
|
1154
1154
|
}
|
|
1155
1155
|
//#endregion
|
|
1156
1156
|
//#region src/region.ts
|
|
1157
|
+
/**
|
|
1158
|
+
* Code the Host reports for a region this build recognises but cannot serve.
|
|
1159
|
+
* The card and any external caller can switch on it; the message is only for
|
|
1160
|
+
* humans.
|
|
1161
|
+
*/
|
|
1162
|
+
const TRAE_UNSUPPORTED_REGION_CODE = "unsupported-region";
|
|
1163
|
+
/** Message for a US credential, phrased to end the re-login loop. */
|
|
1164
|
+
const TRAE_UNSUPPORTED_REGION_MESSAGE = "This Trae account belongs to the United States region (userRegion \"US\", host *.traeapi.us), which dsh-connect-trae does not support yet. Requests are NOT sent to the Singapore gateway; sign in with a CN or Singapore account (or use the CN build of Trae) to use this panel.";
|
|
1165
|
+
/**
|
|
1166
|
+
* A credential that resolved to a region this build does not serve.
|
|
1167
|
+
*
|
|
1168
|
+
* Thrown BEFORE any request is built, by every client that reads a credential:
|
|
1169
|
+
* the failure has to be a local, explicit verdict, not a 401 from the wrong
|
|
1170
|
+
* gateway (issue #66: a US token sent to `*.trae.ai` answers 401, which read as
|
|
1171
|
+
* "your sign-in expired" and produced an endless re-login loop).
|
|
1172
|
+
*/
|
|
1173
|
+
var TraeUnsupportedRegionError = class extends Error {
|
|
1174
|
+
region;
|
|
1175
|
+
name = "TraeUnsupportedRegionError";
|
|
1176
|
+
code = TRAE_UNSUPPORTED_REGION_CODE;
|
|
1177
|
+
constructor(region) {
|
|
1178
|
+
super(TRAE_UNSUPPORTED_REGION_MESSAGE);
|
|
1179
|
+
this.region = region;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1157
1182
|
/** Every panel, in card tab order. */
|
|
1158
1183
|
const TRAE_PANELS = [
|
|
1159
1184
|
"cn",
|
|
@@ -1223,6 +1248,10 @@ function regionOfEdition(edition) {
|
|
|
1223
1248
|
* Region from the credential's `userRegion` claim. The desktop storage spells
|
|
1224
1249
|
* it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
|
|
1225
1250
|
* spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
|
|
1251
|
+
*
|
|
1252
|
+
* `"US"` is recognized as its own region (issue #66). It used to fall through
|
|
1253
|
+
* this function unrecognised, which is what let a US credential inherit the SG
|
|
1254
|
+
* routing from the edition label.
|
|
1226
1255
|
*/
|
|
1227
1256
|
function regionOfUserRegion(value) {
|
|
1228
1257
|
const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
|
|
@@ -1230,8 +1259,17 @@ function regionOfUserRegion(value) {
|
|
|
1230
1259
|
const lowered = raw.trim().toLowerCase();
|
|
1231
1260
|
if (lowered === "cn") return "cn";
|
|
1232
1261
|
if (lowered === "sg" || lowered === "ai") return "ai";
|
|
1262
|
+
if (lowered === "us") return "us";
|
|
1233
1263
|
}
|
|
1234
|
-
/**
|
|
1264
|
+
/**
|
|
1265
|
+
* Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` →
|
|
1266
|
+
* cn, `.traeapi.us` → us).
|
|
1267
|
+
*
|
|
1268
|
+
* The US suffix is checked explicitly. It is NOT a special case of
|
|
1269
|
+
* `.trae.ai`: `grow-normal.traeapi.us` is a different cluster with its own
|
|
1270
|
+
* pay/pay-status endpoints, and the plugin's SG gateways answer a US token
|
|
1271
|
+
* with 401 (issue #66).
|
|
1272
|
+
*/
|
|
1235
1273
|
function regionOfHost(host) {
|
|
1236
1274
|
if (host === void 0) return void 0;
|
|
1237
1275
|
const trimmed = host.trim();
|
|
@@ -1242,6 +1280,7 @@ function regionOfHost(host) {
|
|
|
1242
1280
|
} catch {
|
|
1243
1281
|
return;
|
|
1244
1282
|
}
|
|
1283
|
+
if (hostname === "traeapi.us" || hostname.endsWith(".traeapi.us")) return "us";
|
|
1245
1284
|
if (hostname === "trae.ai" || hostname.endsWith(".trae.ai")) return "ai";
|
|
1246
1285
|
if (hostname === "trae.cn" || hostname.endsWith(".trae.cn") || hostname.endsWith(".trae.com.cn")) return "cn";
|
|
1247
1286
|
}
|
|
@@ -1249,10 +1288,32 @@ function regionOfHost(host) {
|
|
|
1249
1288
|
* Region of a credential: the `userRegion` claim wins, the host suffix is the
|
|
1250
1289
|
* fallback, and the edition label is the last resort. Every level is derived
|
|
1251
1290
|
* from data the credential itself carries, so no user configuration is needed.
|
|
1291
|
+
*
|
|
1292
|
+
* The edition fallback is only ever reached by a credential that carries
|
|
1293
|
+
* NEITHER a claim NOR a resolvable host (a CLI token). That matters: it is the
|
|
1294
|
+
* reason a US credential no longer becomes `ai` — its own claim resolves first.
|
|
1252
1295
|
*/
|
|
1253
1296
|
function regionOfCredential(credential) {
|
|
1254
1297
|
return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host) ?? regionOfEdition(credential.edition);
|
|
1255
1298
|
}
|
|
1299
|
+
/** Whether this build actually serves the region (only `cn` and `ai` today). */
|
|
1300
|
+
function isSupportedRegion(region) {
|
|
1301
|
+
return region !== "us";
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Narrow a resolved credential region to a servable bucket, or throw
|
|
1305
|
+
* {@link TraeUnsupportedRegionError}.
|
|
1306
|
+
*
|
|
1307
|
+
* Every gateway lookup goes through this instead of indexing
|
|
1308
|
+
* `REGION_GATEWAYS`/`TRAE_REMOTE_DIRECTORY_FUNCTIONS` directly: those maps hold
|
|
1309
|
+
* only the two supported clusters, and the missing index IS the bug in issue
|
|
1310
|
+
* #66 (`REGION_GATEWAYS[regionOfCredential(us)]` silently read the `ai` entry,
|
|
1311
|
+
* because the resolver had already rounded the US claim down to SG).
|
|
1312
|
+
*/
|
|
1313
|
+
function assertSupportedRegion(region) {
|
|
1314
|
+
if (!isSupportedRegion(region)) throw new TraeUnsupportedRegionError(region);
|
|
1315
|
+
return region;
|
|
1316
|
+
}
|
|
1256
1317
|
//#endregion
|
|
1257
1318
|
//#region src/status-paths.ts
|
|
1258
1319
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
@@ -2839,7 +2900,7 @@ var TraeCredentialStore = class {
|
|
|
2839
2900
|
accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`),
|
|
2840
2901
|
edition: credential.edition,
|
|
2841
2902
|
enterprise: isEnterpriseAccount(credential),
|
|
2842
|
-
region: regionOfCredential(credential),
|
|
2903
|
+
region: assertSupportedRegion(regionOfCredential(credential)),
|
|
2843
2904
|
source: "dsh",
|
|
2844
2905
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
2845
2906
|
...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
|
|
@@ -3042,7 +3103,11 @@ var TraeCredentialStore = class {
|
|
|
3042
3103
|
}
|
|
3043
3104
|
async resolve() {
|
|
3044
3105
|
const credential = await this.current();
|
|
3045
|
-
if (credential === void 0)
|
|
3106
|
+
if (credential === void 0) {
|
|
3107
|
+
const first = (await this.unsupportedCredentials().catch(() => []))[0];
|
|
3108
|
+
if (first !== void 0) throw new TraeUnsupportedRegionError(first.region);
|
|
3109
|
+
throw new Error(`trae: no signed-in account found (${this.candidates().map((item) => item.path).join(" or ")})`);
|
|
3110
|
+
}
|
|
3046
3111
|
if (credential.expiresAtMs > Date.now() + this.refreshMarginMs) return credential;
|
|
3047
3112
|
this.inflight ??= this.refreshNow(credential).finally(() => {
|
|
3048
3113
|
this.inflight = void 0;
|
|
@@ -3062,6 +3127,42 @@ var TraeCredentialStore = class {
|
|
|
3062
3127
|
return { state: "signed-out" };
|
|
3063
3128
|
}
|
|
3064
3129
|
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Credentials this machine holds for a region this build cannot serve yet.
|
|
3132
|
+
*
|
|
3133
|
+
* Issue #66: a US credential used to resolve to `ai`, so the panel billed it
|
|
3134
|
+
* against the SG gateway and every read answered 401. Now it is excluded from
|
|
3135
|
+
* resolution (see `readAll`), which would leave the panel saying "not signed
|
|
3136
|
+
* in" on a machine the user knows IS signed in — so the exclusion has to be
|
|
3137
|
+
* explainable. This reads exactly the credentials `readAll` refuses to
|
|
3138
|
+
* RESOLVE and reports their display identity; nothing here is selectable,
|
|
3139
|
+
* billable, or sent anywhere.
|
|
3140
|
+
*
|
|
3141
|
+
* Scoped to the international bucket: a US install is an international
|
|
3142
|
+
* install, so only the `ai` panel (and an unscoped diagnostic store) can be
|
|
3143
|
+
* the one missing an account because of it.
|
|
3144
|
+
*/
|
|
3145
|
+
async unsupportedCredentials() {
|
|
3146
|
+
if (this.region !== void 0 && this.region !== "ai") return [];
|
|
3147
|
+
const found = /* @__PURE__ */ new Map();
|
|
3148
|
+
const consider = (credential) => {
|
|
3149
|
+
const region = regionOfCredential(credential);
|
|
3150
|
+
if (isSupportedRegion(region)) return;
|
|
3151
|
+
const id = traeAccountId(credential);
|
|
3152
|
+
if (found.has(id)) return;
|
|
3153
|
+
found.set(id, {
|
|
3154
|
+
region,
|
|
3155
|
+
accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`)
|
|
3156
|
+
});
|
|
3157
|
+
};
|
|
3158
|
+
try {
|
|
3159
|
+
for (const credential of (await this.readDesktopAll()).credentials) consider(credential);
|
|
3160
|
+
} catch {}
|
|
3161
|
+
try {
|
|
3162
|
+
for (const credential of await this.readOwns()) consider(credential);
|
|
3163
|
+
} catch {}
|
|
3164
|
+
return [...found.values()];
|
|
3165
|
+
}
|
|
3065
3166
|
async desktopFilePresent() {
|
|
3066
3167
|
for (const candidate of this.candidates()) try {
|
|
3067
3168
|
if ((await stat(candidate.path)).isFile()) return true;
|
|
@@ -3216,6 +3317,7 @@ var TraeCredentialStore = class {
|
|
|
3216
3317
|
const { credentials: desktop } = await this.readDesktopAll();
|
|
3217
3318
|
const byAccount = /* @__PURE__ */ new Map();
|
|
3218
3319
|
const consider = (credential) => {
|
|
3320
|
+
if (!isSupportedRegion(regionOfCredential(credential))) return;
|
|
3219
3321
|
if (!this.matchesRegion(credential)) return;
|
|
3220
3322
|
const id = traeAccountId(credential);
|
|
3221
3323
|
const incumbent = byAccount.get(id);
|
|
@@ -4608,7 +4710,7 @@ var TraeOfficialRosterClient = class {
|
|
|
4608
4710
|
if (this.failedAt !== void 0 && now - this.failedAt < ROSTER_CACHE_MS) return void 0;
|
|
4609
4711
|
try {
|
|
4610
4712
|
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
4611
|
-
const region = regionOfCredential(credential);
|
|
4713
|
+
const region = assertSupportedRegion(regionOfCredential(credential));
|
|
4612
4714
|
const base = this.options.baseUrl ?? (this.options.route === "enterprise" ? "https://console.enterprise.trae.cn" : REGION_GATEWAYS[region].chat);
|
|
4613
4715
|
const response = await this.fetchImpl(traeEndpoint(base, TRAE_MODEL_DETAIL_PATH), {
|
|
4614
4716
|
method: "POST",
|
|
@@ -5129,7 +5231,7 @@ var TraeSoloUpstreamClient = class {
|
|
|
5129
5231
|
*/
|
|
5130
5232
|
async fetchModels(signal) {
|
|
5131
5233
|
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
5132
|
-
const region = regionOfCredential(credential);
|
|
5234
|
+
const region = assertSupportedRegion(regionOfCredential(credential));
|
|
5133
5235
|
const base = this.options.baseUrl ?? this.chatBaseOf(credential);
|
|
5134
5236
|
const headers = {
|
|
5135
5237
|
...buildTraeCnHeaders(credential, identity),
|
|
@@ -5227,7 +5329,18 @@ var TraeSoloUpstreamClient = class {
|
|
|
5227
5329
|
}
|
|
5228
5330
|
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
5229
5331
|
const headers = buildTraeCnHeaders(credential, identity);
|
|
5230
|
-
|
|
5332
|
+
let base;
|
|
5333
|
+
try {
|
|
5334
|
+
assertSupportedRegion(regionOfCredential(credential));
|
|
5335
|
+
base = this.options.baseUrl ?? this.chatBaseOf(credential);
|
|
5336
|
+
} catch (error) {
|
|
5337
|
+
return {
|
|
5338
|
+
ok: false,
|
|
5339
|
+
status: 503,
|
|
5340
|
+
kind: "unconfigured",
|
|
5341
|
+
message: error instanceof Error ? error.message : String(error)
|
|
5342
|
+
};
|
|
5343
|
+
}
|
|
5231
5344
|
if (this.options.route === "enterprise") try {
|
|
5232
5345
|
const parsed = JSON.parse(prepared);
|
|
5233
5346
|
if (parsed["session_id"] === void 0) parsed["session_id"] = randomUUID();
|
|
@@ -5304,7 +5417,7 @@ var TraeSoloUpstreamClient = class {
|
|
|
5304
5417
|
/** Chat gateway for this client's pinned route. */
|
|
5305
5418
|
chatBaseOf(credential) {
|
|
5306
5419
|
if (this.options.route === "enterprise") return TRAE_ENTERPRISE_HOST;
|
|
5307
|
-
return REGION_GATEWAYS[regionOfCredential(credential)].chat;
|
|
5420
|
+
return REGION_GATEWAYS[assertSupportedRegion(regionOfCredential(credential))].chat;
|
|
5308
5421
|
}
|
|
5309
5422
|
};
|
|
5310
5423
|
/**
|
|
@@ -5468,6 +5581,12 @@ function decodeTraeEvent(event) {
|
|
|
5468
5581
|
...typeof record["cache_read_input_tokens"] === "number" ? { cacheReadTokens: record["cache_read_input_tokens"] } : {},
|
|
5469
5582
|
...typeof record["cache_creation_input_tokens"] === "number" ? { cacheWriteTokens: record["cache_creation_input_tokens"] } : {}
|
|
5470
5583
|
};
|
|
5584
|
+
if (event.event === "metadata" || event.event === "timing_cost") return {
|
|
5585
|
+
type: "diagnostic",
|
|
5586
|
+
...record["prompt_cache_pool_record"] === void 0 ? {} : { promptCachePoolRecord: record["prompt_cache_pool_record"] },
|
|
5587
|
+
...typeof record["name"] === "string" ? { pipeline: record["name"] } : {},
|
|
5588
|
+
...typeof record["provider_model_name"] === "string" ? { providerModelName: record["provider_model_name"] } : {}
|
|
5589
|
+
};
|
|
5471
5590
|
if (event.event === "done" || typeof record["finish_reason"] === "string" && record["response"] === void 0) return {
|
|
5472
5591
|
type: "done",
|
|
5473
5592
|
finishReason: typeof record["finish_reason"] === "string" ? record["finish_reason"] : "stop"
|
|
@@ -5634,7 +5753,7 @@ function bridgeTraeSoloStream(response, model) {
|
|
|
5634
5753
|
return bridgeTraeSource(source, model);
|
|
5635
5754
|
}
|
|
5636
5755
|
/** {@link bridgeTraeSoloStream} over an already-opened byte stream. */
|
|
5637
|
-
function bridgeTraeSource(source, model) {
|
|
5756
|
+
function bridgeTraeSource(source, model, log) {
|
|
5638
5757
|
const id = `chatcmpl-${randomUUID().replaceAll("-", "").slice(0, 24)}`;
|
|
5639
5758
|
const created = Math.floor(Date.now() / 1e3);
|
|
5640
5759
|
const decoder = new TextDecoder();
|
|
@@ -5667,6 +5786,15 @@ function bridgeTraeSource(source, model) {
|
|
|
5667
5786
|
if (failure !== void 0) upstreamError = new Error(describeTraeFailure(failure));
|
|
5668
5787
|
return;
|
|
5669
5788
|
}
|
|
5789
|
+
if (decoded.type === "diagnostic") {
|
|
5790
|
+
if (decoded.promptCachePoolRecord !== void 0 || decoded.pipeline !== void 0) log?.("dsh-connect-trae: upstream request diagnostics", {
|
|
5791
|
+
model,
|
|
5792
|
+
promptCachePoolRecord: decoded.promptCachePoolRecord,
|
|
5793
|
+
pipeline: decoded.pipeline,
|
|
5794
|
+
providerModelName: decoded.providerModelName
|
|
5795
|
+
});
|
|
5796
|
+
return;
|
|
5797
|
+
}
|
|
5670
5798
|
if (decoded.type === "delta") {
|
|
5671
5799
|
const delta = {};
|
|
5672
5800
|
if (decoded.text !== "") delta["content"] = decoded.text;
|
|
@@ -5819,7 +5947,7 @@ async function openTraeSource(response) {
|
|
|
5819
5947
|
message: describeTraeFailure(failure)
|
|
5820
5948
|
};
|
|
5821
5949
|
}
|
|
5822
|
-
if (decoded.type !== "unknown") answered = true;
|
|
5950
|
+
if (decoded.type !== "unknown" && decoded.type !== "diagnostic") answered = true;
|
|
5823
5951
|
}
|
|
5824
5952
|
if (answered) break;
|
|
5825
5953
|
}
|
|
@@ -5843,12 +5971,12 @@ async function openTraeSource(response) {
|
|
|
5843
5971
|
* The sibling {@link bridgeTraeSoloStream} stays synchronous and always answers
|
|
5844
5972
|
* 200; it is the primitive, and this is the policy the bridge class uses.
|
|
5845
5973
|
*/
|
|
5846
|
-
async function bridgeTraeSoloStreamOrFail(response, model) {
|
|
5974
|
+
async function bridgeTraeSoloStreamOrFail(response, model, log) {
|
|
5847
5975
|
const opened = await openTraeSource(response);
|
|
5848
5976
|
if (!opened.ok) return opened;
|
|
5849
5977
|
return {
|
|
5850
5978
|
ok: true,
|
|
5851
|
-
response: bridgeTraeSource(opened.source, model)
|
|
5979
|
+
response: bridgeTraeSource(opened.source, model, log)
|
|
5852
5980
|
};
|
|
5853
5981
|
}
|
|
5854
5982
|
/**
|
|
@@ -5889,10 +6017,12 @@ var TraeSoloBridge = class {
|
|
|
5889
6017
|
upstream;
|
|
5890
6018
|
catalog;
|
|
5891
6019
|
wireResolver;
|
|
5892
|
-
|
|
6020
|
+
log;
|
|
6021
|
+
constructor(upstream, catalog, wireResolver, log) {
|
|
5893
6022
|
this.upstream = upstream;
|
|
5894
6023
|
this.catalog = catalog;
|
|
5895
6024
|
this.wireResolver = wireResolver;
|
|
6025
|
+
this.log = log;
|
|
5896
6026
|
}
|
|
5897
6027
|
async chatStream(bodyJson, signal) {
|
|
5898
6028
|
let model = "glm-5.2";
|
|
@@ -5980,7 +6110,7 @@ var TraeSoloBridge = class {
|
|
|
5980
6110
|
}
|
|
5981
6111
|
const result = await this.upstream.chatStream(prepared, signal);
|
|
5982
6112
|
if (!result.ok) return result;
|
|
5983
|
-
return await bridgeTraeSoloStreamOrFail(result.response, model);
|
|
6113
|
+
return await bridgeTraeSoloStreamOrFail(result.response, model, this.log);
|
|
5984
6114
|
}
|
|
5985
6115
|
};
|
|
5986
6116
|
//#endregion
|
|
@@ -6071,6 +6201,72 @@ function parseTraeRemoteModel(value, groupFunction) {
|
|
|
6071
6201
|
};
|
|
6072
6202
|
}
|
|
6073
6203
|
//#endregion
|
|
6204
|
+
//#region src/failure.ts
|
|
6205
|
+
/**
|
|
6206
|
+
* Failure classification shared by the Host's HTTP routes.
|
|
6207
|
+
*
|
|
6208
|
+
* Issue #66: the model-refresh route wrapped EVERY thrown error as HTTP 500.
|
|
6209
|
+
* A US credential sent to the Singapore gateway answers `401`, that 401 became
|
|
6210
|
+
* `{"error":"SOLO remote models returned HTTP 401"}` under status 500, and the
|
|
6211
|
+
* card could only say "HTTP 500" — so a region/routing problem was
|
|
6212
|
+
* indistinguishable from a plugin crash, and the user re-signed-in instead of
|
|
6213
|
+
* looking at the region.
|
|
6214
|
+
*
|
|
6215
|
+
* The fix is deliberately narrow: upstream HTTP verdicts and the local
|
|
6216
|
+
* "region not served" verdict keep their meaning when they cross the route
|
|
6217
|
+
* boundary. Only failures that carry no status of their own stay 500.
|
|
6218
|
+
*
|
|
6219
|
+
* @module dsh-connect-trae/failure
|
|
6220
|
+
*/
|
|
6221
|
+
/** Code for an upstream that refused the credential (401 / 403). */
|
|
6222
|
+
const TRAE_AUTH_FAILURE_CODE = "authentication";
|
|
6223
|
+
/** Code for any other upstream HTTP verdict (4xx/5xx that is not auth). */
|
|
6224
|
+
const TRAE_UPSTREAM_FAILURE_CODE = "upstream";
|
|
6225
|
+
/**
|
|
6226
|
+
* A non-2xx answer from a Trae gateway, carrying the status the gateway
|
|
6227
|
+
* actually sent.
|
|
6228
|
+
*
|
|
6229
|
+
* Plain `Error`s are still allowed everywhere (and stay 500 at the route); this
|
|
6230
|
+
* class exists so the paths that DO know an upstream status can pass it on
|
|
6231
|
+
* instead of leaving it buried in a message string.
|
|
6232
|
+
*/
|
|
6233
|
+
var TraeHttpError = class extends Error {
|
|
6234
|
+
status;
|
|
6235
|
+
name = "TraeHttpError";
|
|
6236
|
+
constructor(status, message) {
|
|
6237
|
+
super(message);
|
|
6238
|
+
this.status = status;
|
|
6239
|
+
}
|
|
6240
|
+
};
|
|
6241
|
+
/**
|
|
6242
|
+
* Map a thrown failure onto the Host's HTTP answer.
|
|
6243
|
+
*
|
|
6244
|
+
* - a region this build does not serve → `501` + `unsupported-region`
|
|
6245
|
+
* (the request is well-formed; this build simply has no route for it);
|
|
6246
|
+
* - an upstream 401/403 → the SAME status + `authentication`, so the client
|
|
6247
|
+
* can distinguish "sign in again" from "the plugin broke";
|
|
6248
|
+
* - any other upstream status → passed through verbatim + `upstream`;
|
|
6249
|
+
* - anything else (a programming error, a transport failure) → `500`.
|
|
6250
|
+
*/
|
|
6251
|
+
function httpFailureOf(error) {
|
|
6252
|
+
if (error instanceof TraeUnsupportedRegionError) return {
|
|
6253
|
+
status: 501,
|
|
6254
|
+
code: TRAE_UNSUPPORTED_REGION_CODE
|
|
6255
|
+
};
|
|
6256
|
+
if (error instanceof TraeHttpError) {
|
|
6257
|
+
const status = Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : 502;
|
|
6258
|
+
if (status === 401 || status === 403) return {
|
|
6259
|
+
status,
|
|
6260
|
+
code: TRAE_AUTH_FAILURE_CODE
|
|
6261
|
+
};
|
|
6262
|
+
return {
|
|
6263
|
+
status,
|
|
6264
|
+
code: TRAE_UPSTREAM_FAILURE_CODE
|
|
6265
|
+
};
|
|
6266
|
+
}
|
|
6267
|
+
return { status: 500 };
|
|
6268
|
+
}
|
|
6269
|
+
//#endregion
|
|
6074
6270
|
//#region src/solo-remote.ts
|
|
6075
6271
|
const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
|
|
6076
6272
|
/**
|
|
@@ -6368,7 +6564,7 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
|
6368
6564
|
}
|
|
6369
6565
|
async fetchModels(signal) {
|
|
6370
6566
|
const credential = await this.options.credential();
|
|
6371
|
-
const region = regionOfCredential(credential);
|
|
6567
|
+
const region = assertSupportedRegion(regionOfCredential(credential));
|
|
6372
6568
|
const officialBase = this.baseUrl ?? REGION_GATEWAYS[region].remote;
|
|
6373
6569
|
const headers = await this.headers(region);
|
|
6374
6570
|
const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
|
|
@@ -6388,7 +6584,7 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
|
|
|
6388
6584
|
continue;
|
|
6389
6585
|
}
|
|
6390
6586
|
if (!response.ok) {
|
|
6391
|
-
if (response.status < 500) throw new
|
|
6587
|
+
if (response.status < 500) throw new TraeHttpError(response.status, `SOLO remote models returned HTTP ${response.status}`);
|
|
6392
6588
|
if (base === officialBase) this.officialRetryAtMs = Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS;
|
|
6393
6589
|
lastError = /* @__PURE__ */ new Error(`SOLO remote models returned HTTP ${response.status}`);
|
|
6394
6590
|
continue;
|
|
@@ -6729,9 +6925,20 @@ var TraeRawChatUpstreamClient = class {
|
|
|
6729
6925
|
...this.options.config.abVersion === void 0 ? {} : { ab_version: this.options.config.abVersion },
|
|
6730
6926
|
pass_back_reasoning: runtime?.passBackReasoning ?? this.options.config.passBackReasoning
|
|
6731
6927
|
};
|
|
6928
|
+
let base;
|
|
6929
|
+
try {
|
|
6930
|
+
base = this.options.baseUrl ?? REGION_GATEWAYS[assertSupportedRegion(regionOfCredential(credential))].chat;
|
|
6931
|
+
} catch (error) {
|
|
6932
|
+
return {
|
|
6933
|
+
ok: false,
|
|
6934
|
+
status: 503,
|
|
6935
|
+
kind: "unconfigured",
|
|
6936
|
+
message: error instanceof Error ? error.message : String(error)
|
|
6937
|
+
};
|
|
6938
|
+
}
|
|
6732
6939
|
let response;
|
|
6733
6940
|
try {
|
|
6734
|
-
response = await this.fetchImpl(traeEndpoint(
|
|
6941
|
+
response = await this.fetchImpl(traeEndpoint(base, TRAE_RAW_CHAT_V2_PATH), {
|
|
6735
6942
|
method: "POST",
|
|
6736
6943
|
headers,
|
|
6737
6944
|
body: JSON.stringify(body),
|
|
@@ -7378,10 +7585,16 @@ var TraeUsageClient = class {
|
|
|
7378
7585
|
this.baseUrl = options.baseUrl;
|
|
7379
7586
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
7380
7587
|
}
|
|
7381
|
-
/**
|
|
7588
|
+
/**
|
|
7589
|
+
* Region of the current credential; `cn` when unresolvable.
|
|
7590
|
+
*
|
|
7591
|
+
* A US credential throws {@link TraeUnsupportedRegionError} instead of being
|
|
7592
|
+
* rounded to the international bucket — issue #66: the rounding is what sent
|
|
7593
|
+
* a US token to the SG pay gateway, where the answer is 401.
|
|
7594
|
+
*/
|
|
7382
7595
|
async currentRegion() {
|
|
7383
7596
|
const credential = await this.options.credential();
|
|
7384
|
-
return credential === void 0 ? "cn" : regionOfCredential(credential);
|
|
7597
|
+
return credential === void 0 ? "cn" : assertSupportedRegion(regionOfCredential(credential));
|
|
7385
7598
|
}
|
|
7386
7599
|
/**
|
|
7387
7600
|
* Pay base for the current credential's region. An explicit baseUrl (tests,
|
|
@@ -7428,7 +7641,7 @@ var TraeUsageClient = class {
|
|
|
7428
7641
|
body: JSON.stringify(data),
|
|
7429
7642
|
signal: signal ?? AbortSignal.timeout(this.timeoutMs)
|
|
7430
7643
|
});
|
|
7431
|
-
if (!response.ok) throw new
|
|
7644
|
+
if (!response.ok) throw new TraeHttpError(response.status, `Trae usage endpoint ${path} returned HTTP ${response.status}`);
|
|
7432
7645
|
return await response.json();
|
|
7433
7646
|
}
|
|
7434
7647
|
/**
|
|
@@ -7966,7 +8179,9 @@ function previewTransferFile(text) {
|
|
|
7966
8179
|
* stack with a credential that belongs to no real CN account. So this helper
|
|
7967
8180
|
* reuses `regionOfUserRegion` / `regionOfHost` — both of which already return
|
|
7968
8181
|
* `undefined` on unrecognised input — and deliberately does NOT consult the
|
|
7969
|
-
* edition fallback.
|
|
8182
|
+
* edition fallback. A US claim (`userRegion: "US"` or a `*.traeapi.us` host)
|
|
8183
|
+
* therefore resolves to `us` and matches neither importable region, so that
|
|
8184
|
+
* record is skipped like any other foreign one (issue #66).
|
|
7970
8185
|
*/
|
|
7971
8186
|
function knownRegionOfCredential(credential) {
|
|
7972
8187
|
return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host);
|
|
@@ -8163,6 +8378,25 @@ function json(res, status, body) {
|
|
|
8163
8378
|
});
|
|
8164
8379
|
res.end(payload);
|
|
8165
8380
|
}
|
|
8381
|
+
/**
|
|
8382
|
+
* Answer a failed route with the meaning the failure actually has (issue #66).
|
|
8383
|
+
*
|
|
8384
|
+
* The old rule was `json(res, 500, { error })` everywhere, so a US credential
|
|
8385
|
+
* sent to the Singapore gateway surfaced as `HTTP 500
|
|
8386
|
+
* {"error":"SOLO remote models returned HTTP 401"}`: the status said "the
|
|
8387
|
+
* plugin is broken" while the text said "the gateway refused you". The client
|
|
8388
|
+
* could not tell "sign in again" from "this region is unsupported", which is
|
|
8389
|
+
* exactly the loop the issue reports. `httpFailureOf` keeps an upstream 401/403
|
|
8390
|
+
* as 401/403 and turns the local "not served" verdict into 501 +
|
|
8391
|
+
* `unsupported-region`; everything else stays 500.
|
|
8392
|
+
*/
|
|
8393
|
+
function failJson(res, error) {
|
|
8394
|
+
const failure = httpFailureOf(error);
|
|
8395
|
+
json(res, failure.status, {
|
|
8396
|
+
error: safeMessage(error),
|
|
8397
|
+
...failure.code === void 0 ? {} : { code: failure.code }
|
|
8398
|
+
});
|
|
8399
|
+
}
|
|
8166
8400
|
/** Bound on how many OTHER accounts one status read may probe (issue #25). */
|
|
8167
8401
|
const TRAE_CREDIT_ALTERNATIVE_LIMIT = 8;
|
|
8168
8402
|
/**
|
|
@@ -8413,6 +8647,21 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
8413
8647
|
* it reads nothing but file names and sizes.
|
|
8414
8648
|
*/
|
|
8415
8649
|
const quarantined = await Promise.resolve().then(() => store.quarantinedCopies()).catch(() => []);
|
|
8650
|
+
/**
|
|
8651
|
+
* A region this build recognizes but does not serve (issue #66).
|
|
8652
|
+
*
|
|
8653
|
+
* Read ONCE per document and never allowed to fail it: this is a notice about
|
|
8654
|
+
* the user's disk, and the panel it explains is usually the one that has just
|
|
8655
|
+
* been excluded from billing. It is also why the panel can say "a US account
|
|
8656
|
+
* is here, and it is not being used" instead of an undiagnosable "not signed
|
|
8657
|
+
* in" on a machine the user knows is signed in.
|
|
8658
|
+
*/
|
|
8659
|
+
const unsupported = await Promise.resolve().then(() => store.unsupportedCredentials()).catch(() => []);
|
|
8660
|
+
const firstUnsupported = unsupported[0];
|
|
8661
|
+
const unsupportedRegion = firstUnsupported === void 0 ? void 0 : {
|
|
8662
|
+
region: firstUnsupported.region,
|
|
8663
|
+
accounts: unsupported.map((item) => item.accountName)
|
|
8664
|
+
};
|
|
8416
8665
|
if ((await store.status()).state !== "signed-in") {
|
|
8417
8666
|
const { failures } = await store.diagnose();
|
|
8418
8667
|
return {
|
|
@@ -8420,6 +8669,7 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
8420
8669
|
accounts,
|
|
8421
8670
|
enabled,
|
|
8422
8671
|
quarantined,
|
|
8672
|
+
...unsupportedRegion === void 0 ? {} : { unsupportedRegion },
|
|
8423
8673
|
searched: failures.map((failure) => ({
|
|
8424
8674
|
path: failure.path,
|
|
8425
8675
|
edition: failure.edition,
|
|
@@ -8434,12 +8684,14 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
8434
8684
|
credential = fast ? await store.current() : await store.resolve();
|
|
8435
8685
|
if (credential === void 0) credential = await store.resolve();
|
|
8436
8686
|
} catch (error) {
|
|
8687
|
+
const explained = error instanceof TraeUnsupportedRegionError && unsupportedRegion !== void 0;
|
|
8437
8688
|
return {
|
|
8438
8689
|
status: "signed-out",
|
|
8439
8690
|
accounts,
|
|
8440
8691
|
enabled,
|
|
8441
8692
|
quarantined,
|
|
8442
|
-
|
|
8693
|
+
...unsupportedRegion === void 0 ? {} : { unsupportedRegion },
|
|
8694
|
+
...explained ? {} : { message: safeMessage(error) }
|
|
8443
8695
|
};
|
|
8444
8696
|
}
|
|
8445
8697
|
const account = {
|
|
@@ -8449,6 +8701,7 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
8449
8701
|
...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
|
|
8450
8702
|
region: regionOfPanel(panel),
|
|
8451
8703
|
enabled,
|
|
8704
|
+
...unsupportedRegion === void 0 ? {} : { unsupportedRegion },
|
|
8452
8705
|
accounts,
|
|
8453
8706
|
models: deps.displayModels(panel).map((model) => ({
|
|
8454
8707
|
...model,
|
|
@@ -8809,7 +9062,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
8809
9062
|
try {
|
|
8810
9063
|
json(res, 200, await traeWebUsage(deps, panel, { detail }));
|
|
8811
9064
|
} catch (error) {
|
|
8812
|
-
|
|
9065
|
+
failJson(res, error);
|
|
8813
9066
|
}
|
|
8814
9067
|
}
|
|
8815
9068
|
});
|
|
@@ -8830,7 +9083,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
8830
9083
|
try {
|
|
8831
9084
|
json(res, 200, { accounts: await traeAccountCredits(deps, panel) });
|
|
8832
9085
|
} catch (error) {
|
|
8833
|
-
|
|
9086
|
+
failJson(res, error);
|
|
8834
9087
|
}
|
|
8835
9088
|
}
|
|
8836
9089
|
});
|
|
@@ -8857,7 +9110,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
8857
9110
|
if (state === void 0) return json(res, 404, { error: "unknown account" });
|
|
8858
9111
|
json(res, 200, state);
|
|
8859
9112
|
} catch (error) {
|
|
8860
|
-
|
|
9113
|
+
failJson(res, error);
|
|
8861
9114
|
}
|
|
8862
9115
|
}
|
|
8863
9116
|
});
|
|
@@ -8949,7 +9202,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
8949
9202
|
try {
|
|
8950
9203
|
json(res, 200, { models: await deps.discoverModels(panel) });
|
|
8951
9204
|
} catch (error) {
|
|
8952
|
-
|
|
9205
|
+
failJson(res, error);
|
|
8953
9206
|
}
|
|
8954
9207
|
}
|
|
8955
9208
|
});
|
|
@@ -8983,7 +9236,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
8983
9236
|
try {
|
|
8984
9237
|
json(res, 200, await deps.probeModels(panel, ids));
|
|
8985
9238
|
} catch (error) {
|
|
8986
|
-
|
|
9239
|
+
failJson(res, error);
|
|
8987
9240
|
}
|
|
8988
9241
|
}
|
|
8989
9242
|
});
|
|
@@ -9067,7 +9320,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
9067
9320
|
checkin: toCheckin(checkin)
|
|
9068
9321
|
});
|
|
9069
9322
|
} catch (error) {
|
|
9070
|
-
|
|
9323
|
+
failJson(res, error);
|
|
9071
9324
|
}
|
|
9072
9325
|
}
|
|
9073
9326
|
});
|
|
@@ -9160,7 +9413,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
9160
9413
|
try {
|
|
9161
9414
|
json(res, 200, { rows: (await pool.checkin(panel)).map((row) => poolCheckinRowViewOf(row)) });
|
|
9162
9415
|
} catch (error) {
|
|
9163
|
-
|
|
9416
|
+
failJson(res, error);
|
|
9164
9417
|
}
|
|
9165
9418
|
return;
|
|
9166
9419
|
}
|
|
@@ -9185,7 +9438,7 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
9185
9438
|
if (row === void 0) return json(res, 404, { error: "unknown account" });
|
|
9186
9439
|
return json(res, 200, { rows: [poolTestRowViewOf(row)] });
|
|
9187
9440
|
} catch (error) {
|
|
9188
|
-
return
|
|
9441
|
+
return failJson(res, error);
|
|
9189
9442
|
}
|
|
9190
9443
|
res.writeHead(200, {
|
|
9191
9444
|
"Content-Type": "application/x-ndjson",
|
|
@@ -10953,7 +11206,7 @@ function apply(ctx, config) {
|
|
|
10953
11206
|
route,
|
|
10954
11207
|
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
10955
11208
|
});
|
|
10956
|
-
const delegating = new TraeDelegatingUpstreamClient(new TraeSoloBridge(client, catalog, wireResolver));
|
|
11209
|
+
const delegating = new TraeDelegatingUpstreamClient(new TraeSoloBridge(client, catalog, wireResolver, (message, detail) => ctx.logger.warn(message, detail)));
|
|
10957
11210
|
routeClients[provider] = {
|
|
10958
11211
|
provider,
|
|
10959
11212
|
route,
|
|
@@ -10994,7 +11247,7 @@ function apply(ctx, config) {
|
|
|
10994
11247
|
runtime: resolved.runtime
|
|
10995
11248
|
}
|
|
10996
11249
|
}),
|
|
10997
|
-
solo: new TraeSoloBridge(primary.client, catalog, wireResolver),
|
|
11250
|
+
solo: new TraeSoloBridge(primary.client, catalog, wireResolver, (message, detail) => ctx.logger.warn(message, detail)),
|
|
10998
11251
|
endpoint: `${REGION_GATEWAYS.cn.chat}/api/ide/v2/llm_raw_chat`,
|
|
10999
11252
|
edition: resolved.identity.edition,
|
|
11000
11253
|
identity: {
|
|
@@ -11391,7 +11644,7 @@ function apply(ctx, config) {
|
|
|
11391
11644
|
identity: () => stack.identity(),
|
|
11392
11645
|
route: stack.routeOf(providerOfPanel(panel)),
|
|
11393
11646
|
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
11394
|
-
}), stack.catalog, stack.wireResolver)
|
|
11647
|
+
}), stack.catalog, stack.wireResolver, (message, detail) => ctx.logger.warn(message, detail))
|
|
11395
11648
|
};
|
|
11396
11649
|
}
|
|
11397
11650
|
/**
|
|
@@ -11473,7 +11726,7 @@ function apply(ctx, config) {
|
|
|
11473
11726
|
identity: () => stack.identity(),
|
|
11474
11727
|
route: stack.routeOf(providerOfPanel(panel)),
|
|
11475
11728
|
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
11476
|
-
}), stack.catalog, stack.wireResolver);
|
|
11729
|
+
}), stack.catalog, stack.wireResolver, (message, detail) => ctx.logger.warn(message, detail));
|
|
11477
11730
|
const chat = (body, signal) => bridge.chatStream(body, signal);
|
|
11478
11731
|
bridges.set(accountId, chat);
|
|
11479
11732
|
return chat;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-trae",
|
|
3
3
|
"displayName": "DSH Connect Trae",
|
|
4
4
|
"description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
|
|
5
|
-
"version": "4.6.
|
|
5
|
+
"version": "4.6.4",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|