dsh-whale-widget 0.3.1 → 0.3.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 +17 -2
- package/assets/whale-widget.js +288 -142
- package/lib/index.js +36 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,13 +4,28 @@
|
|
|
4
4
|
|
|
5
5
|
DeepSeek Harness(DSH)Web 界面右下角的常驻挂件:小鲸鱼气泡图 + DeepSeek API 余额 + 今日已用 + 每轮对话消耗,并且**泡泡内容可以完全自定义**(点击序列、模块化排版、并列加权出泡、随机语句/随机图片)。标准 DSH bundle 插件,`dsh plugin` 一键安装,无需任何会话令牌。
|
|
6
6
|
|
|
7
|
+
## 两条分支怎么选
|
|
8
|
+
|
|
9
|
+
本仓库有两条**互不兼容**的产品线,按你要挂在哪里选一条:
|
|
10
|
+
|
|
11
|
+
| 你要挂在哪 | 用哪条分支 | 安装方式 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **DSH Web 界面右下角**(就是这个 README 描述的插件) | `main`(默认分支) | `dsh plugin --profile web add dsh-whale-widget`(推荐,装 npm 已发布版);也可以从本仓库装 `dsh plugin --profile web add github:MeteorNOX/DeepSeek-Balance-Whale-Widget`,但那样装的是 **main 当前状态、不跟随已发布版本** |
|
|
14
|
+
| **Codex 桌面应用**(跟随 Codex 窗口、无独立网页) | [`For-Codex`](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/tree/For-Codex) | 该分支的 `api-balance-whale`:解压到 `%USERPROFILE%\plugins\api-balance-whale`,再按分支内的 [安装说明](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/blob/For-Codex/docs/INSTALL-AND-ROLLBACK-0.2.0.md) 注册计划任务(也可以直接交给 Codex 自己装喵~) |
|
|
15
|
+
|
|
16
|
+
⚠️ **两条分支互不兼容**:`For-Codex` 的插件不能用 `dsh plugin … add` 装进 DSH 网页;本主分支的插件也不能在 Codex 桌面里运行。上表第一行是本仓库默认分支(`main`)的能力,第二行是另一个分支的能力。
|
|
17
|
+
|
|
18
|
+
⚠️ 同一分支的两种安装方式(npm 名 / `github:`)**二选一、别混用** —— 它们是同一个包名,先后安装会并存冲突;已经是 `github:` 装的,要换 npm 名请先 `dsh plugin --profile web remove dsh-whale-widget`。
|
|
19
|
+
|
|
20
|
+
> 两条产品线**各自独立维护**(无共享代码/无跨分支依赖):`main` = DSH Web 插件,`For-Codex` = Codex 桌面端口,各自的版本线、发布方式与测试互不影响。
|
|
21
|
+
|
|
7
22
|
## 特性
|
|
8
23
|
|
|
9
24
|
### 记账与显示
|
|
10
25
|
|
|
11
26
|
- 🐋 **常驻自启**:随 DSH Web 界面每次打开自动出现(标准 DSH bundle 插件)
|
|
12
27
|
- 💰 **余额**:60 秒自动刷新 + 点击鲸鱼手动刷新;余额变化时数字**滚动动画**;瞬时网络抖动自动沿用最近余额不报错
|
|
13
|
-
- 📊 **今日已用(小鲸鱼记账)**:不需要任何令牌。余额**下降**按观测累计为消费,余额**上升**(充值 / 赠金)单独记录、不会冲掉已有消费;检测到余额增加会提示「待核对余额调整」,可在「小鲸鱼记账 →
|
|
28
|
+
- 📊 **今日已用(小鲸鱼记账)**:不需要任何令牌。余额**下降**按观测累计为消费,余额**上升**(充值 / 赠金)单独记录、不会冲掉已有消费;检测到余额增加会提示「待核对余额调整」,可在「小鲸鱼记账 → **DeepSeek(内置)→ 设置 → 余额校正**」按实际累计到账金额与非调用扣减校正 —— **「已观测消费」与「余额校正」都是 DeepSeek 账户口径**(由该 API key 的余额观测得出,不含其它厂商;「本机模型费用」才是所有模型的本地估算)(逐轮明细保留 90 天或最多 2 万条、逐日归档保留 365 天,超期数据归档到 `.dshw-usage-archive.json`);金额按 8 位小数记账、显示保留两位
|
|
14
29
|
- 💬 **每轮对话消耗**:监听 DSH 本机会话事件,按模型**真实 usage**(input / cache / output / reasoning tokens)换算金额,每轮结束弹出消耗泡泡;可开关、自动关闭秒数可设(0 = 不自动关闭),**泡泡内容可自定义**(模块化,金额用 `{cost}` 引用),入口:菜单 → 每轮消耗提示 → 「自定义提示」
|
|
15
30
|
- ⛰️ **峰谷定价**:工作日高峰 9:00–12:00、14:00–18:00(北京时间),其余空闲;2026-08-23 起**周末全天谷价**。峰谷模块支持状态文字、倒计时、"梁文峰谷 / !?强强?! / 简洁"等多种样式
|
|
16
31
|
- 📒 **用量记录窗口**:今日模型消费、近 7 天、全部记录;模型占比条;按日期展开逐条明细(含时间与金额);支持按日期或**模型名搜索**;模型名带友好标注(`deepseek-flash` → `DeepSeek-V4.1-Flash`,旧名标注"同 V4.1 Flash")
|
|
@@ -341,7 +356,7 @@ curl http://127.0.0.1:3080/dsh-whale/audio.json
|
|
|
341
356
|
- **图片/音效不显示、没声音**:确认插件包内 `assets/` 完整(`DSniang1.png`、`*.mp3`、`minecraft-exp-orb.wav` 等);缺失时相关功能静默降级。
|
|
342
357
|
- **余额报「未配置 DEEPSEEK_API_KEY」**:去 DSH 凭据里配置。
|
|
343
358
|
- **今日已用显示 `--`**:先等一次成功的余额观测;统计从该观测时刻开始,起点之前的消费不在此区间内。
|
|
344
|
-
- **今日已用与官网有差异**:先核对同一账户、同一币种与同一统计区间;若有充值或其它余额调整,用「小鲸鱼记账 →
|
|
359
|
+
- **今日已用与官网有差异**:先核对同一账户、同一币种与同一统计区间;若有充值或其它余额调整,用「小鲸鱼记账 → DeepSeek(内置)→ 设置 → 余额校正」填写实际到账金额。余额接口只返回余额快照、不提供充值流水,充值与消费发生在同一次刷新间隔时需要实际到账金额才能校正。
|
|
345
360
|
- **充值后消费数字没变**:这是预期行为——充值不会增加消费;数字上方会出现「待核对余额调整」,提示你补充本统计区间的累计到账金额。
|
|
346
361
|
- **每轮消耗不显示**:确认菜单里「每轮对话后自动显示消耗金额」已勾选;一轮对话要完整结束(`turn/end`)才结算。余额变化泡泡与消耗泡泡抢层时,提醒会退化为居中卡片。
|
|
347
362
|
- **每轮消耗泡泡的内容**:菜单 → 每轮消耗提示 → 「自定义提示」里编辑(模块化,金额用 `{cost}`);这里同时能设自动关闭秒数与任务结束音效。
|
package/assets/whale-widget.js
CHANGED
|
@@ -11,38 +11,145 @@ window.__dshWhaleWidget = true
|
|
|
11
11
|
function dshwIsChatRoot(r) {
|
|
12
12
|
return !!(r && (r.querySelector('textarea') || r.querySelector('[contenteditable="true"]')))
|
|
13
13
|
}
|
|
14
|
-
var
|
|
14
|
+
var dshwStarted = false
|
|
15
|
+
function dshwStartOnce() {
|
|
16
|
+
if (dshwStarted) return
|
|
17
|
+
dshwStarted = true
|
|
18
|
+
try { dshwInit() } catch (err) {}
|
|
19
|
+
}
|
|
20
|
+
// 是否已到主聊天界面;是则启动(只启动一次,之后由 dshwInit 内部标记去重)
|
|
21
|
+
var dshwLastCheck = 0
|
|
22
|
+
function dshwTryStart(force) {
|
|
23
|
+
if (dshwStarted) return true
|
|
24
|
+
var now = Date.now()
|
|
25
|
+
// 连续 DOM 变化时合并检查,避免每次 mutation 都 querySelector
|
|
26
|
+
if (!force && now - dshwLastCheck < 200) return false
|
|
27
|
+
dshwLastCheck = now
|
|
28
|
+
try {
|
|
29
|
+
if (dshwIsChatRoot(document.getElementById('root'))) { dshwStartOnce(); return true }
|
|
30
|
+
} catch (err) {}
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
15
33
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return
|
|
34
|
+
if (!dshwIsChatRoot(document.getElementById('root'))) {
|
|
35
|
+
// 尚未渲染:MutationObserver 无限等待(v739 去掉原来的「5 秒死线」)。
|
|
36
|
+
// 原来 500ms × 10 次就永久放弃,而 window.__dshWhaleWidget 是一次性闸门 ——
|
|
37
|
+
// 慢启动机器、或页面最小化时定时器被浏览器节流,都会让"第一次没赶上"变成
|
|
38
|
+
// "这次会话永远不出现"(用户反馈 / issue #102 第 2 条)。
|
|
39
|
+
// 约束不变:检测到 composer 之前一行 DOM 都不碰、不注册任何全局监听。
|
|
40
|
+
var dshwObserver = null
|
|
41
|
+
try {
|
|
42
|
+
if (typeof MutationObserver === 'function') {
|
|
43
|
+
dshwObserver = new MutationObserver(function () {
|
|
44
|
+
if (dshwTryStart()) { try { dshwObserver.disconnect() } catch (err) {} }
|
|
45
|
+
})
|
|
46
|
+
dshwObserver.observe(document.documentElement || document.body, { childList: true, subtree: true })
|
|
30
47
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
} catch (err) {}
|
|
49
|
+
// 兜底:observer 不可用时低频轮询继续等(不设上限,找到即停)
|
|
50
|
+
var dshwFallbackPoll = setInterval(function () {
|
|
51
|
+
if (dshwTryStart(true)) {
|
|
52
|
+
clearInterval(dshwFallbackPoll)
|
|
53
|
+
try { if (dshwObserver) dshwObserver.disconnect() } catch (err) {}
|
|
34
54
|
}
|
|
35
|
-
},
|
|
55
|
+
}, 2000)
|
|
36
56
|
}
|
|
37
57
|
} catch (err) {}
|
|
38
|
-
if (!dshwEnabled) {
|
|
39
|
-
// 非主界面(或等待超时):不初始化挂件
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
58
|
function dshwInit() {
|
|
43
59
|
if (window.__dshWhaleInit) return
|
|
44
60
|
window.__dshWhaleInit = true
|
|
45
61
|
|
|
62
|
+
// ===== v739:音效改用 Web Audio 播放(不再用 <audio> / HTMLAudioElement)=====
|
|
63
|
+
// 为什么改:HTMLAudioElement 会被 macOS 注册进系统「正在播放」,带 Touch Bar 的机器上
|
|
64
|
+
// 每次按小鲸鱼都会弹出音频播放条并持续动画 → 明显卡顿(用户反馈)。AudioBufferSourceNode
|
|
65
|
+
// 不经过媒体元素,系统媒体控件不会出现,也没有那层额外的解码/合成开销。
|
|
66
|
+
// 兼容做法:给一个与 HTMLAudioElement 常用表面一致的 shim(play/pause/currentTime/volume/
|
|
67
|
+
// onended/preload/src),这样既有的播放、音量、重播、结束回调逻辑一行都不用改。
|
|
68
|
+
var dshwvAudioCtx = null
|
|
69
|
+
function dshwvAudio() {
|
|
70
|
+
try {
|
|
71
|
+
if (!dshwvAudioCtx) dshwvAudioCtx = new (window.AudioContext || window.webkitAudioContext)()
|
|
72
|
+
if (dshwvAudioCtx.state === 'suspended') { try { dshwvAudioCtx.resume() } catch (err) {} }
|
|
73
|
+
return dshwvAudioCtx
|
|
74
|
+
} catch (err) { return null }
|
|
75
|
+
}
|
|
76
|
+
var dshwvAudioBuffers = {} // url -> Promise<AudioBuffer>(解码结果缓存,同一片段不重复下载/解码)
|
|
77
|
+
function dshwvAudioBuffer(url) {
|
|
78
|
+
if (!url) return Promise.reject(new Error('empty url'))
|
|
79
|
+
if (!dshwvAudioBuffers[url]) {
|
|
80
|
+
dshwvAudioBuffers[url] = fetch(url, { cache: 'force-cache' })
|
|
81
|
+
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer() })
|
|
82
|
+
.then(function (raw) {
|
|
83
|
+
var c = dshwvAudio()
|
|
84
|
+
if (!c) throw new Error('no audio context')
|
|
85
|
+
return new Promise(function (res, rej) { c.decodeAudioData(raw, res, rej) })
|
|
86
|
+
})
|
|
87
|
+
.catch(function (err) { delete dshwvAudioBuffers[url]; throw err })
|
|
88
|
+
}
|
|
89
|
+
return dshwvAudioBuffers[url]
|
|
90
|
+
}
|
|
91
|
+
function dshwvSoundStop(el) {
|
|
92
|
+
el._token = (el._token || 0) + 1
|
|
93
|
+
var node = el._node
|
|
94
|
+
el._node = null
|
|
95
|
+
if (node) {
|
|
96
|
+
try { node.onended = null } catch (err) {}
|
|
97
|
+
try { node.stop() } catch (err) {}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function dshwvSound(url) {
|
|
101
|
+
var el = { preload: 'auto', volume: 1, onended: null, loop: false, _url: String(url || ''), _node: null, _gain: null, _offset: 0, _token: 0 }
|
|
102
|
+
Object.defineProperty(el, 'src', {
|
|
103
|
+
get: function () { return el._url },
|
|
104
|
+
set: function (v) { dshwvSoundStop(el); el._url = String(v || ''); el._offset = 0 },
|
|
105
|
+
})
|
|
106
|
+
Object.defineProperty(el, 'currentTime', {
|
|
107
|
+
get: function () { return el._offset },
|
|
108
|
+
// 既有逻辑用「currentTime = 0」表示重播 → 这里顺手停掉正在播的那一份
|
|
109
|
+
set: function (v) { el._offset = Number(v) || 0; dshwvSoundStop(el) },
|
|
110
|
+
})
|
|
111
|
+
el.play = function () {
|
|
112
|
+
var c = dshwvAudio()
|
|
113
|
+
if (!c || !el._url) return Promise.resolve()
|
|
114
|
+
try {
|
|
115
|
+
if (!el._gain) { el._gain = c.createGain(); el._gain.connect(c.destination) }
|
|
116
|
+
el._gain.gain.value = Math.max(0, Math.min(1, Number(el.volume) || 0))
|
|
117
|
+
} catch (err) { return Promise.resolve() }
|
|
118
|
+
var token = (el._token = (el._token || 0) + 1)
|
|
119
|
+
var url = el._url
|
|
120
|
+
dshwvAudioBuffer(url).then(function (buf) {
|
|
121
|
+
if (token !== el._token) return // 期间被重播/暂停/换源 → 丢弃这次
|
|
122
|
+
try {
|
|
123
|
+
var src = c.createBufferSource()
|
|
124
|
+
src.buffer = buf
|
|
125
|
+
src.connect(el._gain)
|
|
126
|
+
src.onended = function () {
|
|
127
|
+
if (el._node !== src) return
|
|
128
|
+
el._node = null
|
|
129
|
+
if (typeof el.onended === 'function') { try { el.onended() } catch (err) {} }
|
|
130
|
+
}
|
|
131
|
+
el._node = src
|
|
132
|
+
var dur = Math.max(0.001, buf.duration)
|
|
133
|
+
src.start(0, Math.max(0, el._offset) % dur)
|
|
134
|
+
} catch (err) {}
|
|
135
|
+
}).catch(function () {})
|
|
136
|
+
return Promise.resolve()
|
|
137
|
+
}
|
|
138
|
+
el.pause = function () { dshwvSoundStop(el) }
|
|
139
|
+
return el
|
|
140
|
+
}
|
|
141
|
+
// 自动播放策略:AudioContext 初始是 suspended,要有一次用户手势才能出声。
|
|
142
|
+
// 任务结束音不是手势触发的,所以先挂一次性解锁(首次点击/按键后移除)。
|
|
143
|
+
try {
|
|
144
|
+
var dshwvAudioUnlock = function () {
|
|
145
|
+
dshwvAudio()
|
|
146
|
+
try { document.removeEventListener('pointerdown', dshwvAudioUnlock, true) } catch (err) {}
|
|
147
|
+
try { document.removeEventListener('keydown', dshwvAudioUnlock, true) } catch (err) {}
|
|
148
|
+
}
|
|
149
|
+
document.addEventListener('pointerdown', dshwvAudioUnlock, true)
|
|
150
|
+
document.addEventListener('keydown', dshwvAudioUnlock, true)
|
|
151
|
+
} catch (err) {}
|
|
152
|
+
|
|
46
153
|
var MIN_SCALE = 0.6
|
|
47
154
|
var MAX_SCALE = 2.5
|
|
48
155
|
var STEP = 0.1
|
|
@@ -293,7 +400,7 @@ var css = [
|
|
|
293
400
|
'.dshwv-usagepanel{position:fixed;z-index:26020;background:#fff;border:1px solid rgba(32,49,112,.35);border-radius:10px;box-shadow:0 8px 22px rgba(15,23,42,.22);padding:10px 12px;color-scheme:light;max-height:70vh;overflow-y:auto}',
|
|
294
401
|
// 用量作为主菜单内的子界面
|
|
295
402
|
'.dshwv-menuview{display:block}',
|
|
296
|
-
'.dshwv-usage-sub{display:none;max-height:min(70vh,560px);overflow-y:auto;padding-right:2px;width:100%;box-sizing:border-box}',
|
|
403
|
+
'.dshwv-usage-sub{display:none;max-height:min(70vh,560px);overflow-y:auto;padding-right:2px;padding-bottom:12px;width:100%;box-sizing:border-box}',
|
|
297
404
|
'.dshwv-usage-back{border:none;background:none;color:#203170;font-size:12px;font-weight:600;cursor:pointer;padding:0 0 2px;text-align:left;width:100%}',
|
|
298
405
|
'.dshwv-usage-back:hover{color:#2f4488;text-decoration:underline}',
|
|
299
406
|
'.dshwv-usagebody{display:flex;flex-direction:column;gap:2px;color:#203170;font-size:12px;min-width:0;overflow-x:hidden}',
|
|
@@ -1006,7 +1113,7 @@ function playTaskEndSound() {
|
|
|
1006
1113
|
url = '/dsh-whale/sound/' + (parts[2] === 'release' ? 'release' : 'press') + '.mp3?set=' + parts[1]
|
|
1007
1114
|
}
|
|
1008
1115
|
if (!url) return
|
|
1009
|
-
var a =
|
|
1116
|
+
var a = dshwvSound(url)
|
|
1010
1117
|
try { a.volume = Number(soundVol) || 0.9 } catch (err) {}
|
|
1011
1118
|
a.play().catch(function () {})
|
|
1012
1119
|
} catch (err) {}
|
|
@@ -1025,7 +1132,7 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1025
1132
|
// 按压留空:无按下音,直接播松开(模拟按下即松开的完整点按);松开留空:只播按压
|
|
1026
1133
|
if (pressEmpty) {
|
|
1027
1134
|
if (!releaseEmpty) {
|
|
1028
|
-
var relOnly =
|
|
1135
|
+
var relOnly = dshwvSound('/dsh-whale/sound/release.mp3?set=' + encodeURIComponent(groupId))
|
|
1029
1136
|
try { relOnly.volume = vol } catch (err) {}
|
|
1030
1137
|
relOnly.currentTime = 0
|
|
1031
1138
|
var pr = relOnly.play()
|
|
@@ -1033,7 +1140,7 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1033
1140
|
}
|
|
1034
1141
|
return
|
|
1035
1142
|
}
|
|
1036
|
-
var press =
|
|
1143
|
+
var press = dshwvSound('/dsh-whale/sound/press.mp3?set=' + encodeURIComponent(groupId))
|
|
1037
1144
|
try { press.volume = vol } catch (err) {}
|
|
1038
1145
|
if (releaseEmpty) {
|
|
1039
1146
|
press.currentTime = 0
|
|
@@ -1041,7 +1148,7 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1041
1148
|
if (pp && pp.catch) pp.catch(function () {})
|
|
1042
1149
|
return
|
|
1043
1150
|
}
|
|
1044
|
-
var release =
|
|
1151
|
+
var release = dshwvSound('/dsh-whale/sound/release.mp3?set=' + encodeURIComponent(groupId))
|
|
1045
1152
|
try { release.volume = vol } catch (err) {}
|
|
1046
1153
|
var relPlayed = false
|
|
1047
1154
|
function playRel() {
|
|
@@ -1870,6 +1977,13 @@ function apiModelById(id) {
|
|
|
1870
1977
|
for (var i = 0; i < apiModels.length; i++) if (apiModels[i] && apiModels[i].id === id) return apiModels[i]
|
|
1871
1978
|
return null
|
|
1872
1979
|
}
|
|
1980
|
+
// 「充值 / 余额校正」只属于固定的 DeepSeek(内置):宿主在模型条目里下发 canAdjustBalance,
|
|
1981
|
+
// 前端据此决定要不要在设置菜单里给出入口 —— 手动新增的同名模型、Kimi 等其它厂商都不会有,
|
|
1982
|
+
// 新增模板也不会自动继承(宿主还会在路由层再校验一次,见 balance-adjustments.json)。
|
|
1983
|
+
function apiCanAdjustBalance(model) {
|
|
1984
|
+
return !!(model && model.id === 'deepseek' && model.builtin === true &&
|
|
1985
|
+
model.provider === 'deepseek' && model.canAdjustBalance === true)
|
|
1986
|
+
}
|
|
1873
1987
|
// 今日已用金额自带的币种(host 下发的 todayUsageCurrency):
|
|
1874
1988
|
// 会话事件金额在 host 已按自定义单价折算成人民币,余额差则是厂商币种 → 显示必须按各自的币种,
|
|
1875
1989
|
// 否则 USD 单价的模型会把人民币数值渲染成 $。
|
|
@@ -2800,7 +2914,7 @@ function openApiModelMenu(modelId) {
|
|
|
2800
2914
|
else st.textContent = '余额 ' + apiFmtMoney(m.balance, m.currency) + ' · 今日已用 ' + apiFmtMoney(m.todayUsage, apiTodayCur(m))
|
|
2801
2915
|
card.appendChild(st)
|
|
2802
2916
|
var ms = (usageSet && usageSet.models && usageSet.models[modelId]) || {}
|
|
2803
|
-
function rowOf(label, stateFn, onEdit) {
|
|
2917
|
+
function rowOf(label, stateFn, onEdit, buttonLabel) {
|
|
2804
2918
|
var r = document.createElement('div')
|
|
2805
2919
|
r.className = 'dshwv-audiorow'
|
|
2806
2920
|
var l = document.createElement('span')
|
|
@@ -2817,8 +2931,9 @@ function openApiModelMenu(modelId) {
|
|
|
2817
2931
|
info.style.textOverflow = 'ellipsis'
|
|
2818
2932
|
info.textContent = stateFn()
|
|
2819
2933
|
r.appendChild(info)
|
|
2820
|
-
r.appendChild(apiBtn('编辑', 'dshwv-roleimport', onEdit))
|
|
2934
|
+
r.appendChild(apiBtn(buttonLabel || '编辑', 'dshwv-roleimport', onEdit))
|
|
2821
2935
|
card.appendChild(r)
|
|
2936
|
+
return r
|
|
2822
2937
|
}
|
|
2823
2938
|
rowOf('余额预警', function () {
|
|
2824
2939
|
var a = ms.alert
|
|
@@ -2875,13 +2990,15 @@ function openApiModelMenu(modelId) {
|
|
|
2875
2990
|
card.appendChild(pr)
|
|
2876
2991
|
}
|
|
2877
2992
|
// 只读行(复用现有 .dshwv-audiorow / .dshwv-usage-hint,不引入新颜色与字体)
|
|
2878
|
-
function readonlyRow(label, text) {
|
|
2993
|
+
function readonlyRow(label, text, hintHtml) {
|
|
2879
2994
|
var r = document.createElement('div')
|
|
2880
2995
|
r.className = 'dshwv-audiorow'
|
|
2881
2996
|
var l = document.createElement('span')
|
|
2882
2997
|
l.textContent = label
|
|
2883
2998
|
l.style.flex = '0 0 auto'
|
|
2884
2999
|
r.appendChild(l)
|
|
3000
|
+
// 需要「?」说明时,说明收进圆圈(悬停显示、点击钉住),与挂件其它说明圈同一套组件
|
|
3001
|
+
if (hintHtml) { try { r.appendChild(dshwvAskDot(hintHtml)) } catch (err) {} }
|
|
2885
3002
|
var v = document.createElement('span')
|
|
2886
3003
|
v.className = 'dshwv-usage-hint'
|
|
2887
3004
|
v.style.flex = '1'
|
|
@@ -2914,6 +3031,32 @@ function openApiModelMenu(modelId) {
|
|
|
2914
3031
|
pTxt = '未设置(沿用内置价目表)→ 在「密钥 / 接口」里填写'
|
|
2915
3032
|
}
|
|
2916
3033
|
readonlyRow('单价', pTxt)
|
|
3034
|
+
// 「已观测消费」= DeepSeek 账户口径(余额观测),只在内置项的设置里显示,与下面的「余额校正」配套。
|
|
3035
|
+
if (apiCanAdjustBalance(m)) {
|
|
3036
|
+
var acc = m.accounting || null
|
|
3037
|
+
var accAmt = (acc && typeof acc.amount === 'number') ? acc.amount : m.todayUsage
|
|
3038
|
+
var accInfo = (acc && acc.firstObservedAt)
|
|
3039
|
+
? '统计起点(北京):' + accountingTime(acc.firstObservedAt) + '。起点前的消费未计入;该账户的观测包含同一个 key 在别处的消费。'
|
|
3040
|
+
: '尚无余额观测:先配置 DeepSeek API key 并成功刷新一次余额。'
|
|
3041
|
+
readonlyRow('已观测消费', ((acc && acc.label) || m.usageLabel || '已观测消费') + ' ' +
|
|
3042
|
+
(isFinite(Number(accAmt)) ? apiFmtMoney(accAmt, apiTodayCur(m)) : '--'), accInfo)
|
|
3043
|
+
// 需要用户动手的提示仍然直接显示(不藏进「?」里)
|
|
3044
|
+
if (acc && acc.needsReview) {
|
|
3045
|
+
var accWarn = document.createElement('div')
|
|
3046
|
+
accWarn.className = 'dshwv-usage-hint'
|
|
3047
|
+
accWarn.style.cssText = 'line-height:1.65;white-space:normal;margin:2px 0 4px'
|
|
3048
|
+
accWarn.textContent = '检测到余额增加,请用下面的「余额校正」核对本区间累计到账。'
|
|
3049
|
+
card.appendChild(accWarn)
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
// 「充值 / 余额校正」入口:只给固定的 DeepSeek(内置),放在「单价」下方、沿用同一行布局与按钮样式。
|
|
3053
|
+
// 其它厂商、手动新增的 DeepSeek、仅改名为「DeepSeek(内置)」的模型都不会走到这里。
|
|
3054
|
+
if (apiCanAdjustBalance(m)) {
|
|
3055
|
+
var adjustmentRow = rowOf('余额校正', function () {
|
|
3056
|
+
return (m.accounting && m.accounting.label) || '充值与余额调整'
|
|
3057
|
+
}, function () { openBalanceAdjustment(modelId) }, '校正')
|
|
3058
|
+
adjustmentRow.lastElementChild.setAttribute('data-action', 'balance-adjustment')
|
|
3059
|
+
}
|
|
2917
3060
|
// 币种不一致提示:今日已用按「自带币种」显示(会话事件为 CNY)。若与模型币种不同且没填汇率,
|
|
2918
3061
|
// 今日预算提醒会被跳过(见 A 方案),这里给出可见的补救提示。
|
|
2919
3062
|
var tuc = String((m && m.todayUsageCurrency) || '').toUpperCase()
|
|
@@ -3009,94 +3152,92 @@ function accountingTime(at) {
|
|
|
3009
3152
|
} catch (err) { return String(at || '') }
|
|
3010
3153
|
}
|
|
3011
3154
|
var accountingMask = null
|
|
3012
|
-
function openBalanceAdjustment() {
|
|
3013
|
-
|
|
3014
|
-
|
|
3155
|
+
function openBalanceAdjustment(modelId) {
|
|
3156
|
+
// 只有固定的 DeepSeek(内置)能打开:入口由宿主下发的 canAdjustBalance 控制,这里再挡一次
|
|
3157
|
+
if (!apiCanAdjustBalance(apiModelById(modelId)) || accountingMask) return
|
|
3158
|
+
var adjustmentUrl = '/dsh-whale/balance-adjustments.json?modelId=' + encodeURIComponent(modelId)
|
|
3159
|
+
closeApiModelPanel()
|
|
3160
|
+
// 与「额度」「余额预警 / 今日预算」等窗口共用同一套骨架与样式:
|
|
3161
|
+
// dshwv-usage-mask + dshwv-usage-card + dshwv-bubtitle/bubhint + apiPanelRow/apiTextInput + dshwv-bubbtns
|
|
3015
3162
|
var mask = document.createElement('div')
|
|
3016
3163
|
mask.className = 'dshwv-usage-mask'
|
|
3017
|
-
mask.style.
|
|
3018
|
-
mask.style.zIndex = '2147483647'
|
|
3164
|
+
mask.style.zIndex = '30000'
|
|
3019
3165
|
var card = document.createElement('form')
|
|
3020
3166
|
card.className = 'dshwv-usage-card'
|
|
3021
|
-
card.style.
|
|
3167
|
+
card.style.width = 'min(430px,94vw)'
|
|
3168
|
+
card.style.padding = '14px 16px'
|
|
3169
|
+
card.style.boxSizing = 'border-box'
|
|
3170
|
+
card.style.textAlign = 'left'
|
|
3022
3171
|
card.setAttribute('role', 'dialog')
|
|
3023
3172
|
card.setAttribute('aria-modal', 'true')
|
|
3024
|
-
card.setAttribute('aria-label', '
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3173
|
+
card.setAttribute('aria-label', 'DeepSeek(内置)余额校正')
|
|
3174
|
+
// 内容可能比 82vh 高:中间这段自己滚,按钮行固定在底部(与 .dshwv-reswrap 同一做法)
|
|
3175
|
+
var body = document.createElement('div')
|
|
3176
|
+
body.style.cssText = 'min-height:0;overflow-y:auto;flex:1 1 auto'
|
|
3177
|
+
var title = document.createElement('div')
|
|
3178
|
+
title.className = 'dshwv-bubtitle'
|
|
3179
|
+
title.textContent = '余额校正 · DeepSeek(内置)'
|
|
3180
|
+
body.appendChild(title)
|
|
3181
|
+
var introduction = document.createElement('div')
|
|
3182
|
+
introduction.className = 'dshwv-bubhint'
|
|
3183
|
+
introduction.style.cssText = 'margin:0 0 8px;white-space:normal;line-height:1.6;text-align:left'
|
|
3030
3184
|
introduction.textContent = '核对统计区间内的全部到账后,重新计算本地消费。此操作不会充值,也不会改变 DeepSeek 账户余额。'
|
|
3031
|
-
introduction
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
var label = document.createElement('label')
|
|
3035
|
-
label.style.cssText = 'display:block;font-size:13px;margin:12px 0'
|
|
3036
|
-
var text = document.createElement('span')
|
|
3037
|
-
text.textContent = labelText
|
|
3038
|
-
label.appendChild(text)
|
|
3039
|
-
input.style.cssText = 'display:block;width:100%;box-sizing:border-box;min-height:40px;margin-top:5px;border:1px solid #b9c5e7;border-radius:8px;padding:8px;background:#fff;color:#203170;font:inherit'
|
|
3040
|
-
label.appendChild(input)
|
|
3041
|
-
card.appendChild(label)
|
|
3042
|
-
return text
|
|
3043
|
-
}
|
|
3044
|
-
var dates = document.createElement('select')
|
|
3045
|
-
field('记账日期(北京时间)', dates)
|
|
3185
|
+
body.appendChild(introduction)
|
|
3186
|
+
var dates = apiSelectEl([], '')
|
|
3187
|
+
body.appendChild(apiPanelRow('日期', dates))
|
|
3046
3188
|
var interval = document.createElement('div')
|
|
3047
|
-
interval.
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
credits
|
|
3189
|
+
interval.className = 'dshwv-bubhint'
|
|
3190
|
+
interval.style.cssText = 'margin:0 0 8px;white-space:pre-line;line-height:1.6;text-align:left'
|
|
3191
|
+
body.appendChild(interval)
|
|
3192
|
+
var credits = apiTextInput('', '未到账请填 0')
|
|
3051
3193
|
credits.inputMode = 'decimal'
|
|
3052
3194
|
credits.required = true
|
|
3053
3195
|
credits.autocomplete = 'off'
|
|
3054
|
-
|
|
3055
|
-
var
|
|
3056
|
-
|
|
3196
|
+
body.appendChild(apiPanelRow('累计到账', credits))
|
|
3197
|
+
var creditsHint = document.createElement('div')
|
|
3198
|
+
creditsHint.className = 'dshwv-bubhint'
|
|
3199
|
+
creditsHint.style.cssText = 'margin:0 0 8px;white-space:normal;line-height:1.6;text-align:left'
|
|
3200
|
+
body.appendChild(creditsHint)
|
|
3201
|
+
var debits = apiTextInput('0', '没有请填 0')
|
|
3057
3202
|
debits.inputMode = 'decimal'
|
|
3058
3203
|
debits.autocomplete = 'off'
|
|
3059
|
-
|
|
3060
|
-
var
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3204
|
+
body.appendChild(apiPanelRow('非调用扣减', debits))
|
|
3205
|
+
var debitsHint = document.createElement('div')
|
|
3206
|
+
debitsHint.className = 'dshwv-bubhint'
|
|
3207
|
+
debitsHint.style.cssText = 'margin:0 0 8px;white-space:normal;line-height:1.6;text-align:left'
|
|
3208
|
+
body.appendChild(debitsHint)
|
|
3064
3209
|
var preview = document.createElement('div')
|
|
3065
|
-
preview.
|
|
3210
|
+
preview.className = 'dshwv-bubhint'
|
|
3211
|
+
preview.style.cssText = 'margin:0 0 8px;white-space:normal;line-height:1.6;text-align:left;font-weight:700;color:#203170'
|
|
3066
3212
|
preview.setAttribute('aria-live', 'polite')
|
|
3067
|
-
|
|
3213
|
+
body.appendChild(preview)
|
|
3068
3214
|
var confirmRow = document.createElement('label')
|
|
3069
|
-
confirmRow.style.cssText = 'display:flex;gap:
|
|
3215
|
+
confirmRow.style.cssText = 'display:flex;gap:6px;align-items:flex-start;flex:1;min-width:0;font-size:12px;color:#203170;cursor:pointer;line-height:1.5'
|
|
3070
3216
|
var confirm = document.createElement('input')
|
|
3071
3217
|
confirm.type = 'checkbox'
|
|
3072
|
-
confirm.style.marginTop = '
|
|
3218
|
+
confirm.style.marginTop = '1px'
|
|
3073
3219
|
confirmRow.appendChild(confirm)
|
|
3074
3220
|
confirmRow.appendChild(document.createTextNode('我已核对本统计区间的全部到账和非调用扣减'))
|
|
3075
|
-
|
|
3221
|
+
body.appendChild(apiPanelRow('确认', confirmRow))
|
|
3076
3222
|
var status = document.createElement('div')
|
|
3223
|
+
status.className = 'dshwv-bubhint'
|
|
3077
3224
|
status.setAttribute('role', 'status')
|
|
3078
|
-
status.style.cssText = '
|
|
3225
|
+
status.style.cssText = 'margin:0 0 6px;white-space:normal;line-height:1.6;text-align:left;color:#b33333'
|
|
3079
3226
|
status.textContent = '正在刷新余额…'
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
actions.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-top:16px'
|
|
3083
|
-
function button(text, callback) {
|
|
3084
|
-
var btn = document.createElement('button')
|
|
3085
|
-
btn.type = 'button'
|
|
3086
|
-
btn.textContent = text
|
|
3087
|
-
btn.className = 'dshwv-bubbtn dshwv-bubbtn-no'
|
|
3088
|
-
btn.style.cssText = 'min-height:38px;padding:8px 12px;cursor:pointer'
|
|
3089
|
-
if (callback) btn.addEventListener('click', callback)
|
|
3090
|
-
actions.appendChild(btn)
|
|
3091
|
-
return btn
|
|
3092
|
-
}
|
|
3227
|
+
body.appendChild(status)
|
|
3228
|
+
card.appendChild(body)
|
|
3093
3229
|
var busy = false
|
|
3094
3230
|
function close() {
|
|
3095
3231
|
if (busy) return
|
|
3096
3232
|
document.removeEventListener('keydown', keyHandler)
|
|
3097
3233
|
mask.remove()
|
|
3098
3234
|
accountingMask = null
|
|
3099
|
-
|
|
3235
|
+
// 关掉校正窗口后回到同一个设置菜单,并把焦点放回「校正」按钮
|
|
3236
|
+
openApiModelMenu(modelId)
|
|
3237
|
+
try {
|
|
3238
|
+
var returnButton = apiModelMaskEl && apiModelMaskEl.querySelector('[data-action="balance-adjustment"]')
|
|
3239
|
+
if (returnButton) returnButton.focus()
|
|
3240
|
+
} catch (err) {}
|
|
3100
3241
|
}
|
|
3101
3242
|
function keyHandler(e) {
|
|
3102
3243
|
if (e.key === 'Escape') { e.preventDefault(); close() }
|
|
@@ -3109,25 +3250,17 @@ function openBalanceAdjustment() {
|
|
|
3109
3250
|
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
|
|
3110
3251
|
}
|
|
3111
3252
|
}
|
|
3112
|
-
|
|
3113
|
-
var
|
|
3114
|
-
|
|
3115
|
-
var
|
|
3116
|
-
|
|
3117
|
-
save
|
|
3118
|
-
|
|
3119
|
-
|
|
3253
|
+
// 按钮行与其它窗口完全一致:.dshwv-bubbtns + apiBtn 的标准按钮样式(不再自绘尺寸/配色)
|
|
3254
|
+
var actions = document.createElement('div')
|
|
3255
|
+
actions.className = 'dshwv-bubbtns'
|
|
3256
|
+
var reload = apiBtn('重新读取', 'dshwv-bubbtn dshwv-bubbtn-no', function () { load() })
|
|
3257
|
+
var reset = apiBtn('撤销该日校正', 'dshwv-bubbtn dshwv-bubbtn-no', function () { submit('reset') })
|
|
3258
|
+
var save = apiBtn('保存校正', 'dshwv-bubbtn dshwv-bubbtn-ok', function () { submit('save') })
|
|
3259
|
+
actions.appendChild(reload)
|
|
3260
|
+
actions.appendChild(reset)
|
|
3261
|
+
actions.appendChild(apiBtn('取消', 'dshwv-bubbtn dshwv-bubbtn-no', function () { close() }))
|
|
3262
|
+
actions.appendChild(save)
|
|
3120
3263
|
card.appendChild(actions)
|
|
3121
|
-
var scrollBody = document.createElement('div')
|
|
3122
|
-
scrollBody.style.cssText = 'min-height:0;overflow:auto;flex:1 1 auto;padding-right:3px'
|
|
3123
|
-
while (card.firstChild && card.firstChild !== actions) scrollBody.appendChild(card.firstChild)
|
|
3124
|
-
card.insertBefore(scrollBody, actions)
|
|
3125
|
-
card.style.display = 'flex'
|
|
3126
|
-
card.style.flexDirection = 'column'
|
|
3127
|
-
card.style.overflow = 'hidden'
|
|
3128
|
-
actions.style.flex = '0 0 auto'
|
|
3129
|
-
actions.style.paddingTop = '10px'
|
|
3130
|
-
actions.style.borderTop = '1px solid #e2e8f5'
|
|
3131
3264
|
mask.appendChild(card)
|
|
3132
3265
|
mask.addEventListener('click', function (e) { if (e.target === mask) close() })
|
|
3133
3266
|
document.body.appendChild(mask)
|
|
@@ -3141,6 +3274,8 @@ function openBalanceAdjustment() {
|
|
|
3141
3274
|
reset.disabled = !selected || !selected.correctedAt || busy
|
|
3142
3275
|
if (!selected) {
|
|
3143
3276
|
interval.textContent = '尚无可校正的余额观测。请先配置 DeepSeek API key 并成功刷新余额。'
|
|
3277
|
+
creditsHint.textContent = ''
|
|
3278
|
+
debitsHint.textContent = ''
|
|
3144
3279
|
return
|
|
3145
3280
|
}
|
|
3146
3281
|
interval.textContent = '统计起点:' + accountingTime(selected.firstObservedAt) +
|
|
@@ -3148,8 +3283,8 @@ function openBalanceAdjustment() {
|
|
|
3148
3283
|
'\n起点余额 ' + usageMoney(selected.openingBalance, selected.currency) +
|
|
3149
3284
|
' → 当前余额 ' + usageMoney(selected.currentBalance, selected.currency) +
|
|
3150
3285
|
'\n当前:' + selected.label + ' ' + usageMoney(selected.amount, selected.currency)
|
|
3151
|
-
|
|
3152
|
-
|
|
3286
|
+
creditsHint.textContent = '本统计区间累计到账金额(' + selected.currency + ',未到账请填 0):包括充值、赠金等;多次到账请填合计,不要只填最后一笔。'
|
|
3287
|
+
debitsHint.textContent = '非调用造成的余额减少(' + selected.currency + ',没有请填 0):到期赠金、余额退回等。仅填写统计起点之后的金额;保存会替换之前的校正值。'
|
|
3153
3288
|
credits.value = selected.credits == null ? '' : String(selected.credits)
|
|
3154
3289
|
debits.value = selected.otherDebits == null ? '0' : String(selected.otherDebits)
|
|
3155
3290
|
confirm.checked = false
|
|
@@ -3173,7 +3308,7 @@ function openBalanceAdjustment() {
|
|
|
3173
3308
|
reset.disabled = true
|
|
3174
3309
|
reload.disabled = true
|
|
3175
3310
|
status.textContent = '正在刷新余额…'
|
|
3176
|
-
fetch(
|
|
3311
|
+
fetch(adjustmentUrl, { cache: 'no-store' })
|
|
3177
3312
|
.then(function (r) { return r.json() })
|
|
3178
3313
|
.then(function (data) {
|
|
3179
3314
|
if (!data.ok) throw new Error(data.error || '读取失败')
|
|
@@ -3198,16 +3333,29 @@ function openBalanceAdjustment() {
|
|
|
3198
3333
|
reset.disabled = true
|
|
3199
3334
|
reload.disabled = true
|
|
3200
3335
|
status.textContent = '正在保存…'
|
|
3201
|
-
fetch(
|
|
3336
|
+
fetch(adjustmentUrl, {
|
|
3202
3337
|
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
3203
3338
|
body: JSON.stringify({
|
|
3204
|
-
day: selected.day, revision: selected.revision, action: action,
|
|
3339
|
+
modelId: modelId, day: selected.day, revision: selected.revision, action: action,
|
|
3205
3340
|
credits: credits.value.trim(), otherDebits: debits.value.trim() || '0', confirmed: confirm.checked
|
|
3206
3341
|
})
|
|
3207
3342
|
})
|
|
3208
3343
|
.then(function (r) { return r.json() })
|
|
3209
3344
|
.then(function (data) {
|
|
3210
3345
|
if (!data.ok) throw new Error(data.error || '保存失败')
|
|
3346
|
+
// 保存后立刻用返回的摘要刷新该模型条目:关窗回到设置菜单时显示的就是新金额
|
|
3347
|
+
var model = apiModelById(modelId)
|
|
3348
|
+
var currentDay = new Date(Date.now() + 8 * 3600000).toISOString().slice(0, 10)
|
|
3349
|
+
if (apiCanAdjustBalance(model) && data.summary && data.summary.day === currentDay) {
|
|
3350
|
+
model.balance = data.summary.currentBalance
|
|
3351
|
+
model.currency = data.summary.currency
|
|
3352
|
+
model.todayUsage = data.summary.amount
|
|
3353
|
+
model.todayUsageCurrency = data.summary.currency
|
|
3354
|
+
model.usageSource = data.summary.source
|
|
3355
|
+
model.usageLabel = data.summary.label
|
|
3356
|
+
model.accounting = data.summary
|
|
3357
|
+
model.error = null
|
|
3358
|
+
}
|
|
3211
3359
|
busy = false
|
|
3212
3360
|
close()
|
|
3213
3361
|
refresh(true)
|
|
@@ -3235,25 +3383,12 @@ function fillUsagePanel(d) {
|
|
|
3235
3383
|
var today = d.today || {}
|
|
3236
3384
|
var todayModels = today.models || []
|
|
3237
3385
|
var hasEvToday = todayModels.length > 0
|
|
3238
|
-
// ①
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
coverage.style.cssText = 'line-height:1.65;white-space:normal;margin:4px 0'
|
|
3245
|
-
coverage.textContent = today.firstObservedAt
|
|
3246
|
-
? '统计起点(北京):' + accountingTime(today.firstObservedAt) + '。起点前的消费未计入。'
|
|
3247
|
-
: '尚无余额观测;此处仅显示本机会话估算或旧版记录。'
|
|
3248
|
-
if (today.needsReview) coverage.textContent += ' 检测到余额增加,请核对本区间累计到账。'
|
|
3249
|
-
wrap.appendChild(coverage)
|
|
3250
|
-
var adjustButton = document.createElement('button')
|
|
3251
|
-
adjustButton.type = 'button'
|
|
3252
|
-
adjustButton.className = 'dshwv-usage-more'
|
|
3253
|
-
adjustButton.textContent = '充值 / 余额校正'
|
|
3254
|
-
adjustButton.addEventListener('click', function (e) { e.stopPropagation(); openBalanceAdjustment() })
|
|
3255
|
-
wrap.appendChild(adjustButton)
|
|
3256
|
-
var modelTitle = uSectionTitle('本机模型费用 · 估算', usageMoney(today.modelTotal, 'CNY'))
|
|
3386
|
+
// ① 本机模型费用(所有模型的本地会话估算)
|
|
3387
|
+
// 「已观测消费」是 DeepSeek 账户口径(余额观测),已移到 小鲸鱼记账 → DeepSeek(内置)→ 设置 里,
|
|
3388
|
+
// 与「余额校正」放在一起,避免在这里被误读成"全模型合计"。
|
|
3389
|
+
var modelTitle = uSectionTitle('本机模型费用', usageMoney(today.modelTotal, 'CNY'))
|
|
3390
|
+
// 标题与下方模型列表之间不再画分隔线(列表本身有边框,够了)
|
|
3391
|
+
modelTitle.style.borderBottom = 'none'
|
|
3257
3392
|
modelTitle.title = '按本机 DSH 会话计算,不按账户余额比例缩放;两者覆盖范围不同。'
|
|
3258
3393
|
wrap.appendChild(modelTitle)
|
|
3259
3394
|
var todayBox = document.createElement('div')
|
|
@@ -3456,7 +3591,7 @@ function resPlayFragment(fid) {
|
|
|
3456
3591
|
try {
|
|
3457
3592
|
if (!fid) return
|
|
3458
3593
|
if (resAudEl) { try { resAudEl.pause() } catch (err) {} resAudEl = null }
|
|
3459
|
-
var a =
|
|
3594
|
+
var a = dshwvSound('/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(fid))
|
|
3460
3595
|
try { a.volume = Number(soundVol) || 0.9 } catch (err) {}
|
|
3461
3596
|
a.onended = function () { resAudEl = null }
|
|
3462
3597
|
resAudEl = a
|
|
@@ -8232,7 +8367,7 @@ var bubbleColorOpenMenu = null // 当前展开的颜色下拉(纯色/跑马灯,
|
|
|
8232
8367
|
// —— 弹层/菜单层级统一助手(所有下拉与弹窗据此取高于当前可见层,避免互相压制) ——
|
|
8233
8368
|
function visibleTopZ() {
|
|
8234
8369
|
var top = 20500
|
|
8235
|
-
var cand = [bubbleMask, bubbleItemMask, moduleMask, usageMoreMask, qeditEl, window.__dshwRemindMask]
|
|
8370
|
+
var cand = [bubbleMask, bubbleItemMask, moduleMask, usageMoreMask, qeditEl, window.__dshwRemindMask, apiModelMaskEl, accountingMask]
|
|
8236
8371
|
function eff(el) {
|
|
8237
8372
|
try {
|
|
8238
8373
|
if (!el) return 0
|
|
@@ -12108,6 +12243,16 @@ function artCenterAt(left, top, w, h, flipped) {
|
|
|
12108
12243
|
var cy = top + h - iw / 2
|
|
12109
12244
|
return { cx: cx, cy: cy }
|
|
12110
12245
|
}
|
|
12246
|
+
// v739(用户反馈「每次新实例的第一次余额请求都失败」):冷启动时凭据服务可能还没就绪,
|
|
12247
|
+
// 首次 DNS+TLS 也最慢;而客户端 25s 超时会先于宿主的两段重试结束 —— 结果是第一次必失败、
|
|
12248
|
+
// 只能干等 60 秒后的下一轮。这里失败后快速重试两次(1.5s / 3s),成功即重置计数。
|
|
12249
|
+
var balanceRetryLeft = 2
|
|
12250
|
+
function balanceRetryLater() {
|
|
12251
|
+
if (balanceRetryLeft <= 0) return
|
|
12252
|
+
var delay = balanceRetryLeft === 2 ? 1500 : 3000
|
|
12253
|
+
balanceRetryLeft--
|
|
12254
|
+
setTimeout(function () { try { refresh(false) } catch (err) {} }, delay)
|
|
12255
|
+
}
|
|
12111
12256
|
function refresh(manual) {
|
|
12112
12257
|
if (busy) return
|
|
12113
12258
|
busy = true
|
|
@@ -12130,6 +12275,7 @@ function refresh(manual) {
|
|
|
12130
12275
|
state.balance = nb
|
|
12131
12276
|
state.currency = nc
|
|
12132
12277
|
state.message = ''
|
|
12278
|
+
balanceRetryLeft = 2
|
|
12133
12279
|
state.todayUsage = data.todayUsage !== undefined ? data.todayUsage : null
|
|
12134
12280
|
state.todayUsageCurrency = data.todayUsageCurrency || data.currency || 'CNY'
|
|
12135
12281
|
state.usageLabel = data.usageLabel || '本地估算'
|
|
@@ -12165,12 +12311,14 @@ function refresh(manual) {
|
|
|
12165
12311
|
state.status = 'error'
|
|
12166
12312
|
state.message = (data && data.error) ? String(data.error) : '获取失败'
|
|
12167
12313
|
render()
|
|
12314
|
+
balanceRetryLater()
|
|
12168
12315
|
}
|
|
12169
12316
|
})
|
|
12170
12317
|
.catch(function () {
|
|
12171
12318
|
state.status = 'error'
|
|
12172
12319
|
state.message = '获取失败'
|
|
12173
12320
|
render()
|
|
12321
|
+
balanceRetryLater()
|
|
12174
12322
|
})
|
|
12175
12323
|
.finally(function () {
|
|
12176
12324
|
busy = false
|
|
@@ -12410,12 +12558,12 @@ function applySoundSet() {
|
|
|
12410
12558
|
var pEmpty = audioGroupSlotEmpty(soundSet, 'press')
|
|
12411
12559
|
var rEmpty = audioGroupSlotEmpty(soundSet, 'release')
|
|
12412
12560
|
if (pEmpty) { pressAudio = null } else {
|
|
12413
|
-
pressAudio =
|
|
12561
|
+
pressAudio = dshwvSound('/dsh-whale/sound/press.mp3?set=' + soundSet)
|
|
12414
12562
|
pressAudio.preload = 'auto'
|
|
12415
12563
|
pressAudio.volume = soundVol
|
|
12416
12564
|
}
|
|
12417
12565
|
if (rEmpty) { releaseAudio = null } else {
|
|
12418
|
-
releaseAudio =
|
|
12566
|
+
releaseAudio = dshwvSound('/dsh-whale/sound/release.mp3?set=' + soundSet)
|
|
12419
12567
|
releaseAudio.preload = 'auto'
|
|
12420
12568
|
releaseAudio.volume = soundVol
|
|
12421
12569
|
}
|
|
@@ -13449,12 +13597,12 @@ function audioEditPreviewEnsure(force) {
|
|
|
13449
13597
|
if (audioEditPreviewRelease) { audioEditPreviewRelease.pause(); audioEditPreviewRelease = null }
|
|
13450
13598
|
} catch (err) {}
|
|
13451
13599
|
if (pressId) {
|
|
13452
|
-
audioEditPreviewEl =
|
|
13600
|
+
audioEditPreviewEl = dshwvSound('/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(pressId))
|
|
13453
13601
|
audioEditPreviewEl.preload = 'auto'
|
|
13454
13602
|
audioEditPreviewEl.volume = soundVol
|
|
13455
13603
|
}
|
|
13456
13604
|
if (releaseId) {
|
|
13457
|
-
audioEditPreviewRelease =
|
|
13605
|
+
audioEditPreviewRelease = dshwvSound('/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(releaseId))
|
|
13458
13606
|
audioEditPreviewRelease.preload = 'auto'
|
|
13459
13607
|
audioEditPreviewRelease.volume = soundVol
|
|
13460
13608
|
}
|
|
@@ -14616,8 +14764,6 @@ function pollLastTurn() {
|
|
|
14616
14764
|
}
|
|
14617
14765
|
setInterval(pollLastTurn, 1000)
|
|
14618
14766
|
}
|
|
14619
|
-
//
|
|
14620
|
-
|
|
14621
|
-
try { dshwInit() } catch (err) {}
|
|
14622
|
-
}
|
|
14767
|
+
// 主界面检测通过(或稍后由 MutationObserver 检测到)后执行挂件初始化;非主界面不启动
|
|
14768
|
+
try { dshwTryStart(true) } catch (err) {}
|
|
14623
14769
|
})()
|
package/lib/index.js
CHANGED
|
@@ -75,6 +75,11 @@ const API_FILE_CANDIDATES = [
|
|
|
75
75
|
path.join(DSH_HOME, 'profiles', 'web', '.dshw-api.json'),
|
|
76
76
|
]
|
|
77
77
|
const API_BUILTIN_ID = 'deepseek'
|
|
78
|
+
// 「充值 / 余额校正」只属于固定的 DeepSeek(内置):模型条目下发 canAdjustBalance,路由层再校验一次。
|
|
79
|
+
// 新增厂商模板 / 手动新增的同名模型 / Kimi 等其它厂商都不会继承这个能力。
|
|
80
|
+
function canAdjustBuiltinBalance(model) {
|
|
81
|
+
return !!(model && model.id === API_BUILTIN_ID && model.builtin === true && model.provider === 'deepseek')
|
|
82
|
+
}
|
|
78
83
|
// 内置厂商模板:balance 描述「怎么取余额」。json 路径支持 a.b[0].c;scale 为取值后的乘数。
|
|
79
84
|
// 余额接口已按官方文档 / 社区参考核对(DeepSeek、OpenRouter、Kimi、阶跃、Novita、OpenAI 兼容中转站);
|
|
80
85
|
// 硅基流动的余额接口已官方下线(410),故改为「无余额接口 + /v1/models 探活」;
|
|
@@ -711,11 +716,19 @@ export default {
|
|
|
711
716
|
}
|
|
712
717
|
|
|
713
718
|
async function fetchBalance() {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
+
// v739(用户反馈「每次新实例的第一次余额请求都失败」):
|
|
720
|
+
// ① 冷启动时凭据服务可能尚未就绪(resolve 抛错),这与「确实没配置」不同 → 短延迟重试一次;
|
|
721
|
+
// ② 单次超时从 20s 收到 8s,保证「2 次尝试 + 退避」明显短于前端 FETCH_TIMEOUT_MS(25s) ——
|
|
722
|
+
// 否则前端先 abort,用户看到的永远是「第一次失败」,只能干等 60 秒后的下一轮。
|
|
723
|
+
let cred = null
|
|
724
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
725
|
+
try {
|
|
726
|
+
cred = await ctx.credentials.resolve('DEEPSEEK_API_KEY')
|
|
727
|
+
break
|
|
728
|
+
} catch (err) {
|
|
729
|
+
if (attempt === 0) { await new Promise((r) => setTimeout(r, 700)); continue }
|
|
730
|
+
return { ok: false, code: 'NO_KEY', error: '凭据读取失败: ' + String((err && err.message) || err).slice(0, 160) }
|
|
731
|
+
}
|
|
719
732
|
}
|
|
720
733
|
if (!cred) {
|
|
721
734
|
return { ok: false, code: 'NO_KEY', error: '未配置 DEEPSEEK_API_KEY' }
|
|
@@ -726,7 +739,7 @@ export default {
|
|
|
726
739
|
try {
|
|
727
740
|
res = await fetch(BALANCE_URL, {
|
|
728
741
|
headers: { Authorization: 'Bearer ' + cred.value },
|
|
729
|
-
signal: AbortSignal.timeout(
|
|
742
|
+
signal: AbortSignal.timeout(8000),
|
|
730
743
|
})
|
|
731
744
|
} catch (err) {
|
|
732
745
|
lastErr = err
|
|
@@ -916,7 +929,7 @@ export default {
|
|
|
916
929
|
const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
|
|
917
930
|
for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
|
|
918
931
|
return {
|
|
919
|
-
ok: true, version: '0.3.
|
|
932
|
+
ok: true, version: '0.3.3', today: todayData, days7,
|
|
920
933
|
total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
|
|
921
934
|
all: {
|
|
922
935
|
days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
|
|
@@ -1824,6 +1837,7 @@ export default {
|
|
|
1824
1837
|
let entry = {
|
|
1825
1838
|
id: m.id, name: m.name, provider: m.provider, currency: m.currency,
|
|
1826
1839
|
keyRef: m.keyRef, builtin: !!m.builtin, baseUrl: m.baseUrl || '',
|
|
1840
|
+
canAdjustBalance: canAdjustBuiltinBalance(m),
|
|
1827
1841
|
matchIds: m.matchIds || [], settings: settings.models && settings.models[m.id] ? settings.models[m.id] : null,
|
|
1828
1842
|
price: m.price || null,
|
|
1829
1843
|
// 手动额度(订阅/资源包):总量、单位、已用、重置周期;前端据此显示额度模块
|
|
@@ -1959,7 +1973,7 @@ export default {
|
|
|
1959
1973
|
const led = readUsageLedger()
|
|
1960
1974
|
const summary = daySummary(led, todayKey())
|
|
1961
1975
|
return {
|
|
1962
|
-
...visible, version: '0.3.
|
|
1976
|
+
...visible, version: '0.3.3', isPeak: isPeakTime(Math.floor(Date.now() / 1000)),
|
|
1963
1977
|
todayUsage: summary.amount, todayUsageCurrency: summary.currency,
|
|
1964
1978
|
usageSource: summary.source, usageLabel: summary.label, usageMode: 'ledger',
|
|
1965
1979
|
accounting: balanceSummary(led, todayKey()),
|
|
@@ -2498,9 +2512,23 @@ export default {
|
|
|
2498
2512
|
path: '/dsh-whale/balance-adjustments.json',
|
|
2499
2513
|
handler: async (req, res) => {
|
|
2500
2514
|
try {
|
|
2515
|
+
// 先核对身份再碰余额或账本:必须是唯一且明确的 DeepSeek(内置)。
|
|
2516
|
+
// 新增厂商模板不会自动继承这个能力,也拒绝其它模型读写校正。
|
|
2517
|
+
const targetIds = new URL(req.url || '/', 'http://localhost').searchParams.getAll('modelId')
|
|
2518
|
+
const targetModelId = targetIds.length === 1 ? targetIds[0] : null
|
|
2519
|
+
if (targetModelId !== API_BUILTIN_ID || !canAdjustBuiltinBalance(apiModelById(targetModelId))) {
|
|
2520
|
+
const error = new Error('余额校正仅支持 DeepSeek(内置),请从该模型的设置菜单进入')
|
|
2521
|
+
error.status = 403
|
|
2522
|
+
throw error
|
|
2523
|
+
}
|
|
2501
2524
|
if (req.method === 'PUT') {
|
|
2502
2525
|
const input = JSON.parse(await readBodyMax(req, 8192))
|
|
2503
2526
|
if (!input || typeof input !== 'object') throw new Error('校正内容无效')
|
|
2527
|
+
if (input.modelId !== targetModelId) {
|
|
2528
|
+
const error = new Error('校正请求的模型不匹配,仅支持 DeepSeek(内置)')
|
|
2529
|
+
error.status = 403
|
|
2530
|
+
throw error
|
|
2531
|
+
}
|
|
2504
2532
|
if (input.day === todayKey()) {
|
|
2505
2533
|
const fresh = await getBalance(true)
|
|
2506
2534
|
if (!fresh.ok || fresh.stale) {
|
package/package.json
CHANGED