dsh-whale-widget 0.2.6 → 0.2.8
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 +31 -15
- package/lib/index.js +174 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,38 +43,54 @@ dsh-whale-widget/
|
|
|
43
43
|
|
|
44
44
|
## 安装
|
|
45
45
|
|
|
46
|
-
### 方式 A
|
|
46
|
+
### 方式 A:本地安装(推荐,从当前仓库)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
在**仓库根目录**(`DeepSeek-Balance-Whale-Widget`,即 `package.json` 所在目录)执行:
|
|
49
49
|
|
|
50
50
|
```powershell
|
|
51
|
-
dsh plugin --profile web add link
|
|
51
|
+
dsh plugin --profile web add link:.
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
说明:
|
|
55
55
|
|
|
56
56
|
- `dsh plugin` 会把参数转发给 pnpm,并在成功后自动把 `dsh-whale-widget` 加入 `dsh.profile.bundles`
|
|
57
|
-
-
|
|
58
|
-
- 安装完成后重启 `dsh web`,再 F5 刷新浏览器
|
|
59
|
-
- **如果之后移动了源码目录**,必须重新到新的项目根目录执行一次:
|
|
57
|
+
- **`link:.` 表示链接当前目录**(仓库根目录本身就是插件包)。如果你复制了仓库到别处,用绝对路径:
|
|
60
58
|
```powershell
|
|
61
|
-
dsh plugin --profile web add link
|
|
59
|
+
dsh plugin --profile web add link:D:\你的路径\DeepSeek-Balance-Whale-Widget
|
|
62
60
|
```
|
|
63
|
-
|
|
61
|
+
- ⚠️ 不要用 `link:.\dsh-whale-widget`——仓库里**没有** `dsh-whale-widget/` 子目录,这样会安装成普通依赖而非插件,重启后挂件不出现
|
|
62
|
+
- 安装完成后重启 `dsh web`,再 F5 刷新浏览器
|
|
63
|
+
- **如果之后移动了源码目录**,必须重新执行一次 `dsh plugin --profile web add link:.<新路径>`。若提示已存在/冲突,先 `dsh plugin --profile web remove dsh-whale-widget` 再重新 add
|
|
64
64
|
|
|
65
65
|
### 方式 B:发布到 npm 后安装
|
|
66
66
|
|
|
67
|
-
如果你把这个包发布到 npm:
|
|
68
|
-
|
|
69
67
|
```powershell
|
|
70
|
-
|
|
71
|
-
npm publish
|
|
68
|
+
dsh plugin --profile web add dsh-whale-widget
|
|
72
69
|
```
|
|
73
70
|
|
|
74
|
-
|
|
71
|
+
### 给 AI 的安装说明(用 dsh 辅助安装时,直接复制给 AI)
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
如果你希望让另一个 DSH / AI 助手帮你安装,把下面这段发给它即可:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
请帮我安装插件 dsh-whale-widget,来源是 GitHub 仓库 MeteorNOX/DeepSeek-Balance-Whale-Widget。
|
|
77
|
+
|
|
78
|
+
步骤:
|
|
79
|
+
1. 确保 pnpm 可用(没有就先:npm install -g pnpm)
|
|
80
|
+
2. 在 Web profile 安装:
|
|
81
|
+
dsh plugin --profile web add dsh-whale-widget
|
|
82
|
+
如果要从本地仓库链接安装(例如本地克隆的仓库根目录),则用:
|
|
83
|
+
dsh plugin --profile web add link:.<仓库绝对路径>
|
|
84
|
+
(注意:仓库根目录就是插件包,不要写成 link:.\dsh-whale-widget 这种带子目录的路径)
|
|
85
|
+
3. 如果报 pnpm 阻止构建脚本(allowBuilds 相关),在 C:\Users\<用户名>\.dsh\profiles\web\pnpm-workspace.yaml 的 allowBuilds 下加对应的包 key,然后重跑
|
|
86
|
+
4. 重启 dsh web,然后 F5 刷新浏览器
|
|
87
|
+
|
|
88
|
+
安装后验证:
|
|
89
|
+
- dsh --profile web --dump-config 应该能看到 dsh-whale-widget 在 bundles 里
|
|
90
|
+
- curl http://127.0.0.1:3080/dsh-whale/balance.json 应返回 200 JSON(含 totalBalance)
|
|
91
|
+
- curl http://127.0.0.1:3080/dsh-whale/widget.js 应返回 200 JS
|
|
92
|
+
|
|
93
|
+
另外请检查 DSH 凭据里是否配置了 DEEPSEEK_API_KEY(没有就提示用户配置;DEEPSEEK_PLATFORM_TOKEN 可选,不配也能用默认的记账模式)。
|
|
78
94
|
```
|
|
79
95
|
|
|
80
96
|
### 关于令牌(安装后必读)
|
package/lib/index.js
CHANGED
|
@@ -69,11 +69,14 @@ const PEAK_HOURS = [
|
|
|
69
69
|
[14, 18],
|
|
70
70
|
]
|
|
71
71
|
const BASE_PRICE = { hit: [0.05, 0.1], miss: [1.5, 3.0], out: [4.5, 9.0] }
|
|
72
|
+
// deepseek-v4-pro 为 flash 的 3 倍价(官方 2026-08-17 生效);vision-exp 与 flash 同价
|
|
73
|
+
const PRO_PRICE = { hit: [0.15, 0.3], miss: [4.5, 9.0], out: [13.5, 27.0] }
|
|
72
74
|
const PRICING = {
|
|
75
|
+
'deepseek-v4-flash-vision-exp': BASE_PRICE,
|
|
76
|
+
'deepseek-v4-flash': BASE_PRICE,
|
|
77
|
+
'deepseek-v4-pro': PRO_PRICE,
|
|
73
78
|
'deepseek-chat': BASE_PRICE,
|
|
74
79
|
'deepseek-reasoner': BASE_PRICE,
|
|
75
|
-
'deepseek-v4-flash': BASE_PRICE,
|
|
76
|
-
'deepseek-v4-pro': BASE_PRICE,
|
|
77
80
|
_default: BASE_PRICE,
|
|
78
81
|
}
|
|
79
82
|
function priceFor(model) {
|
|
@@ -151,16 +154,17 @@ var css = [
|
|
|
151
154
|
'.dshwv-menu-btn.dshwv-menu-btn-visible{opacity:1}',
|
|
152
155
|
'.dshwv-menu-btn span{display:block;width:14px;height:2px;background:#fff;border-radius:1px}',
|
|
153
156
|
'.dshwv-menu-btn:hover{background:#203170}',
|
|
154
|
-
'.dshwv-menu{position:fixed;min-width:
|
|
157
|
+
'.dshwv-menu{position:fixed;min-width:196px;background:rgba(255,255,255,.92);border:1px solid rgba(32,49,112,.35);border-radius:10px;padding:10px 12px;opacity:0;transform:scale(.92) translateY(-4px);transform-origin:top right;transition:opacity .18s ease,transform .2s cubic-bezier(.34,1.56,.64,1);pointer-events:none;z-index:10000;box-shadow:0 6px 18px rgba(0,0,0,.18);color-scheme:light}',
|
|
155
158
|
'.dshwv-menu.dshwv-menu-open{opacity:1;transform:scale(1) translateY(0);pointer-events:auto}',
|
|
156
159
|
'.dshwv-menu-row{display:flex;align-items:center;gap:8px;margin:5px 0;color:#203170;font-size:12px;white-space:nowrap}',
|
|
157
160
|
'.dshwv-range{flex:1;min-width:0;accent-color:#203170}',
|
|
158
|
-
'.dshwv-number{width:
|
|
161
|
+
'.dshwv-number{width:44px;border:1px solid rgba(32,49,112,.4);border-radius:6px;padding:2px 4px;font-size:12px;color:#203170;background:#fff;box-sizing:border-box}',
|
|
162
|
+
'.dshwv-number:disabled{opacity:.4;background:rgba(32,49,112,.06);cursor:not-allowed}',
|
|
159
163
|
'.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}',
|
|
160
164
|
'.dshwv-sound:hover{background:rgba(32,49,112,.16)}',
|
|
161
165
|
'.dshwv-check{width:16px;height:16px;accent-color:#203170;cursor:pointer;flex:0 0 auto}',
|
|
162
166
|
'.dshwv-menu-sep{height:1px;background:rgba(32,49,112,.25);margin:6px 0}',
|
|
163
|
-
'.dshwv-volpct{width:
|
|
167
|
+
'.dshwv-volpct{width:44px;text-align:right;color:#203170;font-size:12px}'
|
|
164
168
|
].join('\\n')
|
|
165
169
|
|
|
166
170
|
var styleEl = document.createElement('style')
|
|
@@ -212,6 +216,13 @@ scaleNumber.value = '10'
|
|
|
212
216
|
scaleInput.addEventListener('pointerdown', function () { root.style.transition = 'none' })
|
|
213
217
|
scaleInput.addEventListener('input', function () { setScale(scaleInput.value) })
|
|
214
218
|
scaleInput.addEventListener('change', function () { root.style.transition = '' })
|
|
219
|
+
scaleNumber.addEventListener('focus', function () { root.style.transition = 'none' })
|
|
220
|
+
scaleNumber.addEventListener('blur', function () { root.style.transition = '' })
|
|
221
|
+
scaleNumber.addEventListener('input', function () {
|
|
222
|
+
var v = Math.round(Number(scaleNumber.value))
|
|
223
|
+
var s = MIN_SCALE + Math.max(0, Math.min(20, v) - 1) * (MAX_SCALE - MIN_SCALE) / 19
|
|
224
|
+
setScale(s)
|
|
225
|
+
})
|
|
215
226
|
scaleNumber.addEventListener('change', function () {
|
|
216
227
|
var v = Math.round(Number(scaleNumber.value))
|
|
217
228
|
var s = MIN_SCALE + Math.max(0, Math.min(20, v) - 1) * (MAX_SCALE - MIN_SCALE) / 19
|
|
@@ -258,8 +269,26 @@ turnCostCloseInput.min = '0'
|
|
|
258
269
|
turnCostCloseInput.step = '1'
|
|
259
270
|
turnCostCloseInput.className = 'dshwv-number'
|
|
260
271
|
turnCostCloseInput.value = '5'
|
|
272
|
+
turnCostCloseInput.disabled = false // 跟随「每轮消耗提示」开关
|
|
261
273
|
turnCostCloseInput.title = '填 0 表示不自动关闭,需手动点击关闭'
|
|
274
|
+
turnCostCloseInput.addEventListener('input', function () { setTurnCostClose(turnCostCloseInput.value) })
|
|
262
275
|
turnCostCloseInput.addEventListener('change', function () { setTurnCostClose(turnCostCloseInput.value) })
|
|
276
|
+
var scrollGapToggle = document.createElement('input')
|
|
277
|
+
scrollGapToggle.type = 'checkbox'
|
|
278
|
+
scrollGapToggle.className = 'dshwv-check'
|
|
279
|
+
scrollGapToggle.checked = false
|
|
280
|
+
scrollGapToggle.title = '开启后挂件右侧按设定像素避开滚动条;关闭则贴边(盖住滚动条)'
|
|
281
|
+
scrollGapToggle.addEventListener('change', function () { setScrollGapOn(scrollGapToggle.checked) })
|
|
282
|
+
var scrollGapInput = document.createElement('input')
|
|
283
|
+
scrollGapInput.type = 'number'
|
|
284
|
+
scrollGapInput.min = '0'
|
|
285
|
+
scrollGapInput.step = '1'
|
|
286
|
+
scrollGapInput.className = 'dshwv-number'
|
|
287
|
+
scrollGapInput.value = '17'
|
|
288
|
+
scrollGapInput.disabled = true // 默认避让关 → 宽度不可修改,勾选后启用
|
|
289
|
+
scrollGapInput.title = '避让滚动条的像素宽度,填 0 表示贴边'
|
|
290
|
+
scrollGapInput.addEventListener('input', function () { setScrollGapPx(scrollGapInput.value) })
|
|
291
|
+
scrollGapInput.addEventListener('change', function () { setScrollGapPx(scrollGapInput.value) })
|
|
263
292
|
var row1 = menuRow()
|
|
264
293
|
row1.appendChild(menuLabel('大小'))
|
|
265
294
|
row1.appendChild(scaleInput)
|
|
@@ -294,21 +323,26 @@ row6.appendChild(bubbleToggle)
|
|
|
294
323
|
var menuSep1 = document.createElement('div')
|
|
295
324
|
menuSep1.className = 'dshwv-menu-sep'
|
|
296
325
|
var row7 = menuRow()
|
|
297
|
-
row7.appendChild(menuLabel('
|
|
326
|
+
row7.appendChild(menuLabel('每轮消耗提示'))
|
|
298
327
|
row7.appendChild(turnCostToggle)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
328
|
+
row7.appendChild(menuLabel('自动关闭'))
|
|
329
|
+
row7.appendChild(turnCostCloseInput)
|
|
330
|
+
row7.appendChild(menuLabel('秒'))
|
|
331
|
+
var row9 = menuRow()
|
|
332
|
+
row9.appendChild(menuLabel('避让滚动条'))
|
|
333
|
+
row9.appendChild(scrollGapToggle)
|
|
334
|
+
row9.appendChild(menuLabel('宽度'))
|
|
335
|
+
row9.appendChild(scrollGapInput)
|
|
336
|
+
row9.appendChild(menuLabel('px'))
|
|
303
337
|
menuBox.appendChild(row1)
|
|
304
338
|
menuBox.appendChild(row2)
|
|
305
339
|
menuBox.appendChild(row3)
|
|
306
340
|
menuBox.appendChild(row4)
|
|
307
341
|
menuBox.appendChild(row5)
|
|
308
342
|
menuBox.appendChild(row6)
|
|
309
|
-
menuBox.appendChild(menuSep1)
|
|
310
343
|
menuBox.appendChild(row7)
|
|
311
|
-
menuBox.appendChild(
|
|
344
|
+
menuBox.appendChild(menuSep1)
|
|
345
|
+
menuBox.appendChild(row9)
|
|
312
346
|
|
|
313
347
|
var textBox = document.createElement('div')
|
|
314
348
|
textBox.className = 'dshwv-text'
|
|
@@ -351,10 +385,13 @@ bubbleBox.addEventListener('click', function (e) {
|
|
|
351
385
|
// 再次点击:关闭
|
|
352
386
|
hideBubble()
|
|
353
387
|
} else {
|
|
354
|
-
//
|
|
388
|
+
// 首次点击:切到随机台词段,并重置自动关闭计时——
|
|
389
|
+
// 保证第二段台词有完整停留时间(否则第 4 秒点击只看到 0.5 秒)
|
|
355
390
|
bubbleRandomActive = true
|
|
356
391
|
bubbleRandomLines = pickRandomLines()
|
|
357
392
|
swapBubbleContent(function () { applyBubbleLines(bubbleRandomLines) })
|
|
393
|
+
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null }
|
|
394
|
+
bubbleTimer = setTimeout(hideBubble, BUBBLE_MS)
|
|
358
395
|
}
|
|
359
396
|
})
|
|
360
397
|
|
|
@@ -613,9 +650,10 @@ function viewport() {
|
|
|
613
650
|
}
|
|
614
651
|
}
|
|
615
652
|
function rightGap() {
|
|
616
|
-
|
|
617
|
-
if (
|
|
618
|
-
|
|
653
|
+
// 开关关闭:贴边(不避让滚动条)
|
|
654
|
+
if (!scrollGapOn) return 0
|
|
655
|
+
// 开启:用用户填写的像素;填 0 也贴边
|
|
656
|
+
return scrollGapPx > 0 ? scrollGapPx : 0
|
|
619
657
|
}
|
|
620
658
|
function fmt(balance, currency) {
|
|
621
659
|
var num = Number(balance)
|
|
@@ -694,8 +732,7 @@ function settle() {
|
|
|
694
732
|
state.left = state.hOff
|
|
695
733
|
} else {
|
|
696
734
|
state.left = clamp(state.left, 0, Math.max(0, vp.w - w - rightGap()))
|
|
697
|
-
}
|
|
698
|
-
if (state.v === 'bottom') {
|
|
735
|
+
} if (state.v === 'bottom') {
|
|
699
736
|
state.top = Math.max(0, vp.h - h - state.vOff)
|
|
700
737
|
} else if (state.v === 'top') {
|
|
701
738
|
state.top = state.vOff
|
|
@@ -778,9 +815,13 @@ var bubbleOn = true
|
|
|
778
815
|
var turnCostOn = true
|
|
779
816
|
var turnCostCloseMs = 5000
|
|
780
817
|
var costBubbleActive = false
|
|
818
|
+
var scrollGapOn = false
|
|
819
|
+
var scrollGapPx = 17
|
|
781
820
|
function saveConfig() {
|
|
782
821
|
try {
|
|
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 }) })
|
|
822
|
+
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, scrollGapOn: scrollGapOn, scrollGapPx: scrollGapPx }) })
|
|
823
|
+
// 锚点位置记忆:记录相对边框的离边距离,窗口 resize 后保持(localStorage)。
|
|
824
|
+
// v:2 = 净距离格式(剥离避让距离),v:1 旧格式含避让距离,恢复时废弃旧格式。
|
|
784
825
|
var vp = viewport()
|
|
785
826
|
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
786
827
|
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
@@ -788,9 +829,13 @@ function saveConfig() {
|
|
|
788
829
|
var rightDist = vp.w - state.left - w
|
|
789
830
|
var topDist = state.top
|
|
790
831
|
var bottomDist = vp.h - state.top - h
|
|
832
|
+
var hAnchor = leftDist <= rightDist ? 'left' : 'right'
|
|
833
|
+
var hDistRaw = Math.round(Math.min(leftDist, rightDist))
|
|
834
|
+
var hDist = hAnchor === 'right' && scrollGapOn ? Math.max(0, hDistRaw - rightGap()) : hDistRaw
|
|
791
835
|
localStorage.setItem('dshw-pos', JSON.stringify({
|
|
792
|
-
|
|
793
|
-
|
|
836
|
+
v: 2,
|
|
837
|
+
hAnchor: hAnchor,
|
|
838
|
+
hDist: hDist,
|
|
794
839
|
vAnchor: topDist <= bottomDist ? 'top' : 'bottom',
|
|
795
840
|
vDist: Math.round(Math.min(topDist, bottomDist))
|
|
796
841
|
}))
|
|
@@ -817,20 +862,41 @@ function setBubbleOn(v) {
|
|
|
817
862
|
function setTurnCostOn(v) {
|
|
818
863
|
turnCostOn = !!v
|
|
819
864
|
turnCostToggle.checked = turnCostOn
|
|
865
|
+
turnCostCloseInput.disabled = !turnCostOn
|
|
820
866
|
saveConfig()
|
|
821
867
|
if (!turnCostOn) hideCostBubble()
|
|
822
868
|
}
|
|
823
869
|
function setTurnCostClose(v) {
|
|
870
|
+
if (!turnCostOn) return
|
|
824
871
|
var n = Math.max(0, Math.round(Number(v) || 0))
|
|
825
872
|
turnCostCloseMs = n * 1000
|
|
826
873
|
turnCostCloseInput.value = String(n)
|
|
827
874
|
saveConfig()
|
|
828
875
|
}
|
|
876
|
+
function setScrollGapOn(v) {
|
|
877
|
+
scrollGapOn = !!v
|
|
878
|
+
scrollGapToggle.checked = scrollGapOn
|
|
879
|
+
scrollGapInput.disabled = !scrollGapOn
|
|
880
|
+
saveConfig()
|
|
881
|
+
settle()
|
|
882
|
+
}
|
|
883
|
+
function setScrollGapPx(v) {
|
|
884
|
+
if (!scrollGapOn) return
|
|
885
|
+
var n = Math.max(0, Math.round(Number(v) || 0))
|
|
886
|
+
scrollGapPx = n
|
|
887
|
+
scrollGapInput.value = String(n)
|
|
888
|
+
saveConfig()
|
|
889
|
+
settle()
|
|
890
|
+
}
|
|
829
891
|
function scaleToDisplay(s) {
|
|
830
892
|
return Math.round((s - MIN_SCALE) / ((MAX_SCALE - MIN_SCALE) / 19)) + 1
|
|
831
893
|
}
|
|
832
894
|
function setScale(v) {
|
|
833
895
|
var next = Math.round(Math.min(MAX_SCALE, Math.max(MIN_SCALE, Number(v))) * 10) / 10
|
|
896
|
+
// 缩放测量需要 left/top 立即到位:临时禁用过渡(滚轮/数字框路径没有
|
|
897
|
+
// 滑块 pointerdown 的 transition:none,否则 r2 测的是过渡起点导致错锚点)
|
|
898
|
+
var prevTrans = root.style.transition
|
|
899
|
+
root.style.transition = 'none'
|
|
834
900
|
var rect = root.getBoundingClientRect()
|
|
835
901
|
// fixed point: the whale's corner — bottom-right when unflipped, bottom-left
|
|
836
902
|
// when flipped. Growing extends the widget up-left / up-right from that
|
|
@@ -854,6 +920,12 @@ function setScale(v) {
|
|
|
854
920
|
}
|
|
855
921
|
state.top = Math.min(Math.max(fy - r2.height, 0), Math.max(0, vp.h - r2.height))
|
|
856
922
|
express()
|
|
923
|
+
// 恢复过渡必须延迟到下一帧:本帧 left/top 已在 none 下设置并提交,
|
|
924
|
+
// 立即恢复会让浏览器对「刚改过的 left/top」重新评估并播放过渡动画
|
|
925
|
+
// (翻转时叠加 transform .3s 更明显,表现为抽搐)。
|
|
926
|
+
requestAnimationFrame(function () {
|
|
927
|
+
root.style.transition = prevTrans
|
|
928
|
+
})
|
|
857
929
|
}
|
|
858
930
|
function setVol(v) {
|
|
859
931
|
var next = Math.round(Math.min(1, Math.max(0, Number(v))) * 100) / 100
|
|
@@ -1079,7 +1151,6 @@ function onDocPointerDown(e) {
|
|
|
1079
1151
|
document.addEventListener('pointermove', onDocPointerMove, true)
|
|
1080
1152
|
document.addEventListener('pointerup', onDocPointerUp, true)
|
|
1081
1153
|
document.addEventListener('pointercancel', onDocPointerCancel, true)
|
|
1082
|
-
document.addEventListener('click', onDocClickStopper, true)
|
|
1083
1154
|
}
|
|
1084
1155
|
function onDocPointerMove(e) {
|
|
1085
1156
|
if (!drag || !drag.active) return
|
|
@@ -1093,12 +1164,21 @@ function onDocPointerMove(e) {
|
|
|
1093
1164
|
state.top = clamp(drag.origTop + dy, 0, Math.max(0, drag.vp.h - drag.h))
|
|
1094
1165
|
express()
|
|
1095
1166
|
}
|
|
1096
|
-
function onDocPointerUp(e) {
|
|
1167
|
+
function onDocPointerUp(e) {
|
|
1168
|
+
// 拦截鲸鱼区域内的 pointerup:防止下方元素(如文件行)监听 pointerup 穿透误触发
|
|
1169
|
+
try { if (isWhaleHit(e)) { e.preventDefault(); e.stopPropagation() } } catch (err) {}
|
|
1170
|
+
endDrag(e, true)
|
|
1171
|
+
}
|
|
1097
1172
|
function onDocPointerCancel(e) { endDrag(e, false) }
|
|
1098
1173
|
function onDocClickStopper(e) {
|
|
1174
|
+
// 只在鲸鱼命中区域拦截 click(保持透明区 pass-through)。
|
|
1175
|
+
// 持久注册(不随 endDrag 移除)——click 在 pointerup 之后派发,
|
|
1176
|
+
// 若在 endDrag 移除会导致 click 穿透到下方元素(如误打开文件)。
|
|
1177
|
+
if (!isWhaleHit(e)) return
|
|
1099
1178
|
try { e.preventDefault(); e.stopPropagation() } catch (err) {}
|
|
1100
1179
|
}
|
|
1101
1180
|
document.addEventListener('pointerdown', onDocPointerDown, true)
|
|
1181
|
+
document.addEventListener('click', onDocClickStopper, true)
|
|
1102
1182
|
|
|
1103
1183
|
var widgetCursor = ''
|
|
1104
1184
|
function setWidgetCursor(v) {
|
|
@@ -1128,7 +1208,6 @@ function endDrag(e, clickAllowed) {
|
|
|
1128
1208
|
document.removeEventListener('pointermove', onDocPointerMove, true)
|
|
1129
1209
|
document.removeEventListener('pointerup', onDocPointerUp, true)
|
|
1130
1210
|
document.removeEventListener('pointercancel', onDocPointerCancel, true)
|
|
1131
|
-
document.removeEventListener('click', onDocClickStopper, true)
|
|
1132
1211
|
pressUp()
|
|
1133
1212
|
root.classList.remove('dshwv-dragging')
|
|
1134
1213
|
setWidgetCursor(isWhaleHit(e) ? 'grab' : '')
|
|
@@ -1162,23 +1241,29 @@ function endDrag(e, clickAllowed) {
|
|
|
1162
1241
|
state.left = left
|
|
1163
1242
|
state.top = top
|
|
1164
1243
|
settle()
|
|
1244
|
+
// 拖拽结束立即保存锚点位置(否则刷新/关闭后位置回退到上次改菜单时)
|
|
1245
|
+
saveConfig()
|
|
1165
1246
|
}
|
|
1166
1247
|
// 窗口尺寸变化时:自由位置的鲸鱼按相对边框锚点重算(保持离边距离,窗口恢复原状即回原位);
|
|
1167
1248
|
// 贴边吸附的鲸鱼走 settle()(保持贴边)
|
|
1168
1249
|
function applyAnchorPos() {
|
|
1169
1250
|
try {
|
|
1170
1251
|
var a = JSON.parse(localStorage.getItem('dshw-pos') || 'null')
|
|
1171
|
-
if (!a || (a.hAnchor !== 'left' && a.hAnchor !== 'right') || typeof a.hDist !== 'number' ||
|
|
1252
|
+
if (!a || a.v !== 2 || (a.hAnchor !== 'left' && a.hAnchor !== 'right') || typeof a.hDist !== 'number' ||
|
|
1172
1253
|
(a.vAnchor !== 'top' && a.vAnchor !== 'bottom') || typeof a.vDist !== 'number') return false
|
|
1173
1254
|
var vp = viewport()
|
|
1174
1255
|
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
1175
1256
|
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
1176
|
-
|
|
1257
|
+
// 与加载恢复一致:锚点存净距离,右锚点按当前避让开关叠加
|
|
1258
|
+
var effectiveRightDist = a.hAnchor === 'right' ? a.hDist + (scrollGapOn ? rightGap() : 0) : a.hDist
|
|
1259
|
+
var l = a.hAnchor === 'left' ? a.hDist : vp.w - effectiveRightDist - w
|
|
1177
1260
|
var t = a.vAnchor === 'top' ? a.vDist : vp.h - a.vDist - h
|
|
1178
1261
|
state.left = clamp(l, 0, Math.max(0, vp.w - w))
|
|
1179
1262
|
state.top = clamp(t, 0, Math.max(0, vp.h - h))
|
|
1180
|
-
state.h =
|
|
1181
|
-
state.
|
|
1263
|
+
state.h = a.hAnchor
|
|
1264
|
+
state.hOff = 0
|
|
1265
|
+
state.v = a.vAnchor
|
|
1266
|
+
state.vOff = 0
|
|
1182
1267
|
express()
|
|
1183
1268
|
return true
|
|
1184
1269
|
} catch (err) { return false }
|
|
@@ -1235,25 +1320,43 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
1235
1320
|
if (d && typeof d.turnCostOn === 'boolean') {
|
|
1236
1321
|
turnCostOn = d.turnCostOn
|
|
1237
1322
|
turnCostToggle.checked = turnCostOn
|
|
1323
|
+
turnCostCloseInput.disabled = !turnCostOn
|
|
1238
1324
|
}
|
|
1239
1325
|
if (d && typeof d.turnCostCloseMs === 'number') {
|
|
1240
1326
|
turnCostCloseMs = d.turnCostCloseMs > 0 ? d.turnCostCloseMs : 0
|
|
1241
1327
|
turnCostCloseInput.value = String(Math.round(turnCostCloseMs / 1000))
|
|
1242
1328
|
}
|
|
1243
|
-
|
|
1329
|
+
if (d && typeof d.scrollGapOn === 'boolean') {
|
|
1330
|
+
scrollGapOn = d.scrollGapOn
|
|
1331
|
+
scrollGapToggle.checked = scrollGapOn
|
|
1332
|
+
scrollGapInput.disabled = !scrollGapOn
|
|
1333
|
+
}
|
|
1334
|
+
if (d && typeof d.scrollGapPx === 'number') {
|
|
1335
|
+
scrollGapPx = d.scrollGapPx > 0 ? Math.round(d.scrollGapPx) : 0
|
|
1336
|
+
scrollGapInput.value = String(scrollGapPx)
|
|
1337
|
+
}
|
|
1338
|
+
// 相对边框恢复(localStorage 锚点):窗口变化后保持离边距离。
|
|
1339
|
+
// 仅认 v:2 净距离格式;旧格式(含避让距离)废弃,挂件保持默认右下角吸附。
|
|
1340
|
+
// 恢复时还原吸附状态(hAnchor/vAnchor → state.h/v),避免挂件变自由位置
|
|
1341
|
+
// 导致避让调节不实时(settle 自由分支只 clamp 不重算位置)。
|
|
1244
1342
|
try {
|
|
1245
1343
|
var a = JSON.parse(localStorage.getItem('dshw-pos') || 'null')
|
|
1246
|
-
if (a && (a.hAnchor === 'left' || a.hAnchor === 'right') && typeof a.hDist === 'number' &&
|
|
1344
|
+
if (a && a.v === 2 && (a.hAnchor === 'left' || a.hAnchor === 'right') && typeof a.hDist === 'number' &&
|
|
1247
1345
|
(a.vAnchor === 'top' || a.vAnchor === 'bottom') && typeof a.vDist === 'number') {
|
|
1248
1346
|
var vpA = viewport()
|
|
1249
1347
|
var wA = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
1250
1348
|
var hA = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
1251
|
-
|
|
1349
|
+
// 锚点存的是净距离:右锚点按当前避让开关叠加避让距离
|
|
1350
|
+
var effectiveRightDist = a.hAnchor === 'right' ? a.hDist + (scrollGapOn ? rightGap() : 0) : a.hDist
|
|
1351
|
+
var lA = a.hAnchor === 'left' ? a.hDist : vpA.w - effectiveRightDist - wA
|
|
1252
1352
|
var tA = a.vAnchor === 'top' ? a.vDist : vpA.h - a.vDist - hA
|
|
1253
1353
|
state.left = clamp(lA, 0, Math.max(0, vpA.w - wA))
|
|
1254
1354
|
state.top = clamp(tA, 0, Math.max(0, vpA.h - hA))
|
|
1255
|
-
|
|
1256
|
-
state.
|
|
1355
|
+
// 按锚点还原吸附状态(贴边锚点 → 吸附;自由位锚点 → 自由)
|
|
1356
|
+
state.h = a.hAnchor
|
|
1357
|
+
state.hOff = 0
|
|
1358
|
+
state.v = a.vAnchor
|
|
1359
|
+
state.vOff = 0
|
|
1257
1360
|
settle()
|
|
1258
1361
|
}
|
|
1259
1362
|
} catch (err) {}
|
|
@@ -1300,59 +1403,64 @@ function apply(ctx) {
|
|
|
1300
1403
|
let balanceCache = null
|
|
1301
1404
|
let balanceInFlight = null
|
|
1302
1405
|
let gifBytes = null
|
|
1303
|
-
//
|
|
1304
|
-
|
|
1406
|
+
// 每轮对话消耗统计:按 (session.id, turn) 分桶聚合,完成后写入 lastTurn。
|
|
1407
|
+
// 用 Map 分桶避免主会话与子代理(spawn/fork)并行时串账。
|
|
1408
|
+
let turnAggs = new Map() // sessionId -> { turn, cost, tokens, lastTs }
|
|
1305
1409
|
let lastTurn = null // { turn, amount, tokens, ts }
|
|
1306
1410
|
let lastTurnSeq = 0
|
|
1307
1411
|
const disposers = []
|
|
1308
1412
|
|
|
1309
|
-
function
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
if (turnAgg && turnAgg.cost > 0) {
|
|
1314
|
-
lastTurn = { turn: turnAgg.turn, amount: turnAgg.cost, tokens: turnAgg.tokens, ts: turnAgg.lastTs }
|
|
1413
|
+
function finalizeTurn(sessionId) {
|
|
1414
|
+
const agg = turnAggs.get(sessionId)
|
|
1415
|
+
if (agg && agg.cost > 0) {
|
|
1416
|
+
lastTurn = { turn: agg.turn, amount: agg.cost, tokens: agg.tokens, ts: agg.lastTs }
|
|
1315
1417
|
lastTurnSeq++
|
|
1316
1418
|
}
|
|
1317
|
-
|
|
1419
|
+
turnAggs.delete(sessionId)
|
|
1318
1420
|
}
|
|
1319
|
-
// 监听会话事件流:assistant/message 携带每步真实 usage,按 turn 聚合;
|
|
1320
|
-
// turn/end
|
|
1321
|
-
function handleSessionEvent(event) {
|
|
1421
|
+
// 监听会话事件流:assistant/message 携带每步真实 usage,按 (session,turn) 聚合;
|
|
1422
|
+
// turn/end 时结算该会话本轮并写入 lastTurn
|
|
1423
|
+
function handleSessionEvent(sessionId, event) {
|
|
1322
1424
|
try {
|
|
1323
1425
|
const type = event && event.type
|
|
1324
1426
|
const d = event && event.data
|
|
1325
1427
|
if (!d || typeof d !== 'object') return
|
|
1326
1428
|
if (type === 'turn/end') {
|
|
1327
|
-
|
|
1328
|
-
finalizeTurn()
|
|
1429
|
+
finalizeTurn(sessionId)
|
|
1329
1430
|
return
|
|
1330
1431
|
}
|
|
1331
1432
|
if (type !== 'assistant/message') return
|
|
1332
1433
|
const turn = Number(d.turn)
|
|
1333
1434
|
const usage = d.usage
|
|
1334
1435
|
if (!usage || typeof usage !== 'object' || !isFinite(turn)) return
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
if (
|
|
1436
|
+
let agg = turnAggs.get(sessionId)
|
|
1437
|
+
if (!agg || agg.turn !== turn) {
|
|
1438
|
+
if (agg) finalizeTurn(sessionId)
|
|
1439
|
+
agg = { turn, cost: 0, tokens: 0, lastTs: Date.now() }
|
|
1440
|
+
turnAggs.set(sessionId, agg)
|
|
1338
1441
|
}
|
|
1339
1442
|
const input = Number(usage.inputTokens) || 0
|
|
1340
1443
|
const cache = Number(usage.cacheReadTokens) || 0
|
|
1341
1444
|
const output = Number(usage.outputTokens) || 0
|
|
1342
1445
|
const reasoning = Number(usage.reasoningTokens) || 0
|
|
1343
|
-
|
|
1446
|
+
agg.tokens += input + cache + output + reasoning
|
|
1344
1447
|
// 定价换算(CNY/百万 token;缓存命中=输入价,其余按各自档位)
|
|
1345
1448
|
const model = d.message && d.message.source ? d.message.source.model : ''
|
|
1346
1449
|
const p = priceFor(model)
|
|
1347
1450
|
const off = isPeakTime(Math.floor(Date.now() / 1000)) ? 1 : 0
|
|
1348
|
-
|
|
1349
|
-
|
|
1451
|
+
agg.cost += (cache / 1e6) * p.hit[off] + (input / 1e6) * p.miss[off] + ((output + reasoning) / 1e6) * p.out[off]
|
|
1452
|
+
agg.lastTs = Date.now()
|
|
1350
1453
|
} catch (err) {}
|
|
1351
1454
|
}
|
|
1352
1455
|
|
|
1353
|
-
//
|
|
1456
|
+
// 监听所有会话的追加事件;按会话 id 分桶,turn/end 时结算该会话本轮
|
|
1354
1457
|
disposers.push(ctx.on('session/event', (session, event) => {
|
|
1355
|
-
|
|
1458
|
+
const sid = session && session.id ? session.id : 'default'
|
|
1459
|
+
handleSessionEvent(sid, event)
|
|
1460
|
+
}))
|
|
1461
|
+
// 会话销毁时清理残留聚合,避免内存泄漏
|
|
1462
|
+
disposers.push(ctx.on('session/disposed', (session) => {
|
|
1463
|
+
if (session && session.id) turnAggs.delete(session.id)
|
|
1356
1464
|
}))
|
|
1357
1465
|
|
|
1358
1466
|
function loadGif() {
|
|
@@ -1640,6 +1748,8 @@ function apply(ctx) {
|
|
|
1640
1748
|
bubbleOn: parsed.bubbleOn !== false,
|
|
1641
1749
|
turnCostOn: parsed.turnCostOn !== false,
|
|
1642
1750
|
turnCostCloseMs: typeof parsed.turnCostCloseMs === 'number' ? parsed.turnCostCloseMs : 5000,
|
|
1751
|
+
scrollGapOn: parsed.scrollGapOn === true,
|
|
1752
|
+
scrollGapPx: typeof parsed.scrollGapPx === 'number' ? Math.round(parsed.scrollGapPx) : 17,
|
|
1643
1753
|
}
|
|
1644
1754
|
}
|
|
1645
1755
|
} catch (err) {}
|
|
@@ -1647,13 +1757,14 @@ function apply(ctx) {
|
|
|
1647
1757
|
return null
|
|
1648
1758
|
}
|
|
1649
1759
|
|
|
1650
|
-
function writeSizeConfig(scale, sound, vol, soundSet, usageMode, peakMode, bubbleOn, turnCostOn, turnCostCloseMs) {
|
|
1760
|
+
function writeSizeConfig(scale, sound, vol, soundSet, usageMode, peakMode, bubbleOn, turnCostOn, turnCostCloseMs, scrollGapOn, scrollGapPx) {
|
|
1651
1761
|
const um = normalizeUsageMode(usageMode)
|
|
1652
1762
|
const pm = peakMode === 'liangwen' || peakMode === 'qiangqiang' ? peakMode : 'default'
|
|
1653
1763
|
const bo = bubbleOn !== false
|
|
1654
1764
|
const tco = turnCostOn !== false
|
|
1655
|
-
|
|
1656
|
-
const
|
|
1765
|
+
const tcc = typeof turnCostCloseMs === 'number' ? (turnCostCloseMs > 0 ? turnCostCloseMs : 0) : 5000
|
|
1766
|
+
const sgo = scrollGapOn === true
|
|
1767
|
+
const sgp = typeof scrollGapPx === 'number' && scrollGapPx > 0 ? Math.round(scrollGapPx) : 0
|
|
1657
1768
|
const body = JSON.stringify({
|
|
1658
1769
|
scale: scale,
|
|
1659
1770
|
sound: sound !== false,
|
|
@@ -1664,6 +1775,8 @@ function apply(ctx) {
|
|
|
1664
1775
|
bubbleOn: bo,
|
|
1665
1776
|
turnCostOn: tco,
|
|
1666
1777
|
turnCostCloseMs: tcc,
|
|
1778
|
+
scrollGapOn: sgo,
|
|
1779
|
+
scrollGapPx: sgp,
|
|
1667
1780
|
updatedAt: new Date().toISOString(),
|
|
1668
1781
|
})
|
|
1669
1782
|
for (const p of SIZE_FILE_CANDIDATES) {
|
|
@@ -1680,6 +1793,8 @@ function apply(ctx) {
|
|
|
1680
1793
|
bubbleOn: bo,
|
|
1681
1794
|
turnCostOn: tco,
|
|
1682
1795
|
turnCostCloseMs: tcc,
|
|
1796
|
+
scrollGapOn: sgo,
|
|
1797
|
+
scrollGapPx: sgp,
|
|
1683
1798
|
}
|
|
1684
1799
|
} catch (err) {}
|
|
1685
1800
|
}
|
|
@@ -1791,7 +1906,7 @@ function apply(ctx) {
|
|
|
1791
1906
|
balanceCache = null
|
|
1792
1907
|
}
|
|
1793
1908
|
}
|
|
1794
|
-
const result = writeSizeConfig(scale, parsed.sound !== false, parsed.vol, parsed.soundSet, parsed.usageMode, parsed.peakMode, parsed.bubbleOn, parsed.turnCostOn, parsed.turnCostCloseMs)
|
|
1909
|
+
const result = writeSizeConfig(scale, parsed.sound !== false, parsed.vol, parsed.soundSet, parsed.usageMode, parsed.peakMode, parsed.bubbleOn, parsed.turnCostOn, parsed.turnCostCloseMs, parsed.scrollGapOn, parsed.scrollGapPx)
|
|
1795
1910
|
res.writeHead(result.ok ? 200 : 500, JSON_HEADERS)
|
|
1796
1911
|
res.end(JSON.stringify(result))
|
|
1797
1912
|
} catch (err) {
|