dsh-connect-trae 4.6.2 → 4.6.3

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,77 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.6.3 (2026-10-27)
4
+
5
+ > **国际版美国区账号不会再被当成新加坡区。** 它的凭据明明写着
6
+ > `host = https://grow-normal.traeapi.us`、`userRegion = "US"`,插件却因为
7
+ > 「不认识 US」回落到 edition 标签(`sg → ai`),把 US token 发去 `*.trae.ai`
8
+ > ——那边只会回 401;刷新模型拿到的 401 又被路由包成 500。用户看到的是
9
+ > 「拉模型失败,请重新登录」,而重登拿到的还是同一份 US 凭据,于是无限循环。
10
+
11
+ ### 修复
12
+
13
+ - **美国区成为显式区域,不再静默回退成新加坡区**(#66)。
14
+ - **现象**:`pay_status` 401;「刷新模型」500
15
+ `{"error":"SOLO remote models returned HTTP 401"}`;宿主日志
16
+ `live ai model directory unavailable; serving the configured catalog`。
17
+ - **根因**:`regionOfUserRegion` 只认 CN/SG、`regionOfHost` 只认
18
+ `.trae.ai`/`.trae.cn`,两级都落空后由 `regionOfEdition("sg")` 决定——
19
+ US 被判定为 `ai`。而 `REGION_GATEWAYS` 只有 cn/ai 两个键,于是「查不到」
20
+ 变成了「拿另一个区的网关」。
21
+ - **修法**:新增 `TraeServiceRegion = 'cn' | 'ai' | 'us'`;`TraeRegion`
22
+ (存储/路由桶)仍是 `cn | ai`。`assertSupportedRegion()` 是唯一从区域
23
+ 身份进到路由桶的入口,US 在**选择网关之前**抛
24
+ `TraeUnsupportedRegionError`。所有读网关的客户端(用量、SOLO wire、远程
25
+ 目录、Raw Chat、IDE 名册)都走这一道,注入 `baseUrl` 的测试/诊断路径也
26
+ 不例外。
27
+ - **为什么不是一行 `us → ai` 映射**:`ai` 的网关/目录/chat 契约都是按新加坡
28
+ 实测出来的,而报告里的 US 实测恰好相反(远程目录 404、`llm_utils_chat`
29
+ 403 `40006`、`create_agent_task` 的 config 绑定未知)。映射过去只是换一种
30
+ 方式发错请求。**美国区本身仍未接入**,本版只保证它不再被误路由。
31
+
32
+ - **上游 401/403 不再被包成 500**(#66)。
33
+ - 新增 `src/failure.ts`:`TraeHttpError` 携带上游状态,`httpFailureOf()` 把
34
+ 401/403 映射为原状态 + `code: authentication`,本地「区域不支持」映射为
35
+ 501 + `code: unsupported-region`,真正没有状态可言的失败仍 500。状态、
36
+ 额度、账号表、模型刷新、模型测速、签到与账号池签到/测速这些路由全部改用
37
+ 它。
38
+ - 卡片改为显示**服务端原文**(账号刷新、模型刷新、状态读取),不再只打印
39
+ `HTTP 401`——「网关拒绝了你」和「插件坏了」从此可以分辨。
40
+
41
+ - **US 凭据不可选中、不计费,但会被点名**(#66)。
42
+ - `readAll()` 过滤掉本版本不服务的区域(作用域与无作用域 store 都过滤),
43
+ 因此它不会被 refresh、不会进账号表、不会发出任何请求;
44
+ `unsupportedCredentials()` 单独把它读出来给卡片点名。
45
+ - 未登录态显示「美国区(US)暂不支持」并列出账号名,写明请求不会发往新加坡
46
+ 网关、重新登录没有变化;诊断面板多一行「未支持的区域」。
47
+ - `resolve()` 在「只有 US 凭据」时抛明确错误,而不是笼统的
48
+ `no signed-in account found`;导入的区域门控同步把 `us` 当外区跳过。
49
+
50
+ ### 测试
51
+
52
+ - `tests/issue66-us-region.spec.ts`(23 例):US 的三条解析路径(claim / host /
53
+ 完整凭据)都得到 `us` 而不是 `ai`;`payStatus` / 远程目录 / wire 名册 /
54
+ Raw Chat / chat 面对 US 凭据**零 `fetch` 调用**(含注入 `baseUrl` 的两例);
55
+ 作用域与无作用域 store 都不解析它、但都能点名;401/403/404/US/普通异常的
56
+ 状态映射;模型刷新路由逐条断言 401、501、500;状态文档携带通告且不重复英文
57
+ 异常文本。
58
+ - `tests/issue66-card-us-notice.spec.tsx`(2 例):未登录文档带
59
+ `unsupportedRegion` 时卡片点名账号;失败请求显示服务端 `error` 原文而非
60
+ `HTTP 401`。
61
+ - **变异验证**:去掉 `regionOfUserRegion` 的 US 分支 → 2 例转红;刷新路由的
62
+ `failJson()` 改回 500 → 2 例转红;删掉 `readAll()` 的区域守卫 → 1 例转红。
63
+ - 全量 **95 files / 1211 tests** 通过(原 93 / 1186 + 新增 2 files / 25 例)。
64
+
65
+ ### 说明
66
+
67
+ - **没有对 US 网关发过真机请求**:开发机没有美国区凭据。结论来自报告人给出的
68
+ 实测对照(同一 token 打 US 200、打 SG 401)与代码路径;测试断言的是「不会
69
+ 发出请求」,不是模拟 US 响应。
70
+ - 美国区的目录与 chat 仍待接入:目录需要 `get_detail_param` 的 `function`
71
+ 单数 + `agent_type: null` 形态,chat 需要一次 IDE 侧的 `/api/agent/*`
72
+ 实抓。拿到报文后即可按现有 `REGION_GATEWAYS` 的形状补齐。
73
+ - 诊断与取舍全文见仓库内 `docs/ISSUE66_DIAGNOSIS.md`(该文档不随包发布)。
74
+
3
75
  ## 4.6.2 (2026-10-27)
4
76
 
5
77
  > **从插件市场装完,重启后插件不会再消失。** 安装其实失败了,但失败发生在
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.
191
212
  */
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;
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).
222
+ */
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,
@@ -3875,7 +3959,13 @@ declare class TraeUsageClient {
3875
3959
  private readonly baseUrl;
3876
3960
  private readonly timeoutMs;
3877
3961
  constructor(options: TraeUsageOptions);
3878
- /** Region of the current credential; `cn` when unresolvable. */
3962
+ /**
3963
+ * Region of the current credential; `cn` when unresolvable.
3964
+ *
3965
+ * A US credential throws {@link TraeUnsupportedRegionError} instead of being
3966
+ * rounded to the international bucket — issue #66: the rounding is what sent
3967
+ * a US token to the SG pay gateway, where the answer is 401.
3968
+ */
3879
3969
  private currentRegion;
3880
3970
  /**
3881
3971
  * 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
  /**
@@ -6071,6 +6184,72 @@ function parseTraeRemoteModel(value, groupFunction) {
6071
6184
  };
6072
6185
  }
6073
6186
  //#endregion
6187
+ //#region src/failure.ts
6188
+ /**
6189
+ * Failure classification shared by the Host's HTTP routes.
6190
+ *
6191
+ * Issue #66: the model-refresh route wrapped EVERY thrown error as HTTP 500.
6192
+ * A US credential sent to the Singapore gateway answers `401`, that 401 became
6193
+ * `{"error":"SOLO remote models returned HTTP 401"}` under status 500, and the
6194
+ * card could only say "HTTP 500" — so a region/routing problem was
6195
+ * indistinguishable from a plugin crash, and the user re-signed-in instead of
6196
+ * looking at the region.
6197
+ *
6198
+ * The fix is deliberately narrow: upstream HTTP verdicts and the local
6199
+ * "region not served" verdict keep their meaning when they cross the route
6200
+ * boundary. Only failures that carry no status of their own stay 500.
6201
+ *
6202
+ * @module dsh-connect-trae/failure
6203
+ */
6204
+ /** Code for an upstream that refused the credential (401 / 403). */
6205
+ const TRAE_AUTH_FAILURE_CODE = "authentication";
6206
+ /** Code for any other upstream HTTP verdict (4xx/5xx that is not auth). */
6207
+ const TRAE_UPSTREAM_FAILURE_CODE = "upstream";
6208
+ /**
6209
+ * A non-2xx answer from a Trae gateway, carrying the status the gateway
6210
+ * actually sent.
6211
+ *
6212
+ * Plain `Error`s are still allowed everywhere (and stay 500 at the route); this
6213
+ * class exists so the paths that DO know an upstream status can pass it on
6214
+ * instead of leaving it buried in a message string.
6215
+ */
6216
+ var TraeHttpError = class extends Error {
6217
+ status;
6218
+ name = "TraeHttpError";
6219
+ constructor(status, message) {
6220
+ super(message);
6221
+ this.status = status;
6222
+ }
6223
+ };
6224
+ /**
6225
+ * Map a thrown failure onto the Host's HTTP answer.
6226
+ *
6227
+ * - a region this build does not serve → `501` + `unsupported-region`
6228
+ * (the request is well-formed; this build simply has no route for it);
6229
+ * - an upstream 401/403 → the SAME status + `authentication`, so the client
6230
+ * can distinguish "sign in again" from "the plugin broke";
6231
+ * - any other upstream status → passed through verbatim + `upstream`;
6232
+ * - anything else (a programming error, a transport failure) → `500`.
6233
+ */
6234
+ function httpFailureOf(error) {
6235
+ if (error instanceof TraeUnsupportedRegionError) return {
6236
+ status: 501,
6237
+ code: TRAE_UNSUPPORTED_REGION_CODE
6238
+ };
6239
+ if (error instanceof TraeHttpError) {
6240
+ const status = Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : 502;
6241
+ if (status === 401 || status === 403) return {
6242
+ status,
6243
+ code: TRAE_AUTH_FAILURE_CODE
6244
+ };
6245
+ return {
6246
+ status,
6247
+ code: TRAE_UPSTREAM_FAILURE_CODE
6248
+ };
6249
+ }
6250
+ return { status: 500 };
6251
+ }
6252
+ //#endregion
6074
6253
  //#region src/solo-remote.ts
6075
6254
  const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
6076
6255
  /**
@@ -6368,7 +6547,7 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
6368
6547
  }
6369
6548
  async fetchModels(signal) {
6370
6549
  const credential = await this.options.credential();
6371
- const region = regionOfCredential(credential);
6550
+ const region = assertSupportedRegion(regionOfCredential(credential));
6372
6551
  const officialBase = this.baseUrl ?? REGION_GATEWAYS[region].remote;
6373
6552
  const headers = await this.headers(region);
6374
6553
  const functions = TRAE_REMOTE_DIRECTORY_FUNCTIONS[region].join(",");
@@ -6388,7 +6567,7 @@ var TraeSoloRemoteCatalogClient = class TraeSoloRemoteCatalogClient {
6388
6567
  continue;
6389
6568
  }
6390
6569
  if (!response.ok) {
6391
- if (response.status < 500) throw new Error(`SOLO remote models returned HTTP ${response.status}`);
6570
+ if (response.status < 500) throw new TraeHttpError(response.status, `SOLO remote models returned HTTP ${response.status}`);
6392
6571
  if (base === officialBase) this.officialRetryAtMs = Date.now() + TraeSoloRemoteCatalogClient.OFFICIAL_RETRY_MS;
6393
6572
  lastError = /* @__PURE__ */ new Error(`SOLO remote models returned HTTP ${response.status}`);
6394
6573
  continue;
@@ -6729,9 +6908,20 @@ var TraeRawChatUpstreamClient = class {
6729
6908
  ...this.options.config.abVersion === void 0 ? {} : { ab_version: this.options.config.abVersion },
6730
6909
  pass_back_reasoning: runtime?.passBackReasoning ?? this.options.config.passBackReasoning
6731
6910
  };
6911
+ let base;
6912
+ try {
6913
+ base = this.options.baseUrl ?? REGION_GATEWAYS[assertSupportedRegion(regionOfCredential(credential))].chat;
6914
+ } catch (error) {
6915
+ return {
6916
+ ok: false,
6917
+ status: 503,
6918
+ kind: "unconfigured",
6919
+ message: error instanceof Error ? error.message : String(error)
6920
+ };
6921
+ }
6732
6922
  let response;
6733
6923
  try {
6734
- response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat, TRAE_RAW_CHAT_V2_PATH), {
6924
+ response = await this.fetchImpl(traeEndpoint(base, TRAE_RAW_CHAT_V2_PATH), {
6735
6925
  method: "POST",
6736
6926
  headers,
6737
6927
  body: JSON.stringify(body),
@@ -7378,10 +7568,16 @@ var TraeUsageClient = class {
7378
7568
  this.baseUrl = options.baseUrl;
7379
7569
  this.timeoutMs = options.timeoutMs ?? 3e4;
7380
7570
  }
7381
- /** Region of the current credential; `cn` when unresolvable. */
7571
+ /**
7572
+ * Region of the current credential; `cn` when unresolvable.
7573
+ *
7574
+ * A US credential throws {@link TraeUnsupportedRegionError} instead of being
7575
+ * rounded to the international bucket — issue #66: the rounding is what sent
7576
+ * a US token to the SG pay gateway, where the answer is 401.
7577
+ */
7382
7578
  async currentRegion() {
7383
7579
  const credential = await this.options.credential();
7384
- return credential === void 0 ? "cn" : regionOfCredential(credential);
7580
+ return credential === void 0 ? "cn" : assertSupportedRegion(regionOfCredential(credential));
7385
7581
  }
7386
7582
  /**
7387
7583
  * Pay base for the current credential's region. An explicit baseUrl (tests,
@@ -7428,7 +7624,7 @@ var TraeUsageClient = class {
7428
7624
  body: JSON.stringify(data),
7429
7625
  signal: signal ?? AbortSignal.timeout(this.timeoutMs)
7430
7626
  });
7431
- if (!response.ok) throw new Error(`Trae usage endpoint ${path} returned HTTP ${response.status}`);
7627
+ if (!response.ok) throw new TraeHttpError(response.status, `Trae usage endpoint ${path} returned HTTP ${response.status}`);
7432
7628
  return await response.json();
7433
7629
  }
7434
7630
  /**
@@ -7966,7 +8162,9 @@ function previewTransferFile(text) {
7966
8162
  * stack with a credential that belongs to no real CN account. So this helper
7967
8163
  * reuses `regionOfUserRegion` / `regionOfHost` — both of which already return
7968
8164
  * `undefined` on unrecognised input — and deliberately does NOT consult the
7969
- * edition fallback.
8165
+ * edition fallback. A US claim (`userRegion: "US"` or a `*.traeapi.us` host)
8166
+ * therefore resolves to `us` and matches neither importable region, so that
8167
+ * record is skipped like any other foreign one (issue #66).
7970
8168
  */
7971
8169
  function knownRegionOfCredential(credential) {
7972
8170
  return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host);
@@ -8163,6 +8361,25 @@ function json(res, status, body) {
8163
8361
  });
8164
8362
  res.end(payload);
8165
8363
  }
8364
+ /**
8365
+ * Answer a failed route with the meaning the failure actually has (issue #66).
8366
+ *
8367
+ * The old rule was `json(res, 500, { error })` everywhere, so a US credential
8368
+ * sent to the Singapore gateway surfaced as `HTTP 500
8369
+ * {"error":"SOLO remote models returned HTTP 401"}`: the status said "the
8370
+ * plugin is broken" while the text said "the gateway refused you". The client
8371
+ * could not tell "sign in again" from "this region is unsupported", which is
8372
+ * exactly the loop the issue reports. `httpFailureOf` keeps an upstream 401/403
8373
+ * as 401/403 and turns the local "not served" verdict into 501 +
8374
+ * `unsupported-region`; everything else stays 500.
8375
+ */
8376
+ function failJson(res, error) {
8377
+ const failure = httpFailureOf(error);
8378
+ json(res, failure.status, {
8379
+ error: safeMessage(error),
8380
+ ...failure.code === void 0 ? {} : { code: failure.code }
8381
+ });
8382
+ }
8166
8383
  /** Bound on how many OTHER accounts one status read may probe (issue #25). */
8167
8384
  const TRAE_CREDIT_ALTERNATIVE_LIMIT = 8;
8168
8385
  /**
@@ -8413,6 +8630,21 @@ async function traeWebUsage(deps, panel, options = {}) {
8413
8630
  * it reads nothing but file names and sizes.
8414
8631
  */
8415
8632
  const quarantined = await Promise.resolve().then(() => store.quarantinedCopies()).catch(() => []);
8633
+ /**
8634
+ * A region this build recognizes but does not serve (issue #66).
8635
+ *
8636
+ * Read ONCE per document and never allowed to fail it: this is a notice about
8637
+ * the user's disk, and the panel it explains is usually the one that has just
8638
+ * been excluded from billing. It is also why the panel can say "a US account
8639
+ * is here, and it is not being used" instead of an undiagnosable "not signed
8640
+ * in" on a machine the user knows is signed in.
8641
+ */
8642
+ const unsupported = await Promise.resolve().then(() => store.unsupportedCredentials()).catch(() => []);
8643
+ const firstUnsupported = unsupported[0];
8644
+ const unsupportedRegion = firstUnsupported === void 0 ? void 0 : {
8645
+ region: firstUnsupported.region,
8646
+ accounts: unsupported.map((item) => item.accountName)
8647
+ };
8416
8648
  if ((await store.status()).state !== "signed-in") {
8417
8649
  const { failures } = await store.diagnose();
8418
8650
  return {
@@ -8420,6 +8652,7 @@ async function traeWebUsage(deps, panel, options = {}) {
8420
8652
  accounts,
8421
8653
  enabled,
8422
8654
  quarantined,
8655
+ ...unsupportedRegion === void 0 ? {} : { unsupportedRegion },
8423
8656
  searched: failures.map((failure) => ({
8424
8657
  path: failure.path,
8425
8658
  edition: failure.edition,
@@ -8434,12 +8667,14 @@ async function traeWebUsage(deps, panel, options = {}) {
8434
8667
  credential = fast ? await store.current() : await store.resolve();
8435
8668
  if (credential === void 0) credential = await store.resolve();
8436
8669
  } catch (error) {
8670
+ const explained = error instanceof TraeUnsupportedRegionError && unsupportedRegion !== void 0;
8437
8671
  return {
8438
8672
  status: "signed-out",
8439
8673
  accounts,
8440
8674
  enabled,
8441
8675
  quarantined,
8442
- message: safeMessage(error)
8676
+ ...unsupportedRegion === void 0 ? {} : { unsupportedRegion },
8677
+ ...explained ? {} : { message: safeMessage(error) }
8443
8678
  };
8444
8679
  }
8445
8680
  const account = {
@@ -8449,6 +8684,7 @@ async function traeWebUsage(deps, panel, options = {}) {
8449
8684
  ...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
8450
8685
  region: regionOfPanel(panel),
8451
8686
  enabled,
8687
+ ...unsupportedRegion === void 0 ? {} : { unsupportedRegion },
8452
8688
  accounts,
8453
8689
  models: deps.displayModels(panel).map((model) => ({
8454
8690
  ...model,
@@ -8809,7 +9045,7 @@ function registerTraeUsageRoute(ctx, deps) {
8809
9045
  try {
8810
9046
  json(res, 200, await traeWebUsage(deps, panel, { detail }));
8811
9047
  } catch (error) {
8812
- json(res, 500, { error: safeMessage(error) });
9048
+ failJson(res, error);
8813
9049
  }
8814
9050
  }
8815
9051
  });
@@ -8830,7 +9066,7 @@ function registerTraeUsageRoute(ctx, deps) {
8830
9066
  try {
8831
9067
  json(res, 200, { accounts: await traeAccountCredits(deps, panel) });
8832
9068
  } catch (error) {
8833
- json(res, 500, { error: safeMessage(error) });
9069
+ failJson(res, error);
8834
9070
  }
8835
9071
  }
8836
9072
  });
@@ -8857,7 +9093,7 @@ function registerTraeUsageRoute(ctx, deps) {
8857
9093
  if (state === void 0) return json(res, 404, { error: "unknown account" });
8858
9094
  json(res, 200, state);
8859
9095
  } catch (error) {
8860
- json(res, 500, { error: safeMessage(error) });
9096
+ failJson(res, error);
8861
9097
  }
8862
9098
  }
8863
9099
  });
@@ -8949,7 +9185,7 @@ function registerTraeUsageRoute(ctx, deps) {
8949
9185
  try {
8950
9186
  json(res, 200, { models: await deps.discoverModels(panel) });
8951
9187
  } catch (error) {
8952
- json(res, 500, { error: safeMessage(error) });
9188
+ failJson(res, error);
8953
9189
  }
8954
9190
  }
8955
9191
  });
@@ -8983,7 +9219,7 @@ function registerTraeUsageRoute(ctx, deps) {
8983
9219
  try {
8984
9220
  json(res, 200, await deps.probeModels(panel, ids));
8985
9221
  } catch (error) {
8986
- json(res, 500, { error: safeMessage(error) });
9222
+ failJson(res, error);
8987
9223
  }
8988
9224
  }
8989
9225
  });
@@ -9067,7 +9303,7 @@ function registerTraeUsageRoute(ctx, deps) {
9067
9303
  checkin: toCheckin(checkin)
9068
9304
  });
9069
9305
  } catch (error) {
9070
- json(res, 500, { error: safeMessage(error) });
9306
+ failJson(res, error);
9071
9307
  }
9072
9308
  }
9073
9309
  });
@@ -9160,7 +9396,7 @@ function registerTraeUsageRoute(ctx, deps) {
9160
9396
  try {
9161
9397
  json(res, 200, { rows: (await pool.checkin(panel)).map((row) => poolCheckinRowViewOf(row)) });
9162
9398
  } catch (error) {
9163
- json(res, 500, { error: safeMessage(error) });
9399
+ failJson(res, error);
9164
9400
  }
9165
9401
  return;
9166
9402
  }
@@ -9185,7 +9421,7 @@ function registerTraeUsageRoute(ctx, deps) {
9185
9421
  if (row === void 0) return json(res, 404, { error: "unknown account" });
9186
9422
  return json(res, 200, { rows: [poolTestRowViewOf(row)] });
9187
9423
  } catch (error) {
9188
- return json(res, 500, { error: safeMessage(error) });
9424
+ return failJson(res, error);
9189
9425
  }
9190
9426
  res.writeHead(200, {
9191
9427
  "Content-Type": "application/x-ndjson",
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.3",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",