dsh-connect-trae 4.4.5 → 4.4.6
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 +71 -0
- package/README.en.md +17 -4
- package/README.md +28 -5
- package/lib/index.d.ts +34 -1
- package/lib/index.js +123 -46
- package/package.json +14 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.6 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
> **对齐宿主 DSH 0.2.1-rc.3(纯版本推进,插件代码零改动),并修掉账号链路上三处
|
|
6
|
+
> 会静默丢数据的问题——其中一处可能正是 #56「被顶掉的账号若干时间后消失」的成因。**
|
|
7
|
+
>
|
|
8
|
+
> 宿主这一版只落地 5 个提交:三个是「导航项自定义」(面板固定/取消固定、More 菜单、埋点),
|
|
9
|
+
> 一个是 DeepSeek API-key 通道改走官方 `/models` 目录,一个是版本号——**均与本插件不相干**。
|
|
10
|
+
> 插件消费的 13 个宿主包里只有 `dsh-client-ui-primitives` 有代码变化,且全是新增,
|
|
11
|
+
> 而本插件对它只有类型层引用,因此**不需要任何适配**。
|
|
12
|
+
>
|
|
13
|
+
> 真正的风险在插件自己身上:一份读不懂的副本文件会被当成「一个账号都没有」,
|
|
14
|
+
> 而三条合并写路径又以它为基底——一次损坏读 + 一次刷新 = 其余账号的副本被无声抹掉。
|
|
15
|
+
|
|
16
|
+
### 修复
|
|
17
|
+
|
|
18
|
+
- **副本文件损坏不再清掉全部账号**(`fix(auth)`,PR #58)。
|
|
19
|
+
`parseOwn` 此前对「无法解析的文件」(半截写入、手改损坏、未知版本)一律返回空数组,而镜像 /
|
|
20
|
+
刷新 / 导入三条合并写路径都以它的结果为合并基底——一次损坏读 + 下一次刷新 = **其余全部账号的
|
|
21
|
+
副本被无声抹掉**,账号行随下一个轮询消失,且因为异常全被吞掉,日志里什么都看不到。
|
|
22
|
+
- `parseOwn` 改为三态(合法 / 合法空 / **无法解析返回 `undefined`**),新增 `readOwnForMerge`
|
|
23
|
+
收口三条写路径:遇到无法解析的文件先隔离为 `.corrupt-<时间戳>`(字节保留、可找回),
|
|
24
|
+
再从干净文件开始;**隔离失败则放弃本次写入,绝不覆盖读不懂的字节**。
|
|
25
|
+
- `deleteAccount` 对不可解析文件**拒绝**而非当空。
|
|
26
|
+
- 这可能是 #56(被顶掉的账号若干时间后消失)的一种成因——值得让报告者检查
|
|
27
|
+
`$DSH_HOME/.trae-auth.cn.json` 及其同目录的 `.corrupt-*` 隔离文件。
|
|
28
|
+
- **墓碑读-改-写收进一把锁**(`fix(auth)`,PR #58)。墓碑文件被两个并发流读改写:镜像
|
|
29
|
+
(清失效墓碑)与删除(加墓碑),此前都是锁外读、锁内写,丢更新的两个方向都疼——丢一个墓碑=
|
|
30
|
+
已删账号在下一轮镜像复活;丢一次清理=墓碑永久残留,该账号之后永远无法再被镜像。
|
|
31
|
+
收口为锁内 `updateTombstones(mutator)`。
|
|
32
|
+
- **池测量的删除不再丢并发写入**(`fix(pool)`,PR #58)。`dropTraePoolProbe` 的读取挪进锁内
|
|
33
|
+
(对齐同文件 `writeTraePoolProbes` 的范式):此前锁外读的快照会把并发批次刚写入的测量覆盖掉。
|
|
34
|
+
|
|
35
|
+
### 变更
|
|
36
|
+
|
|
37
|
+
- **对齐宿主 DSH 0.2.1-rc.3**:13 个 `@deepseek-ai/dsh-*` devDependency 从 `0.2.1-rc.2`
|
|
38
|
+
推进到 `0.2.1-rc.3`。`cordis`(`4.0.5`)/ `schemastery`(`3.18.5`)/ `pi-ai`(仍 `1.1.0`)
|
|
39
|
+
均未变——宿主 `app.asar` 实测随包的 pi-ai 仍是 1.1.0,3.4.2 起的双向适配结论原样成立。
|
|
40
|
+
- **peer 范围无需改动**:`0.2.1-rc.3` 对 `>=0.1.7-rc.1 <0.3.0-0` 已判通过(实测)。
|
|
41
|
+
- **插件代码零改动**:rc.3 对本插件消费的 13 个宿主包里,只有 `dsh-client-ui-primitives`
|
|
42
|
+
存在代码变化,且全部是**新增**(`TooltipSide` 增加 `'left'` 并改用实测边框盒换边放置、
|
|
43
|
+
`MenuItemButton` 增加 `current`、`Menu` 增加 `minWidthFromAnchor`、新增 `IconAppsAddOutline*`)。
|
|
44
|
+
本插件对 `dsh-client-ui-primitives` 只有**类型层引用**(`import type {} from ...`),
|
|
45
|
+
运行时不消费这些组件,故无需适配。
|
|
46
|
+
|
|
47
|
+
### 已核对确认无变化
|
|
48
|
+
|
|
49
|
+
- 逐字节比对 npm 上 `0.2.1-rc.2` 与 `0.2.1-rc.3` 两版的全量文件树:
|
|
50
|
+
`dsh-llm`、`dsh-llm-pi-ai`、`dsh-settings`、`dsh-host-webserver`、`dsh-atomic-write`、
|
|
51
|
+
`dsh-home-paths`、`dsh-attachment`、`dsh-client-ui-renderer`、`dsh-client-ui-settings`、
|
|
52
|
+
`dsh-client-ui-settings-plugins`、`dsh-client-ui-slots`、`dsh-client-locale`
|
|
53
|
+
——**除 package.json 的版本号与内部依赖号外逐字节相同**。
|
|
54
|
+
- 本插件注册的 5 个槽位(`plugins.bundle.config`、`plugins.row.config`、`conversation.input.left`、
|
|
55
|
+
`conversation.composer.dock`、`conversation.input.model.label`)在 rc.3 中**仍然存在且契约未变**;
|
|
56
|
+
宿主新增的 `conversation.session.header.views.actions` 等槽位与本插件无交集。
|
|
57
|
+
- rc.3 宿主上实测:宿主侧 Config 条目 `include:dsh-connect-trae` 在册,客户端
|
|
58
|
+
`plugins.bundle.config` / `plugins.row.config` / `conversation.input.left` 三处注册均 active。
|
|
59
|
+
|
|
60
|
+
### 测试
|
|
61
|
+
|
|
62
|
+
- PR #58 新增回归用例:损坏副本文件被隔离且字节保留、合法文件中坏一行只丢该行、
|
|
63
|
+
隔离失败时放弃写入且原字节不动、墓碑并发删除/写入序列化冒烟、池测量的 drop 与 write 不互相覆盖。
|
|
64
|
+
- `tests/dsh-peer-range.spec.ts` 的 `MUST_ADMIT` 补入 `0.2.1-rc.3`——此前这份「必须放行的版本」
|
|
65
|
+
清单里没有当前正在使用的版本,范围若被误收窄到当前宿主之外,测试仍会全绿。
|
|
66
|
+
- 全仓 **1084** 例通过(84 个测试文件),双 tsconfig 类型检查通过。
|
|
67
|
+
|
|
68
|
+
### 文档
|
|
69
|
+
|
|
70
|
+
- 新增 `docs/DSH_0.2.1_RC1_SYNC.md` 第十节:`0.2.1-rc.3` 复核记录(取证、依赖面、契约面、
|
|
71
|
+
槽位核对、peer 门禁)。
|
|
72
|
+
- `README.md` / `README.en.md` 的宿主版本结论推进到 `0.2.1-rc.3`,并新增
|
|
73
|
+
「`0.2.1-rc.3` 同步结论」小节。
|
|
3
74
|
## 4.4.5 (2026-10-23)
|
|
4
75
|
|
|
5
76
|
> **模型列表不再倒出官方已下架的历史配置:按 Trae IDE 名册退役 12 行、改钉 6 行,
|
package/README.en.md
CHANGED
|
@@ -153,7 +153,7 @@ Restart the DSH process after install/update/uninstall.
|
|
|
153
153
|
## DSH version compatibility
|
|
154
154
|
|
|
155
155
|
**Requires DSH 0.1.7-rc.1 and up** (since 2.3.0 the plugin no longer targets hosts older than 0.1.7; the peer dependency ranges are narrowed accordingly).
|
|
156
|
-
**The peer upper bound is `<0.3.0-0`, so both the 0.1.7 and the 0.2 line are supported (including 0.2.1-rc.
|
|
156
|
+
**The peer upper bound is `<0.3.0-0`, so both the 0.1.7 and the 0.2 line are supported (including 0.2.1-rc.3, verified).**
|
|
157
157
|
|
|
158
158
|
### Both pi-ai context generations are supported (since 3.4.2)
|
|
159
159
|
|
|
@@ -178,13 +178,26 @@ The 0.2.0 line breaks **no** contract this plugin consumes — a symbol-by-symbo
|
|
|
178
178
|
|
|
179
179
|
### DSH 0.2.1 line (pi-ai 1.1.0) sync
|
|
180
180
|
|
|
181
|
-
The `0.2.1-rc.1` host moved `@earendil-works/pi-ai` from `^0.85.1` to **`^1.1.0`** (unchanged through `0.2.1-rc.2`) — the only change on the 0.2 line with real behavioural impact. Three conclusions:
|
|
181
|
+
The `0.2.1-rc.1` host moved `@earendil-works/pi-ai` from `^0.85.1` to **`^1.1.0`** (unchanged through `0.2.1-rc.2` and `0.2.1-rc.3`) — the only change on the 0.2 line with real behavioural impact. Three conclusions:
|
|
182
182
|
|
|
183
183
|
1. **The request body did not regress (measured, not inferred).** The 1.1.0 api reads the prompt **only** from a transcript system message and never touches top-level `systemPrompt` / `tools`, while `dsh-llm-pi-ai`'s `piContext()` still emits the legacy envelope. The bidirectional adapter this plugin has shipped since 3.4.2 covers exactly that: feeding the real host envelope through real `1.1.0` with a local SSE capture yields `roles: ["system","user"]`, an intact system text and `tools: 1`; **without the adapter it degenerates to `roles: ["user"]`, system `null`, `tools: 0`** — with no error. That contrast is pinned as a wire-level matrix in `tests/pi-ai-context.spec.ts`.
|
|
184
184
|
2. **Types were fixed.** 1.1.0 changed `ProviderStreams`' second parameter to a **branded** `TranscriptContext` (a `unique symbol` only `normalizeContext()` can produce). The adapter casts once at that single boundary (`withContextAdaptation` in `src/adapter.ts`) and documents why the brand cannot be obtained honestly there.
|
|
185
|
-
3. **The peer range needed no change.** DSH evaluates with `semver.satisfies(runtime, range, { includePrerelease: true })` and checks only the `@deepseek-ai/dsh*` namespace, so `>=0.1.7-rc.1 <0.3.0-0` already admits
|
|
185
|
+
3. **The peer range needed no change.** DSH evaluates with `semver.satisfies(runtime, range, { includePrerelease: true })` and checks only the `@deepseek-ai/dsh*` namespace, so `>=0.1.7-rc.1 <0.3.0-0` already admits `0.2.1-rc.1`, `0.2.1-rc.2` and `0.2.1-rc.3` (verified). Note the prerelease cliff only exists under **default** options — it is not this plugin's situation.
|
|
186
186
|
|
|
187
|
-
The dev tree pins `@earendil-works/pi-ai` at exactly **1.1.0** (the version the host actually bundles) rather than `^1.1.0`, so local typecheck and tests run against the same contract users do; the `schemastery` override moved to `3.18.5` (`dsh-llm-pi-ai@0.2.1-rc.
|
|
187
|
+
The dev tree pins `@earendil-works/pi-ai` at exactly **1.1.0** (the version the host actually bundles — still 1.1.0 on `0.2.1-rc.3`) rather than `^1.1.0`, so local typecheck and tests run against the same contract users do; the `schemastery` override moved to `3.18.5` (`dsh-llm-pi-ai@0.2.1-rc.3` declares `~3.18.5`).
|
|
188
|
+
|
|
189
|
+
### `0.2.1-rc.3` sync (since 4.4.6)
|
|
190
|
+
|
|
191
|
+
The host moved to `0.2.1-rc.3` (the desktop app's `CFBundleVersion` and the bundled `dsh-desktop-runtime` both report `0.2.1-rc.3`). Upstream shipped five commits: three of them are the new "navigation entry customization" UI (pin/unpin plugin launchers, a More menu, analytics), one switches the DeepSeek API-key route to the official `/models` listing, one is the version bump.
|
|
192
|
+
|
|
193
|
+
For **this plugin**:
|
|
194
|
+
|
|
195
|
+
- Of the 13 host packages the plugin consumes, **only `dsh-client-ui-primitives` changed code**, and every change is **additive**: `Tooltip`'s `TooltipSide` gains `'left'` and now places the bubble from measured border boxes (preferred → opposite → perpendicular side), `MenuItemButton` gains `current`, `Menu` gains `minWidthFromAnchor`, and the icon set gains `IconAppsAddOutline*`. This plugin references `dsh-client-ui-primitives` **at the type level only** (`import type {} from ...`) and never touches those components at runtime, so **no adaptation is needed**.
|
|
196
|
+
- The other 12 packages (`dsh-llm`, `dsh-llm-pi-ai`, `dsh-settings`, `dsh-host-webserver`, `dsh-atomic-write`, `dsh-home-paths`, `dsh-attachment`, `dsh-client-ui-renderer`, `dsh-client-ui-settings`, `dsh-client-ui-settings-plugins`, `dsh-client-ui-slots`, `dsh-client-locale`) are **byte-for-byte unchanged**.
|
|
197
|
+
- Every slot this plugin registers (`plugins.bundle.config`, `plugins.row.config`, `conversation.input.left`, `conversation.composer.dock`, `conversation.input.model.label`) **still exists with an unchanged contract** on rc.3; the host's new slots (e.g. `conversation.session.header.views.actions`) do not overlap with them.
|
|
198
|
+
- The peer range **needs no change**: `0.2.1-rc.3` is admitted by `>=0.1.7-rc.1 <0.3.0-0` (verified).
|
|
199
|
+
|
|
200
|
+
Details: [section 10 of `docs/DSH_0.2.1_RC1_SYNC.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/DSH_0.2.1_RC1_SYNC.md).
|
|
188
201
|
|
|
189
202
|
|
|
190
203
|
### Why the upper bound is not `<0.2.0`
|
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ npm install dsh-connect-trae
|
|
|
172
172
|
## DSH 版本兼容
|
|
173
173
|
|
|
174
174
|
**要求 DSH 0.1.7-rc.1 及以上**(2.3.0 起不再支持 0.1.7 之前的宿主;peer 依赖范围已按此收窄)。
|
|
175
|
-
**peer 上界为 `<0.3.0-0`:0.1.7 与 0.2 两条线均受支持(含 0.2.1-rc.
|
|
175
|
+
**peer 上界为 `<0.3.0-0`:0.1.7 与 0.2 两条线均受支持(含 0.2.1-rc.3,已实测)。**
|
|
176
176
|
|
|
177
177
|
### 同时支持 pi-ai 的两代上下文(3.4.2 起)
|
|
178
178
|
|
|
@@ -206,7 +206,7 @@ peer 范围不满足的 bundle 会被**整体跳过**,只往 stderr 写一行
|
|
|
206
206
|
|
|
207
207
|
### DSH 0.2.1 线(pi-ai 1.1.0)同步结论
|
|
208
208
|
|
|
209
|
-
宿主 `0.2.1-rc.1` 把 `@earendil-works/pi-ai` 从 `^0.85.1` 抬到了 **`^1.1.0`**(`0.2.1-rc.2`
|
|
209
|
+
宿主 `0.2.1-rc.1` 把 `@earendil-works/pi-ai` 从 `^0.85.1` 抬到了 **`^1.1.0`**(`0.2.1-rc.2` / `0.2.1-rc.3` 均保持不变),这是 0.2 线里
|
|
210
210
|
唯一有实际行为影响的变化。三项结论:
|
|
211
211
|
|
|
212
212
|
1. **请求体没有丢东西(已实测)**。1.1.0 的 api **只**读 transcript 里的 system 消息,
|
|
@@ -219,13 +219,36 @@ peer 范围不满足的 bundle 会被**整体跳过**,只往 stderr 写一行
|
|
|
219
219
|
`TranscriptContext`,只有 `normalizeContext()` 能产出。适配层在这条边界上做一次显式转型
|
|
220
220
|
(`src/adapter.ts` 的 `withContextAdaptation`),并在两处调用点写明为何品牌在此处不可诚实获得。
|
|
221
221
|
3. **peer 范围无需改动**。DSH 用 `semver.satisfies(runtime, range, { includePrerelease: true })`
|
|
222
|
-
求值,且只校验 `@deepseek-ai/dsh*` 命名空间,因此 `>=0.1.7-rc.1 <0.3.0-0` 对 `0.2.1-rc.1` / `0.2.1-rc.2` 均已判通过
|
|
222
|
+
求值,且只校验 `@deepseek-ai/dsh*` 命名空间,因此 `>=0.1.7-rc.1 <0.3.0-0` 对 `0.2.1-rc.1` / `0.2.1-rc.2` / `0.2.1-rc.3` 均已判通过
|
|
223
223
|
(已实测)。**注意**:预发布悬崖只在**默认选项**下成立——那是 `dsh-connect-workbuddy` 侧
|
|
224
224
|
调研文档所描述的困境,本插件不适用。
|
|
225
225
|
|
|
226
|
-
开发树的 `@earendil-works/pi-ai` 钉在 **1.1.0
|
|
226
|
+
开发树的 `@earendil-works/pi-ai` 钉在 **1.1.0**(宿主实际随包的版本,`0.2.1-rc.3` 实测仍是 1.1.0)而非 `^1.1.0`,
|
|
227
227
|
使本地 typecheck 与测试跑在与用户完全一致的契约上;`schemastery` 的 override 同步抬到
|
|
228
|
-
`3.18.5`(`dsh-llm-pi-ai@0.2.1-rc.
|
|
228
|
+
`3.18.5`(`dsh-llm-pi-ai@0.2.1-rc.3` 声明 `~3.18.5`)。
|
|
229
|
+
|
|
230
|
+
### `0.2.1-rc.3` 同步结论(4.4.6 起)
|
|
231
|
+
|
|
232
|
+
宿主推进到 `0.2.1-rc.3`(桌面版 `CFBundleVersion` 与随包 `dsh-desktop-runtime` 同为 `0.2.1-rc.3`)。
|
|
233
|
+
上游这一版只落地了 5 个提交,其中 3 个是 UI 的「导航项自定义」(插件化面板的固定/取消固定、
|
|
234
|
+
More 菜单、埋点),1 个是 DeepSeek API-key 通道改走官方 `/models` 目录,1 个是版本号。
|
|
235
|
+
|
|
236
|
+
对**本插件**而言:
|
|
237
|
+
|
|
238
|
+
- 插件消费的 13 个宿主包里,**只有 `dsh-client-ui-primitives` 有代码变化**,且**全是新增**:
|
|
239
|
+
`Tooltip` 的 `TooltipSide` 增加 `'left'` 并改用实测边框盒做「首选 → 对侧 → 垂直侧」的换边放置,
|
|
240
|
+
`MenuItemButton` 增加 `current`、`Menu` 增加 `minWidthFromAnchor`,图标新增 `IconAppsAddOutline*`。
|
|
241
|
+
本插件对 `dsh-client-ui-primitives` 只有**类型层引用**(`import type {} from ...`),
|
|
242
|
+
运行时不碰这些组件,**无需任何适配**。
|
|
243
|
+
- 其余 12 个包(`dsh-llm`、`dsh-llm-pi-ai`、`dsh-settings`、`dsh-host-webserver`、`dsh-atomic-write`、
|
|
244
|
+
`dsh-home-paths`、`dsh-attachment`、`dsh-client-ui-renderer`、`dsh-client-ui-settings`、
|
|
245
|
+
`dsh-client-ui-settings-plugins`、`dsh-client-ui-slots`、`dsh-client-locale`)**逐字节未变**。
|
|
246
|
+
- 插件注册的全部槽位(`plugins.bundle.config`、`plugins.row.config`、`conversation.input.left`、
|
|
247
|
+
`conversation.composer.dock`、`conversation.input.model.label`)在 rc.3 中**仍然存在且契约未变**;
|
|
248
|
+
宿主新增的 `conversation.session.header.views.actions` 等槽位与本插件无交集。
|
|
249
|
+
- peer 范围**无需改动**:`0.2.1-rc.3` 对 `>=0.1.7-rc.1 <0.3.0-0` 已判通过(已实测)。
|
|
250
|
+
|
|
251
|
+
细节见 [`docs/DSH_0.2.1_RC1_SYNC.md`](https://github.com/dingminhua/dsh-connect-trae/blob/main/docs/DSH_0.2.1_RC1_SYNC.md) 第十节。
|
|
229
252
|
|
|
230
253
|
|
|
231
254
|
### peer 上界为什么不写 `<0.2.0`
|
package/lib/index.d.ts
CHANGED
|
@@ -1889,7 +1889,40 @@ declare class TraeCredentialStore {
|
|
|
1889
1889
|
* account, it never locks the user out.
|
|
1890
1890
|
*/
|
|
1891
1891
|
private readTombstones;
|
|
1892
|
-
|
|
1892
|
+
/**
|
|
1893
|
+
* Read, mutate, and write the tombstone list under ONE lock.
|
|
1894
|
+
*
|
|
1895
|
+
* The tombstone file is read-modify-write state shared by two flows that run
|
|
1896
|
+
* concurrently: `mirrorLocalLogins` (drops tombstones whose logins are gone)
|
|
1897
|
+
* and `deleteAccount` (adds the id just deleted). A read OUTSIDE the write's
|
|
1898
|
+
* lock loses updates in both directions — a dropped-add resurrects a deleted
|
|
1899
|
+
* account on the next mirror, a dropped-drop keeps a tombstone forever and
|
|
1900
|
+
* hides every future sign-in of that account. The mutator therefore runs on
|
|
1901
|
+
* the freshly-read set INSIDE the lock, and what it returns is what gets
|
|
1902
|
+
* written, with no window in between.
|
|
1903
|
+
*
|
|
1904
|
+
* Fire-and-forget safe: a failed update only delays a tombstone change until
|
|
1905
|
+
* the next mirror or delete, so errors are swallowed like the readers'.
|
|
1906
|
+
*/
|
|
1907
|
+
private updateTombstones;
|
|
1908
|
+
/**
|
|
1909
|
+
* Read the plugin-owned file as the merge base for a read-modify-write.
|
|
1910
|
+
*
|
|
1911
|
+
* This is the fail-safe that keeps one unreadable file from erasing every
|
|
1912
|
+
* account: `parseOwn` now distinguishes a VALID empty file (`[]`) from one
|
|
1913
|
+
* this build cannot parse (`undefined`), and only the former may serve as a
|
|
1914
|
+
* merge base. For the latter the file is quarantined — renamed aside with
|
|
1915
|
+
* its bytes intact — and the merge then starts from nothing, so the action
|
|
1916
|
+
* the user pressed still succeeds and the lost entries are recoverable from
|
|
1917
|
+
* the quarantine copy instead of being overwritten silently. A quarantine
|
|
1918
|
+
* that fails (an exotic filesystem, a name collision) aborts the write
|
|
1919
|
+
* instead: losing one refresh is recoverable by pressing it again, losing
|
|
1920
|
+
* every account is not.
|
|
1921
|
+
*
|
|
1922
|
+
* Every caller must hold the {@link withFileLock} of `path` across BOTH the
|
|
1923
|
+
* read and the subsequent write — this does not lock by itself.
|
|
1924
|
+
*/
|
|
1925
|
+
private readOwnForMerge;
|
|
1893
1926
|
/**
|
|
1894
1927
|
* Copy every local Trae sign-in into this store's own file, once.
|
|
1895
1928
|
*
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { copyFile, readFile, rm, stat } from "node:fs/promises";
|
|
9
|
+
import { copyFile, readFile, rename, rm, stat } from "node:fs/promises";
|
|
10
10
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
|
|
11
11
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
12
12
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -2223,21 +2223,55 @@ function credentialOfStored(stored) {
|
|
|
2223
2223
|
* needed and an older install's file is never rejected. A malformed row is
|
|
2224
2224
|
* dropped rather than failing the whole file — one bad imported account must
|
|
2225
2225
|
* not hide the rest.
|
|
2226
|
+
*
|
|
2227
|
+
* Returns `undefined` — NOT an empty array — for a file this build cannot parse
|
|
2228
|
+
* at all: a torn JSON write, a hand-edited document, or a version nothing here
|
|
2229
|
+
* knows. The distinction is load-bearing (see {@link readOwnForMerge}): every
|
|
2230
|
+
* merge writer treats `[]` as the merge base but `undefined` as "do not build
|
|
2231
|
+
* on this", because an unparseable file may still HOLD every account, and
|
|
2232
|
+
* merging onto "empty" would erase them on the very next refresh.
|
|
2226
2233
|
*/
|
|
2227
2234
|
function parseOwn(text) {
|
|
2235
|
+
let document;
|
|
2228
2236
|
try {
|
|
2229
|
-
const
|
|
2230
|
-
if (
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2237
|
+
const parsed = JSON.parse(text);
|
|
2238
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
2239
|
+
document = parsed;
|
|
2240
|
+
} catch {
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
if (document.version === OWN_VERSION) {
|
|
2244
|
+
if (!Array.isArray(document.credentials)) return void 0;
|
|
2245
|
+
return document.credentials.map(credentialOfStored).filter((value) => value !== void 0);
|
|
2246
|
+
}
|
|
2247
|
+
if (document.version === OWN_VERSION_SINGLE) {
|
|
2248
|
+
const single = credentialOfStored(document.credential);
|
|
2249
|
+
return single === void 0 ? void 0 : [single];
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* Move a plugin-owned file this build can no longer parse out of the way, so
|
|
2254
|
+
* the next write starts from a clean file instead of building on — or being
|
|
2255
|
+
* refused by — the unreadable one.
|
|
2256
|
+
*
|
|
2257
|
+
* The rename is the data preservation: the bytes stay on disk beside the live
|
|
2258
|
+
* path (`.corrupt-<stamp>`), so a future build that understands the format (or
|
|
2259
|
+
* a user with a text editor) can still recover the accounts it carried. It is
|
|
2260
|
+
* best-effort on purpose: a quarantine that cannot happen must not fail the
|
|
2261
|
+
* user action that triggered it, and the merge writers refuse to overwrite a
|
|
2262
|
+
* file they could not quarantine (see {@link readOwnForMerge}).
|
|
2263
|
+
*
|
|
2264
|
+
* Must be called under the same lock the caller writes under: without the lock
|
|
2265
|
+
* the rename could interleave with a concurrent writer holding the pre-rename
|
|
2266
|
+
* content and write it right back.
|
|
2267
|
+
*/
|
|
2268
|
+
async function quarantineCorruptOwn(path) {
|
|
2269
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2270
|
+
try {
|
|
2271
|
+
await rename(path, `${path}.corrupt-${stamp}`);
|
|
2272
|
+
return true;
|
|
2239
2273
|
} catch {
|
|
2240
|
-
return
|
|
2274
|
+
return false;
|
|
2241
2275
|
}
|
|
2242
2276
|
}
|
|
2243
2277
|
/**
|
|
@@ -2415,9 +2449,26 @@ var TraeCredentialStore = class {
|
|
|
2415
2449
|
return /* @__PURE__ */ new Set();
|
|
2416
2450
|
}
|
|
2417
2451
|
}
|
|
2418
|
-
|
|
2452
|
+
/**
|
|
2453
|
+
* Read, mutate, and write the tombstone list under ONE lock.
|
|
2454
|
+
*
|
|
2455
|
+
* The tombstone file is read-modify-write state shared by two flows that run
|
|
2456
|
+
* concurrently: `mirrorLocalLogins` (drops tombstones whose logins are gone)
|
|
2457
|
+
* and `deleteAccount` (adds the id just deleted). A read OUTSIDE the write's
|
|
2458
|
+
* lock loses updates in both directions — a dropped-add resurrects a deleted
|
|
2459
|
+
* account on the next mirror, a dropped-drop keeps a tombstone forever and
|
|
2460
|
+
* hides every future sign-in of that account. The mutator therefore runs on
|
|
2461
|
+
* the freshly-read set INSIDE the lock, and what it returns is what gets
|
|
2462
|
+
* written, with no window in between.
|
|
2463
|
+
*
|
|
2464
|
+
* Fire-and-forget safe: a failed update only delays a tombstone change until
|
|
2465
|
+
* the next mirror or delete, so errors are swallowed like the readers'.
|
|
2466
|
+
*/
|
|
2467
|
+
async updateTombstones(mutator) {
|
|
2419
2468
|
const path = this.deletedPath();
|
|
2420
2469
|
await withFileLock(path, async () => {
|
|
2470
|
+
const ids = await this.readTombstones();
|
|
2471
|
+
mutator(ids);
|
|
2421
2472
|
await writeFileAtomic(path, `${JSON.stringify({
|
|
2422
2473
|
version: 1,
|
|
2423
2474
|
ids: [...ids]
|
|
@@ -2425,7 +2476,31 @@ var TraeCredentialStore = class {
|
|
|
2425
2476
|
mode: 384,
|
|
2426
2477
|
dirMode: 448
|
|
2427
2478
|
});
|
|
2428
|
-
});
|
|
2479
|
+
}).catch(() => void 0);
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Read the plugin-owned file as the merge base for a read-modify-write.
|
|
2483
|
+
*
|
|
2484
|
+
* This is the fail-safe that keeps one unreadable file from erasing every
|
|
2485
|
+
* account: `parseOwn` now distinguishes a VALID empty file (`[]`) from one
|
|
2486
|
+
* this build cannot parse (`undefined`), and only the former may serve as a
|
|
2487
|
+
* merge base. For the latter the file is quarantined — renamed aside with
|
|
2488
|
+
* its bytes intact — and the merge then starts from nothing, so the action
|
|
2489
|
+
* the user pressed still succeeds and the lost entries are recoverable from
|
|
2490
|
+
* the quarantine copy instead of being overwritten silently. A quarantine
|
|
2491
|
+
* that fails (an exotic filesystem, a name collision) aborts the write
|
|
2492
|
+
* instead: losing one refresh is recoverable by pressing it again, losing
|
|
2493
|
+
* every account is not.
|
|
2494
|
+
*
|
|
2495
|
+
* Every caller must hold the {@link withFileLock} of `path` across BOTH the
|
|
2496
|
+
* read and the subsequent write — this does not lock by itself.
|
|
2497
|
+
*/
|
|
2498
|
+
async readOwnForMerge(path) {
|
|
2499
|
+
const text = await readFile(path, "utf8").catch(() => void 0);
|
|
2500
|
+
if (text === void 0) return [];
|
|
2501
|
+
const parsed = parseOwn(text);
|
|
2502
|
+
if (parsed !== void 0) return parsed;
|
|
2503
|
+
return await quarantineCorruptOwn(path) ? [] : void 0;
|
|
2429
2504
|
}
|
|
2430
2505
|
/**
|
|
2431
2506
|
* Copy every local Trae sign-in into this store's own file, once.
|
|
@@ -2447,20 +2522,17 @@ var TraeCredentialStore = class {
|
|
|
2447
2522
|
const { credentials: discovered } = await this.readDesktopAll();
|
|
2448
2523
|
const own = await this.readOwns();
|
|
2449
2524
|
const ownIds = new Set(own.map((credential) => traeAccountId(credential)));
|
|
2450
|
-
const tombstones = await this.readTombstones();
|
|
2451
2525
|
const liveIds = new Set(discovered.map((credential) => traeAccountId(credential)));
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
if (liveIds.has(id))
|
|
2455
|
-
|
|
2456
|
-
tombstonesChanged = true;
|
|
2457
|
-
}
|
|
2526
|
+
const tombstones = await this.readTombstones();
|
|
2527
|
+
if ([...tombstones].some((id) => !liveIds.has(id))) await this.updateTombstones((ids) => {
|
|
2528
|
+
for (const id of [...ids]) if (!liveIds.has(id)) ids.delete(id);
|
|
2529
|
+
});
|
|
2458
2530
|
const missing = discovered.filter((credential) => this.matchesRegion(credential) && !ownIds.has(traeAccountId(credential)) && !tombstones.has(traeAccountId(credential)));
|
|
2459
|
-
if (tombstonesChanged) await this.writeTombstones(tombstones);
|
|
2460
2531
|
if (missing.length === 0) return;
|
|
2461
2532
|
const ownPath = this.ownAuthPath();
|
|
2462
2533
|
await withFileLock(ownPath, async () => {
|
|
2463
|
-
const existing = await
|
|
2534
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
2535
|
+
if (existing === void 0) return;
|
|
2464
2536
|
const byId = new Map(existing.map((credential) => [traeAccountId(credential), credential]));
|
|
2465
2537
|
for (const credential of missing) {
|
|
2466
2538
|
const id = traeAccountId(credential);
|
|
@@ -2748,7 +2820,8 @@ var TraeCredentialStore = class {
|
|
|
2748
2820
|
const ownPath = this.ownAuthPath();
|
|
2749
2821
|
const written = [];
|
|
2750
2822
|
await withFileLock(ownPath, async () => {
|
|
2751
|
-
const existing = await
|
|
2823
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
2824
|
+
if (existing === void 0) return;
|
|
2752
2825
|
const byId = /* @__PURE__ */ new Map();
|
|
2753
2826
|
for (const item of existing) byId.set(traeAccountId(item), item);
|
|
2754
2827
|
for (const credential of credentials) {
|
|
@@ -2907,7 +2980,7 @@ var TraeCredentialStore = class {
|
|
|
2907
2980
|
let mutated = false;
|
|
2908
2981
|
for (const path of this.ownCandidates()) {
|
|
2909
2982
|
const dropped = await withFileLock(path, async () => {
|
|
2910
|
-
const existing = await readFile(path, "utf8").then(parseOwn, () => null);
|
|
2983
|
+
const existing = await readFile(path, "utf8").then((text) => parseOwn(text) ?? null, () => null);
|
|
2911
2984
|
if (existing === null || !existing.some((credential) => traeAccountId(credential) === accountId)) return false;
|
|
2912
2985
|
const next = existing.filter((credential) => traeAccountId(credential) !== accountId);
|
|
2913
2986
|
await writeOwn(path, next);
|
|
@@ -2915,11 +2988,9 @@ var TraeCredentialStore = class {
|
|
|
2915
2988
|
}).catch(() => false);
|
|
2916
2989
|
mutated = mutated || dropped;
|
|
2917
2990
|
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
await this.writeTombstones(tombstones).catch(() => void 0);
|
|
2922
|
-
}
|
|
2991
|
+
await this.updateTombstones((ids) => {
|
|
2992
|
+
ids.add(accountId);
|
|
2993
|
+
});
|
|
2923
2994
|
if (this.accountId === accountId) {
|
|
2924
2995
|
this.accountId = void 0;
|
|
2925
2996
|
this.inflight = void 0;
|
|
@@ -3039,7 +3110,8 @@ var TraeCredentialStore = class {
|
|
|
3039
3110
|
async readOwns() {
|
|
3040
3111
|
const copies = [];
|
|
3041
3112
|
for (const path of this.ownCandidates()) try {
|
|
3042
|
-
|
|
3113
|
+
const parsed = parseOwn(await readFile(path, "utf8"));
|
|
3114
|
+
if (parsed !== void 0) copies.push(...parsed);
|
|
3043
3115
|
} catch {}
|
|
3044
3116
|
return copies;
|
|
3045
3117
|
}
|
|
@@ -3061,7 +3133,8 @@ var TraeCredentialStore = class {
|
|
|
3061
3133
|
};
|
|
3062
3134
|
const ownPath = this.ownAuthPath();
|
|
3063
3135
|
await withFileLock(ownPath, async () => {
|
|
3064
|
-
const existing = await
|
|
3136
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
3137
|
+
if (existing === void 0) return;
|
|
3065
3138
|
const refreshedId = traeAccountId(refreshed);
|
|
3066
3139
|
const entries = existing.filter((item) => traeAccountId(item) !== refreshedId);
|
|
3067
3140
|
entries.push(refreshed);
|
|
@@ -9254,24 +9327,28 @@ async function writeTraePoolProbes(panel, updates) {
|
|
|
9254
9327
|
* member, and leaving it in the file would resurrect a stale `rate-limited` row if
|
|
9255
9328
|
* the account were added back later. Written through the same lock/atomic path so a
|
|
9256
9329
|
* removal cannot interleave with a batch.
|
|
9330
|
+
*
|
|
9331
|
+
* The READ also sits inside the lock, like {@link writeTraePoolProbes}'s: a
|
|
9332
|
+
* delete is a read-modify-write, and a read taken outside the lock snapshots a
|
|
9333
|
+
* moment that a concurrent batch's write can then be older than — the delete
|
|
9334
|
+
* would write that stale snapshot back and silently discard the measurements
|
|
9335
|
+
* the batch had just learned.
|
|
9257
9336
|
*/
|
|
9258
9337
|
async function dropTraePoolProbe(panel, accountId) {
|
|
9259
|
-
const existing = await readTraePoolProbes(panel);
|
|
9260
|
-
if (existing[accountId] === void 0) return;
|
|
9261
9338
|
const path = traePoolStorePath(panel);
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9339
|
+
await withFileLock(path, async () => {
|
|
9340
|
+
const existing = await readTraePoolProbes(panel);
|
|
9341
|
+
if (existing[accountId] === void 0) return;
|
|
9342
|
+
const probes = {};
|
|
9343
|
+
for (const [id, probe] of Object.entries(existing)) if (id !== accountId) probes[id] = probe;
|
|
9344
|
+
await writeFileAtomic(path, `${JSON.stringify({
|
|
9345
|
+
version: STORE_FORMAT_VERSION,
|
|
9346
|
+
probes
|
|
9347
|
+
}, null, 2)}\n`, {
|
|
9348
|
+
mode: 384,
|
|
9349
|
+
dirMode: 448
|
|
9273
9350
|
});
|
|
9274
|
-
}
|
|
9351
|
+
}).catch(() => {});
|
|
9275
9352
|
}
|
|
9276
9353
|
//#endregion
|
|
9277
9354
|
//#region src/model-config.ts
|
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": "4.4.
|
|
5
|
+
"version": "4.4.6",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -94,19 +94,19 @@
|
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
96
|
"@deepseek-ai/cordis": "^4.0.5",
|
|
97
|
-
"@deepseek-ai/dsh-atomic-write": "0.2.1-rc.
|
|
98
|
-
"@deepseek-ai/dsh-attachment": "0.2.1-rc.
|
|
99
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.
|
|
100
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.
|
|
101
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.
|
|
102
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.
|
|
103
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.1-rc.
|
|
104
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.
|
|
105
|
-
"@deepseek-ai/dsh-home-paths": "0.2.1-rc.
|
|
106
|
-
"@deepseek-ai/dsh-host-webserver": "0.2.1-rc.
|
|
107
|
-
"@deepseek-ai/dsh-llm": "0.2.1-rc.
|
|
108
|
-
"@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.
|
|
109
|
-
"@deepseek-ai/dsh-settings": "0.2.1-rc.
|
|
97
|
+
"@deepseek-ai/dsh-atomic-write": "0.2.1-rc.3",
|
|
98
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-rc.3",
|
|
99
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.3",
|
|
100
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.3",
|
|
101
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.3",
|
|
102
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.3",
|
|
103
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "0.2.1-rc.3",
|
|
104
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.3",
|
|
105
|
+
"@deepseek-ai/dsh-home-paths": "0.2.1-rc.3",
|
|
106
|
+
"@deepseek-ai/dsh-host-webserver": "0.2.1-rc.3",
|
|
107
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.3",
|
|
108
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.2.1-rc.3",
|
|
109
|
+
"@deepseek-ai/dsh-settings": "0.2.1-rc.3",
|
|
110
110
|
"@deepseek-ai/schemastery": "3.18.5",
|
|
111
111
|
"@earendil-works/pi-ai": "1.1.0",
|
|
112
112
|
"@testing-library/dom": "^10.4.2",
|