dsh-connect-trae 2.2.0 → 2.3.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/lib/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import * as dshSettings from "@deepseek-ai/dsh-settings";
3
2
  import { createProvider } from "@earendil-works/pi-ai";
4
3
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
5
4
  import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
@@ -14,6 +13,7 @@ import { createServer } from "node:http";
14
13
  import { Readable } from "node:stream";
15
14
  import { execFile } from "node:child_process";
16
15
  import { promisify } from "node:util";
16
+ import { existsSync } from "node:fs";
17
17
  //#region src/catalog.ts
18
18
  /**
19
19
  * Bootstrap catalog: identity only where current Trae metadata has not been
@@ -150,12 +150,19 @@ function displayKey(name) {
150
150
  * skeleton: it supplies the display id, display name, context windows, credit
151
151
  * multiplier, reasoning and multimodal flags. `wire` (from `get_detail_param`)
152
152
  * supplies the real `llm_utils_chat` `config_name` — the only id the chat
153
- * endpoint actually accepts. A remote row is only callable when it maps to a
154
- * wire `config_name`, so a remote row with no wire match is DROPPED (it would
155
- * otherwise be sent as an invalid `config_name` and rejected with 4001
156
- * "param is invalid"). Verified 2026-08-30: the Remote directory advertises
157
- * `Doubao-Seed-Code` and `glm-5.3`, neither of which is a current `config_name`;
158
- * both fail every request, so they must not be exposed.
153
+ * endpoint actually accepts — plus the authoritative post-discount credit
154
+ * multiplier when `display_contact_config` carries one. A remote row is only
155
+ * callable when it maps to a wire `config_name`, so a remote row with no wire
156
+ * match is DROPPED (it would otherwise be sent as an invalid `config_name` and
157
+ * rejected with 4001 "param is invalid"). Verified 2026-08-30: the Remote
158
+ * directory advertises `Doubao-Seed-Code` and `glm-5.3`, neither of which is a
159
+ * current `config_name`; both fail every request, so they must not be exposed.
160
+ *
161
+ * Credit multiplier precedence: the wire's `display_contact_config` rate wins
162
+ * whenever present (it is the post-discount figure the Trae IDE renders — the
163
+ * Remote directory can report up to 10x the undiscounted value, see
164
+ * `wireCreditMultiplier` in solo.ts); the Remote `consumption_rate` is the
165
+ * fallback when the wire row carries none.
159
166
  *
160
167
  * Joining is two-tier, in priority order:
161
168
  * 1. `wire.id` (the `config_name`) equals the remote id — the model's display
@@ -177,12 +184,13 @@ function mergeTraeModelSources(remote, wire) {
177
184
  for (const model of remote) {
178
185
  const wireModel = wireById.get(displayKey(model.id)) ?? wireByName.get(displayKey(model.name));
179
186
  if (wireModel === void 0) continue;
187
+ const creditMultiplier = wireModel.creditMultiplier ?? model.creditMultiplier;
180
188
  result.push({
181
189
  id: model.id,
182
190
  name: model.name,
183
191
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
184
192
  ...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
185
- ...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
193
+ ...creditMultiplier === void 0 ? {} : { creditMultiplier },
186
194
  input: ["text"],
187
195
  reasoningSupported: model.reasoningSupported,
188
196
  ...model.reasoning === void 0 ? {} : {
@@ -793,6 +801,43 @@ const LINUX_APP_NAMES = {
793
801
  solo: ["trae-solo-cn", "TRAE SOLO CN"],
794
802
  "solo-sg": ["trae-solo", "TRAE SOLO"]
795
803
  };
804
+ /**
805
+ * Windows desktop config directory names.
806
+ *
807
+ * Trae is a VS Code-family Electron app, and that family names its per-user
808
+ * data directory from the installer-registered product name — which on Windows
809
+ * is `product.json`'s `win32DirName`, NOT the macOS bundle spelling. Measured
810
+ * from the shipped bundles (2026-09-26):
811
+ *
812
+ * Trae CN -> win32DirName = "Trae CN" (applicationName trae-cn)
813
+ * TRAE SOLO CN -> win32DirName = "TRAE SOLO CN" (applicationName trae-solo-cn)
814
+ *
815
+ * So the macOS spellings happen to be right here, and `win32DirName` is the
816
+ * authority that says so. The lowercase `applicationName` spellings
817
+ * (`trae-cn`, `trae-solo-cn`) are probed as well because the same family uses
818
+ * them for the Linux config directory, and which of the two a given installer
819
+ * writes has never been confirmed on a real Windows host
820
+ * (docs/WINDOWS_TOKEN_PROBE.md). Windows file systems are case-insensitive, so
821
+ * listing `trae cn` beside `Trae CN` would be the same directory twice — only
822
+ * genuinely different spellings are listed.
823
+ */
824
+ const WINDOWS_APP_NAMES = {
825
+ cn: ["Trae CN", "trae-cn"],
826
+ sg: ["Trae"],
827
+ solo: ["TRAE SOLO CN", "trae-solo-cn"],
828
+ "solo-sg": ["TRAE SOLO"]
829
+ };
830
+ /**
831
+ * One edition's plausible Windows directory spellings, in probe order.
832
+ *
833
+ * Exported so the identity reader and the credential scanner probe the SAME
834
+ * names: the two resolve different files (the install's `product.json` vs the
835
+ * per-user `storage.json`) but they describe one installation, and a spelling
836
+ * that works for one but not the other is a bug rather than a preference.
837
+ */
838
+ function traeWindowsAppNames(edition) {
839
+ return WINDOWS_APP_NAMES[edition];
840
+ }
796
841
  function traeStorageCandidates(platform = process.platform, home = homedir(), env = process.env) {
797
842
  const result = [];
798
843
  for (const edition of [
@@ -809,7 +854,7 @@ function traeStorageCandidates(platform = process.platform, home = homedir(), en
809
854
  appNames = [app];
810
855
  } else if (platform === "win32") {
811
856
  roots = [env.APPDATA, join(home, "AppData", "Roaming")].filter((value, index, all) => typeof value === "string" && value !== "" && all.indexOf(value) === index);
812
- appNames = [app];
857
+ appNames = WINDOWS_APP_NAMES[edition];
813
858
  } else if (platform === "linux") {
814
859
  roots = [env.XDG_CONFIG_HOME || join(home, ".config")];
815
860
  appNames = LINUX_APP_NAMES[edition];
@@ -1395,7 +1440,7 @@ async function readTraeIdentity(candidate, options = {}) {
1395
1440
  if (platform === "darwin") productPaths.push(join("/Applications", `${appName}.app`, "Contents", "Resources", "app", "product.json"));
1396
1441
  else {
1397
1442
  const localRoots = [env.LOCALAPPDATA, join(home, "AppData", "Local")].filter((value) => typeof value === "string" && value !== "").filter((value, index, all) => all.indexOf(value) === index);
1398
- for (const root of localRoots) productPaths.push(join(root, "Programs", appName, "resources", "app", "product.json"));
1443
+ for (const root of localRoots) for (const spelling of traeWindowsAppNames(candidate.edition)) productPaths.push(join(root, "Programs", spelling, "resources", "app", "product.json"));
1399
1444
  }
1400
1445
  }
1401
1446
  let product = {};
@@ -1934,6 +1979,39 @@ function prepareSoloBody(source, defaultModel = "glm-5.2", functionName) {
1934
1979
  }
1935
1980
  return JSON.stringify(body);
1936
1981
  }
1982
+ /**
1983
+ * Read the effective credit multiplier from a `get_detail_param` row.
1984
+ *
1985
+ * The rate the Trae IDE renders lives in `display_contact_config` — a *string*
1986
+ * holding a second JSON document — and its `consumption_rate.data.rate` is
1987
+ * already the post-discount value (verified 2026-09-13, commit 1.4.2: the
1988
+ * Remote directory reports the undiscounted figure, e.g. `0.8`, while the IDE
1989
+ * and this field both say `0.08` under a 限时 1 折 promotion — up to a 10x
1990
+ * difference). The wire figure therefore wins over the Remote one during the
1991
+ * merge; this parser is what feeds it.
1992
+ *
1993
+ * The international (ai) gateway serves no `consumption_rate` anywhere (its
1994
+ * subscription models carry only `features.cost` tags), so rows without the
1995
+ * field keep their bare name — parsed-but-absent, never fabricated.
1996
+ */
1997
+ function wireCreditMultiplier(config) {
1998
+ const raw = config["display_contact_config"];
1999
+ if (typeof raw !== "string" || raw === "") return void 0;
2000
+ let parsed;
2001
+ try {
2002
+ parsed = JSON.parse(raw);
2003
+ } catch {
2004
+ return;
2005
+ }
2006
+ if (typeof parsed !== "object" || parsed === null) return void 0;
2007
+ const consumption = parsed["consumption_rate"];
2008
+ if (typeof consumption !== "object" || consumption === null) return void 0;
2009
+ const entry = consumption;
2010
+ if (entry["enable"] !== true) return void 0;
2011
+ const data = entry["data"];
2012
+ if (typeof data !== "object" || data === null) return void 0;
2013
+ return finitePositive$1(data["rate"]);
2014
+ }
1937
2015
  var TraeSoloUpstreamClient = class {
1938
2016
  options;
1939
2017
  fetchImpl;
@@ -2012,6 +2090,7 @@ var TraeSoloUpstreamClient = class {
2012
2090
  ...config,
2013
2091
  ...detail
2014
2092
  });
2093
+ const creditMultiplier = wireCreditMultiplier(config);
2015
2094
  if (byId.has(id)) continue;
2016
2095
  byId.set(id, {
2017
2096
  id,
@@ -2019,6 +2098,7 @@ var TraeSoloUpstreamClient = class {
2019
2098
  ...contextWindow === void 0 ? {} : { contextWindow },
2020
2099
  ...maxTokens === void 0 ? {} : { maxTokens },
2021
2100
  ...reasoning === void 0 ? {} : { reasoning },
2101
+ ...creditMultiplier === void 0 ? {} : { creditMultiplier },
2022
2102
  function: directoryFunction
2023
2103
  });
2024
2104
  }
@@ -2910,6 +2990,8 @@ function createTraeRawGateway(options) {
2910
2990
  //#endregion
2911
2991
  //#region src/model-cache.ts
2912
2992
  const execFileAsync = promisify(execFile);
2993
+ /** Basename of the SQLite cache sitting beside the credential document. */
2994
+ const TRAE_STATE_DB_FILENAME = "state.vscdb";
2913
2995
  function positive$1(value) {
2914
2996
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
2915
2997
  }
@@ -2937,18 +3019,59 @@ function parseTraeCachedModel(value) {
2937
3019
  };
2938
3020
  }
2939
3021
  /**
3022
+ * The cache paths to try, most-specific first.
3023
+ *
3024
+ * `state.vscdb` is a SIBLING of the credential document (both sit in
3025
+ * `globalStorage`), so a candidate's own path is the authority for its cache —
3026
+ * this is why the database is derived from {@link traeStorageCandidates} rather
3027
+ * than re-spelled here.
3028
+ *
3029
+ * It previously hardcoded the single macOS spelling `Trae CN`, which was wrong
3030
+ * in two ways on Windows: `paths.ts` probes `Trae CN` AND `trae-cn`, and the
3031
+ * edition actually installed is often SOLO (`TRAE SOLO CN`). This module would
3032
+ * then point at a directory that does not exist. That went unnoticed only
3033
+ * because the Windows `sqlite3` dependency is absent, so the lookup failed and
3034
+ * callers fell back — masking the wrong path behind a different error. Sharing
3035
+ * one path table means a spelling that works for sign-in also works for the
3036
+ * cache.
3037
+ */
3038
+ function traeStateDatabaseCandidates(options = {}) {
3039
+ const platform = options.platform ?? process.platform;
3040
+ const home = options.home ?? homedir();
3041
+ const env = options.env ?? process.env;
3042
+ const paths = [];
3043
+ const push = (storagePath) => {
3044
+ const database = join(dirname(storagePath), TRAE_STATE_DB_FILENAME);
3045
+ if (!paths.some((existing) => existing.toLowerCase() === database.toLowerCase())) paths.push(database);
3046
+ };
3047
+ if (options.candidate !== void 0 && options.candidate.source === "desktop") push(options.candidate.path);
3048
+ for (const candidate of traeStorageCandidates(platform, home, env)) {
3049
+ if (candidate.source !== "desktop") continue;
3050
+ if (options.candidate !== void 0 && candidate.edition !== options.candidate.edition) continue;
3051
+ push(candidate.path);
3052
+ }
3053
+ return paths;
3054
+ }
3055
+ /**
2940
3056
  * Read Trae's own current user's model map via sqlite3 without exposing
2941
3057
  * secrets. The sqlite3 command line is a macOS prerequisite; on Windows it is
2942
3058
  * typically absent, so the call fails and callers fall back gracefully.
3059
+ *
3060
+ * Every candidate database is tried in order and the first that EXISTS is
3061
+ * queried. Existence is checked before spawning because a missing file and a
3062
+ * missing `sqlite3` binary are different problems: choosing the wrong directory
3063
+ * would otherwise surface as a confusing `sqlite3` failure, hiding a path bug
3064
+ * behind a dependency error.
2943
3065
  */
2944
3066
  async function readTraeCachedModel(functionName, modelName, userId, options = {}) {
2945
- const platform = options.platform ?? process.platform;
2946
- const home = options.home ?? homedir();
2947
- const env = options.env ?? process.env;
2948
- const database = platform === "win32" ? join(env.APPDATA ?? join(home, "AppData", "Roaming"), "Trae CN", "User", "globalStorage", "state.vscdb") : join(home, "Library", "Application Support", "Trae CN", "User", "globalStorage", "state.vscdb");
3067
+ const exists = options.exists ?? existsSync;
3068
+ const databases = traeStateDatabaseCandidates(options);
3069
+ const fallback = databases[0];
3070
+ if (fallback === void 0) throw new Error("trae: no desktop storage candidate to locate the model cache from");
3071
+ const database = databases.find((candidate) => exists(candidate)) ?? fallback;
2949
3072
  const key = `${userId}_AI.agent.model.model_list_map`;
2950
3073
  const sql = `select value from ItemTable where key=${JSON.stringify(key)} limit 1;`;
2951
- const { stdout } = await execFileAsync("sqlite3", [database, sql], { maxBuffer: 8388608 });
3074
+ const { stdout } = await (options.runSqlite ?? ((db, statement) => execFileAsync("sqlite3", [db, statement], { maxBuffer: 8388608 })))(database, sql);
2952
3075
  const document = JSON.parse(stdout);
2953
3076
  return parseTraeCachedModel((Array.isArray(document[functionName]) ? document[functionName] : []).find((item) => typeof item === "object" && item !== null && item.name === modelName));
2954
3077
  }
@@ -2960,7 +3083,7 @@ async function resolveTraeRawRuntime(store, modelName) {
2960
3083
  const candidate = store.candidates().find((item) => item.edition === credential.edition);
2961
3084
  if (candidate === void 0) throw new Error(`no Trae ${credential.edition} storage candidate for Raw Chat identity`);
2962
3085
  const identity = await readTraeIdentity(candidate);
2963
- const cached = credential.edition === "cn" ? await readTraeCachedModel("solo_agent", modelName, credential.userId).catch(() => void 0) : void 0;
3086
+ const cached = credential.edition === "cn" ? await readTraeCachedModel("solo_agent", modelName, credential.userId, { candidate }).catch(() => void 0) : void 0;
2964
3087
  return {
2965
3088
  identity,
2966
3089
  runtime: buildTraeRawChatRuntimeConfig(modelName, cached, {
@@ -3160,6 +3283,12 @@ var TraeUsageClient = class {
3160
3283
  /**
3161
3284
  * Daily check-in status. Read-only, and answered with or without the device
3162
3285
  * header — the claim is what needs it.
3286
+ *
3287
+ * The read DOES carry the header whenever this installation has one, and
3288
+ * that is deliberate rather than incidental: `did_checked_in` is answered
3289
+ * per device, so omitting the header would make a device that already claimed
3290
+ * today look identical to one that never has (measured 2026-09-26, see
3291
+ * {@link TraeCheckinStatus.didCheckedIn}).
3163
3292
  */
3164
3293
  async checkinStatus(signal) {
3165
3294
  await this.requireCnRegion("check-in status");
@@ -3381,6 +3510,16 @@ function nextRegionEnabled(value, region, enabled) {
3381
3510
  }
3382
3511
  //#endregion
3383
3512
  //#region src/web-status.ts
3513
+ /**
3514
+ * The upstream's business code for "该设备今日已参与签到" — this DEVICE already
3515
+ * used today's check-in (measured 2026-09-26 on a CN account whose claim was
3516
+ * refused with exactly this code after an account switch on the same machine).
3517
+ *
3518
+ * It arrives as HTTP 200 with a non-zero `code`, which is why it has to be
3519
+ * matched on the code rather than on the HTTP status: read as a transport
3520
+ * failure it looks like the plugin is broken, when the day is simply spent.
3521
+ */
3522
+ const CHECKIN_DEVICE_ALREADY_CLAIMED = 9095;
3384
3523
  /** Redact token-like content before it crosses to the browser. */
3385
3524
  function safeMessage(error) {
3386
3525
  return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 500);
@@ -3577,11 +3716,22 @@ function registerTraeUsageRoute(ctx, deps) {
3577
3716
  /**
3578
3717
  * Daily check-in claim — the ONLY route in this plugin that mutates
3579
3718
  * upstream account state, so it is guarded more tightly than its siblings:
3580
- * POST only, loopback origin only, and the status read runs FIRST so an
3581
- * already-claimed day never reaches the upstream claim at all. The upstream
3582
- * is idempotent per Beijing day anyway (verified 2026-09-24: a repeat claim
3583
- * answers `code: 0` with the entitlement total unchanged), but relying on
3584
- * that for correctness would put the guard in someone else's hands.
3719
+ * POST only, loopback origin only, and the status read runs FIRST so a day
3720
+ * the ACCOUNT has already been paid for never reaches the upstream claim.
3721
+ *
3722
+ * `alreadyCheckedIn` and `deviceCheckedIn` are deliberately separate
3723
+ * answers, because the two flags mean different things (see
3724
+ * {@link TraeWebCheckin}): `checkedIn` is the account's reward for today
3725
+ * existing, `didCheckedIn` is this MACHINE having spent its check-in —
3726
+ * possibly for a different account. Treating the latter as "claimed today"
3727
+ * was a shipped bug: after switching accounts the card said "claimed
3728
+ * today" and disabled the button, which is the right call for the wrong
3729
+ * reason and hides that the new account was never rewarded.
3730
+ *
3731
+ * The upstream is idempotent per Beijing day (verified 2026-09-24: a
3732
+ * repeat claim answers `code: 0` with the entitlement total unchanged), but
3733
+ * relying on that for correctness would put the guard in someone else's
3734
+ * hands.
3585
3735
  */
3586
3736
  const disposeCheckin = ctx.webServer.register({
3587
3737
  kind: "exact",
@@ -3596,16 +3746,27 @@ function registerTraeUsageRoute(ctx, deps) {
3596
3746
  const client = deps.client(region);
3597
3747
  const current = await client.checkinStatus();
3598
3748
  if (!current.enabled) return json(res, 409, { error: "check-in is not enabled for this account" });
3599
- if (current.checkedIn || current.didCheckedIn) return json(res, 200, {
3749
+ if (current.checkedIn) return json(res, 200, {
3600
3750
  claimed: false,
3601
3751
  alreadyCheckedIn: true,
3752
+ deviceCheckedIn: current.didCheckedIn,
3753
+ checkin: toCheckin(current)
3754
+ });
3755
+ if (current.didCheckedIn) return json(res, 200, {
3756
+ claimed: false,
3757
+ alreadyCheckedIn: false,
3758
+ deviceCheckedIn: true,
3759
+ code: CHECKIN_DEVICE_ALREADY_CLAIMED,
3760
+ message: "this device already used today's check-in",
3602
3761
  checkin: toCheckin(current)
3603
3762
  });
3604
3763
  const claim = await client.claimCheckin();
3605
3764
  const checkin = await client.checkinStatus();
3765
+ const deviceClaimed = claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED;
3606
3766
  json(res, 200, {
3607
3767
  claimed: claim.claimed,
3608
3768
  alreadyCheckedIn: false,
3769
+ ...deviceClaimed ? { deviceCheckedIn: true } : {},
3609
3770
  code: claim.code,
3610
3771
  message: claim.message,
3611
3772
  checkin: toCheckin(checkin)
@@ -3722,6 +3883,64 @@ function buildTraeModelDetailRequest(configName = "", custom = false) {
3722
3883
  };
3723
3884
  }
3724
3885
  //#endregion
3886
+ //#region src/redact.ts
3887
+ /**
3888
+ * Redaction helpers for diagnostics that a user may paste into a public issue.
3889
+ *
3890
+ * A verification report is only useful if it can be shared, and it is only safe
3891
+ * to share if it carries no personal identifiers. Paths are the hard case:
3892
+ * `C:\Users\<real name>\AppData\Roaming\Trae CN\...` contains both a real
3893
+ * account name AND the single most diagnostic token in the whole report (the
3894
+ * Trae data directory spelling). Redacting the whole path would make the report
3895
+ * worthless; printing it whole would leak the user's name. So the user segment
3896
+ * is replaced and the meaningful tail is preserved.
3897
+ *
3898
+ * @module dsh-connect-trae/redact
3899
+ */
3900
+ /**
3901
+ * Replace the user segment of a path with `<user>`, keeping the rest intact.
3902
+ *
3903
+ * Handles the shapes a Trae install can produce:
3904
+ * - `C:\Users\<name>\...` and `C:\Documents and Settings\<name>\...` (Windows)
3905
+ * - `/Users/<name>/...` and `/home/<name>/...` (macOS / Linux)
3906
+ *
3907
+ * Anything after that segment — edition directory, `User\globalStorage`,
3908
+ * `storage.json` — is preserved verbatim, because that is what a report is
3909
+ * actually read for. Cross-platform on purpose: the function is called by
3910
+ * Windows-focused diagnostics, but the same helpers run in tests and on other
3911
+ * hosts, and a report from the wrong platform must not leak either.
3912
+ */
3913
+ function maskUserPath(path) {
3914
+ return String(path).replace(/([A-Za-z]:\\Users\\)[^\\]+/i, "$1<user>").replace(/([A-Za-z]:\\Documents and Settings\\)[^\\]+/i, "$1<user>").replace(/^(\/(?:Users|home)\/)[^/]+/, "$1<user>");
3915
+ }
3916
+ /**
3917
+ * Describe a name by shape instead of by value: a character count, plus whether
3918
+ * it is purely numeric.
3919
+ *
3920
+ * This is the deliberate middle ground for account names. A Trae account name
3921
+ * is often a real name or a phone number, so it must never be printed; but the
3922
+ * report still needs to show that an account WAS resolved, and the shape
3923
+ * distinguishes a real nickname from a generated `用户<n>` id — which is the
3924
+ * only thing a reader needs to judge whether account discovery worked.
3925
+ */
3926
+ function describeNameShape(name) {
3927
+ const text = String(name ?? "");
3928
+ return `${text.length} 字符${/^\d+$/.test(text) ? ",纯数字" : ""}`;
3929
+ }
3930
+ /**
3931
+ * Describe an identifier by shape instead of by value.
3932
+ *
3933
+ * A device id (`x-device-id`) is a stable installation identifier and is
3934
+ * directly tied to the upstream's per-device rules, so it is never printed.
3935
+ * Its shape still proves which resolution path ran: a real `icube-dc` id is
3936
+ * 15 digits, while the fallback is a 32-character hex hash.
3937
+ */
3938
+ function describeIdShape(value) {
3939
+ const text = String(value ?? "");
3940
+ if (text === "") return "(空)";
3941
+ return `${text.length} 位${/^\d+$/.test(text) ? ",纯数字" : ",含非数字"}`;
3942
+ }
3943
+ //#endregion
3725
3944
  //#region src/raw-envelope.ts
3726
3945
  /** Stable JSON hash candidate used by the native DTO's arg/args_hash fields. */
3727
3946
  function hashTraeRawChatArg(arg) {
@@ -3761,14 +3980,34 @@ const name = "dsh-connect-trae";
3761
3980
  const inject = ["llm"];
3762
3981
  /**
3763
3982
  * Settings namespace, as a plain lowercase literal. DSH 0.1.2 removed the
3764
- * `settingsNamespace` brand constructor from the npm package (the Desktop
3765
- * host still ships it as a legacy shim), and every consumer of this constant —
3766
- * `registerModelDiscovery`, `registerConfigurableProviders`,
3767
- * `settings.installSection`, the settings test — takes it as a kebab string,
3768
- * so no branding is needed for either host generation.
3983
+ * `settingsNamespace` brand constructor from the npm package, and every
3984
+ * consumer of this constant takes it as a kebab string — so no branding is
3985
+ * needed. It is the FALLBACK namespace; the namespace the host actually serves
3986
+ * comes from {@link settingsNamespaceOf} (0.1.7 keys settings by the Loader
3987
+ * entry id, which a profile patch may mount as `dsh-connect-trae` or
3988
+ * `include:dsh-connect-trae`).
3769
3989
  */
3770
3990
  const TRAE_SETTINGS_NS = "trae";
3771
3991
  /**
3992
+ * The namespace the HOST actually serves this plugin under.
3993
+ *
3994
+ * On DSH 0.1.7 `SettingsForms.describe()` keys every namespace by the Loader
3995
+ * entry id, and the harness looks a provider's namespace up by EXACT match —
3996
+ * advertising `trae` while the host serves `include:dsh-connect-trae` made the
3997
+ * provider read as "not configured": its configure affordance and model
3998
+ * discovery both silently went dead.
3999
+ *
4000
+ * `ctx.fiber.entry` is added by the Loader, not by Cordis itself, so it is not
4001
+ * in Cordis's public types and is absent on hosts that mount a plugin without a
4002
+ * Loader entry (a test harness, or `ctx.plugin()` called directly). Hence the
4003
+ * probe plus the documented fallback, mirroring the first-party plugins:
4004
+ * `const settingsNs = ctx.fiber.entry?.options.id ?? NS`.
4005
+ */
4006
+ function settingsNamespaceOf(ctx) {
4007
+ const id = ctx?.fiber?.entry?.options?.id;
4008
+ return typeof id === "string" && id !== "" ? id : TRAE_SETTINGS_NS;
4009
+ }
4010
+ /**
3772
4011
  * One region's saved state. A config written before the region split has only
3773
4012
  * the flat fields: those were always captured from the CN endpoint (the
3774
4013
  * plugin had no international support), so they are read as the CN state and
@@ -3800,23 +4039,18 @@ function regionEnabled(config, region) {
3800
4039
  return regionStateOf(config, region).enabled !== false;
3801
4040
  }
3802
4041
  /**
3803
- * Mark a schema's field as volatile on the DSH lines that support it.
4042
+ * Mark a schema's field as volatile.
3804
4043
  *
3805
- * `volatile()` exists from schemastery 3.18.3 (the DSH 0.1.7 line, which is the
3806
- * only line whose settings write gate reads the marker). Older pinning (3.18.2,
3807
- * the 0.1.5 line) has no such method, and the schema must stay byte-identical
3808
- * to the unmarked original there: hand-writing `meta.volatile = true` would
3809
- * bypass schemastery's own `validateVolatileSchema` checks and produce a schema
3810
- * no 0.1.5 consumer understands — so on that line this degrades to an identity
3811
- * no-op.
4044
+ * `volatile()` exists from schemastery 3.18.3; since 2.3.0 the peer range
4045
+ * requires `>=3.18.4`, so it is always present on a supported host. The probe
4046
+ * is kept as a safety net for a consumer that resolves an older schemastery
4047
+ * anyway (where the helper must degrade to identity — hand-writing
4048
+ * `meta.volatile = true` would bypass schemastery's own `validateVolatileSchema`
4049
+ * checks) and to keep both arms deterministically testable.
3812
4050
  *
3813
4051
  * Without the marker, 0.1.7's settings write gate REJECTS every write with
3814
4052
  * `Plugin entry "trae" has no volatile fields` while the scope's `set()` still
3815
4053
  * resolves — so the card would look like it saved and silently revert.
3816
- *
3817
- * Exported so BOTH arms are testable on any machine: the capability is decided
3818
- * by whichever schemastery the dependency tree resolves, so without a seam the
3819
- * no-op arm would silently stop being exercised the moment the pin moved up.
3820
4054
  */
3821
4055
  function asVolatile(schema) {
3822
4056
  if (typeof schema.volatile === "function") return schema.volatile();
@@ -3863,6 +4097,7 @@ const Config = z.object({
3863
4097
  /** Every region, in card tab order. */
3864
4098
  const REGION_KEYS = ["cn", "ai"];
3865
4099
  function apply(ctx, config) {
4100
+ const settingsNs = settingsNamespaceOf(ctx);
3866
4101
  let current = () => config;
3867
4102
  const enabledSet = (value, region) => new Set(regionStateOf(value, region).enabledModelIds ?? []);
3868
4103
  const imageSet = (value, region) => new Set(regionStateOf(value, region).imageModelIds ?? []);
@@ -4100,7 +4335,7 @@ function apply(ctx, config) {
4100
4335
  registration.cn.directory?.replace(REGION_KEYS.filter((region) => regionEnabled(value, region)).map((region) => ({
4101
4336
  provider: TRAE_PROVIDERS[region],
4102
4337
  displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
4103
- settingsNs: TRAE_SETTINGS_NS,
4338
+ settingsNs,
4104
4339
  settingsPath: [],
4105
4340
  declared: false
4106
4341
  })));
@@ -4115,14 +4350,6 @@ function apply(ctx, config) {
4115
4350
  }
4116
4351
  syncRegionRegistration(value);
4117
4352
  };
4118
- const sectionHooks = {
4119
- setSource(source) {
4120
- current = source;
4121
- },
4122
- onChange() {
4123
- applySelection(current());
4124
- }
4125
- };
4126
4353
  applySelection(config);
4127
4354
  (async () => {
4128
4355
  const id = current().accountId;
@@ -4135,18 +4362,9 @@ function apply(ctx, config) {
4135
4362
  }
4136
4363
  } catch {}
4137
4364
  })();
4138
- const legacyInstallSettingsSection = dshSettings.installSettingsSection;
4139
- if (typeof legacyInstallSettingsSection === "function") legacyInstallSettingsSection(ctx, TRAE_SETTINGS_NS, Config, config, sectionHooks);
4140
- else ctx.inject(["settings"], (settingsCtx) => {
4365
+ ctx.inject(["settings"], (settingsCtx) => {
4141
4366
  const settings = settingsCtx.settings;
4142
- if (typeof settings.configure === "function") {
4143
- settings.configure({ auto: true }, ctx.fiber);
4144
- return;
4145
- }
4146
- if (typeof settings.installSection === "function") {
4147
- const entry = unwrapVolatileDeep(config);
4148
- settings.installSection(ctx, TRAE_SETTINGS_NS, Config, entry, sectionHooks);
4149
- }
4367
+ ctx.effect(() => settings.configure({ auto: true }, ctx.fiber));
4150
4368
  });
4151
4369
  ctx.on("loader/volatile-update", () => {
4152
4370
  applySelection(current());
@@ -4173,7 +4391,7 @@ function apply(ctx, config) {
4173
4391
  };
4174
4392
  registration[region].adapter = region === "cn" ? ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter) : ctx.llm.registerAdapter([TRAE_AI_PROVIDER], trae.adapter);
4175
4393
  }
4176
- ctx.llm.registerModelDiscovery(TRAE_SETTINGS_NS, async (request, signal) => {
4394
+ ctx.llm.registerModelDiscovery(settingsNs, async (request, signal) => {
4177
4395
  const region = regionOfTraeProvider(request.provider ?? "");
4178
4396
  if (region === void 0) return [];
4179
4397
  if (!regionEnabled(current(), region)) return [];
@@ -4189,7 +4407,7 @@ function apply(ctx, config) {
4189
4407
  registration.cn.directory = ctx.llm.registerConfigurableProviders(REGION_KEYS.map((region) => ({
4190
4408
  provider: TRAE_PROVIDERS[region],
4191
4409
  displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
4192
- settingsNs: TRAE_SETTINGS_NS,
4410
+ settingsNs,
4193
4411
  settingsPath: [],
4194
4412
  declared: false
4195
4413
  })));
@@ -4232,4 +4450,4 @@ function apply(ctx, config) {
4232
4450
  });
4233
4451
  }
4234
4452
  //#endregion
4235
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, 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, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStorageCandidates, traeWebUsage, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
4453
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, 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_STATE_DB_FILENAME, 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, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyTraeRawChatFailure, createTraeAdapter, createTraeRawGateway, createTraeShim, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, pickTraeStorageIdentity, prepareSoloBody, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withTraeRegion };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-connect-trae",
3
3
  "displayName": "DSH Connect Trae",
4
- "description": "Connect locally signed-in Trae models to DeepSeek Harness, with a usage/credits summary and daily check-in claiming.",
5
- "version": "2.2.0",
4
+ "description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
5
+ "version": "2.3.1",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -20,6 +20,7 @@
20
20
  },
21
21
  "main": "lib/index.js",
22
22
  "types": "lib/index.d.ts",
23
+ "icon": "icons/dsh-connect-trae-128.png",
23
24
  "exports": {
24
25
  ".": {
25
26
  "types": "./lib/index.d.ts",
@@ -31,7 +32,9 @@
31
32
  },
32
33
  "files": [
33
34
  "lib",
35
+ "icons",
34
36
  "docs/assets/dsh-connect-trae-usage-card.png",
37
+ "scripts/verify-windows.mjs",
35
38
  "screenshots.json",
36
39
  "cordis.patch.yml",
37
40
  "README.md",
@@ -72,34 +75,34 @@
72
75
  "url": "https://github.com/dingminhua/dsh-connect-trae/issues"
73
76
  },
74
77
  "peerDependencies": {
75
- "@deepseek-ai/cordis": "^4.0.1-rc.1",
76
- "@deepseek-ai/dsh-atomic-write": ">=0.1.5-0 <0.2.0-0",
77
- "@deepseek-ai/dsh-attachment": ">=0.1.5-0 <0.2.0-0",
78
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.5-0 <0.2.0-0",
79
- "@deepseek-ai/dsh-home-paths": ">=0.1.5-0 <0.2.0-0",
80
- "@deepseek-ai/dsh-host-webserver": ">=0.1.5-0 <0.2.0-0",
81
- "@deepseek-ai/dsh-llm": ">=0.1.5-0 <0.2.0-0",
82
- "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.5-0 <0.2.0-0",
83
- "@deepseek-ai/dsh-settings": ">=0.1.5-0 <0.2.0-0",
84
- "@deepseek-ai/schemastery": "^3.18.1-rc.1",
78
+ "@deepseek-ai/cordis": ">=4.0.4 <5.0.0",
79
+ "@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.2.0-0",
80
+ "@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.2.0-0",
81
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.2.0-0",
82
+ "@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.2.0-0",
83
+ "@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.2.0-0",
84
+ "@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.2.0-0",
85
+ "@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.2.0-0",
86
+ "@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.2.0-0",
87
+ "@deepseek-ai/schemastery": ">=3.18.4 <4.0.0",
85
88
  "@earendil-works/pi-ai": ">=0.85.1",
86
89
  "react": "^18.2.0"
87
90
  },
88
91
  "devDependencies": {
89
- "@deepseek-ai/cordis": "^4.0.2",
90
- "@deepseek-ai/dsh-atomic-write": "0.1.5-rc.2",
91
- "@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
92
- "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
93
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
94
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
95
- "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
96
- "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.5-rc.2",
97
- "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
98
- "@deepseek-ai/dsh-home-paths": "0.1.5-rc.2",
99
- "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
100
- "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
101
- "@deepseek-ai/dsh-llm-pi-ai": "0.1.5-rc.2",
102
- "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
92
+ "@deepseek-ai/cordis": "^4.0.4",
93
+ "@deepseek-ai/dsh-atomic-write": "0.1.7-rc.1",
94
+ "@deepseek-ai/dsh-attachment": "0.1.7-rc.1",
95
+ "@deepseek-ai/dsh-client-locale": "0.1.7-rc.1",
96
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.1",
97
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.1",
98
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.1",
99
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.7-rc.1",
100
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.1",
101
+ "@deepseek-ai/dsh-home-paths": "0.1.7-rc.1",
102
+ "@deepseek-ai/dsh-host-webserver": "0.1.7-rc.1",
103
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.1",
104
+ "@deepseek-ai/dsh-llm-pi-ai": "0.1.7-rc.1",
105
+ "@deepseek-ai/dsh-settings": "0.1.7-rc.1",
103
106
  "@deepseek-ai/schemastery": "3.18.4",
104
107
  "@earendil-works/pi-ai": "^0.85.1",
105
108
  "@testing-library/dom": "^10.4.2",