dsh-connect-trae 1.4.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 +15 -0
- package/lib/client.js +50 -2
- package/lib/index.d.ts +43 -4
- package/lib/index.js +213 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
3
18
|
## 1.4.0 (2026-09-10)
|
|
4
19
|
|
|
5
20
|
### 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
|
@@ -599,6 +599,46 @@ function parseTraeStorageDocument(text) {
|
|
|
599
599
|
if (typeof value !== "string") throw new Error(`Trae storage document has no ${TRAE_AUTH_STORAGE_KEY}`);
|
|
600
600
|
return parseTraeAuthValue(value);
|
|
601
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
|
+
}
|
|
602
642
|
//#endregion
|
|
603
643
|
//#region src/paths.ts
|
|
604
644
|
const APP_NAMES = {
|
|
@@ -607,6 +647,33 @@ const APP_NAMES = {
|
|
|
607
647
|
solo: "TRAE SOLO CN",
|
|
608
648
|
"solo-sg": "TRAE SOLO"
|
|
609
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
|
+
};
|
|
610
677
|
function traeStorageCandidates(platform = process.platform, home = homedir(), env = process.env) {
|
|
611
678
|
const result = [];
|
|
612
679
|
for (const edition of [
|
|
@@ -617,19 +684,59 @@ function traeStorageCandidates(platform = process.platform, home = homedir(), en
|
|
|
617
684
|
]) {
|
|
618
685
|
const app = APP_NAMES[edition];
|
|
619
686
|
let roots;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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({
|
|
625
702
|
edition,
|
|
626
|
-
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"
|
|
627
732
|
});
|
|
628
733
|
}
|
|
629
734
|
return result;
|
|
630
735
|
}
|
|
631
736
|
//#endregion
|
|
632
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";
|
|
633
740
|
const OWN_VERSION = 1;
|
|
634
741
|
const TRAE_AUTH_FILENAME = ".trae-auth.json";
|
|
635
742
|
function traeOwnAuthPath() {
|
|
@@ -718,10 +825,18 @@ var TraeCredentialStore = class {
|
|
|
718
825
|
this.inflight = void 0;
|
|
719
826
|
}
|
|
720
827
|
candidates() {
|
|
721
|
-
if (this.storagePathOverride !== void 0)
|
|
722
|
-
edition
|
|
723
|
-
|
|
724
|
-
|
|
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
|
+
}
|
|
725
840
|
const all = traeStorageCandidates();
|
|
726
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"));
|
|
727
842
|
}
|
|
@@ -785,21 +900,87 @@ var TraeCredentialStore = class {
|
|
|
785
900
|
await rm(`${this.ownPath}.lock`, { force: true });
|
|
786
901
|
}
|
|
787
902
|
async readAll() {
|
|
788
|
-
const desktop = await this.readDesktopAll();
|
|
903
|
+
const { credentials: desktop } = await this.readDesktopAll();
|
|
789
904
|
const own = await this.readOwn();
|
|
790
905
|
const cnOwn = own?.edition === "cn" || own?.edition === "solo" ? own : void 0;
|
|
791
906
|
if (cnOwn === void 0 || desktop.some((credential) => traeAccountId(credential) === traeAccountId(cnOwn))) return desktop;
|
|
792
907
|
return [...desktop, cnOwn];
|
|
793
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
|
+
}
|
|
794
963
|
async readDesktopAll() {
|
|
795
964
|
const credentials = [];
|
|
965
|
+
const failures = [];
|
|
796
966
|
for (const candidate of this.candidates()) try {
|
|
797
|
-
const credential =
|
|
798
|
-
if (
|
|
799
|
-
} 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
|
+
});
|
|
800
978
|
continue;
|
|
801
979
|
}
|
|
802
|
-
return
|
|
980
|
+
return {
|
|
981
|
+
credentials,
|
|
982
|
+
failures
|
|
983
|
+
};
|
|
803
984
|
}
|
|
804
985
|
async readOwn() {
|
|
805
986
|
try {
|
|
@@ -2513,10 +2694,20 @@ function toCredits(snapshot) {
|
|
|
2513
2694
|
*/
|
|
2514
2695
|
async function traeWebUsage(deps) {
|
|
2515
2696
|
const accounts = await deps.store.accounts();
|
|
2516
|
-
if ((await deps.store.status()).state !== "signed-in")
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
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
|
+
}
|
|
2520
2711
|
let credential;
|
|
2521
2712
|
try {
|
|
2522
2713
|
credential = await deps.store.resolve();
|
|
@@ -2804,9 +2995,10 @@ function apply(ctx, config) {
|
|
|
2804
2995
|
refresh: (credential) => refreshTraeCredential(credential)
|
|
2805
2996
|
});
|
|
2806
2997
|
const identity = async () => {
|
|
2807
|
-
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)) : [{
|
|
2808
2999
|
edition: config.edition === void 0 || config.edition === "auto" ? "solo" : config.edition,
|
|
2809
|
-
path: config.authFile
|
|
3000
|
+
path: config.authFile,
|
|
3001
|
+
source: "desktop"
|
|
2810
3002
|
}];
|
|
2811
3003
|
if (candidates.length === 0) throw new Error("Trae storage was not found");
|
|
2812
3004
|
return pickTraeStorageIdentity(candidates);
|
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.4.
|
|
5
|
+
"version": "1.4.1",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|