dsh-llm-adapters 1.2.0 → 1.2.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.
@@ -30,12 +30,12 @@ export interface RaccoonAccountRecord {
30
30
  loginAt: number;
31
31
  }
32
32
  /**
33
- * One Trae web login.
33
+ * One Trae login.
34
34
  *
35
- * `accessToken` is the `Cloud-IDE-JWT` the website itself uses. `deviceId` and
36
- * `machineId` are the fingerprints captured from that web session — this
37
- * plugin never reads a desktop install — and are replayed on chat, quota and
38
- * check-in requests.
35
+ * `accessToken` is the `Cloud-IDE-JWT` Trae's own clients use. `deviceId` and
36
+ * `machineId` are replayed on chat, quota and check-in requests. A web login
37
+ * captures them from its browser session; a `client` login copies the desktop
38
+ * client's own values, which is the only source the check-in route accepts.
39
39
  */
40
40
  export interface TraeAccountRecord {
41
41
  accessToken: string;
@@ -45,6 +45,8 @@ export interface TraeAccountRecord {
45
45
  userId?: string;
46
46
  name?: string;
47
47
  loginAt: number;
48
+ /** Where the token came from; absent means the plugin's browser login. */
49
+ source?: 'web' | 'client';
48
50
  /** The `Cloud-IDE-JWT` expiry (about 8 hours); absent when the JWT carries none. */
49
51
  expiresAt?: number;
50
52
  /**
@@ -16,7 +16,7 @@ import { LoomyProvider } from './loomy.ts';
16
16
  import { RaccoonProvider } from './raccoon.ts';
17
17
  import { TraeProvider } from './trae.ts';
18
18
  import type { AuthStatus } from './auth.ts';
19
- import type { CheckinStatus, ProviderStatus, QuotaSnapshot } from './types.ts';
19
+ import type { CheckinStatus, ClientLoginStatus, ProviderStatus, QuotaSnapshot } from './types.ts';
20
20
  import type { ReadImage } from './wire.ts';
21
21
  export interface AdapterOptions {
22
22
  brand: BrandId;
@@ -33,7 +33,7 @@ export interface AdapterOptions {
33
33
  *
34
34
  * Declared as an interface rather than a union so a new provider only has to
35
35
  * implement the behaviour, not edit the adapter's type list. The optional
36
- * check-in methods exist only on Trae.
36
+ * client-login and check-in methods exist only on Trae.
37
37
  */
38
38
  export interface ProviderClient {
39
39
  status(): Promise<AuthStatus>;
@@ -51,6 +51,10 @@ export interface ProviderClient {
51
51
  invalidateModels(): void;
52
52
  invalidateQuota(): void;
53
53
  close(): void;
54
+ /** Trae only: the local desktop client's current sign-in state. */
55
+ localClientLogin?(): Promise<ClientLoginStatus>;
56
+ /** Trae only: copy that state into the plugin's own account store. */
57
+ importClientLogin?(): Promise<void>;
54
58
  checkinStatus?(): Promise<CheckinStatus>;
55
59
  claimCheckin?(): Promise<CheckinStatus>;
56
60
  }
@@ -85,6 +89,8 @@ export declare abstract class LocalAdapter extends LlmAdapter {
85
89
  cancelBrowserLogin(): Promise<void>;
86
90
  /** Claim today's reward; providers without one reject it. */
87
91
  claimCheckin(): Promise<CheckinStatus>;
92
+ /** Import a local client login and return the refreshed account reading. */
93
+ importClientLogin(): Promise<ProviderStatus>;
88
94
  invalidateCatalog(): void;
89
95
  invalidateUsage(): void;
90
96
  /** Login state plus quota, shaped for the settings page. */
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Credential state for every provider.
3
3
  *
4
- * Sign-in happens in the provider's own web page, so credentials exist in
5
- * exactly one place: the plugin's own account store. No desktop client file is
6
- * discovered, read, or written, and no token leaves this module in errors or
7
- * display fields.
4
+ * Sign-in happens in the provider's own web page. Trae additionally offers an
5
+ * explicit, read-only import of the local desktop client's login state; nothing
6
+ * is ever written back to that client. Either way credentials end up in exactly
7
+ * one place, the plugin's own account store, and no token leaves this module in
8
+ * errors or display fields.
8
9
  *
9
10
  * @module dsh-llm-adapters/auth
10
11
  */
@@ -26,6 +27,8 @@ export interface AuthStatus {
26
27
  possiblyExpired?: boolean;
27
28
  account?: LocalAccount;
28
29
  error?: string;
30
+ /** Trae only: where the token came from. */
31
+ loginSource?: 'web' | 'client';
29
32
  }
30
33
  export declare class LoomyAuth {
31
34
  private readonly store;
@@ -58,12 +61,13 @@ export declare class RaccoonAuth {
58
61
  logout(): Promise<void>;
59
62
  }
60
63
  /**
61
- * Trae's web login, one per region.
64
+ * Trae's login, one per region.
62
65
  *
63
- * The `Cloud-IDE-JWT` the website issues lives about eight hours, but the
64
- * website session behind it lives about fourteen days. The cookies captured at
65
- * sign-in are replayed against `GetUserToken` to renew the short token silently;
66
- * only when the session itself dies does the user have to sign in again.
66
+ * A web login stores the `Cloud-IDE-JWT` from the website plus the cookies
67
+ * behind it, so the short token can be renewed silently until the website
68
+ * session dies. A client login stores the token the desktop client already
69
+ * uses; that token has no website session, and the client itself refreshes it.
70
+ * Only the client source carries the device id the check-in route accepts.
67
71
  */
68
72
  export declare class TraeAuth {
69
73
  private readonly store;
@@ -77,8 +81,10 @@ export declare class TraeAuth {
77
81
  /**
78
82
  * The bearer token, or an actionable error when there is none.
79
83
  *
80
- * A token near its expiry is renewed first; `force` means the gateway already
81
- * rejected it, so the renewal is attempted even if the clock disagrees.
84
+ * A web token near its expiry is renewed first; `force` means the gateway
85
+ * already rejected it, so the renewal is attempted even if the clock
86
+ * disagrees. A client token is returned until its own expiry and then asks
87
+ * for a fresh import, because only the client can refresh it.
82
88
  */
83
89
  accessToken(force?: boolean): Promise<string>;
84
90
  /** Renew the eight-hour JWT with the stored website cookies. */
@@ -59,10 +59,12 @@ export { OpenRouterCatalog, buildOpenRouterIndex, parseOpenRouterModels, readOpe
59
59
  export type { OpenRouterIndex, OpenRouterLookup, OpenRouterModel, OpenRouterStatus, OpenRouterSyncResult, } from './openrouter.ts';
60
60
  export { LoomyProvider, LOOMY_BASE, LOOMY_VERSION, loomyGatewaySessionValid } from './loomy.ts';
61
61
  export { RaccoonProvider, RACCOON_BASE, RACCOON_CLIENT_VERSION, raccoonAccessTokenValid, raccoonRefreshCandidate, } from './raccoon.ts';
62
- export { TraeProvider, TRAE_CHAT_FUNCTIONS, TRAE_DIRECTORY_FUNCTIONS, mergeTraeModels, normalizeEffort, openTraeStream, parseTraeGlobalUsage, parseTraeRemoteModels, parseTraeUsage, parseTraeWireModels, traeMessages, traeTools, } from './trae.ts';
62
+ export { TraeProvider, TRAE_CHAT_FUNCTIONS, TRAE_CHECKIN_REQ_SOURCE, TRAE_DIRECTORY_FUNCTIONS, mergeTraeModels, normalizeEffort, openTraeStream, parseTraeCheckin, parseTraeGlobalUsage, parseTraeRemoteModels, parseTraeUsage, parseTraeWireModels, traeMessages, traeTools, } from './trae.ts';
63
63
  export type { TraeCatalog, TraeModelMeta, TraeProviderOptions, TraeRemoteModel, TraeWireModel, } from './trae.ts';
64
64
  export { TRAE_APP_ID, TRAE_REGION_PROFILES, TRAE_SOLO_CHAT_PATH, TRAE_WEB_USER_AGENT } from './trae-endpoints.ts';
65
65
  export type { TraeRegionProfile } from './trae-endpoints.ts';
66
+ export { parseTraeClientStorage, readTraeClientLogin, traeClientDataDirs } from './trae-client.ts';
67
+ export type { TraeClientDetection, TraeClientLoginState, TraeClientReadOptions } from './trae-client.ts';
66
68
  export { TraeWebLogin, machineIdOf } from './trae-login.ts';
67
69
  export type { TraeLoginAccount, TraeWebLoginOptions } from './trae-login.ts';
68
70
  export { CodeArtsProvider, CODEARTS_DEFAULT_EFFORT, CODEARTS_EFFORTS, clampMaxTokens, codeartsEfforts, normalizeCodeArtsModelId, parseCodeArtsPackage, parseCodeArtsRoster, pickActiveCredit, priceFactorOfLabel, } from './codearts.ts';
@@ -79,7 +81,7 @@ export type { WireMessage } from './wire.ts';
79
81
  export { API_PREFIX, buildRows } from './api.ts';
80
82
  export { liveConfig, resolveConfig } from './config.ts';
81
83
  export type { Config as ConfigShape, LiveConfig, ParsedConfig, ResolvedConfig } from './config.ts';
82
- export type { CheckinStatus, ProviderStatus, QuotaPack, QuotaSnapshot } from './types.ts';
84
+ export type { CheckinStatus, ClientLoginStatus, ProviderStatus, QuotaPack, QuotaSnapshot } from './types.ts';
83
85
  export declare const name = "llm-adapters";
84
86
  export declare const inject: string[];
85
87
  export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The local Trae desktop client's sign-in state.
3
+ *
4
+ * Trae's check-in route insists on the device id its own client registered, so
5
+ * the website login cannot use it. When the client is installed and already
6
+ * signed in, the plugin can reuse that state instead:
7
+ *
8
+ * - the `Cloud-IDE-JWT` stored under `iCubeAuthInfo://icube.cloudide`;
9
+ * - the 16-digit device id encoded in the `iCubeAuthInfo://icube-dc:<id>` key;
10
+ * - `telemetry.machineId`, the value the client sends as `x-machine-id`.
11
+ *
12
+ * The stored value uses the client's own AES-CBC envelope: six magic bytes, a
13
+ * 32-byte key material, then the ciphertext. The AES key and IV are derived
14
+ * from that key material with SHA-512 and a fixed salt, and the plaintext is a
15
+ * SHA-512 digest followed by the JSON document. Nothing here writes to the
16
+ * client, and the token is never returned by the settings API.
17
+ *
18
+ * @module dsh-llm-adapters/trae-client
19
+ */
20
+ import type { TraeRegion } from './brand.ts';
21
+ import type { ClientLoginStatus } from './types.ts';
22
+ /** What the provider stores after importing a client login. */
23
+ export interface TraeClientLoginState {
24
+ region: TraeRegion;
25
+ token: string;
26
+ deviceId: string;
27
+ machineId: string;
28
+ userId?: string;
29
+ name?: string;
30
+ expiresAt?: number;
31
+ }
32
+ /** The settings-facing half (no token) plus the internal state when usable. */
33
+ export interface TraeClientDetection {
34
+ status: ClientLoginStatus;
35
+ state?: TraeClientLoginState;
36
+ }
37
+ export interface TraeClientReadOptions {
38
+ platform?: NodeJS.Platform;
39
+ env?: NodeJS.ProcessEnv;
40
+ home?: string;
41
+ }
42
+ /**
43
+ * The client's data directory, most specific first.
44
+ *
45
+ * `DSH_TRAE_CLIENT_DIR` is a portable/test override; the region-specific
46
+ * variables let one machine keep separate CN and international installs.
47
+ */
48
+ export declare function traeClientDataDirs(region: TraeRegion, options?: TraeClientReadOptions): string[];
49
+ /** Parse one `storage.json` document; exported for tests and diagnostics. */
50
+ export declare function parseTraeClientStorage(region: TraeRegion, storage: Record<string, unknown>): TraeClientDetection;
51
+ /** Read the currently selected region's client state from this machine. */
52
+ export declare function readTraeClientLogin(region: TraeRegion, options?: TraeClientReadOptions): TraeClientDetection;
@@ -33,6 +33,8 @@ export interface TraeRegionProfile {
33
33
  * the dollar allowance instead.
34
34
  */
35
35
  usageV2: boolean;
36
+ /** Daily check-in exists only on the mainland deployment. */
37
+ checkin: boolean;
36
38
  }
37
39
  export declare const TRAE_REGION_PROFILES: Record<TraeRegion, TraeRegionProfile>;
38
40
  /** The app id Trae's own clients send; not account-specific. */
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Trae sign-in through the provider's own website.
3
3
  *
4
- * The plugin does not read a desktop install: it opens a dedicated browser
4
+ * This flow does not read a desktop install: it opens a dedicated browser
5
5
  * profile, lets the user sign in on trae.cn / trae.ai, and then takes the
6
6
  * session the website itself created. Two things come out of that session:
7
7
  *
8
8
  * - the `Cloud-IDE-JWT` token, fetched with the page's own cookies through
9
9
  * `/cloudide/api/v3/common/GetUserToken` — the same call the site makes;
10
10
  * - the device fingerprint (`__msuuid__` from the page, or one generated in
11
- * the page), which Trae's check-in route requires and which therefore must
12
- * belong to this web session rather than to a machine install.
11
+ * the page), stored with the web login.
12
+ *
13
+ * The check-in route does not accept this web fingerprint, so check-in is
14
+ * enabled only for the separate client-login import in `trae-client.ts`.
13
15
  *
14
16
  * Cookies are captured over the DevTools protocol and kept in the plugin's own
15
17
  * account store so the short-lived JWT can be renewed later; the dedicated
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Trae provider: model directory, SOLO chat and quota.
2
+ * Trae provider: model directory, SOLO chat, quota and client check-in.
3
3
  *
4
4
  * Trae's website login yields the same `Cloud-IDE-JWT` its own clients use, so
5
- * the plugin talks to the same gateways they do. Two catalog sources are merged:
5
+ * the plugin talks to the same gateways they do. A second sign-in method copies
6
+ * that token from the local desktop client; only that method carries the device
7
+ * id the daily check-in route accepts. Two catalog sources are merged:
6
8
  *
7
9
  * - the Remote directory (`/api/remote/v1/models`) is the model skeleton —
8
10
  * display name, context window, multiplier, reasoning options, multimodal;
@@ -29,7 +31,8 @@ import { ModelCatalog } from './catalog.ts';
29
31
  import type { ProviderModel } from './catalog.ts';
30
32
  import type { LoginStatus } from './login-page.ts';
31
33
  import type { OpenRouterIndex, OpenRouterLookup } from './openrouter.ts';
32
- import type { QuotaSnapshot } from './types.ts';
34
+ import type { TraeClientDetection } from './trae-client.ts';
35
+ import type { CheckinStatus, ClientLoginStatus, QuotaSnapshot } from './types.ts';
33
36
  import type { ReadImage, WireMessage } from './wire.ts';
34
37
  /** Chat functions, in the order Trae itself prefers them. */
35
38
  export declare const TRAE_CHAT_FUNCTIONS: readonly string[];
@@ -103,6 +106,8 @@ export interface TraeProviderOptions {
103
106
  logger?: (message: string) => void;
104
107
  /** The shared OpenRouter snapshot; a private one is created when omitted. */
105
108
  openRouter?: OpenRouterLookup;
109
+ /** Overridable in tests; defaults to reading this machine's Trae client. */
110
+ clientLogin?: () => TraeClientDetection;
106
111
  }
107
112
  /**
108
113
  * The persistent browser profile used for one region.
@@ -154,6 +159,12 @@ export declare function parseTraeUsage(payload: unknown): QuotaSnapshot;
154
159
  * dollars and must not be printed as credits.
155
160
  */
156
161
  export declare function parseTraeGlobalUsage(payload: unknown): QuotaSnapshot;
162
+ /**
163
+ * The check-in body's `req_source`: 1 is the IDE, 2 is Trae Work. The desktop
164
+ * client sends 1; the claim route rejects the plugin's former empty body.
165
+ */
166
+ export declare const TRAE_CHECKIN_REQ_SOURCE = 1;
167
+ export declare function parseTraeCheckin(payload: unknown): CheckinStatus;
157
168
  export declare class TraeProvider {
158
169
  private readonly options;
159
170
  private catalogInstance;
@@ -185,6 +196,7 @@ export declare class TraeProvider {
185
196
  possiblyExpired?: boolean;
186
197
  account?: import("./auth.ts").LocalAccount;
187
198
  error?: string;
199
+ loginSource?: "web" | "client";
188
200
  }>;
189
201
  signedIn(): Promise<boolean>;
190
202
  models(fresh?: boolean): Promise<readonly ProviderModel[]>;
@@ -199,6 +211,20 @@ export declare class TraeProvider {
199
211
  private fetchWire;
200
212
  private fetchCatalog;
201
213
  quota(fresh?: boolean): Promise<QuotaSnapshot>;
214
+ /**
215
+ * The second sign-in method: the desktop client's current state.
216
+ *
217
+ * Read-only; the settings page only receives the non-secret status, never
218
+ * the token.
219
+ */
220
+ localClientLogin(): Promise<ClientLoginStatus>;
221
+ private readClientLogin;
222
+ /** Import the desktop client's token, device id and machine id. */
223
+ importClientLogin(): Promise<void>;
224
+ private checkinContext;
225
+ checkinStatus(): Promise<CheckinStatus>;
226
+ /** Claim today's reward. The only state-changing call in this provider. */
227
+ claimCheckin(): Promise<CheckinStatus>;
202
228
  stream(options: GenerateOptions, model: string, readImage: ReadImage | undefined, onInvalidate: () => void): AsyncGenerator<StreamChunk>;
203
229
  startBrowserLogin(region?: TraeRegion): Promise<LoginStatus>;
204
230
  browserLoginStatus(): LoginStatus;
@@ -53,6 +53,22 @@ export interface CheckinStatus {
53
53
  /** A page the user can open when the provider has no claimable API. */
54
54
  url?: string;
55
55
  }
56
+ /**
57
+ * The local Trae client's state, as shown on the settings page.
58
+ *
59
+ * Never carries the token: only whether the client was found, whether a usable
60
+ * login was read, and the non-secret account fields.
61
+ */
62
+ export interface ClientLoginStatus {
63
+ /** A Trae client data directory exists on this machine. */
64
+ available: boolean;
65
+ /** The stored login is present, unexpired and usable. */
66
+ signedIn: boolean;
67
+ name?: string;
68
+ userId?: string;
69
+ expiresAt?: number;
70
+ error?: string;
71
+ }
56
72
  export interface ProviderStatus extends AuthStatus {
57
73
  brand: BrandId;
58
74
  /** Trae only: the region this reading belongs to. */
@@ -60,7 +76,9 @@ export interface ProviderStatus extends AuthStatus {
60
76
  /** Present only after a successful quota read. */
61
77
  quota?: QuotaSnapshot;
62
78
  quotaError?: string;
63
- /** Trae CN only. */
79
+ /** Trae only: the second sign-in method, when the desktop client exists. */
80
+ clientLogin?: ClientLoginStatus;
81
+ /** Trae CN only, and only for a login imported from the client. */
64
82
  checkin?: CheckinStatus;
65
83
  }
66
84
  export type { LocalAccount };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-llm-adapters",
3
3
  "displayName": "Local LLM Adapters",
4
- "description": "DeepSeek Harness adapters for Loomy, SenseTime Raccoon and Trae. Sign in through the provider’s own web page (WeChat QR for Loomy and Raccoon, a dedicated browser window for Trae), then use the account’s models with quota, thinking levels and per-model settings pages.",
5
- "version": "1.2.0",
4
+ "description": "DeepSeek Harness adapters for Loomy, SenseTime Raccoon and Trae. Sign in through the provider’s own web page (WeChat QR for Loomy and Raccoon, a dedicated browser window for Trae), or reuse a signed-in local Trae client for the mainland check-in, then use the account’s models with quota, thinking levels and per-model settings pages.",
5
+ "version": "1.2.1",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "author": "k1-h2",
@@ -19,6 +19,7 @@
19
19
  "qr-login",
20
20
  "wechat-login",
21
21
  "web-login",
22
+ "client-login",
22
23
  "openai-compatible",
23
24
  "model-provider",
24
25
  "trae",
@@ -39,12 +39,12 @@ export interface RaccoonAccountRecord {
39
39
  }
40
40
 
41
41
  /**
42
- * One Trae web login.
42
+ * One Trae login.
43
43
  *
44
- * `accessToken` is the `Cloud-IDE-JWT` the website itself uses. `deviceId` and
45
- * `machineId` are the fingerprints captured from that web session — this
46
- * plugin never reads a desktop install — and are replayed on chat, quota and
47
- * check-in requests.
44
+ * `accessToken` is the `Cloud-IDE-JWT` Trae's own clients use. `deviceId` and
45
+ * `machineId` are replayed on chat, quota and check-in requests. A web login
46
+ * captures them from its browser session; a `client` login copies the desktop
47
+ * client's own values, which is the only source the check-in route accepts.
48
48
  */
49
49
  export interface TraeAccountRecord {
50
50
  accessToken: string
@@ -54,6 +54,8 @@ export interface TraeAccountRecord {
54
54
  userId?: string
55
55
  name?: string
56
56
  loginAt: number
57
+ /** Where the token came from; absent means the plugin's browser login. */
58
+ source?: 'web' | 'client'
57
59
  /** The `Cloud-IDE-JWT` expiry (about 8 hours); absent when the JWT carries none. */
58
60
  expiresAt?: number
59
61
  /**
@@ -138,9 +140,11 @@ function traeRecord(value: unknown, region: TraeRegion): TraeAccountRecord | und
138
140
  const accessToken = text(raw?.accessToken)
139
141
  if (raw === undefined || accessToken === undefined) return undefined
140
142
  const deviceId = text(raw.deviceId)
143
+ const source = raw.source === 'web' || raw.source === 'client' ? raw.source : undefined
141
144
  return {
142
145
  accessToken,
143
146
  region,
147
+ ...source === undefined ? {} : { source },
144
148
  deviceId: deviceId ?? `web-${randomBytes(8).toString('hex')}`,
145
149
  machineId: text(raw.machineId) ?? `web-${randomBytes(16).toString('hex')}`,
146
150
  loginAt: number(raw.loginAt) ?? Date.now(),
package/src/adapter.ts CHANGED
@@ -22,7 +22,7 @@ import { LoomyProvider } from './loomy.ts'
22
22
  import { RaccoonProvider } from './raccoon.ts'
23
23
  import { TraeProvider } from './trae.ts'
24
24
  import type { AuthStatus } from './auth.ts'
25
- import type { CheckinStatus, ProviderStatus, QuotaSnapshot } from './types.ts'
25
+ import type { CheckinStatus, ClientLoginStatus, ProviderStatus, QuotaSnapshot } from './types.ts'
26
26
  import type { ReadImage } from './wire.ts'
27
27
 
28
28
  export interface AdapterOptions {
@@ -41,7 +41,7 @@ export interface AdapterOptions {
41
41
  *
42
42
  * Declared as an interface rather than a union so a new provider only has to
43
43
  * implement the behaviour, not edit the adapter's type list. The optional
44
- * check-in methods exist only on Trae.
44
+ * client-login and check-in methods exist only on Trae.
45
45
  */
46
46
  export interface ProviderClient {
47
47
  status(): Promise<AuthStatus>
@@ -64,6 +64,10 @@ export interface ProviderClient {
64
64
  invalidateModels(): void
65
65
  invalidateQuota(): void
66
66
  close(): void
67
+ /** Trae only: the local desktop client's current sign-in state. */
68
+ localClientLogin?(): Promise<ClientLoginStatus>
69
+ /** Trae only: copy that state into the plugin's own account store. */
70
+ importClientLogin?(): Promise<void>
67
71
  checkinStatus?(): Promise<CheckinStatus>
68
72
  claimCheckin?(): Promise<CheckinStatus>
69
73
  }
@@ -154,6 +158,17 @@ export abstract class LocalAdapter extends LlmAdapter {
154
158
  return result
155
159
  }
156
160
 
161
+ /** Import a local client login and return the refreshed account reading. */
162
+ async importClientLogin(): Promise<ProviderStatus> {
163
+ if (this.client.importClientLogin === undefined) {
164
+ throw new Error('该供应商不支持复用本机客户端登录')
165
+ }
166
+ await this.client.importClientLogin()
167
+ this.invalidateCatalog()
168
+ this.invalidateUsage()
169
+ return this.usageInfo(true)
170
+ }
171
+
157
172
  invalidateCatalog(): void {
158
173
  this.client.invalidateModels()
159
174
  }
@@ -165,7 +180,20 @@ export abstract class LocalAdapter extends LlmAdapter {
165
180
  /** Login state plus quota, shaped for the settings page. */
166
181
  async usageInfo(fresh = false): Promise<ProviderStatus> {
167
182
  const auth = await this.client.status()
168
- const base: ProviderStatus = { brand: this.brand, ...auth }
183
+ // Detection is part of the reading even while signed out: that is what
184
+ // offers the second sign-in method. Only Trae implements it.
185
+ const clientLogin = this.client.localClientLogin === undefined
186
+ ? undefined
187
+ : await this.client.localClientLogin().catch(error => ({
188
+ available: false,
189
+ signedIn: false,
190
+ error: error instanceof Error ? error.message : String(error),
191
+ }))
192
+ const base: ProviderStatus = {
193
+ brand: this.brand,
194
+ ...auth,
195
+ ...clientLogin === undefined ? {} : { clientLogin },
196
+ }
169
197
  // Trae's check-in state is part of the account reading; the other two
170
198
  // providers do not implement it, so this costs them nothing.
171
199
  const checkin = this.client.checkinStatus === undefined
package/src/api.ts CHANGED
@@ -184,8 +184,20 @@ async function handleAccount(req: IncomingMessage, res: ServerResponse, adapters
184
184
  return
185
185
  }
186
186
 
187
- // Trae and CodeArts have a daily reward; the other adapters reject the action,
188
- // so the page can keep one button that simply reports the refusal.
187
+ // Trae's second sign-in method: the desktop client must already be logged in.
188
+ // The response is an ordinary account reading, so it never carries the token.
189
+ if (action === 'client-login') {
190
+ try {
191
+ send(res, 200, await adapter.importClientLogin())
192
+ } catch (error) {
193
+ send(res, 400, { error: error instanceof Error ? error.message : String(error) })
194
+ }
195
+ return
196
+ }
197
+
198
+ // Trae (client login only) and CodeArts have a daily reward; the other
199
+ // adapters reject the action, so the page can keep one button that simply
200
+ // reports the refusal.
189
201
  if (action === 'checkin') {
190
202
  try {
191
203
  send(res, 200, { brand, checkin: await adapter.claimCheckin() })
@@ -195,7 +207,7 @@ async function handleAccount(req: IncomingMessage, res: ServerResponse, adapters
195
207
  return
196
208
  }
197
209
 
198
- send(res, 400, { error: 'action 只接受 browser、browser-cancel、checkin 或 logout' })
210
+ send(res, 400, { error: 'action 只接受 browser、browser-cancel、client-login、checkin 或 logout' })
199
211
  }
200
212
 
201
213
  export async function handleApi(
package/src/auth.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Credential state for every provider.
3
3
  *
4
- * Sign-in happens in the provider's own web page, so credentials exist in
5
- * exactly one place: the plugin's own account store. No desktop client file is
6
- * discovered, read, or written, and no token leaves this module in errors or
7
- * display fields.
4
+ * Sign-in happens in the provider's own web page. Trae additionally offers an
5
+ * explicit, read-only import of the local desktop client's login state; nothing
6
+ * is ever written back to that client. Either way credentials end up in exactly
7
+ * one place, the plugin's own account store, and no token leaves this module in
8
+ * errors or display fields.
8
9
  *
9
10
  * @module dsh-llm-adapters/auth
10
11
  */
@@ -34,6 +35,8 @@ export interface AuthStatus {
34
35
  possiblyExpired?: boolean
35
36
  account?: LocalAccount
36
37
  error?: string
38
+ /** Trae only: where the token came from. */
39
+ loginSource?: 'web' | 'client'
37
40
  }
38
41
 
39
42
  /** Expiry margin for Raccoon's JWT, so a call does not start on a dead token. */
@@ -261,12 +264,13 @@ function mergeSetCookies(current: string, headers: readonly string[]): string {
261
264
  }
262
265
 
263
266
  /**
264
- * Trae's web login, one per region.
267
+ * Trae's login, one per region.
265
268
  *
266
- * The `Cloud-IDE-JWT` the website issues lives about eight hours, but the
267
- * website session behind it lives about fourteen days. The cookies captured at
268
- * sign-in are replayed against `GetUserToken` to renew the short token silently;
269
- * only when the session itself dies does the user have to sign in again.
269
+ * A web login stores the `Cloud-IDE-JWT` from the website plus the cookies
270
+ * behind it, so the short token can be renewed silently until the website
271
+ * session dies. A client login stores the token the desktop client already
272
+ * uses; that token has no website session, and the client itself refreshes it.
273
+ * Only the client source carries the device id the check-in route accepts.
270
274
  */
271
275
  export class TraeAuth {
272
276
  private readonly inflight = new Map<TraeRegion, Promise<string>>()
@@ -286,20 +290,41 @@ export class TraeAuth {
286
290
  const region = this.region()
287
291
  const stored = await this.store.trae(region)
288
292
  if (stored === undefined) {
289
- return { signedIn: false, error: `${traeLabel(region)}尚未登录。请在设置页点「打开浏览器登录」。` }
293
+ return {
294
+ signedIn: false,
295
+ error: `${traeLabel(region)}尚未登录。可以点「打开浏览器登录」,或复用本机已登录的 Trae 客户端。`,
296
+ }
290
297
  }
298
+ const source = stored.source ?? 'web'
291
299
  const now = Date.now()
292
300
  const { token, session } = traeExpiries(stored)
293
301
  const tokenExpired = token !== undefined && token <= now + TRAE_EXPIRY_MARGIN_MS
302
+ const account = traeAccount(stored)
303
+ if (source === 'client') {
304
+ // The client owns the refresh loop; when its token dies the user signs in
305
+ // there again and imports the new state.
306
+ const signedIn = !tokenExpired
307
+ return {
308
+ signedIn,
309
+ loginSource: 'client',
310
+ loginAt: stored.loginAt,
311
+ ...token === undefined ? {} : { expiresAt: token },
312
+ ...!signedIn ? { possiblyExpired: true } : {},
313
+ ...Object.keys(account).length === 0 ? {} : { account },
314
+ ...!signedIn
315
+ ? { error: `${traeLabel(region)}客户端登录令牌已过期,请先在 Trae 客户端重新登录,再点「复用客户端登录态」。` }
316
+ : {},
317
+ }
318
+ }
294
319
  const sessionExpired = session !== undefined && session <= now + TRAE_EXPIRY_MARGIN_MS
295
320
  const refreshable = stored.cookies !== undefined && !sessionExpired
296
321
  const signedIn = !tokenExpired || refreshable
297
- const account = traeAccount(stored)
298
322
  // With a renewable session the user cares about the website-session
299
323
  // deadline, not the eight-hour JWT the plugin replaces on its own.
300
324
  const expiresAt = refreshable ? session ?? token : token
301
325
  return {
302
326
  signedIn,
327
+ loginSource: 'web',
303
328
  loginAt: stored.loginAt,
304
329
  ...expiresAt === undefined ? {} : { expiresAt },
305
330
  ...!signedIn ? { possiblyExpired: true } : {},
@@ -315,17 +340,23 @@ export class TraeAuth {
315
340
  /**
316
341
  * The bearer token, or an actionable error when there is none.
317
342
  *
318
- * A token near its expiry is renewed first; `force` means the gateway already
319
- * rejected it, so the renewal is attempted even if the clock disagrees.
343
+ * A web token near its expiry is renewed first; `force` means the gateway
344
+ * already rejected it, so the renewal is attempted even if the clock
345
+ * disagrees. A client token is returned until its own expiry and then asks
346
+ * for a fresh import, because only the client can refresh it.
320
347
  */
321
348
  async accessToken(force = false): Promise<string> {
322
349
  const region = this.region()
323
350
  const stored = await this.store.trae(region)
324
351
  if (stored === undefined) {
325
- throw new Error(`${traeLabel(region)}尚未登录。请在设置页点「打开浏览器登录」。`)
352
+ throw new Error(`${traeLabel(region)}尚未登录。可以点「打开浏览器登录」,或复用本机已登录的 Trae 客户端。`)
326
353
  }
327
354
  const { token, session } = traeExpiries(stored)
328
355
  const expired = token !== undefined && token <= Date.now() + TRAE_EXPIRY_MARGIN_MS
356
+ if (stored.source === 'client') {
357
+ if (!force && !expired) return stored.accessToken
358
+ throw new Error(`${traeLabel(region)}客户端登录令牌已${force ? '失效' : '过期'},请先在 Trae 客户端重新登录,再点「复用客户端登录态」。`)
359
+ }
329
360
  if (!force && !expired) return stored.accessToken
330
361
  if (stored.cookies === undefined) {
331
362
  throw new Error(`${traeLabel(region)}登录已${force ? '失效' : '过期'},请在设置页重新登录。`)
@@ -342,7 +373,7 @@ export class TraeAuth {
342
373
  if (existing !== undefined) return existing
343
374
  const task = (async () => {
344
375
  const stored = await this.store.trae(region)
345
- if (stored === undefined || stored.cookies === undefined) {
376
+ if (stored === undefined || stored.source === 'client' || stored.cookies === undefined) {
346
377
  throw new Error(`${traeLabel(region)}登录已失效,请在设置页重新登录。`)
347
378
  }
348
379
  const profile = TRAE_REGION_PROFILES[region]
@@ -408,7 +439,7 @@ export class TraeAuth {
408
439
  const region = this.region()
409
440
  const stored = await this.store.trae(region)
410
441
  if (stored === undefined) {
411
- throw new Error(`${traeLabel(region)}尚未登录。请在设置页点「打开浏览器登录」。`)
442
+ throw new Error(`${traeLabel(region)}尚未登录。可以点「打开浏览器登录」,或复用本机已登录的 Trae 客户端。`)
412
443
  }
413
444
  return {
414
445
  deviceId: stored.deviceId,