dsh-connect-trae 4.4.6 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/README.en.md +19 -2
- package/README.md +19 -2
- package/lib/client.js +210 -62
- package/lib/index.d.ts +85 -3
- package/lib/index.js +153 -9
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.0 (2026-10-24)
|
|
4
|
+
|
|
5
|
+
> **宽窗模型多一个 500K 上下文中间档。** Trae 每个模型公布两个窗口——默认(dev)档和
|
|
6
|
+
> Max 档——从 200K 直接跳到 1M 是 5 倍,而 1M 的计费并不是每个会话都用得上。这一版在两者
|
|
7
|
+
> 之间加了一个本地的半步。
|
|
8
|
+
>
|
|
9
|
+
> 500K **不是上游公布的窗口**,所以它有门槛、也有校验:只有当模型的 Max 真的够宽、500K
|
|
10
|
+
> 又确实落在它的两个窗口之间时才出现;落盘校验用同一条规则,旧值撑不破模型的真实窗口。
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- **宽窗模型多一个 500K 上下文中间档(国内版 / 国际版都有)**。
|
|
15
|
+
- Trae 每个模型公布两个窗口:默认(dev,实测 200K / 256K / 272K 三种)与 Max
|
|
16
|
+
(实测 1M,MiMo 为 981504)。从默认直接跳到 Max 是 5 倍,而 1M 的计费并不是每个
|
|
17
|
+
会话都用得上——所以加一个 500K 的半步。
|
|
18
|
+
- **500K 不是上游公布的窗口**,是本地的选择,因此有门槛:只有当该模型的
|
|
19
|
+
`maxContextWindow >= 500000`、且 500K 严格宽于它的 dev 窗、又严格窄于它的 Max
|
|
20
|
+
时才出现(`contextBudgetOptions`)。dev 本身已是 500K、或 Max 正好 500K 时不会
|
|
21
|
+
出现重复选项。
|
|
22
|
+
- **落盘校验用的是同一条规则**:`applyContextBudgets` 只接受该模型可选窗口里的值,
|
|
23
|
+
所以旧配置里留下的 500K 在某模型 Max 被上游调小之后会安静失效、退回 dev 档,
|
|
24
|
+
而不是把模型撑到它没有的窗口。
|
|
25
|
+
- 卡片每行因此可能是 1 / 2 / 3 个单选(无 Max 的行仍是一个只读选项);新增整区按钮
|
|
26
|
+
「整区切到 500K」,只作用于 Max ≥ 500K 的行,写入值与逐行单选完全一致。
|
|
27
|
+
- 测试:`tests/context-budget.spec.ts`(9 例,含「Max 不足 500K 不出现」「Max 正好
|
|
28
|
+
500K 不重复」「Max 调小后旧 500K 失效」)与 `tests/card-context-500k.spec.tsx`
|
|
29
|
+
(3 例,含「只给合格行第三个单选」「整区按钮只写合格行」「逐行与整区落盘一致」)。
|
|
30
|
+
两组均做过变异验证:去掉 500K 档 → 5 例红;放宽接受规则 → 3 例红。
|
|
31
|
+
|
|
32
|
+
### 修复
|
|
33
|
+
|
|
34
|
+
- **发布核验不再重建工作树,宿主不再被核验动作掀翻**(`fix(scripts)`)。
|
|
35
|
+
- **事故**(2026-10-24 14:40:08 / 14:41:01 / 14:42:16):桌面版渲染进程连崩三次
|
|
36
|
+
(`EXC_BREAKPOINT`/`SIGTRAP`),应用白屏,最后要人工重启。取证在
|
|
37
|
+
`~/Library/Logs/DiagnosticReports/DeepSeek Harness Helper (Renderer)-*.ips`。
|
|
38
|
+
- **成因**:开发者常态是把 bundle 以**软链**装进 profile
|
|
39
|
+
(`node_modules/dsh-connect-trae -> <本仓库>`),于是运行中的 DSH 宿主服务的正是本仓库
|
|
40
|
+
`lib/` 的字节;而 `scripts/verify-release.mjs` 的第 4 步(确定性重建比对)原本
|
|
41
|
+
**在工作树里**跑 `npm run build`——等于当着活着的宿主改写它正在服务的客户端 bundle,
|
|
42
|
+
宿主重新 import,渲染进程崩。
|
|
43
|
+
- **修法**:重建改到**临时 worktree**(`git worktree add --detach <tag>` + 软链
|
|
44
|
+
`node_modules`)里做,工作树的 `lib/` 一个字节都不动;`git worktree remove` 在收尾时
|
|
45
|
+
清理。用 worktree 而不是 `git archive | tar -x`,是因为测试机上的 PATH 上有**假的
|
|
46
|
+
`tar`**(发布测试用它冒充 registry),走 tar 会解出错的树。
|
|
47
|
+
- **守卫**:`tests/verify-release.spec.ts` 新增一例「工作树不被重建」。变异验证:把重建
|
|
48
|
+
改回 `cwd: repo` → 该例立刻变红(fixture 的 build 会真的写 `lib/`)。
|
|
49
|
+
- **构建前会提醒「这次会改写正在被宿主服务的字节」**(`chore(scripts)`)。
|
|
50
|
+
新增 `scripts/warn-live-host.mjs`,接到 `prebuild` 钩子:检查「有没有 profile 软链到本仓库」
|
|
51
|
+
且「有没有活着的宿主进程」,两者同时成立就打印警告。**它从不失败构建**——热更新的正当
|
|
52
|
+
流程必须还能走;崩了重启应用即可。
|
|
53
|
+
|
|
54
|
+
### 文档
|
|
55
|
+
|
|
56
|
+
- `DEVELOPMENT.md` 新增首节「危险动作:不要在宿主运行时重建 `lib/`」:讲清软链装法、
|
|
57
|
+
事故时间线与三种场景下该怎么做(不开了随便构建 / 只验证发布用 `verify:release` /
|
|
58
|
+
要热更新就接受代价 / 发版前先关宿主)。
|
|
59
|
+
- `RELEASING.md` 第 8 步补一句:第 4 步的重建在临时 worktree 里做,理由与对策指向上面那节。
|
|
60
|
+
- **在核心位置写明「不做插件之间的联动」这条边界**。
|
|
61
|
+
- `README.md` 开头加一条醒目的边界声明,并新增 `## 与其它插件的关系` 一节(英文版同步):
|
|
62
|
+
不共享状态、不做跨插件的账号/额度调度、不协商槽位、不共享开关,以及立这条边的理由。
|
|
63
|
+
- `src/index.ts` 的**模块头**加 SCOPE BOUNDARY 注释:本模块只与 DSH Host 和 Trae 上游
|
|
64
|
+
两方对话,不读写任何其它插件的文件或状态。
|
|
65
|
+
- 补一条区分:同一族插件之间**互相学习**(workbuddy 的教训被写进本仓库的注释与测试),
|
|
66
|
+
但**不互相调用**——学习是编译期的事,联动是运行时的事。
|
|
67
|
+
|
|
68
|
+
## 4.4.7 (2026-10-24)
|
|
69
|
+
|
|
70
|
+
> **4.4.6 把「读不出来的账号副本」改名留住了,但这件事一直是静默的——现在卡片会把它点名,
|
|
71
|
+
> 包括未登录那一屏。被顶掉的账号「去哪了」,从此不必再去 `~/.dsh` 里翻文件名。**
|
|
72
|
+
>
|
|
73
|
+
> 这一版不改任何账号、刷新、镜像的行为:读不懂的文件依然被隔离、依然不被覆盖。
|
|
74
|
+
> 改的是**看得见**:隔离发生的证据本来就在磁盘上,只是没人告诉用户它在那儿。
|
|
75
|
+
|
|
76
|
+
### 新增
|
|
77
|
+
|
|
78
|
+
- **隔离过的账号副本会被点名**(`feat(card)`)。
|
|
79
|
+
- 宿主侧新增 `TraeCredentialStore.quarantinedCopies()`:**扫描**副本文件旁边的
|
|
80
|
+
`.corrupt-*` 兄弟文件——不另设一本账,**证据本身就是证据**,用户删掉留存文件,提示也就跟着
|
|
81
|
+
消失,没有第二处状态要清理。报告内容是它原来的路径、现在的路径、字节数,以及
|
|
82
|
+
**能读成 JSON 时的账号数**(读不成就不猜:一个猜错的账号数比没有更糟)。
|
|
83
|
+
- 状态文档新增 `quarantined` 字段,**signed-in 与 signed-out 两种形状都带**。
|
|
84
|
+
- 卡片「诊断」栏新增一行「损坏副本留存」,点名那份留存文件。
|
|
85
|
+
- **未登录那一屏也显示同一条通知**:账号消失之后用户落地的正是那一屏,而那一屏根本不渲染分栏,
|
|
86
|
+
诊断栏根本看不到——只把通知放进诊断栏,等于没做。
|
|
87
|
+
- 一台健康的机器上什么都不显示:没有通知,也没有空盒子。
|
|
88
|
+
|
|
89
|
+
### 测试
|
|
90
|
+
|
|
91
|
+
- 新增 **9** 例:`tests/auth.spec.ts`(2 条——报告隔离文件、用户删掉后通知随之消失)、
|
|
92
|
+
`tests/web-status.spec.ts`(2 条——signed-out 与 signed-in 文档都带上通知)、
|
|
93
|
+
`tests/card-quarantine.spec.tsx`(5 条——诊断栏点名、未登录屏可见、账号数可读/不可读、健机器保持沉默)。
|
|
94
|
+
- 全仓 **1093** 例 / 85 个测试文件通过,双 tsconfig 类型检查通过。
|
|
95
|
+
|
|
96
|
+
### 文档
|
|
97
|
+
|
|
98
|
+
- `README.md` / `README.en.md` 的诊断栏说明补上这一行新事实,以及「未登录那一屏也会显示」。
|
|
99
|
+
|
|
100
|
+
> **给 #56 报告人的补充**:4.4.6 的隔离是静默的,所以「出现过隔离文件」这件事过去只能靠翻目录发现。
|
|
101
|
+
> 4.4.7 之后,卡片会直接把那份文件指给你——如果你的 `~/.dsh` 下已经有 `.corrupt-*` 文件,
|
|
102
|
+
> 升级后打开设置页就能看到它曾经存在,不必再自己找。
|
|
3
103
|
## 4.4.6 (2026-10-24)
|
|
4
104
|
|
|
5
105
|
> **对齐宿主 DSH 0.2.1-rc.3(纯版本推进,插件代码零改动),并修掉账号链路上三处
|
package/README.en.md
CHANGED
|
@@ -30,13 +30,15 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
30
30
|
|
|
31
31
|
> Apart from the daily check-in claim, every query this plugin makes is read-only and consumes nothing; the claim only happens when you click the button.
|
|
32
32
|
|
|
33
|
+
> **Boundary: this plugin does not interoperate with any other plugin.** Accounts, credentials, the model directory, the account pool and the settings namespace belong to this plugin alone; a `trae` failure never borrows another plugin's account or credit, and vice versa. Why, and what that rules out, is in [Relationship to other plugins](#relationship-to-other-plugins).
|
|
34
|
+
|
|
33
35
|
## Features
|
|
34
36
|
|
|
35
37
|
- **Three parallel providers** — the CN side registers as the `trae` provider (e.g. `DeepSeek-V4-Flash`, `DeepSeek-V4-Pro`), the enterprise side as `trae-enterprise`, and the international side as `trae-global` (the Gemini/GPT/MiniMax roster); **all three rosters appear in the DSH model picker simultaneously**: different sessions can each pick a side without interfering.
|
|
36
38
|
- **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
39
|
- **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
40
|
- **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.
|
|
41
|
+
- **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
42
|
- **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
43
|
- **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
44
|
- **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.
|
|
@@ -82,6 +84,19 @@ Usage overview hits the read-only `https://api.trae.cn/trae/api/v2/pay/*` and `/
|
|
|
82
84
|
|
|
83
85
|
> See `docs/IMPLEMENTATION_PLAN.md`, `docs/SOLO_ROUTE_DECISION.md`, `docs/USAGE_API_RESEARCH.md`.
|
|
84
86
|
|
|
87
|
+
## Relationship to other plugins
|
|
88
|
+
|
|
89
|
+
**This plugin does not interoperate with any other plugin.** That is a boundary, not a slogan:
|
|
90
|
+
|
|
91
|
+
- **No shared state.** Accounts, credentials, the model directory, the account pool, access tokens and the device id belong to this plugin alone; it reads and writes only its own files (`$DSH_HOME/.trae-auth.*.json`, `.trae-auth.deleted.*.json`, `.trae-pool.*.json`) and its own settings namespace — it does **not** read, write, or supply data to any other plugin. It does edit the Trae desktop app's own sign-in file, but that is another **application**, not another plugin.
|
|
92
|
+
- **No cross-plugin account or credit scheduling.** The pool ranks and switches only among this plugin's own accounts; a `trae` failure is a failure — it never borrows another connector plugin's account, credit or channel, and never hands a request to another plugin to retry. The reverse holds too: no plugin can send requests through this one.
|
|
93
|
+
- **No slot negotiation.** Whether the card and the composer readout appear is decided by **whether this plugin's provider is selected** (read from DSH's own `modelSelection` projection), not by plugin-to-plugin messages allocating space. Two connector plugins installed together coexist because each appears only for its own provider — that is **non-interference**, not collaboration.
|
|
94
|
+
- **No shared switches.** Turning one panel off withdraws only this plugin's provider and requests; it never changes another plugin's provider, models, binding or settings.
|
|
95
|
+
|
|
96
|
+
**Why the boundary exists.** Once plugins depend on each other, any upgrade, uninstall or version-line change silently alters the behaviour of its neighbours — and DSH's load order, parallel versions and user-authored profiles make that coupling impossible to express reliably. Holding the boundary keeps "who is broken" answerable with **one** plugin instead of a chain.
|
|
97
|
+
|
|
98
|
+
> Sibling plugins **learn from** each other (lessons from `dsh-connect-workbuddy` are quoted in this repository's comments and tests) but never **call** each other — learning is a compile-time affair, interoperation is a runtime one, and the two must stay apart.
|
|
99
|
+
|
|
85
100
|
## Image input
|
|
86
101
|
|
|
87
102
|
The plugin declares whether a model accepts images, and DSH uses that to decide whether an attachment is **sent as-is** or **degraded into a text description**. That makes the declaration a hard switch, not a cosmetic label.
|
|
@@ -134,7 +149,9 @@ To use these two models today, use the **Trae IDE itself**.
|
|
|
134
149
|
|
|
135
150
|
The "最大输出" (max output) on the model card comes from `model_detail_list[].max_tokens` in Trae's `get_detail_param` — upstream **does** publish it (live-verified: 80/80 CN configs carry it). As of 4.0.1 the value travels through the catalog merge all the way to the card, `/models/refresh` and DSH model registration; before that the merge step dropped the field, so the whole region rendered blank (issue #42 — full diagnosis in `docs/ISSUE42_DIAGNOSIS.md`). **Rows that are still blank are being honest**: models the directory lists but the SOLO roster has no config for (the three TraeCode rows above) have no candidate value to show — we prefer a blank over an invented cap. The value is metadata only; the plugin never puts `max_tokens` into request bodies (the server decides the real cap).
|
|
136
151
|
|
|
137
|
-
Context windows are a per-model choice between Trae's default (dev) window and its Max window, stored per panel as `contextBudgets`.
|
|
152
|
+
Context windows are a per-model choice between Trae's default (dev) window and its Max window — **and, as of 4.4.8, a 500K middle step for wide-window models** — stored per panel as `contextBudgets`. 500K is **not** an upstream-advertised window; it is a local half-way point. Dev windows are typically 200K/256K/272K and Max is typically 1M, so going straight from default to Max is a 5× jump, and Max-tier billing is not what every session needs. It is therefore **conditional**: the option only appears when that model's Max is at least 500K, and 500K is genuinely wider than its dev window and narrower than its Max. `applyContextBudgets` validates against the **same rule** (`contextBudgetOptions`), so a stored value that is not one of that model's selectable windows always falls back to the dev window — it can never push a model past a window it does not have (if upstream later shrinks a model's Max to 256K, a stale 500K quietly stops applying instead of overflowing).
|
|
153
|
+
|
|
154
|
+
For a whole-region switch, the model section has three buttons: "**Switch all to 500K**" (writes 500K for every row whose Max is at least 500K, leaves the rest alone), "**Switch all to Max**" (writes each Max-capable row's `maxContextWindow`, leaves no-Max rows and existing manual budgets alone) and "**Restore default context**" (clears the region's budget dict). All act on the draft and take effect on Save, producing exactly the same stored shape as the per-row radios (issue #42 side request — 18 Max rows in CN used to mean 18 clicks).
|
|
138
155
|
|
|
139
156
|
## Install
|
|
140
157
|
|
package/README.md
CHANGED
|
@@ -30,13 +30,15 @@
|
|
|
30
30
|
|
|
31
31
|
> 除每日签到领取外,插件的所有查询都是只读的、不消耗额度;签到领取也只在你点击按钮时才发生。
|
|
32
32
|
|
|
33
|
+
> **边界:本插件不与任何其它插件联动。** 账号、凭据、模型目录、账号池与设置命名空间都只属于本插件;`trae` 失败不会借用别的插件的账号或额度,反之亦然。理由与具体含义见[与其它插件的关系](#与其它插件的关系)。
|
|
34
|
+
|
|
33
35
|
## 功能特性
|
|
34
36
|
|
|
35
37
|
- **三供应商并行接入** —— 国内版注册为 DSH 的 `trae` provider(`DeepSeek-V4-Flash`、`DeepSeek-V4-Pro` 等),企业版注册为 `trae-enterprise`,国际版注册为 `trae-global`(Gemini / GPT / MiniMax 阵容),**三边模型同时出现在 DSH 模型选择器里**:不同会话可以各选一边,互不干扰。
|
|
36
38
|
- **企业版是独立计费通道,不是国内版的别名** —— 同一张 CN 凭据若属于 `tob_online` 租户,会被**两个**网关同时服务:消费池(扣个人积分)与租户池(扣企业额度)。同一模型、两种付费方,所以「谁付钱」由你显式选,插件不替你猜。`trae` 永不碰企业网关;`trae-enterprise` **不回落**,被拒绝就如实报错,不会悄悄改用个人额度重跑。企业版**默认关闭**——纯个人账号用不了企业池,默认放开等于塞一条一用就失败的 provider。
|
|
37
39
|
- **可单独关闭任一面板** —— 三个 tab 各带一个勾选(国内版与国际版默认勾选,企业版默认不勾):取消勾选即把该面板的供应商**整个从 DSH 模型选择器撤掉**(不是只藏起 tab),并且不再发起任何请求;重新勾选即恢复,账号、目录、勾选与上下文预算全部保留。用不到国际版就把国际版关掉即可。
|
|
38
40
|
- **插件卡片 tab 切换** —— 设置卡片顶部为「国内版 / 企业版 / 国际版」三个 tab,各含独立的账号选择、用量概览与模型管理;每个 tab 的账号、目录、勾选与未保存草稿完全隔离——在一个 tab 里切账号或刷新模型,不会触碰另一边的运行时目录与会话。国内版与企业版共用同一份 CN 凭据池,但**各有各的账号绑定与模型勾选**:两个池的名录并不相同(`DeepSeek-V4.1-Flash` 只有企业池列),所以勾选互不覆盖。
|
|
39
|
-
- **卡片内再按「账号 / 模型 / 诊断」分栏**(学自同族 `dsh-connect-workbuddy`)—— 上面那排地区是**作用域**(这张卡片讲的是哪个供应商),这一排是同一张卡片里的**分栏**:账号栏管「谁在服务、还剩多少、签到与账号池」,模型栏管「勾哪些模型、图片输入、上下文预算、保存」,诊断栏只读地列出面板名、供应商 id
|
|
41
|
+
- **卡片内再按「账号 / 模型 / 诊断」分栏**(学自同族 `dsh-connect-workbuddy`)—— 上面那排地区是**作用域**(这张卡片讲的是哪个供应商),这一排是同一张卡片里的**分栏**:账号栏管「谁在服务、还剩多少、签到与账号池」,模型栏管「勾哪些模型、图片输入、上下文预算、保存」,诊断栏只读地列出面板名、供应商 id、服务账号、两种令牌到期时间、目录/已启用模型数、账号池与签到支持与否,以及本面板最近一次报错的**原文**。**4.4.7 起诊断栏还多一行「损坏副本留存」**:读不出来、因此被改名留存的账号副本会在这里被点名(连同它曾经保存了几个账号)——被顶掉的账号「去哪了」这个问题,从此不用再去 `~/.dsh` 里翻文件名;未登录那一屏也会显示同一条通知,因为账号消失之后用户落地的正是那一屏。分栏的理由是**提交语义不同**:账号改动点一下即生效,模型改动是草稿、必须点「保存」——一列长页会把保存按钮放到离它要保存的东西三屏之外。切栏不重新拉取、不丢未保存的模型草稿、账号池日志也不重建;未登录时不出分栏,那一屏说明不会被看不见的 tab 藏起来。**模型栏把所有模型一次性铺开,没有内层滚动条**——不再是一个套在滚动页面里的小窗口,唯一滚动的就是卡片自己那一页。
|
|
40
42
|
- **账号池:自动选号 + 失败换号(默认关闭)** —— 本机读完几个账号后,「该用谁的额度」可以交给插件按规则排序:**先排除不可用的**(凭据被拒、套餐不覆盖、账号已不存在、限流未到重试时间、额度耗尽),再在可用账号里**优先快到期的那点额度**(与其放着过期,不如先花掉),最后才比总额、token 新鲜度,并以账号 id 兜底成**全序**(同样的输入永远得到同样的顺序,不会在两行之间来回抖)。三个面板**各有各的池**:国内版与企业版共用同一份 CN 凭据,但企业版只挑租户账号、国内版不会漂到租户号上。开着池时,发请求前先按上面的顺序选中一个账号,并且**真实请求失败会换下一个账号重试(最多 3 个)**——只有账号级失败才换(`401` 凭据被拒、额度耗尽、限流、上游 5xx);模型 id 写错、请求本身不合法**不换号**,因为那不是账号的问题,换号只会把同一个错误烧两遍、还冤枉一个没机会服务的账号。换号失败的真相会写进池的探针记录,下一次排序直接把它排到后面,而每一次结论都能在卡片上看到:`可用` / `限流` / `额度耗尽` / `凭据被拒` / `套餐不覆盖` / `上游不提供` / `本次未测出`,以及「什么时候可以重试」。**关掉池就回到严格绑定**,不会有任何残留 override。卡片里的「全部测速」会把每个成员按目标模型各测一次,结果**边跑边显示**(一行一个答案,不等整批跑完),并落盘成下次排序的依据。
|
|
41
43
|
|
|
42
44
|
> 池是**显式选择**:默认关闭。它不会替你在没打开池的情况下换账号——「换账号等于换人计费」,所以这件事只能由你点头。池打开后卡片会明确写出**当前是谁在付费**(Host 报告的是「下一个真正发送请求的账号」,不是保存的那个绑定)。
|
|
@@ -87,6 +89,19 @@ DSH PiAiAdapter(每个 provider 一套)
|
|
|
87
89
|
|
|
88
90
|
> 详见 `docs/IMPLEMENTATION_PLAN.md`、`docs/SOLO_ROUTE_DECISION.md`、`docs/USAGE_API_RESEARCH.md`。
|
|
89
91
|
|
|
92
|
+
## 与其它插件的关系
|
|
93
|
+
|
|
94
|
+
**本插件不与任何其它插件联动。** 这是一条边界,不是一句口号:
|
|
95
|
+
|
|
96
|
+
- **不共享状态。** 账号、凭据、模型目录、账号池、访问令牌、设备号都只属于本插件;插件只读写自己的文件(`$DSH_HOME/.trae-auth.*.json`、`.trae-auth.deleted.*.json`、`.trae-pool.*.json`)与自己的设置命名空间,**不读、不写、也不为任何别的插件供数**。它确实会编辑 Trae 桌面应用自己的登录文件,但那是另一个**应用**,不是另一个插件。
|
|
97
|
+
- **不做跨插件的账号或额度调度。** 账号池只在本插件自己的账号之间排序与换号;`trae` 失败就是失败,不会去「借」另一个连接器插件的账号、额度或通道,也不会把请求转交给别的插件重试。反方向同样成立:没有任何插件能通过本插件代发请求。
|
|
98
|
+
- **不协商槽位。** 卡片与 composer 读数是否出现,由**本插件的 provider 是否被选中**决定(读 DSH 自己的 `modelSelection` 投影),不靠插件间通信分配位置。两个连接器插件同时安装时,它们靠「各自只在自己的 provider 被选中时出现」共存——这是**互不干扰**,不是协作。
|
|
99
|
+
- **不共享开关。** 关掉本插件的某个面板,只撤下本插件的 provider 与请求,不会改动另一个插件的供应商、模型、绑定或设置。
|
|
100
|
+
|
|
101
|
+
**为什么立这条边界。** 插件之间一旦互相依赖,任何一个插件的升级、卸载或版本线变化,都会悄悄改变另一个插件的行为;而 DSH 的加载顺序、多版本并存与用户自定义 profile 让这种耦合无法可靠表达。立住这条边界,「出问题的是谁」永远可以定位到**一个**插件,而不是一串。
|
|
102
|
+
|
|
103
|
+
> 同一族插件之间**互相学习**(`dsh-connect-workbuddy` 的教训被写进本仓库的注释与测试),但**不互相调用**——学习是编译期的事,联动是运行时的事,这两件事必须分开。
|
|
104
|
+
|
|
90
105
|
## 图片输入(识图)
|
|
91
106
|
|
|
92
107
|
插件对外声明「这个模型是否接受图片」,DSH 据此决定附图是**原样发出**还是**降级成文字描述**。所以这条声明是硬开关,不是一个无关紧要的标记。
|
|
@@ -151,7 +166,9 @@ Trae does not serve this model under the SOLO function the request used
|
|
|
151
166
|
|
|
152
167
|
模型卡上的「最大输出」来自 Trae `get_detail_param` 的 `model_detail_list[].max_tokens` —— 上游**是**发布这个值的(CN 实测 80/80 config 都带)。4.0.1 起它随目录合并一路带到卡片、`/models/refresh` 与 DSH 模型注册;此前合并层漏传了这个字段,整区模型全部显示空白(issue #42,诊断见 `docs/ISSUE42_DIAGNOSIS.md`)。**仍然空白的行是诚实的**:目录里有、但 SOLO 名单里没有对应 config 的模型(上面那 3 个 TraeCode 通道行)拿不到任何候选值,宁可空白也不编一个上限。这个值是元数据,插件不把它写进请求体(输出上限由 Trae 服务端决定)。
|
|
153
168
|
|
|
154
|
-
上下文窗口在 Trae 公布的默认(dev)档与 Max
|
|
169
|
+
上下文窗口在 Trae 公布的默认(dev)档与 Max 档之间选,**4.4.8 起对宽窗模型多一个 500K 中间档**,按面板逐模型存成 `contextBudgets`。500K **不是**上游公布的窗口,是本地的半步:dev 常见 200K/256K/272K,Max 常见 1M,从默认直接跳到 Max 是 5 倍,而 1M 的计费并不是每个会话都用得上。所以它是**有门槛的**:只有当该模型的 Max ≥ 500K、且 500K 确实比 dev 宽、又确实比 Max 窄时才出现——`applyContextBudgets` 用**同一条规则**(`contextBudgetOptions`)校验,落盘值不是该模型可选窗口就一律退回 dev 档,绝不会把模型撑到它没有的窗口(比如某模型后来被上游把 Max 调到 256K,旧的 500K 会安静地失效而不是溢出)。
|
|
170
|
+
|
|
171
|
+
想整区一次切换:模型管理区有「**整区切到 500K**」(给本区所有 Max ≥ 500K 的行写入 500K,其余不动)、「**整区切到 Max**」(给每个带 Max 档的行写入其 `maxContextWindow`,没有 Max 档的行不动)和「**全部恢复默认上下文**」(清空本区预算)三个按钮,作用在草稿上、按「保存」生效,与逐行单选落盘的结果完全一致(issue #42 附带需求——CN 有 18 个 Max 模型,以前要点 18 次)。
|
|
155
172
|
|
|
156
173
|
## 安装
|
|
157
174
|
|
package/lib/client.js
CHANGED
|
@@ -411,6 +411,55 @@ window.__ModuleLoader__.load({
|
|
|
411
411
|
return tier !== void 0 && tier !== "" && !/^free$/i.test(tier);
|
|
412
412
|
}
|
|
413
413
|
//#endregion
|
|
414
|
+
//#region src/context-budget.ts
|
|
415
|
+
/**
|
|
416
|
+
* Context-budget arithmetic shared by the HOST (which applies a saved budget to
|
|
417
|
+
* the catalog DSH is told about) and the CLIENT card (which renders the radios).
|
|
418
|
+
*
|
|
419
|
+
* It lives in its own module because the client bundle cannot import
|
|
420
|
+
* `catalog.ts` (host-only) and the two sides must agree EXACTLY on which
|
|
421
|
+
* windows are selectable: a card that offers a window the host then refuses
|
|
422
|
+
* would be a control that silently does nothing.
|
|
423
|
+
*/
|
|
424
|
+
/**
|
|
425
|
+
* The one intermediate budget the card offers, in tokens: 500K.
|
|
426
|
+
*
|
|
427
|
+
* WHY IT EXISTS: Trae advertises a dev window (typically 200K/256K/272K) and a
|
|
428
|
+
* Max window (typically 1M). Switching dev→Max is a 5× jump for a user who
|
|
429
|
+
* just wants more room without paying Max-tier pricing for the whole session.
|
|
430
|
+
* 500K is the half-way point, and it is only ever offered for a model whose
|
|
431
|
+
* Max window is at least that wide.
|
|
432
|
+
*
|
|
433
|
+
* WHY A CONSTANT AND NOT A PER-MODEL VALUE: this is a LOCAL preference that
|
|
434
|
+
* DSH uses to decide when to compress; it is not a Trae-advertised window. The
|
|
435
|
+
* value must be sane for every model that can select it, so eligibility is
|
|
436
|
+
* gated on `maxContextWindow >= 500000` rather than inventing a window per
|
|
437
|
+
* model. `applyContextBudgets` additionally refuses any budget wider than what
|
|
438
|
+
* the model advertises, so a stale saved value cannot overflow a shrunken Max.
|
|
439
|
+
*/
|
|
440
|
+
const TRAE_CONTEXT_BUDGET_MID = 5e5;
|
|
441
|
+
/**
|
|
442
|
+
* The selectable windows for one model, smallest first: dev, then 500K (only
|
|
443
|
+
* when it is a genuine middle step), then Max. Duplicates are dropped so a
|
|
444
|
+
* model whose dev window is already 500K offers it once.
|
|
445
|
+
*
|
|
446
|
+
* A model with no Max window yields exactly one option — its dev window — which
|
|
447
|
+
* is what makes the card's radios read-only for such models.
|
|
448
|
+
*/
|
|
449
|
+
function contextBudgetOptions(model) {
|
|
450
|
+
const dev = model.contextWindow;
|
|
451
|
+
const max = model.maxContextWindow;
|
|
452
|
+
const options = [];
|
|
453
|
+
const push = (value) => {
|
|
454
|
+
if (value === void 0 || value < 1) return;
|
|
455
|
+
if (!options.includes(value)) options.push(value);
|
|
456
|
+
};
|
|
457
|
+
push(dev);
|
|
458
|
+
if (max !== void 0 && max >= 5e5 && (dev === void 0 || 5e5 > dev)) push(TRAE_CONTEXT_BUDGET_MID);
|
|
459
|
+
push(max);
|
|
460
|
+
return options.sort((left, right) => left - right);
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
414
463
|
//#region src/model-order.ts
|
|
415
464
|
/**
|
|
416
465
|
* Wire name of the default SOLO chat function.
|
|
@@ -1183,6 +1232,16 @@ window.__ModuleLoader__.load({
|
|
|
1183
1232
|
.dsm-trae-searched-list li{display:flex;flex-direction:column;gap:1px;min-width:0}
|
|
1184
1233
|
.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
1234
|
.dsm-trae-searched-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
|
|
1235
|
+
/* The quarantine notice: a copy that was kept aside instead of being erased.
|
|
1236
|
+
It borrows the "probed paths" look but stands on its own class, because it
|
|
1237
|
+
is a different KIND of fact — and because a screen may carry both. */
|
|
1238
|
+
.dsm-trae-quarantine{margin-top:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
1239
|
+
.dsm-trae-quarantine>summary{cursor:pointer;color:var(--dsw-alias-label-secondary,#b8b8b8);user-select:none}
|
|
1240
|
+
.dsm-trae-quarantine-hint{margin:6px 0 4px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
1241
|
+
.dsm-trae-quarantine-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
|
|
1242
|
+
.dsm-trae-quarantine-list li{display:flex;flex-direction:column;gap:1px;min-width:0}
|
|
1243
|
+
.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}
|
|
1244
|
+
.dsm-trae-quarantine-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
|
|
1186
1245
|
.dsm-trae-usage-cheer:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:2px}
|
|
1187
1246
|
.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
1247
|
.dsm-trae-checkin-copy{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
@@ -4959,6 +5018,27 @@ window.__ModuleLoader__.load({
|
|
|
4959
5018
|
});
|
|
4960
5019
|
};
|
|
4961
5020
|
/**
|
|
5021
|
+
* Bulk 500K switch: every model that offers the middle step goes to it, and
|
|
5022
|
+
* every other model is left exactly as configured.
|
|
5023
|
+
*
|
|
5024
|
+
* The reporter-shaped case this serves: a region can hold 19 models with a
|
|
5025
|
+
* 1M Max, and picking the half-way window one radio at a time is 19 clicks.
|
|
5026
|
+
* It writes the literal {@link TRAE_CONTEXT_BUDGET_MID} — the same value the
|
|
5027
|
+
* per-row radio writes — so bulk and per-row land on identical saved state,
|
|
5028
|
+
* and `applyContextBudgets` re-validates it against each model's windows on
|
|
5029
|
+
* every read.
|
|
5030
|
+
*/
|
|
5031
|
+
const setAllContextMid = () => {
|
|
5032
|
+
editDraft((current) => {
|
|
5033
|
+
const next = { ...current.contextBudgets };
|
|
5034
|
+
for (const model of current.models) if (contextBudgetOptions(model).includes(5e5)) next[model.id] = TRAE_CONTEXT_BUDGET_MID;
|
|
5035
|
+
return {
|
|
5036
|
+
...current,
|
|
5037
|
+
contextBudgets: next
|
|
5038
|
+
};
|
|
5039
|
+
});
|
|
5040
|
+
};
|
|
5041
|
+
/**
|
|
4962
5042
|
* 「恢复默认上下文」 clears every override in this region's draft. Empty
|
|
4963
5043
|
* dict = every row falls back to its default window (the same state as a
|
|
4964
5044
|
* region that was never configured), and clearing is what makes the bulk
|
|
@@ -5576,26 +5656,22 @@ window.__ModuleLoader__.load({
|
|
|
5576
5656
|
toggleImage(model.id);
|
|
5577
5657
|
}
|
|
5578
5658
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("row.modelImage") })]
|
|
5579
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
5659
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("fieldset", {
|
|
5580
5660
|
className: "dsm-trae-context-budget",
|
|
5581
5661
|
"aria-label": t("row.contextBudget"),
|
|
5582
|
-
children:
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
onChange: () => {
|
|
5596
|
-
setContextBudget(model.id, model.maxContextWindow);
|
|
5597
|
-
}
|
|
5598
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(model.maxContextWindow ?? model.contextWindow, t("row.modelUnknown")) })] })]
|
|
5662
|
+
children: contextBudgetOptions(model).map((option) => {
|
|
5663
|
+
const selectable = model.maxContextWindow !== void 0;
|
|
5664
|
+
const checked = (activeContextBudgets[model.id] ?? model.contextWindow) === option;
|
|
5665
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
5666
|
+
type: "radio",
|
|
5667
|
+
name: `context-${model.id}`,
|
|
5668
|
+
checked,
|
|
5669
|
+
disabled: !selectable || settingsScope?.getSnapshot().writable !== true || saving,
|
|
5670
|
+
onChange: () => {
|
|
5671
|
+
setContextBudget(model.id, option);
|
|
5672
|
+
}
|
|
5673
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatCapacity(option, t("row.modelUnknown")) })] }, option);
|
|
5674
|
+
})
|
|
5599
5675
|
})]
|
|
5600
5676
|
})]
|
|
5601
5677
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -5623,21 +5699,32 @@ window.__ModuleLoader__.load({
|
|
|
5623
5699
|
}),
|
|
5624
5700
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5625
5701
|
className: "dsm-trae-context-bulk",
|
|
5626
|
-
children: [
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5702
|
+
children: [
|
|
5703
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5704
|
+
type: "button",
|
|
5705
|
+
className: "dsm-btn dsm-btn-outline",
|
|
5706
|
+
disabled: busy || saving || settingsScope?.getSnapshot().writable !== true || !visibleModels.some((model) => contextBudgetOptions(model).includes(5e5)),
|
|
5707
|
+
title: t("row.contextAllMidHint"),
|
|
5708
|
+
onClick: setAllContextMid,
|
|
5709
|
+
children: t("row.contextAllMid")
|
|
5710
|
+
}),
|
|
5711
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5712
|
+
type: "button",
|
|
5713
|
+
className: "dsm-btn dsm-btn-outline",
|
|
5714
|
+
disabled: busy || saving || settingsScope?.getSnapshot().writable !== true || !visibleModels.some((model) => model.maxContextWindow !== void 0),
|
|
5715
|
+
title: t("row.contextAllMaxHint"),
|
|
5716
|
+
onClick: setAllContextMax,
|
|
5717
|
+
children: t("row.contextAllMax")
|
|
5718
|
+
}),
|
|
5719
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5720
|
+
type: "button",
|
|
5721
|
+
className: "dsm-btn dsm-btn-outline",
|
|
5722
|
+
disabled: busy || saving || settingsScope?.getSnapshot().writable !== true || Object.keys(activeContextBudgets).length === 0,
|
|
5723
|
+
title: t("row.contextAllDefaultHint"),
|
|
5724
|
+
onClick: resetAllContextBudgets,
|
|
5725
|
+
children: t("row.contextAllDefault")
|
|
5726
|
+
})
|
|
5727
|
+
]
|
|
5641
5728
|
}),
|
|
5642
5729
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5643
5730
|
className: "dsm-trae-model-capability-note",
|
|
@@ -5805,6 +5892,19 @@ window.__ModuleLoader__.load({
|
|
|
5805
5892
|
children: failures.join(" · ")
|
|
5806
5893
|
});
|
|
5807
5894
|
})()]
|
|
5895
|
+
}),
|
|
5896
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5897
|
+
className: "dsm-trae-diag-row",
|
|
5898
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5899
|
+
className: "dsm-trae-diag-label",
|
|
5900
|
+
children: t("row.diagQuarantine")
|
|
5901
|
+
}), status.quarantined === void 0 || status.quarantined.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5902
|
+
className: "dsm-trae-diag-value",
|
|
5903
|
+
children: t("row.diagQuarantineNone")
|
|
5904
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5905
|
+
className: "dsm-trae-diag-value dsm-trae-diag-bad dsm-trae-diag-mono",
|
|
5906
|
+
children: status.quarantined.map((item) => item.quarantinedAs).join(" · ")
|
|
5907
|
+
})]
|
|
5808
5908
|
})
|
|
5809
5909
|
]
|
|
5810
5910
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -5817,35 +5917,65 @@ window.__ModuleLoader__.load({
|
|
|
5817
5917
|
role: "status",
|
|
5818
5918
|
children: t("row.accountsLoading")
|
|
5819
5919
|
}) }) : 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
|
-
|
|
5920
|
+
status.status === "signed-out" && !loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5921
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5922
|
+
className: "dsm-trae-usage-text",
|
|
5923
|
+
children: status.message ?? t("row.signedOutHint")
|
|
5924
|
+
}),
|
|
5925
|
+
status.quarantined && status.quarantined.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
5926
|
+
className: "dsm-trae-quarantine",
|
|
5927
|
+
open: true,
|
|
5928
|
+
children: [
|
|
5929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
|
|
5930
|
+
t("row.quarantineTitle"),
|
|
5931
|
+
" (",
|
|
5932
|
+
status.quarantined.length,
|
|
5933
|
+
")"
|
|
5934
|
+
] }),
|
|
5935
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5936
|
+
className: "dsm-trae-quarantine-hint",
|
|
5937
|
+
children: t("row.quarantineHint")
|
|
5938
|
+
}),
|
|
5939
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
5940
|
+
className: "dsm-trae-quarantine-list",
|
|
5941
|
+
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", {
|
|
5942
|
+
className: "dsm-trae-quarantine-reason",
|
|
5943
|
+
children: [
|
|
5944
|
+
item.accounts === void 0 ? t("row.quarantineAccountsUnknown") : t("row.quarantineAccounts", { count: item.accounts }),
|
|
5945
|
+
" · ",
|
|
5946
|
+
t("row.quarantineBytes", { bytes: item.bytes })
|
|
5947
|
+
]
|
|
5948
|
+
})] }, item.quarantinedAs))
|
|
5949
|
+
})
|
|
5950
|
+
]
|
|
5951
|
+
}) : null,
|
|
5952
|
+
status.searched && status.searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
5953
|
+
className: "dsm-trae-searched",
|
|
5954
|
+
children: [
|
|
5955
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
|
|
5956
|
+
t("row.searchedTitle"),
|
|
5957
|
+
" (",
|
|
5958
|
+
status.searched.length,
|
|
5959
|
+
")"
|
|
5960
|
+
] }),
|
|
5961
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5962
|
+
className: "dsm-trae-searched-hint",
|
|
5963
|
+
children: t("row.searchedHint")
|
|
5964
|
+
}),
|
|
5965
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
5966
|
+
className: "dsm-trae-searched-list",
|
|
5967
|
+
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", {
|
|
5968
|
+
className: "dsm-trae-searched-reason",
|
|
5969
|
+
children: [
|
|
5970
|
+
t(item.source === "cli" ? "row.sourceCli" : "row.sourceDesktop"),
|
|
5971
|
+
" · ",
|
|
5972
|
+
t(item.reason === "missing" ? "row.reasonMissing" : item.reason === "unreadable" ? "row.reasonUnreadable" : "row.reasonInvalid")
|
|
5973
|
+
]
|
|
5974
|
+
})] }, `${item.source}:${item.path}`))
|
|
5975
|
+
})
|
|
5976
|
+
]
|
|
5977
|
+
}) : null
|
|
5978
|
+
] }) : null,
|
|
5849
5979
|
status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5850
5980
|
className: "dsm-trae-usage-error",
|
|
5851
5981
|
children: status.message
|
|
@@ -6583,6 +6713,8 @@ window.__ModuleLoader__.load({
|
|
|
6583
6713
|
"row.diagRawOff": "not checked",
|
|
6584
6714
|
"row.diagFailures": "Last reported failure",
|
|
6585
6715
|
"row.diagNoFailures": "none",
|
|
6716
|
+
"row.diagQuarantine": "Quarantined copies",
|
|
6717
|
+
"row.diagQuarantineNone": "none",
|
|
6586
6718
|
"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
6719
|
"row.settingsWriteFailed": "The setting was not saved: {message}",
|
|
6588
6720
|
"row.signedOut": "Not signed in",
|
|
@@ -6590,6 +6722,11 @@ window.__ModuleLoader__.load({
|
|
|
6590
6722
|
"row.searchedTitle": "Paths checked",
|
|
6591
6723
|
"row.searchedHint": "No sign-in was found. These locations were probed; if your Trae stores its login elsewhere, please report the path.",
|
|
6592
6724
|
"row.searchedMore": "Show {count} more",
|
|
6725
|
+
"row.quarantineTitle": "Quarantined credential copies",
|
|
6726
|
+
"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.",
|
|
6727
|
+
"row.quarantineAccounts": "{count} account(s)",
|
|
6728
|
+
"row.quarantineAccountsUnknown": "account count unreadable",
|
|
6729
|
+
"row.quarantineBytes": "{bytes} bytes",
|
|
6593
6730
|
"row.reasonMissing": "not found",
|
|
6594
6731
|
"row.reasonUnreadable": "unreadable",
|
|
6595
6732
|
"row.reasonInvalid": "unreadable format",
|
|
@@ -6734,6 +6871,8 @@ window.__ModuleLoader__.load({
|
|
|
6734
6871
|
"row.expiryEarliest": "Earliest: {time}",
|
|
6735
6872
|
"row.settingsDirty": "Unsaved changes",
|
|
6736
6873
|
"row.modelsTestEmpty": "Enable at least one model first.",
|
|
6874
|
+
"row.contextAllMid": "Switch all to 500K",
|
|
6875
|
+
"row.contextAllMidHint": "Sets the DSH context budget of every model in this region whose Max window is at least 500K to the 500K middle step, leaving the rest alone. Half-way between the default window and Max — for when 1M is more than you need. Still a draft — press Save to apply.",
|
|
6737
6876
|
"row.contextAllMax": "Switch all to Max",
|
|
6738
6877
|
"row.contextAllMaxHint": "Sets the DSH context budget of every model in this region that has a Max window to its Max window at once. Still a draft — press Save to apply. Models without a Max window are left alone.",
|
|
6739
6878
|
"row.contextAllDefault": "Restore default context",
|
|
@@ -6953,6 +7092,8 @@ window.__ModuleLoader__.load({
|
|
|
6953
7092
|
"row.diagRawOff": "未检测",
|
|
6954
7093
|
"row.diagFailures": "最近一次报错",
|
|
6955
7094
|
"row.diagNoFailures": "无",
|
|
7095
|
+
"row.diagQuarantine": "损坏副本留存",
|
|
7096
|
+
"row.diagQuarantineNone": "无",
|
|
6956
7097
|
"row.diagHint": "以上全部只读,可整段原样贴进反馈。数值来自本面板最近一次刷新,不是概括。",
|
|
6957
7098
|
"row.settingsWriteFailed": "设置未能保存:{message}",
|
|
6958
7099
|
"row.signedOut": "未登录",
|
|
@@ -6960,6 +7101,11 @@ window.__ModuleLoader__.load({
|
|
|
6960
7101
|
"row.searchedTitle": "已检查的路径",
|
|
6961
7102
|
"row.searchedHint": "未找到登录信息。以下是插件探测过的位置;如果你的 Trae 把登录信息存在别处,请把该路径反馈给我们。",
|
|
6962
7103
|
"row.searchedMore": "展开其余 {count} 条",
|
|
7104
|
+
"row.quarantineTitle": "被隔离的账号副本",
|
|
7105
|
+
"row.quarantineHint": "有一份账号副本文件读不出来,插件没有覆盖它,而是改名留存(字节完整)。它曾经保存的账号只可能在那份文件里;确认已取回后删掉它即可不再提示。",
|
|
7106
|
+
"row.quarantineAccounts": "含 {count} 个账号",
|
|
7107
|
+
"row.quarantineAccountsUnknown": "账号数无法读取",
|
|
7108
|
+
"row.quarantineBytes": "{bytes} 字节",
|
|
6963
7109
|
"row.reasonMissing": "文件不存在",
|
|
6964
7110
|
"row.reasonUnreadable": "无法读取",
|
|
6965
7111
|
"row.reasonInvalid": "格式无法识别",
|
|
@@ -7104,6 +7250,8 @@ window.__ModuleLoader__.load({
|
|
|
7104
7250
|
"row.expiryEarliest": "最早到期:{time}",
|
|
7105
7251
|
"row.settingsDirty": "有未保存的修改",
|
|
7106
7252
|
"row.modelsTestEmpty": "请先启用至少一个模型。",
|
|
7253
|
+
"row.contextAllMid": "整区切到 500K",
|
|
7254
|
+
"row.contextAllMidHint": "把本区所有 Max 档不小于 500K 的模型的 DSH 上下文预算一次性设为 500K 中间档,其余模型不动。这是默认窗口与 Max 之间的半步——觉得 1M 用不上、又嫌默认太小时用。这只是草稿——要按「保存」才生效。",
|
|
7107
7255
|
"row.contextAllMax": "整区切到 Max",
|
|
7108
7256
|
"row.contextAllMaxHint": "把本区所有带 Max 档的模型的 DSH 上下文预算一次性设为各自的 Max 档。这只是草稿——要按「保存」才生效。没有 Max 档的模型不会被改动。",
|
|
7109
7257
|
"row.contextAllDefault": "全部恢复默认上下文",
|
package/lib/index.d.ts
CHANGED
|
@@ -481,9 +481,13 @@ declare function withChatCallableFunctions(rows: readonly TraeModelInfo[], chatF
|
|
|
481
481
|
/** Local DSH context budget per model; a value may only select an advertised window. */
|
|
482
482
|
type TraeContextBudget = number;
|
|
483
483
|
/**
|
|
484
|
-
* Apply the saved local budget. Trae advertises
|
|
485
|
-
*
|
|
486
|
-
*
|
|
484
|
+
* Apply the saved local budget. Trae advertises a dev window and a Max window
|
|
485
|
+
* per model, and the card may also offer the 500K middle step (see
|
|
486
|
+
* {@link contextBudgetOptions}), so a budget may only switch a model to a
|
|
487
|
+
* window that model actually offers — never to a fabricated number, and never
|
|
488
|
+
* to something wider than its Max (a saved 500K for a model later refreshed
|
|
489
|
+
* down to a 256K Max must fall back, not overflow). Everything else keeps the
|
|
490
|
+
* dev window.
|
|
487
491
|
*/
|
|
488
492
|
declare function applyContextBudgets(catalog: readonly TraeModelInfo[], budgets?: Readonly<Record<string, TraeContextBudget | undefined>>): TraeModelInfo[];
|
|
489
493
|
/** Convert Trae metadata into catalog rows; image input is decided by {@link applyImageSelection}. */
|
|
@@ -1081,6 +1085,29 @@ interface TraeWebSearchPath {
|
|
|
1081
1085
|
reason: 'missing' | 'unreadable' | 'invalid';
|
|
1082
1086
|
message?: string;
|
|
1083
1087
|
}
|
|
1088
|
+
/**
|
|
1089
|
+
* A plugin-owned credential copy that was found unreadable and moved aside.
|
|
1090
|
+
*
|
|
1091
|
+
* Since 4.4.6 such a copy is quarantined (renamed aside, bytes intact)
|
|
1092
|
+
* instead of being merged over, because merging over it would erase every
|
|
1093
|
+
* account it carried. The copy then exists ONLY in that quarantine file — so
|
|
1094
|
+
* the card says so, instead of the user discovering it by noticing accounts
|
|
1095
|
+
* missing (issue #56).
|
|
1096
|
+
*
|
|
1097
|
+
* Only facts about the plugin's own file cross to the browser: its path, the
|
|
1098
|
+
* size of the quarantine copy, and how many accounts it still shows. No token
|
|
1099
|
+
* material, and no path outside DSH's own home.
|
|
1100
|
+
*/
|
|
1101
|
+
interface TraeWebQuarantine {
|
|
1102
|
+
/** The live path the copy had before it was moved aside. */
|
|
1103
|
+
file: string;
|
|
1104
|
+
/** The quarantine copy's path; its name carries the moment it was taken. */
|
|
1105
|
+
quarantinedAs: string;
|
|
1106
|
+
/** Size of the quarantined copy in bytes (`0` when it could not be read). */
|
|
1107
|
+
bytes: number;
|
|
1108
|
+
/** Accounts the copy still shows, when it could be read as JSON. */
|
|
1109
|
+
accounts?: number;
|
|
1110
|
+
}
|
|
1084
1111
|
/**
|
|
1085
1112
|
* Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
|
|
1086
1113
|
* volatile-marked settings field as (see {@link unwrapVolatileDeep}).
|
|
@@ -1229,6 +1256,12 @@ type TraeWebUsage = {
|
|
|
1229
1256
|
accounts: readonly TraeWebAccount[];
|
|
1230
1257
|
message?: string;
|
|
1231
1258
|
searched?: readonly TraeWebSearchPath[];
|
|
1259
|
+
/**
|
|
1260
|
+
* Copies that were found unreadable and moved aside. Present on BOTH
|
|
1261
|
+
* shapes on purpose: the signed-out screen is exactly where a user lands
|
|
1262
|
+
* after a lost copy, so that is where the notice has to be.
|
|
1263
|
+
*/
|
|
1264
|
+
quarantined?: readonly TraeWebQuarantine[];
|
|
1232
1265
|
enabled?: boolean;
|
|
1233
1266
|
} | {
|
|
1234
1267
|
status: 'signed-in';
|
|
@@ -1249,6 +1282,12 @@ type TraeWebUsage = {
|
|
|
1249
1282
|
accounts: readonly TraeWebAccount[];
|
|
1250
1283
|
models: readonly TraeWebModel[];
|
|
1251
1284
|
enabledModelIds: readonly string[];
|
|
1285
|
+
/**
|
|
1286
|
+
* Copies that were found unreadable and moved aside. Present on BOTH
|
|
1287
|
+
* shapes on purpose: the signed-out screen is exactly where a user lands
|
|
1288
|
+
* after a lost copy, so that is where the notice has to be.
|
|
1289
|
+
*/
|
|
1290
|
+
quarantined?: readonly TraeWebQuarantine[];
|
|
1252
1291
|
rawChat?: {
|
|
1253
1292
|
state: 'disabled' | 'unchecked' | 'available' | 'protocol-gated' | 'authentication' | 'credit' | 'rate' | 'transport' | 'server';
|
|
1254
1293
|
checkedAtMs?: number;
|
|
@@ -1786,6 +1825,34 @@ interface TraeCandidateFailure {
|
|
|
1786
1825
|
reason: 'missing' | 'unreadable' | 'invalid';
|
|
1787
1826
|
message?: string;
|
|
1788
1827
|
}
|
|
1828
|
+
/**
|
|
1829
|
+
* A plugin-owned copy that was found UNREADABLE and therefore moved aside.
|
|
1830
|
+
*
|
|
1831
|
+
* Since 4.4.6 a copy this build cannot parse is never merged over: it is
|
|
1832
|
+
* renamed to `<file>.corrupt-<stamp>` (bytes intact, recoverable) and the merge
|
|
1833
|
+
* restarts from a clean file. That trade — a quarantined copy instead of a
|
|
1834
|
+
* silently emptied one — is only fair if the USER can see it happened, because
|
|
1835
|
+
* the accounts the copy carried are, from that moment on, only in the
|
|
1836
|
+
* quarantine file.
|
|
1837
|
+
*
|
|
1838
|
+
* This notice is what makes that visible: the card's diagnostics panel shows
|
|
1839
|
+
* it, and so does the signed-out screen — which is exactly the state a user
|
|
1840
|
+
* lands in after the loss (issue #56).
|
|
1841
|
+
*/
|
|
1842
|
+
interface TraeQuarantineNotice {
|
|
1843
|
+
/** The live path the copy had before it was moved aside. */
|
|
1844
|
+
file: string;
|
|
1845
|
+
/** The quarantine copy's full path; its name carries the moment it was taken. */
|
|
1846
|
+
quarantinedAs: string;
|
|
1847
|
+
/** Size of the quarantined copy in bytes (`0` when it could not be read). */
|
|
1848
|
+
bytes: number;
|
|
1849
|
+
/**
|
|
1850
|
+
* How many accounts the quarantined copy held, when it could still be read
|
|
1851
|
+
* as JSON. A copy quarantined for being TORN cannot be counted, and this is
|
|
1852
|
+
* then absent rather than guessed.
|
|
1853
|
+
*/
|
|
1854
|
+
accounts?: number;
|
|
1855
|
+
}
|
|
1789
1856
|
/**
|
|
1790
1857
|
* Plugin-owned copy path for one region. Each region's store refreshes into
|
|
1791
1858
|
* its own file so two simultaneously signed-in regions never overwrite each
|
|
@@ -2209,6 +2276,21 @@ declare class TraeCredentialStore {
|
|
|
2209
2276
|
* files are skipped rather than propagated.
|
|
2210
2277
|
*/
|
|
2211
2278
|
private readOwns;
|
|
2279
|
+
/**
|
|
2280
|
+
* The copies that were found unreadable and moved aside.
|
|
2281
|
+
*
|
|
2282
|
+
* SCANNED, not remembered: the quarantine file is itself the durable
|
|
2283
|
+
* record (its name carries the moment it was taken), so the plugin does not
|
|
2284
|
+
* keep a second, parallel ledger that could disagree with the disk. The
|
|
2285
|
+
* notice therefore appears exactly as long as the evidence does — and
|
|
2286
|
+
* disappears when the user deletes the quarantined copy, which is what
|
|
2287
|
+
* "I have dealt with it" means.
|
|
2288
|
+
*
|
|
2289
|
+
* A notice is a FACT about the user's disk, never about their credentials:
|
|
2290
|
+
* it names a path, a size and a count, and the path is the one the plugin
|
|
2291
|
+
* owns (inside DSH's own home), never a Trae application directory.
|
|
2292
|
+
*/
|
|
2293
|
+
quarantinedCopies(): Promise<TraeQuarantineNotice[]>;
|
|
2212
2294
|
private refreshNow;
|
|
2213
2295
|
}
|
|
2214
2296
|
//#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, rename, 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";
|
|
@@ -15,6 +15,55 @@ import { execFile } from "node:child_process";
|
|
|
15
15
|
import { promisify } from "node:util";
|
|
16
16
|
import { createServer } from "node:http";
|
|
17
17
|
import { Readable } from "node:stream";
|
|
18
|
+
//#region src/context-budget.ts
|
|
19
|
+
/**
|
|
20
|
+
* Context-budget arithmetic shared by the HOST (which applies a saved budget to
|
|
21
|
+
* the catalog DSH is told about) and the CLIENT card (which renders the radios).
|
|
22
|
+
*
|
|
23
|
+
* It lives in its own module because the client bundle cannot import
|
|
24
|
+
* `catalog.ts` (host-only) and the two sides must agree EXACTLY on which
|
|
25
|
+
* windows are selectable: a card that offers a window the host then refuses
|
|
26
|
+
* would be a control that silently does nothing.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The one intermediate budget the card offers, in tokens: 500K.
|
|
30
|
+
*
|
|
31
|
+
* WHY IT EXISTS: Trae advertises a dev window (typically 200K/256K/272K) and a
|
|
32
|
+
* Max window (typically 1M). Switching dev→Max is a 5× jump for a user who
|
|
33
|
+
* just wants more room without paying Max-tier pricing for the whole session.
|
|
34
|
+
* 500K is the half-way point, and it is only ever offered for a model whose
|
|
35
|
+
* Max window is at least that wide.
|
|
36
|
+
*
|
|
37
|
+
* WHY A CONSTANT AND NOT A PER-MODEL VALUE: this is a LOCAL preference that
|
|
38
|
+
* DSH uses to decide when to compress; it is not a Trae-advertised window. The
|
|
39
|
+
* value must be sane for every model that can select it, so eligibility is
|
|
40
|
+
* gated on `maxContextWindow >= 500000` rather than inventing a window per
|
|
41
|
+
* model. `applyContextBudgets` additionally refuses any budget wider than what
|
|
42
|
+
* the model advertises, so a stale saved value cannot overflow a shrunken Max.
|
|
43
|
+
*/
|
|
44
|
+
const TRAE_CONTEXT_BUDGET_MID = 5e5;
|
|
45
|
+
/**
|
|
46
|
+
* The selectable windows for one model, smallest first: dev, then 500K (only
|
|
47
|
+
* when it is a genuine middle step), then Max. Duplicates are dropped so a
|
|
48
|
+
* model whose dev window is already 500K offers it once.
|
|
49
|
+
*
|
|
50
|
+
* A model with no Max window yields exactly one option — its dev window — which
|
|
51
|
+
* is what makes the card's radios read-only for such models.
|
|
52
|
+
*/
|
|
53
|
+
function contextBudgetOptions(model) {
|
|
54
|
+
const dev = model.contextWindow;
|
|
55
|
+
const max = model.maxContextWindow;
|
|
56
|
+
const options = [];
|
|
57
|
+
const push = (value) => {
|
|
58
|
+
if (value === void 0 || value < 1) return;
|
|
59
|
+
if (!options.includes(value)) options.push(value);
|
|
60
|
+
};
|
|
61
|
+
push(dev);
|
|
62
|
+
if (max !== void 0 && max >= 5e5 && (dev === void 0 || 5e5 > dev)) push(TRAE_CONTEXT_BUDGET_MID);
|
|
63
|
+
push(max);
|
|
64
|
+
return options.sort((left, right) => left - right);
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
18
67
|
//#region src/catalog.ts
|
|
19
68
|
/**
|
|
20
69
|
* Bootstrap catalog: identity only where current Trae metadata has not been
|
|
@@ -425,15 +474,22 @@ function withChatCallableFunctions(rows, chatFunctions) {
|
|
|
425
474
|
});
|
|
426
475
|
}
|
|
427
476
|
/**
|
|
428
|
-
* Apply the saved local budget. Trae advertises
|
|
429
|
-
*
|
|
430
|
-
*
|
|
477
|
+
* Apply the saved local budget. Trae advertises a dev window and a Max window
|
|
478
|
+
* per model, and the card may also offer the 500K middle step (see
|
|
479
|
+
* {@link contextBudgetOptions}), so a budget may only switch a model to a
|
|
480
|
+
* window that model actually offers — never to a fabricated number, and never
|
|
481
|
+
* to something wider than its Max (a saved 500K for a model later refreshed
|
|
482
|
+
* down to a 256K Max must fall back, not overflow). Everything else keeps the
|
|
483
|
+
* dev window.
|
|
431
484
|
*/
|
|
432
485
|
function applyContextBudgets(catalog, budgets = {}) {
|
|
433
|
-
return catalog.map((model) =>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
486
|
+
return catalog.map((model) => {
|
|
487
|
+
const budget = budgets[model.id];
|
|
488
|
+
return {
|
|
489
|
+
...model,
|
|
490
|
+
...budget !== void 0 && contextBudgetOptions(model).includes(budget) ? { contextWindow: budget } : {}
|
|
491
|
+
};
|
|
492
|
+
});
|
|
437
493
|
}
|
|
438
494
|
/** Convert Trae metadata into catalog rows; image input is decided by {@link applyImageSelection}. */
|
|
439
495
|
function discoveredCatalog(models) {
|
|
@@ -2293,6 +2349,30 @@ async function writeOwn(path, credentials) {
|
|
|
2293
2349
|
dirMode: 448
|
|
2294
2350
|
});
|
|
2295
2351
|
}
|
|
2352
|
+
/**
|
|
2353
|
+
* How many accounts a quarantined copy still shows, when it can be read at all.
|
|
2354
|
+
*
|
|
2355
|
+
* A copy is quarantined because this build could not PARSE it — but not every
|
|
2356
|
+
* unparseable copy is unreadable: a file whose JSON is intact yet carries a
|
|
2357
|
+
* version this build does not know still parses as JSON, and its account rows
|
|
2358
|
+
* are then countable. That number is what tells the user whether one account
|
|
2359
|
+
* or five went missing, so it is worth the attempt.
|
|
2360
|
+
*
|
|
2361
|
+
* A torn copy yields nothing: the count is then ABSENT rather than guessed,
|
|
2362
|
+
* because a wrong count is worse than no count.
|
|
2363
|
+
*/
|
|
2364
|
+
function countStoredAccounts(text) {
|
|
2365
|
+
try {
|
|
2366
|
+
const parsed = JSON.parse(text);
|
|
2367
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
2368
|
+
const document = parsed;
|
|
2369
|
+
if (Array.isArray(document.credentials)) return document.credentials.length;
|
|
2370
|
+
if (document.credential !== void 0) return 1;
|
|
2371
|
+
return;
|
|
2372
|
+
} catch {
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2296
2376
|
/** The key Trae stores the per-user region map under, beside the login itself. */
|
|
2297
2377
|
const TRAE_USERTAG_STORAGE_KEY = "iCubeAuthInfo://usertag";
|
|
2298
2378
|
/**
|
|
@@ -3115,6 +3195,54 @@ var TraeCredentialStore = class {
|
|
|
3115
3195
|
} catch {}
|
|
3116
3196
|
return copies;
|
|
3117
3197
|
}
|
|
3198
|
+
/**
|
|
3199
|
+
* The copies that were found unreadable and moved aside.
|
|
3200
|
+
*
|
|
3201
|
+
* SCANNED, not remembered: the quarantine file is itself the durable
|
|
3202
|
+
* record (its name carries the moment it was taken), so the plugin does not
|
|
3203
|
+
* keep a second, parallel ledger that could disagree with the disk. The
|
|
3204
|
+
* notice therefore appears exactly as long as the evidence does — and
|
|
3205
|
+
* disappears when the user deletes the quarantined copy, which is what
|
|
3206
|
+
* "I have dealt with it" means.
|
|
3207
|
+
*
|
|
3208
|
+
* A notice is a FACT about the user's disk, never about their credentials:
|
|
3209
|
+
* it names a path, a size and a count, and the path is the one the plugin
|
|
3210
|
+
* owns (inside DSH's own home), never a Trae application directory.
|
|
3211
|
+
*/
|
|
3212
|
+
async quarantinedCopies() {
|
|
3213
|
+
const notices = [];
|
|
3214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3215
|
+
for (const path of this.ownCandidates()) {
|
|
3216
|
+
const directory = dirname(path);
|
|
3217
|
+
const prefix = `${basename(path)}.corrupt-`;
|
|
3218
|
+
let entries;
|
|
3219
|
+
try {
|
|
3220
|
+
entries = await readdir(directory);
|
|
3221
|
+
} catch {
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
for (const name of entries) {
|
|
3225
|
+
if (!name.startsWith(prefix)) continue;
|
|
3226
|
+
const quarantinedAs = join(directory, name);
|
|
3227
|
+
if (seen.has(quarantinedAs)) continue;
|
|
3228
|
+
seen.add(quarantinedAs);
|
|
3229
|
+
let bytes = 0;
|
|
3230
|
+
let accounts;
|
|
3231
|
+
try {
|
|
3232
|
+
const text = await readFile(quarantinedAs, "utf8");
|
|
3233
|
+
bytes = Buffer.byteLength(text);
|
|
3234
|
+
accounts = countStoredAccounts(text);
|
|
3235
|
+
} catch {}
|
|
3236
|
+
notices.push({
|
|
3237
|
+
file: path,
|
|
3238
|
+
quarantinedAs,
|
|
3239
|
+
bytes,
|
|
3240
|
+
...accounts === void 0 ? {} : { accounts }
|
|
3241
|
+
});
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
return notices;
|
|
3245
|
+
}
|
|
3118
3246
|
async refreshNow(credential) {
|
|
3119
3247
|
if (credential.refreshToken === void 0 || credential.refreshExpiresAtMs !== void 0 && credential.refreshExpiresAtMs <= Date.now()) {
|
|
3120
3248
|
if (credential.expiresAtMs > Date.now() + 3e4) return credential;
|
|
@@ -8010,12 +8138,26 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
8010
8138
|
* would offer a binding that cannot work.
|
|
8011
8139
|
*/
|
|
8012
8140
|
const accounts = (await store.accounts()).filter((account) => panel !== "enterprise" || account.enterprise);
|
|
8141
|
+
/**
|
|
8142
|
+
* Copies that were found unreadable and moved aside (4.4.6+).
|
|
8143
|
+
*
|
|
8144
|
+
* Read ONCE per document and never allowed to fail it: a notice is a fact
|
|
8145
|
+
* about the user's disk, and a card that failed to load would teach the user
|
|
8146
|
+
* nothing about it. It is also the one piece of the document that explains a
|
|
8147
|
+
* MISSING ACCOUNT, so it belongs on both shapes — the signed-out one
|
|
8148
|
+
* especially, because that is where a lost copy leaves the user (issue #56).
|
|
8149
|
+
*
|
|
8150
|
+
* The scan is a directory listing beside the plugin's OWN files: cheap, and
|
|
8151
|
+
* it reads nothing but file names and sizes.
|
|
8152
|
+
*/
|
|
8153
|
+
const quarantined = await Promise.resolve().then(() => store.quarantinedCopies()).catch(() => []);
|
|
8013
8154
|
if ((await store.status()).state !== "signed-in") {
|
|
8014
8155
|
const { failures } = await store.diagnose();
|
|
8015
8156
|
return {
|
|
8016
8157
|
status: "signed-out",
|
|
8017
8158
|
accounts,
|
|
8018
8159
|
enabled,
|
|
8160
|
+
quarantined,
|
|
8019
8161
|
searched: failures.map((failure) => ({
|
|
8020
8162
|
path: failure.path,
|
|
8021
8163
|
edition: failure.edition,
|
|
@@ -8034,6 +8176,7 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
8034
8176
|
status: "signed-out",
|
|
8035
8177
|
accounts,
|
|
8036
8178
|
enabled,
|
|
8179
|
+
quarantined,
|
|
8037
8180
|
message: safeMessage(error)
|
|
8038
8181
|
};
|
|
8039
8182
|
}
|
|
@@ -8050,6 +8193,7 @@ async function traeWebUsage(deps, panel, options = {}) {
|
|
|
8050
8193
|
...model.input === void 0 ? {} : { input: [...model.input] }
|
|
8051
8194
|
})),
|
|
8052
8195
|
enabledModelIds: [...deps.enabledModelIds(panel)],
|
|
8196
|
+
quarantined,
|
|
8053
8197
|
...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(panel) }
|
|
8054
8198
|
};
|
|
8055
8199
|
if (fast) {
|
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.
|
|
5
|
+
"version": "4.5.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"verify:release": "node scripts/verify-release.mjs",
|
|
53
53
|
"card:shot": "vitest run --config vitest.card.config.ts",
|
|
54
54
|
"promo:shot": "vitest run --config vitest.card.config.ts -t \"promo shot\"",
|
|
55
|
-
"panels:shot": "vitest run --config vitest.card.config.ts -t \"panels shot\""
|
|
55
|
+
"panels:shot": "vitest run --config vitest.card.config.ts -t \"panels shot\"",
|
|
56
|
+
"prebuild": "node scripts/warn-live-host.mjs"
|
|
56
57
|
},
|
|
57
58
|
"dsh": {
|
|
58
59
|
"bundle": {
|