dsh-connect-trae 4.4.5 → 4.4.7
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 +106 -0
- package/README.en.md +18 -5
- package/README.md +29 -6
- package/lib/client.js +96 -29
- package/lib/index.d.ts +112 -1
- package/lib/index.js +212 -47
- package/package.json +14 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,111 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.7 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
> **4.4.6 把「读不出来的账号副本」改名留住了,但这件事一直是静默的——现在卡片会把它点名,
|
|
6
|
+
> 包括未登录那一屏。被顶掉的账号「去哪了」,从此不必再去 `~/.dsh` 里翻文件名。**
|
|
7
|
+
>
|
|
8
|
+
> 这一版不改任何账号、刷新、镜像的行为:读不懂的文件依然被隔离、依然不被覆盖。
|
|
9
|
+
> 改的是**看得见**:隔离发生的证据本来就在磁盘上,只是没人告诉用户它在那儿。
|
|
10
|
+
|
|
11
|
+
### 新增
|
|
12
|
+
|
|
13
|
+
- **隔离过的账号副本会被点名**(`feat(card)`)。
|
|
14
|
+
- 宿主侧新增 `TraeCredentialStore.quarantinedCopies()`:**扫描**副本文件旁边的
|
|
15
|
+
`.corrupt-*` 兄弟文件——不另设一本账,**证据本身就是证据**,用户删掉留存文件,提示也就跟着
|
|
16
|
+
消失,没有第二处状态要清理。报告内容是它原来的路径、现在的路径、字节数,以及
|
|
17
|
+
**能读成 JSON 时的账号数**(读不成就不猜:一个猜错的账号数比没有更糟)。
|
|
18
|
+
- 状态文档新增 `quarantined` 字段,**signed-in 与 signed-out 两种形状都带**。
|
|
19
|
+
- 卡片「诊断」栏新增一行「损坏副本留存」,点名那份留存文件。
|
|
20
|
+
- **未登录那一屏也显示同一条通知**:账号消失之后用户落地的正是那一屏,而那一屏根本不渲染分栏,
|
|
21
|
+
诊断栏根本看不到——只把通知放进诊断栏,等于没做。
|
|
22
|
+
- 一台健康的机器上什么都不显示:没有通知,也没有空盒子。
|
|
23
|
+
|
|
24
|
+
### 测试
|
|
25
|
+
|
|
26
|
+
- 新增 **9** 例:`tests/auth.spec.ts`(2 条——报告隔离文件、用户删掉后通知随之消失)、
|
|
27
|
+
`tests/web-status.spec.ts`(2 条——signed-out 与 signed-in 文档都带上通知)、
|
|
28
|
+
`tests/card-quarantine.spec.tsx`(5 条——诊断栏点名、未登录屏可见、账号数可读/不可读、健机器保持沉默)。
|
|
29
|
+
- 全仓 **1093** 例 / 85 个测试文件通过,双 tsconfig 类型检查通过。
|
|
30
|
+
|
|
31
|
+
### 文档
|
|
32
|
+
|
|
33
|
+
- `README.md` / `README.en.md` 的诊断栏说明补上这一行新事实,以及「未登录那一屏也会显示」。
|
|
34
|
+
|
|
35
|
+
> **给 #56 报告人的补充**:4.4.6 的隔离是静默的,所以「出现过隔离文件」这件事过去只能靠翻目录发现。
|
|
36
|
+
> 4.4.7 之后,卡片会直接把那份文件指给你——如果你的 `~/.dsh` 下已经有 `.corrupt-*` 文件,
|
|
37
|
+
> 升级后打开设置页就能看到它曾经存在,不必再自己找。
|
|
38
|
+
## 4.4.6 (2026-10-24)
|
|
39
|
+
|
|
40
|
+
> **对齐宿主 DSH 0.2.1-rc.3(纯版本推进,插件代码零改动),并修掉账号链路上三处
|
|
41
|
+
> 会静默丢数据的问题——其中一处可能正是 #56「被顶掉的账号若干时间后消失」的成因。**
|
|
42
|
+
>
|
|
43
|
+
> 宿主这一版只落地 5 个提交:三个是「导航项自定义」(面板固定/取消固定、More 菜单、埋点),
|
|
44
|
+
> 一个是 DeepSeek API-key 通道改走官方 `/models` 目录,一个是版本号——**均与本插件不相干**。
|
|
45
|
+
> 插件消费的 13 个宿主包里只有 `dsh-client-ui-primitives` 有代码变化,且全是新增,
|
|
46
|
+
> 而本插件对它只有类型层引用,因此**不需要任何适配**。
|
|
47
|
+
>
|
|
48
|
+
> 真正的风险在插件自己身上:一份读不懂的副本文件会被当成「一个账号都没有」,
|
|
49
|
+
> 而三条合并写路径又以它为基底——一次损坏读 + 一次刷新 = 其余账号的副本被无声抹掉。
|
|
50
|
+
|
|
51
|
+
### 修复
|
|
52
|
+
|
|
53
|
+
- **副本文件损坏不再清掉全部账号**(`fix(auth)`,PR #58)。
|
|
54
|
+
`parseOwn` 此前对「无法解析的文件」(半截写入、手改损坏、未知版本)一律返回空数组,而镜像 /
|
|
55
|
+
刷新 / 导入三条合并写路径都以它的结果为合并基底——一次损坏读 + 下一次刷新 = **其余全部账号的
|
|
56
|
+
副本被无声抹掉**,账号行随下一个轮询消失,且因为异常全被吞掉,日志里什么都看不到。
|
|
57
|
+
- `parseOwn` 改为三态(合法 / 合法空 / **无法解析返回 `undefined`**),新增 `readOwnForMerge`
|
|
58
|
+
收口三条写路径:遇到无法解析的文件先隔离为 `.corrupt-<时间戳>`(字节保留、可找回),
|
|
59
|
+
再从干净文件开始;**隔离失败则放弃本次写入,绝不覆盖读不懂的字节**。
|
|
60
|
+
- `deleteAccount` 对不可解析文件**拒绝**而非当空。
|
|
61
|
+
- 这可能是 #56(被顶掉的账号若干时间后消失)的一种成因——值得让报告者检查
|
|
62
|
+
`$DSH_HOME/.trae-auth.cn.json` 及其同目录的 `.corrupt-*` 隔离文件。
|
|
63
|
+
- **墓碑读-改-写收进一把锁**(`fix(auth)`,PR #58)。墓碑文件被两个并发流读改写:镜像
|
|
64
|
+
(清失效墓碑)与删除(加墓碑),此前都是锁外读、锁内写,丢更新的两个方向都疼——丢一个墓碑=
|
|
65
|
+
已删账号在下一轮镜像复活;丢一次清理=墓碑永久残留,该账号之后永远无法再被镜像。
|
|
66
|
+
收口为锁内 `updateTombstones(mutator)`。
|
|
67
|
+
- **池测量的删除不再丢并发写入**(`fix(pool)`,PR #58)。`dropTraePoolProbe` 的读取挪进锁内
|
|
68
|
+
(对齐同文件 `writeTraePoolProbes` 的范式):此前锁外读的快照会把并发批次刚写入的测量覆盖掉。
|
|
69
|
+
|
|
70
|
+
### 变更
|
|
71
|
+
|
|
72
|
+
- **对齐宿主 DSH 0.2.1-rc.3**:13 个 `@deepseek-ai/dsh-*` devDependency 从 `0.2.1-rc.2`
|
|
73
|
+
推进到 `0.2.1-rc.3`。`cordis`(`4.0.5`)/ `schemastery`(`3.18.5`)/ `pi-ai`(仍 `1.1.0`)
|
|
74
|
+
均未变——宿主 `app.asar` 实测随包的 pi-ai 仍是 1.1.0,3.4.2 起的双向适配结论原样成立。
|
|
75
|
+
- **peer 范围无需改动**:`0.2.1-rc.3` 对 `>=0.1.7-rc.1 <0.3.0-0` 已判通过(实测)。
|
|
76
|
+
- **插件代码零改动**:rc.3 对本插件消费的 13 个宿主包里,只有 `dsh-client-ui-primitives`
|
|
77
|
+
存在代码变化,且全部是**新增**(`TooltipSide` 增加 `'left'` 并改用实测边框盒换边放置、
|
|
78
|
+
`MenuItemButton` 增加 `current`、`Menu` 增加 `minWidthFromAnchor`、新增 `IconAppsAddOutline*`)。
|
|
79
|
+
本插件对 `dsh-client-ui-primitives` 只有**类型层引用**(`import type {} from ...`),
|
|
80
|
+
运行时不消费这些组件,故无需适配。
|
|
81
|
+
|
|
82
|
+
### 已核对确认无变化
|
|
83
|
+
|
|
84
|
+
- 逐字节比对 npm 上 `0.2.1-rc.2` 与 `0.2.1-rc.3` 两版的全量文件树:
|
|
85
|
+
`dsh-llm`、`dsh-llm-pi-ai`、`dsh-settings`、`dsh-host-webserver`、`dsh-atomic-write`、
|
|
86
|
+
`dsh-home-paths`、`dsh-attachment`、`dsh-client-ui-renderer`、`dsh-client-ui-settings`、
|
|
87
|
+
`dsh-client-ui-settings-plugins`、`dsh-client-ui-slots`、`dsh-client-locale`
|
|
88
|
+
——**除 package.json 的版本号与内部依赖号外逐字节相同**。
|
|
89
|
+
- 本插件注册的 5 个槽位(`plugins.bundle.config`、`plugins.row.config`、`conversation.input.left`、
|
|
90
|
+
`conversation.composer.dock`、`conversation.input.model.label`)在 rc.3 中**仍然存在且契约未变**;
|
|
91
|
+
宿主新增的 `conversation.session.header.views.actions` 等槽位与本插件无交集。
|
|
92
|
+
- rc.3 宿主上实测:宿主侧 Config 条目 `include:dsh-connect-trae` 在册,客户端
|
|
93
|
+
`plugins.bundle.config` / `plugins.row.config` / `conversation.input.left` 三处注册均 active。
|
|
94
|
+
|
|
95
|
+
### 测试
|
|
96
|
+
|
|
97
|
+
- PR #58 新增回归用例:损坏副本文件被隔离且字节保留、合法文件中坏一行只丢该行、
|
|
98
|
+
隔离失败时放弃写入且原字节不动、墓碑并发删除/写入序列化冒烟、池测量的 drop 与 write 不互相覆盖。
|
|
99
|
+
- `tests/dsh-peer-range.spec.ts` 的 `MUST_ADMIT` 补入 `0.2.1-rc.3`——此前这份「必须放行的版本」
|
|
100
|
+
清单里没有当前正在使用的版本,范围若被误收窄到当前宿主之外,测试仍会全绿。
|
|
101
|
+
- 全仓 **1084** 例通过(84 个测试文件),双 tsconfig 类型检查通过。
|
|
102
|
+
|
|
103
|
+
### 文档
|
|
104
|
+
|
|
105
|
+
- 新增 `docs/DSH_0.2.1_RC1_SYNC.md` 第十节:`0.2.1-rc.3` 复核记录(取证、依赖面、契约面、
|
|
106
|
+
槽位核对、peer 门禁)。
|
|
107
|
+
- `README.md` / `README.en.md` 的宿主版本结论推进到 `0.2.1-rc.3`,并新增
|
|
108
|
+
「`0.2.1-rc.3` 同步结论」小节。
|
|
3
109
|
## 4.4.5 (2026-10-23)
|
|
4
110
|
|
|
5
111
|
> **模型列表不再倒出官方已下架的历史配置:按 Trae IDE 名册退役 12 行、改钉 6 行,
|
package/README.en.md
CHANGED
|
@@ -36,7 +36,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
36
36
|
- **Enterprise is a separate billing channel, not an alias for CN** — the same CN credential, when it belongs to a `tob_online` tenant, is served by **two** gateways: the consumer pool (billed against personal credits) and the tenant pool (billed against the enterprise allowance). Same model, two payers, so **you** choose who pays; the plugin never guesses. `trae` never touches the enterprise gateway, and `trae-enterprise` **never falls back** — a refusal is reported as a refusal instead of quietly re-running on personal credits. Enterprise is **off by default**: a personal-only account cannot use the tenant pool, so shipping it enabled would mean handing everyone a provider that fails on first use.
|
|
37
37
|
- **Any panel can be switched off on its own** — each tab carries its own checkbox (CN and international checked by default, enterprise not): unchecking **withdraws that provider from the DSH model picker entirely** (not merely hiding the tab) and stops all of its requests; checking it again restores it with the account, directory, picks, and context budgets intact. If you never use the international side, just uncheck it.
|
|
38
38
|
- **Tabbed settings card** — the card's top carries a "Domestic / Enterprise / Global" tab bar; each tab holds its own account picker, usage overview, and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions. CN and enterprise share one CN credential pool but keep **separate account bindings and model picks**: the two rosters differ (`DeepSeek-V4.1-Flash` is listed only on the enterprise pool), so picks never overwrite each other.
|
|
39
|
-
- **The card then splits into "Account / Models / Diagnostics"** (learned from the sibling `dsh-connect-workbuddy`). The region row above is the **scope** — which provider this whole card is about — while these are sections of that one card: Account covers who serves, what is left, the check-in and the pool; Models covers which models to tick, image input, the context budget and Save; Diagnostics read-only lists the panel, the provider id, the serving account, both token deadlines, the catalog/enabled model counts, whether the pool and the check-in are offered, and the **verbatim** text of this panel's last reported failure. The split exists because the two halves **commit differently**: an account change lands on the click, a catalog change is a draft until Save — so a single long column put the save button three screens away from what it saves. Switching sections never refetches, never drops a half-filled model draft, and never rebuilds the pool's log. A signed-out card renders no section strip, so its one screen of instructions cannot be hidden behind a tab the user cannot see. **The Models section lays every model out at once, with no inner scrollbar** — it is not a little window scrolling inside a page that already scrolls; the card's own page is the only scroll.
|
|
39
|
+
- **The card then splits into "Account / Models / Diagnostics"** (learned from the sibling `dsh-connect-workbuddy`). The region row above is the **scope** — which provider this whole card is about — while these are sections of that one card: Account covers who serves, what is left, the check-in and the pool; Models covers which models to tick, image input, the context budget and Save; Diagnostics read-only lists the panel, the provider id, the serving account, both token deadlines, the catalog/enabled model counts, whether the pool and the check-in are offered, and the **verbatim** text of this panel's last reported failure. **As of 4.4.7 the diagnostics section carries one more line, "quarantined copies"**: a credential copy that could not be read — and was therefore renamed aside instead of being erased — is named there, together with how many accounts it still holds. "Where did the account I remember go?" is now answered by the card instead of by browsing `~/.dsh` for a file name; the signed-out screen shows the same notice, because that is the screen a user lands on after an account disappears. The split exists because the two halves **commit differently**: an account change lands on the click, a catalog change is a draft until Save — so a single long column put the save button three screens away from what it saves. Switching sections never refetches, never drops a half-filled model draft, and never rebuilds the pool's log. A signed-out card renders no section strip, so its one screen of instructions cannot be hidden behind a tab the user cannot see. **The Models section lays every model out at once, with no inner scrollbar** — it is not a little window scrolling inside a page that already scrolls; the card's own page is the only scroll.
|
|
40
40
|
- **Multiplier in the model name** — model names show the credit multiplier in Trae's own menu format (e.g. `GLM-5.2 · x0.79`), updated with each directory refresh.
|
|
41
41
|
- **Promotion badge** — Trae publishes its discount rules in the model directory (`activity_discount`: pay ratio, list price, off-peak windows with their time zone). The plugin judges them against **the clock at render time** and paints one capsule beside the model name (`5 折` / `off-peak 5 折` / `limited 5 折`), with the old price, the new price and the window on hover. Judging at render time is what keeps a night-time discount from still hanging there the next morning; when upstream says nothing is discounted right now no badge is painted, the multiplier is never used to guess a promotion, and no campaign without an end time is labelled "limited". It is information only: no request and no billing changes, and the international region (subscription) has no such data and therefore shows no badge. See [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md).
|
|
42
42
|
- **DSH local tool loop** — gets pending structured `tool_calls` from Trae `llm_utils_chat`, lets DSH execute its own local tools, then returns tool results to the model.
|
|
@@ -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
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
- **企业版是独立计费通道,不是国内版的别名** —— 同一张 CN 凭据若属于 `tob_online` 租户,会被**两个**网关同时服务:消费池(扣个人积分)与租户池(扣企业额度)。同一模型、两种付费方,所以「谁付钱」由你显式选,插件不替你猜。`trae` 永不碰企业网关;`trae-enterprise` **不回落**,被拒绝就如实报错,不会悄悄改用个人额度重跑。企业版**默认关闭**——纯个人账号用不了企业池,默认放开等于塞一条一用就失败的 provider。
|
|
37
37
|
- **可单独关闭任一面板** —— 三个 tab 各带一个勾选(国内版与国际版默认勾选,企业版默认不勾):取消勾选即把该面板的供应商**整个从 DSH 模型选择器撤掉**(不是只藏起 tab),并且不再发起任何请求;重新勾选即恢复,账号、目录、勾选与上下文预算全部保留。用不到国际版就把国际版关掉即可。
|
|
38
38
|
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 企业版 / 国际版」三个 tab,各含独立的账号选择、用量概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。国内版与企业版共用同一份 CN 凭据池,但**各有各的账号绑定与模型勾选**:两个池的名录并不相同(`DeepSeek-V4.1-Flash` 只有企业池列),所以勾选互不覆盖。
|
|
39
|
-
- **卡片内再按「账号 / 模型 / 诊断」分栏**(学自同族 `dsh-connect-workbuddy`)—— 上面那排地区是**作用域**(这张卡片讲的是哪个供应商),这一排是同一张卡片里的**分栏**:账号栏管「谁在服务、还剩多少、签到与账号池」,模型栏管「勾哪些模型、图片输入、上下文预算、保存」,诊断栏只读地列出面板名、供应商 id
|
|
39
|
+
- **卡片内再按「账号 / 模型 / 诊断」分栏**(学自同族 `dsh-connect-workbuddy`)—— 上面那排地区是**作用域**(这张卡片讲的是哪个供应商),这一排是同一张卡片里的**分栏**:账号栏管「谁在服务、还剩多少、签到与账号池」,模型栏管「勾哪些模型、图片输入、上下文预算、保存」,诊断栏只读地列出面板名、供应商 id、服务账号、两种令牌到期时间、目录/已启用模型数、账号池与签到支持与否,以及本面板最近一次报错的**原文**。**4.4.7 起诊断栏还多一行「损坏副本留存」**:读不出来、因此被改名留存的账号副本会在这里被点名(连同它曾经保存了几个账号)——被顶掉的账号「去哪了」这个问题,从此不用再去 `~/.dsh` 里翻文件名;未登录那一屏也会显示同一条通知,因为账号消失之后用户落地的正是那一屏。分栏的理由是**提交语义不同**:账号改动点一下即生效,模型改动是草稿、必须点「保存」——一列长页会把保存按钮放到离它要保存的东西三屏之外。切栏不重新拉取、不丢未保存的模型草稿、账号池日志也不重建;未登录时不出分栏,那一屏说明不会被看不见的 tab 藏起来。**模型栏把所有模型一次性铺开,没有内层滚动条**——不再是一个套在滚动页面里的小窗口,唯一滚动的就是卡片自己那一页。
|
|
40
40
|
- **账号池:自动选号 + 失败换号(默认关闭)** —— 本机读完几个账号后,「该用谁的额度」可以交给插件按规则排序:**先排除不可用的**(凭据被拒、套餐不覆盖、账号已不存在、限流未到重试时间、额度耗尽),再在可用账号里**优先快到期的那点额度**(与其放着过期,不如先花掉),最后才比总额、token 新鲜度,并以账号 id 兜底成**全序**(同样的输入永远得到同样的顺序,不会在两行之间来回抖)。三个面板**各有各的池**:国内版与企业版共用同一份 CN 凭据,但企业版只挑租户账号、国内版不会漂到租户号上。开着池时,发请求前先按上面的顺序选中一个账号,并且**真实请求失败会换下一个账号重试(最多 3 个)**——只有账号级失败才换(`401` 凭据被拒、额度耗尽、限流、上游 5xx);模型 id 写错、请求本身不合法**不换号**,因为那不是账号的问题,换号只会把同一个错误烧两遍、还冤枉一个没机会服务的账号。换号失败的真相会写进池的探针记录,下一次排序直接把它排到后面,而每一次结论都能在卡片上看到:`可用` / `限流` / `额度耗尽` / `凭据被拒` / `套餐不覆盖` / `上游不提供` / `本次未测出`,以及「什么时候可以重试」。**关掉池就回到严格绑定**,不会有任何残留 override。卡片里的「全部测速」会把每个成员按目标模型各测一次,结果**边跑边显示**(一行一个答案,不等整批跑完),并落盘成下次排序的依据。
|
|
41
41
|
|
|
42
42
|
> 池是**显式选择**:默认关闭。它不会替你在没打开池的情况下换账号——「换账号等于换人计费」,所以这件事只能由你点头。池打开后卡片会明确写出**当前是谁在付费**(Host 报告的是「下一个真正发送请求的账号」,不是保存的那个绑定)。
|
|
@@ -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/client.js
CHANGED
|
@@ -1183,6 +1183,16 @@ window.__ModuleLoader__.load({
|
|
|
1183
1183
|
.dsm-trae-searched-list li{display:flex;flex-direction:column;gap:1px;min-width:0}
|
|
1184
1184
|
.dsm-trae-searched-list code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#b8b8b8);word-break:break-all}
|
|
1185
1185
|
.dsm-trae-searched-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
|
|
1186
|
+
/* The quarantine notice: a copy that was kept aside instead of being erased.
|
|
1187
|
+
It borrows the "probed paths" look but stands on its own class, because it
|
|
1188
|
+
is a different KIND of fact — and because a screen may carry both. */
|
|
1189
|
+
.dsm-trae-quarantine{margin-top:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
1190
|
+
.dsm-trae-quarantine>summary{cursor:pointer;color:var(--dsw-alias-label-secondary,#b8b8b8);user-select:none}
|
|
1191
|
+
.dsm-trae-quarantine-hint{margin:6px 0 4px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
1192
|
+
.dsm-trae-quarantine-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
|
|
1193
|
+
.dsm-trae-quarantine-list li{display:flex;flex-direction:column;gap:1px;min-width:0}
|
|
1194
|
+
.dsm-trae-quarantine-list code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#b8b8b8);word-break:break-all}
|
|
1195
|
+
.dsm-trae-quarantine-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
|
|
1186
1196
|
.dsm-trae-usage-cheer:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:2px}
|
|
1187
1197
|
.dsm-trae-checkin{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:14px;background:var(--dsw-alias-bg-layer-2,#24262c)}
|
|
1188
1198
|
.dsm-trae-checkin-copy{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
@@ -5805,6 +5815,19 @@ window.__ModuleLoader__.load({
|
|
|
5805
5815
|
children: failures.join(" · ")
|
|
5806
5816
|
});
|
|
5807
5817
|
})()]
|
|
5818
|
+
}),
|
|
5819
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5820
|
+
className: "dsm-trae-diag-row",
|
|
5821
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5822
|
+
className: "dsm-trae-diag-label",
|
|
5823
|
+
children: t("row.diagQuarantine")
|
|
5824
|
+
}), status.quarantined === void 0 || status.quarantined.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5825
|
+
className: "dsm-trae-diag-value",
|
|
5826
|
+
children: t("row.diagQuarantineNone")
|
|
5827
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5828
|
+
className: "dsm-trae-diag-value dsm-trae-diag-bad dsm-trae-diag-mono",
|
|
5829
|
+
children: status.quarantined.map((item) => item.quarantinedAs).join(" · ")
|
|
5830
|
+
})]
|
|
5808
5831
|
})
|
|
5809
5832
|
]
|
|
5810
5833
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -5817,35 +5840,65 @@ window.__ModuleLoader__.load({
|
|
|
5817
5840
|
role: "status",
|
|
5818
5841
|
children: t("row.accountsLoading")
|
|
5819
5842
|
}) }) : null,
|
|
5820
|
-
status.status === "signed-out" && !loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5843
|
+
status.status === "signed-out" && !loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5844
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5845
|
+
className: "dsm-trae-usage-text",
|
|
5846
|
+
children: status.message ?? t("row.signedOutHint")
|
|
5847
|
+
}),
|
|
5848
|
+
status.quarantined && status.quarantined.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
5849
|
+
className: "dsm-trae-quarantine",
|
|
5850
|
+
open: true,
|
|
5851
|
+
children: [
|
|
5852
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
|
|
5853
|
+
t("row.quarantineTitle"),
|
|
5854
|
+
" (",
|
|
5855
|
+
status.quarantined.length,
|
|
5856
|
+
")"
|
|
5857
|
+
] }),
|
|
5858
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5859
|
+
className: "dsm-trae-quarantine-hint",
|
|
5860
|
+
children: t("row.quarantineHint")
|
|
5861
|
+
}),
|
|
5862
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
5863
|
+
className: "dsm-trae-quarantine-list",
|
|
5864
|
+
children: status.quarantined.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: item.quarantinedAs }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5865
|
+
className: "dsm-trae-quarantine-reason",
|
|
5866
|
+
children: [
|
|
5867
|
+
item.accounts === void 0 ? t("row.quarantineAccountsUnknown") : t("row.quarantineAccounts", { count: item.accounts }),
|
|
5868
|
+
" · ",
|
|
5869
|
+
t("row.quarantineBytes", { bytes: item.bytes })
|
|
5870
|
+
]
|
|
5871
|
+
})] }, item.quarantinedAs))
|
|
5872
|
+
})
|
|
5873
|
+
]
|
|
5874
|
+
}) : null,
|
|
5875
|
+
status.searched && status.searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
5876
|
+
className: "dsm-trae-searched",
|
|
5877
|
+
children: [
|
|
5878
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
|
|
5879
|
+
t("row.searchedTitle"),
|
|
5880
|
+
" (",
|
|
5881
|
+
status.searched.length,
|
|
5882
|
+
")"
|
|
5883
|
+
] }),
|
|
5884
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5885
|
+
className: "dsm-trae-searched-hint",
|
|
5886
|
+
children: t("row.searchedHint")
|
|
5887
|
+
}),
|
|
5888
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
5889
|
+
className: "dsm-trae-searched-list",
|
|
5890
|
+
children: status.searched.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: item.path }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5891
|
+
className: "dsm-trae-searched-reason",
|
|
5892
|
+
children: [
|
|
5893
|
+
t(item.source === "cli" ? "row.sourceCli" : "row.sourceDesktop"),
|
|
5894
|
+
" · ",
|
|
5895
|
+
t(item.reason === "missing" ? "row.reasonMissing" : item.reason === "unreadable" ? "row.reasonUnreadable" : "row.reasonInvalid")
|
|
5896
|
+
]
|
|
5897
|
+
})] }, `${item.source}:${item.path}`))
|
|
5898
|
+
})
|
|
5899
|
+
]
|
|
5900
|
+
}) : null
|
|
5901
|
+
] }) : null,
|
|
5849
5902
|
status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5850
5903
|
className: "dsm-trae-usage-error",
|
|
5851
5904
|
children: status.message
|
|
@@ -6583,6 +6636,8 @@ window.__ModuleLoader__.load({
|
|
|
6583
6636
|
"row.diagRawOff": "not checked",
|
|
6584
6637
|
"row.diagFailures": "Last reported failure",
|
|
6585
6638
|
"row.diagNoFailures": "none",
|
|
6639
|
+
"row.diagQuarantine": "Quarantined copies",
|
|
6640
|
+
"row.diagQuarantineNone": "none",
|
|
6586
6641
|
"row.diagHint": "Everything above is read-only and can be quoted verbatim into a bug report. The values are this panel’s own last refresh, not a summary of it.",
|
|
6587
6642
|
"row.settingsWriteFailed": "The setting was not saved: {message}",
|
|
6588
6643
|
"row.signedOut": "Not signed in",
|
|
@@ -6590,6 +6645,11 @@ window.__ModuleLoader__.load({
|
|
|
6590
6645
|
"row.searchedTitle": "Paths checked",
|
|
6591
6646
|
"row.searchedHint": "No sign-in was found. These locations were probed; if your Trae stores its login elsewhere, please report the path.",
|
|
6592
6647
|
"row.searchedMore": "Show {count} more",
|
|
6648
|
+
"row.quarantineTitle": "Quarantined credential copies",
|
|
6649
|
+
"row.quarantineHint": "A credential copy could not be read, so the plugin did not overwrite it — it was renamed aside with its bytes intact. The accounts it held exist only in that file; delete it once you have them back.",
|
|
6650
|
+
"row.quarantineAccounts": "{count} account(s)",
|
|
6651
|
+
"row.quarantineAccountsUnknown": "account count unreadable",
|
|
6652
|
+
"row.quarantineBytes": "{bytes} bytes",
|
|
6593
6653
|
"row.reasonMissing": "not found",
|
|
6594
6654
|
"row.reasonUnreadable": "unreadable",
|
|
6595
6655
|
"row.reasonInvalid": "unreadable format",
|
|
@@ -6953,6 +7013,8 @@ window.__ModuleLoader__.load({
|
|
|
6953
7013
|
"row.diagRawOff": "未检测",
|
|
6954
7014
|
"row.diagFailures": "最近一次报错",
|
|
6955
7015
|
"row.diagNoFailures": "无",
|
|
7016
|
+
"row.diagQuarantine": "损坏副本留存",
|
|
7017
|
+
"row.diagQuarantineNone": "无",
|
|
6956
7018
|
"row.diagHint": "以上全部只读,可整段原样贴进反馈。数值来自本面板最近一次刷新,不是概括。",
|
|
6957
7019
|
"row.settingsWriteFailed": "设置未能保存:{message}",
|
|
6958
7020
|
"row.signedOut": "未登录",
|
|
@@ -6960,6 +7022,11 @@ window.__ModuleLoader__.load({
|
|
|
6960
7022
|
"row.searchedTitle": "已检查的路径",
|
|
6961
7023
|
"row.searchedHint": "未找到登录信息。以下是插件探测过的位置;如果你的 Trae 把登录信息存在别处,请把该路径反馈给我们。",
|
|
6962
7024
|
"row.searchedMore": "展开其余 {count} 条",
|
|
7025
|
+
"row.quarantineTitle": "被隔离的账号副本",
|
|
7026
|
+
"row.quarantineHint": "有一份账号副本文件读不出来,插件没有覆盖它,而是改名留存(字节完整)。它曾经保存的账号只可能在那份文件里;确认已取回后删掉它即可不再提示。",
|
|
7027
|
+
"row.quarantineAccounts": "含 {count} 个账号",
|
|
7028
|
+
"row.quarantineAccountsUnknown": "账号数无法读取",
|
|
7029
|
+
"row.quarantineBytes": "{bytes} 字节",
|
|
6963
7030
|
"row.reasonMissing": "文件不存在",
|
|
6964
7031
|
"row.reasonUnreadable": "无法读取",
|
|
6965
7032
|
"row.reasonInvalid": "格式无法识别",
|
package/lib/index.d.ts
CHANGED
|
@@ -1081,6 +1081,29 @@ interface TraeWebSearchPath {
|
|
|
1081
1081
|
reason: 'missing' | 'unreadable' | 'invalid';
|
|
1082
1082
|
message?: string;
|
|
1083
1083
|
}
|
|
1084
|
+
/**
|
|
1085
|
+
* A plugin-owned credential copy that was found unreadable and moved aside.
|
|
1086
|
+
*
|
|
1087
|
+
* Since 4.4.6 such a copy is quarantined (renamed aside, bytes intact)
|
|
1088
|
+
* instead of being merged over, because merging over it would erase every
|
|
1089
|
+
* account it carried. The copy then exists ONLY in that quarantine file — so
|
|
1090
|
+
* the card says so, instead of the user discovering it by noticing accounts
|
|
1091
|
+
* missing (issue #56).
|
|
1092
|
+
*
|
|
1093
|
+
* Only facts about the plugin's own file cross to the browser: its path, the
|
|
1094
|
+
* size of the quarantine copy, and how many accounts it still shows. No token
|
|
1095
|
+
* material, and no path outside DSH's own home.
|
|
1096
|
+
*/
|
|
1097
|
+
interface TraeWebQuarantine {
|
|
1098
|
+
/** The live path the copy had before it was moved aside. */
|
|
1099
|
+
file: string;
|
|
1100
|
+
/** The quarantine copy's path; its name carries the moment it was taken. */
|
|
1101
|
+
quarantinedAs: string;
|
|
1102
|
+
/** Size of the quarantined copy in bytes (`0` when it could not be read). */
|
|
1103
|
+
bytes: number;
|
|
1104
|
+
/** Accounts the copy still shows, when it could be read as JSON. */
|
|
1105
|
+
accounts?: number;
|
|
1106
|
+
}
|
|
1084
1107
|
/**
|
|
1085
1108
|
* Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
|
|
1086
1109
|
* volatile-marked settings field as (see {@link unwrapVolatileDeep}).
|
|
@@ -1229,6 +1252,12 @@ type TraeWebUsage = {
|
|
|
1229
1252
|
accounts: readonly TraeWebAccount[];
|
|
1230
1253
|
message?: string;
|
|
1231
1254
|
searched?: readonly TraeWebSearchPath[];
|
|
1255
|
+
/**
|
|
1256
|
+
* Copies that were found unreadable and moved aside. Present on BOTH
|
|
1257
|
+
* shapes on purpose: the signed-out screen is exactly where a user lands
|
|
1258
|
+
* after a lost copy, so that is where the notice has to be.
|
|
1259
|
+
*/
|
|
1260
|
+
quarantined?: readonly TraeWebQuarantine[];
|
|
1232
1261
|
enabled?: boolean;
|
|
1233
1262
|
} | {
|
|
1234
1263
|
status: 'signed-in';
|
|
@@ -1249,6 +1278,12 @@ type TraeWebUsage = {
|
|
|
1249
1278
|
accounts: readonly TraeWebAccount[];
|
|
1250
1279
|
models: readonly TraeWebModel[];
|
|
1251
1280
|
enabledModelIds: readonly string[];
|
|
1281
|
+
/**
|
|
1282
|
+
* Copies that were found unreadable and moved aside. Present on BOTH
|
|
1283
|
+
* shapes on purpose: the signed-out screen is exactly where a user lands
|
|
1284
|
+
* after a lost copy, so that is where the notice has to be.
|
|
1285
|
+
*/
|
|
1286
|
+
quarantined?: readonly TraeWebQuarantine[];
|
|
1252
1287
|
rawChat?: {
|
|
1253
1288
|
state: 'disabled' | 'unchecked' | 'available' | 'protocol-gated' | 'authentication' | 'credit' | 'rate' | 'transport' | 'server';
|
|
1254
1289
|
checkedAtMs?: number;
|
|
@@ -1786,6 +1821,34 @@ interface TraeCandidateFailure {
|
|
|
1786
1821
|
reason: 'missing' | 'unreadable' | 'invalid';
|
|
1787
1822
|
message?: string;
|
|
1788
1823
|
}
|
|
1824
|
+
/**
|
|
1825
|
+
* A plugin-owned copy that was found UNREADABLE and therefore moved aside.
|
|
1826
|
+
*
|
|
1827
|
+
* Since 4.4.6 a copy this build cannot parse is never merged over: it is
|
|
1828
|
+
* renamed to `<file>.corrupt-<stamp>` (bytes intact, recoverable) and the merge
|
|
1829
|
+
* restarts from a clean file. That trade — a quarantined copy instead of a
|
|
1830
|
+
* silently emptied one — is only fair if the USER can see it happened, because
|
|
1831
|
+
* the accounts the copy carried are, from that moment on, only in the
|
|
1832
|
+
* quarantine file.
|
|
1833
|
+
*
|
|
1834
|
+
* This notice is what makes that visible: the card's diagnostics panel shows
|
|
1835
|
+
* it, and so does the signed-out screen — which is exactly the state a user
|
|
1836
|
+
* lands in after the loss (issue #56).
|
|
1837
|
+
*/
|
|
1838
|
+
interface TraeQuarantineNotice {
|
|
1839
|
+
/** The live path the copy had before it was moved aside. */
|
|
1840
|
+
file: string;
|
|
1841
|
+
/** The quarantine copy's full path; its name carries the moment it was taken. */
|
|
1842
|
+
quarantinedAs: string;
|
|
1843
|
+
/** Size of the quarantined copy in bytes (`0` when it could not be read). */
|
|
1844
|
+
bytes: number;
|
|
1845
|
+
/**
|
|
1846
|
+
* How many accounts the quarantined copy held, when it could still be read
|
|
1847
|
+
* as JSON. A copy quarantined for being TORN cannot be counted, and this is
|
|
1848
|
+
* then absent rather than guessed.
|
|
1849
|
+
*/
|
|
1850
|
+
accounts?: number;
|
|
1851
|
+
}
|
|
1789
1852
|
/**
|
|
1790
1853
|
* Plugin-owned copy path for one region. Each region's store refreshes into
|
|
1791
1854
|
* its own file so two simultaneously signed-in regions never overwrite each
|
|
@@ -1889,7 +1952,40 @@ declare class TraeCredentialStore {
|
|
|
1889
1952
|
* account, it never locks the user out.
|
|
1890
1953
|
*/
|
|
1891
1954
|
private readTombstones;
|
|
1892
|
-
|
|
1955
|
+
/**
|
|
1956
|
+
* Read, mutate, and write the tombstone list under ONE lock.
|
|
1957
|
+
*
|
|
1958
|
+
* The tombstone file is read-modify-write state shared by two flows that run
|
|
1959
|
+
* concurrently: `mirrorLocalLogins` (drops tombstones whose logins are gone)
|
|
1960
|
+
* and `deleteAccount` (adds the id just deleted). A read OUTSIDE the write's
|
|
1961
|
+
* lock loses updates in both directions — a dropped-add resurrects a deleted
|
|
1962
|
+
* account on the next mirror, a dropped-drop keeps a tombstone forever and
|
|
1963
|
+
* hides every future sign-in of that account. The mutator therefore runs on
|
|
1964
|
+
* the freshly-read set INSIDE the lock, and what it returns is what gets
|
|
1965
|
+
* written, with no window in between.
|
|
1966
|
+
*
|
|
1967
|
+
* Fire-and-forget safe: a failed update only delays a tombstone change until
|
|
1968
|
+
* the next mirror or delete, so errors are swallowed like the readers'.
|
|
1969
|
+
*/
|
|
1970
|
+
private updateTombstones;
|
|
1971
|
+
/**
|
|
1972
|
+
* Read the plugin-owned file as the merge base for a read-modify-write.
|
|
1973
|
+
*
|
|
1974
|
+
* This is the fail-safe that keeps one unreadable file from erasing every
|
|
1975
|
+
* account: `parseOwn` now distinguishes a VALID empty file (`[]`) from one
|
|
1976
|
+
* this build cannot parse (`undefined`), and only the former may serve as a
|
|
1977
|
+
* merge base. For the latter the file is quarantined — renamed aside with
|
|
1978
|
+
* its bytes intact — and the merge then starts from nothing, so the action
|
|
1979
|
+
* the user pressed still succeeds and the lost entries are recoverable from
|
|
1980
|
+
* the quarantine copy instead of being overwritten silently. A quarantine
|
|
1981
|
+
* that fails (an exotic filesystem, a name collision) aborts the write
|
|
1982
|
+
* instead: losing one refresh is recoverable by pressing it again, losing
|
|
1983
|
+
* every account is not.
|
|
1984
|
+
*
|
|
1985
|
+
* Every caller must hold the {@link withFileLock} of `path` across BOTH the
|
|
1986
|
+
* read and the subsequent write — this does not lock by itself.
|
|
1987
|
+
*/
|
|
1988
|
+
private readOwnForMerge;
|
|
1893
1989
|
/**
|
|
1894
1990
|
* Copy every local Trae sign-in into this store's own file, once.
|
|
1895
1991
|
*
|
|
@@ -2176,6 +2272,21 @@ declare class TraeCredentialStore {
|
|
|
2176
2272
|
* files are skipped rather than propagated.
|
|
2177
2273
|
*/
|
|
2178
2274
|
private readOwns;
|
|
2275
|
+
/**
|
|
2276
|
+
* The copies that were found unreadable and moved aside.
|
|
2277
|
+
*
|
|
2278
|
+
* SCANNED, not remembered: the quarantine file is itself the durable
|
|
2279
|
+
* record (its name carries the moment it was taken), so the plugin does not
|
|
2280
|
+
* keep a second, parallel ledger that could disagree with the disk. The
|
|
2281
|
+
* notice therefore appears exactly as long as the evidence does — and
|
|
2282
|
+
* disappears when the user deletes the quarantined copy, which is what
|
|
2283
|
+
* "I have dealt with it" means.
|
|
2284
|
+
*
|
|
2285
|
+
* A notice is a FACT about the user's disk, never about their credentials:
|
|
2286
|
+
* it names a path, a size and a count, and the path is the one the plugin
|
|
2287
|
+
* owns (inside DSH's own home), never a Trae application directory.
|
|
2288
|
+
*/
|
|
2289
|
+
quarantinedCopies(): Promise<TraeQuarantineNotice[]>;
|
|
2179
2290
|
private refreshNow;
|
|
2180
2291
|
}
|
|
2181
2292
|
//#endregion
|
package/lib/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completio
|
|
|
4
4
|
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
5
5
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
7
|
+
import { basename, 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, readdir, 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
|
-
return document.credentials.map(credentialOfStored).filter((value) => value !== void 0);
|
|
2233
|
-
}
|
|
2234
|
-
if (document.version === OWN_VERSION_SINGLE) {
|
|
2235
|
-
const single = credentialOfStored(document.credential);
|
|
2236
|
-
return single === void 0 ? [] : [single];
|
|
2237
|
-
}
|
|
2238
|
-
return [];
|
|
2237
|
+
const parsed = JSON.parse(text);
|
|
2238
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
2239
|
+
document = parsed;
|
|
2239
2240
|
} catch {
|
|
2240
|
-
return
|
|
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;
|
|
2273
|
+
} catch {
|
|
2274
|
+
return false;
|
|
2241
2275
|
}
|
|
2242
2276
|
}
|
|
2243
2277
|
/**
|
|
@@ -2259,6 +2293,30 @@ async function writeOwn(path, credentials) {
|
|
|
2259
2293
|
dirMode: 448
|
|
2260
2294
|
});
|
|
2261
2295
|
}
|
|
2296
|
+
/**
|
|
2297
|
+
* How many accounts a quarantined copy still shows, when it can be read at all.
|
|
2298
|
+
*
|
|
2299
|
+
* A copy is quarantined because this build could not PARSE it — but not every
|
|
2300
|
+
* unparseable copy is unreadable: a file whose JSON is intact yet carries a
|
|
2301
|
+
* version this build does not know still parses as JSON, and its account rows
|
|
2302
|
+
* are then countable. That number is what tells the user whether one account
|
|
2303
|
+
* or five went missing, so it is worth the attempt.
|
|
2304
|
+
*
|
|
2305
|
+
* A torn copy yields nothing: the count is then ABSENT rather than guessed,
|
|
2306
|
+
* because a wrong count is worse than no count.
|
|
2307
|
+
*/
|
|
2308
|
+
function countStoredAccounts(text) {
|
|
2309
|
+
try {
|
|
2310
|
+
const parsed = JSON.parse(text);
|
|
2311
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
2312
|
+
const document = parsed;
|
|
2313
|
+
if (Array.isArray(document.credentials)) return document.credentials.length;
|
|
2314
|
+
if (document.credential !== void 0) return 1;
|
|
2315
|
+
return;
|
|
2316
|
+
} catch {
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2262
2320
|
/** The key Trae stores the per-user region map under, beside the login itself. */
|
|
2263
2321
|
const TRAE_USERTAG_STORAGE_KEY = "iCubeAuthInfo://usertag";
|
|
2264
2322
|
/**
|
|
@@ -2415,9 +2473,26 @@ var TraeCredentialStore = class {
|
|
|
2415
2473
|
return /* @__PURE__ */ new Set();
|
|
2416
2474
|
}
|
|
2417
2475
|
}
|
|
2418
|
-
|
|
2476
|
+
/**
|
|
2477
|
+
* Read, mutate, and write the tombstone list under ONE lock.
|
|
2478
|
+
*
|
|
2479
|
+
* The tombstone file is read-modify-write state shared by two flows that run
|
|
2480
|
+
* concurrently: `mirrorLocalLogins` (drops tombstones whose logins are gone)
|
|
2481
|
+
* and `deleteAccount` (adds the id just deleted). A read OUTSIDE the write's
|
|
2482
|
+
* lock loses updates in both directions — a dropped-add resurrects a deleted
|
|
2483
|
+
* account on the next mirror, a dropped-drop keeps a tombstone forever and
|
|
2484
|
+
* hides every future sign-in of that account. The mutator therefore runs on
|
|
2485
|
+
* the freshly-read set INSIDE the lock, and what it returns is what gets
|
|
2486
|
+
* written, with no window in between.
|
|
2487
|
+
*
|
|
2488
|
+
* Fire-and-forget safe: a failed update only delays a tombstone change until
|
|
2489
|
+
* the next mirror or delete, so errors are swallowed like the readers'.
|
|
2490
|
+
*/
|
|
2491
|
+
async updateTombstones(mutator) {
|
|
2419
2492
|
const path = this.deletedPath();
|
|
2420
2493
|
await withFileLock(path, async () => {
|
|
2494
|
+
const ids = await this.readTombstones();
|
|
2495
|
+
mutator(ids);
|
|
2421
2496
|
await writeFileAtomic(path, `${JSON.stringify({
|
|
2422
2497
|
version: 1,
|
|
2423
2498
|
ids: [...ids]
|
|
@@ -2425,7 +2500,31 @@ var TraeCredentialStore = class {
|
|
|
2425
2500
|
mode: 384,
|
|
2426
2501
|
dirMode: 448
|
|
2427
2502
|
});
|
|
2428
|
-
});
|
|
2503
|
+
}).catch(() => void 0);
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Read the plugin-owned file as the merge base for a read-modify-write.
|
|
2507
|
+
*
|
|
2508
|
+
* This is the fail-safe that keeps one unreadable file from erasing every
|
|
2509
|
+
* account: `parseOwn` now distinguishes a VALID empty file (`[]`) from one
|
|
2510
|
+
* this build cannot parse (`undefined`), and only the former may serve as a
|
|
2511
|
+
* merge base. For the latter the file is quarantined — renamed aside with
|
|
2512
|
+
* its bytes intact — and the merge then starts from nothing, so the action
|
|
2513
|
+
* the user pressed still succeeds and the lost entries are recoverable from
|
|
2514
|
+
* the quarantine copy instead of being overwritten silently. A quarantine
|
|
2515
|
+
* that fails (an exotic filesystem, a name collision) aborts the write
|
|
2516
|
+
* instead: losing one refresh is recoverable by pressing it again, losing
|
|
2517
|
+
* every account is not.
|
|
2518
|
+
*
|
|
2519
|
+
* Every caller must hold the {@link withFileLock} of `path` across BOTH the
|
|
2520
|
+
* read and the subsequent write — this does not lock by itself.
|
|
2521
|
+
*/
|
|
2522
|
+
async readOwnForMerge(path) {
|
|
2523
|
+
const text = await readFile(path, "utf8").catch(() => void 0);
|
|
2524
|
+
if (text === void 0) return [];
|
|
2525
|
+
const parsed = parseOwn(text);
|
|
2526
|
+
if (parsed !== void 0) return parsed;
|
|
2527
|
+
return await quarantineCorruptOwn(path) ? [] : void 0;
|
|
2429
2528
|
}
|
|
2430
2529
|
/**
|
|
2431
2530
|
* Copy every local Trae sign-in into this store's own file, once.
|
|
@@ -2447,20 +2546,17 @@ var TraeCredentialStore = class {
|
|
|
2447
2546
|
const { credentials: discovered } = await this.readDesktopAll();
|
|
2448
2547
|
const own = await this.readOwns();
|
|
2449
2548
|
const ownIds = new Set(own.map((credential) => traeAccountId(credential)));
|
|
2450
|
-
const tombstones = await this.readTombstones();
|
|
2451
2549
|
const liveIds = new Set(discovered.map((credential) => traeAccountId(credential)));
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
if (liveIds.has(id))
|
|
2455
|
-
|
|
2456
|
-
tombstonesChanged = true;
|
|
2457
|
-
}
|
|
2550
|
+
const tombstones = await this.readTombstones();
|
|
2551
|
+
if ([...tombstones].some((id) => !liveIds.has(id))) await this.updateTombstones((ids) => {
|
|
2552
|
+
for (const id of [...ids]) if (!liveIds.has(id)) ids.delete(id);
|
|
2553
|
+
});
|
|
2458
2554
|
const missing = discovered.filter((credential) => this.matchesRegion(credential) && !ownIds.has(traeAccountId(credential)) && !tombstones.has(traeAccountId(credential)));
|
|
2459
|
-
if (tombstonesChanged) await this.writeTombstones(tombstones);
|
|
2460
2555
|
if (missing.length === 0) return;
|
|
2461
2556
|
const ownPath = this.ownAuthPath();
|
|
2462
2557
|
await withFileLock(ownPath, async () => {
|
|
2463
|
-
const existing = await
|
|
2558
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
2559
|
+
if (existing === void 0) return;
|
|
2464
2560
|
const byId = new Map(existing.map((credential) => [traeAccountId(credential), credential]));
|
|
2465
2561
|
for (const credential of missing) {
|
|
2466
2562
|
const id = traeAccountId(credential);
|
|
@@ -2748,7 +2844,8 @@ var TraeCredentialStore = class {
|
|
|
2748
2844
|
const ownPath = this.ownAuthPath();
|
|
2749
2845
|
const written = [];
|
|
2750
2846
|
await withFileLock(ownPath, async () => {
|
|
2751
|
-
const existing = await
|
|
2847
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
2848
|
+
if (existing === void 0) return;
|
|
2752
2849
|
const byId = /* @__PURE__ */ new Map();
|
|
2753
2850
|
for (const item of existing) byId.set(traeAccountId(item), item);
|
|
2754
2851
|
for (const credential of credentials) {
|
|
@@ -2907,7 +3004,7 @@ var TraeCredentialStore = class {
|
|
|
2907
3004
|
let mutated = false;
|
|
2908
3005
|
for (const path of this.ownCandidates()) {
|
|
2909
3006
|
const dropped = await withFileLock(path, async () => {
|
|
2910
|
-
const existing = await readFile(path, "utf8").then(parseOwn, () => null);
|
|
3007
|
+
const existing = await readFile(path, "utf8").then((text) => parseOwn(text) ?? null, () => null);
|
|
2911
3008
|
if (existing === null || !existing.some((credential) => traeAccountId(credential) === accountId)) return false;
|
|
2912
3009
|
const next = existing.filter((credential) => traeAccountId(credential) !== accountId);
|
|
2913
3010
|
await writeOwn(path, next);
|
|
@@ -2915,11 +3012,9 @@ var TraeCredentialStore = class {
|
|
|
2915
3012
|
}).catch(() => false);
|
|
2916
3013
|
mutated = mutated || dropped;
|
|
2917
3014
|
}
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
await this.writeTombstones(tombstones).catch(() => void 0);
|
|
2922
|
-
}
|
|
3015
|
+
await this.updateTombstones((ids) => {
|
|
3016
|
+
ids.add(accountId);
|
|
3017
|
+
});
|
|
2923
3018
|
if (this.accountId === accountId) {
|
|
2924
3019
|
this.accountId = void 0;
|
|
2925
3020
|
this.inflight = void 0;
|
|
@@ -3039,10 +3134,59 @@ var TraeCredentialStore = class {
|
|
|
3039
3134
|
async readOwns() {
|
|
3040
3135
|
const copies = [];
|
|
3041
3136
|
for (const path of this.ownCandidates()) try {
|
|
3042
|
-
|
|
3137
|
+
const parsed = parseOwn(await readFile(path, "utf8"));
|
|
3138
|
+
if (parsed !== void 0) copies.push(...parsed);
|
|
3043
3139
|
} catch {}
|
|
3044
3140
|
return copies;
|
|
3045
3141
|
}
|
|
3142
|
+
/**
|
|
3143
|
+
* The copies that were found unreadable and moved aside.
|
|
3144
|
+
*
|
|
3145
|
+
* SCANNED, not remembered: the quarantine file is itself the durable
|
|
3146
|
+
* record (its name carries the moment it was taken), so the plugin does not
|
|
3147
|
+
* keep a second, parallel ledger that could disagree with the disk. The
|
|
3148
|
+
* notice therefore appears exactly as long as the evidence does — and
|
|
3149
|
+
* disappears when the user deletes the quarantined copy, which is what
|
|
3150
|
+
* "I have dealt with it" means.
|
|
3151
|
+
*
|
|
3152
|
+
* A notice is a FACT about the user's disk, never about their credentials:
|
|
3153
|
+
* it names a path, a size and a count, and the path is the one the plugin
|
|
3154
|
+
* owns (inside DSH's own home), never a Trae application directory.
|
|
3155
|
+
*/
|
|
3156
|
+
async quarantinedCopies() {
|
|
3157
|
+
const notices = [];
|
|
3158
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3159
|
+
for (const path of this.ownCandidates()) {
|
|
3160
|
+
const directory = dirname(path);
|
|
3161
|
+
const prefix = `${basename(path)}.corrupt-`;
|
|
3162
|
+
let entries;
|
|
3163
|
+
try {
|
|
3164
|
+
entries = await readdir(directory);
|
|
3165
|
+
} catch {
|
|
3166
|
+
continue;
|
|
3167
|
+
}
|
|
3168
|
+
for (const name of entries) {
|
|
3169
|
+
if (!name.startsWith(prefix)) continue;
|
|
3170
|
+
const quarantinedAs = join(directory, name);
|
|
3171
|
+
if (seen.has(quarantinedAs)) continue;
|
|
3172
|
+
seen.add(quarantinedAs);
|
|
3173
|
+
let bytes = 0;
|
|
3174
|
+
let accounts;
|
|
3175
|
+
try {
|
|
3176
|
+
const text = await readFile(quarantinedAs, "utf8");
|
|
3177
|
+
bytes = Buffer.byteLength(text);
|
|
3178
|
+
accounts = countStoredAccounts(text);
|
|
3179
|
+
} catch {}
|
|
3180
|
+
notices.push({
|
|
3181
|
+
file: path,
|
|
3182
|
+
quarantinedAs,
|
|
3183
|
+
bytes,
|
|
3184
|
+
...accounts === void 0 ? {} : { accounts }
|
|
3185
|
+
});
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
return notices;
|
|
3189
|
+
}
|
|
3046
3190
|
async refreshNow(credential) {
|
|
3047
3191
|
if (credential.refreshToken === void 0 || credential.refreshExpiresAtMs !== void 0 && credential.refreshExpiresAtMs <= Date.now()) {
|
|
3048
3192
|
if (credential.expiresAtMs > Date.now() + 3e4) return credential;
|
|
@@ -3061,7 +3205,8 @@ var TraeCredentialStore = class {
|
|
|
3061
3205
|
};
|
|
3062
3206
|
const ownPath = this.ownAuthPath();
|
|
3063
3207
|
await withFileLock(ownPath, async () => {
|
|
3064
|
-
const existing = await
|
|
3208
|
+
const existing = await this.readOwnForMerge(ownPath);
|
|
3209
|
+
if (existing === void 0) return;
|
|
3065
3210
|
const refreshedId = traeAccountId(refreshed);
|
|
3066
3211
|
const entries = existing.filter((item) => traeAccountId(item) !== refreshedId);
|
|
3067
3212
|
entries.push(refreshed);
|
|
@@ -7937,12 +8082,26 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
7937
8082
|
* would offer a binding that cannot work.
|
|
7938
8083
|
*/
|
|
7939
8084
|
const accounts = (await store.accounts()).filter((account) => panel !== "enterprise" || account.enterprise);
|
|
8085
|
+
/**
|
|
8086
|
+
* Copies that were found unreadable and moved aside (4.4.6+).
|
|
8087
|
+
*
|
|
8088
|
+
* Read ONCE per document and never allowed to fail it: a notice is a fact
|
|
8089
|
+
* about the user's disk, and a card that failed to load would teach the user
|
|
8090
|
+
* nothing about it. It is also the one piece of the document that explains a
|
|
8091
|
+
* MISSING ACCOUNT, so it belongs on both shapes — the signed-out one
|
|
8092
|
+
* especially, because that is where a lost copy leaves the user (issue #56).
|
|
8093
|
+
*
|
|
8094
|
+
* The scan is a directory listing beside the plugin's OWN files: cheap, and
|
|
8095
|
+
* it reads nothing but file names and sizes.
|
|
8096
|
+
*/
|
|
8097
|
+
const quarantined = await Promise.resolve().then(() => store.quarantinedCopies()).catch(() => []);
|
|
7940
8098
|
if ((await store.status()).state !== "signed-in") {
|
|
7941
8099
|
const { failures } = await store.diagnose();
|
|
7942
8100
|
return {
|
|
7943
8101
|
status: "signed-out",
|
|
7944
8102
|
accounts,
|
|
7945
8103
|
enabled,
|
|
8104
|
+
quarantined,
|
|
7946
8105
|
searched: failures.map((failure) => ({
|
|
7947
8106
|
path: failure.path,
|
|
7948
8107
|
edition: failure.edition,
|
|
@@ -7961,6 +8120,7 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
7961
8120
|
status: "signed-out",
|
|
7962
8121
|
accounts,
|
|
7963
8122
|
enabled,
|
|
8123
|
+
quarantined,
|
|
7964
8124
|
message: safeMessage(error)
|
|
7965
8125
|
};
|
|
7966
8126
|
}
|
|
@@ -7977,6 +8137,7 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
7977
8137
|
...model.input === void 0 ? {} : { input: [...model.input] }
|
|
7978
8138
|
})),
|
|
7979
8139
|
enabledModelIds: [...deps.enabledModelIds(panel)],
|
|
8140
|
+
quarantined,
|
|
7980
8141
|
...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(panel) }
|
|
7981
8142
|
};
|
|
7982
8143
|
if (fast) {
|
|
@@ -9254,24 +9415,28 @@ async function writeTraePoolProbes(panel, updates) {
|
|
|
9254
9415
|
* member, and leaving it in the file would resurrect a stale `rate-limited` row if
|
|
9255
9416
|
* the account were added back later. Written through the same lock/atomic path so a
|
|
9256
9417
|
* removal cannot interleave with a batch.
|
|
9418
|
+
*
|
|
9419
|
+
* The READ also sits inside the lock, like {@link writeTraePoolProbes}'s: a
|
|
9420
|
+
* delete is a read-modify-write, and a read taken outside the lock snapshots a
|
|
9421
|
+
* moment that a concurrent batch's write can then be older than — the delete
|
|
9422
|
+
* would write that stale snapshot back and silently discard the measurements
|
|
9423
|
+
* the batch had just learned.
|
|
9257
9424
|
*/
|
|
9258
9425
|
async function dropTraePoolProbe(panel, accountId) {
|
|
9259
|
-
const existing = await readTraePoolProbes(panel);
|
|
9260
|
-
if (existing[accountId] === void 0) return;
|
|
9261
9426
|
const path = traePoolStorePath(panel);
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9427
|
+
await withFileLock(path, async () => {
|
|
9428
|
+
const existing = await readTraePoolProbes(panel);
|
|
9429
|
+
if (existing[accountId] === void 0) return;
|
|
9430
|
+
const probes = {};
|
|
9431
|
+
for (const [id, probe] of Object.entries(existing)) if (id !== accountId) probes[id] = probe;
|
|
9432
|
+
await writeFileAtomic(path, `${JSON.stringify({
|
|
9433
|
+
version: STORE_FORMAT_VERSION,
|
|
9434
|
+
probes
|
|
9435
|
+
}, null, 2)}\n`, {
|
|
9436
|
+
mode: 384,
|
|
9437
|
+
dirMode: 448
|
|
9273
9438
|
});
|
|
9274
|
-
}
|
|
9439
|
+
}).catch(() => {});
|
|
9275
9440
|
}
|
|
9276
9441
|
//#endregion
|
|
9277
9442
|
//#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.7",
|
|
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",
|