dsh-whale-widget 0.3.16 → 0.3.17

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 CHANGED
@@ -68,7 +68,7 @@ DeepSeek Harness(DSH)Web 界面右下角的常驻挂件:小鲸鱼气泡图
68
68
  - 🔊 **按压 & 松开音效**:内置「小黄鸭」「音效1」两套预置;也可导入音频片段并自由组合成**自定义音效组**(槽位可留空 = 该事件静音)
69
69
  - 🎵 **任务结束音**:一轮回复完成时播放;内置两个预设音效 —— 默认 **Minecraft·经验球**(`assets/minecraft-exp-orb.wav`)与 **A**(`assets/task-end-a.wav`),也可选任意片段或音效组
70
70
  - ⏳ **提问音 / 授权音**:DSH 需要你回答或批准时播放;与任务结束音共用同一套音效库,**各自可调音量**,音效行左侧的 `[✓]` 决定「响不响」(不响 = 仍会冒泡提示)
71
- - 🔉 **每个音效独立音量**:按压音效、任务结束音、提问音、授权音各自一条音量滑块(默认 100%),每行都带「试听」
71
+ - 🔉 **音量:一条全局 + 三条按事件**:①区「按压音效」的音量是**全局基准**(按压 / 松开音用它);②③④ 区(每轮消耗提示 / 提问提示 / 授权提示)各有一条自己的音量滑块,每行都带「试听」。**没动过某个事件的滑块时,它跟随①区音量**(入口行摘要会写明 `跟随按压音量 N%`);**一旦你拖过那个滑块,就按你设的值播**(摘要只显示百分比)。这样既能"一个总音量管全局",也能单独压低某类提示音。
72
72
  - ✂️ **音频片段管理**:导入时可视化裁剪、试听;资源管理窗口(菜单 → 资源管理 → 「管理」)按 **图片 / 音频 两个可折叠入口**统一查看/试听/删除 —— 入口行右侧直接显示数量(如 `角色 1 · 泡泡图 2`、`音效组 3 · 片段 4`),点一行才展开明细;「导入片段」在音频那一区里;收起/展开时会顺带停掉正在试听的那一段
73
73
  - 🐳 **自定义角色**:上传自己的鲸鱼图片(图库管理,可回退默认)
74
74
  - 🖼️ **泡泡图库**:内置 `petpet`、`money1` 两张图,也可上传 png/gif,供图片/随机图片模块使用
@@ -394,6 +394,7 @@ curl http://127.0.0.1:3080/dsh-whale/audio.json
394
394
  - `/dsh-whale/audio.json` → 200,含 `groups` / `fragments`(其中内置片段 `exp_orb` = Minecraft·经验球、`end_a` = A)
395
395
  - `/dsh-whale/audio-fragment.wav?id=exp_orb` → 200 `audio/wav`(内置任务结束音;无需用户导入)
396
396
  - `/dsh-whale/audio-fragment.wav?id=end_a` → 200 `audio/wav`(内置任务结束音 A)
397
+ - `/dsh-whale/wait.json` → 200 JSON,含 `{ok:true, pending}`;`pending` 为当前挂起的「提问 / 授权」(`{kind:'question'|'approval', id, ts}`)或 `null` —— 这是「提问提示 / 授权提示」音效与常驻气泡的数据源(默认每秒轮询一次)
397
398
  - 浏览器 F5 后右下角出现挂件
398
399
 
399
400
  > ⚠️ **关于上面这些 `curl`**:全部 **23 个** `/dsh-whale/*` 路由都已接入 **DSH 浏览器信任栅栏**(`connection.requestRejection`)。
@@ -423,6 +424,9 @@ curl http://127.0.0.1:3080/dsh-whale/audio.json
423
424
  - **每轮消耗不显示**:确认「音效与提示 → 全局设置」里「每轮消耗提示」那一行**左边的 `[✓]`** 是勾上的(入口行右侧摘要会直接显示 `已关闭` 或当前配置);一轮对话要完整结束(`turn/end`)才结算。余额变化泡泡与消耗泡泡抢层时,提醒会退化为居中卡片。
424
425
  - **每轮消耗泡泡的内容**:「音效与提示 → 全局设置」→ **展开「每轮消耗提示」** → 「编辑提示内容」(模块化,金额用 `{cost}`);同一区里能设自动关闭秒数、提示音量与任务结束音效。
425
426
  - **任务结束音没响**:该开关**默认开着**(默认音是内置 **`A`**,另有内置的 Minecraft·经验球可选);到「音效与提示 → 全局设置」→ **展开「每轮消耗提示」**,确认「任务结束音」那行左侧的 `[✓]` 是勾上的(取消勾选 = 静音)。若自定义片段文件被删,会回退/静音。
427
+ - **提示音量调了没用 / 到底听谁的**:把「每轮消耗提示」那一行展开,拖动它自己的「**提示音量**」滑块 ⇒ 该事件的音量**立刻由它说了算**(入口行摘要从 `跟随按压音量 N%` 变成 `N%`)。**没拖过**时它跟随①区「按压音效」的音量 —— 摘要里会写明,滑块也显示实际生效值。想让某类提示更轻就把自己的滑块调小;想整体轻就调①区。点「恢复默认」会把这几条都退回"跟随①区"。(0.3.17 之前这个滑块确实不生效,是本版修好的。)
428
+ - **保存后设置没生效 / 保存到底成没成**:0.3.17 起保存失败**不再静默** —— 点「保存」若写入失败,会自动重试一次,仍失败则弹出 `⚠ 设置保存失败:<原因>`(例如数据目录只读、接口被拦)。看到这条提示说明**这次改动没有落盘**,请按原因处理后再保存一次。
429
+ - **等待提问 / 授权的气泡关不掉**:**点一下那个气泡即可收起**(同一条挂起不会再自动弹回;你回答/批准后,或下一次新的提问/授权到来时照常提示)。想让**点角色(鲸鱼)也能收起**提示气泡,去「音效与提示 → 全局设置」面板最下面勾上「**点按角色关闭提示气泡**」(默认关)。两处说明都收在旁边的 `?` 圈圈里。
426
430
  - **换了 / 删了 API key 之后,更早的记账天变成「本地估算 ¥0.00」**:记账按**密钥指纹分本**(一把 key 一本账),换 key 等于换了一本 —— 数据**没丢**,都在 `.dshw-usage.json` 的 `accounting.books` 里。当前版本已让界面把历史本里的日期正常显示(标注「已观测消费 · 历史账户」),并在「小鲸鱼记账」面板顶部提示「检测到 N 个记账本」;**两个本不会被相加**(插件无法判断两次是不是同一个账户),但同一天的明细可以在该面板的「每日与逐条明细」里逐日查看。
427
431
  - **手机上拖不动鲸鱼**:本版本已用触摸事件接管拖拽,请硬刷新页面拿到最新 `whale-widget.js`;仍不行请反馈浏览器型号。
428
432
  - **隐藏了菜单按钮怎么进菜单**:电脑端右键鲸鱼,手机端长按鲸鱼约 1.5 秒。
@@ -448,6 +452,11 @@ curl http://127.0.0.1:3080/dsh-whale/audio.json
448
452
 
449
453
  ## 致谢
450
454
 
455
+ - **0.3.17 的「②区『提示音量』滑块其实不生效 + ②区『冒泡提示』没人读」**由一位 **QQ 群友**(未留 GitHub 账号)做了一轮完整的代码分析后报告:他复现了"拖到最低仍出声"、定位到 `playTaskEndSound()` 读的是①区全局音量、并附了可复用的补丁与两个预打版压缩包供对比。**这两个都是真 bug**(第二个是我顺着他的思路查出来的),0.3.17 已按"解析器 + 三态音量"的方式修复(比补丁覆盖面更广:**音效组那条路也修了**),并新增了一条 CI 检查专门拦这类"控件有值、没有消费方"的问题。感谢他的报告。
456
+ - **0.3.16 的「换 / 删 API key 后旧记账被分本隐藏」**(切换当天金额被覆盖、更早的天数退化成「本地估算」)由 GitHub 用户 [@0Sakura721](https://github.com/0Sakura721) 在 [#163](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/issues/163) 中报告,并**直接给出了根因位置**(`lib/index.js` 里用密钥哈希当账户标识、`accounting.mjs` 的 `observeBalance` / `currentBook` 只读 active 本)与数据佐证 —— 让这次能一次做实"数据没丢、只是没有入口"这个判断。0.3.16 据此修复(旧本日期照常显示并标注「已观测消费 · 历史账户」)。感谢他。
457
+ - **0.3.16 的「下载管理器抢走挂件音效请求」**(IDM 等每次打开 DSH 弹下载框、关音效也照样弹)由 GitHub 用户 [@VaeKaras](https://github.com/VaeKaras) 在 [#158](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/issues/158) 中报告,复现步骤、被抢的 URL 形态与"关音效无效"的观察都很完整 —— README 据此新增该已知问题与站点排除做法。感谢他。
458
+ - **官方桌面端「新装也可能需要重开客户端」**由 GitHub 用户 [@MengLs1620](https://github.com/MengLs1620) 在 [#162](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/issues/162) 中实测反馈,README 的生效方式据此补正。感谢他。
459
+ - **「让鲸鱼娘响应任务状态并给出音效提示」**由 GitHub 用户 [@Jy-EggRoll](https://github.com/Jy-EggRoll) 在 [#160](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/issues/160) 中提出,并**提交了实现 PR [#161](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/pull/161)**(其中 `GET /dsh-whale/wait.json` 的数据形状 `{ok, pending:{kind,id,ts}}` 与主线最终方案一致;本 README 的 `wait.json` 路由说明即采纳其写法)。0.3.16 的提问/授权提示音效与常驻气泡按主线方案落地(四入口面板 / 每事件音量与试听 / 抢占置顶)。感谢他,也欢迎他继续参与这个项目的工程化改进。
451
460
  - **0.3.15 的凭据安全修复**(写入自定义模型即可让宿主动用真实 API key 去请求任意地址,从而外带凭据)由 **B 站用户「星丶白羽莲」** 负责任地报告:把复现步骤、环境与前置条件一起给出,使这次能在**不打死自建网关这类正当用法**的前提下收紧边界。感谢他的支持。
452
461
  - **DSH 账号登录态读余额**(端点与鉴权头、凭据记录的位置、DSH 自身 `deepseekAccount` 服务的推荐调用方式,以及三个集成坑:账户标识字符集、赠金要计入基准、服务可能不存在)由 GitHub 用户 [@yybai25](https://github.com/yybai25) 在 [#157](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget/issues/157) 中给出完整规格与**已在自己机器上验证过的参考实现**;**0.3.14 据此实现**(只调 DSH 服务、不接触账号令牌)。感谢他的贡献。
453
462
  - 充值记账修复方案(余额上升与下降分开记账、显式余额校正公式、按账户/币种隔离观测窗口、账本原子写入与迁移备份)由 GitHub 用户 [@Yang-huai406](https://github.com/Yang-huai406) 独立设计并实现为可运行的修复分支;**0.3.1 在该方案基础上移植合并**,并保留本项目既有的音效修复。感谢他的支持。
@@ -1389,21 +1389,37 @@ function soundVolClamped(fallback) {
1389
1389
  if (!isFinite(v)) v = (typeof fallback === "number") ? fallback : 0.9
1390
1390
  return Math.max(0, Math.min(1, v))
1391
1391
  }
1392
+ // v778:**音量解析器 = 音量的唯一真源**。所有播放入口都必须经过它取音量(禁止各自去读全局),
1393
+ // 优先级:① 显式 override(绑定事件 / 试听传进来的值)→ ② 该事件**用户显式调过**的音量
1394
+ // (`cfg.volSet === true`)→ ③ ①区「按压音量」。
1395
+ // 为什么需要 volSet 这一层:出厂默认 `events.turnCost.vol = 1`,而面板每次保存都会把这个键写进磁盘
1396
+ // ⇒ 只看"有没有值"无法区分「默认 100%」和「用户设了 100%」;若让每事件无条件优先,会把
1397
+ // 「只调过①区、没碰过②区」的老用户的任务结束音静默抬到 100%(见 SPEC §24)。
1398
+ // ⚠️ 约定:本文件里 `soundVolClamped(` 只允许出现在本函数与它自己的定义处 —— 由 `_v778` 静态钉住。
1399
+ function soundVolumeOf(kind, override) {
1400
+ try {
1401
+ if (typeof override === 'number' && isFinite(override) && override >= 0) {
1402
+ return Math.max(0, Math.min(1, override))
1403
+ }
1404
+ if (kind) {
1405
+ var cfg = soundEventCfg(kind) || {}
1406
+ if (cfg.volSet === true) {
1407
+ var v = Number(cfg.vol)
1408
+ if (isFinite(v) && v >= 0) return Math.max(0, Math.min(1, v))
1409
+ }
1410
+ }
1411
+ return soundVolClamped(0.9)
1412
+ } catch (err) { return soundVolClamped(0.9) }
1413
+ }
1414
+ // v778:绑定音(按压 / 提问 / 授权 / 试听)—— 把 `{sel, vol}` **显式传进去**。
1415
+ // 旧实现靠"临时把全局 soundVol / usageSet.taskEnd 改掉、finally 再改回来"来传值;那正是
1416
+ // "每轮结束音读错音量"的同源隐患:任何新增播放入口忘了临时改一下,就会静默读全局(正是 0.3.16 的 bug)。
1392
1417
  function playBindingSound(bind, ev) {
1393
1418
  try {
1394
1419
  if (!bind || !bind.on || soundOn === false) return
1395
1420
  var sel = bind.sel || ''
1396
1421
  if (!sel) return // 防御:空选择不出声(v762 起下拉里已无「静音」项,正常不会再为空)
1397
- var savedTaskEnd = usageSet ? usageSet.taskEnd : undefined
1398
- var savedVol = soundVol
1399
- try {
1400
- if (usageSet) usageSet.taskEnd = { on: true, sel: sel }
1401
- if (ev && typeof ev.vol === 'number' && ev.vol >= 0) soundVol = ev.vol
1402
- playTaskEndSound()
1403
- } finally {
1404
- if (usageSet) usageSet.taskEnd = savedTaskEnd
1405
- soundVol = savedVol
1406
- }
1422
+ playTaskEndSound({ sel: sel, vol: (ev && typeof ev.vol === 'number') ? ev.vol : undefined })
1407
1423
  } catch (err) {}
1408
1424
  }
1409
1425
  // v761:等待交互时的**会话名**(wait.json 每秒轮询回填)+ 超长截断(默认 12 字符 + …)
@@ -1425,13 +1441,19 @@ function bubbleSessionText(m) {
1425
1441
  m = m || {}
1426
1442
  return bubbleSessionLabel(m.len === undefined || m.len === null ? WAIT_SESSION_MAX : m.len)
1427
1443
  }
1428
- function playTaskEndSound() {
1444
+ // v778:`opts` = { sel?, vol? } —— 传入 sel 表示"播这个绑定音效"(绕过 taskEnd 开关,因为它自带 on);
1445
+ // 不传 sel = 路径 A(每轮结束音),按 usageSet.taskEnd 的门控与选择。
1446
+ function playTaskEndSound(opts) {
1429
1447
  try {
1430
- if (!usageSet || !usageSet.taskEnd || !usageSet.taskEnd.on || soundOn === false) return
1431
- var sel = usageSet.taskEnd.sel || taskEndSel.value || ''
1448
+ var o = opts || {}
1449
+ if (soundOn === false) return
1450
+ if (!o.sel) {
1451
+ if (!usageSet || !usageSet.taskEnd || !usageSet.taskEnd.on) return
1452
+ }
1453
+ var sel = o.sel || usageSet.taskEnd.sel || taskEndSel.value || ''
1432
1454
  var url = ''
1433
1455
  var altUrl = ''
1434
- if (sel.indexOf('grp:') === 0) { playTaskEndGroupClick(sel.slice(4)); return }
1456
+ if (sel.indexOf('grp:') === 0) { playTaskEndGroupClick(sel.slice(4), o.vol); return }
1435
1457
  if (sel.indexOf('frag:') === 0) url = '/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(sel.slice(5))
1436
1458
  else if (sel.indexOf('preset:') === 0) {
1437
1459
  // v752:预设片段也改走片段路由(与本体按压同一套选择逻辑 + 备用路由)
@@ -1443,13 +1465,14 @@ function playTaskEndSound() {
1443
1465
  if (!url) return
1444
1466
  var a = dshwvSound(url)
1445
1467
  if (altUrl) a._alt = altUrl
1446
- try { a.volume = soundVolClamped(0.9) } catch (err) {}
1468
+ // v778:音量统一走解析器(②区「提示音量」显式设置过就用它,否则跟随①区「按压音量」)
1469
+ try { a.volume = soundVolumeOf('turnCost', o.vol) } catch (err) {}
1447
1470
  a.play().catch(function () {})
1448
1471
  } catch (err) {}
1449
1472
  }
1450
1473
  // 任务结束音=音效组时:模拟点按一次该组——音1完整播放结束后立即接音2,
1451
1474
  // 即“按下到音1结束才松开”的无缝连续点按听感;槽位留空(该事件静音)时跳过对应音频
1452
- function playTaskEndGroupClick(groupId) {
1475
+ function playTaskEndGroupClick(groupId, volOverride) {
1453
1476
  try {
1454
1477
  if (!groupId) return
1455
1478
  var g = null
@@ -1457,7 +1480,8 @@ function playTaskEndGroupClick(groupId) {
1457
1480
  var pressEmpty = !!(g && g.press === '') // 仅显式留空算静音;缺失字段(null)按旧数据回落预设
1458
1481
  var releaseEmpty = !!(g && g.release === '')
1459
1482
  if (pressEmpty && releaseEmpty) return
1460
- var vol = soundVolClamped(0.9)
1483
+ // v778:**"任务结束音 = 音效组"也必须走解析器**(第三方补丁只改了单片段那条路,音效组仍读①区 ⇒ 滑块照旧无效)
1484
+ var vol = soundVolumeOf('turnCost', volOverride)
1461
1485
  // v752:任务结束音同样改走片段路由(与本体按压同一套 URL 选择 + 备用路由逻辑)——
1462
1486
  // 老路由 /dsh-whale/sound/*.mp3?set=… 在部分环境会被本机那层东西拦成空的 204,
1463
1487
  // 不改的话"点按有声、但每轮结束音没声"会变成同一个问题的另一半。
@@ -1743,17 +1767,37 @@ function loadUsageSettings(cb) {
1743
1767
  .catch(function () { if (cb) cb() })
1744
1768
  } catch (err) { if (cb) cb() }
1745
1769
  }
1746
- function saveUsageSettings(patch) {
1747
- try {
1748
- fetch(USAGE_SET_URL, {
1749
- method: 'PUT',
1750
- headers: { 'Content-Type': 'application/json' },
1751
- body: JSON.stringify(patch || {}),
1770
+ // v778:与 `configPut()` **同一套语义**(读响应 → 失败静默重试一次 → 仍失败才提示用户)。
1771
+ // 旧实现只 `fetch(...).then(r=>r.json()).then(d=>{...}).catch(function(){})`:**不查 HTTP 状态码、
1772
+ // 失败完全静默** ⇒ 用户点了「保存」、面板正常关闭,其实一个字节都没写进磁盘(第三方分析报告点出的
1773
+ // 现象是真的,只是它把归因写成了"消耗数据丢存"——这个接口存的是**设置**,消耗记录由宿主自己记账)。
1774
+ function usageSettingsPut(patch, retried) {
1775
+ return fetch(USAGE_SET_URL, {
1776
+ method: 'PUT',
1777
+ headers: { 'Content-Type': 'application/json' },
1778
+ body: JSON.stringify(patch || {}),
1779
+ })
1780
+ .then(function (r) {
1781
+ return r.json().catch(function () { return null }).then(function (d) { return { ok: r.ok, status: r.status, d: d } })
1752
1782
  })
1753
- .then(function (r) { return r.json() })
1754
- .then(function (d) { if (d && d.ok && d.settings) usageSet = d.settings })
1755
- .catch(function () {})
1756
- } catch (err) {}
1783
+ .then(function (x) {
1784
+ if (x.ok && (!x.d || x.d.ok !== false)) {
1785
+ if (x.d && x.d.settings) usageSet = x.d.settings
1786
+ return true
1787
+ }
1788
+ if (!retried) return new Promise(function (res) { setTimeout(function () { res(usageSettingsPut(patch, true)) }, 900) })
1789
+ // 复用既有 toast(内部已对动态值做转义,见 v756/issue #143)
1790
+ configSaveFailNotice((x.d && x.d.error) || ('HTTP ' + x.status))
1791
+ return false
1792
+ })
1793
+ .catch(function (err) {
1794
+ if (!retried) return new Promise(function (res) { setTimeout(function () { res(usageSettingsPut(patch, true)) }, 900) })
1795
+ configSaveFailNotice((err && err.message) || err)
1796
+ return false
1797
+ })
1798
+ }
1799
+ function saveUsageSettings(patch) {
1800
+ try { return usageSettingsPut(patch, false) } catch (err) { return null }
1757
1801
  }
1758
1802
  // 把主菜单已有行收进 menuRootView;用量记录作为 menuBox 内的子视图切换
1759
1803
  var menuRootView = document.createElement('div')
@@ -2002,7 +2046,11 @@ function soundEventDefaults() {
2002
2046
  // ⚠️ v764:turnCost **也不再带** autoClose / ttlSec —— 它们从来没有消费方,而面板 ② 区
2003
2047
  // 「自动关闭」的真实落点是 .dshw-size.json 的 turnCostCloseMs(见 openSoundSettingsPanel)。
2004
2048
  // 给 turnCost 重新长出这两个键,会让同一个设置又变回两个来源(其中一个还是假的)。
2005
- turnCost: { vol: 1, bubbleOn: true },
2049
+ turnCost: { vol: 1, volSet: false, bubbleOn: true },
2050
+ // ⚠️ v778:`volSet` = "用户**显式**调过这一事件的音量"(默认 false)。它只表达"要不要覆盖①区",
2051
+ // 不是另一个音量来源 —— 取值永远看 `vol`。没有它就无法区分"默认 100%"与"用户设了 100%",
2052
+ // 会让"②区优先"静默改掉老用户的响度(详见 soundVolumeOf() 与 SPEC §24)。
2053
+ // ⚠️ 与宿主 soundEventsDefaults() 的同名字段**必须逐字段一致**(由探针 §⑥ 跨端一致钉住)。
2006
2054
  // v774:出厂默认 = 作者当前用法:提问/授权**开着**(要冒泡),但音效**不响**(soundOn: false)。
2007
2055
  // 要响就把音效行的 [✓] 勾上;不想冒泡就取消入口行的 [✓]。
2008
2056
  question: { on: true, soundOn: false, sel: 'frag:exp_orb', vol: 1, autoClose: true, ttlSec: 180, bubbleOn: true },
@@ -2029,6 +2077,9 @@ function soundEventOut(cfg, withBind) {
2029
2077
  vol: Number(cfg.vol) || 0,
2030
2078
  bubbleOn: cfg.bubbleOn !== false,
2031
2079
  }
2080
+ // v778:`volSet` 只属于 turnCost(= `withBind` 为假的那一个)。提问/授权的音量在"用户勾了音效开关"
2081
+ // 时才出声,不存在"默认值 vs 显式值"的歧义,所以不需要这个键。
2082
+ if (!withBind) o.volSet = cfg.volSet === true
2032
2083
  if (withBind) {
2033
2084
  // v764:autoClose / ttlSec 跟着 on/sel/soundOn 一起,只属于提问/授权。turnCost 的「自动关闭」
2034
2085
  // 真实落点是 .dsh-size.json 的 turnCostCloseMs、**不走 events 载荷** —— 别再无条件塞进去,
@@ -2067,6 +2118,11 @@ function openSoundSettingsPanel() {
2067
2118
  // 唯一消费点 sceneOpen('cost', …, turnCostCloseMs > 0 ? turnCostCloseMs : 0)),所以它也要进快照、
2068
2119
  // 也要能随「取消」还原(否则面板那行「自动关闭」就是个死控件)。
2069
2120
  var snapTurnCostCloseMs = Math.max(0, Math.round(Number(turnCostCloseMs) || 0))
2121
+ // v777(用户要求):底部新增「点按角色关闭提示气泡」——默认关(默认只有点泡泡能收起等待提示)。
2122
+ // 存在 usage.json 的 settings.wait.charClose(宿主 usageSettingsDefaults() 里默认 false);
2123
+ // 窗口期间只改缓冲,保存才 PUT,取消按快照还原,与其它设置项同一套语义。
2124
+ var bufWaitCharClose = !!(((usageSet || {}).wait || {}).charClose)
2125
+ var snapWaitCharClose = bufWaitCharClose
2070
2126
  // 内存缓冲:窗口期间只改这里
2071
2127
  var sndBuf = {
2072
2128
  turnCost: soundEventCfg('turnCost'),
@@ -2168,7 +2224,14 @@ function openSoundSettingsPanel() {
2168
2224
  var pre = (tcCloseBuf.autoClose !== false && sec > 0) ? ('自动关 ' + sec + 's') : '不自动关'
2169
2225
  // v768:静音时**不再显示已选的音效名**,直接告诉用户去哪儿改(用户要求的确切文案)
2170
2226
  txt = pre + ' · ' + (bufTaskEndOn ? (soundNameOf(e.sel) || '—') : '当前为静音,在下拉设置中修改')
2171
- if (bufTaskEndOn && sndPct(sndBuf.turnCost.vol) !== '100%') txt += ' · ' + sndPct(sndBuf.turnCost.vol)
2227
+ // v778:音量的两种状态写清楚 —— 没拖过 ②区滑块 = **跟随①区按压音量**;拖过 = 用这里的值。
2228
+ if (bufTaskEndOn) {
2229
+ if (sndBuf.turnCost.volSet === true) {
2230
+ if (sndPct(sndBuf.turnCost.vol) !== '100%') txt += ' · ' + sndPct(sndBuf.turnCost.vol)
2231
+ } else {
2232
+ txt += ' · 跟随按压音量 ' + sndPct(soundVol)
2233
+ }
2234
+ }
2172
2235
  }
2173
2236
  } else {
2174
2237
  var cfg = sndBuf[e.kind] || {}
@@ -2181,6 +2244,16 @@ function openSoundSettingsPanel() {
2181
2244
  }
2182
2245
  try { if (e.summary) e.summary.textContent = txt } catch (err) {}
2183
2246
  }
2247
+ // v778:②区「提示音量」滑块在**未显式设置**时显示"实际生效的音量"(= ①区按压音量),
2248
+ // 否则滑块停在 100% 而实际按 60% 播 —— 那就又变成"界面在骗人"(这次修的就是这类问题)。
2249
+ try {
2250
+ var tcCfg = sndBuf.turnCost || {}
2251
+ if (tcVol && tcVol.input && tcCfg.volSet !== true) {
2252
+ var eff = soundVolumeOf('turnCost')
2253
+ tcVol.input.value = String(eff)
2254
+ if (tcVol.pct) tcVol.pct.textContent = Math.round(eff * 100) + '%'
2255
+ }
2256
+ } catch (err) {}
2184
2257
  // v773:摘要与"置灰状态"永远一起刷(所有既有刷新点都会走到这里:开面板 / 卡内 input·change /
2185
2258
  // 点文档任意处 / 「恢复默认」末尾)—— 所以开关一动,相关设置立刻跟着灰/亮,不需要另挂监听。
2186
2259
  applySndDisabled()
@@ -2385,7 +2458,13 @@ function openSoundSettingsPanel() {
2385
2458
  fillSoundSelect(teSel, bufTaskEndSel)
2386
2459
  // 「静音」项删除后下拉不该留空:把兜底后的真实值回写缓冲(旧数据 sel 为空串时兜底成列表里的默认音)
2387
2460
  if (teSel.value) bufTaskEndSel = teSel.value
2388
- var tcVol = volSlider(function () { return sndBuf.turnCost.vol }, function (v) { sndBuf.turnCost.vol = v })
2461
+ // v778:②区「提示音量」—— 拖动即视为**用户显式设置**(volSet = true),此后该事件用自己的值;
2462
+ // 没拖过则保持"跟随①区按压音量"(见 soundVolumeOf),摘要里会写明,避免再出现"滑块看着能调、其实无效"。
2463
+ var tcVol = volSlider(function () { return sndBuf.turnCost.vol }, function (v) {
2464
+ sndBuf.turnCost.vol = v
2465
+ sndBuf.turnCost.volSet = true
2466
+ try { refreshSummaries() } catch (err) {}
2467
+ })
2389
2468
  var rTcVol = row('提示音量', tcEnt.body)
2390
2469
  rTcVol.appendChild(tcVol.input)
2391
2470
  rTcVol.appendChild(tcVol.pct)
@@ -2480,6 +2559,9 @@ function openSoundSettingsPanel() {
2480
2559
  setTurnCostClose(Math.round(snapTurnCostCloseMs / 1000))
2481
2560
  }
2482
2561
  } catch (err) {}
2562
+ // v777:底部「点按角色关闭提示气泡」——回滚缓冲与控件(它只活在缓冲里,没保存过就没落盘)
2563
+ try { bufWaitCharClose = snapWaitCharClose } catch (err) {}
2564
+ try { if (waitCharChk) waitCharChk.checked = snapWaitCharClose } catch (err) {}
2483
2565
  }
2484
2566
  try { audioGroupPanel.style.zIndex = '' } catch (err) {}
2485
2567
  try { audioEditMask.style.zIndex = '' } catch (err) {}
@@ -2540,7 +2622,12 @@ function openSoundSettingsPanel() {
2540
2622
  usageSet.taskEnd = te
2541
2623
  try { applyTaskEndLocal(te.on, te.sel) } catch (err) {}
2542
2624
  mergeEventsLocal(ev)
2543
- saveUsageSettings({ events: ev, taskEnd: te })
2625
+ // v777:底部「点按角色关闭提示气泡」跟着同一次 PUT 落盘(键 = settings.wait.charClose)
2626
+ try {
2627
+ usageSet = usageSet || {}
2628
+ usageSet.wait = Object.assign({}, usageSet.wait || {}, { charClose: !!bufWaitCharClose })
2629
+ } catch (err) {}
2630
+ saveUsageSettings({ events: ev, taskEnd: te, wait: { charClose: !!bufWaitCharClose } })
2544
2631
  // v763:按压音量 / 音效总开关 / 每轮消耗提示开关这三项**不在**上面这次 PUT 的覆盖范围里
2545
2632
  //(它们存在 .dsh-size.json,走 saveConfig())⇒ 保存时必须各自按**当前内存值**提交一次,
2546
2633
  // 否则「恢复默认」对它们不生效(用户会觉得恢复默认对音量/开关没作用)。
@@ -2601,6 +2688,11 @@ function openSoundSettingsPanel() {
2601
2688
  bufTaskEndSel = 'frag:end_a' // = 宿主 usageSettingsDefaults().taskEnd.sel
2602
2689
  bufTaskEndKeep = 'frag:end_a' // = 宿主 usageSettingsDefaults().taskEnd.sel
2603
2690
  bufTaskEndOn = true // v774:② 音效行的 [✓] 复位(宿主 taskEnd.on 默认 true)
2691
+ // v778:②区「提示音量」也复位成"跟随①区"(volSet=false;滑块显示由 refreshSummaries 同步成①区值)
2692
+ try { sndBuf.turnCost.volSet = false } catch (err) {}
2693
+ // v777:底部「点按角色关闭提示气泡」恢复出厂默认(false = 只有点泡泡能收)
2694
+ bufWaitCharClose = false
2695
+ try { if (waitCharChk) waitCharChk.checked = false } catch (err) {}
2604
2696
  // 控件回填
2605
2697
  try { pressOnChk.checked = true } catch (err) {}
2606
2698
  try { soundToggle.checked = true } catch (err) {} // v763:与 setSoundOn() 同步的那只开关(只改内存显示)
@@ -2639,6 +2731,54 @@ function openSoundSettingsPanel() {
2639
2731
  // · usageSet.events 的 lines(提问/授权提示内容)不再被「恢复默认」清空(与按钮 title 一致)
2640
2732
  } catch (err) {}
2641
2733
  }
2734
+ // v777(用户要求):「取消/恢复默认/保存」上面一行 —— 与「自定义泡泡」窗口的
2735
+ // 「点按角色推进泡泡队列」同款排版([✓] + 标签 + ? 圈圈),说明收进 ? 圈圈里(复用 v647 的 dshwvAskDot)。
2736
+ var waitCharRow = document.createElement('div')
2737
+ waitCharRow.className = 'dshwv-bubsec'
2738
+ waitCharRow.style.display = 'flex'
2739
+ waitCharRow.style.alignItems = 'center'
2740
+ waitCharRow.style.flexWrap = 'wrap'
2741
+ waitCharRow.style.gap = '4px 6px'
2742
+ waitCharRow.style.color = '#203170'
2743
+ waitCharRow.style.marginTop = '8px'
2744
+ // v777c(2026-09-27 追加要求):
2745
+ // ① 与上面那几行(入口行 `dshwvFoldEntry`,如「授权提示」)的复选框**左对齐** —— 入口行是
2746
+ // `rowEl.style.padding = '6px 6px'` 直接挂在 card 上 ⇒ 复选框在 6px 处;本行 `dshwv-bubsec`
2747
+ // 没有左内边距(默认 0)⇒ 补 6px 即对齐。(⚠️ 注意区分:展开后**折叠体里**的行额外有
2748
+ // `body.style.paddingLeft = 10px`,在 10px 处;本行是全局开关、不属于任何折叠体,故按入口行对齐。)
2749
+ // ② 本行**不要鼠标悬浮提示**(原生 title):问号圈圈自带的「查看说明」与复选框那句说明都清掉 ——
2750
+ // 说明统一收进问号圈圈的弹层里(悬停/点击问号时才显示),避免两个入口说两遍。
2751
+ waitCharRow.style.paddingLeft = '6px'
2752
+ var waitCharAsk = dshwvAskDot(
2753
+ '等待提问 / 授权的提示气泡:<b>默认点气泡就能收起来</b>(点一下收起,同一条挂起不会再自动弹回;' +
2754
+ '你回答或批准之后、或下一次新的提问 / 授权到来时,照常冒泡并响铃)。<br><br>' +
2755
+ '打开下面这个开关后,<b>点角色(鲸鱼)也能收掉提示气泡</b> —— 和「自定义泡泡」里的' +
2756
+ '「点按角色推进泡泡队列」是同一类手感。<br><br>默认关闭。'
2757
+ )
2758
+ // v777b(2026-09-27 追加要求):? 圈圈放到**文字后面**,并把与文字的间距收紧 ——
2759
+ // .dshwv-askq 自带 margin-right:5px,加上行的 column-gap 6px,放前面时离文字有 11px;
2760
+ // v777b 先用行内 margin-left:-3px 压到约 3px;
2761
+ // v777d(2026-09-27 追加要求「再稍稍右移」):改成 0 ⇒ 间距回到行的自然列间距 6px(右移 3px)。
2762
+ waitCharAsk.style.marginLeft = '0'
2763
+ waitCharAsk.style.marginRight = '0'
2764
+ waitCharAsk.title = '' // v777c:清掉 dshwvAskDot 自带的「查看说明」原生 tooltip(用户要求本行不要悬浮提示)
2765
+ var waitCharChk = document.createElement('input')
2766
+ waitCharChk.type = 'checkbox'
2767
+ waitCharChk.className = 'dshwv-check'
2768
+ waitCharChk.id = 'dshwv-waitcharclose'
2769
+ waitCharChk.checked = bufWaitCharClose
2770
+ // v777c:复选框也不挂原生 tooltip(原来那句"打开后:点角色也能收起…"已写进问号圈圈的说明里)
2771
+ waitCharChk.title = ''
2772
+ waitCharChk.addEventListener('change', function () { bufWaitCharClose = !!waitCharChk.checked })
2773
+ waitCharRow.appendChild(waitCharChk)
2774
+ var waitCharLab = document.createElement('label')
2775
+ waitCharLab.setAttribute('for', 'dshwv-waitcharclose')
2776
+ waitCharLab.style.cursor = 'pointer'
2777
+ waitCharLab.style.fontSize = '12px'
2778
+ waitCharLab.textContent = '点按角色关闭提示气泡'
2779
+ waitCharRow.appendChild(waitCharLab)
2780
+ waitCharRow.appendChild(waitCharAsk)
2781
+ card.appendChild(waitCharRow)
2642
2782
  var btns = document.createElement('div')
2643
2783
  btns.className = 'dshwv-bubbtns'
2644
2784
  var cancelBtn = document.createElement('button')
@@ -4854,7 +4994,8 @@ function resPlayFragment(fid) {
4854
4994
  if (!fid) return
4855
4995
  if (resAudEl) { try { resAudEl.pause() } catch (err) {} resAudEl = null }
4856
4996
  var a = dshwvSound('/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(fid))
4857
- try { a.volume = soundVolClamped(0.9) } catch (err) {}
4997
+ // v778:资源窗口的片段试听也走音量解析器(它是"这一条片段本身的试听",与按压音量同源)
4998
+ try { a.volume = soundVolumeOf('press') } catch (err) {}
4858
4999
  a.onended = function () { resAudEl = null }
4859
5000
  resAudEl = a
4860
5001
  a.play().catch(function () { resAudEl = null })
@@ -9522,19 +9663,31 @@ var bubbleTapAdvChk = document.createElement('input')
9522
9663
  bubbleTapAdvChk.type = 'checkbox'
9523
9664
  bubbleTapAdvChk.className = 'dshwv-check'
9524
9665
  bubbleTapAdvChk.id = 'dshwv-tapadv'
9525
- bubbleTapAdvChk.title = '开启后:点一下角色=往后推进一项(不再回到首次点击泡泡);走到最后一项再点=收起泡泡'
9666
+ // v777e(2026-09-27 用户要求):本行复选框**不再挂原生 tooltip**(原来那句"开启后:点一下角色=往后推进一项…"
9667
+ // 与问号圈圈里的说明重复)。说明统一收进 ? 圈圈的弹层里(与「提示与音效设置」面板底部那行一致)。
9668
+ bubbleTapAdvChk.title = ''
9526
9669
  var bubbleTapAdvLab = document.createElement('label')
9527
9670
  bubbleTapAdvLab.setAttribute('for', 'dshwv-tapadv')
9528
9671
  bubbleTapAdvLab.style.cursor = 'pointer'
9529
9672
  bubbleTapAdvLab.style.fontSize = '12px'
9530
9673
  bubbleTapAdvLab.textContent = '点按角色推进泡泡队列'
9531
- var bubbleTapAdvHint = document.createElement('span')
9532
- bubbleTapAdvHint.className = 'dshwv-bubhint'
9533
- bubbleTapAdvHint.style.margin = '0'
9534
- bubbleTapAdvHint.textContent = '(关闭=点角色回到第 1 个泡泡;开启=点一下往后一个)'
9674
+ // v777(用户要求):原来跟在标签后面的那一行说明字(「关闭=点角色回到第 1 个泡泡;开启=点一下往后一个」)
9675
+ // 收进问号圈圈里 —— 复用 v647 的 dshwvAskDot(悬停即显示、点击固定、触摸端点按开关),与
9676
+ // 「提示与音效设置」面板底部那一行同款。说明里顺带把原来 title 里的完整描述也写进去。
9677
+ // v777b(2026-09-27 追加要求):? 圈圈放在**文字后面**,并把与文字的间距收紧(同面板那处)。
9678
+ // v777d:「再稍稍右移」⇒ margin-left 由 -3px 改成 0(间距 = 行的自然列间距 6px)。
9679
+ var bubbleTapAdvAsk = dshwvAskDot(
9680
+ '关闭(默认):<b>点角色</b>=回到第 1 个泡泡(点泡泡才是往后推进)。<br><br>' +
9681
+ '开启:<b>点一下角色 = 往后推进一项</b>(不再回到首次点击泡泡);走到最后一项再点=收起泡泡。'
9682
+ )
9683
+ bubbleTapAdvAsk.style.marginLeft = '0'
9684
+ bubbleTapAdvAsk.style.marginRight = '0'
9685
+ // v777e:与面板底部那行同款处理 —— 清掉问号圈圈自带的「查看说明」原生 tooltip,
9686
+ // 说明只在悬停/点击问号时以自绘弹层出现(本行鼠标悬浮不再出现浏览器小黄条)。
9687
+ bubbleTapAdvAsk.title = ''
9535
9688
  bubbleTapAdvRow.appendChild(bubbleTapAdvChk)
9536
9689
  bubbleTapAdvRow.appendChild(bubbleTapAdvLab)
9537
- bubbleTapAdvRow.appendChild(bubbleTapAdvHint)
9690
+ bubbleTapAdvRow.appendChild(bubbleTapAdvAsk)
9538
9691
  bubbleCard.appendChild(bubbleTapAdvRow)
9539
9692
  // 按钮行
9540
9693
  var bubbleBtns = document.createElement('div')
@@ -13503,6 +13656,12 @@ function whaleClick() {
13503
13656
  bubbleShowSeqNext()
13504
13657
  return
13505
13658
  }
13659
+ // v777:等待交互提示期间点角色 —— 开关「点按角色关闭提示气泡」打开时,点角色=收起这条等待提示
13660
+ //(默认关:保持"点角色不动等待提示",只有点泡泡才关)。收起来后 pollWaitState 不会把它弹回来。
13661
+ if (bubbleScene && bubbleScene.kind === 'wait') {
13662
+ if (waitCharCloseOn()) dismissWaitBubble()
13663
+ return
13664
+ }
13506
13665
  // v727:开启「点按角色推进泡泡队列」→ 点角色=往后推进一项(不再回到第 1 项);
13507
13666
  // 已是最后一项时与「点泡泡」一致:收起泡泡,下次点按从第 1 项开始。
13508
13667
  if (bubbleTapAdvance) { bubbleNext(); return }
@@ -13523,9 +13682,12 @@ function bubbleNext() {
13523
13682
  if (!bubbleShown) return
13524
13683
  if (bubbleScene && bubbleScene.kind === 'cost') { hideCostBubble(); return }
13525
13684
  if (bubbleScene && bubbleScene.kind === 'alert') { hideUsageAlertBubble(); return }
13526
- // v761(#161 C5):等待交互泡泡必须**常驻到被回答/批准**——点它不关(点击泡泡 = 推进/收起是给
13527
- // 手动轮的语义);避免用户误点把「正在等待你的回答」收掉、而后台其实还挂着。
13528
- if (bubbleScene && bubbleScene.kind === 'wait') return
13685
+ // v777:等待交互泡泡**可以点掉**(用户反馈「无法点击关闭」)。原来的实现在这里是 `return`
13686
+ //(设计意图是"别让误点把'正在等待你回答'收掉"),但真机上用户就是想把它点掉 ⇒ 改成:
13687
+ // 点泡泡 = 收起,并记住"这条挂起已被点掉",pollWaitState 不再自动弹回(回答/批准或下一次新挂起照常)。
13688
+ if (bubbleScene && bubbleScene.kind === 'wait') { dismissWaitBubble(); return }
13689
+ // v761(#161 C5)曾在此处"等待交互泡泡点它不关"(必须等被回答/批准)——
13690
+ // v777 按用户反馈改成"可点关",语义与实现见上面的 dismissWaitBubble();这里不再拦截。
13529
13691
  if (bubbleRoundOn && bubbleSeqIdx < bubbleSeq.length) { bubbleShowSeqNext(); return }
13530
13692
  hideBubble()
13531
13693
  } catch (err) {}
@@ -13552,6 +13714,13 @@ function hideBubble() {
13552
13714
  }
13553
13715
  function showCostBubble(amount) {
13554
13716
  if (!bubbleOn || !turnCostOn) return
13717
+ // v778:②区「冒泡提示」勾选框(events.turnCost.bubbleOn)以前**只写不读** —— 取消勾选照样弹。
13718
+ // 同类问题("控件有值、没有消费方")与本次修的音量滑块同源,一并接上:
13719
+ // 缺省(未设置/旧数据)= 照常冒泡,只有**显式**取消勾选才不弹 ⇒ 老用户行为不变。
13720
+ try {
13721
+ var tcCfg = soundEventCfg('turnCost') || {}
13722
+ if (tcCfg.bubbleOn === false) return
13723
+ } catch (err) {}
13555
13724
  // 进入系统泡泡队列(等级3):若同批有预警/预算,则排在它们之后展示
13556
13725
  whaleSysPush({ kind: 'cost', amount: amount, rank: 3 })
13557
13726
  }
@@ -13769,6 +13938,23 @@ function hideWaitBubble() {
13769
13938
  if (whaleSysQueue.length !== beforeLen) whaleSysTick()
13770
13939
  } catch (err) {}
13771
13940
  }
13941
+ // v777(用户反馈「授权提示/提问提示无法点击关闭」):
13942
+ // · 默认:**点泡泡**即可关掉等待气泡(点角色不管,除非打开下面的开关);
13943
+ // · 开关「点按角色关闭提示气泡」打开后:**点角色**也能关;
13944
+ // · 关掉的是"这条挂起"——宿主每秒还会下发同一条挂起(pollWaitState 是每分钟…不是,是每秒对齐一次),
13945
+ // 所以必须记住"用户把这条点掉了、别再自动弹回",否则关掉后 1 秒内又回来 = 看起来根本关不掉。
13946
+ var waitDismissedId = '' // 被用户点掉的挂起 id('' = 没有)
13947
+ var waitPendingId = '' // 宿主最近一次上报的挂起 id(用于"点掉的就是当前这条")
13948
+ function waitCharCloseOn() {
13949
+ try { return !!(((usageSet || {}).wait || {}).charClose) } catch (err) { return false }
13950
+ }
13951
+ // 用户主动收起等待气泡:记住这条挂起已被点掉,然后收起
13952
+ function dismissWaitBubble() {
13953
+ try {
13954
+ if (waitPendingId) waitDismissedId = waitPendingId
13955
+ hideWaitBubble()
13956
+ } catch (err) {}
13957
+ }
13772
13958
  function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v) }
13773
13959
  function viewport() {
13774
13960
  return {
@@ -16783,17 +16969,27 @@ function pollWaitState() {
16783
16969
  if (!p || !p.kind) {
16784
16970
  // 挂起已解除:收起泡泡,并允许下一次挂起照常响
16785
16971
  waitSeenId = ''
16972
+ waitPendingId = '' // v777:挂起解除 ⇒ "被点掉"的标记一并作废(下一次挂起照常冒泡)
16973
+ waitDismissedId = ''
16786
16974
  if (typeof hideWaitBubble === 'function') hideWaitBubble()
16787
16975
  return
16788
16976
  }
16789
16977
  var kind = p.kind === 'approval' ? 'approval' : 'question'
16790
16978
  var ev = waitEventCfg(kind)
16979
+ // v777:这条挂起已经被用户点掉了 ⇒ 不再自动弹回(但仍然照常响铃判定与去重;回答/批准后
16980
+ // 上面的 !p 分支会把标记清掉,所以"下一次新的提问/授权"照常冒泡)。
16981
+ waitPendingId = String(p.id || '')
16982
+ if (waitPendingId && waitPendingId === waitDismissedId) {
16983
+ // 什么都不做:既不显示,也不清 waitSeenId(同一挂起不重复响)
16984
+ } else {
16985
+ if (ev.bubbleOn !== false && typeof showWaitBubble === 'function') showWaitBubble(kind, p)
16986
+ }
16791
16987
  // v771:**每秒都把泡泡与宿主的挂起状态对齐一次**(showWaitBubble 幂等:同类直接返回 false,
16792
16988
  // 不会重渲染)。旧实现只在"新 id"时才调显示,于是一旦出现瞬时不同步(换类型 / 泡泡被别的
16793
16989
  // 场景顶掉 / 某次渲染失败),泡泡就会卡在那个状态再也回不来 —— 这正是真机那次卡死的另一半原因。
16794
16990
  // 现在任何不同步都会在 1 秒内自愈:该换内容就换、该收就收(收在下面 !p 分支)。
16795
16991
  // 门控照旧:该事件的「冒泡提示」关掉就不冒泡(与声音各自独立判)。
16796
- if (ev.bubbleOn !== false && typeof showWaitBubble === 'function') showWaitBubble(kind, p)
16992
+ // v777:显示那一步已上移到"被点掉的挂起"判定里(见上面的 waitDismissedId 分支),这里不再重复调用。
16797
16993
  // 声音:同一个未回答的挂起只响一次(刷新页面也不重复响)
16798
16994
  var key = kind + ':' + String(p.id || '')
16799
16995
  var remembered = ''
package/lib/index.js CHANGED
@@ -1285,7 +1285,7 @@ export default {
1285
1285
  const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
1286
1286
  for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
1287
1287
  return {
1288
- ok: true, version: '0.3.16', today: todayData, days7,
1288
+ ok: true, version: '0.3.17', today: todayData, days7,
1289
1289
  total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
1290
1290
  all: {
1291
1291
  days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
@@ -1321,7 +1321,10 @@ export default {
1321
1321
  // (只冒泡)—— 要响就把音效行的 [✓] 勾上。
1322
1322
  // v764:turnCost **刻意不带** autoClose / ttlSec —— 自动关闭的真实来源是 .dshw-size.json 的
1323
1323
  // turnCostCloseMs,留着这两个键会让同一个设置有两个来源(其中一个永远是假的)。
1324
- turnCost: { vol: 1, bubbleOn: true },
1324
+ // v778:`volSet` = 用户**显式**调过②区「提示音量」没有(默认 false = 跟随①区按压音量)。
1325
+ // 没有它就无法区分"默认 100%"与"用户设了 100%",前端 soundVolumeOf() 靠它决定优先级。
1326
+ // ⚠️ 与前端 soundEventDefaults().turnCost **必须逐字段一致**(探针 §⑥ 跨端一致钉住)。
1327
+ turnCost: { vol: 1, volSet: false, bubbleOn: true },
1325
1328
  question: { on: true, soundOn: false, sel: 'frag:exp_orb', vol: 1, autoClose: true, ttlSec: 180, bubbleOn: true, lines: waitDefaultLines('question') },
1326
1329
  approval: { on: true, soundOn: false, sel: 'frag:exp_orb', vol: 1, autoClose: true, ttlSec: 180, bubbleOn: true, lines: waitDefaultLines('approval') },
1327
1330
  }
@@ -1329,6 +1332,10 @@ export default {
1329
1332
  function usageSettingsDefaults() {
1330
1333
  return { // 任务结束音:v774 起出厂默认 = **开**(作者当前用法),默认音 = 内置 A(end_a)
1331
1334
  taskEnd: { on: true, sel: 'frag:end_a' },
1335
+ // v777:「等待交互提示」的交互开关(用户 2026-09-26 要求)
1336
+ // charClose=false(出厂默认)= 只有**点泡泡**能收起等待提示;true = **点角色**也能收起。
1337
+ // 两种情况下"点掉的那条挂起"都不会再被每秒轮询自动弹回(前端 waitDismissedId)。
1338
+ wait: { charClose: false },
1332
1339
  // v761:四个事件的音效/自动关闭/冒泡配置(面板「全局音效设置」)
1333
1340
  events: soundEventsDefaults(),
1334
1341
  // 默认固化自开发环境当前 usage.json 设置(全新安装即此体验)
@@ -1411,6 +1418,8 @@ export default {
1411
1418
  if (p.budget && typeof p.budget === 'object') led.settings.budget = Object.assign({}, led.settings.budget || {}, p.budget)
1412
1419
  // 每轮消耗提示内容(自定义提示窗口)
1413
1420
  if (p.turnCost && typeof p.turnCost === 'object') led.settings.turnCost = Object.assign({}, led.settings.turnCost || {}, p.turnCost)
1421
+ // v777:「等待交互提示」的交互开关(点角色是否也能关闭等待提示气泡)
1422
+ if (p.wait && typeof p.wait === 'object') led.settings.wait = Object.assign({}, led.settings.wait || {}, p.wait)
1414
1423
  // v761(全局音效设置):events 补丁逐事件合并(前端只发改动的那几个字段)
1415
1424
  if (p.events && typeof p.events === 'object') {
1416
1425
  led.settings.events = led.settings.events && typeof led.settings.events === 'object' ? led.settings.events : {}
@@ -1426,6 +1435,8 @@ export default {
1426
1435
  // 「恢复默认」:只重置音效/提示类键(不动外观、位置、账本、角色与泡泡自定义)
1427
1436
  if (p.resetEvents === true) {
1428
1437
  led.settings.events = soundEventsDefaults()
1438
+ // v777:等待提示的交互开关也属于"提示类键" ⇒ 一起恢复出厂默认(charClose=false)
1439
+ led.settings.wait = { charClose: false }
1429
1440
  // 任务结束音也属于「音效类键」:宿主重置与面板显示必须口径一致(否则面板显示与真实状态撕裂)
1430
1441
  // v774:重置目标 = 新的出厂默认(开 + 内置 A)
1431
1442
  led.settings.taskEnd = { on: true, sel: 'frag:end_a' }
@@ -2624,7 +2635,7 @@ export default {
2624
2635
  const summary = daySummary(led, todayKey())
2625
2636
  const nowSec = Math.floor(Date.now() / 1000)
2626
2637
  return {
2627
- ...visible, version: '0.3.16', isPeak: isPeakTime(nowSec),
2638
+ ...visible, version: '0.3.17', isPeak: isPeakTime(nowSec),
2628
2639
  // 峰谷切换点与节假日清单:前端倒计时要与宿主同源(否则法定节假日会算错切换点)
2629
2640
  peakNextChangeAt: nextPeakChangeAt(nowSec),
2630
2641
  peakHolidays: HOLIDAY_VALLEY_LIST,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-widget",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义",
5
5
  "keywords": [
6
6
  "dsh",