dsh-connect-trae 4.1.0 → 4.3.0

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.d.ts CHANGED
@@ -3,6 +3,46 @@ import { Context, ProviderStreams } from "@earendil-works/pi-ai";
3
3
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
4
  import { Context as Context$1 } from "@deepseek-ai/cordis";
5
5
  import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
6
+ //#region src/promo.d.ts
7
+ /** One price pair, as upstream published it. Rates are credits per Mtoken. */
8
+ interface TraePromoPrice {
9
+ /** Undiscounted rate. */
10
+ beforeRate?: number;
11
+ /** Discounted rate, when upstream stated one for this branch. */
12
+ rate?: number;
13
+ /**
14
+ * Pay ratio in percent, straight from `discount`: 100 = full price, 50 = 五折,
15
+ * 25 = 二五折. NOT a 0–1 multiplier — read literally, the way it is written.
16
+ */
17
+ discountPercent?: number;
18
+ }
19
+ /** One `off_peak` window: ISO weekdays 1(Monday)–7(Sunday), minutes 0–1440. */
20
+ interface TraePromoWindow {
21
+ weekdays: number[];
22
+ /** Inclusive. */
23
+ startMinute: number;
24
+ /** Exclusive; 1440 is upstream's way of writing "end of day". */
25
+ endMinute: number;
26
+ }
27
+ /** The whole `activity_discount` document, kept unflattened on purpose. */
28
+ interface TraeActivityDiscount {
29
+ /** Upstream's machine name for the campaign, e.g. `subsidy_member_discount`. */
30
+ subKey?: string;
31
+ /** Upstream's own "right now" verdict. */
32
+ current?: TraePromoPrice & {
33
+ discountType?: string;
34
+ };
35
+ /** Subscription price — hover material only, see the module note. */
36
+ member?: TraePromoPrice;
37
+ /** Recurring cheap hours, judged locally against each model's own zone. */
38
+ offPeak?: TraePromoPrice & {
39
+ timeZone?: string;
40
+ windows: TraePromoWindow[];
41
+ };
42
+ /** Subsidy branch, when upstream names it separately from `current`. */
43
+ subsidy?: TraePromoPrice;
44
+ }
45
+ //#endregion
6
46
  //#region src/reasoning.d.ts
7
47
  declare const TRAE_REASONING_EFFORTS: readonly ["minimal", "low", "medium", "high", "xhigh"];
8
48
  type TraeReasoningEffort = typeof TRAE_REASONING_EFFORTS[number];
@@ -43,6 +83,11 @@ interface TraeDiscoveredModel {
43
83
  * `config_info_list` for the chat functions).
44
84
  */
45
85
  function?: string;
86
+ /**
87
+ * Trae's discount rule for this row (`features.activity_discount`), kept whole
88
+ * so the card can judge it against the clock at paint time (src/promo.ts).
89
+ */
90
+ activityDiscount?: TraeActivityDiscount;
46
91
  }
47
92
  /**
48
93
  * Parse only capabilities explicitly advertised by Trae's remote model API.
@@ -181,6 +226,16 @@ interface TraeModelInfo {
181
226
  * image to a model that cannot read one burns credits for a silent failure.
182
227
  */
183
228
  multimodal?: boolean;
229
+ /**
230
+ * Trae's discount RULE for this model, exactly as published (see src/promo.ts).
231
+ *
232
+ * Kept unflattened and unjudged: whether a badge is painted depends on the
233
+ * clock, so the card judges it at paint time rather than trusting a verdict
234
+ * frozen into a saved catalog. Absent means the model carries no promotion
235
+ * document — including every `ai` (international) row, whose subscription
236
+ * models publish no `consumption_rate` at all.
237
+ */
238
+ activityDiscount?: TraeActivityDiscount;
184
239
  }
185
240
  /**
186
241
  * Bootstrap catalog: identity only where current Trae metadata has not been
@@ -267,6 +322,15 @@ interface TraeWireModel {
267
322
  * therefore has to replay the directory's own function.
268
323
  */
269
324
  function?: string;
325
+ /**
326
+ * The row's own `activity_discount`, read from `display_contact_config`.
327
+ *
328
+ * The wire roster is the richer of the two directories (8 rows carrying a
329
+ * discount vs the Remote directory's 4), and the same model can name a
330
+ * different campaign on each side, so this value wins over the Remote one
331
+ * during the merge — the precedence `creditMultiplier` already follows.
332
+ */
333
+ activityDiscount?: TraeActivityDiscount;
270
334
  }
271
335
  /**
272
336
  * Merge the two Trae model sources into one authoritative catalog.
@@ -748,6 +812,13 @@ interface TraeWebModel {
748
812
  probe?: TraeModelProbeVerdict;
749
813
  /** Upstream's own words for the verdict, when it gave any. */
750
814
  probeDetail?: string;
815
+ /**
816
+ * Trae's discount rule for this row, straight from the directory that
817
+ * advertised it (see src/promo.ts). The CARD decides whether it is live right
818
+ * now: the rule carries weekday/time windows in an upstream-declared zone, so
819
+ * a verdict baked in here would be stale by the next morning.
820
+ */
821
+ activityDiscount?: TraeActivityDiscount;
751
822
  }
752
823
  /**
753
824
  * One model's availability verdict from the one-click test.
@@ -2717,6 +2788,11 @@ interface TraeSoloModel {
2717
2788
  reasoning?: TraeReasoningCapability;
2718
2789
  /** Effective (post-discount) credit multiplier, as the Trae IDE shows it. */
2719
2790
  creditMultiplier?: number;
2791
+ /**
2792
+ * The row's `activity_discount` rule, read from `display_contact_config`.
2793
+ * Display data only: src/promo.ts decides at paint time whether a badge shows.
2794
+ */
2795
+ activityDiscount?: TraeActivityDiscount;
2720
2796
  /** The directory function that listed this config (replayed when calling it). */
2721
2797
  function?: string;
2722
2798
  }
package/lib/index.js CHANGED
@@ -236,12 +236,14 @@ function mergeTraeModelSources(remote, wire) {
236
236
  const wireModel = wireById.get(displayKey(model.id)) ?? wireByName.get(displayKey(model.name));
237
237
  const creditMultiplier = wireModel?.creditMultiplier ?? model.creditMultiplier;
238
238
  const wireFunction = wireModel?.function ?? model.function;
239
+ const activityDiscount = wireModel?.activityDiscount ?? model.activityDiscount;
239
240
  result.push({
240
241
  id: model.id,
241
242
  name: model.name,
242
243
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
243
244
  ...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
244
245
  ...creditMultiplier === void 0 ? {} : { creditMultiplier },
246
+ ...activityDiscount === void 0 ? {} : { activityDiscount },
245
247
  input: ["text"],
246
248
  ...wireModel?.maxTokens === void 0 ? {} : { maxTokens: wireModel.maxTokens },
247
249
  ...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
@@ -310,6 +312,7 @@ function discoveredCatalog(models) {
310
312
  ...model.maxContextWindow === void 0 ? {} : { maxContextWindow: model.maxContextWindow },
311
313
  input: ["text"],
312
314
  ...model.creditMultiplier === void 0 ? {} : { creditMultiplier: model.creditMultiplier },
315
+ ...model.activityDiscount === void 0 ? {} : { activityDiscount: model.activityDiscount },
313
316
  ...model.multimodal === void 0 ? {} : { multimodal: model.multimodal },
314
317
  ...model.function === void 0 ? {} : { wireFunction: model.function },
315
318
  reasoningSupported: model.reasoningSupported,
@@ -3738,6 +3741,97 @@ function traeEndpoint(baseUrl, path) {
3738
3741
  return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
3739
3742
  }
3740
3743
  //#endregion
3744
+ //#region src/promo.ts
3745
+ function record$1(value) {
3746
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
3747
+ }
3748
+ function finite(value) {
3749
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3750
+ }
3751
+ function percent(value) {
3752
+ const parsed = finite(value);
3753
+ return parsed !== void 0 && parsed > 0 && parsed < 100 ? parsed : void 0;
3754
+ }
3755
+ function ratePair(raw) {
3756
+ const entry = record$1(raw);
3757
+ if (entry === void 0) return {};
3758
+ const beforeRate = finite(entry["before_consumption_rate"]);
3759
+ const rate = finite(entry["consumption_rate"]) ?? finite(entry["after_consumption_rate"]);
3760
+ const discountPercent = percent(entry["discount"]);
3761
+ return {
3762
+ ...beforeRate === void 0 || beforeRate <= 0 ? {} : { beforeRate },
3763
+ ...rate === void 0 || rate <= 0 ? {} : { rate },
3764
+ ...discountPercent === void 0 ? {} : { discountPercent }
3765
+ };
3766
+ }
3767
+ function windows(raw) {
3768
+ const list = Array.isArray(raw) ? raw : [];
3769
+ const parsed = [];
3770
+ for (const item of list) {
3771
+ const entry = record$1(item);
3772
+ if (entry === void 0) continue;
3773
+ const weekdays = Array.isArray(entry["weekdays"]) ? entry["weekdays"].filter((day) => Number.isInteger(day) && day >= 1 && day <= 7) : [];
3774
+ const startMinute = finite(entry["start_minute"]);
3775
+ const endMinute = finite(entry["end_minute"]);
3776
+ if (weekdays.length === 0 || startMinute === void 0 || endMinute === void 0) continue;
3777
+ if (startMinute < 0 || startMinute > 1439 || endMinute < 1 || endMinute > 1440) continue;
3778
+ if (startMinute >= endMinute) continue;
3779
+ parsed.push({
3780
+ weekdays,
3781
+ startMinute,
3782
+ endMinute
3783
+ });
3784
+ }
3785
+ return parsed;
3786
+ }
3787
+ /**
3788
+ * Parse one `activity_discount` document.
3789
+ *
3790
+ * `value` may be the already-parsed object (both call sites hand us the parent
3791
+ * object) or the raw JSON string; anything without `enable: true` reads as "no
3792
+ * promotion" rather than as a rule we might later misjudge.
3793
+ */
3794
+ function parseTraeActivityDiscount(value) {
3795
+ let source = value;
3796
+ if (typeof source === "string") {
3797
+ if (source === "") return void 0;
3798
+ try {
3799
+ source = JSON.parse(source);
3800
+ } catch {
3801
+ return;
3802
+ }
3803
+ }
3804
+ const raw = record$1(source);
3805
+ if (raw === void 0 || raw["enable"] !== true) return void 0;
3806
+ const data = record$1(raw["data"]);
3807
+ if (data === void 0) return void 0;
3808
+ const subKey = typeof raw["subKey"] === "string" && raw["subKey"] !== "" ? raw["subKey"] : void 0;
3809
+ const currentRaw = record$1(data["current"]);
3810
+ const currentType = typeof currentRaw?.["discount_type"] === "string" ? currentRaw["discount_type"] : void 0;
3811
+ const current = currentRaw === void 0 ? void 0 : {
3812
+ ...ratePair(currentRaw),
3813
+ ...currentType === void 0 ? {} : { discountType: currentType }
3814
+ };
3815
+ const memberRaw = data["member"];
3816
+ const member = record$1(memberRaw) === void 0 ? void 0 : ratePair(memberRaw);
3817
+ const subsidyRaw = data["subsidy"];
3818
+ const subsidy = record$1(subsidyRaw) === void 0 ? void 0 : ratePair(subsidyRaw);
3819
+ const offPeakRaw = record$1(data["off_peak"]);
3820
+ const offPeak = offPeakRaw === void 0 ? void 0 : {
3821
+ ...ratePair(offPeakRaw),
3822
+ ...typeof offPeakRaw["time_zone"] === "string" && offPeakRaw["time_zone"] !== "" ? { timeZone: offPeakRaw["time_zone"] } : {},
3823
+ windows: windows(offPeakRaw["time_windows"])
3824
+ };
3825
+ const parsed = {
3826
+ ...subKey === void 0 ? {} : { subKey },
3827
+ ...current === void 0 ? {} : { current },
3828
+ ...member === void 0 ? {} : { member },
3829
+ ...offPeak === void 0 ? {} : { offPeak },
3830
+ ...subsidy === void 0 ? {} : { subsidy }
3831
+ };
3832
+ return parsed.current === void 0 && parsed.member === void 0 && parsed.offPeak === void 0 && parsed.subsidy === void 0 ? void 0 : parsed;
3833
+ }
3834
+ //#endregion
3741
3835
  //#region src/reasoning.ts
3742
3836
  const TRAE_REASONING_EFFORTS = [
3743
3837
  "minimal",
@@ -3905,6 +3999,29 @@ function prepareSoloBody(source, defaultModel = "glm-5.2", functionName) {
3905
3999
  return JSON.stringify(body);
3906
4000
  }
3907
4001
  /**
4002
+ * Parse this row's `display_contact_config` document.
4003
+ *
4004
+ * It is a *string* holding a second JSON document, and three different readers
4005
+ * (the credit multiplier, the promotion rule, and whatever upstream adds next)
4006
+ * need it — so it is decoded once here rather than three times.
4007
+ */
4008
+ function wireContactConfig(config) {
4009
+ const raw = config["display_contact_config"];
4010
+ if (typeof raw !== "string" || raw === "") return void 0;
4011
+ let parsed;
4012
+ try {
4013
+ parsed = JSON.parse(raw);
4014
+ } catch {
4015
+ return;
4016
+ }
4017
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
4018
+ return parsed;
4019
+ }
4020
+ /** The promotion rule from `display_contact_config`, when the row carries one. */
4021
+ function wireActivityDiscount(config) {
4022
+ return parseTraeActivityDiscount(wireContactConfig(config)?.["activity_discount"]);
4023
+ }
4024
+ /**
3908
4025
  * Read the effective credit multiplier from a `get_detail_param` row.
3909
4026
  *
3910
4027
  * The rate the Trae IDE renders lives in `display_contact_config` — a *string*
@@ -3920,15 +4037,8 @@ function prepareSoloBody(source, defaultModel = "glm-5.2", functionName) {
3920
4037
  * field keep their bare name — parsed-but-absent, never fabricated.
3921
4038
  */
3922
4039
  function wireCreditMultiplier(config) {
3923
- const raw = config["display_contact_config"];
3924
- if (typeof raw !== "string" || raw === "") return void 0;
3925
- let parsed;
3926
- try {
3927
- parsed = JSON.parse(raw);
3928
- } catch {
3929
- return;
3930
- }
3931
- if (typeof parsed !== "object" || parsed === null) return void 0;
4040
+ const parsed = wireContactConfig(config);
4041
+ if (parsed === void 0) return void 0;
3932
4042
  const consumption = parsed["consumption_rate"];
3933
4043
  if (typeof consumption !== "object" || consumption === null) return void 0;
3934
4044
  const entry = consumption;
@@ -4036,6 +4146,7 @@ var TraeSoloUpstreamClient = class {
4036
4146
  ...detail
4037
4147
  });
4038
4148
  const creditMultiplier = wireCreditMultiplier(config);
4149
+ const activityDiscount = wireActivityDiscount(config);
4039
4150
  if (byId.has(id)) continue;
4040
4151
  byId.set(id, {
4041
4152
  id,
@@ -4044,6 +4155,7 @@ var TraeSoloUpstreamClient = class {
4044
4155
  ...maxTokens === void 0 ? {} : { maxTokens },
4045
4156
  ...reasoning === void 0 ? {} : { reasoning },
4046
4157
  ...creditMultiplier === void 0 ? {} : { creditMultiplier },
4158
+ ...activityDiscount === void 0 ? {} : { activityDiscount },
4047
4159
  function: directoryFunction
4048
4160
  });
4049
4161
  }
@@ -4858,6 +4970,7 @@ function parseTraeRemoteModel(value, groupFunction) {
4858
4970
  const consumptionData = record(consumption?.["data"]);
4859
4971
  const creditMultiplier = consumption?.["enable"] === true ? finitePositive(consumptionData?.["rate"]) : void 0;
4860
4972
  const reasoningSupported = record(features?.["reasoning"])?.["enable"] === true;
4973
+ const activityDiscount = parseTraeActivityDiscount(features?.["activity_discount"]);
4861
4974
  const reasoningConfig = record(raw.reasoning_effort_config);
4862
4975
  const supported = (Array.isArray(reasoningConfig?.["options"]) ? reasoningConfig["options"] : []).flatMap((option) => {
4863
4976
  if (typeof option !== "string") return [];
@@ -4874,6 +4987,7 @@ function parseTraeRemoteModel(value, groupFunction) {
4874
4987
  ...dev === void 0 ? {} : { contextWindow: dev },
4875
4988
  ...max === void 0 ? {} : { maxContextWindow: max },
4876
4989
  ...creditMultiplier === void 0 ? {} : { creditMultiplier },
4990
+ ...activityDiscount === void 0 ? {} : { activityDiscount },
4877
4991
  reasoningSupported,
4878
4992
  ...supported.length === 0 ? {} : { reasoning: {
4879
4993
  supported,
@@ -4963,8 +5077,7 @@ function traeSoloRemoteFallbackBases(region, officialBase) {
4963
5077
  return backupBase === officialBase ? [] : [backupBase];
4964
5078
  }
4965
5079
  /**
4966
- * Of two rows for the SAME model id, the one that advertises the stronger
4967
- * capacity (issue #23).
5080
+ * Of two rows for the SAME model id, the one the catalog should keep.
4968
5081
  *
4969
5082
  * The gateway groups its directory by function and reports a model's capacity
4970
5083
  * per group, with the group order varying between calls. Keeping the strongest
@@ -4972,6 +5085,9 @@ function traeSoloRemoteFallbackBases(region, officialBase) {
4972
5085
  * LEXICOGRAPHIC and must be applied all the way down, or ordering leaks back in
4973
5086
  * at the next field:
4974
5087
  *
5088
+ * 0. the kept row must also carry the promotional rule, which the groups
5089
+ * disagree about — the rule is copied onto the capacity winner, and of two
5090
+ * rules the one that can say more wins (see the comment in the body);
4975
5091
  * 1. a row with a Max tier beats one without (`maxContextWindow` is only set
4976
5092
  * when the group reported `max_mode: true` with a positive max);
4977
5093
  * 2. two Max rows: the larger max wins;
@@ -4989,6 +5105,52 @@ function traeSoloRemoteFallbackBases(region, officialBase) {
4989
5105
  */
4990
5106
  function preferStrongerRow(candidate, incumbent) {
4991
5107
  if (incumbent === void 0) return candidate;
5108
+ const winner = strongerCapacityRow(candidate, incumbent);
5109
+ const rule = richerRule(candidate.activityDiscount, incumbent.activityDiscount);
5110
+ if (rule === void 0 || rule === winner.activityDiscount) return winner;
5111
+ return {
5112
+ ...winner,
5113
+ activityDiscount: rule
5114
+ };
5115
+ }
5116
+ /**
5117
+ * Step 0's tie-break: of two rules for the same model id, the one that can say
5118
+ * more about this moment. Every parsed rule already passed `enable: true` (see
5119
+ * parseTraeActivityDiscount), so the ranking is: an upstream "right now"
5120
+ * discount first, then the rule covering more clock. Two rules that rank EQUAL
5121
+ * are compared by their canonical content, so the answer depends on the rule
5122
+ * documents and never on which group the gateway answered first (issue #23).
5123
+ */
5124
+ function richerRule(candidate, incumbent) {
5125
+ if (candidate === void 0) return incumbent;
5126
+ if (incumbent === void 0) return candidate;
5127
+ const score = (rule) => {
5128
+ return (rule.current !== void 0 && (rule.current.discountType ?? "none") !== "none" ? 1 : 0) * 1e3 + (rule.offPeak?.windows.length ?? 0);
5129
+ };
5130
+ const candidateScore = score(candidate);
5131
+ const incumbentScore = score(incumbent);
5132
+ if (candidateScore !== incumbentScore) return candidateScore > incumbentScore ? candidate : incumbent;
5133
+ return canonicalRule(candidate) < canonicalRule(incumbent) ? candidate : incumbent;
5134
+ }
5135
+ /**
5136
+ * Content-only identity for two rules that scored the same.
5137
+ *
5138
+ * The score cannot distinguish, say, two `current: none` rules with two windows
5139
+ * each in different hours — and keeping "whichever arrived first" would let the
5140
+ * gateway's group order decide whether tonight's badge appears. Key order is
5141
+ * normalised (the objects are ours, but a caller may hand us a literal), so
5142
+ * equal documents compare equal and the incumbent is kept unchanged.
5143
+ */
5144
+ function canonicalRule(rule) {
5145
+ const canonical = (value) => {
5146
+ if (Array.isArray(value)) return value.map((item) => canonical(item));
5147
+ if (typeof value !== "object" || value === null) return value;
5148
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, canonical(item)]));
5149
+ };
5150
+ return JSON.stringify(canonical(rule));
5151
+ }
5152
+ /** Steps 1–4 of the documented order: the row advertising the stronger capacity. */
5153
+ function strongerCapacityRow(candidate, incumbent) {
4992
5154
  const candidateMax = candidate.maxContextWindow;
4993
5155
  const incumbentMax = incumbent.maxContextWindow;
4994
5156
  if (candidateMax !== void 0 && incumbentMax === void 0) return candidate;
@@ -8756,7 +8918,8 @@ const modelConfig = z.object({
8756
8918
  creditMultiplier: z.number(),
8757
8919
  wireFunction: z.string(),
8758
8920
  wireConfigName: z.string(),
8759
- multimodal: z.boolean()
8921
+ multimodal: z.boolean(),
8922
+ activityDiscount: z.any()
8760
8923
  });
8761
8924
  /**
8762
8925
  * One panel's saved state. Panels are keyed `cn | enterprise | ai` — see
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.1.0",
5
+ "version": "4.3.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -50,7 +50,9 @@
50
50
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
51
51
  "check": "pnpm run typecheck && pnpm run test && pnpm run build",
52
52
  "verify:release": "node scripts/verify-release.mjs",
53
- "card:shot": "vitest run --config vitest.card.config.ts"
53
+ "card:shot": "vitest run --config vitest.card.config.ts",
54
+ "promo:shot": "vitest run --config vitest.card.config.ts -t \"promo shot\"",
55
+ "panels:shot": "vitest run --config vitest.card.config.ts -t \"panels shot\""
54
56
  },
55
57
  "dsh": {
56
58
  "bundle": {