dsh-llm-adapters 1.2.0 → 1.2.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/DEVELOPMENT.md CHANGED
@@ -16,11 +16,12 @@ src/
16
16
  loomy-login.ts Loomy 微信扫码:uuid、长轮询、讯飞账号 HMAC 接口
17
17
  raccoon-login.ts 小浣熊扫码:服务端签发二维码、轮询、业务会话确认
18
18
  trae-login.ts Trae 网页登录:独立浏览器配置、CDP 读 cookie 与设备指纹
19
+ trae-client.ts Trae 桌面客户端登录态:只读 storage.json、本机解密、设备指纹
19
20
  trae-endpoints.ts 两个区域的站点 / 网关 / 计费基址
20
21
  account-profile.ts 小浣熊账号展示字段
21
22
  loomy.ts Loomy 目录 / 积分 / 流式调用
22
23
  raccoon.ts 小浣熊目录 / 余额 / 流式调用
23
- trae.ts Trae 目录合并 / SOLO 流式调用 / 额度
24
+ trae.ts Trae 目录合并 / SOLO 流式调用 / 额度 / 客户端签到
24
25
  codearts-login.ts CodeArts 浏览器授权:本地回调、PKCE/DPoP、ticket 回退
25
26
  codearts-oauth.ts STS 令牌换取与续期(PKCE + DPoP,零依赖 JWS)
26
27
  codearts-sign.ts 华为 SDK-HMAC-SHA256 请求签名
@@ -188,8 +189,20 @@ metadata”)。远端目录在多个分组里用不同大小写/显示名重
188
189
  `end_time` 重置时间)。国际版的 `ide_user_pay_status` 只有套餐名、没有任何金额,官网的
189
190
  “Dollar Usage”面板读的也是 v1 这份 roster。两条路线的选择由区域档案的 `usageV2` 决定。
190
191
 
191
- Trae 的每日签到入口已移除:签到只在客户端里生效,网页登录派生的设备号在 `claim` 上会被上游
192
- 回 `9074`,留着按钮只会一直失败。
192
+ ### Trae 客户端登录与签到(`trae-client.ts`)
193
+
194
+ Trae 的每日签到只在“复用本机客户端登录态”时开放。网页登录派生的设备号在 `claim` 上会被上游
195
+ 回 `9074`,因此网页登录不显示签到入口。`trae-client.ts` 只读客户端
196
+ `User/globalStorage/storage.json` 里的 `iCubeAuthInfo://icube.cloudide`:密文头部是 6 字节
197
+ magic + 32 字节密钥材料,AES-128-CBC 的 key/IV 由两级 SHA-512 与固定 salt 派生,明文是
198
+ SHA-512 摘要 + JSON。插件从解密结果取 `Cloud-IDE-JWT`,从
199
+ `iCubeAuthInfo://icube-dc:<16 位设备号>` 取设备号,从 `telemetry.machineId` 取机器码;
200
+ 只读、不回写,客户端退出或令牌过期后重新复用。`checkinStatus` / `claimCheckin` 只在该来源下
201
+ 调用 `/trae/api/v2/ug/checkin_credits/{status,claim}`,body 为 `{ req_source: 1 }`,并直接
202
+ 使用客户端设备号(不做派生)。数据目录按区域默认取 `%APPDATA%\Trae CN` / `%APPDATA%\Trae`
203
+ (macOS 为 `Application Support`,Linux 为 XDG 配置目录);`DSH_TRAE_CLIENT_DIR`、
204
+ `DSH_TRAE_CN_CLIENT_DIR` 与 `DSH_TRAE_GLOBAL_CLIENT_DIR` 可覆盖它,测试与便携安装都走这组
205
+ 环境变量。
193
206
 
194
207
  ### 小浣熊的每日积分(`raccoon.ts`)
195
208
 
@@ -385,12 +398,15 @@ Raccoon 的思考控制有两条通道,都要发:`extra_body.thinking.type =
385
398
  | 路由 | 用途 |
386
399
  | --- | --- |
387
400
  | `GET /api/llm-adapters/models?brand=` | 实时目录 + 开关 / 窗口,`fresh=1` 清缓存 |
388
- | `GET /api/llm-adapters/status?brand=` | 登录状态、到期时间、额度 |
401
+ | `GET /api/llm-adapters/status?brand=` | 登录状态、登录来源、本机客户端检测、到期时间、额度 |
389
402
  | `GET /api/llm-adapters/account/browser?brand=` | 登录进度(本地登录页 URL、Trae 的浏览器状态或 CodeArts 的授权页地址) |
390
- | `POST /api/llm-adapters/account` | `browser` / `browser-cancel` / `checkin` / `logout` |
403
+ | `POST /api/llm-adapters/account` | `browser` / `browser-cancel` / `client-login` / `checkin` / `logout` |
391
404
 
392
- `checkin` 由 Trae(国内版)与 CodeArts 实现,其余供应商会明确拒绝;`browser` 对 CodeArts 返回
393
- 华为云授权页地址,页面在新标签页打开它,之后仍由宿主轮询 `/account/browser` 接回。
405
+ `checkin` 由 Trae(仅国内版且客户端登录态)与 CodeArts 实现,其余供应商会明确拒绝;`browser`
406
+ 对 CodeArts 返回华为云授权页地址,页面在新标签页打开它,之后仍由宿主轮询 `/account/browser`
407
+ 接回。`client-login` 只对 Trae 生效:服务端读取本机客户端登录态后返回普通账户读数,响应里
408
+ 没有令牌。`GET /status` 的 `clientLogin` 字段只暴露客户端是否安装、是否已登录与账号名,
409
+ 同样不含令牌。
394
410
 
395
411
  写操作在 body 里带 `brand`,URL 不承载写目标。`browser` 还可以带 `region`(`cn` / `global`),
396
412
  只有 Trae 使用它。配置写入仍走宿主 `configForms` 的修订栅栏,不经过插件自己的写接口。设置页
@@ -433,8 +449,9 @@ npm run build
433
449
  没有 `lib/index.js` 的包会在启用时报 `ERR_MODULE_NOT_FOUND`。改完 `src/` 必须重新构建并提交产物。
434
450
 
435
451
  `vitest` 覆盖目录解析、登录页状态机、Loomy 长轮询与扫码绑定、小浣熊扫码轮询、Trae 两份目录
436
- 的合并与档位映射、CodeArts 的签名 / PKCE-DPoP / 双目录 / 倍率时段 / 额度 / 签到、SSE 与工具
437
- 调用、凭据存储与并发、过期判定、设置 API。登录相关的测试全部注入假 `fetch`,不访问真实上游,
452
+ 的合并与档位映射、Trae 客户端登录态的解密 / 设备号 / 过期与区域判定、Trae 客户端签到、
453
+ CodeArts 的签名 / PKCE-DPoP / 双目录 / 倍率时段 / 额度 / 签到、SSE 与工具调用、凭据存储与并发、
454
+ 过期判定、设置 API。登录相关的测试全部注入假 `fetch`,不访问真实上游,
438
455
  也不启动浏览器。需要真机验证时,手动点一次「打开扫码登录」或「打开浏览器登录」,确认接回、
439
456
  模型列表与一次对话。
440
457
 
package/FAQ.md CHANGED
@@ -261,8 +261,14 @@ STS 令牌本来就只有两小时,插件在到期前 5 分钟用续期令牌
261
261
 
262
262
  有,但只对**积分计费账户**开放:插件先读账户类型,再查活动列表里的 `USER_LOGIN`,可领取时才
263
263
  `claim`(服务端要求确认时再补 `confirm`)。Token 计费账户会显示「非积分计费账户,没有签到活动」,
264
- 而不是报领取失败。Trae 的签到入口已移除:签到只在 Trae 客户端里生效,网页登录派生的设备号拿不到
265
- 领取资格,留着按钮只会一直失败。
264
+ 而不是报领取失败。
265
+
266
+ **Trae 有签到吗?**
267
+
268
+ 有,但只在本机 Trae 客户端已登录、并在设置页选择「复用客户端登录态」时显示。签到接口要求
269
+ 客户端自己的 16 位设备号;网页登录派生的设备号在 claim 上会被上游回 `9074`,所以网页登录
270
+ 不显示按钮。插件只读客户端的登录态,不修改也不退出客户端;客户端退出或令牌过期后,先在客户端
271
+ 重新登录,再点一次「重新读取客户端登录态」。国际版没有签到活动。
266
272
 
267
273
  **CodeArts 报 `TM.00001041 并发会话数已达上限(3个)`?**
268
274
 
package/README.md CHANGED
@@ -3,9 +3,11 @@
3
3
  把 **Loomy(讯飞)**、**商汤小浣熊**、**Trae** 与 **华为云 CodeArts** 接进
4
4
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)。
5
5
 
6
- - **登录都在网页里完成,不用装客户端**:Loomy、小浣熊点「打开扫码登录」用微信扫码;
6
+ - **默认在网页里登录,不用装客户端**:Loomy、小浣熊点「打开扫码登录」用微信扫码;
7
7
  Trae 点「打开浏览器登录」在独立浏览器窗口里登录官网;CodeArts 点「打开浏览器登录」在华为云
8
8
  完成一次 OAuth 授权。插件自己接回结果,不读日常浏览器的 cookie,也不接收密码。
9
+ - **Trae 可选复用本机客户端**:本机安装了 Trae 且已登录时,Trae 卡片会多出「复用客户端登录态」;
10
+ 插件只读客户端的登录态,不修改也不退出客户端。签到只在用这种方式登录时显示。
9
11
  - **Trae 两种区域**:国内版(trae.cn)与国际版(trae.ai)各自保存账号,同一张卡片切换。
10
12
  - **额度可见**:Loomy 显示可用 / 永久 / 每日积分;小浣熊显示剩余积分;Trae 国内版与 CodeArts
11
13
  显示「已用 / 总额」进度条与资源包,国际版显示套餐与美元用量(如 `$0.00 / $1.00`)及重置时间。
@@ -17,8 +19,8 @@
17
19
 
18
20
  ## 为什么需要它
19
21
 
20
- 四家都没有面向个人用户的开放 API。网页版登录后的会话不能直接当模型网关凭据,桌面客户端
21
- 又把登录态锁在自己的文件里,插件拿不到可用的令牌。
22
+ 四家都没有面向个人用户的开放 API。网页版登录后的会话不能直接当模型网关凭据;桌面客户端的
23
+ 登录态通常锁在客户端自己的文件里,Trae 是唯一提供显式、只读复用的例外(见下文)。
22
24
 
23
25
  插件因此在宿主侧完成登录:Loomy 与小浣熊起一个本地登录页,页面上是真正的微信登录二维码;
24
26
  Trae 则启动一个**独立配置目录**的浏览器窗口,让用户在官网正常登录,再通过调试协议读回
@@ -26,9 +28,10 @@ cookie 与 `localStorage` 里的设备指纹。全程由宿主轮询,不依赖
26
28
  拿到凭据后,模型调用沿用各家的网页网关协议;插件负责把 DSH 的消息、图片、工具调用与思考流
27
29
  翻译成对应格式。
28
30
 
29
- Trae 的设备指纹(`x-device-id` / `x-machine-id`)只来自这次网页登录:插件不读本机 Trae
30
- 客户端的任何文件,更没有 CLI token 通道。登录拿到的网页 cookie 与短期 JWT 一起保存在插件
31
- 自己的账号文件里(仅当前用户可读),用于在 JWT 到期前自动续期,不会回写任何客户端凭据。
31
+ Trae 的网页登录把设备指纹(`x-device-id` / `x-machine-id`)和短期 JWT 一起保存在插件
32
+ 自己的账号文件里(仅当前用户可读),用于在 JWT 到期前自动续期。若本机 Trae 客户端已登录,
33
+ 插件也可以只读解析客户端自己的加密登录态,复用它的 JWT、16 位设备号与 machine id;这条路径
34
+ 不会回写客户端,也不会退出客户端。两种来源的令牌都只存在插件自己的账号文件里。
32
35
 
33
36
  CodeArts 走华为云 OAuth:浏览器里授权后,宿主拿到 AK/SK 与 STS 安全令牌,之后每个请求都用
34
37
  `SDK-HMAC-SHA256` 签名调用网关。它同样不读本机 CodeArts IDE 的任何文件,也不需要安装 IDE。
@@ -43,10 +46,14 @@ CodeArts 走华为云 OAuth:浏览器里授权后,宿主拿到 AK/SK 与 STS
43
46
  **设置 → Loomy** 或 **设置 → 商汤小浣熊** → 点「打开扫码登录」。浏览器会打开一个本地
44
47
  登录页,用微信扫码并在手机上确认即可;成功后账号、额度与模型列表随即刷新。
45
48
 
46
- **设置 → Trae** → 先选区域(国内版 / 国际版),再点「打开浏览器登录」。插件会启动一个
47
- 独立且持久的浏览器配置目录(不影响你自己的浏览器),在其中登录 Trae 官网;登录成功后窗口
48
- 自动关闭,账号、额度与模型列表随即刷新。之后约 8 小时到期的 JWT 会用这次登录的网页会话
49
- 自动续期。
49
+ **设置 → Trae** → 先选区域(国内版 / 国际版),然后二选一:
50
+
51
+ - 点「打开浏览器登录」:插件会启动一个独立且持久的浏览器配置目录(不影响你自己的浏览器),
52
+ 在其中登录 Trae 官网;登录成功后窗口自动关闭,账号、额度与模型列表随即刷新。之后约 8 小时
53
+ 到期的 JWT 会用这次登录的网页会话自动续期。
54
+ - 若本机已安装并登录 Trae 客户端,点「复用客户端登录态」:插件只读客户端自己的登录态,直接
55
+ 复用其 JWT、16 位设备号与 machine id。客户端退出登录或令牌过期后,先在客户端重新登录,再点
56
+ 一次「重新读取客户端登录态」即可。国内版的签到按钮只在这个方式下出现。
50
57
 
51
58
  **设置 → 华为云 CodeArts** → 点「打开浏览器登录」,在华为云页面完成授权。授权后浏览器回到
52
59
  本机,宿主接回凭据,页面无需复制任何内容。
@@ -55,10 +62,11 @@ CodeArts 走华为云 OAuth:浏览器里授权后,宿主拿到 AK/SK 与 STS
55
62
  账号侧的要求;绑定完成后才会下发 `session`。
56
63
  - **小浣熊**:二维码由官方服务端一次性签发,扫码确认后自动接回,没有额外表单;过期或已用过
57
64
  的码会明确提示,重新点一次即可换新码。
58
- - **Trae**:登录窗口使用插件自己的持久配置目录,再次登录会复用同一设备指纹;没有桌面客户端、
59
- 没有 CLI token,也不回写任何客户端凭据。网页会话约 14 天,期间 8 小时的 JWT 会自动续期;
60
- 只有网页会话失效才需要重新登录。区域在卡片上切换,两个区域的账号互不影响,切换回来仍是原账号。
61
- 国际版若提示 `account is at risk`,请用 Log in 登录已有账号(与你在 Edge 里能正常登录的
65
+ - **Trae**:两种登录方式。网页登录使用插件自己的持久配置目录,再次登录会复用同一设备指纹;
66
+ 网页会话约 14 天,期间 8 小时的 JWT 会自动续期,只有网页会话失效才需要重新登录。复用本机
67
+ 客户端登录态则直接读取客户端已保存的 JWT、16 位设备号与 machine id,不回写、不退出客户端;
68
+ 客户端自己续期,插件里的令牌过期后重新复用即可。区域在卡片上切换,两个区域的账号互不影响,
69
+ 切换回来仍是原账号。国际版若提示 `account is at risk`,请用 Log in 登录已有账号(与你在 Edge 里能正常登录的
62
70
  Google/GitHub 账号一致),不要用新邮箱注册;独立配置首次登录会被 Trae 视为新设备。
63
71
  - **CodeArts**:授权页是华为云自己的 OAuth portal,回调地址是 `127.0.0.1` 上的临时端口;
64
72
  凭据只在宿主侧保存,浏览器拿不到。STS 令牌两小时有效,到期自动续期;续期令牌失效(登录
@@ -70,12 +78,17 @@ CodeArts 走华为云 OAuth:浏览器里授权后,宿主拿到 AK/SK 与 STS
70
78
 
71
79
  ## 设置页
72
80
 
73
- - **登录卡**:显示账号、到期时间与额度;未登录时可发起登录,登录中可取消,也可退出登录。
74
- Trae 的卡片顶部是区域切换(国内版 / 国际版),CodeArts 与小浣熊另有「签到领积分」
75
- 按钮;签到状态按「今日已签到」(账号)与「本机今日已签到」(设备)分别显示。小浣熊没有独立签到接口:插件会
81
+ - **登录卡**:显示账号、到期时间、登录方式与额度;未登录时可发起登录,登录中可取消,也可退出登录。
82
+ Trae 的卡片顶部是区域切换(国内版 / 国际版),并提供网页登录与复用本机客户端两种方式;
83
+ Trae 国内版(仅客户端登录态)、CodeArts 与小浣熊另有「签到领积分」按钮;签到状态按
84
+ 「今日已签到」(账号)与「本机今日已签到」(设备)分别显示。小浣熊没有独立签到接口:插件会
76
85
  核对积分流水里的 `daily_grant`(只认今天的记录),并在到账时显示具体到账时间;未到账时调用
77
86
  网页当日首次加载用的 `setting_info` 尝试触发。「打开网页签到」链接无论是否已到账都始终保留。
78
87
  插件页还可设置自动监测间隔(关闭 / 15 分钟 / 30 分钟 / 1 小时 / 2 小时),关闭后只保留手动入口。
88
+ - **Trae 本机客户端登录**:检测到本机安装了 Trae CN / 国际版客户端时,登录卡会显示客户端账号与
89
+ 「复用客户端登录态」。插件只读 `User/globalStorage/storage.json` 里的加密登录态并在本机解密,
90
+ 不修改客户端,也不把令牌返回给页面;网页登录与客户端登录可以随时切换,退出插件登录不会退出
91
+ 客户端。签到按钮只出现在客户端登录态下。
79
92
  Trae 国内版与 CodeArts 的资源包默认只显示一行
80
93
  摘要(包数、合计额度、最近到期),点「详细信息」才展开每个包的已用 / 总额与到期时间,展开区限高可滚动。
81
94
  这两家的额度区顶部是「已用 / 总额」进度条;CodeArts 的账号名取自登录令牌里的用户信息,老账号
package/client.js CHANGED
@@ -117,9 +117,22 @@ window.__ModuleLoader__.load({
117
117
  raccoonIntro: '点“打开扫码登录”,用微信扫码登录小浣熊即可使用,无需安装客户端。改动即时生效,无需重启。',
118
118
  traeNav: 'Trae',
119
119
  traeTitle: 'Trae',
120
- traeIntro: '先选国内版或国际版,再点“打开浏览器登录”在 Trae 官网登录。设备指纹由这次网页登录生成,不需要安装桌面客户端。改动即时生效,无需重启。',
121
- traeHint: 'Trae 的网页令牌只有约 8 小时,插件会用登录时拿到的网页会话自动续期;网页会话失效后才需重新登录。',
122
- traeLoginMissing: 'Trae 尚未登录。请点“打开浏览器登录”。',
120
+ traeIntro: '先选国内版或国际版,再任选一种登录方式:在 Trae 官网网页登录,或复用本机 Trae 客户端的登录态。网页登录会用网页会话自动续期;只有复用客户端登录态时,国内版才显示签到按钮。改动即时生效,无需重启。',
121
+ traeHint: '网页登录的令牌约 8 小时,插件会用登录时拿到的网页会话自动续期;复用本机客户端登录态则直接使用客户端令牌,客户端退出登录或令牌过期后重新复用即可。签到仅在国内版、且使用客户端登录态时可用。',
122
+ traeLoginMissing: 'Trae 尚未登录。可以点“打开浏览器登录”,或复用本机已登录的 Trae 客户端。',
123
+ traeClientTitle: '复用本机客户端登录',
124
+ traeClientHint: '检测到本机已安装 Trae 客户端。这里只读取客户端当前的登录态,不会修改或退出客户端;复用后会替换插件里的当前登录态。用这种方式登录后才显示签到按钮。',
125
+ traeClientReady: '本机客户端已登录,可直接复用。',
126
+ traeClientNotLoggedIn: '检测到本机 Trae 客户端,但尚未登录。请先打开客户端完成登录,再点“重新读取登录态”。',
127
+ traeClientAccount: '客户端账号',
128
+ traeClientUse: '复用客户端登录态',
129
+ traeClientReuse: '重新读取客户端登录态',
130
+ traeClientBusy: '正在读取…',
131
+ traeClientFailed: '读取客户端登录态失败',
132
+ traeClientSource: '登录方式',
133
+ traeSourceWeb: '网页登录',
134
+ traeSourceClient: '本机客户端',
135
+ traeClientLogoutNote: '退出登录只会清除插件里的登录态,不会退出本机 Trae 客户端。',
123
136
  regionLabel: '区域',
124
137
  regionCn: '国内版 · trae.cn',
125
138
  regionGlobal: '国际版 · trae.ai',
@@ -281,9 +294,22 @@ window.__ModuleLoader__.load({
281
294
  raccoonIntro: 'Use “Open QR sign-in” and scan the code in WeChat; no client install needed. Changes apply immediately.',
282
295
  traeNav: 'Trae',
283
296
  traeTitle: 'Trae',
284
- traeIntro: 'Pick the mainland or international edition, then use “Open browser sign-in” to sign in on Trae’s website. The device fingerprint comes from that web session, so no desktop client is needed. Changes apply immediately.',
285
- traeHint: 'Trae’s web token lasts about 8 hours; the plugin renews it with the website session captured at sign-in, so you only sign in again when that session dies.',
286
- traeLoginMissing: 'Trae is not signed in. Use “Open browser sign-in”.',
297
+ traeIntro: 'Pick the mainland or international edition, then use either method: sign in on Trae’s website, or reuse the local Trae client’s sign-in state. A browser login renews itself; the mainland check-in button appears only with the client sign-in. Changes apply immediately.',
298
+ traeHint: 'A browser login uses the website session captured at sign-in; a client login uses the desktop client’s token and is imported again after the client refreshes or signs out. Check-in is mainland-only and client-login-only.',
299
+ traeLoginMissing: 'Trae is not signed in. Use browser sign-in, or reuse a signed-in local Trae client.',
300
+ traeClientTitle: 'Reuse the local client sign-in',
301
+ traeClientHint: 'A Trae desktop client was found. This only reads its current sign-in state and never modifies or signs out the client; reusing it replaces the plugin’s current login. The check-in button appears only after signing in this way.',
302
+ traeClientReady: 'The local client is signed in and ready to reuse.',
303
+ traeClientNotLoggedIn: 'A Trae desktop client was found but is not signed in. Sign in there, then re-read the login state.',
304
+ traeClientAccount: 'Client account',
305
+ traeClientUse: 'Reuse client sign-in',
306
+ traeClientReuse: 'Re-read client sign-in',
307
+ traeClientBusy: 'Reading…',
308
+ traeClientFailed: 'Could not read the client sign-in',
309
+ traeClientSource: 'Sign-in method',
310
+ traeSourceWeb: 'Browser',
311
+ traeSourceClient: 'Local client',
312
+ traeClientLogoutNote: 'Signing out only clears the plugin’s login; the local Trae client stays signed in.',
287
313
  regionLabel: 'Region',
288
314
  regionCn: 'Mainland · trae.cn',
289
315
  regionGlobal: 'International · trae.ai',
@@ -627,6 +653,7 @@ window.__ModuleLoader__.load({
627
653
  const [actionBusy, setActionBusy] = useState(false)
628
654
  const [actionError, setActionError] = useState(undefined)
629
655
  const [checkinBusy, setCheckinBusy] = useState(false)
656
+ const [clientBusy, setClientBusy] = useState(false)
630
657
  const [packsExpanded, setPacksExpanded] = useState(false)
631
658
  const ticket = useRef(0)
632
659
 
@@ -745,6 +772,23 @@ window.__ModuleLoader__.load({
745
772
  }
746
773
  }, [accountRequest, t])
747
774
 
775
+ const importClientLogin = useCallback(async () => {
776
+ setClientBusy(true)
777
+ setActionError(undefined)
778
+ try {
779
+ const payload = await accountRequest({ action: 'client-login' })
780
+ setStatus(payload)
781
+ setBrowser({ running: false, phase: 'idle' })
782
+ writeCache(cacheId, 'status', { ...payload, cachedAt: Date.now() })
783
+ onStatus?.(payload)
784
+ onAfterRefresh?.()
785
+ } catch (err) {
786
+ setActionError(`${t('traeClientFailed')}:${String(err?.message ?? err)}`)
787
+ } finally {
788
+ setClientBusy(false)
789
+ }
790
+ }, [accountRequest, cacheId, onStatus, onAfterRefresh, t])
791
+
748
792
  // Claiming is the only state-changing account action. The reward changes
749
793
  // the quota, so the reading is refreshed afterwards instead of guessed.
750
794
  const claimCheckin = useCallback(async () => {
@@ -887,12 +931,14 @@ window.__ModuleLoader__.load({
887
931
  )
888
932
  })()
889
933
 
890
- // CodeArts grants one reward per account per day, but its device flag can
891
- // also block the claim; the two reasons are shown apart so "本机已签"
892
- // never reads as "this account already claimed".
893
- const checkin = brand.id === 'codearts' || brand.id === 'raccoon'
894
- ? status?.checkin
895
- : undefined
934
+ // Trae's check-in needs the device id only a client-login import has, so
935
+ // a web login must not show the panel even though the provider exposes
936
+ // the method. CodeArts grants one reward per account per day, but its
937
+ // device flag can also block the claim; the two reasons are shown apart
938
+ // so "本机已签" never reads as "this account already claimed".
939
+ const checkin = brand.id === 'trae'
940
+ ? (status?.loginSource === 'client' ? status?.checkin : undefined)
941
+ : (brand.id === 'codearts' || brand.id === 'raccoon' ? status?.checkin : undefined)
896
942
  const checkinBlocked = checkin === undefined
897
943
  || checkin.available === false
898
944
  || checkin.claimed === true
@@ -923,7 +969,7 @@ window.__ModuleLoader__.load({
923
969
  ? h('div', { style: css.bar },
924
970
  h('button', {
925
971
  style: { ...css.btn, ...css.btnPrimary },
926
- disabled: checkinBlocked || checkinBusy || actionBusy,
972
+ disabled: checkinBlocked || checkinBusy || clientBusy || actionBusy,
927
973
  onClick: () => void claimCheckin(),
928
974
  }, checkinBusy ? t('checkinBusy') : checkinLabel),
929
975
  checkinNote === undefined ? null : h('span', { style: css.note }, checkinNote),
@@ -986,9 +1032,42 @@ window.__ModuleLoader__.load({
986
1032
  browser !== undefined && browser.running === false && browser.error !== undefined
987
1033
  ? h('div', { style: css.err }, browser.error)
988
1034
  : null,
989
- actionError !== undefined ? h('div', { style: css.err }, actionError) : null,
990
1035
  )
991
1036
 
1037
+ const clientLogin = brand.id === 'trae' ? status?.clientLogin : undefined
1038
+ const clientAccount = clientLogin?.signedIn === true
1039
+ ? ([clientLogin.name, clientLogin.expiresAt === undefined
1040
+ ? undefined
1041
+ : `${t('expires')} ${dateLabel(clientLogin.expiresAt)}`]
1042
+ .filter(part => typeof part === 'string' && part.length > 0)
1043
+ .join(' · ') || t('accountUnknown'))
1044
+ : undefined
1045
+ const clientPanel = brand.id === 'trae' && clientLogin?.available === true
1046
+ ? h('div', { style: css.loginBox },
1047
+ h('h4', { style: css.h4 }, t('traeClientTitle')),
1048
+ h('div', { style: css.note }, t('traeClientHint')),
1049
+ clientLogin.signedIn === true
1050
+ ? h('div', { style: css.line },
1051
+ h('span', { style: css.label }, t('traeClientAccount')),
1052
+ h('span', { style: css.value }, clientAccount),
1053
+ )
1054
+ : h('div', { style: css.note }, clientLogin.error ?? t('traeClientNotLoggedIn')),
1055
+ h('div', { style: css.bar },
1056
+ h('button', {
1057
+ style: { ...css.btn, ...(clientLogin.signedIn === true ? css.btnPrimary : {}) },
1058
+ disabled: clientBusy || checkinBusy || actionBusy || browserRunning || clientLogin.signedIn !== true,
1059
+ onClick: () => void importClientLogin(),
1060
+ }, clientBusy
1061
+ ? t('traeClientBusy')
1062
+ : status?.loginSource === 'client' ? t('traeClientReuse') : t('traeClientUse')),
1063
+ clientLogin.signedIn === true
1064
+ ? h('span', { style: css.note }, t('traeClientReady'))
1065
+ : null,
1066
+ ),
1067
+ h('div', { style: css.note }, t('traeClientLogoutNote')),
1068
+ )
1069
+ : null
1070
+
992
1071
  // Trae's region is a saved setting, not a tab: the selector writes the
993
1072
  // same `traeRegion` the provider reads, and the page then re-reads both
994
1073
  // the account and the catalog for the newly selected region.
@@ -1041,6 +1120,14 @@ window.__ModuleLoader__.load({
1041
1120
  h('span', { style: css.value }, dateLabel(status?.expiresAt)),
1042
1121
  )
1043
1122
  : null,
1123
+ signedIn && brand.id === 'trae'
1124
+ ? h('div', { style: css.line },
1125
+ h('span', { style: css.label }, t('traeClientSource')),
1126
+ h('span', { style: css.value }, status?.loginSource === 'client'
1127
+ ? t('traeSourceClient')
1128
+ : t('traeSourceWeb')),
1129
+ )
1130
+ : null,
1044
1131
  signedIn && brand.id === 'raccoon'
1045
1132
  ? h('div', { style: css.line },
1046
1133
  h('span', { style: css.label }, t('raccoonOwner')),
@@ -1068,6 +1155,8 @@ window.__ModuleLoader__.load({
1068
1155
  )
1069
1156
  : null,
1070
1157
  !signedIn || status?.possiblyExpired === true ? browserPanel : null,
1158
+ clientPanel,
1159
+ actionError !== undefined ? h('div', { style: css.err }, actionError) : null,
1071
1160
  error !== undefined ? h('div', { style: css.err }, `${t('loadFailed')}:${error}`) : null,
1072
1161
  )
1073
1162
  }