dsh-whale-widget 0.2.2 → 0.2.5
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 +62 -8
- package/assets/DSH2.png +0 -0
- package/assets/DSniang02.png +0 -0
- package/assets/rua.gif +0 -0
- package/lib/index.js +363 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# DSH 小鲸鱼余额挂件(DeepSeek Balance Whale Widget)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
DeepSeek Harness(DSH)Web 界面右下角的常驻余额挂件:小鲸鱼气泡图 + DeepSeek API 余额 + 今日已用 + 每轮对话消耗统计,每次打开界面自动启用。本项目是标准 DSH 插件包,可通过 `dsh plugin` 安装/卸载。
|
|
4
6
|
|
|
5
7
|
## 特性
|
|
6
8
|
|
|
@@ -9,12 +11,15 @@ DeepSeek Harness(DSH)Web 界面右下角的常驻余额挂件:小鲸鱼气
|
|
|
9
11
|
- 📊 **今日已用**:两种模式任选(见下),显示今日消耗金额
|
|
10
12
|
- **小鲸鱼记账(推荐,免令牌)**:不需要任何会话令牌,鲸鱼娘每次观测余额后用余额差值自动记账(`.dshw-usage.json`,跨天自动归零归档)
|
|
11
13
|
- **实时·令牌**:填入平台会话令牌后直接调用平台用量接口,按**峰谷定价**(空闲 9:00–12:00 与 14:00–18:00 之外 / 高峰 9–12 与 14–18 点)实时换算今日已用
|
|
14
|
+
- 💬 **每轮对话消耗统计**:监听本机会话事件,每轮对话结束后弹出本轮消耗金额(精确 usage,非估算)
|
|
15
|
+
- 菜单可开关「每轮对话后自动显示消耗金额」;「自动关闭时间」可自定义秒数(填 0 表示不自动关闭)
|
|
16
|
+
- 消耗金额泡泡显示期间,余额变动不弹普通泡泡
|
|
12
17
|
- 🖱️ **拖拽 + 四边四分之一吸附**(左/右/上/下,角落可组合)
|
|
13
18
|
- 🔄 左吸附时整体**水平镜像翻转**(文字同步反向、带动画)
|
|
14
19
|
- 🧸 **按压 Q 弹**玩偶效果(按压时底部坐标不变)
|
|
15
|
-
- 🎚️ **汉堡菜单**(悬停鲸鱼右上角出现):大小滑块(0.6–2.5
|
|
20
|
+
- 🎚️ **汉堡菜单**(悬停鲸鱼右上角出现):大小滑块(0.6–2.5 倍)、音效切换(小黄鸭 / 音效1)、音量调节、用量模式、峰谷提示文案(默认 / 梁文峰谷 / !?强强?!)、气泡开关、每轮消耗开关与自动关闭时间
|
|
16
21
|
- 🔊 **音效**:按压/松手音效(可选包内 mp3,缺失时静默降级)
|
|
17
|
-
- 💬
|
|
22
|
+
- 💬 **随机台词**:点击气泡切换随机台词段(加权随机,含峰谷提示/今日已用/gif 动图/卖萌吐槽),再点一次关闭;气泡总显示 5 秒自动收起
|
|
18
23
|
- 📐 随浏览器窗口自动缩放;文字位置/字号与图片联动
|
|
19
24
|
|
|
20
25
|
## 目录结构
|
|
@@ -27,7 +32,10 @@ dsh-whale-widget/
|
|
|
27
32
|
├── lib/
|
|
28
33
|
│ └── index.js # 宿主侧插件本体
|
|
29
34
|
├── assets/
|
|
35
|
+
│ ├── DSH2.png # README 顶部展示图
|
|
30
36
|
│ ├── DSniang1.png # 小鲸鱼本体(cut-out,气泡由代码绘制)
|
|
37
|
+
│ ├── DSniang02.png # 备用整图(兼容旧版手动安装路径)
|
|
38
|
+
│ ├── rua.gif # 随机台词 gif(可选)
|
|
31
39
|
│ ├── Ya1.mp3 / Ya2.mp3 # 小黄鸭音效(可选)
|
|
32
40
|
│ └── D1.mp3 / D2.mp3 # 音效1(可选)
|
|
33
41
|
└── whale-widget-prompt.md # 完整规格/维护提示词
|
|
@@ -69,6 +77,12 @@ npm publish
|
|
|
69
77
|
dsh plugin --profile web add dsh-whale-widget
|
|
70
78
|
```
|
|
71
79
|
|
|
80
|
+
### 关于令牌(安装后必读)
|
|
81
|
+
|
|
82
|
+
> **默认不需要任何令牌。** 安装后只需配置 `DEEPSEEK_API_KEY`(拉取余额必需),「今日已用」会自动使用默认的**小鲸鱼记账**模式(余额差值本地记账),开箱即用。
|
|
83
|
+
>
|
|
84
|
+
> 「实时·令牌」模式用到的 `DEEPSEEK_PLATFORM_TOKEN`(DeepSeek 平台网页会话令牌)是**可选的**,仅在你想要更精确的实时用量换算时才需要配置。获取方式见下方「用量模式使用教程」。
|
|
85
|
+
|
|
72
86
|
## 卸载
|
|
73
87
|
|
|
74
88
|
```powershell
|
|
@@ -104,11 +118,48 @@ Remove-Item "$web\DSniang02.png" -ErrorAction SilentlyContinue
|
|
|
104
118
|
|
|
105
119
|
清理后再执行上面的安装命令。
|
|
106
120
|
|
|
107
|
-
##
|
|
121
|
+
## 用量模式使用教程
|
|
122
|
+
|
|
123
|
+
### 必需的凭据
|
|
124
|
+
|
|
125
|
+
- **`DEEPSEEK_API_KEY`**(必需):DeepSeek API 密钥,用于拉取余额(`api.deepseek.com/user/balance`)。在 DSH 凭据服务中配置即可(`dsh` 的凭据管理界面 / `.dsh/.credentials.yaml`)。
|
|
126
|
+
|
|
127
|
+
### 两种用量模式
|
|
128
|
+
|
|
129
|
+
挂件的「今日已用」有两种模式,在**菜单 → 用量**中选择:
|
|
130
|
+
|
|
131
|
+
**① 小鲸鱼记账(推荐,默认)—— 完全不需要额外配置**
|
|
132
|
+
|
|
133
|
+
鲸鱼娘自己用**余额差值**记账:每次观测到余额下降就把差值累加到当天用量,跨天自动归零归档(保留 30 天)。只要配好了 `DEEPSEEK_API_KEY` 就能用,**开箱即用**。
|
|
134
|
+
|
|
135
|
+
- 账本文件:`$DSH_HOME/.dshw-usage.json`(自动生成)
|
|
136
|
+
- 优点:零配置、免令牌
|
|
137
|
+
- 说明:依赖「观测到的余额下降」累计,若 DSH 关闭期间有消耗会漏记;要精确请用令牌模式
|
|
138
|
+
|
|
139
|
+
**② 实时·令牌(可选)—— 需要 `DEEPSEEK_PLATFORM_TOKEN`**
|
|
140
|
+
|
|
141
|
+
鲸鱼娘直接调用 DeepSeek 平台用量接口,按**峰谷定价**实时换算今日已用,**精确到每小时的 token 用量**。
|
|
142
|
+
|
|
143
|
+
**令牌在哪获取:**
|
|
144
|
+
1. 浏览器打开并登录 **https://platform.deepseek.com**
|
|
145
|
+
2. 按 **F12** 打开开发者工具 → 切到 **Network(网络)** 标签
|
|
146
|
+
3. 在平台页面点击「用量」或刷新页面,找到名为 `usage/by_api_key/amount` 的请求
|
|
147
|
+
4. 点开该请求 → **Request Headers(请求标头)** → 复制 `Authorization` 的值(形如 `Bearer eyJ...` 的一长串)
|
|
148
|
+
5. 把整段值(含 `Bearer` 前缀或只要后面的 token 部分均可)配置为 DSH 凭据 `DEEPSEEK_PLATFORM_TOKEN`:
|
|
149
|
+
```powershell
|
|
150
|
+
# 在 DSH 凭据服务中设置,例如编辑 $env:USERPROFILE\.dsh\.credentials.yaml
|
|
151
|
+
# DEEPSEEK_PLATFORM_TOKEN: <你复制的令牌>
|
|
152
|
+
```
|
|
153
|
+
6. 重启 `dsh web`,在**菜单 → 用量**里选择「实时·令牌」
|
|
154
|
+
|
|
155
|
+
**说明:**
|
|
156
|
+
- ⚠️ **令牌非必需**:不配置时挂件自动使用默认的「小鲸鱼记账」模式,功能不受影响
|
|
157
|
+
- 该令牌是 DeepSeek **平台网页的会话令牌**(不是 `sk-` 开头的 API key),仅在登录平台网页时有效;重新登录后可能需要重新获取
|
|
158
|
+
- 接口不返回金额,只返回 token 分桶,挂件会按内置峰谷定价表换算成金额;定价表在 `lib/index.js` 顶部 `PRICING` 常量,DeepSeek 调价时可自行修改
|
|
159
|
+
|
|
160
|
+
### 每轮对话消耗(无需任何凭据)
|
|
108
161
|
|
|
109
|
-
|
|
110
|
-
- **实时·令牌模式**:需要 `DEEPSEEK_PLATFORM_TOKEN`(平台会话令牌,从浏览器 DevTools 的用量请求 `Authorization` 头获取),用于 `platform.deepseek.com/api/v0/usage/by_api_key/amount`
|
|
111
|
-
- **小鲸鱼记账模式(默认)**:不需要平台令牌,鲸鱼娘用余额差值自动记账,跨天自动归档
|
|
162
|
+
「每轮对话消耗统计」直接监听 DSH 本机会话事件,按模型真实 usage 换算金额(与今日已用同一套峰谷定价表),**不需要** `DEEPSEEK_PLATFORM_TOKEN`。
|
|
112
163
|
|
|
113
164
|
## 验证
|
|
114
165
|
|
|
@@ -118,11 +169,13 @@ dsh --profile web --dump-config | Select-String -Pattern "whale"
|
|
|
118
169
|
curl http://127.0.0.1:3080/dsh-whale/image.png
|
|
119
170
|
curl http://127.0.0.1:3080/dsh-whale/balance.json
|
|
120
171
|
curl http://127.0.0.1:3080/dsh-whale/size.json
|
|
172
|
+
curl http://127.0.0.1:3080/dsh-whale/last-turn.json
|
|
121
173
|
```
|
|
122
174
|
|
|
123
175
|
- `/dsh-whale/image.png` → 200 `image/png`
|
|
124
176
|
- `/dsh-whale/balance.json` → 200,含 `{"ok":true,"totalBalance":...,"currency":"CNY","todayUsage":...}`
|
|
125
|
-
- `/dsh-whale/size.json` → GET
|
|
177
|
+
- `/dsh-whale/size.json` → GET 返回配置;PUT 写入
|
|
178
|
+
- `/dsh-whale/last-turn.json` → 200,含最近一轮对话消耗 `{seq, turn, amount, tokens}`
|
|
126
179
|
- 浏览器 F5 后右下角出现挂件
|
|
127
180
|
|
|
128
181
|
## 常见问题
|
|
@@ -131,6 +184,7 @@ curl http://127.0.0.1:3080/dsh-whale/size.json
|
|
|
131
184
|
- **图片不显示**:确认 `assets/DSniang1.png` 在插件包内,且没有把旧文件放在 profile 里占用了同名路由。
|
|
132
185
|
- **余额报「未配置 DEEPSEEK_API_KEY」**:去 DSH 配置凭据。
|
|
133
186
|
- **今日已用显示 --**:记账模式下需要先跑一次余额观测(60 秒内自动完成);令牌模式需要配置 `DEEPSEEK_PLATFORM_TOKEN`。
|
|
187
|
+
- **每轮消耗不显示**:确认菜单「每轮对话后自动显示消耗金额」已勾选;一轮对话必须完整结束(turn/end)才会结算。
|
|
134
188
|
- **没有声音**:确认 `assets/*.mp3` 在包内;若不想带音效文件,静默降级为无声音。
|
|
135
189
|
- **本地开发改了代码不生效**:使用 `link:` 安装时,修改源码后重启 `dsh web`(ESM 模块缓存);如果用已发布版本,需要 `npm publish` 新版本后 `dsh plugin --profile web update dsh-whale-widget`。
|
|
136
190
|
- **自定义图片**:气泡由代码绘制(SVG),鲸鱼本体为 cut-out PNG,放在右下角 59.45%;换图需保证透明背景 cut-out,否则按 `whale-widget-prompt.md` 调整几何参数。
|
package/assets/DSH2.png
ADDED
|
Binary file
|
|
Binary file
|
package/assets/rua.gif
ADDED
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
|
|
6
|
-
// Package root: lib/index.js -> package root.
|
|
6
|
+
// Package root: lib/index.js -> package root. Keeps the bundle relocatable
|
|
7
7
|
// when installed as a normal DSH npm plugin (node_modules or a local link).
|
|
8
8
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
9
|
|
|
@@ -11,11 +11,10 @@ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
|
|
11
11
|
// be read-only or cleaned on update.
|
|
12
12
|
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
// no matter where it is installed. Legacy absolute paths remain as harmless
|
|
16
|
-
// fallbacks for older manual installs.
|
|
14
|
+
// Whale image: package-relative first, legacy absolute paths as fallback.
|
|
17
15
|
const IMAGE_CANDIDATES = [
|
|
18
16
|
path.join(PACKAGE_ROOT, 'assets', 'DSniang1.png'),
|
|
17
|
+
path.join(PACKAGE_ROOT, 'assets', 'DSniang02.png'),
|
|
19
18
|
'D:/TestBox/deepseek/DSniang1.png',
|
|
20
19
|
'D:/TestBox/deepseek/DSniang02.png',
|
|
21
20
|
'D:/TestBox/deepseek/skin/DSniang02.png',
|
|
@@ -37,8 +36,8 @@ const USAGE_FILE_CANDIDATES = [
|
|
|
37
36
|
'D:/TestBox/deepseek/skin/.dshw-usage.json',
|
|
38
37
|
]
|
|
39
38
|
|
|
40
|
-
// Sound assets: package-relative first (
|
|
41
|
-
//
|
|
39
|
+
// Sound assets: package-relative first (ship Ya1/Ya2/D1/D2.mp3 in assets/ for
|
|
40
|
+
// sounds out of the box), legacy paths as fallback.
|
|
42
41
|
const SOUND_SETS = {
|
|
43
42
|
duck: {
|
|
44
43
|
press: [path.join(PACKAGE_ROOT, 'assets', 'Ya1.mp3'), 'D:/TestBox/deepseek/skin/Ya1.mp3'],
|
|
@@ -58,6 +57,11 @@ function soundSetFromUrl(url) {
|
|
|
58
57
|
}
|
|
59
58
|
const BALANCE_URL = 'https://api.deepseek.com/user/balance'
|
|
60
59
|
const BALANCE_TTL_MS = 25000
|
|
60
|
+
const RUA_GIF_CANDIDATES = [
|
|
61
|
+
path.join(PACKAGE_ROOT, 'assets', 'rua.gif'),
|
|
62
|
+
'D:/TestBox/deepseek/skin/rua.gif',
|
|
63
|
+
'D:/TestBox/deepseek/rua.gif',
|
|
64
|
+
]
|
|
61
65
|
// DeepSeek CNY prices per million tokens: [空闲时段价, 高峰时段价].
|
|
62
66
|
// 高峰时段:每日 9:00–12:00 和 14:00–18:00(北京时间)。Adjust here if DeepSeek changes pricing.
|
|
63
67
|
const PEAK_HOURS = [
|
|
@@ -112,6 +116,7 @@ var FETCH_TIMEOUT_MS = 25000
|
|
|
112
116
|
var BALANCE_URL = '/dsh-whale/balance.json'
|
|
113
117
|
var SIZE_URL = '/dsh-whale/size.json'
|
|
114
118
|
var IMG_URL = '/dsh-whale/image.png?v=2'
|
|
119
|
+
var GIF_URL = '/dsh-whale/rua.gif'
|
|
115
120
|
|
|
116
121
|
var css = [
|
|
117
122
|
'.dshwv-root{position:fixed;right:0;bottom:0;--dshw-scale:1;--dshw-base:clamp(122px,calc(min(250px,min(100vw,100vh) * 0.28) * var(--dshw-scale)),625px);width:var(--dshw-base);height:var(--dshw-base);pointer-events:none;user-select:none;-webkit-user-select:none;z-index:9999;font-family:inherit;transition:left .16s ease,top .16s ease,transform .3s ease}',
|
|
@@ -119,26 +124,29 @@ var css = [
|
|
|
119
124
|
'.dshwv-root.dshwv-dragging{cursor:grabbing;transition:none}',
|
|
120
125
|
'.dshwv-body{position:absolute;left:0;top:0;width:100%;height:100%;transform-origin:50% 100%;transition:transform .22s cubic-bezier(.34,1.56,.64,1)}',
|
|
121
126
|
'.dshwv-img{position:absolute;right:0;bottom:0;width:59.45%;height:59.45%;display:block;pointer-events:none;-webkit-user-drag:none;user-select:none}',
|
|
122
|
-
'.dshwv-bubble{position:absolute;left:0;top:0;width:100%;aspect-ratio:1026/700;pointer-events:none;z-index:1}',
|
|
127
|
+
'.dshwv-bubble{position:absolute;left:0;top:0;width:100%;aspect-ratio:1026/700;pointer-events:none;z-index:1;--dshw-u:calc(var(--dshw-base) / 1026)}',
|
|
123
128
|
'.dshwv-bubble svg{display:block;width:100%;height:100%;pointer-events:none}',
|
|
124
129
|
'.dshwv-bubble svg path,.dshwv-bubble svg ellipse{pointer-events:none;cursor:pointer}',
|
|
125
130
|
'.dshwv-bubble.dshwv-bubble-open svg path,.dshwv-bubble.dshwv-bubble-open svg ellipse{pointer-events:visiblePainted}',
|
|
126
131
|
'.dshwv-bubble .dshwv-bshape,.dshwv-bubble .dshwv-b1,.dshwv-bubble .dshwv-b2{opacity:0;transform:scale(.7);transform-box:fill-box;transform-origin:50% 50%;transition:opacity .2s ease,transform .2s ease}',
|
|
127
132
|
'.dshwv-bubble.dshwv-bubble-open .dshwv-bshape,.dshwv-bubble.dshwv-bubble-open .dshwv-b1,.dshwv-bubble.dshwv-bubble-open .dshwv-b2{opacity:1;transform:none}',
|
|
133
|
+
'.dshwv-gif{position:absolute;left:44.25%;top:38%;transform:translate(-50%,-50%);max-width:calc(var(--dshw-u) * 560);max-height:calc(var(--dshw-u) * 400);display:none;opacity:0;transition:opacity .2s ease;pointer-events:none;-webkit-user-drag:none;user-select:none;object-fit:contain}',
|
|
134
|
+
'.dshwv-root.dshwv-left .dshwv-gif{transform:translate(-50%,-50%) scaleX(-1)}',
|
|
135
|
+
'.dshwv-bubble.dshwv-bubble-open .dshwv-gif{opacity:1}',
|
|
128
136
|
'.dshwv-bubble.dshwv-bubble-open .dshwv-b2{transition-delay:0s}',
|
|
129
137
|
'.dshwv-bubble.dshwv-bubble-open .dshwv-b1{transition-delay:.13s}',
|
|
130
138
|
'.dshwv-bubble.dshwv-bubble-open .dshwv-bshape{transition-delay:.26s}',
|
|
131
139
|
'.dshwv-bubble .dshwv-bshape{transition-delay:.1s}',
|
|
132
140
|
'.dshwv-bubble .dshwv-b1{transition-delay:.2s}',
|
|
133
141
|
'.dshwv-bubble .dshwv-b2{transition-delay:.3s}',
|
|
134
|
-
'.dshwv-text{position:absolute;left:44.25%;top:38%;transform:translate(-50%,-50%);text-align:center;color:#536ba9;line-height:1.15;white-space:nowrap
|
|
142
|
+
'.dshwv-text{position:absolute;left:44.25%;top:38%;transform:translate(-50%,-50%);text-align:center;color:#536ba9;line-height:1.15;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .16s ease,transform .3s ease}',
|
|
135
143
|
'.dshwv-bubble.dshwv-bubble-open .dshwv-text{opacity:1;transition:opacity .16s ease .36s,transform .3s ease}',
|
|
136
144
|
'.dshwv-root.dshwv-left .dshwv-text{transform:translate(-50%,-50%) scaleX(-1)}',
|
|
137
145
|
'.dshwv-label{font-size:calc(var(--dshw-u) * 66);font-weight:600;letter-spacing:.06em}',
|
|
138
146
|
'.dshwv-amount{font-size:calc(var(--dshw-u) * 128);font-weight:800;line-height:1.05}',
|
|
139
147
|
'.dshwv-period{font-size:calc(var(--dshw-u) * 104);font-weight:800;line-height:1.05}',
|
|
140
148
|
'.dshwv-wrap{white-space:normal;max-width:calc(var(--dshw-u) * 560);line-height:1.2}',
|
|
141
|
-
'.dshwv-hint{font-size:calc(var(--dshw-u) * 56);color:#9fb0d9;letter-spacing:.02em;margin-top:calc(var(--dshw-u) * 9)}',
|
|
149
|
+
'.dshwv-hint{font-size:calc(var(--dshw-u) * 56);color:#9fb0d9;letter-spacing:.02em;margin-top:calc(var(--dshw-u) * 9);min-height:calc(var(--dshw-u) * 64);line-height:1.15}',
|
|
142
150
|
'.dshwv-menu-btn{position:absolute;top:calc(40.55% + 4px);right:4px;width:26px;height:26px;border:none;border-radius:6px;background:rgba(32,49,112,.85);cursor:pointer;pointer-events:auto;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:0;z-index:2;opacity:0;transition:opacity .15s ease}',
|
|
143
151
|
'.dshwv-menu-btn.dshwv-menu-btn-visible{opacity:1}',
|
|
144
152
|
'.dshwv-menu-btn span{display:block;width:14px;height:2px;background:#fff;border-radius:1px}',
|
|
@@ -150,6 +158,8 @@ var css = [
|
|
|
150
158
|
'.dshwv-number{width:46px;border:1px solid rgba(32,49,112,.4);border-radius:6px;padding:2px 4px;font-size:12px;color:#203170;background:#fff}',
|
|
151
159
|
'.dshwv-sound{flex:1;border:1px solid rgba(32,49,112,.4);border-radius:6px;background:rgba(32,49,112,.08);color:#203170;font-size:12px;padding:3px 0;cursor:pointer}',
|
|
152
160
|
'.dshwv-sound:hover{background:rgba(32,49,112,.16)}',
|
|
161
|
+
'.dshwv-check{width:16px;height:16px;accent-color:#203170;cursor:pointer;flex:0 0 auto}',
|
|
162
|
+
'.dshwv-menu-sep{height:1px;background:rgba(32,49,112,.25);margin:6px 0}',
|
|
153
163
|
'.dshwv-volpct{width:36px;text-align:right;color:#203170;font-size:12px}'
|
|
154
164
|
].join('\\n')
|
|
155
165
|
|
|
@@ -224,6 +234,32 @@ usageSelect.className = 'dshwv-sound'
|
|
|
224
234
|
usageSelect.appendChild(soundOpt('ledger', '小鲸鱼记账 (推荐)'))
|
|
225
235
|
usageSelect.appendChild(soundOpt('token', '实时·令牌 (用法:去问dsh)'))
|
|
226
236
|
usageSelect.addEventListener('change', function () { setUsageMode(usageSelect.value) })
|
|
237
|
+
var peakSelect = document.createElement('select')
|
|
238
|
+
peakSelect.className = 'dshwv-sound'
|
|
239
|
+
peakSelect.appendChild(soundOpt('default', '默认'))
|
|
240
|
+
peakSelect.appendChild(soundOpt('liangwen', '梁文峰谷'))
|
|
241
|
+
peakSelect.appendChild(soundOpt('qiangqiang', '!?强强?!'))
|
|
242
|
+
peakSelect.addEventListener('change', function () { setPeakMode(peakSelect.value) })
|
|
243
|
+
var bubbleToggle = document.createElement('input')
|
|
244
|
+
bubbleToggle.type = 'checkbox'
|
|
245
|
+
bubbleToggle.className = 'dshwv-check'
|
|
246
|
+
bubbleToggle.checked = true
|
|
247
|
+
bubbleToggle.title = '开启/关闭思考气泡'
|
|
248
|
+
bubbleToggle.addEventListener('change', function () { setBubbleOn(bubbleToggle.checked) })
|
|
249
|
+
var turnCostToggle = document.createElement('input')
|
|
250
|
+
turnCostToggle.type = 'checkbox'
|
|
251
|
+
turnCostToggle.className = 'dshwv-check'
|
|
252
|
+
turnCostToggle.checked = true
|
|
253
|
+
turnCostToggle.title = '每轮对话结束后自动显示本轮消耗金额'
|
|
254
|
+
turnCostToggle.addEventListener('change', function () { setTurnCostOn(turnCostToggle.checked) })
|
|
255
|
+
var turnCostCloseInput = document.createElement('input')
|
|
256
|
+
turnCostCloseInput.type = 'number'
|
|
257
|
+
turnCostCloseInput.min = '0'
|
|
258
|
+
turnCostCloseInput.step = '1'
|
|
259
|
+
turnCostCloseInput.className = 'dshwv-number'
|
|
260
|
+
turnCostCloseInput.value = '5'
|
|
261
|
+
turnCostCloseInput.title = '填 0 表示不自动关闭,需手动点击关闭'
|
|
262
|
+
turnCostCloseInput.addEventListener('change', function () { setTurnCostClose(turnCostCloseInput.value) })
|
|
227
263
|
var row1 = menuRow()
|
|
228
264
|
row1.appendChild(menuLabel('大小'))
|
|
229
265
|
row1.appendChild(scaleInput)
|
|
@@ -249,10 +285,30 @@ row3.appendChild(volPct)
|
|
|
249
285
|
var row4 = menuRow()
|
|
250
286
|
row4.appendChild(menuLabel('用量'))
|
|
251
287
|
row4.appendChild(usageSelect)
|
|
288
|
+
var row5 = menuRow()
|
|
289
|
+
row5.appendChild(menuLabel('峰谷'))
|
|
290
|
+
row5.appendChild(peakSelect)
|
|
291
|
+
var row6 = menuRow()
|
|
292
|
+
row6.appendChild(menuLabel('气泡'))
|
|
293
|
+
row6.appendChild(bubbleToggle)
|
|
294
|
+
var menuSep1 = document.createElement('div')
|
|
295
|
+
menuSep1.className = 'dshwv-menu-sep'
|
|
296
|
+
var row7 = menuRow()
|
|
297
|
+
row7.appendChild(menuLabel('每轮消耗'))
|
|
298
|
+
row7.appendChild(turnCostToggle)
|
|
299
|
+
var row8 = menuRow()
|
|
300
|
+
row8.appendChild(menuLabel('自动关闭时间'))
|
|
301
|
+
row8.appendChild(turnCostCloseInput)
|
|
302
|
+
row8.appendChild(menuLabel('秒'))
|
|
252
303
|
menuBox.appendChild(row1)
|
|
253
304
|
menuBox.appendChild(row2)
|
|
254
305
|
menuBox.appendChild(row3)
|
|
255
306
|
menuBox.appendChild(row4)
|
|
307
|
+
menuBox.appendChild(row5)
|
|
308
|
+
menuBox.appendChild(row6)
|
|
309
|
+
menuBox.appendChild(menuSep1)
|
|
310
|
+
menuBox.appendChild(row7)
|
|
311
|
+
menuBox.appendChild(row8)
|
|
256
312
|
|
|
257
313
|
var textBox = document.createElement('div')
|
|
258
314
|
textBox.className = 'dshwv-text'
|
|
@@ -274,10 +330,21 @@ bubbleBox.innerHTML = '<svg viewBox="0 0 1026 700" preserveAspectRatio="xMidYMid
|
|
|
274
330
|
'<ellipse class="dshwv-b1" cx="352" cy="561" rx="37.5" ry="26" fill="#FFFFFF" stroke="#203170" stroke-width="18"/>' +
|
|
275
331
|
'<ellipse class="dshwv-b2" cx="442" cy="646" rx="24.5" ry="18" fill="#FFFFFF" stroke="#203170" stroke-width="18"/>' +
|
|
276
332
|
'</svg>'
|
|
333
|
+
var gifEl = document.createElement('img')
|
|
334
|
+
gifEl.className = 'dshwv-gif'
|
|
335
|
+
gifEl.src = GIF_URL
|
|
336
|
+
gifEl.alt = ''
|
|
337
|
+
gifEl.draggable = false
|
|
338
|
+
bubbleBox.appendChild(gifEl)
|
|
277
339
|
bubbleBox.appendChild(textBox)
|
|
278
340
|
bubbleBox.addEventListener('click', function (e) {
|
|
279
341
|
e.stopPropagation()
|
|
280
342
|
if (!bubbleShown) return
|
|
343
|
+
if (costBubbleActive) {
|
|
344
|
+
// 消耗金额泡泡:点击关闭(确认)
|
|
345
|
+
hideCostBubble()
|
|
346
|
+
return
|
|
347
|
+
}
|
|
281
348
|
if (bubbleRandomActive) {
|
|
282
349
|
// 再次点击:关闭
|
|
283
350
|
hideBubble()
|
|
@@ -333,18 +400,27 @@ function pickOne(arr) { return arr[Math.floor(Math.random() * arr.length)] }
|
|
|
333
400
|
function singleCenter(style, text, color, wrap) { return [null, { t: text, s: style, c: color || '', w: !!wrap }, null] }
|
|
334
401
|
function buildGroup1() {
|
|
335
402
|
var peak = !!state.isPeak
|
|
403
|
+
var offText = '空闲时段'
|
|
404
|
+
var peakText = '高峰时段'
|
|
405
|
+
if (peakMode === 'liangwen') {
|
|
406
|
+
offText = '梁文谷'
|
|
407
|
+
peakText = '梁文峰'
|
|
408
|
+
} else if (peakMode === 'qiangqiang') {
|
|
409
|
+
offText = '!?谷谷?!'
|
|
410
|
+
peakText = '!?峰峰?!'
|
|
411
|
+
}
|
|
336
412
|
return [
|
|
337
413
|
{ t: '当前时间段为:', s: 'A', c: '' },
|
|
338
|
-
{ t: peak ?
|
|
414
|
+
{ t: peak ? peakText : offText, s: 'P', c: peak ? '#e0433f' : '#2fa24c' },
|
|
339
415
|
{ t: '今日已用 ' + fmt(state.todayUsage, state.currency), s: 'C', c: '' },
|
|
340
416
|
]
|
|
341
417
|
}
|
|
342
418
|
var RANDOM_GROUPS = [
|
|
343
|
-
{ w:
|
|
419
|
+
{ w: 45, lines: buildGroup1 },
|
|
344
420
|
{ w: 7, lines: function () { return singleCenter('B', pickOne(['好模型... ↓', '好女孩...↓'])) } },
|
|
345
421
|
{ w: 7, lines: function () { return singleCenter('A', pickOne(['不知道用户有什么用,先赶走吧~', '我...我...我也要挣钱吗?', '我去吃饭啦,测完叫我', '压力一只蓝色大肥鱼?!', 'DeepSleep...', '坏了...用户彻底怒了!']), '', true) } },
|
|
422
|
+
{ w: 10, lines: function () { return { gif: true } } },
|
|
346
423
|
{ w: 3, lines: function () { return singleCenter('A', pickOne(['你目录里的dsh是什么...大烧货吗...?', '恭喜你实现token自由!token全跑了!', '真当我是便宜货啊...']), '', true) } },
|
|
347
|
-
{ w: 1, lines: function () { return [{ t: '这个', s: 'A', c: '' }, { t: '凶', s: 'B', c: '' }, { t: '是什么意思呀...', s: 'A', c: '' }] } },
|
|
348
424
|
{ w: 1, lines: function () { return singleCenter('B', '哦鲸鲸... ') } },
|
|
349
425
|
]
|
|
350
426
|
function pickRandomLines() {
|
|
@@ -358,6 +434,19 @@ function pickRandomLines() {
|
|
|
358
434
|
return RANDOM_GROUPS[RANDOM_GROUPS.length - 1].lines()
|
|
359
435
|
}
|
|
360
436
|
function applyBubbleLines(lines) {
|
|
437
|
+
if (lines && lines.gif) {
|
|
438
|
+
// gif 台词组:只显示 gif,隐藏三行文字(display 必须显式覆盖 CSS 的 none)
|
|
439
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
440
|
+
gifEl.style.display = 'block'
|
|
441
|
+
gifEl.style.opacity = ''
|
|
442
|
+
labelEl.style.display = 'none'
|
|
443
|
+
amountEl.style.display = 'none'
|
|
444
|
+
hintEl.style.display = 'none'
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
448
|
+
gifEl.style.display = 'none'
|
|
449
|
+
gifEl.style.opacity = ''
|
|
361
450
|
var els = [labelEl, amountEl, hintEl]
|
|
362
451
|
for (var i = 0; i < 3; i++) {
|
|
363
452
|
var el = els[i]
|
|
@@ -376,13 +465,16 @@ function applyBubbleLines(lines) {
|
|
|
376
465
|
}
|
|
377
466
|
var bubbleSwapTimer = null
|
|
378
467
|
var hintFadeTimer = null
|
|
468
|
+
var gifFadeTimer = null
|
|
379
469
|
var lastHintText = null
|
|
380
470
|
function setHint(text) {
|
|
381
|
-
//
|
|
471
|
+
// 首次/恢复(lastHintText===null)时直接写文本,不做淡出淡入——否则
|
|
472
|
+
// 气泡打开或按压重开时会先淡出再淡入,造成「消失一下又出现」。
|
|
473
|
+
// 只有气泡打开期间的内容变化(加载中→今日已用)才走动画。
|
|
382
474
|
if (text === lastHintText) return
|
|
475
|
+
var first = lastHintText === null
|
|
383
476
|
lastHintText = text
|
|
384
|
-
if (
|
|
385
|
-
if (!bubbleShown) {
|
|
477
|
+
if (first || !bubbleShown) {
|
|
386
478
|
hintEl.textContent = text
|
|
387
479
|
return
|
|
388
480
|
}
|
|
@@ -415,21 +507,30 @@ function swapBubbleContent(applyFn) {
|
|
|
415
507
|
function restoreBubbleLines() {
|
|
416
508
|
if (bubbleSwapTimer) { clearTimeout(bubbleSwapTimer); bubbleSwapTimer = null }
|
|
417
509
|
if (hintFadeTimer) { clearTimeout(hintFadeTimer); hintFadeTimer = null }
|
|
510
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
418
511
|
lastHintText = null
|
|
419
512
|
textBox.style.transition = ''
|
|
420
513
|
textBox.style.opacity = ''
|
|
514
|
+
gifEl.style.display = 'none'
|
|
515
|
+
gifEl.style.opacity = ''
|
|
421
516
|
labelEl.style.display = ''
|
|
422
517
|
labelEl.className = 'dshwv-label'
|
|
423
518
|
labelEl.textContent = 'DeepSeek 余额'
|
|
424
519
|
labelEl.style.color = ''
|
|
520
|
+
amountEl.style.display = ''
|
|
425
521
|
amountEl.className = 'dshwv-amount'
|
|
426
522
|
amountEl.style.color = ''
|
|
523
|
+
hintEl.style.display = ''
|
|
427
524
|
hintEl.className = 'dshwv-hint'
|
|
428
525
|
hintEl.style.color = ''
|
|
429
526
|
render()
|
|
430
527
|
}
|
|
431
528
|
function showBubble() {
|
|
529
|
+
if (!bubbleOn) return
|
|
530
|
+
// 消耗金额泡泡显示期间,余额变动不再弹出普通泡泡
|
|
531
|
+
if (costBubbleActive) return
|
|
432
532
|
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null }
|
|
533
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
433
534
|
bubbleShown = true
|
|
434
535
|
bubbleRandomActive = false
|
|
435
536
|
restoreBubbleLines()
|
|
@@ -448,7 +549,53 @@ function hideBubble() {
|
|
|
448
549
|
bubbleRandomActive = false
|
|
449
550
|
bubbleRandomLines = null
|
|
450
551
|
bubbleShown = false
|
|
552
|
+
// 只销毁 gif 显示;三行文字保持现状让气泡自然淡出——不能在关闭瞬间
|
|
553
|
+
// 恢复成余额内容(否则随机台词界面会闪现余额)。文字恢复交给下次
|
|
554
|
+
// showBubble() 的 restoreBubbleLines()(那时气泡隐藏,恢复过程不可见)。
|
|
451
555
|
bubbleBox.classList.remove('dshwv-bubble-open')
|
|
556
|
+
// gif 靠 CSS opacity 过渡淡出;display:none 会跳过过渡,须等淡出完成再隐藏
|
|
557
|
+
gifFadeTimer = setTimeout(function () {
|
|
558
|
+
gifFadeTimer = null
|
|
559
|
+
gifEl.style.display = 'none'
|
|
560
|
+
}, 240)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// —— 每轮对话消耗金额泡泡 ——
|
|
564
|
+
var costBubbleTimer = null
|
|
565
|
+
function showCostBubble(amount) {
|
|
566
|
+
if (!bubbleOn || !turnCostOn) return
|
|
567
|
+
if (costBubbleTimer) { clearTimeout(costBubbleTimer); costBubbleTimer = null }
|
|
568
|
+
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null }
|
|
569
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
570
|
+
costBubbleActive = true
|
|
571
|
+
bubbleRandomActive = false
|
|
572
|
+
bubbleShown = true
|
|
573
|
+
lastHintText = null
|
|
574
|
+
// 样式:第一行 A(标签),第二行 B(红色金额),居中两行
|
|
575
|
+
gifEl.style.display = 'none'
|
|
576
|
+
gifEl.style.opacity = ''
|
|
577
|
+
labelEl.style.display = ''
|
|
578
|
+
labelEl.className = 'dshwv-label'
|
|
579
|
+
labelEl.textContent = '上一轮对话消耗:'
|
|
580
|
+
labelEl.style.color = ''
|
|
581
|
+
amountEl.style.display = ''
|
|
582
|
+
amountEl.className = 'dshwv-amount'
|
|
583
|
+
amountEl.textContent = '¥ ' + (isFinite(amount) ? Number(amount).toFixed(2) : '--')
|
|
584
|
+
amountEl.style.color = '#e0433f'
|
|
585
|
+
hintEl.style.display = 'none'
|
|
586
|
+
hintEl.textContent = ''
|
|
587
|
+
hintEl.style.color = ''
|
|
588
|
+
textBox.style.transition = ''
|
|
589
|
+
textBox.style.opacity = ''
|
|
590
|
+
bubbleBox.classList.add('dshwv-bubble-open')
|
|
591
|
+
if (turnCostCloseMs > 0) {
|
|
592
|
+
costBubbleTimer = setTimeout(hideCostBubble, turnCostCloseMs)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function hideCostBubble() {
|
|
596
|
+
if (costBubbleTimer) { clearTimeout(costBubbleTimer); costBubbleTimer = null }
|
|
597
|
+
costBubbleActive = false
|
|
598
|
+
hideBubble()
|
|
452
599
|
}
|
|
453
600
|
|
|
454
601
|
function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v) }
|
|
@@ -464,6 +611,8 @@ function fmt(balance, currency) {
|
|
|
464
611
|
return currency === 'CNY' ? '¥ ' + fixed : fixed + ' ' + currency
|
|
465
612
|
}
|
|
466
613
|
function animateAmount(from, to, currency, duration) {
|
|
614
|
+
// 消耗金额泡泡显示期间,余额数字滚动不触碰金额行
|
|
615
|
+
if (costBubbleActive) return
|
|
467
616
|
if (animId) cancelAnimationFrame(animId)
|
|
468
617
|
if (from === null || !isFinite(from)) from = to
|
|
469
618
|
if (from === to) {
|
|
@@ -489,6 +638,8 @@ function animateAmount(from, to, currency, duration) {
|
|
|
489
638
|
animId = requestAnimationFrame(step)
|
|
490
639
|
}
|
|
491
640
|
function render() {
|
|
641
|
+
// 消耗金额泡泡显示期间,余额渲染不覆盖其内容(金额行/标题行/提示行)
|
|
642
|
+
if (costBubbleActive) return
|
|
492
643
|
var amount, hint
|
|
493
644
|
if (state.status === 'error') {
|
|
494
645
|
amount = shown !== null ? fmt(shown, state.currency) : '--'
|
|
@@ -610,9 +761,15 @@ var soundOn = true
|
|
|
610
761
|
var soundVol = 0.9
|
|
611
762
|
var soundSet = 'duck'
|
|
612
763
|
var usageMode = 'ledger'
|
|
764
|
+
var peakMode = 'default'
|
|
765
|
+
var bubbleOn = true
|
|
766
|
+
var turnCostOn = true
|
|
767
|
+
var turnCostCloseMs = 5000
|
|
768
|
+
var lastCostSeq = 0
|
|
769
|
+
var costBubbleActive = false
|
|
613
770
|
function saveConfig() {
|
|
614
771
|
try {
|
|
615
|
-
fetch(SIZE_URL, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scale: state.scale, sound: soundOn, vol: soundVol, soundSet: soundSet, usageMode: usageMode }) })
|
|
772
|
+
fetch(SIZE_URL, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scale: state.scale, sound: soundOn, vol: soundVol, soundSet: soundSet, usageMode: usageMode, peakMode: peakMode, bubbleOn: bubbleOn, turnCostOn: turnCostOn, turnCostCloseMs: turnCostCloseMs }) })
|
|
616
773
|
} catch (err) {}
|
|
617
774
|
}
|
|
618
775
|
function setUsageMode(v) {
|
|
@@ -621,6 +778,29 @@ function setUsageMode(v) {
|
|
|
621
778
|
saveConfig()
|
|
622
779
|
refresh(false)
|
|
623
780
|
}
|
|
781
|
+
function setPeakMode(v) {
|
|
782
|
+
peakMode = v === 'liangwen' || v === 'qiangqiang' ? v : 'default'
|
|
783
|
+
peakSelect.value = peakMode
|
|
784
|
+
saveConfig()
|
|
785
|
+
}
|
|
786
|
+
function setBubbleOn(v) {
|
|
787
|
+
bubbleOn = !!v
|
|
788
|
+
bubbleToggle.checked = bubbleOn
|
|
789
|
+
saveConfig()
|
|
790
|
+
if (!bubbleOn) hideBubble()
|
|
791
|
+
}
|
|
792
|
+
function setTurnCostOn(v) {
|
|
793
|
+
turnCostOn = !!v
|
|
794
|
+
turnCostToggle.checked = turnCostOn
|
|
795
|
+
saveConfig()
|
|
796
|
+
if (!turnCostOn) hideCostBubble()
|
|
797
|
+
}
|
|
798
|
+
function setTurnCostClose(v) {
|
|
799
|
+
var n = Math.max(0, Math.round(Number(v) || 0))
|
|
800
|
+
turnCostCloseMs = n * 1000
|
|
801
|
+
turnCostCloseInput.value = String(n)
|
|
802
|
+
saveConfig()
|
|
803
|
+
}
|
|
624
804
|
function scaleToDisplay(s) {
|
|
625
805
|
return Math.round((s - MIN_SCALE) / ((MAX_SCALE - MIN_SCALE) / 19)) + 1
|
|
626
806
|
}
|
|
@@ -996,10 +1176,54 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
996
1176
|
usageMode = d.usageMode === 'token' ? 'token' : 'ledger'
|
|
997
1177
|
usageSelect.value = usageMode
|
|
998
1178
|
}
|
|
1179
|
+
if (d && typeof d.peakMode === 'string') {
|
|
1180
|
+
peakMode = d.peakMode === 'liangwen' || d.peakMode === 'qiangqiang' ? d.peakMode : 'default'
|
|
1181
|
+
peakSelect.value = peakMode
|
|
1182
|
+
}
|
|
1183
|
+
if (d && typeof d.bubbleOn === 'boolean') {
|
|
1184
|
+
bubbleOn = d.bubbleOn
|
|
1185
|
+
bubbleToggle.checked = bubbleOn
|
|
1186
|
+
}
|
|
1187
|
+
if (d && typeof d.turnCostOn === 'boolean') {
|
|
1188
|
+
turnCostOn = d.turnCostOn
|
|
1189
|
+
turnCostToggle.checked = turnCostOn
|
|
1190
|
+
}
|
|
1191
|
+
if (d && typeof d.turnCostCloseMs === 'number') {
|
|
1192
|
+
turnCostCloseMs = d.turnCostCloseMs > 0 ? d.turnCostCloseMs : 0
|
|
1193
|
+
turnCostCloseInput.value = String(Math.round(turnCostCloseMs / 1000))
|
|
1194
|
+
}
|
|
999
1195
|
refresh(false)
|
|
1000
1196
|
})
|
|
1001
1197
|
.catch(function () { refresh(false) })
|
|
1002
1198
|
setInterval(function () { refresh(false) }, REFRESH_MS)
|
|
1199
|
+
|
|
1200
|
+
// —— 每轮对话消耗检测:轮询 last-turn.json,出现新 seq 时弹消耗金额泡泡 ——
|
|
1201
|
+
var LAST_TURN_URL = '/dsh-whale/last-turn.json'
|
|
1202
|
+
var lastCostSeq = 0
|
|
1203
|
+
var lastCostAligned = false
|
|
1204
|
+
function pollLastTurn() {
|
|
1205
|
+
try {
|
|
1206
|
+
fetch(LAST_TURN_URL, { cache: 'no-store' })
|
|
1207
|
+
.then(function (r) { return r.json() })
|
|
1208
|
+
.then(function (d) {
|
|
1209
|
+
if (!d || !d.ok || typeof d.seq !== 'number') return
|
|
1210
|
+
if (!lastCostAligned) {
|
|
1211
|
+
// 首次拿到数据:只对齐 seq,不弹旧轮次
|
|
1212
|
+
lastCostSeq = d.seq
|
|
1213
|
+
lastCostAligned = true
|
|
1214
|
+
return
|
|
1215
|
+
}
|
|
1216
|
+
if (d.seq > lastCostSeq) {
|
|
1217
|
+
lastCostSeq = d.seq
|
|
1218
|
+
if (d.turn !== null && d.amount !== null) {
|
|
1219
|
+
showCostBubble(Number(d.amount))
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
})
|
|
1223
|
+
.catch(function () {})
|
|
1224
|
+
} catch (err) {}
|
|
1225
|
+
}
|
|
1226
|
+
setInterval(pollLastTurn, 1000)
|
|
1003
1227
|
})()`
|
|
1004
1228
|
|
|
1005
1229
|
|
|
@@ -1010,6 +1234,79 @@ function apply(ctx) {
|
|
|
1010
1234
|
let imageBytes = null
|
|
1011
1235
|
let balanceCache = null
|
|
1012
1236
|
let balanceInFlight = null
|
|
1237
|
+
let gifBytes = null
|
|
1238
|
+
// 每轮对话消耗统计:turn -> 聚合中的成本/分词;完成后写入 lastTurn
|
|
1239
|
+
let turnAgg = null // { turn, cost, tokens, lastTs }
|
|
1240
|
+
let lastTurn = null // { turn, amount, tokens, ts }
|
|
1241
|
+
let lastTurnSeq = 0
|
|
1242
|
+
const disposers = []
|
|
1243
|
+
|
|
1244
|
+
function resetTurnAgg() {
|
|
1245
|
+
turnAgg = null
|
|
1246
|
+
}
|
|
1247
|
+
function finalizeTurn() {
|
|
1248
|
+
if (turnAgg && turnAgg.cost > 0) {
|
|
1249
|
+
lastTurn = { turn: turnAgg.turn, amount: turnAgg.cost, tokens: turnAgg.tokens, ts: turnAgg.lastTs }
|
|
1250
|
+
lastTurnSeq++
|
|
1251
|
+
}
|
|
1252
|
+
turnAgg = null
|
|
1253
|
+
}
|
|
1254
|
+
// 监听会话事件流:assistant/message 携带每步真实 usage,按 turn 聚合;
|
|
1255
|
+
// turn/end 时结算本轮并写入 lastTurn
|
|
1256
|
+
function handleSessionEvent(event) {
|
|
1257
|
+
try {
|
|
1258
|
+
const type = event && event.type
|
|
1259
|
+
const d = event && event.data
|
|
1260
|
+
if (!d || typeof d !== 'object') return
|
|
1261
|
+
if (type === 'turn/end') {
|
|
1262
|
+
// 一轮对话结束:结算并清空聚合
|
|
1263
|
+
finalizeTurn()
|
|
1264
|
+
return
|
|
1265
|
+
}
|
|
1266
|
+
if (type !== 'assistant/message') return
|
|
1267
|
+
const turn = Number(d.turn)
|
|
1268
|
+
const usage = d.usage
|
|
1269
|
+
if (!usage || typeof usage !== 'object' || !isFinite(turn)) return
|
|
1270
|
+
if (!turnAgg || turnAgg.turn !== turn) {
|
|
1271
|
+
if (turnAgg && turnAgg.turn !== turn) finalizeTurn()
|
|
1272
|
+
if (!turnAgg || turnAgg.turn !== turn) turnAgg = { turn, cost: 0, tokens: 0, lastTs: Date.now() }
|
|
1273
|
+
}
|
|
1274
|
+
const input = Number(usage.inputTokens) || 0
|
|
1275
|
+
const cache = Number(usage.cacheReadTokens) || 0
|
|
1276
|
+
const output = Number(usage.outputTokens) || 0
|
|
1277
|
+
const reasoning = Number(usage.reasoningTokens) || 0
|
|
1278
|
+
turnAgg.tokens += input + cache + output + reasoning
|
|
1279
|
+
// 定价换算(CNY/百万 token;缓存命中=输入价,其余按各自档位)
|
|
1280
|
+
const model = d.message && d.message.source ? d.message.source.model : ''
|
|
1281
|
+
const p = priceFor(model)
|
|
1282
|
+
const off = isPeakTime(Math.floor(Date.now() / 1000)) ? 1 : 0
|
|
1283
|
+
turnAgg.cost += (cache / 1e6) * p.hit[off] + (input / 1e6) * p.miss[off] + ((output + reasoning) / 1e6) * p.out[off]
|
|
1284
|
+
turnAgg.lastTs = Date.now()
|
|
1285
|
+
} catch (err) {}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// 监听所有会话的追加事件;turn/end 时结算本轮
|
|
1289
|
+
disposers.push(ctx.on('session/event', (session, event) => {
|
|
1290
|
+
// [debug] 确认事件是否到达(下个版本移除)
|
|
1291
|
+
if (event && (event.type === 'turn/end' || event.type === 'assistant/message')) {
|
|
1292
|
+
try { console.error('[whale-balance:event]', event.type, 'turn=' + (event.data && event.data.turn), 'usage=' + (event.data && event.data.usage ? 'yes' : 'no')) } catch (err) {}
|
|
1293
|
+
}
|
|
1294
|
+
handleSessionEvent(event)
|
|
1295
|
+
}))
|
|
1296
|
+
|
|
1297
|
+
function loadGif() {
|
|
1298
|
+
if (gifBytes) return gifBytes
|
|
1299
|
+
for (const p of RUA_GIF_CANDIDATES) {
|
|
1300
|
+
try {
|
|
1301
|
+
const bytes = fs.readFileSync(p)
|
|
1302
|
+
if (bytes && bytes.length > 0) {
|
|
1303
|
+
gifBytes = bytes
|
|
1304
|
+
return bytes
|
|
1305
|
+
}
|
|
1306
|
+
} catch (err) {}
|
|
1307
|
+
}
|
|
1308
|
+
throw new Error('rua gif not found')
|
|
1309
|
+
}
|
|
1013
1310
|
|
|
1014
1311
|
function loadImage() {
|
|
1015
1312
|
if (imageBytes) return imageBytes
|
|
@@ -1267,6 +1564,10 @@ function apply(ctx) {
|
|
|
1267
1564
|
vol: typeof parsed.vol === 'number' ? parsed.vol : 0.9,
|
|
1268
1565
|
soundSet: parsed.soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1269
1566
|
usageMode: normalizeUsageMode(parsed.usageMode),
|
|
1567
|
+
peakMode: parsed.peakMode === 'liangwen' || parsed.peakMode === 'qiangqiang' ? parsed.peakMode : 'default',
|
|
1568
|
+
bubbleOn: parsed.bubbleOn !== false,
|
|
1569
|
+
turnCostOn: parsed.turnCostOn !== false,
|
|
1570
|
+
turnCostCloseMs: typeof parsed.turnCostCloseMs === 'number' ? parsed.turnCostCloseMs : 5000,
|
|
1270
1571
|
}
|
|
1271
1572
|
}
|
|
1272
1573
|
} catch (err) {}
|
|
@@ -1274,14 +1575,22 @@ function apply(ctx) {
|
|
|
1274
1575
|
return null
|
|
1275
1576
|
}
|
|
1276
1577
|
|
|
1277
|
-
function writeSizeConfig(scale, sound, vol, soundSet, usageMode) {
|
|
1578
|
+
function writeSizeConfig(scale, sound, vol, soundSet, usageMode, peakMode, bubbleOn, turnCostOn, turnCostCloseMs) {
|
|
1278
1579
|
const um = normalizeUsageMode(usageMode)
|
|
1580
|
+
const pm = peakMode === 'liangwen' || peakMode === 'qiangqiang' ? peakMode : 'default'
|
|
1581
|
+
const bo = bubbleOn !== false
|
|
1582
|
+
const tco = turnCostOn !== false
|
|
1583
|
+
const tcc = typeof turnCostCloseMs === 'number' && turnCostCloseMs > 0 ? turnCostCloseMs : 5000
|
|
1279
1584
|
const body = JSON.stringify({
|
|
1280
1585
|
scale: scale,
|
|
1281
1586
|
sound: sound !== false,
|
|
1282
1587
|
vol: typeof vol === 'number' ? vol : 0.9,
|
|
1283
1588
|
soundSet: soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1284
1589
|
usageMode: um,
|
|
1590
|
+
peakMode: pm,
|
|
1591
|
+
bubbleOn: bo,
|
|
1592
|
+
turnCostOn: tco,
|
|
1593
|
+
turnCostCloseMs: tcc,
|
|
1285
1594
|
updatedAt: new Date().toISOString(),
|
|
1286
1595
|
})
|
|
1287
1596
|
for (const p of SIZE_FILE_CANDIDATES) {
|
|
@@ -1294,6 +1603,10 @@ function apply(ctx) {
|
|
|
1294
1603
|
vol: typeof vol === 'number' ? vol : 0.9,
|
|
1295
1604
|
soundSet: soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1296
1605
|
usageMode: um,
|
|
1606
|
+
peakMode: pm,
|
|
1607
|
+
bubbleOn: bo,
|
|
1608
|
+
turnCostOn: tco,
|
|
1609
|
+
turnCostCloseMs: tcc,
|
|
1297
1610
|
}
|
|
1298
1611
|
} catch (err) {}
|
|
1299
1612
|
}
|
|
@@ -1318,8 +1631,6 @@ function apply(ctx) {
|
|
|
1318
1631
|
})
|
|
1319
1632
|
}
|
|
1320
1633
|
|
|
1321
|
-
const disposers = []
|
|
1322
|
-
|
|
1323
1634
|
disposers.push(ctx.webServer.register({
|
|
1324
1635
|
kind: 'exact',
|
|
1325
1636
|
path: '/dsh-whale/image.png',
|
|
@@ -1339,6 +1650,25 @@ function apply(ctx) {
|
|
|
1339
1650
|
},
|
|
1340
1651
|
}))
|
|
1341
1652
|
|
|
1653
|
+
disposers.push(ctx.webServer.register({
|
|
1654
|
+
kind: 'exact',
|
|
1655
|
+
path: '/dsh-whale/rua.gif',
|
|
1656
|
+
handler: (req, res) => {
|
|
1657
|
+
try {
|
|
1658
|
+
const bytes = loadGif()
|
|
1659
|
+
res.writeHead(200, {
|
|
1660
|
+
'Content-Type': 'image/gif',
|
|
1661
|
+
'Cache-Control': 'no-store',
|
|
1662
|
+
'Content-Length': String(bytes.length),
|
|
1663
|
+
})
|
|
1664
|
+
res.end(bytes)
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1667
|
+
res.end('rua gif unavailable: ' + String((err && err.message) || err))
|
|
1668
|
+
}
|
|
1669
|
+
},
|
|
1670
|
+
}))
|
|
1671
|
+
|
|
1342
1672
|
disposers.push(ctx.webServer.register({
|
|
1343
1673
|
kind: 'exact',
|
|
1344
1674
|
path: '/dsh-whale/balance.json',
|
|
@@ -1354,6 +1684,19 @@ function apply(ctx) {
|
|
|
1354
1684
|
},
|
|
1355
1685
|
}))
|
|
1356
1686
|
|
|
1687
|
+
disposers.push(ctx.webServer.register({
|
|
1688
|
+
kind: 'exact',
|
|
1689
|
+
path: '/dsh-whale/last-turn.json',
|
|
1690
|
+
handler: (req, res) => {
|
|
1691
|
+
// 返回最近一轮已完成的对话消耗;seq 递增供前端判断「新的一轮」
|
|
1692
|
+
const payload = lastTurn
|
|
1693
|
+
? { ok: true, seq: lastTurnSeq, turn: lastTurn.turn, amount: lastTurn.amount, tokens: lastTurn.tokens, ts: lastTurn.ts }
|
|
1694
|
+
: { ok: true, seq: 0, turn: null, amount: null, tokens: null, ts: null }
|
|
1695
|
+
res.writeHead(200, JSON_HEADERS)
|
|
1696
|
+
res.end(JSON.stringify(payload))
|
|
1697
|
+
},
|
|
1698
|
+
}))
|
|
1699
|
+
|
|
1357
1700
|
disposers.push(ctx.webServer.register({
|
|
1358
1701
|
kind: 'exact',
|
|
1359
1702
|
path: '/dsh-whale/size.json',
|
|
@@ -1375,7 +1718,7 @@ function apply(ctx) {
|
|
|
1375
1718
|
balanceCache = null
|
|
1376
1719
|
}
|
|
1377
1720
|
}
|
|
1378
|
-
const result = writeSizeConfig(scale, parsed.sound !== false, parsed.vol, parsed.soundSet, parsed.usageMode)
|
|
1721
|
+
const result = writeSizeConfig(scale, parsed.sound !== false, parsed.vol, parsed.soundSet, parsed.usageMode, parsed.peakMode, parsed.bubbleOn, parsed.turnCostOn, parsed.turnCostCloseMs)
|
|
1379
1722
|
res.writeHead(result.ok ? 200 : 500, JSON_HEADERS)
|
|
1380
1723
|
res.end(JSON.stringify(result))
|
|
1381
1724
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-whale-widget",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "DSH Web 界面右下角的 DeepSeek
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件(含今日已用、峰谷定价、随机台词、音效与每轮消耗统计)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|