dsh-api-dashboard 1.0.3 → 1.1.2
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 +8 -1
- package/assets/whale/D1.mp3 +0 -0
- package/assets/whale/D2.mp3 +0 -0
- package/assets/whale/DSniang1.png +0 -0
- package/assets/whale/LICENSE-whale-widget.txt +21 -0
- package/assets/whale/Ya1.mp3 +0 -0
- package/assets/whale/Ya2.mp3 +0 -0
- package/assets/whale/rua.gif +0 -0
- package/client/client.js +682 -6
- package/package.json +1 -1
- package/src/index.js +130 -0
package/README.md
CHANGED
|
@@ -198,7 +198,14 @@ v0.7.1 — 修复负余额显示正常(负数→红);智谱解析保留百
|
|
|
198
198
|
v1.0.0 — 正式版:插件改名「哦鲸鲸」+ 自定义图标(/api-dashboard/icon);全量 UI 改版(固定色值去主题变量、三段式头部、卡片右栏上下结构、选中模型置顶、分组折叠、详情/设置/版本更新区重排);若干样式补全与清理
|
|
199
199
|
v1.0.2 — 余额解析稳健性(平台字段存在性校验,缺字段时如实显示「无法解析/未开放」而非误报 0);智谱配额解析重写(按 `limits.unit` 区分 5 小时窗口、周配额、工具维度,`percentage` 修正为「已用/填充度」,不再把「已用完」误显示为「余额 100%」);价格表更新(DeepSeek V4 新增 `deepseek-v4-flash-vision-exp`、`deepseek-chat/reasoner/r1` 标记为已退役;通用价格表新增现役主力 OpenAI GPT-5.6 / Claude 4.x / Gemini 3.x / Kimi K3 / StepFun 3.x,旧模型标为历史参考);自更新下载包增加包名校验
|
|
200
200
|
v1.0.3 — 文档更新:安装与升级指引改为以 npm 一行命令为主、源码方式为备选,并补充升级路径;无功能变更
|
|
201
|
+
v1.1.0 — 🐋 新增「收养大肥鱼」互动挂件:设置 → 基础设置 → 开启后屏幕边缘探出一只大肥鱼(露出约一半),点击弹全身 + 随机台词泡泡(含峰谷时段文案、gif、卖萌吐槽)+ 按压 Q 弹与音效;**可全屏拖拽**,松手自动吸附最近边缘(挂件菜单可关掉「自动靠边」改为自由摆放),左侧吸附时整体镜像翻转;挂件自带汉堡菜单(大小 0.6~2.5 倍 / 探出比例 / 音效开关 / 音效组 / 音量 / 气泡开关 / 峰谷文案 / 自动靠边),位置与全部设置持久化;开关即拨即存(不必点保存);纯互动不显示任何余额与消耗(那部分照旧在输入框下方);互动部分移植自 [MeteorNOX/DeepSeek-Balance-Whale-Widget](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget)(MIT License, Copyright (c) 2026 MeteorNOX),许可副本见 `assets/whale/LICENSE-whale-widget.txt`
|
|
202
|
+
v1.1.2 — 大肥鱼交互打磨:设置面板新增独立「大肥鱼」页签,总开关与全部细项(大小 / 探出 / 自动靠边 / 气泡 / 峰谷文案 / 音效 / 音效组 / 音量)集中到这一页,全部即改即存无需点保存,并去掉挂件本体右上角的浮层菜单;开关行图标改用鲸鱼本体缩略图;收回改为**停手 3 秒**自动缩回(连续点击持续续命,不再"点第二下立刻收回");台词气泡改为运行时避让,贴边时不再有一半在屏幕外,上方空间不足自动翻到下方;吸附收窄为**只有左右两侧边缘带**触发,停在屏幕中间保持整只显示;贴边时**纵向滑动不再展开**,沿边上下挪位,横向拉开才脱轨;连点节流 420ms,快速点击气泡稳定不闪
|
|
201
203
|
|
|
202
204
|
## License
|
|
203
205
|
|
|
204
|
-
[MIT](./LICENSE)
|
|
206
|
+
[MIT](./LICENSE)
|
|
207
|
+
|
|
208
|
+
本项目「收养大肥鱼」挂件的互动部分(拖拽吸附、Q 弹按压、台词气泡、音效、菜单)移植自
|
|
209
|
+
[MeteorNOX/DeepSeek-Balance-Whale-Widget](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget),
|
|
210
|
+
原作者 Copyright (c) 2026 MeteorNOX,MIT License,许可全文见 `assets/whale/LICENSE-whale-widget.txt`。
|
|
211
|
+
鲸鱼形象与音效素材同样来自该项目。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/client/client.js
CHANGED
|
@@ -239,8 +239,9 @@ window.__ModuleLoader__.load({
|
|
|
239
239
|
.dshadb_switch_on .dshadb_switch_knob{transform:translateX(17px)}
|
|
240
240
|
|
|
241
241
|
/* Tab 导航 */
|
|
242
|
-
.dshadb_tabs{display:flex;gap:7px;padding:4px 0 12px;overflow:auto}
|
|
243
|
-
.
|
|
242
|
+
.dshadb_tabs{display:flex;gap:7px;padding:4px 0 12px;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}
|
|
243
|
+
.dshadb_tabs::-webkit-scrollbar{display:none}
|
|
244
|
+
.dshadb_tab{flex:none;border:0;background:#e9eaf0;border-radius:12px;padding:8px 13px;font-size:11px;color:#686c76;white-space:nowrap;cursor:pointer;font-family:inherit;transition:all .12s ease}
|
|
244
245
|
.dshadb_tab_active{background:#4f7cff;color:#fff}
|
|
245
246
|
|
|
246
247
|
/* 输入框 */
|
|
@@ -296,7 +297,60 @@ window.__ModuleLoader__.load({
|
|
|
296
297
|
.dshadb_icon_btn{display:inline-flex;align-items:center;justify-content:center;min-width:38px;min-height:38px;padding:8px;border-radius:10px;background:transparent;border:none;color:#777b84;cursor:pointer;transition:background .1s ease}
|
|
297
298
|
.dshadb_icon_btn:active{background:#f0f2f8;opacity:0.85}
|
|
298
299
|
.dshadb_refresh_badge{font-size:9.5px;font-weight:600;color:#9ca0aa;background:#f0f2f8;padding:1px 7px;border-radius:999px;line-height:15px;white-space:nowrap}
|
|
299
|
-
.dshadb_title_ic{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;overflow:hidden;flex:none}
|
|
300
|
+
.dshadb_title_ic{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;overflow:hidden;flex:none}
|
|
301
|
+
/* ===== 设置面板 · 开关行 (v1.1.0) ===== */
|
|
302
|
+
.dshadb_settings_row{display:flex;align-items:center;gap:12px;padding:12px 14px;margin:14px 0 4px;border-radius:16px;background:#ffffff;border:1px solid #e7e8ec;box-shadow:0 6px 20px rgba(0,0,0,0.03)}
|
|
303
|
+
.dshadb_settings_row_main{flex:1;min-width:0}
|
|
304
|
+
.dshadb_settings_row_title{display:block;font-size:13px;font-weight:750;color:#17181c;line-height:1.3}
|
|
305
|
+
.dshadb_settings_row_sub{display:block;font-size:10.5px;font-weight:500;color:#9ca0aa;line-height:1.45;margin-top:3px}
|
|
306
|
+
/* ===== 设置面板 · 大肥鱼页 (v1.1.0) ===== */
|
|
307
|
+
.dshadb_wf_row{display:flex;align-items:center;gap:10px;padding:11px 14px;margin-bottom:8px;border-radius:14px;background:#ffffff;border:1px solid #e7e8ec}
|
|
308
|
+
.dshadb_wf_row_col{display:block;padding:11px 14px;margin-bottom:8px;border-radius:14px;background:#ffffff;border:1px solid #e7e8ec}
|
|
309
|
+
.dshadb_wf_name{font-size:12.5px;font-weight:700;color:#17181c;flex:none}
|
|
310
|
+
.dshadb_wf_sub{display:block;font-size:10px;font-weight:500;color:#9ca0aa;line-height:1.4;margin-top:2px}
|
|
311
|
+
.dshadb_wf_head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
|
|
312
|
+
.dshadb_wf_val{font-size:11px;font-weight:800;color:#4f7cff;font-variant-numeric:tabular-nums;flex:none}
|
|
313
|
+
.dshadb_wf_range{width:100%;height:24px;accent-color:#4f7cff;margin:0}
|
|
314
|
+
.dshadb_wf_seg{display:flex;gap:6px;margin-left:auto;flex:none}
|
|
315
|
+
.dshadb_wf_segbtn{font-size:11px;font-weight:700;font-family:inherit;padding:6px 11px;border-radius:10px;border:1px solid #e7e8ec;background:#f5f6f8;color:#9ca0aa;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:background .15s ease,color .15s ease}
|
|
316
|
+
.dshadb_wf_segbtn_on{background:#4f7cff;border-color:#4f7cff;color:#ffffff}
|
|
317
|
+
.dshadb_wf_disabled{opacity:.45;pointer-events:none}
|
|
318
|
+
.dshadb_wf_tip{font-size:10.5px;color:#9ca0aa;line-height:1.5;padding:2px 4px 0}
|
|
319
|
+
.dshadb_wf_avatar{flex:none;width:34px;height:34px;object-fit:contain;object-position:center;display:block;-webkit-user-drag:none;filter:drop-shadow(0 2px 4px rgba(0,0,0,.12))}
|
|
320
|
+
@media (prefers-color-scheme:dark){
|
|
321
|
+
.dshadb_wf_row,.dshadb_wf_row_col{background:#262a33;border-color:#363c48}
|
|
322
|
+
.dshadb_wf_name{color:#f2f4f8}
|
|
323
|
+
.dshadb_wf_segbtn{background:#1e222a;border-color:#363c48;color:#8b91a0}
|
|
324
|
+
.dshadb_wf_segbtn_on{background:#4f7cff;border-color:#4f7cff;color:#fff}}
|
|
325
|
+
/* ===== 大肥鱼互动挂件 (v1.1.0) ===== */
|
|
326
|
+
.dshadb-whale{position:fixed;left:0;top:60vh;z-index:9600;--dshw-scale:1;--dshw-size:clamp(96px,calc(min(150px,min(100vw,100vh) * 0.30) * var(--dshw-scale)),420px);width:var(--dshw-size);height:var(--dshw-size);pointer-events:none;-webkit-user-select:none;user-select:none;touch-action:none;transition:transform .45s cubic-bezier(.34,1.4,.64,1),left .3s ease,top .3s ease}
|
|
327
|
+
.dshadb-whale.dshadb-whale-dragging{transition:none}
|
|
328
|
+
.dshadb-whale.dshadb-whale-flip .dshadb-whale-sprite{transform:scaleX(-1)}
|
|
329
|
+
.dshadb-whale-sprite{position:absolute;left:0;top:0;width:100%;height:100%;transition:transform .3s ease}
|
|
330
|
+
.dshadb-whale-body{position:absolute;left:0;top:0;width:100%;height:100%;transform-origin:50% 92%;transition:transform .2s cubic-bezier(.34,1.56,.64,1);pointer-events:auto;cursor:grab;-webkit-tap-highlight-color:transparent}
|
|
331
|
+
.dshadb-whale-press .dshadb-whale-body{transform:scale(1.1,.88);cursor:grabbing}
|
|
332
|
+
.dshadb-whale-img{width:100%;height:100%;object-fit:contain;display:block;pointer-events:none;-webkit-user-drag:none;filter:drop-shadow(0 6px 14px rgba(0,0,0,.18))}
|
|
333
|
+
.dshadb-whale-bubble{position:absolute;left:50%;bottom:calc(100% - 6px);--dshw-bdx:0px;--dshw-btx:50%;transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(10px) scale(.82);transform-origin:var(--dshw-btx) 100%;opacity:0;pointer-events:none;background:#fff;border-radius:16px;padding:10px 14px;min-width:112px;max-width:min(74vw,248px);box-shadow:0 8px 28px rgba(0,0,0,.16);transition:opacity .2s ease,transform .26s cubic-bezier(.34,1.5,.64,1);z-index:2;text-align:center;box-sizing:border-box}
|
|
334
|
+
.dshadb-whale-bubble::after{content:"";position:absolute;left:var(--dshw-btx);top:100%;transform:translateX(-50%);border:8px solid transparent;border-top-color:#fff}
|
|
335
|
+
.dshadb-whale-bubble-on{opacity:1;transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(0) scale(1)}
|
|
336
|
+
/* 连点时气泡不做淡出淡入的来回, 只跟位置走 —— 避免闪烁 */
|
|
337
|
+
.dshadb-whale-bubble-on.dshadb-whale-bubble-keep{transition:transform .16s ease}
|
|
338
|
+
/* 气泡在上方放不下时改到下方, 尾巴翻到顶边 */
|
|
339
|
+
.dshadb-whale-bubble-below{bottom:auto;top:calc(100% - 6px);transform-origin:var(--dshw-btx) 0}
|
|
340
|
+
.dshadb-whale-bubble-below::after{top:auto;bottom:100%;border-top-color:transparent;border-bottom-color:#fff}
|
|
341
|
+
/* 镜像只作用于鲸鱼本体, 气泡与文字保持正向 */
|
|
342
|
+
.dshadb-whale-flip .dshadb-whale-bubble{transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(10px) scale(.82)}
|
|
343
|
+
.dshadb-whale-flip .dshadb-whale-bubble-on{transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(0) scale(1)}
|
|
344
|
+
.dshadb-whale-btext{display:block}
|
|
345
|
+
.dshadb-whale-gif{display:none;width:100%;max-width:190px;border-radius:10px}
|
|
346
|
+
.dshadb-whale-l-a{font-size:12.5px;font-weight:600;color:#3b4356;letter-spacing:.02em;line-height:1.4;white-space:nowrap}
|
|
347
|
+
.dshadb-whale-l-b{font-size:21px;font-weight:800;color:#17181c;line-height:1.15;white-space:nowrap}
|
|
348
|
+
.dshadb-whale-l-p{font-size:18px;font-weight:800;line-height:1.2;white-space:nowrap}
|
|
349
|
+
.dshadb-whale-l-c{font-size:11px;font-weight:600;color:#9ca0aa;line-height:1.4;white-space:nowrap}
|
|
350
|
+
.dshadb-whale-wrap{white-space:normal;word-break:break-word}
|
|
351
|
+
@media (prefers-color-scheme:dark){
|
|
352
|
+
.dshadb-whale-bubble{background:#262a33}.dshadb-whale-bubble::after{border-top-color:#262a33}
|
|
353
|
+
.dshadb-whale-l-a{color:#d3d7e0}.dshadb-whale-l-b{color:#f2f4f8}.dshadb-whale-l-c{color:#9298a6}}`
|
|
300
354
|
|
|
301
355
|
document.head.appendChild(tag);
|
|
302
356
|
/* 渐进增强: 高端机 (逻辑核 >= 6) 才启用毛玻璃等特效 */
|
|
@@ -331,6 +385,17 @@ window.__ModuleLoader__.load({
|
|
|
331
385
|
"update.done": "已更新, 重启 Web GUI 生效", "update.fail": "更新失败, 已保持原版本",
|
|
332
386
|
"update.checkfail": "检查失败 (网络异常)",
|
|
333
387
|
"settings.section.basic": "基础设置", "settings.section.relays": "中转站", "settings.section.models": "自定义模型",
|
|
388
|
+
"settings.section.whale": "大肥鱼",
|
|
389
|
+
"whale.enable": "收养大肥鱼", "whale.enableHint": "在屏幕边缘养一只会探头的大肥鱼(纯互动,不显示余额)",
|
|
390
|
+
"whale.scale": "身体大小", "whale.peek": "探出多少",
|
|
391
|
+
"whale.sound": "按压音效", "whale.soundSet": "音效组", "whale.volume": "音量",
|
|
392
|
+
"whale.bubble": "台词气泡", "whale.peakMode": "峰谷文案", "whale.snap": "自动靠边",
|
|
393
|
+
"whale.snapHint": "松手后吸附到最近的屏幕边缘;关掉就停在你放的位置",
|
|
394
|
+
"whale.on": "开", "whale.off": "关",
|
|
395
|
+
"whale.duck": "小黄鸭", "whale.fx1": "音效1",
|
|
396
|
+
"whale.peak.default": "默认", "whale.peak.liangwen": "梁文峰谷", "whale.peak.qiangqiang": "!?强强?!",
|
|
397
|
+
"whale.tip": "拖着大肥鱼可以在屏幕上随意移动;点一下弹全身和台词,再点一下收回。",
|
|
398
|
+
"whale.offTip": "先打开上面的「收养大肥鱼」,才能调下面这些。",
|
|
334
399
|
"settings.modelName": "模型名称", "settings.apiUrl": "余额接口 URL", "settings.queryType": "解析方式",
|
|
335
400
|
"settings.totalPath": "总余额字段(可选)", "settings.usedPath": "已用字段(可选)",
|
|
336
401
|
"settings.modelHint": "填你的余额接口(返回 JSON),可选填 API Key 与字段路径,如 data.balance",
|
|
@@ -367,6 +432,17 @@ window.__ModuleLoader__.load({
|
|
|
367
432
|
"update.done": "Updated. Restart Web GUI to apply", "update.fail": "Update failed, version unchanged",
|
|
368
433
|
"update.checkfail": "Check failed (network)",
|
|
369
434
|
"settings.section.basic": "Basic", "settings.section.relays": "Relays", "settings.section.models": "Custom Models",
|
|
435
|
+
"settings.section.whale": "Whale",
|
|
436
|
+
"whale.enable": "Adopt Big Whale", "whale.enableHint": "Keep a peeking whale on the screen edge (interactive only)",
|
|
437
|
+
"whale.scale": "Body size", "whale.peek": "Peek amount",
|
|
438
|
+
"whale.sound": "Press sound", "whale.soundSet": "Sound set", "whale.volume": "Volume",
|
|
439
|
+
"whale.bubble": "Speech bubble", "whale.peakMode": "Peak wording", "whale.snap": "Snap to edge",
|
|
440
|
+
"whale.snapHint": "Snaps to the nearest edge on release; turn off to keep it where you drop it",
|
|
441
|
+
"whale.on": "On", "whale.off": "Off",
|
|
442
|
+
"whale.duck": "Duck", "whale.fx1": "FX 1",
|
|
443
|
+
"whale.peak.default": "Default", "whale.peak.liangwen": "Liangwen", "whale.peak.qiangqiang": "!?Qiang?!",
|
|
444
|
+
"whale.tip": "Drag the whale anywhere on screen; tap once for full body and a line, tap again to tuck it back.",
|
|
445
|
+
"whale.offTip": "Turn on \"Adopt Big Whale\" above to edit these.",
|
|
370
446
|
"settings.modelName": "Model name", "settings.apiUrl": "Balance API URL", "settings.queryType": "Parse type",
|
|
371
447
|
"settings.totalPath": "Total field (opt)", "settings.usedPath": "Used field (opt)",
|
|
372
448
|
"settings.modelHint": "A JSON endpoint for your balance; optional API key & field paths like data.balance",
|
|
@@ -407,7 +483,9 @@ window.__ModuleLoader__.load({
|
|
|
407
483
|
const changed = JSON.stringify(newBalances) !== JSON.stringify(oldBalances) || snapshot.status !== "ok";
|
|
408
484
|
const nextFetchedAt = data.fetchedAt || Date.now();
|
|
409
485
|
if (!changed) {
|
|
410
|
-
|
|
486
|
+
// v1.1.0: config 必须始终跟随最新响应 —— 旧写法在余额未变时保留旧 config,
|
|
487
|
+
// 导致设置里改完开关(如 whaleEnabled)后前端永远读不到新值, 表现为"开了又变回关"
|
|
488
|
+
snapshot = { ...snapshot, config: data.config || snapshot.config, fetchedAt: nextFetchedAt, isRefreshing: false };
|
|
411
489
|
} else {
|
|
412
490
|
snapshot = { status: "ok", balances: newBalances, config: data.config || snapshot.config, fetchedAt: nextFetchedAt, isRefreshing: false };
|
|
413
491
|
}
|
|
@@ -929,6 +1007,12 @@ window.__ModuleLoader__.load({
|
|
|
929
1007
|
const [warn, setWarn] = react.useState(10);
|
|
930
1008
|
const [currency, setCurrency] = react.useState("CNY");
|
|
931
1009
|
const [refreshSec, setRefreshSec] = react.useState(5);
|
|
1010
|
+
const [whaleOn, setWhaleOn] = react.useState(!!(config && config.whaleEnabled));
|
|
1011
|
+
// v1.1.0: 大肥鱼细项 (独立页签, 走 /api-dashboard/whale/settings, 与看板主配置分开)
|
|
1012
|
+
const [wf, setWf] = react.useState({
|
|
1013
|
+
scale: 1, peekRatio: 0.5, soundOn: true, soundSet: "duck",
|
|
1014
|
+
volume: 0.5, bubbleOn: true, peakMode: "default", snapOn: true,
|
|
1015
|
+
});
|
|
932
1016
|
const [form, setForm] = react.useState({ name: "", baseUrl: "", apiKey: "", queryType: "auto" });
|
|
933
1017
|
const [mform, setMform] = react.useState({ name: "", apiUrl: "", apiKey: "", queryType: "auto", totalPath: "", usedPath: "", currency: "" });
|
|
934
1018
|
const [loading, setLoading] = react.useState(false);
|
|
@@ -982,6 +1066,7 @@ window.__ModuleLoader__.load({
|
|
|
982
1066
|
setWarn(config?.warnThreshold ?? 10);
|
|
983
1067
|
setCurrency(config?.currency ?? "CNY");
|
|
984
1068
|
setRefreshSec(Math.round((config?.clientPollIntervalMs || 5000) / 1000) || 5);
|
|
1069
|
+
setWhaleOn(!!(config && config.whaleEnabled));
|
|
985
1070
|
let cancelled = false;
|
|
986
1071
|
fetch("/api-dashboard/config", { cache: "no-store" }).then(r => r.json()).then(d => {
|
|
987
1072
|
if (cancelled) return;
|
|
@@ -989,8 +1074,24 @@ window.__ModuleLoader__.load({
|
|
|
989
1074
|
setRelays(d.customRelays || []);
|
|
990
1075
|
setModels(d.customModels || []);
|
|
991
1076
|
if (d.refreshIntervalSec) setRefreshSec(d.refreshIntervalSec);
|
|
1077
|
+
if (typeof d.whaleEnabled === "boolean") setWhaleOn(d.whaleEnabled);
|
|
992
1078
|
}
|
|
993
1079
|
}).finally(() => { if (!cancelled) setLoading(false); });
|
|
1080
|
+
// 大肥鱼细项与主配置并行拉取, 失败保持默认值
|
|
1081
|
+
fetch("/api-dashboard/whale/settings", { cache: "no-store" }).then(r => r.json()).then(d => {
|
|
1082
|
+
if (cancelled || !d || !d.ok || !d.settings) return;
|
|
1083
|
+
const s = d.settings;
|
|
1084
|
+
setWf(prev => ({
|
|
1085
|
+
scale: typeof s.scale === "number" ? s.scale : prev.scale,
|
|
1086
|
+
peekRatio: typeof s.peekRatio === "number" ? s.peekRatio : prev.peekRatio,
|
|
1087
|
+
soundOn: typeof s.soundOn === "boolean" ? s.soundOn : prev.soundOn,
|
|
1088
|
+
soundSet: s.soundSet || prev.soundSet,
|
|
1089
|
+
volume: typeof s.volume === "number" ? s.volume : prev.volume,
|
|
1090
|
+
bubbleOn: typeof s.bubbleOn === "boolean" ? s.bubbleOn : prev.bubbleOn,
|
|
1091
|
+
peakMode: s.peakMode || prev.peakMode,
|
|
1092
|
+
snapOn: typeof s.snapOn === "boolean" ? s.snapOn : prev.snapOn,
|
|
1093
|
+
}));
|
|
1094
|
+
}).catch(() => {});
|
|
994
1095
|
return () => { cancelled = true; };
|
|
995
1096
|
}, [isOpen]);
|
|
996
1097
|
if (!isOpen) return null;
|
|
@@ -1001,7 +1102,12 @@ window.__ModuleLoader__.load({
|
|
|
1001
1102
|
customRelays: relays, customModels: models,
|
|
1002
1103
|
safeThreshold: Number(safe), warnThreshold: Number(warn), currency,
|
|
1003
1104
|
refreshIntervalSec: Math.min(Math.max(Number(refreshSec) || 5, 5), 60),
|
|
1105
|
+
whaleEnabled: !!whaleOn,
|
|
1004
1106
|
}) });
|
|
1107
|
+
// v1.1.0: 保存后立刻强拉一次, 让开关类设置(大肥鱼)即时生效, 不必等下一轮轮询
|
|
1108
|
+
try { await store.forceRefresh(); } catch (e) { /* 忽略 */ }
|
|
1109
|
+
// 双保险: 快照万一没及时更新, 也按用户刚保存的值直接挂载/卸载 (两处均幂等)
|
|
1110
|
+
if (whaleOn) ensureWhaleWidget(); else removeWhaleWidget();
|
|
1005
1111
|
onClose();
|
|
1006
1112
|
} finally { setSaving(false); }
|
|
1007
1113
|
};
|
|
@@ -1017,13 +1123,39 @@ window.__ModuleLoader__.load({
|
|
|
1017
1123
|
setMform({ name: "", apiUrl: "", apiKey: "", queryType: "auto", totalPath: "", usedPath: "", currency: "" });
|
|
1018
1124
|
};
|
|
1019
1125
|
const removeModel = (id) => setModels(models.filter(m => m.id !== id));
|
|
1126
|
+
// v1.1.0 修复: 开关类设置即时保存 —— 不能要求用户必须点「保存并生效」,
|
|
1127
|
+
// 否则拨完开关直接返回/关闭就丢了, 表现为"开了又变回关"
|
|
1128
|
+
const toggleWhale = async (next) => {
|
|
1129
|
+
setWhaleOn(next);
|
|
1130
|
+
if (next) ensureWhaleWidget(); else removeWhaleWidget();
|
|
1131
|
+
try {
|
|
1132
|
+
await fetch("/api-dashboard/config", {
|
|
1133
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
1134
|
+
body: JSON.stringify({ whaleEnabled: !!next }),
|
|
1135
|
+
});
|
|
1136
|
+
await store.forceRefresh();
|
|
1137
|
+
} catch (e) { /* 网络失败: 本地仍生效, 下次保存/刷新会对齐 */ }
|
|
1138
|
+
};
|
|
1020
1139
|
const qtLabel = (q) => t("qt." + (q || "auto"));
|
|
1021
1140
|
|
|
1141
|
+
// v1.1.0: 大肥鱼细项即时生效 + 即时落盘 (滑块/开关都不依赖「保存并生效」按钮)
|
|
1142
|
+
const patchWf = (patch) => {
|
|
1143
|
+
setWf(prev => ({ ...prev, ...patch }));
|
|
1144
|
+
patchWhaleWidget(patch);
|
|
1145
|
+
try {
|
|
1146
|
+
fetch("/api-dashboard/whale/settings", {
|
|
1147
|
+
method: "PUT", headers: { "Content-Type": "application/json" },
|
|
1148
|
+
body: JSON.stringify(patch),
|
|
1149
|
+
}).catch(() => {});
|
|
1150
|
+
} catch (e) { /* 忽略 */ }
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1022
1153
|
const tab = (id, label) => react.createElement("button", { type: "button", className: "dshadb_tab" + (section === id ? " dshadb_tab_active" : ""), onClick: () => setSection(id), key: id }, label);
|
|
1023
1154
|
const tabsNode = react.createElement("div", { className: "dshadb_tabs", key: "tabs" }, [
|
|
1024
1155
|
tab("basic", t("settings.section.basic")),
|
|
1025
1156
|
tab("relays", t("settings.section.relays")),
|
|
1026
1157
|
tab("models", t("settings.section.models")),
|
|
1158
|
+
tab("whale", t("settings.section.whale")),
|
|
1027
1159
|
]);
|
|
1028
1160
|
|
|
1029
1161
|
// 基础设置: 阈值 / 币种 / 刷新间隔
|
|
@@ -1144,6 +1276,71 @@ window.__ModuleLoader__.load({
|
|
|
1144
1276
|
]) : null,
|
|
1145
1277
|
]);
|
|
1146
1278
|
|
|
1279
|
+
// v1.1.0: 大肥鱼页 —— 总开关 + 全部细项, 全部即改即存, 不依赖底部保存按钮
|
|
1280
|
+
const wfRow = (key, label, control, sub) => react.createElement("div", { className: "dshadb_wf_row", key }, [
|
|
1281
|
+
react.createElement("div", { key: "n", style: { minWidth: 0 } }, [
|
|
1282
|
+
react.createElement("span", { className: "dshadb_wf_name", key: "t" }, label),
|
|
1283
|
+
sub ? react.createElement("span", { className: "dshadb_wf_sub", key: "s" }, sub) : null,
|
|
1284
|
+
]),
|
|
1285
|
+
control,
|
|
1286
|
+
]);
|
|
1287
|
+
const wfSlider = (key, label, valText, min, max, step, value, onCommit) =>
|
|
1288
|
+
react.createElement("div", { className: "dshadb_wf_row_col", key }, [
|
|
1289
|
+
react.createElement("div", { className: "dshadb_wf_head", key: "h" }, [
|
|
1290
|
+
react.createElement("span", { className: "dshadb_wf_name", key: "n" }, label),
|
|
1291
|
+
react.createElement("span", { className: "dshadb_wf_val", key: "v" }, valText),
|
|
1292
|
+
]),
|
|
1293
|
+
react.createElement("input", {
|
|
1294
|
+
className: "dshadb_wf_range", type: "range", min, max, step, value, key: "r",
|
|
1295
|
+
onChange: (e) => onCommit(Number(e.target.value)),
|
|
1296
|
+
}),
|
|
1297
|
+
]);
|
|
1298
|
+
const wfSeg = (key, label, options, value, onPick) =>
|
|
1299
|
+
wfRow(key, label, react.createElement("div", { className: "dshadb_wf_seg", key: "seg" },
|
|
1300
|
+
options.map(([v, lb]) => react.createElement("button", {
|
|
1301
|
+
type: "button", key: String(v),
|
|
1302
|
+
className: "dshadb_wf_segbtn" + (value === v ? " dshadb_wf_segbtn_on" : ""),
|
|
1303
|
+
onClick: () => onPick(v),
|
|
1304
|
+
}, lb))));
|
|
1305
|
+
|
|
1306
|
+
const whaleSection = react.createElement("div", { className: "dshadb_settings_section", key: "s_whale" }, [
|
|
1307
|
+
// 总开关 (从基础设置迁移过来); 图标用鲸鱼本体缩略图, 不用 emoji
|
|
1308
|
+
react.createElement("div", { className: "dshadb_settings_row", key: "en", style: { margin: "2px 0 10px" } }, [
|
|
1309
|
+
react.createElement("img", {
|
|
1310
|
+
className: "dshadb_wf_avatar", src: "/api-dashboard/whale/image.png",
|
|
1311
|
+
alt: "", draggable: false, key: "av",
|
|
1312
|
+
}),
|
|
1313
|
+
react.createElement("div", { className: "dshadb_settings_row_main", key: "m" }, [
|
|
1314
|
+
react.createElement("span", { className: "dshadb_settings_row_title", key: "t" }, t("whale.enable")),
|
|
1315
|
+
react.createElement("span", { className: "dshadb_settings_row_sub", key: "s" }, t("whale.enableHint")),
|
|
1316
|
+
]),
|
|
1317
|
+
react.createElement(Switch, { checked: whaleOn, onChange: toggleWhale, key: "sw" }),
|
|
1318
|
+
]),
|
|
1319
|
+
// 细项: 未开启时置灰不可点
|
|
1320
|
+
react.createElement("div", { className: whaleOn ? "" : "dshadb_wf_disabled", key: "detail" }, [
|
|
1321
|
+
!whaleOn ? react.createElement("div", { className: "dshadb_wf_tip", key: "offtip", style: { marginBottom: "8px" } }, t("whale.offTip")) : null,
|
|
1322
|
+
wfSlider("scale", t("whale.scale"), Math.round(wf.scale * 100) + "%", 0.6, 2.5, 0.05, wf.scale,
|
|
1323
|
+
(v) => patchWf({ scale: v })),
|
|
1324
|
+
wfSlider("peek", t("whale.peek"), Math.round(wf.peekRatio * 100) + "%", 0.15, 0.9, 0.05, wf.peekRatio,
|
|
1325
|
+
(v) => patchWf({ peekRatio: v })),
|
|
1326
|
+
wfSeg("snap", t("whale.snap"), [[true, t("whale.on")], [false, t("whale.off")]], wf.snapOn,
|
|
1327
|
+
(v) => patchWf({ snapOn: v })),
|
|
1328
|
+
react.createElement("div", { className: "dshadb_wf_tip", key: "snaphint", style: { margin: "-2px 0 8px" } }, t("whale.snapHint")),
|
|
1329
|
+
wfSeg("bubble", t("whale.bubble"), [[true, t("whale.on")], [false, t("whale.off")]], wf.bubbleOn,
|
|
1330
|
+
(v) => patchWf({ bubbleOn: v })),
|
|
1331
|
+
wfSeg("peak", t("whale.peakMode"), [
|
|
1332
|
+
["default", t("whale.peak.default")], ["liangwen", t("whale.peak.liangwen")], ["qiangqiang", t("whale.peak.qiangqiang")],
|
|
1333
|
+
], wf.peakMode, (v) => patchWf({ peakMode: v })),
|
|
1334
|
+
wfSeg("sound", t("whale.sound"), [[true, t("whale.on")], [false, t("whale.off")]], wf.soundOn,
|
|
1335
|
+
(v) => patchWf({ soundOn: v })),
|
|
1336
|
+
wfSeg("soundset", t("whale.soundSet"), [["duck", t("whale.duck")], ["fx1", t("whale.fx1")]], wf.soundSet,
|
|
1337
|
+
(v) => patchWf({ soundSet: v })),
|
|
1338
|
+
wfSlider("vol", t("whale.volume"), Math.round(wf.volume * 100) + "%", 0, 1, 0.05, wf.volume,
|
|
1339
|
+
(v) => patchWf({ volume: v })),
|
|
1340
|
+
react.createElement("div", { className: "dshadb_wf_tip", key: "tip" }, t("whale.tip")),
|
|
1341
|
+
]),
|
|
1342
|
+
]);
|
|
1343
|
+
|
|
1147
1344
|
const output = react.createElement("div", { className: "dshadb_drawer", style: { maxHeight: "86vh" }, onClick: (e) => e.stopPropagation(), key: "drawer" }, [
|
|
1148
1345
|
react.createElement("div", { className: "dshadb_handle", key: "handle" }, react.createElement("div", { className: "dshadb_handle_bar" })),
|
|
1149
1346
|
react.createElement("div", { className: "dshadb_header", key: "header" }, [
|
|
@@ -1157,8 +1354,9 @@ window.__ModuleLoader__.load({
|
|
|
1157
1354
|
]),
|
|
1158
1355
|
react.createElement("div", { className: "dshadb_body", key: "body" }, [
|
|
1159
1356
|
tabsNode,
|
|
1160
|
-
section === "basic" ? basicSection : section === "relays" ? relaySection : modelSection,
|
|
1161
|
-
|
|
1357
|
+
section === "basic" ? basicSection : section === "relays" ? relaySection : section === "whale" ? whaleSection : modelSection,
|
|
1358
|
+
// 大肥鱼页全部即改即存, 不需要保存按钮
|
|
1359
|
+
section === "whale" ? null : react.createElement("button", { type: "button", className: "dshadb_add_btn", onClick: save, disabled: saving, key: "save", style: { width: "100%", borderStyle: "solid", background: "#17181c", color: "#fff", borderColor: "#17181c" } }, saving ? t("refreshing") : t("settings.save")),
|
|
1162
1360
|
// v0.6.0: 版本与更新区块
|
|
1163
1361
|
react.createElement("div", { key: "upd", style: { marginTop: "12px", paddingTop: "10px", borderTop: "1px solid #e7e8ec" } }, [
|
|
1164
1362
|
react.createElement("div", { style: { fontSize: "11px", fontWeight: "700", color: "#777b84", marginBottom: "8px" }, key: "upd_t" }, t("update.title")),
|
|
@@ -1182,6 +1380,472 @@ window.__ModuleLoader__.load({
|
|
|
1182
1380
|
}
|
|
1183
1381
|
//#endregion
|
|
1184
1382
|
|
|
1383
|
+
//#region 大肥鱼互动挂件 (v1.1.0)
|
|
1384
|
+
// 移植自 MeteorNOX/DeepSeek-Balance-Whale-Widget (MIT License, Copyright (c) 2026 MeteorNOX)
|
|
1385
|
+
// 保留原版全部互动能力: 拖拽 / 四边吸附 / 左吸附镜像 / 按压Q弹 / 音效 / 随机台词 / 汉堡菜单;
|
|
1386
|
+
// 去掉的只有余额与消耗显示 (那部分由输入框下方的看板负责)。
|
|
1387
|
+
function Switch({ checked, onChange }) {
|
|
1388
|
+
return react.createElement("button", {
|
|
1389
|
+
type: "button", role: "switch", "aria-checked": !!checked,
|
|
1390
|
+
className: "dshadb_switch" + (checked ? " dshadb_switch_on" : ""),
|
|
1391
|
+
onClick: () => onChange(!checked), style: { marginLeft: "auto" },
|
|
1392
|
+
}, react.createElement("span", { className: "dshadb_switch_knob" }));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
var WHALE_DEFAULTS = {
|
|
1396
|
+
scale: 1, soundOn: true, soundSet: "duck", volume: 0.5, bubbleOn: true,
|
|
1397
|
+
peakMode: "default", snapOn: true, peekRatio: 0.5, left: null, top: null, side: "right",
|
|
1398
|
+
};
|
|
1399
|
+
var whaleCtxInfo = { isPeak: false };
|
|
1400
|
+
function updateWhaleContext(info) {
|
|
1401
|
+
whaleCtxInfo = { isPeak: !!(info && info.isPeak) };
|
|
1402
|
+
if (whaleWidget && whaleWidget.onContext) whaleWidget.onContext();
|
|
1403
|
+
}
|
|
1404
|
+
function whalePeakWords(peakMode, isPeak) {
|
|
1405
|
+
if (peakMode === "liangwen") return isPeak ? "梁文峰" : "梁文谷";
|
|
1406
|
+
if (peakMode === "qiangqiang") return isPeak ? "!?峰峰?!" : "!?谷谷?!";
|
|
1407
|
+
return isPeak ? "高峰时段" : "空闲时段";
|
|
1408
|
+
}
|
|
1409
|
+
function whalePickOne(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
|
|
1410
|
+
// 台词组: [权重, 生成函数] —— 生成 {gif:true} 或 [{t,s,c}] 三行 (s: A小字 / B大字 / P峰谷 / C灰注)
|
|
1411
|
+
var WHALE_GROUPS = [
|
|
1412
|
+
[40, function (st) {
|
|
1413
|
+
var isPeak = !!whaleCtxInfo.isPeak;
|
|
1414
|
+
return [
|
|
1415
|
+
{ t: "当前时间段为:", s: "A" },
|
|
1416
|
+
{ t: whalePeakWords(st.peakMode, isPeak), s: "P", c: isPeak ? "#e0433f" : "#2fa24c" },
|
|
1417
|
+
{ t: isPeak ? "09:00~12:00 / 14:00~18:00" : "其余时段 / 周末全天", s: "C" },
|
|
1418
|
+
];
|
|
1419
|
+
}],
|
|
1420
|
+
[10, function () { return { gif: true }; }],
|
|
1421
|
+
[8, function () { return [null, { t: whalePickOne(["好模型... ↓", "好女孩...↓"]), s: "B" }, null]; }],
|
|
1422
|
+
[8, function () {
|
|
1423
|
+
return [null, { t: whalePickOne([
|
|
1424
|
+
"不知道用户有什么用,先赶走吧~", "我...我...我也要挣钱吗?", "我去吃饭啦,测完叫我",
|
|
1425
|
+
"压力一只蓝色大肥鱼?!", "DeepSleep...", "坏了...用户彻底怒了!",
|
|
1426
|
+
"摸头摸头~ 再摸摸嘛", "尾巴被你拖到墙角啦", "偷看你打代码半天啦",
|
|
1427
|
+
]), s: "A", w: true }, null]; }],
|
|
1428
|
+
[4, function () {
|
|
1429
|
+
return [null, { t: whalePickOne([
|
|
1430
|
+
"你目录里的dsh是什么...大烧货吗...?", "恭喜你实现token自由!token全跑了!", "真当我是便宜货啊...",
|
|
1431
|
+
]), s: "A", w: true }, null]; }],
|
|
1432
|
+
[2, function () { return [{ t: "这个", s: "A" }, { t: "凶", s: "B" }, { t: "是什么意思呀...", s: "A" }]; }],
|
|
1433
|
+
[1, function () { return [null, { t: "哦鲸鲸... ", s: "B" }, null]; }],
|
|
1434
|
+
];
|
|
1435
|
+
function whalePickLines(st) {
|
|
1436
|
+
var total = 0, i;
|
|
1437
|
+
for (i = 0; i < WHALE_GROUPS.length; i++) total += WHALE_GROUPS[i][0];
|
|
1438
|
+
var r = Math.random() * total;
|
|
1439
|
+
for (i = 0; i < WHALE_GROUPS.length; i++) { r -= WHALE_GROUPS[i][0]; if (r < 0) return WHALE_GROUPS[i][1](st); }
|
|
1440
|
+
return WHALE_GROUPS[0][1](st);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
var whaleWidget = null;
|
|
1444
|
+
// 停止互动后多久缩回 / 气泡存活时长 (用户要求: 3 秒)
|
|
1445
|
+
var WHALE_IDLE_MS = 3000;
|
|
1446
|
+
// 左右「边缘带」占屏宽比例: 松手时中心落在带内才吸附缩回, 中间区域原地不动
|
|
1447
|
+
var WHALE_EDGE_ZONE = 0.22;
|
|
1448
|
+
// 贴边时横向要拉开多少 (占鲸鱼宽比例) 才脱离边轨、转为自由拖拽并展开
|
|
1449
|
+
var WHALE_RAIL_BREAK = 0.42;
|
|
1450
|
+
// 连点节流: 间隔小于此值只续命不换台词 (换词会重建气泡, 连点时会闪没)
|
|
1451
|
+
var WHALE_LINE_MIN_MS = 420;
|
|
1452
|
+
function ensureWhaleWidget() {
|
|
1453
|
+
if (whaleWidget) return;
|
|
1454
|
+
if (typeof document === "undefined") return;
|
|
1455
|
+
var st = {};
|
|
1456
|
+
for (var k in WHALE_DEFAULTS) st[k] = WHALE_DEFAULTS[k];
|
|
1457
|
+
|
|
1458
|
+
// ---------- DOM ----------
|
|
1459
|
+
var root = document.createElement("div");
|
|
1460
|
+
root.className = "dshadb-whale";
|
|
1461
|
+
var sprite = document.createElement("div");
|
|
1462
|
+
sprite.className = "dshadb-whale-sprite";
|
|
1463
|
+
var body = document.createElement("div");
|
|
1464
|
+
body.className = "dshadb-whale-body";
|
|
1465
|
+
var img = document.createElement("img");
|
|
1466
|
+
img.className = "dshadb-whale-img";
|
|
1467
|
+
img.src = "/api-dashboard/whale/image.png";
|
|
1468
|
+
img.alt = "大肥鱼"; img.draggable = false;
|
|
1469
|
+
body.appendChild(img);
|
|
1470
|
+
sprite.appendChild(body);
|
|
1471
|
+
var bubble = document.createElement("div");
|
|
1472
|
+
bubble.className = "dshadb-whale-bubble";
|
|
1473
|
+
var bText = document.createElement("div");
|
|
1474
|
+
bText.className = "dshadb-whale-btext";
|
|
1475
|
+
var gif = document.createElement("img");
|
|
1476
|
+
gif.className = "dshadb-whale-gif";
|
|
1477
|
+
gif.src = "/api-dashboard/whale/rua.gif";
|
|
1478
|
+
gif.alt = ""; gif.draggable = false;
|
|
1479
|
+
var gifBroken = false;
|
|
1480
|
+
gif.addEventListener("error", function () { gifBroken = true; });
|
|
1481
|
+
bubble.appendChild(gif);
|
|
1482
|
+
bubble.appendChild(bText);
|
|
1483
|
+
root.appendChild(bubble);
|
|
1484
|
+
root.appendChild(sprite);
|
|
1485
|
+
document.body.appendChild(root);
|
|
1486
|
+
|
|
1487
|
+
// ---------- 音效 ----------
|
|
1488
|
+
var audio = { press: null, release: null };
|
|
1489
|
+
function reloadAudio() {
|
|
1490
|
+
audio.press = new Audio("/api-dashboard/whale/sound/press.mp3?set=" + st.soundSet);
|
|
1491
|
+
audio.release = new Audio("/api-dashboard/whale/sound/release.mp3?set=" + st.soundSet);
|
|
1492
|
+
audio.press.volume = st.volume; audio.release.volume = st.volume;
|
|
1493
|
+
}
|
|
1494
|
+
reloadAudio();
|
|
1495
|
+
function playSound(which) {
|
|
1496
|
+
if (!st.soundOn || st.volume <= 0) return;
|
|
1497
|
+
var a = audio[which]; if (!a) return;
|
|
1498
|
+
try { a.volume = st.volume; a.currentTime = 0; var p = a.play(); if (p && p.catch) p.catch(function () {}); } catch (e) {}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// ---------- 状态 ----------
|
|
1502
|
+
var full = false; // 是否整只露出
|
|
1503
|
+
var bubbleOpen = false;
|
|
1504
|
+
var hideTimer = null, bubbleTimer = null, saveTimer = null;
|
|
1505
|
+
var drag = null;
|
|
1506
|
+
var lastLineAt = 0; // 上次换台词的时刻 (连点节流用)
|
|
1507
|
+
|
|
1508
|
+
function sizePx() { return root.getBoundingClientRect().width || 120; }
|
|
1509
|
+
function vw() { return window.innerWidth || document.documentElement.clientWidth; }
|
|
1510
|
+
function vh() { return window.innerHeight || document.documentElement.clientHeight; }
|
|
1511
|
+
|
|
1512
|
+
function applyScale() { root.style.setProperty("--dshw-scale", String(st.scale)); }
|
|
1513
|
+
|
|
1514
|
+
/** 把 state 的位置写进 left/top; 探头位移交给 transform, 所以这里一律夹在屏幕内 */
|
|
1515
|
+
function applyPos() {
|
|
1516
|
+
var s = sizePx();
|
|
1517
|
+
if (st.left === null || st.top === null) {
|
|
1518
|
+
st.left = Math.max(0, vw() - s);
|
|
1519
|
+
st.top = Math.round(vh() * 0.62);
|
|
1520
|
+
}
|
|
1521
|
+
st.left = Math.min(Math.max(st.left, 0), Math.max(0, vw() - s));
|
|
1522
|
+
st.top = Math.min(Math.max(st.top, 0), Math.max(0, vh() - s));
|
|
1523
|
+
root.style.left = st.left + "px";
|
|
1524
|
+
root.style.top = st.top + "px";
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
/** 探头位移: 只有吸在左右两侧时才把身子推出屏幕; 中间自由摆放时整只显示 */
|
|
1528
|
+
function applyPeek() {
|
|
1529
|
+
if (full || !st.side) { root.style.transform = "translate(0px, 0px)"; return; }
|
|
1530
|
+
var s = sizePx();
|
|
1531
|
+
var out = Math.round(s * (1 - st.peekRatio));
|
|
1532
|
+
var dx = st.side === "left" ? -out : st.side === "right" ? out : 0;
|
|
1533
|
+
root.style.transform = "translate(" + dx + "px, 0px)";
|
|
1534
|
+
}
|
|
1535
|
+
function applyFlip() {
|
|
1536
|
+
if (st.side === "left") root.classList.add("dshadb-whale-flip");
|
|
1537
|
+
else root.classList.remove("dshadb-whale-flip");
|
|
1538
|
+
}
|
|
1539
|
+
function applyAll() { applyScale(); applyPos(); applyFlip(); applyPeek(); }
|
|
1540
|
+
|
|
1541
|
+
// ---------- 持久化 ----------
|
|
1542
|
+
function saveSoon() {
|
|
1543
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
1544
|
+
saveTimer = setTimeout(function () {
|
|
1545
|
+
saveTimer = null;
|
|
1546
|
+
try {
|
|
1547
|
+
fetch("/api-dashboard/whale/settings", {
|
|
1548
|
+
method: "PUT", headers: { "Content-Type": "application/json" },
|
|
1549
|
+
body: JSON.stringify({
|
|
1550
|
+
scale: st.scale, soundOn: st.soundOn, soundSet: st.soundSet, volume: st.volume,
|
|
1551
|
+
bubbleOn: st.bubbleOn, peakMode: st.peakMode, snapOn: st.snapOn,
|
|
1552
|
+
peekRatio: st.peekRatio, left: st.left, top: st.top, side: st.side,
|
|
1553
|
+
}),
|
|
1554
|
+
}).catch(function () {});
|
|
1555
|
+
} catch (e) {}
|
|
1556
|
+
}, 400);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// ---------- 气泡 ----------
|
|
1560
|
+
var STYLE_CLASS = { A: "dshadb-whale-l-a", B: "dshadb-whale-l-b", P: "dshadb-whale-l-p", C: "dshadb-whale-l-c" };
|
|
1561
|
+
function renderLines(lines) {
|
|
1562
|
+
bText.textContent = "";
|
|
1563
|
+
if (lines && lines.gif && !gifBroken) { gif.style.display = "block"; bText.style.display = "none"; return; }
|
|
1564
|
+
gif.style.display = "none"; bText.style.display = "block";
|
|
1565
|
+
var arr = (lines && lines.gif) ? [null, { t: "动图跑掉了...", s: "A", w: true }, null] : lines;
|
|
1566
|
+
for (var i = 0; i < arr.length; i++) {
|
|
1567
|
+
var ln = arr[i]; if (!ln) continue;
|
|
1568
|
+
var el = document.createElement("div");
|
|
1569
|
+
el.className = STYLE_CLASS[ln.s] || STYLE_CLASS.A;
|
|
1570
|
+
if (ln.w) el.className += " dshadb-whale-wrap";
|
|
1571
|
+
if (ln.c) el.style.color = ln.c;
|
|
1572
|
+
el.textContent = ln.t;
|
|
1573
|
+
bText.appendChild(el);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* 鲸鱼的「目标」位置 —— 不用 getBoundingClientRect。
|
|
1578
|
+
* 探头/展开是 0.45s transform 过渡, 点击瞬间读实时 rect 拿到的是过渡中的中间态,
|
|
1579
|
+
* 气泡会按旧位置算避让, 连点时越算越偏, 最后整个飘到屏幕外 = 看着"不显示"。
|
|
1580
|
+
*/
|
|
1581
|
+
function targetRect() {
|
|
1582
|
+
var s = sizePx();
|
|
1583
|
+
var dx = 0;
|
|
1584
|
+
if (!full && st.side) {
|
|
1585
|
+
var out = Math.round(s * (1 - st.peekRatio));
|
|
1586
|
+
dx = st.side === "left" ? -out : st.side === "right" ? out : 0;
|
|
1587
|
+
}
|
|
1588
|
+
return { left: st.left + dx, top: st.top, size: s };
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* 把气泡摆到不出屏的位置:
|
|
1592
|
+
* 1) 水平方向整体平移, 保证左右都留 8px 边距 (贴边时不会有一半在屏幕外)
|
|
1593
|
+
* 2) 尾巴单独定位, 仍然指向鲸鱼中心
|
|
1594
|
+
* 3) 上方空间不够时翻到鲸鱼下方
|
|
1595
|
+
*/
|
|
1596
|
+
function placeBubble() {
|
|
1597
|
+
var margin = 8;
|
|
1598
|
+
bubble.classList.remove("dshadb-whale-bubble-below");
|
|
1599
|
+
bubble.style.setProperty("--dshw-bdx", "0px");
|
|
1600
|
+
bubble.style.setProperty("--dshw-btx", "50%");
|
|
1601
|
+
var r = targetRect();
|
|
1602
|
+
var bw = bubble.offsetWidth, bh = bubble.offsetHeight;
|
|
1603
|
+
if (!bw) return;
|
|
1604
|
+
var cx = r.left + r.size / 2;
|
|
1605
|
+
var want = cx - bw / 2;
|
|
1606
|
+
var shift = 0;
|
|
1607
|
+
if (want < margin) shift = margin - want;
|
|
1608
|
+
else if (want + bw > vw() - margin) shift = (vw() - margin) - (want + bw);
|
|
1609
|
+
bubble.style.setProperty("--dshw-bdx", Math.round(shift) + "px");
|
|
1610
|
+
var tail = bw / 2 - shift;
|
|
1611
|
+
tail = Math.min(Math.max(tail, 18), bw - 18);
|
|
1612
|
+
bubble.style.setProperty("--dshw-btx", Math.round(tail) + "px");
|
|
1613
|
+
if (r.top - bh - 6 < margin) bubble.classList.add("dshadb-whale-bubble-below");
|
|
1614
|
+
}
|
|
1615
|
+
function openBubble() {
|
|
1616
|
+
if (!st.bubbleOn) return;
|
|
1617
|
+
renderLines(whalePickLines(st));
|
|
1618
|
+
bubble.classList.remove("dshadb-whale-bubble-keep");
|
|
1619
|
+
bubble.classList.add("dshadb-whale-bubble-on");
|
|
1620
|
+
bubbleOpen = true;
|
|
1621
|
+
placeBubble();
|
|
1622
|
+
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
1623
|
+
bubbleTimer = setTimeout(closeBubble, WHALE_IDLE_MS);
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* 连点续命: 保持当前台词不变, 只把气泡留住并重新计时。
|
|
1627
|
+
* 连点时不重建内容 —— 重建会让气泡宽度反复跳变、重新测量, 视觉上就是闪一下没了。
|
|
1628
|
+
*/
|
|
1629
|
+
function keepBubble() {
|
|
1630
|
+
if (!st.bubbleOn) return;
|
|
1631
|
+
if (!bubbleOpen) { openBubble(); return; }
|
|
1632
|
+
bubble.classList.add("dshadb-whale-bubble-on");
|
|
1633
|
+
bubble.classList.add("dshadb-whale-bubble-keep");
|
|
1634
|
+
placeBubble();
|
|
1635
|
+
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
1636
|
+
bubbleTimer = setTimeout(closeBubble, WHALE_IDLE_MS);
|
|
1637
|
+
}
|
|
1638
|
+
function closeBubble() {
|
|
1639
|
+
bubble.classList.remove("dshadb-whale-bubble-on");
|
|
1640
|
+
bubble.classList.remove("dshadb-whale-bubble-keep");
|
|
1641
|
+
bubbleOpen = false;
|
|
1642
|
+
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null; }
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// ---------- 探头 / 全身 ----------
|
|
1646
|
+
function toFull() {
|
|
1647
|
+
full = true; applyPeek();
|
|
1648
|
+
lastLineAt = Date.now();
|
|
1649
|
+
openBubble();
|
|
1650
|
+
playSound("press");
|
|
1651
|
+
armHide();
|
|
1652
|
+
}
|
|
1653
|
+
function toPeek() {
|
|
1654
|
+
full = false; applyPeek();
|
|
1655
|
+
closeBubble();
|
|
1656
|
+
// 只在真的会缩回边缘时才播收回音 (停在屏幕中间时不缩, 也就不该有动静)
|
|
1657
|
+
if (st.side) playSound("release");
|
|
1658
|
+
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
|
|
1659
|
+
}
|
|
1660
|
+
/** 停手 WHALE_IDLE_MS 后自动缩回; 每次互动都重新计时 */
|
|
1661
|
+
function armHide() {
|
|
1662
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
1663
|
+
hideTimer = setTimeout(function () { hideTimer = null; if (full) toPeek(); }, WHALE_IDLE_MS);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// ---------- 吸附 ----------
|
|
1667
|
+
/**
|
|
1668
|
+
* 松手后的落位: 只有拖到左右两侧的「边缘带」里才吸附 + 探头,
|
|
1669
|
+
* 停在屏幕中间就整只留在原地 (side="" → applyPeek 不做位移)。
|
|
1670
|
+
*/
|
|
1671
|
+
function settle() {
|
|
1672
|
+
var s = sizePx();
|
|
1673
|
+
var W = vw(), H = vh();
|
|
1674
|
+
if (!st.snapOn) {
|
|
1675
|
+
st.side = "";
|
|
1676
|
+
applyPos(); applyFlip(); applyPeek(); saveSoon();
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
var cx = st.left + s / 2;
|
|
1680
|
+
var zone = Math.max(s * 0.75, W * WHALE_EDGE_ZONE); // 边缘带宽度
|
|
1681
|
+
if (cx <= zone) { st.side = "left"; st.left = 0; }
|
|
1682
|
+
else if (cx >= W - zone) { st.side = "right"; st.left = W - s; }
|
|
1683
|
+
else { st.side = ""; } // 屏幕中间: 不缩
|
|
1684
|
+
// 纵向永远不缩, 只是别跑出屏幕
|
|
1685
|
+
st.top = Math.min(Math.max(st.top, 0), Math.max(0, H - s));
|
|
1686
|
+
applyPos(); applyFlip(); applyPeek(); saveSoon();
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// ---------- 拖拽 ----------
|
|
1690
|
+
function onDown(e) {
|
|
1691
|
+
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
|
|
1692
|
+
drag = {
|
|
1693
|
+
id: e.pointerId, sx: e.clientX, sy: e.clientY,
|
|
1694
|
+
ox: st.left, oy: st.top, moved: false,
|
|
1695
|
+
// 起手时贴着哪一侧: 贴边状态下的纵向滑动算「沿边轨道滑」, 不展开
|
|
1696
|
+
rail: full ? "" : st.side, freed: false,
|
|
1697
|
+
};
|
|
1698
|
+
root.classList.add("dshadb-whale-dragging");
|
|
1699
|
+
root.classList.add("dshadb-whale-press");
|
|
1700
|
+
try { body.setPointerCapture(e.pointerId); } catch (err) {}
|
|
1701
|
+
playSound("press");
|
|
1702
|
+
}
|
|
1703
|
+
function onMove(e) {
|
|
1704
|
+
if (!drag || e.pointerId !== drag.id) return;
|
|
1705
|
+
var dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
|
|
1706
|
+
if (!drag.moved && dx * dx + dy * dy < 9) return;
|
|
1707
|
+
drag.moved = true;
|
|
1708
|
+
// 沿边纵向滑动: 横向没拉开就保持探头姿态, 只上下移动
|
|
1709
|
+
if (drag.rail && !drag.freed) {
|
|
1710
|
+
var breakOut = Math.abs(dx) > Math.max(28, sizePx() * WHALE_RAIL_BREAK);
|
|
1711
|
+
if (!breakOut) {
|
|
1712
|
+
st.top = drag.oy + dy;
|
|
1713
|
+
applyPos(); // 夹在屏幕内并写回 top
|
|
1714
|
+
applyPeek(); // 维持探头位移, 不展开
|
|
1715
|
+
e.preventDefault();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
drag.freed = true; // 横向拉开了 → 转为自由拖拽
|
|
1719
|
+
}
|
|
1720
|
+
if (!full) {
|
|
1721
|
+
full = true; // 拖动时整只跟手, 不再半隐
|
|
1722
|
+
root.style.transform = "translate(0px, 0px)";
|
|
1723
|
+
closeBubble();
|
|
1724
|
+
}
|
|
1725
|
+
st.left = drag.ox + dx; st.top = drag.oy + dy;
|
|
1726
|
+
root.style.left = st.left + "px";
|
|
1727
|
+
root.style.top = st.top + "px";
|
|
1728
|
+
e.preventDefault();
|
|
1729
|
+
}
|
|
1730
|
+
function onUp(e) {
|
|
1731
|
+
if (!drag || e.pointerId !== drag.id) return;
|
|
1732
|
+
var moved = drag.moved;
|
|
1733
|
+
var railed = !!drag.rail && !drag.freed;
|
|
1734
|
+
drag = null;
|
|
1735
|
+
root.classList.remove("dshadb-whale-dragging");
|
|
1736
|
+
root.classList.remove("dshadb-whale-press");
|
|
1737
|
+
try { body.releasePointerCapture(e.pointerId); } catch (err) {}
|
|
1738
|
+
if (moved) {
|
|
1739
|
+
// 沿边滑完仍是探头状态: 只记住新高度, 不吸附动画也不展开
|
|
1740
|
+
if (railed) { saveSoon(); return; }
|
|
1741
|
+
settle(); armHide(); return;
|
|
1742
|
+
}
|
|
1743
|
+
// 没移动 = 点击: 弹全身 / 换台词, 但绝不立刻收回 —— 一律等停手 3 秒
|
|
1744
|
+
playSound("release");
|
|
1745
|
+
if (!full) { toFull(); return; }
|
|
1746
|
+
// 连点节流: 间隔太短只续命不换词, 避免气泡反复重建导致"看不到"
|
|
1747
|
+
var now = Date.now();
|
|
1748
|
+
if (now - lastLineAt < WHALE_LINE_MIN_MS) { keepBubble(); armHide(); return; }
|
|
1749
|
+
lastLineAt = now;
|
|
1750
|
+
openBubble();
|
|
1751
|
+
armHide();
|
|
1752
|
+
}
|
|
1753
|
+
body.addEventListener("pointerdown", onDown);
|
|
1754
|
+
body.addEventListener("pointermove", onMove);
|
|
1755
|
+
body.addEventListener("pointerup", onUp);
|
|
1756
|
+
body.addEventListener("pointercancel", onUp);
|
|
1757
|
+
|
|
1758
|
+
// ---------- 外部设置接口 ----------
|
|
1759
|
+
// 设置面板「大肥鱼」页改动后调用: 立刻应用到当前挂件并落盘
|
|
1760
|
+
function applyPatch(patch) {
|
|
1761
|
+
var soundTouched = false;
|
|
1762
|
+
for (var key in patch) {
|
|
1763
|
+
if (!Object.prototype.hasOwnProperty.call(patch, key)) continue;
|
|
1764
|
+
if (patch[key] === undefined) continue;
|
|
1765
|
+
st[key] = patch[key];
|
|
1766
|
+
if (key === "soundSet") soundTouched = true;
|
|
1767
|
+
if (key === "volume" || key === "soundOn") soundTouched = true;
|
|
1768
|
+
}
|
|
1769
|
+
if (soundTouched) {
|
|
1770
|
+
if (patch.soundSet !== undefined) reloadAudio();
|
|
1771
|
+
if (audio.press) audio.press.volume = st.volume;
|
|
1772
|
+
if (audio.release) audio.release.volume = st.volume;
|
|
1773
|
+
}
|
|
1774
|
+
if (patch.bubbleOn === false) closeBubble();
|
|
1775
|
+
if (patch.snapOn !== undefined) {
|
|
1776
|
+
if (st.snapOn) { settle(); return; }
|
|
1777
|
+
st.side = "";
|
|
1778
|
+
}
|
|
1779
|
+
applyAll();
|
|
1780
|
+
if (bubbleOpen) placeBubble();
|
|
1781
|
+
saveSoon();
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// ---------- 全局监听 ----------
|
|
1785
|
+
var onDocDown = function (e) {
|
|
1786
|
+
if (root.contains(e.target)) return;
|
|
1787
|
+
if (full && !drag) toPeek();
|
|
1788
|
+
};
|
|
1789
|
+
document.addEventListener("pointerdown", onDocDown);
|
|
1790
|
+
var onResize = function () {
|
|
1791
|
+
applyPos();
|
|
1792
|
+
if (st.snapOn && st.side) settle(); else applyPeek();
|
|
1793
|
+
if (bubbleOpen) placeBubble();
|
|
1794
|
+
};
|
|
1795
|
+
window.addEventListener("resize", onResize);
|
|
1796
|
+
var onVis = function () {
|
|
1797
|
+
if (document.hidden) { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } }
|
|
1798
|
+
else if (full) armHide();
|
|
1799
|
+
};
|
|
1800
|
+
document.addEventListener("visibilitychange", onVis);
|
|
1801
|
+
|
|
1802
|
+
whaleWidget = {
|
|
1803
|
+
root: root,
|
|
1804
|
+
applyPatch: applyPatch,
|
|
1805
|
+
getState: function () {
|
|
1806
|
+
var out = {};
|
|
1807
|
+
for (var k in st) out[k] = st[k];
|
|
1808
|
+
return out;
|
|
1809
|
+
},
|
|
1810
|
+
onContext: function () { /* 峰谷变化下次开泡时自动取用 */ },
|
|
1811
|
+
dispose: function () {
|
|
1812
|
+
body.removeEventListener("pointerdown", onDown);
|
|
1813
|
+
body.removeEventListener("pointermove", onMove);
|
|
1814
|
+
body.removeEventListener("pointerup", onUp);
|
|
1815
|
+
body.removeEventListener("pointercancel", onUp);
|
|
1816
|
+
document.removeEventListener("pointerdown", onDocDown);
|
|
1817
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
1818
|
+
window.removeEventListener("resize", onResize);
|
|
1819
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
1820
|
+
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
1821
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
1822
|
+
if (root.parentNode) root.parentNode.removeChild(root);
|
|
1823
|
+
whaleWidget = null;
|
|
1824
|
+
},
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
// ---------- 拉取已存设置 ----------
|
|
1828
|
+
applyAll();
|
|
1829
|
+
fetch("/api-dashboard/whale/settings", { cache: "no-store" })
|
|
1830
|
+
.then(function (r) { return r.json(); })
|
|
1831
|
+
.then(function (d) {
|
|
1832
|
+
if (!d || !d.ok || !d.settings) return;
|
|
1833
|
+
for (var key in WHALE_DEFAULTS) {
|
|
1834
|
+
if (d.settings[key] !== undefined && d.settings[key] !== null) st[key] = d.settings[key];
|
|
1835
|
+
}
|
|
1836
|
+
if (d.settings.left !== undefined) st.left = d.settings.left;
|
|
1837
|
+
if (d.settings.top !== undefined) st.top = d.settings.top;
|
|
1838
|
+
applyAll();
|
|
1839
|
+
})
|
|
1840
|
+
.catch(function () {});
|
|
1841
|
+
}
|
|
1842
|
+
function removeWhaleWidget() { if (whaleWidget) whaleWidget.dispose(); }
|
|
1843
|
+
/** 设置面板改动后推给活着的挂件 (未挂载时静默忽略, 值已由端点落盘) */
|
|
1844
|
+
function patchWhaleWidget(patch) {
|
|
1845
|
+
if (whaleWidget && whaleWidget.applyPatch) whaleWidget.applyPatch(patch);
|
|
1846
|
+
}
|
|
1847
|
+
//#endregion
|
|
1848
|
+
|
|
1185
1849
|
//#region plugin
|
|
1186
1850
|
const inject = ["slots", "locale", "modelDirectories"];
|
|
1187
1851
|
function apply(ctx) {
|
|
@@ -1220,6 +1884,18 @@ window.__ModuleLoader__.load({
|
|
|
1220
1884
|
const dashData = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1221
1885
|
const config = dashData.config;
|
|
1222
1886
|
|
|
1887
|
+
// v1.1.0: 大肥鱼互动挂件 — 设置里开启「收养大肥鱼」后挂载/卸载 (纯互动, 不含余额)
|
|
1888
|
+
const whaleOn = !!(config && config.whaleEnabled);
|
|
1889
|
+
react.useEffect(() => {
|
|
1890
|
+
if (!whaleOn) { removeWhaleWidget(); return; }
|
|
1891
|
+
ensureWhaleWidget();
|
|
1892
|
+
return removeWhaleWidget;
|
|
1893
|
+
}, [whaleOn]);
|
|
1894
|
+
// 峰谷时段变化同步给挂件 (台词里的「当前时间段」用它, 不涉及任何金额)
|
|
1895
|
+
react.useEffect(() => {
|
|
1896
|
+
updateWhaleContext({ isPeak: !!(config && config.isPeak) });
|
|
1897
|
+
}, [config && config.isPeak]);
|
|
1898
|
+
|
|
1223
1899
|
const handleSelect = (id) => { setSelected(id); setSelectedId(id); };
|
|
1224
1900
|
|
|
1225
1901
|
// v0.5.15: 按模型名切到对应平台的核心逻辑 (两条信号共用)
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -441,6 +441,19 @@ export const Config = Schema.object({
|
|
|
441
441
|
cacheMiss: Schema.number().min(0).default(1),
|
|
442
442
|
output: Schema.number().min(0).default(2),
|
|
443
443
|
}).default({}),
|
|
444
|
+
/** 收养大肥鱼: 屏幕侧边互动宠物挂件 (v1.1.0, 纯互动不含余额, 移植自 MeteorNOX/DeepSeek-Balance-Whale-Widget, MIT) */
|
|
445
|
+
whaleEnabled: Schema.boolean().default(false),
|
|
446
|
+
/** 大肥鱼挂件设置: 大小/音效/音量/气泡/峰谷文案/吸附/位置记忆 */
|
|
447
|
+
whaleSettings: Schema.object({
|
|
448
|
+
scale: Schema.number().min(0.6).max(2.5).default(1),
|
|
449
|
+
soundOn: Schema.boolean().default(true),
|
|
450
|
+
soundSet: Schema.string().default('duck'),
|
|
451
|
+
volume: Schema.number().min(0).max(1).default(0.5),
|
|
452
|
+
bubbleOn: Schema.boolean().default(true),
|
|
453
|
+
peakMode: Schema.string().default('default'),
|
|
454
|
+
snapOn: Schema.boolean().default(true),
|
|
455
|
+
peekRatio: Schema.number().min(0.15).max(0.9).default(0.5),
|
|
456
|
+
}).default({}),
|
|
444
457
|
})
|
|
445
458
|
|
|
446
459
|
// ============================================================
|
|
@@ -953,6 +966,13 @@ export function apply(ctx, config) {
|
|
|
953
966
|
currency: persisted.currency ?? config.currency ?? 'CNY',
|
|
954
967
|
safeThreshold: persisted.safeThreshold ?? config.safeThreshold ?? 50,
|
|
955
968
|
warnThreshold: persisted.warnThreshold ?? config.warnThreshold ?? 10,
|
|
969
|
+
whaleEnabled: persisted.whaleEnabled ?? config.whaleEnabled ?? false,
|
|
970
|
+
whaleSettings: {
|
|
971
|
+
scale: 1, soundOn: true, soundSet: 'duck', volume: 0.5, bubbleOn: true,
|
|
972
|
+
peakMode: 'default', snapOn: true, peekRatio: 0.5, left: null, top: null, side: 'right',
|
|
973
|
+
...(config.whaleSettings ?? {}),
|
|
974
|
+
...(persisted.whaleSettings ?? {}),
|
|
975
|
+
},
|
|
956
976
|
}
|
|
957
977
|
|
|
958
978
|
const getConfig = () => runtimeConfig
|
|
@@ -1036,6 +1056,7 @@ export function apply(ctx, config) {
|
|
|
1036
1056
|
currency: runtimeConfig.currency,
|
|
1037
1057
|
isPeak: isPeakTime(),
|
|
1038
1058
|
isWeekend: isWeekend(),
|
|
1059
|
+
whaleEnabled: !!runtimeConfig.whaleEnabled,
|
|
1039
1060
|
},
|
|
1040
1061
|
}
|
|
1041
1062
|
// A3: 告警检测
|
|
@@ -1178,6 +1199,110 @@ export function apply(ctx, config) {
|
|
|
1178
1199
|
},
|
|
1179
1200
|
}), 'dsh-api-dashboard: icon route')
|
|
1180
1201
|
|
|
1202
|
+
// v1.1.0: 大肥鱼互动挂件资产 (移植自 MeteorNOX/DeepSeek-Balance-Whale-Widget, MIT License)
|
|
1203
|
+
const WHALE_ASSET_ROOT = join(SELF_ROOT, 'assets', 'whale')
|
|
1204
|
+
const whaleAsset = (name) => join(WHALE_ASSET_ROOT, name)
|
|
1205
|
+
let whaleImgCache = null
|
|
1206
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
1207
|
+
kind: 'exact', path: '/api-dashboard/whale/image.png',
|
|
1208
|
+
async handler(req, res) {
|
|
1209
|
+
if (req.method !== 'GET') { res.writeHead(405, { Allow: 'GET' }); res.end(); return }
|
|
1210
|
+
try {
|
|
1211
|
+
if (!whaleImgCache) whaleImgCache = readFileSync(whaleAsset('DSniang1.png'))
|
|
1212
|
+
res.writeHead(200, {
|
|
1213
|
+
'Content-Type': 'image/png',
|
|
1214
|
+
'Cache-Control': 'public, max-age=86400',
|
|
1215
|
+
'Content-Length': whaleImgCache.length,
|
|
1216
|
+
})
|
|
1217
|
+
res.end(whaleImgCache)
|
|
1218
|
+
} catch { res.writeHead(404); res.end() }
|
|
1219
|
+
},
|
|
1220
|
+
}), 'dsh-api-dashboard: whale image route')
|
|
1221
|
+
// rua.gif (随机台词动图)
|
|
1222
|
+
let whaleGifCache = null
|
|
1223
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
1224
|
+
kind: 'exact', path: '/api-dashboard/whale/rua.gif',
|
|
1225
|
+
async handler(req, res) {
|
|
1226
|
+
if (req.method !== 'GET') { res.writeHead(405, { Allow: 'GET' }); res.end(); return }
|
|
1227
|
+
try {
|
|
1228
|
+
if (!whaleGifCache) whaleGifCache = readFileSync(whaleAsset('rua.gif'))
|
|
1229
|
+
res.writeHead(200, {
|
|
1230
|
+
'Content-Type': 'image/gif',
|
|
1231
|
+
'Cache-Control': 'public, max-age=86400',
|
|
1232
|
+
'Content-Length': whaleGifCache.length,
|
|
1233
|
+
})
|
|
1234
|
+
res.end(whaleGifCache)
|
|
1235
|
+
} catch { res.writeHead(404); res.end() }
|
|
1236
|
+
},
|
|
1237
|
+
}), 'dsh-api-dashboard: whale gif route')
|
|
1238
|
+
// 音效 (每请求读盘 + no-store, 更换音频即生效; ?set=duck|fx1 选音效组)
|
|
1239
|
+
for (const kind of ['press', 'release']) {
|
|
1240
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
1241
|
+
kind: 'exact', path: `/api-dashboard/whale/sound/${kind}.mp3`,
|
|
1242
|
+
async handler(req, res) {
|
|
1243
|
+
if (req.method !== 'GET') { res.writeHead(405, { Allow: 'GET' }); res.end(); return }
|
|
1244
|
+
try {
|
|
1245
|
+
const set = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get('set') === 'fx1' ? 'fx1' : 'duck'
|
|
1246
|
+
const table = { duck: { press: 'Ya1.mp3', release: 'Ya2.mp3' }, fx1: { press: 'D1.mp3', release: 'D2.mp3' } }
|
|
1247
|
+
const data = readFileSync(whaleAsset(table[set][kind]))
|
|
1248
|
+
res.writeHead(200, {
|
|
1249
|
+
'Content-Type': 'audio/mpeg',
|
|
1250
|
+
'Cache-Control': 'no-store',
|
|
1251
|
+
'Content-Length': data.length,
|
|
1252
|
+
})
|
|
1253
|
+
res.end(data)
|
|
1254
|
+
} catch { res.writeHead(404); res.end() }
|
|
1255
|
+
},
|
|
1256
|
+
}), `dsh-api-dashboard: whale sound ${kind} route`)
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// v1.1.0: 大肥鱼挂件设置 (大小/音效/音量/气泡/峰谷文案/吸附/位置) —— 独立轻量端点,
|
|
1260
|
+
// 与看板主配置分开, 滑块拖动等高频写入不触发余额刷新
|
|
1261
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
1262
|
+
kind: 'exact', path: '/api-dashboard/whale/settings',
|
|
1263
|
+
async handler(req, res) {
|
|
1264
|
+
if (req.method === 'GET') { sendJson(res, 200, { ok: true, settings: runtimeConfig.whaleSettings }); return }
|
|
1265
|
+
if (req.method === 'PUT' || req.method === 'POST') {
|
|
1266
|
+
try {
|
|
1267
|
+
let raw = ''
|
|
1268
|
+
for await (const chunk of req) { raw += chunk }
|
|
1269
|
+
const body = raw ? JSON.parse(raw) : {}
|
|
1270
|
+
const cur = runtimeConfig.whaleSettings
|
|
1271
|
+
const num = (v, lo, hi, dflt) => (typeof v === 'number' && Number.isFinite(v) ? Math.min(Math.max(v, lo), hi) : dflt)
|
|
1272
|
+
runtimeConfig.whaleSettings = {
|
|
1273
|
+
scale: num(body.scale, 0.6, 2.5, cur.scale),
|
|
1274
|
+
soundOn: typeof body.soundOn === 'boolean' ? body.soundOn : cur.soundOn,
|
|
1275
|
+
soundSet: body.soundSet === 'fx1' ? 'fx1' : (body.soundSet === 'duck' ? 'duck' : cur.soundSet),
|
|
1276
|
+
volume: num(body.volume, 0, 1, cur.volume),
|
|
1277
|
+
bubbleOn: typeof body.bubbleOn === 'boolean' ? body.bubbleOn : cur.bubbleOn,
|
|
1278
|
+
peakMode: ['default', 'liangwen', 'qiangqiang'].includes(body.peakMode) ? body.peakMode : cur.peakMode,
|
|
1279
|
+
snapOn: typeof body.snapOn === 'boolean' ? body.snapOn : cur.snapOn,
|
|
1280
|
+
peekRatio: num(body.peekRatio, 0.15, 0.9, cur.peekRatio),
|
|
1281
|
+
left: typeof body.left === 'number' && Number.isFinite(body.left) ? body.left : cur.left,
|
|
1282
|
+
top: typeof body.top === 'number' && Number.isFinite(body.top) ? body.top : cur.top,
|
|
1283
|
+
// v1.1.0: 只保留左右吸附 (上下不再缩回); '' = 停在屏幕中间不缩
|
|
1284
|
+
side: ['left', 'right', ''].includes(body.side) ? body.side : cur.side,
|
|
1285
|
+
}
|
|
1286
|
+
savePersistedState({
|
|
1287
|
+
refreshIntervalMs: runtimeConfig.refreshIntervalMs,
|
|
1288
|
+
clientPollIntervalMs: runtimeConfig.clientPollIntervalMs,
|
|
1289
|
+
timeoutMs: runtimeConfig.timeoutMs,
|
|
1290
|
+
customRelays: runtimeConfig.customRelays,
|
|
1291
|
+
customModels: runtimeConfig.customModels,
|
|
1292
|
+
currency: runtimeConfig.currency,
|
|
1293
|
+
safeThreshold: runtimeConfig.safeThreshold,
|
|
1294
|
+
warnThreshold: runtimeConfig.warnThreshold,
|
|
1295
|
+
whaleEnabled: runtimeConfig.whaleEnabled,
|
|
1296
|
+
whaleSettings: runtimeConfig.whaleSettings,
|
|
1297
|
+
})
|
|
1298
|
+
sendJson(res, 200, { ok: true, settings: runtimeConfig.whaleSettings })
|
|
1299
|
+
} catch (err) { sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) }) }
|
|
1300
|
+
return
|
|
1301
|
+
}
|
|
1302
|
+
res.writeHead(405, { Allow: 'GET, PUT, POST' }); res.end()
|
|
1303
|
+
},
|
|
1304
|
+
}), 'dsh-api-dashboard: whale settings route')
|
|
1305
|
+
|
|
1181
1306
|
webCtx.effect(() => webCtx.webServer.register({
|
|
1182
1307
|
kind: 'exact', path: '/api-dashboard/config',
|
|
1183
1308
|
async handler(req, res) {
|
|
@@ -1188,6 +1313,7 @@ export function apply(ctx, config) {
|
|
|
1188
1313
|
customModels: runtimeConfig.customModels.map(m => ({ ...m, apiKey: m.apiKey ? '***' : '' })),
|
|
1189
1314
|
presets: runtimeConfig.presets,
|
|
1190
1315
|
refreshIntervalSec: Math.round(runtimeConfig.refreshIntervalMs / 1000),
|
|
1316
|
+
whaleEnabled: !!runtimeConfig.whaleEnabled,
|
|
1191
1317
|
})
|
|
1192
1318
|
return
|
|
1193
1319
|
}
|
|
@@ -1226,6 +1352,8 @@ export function apply(ctx, config) {
|
|
|
1226
1352
|
if (typeof body.safeThreshold === 'number' && body.safeThreshold >= 0) runtimeConfig.safeThreshold = body.safeThreshold
|
|
1227
1353
|
if (typeof body.warnThreshold === 'number' && body.warnThreshold >= 0) runtimeConfig.warnThreshold = body.warnThreshold
|
|
1228
1354
|
if (typeof body.currency === 'string' && body.currency.trim()) runtimeConfig.currency = body.currency.trim().toUpperCase()
|
|
1355
|
+
// v1.1.0: 收养大肥鱼开关
|
|
1356
|
+
if (typeof body.whaleEnabled === 'boolean') runtimeConfig.whaleEnabled = body.whaleEnabled
|
|
1229
1357
|
// 持久化: 写入状态文件, 重启后恢复 (用户配置优先)
|
|
1230
1358
|
savePersistedState({
|
|
1231
1359
|
refreshIntervalMs: runtimeConfig.refreshIntervalMs,
|
|
@@ -1236,6 +1364,7 @@ export function apply(ctx, config) {
|
|
|
1236
1364
|
currency: runtimeConfig.currency,
|
|
1237
1365
|
safeThreshold: runtimeConfig.safeThreshold,
|
|
1238
1366
|
warnThreshold: runtimeConfig.warnThreshold,
|
|
1367
|
+
whaleEnabled: runtimeConfig.whaleEnabled,
|
|
1239
1368
|
})
|
|
1240
1369
|
resetLoop(); await refreshAll()
|
|
1241
1370
|
sendJson(res, 200, {
|
|
@@ -1243,6 +1372,7 @@ export function apply(ctx, config) {
|
|
|
1243
1372
|
customRelays: runtimeConfig.customRelays.map(r => ({ ...r, apiKey: r.apiKey ? '***' : '' })),
|
|
1244
1373
|
customModels: runtimeConfig.customModels.map(m => ({ ...m, apiKey: m.apiKey ? '***' : '' })),
|
|
1245
1374
|
refreshIntervalSec: Math.round(runtimeConfig.refreshIntervalMs / 1000),
|
|
1375
|
+
whaleEnabled: !!runtimeConfig.whaleEnabled,
|
|
1246
1376
|
})
|
|
1247
1377
|
} catch (err) { sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) }) }
|
|
1248
1378
|
return
|