dsh-whale-widget 0.2.5 → 0.2.7
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 +35 -15
- package/lib/index.js +245 -62
- package/package.json +1 -1
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
|
@@ -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
|
### 关于令牌(安装后必读)
|
|
@@ -192,3 +208,7 @@ curl http://127.0.0.1:3080/dsh-whale/last-turn.json
|
|
|
192
208
|
## 开发与维护
|
|
193
209
|
|
|
194
210
|
完整规格、视觉参数、架构结论和生成提示词见 `whale-widget-prompt.md`。修改文字位置、颜色、动画、吸附逻辑、台词组或定价表时参考该文件。
|
|
211
|
+
|
|
212
|
+
## 许可证
|
|
213
|
+
|
|
214
|
+
本项目基于 **MIT License** 开源,详见 [LICENSE](LICENSE)。
|
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'
|
|
@@ -336,6 +370,8 @@ gifEl.src = GIF_URL
|
|
|
336
370
|
gifEl.alt = ''
|
|
337
371
|
gifEl.draggable = false
|
|
338
372
|
bubbleBox.appendChild(gifEl)
|
|
373
|
+
var gifFailed = false
|
|
374
|
+
gifEl.onerror = function () { gifFailed = true }
|
|
339
375
|
bubbleBox.appendChild(textBox)
|
|
340
376
|
bubbleBox.addEventListener('click', function (e) {
|
|
341
377
|
e.stopPropagation()
|
|
@@ -436,13 +472,18 @@ function pickRandomLines() {
|
|
|
436
472
|
function applyBubbleLines(lines) {
|
|
437
473
|
if (lines && lines.gif) {
|
|
438
474
|
// gif 台词组:只显示 gif,隐藏三行文字(display 必须显式覆盖 CSS 的 none)
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
475
|
+
if (gifFailed) {
|
|
476
|
+
// gif 加载失败/路由缺失:降级为文字台词,避免空白白色气泡
|
|
477
|
+
lines = singleCenter('A', pickOne(['gif 加载失败了...', '今天没有动图给你看~', '呜呜 动图不见了...']), '', true)
|
|
478
|
+
} else {
|
|
479
|
+
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
480
|
+
gifEl.style.display = 'block'
|
|
481
|
+
gifEl.style.opacity = ''
|
|
482
|
+
labelEl.style.display = 'none'
|
|
483
|
+
amountEl.style.display = 'none'
|
|
484
|
+
hintEl.style.display = 'none'
|
|
485
|
+
return
|
|
486
|
+
}
|
|
446
487
|
}
|
|
447
488
|
if (gifFadeTimer) { clearTimeout(gifFadeTimer); gifFadeTimer = null }
|
|
448
489
|
gifEl.style.display = 'none'
|
|
@@ -605,6 +646,12 @@ function viewport() {
|
|
|
605
646
|
h: window.innerHeight || document.documentElement.clientHeight || 800
|
|
606
647
|
}
|
|
607
648
|
}
|
|
649
|
+
function rightGap() {
|
|
650
|
+
// 开关关闭:贴边(不避让滚动条)
|
|
651
|
+
if (!scrollGapOn) return 0
|
|
652
|
+
// 开启:用用户填写的像素;填 0 也贴边
|
|
653
|
+
return scrollGapPx > 0 ? scrollGapPx : 0
|
|
654
|
+
}
|
|
608
655
|
function fmt(balance, currency) {
|
|
609
656
|
var num = Number(balance)
|
|
610
657
|
var fixed = isFinite(num) ? num.toFixed(2) : '--'
|
|
@@ -671,19 +718,18 @@ function settle() {
|
|
|
671
718
|
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
672
719
|
if (drag && drag.active) {
|
|
673
720
|
// mid-drag resize: keep the pointer-follow position, just clamp into view
|
|
674
|
-
state.left = clamp(state.left, 0, Math.max(0, vp.w - w))
|
|
721
|
+
state.left = clamp(state.left, 0, Math.max(0, vp.w - w - rightGap()))
|
|
675
722
|
state.top = clamp(state.top, 0, Math.max(0, vp.h - h))
|
|
676
723
|
express()
|
|
677
724
|
return
|
|
678
725
|
}
|
|
679
726
|
if (state.h === 'right') {
|
|
680
|
-
state.left = Math.max(0, vp.w - w - state.hOff)
|
|
727
|
+
state.left = Math.max(0, vp.w - w - state.hOff - rightGap())
|
|
681
728
|
} else if (state.h === 'left') {
|
|
682
729
|
state.left = state.hOff
|
|
683
730
|
} else {
|
|
684
|
-
state.left = clamp(state.left, 0, Math.max(0, vp.w - w))
|
|
685
|
-
}
|
|
686
|
-
if (state.v === 'bottom') {
|
|
731
|
+
state.left = clamp(state.left, 0, Math.max(0, vp.w - w - rightGap()))
|
|
732
|
+
} if (state.v === 'bottom') {
|
|
687
733
|
state.top = Math.max(0, vp.h - h - state.vOff)
|
|
688
734
|
} else if (state.v === 'top') {
|
|
689
735
|
state.top = state.vOff
|
|
@@ -765,11 +811,31 @@ var peakMode = 'default'
|
|
|
765
811
|
var bubbleOn = true
|
|
766
812
|
var turnCostOn = true
|
|
767
813
|
var turnCostCloseMs = 5000
|
|
768
|
-
var lastCostSeq = 0
|
|
769
814
|
var costBubbleActive = false
|
|
815
|
+
var scrollGapOn = false
|
|
816
|
+
var scrollGapPx = 17
|
|
770
817
|
function saveConfig() {
|
|
771
818
|
try {
|
|
772
|
-
fetch(SIZE_URL, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scale: state.scale, sound: soundOn, vol: soundVol, soundSet: soundSet, usageMode: usageMode, peakMode: peakMode, bubbleOn: bubbleOn, turnCostOn: turnCostOn, turnCostCloseMs: turnCostCloseMs }) })
|
|
819
|
+
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 }) })
|
|
820
|
+
// 锚点位置记忆:记录相对边框的离边距离,窗口 resize 后保持(localStorage)。
|
|
821
|
+
// v:2 = 净距离格式(剥离避让距离),v:1 旧格式含避让距离,恢复时废弃旧格式。
|
|
822
|
+
var vp = viewport()
|
|
823
|
+
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
824
|
+
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
825
|
+
var leftDist = state.left
|
|
826
|
+
var rightDist = vp.w - state.left - w
|
|
827
|
+
var topDist = state.top
|
|
828
|
+
var bottomDist = vp.h - state.top - h
|
|
829
|
+
var hAnchor = leftDist <= rightDist ? 'left' : 'right'
|
|
830
|
+
var hDistRaw = Math.round(Math.min(leftDist, rightDist))
|
|
831
|
+
var hDist = hAnchor === 'right' && scrollGapOn ? Math.max(0, hDistRaw - rightGap()) : hDistRaw
|
|
832
|
+
localStorage.setItem('dshw-pos', JSON.stringify({
|
|
833
|
+
v: 2,
|
|
834
|
+
hAnchor: hAnchor,
|
|
835
|
+
hDist: hDist,
|
|
836
|
+
vAnchor: topDist <= bottomDist ? 'top' : 'bottom',
|
|
837
|
+
vDist: Math.round(Math.min(topDist, bottomDist))
|
|
838
|
+
}))
|
|
773
839
|
} catch (err) {}
|
|
774
840
|
}
|
|
775
841
|
function setUsageMode(v) {
|
|
@@ -787,25 +853,47 @@ function setBubbleOn(v) {
|
|
|
787
853
|
bubbleOn = !!v
|
|
788
854
|
bubbleToggle.checked = bubbleOn
|
|
789
855
|
saveConfig()
|
|
790
|
-
|
|
856
|
+
// 必须走 hideCostBubble:残留的 costBubbleActive 会让 render()/showBubble() 永久早退
|
|
857
|
+
if (!bubbleOn) hideCostBubble()
|
|
791
858
|
}
|
|
792
859
|
function setTurnCostOn(v) {
|
|
793
860
|
turnCostOn = !!v
|
|
794
861
|
turnCostToggle.checked = turnCostOn
|
|
862
|
+
turnCostCloseInput.disabled = !turnCostOn
|
|
795
863
|
saveConfig()
|
|
796
864
|
if (!turnCostOn) hideCostBubble()
|
|
797
865
|
}
|
|
798
866
|
function setTurnCostClose(v) {
|
|
867
|
+
if (!turnCostOn) return
|
|
799
868
|
var n = Math.max(0, Math.round(Number(v) || 0))
|
|
800
869
|
turnCostCloseMs = n * 1000
|
|
801
870
|
turnCostCloseInput.value = String(n)
|
|
802
871
|
saveConfig()
|
|
803
872
|
}
|
|
873
|
+
function setScrollGapOn(v) {
|
|
874
|
+
scrollGapOn = !!v
|
|
875
|
+
scrollGapToggle.checked = scrollGapOn
|
|
876
|
+
scrollGapInput.disabled = !scrollGapOn
|
|
877
|
+
saveConfig()
|
|
878
|
+
settle()
|
|
879
|
+
}
|
|
880
|
+
function setScrollGapPx(v) {
|
|
881
|
+
if (!scrollGapOn) return
|
|
882
|
+
var n = Math.max(0, Math.round(Number(v) || 0))
|
|
883
|
+
scrollGapPx = n
|
|
884
|
+
scrollGapInput.value = String(n)
|
|
885
|
+
saveConfig()
|
|
886
|
+
settle()
|
|
887
|
+
}
|
|
804
888
|
function scaleToDisplay(s) {
|
|
805
889
|
return Math.round((s - MIN_SCALE) / ((MAX_SCALE - MIN_SCALE) / 19)) + 1
|
|
806
890
|
}
|
|
807
891
|
function setScale(v) {
|
|
808
892
|
var next = Math.round(Math.min(MAX_SCALE, Math.max(MIN_SCALE, Number(v))) * 10) / 10
|
|
893
|
+
// 缩放测量需要 left/top 立即到位:临时禁用过渡(滚轮/数字框路径没有
|
|
894
|
+
// 滑块 pointerdown 的 transition:none,否则 r2 测的是过渡起点导致错锚点)
|
|
895
|
+
var prevTrans = root.style.transition
|
|
896
|
+
root.style.transition = 'none'
|
|
809
897
|
var rect = root.getBoundingClientRect()
|
|
810
898
|
// fixed point: the whale's corner — bottom-right when unflipped, bottom-left
|
|
811
899
|
// when flipped. Growing extends the widget up-left / up-right from that
|
|
@@ -829,6 +917,12 @@ function setScale(v) {
|
|
|
829
917
|
}
|
|
830
918
|
state.top = Math.min(Math.max(fy - r2.height, 0), Math.max(0, vp.h - r2.height))
|
|
831
919
|
express()
|
|
920
|
+
// 恢复过渡必须延迟到下一帧:本帧 left/top 已在 none 下设置并提交,
|
|
921
|
+
// 立即恢复会让浏览器对「刚改过的 left/top」重新评估并播放过渡动画
|
|
922
|
+
// (翻转时叠加 transform .3s 更明显,表现为抽搐)。
|
|
923
|
+
requestAnimationFrame(function () {
|
|
924
|
+
root.style.transition = prevTrans
|
|
925
|
+
})
|
|
832
926
|
}
|
|
833
927
|
function setVol(v) {
|
|
834
928
|
var next = Math.round(Math.min(1, Math.max(0, Number(v))) * 100) / 100
|
|
@@ -955,7 +1049,7 @@ function snapCheck() {
|
|
|
955
1049
|
} else if (centerX > vp.w * 3 / 4) {
|
|
956
1050
|
state.h = 'right'
|
|
957
1051
|
state.hOff = 0
|
|
958
|
-
left = vp.w - w
|
|
1052
|
+
left = vp.w - w - rightGap()
|
|
959
1053
|
moved = true
|
|
960
1054
|
} else {
|
|
961
1055
|
state.h = null
|
|
@@ -1009,7 +1103,9 @@ function setupHitTest() {
|
|
|
1009
1103
|
var probe = new Image()
|
|
1010
1104
|
probe.onload = function () {
|
|
1011
1105
|
try {
|
|
1012
|
-
|
|
1106
|
+
// 拉伸到 610×610 与 isWhaleHit 的坐标映射对齐;不指定尺寸会按原图大小绘制,
|
|
1107
|
+
// 回退到非 610×610 素材(如 DSniang02.png)时命中区域会错位
|
|
1108
|
+
hitCanvas.getContext('2d').drawImage(probe, 0, 0, 610, 610)
|
|
1013
1109
|
hitReady = true
|
|
1014
1110
|
} catch (err) {}
|
|
1015
1111
|
}
|
|
@@ -1052,7 +1148,6 @@ function onDocPointerDown(e) {
|
|
|
1052
1148
|
document.addEventListener('pointermove', onDocPointerMove, true)
|
|
1053
1149
|
document.addEventListener('pointerup', onDocPointerUp, true)
|
|
1054
1150
|
document.addEventListener('pointercancel', onDocPointerCancel, true)
|
|
1055
|
-
document.addEventListener('click', onDocClickStopper, true)
|
|
1056
1151
|
}
|
|
1057
1152
|
function onDocPointerMove(e) {
|
|
1058
1153
|
if (!drag || !drag.active) return
|
|
@@ -1066,12 +1161,21 @@ function onDocPointerMove(e) {
|
|
|
1066
1161
|
state.top = clamp(drag.origTop + dy, 0, Math.max(0, drag.vp.h - drag.h))
|
|
1067
1162
|
express()
|
|
1068
1163
|
}
|
|
1069
|
-
function onDocPointerUp(e) {
|
|
1164
|
+
function onDocPointerUp(e) {
|
|
1165
|
+
// 拦截鲸鱼区域内的 pointerup:防止下方元素(如文件行)监听 pointerup 穿透误触发
|
|
1166
|
+
try { if (isWhaleHit(e)) { e.preventDefault(); e.stopPropagation() } } catch (err) {}
|
|
1167
|
+
endDrag(e, true)
|
|
1168
|
+
}
|
|
1070
1169
|
function onDocPointerCancel(e) { endDrag(e, false) }
|
|
1071
1170
|
function onDocClickStopper(e) {
|
|
1171
|
+
// 只在鲸鱼命中区域拦截 click(保持透明区 pass-through)。
|
|
1172
|
+
// 持久注册(不随 endDrag 移除)——click 在 pointerup 之后派发,
|
|
1173
|
+
// 若在 endDrag 移除会导致 click 穿透到下方元素(如误打开文件)。
|
|
1174
|
+
if (!isWhaleHit(e)) return
|
|
1072
1175
|
try { e.preventDefault(); e.stopPropagation() } catch (err) {}
|
|
1073
1176
|
}
|
|
1074
1177
|
document.addEventListener('pointerdown', onDocPointerDown, true)
|
|
1178
|
+
document.addEventListener('click', onDocClickStopper, true)
|
|
1075
1179
|
|
|
1076
1180
|
var widgetCursor = ''
|
|
1077
1181
|
function setWidgetCursor(v) {
|
|
@@ -1101,7 +1205,6 @@ function endDrag(e, clickAllowed) {
|
|
|
1101
1205
|
document.removeEventListener('pointermove', onDocPointerMove, true)
|
|
1102
1206
|
document.removeEventListener('pointerup', onDocPointerUp, true)
|
|
1103
1207
|
document.removeEventListener('pointercancel', onDocPointerCancel, true)
|
|
1104
|
-
document.removeEventListener('click', onDocClickStopper, true)
|
|
1105
1208
|
pressUp()
|
|
1106
1209
|
root.classList.remove('dshwv-dragging')
|
|
1107
1210
|
setWidgetCursor(isWhaleHit(e) ? 'grab' : '')
|
|
@@ -1136,7 +1239,32 @@ function endDrag(e, clickAllowed) {
|
|
|
1136
1239
|
state.top = top
|
|
1137
1240
|
settle()
|
|
1138
1241
|
}
|
|
1242
|
+
// 窗口尺寸变化时:自由位置的鲸鱼按相对边框锚点重算(保持离边距离,窗口恢复原状即回原位);
|
|
1243
|
+
// 贴边吸附的鲸鱼走 settle()(保持贴边)
|
|
1244
|
+
function applyAnchorPos() {
|
|
1245
|
+
try {
|
|
1246
|
+
var a = JSON.parse(localStorage.getItem('dshw-pos') || 'null')
|
|
1247
|
+
if (!a || a.v !== 2 || (a.hAnchor !== 'left' && a.hAnchor !== 'right') || typeof a.hDist !== 'number' ||
|
|
1248
|
+
(a.vAnchor !== 'top' && a.vAnchor !== 'bottom') || typeof a.vDist !== 'number') return false
|
|
1249
|
+
var vp = viewport()
|
|
1250
|
+
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
1251
|
+
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
1252
|
+
// 与加载恢复一致:锚点存净距离,右锚点按当前避让开关叠加
|
|
1253
|
+
var effectiveRightDist = a.hAnchor === 'right' ? a.hDist + (scrollGapOn ? rightGap() : 0) : a.hDist
|
|
1254
|
+
var l = a.hAnchor === 'left' ? a.hDist : vp.w - effectiveRightDist - w
|
|
1255
|
+
var t = a.vAnchor === 'top' ? a.vDist : vp.h - a.vDist - h
|
|
1256
|
+
state.left = clamp(l, 0, Math.max(0, vp.w - w))
|
|
1257
|
+
state.top = clamp(t, 0, Math.max(0, vp.h - h))
|
|
1258
|
+
state.h = a.hAnchor
|
|
1259
|
+
state.hOff = 0
|
|
1260
|
+
state.v = a.vAnchor
|
|
1261
|
+
state.vOff = 0
|
|
1262
|
+
express()
|
|
1263
|
+
return true
|
|
1264
|
+
} catch (err) { return false }
|
|
1265
|
+
}
|
|
1139
1266
|
window.addEventListener('resize', function () {
|
|
1267
|
+
if (state.h === null && state.v === null && applyAnchorPos()) return
|
|
1140
1268
|
settle()
|
|
1141
1269
|
})
|
|
1142
1270
|
|
|
@@ -1187,11 +1315,46 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
1187
1315
|
if (d && typeof d.turnCostOn === 'boolean') {
|
|
1188
1316
|
turnCostOn = d.turnCostOn
|
|
1189
1317
|
turnCostToggle.checked = turnCostOn
|
|
1318
|
+
turnCostCloseInput.disabled = !turnCostOn
|
|
1190
1319
|
}
|
|
1191
1320
|
if (d && typeof d.turnCostCloseMs === 'number') {
|
|
1192
1321
|
turnCostCloseMs = d.turnCostCloseMs > 0 ? d.turnCostCloseMs : 0
|
|
1193
1322
|
turnCostCloseInput.value = String(Math.round(turnCostCloseMs / 1000))
|
|
1194
1323
|
}
|
|
1324
|
+
if (d && typeof d.scrollGapOn === 'boolean') {
|
|
1325
|
+
scrollGapOn = d.scrollGapOn
|
|
1326
|
+
scrollGapToggle.checked = scrollGapOn
|
|
1327
|
+
scrollGapInput.disabled = !scrollGapOn
|
|
1328
|
+
}
|
|
1329
|
+
if (d && typeof d.scrollGapPx === 'number') {
|
|
1330
|
+
scrollGapPx = d.scrollGapPx > 0 ? Math.round(d.scrollGapPx) : 0
|
|
1331
|
+
scrollGapInput.value = String(scrollGapPx)
|
|
1332
|
+
}
|
|
1333
|
+
// 相对边框恢复(localStorage 锚点):窗口变化后保持离边距离。
|
|
1334
|
+
// 仅认 v:2 净距离格式;旧格式(含避让距离)废弃,挂件保持默认右下角吸附。
|
|
1335
|
+
// 恢复时还原吸附状态(hAnchor/vAnchor → state.h/v),避免挂件变自由位置
|
|
1336
|
+
// 导致避让调节不实时(settle 自由分支只 clamp 不重算位置)。
|
|
1337
|
+
try {
|
|
1338
|
+
var a = JSON.parse(localStorage.getItem('dshw-pos') || 'null')
|
|
1339
|
+
if (a && a.v === 2 && (a.hAnchor === 'left' || a.hAnchor === 'right') && typeof a.hDist === 'number' &&
|
|
1340
|
+
(a.vAnchor === 'top' || a.vAnchor === 'bottom') && typeof a.vDist === 'number') {
|
|
1341
|
+
var vpA = viewport()
|
|
1342
|
+
var wA = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
1343
|
+
var hA = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
1344
|
+
// 锚点存的是净距离:右锚点按当前避让开关叠加避让距离
|
|
1345
|
+
var effectiveRightDist = a.hAnchor === 'right' ? a.hDist + (scrollGapOn ? rightGap() : 0) : a.hDist
|
|
1346
|
+
var lA = a.hAnchor === 'left' ? a.hDist : vpA.w - effectiveRightDist - wA
|
|
1347
|
+
var tA = a.vAnchor === 'top' ? a.vDist : vpA.h - a.vDist - hA
|
|
1348
|
+
state.left = clamp(lA, 0, Math.max(0, vpA.w - wA))
|
|
1349
|
+
state.top = clamp(tA, 0, Math.max(0, vpA.h - hA))
|
|
1350
|
+
// 按锚点还原吸附状态(贴边锚点 → 吸附;自由位锚点 → 自由)
|
|
1351
|
+
state.h = a.hAnchor
|
|
1352
|
+
state.hOff = 0
|
|
1353
|
+
state.v = a.vAnchor
|
|
1354
|
+
state.vOff = 0
|
|
1355
|
+
settle()
|
|
1356
|
+
}
|
|
1357
|
+
} catch (err) {}
|
|
1195
1358
|
refresh(false)
|
|
1196
1359
|
})
|
|
1197
1360
|
.catch(function () { refresh(false) })
|
|
@@ -1235,63 +1398,64 @@ function apply(ctx) {
|
|
|
1235
1398
|
let balanceCache = null
|
|
1236
1399
|
let balanceInFlight = null
|
|
1237
1400
|
let gifBytes = null
|
|
1238
|
-
//
|
|
1239
|
-
|
|
1401
|
+
// 每轮对话消耗统计:按 (session.id, turn) 分桶聚合,完成后写入 lastTurn。
|
|
1402
|
+
// 用 Map 分桶避免主会话与子代理(spawn/fork)并行时串账。
|
|
1403
|
+
let turnAggs = new Map() // sessionId -> { turn, cost, tokens, lastTs }
|
|
1240
1404
|
let lastTurn = null // { turn, amount, tokens, ts }
|
|
1241
1405
|
let lastTurnSeq = 0
|
|
1242
1406
|
const disposers = []
|
|
1243
1407
|
|
|
1244
|
-
function
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
if (turnAgg && turnAgg.cost > 0) {
|
|
1249
|
-
lastTurn = { turn: turnAgg.turn, amount: turnAgg.cost, tokens: turnAgg.tokens, ts: turnAgg.lastTs }
|
|
1408
|
+
function finalizeTurn(sessionId) {
|
|
1409
|
+
const agg = turnAggs.get(sessionId)
|
|
1410
|
+
if (agg && agg.cost > 0) {
|
|
1411
|
+
lastTurn = { turn: agg.turn, amount: agg.cost, tokens: agg.tokens, ts: agg.lastTs }
|
|
1250
1412
|
lastTurnSeq++
|
|
1251
1413
|
}
|
|
1252
|
-
|
|
1414
|
+
turnAggs.delete(sessionId)
|
|
1253
1415
|
}
|
|
1254
|
-
// 监听会话事件流:assistant/message 携带每步真实 usage,按 turn 聚合;
|
|
1255
|
-
// turn/end
|
|
1256
|
-
function handleSessionEvent(event) {
|
|
1416
|
+
// 监听会话事件流:assistant/message 携带每步真实 usage,按 (session,turn) 聚合;
|
|
1417
|
+
// turn/end 时结算该会话本轮并写入 lastTurn
|
|
1418
|
+
function handleSessionEvent(sessionId, event) {
|
|
1257
1419
|
try {
|
|
1258
1420
|
const type = event && event.type
|
|
1259
1421
|
const d = event && event.data
|
|
1260
1422
|
if (!d || typeof d !== 'object') return
|
|
1261
1423
|
if (type === 'turn/end') {
|
|
1262
|
-
|
|
1263
|
-
finalizeTurn()
|
|
1424
|
+
finalizeTurn(sessionId)
|
|
1264
1425
|
return
|
|
1265
1426
|
}
|
|
1266
1427
|
if (type !== 'assistant/message') return
|
|
1267
1428
|
const turn = Number(d.turn)
|
|
1268
1429
|
const usage = d.usage
|
|
1269
1430
|
if (!usage || typeof usage !== 'object' || !isFinite(turn)) return
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1431
|
+
let agg = turnAggs.get(sessionId)
|
|
1432
|
+
if (!agg || agg.turn !== turn) {
|
|
1433
|
+
if (agg) finalizeTurn(sessionId)
|
|
1434
|
+
agg = { turn, cost: 0, tokens: 0, lastTs: Date.now() }
|
|
1435
|
+
turnAggs.set(sessionId, agg)
|
|
1273
1436
|
}
|
|
1274
1437
|
const input = Number(usage.inputTokens) || 0
|
|
1275
1438
|
const cache = Number(usage.cacheReadTokens) || 0
|
|
1276
1439
|
const output = Number(usage.outputTokens) || 0
|
|
1277
1440
|
const reasoning = Number(usage.reasoningTokens) || 0
|
|
1278
|
-
|
|
1441
|
+
agg.tokens += input + cache + output + reasoning
|
|
1279
1442
|
// 定价换算(CNY/百万 token;缓存命中=输入价,其余按各自档位)
|
|
1280
1443
|
const model = d.message && d.message.source ? d.message.source.model : ''
|
|
1281
1444
|
const p = priceFor(model)
|
|
1282
1445
|
const off = isPeakTime(Math.floor(Date.now() / 1000)) ? 1 : 0
|
|
1283
|
-
|
|
1284
|
-
|
|
1446
|
+
agg.cost += (cache / 1e6) * p.hit[off] + (input / 1e6) * p.miss[off] + ((output + reasoning) / 1e6) * p.out[off]
|
|
1447
|
+
agg.lastTs = Date.now()
|
|
1285
1448
|
} catch (err) {}
|
|
1286
1449
|
}
|
|
1287
1450
|
|
|
1288
|
-
//
|
|
1451
|
+
// 监听所有会话的追加事件;按会话 id 分桶,turn/end 时结算该会话本轮
|
|
1289
1452
|
disposers.push(ctx.on('session/event', (session, event) => {
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1453
|
+
const sid = session && session.id ? session.id : 'default'
|
|
1454
|
+
handleSessionEvent(sid, event)
|
|
1455
|
+
}))
|
|
1456
|
+
// 会话销毁时清理残留聚合,避免内存泄漏
|
|
1457
|
+
disposers.push(ctx.on('session/disposed', (session) => {
|
|
1458
|
+
if (session && session.id) turnAggs.delete(session.id)
|
|
1295
1459
|
}))
|
|
1296
1460
|
|
|
1297
1461
|
function loadGif() {
|
|
@@ -1322,6 +1486,17 @@ function apply(ctx) {
|
|
|
1322
1486
|
throw new Error('whale image not found')
|
|
1323
1487
|
}
|
|
1324
1488
|
|
|
1489
|
+
function pickBalanceInfo(infos) {
|
|
1490
|
+
if (!Array.isArray(infos) || infos.length === 0) return null
|
|
1491
|
+
const num = (x) => (x && x.total_balance !== undefined ? Number(x.total_balance) : NaN)
|
|
1492
|
+
return (
|
|
1493
|
+
infos.find((x) => x && x.currency === 'CNY' && num(x) > 0) ||
|
|
1494
|
+
infos.find((x) => num(x) > 0) ||
|
|
1495
|
+
infos.find((x) => x && x.currency === 'CNY') ||
|
|
1496
|
+
infos[0]
|
|
1497
|
+
)
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1325
1500
|
async function fetchBalance() {
|
|
1326
1501
|
let cred
|
|
1327
1502
|
try {
|
|
@@ -1357,7 +1532,7 @@ function apply(ctx) {
|
|
|
1357
1532
|
} catch (err) {
|
|
1358
1533
|
return { ok: false, code: 'PARSE', error: '余额接口返回不是合法 JSON' }
|
|
1359
1534
|
}
|
|
1360
|
-
const info = data &&
|
|
1535
|
+
const info = pickBalanceInfo(data && data.balance_infos)
|
|
1361
1536
|
if (!info || info.total_balance === undefined) {
|
|
1362
1537
|
return { ok: false, code: 'SHAPE', error: '余额接口返回结构异常' }
|
|
1363
1538
|
}
|
|
@@ -1568,6 +1743,8 @@ function apply(ctx) {
|
|
|
1568
1743
|
bubbleOn: parsed.bubbleOn !== false,
|
|
1569
1744
|
turnCostOn: parsed.turnCostOn !== false,
|
|
1570
1745
|
turnCostCloseMs: typeof parsed.turnCostCloseMs === 'number' ? parsed.turnCostCloseMs : 5000,
|
|
1746
|
+
scrollGapOn: parsed.scrollGapOn === true,
|
|
1747
|
+
scrollGapPx: typeof parsed.scrollGapPx === 'number' ? Math.round(parsed.scrollGapPx) : 17,
|
|
1571
1748
|
}
|
|
1572
1749
|
}
|
|
1573
1750
|
} catch (err) {}
|
|
@@ -1575,12 +1752,14 @@ function apply(ctx) {
|
|
|
1575
1752
|
return null
|
|
1576
1753
|
}
|
|
1577
1754
|
|
|
1578
|
-
function writeSizeConfig(scale, sound, vol, soundSet, usageMode, peakMode, bubbleOn, turnCostOn, turnCostCloseMs) {
|
|
1755
|
+
function writeSizeConfig(scale, sound, vol, soundSet, usageMode, peakMode, bubbleOn, turnCostOn, turnCostCloseMs, scrollGapOn, scrollGapPx) {
|
|
1579
1756
|
const um = normalizeUsageMode(usageMode)
|
|
1580
1757
|
const pm = peakMode === 'liangwen' || peakMode === 'qiangqiang' ? peakMode : 'default'
|
|
1581
1758
|
const bo = bubbleOn !== false
|
|
1582
1759
|
const tco = turnCostOn !== false
|
|
1583
|
-
const tcc = typeof turnCostCloseMs === 'number'
|
|
1760
|
+
const tcc = typeof turnCostCloseMs === 'number' ? (turnCostCloseMs > 0 ? turnCostCloseMs : 0) : 5000
|
|
1761
|
+
const sgo = scrollGapOn === true
|
|
1762
|
+
const sgp = typeof scrollGapPx === 'number' && scrollGapPx > 0 ? Math.round(scrollGapPx) : 0
|
|
1584
1763
|
const body = JSON.stringify({
|
|
1585
1764
|
scale: scale,
|
|
1586
1765
|
sound: sound !== false,
|
|
@@ -1591,6 +1770,8 @@ function apply(ctx) {
|
|
|
1591
1770
|
bubbleOn: bo,
|
|
1592
1771
|
turnCostOn: tco,
|
|
1593
1772
|
turnCostCloseMs: tcc,
|
|
1773
|
+
scrollGapOn: sgo,
|
|
1774
|
+
scrollGapPx: sgp,
|
|
1594
1775
|
updatedAt: new Date().toISOString(),
|
|
1595
1776
|
})
|
|
1596
1777
|
for (const p of SIZE_FILE_CANDIDATES) {
|
|
@@ -1607,6 +1788,8 @@ function apply(ctx) {
|
|
|
1607
1788
|
bubbleOn: bo,
|
|
1608
1789
|
turnCostOn: tco,
|
|
1609
1790
|
turnCostCloseMs: tcc,
|
|
1791
|
+
scrollGapOn: sgo,
|
|
1792
|
+
scrollGapPx: sgp,
|
|
1610
1793
|
}
|
|
1611
1794
|
} catch (err) {}
|
|
1612
1795
|
}
|
|
@@ -1718,7 +1901,7 @@ function apply(ctx) {
|
|
|
1718
1901
|
balanceCache = null
|
|
1719
1902
|
}
|
|
1720
1903
|
}
|
|
1721
|
-
const result = writeSizeConfig(scale, parsed.sound !== false, parsed.vol, parsed.soundSet, parsed.usageMode, parsed.peakMode, parsed.bubbleOn, parsed.turnCostOn, parsed.turnCostCloseMs)
|
|
1904
|
+
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)
|
|
1722
1905
|
res.writeHead(result.ok ? 200 : 500, JSON_HEADERS)
|
|
1723
1906
|
res.end(JSON.stringify(result))
|
|
1724
1907
|
} catch (err) {
|