dsh-connect-trae 4.16.0 → 4.17.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,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.17.0 (2026-11-09)
4
+
5
+ ### 自动刷新改为可选项:整个插件一个节奏,默认 5 分钟
6
+
7
+ 改动前,插件对**同两条只读上游路由**用了**两个硬编码节奏**:卡片每 **60 秒**重读一次用量
8
+ 文档,输入框读数每 **5 分钟**一次。取哪个取决于你正好在看哪个界面,两个都改不了。而每次
9
+ 用量读取是**两次上游请求**,所以「正在看卡片」这件事本身就悄悄把后台流量翻了几倍。
10
+
11
+ 现在是一个字段 `refreshIntervalMinutes`,一处 UI(「设置 → Trae → **能力**」里的
12
+ 「自动刷新」),可选 **1 / 5 / 10 / 30 / 60 分钟**,**默认 5**。默认值选 5 是因为它正是
13
+ 输入框读数原本的节奏——升级不改变任何人的观感,而卡片的 60 秒被拉到同一个值上。
14
+
15
+ **为什么是「整个插件一个值」而不是每面板一个。** 它配的是插件的后台噪声,不是某个区域的
16
+ 数据:两处打的是同两条路由、同样的代价。按面板存会让同一块屏幕出现两个节奏,而这正是这次
17
+ 要消灭的东西。
18
+
19
+ **写读链路,以及为什么不是简单的 `setInterval` 换个数。**
20
+
21
+ | 环节 | 做法 | 不做会怎样 |
22
+ | --- | --- | --- |
23
+ | schema | `z.union([1,5,10,30,60])`,默认 5 | 手改文件写个 `7` 会被接受,得到一个没人选过的节奏 |
24
+ | 读取 | `refreshIntervalMinutesOf()`,非集合内一律回落默认 | 认不出的值让刷新**静默停止**,症状只是数字悄悄变旧 |
25
+ | 写入 | `writeSettingsField` 读回校验,比的是**解析后的分钟数** | 写入被接受但没落地时,选择器显示新值而定时器还是旧的 |
26
+ | 定时器 | 间隔进 effect 依赖,cleanup 先清旧定时器、中止在途请求 | 改一次间隔就多一个循环并跑,上游请求翻倍且界面看不出来 |
27
+ | 共享 | 卡片与输入框读数读**同一个** reader | 两个界面漂回两个节奏,正是本版要修的缺陷 |
28
+
29
+ 选项列表本身是**共享常量** `TRAE_REFRESH_INTERVAL_MINUTES`,schema 与选择器都从它渲染:
30
+ 两处各写一份字面量,就会出现「选择器给得出、写入拒绝」的选项。
31
+
32
+ **输入框读数补了一次设置订阅。** 它挂在 gate 之下,而 gate 订阅的是自己的两个开关——
33
+ 子组件不会因为父组件订阅了而重渲染。没有这次订阅,改了间隔要等到下一次无关渲染才生效。
34
+
35
+ ### Tests
36
+
37
+ - 新增 `tests/refresh-interval.spec.tsx`(16 例):读取器的默认/全集/回落/`{get()}` 实时引用;
38
+ schema 接受五个值、拒绝 `7` 与 `0`;选择器在「能力」分栏、显示存储值、只写一个字段、
39
+ 只读时拒写;**卡片按存储间隔读**(10 分钟时 1 分钟处零请求、10 分钟处恰好一次);
40
+ **改间隔会替换定时器**(旧 60 分钟 → 新 1 分钟,一分钟处恰好一次读);**输入框读数用同一
41
+ 字段**(1 分钟处就有读,不等旧的 5 分钟常量)。
42
+ - **变异验证 4 次全部被抓**:卡片定时器改回固定 60s → 1 例失败;输入框读数改回自己的常量
43
+ → 1 例失败;读取器接受任意数字 → 1 例失败;(另有一次改动由既有 `card-styles` 的
44
+ 「每个渲染出来的类都要有规则」守卫抓出,补了 `.dsm-trae-refresh-interval` 的样式)。
45
+ - 全仓 1446 → **1462**。
46
+
47
+ ### 顺带
48
+
49
+ `styles.ts` 的注释里曾写了一个反引号包住的类名——那个模板字符串会当场截断,整份样式表变成
50
+ 语法碎片,而 typecheck 与 build 都不报错。`card-styles` 的「顶层语句只能是变量声明」守卫
51
+ 当场抓到,改回普通文字。这个坑本仓库文档里已经记过一次(workbuddy 的 busy veil)。
52
+
3
53
  ## 4.16.0 (2026-11-08)
4
54
 
5
55
  ### 模型标签槽:按宿主的声明注册,为 `keyed` 做好准备
package/README.en.md CHANGED
@@ -61,11 +61,12 @@ In the account pool, open **More settings** and choose **Claude style** or **Def
61
61
  > `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.
62
62
  - **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.
63
63
  - **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.
64
- - **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** (as of 4.9.0 the Settings button targets the settings panel; an interim revision removed it on the belief that this was impossible, which was wrong — the host publishes no id-targeted *service*, but the settings shell pins its store seat to the registration so `slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` is a real route, and the host's own tests drive the dialog exactly that way). 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. To change anything, open **Settings → Trae** — the plugin's only configuration surface. 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).
64
+ - **Composer readout** — the composer tool row (between "Expert" and the model selector) shows a clickable readout, refreshing every 5 minutes by default (the interval is configurable — see the next bullet). **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** (as of 4.9.0 the Settings button targets the settings panel; an interim revision removed it on the belief that this was impossible, which was wrong — the host publishes no id-targeted *service*, but the settings shell pins its store seat to the registration so `slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` is a real route, and the host's own tests drive the dialog exactly that way). 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. To change anything, open **Settings → Trae** — the plugin's only configuration surface. The balance needs no refresh button of its own — it re-reads when the panel opens, on every account switch, and on every refresh interval, 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).
65
65
 
66
66
  <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" />
67
67
 
68
68
  **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).
69
+ - **The automatic-refresh interval is configurable** — "refresh every" under **Settings → Trae → Capabilities** decides how often the card and the composer readout re-read usage by themselves: **1 / 5 / 10 / 30 / 60 minutes**, **5 by default**. **One value for the whole plugin**, not one per panel: both surfaces hit the same two read-only routes, so a per-panel copy would make one screen refresh at two cadences. A change takes effect **immediately** — the old timer is cleared and its in-flight read aborted before a new one starts at the new period, so a change cannot leave two loops running. **A shorter interval means more requests**: each refresh is two read-only upstream requests and nothing here spends credits. Before this setting the card was hardcoded to 60 seconds while the composer readout was hardcoded to five minutes — two cadences for the same data, decided by which surface you happened to be looking at. Both now read this one field.
69
70
 
70
71
  - **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 settings 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**.
71
72
  <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" />
package/README.md CHANGED
@@ -66,11 +66,12 @@
66
66
  > `4011` 是**限流**不是「模型不可用」(issue #33 实测):它额度耗尽走的是另一个码 `4008`,所以「额度还有很多却报 4011」两句话可以同时成立。被限流的模型**等你一会儿再试**即可,不要删。
67
67
  - **一键测试所有模型** —— 同一套探测,作用于当前 region 的**全部可见模型**(不限于已启用的)。想知道「没启用的那些到底还能不能用」时用它——比如套餐降级后,或 Trae 调整了模型阵容之后。
68
68
  - **每日签到领取(逐账号)** —— 国内版**每个账号行各有一个签到按钮**,当日奖励直接写在按钮上(`签到 +150`);该账号当天已领显示「已领取」并禁用,**本机今日名额已被别的账号用掉**时显示「本机已签」并禁用——如实区分「这个账号领了」和「这台设备领过了」,绝不把后者说成「已领取」。这是插件里**唯一会改变账号状态**的操作,由你主动点击触发;一次点击只针对该行账号、只发一次请求。Trae 的签到按「**每台设备每天一次**」计(换账号不会重置当天名额),所以逐账号签到始终是主路径。账号池里另有一个「全部签到」,它**不是**把同一件事盲目重复:一旦上游回 `9095`(本机今日已领),整批立刻停止并把剩余账号如实标成「本机已签」,产出的是真相而不是一排假失败。国际版没有签到活动,因此不显示该按钮。
69
- - **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,5 分钟自动刷新。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择)。浮层右上角是**「刷新」与「设置」两个控件**(4.9.0 起「设置」改指设置面板;中途曾以为做不到而删掉,是错的——宿主确实没有 id 定向的服务,但设置面板把自己的 store 座位钉在注册声明上,`slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` 是真实可用的路径,宿主自己的测试就是这么驱动的)。按「刷新」时图标会转,直到这批探测结束——它是串行跑的,最慢的账号决定什么时候结束。**「刷新」**把账号池里勾选的成员逐个问一遍「能不能应答」(走账号池那条 `action=test` 路由,与账号池分栏里的测试是同一个动作),结果直接报在浮层底部:`账号池已测 · 2 of 2 可应答`。**这个按钮有两件事标签说不出来,都写在悬停提示与结果行里**:它测的是**账号池里勾选的成员**,不是你眼前这张表里的每个账号;而且它是**插件里唯一消耗额度**的按钮(每个成员一条最小 `ping`)——一个光秃秃的「刷新」读起来是免费的,所以工具提示必须留着看。要改配置就去「设置 → Trae」:那是本插件唯一一处配置界面。积分本身不需要刷新按钮:**打开浮层、切换账号、每 5 分钟**都会重新读一次,所以那个按钮被换掉而不是并排加进来。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
69
+ - **输入框读数** —— 在输入框工具栏(「专家」与模型选择器之间)显示一块可点击读数,默认每 5 分钟自动刷新(间隔可调,见下一条)。**两个区域都显示,但显示的东西不同,这是数据决定的**:国内版是积分制,显示「Trae CN · <通用积分>」;国际版是订阅制,它的用量文档里根本没有积分字段,所以显示「Trae 国际版 · <套餐>」(如 `Pro` / `Free`)。**整块可点击**,弹出浮层显示一张**表格**:本区域**每个账号一行**,国内版列**通用积分**、国际版列**套餐**,**点行即可切换账号**(写入与卡片相同的 `accounts` 字段、合并时不丢掉另一个区域的选择)。浮层右上角是**「刷新」与「设置」两个控件**(4.9.0 起「设置」改指设置面板;中途曾以为做不到而删掉,是错的——宿主确实没有 id 定向的服务,但设置面板把自己的 store 座位钉在注册声明上,`slots.entries('sidebar.settings')[0].store.create().actions.openSection(id)` 是真实可用的路径,宿主自己的测试就是这么驱动的)。按「刷新」时图标会转,直到这批探测结束——它是串行跑的,最慢的账号决定什么时候结束。**「刷新」**把账号池里勾选的成员逐个问一遍「能不能应答」(走账号池那条 `action=test` 路由,与账号池分栏里的测试是同一个动作),结果直接报在浮层底部:`账号池已测 · 2 of 2 可应答`。**这个按钮有两件事标签说不出来,都写在悬停提示与结果行里**:它测的是**账号池里勾选的成员**,不是你眼前这张表里的每个账号;而且它是**插件里唯一消耗额度**的按钮(每个成员一条最小 `ping`)——一个光秃秃的「刷新」读起来是免费的,所以工具提示必须留着看。要改配置就去「设置 → Trae」:那是本插件唯一一处配置界面。积分本身不需要刷新按钮:**打开浮层、切换账号、每个刷新间隔**都会重新读一次,所以那个按钮被换掉而不是并排加进来。**只在当前会话选中的模型属于本插件时出现**——选其它供应商的模型时整块不渲染。这样两个连接器插件不会争同一个位置(侧边栏底部是共享的根级行,输入框工具栏是按会话、按供应商的)。
70
70
 
71
71
  <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" />
72
72
 
73
73
  **读数显示在哪里可以选**(学自同族 `dsh-connect-workbuddy` issue #62)。默认在**输入框工具栏**(即上面说的位置);如果那里被别的插件占了、读数不显示,改成**输入框下方**——那是「轮次 / 步数 / token」统计所在的那一行,读数是**追加**在官方统计之后,不会顶掉它们。位置**按面板分别设置**(三个面板各存各的),在卡片「设置」区紧挨「在输入框工具栏显示积分读数」那个勾选框。之所以要有第二个位置:输入框工具栏是**共享的 list 插槽**,外壳自己的控件和其他插件都注册在那里,而共享插槽**无法保证每个占用者都可见**——同族的报告者装了第三方插件后读数就整个消失了(本插件早年放在侧边栏底部时也被挤成过一个「Tr」的残片)。
74
+ - **自动刷新间隔可调** —— 「设置 → Trae → 能力」里的「自动刷新」决定卡片与输入框读数各自多久自己重新读一次用量,可选 **1 / 5 / 10 / 30 / 60 分钟**,默认 **5 分钟**。**整个插件共用一个值**(不是每个面板一个):两处打的是同两条只读路由,分开设置只会让同一块屏幕出现两个节奏。改动**即时生效**——旧定时器先被清掉、在途请求被中止,再按新间隔重建,所以不会留下两个循环并跑。**间隔越短请求越频繁**:每次刷新是两次只读上游请求,这里不消耗积分。这个设置之前,卡片硬编码 60 秒、输入框读数硬编码 5 分钟——同一份数据两个节奏,取决于你正好在看哪个界面;现在统一到这一个字段。
74
75
 
75
76
  - **可选:把 Trae 放进侧边栏**(学自同族 `dsh-connect-workbuddy` 的 PR #188,同样**默认关闭**)。在卡片「设置」区勾上「在侧边栏显示 Trae」,DSH 侧边栏就多一个 Trae 行,点开是**这张卡片的整页版**:同样的地区页签、同样的账号 / 模型 / 诊断分栏,只是没有卡片边框,顶部是标题、说明和「返回会话」。**默认关闭的理由**:侧边栏的全局行是宿主自己的页面,这一行占的是**所有已安装插件共用**的座位,而「设置 → Trae」本来就能看到同样的内容——一行没人要求的导航会改变一个插件并不拥有的界面。**点一下即生效**,不用保存:开关写入 `sidebarPanel` 后,注册与注销在同一秒发生(不是把组件渲染成 null——那会留下一个空白却仍可点的按钮);页面开着时关掉它,中心栏会自己回到会话。整页的「返回会话」走宿主的 `layout.selectPanel(null)`,宿主没这个服务时**不画**这个按钮,而不是画一个点不动的。
76
77
  <img src="docs/assets/dsh-connect-trae-sidebar-page.png" width="720" alt="Trae 侧边栏整页版:顶部是返回会话、标题与说明,下面是同一张卡片的地区页签和账号栏" />
package/lib/client.js CHANGED
@@ -574,6 +574,45 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
574
574
  });
575
575
  }
576
576
  /**
577
+ * The automatic-refresh cadences this plugin offers, in MINUTES.
578
+ *
579
+ * The list is the setting's whole value domain: the schema validates against
580
+ * exactly these, so a hand-edited `7` is refused at write time instead of
581
+ * silently producing a cadence nobody chose. `1` is the floor because every
582
+ * usage read costs two upstream requests and a tighter loop would turn a
583
+ * background refresh into traffic the user never asked for.
584
+ */
585
+ const TRAE_REFRESH_INTERVAL_MINUTES = [
586
+ 1,
587
+ 5,
588
+ 10,
589
+ 30,
590
+ 60
591
+ ];
592
+ const REFRESH_INTERVAL_SET = new Set(TRAE_REFRESH_INTERVAL_MINUTES);
593
+ /**
594
+ * How often the plugin refreshes on its own, in minutes.
595
+ *
596
+ * Anything that is not one of {@link TRAE_REFRESH_INTERVAL_MINUTES} reads as
597
+ * the default — absent (a profile written before the setting existed), a
598
+ * hand-edited `7`, a string `'5'`, or a live reference that resolves to none of
599
+ * those. Falling back rather than refusing to refresh is deliberate: a readout
600
+ * that stops updating because of an unrecognized value is the failure this
601
+ * setting must never introduce, and the schema already refuses such a value on
602
+ * the write path.
603
+ *
604
+ * `value` is the whole settings section (this field is plugin-wide, not
605
+ * per-region — see `Config.refreshIntervalMinutes`).
606
+ */
607
+ function refreshIntervalMinutesOf(value) {
608
+ const stored = unwrapVolatileDeep(value)?.refreshIntervalMinutes;
609
+ return typeof stored === "number" && REFRESH_INTERVAL_SET.has(stored) ? stored : 5;
610
+ }
611
+ /** The same cadence as a millisecond period, for `setInterval`. */
612
+ function refreshIntervalMsOf(value) {
613
+ return refreshIntervalMinutesOf(value) * 6e4;
614
+ }
615
+ /**
577
616
  * One panel's saved pool PREFERENCES, as the card's settings block edits them.
578
617
  *
579
618
  * Read from the settings document rather than from the usage answer for the same
@@ -1403,6 +1442,19 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
1403
1442
  question and sits beside the words that ask it. */
1404
1443
  .dsm-trae-credit-position{flex:none;max-width:none;margin-top:0;padding:3px 8px;font-size:12px;line-height:16px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:7px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-primary,#e6e6e6);cursor:pointer}
1405
1444
  .dsm-trae-credit-position:disabled{cursor:default}
1445
+ /* The automatic-refresh picker, a sibling of the seat picker above and styled
1446
+ to match it — the two answer the same kind of question ("how this readout
1447
+ behaves") and sit on consecutive lines, so a second visual idiom would read
1448
+ as an unrelated control.
1449
+
1450
+ Its OWN class, deliberately NOT the seat picker's: a test that selects the
1451
+ seat picker by class must not start finding the cadence picker the moment
1452
+ this row is added, which is exactly the failure the seat picker's own comment
1453
+ records. The row borrows the checkbox column's padding for the same reason
1454
+ its sibling does. */
1455
+ .dsm-trae-refresh-interval-pref{align-items:flex-start;padding-left:25px}
1456
+ .dsm-trae-refresh-interval{flex:none;max-width:none;margin-top:0;padding:3px 8px;font-size:12px;line-height:16px;border:1px solid var(--dsw-alias-border-l2,#3a3d45);border-radius:7px;background:var(--dsw-alias-bg-layer-3,#2a2c33);color:var(--dsw-alias-label-primary,#e6e6e6);cursor:pointer}
1457
+ .dsm-trae-refresh-interval:disabled{cursor:default}
1406
1458
  /* The card's checkboxes are the platform's own, styled only with
1407
1459
  accent-color 鈥?the SAME idiom .dsm-trae-mainui-switch-label above already
1408
1460
  uses for the composer-line switch, and the one the sibling
@@ -4409,7 +4461,7 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
4409
4461
  * is precisely why this section has no 「保存」 of its own — the model catalog,
4410
4462
  * which does, lives in its own section.
4411
4463
  */
4412
- function TraeAccountPanel({ t, settingsScope, panel, status, statusLabel, credentialRegionIsAi, regionOn, showPointsInMainUi, onToggleShowPoints, creditPosition, onCreditPositionChange, sidebarPanelOn, sidebarPanelBusy, onToggleSidebarPanel, savingPreferences, creditsLabelId, creditsHintId, rows, busy, rescanNote, onRescan, switchingAccount, onSwitchAccount, onClaim, onTransferChanged, poolDetails, poolClaims, models = [], deviceClaimed = false, onPoolChanged = () => {}, onPoolRefresh = () => {}, creditsUnavailable, creditsExpiry, alternatives, onSwitchToAlternative, section = "all" }) {
4464
+ function TraeAccountPanel({ t, settingsScope, panel, status, statusLabel, credentialRegionIsAi, regionOn, showPointsInMainUi, onToggleShowPoints, creditPosition, onCreditPositionChange, refreshIntervalMinutes, onRefreshIntervalChange, sidebarPanelOn, sidebarPanelBusy, onToggleSidebarPanel, savingPreferences, creditsLabelId, creditsHintId, rows, busy, rescanNote, onRescan, switchingAccount, onSwitchAccount, onClaim, onTransferChanged, poolDetails, poolClaims, models = [], deviceClaimed = false, onPoolChanged = () => {}, onPoolRefresh = () => {}, creditsUnavailable, creditsExpiry, alternatives, onSwitchToAlternative, section = "all" }) {
4413
4465
  const writable = settingsScope?.getSnapshot().writable === true;
4414
4466
  const signedIn = status.status === "signed-in";
4415
4467
  const accounts = signedIn ? status.accounts : [];
@@ -4474,6 +4526,37 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
4474
4526
  })]
4475
4527
  })
4476
4528
  }),
4529
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
4530
+ className: "dsm-trae-pref dsm-trae-refresh-interval-pref",
4531
+ title: t("row.refreshIntervalHint"),
4532
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4533
+ className: "dsm-trae-pref-copy",
4534
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4535
+ className: "dsm-trae-pref-lead",
4536
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4537
+ className: "dsm-trae-pref-label",
4538
+ children: t("row.refreshIntervalLabel")
4539
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
4540
+ className: "dsm-trae-refresh-interval",
4541
+ value: String(refreshIntervalMinutes),
4542
+ disabled: !writable || savingPreferences,
4543
+ "aria-label": t("row.refreshIntervalAria"),
4544
+ onChange: (event) => {
4545
+ const picked = Number(event.currentTarget.value);
4546
+ const match = TRAE_REFRESH_INTERVAL_MINUTES.find((each) => each === picked);
4547
+ if (match !== void 0) onRefreshIntervalChange(match);
4548
+ },
4549
+ children: TRAE_REFRESH_INTERVAL_MINUTES.map((minutes) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4550
+ value: String(minutes),
4551
+ children: t("row.refreshIntervalOption", { minutes: String(minutes) })
4552
+ }, minutes))
4553
+ })]
4554
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4555
+ className: "dsm-trae-pref-hint",
4556
+ children: t("row.refreshIntervalHint")
4557
+ })]
4558
+ })
4559
+ }),
4477
4560
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4478
4561
  className: "dsm-trae-pref",
4479
4562
  title: t("row.sidebarPanelHint"),
@@ -4881,7 +4964,16 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
4881
4964
  }
4882
4965
  return `HTTP ${status}`;
4883
4966
  }
4884
- const POLL_INTERVAL_MS = 6e4;
4967
+ /**
4968
+ * The card's own poll period is NOT a constant: the cadence is the user's
4969
+ * (`Config.refreshIntervalMinutes`, default 5 minutes), read through
4970
+ * {@link refreshIntervalMsOf} so the card and the composer readout cannot drift
4971
+ * apart. It used to be a fixed 60_000 here while the composer readout polled
4972
+ * every five minutes — two cadences for the same two upstream routes, decided
4973
+ * by which surface you happened to be looking at. The floor of the offered
4974
+ * values is one minute; every read is two read-only upstream calls, which is
4975
+ * why the setting does not go below that.
4976
+ */
4885
4977
  const TRAE_GITHUB_URL = "https://github.com/dingminhua/dsh-connect-trae";
4886
4978
  /**
4887
4979
  * Write one settings field and confirm the value actually landed.
@@ -5248,7 +5340,20 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
5248
5340
  * local state: a write made elsewhere (the DSH settings form, another window)
5249
5341
  * then shows up here instead of being shadowed by a stale copy.
5250
5342
  */
5251
- const committedSettings = { showPointsInMainUi: unwrapVolatileDeep(settingsScope?.getSnapshot().value)?.showPointsInMainUi === true };
5343
+ const committedSettings = {
5344
+ showPointsInMainUi: unwrapVolatileDeep(settingsScope?.getSnapshot().value)?.showPointsInMainUi === true,
5345
+ /**
5346
+ * The automatic-refresh cadence (`Config.refreshIntervalMinutes`).
5347
+ *
5348
+ * Read through the SHARED reader rather than off the document here: the
5349
+ * composer readout reads the same field through the same function, and the
5350
+ * two surfaces disagreeing about the cadence is exactly what a second
5351
+ * interpretation would produce. It also owns the fallback for a document
5352
+ * that never went through the schema (a hand-edited file), so the card
5353
+ * cannot end up on a cadence the write path would have refused.
5354
+ */
5355
+ refreshIntervalMinutes: refreshIntervalMinutesOf(settingsScope?.getSnapshot().value)
5356
+ };
5252
5357
  /**
5253
5358
  * Ids for the block's label/hint wiring. `useId`, not constants: DSH can mount
5254
5359
  * this card twice at once (the settings ROW and the expanded PAGE), and two
@@ -5260,11 +5365,25 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
5260
5365
  * over it. Reading BOTH sources here is what lets the write above stay simple —
5261
5366
  * it never has to write into component state that a Host refresh would fight.
5262
5367
  */
5263
- const settingsEdits = { showPointsInMainUi: optimisticSettings.current.showPointsInMainUi ?? committedSettings.showPointsInMainUi };
5368
+ const settingsEdits = {
5369
+ showPointsInMainUi: optimisticSettings.current.showPointsInMainUi ?? committedSettings.showPointsInMainUi,
5370
+ refreshIntervalMinutes: optimisticSettings.current.refreshIntervalMinutes ?? committedSettings.refreshIntervalMinutes
5371
+ };
5264
5372
  const settingsIds = (0, react.useId)();
5265
5373
  const creditsLabelId = `${settingsIds}-credits`;
5266
5374
  const creditsHintId = `${settingsIds}-credits-hint`;
5267
5375
  /**
5376
+ * The poll period, in milliseconds, as the CARD uses it.
5377
+ *
5378
+ * Derived from the committed-plus-optimistic value above rather than from the
5379
+ * document directly, so a pick shows its effect immediately: the timer is
5380
+ * rebuilt from this on the next render (see the poll effect's dependency
5381
+ * list). Reading it here also keeps the number and the picker in one place —
5382
+ * a picker on one cadence and a timer on another is precisely the class of
5383
+ * bug this setting replaced.
5384
+ */
5385
+ const refreshIntervalMs = settingsEdits.refreshIntervalMinutes * 6e4;
5386
+ /**
5268
5387
  * Write ONE preference switch, on the click that flipped it.
5269
5388
  *
5270
5389
  * This replaced a draft + 「保存」/「放弃」 pair, which is what the sibling
@@ -5302,6 +5421,47 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
5302
5421
  }
5303
5422
  };
5304
5423
  /**
5424
+ * Change how often the plugin refreshes on its own
5425
+ * (`Config.refreshIntervalMinutes`).
5426
+ *
5427
+ * Same shape as {@link writePreference}, and for the same reason: this is a
5428
+ * standing preference, so it commits on the pick and is read-back verified —
5429
+ * a picker that merely LOOKS changed while the timer keeps its old period is
5430
+ * the failure mode worth guarding against here. The read-back compares
5431
+ * through `refreshIntervalMinutesOf`, so the value has to land as one of the
5432
+ * offered cadences rather than merely being present.
5433
+ *
5434
+ * The write alone changes nothing on screen until the settings snapshot
5435
+ * notifies, which the poll effect below is keyed on: the interval is rebuilt
5436
+ * from the new value on the next render, so the old timer is cleared and a
5437
+ * fresh one started. That is deliberate — a cadence change must take effect
5438
+ * without a reload, and it must not leave the previous interval running.
5439
+ */
5440
+ const writeRefreshInterval = async (value) => {
5441
+ if (settingsScope === void 0 || settingsScope.getSnapshot().writable !== true) return;
5442
+ setSavingSettings(true);
5443
+ setWriteError(void 0);
5444
+ optimisticSettings.current = {
5445
+ ...optimisticSettings.current,
5446
+ refreshIntervalMinutes: value
5447
+ };
5448
+ setSettingsRevision((revision) => revision + 1);
5449
+ try {
5450
+ await writeSettingsField(settingsScope, "refreshIntervalMinutes", value, (readBack) => refreshIntervalMinutesOf(readBack) === value);
5451
+ } catch (error) {
5452
+ if (mounted.current) setWriteError(error instanceof Error ? error.message : t("row.requestFailed"));
5453
+ } finally {
5454
+ if (mounted.current) {
5455
+ optimisticSettings.current = {
5456
+ ...optimisticSettings.current,
5457
+ refreshIntervalMinutes: void 0
5458
+ };
5459
+ setSettingsRevision((revision) => revision + 1);
5460
+ }
5461
+ if (mounted.current) setSavingSettings(false);
5462
+ }
5463
+ };
5464
+ /**
5305
5465
  * Flip the OPTIONAL sidebar row on or off (`Config.sidebarPanel`).
5306
5466
  *
5307
5467
  * Two things happen on a verified write, and BOTH are required:
@@ -5563,7 +5723,7 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
5563
5723
  const controller = new AbortController();
5564
5724
  const timer = window.setInterval(() => {
5565
5725
  refreshUsage(activeRegion, controller.signal);
5566
- }, POLL_INTERVAL_MS);
5726
+ }, refreshIntervalMs);
5567
5727
  return () => {
5568
5728
  window.clearInterval(timer);
5569
5729
  controller.abort();
@@ -5573,7 +5733,8 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
5573
5733
  activeRegion,
5574
5734
  activeRegionOn,
5575
5735
  refreshUsage,
5576
- status.status
5736
+ status.status,
5737
+ refreshIntervalMs
5577
5738
  ]);
5578
5739
  const rescanAccounts = async () => {
5579
5740
  setBusy(true);
@@ -6635,6 +6796,10 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
6635
6796
  onCreditPositionChange: (position) => {
6636
6797
  changeCreditPosition(activeRegion, position);
6637
6798
  },
6799
+ refreshIntervalMinutes: settingsEdits.refreshIntervalMinutes,
6800
+ onRefreshIntervalChange: (minutes) => {
6801
+ writeRefreshInterval(minutes);
6802
+ },
6638
6803
  sidebarPanelOn,
6639
6804
  sidebarPanelBusy,
6640
6805
  onToggleSidebarPanel: (value) => {
@@ -6715,6 +6880,10 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
6715
6880
  onCreditPositionChange: (position) => {
6716
6881
  changeCreditPosition(activeRegion, position);
6717
6882
  },
6883
+ refreshIntervalMinutes: settingsEdits.refreshIntervalMinutes,
6884
+ onRefreshIntervalChange: (minutes) => {
6885
+ writeRefreshInterval(minutes);
6886
+ },
6718
6887
  sidebarPanelOn,
6719
6888
  sidebarPanelBusy,
6720
6889
  onToggleSidebarPanel: (value) => {
@@ -7644,9 +7813,17 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
7644
7813
  }
7645
7814
  }
7646
7815
  /**
7647
- * Automatic refresh period. Five minutes, matching the sidebar line: a credit
7648
- * balance moves on the scale of model calls, not seconds, and each read is two
7649
- * read-only upstream calls.
7816
+ * Automatic refresh period when nothing says otherwise.
7817
+ *
7818
+ * Five minutes, matching the sidebar line: a credit balance moves on the scale
7819
+ * of model calls, not seconds, and each read is two read-only upstream calls.
7820
+ *
7821
+ * The LIVE period is the user's `Config.refreshIntervalMinutes` (see
7822
+ * {@link refreshIntervalMsOf}), whose default is this same five minutes — so
7823
+ * this constant is now the fallback for a caller that renders the readout
7824
+ * without a settings scope (a test, or a host that serves none), not the rule.
7825
+ * Both polling surfaces read that ONE field, so the composer row and the card
7826
+ * cannot end up refreshing the same routes at different rates.
7650
7827
  */
7651
7828
  const COMPOSER_POINTS_REFRESH_INTERVAL_MS = 3e5;
7652
7829
  /**
@@ -7745,6 +7922,16 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
7745
7922
  if (t === void 0) throw new Error("Composer points readout requires its translation function");
7746
7923
  const owned = provider === void 0 ? void 0 : TRAE_COMPOSER_PROVIDERS[provider];
7747
7924
  const activeRegion = region ?? owned;
7925
+ /**
7926
+ * The poll period, from the user's own cadence setting.
7927
+ *
7928
+ * Read through the SHARED reader (the card's poll reads the same field the
7929
+ * same way), with the constant above as the fallback when this readout is
7930
+ * rendered with no settings scope at all. It re-reads on every render, so the
7931
+ * scope's notification after a write is enough to rebuild the timer — the
7932
+ * effect below lists it as a dependency for exactly that reason.
7933
+ */
7934
+ const refreshIntervalMs = settingsScope === void 0 ? COMPOSER_POINTS_REFRESH_INTERVAL_MS : refreshIntervalMsOf(settingsScope.getSnapshot().value);
7748
7935
  const [usage, setUsage] = (0, react.useState)(void 0);
7749
7936
  const [accountCredits, setAccountCredits] = (0, react.useState)(void 0);
7750
7937
  /**
@@ -7816,6 +8003,23 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
7816
8003
  mounted.current = false;
7817
8004
  };
7818
8005
  }, []);
8006
+ /**
8007
+ * Re-render when the settings document changes, so a cadence picked in the
8008
+ * card reaches THIS readout without a reload.
8009
+ *
8010
+ * The scope is a stable injected reference, so the subscription is set up once
8011
+ * per scope. `settingsRevision` is not read anywhere — bumping it is the whole
8012
+ * point, since `refreshIntervalMs` above is recomputed on every render.
8013
+ *
8014
+ * The gate above (`ComposerPointsGate`) already subscribes for its own two
8015
+ * switches, but a child does not re-render because its PARENT subscribed: this
8016
+ * component is mounted through that gate, so without its own subscription a
8017
+ * cadence change would only land on the next unrelated render.
8018
+ */
8019
+ const [, setSettingsRevision] = (0, react.useState)(0);
8020
+ (0, react.useEffect)(() => settingsScope?.subscribe(() => {
8021
+ setSettingsRevision((revision) => revision + 1);
8022
+ }), [settingsScope]);
7819
8023
  const position = useAnchoredPosition({
7820
8024
  open: open && activeRegion !== void 0,
7821
8025
  anchorRef: rootRef,
@@ -7877,14 +8081,18 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
7877
8081
  if (cancelled) return;
7878
8082
  timer = window.setInterval(() => {
7879
8083
  fetchUsage(controller.signal);
7880
- }, COMPOSER_POINTS_REFRESH_INTERVAL_MS);
8084
+ }, refreshIntervalMs);
7881
8085
  });
7882
8086
  return () => {
7883
8087
  cancelled = true;
7884
8088
  if (timer !== void 0) window.clearInterval(timer);
7885
8089
  controller.abort();
7886
8090
  };
7887
- }, [owned, fetchUsage]);
8091
+ }, [
8092
+ owned,
8093
+ fetchUsage,
8094
+ refreshIntervalMs
8095
+ ]);
7888
8096
  /**
7889
8097
  * Every account's PLAN, one status read per account — ai only.
7890
8098
  *
@@ -8844,6 +9052,10 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
8844
9052
  "row.creditPositionStats": "below the input box",
8845
9053
  "row.creditPositionAria": "Credit readout position for {region}",
8846
9054
  "row.creditPositionHint": "The default position is on the model picker row. If another connector occupies the same position, the readout loaded later moves left automatically. Choose below the input box to show it beside the round, step and token statistics.",
9055
+ "row.refreshIntervalLabel": "refresh every",
9056
+ "row.refreshIntervalOption": "{minutes} min",
9057
+ "row.refreshIntervalAria": "Automatic refresh interval",
9058
+ "row.refreshIntervalHint": "How often the card and the composer readout re-read usage by themselves, every 5 minutes by default. Each refresh is two read-only upstream requests, so a shorter interval means more traffic; nothing here spends credits. A change takes effect immediately, and turning a panel off stops its reads.",
8847
9059
  "composer.switchTo": "Switch to {account}",
8848
9060
  "composer.noAccounts": "No account in this region",
8849
9061
  "composer.panelTitle": "Trae credits",
@@ -9263,6 +9475,10 @@ ${u} ${p}-pool .sw input:checked+.sw-track::after{transform:translateX(16px)}
9263
9475
  "row.creditPositionStats": "输入框下方",
9264
9476
  "row.creditPositionAria": "{region} 的积分显示位置",
9265
9477
  "row.creditPositionHint": "默认与模型选择同一行显示;遇到其他连接器占用排序位置时,后加载的读数自动向左排列。也可选择「输入框下方」,与轮次、步数和 token 统计同一行显示。",
9478
+ "row.refreshIntervalLabel": "自动刷新",
9479
+ "row.refreshIntervalOption": "{minutes} 分钟",
9480
+ "row.refreshIntervalAria": "自动刷新间隔",
9481
+ "row.refreshIntervalHint": "卡片与输入框读数各自多久重新读取一次用量,默认 5 分钟。每次刷新是两次只读的上游请求,间隔越短请求越频繁;这里不消耗积分。改动即时生效;面板关闭后该面板不再自动读取。",
9266
9482
  "composer.switchTo": "切换到 {account}",
9267
9483
  "composer.noAccounts": "该区域暂无账号",
9268
9484
  "composer.panelTitle": "Trae 积分",
package/lib/index.d.ts CHANGED
@@ -1331,6 +1331,18 @@ declare function regionCreditPositionOf(value: unknown, panel: TraePanel): TraeC
1331
1331
  * 'regions', ...)` needs.
1332
1332
  */
1333
1333
  declare function nextRegionCreditPosition(value: unknown, panel: TraePanel, position: TraeCreditPosition): Record<string, unknown>;
1334
+ /**
1335
+ * The automatic-refresh cadences this plugin offers, in MINUTES.
1336
+ *
1337
+ * The list is the setting's whole value domain: the schema validates against
1338
+ * exactly these, so a hand-edited `7` is refused at write time instead of
1339
+ * silently producing a cadence nobody chose. `1` is the floor because every
1340
+ * usage read costs two upstream requests and a tighter loop would turn a
1341
+ * background refresh into traffic the user never asked for.
1342
+ */
1343
+ declare const TRAE_REFRESH_INTERVAL_MINUTES: readonly [1, 5, 10, 30, 60];
1344
+ /** One accepted cadence. */
1345
+ type TraeRefreshIntervalMinutes = typeof TRAE_REFRESH_INTERVAL_MINUTES[number];
1334
1346
  /** Subscription status of an international (ai) account, rendered instead of the CN credit packs. */
1335
1347
  interface TraeWebPayStatus {
1336
1348
  isDollarUsageBilling: boolean;
@@ -4821,6 +4833,23 @@ interface Config {
4821
4833
  * where the entry seeds the switch at page load).
4822
4834
  */
4823
4835
  sidebarPanel?: boolean;
4836
+ /**
4837
+ * How often the plugin refreshes its own readouts, in MINUTES.
4838
+ *
4839
+ * ONE plugin-wide number rather than one per region, because the thing it
4840
+ * paces is the plugin's background chatter, not a region's data: both
4841
+ * surfaces that poll (the card's usage document and the composer readout)
4842
+ * read the same routes at the same cost, and a per-region copy would let the
4843
+ * same screen refresh at two cadences.
4844
+ *
4845
+ * `undefined` means the default (5 minutes), which is what both surfaces
4846
+ * already used before this was settable — so an upgrade moves nothing.
4847
+ * The accepted values are exactly {@link TRAE_REFRESH_INTERVAL_MINUTES}
4848
+ * (1 / 5 / 10 / 30 / 60); the schema below refuses anything else, and the
4849
+ * readers fall back to the default for a document that never went through
4850
+ * it (a hand-edited file, or a profile written before this field existed).
4851
+ */
4852
+ refreshIntervalMinutes?: TraeRefreshIntervalMinutes;
4824
4853
  /**
4825
4854
  * Per-region model state, keyed `cn` | `ai`. The CN and international apps
4826
4855
  * expose different rosters, so each keeps its own directory and selection
package/lib/index.js CHANGED
@@ -2411,6 +2411,23 @@ function nextRegionCreditPosition(value, panel, position) {
2411
2411
  });
2412
2412
  }
2413
2413
  /**
2414
+ * The automatic-refresh cadences this plugin offers, in MINUTES.
2415
+ *
2416
+ * The list is the setting's whole value domain: the schema validates against
2417
+ * exactly these, so a hand-edited `7` is refused at write time instead of
2418
+ * silently producing a cadence nobody chose. `1` is the floor because every
2419
+ * usage read costs two upstream requests and a tighter loop would turn a
2420
+ * background refresh into traffic the user never asked for.
2421
+ */
2422
+ const TRAE_REFRESH_INTERVAL_MINUTES = [
2423
+ 1,
2424
+ 5,
2425
+ 10,
2426
+ 30,
2427
+ 60
2428
+ ];
2429
+ new Set(TRAE_REFRESH_INTERVAL_MINUTES);
2430
+ /**
2414
2431
  * Panels whose text-to-image path this plugin has actually MEASURED, in
2415
2432
  * `TRAE_PANELS` tab order.
2416
2433
  *
@@ -11652,6 +11669,7 @@ const Config = z.object({
11652
11669
  accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
11653
11670
  showPointsInMainUi: asVolatile(z.boolean().default(false).description("在输入框工具栏显示积分读数「Trae CN · <数值>」(仅当选中的模型属于本插件时出现,可点击查看账号与积分明细)")),
11654
11671
  sidebarPanel: asVolatile(z.boolean().default(false).description("在 DSH 侧边栏显示「Trae」面板:点开是这张卡片的整页版。默认关闭——这一行占的是所有插件共用的全局侧边栏座位,而「插件」页本来就能看到同样的内容。点一下即生效,不用保存。")),
11672
+ refreshIntervalMinutes: asVolatile(z.union(TRAE_REFRESH_INTERVAL_MINUTES).default(5).description("自动刷新间隔(分钟):卡片与输入框读数共用。可选 1 / 5 / 10 / 30 / 60,默认 5。每次刷新会读取用量(两次只读的上游请求),间隔越短请求越频繁。")),
11655
11673
  accounts: asVolatile(accountSelectionConfig.description("Per-panel account selections, keyed cn | enterprise | ai")),
11656
11674
  regions: asVolatile(z.dict(regionStateConfig).default({}).description("Per-panel model directory and selection, keyed cn | enterprise | ai")),
11657
11675
  lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
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.16.0",
5
+ "version": "4.17.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",