dsh-connect-trae 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/lib/client.js +50 -2
- package/lib/index.d.ts +43 -4
- package/lib/index.js +221 -25
- package/package.json +24 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.1 (2026-09-14)
|
|
4
|
+
|
|
5
|
+
> **版本号说明**:`v1.4.1` ~ `v1.4.4` 四个 git tag 曾推送到远端,但对应代码已整体回档到 `1.4.0`,
|
|
6
|
+
> 且这批版本**从未发布到 npm**。本次发布前已删除这四个废弃 tag(备份见
|
|
7
|
+
> `backup/abandoned-v1.4.1-v1.4.4` 分支及 `abandoned-tags-v1.4.1-4.bundle`),
|
|
8
|
+
> 使 git tag 与 npm 版本号从 `1.4.1` 起重新对齐、连续编号。
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
|
|
12
|
+
- 修复 CLI 登录(`traecli`)不被识别、插件恒显「未登录」的问题(issue #5,WSL2 用户报告)。插件此前只认桌面版 Electron 的 `globalStorage/storage.json`(加密值 `iCubeAuthInfo://icube.cloudide`),而 `traecli` 把登录信息写在自己的家目录里、内容是**未加密的裸 JWT**(实测 macOS 上是 `~/.trae-cn/trae-jwt-token`);纯 CLI 环境(WSL2 常见)根本没有 `storage.json`,因此永远解析不出账号。现在:
|
|
13
|
+
- 凭据来源扩展为 `desktop` / `cli` 两类,`TraeStorageCandidate` 新增 `source` 字段;新增 `parseTraeCliToken()` 直接解析裸 JWT(取 `data.user_id` 与 `exp`),不走 AES 解密链路。CLI token 不含 host 声明,统一补 CN 主机 `https://api.trae.cn`,避免空字符串变成不可用的 base URL。
|
|
14
|
+
- CLI 候选路径在 macOS / Windows / Linux 三平台都会探测(`~/.trae-cn/`、`~/.trae/`)。
|
|
15
|
+
- Linux 桌面版目录名改为**多候选并列探测**(`trae-cn` 与 macOS 拼写 `Trae CN` 都试)。此前只认从 macOS 抄来的 `Trae CN`,而 Linux 上 Electron 应用通常用小写无空格目录名;该拼写从未在 Linux 真机验证过。多探测保证猜错也不会漏掉真实安装。
|
|
16
|
+
- 把 `readDesktopAll()` 里的静默 `catch { continue }` 改为记录失败原因,新增 `TraeCredentialStore.diagnose()`:返回探测过的每个路径及其失败类型(`missing` / `unreadable` / `invalid`)。未登录时卡片新增可折叠的「已检查的路径」列表。此前无论路径不存在、key 缺失还是加密头不支持,用户都只看到「未登录」三个字,无从自助定位——`docs/WINDOWS_TOKEN_PROBE.md` 整篇文档的存在本身就是这个可观测性缺口的补丁。诊断内容只含路径与固定原因文案(错误消息不回显输入),不携带任何 token 材料,并有专门测试守住这一点。
|
|
17
|
+
|
|
18
|
+
## 1.4.0 (2026-09-10)
|
|
19
|
+
|
|
20
|
+
### Changes
|
|
21
|
+
|
|
22
|
+
- 对齐 DSH 内核 `0.1.2-rc.1` → `0.1.5-rc.2`(桌面 `dsh-plugin-desktop` 2.0.9 所捆绑通道;`0.1.5-rc.1` 为 npm `latest`,`rc.2` 在 `next`):
|
|
23
|
+
- `ResolvedPiAiProviderProfile` 在 0.1.5 新增**必填**字段 `modelErrors`:`PiAiAdapter` 现在按模型查此表,命中即以 `INVALID_CONFIG` 拒绝请求;`piProvider` 同时由必填改为可选。本插件是手工构造 profile(不走内核目录解析,故该表不会被子系统填充),补 `modelErrors: new Map()` 表达「所服务的模型全部可用」这一事实。不补则本插件在 0.1.5 宿主上**无法通过类型检查**(`TS2741`)。
|
|
24
|
+
- devDependencies 升级至 `@deepseek-ai/dsh-*@0.1.5-rc.2`,`@earendil-works/pi-ai` 由 `0.84.2` 升至 `^0.85.1`:`dsh-llm-pi-ai@0.1.5-rc.2` 要求 `pi-ai@^0.85.1`,版本不一致会在 `node_modules` 里留下两份互不兼容的 `pi-ai`,导致 `Provider<Api>` 结构不匹配的编译错误。
|
|
25
|
+
- `peerDependencies` 的 `@deepseek-ai/dsh-*` 由 `>=0.1.2-0` 抬到 `>=0.1.5-0`(`modelErrors` 为编译期硬依赖,0.1.2 宿主无法满足),`@earendil-works/pi-ai` 抬到 `>=0.85.1`。仍按范围声明、不锁死补丁版本,与内核 `next` 通道继续前进保持一致。
|
|
26
|
+
- 经核实本次不受文档列出的其余破坏项影响:未使用 `dsh-session` / `dsh-session-persistence` / `dsh-persona` / `dsh-system-prompt` / `dsh-message-feedback` / `dsh-subagent`;未读取 `DSH_SESSION_JSONL`、未调用已移除的 `locate()` / `readRaw()`;`AttachmentStore` 仅作类型引用,`IconChevronDownOutline14` 与客户端 `dsh.client.inject` 六个包在 0.1.5 均仍存在。会话格式 v3 与 persona 段改名对本插件不适用。
|
|
27
|
+
|
|
28
|
+
### Fixes
|
|
29
|
+
|
|
30
|
+
- 修复内置回退模型表被实时线路映射过滤、导致真实安装上模型几乎全部消失的问题:`FALLBACK_TRAE_MODELS` 是本插件自带的静态兜底表,其中每个 id 都**从未**由 Trae Remote 目录公布,因此永远不会出现在 `callableKeys` 里。此前 `configuredModels` / `derive` 把这张表也交给 `dropDeadModels` 过滤,只要实时目录只返回一部分模型(账号权限、版本或网络给出的子集),过滤就会把「这次没被提到的」回退模型一并删掉——本机实测在无凭据环境下只剩 `glm-5.2` / `kimi-k2.6` 两个模型可用。现在回退表不再参与线路过滤,并以 `wireResolved` 标志取代「`callableKeys` 非空」作为「线路目录已解析」的判据,避免空目录被误读为「没有模型可用」。新增回归测试覆盖该路径;该缺陷在本仓库 1.3.0 上本来就会失败(与内核升级无关),修复后 `pnpm run test` 首次达到 157/157 全绿(此前 154/156)。
|
|
31
|
+
|
|
3
32
|
## 1.3.0 (2026-09-08)
|
|
4
33
|
|
|
5
34
|
### Changes
|
package/lib/client.js
CHANGED
|
@@ -114,6 +114,13 @@ window.__ModuleLoader__.load({
|
|
|
114
114
|
.dsm-trae-usage-cheer{display:inline-flex;align-items:center;gap:4px;flex:none;text-decoration:underline;text-underline-offset:2px;color:var(--dsw-alias-label-tertiary,#999);font-size:13px;line-height:1.5;transition:color .16s}
|
|
115
115
|
.dsm-trae-usage-cheer-star{font-size:12px;line-height:1;display:inline-flex}
|
|
116
116
|
.dsm-trae-usage-cheer:hover{color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
117
|
+
.dsm-trae-searched{margin-top:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
118
|
+
.dsm-trae-searched>summary{cursor:pointer;color:var(--dsw-alias-label-secondary,#b8b8b8);user-select:none}
|
|
119
|
+
.dsm-trae-searched-hint{margin:6px 0 4px;color:var(--dsw-alias-label-tertiary,#999)}
|
|
120
|
+
.dsm-trae-searched-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}
|
|
121
|
+
.dsm-trae-searched-list li{display:flex;flex-direction:column;gap:1px;min-width:0}
|
|
122
|
+
.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}
|
|
123
|
+
.dsm-trae-searched-reason{color:var(--dsw-alias-label-tertiary,#999);font-size:11px}
|
|
117
124
|
.dsm-trae-usage-cheer:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:2px}
|
|
118
125
|
`;
|
|
119
126
|
//#endregion
|
|
@@ -618,10 +625,35 @@ window.__ModuleLoader__.load({
|
|
|
618
625
|
]
|
|
619
626
|
})
|
|
620
627
|
] }) : null,
|
|
621
|
-
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
628
|
+
status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
622
629
|
className: "dsm-trae-usage-text",
|
|
623
630
|
children: status.message ?? t("row.signedOutHint")
|
|
624
|
-
})
|
|
631
|
+
}), status.searched && status.searched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
632
|
+
className: "dsm-trae-searched",
|
|
633
|
+
children: [
|
|
634
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: [
|
|
635
|
+
t("row.searchedTitle"),
|
|
636
|
+
" (",
|
|
637
|
+
status.searched.length,
|
|
638
|
+
")"
|
|
639
|
+
] }),
|
|
640
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
641
|
+
className: "dsm-trae-searched-hint",
|
|
642
|
+
children: t("row.searchedHint")
|
|
643
|
+
}),
|
|
644
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
645
|
+
className: "dsm-trae-searched-list",
|
|
646
|
+
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", {
|
|
647
|
+
className: "dsm-trae-searched-reason",
|
|
648
|
+
children: [
|
|
649
|
+
t(item.source === "cli" ? "row.sourceCli" : "row.sourceDesktop"),
|
|
650
|
+
" · ",
|
|
651
|
+
t(item.reason === "missing" ? "row.reasonMissing" : item.reason === "unreadable" ? "row.reasonUnreadable" : "row.reasonInvalid")
|
|
652
|
+
]
|
|
653
|
+
})] }, `${item.source}:${item.path}`))
|
|
654
|
+
})
|
|
655
|
+
]
|
|
656
|
+
}) : null] }) : null,
|
|
625
657
|
status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
626
658
|
className: "dsm-trae-usage-error",
|
|
627
659
|
children: status.message
|
|
@@ -645,6 +677,14 @@ window.__ModuleLoader__.load({
|
|
|
645
677
|
"row.collapse": "Collapse",
|
|
646
678
|
"row.signedOut": "Not signed in",
|
|
647
679
|
"row.signedOutHint": "Sign in once in the Trae desktop app; this plugin follows that sign-in automatically.",
|
|
680
|
+
"row.searchedTitle": "Paths checked",
|
|
681
|
+
"row.searchedHint": "No sign-in was found. These locations were probed; if your Trae stores its login elsewhere, please report the path.",
|
|
682
|
+
"row.searchedMore": "Show {count} more",
|
|
683
|
+
"row.reasonMissing": "not found",
|
|
684
|
+
"row.reasonUnreadable": "unreadable",
|
|
685
|
+
"row.reasonInvalid": "unreadable format",
|
|
686
|
+
"row.sourceDesktop": "desktop app",
|
|
687
|
+
"row.sourceCli": "Trae CLI",
|
|
648
688
|
"row.signedIn": "Signed in: {accountName}",
|
|
649
689
|
"row.tokenExpiry": "Access token expires {expiresAt} (auto-renewal)",
|
|
650
690
|
"row.requestFailed": "Request failed",
|
|
@@ -692,6 +732,14 @@ window.__ModuleLoader__.load({
|
|
|
692
732
|
"row.collapse": "收起",
|
|
693
733
|
"row.signedOut": "未登录",
|
|
694
734
|
"row.signedOutHint": "在 Trae 桌面 App 里登录一次即可,插件会自动跟随当前登录的账号。",
|
|
735
|
+
"row.searchedTitle": "已检查的路径",
|
|
736
|
+
"row.searchedHint": "未找到登录信息。以下是插件探测过的位置;如果你的 Trae 把登录信息存在别处,请把该路径反馈给我们。",
|
|
737
|
+
"row.searchedMore": "展开其余 {count} 条",
|
|
738
|
+
"row.reasonMissing": "文件不存在",
|
|
739
|
+
"row.reasonUnreadable": "无法读取",
|
|
740
|
+
"row.reasonInvalid": "格式无法识别",
|
|
741
|
+
"row.sourceDesktop": "桌面版",
|
|
742
|
+
"row.sourceCli": "Trae CLI",
|
|
695
743
|
"row.signedIn": "已登录:{accountName}",
|
|
696
744
|
"row.tokenExpiry": "访问令牌 {expiresAt} 过期(自动续期)",
|
|
697
745
|
"row.requestFailed": "请求失败",
|
package/lib/index.d.ts
CHANGED
|
@@ -187,9 +187,16 @@ declare function createTraeAdapter(options: TraeAdapterOptions): TraeAdapter;
|
|
|
187
187
|
//#endregion
|
|
188
188
|
//#region src/paths.d.ts
|
|
189
189
|
type TraeEdition = 'cn' | 'sg' | 'solo' | 'solo-sg';
|
|
190
|
+
/**
|
|
191
|
+
* Where a local sign-in was persisted. `desktop` candidates hold an Electron
|
|
192
|
+
* `globalStorage/storage.json` whose auth value is encrypted; `cli` candidates
|
|
193
|
+
* hold a bare JWT written by the Trae CLI, which needs no decryption.
|
|
194
|
+
*/
|
|
195
|
+
type TraeCredentialSource = 'desktop' | 'cli';
|
|
190
196
|
interface TraeStorageCandidate {
|
|
191
197
|
edition: TraeEdition;
|
|
192
198
|
path: string;
|
|
199
|
+
source: TraeCredentialSource;
|
|
193
200
|
}
|
|
194
201
|
declare function traeStorageCandidates(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): TraeStorageCandidate[];
|
|
195
202
|
//#endregion
|
|
@@ -203,7 +210,7 @@ interface TraeCredential {
|
|
|
203
210
|
expiresAtMs: number;
|
|
204
211
|
refreshExpiresAtMs?: number;
|
|
205
212
|
edition: TraeEdition;
|
|
206
|
-
source: 'desktop' | 'dsh';
|
|
213
|
+
source: 'desktop' | 'dsh' | 'cli';
|
|
207
214
|
}
|
|
208
215
|
interface TraeRefreshOutcome {
|
|
209
216
|
accessToken: string;
|
|
@@ -224,10 +231,21 @@ interface TraeAccountChoice {
|
|
|
224
231
|
id: string;
|
|
225
232
|
accountName: string;
|
|
226
233
|
edition: TraeEdition;
|
|
227
|
-
source: 'desktop' | 'dsh';
|
|
234
|
+
source: 'desktop' | 'dsh' | 'cli';
|
|
228
235
|
tokenExpiresAtMs: number;
|
|
229
236
|
selected: boolean;
|
|
230
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Why one candidate path did not yield an account. These are safe to surface:
|
|
240
|
+
* they carry paths and error text, never token material.
|
|
241
|
+
*/
|
|
242
|
+
interface TraeCandidateFailure {
|
|
243
|
+
path: string;
|
|
244
|
+
edition: TraeEdition;
|
|
245
|
+
source: TraeCredentialSource;
|
|
246
|
+
reason: 'missing' | 'unreadable' | 'invalid';
|
|
247
|
+
message?: string;
|
|
248
|
+
}
|
|
231
249
|
declare function traeOwnAuthPath(): string;
|
|
232
250
|
declare function normalizeTraeCredential(raw: unknown, edition: TraeEdition, source: TraeCredential['source']): TraeCredential | undefined;
|
|
233
251
|
declare class TraeCredentialStore {
|
|
@@ -256,11 +274,23 @@ declare class TraeCredentialStore {
|
|
|
256
274
|
state: 'signed-in' | 'signed-out';
|
|
257
275
|
edition?: TraeEdition;
|
|
258
276
|
expiresAtMs?: number;
|
|
259
|
-
source?: '
|
|
277
|
+
source?: TraeCredential['source'];
|
|
260
278
|
}>;
|
|
261
279
|
desktopFilePresent(): Promise<boolean>;
|
|
262
280
|
logout(): Promise<void>;
|
|
263
281
|
private readAll;
|
|
282
|
+
/**
|
|
283
|
+
* Which paths were tried and why each one failed. Read-only and token-free:
|
|
284
|
+
* it exists so a signed-out card can explain itself instead of showing a bare
|
|
285
|
+
* "not signed in", which is undiagnosable on a machine whose layout differs
|
|
286
|
+
* from the ones the plugin was written against.
|
|
287
|
+
*/
|
|
288
|
+
diagnose(): Promise<{
|
|
289
|
+
tried: TraeStorageCandidate[];
|
|
290
|
+
failures: TraeCandidateFailure[];
|
|
291
|
+
}>;
|
|
292
|
+
/** Parse one candidate file's text into a credential, or throw. */
|
|
293
|
+
private credentialFrom;
|
|
264
294
|
private readDesktopAll;
|
|
265
295
|
private readOwn;
|
|
266
296
|
private refreshNow;
|
|
@@ -976,15 +1006,24 @@ interface TraeWebAccount {
|
|
|
976
1006
|
id: string;
|
|
977
1007
|
accountName: string;
|
|
978
1008
|
edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
|
|
979
|
-
source: 'desktop' | 'dsh';
|
|
1009
|
+
source: 'desktop' | 'dsh' | 'cli';
|
|
980
1010
|
tokenExpiresAtMs: number;
|
|
981
1011
|
selected: boolean;
|
|
982
1012
|
}
|
|
1013
|
+
/** One probed candidate path and why it did not yield an account. */
|
|
1014
|
+
interface TraeWebSearchPath {
|
|
1015
|
+
path: string;
|
|
1016
|
+
edition: 'cn' | 'sg' | 'solo' | 'solo-sg';
|
|
1017
|
+
source: 'desktop' | 'cli';
|
|
1018
|
+
reason: 'missing' | 'unreadable' | 'invalid';
|
|
1019
|
+
message?: string;
|
|
1020
|
+
}
|
|
983
1021
|
/** The JSON document the plugin card renders. */
|
|
984
1022
|
type TraeWebUsage = {
|
|
985
1023
|
status: 'signed-out';
|
|
986
1024
|
accounts: readonly TraeWebAccount[];
|
|
987
1025
|
message?: string;
|
|
1026
|
+
searched?: readonly TraeWebSearchPath[];
|
|
988
1027
|
} | {
|
|
989
1028
|
status: 'signed-in';
|
|
990
1029
|
accountId: string;
|
package/lib/index.js
CHANGED
|
@@ -262,6 +262,7 @@ function createTraeAdapter(options) {
|
|
|
262
262
|
streamIdleTimeoutMs: TRAE_STREAM_IDLE_TIMEOUT_MS,
|
|
263
263
|
retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-trae retryPolicy"),
|
|
264
264
|
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
265
|
+
modelErrors: /* @__PURE__ */ new Map(),
|
|
265
266
|
...REQUEST_IMAGE_BUDGETS,
|
|
266
267
|
piProvider: provider
|
|
267
268
|
};
|
|
@@ -598,6 +599,46 @@ function parseTraeStorageDocument(text) {
|
|
|
598
599
|
if (typeof value !== "string") throw new Error(`Trae storage document has no ${TRAE_AUTH_STORAGE_KEY}`);
|
|
599
600
|
return parseTraeAuthValue(value);
|
|
600
601
|
}
|
|
602
|
+
function decodeBase64UrlJson(segment) {
|
|
603
|
+
try {
|
|
604
|
+
const padded = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
605
|
+
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
|
606
|
+
const parsed = JSON.parse(decoded);
|
|
607
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
608
|
+
} catch {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Parse the CLI token file. Accepts either the bare JWT itself or a JSON
|
|
614
|
+
* envelope containing one, since the on-disk shape is only verified on macOS
|
|
615
|
+
* and a future CLI revision may wrap it.
|
|
616
|
+
*/
|
|
617
|
+
function parseTraeCliToken(text) {
|
|
618
|
+
const trimmed = text.trim();
|
|
619
|
+
if (trimmed === "") throw new Error("Trae CLI token file is empty");
|
|
620
|
+
let token = trimmed;
|
|
621
|
+
if (trimmed.startsWith("{")) {
|
|
622
|
+
const envelope = JSON.parse(trimmed);
|
|
623
|
+
const candidate = envelope["token"] ?? envelope["accessToken"] ?? envelope["jwt"];
|
|
624
|
+
if (typeof candidate !== "string" || candidate.trim() === "") throw new Error("Trae CLI token document has no token field");
|
|
625
|
+
token = candidate.trim();
|
|
626
|
+
}
|
|
627
|
+
const segments = token.split(".");
|
|
628
|
+
if (segments.length !== 3 || segments.some((segment) => segment === "")) throw new Error("Trae CLI token is not a three-part JWT");
|
|
629
|
+
const payload = decodeBase64UrlJson(segments[1]);
|
|
630
|
+
if (payload === void 0) throw new Error("Trae CLI token payload is not decodable JSON");
|
|
631
|
+
const data = typeof payload["data"] === "object" && payload["data"] !== null && !Array.isArray(payload["data"]) ? payload["data"] : void 0;
|
|
632
|
+
const userId = typeof data?.["user_id"] === "string" ? data["user_id"] : void 0;
|
|
633
|
+
if (userId === void 0 || userId === "") throw new Error("Trae CLI token has no data.user_id claim");
|
|
634
|
+
const exp = payload["exp"];
|
|
635
|
+
const expiresAtMs = typeof exp === "number" && Number.isFinite(exp) && exp > 0 ? exp * 1e3 : void 0;
|
|
636
|
+
return {
|
|
637
|
+
accessToken: token,
|
|
638
|
+
userId,
|
|
639
|
+
...expiresAtMs === void 0 ? {} : { expiresAtMs }
|
|
640
|
+
};
|
|
641
|
+
}
|
|
601
642
|
//#endregion
|
|
602
643
|
//#region src/paths.ts
|
|
603
644
|
const APP_NAMES = {
|
|
@@ -606,6 +647,33 @@ const APP_NAMES = {
|
|
|
606
647
|
solo: "TRAE SOLO CN",
|
|
607
648
|
"solo-sg": "TRAE SOLO"
|
|
608
649
|
};
|
|
650
|
+
/**
|
|
651
|
+
* Directory names the Trae CLI uses for its own home. These are deliberately
|
|
652
|
+
* separate from the Electron app-support names above: the CLI keeps a dotfile
|
|
653
|
+
* home (observed on macOS as `~/.trae-cn`) rather than an `Application Support`
|
|
654
|
+
* entry, so a machine with only the CLI installed has no `storage.json` at all.
|
|
655
|
+
*/
|
|
656
|
+
const CLI_HOME_NAMES = [".trae-cn", ".trae"];
|
|
657
|
+
/** Basename of the CLI's persisted bare JWT. */
|
|
658
|
+
const TRAE_CLI_TOKEN_FILENAME = "trae-jwt-token";
|
|
659
|
+
/**
|
|
660
|
+
* Linux desktop config directory names. Electron apps on Linux normally use a
|
|
661
|
+
* lowercase, space-free name rather than the macOS `Trae CN` spelling, so both
|
|
662
|
+
* are probed: guessing only the macOS spelling would silently miss a real
|
|
663
|
+
* install, and guessing only the Linux spelling would break every existing
|
|
664
|
+
* user. The actual name is unverified on a real Linux host.
|
|
665
|
+
*/
|
|
666
|
+
const LINUX_APP_NAMES = {
|
|
667
|
+
cn: [
|
|
668
|
+
"trae-cn",
|
|
669
|
+
"Trae CN",
|
|
670
|
+
"trae",
|
|
671
|
+
"Trae"
|
|
672
|
+
],
|
|
673
|
+
sg: ["trae", "Trae"],
|
|
674
|
+
solo: ["trae-solo-cn", "TRAE SOLO CN"],
|
|
675
|
+
"solo-sg": ["trae-solo", "TRAE SOLO"]
|
|
676
|
+
};
|
|
609
677
|
function traeStorageCandidates(platform = process.platform, home = homedir(), env = process.env) {
|
|
610
678
|
const result = [];
|
|
611
679
|
for (const edition of [
|
|
@@ -616,19 +684,59 @@ function traeStorageCandidates(platform = process.platform, home = homedir(), en
|
|
|
616
684
|
]) {
|
|
617
685
|
const app = APP_NAMES[edition];
|
|
618
686
|
let roots;
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
687
|
+
let appNames;
|
|
688
|
+
if (platform === "darwin") {
|
|
689
|
+
roots = [join(home, "Library", "Application Support")];
|
|
690
|
+
appNames = [app];
|
|
691
|
+
} else if (platform === "win32") {
|
|
692
|
+
roots = [env.APPDATA, join(home, "AppData", "Roaming")].filter((value, index, all) => typeof value === "string" && value !== "" && all.indexOf(value) === index);
|
|
693
|
+
appNames = [app];
|
|
694
|
+
} else if (platform === "linux") {
|
|
695
|
+
roots = [env.XDG_CONFIG_HOME || join(home, ".config")];
|
|
696
|
+
appNames = LINUX_APP_NAMES[edition];
|
|
697
|
+
} else {
|
|
698
|
+
roots = [];
|
|
699
|
+
appNames = [app];
|
|
700
|
+
}
|
|
701
|
+
for (const root of roots) for (const appName of appNames) result.push({
|
|
624
702
|
edition,
|
|
625
|
-
path: join(root,
|
|
703
|
+
path: join(root, appName, "User", "globalStorage", "storage.json"),
|
|
704
|
+
source: "desktop"
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return [...result, ...traeCliCandidates(platform, home, env)];
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Candidate CLI token paths. Only CN/SOLO editions are targeted here for the
|
|
711
|
+
* same reason the store ignores SG desktop installs, and `.trae-cn` is mapped
|
|
712
|
+
* to `cn` while `.trae` is the CLI's international home and is therefore
|
|
713
|
+
* skipped by the store's edition filter.
|
|
714
|
+
*
|
|
715
|
+
* The CLI home is a dotfile directory directly under `$HOME` on every platform
|
|
716
|
+
* observed so far, but this is only verified on macOS; the Windows and Linux
|
|
717
|
+
* spellings are probed speculatively and a miss is harmless because every
|
|
718
|
+
* candidate is tried in order.
|
|
719
|
+
*/
|
|
720
|
+
function traeCliCandidates(platform = process.platform, home = homedir(), env = process.env) {
|
|
721
|
+
const roots = [];
|
|
722
|
+
if (platform === "win32") {
|
|
723
|
+
for (const value of [env.USERPROFILE, home]) if (typeof value === "string" && value !== "" && !roots.includes(value)) roots.push(value);
|
|
724
|
+
} else roots.push(home);
|
|
725
|
+
const result = [];
|
|
726
|
+
for (const root of roots) for (const name of CLI_HOME_NAMES) {
|
|
727
|
+
const edition = name === ".trae-cn" ? "cn" : "sg";
|
|
728
|
+
result.push({
|
|
729
|
+
edition,
|
|
730
|
+
path: join(root, name, TRAE_CLI_TOKEN_FILENAME),
|
|
731
|
+
source: "cli"
|
|
626
732
|
});
|
|
627
733
|
}
|
|
628
734
|
return result;
|
|
629
735
|
}
|
|
630
736
|
//#endregion
|
|
631
737
|
//#region src/auth.ts
|
|
738
|
+
/** Host used for CLI tokens, which carry no host claim of their own. */
|
|
739
|
+
const CLI_DEFAULT_HOST = "https://api.trae.cn";
|
|
632
740
|
const OWN_VERSION = 1;
|
|
633
741
|
const TRAE_AUTH_FILENAME = ".trae-auth.json";
|
|
634
742
|
function traeOwnAuthPath() {
|
|
@@ -717,10 +825,18 @@ var TraeCredentialStore = class {
|
|
|
717
825
|
this.inflight = void 0;
|
|
718
826
|
}
|
|
719
827
|
candidates() {
|
|
720
|
-
if (this.storagePathOverride !== void 0)
|
|
721
|
-
edition
|
|
722
|
-
|
|
723
|
-
|
|
828
|
+
if (this.storagePathOverride !== void 0) {
|
|
829
|
+
const edition = this.edition === "auto" ? "cn" : this.edition;
|
|
830
|
+
return [{
|
|
831
|
+
edition,
|
|
832
|
+
path: this.storagePathOverride,
|
|
833
|
+
source: "desktop"
|
|
834
|
+
}, {
|
|
835
|
+
edition,
|
|
836
|
+
path: this.storagePathOverride,
|
|
837
|
+
source: "cli"
|
|
838
|
+
}];
|
|
839
|
+
}
|
|
724
840
|
const all = traeStorageCandidates();
|
|
725
841
|
return this.edition === "auto" ? all.filter((candidate) => candidate.edition === "cn" || candidate.edition === "solo") : all.filter((candidate) => candidate.edition === this.edition && (candidate.edition === "cn" || candidate.edition === "solo"));
|
|
726
842
|
}
|
|
@@ -784,21 +900,87 @@ var TraeCredentialStore = class {
|
|
|
784
900
|
await rm(`${this.ownPath}.lock`, { force: true });
|
|
785
901
|
}
|
|
786
902
|
async readAll() {
|
|
787
|
-
const desktop = await this.readDesktopAll();
|
|
903
|
+
const { credentials: desktop } = await this.readDesktopAll();
|
|
788
904
|
const own = await this.readOwn();
|
|
789
905
|
const cnOwn = own?.edition === "cn" || own?.edition === "solo" ? own : void 0;
|
|
790
906
|
if (cnOwn === void 0 || desktop.some((credential) => traeAccountId(credential) === traeAccountId(cnOwn))) return desktop;
|
|
791
907
|
return [...desktop, cnOwn];
|
|
792
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Which paths were tried and why each one failed. Read-only and token-free:
|
|
911
|
+
* it exists so a signed-out card can explain itself instead of showing a bare
|
|
912
|
+
* "not signed in", which is undiagnosable on a machine whose layout differs
|
|
913
|
+
* from the ones the plugin was written against.
|
|
914
|
+
*/
|
|
915
|
+
async diagnose() {
|
|
916
|
+
const tried = this.candidates();
|
|
917
|
+
const failures = [];
|
|
918
|
+
for (const candidate of tried) {
|
|
919
|
+
const raw = await readFile(candidate.path, "utf8").then((text) => ({ text }), (error) => ({ error }));
|
|
920
|
+
if ("error" in raw) {
|
|
921
|
+
const code = typeof raw.error === "object" && raw.error !== null && "code" in raw.error ? raw.error.code : void 0;
|
|
922
|
+
failures.push({
|
|
923
|
+
path: candidate.path,
|
|
924
|
+
edition: candidate.edition,
|
|
925
|
+
source: candidate.source,
|
|
926
|
+
reason: code === "ENOENT" ? "missing" : "unreadable",
|
|
927
|
+
...code === "ENOENT" ? {} : { message: String(raw.error) }
|
|
928
|
+
});
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
this.credentialFrom(candidate, raw.text);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
failures.push({
|
|
935
|
+
path: candidate.path,
|
|
936
|
+
edition: candidate.edition,
|
|
937
|
+
source: candidate.source,
|
|
938
|
+
reason: "invalid",
|
|
939
|
+
message: error instanceof Error ? error.message : String(error)
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
tried,
|
|
945
|
+
failures
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
/** Parse one candidate file's text into a credential, or throw. */
|
|
949
|
+
credentialFrom(candidate, text) {
|
|
950
|
+
let credential;
|
|
951
|
+
if (candidate.source === "cli") {
|
|
952
|
+
const claims = parseTraeCliToken(text);
|
|
953
|
+
credential = normalizeTraeCredential({
|
|
954
|
+
token: claims.accessToken,
|
|
955
|
+
userId: claims.userId,
|
|
956
|
+
host: CLI_DEFAULT_HOST,
|
|
957
|
+
expiredAt: claims.expiresAtMs
|
|
958
|
+
}, candidate.edition === "sg" ? "cn" : candidate.edition, "cli");
|
|
959
|
+
} else credential = normalizeTraeCredential(parseTraeStorageDocument(text), candidate.edition, "desktop");
|
|
960
|
+
if (credential === void 0) throw new Error(`${candidate.source} candidate could not be normalized into a credential`);
|
|
961
|
+
return credential;
|
|
962
|
+
}
|
|
793
963
|
async readDesktopAll() {
|
|
794
964
|
const credentials = [];
|
|
965
|
+
const failures = [];
|
|
795
966
|
for (const candidate of this.candidates()) try {
|
|
796
|
-
const credential =
|
|
797
|
-
if (
|
|
798
|
-
} catch {
|
|
967
|
+
const credential = this.credentialFrom(candidate, await readFile(candidate.path, "utf8"));
|
|
968
|
+
if (!credentials.some((existing) => traeAccountId(existing) === traeAccountId(credential))) credentials.push(credential);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
971
|
+
failures.push({
|
|
972
|
+
path: candidate.path,
|
|
973
|
+
edition: candidate.edition,
|
|
974
|
+
source: candidate.source,
|
|
975
|
+
reason: code === "ENOENT" ? "missing" : code === void 0 ? "invalid" : "unreadable",
|
|
976
|
+
...code === "ENOENT" || code === void 0 && !(error instanceof Error) ? {} : { message: error instanceof Error ? error.message : String(error) }
|
|
977
|
+
});
|
|
799
978
|
continue;
|
|
800
979
|
}
|
|
801
|
-
return
|
|
980
|
+
return {
|
|
981
|
+
credentials,
|
|
982
|
+
failures
|
|
983
|
+
};
|
|
802
984
|
}
|
|
803
985
|
async readOwn() {
|
|
804
986
|
try {
|
|
@@ -2512,10 +2694,20 @@ function toCredits(snapshot) {
|
|
|
2512
2694
|
*/
|
|
2513
2695
|
async function traeWebUsage(deps) {
|
|
2514
2696
|
const accounts = await deps.store.accounts();
|
|
2515
|
-
if ((await deps.store.status()).state !== "signed-in")
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2697
|
+
if ((await deps.store.status()).state !== "signed-in") {
|
|
2698
|
+
const { failures } = await deps.store.diagnose();
|
|
2699
|
+
return {
|
|
2700
|
+
status: "signed-out",
|
|
2701
|
+
accounts,
|
|
2702
|
+
searched: failures.map((failure) => ({
|
|
2703
|
+
path: failure.path,
|
|
2704
|
+
edition: failure.edition,
|
|
2705
|
+
source: failure.source,
|
|
2706
|
+
reason: failure.reason,
|
|
2707
|
+
...failure.message === void 0 ? {} : { message: safeMessage(failure.message) }
|
|
2708
|
+
}))
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2519
2711
|
let credential;
|
|
2520
2712
|
try {
|
|
2521
2713
|
credential = await deps.store.resolve();
|
|
@@ -2783,17 +2975,19 @@ function apply(ctx, config) {
|
|
|
2783
2975
|
const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
|
|
2784
2976
|
const imageSet = (value) => new Set(value.imageModelIds ?? []);
|
|
2785
2977
|
const callableKeys = /* @__PURE__ */ new Set();
|
|
2978
|
+
let wireResolved = false;
|
|
2786
2979
|
const dropDeadModels = (rows) => {
|
|
2787
|
-
if (
|
|
2980
|
+
if (!wireResolved) return rows;
|
|
2788
2981
|
return rows.filter((model) => callableKeys.has(model.id.trim().toLowerCase()) || callableKeys.has(model.name.trim().toLowerCase()));
|
|
2789
2982
|
};
|
|
2983
|
+
const fallbackModels = (value) => applyImageSelection(FALLBACK_TRAE_MODELS, imageSet(value));
|
|
2790
2984
|
const derive = (value, raw) => {
|
|
2791
2985
|
const selectedImages = imageSet(value);
|
|
2792
2986
|
const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw)), selectedImages), enabledSet(value), value.contextBudgets ?? {});
|
|
2793
|
-
return derived.length > 0 ? derived :
|
|
2987
|
+
return derived.length > 0 ? derived : fallbackModels(value);
|
|
2794
2988
|
};
|
|
2795
|
-
const configuredModels = (value) => value.lastCatalog?.length ? derive(value, value.lastCatalog) : value.models?.length ? derive(value, value.models) :
|
|
2796
|
-
const displayModels = (value) => value.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(value.lastCatalog)) : value.models?.length ? dropDeadModels(sanitizeCatalog(value.models)) :
|
|
2989
|
+
const configuredModels = (value) => value.lastCatalog?.length ? derive(value, value.lastCatalog) : value.models?.length ? derive(value, value.models) : fallbackModels(value);
|
|
2990
|
+
const displayModels = (value) => value.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(value.lastCatalog)) : value.models?.length ? dropDeadModels(sanitizeCatalog(value.models)) : FALLBACK_TRAE_MODELS;
|
|
2797
2991
|
const store = new TraeCredentialStore({
|
|
2798
2992
|
...config.authFile === void 0 ? {} : { storagePath: config.authFile },
|
|
2799
2993
|
edition: config.edition ?? "auto",
|
|
@@ -2801,9 +2995,10 @@ function apply(ctx, config) {
|
|
|
2801
2995
|
refresh: (credential) => refreshTraeCredential(credential)
|
|
2802
2996
|
});
|
|
2803
2997
|
const identity = async () => {
|
|
2804
|
-
const candidates = config.authFile === void 0 ? traeStorageCandidates().filter((item) => (item.edition === "cn" || item.edition === "solo") && (config.edition === void 0 || config.edition === "auto" || item.edition === config.edition)) : [{
|
|
2998
|
+
const candidates = config.authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (item.edition === "cn" || item.edition === "solo") && (config.edition === void 0 || config.edition === "auto" || item.edition === config.edition)) : [{
|
|
2805
2999
|
edition: config.edition === void 0 || config.edition === "auto" ? "solo" : config.edition,
|
|
2806
|
-
path: config.authFile
|
|
3000
|
+
path: config.authFile,
|
|
3001
|
+
source: "desktop"
|
|
2807
3002
|
}];
|
|
2808
3003
|
if (candidates.length === 0) throw new Error("Trae storage was not found");
|
|
2809
3004
|
return pickTraeStorageIdentity(candidates);
|
|
@@ -2869,6 +3064,7 @@ function apply(ctx, config) {
|
|
|
2869
3064
|
callableKeys.add(model.id.trim().toLowerCase());
|
|
2870
3065
|
callableKeys.add(model.name.trim().toLowerCase());
|
|
2871
3066
|
}
|
|
3067
|
+
wireResolved = merged.length > 0;
|
|
2872
3068
|
wireById.clear();
|
|
2873
3069
|
wireByName.clear();
|
|
2874
3070
|
for (const model of merged) if (model.wireConfigName !== void 0) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-trae",
|
|
3
3
|
"displayName": "DSH Connect Trae",
|
|
4
4
|
"description": "Connect locally signed-in Trae models to DeepSeek Harness, plus a read-only usage/credits summary.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.4.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -73,35 +73,35 @@
|
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
76
|
-
"@deepseek-ai/dsh-atomic-write": ">=0.1.
|
|
77
|
-
"@deepseek-ai/dsh-attachment": ">=0.1.
|
|
78
|
-
"@deepseek-ai/dsh-home-paths": ">=0.1.
|
|
79
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1.
|
|
80
|
-
"@deepseek-ai/dsh-llm": ">=0.1.
|
|
81
|
-
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.
|
|
82
|
-
"@deepseek-ai/dsh-settings": ">=0.1.
|
|
83
|
-
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.
|
|
76
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.1.5-0 <0.2.0-0",
|
|
77
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.5-0 <0.2.0-0",
|
|
78
|
+
"@deepseek-ai/dsh-home-paths": ">=0.1.5-0 <0.2.0-0",
|
|
79
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.5-0 <0.2.0-0",
|
|
80
|
+
"@deepseek-ai/dsh-llm": ">=0.1.5-0 <0.2.0-0",
|
|
81
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.5-0 <0.2.0-0",
|
|
82
|
+
"@deepseek-ai/dsh-settings": ">=0.1.5-0 <0.2.0-0",
|
|
83
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.5-0 <0.2.0-0",
|
|
84
84
|
"@deepseek-ai/schemastery": "^3.18.1-rc.1",
|
|
85
|
-
"@earendil-works/pi-ai": ">=0.
|
|
85
|
+
"@earendil-works/pi-ai": ">=0.85.1",
|
|
86
86
|
"react": "^18.2.0"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
89
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
90
|
-
"@deepseek-ai/dsh-atomic-write": "
|
|
91
|
-
"@deepseek-ai/dsh-attachment": "
|
|
92
|
-
"@deepseek-ai/dsh-client-locale": "0.1.
|
|
93
|
-
"@deepseek-ai/dsh-client-ui-renderer": "0.1.
|
|
94
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.
|
|
96
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.1.
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.1.
|
|
98
|
-
"@deepseek-ai/dsh-home-paths": "
|
|
99
|
-
"@deepseek-ai/dsh-host-webserver": "0.1.
|
|
100
|
-
"@deepseek-ai/dsh-llm": "
|
|
101
|
-
"@deepseek-ai/dsh-llm-pi-ai": "
|
|
102
|
-
"@deepseek-ai/dsh-settings": "
|
|
90
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.5-rc.2",
|
|
91
|
+
"@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
|
|
92
|
+
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
|
|
93
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.5-rc.2",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
|
|
98
|
+
"@deepseek-ai/dsh-home-paths": "0.1.5-rc.2",
|
|
99
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
|
|
100
|
+
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
|
|
101
|
+
"@deepseek-ai/dsh-llm-pi-ai": "0.1.5-rc.2",
|
|
102
|
+
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
|
|
103
103
|
"@deepseek-ai/schemastery": "3.18.2",
|
|
104
|
-
"@earendil-works/pi-ai": "0.
|
|
104
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
105
105
|
"@types/node": "^22.20.0",
|
|
106
106
|
"@types/react": "~18.3.31",
|
|
107
107
|
"react": "^18.3.1",
|