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 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(`HTTP ${response.status}`);
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(`HTTP ${response.status}`);
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(`HTTP ${response.status}`);
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 regionOfUserRegion(value: unknown): TraeRegion | undefined;
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
- }): TraeRegion;
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
- constructor(upstream: TraeUpstreamClient, catalog?: Pick<TraeCatalog, "current"> | undefined, wireResolver?: TraeWireResolver | undefined);
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
- /** Region of the current credential; `cn` when unresolvable. */
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
- /** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
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) throw new Error(`trae: no signed-in account found (${this.candidates().map((item) => item.path).join(" or ")})`);
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
- const base = this.options.baseUrl ?? this.chatBaseOf(credential);
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
- constructor(upstream, catalog, wireResolver) {
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 Error(`SOLO remote models returned HTTP ${response.status}`);
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(this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat, TRAE_RAW_CHAT_V2_PATH), {
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
- /** Region of the current credential; `cn` when unresolvable. */
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 Error(`Trae usage endpoint ${path} returned HTTP ${response.status}`);
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
- message: safeMessage(error)
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
- json(res, 500, { error: safeMessage(error) });
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
- json(res, 500, { error: safeMessage(error) });
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
- json(res, 500, { error: safeMessage(error) });
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
- json(res, 500, { error: safeMessage(error) });
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
- json(res, 500, { error: safeMessage(error) });
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
- json(res, 500, { error: safeMessage(error) });
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
- json(res, 500, { error: safeMessage(error) });
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 json(res, 500, { error: safeMessage(error) });
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.2",
5
+ "version": "4.6.4",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",