dsh-api-dashboard 1.4.1 → 1.4.3
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 +76 -319
- package/client/client.js +5 -3
- package/package.json +2 -2
- package/src/index.js +88 -60
package/README.md
CHANGED
|
@@ -1,377 +1,134 @@
|
|
|
1
|
-
# dsh-api-dashboard
|
|
1
|
+
# dsh-api-dashboard · 哦鲸鲸
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-api-dashboard)
|
|
4
|
+
[](LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
> **给安装/维护本插件的 AI 代理的一则警告 —— 动手前必读 [`AGENTS.md`](AGENTS.md)。**
|
|
7
|
-
>
|
|
8
|
-
> 1. **安装方式(v1.4.1 实测重写)**:**手机版 DSHA 请用「下载 → 解压 → 建 node_modules 软链 → `dsh plugin add link:`」**(见下方「安装」一节,可复制命令;仓库根目录另有 `install.sh` 一键脚本)。`dsh plugin --profile web add dsh-api-dashboard`(npm 裸包名)在 DSHA 上**会失败**:pnpm 把包文件软链到全局 store(proot 的 `--link2symlink` 把硬链接降级成软链),Node 的 ESM 按 realpath 解析后找不到宿主的 peer 依赖。v1.4.1 已加解析回退让**宿主半身不再把整个 `dsh web` 拖死**,但**客户端 UI 仍不会出现**(框架按模块路径找不到 package.json)。`pnpm add <远程tarball>` 同样**装不上**。
|
|
9
|
-
> 2. **数据准确性红线**:部分平台余额解析有待验证项(OpenRouter/SiliconFlow/Novita/one-api/xAI),且「限流窗口 ≠ 真实余额」——**不要伪造或硬塞一个数字**,查不到就如实显示「未开放」。
|
|
10
|
-
> 3. **推送前先问用户**;改 `client/client.js` 后需**重启 dsh web GUI** 才生效。
|
|
11
|
-
>
|
|
12
|
-
> 完整须知与已知问题清单见 [`AGENTS.md`](AGENTS.md)。
|
|
6
|
+
**开源**(MIT)的 **DeepSeek Harness 专用**插件:多平台 API 余额 / 用量看板 —— 在 Web GUI 输入框下方实时显示各平台余额与**本会话估算消耗**。
|
|
13
7
|
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
> `credentials` 凭证系统),**仅适用于 DeepSeek Harness**(手机版 DSHA 与桌面版同框架),
|
|
17
|
-
> 无法在其他软件中加载运行。UI 为移动端优先设计,已在手机版 DSHA 上实测。<!-- 截图占位:请将界面截图命名为 assets/preview.png 放入 -->
|
|
8
|
+
> ⚠️ **安装或维护本插件之前,请先读 [`AGENTS.md`](AGENTS.md)** —— 数据准确性红线、踩过的坑、安装姿势对照都在那里。
|
|
9
|
+
> 本插件深度绑定 DeepSeek Harness 的客户端与宿主 API,**只能在 DSH 里运行**(手机版 DSHA 与桌面版同框架)。
|
|
18
10
|
|
|
19
|
-
##
|
|
11
|
+
## 界面预览
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
- **一层 状态条**:输入框下方显示 `[品牌图标] 平台名 余额 🟢 ☀️/🌙`,右侧有设置齿轮
|
|
23
|
-
- **二层 看板**:底部抽屉,DeepSeek 单独第一 → 国内平台 → 海外平台 → 中转站(默认折叠)
|
|
24
|
-
- **三层 详情**:点卡片右侧 `ⓘ` 看总余额/总充值/总使用/会话消耗/峰谷趣味卡片
|
|
25
|
-
|
|
26
|
-
### 峰谷趣味计费
|
|
27
|
-
- **梁文峰 ☀️**:工作日 09:00-12:00 / 14:00-18:00 峰时
|
|
28
|
-
- **梁文谷 🌙**:其余时间 + 周末全天,5折特惠
|
|
29
|
-
- 详情页有峰谷趣味卡片 + 俏皮话
|
|
30
|
-
|
|
31
|
-
### 三色阈值灯
|
|
32
|
-
- **绿色**:余额 > 安全阈值(默认 50)
|
|
33
|
-
- **黄色**:安全阈值 > 余额 > 预警阈值(默认 10)
|
|
34
|
-
- **红色**:余额 < 预警阈值
|
|
35
|
-
|
|
36
|
-
### 余额告警通知 (v0.4.0)
|
|
37
|
-
- 余额首次低于预警阈值时自动推送通知 (黄色预警)
|
|
38
|
-
- 余额首次低于危险阈值时高优先级推送 (红色警告)
|
|
39
|
-
|
|
40
|
-
### 扩展模型价格表 (v0.4.0)
|
|
41
|
-
- 补充 30+ 主流模型单价 (OpenAI / Claude / Gemini / 智谱 / 通义 / Kimi / 阶跃等)
|
|
42
|
-
- 会话消耗估算更准确 (支持前缀匹配)
|
|
43
|
-
|
|
44
|
-
### 完整设置面板
|
|
45
|
-
- 安全阈值 / 预警阈值 / 计价货币 / 自定义中转站管理
|
|
46
|
-
- **海外模型计价货币可独立选择 (v1.3.2)**:跟随主货币 / 美元 / 人民币。
|
|
47
|
-
海外厂商官方价本就是 USD,选美元可免去 ×7 折算误差;混合会话按币种两段显示 `~¥3.40+$12.50`,不折算合并
|
|
48
|
-
- 设置齿轮直接出现在状态条右侧
|
|
49
|
-
|
|
50
|
-
### 自动刷新 (v0.4.4, v1.4.0 起下限 1 秒 + 首屏不阻塞)
|
|
51
|
-
- 服务端定时拉取余额 → 前端轮询读取 → 手动强刷 → 页面回到前台刷新
|
|
52
|
-
- **自定义刷新间隔**:设置面板可调 **1~60 秒**
|
|
53
|
-
- **首屏/切回前台不阻塞**(stale-while-revalidate):先把服务端手上的数据立刻给你,刷新丢后台 —— 不必再等一次全量轮询(最慢的端点可以拖满 8 秒超时)
|
|
54
|
-
|
|
55
|
-
### 自定义模型余额显示 (v0.4.4)
|
|
56
|
-
- 用户自建余额接口(返回 JSON)即可接入:填接口 URL + 可选 API Key
|
|
57
|
-
- 解析方式:自动探测 / OpenAI / OneAPI·NewAPI quota / DeepSeek / **手动映射**(点分路径如 `data.balance`)
|
|
58
|
-
- 显示在独立「自定义」分组
|
|
59
|
-
|
|
60
|
-
### 配置持久化 (v0.4.5)
|
|
61
|
-
- 设置面板保存的配置(刷新时间/自定义模型/中转站/阈值/币种)自动写入本地状态文件,**重启不丢失**
|
|
62
|
-
- 状态文件位于 DSH 数据目录(`~/.dsh/dsh-api-dashboard.json`),**不随插件发布,不进版本库**
|
|
63
|
-
|
|
64
|
-
### UI 优化 (v0.4.4 / v0.4.6)
|
|
65
|
-
- 分类分组展开/收起过渡动画(GPU 友好:transform/opacity,无重排)
|
|
66
|
-
- 分类彩色圆点(国内/海外/本地/中转站/自定义)
|
|
67
|
-
- 设置面板三段页签导航;毛玻璃现代化界面
|
|
13
|
+
**余额条**常驻在输入框下方;会话里有子代理时,下面会多一行**可横滑的消耗胶囊**(顺序 = 创建顺序,长按看详情)。
|
|
68
14
|
|
|
69
|
-
|
|
15
|
+
<a href="docs/screenshots/1-balance-bar.webp"><img src="docs/screenshots/1-balance-bar.webp" width="100%" alt="输入框下方的余额条"></a>
|
|
16
|
+
<a href="docs/screenshots/2-subagent-row.webp"><img src="docs/screenshots/2-subagent-row.webp" width="100%" alt="子代理消耗行"></a>
|
|
17
|
+
|
|
18
|
+
| 看板(点余额条打开) | 大肥鱼挂件(可拖拽 / 吸附边缘) |
|
|
19
|
+
|:---:|:---:|
|
|
20
|
+
| <img src="docs/screenshots/3-dashboard.webp" width="300" alt="看板"> | <img src="docs/screenshots/6-whale-tab.webp" width="300" alt="大肥鱼"> |
|
|
21
|
+
| **设置 · 基础** | **设置 · 大肥鱼** |
|
|
22
|
+
| <img src="docs/screenshots/4-settings-basic.webp" width="300" alt="设置基础"> | <img src="docs/screenshots/5-settings-whale.png" width="300" alt="设置大肥鱼"> |
|
|
70
23
|
|
|
71
|
-
|
|
24
|
+
## 功能
|
|
25
|
+
|
|
26
|
+
- **三层 UI**:状态条(输入框下方)→ 看板抽屉(点状态条)→ 平台详情(点卡片 ⓘ)
|
|
27
|
+
- **峰谷趣味计费**:DeepSeek 工作日 9:00-12:00 / 14:00-18:00 显示 ☀️ 梁文峰,其余时间与周末 🌙 梁文谷(半价)
|
|
28
|
+
- **三色阈值灯**:绿 / 黄 / 红,阈值可自定义;余额首次跌破预警线自动推送通知
|
|
29
|
+
- **会话消耗估算**:按模型单价折算;混合币种两段显示 `~¥3.40+$12.50`,**不做汇率折算合并**
|
|
30
|
+
- **子代理消耗**:子代理单独成行、可横滑,长按看完整详情
|
|
31
|
+
- **大肥鱼挂件**(可选):屏幕边缘一只可拖拽的互动挂件,纯娱乐,不显示任何金额
|
|
32
|
+
- **自定义接入**:中转站自动探测余额接口;自建接口支持点分路径手动映射
|
|
33
|
+
- **配置持久化**:阈值 / 币种 / 刷新间隔(1~60 秒)/ 自定义条目都存本地状态文件,重启不丢
|
|
34
|
+
- **一键自更新**:设置面板内检查并安装新版本(下载 → 校验 → 备份 → 替换 → 失败回滚)
|
|
72
35
|
|
|
73
|
-
|
|
74
|
-
DeepSeek、智谱GLM、Kimi、阶跃星辰、硅基流动、MiniMax
|
|
36
|
+
## 支持平台
|
|
75
37
|
|
|
76
|
-
|
|
77
|
-
|
|
38
|
+
| 分组 | 平台 |
|
|
39
|
+
|---|---|
|
|
40
|
+
| 国内 | DeepSeek、智谱 GLM、Kimi、阶跃星辰、硅基流动、MiniMax |
|
|
41
|
+
| 海外 | OpenRouter、Novita AI、xAI Grok |
|
|
42
|
+
| 其他 | 自定义中转站(自动探测余额接口)、自定义模型(手动映射) |
|
|
78
43
|
|
|
79
|
-
|
|
80
|
-
|
|
44
|
+
> OpenAI / Claude / Gemini / Groq / Mistral / Together 等**未开放余额查询接口**,面板会如实显示「未开放」,
|
|
45
|
+
> **不会伪造数字**。智谱按量付费账户同样没有公开余额接口(显示中性「未开放」,不标红)。
|
|
81
46
|
|
|
82
47
|
## 安装
|
|
83
48
|
|
|
84
|
-
###
|
|
49
|
+
### 手机版 DSHA(Android)—— 实测可用,照抄即可
|
|
85
50
|
|
|
86
51
|
```sh
|
|
87
|
-
# 1.
|
|
52
|
+
# 1. 下载源码
|
|
88
53
|
curl -L "https://codeload.github.com/133563825as-ai/dsh-api-dashboard/tar.gz/refs/heads/main" \
|
|
89
54
|
-o /tmp/dsh-api-dashboard.tar.gz
|
|
90
55
|
|
|
91
|
-
# 2. 解压到固定位置(--strip-components=1
|
|
56
|
+
# 2. 解压到固定位置(--strip-components=1 必须带)
|
|
92
57
|
rm -rf /root/dsha-api-dashboard && mkdir -p /root/dsha-api-dashboard
|
|
93
58
|
tar xzf /tmp/dsh-api-dashboard.tar.gz -C /root/dsha-api-dashboard --strip-components=1
|
|
94
59
|
|
|
95
|
-
# 3. 建 node_modules 软链 ——
|
|
96
|
-
# 插件的 peer 依赖(@deepseek-ai/schemastery / zod)由宿主 DSH 提供,
|
|
97
|
-
# 软链让它从源码目录就能解析到它们。
|
|
60
|
+
# 3. 建 node_modules 软链 —— 这一步不能省(插件的 peer 依赖由宿主 DSH 提供)
|
|
98
61
|
ln -sfn /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules /root/dsha-api-dashboard/node_modules
|
|
99
62
|
|
|
100
|
-
# 4. 装进 web profile
|
|
63
|
+
# 4. 用 link: 装进 web profile(文件保持真实路径,客户端 UI 才能被框架找到)
|
|
101
64
|
dsh plugin --profile web add link:/root/dsha-api-dashboard
|
|
102
65
|
|
|
103
66
|
# 5. 重启 dsh web
|
|
104
67
|
```
|
|
105
68
|
|
|
106
|
-
> 仓库根目录的 `install.sh` 把上面 5
|
|
69
|
+
> 仓库根目录的 [`install.sh`](install.sh) 把上面 5 步做完了:`sh install.sh`。
|
|
107
70
|
|
|
108
|
-
|
|
71
|
+
**桌面版**同理(路径换成你自己的)。npm 一行命令 `dsh plugin --profile web add dsh-api-dashboard`
|
|
72
|
+
在桌面版通常可用,但在**手机版 DSHA 上装完客户端 UI 不会出现** —— 原因与逐条实测对照见
|
|
73
|
+
[`AGENTS.md`](AGENTS.md#二安装须知ai-代理注意)。
|
|
109
74
|
|
|
110
|
-
|
|
111
|
-
npm 一行命令 `dsh plugin --profile web add dsh-api-dashboard` 在**桌面版**上通常可用
|
|
112
|
-
(桌面文件系统支持硬链接,pnpm 不会把包文件软链到 store);但 v1.4.1 起仍建议用 `link:`,
|
|
113
|
-
因为自更新与资产路径都按真实路径工作。
|
|
75
|
+
### 升级
|
|
114
76
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
1. **推荐**:重新执行上面第 1、2 步(覆盖源码目录)→ 重启 `dsh web`;
|
|
118
|
-
2. 插件设置面板里的「一键自更新」也可用(下载→校验→备份→替换→失败回滚,重启生效);
|
|
119
|
-
⚠️ 它会**保留 `.git` 与 `node_modules`**(v1.4.1 修复:此前会把 `.git` 一起删掉,历史无法恢复);
|
|
77
|
+
1. 插件设置面板 →「检查更新」→「一键自更新」,重启 `dsh web` 生效;
|
|
78
|
+
2. 或重跑上面第 1、2 步覆盖源码目录后重启;
|
|
120
79
|
3. 或 `dsh plugin --profile web remove dsh-api-dashboard` 后重新安装。
|
|
121
80
|
|
|
122
|
-
### ⚠️ 安装姿势对照(2026-09-11 在 DSHA 上逐条实测)
|
|
123
|
-
|
|
124
|
-
| 方式 | 结果 |
|
|
125
|
-
|------|------|
|
|
126
|
-
| 源码 + `node_modules` 软链 + `dsh plugin add link:<dir>` | ✅ **可用**(推荐) |
|
|
127
|
-
| 包目录放在 `$DSH_HOME/profiles/` 之下再 `add file:` | ✅ 可用 |
|
|
128
|
-
| `dsh plugin --profile web add dsh-api-dashboard`(npm 裸包名,**手机版 DSHA**) | ❌ 客户端 UI 不会出现;v1.4.1 之前更糟 —— **整个 `dsh web` 启动失败** |
|
|
129
|
-
| `dsh plugin --profile web add file:/源码目录`(目录里**没有** node_modules) | ❌ 同上 |
|
|
130
|
-
| `pnpm add <远程 tarball URL>` | ❌ pnpm 不剥离 codeload 压缩包顶层目录,装出来是空壳 |
|
|
131
|
-
| 手动软链整个插件目录到 `node_modules` | ⚠️ 会被启动校准摘除,别用;软链 `node_modules` 子目录是**必须**的 |
|
|
132
|
-
|
|
133
81
|
## 配置
|
|
134
82
|
|
|
135
|
-
|
|
136
|
-
|
|
83
|
+
在设置面板里调即可(保存后写入 `~/.dsh/dsh-api-dashboard.json`)。
|
|
84
|
+
也可以直接写 `cordis.patch.yml`:
|
|
137
85
|
|
|
138
86
|
```yaml
|
|
139
87
|
- id: dsh-api-dashboard
|
|
140
88
|
config:
|
|
141
|
-
safeThreshold: 50
|
|
142
|
-
warnThreshold: 10
|
|
143
|
-
currency: CNY
|
|
144
|
-
|
|
145
|
-
overseasCurrency: follow
|
|
89
|
+
safeThreshold: 50 # 绿色阈值
|
|
90
|
+
warnThreshold: 10 # 黄色阈值(低于它变红)
|
|
91
|
+
currency: CNY # 主计价货币
|
|
92
|
+
overseasCurrency: USD # 海外模型计价:follow | USD | CNY
|
|
146
93
|
refreshIntervalMs: 300000
|
|
147
94
|
clientPollIntervalMs: 30000
|
|
148
95
|
timeoutMs: 8000
|
|
149
96
|
```
|
|
150
97
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
- `SILICONFLOW_API_KEY` / `MINIMAX_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY`
|
|
155
|
-
- `XAI_API_KEY`
|
|
98
|
+
**API Key 自动读取**,无需写进配置:先查环境变量(`DEEPSEEK_API_KEY` / `ZHIPU_API_KEY` / `MOONSHOT_API_KEY` /
|
|
99
|
+
`STEPFUN_API_KEY` / `SILICONFLOW_API_KEY` / `MINIMAX_API_KEY` / `OPENROUTER_API_KEY` / `NOVITA_API_KEY` / `XAI_API_KEY`),
|
|
100
|
+
再查 DSH 凭证系统 `~/.dsh/.credentials.yaml`。
|
|
156
101
|
|
|
157
102
|
### 官方直连 / 中转站判定
|
|
158
103
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
1. **用户显式名单**:设置面板「基础设置 → 官方直连 provider」多行框,逗号/换行分隔。
|
|
162
|
-
写在这里的 provider 名**一律按官方直连**处理,覆盖下面两层的一切误判。
|
|
163
|
-
2. **baseURL 域名**:服务端读 `settings.yaml` 里 `llm-pi-ai.providers.<name>.baseURL`,
|
|
164
|
-
按**域名**比对官方端点白名单(不是比对 provider 名)。命中→官方,不命中→中转站。
|
|
165
|
-
**没写 baseURL 的 provider 不表态**,交给下一层。
|
|
166
|
-
3. **命名约定**:DSH 官方插件注册的 provider 带 `-official` / `_official` 后缀
|
|
167
|
-
(如 `deepseek-official`),按官方直连。
|
|
168
|
-
|
|
169
|
-
三层都不命中时**默认按中转站**处理(保守取向:宁可不显示余额,也不显示错的余额)。
|
|
170
|
-
设置面板会在输入框下方列出当前的**自动判定结果**标签,哪些 provider 判成「官方」、哪些判成「中转」,一眼就能看出还需不需要手动填写覆盖。
|
|
171
|
-
|
|
172
|
-
## 文件结构
|
|
173
|
-
|
|
174
|
-
```
|
|
175
|
-
dsh-api-dashboard/
|
|
176
|
-
├── src/index.js # 服务端:余额查询引擎、峰谷计费、会话消耗投影、配置持久化
|
|
177
|
-
├── client/client.js # 客户端:三层 UI、官方 SVG 图标、设置面板、分组动画
|
|
178
|
-
├── assets/icons/ # 统一风格纯图形品牌官方图标
|
|
179
|
-
├── cordis.patch.yml # 插件配置补丁
|
|
180
|
-
├── package.json # 包配置
|
|
181
|
-
├── LICENSE # MIT
|
|
182
|
-
└── .gitignore # 排除 node_modules 与本地状态
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## 安全说明
|
|
186
|
-
|
|
187
|
-
- **API Key 不写入代码/仓库**:各平台 Key 从环境变量或 DSH 凭证系统读取
|
|
188
|
-
- **接口掩码**:`/api-dashboard/config` 返回的 Key 一律显示为 `***`,保存时服务端按 id 保留原值
|
|
189
|
-
- **客户端不外发**:浏览器端只请求同源的 `/api-dashboard/*`,不向任何第三方域名发送数据
|
|
190
|
-
- **本地状态文件**:设置面板保存的中转站/自定义模型 Key 写入 `~/.dsh/dsh-api-dashboard.json`
|
|
191
|
-
(权限 0600),该文件被 `.gitignore` 排除,**不随仓库分发**
|
|
192
|
-
- **自定义接口提示**:自定义模型/中转站的 API Key 会随请求发往**你填写的接口地址**,
|
|
193
|
-
请确保只填自己信任的服务地址
|
|
194
|
-
|
|
195
|
-
## 开发说明
|
|
196
|
-
|
|
197
|
-
- 服务端:`src/index.js`(ESM,零构建)
|
|
198
|
-
- 客户端:`client/client.js`(手写 CJS 工厂格式,修改后需重启 dsh web)
|
|
199
|
-
- 图标:15个纯图形 24x24 SVG,来自 Simple Icons / Iconify
|
|
200
|
-
|
|
201
|
-
## 版本
|
|
202
|
-
|
|
203
|
-
v1.4.1 — **审计修复版**(全部先在真机/隔离环境复现再修,新增 `test/test-v141-fixes.mjs` 62 条回归钉子):
|
|
204
|
-
🔴 **状态文件形状损坏不再拖死整个 GUI** —— `{"customRelays": 5}` 这类「合法 JSON 但字段类型跑偏」以前会让 `apply()` 抛 `TypeError`,进而**整个 `dsh web` 启动失败**(不是插件不显示,是 GUI 打不开)。现在加载时统一消毒形状并夹取数值范围。
|
|
205
|
-
🔴 **一键自更新不再删 `.git`** —— codeload 的 tarball 里没有 `.git`,旧代码 `rmSync` 掉除 `node_modules` 外的一切再覆盖,点一次更新就抹掉 git 历史且无法恢复。现在保留 `.git`;`~/dsha-api-dashboard` 的自动同步**只对非 git 工作区生效**;target 是符号链接时直接拒绝(旧行为会删光软链指向的真实目录,且回滚不回来)。
|
|
206
|
-
🔴 **插件路由补鉴权** —— `/api-dashboard/*` 以前完全绕过 DSH 的浏览器鉴权:无 token、无 cookie 就能读配置/改配置/触发自更新;且 `Content-Type: text/plain` 属于浏览器**不预检的 simple request**,实测带 `Origin: https://evil.example` 的跨域 POST 返回 **200 且配置真的被改了**。现在复用 `connection.requestRejection`(与 `dsh-web-mobile` 同一闸门):无 cookie → 401、跨域 → 403。
|
|
207
|
-
🔴 **peer 依赖解析回退** —— npm 安装后宿主半身不再 `Cannot find package '@deepseek-ai/schemastery'`;另修 `SELF_ROOT` 走 realpath 导致 npm 布局下图标/挂件资产 404、版本号读不到的问题。
|
|
208
|
-
🐛 **「刷新间隔 1 秒」真正生效** —— 输入框与服务端都允许 1 秒,只有 `save()` 悄悄夹成 5 秒;旧回归断言钉的是**旧变量名**,换个名字就绕过(假绿)。四处下限现已一致。
|
|
209
|
-
🐛 **冷启动打开设置面板不再覆盖用户配置** —— 面板以前只从 `/balances` 拿阈值/币种,冷启动要等 8~10s,那期间点「保存并生效」会把真实阈值写成默认 50/10、把 `overseasCurrency` 写回 `'follow'`。`/config` 现在返回阈值,客户端也补读。
|
|
210
|
-
🐛 **`openai` 分支不再伪造负数余额**(`total_granted` 为 null/非数字时 `0 - used` → 面板红色「-5」),与已修的 `openrouter` 同类。
|
|
211
|
-
🐛 **冷启动不再「等半天 + 报失败」** —— 实测维护者真实配置下一次全量刷新要 **8~13.6 秒**(5 个中转站的 auto 探测是串行试 3 个候选端点,每个跑满 8s)。
|
|
212
|
-
v1.4.1 第一版给 fetch 统一加了 15s 超时,正好卡在这中间 → **冷启动首屏必超时 → 面板显示「余额接口请求失败」**。现已:① 余额端点超时放宽到 90s(超时只用来兜底卡死的 socket,不是给正常慢启动设上限);② 冷启动改成**后台刷新**——服务端立刻回 `loading:true` + 空列表,客户端保持骨架屏并把轮询压到 1.5 秒,数据一到就上屏(实测前 5 发 2~3ms 返回,第 6 发拿到数据);③ 加载超 4 秒补一句「首次加载要逐个平台拉取余额…请稍候」;④ **记住每个中转站上次命中的余额端点**并优先试它(存状态文件 `relayEndpoints`,重启后仍生效),稳态下每个中转站只打 1 个请求。
|
|
213
|
-
🐛 **错误路径有了出口** —— fetch 全部加 15s 超时(以前一个 `AbortController` 都没有,请求挂住就永久骨架屏且从此不再轮询);抽屉在请求失败时显示错误文案 + 「重试」按钮(以前只画空看板,`snapshot.message` 从不渲染);保存与配置请求补 `catch`。
|
|
214
|
-
🐛 **状态条金额三色类名补上 CSS**(`.dshadb_bar_ok/warn/err` 以前只有使用处、没有规则 = 死类名,负余额金额从不标红)。
|
|
215
|
-
🐛 **子代理胶囊监听器不再累加**(内联 ref 使每次 render 都挂 7 个 `addEventListener`,5 秒轮询下每小时约 5000 个)。
|
|
216
|
-
🐛 **强刷加 2 秒节流 + 响应序列号**(连点刷新会并发,且旧响应会覆盖新响应造成数据倒退)。
|
|
217
|
-
🐛 **中文名不再被写坏**:请求体改为按字节收集后整体 `utf8` 解码(跨 chunk 切分多字节字符会产生 U+FFFD 并存进状态文件)。
|
|
218
|
-
🐛 持久化的 `refreshIntervalMs` 加载时夹取(曾经能是 -1 → 3 秒内 1794 次上游请求);状态文件保存前建父目录,失败时如实上报。
|
|
219
|
-
🐛 **设置面板开着时大肥鱼可以拖动了** —— 旧逻辑把「设置面板」也算进 `overlayOpen`,一进设置挂件立刻被 `pointer-events:none` 锁死,必须关掉才能拖(维护者反馈)。而设置里的「大肥鱼」页恰恰是要边看实时预览边拖位置/调大小的地方。现在只对会盖住挂件的**看板 / 详情**两个整屏抽屉上锁。
|
|
220
|
-
🎨 **UI:余额条恢复成独立药丸,整块水平居中**(子代理行同样居中;不再塞进一个容器)。子代理行真正溢出时右侧渐隐,不再被屏幕齐口切一半。
|
|
221
|
-
⚠️ **已知未修**:会话消耗按「查看时刻」的峰谷单价重算(同一会话在峰谷切换时会显示 1×/2× 两个数)。要修得在事件折叠时按 `isPeakTime(事件时刻)` 分桶存储并 bump `stateVersion`,属投影状态结构变更,留待下个版本。
|
|
222
|
-
v1.4.0 — 价格表改原生币种 + 会话消耗漏计/前缀吞模型修复 + 子代理消耗可见 + DSH provider 自动入列 + 设置界面统一 + 三处交互修复
|
|
223
|
-
v0.2.0 — 最初版本
|
|
224
|
-
v0.2.1 — 修复图标映射,加入 DeepSeek 峰谷计费
|
|
225
|
-
v0.3.0 — 完整三层 UI,国内外分组,三色阈值,趣味峰谷,设置面板
|
|
226
|
-
v0.4.0 — 余额告警通知, 扩展模型价格表, 毛玻璃现代化 UI, 进入页面自动刷新, 自定义安全/预警阈值
|
|
227
|
-
v0.4.1 — 移除余额趋势图, 修复 percent 类平台阈值不跟自定义, 首次进入/回前台自动强制刷新
|
|
228
|
-
v0.4.3 — 修复本会话消耗投影(框架投影 API 形状:stateSchema + wire)
|
|
229
|
-
v0.4.4 — 自定义模型余额显示, 自定义刷新时间(5~60s), 分组过渡动画, 分类色点, 设置三段页签
|
|
230
|
-
v0.4.5 — 配置持久化(保存后重启不丢), 插件迁移标准安装位置
|
|
231
|
-
v0.4.6 — 动画流畅度优化(去重排, blur 降档, GPU 友好过渡)
|
|
232
|
-
v0.5.0 — 手机端性能适配:去 backdrop-filter、静态指示器、44px 热区、ETag 协商缓存、快照级重渲控制
|
|
233
|
-
v0.5.1 — iOS 18 柔和色板, 峰谷标记平涂化, 背景层次跟随主题 token, 桌面端 hover 特效
|
|
234
|
-
v0.5.2 — HEAD 响应 ETag 顺序修复
|
|
235
|
-
v0.5.3 — 模型切换自动联动平台; 修复 deepseek-chat/reasoner 价格被 v4 峰谷表劫持(计费虚高)
|
|
236
|
-
v0.5.4 — 详情页回退底部抽屉交互
|
|
237
|
-
v0.5.5 — 智谱 GLM Coding 套餐配额解析(open.bigmodel.cn + CREDIT_LIMIT); 配色统一跟随主题; DeepSeek 峰谷价格卡
|
|
238
|
-
v0.5.6 — 余额展示纠错(去重充值/零使用行), 货币金额不再误报异常红; 价格接口收敛至 v4 两档
|
|
239
|
-
v0.5.7 — 平台瞬时错误 last-known-good 兜底, 智谱异常闪现修复
|
|
240
|
-
v0.5.8 — 本会话消耗并入状态胶囊
|
|
241
|
-
v0.5.9 — 详情页重设计:大号余额 + 三列指标卡
|
|
242
|
-
v0.5.16 — 开源发布安全加固(.gitignore 排除凭证/备份/环境文件)
|
|
243
|
-
v0.6.0 — 内置检查更新 + 一键自更新:启动自动检查远端版本(5 分钟节流),有新版可在设置中一键下载安装(自动备份、校验、失败回滚,重启 Web GUI 生效)
|
|
244
|
-
v0.6.1 — 修复打开设置后页签被轮询拽回「基础设置」、编辑中表单被打回(初始化 effect 仅在弹窗打开瞬间执行一次)
|
|
245
|
-
v0.6.2 — 检查更新补全反馈:无论「已是最新」还是「有新版」或网络失败,检查完都给出明确提示;弹窗初值直接复用预热结果避免闪烁
|
|
246
|
-
v0.7.0 — 玻璃 UI 改造(方案 A 第一阶段):半屏抽屉玻璃拟态(blur 仅限抽屉主背景)、卡片/按钮/输入框半透明+高光、大圆角、输入框 focus 高光阴影;用 color-mix 让玻璃色跟随主题 token(暗色不发白)
|
|
247
|
-
v0.7.1 — 修复负余额显示正常(负数→红);智谱解析保留百分比(不把限流窗口当余额);设置面板新增「返回」按钮与 IconBack;新增 AGENTS.md(给维护 AI 的必读须知)+ README 顶部 AI 警告块
|
|
248
|
-
v1.0.0 — 正式版:插件改名「哦鲸鲸」+ 自定义图标(/api-dashboard/icon);全量 UI 改版(固定色值去主题变量、三段式头部、卡片右栏上下结构、选中模型置顶、分组折叠、详情/设置/版本更新区重排);若干样式补全与清理
|
|
249
|
-
v1.0.2 — 余额解析稳健性(平台字段存在性校验,缺字段时如实显示「无法解析/未开放」而非误报 0);智谱配额解析重写(按 `limits.unit` 区分 5 小时窗口、周配额、工具维度,`percentage` 修正为「已用/填充度」,不再把「已用完」误显示为「余额 100%」);价格表更新(DeepSeek V4 新增 `deepseek-v4-flash-vision-exp`、`deepseek-chat/reasoner/r1` 标记为已退役;通用价格表新增现役主力 OpenAI GPT-5.6 / Claude 4.x / Gemini 3.x / Kimi K3 / StepFun 3.x,旧模型标为历史参考);自更新下载包增加包名校验
|
|
250
|
-
v1.0.3 — 文档更新:安装与升级指引改为以 npm 一行命令为主、源码方式为备选,并补充升级路径;无功能变更
|
|
251
|
-
v1.1.0 — 🐋 新增「收养大肥鱼」互动挂件:设置 → 基础设置 → 开启后屏幕边缘探出一只大肥鱼(露出约一半),点击弹全身 + 随机台词泡泡(含峰谷时段文案、gif、卖萌吐槽)+ 按压 Q 弹与音效;**可全屏拖拽**,松手自动吸附最近边缘(挂件菜单可关掉「自动靠边」改为自由摆放),左侧吸附时整体镜像翻转;挂件自带汉堡菜单(大小 0.6~2.5 倍 / 探出比例 / 音效开关 / 音效组 / 音量 / 气泡开关 / 峰谷文案 / 自动靠边),位置与全部设置持久化;开关即拨即存(不必点保存);纯互动不显示任何余额与消耗(那部分照旧在输入框下方);互动部分移植自 [MeteorNOX/DeepSeek-Balance-Whale-Widget](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget)(MIT License, Copyright (c) 2026 MeteorNOX),许可副本见 `assets/whale/LICENSE-whale-widget.txt`
|
|
252
|
-
v1.1.2 — 大肥鱼交互打磨:设置面板新增独立「大肥鱼」页签,总开关与全部细项(大小 / 探出 / 自动靠边 / 气泡 / 峰谷文案 / 音效 / 音效组 / 音量)集中到这一页,全部即改即存无需点保存,并去掉挂件本体右上角的浮层菜单;开关行图标改用鲸鱼本体缩略图;收回改为**停手 3 秒**自动缩回(连续点击持续续命,不再"点第二下立刻收回");台词气泡改为运行时避让,贴边时不再有一半在屏幕外,上方空间不足自动翻到下方;吸附收窄为**只有左右两侧边缘带**触发,停在屏幕中间保持整只显示;贴边时**纵向滑动不再展开**,沿边上下挪位,横向拉开才脱轨;连点节流 420ms,快速点击气泡稳定不闪
|
|
253
|
-
v1.1.3 — 开源化改造 + 全量纠错:**provider 官方/中转三层判定**(设置面板可显式声明「官方直连 provider」覆盖自动判定;服务端读 settings.yaml 按 baseURL 域名比对官方白名单;`-official` 后缀兜底;都不命中默认按中转站显示「—」,不再靠硬编码 provider 名猜);承接 28 项 bug 修复(畸形响应、时区峰谷、前缀匹配吞 0、负余额红显、字段存在性校验等);清理死代码(JS 孤立符号 + 18 条 CSS);新增无余额模型品牌分组(OpenAI/Claude/Gemini/Qwen/MiMo,默认隐藏可开关);更新价格表并统一 USD 基准(GPT-5.6 系列 / Claude 5 / Gemini 3.x / Qwen3.x / GLM-5 / Kimi K3 / MiMo;StepFun/MiMo/Qwen3.8 官方 CNY 价 ÷7 换算并标注;qwen3.8-max 免费额度超出按 ¥12/¥36 计入);通用模型消耗按「计价货币」自动换算(选 CNY ×7、选 USD 原样,与 DeepSeek 峰谷表口径一致);模型→平台映射扩充且认不出不再默认落 DeepSeek;会话投影新增 currentProvider;大肥鱼位置跳动根治(位置缓存 / 键盘不吸附 / 禁用初始化过渡);状态文件路径改用 DSH_HOME 而非硬编码
|
|
254
|
-
v1.2.0 — 价格表扩充(对照 [modelradar.cn](https://modelradar.cn) 2026-09-03 快照,仅采纳官方定价页无分歧条目):新增 `gpt-5.3-codex`、`gemini-3.8-flash`、`gemini-2.5-flash`、Kimi `k2.6`/`k2.5`、通义 `qwen3.8-flash`/`qwen3.8-27b`/`qwen3.6-plus`、豆包 Seed 2.0 全 12 档(pro/lite/mini/code × 32k/128k/256k)、混元 `2.0-instruct-128k`/`2.0-think-128k`/`turbo-s`;修正 `claude-opus-5` 缓存读价 5.0→0.5(Anthropic 缓存读=0.1×输入,原误标「无缓存折扣」);radar 的 GPT-5.6 系输出价(输入×1.25 异常模式)与 qwen3.7-max 促销原价**未采纳**,原可信值保留并注明;官方品牌图标替换字母兜底:MiniMax(#E73562 官方紫红)、xAI(X logo)、小米 MiMo(Xiaomi logo),stepfun/novita 无官方 SVG 仍用文字图标
|
|
255
|
-
v1.2.1 — **把手下滑关闭**:看板抽屉 / 平台详情 / 设置面板三处顶部把手支持下滑手势关闭(pointer 事件跟手拖拽整个抽屉,位移 >72px 或快速轻扫即播滑出动画后关闭,不足则回弹;`touch-action:none` 防滚动冲突,触屏/鼠标通用);**豆包/混元入列模型品牌分组**(v1.2.0 只进了价格表未显示,现与 OpenAI/Claude/Gemini/Qwen/MiMo 同列「模型品牌」,`modelToPlatform` 增加 `doubao-*`/`hunyuan*` 映射,会话消耗按对应品牌计价);豆包用字节跳动官方 logo(#3C8CFF),混元暂无官方 SVG 沿用文字图标(品牌蓝 #0052D9)
|
|
256
|
-
v1.2.2 — **豆包/混元换官方品牌图标**:豆包改用豆包官方 64×64 图标(官网 favicon PNG 转 data URI 内嵌,替换此前借用的字节跳动 logo);混元改用腾讯混元官方 logo.svg(hunyuan.tencent.com 官方矢量,净化后内嵌,多色圆弧标),移出 TEXT_ONLY 文字图标列;两者均自官网/官方 CDN 获取
|
|
257
|
-
v1.2.3 — **修复 GLM-5.3-flash 缓存读价误标**(用户实测反馈:长会话估算 ¥9.93 vs 真实账单 ¥5.19):原表把 `glm-5.3-flash` 标为「无缓存折扣」(cacheHit=cacheMiss=0.15),但 GLM 系缓存读=输入×20%(同表 glm-5.2 0.26/1.4、glm-5-turbo 0.24/1.2 交叉佐证),多轮长会话数百万缓存读 token 全按全价计导致估算虚高 ~5 倍;修正 cacheHit→0.03 并新增回归断言(GLM 系 cacheHit 必须低于 cacheMiss);其余「无缓存折扣」条目(qwen3.7/3.8-max 促销价、混元等)因缺官方缓存价佐证维持原值
|
|
258
|
-
v1.2.4 — **parse-error 透传接口业务错误消息**:智谱套餐过期实测返回 HTTP 200 + `{"code":500,"msg":"当前用户不存在coding plan","success":false}`,原逻辑只显示笼统的「无法解析余额数据」,误导用户以为是解析代码坏了;现在识别业务层错误(`success:false` 或 `code≠200` 且带 `msg`)并透传原始消息(显示为「无法解析余额数据 (接口返回: 当前用户不存在coding plan)」),适用于全部预设平台
|
|
259
|
-
v1.2.5 — **智谱按量付费账户改为中性「未开放」展示**:实测按量付费用户(非 Coding Plan 套餐)调用配额接口返回「当前用户不存在coding plan」,且候选余额端点(`/api/monitor/account/balance`、`/api/paas/v4/dashboard/billing/*` 等)全部 404 —— 智谱按量付费无公开余额接口;识别该业务消息后状态从红色 parse-error 改为 no-balance-api,其余业务错误仍透传原始消息
|
|
260
|
-
v1.2.6 — **智谱四类账户分流固化 + 套餐用户回归测试**(开源前防回归):业务错误分类抽为可单测的 `classifyBizError(queryType, json)`;新增 13 条断言覆盖 Coding Plan 套餐用户(有额度 / 周配额用完 / 全用尽三种形状必须照常出配额,**绝不能被按量付费改动吞掉**)、按量付费/套餐过期(中性未开放)、其他业务错误(透传 msg)、非智谱平台不误套规则、JSON 解析失败保持原文案;文案不替平台断言账户类型——按量付费与套餐已过期返回**同一条** `msg`,接口层无法区分,故统一表述为「无 Coding Plan 套餐(按量付费 / 套餐已过期均返回此结果)」
|
|
261
|
-
|
|
262
|
-
v1.3.2 — **海外模型可独立选计价货币**(用户实测反馈:面板显示 ¥1285 被误读成 $1285,实为 $183.7):设置里「计价货币」下方新增「海外模型计价」下拉(跟随主货币 / 美元 / 人民币,**默认「跟随」= v1.2.6 行为完全不变**)。海外厂商官方定价页本来就是 USD,选美元即绕开 ×7 折算带来的误差与「¥ 被看成 $」的误读。新增 `modelRegion(model)` 前缀判定产地(`gpt`/`claude`/`gemini`/`grok`/`mistral` 等 → 海外,`deepseek`/`glm`/`kimi`/`qwen`/`doubao`/`hunyuan`/`step`/`mimo`/`minimax` 等 → 国内,**未命中一律不表态、走主货币**)与 `currencyForModel(config, model)`;会话消耗投影改为按币种分组 `costByCurrency`,混合会话(如 claude + deepseek)**不按汇率折算合并**,状态条两段拼接显示 `~¥3.40+$12.50`——折算等于把刚修掉的 ×7 误差又引回来。新增 `test-cost-currency.mjs`(18 断言)+ 产地/币种断言 17 条 + 客户端格式化器断言 15 条,全套 10 文件 191 断言
|
|
263
|
-
|
|
264
|
-
## 发布方式(维护者)
|
|
265
|
-
|
|
266
|
-
npm 发布走 **Trusted Publishing (OIDC)**,仓库内不存任何 token:
|
|
267
|
-
|
|
268
|
-
```sh
|
|
269
|
-
# package.json 版本改好并提交后
|
|
270
|
-
git tag v1.3.2 && git push origin v1.3.2
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
`.github/workflows/publish.yml` 会校验「tag 与 package.json 版本一致」「dependencies 为空」
|
|
274
|
-
「语法」「全套测试全绿」,全过才发布,并附带 provenance 溯源。
|
|
275
|
-
首次使用需先在 npm 网页把本仓库配成 Trusted Publisher(详见该 workflow 顶部注释)。
|
|
276
|
-
|
|
277
|
-
v1.3.3 — **修复「一键更新」按钮无响应**(DSHA webview 拦截 `window.confirm` 导致点击后函数在第一行 return):去掉确认弹窗,直接执行安装;更新结果改为醒目横幅(成功绿底/失败红底),不再只显示 11px 灰色字;`already up to date` 返回 200 而非 500
|
|
104
|
+
状态条显示的是「官方余额」还是「—(中转站无余额接口)」,按三层判定,优先级从高到低:
|
|
278
105
|
|
|
279
|
-
|
|
106
|
+
1. **用户显式名单** —— 设置面板「官方直连 provider」,写进去的一律按官方;
|
|
107
|
+
2. **baseURL 域名** —— 服务端读 `settings.yaml` 的 provider `baseURL`,按**域名**比对官方端点白名单;没写 baseURL 的不表态;
|
|
108
|
+
3. **命名约定** —— 带 `-official` / `_official` 后缀的按官方。
|
|
280
109
|
|
|
281
|
-
|
|
110
|
+
三层都不命中默认按**中转站**处理(宁可不显示,也不显示错的余额)。设置面板会列出当前的自动判定结果。
|
|
282
111
|
|
|
283
|
-
|
|
112
|
+
## 安全
|
|
284
113
|
|
|
285
|
-
|
|
114
|
+
- API Key 只从环境变量 / DSH 凭证系统读取,**不写进代码或仓库**;
|
|
115
|
+
- `/api-dashboard/config` 返回的 Key 一律 `***`;浏览器端只请求同源接口,不向第三方域名发数据;
|
|
116
|
+
- 本地状态文件含自定义条目的 Key,权限 `0600`,被 `.gitignore` 排除;
|
|
117
|
+
- 自定义模型 / 中转站的 Key 会发往**你自己填的接口地址**,请只填信任的服务;
|
|
118
|
+
- v1.4.1 起插件路由带鉴权闸门(无 cookie → 401,跨域 → 403)。
|
|
286
119
|
|
|
287
|
-
|
|
120
|
+
## 开发
|
|
288
121
|
|
|
289
|
-
|
|
122
|
+
- 服务端 `src/index.js`(ESM,**零依赖、零构建**);客户端 `client/client.js`(改完需重启 `dsh web`)
|
|
123
|
+
- 测试:`for f in test/*.mjs; do node "$f"; done`(退出码非 0 即失败)
|
|
124
|
+
- 改动红线、踩坑记录、发布流程:[`AGENTS.md`](AGENTS.md)、[`docs/RELEASING.md`](docs/RELEASING.md)
|
|
290
125
|
|
|
291
|
-
|
|
126
|
+
## 更新日志
|
|
292
127
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
⑦ 🐛 **`computeProviderKinds` 不再按 provider 名字猜「官方直连」** —— 该兜底与 AGENTS.md 铁律 9(没写 baseURL 的 provider 不表态、交 `-official` 后缀兜底)冲突,会把同名中转站会话顶上官方余额。客户端抽屉文案硬编码的「· 配额正常」也改为按三色灯 `level` 取标签(原先 GLM 配额耗尽会显示成「…已用完 · 配额正常」)。
|
|
296
|
-
|
|
297
|
-
⑧ 🆕 **子代理(subagent)消耗现在能看到** —— 此前投影只折叠**本会话**的事件,而子代理跑在自己的子会话里,它烧的 token 完全不在主板数字里。现在输入框下方的主状态条**下面换行**多出一行子代理胶囊,**按创建顺序从左到右**排列(顺序取自父会话的 `subagentCatalog` 投影,即父会话里的 catalog 事件顺序,客户端不重排)。
|
|
298
|
-
金额是「该子代理 **+ 其后代**」的向上汇总(孙代理并进它的直接父代理那一条,深度封顶 4 层、行数封顶 12 条),计价口径与主板数字**完全同一套函数**(`summarize`),因此峰谷、原生币种、缓存读写分桶全部一致;子代理用海外模型时按原生 `$` 显示,不折算合并。
|
|
299
|
-
⚠️ **主板数字仍是「本会话」**,不含子代理 —— 两者分开展示,避免一个数字混了两种口径。
|
|
300
|
-
|
|
301
|
-
⑨ 🐛 **修掉一个会让新默认值对老用户失效的坑**:配置持久化在 `~/.dsh/dsh-api-dashboard.json`,老状态文件里写着 `"overseasCurrency": "follow"`(那是**旧默认值**被存下来的,不是用户的显式选择),会把 ① 里的新默认值 `'USD'` 钉死。已引入状态文件结构版本 `configVersion` 并在加载时迁移(1 → 2):把旧默认值改写成 `'USD'`;迁移后用户再手动选 `follow` 会被正常尊重。
|
|
302
|
-
|
|
303
|
-
⑩ 🐛 **修掉子代理显示 `~—`**(⑧ 的实测缺陷):子代理跑完就**由热转冷** —— 框架的 `SubagentListEntry.activity` 只有 `'running' | 'inactive'`,**`inactive` = 只存在于持久化里**,此时它已不在 `ctx.sessions` 的常驻表里,`sessions.get()` 取不到 → 读不到消耗 → 面板显示 `~—`。
|
|
304
|
-
已加**冷路径兜底**:同步读持久化投影缓存 `storages/session_projcache/sessions/<id>.json`(形状取自实测)。热路径仍优先(更新鲜),冷路径只在热路径拿不到数据时启用,带 3 秒 TTL 内存缓存,避免每次投影变化都读盘。
|
|
305
|
-
✅ 用本机真实缓存验证:同一个子代理从 `~—` 变成 **`~¥0.588`**(13.7 万未缓存输入 + 974 万缓存读 + 6.4 万输出,deepseek-flash)。
|
|
306
|
-
⚠️ 为什么不用框架自己的 `listChildren()`:它走投影缓存读、能处理冷会话,但是 **async**,而投影的 `view()` 契约要求**同步**。
|
|
307
|
-
|
|
308
|
-
⑪ 🎨 **设置界面控件统一**(此前用的是浏览器默认控件,与整体风格不搭;**只动外观、不动结构**):去掉 `<select>` 的系统外观换自绘箭头(`.dshadb_field_select`)、去掉数字框的系统上下箭头、滑块由「只设 accent-color」改为自绘轨道 + 滑块,并**补齐深色模式** —— 设置面板的输入框/开关/Tab、状态条、子代理胶囊此前**一律硬编码白底**(深色模式下就是一块白)。
|
|
309
|
-
⚠️ 曾把「计价货币」「海外模型计价」这几处 2~3 选项的 `<select>` 换成插件自己的分段按钮(`.dshadb_segbtn`)—— 在 2 列网格里「人民币」被截成「人民…」,**观感反而更差**(用户原话「还不如不改」),**已回退成 `<select>`**。教训:**窄容器里优先保证文字完整**,改控件外观前先确认最长的那个文案放得下。
|
|
310
|
-
|
|
311
|
-
⑫ 🎨 **子代理那一行:固定单行 + 横向滑动 + 长按详情浮层**
|
|
312
|
-
- 原先自动换行,子代理一多(比如 4 个)会把输入框往上顶。现在 `flex-wrap:nowrap` + `overflow-x:auto`(隐藏滚动条)+ 名字截断到 72px,**无论几个子代理都只占一行约 20px**。
|
|
313
|
-
- 完整名字/模型/精确金额**不再依赖 `title`** —— 手机上 `title` 根本不显示,长按弹的是系统「选择/复制」菜单(用户实测反馈「长按只能复制」)。改为 `pointerdown` 计时器(420ms,位移 >8px 取消)弹 `.dshadb_subtip` 浮层,配 `user-select:none` + `-webkit-touch-callout:none` 压掉系统菜单;浮层挂 `document.body`(挂在胶囊里会被 `.dshadb_subs` 的 `overflow-x:auto` 裁掉),点别处/滚动/5 秒自动收起。完整详情同时写进 `data-tip`(浮层读它,也是单测抓手)。
|
|
314
|
-
|
|
315
|
-
> ⚠️ **未取到官方原文的条目已在代码注释里逐条标注「未核实」**:豆包 Seed 2.0 全系、腾讯混元、`kimi-k2.5`、`moonshot-v1-*`、`step-1-*` 与 DeepSeek 已退役的三条占位价(本轮按 `×7` 保号迁移,仅保证显示值不跳变)。海外三家(OpenAI / Anthropic / Gemini)官方定价页在容器环境被 403 / 地域封锁,亦未复核。
|
|
316
|
-
|
|
317
|
-
> 🔎 **「自动判定」现在是真的了**(完整实测结论见 `AGENTS.md` 第六节)。原先半真半假:**provider 官方/中转判定、API Key 发现、端点/格式探测都是真的,但「平台余额清单是硬编码的」—— 插件根本不认 DSH 的 provider 列表**。本轮补齐了这块(见 ⑬)。仍**故意**保留的一处「不自动」:没写 `baseURL` 的 provider 一律不表态、按中转站显示「—」—— 这是铁律 9 的设计(内置目录指向官方域名 ≠ 用户的 key 来自官方,`xiaomi` 就是反例),不是 bug,也别去「修」。
|
|
318
|
-
|
|
319
|
-
⑬ 🆕 **「自动判定」补上最后一块短板:插件开始读 DSH 的 `settings.yaml` provider 列表** —— 此前插件只查自己的 `customRelays`,用户在 DSH 里配好的中转站**一个都查不到余额**,必须在插件设置里手抄一遍 baseUrl + key(这是整个插件最大的体验断点)。
|
|
320
|
-
现在服务端直接解析 `llm-pi-ai.providers.<name>` 的 `baseURL` + `apiKeyEnv`,**自动合成中转站条目**并纳入轮询;key 走与预设平台**同一套三层兜底**(环境变量 → DSH `credentials` 服务 → `~/.dsh/.credentials.yaml` 的 `refs:`)。三个过滤条件都对应既有铁律:**没写 `baseURL` 的跳过**(铁律 9 不表态)、**判成 official 的跳过**(官方直连归预设平台管,别重复成一条中转站)、**用户关掉的跳过**。与手填的 `customRelays` 合并时**手填优先**(同 id 或同 `baseUrl` 不重复查)。
|
|
321
|
-
设置面板新增「来自 DSH 的中转站(自动)」区块:**默认全部启用、每个可单独关**,关掉的记进状态文件 `dshProviderOptOut`,下次自动发现**不会再打开**;官方直连与没写 baseURL 的只展示不可开。下发给客户端的只有名字/baseURL/开关状态,**绝不含 key**。
|
|
322
|
-
✅ 本机实测(8 个 provider):自动发现 5 条中转站(`dshzuoxhe` / `jiyuan` / `jiyuanlvdong` / `mimov` / `new`),`zhipu` 因判为 official 跳过,`xiaomi` / `opencode` 因没写 baseURL 跳过 —— 与铁律 9 的设计完全一致。
|
|
323
|
-
🔧 顺带把 `settings.yaml` 的解析抽成通用取值器 `collectProviderFields`(原先只取 `baseURL` 一个字段),`parseProviderBaseURLs` 的返回语义**保持不变**(有回归断言盯着),新增 `parseProviderEntries` 与纯函数 `selectDshProviders`(过滤规则可单测)。
|
|
324
|
-
|
|
325
|
-
⑭ 🐛/🎨 **四项实测反馈修复**
|
|
326
|
-
|
|
327
|
-
① 🐛 **在自己面板里横滑,会被手机壳判成「开侧边栏」** —— 调大肥鱼的「身体大小 / 探出多少」滑块时,侧边栏会被拉出来(维护者两次反馈「滑动的时候容易把侧边栏拉过来」)。
|
|
328
|
-
**真根因(读 `dsh-web-mobile` 源码确认,不是挂件抢触摸)**:手机壳插件 `dsh-web-mobile` 的 `sidebar-swipe` 手势层在 **document 捕获阶段**吃 `pointerdown` —— 起手点在**屏幕左侧 45%** 以内(`START_ZONE_RATIO = 0.45`)且横向位移占优(`LOCK_PX = 8`),就判成「左边缘滑入打开侧边栏」。滑块在左半屏起手往右拖,正好全中;捕获阶段**先于**我们的监听器,插件里拦不住。
|
|
329
|
-
修法:它的 `beginStroke` 留了一条**让路规则** —— 起手元素若属于「真·横向滚动容器」(`overflow-x` 为 `auto/scroll` 且 `scrollWidth > clientWidth + 1`)就直接放弃识别。于是给遮罩 `.dshadb_scrim` 补 **2px 不可见横向溢出**(由 0 高度的 `.dshadb_swipeguard` 提供),让整块面板都落进让路条件。抽屉自身是 `position:fixed`,遮罩滚动**不会**移动面板 —— 视觉与布局零影响。
|
|
330
|
-
⚠️ **别想着改用 `aria-modal="true"` 去命中另一条让路规则**(`modalOpen()`):手机壳把 `[aria-modal="true"]` 当自家对话框,用 `[class*="_header"]` / `[class*="_row"]` / `[class*="_section"]` / `[class*="_tabs"]` 这类**子串选择器**重排里面的元素,而 `dshadb_header` / `dshadb_settings_row` / `dshadb_settings_section` / `dshadb_tabs` 全都会命中 —— 面板会被改烂。
|
|
331
|
-
🛡️ 同时**保留挂件锁**(`.dshadb-whale-locked`,`overlayOpen` 时 `pointer-events:none`)作为加固:挂件是 `body` 的直接子元素(`z-index:9600`),面板嵌在 DSH 的 composer dock 里 —— 面板一旦被某个祖先关进更低的层叠上下文,挂件就会盖在面板上抢触摸。**只关交互、不隐藏**(调大小时要看实时预览),`onDown` 里再兜一道。顺带把滑块触摸区 24px → **34px**、圆点 20→24px。
|
|
332
|
-
|
|
333
|
-
② ⚡ **首屏/切回前台不再阻塞**(用户反馈「切掉后台重新进来,插件加载有点慢」)。根因在服务端:客户端首屏和 `visibilitychange` 都走 `force=1`,而那条路底下是 `await refreshAll()` —— 一次全量轮询要等**最慢**的端点,最长能拖满 `timeoutMs`(默认 8s)。
|
|
334
|
-
现在新增 `?stale=1`(stale-while-revalidate):**有缓存就立刻回旧数据,刷新丢后台**,由下一次轮询把新数据带上来;只有「服务端刚重启、缓存为空」时才真的需要等(那时确实没东西可显示)。显式强刷(手动刷新按钮、保存设置后)仍然阻塞等新数据。取数策略抽成纯函数 `planBalancesFetch`(`wait`/`background`/`none`)并单独回归 —— 这策略太容易被顺手改回阻塞式。
|
|
335
|
-
ℹ️ 另一半原因是 **DSH 自己的行为**:应用切回前台时 webview 可能整页重载,所有插件都要重新 init,这一段不归插件管。
|
|
336
|
-
|
|
337
|
-
③ 🎨 **刷新间隔下限 5 秒 → 1 秒**(用户要求)。客户端输入框 `min`/`step` 与轮询接受阈值、服务端 `clampRefreshSec` 三处一起改;上限仍是 60 秒。
|
|
338
|
-
⚠️ 1 秒会把请求量放大 5 倍,**平台接口可能有速率限制**;插件本身已有 2 秒强刷节流兜底,但建议只在自己可控的中转站上用 1 秒。
|
|
339
|
-
|
|
340
|
-
④ 🎨 **「来自 DSH 的中转站」列表收进折叠层**(维护者要求「跟首页一样的折叠方法」)—— 直接复用首页的分组组件 `GroupSection`(表头 + 条数 + 箭头 + `max-height` 过渡),**默认收起**,点开才看到各条中转站与开关。
|
|
341
|
-
⚠️ 上一轮曾误把「自动判定结果」折叠起来,维护者澄清「那个不需要改,应该折叠的是来自 DSH 中转站下面的那一罗列」—— 已改回平铺,`.dshadb_kinds_head` 样式与 `showKinds` 状态一并删掉。
|
|
342
|
-
|
|
343
|
-
⑯ 🐛 **大肥鱼挂件两处交互修复**(维护者实测反馈)
|
|
344
|
-
|
|
345
|
-
① **台词气泡压在头顶** —— 素材 `DSniang1.png` 是 610×610,不透明像素从 `y=10` 就开始(头顶呆毛几乎顶到画布上沿),而 `object-fit:contain` 在正方形盒子里等于铺满,所以**盒子顶 ≈ 呆毛顶**。原 `bottom:calc(100% - 6px)` 让气泡**下沿落进盒顶往下 6px**,尾巴(`::after` 再 8px)扎到 14px —— 鱼只有 64.8px(scale 0.6)时那是整条鱼的 **21%**,看着就是顶在头上。
|
|
346
|
-
已改为 `bottom:calc(100% + 6px)`:气泡与尾巴整体移到盒子上方,**尾尖正好点在呆毛尖端**;下方兜底分支同步改为 `top:calc(100% + 6px)`,上下对称。翻转阈值 `r.top - bh - 6` 本来就是这个口径,**JS 一行没动**。
|
|
347
|
-
|
|
348
|
-
② **在鱼身上往右拖会被手机壳判成「开侧边栏」** —— 与 ⑭① 是**同一条**手势层,但挂件本体此前**完全没有守卫**(⑭① 只覆盖了我们自己的面板)。
|
|
349
|
-
复现位置是**鱼停在屏幕左侧 45% 以内时**(典型 = 左边缘吸附位):按本机 360px 视口、scale 0.6 的 64.8px 鱼算,贴左边时可见区 ≈ `x∈[0,32]`,正落在识别带 `[0,162]` 里;贴右边时 ≈ `x∈[328,360]`,**不会**被判定。(维护者先报「从右往左滑」,核实后更正为「从左往右滑」—— 与源码规则 `if (dx <= 0) return 'none'` 一致,也印证了根因。)
|
|
350
|
-
⚠️ 挂件自己的拖拽**同时也照常执行**(捕获阶段只判手势、不拦事件),所以现象是「鱼被拖走了 + 抽屉也被拉出来」。
|
|
351
|
-
修法:在 `.dshadb-whale-body` **内部**垫一层透明抓取层 `.dshadb-whale-grab`(`overflow-x:auto` + 0 高度 `width:calc(100% + 2px)` 守卫子元素 + `touch-action:none` + 隐藏滚动条),起手点落在它身上 → 手机壳的 `findHorizontalScroller` 命中 → 放弃识别。
|
|
352
|
-
⚠️ 挂在 body **内部**(不是 root 下)是为了让 `pointerdown` 照常冒泡到 body 上已有的拖拽处理器 —— **拖拽逻辑一行未改**;挂 root 下同样能让路,但鱼也拖不动了。
|
|
353
|
-
⚠️ `touch-action:none` 必须写在这一层:滚动容器是浏览器判定可触摸行为的终点,漏了它横向 pan 会被这个滚动容器自己抢走(`pointercancel`)→ 鱼直接拖不动。
|
|
354
|
-
⚠️ 别改成直接给 `.dshadb-whale-body` 加 `overflow` —— 那会把它里面 `img` 的 `drop-shadow` 裁掉,鱼会显平。另:`pointer-events` **不是「继承即锁」**,抓取层自己写了 `auto`,所以 `.dshadb-whale-locked` 必须**单独**把抓取层也锁上。
|
|
355
|
-
代价:鱼压在左边缘时,它盖住的那 ~32×65px 不能再作为「左边缘开抽屉」的起手点(与面板守卫同一取舍,把鱼拖走即可)。
|
|
356
|
-
|
|
357
|
-
③ **冷启动「抽搐一下」**(维护者反馈「每次重启首次进入界面时会抽搐一下」,后更正为「打开设置界面才抖动」)—— **真机埋点定位出来的**,不是猜的:用 `PerformanceObserver({type:'layout-shift', buffered:true})` 的 attribution 直接点名「哪个节点位移了多少」,并把「打开设置面板」那一刻的时间点一起上报,真机(360×754、dpr=4)拿到三条位移:
|
|
358
|
-
- `div.uV2eYG_scroll` [0,0,0,0]→[16,580,322,36],**CLS 0.01725** —— 外壳自己的会话滚动容器首帧,**不归我们**;
|
|
359
|
-
- `span.dshadb_barwrap` 124×26 → 230×**28**,CLS 0.00094+0.00053 —— 钱数/平台名进来时**状态条高了 2px**,把上面的会话区顶了一下;
|
|
360
|
-
- `div.dshadb-whale-grab` **[306,467,54,108] → [327,160,33,65]**,**CLS 0.01193** —— **挂件往上跳 307px 且从 108px 缩到 65px**,这是我们这边最大的一笔。
|
|
361
|
-
两个成因与修法:
|
|
362
|
-
- **挂件**:`ensureWhaleWidget()` 先用默认值(`scale=1` → 108px、贴右边、`top=62%` 屏高)画出来,等 `/whale/settings` 回来才改成保存值 → 那一帧默认值被用户看见,紧接着就是硬跳(`transition` 此刻还是 `none`,所以是跳不是滑)。改为**首帧 `visibility:hidden`**、设置到位才 `reveal()`(幂等;成功/失败/提前 return **三条分支都接** `.then(reveal)`,另有 **2s 兜底**,否则请求异常会让鱼永远不出现)。真机那笔 0.01193 就是从这里来的。
|
|
363
|
-
⚠️ **为什么它看起来像「打开设置界面才抖」**:`whaleEnabled` 搭在 `/api-dashboard/balances` 响应的 `config` 里,而服务端对「刚重启、无缓存」的 `?stale=1` 仍走 `wait` → `await refreshAll()`(最长 8s)→ 冷启动时配置 **~10s** 才到,挂件那时才被创建、随即跳一下;用户正好在那几秒里开着设置面板看,就归因到了设置面板上。真机时间线:`settings-opened` t=8357 → `whale-created` t=10394 → `whale-settings-applied` t=10433 → 位移 t=10908。
|
|
364
|
-
ℹ️ 另外顺手否掉了一个**看似合理但错的**假设:设置面板自己「先空壳后填充」(`relays:[]`/`dshProviders:[]`/`wf.scale=1` 起手,两个接口回来才填)**并不会**造成位移 —— 抽屉是 `position:fixed` + `max-height:86vh`,高度五次采样全是 648(=0.86×754),内容在内部滚动,所以它不登记 layout-shift。(首次打开那两个请求很慢:3 秒内没回来;暖了之后 35ms。)
|
|
365
|
-
- **状态条**:空态内容 18px、有数据态 20px → 26px 变 28px。给 `.dshadb_bar` 加 `min-height:28px;box-sizing:border-box`(28 = 有数据态的实测总高,所以有数据时外观不变)。**用 `min-height` 而不是 `height`**:系统字体放大时仍能自然增高,不会裁字。
|
|
366
|
-
|
|
367
|
-
⑰ 测试由 **10 文件 208 断言**扩到 **15 文件 456 断言**:新增 `test-cost-projection.mjs`(重试/attempt 事件语义)、`test-balance-guards.mjs`(余额红线)、`test-subagents.mjs`(子代理顺序 / 后代汇总 / 币种 / 冷路径兜底 / 上限截断 / strict schema)、`test-dsh-providers.mjs`(33 条:`baseURL`/`apiKeyEnv` 解析、两条过滤规则、开关大小写不敏感、畸形输入静默、与手填条目的合并优先级)、`test-fetch-policy.mjs`(29 条:`planBalancesFetch` 的冷启动/peek/强刷三分支与节流、`clampRefreshSec` 1~60 边界);`test-pricing.mjs` 重写为「原生币种 + 官方价逐条」并补前缀匹配与 `nativeCurrencyOf` 回归;`test-bar.mjs`(91 条)补子代理行渲染/长按 `data-tip`/挂件锁定/DSH 列表折叠/侧滑守卫/刷新下限/**挂件让路抓取层与气泡位置/首帧隐藏与 reveal 三分支/状态条定高**断言,另加一组**结构完整性**断言(`dshadb_barwrap` / `dshadb_drawer` 类名必须在、不允许出现空属性对象 `createElement(x, { })`)—— 清理临时埋点时真把 `className` 一起删掉过,语法与其余断言全绿却会让列布局失效,所以钉住;`test-dshadb-client.mjs`(29 条)把假 DOM 的元素树改成可追踪,新增 C14 钉住「抓取层必须挂在 `.dshadb-whale-body` **内部**」—— 这条最容易在重构里被悄悄挪到 root 下(挪了手机壳确实让路,但鱼也拖不动了),三个变异(挪位置/去 `touch-action`/去 2px 守卫)都实测会让断言报错。
|
|
368
|
-
⚠️ 旧夹具用的是 `assistant/chunk` 这个不存在的事件 —— 这正是漏计 bug 一直没被回归拦住的原因,已一并改成真实的 `assistant/message`。
|
|
128
|
+
最近一版 **v1.4.3** —— 子代理胶囊只显示**它自己新产生**的消耗(不再把父会话继承来的历史、兄弟或后代子代理的消耗算进去)。一版一行的完整历史见 [`CHANGELOG.md`](CHANGELOG.md)。
|
|
369
129
|
|
|
370
130
|
## License
|
|
371
131
|
|
|
372
|
-
[
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
[MeteorNOX/DeepSeek-Balance-Whale-Widget](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget),
|
|
376
|
-
原作者 Copyright (c) 2026 MeteorNOX,MIT License,许可全文见 `assets/whale/LICENSE-whale-widget.txt`。
|
|
377
|
-
鲸鱼形象与音效素材同样来自该项目。
|
|
132
|
+
MIT —— 见 [LICENSE](LICENSE)。大肥鱼互动挂件移植自
|
|
133
|
+
[MeteorNOX/DeepSeek-Balance-Whale-Widget](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget)(MIT),
|
|
134
|
+
许可副本见 [`assets/whale/LICENSE-whale-widget.txt`](assets/whale/LICENSE-whale-widget.txt)。
|
package/client/client.js
CHANGED
|
@@ -858,13 +858,15 @@ window.__ModuleLoader__.load({
|
|
|
858
858
|
ref: (n) => { if (n) requestAnimationFrame(() => syncSubsOverflow(n)); },
|
|
859
859
|
onScroll: (e) => syncSubsOverflow(e.currentTarget) },
|
|
860
860
|
list.map((s, i) => {
|
|
861
|
-
const f = formatSessionCost({ cost: s.cost, currency: mainCur, costByCurrency: s.costByCurrency, waiting:
|
|
861
|
+
const f = formatSessionCost({ cost: s.cost, currency: mainCur, costByCurrency: s.costByCurrency, waiting: s.waiting === true });
|
|
862
|
+
// waiting(等自身数据) 时 formatSessionCost 直接返回 null → "~—";
|
|
863
|
+
// 真实零消耗时 f 非 null 但 hasValue=false, 也必须回落 "~—"(与 v1.4.x 一致), 不能显示 ~¥0.00
|
|
862
864
|
const amt = f !== null && f.hasValue ? "~" + f.text : "~—";
|
|
863
865
|
const kind = s.mode === "continuable" ? "可续聊子代理" : "一次性子代理";
|
|
864
866
|
const models = (s.models || []).join(", ");
|
|
865
867
|
const tip = [
|
|
866
868
|
kind + " " + (s.label || "子代理"),
|
|
867
|
-
"
|
|
869
|
+
"自身消耗 " + (f !== null ? f.title : "等待自身用量数据"),
|
|
868
870
|
models ? "模型 " + models : "",
|
|
869
871
|
s.id ? "ID " + String(s.id).slice(0, 8) : "",
|
|
870
872
|
].filter(Boolean).join("\n");
|
|
@@ -2645,7 +2647,7 @@ window.__ModuleLoader__.load({
|
|
|
2645
2647
|
|
|
2646
2648
|
// v1.4.0: 子代理消耗行 —— 有子代理时在状态条**下面换行**追加一行,
|
|
2647
2649
|
// 按父会话里的创建顺序从左到右排列 (顺序由服务端 subagentCatalog 决定, 这里不排序)。
|
|
2648
|
-
//
|
|
2650
|
+
// 只计该子代理自身新用量,不含继承历史、兄弟或后代。
|
|
2649
2651
|
const subRow = buildSubagentRow(cost);
|
|
2650
2652
|
|
|
2651
2653
|
return react.createElement("span", { className: "dshadb_barwrap" }, [
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-api-dashboard",
|
|
3
|
-
"version": "1.4.
|
|
4
|
-
"description": "多平台 API
|
|
3
|
+
"version": "1.4.3",
|
|
4
|
+
"description": "DeepSeek Harness 多平台 API 余额 / 用量看板:输入框下方实时显示各平台余额与本会话估算消耗",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
package/src/index.js
CHANGED
|
@@ -1262,16 +1262,30 @@ function checkAlerts(balances, config, ctx) {
|
|
|
1262
1262
|
// ============================================================
|
|
1263
1263
|
|
|
1264
1264
|
// 纯函数, 导出便于单测 (不影响对外行为)
|
|
1265
|
-
export function parseResponse(queryType, json) {
|
|
1265
|
+
export function parseResponse(queryType, json, pref) {
|
|
1266
1266
|
if (!json || typeof json !== 'object' || Array.isArray(json)) return null
|
|
1267
1267
|
switch (queryType) {
|
|
1268
1268
|
case 'deepseek': {
|
|
1269
1269
|
const infos = Array.isArray(json?.balance_infos) ? json.balance_infos : []
|
|
1270
|
-
|
|
1270
|
+
// H-4d (v1.4.2): balance_infos 是「一个币种钱包一条」—— 官方文档 currency 取值 CNY / USD。
|
|
1271
|
+
// 旧代码盲取 infos[0],而**数组顺序不保证**:真机实测同一 key 连打 5 次,第 4 次顺序翻成
|
|
1272
|
+
// [USD=0.00, CNY=123.45] → 读到 USD 那条 → 有 123.45 元的账户显示成「$0.00 · 异常」。
|
|
1273
|
+
// 更糟的是下面那道 `total_balance == null` 红线**拦不住**它 —— "0.00" 是合法字符串,
|
|
1274
|
+
// 于是红线守卫被绕过、0 被当成真实余额渲染(与 v1.4.0 openai-credit-grants、
|
|
1275
|
+
// v1.4.1 openrouter 是同一族漏洞:选错一条就当真实数字)。
|
|
1276
|
+
// 现在按「主货币优先 → 余额 > 0 → 首条」确定性挑选,结果与接口返回顺序无关。
|
|
1277
|
+
const amountOf = (v) => {
|
|
1278
|
+
if (v === null || v === undefined || v === '') return null
|
|
1279
|
+
const n = Number(v)
|
|
1280
|
+
return Number.isFinite(n) ? n : null
|
|
1281
|
+
}
|
|
1282
|
+
const want = String(pref ?? '').trim().toUpperCase()
|
|
1283
|
+
const usable = infos.filter((x) => x && amountOf(x.total_balance) !== null)
|
|
1284
|
+
const p =
|
|
1285
|
+
(want ? usable.find((x) => String(x.currency || '').toUpperCase() === want) : undefined) ??
|
|
1286
|
+
usable.find((x) => amountOf(x.total_balance) > 0) ??
|
|
1287
|
+
usable[0]
|
|
1271
1288
|
if (!p) return null
|
|
1272
|
-
// v1.4.0 修复: total_balance 缺失时 toAmount(null)=0 会伪造「余额 0」。
|
|
1273
|
-
// 与 AGENTS.md「字段存在性校验」一致 —— 缺关键字段即返回 null, 交给上层显示「未开放」。
|
|
1274
|
-
if (p.total_balance == null) return null
|
|
1275
1289
|
// total_balance 当前余额, granted_balance 赠送, topped_up_balance 充值
|
|
1276
1290
|
const total = toAmount(p.total_balance)
|
|
1277
1291
|
const grant = toAmount(p.granted_balance)
|
|
@@ -1490,7 +1504,7 @@ async function queryPreset(platform, apiKey, config) {
|
|
|
1490
1504
|
const text = await res.text()
|
|
1491
1505
|
let json
|
|
1492
1506
|
try { json = JSON.parse(text) } catch { json = null }
|
|
1493
|
-
const parsed = parseResponse(queryType, json)
|
|
1507
|
+
const parsed = parseResponse(queryType, json, config?.currency)
|
|
1494
1508
|
|
|
1495
1509
|
if (!parsed) {
|
|
1496
1510
|
// v1.2.6: 业务错误分类抽到 classifyBizError (可单测)。
|
|
@@ -1569,7 +1583,7 @@ async function queryCustomRelay(relay, config) {
|
|
|
1569
1583
|
const text = await res.text()
|
|
1570
1584
|
let json
|
|
1571
1585
|
try { json = JSON.parse(text) } catch { continue }
|
|
1572
|
-
const parsed = parseResponse(cand.type, json)
|
|
1586
|
+
const parsed = parseResponse(cand.type, json, config?.currency)
|
|
1573
1587
|
if (parsed) {
|
|
1574
1588
|
// A: 记住这次命中的端点(变了才落盘, 避免每次刷新都写状态文件)
|
|
1575
1589
|
if (relayEndpointHints.get(id) !== cand.type) {
|
|
@@ -1635,14 +1649,14 @@ export async function queryCustomModel(model, config) {
|
|
|
1635
1649
|
|
|
1636
1650
|
// 2) 指定解析类型
|
|
1637
1651
|
if (queryType && queryType !== 'auto') {
|
|
1638
|
-
const parsed = parseResponse(queryType, json)
|
|
1652
|
+
const parsed = parseResponse(queryType, json, config?.currency)
|
|
1639
1653
|
if (parsed) {
|
|
1640
1654
|
return { ...base, status: 'ok', total: parsed.total, currency: parsed.currency, available: parsed.available, used: parsed.used, note: parsed.note, percent: parsed.percent, noBalance: false, queryType, fetchedAt: Date.now() }
|
|
1641
1655
|
}
|
|
1642
1656
|
} else {
|
|
1643
1657
|
// 3) auto: 尝试常见格式
|
|
1644
1658
|
for (const qt of ['openai', 'quota', 'deepseek', 'openai-billing']) {
|
|
1645
|
-
const parsed = parseResponse(qt, json)
|
|
1659
|
+
const parsed = parseResponse(qt, json, config?.currency)
|
|
1646
1660
|
if (parsed) {
|
|
1647
1661
|
return { ...base, status: 'ok', total: parsed.total, currency: parsed.currency, available: parsed.available, used: parsed.used, note: parsed.note, percent: parsed.percent, noBalance: false, queryType: qt, fetchedAt: Date.now() }
|
|
1648
1662
|
}
|
|
@@ -1712,7 +1726,8 @@ const readSessionCacheRecord = (sessionId) => {
|
|
|
1712
1726
|
|
|
1713
1727
|
/** 冷路径: 从缓存文件里取该会话的 queryBalanceCost **状态**(不是 wire 视图)。 */
|
|
1714
1728
|
const cachedCostState = (sessionId) => {
|
|
1715
|
-
const
|
|
1729
|
+
const row = readSessionCacheRecord(sessionId)?.queryBalanceCost
|
|
1730
|
+
const val = row?.ver === 3 ? row.val : null
|
|
1716
1731
|
return (val !== null && typeof val === 'object' && Array.isArray(val.modelOrder) && val.byModel !== null && typeof val.byModel === 'object') ? val : null
|
|
1717
1732
|
}
|
|
1718
1733
|
|
|
@@ -1731,7 +1746,7 @@ const cachedCatalog = (sessionId) => {
|
|
|
1731
1746
|
}))
|
|
1732
1747
|
}
|
|
1733
1748
|
|
|
1734
|
-
export function collectSubagentCosts(services, rootSessionId, summarize) {
|
|
1749
|
+
export function collectSubagentCosts(services, rootSessionId, summarize, ownChildIds) {
|
|
1735
1750
|
const out = []
|
|
1736
1751
|
if (typeof rootSessionId !== 'string' || rootSessionId === '') return out
|
|
1737
1752
|
let sessions = null, projections = null
|
|
@@ -1765,32 +1780,26 @@ export function collectSubagentCosts(services, rootSessionId, summarize) {
|
|
|
1765
1780
|
if (s !== null && projections !== null) {
|
|
1766
1781
|
try {
|
|
1767
1782
|
const st = projections.stateOf(s, 'queryBalanceCost')
|
|
1768
|
-
if (st !== null && st !== undefined
|
|
1783
|
+
if (st !== null && st !== undefined) return st
|
|
1769
1784
|
} catch { /* 落到冷路径 */ }
|
|
1770
1785
|
}
|
|
1771
1786
|
return cachedCostState(sessionId)
|
|
1772
1787
|
}
|
|
1773
1788
|
|
|
1774
|
-
/** 递归汇总: 自身 + 后代, 返回与 summarize 同形的汇总。 */
|
|
1775
|
-
const rollup = (sessionId, depth) => {
|
|
1776
|
-
const st = costStateOf(sessionId)
|
|
1777
|
-
let acc = (st !== null && subagentCostSummarize !== null) ? subagentCostSummarize(st) : null
|
|
1778
|
-
if (depth >= SUBAGENT_MAX_DEPTH) return acc
|
|
1779
|
-
for (const entry of childrenOf(sessionId)) {
|
|
1780
|
-
acc = mergeSummary(acc, rollup(entry.id, depth + 1))
|
|
1781
|
-
}
|
|
1782
|
-
return acc
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
1789
|
for (const entry of childrenOf(rootSessionId)) {
|
|
1786
1790
|
if (out.length >= SUBAGENT_MAX_ROWS) break
|
|
1787
|
-
|
|
1791
|
+
if (Array.isArray(ownChildIds) && !ownChildIds.includes(entry.id)) continue
|
|
1792
|
+
const st = costStateOf(entry.id)
|
|
1793
|
+
const ready = st?.ownBoundaryKnown === true && st?.own && Array.isArray(st.own.modelOrder)
|
|
1794
|
+
const waiting = !ready || st.own.modelOrder.length === 0
|
|
1795
|
+
const s = ready ? summarize(st.own) : emptySummary()
|
|
1788
1796
|
out.push({
|
|
1789
1797
|
id: String(entry.id),
|
|
1790
1798
|
label: typeof entry.label === 'string' && entry.label !== '' ? entry.label : String(entry.id).slice(0, 12),
|
|
1791
1799
|
mode: entry.mode === 'continuable' ? 'continuable' : 'one-shot',
|
|
1792
1800
|
createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : 0,
|
|
1793
|
-
cost: s.cost,
|
|
1801
|
+
cost: waiting ? -1 : s.cost,
|
|
1802
|
+
waiting,
|
|
1794
1803
|
costByCurrency: s.costByCurrency,
|
|
1795
1804
|
currencyByModel: s.currencyByModel,
|
|
1796
1805
|
mixedCurrency: s.mixedCurrency,
|
|
@@ -1938,39 +1947,7 @@ export function makeCostProjection(configOrGetter, services) {
|
|
|
1938
1947
|
}
|
|
1939
1948
|
}
|
|
1940
1949
|
|
|
1941
|
-
|
|
1942
|
-
key: 'queryBalanceCost',
|
|
1943
|
-
// 框架要求的投影定义 API: stateSchema(内部状态) + wire.{viewSchema,view}(客户端可见视图)。
|
|
1944
|
-
// 旧版误用顶层 schema+view, 导致 wire 缺失, 服务端 drive 永不通知、客户端永远拿不到值。
|
|
1945
|
-
stateSchema: z.object({
|
|
1946
|
-
currentModel: z.string().nullable(),
|
|
1947
|
-
currentProvider: z.string().nullable(),
|
|
1948
|
-
last: z.object({
|
|
1949
|
-
turn: z.number(),
|
|
1950
|
-
step: z.number(),
|
|
1951
|
-
model: z.string(),
|
|
1952
|
-
buckets: z.object({
|
|
1953
|
-
uncachedInputTokens: z.number(),
|
|
1954
|
-
cacheReadTokens: z.number(),
|
|
1955
|
-
cacheWriteTokens: z.number(),
|
|
1956
|
-
outputTokens: z.number(),
|
|
1957
|
-
}),
|
|
1958
|
-
}).nullable(),
|
|
1959
|
-
byModel: z.record(z.string(), z.object({
|
|
1960
|
-
uncachedInputTokens: z.number(),
|
|
1961
|
-
cacheReadTokens: z.number(),
|
|
1962
|
-
cacheWriteTokens: z.number(),
|
|
1963
|
-
outputTokens: z.number(),
|
|
1964
|
-
})),
|
|
1965
|
-
modelOrder: z.array(z.string()),
|
|
1966
|
-
/** v1.4.0: 本投影所属会话 id —— 子代理汇总要拿它去查 `subagentCatalog`。空串表示未知。 */
|
|
1967
|
-
sessionId: z.string(),
|
|
1968
|
-
}),
|
|
1969
|
-
init: (header) => ({
|
|
1970
|
-
currentModel: null, currentProvider: null, last: null, byModel: {}, modelOrder: [],
|
|
1971
|
-
sessionId: typeof header?.id === 'string' ? header.id : '',
|
|
1972
|
-
}),
|
|
1973
|
-
apply: (state, event) => {
|
|
1950
|
+
const foldUsage = (state, event) => {
|
|
1974
1951
|
// v1.4.0: `llm/retry-started` 关闭「替换槽位」—— 被重试的那次 attempt 的用量要**留在总量里**,
|
|
1975
1952
|
// 下一次 attempt 是**新增**而不是替换。与 dsh-token-meter 的 usage-projection 对齐。
|
|
1976
1953
|
if (event.type === 'llm/retry-started') {
|
|
@@ -2013,6 +1990,56 @@ export function makeCostProjection(configOrGetter, services) {
|
|
|
2013
1990
|
if (prev !== null) byModel = { ...byModel, [prev.model]: subBuckets(byModel[prev.model] ?? zero(), prev.buckets) }
|
|
2014
1991
|
byModel = { ...byModel, [model]: addBuckets(byModel[model] ?? zero(), buckets) }
|
|
2015
1992
|
return { ...state, currentModel: nextModel, currentProvider: nextProvider, last: { turn, step, model, buckets }, byModel, modelOrder: isNewModel ? [...state.modelOrder, model] : state.modelOrder }
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
return {
|
|
1996
|
+
key: 'queryBalanceCost',
|
|
1997
|
+
// 框架要求的投影定义 API: stateSchema(内部状态) + wire.{viewSchema,view}(客户端可见视图)。
|
|
1998
|
+
// 旧版误用顶层 schema+view, 导致 wire 缺失, 服务端 drive 永不通知、客户端永远拿不到值。
|
|
1999
|
+
stateSchema: z.object({
|
|
2000
|
+
currentModel: z.string().nullable(),
|
|
2001
|
+
currentProvider: z.string().nullable(),
|
|
2002
|
+
last: z.object({
|
|
2003
|
+
turn: z.number(),
|
|
2004
|
+
step: z.number(),
|
|
2005
|
+
model: z.string(),
|
|
2006
|
+
buckets: z.object({
|
|
2007
|
+
uncachedInputTokens: z.number(),
|
|
2008
|
+
cacheReadTokens: z.number(),
|
|
2009
|
+
cacheWriteTokens: z.number(),
|
|
2010
|
+
outputTokens: z.number(),
|
|
2011
|
+
}),
|
|
2012
|
+
}).nullable(),
|
|
2013
|
+
byModel: z.record(z.string(), z.object({
|
|
2014
|
+
uncachedInputTokens: z.number(),
|
|
2015
|
+
cacheReadTokens: z.number(),
|
|
2016
|
+
cacheWriteTokens: z.number(),
|
|
2017
|
+
outputTokens: z.number(),
|
|
2018
|
+
})),
|
|
2019
|
+
modelOrder: z.array(z.string()),
|
|
2020
|
+
/** v1.4.0: 本投影所属会话 id —— 子代理汇总要拿它去查 `subagentCatalog`。空串表示未知。 */
|
|
2021
|
+
sessionId: z.string(),
|
|
2022
|
+
inheritedEventCount: z.number().int().nonnegative(),
|
|
2023
|
+
ownBoundaryKnown: z.boolean(),
|
|
2024
|
+
ownChildIds: z.array(z.string()),
|
|
2025
|
+
own: z.object({ currentModel: z.string().nullable(), currentProvider: z.string().nullable(), last: z.any().nullable(), byModel: z.record(z.string(), z.object({ uncachedInputTokens: z.number().nonnegative(), cacheReadTokens: z.number().nonnegative(), cacheWriteTokens: z.number().nonnegative(), outputTokens: z.number().nonnegative() })), modelOrder: z.array(z.string()) }),
|
|
2026
|
+
}),
|
|
2027
|
+
init: (header, inheritedEventCount) => ({
|
|
2028
|
+
currentModel: null, currentProvider: null, last: null, byModel: {}, modelOrder: [],
|
|
2029
|
+
sessionId: typeof header?.id === 'string' ? header.id : '',
|
|
2030
|
+
inheritedEventCount: Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0 ? inheritedEventCount : 0,
|
|
2031
|
+
ownBoundaryKnown: (Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0) || !header?.isSeeded,
|
|
2032
|
+
ownChildIds: [],
|
|
2033
|
+
own: { currentModel: null, currentProvider: null, last: null, byModel: {}, modelOrder: [] },
|
|
2034
|
+
}),
|
|
2035
|
+
apply: (state, event) => {
|
|
2036
|
+
const full = foldUsage(state, event)
|
|
2037
|
+
const isOwn = state.ownBoundaryKnown && (state.inheritedEventCount === 0 || (Number.isSafeInteger(event.seq) && event.seq >= state.inheritedEventCount))
|
|
2038
|
+
// Inherited model context is useful; inherited usage and retry slots are not.
|
|
2039
|
+
const own = isOwn ? foldUsage(state.own, event) : { ...state.own, currentModel: full.currentModel, currentProvider: full.currentProvider }
|
|
2040
|
+
const childId = isOwn && event.type === 'subagent/catalog' ? event.data?.childId : null
|
|
2041
|
+
const ownChildIds = typeof childId === 'string' && !state.ownChildIds.includes(childId) ? [...state.ownChildIds, childId] : state.ownChildIds
|
|
2042
|
+
return { ...full, own, ownChildIds }
|
|
2016
2043
|
},
|
|
2017
2044
|
wire: {
|
|
2018
2045
|
viewSchema: z.object({
|
|
@@ -2041,6 +2068,7 @@ export function makeCostProjection(configOrGetter, services) {
|
|
|
2041
2068
|
mode: z.enum(['one-shot', 'continuable']),
|
|
2042
2069
|
createdAt: z.number(),
|
|
2043
2070
|
cost: z.number(),
|
|
2071
|
+
waiting: z.boolean().optional(),
|
|
2044
2072
|
costByCurrency: z.record(z.string(), z.number().nonnegative()),
|
|
2045
2073
|
currencyByModel: z.record(z.string(), z.string()),
|
|
2046
2074
|
mixedCurrency: z.boolean(),
|
|
@@ -2057,7 +2085,7 @@ export function makeCostProjection(configOrGetter, services) {
|
|
|
2057
2085
|
const cfg = getConfig()
|
|
2058
2086
|
const mainCurrency = (cfg.currency ?? 'CNY').toUpperCase()
|
|
2059
2087
|
// v1.4.0: 子代理消耗 (换行单独展示)。取不到服务/没有子代理 → 空数组。
|
|
2060
|
-
const subagents = collectSubagentCosts(services, state.sessionId, summarize)
|
|
2088
|
+
const subagents = collectSubagentCosts(services, state.sessionId, summarize, state.ownChildIds)
|
|
2061
2089
|
// 无事件时返回 waiting 标记, 客户端据此显示 "~—" 而非 "~¥0"
|
|
2062
2090
|
if (state.modelOrder.length === 0) {
|
|
2063
2091
|
return { models: [], currentModel: state.currentModel ?? null, currentProvider: state.currentProvider ?? null, cost: -1, costByModel: {}, costByCurrency: {}, currencyByModel: {}, mixedCurrency: false, tokens: { uncachedInput: 0, cacheRead: 0, cacheWrite: 0, output: 0 }, tokensByModel: {}, currency: mainCurrency, isPeak: isPeakTime(), waiting: true, subagents }
|
|
@@ -2071,7 +2099,7 @@ export function makeCostProjection(configOrGetter, services) {
|
|
|
2071
2099
|
}
|
|
2072
2100
|
},
|
|
2073
2101
|
},
|
|
2074
|
-
stateVersion:
|
|
2102
|
+
stateVersion: 3,
|
|
2075
2103
|
}
|
|
2076
2104
|
}
|
|
2077
2105
|
|