dsh-connect-trae 4.14.2 → 4.15.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 CHANGED
@@ -1,5 +1,210 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.15.0 (2026-11-08)
4
+
5
+ ### 新增:模型选择器里的胶囊,改成「谁都能显示」
6
+
7
+ 按同族插件的做法(`dsh-connect-workbuddy` 的 `docs/MODEL-LABEL-SPEC.md`)补完两件事。
8
+ 两件是**同一条链的两头**,缺一头都不成立。
9
+
10
+ **背景一句话**:`conversation.input.model.label` 是 `single` 槽,只渲染一条注册,
11
+ 而赢家的组件会被**所有** provider 的行走一遍。所以:
12
+
13
+ | 位置 | 谁能画 | 靠什么 |
14
+ | --- | --- | --- |
15
+ | 自己的行 | 自己的组件 | 自己判定的促销胶囊(时钟判定,已有) |
16
+ | 别人的行 | **槽位赢家** | 只有 `model.badge`——别人**发布过**的东西 |
17
+
18
+ 4.8.1 只做了下半张表(赢家复现别人的 `badge`),本版补上半张:**本插件也开始发布**。
19
+ 不发布的话,一旦 qoder / workbuddy 抢先占了那格,本插件的行就什么装饰都没有——
20
+ 这正是那两家各自胶囊消失时看到的现象。
21
+
22
+ **一、adapter 发布 `badge`(`src/model-badge.ts` + `src/adapter.ts`)**
23
+
24
+ 覆盖 `listModels()`,只挂 `badge`,别的一个字段都不动:
25
+
26
+ ```ts
27
+ const now = new Date() // ONE clock for the whole catalog
28
+ return models.map((model) => {
29
+ const badge = this.badgeOf(model.id, now)
30
+ return { ...model, ...(badge === undefined ? {} : { badge }) }
31
+ })
32
+ ```
33
+
34
+ 三条都是踩过的坑,写死在测试里:
35
+
36
+ 1. **必须用展开(`...`)而不是字面量直写 `badge`。** 0.1.x 的 `LlmModelInfo` 没有这个
37
+ 字段,字面量会被 tsc 判为未知属性;展开一个变量不受这条检查约束,同一份代码在两个
38
+ 宿主线上都能编译(已用 0.2.2-rc.1 的类型实测)。旧宿主会在注册表里**静默丢掉**它,
39
+ 不报错、不崩,行退化成原来的名字。
40
+ 2. **「没有 badge」是 `undefined`,不是 `null`。** 注册表对传入值直接做 `badge.text`,
41
+ `null` 会在 `listModels` 里抛错,而那个错误是**按 provider 的**——`trae` 的**全部**
42
+ 模型会一起从目录消失(进 `failures`),不只是带胶囊那一行。
43
+ 3. **文案 ≤ 12 个 grapheme、`tone` 只能用四个字面量、翻译表必须含 `en`。** 这条链上
44
+ 没有任何一步校验它们:注册表只做规范化,宿主的 `Tag` 只把 tone 写成 `data-tone`。
45
+ 写错的后果都是**安静的**——缺 `en` 是一枚空胶囊,错的 tone 是一枚没有配色的胶囊。
46
+
47
+ **说的是哪一件事:此刻正在跑的促销。** 与卡片上、与自己行上画的那枚胶囊**同一份数据、
48
+ 同一个判定**(`selectTraePromoBadge`)——`5 折` / `闲时 5 折` / `限时 5 折`,中文照上游
49
+ 的「折」,英文改写成百分比(英文没有「折」)。`subsidy` / `limited` 落 `warning`,
50
+ `offPeak` 落 `info`,与 `PROMO_TONES` 一致。
51
+
52
+ **时钟在 `listModels()` 里读,这是规范说的「where it is drawn」那一刻**,而**不是**
53
+ 把促销冻进目录:`activityDiscount` 一直是以未判定的形态挂在模型条目上的,判定发生在
54
+ 画的那一刻,窗口一关就不再出现。整个目录**只取一次时间**——否则一次刷新期间窗口边界
55
+ 落下,同一张菜单里会有一行带胶囊、隔壁那行不带。
56
+
57
+ **这一版推翻了两小时前的一稿,原因值得记下来。** 第一稿放的是「免费」
58
+ (`creditMultiplier === 0` → `Free`/`免费`,`success`),理由是「badge 是目录数据,
59
+ 不该装随时钟变的东西」。两处都错:
60
+
61
+ | 错在哪 | 实测 |
62
+ | --- | --- |
63
+ | **那个判据根本不可达** | 两条解析路径都把上游的 `consumption_rate` 过一遍 `finitePositive`(`src/solo.ts:103`、`src/model-metadata.ts:54`),它要求 `> 0`,所以 `rate: 0` 进来是 `undefined`。`creditMultiplier` 恰好为 `0` 只能来自**已保存的** `lastCatalog`,永远不来自实时目录。喂 `rate: 0` / `0.0` / `'0'` 给真实解析器,三个都是 `undefined` |
64
+ | **我把规范的边界读反了** | 规范「三个已知边界」第 2 条反对的是**分钟级负载态**(负载等级、剩余积分、队列长度),促销恰恰是第 1 步 ✅ 示例的头一项;而 workbuddy 自己的 `resolveModelBadge` 放的就是 `selectPromoBadge(info.promos, Date.now())` |
65
+
66
+ 第一稿能活下来,是因为**夹具是我手写的** `creditMultiplier: 0`——一个没有任何解析
67
+ 路径能产生的值。测试因此全绿,功能其实一行都不会显示。现在补了一条守卫直接喂真实
68
+ 解析器(`tests/model-badge.spec.ts` 的 `why a price-derived badge is impossible`),
69
+ 把这条不可达性钉住。
70
+
71
+ **英文措辞按最坏情况定的。** 12 grapheme 的上限要按 `percent = 100` 检查,不是按好看的
72
+ `50`:`Off-peak 100% off` 是 17,会撑破一行;`Offpeak 100%` 正好 12。测试专门构造了
73
+ 100% 的夹具,只测 50 会漏掉真实的溢出。
74
+
75
+ **二、别人的行:按宿主的样子复现(`src/client/TraeModelLabel.tsx`)**
76
+
77
+ 非本插件的行本来就会复现 `badge`(4.8.1),本版按宿主真值校正了三处,并让这一支
78
+ **对任何形状都不抛**:
79
+
80
+ - **顺序**:`trigger` 时胶囊在**前**、名字在后;`menu` / `option` 时名字在前、胶囊在
81
+ 后。`selected` 不参与。
82
+ - **配色**:`info` 是 `state-business-primary`(不是 `brand-primary`)、`warning` 是
83
+ `state-warn-primary`(不是 `state-warn-secondary`),底色是各自颜色 10%(`warning`
84
+ 12%)的 `color-mix`,**没有描边**。原来的实现这两个 token 都抄错了,而且加了描边。
85
+ 差一个 token 仍然「看着像蓝的」,所以这种错能活过一次 review——但同一行两颗胶囊是
86
+ 并排比较的。
87
+ - **几何**:`padding: 1px 8px`、`font-size: 11px`、`line-height: 17px`、`999px` 圆角,
88
+ 且**不截断**(原来加了 `max-width` + 省略号,宿主不截断,截了反而在同宽度下显示成
89
+ 「限时免…」)。
90
+ - **不抛**:`null`、未知 tone、非字符串 `text`、根本不是对象的 `badge`,一律退化成
91
+ 纯名字。这一支崩了不是丢一枚胶囊——`entriesOfSlot` 会把**整个组件**从那一格摘掉,
92
+ 于是选择器里**每一行**的模型名都跟着没。
93
+
94
+ 这一支仍然是纯函数:不发请求、不读快照、不起定时器(一个菜单可能同时有二十行别家的
95
+ 模型)。
96
+
97
+ **没改的**:`model.name` 一个字符都没动。名字还被宿主用作菜单 `title`、触发器的
98
+ `aria-label` 和搜索排序键,把胶囊语法塞进去会从这三个地方漏出来。
99
+
100
+ ### 修复:面板里那个空框
101
+
102
+ 积分面板的表格和「最近刷新」之间会画出一个**空白的框**——有边框、有高度,里面一个字
103
+ 都没有。截图报告的正是它。
104
+
105
+ **根因是守卫问错了问题。** 渲染条件是 `signedIn?.credits?.expiry !== undefined`,只要
106
+ 上游把四个桶发下来就成立;而真正决定「有没有话说」的是 `expiryNotice()`,它在四个桶
107
+ **全部 `packs: 0`** 时返回 `undefined`——没有时间范围、没有金额、也没有包数可报。
108
+ 两层判断不一致,于是 `<div class="dsm-trae-expiry">` 照样渲染,里面那个 `<span>` 是
109
+ 空的,边框用的还是 `tone ?? 'lapsed'` 的兜底值:
110
+
111
+ ```
112
+ <div ...><span></span></div> ← 测试里真实拿到的东西
113
+ ```
114
+
115
+ **这个形状不是异常数据。** `/usage` 返回一个 `expiry` 块、而所有包要么不在 7 天窗口内
116
+ 要么已经用尽,是普通账号的常态——每个这样的账号都会顶着一个空框。
117
+
118
+ **修法**:把「取 notice」收成一个函数 `expiryNoticeOf()`,渲染条件直接读它的返回值。
119
+ 这样「有没有框」和「有没有句子」是同一个布尔值,不可能再对不上。卡片那侧的
120
+ `TraeAccountPanel` 本来就是对的(`notice === undefined` 直接 `return null`),所以这次
121
+ 只动了面板一处;两处共用的层级判定仍在 `expiry-ladder.ts`。
122
+
123
+ **测试**:先写了一条会失败的用例(`expiry` 对象在、四个桶全空 → 断言
124
+ `.dsm-trae-expiry` 为 `null`),确认它在修之前红,输出就是上面那个空 div。修完转绿,
125
+ 同时断言 `.dsm-trae-composer-panel-foot` 仍在——证明是「框没了」,不是「整个面板没渲染」。
126
+
127
+ ### 测试
128
+
129
+ 全仓 **110 个测试文件 / 1425 例**通过(新增 2 个文件 / 36 例)。
130
+
131
+ - `tests/model-badge.spec.ts`(15 例):促销行带 badge、无促销行**连键都没有**
132
+ (`'badge' in model`)、名字不变、`en` 必填、tone 在四个字面量内、时钟判定(窗口内
133
+ 有、窗口外没有)、与卡片措辞一致;**并按最坏情况(`percent = 100`)**断言 12 grapheme
134
+ 上限——只测 50 会漏掉 `Off-peak 100% off` 那个真实的溢出。
135
+ 另有一条 `why a price-derived badge is impossible`:喂真实解析器 `rate: 0` / `0.0` /
136
+ `'0'`,断言三者都解析成 `undefined`。这是这次翻车的那条路,留着防止有人再回头用它。
137
+ 还有一条守着「英文**故意**比卡片短」:把卡片那句 `Off-peak 100% off` 拿来断言它确实
138
+ 超过 12,再断言 badge 的英文不是它——将来谁想「统一」两处文案,会先在这里失败。
139
+ - `tests/composer-points.spec.tsx` 新增 1 例(空桶不画框,见上)。
140
+ - `tests/model-label-host-badge.spec.tsx`(19 例):三种 placement 的顺序与那个空格、
141
+ 四个 tone 的 token、无描边、宿主几何、不截断、未知 tone 落 `neutral`,以及 `null` /
142
+ 非对象 / 非字符串 `text` / 空白串全部退化成纯名字且**不抛**、别家行不发请求不起定时器。
143
+ - `tests/settings-integration.spec.ts` 新增 1 例端到端:经**真实**的 `LlmRuntime` 走一遍
144
+ 设置 schema → 目录 → `listModels()`,断言促销行拿到 badge、普通行没有这个键。
145
+ - 反向验证(不是只在修好之后断言,六次全部被抓):`placement` 两个分支对调 → 6 例红;
146
+ `badge` 改成 `null` → 1 例红;展开写法改成 `{ ...model, badge }` → 2 例红;忽略传入的
147
+ 时钟(写死一个日期)→ 1 例红;去掉 `en` → 4 例红;`offPeak` 的 tone 改成 `brand` →
148
+ 1 例红。
149
+ - **一次没抓住的变异,记在这里**:把 `promoZhe` 的 `Number.isInteger(zhe) ? String(zhe)
150
+ : zhe.toFixed(1)` 改成 `String(zhe)`,14 例全绿。实测 `percent` 取遍 1–100,两种写法
151
+ 的输出**完全相同**,所以任何断言都抓不到它。这一条不是测试不够,是该分支在当前取值
152
+ 域里等价;注释里已写明这个重复实现为什么不会漂移。
153
+
154
+ ## 4.14.3 (2026-11-07)
155
+
156
+ ### 修复:CI 连续 8 次失败——两处都是配置,与代码无关
157
+
158
+ `main` 上从 4.13.0 起连续 8 个提交 CI 全红(ubuntu + windows 同时),但
159
+ `pnpm test` 本地一直是绿的。两处原因都在安装阶段,代码从未被跑到。
160
+
161
+ **第一处:pnpm 11 把「忽略构建脚本」当成错误。**
162
+
163
+ CI 用 pnpm 11,本地用 pnpm 10。同一个 `pnpm install --frozen-lockfile`:
164
+
165
+ | 版本 | 遇到未声明的构建脚本 |
166
+ | --- | --- |
167
+ | pnpm 10(本地) | 只警告,继续装 |
168
+ | pnpm 11(CI) | `ERR_PNPM_IGNORED_BUILDS`,**exit 1** |
169
+
170
+ `pnpm-workspace.yaml` 里的 `allowBuilds` 从初始提交起就有,但只列了
171
+ `@google/genai` 和 `protobufjs`,没列 `tesseract.js`。现在显式写 `false`:
172
+ 它的 postinstall 只是 `opencollective-postinstall`(捐款横幅),OCR 不需要它。
173
+
174
+ **为什么本地永远复现不出来**:pnpm 版本不同。这条教训值得记住——本地绿不等于
175
+ CI 绿,而这里的差异来自**包管理器版本**,不是代码。
176
+
177
+ **第二处:CI 的 OCR 语言包用了会崩的版本。**
178
+
179
+ 修好安装后,隐私门禁会立刻暴露出下一个问题。CI 下载的是
180
+ `4.0.0_best/eng.traineddata.gz`,tesseract.js 7 加载它直接 abort:
181
+
182
+ ```
183
+ Aborted(missing function: _ZN9tesseract13DotProductSSEEPKfS1_i)
184
+ ```
185
+
186
+ 换成 `4.0.0`(tesseract.js 自己会去取的那个版本)就正常。实测对比:
187
+
188
+ | 文件 | 大小 | 结果 |
189
+ | --- | --- | --- |
190
+ | `4.0.0_best` | 12,821,300 | abort,门禁记 SKIP |
191
+ | `4.0.0` | 10,923,060 | OCR 正常,读出 768 字符 |
192
+
193
+ 这个 SKIP 是个陷阱:`verify-privacy.mjs` 把后端失败记成「无法验证」而不是
194
+ 「通过」,所以它不会误报绿——但会挡住发布,且提示语指向 `DSH_TESSDATA`,
195
+ 容易让人以为是本地环境问题而不是数据文件问题。
196
+
197
+ ### 验证
198
+
199
+ 在临时副本里用 **pnpm 11.28.11** 复刻 CI 全流程(不是本地 pnpm 10):
200
+
201
+ - `pnpm install --frozen-lockfile` → 通过(去掉 `tesseract.js: false` 立刻复现
202
+ `ERR_PNPM_IGNORED_BUILDS`,证明这条是必需的)
203
+ - `typecheck` / `test`(108 文件 1389 例)/ `build` → 全部通过
204
+ - `verify:privacy` + `DSH_TESSDATA` → 17 项,**无 SKIP**
205
+
206
+ 两处修复分别单独验证过:缺任一条,对应的步骤就失败。
207
+
3
208
  ## 4.14.2 (2026-11-07)
4
209
 
5
210
  ### 修复:非 loopback 绑定下,同源请求被当成外站(issue #78)
package/README.en.md CHANGED
@@ -47,7 +47,7 @@ In the account pool, open **More settings** and choose **Claude style** or **Def
47
47
  - **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.
48
48
  - **The card then splits into "Account / Models / Diagnostics"** (learned from the sibling `dsh-connect-workbuddy`). The region row above is the **scope** — which provider this whole card is about — while these are sections of that one card: Account covers who serves, what is left, the check-in and the pool; Models covers which models to tick, image input, the context budget and Save; Diagnostics read-only lists the panel, the provider id, the serving account, both token deadlines, the catalog/enabled model counts, whether the pool and the check-in are offered, and the **verbatim** text of this panel's last reported failure. **As of 4.4.7 the diagnostics section carries one more line, "quarantined copies"**: a credential copy that could not be read — and was therefore renamed aside instead of being erased — is named there, together with how many accounts it still holds. "Where did the account I remember go?" is now answered by the card instead of by browsing `~/.dsh` for a file name; the signed-out screen shows the same notice, because that is the screen a user lands on after an account disappears. The split exists because the two halves **commit differently**: an account change lands on the click, a catalog change is a draft until Save — so a single long column put the save button three screens away from what it saves. Switching sections never refetches, never drops a half-filled model draft, and never rebuilds the pool's log. A signed-out card renders no section strip, so its one screen of instructions cannot be hidden behind a tab the user cannot see. **The Models section lays every model out at once, with no inner scrollbar** — it is not a little window scrolling inside a page that already scrolls; the card's own page is the only scroll.
49
49
  - **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.
50
- - **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).
50
+ - **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). **The same capsule is drawn in DSH's own model picker** (the host's `conversation.input.model.label` slot), where the choice is actually made — a badge you only read in the settings card arrives after you have already picked. In the picker **the multiplier appears exactly once**: the catalog rate embedded in the name is pulled back out and, while a discount runs, replaced by the **discounted** price (same clock, same data as the card), and opening the picker triggers no extra upstream read. **And the plugin also publishes its own selector badge now**: through `LlmModelInfo.badge` it hands over **the promotion running right now** (`5 折` / `off-peak 5 折` / `limited 5 折`), from the same data and the same judgement the card uses. The clock is read where the host rebuilds the catalog (`listModels()`), so a closed window stops appearing, and one instant covers the whole directory. The picker's label slot is `single`, so the plugin that wins it draws every provider's rows; a connector that publishes no badge has bare rows the moment another plugin holds that cell.
51
51
  - **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.
52
52
  - **Text-to-image (`trae_draw_image`)** — brings the signed-in account's Trae text-to-image service into the conversation: the model draws one image and the plugin **fetches the bytes too**, writes them under the session cwd and shows the picture inline. **Off by default, enabled per panel** — generation really spends credits (measured: ~8.8 for one `1920x1920` on CN personal), so unlike the image-input declaration it only happens when you say so. Protocol, size bounds, the two-step fetch and why the old "fetch" GET is really a second generation are in "Text-to-image" below.
53
53
  - **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. **The US cluster (`userRegion: "US"` / `*.traeapi.us`) is recognized but not served yet**: a US credential is never routed to the Singapore gateways; the account panel names the account and says the region is unsupported, and that signing in again changes nothing (issue #66).
package/README.md CHANGED
@@ -51,7 +51,7 @@
51
51
  > 池是**显式选择**:默认关闭。它不会替你在没打开池的情况下换账号——「换账号等于换人计费」,所以这件事只能由你点头。池打开后卡片会明确写出**当前是谁在付费**(Host 报告的是「下一个真正发送请求的账号」,不是保存的那个绑定)。
52
52
 
53
53
  - **倍率内嵌模型名** —— 模型名称按 Trae 自身菜单的格式显示积分倍率(如 `GLM-5.2 · x0.79`),倍率随目录刷新更新。
54
- - **活动优惠徽标** —— Trae 把促销规则发在目录里(`activity_discount`:折扣比例、原价、闲时时段与时区),插件按**当前时钟**判定后,在模型名旁边画一枚胶囊:`5 折` / `闲时 5 折` / `限时 5 折`,悬停写明原价、现价与时段。判定放在**渲染时刻**而不是解析时刻,所以夜间折扣不会在第二天早上继续挂着;上游说「此刻没打折」就不画,**不从倍率反推**,也不给没有结束时间的活动编「限时」。这是纯提示:不改请求、不改计费,国际区(订阅制)没有这项数据因此不显示。详见 [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md)。**同一枚胶囊也画在 DSH 的模型选择器里**(宿主 `conversation.input.model.label` 插槽),挑模型时就能看见——毕竟徽标画在设置卡片里时,你已经选完了。选择器里**倍率只出现一次**:名字里已有的目录倍率会被拆出来,有折扣时换成**折后价**(与卡片同一套时钟判定,同一份数据),打开选择器不会额外触发上游读。
54
+ - **活动优惠徽标** —— Trae 把促销规则发在目录里(`activity_discount`:折扣比例、原价、闲时时段与时区),插件按**当前时钟**判定后,在模型名旁边画一枚胶囊:`5 折` / `闲时 5 折` / `限时 5 折`,悬停写明原价、现价与时段。判定放在**渲染时刻**而不是解析时刻,所以夜间折扣不会在第二天早上继续挂着;上游说「此刻没打折」就不画,**不从倍率反推**,也不给没有结束时间的活动编「限时」。这是纯提示:不改请求、不改计费,国际区(订阅制)没有这项数据因此不显示。详见 [docs/PROMO_BADGE.md](docs/PROMO_BADGE.md)。**同一枚胶囊也画在 DSH 的模型选择器里**(宿主 `conversation.input.model.label` 插槽),挑模型时就能看见——毕竟徽标画在设置卡片里时,你已经选完了。选择器里**倍率只出现一次**:名字里已有的目录倍率会被拆出来,有折扣时换成**折后价**(与卡片同一套时钟判定,同一份数据),打开选择器不会额外触发上游读。**现在本插件也发布自己的选择器徽标**:通过 `LlmModelInfo.badge` 把**此刻正在跑的促销**交出去(`5 折` / `闲时 5 折` / `限时 5 折`),与卡片上是同一份数据、同一个判定。时钟在宿主重建目录的那一刻(`listModels()`)读,所以窗口一关就不会再挂在那儿;整个目录只取一次时间。选择器的标签槽是 `single`,赢家替**所有** provider 的行画;插件不发布 `badge`,一旦别的插件占了那格,本插件的行就什么装饰都没有。
55
55
  - **DSH 本地工具循环** —— 通过 Trae `llm_utils_chat` 获取待执行的结构化 `tool_calls`,交由 DSH 自带的本地工具执行,再将工具结果回传模型。
56
56
  - **文生图(`trae_draw_image`)** —— 把登录账号的 Trae 文生图能力接进会话:模型画一张图,插件把**图片字节一起取回来**、写进会话目录再显示给你。**默认关闭、按面板单独开**——生图真实消耗积分(实测 CN 个人版 `1920x1920` 一张约 8.8),所以它和识图那种「让不让看图」的开关不同,只能由你点头。协议、尺寸边界、取图那两步,以及为什么「取图」那条 GET 其实是又一次生成,见下方「[文生图](#文生图trae_draw_image)」。
57
57
  - **国内国际双区域自动识别** —— 自动发现 Trae CN / TRAE SOLO CN / Trae / TRAE SOLO 四个本地安装的登录账号;区域由凭证自带的 `userRegion` 声明自动判定(host 后缀与 edition 标签兜底),无需手动指定。**美国区(`userRegion: "US"` / `*.traeapi.us`)目前「识别但不接入」**:插件不会把 US 凭据当成新加坡区发出去,而会在账号面板明确写「美国区(US)暂不支持」并点名账号,同时说明重新登录不会改变结果(issue #66)。
package/lib/client.js CHANGED
@@ -7684,13 +7684,22 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
7684
7684
  return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
7685
7685
  }
7686
7686
  /**
7687
- * The soonest expiry the chosen tier names, or undefined when none carries one.
7688
- *
7689
- * Read from the tier the shared ladder picked, so the time shown always belongs
7690
- * to the horizon the headline claims — the two cannot disagree.
7687
+ * The notice this panel would render, or undefined when there is nothing to say.
7688
+ *
7689
+ * THE ONE GUARD THE RENDER READS. An earlier version asked
7690
+ * `signedIn?.credits?.expiry !== undefined`, which is true whenever upstream sent
7691
+ * the four buckets — even with every `packs` count at 0. The box then rendered
7692
+ * with an empty `<span>` inside and the `lapsed` border (the tone fallback),
7693
+ * i.e. a warning-shaped rectangle containing nothing. Every empty-bucket
7694
+ * document produced one, which is the blank box in the reported screenshot.
7695
+ *
7696
+ * Putting the whole lookup here means the render cannot ask a weaker question
7697
+ * than the copy does: "is there a box" and "is there a sentence" are now the
7698
+ * same boolean.
7691
7699
  */
7692
- function soonestExpiryOf(expiry) {
7693
- return expiryNotice(expiry)?.soonestExpiresAtMs;
7700
+ function expiryNoticeOf(signedIn) {
7701
+ const expiry = signedIn?.credits?.expiry;
7702
+ return expiry === void 0 ? void 0 : expiryNotice(expiry);
7694
7703
  }
7695
7704
  /**
7696
7705
  * The notice's headline sentence, naming the amount and the pack count.
@@ -8255,12 +8264,12 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
8255
8264
  }, row.id);
8256
8265
  }) })]
8257
8266
  }),
8258
- activeRegion === "cn" && signedIn?.credits?.expiry !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8259
- className: `dsm-trae-expiry ${expiryToneClass(expiryNotice(signedIn.credits.expiry)?.tier ?? "lapsed")}`,
8267
+ activeRegion === "cn" && expiryNoticeOf(signedIn) !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8268
+ className: `dsm-trae-expiry ${expiryToneClass(expiryNoticeOf(signedIn).tier)}`,
8260
8269
  role: "status",
8261
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: expiryMessageOf(signedIn.credits.expiry, t) }), soonestExpiryOf(signedIn.credits.expiry) === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8270
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: expiryMessageOf(signedIn.credits.expiry, t) }), expiryNoticeOf(signedIn).soonestExpiresAtMs === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8262
8271
  className: "dsm-trae-expiry-detail",
8263
- children: t("row.expiryEarliest", { time: formatDateTime(soonestExpiryOf(signedIn.credits.expiry)) })
8272
+ children: t("row.expiryEarliest", { time: formatDateTime(expiryNoticeOf(signedIn).soonestExpiresAtMs) })
8264
8273
  })]
8265
8274
  }) : null,
8266
8275
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -9827,87 +9836,150 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9827
9836
  * made the throw rarer and harder to read, not absent.
9828
9837
  */
9829
9838
  const MODEL_LABEL_PRIORITY = -102;
9830
- /** The tone colours, mapped onto the same theme tokens the host's Tag paints. */
9839
+ /** The four tones the host understands, as a lookup for narrowing. */
9840
+ const HOST_BADGE_TONE_NAMES = [
9841
+ "neutral",
9842
+ "info",
9843
+ "success",
9844
+ "warning"
9845
+ ];
9846
+ /**
9847
+ * The tone palette, transcribed from the host's `Tag.module.css` (0.2.2-rc.1).
9848
+ *
9849
+ * TWO TOKENS ARE EASY TO MISCOPY, AND BOTH WERE: `info` paints
9850
+ * `state-business-primary` (not `brand-primary`) and `warning` paints
9851
+ * `state-warn-primary` (not `state-warn-secondary`). A near-miss token still
9852
+ * "looks blue" or "looks amber", which is why this survived a review — but two
9853
+ * capsules on one row are compared directly, so the drift is visible.
9854
+ *
9855
+ * The fill is the tone's own colour mixed into transparent (10%, or 12% for
9856
+ * `warning`), and there is NO border: the host's capsule is a fill only.
9857
+ */
9831
9858
  const HOST_BADGE_TONES = {
9832
9859
  neutral: {
9833
9860
  color: "var(--dsw-alias-label-secondary)",
9834
- background: "var(--dsw-alias-bg-layer-2)",
9835
- border: "var(--dsw-alias-border-l2)"
9861
+ background: "var(--dsw-alias-bg-module-platform)"
9836
9862
  },
9837
9863
  info: {
9838
- color: "var(--dsw-alias-brand-primary)",
9839
- background: "var(--dsw-alias-bg-layer-2)",
9840
- border: "var(--dsw-alias-border-l2)"
9864
+ color: "var(--dsw-alias-state-business-primary)",
9865
+ background: "color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)"
9841
9866
  },
9842
9867
  success: {
9843
9868
  color: "var(--dsw-alias-state-success-primary)",
9844
- background: "var(--dsw-alias-bg-layer-2)",
9845
- border: "var(--dsw-alias-border-l2)"
9869
+ background: "color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)"
9846
9870
  },
9847
9871
  warning: {
9848
- color: "var(--dsw-alias-state-warn-secondary)",
9849
- background: "var(--dsw-alias-bg-layer-2)",
9850
- border: "var(--dsw-alias-border-l2)"
9872
+ color: "var(--dsw-alias-state-warn-primary)",
9873
+ background: "color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent)"
9851
9874
  }
9852
9875
  };
9876
+ /** Whether a value is one of the host's four tone literals. */
9877
+ function isHostBadgeTone(value) {
9878
+ return typeof value === "string" && HOST_BADGE_TONE_NAMES.includes(value);
9879
+ }
9853
9880
  /**
9854
9881
  * The one line a badge shows, in the reader's language.
9855
9882
  *
9856
- * The host contract requires an English entry, so that is the last resort; the
9857
- * active language and its base tag (`de-DE` → `de`) come first, matching what
9858
- * the host's own resolver does. An empty string is treated as absent — a blank
9859
- * chip would be a decoration that says nothing.
9883
+ * `text` is `unknown` because it comes off the wire. A literal is used as-is; a
9884
+ * table is looked up exactly the way the host's `resolveText` does it — active
9885
+ * language, then its base tag (`pt-BR` → `pt-br` → `pt`), then the required
9886
+ * `en`. Anything else (a number, an array, a nested object) is not text, so it
9887
+ * reads as absent rather than being stringified into `[object Object]`.
9888
+ *
9889
+ * A BLANK string is treated as absent, which is deliberately STRICTER than the
9890
+ * host: `resolveText` returns `''` untouched (it only fills in missing keys), so
9891
+ * `{ en: '' }` paints a real, empty capsule. Refusing that here means a
9892
+ * decoration never says nothing — a rule this component adds, not the host's.
9860
9893
  */
9861
9894
  function resolveHostBadgeText(text) {
9862
9895
  if (typeof text === "string") return text.trim() === "" ? void 0 : text;
9896
+ if (typeof text !== "object" || text === null || Array.isArray(text)) return void 0;
9897
+ const table = text;
9863
9898
  const language = uiLanguage().toLowerCase();
9864
9899
  const base = language.split("-")[0] ?? "";
9865
- const picked = text[language] ?? (base === "" ? void 0 : text[base]) ?? text.en;
9866
- return picked === void 0 || picked.trim() === "" ? void 0 : picked;
9900
+ const candidates = [
9901
+ table[language],
9902
+ base === "" ? void 0 : table[base],
9903
+ table["en"]
9904
+ ];
9905
+ for (const candidate of candidates) {
9906
+ if (typeof candidate !== "string") continue;
9907
+ if (candidate.trim() !== "") return candidate;
9908
+ }
9909
+ }
9910
+ /**
9911
+ * Read one provider-authored badge, or `undefined` when there is nothing to say.
9912
+ *
9913
+ * THE WHOLE POINT IS THAT THIS DOES NOT THROW. `badge` arrives from another
9914
+ * plugin's catalog, so its shape is that plugin's business: a `null`, an unknown
9915
+ * tone, a missing `text` and a numeric `text` are all things a third party can
9916
+ * ship. A throw here does not lose the capsule — it retires THIS COMPONENT from
9917
+ * the `single` cell, which strips the name off every row in the picker. The
9918
+ * degradation is therefore total: anything unreadable becomes the plain name.
9919
+ */
9920
+ function readHostBadge(badge) {
9921
+ if (typeof badge !== "object" || badge === null || Array.isArray(badge)) return void 0;
9922
+ const label = resolveHostBadgeText(badge.text);
9923
+ if (label === void 0) return void 0;
9924
+ const tone = badge.tone;
9925
+ return {
9926
+ tone: isHostBadgeTone(tone) ? tone : "neutral",
9927
+ label
9928
+ };
9867
9929
  }
9868
- /** One provider-authored selector badge, drawn where the host's own label draws it. */
9930
+ /**
9931
+ * One provider-authored selector badge, drawn where the host's own label draws it.
9932
+ *
9933
+ * Styled INLINE rather than by class, because a slot component renders OUTSIDE
9934
+ * the card: the card's stylesheet is only mounted while the card is, so a picker
9935
+ * opened on a page that never mounted it would otherwise get an uncoloured
9936
+ * capsule. The geometry is the host's `Tag` verbatim — `padding: 1px 8px`,
9937
+ * `font-size: 11px`, `line-height: 17px`, `border-radius: 999px`,
9938
+ * `corner-shape: round`.
9939
+ *
9940
+ * NO `max-width` / `text-overflow`, deliberately: the host does not truncate, so
9941
+ * truncating here would show "限时免…" at exactly the row width where the host
9942
+ * showed the whole thing — less consistent, not more.
9943
+ */
9869
9944
  function HostBadge(props) {
9870
- const tone = HOST_BADGE_TONES[props.badge.tone] ?? HOST_BADGE_TONES.neutral;
9945
+ const tone = HOST_BADGE_TONES[props.badge.tone];
9946
+ const style = {
9947
+ display: "inline-flex",
9948
+ alignItems: "center",
9949
+ flex: "none",
9950
+ borderRadius: "999px",
9951
+ padding: "1px 8px",
9952
+ fontSize: "11px",
9953
+ lineHeight: "17px",
9954
+ fontWeight: 500,
9955
+ whiteSpace: "nowrap",
9956
+ color: tone.color,
9957
+ background: tone.background,
9958
+ cornerShape: "round"
9959
+ };
9871
9960
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9872
- className: "dsm-trae-model-label-host-badge",
9873
- style: {
9874
- flex: "none",
9875
- borderRadius: "999px",
9876
- padding: "0 6px",
9877
- fontSize: "10px",
9878
- fontWeight: 500,
9879
- lineHeight: "15px",
9880
- whiteSpace: "nowrap",
9881
- border: `1px solid ${tone.border}`,
9882
- color: tone.color,
9883
- background: tone.background,
9884
- maxWidth: "8em",
9885
- overflow: "hidden",
9886
- textOverflow: "ellipsis"
9887
- },
9888
- children: props.label
9961
+ className: `dsm-trae-model-label-host-badge dsm-trae-model-label-host-badge-${props.badge.tone}`,
9962
+ style,
9963
+ children: props.badge.label
9889
9964
  });
9890
9965
  }
9891
9966
  function TraeModelLabel(props) {
9892
9967
  const { provider, model } = props;
9893
9968
  const panel = panelOfProvider(provider.id);
9894
9969
  if (panel === void 0) {
9895
- const badgeInfo = model.badge;
9896
- const label = badgeInfo === void 0 ? void 0 : resolveHostBadgeText(badgeInfo.text);
9897
- if (badgeInfo === void 0 || label === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
9898
- const badge = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostBadge, {
9899
- badge: badgeInfo,
9900
- label
9970
+ const badge = readHostBadge(model.badge);
9971
+ if (badge === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name });
9972
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
9973
+ className: "dsm-trae-model-label",
9974
+ style: {
9975
+ display: "inline-flex",
9976
+ alignItems: "center",
9977
+ gap: "5px",
9978
+ whiteSpace: "nowrap",
9979
+ verticalAlign: "baseline"
9980
+ },
9981
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostBadge, { badge })]
9901
9982
  });
9902
- return props.placement === "trigger" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
9903
- badge,
9904
- " ",
9905
- model.name
9906
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
9907
- model.name,
9908
- " ",
9909
- badge
9910
- ] });
9911
9983
  }
9912
9984
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraeModelLabelInner, {
9913
9985
  modelId: model.id,
package/lib/index.d.ts CHANGED
@@ -3408,6 +3408,69 @@ declare class TraeGatedUpstreamClient implements TraeUpstreamClient {
3408
3408
  chatStream(bodyJson: string, signal?: AbortSignal): Promise<TraeChatResult>;
3409
3409
  }
3410
3410
  //#endregion
3411
+ //#region src/model-badge.d.ts
3412
+ /**
3413
+ * Emphasis of a selector badge, spelled out locally.
3414
+ *
3415
+ * Deliberately NOT imported from `@deepseek-ai/dsh-llm`: `LlmModelBadge` does
3416
+ * not exist on 0.1.x, so importing it would make this file uncompilable on the
3417
+ * host range the plugin still declares. The four literals below are the host's
3418
+ * complete vocabulary, and {@link TRAE_BADGE_TONES} is what the test checks
3419
+ * them against.
3420
+ */
3421
+ type TraeBadgeTone = 'neutral' | 'info' | 'success' | 'warning';
3422
+ /** Every tone the host understands; the test asserts this list is complete. */
3423
+ declare const TRAE_BADGE_TONES: readonly TraeBadgeTone[];
3424
+ /**
3425
+ * One selector badge.
3426
+ *
3427
+ * `text` is a translation table rather than a literal so the capsule reads in
3428
+ * the reader's language. `en` is REQUIRED (the host falls back to it for any
3429
+ * language it cannot find) and both keys are lowercase language ids, matching
3430
+ * the host's `localeKey()` lookup.
3431
+ */
3432
+ interface TraeModelBadge {
3433
+ text: {
3434
+ readonly en: string;
3435
+ readonly zh: string;
3436
+ };
3437
+ tone: TraeBadgeTone;
3438
+ }
3439
+ /**
3440
+ * The badge one catalog row should carry, or `undefined` for none.
3441
+ *
3442
+ * WHY THE PROMOTION, AND NOT THE PRICE. A badge is the ONE thing a sibling can
3443
+ * draw on this plugin's behalf, so it has to be the thing a user would act on.
3444
+ * "This model is 50% off right now" changes the choice; a price that has been
3445
+ * identical for months does not.
3446
+ *
3447
+ * WHY THE CLOCK IS READ HERE. `activityDiscount` rides along UNRESOLVED on the
3448
+ * model entry precisely so the judgement happens where the row is drawn
3449
+ * (`src/promo.ts` explains why freezing it at parse time would leave a night-time
3450
+ * discount hanging there the next morning). `listModels()` IS that moment on the
3451
+ * host side — it runs when the host builds the catalog — and the plugin asks the
3452
+ * host to re-read the catalog via `llm/models-updated` whenever the card's own
3453
+ * clock crosses a window boundary. So this is not "catalog data that ignores the
3454
+ * clock"; it is a clock judgement made at each catalog rebuild.
3455
+ *
3456
+ * A NOTE ON WHAT WAS TRIED FIRST AND REJECTED. An earlier version advertised
3457
+ * `creditMultiplier === 0` as "免费"/"Free". It never fired: every path that
3458
+ * parses upstream's `consumption_rate` runs it through `finitePositive`
3459
+ * (`src/solo.ts`, `src/model-metadata.ts`), which requires `> 0`, so a rate of
3460
+ * `0` arrives as `undefined`. A multiplier of exactly `0` can only come from a
3461
+ * saved `lastCatalog`, never from the live directory. Do not reintroduce a
3462
+ * price-derived badge from that field.
3463
+ *
3464
+ * @param model - the catalog row about to be advertised.
3465
+ * @param now - the clock the promotion is judged against. Passed in rather than
3466
+ * read from `Date.now()` so the caller owns the "one clock for the whole
3467
+ * catalog" decision and the tests can move time.
3468
+ * @returns the badge, or `undefined` — never `null`. A `null` makes the host
3469
+ * registry throw inside `listModels`, which drops this provider's
3470
+ * ENTIRE catalog into `failures`, not just the decorated row.
3471
+ */
3472
+ declare function traeModelBadge(model: Pick<TraeModelInfo, 'activityDiscount'>, now: Date): TraeModelBadge | undefined;
3473
+ //#endregion
3411
3474
  //#region src/refresh.d.ts
3412
3475
  /**
3413
3476
  * Stable device identity for the DeviceInfo body, sourced from the app's own
@@ -4867,4 +4930,4 @@ declare function asVolatile<S>(schema: S): S;
4867
4930
  declare const Config: z<Config>;
4868
4931
  declare function apply(ctx: Context$1, config: Config): void;
4869
4932
  //#endregion
4870
- export { Config, FALLBACK_CONTEXT_WINDOW, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, 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, type TraeActivityRule, type TraeAdapter, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, type TraeFailure, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelDetailRequest, type TraeModelGroup, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelView, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeOfficialChannel, type TraeOfficialFilterResult, type TraeOfficialRoster, TraeOfficialRosterClient, type TraeOfficialVerdict, type TraeOrderableModel, type TraePanel, type TraeProbeResult, type TraeProbeVerdict, type TraeProviderRoute, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, type TraeWireTarget, 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 };
4933
+ export { Config, FALLBACK_CONTEXT_WINDOW, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, MAX_PROBE_INPUT_TOKENS, type PiAiGeneration, type PiAiRuntimeInfo, REGION_GATEWAYS, type RawChatDelta, type RawChatMessage, type RawChatTool, SseDecoder, type SseEvent, 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, type TraeActivityRule, type TraeAdapter, type TraeBadgeTone, type TraeCachedModelConfig, TraeCatalog, type TraeChatResult, type TraeCheckinClaim, type TraeCheckinStatus, type TraeContextBudget, type TraeCredential, TraeCredentialStore, TraeDelegatingUpstreamClient, type TraeDiscoveredModel, type TraeDiscoveredReasoning, type TraeEdition, type TraeFailure, TraeFallbackUpstreamClient, type TraeFallbackUpstreamOptions, type TraeFusionRawChatEnvelope, TraeGatedUpstreamClient, type TraeGatedUpstreamOptions, type TraeIdentity, type TraeInputModality, type TraeModelBadge, type TraeModelDetailRequest, type TraeModelGroup, type TraeModelInfo, type TraeModelProbeVerdict, type TraeModelView, type TraeModelsTestResponse, type TraeObservedModelConfig, type TraeOfficialChannel, type TraeOfficialFilterResult, type TraeOfficialRoster, TraeOfficialRosterClient, type TraeOfficialVerdict, type TraeOrderableModel, type TraePanel, type TraeProbeResult, type TraeProbeVerdict, type TraeProviderRoute, TraeRawCapabilityController, type TraeRawCapabilityControllerOptions, type TraeRawCapabilitySnapshot, TraeRawCapabilityState, type TraeRawChatBehaviorConfig, type TraeRawChatCapability, type TraeRawChatClientOptions, type TraeRawChatConfig, type TraeRawChatFailureReason, type TraeRawChatRuntimeConfig, TraeRawChatUpstreamClient, type TraeRawDiagnostic, type TraeRawDiagnosticState, type TraeRawGateway, type TraeRawGatewayOptions, type TraeRawResolverResult, type TraeReasoningCapability, type TraeReasoningEffort, type TraeRefreshDevice, type TraeRegion, type TraeRegionGateways, TraeRegionState, type TraeShim, TraeSoloBridge, type TraeSoloClientOptions, TraeSoloRemoteCatalogClient, type TraeSoloRemoteCatalogOptions, TraeSoloUpstreamClient, type TraeStorageCandidate, type TraeStreamEvent, type TraeUpstreamClient, TraeUsageClient, type TraeUsageOptions, type TraeUsagePack, type TraeUsageRouteOptions, type TraeUsageSnapshot, type TraeUsageSummary, type TraeUsageView, type TraeWebAccountState, type TraeWebActivity, type TraeWebCheckin, type TraeWebCheckinClaim, type TraeWebCredits, type TraeWebUsage, type TraeWireModel, type TraeWireTarget, 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/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.15.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",