dsh-connect-trae 3.0.0 → 3.0.1

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.1 (2026-10-10)
4
+
5
+ ### 修复
6
+
7
+ - **跟随 Trae 当前登录的账号(issue #29)**。在 Trae 桌面端**退出换登录**后,插件此前一直绑定**换号前**的账号继续计费,而同族插件 `dsh-connect-workbuddy` 会自动跟随 App 的新登录;报告人需要每次手动打开账号下拉切一次。根因是两处:`TraeCredentialStore.preferred()` 只取 `credentials[0]`(**不认哪个来源是 App 的 live 登录**),`current()` 在 `accountId` 有值时**严格绑定、find 不命中也不回退**(刻意为之,见 `src/auth.ts` 的注释——避免悄悄给用户换一个计费账号)。
8
+
9
+ 分两条路径修,**都不改变未勾选用户的任何行为**:
10
+
11
+ 1. **`preferred()` 改为 live 登录优先**(`source === 'desktop'` 的凭据先于插件自己的刷新副本)。只改**排序**、不改 `readAll()` 的取值去重——同一账号仍保留更长寿的副本,否则会退回「有效 token 在隔壁文件却读成过期」的旧问题(`src/auth.ts:371-384` 的注释)。这修好「从未显式选择」的默认路径,与 workbuddy 对齐。
12
+ 2. **显式绑定的跟随**:新增只读方法 `liveLogin()` 报告「App 当前登录 ≠ 绑定账号」的**不一致**(纯读,绝不静默换绑);卡片在账号下拉旁给出**提示行 + 一键切换**(复用与下拉相同的 `switchAccount` 写入,保持单一写入路径);新增配置字段 `followAppLogin`(volatile,默认**关闭**)让宿主在每次配置生效时自动重绑——**默认关**是因为自动换绑意味着换人计费,必须由用户主动勾选,未勾选时提示行就是手动通道。
13
+
14
+ 配套:usage 文档新增 `appLogin` 字段(仅在不一致时携带,健康文档不加字段、扫描失败也不拖垮整个文档),双语文案 `row.appLoginMismatch` / `row.appLoginFollow` / `row.followAppLogin`,README「多账号切换」补充该行为说明。
15
+
16
+ ### 测试
17
+
18
+ - 新增 5 个:`preferred()` 优先 App 登录(旧 own 副本仍列出但不默认选中)、`liveLogin()` 三种不一致/一致状态、usage 文档 `appLogin` 的携带/省略/扫描失败降级;集成测试的 `MemorySettings` 补上 `revision` 冲突门(建模真实 `SettingsConflictError`)与新 volatile 字段 `followAppLogin`。
19
+ - `pnpm run check`(typecheck + 全量测试 + build)通过。
20
+
3
21
  ## 3.0.0 (2026-10-07)
4
22
 
5
23
  > **积分从「侧边栏常驻一行」改为「输入框工具栏的按供应商读数 + 账号表格浮层」。**
package/README.md CHANGED
@@ -41,6 +41,7 @@
41
41
  - **目录与勾选按区域隔离** —— 国内版与国际版各一套模型目录、勾选、图片开关与上下文预算,两个供应商各自读各自的槽位。
42
42
  - **识图能力跟随 Trae 自身的标注** —— Trae 把某个模型标为 `multimodal` 时,插件直接按多模态对外声明,无需你在卡片上逐一手勾(issue #16)。判据是「**你手动勾选的 ∪ Trae 标注多模态的**」:Trae 明确标 `false` 或未标注的模型保持纯文本,你可以照旧手动为它们打开图片输入。卡片上被自动开启的那一行显示为已勾选且只读,并说明原因。详见下方「图片输入(识图)」。
43
43
  - **多账号切换** —— 支持重新读取 Token 列表并选择账号;Token 不写入 DSH 设置。**你的选择会被记住并长期生效**:插件不会因为某个账号额度用尽、或检测到另一个可用账号就自作主张替你切换(换账号意味着换人计费)。所以当绑定的账号额度耗尽时,卡片会**明确告诉你**,并把本机其他还有额度的账号连同其可用积分数列出来,由你一键切换——而不是让面板一直空着、也不自动把你切走。「刷新账号与 Token」每次都会回报结果(找到几个账号、当前仍绑定谁),不会再「点了没反应」。下拉里显示的是产品名(`Trae` / `TRAE SOLO` / 国际版),便于分辨凭据来自哪个安装。
44
+ - **跟随 Trae 当前登录的账号** —— 你在 Trae 桌面端**退出并换登录**后,卡片会检测到「插件绑定的账号」与「Trae 现在登录的账号」不一致,并给出一行提示与**一键切换**(复用账号下拉的同一次写入,不引入第二条切换路径)。想让插件**自动跟随**,可打开设置卡片里的「跟随 Trae 当前登录的账号」开关:此后每次配置生效都会把 `accounts.<region>` 重绑到 App 当前登录的账号。**该开关默认关闭**——已有配置保持原来的严格绑定,自动换绑意味着换人计费,必须由你主动勾选;未勾选时上面那行提示就是你的手动通道。(issue #29)
44
45
  - **只读用量与模型管理** —— 国内版查看 Work 积分与通用积分,国际版查看订阅/试用状态;刷新/启用 Trae 模型;只读查询不消耗额度。
45
46
  - **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`/`4011`,SOLO 通道没有这个模型)/ **被限流** / **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
46
47
  - **每日签到领取** —— 国内版卡片上一键领取当日签到积分(按钮显示每日奖励,该账号已领取则显示「今日已领取」并禁用);这是插件里**唯一会改变账号状态**的操作,由你主动点击触发。Trae 的签到按「**每台设备每天一次**」限制:切换账号后如果本机今天的名额已经用掉(无论是哪个账号用的),当前账号在本机当天无法再签,卡片会说明原因而不是谎报「已领取」。国际版没有签到活动,因此不显示该按钮。
package/lib/client.js CHANGED
@@ -103,6 +103,20 @@ window.__ModuleLoader__.load({
103
103
  return typeof accounts === "object" && accounts !== null ? accounts : {};
104
104
  }
105
105
  /**
106
+ * Whether the host-side "follow the Trae app's sign-in" switch is on (issue
107
+ * #29). Absent reads as OFF, so an existing configuration keeps the strict
108
+ * bind it shipped with and the switch is an opt-in — the same opt-out
109
+ * semantics as {@link regionEnabledOf}, for the same reason: a config written
110
+ * before this field existed cannot ask for behavior it never declared.
111
+ *
112
+ * Reads the whole settings section (or the bare field); deep-unwraps first
113
+ * because on DSH 0.1.7 a volatile field arrives as a `{get(): T}` live
114
+ * reference, whose `typeof === 'object'` would otherwise pass a naive check.
115
+ */
116
+ function followAppLoginEnabled(configured) {
117
+ return unwrapVolatileDeep(configured)?.followAppLogin === true;
118
+ }
119
+ /**
106
120
  * Build the next `regions` settings value for the card's save. The write
107
121
  * targets ONLY the signed-in account's region slot; every other region's slot
108
122
  * is carried over untouched, so switching accounts never clobbers the other
@@ -822,6 +836,36 @@ window.__ModuleLoader__.load({
822
836
  }
823
837
  };
824
838
  /**
839
+ * Follow the Trae app's current sign-in from the hint row (issue #29).
840
+ *
841
+ * One click, and it goes through the SAME `switchAccount` write the account
842
+ * dropdown uses — so the card has exactly one writer for `accounts`, the Host
843
+ * sees the change through `loader/volatile-update` like every other card
844
+ * write, and there is no second settings path to keep in sync. The app's
845
+ * sign-in is `status.appLogin`, which the Host only reports when it differs
846
+ * from the bound account, so a click here always moves the binding to what
847
+ * the app is actually logged into.
848
+ */
849
+ const followAppLogin = async () => {
850
+ if (status.status !== "signed-in" || status.appLogin === void 0) return;
851
+ await switchAccount(status.appLogin.id);
852
+ };
853
+ /**
854
+ * Toggle the follow-the-app switch (`followAppLogin`, issue #29): the Host's
855
+ * automatic rebind follows this field, so the card only commits it. OFF by
856
+ * default in the schema; an existing config keeps the strict bind it shipped
857
+ * with until the user ticks this.
858
+ */
859
+ const toggleFollowAppLogin = async (enabled) => {
860
+ if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
861
+ setWriteError(void 0);
862
+ try {
863
+ await writeSettingsField(settingsScope, "followAppLogin", enabled, (readBack) => followAppLoginEnabled(readBack) === enabled);
864
+ } catch (error) {
865
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
866
+ }
867
+ };
868
+ /**
825
869
  * Claim today's check-in reward for the active tab's region.
826
870
  *
827
871
  * The Host route owns every guard that matters — POST only, loopback origin
@@ -1229,6 +1273,21 @@ window.__ModuleLoader__.load({
1229
1273
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.showPointsInMainUi") })]
1230
1274
  })
1231
1275
  }),
1276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1277
+ className: "dsm-trae-mainui-switch",
1278
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1279
+ className: "dsm-trae-mainui-switch-label",
1280
+ title: t("row.followAppLoginHint"),
1281
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1282
+ type: "checkbox",
1283
+ checked: followAppLoginEnabled(settingsValue),
1284
+ disabled: settingsScope?.getSnapshot().writable !== true,
1285
+ onChange: (event) => {
1286
+ toggleFollowAppLogin(event.currentTarget.checked);
1287
+ }
1288
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.followAppLogin") })]
1289
+ })
1290
+ }),
1232
1291
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1233
1292
  className: "dsm-trae-usage-account",
1234
1293
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1301,6 +1360,22 @@ window.__ModuleLoader__.load({
1301
1360
  children: t("row.creditsAlternativeSwitch")
1302
1361
  })]
1303
1362
  }) : null,
1363
+ status.status === "signed-in" && status.appLogin !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1364
+ className: "dsm-trae-usage-alternative",
1365
+ role: "status",
1366
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1367
+ className: "dsm-trae-usage-alternative-text",
1368
+ children: t("row.appLoginMismatch", { account: status.appLogin.accountName })
1369
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1370
+ type: "button",
1371
+ className: "dsm-trae-usage-switch-button",
1372
+ disabled: switchingAccount || settingsScope?.getSnapshot()?.writable !== true,
1373
+ onClick: () => {
1374
+ followAppLogin();
1375
+ },
1376
+ children: t("row.appLoginFollow")
1377
+ })]
1378
+ }) : null,
1304
1379
  rescanNote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1305
1380
  className: "dsm-trae-usage-note",
1306
1381
  role: "status",
@@ -2285,6 +2360,10 @@ window.__ModuleLoader__.load({
2285
2360
  "row.creditsExhausted": "The bound account ({account}) has run out of credit",
2286
2361
  "row.creditsAlternative": "Another account on this machine still has credit: {accounts}",
2287
2362
  "row.creditsAlternativeSwitch": "Switch to it",
2363
+ "row.appLoginMismatch": "Trae is signed in to {account}, but this plugin is still bound to the previous account",
2364
+ "row.appLoginFollow": "Switch to it",
2365
+ "row.followAppLogin": "Follow the account Trae is signed in to",
2366
+ "row.followAppLoginHint": "When you switch accounts in the Trae desktop app, rebind to the new sign-in automatically instead of staying on the previous one. Off by default: an existing binding is kept until you tick this.",
2288
2367
  "row.modelsTestEmpty": "Enable at least one model first.",
2289
2368
  "row.modelsTestError": "Test failed: {message}",
2290
2369
  "row.modelsTestDone": "{available} of {total} answered.",
@@ -2401,6 +2480,10 @@ window.__ModuleLoader__.load({
2401
2480
  "row.creditsExhausted": "当前绑定账号({account})额度已用尽",
2402
2481
  "row.creditsAlternative": "本机还有可用账号:{accounts}",
2403
2482
  "row.creditsAlternativeSwitch": "切换到它",
2483
+ "row.appLoginMismatch": "Trae 当前登录的是 {account},但本插件仍绑定在上一个账号",
2484
+ "row.appLoginFollow": "切换过去",
2485
+ "row.followAppLogin": "跟随 Trae 当前登录的账号",
2486
+ "row.followAppLoginHint": "在 Trae 桌面端切换登录后,自动换绑到新登录的账号,而不是停留在旧账号。默认关闭:已有绑定保持不变,直到你勾选它。",
2404
2487
  "row.modelsTestEmpty": "请先启用至少一个模型。",
2405
2488
  "row.modelsTestError": "测试失败:{message}",
2406
2489
  "row.modelsTestDone": "{total} 个里有 {available} 个正常应答。",
package/lib/index.d.ts CHANGED
@@ -475,12 +475,35 @@ declare class TraeCredentialStore {
475
475
  selectAccount(accountId: string | undefined): void;
476
476
  candidates(): TraeStorageCandidate[];
477
477
  /**
478
- * Deterministic default when no account is explicitly selected: the first
479
- * discovered account. This is NOT credit-seeking — it never reorders accounts
480
- * to find one with general credits. The plugin bills exactly the account the
481
- * user selected, or the first account when nothing has been selected yet.
478
+ * Deterministic default when no account is explicitly selected: the account
479
+ * the Trae desktop app is signed in to RIGHT NOW, else the first discovered
480
+ * one. This is NOT credit-seeking — it never reorders accounts to find one
481
+ * with general credits. The plugin bills exactly the account the user
482
+ * selected, the app's current sign-in when nothing has been selected, and
483
+ * only then the first account.
484
+ *
485
+ * The app's live sign-in outranks the plugin's own refreshed copies (issue
486
+ * #29): after the user switches accounts in Trae, the desktop storage file
487
+ * holds the NEW login while the plugin's copy of the old one still resolves,
488
+ * so a bare `credentials[0]` kept defaulting to a sign-in the app has
489
+ * abandoned — and `dsh-connect-workbuddy`, whose `preferred()` ranks the live
490
+ * file first, followed the app instead. Ordering only: the VALUE behind an
491
+ * account id is still whichever copy expires later (`readAll`), so a refresh
492
+ * the plugin performed is never discarded by this preference.
482
493
  */
483
494
  private preferred;
495
+ /**
496
+ * The account the Trae app is signed in to right now, when that differs from
497
+ * the account this store is bound to (issue #29).
498
+ *
499
+ * This is a READ, not a rebind: `current()` keeps resolving the bound
500
+ * account verbatim so billing never moves without a decision — the store
501
+ * only reports the mismatch, and the card (hint row) or the host (follow
502
+ * switch) turns it into a settings write the user can see and undo. Absent
503
+ * when nothing is live, when the live account IS the bound one, or when the
504
+ * binding itself already fell through to the live account.
505
+ */
506
+ liveLogin(): Promise<TraeAccountChoice | undefined>;
484
507
  accounts(): Promise<TraeAccountChoice[]>;
485
508
  current(): Promise<TraeCredential | undefined>;
486
509
  /**
@@ -1689,6 +1712,24 @@ interface TraeWebAccount {
1689
1712
  tokenExpiresAtMs: number;
1690
1713
  selected: boolean;
1691
1714
  }
1715
+ /**
1716
+ * The account the Trae desktop app is signed in to right now, when it differs
1717
+ * from the one this plugin is bound to (issue #29).
1718
+ *
1719
+ * Reported on the usage document so the card can offer one-click follow without
1720
+ * an extra round trip; also written back on a POST to the accounts refresh
1721
+ * route when the client confirms it saw the hint. Absent whenever the two
1722
+ * agree or the app is not readable — a healthy document carries no extra
1723
+ * fields and the plugin never bills from this value directly: the FOLLOW is a
1724
+ * settings write of `accounts.<region>`, made by the card or by the host's
1725
+ * follow switch, never a silent rebind here.
1726
+ */
1727
+ interface TraeWebAppLogin {
1728
+ id: string;
1729
+ accountName: string;
1730
+ edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
1731
+ region: TraeRegion;
1732
+ }
1692
1733
  interface TraeWebCreditAlternative {
1693
1734
  id: string;
1694
1735
  accountName: string;
@@ -1729,6 +1770,33 @@ declare function unwrapVolatile<T>(value: T): T;
1729
1770
  * value is never touched.
1730
1771
  */
1731
1772
  declare function unwrapVolatileDeep<T>(value: T): T;
1773
+ /**
1774
+ * Read the per-region account selections out of a settings value.
1775
+ *
1776
+ * SHARED because `accounts` now has TWO writers: the card, and the composer
1777
+ * panel's account table (2.14.0). Two writers of one field must use one merge
1778
+ * rule, or they drift — and the rule is not obvious.
1779
+ *
1780
+ * Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
1781
+ * live reference, which passes the `typeof === 'object'` check and would be
1782
+ * returned as if it were the map. Every lookup on it is then `undefined`, and
1783
+ * the merge (`{ ...configured, [region]: id }`) would spread a reference into
1784
+ * `{get: <function>}` — dropping the other region's selection and leaking a
1785
+ * function into the settings document.
1786
+ */
1787
+ declare function configuredAccountsOf(configured: unknown): Record<string, string>;
1788
+ /**
1789
+ * Whether the host-side "follow the Trae app's sign-in" switch is on (issue
1790
+ * #29). Absent reads as OFF, so an existing configuration keeps the strict
1791
+ * bind it shipped with and the switch is an opt-in — the same opt-out
1792
+ * semantics as {@link regionEnabledOf}, for the same reason: a config written
1793
+ * before this field existed cannot ask for behavior it never declared.
1794
+ *
1795
+ * Reads the whole settings section (or the bare field); deep-unwraps first
1796
+ * because on DSH 0.1.7 a volatile field arrives as a `{get(): T}` live
1797
+ * reference, whose `typeof === 'object'` would otherwise pass a naive check.
1798
+ */
1799
+ declare function followAppLoginEnabled(configured: unknown): boolean;
1732
1800
  /**
1733
1801
  * Build the next `regions` settings value for the card's save. The write
1734
1802
  * targets ONLY the signed-in account's region slot; every other region's slot
@@ -1819,6 +1887,15 @@ type TraeWebUsage = {
1819
1887
  * other case so a healthy account never pays for the extra read.
1820
1888
  */
1821
1889
  alternatives?: readonly TraeWebCreditAlternative[];
1890
+ /**
1891
+ * Present only when the Trae app's CURRENT sign-in differs from the
1892
+ * account this plugin is bound to (issue #29): the card then offers a
1893
+ * one-click follow instead of making the user hunt the account dropdown
1894
+ * after every app-side sign-out. Absent in every other case — a healthy
1895
+ * document costs no extra read beyond the accounts scan the document
1896
+ * already runs.
1897
+ */
1898
+ appLogin?: TraeWebAppLogin;
1822
1899
  /**
1823
1900
  * Daily check-in state (CN only). Absent on the international region,
1824
1901
  * whose check-in surface does not exist — probed 2026-09-24: the
@@ -1853,6 +1930,14 @@ interface TraeUsageRouteOptions {
1853
1930
  * exhausted-account hint is simply not offered.
1854
1931
  */
1855
1932
  creditsOfAccount?(region: TraeRegion, accountId: string): Promise<TraeWebCredits | undefined>;
1933
+ /**
1934
+ * The account the Trae app is signed in to right now, when it differs from
1935
+ * the bound one (issue #29). Optional: absent means the card's follow hint
1936
+ * simply is not offered (a test harness or a host without the store's scan).
1937
+ * Read-only — the HOST never rebinds from this value; the follow is a
1938
+ * settings write, made by the card or by the host's follow switch.
1939
+ */
1940
+ liveLogin?(region: TraeRegion): Promise<TraeWebAppLogin | undefined>;
1856
1941
  /** The requested region's last-refreshed raw directory (one entry per upstream model). */
1857
1942
  displayModels(region: TraeRegion): readonly TraeModelInfo[];
1858
1943
  /** The user's model selection in the requested region, stored as model id (= Trae name). */
@@ -2030,6 +2115,26 @@ interface Config {
2030
2115
  * settings form can both read and write it.
2031
2116
  */
2032
2117
  showPointsInMainUi?: boolean;
2118
+ /**
2119
+ * Follow the Trae desktop app's CURRENT sign-in instead of staying bound to
2120
+ * the account the user (or the default) picked earlier (issue #29).
2121
+ *
2122
+ * When the app switches accounts, the old binding kept billing the previous
2123
+ * account forever while `dsh-connect-workbuddy` followed the app
2124
+ * automatically — every Trae sign-out then needed a manual switch here. With
2125
+ * this switch ON, each settings re-apply rebinds `accounts.<region>` to
2126
+ * whatever the app is signed in to right now (the live login outranks the
2127
+ * plugin's own refreshed copies, see `TraeCredentialStore.preferred`).
2128
+ *
2129
+ * OFF by default: an existing configuration keeps the strict bind it
2130
+ * shipped with, because silently switching billing accounts is exactly what
2131
+ * `current()`'s no-fallback rule exists to prevent — the user opts in. When
2132
+ * OFF, the card still shows a one-click hint (its own settings write), so
2133
+ * the report in #29 is solved on both paths without any behavior change
2134
+ * nobody asked for. Declared volatile so the card's switch can read and
2135
+ * write it on the 0.1.7 settings form.
2136
+ */
2137
+ followAppLogin?: boolean;
2033
2138
  /**
2034
2139
  * Per-region model state, keyed `cn` | `ai`. The CN and international apps
2035
2140
  * expose different rosters, so each keeps its own directory and selection
@@ -2088,4 +2193,4 @@ declare function asVolatile<S>(schema: S): S;
2088
2193
  declare const Config: z<Config>;
2089
2194
  declare function apply(ctx: Context, config: Config): void;
2090
2195
  //#endregion
2091
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeProbeResult, type TraeProbeVerdict, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
2196
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeProbeResult, type TraeProbeVerdict, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebActivity, type TraeWebAppLogin, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
package/lib/index.js CHANGED
@@ -452,6 +452,202 @@ function createTraeAdapter(options) {
452
452
  };
453
453
  }
454
454
  //#endregion
455
+ //#region src/status-paths.ts
456
+ /** Plugin-owned usage endpoint consumed by its browser half. */
457
+ const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
458
+ /** Plugin-owned live model refresh endpoint. */
459
+ const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
460
+ /**
461
+ * Plugin-owned availability probe: POST the model ids to test, get one verdict
462
+ * per model. Unlike every read-only sibling this route SPENDS quota — a real
463
+ * (minimal) chat call per model — so it is user-initiated only, and the card
464
+ * labels the button as consuming a small amount.
465
+ */
466
+ const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
467
+ /** Plugin-owned local account rescan endpoint. */
468
+ const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
469
+ /**
470
+ * General balance of EVERY account in a region, for the composer panel's table.
471
+ *
472
+ * Separate from the usage route on purpose: the usage document backs the
473
+ * 5-minute readout, and multiplying its upstream reads by the account count
474
+ * would charge a healthy account for something it never looks at — the same
475
+ * principle that keeps `creditAlternatives` firing only when a balance hits
476
+ * zero. This one is paid when the panel is actually opened, which is the only
477
+ * place the table is shown.
478
+ */
479
+ const TRAE_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-trae/account-credits";
480
+ /**
481
+ * Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
482
+ * route in this plugin that changes upstream account state.
483
+ */
484
+ const TRAE_CHECKIN_PATH = "/plugins/dsh-connect-trae/checkin";
485
+ /** Query parameter naming the region a card request addresses. */
486
+ const TRAE_REGION_PARAM = "region";
487
+ /** Every region, in card tab order. */
488
+ const TRAE_REGIONS = ["cn", "ai"];
489
+ /**
490
+ * Address one region's status route. The two regions are separate provider
491
+ * stacks; every card request carries the region whose tab the user is on.
492
+ */
493
+ function withTraeRegion(path, region) {
494
+ return `${path}?${TRAE_REGION_PARAM}=${region}`;
495
+ }
496
+ /**
497
+ * Read the region parameter off a status-route URL. Absent means the domestic
498
+ * tab (`cn`); a present-but-unknown value returns undefined so the route can
499
+ * answer 400 instead of guessing.
500
+ */
501
+ function regionOfTraeStatusUrl(url) {
502
+ const at = url.indexOf("?");
503
+ const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(TRAE_REGION_PARAM);
504
+ if (value === null || value === "") return "cn";
505
+ return TRAE_REGIONS.includes(value) ? value : void 0;
506
+ }
507
+ /**
508
+ * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
509
+ * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
510
+ */
511
+ function unwrapVolatile(value) {
512
+ if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get();
513
+ return value;
514
+ }
515
+ /**
516
+ * Deep copy of a value with every `{get(): T}` live reference replaced by the
517
+ * value it resolves to.
518
+ *
519
+ * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
520
+ * reference is still `typeof === 'object'` — so it passes a naive object check
521
+ * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
522
+ * does NOT read the field: it produces `{ get: <function> }`. Any merge that
523
+ * spreads a resolved field to preserve its siblings — the card's "write one
524
+ * region, keep the other" write, or the account map's spread — would otherwise
525
+ * DROP every sibling and leak a function into the document.
526
+ *
527
+ * Both halves therefore unwrap before touching a resolved field. This lives in
528
+ * the node-free host↔client bridge because the browser half needs it too.
529
+ *
530
+ * Non-reference values are recursed into so a nested volatile field is caught
531
+ * as well; arrays and objects are rebuilt rather than mutated, so the caller's
532
+ * value is never touched.
533
+ */
534
+ function unwrapVolatileDeep(value) {
535
+ if (value === null || typeof value !== "object") return value;
536
+ if (typeof value.get === "function") return unwrapVolatileDeep(value.get());
537
+ if (Array.isArray(value)) return value.map((entry) => unwrapVolatileDeep(entry));
538
+ const source = value;
539
+ const out = {};
540
+ for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
541
+ return out;
542
+ }
543
+ /**
544
+ * Read the per-region account selections out of a settings value.
545
+ *
546
+ * SHARED because `accounts` now has TWO writers: the card, and the composer
547
+ * panel's account table (2.14.0). Two writers of one field must use one merge
548
+ * rule, or they drift — and the rule is not obvious.
549
+ *
550
+ * Deep-unwraps first: on DSH 0.1.7 a volatile field arrives as a `{get(): T}`
551
+ * live reference, which passes the `typeof === 'object'` check and would be
552
+ * returned as if it were the map. Every lookup on it is then `undefined`, and
553
+ * the merge (`{ ...configured, [region]: id }`) would spread a reference into
554
+ * `{get: <function>}` — dropping the other region's selection and leaking a
555
+ * function into the settings document.
556
+ */
557
+ function configuredAccountsOf(configured) {
558
+ const accounts = unwrapVolatileDeep(configured)?.accounts;
559
+ return typeof accounts === "object" && accounts !== null ? accounts : {};
560
+ }
561
+ /**
562
+ * Whether the host-side "follow the Trae app's sign-in" switch is on (issue
563
+ * #29). Absent reads as OFF, so an existing configuration keeps the strict
564
+ * bind it shipped with and the switch is an opt-in — the same opt-out
565
+ * semantics as {@link regionEnabledOf}, for the same reason: a config written
566
+ * before this field existed cannot ask for behavior it never declared.
567
+ *
568
+ * Reads the whole settings section (or the bare field); deep-unwraps first
569
+ * because on DSH 0.1.7 a volatile field arrives as a `{get(): T}` live
570
+ * reference, whose `typeof === 'object'` would otherwise pass a naive check.
571
+ */
572
+ function followAppLoginEnabled(configured) {
573
+ return unwrapVolatileDeep(configured)?.followAppLogin === true;
574
+ }
575
+ /**
576
+ * Build the next `regions` settings value for the card's save. The write
577
+ * targets ONLY the signed-in account's region slot; every other region's slot
578
+ * is carried over untouched, so switching accounts never clobbers the other
579
+ * region's picks. Tolerates any stored shape (absent, non-object) by starting
580
+ * from an empty document.
581
+ */
582
+ function nextRegionSlots(regions, region, slot) {
583
+ const unwrapped = unwrapVolatileDeep(regions);
584
+ return {
585
+ ...typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped) ? unwrapped : {},
586
+ [region]: slot
587
+ };
588
+ }
589
+ /**
590
+ * Narrow a settings value to the `regions` map. Accepts EITHER the whole
591
+ * settings section (`{ regions: {...}, ... }`) or the `regions` map itself, and
592
+ * unwraps the former. This tolerance is deliberate: passing the whole section
593
+ * where the map was expected was a real shipped bug — the lookup then read
594
+ * `section['cn']` (absent), so the card's checkbox reported `true` forever and
595
+ * clicking it appeared to do nothing even though the write succeeded.
596
+ *
597
+ * Unwraps live references first: on DSH 0.1.7 the `regions` field arrives as a
598
+ * `{get(): T}` reference, which passes the `typeof === 'object'` check below
599
+ * and would otherwise be returned as if it were the map — making every region
600
+ * read back as absent (and the enabled flag read as "on" forever).
601
+ */
602
+ function regionsMapOf(value) {
603
+ const unwrapped = unwrapVolatileDeep(value);
604
+ if (typeof unwrapped !== "object" || unwrapped === null || Array.isArray(unwrapped)) return {};
605
+ const record = unwrapped;
606
+ const nested = record["regions"];
607
+ if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
608
+ return record;
609
+ }
610
+ /** One region's stored slot as a plain object; any other shape reads as empty. */
611
+ function regionSlotOf(value, region) {
612
+ const slot = unwrapVolatile(regionsMapOf(value)[region]);
613
+ return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
614
+ }
615
+ /**
616
+ * Whether one region's provider is switched on. Opt-out semantics: only an
617
+ * explicit `false` disables it, so a config written before this switch existed
618
+ * (and the pre-region-split flat fields, which never carry `enabled`) keep both
619
+ * providers running exactly as before. The Host reads the same rule through
620
+ * `regionStateOf`, so card and Host can never disagree about a region's state.
621
+ *
622
+ * `value` may be the whole settings section or the `regions` map (see
623
+ * {@link regionsMapOf}).
624
+ */
625
+ function regionEnabledOf(value, region) {
626
+ return regionSlotOf(value, region)["enabled"] !== false;
627
+ }
628
+ /**
629
+ * Build the next `regions` settings value for a provider on/off toggle. ONLY
630
+ * the target region's `enabled` flag changes: every other field of that slot
631
+ * (its directory, selection, image opt-ins, context budgets) and every other
632
+ * region's slot are carried over verbatim, so switching a provider off never
633
+ * discards the user's model picks and switching it back on restores them.
634
+ *
635
+ * This is deliberately separate from {@link nextRegionSlots}: that helper
636
+ * writes a whole slot from a signed-in tab's draft, while this one must work
637
+ * for a region that is signed OUT — which is precisely the region a user wants
638
+ * to switch off (no international install, no international account).
639
+ *
640
+ * `value` may be the whole settings section or the `regions` map; the RETURN
641
+ * value is always the `regions` map, i.e. exactly what `settingsScope.set(
642
+ * 'regions', ...)` needs.
643
+ */
644
+ function nextRegionEnabled(value, region, enabled) {
645
+ return nextRegionSlots(regionsMapOf(value), region, {
646
+ ...regionSlotOf(value, region),
647
+ enabled
648
+ });
649
+ }
650
+ //#endregion
455
651
  //#region src/decrypt.ts
456
652
  const TRAE_AUTH_STORAGE_KEY = "iCubeAuthInfo://icube.cloudide";
457
653
  const SALT_A = Uint8Array.from([
@@ -1171,13 +1367,52 @@ var TraeCredentialStore = class {
1171
1367
  return this.edition === "auto" ? all.filter((candidate) => candidate.source === "desktop" || candidate.edition === cliEdition) : all.filter((candidate) => candidate.edition === this.edition && (candidate.source === "desktop" || candidate.edition === cliEdition));
1172
1368
  }
1173
1369
  /**
1174
- * Deterministic default when no account is explicitly selected: the first
1175
- * discovered account. This is NOT credit-seeking — it never reorders accounts
1176
- * to find one with general credits. The plugin bills exactly the account the
1177
- * user selected, or the first account when nothing has been selected yet.
1370
+ * Deterministic default when no account is explicitly selected: the account
1371
+ * the Trae desktop app is signed in to RIGHT NOW, else the first discovered
1372
+ * one. This is NOT credit-seeking — it never reorders accounts to find one
1373
+ * with general credits. The plugin bills exactly the account the user
1374
+ * selected, the app's current sign-in when nothing has been selected, and
1375
+ * only then the first account.
1376
+ *
1377
+ * The app's live sign-in outranks the plugin's own refreshed copies (issue
1378
+ * #29): after the user switches accounts in Trae, the desktop storage file
1379
+ * holds the NEW login while the plugin's copy of the old one still resolves,
1380
+ * so a bare `credentials[0]` kept defaulting to a sign-in the app has
1381
+ * abandoned — and `dsh-connect-workbuddy`, whose `preferred()` ranks the live
1382
+ * file first, followed the app instead. Ordering only: the VALUE behind an
1383
+ * account id is still whichever copy expires later (`readAll`), so a refresh
1384
+ * the plugin performed is never discarded by this preference.
1178
1385
  */
1179
1386
  preferred(credentials) {
1180
- return credentials[0];
1387
+ return credentials.find((credential) => credential.source === "desktop") ?? credentials[0];
1388
+ }
1389
+ /**
1390
+ * The account the Trae app is signed in to right now, when that differs from
1391
+ * the account this store is bound to (issue #29).
1392
+ *
1393
+ * This is a READ, not a rebind: `current()` keeps resolving the bound
1394
+ * account verbatim so billing never moves without a decision — the store
1395
+ * only reports the mismatch, and the card (hint row) or the host (follow
1396
+ * switch) turns it into a settings write the user can see and undo. Absent
1397
+ * when nothing is live, when the live account IS the bound one, or when the
1398
+ * binding itself already fell through to the live account.
1399
+ */
1400
+ async liveLogin() {
1401
+ if (this.accountId === void 0 || this.accountId === "") return void 0;
1402
+ const credentials = await this.readAll();
1403
+ const live = this.preferred(credentials);
1404
+ if (live === void 0) return void 0;
1405
+ const liveId = traeAccountId(live);
1406
+ if (liveId === this.accountId) return void 0;
1407
+ return {
1408
+ id: liveId,
1409
+ accountName: live.accountName ?? (live.userId || `${live.edition} account`),
1410
+ edition: live.edition,
1411
+ region: regionOfCredential(live),
1412
+ source: live.source,
1413
+ tokenExpiresAtMs: live.expiresAtMs,
1414
+ selected: false
1415
+ };
1181
1416
  }
1182
1417
  async accounts() {
1183
1418
  const credentials = await this.readAll();
@@ -3927,170 +4162,6 @@ var TraeUsageClient = class {
3927
4162
  }
3928
4163
  };
3929
4164
  //#endregion
3930
- //#region src/status-paths.ts
3931
- /** Plugin-owned usage endpoint consumed by its browser half. */
3932
- const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
3933
- /** Plugin-owned live model refresh endpoint. */
3934
- const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
3935
- /**
3936
- * Plugin-owned availability probe: POST the model ids to test, get one verdict
3937
- * per model. Unlike every read-only sibling this route SPENDS quota — a real
3938
- * (minimal) chat call per model — so it is user-initiated only, and the card
3939
- * labels the button as consuming a small amount.
3940
- */
3941
- const TRAE_MODELS_TEST_PATH = "/plugins/dsh-connect-trae/models/test";
3942
- /** Plugin-owned local account rescan endpoint. */
3943
- const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
3944
- /**
3945
- * General balance of EVERY account in a region, for the composer panel's table.
3946
- *
3947
- * Separate from the usage route on purpose: the usage document backs the
3948
- * 5-minute readout, and multiplying its upstream reads by the account count
3949
- * would charge a healthy account for something it never looks at — the same
3950
- * principle that keeps `creditAlternatives` firing only when a balance hits
3951
- * zero. This one is paid when the panel is actually opened, which is the only
3952
- * place the table is shown.
3953
- */
3954
- const TRAE_ACCOUNT_CREDITS_PATH = "/plugins/dsh-connect-trae/account-credits";
3955
- /**
3956
- * Plugin-owned daily check-in claim endpoint. POST, loopback-only, and the only
3957
- * route in this plugin that changes upstream account state.
3958
- */
3959
- const TRAE_CHECKIN_PATH = "/plugins/dsh-connect-trae/checkin";
3960
- /** Query parameter naming the region a card request addresses. */
3961
- const TRAE_REGION_PARAM = "region";
3962
- /** Every region, in card tab order. */
3963
- const TRAE_REGIONS = ["cn", "ai"];
3964
- /**
3965
- * Address one region's status route. The two regions are separate provider
3966
- * stacks; every card request carries the region whose tab the user is on.
3967
- */
3968
- function withTraeRegion(path, region) {
3969
- return `${path}?${TRAE_REGION_PARAM}=${region}`;
3970
- }
3971
- /**
3972
- * Read the region parameter off a status-route URL. Absent means the domestic
3973
- * tab (`cn`); a present-but-unknown value returns undefined so the route can
3974
- * answer 400 instead of guessing.
3975
- */
3976
- function regionOfTraeStatusUrl(url) {
3977
- const at = url.indexOf("?");
3978
- const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(TRAE_REGION_PARAM);
3979
- if (value === null || value === "") return "cn";
3980
- return TRAE_REGIONS.includes(value) ? value : void 0;
3981
- }
3982
- /**
3983
- * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
3984
- * volatile-marked settings field as (see {@link unwrapVolatileDeep}).
3985
- */
3986
- function unwrapVolatile(value) {
3987
- if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get();
3988
- return value;
3989
- }
3990
- /**
3991
- * Deep copy of a value with every `{get(): T}` live reference replaced by the
3992
- * value it resolves to.
3993
- *
3994
- * DSH 0.1.7 hands volatile-marked fields back as live references, and a live
3995
- * reference is still `typeof === 'object'` — so it passes a naive object check
3996
- * and every lookup on it is `undefined`. Worse, spreading one (`{ ...ref }`)
3997
- * does NOT read the field: it produces `{ get: <function> }`. Any merge that
3998
- * spreads a resolved field to preserve its siblings — the card's "write one
3999
- * region, keep the other" write, or the account map's spread — would otherwise
4000
- * DROP every sibling and leak a function into the document.
4001
- *
4002
- * Both halves therefore unwrap before touching a resolved field. This lives in
4003
- * the node-free host↔client bridge because the browser half needs it too.
4004
- *
4005
- * Non-reference values are recursed into so a nested volatile field is caught
4006
- * as well; arrays and objects are rebuilt rather than mutated, so the caller's
4007
- * value is never touched.
4008
- */
4009
- function unwrapVolatileDeep(value) {
4010
- if (value === null || typeof value !== "object") return value;
4011
- if (typeof value.get === "function") return unwrapVolatileDeep(value.get());
4012
- if (Array.isArray(value)) return value.map((entry) => unwrapVolatileDeep(entry));
4013
- const source = value;
4014
- const out = {};
4015
- for (const key of Object.keys(source)) out[key] = unwrapVolatileDeep(source[key]);
4016
- return out;
4017
- }
4018
- /**
4019
- * Build the next `regions` settings value for the card's save. The write
4020
- * targets ONLY the signed-in account's region slot; every other region's slot
4021
- * is carried over untouched, so switching accounts never clobbers the other
4022
- * region's picks. Tolerates any stored shape (absent, non-object) by starting
4023
- * from an empty document.
4024
- */
4025
- function nextRegionSlots(regions, region, slot) {
4026
- const unwrapped = unwrapVolatileDeep(regions);
4027
- return {
4028
- ...typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped) ? unwrapped : {},
4029
- [region]: slot
4030
- };
4031
- }
4032
- /**
4033
- * Narrow a settings value to the `regions` map. Accepts EITHER the whole
4034
- * settings section (`{ regions: {...}, ... }`) or the `regions` map itself, and
4035
- * unwraps the former. This tolerance is deliberate: passing the whole section
4036
- * where the map was expected was a real shipped bug — the lookup then read
4037
- * `section['cn']` (absent), so the card's checkbox reported `true` forever and
4038
- * clicking it appeared to do nothing even though the write succeeded.
4039
- *
4040
- * Unwraps live references first: on DSH 0.1.7 the `regions` field arrives as a
4041
- * `{get(): T}` reference, which passes the `typeof === 'object'` check below
4042
- * and would otherwise be returned as if it were the map — making every region
4043
- * read back as absent (and the enabled flag read as "on" forever).
4044
- */
4045
- function regionsMapOf(value) {
4046
- const unwrapped = unwrapVolatileDeep(value);
4047
- if (typeof unwrapped !== "object" || unwrapped === null || Array.isArray(unwrapped)) return {};
4048
- const record = unwrapped;
4049
- const nested = record["regions"];
4050
- if (typeof nested === "object" && nested !== null && !Array.isArray(nested)) return nested;
4051
- return record;
4052
- }
4053
- /** One region's stored slot as a plain object; any other shape reads as empty. */
4054
- function regionSlotOf(value, region) {
4055
- const slot = unwrapVolatile(regionsMapOf(value)[region]);
4056
- return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
4057
- }
4058
- /**
4059
- * Whether one region's provider is switched on. Opt-out semantics: only an
4060
- * explicit `false` disables it, so a config written before this switch existed
4061
- * (and the pre-region-split flat fields, which never carry `enabled`) keep both
4062
- * providers running exactly as before. The Host reads the same rule through
4063
- * `regionStateOf`, so card and Host can never disagree about a region's state.
4064
- *
4065
- * `value` may be the whole settings section or the `regions` map (see
4066
- * {@link regionsMapOf}).
4067
- */
4068
- function regionEnabledOf(value, region) {
4069
- return regionSlotOf(value, region)["enabled"] !== false;
4070
- }
4071
- /**
4072
- * Build the next `regions` settings value for a provider on/off toggle. ONLY
4073
- * the target region's `enabled` flag changes: every other field of that slot
4074
- * (its directory, selection, image opt-ins, context budgets) and every other
4075
- * region's slot are carried over verbatim, so switching a provider off never
4076
- * discards the user's model picks and switching it back on restores them.
4077
- *
4078
- * This is deliberately separate from {@link nextRegionSlots}: that helper
4079
- * writes a whole slot from a signed-in tab's draft, while this one must work
4080
- * for a region that is signed OUT — which is precisely the region a user wants
4081
- * to switch off (no international install, no international account).
4082
- *
4083
- * `value` may be the whole settings section or the `regions` map; the RETURN
4084
- * value is always the `regions` map, i.e. exactly what `settingsScope.set(
4085
- * 'regions', ...)` needs.
4086
- */
4087
- function nextRegionEnabled(value, region, enabled) {
4088
- return nextRegionSlots(regionsMapOf(value), region, {
4089
- ...regionSlotOf(value, region),
4090
- enabled
4091
- });
4092
- }
4093
- //#endregion
4094
4165
  //#region src/web-status.ts
4095
4166
  /**
4096
4167
  * The upstream's business code for "该设备今日已参与签到" — this DEVICE already
@@ -4233,6 +4304,10 @@ async function traeWebUsage(deps, region) {
4233
4304
  message: safeMessage(error)
4234
4305
  };
4235
4306
  }
4307
+ let appLogin;
4308
+ try {
4309
+ appLogin = await deps.liveLogin?.(region);
4310
+ } catch {}
4236
4311
  const account = {
4237
4312
  accountId: accounts.find((item) => item.selected)?.id ?? "",
4238
4313
  accountName: credential.accountName ?? credential.userId,
@@ -4240,6 +4315,7 @@ async function traeWebUsage(deps, region) {
4240
4315
  region,
4241
4316
  enabled,
4242
4317
  accounts,
4318
+ ...appLogin === void 0 ? {} : { appLogin },
4243
4319
  models: deps.displayModels(region).map((model) => ({
4244
4320
  ...model,
4245
4321
  ...model.input === void 0 ? {} : { input: [...model.input] }
@@ -4837,6 +4913,7 @@ const Config = z.object({
4837
4913
  ]).default("auto").description("Trae edition hint")),
4838
4914
  accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
4839
4915
  showPointsInMainUi: asVolatile(z.boolean().default(false).description("在输入框工具栏显示积分读数「Trae CN · <数值>」(仅当选中的模型属于本插件时出现,可点击查看账号与积分明细)")),
4916
+ followAppLogin: asVolatile(z.boolean().default(false).description("跟随 Trae 桌面端当前登录账号换绑(打开后每次配置生效时重绑到 App 现在登录的账号)")),
4840
4917
  accounts: asVolatile(accountSelectionConfig.description("Per-region account selections, keyed cn | ai")),
4841
4918
  regions: asVolatile(z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | ai")),
4842
4919
  lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
@@ -5092,6 +5169,49 @@ function apply(ctx, config) {
5092
5169
  rawDiagnostic: () => rawDiagnostic()
5093
5170
  };
5094
5171
  }
5172
+ /**
5173
+ * The app's current sign-in when it differs from the bound account (issue
5174
+ * #29). Pure read: the store resolves the bound account verbatim, so billing
5175
+ * never moves from here — the card's hint and the follow switch below are
5176
+ * the only writers.
5177
+ */
5178
+ const liveLoginOf = async (region) => {
5179
+ try {
5180
+ const live = await stacks[region].store.liveLogin();
5181
+ if (live === void 0 || live.region !== region) return void 0;
5182
+ return {
5183
+ id: live.id,
5184
+ accountName: live.accountName,
5185
+ edition: live.edition,
5186
+ region: live.region
5187
+ };
5188
+ } catch {
5189
+ return;
5190
+ }
5191
+ };
5192
+ /**
5193
+ * Persist `accounts.<region>` = the app's live sign-in and rebind the store
5194
+ * (issue #29). The ONE Host-side path that moves the binding, driven by the
5195
+ * follow switch in {@link maybeFollowAppLogin}; the card's one-click hint
5196
+ * goes through its own settings write instead (one writer per path). Returns
5197
+ * the applied login so the caller can report what moved.
5198
+ *
5199
+ * The write goes through `settings.update` (the volatile write gate), not a
5200
+ * direct config mutation: `apply()` receives a snapshot, and the settings
5201
+ * event (`loader/volatile-update`) is what re-runs `applySelection` with the
5202
+ * updated document — the same channel every card write already uses.
5203
+ */
5204
+ const followAppLogin = async (region) => {
5205
+ const live = await liveLoginOf(region);
5206
+ if (live === void 0 || writeSettings === void 0) return void 0;
5207
+ const configured = configuredAccountsOf(current());
5208
+ if (configured[region] === live.id) return live;
5209
+ await writeSettings({ accounts: {
5210
+ ...configured,
5211
+ [region]: live.id
5212
+ } });
5213
+ return live;
5214
+ };
5095
5215
  ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
5096
5216
  store: (region) => stacks[region].store,
5097
5217
  client: (region) => stacks[region].usageClient,
@@ -5101,7 +5221,8 @@ function apply(ctx, config) {
5101
5221
  discoverModels: (region, signal) => stacks[region].discoverModels(signal),
5102
5222
  probeModels: (region, ids) => stacks[region].probeModels(ids),
5103
5223
  rawDiagnostic: (region) => stacks[region].rawDiagnostic(),
5104
- creditsOfAccount: (region, accountId) => stacks[region].creditsOfAccount(accountId)
5224
+ creditsOfAccount: (region, accountId) => stacks[region].creditsOfAccount(accountId),
5225
+ liveLogin: (region) => liveLoginOf(region)
5105
5226
  }));
5106
5227
  /**
5107
5228
  * Live registration handles per region, filled once the shim is listening.
@@ -5157,13 +5278,59 @@ function apply(ctx, config) {
5157
5278
  }
5158
5279
  } catch {}
5159
5280
  })();
5281
+ let writeSettings;
5160
5282
  ctx.inject(["settings"], (settingsCtx) => {
5161
5283
  const settings = settingsCtx.settings;
5162
5284
  ctx.effect(() => settings.configure({ auto: true }, ctx.fiber));
5285
+ writeSettings = async (patch) => {
5286
+ const revision = () => settings.describe().find((entry) => entry.ns === settingsNs)?.revision;
5287
+ const conflict = (error) => typeof error === "object" && error !== null && error.code === "SETTINGS_CONFLICT";
5288
+ try {
5289
+ await settings.update(settingsNs, patch, revision());
5290
+ } catch (error) {
5291
+ if (!conflict(error)) throw error;
5292
+ await settings.update(settingsNs, patch, revision());
5293
+ }
5294
+ };
5295
+ ctx.effect(() => () => {
5296
+ writeSettings = void 0;
5297
+ });
5163
5298
  });
5164
5299
  ctx.on("loader/volatile-update", () => {
5165
5300
  applySelection(current());
5301
+ maybeFollowAppLogin();
5166
5302
  });
5303
+ /**
5304
+ * Follow the app's sign-in on every settings change when the switch is ON
5305
+ * (issue #29). Runs AFTER `applySelection` so this pass binds the previous
5306
+ * document, reads the app's live login against it, and — if they differ —
5307
+ * writes `accounts.<region>` once; that write emits `volatile-update` again,
5308
+ * which re-runs `applySelection` on the NEW document. The loop terminates on
5309
+ * its own the moment `liveLogin()` reports no mismatch (the second pass binds
5310
+ * the live account, so `preferred` and `current` agree). The guard below
5311
+ * additionally forbids two writes in flight, so a stuck mismatch can never
5312
+ * turn into a write storm: the next settings change retries from scratch.
5313
+ *
5314
+ * The switch reads FALSE for an existing config (see {@link Config}), so a
5315
+ * user who never opted in never gets a rebind — the card's one-click hint is
5316
+ * their path instead.
5317
+ */
5318
+ let followInFlight = false;
5319
+ const maybeFollowAppLogin = async () => {
5320
+ if (followInFlight || !followAppLoginEnabled(current())) return;
5321
+ followInFlight = true;
5322
+ try {
5323
+ for (const region of REGION_KEYS) {
5324
+ if (!regionEnabled(current(), region)) continue;
5325
+ await followAppLogin(region);
5326
+ }
5327
+ } catch (error) {
5328
+ ctx.logger.warn("dsh-connect-trae: follow-app-login rebind skipped", error);
5329
+ } finally {
5330
+ followInFlight = false;
5331
+ }
5332
+ };
5333
+ maybeFollowAppLogin();
5167
5334
  let stopped = false;
5168
5335
  ctx.effect(() => () => {
5169
5336
  stopped = true;
@@ -5245,4 +5412,4 @@ function apply(ctx, config) {
5245
5412
  });
5246
5413
  }
5247
5414
  //#endregion
5248
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
5415
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
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": "3.0.0",
5
+ "version": "3.0.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",