dsh-connect-trae 4.0.1 → 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 +109 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/client.js +218 -3
- package/lib/index.d.ts +214 -2
- package/lib/index.js +477 -39
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,114 @@
|
|
|
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
|
+
|
|
60
|
+
## 4.1.0 (2026-10-21)
|
|
61
|
+
|
|
62
|
+
> **受限网络(企业网只放行少数域名)下「刷新模型 500 / 聊天 4001」的完整修复。**
|
|
63
|
+
>
|
|
64
|
+
> 链路是三段式的:`solo.trae.cn` 的 CONNECT 被代理拒绝(403)→ 目录 `fetch failed`(刷新 500)→
|
|
65
|
+
> 模型丢失 `wireFunction` → 聊天按默认 `solo_work_lite` 发请求 → 模型不在该 function 的 config 名单里就
|
|
66
|
+
> 4001 `config_name rejected`。本版对三段各有一处改动:目录 host 回退到聊天网关的同路径(带冷却重试)、
|
|
67
|
+
> 目录分组的 function 兜底 `wireFunction`(只认可聊分组)、bridge 对「只带 function」的目录行保留
|
|
68
|
+
> resolver 的 `config_name` 大小写(issue #38 的坑)。
|
|
69
|
+
>
|
|
70
|
+
> 合并 PR #43 前做了对抗性复现,修掉了四处会伤到真实用户的回归——**最关键的一处**:原实现把功能名
|
|
71
|
+
> 属于 `solo_agent`/`builder_v3` 的目录行整行丢弃,而受限网络下那正是 9 个只在 `solo_agent` 分组里
|
|
72
|
+
> 出现的模型(`gpt-5.5`、`gpt-5.6-*`、`gpt-6-astra`、`glm-5.2`、`kimi-k2.7-code`、`Dola-Seed-2.0-Code`、
|
|
73
|
+
> `deepseek-v4-flash-0731`),它们正是 issue #19 修复要暴露的那批、且实测在默认 chat function 下
|
|
74
|
+
> 都能正常应答。现在只把非可聊 function **降级清空**,行保留。
|
|
75
|
+
|
|
76
|
+
### 修复
|
|
77
|
+
|
|
78
|
+
- **受限网络下模型目录走聊天网关备用 host,目录分组 function 兜底 `wireFunction`**(`fix(cn)`)。修复分三层:
|
|
79
|
+
- **目录 host 回退**:官方 remote host 网络错误 / 超时 / 5xx 时,按序尝试聊天网关的同一路径
|
|
80
|
+
(CN 为 `trae-api-cn.mchost.guru/api/remote/v1`,实测同路径同响应结构);401/403/404 等语义性拒绝**不回退**。
|
|
81
|
+
进程内记住最近一次**给出了非空目录**的 host。官方 host 失败后进入 5 分钟冷却:冷却期内先走记住的备用
|
|
82
|
+
host、官方 host 排在链尾(备用也挂了时仍有一次机会),冷却到期后官方 host 重新排第一 —— 只把官方 host
|
|
83
|
+
「排在第二」并不能让它被重试,因为循环在第一个成功处返回,所以必须显式冷却重试,恢复的网络才会在
|
|
84
|
+
一个冷却窗口内重新接管。空目录(200-but-empty)既不算成功(不记忆)也不终止链路(继续试下一个 base),
|
|
85
|
+
否则一个还没同步的镜像会永久堵死官方 host。回退只作用于 remote/v1 目录这一个 base;
|
|
86
|
+
`get_detail_param`、`llm_utils_chat` 照旧走各自原有 host。
|
|
87
|
+
- **目录分组 function 归因**:`fetchModels` 不再丢弃 `data.list[].function`——解析时把**可聊**分组的 function
|
|
88
|
+
附到每个模型(同一模型出现于多个分组时按 `TRAE_DIRECTORY_FUNCTIONS` 的实测优先级取,而非请求参数
|
|
89
|
+
`functions=` 的顺序;后者是历史列表,与 wire roster 自己的收集顺序不同,混用会让同一模型在「roster 有没有
|
|
90
|
+
返回」两种网络下拿到不同 function,`solo_agent_remote` 在 Remote 付费门后,优先它会多出 1005)。
|
|
91
|
+
`mergeTraeModelSources` 的 `wireFunction` 优先取 `get_detail_param` 的结论,目录分组兜底(roster 知道
|
|
92
|
+
账号的付费/可用状态,目录不知道)。同一 function 名重复出现时按名分桶保留全部,不再只留最后一组。
|
|
93
|
+
- **非可聊 function 只降级、不丢行**:`solo_agent`、`builder_v3` 是 IDE/Remote-agent roster,聊天通道实测
|
|
94
|
+
4011/4001,所以**不把这两个 function 写进 `wireFunction`**;模型本身**保留**,改由默认 chat function
|
|
95
|
+
调用。把它们整行丢掉等于在本次修复针对的受限网络里重新藏起 issue #19 的成果(详见上方导语)。
|
|
96
|
+
仅带 `wireFunction`(无 `wireConfigName`)的目录行,聊天请求会带上该 function,但 `config_name` 仍由
|
|
97
|
+
resolver 提供(目录 id 只是展示名,网关 `config_name` 大小写敏感,见 issue #38)。默认 function 与
|
|
98
|
+
fallback 模型常量未动。
|
|
99
|
+
- 已知取舍:模型被目录放在 `chat_v3` 分组下、但实际不可聊时,插件仅凭目录数据无法区分,仍会展示,
|
|
100
|
+
聊天失败走现有 4001 提示翻译(诊断入口见 `docs/ISSUE19_DIAGNOSIS.md`)。
|
|
101
|
+
|
|
102
|
+
### 测试
|
|
103
|
+
|
|
104
|
+
- 回归用例:host 回退(官方网络错误 → 备用 host 同路径同查询;401 不回退;5xx 回退;**空目录继续尝试下一个
|
|
105
|
+
base 且不被记住**;记住最近成功 host;**冷却到期后官方 host 重新排第一**;显式 baseUrl 钉住单 host)、
|
|
106
|
+
分组 function 归因(可聊分组优先;**同一 function 名重复出现时不丢模型**;容量来自后分组时保留先到先得的
|
|
107
|
+
function)、合并兜底与 `withChatCallableFunctions` 的**降级而非丢弃**、bridge 对 wireFunction-only 目录行
|
|
108
|
+
保留 resolver 的 `config_name`(大小写)、以及受限网络端到端(wire roster 为空时 9 个 `solo_agent`-only
|
|
109
|
+
模型仍然在目录里)。
|
|
110
|
+
- 全仓 **921** 例通过,0 skipped;`tsc` 两个工程干净;`build` 通过;CI(ubuntu + windows)全绿。
|
|
111
|
+
|
|
3
112
|
## 4.0.1 (2026-10-19)
|
|
4
113
|
|
|
5
114
|
> CN 区模型卡「最大输出」整列空白(issue #42)修复:上游**是**发布输出上限的——`get_detail_param` 的
|
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": "保存",
|