dsh-connect-trae 2.3.0 → 2.3.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 +81 -2
- package/README.en.md +61 -6
- package/README.md +61 -6
- package/lib/client.js +42 -9
- package/lib/index.d.ts +159 -11
- package/lib/index.js +192 -15
- package/package.json +3 -2
- package/scripts/verify-windows.mjs +311 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.1 (2026-09-26)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **Windows 真机验证脚本 `scripts/verify-windows.mjs`**:把「Windows 上到底能不能读到 Trae 登录」从一串手工步骤变成**一条命令给出结论**。
|
|
8
|
+
- **跑的是插件自己的构建产物**(`lib/`),不是重写一份逻辑——单元测试只能证明「代码在 Windows 上不崩」,因为 CI 的 windows-latest 上并没有装 Trae;真机验证要回答的是「这台机器上的 Trae 登录文件,插件读不读得到」,只有把真机文件交给真代码才成立。
|
|
9
|
+
- 报告四项:① 它在这台机器上探测的**每条路径 + 文件是否存在**;② 真实账号解析解出了几个账号;③ 即将发给 Trae 的设备指纹(`x-device-type` / `x-os-version` / `x-device-id` / `x-app-version`);④ 全部通过 / 几项未通过的结论。
|
|
10
|
+
- **输出默认脱敏,可直接贴公开 issue**:Windows 用户名替换为 `<user>`(`C:\Users\<真名>\...`)、账号名与设备号只报**形态**(长度 / 是否纯数字)。脱敏而非全删是关键——`Trae CN` vs `trae-cn` 这个目录拼写正是报告唯一要查的东西,抹掉它报告就没价值。
|
|
11
|
+
- 脱敏规则抽到 `src/redact.ts`(`maskUserPath` / `describeNameShape` / `describeIdShape`)并由脚本从插件导出中取用,避免「两处各写一版、改一处漏一处」。**+11 条单元测试**,既断言敏感值必须消失,也断言目录拼写/形态信息必须保留(只测「名字没了」的话,一个恒返回 `<redacted>` 的实现在测试下也会通过)。
|
|
12
|
+
- 随 npm 包分发(`files` 白名单新增该单文件 8.8KB):README 指引用户运行它,不随包则 npm 用户根本执行不了这一步。
|
|
13
|
+
- **非 Windows 平台会显式说明**:脚本在 macOS / Linux 上也能跑,但会标注「当前平台非 Windows,平台相关的失败属预期」,避免被误读成兼容性缺陷。
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **验证脚本在 Windows 上启动即崩,一条检查都跑不到**(在 Windows 真机上实测发现):`scripts/verify-windows.mjs` 的 `loadPlugin()` 写的是 `import(join(here, '..', 'lib', 'index.js'))`,把**路径**当成 **URL** 传给了 `import()`。Node 把 `C:\...` 解析成 scheme 为 `c:` 的 URL,抛 `ERR_UNSUPPORTED_ESM_URL_SCHEME`("On Windows, absolute paths must be valid file:// URLs")——**恰好在这个脚本唯一存在的目的平台上崩**。修法:改用 `pathToFileURL(local).href`。
|
|
18
|
+
- **为什么 CI 没拦住**:`.github/workflows/ci.yml` 只跑 typecheck / test / build,**从不执行这个脚本**;而 `tests/verify-windows.spec.ts` 测的是 `src/`,脚本是个独立的 `.mjs` 入口——「代码正确」与「入口能跑起来」是两件事,这条缝隙正好够一个致命启动错误通过。这不是笔误而是覆盖盲区:对**以单文件入口形式交付**的产物,测它所调用的库不等于测它本身。
|
|
19
|
+
- **回归测试**(+1 例,全仓 337 → 338):把脚本**复制到临时目录**并配一个桩 `lib/index.js`(令 `existsSync(local)` 为真,从而精确命中崩掉的那个分支),再以**子进程**执行它,断言 ① 不出现 `ERR_UNSUPPORTED_ESM_URL_SCHEME`、② 输出走到了「结论:」而非死在加载器里。用桩而非本仓 `lib/` 是刻意的:`lib/` 不进版本库,且 CI 在**测试之后**才构建,测试不能依赖产物已存在。
|
|
20
|
+
- **变异验证**:把脚本退回 `import(local)` → 该用例失败(`stdout` 为空);确认两条 stderr 断言也真的有约束力(同一变异下 `stderr` 确实含该错误码),不是空转。
|
|
21
|
+
- **`[2]` 段把「已成功解出账号的那个文件」报成 `invalid`**(同一轮真机演练发现):`TraeCredentialStore` 绑定单个 `storagePath` 时,会把该路径**同时当作桌面 `storage.json` 和 CLI token 文件各探一次**(覆盖路径的形态事先不可知,见 `candidates()`),因此直接打印 `diagnose()` 的 `failures` 会导致:① 每条路径重复出现两遍;② **刚刚成功解出账号的那个文件**被列为 `invalid`——一次按「不是存储文档」、一次按「不是 CLI token」。一台**唯一凭证已被成功找到**的机器,报告读起来却像有故障。修法:两段合并为**一次遍历**(`accounts()` 与 `diagnose()` 读同一批文件,探两遍纯属重复劳动),按路径**合并成一个结论**,命中账号的路径记为「已解出」;失败原因优先取更具体的一项(`missing` 是两种形态对不存在的文件的共同答案,非 `missing` 才说明读到了内容但被拒)。
|
|
22
|
+
- 实测修正前后:`invalid [solo] ...TRAE SOLO CN\...` + 每行重复两遍 → `已解出 [solo] ...TRAE SOLO CN\... (桌面)`,与「解出至少一个账号」的结论一致。
|
|
23
|
+
|
|
24
|
+
- **Windows 上只探测单一目录名,且这条路径从未在真机验证过**("项目要支持 Windows")。Trae 是 VS Code 系 Electron 应用,其每用户数据目录由安装器注册的产品名决定;插件此前在 Windows 上只按 macOS 的名字(`Trae CN` / `TRAE SOLO CN`)各探一条路径,猜错就直接「未登录」,而 `docs/WINDOWS_TOKEN_PROBE.md` 自己就写着「目录名是从 macOS 抄过来的,Windows 上实际名称未经确认」——即这条主路径从未验证,且失败时用户只看到「未登录」。
|
|
25
|
+
- **依据(2026-09-26 实测 macOS 包)**:`product.json` 的 `win32DirName` 才是 Windows 侧目录名的权威字段——`Trae CN` 为 `Trae CN`(`applicationName: trae-cn`)、`TRAE SOLO CN` 为 `TRAE SOLO CN`(`applicationName: trae-solo-cn`)。因此 macOS 拼写大概率正确,但同一产品族在 Linux 用的是小写 `applicationName` 拼写,Windows 实际写哪个未验证。
|
|
26
|
+
- **修法**:新增 `traeWindowsAppNames()`,Windows 侧改为**多候选并列探测**(`Trae CN` + `trae-cn`、`TRAE SOLO CN` + `trae-solo-cn`),与 Linux 侧既有的 `LINUX_APP_NAMES` 处理方式对齐;`src/identity.ts` 读 `product.json` 也共用同一份拼写表,避免「一个文件认这个名字、另一个不认」的不对称。**不再列出仅大小写不同的重复项**——Windows 文件系统不区分大小写,`trae cn` 与 `Trae CN` 是同一个目录,列两遍只会让探测清单和卡片上的「已检查的路径」重复一倍。
|
|
27
|
+
- **明确未解决的部分**:Windows 真机上的实际目录名**仍然没有验证**(本机为 macOS,无 Windows 环境)。多候选只是把「猜错即失败」降级为「多一次失败的 `readFile`」,并不是验证。`docs/WINDOWS_TOKEN_PROBE.md` 保留为待真机回报的清单,并把「第二步:确认数据目录名」标为仍需执行。
|
|
28
|
+
- **回归测试**(+2 例,全仓 319 → 322):`paths.spec.ts`「探测 Windows 两种拼写」「不列出仅大小写不同的重复项」;`identity.spec.ts`「备用拼写(`trae-solo-cn`)下也能读到 `product.json`」。变异验证:`paths.ts` 退回单一目录名 → 1 条失败;`identity.ts` 退回单一拼写 → 1 条失败。
|
|
29
|
+
|
|
30
|
+
- **`src/model-cache.ts` 的缓存路径与凭据路径各自为政,在真机上指向一个不存在的目录**(Windows 真机适配检查时发现,属**潜伏缺陷**):
|
|
31
|
+
- **症状**:该模块把 `state.vscdb` 路径**硬编码为单一拼写 `Trae CN`**(macOS 与 Windows 两分支都写死这一个名字),而 `paths.ts` 早已是**多候选拼写**探测(`Trae CN` / `trae-cn`、`TRAE SOLO CN` / `trae-solo-cn`)。它**自己重算了一遍目录**,没有复用 `traeStorageCandidates`——即 2.3.1 修「多候选」时只修了登录路径,漏了这个兄弟模块。
|
|
32
|
+
- **真机证据**(Windows 10.0.22621 + TRAE SOLO CN):真实数据目录是 `TRAE SOLO CN`,而旧代码拼出的是 `%APPDATA%\Trae CN\User\globalStorage\state.vscdb` —— **该文件根本不存在**(实测 `existsSync` 为 false,而 `TRAE SOLO CN\...\state.vscdb` 存在)。**即便装了 `sqlite3` 也读不到**,与「装了 `sqlite3` 且小写拼写才失效」的初步判断相比,实际失效面**更大**:只要装的不是目录名恰好为 `Trae CN` 的版本就一定失败。
|
|
33
|
+
- **为什么一直没被发现**:Windows 上 `sqlite3` 通常不存在,该调用**必然**先抛 `ENOENT`;调用方 `.catch(() => undefined)` 兜底后功能退化为「无缓存」,于是**一个错误的路径被另一个看似合理的错误(依赖缺失)掩盖**。这类「两个错误互相掩护」的情形,单看错误码永远排查不到——只有把期望的路径与磁盘实际路径对照才暴露。
|
|
34
|
+
- **修法**:不再拼写目录,改为**从凭据路径推导**——`state.vscdb` 与 `storage.json` 同在 `globalStorage`,故同一个候选目录既管登录也管缓存;`traeStateDatabaseCandidates()` 复用 `traeStorageCandidates()`(单一事实来源),`raw-resolver.ts` 把命中的候选传下去。**明确带 `candidate` 时只看该 edition**:多版本共存时读另一个安装的数据库会拿到**别的账号**的模型表。选择顺序是「先看存在的,都不存在才用最可能的那条」,这样 `sqlite3` 缺失时调用方仍能看到**真实**失败,而不是被换成一个猜出来的错误。
|
|
35
|
+
- **回归测试**(+7 例,全仓 338 → 345):① 每个凭据候选的兄弟 `state.vscdb` 都必须可探测(**断言「与凭据表同源」而非硬编码期望字符串**——跟着写死拼写的测试会与 bug 一致地通过);② Windows 上必须能找到 `TRAE SOLO CN`(旧的写死实现永远够不到);③ 带 `candidate` 时它排第一且不越出自己的 edition;④ 大小写不同不重复列;⑤ macOS / Linux 也从各自凭据表推导;⑥ **选中的路径真的传给了执行器**(用注入的 `runSqlite` 断言 argv,否则「列表正确但查错条目」无法被发现);⑦ 全都不存在时仍发起一次查询以暴露真实失败。
|
|
36
|
+
- **变异验证**:把实现退回「硬编码单一拼写」→ **9 条中 5 条失败**;恢复后全绿。(首版变异脚本因文件是 CRLF 而静默未生效,导致测试「通过」——已加断言令变异未落地时**直接报错**,否则这种假阴性会被误读成「测试抓不到这个 bug」。)
|
|
37
|
+
- **顺带修掉一个测试自身的问题**:⑥ 首版用 `path.includes('Library/Application Support')` 断言 macOS 路径,而 `join()` 在 Windows 上产出反斜杠——该测试在作者机器(macOS)通过而会在 CI 的 `windows-latest` 上失败。现先归一化分隔符再断言,这正是本项目「Windows 必须真机/win runner 验证」这条约定要防的错。
|
|
38
|
+
|
|
39
|
+
- **切换账号后无法领取签到:把「本机已签到」误判成「该账号已领取」**(用户上报)。2.3.0 的卡片与路由把 `checked_in` 与 `did_checked_in` **一并**当作「今日已领取」,于是切换账号后新账号显示「今日已领取」、按钮置灰、点不动。
|
|
40
|
+
- **根因(2026-09-26 实测,两个 CN 账号 + 同一台机器 + 同一北京日)**:两个字段判定维度不同——`checked_in` 是**账号**口径(该账号今天的奖励包已存在),`did_checked_in` 是**设备**口径(这台机器的今日名额已用掉,由 `x-device-id` 决定,与是哪个账号用的无关)。切换账号后正是后者:设备名额已用,而当前账号**没有拿到任何奖励**——此时说「今日已领取」把「没领到」说成了「已领到」。(详见 `docs/USAGE_API_RESEARCH.md` 新增的「决定性修正」一节,含四组请求头对照表。)
|
|
41
|
+
- **实测要点**:账号 A(当天已领取)与账号 B(当天未领取,无 `checkin_20260926_*` 奖励包)带**同一真实设备号**读状态,两者都返回 `did_checked_in:true`;不带该头、带合成号、或带本机另一安装的设备号,两者都返回 `false`;同一合成号连读三次也不会变 `true`(所以它不是「读过就算」的标记)。账号 B 带真实设备号 `claim` → `{"code":9095,"message":"当前设备今日已经签到,请明日再来哦~"}`,**未发放**,且 `total/consumed` 前后逐字节相同(`1900 / 1522.85`)。官方 NLS 文案 `2345 -> 该设备今日已参与签到` 亦为设备口径。
|
|
42
|
+
- **修法**:`checked_in` 成为唯一的「今日已领取」(账号口径)判据——已领取才直接返回 `alreadyCheckedIn` 且不发请求;`did_checked_in` 且未领取时,路由返回新的 `deviceCheckedIn: true`(不再谎报 `alreadyCheckedIn`),卡片保持按钮禁用但改说「本机今日的签到已用掉」并说明可换设备 / 明天再来;上游以 `9095` 拒绝时(窗口竞争)同样按 `deviceCheckedIn` 处理,**不当成失败**——那是一条上游规则,不是插件故障。
|
|
43
|
+
- **回归测试**(+4 例,全仓 316 → 319):路由侧「设备已签但账号未领 → 不发请求且返回 `deviceCheckedIn:true` / `alreadyCheckedIn:false` / `code:9095`」「上游 9095 → 按设备已满上报而非错误」,卡片侧「设备已签不显示『今日已领取』且显示设备说明」「路由回 9095 时显示设备说明而非『签到失败』」。做过**变异验证**:把路由的 `checkedIn || didCheckedIn` 复原 → 1 条失败;把卡片的两个字段合并 → 1 条失败;删掉路由的设备守卫(照发请求)→ 1 条失败。
|
|
44
|
+
|
|
45
|
+
### Tests
|
|
46
|
+
|
|
47
|
+
- **Windows 真机验证脚本的三处可用性缺陷**(在真正按「Windows 用户拉代码 → 跑脚本」的流程演练时发现,全部是脚本自身的问题):
|
|
48
|
+
- **空机器误报「解出 1 个账号」**(最严重):`TraeCredentialStore` 不传 `storagePath` 时会按 `process.platform` 扫描**宿主**目录,于是在一个刻意清空的 HOME 上,脚本从开发者本机解出了凭证——即「断言在一台没有任何 Trae 的机器上找到了 Trae」。这是本脚本可能产出的最误导性结果,因为它恰好会被读成「Windows 能用了」。修法:每个候选都**绑定到它自己的路径**,绝不回退到宿主扫描;并新增 4 条回归测试(`tests/verify-windows.spec.ts`)锁死该性质,变异验证:让 store 忽略固定路径 → 2 条失败。
|
|
49
|
+
- **`[1]` 与 `[2]` 可能描述不同机器**:`[1]` 列的是 win32 候选,`[2]` 却扫宿主平台目录。现在两段同源,且 `[2]` 逐候选诊断并给出**命中路径**归属。
|
|
50
|
+
- **未构建时的报错不可用**:`lib/` 不进版本库,拉代码后直接跑脚本会撞 `ERR_MODULE_NOT_FOUND` 堆栈。现在明确提示「这是源码 checkout、需要先 `pnpm install && pnpm run build`」,并以退出码 1 结束(实测)。
|
|
51
|
+
- **CLI-only 场景的两处误判**:① 身份解析误把 CLI token 当 `storage.json` 解析而抛 JSON 错——`resolveTraeIdentity` 的 CLI 兜底**只在文件不存在时**触发,故 CLI 候选改用 `readTraeCliIdentity` 直连;② `x-app-version` 缺失被计为失败,但那只是少一个请求头(登录/聊天/签到均不受影响),会让一台完全可用的机器报「未通过」,现改为 `[INFO]`,并对 CLI 候选改用不同措辞(CLI 从 `ide_version.json` 取版本,而非 `product.json`)。
|
|
52
|
+
- **修复 Windows CI 的冷启动超时抖动**(CI #60 失败,同一份代码在 #59 / #61 通过)。`windows-latest` 上每个测试文件的**第一个**用例会吃到一次性的模块转换成本,超过 vitest 默认的 5000ms:
|
|
53
|
+
- 实测(#60,该次仅改 `.md`,代码与通过的那次逐字节相同):`identity.spec.ts` 首个用例 5179ms 超时、同文件后续用例 6–60ms;`card-checkin.spec.tsx` 首个用例 9435ms 超时、后续 50–196ms;该次总计 `transform 17.71s / import 24.28s`。
|
|
54
|
+
- **判定为抖动而非缺陷的依据**:只有每个文件的第一个用例受影响,同文件其余用例均在毫秒级;且同一份代码在前后两次 CI 均通过。
|
|
55
|
+
- **修法**:`vitest.config.ts` 设 `testTimeout: 20_000`,**不动任何断言**。已验证超时机制仍生效(用一个必然挂起的探测用例确认仍会失败,探测文件随后删除)。
|
|
56
|
+
- **明写在配置注释里**:不得用「放宽断言」的方式消除超时——若某个原本很快的用例在**所有**平台上都开始超时,那是真实回归,改这个数字不是答案。
|
|
57
|
+
|
|
58
|
+
### Docs
|
|
59
|
+
|
|
60
|
+
- **Windows 真机验证取得首个通过结果,README / 验证指引的「未验证项」措辞随之收敛**:此前 README(中英双语)与 `docs/WINDOWS_VERIFY_GUIDE.md` 都以「Windows 上 Trae 数据目录的实际名称尚未在真机确认过」为前提写成,而现在已有真机证据,把这句话留着就是反向的失真(本项目对「未验证项不得写成既成事实」有明确要求,反过来把**已验证项**写成未验证同样会误导)。
|
|
61
|
+
- **实测结果**(Windows 10.0.22621 + TRAE SOLO CN,`node scripts/verify-windows.mjs` 退出码 0):命中 `%APPDATA%\TRAE SOLO CN\User\globalStorage\storage.json`;解出 1 个账号(`solo`/`cn`);`x-device-type: windows`;`x-os-version: Windows 10.0.22621`;`x-device-id` 为 16 位纯数字(来自数据目录,**非**兜底哈希);`x-app-version: 0.1.56`(取自 `%LOCALAPPDATA%\Programs\TRAE SOLO CN\resources\app\product.json`)。
|
|
62
|
+
- **确认的是**:`product.json` 的 `win32DirName` 作为 Windows 目录名依据**是对的**——`TRAE SOLO CN` 确实是真机上的实际目录名;安装路径推导与设备指纹四个字段也都成立。即「Windows 上插件读得到 Trae 登录」由真机证据支持,不再是推断。
|
|
63
|
+
- **仍然未验证的是**:`Trae CN` / `trae-cn`(中国版)两个拼写——那台机器只装了 SOLO 版。README 与验证指引都**明确保留**这一条并说明「它在此机器上不存在只反映本机没装该版本,不代表拼写有误」,避免读者把「本机没命中」误读成「拼写错了」。
|
|
64
|
+
- 因此平台支持表的 Windows 行由「目录名见下方『未验证项』」改为「**已在一台真机通过验证**」,并新增「真机验证结果」小节逐项列出实测值;`docs/WINDOWS_VERIFY_GUIDE.md` 删去「本机(macOS)无法执行这一步」的旧前提,改为状态节 + 实测输出样例;`docs/WINDOWS_TOKEN_PROBE.md` 文首改用状态更新说明,并在第二节与「已知问题」逐条标注哪些已由真机确认。
|
|
65
|
+
- `docs/WINDOWS_VERIFY_GUIDE.md` 的样例输出**改为真实输出的形态**(含 `[2]` 段一行一个候选的写法与退出码 0/1/2 的含义),此前的样例是示意性的,与实际输出格式已有偏差。
|
|
66
|
+
- **验证脚本的检查范围被明确划出,并补跑脚本之外的主流程实测**:`verify-windows.mjs` 的六项**只覆盖路径探测与设备指纹**,把它读成「整个插件在 Windows 上没问题」是过度外推。因此同一台真机上另外直接调用 `lib/` 导出跑了主流程:
|
|
67
|
+
- `TraeCredentialStore.resolve()` ✅ 返回凭据(host `https://api.trae.cn`);`identityHeaders()` ✅ 10 个请求头全部生成;只读用量查询 ✅ 9 个积分包(总额 2050 / 已用 1522.85);只读模型目录 ✅ **42 个模型**;`writeFileAtomic` ✅ 写入回读一致;并发 `withFileLock` ✅ **8 并发串行化且无丢失更新**(此前 README 只写「宿主原生处理 Windows 独占创建语义」,现改为**实测成立**);`SSE` 解码 ✅ 按 `\r?\n` 切分,CRLF 与分块均支持。
|
|
68
|
+
- **实测确认了一处已知限制的真实行为**:`model-cache` 的 `sqlite3` 在本机不存在,抛 `ENOENT`(`spawn sqlite3 ENOENT`),调用方 `.catch(() => undefined)` 正确兜底,主流程不受影响——README 原写「会失败并安全回退」,现补上错误码作为依据。
|
|
69
|
+
- **顺带发现并修掉一处真实精度缺口**(详见下方 Bug Fixes):`src/model-cache.ts` 在 Windows 上把 `state.vscdb` 路径**硬编码为单一拼写 `Trae CN`**(`paths.ts` 是多候选 `Trae CN` / `trae-cn`),且该模块自己重算了一遍目录而没复用 `traeStorageCandidates`。本机真实目录名是 `TRAE SOLO CN`,旧代码指向的 `...\Trae CN\...\state.vscdb` **根本不存在**;只因 `sqlite3` 缺失、该调用必然失败并兜底,才把一个**错误的路径**藏在「依赖缺失」这个看似合理的错误后面。
|
|
70
|
+
- 以上「脚本之外」的实测结果同时写入 README(中英)与 `docs/WINDOWS_VERIFY_GUIDE.md`,与脚本自身那六项**分表列出**,不让读者混淆两者的证据强度。
|
|
71
|
+
- **`docs/WINDOWS_TOKEN_PROBE.md` 的「已知的其他 Windows 问题」整节已过期**:其中两条(`product.json` 硬编码 macOS 路径、`osVersion` 拼成 `win32 <release>`)**在代码里早已修复**,文档却仍写成未解决的缺陷——按它排查会把 Windows 用户引向错误方向。已复核并改写为「已处理 + 当前实现」,同时补上「Windows 安装/数据目录名仍未验证」这一条真实缺口,以及 `mode: 0o600` 在 Windows 被忽略属已知无害。
|
|
72
|
+
- 该文档**改为「先跑脚本」**:手工五步降级为「脚本跑不起来时的备选路径」,并把第二步(数据目录名)更新为当前的**多候选拼写**清单与依据,明确「这一条是本次要查的核心」。
|
|
73
|
+
- **README 明确 Windows 支持,并把「以后都要考虑 Windows」固化为开发约定**("我们的项目要支持 Windows 的" / "之后都要考虑对 Windows 的影响")。
|
|
74
|
+
- README(中英双语)新增**平台支持表**(macOS ✅ 完全支持 / Windows ✅ 支持 / Linux ✅ 支持),并在开头段落明示「支持 macOS、Windows 与 Linux」+ CI 双平台矩阵,让 Windows 支持成为对外可见的承诺而非隐含行为。
|
|
75
|
+
- 「Windows 说明」节**改写为与代码逐条对应**:账号目录(多候选拼写探测 + 与安装目录无关)、应用版本头(含 `LOCALAPPDATA` 回退)、设备指纹(`x-device-type: windows`、`Windows <release>`)、文件锁(宿主原生处理)、Raw Chat 的 `sqlite3` 限制、权限位被忽略。**每条都核对了代码位置后写入**。
|
|
76
|
+
- **删掉一句未经验证的断言**:原文写「目录名与 macOS 一致,**无需额外配置**」——而 `docs/WINDOWS_TOKEN_PROBE.md` 自己就说该目录名从未在真机确认。这正是本项目反复强调的「不得把未验证项写成既成事实」,故改为「未验证项(欢迎回报)」小节,说明依据是 `product.json` 的 `win32DirName`、兜底是 `applicationName` 拼写,并给出回报路径与 `authFile` 临时绕法。
|
|
77
|
+
- `DEVELOPMENT.md` 新增「**多平台要求(Windows 为长期支持目标)**」一节:给出平台相关面/无关面的分区表(路径解析与设备指纹为相关面,其余共用),以及 6 条改动强制检查项——新增路径必须带平台分支且多候选、不得用仅大小写不同的重复候选、新增外部命令依赖须确认 Windows 行为并登记限制、CI 必须在 `windows-latest` 通过、平台结论必须区分已验证/未验证、文档同步。这条约定使「考虑 Windows 影响」成为可执行的检查项而非口头要求。
|
|
78
|
+
- **顺带修掉一处链接会在 npm 页 404 的问题**:本次新增的「未验证项」指引用户去看 `docs/WINDOWS_TOKEN_PROBE.md`,但 `package.json` 的 `files` 白名单(`RELEASING.md` 有明确记载,属有意的包体控制)只放行 `docs/assets/` 下那张截图,**所有 `docs/*.md` 都不进 npm 包**——写成相对 Markdown 链接会在 npm 页面上 404。既有引用(`docs/IMPLEMENTATION_PLAN.md` 等)用的是反引号纯文本故不受影响,本次改为指向 GitHub 的绝对链接(实测 HTTP 200)。
|
|
79
|
+
- `RELEASING.md` 的打包内容清单同步更新(新增 `scripts/verify-windows.mjs`),并记下两条纪律:该脚本必须随包(否则用户无法执行验证步骤)、`docs/*.md` 不进包故 README 引用必须用绝对链接。
|
|
80
|
+
|
|
81
|
+
|
|
3
82
|
## 2.3.0 (2026-09-25)
|
|
4
83
|
|
|
5
84
|
### Breaking Changes
|
|
@@ -93,8 +172,8 @@
|
|
|
93
172
|
- **领取前的守卫比上游更强**:路由 `POST /plugins/dsh-connect-trae/checkin` 仅接受 POST、仅接受回环来源、仅服务 `cn` 区域,并且**先读状态**——已领取(`checked_in` 或 `did_checked_in` 任一为真)就直接返回 `alreadyCheckedIn` 且**根本不发领取请求**;活动关闭答 409。国际版直接答 404 并说明原因,而不是把上游的 HTML 404 当成网络故障抛给用户。
|
|
94
173
|
- **已实测上游按北京自然日幂等**:重复领取仍回 `code: 0`,而权益总额(`total=1600, consumed=390.8`)逐字节不变,**不会重复发放**。插件仍然自行守卫——「不会重复发放」是上游的行为,不是插件可以依赖的保证。
|
|
95
174
|
- **业务拒绝不当成故障**:`claimed: false` + 原始业务码与 message 原样回给卡片并显示出来,而不是包成 500。否则用户只会看到「领取失败」,而丢掉唯一能解释原因的 `9004`。
|
|
96
|
-
- **对齐官方客户端的状态判定**:`checked_in` 与 `did_checked_in`
|
|
97
|
-
- 卡片新增 8 条真实点击测试(`tests/card-checkin.spec.tsx`,渲染**真组件**、`fireEvent.click` 真按钮、断言真发出去的请求),`usage.spec.ts` 补 6 条、`web-status.spec.ts` 补 7 条,共 21 条新测试(全仓 261 → 282)。做过**变异验证**:去掉路由的 `did_checked_in` 守卫 → 2 条失败;去掉卡片的同款判定 → 1 条失败;把卡片的 `POST` 改成 `GET` → 1 条失败;删掉「已领取则不发请求」的守卫 → 2
|
|
175
|
+
- **对齐官方客户端的状态判定**:`checked_in` 与 `did_checked_in` 是两个独立字段,官方在「今日已领取」判定上也参考后者,插件据此保持按钮禁用。~~插件在卡片与路由两侧都按同一规则判定~~ —— **注:这里的口径在 2.3.1 被修正**。当时把两者一并当作「今日已领取」是**错的**:`did_checked_in` 实际是**设备**口径(`x-device-id`),不是账号口径,切换账号后会谎报「已领取」。以 2.3.1 为准;`extra_credits` 的展示(避免把当天实际到账的 200 说成 150)保持不变。
|
|
176
|
+
- 卡片新增 8 条真实点击测试(`tests/card-checkin.spec.tsx`,渲染**真组件**、`fireEvent.click` 真按钮、断言真发出去的请求),`usage.spec.ts` 补 6 条、`web-status.spec.ts` 补 7 条,共 21 条新测试(全仓 261 → 282)。做过**变异验证**:去掉路由的 `did_checked_in` 守卫 → 2 条失败;去掉卡片的同款判定 → 1 条失败;把卡片的 `POST` 改成 `GET` → 1 条失败;删掉「已领取则不发请求」的守卫 → 2 条失败。(其中针对 `did_checked_in` 的两条断言在 2.3.1 按其真实语义重写。)
|
|
98
177
|
- 取证与接口契约:`docs/USAGE_API_RESEARCH.md` 新增「每日签到领取」一节(含决定性请求头对照表、幂等性实测表、区域限制与官方字段语义)。
|
|
99
178
|
|
|
100
179
|
## 2.0.6 (2026-09-22)
|
package/README.en.md
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle plugin that connects locally signed-in Trae models (**both CN and international installs**) to the DSH model picker. Trae generates structured tool calls while DSH executes its own local tools, with a usage overview (Work/general credits on CN, subscription status on international), daily check-in claiming, and a model-management panel. **The CN and international sides are two parallel providers (`trae` / `trae-global`) that can be used at the same time**; the settings card separates them with tabs for convenient management.
|
|
28
28
|
|
|
29
|
+
**Runs on macOS, Windows, and Linux**: CI runs the full typecheck / test / build matrix on `ubuntu-latest` and `windows-latest`, and Windows gets purpose-built handling for directory probing, app version headers, and lock behavior — see [Windows notes](#windows-notes).
|
|
30
|
+
|
|
29
31
|
> Apart from the daily check-in claim, every query this plugin makes is read-only and consumes nothing; the claim only happens when you click the button.
|
|
30
32
|
|
|
31
33
|
## Features
|
|
@@ -39,7 +41,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
39
41
|
- **Region-isolated directories and selections** — CN and international each keep their own model directory, enabled picks, image opt-ins, and context budgets, and each provider reads only its own slot.
|
|
40
42
|
- **Account switching** — refreshes the token list and lets users select an account without storing tokens in DSH settings.
|
|
41
43
|
- **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.
|
|
42
|
-
- **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
|
|
44
|
+
- **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.
|
|
43
45
|
- **Secure loopback shim** — one random port + in-process random secret per region; the real Trae token is never handed to pi-ai.
|
|
44
46
|
|
|
45
47
|
## How it works
|
|
@@ -57,7 +59,7 @@ DSH PiAiAdapter (one stack per provider)
|
|
|
57
59
|
|
|
58
60
|
The CN and international sides each own a complete runtime stack — credential store, model catalog, wire map, upstream clients, loopback shim, adapter — with visibility filtered by the credential's own region claim, so **both regions' accounts can be signed in and used by different sessions at the same time**.
|
|
59
61
|
|
|
60
|
-
Usage overview hits the read-only `https://api.trae.cn/trae/api/v2/pay/*` and `/trae/api/v2/ug/*` endpoints (international accounts read their own gateway's subscription status); the daily check-in claim is the one write among them, going to `POST /trae/api/v2/ug/checkin_credits/claim` with this installation's device id (`x-device-id`, the same identity the chat path sends). Refreshed tokens are kept per region in `$DSH_HOME/.trae-auth.cn.json` and `$DSH_HOME/.trae-auth.ai.json` (two simultaneously signed-in accounts never overwrite each other; the legacy single file `.trae-auth.json` is still read as a migration source).
|
|
62
|
+
Usage overview hits the read-only `https://api.trae.cn/trae/api/v2/pay/*` and `/trae/api/v2/ug/*` endpoints (international accounts read their own gateway's subscription status); the daily check-in claim is the one write among them, going to `POST /trae/api/v2/ug/checkin_credits/claim` with this installation's device id (`x-device-id`, the same identity the chat path sends). That device id is also what Trae keys the "one check-in per device per day" rule on, so switching accounts does not reset the day's slot. Refreshed tokens are kept per region in `$DSH_HOME/.trae-auth.cn.json` and `$DSH_HOME/.trae-auth.ai.json` (two simultaneously signed-in accounts never overwrite each other; the legacy single file `.trae-auth.json` is still read as a migration source).
|
|
61
63
|
|
|
62
64
|
> See `docs/IMPLEMENTATION_PLAN.md`, `docs/SOLO_ROUTE_DECISION.md`, `docs/USAGE_API_RESEARCH.md`.
|
|
63
65
|
|
|
@@ -106,12 +108,65 @@ Restart the DSH process after install/update/uninstall.
|
|
|
106
108
|
- **Schema writable marker**: writable fields must be marked `volatile()` (`asVolatile`), or 0.1.7's write gate rejects every write; 0.1.7 delivers config values as `{get(): T}` live references, so every read and merge path unwraps them first (`unwrapVolatile` / `unwrapVolatileDeep`).
|
|
107
109
|
- **The collapse caret is pure CSS**, with no static primitives icon import — icon naming changes across releases, so a static import is not a stable contract.
|
|
108
110
|
|
|
111
|
+
## Platform support
|
|
112
|
+
|
|
113
|
+
| Platform | Status | Notes |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| **macOS** | ✅ Fully supported | Development and verification environment; directory names confirmed on a real host |
|
|
116
|
+
| **Windows** | ✅ Supported | CI runs `windows-latest`; account directory, app version headers, and lock behavior all handled. **Verified on a real host** (Windows 10.0.22621 + TRAE SOLO CN) — see below |
|
|
117
|
+
| **Linux** | ✅ Supported | Includes the WSL2 + Trae CLI case (issue #5); directory names probed as multiple candidates |
|
|
118
|
+
|
|
119
|
+
All three platforms share one implementation: account discovery, decryption, region routing, check-in, and usage queries are platform-independent. Platform differences are confined to **path resolution** (`src/paths.ts`) and the **device fingerprint** (`src/identity.ts`).
|
|
120
|
+
|
|
109
121
|
## Windows notes
|
|
110
122
|
|
|
111
|
-
- **Account data directory**: the plugin reads `%APPDATA
|
|
112
|
-
- **App version headers**:
|
|
113
|
-
- **
|
|
114
|
-
-
|
|
123
|
+
- **Account data directory**: the plugin reads `%APPDATA%\<directory name>\User\globalStorage\storage.json` (unrelated to the **install** directory — this Electron family keeps user data under `%APPDATA%` even when installed on another drive). It probes several directory spellings **in parallel** (`Trae CN` and `trae-cn`, `TRAE SOLO CN` and `trae-solo-cn`), and any hit works. Windows file systems are case-insensitive, so the same directory is never probed twice.
|
|
124
|
+
- **App version headers**: read from `<LOCALAPPDATA>\Programs\<install directory name>\resources\app\product.json` (falling back to `<home>\AppData\Local` when `LOCALAPPDATA` is unset) and sent as `x-app-version` / `x-ide-version`; if unreadable those headers are omitted and nothing else is affected.
|
|
125
|
+
- **Device fingerprint**: `x-device-type` is sent as `windows` and `x-os-version` as `Windows <release>`. The machine id comes from the data directory's `telemetry.machineId` (or a sibling `machineid` file), which holds on Windows.
|
|
126
|
+
- **File locking**: the host's `dsh-atomic-write` handles Windows exclusive-create semantics (`EPERM` / `EBUSY` retries) natively; no plugin-side adaptation is needed.
|
|
127
|
+
- **Raw Chat probing (known limitation)**: `model-cache` depends on the `sqlite3` command-line tool, which is not installed by default on Windows; that probe fails and **falls back safely**. Raw Chat is off by default and does not affect the main flow.
|
|
128
|
+
- **Permission bits**: credential copies are written with `mode: 0o600` / `dirMode: 0o700`, which Windows ignores — no error, documented as harmless.
|
|
129
|
+
|
|
130
|
+
### Real-host verification result (2026-09-26, Windows 10.0.22621 + TRAE SOLO CN)
|
|
131
|
+
|
|
132
|
+
`node scripts/verify-windows.mjs` passed **everything** (exit code 0):
|
|
133
|
+
|
|
134
|
+
| Check | Observed |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| Directory probe | Hit `%APPDATA%\TRAE SOLO CN\User\globalStorage\storage.json` (all other candidates missing) |
|
|
137
|
+
| Account resolution | 1 account resolved, `solo` / `cn` region |
|
|
138
|
+
| `x-device-type` | `windows` ✅ |
|
|
139
|
+
| `x-os-version` | `Windows 10.0.22621` ✅ |
|
|
140
|
+
| `x-device-id` | 16 digits, all numeric (read from the data directory, not the fallback hash) ✅ |
|
|
141
|
+
| `x-app-version` | `0.1.56`, read from `%LOCALAPPDATA%\Programs\TRAE SOLO CN\resources\app\product.json` ✅ |
|
|
142
|
+
|
|
143
|
+
**What this confirms**: using `win32DirName` as the authority is **correct** — the `TRAE SOLO CN` spelling really is the directory name on a real Windows host, and both the `product.json` install-path derivation and every device-fingerprint field hold. In other words "the plugin can read Trae's sign-in on Windows" is now backed by real evidence rather than inference.
|
|
144
|
+
|
|
145
|
+
#### Runtime paths beyond the script (same host, 2026-09-26)
|
|
146
|
+
|
|
147
|
+
The six checks above cover **path probing and the device fingerprint only** — that is not the same as "the whole plugin works on Windows". So the plugin's main flows were exercised separately, calling the `lib/` exports directly (i.e. the product code itself):
|
|
148
|
+
|
|
149
|
+
| Runtime path | Observed |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `TraeCredentialStore.resolve()` | ✅ returned a credential, host `https://api.trae.cn`, token expiry parsed |
|
|
152
|
+
| `identityHeaders()` (all headers) | ✅ all 10 headers produced (`x-device-id` / `x-machine-id` / `x-device-cpu` / `x-ide-version-code`, …) |
|
|
153
|
+
| Usage query (read-only, consumes no credits) | ✅ 9 credit packs, total 2050 / consumed 1522.85 |
|
|
154
|
+
| Model directory fetch (read-only) | ✅ **42 models** returned |
|
|
155
|
+
| `writeFileAtomic` | ✅ write + read-back identical |
|
|
156
|
+
| Concurrent `withFileLock` | ✅ 8 concurrent writers serialized correctly with **no lost updates** (Windows exclusive-create semantics are handled by the host — now verified rather than assumed) |
|
|
157
|
+
| `SSE` decoding | ✅ splits on `\r?\n`, so CRLF and chunk splits are both handled |
|
|
158
|
+
|
|
159
|
+
In short: sign-in, identity, usage, model directory, and concurrent writes are all **verified working on Windows**.
|
|
160
|
+
|
|
161
|
+
> **Known limitation (its behaviour confirmed on this host)**: `model-cache` depends on the `sqlite3` command-line tool, which is not installed here; it threw `ENOENT` (`spawn sqlite3 ENOENT`) and the caller's `.catch(() => undefined)` fell back correctly — **the main flow is unaffected**, and Raw Chat is off by default.
|
|
162
|
+
>
|
|
163
|
+
> **That module's directory-spelling gap has been fixed** (found and fixed in the same round): it used to **hardcode the single spelling `Trae CN`** for the `state.vscdb` path while `paths.ts` probes multiple candidates, and it re-derived the directory instead of reusing `traeStorageCandidates`. On this host — whose real directory name is `TRAE SOLO CN` — the old code pointed at `...\Trae CN\...\state.vscdb`, which **does not exist at all**; only because `sqlite3` is absent did that call necessarily fail and fall back, hiding a **wrong path** behind a different, plausible-looking error (a missing dependency).
|
|
164
|
+
>
|
|
165
|
+
> The fix derives the cache path **from the credential path**: `state.vscdb` and `storage.json` share one `globalStorage` directory, so the candidate that yields the sign-in also owns the cache. `raw-resolver` now passes the resolved candidate down, so a machine with several editions installed cannot read another installation's model map. Re-measured on the real host: the new code selects `%APPDATA%\TRAE SOLO CN\User\globalStorage\state.vscdb` (which exists), where the old code only ever looked at `Trae CN` (which does not).
|
|
166
|
+
|
|
167
|
+
> **What remains unverified**: this host only had TRAE SOLO CN installed, so the `Trae CN` / `trae-cn` spellings are **still unconfirmed on a real host** — their absence here only means this build is not installed, not that the spelling is wrong. Windows users running Trae China Edition are still welcome to run the command above and report.
|
|
168
|
+
|
|
169
|
+
> **Running Trae China Edition, or a different setup? One command reports it**: `node scripts/verify-windows.mjs` — it uses the plugin's own build output to list every path it probes on this machine, whether an account can be resolved, and the device fingerprint. Its output is **already redacted** (user name becomes `<user>`; account and device ids are reported as shape only) and can be pasted into an issue as-is. **Full steps and how to read the result are in [`docs/WINDOWS_VERIFY_GUIDE.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/WINDOWS_VERIFY_GUIDE.md)**; step-by-step manual troubleshooting is in [`docs/WINDOWS_TOKEN_PROBE.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/WINDOWS_TOKEN_PROBE.md) (**it only asks for directory and key names, never for tokens**). If the directory name still does not match, `authFile` + `edition` can point at the exact path as a workaround.
|
|
115
170
|
|
|
116
171
|
## Development
|
|
117
172
|
|
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
一个独立的 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bundle 插件。它把本机已登录的 Trae 账号(**国内版与国际版均支持**)接到 DSH 的模型选择器:模型负责生成结构化工具调用,`bash` / `read` / `write` / `edit` 等工具由 DSH 本地执行;同时提供用量概览(国内版 Work/通用积分、国际版订阅状态)、每日签到领取与模型管理界面。**国内版与国际版是两个并行的供应商(`trae` / `trae-global`),可同时使用**;插件设置卡片以 tab 区分两者,方便统一管理。
|
|
28
28
|
|
|
29
|
+
**支持 macOS、Windows 与 Linux**:CI 在 `ubuntu-latest` 与 `windows-latest` 上跑完整 typecheck / 测试 / 构建;Windows 侧的目录探测、应用版本头与锁行为均有针对性处理,详见 [Windows 说明](#windows-说明)。
|
|
30
|
+
|
|
29
31
|
> 除每日签到领取外,插件的所有查询都是只读的、不消耗额度;签到领取也只在你点击按钮时才发生。
|
|
30
32
|
|
|
31
33
|
## 功能特性
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
- **目录与勾选按区域隔离** —— 国内版与国际版各一套模型目录、勾选、图片开关与上下文预算,两个供应商各自读各自的槽位。
|
|
40
42
|
- **多账号切换** —— 支持重新读取 Token 列表并选择账号;Token 不写入 DSH 设置。
|
|
41
43
|
- **只读用量与模型管理** —— 国内版查看 Work 积分与通用积分,国际版查看订阅/试用状态;刷新/启用 Trae 模型;只读查询不消耗额度。
|
|
42
|
-
- **每日签到领取** ——
|
|
44
|
+
- **每日签到领取** —— 国内版卡片上一键领取当日签到积分(按钮显示每日奖励,该账号已领取则显示「今日已领取」并禁用);这是插件里**唯一会改变账号状态**的操作,由你主动点击触发。Trae 的签到按「**每台设备每天一次**」限制:切换账号后如果本机今天的名额已经用掉(无论是哪个账号用的),当前账号在本机当天无法再签,卡片会说明原因而不是谎报「已领取」。国际版没有签到活动,因此不显示该按钮。
|
|
43
45
|
- **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 Trae token 不交给 pi-ai。
|
|
44
46
|
|
|
45
47
|
## 工作原理
|
|
@@ -57,7 +59,7 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
57
59
|
|
|
58
60
|
国内版与国际版各持一套完整的运行时栈——凭据 store、模型 catalog、wire 映射、上游客户端、回环 shim、adapter——按凭证自带的区域声明隔离可见账号,所以**两个区域的账号可以同时在线、同时被不同会话使用**。
|
|
59
61
|
|
|
60
|
-
用量概览走 `https://api.trae.cn/trae/api/v2/pay/*` 与 `/trae/api/v2/ug/*` 只读接口(国际账号走其自有网关的订阅状态端点);每日签到领取是其中唯一的写操作,走 `POST /trae/api/v2/ug/checkin_credits/claim`,需要携带本机安装的设备号(`x-device-id
|
|
62
|
+
用量概览走 `https://api.trae.cn/trae/api/v2/pay/*` 与 `/trae/api/v2/ug/*` 只读接口(国际账号走其自有网关的订阅状态端点);每日签到领取是其中唯一的写操作,走 `POST /trae/api/v2/ug/checkin_credits/claim`,需要携带本机安装的设备号(`x-device-id`,与聊天通道同源)。该设备号也正是签到「每台设备每天一次」的判定依据,所以换账号不会重置当天名额。刷新得到的 token 按区域存放在 `$DSH_HOME/.trae-auth.cn.json` 与 `$DSH_HOME/.trae-auth.ai.json`(两个账号同时在线互不覆盖;旧的单文件 `.trae-auth.json` 作为迁移来源保留读取)。
|
|
61
63
|
|
|
62
64
|
> 详见 `docs/IMPLEMENTATION_PLAN.md`、`docs/SOLO_ROUTE_DECISION.md`、`docs/USAGE_API_RESEARCH.md`。
|
|
63
65
|
|
|
@@ -108,12 +110,65 @@ npm install dsh-connect-trae
|
|
|
108
110
|
- **schema 可写声明**:可写字段必须标 `volatile()`(`asVolatile`),否则写入被 0.1.7 的写入门直接拒绝;0.1.7 以 `{get(): T}` 活引用交付配置值,所有读取与合并路径都先解包(`unwrapVolatile` / `unwrapVolatileDeep`)。
|
|
109
111
|
- **折叠箭头用纯 CSS**,不静态导入任何 primitives 图标——图标命名族随版本变动,静态导入不是稳定契约。
|
|
110
112
|
|
|
113
|
+
## 平台支持
|
|
114
|
+
|
|
115
|
+
| 平台 | 状态 | 说明 |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| **macOS** | ✅ 完全支持 | 开发与验证环境;目录名均经真机确认 |
|
|
118
|
+
| **Windows** | ✅ 支持 | CI 跑 `windows-latest`;账号目录、应用版本头、锁行为均有专门处理。**已在一台真机(Windows 10.0.22621 + TRAE SOLO CN)通过验证**,见下方 |
|
|
119
|
+
| **Linux** | ✅ 支持 | 含 WSL2 + Trae CLI 场景(issue #5);目录名按多候选探测 |
|
|
120
|
+
|
|
121
|
+
三个平台共享同一套逻辑:账号读取、解密、区域判定、签到与用量查询均与平台无关,平台差异只集中在**路径解析**(`src/paths.ts`)与**设备指纹**(`src/identity.ts`)两处。
|
|
122
|
+
|
|
111
123
|
## Windows 说明
|
|
112
124
|
|
|
113
|
-
- **账号数据目录**:插件读取 `%APPDATA
|
|
114
|
-
- **应用版本头**:插件从安装目录 `<LOCALAPPDATA>\Programs
|
|
115
|
-
-
|
|
116
|
-
-
|
|
125
|
+
- **账号数据目录**:插件读取 `%APPDATA%\<目录名>\User\globalStorage\storage.json`(与**安装目录无关**——Electron 系应用的用户数据固定放 `%APPDATA%`,装到 D 盘也一样)。插件会**并列探测**多个目录拼写(`Trae CN` 与 `trae-cn`、`TRAE SOLO CN` 与 `trae-solo-cn`),命中任一即可;Windows 文件名不区分大小写,因此不会重复探测同一目录。
|
|
126
|
+
- **应用版本头**:插件从安装目录 `<LOCALAPPDATA>\Programs\<安装目录名>\resources\app\product.json` 读取 `appVersion`(`LOCALAPPDATA` 缺失时回退 `<home>\AppData\Local`),随请求发送 `x-app-version` / `x-ide-version`;读不到时这些头不发送,不影响登录与聊天。
|
|
127
|
+
- **设备指纹**:`x-device-type` 发 `windows`,`x-os-version` 为 `Windows <版本>`。机器 ID 取自数据目录的 `telemetry.machineId`(或同目录 `machineid`),该推导在 Windows 上成立。
|
|
128
|
+
- **文件锁**:宿主 `dsh-atomic-write` 原生处理 Windows 的独占创建语义(`EPERM` / `EBUSY` 重试),无需插件侧适配。
|
|
129
|
+
- **Raw Chat 探测(已知限制)**:`model-cache` 依赖 `sqlite3` 命令行,Windows 默认未安装,该探测会失败并**安全回退**。Raw Chat 默认关闭,不影响主流程。
|
|
130
|
+
- **权限位**:写凭据副本时传 `mode: 0o600` / `dirMode: 0o700`,Windows 忽略 POSIX 权限位——不报错,属已知且无害。
|
|
131
|
+
|
|
132
|
+
### 真机验证结果(2026-09-26,Windows 10.0.22621 + TRAE SOLO CN)
|
|
133
|
+
|
|
134
|
+
`node scripts/verify-windows.mjs` 实测**全部通过**(退出码 0):
|
|
135
|
+
|
|
136
|
+
| 检查项 | 实测结果 |
|
|
137
|
+
| --- | --- |
|
|
138
|
+
| 目录探测 | 命中 `%APPDATA%\TRAE SOLO CN\User\globalStorage\storage.json`(其余候选为 missing) |
|
|
139
|
+
| 账号解析 | 解出 1 个账号,`solo` / `cn` 区域 |
|
|
140
|
+
| `x-device-type` | `windows` ✅ |
|
|
141
|
+
| `x-os-version` | `Windows 10.0.22621` ✅ |
|
|
142
|
+
| `x-device-id` | 16 位纯数字(来自数据目录,非兜底哈希)✅ |
|
|
143
|
+
| `x-app-version` | `0.1.56`,取自 `%LOCALAPPDATA%\Programs\TRAE SOLO CN\resources\app\product.json` ✅ |
|
|
144
|
+
|
|
145
|
+
**这确认了什么**:`win32DirName` 作依据是**正确**的——`TRAE SOLO CN` 这个拼写确实是 Windows 真机上的实际目录名,且 `product.json` 的安装路径推导与设备指纹各字段都成立。即「Windows 上插件读得到 Trae 登录」已由真机证据支持,不再是推断。
|
|
146
|
+
|
|
147
|
+
#### 脚本之外的运行路径(同一台真机,2026-09-26)
|
|
148
|
+
|
|
149
|
+
上表四项只覆盖**路径探测与设备指纹**,不等于「整个插件在 Windows 上跑得通」。因此另外单独实测了插件的主流程(直接调用 `lib/` 的导出,即产品代码本身):
|
|
150
|
+
|
|
151
|
+
| 运行路径 | 实测结果 |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `TraeCredentialStore.resolve()` | ✅ 返回凭据,host `https://api.trae.cn`,token 有效期正常解析 |
|
|
154
|
+
| `identityHeaders()` 全部请求头 | ✅ 10 个头全部生成(`x-device-id` / `x-machine-id` / `x-device-cpu` / `x-ide-version-code` 等) |
|
|
155
|
+
| 用量查询(只读,不消耗积分) | ✅ 拿到 9 个积分包,总额 2050 / 已用 1522.85 |
|
|
156
|
+
| 模型目录拉取(只读) | ✅ 拉到 **42 个模型** |
|
|
157
|
+
| 原子写 `writeFileAtomic` | ✅ 写入 + 回读一致 |
|
|
158
|
+
| 并发文件锁 `withFileLock` | ✅ 8 个并发写串行化正确,**无丢失更新**(Windows 独占创建语义由宿主处理,验证成立) |
|
|
159
|
+
| `SSE` 解码 | ✅ 代码按 `\r?\n` 切分,CRLF 与分块均支持 |
|
|
160
|
+
|
|
161
|
+
也就是说:登录、身份、用量、模型目录、并发写入这些**主流程在 Windows 上均实测可用**。
|
|
162
|
+
|
|
163
|
+
> **已知限制(本机实测确认其行为)**:`model-cache` 依赖 `sqlite3` 命令行,本机未安装,实测抛 `ENOENT`(`spawn sqlite3 ENOENT`),调用方 `.catch(() => undefined)` 正确兜底——**不影响主流程**,Raw Chat 默认关闭。
|
|
164
|
+
>
|
|
165
|
+
> **该模块的目录拼写缺口已修复**(同轮发现并修掉):它原先把 `state.vscdb` 路径**硬编码为 `Trae CN` 单一拼写**,而 `paths.ts` 是多候选,且它自己重算了一遍目录而没复用 `traeStorageCandidates`。在本机(真实目录名为 `TRAE SOLO CN`)上,旧代码指向的 `...\Trae CN\...\state.vscdb` **根本不存在**;只因 `sqlite3` 缺失、该调用必然失败并兜底,才把一个**错误的路径**藏在了另一个看似合理的错误(依赖缺失)后面。
|
|
166
|
+
>
|
|
167
|
+
> 修法是让缓存路径**从凭据路径推导**——`state.vscdb` 与 `storage.json` 同在 `globalStorage`,因而同一个候选目录既管登录也管缓存;`raw-resolver` 现在把命中的候选传下去,多版本共存时不会读到另一个安装的模型表。真机复测:新代码正确选中 `%APPDATA%\TRAE SOLO CN\User\globalStorage\state.vscdb`(存在),旧代码只找 `Trae CN`(不存在)。
|
|
168
|
+
|
|
169
|
+
> **仍然未验证的部分**:本机只装了 TRAE SOLO CN,因此 `Trae CN` / `trae-cn` 这两个拼写**仍未经真机确认**——但它们在此机器上不存在只反映「本机没装这个版本」,不代表拼写有误。装了 Trae 中国版的 Windows 用户仍欢迎跑一次上面的命令回报。
|
|
170
|
+
|
|
171
|
+
> **安装 Trae 中国版或别的情形?一条命令即可回报**:`node scripts/verify-windows.mjs` —— 它用插件自身的构建产物列出在这台机器上探测的每条路径、能否解出账号、以及设备指纹,输出**已脱敏**(用户名替换为 `<user>`,账号名与设备号只给长度),可直接贴进 issue。**完整的验证步骤与结论判读见 [`docs/WINDOWS_VERIFY_GUIDE.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/WINDOWS_VERIFY_GUIDE.md)**;手工逐步排查见 [`docs/WINDOWS_TOKEN_PROBE.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/WINDOWS_TOKEN_PROBE.md)(**只要目录名与 key 名,不要 token**)。若目录名仍对不上,可用 `authFile` + `edition` 直接指定完整路径救急。
|
|
117
172
|
|
|
118
173
|
## 开发
|
|
119
174
|
|
package/lib/client.js
CHANGED
|
@@ -265,6 +265,10 @@ window.__ModuleLoader__.load({
|
|
|
265
265
|
.dsm-trae-checkin-label{color:var(--dsw-alias-label-primary,#e6e6e6);font-size:13px;line-height:18px;font-weight:600}
|
|
266
266
|
.dsm-trae-checkin-hint{color:var(--dsw-alias-label-tertiary,#9aa0a8);font-size:12px;line-height:17px}
|
|
267
267
|
.dsm-trae-checkin-button{flex:none}
|
|
268
|
+
/* The "this machine already used today's check-in" note is INFORMATIONAL, not
|
|
269
|
+
an error: it is Trae's one-per-device-per-day rule, and the day is simply
|
|
270
|
+
spent. Rendering it in the error colour would read as a plugin fault. */
|
|
271
|
+
.dsm-trae-checkin-note{margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#c6c9d0)}
|
|
268
272
|
`;
|
|
269
273
|
//#endregion
|
|
270
274
|
//#region src/client/TraeUsageCard.tsx
|
|
@@ -389,6 +393,14 @@ window.__ModuleLoader__.load({
|
|
|
389
393
|
/** Last claim refusal, shown until the next successful refresh. */
|
|
390
394
|
const [claimError, setClaimError] = (0, react.useState)(void 0);
|
|
391
395
|
/**
|
|
396
|
+
* Whether this MACHINE has spent today's check-in while the selected account
|
|
397
|
+
* has not been paid — the "switched account" state. Kept separate from
|
|
398
|
+
* `claimError` because it is not a failure: it is the upstream's one-per-
|
|
399
|
+
* device-per-day rule, and rendering it through the "签到失败:…" line would
|
|
400
|
+
* present a normal rule as a plugin fault.
|
|
401
|
+
*/
|
|
402
|
+
const [deviceSpentNotice, setDeviceSpentNotice] = (0, react.useState)(false);
|
|
403
|
+
/**
|
|
392
404
|
* A refused or unpersisted settings write, surfaced instead of silently
|
|
393
405
|
* reverting. On 0.1.7 a rejected write resolves normally, so without this
|
|
394
406
|
* the control would flip and then quietly snap back on the next snapshot.
|
|
@@ -469,6 +481,7 @@ window.__ModuleLoader__.load({
|
|
|
469
481
|
*/
|
|
470
482
|
(0, react.useEffect)(() => {
|
|
471
483
|
setClaimError(void 0);
|
|
484
|
+
setDeviceSpentNotice(false);
|
|
472
485
|
}, [activeRegion]);
|
|
473
486
|
(0, react.useEffect)(() => {
|
|
474
487
|
if (!open || !activeRegionOn || status.status !== "signed-in") return;
|
|
@@ -536,13 +549,21 @@ window.__ModuleLoader__.load({
|
|
|
536
549
|
* (it stops an obviously pointless click), not the protection.
|
|
537
550
|
*
|
|
538
551
|
* A refusal is not thrown away: the route answers `claimed: false` with the
|
|
539
|
-
* upstream's business code and message, and the card shows it.
|
|
540
|
-
*
|
|
541
|
-
*
|
|
552
|
+
* upstream's business code and message, and the card shows it. Three answers
|
|
553
|
+
* are distinguished rather than collapsed into "failed":
|
|
554
|
+
*
|
|
555
|
+
* - `alreadyCheckedIn` — this ACCOUNT was already paid today; show it as
|
|
556
|
+
* claimed and refresh, never as an error.
|
|
557
|
+
* - `deviceCheckedIn` (code 9095) — this MACHINE already spent today's
|
|
558
|
+
* check-in, so this account cannot claim here today. That is the normal
|
|
559
|
+
* outcome after switching accounts, and saying "check-in failed" would
|
|
560
|
+
* blame the plugin for an upstream rule.
|
|
561
|
+
* - anything else — a real refusal, reported verbatim.
|
|
542
562
|
*/
|
|
543
563
|
const claimCheckin = async () => {
|
|
544
564
|
setClaiming(true);
|
|
545
565
|
setClaimError(void 0);
|
|
566
|
+
setDeviceSpentNotice(false);
|
|
546
567
|
try {
|
|
547
568
|
const response = await fetch(withTraeRegion(TRAE_CHECKIN_PATH, activeRegion), {
|
|
548
569
|
method: "POST",
|
|
@@ -551,6 +572,11 @@ window.__ModuleLoader__.load({
|
|
|
551
572
|
});
|
|
552
573
|
const body = await response.json().catch(() => void 0);
|
|
553
574
|
if (!response.ok) throw new Error(body?.error ?? `HTTP ${response.status}`);
|
|
575
|
+
if (body?.claimed === false && body.deviceCheckedIn === true) {
|
|
576
|
+
await refreshUsage(activeRegion);
|
|
577
|
+
if (mounted.current) setDeviceSpentNotice(true);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
554
580
|
if (body?.claimed === false && body.alreadyCheckedIn !== true) throw new Error(body.message === void 0 || body.message === "" ? t("row.checkinRefused", { code: String(body.code ?? "") }) : `${t("row.checkinRefused", { code: String(body.code ?? "") })}: ${body.message}`);
|
|
555
581
|
await refreshUsage(activeRegion);
|
|
556
582
|
} catch (error) {
|
|
@@ -955,10 +981,11 @@ window.__ModuleLoader__.load({
|
|
|
955
981
|
children: t("row.creditsError", { message: status.creditsError })
|
|
956
982
|
}),
|
|
957
983
|
status.checkin === void 0 ? null : (() => {
|
|
958
|
-
const
|
|
984
|
+
const accountClaimed = status.checkin.checkedIn;
|
|
985
|
+
const deviceSpent = !accountClaimed && (status.checkin.didCheckedIn || deviceSpentNotice);
|
|
959
986
|
const reward = formatNumber(status.checkin.credits);
|
|
960
987
|
const bonus = status.checkin.extraCredits === void 0 ? "" : ` + ${formatNumber(status.checkin.extraCredits)}`;
|
|
961
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
988
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
962
989
|
className: "dsm-trae-checkin",
|
|
963
990
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
964
991
|
className: "dsm-trae-checkin-copy",
|
|
@@ -967,18 +994,22 @@ window.__ModuleLoader__.load({
|
|
|
967
994
|
children: t("row.checkinLabel")
|
|
968
995
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
969
996
|
className: "dsm-trae-checkin-hint",
|
|
970
|
-
children: [t("row.checkinReward", { reward: `${reward}${bonus}` }),
|
|
997
|
+
children: [t("row.checkinReward", { reward: `${reward}${bonus}` }), accountClaimed ? ` · ${t("row.checkinDoneHint")}` : ""]
|
|
971
998
|
})]
|
|
972
999
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
973
1000
|
type: "button",
|
|
974
1001
|
className: "dsm-btn dsm-btn-primary dsm-trae-checkin-button",
|
|
975
|
-
disabled: !status.checkin.enabled ||
|
|
1002
|
+
disabled: !status.checkin.enabled || accountClaimed || deviceSpent || claiming,
|
|
976
1003
|
onClick: () => {
|
|
977
1004
|
claimCheckin();
|
|
978
1005
|
},
|
|
979
|
-
children: claiming ? t("row.checkinClaiming") :
|
|
1006
|
+
children: claiming ? t("row.checkinClaiming") : accountClaimed ? t("row.checkinClaimed") : t("row.checkinClaim")
|
|
980
1007
|
})]
|
|
981
|
-
})
|
|
1008
|
+
}), deviceSpent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1009
|
+
className: "dsm-trae-checkin-note",
|
|
1010
|
+
role: "status",
|
|
1011
|
+
children: t("row.checkinDeviceSpent")
|
|
1012
|
+
}) : null] });
|
|
982
1013
|
})(),
|
|
983
1014
|
status.checkin !== void 0 && !status.checkin.enabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
984
1015
|
className: "dsm-trae-usage-error",
|
|
@@ -1210,6 +1241,7 @@ window.__ModuleLoader__.load({
|
|
|
1210
1241
|
"row.checkinDoneHint": "come back tomorrow",
|
|
1211
1242
|
"row.checkinRefused": "Trae refused the check-in (code {code})",
|
|
1212
1243
|
"row.checkinError": "Check-in failed: {message}",
|
|
1244
|
+
"row.checkinDeviceSpent": "This machine already used today's check-in — Trae allows one per device per day, so this account cannot claim here today. Claim it on another device or from the account that checked in, or come back tomorrow.",
|
|
1213
1245
|
"row.checkinDisabled": "This account has no check-in activity available.",
|
|
1214
1246
|
"row.regionCn": "CN",
|
|
1215
1247
|
"row.regionAi": "International",
|
|
@@ -1288,6 +1320,7 @@ window.__ModuleLoader__.load({
|
|
|
1288
1320
|
"row.checkinDoneHint": "明天再来",
|
|
1289
1321
|
"row.checkinRefused": "Trae 拒绝了本次签到(业务码 {code})",
|
|
1290
1322
|
"row.checkinError": "签到失败:{message}",
|
|
1323
|
+
"row.checkinDeviceSpent": "本机今日的签到已用掉——Trae 按「每台设备每天一次」限制,所以当前账号在本机今天无法再签。可换一台设备领取,或用今天已签到的那台设备/账号,或明天再来。",
|
|
1291
1324
|
"row.checkinDisabled": "该账号当前没有可用的签到活动。",
|
|
1292
1325
|
"row.regionCn": "国内版",
|
|
1293
1326
|
"row.regionAi": "国际版",
|