dsh-connect-trae 4.4.4 → 4.4.5

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
@@ -3513,6 +3513,182 @@ function identityHeaders(identity) {
3513
3513
  };
3514
3514
  }
3515
3515
  //#endregion
3516
+ //#region src/model-order.ts
3517
+ /**
3518
+ * Wire name of the default SOLO chat function.
3519
+ *
3520
+ * Lives here rather than in `solo.ts` so this module stays importable from the
3521
+ * client; `solo.ts` re-exports it, so there is still exactly one definition.
3522
+ */
3523
+ const TRAE_SOLO_FUNCTION = "solo_work_lite";
3524
+ /**
3525
+ * The chat functions of one region, in PRIORITY order.
3526
+ *
3527
+ * Two consumers read it, and both mean "priority", which is why they can share
3528
+ * one array: the wire asks these functions in order and the first to list a
3529
+ * config owns it (`solo.ts`), and the card draws the groups in the same order so
3530
+ * the row a user sees first is the one the resolver prefers.
3531
+ */
3532
+ const TRAE_CHANNEL_PRIORITY = {
3533
+ cn: [
3534
+ "solo_work_remote",
3535
+ TRAE_SOLO_FUNCTION,
3536
+ "solo_agent_remote",
3537
+ "chat_v3",
3538
+ "solo_coder"
3539
+ ],
3540
+ ai: [
3541
+ "solo_work_remote",
3542
+ TRAE_SOLO_FUNCTION,
3543
+ "solo_agent_remote",
3544
+ "chat_v3"
3545
+ ]
3546
+ };
3547
+ /**
3548
+ * Rank of one channel inside the region's priority list.
3549
+ *
3550
+ * Three cases, and the order between them is deliberate:
3551
+ *
3552
+ * - a known chat channel: its index in the priority list;
3553
+ * - an UNKNOWN channel name (a function the region does not call through —
3554
+ * `withChatCallableFunctions` clears those, so this is a defensive branch):
3555
+ * after every known channel, then alphabetically among themselves;
3556
+ * - NO channel at all (a row demoted to the default chat function): last. Such
3557
+ * a row is the one the resolver knows least about, so it must not push a
3558
+ * properly attributed row down the list.
3559
+ */
3560
+ function channelRank(channel, priority) {
3561
+ if (channel === void 0 || channel === "") return priority.length + 1;
3562
+ const known = priority.indexOf(channel);
3563
+ return known === -1 ? priority.length : known;
3564
+ }
3565
+ /**
3566
+ * Compare two display names the way a person reading a model list expects:
3567
+ * case-insensitively, with embedded numbers compared as NUMBERS, so `GLM-5.10`
3568
+ * sorts after `GLM-5.9`. Plain text comparison puts `5.10` first, which reads as
3569
+ * a bug to anyone who sees the version numbers.
3570
+ */
3571
+ function compareNames(left, right) {
3572
+ const byName = left.trim().localeCompare(right.trim(), void 0, {
3573
+ numeric: true,
3574
+ sensitivity: "base"
3575
+ });
3576
+ return byName !== 0 ? byName : left.localeCompare(right);
3577
+ }
3578
+ /**
3579
+ * The catalog in display order: channel priority first, then name.
3580
+ *
3581
+ * Total and stable — ties fall through to `id`, so two rows with the same name
3582
+ * in the same channel still get one fixed order rather than the order they
3583
+ * arrived in.
3584
+ */
3585
+ function orderTraeModels(rows, region = "cn") {
3586
+ const priority = TRAE_CHANNEL_PRIORITY[region] ?? TRAE_CHANNEL_PRIORITY.cn;
3587
+ return [...rows].sort((left, right) => {
3588
+ const byRank = channelRank(left.wireFunction, priority) - channelRank(right.wireFunction, priority);
3589
+ if (byRank !== 0) return byRank;
3590
+ if (left.wireFunction !== right.wireFunction) return (left.wireFunction ?? "").localeCompare(right.wireFunction ?? "");
3591
+ const byName = compareNames(left.name, right.name);
3592
+ return byName !== 0 ? byName : left.id.localeCompare(right.id);
3593
+ });
3594
+ }
3595
+ /**
3596
+ * Split rows into channel groups, in display order.
3597
+ *
3598
+ * Ordering is re-applied here rather than trusted from the caller: a group list
3599
+ * built from an unsorted input would put the groups in arrival order, which is
3600
+ * the exact instability this module exists to remove.
3601
+ */
3602
+ function groupTraeModels(rows, region = "cn") {
3603
+ const groups = [];
3604
+ const byChannel = /* @__PURE__ */ new Map();
3605
+ for (const row of orderTraeModels(rows, region)) {
3606
+ const channel = row.wireFunction === "" ? void 0 : row.wireFunction;
3607
+ const key = channel ?? "\0default";
3608
+ const existing = byChannel.get(key);
3609
+ if (existing !== void 0) {
3610
+ existing.rows.push(row);
3611
+ continue;
3612
+ }
3613
+ const group = {
3614
+ channel,
3615
+ rows: [row]
3616
+ };
3617
+ byChannel.set(key, group);
3618
+ groups.push(group);
3619
+ }
3620
+ return groups;
3621
+ }
3622
+ /**
3623
+ * Does one row match a search query?
3624
+ *
3625
+ * Matches the display name, the display id and the WIRE config name — the last
3626
+ * one because a user debugging a `4001 config_name rejected` arrives with the
3627
+ * upstream name in hand, and the two differ exactly on the rows that carry a
3628
+ * suffix (`DeepSeek-V4-Flash` vs `DeepSeek-V4-Flash-Official`).
3629
+ */
3630
+ function traeModelMatchesQuery(model, query) {
3631
+ const needle = query.trim().toLowerCase();
3632
+ if (needle === "") return true;
3633
+ if (model.name.toLowerCase().includes(needle)) return true;
3634
+ if (model.id.toLowerCase().includes(needle)) return true;
3635
+ return (model.wireConfigName ?? "").toLowerCase().includes(needle);
3636
+ }
3637
+ /** Filter by {@link traeModelMatchesQuery}, preserving order. */
3638
+ function filterTraeModels(rows, query) {
3639
+ if (query.trim() === "") return [...rows];
3640
+ return rows.filter((row) => traeModelMatchesQuery(row, query));
3641
+ }
3642
+ /**
3643
+ * The channels a NORMAL Trae chat session actually runs on: the `solo_work_*`
3644
+ * family, i.e. the panel's own chat modes.
3645
+ *
3646
+ * Deliberately not "every channel we can call". After issue #52 P1 the catalog
3647
+ * is 34 rows and 10 of them are reachable only through a SPECIALIST channel:
3648
+ * `solo_coder` (7: `Doubao-Seed-2.0-Code`, `glm-5.1`, `glm-5v-turbo`,
3649
+ * `kimi-k2.5`, `minimax-m2.7`, `qwen-3.5`, `qwen-3.6-plus`), `chat_v3` (2:
3650
+ * `kimi-k2.8-preview`, `qwen3.8-flash`) and `solo_agent_remote` (1:
3651
+ * `Doubao-Seed-Code`). Measured 2026-10-23 on a live CN account. Those rows work
3652
+ * — that is why P1 kept them — but they are not what Trae's main chat offers,
3653
+ * and mixing them into the default view is exactly the "a code model answering a
3654
+ * solo request" shape issue #52 asked about.
3655
+ */
3656
+ const TRAE_COMMON_FUNCTIONS = {
3657
+ cn: ["solo_work_remote", TRAE_SOLO_FUNCTION],
3658
+ ai: ["solo_work_remote", TRAE_SOLO_FUNCTION]
3659
+ };
3660
+ /**
3661
+ * The function a request for this row will NAME.
3662
+ *
3663
+ * A row with no `wireFunction` is not "unknown to the wire": the bridge sends
3664
+ * the region default (`TRAE_SOLO_FUNCTION`), so for every question about where
3665
+ * the turn goes, the default is the answer. The distinction still matters for
3666
+ * DISPLAY (`groupTraeModels` keeps these in their own group, because their
3667
+ * provenance — which directory group advertised them — really is unknown), but
3668
+ * not for scope: a `solo_work_lite` turn is a normal chat turn either way.
3669
+ */
3670
+ function traeModelCallChannel(model) {
3671
+ const channel = model.wireFunction;
3672
+ return channel === void 0 || channel === "" ? TRAE_SOLO_FUNCTION : channel;
3673
+ }
3674
+ /**
3675
+ * Does this row belong to the view?
3676
+ *
3677
+ * Judged on {@link traeModelCallChannel}, never on `wireFunction` directly:
3678
+ * otherwise the one row without a function (`doubao_1_8`, measured 2026-10-23)
3679
+ * would fall OUT of the common view while being called through exactly the
3680
+ * common channel it belongs to.
3681
+ */
3682
+ function traeModelInView(model, view, region = "cn") {
3683
+ if (view === "all") return true;
3684
+ return (TRAE_COMMON_FUNCTIONS[region] ?? TRAE_COMMON_FUNCTIONS.cn).includes(traeModelCallChannel(model));
3685
+ }
3686
+ /** Keep the rows a view asks for, preserving order. */
3687
+ function scopeTraeModels(rows, view, region = "cn") {
3688
+ if (view === "all") return [...rows];
3689
+ return rows.filter((row) => traeModelInView(row, view, region));
3690
+ }
3691
+ //#endregion
3516
3692
  //#region src/shim.ts
3517
3693
  const BODY_LIMIT = 67108864;
3518
3694
  const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
@@ -3772,6 +3948,48 @@ function createTraeShim(options) {
3772
3948
  };
3773
3949
  }
3774
3950
  //#endregion
3951
+ //#region src/model-detail.ts
3952
+ const TRAE_MODEL_DETAIL_PATH = "/api/ide/v1/batch_get_detail_param";
3953
+ const TRAE_MODEL_DETAIL_FUNCTIONS = [
3954
+ "ui_builder_v2",
3955
+ "solo_coder",
3956
+ "chat_v3",
3957
+ "solo_builder",
3958
+ "builder_v3",
3959
+ "builder",
3960
+ "chat",
3961
+ "inline_chat",
3962
+ "git_ai",
3963
+ "custom_agent_generation",
3964
+ "utils",
3965
+ "code_reviewer",
3966
+ "code_review_summary",
3967
+ "solo_agent",
3968
+ "solo_agent_remote",
3969
+ "solo_work_remote",
3970
+ "solo_agent_lite",
3971
+ "solo_work_lite",
3972
+ "solo_design_lite",
3973
+ "solo_design_remote",
3974
+ "multimodal",
3975
+ "system_diagnosis"
3976
+ ];
3977
+ /** Build the exact request shape observed in successful current Trae CN logs. */
3978
+ function buildTraeModelDetailRequest(configName = "", custom = false) {
3979
+ return {
3980
+ functions: [...TRAE_MODEL_DETAIL_FUNCTIONS],
3981
+ agent_type: configName === "" ? "" : "solo_agent",
3982
+ current_config_info: {
3983
+ config_name: configName,
3984
+ is_custom_model: custom
3985
+ },
3986
+ mode_type: 0,
3987
+ access_type: 0,
3988
+ ab_force_vids: "",
3989
+ ab_autotest_advanced_mode: 0
3990
+ };
3991
+ }
3992
+ //#endregion
3775
3993
  //#region src/protocol.ts
3776
3994
  const TRAE_CN_AGENT_TASK_PATH = "/api/agent/v3/create_agent_task";
3777
3995
  const TRAE_CN_TITLE_PATH = "/api/agent/v3/llm_utils_chat";
@@ -3875,6 +4093,231 @@ function traeEndpoint(baseUrl, path) {
3875
4093
  return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
3876
4094
  }
3877
4095
  //#endregion
4096
+ //#region src/official-roster.ts
4097
+ const CHAT_USAGE = "chat_completion";
4098
+ /**
4099
+ * Parse a `batch_get_detail_param` answer into a per-(config, function) index.
4100
+ *
4101
+ * Tolerant on purpose: this document is 1.8 MB of upstream-owned shape
4102
+ * (`function_configs[] → config_info_list[] → model_detail_list[]`), and the
4103
+ * plugin must not lose the whole refresh because one nested field changed
4104
+ * spelling. Rows that cannot be read are skipped; a document with no
4105
+ * `function_configs` at all yields an empty roster, which the filter reads as
4106
+ * "nothing to say" rather than "hide everything".
4107
+ *
4108
+ * Duplicate pairs are collapsed first-wins, matching the rest of the catalog's
4109
+ * convention (`mergeTraeModelSources`). Two functions can both list a config,
4110
+ * and those are DIFFERENT pairs, so both are kept.
4111
+ */
4112
+ function parseTraeOfficialRoster(payload) {
4113
+ const channels = /* @__PURE__ */ new Map();
4114
+ const seen = /* @__PURE__ */ new Set();
4115
+ let pairs = 0;
4116
+ const document = typeof payload === "object" && payload !== null ? payload : {};
4117
+ const groups = Array.isArray(document["function_configs"]) ? document["function_configs"] : [];
4118
+ for (const group of groups) {
4119
+ if (typeof group !== "object" || group === null) continue;
4120
+ const record = group;
4121
+ const functionName = typeof record["function"] === "string" ? record["function"] : "";
4122
+ if (functionName === "") continue;
4123
+ const list = Array.isArray(record["config_info_list"]) ? record["config_info_list"] : [];
4124
+ for (const item of list) {
4125
+ if (typeof item !== "object" || item === null) continue;
4126
+ const config = item;
4127
+ const configName = typeof config["config_name"] === "string" ? config["config_name"].trim() : "";
4128
+ if (configName === "") continue;
4129
+ const key = `${functionName}\u0000${configName.toLowerCase()}`;
4130
+ if (seen.has(key)) continue;
4131
+ seen.add(key);
4132
+ const channel = {
4133
+ function: functionName,
4134
+ usage: typeof config["usage"] === "string" ? config["usage"] : "",
4135
+ enabled: config["config_switch"] !== false,
4136
+ invisible: config["is_invisible_to_user"] === true
4137
+ };
4138
+ pairs += 1;
4139
+ const bucket = channels.get(configName.toLowerCase());
4140
+ if (bucket === void 0) channels.set(configName.toLowerCase(), [channel]);
4141
+ else bucket.push(channel);
4142
+ }
4143
+ }
4144
+ return {
4145
+ channels,
4146
+ size: {
4147
+ configs: channels.size,
4148
+ pairs
4149
+ }
4150
+ };
4151
+ }
4152
+ /** The `config_name` a catalog row would actually send (see `wireConfigName`). */
4153
+ function officialKeyOf(model) {
4154
+ return (model.wireConfigName ?? model.id).trim().toLowerCase();
4155
+ }
4156
+ /**
4157
+ * Decide one row from the roster.
4158
+ *
4159
+ * `chatFunctions` is the region's own `TRAE_DIRECTORY_FUNCTIONS` list, in
4160
+ * priority order: only those functions can carry a VERDICT, because only those
4161
+ * are functions `llm_utils_chat` accepts. A config the vendor shows under
4162
+ * `code_review_summary` alone is not offered as a chat model at all — that is
4163
+ * exactly the `glm-4.7` case, and it is a "hide", not a "re-pin".
4164
+ *
4165
+ * The verdict is `keep` when the roster cannot speak: no entry for the config
4166
+ * (measured 2026-10-23: `kimi-k2.8-preview` and `qwen3.8-flash` exist only in
4167
+ * the Remote directory's `chat_v3` group), only tool rows (`usage: 'summary'`,
4168
+ * `fast_apply`, …), or no chat function at all. Silence is never a verdict.
4169
+ */
4170
+ function officialVerdictOf(model, roster, chatFunctions) {
4171
+ const entries = roster.channels.get(officialKeyOf(model));
4172
+ if (entries === void 0 || entries.length === 0) return { kind: "keep" };
4173
+ const callable = entries.filter((entry) => entry.usage === "" || entry.usage === CHAT_USAGE).filter((entry) => chatFunctions.includes(entry.function));
4174
+ if (callable.length === 0) return { kind: "keep" };
4175
+ const listedBy = callable.map((entry) => entry.function);
4176
+ const shown = callable.filter((entry) => !entry.invisible);
4177
+ if (shown.length === 0) return {
4178
+ kind: "hide",
4179
+ function: model.wireFunction,
4180
+ listedBy
4181
+ };
4182
+ const own = model.wireFunction === void 0 ? void 0 : callable.find((entry) => entry.function === model.wireFunction);
4183
+ if (own !== void 0 && !own.invisible) return { kind: "keep" };
4184
+ const to = chatFunctions.find((fn) => shown.some((entry) => entry.function === fn));
4185
+ if (to === void 0) return { kind: "keep" };
4186
+ return {
4187
+ kind: "repin",
4188
+ from: model.wireFunction,
4189
+ to,
4190
+ listedBy
4191
+ };
4192
+ }
4193
+ /**
4194
+ * Apply the roster: retire what no chat function offers any more, and re-pin
4195
+ * what the vendor offers under a different channel.
4196
+ *
4197
+ * FAIL-OPEN BY CONSTRUCTION, which is the whole safety argument for applying
4198
+ * this by default:
4199
+ *
4200
+ * - `undefined` roster (transport failure, restricted network, a credential
4201
+ * that cannot read the IDE document) keeps EVERY row. A refresh must never
4202
+ * lose models because a second, optional endpoint was unreachable.
4203
+ * - `keep` covers every ambiguity — no entry, tool-only entries, a channel the
4204
+ * roster does not name. Only an explicit "the vendor shows this nowhere we
4205
+ * can call it" removes a row.
4206
+ * - `config_switch === false` alone does NOT remove a row: the flag is
4207
+ * recorded on {@link TraeOfficialChannel} but not used as a verdict, because
4208
+ * its semantics are unmeasured and "disabled" there may describe a function
4209
+ * override rather than the model itself.
4210
+ *
4211
+ * This is deliberately NOT "match the Trae client": the client renders one
4212
+ * function, the plugin serves the union, and issue #46's reporter wants the
4213
+ * dated snapshots the union carries. What changes here is only the class where
4214
+ * the union is provably wrong — twelve retired configs that made up the reported
4215
+ * "tail of old models", plus rows pinned to a channel their own vendor hides
4216
+ * them on (the "code model on a solo channel" risk of issue #52).
4217
+ */
4218
+ function applyOfficialRoster(rows, roster, chatFunctions) {
4219
+ if (roster === void 0) return {
4220
+ rows: [...rows],
4221
+ hidden: [],
4222
+ repinned: []
4223
+ };
4224
+ const kept = [];
4225
+ const hidden = [];
4226
+ const repinned = [];
4227
+ for (const row of rows) {
4228
+ const verdict = officialVerdictOf(row, roster, chatFunctions);
4229
+ if (verdict.kind === "hide") {
4230
+ hidden.push({
4231
+ id: row.id,
4232
+ function: verdict.function
4233
+ });
4234
+ continue;
4235
+ }
4236
+ if (verdict.kind === "repin") {
4237
+ repinned.push({
4238
+ id: row.id,
4239
+ from: verdict.from,
4240
+ to: verdict.to
4241
+ });
4242
+ kept.push({
4243
+ ...row,
4244
+ wireFunction: verdict.to
4245
+ });
4246
+ continue;
4247
+ }
4248
+ kept.push(row);
4249
+ }
4250
+ return {
4251
+ rows: kept,
4252
+ hidden,
4253
+ repinned
4254
+ };
4255
+ }
4256
+ /** How long one successful roster read stays usable, in milliseconds. */
4257
+ const ROSTER_CACHE_MS = 6e5;
4258
+ /** Timeout for the roster read. It is an OPTIONAL signal; it must not stall a refresh. */
4259
+ const ROSTER_TIMEOUT_MS = 2e4;
4260
+ /**
4261
+ * Reads the IDE's roster once per cache window and never throws.
4262
+ *
4263
+ * `undefined` is a first-class answer: it is what a caller sees when the
4264
+ * document could not be read, and {@link applyOfficialRoster} treats it as "no
4265
+ * opinion". Silence on failure is intentional — this is a refinement of a
4266
+ * refresh that already succeeded, so a strict error would turn a usable catalog
4267
+ * into a failed refresh.
4268
+ */
4269
+ var TraeOfficialRosterClient = class {
4270
+ options;
4271
+ fetchImpl;
4272
+ now;
4273
+ cached;
4274
+ /** The last failure, so a repeatedly-broken endpoint is not re-probed on every refresh. */
4275
+ failedAt;
4276
+ constructor(options) {
4277
+ this.options = options;
4278
+ this.fetchImpl = options.fetchImpl ?? fetch;
4279
+ this.now = options.now ?? Date.now;
4280
+ }
4281
+ /** Cached roster, without any network I/O. */
4282
+ cachedRoster() {
4283
+ return this.cached === void 0 ? void 0 : this.cached.roster;
4284
+ }
4285
+ async fetch(signal) {
4286
+ const now = this.now();
4287
+ if (this.cached !== void 0 && now - this.cached.at < ROSTER_CACHE_MS) return this.cached.roster;
4288
+ if (this.failedAt !== void 0 && now - this.failedAt < ROSTER_CACHE_MS) return void 0;
4289
+ try {
4290
+ const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
4291
+ const region = regionOfCredential(credential);
4292
+ const base = this.options.baseUrl ?? (this.options.route === "enterprise" ? "https://console.enterprise.trae.cn" : REGION_GATEWAYS[region].chat);
4293
+ const response = await this.fetchImpl(traeEndpoint(base, TRAE_MODEL_DETAIL_PATH), {
4294
+ method: "POST",
4295
+ headers: buildTraeCnHeaders(credential, identity, { profile: "model-detail" }),
4296
+ body: JSON.stringify({
4297
+ ...buildTraeModelDetailRequest(""),
4298
+ show_custom_model: true
4299
+ }),
4300
+ signal: signal ?? AbortSignal.timeout(ROSTER_TIMEOUT_MS)
4301
+ });
4302
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
4303
+ const roster = parseTraeOfficialRoster(await response.json());
4304
+ if (roster.size.configs === 0) throw new Error("roster document carried no config_name");
4305
+ this.cached = {
4306
+ at: now,
4307
+ roster
4308
+ };
4309
+ this.failedAt = void 0;
4310
+ return roster;
4311
+ } catch (error) {
4312
+ this.failedAt = now;
4313
+ this.options.log?.("dsh-connect-trae: IDE model roster unavailable, keeping every discovered row", { reason: error instanceof Error ? error.message : String(error) });
4314
+ return;
4315
+ }
4316
+ }
4317
+ };
4318
+ /** Where the client asks for the roster; re-exported so diagnostics do not hard-code it. */
4319
+ const TRAE_OFFICIAL_ROSTER_PATH = TRAE_MODEL_DETAIL_PATH;
4320
+ //#endregion
3878
4321
  //#region src/raw-chat.ts
3879
4322
  const TRAE_RAW_CHAT_V2_PATH = "/api/ide/v2/llm_raw_chat";
3880
4323
  const TRAE_RAW_CHAT_V1_PATH = "/api/ide/v1/llm_raw_chat";
@@ -4138,7 +4581,6 @@ function applyReasoningEffort(body, effort, capability) {
4138
4581
  }
4139
4582
  //#endregion
4140
4583
  //#region src/solo.ts
4141
- const TRAE_SOLO_FUNCTION = "solo_work_lite";
4142
4584
  /**
4143
4585
  * `llm_utils_chat` functions to union per region, in priority order.
4144
4586
  *
@@ -4202,22 +4644,12 @@ const TRAE_SOLO_FUNCTION = "solo_work_lite";
4202
4644
  * A model in NO chat function's roster is still left to the merge: it ships
4203
4645
  * with no wire target and its call fails with a readable upstream message
4204
4646
  * rather than being hidden (see mergeTraeModelSources).
4647
+ *
4648
+ * The list itself lives in {@link TRAE_CHANNEL_PRIORITY} (`model-order.ts`) and
4649
+ * is aliased here, not copied: the card groups its rows by these same names in
4650
+ * this same order.
4205
4651
  */
4206
- const TRAE_DIRECTORY_FUNCTIONS = {
4207
- cn: [
4208
- "solo_work_remote",
4209
- TRAE_SOLO_FUNCTION,
4210
- "solo_agent_remote",
4211
- "chat_v3",
4212
- "solo_coder"
4213
- ],
4214
- ai: [
4215
- "solo_work_remote",
4216
- TRAE_SOLO_FUNCTION,
4217
- "solo_agent_remote",
4218
- "chat_v3"
4219
- ]
4220
- };
4652
+ const TRAE_DIRECTORY_FUNCTIONS = TRAE_CHANNEL_PRIORITY;
4221
4653
  const TRAE_SOLO_CHAT_PATH = "/api/agent/v3/llm_utils_chat";
4222
4654
  const TRAE_SOLO_MODELS_PATH = "/api/ide/v1/get_detail_param";
4223
4655
  function classify(status) {
@@ -8898,48 +9330,6 @@ function parseTraeModelExtraConfigLogLine(line) {
8898
9330
  }
8899
9331
  }
8900
9332
  //#endregion
8901
- //#region src/model-detail.ts
8902
- const TRAE_MODEL_DETAIL_PATH = "/api/ide/v1/batch_get_detail_param";
8903
- const TRAE_MODEL_DETAIL_FUNCTIONS = [
8904
- "ui_builder_v2",
8905
- "solo_coder",
8906
- "chat_v3",
8907
- "solo_builder",
8908
- "builder_v3",
8909
- "builder",
8910
- "chat",
8911
- "inline_chat",
8912
- "git_ai",
8913
- "custom_agent_generation",
8914
- "utils",
8915
- "code_reviewer",
8916
- "code_review_summary",
8917
- "solo_agent",
8918
- "solo_agent_remote",
8919
- "solo_work_remote",
8920
- "solo_agent_lite",
8921
- "solo_work_lite",
8922
- "solo_design_lite",
8923
- "solo_design_remote",
8924
- "multimodal",
8925
- "system_diagnosis"
8926
- ];
8927
- /** Build the exact request shape observed in successful current Trae CN logs. */
8928
- function buildTraeModelDetailRequest(configName = "", custom = false) {
8929
- return {
8930
- functions: [...TRAE_MODEL_DETAIL_FUNCTIONS],
8931
- agent_type: configName === "" ? "" : "solo_agent",
8932
- current_config_info: {
8933
- config_name: configName,
8934
- is_custom_model: custom
8935
- },
8936
- mode_type: 0,
8937
- access_type: 0,
8938
- ab_force_vids: "",
8939
- ab_autotest_advanced_mode: 0
8940
- };
8941
- }
8942
- //#endregion
8943
9333
  //#region src/redact.ts
8944
9334
  /**
8945
9335
  * Redaction helpers for diagnostics that a user may paste into a public issue.
@@ -9421,6 +9811,24 @@ function apply(ctx, config) {
9421
9811
  credential: () => store.resolve(),
9422
9812
  log: (message, detail) => ctx.logger.warn(message, detail)
9423
9813
  });
9814
+ /**
9815
+ * The IDE's own roster: which models the vendor's picker shows, per
9816
+ * function (issue #52).
9817
+ *
9818
+ * It is a REFINEMENT of a successful discovery, never a dependency: every
9819
+ * failure path in {@link TraeOfficialRosterClient.fetch} collapses to
9820
+ * `undefined`, and `applyOfficialRoster` then keeps every row. That is why
9821
+ * this can be wired in without a setting — it only acts where every chat
9822
+ * function hides a config (retire it) or where the vendor shows it on a
9823
+ * DIFFERENT chat function than the one we guessed (re-pin). Both are the
9824
+ * upstream's own answers, and both leave anything ambiguous alone.
9825
+ */
9826
+ const officialRoster = new TraeOfficialRosterClient({
9827
+ credential: () => store.resolve(),
9828
+ identity,
9829
+ ...panel === "enterprise" ? { route: "enterprise" } : {},
9830
+ log: (message, detail) => ctx.logger.warn(message, detail)
9831
+ });
9424
9832
  const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
9425
9833
  /**
9426
9834
  * One client + bridge per provider id.
@@ -9572,25 +9980,52 @@ function apply(ctx, config) {
9572
9980
  };
9573
9981
  },
9574
9982
  discoverModels: async (signal) => {
9575
- const merged = mergeTraeModelSources(await withDirectoryRetry(() => remoteCatalog.fetchModels(signal)), await solo.fetchModels(signal));
9983
+ const remote = await withDirectoryRetry(() => remoteCatalog.fetchModels(signal));
9984
+ const [wireModels, roster] = await Promise.all([solo.fetchModels(signal), officialRoster.fetch(signal)]);
9985
+ const merged = mergeTraeModelSources(remote, wireModels);
9576
9986
  const callable = withChatCallableFunctions(merged, TRAE_DIRECTORY_FUNCTIONS[region]);
9577
9987
  const chatFunctions = TRAE_DIRECTORY_FUNCTIONS[region];
9578
9988
  for (const model of merged) {
9579
9989
  if (model.wireFunction === void 0 || chatFunctions.includes(model.wireFunction)) continue;
9580
9990
  ctx.logger.warn(`model directory row is pinned to a non-chat function and will be called through the default chat function instead: ${model.id} (pinned to function ${model.wireFunction})`);
9581
9991
  }
9992
+ /**
9993
+ * Retire what no chat function offers any more, and re-pin what the
9994
+ * vendor offers under a DIFFERENT channel (issue #52).
9995
+ *
9996
+ * Runs AFTER `withChatCallableFunctions` on purpose: that step decides
9997
+ * the `wireFunction` this filter judges the row on. Rows the IDE roster
9998
+ * does not mention, and every row when the roster could not be read at
9999
+ * all, survive untouched — see `applyOfficialRoster`.
10000
+ */
10001
+ const official = applyOfficialRoster(callable, roster, chatFunctions);
10002
+ /**
10003
+ * Fix the ORDER once, here, so it is a property of the catalog rather
10004
+ * than of the screen that happens to draw it (issue #52, P0).
10005
+ *
10006
+ * The merge walks twelve directory groups and `solo-remote.ts` records
10007
+ * that the group order varies between calls, so the same account could
10008
+ * get two different lists from two refreshes. Sorting by (channel
10009
+ * priority, name) makes the saved `lastCatalog` — and therefore the DSH
10010
+ * model picker, which renders in catalog order — identical across
10011
+ * refreshes. The card re-applies the same function for catalogs saved
10012
+ * before this existed.
10013
+ */
10014
+ const visible = orderTraeModels(official.rows, region);
10015
+ if (official.hidden.length > 0) ctx.logger.warn(`hiding ${official.hidden.length} model(s) no chat function offers any more (issue #52)`, official.hidden);
10016
+ if (official.repinned.length > 0) ctx.logger.warn(`re-pinning ${official.repinned.length} model(s) to the function their vendor shows them on (issue #52)`, official.repinned);
9582
10017
  wire.callableKeys.clear();
9583
- for (const model of callable) {
10018
+ for (const model of visible) {
9584
10019
  wire.callableKeys.add(model.id.trim().toLowerCase());
9585
10020
  wire.callableKeys.add(model.name.trim().toLowerCase());
9586
10021
  }
9587
- wire.resolved = callable.length > 0;
9588
- const wireTargets = wireTargetsOf(callable);
10022
+ wire.resolved = visible.length > 0;
10023
+ const wireTargets = wireTargetsOf(visible);
9589
10024
  wire.byId = wireTargets.byId;
9590
10025
  wire.byName = wireTargets.byName;
9591
10026
  latestDiscovery.length = 0;
9592
- latestDiscovery.push(...callable);
9593
- return callable;
10027
+ latestDiscovery.push(...visible);
10028
+ return visible;
9594
10029
  },
9595
10030
  /**
9596
10031
  * One-click availability test. The ids come from the card, so each one is
@@ -10294,4 +10729,4 @@ function apply(ctx, config) {
10294
10729
  });
10295
10730
  }
10296
10731
  //#endregion
10297
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, 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, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, isPromotableWireModel, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeToolSchema, normalizeTraeToolSchemas, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, wireTargetsOf, withContextAdaptation, withTraeRegion };
10732
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHANNEL_PRIORITY, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_COMMON_FUNCTIONS, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_OFFICIAL_ROSTER_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, 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, TraeOfficialRosterClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyOfficialRoster, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, filterTraeModels, generationOf, groupTraeModels, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, isPromotableWireModel, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeToolSchema, normalizeTraeToolSchemas, normalizeTraeVersionCode, officialVerdictOf, orderTraeModels, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeOfficialRoster, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, scopeTraeModels, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelCallChannel, traeModelDisplayName, traeModelInView, traeModelMatchesQuery, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, wireTargetsOf, withChatCallableFunctions, withContextAdaptation, withTraeRegion };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-trae",
3
3
  "displayName": "DSH Connect Trae",
4
4
  "description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
5
- "version": "4.4.4",
5
+ "version": "4.4.5",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",