dsh-connect-trae 3.0.0 → 3.3.0

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,201 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.3.0 (2026-10-11)
4
+
5
+ ### 新增
6
+
7
+ - **一键测试所有模型**:模型区在「一键测试启用模型」旁新增「一键测试所有模型」,对当前 region 的**全部可见模型**发起可用性探测。两个按钮共用同一套探测路径,唯一区别是提交的 id 集合(勾选的行 / 全部可见行)。
8
+ - **`/usage` 的每个积分包携带自己的到期时刻**(`credits.accounts[].expiresAtMs`)。剩余额度只回答「还有多少」,回答不了「哪一笔先到期」,而后者才是「该先用哪个」的依据。上游本来就在 `user_entitlement_pack_list[].entitlement_base_info.end_time` 里逐包给出该时刻,这里只是把它投影进卡片文档——**不新增任何上游读**,卡片自身也不因此多一次请求。没有可用时刻的包**不带该字段**(而不是给 0):0 是 1970,会排在所有真实日期之前,读起来像「早已过期」。
9
+
10
+ ### 新增(国际版的输入框读数)
11
+
12
+ - **输入框工具栏读数扩展到国际版**(用户反馈「国际版也要这个显示,现在没有」)。此前只有国内版显示「Trae CN · <积分>」,选中国际版模型时整块读数消失。
13
+ - 原限制是**数据决定的**,不是偏好:国际版的用量文档由 `payStatus` 构建,从不带 `credits`(`webStatusFor` 对 `region === 'ai'` 提前返回),把订阅制成的东西套进数字槽位只会渲染一个破折号。但正确的修法是**渲染那一边真正有的东西**,而不是什么都不渲染——所以国际版显示**套餐**(`Pro` / `Free`),与卡片账号行的额度列同源。
14
+ - 三处文案随之区域化:产品名(`Trae CN` / `Trae 国际版`)、面板标题(`Trae 积分` / `Trae 国际版套餐`)、列头(`通用积分` / `套餐`)。在国际版面板里写「通用积分」而下面是 `Pro`,是在命名一个不存在的量。
15
+ - 面板的**逐账号套餐**走的是卡片名册用的那条**账号状态路由**(它对 ai 返回 `payStatus`),而不是积分路由——后者是 CN 形状的(它调用积分快照)。仍然**只在面板打开时读**,5 分钟周期不为它付费。
16
+ - 顺带把套餐判定抽成 `src/client/plan-text.ts`:卡片账号行的额度列、这里现在共用一个实现。此前卡片里另有一个独立订阅框与账号行各读各的源,正因如此才对同一账号给出不同档位(该框已删除,见下)。
17
+
18
+ ### 修复(「刷新账号与 Token」显示已绑定,设置里却是空的)
19
+
20
+ - **刷新账号后显示的账号,就是真正被写进设置的账号**。报告的现象是:刷新后提示「找到 1 个账号 · 仍绑定 丁敏华」,而设置文档里 `accounts.cn` **一直是空的**。
21
+ - 根因是把路由的 `selected` 标志读成了「已经绑定过」。该标志回答的是「**哪个账号正在使用**」,而不是「哪个账号被显式保存过」:没有保存过绑定时,存储层会用**探测到的默认值**(Trae App 当前登录的那个)来回答。于是「从未显式绑定」被读成「检测到的默认值正在使用,无需写入」——刷新恰好**什么都没保存**,而卡片却把它显示成已绑定;下一次存储重建(重启、或 Trae 换登录)就静默改回别的账号。
22
+ - 现在按「**这个区域实际在用的账号**(存储层的裁决,也正是卡片显示的那个)写盘」,仅在路由完全没给出裁决时才退回该区域自己的名册;**已经是保存中绑定**的行不重复写——一次点击不该为无变化重写设置文档(那会触发 `loader/volatile-update`、重跑 `applySelection` 并重建两个存储)。
23
+ - 提示语随之区分两件事:`已绑定 X`(这次真的写入了)与 `仍绑定 X`(这次没写,X 是既有绑定)。旧措辞把「保存了」和「本来就没动」压成同一句话,正是这个缺口看不见的原因。
24
+ - `tests/card-accounts-rescan.spec.tsx`(新增 6 条)钉住不变式:**刷新后卡片显示为在用的账号,必须就是设置文档记录的账号**——显示一个、保存零个是 bug,而它在截图里完全看不出来。覆盖:未绑定时必须写入、必须携带另一区域的绑定(不能把国内版的选择刷掉)、已是保存绑定时不得重写、路由未标 `selected` 时仍绑定它、只读 scope 不写且如实报告、以及提示语必须区分「已绑定」与「仍绑定」。
25
+
26
+ ### 新增(输入框读数开关扩展到国际版)
27
+
28
+ - **「在输入框工具栏显示积分读数」不再只在国内版出现**。该开关此前是 CN 专属,理由是它背后的读数只服务国内版;读数扩展后它服务**两条供应商路由**(`trae` 上显示余额、`trae-global` 上显示套餐),继续藏起开关等于让国际版的读数**从任何入口都够不着**。
29
+ - 仍是**同一个设置、同一次写入**,不是第二个开关——按区域各存一份会让读数与设置它的 tab 对不上。
30
+ - 提示文案按所在 tab 取值(新增 `row.showPointsInMainUiHintAi`):原提示一句话描述了两个区域(「…国内版为…,国际版为…」),那是它只在一个 tab 上渲染时写的;现在两个 tab 都渲染,提示只说自己这一边,勾之前就能知道会得到什么。
31
+
32
+ ### 移除(国际版独立的「订阅状态」框)
33
+
34
+ - **卡片里那个独立的「订阅状态」框已删除**,订阅档位**只由账号表「额度」列渲染**。
35
+ - 它当初是为了补「国际版没有那个积分框」的不对称而恢复的(595e01e),但账号表的额度列此后已经吸收了同一个数字,于是它**直接重复了正上方那一行**。同一份裁决有两个渲染器正是 `plan-text.ts` 存在要防的漂移,而该框能说的永远只是被标为「当前使用」的那行已经说过的内容。
36
+ - `tests/card-subscription.spec.tsx` 的三条框测试替换为**反向守卫**:国际版 tab 上不得再出现 `row.subscriptionLabel` / `row.payStatusUnknown`,包括**读失败**这一路也不得把它以降级形态复活。
37
+
38
+ ### 界面重做(用户反馈「现在的样式太难看了,而且内容也复杂」)
39
+
40
+ - **账号区改为定宽网格表格**(用户反馈「选择的样式……都和 WorkBuddy 有区别」,随后确认推翻上一版的列表要求)。每账号一行,四列:**账号 / 额度 / 签到 / 操作**,上方带表头行。
41
+ - 「Token 到期」整列不再保留:它独占一列却没有人据此做决策。
42
+ - 同一信息原本出现三次(状态点、名字旁的徽标、操作列又写一遍「当前使用」),现在只保留名字旁一个标记;当前绑定账号不再渲染任何动作按钮。
43
+ - 国内版额度(积分)与国际版订阅(套餐)落在**额度列**,不再各占一个独立统计块。
44
+ - **列宽用容器的分数**(`grid-template-columns:1.7fr 1.1fr 1fr 1.1fr`),这是关键:上一版四列各自按内容撑开,列与列在行间对不齐,才是「表格难看」的真正原因;定宽后可以沿列向下读。国际版没有签到列(该 region 无签到接口),列数降为 3,让出的宽度给账号名。
45
+ - 当前使用中的行用**左侧强调竖条 + 淡色底**标记:只有 7% 透明度底色时,长表格里它会被误读成斑马纹的隔行底色。
46
+ - 行内「切换」按钮改用共享的 `dsm-btn dsm-btn-outline` 原语并缩小,不再自造一套按钮。
47
+ - **设置区改为裸行开关、即点即存**(同一轮对齐)。去掉边框盒子、标题与「保存 / 放弃修改」按钮,两个开关各自成为一行带复选框的 `<label>`,**点击即写**。见下方「刻意改变的决定」。
48
+ - **签到按钮并入账号行**,奖励直接写在按钮上(`签到 +150`),因此删掉了旁边单独一行的「每日 150 积分」说明。见下方「刻意改变的决定」。
49
+ - **开关统一为平台原生复选框**(同一轮对齐)。tab 切换与设置项都改为 `<input type="checkbox">` + `accent-color`,撤销本次重做里自绘的圆钮开关。原委见下方「刻意改变的决定」。
50
+ - **文案精简**:tab 提示、设置说明、账号说明、两个开关说明、供应商关闭提示等 15 处长段落各压缩为一行,关键信息不丢。完整解释移入 `title` 提示。
51
+
52
+ ### 修复(三处静默失效,任何行为测试都测不到)
53
+
54
+ - **`TraeUsagePack.endTimeMs` 装的是秒,不是毫秒**(外部贡献,PR #31 by [@welsongit](https://github.com/welsongit))。该字段直接取自上游的 `entitlement_base_info.end_time`(`src/usage.ts`),而 Trae 给出的 `end_time` 是**秒**:对照真机 CN 账号,`1788340660` 是 2026-09-02;按毫秒读则是 1970-01-21。字段名写的是 `Ms`,于是任何照名字取用的调用方都会得到一个 1970 的日期——它排在所有真实日期之前,读起来像「早已过期」。
55
+ - 该字段此前**没有任何读取方**(全仓只有写入点、没有一处消费),所以这个错误没有表现,也就没被发现。
56
+ - 现在在解析处按量级归一(`> 1e12` 视为毫秒、否则按秒换算),与 `src/auth.ts` 对存储过期字段的既有判定一致;上游若某天改用毫秒也不会被乘成公元 57000 年。
57
+ - `tests/web-status.spec.ts` 新增用例钉住这条契约:到期时刻必须成为毫秒,且「上游没给」时字段**缺席**而不是 0。
58
+
59
+ - **游离的右括号吞掉了一整条规则**:`.dsm-trae-models-actions` 之后有一个孤立的 `}`(全文 193 个 `{` 对 194 个 `}`)。它让紧随其后的 `.dsm-trae-usage-alternative` **完全不生效**——积分耗尽提示框因此没有琥珀底、没有边框,按钮被挤到文字下方(flex 失效)而不是并排。jsdom 不做层叠计算,一个没样式但类名与文本都正确的元素能通过任何行为断言,所以这个 bug 骗过了当时全部 517 个测试。
60
+ - **6 个 DSH 并不存在的 token**:`--dsw-alias-bg-layer-4`、`--dsw-alias-state-warning-bg/-border/-primary`、`--dsw-state-warning`、`--dsw-state-success`。`var(--nope, #333740)` 是合法 CSS,**看起来已被兜底处理**,但兜底值会在**两个主题下都生效**:
61
+ - 那个 hover 因此长期是深色 `#333740`:浅色主题下按钮底色本是白的,一 hover 变成深炭色而文字仍是近黑 `#0f1115`,**几乎看不见**;深色主题下 `#353638 → #333740` 每个通道只差 2,**等于没有反馈**。改用 DSH 真正为此提供的 `--dsw-alias-interactive-bg-hover`(半透明叠加层,自适应任何底色且文字始终可读)。
62
+ - 警告类 token 改用真实名称 `state-warn-primary/-secondary/-tertiary`,其中 `-tertiary` 本身按主题取值(浅色 amber-100 / 深色 amber-900),警告框从此随主题正确显示。
63
+ - **开关打开态的旋钮在深色主题下不可见**:旋钮硬编码 `#fff`,而轨道用 `--dsw-alias-brand-primary`——该 token 不是蓝色,而是**反差色**(浅色近黑 / 深色近白)。深色主题下即白旋钮配近白轨道。旋钮改用 `--dsw-alias-bg-layer-1`(与轨道在两个主题下都相反)。
64
+ - **「仍绑定在上一个账号」提示看不懂**。该提示只在 App 登录与插件绑定确实不一致时出现(`liveLogin()` 在 `liveId === this.accountId` 时返回 `undefined`),事实没错,但措辞有三个问题:只点名 App 登录的账号、把绑定账号写成**「上一个账号」**(而这条提示存在的意义恰恰是省掉去列表里找这一步);用「当前登录」与列表里绑定账号的**「当前使用」**徽标撞车,两个「当前」指向不同账号,读起来像自相矛盾;且「上一个」断言了插件无从得知的顺序。现改为同时点名两个账号(`Trae 已登录 X,但本插件绑定的是 Y。`),按钮也改为点明目标(`改用 X`)。
65
+
66
+ ### 测试
67
+
68
+ - `tests/card-styles.spec.tsx` 新增两组**结构性**防线,各对应一个已发生的真实故障:
69
+ - **括号平衡**:失效时报出偏移量(`expected -1 to be 1` 说不出任何有用信息)。
70
+ - **token 白名单**:15 个 token 全部逐一对照过 DSH;新增 token 必须人工加入白名单——那正是该去 `grep` 一遍 DSH、而不是「编一个看起来合理的名字」的时刻。该白名单同时校验**不得成为无人清理的超集**,删掉圆钮开关后 `--dsw-alias-bg-layer-1` 就此暴露并移除。
71
+ - 同时**刻意没有**加「所有 `var()` 必须带兜底值」这条:本次 bug 恰恰是**错误兜底值**造成的,裸 `var()` 会明显失效,而错误的兜底值只会静默变色。
72
+ - `tests/card-accounts-table.spec.tsx` 新增「过期绑定提示」两条:句子必须同时包含绑定账号名与 App 登录账号名;按钮文案必须包含要切过去的账号名。该提示此前**零测试覆盖**,旧文案才能一直躺在那里。
73
+ - `tests/card-checkin.spec.tsx` 新增一条:一次签到必须**重读该 region 的每一行**,而不只是被点的那一行(见下)。
74
+ - `tests/card-composer-credits-switch.spec.tsx` 随「即点即存」改写:原来断言「点开关不写、按保存才写、放弃修改不写」的三条,改为断言**点击即产生一次且仅一次写入**、且**只写被点的那个字段**(另一开关已在文档中为 true 时不得被这次点击覆盖)、写入落定后方框显示提交值且页面上不存在任何「未保存」标记。
75
+ - 开关的可访问名随 `<label>` 包裹提示文字而变化(由 `row.showPointsInMainUi` 变为 `row.showPointsInMainUi row.showPointsInMainUiHint`),测试改为**锚定前缀**而不是要求整串相等。
76
+ - `tests/card-accounts-table.spec.tsx` / `card-checkin.spec.tsx` 的行定位从 `li` / `role=listitem` 改为 `role=row` 与 `.dsm-trae-account-row`;行数断言由 2 变 3,并把**表头行必须存在**写成契约的一部分——表头正是「能沿列读下去」的前提。
77
+ - 全量 `pnpm run check`(typecheck × 2 + 534 测试 + build)通过(本节各轮次累计后的当前值)。
78
+
79
+ ### 刻意改变的决定(国际版读数显示套餐,而不是隐藏整块)
80
+
81
+ 原实现**刻意**把 `trae-global` 排除在 `TRAE_COMPOSER_PROVIDERS` 之外,理由写在注释里:国际版没有可比的余额,映射过去会渲染「Trae CN」标签配一个破折号——一个没有数字的数字槽位。那个判断对了一半:**标签硬编码确实是错的**,但结论不该是整块消失。
82
+
83
+ - 现在两个 provider 路由都映射,读数按区域决定显示什么:CN 显示通用积分,ai 显示套餐名。`composer.points` 也从硬编码的 `Trae CN` 改为按区域取(新增 `composer.pointsGlobal`)。
84
+ - 对国际版用户来说,此前这个功能的实际行为是「打开开关,什么都没有」;现在至少回答「这个账号是什么档位」。
85
+
86
+ ### 刻意改变的决定(设置区去掉草稿式保存,改为即点即存)
87
+
88
+ 3.2.0 把设置区做成**草稿式保存**:「点『保存』才生效」,两个开关先写进本地草稿,由「保存」统一落盘,「放弃修改」丢弃草稿。本次改为**点击即写**,删除「保存 / 放弃修改」「设置」标题与整个边框盒子。
89
+
90
+ - **理由**:它保护的只有两个布尔值。让用户为两个布尔值记住「还要按一下保存」,成本大于收益;而同一张卡片里,tab 栏的供应商开关本来就是即点即写——卡片内部因此有**两套提交模型**。改为一套后,用户不必分辨哪个开关需要保存。
91
+ - **不丢的那点保护**:写入本身仍是**乐观更新 + 以宿主为准**——点击先立即反映在方框上,写入完成后撤掉乐观值,改读已提交的文档。写失败时方框会**弹回宿主的值**,而不是继续显示一个从未落盘的状态。两个写入者由 `savingSettings` 串行化,因为它们写同一份设置文档。
92
+ - **只写被点的那一个字段**:不发整个对象,避免一次点击用陈旧快照覆盖另一个开关在别处发生的改动。
93
+
94
+ ### 刻意改变的决定(撤销本次重做里的圆钮开关)
95
+
96
+ 本次界面重做把 tab 切换与设置项做成了**自绘圆钮开关**(34×20 轨道 + 14px 旋钮),理由写在当时的 CSS 注释里:「让 tab 栏和设置区看起来是同一个产品,而不是旁边放一个原生复选框」。用户看到 WorkBuddy 卡片后指出「选择的样式……都和 WorkBuddy 有区别」,现全部改回**平台原生复选框 + `accent-color`**。
97
+
98
+ - **原设计有两个说不通的地方。** 其一,同一个插件里的**输入框工具栏开关**(`.dsm-trae-mainui-switch-label`)一直就是原生 checkbox + `accent-color`,圆钮只出现在卡片里,插件自己就不统一;其二,圆钮的选中色用 `--dsw-alias-brand-primary`,而该 token 是 DSH 的**反差色**(浅色 `#0f1115` / 深色 `#f9fafb`),不是强调色——于是「打开」渲染成一枚**纯黑大胶囊**,比卡片里任何元素都重,且和两步之外的原生复选框毫无关系。
99
+ - **原生复选框顺带解决主题与可访问性**:勾选态、深色模式、键盘焦点环、高对比度模式全部交给宿主,插件不再维护旋钮几何。行为不变(仍是同一个 `type="checkbox"`,`aria-label` / `aria-labelledby` 与写设置路径都没动),因此行为测试全绿。
100
+ - **测试同步改写**:原来锁死旋钮的断言(`dsm-trae-switch-track`、`input:checked+track`、`::after`)改为锁**反向不变式**——必须存在 `accent-color` 规则,且**不得**再出现手绘轨道或 `clip:rect` 隐藏原生框的写法,防止圆钮被无意间改回来。
101
+
102
+ ### 刻意改变的决定(取代 3.2.0 中「不提供逐账号签到」)
103
+
104
+ 3.2.0 明确写过「**账号表格里不提供任何签到操作**,签到按钮仍只有全卡唯一一个」,理由是 Trae 的签到按**每台设备每天一次**计,批量签到只会制造一排假失败。本次改为**每行各带一个按钮**,理由与边界如下:
105
+
106
+ - **仍然没有「一键全签」**:一次点击只发一次 claim,只针对该行账号;测试继续断言不存在批量控件。3.2.0 反对的是**批量遍历**,不是「让用户自己选哪个账号」。
107
+ - **每行如实显示设备级状态**:可领 → `签到 +150`;该账号当天已领 → `已领取`;**本机额度已用但该账号没领** → `本机已签`(绝不显示成「已领取」)。设备额度用完后按钮**直接禁用**,而不是留给用户点一次再吃 9095。
108
+ - **新增:一次签到后重读所有行**。设备额度被本次签到花掉后,其它行从「可签到」变为「本机已签」;只刷新被点的那一行会让兄弟行继续展示一个只可能被 9095 拒绝的按钮——正是 3.2.0 想避免的「一排假失败」的弱化版。改为**重读服务端状态**而不是前端猜测:本次签到若并未花掉设备额度(该账号在别处已领),兄弟行照旧保留可签按钮。
109
+ ## 3.2.0 (2026-10-11)
110
+
111
+ ### 设置与账号区按 WorkBuddy 的组织方式重做
112
+
113
+ 用户反馈「没看到设置菜单变化,WorkBuddy 里设置菜单变化挺大的」。对齐 WorkBuddy 卡片的组织方式后,本插件的设置与账号区从「散落的裸复选框 + 一个下拉框」变成两个有标题、有说明、有明确提交边界的区块:
114
+
115
+ - **「设置」区块(新)**:标题 + 说明 + 每项一行「标签 + 说明 + 开关」。
116
+ - 两项偏好(`followAppLogin` 跟随 Trae 登录、`showPointsInMainUi` 输入框积分读数)改为**草稿式提交**:改动先留在卡片里,出现「有未保存的修改」提示,点「保存」才写入,点「放弃修改」还原为已保存的值。
117
+ - 保存**只写被改动的那一项**——另一项若在别处(DSH 设置表单、另一个窗口)被改过,不会被本次保存用卡片里的旧值覆盖。写入仍走原有回读校验(`set()` 返回成功不算落地,必须读回一致),失败照旧显示错误行而不是静默回滚。
118
+ - 开关是真正的开关样式(含键盘焦点环),不再是一个贴在右下角、没有任何说明的裸复选框。
119
+ - **「Trae 账号」区块(新)**:下拉框换成表格,列为 **账号 / 通用积分 / Token 到期 / 操作**。
120
+ - 每行显示账号名、**产品名**(Trae / Trae 国际版 / TRAE SOLO / TRAE SOLO 国际版——不再是难以辨认的 `solo` 原始标记)、region、来源(桌面端 / Trae CLI / 插件副本)、Token 到期时间。
121
+ - 当前绑定账号带「当前使用」标记且该行操作禁用;点击其它行的「切换」即换绑。切换**只写本 region 的槽位**,另一 region 的绑定原样保留(两个供应商并行时的既有约定,issue #11)。
122
+ - 余额列**按需读取**:点「读取各账号余额」才发请求(该路由每账号一次上游读取,因此**绝不在卡片打开时自动读**,否则看一眼卡片就要为整个账号列表付费);绑定账号的余额直接来自用量文档,不额外发请求。读取失败会**报错且不清空账号列表**,未知余额显示 `—` 而不是 `0`。
123
+
124
+ ### 刻意不做的事(与 3.1.1 一致,这里再次钉死)
125
+
126
+ - **没有一键签到、没有逐账号签到**。签到仍是当前账号的单次点击:Trae 的签到按**每台设备每天一次**计(2026-10-11 本机实测:`did_checked_in` 随 `x-device-id` 变化而翻转),设备额度用完后其它账号 claim 只会被 `9095` 拒绝,批量只会制造一排假失败。账号表格里**不提供**任何签到操作,签到按钮仍只有全卡唯一一个;测试直接断言这一点。
127
+ - **没有「隐藏/删除账号」**。WorkBuddy 有这项是因为它有宿主侧的隐藏名单(`.dismissed.json`);Trae 侧没有这套持久化,本次是纯前端重组与交互补齐,不假装提供该能力。
128
+
129
+ ### 测试
130
+
131
+ - `tests/card-composer-credits-switch.spec.tsx`:两个开关的断言从「点击即写」改为**保存流**——点开关**不产生任何写入**、出现「有未保存的修改」、「保存」后才写入且提示消失、「放弃修改」丢弃草稿且零写入、一次保存只写被改动的那一项、只读 scope 下仍不可写。
132
+ - `tests/card-accounts-table.spec.tsx`(新增 7 条):账号表格渲染与产品名/来源;当前账号被标记且无操作、其它账号可切换;切换只写本 region 槽位并保留另一 region;**打开卡片不读余额**而按需点击才读(且只读一次、带 region 参数);余额读取失败报错但名单保留;**全卡只有一个签到按钮且不在账号表格内**(禁止逐账号/批量签到的防回归)。
133
+ - 全量 `pnpm run check`(typecheck × 2 + 507 测试 + build)通过。
134
+
135
+ ## 3.1.1 (2026-10-11)
136
+
137
+ ### 签到与多账号切换的边界(验证后说明)
138
+
139
+ - **多账号切换沿用 WorkBuddy 同款用法**:卡片顶部账号下拉切换当前绑定账号、跟随 Trae 当前登录(issue #29)、积分耗尽时提示可切换的其它账号(issue #25)——**切到哪个账号,积分读数与签到就属于哪个账号**。
140
+ - **刻意不做「一键签到」**:签到按钮只对当前选中的账号生效,点击一次只发一次 claim 请求(防回归测试见下),**不会**遍历本机其它账号逐个签到。理由是本机实测的 Trae 规则:
141
+ - **Trae 的签到按「每台设备每天一次」计,不按账号**(2026-10-11 本机实测:同一 CN 账号同一天,带本机 `x-device-id` 读签到状态得 `did_checked_in: true`——本机额度已用;去掉该请求头再读同一时刻变 `false`——设备级键控坐实)。
142
+ - 「账号未领、设备已签」(`checked_in: false, did_checked_in: true`)正是**切换账号后的常态**:新账号今天在本机领不到,claim 只会被业务码 `9095`("该设备今日已参与签到")拒绝。所以**一键全签没有意义**——只有第一个账号能成功,其余全部被拒,只会制造一排虚假的「签到失败」。
143
+ - 行为不变,只是把上述边界写进文档并钉死:设备已签时宿主路由**先读状态、不发 claim**(测试断言 upstream 调用数为 0),把 `9095` 呈现为「本机今日已签到,可换设备或明天」的说明而非错误。
144
+
145
+ ### 测试
146
+
147
+ - `tests/card-checkin.spec.tsx` 新增:**多账号在场(含跨 region 账号)时,点击签到仍只产生一次 claim POST**,不向其它账号扩散——防止未来把单账号签到改成批量签到。
148
+ - 全量 `pnpm run check`(typecheck × 2 + 497 测试 + build)通过。
149
+
150
+ ## 3.1.0 (2026-10-11)
151
+
152
+ ### 修复
153
+
154
+ - **适配 Trae 新版刷新契约(`DeviceProof` 签名),国内版与国际版一并支持**。原报告「国际版 token 获取不到」的表象是国际版 tab 恒报 `trae: token refresh failed … (http 401); sign in again in Trae`,根因有**两层**:
155
+
156
+ 1. **表层——绑定了失效账号。** `accounts.ai` 指向的账号来自本机已卸载的 **TRAE SOLO 国际版**,其 access token 已于 2026-10-09 过期;同区域另一个有效账号(Trae 国际桌面版)就在旁边,但 `current()` 刻意不回退(避免静默换人计费),于是整个区域显示未登录。用配置里的账号下拉切一次即可恢复。
157
+
158
+ 2. **深层——刷新契约已换代(本次修复的主体)。** 官方客户端(Trae CN 3.4.2 / Trae 国际 3.6.2)已从 `/cloudide/api/v3/trae/oauth/ExchangeToken` 换到 **`/trae/api/v3/oauth/ExchangeToken`**,并**强制要求 `DeviceProof`**:
159
+
160
+ - `DeviceProof.Signature` = 用**本机 ECDSA P-256 私钥**对 `["POST", 路径, ClientID, RefreshToken, 时间戳秒, nonce]` 做 SHA-256 签名(base64)。**六个字段用换行符连接**——官方 bundle 是 `.join("\n")`,用空格连接会被拒为 `20403 Token device not match`,表现与「密钥不对」**完全一致**,极易误诊。
161
+ - 私钥就存在 `storage.json` 的 `iCubeAuthInfo://icube-dc:<deviceId>` 里,与凭证同一套 AES 加密,**用现有的 `decryptTraeStorageValue` 即可解出**(四版通吃)——不需要 keychain。
162
+ - ClientID **按「是否 SOLO 包」分叉,不按 region**:桌面(cn/sg)→ `ono9krqynydwx5` + `IDE_PC`;SOLO(**solo 与 solo-sg**)→ `en1oxy7wnw8j9n` + `SOLO_PC`。**旧的「SOLO CN 用桌面 ClientID」在新契约下会得到 `10101 无效参数`**——这正是旧 `REFRESH_CONTRACT` 表的失效点。
163
+
164
+ 实现:`src/refresh.ts` 重写为「当前契约(带签名)优先、旧契约回退」,`src/identity.ts` 新增 `deviceKeyPairOf()` 读出 keypair 并补 `DeviceName`/`DeviceModel`,宿主把 keypair 随 DeviceInfo 传入。**没有 keypair 时(CLI-only 或旧安装)直接走旧契约**,不发送注定被 `20405 Device proof required` 拒绝的请求。
165
+
166
+ - **顺带修掉一个更隐蔽的老 bug:旧契约的 ClientID 一直用错。** 实测四个 edition 在旧 `/cloudide` 路径上**都能刷新成功**,但**必须用各自对应的 ClientID**(用错 → `10101 refresh token is not matched to the client`)。这说明该分叉**属于 OAuth client,不属于 endpoint**,而 3.1.0 之前插件在旧路径上**写死桌面 ClientID**,所以 **`solo` 与 `solo-sg` 的旧路径一直是 400**——国际 SOLO 账号一旦过期就再也刷不回来,正是本次报告的现象。现在两条路径共用同一张按 edition 分叉的 ClientID 表,四个 edition 在**两条路径上均实测 200**。
167
+
168
+ - 错误码语义已实测归档(`docs/INTL_SG_EVIDENCE.md` §2.2.1):`20405` 完全没带 proof、`20403` proof 对不上、`10101` ClientID 用错、`20101` refresh token 本身失效。
169
+
170
+ ### 证据
171
+
172
+ - 两条独立来源交叉:**读客户端 bundle**(`NPe()` 签名函数、`$v()` ClientID 分支、`m(t)` 请求头)+ **读 App 自己写的请求日志**(`[exchangeTokenByRefreshToken] request …` / `[exchangeToken] response success`),再用**四个 edition 实测**验证:cn / sg / solo / solo-sg **全部 HTTP 200**。
173
+ - `DeviceInfo` 必填梯度实测:`DeviceID` + `PlatformCode` + `DeviceType` 必填(去掉 `DeviceType` → `20403`),`MachineID` / `ClientVersion` / `DevicePublicKey` 可省(实现仍带全,与官方同形)。
174
+ - 成功响应新增记录 `RefreshExpireAt`(此前被丢弃)。
175
+
176
+ ### 测试
177
+
178
+ - `tests/refresh.spec.ts` 重写:四 edition 的路径/ClientID/PlatformCode、proof 用**真实 P-256 密钥**签发并按换行消息验签(并断言空格连接必然验签失败)、无 keypair 降级、当前契约被拒后回退、两条路径失败都进错误信息。
179
+ - 全量 `pnpm run check`(typecheck × 2 + 495 测试 + build)通过;并用**构建产物 `lib/index.js`** 对四个 edition 做了端到端实测。
180
+
181
+ ## 3.0.1 (2026-10-10)
182
+
183
+ ### 修复
184
+
185
+ - **跟随 Trae 当前登录的账号(issue #29)**。在 Trae 桌面端**退出换登录**后,插件此前一直绑定**换号前**的账号继续计费,而同族插件 `dsh-connect-workbuddy` 会自动跟随 App 的新登录;报告人需要每次手动打开账号下拉切一次。根因是两处:`TraeCredentialStore.preferred()` 只取 `credentials[0]`(**不认哪个来源是 App 的 live 登录**),`current()` 在 `accountId` 有值时**严格绑定、find 不命中也不回退**(刻意为之,见 `src/auth.ts` 的注释——避免悄悄给用户换一个计费账号)。
186
+
187
+ 分两条路径修,**都不改变未勾选用户的任何行为**:
188
+
189
+ 1. **`preferred()` 改为 live 登录优先**(`source === 'desktop'` 的凭据先于插件自己的刷新副本)。只改**排序**、不改 `readAll()` 的取值去重——同一账号仍保留更长寿的副本,否则会退回「有效 token 在隔壁文件却读成过期」的旧问题(`src/auth.ts:371-384` 的注释)。这修好「从未显式选择」的默认路径,与 workbuddy 对齐。
190
+ 2. **显式绑定的跟随**:新增只读方法 `liveLogin()` 报告「App 当前登录 ≠ 绑定账号」的**不一致**(纯读,绝不静默换绑);卡片在账号下拉旁给出**提示行 + 一键切换**(复用与下拉相同的 `switchAccount` 写入,保持单一写入路径);新增配置字段 `followAppLogin`(volatile,默认**关闭**)让宿主在每次配置生效时自动重绑——**默认关**是因为自动换绑意味着换人计费,必须由用户主动勾选,未勾选时提示行就是手动通道。
191
+
192
+ 配套:usage 文档新增 `appLogin` 字段(仅在不一致时携带,健康文档不加字段、扫描失败也不拖垮整个文档),双语文案 `row.appLoginMismatch` / `row.appLoginFollow` / `row.followAppLogin`,README「多账号切换」补充该行为说明。
193
+
194
+ ### 测试
195
+
196
+ - 新增 5 个:`preferred()` 优先 App 登录(旧 own 副本仍列出但不默认选中)、`liveLogin()` 三种不一致/一致状态、usage 文档 `appLogin` 的携带/省略/扫描失败降级;集成测试的 `MemorySettings` 补上 `revision` 冲突门(建模真实 `SettingsConflictError`)与新 volatile 字段 `followAppLogin`。
197
+ - `pnpm run check`(typecheck + 全量测试 + build)通过。
198
+
3
199
  ## 3.0.0 (2026-10-07)
4
200
 
5
201
  > **积分从「侧边栏常驻一行」改为「输入框工具栏的按供应商读数 + 账号表格浮层」。**
package/README.en.md CHANGED
@@ -43,8 +43,9 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
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
  - **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`/`4011`; no SOLO function serves it) / **throttled** / **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.
46
- - **Daily check-in claiming** — a one-click claim on the CN card (the button shows the daily reward, and reads "Claimed today" and disables itself once that account has been paid). This is the plugin's **only** operation that changes account state, and it runs only when you click. Trae allows **one check-in per device per day**: after switching accounts, if this machine's slot is already spent (by any account), the selected account cannot claim here that day — the card explains that rather than pretending the account was paid. The international side has no check-in activity, so no button is shown there.
47
- - **Composer credit readout** — the composer tool row (between "Expert" and the model selector) shows "Trae CN · <n>", refreshing every 5 minutes. The whole readout is clickable and opens a panel showing a TABLE: one row per account in the region with its GENERAL balance only, 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).
46
+ - **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
+ - **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
+ - **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).
48
49
 
49
50
  <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="536" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
50
51
 
package/README.md CHANGED
@@ -41,10 +41,12 @@
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
- - **每日签到领取** —— 国内版卡片上一键领取当日签到积分(按钮显示每日奖励,该账号已领取则显示「今日已领取」并禁用);这是插件里**唯一会改变账号状态**的操作,由你主动点击触发。Trae 的签到按「**每台设备每天一次**」限制:切换账号后如果本机今天的名额已经用掉(无论是哪个账号用的),当前账号在本机当天无法再签,卡片会说明原因而不是谎报「已领取」。国际版没有签到活动,因此不显示该按钮。
47
- - **输入框积分读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示「Trae CN · <数值>」,5 分钟自动刷新。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**、只列**通用积分**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择),并把手动刷新按钮放在浮层里。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
47
+ - **一键测试所有模型** —— 同一套探测,作用于当前 region 的**全部可见模型**(不限于已启用的)。想知道「没启用的那些到底还能不能用」时用它——比如套餐降级后,或 Trae 调整了模型阵容之后。
48
+ - **每日签到领取(逐账号)** —— 国内版**每个账号行各有一个签到按钮**,当日奖励直接写在按钮上(`签到 +150`);该账号当天已领显示「已领取」并禁用,**本机今日名额已被别的账号用掉**时显示「本机已签」并禁用——如实区分「这个账号领了」和「这台设备领过了」,绝不把后者说成「已领取」。这是插件里**唯一会改变账号状态**的操作,由你主动点击触发;一次点击只针对该行账号、只发一次请求,**刻意没有「一键全签」**——Trae 的签到按「**每台设备每天一次**」计(换账号不会重置当天名额),批量只会酿成一排假失败。国际版没有签到活动,因此不显示该按钮。
49
+ - **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,5 分钟自动刷新。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择),并把手动刷新按钮放在浮层里。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
48
50
 
49
51
  <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="536" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
50
52