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.
- package/DEVELOPMENT.md +26 -9
- package/FAQ.md +8 -2
- package/README.md +30 -17
- package/client.js +103 -14
- package/lib/index.js +553 -98
- package/lib/types/account-store.d.ts +7 -5
- package/lib/types/adapter.d.ts +8 -2
- package/lib/types/auth.d.ts +17 -11
- package/lib/types/index.d.ts +4 -2
- package/lib/types/trae-client.d.ts +52 -0
- package/lib/types/trae-endpoints.d.ts +2 -0
- package/lib/types/trae-login.d.ts +5 -3
- package/lib/types/trae.d.ts +29 -3
- package/lib/types/types.d.ts +19 -1
- package/package.json +3 -2
- package/src/account-store.ts +9 -5
- package/src/adapter.ts +31 -3
- package/src/api.ts +15 -3
- package/src/auth.ts +47 -16
- package/src/index.ts +5 -3
- package/src/trae-client.ts +312 -0
- package/src/trae-endpoints.ts +4 -0
- package/src/trae-login.ts +5 -3
- package/src/trae.ts +191 -3
- package/src/types.ts +20 -1
|
@@ -30,12 +30,12 @@ export interface RaccoonAccountRecord {
|
|
|
30
30
|
loginAt: number;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* One Trae
|
|
33
|
+
* One Trae login.
|
|
34
34
|
*
|
|
35
|
-
* `accessToken` is the `Cloud-IDE-JWT`
|
|
36
|
-
* `machineId` are
|
|
37
|
-
*
|
|
38
|
-
* check-in
|
|
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
|
/**
|
package/lib/types/adapter.d.ts
CHANGED
|
@@ -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. */
|
package/lib/types/auth.d.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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
64
|
+
* Trae's login, one per region.
|
|
62
65
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
|
81
|
-
* rejected it, so the renewal is attempted even if the clock
|
|
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. */
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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),
|
|
12
|
-
*
|
|
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
|
package/lib/types/trae.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Trae provider: model directory, SOLO chat and
|
|
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.
|
|
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 {
|
|
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;
|
package/lib/types/types.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|
package/src/account-store.ts
CHANGED
|
@@ -39,12 +39,12 @@ export interface RaccoonAccountRecord {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* One Trae
|
|
42
|
+
* One Trae login.
|
|
43
43
|
*
|
|
44
|
-
* `accessToken` is the `Cloud-IDE-JWT`
|
|
45
|
-
* `machineId` are
|
|
46
|
-
*
|
|
47
|
-
* check-in
|
|
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
|
-
|
|
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
|
|
188
|
-
//
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
267
|
+
* Trae's login, one per region.
|
|
265
268
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
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 {
|
|
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
|
|
319
|
-
* rejected it, so the renewal is attempted even if the clock
|
|
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,
|