dsh-ds-balance 1.0.0 → 1.1.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/README.md +115 -49
- package/README_en.md +114 -48
- package/lib/client.js +60 -126
- package/lib/config.d.ts +10 -1
- package/lib/config.js +13 -1
- package/lib/http/handlers.js +2 -2
- package/lib/services/balance-service.js +2 -1
- package/lib/types/client/locales.d.ts +6 -8
- package/lib/types/client/model.d.ts +12 -0
- package/lib/types/client/settings/fields.d.ts +6 -23
- package/lib/types/client/settings/use-config-form.d.ts +2 -1
- package/lib/types/client/sidebar/BalancePopover.d.ts +2 -4
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,81 +1,147 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">dsh-ds-balance</h1>
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<div align="center">
|
|
6
|
+
<p><strong>把 DeepSeek 账户余额放进 DSH 的左边栏</strong></p>
|
|
7
|
+
<p><em>DeepSeek account balance in the DSH sidebar</em></p>
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://api-docs.deepseek.com/"><img src="https://img.shields.io/badge/DeepSeek%20API-Official-4D6BFE?style=flat" alt="DeepSeek 官方接口"></a>
|
|
11
|
+
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/DeepSeek%20Harness-Plugin-4176E6?style=flat" alt="DeepSeek Harness Plugin"></a>
|
|
12
|
+
</p>
|
|
9
13
|
|
|
10
|
-
>
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
<p>
|
|
15
|
+
<a href="https://github.com/zlZayn/dsh-ds-balance/actions/workflows/ci.yml"><img src="https://github.com/zlZayn/dsh-ds-balance/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/dsh-ds-balance"><img src="https://img.shields.io/npm/v/dsh-ds-balance.svg" alt="npm"></a>
|
|
17
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT 许可证"></a>
|
|
18
|
+
</p>
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
<p>
|
|
21
|
+
<strong><a href="README.md">简体中文</a></strong> · <a href="README_en.md">English</a>
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
16
24
|
|
|
17
|
-
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
> [!NOTE]
|
|
28
|
+
> **余额读自 DeepSeek 官方 `GET /user/balance`**,不是估算。凭据只经 DSH 的凭据通道解析:API Key 不会出现在设置文件里,也不会返回给界面。
|
|
29
|
+
|
|
30
|
+
余额要有地方看,但不该占地方。左边栏底部一个常驻的状态环,点开是三段金额与数据新鲜度;要改配置,进设置页那一张卡片。
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="assets/sidebar-popover.png" alt="左边栏底部的「DeepSeek 余额」条目与展开的余额浮层" width="360">
|
|
34
|
+
<br>
|
|
35
|
+
<em>常驻<strong>左边栏底部</strong>,与「使用统计」「设置」并排;点开是余额、赠送 / 充值拆分与数据新鲜度。</em>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
## 界面一览
|
|
39
|
+
|
|
40
|
+
| 界面 | 一句话 | 你会用它来 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| 侧栏圆环 | 常驻左边栏底部的状态环 + 名称,**环里填了多少 = 余额离该币种预警线还有多远** | 扫一眼就知道还剩多少、离预警线多远 |
|
|
43
|
+
| 余额浮层 | 点条目展开:总额、赠送 / 充值拆分、数据新鲜度、手动刷新(带冷却) | 核对具体数字,以及数据是几分钟前的 |
|
|
44
|
+
| 设置卡片 | 连接 / 展示 / 阈值 / 刷新四组,各自可折叠,默认四组全收起 | 换端点、换币种、调预警线、调节奏 |
|
|
45
|
+
|
|
46
|
+
三种界面的分工是死的:**圆环回答「大概还剩多少」,浮层回答「具体是多少」,卡片回答「怎么算」。**
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="assets/settings-card.png" alt="插件配置页中的「DeepSeek 余额」卡片" width="360">
|
|
50
|
+
<br>
|
|
51
|
+
<em>在 <strong>设置 → 插件 → 插件配置</strong> 中与其他插件并排;四组默认全收起,卡片一打开只占四行折叠头。</em>
|
|
52
|
+
</p>
|
|
18
53
|
|
|
19
54
|
## 能力
|
|
20
55
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
-
|
|
56
|
+
- 左边栏底部常驻一个状态环加名称,点开看明细;收起与展开是同一个环,位置不变。
|
|
57
|
+
- 余额按设定的周期自动刷新,界面读的是缓存 —— 开着界面不会反复去打上游接口。
|
|
58
|
+
- 浮层给出总额、赠送 / 充值拆分、数据是多久之前的,以及一个带冷却的手动刷新。
|
|
59
|
+
- 多币种:显示哪个币种由账户决定;设置里选的币种账户里没有时,浮层会说明并给一键改用。
|
|
60
|
+
- 设置卡片分四组、各自可折叠,默认全收起;组里有填错的项时,那一组会自己展开。
|
|
61
|
+
- 凭据默认继承官方模型页配好的那一份,不必重填;徽标只报「已配置密钥。/ 未配置密钥。」(官方卡片同款),
|
|
62
|
+
只读时不给编辑,并说明原因。
|
|
63
|
+
- 颜色只表达状态(正常 / 偏低 / 告急),与金额大小无关;读法与理由见「[圆环怎么读](#圆环怎么读)」。
|
|
29
64
|
|
|
30
65
|
## 安装
|
|
31
66
|
|
|
32
|
-
|
|
67
|
+
### 前置
|
|
68
|
+
|
|
69
|
+
- **DSH**:版本范围以 [package.json](package.json) 的 `engines.dsh` 与 `peerDependencies` 为准,本插件跟的是宿主当前那条 alpha 线。
|
|
70
|
+
- Node `>= 20`(同上,真源是 `engines.node`)。
|
|
71
|
+
|
|
72
|
+
装宿主时**要显式点名版本线**:`@deepseek-ai/dsh` 的 `latest` 标签比本插件要求的那条线还旧 —— 按默认方式装会落在声明范围之外。
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install -g @deepseek-ai/dsh@alpha # 本插件承诺支持的线
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
兼容性不是推断出来的:每周由 [compat.yml](.github/workflows/compat.yml) 在 `alpha` 与 `next` 两条线上换包实跑一遍现有测试。当前结论与红了怎么办见 [兼容性](docs/PUBLISHING.md#兼容性)。
|
|
33
79
|
|
|
34
|
-
从 npm
|
|
80
|
+
### 从 npm 安装
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
dsh plugin --profile web add dsh-ds-balance
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
装完**重启 `dsh --profile web`** 生效。
|
|
87
|
+
|
|
88
|
+
### 从源码安装
|
|
35
89
|
|
|
36
90
|
```bash
|
|
37
91
|
git clone https://github.com/zlZayn/dsh-ds-balance.git
|
|
38
92
|
cd dsh-ds-balance
|
|
39
|
-
npm install
|
|
40
|
-
|
|
41
|
-
dsh plugin --profile
|
|
93
|
+
npm install && npm run build
|
|
94
|
+
|
|
95
|
+
dsh plugin --profile web add "$PWD"
|
|
42
96
|
```
|
|
43
97
|
|
|
44
|
-
|
|
45
|
-
|
|
98
|
+
与 npm 那条路一样,重启后生效。
|
|
99
|
+
|
|
100
|
+
### 发现与安装
|
|
46
101
|
|
|
47
|
-
|
|
102
|
+
- **npm**:[`dsh-ds-balance`](https://www.npmjs.com/package/dsh-ds-balance)
|
|
103
|
+
- **GitHub**:[`zlZayn/dsh-ds-balance`](https://github.com/zlZayn/dsh-ds-balance)
|
|
48
104
|
|
|
49
|
-
|
|
105
|
+
仓库带有 GitHub topic [`dsh-plugin`](https://github.com/topics/dsh-plugin),插件市场据此自动发现插件。
|
|
50
106
|
|
|
51
107
|
## 配置
|
|
52
108
|
|
|
53
|
-
|
|
109
|
+
打开 **设置 → 插件 → 插件配置 → DeepSeek 余额**,四组各自可折叠:
|
|
54
110
|
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
111
|
+
- **连接**:API 地址与凭据,两项都默认留空 —— 地址留空即用 DeepSeek 官方端点,凭据继承官方模型页那一份、只读不可改;
|
|
112
|
+
二级「自定义设置」里只放凭据引用名。
|
|
113
|
+
- **展示**:金额用哪种币种,或让它自动跟随账户。
|
|
114
|
+
- **阈值**:每个币种两档提醒线(预警 / 告急)。同一币种内**告急必须严格低于预警**,相等也会被拒绝。
|
|
115
|
+
- **刷新**:服务端刷新周期与界面轮询周期。
|
|
59
116
|
|
|
60
|
-
|
|
117
|
+
保存即生效,不必重启 DSH。
|
|
61
118
|
|
|
62
|
-
|
|
63
|
-
- **API Key 永不返回前端**:配置接口只回一个固定长度的掩码串,连末几位也不给。
|
|
64
|
-
- 密钥只经 DSH 的凭据通道解析,不落日志、不落本插件的文件。
|
|
65
|
-
- 余额快照落在 DSH 自己的存储目录(`ctx.storageDomain`),按凭据派生出的账本标识分组;
|
|
66
|
-
换 key 自动开新账本,旧快照不会被混用。
|
|
67
|
-
- 一个非记录型文件 `.salt` 落在 `$DSH_HOME` 下,用来派生账本标识;**它丢了旧快照会读不回来**。
|
|
119
|
+
### 圆环怎么读
|
|
68
120
|
|
|
69
|
-
|
|
121
|
+
- 环里填多少 = 当前余额占该币种**预警线**的比例,100% 封顶;告急线不参与画环 —— 它已经决定了颜色。
|
|
122
|
+
- 颜色只表达状态,与金额大小无关:正常、偏低、告急各一色;账户读不到时另画一个带叉号的环。
|
|
123
|
+
- 没配阈值的币种退回按状态定性:正常与不可用画满环,偏低 3/4,告急 1/4,未知空环。
|
|
124
|
+
- 为什么颜色不由金额算、阈值为什么只当刻度 → [数据流](docs/ARCHITECTURE.md#数据流)。
|
|
70
125
|
|
|
71
|
-
|
|
72
|
-
- 金额一律是八位小数的字符串,前端按字符串裁两位显示,不经过浮点数。
|
|
73
|
-
- 完整响应形状与配置契约 → [docs/ui-handoff.md](docs/ui-handoff.md)。
|
|
126
|
+
### 凭据
|
|
74
127
|
|
|
75
|
-
|
|
128
|
+
密钥只经 DSH 的凭据通道解析,卡片里的 `API Key` 一栏默认继承官方模型页配好的那一份,且只读不可改。
|
|
129
|
+
由启动环境(环境变量)提供时,字段只读,徽标写明来源。
|
|
130
|
+
|
|
131
|
+
## 安全与边界
|
|
132
|
+
|
|
133
|
+
- **API Key 永不返回界面**:配置接口只回一个固定长度的掩码串,连末几位也不给。
|
|
134
|
+
- 密钥不落日志、不落本插件的文件;设置文件里只有引用名,卡片可以安全截图或分享。
|
|
135
|
+
- 余额快照存在 DSH 自己的数据目录里,按凭据派生出的账本标识分组 —— 换 key 自动开新账本,旧快照不会被混用。
|
|
136
|
+
- 账本标识还要一个 `.salt` 文件(在 DSH 的 home 目录下)参与派生;**它丢了,旧快照就读不回来**。
|
|
137
|
+
- 只访问 `api.deepseek.com`,不代理、不转发其他流量。
|
|
76
138
|
|
|
77
|
-
|
|
139
|
+
## 许可
|
|
140
|
+
|
|
141
|
+
[MIT](LICENSE)。
|
|
142
|
+
|
|
143
|
+
## 贡献
|
|
78
144
|
|
|
79
|
-
|
|
145
|
+
外部贡献入口(报 bug 带什么、提功能前先翻什么、提 PR 前做什么)→ [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
80
146
|
|
|
81
|
-
|
|
147
|
+
设计取向与实现约束 → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md);发布流程与版本号判定 → [docs/PUBLISHING.md](docs/PUBLISHING.md);维护者文档地图 → [AGENTS.md](AGENTS.md)。
|
package/README_en.md
CHANGED
|
@@ -1,81 +1,147 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">dsh-ds-balance</h1>
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<div align="center">
|
|
6
|
+
<p><strong>DeepSeek account balance in the DSH sidebar</strong></p>
|
|
7
|
+
<p><em>把 DeepSeek 账户余额放进 DSH 的左边栏</em></p>
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://api-docs.deepseek.com/"><img src="https://img.shields.io/badge/DeepSeek%20API-Official-4D6BFE?style=flat" alt="DeepSeek official API"></a>
|
|
11
|
+
<a href="https://github.com/deepseek-ai/deepseek-harness"><img src="https://img.shields.io/badge/DeepSeek%20Harness-Plugin-4176E6?style=flat" alt="DeepSeek Harness Plugin"></a>
|
|
12
|
+
</p>
|
|
9
13
|
|
|
10
|
-
>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
<p>
|
|
15
|
+
<a href="https://github.com/zlZayn/dsh-ds-balance/actions/workflows/ci.yml"><img src="https://github.com/zlZayn/dsh-ds-balance/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/dsh-ds-balance"><img src="https://img.shields.io/npm/v/dsh-ds-balance.svg" alt="npm"></a>
|
|
17
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="MIT License"></a>
|
|
18
|
+
</p>
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
<p>
|
|
21
|
+
<a href="README.md">简体中文</a> · <strong><a href="README_en.md">English</a></strong>
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
> [!NOTE]
|
|
28
|
+
> **The balance is read from the official `GET /user/balance`** — not estimated. The credential is resolved only through DSH's credential channel: the API key never lands in a settings file and is never returned to the UI.
|
|
29
|
+
|
|
30
|
+
The balance needs somewhere to live that does not take up room. A permanent status ring at the bottom of the sidebar; click it for the three amounts and the freshness of the data. Anything you want to configure lives on the settings card.
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="assets/sidebar-popover_en.png" alt="The DeepSeek balance entry at the bottom of the sidebar, with its popover open" width="360">
|
|
34
|
+
<br>
|
|
35
|
+
<em>A permanent fixture at the <strong>bottom of the sidebar</strong>, alongside Usage statistics and Settings; click it for the balance, the granted / topped-up split, and how fresh the data is.</em>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
## Surface at a glance
|
|
39
|
+
|
|
40
|
+
| Surface | One line | Use it for |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Sidebar ring | A status ring plus label at the bottom of the sidebar; **how full it is = how far the balance is from that currency's warning line** | Seeing at a glance how much is left and how close it is to the warning line |
|
|
43
|
+
| Balance popover | Click the entry: total, granted / topped-up split, data freshness, manual refresh (with cooldown) | Checking the exact figures, and how many minutes old they are |
|
|
44
|
+
| Settings card | Connection / Display / Thresholds / Refresh, each collapsible, all collapsed by default | Changing the endpoint, the currency, the warning lines, the cadence |
|
|
45
|
+
|
|
46
|
+
The division of labour is fixed: **the ring answers "roughly how much is left", the popover answers "exactly how much", and the card answers "how is that computed".**
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<img src="assets/settings-card_en.png" alt="The DeepSeek balance card in the plugin settings" width="360">
|
|
50
|
+
<br>
|
|
51
|
+
<em>Sits alongside other plugins in <strong>Settings → Plugins → Plugin configuration</strong>; all four groups are collapsed by default, so the card opens as four header rows.</em>
|
|
52
|
+
</p>
|
|
19
53
|
|
|
20
54
|
## Capabilities
|
|
21
55
|
|
|
22
|
-
- A permanent status ring plus label at the bottom of the sidebar; click it
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- Multiple currencies: the
|
|
26
|
-
- The settings card has four collapsible groups
|
|
27
|
-
-
|
|
28
|
-
|
|
56
|
+
- A permanent status ring plus label at the bottom of the sidebar; click it for the breakdown. The collapsed and expanded states share the same ring, in the same place.
|
|
57
|
+
- The balance refreshes on its own schedule and the UI reads a cache — leaving the interface open does not hammer the upstream.
|
|
58
|
+
- The popover shows the total, the granted / topped-up split, how old the data is, and a manual refresh with a cooldown.
|
|
59
|
+
- Multiple currencies: the account decides which currency is shown; when the one chosen in settings is absent, the popover explains and offers a one-click switch.
|
|
60
|
+
- The settings card has four collapsible groups, all collapsed by default; a group holding a bad value opens itself.
|
|
61
|
+
- The credential is inherited from the official model settings by default, so there is nothing to re-enter; where it comes from is on a badge
|
|
62
|
+
(a key is configured / no key is configured, exactly the official card's wording), and a read-only field explains itself instead of offering an edit.
|
|
63
|
+
- Colour carries state only (normal / low / critical), never an amount; see "[Reading the ring](#reading-the-ring)".
|
|
29
64
|
|
|
30
65
|
## Installation
|
|
31
66
|
|
|
32
|
-
|
|
67
|
+
### Requirements
|
|
68
|
+
|
|
69
|
+
- **DSH**: the range is whatever [package.json](package.json) declares under `engines.dsh` and `peerDependencies`; this plugin follows the alpha line the host is on.
|
|
70
|
+
- Node `>= 20` (same source of truth: `engines.node`).
|
|
71
|
+
|
|
72
|
+
Install the host by **naming the version line explicitly**: the `latest` tag of `@deepseek-ai/dsh` is older than the line this plugin requires — a default install lands outside the declared range.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install -g @deepseek-ai/dsh@alpha # the line this plugin promises to support
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Compatibility is measured, not inferred: every week [compat.yml](.github/workflows/compat.yml) swaps packages onto the `alpha` and `next` lines and reruns the existing tests. The current verdict, and what to do when it goes red, are in [Compatibility](docs/PUBLISHING.md#兼容性).
|
|
33
79
|
|
|
34
|
-
|
|
80
|
+
### From npm
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
dsh plugin --profile web add dsh-ds-balance
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Restarting `dsh --profile web`** is what makes it take effect.
|
|
87
|
+
|
|
88
|
+
### From source
|
|
35
89
|
|
|
36
90
|
```bash
|
|
37
91
|
git clone https://github.com/zlZayn/dsh-ds-balance.git
|
|
38
92
|
cd dsh-ds-balance
|
|
39
|
-
npm install
|
|
40
|
-
|
|
41
|
-
dsh plugin --profile
|
|
93
|
+
npm install && npm run build
|
|
94
|
+
|
|
95
|
+
dsh plugin --profile web add "$PWD"
|
|
42
96
|
```
|
|
43
97
|
|
|
44
|
-
|
|
45
|
-
|
|
98
|
+
Same as the npm route: it takes effect after a restart.
|
|
99
|
+
|
|
100
|
+
### Discovery
|
|
46
101
|
|
|
47
|
-
**
|
|
102
|
+
- **npm**: [`dsh-ds-balance`](https://www.npmjs.com/package/dsh-ds-balance)
|
|
103
|
+
- **GitHub**: [`zlZayn/dsh-ds-balance`](https://github.com/zlZayn/dsh-ds-balance)
|
|
48
104
|
|
|
49
|
-
|
|
50
|
-
and a status ring at the bottom of the sidebar.
|
|
105
|
+
The repository carries the GitHub topic [`dsh-plugin`](https://github.com/topics/dsh-plugin), which is how the plugin marketplace discovers plugins.
|
|
51
106
|
|
|
52
107
|
## Configuration
|
|
53
108
|
|
|
54
|
-
|
|
109
|
+
Open **Settings → Plugins → Plugin configuration → DeepSeek balance**. Four groups, each collapsible:
|
|
55
110
|
|
|
56
|
-
- Connection
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
111
|
+
- **Connection**: the API base URL and the credential, both blank by default — a blank URL means the official DeepSeek endpoint, and the credential is inherited from the official model page and is read-only.
|
|
112
|
+
The nested "Customised settings" holds only the credential reference name.
|
|
113
|
+
- **Display**: which currency to use for amounts, or let it follow the account.
|
|
114
|
+
- **Thresholds**: two alert lines per currency (warning / critical). **Within one currency the critical line must be strictly lower than the warning line** — equality is rejected too.
|
|
115
|
+
- **Refresh**: the server refresh interval and the UI poll interval.
|
|
60
116
|
|
|
61
|
-
|
|
117
|
+
Saving applies immediately; there is no need to restart DSH.
|
|
62
118
|
|
|
63
|
-
|
|
64
|
-
- **The API key is never returned to the frontend**: the config endpoint returns only a fixed-length mask, not even the last few characters.
|
|
65
|
-
- The key is resolved only through the DSH credential channel; it is never logged and never written to a file owned by this plugin.
|
|
66
|
-
- Balance snapshots land in DSH's own storage (`ctx.storageDomain`), grouped by a ledger identifier derived from the credential. Changing the key opens a new ledger; old snapshots are never mixed in.
|
|
67
|
-
- One non-record file, `.salt`, lives under `$DSH_HOME` and derives the ledger identifier. **Losing it makes old snapshots unreadable.**
|
|
119
|
+
### Reading the ring
|
|
68
120
|
|
|
69
|
-
|
|
121
|
+
- How full the ring is = the current balance as a fraction of that currency's **warning line**, capped at 100%; the critical line takes no part in drawing it — it already decided the colour.
|
|
122
|
+
- Colour carries state only, never an amount: normal, low and critical each get one hue; an account that cannot be read gets a ring with a cross instead.
|
|
123
|
+
- A currency with no threshold configured falls back to state: full ring for normal and unavailable, 3/4 for low, 1/4 for critical, empty for unknown.
|
|
124
|
+
- Why colour is never computed from an amount, and why thresholds are only a scale → [Data flow](docs/ARCHITECTURE.md#数据流).
|
|
70
125
|
|
|
71
|
-
|
|
72
|
-
- Amounts are always eight-decimal strings; the frontend trims them to two characters-wise and never goes through floating point.
|
|
73
|
-
- Full response shape and configuration contract → [docs/ui-handoff.md](docs/ui-handoff.md).
|
|
126
|
+
### Credentials
|
|
74
127
|
|
|
75
|
-
|
|
128
|
+
The key is resolved only through DSH's credential channel; the card's "API key" inherits the one already configured on the official model settings page.
|
|
129
|
+
When it comes from the launch environment (an environment variable) the field is read-only and the badge says where it came from.
|
|
130
|
+
|
|
131
|
+
## Security and boundaries
|
|
76
132
|
|
|
77
|
-
|
|
133
|
+
- **The API key is never returned to the UI**: the config endpoint returns only a fixed-length mask, not even the last few characters.
|
|
134
|
+
- The key is never logged and never written to a file owned by this plugin; the settings file holds only a reference name, so the card is safe to screenshot or share.
|
|
135
|
+
- Balance snapshots live in DSH's own data directory, grouped by a ledger identifier derived from the credential — changing the key opens a new ledger and old snapshots are never mixed in.
|
|
136
|
+
- That identifier also involves a `.salt` file in DSH's home directory; **lose it and old snapshots become unreadable**.
|
|
137
|
+
- Only `api.deepseek.com` is contacted; nothing is proxied or forwarded.
|
|
78
138
|
|
|
79
139
|
## License
|
|
80
140
|
|
|
81
|
-
MIT
|
|
141
|
+
[MIT](LICENSE).
|
|
142
|
+
|
|
143
|
+
## Contributing
|
|
144
|
+
|
|
145
|
+
Where to report bugs, what to check before proposing a feature, and what to do before sending a PR → [CONTRIBUTING_en.md](CONTRIBUTING_en.md).
|
|
146
|
+
|
|
147
|
+
Design stance and implementation constraints → [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md); the release flow and the version-bump decision chain → [docs/PUBLISHING.md](docs/PUBLISHING.md); the maintainer's document map → [AGENTS.md](AGENTS.md).
|
package/lib/client.js
CHANGED
|
@@ -85,9 +85,9 @@ var zh = {
|
|
|
85
85
|
"settings.group.refresh": "\u5237\u65B0",
|
|
86
86
|
"settings.group.display": "\u5C55\u793A",
|
|
87
87
|
"settings.group.thresholds": "\u9608\u503C",
|
|
88
|
-
"settings.field.apiKey": "API
|
|
88
|
+
"settings.field.apiKey": "API Key",
|
|
89
89
|
"settings.field.apiKeyRef": "API \u5BC6\u94A5\u5F15\u7528\u540D",
|
|
90
|
-
"settings.field.baseUrl": "
|
|
90
|
+
"settings.field.baseUrl": "\u63A5\u53E3\u5730\u5740",
|
|
91
91
|
"settings.field.serverRefreshSeconds": "\u670D\u52A1\u7AEF\u5237\u65B0\u9891\u7387\uFF08\u79D2\uFF09",
|
|
92
92
|
"settings.field.clientPollSeconds": "\u5BA2\u6237\u7AEF\u8F6E\u8BE2\u9891\u7387\uFF08\u79D2\uFF09",
|
|
93
93
|
"settings.field.manualRefreshCooldownSeconds": "\u624B\u52A8\u5237\u65B0\u51B7\u5374\uFF08\u79D2\uFF09",
|
|
@@ -98,7 +98,7 @@ var zh = {
|
|
|
98
98
|
"settings.field.usdCritical": "USD \u544A\u6025",
|
|
99
99
|
"settings.hint.apiKey": "\u7559\u7A7A\u5219\u6539\u7528\u5F15\u7528\u540D\u4ECE\u51ED\u636E\u5B58\u50A8\u8BFB\u53D6\u3002",
|
|
100
100
|
"settings.hint.apiKeyRef": "\u51ED\u636E\u5B58\u50A8\u91CC\u7684\u6761\u76EE\u540D\uFF1B\u975E\u7A7A\u65F6\u4F18\u5148\u4E8E API Key\u3002",
|
|
101
|
-
"settings.hint.baseUrl": "\
|
|
101
|
+
"settings.hint.baseUrl": "\u7559\u7A7A\u5219\u4F7F\u7528\u63D0\u4F9B\u65B9\u9ED8\u8BA4\u5730\u5740\u3002",
|
|
102
102
|
"settings.hint.displayCurrency": "\u6240\u9009\u5E01\u79CD\u4E0D\u5B58\u5728\u65F6\uFF0C\u56DE\u843D\u5230\u8D26\u6237\u5B9E\u9645\u6301\u6709\u7684\u5E01\u79CD\u3002",
|
|
103
103
|
"settings.hint.number": "\u8BF7\u586B\u6570\u5B57\uFF1B\u7559\u7A7A\u8868\u793A\u4F7F\u7528\u9ED8\u8BA4\u503C\u3002",
|
|
104
104
|
"settings.hint.threshold": "\u540E\u7AEF\u5224\u5B9A\uFF1B\u754C\u9762\u989C\u8272\u8DDF\u968F\u8FD4\u56DE\u7684\u4E25\u91CD\u5EA6\u3002",
|
|
@@ -107,14 +107,13 @@ var zh = {
|
|
|
107
107
|
"settings.hint.clientPollSeconds": "\u5237\u65B0\u663E\u793A\u7684\u95F4\u9694\uFF1B\u53EA\u8BFB\u672C\u5730\u7F13\u5B58\uFF0C\u4E0D\u8BBF\u95EE\u4E0A\u6E38\u3002",
|
|
108
108
|
"settings.hint.manualRefreshCooldownSeconds": "\u4E24\u6B21\u624B\u52A8\u5237\u65B0\u4E4B\u95F4\u7684\u6700\u77ED\u95F4\u9694\u3002",
|
|
109
109
|
"settings.currency.auto": "\u81EA\u52A8\uFF08\u8DDF\u968F\u8D26\u6237\uFF09",
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
"settings.credential.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"settings.hint.credential
|
|
117
|
-
"settings.hint.credential.overridden": "\u4F7F\u7528\u81EA\u5B9A\u4E49\u8BBE\u7F6E\u91CC\u7684\u503C\uFF0C\u5FFD\u7565\u7EE7\u627F\u3002",
|
|
110
|
+
// 凭据行的**状态徽章**:两条逐字用官方措辞 —— 取自 ui-settings-plugins 的
|
|
111
|
+
// webSearchApiKeySet / webSearchApiKeyUnset(后者去掉搜索相关的后半句)。
|
|
112
|
+
"settings.credential.configured": "\u5DF2\u914D\u7F6E\u5BC6\u94A5\u3002",
|
|
113
|
+
"settings.credential.notConfigured": "\u672A\u914D\u7F6E\u5BC6\u94A5\u3002",
|
|
114
|
+
// 凭据行下方的说明。官方 webSearchApiKeyHint 是一句与状态无关的常量说明,这里照做:
|
|
115
|
+
// 状态由徽章交代,说明只讲字段契约。
|
|
116
|
+
"settings.hint.credential": "\u4E0D\u5199\u5165\u8BBE\u7F6E\u6587\u4EF6\u3002\u7559\u7A7A\u8868\u793A\u4FDD\u6301\u5F53\u524D\u5BC6\u94A5\u3002",
|
|
118
117
|
"settings.group.customized": "\u81EA\u5B9A\u4E49\u8BBE\u7F6E",
|
|
119
118
|
"settings.test": "\u6D4B\u8BD5\u8FDE\u63A5",
|
|
120
119
|
"settings.testing": "\u6D4B\u8BD5\u4E2D\u2026",
|
|
@@ -168,7 +167,7 @@ var en = {
|
|
|
168
167
|
"settings.group.thresholds": "Thresholds",
|
|
169
168
|
"settings.field.apiKey": "API key",
|
|
170
169
|
"settings.field.apiKeyRef": "API key reference",
|
|
171
|
-
"settings.field.baseUrl": "
|
|
170
|
+
"settings.field.baseUrl": "Endpoint",
|
|
172
171
|
"settings.field.serverRefreshSeconds": "Server refresh (seconds)",
|
|
173
172
|
"settings.field.clientPollSeconds": "Client poll (seconds)",
|
|
174
173
|
"settings.field.manualRefreshCooldownSeconds": "Manual refresh cooldown (seconds)",
|
|
@@ -179,7 +178,7 @@ var en = {
|
|
|
179
178
|
"settings.field.usdCritical": "USD critical",
|
|
180
179
|
"settings.hint.apiKey": "Leave empty to resolve from the credential reference instead.",
|
|
181
180
|
"settings.hint.apiKeyRef": "Credential store entry name; takes precedence over the API key.",
|
|
182
|
-
"settings.hint.baseUrl": "
|
|
181
|
+
"settings.hint.baseUrl": "Leave blank to use the provider default.",
|
|
183
182
|
"settings.hint.displayCurrency": "Falls back to a currency the account actually holds.",
|
|
184
183
|
"settings.hint.number": "Enter a number; leave empty to use the default.",
|
|
185
184
|
"settings.hint.threshold": "Decided on the server; the UI colour follows the returned severity.",
|
|
@@ -188,11 +187,9 @@ var en = {
|
|
|
188
187
|
"settings.hint.clientPollSeconds": "How often the display refreshes; local cache only.",
|
|
189
188
|
"settings.hint.manualRefreshCooldownSeconds": "Minimum gap between two manual refreshes.",
|
|
190
189
|
"settings.currency.auto": "Automatic (follow account)",
|
|
191
|
-
"settings.credential.
|
|
192
|
-
"settings.
|
|
193
|
-
"settings.hint.credential
|
|
194
|
-
"settings.hint.credential.notConfigured": "Not configured yet.",
|
|
195
|
-
"settings.hint.credential.overridden": "Uses the value in Customised settings; the inherited one is ignored.",
|
|
190
|
+
"settings.credential.configured": "A key is configured.",
|
|
191
|
+
"settings.credential.notConfigured": "No key is configured.",
|
|
192
|
+
"settings.hint.credential": "Stored outside the settings file. Leave blank to keep the current key.",
|
|
196
193
|
"settings.group.customized": "Customized settings",
|
|
197
194
|
"settings.test": "Test connection",
|
|
198
195
|
"settings.testing": "Testing\u2026",
|
|
@@ -242,6 +239,7 @@ var fields_default = {
|
|
|
242
239
|
badges: "fields_badges",
|
|
243
240
|
reset: "fields_reset",
|
|
244
241
|
input: "fields_input",
|
|
242
|
+
inputStatic: "fields_inputStatic",
|
|
245
243
|
inputInvalid: "fields_inputInvalid",
|
|
246
244
|
inputWithAction: "fields_inputWithAction",
|
|
247
245
|
reveal: "fields_reveal",
|
|
@@ -342,11 +340,10 @@ function ReadOnlyControl(props) {
|
|
|
342
340
|
"input",
|
|
343
341
|
{
|
|
344
342
|
id: props.id,
|
|
345
|
-
className: fields_default.input,
|
|
343
|
+
className: clsx_default(fields_default.input, fields_default.inputStatic),
|
|
346
344
|
type: "text",
|
|
347
345
|
value: "",
|
|
348
|
-
readOnly: true
|
|
349
|
-
disabled: true
|
|
346
|
+
readOnly: true
|
|
350
347
|
}
|
|
351
348
|
);
|
|
352
349
|
}
|
|
@@ -367,39 +364,6 @@ function DetailsGroup(props) {
|
|
|
367
364
|
}
|
|
368
365
|
);
|
|
369
366
|
}
|
|
370
|
-
function SecretControl(props) {
|
|
371
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: fields_default.inputWithAction, children: [
|
|
372
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
373
|
-
"input",
|
|
374
|
-
{
|
|
375
|
-
id: props.id,
|
|
376
|
-
className: props.invalid ? fields_default.inputInvalid : fields_default.input,
|
|
377
|
-
type: props.revealed ? "text" : "password",
|
|
378
|
-
autoComplete: "off",
|
|
379
|
-
spellCheck: false,
|
|
380
|
-
...props.invalid ? { "aria-invalid": true } : {},
|
|
381
|
-
value: props.text,
|
|
382
|
-
disabled: props.disabled,
|
|
383
|
-
onChange: (event) => {
|
|
384
|
-
props.onEdit(event.target.value);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
),
|
|
388
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
389
|
-
"button",
|
|
390
|
-
{
|
|
391
|
-
type: "button",
|
|
392
|
-
className: clsx_default(fields_default.reveal, props.revealed && fields_default.revealOn),
|
|
393
|
-
"aria-label": props.revealLabel,
|
|
394
|
-
"aria-pressed": props.revealed,
|
|
395
|
-
title: props.revealLabel,
|
|
396
|
-
disabled: props.disabled,
|
|
397
|
-
onClick: props.onToggleReveal,
|
|
398
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconInspectOutline12, { size: 14 })
|
|
399
|
-
}
|
|
400
|
-
)
|
|
401
|
-
] });
|
|
402
|
-
}
|
|
403
367
|
function SelectorControl(props) {
|
|
404
368
|
const [open, setOpen] = (0, import_react.useState)(false);
|
|
405
369
|
const selected = props.options.find((option) => option.id === props.selectedId);
|
|
@@ -545,7 +509,7 @@ var SPEC_BY_FIELD = new Map(
|
|
|
545
509
|
var TEST_LATENCY_MS = 800;
|
|
546
510
|
function probeFailure(baseUrl, apiKey, apiKeyRef) {
|
|
547
511
|
const url = baseUrl.trim();
|
|
548
|
-
if (!/^https?:\/\//i.test(url)) return "base URL must start with http:// or https://";
|
|
512
|
+
if (url !== "" && !/^https?:\/\//i.test(url)) return "base URL must start with http:// or https://";
|
|
549
513
|
if (apiKey.trim() === "" && apiKeyRef.trim() === "") return "no API key or credential reference configured";
|
|
550
514
|
return null;
|
|
551
515
|
}
|
|
@@ -950,16 +914,12 @@ var BalanceSettingsCard_default = {
|
|
|
950
914
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
951
915
|
var DEFAULT_API_KEY_REF = "DEEPSEEK_API_KEY";
|
|
952
916
|
var CREDENTIAL_BADGE = {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
env: "settings.hint.credential.env",
|
|
960
|
-
configured: "settings.hint.credential.configured",
|
|
961
|
-
notConfigured: "settings.hint.credential.notConfigured",
|
|
962
|
-
overridden: "settings.hint.credential.overridden"
|
|
917
|
+
// 官方那张卡右上角只有一句话:「已配置密钥。」——继承来的、自己填的、环境提供的都算配置好了。
|
|
918
|
+
// 四档判据留着,是因为「覆盖 / 未覆盖」决定二级折叠里那两个字段的状态;徽章本身不区分它们。
|
|
919
|
+
env: "settings.credential.configured",
|
|
920
|
+
configured: "settings.credential.configured",
|
|
921
|
+
notConfigured: "settings.credential.notConfigured",
|
|
922
|
+
overridden: "settings.credential.configured"
|
|
963
923
|
};
|
|
964
924
|
var DEFAULT_GROUP_OPEN = {
|
|
965
925
|
connection: false,
|
|
@@ -994,7 +954,6 @@ function isFilled(value) {
|
|
|
994
954
|
function BalanceSettingsCard({ t, scope }) {
|
|
995
955
|
const form = useConfigForm(scope);
|
|
996
956
|
const [open, setOpen] = (0, import_react4.useState)(false);
|
|
997
|
-
const [revealed, setRevealed] = (0, import_react4.useState)(false);
|
|
998
957
|
const [groupOpen, setGroupOpen] = (0, import_react4.useState)(DEFAULT_GROUP_OPEN);
|
|
999
958
|
const saveStarted = (0, import_react4.useRef)(false);
|
|
1000
959
|
const toggleGroup = (key) => {
|
|
@@ -1189,7 +1148,7 @@ function BalanceSettingsCard({ t, scope }) {
|
|
|
1189
1148
|
pendingLabel: t("settings.unsaved"),
|
|
1190
1149
|
resetLabel: t("settings.reset"),
|
|
1191
1150
|
invalid: false,
|
|
1192
|
-
hint: t(
|
|
1151
|
+
hint: t("settings.hint.credential"),
|
|
1193
1152
|
disabled: true,
|
|
1194
1153
|
onReset: () => {
|
|
1195
1154
|
},
|
|
@@ -1211,44 +1170,7 @@ function BalanceSettingsCard({ t, scope }) {
|
|
|
1211
1170
|
onClick: form.runTest
|
|
1212
1171
|
}
|
|
1213
1172
|
) }, "test"),
|
|
1214
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
1215
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1216
|
-
FieldFrame,
|
|
1217
|
-
{
|
|
1218
|
-
id: FIELD_IDS.apiKey ?? "apiKey",
|
|
1219
|
-
label: t("settings.field.apiKey"),
|
|
1220
|
-
status: credentialStatus("apiKey"),
|
|
1221
|
-
pending: apiKey.dirty,
|
|
1222
|
-
resettable: apiKey.overridden || apiKey.dirty,
|
|
1223
|
-
pendingLabel: t("settings.unsaved"),
|
|
1224
|
-
resetLabel: t("settings.reset"),
|
|
1225
|
-
invalid: false,
|
|
1226
|
-
hint: t("settings.hint.apiKey"),
|
|
1227
|
-
disabled,
|
|
1228
|
-
onReset: () => {
|
|
1229
|
-
form.resetField("apiKey");
|
|
1230
|
-
},
|
|
1231
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1232
|
-
SecretControl,
|
|
1233
|
-
{
|
|
1234
|
-
id: FIELD_IDS.apiKey ?? "apiKey",
|
|
1235
|
-
text: apiKey.text,
|
|
1236
|
-
invalid: false,
|
|
1237
|
-
disabled,
|
|
1238
|
-
revealed,
|
|
1239
|
-
revealLabel: t("settings.field.apiKey"),
|
|
1240
|
-
onToggleReveal: () => {
|
|
1241
|
-
setRevealed(!revealed);
|
|
1242
|
-
},
|
|
1243
|
-
onEdit: (text) => {
|
|
1244
|
-
form.edit("apiKey", text);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
)
|
|
1248
|
-
}
|
|
1249
|
-
),
|
|
1250
|
-
textRow("apiKeyRef", "settings.field.apiKeyRef", "settings.hint.apiKeyRef", true)
|
|
1251
|
-
] })
|
|
1173
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DetailsGroup, { title: t("settings.group.customized"), children: textRow("apiKeyRef", "settings.field.apiKeyRef", "settings.hint.apiKeyRef", true) })
|
|
1252
1174
|
]
|
|
1253
1175
|
}
|
|
1254
1176
|
),
|
|
@@ -1430,6 +1352,10 @@ function ageBucket(ageMs) {
|
|
|
1430
1352
|
if (hours < 24) return { bucket: "hours", value: hours };
|
|
1431
1353
|
return { bucket: "days", value: Math.floor(hours / 24) };
|
|
1432
1354
|
}
|
|
1355
|
+
function currentAgeMs(seenAt, seenAgeMs, now) {
|
|
1356
|
+
const base = Number.isFinite(seenAgeMs) ? Math.max(0, seenAgeMs) : 0;
|
|
1357
|
+
return Math.max(0, base + (now - seenAt));
|
|
1358
|
+
}
|
|
1433
1359
|
|
|
1434
1360
|
// src/client/mock/scenarios.ts
|
|
1435
1361
|
var T0 = 176e10;
|
|
@@ -1638,8 +1564,7 @@ function BalancePopover(props) {
|
|
|
1638
1564
|
t,
|
|
1639
1565
|
selection,
|
|
1640
1566
|
displayCurrency,
|
|
1641
|
-
|
|
1642
|
-
now,
|
|
1567
|
+
ageMs,
|
|
1643
1568
|
refreshing,
|
|
1644
1569
|
cooldownSeconds,
|
|
1645
1570
|
panelRef,
|
|
@@ -1652,7 +1577,7 @@ function BalancePopover(props) {
|
|
|
1652
1577
|
const totalText = shown === null ? BALANCE_PLACEHOLDER : formatMoney(shown.total, shown.currency);
|
|
1653
1578
|
const grantedText = shown === null ? BALANCE_PLACEHOLDER : formatMoney(shown.granted, shown.currency);
|
|
1654
1579
|
const toppedUpText = shown === null ? BALANCE_PLACEHOLDER : formatMoney(shown.toppedUp, shown.currency);
|
|
1655
|
-
const age = ageBucket(
|
|
1580
|
+
const age = ageBucket(ageMs);
|
|
1656
1581
|
const updatedText = age.bucket === "just-now" || age.bucket === "unknown" ? t("popover.updated.justNow") : interpolate(t("popover.updated"), {
|
|
1657
1582
|
value: ageValueText(age.bucket, age.value, unitLocale(t))
|
|
1658
1583
|
});
|
|
@@ -1812,11 +1737,14 @@ function ringSpecFor(severity) {
|
|
|
1812
1737
|
return { state: spec.state === "ongoing" ? "idle" : spec.state, marker: spec.marker };
|
|
1813
1738
|
}
|
|
1814
1739
|
function SidebarBalance({ wide, t, config }) {
|
|
1740
|
+
const [view, setView] = (0, import_react5.useState)(() => {
|
|
1741
|
+
const response2 = resolveScenario() === null ? pendingView() : currentBalance();
|
|
1742
|
+
return { response: response2, seenAt: Date.now(), seenAgeMs: response2.ageMs };
|
|
1743
|
+
});
|
|
1744
|
+
const response = view.response;
|
|
1815
1745
|
const [mock, setMock] = (0, import_react5.useState)(() => resolveScenario() !== null);
|
|
1816
|
-
const [response, setResponse] = (0, import_react5.useState)(() => resolveScenario() === null ? pendingView() : currentBalance());
|
|
1817
1746
|
const loadedRef = (0, import_react5.useRef)(false);
|
|
1818
1747
|
const [localCurrency, setLocalCurrency] = (0, import_react5.useState)(null);
|
|
1819
|
-
const [localFetchedAt, setLocalFetchedAt] = (0, import_react5.useState)(null);
|
|
1820
1748
|
const [open, setOpen] = (0, import_react5.useState)(false);
|
|
1821
1749
|
const [refreshing, setRefreshing] = (0, import_react5.useState)(false);
|
|
1822
1750
|
const [cooldownUntil, setCooldownUntil] = (0, import_react5.useState)(0);
|
|
@@ -1825,21 +1753,28 @@ function SidebarBalance({ wide, t, config }) {
|
|
|
1825
1753
|
const panelRef = (0, import_react5.useRef)(null);
|
|
1826
1754
|
const refreshTimer = (0, import_react5.useRef)(void 0);
|
|
1827
1755
|
const lastScenario = (0, import_react5.useRef)(null);
|
|
1828
|
-
const
|
|
1756
|
+
const acceptBalance = (0, import_react5.useCallback)((next) => {
|
|
1757
|
+
setView({ response: next, seenAt: Date.now(), seenAgeMs: next.ageMs });
|
|
1758
|
+
}, []);
|
|
1759
|
+
const showUnreachable = (0, import_react5.useCallback)((error) => {
|
|
1760
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1761
|
+
setView((current) => ({ ...current, response: unreachableView(message) }));
|
|
1762
|
+
}, []);
|
|
1763
|
+
const markFresh = (0, import_react5.useCallback)(() => {
|
|
1764
|
+
setView((current) => ({ ...current, seenAt: Date.now(), seenAgeMs: 0 }));
|
|
1765
|
+
}, []);
|
|
1829
1766
|
const preference = localCurrency ?? config.displayCurrency;
|
|
1830
1767
|
const selection = (0, import_react5.useMemo)(() => selectionOf(response, preference), [response, preference]);
|
|
1831
|
-
const fetchedAt = localFetchedAt ?? mountedAt.current - response.ageMs;
|
|
1832
1768
|
(0, import_react5.useEffect)(() => subscribeScenario((key) => {
|
|
1833
1769
|
const active = resolveScenario() !== null;
|
|
1834
1770
|
setMock(active);
|
|
1835
1771
|
if (!active || lastScenario.current === key) return;
|
|
1836
1772
|
lastScenario.current = key;
|
|
1837
|
-
|
|
1838
|
-
setLocalFetchedAt(null);
|
|
1773
|
+
acceptBalance(currentBalance());
|
|
1839
1774
|
setRefreshing(false);
|
|
1840
1775
|
setCooldownUntil(0);
|
|
1841
1776
|
setOpen(false);
|
|
1842
|
-
}), []);
|
|
1777
|
+
}), [acceptBalance]);
|
|
1843
1778
|
(0, import_react5.useEffect)(() => {
|
|
1844
1779
|
if (mock) return;
|
|
1845
1780
|
let cancelled = false;
|
|
@@ -1848,11 +1783,10 @@ function SidebarBalance({ wide, t, config }) {
|
|
|
1848
1783
|
const next = await requestBalance({ currency: preference });
|
|
1849
1784
|
if (cancelled) return;
|
|
1850
1785
|
loadedRef.current = true;
|
|
1851
|
-
|
|
1852
|
-
setLocalFetchedAt(null);
|
|
1786
|
+
acceptBalance(next);
|
|
1853
1787
|
} catch (error) {
|
|
1854
1788
|
if (cancelled || loadedRef.current) return;
|
|
1855
|
-
|
|
1789
|
+
showUnreachable(error);
|
|
1856
1790
|
}
|
|
1857
1791
|
};
|
|
1858
1792
|
void load();
|
|
@@ -1863,7 +1797,7 @@ function SidebarBalance({ wide, t, config }) {
|
|
|
1863
1797
|
cancelled = true;
|
|
1864
1798
|
window.clearInterval(id);
|
|
1865
1799
|
};
|
|
1866
|
-
}, [mock, preference, config.clientPollSeconds, config.configSignature]);
|
|
1800
|
+
}, [mock, preference, config.clientPollSeconds, config.configSignature, acceptBalance, showUnreachable]);
|
|
1867
1801
|
(0, import_react5.useEffect)(() => () => {
|
|
1868
1802
|
if (refreshTimer.current !== void 0) window.clearTimeout(refreshTimer.current);
|
|
1869
1803
|
}, []);
|
|
@@ -1907,13 +1841,13 @@ function SidebarBalance({ wide, t, config }) {
|
|
|
1907
1841
|
if (Date.now() < cooldownUntil) return;
|
|
1908
1842
|
setRefreshing(true);
|
|
1909
1843
|
const finish = () => {
|
|
1910
|
-
setLocalFetchedAt(Date.now());
|
|
1911
1844
|
setRefreshing(false);
|
|
1912
1845
|
setCooldownUntil(Date.now() + Math.max(0, config.manualRefreshCooldownSeconds) * MS_PER_SECOND);
|
|
1913
1846
|
};
|
|
1914
1847
|
if (mock) {
|
|
1915
1848
|
refreshTimer.current = window.setTimeout(() => {
|
|
1916
1849
|
refreshTimer.current = void 0;
|
|
1850
|
+
markFresh();
|
|
1917
1851
|
finish();
|
|
1918
1852
|
}, REFRESH_SIMULATION_MS);
|
|
1919
1853
|
return;
|
|
@@ -1923,14 +1857,14 @@ function SidebarBalance({ wide, t, config }) {
|
|
|
1923
1857
|
await requestRefresh({ reason: "manual" });
|
|
1924
1858
|
const next = await requestBalance({ currency: preference });
|
|
1925
1859
|
loadedRef.current = true;
|
|
1926
|
-
|
|
1860
|
+
acceptBalance(next);
|
|
1927
1861
|
} catch (error) {
|
|
1928
|
-
if (!loadedRef.current)
|
|
1862
|
+
if (!loadedRef.current) showUnreachable(error);
|
|
1929
1863
|
} finally {
|
|
1930
1864
|
finish();
|
|
1931
1865
|
}
|
|
1932
1866
|
})();
|
|
1933
|
-
}, [cooldownUntil, config.manualRefreshCooldownSeconds, mock, preference, refreshing]);
|
|
1867
|
+
}, [cooldownUntil, config.manualRefreshCooldownSeconds, mock, preference, refreshing, acceptBalance, showUnreachable, markFresh]);
|
|
1934
1868
|
const handleUseShown = (0, import_react5.useCallback)(() => {
|
|
1935
1869
|
const current = selection.shown;
|
|
1936
1870
|
if (current === null) return;
|
|
@@ -1962,6 +1896,7 @@ function SidebarBalance({ wide, t, config }) {
|
|
|
1962
1896
|
if (markerLabel !== null) ariaParts.push(markerLabel);
|
|
1963
1897
|
const ariaLabel = ariaParts.join(" ");
|
|
1964
1898
|
const cooldownSeconds = Math.max(0, Math.ceil((cooldownUntil - now) / MS_PER_SECOND));
|
|
1899
|
+
const ageMs = currentAgeMs(view.seenAt, view.seenAgeMs, now);
|
|
1965
1900
|
const ringTitle = !wide && stateText !== null ? `${t("sidebar.aria.ring")} ${stateText}` : void 0;
|
|
1966
1901
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1967
1902
|
"div",
|
|
@@ -1993,8 +1928,7 @@ function SidebarBalance({ wide, t, config }) {
|
|
|
1993
1928
|
t,
|
|
1994
1929
|
selection,
|
|
1995
1930
|
displayCurrency: preference,
|
|
1996
|
-
|
|
1997
|
-
now,
|
|
1931
|
+
ageMs,
|
|
1998
1932
|
refreshing,
|
|
1999
1933
|
cooldownSeconds,
|
|
2000
1934
|
panelRef,
|
|
@@ -2106,7 +2040,7 @@ function apply(ctx) {
|
|
|
2106
2040
|
(seat) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SidebarSeatComponent, { seat, scope })
|
|
2107
2041
|
));
|
|
2108
2042
|
}
|
|
2109
|
-
var __dsbCss = "/* src/client/settings/fields.module.css */\n.fields_group {\n padding: 12px 0 0;\n}\n.fields_groupLast {\n padding-bottom: 12px;\n}\n.fields_groupNote {\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_fields {\n display: flex;\n flex-direction: column;\n}\n.fields_fields > * + * {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.fields_details {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin-top: 12px;\n padding-top: 12px;\n}\n.fields_detailsSummary {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n list-style: none;\n}\n.fields_detailsSummary::-webkit-details-marker {\n display: none;\n}\n.fields_detailsSummary::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-right: 1.5px solid currentColor;\n border-bottom: 1.5px solid currentColor;\n transform: translate(-0.5px, -1px) rotate(-45deg);\n transition: transform 120ms ease;\n}\n.fields_details[open] > .fields_detailsSummary::before {\n transform: translate(-0.5px, -1px) rotate(45deg);\n}\n.fields_detailsBody {\n padding-top: 10px;\n}\n.fields_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.fields_row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 0;\n}\n.fields_rowText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-right: 48px;\n}\n.fields_rowTitleLine {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_rowTitle {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_badges {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.fields_reset {\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n.fields_reset:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reset:disabled {\n cursor: default;\n}\n.fields_input {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_input:disabled {\n color: var(--dsw-alias-label-tertiary);\n opacity: 0.6;\n cursor: default;\n}\n.fields_inputInvalid {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_inputInvalid:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_inputInvalid:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_inputWithAction {\n position: relative;\n display: flex;\n align-items: center;\n}\n.fields_inputWithAction .fields_input,\n.fields_inputWithAction .fields_inputInvalid {\n padding-right: 38px;\n}\n.fields_reveal {\n position: absolute;\n right: 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.fields_reveal:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.fields_revealOn {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reveal:disabled {\n cursor: default;\n opacity: 0.4;\n}\n.fields_selectorAnchor {\n flex: none;\n}\n.fields_selector {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n flex: none;\n max-width: 100%;\n height: 36px;\n padding: 0 14px;\n border: none;\n border-radius: 18px;\n background: var(--dsw-alias-bg-module-platform);\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n}\n.fields_selector:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.fields_selector:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_selectorChevron {\n flex: none;\n}\n.fields_hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_pairNote {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warn-primary);\n}\n.fields_pairNote::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-radius: 999px;\n corner-shape: round;\n background: currentColor;\n}\n.fields_invalid {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.fields_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_actionButton {\n appearance: none;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.fields_actionButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.fields_actionButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.fields_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.fields_resultOk {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-success-primary);\n}\n.fields_resultFail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* src/client/settings/BalanceSettingsCard.module.css */\n.BalanceSettingsCard_card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.BalanceSettingsCard_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.BalanceSettingsCard_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.BalanceSettingsCard_headText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.BalanceSettingsCard_name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.BalanceSettingsCard_description {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_chevronWrap {\n flex: none;\n display: inline-flex;\n align-items: center;\n}\n.BalanceSettingsCard_chevron {\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.BalanceSettingsCard_chevronOpen {\n transform: rotate(180deg);\n}\n.BalanceSettingsCard_pending {\n flex: none;\n}\n.BalanceSettingsCard_body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding-bottom: 0;\n}\n.BalanceSettingsCard_readOnly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalanceSettingsCard_failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.BalanceSettingsCard_notice {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_discard,\n.BalanceSettingsCard_save {\n appearance: none;\n border: 0.5px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.BalanceSettingsCard_discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.BalanceSettingsCard_discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.BalanceSettingsCard_discard:disabled,\n.BalanceSettingsCard_save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalanceSettingsCard_discard:focus-visible,\n.BalanceSettingsCard_save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* src/client/sidebar/BalancePopover.module.css */\n.BalancePopover_panel {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: 12px;\n background: var(--dsw-specific-menu);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n.BalancePopover_title {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n.BalancePopover_titleLabel {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n.BalancePopover_titleLabel svg {\n flex: none;\n width: 14px;\n height: auto;\n}\n.BalancePopover_titleRule {\n margin-bottom: 10px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalancePopover_details {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 6px 16px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_details dt,\n.BalancePopover_details dd {\n min-width: 0;\n margin: 0;\n}\n.BalancePopover_details dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n.BalancePopover_notice {\n margin-top: 12px;\n}\n.BalancePopover_noticeText {\n margin: 0 0 8px;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_noticeActions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n.BalancePopover_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n}\n.BalancePopover_updated {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_status {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_refresh {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.BalancePopover_refresh:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_refresh:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.BalancePopover_refresh:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalancePopover_refresh[data-cooling] {\n color: var(--dsw-alias-label-caption);\n}\n.BalancePopover_refresh[data-refreshing] svg {\n animation: BalancePopover_balance-refresh-spin 900ms linear infinite;\n}\n@keyframes BalancePopover_balance-refresh-spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .BalancePopover_refresh[data-refreshing] svg {\n animation-duration: 2400ms;\n }\n}\n\n/* src/client/sidebar/PercentRing.module.css */\n.PercentRing_ring {\n display: block;\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.PercentRing_ring[data-state=done] {\n color: var(--dsw-alias-state-success-primary);\n}\n.PercentRing_ring[data-state=warning] {\n color: var(--dsw-alias-state-warn-primary);\n}\n.PercentRing_ring[data-state=error] {\n color: var(--dsw-alias-state-error-primary);\n}\n.PercentRing_track {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 2;\n}\n.PercentRing_fill {\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n}\n.PercentRing_cross {\n fill: none;\n stroke: currentColor;\n stroke-width: 1.5;\n stroke-linecap: round;\n}\n\n/* src/client/sidebar/SidebarBalance.module.css */\n.SidebarBalance_root {\n position: relative;\n display: flex;\n min-width: 0;\n}\n.SidebarBalance_root[data-mode=wide] {\n flex: none;\n width: calc(100% + 4px);\n margin: 4px -2px 0;\n}\n.SidebarBalance_root[data-mode=rail] {\n flex: none;\n margin: 8px 0 0;\n}\n.SidebarBalance_trigger {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n overflow: hidden;\n}\n.SidebarBalance_trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.SidebarBalance_trigger:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.SidebarBalance_root[data-mode=wide] .SidebarBalance_trigger {\n flex: none;\n gap: 8px;\n width: 100%;\n height: 42px;\n padding: 0 10px 0 8px;\n border-radius: 12px;\n}\n.SidebarBalance_root[data-mode=rail] .SidebarBalance_trigger {\n flex: none;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 50%;\n corner-shape: round;\n}\n.SidebarBalance_icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n}\n.SidebarBalance_label {\n flex: 0 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.SidebarBalance_marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* src/client/sidebar/footer-stack.module.css */\ndiv:has(> [data-slot=\"sidebar.footer.action\"]) {\n flex-direction: column;\n}\n";
|
|
2043
|
+
var __dsbCss = "/* src/client/settings/fields.module.css */\n.fields_group {\n padding: 12px 0 0;\n}\n.fields_groupLast {\n padding-bottom: 12px;\n}\n.fields_groupNote {\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_fields {\n display: flex;\n flex-direction: column;\n}\n.fields_fields > * + * {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.fields_details {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin-top: 12px;\n padding-top: 12px;\n}\n.fields_detailsSummary {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n list-style: none;\n}\n.fields_detailsSummary::-webkit-details-marker {\n display: none;\n}\n.fields_detailsSummary::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-right: 1.5px solid currentColor;\n border-bottom: 1.5px solid currentColor;\n transform: translate(-0.5px, -1px) rotate(-45deg);\n transition: transform 120ms ease;\n}\n.fields_details[open] > .fields_detailsSummary::before {\n transform: translate(-0.5px, -1px) rotate(45deg);\n}\n.fields_detailsBody {\n padding-top: 10px;\n}\n.fields_field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.fields_row {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 0;\n}\n.fields_rowText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-right: 48px;\n}\n.fields_rowTitleLine {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_rowTitle {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_badges {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n.fields_reset {\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n.fields_reset:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reset:disabled {\n cursor: default;\n}\n.fields_input {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_input:disabled {\n color: var(--dsw-alias-label-tertiary);\n opacity: 0.6;\n cursor: default;\n}\ninput.fields_inputStatic,\ninput.fields_inputStatic:hover,\ninput.fields_inputStatic:focus,\ninput.fields_inputStatic:focus-visible,\ninput.fields_inputStatic:active {\n border-color: var(--dsw-alias-border-l4);\n background: var(--dsw-alias-bg-layer-3);\n outline: none;\n box-shadow: none;\n cursor: default;\n pointer-events: none;\n}\n.fields_inputInvalid {\n box-sizing: border-box;\n width: 100%;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.fields_inputInvalid:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.fields_inputInvalid:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_inputWithAction {\n position: relative;\n display: flex;\n align-items: center;\n}\n.fields_inputWithAction .fields_input,\n.fields_inputWithAction .fields_inputInvalid {\n padding-right: 38px;\n}\n.fields_reveal {\n position: absolute;\n right: 4px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.fields_reveal:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.fields_revealOn {\n color: var(--dsw-alias-label-primary);\n}\n.fields_reveal:disabled {\n cursor: default;\n opacity: 0.4;\n}\n.fields_selectorAnchor {\n flex: none;\n}\n.fields_selector {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n flex: none;\n max-width: 100%;\n height: 36px;\n padding: 0 14px;\n border: none;\n border-radius: 18px;\n background: var(--dsw-alias-bg-module-platform);\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n}\n.fields_selector:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.fields_selector:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.fields_selectorChevron {\n flex: none;\n}\n.fields_hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.fields_pairNote {\n display: flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-warn-primary);\n}\n.fields_pairNote::before {\n content: \"\";\n flex: none;\n width: 5px;\n height: 5px;\n border-radius: 999px;\n corner-shape: round;\n background: currentColor;\n}\n.fields_invalid {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.fields_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.fields_actionButton {\n appearance: none;\n border: 0.5px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.fields_actionButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.fields_actionButton:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.fields_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.fields_resultOk {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-success-primary);\n}\n.fields_resultFail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n\n/* src/client/settings/BalanceSettingsCard.module.css */\n.BalanceSettingsCard_card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.BalanceSettingsCard_card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_cardOpen {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.BalanceSettingsCard_header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.BalanceSettingsCard_headText {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.BalanceSettingsCard_name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.BalanceSettingsCard_description {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_chevronWrap {\n flex: none;\n display: inline-flex;\n align-items: center;\n}\n.BalanceSettingsCard_chevron {\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.BalanceSettingsCard_chevronOpen {\n transform: rotate(180deg);\n}\n.BalanceSettingsCard_pending {\n flex: none;\n}\n.BalanceSettingsCard_body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding-bottom: 0;\n}\n.BalanceSettingsCard_readOnly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalanceSettingsCard_failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary);\n}\n.BalanceSettingsCard_notice {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalanceSettingsCard_discard,\n.BalanceSettingsCard_save {\n appearance: none;\n border: 0.5px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.BalanceSettingsCard_discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.BalanceSettingsCard_discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.BalanceSettingsCard_save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.BalanceSettingsCard_discard:disabled,\n.BalanceSettingsCard_save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalanceSettingsCard_discard:focus-visible,\n.BalanceSettingsCard_save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* src/client/sidebar/BalancePopover.module.css */\n.BalancePopover_panel {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: max-content;\n min-width: min(300px, calc(100vw - 24px));\n max-width: min(440px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: 12px;\n background: var(--dsw-specific-menu);\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n cursor: default;\n}\n.BalancePopover_title {\n display: flex;\n justify-content: space-between;\n gap: 16px;\n margin-bottom: 8px;\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n.BalancePopover_titleLabel {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n min-width: 0;\n}\n.BalancePopover_titleLabel svg {\n flex: none;\n width: 14px;\n height: auto;\n}\n.BalancePopover_titleRule {\n margin-bottom: 10px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.BalancePopover_details {\n display: grid;\n grid-template-columns: minmax(76px, auto) minmax(0, 1fr);\n gap: 6px 16px;\n margin: 0;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_details dt,\n.BalancePopover_details dd {\n min-width: 0;\n margin: 0;\n}\n.BalancePopover_details dd {\n color: var(--dsw-alias-label-secondary);\n font-variant-numeric: tabular-nums;\n text-align: right;\n}\n.BalancePopover_notice {\n margin-top: 12px;\n}\n.BalancePopover_noticeText {\n margin: 0 0 8px;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_noticeActions {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n}\n.BalancePopover_footer {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 12px;\n}\n.BalancePopover_updated {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_status {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n text-align: right;\n color: var(--dsw-alias-label-tertiary);\n}\n.BalancePopover_refresh {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 999px;\n corner-shape: round;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.BalancePopover_refresh:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.BalancePopover_refresh:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n.BalancePopover_refresh:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.BalancePopover_refresh[data-cooling] {\n color: var(--dsw-alias-label-caption);\n}\n.BalancePopover_refresh[data-refreshing] svg {\n animation: BalancePopover_balance-refresh-spin 900ms linear infinite;\n}\n@keyframes BalancePopover_balance-refresh-spin {\n to {\n transform: rotate(360deg);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .BalancePopover_refresh[data-refreshing] svg {\n animation-duration: 2400ms;\n }\n}\n\n/* src/client/sidebar/PercentRing.module.css */\n.PercentRing_ring {\n display: block;\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.PercentRing_ring[data-state=done] {\n color: var(--dsw-alias-state-success-primary);\n}\n.PercentRing_ring[data-state=warning] {\n color: var(--dsw-alias-state-warn-primary);\n}\n.PercentRing_ring[data-state=error] {\n color: var(--dsw-alias-state-error-primary);\n}\n.PercentRing_track {\n fill: none;\n stroke: var(--dsw-alias-border-l3);\n stroke-width: 2;\n}\n.PercentRing_fill {\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n}\n.PercentRing_cross {\n fill: none;\n stroke: currentColor;\n stroke-width: 1.5;\n stroke-linecap: round;\n}\n\n/* src/client/sidebar/SidebarBalance.module.css */\n.SidebarBalance_root {\n position: relative;\n display: flex;\n min-width: 0;\n}\n.SidebarBalance_root[data-mode=wide] {\n flex: none;\n width: calc(100% + 4px);\n margin: 4px -2px 0;\n}\n.SidebarBalance_root[data-mode=rail] {\n flex: none;\n margin: 8px 0 0;\n}\n.SidebarBalance_trigger {\n display: flex;\n align-items: center;\n box-sizing: border-box;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font-family: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n overflow: hidden;\n}\n.SidebarBalance_trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.SidebarBalance_trigger:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.SidebarBalance_root[data-mode=wide] .SidebarBalance_trigger {\n flex: none;\n gap: 8px;\n width: 100%;\n height: 42px;\n padding: 0 10px 0 8px;\n border-radius: 12px;\n}\n.SidebarBalance_root[data-mode=rail] .SidebarBalance_trigger {\n flex: none;\n justify-content: center;\n width: 36px;\n height: 36px;\n padding: 0;\n border-radius: 50%;\n corner-shape: round;\n}\n.SidebarBalance_icon {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n}\n.SidebarBalance_label {\n flex: 0 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.SidebarBalance_marker {\n display: inline-flex;\n flex: none;\n align-items: center;\n justify-content: center;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* src/client/sidebar/footer-stack.module.css */\ndiv:has(> [data-slot=\"sidebar.footer.action\"]) {\n flex-direction: column;\n}\n";
|
|
2110
2044
|
var __dsbTagId = "dsh-ds-balance/client.css";
|
|
2111
2045
|
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="' + __dsbTagId + '"]') === null) {
|
|
2112
2046
|
var __dsbTag = document.createElement('style');
|
package/lib/config.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ export declare const SETTINGS_NAMESPACE = "ds-balance";
|
|
|
12
12
|
export declare const CURRENCY_AUTO = "auto";
|
|
13
13
|
/** 官方凭据的默认引用名。与 `llm-deepseek` 的默认逐字相同。 */
|
|
14
14
|
export declare const DEFAULT_API_KEY_REF = "DEEPSEEK_API_KEY";
|
|
15
|
+
/**
|
|
16
|
+
* 生效的端点基址。
|
|
17
|
+
*
|
|
18
|
+
* **空串翻译成官方默认地址的唯一一处**:界面默认留空(官方「网页搜索」卡片同款),
|
|
19
|
+
* 用户填了就用填的。抓取与测连接都必须走这里,不许各自判空。
|
|
20
|
+
* @param config - 至少含 `baseUrl` 的配置。
|
|
21
|
+
* @returns 可以直接拼路径的基址。
|
|
22
|
+
*/
|
|
23
|
+
export declare function endpointOf(config: Pick<Config, 'baseUrl'>): string;
|
|
15
24
|
/** 超时常量。UI 不暴露,改环境变量即可。 */
|
|
16
25
|
export declare const DEFAULT_TIMEOUT_MS = 8000;
|
|
17
26
|
/** 覆盖超时的环境变量名。 */
|
|
@@ -27,7 +36,7 @@ export interface Config {
|
|
|
27
36
|
apiKey: string;
|
|
28
37
|
/** 凭据引用名。必须匹配 `^[A-Za-z_][A-Za-z0-9_]*$`。 */
|
|
29
38
|
apiKeyRef: string;
|
|
30
|
-
/**
|
|
39
|
+
/** 端点基址。**端点独立**,不继承对话适配器;**留空表示用官方默认地址**。 */
|
|
31
40
|
baseUrl: string;
|
|
32
41
|
/** 服务端刷新频率(秒)。 */
|
|
33
42
|
serverRefreshSeconds: number;
|
package/lib/config.js
CHANGED
|
@@ -13,6 +13,18 @@ export const SETTINGS_NAMESPACE = 'ds-balance';
|
|
|
13
13
|
export const CURRENCY_AUTO = 'auto';
|
|
14
14
|
/** 官方凭据的默认引用名。与 `llm-deepseek` 的默认逐字相同。 */
|
|
15
15
|
export const DEFAULT_API_KEY_REF = 'DEEPSEEK_API_KEY';
|
|
16
|
+
/**
|
|
17
|
+
* 生效的端点基址。
|
|
18
|
+
*
|
|
19
|
+
* **空串翻译成官方默认地址的唯一一处**:界面默认留空(官方「网页搜索」卡片同款),
|
|
20
|
+
* 用户填了就用填的。抓取与测连接都必须走这里,不许各自判空。
|
|
21
|
+
* @param config - 至少含 `baseUrl` 的配置。
|
|
22
|
+
* @returns 可以直接拼路径的基址。
|
|
23
|
+
*/
|
|
24
|
+
export function endpointOf(config) {
|
|
25
|
+
const url = config.baseUrl.trim();
|
|
26
|
+
return url === '' ? DEFAULT_BASE_URL : url;
|
|
27
|
+
}
|
|
16
28
|
/** 超时常量。UI 不暴露,改环境变量即可。 */
|
|
17
29
|
export const DEFAULT_TIMEOUT_MS = 8000;
|
|
18
30
|
/** 覆盖超时的环境变量名。 */
|
|
@@ -47,7 +59,7 @@ export const CONFIG_FIELDS = [
|
|
|
47
59
|
export const Config = z.object({
|
|
48
60
|
apiKey: z.string().role('secret').default(''),
|
|
49
61
|
apiKeyRef: z.string().role('credential-ref').default(DEFAULT_API_KEY_REF),
|
|
50
|
-
baseUrl: z.string().default(
|
|
62
|
+
baseUrl: z.string().default(''),
|
|
51
63
|
serverRefreshSeconds: z.natural().min(10).max(3600).default(60),
|
|
52
64
|
clientPollSeconds: z.natural().min(5).max(600).default(30),
|
|
53
65
|
manualRefreshCooldownSeconds: z.natural().min(0).max(600).default(30),
|
package/lib/http/handlers.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @module dsh-ds-balance/http/handlers
|
|
11
11
|
*/
|
|
12
12
|
import { classify } from '../domain/errors.js';
|
|
13
|
-
import { CONFIG_FIELDS } from '../config.js';
|
|
13
|
+
import { CONFIG_FIELDS, endpointOf } from '../config.js';
|
|
14
14
|
import { PLUGIN_VERSION, SCHEMA_VERSION } from '../version.js';
|
|
15
15
|
import { newRequestId, toWireBalanceView, toWireError, } from './wire.js';
|
|
16
16
|
/** `testConnection` 超时的可接受区间;越界一律回落默认值。 */
|
|
@@ -213,7 +213,7 @@ export async function handleTestConnection(request, deps) {
|
|
|
213
213
|
const body = await readJsonObject(request);
|
|
214
214
|
const config = deps.config.current();
|
|
215
215
|
const rawBaseUrl = body?.baseUrl;
|
|
216
|
-
const baseUrl = typeof rawBaseUrl === 'string' && rawBaseUrl.trim() !== '' ? rawBaseUrl.trim() : config
|
|
216
|
+
const baseUrl = typeof rawBaseUrl === 'string' && rawBaseUrl.trim() !== '' ? rawBaseUrl.trim() : endpointOf(config);
|
|
217
217
|
const rawTimeout = Number(body?.timeoutMs);
|
|
218
218
|
const timeoutMs = Number.isFinite(rawTimeout) && rawTimeout >= TEST_TIMEOUT_RANGE.min && rawTimeout <= TEST_TIMEOUT_RANGE.max
|
|
219
219
|
? Math.trunc(rawTimeout)
|
|
@@ -10,6 +10,7 @@ import { normalize } from '../domain/normalize.js';
|
|
|
10
10
|
import { pickBalance } from '../domain/select.js';
|
|
11
11
|
import { severityOf, thresholdsFor } from '../domain/severity.js';
|
|
12
12
|
import { noopMetrics } from '../ports/metrics.js';
|
|
13
|
+
import { endpointOf } from '../config.js';
|
|
13
14
|
import { accountTag8, computeAccountTag } from './account-tag.js';
|
|
14
15
|
/** 余额缓存与状态机。 */
|
|
15
16
|
export class BalanceService {
|
|
@@ -127,7 +128,7 @@ export class BalanceService {
|
|
|
127
128
|
const apiKey = await this.options.keys.resolve();
|
|
128
129
|
const config = this.options.config.current();
|
|
129
130
|
const raw = await this.options.client.fetchBalance({
|
|
130
|
-
baseUrl: config
|
|
131
|
+
baseUrl: endpointOf(config),
|
|
131
132
|
apiKey,
|
|
132
133
|
timeoutMs: this.options.config.timeoutMs(),
|
|
133
134
|
});
|
|
@@ -34,9 +34,9 @@ export declare const zh: {
|
|
|
34
34
|
readonly 'settings.group.refresh': "刷新";
|
|
35
35
|
readonly 'settings.group.display': "展示";
|
|
36
36
|
readonly 'settings.group.thresholds': "阈值";
|
|
37
|
-
readonly 'settings.field.apiKey': "API
|
|
37
|
+
readonly 'settings.field.apiKey': "API Key";
|
|
38
38
|
readonly 'settings.field.apiKeyRef': "API 密钥引用名";
|
|
39
|
-
readonly 'settings.field.baseUrl': "
|
|
39
|
+
readonly 'settings.field.baseUrl': "接口地址";
|
|
40
40
|
readonly 'settings.field.serverRefreshSeconds': "服务端刷新频率(秒)";
|
|
41
41
|
readonly 'settings.field.clientPollSeconds': "客户端轮询频率(秒)";
|
|
42
42
|
readonly 'settings.field.manualRefreshCooldownSeconds': "手动刷新冷却(秒)";
|
|
@@ -47,7 +47,7 @@ export declare const zh: {
|
|
|
47
47
|
readonly 'settings.field.usdCritical': "USD 告急";
|
|
48
48
|
readonly 'settings.hint.apiKey': "留空则改用引用名从凭据存储读取。";
|
|
49
49
|
readonly 'settings.hint.apiKeyRef': "凭据存储里的条目名;非空时优先于 API Key。";
|
|
50
|
-
readonly 'settings.hint.baseUrl': "
|
|
50
|
+
readonly 'settings.hint.baseUrl': "留空则使用提供方默认地址。";
|
|
51
51
|
readonly 'settings.hint.displayCurrency': "所选币种不存在时,回落到账户实际持有的币种。";
|
|
52
52
|
readonly 'settings.hint.number': "请填数字;留空表示使用默认值。";
|
|
53
53
|
readonly 'settings.hint.threshold': "后端判定;界面颜色跟随返回的严重度。";
|
|
@@ -56,11 +56,9 @@ export declare const zh: {
|
|
|
56
56
|
readonly 'settings.hint.clientPollSeconds': "刷新显示的间隔;只读本地缓存,不访问上游。";
|
|
57
57
|
readonly 'settings.hint.manualRefreshCooldownSeconds': "两次手动刷新之间的最短间隔。";
|
|
58
58
|
readonly 'settings.currency.auto': "自动(跟随账户)";
|
|
59
|
-
readonly 'settings.credential.
|
|
60
|
-
readonly 'settings.
|
|
61
|
-
readonly 'settings.hint.credential
|
|
62
|
-
readonly 'settings.hint.credential.notConfigured': "尚未配置。";
|
|
63
|
-
readonly 'settings.hint.credential.overridden': "使用自定义设置里的值,忽略继承。";
|
|
59
|
+
readonly 'settings.credential.configured': "已配置密钥。";
|
|
60
|
+
readonly 'settings.credential.notConfigured': "未配置密钥。";
|
|
61
|
+
readonly 'settings.hint.credential': "不写入设置文件。留空表示保持当前密钥。";
|
|
64
62
|
readonly 'settings.group.customized': "自定义设置";
|
|
65
63
|
readonly 'settings.test': "测试连接";
|
|
66
64
|
readonly 'settings.testing': "测试中…";
|
|
@@ -91,3 +91,15 @@ export declare function ageBucket(ageMs: number): {
|
|
|
91
91
|
bucket: AgeBucket;
|
|
92
92
|
value: number;
|
|
93
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* 当前年龄:上一次得知的年龄,加上此后流逝的时间。
|
|
96
|
+
*
|
|
97
|
+
* 基准必须是**收到那份响应的时刻**,不是组件挂载的时刻 ——
|
|
98
|
+
* 拿挂载时刻去换算 `ageMs` 的话,自动轮询带回来的新快照永远拨不回「刚刚」,
|
|
99
|
+
* 只有另记了时刻的手动刷新看起来才会动。
|
|
100
|
+
* @param seenAt - 收到那份响应的本地时刻(毫秒)。
|
|
101
|
+
* @param seenAgeMs - 那份响应里后端算好的年龄(毫秒)。
|
|
102
|
+
* @param now - 当前本地时刻(毫秒)。
|
|
103
|
+
* @returns 毫秒计的当前年龄;基准缺失时按 0 算。
|
|
104
|
+
*/
|
|
105
|
+
export declare function currentAgeMs(seenAt: number, seenAgeMs: number, now: number): number;
|
|
@@ -97,12 +97,14 @@ export declare function TextControl(props: TextControlProps): import("react").JS
|
|
|
97
97
|
/**
|
|
98
98
|
* 只读输入:字段照常渲染,但不可编辑,**框内不写任何文字**。
|
|
99
99
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
100
|
+
* 不从「隐藏字段」也不从「换一块只读文本」走:仍是真输入框,与官方凭据字段同一形态。
|
|
101
|
+
* **只给 `readOnly`,不给 `disabled`** —— 官方「网页搜索」卡片里配好的密钥就是一个常态空框
|
|
102
|
+
* (不降透明度),置灰只属于整份设置不可写的那种情况。
|
|
103
|
+
* 只读的因由由标签行右侧的状态徽章与它下方的说明行承担,那两处的措辞来自词典。
|
|
103
104
|
*
|
|
104
105
|
* **为什么没有 placeholder**:灰字写在框里读起来像「这里该填但没填」。
|
|
105
|
-
*
|
|
106
|
+
* **交互一律不响应**:它看着像输入框,但悬停不换描边、指针不是插入符、点了也没有焦点环
|
|
107
|
+
* (`inputStatic` 那条规则,含 `pointer-events: none`)—— 看着像能编辑才是错的。
|
|
106
108
|
*/
|
|
107
109
|
export interface ReadOnlyControlProps {
|
|
108
110
|
id: string;
|
|
@@ -129,25 +131,6 @@ export interface DetailsGroupProps {
|
|
|
129
131
|
* @returns 折叠块元素。
|
|
130
132
|
*/
|
|
131
133
|
export declare function DetailsGroup(props: DetailsGroupProps): import("react").JSX.Element;
|
|
132
|
-
/** 掩码输入:口令类型加一个显隐切换。 */
|
|
133
|
-
export interface SecretControlProps {
|
|
134
|
-
id: string;
|
|
135
|
-
text: string;
|
|
136
|
-
invalid: boolean;
|
|
137
|
-
disabled: boolean;
|
|
138
|
-
/** 当前是否明文显示。 */
|
|
139
|
-
revealed: boolean;
|
|
140
|
-
/** 切换显隐按钮的可访问名;语义状态走在 aria-pressed 上。 */
|
|
141
|
-
revealLabel: string;
|
|
142
|
-
onToggleReveal: () => void;
|
|
143
|
-
onEdit: (text: string) => void;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* 渲染掩码输入与它的显隐切换。
|
|
147
|
-
* @param props - 草稿文本、显隐状态与回调。
|
|
148
|
-
* @returns 输入行元素。
|
|
149
|
-
*/
|
|
150
|
-
export declare function SecretControl(props: SecretControlProps): import("react").JSX.Element;
|
|
151
134
|
/** 选择器的一个选项。 */
|
|
152
135
|
export interface SelectorOption {
|
|
153
136
|
readonly id: string;
|
|
@@ -177,7 +177,8 @@ export interface ConfigFormApi {
|
|
|
177
177
|
export declare const TEST_LATENCY_MS = 800;
|
|
178
178
|
/**
|
|
179
179
|
* 判定一次模拟连接测试是否失败。
|
|
180
|
-
* 规则:Base URL
|
|
180
|
+
* 规则:Base URL **留空合法**(走官方默认地址),非空时必须以 http:// 或 https:// 开头;
|
|
181
|
+
* API Key 与引用名不能同时为空。
|
|
181
182
|
* 返回的是宿主诊断风格的英文短句,与官方卡片直接展示宿主诊断的做法一致。
|
|
182
183
|
* @param baseUrl - 当前草稿或生效的 Base URL。
|
|
183
184
|
* @param apiKey - 当前草稿或生效的 API Key。
|
|
@@ -19,10 +19,8 @@ export interface BalancePopoverProps {
|
|
|
19
19
|
selection: CurrencySelection;
|
|
20
20
|
/** 用户在设置里选的币种,用于不匹配文案。 */
|
|
21
21
|
displayCurrency: string;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
/** 当前时刻(毫秒),由父组件按秒推进。 */
|
|
25
|
-
now: number;
|
|
22
|
+
/** 数据现在的年龄(毫秒)= 收到那份响应时后端报的年龄 + 此后流逝的时间。 */
|
|
23
|
+
ageMs: number;
|
|
26
24
|
/** 是否正在刷新。 */
|
|
27
25
|
refreshing: boolean;
|
|
28
26
|
/** 冷却剩余秒数;0 表示可以刷新。 */
|
package/lib/version.d.ts
CHANGED
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
/** 线上 schema 版本。响应里逐条回传,消费方据此判断兼容性。 */
|
|
9
9
|
export declare const SCHEMA_VERSION = 1;
|
|
10
10
|
/** 插件版本。与 `package.json` 的 `version` 一致(有测试兜底)。 */
|
|
11
|
-
export declare const PLUGIN_VERSION = "1.
|
|
11
|
+
export declare const PLUGIN_VERSION = "1.1.0";
|
|
12
12
|
//# sourceMappingURL=version.d.ts.map
|
package/lib/version.js
CHANGED