dsh-connect-trae 4.1.0 → 4.2.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/CHANGELOG.md +57 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/client.js +218 -3
- package/lib/index.d.ts +76 -0
- package/lib/index.js +175 -12
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.2.0 (2026-10-21)
|
|
4
|
+
|
|
5
|
+
> **模型行显示活动优惠徽标:只在上游此刻真在打折时出现。**
|
|
6
|
+
>
|
|
7
|
+
> Trae 把促销放在模型目录的同一次响应里——remote 目录的 `features.activity_discount`、wire 名单的
|
|
8
|
+
> `display_contact_config.activity_discount`(都是 JSON 字符串)。上游发的是**规则**(折扣比例、原价、
|
|
9
|
+
> 闲时时段与命名时区),不是文案。插件解析规则、在卡片**每次渲染时**按当前时钟判定,在模型名旁边画一枚
|
|
10
|
+
> 胶囊:`5 折` / `闲时 5 折` / `限时 5 折`,悬停给出原价→现价与时段。请求数、请求身份、积分语义全部不变。
|
|
11
|
+
>
|
|
12
|
+
> 判定放在渲染时刻而不是解析时刻,是因为 `time_windows` 是「Asia/Shanghai 周一到周五 00:00–09:00、
|
|
13
|
+
> 12:00–14:00、18:00–24:00,周末全天」这种墙钟时段:若在解析时挑定结论,夜里读到的「闲时 5 折」会在
|
|
14
|
+
> 第二天早上继续挂着。实测同一份规则,时钟平移 +0/6/12/24/36h 分别画出 3/5/5/3/5 枚(2026-10-21 18:16)。
|
|
15
|
+
|
|
16
|
+
### 新增
|
|
17
|
+
|
|
18
|
+
- **模型行显示活动优惠徽标**(`feat(model)`)。解析 Trae 目录里的 `activity_discount` 规则,按渲染时刻判定后
|
|
19
|
+
在模型名旁边画一枚胶囊:`5 折` / `闲时 5 折` / `限时 5 折`,悬停给出原价→现价、时段与时区。
|
|
20
|
+
- 解析层只留规则、不挑徽标;规则随 `lastCatalog` 持久化,判定在卡片每次渲染时做,胶囊随窗口自动出现和消失。
|
|
21
|
+
- **先信上游的 `current`**(服务端算好的「此刻状态」):`discount_type !== 'none'` 且折后价真比折前低才显示;
|
|
22
|
+
只有它报「没打折」时才用 `off_peak.time_windows` 按模型声明的时区自己算(跨零点不推断、`start` 含 `end` 不含)。
|
|
23
|
+
- 文案只认上游明说的:只有 `limited_discount` 写「限时」,其余按「补贴 / 闲时」;颜色是插件自己的选择。
|
|
24
|
+
- **不从倍率反推折扣**:那一行本来就显示 `x0.40`,再挂「5 折」是同一句话说两遍。
|
|
25
|
+
- 纯提示:不新增请求、不改计费语义;国际区(订阅制)目录里没有这份数据,显示路径同一条、上游补齐即生效。
|
|
26
|
+
- 详见 `docs/PROMO_BADGE.md`;复算脚本 `scripts/probe-promo.mjs`(只读,不消耗积分)。
|
|
27
|
+
|
|
28
|
+
### 修复
|
|
29
|
+
|
|
30
|
+
- **跨 group 去重不再丢促销规则**(`fix(solo-remote)`)。同一次目录响应里,同一个模型在部分 group 带
|
|
31
|
+
`activity_discount`、在部分 group 完全没有,而 group 顺序每次调用都可能不同(issue #23 的同一现象)。
|
|
32
|
+
`preferStrongerRow` 原先只按容量选行(等容量时先到先得),会把带规则的那一行丢掉,导致同一份上游数据
|
|
33
|
+
相邻两次调用一次 1 枚徽标、一次 0 枚。现在选行仍按容量,但把另一行携带的规则**并到赢家身上**;
|
|
34
|
+
没有改成「选带规则的行」,因为那会丢掉 1M Max 档——徽标不能以用户失去上下文预算为代价。
|
|
35
|
+
- **活动徽标的两处边界收紧**(`fix(promo)`)。
|
|
36
|
+
- 两条规则评分并列(都没有 `current`、各 1 个窗口)时,原先留先到的那条,今晚有没有徽标就取决于网关的
|
|
37
|
+
分组顺序;改按规则内容的规范化 JSON(键名排序)取定,任意分组顺序结果一致。
|
|
38
|
+
- 上游 `time_zone` 写坏时,判定退回账号时区但悬停仍报那个读不出来的名字;改为报**实际用于判定的**时区。
|
|
39
|
+
|
|
40
|
+
### 文档
|
|
41
|
+
|
|
42
|
+
- 新增 `docs/PROMO_BADGE.md`(两处字段位置、逐字抓包、判定规则、与 WorkBuddy #91 的能力对照、实机复核输出)。
|
|
43
|
+
- README / README.en 功能列表各加一条「活动优惠徽标」。
|
|
44
|
+
- `docs/PROMO_BADGE.md` 按 2026-10-21 17:14 复测校正:wire 侧实测 214 行 / 26 行带规则 / 9 个模型
|
|
45
|
+
(旧记 54 行 / 8 行 / 3 行在打折);补记判不了的窗口直接丢弃(不推断「全天」或跨零点)、
|
|
46
|
+
`limited` 分支的 `end_at` 未被读取(实测限时活动 `current` 报 `none`,所以「限时」暂不会真的出现)。
|
|
47
|
+
|
|
48
|
+
### 测试
|
|
49
|
+
|
|
50
|
+
- 新增 `tests/promo.spec.ts`(20 例):`enable: false` 丢弃、JSON 字符串形态、两段价格字段(`consumption_rate` /
|
|
51
|
+
`after_consumption_rate`)、窗口过滤与丢弃(缺周几、空区间、越界)、时区判定、坏时区退回账号时区、
|
|
52
|
+
跨零点不外推、`current` 优先、非 `limited` 不写「限时」、无数字不画、以及「同一份规则在框内/框外判定不同」。
|
|
53
|
+
- 新增 `tests/card-promo.spec.tsx`(6 例):胶囊渲染在模型名旁边、三种类型的颜色类、悬停数字、无规则不渲染、
|
|
54
|
+
窗口外不渲染。
|
|
55
|
+
- `tests/solo-remote.spec.ts` 补跨 group 去重 4 例:容量与规则并存、评分排序、并列按内容取定、
|
|
56
|
+
同一份数据换 group 顺序结果不变。
|
|
57
|
+
- 全仓 **78 文件 951 例**通过;`tsc` 两个工程干净;`build` 通过(CJS 344.00 kB / ESM 426.33 kB);
|
|
58
|
+
CI(ubuntu + windows)全绿。
|
|
59
|
+
|
|
3
60
|
## 4.1.0 (2026-10-21)
|
|
4
61
|
|
|
5
62
|
> **受限网络(企业网只放行少数域名)下「刷新模型 500 / 聊天 4001」的完整修复。**
|
package/README.en.md
CHANGED
|
@@ -37,6 +37,7 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
|
|
|
37
37
|
- **Any panel can be switched off on its own** — each tab carries its own checkbox (CN and international checked by default, enterprise not): unchecking **withdraws that provider from the DSH model picker entirely** (not merely hiding the tab) and stops all of its requests; checking it again restores it with the account, directory, picks, and context budgets intact. If you never use the international side, just uncheck it.
|
|
38
38
|
- **Tabbed settings card** — the card's top carries a "Domestic / Enterprise / Global" tab bar; each tab holds its own account picker, usage overview, and model management. Account, directory, selection, and unsaved drafts are fully isolated per tab — switching accounts or refreshing models on one tab never touches the other side's runtime catalog or sessions. CN and enterprise share one CN credential pool but keep **separate account bindings and model picks**: the two rosters differ (`DeepSeek-V4.1-Flash` is listed only on the enterprise pool), so picks never overwrite each other.
|
|
39
39
|
- **Multiplier in the model name** — model names show the credit multiplier in Trae's own menu format (e.g. `GLM-5.2 · x0.79`), updated with each directory refresh.
|
|
40
|
+
- **Promotion badge** — Trae publishes its discount rules in the model directory (`activity_discount`: pay ratio, list price, off-peak windows with their time zone). The plugin judges them against **the clock at render time** and paints one capsule beside the model name (`5 折` / `off-peak 5 折` / `limited 5 折`), with the old price, the new price and the window on hover. Judging at render time is what keeps a night-time discount from still hanging there the next morning; when upstream says nothing is discounted right now no badge is painted, the multiplier is never used to guess a promotion, and no campaign without an end time is labelled "limited". It is information only: no request and no billing changes, and the international region (subscription) has no such data and therefore shows no badge. See [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md).
|
|
40
41
|
- **DSH local tool loop** — gets pending structured `tool_calls` from Trae `llm_utils_chat`, lets DSH execute its own local tools, then returns tool results to the model.
|
|
41
42
|
- **Dual-region detection (CN / international)** — detects local sign-ins across Trae CN, TRAE SOLO CN, Trae, and TRAE SOLO installs. The region is derived from the credential's own `userRegion` claim (host suffix and edition label as fallbacks), with no manual switch.
|
|
42
43
|
- **Region-isolated directories and selections** — CN and international each keep their own model directory, enabled picks, image opt-ins, and context budgets, and each provider reads only its own slot.
|
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
> 池是**显式选择**:默认关闭。它不会替你在没打开池的情况下换账号——「换账号等于换人计费」,所以这件事只能由你点头。池打开后卡片会明确写出**当前是谁在付费**(Host 报告的是「下一个真正发送请求的账号」,不是保存的那个绑定)。
|
|
42
42
|
|
|
43
43
|
- **倍率内嵌模型名** —— 模型名称按 Trae 自身菜单的格式显示积分倍率(如 `GLM-5.2 · x0.79`),倍率随目录刷新更新。
|
|
44
|
+
- **活动优惠徽标** —— Trae 把促销规则发在目录里(`activity_discount`:折扣比例、原价、闲时时段与时区),插件按**当前时钟**判定后,在模型名旁边画一枚胶囊:`5 折` / `闲时 5 折` / `限时 5 折`,悬停写明原价、现价与时段。判定放在**渲染时刻**而不是解析时刻,所以夜间折扣不会在第二天早上继续挂着;上游说「此刻没打折」就不画,**不从倍率反推**,也不给没有结束时间的活动编「限时」。这是纯提示:不改请求、不改计费,国际区(订阅制)没有这项数据因此不显示。详见 [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md)。
|
|
44
45
|
- **DSH 本地工具循环** —— 通过 Trae `llm_utils_chat` 获取待执行的结构化 `tool_calls`,交由 DSH 自带的本地工具执行,再将工具结果回传模型。
|
|
45
46
|
- **国内国际双区域自动识别** —— 自动发现 Trae CN / TRAE SOLO CN / Trae / TRAE SOLO 四个本地安装的登录账号;区域由凭证自带的 `userRegion` 声明自动判定(host 后缀与 edition 标签兜底),无需手动指定。
|
|
46
47
|
- **目录与勾选按面板隔离** —— 三个面板各一套模型目录、勾选、图片开关与上下文预算,各自读各自的槽位。名录从**该面板所钉的那条网关**读取——原本钉在企业池的客户端去读消费池名录,会永远发现不了自己池的独占模型。
|
package/lib/client.js
CHANGED
|
@@ -374,6 +374,125 @@ window.__ModuleLoader__.load({
|
|
|
374
374
|
return tier !== void 0 && tier !== "" && !/^free$/i.test(tier);
|
|
375
375
|
}
|
|
376
376
|
//#endregion
|
|
377
|
+
//#region src/promo.ts
|
|
378
|
+
/** Zone used when upstream omits `off_peak.time_zone` (CN accounts). */
|
|
379
|
+
const TRAE_PROMO_DEFAULT_TIME_ZONE = "Asia/Shanghai";
|
|
380
|
+
const WEEKDAY_INDEX = {
|
|
381
|
+
Mon: 1,
|
|
382
|
+
Tue: 2,
|
|
383
|
+
Wed: 3,
|
|
384
|
+
Thu: 4,
|
|
385
|
+
Fri: 5,
|
|
386
|
+
Sat: 6,
|
|
387
|
+
Sun: 7
|
|
388
|
+
};
|
|
389
|
+
/** Wall clock in one named zone: ISO weekday (1–7) plus minutes since midnight. */
|
|
390
|
+
function zonedClock(now, timeZone) {
|
|
391
|
+
let parts;
|
|
392
|
+
try {
|
|
393
|
+
parts = new Intl.DateTimeFormat("en-US", {
|
|
394
|
+
timeZone,
|
|
395
|
+
weekday: "short",
|
|
396
|
+
hour: "2-digit",
|
|
397
|
+
minute: "2-digit",
|
|
398
|
+
hourCycle: "h23"
|
|
399
|
+
}).formatToParts(now);
|
|
400
|
+
} catch {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
let isoWeekday;
|
|
404
|
+
let hour;
|
|
405
|
+
let minute;
|
|
406
|
+
for (const part of parts) if (part.type === "weekday") isoWeekday = WEEKDAY_INDEX[part.value];
|
|
407
|
+
else if (part.type === "hour") hour = Number.parseInt(part.value, 10);
|
|
408
|
+
else if (part.type === "minute") minute = Number.parseInt(part.value, 10);
|
|
409
|
+
if (isoWeekday === void 0 || hour === void 0 || minute === void 0) return void 0;
|
|
410
|
+
if (!Number.isFinite(hour) || !Number.isFinite(minute)) return void 0;
|
|
411
|
+
return {
|
|
412
|
+
isoWeekday,
|
|
413
|
+
minutes: hour % 24 * 60 + minute
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function clockOf(minutes) {
|
|
417
|
+
const hour = Math.floor(minutes / 60);
|
|
418
|
+
const minute = minutes % 60;
|
|
419
|
+
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
420
|
+
}
|
|
421
|
+
function runningWindow(discount, now, defaultTimeZone) {
|
|
422
|
+
const offPeak = discount.offPeak;
|
|
423
|
+
if (offPeak === void 0 || offPeak.windows.length === 0) return void 0;
|
|
424
|
+
const declaredZone = offPeak.timeZone ?? defaultTimeZone;
|
|
425
|
+
let usedZone = declaredZone;
|
|
426
|
+
let clock = zonedClock(now, declaredZone);
|
|
427
|
+
if (clock === void 0 && declaredZone !== defaultTimeZone) {
|
|
428
|
+
usedZone = defaultTimeZone;
|
|
429
|
+
clock = zonedClock(now, defaultTimeZone);
|
|
430
|
+
}
|
|
431
|
+
if (clock === void 0) return void 0;
|
|
432
|
+
const match = offPeak.windows.find((window) => window.weekdays.includes(clock.isoWeekday) && clock.minutes >= window.startMinute && clock.minutes < window.endMinute);
|
|
433
|
+
return match === void 0 ? void 0 : {
|
|
434
|
+
...match,
|
|
435
|
+
timeZone: usedZone
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** `subKey` is upstream's machine name; it is the only honest source for "限时". */
|
|
439
|
+
function kindOf(discount) {
|
|
440
|
+
const key = discount.subKey ?? "";
|
|
441
|
+
if (key.includes("off_peak")) return "offPeak";
|
|
442
|
+
if (key.includes("subsidy")) return "subsidy";
|
|
443
|
+
if (key.includes("limited")) return "limited";
|
|
444
|
+
const type = discount.current?.discountType ?? "";
|
|
445
|
+
if (type.includes("off")) return "offPeak";
|
|
446
|
+
if (type.includes("limited")) return "limited";
|
|
447
|
+
return "subsidy";
|
|
448
|
+
}
|
|
449
|
+
/** Pay ratio as upstream would phrase it; falls back to the rates it published. */
|
|
450
|
+
function discountOf(price) {
|
|
451
|
+
if (price.discountPercent !== void 0) return price.discountPercent;
|
|
452
|
+
if (price.beforeRate === void 0 || price.rate === void 0) return void 0;
|
|
453
|
+
if (price.beforeRate <= 0 || price.rate >= price.beforeRate) return void 0;
|
|
454
|
+
return Math.round(price.rate / price.beforeRate * 100);
|
|
455
|
+
}
|
|
456
|
+
function badgeOf(kind, price, discount, window) {
|
|
457
|
+
const discountPercent = discountOf(price);
|
|
458
|
+
if (discountPercent === void 0) return void 0;
|
|
459
|
+
const memberDiscountPercent = discount.member === void 0 ? void 0 : discountOf(discount.member);
|
|
460
|
+
return {
|
|
461
|
+
kind,
|
|
462
|
+
discountPercent,
|
|
463
|
+
percentOff: 100 - discountPercent,
|
|
464
|
+
...price.beforeRate === void 0 ? {} : { beforeRate: price.beforeRate },
|
|
465
|
+
...price.rate === void 0 ? {} : { rate: price.rate },
|
|
466
|
+
...memberDiscountPercent === void 0 ? {} : { memberDiscountPercent },
|
|
467
|
+
...window === void 0 ? {} : {
|
|
468
|
+
fromClock: clockOf(window.startMinute),
|
|
469
|
+
untilClock: clockOf(window.endMinute),
|
|
470
|
+
timeZone: window.timeZone
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Decide the one badge (if any) a model shows right now.
|
|
476
|
+
*
|
|
477
|
+
* `now` is injected so the verdict is testable and so the card, which re-renders
|
|
478
|
+
* on its own poll, always judges against the current clock. `defaultTimeZone` is
|
|
479
|
+
* the account's zone, used only when upstream omits `off_peak.time_zone`.
|
|
480
|
+
*/
|
|
481
|
+
function selectTraePromoBadge(discount, now, defaultTimeZone = TRAE_PROMO_DEFAULT_TIME_ZONE) {
|
|
482
|
+
if (discount === void 0) return void 0;
|
|
483
|
+
const current = discount.current;
|
|
484
|
+
if (current !== void 0) {
|
|
485
|
+
if ((current.discountType ?? "none") !== "none" && (current.beforeRate === void 0 || current.rate === void 0 || current.rate < current.beforeRate)) {
|
|
486
|
+
const badge = badgeOf(kindOf(discount), current, discount);
|
|
487
|
+
if (badge !== void 0) return badge;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const window = runningWindow(discount, now, defaultTimeZone);
|
|
491
|
+
if (window !== void 0 && discount.offPeak !== void 0) {
|
|
492
|
+
if ((discount.current?.discountType ?? "none") === "none" || discountOf(discount.offPeak) !== void 0) return badgeOf("offPeak", discount.offPeak, discount, window);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
377
496
|
//#region src/client/icon.ts
|
|
378
497
|
/**
|
|
379
498
|
* Plugin icon (LD brand logo, 64px) shared across the LaoDing plugin family.
|
|
@@ -691,6 +810,15 @@ window.__ModuleLoader__.load({
|
|
|
691
810
|
.dsm-trae-model-probe-gated,.dsm-trae-model-probe-unsupported{color:var(--dsw-alias-state-warn-primary,#f59e0b);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 40%,transparent)}
|
|
692
811
|
.dsm-trae-model-probe-auth{color:var(--dsw-alias-state-error-primary,#d92d20);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#d92d20) 40%,transparent)}
|
|
693
812
|
.dsm-trae-model-probe-rate-limited,.dsm-trae-model-probe-unknown{color:var(--dsw-alias-label-tertiary,#999);border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary,#999) 35%,transparent)}
|
|
813
|
+
/* Promotion capsule (src/promo.ts). Unlike WorkBuddy's badge there is NO
|
|
814
|
+
upstream colour to honour — Trae publishes a pay ratio and nothing else — so
|
|
815
|
+
these three colours are the plugin's own choice, picked only so the kinds stay
|
|
816
|
+
distinguishable at a glance: red = subsidy price, orange = explicitly
|
|
817
|
+
"limited", blue = off-peak hours. */
|
|
818
|
+
.dsm-trae-model-promo{flex:none;border-radius:999px;padding:1px 7px;font-size:10px;font-weight:500;line-height:15px;white-space:nowrap;border:1px solid transparent}
|
|
819
|
+
.dsm-trae-model-promo-subsidy{color:var(--dsw-alias-state-error-primary,#d92d20);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d92d20) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#d92d20) 10%,transparent)}
|
|
820
|
+
.dsm-trae-model-promo-limited{color:var(--dsw-alias-state-warn-primary,#f59e0b);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 45%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 12%,transparent)}
|
|
821
|
+
.dsm-trae-model-promo-offPeak{color:var(--dsw-alias-brand-primary,#5686fe);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#5686fe) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#5686fe) 10%,transparent)}
|
|
694
822
|
.dsm-trae-model-meta{display:flex;align-items:center;gap:7px 12px;flex-wrap:wrap;color:var(--dsw-alias-label-tertiary,#999);font-size:11px;line-height:16px}
|
|
695
823
|
.dsm-trae-model-options{display:flex;align-items:center;justify-content:flex-end;gap:12px;flex:none}
|
|
696
824
|
.dsm-trae-model-image{display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:16px;cursor:pointer}
|
|
@@ -2973,6 +3101,72 @@ window.__ModuleLoader__.load({
|
|
|
2973
3101
|
children: t(PROBE_LABELS[probe.verdict])
|
|
2974
3102
|
});
|
|
2975
3103
|
}
|
|
3104
|
+
/**
|
|
3105
|
+
* How each promotion kind is phrased.
|
|
3106
|
+
*
|
|
3107
|
+
* The wording is PLUGIN copy, not upstream's: Trae publishes a rule — a pay
|
|
3108
|
+
* ratio plus optional cheap-hours windows — and never a label (contrast
|
|
3109
|
+
* WorkBuddy's `badge.label`, see docs/PROMO_BADGE.md §3). Two things
|
|
3110
|
+
* follow, and both are deliberate:
|
|
3111
|
+
*
|
|
3112
|
+
* - only `limited_discount` says 限时 / "limited"; upstream gives no end time
|
|
3113
|
+
* for the other kinds, so claiming one would be inventing urgency;
|
|
3114
|
+
* - the number shown is upstream's own pay ratio (`discount: 50` ⇒ 五折 /
|
|
3115
|
+
* "50% off"), never a re-derived price.
|
|
3116
|
+
*/
|
|
3117
|
+
const PROMO_LABELS = {
|
|
3118
|
+
limited: "row.promoLimited",
|
|
3119
|
+
subsidy: "row.promoSubsidy",
|
|
3120
|
+
offPeak: "row.promoOffPeak"
|
|
3121
|
+
};
|
|
3122
|
+
/** Pay ratio as the "折" figure a Chinese price tag uses: 50 → "5", 25 → "2.5". */
|
|
3123
|
+
function promoZhe(discountPercent) {
|
|
3124
|
+
const zhe = Math.round(discountPercent * 10) / 100;
|
|
3125
|
+
return Number.isInteger(zhe) ? String(zhe) : zhe.toFixed(1);
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* The model row's promotion capsule, or nothing.
|
|
3129
|
+
*
|
|
3130
|
+
* Judged HERE, at paint time, not when the directory was parsed: the rule's
|
|
3131
|
+
* windows are wall clocks in an upstream-named zone (00:00–09:00 on weekdays,
|
|
3132
|
+
* …), so a verdict frozen into a saved catalog would still be shouting about
|
|
3133
|
+
* last night's discount tomorrow afternoon. The card re-renders on its poll, so
|
|
3134
|
+
* the capsule appears and disappears with the window.
|
|
3135
|
+
*
|
|
3136
|
+
* One capsule at most, and it sits BESIDE the name rather than inside it: the
|
|
3137
|
+
* name is ellipsis-truncated, so a badge placed inside would be clipped away
|
|
3138
|
+
* exactly on the long model names.
|
|
3139
|
+
*/
|
|
3140
|
+
function promoBadge(model, t, now) {
|
|
3141
|
+
const badge = selectTraePromoBadge(model.activityDiscount, now);
|
|
3142
|
+
if (badge === void 0) return null;
|
|
3143
|
+
const zhe = promoZhe(badge.discountPercent);
|
|
3144
|
+
const hints = [];
|
|
3145
|
+
if (badge.beforeRate !== void 0 && badge.rate !== void 0) hints.push(t("row.promoHintRate", {
|
|
3146
|
+
before: String(badge.beforeRate),
|
|
3147
|
+
rate: String(badge.rate)
|
|
3148
|
+
}));
|
|
3149
|
+
if (badge.fromClock !== void 0 && badge.untilClock !== void 0 && badge.timeZone !== void 0) hints.push(t("row.promoHintWindow", {
|
|
3150
|
+
from: badge.fromClock,
|
|
3151
|
+
until: badge.untilClock,
|
|
3152
|
+
zone: badge.timeZone
|
|
3153
|
+
}));
|
|
3154
|
+
if (badge.memberDiscountPercent !== void 0) hints.push(t("row.promoHintMember", {
|
|
3155
|
+
zhe: promoZhe(badge.memberDiscountPercent),
|
|
3156
|
+
percent: String(100 - badge.memberDiscountPercent)
|
|
3157
|
+
}));
|
|
3158
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3159
|
+
className: `dsm-trae-model-promo dsm-trae-model-promo-${badge.kind}`,
|
|
3160
|
+
title: hints.length === 0 ? t(PROMO_LABELS[badge.kind], {
|
|
3161
|
+
zhe,
|
|
3162
|
+
percent: String(badge.percentOff)
|
|
3163
|
+
}) : hints.join(" · "),
|
|
3164
|
+
children: t(PROMO_LABELS[badge.kind], {
|
|
3165
|
+
zhe,
|
|
3166
|
+
percent: String(badge.percentOff)
|
|
3167
|
+
})
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
2976
3170
|
function dotStyle(status) {
|
|
2977
3171
|
return { background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)" };
|
|
2978
3172
|
}
|
|
@@ -3804,6 +3998,15 @@ window.__ModuleLoader__.load({
|
|
|
3804
3998
|
const savedImageIds = new Set(slotImageIds ?? legacyImageIds);
|
|
3805
3999
|
const draft = drafts[activeRegion];
|
|
3806
4000
|
const visibleModels = draft?.models ?? (status.status === "signed-in" ? status.models : []);
|
|
4001
|
+
/**
|
|
4002
|
+
* One clock reading for the whole model list.
|
|
4003
|
+
*
|
|
4004
|
+
* Promotion badges are judged against it (see `promoBadge`): reading the clock
|
|
4005
|
+
* once per render keeps every row's verdict consistent with the others, and
|
|
4006
|
+
* because the card re-renders on its poll, a badge still appears and vanishes
|
|
4007
|
+
* with its window instead of sticking until the next manual refresh.
|
|
4008
|
+
*/
|
|
4009
|
+
const nowForPromo = /* @__PURE__ */ new Date();
|
|
3807
4010
|
const savedEnabledIds = status.status === "signed-in" ? new Set(status.enabledModelIds) : /* @__PURE__ */ new Set();
|
|
3808
4011
|
const activeEnabledIds = draft?.enabledIds ?? savedEnabledIds;
|
|
3809
4012
|
const activeImageIds = draft?.imageIds ?? savedImageIds;
|
|
@@ -4567,9 +4770,9 @@ window.__ModuleLoader__.load({
|
|
|
4567
4770
|
onChange: () => {
|
|
4568
4771
|
toggleModel(model.id);
|
|
4569
4772
|
}
|
|
4570
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
4773
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4571
4774
|
className: "dsm-trae-model-copy",
|
|
4572
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4775
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4573
4776
|
className: "dsm-trae-model-name",
|
|
4574
4777
|
children: [
|
|
4575
4778
|
model.name,
|
|
@@ -4579,7 +4782,7 @@ window.__ModuleLoader__.load({
|
|
|
4579
4782
|
}),
|
|
4580
4783
|
probeBadge(probeByRegion[activeRegion][model.id], t)
|
|
4581
4784
|
]
|
|
4582
|
-
})
|
|
4785
|
+
}), promoBadge(model, t, nowForPromo)]
|
|
4583
4786
|
})]
|
|
4584
4787
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4585
4788
|
className: "dsm-trae-model-options",
|
|
@@ -5574,6 +5777,12 @@ window.__ModuleLoader__.load({
|
|
|
5574
5777
|
"row.probeRateLimited": "throttled",
|
|
5575
5778
|
"row.probeAuth": "sign-in rejected",
|
|
5576
5779
|
"row.probeUnknown": "not determined",
|
|
5780
|
+
"row.promoLimited": "Limited {percent}% off",
|
|
5781
|
+
"row.promoSubsidy": "{percent}% off",
|
|
5782
|
+
"row.promoOffPeak": "Off-peak {percent}% off",
|
|
5783
|
+
"row.promoHintRate": "was {before} credits/Mtoken, now {rate}",
|
|
5784
|
+
"row.promoHintWindow": "off-peak hours {from}–{until} ({zone})",
|
|
5785
|
+
"row.promoHintMember": "subscription price is a further {percent}% off",
|
|
5577
5786
|
"row.modelEnabled": "Enabled",
|
|
5578
5787
|
"row.discard": "Discard changes",
|
|
5579
5788
|
"row.save": "Save",
|
|
@@ -5894,6 +6103,12 @@ window.__ModuleLoader__.load({
|
|
|
5894
6103
|
"row.probeRateLimited": "被限流",
|
|
5895
6104
|
"row.probeAuth": "登录凭据被拒",
|
|
5896
6105
|
"row.probeUnknown": "本次未测出",
|
|
6106
|
+
"row.promoLimited": "限时 {zhe} 折",
|
|
6107
|
+
"row.promoSubsidy": "{zhe} 折",
|
|
6108
|
+
"row.promoOffPeak": "闲时 {zhe} 折",
|
|
6109
|
+
"row.promoHintRate": "原价 {before} 积分/百万 token,现价 {rate}",
|
|
6110
|
+
"row.promoHintWindow": "闲时时段 {from}–{until}({zone})",
|
|
6111
|
+
"row.promoHintMember": "会员价另有 {zhe} 折",
|
|
5897
6112
|
"row.modelEnabled": "启用模型",
|
|
5898
6113
|
"row.discard": "放弃修改",
|
|
5899
6114
|
"row.save": "保存",
|
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
|
|
3924
|
-
if (
|
|
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
|
|
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.
|
|
5
|
+
"version": "4.2.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -50,7 +50,8 @@
|
|
|
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\""
|
|
54
55
|
},
|
|
55
56
|
"dsh": {
|
|
56
57
|
"bundle": {
|