dsh-llm-adapters 1.2.0 → 1.2.2
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 +92 -24
- package/FAQ.md +47 -10
- package/README.md +39 -25
- package/client.js +1802 -1733
- package/lib/index.js +733 -302
- package/lib/types/account-store.d.ts +17 -8
- package/lib/types/adapter.d.ts +8 -2
- package/lib/types/auth.d.ts +24 -11
- package/lib/types/codearts-oauth.d.ts +19 -0
- package/lib/types/codearts.d.ts +1 -5
- package/lib/types/config.d.ts +0 -5
- package/lib/types/index.d.ts +5 -6
- package/lib/types/raccoon.d.ts +79 -6
- 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 +20 -8
- package/src/adapter.ts +31 -3
- package/src/api.ts +15 -3
- package/src/auth.ts +62 -17
- package/src/codearts-oauth.ts +36 -5
- package/src/codearts.ts +24 -29
- package/src/config.ts +258 -264
- package/src/index.ts +6 -29
- package/src/raccoon.ts +163 -50
- 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
- package/lib/types/codearts-window.d.ts +0 -65
- package/src/codearts-window.ts +0 -136
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path, { dirname, join } from "node:path";
|
|
2
2
|
import { LlmAdapter, attributionHeaders, offloadedImageText, requestImageHandleText } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { existsSync, mkdirSync, mkdtempSync, promises, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { createHash, createHmac, createPrivateKey, generateKeyPairSync, randomBytes, randomUUID, sign } from "node:crypto";
|
|
4
|
+
import { createDecipheriv, createHash, createHmac, createPrivateKey, generateKeyPairSync, randomBytes, randomUUID, sign } from "node:crypto";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import { isVolatile } from "@deepseek-ai/cosmokit";
|
|
7
7
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -485,6 +485,14 @@ var LocalAdapter = class extends LlmAdapter {
|
|
|
485
485
|
this.invalidateUsage();
|
|
486
486
|
return result;
|
|
487
487
|
}
|
|
488
|
+
/** Import a local client login and return the refreshed account reading. */
|
|
489
|
+
async importClientLogin() {
|
|
490
|
+
if (this.client.importClientLogin === void 0) throw new Error("该供应商不支持复用本机客户端登录");
|
|
491
|
+
await this.client.importClientLogin();
|
|
492
|
+
this.invalidateCatalog();
|
|
493
|
+
this.invalidateUsage();
|
|
494
|
+
return this.usageInfo(true);
|
|
495
|
+
}
|
|
488
496
|
invalidateCatalog() {
|
|
489
497
|
this.client.invalidateModels();
|
|
490
498
|
}
|
|
@@ -494,9 +502,15 @@ var LocalAdapter = class extends LlmAdapter {
|
|
|
494
502
|
/** Login state plus quota, shaped for the settings page. */
|
|
495
503
|
async usageInfo(fresh = false) {
|
|
496
504
|
const auth = await this.client.status();
|
|
505
|
+
const clientLogin = this.client.localClientLogin === void 0 ? void 0 : await this.client.localClientLogin().catch((error) => ({
|
|
506
|
+
available: false,
|
|
507
|
+
signedIn: false,
|
|
508
|
+
error: error instanceof Error ? error.message : String(error)
|
|
509
|
+
}));
|
|
497
510
|
const base = {
|
|
498
511
|
brand: this.brand,
|
|
499
|
-
...auth
|
|
512
|
+
...auth,
|
|
513
|
+
...clientLogin === void 0 ? {} : { clientLogin }
|
|
500
514
|
};
|
|
501
515
|
const checkin = this.client.checkinStatus === void 0 ? void 0 : await this.client.checkinStatus().catch(() => void 0);
|
|
502
516
|
const withCheckin = checkin === void 0 ? base : {
|
|
@@ -720,45 +734,48 @@ function defaultRoot() {
|
|
|
720
734
|
const base = dshHome !== void 0 && dshHome !== "" ? dshHome : path.join(homedir(), ".dsh");
|
|
721
735
|
return path.join(base, "llm-adapters");
|
|
722
736
|
}
|
|
723
|
-
function record$
|
|
737
|
+
function record$5(value) {
|
|
724
738
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
725
739
|
}
|
|
726
|
-
function text$
|
|
740
|
+
function text$5(value) {
|
|
727
741
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
728
742
|
}
|
|
729
743
|
function number(value) {
|
|
730
744
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
731
745
|
}
|
|
732
746
|
function traeRecord(value, region) {
|
|
733
|
-
const raw = record$
|
|
734
|
-
const accessToken = text$
|
|
747
|
+
const raw = record$5(value);
|
|
748
|
+
const accessToken = text$5(raw?.accessToken);
|
|
735
749
|
if (raw === void 0 || accessToken === void 0) return void 0;
|
|
750
|
+
const deviceId = text$5(raw.deviceId);
|
|
751
|
+
const source = raw.source === "web" || raw.source === "client" ? raw.source : void 0;
|
|
736
752
|
return {
|
|
737
753
|
accessToken,
|
|
738
754
|
region,
|
|
739
|
-
|
|
740
|
-
|
|
755
|
+
...source === void 0 ? {} : { source },
|
|
756
|
+
deviceId: deviceId ?? `web-${randomBytes(8).toString("hex")}`,
|
|
757
|
+
machineId: text$5(raw.machineId) ?? `web-${randomBytes(16).toString("hex")}`,
|
|
741
758
|
loginAt: number(raw.loginAt) ?? Date.now(),
|
|
742
|
-
...text$
|
|
743
|
-
...text$
|
|
759
|
+
...text$5(raw.userId) === void 0 ? {} : { userId: text$5(raw.userId) },
|
|
760
|
+
...text$5(raw.name) === void 0 ? {} : { name: text$5(raw.name) },
|
|
744
761
|
...number(raw.expiresAt) === void 0 ? {} : { expiresAt: number(raw.expiresAt) },
|
|
745
|
-
...text$
|
|
762
|
+
...text$5(raw.cookies) === void 0 ? {} : { cookies: text$5(raw.cookies) },
|
|
746
763
|
...number(raw.sessionExpiresAt) === void 0 ? {} : { sessionExpiresAt: number(raw.sessionExpiresAt) }
|
|
747
764
|
};
|
|
748
765
|
}
|
|
749
766
|
function codeartsRecord(value) {
|
|
750
|
-
const raw = record$
|
|
751
|
-
const accessKeyId = text$
|
|
752
|
-
const secretAccessKey = text$
|
|
753
|
-
const securityToken = text$
|
|
767
|
+
const raw = record$5(value);
|
|
768
|
+
const accessKeyId = text$5(raw?.accessKeyId);
|
|
769
|
+
const secretAccessKey = text$5(raw?.secretAccessKey);
|
|
770
|
+
const securityToken = text$5(raw?.securityToken);
|
|
754
771
|
if (raw === void 0 || accessKeyId === void 0 || secretAccessKey === void 0 || securityToken === void 0) return;
|
|
755
|
-
const jwk = record$
|
|
756
|
-
const key = jwk !== void 0 && text$
|
|
772
|
+
const jwk = record$5(raw.dpopPrivateJwk);
|
|
773
|
+
const key = jwk !== void 0 && text$5(jwk.x) !== void 0 && text$5(jwk.y) !== void 0 && text$5(jwk.d) !== void 0 ? {
|
|
757
774
|
kty: "EC",
|
|
758
775
|
crv: "P-256",
|
|
759
|
-
x: text$
|
|
760
|
-
y: text$
|
|
761
|
-
d: text$
|
|
776
|
+
x: text$5(jwk.x),
|
|
777
|
+
y: text$5(jwk.y),
|
|
778
|
+
d: text$5(jwk.d)
|
|
762
779
|
} : void 0;
|
|
763
780
|
const noBenefit = Array.isArray(raw.noBenefit) ? raw.noBenefit.filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()) : [];
|
|
764
781
|
return {
|
|
@@ -767,10 +784,11 @@ function codeartsRecord(value) {
|
|
|
767
784
|
securityToken,
|
|
768
785
|
loginAt: number(raw.loginAt) ?? Date.now(),
|
|
769
786
|
...number(raw.expiresAt) === void 0 ? {} : { expiresAt: number(raw.expiresAt) },
|
|
770
|
-
...text$
|
|
771
|
-
...text$
|
|
787
|
+
...text$5(raw.refreshToken) === void 0 ? {} : { refreshToken: text$5(raw.refreshToken) },
|
|
788
|
+
...text$5(raw.codeVerifier) === void 0 ? {} : { codeVerifier: text$5(raw.codeVerifier) },
|
|
772
789
|
...key === void 0 ? {} : { dpopPrivateJwk: key },
|
|
773
|
-
...text$
|
|
790
|
+
...text$5(raw.userName) === void 0 ? {} : { userName: text$5(raw.userName) },
|
|
791
|
+
...text$5(raw.otSessionId) === void 0 ? {} : { otSessionId: text$5(raw.otSessionId) },
|
|
774
792
|
...noBenefit.length === 0 ? {} : { noBenefit }
|
|
775
793
|
};
|
|
776
794
|
}
|
|
@@ -794,30 +812,30 @@ var AccountStore = class {
|
|
|
794
812
|
} catch {
|
|
795
813
|
return {};
|
|
796
814
|
}
|
|
797
|
-
const raw = record$
|
|
815
|
+
const raw = record$5(parsed);
|
|
798
816
|
if (raw === void 0) return {};
|
|
799
817
|
const out = {};
|
|
800
|
-
const loomy = record$
|
|
801
|
-
const loomySession = text$
|
|
818
|
+
const loomy = record$5(raw.loomy);
|
|
819
|
+
const loomySession = text$5(loomy?.session);
|
|
802
820
|
if (loomySession !== void 0) out.loomy = {
|
|
803
821
|
session: loomySession,
|
|
804
|
-
deviceId: text$
|
|
822
|
+
deviceId: text$5(loomy?.deviceId) ?? `web-${randomBytes(8).toString("hex")}`,
|
|
805
823
|
loginAt: number(loomy?.loginAt) ?? Date.now(),
|
|
806
824
|
expiresAt: number(loomy?.expiresAt) ?? Date.now(),
|
|
807
|
-
...text$
|
|
808
|
-
...text$
|
|
809
|
-
...text$
|
|
825
|
+
...text$5(loomy?.userId) === void 0 ? {} : { userId: text$5(loomy?.userId) },
|
|
826
|
+
...text$5(loomy?.phone) === void 0 ? {} : { phone: text$5(loomy?.phone) },
|
|
827
|
+
...text$5(loomy?.name) === void 0 ? {} : { name: text$5(loomy?.name) }
|
|
810
828
|
};
|
|
811
|
-
const raccoon = record$
|
|
812
|
-
const access = text$
|
|
829
|
+
const raccoon = record$5(raw.raccoon);
|
|
830
|
+
const access = text$5(raccoon?.accessToken);
|
|
813
831
|
if (access !== void 0) out.raccoon = {
|
|
814
832
|
accessToken: access,
|
|
815
833
|
loginAt: number(raccoon?.loginAt) ?? Date.now(),
|
|
816
|
-
...text$
|
|
817
|
-
...text$
|
|
818
|
-
...text$
|
|
834
|
+
...text$5(raccoon?.phone) === void 0 ? {} : { phone: text$5(raccoon?.phone) },
|
|
835
|
+
...text$5(raccoon?.name) === void 0 ? {} : { name: text$5(raccoon?.name) },
|
|
836
|
+
...text$5(raccoon?.ownerType) === void 0 ? {} : { ownerType: text$5(raccoon?.ownerType) }
|
|
819
837
|
};
|
|
820
|
-
const trae = record$
|
|
838
|
+
const trae = record$5(raw.trae);
|
|
821
839
|
if (trae !== void 0) {
|
|
822
840
|
const cn = traeRecord(trae.cn, "cn");
|
|
823
841
|
const global = traeRecord(trae.global, "global");
|
|
@@ -1051,6 +1069,14 @@ async function handleAccount(req, res, adapters) {
|
|
|
1051
1069
|
});
|
|
1052
1070
|
return;
|
|
1053
1071
|
}
|
|
1072
|
+
if (action === "client-login") {
|
|
1073
|
+
try {
|
|
1074
|
+
send(res, 200, await adapter.importClientLogin());
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
send(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
1077
|
+
}
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1054
1080
|
if (action === "checkin") {
|
|
1055
1081
|
try {
|
|
1056
1082
|
send(res, 200, {
|
|
@@ -1062,7 +1088,7 @@ async function handleAccount(req, res, adapters) {
|
|
|
1062
1088
|
}
|
|
1063
1089
|
return;
|
|
1064
1090
|
}
|
|
1065
|
-
send(res, 400, { error: "action 只接受 browser、browser-cancel、checkin 或 logout" });
|
|
1091
|
+
send(res, 400, { error: "action 只接受 browser、browser-cancel、client-login、checkin 或 logout" });
|
|
1066
1092
|
}
|
|
1067
1093
|
async function handleApi(req, res, adapters, openRouter) {
|
|
1068
1094
|
try {
|
|
@@ -1400,6 +1426,18 @@ function readUserProfile(token) {
|
|
|
1400
1426
|
...domainId === void 0 ? {} : { domainId }
|
|
1401
1427
|
};
|
|
1402
1428
|
}
|
|
1429
|
+
/**
|
|
1430
|
+
* The session id this account should chat under, derived from who the account is.
|
|
1431
|
+
*
|
|
1432
|
+
* 32 lowercase hex, the shape the IDE's own kernel sends. Nothing but stability
|
|
1433
|
+
* matters here — the gateway uses the value as a key, not as an identity — so it
|
|
1434
|
+
* is hashed from the principal id rather than kept in a file.
|
|
1435
|
+
*/
|
|
1436
|
+
function otSessionIdOf(profile) {
|
|
1437
|
+
const seed = profile?.userId ?? profile?.userName;
|
|
1438
|
+
if (seed === void 0) return void 0;
|
|
1439
|
+
return createHash("sha256").update(`codearts-ot-session:${seed}`).digest("hex").slice(0, 32);
|
|
1440
|
+
}
|
|
1403
1441
|
/** The portal URL the browser opens; the callback is on loopback. */
|
|
1404
1442
|
function buildAuthorizeUrl(port, pkce, ticketId) {
|
|
1405
1443
|
return `${CODEARTS_PORTAL_AUTHORIZE}?theme=2&locale=zh-cn&uri_scheme=${CODEARTS_CLIENT_ID}&client_id=${CODEARTS_CLIENT_ID}&port=${port}&code_challenge=${pkce.codeChallenge}&code_challenge_method=SHA-256&ticket_id=${ticketId}&plugin-name=snap_AIIDE&plugin-version=5.2.0`;
|
|
@@ -1450,25 +1488,30 @@ async function exchangeRefreshToken(refreshToken, codeVerifier, keyPair, fetcher
|
|
|
1450
1488
|
grant_type: "refresh_token",
|
|
1451
1489
|
refresh_token: refreshToken
|
|
1452
1490
|
}, keyPair, fetcher), codeVerifier, keyPair);
|
|
1453
|
-
const
|
|
1491
|
+
const previous = readUserProfile(refreshToken);
|
|
1492
|
+
const userName = credential.userName ?? previous?.userName;
|
|
1493
|
+
const otSessionId = credential.otSessionId ?? otSessionIdOf(previous);
|
|
1454
1494
|
return {
|
|
1455
1495
|
...credential,
|
|
1456
1496
|
refreshToken: credential.refreshToken ?? refreshToken,
|
|
1457
|
-
...userName === void 0 ? {} : { userName }
|
|
1497
|
+
...userName === void 0 ? {} : { userName },
|
|
1498
|
+
...otSessionId === void 0 ? {} : { otSessionId }
|
|
1458
1499
|
};
|
|
1459
1500
|
}
|
|
1460
1501
|
function credentialFrom(token, codeVerifier, keyPair) {
|
|
1461
1502
|
const credentials = token.credentials ?? {};
|
|
1462
1503
|
const expiration = credentials.expiration;
|
|
1463
1504
|
const expiresAt = expiration === void 0 ? void 0 : Date.parse(expiration);
|
|
1464
|
-
const
|
|
1505
|
+
const profile = readUserProfile(token.refresh_token);
|
|
1506
|
+
const otSessionId = otSessionIdOf(profile);
|
|
1465
1507
|
return {
|
|
1466
1508
|
accessKeyId: credentials.access_key_id ?? "",
|
|
1467
1509
|
secretAccessKey: credentials.secret_access_key ?? "",
|
|
1468
1510
|
securityToken: credentials.security_token ?? "",
|
|
1469
1511
|
...expiresAt === void 0 || Number.isNaN(expiresAt) ? {} : { expiresAt },
|
|
1470
1512
|
...token.refresh_token === void 0 ? {} : { refreshToken: token.refresh_token },
|
|
1471
|
-
...userName === void 0 ? {} : { userName },
|
|
1513
|
+
...profile?.userName === void 0 ? {} : { userName: profile.userName },
|
|
1514
|
+
...otSessionId === void 0 ? {} : { otSessionId },
|
|
1472
1515
|
codeVerifier,
|
|
1473
1516
|
dpopPrivateJwk: keyPair.privateKeyJwk
|
|
1474
1517
|
};
|
|
@@ -2080,7 +2123,8 @@ const TRAE_REGION_PROFILES = {
|
|
|
2080
2123
|
payBase: "https://api.trae.cn",
|
|
2081
2124
|
timezone: "Asia/Shanghai",
|
|
2082
2125
|
language: "zh-cn",
|
|
2083
|
-
usageV2: true
|
|
2126
|
+
usageV2: true,
|
|
2127
|
+
checkin: true
|
|
2084
2128
|
},
|
|
2085
2129
|
global: {
|
|
2086
2130
|
id: "global",
|
|
@@ -2092,7 +2136,8 @@ const TRAE_REGION_PROFILES = {
|
|
|
2092
2136
|
payBase: "https://growsg-normal.trae.ai",
|
|
2093
2137
|
timezone: "Asia/Singapore",
|
|
2094
2138
|
language: "en",
|
|
2095
|
-
usageV2: false
|
|
2139
|
+
usageV2: false,
|
|
2140
|
+
checkin: false
|
|
2096
2141
|
}
|
|
2097
2142
|
};
|
|
2098
2143
|
/** The app id Trae's own clients send; not account-specific. */
|
|
@@ -2197,7 +2242,8 @@ function decodeJwtFacts(token) {
|
|
|
2197
2242
|
...typeof record.name === "string" ? { name: record.name } : {},
|
|
2198
2243
|
...typeof record.phone === "string" ? { phone: record.phone } : {},
|
|
2199
2244
|
...typeof record.owner_type === "string" ? { ownerType: record.owner_type } : {},
|
|
2200
|
-
...typeof record.sid === "string" ? { sessionId: record.sid } : {}
|
|
2245
|
+
...typeof record.sid === "string" ? { sessionId: record.sid } : {},
|
|
2246
|
+
...typeof record.sub === "string" ? { subject: record.sub } : {}
|
|
2201
2247
|
};
|
|
2202
2248
|
} catch {
|
|
2203
2249
|
return {};
|
|
@@ -2298,12 +2344,13 @@ function mergeSetCookies(current, headers) {
|
|
|
2298
2344
|
return [...jar.entries()].map(([name, value]) => `${name}=${value}`).join("; ");
|
|
2299
2345
|
}
|
|
2300
2346
|
/**
|
|
2301
|
-
* Trae's
|
|
2347
|
+
* Trae's login, one per region.
|
|
2302
2348
|
*
|
|
2303
|
-
*
|
|
2304
|
-
*
|
|
2305
|
-
*
|
|
2306
|
-
*
|
|
2349
|
+
* A web login stores the `Cloud-IDE-JWT` from the website plus the cookies
|
|
2350
|
+
* behind it, so the short token can be renewed silently until the website
|
|
2351
|
+
* session dies. A client login stores the token the desktop client already
|
|
2352
|
+
* uses; that token has no website session, and the client itself refreshes it.
|
|
2353
|
+
* Only the client source carries the device id the check-in route accepts.
|
|
2307
2354
|
*/
|
|
2308
2355
|
var TraeAuth = class {
|
|
2309
2356
|
store;
|
|
@@ -2324,18 +2371,32 @@ var TraeAuth = class {
|
|
|
2324
2371
|
const stored = await this.store.trae(region);
|
|
2325
2372
|
if (stored === void 0) return {
|
|
2326
2373
|
signedIn: false,
|
|
2327
|
-
error: `${traeLabel(region)}
|
|
2374
|
+
error: `${traeLabel(region)}尚未登录。可以点「打开浏览器登录」,或复用本机已登录的 Trae 客户端。`
|
|
2328
2375
|
};
|
|
2376
|
+
const source = stored.source ?? "web";
|
|
2329
2377
|
const now = Date.now();
|
|
2330
2378
|
const { token, session } = traeExpiries(stored);
|
|
2331
2379
|
const tokenExpired = token !== void 0 && token <= now + TRAE_EXPIRY_MARGIN_MS;
|
|
2380
|
+
const account = traeAccount(stored);
|
|
2381
|
+
if (source === "client") {
|
|
2382
|
+
const signedIn = !tokenExpired;
|
|
2383
|
+
return {
|
|
2384
|
+
signedIn,
|
|
2385
|
+
loginSource: "client",
|
|
2386
|
+
loginAt: stored.loginAt,
|
|
2387
|
+
...token === void 0 ? {} : { expiresAt: token },
|
|
2388
|
+
...!signedIn ? { possiblyExpired: true } : {},
|
|
2389
|
+
...Object.keys(account).length === 0 ? {} : { account },
|
|
2390
|
+
...!signedIn ? { error: `${traeLabel(region)}客户端登录令牌已过期,请先在 Trae 客户端重新登录,再点「复用客户端登录态」。` } : {}
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2332
2393
|
const sessionExpired = session !== void 0 && session <= now + TRAE_EXPIRY_MARGIN_MS;
|
|
2333
2394
|
const refreshable = stored.cookies !== void 0 && !sessionExpired;
|
|
2334
2395
|
const signedIn = !tokenExpired || refreshable;
|
|
2335
|
-
const account = traeAccount(stored);
|
|
2336
2396
|
const expiresAt = refreshable ? session ?? token : token;
|
|
2337
2397
|
return {
|
|
2338
2398
|
signedIn,
|
|
2399
|
+
loginSource: "web",
|
|
2339
2400
|
loginAt: stored.loginAt,
|
|
2340
2401
|
...expiresAt === void 0 ? {} : { expiresAt },
|
|
2341
2402
|
...!signedIn ? { possiblyExpired: true } : {},
|
|
@@ -2346,15 +2407,21 @@ var TraeAuth = class {
|
|
|
2346
2407
|
/**
|
|
2347
2408
|
* The bearer token, or an actionable error when there is none.
|
|
2348
2409
|
*
|
|
2349
|
-
* A token near its expiry is renewed first; `force` means the gateway
|
|
2350
|
-
* rejected it, so the renewal is attempted even if the clock
|
|
2410
|
+
* A web token near its expiry is renewed first; `force` means the gateway
|
|
2411
|
+
* already rejected it, so the renewal is attempted even if the clock
|
|
2412
|
+
* disagrees. A client token is returned until its own expiry and then asks
|
|
2413
|
+
* for a fresh import, because only the client can refresh it.
|
|
2351
2414
|
*/
|
|
2352
2415
|
async accessToken(force = false) {
|
|
2353
2416
|
const region = this.region();
|
|
2354
2417
|
const stored = await this.store.trae(region);
|
|
2355
|
-
if (stored === void 0) throw new Error(`${traeLabel(region)}
|
|
2418
|
+
if (stored === void 0) throw new Error(`${traeLabel(region)}尚未登录。可以点「打开浏览器登录」,或复用本机已登录的 Trae 客户端。`);
|
|
2356
2419
|
const { token, session } = traeExpiries(stored);
|
|
2357
2420
|
const expired = token !== void 0 && token <= Date.now() + TRAE_EXPIRY_MARGIN_MS;
|
|
2421
|
+
if (stored.source === "client") {
|
|
2422
|
+
if (!force && !expired) return stored.accessToken;
|
|
2423
|
+
throw new Error(`${traeLabel(region)}客户端登录令牌已${force ? "失效" : "过期"},请先在 Trae 客户端重新登录,再点「复用客户端登录态」。`);
|
|
2424
|
+
}
|
|
2358
2425
|
if (!force && !expired) return stored.accessToken;
|
|
2359
2426
|
if (stored.cookies === void 0) throw new Error(`${traeLabel(region)}登录已${force ? "失效" : "过期"},请在设置页重新登录。`);
|
|
2360
2427
|
if (session !== void 0 && session <= Date.now() + TRAE_EXPIRY_MARGIN_MS) throw new Error(`${traeLabel(region)}网页会话已过期,请重新登录。`);
|
|
@@ -2366,7 +2433,7 @@ var TraeAuth = class {
|
|
|
2366
2433
|
if (existing !== void 0) return existing;
|
|
2367
2434
|
const task = (async () => {
|
|
2368
2435
|
const stored = await this.store.trae(region);
|
|
2369
|
-
if (stored === void 0 || stored.cookies === void 0) throw new Error(`${traeLabel(region)}登录已失效,请在设置页重新登录。`);
|
|
2436
|
+
if (stored === void 0 || stored.source === "client" || stored.cookies === void 0) throw new Error(`${traeLabel(region)}登录已失效,请在设置页重新登录。`);
|
|
2370
2437
|
const profile = TRAE_REGION_PROFILES[region];
|
|
2371
2438
|
let response;
|
|
2372
2439
|
try {
|
|
@@ -2425,7 +2492,7 @@ var TraeAuth = class {
|
|
|
2425
2492
|
async identity() {
|
|
2426
2493
|
const region = this.region();
|
|
2427
2494
|
const stored = await this.store.trae(region);
|
|
2428
|
-
if (stored === void 0) throw new Error(`${traeLabel(region)}
|
|
2495
|
+
if (stored === void 0) throw new Error(`${traeLabel(region)}尚未登录。可以点「打开浏览器登录」,或复用本机已登录的 Trae 客户端。`);
|
|
2429
2496
|
return {
|
|
2430
2497
|
deviceId: stored.deviceId,
|
|
2431
2498
|
machineId: stored.machineId,
|
|
@@ -2509,7 +2576,8 @@ var CodeArtsAuth = class {
|
|
|
2509
2576
|
securityToken: next.securityToken,
|
|
2510
2577
|
...next.expiresAt === void 0 ? {} : { expiresAt: next.expiresAt },
|
|
2511
2578
|
...next.refreshToken === void 0 ? {} : { refreshToken: next.refreshToken },
|
|
2512
|
-
...next.userName === void 0 ? {} : { userName: next.userName }
|
|
2579
|
+
...next.userName === void 0 ? {} : { userName: next.userName },
|
|
2580
|
+
...next.otSessionId === void 0 ? {} : { otSessionId: next.otSessionId }
|
|
2513
2581
|
});
|
|
2514
2582
|
this.lastError = void 0;
|
|
2515
2583
|
return next;
|
|
@@ -2529,6 +2597,7 @@ var CodeArtsAuth = class {
|
|
|
2529
2597
|
}
|
|
2530
2598
|
};
|
|
2531
2599
|
function credentialOf(record) {
|
|
2600
|
+
const otSessionId = record.otSessionId ?? otSessionIdOf(readUserProfile(record.refreshToken));
|
|
2532
2601
|
return {
|
|
2533
2602
|
accessKeyId: record.accessKeyId,
|
|
2534
2603
|
secretAccessKey: record.secretAccessKey,
|
|
@@ -2536,7 +2605,8 @@ function credentialOf(record) {
|
|
|
2536
2605
|
...record.expiresAt === void 0 ? {} : { expiresAt: record.expiresAt },
|
|
2537
2606
|
...record.refreshToken === void 0 ? {} : { refreshToken: record.refreshToken },
|
|
2538
2607
|
...record.codeVerifier === void 0 ? {} : { codeVerifier: record.codeVerifier },
|
|
2539
|
-
...record.dpopPrivateJwk === void 0 ? {} : { dpopPrivateJwk: record.dpopPrivateJwk }
|
|
2608
|
+
...record.dpopPrivateJwk === void 0 ? {} : { dpopPrivateJwk: record.dpopPrivateJwk },
|
|
2609
|
+
...otSessionId === void 0 ? {} : { otSessionId }
|
|
2540
2610
|
};
|
|
2541
2611
|
}
|
|
2542
2612
|
// src/config.ts
|
|
@@ -2583,8 +2653,7 @@ const Config$1 = z.object({
|
|
|
2583
2653
|
defaultMaxTokens: z.number().step(1).min(1).default(32e3).description("模型目录未声明输出上限时使用的输出上限。").volatile(),
|
|
2584
2654
|
modelCacheTtlSeconds: z.number().step(1).min(10).max(86400).default(120).description("模型列表缓存时长(秒)。调小可更快看到账号侧变化。").volatile(),
|
|
2585
2655
|
catalogFetchTimeoutSeconds: z.number().step(1).min(1).max(300).default(20).description("读取模型目录、额度等请求的时限(秒)。").volatile(),
|
|
2586
|
-
openRouterSyncMinutes: z.number().step(1).min(0).max(43200).default(60).description("OpenRouter 模型元数据的自动同步间隔(分钟)。0 表示关闭自动同步;缓存保存在本机,连不上时继续使用上次成功同步的数据。").volatile()
|
|
2587
|
-
raccoonCheckinMinutes: z.number().step(1).min(0).max(1440).default(30).description("小浣熊每日积分自动监测间隔(分钟):到点核对积分流水,未到账时按网页方式尝试触发。0 表示关闭自动监测;「打开网页签到」入口始终保留。").volatile()
|
|
2656
|
+
openRouterSyncMinutes: z.number().step(1).min(0).max(43200).default(60).description("OpenRouter 模型元数据的自动同步间隔(分钟)。0 表示关闭自动同步;缓存保存在本机,连不上时继续使用上次成功同步的数据。").volatile()
|
|
2588
2657
|
});
|
|
2589
2658
|
function liveConfig(config) {
|
|
2590
2659
|
const source = config ?? {};
|
|
@@ -2625,8 +2694,7 @@ function liveConfig(config) {
|
|
|
2625
2694
|
defaultMaxTokens: () => positive(field("defaultMaxTokens"), 32e3),
|
|
2626
2695
|
modelCacheTtlMs: () => positive(field("modelCacheTtlSeconds"), 120) * 1e3,
|
|
2627
2696
|
catalogFetchTimeoutMs: () => positive(field("catalogFetchTimeoutSeconds"), 20) * 1e3,
|
|
2628
|
-
openRouterSyncMinutes: () => nonNegative(field("openRouterSyncMinutes"), 60)
|
|
2629
|
-
raccoonCheckinMinutes: () => nonNegative(field("raccoonCheckinMinutes"), 30)
|
|
2697
|
+
openRouterSyncMinutes: () => nonNegative(field("openRouterSyncMinutes"), 60)
|
|
2630
2698
|
};
|
|
2631
2699
|
}
|
|
2632
2700
|
function resolveConfig(config) {
|
|
@@ -2664,15 +2732,14 @@ function resolveConfig(config) {
|
|
|
2664
2732
|
defaultMaxTokens: live.defaultMaxTokens(),
|
|
2665
2733
|
modelCacheTtlMs: live.modelCacheTtlMs(),
|
|
2666
2734
|
catalogFetchTimeoutMs: live.catalogFetchTimeoutMs(),
|
|
2667
|
-
openRouterSyncMinutes: live.openRouterSyncMinutes()
|
|
2668
|
-
raccoonCheckinMinutes: live.raccoonCheckinMinutes()
|
|
2735
|
+
openRouterSyncMinutes: live.openRouterSyncMinutes()
|
|
2669
2736
|
};
|
|
2670
2737
|
}
|
|
2671
|
-
function text$
|
|
2738
|
+
function text$4(value) {
|
|
2672
2739
|
return typeof value === "string" ? value.trim() : "";
|
|
2673
2740
|
}
|
|
2674
2741
|
function nonEmpty(value, fallback) {
|
|
2675
|
-
const valueText = text$
|
|
2742
|
+
const valueText = text$4(value);
|
|
2676
2743
|
return valueText.length > 0 ? valueText : fallback;
|
|
2677
2744
|
}
|
|
2678
2745
|
function positive(value, fallback) {
|
|
@@ -3008,117 +3075,6 @@ function headersForFetch(signed) {
|
|
|
3008
3075
|
}
|
|
3009
3076
|
return out;
|
|
3010
3077
|
}
|
|
3011
|
-
// src/codearts-window.ts
|
|
3012
|
-
/**
|
|
3013
|
-
* The trial plan's chat budget.
|
|
3014
|
-
*
|
|
3015
|
-
* The gateway opens one "session" per chat request and allows **three per
|
|
3016
|
-
* rolling minute** for the whole account. The window is shared by every model,
|
|
3017
|
-
* it is not keyed by `session-id` (re-sending the same id does not buy a slot),
|
|
3018
|
-
* and a request rejected with `InferHub.4004.200` spends a slot just like a
|
|
3019
|
-
* successful one. Measured behaviour, three slots then a lockout:
|
|
3020
|
-
*
|
|
3021
|
-
* ```
|
|
3022
|
-
* t+0s ok ok ok cap cap cap burst of six
|
|
3023
|
-
* t+10s cap …
|
|
3024
|
-
* t+51s cap
|
|
3025
|
-
* t+61s ok ok ok the burst ages out of the window
|
|
3026
|
-
* ```
|
|
3027
|
-
*
|
|
3028
|
-
* So a turn cannot be made to "go faster" by retrying: the only way a request
|
|
3029
|
-
* stops failing is to spend the budget deliberately — wait for the oldest start
|
|
3030
|
-
* to age out instead of firing into a wall. This class owns that bookkeeping and
|
|
3031
|
-
* is shared by every provider instance in the process, because a hot reload
|
|
3032
|
-
* must not reset the count the gateway still remembers.
|
|
3033
|
-
*
|
|
3034
|
-
* @module dsh-llm-adapters/codearts-window
|
|
3035
|
-
*/
|
|
3036
|
-
/** Sessions the trial plan allows inside one window. */
|
|
3037
|
-
const CODEARTS_SESSION_LIMIT = 3;
|
|
3038
|
-
/** How long a start keeps its slot. Measured: the burst frees at ~60 s. */
|
|
3039
|
-
const CODEARTS_SESSION_WINDOW_MS = 6e4;
|
|
3040
|
-
/** A little slack so a request never lands a few milliseconds early. */
|
|
3041
|
-
const MARGIN_MS = 1500;
|
|
3042
|
-
function sleep$2(ms, signal) {
|
|
3043
|
-
return new Promise((resolve, reject) => {
|
|
3044
|
-
if (signal?.aborted === true) {
|
|
3045
|
-
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
|
|
3046
|
-
return;
|
|
3047
|
-
}
|
|
3048
|
-
const onAbort = () => {
|
|
3049
|
-
clearTimeout(timer);
|
|
3050
|
-
reject(signal?.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
|
|
3051
|
-
};
|
|
3052
|
-
const timer = setTimeout(() => {
|
|
3053
|
-
signal?.removeEventListener("abort", onAbort);
|
|
3054
|
-
resolve();
|
|
3055
|
-
}, ms);
|
|
3056
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3057
|
-
});
|
|
3058
|
-
}
|
|
3059
|
-
var CodeArtsSessionWindow = class {
|
|
3060
|
-
limit;
|
|
3061
|
-
windowMs;
|
|
3062
|
-
now;
|
|
3063
|
-
starts = [];
|
|
3064
|
-
/** Serialises waiters so two callers cannot both take the last slot. */
|
|
3065
|
-
tail = Promise.resolve();
|
|
3066
|
-
constructor(limit = 3, windowMs = CODEARTS_SESSION_WINDOW_MS, now = Date.now) {
|
|
3067
|
-
this.limit = limit;
|
|
3068
|
-
this.windowMs = windowMs;
|
|
3069
|
-
this.now = now;
|
|
3070
|
-
}
|
|
3071
|
-
/** Starts recorded inside the current window. */
|
|
3072
|
-
get size() {
|
|
3073
|
-
this.trim();
|
|
3074
|
-
return this.starts.length;
|
|
3075
|
-
}
|
|
3076
|
-
/** Milliseconds until another start fits; `0` when the window has room. */
|
|
3077
|
-
waitMs() {
|
|
3078
|
-
this.trim();
|
|
3079
|
-
if (this.starts.length < this.limit) return 0;
|
|
3080
|
-
const oldest = this.starts[0] ?? 0;
|
|
3081
|
-
return Math.max(0, this.windowMs - (this.now() - oldest));
|
|
3082
|
-
}
|
|
3083
|
-
/**
|
|
3084
|
-
* Claim a slot, waiting for the window to roll over when it is full.
|
|
3085
|
-
*
|
|
3086
|
-
* The slot is recorded on return, not on completion: the gateway counts the
|
|
3087
|
-
* start, and holds it for a fixed time whether the answer was short or long.
|
|
3088
|
-
*/
|
|
3089
|
-
async open(signal) {
|
|
3090
|
-
const run = this.tail.then(async () => {
|
|
3091
|
-
for (;;) {
|
|
3092
|
-
const wait = this.waitMs();
|
|
3093
|
-
if (wait === 0) {
|
|
3094
|
-
const start = this.now();
|
|
3095
|
-
this.starts.push(start);
|
|
3096
|
-
return { release: () => {
|
|
3097
|
-
const index = this.starts.indexOf(start);
|
|
3098
|
-
if (index >= 0) this.starts.splice(index, 1);
|
|
3099
|
-
} };
|
|
3100
|
-
}
|
|
3101
|
-
await sleep$2(wait + MARGIN_MS, signal);
|
|
3102
|
-
}
|
|
3103
|
-
});
|
|
3104
|
-
this.tail = run.then(() => void 0, () => void 0);
|
|
3105
|
-
return await run;
|
|
3106
|
-
}
|
|
3107
|
-
/** Forget every start (tests, and a fresh login on another account). */
|
|
3108
|
-
reset() {
|
|
3109
|
-
this.starts.length = 0;
|
|
3110
|
-
}
|
|
3111
|
-
trim() {
|
|
3112
|
-
const now = this.now();
|
|
3113
|
-
while (this.starts.length > 0 && now - (this.starts[0] ?? 0) >= this.windowMs) this.starts.shift();
|
|
3114
|
-
}
|
|
3115
|
-
};
|
|
3116
|
-
let shared;
|
|
3117
|
-
/** The process-wide window; a plugin reload reuses it on purpose. */
|
|
3118
|
-
function sharedSessionWindow() {
|
|
3119
|
-
shared ??= new CodeArtsSessionWindow();
|
|
3120
|
-
return shared;
|
|
3121
|
-
}
|
|
3122
3078
|
// src/openrouter.ts
|
|
3123
3079
|
/**
|
|
3124
3080
|
* Live capability lookup from OpenRouter's public model catalog.
|
|
@@ -3499,10 +3455,10 @@ function codeartsEfforts(live) {
|
|
|
3499
3455
|
}
|
|
3500
3456
|
/** Every level a CodeArts model can get: the fallback when the router is silent. */
|
|
3501
3457
|
const CODEARTS_EFFORTS = codeartsEfforts();
|
|
3502
|
-
function record$
|
|
3458
|
+
function record$4(value) {
|
|
3503
3459
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3504
3460
|
}
|
|
3505
|
-
function text$
|
|
3461
|
+
function text$3(value) {
|
|
3506
3462
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
3507
3463
|
}
|
|
3508
3464
|
function num$1(value) {
|
|
@@ -3521,7 +3477,7 @@ function unwrapEnvelope(payload) {
|
|
|
3521
3477
|
const code = payload.code;
|
|
3522
3478
|
if (typeof code !== "number") return payload;
|
|
3523
3479
|
if (code !== 0) return void 0;
|
|
3524
|
-
return record$
|
|
3480
|
+
return record$4(payload.data);
|
|
3525
3481
|
}
|
|
3526
3482
|
/** `deepseek-v4-flash-0731` → `deepseek-v4-flash`; chat only knows the latter. */
|
|
3527
3483
|
function normalizeCodeArtsModelId(id) {
|
|
@@ -3537,21 +3493,21 @@ function normalizeCodeArtsModelId(id) {
|
|
|
3537
3493
|
* how the upstream behaves when a model has no slots at all.
|
|
3538
3494
|
*/
|
|
3539
3495
|
function pickActiveCredit(credit, slots, nowMs = Date.now()) {
|
|
3540
|
-
const entries = credit.map((entry) => record$
|
|
3496
|
+
const entries = credit.map((entry) => record$4(entry)).filter((entry) => entry !== void 0);
|
|
3541
3497
|
if (entries.length === 0) return void 0;
|
|
3542
|
-
const table = record$
|
|
3498
|
+
const table = record$4(slots);
|
|
3543
3499
|
if (table === void 0) return entries[0];
|
|
3544
3500
|
for (const entry of entries) {
|
|
3545
|
-
const slotId = text$
|
|
3501
|
+
const slotId = text$3(entry.time_slot_id);
|
|
3546
3502
|
if (slotId === void 0) continue;
|
|
3547
|
-
const slot = record$
|
|
3503
|
+
const slot = record$4(table[slotId]);
|
|
3548
3504
|
if (slot === void 0) continue;
|
|
3549
3505
|
if (slotActive(slot, nowMs)) return entry;
|
|
3550
3506
|
}
|
|
3551
3507
|
return entries[0];
|
|
3552
3508
|
}
|
|
3553
3509
|
function slotActive(slot, nowMs) {
|
|
3554
|
-
const schedule = text$
|
|
3510
|
+
const schedule = text$3(slot.schedule);
|
|
3555
3511
|
if (schedule === void 0) return false;
|
|
3556
3512
|
const parts = schedule.split(/\s+/);
|
|
3557
3513
|
if (parts.length < 3) return false;
|
|
@@ -3570,18 +3526,18 @@ function parseCodeArtsRoster(items, nowMs = Date.now()) {
|
|
|
3570
3526
|
const seen = /* @__PURE__ */ new Set();
|
|
3571
3527
|
const out = [];
|
|
3572
3528
|
for (const item of items) {
|
|
3573
|
-
const entry = record$
|
|
3529
|
+
const entry = record$4(item);
|
|
3574
3530
|
if (entry === void 0) continue;
|
|
3575
|
-
const rawId = text$
|
|
3531
|
+
const rawId = text$3(entry.model_id);
|
|
3576
3532
|
if (rawId === void 0) continue;
|
|
3577
3533
|
const id = normalizeCodeArtsModelId(rawId);
|
|
3578
3534
|
const key = id.toLowerCase();
|
|
3579
3535
|
if (seen.has(key)) continue;
|
|
3580
3536
|
seen.add(key);
|
|
3581
|
-
const rateLabel = text$
|
|
3537
|
+
const rateLabel = text$3(pickActiveCredit(Array.isArray(entry.credit) ? entry.credit : [], entry.time_slots, nowMs)?.ratio_display);
|
|
3582
3538
|
out.push({
|
|
3583
3539
|
id,
|
|
3584
|
-
name: text$
|
|
3540
|
+
name: text$3(entry.model_name) ?? id,
|
|
3585
3541
|
...rateLabel === void 0 ? {} : { rateLabel },
|
|
3586
3542
|
...entry.supports_images === true ? { images: true } : {}
|
|
3587
3543
|
});
|
|
@@ -3644,18 +3600,20 @@ const sleep$1 = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
3644
3600
|
}, ms);
|
|
3645
3601
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3646
3602
|
});
|
|
3647
|
-
/** Retries for a gateway that is busy for reasons the
|
|
3603
|
+
/** Retries for a gateway that is busy for reasons the caller cannot predict. */
|
|
3648
3604
|
const QUEUE_ATTEMPTS = 3;
|
|
3649
|
-
/**
|
|
3605
|
+
/** Pause before retrying a rejected call. */
|
|
3650
3606
|
const QUEUE_BACKOFF_MS = 2e4;
|
|
3651
3607
|
/**
|
|
3652
3608
|
* The message a user sees once waiting is hopeless.
|
|
3653
3609
|
*
|
|
3654
|
-
* The raw gateway text (`TM.00001041`) reads like a bug
|
|
3655
|
-
* trial plan's, and
|
|
3610
|
+
* The raw gateway text (`TM.00001041`) reads like a bug. The three concurrent
|
|
3611
|
+
* sessions are the trial plan's, and one plugin chatting under a single session
|
|
3612
|
+
* id is not meant to reach them: the slots are being held by something else on
|
|
3613
|
+
* the same account.
|
|
3656
3614
|
*/
|
|
3657
3615
|
function queueExhausted() {
|
|
3658
|
-
return new ProviderHttpError("CodeArts
|
|
3616
|
+
return new ProviderHttpError("CodeArts 这个账号的 3 个并发会话已被占满(通常是码道 IDE 或另一台设备),请稍后再发,或关闭其它 CodeArts 会话。", 429);
|
|
3659
3617
|
}
|
|
3660
3618
|
/** Image transcripts live beside accounts.json, so a restart does not re-bill. */
|
|
3661
3619
|
const IMAGE_CACHE_FILE = "codearts-image-cache.json";
|
|
@@ -3688,7 +3646,6 @@ var CodeArtsProvider = class {
|
|
|
3688
3646
|
/** Models that answered `InferHub.4004.200`: no allowance pack to bill. */
|
|
3689
3647
|
noBenefit = /* @__PURE__ */ new Set();
|
|
3690
3648
|
noBenefitLoaded = false;
|
|
3691
|
-
window;
|
|
3692
3649
|
openRouter;
|
|
3693
3650
|
sessionId = randomUUID();
|
|
3694
3651
|
/**
|
|
@@ -3703,7 +3660,6 @@ var CodeArtsProvider = class {
|
|
|
3703
3660
|
imageCacheWrite = Promise.resolve();
|
|
3704
3661
|
constructor(options) {
|
|
3705
3662
|
this.options = options;
|
|
3706
|
-
this.window = options.window ?? sharedSessionWindow();
|
|
3707
3663
|
this.openRouter = options.openRouter ?? new OpenRouterCatalog(options.fetchImpl ?? fetch);
|
|
3708
3664
|
this.catalog = new ModelCatalog(() => this.loadModels(), options.ttlMs);
|
|
3709
3665
|
this.login = new CodeArtsLogin({
|
|
@@ -3712,7 +3668,6 @@ var CodeArtsProvider = class {
|
|
|
3712
3668
|
...options.logger === void 0 ? {} : { logger: options.logger },
|
|
3713
3669
|
save: async (account) => {
|
|
3714
3670
|
await options.store.saveCodearts(account);
|
|
3715
|
-
this.window.reset();
|
|
3716
3671
|
this.noBenefit.clear();
|
|
3717
3672
|
this.invalidateModels();
|
|
3718
3673
|
this.invalidateQuota();
|
|
@@ -3762,8 +3717,8 @@ var CodeArtsProvider = class {
|
|
|
3762
3717
|
const now = Date.now();
|
|
3763
3718
|
const gateway = await this.signedGet(credential, CODEARTS_GATEWAY_CONFIG_URL);
|
|
3764
3719
|
const builtin = await this.signedGet(credential, `${CODEARTS_SNAP_BASE}${CODEARTS_MODEL_BUILTIN_PATH}`);
|
|
3765
|
-
const gatewayRows = parseCodeArtsRoster(record$
|
|
3766
|
-
const builtinRows = parseCodeArtsRoster(record$
|
|
3720
|
+
const gatewayRows = parseCodeArtsRoster(record$4(record$4(gateway)?.result)?.models, now);
|
|
3721
|
+
const builtinRows = parseCodeArtsRoster(record$4(builtin)?.builtinModels, now);
|
|
3767
3722
|
if (gatewayRows.length === 0 && builtinRows.length === 0) throw new Error("CodeArts 没有返回任何模型(签名、凭据或网关异常)");
|
|
3768
3723
|
if (gatewayRows.length > 0) this.benefitModels = new Set(gatewayRows.map((row) => row.id));
|
|
3769
3724
|
const textOnly = this.options.textOnly();
|
|
@@ -3844,7 +3799,7 @@ var CodeArtsProvider = class {
|
|
|
3844
3799
|
}
|
|
3845
3800
|
async readQuota() {
|
|
3846
3801
|
const credential = await this.credential();
|
|
3847
|
-
const payload = record$
|
|
3802
|
+
const payload = record$4(await this.signedGet(credential, `${CODEARTS_SNAP_BASE}${CODEARTS_PACKAGE_INFO_PATH}`));
|
|
3848
3803
|
if (payload === void 0) throw new Error("CodeArts 没有返回账户信息");
|
|
3849
3804
|
const info = parseCodeArtsPackage(payload);
|
|
3850
3805
|
const total = info.credit.amount ?? info.credit.remaining;
|
|
@@ -3936,26 +3891,26 @@ var CodeArtsProvider = class {
|
|
|
3936
3891
|
signal: AbortSignal.timeout(this.options.timeoutMs())
|
|
3937
3892
|
});
|
|
3938
3893
|
if (!response.ok) return void 0;
|
|
3939
|
-
return unwrapEnvelope(record$
|
|
3894
|
+
return unwrapEnvelope(record$4(await response.json()));
|
|
3940
3895
|
} catch {
|
|
3941
3896
|
return;
|
|
3942
3897
|
}
|
|
3943
3898
|
}
|
|
3944
3899
|
async accountInfo(credential) {
|
|
3945
|
-
const payload = record$
|
|
3900
|
+
const payload = record$4(await this.signedGet(credential, `${CODEARTS_SNAP_BASE}${CODEARTS_PACKAGE_INFO_PATH}`));
|
|
3946
3901
|
return payload === void 0 ? void 0 : parseCodeArtsPackage(payload);
|
|
3947
3902
|
}
|
|
3948
3903
|
async activities(credential) {
|
|
3949
3904
|
const url = `${CODEARTS_SNAP_BASE}${CODEARTS_OPS_DELIVERY_PATH}?channel=IDE`;
|
|
3950
|
-
const items = unwrapEnvelope(record$
|
|
3905
|
+
const items = unwrapEnvelope(record$4(await this.signedGet(credential, url)))?.items;
|
|
3951
3906
|
if (!Array.isArray(items)) return void 0;
|
|
3952
3907
|
return items.map((item) => {
|
|
3953
|
-
const entry = record$
|
|
3908
|
+
const entry = record$4(item) ?? {};
|
|
3954
3909
|
return {
|
|
3955
3910
|
campaignId: String(entry.campaignId ?? ""),
|
|
3956
|
-
type: text$
|
|
3911
|
+
type: text$3(entry.type) ?? "",
|
|
3957
3912
|
claimable: entry.claimable === true || String(entry.claimable).toLowerCase() === "true",
|
|
3958
|
-
status: text$
|
|
3913
|
+
status: text$3(entry.status) ?? "",
|
|
3959
3914
|
amount: num$1(entry.benefitAmount) ?? num$1(entry.amount) ?? num$1(entry.creditAmount) ?? 0
|
|
3960
3915
|
};
|
|
3961
3916
|
});
|
|
@@ -3971,7 +3926,7 @@ var CodeArtsProvider = class {
|
|
|
3971
3926
|
const cache = /* @__PURE__ */ new Map();
|
|
3972
3927
|
const file = this.imageCacheFile();
|
|
3973
3928
|
if (file !== void 0) try {
|
|
3974
|
-
const entries = record$
|
|
3929
|
+
const entries = record$4(record$4(JSON.parse(await promises.readFile(file, "utf8")))?.entries);
|
|
3975
3930
|
for (const [key, value] of Object.entries(entries ?? {})) if (typeof value === "string" && value.length > 0) cache.set(key, value);
|
|
3976
3931
|
} catch {}
|
|
3977
3932
|
this.imageCache = cache;
|
|
@@ -4024,7 +3979,7 @@ var CodeArtsProvider = class {
|
|
|
4024
3979
|
for (let index = 0; index < parts.length; index++) {
|
|
4025
3980
|
const part = parts[index];
|
|
4026
3981
|
if (part.type !== "image_url") continue;
|
|
4027
|
-
const url = record$
|
|
3982
|
+
const url = record$4(part.image_url)?.url;
|
|
4028
3983
|
if (typeof url !== "string" || url.length === 0) continue;
|
|
4029
3984
|
const key = sha256Hex(url);
|
|
4030
3985
|
let transcript = cache.get(key);
|
|
@@ -4041,7 +3996,7 @@ var CodeArtsProvider = class {
|
|
|
4041
3996
|
}
|
|
4042
3997
|
}
|
|
4043
3998
|
}
|
|
4044
|
-
/** Ask the vision model to describe one image
|
|
3999
|
+
/** Ask the vision model to describe one image; another turn on the same session. */
|
|
4045
4000
|
async describeImage(dataUrl, signal) {
|
|
4046
4001
|
const payload = JSON.stringify({
|
|
4047
4002
|
model: CODEARTS_VISION_MODEL,
|
|
@@ -4062,7 +4017,6 @@ var CodeArtsProvider = class {
|
|
|
4062
4017
|
let authRefreshed = false;
|
|
4063
4018
|
let queueAttempts = 0;
|
|
4064
4019
|
for (;;) {
|
|
4065
|
-
const slot = await this.window.open(signal);
|
|
4066
4020
|
let response;
|
|
4067
4021
|
try {
|
|
4068
4022
|
response = await this.send(chatId, payload, CODEARTS_VISION_MODEL, false, signal);
|
|
@@ -4073,10 +4027,9 @@ var CodeArtsProvider = class {
|
|
|
4073
4027
|
continue;
|
|
4074
4028
|
}
|
|
4075
4029
|
if (error instanceof CodeArtsRetry && error.kind === "queue") {
|
|
4076
|
-
slot.release();
|
|
4077
4030
|
if (queueAttempts < QUEUE_ATTEMPTS) {
|
|
4078
4031
|
queueAttempts++;
|
|
4079
|
-
await sleep$1(
|
|
4032
|
+
await sleep$1(QUEUE_BACKOFF_MS, signal);
|
|
4080
4033
|
continue;
|
|
4081
4034
|
}
|
|
4082
4035
|
throw queueExhausted();
|
|
@@ -4088,19 +4041,18 @@ var CodeArtsProvider = class {
|
|
|
4088
4041
|
for await (const chunk of consumeOpenAiStream(response, {
|
|
4089
4042
|
...signal === void 0 ? {} : { signal },
|
|
4090
4043
|
onFrame: (frame) => {
|
|
4091
|
-
const code = text$
|
|
4044
|
+
const code = text$3(frame.error_code);
|
|
4092
4045
|
if (code === void 0) return;
|
|
4093
|
-
const message = text$
|
|
4046
|
+
const message = text$3(frame.error_msg) ?? code;
|
|
4094
4047
|
if (QUEUE_MARKERS.some((marker) => code.includes(marker))) throw new CodeArtsRetry("queue", message);
|
|
4095
4048
|
throw new ProviderHttpError(message, 502);
|
|
4096
4049
|
}
|
|
4097
4050
|
})) if (chunk.type === "text-delta") transcript += chunk.text;
|
|
4098
4051
|
} catch (error) {
|
|
4099
4052
|
if (error instanceof CodeArtsRetry && error.kind === "queue") {
|
|
4100
|
-
slot.release();
|
|
4101
4053
|
if (queueAttempts < QUEUE_ATTEMPTS) {
|
|
4102
4054
|
queueAttempts++;
|
|
4103
|
-
await sleep$1(
|
|
4055
|
+
await sleep$1(QUEUE_BACKOFF_MS, signal);
|
|
4104
4056
|
continue;
|
|
4105
4057
|
}
|
|
4106
4058
|
throw queueExhausted();
|
|
@@ -4139,7 +4091,6 @@ var CodeArtsProvider = class {
|
|
|
4139
4091
|
let benefitDropped = false;
|
|
4140
4092
|
let queueAttempts = 0;
|
|
4141
4093
|
for (;;) {
|
|
4142
|
-
const slot = await this.window.open(options.signal);
|
|
4143
4094
|
let response;
|
|
4144
4095
|
try {
|
|
4145
4096
|
response = await this.send(chatId, payload, model, benefitDropped, options.signal);
|
|
@@ -4162,8 +4113,7 @@ var CodeArtsProvider = class {
|
|
|
4162
4113
|
}
|
|
4163
4114
|
if (error.kind === "queue" && queueAttempts < QUEUE_ATTEMPTS) {
|
|
4164
4115
|
queueAttempts++;
|
|
4165
|
-
|
|
4166
|
-
await sleep$1(Math.max(this.window.waitMs(), QUEUE_BACKOFF_MS), options.signal);
|
|
4116
|
+
await sleep$1(QUEUE_BACKOFF_MS, options.signal);
|
|
4167
4117
|
continue;
|
|
4168
4118
|
}
|
|
4169
4119
|
if (error.kind === "queue") throw queueExhausted();
|
|
@@ -4175,9 +4125,9 @@ var CodeArtsProvider = class {
|
|
|
4175
4125
|
for await (const chunk of consumeOpenAiStream(response, {
|
|
4176
4126
|
...options.signal === void 0 ? {} : { signal: options.signal },
|
|
4177
4127
|
onFrame: (frame) => {
|
|
4178
|
-
const code = text$
|
|
4128
|
+
const code = text$3(frame.error_code);
|
|
4179
4129
|
if (code === void 0) return;
|
|
4180
|
-
const message = text$
|
|
4130
|
+
const message = text$3(frame.error_msg) ?? code;
|
|
4181
4131
|
if (QUEUE_MARKERS.some((marker) => code.includes(marker))) throw new CodeArtsRetry("queue", message);
|
|
4182
4132
|
if (code.includes("InferHub.4004.200")) throw new CodeArtsRetry("benefit", message);
|
|
4183
4133
|
throw new ProviderHttpError(message, 502);
|
|
@@ -4190,8 +4140,7 @@ var CodeArtsProvider = class {
|
|
|
4190
4140
|
} catch (error) {
|
|
4191
4141
|
if (!produced && error instanceof CodeArtsRetry && error.kind === "queue" && queueAttempts < QUEUE_ATTEMPTS) {
|
|
4192
4142
|
queueAttempts++;
|
|
4193
|
-
|
|
4194
|
-
await sleep$1(Math.max(this.window.waitMs(), QUEUE_BACKOFF_MS), options.signal);
|
|
4143
|
+
await sleep$1(QUEUE_BACKOFF_MS, options.signal);
|
|
4195
4144
|
continue;
|
|
4196
4145
|
}
|
|
4197
4146
|
if (error instanceof CodeArtsRetry && error.kind === "queue") throw queueExhausted();
|
|
@@ -4211,13 +4160,17 @@ var CodeArtsProvider = class {
|
|
|
4211
4160
|
async send(chatId, payload, model, benefitDropped, signal) {
|
|
4212
4161
|
const credential = await this.credential();
|
|
4213
4162
|
const benefit = !benefitDropped && this.isBenefitModel(model);
|
|
4163
|
+
const otSessionId = credential.otSessionId ?? this.sessionId.replace(/-/g, "");
|
|
4214
4164
|
const headers = {
|
|
4215
4165
|
...headersForFetch(await signRequestHuawei(credential.accessKeyId, credential.secretAccessKey, credential.securityToken, "POST", CODEARTS_CHAT_URL, payload, benefit ? { maas_type: "benefit" } : void 0)),
|
|
4216
4166
|
"content-type": "application/json",
|
|
4217
4167
|
accept: "text/event-stream",
|
|
4218
4168
|
"chat-id": chatId,
|
|
4219
4169
|
"session-id": this.sessionId,
|
|
4220
|
-
lang: "en"
|
|
4170
|
+
lang: "en",
|
|
4171
|
+
"x-ot-session-id": otSessionId,
|
|
4172
|
+
"user-session-id": otSessionId,
|
|
4173
|
+
"x-ot-trace-id": randomUUID().replace(/-/g, "")
|
|
4221
4174
|
};
|
|
4222
4175
|
const timeout = AbortSignal.timeout(Math.max(this.options.timeoutMs(), 6e4));
|
|
4223
4176
|
const response = await this.fetcher(CODEARTS_CHAT_URL, {
|
|
@@ -4247,7 +4200,6 @@ var CodeArtsProvider = class {
|
|
|
4247
4200
|
}
|
|
4248
4201
|
async logoutAccount() {
|
|
4249
4202
|
await this.options.auth.logout();
|
|
4250
|
-
this.window.reset();
|
|
4251
4203
|
this.invalidateModels();
|
|
4252
4204
|
this.invalidateQuota();
|
|
4253
4205
|
}
|
|
@@ -4275,8 +4227,8 @@ function clampMaxTokens(value) {
|
|
|
4275
4227
|
* for them would claim credits expire when they do not.
|
|
4276
4228
|
*/
|
|
4277
4229
|
function parseCodeArtsPackage(payload) {
|
|
4278
|
-
const pkg = record$
|
|
4279
|
-
const endDate = text$
|
|
4230
|
+
const pkg = record$4(payload.package) ?? {};
|
|
4231
|
+
const endDate = text$3(payload.end_date);
|
|
4280
4232
|
const endMs = endDate === void 0 ? void 0 : Date.parse(endDate);
|
|
4281
4233
|
const labels = {
|
|
4282
4234
|
usageBasicPackageCredit: "基础积分包",
|
|
@@ -4286,9 +4238,9 @@ function parseCodeArtsPackage(payload) {
|
|
|
4286
4238
|
const packs = [];
|
|
4287
4239
|
let summary;
|
|
4288
4240
|
for (const item of Array.isArray(payload.metrics) ? payload.metrics : []) {
|
|
4289
|
-
const metric = record$
|
|
4241
|
+
const metric = record$4(item);
|
|
4290
4242
|
if (metric === void 0) continue;
|
|
4291
|
-
const name = text$
|
|
4243
|
+
const name = text$3(metric.name) ?? "";
|
|
4292
4244
|
const amount = num$1(metric.package_credit_amount);
|
|
4293
4245
|
const used = num$1(metric.package_credit_used);
|
|
4294
4246
|
const remain = num$1(metric.package_credit_remain);
|
|
@@ -4321,8 +4273,8 @@ function parseCodeArtsPackage(payload) {
|
|
|
4321
4273
|
});
|
|
4322
4274
|
return {
|
|
4323
4275
|
isCreditPackage: pkg.is_credit_package === true || String(pkg.is_credit_package).toLowerCase() === "true",
|
|
4324
|
-
packageName: text$
|
|
4325
|
-
packageStatus: text$
|
|
4276
|
+
packageName: text$3(pkg.package_name_cn) ?? text$3(pkg.package_name_en) ?? "",
|
|
4277
|
+
packageStatus: text$3(pkg.status) ?? "",
|
|
4326
4278
|
credit: {
|
|
4327
4279
|
...credit,
|
|
4328
4280
|
packs
|
|
@@ -6278,7 +6230,7 @@ function numeric(value) {
|
|
|
6278
6230
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
6279
6231
|
if (typeof value === "string" && /^-?\d+(?:\.\d+)?$/.test(value.trim())) return Number(value);
|
|
6280
6232
|
}
|
|
6281
|
-
function record$
|
|
6233
|
+
function record$3(value) {
|
|
6282
6234
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6283
6235
|
}
|
|
6284
6236
|
function messageOf(payload) {
|
|
@@ -6311,6 +6263,100 @@ function raccoonHeaders(token) {
|
|
|
6311
6263
|
"x-client-version": RACCOON_CLIENT_VERSION
|
|
6312
6264
|
};
|
|
6313
6265
|
}
|
|
6266
|
+
/** Wording the gateway has used alongside that code. */
|
|
6267
|
+
const RATE_LIMIT_WORDS = [
|
|
6268
|
+
"user_model_rate_limited",
|
|
6269
|
+
"rate_limit",
|
|
6270
|
+
"频率超限"
|
|
6271
|
+
];
|
|
6272
|
+
/**
|
|
6273
|
+
* Whether a rejection is the token wall rather than a generic failure.
|
|
6274
|
+
*
|
|
6275
|
+
* Matched on the status, the code and the wording, because the same condition
|
|
6276
|
+
* arrives as an HTTP 429 and as an error frame inside a 200 stream.
|
|
6277
|
+
*/
|
|
6278
|
+
function isRaccoonRateLimited(status, text) {
|
|
6279
|
+
if (status === 429) return true;
|
|
6280
|
+
const lower = text.toLowerCase();
|
|
6281
|
+
return RATE_LIMIT_WORDS.some((word) => lower.includes(word.toLowerCase()));
|
|
6282
|
+
}
|
|
6283
|
+
/**
|
|
6284
|
+
* The wall, as an error the caller can tell apart from a generic 429.
|
|
6285
|
+
*
|
|
6286
|
+
* A subclass rather than a flag: the in-band path and the HTTP path would
|
|
6287
|
+
* otherwise both be caught by the same handler, and the second pass would wrap
|
|
6288
|
+
* the message the first pass had already rewritten.
|
|
6289
|
+
*/
|
|
6290
|
+
var RaccoonRateLimitedError = class extends ProviderHttpError {
|
|
6291
|
+
constructor(message) {
|
|
6292
|
+
super(message, 429);
|
|
6293
|
+
this.name = "RaccoonRateLimitedError";
|
|
6294
|
+
}
|
|
6295
|
+
};
|
|
6296
|
+
/**
|
|
6297
|
+
* The message a user sees once the wall is hit.
|
|
6298
|
+
*
|
|
6299
|
+
* The raw text says "retry later" and gives no reason, which is why this reads
|
|
6300
|
+
* as the plugin being broken. The honest answer names the cause and the one
|
|
6301
|
+
* lever the user actually has on this side.
|
|
6302
|
+
*/
|
|
6303
|
+
function raccoonRateLimited(detail) {
|
|
6304
|
+
return new RaccoonRateLimitedError(`小浣熊账号的模型吞吐已达上限(不是每分钟请求数,而是按账号的 token 速率)。一轮对话带的上下文越大越快撞到,撞到后额度按吞吐缓慢回血,上游也没有给恢复时间。稍等一会儿再发;把该模型的上下文窗口调小,同一会话能撑得更久。上游原文:${detail}`);
|
|
6305
|
+
}
|
|
6306
|
+
/** The `error` object a compatible gateway puts in an SSE frame. */
|
|
6307
|
+
function frameFailure(frame) {
|
|
6308
|
+
const error = frame.error;
|
|
6309
|
+
if (error === void 0) return void 0;
|
|
6310
|
+
if (typeof error === "string") return { message: error };
|
|
6311
|
+
if (error === null || typeof error !== "object") return void 0;
|
|
6312
|
+
const record = error;
|
|
6313
|
+
const code = typeof record.code === "string" ? record.code : void 0;
|
|
6314
|
+
const message = typeof record.message === "string" ? record.message : void 0;
|
|
6315
|
+
return {
|
|
6316
|
+
...code === void 0 ? {} : { code },
|
|
6317
|
+
...message === void 0 ? {} : { message }
|
|
6318
|
+
};
|
|
6319
|
+
}
|
|
6320
|
+
/**
|
|
6321
|
+
* One conversation's session id, stable across the turns of that conversation.
|
|
6322
|
+
*
|
|
6323
|
+
* The official client never opens a session per request — measured on its own
|
|
6324
|
+
* logs, 125 calls over 26 minutes all carried one `sess-0-53927d74` — while an
|
|
6325
|
+
* implementation that mints a fresh id per request tells the gateway that one
|
|
6326
|
+
* account opened hundreds of sessions within minutes.
|
|
6327
|
+
*
|
|
6328
|
+
* The id is a pure function of (account, model) rather than a random value, so a
|
|
6329
|
+
* restarted or hot-reloaded process continues the same conversation instead of
|
|
6330
|
+
* presenting a new session, and two models stay two conversations. The principal
|
|
6331
|
+
* itself never leaves the process — only a truncated digest of it does.
|
|
6332
|
+
*/
|
|
6333
|
+
var RaccoonSessionIds = class {
|
|
6334
|
+
principal;
|
|
6335
|
+
/**
|
|
6336
|
+
* @param principal - a value that is stable for one account and identifies it
|
|
6337
|
+
* without being the token itself (the JWT's `sub`).
|
|
6338
|
+
*/
|
|
6339
|
+
constructor(principal) {
|
|
6340
|
+
this.principal = principal;
|
|
6341
|
+
}
|
|
6342
|
+
async for(model) {
|
|
6343
|
+
const principal = await this.principal();
|
|
6344
|
+
if (principal === void 0) return void 0;
|
|
6345
|
+
return raccoonSessionId(principal, model);
|
|
6346
|
+
}
|
|
6347
|
+
};
|
|
6348
|
+
/**
|
|
6349
|
+
* `sess-<sequence>-<8 hex>`, the shape the official client sends.
|
|
6350
|
+
*
|
|
6351
|
+
* The sequence is the official client's process-local counter and is pinned to 0
|
|
6352
|
+
* here on purpose: an incrementing value would make one conversation look like a
|
|
6353
|
+
* different session after a restart, which is the one thing this id exists to
|
|
6354
|
+
* prevent. Identity lives in the digest, which is truncated on purpose — the id
|
|
6355
|
+
* must separate accounts without naming them on the wire.
|
|
6356
|
+
*/
|
|
6357
|
+
function raccoonSessionId(principal, model) {
|
|
6358
|
+
return `sess-0-${createHash("sha256").update(`${principal}\0${model}`).digest("hex").slice(0, 8)}`;
|
|
6359
|
+
}
|
|
6314
6360
|
/** Whether the gateway accepts a token harvested from the browser. */
|
|
6315
6361
|
async function raccoonAccessTokenValid(token, fetchImpl = fetch) {
|
|
6316
6362
|
try {
|
|
@@ -6332,7 +6378,7 @@ async function raccoonAccessTokenValid(token, fetchImpl = fetch) {
|
|
|
6332
6378
|
return false;
|
|
6333
6379
|
}
|
|
6334
6380
|
}
|
|
6335
|
-
function jwtExpiry$
|
|
6381
|
+
function jwtExpiry$2(token) {
|
|
6336
6382
|
const parts = token.split(".");
|
|
6337
6383
|
if (parts.length < 2) return void 0;
|
|
6338
6384
|
try {
|
|
@@ -6354,7 +6400,7 @@ function raccoonRefreshCandidate(tokens, accessToken) {
|
|
|
6354
6400
|
let best;
|
|
6355
6401
|
for (const token of tokens) {
|
|
6356
6402
|
if (token === accessToken) continue;
|
|
6357
|
-
const expiry = jwtExpiry$
|
|
6403
|
+
const expiry = jwtExpiry$2(token);
|
|
6358
6404
|
if (expiry === void 0) continue;
|
|
6359
6405
|
if (best === void 0 || expiry > best.expiry) best = {
|
|
6360
6406
|
token,
|
|
@@ -6368,12 +6414,16 @@ var RaccoonProvider = class {
|
|
|
6368
6414
|
catalog;
|
|
6369
6415
|
quotaCache;
|
|
6370
6416
|
quotaInflight;
|
|
6371
|
-
checkinInflight;
|
|
6372
6417
|
loginFlow;
|
|
6373
6418
|
openRouter;
|
|
6419
|
+
sessionIds;
|
|
6374
6420
|
constructor(options) {
|
|
6375
6421
|
this.options = options;
|
|
6376
6422
|
this.openRouter = options.openRouter ?? new OpenRouterCatalog(options.fetchImpl ?? fetch);
|
|
6423
|
+
this.sessionIds = options.sessionIds ?? new RaccoonSessionIds(async () => {
|
|
6424
|
+
const stored = await options.store.raccoon();
|
|
6425
|
+
return stored === void 0 ? void 0 : decodeJwtFacts(stored.accessToken).subject;
|
|
6426
|
+
});
|
|
6377
6427
|
this.catalog = new ModelCatalog(() => this.fetchModels(), options.ttlMs);
|
|
6378
6428
|
}
|
|
6379
6429
|
async status() {
|
|
@@ -6427,6 +6477,21 @@ var RaccoonProvider = class {
|
|
|
6427
6477
|
headers(token) {
|
|
6428
6478
|
return raccoonHeaders(token);
|
|
6429
6479
|
}
|
|
6480
|
+
/**
|
|
6481
|
+
* The chat headers, including the session id the gateway counts sessions by.
|
|
6482
|
+
*
|
|
6483
|
+
* Only chat carries it: the catalogue, quota and check-in routes are plain
|
|
6484
|
+
* business calls the official client sends outside any session, so leaving
|
|
6485
|
+
* them alone keeps this change to the one route it was measured on.
|
|
6486
|
+
*/
|
|
6487
|
+
async chatHeaders(token, model) {
|
|
6488
|
+
const base = raccoonHeaders(token);
|
|
6489
|
+
const sessionId = await this.sessionIds.for(model).catch(() => void 0);
|
|
6490
|
+
return sessionId === void 0 ? base : {
|
|
6491
|
+
...base,
|
|
6492
|
+
"x-raccoon-session-id": sessionId
|
|
6493
|
+
};
|
|
6494
|
+
}
|
|
6430
6495
|
/** Run one JSON call; a rejected token means this login needs a new scan. */
|
|
6431
6496
|
async authorizedJson(path, init = {}, extraHeaders = {}) {
|
|
6432
6497
|
const token = await this.options.auth.accessToken();
|
|
@@ -6517,13 +6582,13 @@ var RaccoonProvider = class {
|
|
|
6517
6582
|
*/
|
|
6518
6583
|
async dailyGrant() {
|
|
6519
6584
|
const payload = await this.authorizedJson("/points/v2/bills?paging.limit=50&type=all");
|
|
6520
|
-
const root = record$
|
|
6585
|
+
const root = record$3(payload) ?? {};
|
|
6521
6586
|
if (typeof root.code === "number" && root.code !== 0) throw new Error(messageOf(payload) ?? `小浣熊积分流水返回 code=${root.code}`);
|
|
6522
|
-
const data = record$
|
|
6587
|
+
const data = record$3(root.data) ?? root;
|
|
6523
6588
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
6524
6589
|
const today = raccoonDay(Date.now());
|
|
6525
6590
|
for (const raw of items) {
|
|
6526
|
-
const item = record$
|
|
6591
|
+
const item = record$3(raw);
|
|
6527
6592
|
if (item?.biz_type !== "daily_grant") continue;
|
|
6528
6593
|
const day = item.grant_at ?? item.created_at;
|
|
6529
6594
|
if (typeof day !== "string" && typeof day !== "number") continue;
|
|
@@ -6542,6 +6607,15 @@ var RaccoonProvider = class {
|
|
|
6542
6607
|
};
|
|
6543
6608
|
}
|
|
6544
6609
|
}
|
|
6610
|
+
/**
|
|
6611
|
+
* Read today's grant. **Read-only on purpose.**
|
|
6612
|
+
*
|
|
6613
|
+
* The gateway has no claim endpoint: points are issued on the day's first
|
|
6614
|
+
* website visit. Replaying that visit (`setting_info`) can trigger the grant
|
|
6615
|
+
* from here, but doing so takes a state-changing action on the account without
|
|
6616
|
+
* the user asking for it in that moment, so the plugin only reports. Claiming
|
|
6617
|
+
* is the website's job, and the card links there.
|
|
6618
|
+
*/
|
|
6545
6619
|
async checkinStatus() {
|
|
6546
6620
|
const grant = await this.dailyGrant();
|
|
6547
6621
|
if (grant !== void 0) return {
|
|
@@ -6561,46 +6635,6 @@ var RaccoonProvider = class {
|
|
|
6561
6635
|
url: RACCOON_CHECKIN_URL
|
|
6562
6636
|
};
|
|
6563
6637
|
}
|
|
6564
|
-
async claimCheckin() {
|
|
6565
|
-
await this.authorizedJson("/office/v3/setting_info", {}, {
|
|
6566
|
-
"x-client-platform": "web",
|
|
6567
|
-
"x-client-version": "131.0.0.0"
|
|
6568
|
-
});
|
|
6569
|
-
const grant = await this.dailyGrant();
|
|
6570
|
-
if (grant === void 0) return {
|
|
6571
|
-
available: true,
|
|
6572
|
-
claimed: false,
|
|
6573
|
-
deviceClaimed: false,
|
|
6574
|
-
message: "每日积分尚未到账;请打开网页签到",
|
|
6575
|
-
url: RACCOON_CHECKIN_URL
|
|
6576
|
-
};
|
|
6577
|
-
this.invalidateQuota();
|
|
6578
|
-
return {
|
|
6579
|
-
available: true,
|
|
6580
|
-
claimed: true,
|
|
6581
|
-
deviceClaimed: false,
|
|
6582
|
-
...grant.points === void 0 ? {} : { reward: grant.points },
|
|
6583
|
-
...grant.grantedAt === void 0 ? {} : { grantedAt: grant.grantedAt },
|
|
6584
|
-
message: "每日积分已到账",
|
|
6585
|
-
url: RACCOON_CHECKIN_URL
|
|
6586
|
-
};
|
|
6587
|
-
}
|
|
6588
|
-
/**
|
|
6589
|
-
* One automatic check-in pass: report today's grant, or ask the website path
|
|
6590
|
-
* to issue it. Shared so overlapping timers cannot spend two call slots.
|
|
6591
|
-
*/
|
|
6592
|
-
async ensureDailyCheckin() {
|
|
6593
|
-
if (this.checkinInflight !== void 0) return this.checkinInflight;
|
|
6594
|
-
this.checkinInflight = (async () => {
|
|
6595
|
-
const status = await this.checkinStatus();
|
|
6596
|
-
return status.claimed ? status : this.claimCheckin();
|
|
6597
|
-
})();
|
|
6598
|
-
try {
|
|
6599
|
-
return await this.checkinInflight;
|
|
6600
|
-
} finally {
|
|
6601
|
-
this.checkinInflight = void 0;
|
|
6602
|
-
}
|
|
6603
|
-
}
|
|
6604
6638
|
async *stream(options, model, readImage, onUnauthorized) {
|
|
6605
6639
|
const token = await this.options.auth.accessToken();
|
|
6606
6640
|
const body = buildBody(options, model, await buildMessages(options, readImage), buildTools(options));
|
|
@@ -6617,7 +6651,7 @@ var RaccoonProvider = class {
|
|
|
6617
6651
|
try {
|
|
6618
6652
|
yield* openAiStream({
|
|
6619
6653
|
url: `${RACCOON_BASE}/llm/v2/chat/completions`,
|
|
6620
|
-
headers: this.
|
|
6654
|
+
headers: await this.chatHeaders(token, model),
|
|
6621
6655
|
body,
|
|
6622
6656
|
signal: options.signal,
|
|
6623
6657
|
fetchImpl: this.options.fetchImpl,
|
|
@@ -6625,27 +6659,268 @@ var RaccoonProvider = class {
|
|
|
6625
6659
|
this.invalidateModels();
|
|
6626
6660
|
this.invalidateQuota();
|
|
6627
6661
|
onUnauthorized?.();
|
|
6662
|
+
},
|
|
6663
|
+
onFrame: (frame) => {
|
|
6664
|
+
const failure = frameFailure(frame);
|
|
6665
|
+
if (failure === void 0) return;
|
|
6666
|
+
const detail = failure.message ?? failure.code ?? "上游返回错误";
|
|
6667
|
+
throw isRaccoonRateLimited(void 0, `${failure.code ?? ""} ${detail}`) ? raccoonRateLimited(detail) : new ProviderHttpError(detail, 502);
|
|
6628
6668
|
}
|
|
6629
6669
|
});
|
|
6630
6670
|
} catch (error) {
|
|
6631
6671
|
if (error instanceof ProviderHttpError && (error.status === 401 || error.status === 403)) throw new Error("小浣熊登录已失效,请在设置页点「打开扫码登录」。");
|
|
6672
|
+
if (error instanceof ProviderHttpError && !(error instanceof RaccoonRateLimitedError) && error.status === 429) throw raccoonRateLimited(error.message);
|
|
6632
6673
|
throw error;
|
|
6633
6674
|
}
|
|
6634
6675
|
}
|
|
6635
6676
|
};
|
|
6677
|
+
// src/trae-client.ts
|
|
6678
|
+
/**
|
|
6679
|
+
* The local Trae desktop client's sign-in state.
|
|
6680
|
+
*
|
|
6681
|
+
* Trae's check-in route insists on the device id its own client registered, so
|
|
6682
|
+
* the website login cannot use it. When the client is installed and already
|
|
6683
|
+
* signed in, the plugin can reuse that state instead:
|
|
6684
|
+
*
|
|
6685
|
+
* - the `Cloud-IDE-JWT` stored under `iCubeAuthInfo://icube.cloudide`;
|
|
6686
|
+
* - the 16-digit device id encoded in the `iCubeAuthInfo://icube-dc:<id>` key;
|
|
6687
|
+
* - `telemetry.machineId`, the value the client sends as `x-machine-id`.
|
|
6688
|
+
*
|
|
6689
|
+
* The stored value uses the client's own AES-CBC envelope: six magic bytes, a
|
|
6690
|
+
* 32-byte key material, then the ciphertext. The AES key and IV are derived
|
|
6691
|
+
* from that key material with SHA-512 and a fixed salt, and the plaintext is a
|
|
6692
|
+
* SHA-512 digest followed by the JSON document. Nothing here writes to the
|
|
6693
|
+
* client, and the token is never returned by the settings API.
|
|
6694
|
+
*
|
|
6695
|
+
* @module dsh-llm-adapters/trae-client
|
|
6696
|
+
*/
|
|
6697
|
+
const AUTH_PROVIDER_KEY = "iCubeAuthInfo://icube.cloudide";
|
|
6698
|
+
const DEVICE_KEY_PREFIX = "iCubeAuthInfo://icube-dc:";
|
|
6699
|
+
const MACHINE_KEY = "telemetry.machineId";
|
|
6700
|
+
const SERVICE_MACHINE_KEY = "storage.serviceMachineId";
|
|
6701
|
+
const AES_MAGIC = Buffer.from([
|
|
6702
|
+
116,
|
|
6703
|
+
99,
|
|
6704
|
+
5,
|
|
6705
|
+
16,
|
|
6706
|
+
0,
|
|
6707
|
+
0
|
|
6708
|
+
]);
|
|
6709
|
+
const AES_PRIVATE_MAGIC = Buffer.from([
|
|
6710
|
+
18,
|
|
6711
|
+
57,
|
|
6712
|
+
32,
|
|
6713
|
+
32,
|
|
6714
|
+
2,
|
|
6715
|
+
3
|
|
6716
|
+
]);
|
|
6717
|
+
const KEY_OFFSET = 6;
|
|
6718
|
+
const HASH_LENGTH = 64;
|
|
6719
|
+
/** Aue ^ Due from the client's byteCrypto module. */
|
|
6720
|
+
const AES_SALT = Buffer.from("4dd4c2e6b83162090e52b3c7a6733ba41cb2462b829ab58a196b39db57177524f49baf7f08e8d68d26a72e37c1a95a2f1f05a51892aef2949732b62a38aadd58", "hex");
|
|
6721
|
+
/** Tue ^ Iue, the AES_PRIVATE variant of the same envelope. */
|
|
6722
|
+
const AES_PRIVATE_SALT = Buffer.from("490cc21292b05d0cc06ccbe91fb92e21b5a7c538811dc85e27a96e28104e1cf69c7166efab232807390da78a8b91e49c44b49a43feb941a1e46f2656e613e47b", "hex");
|
|
6723
|
+
const DEVICE_RE = /^\d{16}$/;
|
|
6724
|
+
function record$2(value) {
|
|
6725
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6726
|
+
}
|
|
6727
|
+
function text$2(value) {
|
|
6728
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
6729
|
+
}
|
|
6730
|
+
/** Accept epoch seconds, epoch milliseconds, or an ISO date. */
|
|
6731
|
+
function epochMs$2(value) {
|
|
6732
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value < 0xe8d4a51000 ? Math.round(value * 1e3) : Math.round(value);
|
|
6733
|
+
if (typeof value === "string") {
|
|
6734
|
+
if (/^\d+$/.test(value.trim())) return epochMs$2(Number(value));
|
|
6735
|
+
const parsed = Date.parse(value);
|
|
6736
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
6737
|
+
}
|
|
6738
|
+
}
|
|
6739
|
+
function jwtExpiry$1(token) {
|
|
6740
|
+
const parts = token.split(".");
|
|
6741
|
+
if (parts.length < 2) return void 0;
|
|
6742
|
+
try {
|
|
6743
|
+
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
6744
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
6745
|
+
return epochMs$2(record$2(JSON.parse(Buffer.from(padded, "base64").toString("utf8")))?.exp);
|
|
6746
|
+
} catch {
|
|
6747
|
+
return;
|
|
6748
|
+
}
|
|
6749
|
+
}
|
|
6750
|
+
/**
|
|
6751
|
+
* Open one `iCubeAuthInfo` value.
|
|
6752
|
+
*
|
|
6753
|
+
* The digest check is the client's own integrity check: the first 64 plaintext
|
|
6754
|
+
* bytes are SHA-512 of the document that follows. A malformed or changed
|
|
6755
|
+
* envelope is rejected instead of being parsed as garbage.
|
|
6756
|
+
*/
|
|
6757
|
+
function decryptAuthValue(value) {
|
|
6758
|
+
const bytes = Buffer.from(value, "base64");
|
|
6759
|
+
if (bytes.length < 102) throw new Error("数据长度不足");
|
|
6760
|
+
const magic = bytes.subarray(0, KEY_OFFSET);
|
|
6761
|
+
const salt = magic.equals(AES_MAGIC) ? AES_SALT : magic.equals(AES_PRIVATE_MAGIC) ? AES_PRIVATE_SALT : void 0;
|
|
6762
|
+
if (salt === void 0) throw new Error("未知的加密格式");
|
|
6763
|
+
const keyMaterial = bytes.subarray(KEY_OFFSET, 38);
|
|
6764
|
+
const mixed = createHash("sha512").update(keyMaterial).digest();
|
|
6765
|
+
const derived = createHash("sha512").update(Buffer.concat([mixed, salt])).digest();
|
|
6766
|
+
const decipher = createDecipheriv("aes-128-cbc", derived.subarray(0, 16), derived.subarray(16, 32));
|
|
6767
|
+
const plain = Buffer.concat([decipher.update(bytes.subarray(38)), decipher.final()]);
|
|
6768
|
+
if (plain.length < HASH_LENGTH) throw new Error("解密结果为空");
|
|
6769
|
+
const body = plain.subarray(HASH_LENGTH);
|
|
6770
|
+
if (!createHash("sha512").update(body).digest().equals(plain.subarray(0, HASH_LENGTH))) throw new Error("校验失败");
|
|
6771
|
+
return JSON.parse(body.toString("utf8"));
|
|
6772
|
+
}
|
|
6773
|
+
/**
|
|
6774
|
+
* The client's data directory, most specific first.
|
|
6775
|
+
*
|
|
6776
|
+
* `DSH_TRAE_CLIENT_DIR` is a portable/test override; the region-specific
|
|
6777
|
+
* variables let one machine keep separate CN and international installs.
|
|
6778
|
+
*/
|
|
6779
|
+
function traeClientDataDirs(region, options = {}) {
|
|
6780
|
+
const platform = options.platform ?? process.platform;
|
|
6781
|
+
const env = options.env ?? process.env;
|
|
6782
|
+
const home = options.home ?? homedir();
|
|
6783
|
+
const name = region === "cn" ? "Trae CN" : "Trae";
|
|
6784
|
+
const out = [];
|
|
6785
|
+
const add = (directory) => {
|
|
6786
|
+
if (directory !== void 0 && directory !== "") out.push(directory);
|
|
6787
|
+
};
|
|
6788
|
+
add(env[region === "cn" ? "DSH_TRAE_CN_CLIENT_DIR" : "DSH_TRAE_GLOBAL_CLIENT_DIR"]);
|
|
6789
|
+
add(env["DSH_TRAE_CLIENT_DIR"]);
|
|
6790
|
+
if (platform === "win32") {
|
|
6791
|
+
const appData = env["APPDATA"] ?? join(home, "AppData", "Roaming");
|
|
6792
|
+
add(join(appData, name));
|
|
6793
|
+
} else if (platform === "darwin") add(join(home, "Library", "Application Support", name));
|
|
6794
|
+
else {
|
|
6795
|
+
const config = env["XDG_CONFIG_HOME"] ?? join(home, ".config");
|
|
6796
|
+
add(join(config, name));
|
|
6797
|
+
}
|
|
6798
|
+
return [...new Set(out)];
|
|
6799
|
+
}
|
|
6800
|
+
function storageFile(userDataDir) {
|
|
6801
|
+
return join(userDataDir, "User", "globalStorage", "storage.json");
|
|
6802
|
+
}
|
|
6803
|
+
function authValueOf(storage) {
|
|
6804
|
+
const direct = text$2(storage[AUTH_PROVIDER_KEY]);
|
|
6805
|
+
if (direct !== void 0) return direct;
|
|
6806
|
+
for (const key of Object.keys(storage)) if (key.startsWith("iCubeAuthInfo://") && key.endsWith(".cloudide")) {
|
|
6807
|
+
const value = text$2(storage[key]);
|
|
6808
|
+
if (value !== void 0) return value;
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
/** The client has one device id per machine; the last valid key is current. */
|
|
6812
|
+
function deviceIdOf(storage) {
|
|
6813
|
+
const keys = Object.keys(storage).filter((key) => key.startsWith(DEVICE_KEY_PREFIX));
|
|
6814
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
6815
|
+
const value = text$2(keys[index].slice(25));
|
|
6816
|
+
if (value !== void 0 && DEVICE_RE.test(value)) return value;
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
function regionMismatch(region, userInfo) {
|
|
6820
|
+
const account = record$2(userInfo.account);
|
|
6821
|
+
const evidence = (text$2(record$2(userInfo.userRegion)?.region) ?? text$2(account?.storeRegion))?.toUpperCase();
|
|
6822
|
+
if (evidence === void 0) return false;
|
|
6823
|
+
return region === "cn" ? evidence !== "CN" : evidence === "CN";
|
|
6824
|
+
}
|
|
6825
|
+
/** Parse one `storage.json` document; exported for tests and diagnostics. */
|
|
6826
|
+
function parseTraeClientStorage(region, storage) {
|
|
6827
|
+
const encrypted = authValueOf(storage);
|
|
6828
|
+
if (encrypted === void 0) return { status: {
|
|
6829
|
+
available: true,
|
|
6830
|
+
signedIn: false,
|
|
6831
|
+
error: "本机 Trae 客户端尚未登录。请先在客户端完成登录。"
|
|
6832
|
+
} };
|
|
6833
|
+
let parsed;
|
|
6834
|
+
try {
|
|
6835
|
+
parsed = decryptAuthValue(encrypted);
|
|
6836
|
+
} catch (error) {
|
|
6837
|
+
return { status: {
|
|
6838
|
+
available: true,
|
|
6839
|
+
signedIn: false,
|
|
6840
|
+
error: `读取本机 Trae 客户端登录态失败:${error instanceof Error ? error.message : String(error)}`
|
|
6841
|
+
} };
|
|
6842
|
+
}
|
|
6843
|
+
const userInfo = record$2(parsed);
|
|
6844
|
+
const token = text$2(userInfo?.token);
|
|
6845
|
+
if (userInfo === void 0 || token === void 0) return { status: {
|
|
6846
|
+
available: true,
|
|
6847
|
+
signedIn: false,
|
|
6848
|
+
error: "本机 Trae 客户端登录态里没有令牌,请先在客户端重新登录。"
|
|
6849
|
+
} };
|
|
6850
|
+
const account = record$2(userInfo.account);
|
|
6851
|
+
const name = text$2(account?.username) ?? text$2(account?.name);
|
|
6852
|
+
const userId = text$2(userInfo.userId);
|
|
6853
|
+
const expiresAt = epochMs$2(userInfo.expiredAt) ?? jwtExpiry$1(token);
|
|
6854
|
+
const identity = {
|
|
6855
|
+
...name === void 0 ? {} : { name },
|
|
6856
|
+
...userId === void 0 ? {} : { userId },
|
|
6857
|
+
...expiresAt === void 0 ? {} : { expiresAt }
|
|
6858
|
+
};
|
|
6859
|
+
const unavailable = (error) => ({ status: {
|
|
6860
|
+
available: true,
|
|
6861
|
+
signedIn: false,
|
|
6862
|
+
...identity,
|
|
6863
|
+
error
|
|
6864
|
+
} });
|
|
6865
|
+
if (regionMismatch(region, userInfo)) return unavailable(region === "cn" ? "本机 Trae 客户端登录的是国际版账号,请切换到国际版后再复用。" : "本机 Trae 客户端登录的是国内版账号,请切换到国内版后再复用。");
|
|
6866
|
+
if (expiresAt !== void 0 && expiresAt <= Date.now()) return unavailable("本机 Trae 客户端登录已过期,请先在客户端重新登录。");
|
|
6867
|
+
const deviceId = deviceIdOf(storage);
|
|
6868
|
+
if (deviceId === void 0) return unavailable("本机 Trae 客户端登录缺少 16 位设备号,请先在客户端重新登录。");
|
|
6869
|
+
const machineId = text$2(storage[MACHINE_KEY]) ?? text$2(storage[SERVICE_MACHINE_KEY]);
|
|
6870
|
+
if (machineId === void 0) return unavailable("本机 Trae 客户端登录缺少机器码,请先在客户端重新登录。");
|
|
6871
|
+
return {
|
|
6872
|
+
status: {
|
|
6873
|
+
available: true,
|
|
6874
|
+
signedIn: true,
|
|
6875
|
+
...identity
|
|
6876
|
+
},
|
|
6877
|
+
state: {
|
|
6878
|
+
region,
|
|
6879
|
+
token,
|
|
6880
|
+
deviceId,
|
|
6881
|
+
machineId,
|
|
6882
|
+
...identity
|
|
6883
|
+
}
|
|
6884
|
+
};
|
|
6885
|
+
}
|
|
6886
|
+
/** Read the currently selected region's client state from this machine. */
|
|
6887
|
+
function readTraeClientLogin(region, options = {}) {
|
|
6888
|
+
let available = false;
|
|
6889
|
+
let error;
|
|
6890
|
+
for (const directory of traeClientDataDirs(region, options)) {
|
|
6891
|
+
const file = storageFile(directory);
|
|
6892
|
+
if (!existsSync(file)) continue;
|
|
6893
|
+
available = true;
|
|
6894
|
+
try {
|
|
6895
|
+
const raw = readFileSync(file, "utf8").replace(/^\uFEFF/, "");
|
|
6896
|
+
const storage = record$2(JSON.parse(raw));
|
|
6897
|
+
if (storage === void 0) throw new Error("storage.json 不是对象");
|
|
6898
|
+
return parseTraeClientStorage(region, storage);
|
|
6899
|
+
} catch (cause) {
|
|
6900
|
+
error = `读取本机 Trae 客户端登录态失败:${cause instanceof Error ? cause.message : String(cause)}`;
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
return { status: {
|
|
6904
|
+
available,
|
|
6905
|
+
signedIn: false,
|
|
6906
|
+
...error === void 0 ? {} : { error }
|
|
6907
|
+
} };
|
|
6908
|
+
}
|
|
6636
6909
|
// src/trae-login.ts
|
|
6637
6910
|
/**
|
|
6638
6911
|
* Trae sign-in through the provider's own website.
|
|
6639
6912
|
*
|
|
6640
|
-
*
|
|
6913
|
+
* This flow does not read a desktop install: it opens a dedicated browser
|
|
6641
6914
|
* profile, lets the user sign in on trae.cn / trae.ai, and then takes the
|
|
6642
6915
|
* session the website itself created. Two things come out of that session:
|
|
6643
6916
|
*
|
|
6644
6917
|
* - the `Cloud-IDE-JWT` token, fetched with the page's own cookies through
|
|
6645
6918
|
* `/cloudide/api/v3/common/GetUserToken` — the same call the site makes;
|
|
6646
6919
|
* - the device fingerprint (`__msuuid__` from the page, or one generated in
|
|
6647
|
-
* the page),
|
|
6648
|
-
*
|
|
6920
|
+
* the page), stored with the web login.
|
|
6921
|
+
*
|
|
6922
|
+
* The check-in route does not accept this web fingerprint, so check-in is
|
|
6923
|
+
* enabled only for the separate client-login import in `trae-client.ts`.
|
|
6649
6924
|
*
|
|
6650
6925
|
* Cookies are captured over the DevTools protocol and kept in the plugin's own
|
|
6651
6926
|
* account store so the short-lived JWT can be renewed later; the dedicated
|
|
@@ -7257,10 +7532,12 @@ var TraeWebLogin = class {
|
|
|
7257
7532
|
};
|
|
7258
7533
|
// src/trae.ts
|
|
7259
7534
|
/**
|
|
7260
|
-
* Trae provider: model directory, SOLO chat and
|
|
7535
|
+
* Trae provider: model directory, SOLO chat, quota and client check-in.
|
|
7261
7536
|
*
|
|
7262
7537
|
* Trae's website login yields the same `Cloud-IDE-JWT` its own clients use, so
|
|
7263
|
-
* the plugin talks to the same gateways they do.
|
|
7538
|
+
* the plugin talks to the same gateways they do. A second sign-in method copies
|
|
7539
|
+
* that token from the local desktop client; only that method carries the device
|
|
7540
|
+
* id the daily check-in route accepts. Two catalog sources are merged:
|
|
7264
7541
|
*
|
|
7265
7542
|
* - the Remote directory (`/api/remote/v1/models`) is the model skeleton —
|
|
7266
7543
|
* display name, context window, multiplier, reasoning options, multimodal;
|
|
@@ -7962,6 +8239,22 @@ function parseTraeGlobalUsage(payload) {
|
|
|
7962
8239
|
at: Date.now()
|
|
7963
8240
|
};
|
|
7964
8241
|
}
|
|
8242
|
+
/**
|
|
8243
|
+
* The check-in body's `req_source`: 1 is the IDE, 2 is Trae Work. The desktop
|
|
8244
|
+
* client sends 1; the claim route rejects the plugin's former empty body.
|
|
8245
|
+
*/
|
|
8246
|
+
const TRAE_CHECKIN_REQ_SOURCE = 1;
|
|
8247
|
+
function parseTraeCheckin(payload) {
|
|
8248
|
+
const root = record(payload) ?? {};
|
|
8249
|
+
const reward = num(root.credits) ?? 0;
|
|
8250
|
+
return {
|
|
8251
|
+
available: root.enable !== false,
|
|
8252
|
+
claimed: root.checked_in === true,
|
|
8253
|
+
deviceClaimed: root.did_checked_in === true,
|
|
8254
|
+
...reward === 0 ? {} : { reward },
|
|
8255
|
+
...text(root.message) === void 0 ? {} : { message: text(root.message) }
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
7965
8258
|
var TraeProvider = class {
|
|
7966
8259
|
options;
|
|
7967
8260
|
catalogInstance;
|
|
@@ -8172,6 +8465,160 @@ var TraeProvider = class {
|
|
|
8172
8465
|
this.quotaInflightRegion = region;
|
|
8173
8466
|
return this.quotaInflight;
|
|
8174
8467
|
}
|
|
8468
|
+
/**
|
|
8469
|
+
* The second sign-in method: the desktop client's current state.
|
|
8470
|
+
*
|
|
8471
|
+
* Read-only; the settings page only receives the non-secret status, never
|
|
8472
|
+
* the token.
|
|
8473
|
+
*/
|
|
8474
|
+
async localClientLogin() {
|
|
8475
|
+
return this.readClientLogin().status;
|
|
8476
|
+
}
|
|
8477
|
+
readClientLogin() {
|
|
8478
|
+
return (this.options.clientLogin ?? (() => readTraeClientLogin(this.options.region())))();
|
|
8479
|
+
}
|
|
8480
|
+
/** Import the desktop client's token, device id and machine id. */
|
|
8481
|
+
async importClientLogin() {
|
|
8482
|
+
const region = this.ensureRegion();
|
|
8483
|
+
const detection = this.readClientLogin();
|
|
8484
|
+
if (!detection.status.available) throw new Error("本机没有检测到 Trae 客户端。请先安装并登录客户端,或改用网页登录。");
|
|
8485
|
+
const state = detection.state;
|
|
8486
|
+
if (!detection.status.signedIn || state === void 0) throw new Error(detection.status.error ?? "本机 Trae 客户端尚未登录。请先在客户端完成登录。");
|
|
8487
|
+
await this.loginFlow?.cancel();
|
|
8488
|
+
await this.options.store.saveTrae({
|
|
8489
|
+
accessToken: state.token,
|
|
8490
|
+
region,
|
|
8491
|
+
source: "client",
|
|
8492
|
+
deviceId: state.deviceId,
|
|
8493
|
+
machineId: state.machineId,
|
|
8494
|
+
loginAt: Date.now(),
|
|
8495
|
+
...state.userId === void 0 ? {} : { userId: state.userId },
|
|
8496
|
+
...state.name === void 0 ? {} : { name: state.name },
|
|
8497
|
+
...state.expiresAt === void 0 ? {} : { expiresAt: state.expiresAt }
|
|
8498
|
+
});
|
|
8499
|
+
this.invalidateModels();
|
|
8500
|
+
this.invalidateQuota();
|
|
8501
|
+
}
|
|
8502
|
+
async checkinContext() {
|
|
8503
|
+
this.ensureRegion();
|
|
8504
|
+
const profile = this.profile();
|
|
8505
|
+
if (!profile.checkin) return {
|
|
8506
|
+
ok: false,
|
|
8507
|
+
status: {
|
|
8508
|
+
available: false,
|
|
8509
|
+
claimed: false,
|
|
8510
|
+
deviceClaimed: false,
|
|
8511
|
+
message: "Trae 国际版没有签到活动。"
|
|
8512
|
+
}
|
|
8513
|
+
};
|
|
8514
|
+
const auth = await this.options.auth.status();
|
|
8515
|
+
if (!auth.signedIn) return {
|
|
8516
|
+
ok: false,
|
|
8517
|
+
status: {
|
|
8518
|
+
available: false,
|
|
8519
|
+
claimed: false,
|
|
8520
|
+
deviceClaimed: false,
|
|
8521
|
+
message: auth.error ?? "尚未登录,无法签到。"
|
|
8522
|
+
}
|
|
8523
|
+
};
|
|
8524
|
+
if (auth.loginSource !== "client") return {
|
|
8525
|
+
ok: false,
|
|
8526
|
+
status: {
|
|
8527
|
+
available: false,
|
|
8528
|
+
claimed: false,
|
|
8529
|
+
deviceClaimed: false,
|
|
8530
|
+
message: "签到仅在复用本机 Trae 客户端登录时可用;网页登录没有客户端设备号。"
|
|
8531
|
+
}
|
|
8532
|
+
};
|
|
8533
|
+
let token;
|
|
8534
|
+
let identity;
|
|
8535
|
+
try {
|
|
8536
|
+
token = await this.options.auth.accessToken();
|
|
8537
|
+
identity = await this.options.auth.identity();
|
|
8538
|
+
} catch (error) {
|
|
8539
|
+
return {
|
|
8540
|
+
ok: false,
|
|
8541
|
+
status: {
|
|
8542
|
+
available: false,
|
|
8543
|
+
claimed: false,
|
|
8544
|
+
deviceClaimed: false,
|
|
8545
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8546
|
+
}
|
|
8547
|
+
};
|
|
8548
|
+
}
|
|
8549
|
+
if (!/^\d{16}$/.test(identity.deviceId)) return {
|
|
8550
|
+
ok: false,
|
|
8551
|
+
status: {
|
|
8552
|
+
available: false,
|
|
8553
|
+
claimed: false,
|
|
8554
|
+
deviceClaimed: false,
|
|
8555
|
+
message: "本机 Trae 客户端登录缺少 16 位设备号,请先在客户端重新登录后再复用。"
|
|
8556
|
+
}
|
|
8557
|
+
};
|
|
8558
|
+
return {
|
|
8559
|
+
ok: true,
|
|
8560
|
+
profile,
|
|
8561
|
+
token,
|
|
8562
|
+
identity: {
|
|
8563
|
+
deviceId: identity.deviceId,
|
|
8564
|
+
machineId: identity.machineId
|
|
8565
|
+
}
|
|
8566
|
+
};
|
|
8567
|
+
}
|
|
8568
|
+
async checkinStatus() {
|
|
8569
|
+
const context = await this.checkinContext();
|
|
8570
|
+
if (!context.ok) return context.status;
|
|
8571
|
+
const response = await this.fetchImpl(`${context.profile.apiBase}/trae/api/v2/ug/checkin_credits/status`, {
|
|
8572
|
+
method: "POST",
|
|
8573
|
+
headers: {
|
|
8574
|
+
...this.headers(context.token, context.profile),
|
|
8575
|
+
"x-user-region": "CN",
|
|
8576
|
+
"x-device-id": context.identity.deviceId,
|
|
8577
|
+
"x-machine-id": context.identity.machineId
|
|
8578
|
+
},
|
|
8579
|
+
body: JSON.stringify({ req_source: 1 }),
|
|
8580
|
+
signal: AbortSignal.timeout(this.options.timeoutMs())
|
|
8581
|
+
});
|
|
8582
|
+
if (!response.ok) throw new ProviderHttpError(await responseError(response), response.status);
|
|
8583
|
+
return parseTraeCheckin(await response.json());
|
|
8584
|
+
}
|
|
8585
|
+
/** Claim today's reward. The only state-changing call in this provider. */
|
|
8586
|
+
async claimCheckin() {
|
|
8587
|
+
const context = await this.checkinContext();
|
|
8588
|
+
if (!context.ok) throw new Error(context.status.message ?? "当前登录方式不能签到。");
|
|
8589
|
+
const response = await this.fetchImpl(`${context.profile.apiBase}/trae/api/v2/ug/checkin_credits/claim`, {
|
|
8590
|
+
method: "POST",
|
|
8591
|
+
headers: {
|
|
8592
|
+
...this.headers(context.token, context.profile),
|
|
8593
|
+
"x-user-region": "CN",
|
|
8594
|
+
"x-device-id": context.identity.deviceId,
|
|
8595
|
+
"x-machine-id": context.identity.machineId
|
|
8596
|
+
},
|
|
8597
|
+
body: JSON.stringify({ req_source: 1 }),
|
|
8598
|
+
signal: AbortSignal.timeout(this.options.timeoutMs())
|
|
8599
|
+
});
|
|
8600
|
+
if (!response.ok) throw new ProviderHttpError(await responseError(response), response.status);
|
|
8601
|
+
const payload = record(await response.json()) ?? {};
|
|
8602
|
+
const code = typeof payload.code === "number" ? payload.code : 0;
|
|
8603
|
+
if (code === 0) {
|
|
8604
|
+
this.invalidateQuota();
|
|
8605
|
+
const reward = num(payload.credits);
|
|
8606
|
+
return {
|
|
8607
|
+
available: true,
|
|
8608
|
+
claimed: true,
|
|
8609
|
+
deviceClaimed: false,
|
|
8610
|
+
...reward === void 0 || reward === 0 ? {} : { reward },
|
|
8611
|
+
...text(payload.message) === void 0 ? {} : { message: text(payload.message) }
|
|
8612
|
+
};
|
|
8613
|
+
}
|
|
8614
|
+
if (code === 9095) return {
|
|
8615
|
+
available: true,
|
|
8616
|
+
claimed: false,
|
|
8617
|
+
deviceClaimed: true,
|
|
8618
|
+
message: "本机今日已签到"
|
|
8619
|
+
};
|
|
8620
|
+
throw new Error(text(payload.message) ?? `签到失败(code ${code})`);
|
|
8621
|
+
}
|
|
8175
8622
|
async *stream(options, model, readImage, onInvalidate) {
|
|
8176
8623
|
this.ensureRegion();
|
|
8177
8624
|
const profile = this.profile();
|
|
@@ -8229,6 +8676,7 @@ var TraeProvider = class {
|
|
|
8229
8676
|
await this.options.store.saveTrae({
|
|
8230
8677
|
accessToken: account.accessToken,
|
|
8231
8678
|
region: target,
|
|
8679
|
+
source: "web",
|
|
8232
8680
|
deviceId: account.deviceId,
|
|
8233
8681
|
machineId: account.machineId,
|
|
8234
8682
|
loginAt: Date.now(),
|
|
@@ -8428,23 +8876,6 @@ function apply(ctx, config) {
|
|
|
8428
8876
|
syncTimer.unref?.();
|
|
8429
8877
|
syncOpenRouter();
|
|
8430
8878
|
ctx.effect(() => () => clearInterval(syncTimer), "llm-adapters: openrouter sync");
|
|
8431
|
-
let lastRaccoonCheckin = Date.now();
|
|
8432
|
-
const checkinMonitor = () => {
|
|
8433
|
-
if (!live.raccoonEnabled()) return;
|
|
8434
|
-
const minutes = live.raccoonCheckinMinutes();
|
|
8435
|
-
if (minutes <= 0) return;
|
|
8436
|
-
if (Date.now() - lastRaccoonCheckin < minutes * 6e4) return;
|
|
8437
|
-
lastRaccoonCheckin = Date.now();
|
|
8438
|
-
(async () => {
|
|
8439
|
-
if (!await raccoonProvider.signedIn()) return;
|
|
8440
|
-
await raccoonProvider.ensureDailyCheckin();
|
|
8441
|
-
})().catch((error) => {
|
|
8442
|
-
ctx.logger?.warn(`llm-adapters: 小浣熊签到监测失败:${error instanceof Error ? error.message : String(error)}`);
|
|
8443
|
-
});
|
|
8444
|
-
};
|
|
8445
|
-
const checkinTimer = setInterval(checkinMonitor, 6e4);
|
|
8446
|
-
checkinTimer.unref?.();
|
|
8447
|
-
ctx.effect(() => () => clearInterval(checkinTimer), "llm-adapters: raccoon check-in monitor");
|
|
8448
8879
|
ctx.inject(["attachments"], (scope) => {
|
|
8449
8880
|
attachments = scope.attachments;
|
|
8450
8881
|
scope.effect(() => () => {
|
|
@@ -8510,4 +8941,4 @@ function apply(ctx, config) {
|
|
|
8510
8941
|
ctx.logger?.info(`llm-adapters: 设置接口已挂载于 ${API_PREFIX}`);
|
|
8511
8942
|
});
|
|
8512
8943
|
}
|
|
8513
|
-
export { API_PREFIX, AccountStore, BRANDS, CODEARTS_BENEFIT_FALLBACK, CODEARTS_BRAND, CODEARTS_CHAT_URL, CODEARTS_CONTEXT_WINDOWS, CODEARTS_DEFAULT_EFFORT, CODEARTS_EFFORTS, CODEARTS_EFFORT_LEVELS, CODEARTS_FALLBACK_CONTEXT_WINDOW, CODEARTS_MAX_OUTPUT_TOKENS, CODEARTS_PROVIDER,
|
|
8944
|
+
export { API_PREFIX, AccountStore, BRANDS, CODEARTS_BENEFIT_FALLBACK, CODEARTS_BRAND, CODEARTS_CHAT_URL, CODEARTS_CONTEXT_WINDOWS, CODEARTS_DEFAULT_EFFORT, CODEARTS_EFFORTS, CODEARTS_EFFORT_LEVELS, CODEARTS_FALLBACK_CONTEXT_WINDOW, CODEARTS_MAX_OUTPUT_TOKENS, CODEARTS_PROVIDER, CODEARTS_SNAP_BASE, CODEARTS_VISION_MODELS, CodeArtsAdapter, CodeArtsAuth, CodeArtsLogin, CodeArtsProvider, Config, EMPTY_OPENROUTER_INDEX, LOOMY_ACCOUNT_BASE, LOOMY_BASE, LOOMY_BRAND, LOOMY_PROVIDER, LOOMY_VERSION, LOOMY_WECHAT_APP_ID, LOOMY_WECHAT_REDIRECT_URI, LocalAdapter, LoomyAdapter, LoomyAuth, LoomyProvider, LoomyQrLogin, ModelCatalog, OpenRouterCatalog, ProviderHttpError, RACCOON_BASE, RACCOON_BRAND, RACCOON_CLIENT_VERSION, RACCOON_IAM_BASE, RACCOON_PROVIDER, RACCOON_QR_APP_NAME, RACCOON_QR_ORIGIN, RaccoonAdapter, RaccoonAuth, RaccoonIamError, RaccoonProvider, RaccoonQrLogin, RefreshTokenExpiredError, TRAE_APP_ID, TRAE_BRAND, TRAE_CHAT_FUNCTIONS, TRAE_CHECKIN_REQ_SOURCE, TRAE_DIRECTORY_FUNCTIONS, TRAE_PROVIDER, TRAE_REGION_PROFILES, TRAE_SOLO_CHAT_PATH, TRAE_WEB_USER_AGENT, TraeAdapter, TraeAuth, TraeProvider, TraeWebLogin, apply, buildAuthorizeUrl, buildLoomyWechatAuthUrl, buildMessages, buildOpenRouterIndex, buildQrMatrix, buildRows, buildTools, clampMaxTokens, codeartsContextWindow, codeartsEfforts, consumeOpenAiStream, createRaccoonQr, decodeJwtFacts, defaultEffortOf, effortLabel, exchangeAuthorizationCode, exchangeRefreshToken, extractWechatUuid, fetchLoomyWechatUuid, generateDpopKeyPair, generatePkcePair, headersForFetch, inject, isBrandId, liveConfig, loomyGatewaySessionValid, machineIdOf, mergeTraeModels, modelKey, name, normalizeCodeArtsModelId, normalizeEffort, normalizeTraeRegion, openAiStream, openTraeStream, otSessionIdOf, parseCodeArtsPackage, parseCodeArtsRoster, parseLoomyCatalog, parseModelName, parseOpenRouterModels, parseRaccoonCatalog, parseTraeCheckin, parseTraeClientStorage, parseTraeGlobalUsage, parseTraeRemoteModels, parseTraeUsage, parseTraeWireModels, pickActiveCredit, pollRaccoonQr, priceFactorOf, priceFactorOfLabel, raccoonAccessTokenValid, raccoonQrContent, raccoonRefreshCandidate, readJwtExpiry, readOpenRouter, readTraeClientLogin, renderQrSvg, resolveCapabilities, resolveConfig, resolveModelId, signDpopJws, signRequestHuawei, sseEvents, startLoginPage, traeClientDataDirs, traeMessages, traeTools };
|