dsh-whale-widget 0.2.2 → 0.2.6
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/LICENSE +21 -0
- package/README.md +66 -8
- package/assets/DSH2.png +0 -0
- package/assets/DSniang02.png +0 -0
- package/assets/rua.gif +0 -0
- package/lib/index.js +442 -26
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MeteorNOX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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` 就能用,**开箱即用**。
|
|
108
134
|
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
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
|
+
### 每轮对话消耗(无需任何凭据)
|
|
161
|
+
|
|
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` 调整几何参数。
|
|
@@ -138,3 +192,7 @@ curl http://127.0.0.1:3080/dsh-whale/size.json
|
|
|
138
192
|
## 开发与维护
|
|
139
193
|
|
|
140
194
|
完整规格、视觉参数、架构结论和生成提示词见 `whale-widget-prompt.md`。修改文字位置、颜色、动画、吸附逻辑、台词组或定价表时参考该文件。
|
|
195
|
+
|
|
196
|
+
## 许可证
|
|
197
|
+
|
|
198
|
+
本项目基于 **MIT License** 开源,详见 [LICENSE](LICENSE)。
|
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,23 @@ 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)
|
|
339
|
+
var gifFailed = false
|
|
340
|
+
gifEl.onerror = function () { gifFailed = true }
|
|
277
341
|
bubbleBox.appendChild(textBox)
|
|
278
342
|
bubbleBox.addEventListener('click', function (e) {
|
|
279
343
|
e.stopPropagation()
|
|
280
344
|
if (!bubbleShown) return
|
|
345
|
+
if (costBubbleActive) {
|
|
346
|
+
// 消耗金额泡泡:点击关闭(确认)
|
|
347
|
+
hideCostBubble()
|
|
348
|
+
return
|
|
349
|
+
}
|
|
281
350
|
if (bubbleRandomActive) {
|
|
282
351
|
// 再次点击:关闭
|
|
283
352
|
hideBubble()
|
|
@@ -333,18 +402,27 @@ function pickOne(arr) { return arr[Math.floor(Math.random() * arr.length)] }
|
|
|
333
402
|
function singleCenter(style, text, color, wrap) { return [null, { t: text, s: style, c: color || '', w: !!wrap }, null] }
|
|
334
403
|
function buildGroup1() {
|
|
335
404
|
var peak = !!state.isPeak
|
|
405
|
+
var offText = '空闲时段'
|
|
406
|
+
var peakText = '高峰时段'
|
|
407
|
+
if (peakMode === 'liangwen') {
|
|
408
|
+
offText = '梁文谷'
|
|
409
|
+
peakText = '梁文峰'
|
|
410
|
+
} else if (peakMode === 'qiangqiang') {
|
|
411
|
+
offText = '!?谷谷?!'
|
|
412
|
+
peakText = '!?峰峰?!'
|
|
413
|
+
}
|
|
336
414
|
return [
|
|
337
415
|
{ t: '当前时间段为:', s: 'A', c: '' },
|
|
338
|
-
{ t: peak ?
|
|
416
|
+
{ t: peak ? peakText : offText, s: 'P', c: peak ? '#e0433f' : '#2fa24c' },
|
|
339
417
|
{ t: '今日已用 ' + fmt(state.todayUsage, state.currency), s: 'C', c: '' },
|
|
340
418
|
]
|
|
341
419
|
}
|
|
342
420
|
var RANDOM_GROUPS = [
|
|
343
|
-
{ w:
|
|
421
|
+
{ w: 45, lines: buildGroup1 },
|
|
344
422
|
{ w: 7, lines: function () { return singleCenter('B', pickOne(['好模型... ↓', '好女孩...↓'])) } },
|
|
345
423
|
{ w: 7, lines: function () { return singleCenter('A', pickOne(['不知道用户有什么用,先赶走吧~', '我...我...我也要挣钱吗?', '我去吃饭啦,测完叫我', '压力一只蓝色大肥鱼?!', 'DeepSleep...', '坏了...用户彻底怒了!']), '', true) } },
|
|
424
|
+
{ w: 10, lines: function () { return { gif: true } } },
|
|
346
425
|
{ 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
426
|
{ w: 1, lines: function () { return singleCenter('B', '哦鲸鲸... ') } },
|
|
349
427
|
]
|
|
350
428
|
function pickRandomLines() {
|
|
@@ -358,6 +436,24 @@ function pickRandomLines() {
|
|
|
358
436
|
return RANDOM_GROUPS[RANDOM_GROUPS.length - 1].lines()
|
|
359
437
|
}
|
|
360
438
|
function applyBubbleLines(lines) {
|
|
439
|
+
if (lines && lines.gif) {
|
|
440
|
+
// gif 台词组:只显示 gif,隐藏三行文字(display 必须显式覆盖 CSS 的 none)
|
|
441
|
+
if (gifFailed) {
|
|
442
|
+
// gif 加载失败/路由缺失:降级为文字台词,避免空白白色气泡
|
|
443
|
+
lines = singleCenter('A', pickOne(['gif 加载失败了...', '今天没有动图给你看~', '呜呜 动图不见了...']), '', true)
|
|
444
|
+
} else {
|
|
445
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
446
|
+
gifEl.style.display = 'block'
|
|
447
|
+
gifEl.style.opacity = ''
|
|
448
|
+
labelEl.style.display = 'none'
|
|
449
|
+
amountEl.style.display = 'none'
|
|
450
|
+
hintEl.style.display = 'none'
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
455
|
+
gifEl.style.display = 'none'
|
|
456
|
+
gifEl.style.opacity = ''
|
|
361
457
|
var els = [labelEl, amountEl, hintEl]
|
|
362
458
|
for (var i = 0; i < 3; i++) {
|
|
363
459
|
var el = els[i]
|
|
@@ -376,13 +472,16 @@ function applyBubbleLines(lines) {
|
|
|
376
472
|
}
|
|
377
473
|
var bubbleSwapTimer = null
|
|
378
474
|
var hintFadeTimer = null
|
|
475
|
+
var gifFadeTimer = null
|
|
379
476
|
var lastHintText = null
|
|
380
477
|
function setHint(text) {
|
|
381
|
-
//
|
|
478
|
+
// 首次/恢复(lastHintText===null)时直接写文本,不做淡出淡入——否则
|
|
479
|
+
// 气泡打开或按压重开时会先淡出再淡入,造成「消失一下又出现」。
|
|
480
|
+
// 只有气泡打开期间的内容变化(加载中→今日已用)才走动画。
|
|
382
481
|
if (text === lastHintText) return
|
|
482
|
+
var first = lastHintText === null
|
|
383
483
|
lastHintText = text
|
|
384
|
-
if (
|
|
385
|
-
if (!bubbleShown) {
|
|
484
|
+
if (first || !bubbleShown) {
|
|
386
485
|
hintEl.textContent = text
|
|
387
486
|
return
|
|
388
487
|
}
|
|
@@ -415,21 +514,30 @@ function swapBubbleContent(applyFn) {
|
|
|
415
514
|
function restoreBubbleLines() {
|
|
416
515
|
if (bubbleSwapTimer) { clearTimeout(bubbleSwapTimer); bubbleSwapTimer = null }
|
|
417
516
|
if (hintFadeTimer) { clearTimeout(hintFadeTimer); hintFadeTimer = null }
|
|
517
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
418
518
|
lastHintText = null
|
|
419
519
|
textBox.style.transition = ''
|
|
420
520
|
textBox.style.opacity = ''
|
|
521
|
+
gifEl.style.display = 'none'
|
|
522
|
+
gifEl.style.opacity = ''
|
|
421
523
|
labelEl.style.display = ''
|
|
422
524
|
labelEl.className = 'dshwv-label'
|
|
423
525
|
labelEl.textContent = 'DeepSeek 余额'
|
|
424
526
|
labelEl.style.color = ''
|
|
527
|
+
amountEl.style.display = ''
|
|
425
528
|
amountEl.className = 'dshwv-amount'
|
|
426
529
|
amountEl.style.color = ''
|
|
530
|
+
hintEl.style.display = ''
|
|
427
531
|
hintEl.className = 'dshwv-hint'
|
|
428
532
|
hintEl.style.color = ''
|
|
429
533
|
render()
|
|
430
534
|
}
|
|
431
535
|
function showBubble() {
|
|
536
|
+
if (!bubbleOn) return
|
|
537
|
+
// 消耗金额泡泡显示期间,余额变动不再弹出普通泡泡
|
|
538
|
+
if (costBubbleActive) return
|
|
432
539
|
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null }
|
|
540
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
433
541
|
bubbleShown = true
|
|
434
542
|
bubbleRandomActive = false
|
|
435
543
|
restoreBubbleLines()
|
|
@@ -448,7 +556,53 @@ function hideBubble() {
|
|
|
448
556
|
bubbleRandomActive = false
|
|
449
557
|
bubbleRandomLines = null
|
|
450
558
|
bubbleShown = false
|
|
559
|
+
// 只销毁 gif 显示;三行文字保持现状让气泡自然淡出——不能在关闭瞬间
|
|
560
|
+
// 恢复成余额内容(否则随机台词界面会闪现余额)。文字恢复交给下次
|
|
561
|
+
// showBubble() 的 restoreBubbleLines()(那时气泡隐藏,恢复过程不可见)。
|
|
451
562
|
bubbleBox.classList.remove('dshwv-bubble-open')
|
|
563
|
+
// gif 靠 CSS opacity 过渡淡出;display:none 会跳过过渡,须等淡出完成再隐藏
|
|
564
|
+
gifFadeTimer = setTimeout(function () {
|
|
565
|
+
gifFadeTimer = null
|
|
566
|
+
gifEl.style.display = 'none'
|
|
567
|
+
}, 240)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// —— 每轮对话消耗金额泡泡 ——
|
|
571
|
+
var costBubbleTimer = null
|
|
572
|
+
function showCostBubble(amount) {
|
|
573
|
+
if (!bubbleOn || !turnCostOn) return
|
|
574
|
+
if (costBubbleTimer) { clearTimeout(costBubbleTimer); costBubbleTimer = null }
|
|
575
|
+
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null }
|
|
576
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
577
|
+
costBubbleActive = true
|
|
578
|
+
bubbleRandomActive = false
|
|
579
|
+
bubbleShown = true
|
|
580
|
+
lastHintText = null
|
|
581
|
+
// 样式:第一行 A(标签),第二行 B(红色金额),居中两行
|
|
582
|
+
gifEl.style.display = 'none'
|
|
583
|
+
gifEl.style.opacity = ''
|
|
584
|
+
labelEl.style.display = ''
|
|
585
|
+
labelEl.className = 'dshwv-label'
|
|
586
|
+
labelEl.textContent = '上一轮对话消耗:'
|
|
587
|
+
labelEl.style.color = ''
|
|
588
|
+
amountEl.style.display = ''
|
|
589
|
+
amountEl.className = 'dshwv-amount'
|
|
590
|
+
amountEl.textContent = '¥ ' + (isFinite(amount) ? Number(amount).toFixed(2) : '--')
|
|
591
|
+
amountEl.style.color = '#e0433f'
|
|
592
|
+
hintEl.style.display = 'none'
|
|
593
|
+
hintEl.textContent = ''
|
|
594
|
+
hintEl.style.color = ''
|
|
595
|
+
textBox.style.transition = ''
|
|
596
|
+
textBox.style.opacity = ''
|
|
597
|
+
bubbleBox.classList.add('dshwv-bubble-open')
|
|
598
|
+
if (turnCostCloseMs > 0) {
|
|
599
|
+
costBubbleTimer = setTimeout(hideCostBubble, turnCostCloseMs)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function hideCostBubble() {
|
|
603
|
+
if (costBubbleTimer) { clearTimeout(costBubbleTimer); costBubbleTimer = null }
|
|
604
|
+
costBubbleActive = false
|
|
605
|
+
hideBubble()
|
|
452
606
|
}
|
|
453
607
|
|
|
454
608
|
function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v) }
|
|
@@ -458,12 +612,19 @@ function viewport() {
|
|
|
458
612
|
h: window.innerHeight || document.documentElement.clientHeight || 800
|
|
459
613
|
}
|
|
460
614
|
}
|
|
615
|
+
function rightGap() {
|
|
616
|
+
var sw = window.innerWidth - document.documentElement.clientWidth
|
|
617
|
+
if (sw < 8) sw = 10 // 悬浮滚动条测量为0时兜底
|
|
618
|
+
return sw + 2 // 滚动条宽度 + 2px
|
|
619
|
+
}
|
|
461
620
|
function fmt(balance, currency) {
|
|
462
621
|
var num = Number(balance)
|
|
463
622
|
var fixed = isFinite(num) ? num.toFixed(2) : '--'
|
|
464
623
|
return currency === 'CNY' ? '¥ ' + fixed : fixed + ' ' + currency
|
|
465
624
|
}
|
|
466
625
|
function animateAmount(from, to, currency, duration) {
|
|
626
|
+
// 消耗金额泡泡显示期间,余额数字滚动不触碰金额行
|
|
627
|
+
if (costBubbleActive) return
|
|
467
628
|
if (animId) cancelAnimationFrame(animId)
|
|
468
629
|
if (from === null || !isFinite(from)) from = to
|
|
469
630
|
if (from === to) {
|
|
@@ -489,6 +650,8 @@ function animateAmount(from, to, currency, duration) {
|
|
|
489
650
|
animId = requestAnimationFrame(step)
|
|
490
651
|
}
|
|
491
652
|
function render() {
|
|
653
|
+
// 消耗金额泡泡显示期间,余额渲染不覆盖其内容(金额行/标题行/提示行)
|
|
654
|
+
if (costBubbleActive) return
|
|
492
655
|
var amount, hint
|
|
493
656
|
if (state.status === 'error') {
|
|
494
657
|
amount = shown !== null ? fmt(shown, state.currency) : '--'
|
|
@@ -520,17 +683,17 @@ function settle() {
|
|
|
520
683
|
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
521
684
|
if (drag && drag.active) {
|
|
522
685
|
// mid-drag resize: keep the pointer-follow position, just clamp into view
|
|
523
|
-
state.left = clamp(state.left, 0, Math.max(0, vp.w - w))
|
|
686
|
+
state.left = clamp(state.left, 0, Math.max(0, vp.w - w - rightGap()))
|
|
524
687
|
state.top = clamp(state.top, 0, Math.max(0, vp.h - h))
|
|
525
688
|
express()
|
|
526
689
|
return
|
|
527
690
|
}
|
|
528
691
|
if (state.h === 'right') {
|
|
529
|
-
state.left = Math.max(0, vp.w - w - state.hOff)
|
|
692
|
+
state.left = Math.max(0, vp.w - w - state.hOff - rightGap())
|
|
530
693
|
} else if (state.h === 'left') {
|
|
531
694
|
state.left = state.hOff
|
|
532
695
|
} else {
|
|
533
|
-
state.left = clamp(state.left, 0, Math.max(0, vp.w - w))
|
|
696
|
+
state.left = clamp(state.left, 0, Math.max(0, vp.w - w - rightGap()))
|
|
534
697
|
}
|
|
535
698
|
if (state.v === 'bottom') {
|
|
536
699
|
state.top = Math.max(0, vp.h - h - state.vOff)
|
|
@@ -610,9 +773,27 @@ var soundOn = true
|
|
|
610
773
|
var soundVol = 0.9
|
|
611
774
|
var soundSet = 'duck'
|
|
612
775
|
var usageMode = 'ledger'
|
|
776
|
+
var peakMode = 'default'
|
|
777
|
+
var bubbleOn = true
|
|
778
|
+
var turnCostOn = true
|
|
779
|
+
var turnCostCloseMs = 5000
|
|
780
|
+
var costBubbleActive = false
|
|
613
781
|
function saveConfig() {
|
|
614
782
|
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 }) })
|
|
783
|
+
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 }) })
|
|
784
|
+
var vp = viewport()
|
|
785
|
+
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
786
|
+
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
787
|
+
var leftDist = state.left
|
|
788
|
+
var rightDist = vp.w - state.left - w
|
|
789
|
+
var topDist = state.top
|
|
790
|
+
var bottomDist = vp.h - state.top - h
|
|
791
|
+
localStorage.setItem('dshw-pos', JSON.stringify({
|
|
792
|
+
hAnchor: leftDist <= rightDist ? 'left' : 'right',
|
|
793
|
+
hDist: Math.round(Math.min(leftDist, rightDist)),
|
|
794
|
+
vAnchor: topDist <= bottomDist ? 'top' : 'bottom',
|
|
795
|
+
vDist: Math.round(Math.min(topDist, bottomDist))
|
|
796
|
+
}))
|
|
616
797
|
} catch (err) {}
|
|
617
798
|
}
|
|
618
799
|
function setUsageMode(v) {
|
|
@@ -621,6 +802,30 @@ function setUsageMode(v) {
|
|
|
621
802
|
saveConfig()
|
|
622
803
|
refresh(false)
|
|
623
804
|
}
|
|
805
|
+
function setPeakMode(v) {
|
|
806
|
+
peakMode = v === 'liangwen' || v === 'qiangqiang' ? v : 'default'
|
|
807
|
+
peakSelect.value = peakMode
|
|
808
|
+
saveConfig()
|
|
809
|
+
}
|
|
810
|
+
function setBubbleOn(v) {
|
|
811
|
+
bubbleOn = !!v
|
|
812
|
+
bubbleToggle.checked = bubbleOn
|
|
813
|
+
saveConfig()
|
|
814
|
+
// 必须走 hideCostBubble:残留的 costBubbleActive 会让 render()/showBubble() 永久早退
|
|
815
|
+
if (!bubbleOn) hideCostBubble()
|
|
816
|
+
}
|
|
817
|
+
function setTurnCostOn(v) {
|
|
818
|
+
turnCostOn = !!v
|
|
819
|
+
turnCostToggle.checked = turnCostOn
|
|
820
|
+
saveConfig()
|
|
821
|
+
if (!turnCostOn) hideCostBubble()
|
|
822
|
+
}
|
|
823
|
+
function setTurnCostClose(v) {
|
|
824
|
+
var n = Math.max(0, Math.round(Number(v) || 0))
|
|
825
|
+
turnCostCloseMs = n * 1000
|
|
826
|
+
turnCostCloseInput.value = String(n)
|
|
827
|
+
saveConfig()
|
|
828
|
+
}
|
|
624
829
|
function scaleToDisplay(s) {
|
|
625
830
|
return Math.round((s - MIN_SCALE) / ((MAX_SCALE - MIN_SCALE) / 19)) + 1
|
|
626
831
|
}
|
|
@@ -775,7 +980,7 @@ function snapCheck() {
|
|
|
775
980
|
} else if (centerX > vp.w * 3 / 4) {
|
|
776
981
|
state.h = 'right'
|
|
777
982
|
state.hOff = 0
|
|
778
|
-
left = vp.w - w
|
|
983
|
+
left = vp.w - w - rightGap()
|
|
779
984
|
moved = true
|
|
780
985
|
} else {
|
|
781
986
|
state.h = null
|
|
@@ -829,7 +1034,9 @@ function setupHitTest() {
|
|
|
829
1034
|
var probe = new Image()
|
|
830
1035
|
probe.onload = function () {
|
|
831
1036
|
try {
|
|
832
|
-
|
|
1037
|
+
// 拉伸到 610×610 与 isWhaleHit 的坐标映射对齐;不指定尺寸会按原图大小绘制,
|
|
1038
|
+
// 回退到非 610×610 素材(如 DSniang02.png)时命中区域会错位
|
|
1039
|
+
hitCanvas.getContext('2d').drawImage(probe, 0, 0, 610, 610)
|
|
833
1040
|
hitReady = true
|
|
834
1041
|
} catch (err) {}
|
|
835
1042
|
}
|
|
@@ -956,7 +1163,28 @@ function endDrag(e, clickAllowed) {
|
|
|
956
1163
|
state.top = top
|
|
957
1164
|
settle()
|
|
958
1165
|
}
|
|
1166
|
+
// 窗口尺寸变化时:自由位置的鲸鱼按相对边框锚点重算(保持离边距离,窗口恢复原状即回原位);
|
|
1167
|
+
// 贴边吸附的鲸鱼走 settle()(保持贴边)
|
|
1168
|
+
function applyAnchorPos() {
|
|
1169
|
+
try {
|
|
1170
|
+
var a = JSON.parse(localStorage.getItem('dshw-pos') || 'null')
|
|
1171
|
+
if (!a || (a.hAnchor !== 'left' && a.hAnchor !== 'right') || typeof a.hDist !== 'number' ||
|
|
1172
|
+
(a.vAnchor !== 'top' && a.vAnchor !== 'bottom') || typeof a.vDist !== 'number') return false
|
|
1173
|
+
var vp = viewport()
|
|
1174
|
+
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
1175
|
+
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
1176
|
+
var l = a.hAnchor === 'left' ? a.hDist : vp.w - a.hDist - w
|
|
1177
|
+
var t = a.vAnchor === 'top' ? a.vDist : vp.h - a.vDist - h
|
|
1178
|
+
state.left = clamp(l, 0, Math.max(0, vp.w - w))
|
|
1179
|
+
state.top = clamp(t, 0, Math.max(0, vp.h - h))
|
|
1180
|
+
state.h = null
|
|
1181
|
+
state.v = null
|
|
1182
|
+
express()
|
|
1183
|
+
return true
|
|
1184
|
+
} catch (err) { return false }
|
|
1185
|
+
}
|
|
959
1186
|
window.addEventListener('resize', function () {
|
|
1187
|
+
if (state.h === null && state.v === null && applyAnchorPos()) return
|
|
960
1188
|
settle()
|
|
961
1189
|
})
|
|
962
1190
|
|
|
@@ -996,10 +1224,71 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
996
1224
|
usageMode = d.usageMode === 'token' ? 'token' : 'ledger'
|
|
997
1225
|
usageSelect.value = usageMode
|
|
998
1226
|
}
|
|
1227
|
+
if (d && typeof d.peakMode === 'string') {
|
|
1228
|
+
peakMode = d.peakMode === 'liangwen' || d.peakMode === 'qiangqiang' ? d.peakMode : 'default'
|
|
1229
|
+
peakSelect.value = peakMode
|
|
1230
|
+
}
|
|
1231
|
+
if (d && typeof d.bubbleOn === 'boolean') {
|
|
1232
|
+
bubbleOn = d.bubbleOn
|
|
1233
|
+
bubbleToggle.checked = bubbleOn
|
|
1234
|
+
}
|
|
1235
|
+
if (d && typeof d.turnCostOn === 'boolean') {
|
|
1236
|
+
turnCostOn = d.turnCostOn
|
|
1237
|
+
turnCostToggle.checked = turnCostOn
|
|
1238
|
+
}
|
|
1239
|
+
if (d && typeof d.turnCostCloseMs === 'number') {
|
|
1240
|
+
turnCostCloseMs = d.turnCostCloseMs > 0 ? d.turnCostCloseMs : 0
|
|
1241
|
+
turnCostCloseInput.value = String(Math.round(turnCostCloseMs / 1000))
|
|
1242
|
+
}
|
|
1243
|
+
// 相对边框恢复(localStorage 锚点):窗口变化后保持离边距离
|
|
1244
|
+
try {
|
|
1245
|
+
var a = JSON.parse(localStorage.getItem('dshw-pos') || 'null')
|
|
1246
|
+
if (a && (a.hAnchor === 'left' || a.hAnchor === 'right') && typeof a.hDist === 'number' &&
|
|
1247
|
+
(a.vAnchor === 'top' || a.vAnchor === 'bottom') && typeof a.vDist === 'number') {
|
|
1248
|
+
var vpA = viewport()
|
|
1249
|
+
var wA = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
1250
|
+
var hA = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
1251
|
+
var lA = a.hAnchor === 'left' ? a.hDist : vpA.w - a.hDist - wA
|
|
1252
|
+
var tA = a.vAnchor === 'top' ? a.vDist : vpA.h - a.vDist - hA
|
|
1253
|
+
state.left = clamp(lA, 0, Math.max(0, vpA.w - wA))
|
|
1254
|
+
state.top = clamp(tA, 0, Math.max(0, vpA.h - hA))
|
|
1255
|
+
state.h = null // 恢复为自由位置,避免 settle() 重新吸附覆盖
|
|
1256
|
+
state.v = null
|
|
1257
|
+
settle()
|
|
1258
|
+
}
|
|
1259
|
+
} catch (err) {}
|
|
999
1260
|
refresh(false)
|
|
1000
1261
|
})
|
|
1001
1262
|
.catch(function () { refresh(false) })
|
|
1002
1263
|
setInterval(function () { refresh(false) }, REFRESH_MS)
|
|
1264
|
+
|
|
1265
|
+
// —— 每轮对话消耗检测:轮询 last-turn.json,出现新 seq 时弹消耗金额泡泡 ——
|
|
1266
|
+
var LAST_TURN_URL = '/dsh-whale/last-turn.json'
|
|
1267
|
+
var lastCostSeq = 0
|
|
1268
|
+
var lastCostAligned = false
|
|
1269
|
+
function pollLastTurn() {
|
|
1270
|
+
try {
|
|
1271
|
+
fetch(LAST_TURN_URL, { cache: 'no-store' })
|
|
1272
|
+
.then(function (r) { return r.json() })
|
|
1273
|
+
.then(function (d) {
|
|
1274
|
+
if (!d || !d.ok || typeof d.seq !== 'number') return
|
|
1275
|
+
if (!lastCostAligned) {
|
|
1276
|
+
// 首次拿到数据:只对齐 seq,不弹旧轮次
|
|
1277
|
+
lastCostSeq = d.seq
|
|
1278
|
+
lastCostAligned = true
|
|
1279
|
+
return
|
|
1280
|
+
}
|
|
1281
|
+
if (d.seq > lastCostSeq) {
|
|
1282
|
+
lastCostSeq = d.seq
|
|
1283
|
+
if (d.turn !== null && d.amount !== null) {
|
|
1284
|
+
showCostBubble(Number(d.amount))
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
})
|
|
1288
|
+
.catch(function () {})
|
|
1289
|
+
} catch (err) {}
|
|
1290
|
+
}
|
|
1291
|
+
setInterval(pollLastTurn, 1000)
|
|
1003
1292
|
})()`
|
|
1004
1293
|
|
|
1005
1294
|
|
|
@@ -1010,6 +1299,75 @@ function apply(ctx) {
|
|
|
1010
1299
|
let imageBytes = null
|
|
1011
1300
|
let balanceCache = null
|
|
1012
1301
|
let balanceInFlight = null
|
|
1302
|
+
let gifBytes = null
|
|
1303
|
+
// 每轮对话消耗统计:turn -> 聚合中的成本/分词;完成后写入 lastTurn
|
|
1304
|
+
let turnAgg = null // { turn, cost, tokens, lastTs }
|
|
1305
|
+
let lastTurn = null // { turn, amount, tokens, ts }
|
|
1306
|
+
let lastTurnSeq = 0
|
|
1307
|
+
const disposers = []
|
|
1308
|
+
|
|
1309
|
+
function resetTurnAgg() {
|
|
1310
|
+
turnAgg = null
|
|
1311
|
+
}
|
|
1312
|
+
function finalizeTurn() {
|
|
1313
|
+
if (turnAgg && turnAgg.cost > 0) {
|
|
1314
|
+
lastTurn = { turn: turnAgg.turn, amount: turnAgg.cost, tokens: turnAgg.tokens, ts: turnAgg.lastTs }
|
|
1315
|
+
lastTurnSeq++
|
|
1316
|
+
}
|
|
1317
|
+
turnAgg = null
|
|
1318
|
+
}
|
|
1319
|
+
// 监听会话事件流:assistant/message 携带每步真实 usage,按 turn 聚合;
|
|
1320
|
+
// turn/end 时结算本轮并写入 lastTurn
|
|
1321
|
+
function handleSessionEvent(event) {
|
|
1322
|
+
try {
|
|
1323
|
+
const type = event && event.type
|
|
1324
|
+
const d = event && event.data
|
|
1325
|
+
if (!d || typeof d !== 'object') return
|
|
1326
|
+
if (type === 'turn/end') {
|
|
1327
|
+
// 一轮对话结束:结算并清空聚合
|
|
1328
|
+
finalizeTurn()
|
|
1329
|
+
return
|
|
1330
|
+
}
|
|
1331
|
+
if (type !== 'assistant/message') return
|
|
1332
|
+
const turn = Number(d.turn)
|
|
1333
|
+
const usage = d.usage
|
|
1334
|
+
if (!usage || typeof usage !== 'object' || !isFinite(turn)) return
|
|
1335
|
+
if (!turnAgg || turnAgg.turn !== turn) {
|
|
1336
|
+
if (turnAgg && turnAgg.turn !== turn) finalizeTurn()
|
|
1337
|
+
if (!turnAgg || turnAgg.turn !== turn) turnAgg = { turn, cost: 0, tokens: 0, lastTs: Date.now() }
|
|
1338
|
+
}
|
|
1339
|
+
const input = Number(usage.inputTokens) || 0
|
|
1340
|
+
const cache = Number(usage.cacheReadTokens) || 0
|
|
1341
|
+
const output = Number(usage.outputTokens) || 0
|
|
1342
|
+
const reasoning = Number(usage.reasoningTokens) || 0
|
|
1343
|
+
turnAgg.tokens += input + cache + output + reasoning
|
|
1344
|
+
// 定价换算(CNY/百万 token;缓存命中=输入价,其余按各自档位)
|
|
1345
|
+
const model = d.message && d.message.source ? d.message.source.model : ''
|
|
1346
|
+
const p = priceFor(model)
|
|
1347
|
+
const off = isPeakTime(Math.floor(Date.now() / 1000)) ? 1 : 0
|
|
1348
|
+
turnAgg.cost += (cache / 1e6) * p.hit[off] + (input / 1e6) * p.miss[off] + ((output + reasoning) / 1e6) * p.out[off]
|
|
1349
|
+
turnAgg.lastTs = Date.now()
|
|
1350
|
+
} catch (err) {}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// 监听所有会话的追加事件;turn/end 时结算本轮
|
|
1354
|
+
disposers.push(ctx.on('session/event', (session, event) => {
|
|
1355
|
+
handleSessionEvent(event)
|
|
1356
|
+
}))
|
|
1357
|
+
|
|
1358
|
+
function loadGif() {
|
|
1359
|
+
if (gifBytes) return gifBytes
|
|
1360
|
+
for (const p of RUA_GIF_CANDIDATES) {
|
|
1361
|
+
try {
|
|
1362
|
+
const bytes = fs.readFileSync(p)
|
|
1363
|
+
if (bytes && bytes.length > 0) {
|
|
1364
|
+
gifBytes = bytes
|
|
1365
|
+
return bytes
|
|
1366
|
+
}
|
|
1367
|
+
} catch (err) {}
|
|
1368
|
+
}
|
|
1369
|
+
throw new Error('rua gif not found')
|
|
1370
|
+
}
|
|
1013
1371
|
|
|
1014
1372
|
function loadImage() {
|
|
1015
1373
|
if (imageBytes) return imageBytes
|
|
@@ -1025,6 +1383,17 @@ function apply(ctx) {
|
|
|
1025
1383
|
throw new Error('whale image not found')
|
|
1026
1384
|
}
|
|
1027
1385
|
|
|
1386
|
+
function pickBalanceInfo(infos) {
|
|
1387
|
+
if (!Array.isArray(infos) || infos.length === 0) return null
|
|
1388
|
+
const num = (x) => (x && x.total_balance !== undefined ? Number(x.total_balance) : NaN)
|
|
1389
|
+
return (
|
|
1390
|
+
infos.find((x) => x && x.currency === 'CNY' && num(x) > 0) ||
|
|
1391
|
+
infos.find((x) => num(x) > 0) ||
|
|
1392
|
+
infos.find((x) => x && x.currency === 'CNY') ||
|
|
1393
|
+
infos[0]
|
|
1394
|
+
)
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1028
1397
|
async function fetchBalance() {
|
|
1029
1398
|
let cred
|
|
1030
1399
|
try {
|
|
@@ -1060,7 +1429,7 @@ function apply(ctx) {
|
|
|
1060
1429
|
} catch (err) {
|
|
1061
1430
|
return { ok: false, code: 'PARSE', error: '余额接口返回不是合法 JSON' }
|
|
1062
1431
|
}
|
|
1063
|
-
const info = data &&
|
|
1432
|
+
const info = pickBalanceInfo(data && data.balance_infos)
|
|
1064
1433
|
if (!info || info.total_balance === undefined) {
|
|
1065
1434
|
return { ok: false, code: 'SHAPE', error: '余额接口返回结构异常' }
|
|
1066
1435
|
}
|
|
@@ -1267,6 +1636,10 @@ function apply(ctx) {
|
|
|
1267
1636
|
vol: typeof parsed.vol === 'number' ? parsed.vol : 0.9,
|
|
1268
1637
|
soundSet: parsed.soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1269
1638
|
usageMode: normalizeUsageMode(parsed.usageMode),
|
|
1639
|
+
peakMode: parsed.peakMode === 'liangwen' || parsed.peakMode === 'qiangqiang' ? parsed.peakMode : 'default',
|
|
1640
|
+
bubbleOn: parsed.bubbleOn !== false,
|
|
1641
|
+
turnCostOn: parsed.turnCostOn !== false,
|
|
1642
|
+
turnCostCloseMs: typeof parsed.turnCostCloseMs === 'number' ? parsed.turnCostCloseMs : 5000,
|
|
1270
1643
|
}
|
|
1271
1644
|
}
|
|
1272
1645
|
} catch (err) {}
|
|
@@ -1274,14 +1647,23 @@ function apply(ctx) {
|
|
|
1274
1647
|
return null
|
|
1275
1648
|
}
|
|
1276
1649
|
|
|
1277
|
-
function writeSizeConfig(scale, sound, vol, soundSet, usageMode) {
|
|
1650
|
+
function writeSizeConfig(scale, sound, vol, soundSet, usageMode, peakMode, bubbleOn, turnCostOn, turnCostCloseMs) {
|
|
1278
1651
|
const um = normalizeUsageMode(usageMode)
|
|
1652
|
+
const pm = peakMode === 'liangwen' || peakMode === 'qiangqiang' ? peakMode : 'default'
|
|
1653
|
+
const bo = bubbleOn !== false
|
|
1654
|
+
const tco = turnCostOn !== false
|
|
1655
|
+
// 0 是合法值(不自动关闭),只有缺失/非法时才回落默认 5 秒
|
|
1656
|
+
const tcc = typeof turnCostCloseMs === 'number' && turnCostCloseMs >= 0 ? turnCostCloseMs : 5000
|
|
1279
1657
|
const body = JSON.stringify({
|
|
1280
1658
|
scale: scale,
|
|
1281
1659
|
sound: sound !== false,
|
|
1282
1660
|
vol: typeof vol === 'number' ? vol : 0.9,
|
|
1283
1661
|
soundSet: soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1284
1662
|
usageMode: um,
|
|
1663
|
+
peakMode: pm,
|
|
1664
|
+
bubbleOn: bo,
|
|
1665
|
+
turnCostOn: tco,
|
|
1666
|
+
turnCostCloseMs: tcc,
|
|
1285
1667
|
updatedAt: new Date().toISOString(),
|
|
1286
1668
|
})
|
|
1287
1669
|
for (const p of SIZE_FILE_CANDIDATES) {
|
|
@@ -1294,6 +1676,10 @@ function apply(ctx) {
|
|
|
1294
1676
|
vol: typeof vol === 'number' ? vol : 0.9,
|
|
1295
1677
|
soundSet: soundSet === 'fx1' ? 'fx1' : 'duck',
|
|
1296
1678
|
usageMode: um,
|
|
1679
|
+
peakMode: pm,
|
|
1680
|
+
bubbleOn: bo,
|
|
1681
|
+
turnCostOn: tco,
|
|
1682
|
+
turnCostCloseMs: tcc,
|
|
1297
1683
|
}
|
|
1298
1684
|
} catch (err) {}
|
|
1299
1685
|
}
|
|
@@ -1318,8 +1704,6 @@ function apply(ctx) {
|
|
|
1318
1704
|
})
|
|
1319
1705
|
}
|
|
1320
1706
|
|
|
1321
|
-
const disposers = []
|
|
1322
|
-
|
|
1323
1707
|
disposers.push(ctx.webServer.register({
|
|
1324
1708
|
kind: 'exact',
|
|
1325
1709
|
path: '/dsh-whale/image.png',
|
|
@@ -1339,6 +1723,25 @@ function apply(ctx) {
|
|
|
1339
1723
|
},
|
|
1340
1724
|
}))
|
|
1341
1725
|
|
|
1726
|
+
disposers.push(ctx.webServer.register({
|
|
1727
|
+
kind: 'exact',
|
|
1728
|
+
path: '/dsh-whale/rua.gif',
|
|
1729
|
+
handler: (req, res) => {
|
|
1730
|
+
try {
|
|
1731
|
+
const bytes = loadGif()
|
|
1732
|
+
res.writeHead(200, {
|
|
1733
|
+
'Content-Type': 'image/gif',
|
|
1734
|
+
'Cache-Control': 'no-store',
|
|
1735
|
+
'Content-Length': String(bytes.length),
|
|
1736
|
+
})
|
|
1737
|
+
res.end(bytes)
|
|
1738
|
+
} catch (err) {
|
|
1739
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1740
|
+
res.end('rua gif unavailable: ' + String((err && err.message) || err))
|
|
1741
|
+
}
|
|
1742
|
+
},
|
|
1743
|
+
}))
|
|
1744
|
+
|
|
1342
1745
|
disposers.push(ctx.webServer.register({
|
|
1343
1746
|
kind: 'exact',
|
|
1344
1747
|
path: '/dsh-whale/balance.json',
|
|
@@ -1354,6 +1757,19 @@ function apply(ctx) {
|
|
|
1354
1757
|
},
|
|
1355
1758
|
}))
|
|
1356
1759
|
|
|
1760
|
+
disposers.push(ctx.webServer.register({
|
|
1761
|
+
kind: 'exact',
|
|
1762
|
+
path: '/dsh-whale/last-turn.json',
|
|
1763
|
+
handler: (req, res) => {
|
|
1764
|
+
// 返回最近一轮已完成的对话消耗;seq 递增供前端判断「新的一轮」
|
|
1765
|
+
const payload = lastTurn
|
|
1766
|
+
? { ok: true, seq: lastTurnSeq, turn: lastTurn.turn, amount: lastTurn.amount, tokens: lastTurn.tokens, ts: lastTurn.ts }
|
|
1767
|
+
: { ok: true, seq: 0, turn: null, amount: null, tokens: null, ts: null }
|
|
1768
|
+
res.writeHead(200, JSON_HEADERS)
|
|
1769
|
+
res.end(JSON.stringify(payload))
|
|
1770
|
+
},
|
|
1771
|
+
}))
|
|
1772
|
+
|
|
1357
1773
|
disposers.push(ctx.webServer.register({
|
|
1358
1774
|
kind: 'exact',
|
|
1359
1775
|
path: '/dsh-whale/size.json',
|
|
@@ -1375,7 +1791,7 @@ function apply(ctx) {
|
|
|
1375
1791
|
balanceCache = null
|
|
1376
1792
|
}
|
|
1377
1793
|
}
|
|
1378
|
-
const result = writeSizeConfig(scale, parsed.sound !== false, parsed.vol, parsed.soundSet, parsed.usageMode)
|
|
1794
|
+
const result = writeSizeConfig(scale, parsed.sound !== false, parsed.vol, parsed.soundSet, parsed.usageMode, parsed.peakMode, parsed.bubbleOn, parsed.turnCostOn, parsed.turnCostCloseMs)
|
|
1379
1795
|
res.writeHead(result.ok ? 200 : 500, JSON_HEADERS)
|
|
1380
1796
|
res.end(JSON.stringify(result))
|
|
1381
1797
|
} 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.6",
|
|
4
|
+
"description": "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件(含今日已用、峰谷定价、随机台词、音效与每轮消耗统计)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|