dsh-connect-trae 4.14.2 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +248 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/lib/client.js +295 -67
- package/lib/index.d.ts +64 -1
- package/lib/index.js +444 -97
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,253 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.16.0 (2026-11-08)
|
|
4
|
+
|
|
5
|
+
### 模型标签槽:按宿主的声明注册,为 `keyed` 做好准备
|
|
6
|
+
|
|
7
|
+
`conversation.input.model.label` 的 kind 写在宿主包里(`ui-model-selection`)。
|
|
8
|
+
在它改口之前是 `single`:只有一格,三家连接器只能按 `-101` / `-102` / `-105`
|
|
9
|
+
互相压价。目标形态是 `keyed`——每个 `options.key` 自成一格,宿主按 `provider.id`
|
|
10
|
+
派发 `{ entryKey }`,没人贡献的 provider 落回宿主自己的 fallback。
|
|
11
|
+
|
|
12
|
+
**本版做的是一件插件侧唯一该做的事:注册几条跟着宿主声明的 kind 走。**
|
|
13
|
+
|
|
14
|
+
- 新增 `src/client/model-label-seat.ts`:运行时读 `ctx.slots.spec(key)?.kind`,
|
|
15
|
+
`keyed` 就为 `trae` / `trae-enterprise` / `trae-global` 各注册一格,其余情况
|
|
16
|
+
(含读不到)保持历史上的单条。**一个构建同时服务两条宿主线**,宿主改口那天
|
|
17
|
+
不需要重新发版。
|
|
18
|
+
- `registerSlotEntry` 现在接受「多个 disposer」,并把它们合成一个按**反序**释放的
|
|
19
|
+
组合 disposer:注册是一条命,拆开释放会留下「某几格还占着、兄弟格已消失」的半死状态。
|
|
20
|
+
|
|
21
|
+
**为什么不能只改宿主。** 两个方向都会**静默**失败,而本仓库的注册路径把一个
|
|
22
|
+
try/catch 包在回调里(issue #74 的修复),于是它们都变成一行 `console.error`:
|
|
23
|
+
|
|
24
|
+
| 情况 | 结果 |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| keyed 的两条打到 `single` 宿主 | 第二条抛 `already has a registration`,第一条已落地 → 半注册 |
|
|
27
|
+
| 无 key 的一条打到 `keyed` 宿主 | 抛 `keyed slot … requires options.key` → 标签消失 |
|
|
28
|
+
|
|
29
|
+
`keyed` **强制要求 `key`**,这是上游提案里没写的第一道陷阱:**此前已发布**的本族每个
|
|
30
|
+
版本都注册一条无 key 的条目,只改宿主声明会让它们一起失效(本版之后的新构建会跟着
|
|
31
|
+
声明走,所以受影响的是「已经装在用户机器上」的那些版本)。第二道陷阱见下一节。
|
|
32
|
+
|
|
33
|
+
**改声明不够:宿主还必须传 `entryKey`。** 复核实机产物
|
|
34
|
+
(`ui-model-selection@0.2.2-rc.1`)发现,渲染该槽只有一个调用点,第三个参数只有
|
|
35
|
+
`fallback`、**没有 `entryKey`**;而 keyed 分支是
|
|
36
|
+
`find(e => e.options.key === opts?.entryKey)`——`undefined` 匹配不到任何条目,于是
|
|
37
|
+
**每一行**都落到 fallback。本插件的注册会成功、回调也跑、无异常无日志,只是组件再也
|
|
38
|
+
不被调用。上游的最小改动是**两处**:声明 + 调用点补 `{ entryKey: provider.id }`。
|
|
39
|
+
|
|
40
|
+
`MODEL_LABEL_PRIORITY = -102` 不改。keyed 下它不再决定可见性,只剩「同一格内谁先」;
|
|
41
|
+
三家之间不再有压制关系——停战靠的是换槽的基数,不是再压一次价。
|
|
42
|
+
|
|
43
|
+
外来行分支保留:keyed 下运行时走不到,但一个构建要同时服务两种宿主,而 `single`
|
|
44
|
+
宿主上它就是整行文字。它同时也是最便宜的哨兵——自己的行若走进那一支,说明 `key` 写错了。
|
|
45
|
+
|
|
46
|
+
## 4.15.0 (2026-11-08)
|
|
47
|
+
|
|
48
|
+
### 新增:模型选择器里的胶囊,改成「谁都能显示」
|
|
49
|
+
|
|
50
|
+
按同族插件的做法(`dsh-connect-workbuddy` 的 `docs/MODEL-LABEL-SPEC.md`)补完两件事。
|
|
51
|
+
两件是**同一条链的两头**,缺一头都不成立。
|
|
52
|
+
|
|
53
|
+
**背景一句话**:`conversation.input.model.label` 是 `single` 槽,只渲染一条注册,
|
|
54
|
+
而赢家的组件会被**所有** provider 的行走一遍。所以:
|
|
55
|
+
|
|
56
|
+
| 位置 | 谁能画 | 靠什么 |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| 自己的行 | 自己的组件 | 自己判定的促销胶囊(时钟判定,已有) |
|
|
59
|
+
| 别人的行 | **槽位赢家** | 只有 `model.badge`——别人**发布过**的东西 |
|
|
60
|
+
|
|
61
|
+
4.8.1 只做了下半张表(赢家复现别人的 `badge`),本版补上半张:**本插件也开始发布**。
|
|
62
|
+
不发布的话,一旦 qoder / workbuddy 抢先占了那格,本插件的行就什么装饰都没有——
|
|
63
|
+
这正是那两家各自胶囊消失时看到的现象。
|
|
64
|
+
|
|
65
|
+
**一、adapter 发布 `badge`(`src/model-badge.ts` + `src/adapter.ts`)**
|
|
66
|
+
|
|
67
|
+
覆盖 `listModels()`,只挂 `badge`,别的一个字段都不动:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const now = new Date() // ONE clock for the whole catalog
|
|
71
|
+
return models.map((model) => {
|
|
72
|
+
const badge = this.badgeOf(model.id, now)
|
|
73
|
+
return { ...model, ...(badge === undefined ? {} : { badge }) }
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
三条都是踩过的坑,写死在测试里:
|
|
78
|
+
|
|
79
|
+
1. **必须用展开(`...`)而不是字面量直写 `badge`。** 0.1.x 的 `LlmModelInfo` 没有这个
|
|
80
|
+
字段,字面量会被 tsc 判为未知属性;展开一个变量不受这条检查约束,同一份代码在两个
|
|
81
|
+
宿主线上都能编译(已用 0.2.2-rc.1 的类型实测)。旧宿主会在注册表里**静默丢掉**它,
|
|
82
|
+
不报错、不崩,行退化成原来的名字。
|
|
83
|
+
2. **「没有 badge」是 `undefined`,不是 `null`。** 注册表对传入值直接做 `badge.text`,
|
|
84
|
+
`null` 会在 `listModels` 里抛错,而那个错误是**按 provider 的**——`trae` 的**全部**
|
|
85
|
+
模型会一起从目录消失(进 `failures`),不只是带胶囊那一行。
|
|
86
|
+
3. **文案 ≤ 12 个 grapheme、`tone` 只能用四个字面量、翻译表必须含 `en`。** 这条链上
|
|
87
|
+
没有任何一步校验它们:注册表只做规范化,宿主的 `Tag` 只把 tone 写成 `data-tone`。
|
|
88
|
+
写错的后果都是**安静的**——缺 `en` 是一枚空胶囊,错的 tone 是一枚没有配色的胶囊。
|
|
89
|
+
|
|
90
|
+
**说的是哪一件事:此刻正在跑的促销。** 与卡片上、与自己行上画的那枚胶囊**同一份数据、
|
|
91
|
+
同一个判定**(`selectTraePromoBadge`)——`5 折` / `闲时 5 折` / `限时 5 折`,中文照上游
|
|
92
|
+
的「折」,英文改写成百分比(英文没有「折」)。`subsidy` / `limited` 落 `warning`,
|
|
93
|
+
`offPeak` 落 `info`,与 `PROMO_TONES` 一致。
|
|
94
|
+
|
|
95
|
+
**时钟在 `listModels()` 里读,这是规范说的「where it is drawn」那一刻**,而**不是**
|
|
96
|
+
把促销冻进目录:`activityDiscount` 一直是以未判定的形态挂在模型条目上的,判定发生在
|
|
97
|
+
画的那一刻,窗口一关就不再出现。整个目录**只取一次时间**——否则一次刷新期间窗口边界
|
|
98
|
+
落下,同一张菜单里会有一行带胶囊、隔壁那行不带。
|
|
99
|
+
|
|
100
|
+
**这一版推翻了两小时前的一稿,原因值得记下来。** 第一稿放的是「免费」
|
|
101
|
+
(`creditMultiplier === 0` → `Free`/`免费`,`success`),理由是「badge 是目录数据,
|
|
102
|
+
不该装随时钟变的东西」。两处都错:
|
|
103
|
+
|
|
104
|
+
| 错在哪 | 实测 |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| **那个判据根本不可达** | 两条解析路径都把上游的 `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` |
|
|
107
|
+
| **我把规范的边界读反了** | 规范「三个已知边界」第 2 条反对的是**分钟级负载态**(负载等级、剩余积分、队列长度),促销恰恰是第 1 步 ✅ 示例的头一项;而 workbuddy 自己的 `resolveModelBadge` 放的就是 `selectPromoBadge(info.promos, Date.now())` |
|
|
108
|
+
|
|
109
|
+
第一稿能活下来,是因为**夹具是我手写的** `creditMultiplier: 0`——一个没有任何解析
|
|
110
|
+
路径能产生的值。测试因此全绿,功能其实一行都不会显示。现在补了一条守卫直接喂真实
|
|
111
|
+
解析器(`tests/model-badge.spec.ts` 的 `why a price-derived badge is impossible`),
|
|
112
|
+
把这条不可达性钉住。
|
|
113
|
+
|
|
114
|
+
**英文措辞按最坏情况定的。** 12 grapheme 的上限要按 `percent = 100` 检查,不是按好看的
|
|
115
|
+
`50`:`Off-peak 100% off` 是 17,会撑破一行;`Offpeak 100%` 正好 12。测试专门构造了
|
|
116
|
+
100% 的夹具,只测 50 会漏掉真实的溢出。
|
|
117
|
+
|
|
118
|
+
**二、别人的行:按宿主的样子复现(`src/client/TraeModelLabel.tsx`)**
|
|
119
|
+
|
|
120
|
+
非本插件的行本来就会复现 `badge`(4.8.1),本版按宿主真值校正了三处,并让这一支
|
|
121
|
+
**对任何形状都不抛**:
|
|
122
|
+
|
|
123
|
+
- **顺序**:`trigger` 时胶囊在**前**、名字在后;`menu` / `option` 时名字在前、胶囊在
|
|
124
|
+
后。`selected` 不参与。
|
|
125
|
+
- **配色**:`info` 是 `state-business-primary`(不是 `brand-primary`)、`warning` 是
|
|
126
|
+
`state-warn-primary`(不是 `state-warn-secondary`),底色是各自颜色 10%(`warning`
|
|
127
|
+
12%)的 `color-mix`,**没有描边**。原来的实现这两个 token 都抄错了,而且加了描边。
|
|
128
|
+
差一个 token 仍然「看着像蓝的」,所以这种错能活过一次 review——但同一行两颗胶囊是
|
|
129
|
+
并排比较的。
|
|
130
|
+
- **几何**:`padding: 1px 8px`、`font-size: 11px`、`line-height: 17px`、`999px` 圆角,
|
|
131
|
+
且**不截断**(原来加了 `max-width` + 省略号,宿主不截断,截了反而在同宽度下显示成
|
|
132
|
+
「限时免…」)。
|
|
133
|
+
- **不抛**:`null`、未知 tone、非字符串 `text`、根本不是对象的 `badge`,一律退化成
|
|
134
|
+
纯名字。这一支崩了不是丢一枚胶囊——`entriesOfSlot` 会把**整个组件**从那一格摘掉,
|
|
135
|
+
于是选择器里**每一行**的模型名都跟着没。
|
|
136
|
+
|
|
137
|
+
这一支仍然是纯函数:不发请求、不读快照、不起定时器(一个菜单可能同时有二十行别家的
|
|
138
|
+
模型)。
|
|
139
|
+
|
|
140
|
+
**没改的**:`model.name` 一个字符都没动。名字还被宿主用作菜单 `title`、触发器的
|
|
141
|
+
`aria-label` 和搜索排序键,把胶囊语法塞进去会从这三个地方漏出来。
|
|
142
|
+
|
|
143
|
+
### 修复:面板里那个空框
|
|
144
|
+
|
|
145
|
+
积分面板的表格和「最近刷新」之间会画出一个**空白的框**——有边框、有高度,里面一个字
|
|
146
|
+
都没有。截图报告的正是它。
|
|
147
|
+
|
|
148
|
+
**根因是守卫问错了问题。** 渲染条件是 `signedIn?.credits?.expiry !== undefined`,只要
|
|
149
|
+
上游把四个桶发下来就成立;而真正决定「有没有话说」的是 `expiryNotice()`,它在四个桶
|
|
150
|
+
**全部 `packs: 0`** 时返回 `undefined`——没有时间范围、没有金额、也没有包数可报。
|
|
151
|
+
两层判断不一致,于是 `<div class="dsm-trae-expiry">` 照样渲染,里面那个 `<span>` 是
|
|
152
|
+
空的,边框用的还是 `tone ?? 'lapsed'` 的兜底值:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
<div ...><span></span></div> ← 测试里真实拿到的东西
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**这个形状不是异常数据。** `/usage` 返回一个 `expiry` 块、而所有包要么不在 7 天窗口内
|
|
159
|
+
要么已经用尽,是普通账号的常态——每个这样的账号都会顶着一个空框。
|
|
160
|
+
|
|
161
|
+
**修法**:把「取 notice」收成一个函数 `expiryNoticeOf()`,渲染条件直接读它的返回值。
|
|
162
|
+
这样「有没有框」和「有没有句子」是同一个布尔值,不可能再对不上。卡片那侧的
|
|
163
|
+
`TraeAccountPanel` 本来就是对的(`notice === undefined` 直接 `return null`),所以这次
|
|
164
|
+
只动了面板一处;两处共用的层级判定仍在 `expiry-ladder.ts`。
|
|
165
|
+
|
|
166
|
+
**测试**:先写了一条会失败的用例(`expiry` 对象在、四个桶全空 → 断言
|
|
167
|
+
`.dsm-trae-expiry` 为 `null`),确认它在修之前红,输出就是上面那个空 div。修完转绿,
|
|
168
|
+
同时断言 `.dsm-trae-composer-panel-foot` 仍在——证明是「框没了」,不是「整个面板没渲染」。
|
|
169
|
+
|
|
170
|
+
### 测试
|
|
171
|
+
|
|
172
|
+
全仓 **110 个测试文件 / 1425 例**通过(新增 2 个文件 / 36 例)。
|
|
173
|
+
|
|
174
|
+
- `tests/model-badge.spec.ts`(15 例):促销行带 badge、无促销行**连键都没有**
|
|
175
|
+
(`'badge' in model`)、名字不变、`en` 必填、tone 在四个字面量内、时钟判定(窗口内
|
|
176
|
+
有、窗口外没有)、与卡片措辞一致;**并按最坏情况(`percent = 100`)**断言 12 grapheme
|
|
177
|
+
上限——只测 50 会漏掉 `Off-peak 100% off` 那个真实的溢出。
|
|
178
|
+
另有一条 `why a price-derived badge is impossible`:喂真实解析器 `rate: 0` / `0.0` /
|
|
179
|
+
`'0'`,断言三者都解析成 `undefined`。这是这次翻车的那条路,留着防止有人再回头用它。
|
|
180
|
+
还有一条守着「英文**故意**比卡片短」:把卡片那句 `Off-peak 100% off` 拿来断言它确实
|
|
181
|
+
超过 12,再断言 badge 的英文不是它——将来谁想「统一」两处文案,会先在这里失败。
|
|
182
|
+
- `tests/composer-points.spec.tsx` 新增 1 例(空桶不画框,见上)。
|
|
183
|
+
- `tests/model-label-host-badge.spec.tsx`(19 例):三种 placement 的顺序与那个空格、
|
|
184
|
+
四个 tone 的 token、无描边、宿主几何、不截断、未知 tone 落 `neutral`,以及 `null` /
|
|
185
|
+
非对象 / 非字符串 `text` / 空白串全部退化成纯名字且**不抛**、别家行不发请求不起定时器。
|
|
186
|
+
- `tests/settings-integration.spec.ts` 新增 1 例端到端:经**真实**的 `LlmRuntime` 走一遍
|
|
187
|
+
设置 schema → 目录 → `listModels()`,断言促销行拿到 badge、普通行没有这个键。
|
|
188
|
+
- 反向验证(不是只在修好之后断言,六次全部被抓):`placement` 两个分支对调 → 6 例红;
|
|
189
|
+
`badge` 改成 `null` → 1 例红;展开写法改成 `{ ...model, badge }` → 2 例红;忽略传入的
|
|
190
|
+
时钟(写死一个日期)→ 1 例红;去掉 `en` → 4 例红;`offPeak` 的 tone 改成 `brand` →
|
|
191
|
+
1 例红。
|
|
192
|
+
- **一次没抓住的变异,记在这里**:把 `promoZhe` 的 `Number.isInteger(zhe) ? String(zhe)
|
|
193
|
+
: zhe.toFixed(1)` 改成 `String(zhe)`,14 例全绿。实测 `percent` 取遍 1–100,两种写法
|
|
194
|
+
的输出**完全相同**,所以任何断言都抓不到它。这一条不是测试不够,是该分支在当前取值
|
|
195
|
+
域里等价;注释里已写明这个重复实现为什么不会漂移。
|
|
196
|
+
|
|
197
|
+
## 4.14.3 (2026-11-07)
|
|
198
|
+
|
|
199
|
+
### 修复:CI 连续 8 次失败——两处都是配置,与代码无关
|
|
200
|
+
|
|
201
|
+
`main` 上从 4.13.0 起连续 8 个提交 CI 全红(ubuntu + windows 同时),但
|
|
202
|
+
`pnpm test` 本地一直是绿的。两处原因都在安装阶段,代码从未被跑到。
|
|
203
|
+
|
|
204
|
+
**第一处:pnpm 11 把「忽略构建脚本」当成错误。**
|
|
205
|
+
|
|
206
|
+
CI 用 pnpm 11,本地用 pnpm 10。同一个 `pnpm install --frozen-lockfile`:
|
|
207
|
+
|
|
208
|
+
| 版本 | 遇到未声明的构建脚本 |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| pnpm 10(本地) | 只警告,继续装 |
|
|
211
|
+
| pnpm 11(CI) | `ERR_PNPM_IGNORED_BUILDS`,**exit 1** |
|
|
212
|
+
|
|
213
|
+
`pnpm-workspace.yaml` 里的 `allowBuilds` 从初始提交起就有,但只列了
|
|
214
|
+
`@google/genai` 和 `protobufjs`,没列 `tesseract.js`。现在显式写 `false`:
|
|
215
|
+
它的 postinstall 只是 `opencollective-postinstall`(捐款横幅),OCR 不需要它。
|
|
216
|
+
|
|
217
|
+
**为什么本地永远复现不出来**:pnpm 版本不同。这条教训值得记住——本地绿不等于
|
|
218
|
+
CI 绿,而这里的差异来自**包管理器版本**,不是代码。
|
|
219
|
+
|
|
220
|
+
**第二处:CI 的 OCR 语言包用了会崩的版本。**
|
|
221
|
+
|
|
222
|
+
修好安装后,隐私门禁会立刻暴露出下一个问题。CI 下载的是
|
|
223
|
+
`4.0.0_best/eng.traineddata.gz`,tesseract.js 7 加载它直接 abort:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
Aborted(missing function: _ZN9tesseract13DotProductSSEEPKfS1_i)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
换成 `4.0.0`(tesseract.js 自己会去取的那个版本)就正常。实测对比:
|
|
230
|
+
|
|
231
|
+
| 文件 | 大小 | 结果 |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| `4.0.0_best` | 12,821,300 | abort,门禁记 SKIP |
|
|
234
|
+
| `4.0.0` | 10,923,060 | OCR 正常,读出 768 字符 |
|
|
235
|
+
|
|
236
|
+
这个 SKIP 是个陷阱:`verify-privacy.mjs` 把后端失败记成「无法验证」而不是
|
|
237
|
+
「通过」,所以它不会误报绿——但会挡住发布,且提示语指向 `DSH_TESSDATA`,
|
|
238
|
+
容易让人以为是本地环境问题而不是数据文件问题。
|
|
239
|
+
|
|
240
|
+
### 验证
|
|
241
|
+
|
|
242
|
+
在临时副本里用 **pnpm 11.28.11** 复刻 CI 全流程(不是本地 pnpm 10):
|
|
243
|
+
|
|
244
|
+
- `pnpm install --frozen-lockfile` → 通过(去掉 `tesseract.js: false` 立刻复现
|
|
245
|
+
`ERR_PNPM_IGNORED_BUILDS`,证明这条是必需的)
|
|
246
|
+
- `typecheck` / `test`(108 文件 1389 例)/ `build` → 全部通过
|
|
247
|
+
- `verify:privacy` + `DSH_TESSDATA` → 17 项,**无 SKIP**
|
|
248
|
+
|
|
249
|
+
两处修复分别单独验证过:缺任一条,对应的步骤就失败。
|
|
250
|
+
|
|
3
251
|
## 4.14.2 (2026-11-07)
|
|
4
252
|
|
|
5
253
|
### 修复:非 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)。
|