dsh-connect-trae 4.14.2 → 4.16.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.js CHANGED
@@ -724,6 +724,375 @@ var TraeCatalog = class {
724
724
  }
725
725
  };
726
726
  //#endregion
727
+ //#region src/promo.ts
728
+ /** Zone used when upstream omits `off_peak.time_zone` (CN accounts). */
729
+ const TRAE_PROMO_DEFAULT_TIME_ZONE = "Asia/Shanghai";
730
+ function record$1(value) {
731
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
732
+ }
733
+ function finite(value) {
734
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
735
+ }
736
+ function percent(value) {
737
+ const parsed = finite(value);
738
+ return parsed !== void 0 && parsed > 0 && parsed < 100 ? parsed : void 0;
739
+ }
740
+ function ratePair(raw) {
741
+ const entry = record$1(raw);
742
+ if (entry === void 0) return {};
743
+ const beforeRate = finite(entry["before_consumption_rate"]);
744
+ const rate = finite(entry["consumption_rate"]) ?? finite(entry["after_consumption_rate"]);
745
+ const discountPercent = percent(entry["discount"]);
746
+ return {
747
+ ...beforeRate === void 0 || beforeRate <= 0 ? {} : { beforeRate },
748
+ ...rate === void 0 || rate <= 0 ? {} : { rate },
749
+ ...discountPercent === void 0 ? {} : { discountPercent }
750
+ };
751
+ }
752
+ function windows(raw) {
753
+ const list = Array.isArray(raw) ? raw : [];
754
+ const parsed = [];
755
+ for (const item of list) {
756
+ const entry = record$1(item);
757
+ if (entry === void 0) continue;
758
+ const weekdays = Array.isArray(entry["weekdays"]) ? entry["weekdays"].filter((day) => Number.isInteger(day) && day >= 1 && day <= 7) : [];
759
+ const startMinute = finite(entry["start_minute"]);
760
+ const endMinute = finite(entry["end_minute"]);
761
+ if (weekdays.length === 0 || startMinute === void 0 || endMinute === void 0) continue;
762
+ if (startMinute < 0 || startMinute > 1439 || endMinute < 1 || endMinute > 1440) continue;
763
+ if (startMinute >= endMinute) continue;
764
+ parsed.push({
765
+ weekdays,
766
+ startMinute,
767
+ endMinute
768
+ });
769
+ }
770
+ return parsed;
771
+ }
772
+ /**
773
+ * Parse one `activity_discount` document.
774
+ *
775
+ * `value` may be the already-parsed object (both call sites hand us the parent
776
+ * object) or the raw JSON string; anything without `enable: true` reads as "no
777
+ * promotion" rather than as a rule we might later misjudge.
778
+ */
779
+ function parseTraeActivityDiscount(value) {
780
+ let source = value;
781
+ if (typeof source === "string") {
782
+ if (source === "") return void 0;
783
+ try {
784
+ source = JSON.parse(source);
785
+ } catch {
786
+ return;
787
+ }
788
+ }
789
+ const raw = record$1(source);
790
+ if (raw === void 0 || raw["enable"] !== true) return void 0;
791
+ const data = record$1(raw["data"]);
792
+ if (data === void 0) return void 0;
793
+ const subKey = typeof raw["subKey"] === "string" && raw["subKey"] !== "" ? raw["subKey"] : void 0;
794
+ const currentRaw = record$1(data["current"]);
795
+ const currentType = typeof currentRaw?.["discount_type"] === "string" ? currentRaw["discount_type"] : void 0;
796
+ const current = currentRaw === void 0 ? void 0 : {
797
+ ...ratePair(currentRaw),
798
+ ...currentType === void 0 ? {} : { discountType: currentType }
799
+ };
800
+ const memberRaw = data["member"];
801
+ const member = record$1(memberRaw) === void 0 ? void 0 : ratePair(memberRaw);
802
+ const subsidyRaw = data["subsidy"];
803
+ const subsidy = record$1(subsidyRaw) === void 0 ? void 0 : ratePair(subsidyRaw);
804
+ const offPeakRaw = record$1(data["off_peak"]);
805
+ const offPeak = offPeakRaw === void 0 ? void 0 : {
806
+ ...ratePair(offPeakRaw),
807
+ ...typeof offPeakRaw["time_zone"] === "string" && offPeakRaw["time_zone"] !== "" ? { timeZone: offPeakRaw["time_zone"] } : {},
808
+ windows: windows(offPeakRaw["time_windows"])
809
+ };
810
+ const parsed = {
811
+ ...subKey === void 0 ? {} : { subKey },
812
+ ...current === void 0 ? {} : { current },
813
+ ...member === void 0 ? {} : { member },
814
+ ...offPeak === void 0 ? {} : { offPeak },
815
+ ...subsidy === void 0 ? {} : { subsidy }
816
+ };
817
+ return parsed.current === void 0 && parsed.member === void 0 && parsed.offPeak === void 0 && parsed.subsidy === void 0 ? void 0 : parsed;
818
+ }
819
+ const WEEKDAY_INDEX = {
820
+ Mon: 1,
821
+ Tue: 2,
822
+ Wed: 3,
823
+ Thu: 4,
824
+ Fri: 5,
825
+ Sat: 6,
826
+ Sun: 7
827
+ };
828
+ /** Wall clock in one named zone: ISO weekday (1–7) plus minutes since midnight. */
829
+ function zonedClock(now, timeZone) {
830
+ let parts;
831
+ try {
832
+ parts = new Intl.DateTimeFormat("en-US", {
833
+ timeZone,
834
+ weekday: "short",
835
+ hour: "2-digit",
836
+ minute: "2-digit",
837
+ hourCycle: "h23"
838
+ }).formatToParts(now);
839
+ } catch {
840
+ return;
841
+ }
842
+ let isoWeekday;
843
+ let hour;
844
+ let minute;
845
+ for (const part of parts) if (part.type === "weekday") isoWeekday = WEEKDAY_INDEX[part.value];
846
+ else if (part.type === "hour") hour = Number.parseInt(part.value, 10);
847
+ else if (part.type === "minute") minute = Number.parseInt(part.value, 10);
848
+ if (isoWeekday === void 0 || hour === void 0 || minute === void 0) return void 0;
849
+ if (!Number.isFinite(hour) || !Number.isFinite(minute)) return void 0;
850
+ return {
851
+ isoWeekday,
852
+ minutes: hour % 24 * 60 + minute
853
+ };
854
+ }
855
+ function clockOf(minutes) {
856
+ const hour = Math.floor(minutes / 60);
857
+ const minute = minutes % 60;
858
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
859
+ }
860
+ function runningWindow(discount, now, defaultTimeZone) {
861
+ const offPeak = discount.offPeak;
862
+ if (offPeak === void 0 || offPeak.windows.length === 0) return void 0;
863
+ const declaredZone = offPeak.timeZone ?? defaultTimeZone;
864
+ let usedZone = declaredZone;
865
+ let clock = zonedClock(now, declaredZone);
866
+ if (clock === void 0 && declaredZone !== defaultTimeZone) {
867
+ usedZone = defaultTimeZone;
868
+ clock = zonedClock(now, defaultTimeZone);
869
+ }
870
+ if (clock === void 0) return void 0;
871
+ const match = offPeak.windows.find((window) => window.weekdays.includes(clock.isoWeekday) && clock.minutes >= window.startMinute && clock.minutes < window.endMinute);
872
+ return match === void 0 ? void 0 : {
873
+ ...match,
874
+ timeZone: usedZone
875
+ };
876
+ }
877
+ /** `subKey` is upstream's machine name; it is the only honest source for "限时". */
878
+ function kindOf(discount) {
879
+ const key = discount.subKey ?? "";
880
+ if (key.includes("off_peak")) return "offPeak";
881
+ if (key.includes("subsidy")) return "subsidy";
882
+ if (key.includes("limited")) return "limited";
883
+ const type = discount.current?.discountType ?? "";
884
+ if (type.includes("off")) return "offPeak";
885
+ if (type.includes("limited")) return "limited";
886
+ return "subsidy";
887
+ }
888
+ /** Pay ratio as upstream would phrase it; falls back to the rates it published. */
889
+ function discountOf(price) {
890
+ if (price.discountPercent !== void 0) return price.discountPercent;
891
+ if (price.beforeRate === void 0 || price.rate === void 0) return void 0;
892
+ if (price.beforeRate <= 0 || price.rate >= price.beforeRate) return void 0;
893
+ return Math.round(price.rate / price.beforeRate * 100);
894
+ }
895
+ /**
896
+ * The member price, but only when it beats the rate the badge is already showing.
897
+ *
898
+ * Two rates on the same base are two ways to reach one price: `member` quoting
899
+ * 0.06 beside a live 0.06 is not "another 5 折". Comparing the post-discount
900
+ * numbers is the only honest test — a branch with no rate cannot be shown to be
901
+ * cheaper, so it is not quoted at all.
902
+ */
903
+ function cheaperMemberOf(price, discount) {
904
+ const member = discount.member;
905
+ if (member === void 0 || member.rate === void 0) return void 0;
906
+ if (price.rate === void 0 || member.rate >= price.rate) return void 0;
907
+ const discountPercent = discountOf(member);
908
+ if (discountPercent === void 0) return void 0;
909
+ return {
910
+ rate: member.rate,
911
+ discountPercent
912
+ };
913
+ }
914
+ function badgeOf(kind, price, discount, window) {
915
+ const discountPercent = discountOf(price);
916
+ if (discountPercent === void 0) return void 0;
917
+ const member = cheaperMemberOf(price, discount);
918
+ return {
919
+ kind,
920
+ discountPercent,
921
+ percentOff: 100 - discountPercent,
922
+ ...price.beforeRate === void 0 ? {} : { beforeRate: price.beforeRate },
923
+ ...price.rate === void 0 ? {} : { rate: price.rate },
924
+ ...member === void 0 ? {} : {
925
+ memberDiscountPercent: member.discountPercent,
926
+ memberRate: member.rate
927
+ },
928
+ ...window === void 0 ? {} : {
929
+ fromClock: clockOf(window.startMinute),
930
+ untilClock: clockOf(window.endMinute),
931
+ timeZone: window.timeZone
932
+ }
933
+ };
934
+ }
935
+ /**
936
+ * Decide the one badge (if any) a model shows right now.
937
+ *
938
+ * `now` is injected so the verdict is testable and so the card, which re-renders
939
+ * on its own poll, always judges against the current clock. `defaultTimeZone` is
940
+ * the account's zone, used only when upstream omits `off_peak.time_zone`.
941
+ */
942
+ function selectTraePromoBadge(discount, now, defaultTimeZone = TRAE_PROMO_DEFAULT_TIME_ZONE) {
943
+ if (discount === void 0) return void 0;
944
+ const current = discount.current;
945
+ if (current !== void 0) {
946
+ if ((current.discountType ?? "none") !== "none" && (current.beforeRate === void 0 || current.rate === void 0 || current.rate < current.beforeRate)) {
947
+ const kind = kindOf(discount);
948
+ const badge = badgeOf(kind, current, discount, kind === "offPeak" ? runningWindow(discount, now, defaultTimeZone) : void 0);
949
+ if (badge !== void 0) return badge;
950
+ }
951
+ }
952
+ const window = runningWindow(discount, now, defaultTimeZone);
953
+ if (window !== void 0 && discount.offPeak !== void 0) {
954
+ if ((discount.current?.discountType ?? "none") === "none" || discountOf(discount.offPeak) !== void 0) return badgeOf("offPeak", discount.offPeak, discount, window);
955
+ }
956
+ }
957
+ //#endregion
958
+ //#region src/model-badge.ts
959
+ /**
960
+ * The one selector badge this plugin advertises on its own catalog rows.
961
+ *
962
+ * WHY A BADGE AT ALL, GIVEN THE CAPSULE ALREADY EXISTS. The picker's model label
963
+ * is a `single` slot: exactly one component renders, and it is called for EVERY
964
+ * provider's rows. Whichever plugin holds that cell is the only thing that can
965
+ * draw a sibling's capsule — and it can only draw what the sibling PUBLISHED,
966
+ * because `model.badge` is the only channel between them. So a connector that
967
+ * writes no badge has bare rows the moment another plugin wins the seat, no
968
+ * matter how carefully that plugin reproduces `badge`. See
969
+ * `docs/MODEL-LABEL-SPEC.md` (sibling `dsh-connect-workbuddy` repository), whose
970
+ * rules this file follows.
971
+ *
972
+ * UNAVAILABLE HOSTS. On 0.1.x there is no `badge` field at all; the registry
973
+ * drops the key and every row renders exactly as before. See `src/adapter.ts`
974
+ * for the conditional-spread that keeps this file compiling there.
975
+ *
976
+ * @module dsh-connect-trae/model-badge
977
+ */
978
+ /** Every tone the host understands; the test asserts this list is complete. */
979
+ const TRAE_BADGE_TONES = [
980
+ "neutral",
981
+ "info",
982
+ "success",
983
+ "warning"
984
+ ];
985
+ /**
986
+ * Why each promotion kind paints the way it does in the picker.
987
+ *
988
+ * MIRRORS `PROMO_TONES` IN `src/client/TraeModelLabel.tsx`, which is the chip
989
+ * this plugin draws on its own rows. The two must agree: when this plugin holds
990
+ * the label slot the same row is drawn by that component, and when a SIBLING
991
+ * holds it the row is drawn from this badge instead. One row, one colour.
992
+ *
993
+ * `subsidy` is the only negative-valence kind (`state-error`, red) because it is
994
+ * Trae paying part of the bill; `limited` warns, `offPeak` is the mildest.
995
+ */
996
+ const PROMO_TONES = {
997
+ subsidy: "warning",
998
+ limited: "warning",
999
+ offPeak: "info"
1000
+ };
1001
+ /**
1002
+ * The badge one catalog row should carry, or `undefined` for none.
1003
+ *
1004
+ * WHY THE PROMOTION, AND NOT THE PRICE. A badge is the ONE thing a sibling can
1005
+ * draw on this plugin's behalf, so it has to be the thing a user would act on.
1006
+ * "This model is 50% off right now" changes the choice; a price that has been
1007
+ * identical for months does not.
1008
+ *
1009
+ * WHY THE CLOCK IS READ HERE. `activityDiscount` rides along UNRESOLVED on the
1010
+ * model entry precisely so the judgement happens where the row is drawn
1011
+ * (`src/promo.ts` explains why freezing it at parse time would leave a night-time
1012
+ * discount hanging there the next morning). `listModels()` IS that moment on the
1013
+ * host side — it runs when the host builds the catalog — and the plugin asks the
1014
+ * host to re-read the catalog via `llm/models-updated` whenever the card's own
1015
+ * clock crosses a window boundary. So this is not "catalog data that ignores the
1016
+ * clock"; it is a clock judgement made at each catalog rebuild.
1017
+ *
1018
+ * A NOTE ON WHAT WAS TRIED FIRST AND REJECTED. An earlier version advertised
1019
+ * `creditMultiplier === 0` as "免费"/"Free". It never fired: every path that
1020
+ * parses upstream's `consumption_rate` runs it through `finitePositive`
1021
+ * (`src/solo.ts`, `src/model-metadata.ts`), which requires `> 0`, so a rate of
1022
+ * `0` arrives as `undefined`. A multiplier of exactly `0` can only come from a
1023
+ * saved `lastCatalog`, never from the live directory. Do not reintroduce a
1024
+ * price-derived badge from that field.
1025
+ *
1026
+ * @param model - the catalog row about to be advertised.
1027
+ * @param now - the clock the promotion is judged against. Passed in rather than
1028
+ * read from `Date.now()` so the caller owns the "one clock for the whole
1029
+ * catalog" decision and the tests can move time.
1030
+ * @returns the badge, or `undefined` — never `null`. A `null` makes the host
1031
+ * registry throw inside `listModels`, which drops this provider's
1032
+ * ENTIRE catalog into `failures`, not just the decorated row.
1033
+ */
1034
+ function traeModelBadge(model, now) {
1035
+ const promo = selectTraePromoBadge(model.activityDiscount, now, TRAE_PROMO_DEFAULT_TIME_ZONE);
1036
+ return promo === void 0 ? void 0 : badgeOfPromo(promo);
1037
+ }
1038
+ /**
1039
+ * The picker's wording for one promotion.
1040
+ *
1041
+ * WHY THE UPSTREAM'S OWN WORDING AND NOT INVENTED COPY. The chip the plugin
1042
+ * draws in its own card says "闲时 5 折"; the badge has to be recognisable as the
1043
+ * same offer, so it reuses the same three phrasings with the same `promoZhe`
1044
+ * figure. If a future kind has no wording here, it must NOT fall through to a
1045
+ * generic "优惠" — an unlabelled capsule in the picker is worse than none.
1046
+ */
1047
+ function badgeOfPromo(promo) {
1048
+ const zhe = promoZhe(promo.discountPercent);
1049
+ const percent = String(promo.percentOff);
1050
+ return {
1051
+ text: {
1052
+ en: PROMO_EN[promo.kind].replace("{percent}", percent),
1053
+ zh: PROMO_ZH[promo.kind].replace("{zhe}", zhe)
1054
+ },
1055
+ tone: PROMO_TONES[promo.kind]
1056
+ };
1057
+ }
1058
+ /**
1059
+ * Pay ratio as the "折" figure a Chinese price tag uses: 50 → "5", 25 → "2.5".
1060
+ *
1061
+ * Duplicated from `src/client/TraeModelLabel.tsx` ON PURPOSE: that module is a
1062
+ * client component and importing it from here would pull React into the host
1063
+ * process. `tests/model-badge.spec.ts` asserts the two produce the same string
1064
+ * for every percent the directory can publish, so they cannot drift silently.
1065
+ */
1066
+ function promoZhe(discountPercent) {
1067
+ const zhe = Math.round(discountPercent * 10) / 100;
1068
+ return Number.isInteger(zhe) ? String(zhe) : zhe.toFixed(1);
1069
+ }
1070
+ /**
1071
+ * English wording per kind.
1072
+ *
1073
+ * NOT a translation table of the Chinese strings: English has no "折", so the
1074
+ * discount has to be restated as a percentage. Same number, different grammar.
1075
+ *
1076
+ * THE ENGLISH HERE IS SHORTER THAN THE CARD'S, BY DESIGN. `src/client/locales.ts`
1077
+ * (the chip in this plugin's own settings card) says `Off-peak {percent}% off`;
1078
+ * the badge cannot, because the picker enforces 12 graphemes and that phrasing is
1079
+ * 17 at `percent = 100`. A settings row has room for the longer sentence; a
1080
+ * picker row does not. The Chinese templates, where the two DO match exactly, are
1081
+ * asserted in `tests/model-badge.spec.ts` — the English difference is deliberate
1082
+ * and is not drift to "fix".
1083
+ */
1084
+ const PROMO_EN = {
1085
+ limited: "Limited {percent}%",
1086
+ subsidy: "{percent}% off",
1087
+ offPeak: "Offpeak {percent}%"
1088
+ };
1089
+ /** Chinese wording per kind, matching the chip this plugin draws in its card. */
1090
+ const PROMO_ZH = {
1091
+ limited: "限时 {zhe} 折",
1092
+ subsidy: "{zhe} 折",
1093
+ offPeak: "闲时 {zhe} 折"
1094
+ };
1095
+ //#endregion
727
1096
  //#region src/pi-ai-runtime.ts
728
1097
  /**
729
1098
  * Which `@earendil-works/pi-ai` copy THIS plugin resolves at runtime, and which
@@ -1375,7 +1744,8 @@ function withContextAdaptation(api, generation) {
1375
1744
  };
1376
1745
  }
1377
1746
  /**
1378
- * The adapter, with the level each model runs at reported upward.
1747
+ * The adapter, with the level each model runs at reported upward and the
1748
+ * selector badge this plugin advertises on its own rows.
1379
1749
  *
1380
1750
  * WHY THIS SUBCLASS EXISTS. The kernel asks the adapter for a model's reasoning
1381
1751
  * metadata and materialises `reasoning.defaultEffort` into every request that
@@ -1387,8 +1757,9 @@ function withContextAdaptation(api, generation) {
1387
1757
  * at the seam the kernel actually consults, is what makes the picker name a
1388
1758
  * level instead of `Default`. See `default-effort.ts` for the precedence.
1389
1759
  *
1390
- * The override is deliberately narrow: it rewrites ONE field of an object the
1391
- * parent just built, and only when the model genuinely declares that level.
1760
+ * Both overrides are deliberately narrow: each rewrites ONE field of an object
1761
+ * the parent just built, and only where this plugin genuinely has something to
1762
+ * say that the parent cannot.
1392
1763
  */
1393
1764
  var DefaultEffortAdapter = class extends PiAiAdapter {
1394
1765
  /**
@@ -1398,9 +1769,53 @@ var DefaultEffortAdapter = class extends PiAiAdapter {
1398
1769
  * a settings change reaches the very next request without a restart.
1399
1770
  */
1400
1771
  defaultEffort;
1401
- constructor(config, defaultEffort = () => void 0) {
1772
+ /**
1773
+ * The badge for one advertised row id, judged at a caller-supplied clock;
1774
+ * `undefined` means "say nothing".
1775
+ *
1776
+ * Looked up per row rather than precomputed, for the same reason as
1777
+ * `defaultEffort`: `listModels()` is answered from the LIVE catalog, which a
1778
+ * refresh can replace between two reads.
1779
+ */
1780
+ badgeOf;
1781
+ constructor(config, defaultEffort = () => void 0, badgeOf = () => void 0) {
1402
1782
  super(config);
1403
1783
  this.defaultEffort = defaultEffort;
1784
+ this.badgeOf = badgeOf;
1785
+ }
1786
+ /**
1787
+ * Attach the selector badge to the rows this provider advertises.
1788
+ *
1789
+ * WHY OVERRIDE `listModels` SPECIFICALLY: `badge` travels the CATALOG path
1790
+ * only. `normalizeModelInfo` (exact-route resolution) does not copy it, and no
1791
+ * request or Session event carries it — the host's `LlmRuntime.listModels`
1792
+ * is the one place it is read off an adapter and forwarded. `PiAiAdapter`'s
1793
+ * own implementation never emits one, so a subclass has to add it here.
1794
+ *
1795
+ * WHY THE CONDITIONAL SPREAD, AND NOT `{ ...model, badge }`: hosts before
1796
+ * 0.2.2 have no `badge` on `LlmModelInfo` at all, and a literal `badge` key
1797
+ * would be rejected there as an unknown property — this file has to compile
1798
+ * against BOTH host lines. Spreading a variable dodges that check, and the
1799
+ * old registry silently drops the key, so a row degrades to exactly the name
1800
+ * it had before. `undefined` must therefore express "no badge"; a `null`
1801
+ * would throw inside the registry's `badge.text` and take the whole provider
1802
+ * out of the catalog (see `model-badge.ts`).
1803
+ *
1804
+ * ONE CLOCK FOR THE WHOLE CATALOG. `now` is fixed before the map, not inside
1805
+ * it: a promotion window can end between two rows, and two models of one
1806
+ * directory disagreeing about whether the same offer is running is a bug the
1807
+ * user sees as one row decorated and its neighbour not.
1808
+ */
1809
+ async listModels(provider) {
1810
+ const models = await super.listModels(provider);
1811
+ const now = /* @__PURE__ */ new Date();
1812
+ return models.map((model) => {
1813
+ const badge = this.badgeOf(model.id, now);
1814
+ return {
1815
+ ...model,
1816
+ ...badge === void 0 ? {} : { badge }
1817
+ };
1818
+ });
1404
1819
  }
1405
1820
  resolveModel(provider, model, signal) {
1406
1821
  return super.resolveModel(provider, model, signal).then((info) => this.advertiseDefaultEffort(info));
@@ -1491,13 +1906,36 @@ function createTraeAdapter(options) {
1491
1906
  defaultEffort: info.reasoning.defaultEffort
1492
1907
  }, options.defaultEfforts?.() ?? {});
1493
1908
  };
1909
+ /**
1910
+ * The badge for one advertised row, resolved from the LIVE catalog.
1911
+ *
1912
+ * `catalog.current()` for the same reason `resolveDefaultEffort` reads it:
1913
+ * those are the rows this provider actually serves, so the badge describes the
1914
+ * promotion running RIGHT NOW rather than one saved earlier.
1915
+ *
1916
+ * ONE CLOCK PER CALL, NOT PER ROW. `listModels()` maps over every row, so
1917
+ * reading `Date.now()` per row would let a window boundary fall between two
1918
+ * rows of the same catalog — one model marked "闲时 5 折" and the next not, in
1919
+ * the same menu, from the same clock tick that never happened. `listModels`
1920
+ * fixes the instant once and passes it here.
1921
+ *
1922
+ * THE ENTERPRISE ROUTE IS NOT SPECIAL-CASED. The badge now describes a
1923
+ * promotion rather than a consumer-pool price, and `activity_discount` is a
1924
+ * property of the row's own config, so a tenant row that publishes one is
1925
+ * genuinely discounted for that tenant. The `showRate` suppression stays where
1926
+ * it belongs: the NAME still omits the multiplier there.
1927
+ */
1928
+ const resolveBadge = (model, now) => {
1929
+ const info = options.catalog.current().find((entry) => entry.id === model);
1930
+ return info === void 0 ? void 0 : traeModelBadge(info, now);
1931
+ };
1494
1932
  return {
1495
1933
  adapter: new DefaultEffortAdapter({
1496
1934
  profiles: () => profiles,
1497
1935
  auth: INERT_AUTH,
1498
1936
  resolveApiKey: async () => options.secret ?? options.shim.token(),
1499
1937
  ...options.resolveAttachments === void 0 ? {} : { resolveAttachments: options.resolveAttachments }
1500
- }, resolveDefaultEffort),
1938
+ }, resolveDefaultEffort, resolveBadge),
1501
1939
  invalidate() {
1502
1940
  profiles = /* @__PURE__ */ new Map([[providerId, profile]]);
1503
1941
  }
@@ -5232,97 +5670,6 @@ function decodeRawChatChunk(value) {
5232
5670
  }] : result;
5233
5671
  }
5234
5672
  //#endregion
5235
- //#region src/promo.ts
5236
- function record$1(value) {
5237
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5238
- }
5239
- function finite(value) {
5240
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5241
- }
5242
- function percent(value) {
5243
- const parsed = finite(value);
5244
- return parsed !== void 0 && parsed > 0 && parsed < 100 ? parsed : void 0;
5245
- }
5246
- function ratePair(raw) {
5247
- const entry = record$1(raw);
5248
- if (entry === void 0) return {};
5249
- const beforeRate = finite(entry["before_consumption_rate"]);
5250
- const rate = finite(entry["consumption_rate"]) ?? finite(entry["after_consumption_rate"]);
5251
- const discountPercent = percent(entry["discount"]);
5252
- return {
5253
- ...beforeRate === void 0 || beforeRate <= 0 ? {} : { beforeRate },
5254
- ...rate === void 0 || rate <= 0 ? {} : { rate },
5255
- ...discountPercent === void 0 ? {} : { discountPercent }
5256
- };
5257
- }
5258
- function windows(raw) {
5259
- const list = Array.isArray(raw) ? raw : [];
5260
- const parsed = [];
5261
- for (const item of list) {
5262
- const entry = record$1(item);
5263
- if (entry === void 0) continue;
5264
- const weekdays = Array.isArray(entry["weekdays"]) ? entry["weekdays"].filter((day) => Number.isInteger(day) && day >= 1 && day <= 7) : [];
5265
- const startMinute = finite(entry["start_minute"]);
5266
- const endMinute = finite(entry["end_minute"]);
5267
- if (weekdays.length === 0 || startMinute === void 0 || endMinute === void 0) continue;
5268
- if (startMinute < 0 || startMinute > 1439 || endMinute < 1 || endMinute > 1440) continue;
5269
- if (startMinute >= endMinute) continue;
5270
- parsed.push({
5271
- weekdays,
5272
- startMinute,
5273
- endMinute
5274
- });
5275
- }
5276
- return parsed;
5277
- }
5278
- /**
5279
- * Parse one `activity_discount` document.
5280
- *
5281
- * `value` may be the already-parsed object (both call sites hand us the parent
5282
- * object) or the raw JSON string; anything without `enable: true` reads as "no
5283
- * promotion" rather than as a rule we might later misjudge.
5284
- */
5285
- function parseTraeActivityDiscount(value) {
5286
- let source = value;
5287
- if (typeof source === "string") {
5288
- if (source === "") return void 0;
5289
- try {
5290
- source = JSON.parse(source);
5291
- } catch {
5292
- return;
5293
- }
5294
- }
5295
- const raw = record$1(source);
5296
- if (raw === void 0 || raw["enable"] !== true) return void 0;
5297
- const data = record$1(raw["data"]);
5298
- if (data === void 0) return void 0;
5299
- const subKey = typeof raw["subKey"] === "string" && raw["subKey"] !== "" ? raw["subKey"] : void 0;
5300
- const currentRaw = record$1(data["current"]);
5301
- const currentType = typeof currentRaw?.["discount_type"] === "string" ? currentRaw["discount_type"] : void 0;
5302
- const current = currentRaw === void 0 ? void 0 : {
5303
- ...ratePair(currentRaw),
5304
- ...currentType === void 0 ? {} : { discountType: currentType }
5305
- };
5306
- const memberRaw = data["member"];
5307
- const member = record$1(memberRaw) === void 0 ? void 0 : ratePair(memberRaw);
5308
- const subsidyRaw = data["subsidy"];
5309
- const subsidy = record$1(subsidyRaw) === void 0 ? void 0 : ratePair(subsidyRaw);
5310
- const offPeakRaw = record$1(data["off_peak"]);
5311
- const offPeak = offPeakRaw === void 0 ? void 0 : {
5312
- ...ratePair(offPeakRaw),
5313
- ...typeof offPeakRaw["time_zone"] === "string" && offPeakRaw["time_zone"] !== "" ? { timeZone: offPeakRaw["time_zone"] } : {},
5314
- windows: windows(offPeakRaw["time_windows"])
5315
- };
5316
- const parsed = {
5317
- ...subKey === void 0 ? {} : { subKey },
5318
- ...current === void 0 ? {} : { current },
5319
- ...member === void 0 ? {} : { member },
5320
- ...offPeak === void 0 ? {} : { offPeak },
5321
- ...subsidy === void 0 ? {} : { subsidy }
5322
- };
5323
- return parsed.current === void 0 && parsed.member === void 0 && parsed.offPeak === void 0 && parsed.subsidy === void 0 ? void 0 : parsed;
5324
- }
5325
- //#endregion
5326
5673
  //#region src/solo.ts
5327
5674
  /**
5328
5675
  * `llm_utils_chat` functions to union per region, in priority order.
@@ -12648,4 +12995,4 @@ function apply(ctx, config) {
12648
12995
  });
12649
12996
  }
12650
12997
  //#endregion
12651
- export { Config, FALLBACK_CONTEXT_WINDOW, 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, servedContextWindow, settingsNamespaceOf, sidebarPanelEnabled, thinkingEffortsOf, traeEndpoint, traeInputModalities, traeModelCallChannel, traeModelDisplayName, traeModelInView, traeModelMatchesQuery, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, wireTargetsOf, withChatCallableFunctions, withContextAdaptation, withTraeRegion };
12998
+ export { Config, FALLBACK_CONTEXT_WINDOW, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_BADGE_TONES, 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, servedContextWindow, settingsNamespaceOf, sidebarPanelEnabled, thinkingEffortsOf, traeEndpoint, traeInputModalities, traeModelBadge, 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.14.2",
5
+ "version": "4.16.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",