dsh-connect-trae 1.4.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.en.md +15 -9
- package/README.md +15 -9
- package/lib/client.js +268 -76
- package/lib/index.d.ts +376 -51
- package/lib/index.js +869 -235
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createDecipheriv, createHash, randomBytes, randomUUID, timingSafeEqual
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
11
11
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
12
|
-
import { cpus, homedir, release } from "node:os";
|
|
12
|
+
import { cpus, homedir, hostname, release } from "node:os";
|
|
13
13
|
import { createServer } from "node:http";
|
|
14
14
|
import { Readable } from "node:stream";
|
|
15
15
|
import { execFile } from "node:child_process";
|
|
@@ -38,6 +38,52 @@ const FALLBACK_TRAE_MODELS = [
|
|
|
38
38
|
name: "Kimi-K2.6"
|
|
39
39
|
}
|
|
40
40
|
];
|
|
41
|
+
/**
|
|
42
|
+
* Bootstrap catalog for the international (ai) region, captured from the
|
|
43
|
+
* live `coresg-normal.trae.ai/api/remote/v1/models` directory on 2026-09-15
|
|
44
|
+
* (docs/INTL_SG_EVIDENCE.md §3). The two rosters barely overlap (the CN list
|
|
45
|
+
* has no Gemini/GPT/MiniMax entries), so an international account must never
|
|
46
|
+
* be seeded with the CN list. Like the CN fallback it is replaced by the live
|
|
47
|
+
* refresh; image input stays the user's explicit opt-in (`imageModelIds`).
|
|
48
|
+
*/
|
|
49
|
+
const FALLBACK_TRAE_MODELS_AI = [
|
|
50
|
+
{
|
|
51
|
+
id: "gemini-3.1-pro",
|
|
52
|
+
name: "Gemini-3.1-Pro-Preview"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "gemini-3-flash-solo",
|
|
56
|
+
name: "Gemini-3-Flash-Preview"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "minimax-m3",
|
|
60
|
+
name: "MiniMax-M3"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "minimax-m2.7",
|
|
64
|
+
name: "MiniMax-M2.7"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "kimi-k2.5",
|
|
68
|
+
name: "Kimi-K2.5"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "gpt-5.4",
|
|
72
|
+
name: "GPT-5.4"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "gpt-5.2",
|
|
76
|
+
name: "GPT-5.2"
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Static fallback directory for a region. Each region keeps its own model
|
|
81
|
+
* slot in settings; the fallback must match the region so an account never
|
|
82
|
+
* shows the other region's roster.
|
|
83
|
+
*/
|
|
84
|
+
function fallbackModelsFor(region) {
|
|
85
|
+
return region === "ai" ? FALLBACK_TRAE_MODELS_AI : FALLBACK_TRAE_MODELS;
|
|
86
|
+
}
|
|
41
87
|
/** Exact DSH modalities for one catalog entry; absent metadata is text-only. */
|
|
42
88
|
function traeInputModalities(model) {
|
|
43
89
|
return [...model.input ?? ["text"]];
|
|
@@ -167,7 +213,15 @@ function deriveCatalog(catalog, enabled, budgets = {}) {
|
|
|
167
213
|
return applyContextBudgets(enabled.size === 0 ? catalog : catalog.filter((model) => enabled.has(model.id)), budgets);
|
|
168
214
|
}
|
|
169
215
|
var TraeCatalog = class {
|
|
170
|
-
models
|
|
216
|
+
models;
|
|
217
|
+
/**
|
|
218
|
+
* @param region Seeds the static fallback for this region; each region's
|
|
219
|
+
* provider must never serve the other region's roster before its first live
|
|
220
|
+
* refresh lands.
|
|
221
|
+
*/
|
|
222
|
+
constructor(region = "cn") {
|
|
223
|
+
this.models = fallbackModelsFor(region);
|
|
224
|
+
}
|
|
171
225
|
current() {
|
|
172
226
|
return this.models;
|
|
173
227
|
}
|
|
@@ -181,7 +235,28 @@ var TraeCatalog = class {
|
|
|
181
235
|
};
|
|
182
236
|
//#endregion
|
|
183
237
|
//#region src/adapter.ts
|
|
238
|
+
/** Provider route this bundle owns for the domestic (CN) gateway. */
|
|
184
239
|
const TRAE_PROVIDER = "trae";
|
|
240
|
+
/**
|
|
241
|
+
* Provider route this bundle owns for the international (trae.ai) gateway.
|
|
242
|
+
* Named `-global` to match the plugin family's convention (`workbuddy-global`);
|
|
243
|
+
* the internal region bucket stays `ai`, which is the gateway/protocol name.
|
|
244
|
+
*/
|
|
245
|
+
const TRAE_AI_PROVIDER = "trae-global";
|
|
246
|
+
/** The provider id each region registers as. */
|
|
247
|
+
const TRAE_PROVIDERS = {
|
|
248
|
+
cn: TRAE_PROVIDER,
|
|
249
|
+
ai: TRAE_AI_PROVIDER
|
|
250
|
+
};
|
|
251
|
+
/** Human-readable provider names, shown in the DSH model picker. */
|
|
252
|
+
const TRAE_PROVIDER_DISPLAY_NAMES = {
|
|
253
|
+
cn: "Trae",
|
|
254
|
+
ai: "Trae Global"
|
|
255
|
+
};
|
|
256
|
+
/** Region a provider route id belongs to. */
|
|
257
|
+
function regionOfTraeProvider(provider) {
|
|
258
|
+
for (const [region, id] of Object.entries(TRAE_PROVIDERS)) if (id === provider) return region;
|
|
259
|
+
}
|
|
185
260
|
const TRAE_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
186
261
|
const INERT_AUTH = {
|
|
187
262
|
credentials: {
|
|
@@ -206,12 +281,12 @@ const REQUEST_IMAGE_BUDGETS = {
|
|
|
206
281
|
requestImagePixelBudget: 4194304,
|
|
207
282
|
requestImageMaxBytes: 1048576
|
|
208
283
|
};
|
|
209
|
-
function toPiModel(info, baseUrl) {
|
|
284
|
+
function toPiModel(info, baseUrl, providerId) {
|
|
210
285
|
return {
|
|
211
286
|
id: info.id,
|
|
212
287
|
name: traeModelDisplayName(info),
|
|
213
288
|
api: "openai-completions",
|
|
214
|
-
provider:
|
|
289
|
+
provider: providerId,
|
|
215
290
|
baseUrl,
|
|
216
291
|
input: traeInputModalities(info),
|
|
217
292
|
cost: {
|
|
@@ -236,11 +311,13 @@ function toPiModel(info, baseUrl) {
|
|
|
236
311
|
};
|
|
237
312
|
}
|
|
238
313
|
function createTraeAdapter(options) {
|
|
239
|
-
const
|
|
314
|
+
const providerId = options.provider ?? "trae";
|
|
315
|
+
const providerName = options.displayName ?? "Trae";
|
|
316
|
+
const buildModels = () => options.catalog.current().map((info) => toPiModel(info, `${options.shim.baseUrl()}/v1`, providerId));
|
|
240
317
|
const provider = {
|
|
241
318
|
...createProvider({
|
|
242
|
-
id:
|
|
243
|
-
name:
|
|
319
|
+
id: providerId,
|
|
320
|
+
name: providerName,
|
|
244
321
|
auth: { apiKey: {
|
|
245
322
|
name: "Trae loopback secret",
|
|
246
323
|
async resolve({ credential }) {
|
|
@@ -257,8 +334,8 @@ function createTraeAdapter(options) {
|
|
|
257
334
|
getModels: () => buildModels()
|
|
258
335
|
};
|
|
259
336
|
const profile = {
|
|
260
|
-
provider:
|
|
261
|
-
displayName:
|
|
337
|
+
provider: providerId,
|
|
338
|
+
displayName: providerName,
|
|
262
339
|
streamIdleTimeoutMs: TRAE_STREAM_IDLE_TIMEOUT_MS,
|
|
263
340
|
retryPolicy: resolveRetryPolicy(void 0, "dsh-connect-trae retryPolicy"),
|
|
264
341
|
configuredMaxTokens: /* @__PURE__ */ new Map(),
|
|
@@ -266,7 +343,7 @@ function createTraeAdapter(options) {
|
|
|
266
343
|
...REQUEST_IMAGE_BUDGETS,
|
|
267
344
|
piProvider: provider
|
|
268
345
|
};
|
|
269
|
-
let profiles = /* @__PURE__ */ new Map([[
|
|
346
|
+
let profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
270
347
|
return {
|
|
271
348
|
adapter: new PiAiAdapter({
|
|
272
349
|
profiles: () => profiles,
|
|
@@ -275,7 +352,7 @@ function createTraeAdapter(options) {
|
|
|
275
352
|
...options.resolveAttachments === void 0 ? {} : { resolveAttachments: options.resolveAttachments }
|
|
276
353
|
}),
|
|
277
354
|
invalidate() {
|
|
278
|
-
profiles = /* @__PURE__ */ new Map([[
|
|
355
|
+
profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
|
|
279
356
|
}
|
|
280
357
|
};
|
|
281
358
|
}
|
|
@@ -734,12 +811,86 @@ function traeCliCandidates(platform = process.platform, home = homedir(), env =
|
|
|
734
811
|
return result;
|
|
735
812
|
}
|
|
736
813
|
//#endregion
|
|
814
|
+
//#region src/region.ts
|
|
815
|
+
/**
|
|
816
|
+
* Verified gateway bases per region (docs/INTL_SG_EVIDENCE.md §2).
|
|
817
|
+
* The AI gateway is shared by both international installs (Trae desktop and
|
|
818
|
+
* TRAE SOLO); its mchost shards (`api16/api22-normal-alisg.mchost.guru`)
|
|
819
|
+
* answer the same bytes but stay internal — `coresg-normal.trae.ai` is the
|
|
820
|
+
* single stable entry point.
|
|
821
|
+
*/
|
|
822
|
+
const REGION_GATEWAYS = {
|
|
823
|
+
cn: {
|
|
824
|
+
chat: "https://trae-api-cn.mchost.guru",
|
|
825
|
+
remote: "https://solo.trae.cn/api/remote/v1",
|
|
826
|
+
pay: "https://api.trae.cn"
|
|
827
|
+
},
|
|
828
|
+
ai: {
|
|
829
|
+
chat: "https://coresg-normal.trae.ai",
|
|
830
|
+
remote: "https://coresg-normal.trae.ai/api/remote/v1",
|
|
831
|
+
pay: "https://growsg-normal.trae.ai"
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
/** Region for an edition label: the international installs belong to `ai`. */
|
|
835
|
+
function regionOfEdition(edition) {
|
|
836
|
+
return edition === "sg" || edition === "solo-sg" ? "ai" : "cn";
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Region from the credential's `userRegion` claim. The desktop storage spells
|
|
840
|
+
* it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
|
|
841
|
+
* spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
|
|
842
|
+
*/
|
|
843
|
+
function regionOfUserRegion(value) {
|
|
844
|
+
const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
|
|
845
|
+
if (typeof raw !== "string") return void 0;
|
|
846
|
+
const lowered = raw.trim().toLowerCase();
|
|
847
|
+
if (lowered === "cn") return "cn";
|
|
848
|
+
if (lowered === "sg" || lowered === "ai") return "ai";
|
|
849
|
+
}
|
|
850
|
+
/** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
|
|
851
|
+
function regionOfHost(host) {
|
|
852
|
+
if (host === void 0) return void 0;
|
|
853
|
+
const trimmed = host.trim();
|
|
854
|
+
if (trimmed === "") return void 0;
|
|
855
|
+
let hostname;
|
|
856
|
+
try {
|
|
857
|
+
hostname = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`).hostname;
|
|
858
|
+
} catch {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (hostname === "trae.ai" || hostname.endsWith(".trae.ai")) return "ai";
|
|
862
|
+
if (hostname === "trae.cn" || hostname.endsWith(".trae.cn") || hostname.endsWith(".trae.com.cn")) return "cn";
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Region of a credential: the `userRegion` claim wins, the host suffix is the
|
|
866
|
+
* fallback, and the edition label is the last resort. Every level is derived
|
|
867
|
+
* from data the credential itself carries, so no user configuration is needed.
|
|
868
|
+
*/
|
|
869
|
+
function regionOfCredential(credential) {
|
|
870
|
+
return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host) ?? regionOfEdition(credential.edition);
|
|
871
|
+
}
|
|
872
|
+
//#endregion
|
|
737
873
|
//#region src/auth.ts
|
|
738
874
|
/** Host used for CLI tokens, which carry no host claim of their own. */
|
|
739
875
|
const CLI_DEFAULT_HOST = "https://api.trae.cn";
|
|
740
876
|
const OWN_VERSION = 1;
|
|
741
877
|
const TRAE_AUTH_FILENAME = ".trae-auth.json";
|
|
742
|
-
|
|
878
|
+
/** Prefix of the plugin-owned per-region credential copies. */
|
|
879
|
+
const TRAE_OWN_PREFIX = ".trae-auth";
|
|
880
|
+
/**
|
|
881
|
+
* Plugin-owned copy path for one region. Each region's store refreshes into
|
|
882
|
+
* its own file so two simultaneously signed-in regions never overwrite each
|
|
883
|
+
* other's refreshed token.
|
|
884
|
+
*/
|
|
885
|
+
function traeOwnAuthPath(region) {
|
|
886
|
+
return join(resolveDshHome(), `${TRAE_OWN_PREFIX}.${region}.json`);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Pre-dual-provider single-copy path. Still read as a migration source (a
|
|
890
|
+
* legacy credential serves the region it belongs to until that region's own
|
|
891
|
+
* first refresh writes the per-region file), and removed by `logout`.
|
|
892
|
+
*/
|
|
893
|
+
function legacyTraeOwnAuthPath() {
|
|
743
894
|
return join(resolveDshHome(), TRAE_AUTH_FILENAME);
|
|
744
895
|
}
|
|
745
896
|
function optionalString(value) {
|
|
@@ -753,6 +904,11 @@ function timeToMs(value) {
|
|
|
753
904
|
const parsed = Date.parse(value);
|
|
754
905
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
755
906
|
}
|
|
907
|
+
/** Extract the `userRegion.region` string from either on-disk shape. */
|
|
908
|
+
function userRegionOf(value) {
|
|
909
|
+
const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
|
|
910
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
|
|
911
|
+
}
|
|
756
912
|
function normalizeTraeCredential(raw, edition, source) {
|
|
757
913
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
|
|
758
914
|
const value = raw;
|
|
@@ -761,6 +917,7 @@ function normalizeTraeCredential(raw, edition, source) {
|
|
|
761
917
|
const expiresAtMs = timeToMs(value["expiredAt"] ?? value["expiresAt"]) ?? 0;
|
|
762
918
|
const refreshExpiresAtMs = timeToMs(value["refreshExpiredAt"] ?? value["refreshExpiresAt"]);
|
|
763
919
|
const refreshToken = optionalString(value["refreshToken"]);
|
|
920
|
+
const userRegion = userRegionOf(value["userRegion"]);
|
|
764
921
|
const accountName = optionalString((typeof value["account"] === "object" && value["account"] !== null && !Array.isArray(value["account"]) ? value["account"] : void 0)?.["username"]);
|
|
765
922
|
return {
|
|
766
923
|
accessToken,
|
|
@@ -768,6 +925,7 @@ function normalizeTraeCredential(raw, edition, source) {
|
|
|
768
925
|
userId: optionalString(value["userId"]) ?? "",
|
|
769
926
|
...accountName === void 0 ? {} : { accountName },
|
|
770
927
|
host: optionalString(value["host"]) ?? "",
|
|
928
|
+
...userRegion === void 0 ? {} : { userRegion },
|
|
771
929
|
expiresAtMs,
|
|
772
930
|
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
773
931
|
edition,
|
|
@@ -790,6 +948,7 @@ function parseOwn(text) {
|
|
|
790
948
|
refreshToken: stored["refreshToken"],
|
|
791
949
|
userId: stored["userId"],
|
|
792
950
|
host: stored["host"],
|
|
951
|
+
userRegion: stored["userRegion"],
|
|
793
952
|
account: stored["accountName"] === void 0 ? void 0 : { username: stored["accountName"] },
|
|
794
953
|
expiredAt: stored["expiresAtMs"],
|
|
795
954
|
refreshExpiredAt: stored["refreshExpiresAtMs"]
|
|
@@ -802,7 +961,10 @@ var TraeCredentialStore = class {
|
|
|
802
961
|
storagePathOverride;
|
|
803
962
|
edition;
|
|
804
963
|
accountId;
|
|
805
|
-
|
|
964
|
+
region;
|
|
965
|
+
ownPathExplicit;
|
|
966
|
+
legacyOwnPath;
|
|
967
|
+
legacyOwnPathExplicit;
|
|
806
968
|
refresh;
|
|
807
969
|
refreshMarginMs;
|
|
808
970
|
inflight;
|
|
@@ -810,10 +972,45 @@ var TraeCredentialStore = class {
|
|
|
810
972
|
this.storagePathOverride = options.storagePath;
|
|
811
973
|
this.edition = options.edition ?? "auto";
|
|
812
974
|
this.accountId = options.accountId;
|
|
813
|
-
this.
|
|
975
|
+
this.region = options.region;
|
|
976
|
+
this.ownPathExplicit = options.ownPath;
|
|
977
|
+
this.legacyOwnPath = options.legacyOwnPath ?? legacyTraeOwnAuthPath();
|
|
978
|
+
this.legacyOwnPathExplicit = options.legacyOwnPath;
|
|
814
979
|
this.refresh = options.refresh;
|
|
815
980
|
this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
|
|
816
981
|
}
|
|
982
|
+
/** Whether a credential's own claim belongs to this store's region. */
|
|
983
|
+
matchesRegion(credential) {
|
|
984
|
+
return this.region === void 0 || regionOfCredential(credential) === this.region;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* The path this store refreshes into: the per-region file for a
|
|
988
|
+
* region-scoped store, the legacy single file otherwise, or an explicitly
|
|
989
|
+
* injected path in tests.
|
|
990
|
+
*/
|
|
991
|
+
ownAuthPath() {
|
|
992
|
+
if (this.ownPathExplicit !== void 0) return this.ownPathExplicit;
|
|
993
|
+
return this.region !== void 0 ? traeOwnAuthPath(this.region) : this.legacyOwnPath;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Every plugin-owned copy to read, most preferred first. A region-scoped
|
|
997
|
+
* store reads the legacy single copy as its migration source (readAll's
|
|
998
|
+
* region filter drops it when it carries the other region's credential); an
|
|
999
|
+
* unscoped store reads everything so diagnostics see both regions.
|
|
1000
|
+
*
|
|
1001
|
+
* With an explicitly injected own path the legacy source is read ONLY when
|
|
1002
|
+
* it was injected too — a test that pins one file must not accidentally see
|
|
1003
|
+
* the real machine's legacy copy.
|
|
1004
|
+
*/
|
|
1005
|
+
ownCandidates() {
|
|
1006
|
+
if (this.ownPathExplicit !== void 0) return this.legacyOwnPathExplicit !== void 0 ? [this.ownPathExplicit, this.legacyOwnPathExplicit] : [this.ownPathExplicit];
|
|
1007
|
+
if (this.region !== void 0) return [traeOwnAuthPath(this.region), this.legacyOwnPath];
|
|
1008
|
+
return [
|
|
1009
|
+
this.legacyOwnPath,
|
|
1010
|
+
traeOwnAuthPath("cn"),
|
|
1011
|
+
traeOwnAuthPath("ai")
|
|
1012
|
+
];
|
|
1013
|
+
}
|
|
817
1014
|
setSource(storagePath, edition = "auto", accountId) {
|
|
818
1015
|
this.storagePathOverride = storagePath;
|
|
819
1016
|
this.edition = edition;
|
|
@@ -838,7 +1035,8 @@ var TraeCredentialStore = class {
|
|
|
838
1035
|
}];
|
|
839
1036
|
}
|
|
840
1037
|
const all = traeStorageCandidates();
|
|
841
|
-
|
|
1038
|
+
const cliEdition = "cn";
|
|
1039
|
+
return this.edition === "auto" ? all.filter((candidate) => candidate.source === "desktop" || candidate.edition === cliEdition) : all.filter((candidate) => candidate.edition === this.edition && (candidate.source === "desktop" || candidate.edition === cliEdition));
|
|
842
1040
|
}
|
|
843
1041
|
/**
|
|
844
1042
|
* Deterministic default when no account is explicitly selected: the first
|
|
@@ -857,6 +1055,7 @@ var TraeCredentialStore = class {
|
|
|
857
1055
|
id: traeAccountId(credential),
|
|
858
1056
|
accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`),
|
|
859
1057
|
edition: credential.edition,
|
|
1058
|
+
region: regionOfCredential(credential),
|
|
860
1059
|
source: credential.source,
|
|
861
1060
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
862
1061
|
selected: selectedExists ? traeAccountId(credential) === this.accountId : credential === defaultSelected
|
|
@@ -895,16 +1094,34 @@ var TraeCredentialStore = class {
|
|
|
895
1094
|
} catch {}
|
|
896
1095
|
return false;
|
|
897
1096
|
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Remove every plugin-owned copy this store could read (per-region file,
|
|
1099
|
+
* legacy single file, and their lock siblings); the desktop storage files
|
|
1100
|
+
* are untouched. A region store's logout therefore also clears the legacy
|
|
1101
|
+
* migration source — deliberate: `logout` is the user's "forget what the
|
|
1102
|
+
* plugin stored" action, not a per-account toggle.
|
|
1103
|
+
*/
|
|
898
1104
|
async logout() {
|
|
899
|
-
|
|
900
|
-
|
|
1105
|
+
for (const path of this.ownCandidates()) {
|
|
1106
|
+
await rm(path, { force: true });
|
|
1107
|
+
await rm(`${path}.lock`, { force: true });
|
|
1108
|
+
}
|
|
901
1109
|
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Every local credential of this store's region, deduplicated by account id.
|
|
1112
|
+
* A region-scoped store sees only its own region's credentials: the other
|
|
1113
|
+
* region's accounts are invisible to selection, refresh, and status alike,
|
|
1114
|
+
* which is what keeps the two regions' providers from cross-billing.
|
|
1115
|
+
*/
|
|
902
1116
|
async readAll() {
|
|
903
1117
|
const { credentials: desktop } = await this.readDesktopAll();
|
|
904
|
-
const
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
|
|
1118
|
+
const credentials = [...desktop.filter((credential) => this.matchesRegion(credential))];
|
|
1119
|
+
for (const own of await this.readOwns()) {
|
|
1120
|
+
if (!this.matchesRegion(own)) continue;
|
|
1121
|
+
if (credentials.some((credential) => traeAccountId(credential) === traeAccountId(own))) continue;
|
|
1122
|
+
credentials.push(own);
|
|
1123
|
+
}
|
|
1124
|
+
return credentials;
|
|
908
1125
|
}
|
|
909
1126
|
/**
|
|
910
1127
|
* Which paths were tried and why each one failed. Read-only and token-free:
|
|
@@ -949,13 +1166,14 @@ var TraeCredentialStore = class {
|
|
|
949
1166
|
credentialFrom(candidate, text) {
|
|
950
1167
|
let credential;
|
|
951
1168
|
if (candidate.source === "cli") {
|
|
1169
|
+
if (regionOfEdition(candidate.edition) !== "cn") throw new Error(`Trae CLI tokens are only verified for the CN region; ${candidate.edition} CLI homes are not supported yet`);
|
|
952
1170
|
const claims = parseTraeCliToken(text);
|
|
953
1171
|
credential = normalizeTraeCredential({
|
|
954
1172
|
token: claims.accessToken,
|
|
955
1173
|
userId: claims.userId,
|
|
956
1174
|
host: CLI_DEFAULT_HOST,
|
|
957
1175
|
expiredAt: claims.expiresAtMs
|
|
958
|
-
}, candidate.edition
|
|
1176
|
+
}, candidate.edition, "cli");
|
|
959
1177
|
} else credential = normalizeTraeCredential(parseTraeStorageDocument(text), candidate.edition, "desktop");
|
|
960
1178
|
if (credential === void 0) throw new Error(`${candidate.source} candidate could not be normalized into a credential`);
|
|
961
1179
|
return credential;
|
|
@@ -982,12 +1200,17 @@ var TraeCredentialStore = class {
|
|
|
982
1200
|
failures
|
|
983
1201
|
};
|
|
984
1202
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1203
|
+
/**
|
|
1204
|
+
* Every readable plugin-owned copy, in candidate order; absent or corrupt
|
|
1205
|
+
* files are skipped rather than propagated.
|
|
1206
|
+
*/
|
|
1207
|
+
async readOwns() {
|
|
1208
|
+
const copies = [];
|
|
1209
|
+
for (const path of this.ownCandidates()) try {
|
|
1210
|
+
const parsed = parseOwn(await readFile(path, "utf8"));
|
|
1211
|
+
if (parsed !== void 0) copies.push(parsed);
|
|
1212
|
+
} catch {}
|
|
1213
|
+
return copies;
|
|
991
1214
|
}
|
|
992
1215
|
async refreshNow(credential) {
|
|
993
1216
|
if (credential.refreshToken === void 0 || credential.refreshExpiresAtMs !== void 0 && credential.refreshExpiresAtMs <= Date.now()) {
|
|
@@ -1005,8 +1228,9 @@ var TraeCredentialStore = class {
|
|
|
1005
1228
|
...outcome.host === void 0 ? {} : { host: outcome.host },
|
|
1006
1229
|
source: "dsh"
|
|
1007
1230
|
};
|
|
1008
|
-
|
|
1009
|
-
|
|
1231
|
+
const ownPath = this.ownAuthPath();
|
|
1232
|
+
await withFileLock(ownPath, async () => {
|
|
1233
|
+
await writeFileAtomic(ownPath, `${JSON.stringify({
|
|
1010
1234
|
version: OWN_VERSION,
|
|
1011
1235
|
credential: refreshed
|
|
1012
1236
|
}, null, 2)}\n`, {
|
|
@@ -1023,29 +1247,60 @@ var TraeCredentialStore = class {
|
|
|
1023
1247
|
};
|
|
1024
1248
|
//#endregion
|
|
1025
1249
|
//#region src/refresh.ts
|
|
1026
|
-
const
|
|
1027
|
-
cn:
|
|
1028
|
-
|
|
1250
|
+
const REFRESH_CONTRACT = {
|
|
1251
|
+
cn: {
|
|
1252
|
+
path: "/cloudide/api/v3/trae/oauth/ExchangeToken",
|
|
1253
|
+
clientId: "ono9krqynydwx5",
|
|
1254
|
+
deviceInfo: false
|
|
1255
|
+
},
|
|
1256
|
+
sg: {
|
|
1257
|
+
path: "/cloudide/api/v3/trae/oauth/ExchangeToken",
|
|
1258
|
+
clientId: "ono9krqynydwx5",
|
|
1259
|
+
deviceInfo: false
|
|
1260
|
+
},
|
|
1261
|
+
solo: {
|
|
1262
|
+
path: "/cloudide/api/v3/trae/oauth/ExchangeToken",
|
|
1263
|
+
clientId: "ono9krqynydwx5",
|
|
1264
|
+
deviceInfo: false
|
|
1265
|
+
},
|
|
1266
|
+
"solo-sg": {
|
|
1267
|
+
path: "/trae/api/v3/oauth/ExchangeToken",
|
|
1268
|
+
clientId: "en1oxy7wnw8j9n",
|
|
1269
|
+
deviceInfo: true
|
|
1270
|
+
}
|
|
1029
1271
|
};
|
|
1030
1272
|
function normalizeHost(host) {
|
|
1031
1273
|
const value = host.trim();
|
|
1032
1274
|
if (value === "") throw new Error("Trae refresh host is missing");
|
|
1033
1275
|
return value.replace(/\/$/, "");
|
|
1034
1276
|
}
|
|
1035
|
-
/**
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1277
|
+
/**
|
|
1278
|
+
* Exchange a refresh token for a fresh access token, following the calling
|
|
1279
|
+
* edition's verified contract. `device` is only used by editions whose
|
|
1280
|
+
* official client sends a DeviceInfo body; when it cannot be resolved the
|
|
1281
|
+
* field is omitted rather than sent empty.
|
|
1282
|
+
*/
|
|
1283
|
+
async function refreshTraeCredential(credential, signal, device) {
|
|
1284
|
+
const contract = REFRESH_CONTRACT[credential.edition];
|
|
1285
|
+
if (contract === void 0) throw new Error(`Trae ${credential.edition} refresh contract is not verified`);
|
|
1039
1286
|
if (credential.refreshToken === void 0) throw new Error("Trae refresh token is missing");
|
|
1040
|
-
const
|
|
1287
|
+
const body = {
|
|
1288
|
+
ClientID: contract.clientId,
|
|
1289
|
+
ClientSecret: "-",
|
|
1290
|
+
RefreshToken: credential.refreshToken,
|
|
1291
|
+
UserID: credential.userId
|
|
1292
|
+
};
|
|
1293
|
+
if (contract.deviceInfo && device !== void 0) body["DeviceInfo"] = {
|
|
1294
|
+
DeviceID: device.deviceId,
|
|
1295
|
+
MachineID: device.machineId,
|
|
1296
|
+
PlatformCode: credential.edition === "solo-sg" ? "SOLO_PC" : "TRAE",
|
|
1297
|
+
DeviceType: "PC",
|
|
1298
|
+
DeviceName: hostname()
|
|
1299
|
+
};
|
|
1300
|
+
const response = await fetch(`${normalizeHost(credential.host)}${contract.path}`, {
|
|
1041
1301
|
method: "POST",
|
|
1042
1302
|
headers: { "Content-Type": "application/json" },
|
|
1043
|
-
body: JSON.stringify(
|
|
1044
|
-
ClientID: clientId,
|
|
1045
|
-
RefreshToken: credential.refreshToken,
|
|
1046
|
-
ClientSecret: "-",
|
|
1047
|
-
UserID: credential.userId
|
|
1048
|
-
}),
|
|
1303
|
+
body: JSON.stringify(body),
|
|
1049
1304
|
signal: signal ?? AbortSignal.timeout(3e4)
|
|
1050
1305
|
});
|
|
1051
1306
|
if (!response.ok) throw new Error(`Trae token refresh failed (http ${response.status})`);
|
|
@@ -1087,7 +1342,12 @@ async function readTraeIdentity(candidate, options = {}) {
|
|
|
1087
1342
|
if (machineId === void 0) throw new Error(`Trae ${candidate.edition} has no stable machine identity`);
|
|
1088
1343
|
const deviceId = dcDevice ?? devDevice ?? createHash("sha256").update(machineId).digest("hex").slice(0, 32);
|
|
1089
1344
|
const buildVersion = nonEmpty(storage["iCubeLastVersion"]);
|
|
1090
|
-
const appName =
|
|
1345
|
+
const appName = {
|
|
1346
|
+
cn: "Trae CN",
|
|
1347
|
+
sg: "Trae",
|
|
1348
|
+
solo: "TRAE SOLO CN",
|
|
1349
|
+
"solo-sg": "TRAE SOLO"
|
|
1350
|
+
}[candidate.edition];
|
|
1091
1351
|
const productPaths = [];
|
|
1092
1352
|
if (appName !== void 0 && (platform === "darwin" || platform === "win32")) {
|
|
1093
1353
|
if (platform === "darwin") productPaths.push(join("/Applications", `${appName}.app`, "Contents", "Resources", "app", "product.json"));
|
|
@@ -1121,6 +1381,8 @@ async function readTraeIdentity(candidate, options = {}) {
|
|
|
1121
1381
|
function isFileMissing(error) {
|
|
1122
1382
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1123
1383
|
}
|
|
1384
|
+
/** Prefix of the "no candidate exists on disk" error; see {@link resolveTraeIdentity}. */
|
|
1385
|
+
const STORAGE_MISSING_PREFIX = "Trae storage was not found";
|
|
1124
1386
|
/**
|
|
1125
1387
|
* Try candidates in order and return the first that yields a valid identity;
|
|
1126
1388
|
* fail hard only when none do. Mirrors the credential store's skip-missing
|
|
@@ -1139,9 +1401,96 @@ async function pickTraeStorageIdentity(candidates, options = {}) {
|
|
|
1139
1401
|
if (!isFileMissing(error)) anyPresent = true;
|
|
1140
1402
|
}
|
|
1141
1403
|
const tried = candidates.map((item) => item.path).join(" or ");
|
|
1142
|
-
if (!anyPresent) throw new Error(
|
|
1404
|
+
if (!anyPresent) throw new Error(`${STORAGE_MISSING_PREFIX} (${tried})`);
|
|
1143
1405
|
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Trae identity could not be resolved (${tried})`);
|
|
1144
1406
|
}
|
|
1407
|
+
/** CLI dotfile home per edition; the CLI keeps its own home, not an Application Support entry. */
|
|
1408
|
+
const CLI_HOME_BY_EDITION = {
|
|
1409
|
+
cn: ".trae-cn",
|
|
1410
|
+
solo: ".trae-cn",
|
|
1411
|
+
sg: ".trae",
|
|
1412
|
+
"solo-sg": ".trae"
|
|
1413
|
+
};
|
|
1414
|
+
/**
|
|
1415
|
+
* Deterministic identity for a machine that only has the Trae CLI (`traecli`).
|
|
1416
|
+
*
|
|
1417
|
+
* A CLI-only machine has no desktop `storage.json`, so the desktop identity
|
|
1418
|
+
* reader has nothing to read — yet the request headers still need stable
|
|
1419
|
+
* machine/device ids. Everything here comes from identifiers the CLI itself
|
|
1420
|
+
* persists (never a per-request random value):
|
|
1421
|
+
*
|
|
1422
|
+
* - `argv.json`'s `crash-reporter-id` — a stable per-install UUID the CLI
|
|
1423
|
+
* writes on first run; used directly as the device id.
|
|
1424
|
+
* - `builtin/ide_version.json`'s `version` — the CLI build, sent as
|
|
1425
|
+
* `x-app-version` (the desktop reader gets this from `product.json`).
|
|
1426
|
+
* - a SHA-256 over the device id, host name and user name for `machineId`,
|
|
1427
|
+
* matching the 64-char hex shape the official clients send.
|
|
1428
|
+
*
|
|
1429
|
+
* When `crash-reporter-id` is absent the device id falls back to the same
|
|
1430
|
+
* hash (still deterministic). A machine with no CLI home at all keeps failing
|
|
1431
|
+
* loudly — a fabricated identity is worse than a visible "not signed in".
|
|
1432
|
+
*/
|
|
1433
|
+
async function readTraeCliIdentity(edition, options = {}) {
|
|
1434
|
+
const platform = options.platform ?? process.platform;
|
|
1435
|
+
const home = options.home ?? homedir();
|
|
1436
|
+
const env = options.env ?? process.env;
|
|
1437
|
+
const cliHome = join(home, CLI_HOME_BY_EDITION[edition]);
|
|
1438
|
+
const argv = await readJsonFile(join(cliHome, "argv.json"));
|
|
1439
|
+
const version = await readJsonFile(join(cliHome, "builtin", "ide_version.json"));
|
|
1440
|
+
const crashReporterId = nonEmpty(argv?.["crash-reporter-id"]);
|
|
1441
|
+
const host = nonEmpty(env["HOSTNAME"]) ?? await readHostname() ?? "unknown-host";
|
|
1442
|
+
const user = nonEmpty(env["USER"]) ?? nonEmpty(env["USERNAME"]) ?? "unknown-user";
|
|
1443
|
+
const deviceId = crashReporterId ?? createHash("sha256").update(`trae-cli\0${host}\0${user}`).digest("hex").slice(0, 32);
|
|
1444
|
+
const machineId = createHash("sha256").update(`trae-cli-machine\0${deviceId}\0${host}`).digest("hex");
|
|
1445
|
+
const appVersion = nonEmpty(version?.["version"]);
|
|
1446
|
+
const deviceCpu = cpus()[0]?.model.split(" ")[0];
|
|
1447
|
+
const osVersion = `${platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform} ${release()}`;
|
|
1448
|
+
return {
|
|
1449
|
+
edition,
|
|
1450
|
+
machineId,
|
|
1451
|
+
deviceId,
|
|
1452
|
+
...appVersion === void 0 ? {} : { appVersion },
|
|
1453
|
+
...deviceCpu === void 0 ? {} : { deviceCpu },
|
|
1454
|
+
osVersion,
|
|
1455
|
+
platform
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
/** Read and parse a JSON file, tolerating absence and malformed content. */
|
|
1459
|
+
async function readJsonFile(path) {
|
|
1460
|
+
try {
|
|
1461
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1462
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
1463
|
+
} catch {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
/** OS host name, or undefined when unavailable. */
|
|
1468
|
+
async function readHostname() {
|
|
1469
|
+
try {
|
|
1470
|
+
const { hostname } = await import("node:os");
|
|
1471
|
+
return nonEmpty(hostname());
|
|
1472
|
+
} catch {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Resolve the request identity for one region: the desktop storage identity
|
|
1478
|
+
* when any desktop install exists, otherwise the CLI home's deterministic
|
|
1479
|
+
* identity (see {@link readTraeCliIdentity}).
|
|
1480
|
+
*
|
|
1481
|
+
* This is the seam the WSL2 / CLI-only case needs: `traecli` writes its token
|
|
1482
|
+
* to `~/.trae-cn/trae-jwt-token` and no `storage.json` exists anywhere, so
|
|
1483
|
+
* desktop-only resolution used to fail every directory refresh and chat
|
|
1484
|
+
* request even though the account itself was found.
|
|
1485
|
+
*/
|
|
1486
|
+
async function resolveTraeIdentity(candidates, edition, options = {}) {
|
|
1487
|
+
try {
|
|
1488
|
+
return await pickTraeStorageIdentity(candidates, options);
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
if (!(error instanceof Error) || !error.message.startsWith(STORAGE_MISSING_PREFIX)) throw error;
|
|
1491
|
+
return readTraeCliIdentity(edition, options);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1145
1494
|
/** Headers derived from actual persisted identity, never a new random identity per request. */
|
|
1146
1495
|
function identityHeaders(identity) {
|
|
1147
1496
|
return {
|
|
@@ -1381,8 +1730,13 @@ function normalizeTraeVersionCode(buildVersion) {
|
|
|
1381
1730
|
const trimmed = buildVersion.trim();
|
|
1382
1731
|
return /^\d+$/.test(trimmed) ? trimmed : TRAE_VERSION_CODE_FALLBACK;
|
|
1383
1732
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1733
|
+
/**
|
|
1734
|
+
* Headers shared by every Trae edition. The same shape is accepted by both
|
|
1735
|
+
* gateways (verified read-only on the international gateway 2026-09-15,
|
|
1736
|
+
* docs/INTL_SG_EVIDENCE.md §2.3: identical `x-app-id`, identity headers, and
|
|
1737
|
+
* `Cloud-IDE-JWT` auth), so there is no per-edition branch any more.
|
|
1738
|
+
*/
|
|
1739
|
+
function buildTraeHeaders(credential, identity, options = {}) {
|
|
1386
1740
|
const requestId = options.requestId ?? randomUUID();
|
|
1387
1741
|
const traceId = requestId.replaceAll("-", "").slice(0, 32);
|
|
1388
1742
|
const profile = options.profile ?? "agent-task";
|
|
@@ -1426,6 +1780,8 @@ function buildTraeCnHeaders(credential, identity, options = {}) {
|
|
|
1426
1780
|
"Accept": "text/event-stream"
|
|
1427
1781
|
};
|
|
1428
1782
|
}
|
|
1783
|
+
/** Backwards-compatible alias; the headers are no longer CN-specific. */
|
|
1784
|
+
const buildTraeCnHeaders = buildTraeHeaders;
|
|
1429
1785
|
function traeEndpoint(baseUrl, path) {
|
|
1430
1786
|
return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
1431
1787
|
}
|
|
@@ -1526,14 +1882,17 @@ var TraeSoloUpstreamClient = class {
|
|
|
1526
1882
|
}
|
|
1527
1883
|
async fetchModels(signal) {
|
|
1528
1884
|
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
1529
|
-
const
|
|
1885
|
+
const region = regionOfCredential(credential);
|
|
1886
|
+
const base = this.options.baseUrl ?? REGION_GATEWAYS[region].chat;
|
|
1887
|
+
const directoryFunction = region === "ai" ? "solo_agent" : TRAE_SOLO_FUNCTION;
|
|
1888
|
+
const response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_MODELS_PATH), {
|
|
1530
1889
|
method: "POST",
|
|
1531
1890
|
headers: {
|
|
1532
1891
|
...buildTraeCnHeaders(credential, identity),
|
|
1533
1892
|
Accept: "application/json"
|
|
1534
1893
|
},
|
|
1535
1894
|
body: JSON.stringify({
|
|
1536
|
-
function:
|
|
1895
|
+
function: directoryFunction,
|
|
1537
1896
|
config_names: null,
|
|
1538
1897
|
need_prompt: false,
|
|
1539
1898
|
current_config_info: null,
|
|
@@ -1589,9 +1948,10 @@ var TraeSoloUpstreamClient = class {
|
|
|
1589
1948
|
}
|
|
1590
1949
|
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
1591
1950
|
const headers = buildTraeCnHeaders(credential, identity);
|
|
1951
|
+
const base = this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat;
|
|
1592
1952
|
let response;
|
|
1593
1953
|
try {
|
|
1594
|
-
response = await this.fetchImpl(traeEndpoint(
|
|
1954
|
+
response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_CHAT_PATH), {
|
|
1595
1955
|
method: "POST",
|
|
1596
1956
|
headers,
|
|
1597
1957
|
body: prepared,
|
|
@@ -1960,6 +2320,24 @@ function parseTraeRemoteModel(value) {
|
|
|
1960
2320
|
//#region src/solo-remote.ts
|
|
1961
2321
|
const TRAE_SOLO_REMOTE_BASE = "https://solo.trae.cn/api/remote/v1";
|
|
1962
2322
|
/**
|
|
2323
|
+
* Region-scoped request dressing. The CN portal is `solo.trae.cn` with the
|
|
2324
|
+
* CN locale headers; the international directory lives on the shared
|
|
2325
|
+
* `coresg-normal.trae.ai` gateway and was verified (2026-09-15) with the
|
|
2326
|
+
* English/Singapore headers — both forms are accepted, each region keeps the
|
|
2327
|
+
* shape its own portal sends.
|
|
2328
|
+
*/
|
|
2329
|
+
function remoteDressing(region) {
|
|
2330
|
+
return region === "ai" ? {
|
|
2331
|
+
referer: "https://coresg-normal.trae.ai/",
|
|
2332
|
+
timezone: "Asia/Singapore",
|
|
2333
|
+
language: "en"
|
|
2334
|
+
} : {
|
|
2335
|
+
referer: "https://solo.trae.cn/",
|
|
2336
|
+
timezone: "Asia/Shanghai",
|
|
2337
|
+
language: "zh-cn"
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
1963
2341
|
* Read-only model catalog client for the SOLO Web API.
|
|
1964
2342
|
*
|
|
1965
2343
|
* This deliberately has no chat/session method: the Remote session protocol
|
|
@@ -1972,22 +2350,26 @@ var TraeSoloRemoteCatalogClient = class {
|
|
|
1972
2350
|
constructor(options) {
|
|
1973
2351
|
this.options = options;
|
|
1974
2352
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
1975
|
-
this.baseUrl = options.baseUrl
|
|
2353
|
+
this.baseUrl = options.baseUrl;
|
|
1976
2354
|
}
|
|
1977
|
-
async headers() {
|
|
2355
|
+
async headers(region) {
|
|
2356
|
+
const credential = await this.options.credential();
|
|
2357
|
+
const dressing = remoteDressing(region);
|
|
1978
2358
|
return {
|
|
1979
|
-
"Authorization": `Cloud-IDE-JWT ${
|
|
2359
|
+
"Authorization": `Cloud-IDE-JWT ${credential.accessToken}`,
|
|
1980
2360
|
"Content-Type": "application/json",
|
|
1981
2361
|
"x-trae-client-type": "web",
|
|
1982
|
-
"x-trae-user-timezone":
|
|
1983
|
-
"x-preferenced-language":
|
|
1984
|
-
"Referer":
|
|
2362
|
+
"x-trae-user-timezone": dressing.timezone,
|
|
2363
|
+
"x-preferenced-language": dressing.language,
|
|
2364
|
+
"Referer": dressing.referer,
|
|
1985
2365
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
1986
2366
|
};
|
|
1987
2367
|
}
|
|
1988
2368
|
async fetchModels(signal) {
|
|
1989
|
-
const
|
|
1990
|
-
const
|
|
2369
|
+
const region = regionOfCredential(await this.options.credential());
|
|
2370
|
+
const base = this.baseUrl ?? REGION_GATEWAYS[region].remote;
|
|
2371
|
+
const headers = await this.headers(region);
|
|
2372
|
+
const response = await this.fetchImpl(`${base}/models?functions=solo_agent_remote,solo_work_remote`, {
|
|
1991
2373
|
headers,
|
|
1992
2374
|
signal: signal ?? AbortSignal.timeout(3e4)
|
|
1993
2375
|
});
|
|
@@ -2164,8 +2546,9 @@ var TraeRawChatUpstreamClient = class {
|
|
|
2164
2546
|
message: "messages are required"
|
|
2165
2547
|
};
|
|
2166
2548
|
const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
|
|
2549
|
+
const requestId = crypto.randomUUID();
|
|
2167
2550
|
const headers = buildTraeCnHeaders(credential, identity, {
|
|
2168
|
-
requestId
|
|
2551
|
+
requestId,
|
|
2169
2552
|
profile: "raw-chat"
|
|
2170
2553
|
});
|
|
2171
2554
|
const runtime = this.options.config.runtime;
|
|
@@ -2190,7 +2573,7 @@ var TraeRawChatUpstreamClient = class {
|
|
|
2190
2573
|
};
|
|
2191
2574
|
let response;
|
|
2192
2575
|
try {
|
|
2193
|
-
response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? credential.
|
|
2576
|
+
response = await this.fetchImpl(traeEndpoint(this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat, TRAE_RAW_CHAT_V2_PATH), {
|
|
2194
2577
|
method: "POST",
|
|
2195
2578
|
headers,
|
|
2196
2579
|
body: JSON.stringify(body),
|
|
@@ -2497,11 +2880,13 @@ var TraeDelegatingUpstreamClient = class {
|
|
|
2497
2880
|
/**
|
|
2498
2881
|
* Read-only Trae usage/credits client.
|
|
2499
2882
|
*
|
|
2500
|
-
*
|
|
2501
|
-
* `docs/USAGE_API_RESEARCH.md`)
|
|
2502
|
-
*
|
|
2503
|
-
*
|
|
2504
|
-
*
|
|
2883
|
+
* CN sources the verified `api.trae.cn` pay/ug endpoints (see
|
|
2884
|
+
* `docs/USAGE_API_RESEARCH.md`); the international (ai) region is
|
|
2885
|
+
* subscription-based and reads `ide_user_pay_status` on its own gateway
|
|
2886
|
+
* (verified 2026-09-15, docs/INTL_SG_EVIDENCE.md §4). All queries are
|
|
2887
|
+
* read-only and do not consume Trae credits. The per-session consumption
|
|
2888
|
+
* detail table is deliberately not included: the endpoint returns no rows
|
|
2889
|
+
* for the current account, so we only expose what is actually retrievable.
|
|
2505
2890
|
*/
|
|
2506
2891
|
const TRAE_PAY_BASE = "https://api.trae.cn";
|
|
2507
2892
|
function asNumber(value) {
|
|
@@ -2559,23 +2944,37 @@ var TraeUsageClient = class {
|
|
|
2559
2944
|
constructor(options) {
|
|
2560
2945
|
this.options = options;
|
|
2561
2946
|
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
2562
|
-
this.baseUrl = options.baseUrl
|
|
2947
|
+
this.baseUrl = options.baseUrl;
|
|
2563
2948
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
2564
2949
|
}
|
|
2950
|
+
/** Region of the current credential; `cn` when unresolvable. */
|
|
2951
|
+
async currentRegion() {
|
|
2952
|
+
const credential = await this.options.credential();
|
|
2953
|
+
return credential === void 0 ? "cn" : regionOfCredential(credential);
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Pay base for the current credential's region. An explicit baseUrl (tests,
|
|
2957
|
+
* diagnostics) pins the endpoint; otherwise CN uses `api.trae.cn` and the
|
|
2958
|
+
* international region its own verified pay gateway.
|
|
2959
|
+
*/
|
|
2960
|
+
async payBase() {
|
|
2961
|
+
return this.baseUrl ?? REGION_GATEWAYS[await this.currentRegion()].pay;
|
|
2962
|
+
}
|
|
2565
2963
|
async authedHeaders() {
|
|
2566
2964
|
const credential = await this.options.credential();
|
|
2567
2965
|
if (credential === void 0 || credential.accessToken === "") throw new Error("Trae credential is not available; cannot query usage");
|
|
2966
|
+
const origin = await this.currentRegion() === "ai" ? "https://www.trae.ai" : "https://www.trae.cn";
|
|
2568
2967
|
return {
|
|
2569
2968
|
"Authorization": `Cloud-IDE-JWT ${credential.accessToken}`,
|
|
2570
2969
|
"Content-Type": "application/json",
|
|
2571
2970
|
"User-Agent": "Mozilla/5.0",
|
|
2572
|
-
"Origin":
|
|
2573
|
-
"Referer":
|
|
2971
|
+
"Origin": origin,
|
|
2972
|
+
"Referer": `${origin}/`
|
|
2574
2973
|
};
|
|
2575
2974
|
}
|
|
2576
2975
|
async post(path, data, signal) {
|
|
2577
2976
|
const headers = await this.authedHeaders();
|
|
2578
|
-
const response = await this.fetchImpl(`${this.
|
|
2977
|
+
const response = await this.fetchImpl(`${await this.payBase()}${path}`, {
|
|
2579
2978
|
method: "POST",
|
|
2580
2979
|
headers,
|
|
2581
2980
|
body: JSON.stringify(data),
|
|
@@ -2584,12 +2983,53 @@ var TraeUsageClient = class {
|
|
|
2584
2983
|
if (!response.ok) throw new Error(`Trae usage endpoint ${path} returned HTTP ${response.status}`);
|
|
2585
2984
|
return await response.json();
|
|
2586
2985
|
}
|
|
2986
|
+
/**
|
|
2987
|
+
* Subscription/pay status of an international (ai) account. The CN region
|
|
2988
|
+
* never calls this (its contract is unverified there); the ai region uses
|
|
2989
|
+
* this instead of the Work-credit `snapshot`.
|
|
2990
|
+
*/
|
|
2991
|
+
async payStatus(signal) {
|
|
2992
|
+
if (await this.currentRegion() !== "ai") throw new Error("Trae pay status is only available for the international (ai) region");
|
|
2993
|
+
const payload = await this.post("/trae/api/v1/pay/ide_user_pay_status", {}, signal);
|
|
2994
|
+
const flag = (key) => payload[key] === true;
|
|
2995
|
+
const trial = typeof payload["trial_status"] === "object" && payload["trial_status"] !== null ? payload["trial_status"] : {};
|
|
2996
|
+
const fissionStart = asNumber(payload["solo_fission_start_time"]);
|
|
2997
|
+
const fissionExpire = asNumber(payload["solo_fission_expire_time"]);
|
|
2998
|
+
const fissionMax = asNumber(payload["solo_fission_max_usage"]);
|
|
2999
|
+
return {
|
|
3000
|
+
isDollarUsageBilling: flag("is_dollar_usage_billing"),
|
|
3001
|
+
hasPackage: flag("has_package"),
|
|
3002
|
+
isPayFreshman: flag("is_pay_freshman") || flag("is_pay_freshman_v2"),
|
|
3003
|
+
inTrial: trial["is_in_trial"] === true,
|
|
3004
|
+
trialEndTimeMs: asNumber(trial["trial_end_time"]) ?? 0,
|
|
3005
|
+
enableSoloLite: flag("enable_solo_lite"),
|
|
3006
|
+
enableSoloBuilder: flag("enable_solo_builder"),
|
|
3007
|
+
enableSoloCoder: flag("enable_solo_coder"),
|
|
3008
|
+
enableSoloWeb: flag("enable_solo_web"),
|
|
3009
|
+
...fissionStart === void 0 || fissionExpire === void 0 || fissionMax === void 0 ? {} : { fission: {
|
|
3010
|
+
startTimeMs: fissionStart,
|
|
3011
|
+
expireTimeMs: fissionExpire,
|
|
3012
|
+
maxUsage: fissionMax
|
|
3013
|
+
} }
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
/**
|
|
3017
|
+
* The Work-credit endpoints are CN-only. The international region is
|
|
3018
|
+
* subscription-based and must read {@link payStatus} instead; guarding here
|
|
3019
|
+
* keeps the card's degraded `creditsError` message diagnosable rather than
|
|
3020
|
+
* letting an ai credential hit an unverified path on its pay gateway.
|
|
3021
|
+
*/
|
|
3022
|
+
async requireCnRegion(method) {
|
|
3023
|
+
if (await this.currentRegion() !== "cn") throw new Error(`Trae ${method} is only available for the CN region; the international (ai) region uses payStatus`);
|
|
3024
|
+
}
|
|
2587
3025
|
/** Total entitlements / credits and per-pack breakdown. */
|
|
2588
3026
|
async snapshot(signal) {
|
|
3027
|
+
await this.requireCnRegion("usage snapshot");
|
|
2589
3028
|
return parseUsageSnapshot(await this.post("/trae/api/v2/pay/web_user_ent_usage", { require_usage: true }, signal));
|
|
2590
3029
|
}
|
|
2591
3030
|
/** Daily check-in status. */
|
|
2592
3031
|
async checkinStatus(signal) {
|
|
3032
|
+
await this.requireCnRegion("check-in status");
|
|
2593
3033
|
const payload = await this.post("/trae/api/v2/ug/checkin_credits/status", {}, signal);
|
|
2594
3034
|
return {
|
|
2595
3035
|
checkedIn: payload["checked_in"] === true,
|
|
@@ -2599,6 +3039,7 @@ var TraeUsageClient = class {
|
|
|
2599
3039
|
}
|
|
2600
3040
|
/** Rewards / activity rules. */
|
|
2601
3041
|
async activities(signal) {
|
|
3042
|
+
await this.requireCnRegion("activities");
|
|
2602
3043
|
const payload = await this.post("/trae/api/v2/ug/activity/info", {}, signal);
|
|
2603
3044
|
const rawActivities = Array.isArray(payload["commercial_activities"]) ? payload["commercial_activities"] : [];
|
|
2604
3045
|
const activities = [];
|
|
@@ -2632,17 +3073,34 @@ var TraeUsageClient = class {
|
|
|
2632
3073
|
};
|
|
2633
3074
|
//#endregion
|
|
2634
3075
|
//#region src/status-paths.ts
|
|
2635
|
-
/**
|
|
2636
|
-
* Node-free constants and types shared by the Host and browser halves.
|
|
2637
|
-
* Mirrors the `dsh-workbuddy-connect` status-route pattern so the plugin card
|
|
2638
|
-
* stays consistent with that project's external presentation.
|
|
2639
|
-
*/
|
|
2640
3076
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
2641
3077
|
const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
|
|
2642
3078
|
/** Plugin-owned live model refresh endpoint. */
|
|
2643
3079
|
const TRAE_MODELS_REFRESH_PATH = "/plugins/dsh-connect-trae/models/refresh";
|
|
2644
3080
|
/** Plugin-owned local account rescan endpoint. */
|
|
2645
3081
|
const TRAE_ACCOUNTS_REFRESH_PATH = "/plugins/dsh-connect-trae/accounts/refresh";
|
|
3082
|
+
/** Query parameter naming the region a card request addresses. */
|
|
3083
|
+
const TRAE_REGION_PARAM = "region";
|
|
3084
|
+
/** Every region, in card tab order. */
|
|
3085
|
+
const TRAE_REGIONS = ["cn", "ai"];
|
|
3086
|
+
/**
|
|
3087
|
+
* Address one region's status route. The two regions are separate provider
|
|
3088
|
+
* stacks; every card request carries the region whose tab the user is on.
|
|
3089
|
+
*/
|
|
3090
|
+
function withTraeRegion(path, region) {
|
|
3091
|
+
return `${path}?${TRAE_REGION_PARAM}=${region}`;
|
|
3092
|
+
}
|
|
3093
|
+
/**
|
|
3094
|
+
* Read the region parameter off a status-route URL. Absent means the domestic
|
|
3095
|
+
* tab (`cn`); a present-but-unknown value returns undefined so the route can
|
|
3096
|
+
* answer 400 instead of guessing.
|
|
3097
|
+
*/
|
|
3098
|
+
function regionOfTraeStatusUrl(url) {
|
|
3099
|
+
const at = url.indexOf("?");
|
|
3100
|
+
const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(TRAE_REGION_PARAM);
|
|
3101
|
+
if (value === null || value === "") return "cn";
|
|
3102
|
+
return TRAE_REGIONS.includes(value) ? value : void 0;
|
|
3103
|
+
}
|
|
2646
3104
|
//#endregion
|
|
2647
3105
|
//#region src/web-status.ts
|
|
2648
3106
|
/** Redact token-like content before it crosses to the browser. */
|
|
@@ -2688,14 +3146,17 @@ function toCredits(snapshot) {
|
|
|
2688
3146
|
};
|
|
2689
3147
|
}
|
|
2690
3148
|
/**
|
|
2691
|
-
* Assemble
|
|
2692
|
-
*
|
|
2693
|
-
*
|
|
3149
|
+
* Assemble one region's card document. `region` is the tab the card is on; the
|
|
3150
|
+
* region-scoped store already answers with only that region's accounts, so the
|
|
3151
|
+
* document's model slots and account list are that region's by construction.
|
|
3152
|
+
* Sign-in state is read-only; credit is a live billing answer whose failure
|
|
3153
|
+
* degrades to `creditsError` rather than failing the whole document.
|
|
2694
3154
|
*/
|
|
2695
|
-
async function traeWebUsage(deps) {
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
|
|
3155
|
+
async function traeWebUsage(deps, region) {
|
|
3156
|
+
const store = deps.store(region);
|
|
3157
|
+
const accounts = await store.accounts();
|
|
3158
|
+
if ((await store.status()).state !== "signed-in") {
|
|
3159
|
+
const { failures } = await store.diagnose();
|
|
2699
3160
|
return {
|
|
2700
3161
|
status: "signed-out",
|
|
2701
3162
|
accounts,
|
|
@@ -2710,7 +3171,7 @@ async function traeWebUsage(deps) {
|
|
|
2710
3171
|
}
|
|
2711
3172
|
let credential;
|
|
2712
3173
|
try {
|
|
2713
|
-
credential = await
|
|
3174
|
+
credential = await store.resolve();
|
|
2714
3175
|
} catch (error) {
|
|
2715
3176
|
return {
|
|
2716
3177
|
status: "signed-out",
|
|
@@ -2722,16 +3183,32 @@ async function traeWebUsage(deps) {
|
|
|
2722
3183
|
accountId: accounts.find((item) => item.selected)?.id ?? "",
|
|
2723
3184
|
accountName: credential.accountName ?? credential.userId,
|
|
2724
3185
|
tokenExpiresAtMs: credential.expiresAtMs,
|
|
3186
|
+
region,
|
|
2725
3187
|
accounts,
|
|
2726
|
-
models: deps.displayModels().map((model) => ({
|
|
3188
|
+
models: deps.displayModels(region).map((model) => ({
|
|
2727
3189
|
...model,
|
|
2728
3190
|
...model.input === void 0 ? {} : { input: [...model.input] }
|
|
2729
3191
|
})),
|
|
2730
|
-
enabledModelIds: [...deps.enabledModelIds()],
|
|
2731
|
-
...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic() }
|
|
3192
|
+
enabledModelIds: [...deps.enabledModelIds(region)],
|
|
3193
|
+
...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(region) }
|
|
2732
3194
|
};
|
|
3195
|
+
const client = deps.client(region);
|
|
3196
|
+
if (region === "ai") try {
|
|
3197
|
+
const payStatus = await client.payStatus();
|
|
3198
|
+
return {
|
|
3199
|
+
status: "signed-in",
|
|
3200
|
+
...account,
|
|
3201
|
+
payStatus
|
|
3202
|
+
};
|
|
3203
|
+
} catch (error) {
|
|
3204
|
+
return {
|
|
3205
|
+
status: "signed-in",
|
|
3206
|
+
...account,
|
|
3207
|
+
payStatusError: safeMessage(error)
|
|
3208
|
+
};
|
|
3209
|
+
}
|
|
2733
3210
|
try {
|
|
2734
|
-
const snapshot = await
|
|
3211
|
+
const snapshot = await client.snapshot();
|
|
2735
3212
|
return {
|
|
2736
3213
|
status: "signed-in",
|
|
2737
3214
|
...account,
|
|
@@ -2745,6 +3222,18 @@ async function traeWebUsage(deps) {
|
|
|
2745
3222
|
};
|
|
2746
3223
|
}
|
|
2747
3224
|
}
|
|
3225
|
+
/**
|
|
3226
|
+
* The region a request addresses, or a 400 answer. Absent parameter means the
|
|
3227
|
+
* domestic tab; an unknown value is refused rather than guessed.
|
|
3228
|
+
*/
|
|
3229
|
+
function requestRegion(req, res) {
|
|
3230
|
+
const region = regionOfTraeStatusUrl(req.url ?? "/");
|
|
3231
|
+
if (region === void 0) {
|
|
3232
|
+
json(res, 400, { error: "unknown region" });
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
return region;
|
|
3236
|
+
}
|
|
2748
3237
|
/** Mount the GET usage route on an optional webServer context. */
|
|
2749
3238
|
function registerTraeUsageRoute(ctx, deps) {
|
|
2750
3239
|
ctx.effect(() => {
|
|
@@ -2760,8 +3249,10 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
2760
3249
|
json(res, 403, { error: "origin-not-trusted" });
|
|
2761
3250
|
return;
|
|
2762
3251
|
}
|
|
3252
|
+
const region = requestRegion(req, res);
|
|
3253
|
+
if (region === void 0) return;
|
|
2763
3254
|
try {
|
|
2764
|
-
json(res, 200, await traeWebUsage(deps));
|
|
3255
|
+
json(res, 200, await traeWebUsage(deps, region));
|
|
2765
3256
|
} catch (error) {
|
|
2766
3257
|
json(res, 500, { error: safeMessage(error) });
|
|
2767
3258
|
}
|
|
@@ -2773,8 +3264,10 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
2773
3264
|
handler: async (req, res) => {
|
|
2774
3265
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
2775
3266
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
3267
|
+
const region = requestRegion(req, res);
|
|
3268
|
+
if (region === void 0) return;
|
|
2776
3269
|
try {
|
|
2777
|
-
json(res, 200, { accounts: await deps.store.accounts() });
|
|
3270
|
+
json(res, 200, { accounts: await deps.store(region).accounts() });
|
|
2778
3271
|
} catch (error) {
|
|
2779
3272
|
json(res, 500, { error: safeMessage(error) });
|
|
2780
3273
|
}
|
|
@@ -2787,8 +3280,10 @@ function registerTraeUsageRoute(ctx, deps) {
|
|
|
2787
3280
|
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
2788
3281
|
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
2789
3282
|
if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
|
|
3283
|
+
const region = requestRegion(req, res);
|
|
3284
|
+
if (region === void 0) return;
|
|
2790
3285
|
try {
|
|
2791
|
-
json(res, 200, { models: await deps.discoverModels() });
|
|
3286
|
+
json(res, 200, { models: await deps.discoverModels(region) });
|
|
2792
3287
|
} catch (error) {
|
|
2793
3288
|
json(res, 500, { error: safeMessage(error) });
|
|
2794
3289
|
}
|
|
@@ -2946,6 +3441,26 @@ const inject = ["llm"];
|
|
|
2946
3441
|
* so no branding is needed for either host generation.
|
|
2947
3442
|
*/
|
|
2948
3443
|
const TRAE_SETTINGS_NS = "trae";
|
|
3444
|
+
/**
|
|
3445
|
+
* One region's saved state. A config written before the region split has only
|
|
3446
|
+
* the flat fields: those were always captured from the CN endpoint (the
|
|
3447
|
+
* plugin had no international support), so they are read as the CN state and
|
|
3448
|
+
* only when no explicit CN slot exists. The ai region never inherits them —
|
|
3449
|
+
* that inheritance is exactly the bug where a stale CN directory would be
|
|
3450
|
+
* intersected with the international catalog and silently drop the user's
|
|
3451
|
+
* picks (the same failure workbuddy fixed with its region split).
|
|
3452
|
+
*/
|
|
3453
|
+
function regionStateOf(config, region) {
|
|
3454
|
+
const stored = config.regions?.[region];
|
|
3455
|
+
if (stored !== void 0) return stored;
|
|
3456
|
+
if (region !== "cn") return {};
|
|
3457
|
+
return {
|
|
3458
|
+
...config.lastCatalog === void 0 ? {} : { lastCatalog: config.lastCatalog },
|
|
3459
|
+
...config.enabledModelIds === void 0 ? {} : { enabledModelIds: config.enabledModelIds },
|
|
3460
|
+
...config.imageModelIds === void 0 ? {} : { imageModelIds: config.imageModelIds },
|
|
3461
|
+
...config.contextBudgets === void 0 ? {} : { contextBudgets: config.contextBudgets }
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
2949
3464
|
const modelConfig = z.object({
|
|
2950
3465
|
id: z.string().required(),
|
|
2951
3466
|
name: z.string().required(),
|
|
@@ -2954,6 +3469,16 @@ const modelConfig = z.object({
|
|
|
2954
3469
|
input: z.array(z.union(["text", "image"])),
|
|
2955
3470
|
creditMultiplier: z.number()
|
|
2956
3471
|
});
|
|
3472
|
+
const regionStateConfig = z.object({
|
|
3473
|
+
lastCatalog: z.array(modelConfig).default([]),
|
|
3474
|
+
enabledModelIds: z.array(z.string()).default([]),
|
|
3475
|
+
imageModelIds: z.array(z.string()).default([]),
|
|
3476
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({})
|
|
3477
|
+
});
|
|
3478
|
+
const accountSelectionConfig = z.object({
|
|
3479
|
+
cn: z.string().description("Selected domestic (CN) account id (never a token)"),
|
|
3480
|
+
ai: z.string().description("Selected international account id (never a token)")
|
|
3481
|
+
});
|
|
2957
3482
|
const Config = z.object({
|
|
2958
3483
|
authFile: z.string().description("Optional Trae storage.json path override"),
|
|
2959
3484
|
edition: z.union([
|
|
@@ -2963,137 +3488,229 @@ const Config = z.object({
|
|
|
2963
3488
|
"solo",
|
|
2964
3489
|
"solo-sg"
|
|
2965
3490
|
]).default("auto").description("Trae edition hint"),
|
|
2966
|
-
accountId: z.string().description("
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3491
|
+
accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
|
|
3492
|
+
accounts: accountSelectionConfig.description("Per-region account selections, keyed cn | ai"),
|
|
3493
|
+
regions: z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | ai"),
|
|
3494
|
+
lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
|
|
3495
|
+
enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
|
|
3496
|
+
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets"),
|
|
3497
|
+
imageModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN image opt-in"),
|
|
2971
3498
|
models: z.array(modelConfig).description("Legacy generated Trae model list")
|
|
2972
3499
|
});
|
|
3500
|
+
/** Every region, in card tab order. */
|
|
3501
|
+
const REGION_KEYS = ["cn", "ai"];
|
|
2973
3502
|
function apply(ctx, config) {
|
|
2974
|
-
|
|
2975
|
-
const enabledSet = (value) => new Set(value.enabledModelIds ?? []);
|
|
2976
|
-
const imageSet = (value) => new Set(value.imageModelIds ?? []);
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3503
|
+
let current = () => config;
|
|
3504
|
+
const enabledSet = (value, region) => new Set(regionStateOf(value, region).enabledModelIds ?? []);
|
|
3505
|
+
const imageSet = (value, region) => new Set(regionStateOf(value, region).imageModelIds ?? []);
|
|
3506
|
+
/**
|
|
3507
|
+
* Per-region wire state. Each region's discovery owns its own maps: the CN
|
|
3508
|
+
* and international rosters overlap (and spell some ids differently), so a
|
|
3509
|
+
* shared map would let one region's answer filter the other region's
|
|
3510
|
+
* catalog. Display keys (lowercased id AND name) of every model known to be
|
|
3511
|
+
* callable via `llm_utils_chat` are recorded by that region's discovery; a
|
|
3512
|
+
* model whose id and name are both absent is a dead config_name (Remote
|
|
3513
|
+
* advertises it, `get_detail_param` has no match, e.g. `Doubao-Seed-Code` /
|
|
3514
|
+
* `glm-5.3`) and must never be served — even from a stale saved
|
|
3515
|
+
* `lastCatalog` / `models` / `enabledModelIds` that still lists it.
|
|
3516
|
+
*/
|
|
3517
|
+
const wireState = {};
|
|
3518
|
+
for (const region of REGION_KEYS) wireState[region] = {
|
|
3519
|
+
callableKeys: /* @__PURE__ */ new Set(),
|
|
3520
|
+
resolved: false,
|
|
3521
|
+
byId: /* @__PURE__ */ new Map(),
|
|
3522
|
+
byName: /* @__PURE__ */ new Map()
|
|
2982
3523
|
};
|
|
2983
|
-
const
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
return derived.length > 0 ? derived : fallbackModels(value);
|
|
3524
|
+
const dropDeadModels = (rows, region) => {
|
|
3525
|
+
const wire = wireState[region];
|
|
3526
|
+
if (!wire.resolved) return rows;
|
|
3527
|
+
return rows.filter((model) => wire.callableKeys.has(model.id.trim().toLowerCase()) || wire.callableKeys.has(model.name.trim().toLowerCase()));
|
|
2988
3528
|
};
|
|
2989
|
-
const
|
|
2990
|
-
const
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
...config.accountId === void 0 ? {} : { accountId: config.accountId },
|
|
2995
|
-
refresh: (credential) => refreshTraeCredential(credential)
|
|
2996
|
-
});
|
|
2997
|
-
const identity = async () => {
|
|
2998
|
-
const candidates = config.authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (item.edition === "cn" || item.edition === "solo") && (config.edition === void 0 || config.edition === "auto" || item.edition === config.edition)) : [{
|
|
2999
|
-
edition: config.edition === void 0 || config.edition === "auto" ? "solo" : config.edition,
|
|
3000
|
-
path: config.authFile,
|
|
3001
|
-
source: "desktop"
|
|
3002
|
-
}];
|
|
3003
|
-
if (candidates.length === 0) throw new Error("Trae storage was not found");
|
|
3004
|
-
return pickTraeStorageIdentity(candidates);
|
|
3529
|
+
const fallbackModels = (value, region) => applyImageSelection(fallbackModelsFor(region), imageSet(value, region));
|
|
3530
|
+
const derive = (value, raw, region) => {
|
|
3531
|
+
const selectedImages = imageSet(value, region);
|
|
3532
|
+
const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw, region)), selectedImages), enabledSet(value, region), regionStateOf(value, region).contextBudgets ?? {});
|
|
3533
|
+
return derived.length > 0 ? derived : fallbackModels(value, region);
|
|
3005
3534
|
};
|
|
3006
|
-
const
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
baseUrl: "https://trae-api-cn.mchost.guru",
|
|
3010
|
-
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
3011
|
-
});
|
|
3012
|
-
const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
|
|
3013
|
-
const wireById = /* @__PURE__ */ new Map();
|
|
3014
|
-
const wireByName = /* @__PURE__ */ new Map();
|
|
3015
|
-
const wireResolver = (displayId) => wireById.get(displayId) ?? wireByName.get(displayId.trim().toLowerCase());
|
|
3016
|
-
const upstream = new TraeSoloBridge(solo, catalog, wireResolver);
|
|
3017
|
-
const delegating = new TraeDelegatingUpstreamClient(upstream);
|
|
3018
|
-
const shim = createTraeShim({
|
|
3019
|
-
catalog,
|
|
3020
|
-
client: delegating,
|
|
3021
|
-
logger: ctx.logger
|
|
3022
|
-
});
|
|
3023
|
-
let rawDiagnostic = () => ({ state: "disabled" });
|
|
3024
|
-
(async () => {
|
|
3025
|
-
try {
|
|
3026
|
-
const { identity, runtime } = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
|
|
3027
|
-
const gateway = createTraeRawGateway({
|
|
3028
|
-
raw: new TraeRawChatUpstreamClient({
|
|
3029
|
-
credential: () => store.resolve(),
|
|
3030
|
-
identity: async () => identity,
|
|
3031
|
-
config: {
|
|
3032
|
-
model: runtime.modelName,
|
|
3033
|
-
configName: runtime.configName,
|
|
3034
|
-
passBackReasoning: true,
|
|
3035
|
-
runtime
|
|
3036
|
-
},
|
|
3037
|
-
baseUrl: "https://trae-api-cn.mchost.guru"
|
|
3038
|
-
}),
|
|
3039
|
-
solo: upstream,
|
|
3040
|
-
endpoint: "https://trae-api-cn.mchost.guru/api/ide/v2/llm_raw_chat",
|
|
3041
|
-
edition: identity.edition,
|
|
3042
|
-
identity: {
|
|
3043
|
-
appVersion: identity.appVersion ?? "",
|
|
3044
|
-
buildVersion: identity.buildVersion ?? ""
|
|
3045
|
-
},
|
|
3046
|
-
runtime,
|
|
3047
|
-
enabled: false
|
|
3048
|
-
});
|
|
3049
|
-
delegating.replace(gateway.upstream);
|
|
3050
|
-
rawDiagnostic = () => gateway.diagnostic();
|
|
3051
|
-
ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
|
|
3052
|
-
} catch (error) {
|
|
3053
|
-
ctx.logger.warn("dsh-connect-trae: Raw gateway unavailable; continuing with native SOLO tool-call channel", error);
|
|
3054
|
-
}
|
|
3055
|
-
})();
|
|
3056
|
-
const usageClient = new TraeUsageClient({ credential: () => store.resolve() });
|
|
3057
|
-
let current = () => config;
|
|
3058
|
-
let invalidateAdapter = () => {};
|
|
3059
|
-
const discoverModels = async (signal) => {
|
|
3060
|
-
const [remote, wire] = await Promise.all([remoteCatalog.fetchModels(signal), solo.fetchModels(signal)]);
|
|
3061
|
-
const merged = mergeTraeModelSources(remote, wire);
|
|
3062
|
-
callableKeys.clear();
|
|
3063
|
-
for (const model of merged) {
|
|
3064
|
-
callableKeys.add(model.id.trim().toLowerCase());
|
|
3065
|
-
callableKeys.add(model.name.trim().toLowerCase());
|
|
3066
|
-
}
|
|
3067
|
-
wireResolved = merged.length > 0;
|
|
3068
|
-
wireById.clear();
|
|
3069
|
-
wireByName.clear();
|
|
3070
|
-
for (const model of merged) if (model.wireConfigName !== void 0) {
|
|
3071
|
-
wireById.set(model.id, model.wireConfigName);
|
|
3072
|
-
wireByName.set(model.name.trim().toLowerCase(), model.wireConfigName);
|
|
3073
|
-
}
|
|
3074
|
-
const next = applyImageSelection(merged, imageSet(current()));
|
|
3075
|
-
catalog.set(next);
|
|
3076
|
-
return merged;
|
|
3535
|
+
const configuredModels = (value, region) => {
|
|
3536
|
+
const state = regionStateOf(value, region);
|
|
3537
|
+
return state.lastCatalog?.length ? derive(value, state.lastCatalog, region) : region === "cn" && value.models?.length ? derive(value, value.models, region) : fallbackModels(value, region);
|
|
3077
3538
|
};
|
|
3539
|
+
const displayModels = (value, region) => {
|
|
3540
|
+
const state = regionStateOf(value, region);
|
|
3541
|
+
return state.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(state.lastCatalog), region) : region === "cn" && value.models?.length ? dropDeadModels(sanitizeCatalog(value.models), region) : fallbackModelsFor(region);
|
|
3542
|
+
};
|
|
3543
|
+
/**
|
|
3544
|
+
* Legacy migration for the pre-split single `accountId`: its region is
|
|
3545
|
+
* resolved once from the local account scan and the selection is then
|
|
3546
|
+
* attributed to that region ONLY — the other region keeps its documented
|
|
3547
|
+
* default (first discovered account of that region) instead of silently
|
|
3548
|
+
* inheriting a selection that belongs to the other side of the split.
|
|
3549
|
+
*/
|
|
3550
|
+
let legacyAccountRegion;
|
|
3551
|
+
const effectiveAccountFor = (region, value) => {
|
|
3552
|
+
const explicit = value.accounts?.[region];
|
|
3553
|
+
if (explicit !== void 0) return explicit;
|
|
3554
|
+
return legacyAccountRegion === region ? value.accountId : void 0;
|
|
3555
|
+
};
|
|
3556
|
+
const stacks = {};
|
|
3557
|
+
for (const region of REGION_KEYS) {
|
|
3558
|
+
const catalog = new TraeCatalog(region);
|
|
3559
|
+
const wire = wireState[region];
|
|
3560
|
+
/**
|
|
3561
|
+
* Best-effort device identity for this region's installation. Prefer the
|
|
3562
|
+
* selected credential's own edition so an international account reads its
|
|
3563
|
+
* own installation's identity (machine/device ids are per-install),
|
|
3564
|
+
* mirroring the credential store's account binding; the explicit `edition`
|
|
3565
|
+
* config still wins as the user's own narrowing. Candidates are then
|
|
3566
|
+
* narrowed to THIS region's editions, so the CN stack can never read an
|
|
3567
|
+
* international install's identity (and vice versa).
|
|
3568
|
+
*/
|
|
3569
|
+
const identity = async () => {
|
|
3570
|
+
let preferred;
|
|
3571
|
+
try {
|
|
3572
|
+
preferred = (await store.current())?.edition;
|
|
3573
|
+
} catch {}
|
|
3574
|
+
const hint = (config.edition !== void 0 && config.edition !== "auto" ? config.edition : void 0) ?? preferred;
|
|
3575
|
+
return resolveTraeIdentity(config.authFile === void 0 ? traeStorageCandidates().filter((item) => item.source === "desktop" && (hint !== void 0 ? item.edition === hint : regionOfEdition(item.edition) === region)) : [{
|
|
3576
|
+
edition: hint ?? (region === "ai" ? "sg" : "solo"),
|
|
3577
|
+
path: config.authFile,
|
|
3578
|
+
source: "desktop"
|
|
3579
|
+
}], hint ?? (region === "ai" ? "solo-sg" : "cn"));
|
|
3580
|
+
};
|
|
3581
|
+
/**
|
|
3582
|
+
* Resolution failures degrade to omitting DeviceInfo (its being required
|
|
3583
|
+
* is unverified, docs/INTL_SG_EVIDENCE.md §5) rather than failing the
|
|
3584
|
+
* refresh.
|
|
3585
|
+
*/
|
|
3586
|
+
const refreshDeviceSafe = async () => {
|
|
3587
|
+
try {
|
|
3588
|
+
const value = await identity();
|
|
3589
|
+
return {
|
|
3590
|
+
deviceId: value.deviceId,
|
|
3591
|
+
machineId: value.machineId
|
|
3592
|
+
};
|
|
3593
|
+
} catch {
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
};
|
|
3597
|
+
const store = new TraeCredentialStore({
|
|
3598
|
+
region,
|
|
3599
|
+
...config.authFile === void 0 ? {} : { storagePath: config.authFile },
|
|
3600
|
+
edition: config.edition ?? "auto",
|
|
3601
|
+
refresh: async (credential) => refreshTraeCredential(credential, void 0, await refreshDeviceSafe())
|
|
3602
|
+
});
|
|
3603
|
+
const solo = new TraeSoloUpstreamClient({
|
|
3604
|
+
credential: () => store.resolve(),
|
|
3605
|
+
identity,
|
|
3606
|
+
log: (message, detail) => ctx.logger.warn(message, detail)
|
|
3607
|
+
});
|
|
3608
|
+
const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
|
|
3609
|
+
const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
|
|
3610
|
+
const upstream = new TraeSoloBridge(solo, catalog, wireResolver);
|
|
3611
|
+
const delegating = new TraeDelegatingUpstreamClient(upstream);
|
|
3612
|
+
const shim = createTraeShim({
|
|
3613
|
+
catalog,
|
|
3614
|
+
client: delegating,
|
|
3615
|
+
logger: ctx.logger
|
|
3616
|
+
});
|
|
3617
|
+
let rawDiagnostic = () => ({ state: "disabled" });
|
|
3618
|
+
if (region === "cn") (async () => {
|
|
3619
|
+
try {
|
|
3620
|
+
const resolved = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
|
|
3621
|
+
const gateway = createTraeRawGateway({
|
|
3622
|
+
raw: new TraeRawChatUpstreamClient({
|
|
3623
|
+
credential: () => store.resolve(),
|
|
3624
|
+
identity: async () => resolved.identity,
|
|
3625
|
+
config: {
|
|
3626
|
+
model: resolved.runtime.modelName,
|
|
3627
|
+
configName: resolved.runtime.configName,
|
|
3628
|
+
passBackReasoning: true,
|
|
3629
|
+
runtime: resolved.runtime
|
|
3630
|
+
}
|
|
3631
|
+
}),
|
|
3632
|
+
solo: upstream,
|
|
3633
|
+
endpoint: `${REGION_GATEWAYS.cn.chat}/api/ide/v2/llm_raw_chat`,
|
|
3634
|
+
edition: resolved.identity.edition,
|
|
3635
|
+
identity: {
|
|
3636
|
+
appVersion: resolved.identity.appVersion ?? "",
|
|
3637
|
+
buildVersion: resolved.identity.buildVersion ?? ""
|
|
3638
|
+
},
|
|
3639
|
+
runtime: resolved.runtime,
|
|
3640
|
+
enabled: false
|
|
3641
|
+
});
|
|
3642
|
+
delegating.replace(gateway.upstream);
|
|
3643
|
+
rawDiagnostic = () => gateway.diagnostic();
|
|
3644
|
+
ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
|
|
3645
|
+
} catch (error) {
|
|
3646
|
+
ctx.logger.warn("dsh-connect-trae: Raw gateway unavailable; continuing with native SOLO tool-call channel", error);
|
|
3647
|
+
}
|
|
3648
|
+
})();
|
|
3649
|
+
stacks[region] = {
|
|
3650
|
+
region,
|
|
3651
|
+
store,
|
|
3652
|
+
catalog,
|
|
3653
|
+
shim,
|
|
3654
|
+
delegating,
|
|
3655
|
+
usageClient: new TraeUsageClient({ credential: () => store.resolve() }),
|
|
3656
|
+
discoverModels: async (signal) => {
|
|
3657
|
+
const [remote, wireModels] = await Promise.all([remoteCatalog.fetchModels(signal), solo.fetchModels(signal)]);
|
|
3658
|
+
const merged = mergeTraeModelSources(remote, wireModels);
|
|
3659
|
+
wire.callableKeys.clear();
|
|
3660
|
+
for (const model of merged) {
|
|
3661
|
+
wire.callableKeys.add(model.id.trim().toLowerCase());
|
|
3662
|
+
wire.callableKeys.add(model.name.trim().toLowerCase());
|
|
3663
|
+
}
|
|
3664
|
+
wire.resolved = merged.length > 0;
|
|
3665
|
+
wire.byId.clear();
|
|
3666
|
+
wire.byName.clear();
|
|
3667
|
+
for (const model of merged) if (model.wireConfigName !== void 0) {
|
|
3668
|
+
wire.byId.set(model.id, model.wireConfigName);
|
|
3669
|
+
wire.byName.set(model.name.trim().toLowerCase(), model.wireConfigName);
|
|
3670
|
+
}
|
|
3671
|
+
return merged;
|
|
3672
|
+
},
|
|
3673
|
+
invalidateAdapter: () => {},
|
|
3674
|
+
rawDiagnostic: () => rawDiagnostic()
|
|
3675
|
+
};
|
|
3676
|
+
}
|
|
3078
3677
|
ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
|
|
3079
|
-
store,
|
|
3080
|
-
client: usageClient,
|
|
3081
|
-
displayModels: () => displayModels(current()),
|
|
3082
|
-
enabledModelIds: () => current().enabledModelIds ?? [],
|
|
3083
|
-
discoverModels,
|
|
3084
|
-
rawDiagnostic: () => rawDiagnostic()
|
|
3678
|
+
store: (region) => stacks[region].store,
|
|
3679
|
+
client: (region) => stacks[region].usageClient,
|
|
3680
|
+
displayModels: (region) => displayModels(current(), region),
|
|
3681
|
+
enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
|
|
3682
|
+
discoverModels: (region, signal) => stacks[region].discoverModels(signal),
|
|
3683
|
+
rawDiagnostic: (region) => stacks[region].rawDiagnostic()
|
|
3085
3684
|
}));
|
|
3685
|
+
/** Push the current config into every region's store selection and catalog. */
|
|
3686
|
+
const applySelection = (value) => {
|
|
3687
|
+
for (const region of REGION_KEYS) {
|
|
3688
|
+
const stack = stacks[region];
|
|
3689
|
+
stack.store.setSource(value.authFile, value.edition ?? "auto", effectiveAccountFor(region, value));
|
|
3690
|
+
stack.catalog.set(configuredModels(value, region));
|
|
3691
|
+
stack.invalidateAdapter();
|
|
3692
|
+
}
|
|
3693
|
+
};
|
|
3086
3694
|
const sectionHooks = {
|
|
3087
3695
|
setSource(source) {
|
|
3088
3696
|
current = source;
|
|
3089
3697
|
},
|
|
3090
3698
|
onChange() {
|
|
3091
|
-
|
|
3092
|
-
store.setSource(next.authFile, next.edition ?? "auto", next.accountId);
|
|
3093
|
-
catalog.set(configuredModels(next));
|
|
3094
|
-
invalidateAdapter();
|
|
3699
|
+
applySelection(current());
|
|
3095
3700
|
}
|
|
3096
3701
|
};
|
|
3702
|
+
applySelection(config);
|
|
3703
|
+
(async () => {
|
|
3704
|
+
const id = current().accountId;
|
|
3705
|
+
if (id === void 0) return;
|
|
3706
|
+
try {
|
|
3707
|
+
for (const region of REGION_KEYS) if ((await stacks[region].store.accounts()).some((account) => account.id === id)) {
|
|
3708
|
+
legacyAccountRegion = region;
|
|
3709
|
+
applySelection(current());
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
} catch {}
|
|
3713
|
+
})();
|
|
3097
3714
|
const legacyInstallSettingsSection = dshSettings.installSettingsSection;
|
|
3098
3715
|
if (typeof legacyInstallSettingsSection === "function") legacyInstallSettingsSection(ctx, TRAE_SETTINGS_NS, Config, config, sectionHooks);
|
|
3099
3716
|
else ctx.inject(["settings"], (settingsCtx) => {
|
|
@@ -3102,32 +3719,34 @@ function apply(ctx, config) {
|
|
|
3102
3719
|
let stopped = false;
|
|
3103
3720
|
ctx.effect(() => () => {
|
|
3104
3721
|
stopped = true;
|
|
3105
|
-
shim.close();
|
|
3722
|
+
for (const region of REGION_KEYS) stacks[region].shim.close();
|
|
3106
3723
|
});
|
|
3107
|
-
shim.ready.then(async () => {
|
|
3724
|
+
Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
|
|
3108
3725
|
if (stopped) return;
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
} catch (error) {
|
|
3112
|
-
ctx.logger.warn("dsh-connect-trae: wire-id resolution failed at startup; falling back to display ids", error);
|
|
3113
|
-
}
|
|
3114
|
-
catalog.set(configuredModels(current()));
|
|
3115
|
-
const trae = createTraeAdapter({
|
|
3116
|
-
shim,
|
|
3117
|
-
catalog,
|
|
3118
|
-
resolveAttachments: () => ctx.get("attachments")
|
|
3119
|
-
});
|
|
3120
|
-
invalidateAdapter = () => {
|
|
3121
|
-
trae.invalidate();
|
|
3122
|
-
};
|
|
3123
|
-
let releaseAdapter;
|
|
3726
|
+
let releaseAdapterCn;
|
|
3727
|
+
let releaseAdapterAi;
|
|
3124
3728
|
let releaseDirectory;
|
|
3125
3729
|
try {
|
|
3126
|
-
|
|
3730
|
+
for (const region of REGION_KEYS) {
|
|
3731
|
+
const stack = stacks[region];
|
|
3732
|
+
const trae = createTraeAdapter({
|
|
3733
|
+
shim: stack.shim,
|
|
3734
|
+
catalog: stack.catalog,
|
|
3735
|
+
provider: TRAE_PROVIDERS[region],
|
|
3736
|
+
displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
|
|
3737
|
+
resolveAttachments: () => ctx.get("attachments")
|
|
3738
|
+
});
|
|
3739
|
+
stack.invalidateAdapter = () => {
|
|
3740
|
+
trae.invalidate();
|
|
3741
|
+
};
|
|
3742
|
+
if (region === "cn") releaseAdapterCn = ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter);
|
|
3743
|
+
else releaseAdapterAi = ctx.llm.registerAdapter([TRAE_AI_PROVIDER], trae.adapter);
|
|
3744
|
+
}
|
|
3127
3745
|
ctx.llm.registerModelDiscovery(TRAE_SETTINGS_NS, async (request, signal) => {
|
|
3128
|
-
|
|
3746
|
+
const region = regionOfTraeProvider(request.provider ?? "");
|
|
3747
|
+
if (region === void 0) return [];
|
|
3129
3748
|
const cancellation = signal ?? request.signal;
|
|
3130
|
-
return applyImageSelection(await discoverModels(cancellation), imageSet(current())).map((model) => ({
|
|
3749
|
+
return applyImageSelection(await stacks[region].discoverModels(cancellation), imageSet(current(), region)).map((model) => ({
|
|
3131
3750
|
id: model.id,
|
|
3132
3751
|
name: traeModelDisplayName(model),
|
|
3133
3752
|
...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
|
|
@@ -3135,31 +3754,46 @@ function apply(ctx, config) {
|
|
|
3135
3754
|
inputModalities: traeInputModalities(model)
|
|
3136
3755
|
}));
|
|
3137
3756
|
});
|
|
3138
|
-
releaseDirectory = ctx.llm.registerConfigurableProviders(
|
|
3139
|
-
provider:
|
|
3140
|
-
displayName:
|
|
3757
|
+
releaseDirectory = ctx.llm.registerConfigurableProviders(REGION_KEYS.map((region) => ({
|
|
3758
|
+
provider: TRAE_PROVIDERS[region],
|
|
3759
|
+
displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
|
|
3141
3760
|
settingsNs: TRAE_SETTINGS_NS,
|
|
3142
3761
|
settingsPath: [],
|
|
3143
3762
|
declared: false
|
|
3144
|
-
}
|
|
3763
|
+
})));
|
|
3145
3764
|
} finally {
|
|
3146
|
-
if (
|
|
3147
|
-
|
|
3765
|
+
if (releaseAdapterCn === void 0 || releaseAdapterAi === void 0 || releaseDirectory === void 0) {
|
|
3766
|
+
releaseAdapterCn?.();
|
|
3767
|
+
releaseAdapterAi?.();
|
|
3148
3768
|
releaseDirectory?.();
|
|
3149
3769
|
}
|
|
3150
3770
|
}
|
|
3151
3771
|
try {
|
|
3152
3772
|
ctx.effect(() => () => {
|
|
3153
|
-
|
|
3773
|
+
releaseAdapterCn?.();
|
|
3774
|
+
releaseAdapterAi?.();
|
|
3154
3775
|
releaseDirectory?.();
|
|
3155
3776
|
});
|
|
3156
3777
|
} catch {
|
|
3157
|
-
|
|
3778
|
+
releaseAdapterCn?.();
|
|
3779
|
+
releaseAdapterAi?.();
|
|
3158
3780
|
releaseDirectory?.();
|
|
3159
3781
|
}
|
|
3782
|
+
for (const region of REGION_KEYS) (async () => {
|
|
3783
|
+
const stack = stacks[region];
|
|
3784
|
+
try {
|
|
3785
|
+
const models = await stack.discoverModels();
|
|
3786
|
+
if (stopped) return;
|
|
3787
|
+
stack.catalog.set(derive(current(), models, region));
|
|
3788
|
+
} catch (error) {
|
|
3789
|
+
ctx.logger.warn(`dsh-connect-trae: live ${region} model directory unavailable; serving the configured catalog`, error);
|
|
3790
|
+
stack.catalog.set(configuredModels(current(), region));
|
|
3791
|
+
}
|
|
3792
|
+
stack.invalidateAdapter();
|
|
3793
|
+
})();
|
|
3160
3794
|
}).catch((error) => {
|
|
3161
|
-
ctx.logger.error("dsh-connect-trae: loopback shim failed;
|
|
3795
|
+
ctx.logger.error("dsh-connect-trae: loopback shim failed; providers not registered", error);
|
|
3162
3796
|
});
|
|
3163
3797
|
}
|
|
3164
3798
|
//#endregion
|
|
3165
|
-
export { Config, FALLBACK_TRAE_MODELS, SseDecoder, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, hashTraeRawChatArg, identityHeaders, inject, mergeTraeModelSources, name, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeIdentity, refreshTraeCredential, registerTraeUsageRoute, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage };
|
|
3799
|
+
export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, mergeTraeModelSources, name, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage, withTraeRegion };
|