dsh-connect-trae 4.6.11 → 4.8.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,386 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.8.0 (2026-11-05)
4
+
5
+ > **从 npm 升级的用户,这一版把 v4.7.0 也一起带上了。** v4.7.0 的 tag 与 GitHub
6
+ > Release 在 11-01 就已存在,但那次 npm 发布从未成功——11-05 核查 registry:最新仍
7
+ > 是 4.6.12,既没有 4.7.0,也没有撤回痕迹。所以「可选侧边栏整页面板」是随本版第一次
8
+ > 到达 npm 的,完整说明见下方 4.7.0 一节。
9
+
10
+ ### 修复
11
+
12
+ - **积分浮层里两个账号显示同一个数字**(用户报告,2026-11-03)。截图里
13
+ 「用户7520607992」和「LaoDing」都是 647,而它们在上游其实是两个不同的余额。
14
+ - **根因:表格里「已选中」那一行走的是共享客户端**,而共享客户端读的是
15
+ `store.resolve()` —— 也就是**当前绑定的账号**,不是这一行自己的账号。于是这一行
16
+ 报出的是另一个账号的数字。实测复现:两个账号都显示 `644.2932`,而上游按 id 读
17
+ 分别是 `678.5424` 与 `643.9992`。
18
+ - 这个分支原本有正当理由:绑定账号存的 token 通常已过期,而
19
+ `creditsOfAccount` 当时是**纯查表**、会把过期 token 原样递出去。**`126faa9`
20
+ (2026-10-18)已经把这个理由消掉了** —— 它让 `creditsOfAccount` 先刷新再读。
21
+ 之后再留着那条分支,就只剩下「可能报错账号」这一个后果。
22
+ - **现在每一行都按 account id 读**(含绑定账号),行与行之间互相独立:每个请求
23
+ 点名自己的账号,没有哪一行能被别的行的凭证回答。
24
+ - **保留了一处回退**:宿主完全没有 `creditsOfAccount` 时,绑定账号仍从共享客户端
25
+ 读——那种宿主上共享客户端必然读的就是绑定账号,所以对这一行它不是谎话,而是唯一
26
+ 来源。它是回退,不再是规则。
27
+ - 测试:改写三条钉住旧约定的用例(它们断言的正是「绑定行走共享客户端」,而那个
28
+ 前提已经不存在),新增一条**断言共享客户端被调用 0 次**——一次调用就是一次把
29
+ 绑定账号的数字贴到别的行上的机会。变异验证:把分支改回旧形状 → 绑定账号那一行
30
+ 变成共享客户端的 `9999`,`expected 9999 to be 3392`。
31
+
32
+ - **重启后首次加载的未捕获错误:`single slot "conversation.input.model.label"`
33
+ 重复注册(issue #74)**。用户报告的是 Windows 上重启 DSH 后第一次渲染抛错、
34
+ 点一次重新加载就好,而且这条错误在三次**不同原因**的崩溃日志里都出现——说明它
35
+ 不是某次崩溃的副产物,而是启动/重建路径上稳定出现的注册冲突。
36
+ - **根因有两个,不是一个。** 报错文本说的是「priority 0 上已经有一个由
37
+ `dsh-connect-trae-client` 注册的条目」,也就是这个插件**撞上了自己**,不是撞上
38
+ 别的插件。两条独立路径都能造成:
39
+ 1. **同一个 client 被 `apply` 两次**(重载/重建路径),前一次的注册还活着。
40
+ 2. **`inject` 的回调会重跑**:`slots.inject()` 在插槽尚未声明时**不立即执行**
41
+ 回调,而是订阅声明变化、稍后再跑。等它真的抛错时,renderer 自己的 `changed()`
42
+ 会把它从 `queueMicrotask` 里**重新抛出去**——调用点的 `try/catch` 够不着,
43
+ 所以它表现为页面上的 uncaught error。
44
+ - **修法 1:优先级不再是默认的 0。** 新增导出的 `MODEL_LABEL_PRIORITY = -0.25`。
45
+ 现场实测(0.2.1-rc.5 桌面端)这个 `single` 槽已经是三方共用:
46
+ `dsh-connect-qoder-client` 占 -1、`dsh-connect-workbuddy-client` 占 -0.5、
47
+ 本插件占 0。**-1 和 -0.5 是那两个兄弟插件的座位,不能用**;-0.25 夹在中间,
48
+ 合法且互不相撞。它刻意**不**去读插槽账本、也不和其它插件协商——这是本仓库
49
+ 的 §零 不变量,宿主自己那个 1017 徽章包的 README 也给出同样结论(同优先级冲突
50
+ 的官方解法是配置决策,不是运行时握手)。
51
+ - **修法 2:注册走统一的 `registerSlotEntry`**,它在**每次注册前先释放自己**持有的
52
+ 上一个条目(`single` 槽不接受同优先级的第二个,而这一个是我们自己造成的、可以
53
+ 自己修),并且**把 catch 放进 `inject` 的回调内部**——这是唯一能拦住上面那条
54
+ `queueMicrotask` 逃逸的位置。于是任何拒绝都降级成一行日志,与本文件其余部分
55
+ 「降级、绝不把页面拖下水」的契约一致。
56
+ - **代价说明(写在这里,不是缺陷)**:该槽是 `single`,本插件占住时,其它注册者
57
+ 在**本插件认领的那些行**上的装饰不会被组合。这是记录在案的取舍,也正是组件对
58
+ 非本插件的 provider 返回纯目录名的原因——那是槽自己的最低要求。
59
+ - 测试:`tests/client-activation.spec.tsx` 新增两例,一例把同一个 entry 跑两次
60
+ `apply`(真实重建形状),断言**每个座位只有一条**而不是两条;另一例用**延迟
61
+ 回调**的槽替身复现 renderer 的真实行为,断言拒绝被记成日志、且**其它座位照常
62
+ 注册**(一个被拒的座位不能连累其余)。两条变异都验证过:去掉「先释放」
63
+ → `expected [...9 items] to deeply equal [...5 items]`;去掉回调内的 catch
64
+ → 直接抛出 `Error: single slot "conversation.input.model.label" ...`。
65
+
66
+ ### 变更
67
+
68
+ - **积分浮层的头部加回「刷新」按钮(在「设置」左边)**(m02091,m02421 改名)——
69
+ 把本面板账号池里**勾选的成员**逐个问一遍「能不能应答」,结果报在浮层底部。
70
+ - **标签按主人的说法叫「刷新」,但这个按钮要做两件事是标签说不出的**,两件都
71
+ 挪进了悬停提示与结果行:**范围**(测的是池里勾选的成员,不是表里每行)和
72
+ **代价**(它是本插件里唯一消耗额度的操作,每个成员一条最小 `ping`)。
73
+ 一个光秃秃的「刷新」读起来是免费的,而这个不是——所以 tooltip 与结果行都要
74
+ 在场,不能因为标签变短就把说明删掉。
75
+ - **造型按参考图改成「幽灵控件」**(m02477):无边框、无底色、图标 + 文字,
76
+ 底色**只在悬停时**出现。第一版是**带 1px 描边的胶囊**,问题是静止时读起来像
77
+ 卡片里的 `dsm-btn-outline`(保存/取消那种控件),而 248px 的头部里放两个
78
+ 有边框或有底色的形状会读成一枚分段控件,而不是两个不同的动作。现在两者的
79
+ 区别是**语义**的:设置是**导航**(带品牌色填充的胶囊),刷新是**就地重跑一批**
80
+ (安静的幽灵控件)。图标是**环形箭头**而不是齿轮——同族不同动词。
81
+ - **交互对齐同族 `dsh-connect-workbuddy` 的同一功能**(m02610),三处都是「按下之后
82
+ 看不见反应」的补漏,不是造型:
83
+ 1. **跑起来时图标转**(900ms 一圈)。禁用按钮配一个不动的图标读起来像坏了,而这批
84
+ 是串行的——最慢的账号决定它什么时候结束,所以「我这一下到底按上了吗」是个真问题,
85
+ 而且在整个面板开着的时间里都成立。`prefers-reduced-motion` 下**动画关掉但状态
86
+ 信号不能跟着消失**:图标保持变暗,标签本来也会变成「刷新中…」——否则箭头会冻在
87
+ 某个随机角度,读起来就是一枚静止图标,正是旋转要修的那种沉默。
88
+ 2. **`aria-label` 用完整的提示语**。标签是一个词,说不清作用域和代价;`title` 对
89
+ 屏幕阅读器**不是**名字,旁边又有个「设置」,两个都念成光秃秃一个词时正是提示语
90
+ 要消除的歧义。
91
+ 3. **提示语写明代价**:「会消耗少量积分,最长约一分钟」。标签叫「刷新」听起来是免费
92
+ 重读,而这个控件底下是一批真实的探测。让人意外掏钱的控件,比标签长一点的控件更糟。
93
+ - **顺带修掉一个真 bug**:批次结束后的重读原本只读 `/usage`,而表里的**数字**来自
94
+ `/account-credits`——于是按下「刷新」之后,表里显示的仍是这批要推翻的那些旧数字。
95
+ 现在两个文档一起重读。这条是**变异验证发现的**:把重读改回只读 `/usage`,新用例
96
+ 居然还是绿的,追下去才发现 `fetchAccountPlans` 把 `usage` 当 `useCallback` 依赖,
97
+ 每轮轮询都拿到新对象身份、于是每轮都重读一遍全部账号状态——那条泄漏让同一个请求
98
+ 从另一条路径补上了。新增 `usageRef` 把它稳定住,并写了 `tests/composer-poll-identity.spec.tsx`
99
+ 钉死「轮询不重读账号状态」。
100
+ - **因此浮层从 248px 加宽到 264px**。加了图标之后英文那行放不下了:实测
101
+ `Trae credits` + `Refresh` + `Settings` 需要 233px 内容宽,而 248px 的面板
102
+ 只剩 222px,设置胶囊**溢出 11px**。两个更糟的替代方案都否决了——改主人的措辞
103
+ (「刷新」是点名要的),或去掉图标(正是它让两个控件读成一家)。加宽之后
104
+ 中英文都 `FITS`(内容右端 271 = 头部右端 271)。这个数字连同它的推导写进了
105
+ 用例,将来加第三个控件时该动的就是这个值。
106
+ - **它测的是账号池的成员,不是表里每个账号**。这条路由
107
+ (`POST …/pool?region=<panel>&action=test`)本来就只认 `effectiveMemberAccountIds`;
108
+ 表里那些没进池的账号不在里面。所以标签不写「测速账号池」——**实测放不下**:
109
+ 头部内宽 222px,标题加两枚胶囊在英文下要 229px,「设置」会被挤出自己的行。
110
+ 短标签不是妥协而是唯一能站住的选择,改由**悬停提示**与**结果行**(`账号池已测 · …`)
111
+ 说清楚。这条是**量出来的**,不是估的:同一份 headless Chrome 实测在中英文下都
112
+ 确认 `FITS`、控件右端 255 = 头部右端 255、间隙 6px。改成「刷新」/`Refresh` 之后
113
+ 又重新量了一遍:中文 40×24、英文 57×24,两语都 `FITS`、`overflowX` 为 0。
114
+ - **按账号计数,不按行**。这条路由的回包是 NDJSON,**末尾还会再发一行
115
+ `{rows}` 把前面每一行重复一遍**(真机上实测:2 行 `{row}` + 1 行 `{rows}`)。
116
+ 照行数数会得到 `4 of 2` 这种自相矛盾的汇总,所以用一个以 **accountId** 为键的
117
+ `Map` 去重,最后报的是账号数。这条有专门的用例守着,并且**用故意改坏的方式验过**
118
+ (改成按行计数会红,打印 `expected 'ok=2,total=4' to be 'ok=1,total=2'`)。
119
+ - **没有账号池就不画这个按钮**:路由在那时会 503/409,画一个按下去必然报错的控件
120
+ 不如不画——与「没有路线时不画设置按钮」同一条规则。
121
+ - **180 秒是截止时间,超时被当成「放弃」而不是「失败」**:`AbortController` 到点
122
+ abort,catch 里判 `signal.aborted` 走 `composer.testTimedOut`,而不是含混的
123
+ 「测试失败」。一个批次跑三分钟还没回来,读者要的是「太久了我停了它」,不是
124
+ 「坏了」。按钮到点恢复可用——超时后永远灰着的控件读起来像卡死。
125
+ **用例用假计时器驱动这个 180 秒**,不是为了快,而是因为等三分钟不叫测试。
126
+ - **200 但解析出零行,不谎报成功**:`sawAny` 为假就抛,走失败行。一个空回包
127
+ 不能读作「全都没问题」。
128
+ - **测完重读一次用量**:这是插件里唯一消耗额度的操作(每条最小 `ping`),测完
129
+ 积分会变,所以 `await fetchUsage()`。与账号池分栏里那个按钮的行为一致。
130
+ - **逻辑抽到共享模块**(`src/client/pool-test.ts`):`applyTestLine` / `testLinesOf` /
131
+ `outcomeLabel` / `POOL_TEST_TIMEOUT_MS` 从 `AccountPool.tsx` 搬出来,两处共用。
132
+ 回包是 NDJSON 且**有重复行**这件事,只该在一个地方被知道——否则两个调用点
133
+ 会各自解出不同的答案。
134
+ - **样式与「设置」同几何、但不填品牌色**:`Test` 是次要动作,抢走品牌色的强调
135
+ 会让主要的那个(去改设置)看起来一样重。hover/active/focus-visible/disabled
136
+ 四态齐全,`prefers-reduced-motion` 关过渡。
137
+ - **输入框积分浮层的「刷新积分」按钮换成「设置」按钮**(m01198)—— 一点就打开
138
+ 本插件自己的设置页,浮层同时关闭。
139
+ - **样式照抄同族的同一个控件**(m01930):`dsh-connect-workbuddy` 在同一个位置
140
+ 解过同一个问题,所以这边用**同一枚胶囊**——齿轮 + 文字、`border-radius:999px`、
141
+ 品牌色 10% 淡底、11px/500、hover 加深、`active`/`focus-visible` 三态齐全、
142
+ `prefers-reduced-motion` 关掉过渡。不是「照着感觉做」:最初这里是**带边框的
143
+ 按钮**,再早同族那版是**下划线链接**,两个都被否掉是同一个理由——静止时**没有
144
+ 形状**,而可供性要在指针到达之前就成立。标题同时改成 `flex:none;white-space:nowrap`,
145
+ 因为控件一挤进那一行标题就会折成两行(同族在真机上量到过);右侧再包一层
146
+ `-actions` 拿 `margin-left:auto`,这样「没有按钮」时头部也不会重排。
147
+ - **为什么换掉而不是并排放两个**:这个浮层的读者此刻的问题是「这数字去哪改」,
148
+ 而原来的答案要求先关浮层、再在插件页里找到这一行。积分本身不需要按钮——
149
+ **打开浮层、切换账号、每 5 分钟**都会重新读一次,刷新按钮提供的那个动作
150
+ 本来就已经在别处发生了。两个短标签并排不是排不下(浮层宽 248px,标题加
151
+ 两个按钮仍有余量),而是**其中一个是多余的**:浮层的头部只该有一个控件,
152
+ 而它应该是那个浮层自己答不出来的问题。
153
+ - **走宿主的 `pluginNavigation.openBundle`,不是自己拼路由**。这台宿主上
154
+ 插件设置不在「设置」页里,而在**插件页**的 bundle 页——`plugins.bundle.config`
155
+ / `plugins.row.config` 就是配置面;「设置」只留只读的内置清单。所以「打开
156
+ 插件设置」= 「在插件页上打开这个 bundle」,官方口子就是那个服务。
157
+ - **按精确包名,且只试一个名字**:`openBundle` 拿 `pkg.name` 做**全等**比较,
158
+ Loader 的条目 id(`include:dsh-connect-trae`)永远匹配不上。也**不**退化成
159
+ 「多试几个拼写」——目标未知时页面会渲染成**列表**,第二个名字会把第一个的
160
+ 导航撤销掉。
161
+ - **回退到 `layout.selectPanel('plugins')`**:`pluginNavigation` 由插件页那个
162
+ bundle 提供,不是外壳。profile 可以不含它,而且**加载顺序不是本插件能假设
163
+ 的**——所以探测放在**按下时**而不是 `apply` 时:在 `apply` 里探一次会把
164
+ `undefined` 永久记下来,按钮从此在整个会话里都是死的。回退是**回退**:
165
+ `'plugins'` 是另一个包的私有 key,而 `openBundle` 才是它公开的口子;
166
+ `selectPanel` 在 key 没注册时会**抛**,所以调用点是 guard 而不是信任。
167
+ - **打不开就说出来,不装死**:`false` 落到浮层自己的错误行(`composer.openSettingsFailed`),
168
+ 而且浮层**不关**——按一下屏幕上什么都没变,与「按钮坏了」无法区分。
169
+ 它有自己的 state,不与「读上游失败」共用一个标志:两句话说的不是一件事,
170
+ 共用标志会让一次点击抹掉另一次的说明。
171
+ - **没有路线时不画这个按钮**:与页面标题栏的返回按钮同一条规则——做不了事的
172
+ 控件不画。
173
+ - **路由单独成模块**(`src/client/settings-route.ts`):客户端入口导入的是浏览器
174
+ 专用包,Node 测试环境装不进来,值得断言的东西必须落在入口之外——与
175
+ `panel-switch.ts` 是同一个缝。判定住在那里,按钮住在组件里。
176
+
177
+ ### 修复(积分座位注册的契约类型)
178
+
179
+ - **`registerPointsSlot` 的 `inject` 契约类型改对了**:它原来把注入体写成
180
+ `TraeUsageCardInjected`,而实际注入的是 `{ t, settingsScope, position }`——
181
+ 多出来的 `position` 之所以没报错,靠的是对象字面量的多余属性检查没有触发,
182
+ 而不是类型正确。现在写成 gate 自己的 `ComposerPointsGateProps`,两个多出来的
183
+ 属性是**契约的一部分**,不是意外。
184
+
185
+ ### 文档
186
+
187
+ - README / README.en 的「输入框读数」一段改为「设置」按钮,并说明积分本身为什么
188
+ 不再需要刷新按钮。
189
+ - README / README.en 的「输入框读数」一段同时说明「刷新」与「设置」两枚胶囊,
190
+ 以及「刷新测的是池里的成员、并且会消耗额度」这两点。
191
+ - **重画 `docs/assets/dsh-connect-trae-composer-credits.png`**(第二次:造型改成幽灵
192
+ 控件、浮层加宽到 264px,截图尺寸变 560×412,README 里的 `width` 同步改):旧图里右上角还是
193
+ 「刷新积分」,若不换,README 的配图会与界面不一致。新图由新增的
194
+ `tools/composer-shot.tsx` 生成(真实 zh 文案 + headless Chrome 实拍,536×412)。
195
+
196
+ ### 测试
197
+
198
+ - 新增 `tests/composer-settings-route.spec.tsx`(22 例),五组:
199
+ - **路由**:`openBundle` 按精确包名优先、`pluginNavigation` 缺席时回退到
200
+ `selectPanel('plugins')`、两者都没有时返回 `false` 而不抛、`openBundle` 抛时
201
+ 仍回退、**两者都抛时返回 `false`**、以及**每次调用都重新探测**(后出现的服务
202
+ 要能找到)。
203
+ - **按钮**:有路线时画出来、按下后**关掉浮层**、**国际版面板同样有它**(那条报告
204
+ 说的就是国际版)、无路线时不画、打不开时显示错误行且**浮层不关**、错误行与
205
+ 「读上游失败」互不覆盖。
206
+ - **穿过 gate**:把路线接到 `ComposerPointsGate` 上真的能按到——否则路线可以
207
+ 接进入口却被 gate 丢掉,而上面每一条断言都还是绿的。
208
+ - **没留下残骸**:两个字典里的 `composer.refresh` / `composer.refreshing`
209
+ 都已删除;新 class 有规则、旧 class 的规则**也已删除**(防止后来者以为它还在用)。
210
+ - **胶囊样式**(结构断言,CSS 字符串在 jsdom 里不过级联):`999px` 圆角、
211
+ `1px solid transparent` 边框(不是带边框按钮、也不是下划线)、品牌色 10% 淡底、
212
+ `font-weight:500`、三态齐全、reduced-motion 关过渡、齿轮 `aria-hidden`、
213
+ 齿轮与文字都 `flex:none`(否则窄浮层里齿轮会被压成椭圆)、标题 `nowrap`、
214
+ 以及**没有按钮时 `-actions` 包装层仍在**。
215
+ - `tests/composer-styles.spec.tsx` 补一条**只在渲染设置按钮时才有效**的类覆盖用例:
216
+ 原来那条是在**没有路线**的情况下渲染的,所以按钮、包装层、齿轮、文字这四个 class
217
+ 从来没被守卫看一眼——正是这个文件存在的意义所在的那类洞。新用例先断言这四个 class
218
+ **确实出现**,再断言每个都有规则。
219
+ - `tests/composer-points.spec.tsx` 里那条「行内没有刷新按钮、刷新在浮层里」的用例
220
+ 在原处改写为「行内没有自己的控件」,并显式断言**浮层里也不再有任何刷新按钮**
221
+ (`.dsm-trae-composer-panel-refresh` 为 null)与「没有路线时不画设置按钮」。
222
+ 这条用例原来锁的规则正是被替换掉的那条,所以**在原处改**而不是删掉:它同时
223
+ 守着「行内只有一个触发器」这个没变的不变量。
224
+ - 新增 `tools/composer-shot.tsx` 并注册进 `vitest.card.config.ts`。它暴露了两个
225
+ 这个仓库已经踩过一次的坑,都写进了文件注释:**浮层是 portal 到 `document.body`
226
+ 的**,只序列化渲染容器会得到一个空浮层(第一版就是这样,AUDIT 全部 null);
227
+ **jsdom 的盒子全是 0**,所以锚定定位会把浮层放到视口外,预览里必须用
228
+ `!important` 钉住位置才拍得到。
229
+ - 新增 `tests/composer-pool-test.spec.tsx`(15 例),守着上面每一条:
230
+ - **位置**:`[刷新, 设置]` 的 DOM 顺序、同一个父节点——顺序错了「设置左边」就不成立。
231
+ - **造型**:静止无边框无底色、圆角 999px、`inline-flex` + `gap:4px`、hover 不带
232
+ 品牌色、图标与文字都 `flex:none`(否则窄浮层里 12px 箭头被压成椭圆)、
233
+ 以及**面板宽度 264px** 这条带推导注释的断言。
234
+ - **不画的条件**:Host 没报 `pool` 时按钮为 null。
235
+ - **打到哪儿**:POST 到精确地址(含 `region` 与 `action=test`)。
236
+ - **数的是什么**:回包里 `{row}` 行 + 重复的 `{rows}` 汇总,只算 **2** 个账号。
237
+ - **测完重读**:结束后重新 GET 一次 usage。
238
+ - **超时**:假计时器推过 180 秒,断言走 `timedOut` 行、且按钮恢复可用。
239
+ - **不并发**:连点两下只发一次请求。
240
+ - **不谎报**:200 空解析走失败行。
241
+ - 三条**故意改坏**的变异验证,确认这些用例真的在看代码而不是在陪跑:按行计数→红
242
+ (`expected 'ok=2,total=4' to be 'ok=1,total=2'`);删掉 `if (!sawAny)` 守卫→红;
243
+ 把「没 pool 就不画」改成无条件画→红。
244
+ - `tests/composer-settings-route.spec.tsx` 里对 `-actions` 的整串断言改为按**规则体**
245
+ 断言:加了 `gap` 之后,把整条声明串原样匹配的写法会无意义地失败。
246
+ - 全仓 **103 个文件 / 1332 例**通过;两个 tsconfig 分别 typecheck 通过;构建通过
247
+ (`lib/client.js` 493.85 kB)。
248
+
249
+ ### 新增
250
+
251
+ - **每个模型的思考强度可以自己选了,而且默认档不再显示成 `Default`**
252
+ (用户要求,2026-11-03:「需要一个默认的思考强度设置,整体做法学习 qoder」)。
253
+ - **原来的问题**:Trae 的目录一直在发布每个模型自己的默认档
254
+ (`reasoning_effort_config.default_level`,实测 `deepseek-v4.1-flash` 是 `high`、
255
+ `glm-5.3` 是 `xhigh`),插件也一直把它解析进了目录 —— 但**适配器从未把它报给
256
+ 宿主**。于是 DSH 的模型选择器只能说 `Default`:既不知道这个模型会跑哪一档,
257
+ 也没法固定成别的档。
258
+ - **现在**:适配器把默认档作为 `reasoning.defaultEffort` 报上去,宿主会把它
259
+ **物化进每一个没有指名档位的请求**,DSH 的选择器也从同一份元数据取「当前档位」。
260
+ 卡片上每个声明了档位的模型多一个下拉:第一项写的是**它实际会跑的档**(例如
261
+ 「默认(high)」),其余是该模型自己声明的档位。
262
+ - **取值优先级**:用户显式选过的档 > 上游声明的默认档 > 该模型自己声明的中档。
263
+ 选「默认」是**删除**这个键,而不是存一个恰好等于今天默认值的档 —— 后者看起来
264
+ 一样,却把选择钉死了,上游改默认时这个模型就不再跟随。
265
+ - **绝不与档位表矛盾**:保存的档位若已不在该模型当前的档位表里(目录刷新会撤回
266
+ 档位),它会被忽略而不是发出去 —— 发出去会被宿主以
267
+ `UNSUPPORTED_REASONING_EFFORT` 拒掉,那比「显示 Default」糟得多。
268
+ - 这是**纯增量**的:不选任何东西时行为与之前完全一致(请求仍跑上游默认档),
269
+ 只是界面上第一次能看见它是哪一档。
270
+ - 涉及:新增 `src/default-effort.ts`;`src/adapter.ts` 的 `DefaultEffortAdapter` 与
271
+ `defaultEfforts` 选项;`src/index.ts` 的 `thinkingEfforts` 配置项、
272
+ `thinkingEffortsOf()` 读取函数与接线;`src/client/TraeUsageCard.tsx` 的
273
+ `effortPicker`、草稿与保存载荷;`src/client/locales.ts` 三条文案;
274
+ `src/client/styles.ts` 的 `.dsm-trae-model-effort*`。
275
+ - 测试:`tests/default-effort.spec.ts`(20 例,含**真实目录行形状**的适配器验证与
276
+ 三条变异验证:换回普通 `PiAiAdapter` → 5 条红;删掉 schema 声明 → 类型校验失效)
277
+ 与 `tests/card-thinking-effort.spec.tsx`(7 例,含「选回默认必须删除键」与
278
+ 「档位要穿过保存」两条,变异验证:从保存载荷里删掉字段 → 2 条红)。
279
+
280
+
281
+ ## 4.7.0 (2026-11-02)
282
+
283
+ > **Trae 现在可以放进侧边栏了 —— 可选,默认关闭。** 学同族
284
+ > `dsh-connect-workbuddy` 的做法(其 PR #188):在卡片「设置」区多一个
285
+ > 「在侧边栏显示 Trae」勾选框,勾上之后 DSH 侧边栏出现一个 Trae 行,点开是
286
+ > **同一张卡片的整页版**。
287
+
288
+ ### 变更
289
+
290
+ - **可选侧边栏面板**(`Config.sidebarPanel`,`src/index.ts` / `src/client/`)。
291
+ - **默认关闭,理由不是保守而是知情**:侧边栏的全局行(`sidebar.panellist`)
292
+ 是宿主自己的页面,这一行占的是**所有已安装插件共用**的座位;而「插件」页
293
+ 本来就能看到同样的卡片。一行没人要求的导航会改变一个插件并不拥有的界面,
294
+ 所以它必须是用户点出来的,不是升级带上来的。
295
+ - **注册与注销,而不是渲染成 null**:侧边栏的导航行是从**插槽条目元数据**
296
+ 生成的(`options.label` + `renderSlot(...)` 作图标),组件返回 `null`
297
+ 只会留下一个空白但仍可点击的按钮。所以开关驱动的是注册/注销本身:
298
+ `panel-switch.ts` 里的模块级 store,`apply()` 订阅它并 reconcile。
299
+ - **不需要刷新页面**:卡片在写入被读回确认之后才 `setSidebarPanelWanted()`,
300
+ 行随即出现或消失。页面开着时关掉,中心栏由宿主的 `layout` 自动回到会话
301
+ (`retainMainPanels` 会丢弃已不存在的 `main` key)。
302
+ - **开机时以宿主答案为准**:开关的种子取自状态文档上的 `sidebarPanelEnabled`,
303
+ 而不是设置镜像——这个命名空间的浏览器镜像是**已知会在受影响的 0.1.7 部署上
304
+ 过期**的,在这里过期的 `false` 意味着「行永远不出现」,且没有任何后续事件会
305
+ 纠正它(镜像报的是命名空间边,不是值变化)。
306
+ - **整页版复用同一份 body**:`TraeUsageCard` 新增 `surface: 'page'` 与
307
+ `backToConversation`,两处渲染的是同一棵组件树(地区页签、分栏、轮询、
308
+ 草稿、账号池日志),只有外层卡片框换成页面标题栏——复制一份组件树就是复制
309
+ 一处将来会漂移的代码。返回按钮走宿主 `layout.selectPanel(null)`,宿主没有
310
+ 这个服务时**不画**,而不是画一个点不动的。
311
+ - **新增文案 4 条**(`nav.panel` / `row.pageBack` / `row.sidebarPanelLabel` /
312
+ `row.sidebarPanelHint`),中英各一份。
313
+ - **配图**:新增 `docs/assets/dsh-connect-trae-sidebar-page.png`(整页版真实渲染,
314
+ 由 `tools/page-shot.tsx` 生成、headless Chrome 实测),中英 README 各挂一张。
315
+ - **测试**:新增 `tests/sidebar-panel-optin.spec.tsx`(17 条)钉住四件事——
316
+ 默认关闭(schema 与两个读取点)、关闭时不注册任何行/页、开关能来回切而不刷新、
317
+ 卡片与宿主状态文档两侧都落实;`tests/client-activation.spec.tsx` 增加一条用
318
+ **真实 `apply()`** 驱动 store 并观察插槽注册表(含注销)的用例——为此把该文件
319
+ 里的 `Slots` 替身改成**忠实返回 disposer**(替身若只 push,会让「关掉之后」
320
+ 和「从未注册」无法区分,而这正是这个开关要区分的东西);`tests/web-status.spec.ts`
321
+ 增加「两个形状都带这个字段,且只有显式 true 才算开」的用例。
322
+
323
+ ## 4.6.12 (2026-10-31)
324
+
325
+ > **4.6.11 的上下文窗口守卫补上了第二条出口。** issue #72 指出:#70 把兜底做进了
326
+ > `toPiModel`(适配器出口),但 `registerModelDiscovery` 回调——模型发现走向 DSH 的
327
+ > 另一条出口——仍把 `contextWindow` 原样透出,而宿主对发现行**不做任何校验**。一条
328
+ > 畸形的目录行因此能绕过 #70。
329
+
330
+ ### 变更
331
+
332
+ - **发现回调永远发出正整数窗口**(issue #72 的主点,`src/index.ts`)。
333
+ - **问题**:宿主 `discoverModels()` 只按 id 去重,`contextWindow`/`maxTokens`
334
+ 原样透传;模型管理界面把发现行**逐字**采纳进 `models` 配置项,而那一项的 schema
335
+ 要求 `step(1).min(1)`。所以原样透出意味着两件事之一:畸形值被带进下游,或者
336
+ (目录省略窗口时)行以「无窗口」抵达 DSH——正是 #70 的故障形态,只是绕开了
337
+ 适配器。症状同为一条坏行 ⇒ `INVALID_MODEL_CONTEXT` ⇒ 整个区域离线。
338
+ - **改法**:回调改为永远发出 `contextWindow: servedContextWindow(model)`——
339
+ 与适配器出口同一条规则、同一个兜底值(200 000)。两条「字段缺失即不发」的
340
+ 分支合并成一条恒发字段,两个出口对同一行给出的值因此不可能分叉。
341
+ - **目录层把「在场但畸形」归一化(三处),「省略」依旧合法**(issue #72 的后半 +
342
+ 评审判断,`src/catalog.ts`)。
343
+ - `mergeTraeModelSources` 的目录分支、`mergeTraeModelSources` 的 wire 提升分支、
344
+ `discoveredCatalog` 各加一条 `servedContextWindow` 归一化:窗口缺失仍按缺失
345
+ 上报(`tests/catalog.spec.ts` 里钉住这条既有契约的用例保持绿),但**在场**的
346
+ 值必须是正整数,否则换成兜底。
347
+ - **为什么目录层也值得补**:上游解析门 `finitePositive` 只查 `isFinite && > 0`,
348
+ 所以 `200000.5` 能活着走完解析;一个分数窗口若跟着目录行存进 `lastCatalog`,
349
+ 会让**整次设置写入**被 schema 拒绝(`modelConfig` 的 `step(1).min(1)`)。
350
+ 归一化留在生产侧,坏值就永远进不了持久化。
351
+ - **评审修正**:报告者建议 wire 提升分支不打守卫,理由是其上游
352
+ (`get_detail_param`)「结构上产不出畸形值」。核对该分支的取值链
353
+ `contextWindow = prompt_max_tokens ?? context_window_tokens.dev`——两个来源
354
+ 都出自 `finitePositive`,分数值(如 `300000.5`)**能**从那里活着到达。
355
+ 故本次三个出口一起补;`FALLBACK_CONTEXT_WINDOW` 与 `servedContextWindow`
356
+ 自 `src/adapter.ts` 移入 `src/catalog.ts` 导出(无循环依赖),两处出口共用。
357
+ - **`maxTokens` 有意不动**:宿主对发现行的 `maxTokens` 同样不校验,但它的
358
+ 校验发生在 `normalizeModelInfo`(`INVALID_MODEL_MAX_TOKENS`,适配器路径,
359
+ #70 已覆盖);目录行里 `maxTokens` 畸形只影响「最大输出」展示,没有
360
+ `INVALID_*` 硬失败面。与 #70 的口径保持一致:本补丁只管上下文窗口。
361
+ - **未采纳(留待单独评估)**:把 `finitePositive` 收紧为 `finitePositiveInteger`
362
+ ——报告者自己标注的行为变更,会同时改变目录解析与 wire 解析的入站语义,
363
+ 不适合作为一个守卫补丁的顺车。
364
+
365
+ ### 测试
366
+
367
+ - 新增 **6** 例(1240 总例,较 4.6.11 净增 6):
368
+ - **真实路径**(`tests/discovery-context-window.spec.ts`,5 例):与
369
+ `tests/settings-integration.spec.ts` 同款宿主挂具(`LlmRuntime` +
370
+ 0.1.7 形状的内存 settings + 临时 DSH_HOME + 临时 storage.json 凭据,含
371
+ `readTraeIdentity` 要求的 `telemetry.*` 身份字段),`vi.stubGlobal('fetch')`
372
+ 桩出目录 / wire 花名册 / IDE 花名册三个端点,走
373
+ `ctx.llm.discoverModels(TRAE_SETTINGS_NS, { provider: 'trae' })` 断言宿主
374
+ **读回**的行:分数目录行 → 兜底、省略窗口行 → **显式**兜底(`Object.hasOwn`
375
+ 钉住「恒发字段」)、正常 168 000 原样保留(兜底是背板不是钳位)、wire 提升
376
+ 行的分数 `prompt_max_tokens` → 兜底、以及一条「所有行都是正整数」的全量扫描。
377
+ - **目录单元**(`tests/catalog.spec.ts`,1 例,覆盖
378
+ 分数/0/负数/NaN × 三处出口 + 正常值不动)。
379
+ - **变异验证**:把发现回调改回原样透出 → 真实路径 5 例中 2 例转红(全量扫描、
380
+ 省略窗口);把 `src/catalog.ts` 三处守卫改回原样 → 目录单元例转红。恢复守卫后
381
+ 全绿。
382
+ - 全仓 **1240** 例通过(97 文件),双 tsconfig 类型检查通过。
383
+
3
384
  ## 4.6.11 (2026-10-30)
4
385
 
5
386
  > **英文界面下的「最近刷新」不再显示成中文式的时间。** issue #71 里顺带提到的那点:
package/README.en.md CHANGED
@@ -48,17 +48,22 @@ A [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) bund
48
48
  - **Vision follows Trae's own declaration** — when Trae marks a model `multimodal`, the plugin advertises it as such with no manual tick per model (issue #16). The rule is "**models you tick ∪ models Trae marks multimodal**": models Trae marks `false`, or does not mark at all, stay text-only and can still be turned on by hand. A row enabled automatically is shown ticked and read-only, with the reason. See "Image input" below.
49
49
  - **Account switching** — refreshes the token list and lets users select an account without storing tokens in DSH settings. **Your choice is remembered and stays in effect**: the plugin will never switch accounts on its own just because the bound one ran out of credit or another usable one was found (switching bills a different person). So when the bound account is exhausted the card **says so explicitly** and lists the other accounts on this machine that still have credit, with their balances, for you to switch to in one click — instead of showing an empty panel or quietly rebinding you. "Refresh accounts & tokens" now always reports what happened (how many accounts were found, who is still bound) instead of appearing to do nothing. The dropdown names the product (`Trae` / `TRAE SOLO` / international) so a credential left behind by an uninstalled app is recognisable.
50
50
  - **Read-only usage and model management** — Work/general credits on CN accounts, subscription/trial status on international ones; enable Trae models freely. Read-only queries consume nothing.
51
+ - **Per-model thinking level, and the default is no longer just `Default`** — Trae's directory has always published each model's own default level, and the plugin has always parsed it into the catalog, but **it was never reported to DSH**. The model picker could therefore only say `Default`: you could not see which level a model would run at, and you could not pin a different one. The adapter now reports that level, the host materialises it into every request that names none, and each model that declares levels gets a dropdown whose first entry names **the level it will actually run at** (e.g. "Default (high)"). Precedence is **your choice > upstream default > that model's own middle tier**; choosing "Default" DELETES the key rather than storing a level that happens to equal today's default — the latter looks identical while quietly pinning the choice, so the model would stop following upstream. A saved level the directory has since withdrawn is ignored rather than sent, because sending it is rejected with `UNSUPPORTED_REASONING_EFFORT`. Choosing nothing behaves exactly as before.
51
52
  - **Credit-near-expiry notices** — the remaining balance only says "how much is left", not "**which pack lapses first**". The notice escalates by urgency (7 days → 24 hours → already lapsed) and names the amount, the pack count and the soonest expiry, so a user watching only the total never silently loses the pack that expires first. Shown in both the usage card and the composer credits panel on the CN side (the international side is subscription-based and has no per-pack expiry).
52
53
  - **One-click test of the enabled models** — sends each **enabled** model one minimal message (a single `ping`, no tools) and reports whether it actually answers: **works** / **needs a higher plan** (Trae `1005`; your plan does not cover it) / **not served** (`4001`; no SOLO function serves it) / **throttled** (`4011`; Trae rate-limited that route) / **sign-in rejected** / **not determined**. The last one is a first-class outcome: a timeout, a dropped connection, a proxy or a 5xx is **never** reported as unusable — only an explicit upstream refusal is a negative verdict. This is the plugin's **only** operation that spends quota (check-in changes account state but spends nothing), it runs only when you click, and its results live in the current session rather than in your settings.
53
54
  > `4011` is a **rate limit**, not "this model is unavailable" (measured for issue #33): an exhausted balance reports a different code, `4008`, so "I still have plenty of credits" and "I got a 4011" are both true at once. A throttled model answers again after a short wait — do not delete it.
54
55
  - **One-click test of all models** — the same probe applied to **every visible model** of the current region, not just the enabled ones. Use it when you want to know whether the models you have switched off could still answer — after a plan change, or after Trae reshuffles its lineup.
55
56
  - **Daily check-in claiming (per account)** — on the CN card **every account row carries its own check-in button**, with the day's reward written on it (`Check in +150`). A row reads **Claimed** and disables itself once *that account* has been paid, and reads **Device used** and disables itself when *this machine* has already spent today's slot through some other account — the two are reported honestly, and the second is never dressed up as "claimed". This is the plugin's **only** operation that changes account state, and it runs only when you click; one click targets that one row's account and sends exactly one request. There is deliberately **no "claim for every account"**: Trae allows **one check-in per device per day** (switching accounts does not reset the slot), so a sweep could only manufacture a row of false failures. The international side has no check-in activity, so no button is shown there.
56
- - **Composer readout** — the composer tool row (between "Expert" and the model selector) shows a clickable readout, refreshing every 5 minutes. **Both regions show it, but they do not show the same quantity, and that is the data's doing**: the CN side is credit-based, so it reads "Trae CN · <general credits>"; the international side is subscription-based and its usage document carries no credit field at all, so it reads "Trae Global · <plan>" (e.g. `Pro` / `Free`). The whole readout is clickable and opens a panel showing a TABLE: one row per account in the region — general credits on CN, plan on international — where clicking a row SWITCHES accounts (through the same merged `accounts` write the card uses, so the other region's selection survives), with the manual refresh button in the panel. It appears **only while the session's selected model belongs to this plugin**, and renders nothing for any other provider — which is what keeps two connector plugins from competing for one shared row (the sidebar foot is a shared root row; the composer row is per-session and provider-scoped).
57
+ - **Composer readout** — the composer tool row (between "Expert" and the model selector) shows a clickable readout, refreshing every 5 minutes. **Both regions show it, but they do not show the same quantity, and that is the data's doing**: the CN side is credit-based, so it reads "Trae CN · <general credits>"; the international side is subscription-based and its usage document carries no credit field at all, so it reads "Trae Global · <plan>" (e.g. `Pro` / `Free`). The whole readout is clickable and opens a panel showing a TABLE: one row per account in the region — general credits on CN, plan on international — where clicking a row SWITCHES accounts (through the same merged `accounts` write the card uses, so the other region's selection survives). The panel's top right carries **two controls, Refresh and Settings, styled differently on purpose** — Settings navigates and carries the brand fill, while Refresh re-runs a batch in place and stays colourless until hovered. While Refresh runs its icon turns: the batch is serial, so the slowest account decides when it stops. **Refresh** asks every account checked into this panel's account pool to answer once (through the pool's `action=test` route — the same batch the pool section's own button runs) and reports the outcome in the panel's foot: `Pool tested · 2 of 2 answered`. **Two things about that press the label cannot say, so both live in the tooltip and in the foot line**: it tests the **pool's checked-in members**, not every account in the table you are looking at, and it is the **only press in this plugin that spends credits** (one minimal ping per member) — a bare "Refresh" reads as free, so the tooltip is worth reading. **Settings** opens this plugin's own settings page and closes the panel, so "where do I change this?" no longer means closing the popover and hunting the Plugins row. The balance needs no refresh button of its own — it re-reads when the panel opens, on every account switch, and every five minutes, which is why that button was replaced rather than joined by another. It appears **only while the session's selected model belongs to this plugin**, and renders nothing for any other provider — which is what keeps two connector plugins from competing for one shared row (the sidebar foot is a shared root row; the composer row is per-session and provider-scoped).
57
58
 
58
- <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="536" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
59
+ <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="560" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
59
60
 
60
61
  **Where that readout sits is a choice** (learned from the sibling `dsh-connect-workbuddy` issue #62). The default is the **composer toolbar** (the position described above); if another plugin occupies it and the readout does not appear, move it **below the input box** — the row carrying the round / step / token statistics, where the readout is APPENDED after the shipped figures rather than replacing them. The seat is set **per panel** (all three are stored separately), in the card's settings block next to the "show credits in the composer row" checkbox. Why a second seat exists at all: the composer toolbar is a **shared list slot** that the shell's own controls and every other plugin register into, and a shared slot **cannot promise that every occupant is visible** — the report behind that issue is a readout that vanished entirely once a third-party plugin was installed (this plugin has been squeezed out of a shared row once before too, down to a clipped "Tr" at the sidebar foot).
61
62
 
63
+ - **Optional: put Trae in the sidebar** (learned from the sibling `dsh-connect-workbuddy` PR #188, and **off by default** here too). Ticking "Show Trae in the sidebar" in the card's settings block adds a Trae row to the DSH sidebar; it opens the **full-page version of this same card** — same region tabs, same account / models / diagnostics sections, minus the card frame, with a title, a description and "Back to conversation" at the top. **Why it is off by default**: the sidebar's global rows are the host's own pages, and this row claims a seat **every installed bundle shares** while the Plugins page already reaches the same content — a nav row nobody asked for re-arranges a surface this plugin does not own. **It takes effect on the click** with no Save: writing `sidebarPanel` registers or disposes the row in the same second (it is not a component rendering null — that would leave a blank but still clickable button), and switching it off while the page is open sends the centre column back to the conversation by itself. The page's back control goes through the host's `layout.selectPanel(null)`; on a host without that service the control is **omitted rather than drawn dead**.
64
+ <img src="docs/assets/dsh-connect-trae-sidebar-page.png" width="720" alt="The sidebar page: back control, title and description at the top, then the same card body below" />
65
+
66
+
62
67
  - **Secure loopback shim** — one random port + in-process random secret per region; the real Trae token is never handed to pi-ai.
63
68
 
64
69
  ## How it works
package/README.md CHANGED
@@ -53,17 +53,22 @@
53
53
  - **多账号切换** —— 支持重新读取 Token 列表并选择账号;Token 不写入 DSH 设置。**你的选择会被记住并长期生效**:插件不会因为某个账号额度用尽、或检测到另一个可用账号就自作主张替你切换(换账号意味着换人计费)——唯一的例外是你**亲自打开**的账号池(见下一条),那是一个显式选择,而不是插件的自作主张。所以当绑定的账号额度耗尽时,卡片会**明确告诉你**,并把本机其他还有额度的账号连同其可用积分数列出来,由你一键切换——而不是让面板一直空着、也不自动把你切走。「刷新账号与 Token」每次都会回报结果(找到几个账号、当前仍绑定谁),不会再「点了没反应」。下拉里显示的是产品名(`Trae` / `TRAE SOLO` / 国际版),便于分辨凭据来自哪个安装。
54
54
  - **账号导入 / 导出** —— 不用再为了用第二个账号而把凭据手工写进默认路径、顶掉上一个。在卡片账号区的「导入 / 导出账号」里:**导出**勾选若干账号下载一份 JSON;**导入**选择文件后先看**脱敏预览**(只显示账号标识与「是否有 Token」,不含凭据本身)再勾选导入。文件格式与同族 `dsh-connect-workbuddy` **互通**,两边导出的文件可以互相导入。导入按账号去重(同一账号再导入即覆盖更新),**认不出所属区域或因缺 Token 的记录会被跳过并计数**——国内版与国际版是两条并行计费通道,跨区导入会导致跨区计费,所以这里只认同能**正向识别**的区域(企业版共用 CN 凭据池,不构成第三条导入通道)。导出文件里是**有效 Token,请当作密码保管**。(issue #37)
55
55
  - **只读用量与模型管理** —— 国内版查看 Work 积分与通用积分,国际版查看订阅/试用状态;刷新/启用 Trae 模型;只读查询不消耗额度。
56
+ - **每个模型的思考强度可选,默认档不再显示成 `Default`** —— Trae 的目录一直在发布每个模型自己的默认档,插件也一直解析进了目录,但**从没报给 DSH**,所以模型选择器只能说 `Default`:既看不出这个模型会跑哪一档,也没法固定成别的档。现在适配器把默认档报上去,宿主会把它物化进每一个没有指名档位的请求,模型栏里每个声明了档位的模型都多一个下拉,第一项写的正是**它实际会跑的档**(例如「默认(high)」)。取值优先级是**你选的 > 上游默认 > 该模型自己的中档**;选「默认」是把这个键**删掉**,而不是存一个恰好等于今天默认值的档(后者看着一样,却把选择钉死了,上游改默认时就不再跟随)。保存的档位若已被目录撤回,会被忽略而不是发出去——发出去会被宿主以 `UNSUPPORTED_REASONING_EFFORT` 拒掉。不选任何东西时行为与之前完全一致。
56
57
  - **积分临近到期会提醒** —— 剩余额度只回答「还有多少」,回答不了「**哪一笔先到期**」;提醒条按到期紧迫程度逐级加重(**7 天 → 24 小时 → 已过期**),写明金额、套餐数和最早到期时间,避免只盯总额时悄悄丢掉最先到期的额度。国内版在用量页与输入框积分面板两处都显示(国际版是订阅制,没有逐包到期数据)。
57
58
  - **一键测试启用模型** —— 给**已启用**的每个模型发一条最小消息(一句 `ping`、不带工具),逐个告诉你能不能真的应答:**可用** / **需要更高档位**(Trae `1005`,你的套餐不覆盖)/ **上游不提供**(`4001`,SOLO 通道没有这个模型)/ **被限流**(`4011`,Trae 限了这条通道)/ **登录凭据被拒** / **本次未测出**。最后一项是一等公民:超时、断连、代理、5xx **一律不算「不可用」**——只有上游明确的拒绝才算负面结论。**这是插件里唯一消耗额度的操作**(签到改变的是账号状态,不消耗),只在你点击时运行,结果只留在当前会话里不写入设置。
58
59
  > `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011」两句话可以同时成立。被限流的模型**等你一会儿再试**即可,不要删。
59
60
  - **一键测试所有模型** —— 同一套探测,作用于当前 region 的**全部可见模型**(不限于已启用的)。想知道「没启用的那些到底还能不能用」时用它——比如套餐降级后,或 Trae 调整了模型阵容之后。
60
61
  - **每日签到领取(逐账号)** —— 国内版**每个账号行各有一个签到按钮**,当日奖励直接写在按钮上(`签到 +150`);该账号当天已领显示「已领取」并禁用,**本机今日名额已被别的账号用掉**时显示「本机已签」并禁用——如实区分「这个账号领了」和「这台设备领过了」,绝不把后者说成「已领取」。这是插件里**唯一会改变账号状态**的操作,由你主动点击触发;一次点击只针对该行账号、只发一次请求。Trae 的签到按「**每台设备每天一次**」计(换账号不会重置当天名额),所以逐账号签到始终是主路径。账号池里另有一个「全部签到」,它**不是**把同一件事盲目重复:一旦上游回 `9095`(本机今日已领),整批立刻停止并把剩余账号如实标成「本机已签」,产出的是真相而不是一排假失败。国际版没有签到活动,因此不显示该按钮。
61
- - **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,5 分钟自动刷新。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择),并把手动刷新按钮放在浮层里。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
62
+ - **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,5 分钟自动刷新。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择)。浮层右上角是**「刷新」和「设置」两个控件**(不同造型,因为它们不是同一类动作:设置是导航、带品牌色底;刷新是就地重跑一批、悬停前不着色)。按「刷新」时图标会转,直到这批探测结束——它是串行跑的,最慢的账号决定什么时候结束。**「刷新」**把账号池里勾选的成员逐个问一遍「能不能应答」(走账号池那条 `action=test` 路由,与账号池分栏里的测试是同一个动作),结果直接报在浮层底部:`账号池已测 · 2 of 2 可应答`。**这个按钮有两件事标签说不出来,都写在悬停提示与结果行里**:它测的是**账号池里勾选的成员**,不是你眼前这张表里的每个账号;而且它是**插件里唯一消耗额度**的按钮(每个成员一条最小 `ping`)——一个光秃秃的「刷新」读起来是免费的,所以工具提示必须留着看。**「设置」**一点就跳到本插件自己的设置页,浮层同时关闭——「这数字去哪改」从此不必先关浮层、再去插件页里找这一行。积分本身不需要刷新按钮:**打开浮层、切换账号、每 5 分钟**都会重新读一次,所以那个按钮被换掉而不是并排加进来。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
62
63
 
63
- <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="536" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
64
+ <img src="docs/assets/dsh-connect-trae-composer-credits.png" width="560" alt="Trae credits panel: a table of accounts with their general balance, and the composer readout below" />
64
65
 
65
66
  **读数显示在哪里可以选**(学自同族 `dsh-connect-workbuddy` issue #62)。默认在**输入框工具栏**(即上面说的位置);如果那里被别的插件占了、读数不显示,改成**输入框下方**——那是「轮次 / 步数 / token」统计所在的那一行,读数是**追加**在官方统计之后,不会顶掉它们。位置**按面板分别设置**(三个面板各存各的),在卡片「设置」区紧挨「在输入框工具栏显示积分读数」那个勾选框。之所以要有第二个位置:输入框工具栏是**共享的 list 插槽**,外壳自己的控件和其他插件都注册在那里,而共享插槽**无法保证每个占用者都可见**——同族的报告者装了第三方插件后读数就整个消失了(本插件早年放在侧边栏底部时也被挤成过一个「Tr」的残片)。
66
67
 
68
+ - **可选:把 Trae 放进侧边栏**(学自同族 `dsh-connect-workbuddy` 的 PR #188,同样**默认关闭**)。在卡片「设置」区勾上「在侧边栏显示 Trae」,DSH 侧边栏就多一个 Trae 行,点开是**这张卡片的整页版**:同样的地区页签、同样的账号 / 模型 / 诊断分栏,只是没有卡片边框,顶部是标题、说明和「返回会话」。**默认关闭的理由**:侧边栏的全局行是宿主自己的页面,这一行占的是**所有已安装插件共用**的座位,而「插件」页本来就能看到同样的内容——一行没人要求的导航会改变一个插件并不拥有的界面。**点一下即生效**,不用保存:开关写入 `sidebarPanel` 后,注册与注销在同一秒发生(不是把组件渲染成 null——那会留下一个空白却仍可点的按钮);页面开着时关掉它,中心栏会自己回到会话。整页的「返回会话」走宿主的 `layout.selectPanel(null)`,宿主没这个服务时**不画**这个按钮,而不是画一个点不动的。
69
+ <img src="docs/assets/dsh-connect-trae-sidebar-page.png" width="720" alt="Trae 侧边栏整页版:顶部是返回会话、标题与说明,下面是同一张卡片的地区页签和账号栏" />
70
+
71
+
67
72
  - **安全 loopback shim** —— 每区域一个随机端口 + 进程内随机 secret,真实 Trae token 不交给 pi-ai。
68
73
 
69
74
  ## 工作原理