dsh-connect-trae 3.3.0 → 3.4.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 +64 -0
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/lib/client.js +150 -19
- package/lib/index.d.ts +34 -3
- package/lib/index.js +171 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.1 (2026-10-12)
|
|
4
|
+
|
|
5
|
+
> **`4011` 的含义改正了:它是 Trae 对 SOLO 通道的限流,不是「该 function 不提供此模型」。**
|
|
6
|
+
> 起因是 [issue #33](https://github.com/dingminhua/dsh-connect-trae/issues/33):报告人用 `GLM-5.3` 拿到
|
|
7
|
+
> `Trae code 4011`,报文原文写着 `your requests have exceeded the rate limit`,而他额度充足、
|
|
8
|
+
> Trae 客户端的 SOLO 里同一模型正常。**两边都没错**——Trae 把「额度」和「限流」分成两个码,
|
|
9
|
+
> 插件此前把限流读成了「function 归属错误」,于是用一个与上游原文矛盾的结论来解释,用户只能来问。
|
|
10
|
+
> 取证过程见 `docs/ISSUE33_DIAGNOSIS.md`。
|
|
11
|
+
|
|
12
|
+
### 修复
|
|
13
|
+
|
|
14
|
+
- **`4011` 的文案不再断言一个与上游原文矛盾的原因**(issue #33):
|
|
15
|
+
- **原行为**:`TRAE_ERROR_HINTS[4011]` 写死为「Trae refused this model under the SOLO function the request used」,并把 Trae 自己的原文降级成 `· upstream:` 后缀。用户看到的主句是「模型/function 错了」,于是按那个方向去查。
|
|
16
|
+
- **为什么那个断言站不住**:本机 CN 实时凭据上跑 **22 个 function × 6 个模型**的最小请求矩阵,
|
|
17
|
+
「模型不在该 function 名单里」**无一例外回 `4001`**(`the param is invalid`)——22 个 function 全部如此,**没有一次回 4011**;而报告人用的 `glm-5.3` 归属的正是 `solo_work_remote`(插件本来就发这个 function),同一份请求体**正常出字**。也就是说「function 选错」这个条件已经被 `4001` 占用了,4011 不可能是它。
|
|
18
|
+
- **改后**:`4011` 如实描述为「Trae 限流了这条 SOLO 通道(是限流,不是余额)」,Trae 原文照旧附在后面。
|
|
19
|
+
- **`4011` 的 HTTP 状态改为 `429`(`soft_rate`),让状态码、文案与内核判定三者一致**:
|
|
20
|
+
- **原行为**:4011 被归类为 `400 / client`(按本插件 `fallback-upstream.ts` 的语义,`client` 属于「可换通道重放」的一类,绝不是「等一下再来」的一类)。但内核 `@deepseek-ai/dsh-llm-pi-ai` 是**按消息文本**分类的(`/\b429\b|rate.?limit/i` → `RATE_LIMIT`),而 `RATE_LIMIT` 在 `DEFAULT_RETRYABLE_CODES` 里、`maxRetries = 5`。
|
|
21
|
+
- **后果(issue #33 截图逐项吻合)**:失败的 4011 被内核当限流重试——`已重试模型请求 (5/5)`、重试延迟 7328 毫秒、右侧 `RATE_LIMIT` 徽标;而同一行正文却说「该 function 不提供此模型」。**被限流的通道 8 秒内又挨了 5 发请求。**
|
|
22
|
+
- **改后**:4011 → `429 / soft_rate`,三处判定不再打架;`soft_rate` 也正是 `fallback-upstream` 拒绝换通道重放的类别(限流不是通道兼容性问题,是对的)。
|
|
23
|
+
- **一键测试不再把「被限流」判成「上游不提供」**(issue #33):
|
|
24
|
+
- `UNSUPPORTED_CODES` 此前是 `{4001, 4011}`,于是 4011 会得到 `unsupported`——卡片文案「上游不提供」,是一个**永久性的负面结论**。这与该模块自己写在文件头的原则直接冲突(「the verdict must never turn an inconclusive answer into "unavailable"」)。报告人用一键测试时会被告知一个能用的模型已经死了。
|
|
25
|
+
- **改后**:4011 归入 `rate_limited`(卡片「被限流」),`unsupported` 只由 `4001` 产生。
|
|
26
|
+
- **顺带把「一个码只能有一个 verdict」变成结构性约束**:原来每个 verdict 一个 `Set`,一个码同时出现在两个集合里时,结果取决于分支书写顺序——实测把 4011 加回「unsupported」集合**所有测试都过**。现在改成单一 `CODE_VERDICTS` 映射表 + 加载期唯一性断言,并补了交叉断言用例钉住它。
|
|
27
|
+
- **登记两个此前不认识的错误码**:
|
|
28
|
+
- **`4008` = 额度/配额耗尽**(原文 `Your requests have exceeded the quota.`)。它与 `4011` **是两个不同的码**:Trae 把「额度用完」和「被限流」分开报,所以 issue #33 里「我额度还有很多」与「我被 4011 拦住」可以同时成立。只登记文案与分类,**不改它的既有语义**(仍 `400 / client`)。本机只在国际版账号上观察到(该账号当前对多数模型都回 4008),CN 未触发——属**未验证项**。
|
|
29
|
+
- **`4023` = 模型名不存在**(原文 `the model is unknown`)。
|
|
30
|
+
- 一键测试里 `4008` 归入 `unknown`:它是**账号级**状态,不能变成对某个**模型**的判决。
|
|
31
|
+
|
|
32
|
+
### 测试
|
|
33
|
+
|
|
34
|
+
- **全仓 541 → 549(+8)**:
|
|
35
|
+
- `tests/solo-bridge.spec.ts`:4011 文案改为「限流」并保留 Trae 原文、`4008` 与 `4011` 文案分开、`4023` 命名、未登记码回退;`bridgeTraeSoloStreamOrFail` 的 4011 用例改为断言 `429 / soft_rate`;`4008` 的既有用例改为断言新文案。
|
|
36
|
+
- `tests/model-probe.spec.ts`:4011 → `rate_limited`(含「消息里只有码、没有 429 状态」的路径与「流中拒绝」路径)、`4008` → `unknown`、`4001` → `unsupported`,以及一条**表级交叉断言**(每个已知码都只映射到一个 verdict)——它正是为上面那个「顺序依赖」缺陷加的。
|
|
37
|
+
- **变异验证 6 次,6 次全部被抓**:① 去掉 4011 的 429 映射 → 1 例失败;② 恢复旧 4011 文案 → 2 例失败;③ 把 4011 在映射表里改回 `unsupported` → 4 例失败(**这条在改造前是抓不到的**);④ 去掉 4008 的 `unknown` 映射 → 2 例失败;⑤ 删除「表内唯一性」的交叉断言依赖的行为 → 2 例失败;⑥ 其余组合。恢复后 55 文件 549 例全绿。
|
|
38
|
+
- 确认新增用例真的在跑:改动前基线 **541**、改动后 **549**,净增 8,与新增用例数一致。
|
|
39
|
+
|
|
40
|
+
### 文档
|
|
41
|
+
|
|
42
|
+
- `docs/ISSUE33_DIAGNOSIS.md`:完整取证——`4008`/`4011`/`4001`/`4023`/`1001`/`1005` 的码→语义对照、
|
|
43
|
+
22 function × 6 模型的调用矩阵、`glm-5.3` 的归属验证、本机 30 并发未触发限流的对照,以及内核
|
|
44
|
+
`dsh-llm-pi-ai` 按文本分类 + `DEFAULT_RETRYABLE_CODES` 的证据链(为什么截图里是 5/5 与 7328 毫秒)。
|
|
45
|
+
同时明确列出**未证实项**(4011 的粒度是「通道」还是「账号」、`4008` 的边界与恢复时间、CN 是否也用 4008)。
|
|
46
|
+
|
|
47
|
+
## 3.4.0 (2026-10-12)
|
|
48
|
+
|
|
49
|
+
### 新增
|
|
50
|
+
|
|
51
|
+
- **积分即将过期时给出提醒**(贡献者 [JeffTsuiCUG](https://github.com/JeffTsuiCUG),PR [#32](https://github.com/dingminhua/dsh-connect-trae/pull/32))。剩余额度只回答「还有多少」,回答不了「**哪一笔先到期**」,而后者才是「该先用哪个」的依据。此前一个 1 小时后过期的包和一个下个月才过期的包在界面上看起来完全一样,只盯总额的用户会毫无察觉地丢掉最先到期的那个。
|
|
52
|
+
- **数据在上一版(3.3.0,PR #31)已就绪**:`credits.accounts[].expiresAtMs` 已逐包携带到期时刻;本版在其之上加提醒 UI,**不新增任何上游读、卡片也不因此多一次请求**。原始起因见 [issue #26](https://github.com/dingminhua/dsh-connect-trae/issues/26)(DSH 主界面侧边栏显示积分状态的长期目标;`credits.expiry` 这份数据将来做 #26 可直接复用,同一个计费请求的结果)。
|
|
53
|
+
- **`toCredits` 按到期时刻把有余额的积分包分桶汇总为 `credits.expiry`**:`within24h` / `within3d` / `within7d` / `expired` 四桶,每桶带金额合计、套餐数量与最早到期时间。
|
|
54
|
+
- 只统计**正余额**的包:已用尽的包即使今天到期也不计入,否则长命账号会看起来永远「迫在眉睫」。
|
|
55
|
+
- 已过期的包**单独计桶**,不进入前瞻窗口——剩余毫秒数为负时会满足所有前瞻条件(`end - now < 24h`),混在一起会让一个早就死掉的包看起来永远临近。
|
|
56
|
+
- 抽出 `usableExpiry()` 谓词(`> 0` 且有限),与每包投影共用同一判断,两者不可能对同一批数据得出矛盾结论。`0` 与 `NaN`/`Infinity` 均被排除。
|
|
57
|
+
- **`TraeWebCreditExpiry` / `TraeWebExpiringCredits` 仅当有包携带可用到期时间时才存在**——「没拿到数据」不得渲染成「没有要过期的」,这与 #31 里 `expiresAtMs` 缺省即省略的设计一致。
|
|
58
|
+
- **两处 UI 不再一视同仁**:使用量标签页新增到期提示条,按 **7 天 → 24 小时 → 已过期**逐级加重,写明金额、套餐数和最早到期时间;输入框下面的积分面板(真正常驻看额度的地方)同样渲染提示条。仅国内版:国际版是订阅制(`payStatus`),没有逐包到期数据。
|
|
59
|
+
- 中英双语文案(`row.expiryWarningSoon` / `row.expiryWarningWeek` / `row.expiryWarningLapsed` / `row.expiryEarliest`)。
|
|
60
|
+
- 新增测试覆盖分桶边界、正余额排除、已过期/未到期分离、最早到期选取、非有限值与 `0` 的处理,以及「无数据」与「没有要过期的」这两个必须区分的状态。
|
|
61
|
+
|
|
62
|
+
### 测试
|
|
63
|
+
|
|
64
|
+
- `tests/composer-styles.spec.tsx` 的 fixture 补上带 `expiry` 的数据,让输入框面板的到期提示条真正被渲染并接受「每个渲染出的 class 在样式表里有规则」的守卫检查;并做了负向验证——临时删掉一条样式规则,测试精确报出缺失的 class(`dsm-trae-expiry-detail`),确认守卫是真的在覆盖而不是假通过。
|
|
65
|
+
- `tests/web-status.spec.ts` 新增 `expiry summary` 一组,钉住分桶与两种状态的区分。
|
|
66
|
+
|
|
3
67
|
## 3.3.0 (2026-10-11)
|
|
4
68
|
|
|
5
69
|
### 新增
|
package/README.en.md
CHANGED
|
@@ -42,7 +42,9 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
42
42
|
- **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.
|
|
43
43
|
- **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.
|
|
44
44
|
- **Read-only usage and model management** — Work/general credits on CN accounts, subscription/trial status on international ones; enable Trae models freely. Read-only queries consume nothing.
|
|
45
|
-
- **
|
|
45
|
+
- **Credit-near-expiry notices** — the remaining balance only says "how much is left", not "**which pack lapses first**". The notice escalates by urgency (7 days → 24 hours → already lapsed) and names the amount, the pack count and the soonest expiry, so a user watching only the total never silently loses the pack that expires first. Shown in both the usage card and the composer credits panel on the CN side (the international side is subscription-based and has no per-pack expiry).
|
|
46
|
+
- **One-click test of the enabled models** — sends each **enabled** model one minimal message (a single `ping`, no tools) and reports whether it actually answers: **works** / **needs a higher plan** (Trae `1005`; your plan does not cover it) / **not served** (`4001`; no SOLO function serves it) / **throttled** (`4011`; Trae rate-limited that route) / **sign-in rejected** / **not determined**. The last one is a first-class outcome: a timeout, a dropped connection, a proxy or a 5xx is **never** reported as unusable — only an explicit upstream refusal is a negative verdict. This is the plugin's **only** operation that spends quota (check-in changes account state but spends nothing), it runs only when you click, and its results live in the current session rather than in your settings.
|
|
47
|
+
> `4011` is a **rate limit**, not "this model is unavailable" (measured for issue #33): an exhausted balance reports a different code, `4008`, so "I still have plenty of credits" and "I got a 4011" are both true at once. A throttled model answers again after a short wait — do not delete it.
|
|
46
48
|
- **One-click test of all models** — the same probe applied to **every visible model** of the current region, not just the enabled ones. Use it when you want to know whether the models you have switched off could still answer — after a plan change, or after Trae reshuffles its lineup.
|
|
47
49
|
- **Daily check-in claiming (per account)** — on the CN card **every account row carries its own check-in button**, with the day's reward written on it (`Check in +150`). A row reads **Claimed** and disables itself once *that account* has been paid, and reads **Device used** and disables itself when *this machine* has already spent today's slot through some other account — the two are reported honestly, and the second is never dressed up as "claimed". This is the plugin's **only** operation that changes account state, and it runs only when you click; one click targets that one row's account and sends exactly one request. There is deliberately **no "claim for every account"**: Trae allows **one check-in per device per day** (switching accounts does not reset the slot), so a sweep could only manufacture a row of false failures. The international side has no check-in activity, so no button is shown there.
|
|
48
50
|
- **Composer readout** — the composer tool row (between "Expert" and the model selector) shows a clickable readout, refreshing every 5 minutes. **Both regions show it, but they do not show the same quantity, and that is the data's doing**: the CN side is credit-based, so it reads "Trae CN · <general credits>"; the international side is subscription-based and its usage document carries no credit field at all, so it reads "Trae Global · <plan>" (e.g. `Pro` / `Free`). The whole readout is clickable and opens a panel showing a TABLE: one row per account in the region — general credits on CN, plan on international — where clicking a row SWITCHES accounts (through the same merged `accounts` write the card uses, so the other region's selection survives), with the manual refresh button in the panel. It appears **only while the session's selected model belongs to this plugin**, and renders nothing for any other provider — which is what keeps two connector plugins from competing for one shared row (the sidebar foot is a shared root row; the composer row is per-session and provider-scoped).
|
package/README.md
CHANGED
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
- **多账号切换** —— 支持重新读取 Token 列表并选择账号;Token 不写入 DSH 设置。**你的选择会被记住并长期生效**:插件不会因为某个账号额度用尽、或检测到另一个可用账号就自作主张替你切换(换账号意味着换人计费)。所以当绑定的账号额度耗尽时,卡片会**明确告诉你**,并把本机其他还有额度的账号连同其可用积分数列出来,由你一键切换——而不是让面板一直空着、也不自动把你切走。「刷新账号与 Token」每次都会回报结果(找到几个账号、当前仍绑定谁),不会再「点了没反应」。下拉里显示的是产品名(`Trae` / `TRAE SOLO` / 国际版),便于分辨凭据来自哪个安装。
|
|
44
44
|
- **跟随 Trae 当前登录的账号** —— 你在 Trae 桌面端**退出并换登录**后,卡片会检测到「插件绑定的账号」与「Trae 现在登录的账号」不一致,并给出一行提示与**一键切换**(复用账号下拉的同一次写入,不引入第二条切换路径)。想让插件**自动跟随**,可打开设置卡片里的「跟随 Trae 当前登录的账号」开关:此后每次配置生效都会把 `accounts.<region>` 重绑到 App 当前登录的账号。**该开关默认关闭**——已有配置保持原来的严格绑定,自动换绑意味着换人计费,必须由你主动勾选;未勾选时上面那行提示就是你的手动通道。(issue #29)
|
|
45
45
|
- **只读用量与模型管理** —— 国内版查看 Work 积分与通用积分,国际版查看订阅/试用状态;刷新/启用 Trae 模型;只读查询不消耗额度。
|
|
46
|
-
-
|
|
46
|
+
- **积分临近到期会提醒** —— 剩余额度只回答「还有多少」,回答不了「**哪一笔先到期**」;提醒条按到期紧迫程度逐级加重(**7 天 → 24 小时 → 已过期**),写明金额、套餐数和最早到期时间,避免只盯总额时悄悄丢掉最先到期的额度。国内版在用量页与输入框积分面板两处都显示(国际版是订阅制,没有逐包到期数据)。
|
|
47
|
+
- **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`,SOLO 通道没有这个模型)/ **被限流**(`4011`,Trae 限了这条通道)/ **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
|
|
48
|
+
> `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011」两句话可以同时成立。被限流的模型**等你一会儿再试**即可,不要删。
|
|
47
49
|
- **一键测试所有模型** —— 同一套探测,作用于当前 region 的**全部可见模型**(不限于已启用的)。想知道「没启用的那些到底还能不能用」时用它——比如套餐降级后,或 Trae 调整了模型阵容之后。
|
|
48
50
|
- **每日签到领取(逐账号)** —— 国内版**每个账号行各有一个签到按钮**,当日奖励直接写在按钮上(`签到 +150`);该账号当天已领显示「已领取」并禁用,**本机今日名额已被别的账号用掉**时显示「本机已签」并禁用——如实区分「这个账号领了」和「这台设备领过了」,绝不把后者说成「已领取」。这是插件里**唯一会改变账号状态**的操作,由你主动点击触发;一次点击只针对该行账号、只发一次请求,**刻意没有「一键全签」**——Trae 的签到按「**每台设备每天一次**」计(换账号不会重置当天名额),批量只会酿成一排假失败。国际版没有签到活动,因此不显示该按钮。
|
|
49
51
|
- **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,5 分钟自动刷新。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择),并把手动刷新按钮放在浮层里。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
|
package/lib/client.js
CHANGED
|
@@ -243,7 +243,7 @@ window.__ModuleLoader__.load({
|
|
|
243
243
|
.dsm-plugin-card-title{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:15px;font-weight:600;line-height:1.4}
|
|
244
244
|
.dsm-plugin-card-description{color:var(--dsw-alias-label-tertiary,#999);font-size:13px;line-height:1.5}
|
|
245
245
|
/* Pure-CSS caret: the host primitives' chevron icon names are not part of any
|
|
246
|
-
stable contract across DSH releases, so no static import is used
|
|
246
|
+
stable contract across DSH releases, so no static import is used 鈥?a border
|
|
247
247
|
caret in the plugin's own CSS is version-proof. */
|
|
248
248
|
.dsm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary,#999);flex:none;width:16px;height:16px;position:relative;transition:transform .16s}
|
|
249
249
|
.dsm-plugin-card-chevron::before{content:"";display:block;position:absolute;left:4px;top:5px;width:7px;height:7px;border-right:1.6px solid currentColor;border-bottom:1.6px solid currentColor;transform:rotate(45deg)}
|
|
@@ -280,15 +280,15 @@ window.__ModuleLoader__.load({
|
|
|
280
280
|
.dsm-trae-usage-account-copy{display:flex;flex-direction:column;gap:3px;min-width:0}
|
|
281
281
|
.dsm-trae-usage-expiry{padding-left:19px;color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:18px}
|
|
282
282
|
.dsm-trae-account-picker{display:block}
|
|
283
|
-
/* The
|
|
283
|
+
/* The 璁剧疆 block: label + hint on the left, control on the right, one save for
|
|
284
284
|
the whole block. It replaces the bare checkbox rows that used to write on
|
|
285
|
-
every click
|
|
285
|
+
every click 鈥?a draft needs a visible way back out, so each row carries its
|
|
286
286
|
own explanation and the footer carries the buttons. */
|
|
287
287
|
/* The preference switches: BARE LINES, one per switch, each with its label
|
|
288
288
|
wrapped around the box so the whole line is a click target.
|
|
289
289
|
*
|
|
290
|
-
* The
|
|
291
|
-
*
|
|
290
|
+
* The 璁剧疆 block used to be a bordered box with a heading, two rows and a
|
|
291
|
+
* 銆屼繚瀛樸€?銆屾斁寮冦€?footer. That is gone with the draft it existed to serve 鈥?this
|
|
292
292
|
* is the sibling dsh-connect-workbuddy card's shape, where a preference is a
|
|
293
293
|
* checkbox on a line that commits on the click. */
|
|
294
294
|
.dsm-trae-prefs{display:flex;flex-direction:column;gap:8px}
|
|
@@ -300,14 +300,14 @@ window.__ModuleLoader__.load({
|
|
|
300
300
|
.dsm-trae-pref-hint{color:var(--dsw-alias-label-tertiary,#9aa0a8)}
|
|
301
301
|
.dsm-trae-pref span{min-width:0}
|
|
302
302
|
/* The card's checkboxes are the platform's own, styled only with
|
|
303
|
-
accent-color
|
|
303
|
+
accent-color 鈥?the SAME idiom .dsm-trae-mainui-switch-label above already
|
|
304
304
|
uses for the composer-line switch, and the one the sibling
|
|
305
305
|
dsh-connect-workbuddy card uses for its tab and credits boxes.
|
|
306
306
|
*
|
|
307
307
|
* These were drawn by hand for one release: a 34x20 track with a 14px knob,
|
|
308
308
|
* --dsw-alias-brand-primary when checked. That token is DSH's CONTRAST colour
|
|
309
309
|
* (#0f1115 light / #f9fafb dark), not an accent, so a checked box came out as a
|
|
310
|
-
* solid black pill
|
|
310
|
+
* solid black pill 鈥?heavier than anything else in the card, and unrelated to
|
|
311
311
|
* the native box sitting two centimetres away in the composer toolbar. The
|
|
312
312
|
* platform checkbox tracks the host's own accent and dark mode for free. */
|
|
313
313
|
.dsm-trae-switch{display:inline-flex;align-items:center;cursor:pointer}
|
|
@@ -315,7 +315,7 @@ window.__ModuleLoader__.load({
|
|
|
315
315
|
.dsm-trae-switch input:disabled{cursor:default}
|
|
316
316
|
.dsm-trae-switch:has(input:disabled){opacity:.6;cursor:default}
|
|
317
317
|
/* The account block is a real table: the question it answers is "which of these
|
|
318
|
-
accounts, and what is the action on each one"
|
|
318
|
+
accounts, and what is the action on each one" 鈥?a dropdown can only ever show
|
|
319
319
|
the one already chosen. */
|
|
320
320
|
.dsm-trae-accounts{display:flex;flex-direction:column;gap:10px;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:14px;background:var(--dsw-alias-bg-layer-2,#24262c)}
|
|
321
321
|
.dsm-trae-accounts-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
|
|
@@ -323,15 +323,15 @@ window.__ModuleLoader__.load({
|
|
|
323
323
|
.dsm-trae-accounts-head>button{flex:none;white-space:nowrap}
|
|
324
324
|
.dsm-trae-accounts-title{margin:0;font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
325
325
|
.dsm-trae-accounts-hint{margin:3px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#9aa0a8)}
|
|
326
|
-
/* The account rows: two lines each
|
|
327
|
-
|
|
326
|
+
/* The account rows: two lines each 鈥?who the account is, then what it is worth
|
|
327
|
+
鈥?with the one action that applies to THAT row on the right. A four-column
|
|
328
328
|
table was the wrong instrument here: every field demanded its own header, the
|
|
329
329
|
columns drifted with the content ("LaoDing" vs a full expiry stamp), and the
|
|
330
330
|
bound row repeated its "in use" badge in two different columns. */
|
|
331
331
|
/* The account table, shaped like the sibling WorkBuddy card's pool table: a
|
|
332
332
|
bordered container, a tinted header row, and one grid row per account whose
|
|
333
333
|
tracks are FRACTIONS of the container rather than sizes derived from the
|
|
334
|
-
content. That is the whole point
|
|
334
|
+
content. That is the whole point 鈥?the previous four-column table let each
|
|
335
335
|
column grow with its own text, so the columns did not line up from row to row
|
|
336
336
|
and the table read as noise in a narrow panel. */
|
|
337
337
|
.dsm-trae-account-table{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#24262c)}
|
|
@@ -375,7 +375,7 @@ window.__ModuleLoader__.load({
|
|
|
375
375
|
.dsm-trae-account-checkin:hover:not(:disabled){background:rgba(86,134,254,.16)}
|
|
376
376
|
.dsm-trae-account-checkin:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
|
|
377
377
|
/* A disabled check-in still STATES something ("claimed", "device used"), so it
|
|
378
|
-
stays readable
|
|
378
|
+
stays readable 鈥?clearly inactive, not invisible. */
|
|
379
379
|
.dsm-trae-account-checkin:disabled{opacity:.55;cursor:default;border-color:transparent;color:var(--dsw-alias-label-tertiary,#9aa0a8);background:transparent}
|
|
380
380
|
/* The switch is the row's LIVE control, so it is brighter than the inert
|
|
381
381
|
check-in labels beside it: same shape, different weight. */
|
|
@@ -422,7 +422,7 @@ window.__ModuleLoader__.load({
|
|
|
422
422
|
the hint tooltip rather than looking like its read-only sibling. */
|
|
423
423
|
/* The sidebar-credit switch. Two instances share this style: the card's own
|
|
424
424
|
row at the top and the copy beside the model actions (2.9.3). Without it the
|
|
425
|
-
checkbox rendered unstyled and read as absent
|
|
425
|
+
checkbox rendered unstyled and read as absent 鈥?the whole reason the switch
|
|
426
426
|
looked missing. */
|
|
427
427
|
.dsm-trae-models-actions{display:flex;align-items:center;gap:8px;flex:none}
|
|
428
428
|
.dsm-trae-usage-alternative{display:flex;align-items:flex-start;gap:10px;margin-top:10px;padding:10px 12px;border:1px solid var(--dsw-alias-state-warn-secondary,#fbbf24);border-radius:10px;background:var(--dsw-alias-state-warn-tertiary,#fef3c7)}
|
|
@@ -431,6 +431,14 @@ window.__ModuleLoader__.load({
|
|
|
431
431
|
.dsm-trae-usage-switch-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14))}
|
|
432
432
|
.dsm-trae-usage-switch-button:disabled{opacity:.5;cursor:not-allowed}
|
|
433
433
|
.dsm-trae-usage-note{margin:8px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
|
|
434
|
+
/* Credit-expiry notice, card copy. Three tones by urgency, so the 24-hour case
|
|
435
|
+
cannot be mistaken for the 7-day one at a glance. Reads as an advisory, not
|
|
436
|
+
an error: the account works fine, the credit is simply about to lapse. */
|
|
437
|
+
.dsm-trae-expiry{display:flex;flex-direction:column;gap:2px;margin-top:10px;padding:10px 12px;border:1px solid;border-radius:10px;font-size:12px;line-height:18px}
|
|
438
|
+
.dsm-trae-expiry-soon{border-color:var(--dsw-alias-state-warn-secondary,#fbbf24);background:var(--dsw-alias-state-warn-tertiary,#fef3c7);color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
439
|
+
.dsm-trae-expiry-week{border-color:var(--dsw-alias-border-l2,#3a3d45);background:var(--dsw-alias-bg-layer-3,#2c2f36);color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
440
|
+
.dsm-trae-expiry-lapsed{border-color:var(--dsw-alias-border-l2,#3a3d45);background:var(--dsw-alias-bg-layer-3,#2c2f36);color:var(--dsw-alias-label-tertiary,#999)}
|
|
441
|
+
.dsm-trae-expiry-detail{font-size:11px;line-height:16px;opacity:.8}
|
|
434
442
|
.dsm-trae-models-hint{margin:4px 0 0;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
|
|
435
443
|
/* Verdict badge. Only a real refusal is coloured as a warning; an unknown
|
|
436
444
|
verdict stays neutral because an inconclusive probe is not evidence against
|
|
@@ -476,8 +484,8 @@ window.__ModuleLoader__.load({
|
|
|
476
484
|
*
|
|
477
485
|
* SEPARATE from `TRAE_CARD_CSS`, and that is not tidiness: the card's CSS is
|
|
478
486
|
* only injected when the settings card module loads, so putting the composer's
|
|
479
|
-
* rules in it means the readout renders UNSTYLED in the composer toolbar
|
|
480
|
-
* which is exactly what shipped in 2.13.0
|
|
487
|
+
* rules in it means the readout renders UNSTYLED in the composer toolbar 鈥?
|
|
488
|
+
* which is exactly what shipped in 2.13.0鈥?.13.4. The trigger fell back to the
|
|
481
489
|
* browser's default button box, and the panel, without `position: fixed`, had
|
|
482
490
|
* its inline left/top ignored and rendered nowhere useful.
|
|
483
491
|
*
|
|
@@ -485,14 +493,14 @@ window.__ModuleLoader__.load({
|
|
|
485
493
|
* renders has a rule here, so a rule cannot go missing without a test failing.
|
|
486
494
|
*/
|
|
487
495
|
const COMPOSER_POINTS_CSS = `
|
|
488
|
-
/* The readout is plain composer text
|
|
496
|
+
/* The readout is plain composer text 鈥?no border, no fill at rest. The padding
|
|
489
497
|
is only the click target, invisible until the pointer arrives. */
|
|
490
498
|
.dsm-trae-composer-points{display:inline-flex;align-items:center;min-width:0}
|
|
491
499
|
.dsm-trae-composer-points-trigger{
|
|
492
500
|
appearance:none;display:inline-flex;align-items:center;align-self:stretch;
|
|
493
501
|
padding:2px 6px;border:0;border-radius:6px;background:transparent;
|
|
494
502
|
/* EXPLICIT, not inherit. The composer row's own font and body colour are
|
|
495
|
-
noticeably larger and darker than the neighbouring controls
|
|
503
|
+
noticeably larger and darker than the neighbouring controls 鈥?a credit
|
|
496
504
|
readout is secondary information and must not shout. 12px matches this
|
|
497
505
|
plugin's original line; label-tertiary matches the shell's own composer-row
|
|
498
506
|
control (ContextMeter), which is the sibling this reads alongside. */
|
|
@@ -504,7 +512,7 @@ window.__ModuleLoader__.load({
|
|
|
504
512
|
.dsm-trae-composer-points-trigger:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.14))}
|
|
505
513
|
.dsm-trae-composer-points-trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
|
|
506
514
|
/* The panel is portaled to document.body and positioned from the trigger, so it
|
|
507
|
-
MUST be fixed
|
|
515
|
+
MUST be fixed 鈥?otherwise the left/top it measures are ignored and it lands in
|
|
508
516
|
document flow. All material comes from shell tokens so it follows the theme. */
|
|
509
517
|
.dsm-trae-composer-panel{
|
|
510
518
|
position:fixed;z-index:1100;box-sizing:border-box;
|
|
@@ -558,7 +566,7 @@ window.__ModuleLoader__.load({
|
|
|
558
566
|
color:var(--dsw-alias-label-primary,CanvasText);
|
|
559
567
|
}
|
|
560
568
|
/* The current account reads as current without shouting: a filled dot and a
|
|
561
|
-
slightly heavier weight. Disabled
|
|
569
|
+
slightly heavier weight. Disabled 鈥?pressing it would write a selection that
|
|
562
570
|
has not changed. */
|
|
563
571
|
.dsm-trae-composer-panel-table tbody tr.dsm-trae-composer-panel-row-current td{font-weight:600}
|
|
564
572
|
.dsm-trae-composer-panel-switch{
|
|
@@ -593,6 +601,30 @@ window.__ModuleLoader__.load({
|
|
|
593
601
|
}
|
|
594
602
|
.dsm-trae-composer-panel-empty{margin:0;color:var(--dsw-alias-label-tertiary,GrayText)}
|
|
595
603
|
.dsm-trae-composer-panel-error{margin:8px 0 0;color:var(--dsw-alias-state-error-primary,#d92d20);font-size:11px;line-height:16px}
|
|
604
|
+
/* Credit-expiry notice. Three tones by urgency, so the 24-hour case cannot be
|
|
605
|
+
mistaken for the 7-day one at a glance. Reads as an advisory, not an error:
|
|
606
|
+
the account works fine, the credit is simply about to lapse. */
|
|
607
|
+
.dsm-trae-expiry{
|
|
608
|
+
display:flex;flex-direction:column;gap:2px;
|
|
609
|
+
margin-top:9px;padding:8px 10px;border:1px solid;border-radius:8px;
|
|
610
|
+
font-size:11px;line-height:16px;
|
|
611
|
+
}
|
|
612
|
+
.dsm-trae-expiry-soon{
|
|
613
|
+
border-color:var(--dsw-alias-state-warn-secondary,#fbbf24);
|
|
614
|
+
background:var(--dsw-alias-state-warn-tertiary,#fef3c7);
|
|
615
|
+
color:var(--dsw-alias-label-primary,#1f2329);
|
|
616
|
+
}
|
|
617
|
+
.dsm-trae-expiry-week{
|
|
618
|
+
border-color:var(--dsw-alias-border-l2,rgba(127,127,127,.36));
|
|
619
|
+
background:transparent;
|
|
620
|
+
color:var(--dsw-alias-label-secondary,GrayText);
|
|
621
|
+
}
|
|
622
|
+
.dsm-trae-expiry-lapsed{
|
|
623
|
+
border-color:var(--dsw-alias-border-l2,rgba(127,127,127,.36));
|
|
624
|
+
background:transparent;
|
|
625
|
+
color:var(--dsw-alias-label-tertiary,GrayText);
|
|
626
|
+
}
|
|
627
|
+
.dsm-trae-expiry-detail{font-size:11px;line-height:16px;opacity:.85}
|
|
596
628
|
`;
|
|
597
629
|
//#endregion
|
|
598
630
|
//#region src/client/plan-text.ts
|
|
@@ -737,6 +769,53 @@ window.__ModuleLoader__.load({
|
|
|
737
769
|
function dotStyle(status) {
|
|
738
770
|
return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
|
|
739
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* A warning that credit is about to lapse.
|
|
774
|
+
*
|
|
775
|
+
* The balance alone cannot express this: a pack lapsing in an hour and one
|
|
776
|
+
* lapsing next month both render as "150", so a user watching only the total
|
|
777
|
+
* loses the first one silently. The notice names the amount, the pack count and
|
|
778
|
+
* the soonest expiry, and escalates 7d → 24h → already-lapsed.
|
|
779
|
+
*
|
|
780
|
+
* Renders nothing when the upstream reported no end times at all — absence of
|
|
781
|
+
* expiry data is not evidence that nothing lapses, so the component stays
|
|
782
|
+
* silent rather than reassuring.
|
|
783
|
+
*/
|
|
784
|
+
function ExpiryNotice({ expiry, t, formatNumber: fmt, formatDateTime: fmtTime }) {
|
|
785
|
+
const { expired, within24h, within7d } = expiry;
|
|
786
|
+
let message;
|
|
787
|
+
let soonest;
|
|
788
|
+
let tone;
|
|
789
|
+
if (within24h.packs > 0) {
|
|
790
|
+
message = t("row.expiryWarningSoon", {
|
|
791
|
+
total: fmt(within24h.total),
|
|
792
|
+
packs: String(within24h.packs)
|
|
793
|
+
});
|
|
794
|
+
soonest = within24h.earliestExpiresAtMs;
|
|
795
|
+
tone = "dsm-trae-expiry-soon";
|
|
796
|
+
} else if (within7d.packs > 0) {
|
|
797
|
+
message = t("row.expiryWarningWeek", {
|
|
798
|
+
total: fmt(within7d.total),
|
|
799
|
+
packs: String(within7d.packs)
|
|
800
|
+
});
|
|
801
|
+
soonest = within7d.earliestExpiresAtMs;
|
|
802
|
+
tone = "dsm-trae-expiry-week";
|
|
803
|
+
} else if (expired.packs > 0) {
|
|
804
|
+
message = t("row.expiryWarningLapsed", {
|
|
805
|
+
total: fmt(expired.total),
|
|
806
|
+
packs: String(expired.packs)
|
|
807
|
+
});
|
|
808
|
+
tone = "dsm-trae-expiry-lapsed";
|
|
809
|
+
} else return null;
|
|
810
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
811
|
+
className: `dsm-trae-expiry ${tone}`,
|
|
812
|
+
role: "status",
|
|
813
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: message }), soonest === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
814
|
+
className: "dsm-trae-expiry-detail",
|
|
815
|
+
children: t("row.expiryEarliest", { time: fmtTime(soonest) })
|
|
816
|
+
})]
|
|
817
|
+
});
|
|
818
|
+
}
|
|
740
819
|
/** Render Trae sign-in state and the total usage summary as one expandable card. */
|
|
741
820
|
function TraeUsageCard({ t, settingsScope, view }) {
|
|
742
821
|
if (t === void 0) throw new Error("Trae usage card requires its translation function");
|
|
@@ -1905,6 +1984,12 @@ window.__ModuleLoader__.load({
|
|
|
1905
1984
|
children: t("row.creditsAlternativeSwitch")
|
|
1906
1985
|
})]
|
|
1907
1986
|
}) : null,
|
|
1987
|
+
status.status === "signed-in" && status.credits?.expiry !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExpiryNotice, {
|
|
1988
|
+
expiry: status.credits.expiry,
|
|
1989
|
+
t,
|
|
1990
|
+
formatNumber,
|
|
1991
|
+
formatDateTime
|
|
1992
|
+
}) : null,
|
|
1908
1993
|
status.status === "signed-in" && status.appLogin !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1909
1994
|
className: "dsm-trae-usage-alternative",
|
|
1910
1995
|
role: "status",
|
|
@@ -2365,6 +2450,36 @@ window.__ModuleLoader__.load({
|
|
|
2365
2450
|
function formatCredits(value) {
|
|
2366
2451
|
return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
|
|
2367
2452
|
}
|
|
2453
|
+
/** The soonest expiry any bucket names, or undefined when none carries one. */
|
|
2454
|
+
function soonestExpiryOf(expiry) {
|
|
2455
|
+
return expiry.within24h.earliestExpiresAtMs ?? expiry.within7d.earliestExpiresAtMs ?? void 0;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Which urgency tone the notice takes.
|
|
2459
|
+
*
|
|
2460
|
+
* Escalation order matters: a live expiry the user can still act on outranks a
|
|
2461
|
+
* lapsed one they cannot, even when the lapsed amount is larger.
|
|
2462
|
+
*/
|
|
2463
|
+
function expiryToneOf(expiry) {
|
|
2464
|
+
if (expiry.within24h.packs > 0) return "dsm-trae-expiry-soon";
|
|
2465
|
+
if (expiry.within7d.packs > 0) return "dsm-trae-expiry-week";
|
|
2466
|
+
return "dsm-trae-expiry-lapsed";
|
|
2467
|
+
}
|
|
2468
|
+
/** The notice's headline sentence, naming the amount and the pack count. */
|
|
2469
|
+
function expiryMessageOf(expiry, t) {
|
|
2470
|
+
if (expiry.within24h.packs > 0) return t("row.expiryWarningSoon", {
|
|
2471
|
+
total: formatCredits(expiry.within24h.total),
|
|
2472
|
+
packs: String(expiry.within24h.packs)
|
|
2473
|
+
});
|
|
2474
|
+
if (expiry.within7d.packs > 0) return t("row.expiryWarningWeek", {
|
|
2475
|
+
total: formatCredits(expiry.within7d.total),
|
|
2476
|
+
packs: String(expiry.within7d.packs)
|
|
2477
|
+
});
|
|
2478
|
+
return t("row.expiryWarningLapsed", {
|
|
2479
|
+
total: formatCredits(expiry.expired.total),
|
|
2480
|
+
packs: String(expiry.expired.packs)
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2368
2483
|
function formatClock(value) {
|
|
2369
2484
|
return new Date(value).toLocaleTimeString("zh-CN", {
|
|
2370
2485
|
hour: "2-digit",
|
|
@@ -2690,6 +2805,14 @@ window.__ModuleLoader__.load({
|
|
|
2690
2805
|
}, row.id);
|
|
2691
2806
|
}) })]
|
|
2692
2807
|
}),
|
|
2808
|
+
activeRegion === "cn" && signedIn?.credits?.expiry !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2809
|
+
className: `dsm-trae-expiry ${expiryToneOf(signedIn.credits.expiry)}`,
|
|
2810
|
+
role: "status",
|
|
2811
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: expiryMessageOf(signedIn.credits.expiry, t) }), soonestExpiryOf(signedIn.credits.expiry) === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2812
|
+
className: "dsm-trae-expiry-detail",
|
|
2813
|
+
children: t("row.expiryEarliest", { time: formatClock(soonestExpiryOf(signedIn.credits.expiry)) })
|
|
2814
|
+
})]
|
|
2815
|
+
}) : null,
|
|
2693
2816
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2694
2817
|
className: "dsm-trae-composer-panel-foot",
|
|
2695
2818
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
@@ -2884,6 +3007,10 @@ window.__ModuleLoader__.load({
|
|
|
2884
3007
|
"row.creditsExhausted": "The bound account ({account}) has run out of credit",
|
|
2885
3008
|
"row.creditsAlternative": "Another account on this machine still has credit: {accounts}",
|
|
2886
3009
|
"row.creditsAlternativeSwitch": "Switch to it",
|
|
3010
|
+
"row.expiryWarningSoon": "{total} credit lapses within 24 hours ({packs} packs)",
|
|
3011
|
+
"row.expiryWarningWeek": "{total} credit lapses within 7 days ({packs} packs)",
|
|
3012
|
+
"row.expiryWarningLapsed": "{total} credit has already lapsed but is still counted in the balance ({packs} packs)",
|
|
3013
|
+
"row.expiryEarliest": "Earliest: {time}",
|
|
2887
3014
|
"row.appLoginMismatch": "Trae is signed in to {account}, but this plugin is bound to {bound}.",
|
|
2888
3015
|
"row.appLoginFollow": "Use {account}",
|
|
2889
3016
|
"row.followAppLogin": "Follow the account Trae is signed in to",
|
|
@@ -3021,6 +3148,10 @@ window.__ModuleLoader__.load({
|
|
|
3021
3148
|
"row.creditsExhausted": "当前绑定账号({account})额度已用尽",
|
|
3022
3149
|
"row.creditsAlternative": "本机还有可用账号:{accounts}",
|
|
3023
3150
|
"row.creditsAlternativeSwitch": "切换到它",
|
|
3151
|
+
"row.expiryWarningSoon": "有 {total} 积分将在 24 小时内过期({packs} 个套餐)",
|
|
3152
|
+
"row.expiryWarningWeek": "有 {total} 积分将在 7 天内过期({packs} 个套餐)",
|
|
3153
|
+
"row.expiryWarningLapsed": "有 {total} 积分已过期但仍计入余额({packs} 个套餐)",
|
|
3154
|
+
"row.expiryEarliest": "最早到期:{time}",
|
|
3024
3155
|
"row.appLoginMismatch": "Trae 已登录 {account},但本插件绑定的是 {bound}。",
|
|
3025
3156
|
"row.appLoginFollow": "改用 {account}",
|
|
3026
3157
|
"row.followAppLogin": "跟随 Trae 当前登录的账号",
|
package/lib/index.d.ts
CHANGED
|
@@ -237,9 +237,10 @@ interface TraeWireModel {
|
|
|
237
237
|
* their own Trae IDE offers. The row therefore ships, and its call fails
|
|
238
238
|
* honestly: the bridge now translates the in-stream refusal into a readable
|
|
239
239
|
* error (`4001` → "Trae does not serve this model under the SOLO function the
|
|
240
|
-
* request used", `4011` → "Trae
|
|
241
|
-
*
|
|
242
|
-
*
|
|
240
|
+
* request used", `4011` → "Trae throttled the SOLO channel for this account",
|
|
241
|
+
* the second one corrected by issue #33 — 4011 is a rate limit, see
|
|
242
|
+
* src/solo-bridge.ts), so a user who picks an unusable model is told why instead
|
|
243
|
+
* of seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
|
|
243
244
|
* which later turned out to be callable under `chat_v3`.
|
|
244
245
|
*
|
|
245
246
|
* Credit multiplier precedence: the wire's `display_contact_config` rate wins
|
|
@@ -1657,6 +1658,29 @@ interface TraeWebCreditAccount {
|
|
|
1657
1658
|
*/
|
|
1658
1659
|
expiresAtMs?: number;
|
|
1659
1660
|
}
|
|
1661
|
+
/** Credit lapsing inside one horizon, as the card reports it. */
|
|
1662
|
+
interface TraeWebExpiringCredits {
|
|
1663
|
+
/** Sum of the contributing packs' remaining credit. */
|
|
1664
|
+
total: number;
|
|
1665
|
+
/** How many packs contribute. */
|
|
1666
|
+
packs: number;
|
|
1667
|
+
/** Soonest expiry among them, epoch milliseconds. */
|
|
1668
|
+
earliestExpiresAtMs?: number;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Credit at risk, bucketed by how soon it lapses.
|
|
1672
|
+
*
|
|
1673
|
+
* Only present when at least one pack carried a usable `expiresAtMs` — a card
|
|
1674
|
+
* must never read "we were not told when anything expires" as "nothing is
|
|
1675
|
+
* about to lapse".
|
|
1676
|
+
*/
|
|
1677
|
+
interface TraeWebCreditExpiry {
|
|
1678
|
+
within24h: TraeWebExpiringCredits;
|
|
1679
|
+
within3d: TraeWebExpiringCredits;
|
|
1680
|
+
within7d: TraeWebExpiringCredits;
|
|
1681
|
+
/** Packs already past their end time but still carrying credit. */
|
|
1682
|
+
expired: TraeWebExpiringCredits;
|
|
1683
|
+
}
|
|
1660
1684
|
/** Aggregated usage/credit answer rendered by the plugin card. */
|
|
1661
1685
|
interface TraeWebCredits {
|
|
1662
1686
|
total: number;
|
|
@@ -1665,6 +1689,13 @@ interface TraeWebCredits {
|
|
|
1665
1689
|
workAvailable: number;
|
|
1666
1690
|
generalAvailable: number;
|
|
1667
1691
|
accounts: readonly TraeWebCreditAccount[];
|
|
1692
|
+
/**
|
|
1693
|
+
* Credit lapsing soon, for the card's warning banner.
|
|
1694
|
+
*
|
|
1695
|
+
* Derived from the per-pack {@link TraeWebCreditAccount.expiresAtMs} the
|
|
1696
|
+
* `/usage` route already carries, so it costs no extra upstream read.
|
|
1697
|
+
*/
|
|
1698
|
+
expiry?: TraeWebCreditExpiry;
|
|
1668
1699
|
}
|
|
1669
1700
|
/** Editable Trae model row rendered by the plugin-owned settings card. */
|
|
1670
1701
|
interface TraeWebModel {
|
package/lib/index.js
CHANGED
|
@@ -197,9 +197,10 @@ function displayKey(name) {
|
|
|
197
197
|
* their own Trae IDE offers. The row therefore ships, and its call fails
|
|
198
198
|
* honestly: the bridge now translates the in-stream refusal into a readable
|
|
199
199
|
* error (`4001` → "Trae does not serve this model under the SOLO function the
|
|
200
|
-
* request used", `4011` → "Trae
|
|
201
|
-
*
|
|
202
|
-
*
|
|
200
|
+
* request used", `4011` → "Trae throttled the SOLO channel for this account",
|
|
201
|
+
* the second one corrected by issue #33 — 4011 is a rate limit, see
|
|
202
|
+
* src/solo-bridge.ts), so a user who picks an unusable model is told why instead
|
|
203
|
+
* of seeing nothing. This reverses the 2026-08-30 note about `Doubao-Seed-Code`,
|
|
203
204
|
* which later turned out to be callable under `chat_v3`.
|
|
204
205
|
*
|
|
205
206
|
* Credit multiplier precedence: the wire's `display_contact_config` rate wins
|
|
@@ -2407,9 +2408,14 @@ const TRAE_SOLO_FUNCTION = "solo_work_lite";
|
|
|
2407
2408
|
* live SG credential, issue #19): it is a Remote-agent roster function, not a
|
|
2408
2409
|
* chat function. Every model sent with `function: "solo_agent"` — `gpt-5.4`,
|
|
2409
2410
|
* `gpt-5.6-sol`, `gpt-6-astra`, `glm-5.2`, `kimi-k2.7-code` — answered
|
|
2410
|
-
* `event:error {"code":4011}
|
|
2411
|
-
*
|
|
2412
|
-
*
|
|
2411
|
+
* `event:error {"code":4011}`, while the same model with the same body answered
|
|
2412
|
+
* normally under `solo_work_remote` / `solo_work_lite` / `solo_agent_remote`.
|
|
2413
|
+
* Issue #33 corrected the reading of that code: `4011` is Trae's rate limit on
|
|
2414
|
+
* the route that was asked (its own text says so, and issue #33's reporter hit
|
|
2415
|
+
* it on the CORRECT function), and the "this route does not serve that model"
|
|
2416
|
+
* condition is `4001` — measured on all 22 functions of a live CN credential.
|
|
2417
|
+
* The conclusion for this list is unchanged either way: a route that refuses
|
|
2418
|
+
* chat is not a chat route, so `solo_agent` stays out.
|
|
2413
2419
|
*
|
|
2414
2420
|
* Note `get_detail_param` DOES answer `solo_agent` with a 43-entry list, so
|
|
2415
2421
|
* asking it looks helpful while making 8 of the 10 exposed ai-region models
|
|
@@ -2809,22 +2815,38 @@ function decodeTraeEvent(event) {
|
|
|
2809
2815
|
//#endregion
|
|
2810
2816
|
//#region src/solo-bridge.ts
|
|
2811
2817
|
/**
|
|
2812
|
-
* What Trae's in-stream error codes mean. Measured 2026-10-01 on
|
|
2813
|
-
*
|
|
2818
|
+
* What Trae's in-stream error codes mean. Measured 2026-10-01 on live
|
|
2819
|
+
* credentials (issue #19), and re-measured 2026-10-12 on a live CN credential
|
|
2820
|
+
* (issue #33) with a 22-function × 6-model chat matrix:
|
|
2814
2821
|
*
|
|
2815
2822
|
* - `1005` — the model needs a paid plan. Trae answers HTTP 200 with
|
|
2816
2823
|
* `event:error {"code":1005,"message":"","extra":"{\"plan\":1}"}`, so the
|
|
2817
2824
|
* gate is only visible in `extra.plan`; `gpt-6-sol` returned it under both
|
|
2818
2825
|
* `solo_work_remote` and `solo_agent_remote` on a free account.
|
|
2819
|
-
* - `4011` —
|
|
2820
|
-
*
|
|
2821
|
-
*
|
|
2826
|
+
* - `4011` — Trae throttled the SOLO route for this account. Trae's own text
|
|
2827
|
+
* is `your requests have exceeded the rate limit. Please wait and try again
|
|
2828
|
+
* later.`, and issue #33 is why that text is now taken at face value: the
|
|
2829
|
+
* earlier reading of 4011 as "this function does not serve that model" is
|
|
2830
|
+
* contradicted by measurement. On CN that condition is `4001`, and all 22
|
|
2831
|
+
* functions answered `4001` — never `4011` — for a model absent from their
|
|
2832
|
+
* roster, while `glm-5.3` streamed normally under its own function. Rate
|
|
2833
|
+
* limiting is per account, route and moment, which is exactly the shape of
|
|
2834
|
+
* that report: the same model through the same function was fine for days
|
|
2835
|
+
* and then failed, with Trae's own text saying "rate limit".
|
|
2822
2836
|
* - `4001` — `config_name` is not one this function serves.
|
|
2837
|
+
* - `4008` — the account's SOLO quota is exhausted. Deliberately a different
|
|
2838
|
+
* code from `4011`: Trae words this one `your requests have exceeded the
|
|
2839
|
+
* quota`, so "I still have plenty of credits" and "I am being throttled" are
|
|
2840
|
+
* two different statements, and a user hits the second without touching the
|
|
2841
|
+
* first (issue #33).
|
|
2842
|
+
* - `4023` — the model name is unknown on the route that was asked.
|
|
2823
2843
|
*/
|
|
2824
2844
|
const TRAE_ERROR_HINTS = {
|
|
2825
2845
|
1005: "Trae requires a paid plan for this model; the current account's plan does not cover it",
|
|
2826
2846
|
4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
|
|
2827
|
-
|
|
2847
|
+
4008: "Trae reports the account's SOLO quota is exhausted for this request",
|
|
2848
|
+
4011: "Trae throttled the SOLO channel for this account (a rate limit, not the credit balance)",
|
|
2849
|
+
4023: "Trae does not recognise this model name on the SOLO route that was asked"
|
|
2828
2850
|
};
|
|
2829
2851
|
/** `extra` is a JSON *string* on the wire; unwrap it to read `plan`. */
|
|
2830
2852
|
function traeErrorPlan(extra) {
|
|
@@ -2854,13 +2876,30 @@ function describeTraeFailure(failure) {
|
|
|
2854
2876
|
* answer with a real error instead of a truncated 200 stream.
|
|
2855
2877
|
*
|
|
2856
2878
|
* `1005` is a subscription gate: 402 is what makes clients stop rather than
|
|
2857
|
-
* retry, and it is truthful.
|
|
2879
|
+
* retry, and it is truthful.
|
|
2880
|
+
*
|
|
2881
|
+
* `4011` is a rate limit and is reported as one — `429` / `soft_rate` (issue
|
|
2882
|
+
* #33). It previously answered `400` / `client`, which contradicted both its own
|
|
2883
|
+
* message and the client's own behaviour: pi-ai classifies an error by its text
|
|
2884
|
+
* (`/\b429\b|rate.?limit/i` → `RATE_LIMIT`), and `RATE_LIMIT` is retryable, so
|
|
2885
|
+
* the kernel retried five times with backoff while this function called the same
|
|
2886
|
+
* failure a non-retryable client error. Reporting the real status makes all
|
|
2887
|
+
* three agree, and `soft_rate` is already what `fallback-upstream` refuses to
|
|
2888
|
+
* replay — correct, since a throttled route is not a route-compatibility
|
|
2889
|
+
* problem.
|
|
2890
|
+
*
|
|
2891
|
+
* Everything else stays a client-side rejection.
|
|
2858
2892
|
*/
|
|
2859
2893
|
function classifyTraeFailure(failure) {
|
|
2860
|
-
|
|
2894
|
+
if (failure.code === 1005) return {
|
|
2861
2895
|
status: 402,
|
|
2862
2896
|
kind: "hard_credit"
|
|
2863
|
-
}
|
|
2897
|
+
};
|
|
2898
|
+
if (failure.code === 4011) return {
|
|
2899
|
+
status: 429,
|
|
2900
|
+
kind: "soft_rate"
|
|
2901
|
+
};
|
|
2902
|
+
return {
|
|
2864
2903
|
status: 400,
|
|
2865
2904
|
kind: "client"
|
|
2866
2905
|
};
|
|
@@ -3934,9 +3973,41 @@ var TraeDelegatingUpstreamClient = class {
|
|
|
3934
3973
|
* beyond this the answer is not worth waiting for and is reported as unknown.
|
|
3935
3974
|
*/
|
|
3936
3975
|
const TRAE_PROBE_TIMEOUT_MS = 3e4;
|
|
3937
|
-
/**
|
|
3938
|
-
|
|
3939
|
-
|
|
3976
|
+
/**
|
|
3977
|
+
* Every upstream code the probe recognises, mapped to the verdict it implies.
|
|
3978
|
+
*
|
|
3979
|
+
* ONE table, not a set per verdict: with separate sets the outcome depended on
|
|
3980
|
+
* the order the branches happened to be written in, so a code left in two sets
|
|
3981
|
+
* was silently decided by whichever line ran first (measured: re-adding `4011`
|
|
3982
|
+
* to the "unsupported" set passed every test). A single map makes a code mean
|
|
3983
|
+
* exactly one thing, and {@link assertCodesAreDistinct} pins that down at
|
|
3984
|
+
* module load.
|
|
3985
|
+
*
|
|
3986
|
+
* - `1005` — the plan does not cover this model (`extra.plan` carries the
|
|
3987
|
+
* tier). A paying account may pass, so it is a gate, not a dead model.
|
|
3988
|
+
* - `4001` — this function's roster does not contain that `config_name`. The
|
|
3989
|
+
* ONLY code that may produce `unsupported`, measured on all 22 functions of
|
|
3990
|
+
* a live CN credential (issue #33).
|
|
3991
|
+
* - `4011` — Trae throttled the route; its own text says the rate limit was
|
|
3992
|
+
* exceeded. Issue #33 moved this here from `unsupported`: reporting a
|
|
3993
|
+
* throttle as "the channel does not serve it" told users a working model was
|
|
3994
|
+
* dead.
|
|
3995
|
+
* - `4008` — the ACCOUNT's SOLO quota is exhausted. It says nothing about any
|
|
3996
|
+
* one model, so it must not become a model verdict; it stays inconclusive
|
|
3997
|
+
* and carries the upstream text, so the user retests once the quota is back.
|
|
3998
|
+
*/
|
|
3999
|
+
const CODE_VERDICTS = {
|
|
4000
|
+
1005: "gated",
|
|
4001
|
+
4001: "unsupported",
|
|
4002
|
+
4008: "unknown",
|
|
4003
|
+
4011: "rate_limited"
|
|
4004
|
+
};
|
|
4005
|
+
/** A code that maps to two verdicts would make the table order-dependent. */
|
|
4006
|
+
function assertCodesAreDistinct(table) {
|
|
4007
|
+
const codes = Object.keys(table);
|
|
4008
|
+
if (new Set(codes).size !== codes.length) throw new Error("dsh-connect-trae: a Trae probe code maps to more than one verdict");
|
|
4009
|
+
}
|
|
4010
|
+
assertCodesAreDistinct(CODE_VERDICTS);
|
|
3940
4011
|
/** Pull `Trae code N` out of an already-translated failure message. */
|
|
3941
4012
|
function codeOf(message) {
|
|
3942
4013
|
const match = /Trae code (\d+)/u.exec(message);
|
|
@@ -3992,12 +4063,9 @@ function classifyProbeResult(result, outputSeen) {
|
|
|
3992
4063
|
detail: message
|
|
3993
4064
|
};
|
|
3994
4065
|
const code = codeOf(message);
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
};
|
|
3999
|
-
if (code !== void 0 && UNSUPPORTED_CODES.has(code)) return {
|
|
4000
|
-
verdict: "unsupported",
|
|
4066
|
+
const mapped = code === void 0 ? void 0 : CODE_VERDICTS[code];
|
|
4067
|
+
if (mapped !== void 0) return {
|
|
4068
|
+
verdict: mapped,
|
|
4001
4069
|
detail: message
|
|
4002
4070
|
};
|
|
4003
4071
|
if (result.kind === "client" && result.status >= 400 && result.status < 500) return {
|
|
@@ -4456,6 +4524,17 @@ function loopbackOrigin(req) {
|
|
|
4456
4524
|
}
|
|
4457
4525
|
}
|
|
4458
4526
|
/**
|
|
4527
|
+
* Whether a pack's expiry is one the card can actually use.
|
|
4528
|
+
*
|
|
4529
|
+
* One predicate, shared by the per-pack projection and the bucket summary, so
|
|
4530
|
+
* the two can never disagree about which packs count as dated. `0` and the
|
|
4531
|
+
* non-finite values are excluded: a `0` here would read as 1970 and sort ahead
|
|
4532
|
+
* of every real date, and `NaN`/`Infinity` are not dates at all.
|
|
4533
|
+
*/
|
|
4534
|
+
function usableExpiry(endTimeMs) {
|
|
4535
|
+
return typeof endTimeMs === "number" && Number.isFinite(endTimeMs) && endTimeMs > 0;
|
|
4536
|
+
}
|
|
4537
|
+
/**
|
|
4459
4538
|
* Map the credit snapshot to the card's compact credit document.
|
|
4460
4539
|
*
|
|
4461
4540
|
* Exported so the "is another account usable?" probe (issue #25) derives its
|
|
@@ -4463,14 +4542,14 @@ function loopbackOrigin(req) {
|
|
|
4463
4542
|
* — a second implementation of the Work/general split would be free to disagree
|
|
4464
4543
|
* with the number shown next to it.
|
|
4465
4544
|
*/
|
|
4466
|
-
function toCredits(snapshot) {
|
|
4545
|
+
function toCredits(snapshot, nowMs = Date.now()) {
|
|
4467
4546
|
const { totalAmount, consumedAmount } = snapshot.summary;
|
|
4468
4547
|
const credit = (value) => Math.round(value * 1e4) / 1e4;
|
|
4469
4548
|
const accounts = snapshot.packs.map((pack) => ({
|
|
4470
4549
|
displayDesc: pack.displayDesc,
|
|
4471
4550
|
remain: credit(Math.max(0, (pack.creditsLimit ?? 0) - (pack.consumedCredits ?? 0))),
|
|
4472
4551
|
size: pack.creditsLimit ?? 0,
|
|
4473
|
-
...
|
|
4552
|
+
...usableExpiry(pack.endTimeMs) ? { expiresAtMs: pack.endTimeMs } : {}
|
|
4474
4553
|
}));
|
|
4475
4554
|
const remaining = (endpoint) => snapshot.packs.filter((pack) => pack.availableEndpoint === endpoint).reduce((sum, pack) => credit(sum + Math.max(0, (pack.creditsLimit ?? 0) - (pack.consumedCredits ?? 0))), 0);
|
|
4476
4555
|
return {
|
|
@@ -4479,9 +4558,74 @@ function toCredits(snapshot) {
|
|
|
4479
4558
|
available: totalAmount - consumedAmount,
|
|
4480
4559
|
workAvailable: remaining(1),
|
|
4481
4560
|
generalAvailable: remaining(0),
|
|
4482
|
-
accounts
|
|
4561
|
+
accounts,
|
|
4562
|
+
...summarisePackExpiry(accounts, nowMs)
|
|
4483
4563
|
};
|
|
4484
4564
|
}
|
|
4565
|
+
/** Empty bucket, used whenever no pack qualifies. */
|
|
4566
|
+
const NO_EXPIRING_CREDIT = {
|
|
4567
|
+
total: 0,
|
|
4568
|
+
packs: 0
|
|
4569
|
+
};
|
|
4570
|
+
/**
|
|
4571
|
+
* Sum the credit lapsing inside `horizonMs` of `nowMs`.
|
|
4572
|
+
*
|
|
4573
|
+
* Only positive balances count: a lapsed pack with nothing left is not a loss,
|
|
4574
|
+
* and including it would make every long-lived account look permanently at
|
|
4575
|
+
* risk. Only *future* expiries count too — see {@link expiredPacks}.
|
|
4576
|
+
*/
|
|
4577
|
+
function expiringWithin(packs, nowMs, horizonMs) {
|
|
4578
|
+
return sumExpiring(packs, (expiresAt) => expiresAt > nowMs && expiresAt - nowMs <= horizonMs);
|
|
4579
|
+
}
|
|
4580
|
+
/**
|
|
4581
|
+
* Sum the credit whose pack has already lapsed but still carries a balance.
|
|
4582
|
+
*
|
|
4583
|
+
* Kept apart from {@link expiringWithin} because a lapsed pack satisfies every
|
|
4584
|
+
* forward-looking horizon at once (`expiresAt - now` is negative, hence "within
|
|
4585
|
+
* 24 hours"), which would make a long-dead pack look permanently imminent.
|
|
4586
|
+
*/
|
|
4587
|
+
function expiredPacks(packs, nowMs) {
|
|
4588
|
+
return sumExpiring(packs, (expiresAt) => expiresAt <= nowMs);
|
|
4589
|
+
}
|
|
4590
|
+
/** Shared accumulator over packs whose expiry passes `matches`. */
|
|
4591
|
+
function sumExpiring(packs, matches) {
|
|
4592
|
+
let total = 0;
|
|
4593
|
+
let count = 0;
|
|
4594
|
+
let earliestExpiresAtMs;
|
|
4595
|
+
for (const pack of packs) {
|
|
4596
|
+
const expiresAt = pack.expiresAtMs;
|
|
4597
|
+
if (!usableExpiry(expiresAt) || pack.remain <= 0) continue;
|
|
4598
|
+
if (!matches(expiresAt)) continue;
|
|
4599
|
+
total += pack.remain;
|
|
4600
|
+
count += 1;
|
|
4601
|
+
if (earliestExpiresAtMs === void 0 || expiresAt < earliestExpiresAtMs) earliestExpiresAtMs = expiresAt;
|
|
4602
|
+
}
|
|
4603
|
+
if (count === 0) return NO_EXPIRING_CREDIT;
|
|
4604
|
+
return {
|
|
4605
|
+
total,
|
|
4606
|
+
packs: count,
|
|
4607
|
+
...earliestExpiresAtMs === void 0 ? {} : { earliestExpiresAtMs }
|
|
4608
|
+
};
|
|
4609
|
+
}
|
|
4610
|
+
/**
|
|
4611
|
+
* Bucket the card's credit by how soon it lapses.
|
|
4612
|
+
*
|
|
4613
|
+
* Returns no `expiry` at all when not one pack carried a usable end time, so a
|
|
4614
|
+
* caller can tell "nothing is expiring" apart from "we were never told when
|
|
4615
|
+
* anything expires" — the second must not render as reassurance. Uses the same
|
|
4616
|
+
* {@link usableExpiry} predicate as the per-pack projection, so a summary is
|
|
4617
|
+
* never produced from a set of dates the card itself would not carry.
|
|
4618
|
+
*/
|
|
4619
|
+
function summarisePackExpiry(accounts, nowMs) {
|
|
4620
|
+
if (!accounts.some((pack) => usableExpiry(pack.expiresAtMs))) return {};
|
|
4621
|
+
const DAY = 864e5;
|
|
4622
|
+
return { expiry: {
|
|
4623
|
+
within24h: expiringWithin(accounts, nowMs, DAY),
|
|
4624
|
+
within3d: expiringWithin(accounts, nowMs, 3 * DAY),
|
|
4625
|
+
within7d: expiringWithin(accounts, nowMs, 7 * DAY),
|
|
4626
|
+
expired: expiredPacks(accounts, nowMs)
|
|
4627
|
+
} };
|
|
4628
|
+
}
|
|
4485
4629
|
/** Map the check-in answer to the card's compact document. */
|
|
4486
4630
|
function toCheckin(status) {
|
|
4487
4631
|
return {
|
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.
|
|
5
|
+
"version": "3.4.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|