dsh-whale-widget 0.3.16 → 0.3.18
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 +12 -2
- package/assets/whale-widget.js +508 -67
- package/lib/accounting.mjs +39 -2
- package/lib/index.js +286 -41
- package/package.json +1 -1
package/assets/whale-widget.js
CHANGED
|
@@ -308,8 +308,26 @@ function dshwvSound(url) {
|
|
|
308
308
|
|
|
309
309
|
// 自动播放策略:AudioContext 初始是 suspended,要有一次用户手势才能出声;任务结束音不是手势触发的,
|
|
310
310
|
// 所以挂一次性解锁(首次点击/按键后移除)。
|
|
311
|
+
// v789(issue #179):**输入法组字期间一律不做事**。用户实测"装了插件后微软拼音打字会自动上屏、
|
|
312
|
+
// 光标跳到最前端"(0.3.16/0.3.17 均复现,卸载即恢复)。我们能做的、也是最有把握的一条,是
|
|
313
|
+
// 让挂件在 composition 期间**完全不碰 DOM、不做重活**:往 document.body 挂节点会触发布局,
|
|
314
|
+
// 而首次按键同步创建/预热 AudioContext 也会让组字被打断。所以:
|
|
315
|
+
// ① 记录 composition 状态,组字中的 keydown 不解锁音频;
|
|
316
|
+
// ② 组字期间所有 body 增删一律排队,等 compositionend 之后再补(见 dshwBodyAppend)。
|
|
317
|
+
var dshwvComposing = false
|
|
318
|
+
var dshwvDeferredBody = []
|
|
319
|
+
function dshwvComposingNow() { return dshwvComposing === true }
|
|
311
320
|
try {
|
|
312
|
-
|
|
321
|
+
document.addEventListener('compositionstart', function () { dshwvComposing = true }, true)
|
|
322
|
+
document.addEventListener('compositionend', function () {
|
|
323
|
+
dshwvComposing = false
|
|
324
|
+
setTimeout(function () { try { dshwvFlushDeferredBody() } catch (err) {} }, 0)
|
|
325
|
+
}, true)
|
|
326
|
+
} catch (err) {}
|
|
327
|
+
try {
|
|
328
|
+
var dshwvAudioUnlock = function (ev) {
|
|
329
|
+
// v789(issue #179):组字中的按键不做音频解锁(isComposing / keyCode 229 = IME 处理中)
|
|
330
|
+
try { if (dshwvComposing || (ev && (ev.isComposing === true || ev.keyCode === 229))) return } catch (err) {}
|
|
313
331
|
// v753(issue #135):音效关掉时**不预解锁** —— 否则一次普通点击就会把 context 转成 running,
|
|
314
332
|
// 断言照挂。注意这里**不摘监听**:之后重新打开开关,下一次点击仍能完成解锁。
|
|
315
333
|
if (dshwvSoundOff()) return
|
|
@@ -891,9 +909,23 @@ document.head.appendChild(styleEl)
|
|
|
891
909
|
// 所以这里统一登记:所有挂到 body 的节点都走 dshwBodyAppend(),守护时逐个补挂;
|
|
892
910
|
// 主动移除(目前只有一处)走 dshwBodyDetach(),避免被守护逻辑"复活"。
|
|
893
911
|
var dshwBodyNodes = []
|
|
912
|
+
// v789(issue #179):输入法组字期间把"往 body 挂节点"的请求排队 —— 改 document.body 的子节点会触发布局,
|
|
913
|
+
// 在 composition 期间做这件事会让浏览器/输入法重置组字(用户实测:打字自动上屏、光标跳到最前端)。
|
|
914
|
+
// 排队到 compositionend 之后再补挂(正常输入时行为完全不变:非组字状态下一律立即挂)。
|
|
915
|
+
function dshwvFlushDeferredBody() {
|
|
916
|
+
try {
|
|
917
|
+
if (dshwvComposing || !dshwvDeferredBody.length) return
|
|
918
|
+
var list = dshwvDeferredBody.slice()
|
|
919
|
+
dshwvDeferredBody.length = 0
|
|
920
|
+
for (var i = 0; i < list.length; i++) {
|
|
921
|
+
try { if (list[i] && list[i].__detach) dshwBodyDetach(list[i].el); else dshwBodyAppend(list[i]) } catch (err) {}
|
|
922
|
+
}
|
|
923
|
+
} catch (err) {}
|
|
924
|
+
}
|
|
894
925
|
function dshwBodyAppend(el) {
|
|
895
926
|
try {
|
|
896
927
|
if (!el) return el
|
|
928
|
+
if (dshwvComposing && dshwvDeferredBody.indexOf(el) < 0) { dshwvDeferredBody.push(el); return el }
|
|
897
929
|
// 注意:这里必须是**原始**的 document.body.appendChild —— 不能走 dshwBodyAppend 自己
|
|
898
930
|
// (v743 批量改写时曾误替换成自我递归,被 try/catch 吞掉后表现为"登记了但从未挂上")
|
|
899
931
|
document.body.appendChild(el)
|
|
@@ -903,6 +935,8 @@ function dshwBodyAppend(el) {
|
|
|
903
935
|
}
|
|
904
936
|
function dshwBodyDetach(el) {
|
|
905
937
|
try {
|
|
938
|
+
// v789(issue #179):组字期间连"移除"也排队(同样是 body 子节点变更,会打断组字)
|
|
939
|
+
if (dshwvComposing) { dshwvDeferredBody.push({ __detach: true, el: el }); return }
|
|
906
940
|
var i = dshwBodyNodes.indexOf(el)
|
|
907
941
|
if (i >= 0) dshwBodyNodes.splice(i, 1)
|
|
908
942
|
if (el && el.parentNode) el.parentNode.removeChild(el)
|
|
@@ -1038,6 +1072,8 @@ if (!window.__dshwCustBound) {
|
|
|
1038
1072
|
dshwCustSelClose()
|
|
1039
1073
|
}, true)
|
|
1040
1074
|
document.addEventListener('keydown', function (e) {
|
|
1075
|
+
// v789(issue #179):组字中按 Esc 是输入法的「取消组字」,别抢
|
|
1076
|
+
if (dshwvComposingNow()) return
|
|
1041
1077
|
if (e.key === 'Escape') dshwCustCloseNow()
|
|
1042
1078
|
}, true)
|
|
1043
1079
|
window.addEventListener('resize', function () { dshwCustCloseNow() })
|
|
@@ -1389,21 +1425,37 @@ function soundVolClamped(fallback) {
|
|
|
1389
1425
|
if (!isFinite(v)) v = (typeof fallback === "number") ? fallback : 0.9
|
|
1390
1426
|
return Math.max(0, Math.min(1, v))
|
|
1391
1427
|
}
|
|
1428
|
+
// v778:**音量解析器 = 音量的唯一真源**。所有播放入口都必须经过它取音量(禁止各自去读全局),
|
|
1429
|
+
// 优先级:① 显式 override(绑定事件 / 试听传进来的值)→ ② 该事件**用户显式调过**的音量
|
|
1430
|
+
// (`cfg.volSet === true`)→ ③ ①区「按压音量」。
|
|
1431
|
+
// 为什么需要 volSet 这一层:出厂默认 `events.turnCost.vol = 1`,而面板每次保存都会把这个键写进磁盘
|
|
1432
|
+
// ⇒ 只看"有没有值"无法区分「默认 100%」和「用户设了 100%」;若让每事件无条件优先,会把
|
|
1433
|
+
// 「只调过①区、没碰过②区」的老用户的任务结束音静默抬到 100%(见 SPEC §24)。
|
|
1434
|
+
// ⚠️ 约定:本文件里 `soundVolClamped(` 只允许出现在本函数与它自己的定义处 —— 由 `_v778` 静态钉住。
|
|
1435
|
+
function soundVolumeOf(kind, override) {
|
|
1436
|
+
try {
|
|
1437
|
+
if (typeof override === 'number' && isFinite(override) && override >= 0) {
|
|
1438
|
+
return Math.max(0, Math.min(1, override))
|
|
1439
|
+
}
|
|
1440
|
+
if (kind) {
|
|
1441
|
+
var cfg = soundEventCfg(kind) || {}
|
|
1442
|
+
if (cfg.volSet === true) {
|
|
1443
|
+
var v = Number(cfg.vol)
|
|
1444
|
+
if (isFinite(v) && v >= 0) return Math.max(0, Math.min(1, v))
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return soundVolClamped(0.9)
|
|
1448
|
+
} catch (err) { return soundVolClamped(0.9) }
|
|
1449
|
+
}
|
|
1450
|
+
// v778:绑定音(按压 / 提问 / 授权 / 试听)—— 把 `{sel, vol}` **显式传进去**。
|
|
1451
|
+
// 旧实现靠"临时把全局 soundVol / usageSet.taskEnd 改掉、finally 再改回来"来传值;那正是
|
|
1452
|
+
// "每轮结束音读错音量"的同源隐患:任何新增播放入口忘了临时改一下,就会静默读全局(正是 0.3.16 的 bug)。
|
|
1392
1453
|
function playBindingSound(bind, ev) {
|
|
1393
1454
|
try {
|
|
1394
1455
|
if (!bind || !bind.on || soundOn === false) return
|
|
1395
1456
|
var sel = bind.sel || ''
|
|
1396
1457
|
if (!sel) return // 防御:空选择不出声(v762 起下拉里已无「静音」项,正常不会再为空)
|
|
1397
|
-
|
|
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
|
-
}
|
|
1458
|
+
playTaskEndSound({ sel: sel, vol: (ev && typeof ev.vol === 'number') ? ev.vol : undefined })
|
|
1407
1459
|
} catch (err) {}
|
|
1408
1460
|
}
|
|
1409
1461
|
// v761:等待交互时的**会话名**(wait.json 每秒轮询回填)+ 超长截断(默认 12 字符 + …)
|
|
@@ -1425,13 +1477,19 @@ function bubbleSessionText(m) {
|
|
|
1425
1477
|
m = m || {}
|
|
1426
1478
|
return bubbleSessionLabel(m.len === undefined || m.len === null ? WAIT_SESSION_MAX : m.len)
|
|
1427
1479
|
}
|
|
1428
|
-
|
|
1480
|
+
// v778:`opts` = { sel?, vol? } —— 传入 sel 表示"播这个绑定音效"(绕过 taskEnd 开关,因为它自带 on);
|
|
1481
|
+
// 不传 sel = 路径 A(每轮结束音),按 usageSet.taskEnd 的门控与选择。
|
|
1482
|
+
function playTaskEndSound(opts) {
|
|
1429
1483
|
try {
|
|
1430
|
-
|
|
1431
|
-
|
|
1484
|
+
var o = opts || {}
|
|
1485
|
+
if (soundOn === false) return
|
|
1486
|
+
if (!o.sel) {
|
|
1487
|
+
if (!usageSet || !usageSet.taskEnd || !usageSet.taskEnd.on) return
|
|
1488
|
+
}
|
|
1489
|
+
var sel = o.sel || usageSet.taskEnd.sel || taskEndSel.value || ''
|
|
1432
1490
|
var url = ''
|
|
1433
1491
|
var altUrl = ''
|
|
1434
|
-
if (sel.indexOf('grp:') === 0) { playTaskEndGroupClick(sel.slice(4)); return }
|
|
1492
|
+
if (sel.indexOf('grp:') === 0) { playTaskEndGroupClick(sel.slice(4), o.vol); return }
|
|
1435
1493
|
if (sel.indexOf('frag:') === 0) url = '/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(sel.slice(5))
|
|
1436
1494
|
else if (sel.indexOf('preset:') === 0) {
|
|
1437
1495
|
// v752:预设片段也改走片段路由(与本体按压同一套选择逻辑 + 备用路由)
|
|
@@ -1443,13 +1501,14 @@ function playTaskEndSound() {
|
|
|
1443
1501
|
if (!url) return
|
|
1444
1502
|
var a = dshwvSound(url)
|
|
1445
1503
|
if (altUrl) a._alt = altUrl
|
|
1446
|
-
|
|
1504
|
+
// v778:音量统一走解析器(②区「提示音量」显式设置过就用它,否则跟随①区「按压音量」)
|
|
1505
|
+
try { a.volume = soundVolumeOf('turnCost', o.vol) } catch (err) {}
|
|
1447
1506
|
a.play().catch(function () {})
|
|
1448
1507
|
} catch (err) {}
|
|
1449
1508
|
}
|
|
1450
1509
|
// 任务结束音=音效组时:模拟点按一次该组——音1完整播放结束后立即接音2,
|
|
1451
1510
|
// 即“按下到音1结束才松开”的无缝连续点按听感;槽位留空(该事件静音)时跳过对应音频
|
|
1452
|
-
function playTaskEndGroupClick(groupId) {
|
|
1511
|
+
function playTaskEndGroupClick(groupId, volOverride) {
|
|
1453
1512
|
try {
|
|
1454
1513
|
if (!groupId) return
|
|
1455
1514
|
var g = null
|
|
@@ -1457,7 +1516,8 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1457
1516
|
var pressEmpty = !!(g && g.press === '') // 仅显式留空算静音;缺失字段(null)按旧数据回落预设
|
|
1458
1517
|
var releaseEmpty = !!(g && g.release === '')
|
|
1459
1518
|
if (pressEmpty && releaseEmpty) return
|
|
1460
|
-
|
|
1519
|
+
// v778:**"任务结束音 = 音效组"也必须走解析器**(第三方补丁只改了单片段那条路,音效组仍读①区 ⇒ 滑块照旧无效)
|
|
1520
|
+
var vol = soundVolumeOf('turnCost', volOverride)
|
|
1461
1521
|
// v752:任务结束音同样改走片段路由(与本体按压同一套 URL 选择 + 备用路由逻辑)——
|
|
1462
1522
|
// 老路由 /dsh-whale/sound/*.mp3?set=… 在部分环境会被本机那层东西拦成空的 204,
|
|
1463
1523
|
// 不改的话"点按有声、但每轮结束音没声"会变成同一个问题的另一半。
|
|
@@ -1743,17 +1803,37 @@ function loadUsageSettings(cb) {
|
|
|
1743
1803
|
.catch(function () { if (cb) cb() })
|
|
1744
1804
|
} catch (err) { if (cb) cb() }
|
|
1745
1805
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1806
|
+
// v778:与 `configPut()` **同一套语义**(读响应 → 失败静默重试一次 → 仍失败才提示用户)。
|
|
1807
|
+
// 旧实现只 `fetch(...).then(r=>r.json()).then(d=>{...}).catch(function(){})`:**不查 HTTP 状态码、
|
|
1808
|
+
// 失败完全静默** ⇒ 用户点了「保存」、面板正常关闭,其实一个字节都没写进磁盘(第三方分析报告点出的
|
|
1809
|
+
// 现象是真的,只是它把归因写成了"消耗数据丢存"——这个接口存的是**设置**,消耗记录由宿主自己记账)。
|
|
1810
|
+
function usageSettingsPut(patch, retried) {
|
|
1811
|
+
return fetch(USAGE_SET_URL, {
|
|
1812
|
+
method: 'PUT',
|
|
1813
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1814
|
+
body: JSON.stringify(patch || {}),
|
|
1815
|
+
})
|
|
1816
|
+
.then(function (r) {
|
|
1817
|
+
return r.json().catch(function () { return null }).then(function (d) { return { ok: r.ok, status: r.status, d: d } })
|
|
1818
|
+
})
|
|
1819
|
+
.then(function (x) {
|
|
1820
|
+
if (x.ok && (!x.d || x.d.ok !== false)) {
|
|
1821
|
+
if (x.d && x.d.settings) usageSet = x.d.settings
|
|
1822
|
+
return true
|
|
1823
|
+
}
|
|
1824
|
+
if (!retried) return new Promise(function (res) { setTimeout(function () { res(usageSettingsPut(patch, true)) }, 900) })
|
|
1825
|
+
// 复用既有 toast(内部已对动态值做转义,见 v756/issue #143)
|
|
1826
|
+
configSaveFailNotice((x.d && x.d.error) || ('HTTP ' + x.status))
|
|
1827
|
+
return false
|
|
1828
|
+
})
|
|
1829
|
+
.catch(function (err) {
|
|
1830
|
+
if (!retried) return new Promise(function (res) { setTimeout(function () { res(usageSettingsPut(patch, true)) }, 900) })
|
|
1831
|
+
configSaveFailNotice((err && err.message) || err)
|
|
1832
|
+
return false
|
|
1752
1833
|
})
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
} catch (err) {}
|
|
1834
|
+
}
|
|
1835
|
+
function saveUsageSettings(patch) {
|
|
1836
|
+
try { return usageSettingsPut(patch, false) } catch (err) { return null }
|
|
1757
1837
|
}
|
|
1758
1838
|
// 把主菜单已有行收进 menuRootView;用量记录作为 menuBox 内的子视图切换
|
|
1759
1839
|
var menuRootView = document.createElement('div')
|
|
@@ -2002,7 +2082,11 @@ function soundEventDefaults() {
|
|
|
2002
2082
|
// ⚠️ v764:turnCost **也不再带** autoClose / ttlSec —— 它们从来没有消费方,而面板 ② 区
|
|
2003
2083
|
// 「自动关闭」的真实落点是 .dshw-size.json 的 turnCostCloseMs(见 openSoundSettingsPanel)。
|
|
2004
2084
|
// 给 turnCost 重新长出这两个键,会让同一个设置又变回两个来源(其中一个还是假的)。
|
|
2005
|
-
turnCost: { vol: 1, bubbleOn: true },
|
|
2085
|
+
turnCost: { vol: 1, volSet: false, bubbleOn: true },
|
|
2086
|
+
// ⚠️ v778:`volSet` = "用户**显式**调过这一事件的音量"(默认 false)。它只表达"要不要覆盖①区",
|
|
2087
|
+
// 不是另一个音量来源 —— 取值永远看 `vol`。没有它就无法区分"默认 100%"与"用户设了 100%",
|
|
2088
|
+
// 会让"②区优先"静默改掉老用户的响度(详见 soundVolumeOf() 与 SPEC §24)。
|
|
2089
|
+
// ⚠️ 与宿主 soundEventsDefaults() 的同名字段**必须逐字段一致**(由探针 §⑥ 跨端一致钉住)。
|
|
2006
2090
|
// v774:出厂默认 = 作者当前用法:提问/授权**开着**(要冒泡),但音效**不响**(soundOn: false)。
|
|
2007
2091
|
// 要响就把音效行的 [✓] 勾上;不想冒泡就取消入口行的 [✓]。
|
|
2008
2092
|
question: { on: true, soundOn: false, sel: 'frag:exp_orb', vol: 1, autoClose: true, ttlSec: 180, bubbleOn: true },
|
|
@@ -2029,6 +2113,9 @@ function soundEventOut(cfg, withBind) {
|
|
|
2029
2113
|
vol: Number(cfg.vol) || 0,
|
|
2030
2114
|
bubbleOn: cfg.bubbleOn !== false,
|
|
2031
2115
|
}
|
|
2116
|
+
// v778:`volSet` 只属于 turnCost(= `withBind` 为假的那一个)。提问/授权的音量在"用户勾了音效开关"
|
|
2117
|
+
// 时才出声,不存在"默认值 vs 显式值"的歧义,所以不需要这个键。
|
|
2118
|
+
if (!withBind) o.volSet = cfg.volSet === true
|
|
2032
2119
|
if (withBind) {
|
|
2033
2120
|
// v764:autoClose / ttlSec 跟着 on/sel/soundOn 一起,只属于提问/授权。turnCost 的「自动关闭」
|
|
2034
2121
|
// 真实落点是 .dsh-size.json 的 turnCostCloseMs、**不走 events 载荷** —— 别再无条件塞进去,
|
|
@@ -2067,6 +2154,11 @@ function openSoundSettingsPanel() {
|
|
|
2067
2154
|
// 唯一消费点 sceneOpen('cost', …, turnCostCloseMs > 0 ? turnCostCloseMs : 0)),所以它也要进快照、
|
|
2068
2155
|
// 也要能随「取消」还原(否则面板那行「自动关闭」就是个死控件)。
|
|
2069
2156
|
var snapTurnCostCloseMs = Math.max(0, Math.round(Number(turnCostCloseMs) || 0))
|
|
2157
|
+
// v777(用户要求):底部新增「点按角色关闭提示气泡」——默认关(默认只有点泡泡能收起等待提示)。
|
|
2158
|
+
// 存在 usage.json 的 settings.wait.charClose(宿主 usageSettingsDefaults() 里默认 false);
|
|
2159
|
+
// 窗口期间只改缓冲,保存才 PUT,取消按快照还原,与其它设置项同一套语义。
|
|
2160
|
+
var bufWaitCharClose = !!(((usageSet || {}).wait || {}).charClose)
|
|
2161
|
+
var snapWaitCharClose = bufWaitCharClose
|
|
2070
2162
|
// 内存缓冲:窗口期间只改这里
|
|
2071
2163
|
var sndBuf = {
|
|
2072
2164
|
turnCost: soundEventCfg('turnCost'),
|
|
@@ -2168,7 +2260,14 @@ function openSoundSettingsPanel() {
|
|
|
2168
2260
|
var pre = (tcCloseBuf.autoClose !== false && sec > 0) ? ('自动关 ' + sec + 's') : '不自动关'
|
|
2169
2261
|
// v768:静音时**不再显示已选的音效名**,直接告诉用户去哪儿改(用户要求的确切文案)
|
|
2170
2262
|
txt = pre + ' · ' + (bufTaskEndOn ? (soundNameOf(e.sel) || '—') : '当前为静音,在下拉设置中修改')
|
|
2171
|
-
|
|
2263
|
+
// v778:音量的两种状态写清楚 —— 没拖过 ②区滑块 = **跟随①区按压音量**;拖过 = 用这里的值。
|
|
2264
|
+
if (bufTaskEndOn) {
|
|
2265
|
+
if (sndBuf.turnCost.volSet === true) {
|
|
2266
|
+
if (sndPct(sndBuf.turnCost.vol) !== '100%') txt += ' · ' + sndPct(sndBuf.turnCost.vol)
|
|
2267
|
+
} else {
|
|
2268
|
+
txt += ' · 跟随按压音量 ' + sndPct(soundVol)
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2172
2271
|
}
|
|
2173
2272
|
} else {
|
|
2174
2273
|
var cfg = sndBuf[e.kind] || {}
|
|
@@ -2181,6 +2280,16 @@ function openSoundSettingsPanel() {
|
|
|
2181
2280
|
}
|
|
2182
2281
|
try { if (e.summary) e.summary.textContent = txt } catch (err) {}
|
|
2183
2282
|
}
|
|
2283
|
+
// v778:②区「提示音量」滑块在**未显式设置**时显示"实际生效的音量"(= ①区按压音量),
|
|
2284
|
+
// 否则滑块停在 100% 而实际按 60% 播 —— 那就又变成"界面在骗人"(这次修的就是这类问题)。
|
|
2285
|
+
try {
|
|
2286
|
+
var tcCfg = sndBuf.turnCost || {}
|
|
2287
|
+
if (tcVol && tcVol.input && tcCfg.volSet !== true) {
|
|
2288
|
+
var eff = soundVolumeOf('turnCost')
|
|
2289
|
+
tcVol.input.value = String(eff)
|
|
2290
|
+
if (tcVol.pct) tcVol.pct.textContent = Math.round(eff * 100) + '%'
|
|
2291
|
+
}
|
|
2292
|
+
} catch (err) {}
|
|
2184
2293
|
// v773:摘要与"置灰状态"永远一起刷(所有既有刷新点都会走到这里:开面板 / 卡内 input·change /
|
|
2185
2294
|
// 点文档任意处 / 「恢复默认」末尾)—— 所以开关一动,相关设置立刻跟着灰/亮,不需要另挂监听。
|
|
2186
2295
|
applySndDisabled()
|
|
@@ -2385,7 +2494,13 @@ function openSoundSettingsPanel() {
|
|
|
2385
2494
|
fillSoundSelect(teSel, bufTaskEndSel)
|
|
2386
2495
|
// 「静音」项删除后下拉不该留空:把兜底后的真实值回写缓冲(旧数据 sel 为空串时兜底成列表里的默认音)
|
|
2387
2496
|
if (teSel.value) bufTaskEndSel = teSel.value
|
|
2388
|
-
|
|
2497
|
+
// v778:②区「提示音量」—— 拖动即视为**用户显式设置**(volSet = true),此后该事件用自己的值;
|
|
2498
|
+
// 没拖过则保持"跟随①区按压音量"(见 soundVolumeOf),摘要里会写明,避免再出现"滑块看着能调、其实无效"。
|
|
2499
|
+
var tcVol = volSlider(function () { return sndBuf.turnCost.vol }, function (v) {
|
|
2500
|
+
sndBuf.turnCost.vol = v
|
|
2501
|
+
sndBuf.turnCost.volSet = true
|
|
2502
|
+
try { refreshSummaries() } catch (err) {}
|
|
2503
|
+
})
|
|
2389
2504
|
var rTcVol = row('提示音量', tcEnt.body)
|
|
2390
2505
|
rTcVol.appendChild(tcVol.input)
|
|
2391
2506
|
rTcVol.appendChild(tcVol.pct)
|
|
@@ -2480,6 +2595,9 @@ function openSoundSettingsPanel() {
|
|
|
2480
2595
|
setTurnCostClose(Math.round(snapTurnCostCloseMs / 1000))
|
|
2481
2596
|
}
|
|
2482
2597
|
} catch (err) {}
|
|
2598
|
+
// v777:底部「点按角色关闭提示气泡」——回滚缓冲与控件(它只活在缓冲里,没保存过就没落盘)
|
|
2599
|
+
try { bufWaitCharClose = snapWaitCharClose } catch (err) {}
|
|
2600
|
+
try { if (waitCharChk) waitCharChk.checked = snapWaitCharClose } catch (err) {}
|
|
2483
2601
|
}
|
|
2484
2602
|
try { audioGroupPanel.style.zIndex = '' } catch (err) {}
|
|
2485
2603
|
try { audioEditMask.style.zIndex = '' } catch (err) {}
|
|
@@ -2540,7 +2658,12 @@ function openSoundSettingsPanel() {
|
|
|
2540
2658
|
usageSet.taskEnd = te
|
|
2541
2659
|
try { applyTaskEndLocal(te.on, te.sel) } catch (err) {}
|
|
2542
2660
|
mergeEventsLocal(ev)
|
|
2543
|
-
|
|
2661
|
+
// v777:底部「点按角色关闭提示气泡」跟着同一次 PUT 落盘(键 = settings.wait.charClose)
|
|
2662
|
+
try {
|
|
2663
|
+
usageSet = usageSet || {}
|
|
2664
|
+
usageSet.wait = Object.assign({}, usageSet.wait || {}, { charClose: !!bufWaitCharClose })
|
|
2665
|
+
} catch (err) {}
|
|
2666
|
+
saveUsageSettings({ events: ev, taskEnd: te, wait: { charClose: !!bufWaitCharClose } })
|
|
2544
2667
|
// v763:按压音量 / 音效总开关 / 每轮消耗提示开关这三项**不在**上面这次 PUT 的覆盖范围里
|
|
2545
2668
|
//(它们存在 .dsh-size.json,走 saveConfig())⇒ 保存时必须各自按**当前内存值**提交一次,
|
|
2546
2669
|
// 否则「恢复默认」对它们不生效(用户会觉得恢复默认对音量/开关没作用)。
|
|
@@ -2601,6 +2724,11 @@ function openSoundSettingsPanel() {
|
|
|
2601
2724
|
bufTaskEndSel = 'frag:end_a' // = 宿主 usageSettingsDefaults().taskEnd.sel
|
|
2602
2725
|
bufTaskEndKeep = 'frag:end_a' // = 宿主 usageSettingsDefaults().taskEnd.sel
|
|
2603
2726
|
bufTaskEndOn = true // v774:② 音效行的 [✓] 复位(宿主 taskEnd.on 默认 true)
|
|
2727
|
+
// v778:②区「提示音量」也复位成"跟随①区"(volSet=false;滑块显示由 refreshSummaries 同步成①区值)
|
|
2728
|
+
try { sndBuf.turnCost.volSet = false } catch (err) {}
|
|
2729
|
+
// v777:底部「点按角色关闭提示气泡」恢复出厂默认(false = 只有点泡泡能收)
|
|
2730
|
+
bufWaitCharClose = false
|
|
2731
|
+
try { if (waitCharChk) waitCharChk.checked = false } catch (err) {}
|
|
2604
2732
|
// 控件回填
|
|
2605
2733
|
try { pressOnChk.checked = true } catch (err) {}
|
|
2606
2734
|
try { soundToggle.checked = true } catch (err) {} // v763:与 setSoundOn() 同步的那只开关(只改内存显示)
|
|
@@ -2639,6 +2767,54 @@ function openSoundSettingsPanel() {
|
|
|
2639
2767
|
// · usageSet.events 的 lines(提问/授权提示内容)不再被「恢复默认」清空(与按钮 title 一致)
|
|
2640
2768
|
} catch (err) {}
|
|
2641
2769
|
}
|
|
2770
|
+
// v777(用户要求):「取消/恢复默认/保存」上面一行 —— 与「自定义泡泡」窗口的
|
|
2771
|
+
// 「点按角色推进泡泡队列」同款排版([✓] + 标签 + ? 圈圈),说明收进 ? 圈圈里(复用 v647 的 dshwvAskDot)。
|
|
2772
|
+
var waitCharRow = document.createElement('div')
|
|
2773
|
+
waitCharRow.className = 'dshwv-bubsec'
|
|
2774
|
+
waitCharRow.style.display = 'flex'
|
|
2775
|
+
waitCharRow.style.alignItems = 'center'
|
|
2776
|
+
waitCharRow.style.flexWrap = 'wrap'
|
|
2777
|
+
waitCharRow.style.gap = '4px 6px'
|
|
2778
|
+
waitCharRow.style.color = '#203170'
|
|
2779
|
+
waitCharRow.style.marginTop = '8px'
|
|
2780
|
+
// v777c(2026-09-27 追加要求):
|
|
2781
|
+
// ① 与上面那几行(入口行 `dshwvFoldEntry`,如「授权提示」)的复选框**左对齐** —— 入口行是
|
|
2782
|
+
// `rowEl.style.padding = '6px 6px'` 直接挂在 card 上 ⇒ 复选框在 6px 处;本行 `dshwv-bubsec`
|
|
2783
|
+
// 没有左内边距(默认 0)⇒ 补 6px 即对齐。(⚠️ 注意区分:展开后**折叠体里**的行额外有
|
|
2784
|
+
// `body.style.paddingLeft = 10px`,在 10px 处;本行是全局开关、不属于任何折叠体,故按入口行对齐。)
|
|
2785
|
+
// ② 本行**不要鼠标悬浮提示**(原生 title):问号圈圈自带的「查看说明」与复选框那句说明都清掉 ——
|
|
2786
|
+
// 说明统一收进问号圈圈的弹层里(悬停/点击问号时才显示),避免两个入口说两遍。
|
|
2787
|
+
waitCharRow.style.paddingLeft = '6px'
|
|
2788
|
+
var waitCharAsk = dshwvAskDot(
|
|
2789
|
+
'等待提问 / 授权的提示气泡:<b>默认点气泡就能收起来</b>(点一下收起,同一条挂起不会再自动弹回;' +
|
|
2790
|
+
'你回答或批准之后、或下一次新的提问 / 授权到来时,照常冒泡并响铃)。<br><br>' +
|
|
2791
|
+
'打开下面这个开关后,<b>点角色(鲸鱼)也能收掉提示气泡</b> —— 和「自定义泡泡」里的' +
|
|
2792
|
+
'「点按角色推进泡泡队列」是同一类手感。<br><br>默认关闭。'
|
|
2793
|
+
)
|
|
2794
|
+
// v777b(2026-09-27 追加要求):? 圈圈放到**文字后面**,并把与文字的间距收紧 ——
|
|
2795
|
+
// .dshwv-askq 自带 margin-right:5px,加上行的 column-gap 6px,放前面时离文字有 11px;
|
|
2796
|
+
// v777b 先用行内 margin-left:-3px 压到约 3px;
|
|
2797
|
+
// v777d(2026-09-27 追加要求「再稍稍右移」):改成 0 ⇒ 间距回到行的自然列间距 6px(右移 3px)。
|
|
2798
|
+
waitCharAsk.style.marginLeft = '0'
|
|
2799
|
+
waitCharAsk.style.marginRight = '0'
|
|
2800
|
+
waitCharAsk.title = '' // v777c:清掉 dshwvAskDot 自带的「查看说明」原生 tooltip(用户要求本行不要悬浮提示)
|
|
2801
|
+
var waitCharChk = document.createElement('input')
|
|
2802
|
+
waitCharChk.type = 'checkbox'
|
|
2803
|
+
waitCharChk.className = 'dshwv-check'
|
|
2804
|
+
waitCharChk.id = 'dshwv-waitcharclose'
|
|
2805
|
+
waitCharChk.checked = bufWaitCharClose
|
|
2806
|
+
// v777c:复选框也不挂原生 tooltip(原来那句"打开后:点角色也能收起…"已写进问号圈圈的说明里)
|
|
2807
|
+
waitCharChk.title = ''
|
|
2808
|
+
waitCharChk.addEventListener('change', function () { bufWaitCharClose = !!waitCharChk.checked })
|
|
2809
|
+
waitCharRow.appendChild(waitCharChk)
|
|
2810
|
+
var waitCharLab = document.createElement('label')
|
|
2811
|
+
waitCharLab.setAttribute('for', 'dshwv-waitcharclose')
|
|
2812
|
+
waitCharLab.style.cursor = 'pointer'
|
|
2813
|
+
waitCharLab.style.fontSize = '12px'
|
|
2814
|
+
waitCharLab.textContent = '点按角色关闭提示气泡'
|
|
2815
|
+
waitCharRow.appendChild(waitCharLab)
|
|
2816
|
+
waitCharRow.appendChild(waitCharAsk)
|
|
2817
|
+
card.appendChild(waitCharRow)
|
|
2642
2818
|
var btns = document.createElement('div')
|
|
2643
2819
|
btns.className = 'dshwv-bubbtns'
|
|
2644
2820
|
var cancelBtn = document.createElement('button')
|
|
@@ -3436,6 +3612,33 @@ function openApiModelPanel(modelId) {
|
|
|
3436
3612
|
var authInp = apiTextInput(bal.auth == null ? 'Bearer {key}' : bal.auth, '请求头模板,{key} 会被替换成密钥')
|
|
3437
3613
|
var authRow = apiPanelRow('请求头', authInp)
|
|
3438
3614
|
card.appendChild(authRow)
|
|
3615
|
+
// v789(issue #190):余额接口不再只能是 GET —— 可选 POST/PUT… + JSON 请求体。
|
|
3616
|
+
// 请求体支持 {key}(密钥)、{base}(Base URL)、{uuid}(下面「请求参数」里的键)
|
|
3617
|
+
var balMethod = apiSelectEl([['GET', 'GET(默认)'], ['POST', 'POST'], ['PUT', 'PUT'], ['PATCH', 'PATCH']],
|
|
3618
|
+
String(bal.method || 'GET').toUpperCase() === 'POST' ? 'POST' : String(bal.method || 'GET').toUpperCase() === 'PUT' ? 'PUT' : String(bal.method || 'GET').toUpperCase() === 'PATCH' ? 'PATCH' : 'GET')
|
|
3619
|
+
var balMethodRow = apiPanelRow('请求方法', balMethod)
|
|
3620
|
+
card.appendChild(balMethodRow)
|
|
3621
|
+
var balBody = apiTextInput(bal.body || '', '如 {"uuid":"{uuid}"}(非 GET 时以 JSON 发送)')
|
|
3622
|
+
var balBodyRow = apiPanelRow('请求体', balBody)
|
|
3623
|
+
card.appendChild(balBodyRow)
|
|
3624
|
+
var prm = (m && m.params) || {}
|
|
3625
|
+
var prmInp = apiTextInput(Object.keys(prm).map(function (k) { return k + '=' + prm[k] }).join(','), '请求体占位符,如 uuid=abc123(逗号分隔)')
|
|
3626
|
+
var prmRow = apiPanelRow('请求参数', prmInp)
|
|
3627
|
+
card.appendChild(prmRow)
|
|
3628
|
+
// v789(issue #189):0.3.15 起的"凭据目的地白名单"唯一例外必须能在界面上勾 ——
|
|
3629
|
+
// 宿主侧 model.allowCustomHost 一直是齐的,缺的就是这个入口(README/FAQ 都在指引用户来勾)。
|
|
3630
|
+
// 勾选/取消都**显式**传 true/false(宿主按 === true / === false 分支,缺省表示"不改动")。
|
|
3631
|
+
var allowHost = document.createElement('input')
|
|
3632
|
+
allowHost.type = 'checkbox'
|
|
3633
|
+
allowHost.checked = !!(m && m.balanceDesc && m.balanceDesc.allowCustomHost === true) || !!(m && m.allowCustomHost === true)
|
|
3634
|
+
var allowHostRow = apiPanelRow('允许把凭据发送到自定义地址', allowHost)
|
|
3635
|
+
card.appendChild(allowHostRow)
|
|
3636
|
+
var allowHint = document.createElement('div')
|
|
3637
|
+
allowHint.className = 'dshwv-bubhint'
|
|
3638
|
+
allowHint.style.margin = '0 0 6px'
|
|
3639
|
+
allowHint.textContent = '默认不勾:地址不是该厂商内置端点时,插件会拒绝把密钥发出去。自建网关(New API / 自托管 / Ollama 等)确认是自己的服务后再勾。' +
|
|
3640
|
+
'该开关只能由本机(回环)来源的写请求置位,远端会话改不了它。'
|
|
3641
|
+
card.appendChild(allowHint)
|
|
3439
3642
|
var jr = (bal.json || {})
|
|
3440
3643
|
var jRem = apiTextInput(jr.remaining || '', '如 balance_infos[0].total_balance')
|
|
3441
3644
|
var jTot = apiTextInput(jr.total || '', '如 data.total_credits')
|
|
@@ -3508,8 +3711,24 @@ function openApiModelPanel(modelId) {
|
|
|
3508
3711
|
balance: {
|
|
3509
3712
|
url: (balUrl.value || '').trim(),
|
|
3510
3713
|
auth: (authInp.value || '').trim(),
|
|
3714
|
+
// v789:显式带上方法/请求体(GET + 空体 = 老行为;宿主侧空值不覆盖模板)
|
|
3715
|
+
method: (balMethod.value || 'GET').toUpperCase(),
|
|
3716
|
+
body: (balBody.value || '').trim(),
|
|
3511
3717
|
json: json,
|
|
3512
3718
|
},
|
|
3719
|
+
// v789(issue #189):显式 true/false(不是"仅 true 才传")—— 用户要能**取消**勾选
|
|
3720
|
+
allowCustomHost: !!allowHost.checked,
|
|
3721
|
+
params: (function () {
|
|
3722
|
+
var o = {}
|
|
3723
|
+
String(prmInp.value || '').split(',').forEach(function (kv) {
|
|
3724
|
+
var i = kv.indexOf('=')
|
|
3725
|
+
if (i <= 0) return
|
|
3726
|
+
var k = kv.slice(0, i).trim()
|
|
3727
|
+
var v = kv.slice(i + 1).trim()
|
|
3728
|
+
if (/^[A-Za-z_][A-Za-z0-9_]{0,30}$/.test(k)) o[k] = v
|
|
3729
|
+
})
|
|
3730
|
+
return o
|
|
3731
|
+
})(),
|
|
3513
3732
|
matchIds: (matchInp.value || '').split(',').map(function (s) { return s.trim() }).filter(function (s) { return s.length > 0 }),
|
|
3514
3733
|
price: { hit: (pHit.value || '').trim(), miss: (pMiss.value || '').trim(), out: (pOut.value || '').trim(), cur: pCur.value, rate: (pRate.value || '').trim() },
|
|
3515
3734
|
},
|
|
@@ -4854,7 +5073,8 @@ function resPlayFragment(fid) {
|
|
|
4854
5073
|
if (!fid) return
|
|
4855
5074
|
if (resAudEl) { try { resAudEl.pause() } catch (err) {} resAudEl = null }
|
|
4856
5075
|
var a = dshwvSound('/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(fid))
|
|
4857
|
-
|
|
5076
|
+
// v778:资源窗口的片段试听也走音量解析器(它是"这一条片段本身的试听",与按压音量同源)
|
|
5077
|
+
try { a.volume = soundVolumeOf('press') } catch (err) {}
|
|
4858
5078
|
a.onended = function () { resAudEl = null }
|
|
4859
5079
|
resAudEl = a
|
|
4860
5080
|
a.play().catch(function () { resAudEl = null })
|
|
@@ -6346,7 +6566,7 @@ var bubbleMoreListEl = null
|
|
|
6346
6566
|
var bubbleFirstChipEl = null
|
|
6347
6567
|
var BUBBLE_KIND_LABEL = { normal: '余额内容', random: '随机语句', custom: '自定义内容' }
|
|
6348
6568
|
// v209 默认泡泡内容 = 与开发者当前线上生效配置一致(全新安装/恢复默认时即此体验)
|
|
6349
|
-
// —— 首次点击:标题文本 +
|
|
6569
|
+
// —— 首次点击:标题文本 + 总余额 + 今日已用 + 峰谷时段
|
|
6350
6570
|
function bubbleDefaultFirstModules() {
|
|
6351
6571
|
// v630:normal/首次 兜底默认 = v615 冻结出厂默认第 1 泡(靛蓝余额卡 5 模块),与「恢复默认」一致;
|
|
6352
6572
|
// 以下旧体(macaron 版)仅在常量缺失时作兜底参照,不再作为默认内容
|
|
@@ -7177,6 +7397,13 @@ var BUBBLE_DEFAULT_ITEMS = [
|
|
|
7177
7397
|
"rgb": "macaron",
|
|
7178
7398
|
"italic": true,
|
|
7179
7399
|
"ul": false
|
|
7400
|
+
},
|
|
7401
|
+
{
|
|
7402
|
+
"t": "token 来!",
|
|
7403
|
+
"w": 3,
|
|
7404
|
+
"size": 16,
|
|
7405
|
+
"rgb": "candy",
|
|
7406
|
+
"color": ""
|
|
7180
7407
|
}
|
|
7181
7408
|
],
|
|
7182
7409
|
"size": 8
|
|
@@ -7469,7 +7696,10 @@ function bubbleDefaultModules(kind) {
|
|
|
7469
7696
|
}
|
|
7470
7697
|
function bubbleModuleSummary(m) {
|
|
7471
7698
|
m = m || {}
|
|
7472
|
-
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '
|
|
7699
|
+
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '总余额(充值金额+赠金)'
|
|
7700
|
+
// v782:赠金 / 充值余额(均为内置数值模块,无 modelId);v787 按用户要求改显示文案
|
|
7701
|
+
if (m.type === 'bonus') return '赠金'
|
|
7702
|
+
if (m.type === 'recharge') return '充值余额'
|
|
7473
7703
|
if (m.type === 'today') return bubbleIsModelMod(m) ? ('今日已用·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '今日已用'
|
|
7474
7704
|
if (m.type === 'session') return '对话名' + (Number(m.len) > 0 ? '(保留 ' + Math.round(Number(m.len)) + ' 字)' : '(不截断)')
|
|
7475
7705
|
if (m.type === 'quota') return '额度·' + ((apiModelById(m.modelId) || {}).name || m.modelId)
|
|
@@ -7487,7 +7717,10 @@ function bubbleModuleListLabel(m) {
|
|
|
7487
7717
|
if (m.type === 'text') return '文本: ' + (String(m.text || '').slice(0, 24) || '(空)')
|
|
7488
7718
|
if (m.type === 'link') return '超链接: ' + (String(m.text || '').slice(0, 24) || '打开链接')
|
|
7489
7719
|
if (m.type === 'random') return m.name || '随机语句' // 只显示模块名,不展示内部句子
|
|
7490
|
-
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '
|
|
7720
|
+
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '总余额(充值金额+赠金)'
|
|
7721
|
+
// v782:赠金 / 充值余额(v787 改显示文案)
|
|
7722
|
+
if (m.type === 'bonus') return '赠金'
|
|
7723
|
+
if (m.type === 'recharge') return '充值余额'
|
|
7491
7724
|
if (m.type === 'today') return bubbleIsModelMod(m) ? ('今日已用·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '今日已用'
|
|
7492
7725
|
if (m.type === 'session') return '对话名' + (Number(m.len) > 0 ? '(保留 ' + Math.round(Number(m.len)) + ' 字)' : '(不截断)')
|
|
7493
7726
|
if (m.type === 'quota') return '额度·' + ((apiModelById(m.modelId) || {}).name || m.modelId)
|
|
@@ -8027,7 +8260,31 @@ function bubbleEditorDirty() {
|
|
|
8027
8260
|
return bubbleEditorSnap !== JSON.stringify([bubbleEditItems, bubbleLib, bubbleTapAdvChk.checked])
|
|
8028
8261
|
} catch (err) { return true }
|
|
8029
8262
|
}
|
|
8263
|
+
// v783:打开编辑器前**先从宿主重读一遍配置**。为什么必须重读:泡泡配置只在页面加载时取一次
|
|
8264
|
+
// (loadBubbleCfg),之后一直用内存里的 bubbleCfg 副本;而桌面端 + 网页端(或两个标签页)会**各持一份**,
|
|
8265
|
+
// 谁最后点「保存」谁就把自己那份写回去 ⇒ 在 A 里删掉的模块(或模块库条目)会被 B 的过期副本**写回来**,
|
|
8266
|
+
// 表现就是"删了很久的自定义模块又冒出来了"。读失败时保持原值(离线/接口异常行为不变)。
|
|
8267
|
+
function refreshBubbleCfgFromHost(cb) {
|
|
8268
|
+
var done = function () { try { cb() } catch (err) {} }
|
|
8269
|
+
try {
|
|
8270
|
+
fetch(BUBBLE_URL, { cache: 'no-store' })
|
|
8271
|
+
.then(function (r) { return r.json() })
|
|
8272
|
+
.then(function (d) {
|
|
8273
|
+
if (d && d.ok && d.config) {
|
|
8274
|
+
bubbleCfg = d.config
|
|
8275
|
+
bubbleLib = (d.config.lib && Array.isArray(d.config.lib)) ? JSON.parse(JSON.stringify(d.config.lib)) : []
|
|
8276
|
+
bubbleTapAdvance = d.config.tapAdvance === true // v727
|
|
8277
|
+
try { applyBubbleCfgSeq() } catch (err) {}
|
|
8278
|
+
}
|
|
8279
|
+
})
|
|
8280
|
+
.catch(function () {})
|
|
8281
|
+
.then(done, done)
|
|
8282
|
+
} catch (err) { done() }
|
|
8283
|
+
}
|
|
8030
8284
|
function openBubbleEditor() {
|
|
8285
|
+
refreshBubbleCfgFromHost(function () { openBubbleEditorWithCache() })
|
|
8286
|
+
}
|
|
8287
|
+
function openBubbleEditorWithCache() {
|
|
8031
8288
|
try {
|
|
8032
8289
|
closeRolePanel()
|
|
8033
8290
|
closeAudioGroupPanel()
|
|
@@ -8214,6 +8471,8 @@ function qeditEnsure() {
|
|
|
8214
8471
|
qeditClose()
|
|
8215
8472
|
}, true)
|
|
8216
8473
|
document.addEventListener('keydown', function (e) {
|
|
8474
|
+
// v789(issue #179):组字中按 Esc 是输入法的事,别抢
|
|
8475
|
+
if (dshwvComposingNow()) return
|
|
8217
8476
|
if (e.key === 'Escape') qeditClose()
|
|
8218
8477
|
})
|
|
8219
8478
|
}
|
|
@@ -8535,8 +8794,8 @@ function openQuickModuleEditor(m, anchorBtn) {
|
|
|
8535
8794
|
var isModelBal = bubbleIsModelMod(m) && (m.type === 'balance' || m.type === 'today')
|
|
8536
8795
|
var isModelQuota = bubbleIsModelMod(m) && m.type === 'quota'
|
|
8537
8796
|
var isModelPlan = bubbleIsModelMod(m) && m.type === 'plan'
|
|
8538
|
-
inp.placeholder = isModelPlan ? '例: {plan} 额度 · {plan_reset} 刷新时间' : (isModelQuota ? '例: 额度 {quota} · 剩 {quota_left}' : (isModelBal ? '例: {balance} 或 今日 {today}' : (m.type === 'balance' ? '例: {balance_ds}' : (m.type === 'today' ? '例: 今日已用 {expense_ds}' : (m.type === 'session' ? '例: {session} 或 当前对话 {session}' : (bubbleIsPeakCount(m) ? '例: 距空闲 {countdown}' : '例: 当前 {status}'))))))
|
|
8539
|
-
inp.title = '可用占位符(英文): ' + (isModelPlan ? '{plan} 额度 / {plan_left} 剩余 / {plan_reset} 刷新时间(多窗口时随「显示样式」所选窗口变化)' : (isModelQuota ? '{quota} / {quota_used} / {quota_left} / {quota_total} / {quota_reset}' : (isModelBal ? '{balance} / {today}' : (m.type === 'peak' || m.type === 'nextpeak' ? '{status} / {countdown}' : (m.type === 'balance' ? '{balance_ds}' : (m.type === 'today' ? '{expense_ds}' : (m.type === 'session' ? '{session} 当前对话名(按「保留长度」截断)' : '{expense_ds}')))))))
|
|
8797
|
+
inp.placeholder = isModelPlan ? '例: {plan} 额度 · {plan_reset} 刷新时间' : (isModelQuota ? '例: 额度 {quota} · 剩 {quota_left}' : (isModelBal ? '例: {balance} 或 今日 {today}' : (m.type === 'balance' ? '例: {balance_ds}' : (m.type === 'bonus' ? '例: {bonus_ds} 或 赠金 {bonus_ds}' : (m.type === 'recharge' ? '例: {recharge_ds} 或 余额 {recharge_ds}' : (m.type === 'today' ? '例: 今日已用 {expense_ds}' : (m.type === 'session' ? '例: {session} 或 当前对话 {session}' : (bubbleIsPeakCount(m) ? '例: 距空闲 {countdown}' : '例: 当前 {status}'))))))))
|
|
8798
|
+
inp.title = '可用占位符(英文): ' + (isModelPlan ? '{plan} 额度 / {plan_left} 剩余 / {plan_reset} 刷新时间(多窗口时随「显示样式」所选窗口变化)' : (isModelQuota ? '{quota} / {quota_used} / {quota_left} / {quota_total} / {quota_reset}' : (isModelBal ? '{balance} / {today}' : (m.type === 'peak' || m.type === 'nextpeak' ? '{status} / {countdown}' : (m.type === 'balance' ? '{balance_ds}' : (m.type === 'bonus' ? '{bonus_ds}' : (m.type === 'recharge' ? '{recharge_ds}' : (m.type === 'today' ? '{expense_ds}' : (m.type === 'session' ? '{session} 当前对话名(按「保留长度」截断)' : '{expense_ds}')))))))))
|
|
8540
8799
|
inp.addEventListener('input', function () { m.tpl = inp.value; changed() })
|
|
8541
8800
|
r.appendChild(inp)
|
|
8542
8801
|
var qb2 = document.createElement('button')
|
|
@@ -8733,7 +8992,12 @@ function renderBubblePal() {
|
|
|
8733
8992
|
bubblePalEl.innerHTML = ''
|
|
8734
8993
|
var defs = [
|
|
8735
8994
|
{ key: 'text', label: '文本', cb: function () { bubbleModuleAdd({ type: 'text', text: '新内容', size: 6, bold: true }) } },
|
|
8736
|
-
{ key: 'balance', label: '
|
|
8995
|
+
{ key: 'balance', label: '总余额(充值金额+赠金)', pin: true, cb: function () { bubbleModuleAdd({ type: 'balance', size: 11, tpl: '{balance_ds}' }) } },
|
|
8996
|
+
// v782:把「总余额」拆出的两个分量各自做成模块 —— 赠金(未过期赠送余额)与充值余额。
|
|
8997
|
+
// ⚠️ 上面那条 `balance` / `{balance_ds}` 的**代码名字与行为一律不动**(历史泡泡与已保存的模块里
|
|
8998
|
+
// 存的是 type/tpl 字面量,改代码名会让老内容显示不出来);v787 只按用户要求改了**显示文案**。
|
|
8999
|
+
{ key: 'bonus', label: '赠金', pin: true, cb: function () { bubbleModuleAdd({ type: 'bonus', size: 11, tpl: '{bonus_ds}' }) } },
|
|
9000
|
+
{ key: 'recharge', label: '充值余额', pin: true, cb: function () { bubbleModuleAdd({ type: 'recharge', size: 11, tpl: '{recharge_ds}' }) } },
|
|
8737
9001
|
{ key: 'today', label: '今日已用', pin: true, cb: function () { bubbleModuleAdd({ type: 'today', size: 1, tpl: '今日已用 {expense_ds}' }) } },
|
|
8738
9002
|
{ key: 'peak', label: '峰谷时段', pin: true, cb: function () { bubbleModuleAdd({ type: 'peak', size: 4, peakColor: '#e0433f', offColor: '#2fa24c', tpl: '{status}' }) } },
|
|
8739
9003
|
{ key: 'nextpeak', label: '时段倒计时', pin: true, cb: function () { bubbleModuleAdd({ type: 'peak', size: 6, bold: true, peakStyle: 'count', peakColor: '#e0433f', offColor: '#2fa24c', tpl: '{countdown}' }) } },
|
|
@@ -8957,7 +9221,8 @@ function bubbleModuleEdit(m, anchorBtn) {
|
|
|
8957
9221
|
try {
|
|
8958
9222
|
if (m && (m.type === 'text' || m.type === 'link')) { openQuickTextEditor(m, anchorBtn); return }
|
|
8959
9223
|
// v769:对话名也走悬浮编辑(像文本那样边改边看效果),不再开整窗编辑器
|
|
8960
|
-
|
|
9224
|
+
// v782:赠金 / 充值余额同样是内置数值模块 ⇒ 也走悬浮编辑(内容锁定,只调字号/颜色/模板)
|
|
9225
|
+
if (m && (m.type === 'balance' || m.type === 'bonus' || m.type === 'recharge' || m.type === 'today' || m.type === 'peak' || m.type === 'nextpeak' || m.type === 'session')) { openQuickModuleEditor(m, anchorBtn); return }
|
|
8961
9226
|
openModuleEditor(m, function (saved) { if (saved) renderBubblePv() })
|
|
8962
9227
|
} catch (err) {}
|
|
8963
9228
|
}
|
|
@@ -9056,6 +9321,9 @@ function bubblePvPaletteToRow(key, ri) {
|
|
|
9056
9321
|
function bubblePaletteModule(key) {
|
|
9057
9322
|
if (key === 'text') return { type: 'text', text: '新内容', size: 6, bold: true }
|
|
9058
9323
|
if (key === 'balance') return { type: 'balance', size: 11, tpl: '{balance_ds}' }
|
|
9324
|
+
// v782:赠金 / 充值余额(拖拽路径;与上面「总余额(充值金额+赠金)」并列的两个内置数值模块)
|
|
9325
|
+
if (key === 'bonus') return { type: 'bonus', size: 11, tpl: '{bonus_ds}' }
|
|
9326
|
+
if (key === 'recharge') return { type: 'recharge', size: 11, tpl: '{recharge_ds}' }
|
|
9059
9327
|
if (key === 'today') return { type: 'today', size: 1, tpl: '今日已用 {expense_ds}' }
|
|
9060
9328
|
if (key === 'peak') return { type: 'peak', size: 4, peakColor: '#e0433f', offColor: '#2fa24c', tpl: '{status}' }
|
|
9061
9329
|
if (key === 'nextpeak') return { type: 'peak', size: 6, bold: true, peakStyle: 'count', peakColor: '#e0433f', offColor: '#2fa24c', tpl: '{countdown}' }
|
|
@@ -9522,19 +9790,31 @@ var bubbleTapAdvChk = document.createElement('input')
|
|
|
9522
9790
|
bubbleTapAdvChk.type = 'checkbox'
|
|
9523
9791
|
bubbleTapAdvChk.className = 'dshwv-check'
|
|
9524
9792
|
bubbleTapAdvChk.id = 'dshwv-tapadv'
|
|
9525
|
-
|
|
9793
|
+
// v777e(2026-09-27 用户要求):本行复选框**不再挂原生 tooltip**(原来那句"开启后:点一下角色=往后推进一项…"
|
|
9794
|
+
// 与问号圈圈里的说明重复)。说明统一收进 ? 圈圈的弹层里(与「提示与音效设置」面板底部那行一致)。
|
|
9795
|
+
bubbleTapAdvChk.title = ''
|
|
9526
9796
|
var bubbleTapAdvLab = document.createElement('label')
|
|
9527
9797
|
bubbleTapAdvLab.setAttribute('for', 'dshwv-tapadv')
|
|
9528
9798
|
bubbleTapAdvLab.style.cursor = 'pointer'
|
|
9529
9799
|
bubbleTapAdvLab.style.fontSize = '12px'
|
|
9530
9800
|
bubbleTapAdvLab.textContent = '点按角色推进泡泡队列'
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9801
|
+
// v777(用户要求):原来跟在标签后面的那一行说明字(「关闭=点角色回到第 1 个泡泡;开启=点一下往后一个」)
|
|
9802
|
+
// 收进问号圈圈里 —— 复用 v647 的 dshwvAskDot(悬停即显示、点击固定、触摸端点按开关),与
|
|
9803
|
+
// 「提示与音效设置」面板底部那一行同款。说明里顺带把原来 title 里的完整描述也写进去。
|
|
9804
|
+
// v777b(2026-09-27 追加要求):? 圈圈放在**文字后面**,并把与文字的间距收紧(同面板那处)。
|
|
9805
|
+
// v777d:「再稍稍右移」⇒ margin-left 由 -3px 改成 0(间距 = 行的自然列间距 6px)。
|
|
9806
|
+
var bubbleTapAdvAsk = dshwvAskDot(
|
|
9807
|
+
'关闭(默认):<b>点角色</b>=回到第 1 个泡泡(点泡泡才是往后推进)。<br><br>' +
|
|
9808
|
+
'开启:<b>点一下角色 = 往后推进一项</b>(不再回到首次点击泡泡);走到最后一项再点=收起泡泡。'
|
|
9809
|
+
)
|
|
9810
|
+
bubbleTapAdvAsk.style.marginLeft = '0'
|
|
9811
|
+
bubbleTapAdvAsk.style.marginRight = '0'
|
|
9812
|
+
// v777e:与面板底部那行同款处理 —— 清掉问号圈圈自带的「查看说明」原生 tooltip,
|
|
9813
|
+
// 说明只在悬停/点击问号时以自绘弹层出现(本行鼠标悬浮不再出现浏览器小黄条)。
|
|
9814
|
+
bubbleTapAdvAsk.title = ''
|
|
9535
9815
|
bubbleTapAdvRow.appendChild(bubbleTapAdvChk)
|
|
9536
9816
|
bubbleTapAdvRow.appendChild(bubbleTapAdvLab)
|
|
9537
|
-
bubbleTapAdvRow.appendChild(
|
|
9817
|
+
bubbleTapAdvRow.appendChild(bubbleTapAdvAsk)
|
|
9538
9818
|
bubbleCard.appendChild(bubbleTapAdvRow)
|
|
9539
9819
|
// 按钮行
|
|
9540
9820
|
var bubbleBtns = document.createElement('div')
|
|
@@ -10125,7 +10405,10 @@ function bubbleFontEditRow(getVal, setVal) {
|
|
|
10125
10405
|
return row
|
|
10126
10406
|
}
|
|
10127
10407
|
function moduleTypeName(t, m) {
|
|
10128
|
-
if (t === 'balance') return '
|
|
10408
|
+
if (t === 'balance') return '总余额(充值金额+赠金)'
|
|
10409
|
+
// v782:赠金 / 充值余额两个数值模块的类型名(v787 改显示文案)
|
|
10410
|
+
if (t === 'bonus') return '赠金'
|
|
10411
|
+
if (t === 'recharge') return '充值余额'
|
|
10129
10412
|
if (t === 'today') return '今日已用'
|
|
10130
10413
|
if (t === 'session') return '对话名'
|
|
10131
10414
|
if (t === 'peak' || t === 'nextpeak') {
|
|
@@ -10173,6 +10456,8 @@ function renderModuleEditor() {
|
|
|
10173
10456
|
inp.value = m.tpl || ''
|
|
10174
10457
|
function hintOf() {
|
|
10175
10458
|
if (m.type === 'balance') return '例: {balance_ds}'
|
|
10459
|
+
if (m.type === 'bonus') return '例: {bonus_ds} 或 赠金 {bonus_ds}'
|
|
10460
|
+
if (m.type === 'recharge') return '例: {recharge_ds} 或 余额 {recharge_ds}'
|
|
10176
10461
|
if (m.type === 'today') return '例: 今日已用 {expense_ds}'
|
|
10177
10462
|
if (m.type === 'session') return '例: {session} 或 当前对话 {session}'
|
|
10178
10463
|
if (bubbleIsPeakCount(m)) return '例: 距空闲 {countdown}'
|
|
@@ -11575,6 +11860,42 @@ bubbleBox.addEventListener('click', function (e) {
|
|
|
11575
11860
|
bubbleNext()
|
|
11576
11861
|
})
|
|
11577
11862
|
|
|
11863
|
+
// ===== v785:桌面客户端「窗口控件带」不进入挂件的可移动范围 =====
|
|
11864
|
+
// 背景(用户 2026-10-02 反馈):客户端右上角的最小化/最大化/关闭会盖在泡泡上方。
|
|
11865
|
+
// 查证:DSH 桌面端产品窗口用 Electron `titleBarStyle:'hidden'` + **`titleBarOverlay`**
|
|
11866
|
+
// (主进程写死 height:40;macOS 是 `hiddenInset` + 左上 traffic light)—— 那三个键由**浏览器引擎**
|
|
11867
|
+
// 画在所有页面内容之上 ⇒ **CSS/z-index 永远盖不过去**(不是我们层级写低了)。
|
|
11868
|
+
// 修法(用户指定):不做层级对抗、也不平移泡泡,而是**把整条顶带排除在挂件的可移动范围之外**:
|
|
11869
|
+
// 挂件顶边(root.top)一律 ≥ 叠层下沿 ⇒ 画在 root 内部的泡泡自然也在带下,
|
|
11870
|
+
// 而鲸鱼与泡泡的相对位置**完全不变**(比"泡泡自己下移"更干净)。
|
|
11871
|
+
var DESKTOP_TITLEBAR_FALLBACK = 44 // 主进程 titleBarOverlay.height = 40,+4 余量
|
|
11872
|
+
function desktopTitlebarBottom() {
|
|
11873
|
+
var wco = null
|
|
11874
|
+
try { wco = navigator.windowControlsOverlay } catch (err) { wco = null }
|
|
11875
|
+
// 没有叠层对象(普通浏览器 / 手机浏览器)或叠层不可见 ⇒ 不需要排除
|
|
11876
|
+
if (!wco || wco.visible === false) return 0
|
|
11877
|
+
// 有叠层:优先用官方矩形;取不到(旧内核/异常)就退回主进程里那个高度,**不能返回 0**
|
|
11878
|
+
try {
|
|
11879
|
+
if (typeof wco.getTitlebarAreaRect === 'function') {
|
|
11880
|
+
var r = wco.getTitlebarAreaRect()
|
|
11881
|
+
if (r && isFinite(r.y) && isFinite(r.height) && r.height > 0) return Math.max(0, Math.round(r.y + r.height))
|
|
11882
|
+
}
|
|
11883
|
+
} catch (err) {}
|
|
11884
|
+
return DESKTOP_TITLEBAR_FALLBACK
|
|
11885
|
+
}
|
|
11886
|
+
// 挂件顶边的**下限**(0 = 不限制,普通浏览器就是 0)
|
|
11887
|
+
function widgetTopMin() {
|
|
11888
|
+
try { return Math.max(0, desktopTitlebarBottom()) } catch (err) { return 0 }
|
|
11889
|
+
}
|
|
11890
|
+
// 把「挂件顶边」夹进可移动范围:[顶带下沿, 视口内上限]
|
|
11891
|
+
function clampWidgetTop(v, maxT) {
|
|
11892
|
+
var min = widgetTopMin()
|
|
11893
|
+
var t = Number(v)
|
|
11894
|
+
if (!isFinite(t)) t = min
|
|
11895
|
+
var hi = Infinity
|
|
11896
|
+
if (maxT !== undefined && isFinite(Number(maxT))) hi = Math.max(min, Number(maxT))
|
|
11897
|
+
return Math.max(min, Math.min(t, hi))
|
|
11898
|
+
}
|
|
11578
11899
|
var body = document.createElement('div')
|
|
11579
11900
|
body.className = 'dshwv-body'
|
|
11580
11901
|
body.appendChild(img)
|
|
@@ -11675,6 +11996,10 @@ var state = {
|
|
|
11675
11996
|
top: 0,
|
|
11676
11997
|
balance: null,
|
|
11677
11998
|
currency: null,
|
|
11999
|
+
// v781:余额的两个分量(赠金 / 充值余额)。宿主在 API key 路与 DSH 账号路都会下发;
|
|
12000
|
+
// 取不到时保持 null ⇒ 模块显示 `—`(见 bubbleBonusText/bubbleRechargeText)。
|
|
12001
|
+
bonusBalance: null,
|
|
12002
|
+
rechargeBalance: null,
|
|
11678
12003
|
todayUsage: null,
|
|
11679
12004
|
todayUsageCurrency: 'CNY',
|
|
11680
12005
|
usageLabel: '本地估算',
|
|
@@ -11927,6 +12252,32 @@ function restoreBubbleLines() {
|
|
|
11927
12252
|
// 消耗=showCostBubble)独立触发, 消耗优先级最高可顶掉当前并暂停手动轮。
|
|
11928
12253
|
var costBubbleTimer = null // 旧版计时器保留声明(新版 bubbleClearAll 仍清理)
|
|
11929
12254
|
var bubbleTtlTimer = null
|
|
12255
|
+
// v789(issue #188/#172):泡泡的自动收起**不能只靠 setTimeout** —— 页面被遮挡 / 最小化 / 切到后台时,
|
|
12256
|
+
// 浏览器会把定时器节流甚至挂起,于是"5 秒后收起"变成几十秒甚至永远不收(用户实测:点击泡一直不消失、
|
|
12257
|
+
// 刷新即恢复)。所以同时记一个**截止时刻**,在页面重新可见 / 拿到焦点时结算一次;再配一个每秒巡检兜底。
|
|
12258
|
+
var bubbleTtlDeadline = 0
|
|
12259
|
+
function bubbleTtlArmed(ttlMs) {
|
|
12260
|
+
try { if (ttlMs > 0) bubbleTtlDeadline = Date.now() + ttlMs; else bubbleTtlDeadline = 0 } catch (err) { bubbleTtlDeadline = 0 }
|
|
12261
|
+
}
|
|
12262
|
+
function bubbleTtlClear() { bubbleTtlDeadline = 0 }
|
|
12263
|
+
// 巡检:只在"本该已经到期"且计时器已经不在了(被节流/挂起)时补收一次
|
|
12264
|
+
function bubbleTtlSweep() {
|
|
12265
|
+
try {
|
|
12266
|
+
if (bubbleTtlTimer || !bubbleTtlDeadline) return
|
|
12267
|
+
if (!bubbleScene || !(bubbleScene.ttlMs > 0)) { bubbleTtlDeadline = 0; return }
|
|
12268
|
+
if (Date.now() < bubbleTtlDeadline) return
|
|
12269
|
+
bubbleTtlDeadline = 0
|
|
12270
|
+
bubbleAutoClose()
|
|
12271
|
+
} catch (err) {}
|
|
12272
|
+
}
|
|
12273
|
+
// 三条触发路径:页面重新可见(切标签回来)、窗口重新拿到焦点(最小化还原)、以及每秒一次的兜底巡检。
|
|
12274
|
+
// 正常前台运行时计时器按时收敛,巡检永远提前返回(不会误收下一个泡泡)。
|
|
12275
|
+
try {
|
|
12276
|
+
document.addEventListener('visibilitychange', function () { try { if (!document.hidden) bubbleTtlSweep() } catch (err) {} })
|
|
12277
|
+
window.addEventListener('focus', function () { bubbleTtlSweep() })
|
|
12278
|
+
window.addEventListener('pageshow', function () { bubbleTtlSweep() })
|
|
12279
|
+
setInterval(bubbleTtlSweep, 1000)
|
|
12280
|
+
} catch (err) {}
|
|
11930
12281
|
var bubbleScene = null // { kind:'normal'|'random'|'cost', ttlMs }
|
|
11931
12282
|
var bubbleSeq = bubbleDefaultQueue() // 默认序列 = 与开发者线上生效一致(首次=余额,再次=随机语句);有配置后由 applyBubbleCfgSeq 覆盖
|
|
11932
12283
|
var bubbleSeqIdx = 0 // 下一项下标(手动轮内推进)
|
|
@@ -12148,6 +12499,8 @@ function bubbleClearAll() {
|
|
|
12148
12499
|
try { if (settleTimer) { clearTimeout(settleTimer); settleTimer = null } } catch (err) {}
|
|
12149
12500
|
}
|
|
12150
12501
|
function bubbleCloseVisual() {
|
|
12502
|
+
// v789(issue #188):视觉收起是"泡泡真的收掉了"的唯一出口 ⇒ 顺带把截止时刻清零(三处对齐之二)
|
|
12503
|
+
bubbleTtlClear()
|
|
12151
12504
|
// v771:泡泡一旦真的收起,等待标记必须一起复位。`waitShown` 平时由 bubbleRenderModules 按场景重算,
|
|
12152
12505
|
// 但"整泡关闭"(hideBubble/hideCostBubble/hideWaitBubble…)不走渲染 ⇒ 不复位就会残留成 true,
|
|
12153
12506
|
// 之后 showWaitBubble 会误判"已经在显示等待泡泡"(历史上正是这类不同步把等待泡泡卡死的)。
|
|
@@ -12196,7 +12549,8 @@ function sceneOpen(kind, renderFn, ttlMs) {
|
|
|
12196
12549
|
}, 180)
|
|
12197
12550
|
} catch (err) {}
|
|
12198
12551
|
}
|
|
12199
|
-
if (ttlMs > 0) bubbleTtlTimer = setTimeout(bubbleAutoClose, ttlMs)
|
|
12552
|
+
if (ttlMs > 0) { bubbleTtlArmed(ttlMs); bubbleTtlTimer = setTimeout(bubbleAutoClose, ttlMs) }
|
|
12553
|
+
else bubbleTtlClear()
|
|
12200
12554
|
}
|
|
12201
12555
|
if (wasOpen) {
|
|
12202
12556
|
// 内容切换:旧内容先淡出,再换新内容并淡入(可被新场景/关闭随时打断)
|
|
@@ -12216,6 +12570,8 @@ function sceneOpen(kind, renderFn, ttlMs) {
|
|
|
12216
12570
|
}
|
|
12217
12571
|
function bubbleAutoClose() {
|
|
12218
12572
|
bubbleTtlTimer = null
|
|
12573
|
+
// v789:走完这条就说明本次 TTL 已经结算,截止时刻必须清零,否则巡检会误收下一个泡泡
|
|
12574
|
+
bubbleTtlClear()
|
|
12219
12575
|
if (bubbleScene && bubbleScene.kind === 'cost') { if (whaleSysSwapNext()) return; hideCostBubble(); return }
|
|
12220
12576
|
if (bubbleScene && bubbleScene.kind === 'alert') { if (whaleSysSwapNext()) return; hideUsageAlertBubble(); return }
|
|
12221
12577
|
// v761(#161 C5):等待交互泡泡是**常驻**的(sceneOpen 收到 ttl 0 不布计时器),正常永远走不到这里;
|
|
@@ -12226,7 +12582,9 @@ function bubbleAutoClose() {
|
|
|
12226
12582
|
// 重置当前泡泡的留存计时(点鲸鱼给第 1 泡续时,不清内容)
|
|
12227
12583
|
function bubbleResetTtl() {
|
|
12228
12584
|
try { if (bubbleTtlTimer) { clearTimeout(bubbleTtlTimer); bubbleTtlTimer = null } } catch (err) {}
|
|
12229
|
-
|
|
12585
|
+
// v789:续时也要同步截止时刻,否则巡检会在"刚续过"的泡泡上提前动作(issue #188 提醒的三处对齐之一)
|
|
12586
|
+
if (bubbleScene && bubbleScene.ttlMs > 0) { bubbleTtlArmed(bubbleScene.ttlMs); bubbleTtlTimer = setTimeout(bubbleAutoClose, bubbleScene.ttlMs) }
|
|
12587
|
+
else bubbleTtlClear()
|
|
12230
12588
|
}
|
|
12231
12589
|
// 默认内容视图 = 现在待机内容(余额/今日已用),由 render/restore 维护
|
|
12232
12590
|
function bubbleRenderDefault() { restoreBubbleLines() }
|
|
@@ -12291,8 +12649,11 @@ function bubbleShowSeqNext() {
|
|
|
12291
12649
|
}
|
|
12292
12650
|
}
|
|
12293
12651
|
// ===== 模块渲染引擎(B1) =====
|
|
12294
|
-
// 模块:{type:'text'|'balance'|'today'|'peak'|'session'|'image'|'random', text?, imgId?, color?, size?(1..8 档),
|
|
12652
|
+
// 模块:{type:'text'|'balance'|'bonus'|'recharge'|'today'|'peak'|'session'|'image'|'random', text?, imgId?, color?, size?(1..8 档),
|
|
12295
12653
|
// lines?:[{t,w}] (random 自带句子列表), tpl?(占位符模板), len?(session 的「保留长度」,0=不截断)}
|
|
12654
|
+
// v782 新增 'bonus'(赠金)与 'recharge'(充值余额):与 'balance' 并列的内置数值模块,
|
|
12655
|
+
// 分别用占位符 {bonus_ds} / {recharge_ds};**'balance' / '{balance_ds}' 的名字与行为一律不动**
|
|
12656
|
+
// (历史泡泡里存的是这些字面量,改名会让老内容显示不出来)。
|
|
12296
12657
|
// 字号档位 1..50,线性细分(1→40u … 50→240u,相对 --dshw-u 的倍数)
|
|
12297
12658
|
function bubbleModuleFontU(level) {
|
|
12298
12659
|
var n = Number(level) || 6
|
|
@@ -12307,6 +12668,19 @@ function bubbleAmountText() {
|
|
|
12307
12668
|
function bubbleTodayText() {
|
|
12308
12669
|
return (state.usageLabel || '今日已用') + ' ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
12309
12670
|
}
|
|
12671
|
+
// v782:赠金 / 充值余额两个数值模块的取数(与「总余额(充值金额+赠金)」同源:宿主余额返回体)。
|
|
12672
|
+
// ⚠️ 两个数字**可能拿不到**(厂商没有该字段、或未登录账号)⇒ 显示 `—`,**不显示 0**
|
|
12673
|
+
// —— 0 的意思是"确实没有了",与"读不到"是两件事(记账也依赖这个区分)。
|
|
12674
|
+
function bubbleBonusText() {
|
|
12675
|
+
var v = state.bonusBalance
|
|
12676
|
+
if (v === null || v === undefined || !isFinite(Number(v))) return '—'
|
|
12677
|
+
return fmt(Number(v), state.currency)
|
|
12678
|
+
}
|
|
12679
|
+
function bubbleRechargeText() {
|
|
12680
|
+
var v = state.rechargeBalance
|
|
12681
|
+
if (v === null || v === undefined || !isFinite(Number(v))) return '—'
|
|
12682
|
+
return fmt(Number(v), state.currency)
|
|
12683
|
+
}
|
|
12310
12684
|
// 模块「内容」模板:占位符统一英文(便于兼容其他模型 API 时区分来源/字段):
|
|
12311
12685
|
// {expense_ds} 今日已用金额 · {balance_ds} 余额 · {status} 高峰/空闲状态字 · {countdown} 倒计时
|
|
12312
12686
|
// —— 自定义 API 模型的余额/今日已用(Phase 3)——
|
|
@@ -12676,6 +13050,12 @@ function bubbleContentTokenMap(m) {
|
|
|
12676
13050
|
if (m.type === 'balance') {
|
|
12677
13051
|
v = bubbleAmountText()
|
|
12678
13052
|
map['balance_ds'] = v
|
|
13053
|
+
} else if (m.type === 'bonus') {
|
|
13054
|
+
// v782:赠金模块({bonus_ds})。取不到数 = '—',不显示 0。
|
|
13055
|
+
map['bonus_ds'] = bubbleBonusText()
|
|
13056
|
+
} else if (m.type === 'recharge') {
|
|
13057
|
+
// v782:充值余额模块({recharge_ds})。
|
|
13058
|
+
map['recharge_ds'] = bubbleRechargeText()
|
|
12679
13059
|
} else if (m.type === 'today') {
|
|
12680
13060
|
v = (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
12681
13061
|
map['expense_ds'] = v
|
|
@@ -12713,7 +13093,9 @@ function bubbleTplHelpItems(m) {
|
|
|
12713
13093
|
add('plan_reset', '订阅额度刷新倒计时(同上;全部窗口时为紧凑倒计时)')
|
|
12714
13094
|
return arr
|
|
12715
13095
|
}
|
|
12716
|
-
if (m.type === 'balance') add('balance_ds', '
|
|
13096
|
+
if (m.type === 'balance') add('balance_ds', '总余额(充值金额+赠金)')
|
|
13097
|
+
if (m.type === 'bonus') add('bonus_ds', '赠金余额(未过期的赠送余额;取不到显示 —)')
|
|
13098
|
+
if (m.type === 'recharge') add('recharge_ds', '充值余额(不含赠金;取不到显示 —)')
|
|
12717
13099
|
else if (m.type === 'today') add('expense_ds', '今日已用金额')
|
|
12718
13100
|
else if (m.type === 'session') add('session', '当前对话名(超过「保留长度」会截断为 前N字...)')
|
|
12719
13101
|
else if (m.type === 'peak' || m.type === 'nextpeak') {
|
|
@@ -12736,7 +13118,7 @@ function bubbleTplHelpToggle(m, anchor) {
|
|
|
12736
13118
|
} catch (err) {}
|
|
12737
13119
|
dshwvTplHelpEl.style.display = 'none'
|
|
12738
13120
|
}, true)
|
|
12739
|
-
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') dshwvTplHelpEl.style.display = 'none' })
|
|
13121
|
+
document.addEventListener('keydown', function (e) { if (dshwvComposingNow()) return; if (e.key === 'Escape') dshwvTplHelpEl.style.display = 'none' })
|
|
12740
13122
|
}
|
|
12741
13123
|
if (dshwvTplHelpEl.style.display === 'block') { dshwvTplHelpEl.style.display = 'none'; return }
|
|
12742
13124
|
var items = bubbleTplHelpItems(m)
|
|
@@ -12780,7 +13162,7 @@ function dshwvHintEnsure() {
|
|
|
12780
13162
|
} catch (err) {}
|
|
12781
13163
|
dshwvHintHide()
|
|
12782
13164
|
}, true)
|
|
12783
|
-
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') dshwvHintHide() })
|
|
13165
|
+
document.addEventListener('keydown', function (e) { if (dshwvComposingNow()) return; if (e.key === 'Escape') dshwvHintHide() })
|
|
12784
13166
|
return dshwvHintEl
|
|
12785
13167
|
}
|
|
12786
13168
|
function dshwvHintShow(html, anchor, pinned) {
|
|
@@ -13111,6 +13493,9 @@ function bubbleRowContentOf(mod) {
|
|
|
13111
13493
|
var bv = bubbleIsModelMod(mod) ? apiModelBalanceText(mod.modelId) : bubbleAmountText()
|
|
13112
13494
|
return { txt: bubbleContentText(mod, bv), line: null }
|
|
13113
13495
|
}
|
|
13496
|
+
// v782:赠金 / 充值余额两个数值模块(与「总余额(充值金额+赠金)」并列,取不到数时自动文案是 —)
|
|
13497
|
+
if (mod.type === 'bonus') return { txt: bubbleContentText(mod, bubbleBonusText()), line: null }
|
|
13498
|
+
if (mod.type === 'recharge') return { txt: bubbleContentText(mod, bubbleRechargeText()), line: null }
|
|
13114
13499
|
if (mod.type === 'today') {
|
|
13115
13500
|
var tv2 = bubbleIsModelMod(mod) ? apiModelTodayText(mod.modelId) : (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
13116
13501
|
return { txt: bubbleContentText(mod, '今日已用 ' + tv2), line: null }
|
|
@@ -13503,6 +13888,12 @@ function whaleClick() {
|
|
|
13503
13888
|
bubbleShowSeqNext()
|
|
13504
13889
|
return
|
|
13505
13890
|
}
|
|
13891
|
+
// v777:等待交互提示期间点角色 —— 开关「点按角色关闭提示气泡」打开时,点角色=收起这条等待提示
|
|
13892
|
+
//(默认关:保持"点角色不动等待提示",只有点泡泡才关)。收起来后 pollWaitState 不会把它弹回来。
|
|
13893
|
+
if (bubbleScene && bubbleScene.kind === 'wait') {
|
|
13894
|
+
if (waitCharCloseOn()) dismissWaitBubble()
|
|
13895
|
+
return
|
|
13896
|
+
}
|
|
13506
13897
|
// v727:开启「点按角色推进泡泡队列」→ 点角色=往后推进一项(不再回到第 1 项);
|
|
13507
13898
|
// 已是最后一项时与「点泡泡」一致:收起泡泡,下次点按从第 1 项开始。
|
|
13508
13899
|
if (bubbleTapAdvance) { bubbleNext(); return }
|
|
@@ -13523,9 +13914,12 @@ function bubbleNext() {
|
|
|
13523
13914
|
if (!bubbleShown) return
|
|
13524
13915
|
if (bubbleScene && bubbleScene.kind === 'cost') { hideCostBubble(); return }
|
|
13525
13916
|
if (bubbleScene && bubbleScene.kind === 'alert') { hideUsageAlertBubble(); return }
|
|
13526
|
-
//
|
|
13527
|
-
|
|
13528
|
-
|
|
13917
|
+
// v777:等待交互泡泡**可以点掉**(用户反馈「无法点击关闭」)。原来的实现在这里是 `return`
|
|
13918
|
+
//(设计意图是"别让误点把'正在等待你回答'收掉"),但真机上用户就是想把它点掉 ⇒ 改成:
|
|
13919
|
+
// 点泡泡 = 收起,并记住"这条挂起已被点掉",pollWaitState 不再自动弹回(回答/批准或下一次新挂起照常)。
|
|
13920
|
+
if (bubbleScene && bubbleScene.kind === 'wait') { dismissWaitBubble(); return }
|
|
13921
|
+
// v761(#161 C5)曾在此处"等待交互泡泡点它不关"(必须等被回答/批准)——
|
|
13922
|
+
// v777 按用户反馈改成"可点关",语义与实现见上面的 dismissWaitBubble();这里不再拦截。
|
|
13529
13923
|
if (bubbleRoundOn && bubbleSeqIdx < bubbleSeq.length) { bubbleShowSeqNext(); return }
|
|
13530
13924
|
hideBubble()
|
|
13531
13925
|
} catch (err) {}
|
|
@@ -13552,6 +13946,13 @@ function hideBubble() {
|
|
|
13552
13946
|
}
|
|
13553
13947
|
function showCostBubble(amount) {
|
|
13554
13948
|
if (!bubbleOn || !turnCostOn) return
|
|
13949
|
+
// v778:②区「冒泡提示」勾选框(events.turnCost.bubbleOn)以前**只写不读** —— 取消勾选照样弹。
|
|
13950
|
+
// 同类问题("控件有值、没有消费方")与本次修的音量滑块同源,一并接上:
|
|
13951
|
+
// 缺省(未设置/旧数据)= 照常冒泡,只有**显式**取消勾选才不弹 ⇒ 老用户行为不变。
|
|
13952
|
+
try {
|
|
13953
|
+
var tcCfg = soundEventCfg('turnCost') || {}
|
|
13954
|
+
if (tcCfg.bubbleOn === false) return
|
|
13955
|
+
} catch (err) {}
|
|
13555
13956
|
// 进入系统泡泡队列(等级3):若同批有预警/预算,则排在它们之后展示
|
|
13556
13957
|
whaleSysPush({ kind: 'cost', amount: amount, rank: 3 })
|
|
13557
13958
|
}
|
|
@@ -13769,6 +14170,23 @@ function hideWaitBubble() {
|
|
|
13769
14170
|
if (whaleSysQueue.length !== beforeLen) whaleSysTick()
|
|
13770
14171
|
} catch (err) {}
|
|
13771
14172
|
}
|
|
14173
|
+
// v777(用户反馈「授权提示/提问提示无法点击关闭」):
|
|
14174
|
+
// · 默认:**点泡泡**即可关掉等待气泡(点角色不管,除非打开下面的开关);
|
|
14175
|
+
// · 开关「点按角色关闭提示气泡」打开后:**点角色**也能关;
|
|
14176
|
+
// · 关掉的是"这条挂起"——宿主每秒还会下发同一条挂起(pollWaitState 是每分钟…不是,是每秒对齐一次),
|
|
14177
|
+
// 所以必须记住"用户把这条点掉了、别再自动弹回",否则关掉后 1 秒内又回来 = 看起来根本关不掉。
|
|
14178
|
+
var waitDismissedId = '' // 被用户点掉的挂起 id('' = 没有)
|
|
14179
|
+
var waitPendingId = '' // 宿主最近一次上报的挂起 id(用于"点掉的就是当前这条")
|
|
14180
|
+
function waitCharCloseOn() {
|
|
14181
|
+
try { return !!(((usageSet || {}).wait || {}).charClose) } catch (err) { return false }
|
|
14182
|
+
}
|
|
14183
|
+
// 用户主动收起等待气泡:记住这条挂起已被点掉,然后收起
|
|
14184
|
+
function dismissWaitBubble() {
|
|
14185
|
+
try {
|
|
14186
|
+
if (waitPendingId) waitDismissedId = waitPendingId
|
|
14187
|
+
hideWaitBubble()
|
|
14188
|
+
} catch (err) {}
|
|
14189
|
+
}
|
|
13772
14190
|
function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v) }
|
|
13773
14191
|
function viewport() {
|
|
13774
14192
|
return {
|
|
@@ -13846,6 +14264,10 @@ function express() {
|
|
|
13846
14264
|
root.style.right = 'auto'
|
|
13847
14265
|
root.style.bottom = 'auto'
|
|
13848
14266
|
root.style.left = state.left + 'px'
|
|
14267
|
+
// v785:最后一道保险 —— 所有"写 state.top"的路径最终都会走到这里,统一把顶边夹出"窗口控件带"
|
|
14268
|
+
// (拖动/吸附/settle 各自也夹了一次;这里兜住启动、自愈、外部直接改 state 等路径)。
|
|
14269
|
+
// 见 clampWidgetTop:下限 = 叠层下沿(普通浏览器为 0 ⇒ 与以前完全一致)。
|
|
14270
|
+
state.top = clampWidgetTop(state.top)
|
|
13849
14271
|
root.style.top = state.top + 'px'
|
|
13850
14272
|
root.classList.toggle('dshwv-left', !!state.flip)
|
|
13851
14273
|
}
|
|
@@ -13856,7 +14278,7 @@ function settle() {
|
|
|
13856
14278
|
if (drag && drag.active) {
|
|
13857
14279
|
// mid-drag resize: keep the pointer-follow position, just clamp into view
|
|
13858
14280
|
state.left = clamp(state.left, 0, Math.max(0, vp.w - w - rightGap()))
|
|
13859
|
-
state.top =
|
|
14281
|
+
state.top = clampWidgetTop(state.top, Math.max(0, vp.h - h))
|
|
13860
14282
|
express()
|
|
13861
14283
|
return
|
|
13862
14284
|
}
|
|
@@ -13880,22 +14302,22 @@ function settle() {
|
|
|
13880
14302
|
}
|
|
13881
14303
|
if (state.v === 'bottom') {
|
|
13882
14304
|
var rawB = vp.h - h - state.vOff
|
|
13883
|
-
state.top =
|
|
14305
|
+
state.top = clampWidgetTop(rawB, maxT)
|
|
13884
14306
|
if (state.top !== rawB) outOfRange = true
|
|
13885
14307
|
} else if (state.v === 'top') {
|
|
13886
14308
|
var rawT = state.vOff
|
|
13887
|
-
state.top =
|
|
14309
|
+
state.top = clampWidgetTop(rawT, maxT)
|
|
13888
14310
|
if (state.top !== rawT) outOfRange = true
|
|
13889
14311
|
} else {
|
|
13890
|
-
state.top =
|
|
14312
|
+
state.top = clampWidgetTop(state.top, maxT)
|
|
13891
14313
|
}
|
|
13892
14314
|
// 偏移确实越界(脏数据 / 视口变小):先把偏移夹回合法范围再落盘,下次启动不再复现。
|
|
13893
14315
|
// 只在真的越界时写,正常 resize 不会产生额外的 localStorage 写入。
|
|
13894
14316
|
if (outOfRange) {
|
|
13895
14317
|
if (state.h === 'right') state.hOff = clamp(state.hOff, 0, Math.max(0, vp.w - w - rightGap()))
|
|
13896
14318
|
else if (state.h === 'left') state.hOff = clamp(state.hOff, 0, maxLAnchor)
|
|
13897
|
-
if (state.v === 'bottom') state.vOff = clamp(state.vOff, 0, maxT)
|
|
13898
|
-
else if (state.v === 'top') state.vOff = clamp(state.vOff,
|
|
14319
|
+
if (state.v === 'bottom') state.vOff = clamp(state.vOff, 0, Math.max(0, maxT - widgetTopMin()))
|
|
14320
|
+
else if (state.v === 'top') state.vOff = clamp(state.vOff, widgetTopMin(), maxT)
|
|
13899
14321
|
try { saveAnchorPos() } catch (err) {}
|
|
13900
14322
|
}
|
|
13901
14323
|
refreshFlip()
|
|
@@ -14003,6 +14425,9 @@ function refresh(manual) {
|
|
|
14003
14425
|
balanceRetryLeft = 2
|
|
14004
14426
|
state.todayUsage = data.todayUsage !== undefined ? data.todayUsage : null
|
|
14005
14427
|
state.todayUsageCurrency = data.todayUsageCurrency || data.currency || 'CNY'
|
|
14428
|
+
// v781:赠金 / 充值余额(宿主两条路都下发;缺失给 null ⇒ 模块显示 —)
|
|
14429
|
+
state.bonusBalance = isFinite(Number(data.bonusBalance)) ? Number(data.bonusBalance) : null
|
|
14430
|
+
state.rechargeBalance = isFinite(Number(data.rechargeBalance)) ? Number(data.rechargeBalance) : null
|
|
14006
14431
|
state.usageLabel = data.usageLabel || '本地估算'
|
|
14007
14432
|
if (data.stale) state.usageLabel += ' · 余额未刷新'
|
|
14008
14433
|
state.isPeak = !!data.isPeak
|
|
@@ -14037,6 +14462,9 @@ function refresh(manual) {
|
|
|
14037
14462
|
} else {
|
|
14038
14463
|
state.status = 'error'
|
|
14039
14464
|
state.message = (data && data.error) ? String(data.error) : '获取失败'
|
|
14465
|
+
// v781:把宿主的错误码也打到控制台 —— "只登录客户端读不到余额"这类反馈里,
|
|
14466
|
+
// 界面只显示一句人类可读的文案,而 code(NO_KEY / BOTH_FAILED / SHAPE …)才是排查入口。
|
|
14467
|
+
try { console.warn('[dsh-whale] 余额读取失败', (data && data.code) || '', state.message) } catch (err) {}
|
|
14040
14468
|
render()
|
|
14041
14469
|
balanceRetryLater()
|
|
14042
14470
|
}
|
|
@@ -14292,7 +14720,7 @@ function setScale(v) {
|
|
|
14292
14720
|
} else {
|
|
14293
14721
|
state.left = Math.min(Math.max(fx - r2.width, 0), Math.max(0, vp.w - r2.width))
|
|
14294
14722
|
}
|
|
14295
|
-
state.top = Math.min(Math.max(fy - r2.height, 0), Math.max(0, vp.h - r2.height))
|
|
14723
|
+
state.top = clampWidgetTop(Math.min(Math.max(fy - r2.height, 0), Math.max(0, vp.h - r2.height)))
|
|
14296
14724
|
express()
|
|
14297
14725
|
// 恢复过渡必须延迟到下一帧:本帧 left/top 已在 none 下设置并提交,
|
|
14298
14726
|
// 立即恢复会让浏览器对「刚改过的 left/top」重新评估并播放过渡动画
|
|
@@ -16303,7 +16731,8 @@ function onDocPointerMove(e) {
|
|
|
16303
16731
|
// were); on release endDrag() recomputes the anchors and settle() flips the
|
|
16304
16732
|
// class with a smooth transition instead of reverting instantly.
|
|
16305
16733
|
state.left = clamp(drag.origLeft + dx, 0, Math.max(0, drag.vp.w - drag.w))
|
|
16306
|
-
|
|
16734
|
+
// v785:拖拽时就把"桌面端窗口控件带"排除在可移动范围外(顶边下限 = 叠层下沿)
|
|
16735
|
+
state.top = clampWidgetTop(drag.origTop + dy, Math.max(0, drag.vp.h - drag.h))
|
|
16307
16736
|
express()
|
|
16308
16737
|
}
|
|
16309
16738
|
function onDocPointerUp(e) {
|
|
@@ -16555,7 +16984,8 @@ function endDrag(e, clickAllowed, cancelled) {
|
|
|
16555
16984
|
var dx = e.clientX - drag.startX
|
|
16556
16985
|
var dy = e.clientY - drag.startY
|
|
16557
16986
|
var left = clamp(drag.origLeft + dx, 0, Math.max(0, drag.vp.w - drag.w))
|
|
16558
|
-
|
|
16987
|
+
// v785:落位计算也要先排除"窗口控件带"(否则吸附判定点会用到带内的 top)
|
|
16988
|
+
var top = clampWidgetTop(drag.origTop + dy, Math.max(0, drag.vp.h - drag.h))
|
|
16559
16989
|
// 自定义吸附区(比例/绝对/关闭)。判定点:左右吸附/翻转 = 图像中心 x,
|
|
16560
16990
|
// 下吸附 = 图像中心 y,上吸附 = 挂件盒中心 y。
|
|
16561
16991
|
// 解析计算,避免拖动结束过渡期强制布局。
|
|
@@ -16611,7 +17041,8 @@ function applyAnchorPos() {
|
|
|
16611
17041
|
var l = a.hAnchor === 'left' ? hDist : vp.w - effectiveRightDist - w
|
|
16612
17042
|
var t = a.vAnchor === 'top' ? vDist : vp.h - vDist - h
|
|
16613
17043
|
state.left = clamp(l, 0, maxOffH)
|
|
16614
|
-
|
|
17044
|
+
// v785:锚点自愈/恢复也要排除"窗口控件带"(这条路径不一定紧跟 express,所以就地夹)
|
|
17045
|
+
state.top = clampWidgetTop(clamp(t, 0, maxOffV), maxOffV)
|
|
16615
17046
|
state.h = a.hAnchor
|
|
16616
17047
|
// 净距离直接还给 hOff/vOff:settle() 对锚定状态从偏移量重算,
|
|
16617
17048
|
// 若置 0 会把刚恢复的距离覆盖成贴边(issue #43)
|
|
@@ -16783,17 +17214,27 @@ function pollWaitState() {
|
|
|
16783
17214
|
if (!p || !p.kind) {
|
|
16784
17215
|
// 挂起已解除:收起泡泡,并允许下一次挂起照常响
|
|
16785
17216
|
waitSeenId = ''
|
|
17217
|
+
waitPendingId = '' // v777:挂起解除 ⇒ "被点掉"的标记一并作废(下一次挂起照常冒泡)
|
|
17218
|
+
waitDismissedId = ''
|
|
16786
17219
|
if (typeof hideWaitBubble === 'function') hideWaitBubble()
|
|
16787
17220
|
return
|
|
16788
17221
|
}
|
|
16789
17222
|
var kind = p.kind === 'approval' ? 'approval' : 'question'
|
|
16790
17223
|
var ev = waitEventCfg(kind)
|
|
17224
|
+
// v777:这条挂起已经被用户点掉了 ⇒ 不再自动弹回(但仍然照常响铃判定与去重;回答/批准后
|
|
17225
|
+
// 上面的 !p 分支会把标记清掉,所以"下一次新的提问/授权"照常冒泡)。
|
|
17226
|
+
waitPendingId = String(p.id || '')
|
|
17227
|
+
if (waitPendingId && waitPendingId === waitDismissedId) {
|
|
17228
|
+
// 什么都不做:既不显示,也不清 waitSeenId(同一挂起不重复响)
|
|
17229
|
+
} else {
|
|
17230
|
+
if (ev.bubbleOn !== false && typeof showWaitBubble === 'function') showWaitBubble(kind, p)
|
|
17231
|
+
}
|
|
16791
17232
|
// v771:**每秒都把泡泡与宿主的挂起状态对齐一次**(showWaitBubble 幂等:同类直接返回 false,
|
|
16792
17233
|
// 不会重渲染)。旧实现只在"新 id"时才调显示,于是一旦出现瞬时不同步(换类型 / 泡泡被别的
|
|
16793
17234
|
// 场景顶掉 / 某次渲染失败),泡泡就会卡在那个状态再也回不来 —— 这正是真机那次卡死的另一半原因。
|
|
16794
17235
|
// 现在任何不同步都会在 1 秒内自愈:该换内容就换、该收就收(收在下面 !p 分支)。
|
|
16795
17236
|
// 门控照旧:该事件的「冒泡提示」关掉就不冒泡(与声音各自独立判)。
|
|
16796
|
-
|
|
17237
|
+
// v777:显示那一步已上移到"被点掉的挂起"判定里(见上面的 waitDismissedId 分支),这里不再重复调用。
|
|
16797
17238
|
// 声音:同一个未回答的挂起只响一次(刷新页面也不重复响)
|
|
16798
17239
|
var key = kind + ':' + String(p.id || '')
|
|
16799
17240
|
var remembered = ''
|