dsh-whale-widget 0.3.14 → 0.3.16
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 +38 -12
- package/assets/whale-widget.js +1477 -167
- package/lib/accounting.mjs +68 -3
- package/lib/index.js +311 -26
- package/package.json +1 -1
package/assets/whale-widget.js
CHANGED
|
@@ -113,7 +113,7 @@ function dshwvAudioIdleArm() {
|
|
|
113
113
|
} catch (err) {}
|
|
114
114
|
}
|
|
115
115
|
// 音效是否被**显式**关掉(v753 的显式开关)。这里刻意用 typeof 保护:
|
|
116
|
-
//
|
|
116
|
+
// 本文件经常被离线自检按片段切出来单独 eval(例如只切「音频垫片 + applySoundSet」那段),
|
|
117
117
|
// 那些沙箱里不一定声明了 soundOn —— typeof 对未声明的标识符是安全的,裸引用会直接抛
|
|
118
118
|
// ReferenceError 把整段沙箱打断。真机上 soundOn 一定存在,行为不受影响。
|
|
119
119
|
function dshwvSoundOff() {
|
|
@@ -195,6 +195,32 @@ function dshwvSoundStop(el) {
|
|
|
195
195
|
try { node.stop() } catch (err) {}
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
+
// v766:**试听名单** —— 供"停止试听"用。为什么要它:`dshwvSound()` 每次都返回一个全新元素,
|
|
199
|
+
// 而任务结束音 / 片段 / 音效组三条试听路径都是**匿名调用**(外面拿不到句柄),播起来之后就停不掉了。
|
|
200
|
+
// 这里只登记 `dshwvPreviewMark > 0` 期间创建的元素 —— 也就是**只登记试听**,绝不误伤真实事件音
|
|
201
|
+
// (真实的任务结束音 / 提问授权提示音是用户要听的,不能在面板里被顺手掐掉)。
|
|
202
|
+
var dshwvPreviewEls = []
|
|
203
|
+
var dshwvPreviewMark = 0
|
|
204
|
+
function dshwvPreviewOn() { dshwvPreviewMark++ }
|
|
205
|
+
function dshwvPreviewOff() { dshwvPreviewMark = Math.max(0, dshwvPreviewMark - 1) }
|
|
206
|
+
function dshwvPreviewTrack(el) {
|
|
207
|
+
if (!(dshwvPreviewMark > 0)) return
|
|
208
|
+
try {
|
|
209
|
+
dshwvPreviewEls.push(el)
|
|
210
|
+
if (dshwvPreviewEls.length > 12) {
|
|
211
|
+
// 播完的(_node 已清空)不再留着,避免名单无限增长
|
|
212
|
+
dshwvPreviewEls = dshwvPreviewEls.filter(function (x) { return x && x._node })
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {}
|
|
215
|
+
}
|
|
216
|
+
function dshwvStopPreviews() {
|
|
217
|
+
try {
|
|
218
|
+
for (var i = 0; i < dshwvPreviewEls.length; i++) {
|
|
219
|
+
try { dshwvSoundStop(dshwvPreviewEls[i]) } catch (err) {}
|
|
220
|
+
}
|
|
221
|
+
dshwvPreviewEls = dshwvPreviewEls.filter(function (x) { return x && x._node })
|
|
222
|
+
} catch (err) {}
|
|
223
|
+
}
|
|
198
224
|
function dshwvSound(url) {
|
|
199
225
|
var el = { preload: 'auto', volume: 1, onended: null, loop: false, _url: String(url || ''), _node: null, _gain: null, _offset: 0, _token: 0, _playingSince: 0 }
|
|
200
226
|
Object.defineProperty(el, 'src', {
|
|
@@ -276,6 +302,7 @@ function dshwvSound(url) {
|
|
|
276
302
|
el.pause = function () { dshwvSoundStop(el) }
|
|
277
303
|
// v752:备用路由(首选失败时自动切过去);由 applySoundSet/playTaskEndGroupClick 填
|
|
278
304
|
el._alt = ''
|
|
305
|
+
dshwvPreviewTrack(el) // v766:只在试听期间登记(见上面的试听名单)
|
|
279
306
|
return el
|
|
280
307
|
}
|
|
281
308
|
|
|
@@ -678,6 +705,9 @@ var css = [
|
|
|
678
705
|
// 新增跑马灯:墨韵黑白 / 靛蓝夜曲(文字·内层文字·菜单)
|
|
679
706
|
'.dshwv-trow.dshwv-rgb-ink,.dshwv-trowtx.dshwv-rgb-ink,.dshwv-qcolmenu .dshwv-rgbopt.opt-ink{background-image:linear-gradient(90deg,rgb(20,20,20),rgb(80,80,80),rgb(140,140,140),rgb(200,200,200),rgb(250,250,250),rgb(250,250,250),rgb(200,200,200),rgb(140,140,140),rgb(80,80,80),rgb(20,20,20))}',
|
|
680
707
|
'.dshwv-trow.dshwv-rgb-indigo,.dshwv-trowtx.dshwv-rgb-indigo,.dshwv-qcolmenu .dshwv-rgbopt.opt-indigo{background-image:linear-gradient(90deg,rgb(32,49,112),rgb(52,76,146),rgb(74,102,180),rgb(100,126,210),rgb(130,132,224),rgb(130,132,224),rgb(100,126,210),rgb(74,102,180),rgb(52,76,146),rgb(32,49,112))}',
|
|
708
|
+
// v770 新增跑马灯:火红烈焰(深绯红→橙→白热芯) / 警示橙黄(高饱和橙→警示黄)
|
|
709
|
+
'.dshwv-trow.dshwv-rgb-blaze,.dshwv-trowtx.dshwv-rgb-blaze,.dshwv-qcolmenu .dshwv-rgbopt.opt-blaze{background-image:linear-gradient(90deg,rgb(150,15,25),rgb(200,30,30),rgb(240,70,25),rgb(255,130,30),rgb(255,200,60),rgb(255,240,170),rgb(255,200,60),rgb(255,130,30),rgb(240,70,25),rgb(200,30,30),rgb(150,15,25))}',
|
|
710
|
+
'.dshwv-trow.dshwv-rgb-amber,.dshwv-trowtx.dshwv-rgb-amber,.dshwv-qcolmenu .dshwv-rgbopt.opt-amber{background-image:linear-gradient(90deg,rgb(230,90,0),rgb(255,140,0),rgb(255,180,0),rgb(255,215,40),rgb(255,240,120),rgb(255,215,40),rgb(255,180,0),rgb(255,140,0),rgb(230,90,0))}',
|
|
681
711
|
// 文字底色(圆角矩形底层):跑马灯底色与文字颜色/文字跑马灯不互相占用,底色铺在文字下方
|
|
682
712
|
'.dshwv-trow.dshwv-bgrgb{text-shadow:none;background-size:200% auto;animation:dshwvRainbow 2.6s linear infinite}',
|
|
683
713
|
'.dshwv-trow.dshwv-bgrgb-macaron{background-image:linear-gradient(90deg,rgb(255,180,200),rgb(255,205,170),rgb(255,225,165),rgb(245,240,180),rgb(190,240,210),rgb(180,230,245),rgb(190,215,250),rgb(220,200,245),rgb(240,200,230),rgb(255,180,200))}',
|
|
@@ -696,6 +726,9 @@ var css = [
|
|
|
696
726
|
// 新增跑马灯底色:墨韵黑白 / 靛蓝夜曲
|
|
697
727
|
'.dshwv-trow.dshwv-bgrgb-ink{background-image:linear-gradient(90deg,rgb(20,20,20),rgb(80,80,80),rgb(140,140,140),rgb(200,200,200),rgb(250,250,250),rgb(250,250,250),rgb(200,200,200),rgb(140,140,140),rgb(80,80,80),rgb(20,20,20))}',
|
|
698
728
|
'.dshwv-trow.dshwv-bgrgb-indigo{background-image:linear-gradient(90deg,rgb(32,49,112),rgb(52,76,146),rgb(74,102,180),rgb(100,126,210),rgb(130,132,224),rgb(130,132,224),rgb(100,126,210),rgb(74,102,180),rgb(52,76,146),rgb(32,49,112))}',
|
|
729
|
+
// v770 新增跑马灯底色:火红烈焰 / 警示橙黄
|
|
730
|
+
'.dshwv-trow.dshwv-bgrgb-blaze{background-image:linear-gradient(90deg,rgb(150,15,25),rgb(200,30,30),rgb(240,70,25),rgb(255,130,30),rgb(255,200,60),rgb(255,240,170),rgb(255,200,60),rgb(255,130,30),rgb(240,70,25),rgb(200,30,30),rgb(150,15,25))}',
|
|
731
|
+
'.dshwv-trow.dshwv-bgrgb-amber{background-image:linear-gradient(90deg,rgb(230,90,0),rgb(255,140,0),rgb(255,180,0),rgb(255,215,40),rgb(255,240,120),rgb(255,215,40),rgb(255,180,0),rgb(255,140,0),rgb(230,90,0))}',
|
|
699
732
|
// 内层文字(带底色时)也支持跑马灯文字颜色:与 .dshwv-trow 相同的 渐变裁字 规则
|
|
700
733
|
'.dshwv-trowtx.dshwv-rgb{background-image:linear-gradient(90deg,rgb(255,180,200),rgb(255,205,170),rgb(255,225,165),rgb(245,240,180),rgb(190,240,210),rgb(180,230,245),rgb(190,215,250),rgb(220,200,245),rgb(240,200,230),rgb(255,180,200));background-size:200% auto;-webkit-background-clip:text;background-clip:text;color:transparent;-webkit-text-fill-color:transparent;animation:dshwvRainbow 2.6s linear infinite;text-shadow:none}',
|
|
701
734
|
'.dshwv-trowtx.dshwv-rgb-candy{background-image:linear-gradient(90deg,rgb(255,145,170),rgb(255,170,130),rgb(255,195,110),rgb(240,220,115),rgb(140,220,175),rgb(115,210,205),rgb(130,195,240),rgb(160,170,235),rgb(210,155,230),rgb(235,135,190),rgb(255,145,170))}',
|
|
@@ -1121,7 +1154,7 @@ function dshwCustSel(sel, opts) {
|
|
|
1121
1154
|
dshwCustSelOpen = { menu: menu, btn: btn }
|
|
1122
1155
|
})
|
|
1123
1156
|
sync()
|
|
1124
|
-
return { sync: sync, refresh: function () { fill(); sync() } }
|
|
1157
|
+
return { sync: sync, refresh: function () { fill(); sync() }, btn: btn, wrap: wrap }
|
|
1125
1158
|
}
|
|
1126
1159
|
// 用量:小鲸鱼记账为唯一记账方式;「用量记录」按钮打开历史用量子面板
|
|
1127
1160
|
var usageRecBtn = document.createElement('button')
|
|
@@ -1165,13 +1198,21 @@ taskEndSel.addEventListener('change', function () {
|
|
|
1165
1198
|
applyTaskEndLocal(undefined, taskEndSel.value)
|
|
1166
1199
|
commitTaskEnd()
|
|
1167
1200
|
})
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1201
|
+
// v761(全局音效设置面板):音效下拉的**通用填充**(任务结束音 / 提问 / 授权三处共用)。
|
|
1202
|
+
// 排列:置顶项恒最前(按置顶先后) → 已导入片段 → 4 个预设单音 → 音效组置底;
|
|
1203
|
+
// 组与组可同名,按 value 去重(不按显示名);其余按 value/显示名去重。
|
|
1204
|
+
// 只改 DOM 与(任务结束音原有的)兜底设置,不写任何新键 ——
|
|
1205
|
+
// 任务结束音(selectEl === taskEndSel)完全沿用原有语义,其它下拉找不到现值时保留原值显示。
|
|
1206
|
+
// v762:**删掉了原第一位的「静音」项**(value 为空串)——「不响」统一由音效行左边的 [✓] 表达
|
|
1207
|
+
// (提问/授权 = events.<kind>.soundOn,任务结束音 = taskEnd.on),一个开关就够了,不再有语义重叠。
|
|
1208
|
+
// 连带效应(正是我们要的):空/失效的 sel 不再被「静音」项接住,于是重新走到下面的兜底分支
|
|
1209
|
+
// (任务结束音挑默认音并**写回 usageSet**),下拉里再也不会出现空值。
|
|
1210
|
+
function fillSoundSelect(selectEl, currentSel) {
|
|
1211
|
+
if (!selectEl) return false
|
|
1212
|
+
var isTaskEnd = selectEl === taskEndSel
|
|
1171
1213
|
// 期望值优先取“持久化设置”,避免启动时被上一轮的临时默认值带偏
|
|
1172
|
-
var
|
|
1173
|
-
|
|
1174
|
-
taskEndSel.innerHTML = ''
|
|
1214
|
+
var cur = String(currentSel == null ? '' : currentSel)
|
|
1215
|
+
selectEl.innerHTML = ''
|
|
1175
1216
|
var seen = {}
|
|
1176
1217
|
var seenLbl = {}
|
|
1177
1218
|
function add(v, lab) {
|
|
@@ -1180,7 +1221,7 @@ function fillTaskEndOptions(pref) {
|
|
|
1180
1221
|
if (seenLbl[lab]) return
|
|
1181
1222
|
seen[v] = 1
|
|
1182
1223
|
seenLbl[lab] = 1
|
|
1183
|
-
|
|
1224
|
+
selectEl.appendChild(soundOpt(v, lab))
|
|
1184
1225
|
}
|
|
1185
1226
|
// 收集全部候选(片段+单音在前,组置底),返回 {v,lab,grp} 列表
|
|
1186
1227
|
var grps = Array.isArray(audioGroups) ? audioGroups : []
|
|
@@ -1216,9 +1257,9 @@ function fillTaskEndOptions(pref) {
|
|
|
1216
1257
|
var allCand = fragCand.concat(preCand, grpCand)
|
|
1217
1258
|
var candByV = {}
|
|
1218
1259
|
allCand.forEach(function (c) { candByV[c.v] = c })
|
|
1219
|
-
// 置顶序列(仍存在的选项按 pin 顺序排最前)
|
|
1260
|
+
// 置顶序列(仍存在的选项按 pin 顺序排最前);置顶只属于任务结束音(值存 usageSet.taskEnd.pins)
|
|
1220
1261
|
var pins = []
|
|
1221
|
-
try { if (usageSet && usageSet.taskEnd && Array.isArray(usageSet.taskEnd.pins)) pins = usageSet.taskEnd.pins.slice() } catch (err) {}
|
|
1262
|
+
if (isTaskEnd) { try { if (usageSet && usageSet.taskEnd && Array.isArray(usageSet.taskEnd.pins)) pins = usageSet.taskEnd.pins.slice() } catch (err) {} }
|
|
1222
1263
|
var ordered = []
|
|
1223
1264
|
var orderedSeen = {}
|
|
1224
1265
|
pins.forEach(function (pv) {
|
|
@@ -1235,48 +1276,75 @@ function fillTaskEndOptions(pref) {
|
|
|
1235
1276
|
if (c.grp) {
|
|
1236
1277
|
if (seen[c.v]) return
|
|
1237
1278
|
seen[c.v] = 1
|
|
1238
|
-
|
|
1279
|
+
selectEl.appendChild(soundOpt(c.v, c.lab))
|
|
1239
1280
|
} else add(c.v, c.lab)
|
|
1240
1281
|
})
|
|
1241
1282
|
var fragN = 0
|
|
1242
|
-
for (var fi = 0; fi <
|
|
1283
|
+
for (var fi = 0; fi < selectEl.options.length; fi++) if (String(selectEl.options[fi].value).indexOf('frag:') === 0) fragN++
|
|
1243
1284
|
var found = false
|
|
1244
|
-
for (var i = 0; i <
|
|
1285
|
+
for (var i = 0; i < selectEl.options.length; i++) if (selectEl.options[i].value === cur) { selectEl.value = cur; found = true; break }
|
|
1245
1286
|
if (!found) {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
taskEndSel.value
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1287
|
+
if (isTaskEnd) {
|
|
1288
|
+
// 期望值是自导入片段,但片段列表尚未就绪(启动时音频还在加载):
|
|
1289
|
+
// 先不落默认、不改写 usageSet,等片段就绪后的下一次 fill 再定,
|
|
1290
|
+
// 避免把用户的片段选择悄悄覆盖成「小黄鸭·按下」并随后续保存持久化。
|
|
1291
|
+
// 注意:这里把 value 置空是**流程需要**(fillTaskEndOptions 靠 `taskEndSel.value || prefSel`
|
|
1292
|
+
// 在下一轮重新取到真实值),与已删除的「静音」项无关 —— 置空后本次会显示为未选中,属预期。
|
|
1293
|
+
if (cur && String(cur).indexOf('frag:') === 0 && fragN === 0) {
|
|
1294
|
+
selectEl.value = ''
|
|
1295
|
+
if (taskEndDrop) taskEndDrop.refresh()
|
|
1296
|
+
return false
|
|
1297
|
+
}
|
|
1298
|
+
// 期望值是音效组,但组列表尚未就绪(启动时 audio.json 还在加载):
|
|
1299
|
+
// 同样先不落默认、不改写 usageSet,等组就绪后的下一次 fill 再定
|
|
1300
|
+
if (cur && String(cur).indexOf('grp:') === 0 && (!Array.isArray(audioGroups) || audioGroups.length === 0)) {
|
|
1301
|
+
selectEl.value = ''
|
|
1302
|
+
if (taskEndDrop) taskEndDrop.refresh()
|
|
1303
|
+
return false
|
|
1304
|
+
}
|
|
1305
|
+
// 默认:优先 name==='entity',否则第一条已导入片段;都没有则选预设
|
|
1306
|
+
var chosen = 'preset:duck:press'
|
|
1307
|
+
for (var j = 0; j < selectEl.options.length; j++) {
|
|
1308
|
+
var v = selectEl.options[j].value
|
|
1309
|
+
if (v.indexOf('frag:') === 0) { chosen = v; if (String(selectEl.options[j].textContent || '') === 'entity') break }
|
|
1310
|
+
}
|
|
1311
|
+
selectEl.value = chosen
|
|
1312
|
+
usageSet = usageSet || {}
|
|
1313
|
+
usageSet.taskEnd = usageSet.taskEnd || { on: false, sel: '' }
|
|
1314
|
+
usageSet.taskEnd.sel = chosen
|
|
1315
|
+
} else if (cur) {
|
|
1316
|
+
// 面板下拉:绑定的音效还没加载出来(如音频列表未就绪)——原样保留并显示,
|
|
1317
|
+
// 不静默改成别的音效(否则用户一保存就把自己的选择丢了)
|
|
1318
|
+
selectEl.appendChild(soundOpt(cur, cur + '(暂未加载)'))
|
|
1319
|
+
selectEl.value = cur
|
|
1320
|
+
} else {
|
|
1321
|
+
// v762:「静音」项已删除,空值不再有任何含义。面板下拉(提问/授权)走到这里表示
|
|
1322
|
+
// 「没有给值」,兜底选中第一项,保证下拉永远有一个**非空**值
|
|
1323
|
+
// (pollWaitState 的 `on && sel` 门控因此仍然成立)。
|
|
1324
|
+
// 任务结束音不会走到这里(上面 isTaskEnd 分支已处理过 cur 为空的情况)。
|
|
1325
|
+
selectEl.selectedIndex = selectEl.options.length ? 0 : -1
|
|
1266
1326
|
}
|
|
1267
|
-
taskEndSel.value = chosen
|
|
1268
|
-
usageSet = usageSet || {}
|
|
1269
|
-
usageSet.taskEnd = usageSet.taskEnd || { on: false, sel: '' }
|
|
1270
|
-
usageSet.taskEnd.sel = chosen
|
|
1271
1327
|
}
|
|
1272
|
-
if (pref && pref.sel) { for (var k = 0; k < taskEndSel.options.length; k++) if (taskEndSel.options[k].value === pref.sel) taskEndSel.value = pref.sel }
|
|
1273
1328
|
// 兜底:按 value 从尾向前去重,确保绝无重复项
|
|
1274
1329
|
var seen2 = {}
|
|
1275
|
-
for (var di =
|
|
1276
|
-
var dv =
|
|
1277
|
-
if (seen2[dv]) { try {
|
|
1330
|
+
for (var di = selectEl.options.length - 1; di >= 0; di--) {
|
|
1331
|
+
var dv = selectEl.options[di].value
|
|
1332
|
+
if (seen2[dv]) { try { selectEl.remove(di) } catch (err) {} }
|
|
1278
1333
|
else seen2[dv] = 1
|
|
1279
1334
|
}
|
|
1335
|
+
try {
|
|
1336
|
+
if (isTaskEnd) { if (taskEndDrop) taskEndDrop.refresh() }
|
|
1337
|
+
else if (selectEl.__dshwDrop) selectEl.__dshwDrop.refresh()
|
|
1338
|
+
} catch (err) {}
|
|
1339
|
+
return found
|
|
1340
|
+
}
|
|
1341
|
+
// 任务结束音下拉:入口与语义不变,内部改走通用 fillSoundSelect
|
|
1342
|
+
// (v762 起选项里**不再有「静音」**:不响由 taskEnd.on / 面板音效行的 [✓] 表达)
|
|
1343
|
+
function fillTaskEndOptions(pref) {
|
|
1344
|
+
var prefSel = (usageSet && usageSet.taskEnd && usageSet.taskEnd.sel) || (pref && pref.sel) || ''
|
|
1345
|
+
var cur = taskEndSel.value || prefSel || ''
|
|
1346
|
+
fillSoundSelect(taskEndSel, cur)
|
|
1347
|
+
if (pref && pref.sel) { for (var k = 0; k < taskEndSel.options.length; k++) if (taskEndSel.options[k].value === pref.sel) taskEndSel.value = pref.sel }
|
|
1280
1348
|
if (taskEndDrop) taskEndDrop.refresh()
|
|
1281
1349
|
}
|
|
1282
1350
|
// 片段列表变化后刷新任务结束音下拉(保留用户当前选择)
|
|
@@ -1310,6 +1378,53 @@ function taskEndTogglePin(v) {
|
|
|
1310
1378
|
fillTaskEndOptions((usageSet && usageSet.taskEnd) || null)
|
|
1311
1379
|
} catch (err) {}
|
|
1312
1380
|
}
|
|
1381
|
+
// v761(全局音效设置):**通用事件音效播放**。
|
|
1382
|
+
// 任务结束音 / 提问音 / 授权音三处共用;不改写 playTaskEndSound 里那段已验证的取值逻辑
|
|
1383
|
+
// (frag: 片段 / preset: 预设 / grp: 音效组 + 备用路由),而是**临时替换它读取的绑定与音量再还原**。
|
|
1384
|
+
// v761:音量取值统一走这里 —— **绝不能用 `Number(x) || 0.9`**:0 是合法音量,
|
|
1385
|
+
// 但 `||` 把它当假值 ⇒ 滑块拖到 0 会变成 0.9(用户真机实测到的 bug)。
|
|
1386
|
+
// 这里对 0 / NaN / 越界都给出确定行为:0 就是静音,NaN 才回落默认值。
|
|
1387
|
+
function soundVolClamped(fallback) {
|
|
1388
|
+
var v = Number(soundVol)
|
|
1389
|
+
if (!isFinite(v)) v = (typeof fallback === "number") ? fallback : 0.9
|
|
1390
|
+
return Math.max(0, Math.min(1, v))
|
|
1391
|
+
}
|
|
1392
|
+
function playBindingSound(bind, ev) {
|
|
1393
|
+
try {
|
|
1394
|
+
if (!bind || !bind.on || soundOn === false) return
|
|
1395
|
+
var sel = bind.sel || ''
|
|
1396
|
+
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
|
+
}
|
|
1407
|
+
} catch (err) {}
|
|
1408
|
+
}
|
|
1409
|
+
// v761:等待交互时的**会话名**(wait.json 每秒轮询回填)+ 超长截断(默认 12 字符 + …)
|
|
1410
|
+
var waitSessionName = ''
|
|
1411
|
+
var WAIT_SESSION_MAX = 12
|
|
1412
|
+
// v768:对话名的**统一取值**入口 —— 等待提示的 `{session}` 文案与「对话名」模块共用。
|
|
1413
|
+
// max <= 0 或非数 = 不截断;名字为空时回落「当前对话」。
|
|
1414
|
+
function bubbleSessionLabel(max) {
|
|
1415
|
+
var s = String(waitSessionName || '').trim()
|
|
1416
|
+
if (!s) s = '当前对话'
|
|
1417
|
+
var n = Number(max)
|
|
1418
|
+
if (!isFinite(n) || n <= 0) return s
|
|
1419
|
+
n = Math.max(1, Math.min(120, Math.round(n)))
|
|
1420
|
+
return s.length > n ? (s.slice(0, n) + '...') : s
|
|
1421
|
+
}
|
|
1422
|
+
function soundSessionLabel() { return bubbleSessionLabel(WAIT_SESSION_MAX) }
|
|
1423
|
+
// 对话名模块的显示文本:模块自己的「保留长度」(m.len)优先,缺省 12
|
|
1424
|
+
function bubbleSessionText(m) {
|
|
1425
|
+
m = m || {}
|
|
1426
|
+
return bubbleSessionLabel(m.len === undefined || m.len === null ? WAIT_SESSION_MAX : m.len)
|
|
1427
|
+
}
|
|
1313
1428
|
function playTaskEndSound() {
|
|
1314
1429
|
try {
|
|
1315
1430
|
if (!usageSet || !usageSet.taskEnd || !usageSet.taskEnd.on || soundOn === false) return
|
|
@@ -1328,7 +1443,7 @@ function playTaskEndSound() {
|
|
|
1328
1443
|
if (!url) return
|
|
1329
1444
|
var a = dshwvSound(url)
|
|
1330
1445
|
if (altUrl) a._alt = altUrl
|
|
1331
|
-
try { a.volume =
|
|
1446
|
+
try { a.volume = soundVolClamped(0.9) } catch (err) {}
|
|
1332
1447
|
a.play().catch(function () {})
|
|
1333
1448
|
} catch (err) {}
|
|
1334
1449
|
}
|
|
@@ -1342,7 +1457,7 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1342
1457
|
var pressEmpty = !!(g && g.press === '') // 仅显式留空算静音;缺失字段(null)按旧数据回落预设
|
|
1343
1458
|
var releaseEmpty = !!(g && g.release === '')
|
|
1344
1459
|
if (pressEmpty && releaseEmpty) return
|
|
1345
|
-
var vol =
|
|
1460
|
+
var vol = soundVolClamped(0.9)
|
|
1346
1461
|
// v752:任务结束音同样改走片段路由(与本体按压同一套 URL 选择 + 备用路由逻辑)——
|
|
1347
1462
|
// 老路由 /dsh-whale/sound/*.mp3?set=… 在部分环境会被本机那层东西拦成空的 204,
|
|
1348
1463
|
// 不改的话"点按有声、但每轮结束音没声"会变成同一个问题的另一半。
|
|
@@ -1433,34 +1548,40 @@ var row1 = menuRow()
|
|
|
1433
1548
|
row1.appendChild(menuLabel('大小'))
|
|
1434
1549
|
row1.appendChild(scaleInput)
|
|
1435
1550
|
row1.appendChild(scaleNumber)
|
|
1436
|
-
|
|
1437
|
-
row2.appendChild(menuLabel('音效'))
|
|
1438
|
-
row2.appendChild(audioGroupBtn)
|
|
1439
|
-
row2.appendChild(audioImportBtn)
|
|
1551
|
+
// —— 音量滑块与百分比(v761 起搬进「提示与音效设置」面板,主菜单不再占行)——
|
|
1440
1552
|
var volInput = document.createElement('input')
|
|
1441
1553
|
volInput.type = 'range'
|
|
1442
1554
|
volInput.min = '0'
|
|
1443
1555
|
volInput.max = '1'
|
|
1444
1556
|
volInput.step = '0.05'
|
|
1445
1557
|
volInput.className = 'dshwv-range'
|
|
1446
|
-
volInput.value = '
|
|
1558
|
+
volInput.value = '1'
|
|
1447
1559
|
var volPct = document.createElement('span')
|
|
1448
1560
|
volPct.className = 'dshwv-volpct'
|
|
1449
|
-
volPct.textContent = '
|
|
1561
|
+
volPct.textContent = '100%'
|
|
1450
1562
|
volInput.addEventListener('input', function () { setVol(volInput.value) })
|
|
1451
|
-
// v753
|
|
1452
|
-
//
|
|
1563
|
+
// 音效总开关(v753/issue #135 引入;v762 起只由「提示与音效设置」面板 ① 表头的同名开关驱动,
|
|
1564
|
+
// 主菜单「音效与提示」行不再放它 —— 但元素保留,因为 setSoundOn()/配置回填会同步它的 checked)
|
|
1453
1565
|
var soundToggle = document.createElement('input')
|
|
1454
1566
|
soundToggle.type = 'checkbox'
|
|
1455
1567
|
soundToggle.className = 'dshwv-check'
|
|
1456
1568
|
soundToggle.checked = true
|
|
1457
1569
|
soundToggle.title = '音效总开关:关掉后不出声(含音效组试听),并立刻挂起音频上下文、把系统睡眠交还给你'
|
|
1458
1570
|
soundToggle.addEventListener('change', function () { setSoundOn(soundToggle.checked) })
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1571
|
+
// v762:「音效与提示」行 = 标签 + 面板入口(总开关搬进面板 ① 表头,本行不再放开关)。
|
|
1572
|
+
// 原「音效」行里的音效组/导入、原「音量」行的滑块,以及每轮消耗/提问/授权的音效,全部收进面板。
|
|
1573
|
+
// ⚠️ soundToggle 这个元素**仍然创建**(setSoundOn() 与配置回填都会 `soundToggle.checked = ...`),
|
|
1574
|
+
// 只是不再挂到本行上。删掉它的创建会让那两处抛 ReferenceError(本项目栽过整个挂件不出现)。
|
|
1575
|
+
var row2 = menuRow()
|
|
1576
|
+
row2.appendChild(menuLabel('音效与提示'))
|
|
1577
|
+
var soundPanelBtn = document.createElement('button')
|
|
1578
|
+
soundPanelBtn.type = 'button'
|
|
1579
|
+
soundPanelBtn.className = 'dshwv-roleimport'
|
|
1580
|
+
soundPanelBtn.style.flex = '1'
|
|
1581
|
+
soundPanelBtn.textContent = '全局设置'
|
|
1582
|
+
soundPanelBtn.title = '提示与音效设置:按压音量/音效组、每轮消耗提示、提问提示、授权提示'
|
|
1583
|
+
soundPanelBtn.addEventListener('click', function (e) { e.stopPropagation(); openSoundSettingsPanel() })
|
|
1584
|
+
row2.appendChild(soundPanelBtn)
|
|
1464
1585
|
var row6 = menuRow()
|
|
1465
1586
|
row6.appendChild(menuLabel('气泡全局开关'))
|
|
1466
1587
|
row6.appendChild(bubbleToggle)
|
|
@@ -1469,8 +1590,8 @@ var bubbleCustomBtn = document.createElement('button')
|
|
|
1469
1590
|
bubbleCustomBtn.type = 'button'
|
|
1470
1591
|
bubbleCustomBtn.className = 'dshwv-roleimport'
|
|
1471
1592
|
bubbleCustomBtn.style.flex = '1'
|
|
1472
|
-
bubbleCustomBtn.textContent = '
|
|
1473
|
-
bubbleCustomBtn.title = '
|
|
1593
|
+
bubbleCustomBtn.textContent = '按压泡泡设置'
|
|
1594
|
+
bubbleCustomBtn.title = '打开“按压泡泡设置”(按压时的泡泡内容与队列)'
|
|
1474
1595
|
bubbleCustomBtn.addEventListener('click', function (e) { e.stopPropagation(); openBubbleEditor() })
|
|
1475
1596
|
row6.appendChild(bubbleCustomBtn)
|
|
1476
1597
|
var menuSep1 = document.createElement('div')
|
|
@@ -1523,9 +1644,12 @@ roleFileInput.addEventListener('change', function () { onRoleFileChosen(roleFile
|
|
|
1523
1644
|
menuBox.appendChild(rowRole)
|
|
1524
1645
|
menuBox.appendChild(row1)
|
|
1525
1646
|
menuBox.appendChild(row2)
|
|
1526
|
-
menuBox.appendChild(row3)
|
|
1527
1647
|
menuBox.appendChild(row6)
|
|
1528
|
-
|
|
1648
|
+
// v762:「每轮消耗提示」整行**不再挂到主菜单**(菜单里已再无此行,配置入口移进「全局设置」面板)。
|
|
1649
|
+
// ⚠️ row7 及其内部控件(turnCostToggle / turnCostCustomBtn)的创建**必须原样保留**:
|
|
1650
|
+
// turnCostToggle 仍被 OPEN 时的状态同步与 setTurnCostOn() 引用,**取消挂载 ≠ 删除创建**。
|
|
1651
|
+
// (本文件曾在「删了一行、别处还引用 rowN」上踩过:init 抛 TypeError 被外层 try 静默吞掉,
|
|
1652
|
+
// 表现就是挂件整个不出现。)
|
|
1529
1653
|
// 「任务结束音效」不再占主菜单(v720):控件挂在一个不插入文档的宿主上,
|
|
1530
1654
|
// 「自定义提示」窗口打开时再把它搬进窗口。需要宿主是因为 dshwCustSel 初始化要求 select 已有父节点。
|
|
1531
1655
|
var taskEndRowHost = document.createElement('div')
|
|
@@ -1863,19 +1987,711 @@ function buildUsageSubShell() {
|
|
|
1863
1987
|
usagePanel.appendChild(usageMainEl)
|
|
1864
1988
|
}
|
|
1865
1989
|
// —— 预警/预算提醒内容编辑:直接复用 W2 模块编辑组件(可选模块入框/拖入、行内并排、拖动排序、按类型编辑、真实预览) ——
|
|
1990
|
+
// ===== v761(全局音效设置面板):一个窗口里调四个事件 =====
|
|
1991
|
+
// 按压音效 / 每轮消耗提示 / 提问音效 / 授权音效,各自的音效 + 独立音量 + 自动关闭 + 冒泡。
|
|
1992
|
+
// 默认值必须与宿主 whale-balance.mjs 的 soundEventsDefaults() **逐字段一致**
|
|
1993
|
+
// (宿主 = 新用户默认;这里 = 面板的读回兜底与「恢复默认」的目标,改一处必须同步另一处)。
|
|
1994
|
+
function soundEventDefaults() {
|
|
1995
|
+
return {
|
|
1996
|
+
// 按压音效(按压 + 松开):音量与音效组仍在 .dsh-size.json(vol / soundSet),这里只占位
|
|
1997
|
+
press: { vol: 1 },
|
|
1998
|
+
// ⚠️ turnCost **刻意不带** soundOn:它的「这个音效播不播」就是既有的 usageSet.taskEnd.on
|
|
1999
|
+
// (宿主 soundEventsDefaults() 的注释与实现完全一致)。
|
|
2000
|
+
// 若给它加 soundOn: true 再同步写 taskEnd.on,会把老用户的任务结束音从「默认关」变成
|
|
2001
|
+
// 「默认开」—— 这是不能悄悄发生的默认行为变更。
|
|
2002
|
+
// ⚠️ v764:turnCost **也不再带** autoClose / ttlSec —— 它们从来没有消费方,而面板 ② 区
|
|
2003
|
+
// 「自动关闭」的真实落点是 .dshw-size.json 的 turnCostCloseMs(见 openSoundSettingsPanel)。
|
|
2004
|
+
// 给 turnCost 重新长出这两个键,会让同一个设置又变回两个来源(其中一个还是假的)。
|
|
2005
|
+
turnCost: { vol: 1, bubbleOn: true },
|
|
2006
|
+
// v774:出厂默认 = 作者当前用法:提问/授权**开着**(要冒泡),但音效**不响**(soundOn: false)。
|
|
2007
|
+
// 要响就把音效行的 [✓] 勾上;不想冒泡就取消入口行的 [✓]。
|
|
2008
|
+
question: { on: true, soundOn: false, sel: 'frag:exp_orb', vol: 1, autoClose: true, ttlSec: 180, bubbleOn: true },
|
|
2009
|
+
approval: { on: true, soundOn: false, sel: 'frag:exp_orb', vol: 1, autoClose: true, ttlSec: 180, bubbleOn: true },
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
// 读某个事件的设置:缺字段/类型不对一律回落默认(只读 usageSet,不写)
|
|
2013
|
+
function soundEventCfg(kind) {
|
|
2014
|
+
var def = soundEventDefaults()[kind] || {}
|
|
2015
|
+
var src = (usageSet && usageSet.events && usageSet.events[kind]) || {}
|
|
2016
|
+
var out = {}
|
|
2017
|
+
for (var k in def) {
|
|
2018
|
+
var v = src[k]
|
|
2019
|
+
if (k === 'vol') { var n = Number(v); out.vol = (isFinite(n) && n >= 0 && n <= 1) ? n : def.vol }
|
|
2020
|
+
else if (k === 'ttlSec') { var s = Number(v); out.ttlSec = (isFinite(s) && s >= 0) ? Math.round(s) : def.ttlSec }
|
|
2021
|
+
else if (k === 'sel') out.sel = (typeof v === 'string') ? v : def.sel
|
|
2022
|
+
else out[k] = (typeof v === 'boolean') ? v : def[k]
|
|
2023
|
+
}
|
|
2024
|
+
return out
|
|
2025
|
+
}
|
|
2026
|
+
// 事件的保存载荷(只带 schema 里的键:turnCost 没有 on/sel/autoClose/ttlSec,宿主那侧的 lines 等内容一律不动)
|
|
2027
|
+
function soundEventOut(cfg, withBind) {
|
|
2028
|
+
var o = {
|
|
2029
|
+
vol: Number(cfg.vol) || 0,
|
|
2030
|
+
bubbleOn: cfg.bubbleOn !== false,
|
|
2031
|
+
}
|
|
2032
|
+
if (withBind) {
|
|
2033
|
+
// v764:autoClose / ttlSec 跟着 on/sel/soundOn 一起,只属于提问/授权。turnCost 的「自动关闭」
|
|
2034
|
+
// 真实落点是 .dsh-size.json 的 turnCostCloseMs、**不走 events 载荷** —— 别再无条件塞进去,
|
|
2035
|
+
// 否则每次保存都会给 turnCost 重新写回这两个死键。
|
|
2036
|
+
o.autoClose = cfg.autoClose !== false
|
|
2037
|
+
o.ttlSec = Math.max(0, Math.round(Number(cfg.ttlSec) || 0))
|
|
2038
|
+
o.on = cfg.on !== false
|
|
2039
|
+
o.sel = (typeof cfg.sel === 'string') ? cfg.sel : ''
|
|
2040
|
+
// v762:音效行的「是否播这个音效」开关。**只有提问/授权**带这个键;
|
|
2041
|
+
// turnCost 的同类开关走既有的 taskEnd.on,不进 events.turnCost(与宿主 schema 一致)。
|
|
2042
|
+
o.soundOn = cfg.soundOn !== false
|
|
2043
|
+
}
|
|
2044
|
+
return o
|
|
2045
|
+
}
|
|
2046
|
+
// 提示与音效设置面板(主菜单「音效与提示」行 →「全局设置」按钮;v761 引入时叫「全局音效设置」)。
|
|
2047
|
+
// 保存语义与既有窗口一致:窗口期间的改动**只落内存缓冲**(sndBuf),点「保存」才一次 PUT
|
|
2048
|
+
// (events + taskEnd);「取消」或点遮罩关闭 = 丢弃缓冲,并把**复用自主菜单的控件**
|
|
2049
|
+
// (音量滑块 volInput、按压总开关、每轮消耗开关 —— 它们的监听本来就是即时提交的)还原回打开时的快照。
|
|
2050
|
+
// v763(真机反馈修):**「恢复默认」也必须守这条语义** —— 它只改内存缓冲与控件显示,一个字节都不落盘
|
|
2051
|
+
// (既不调 setVol/setSoundOn/setTurnCostOn,也不 saveUsageSettings);那几项(按压音量/音效总开关/
|
|
2052
|
+
// 每轮消耗开关/自动关闭秒数,存在 .dsh-size.json)的真落盘统一由 saveAll() 负责,点「取消」则由
|
|
2053
|
+
// cleanup(true) 回滚(自动关闭走 setTurnCostClose,收秒、落 .dsh-size.json 的 turnCostCloseMs)。
|
|
2054
|
+
function openSoundSettingsPanel() {
|
|
2055
|
+
try {
|
|
2056
|
+
var DEF = soundEventDefaults()
|
|
2057
|
+
// v764:② 区「自动关闭」的出厂默认(秒)。它**不属于** events schema —— 真实落点是 .dshw-size.json 的
|
|
2058
|
+
// turnCostCloseMs(宿主 readSizeConfig() / 客户端 `var turnCostCloseMs = 5000` 的兜底值是 5 秒,
|
|
2059
|
+
// 但面板按设计稿把「恢复默认」的目标定为 180 秒,与发布说明里「自动关闭默认 180 秒」一致)。
|
|
2060
|
+
// 这个常量放在面板里,不再借(已摘除的)events.turnCost.ttlSec。
|
|
2061
|
+
var TC_CLOSE_DEFAULT_SEC = 180
|
|
2062
|
+
// 打开时的快照(只覆盖复用自菜单、会即时提交的那几个控件)
|
|
2063
|
+
var snapVol = Number(soundVol) || 0
|
|
2064
|
+
var snapSoundOn = soundOn !== false
|
|
2065
|
+
var snapTurnCostOn = !!turnCostOn
|
|
2066
|
+
// v763:面板 ②区「自动关闭」的真实落点是 .dsh-size.json 的 turnCostCloseMs(= 消耗泡泡的停留时长,
|
|
2067
|
+
// 唯一消费点 sceneOpen('cost', …, turnCostCloseMs > 0 ? turnCostCloseMs : 0)),所以它也要进快照、
|
|
2068
|
+
// 也要能随「取消」还原(否则面板那行「自动关闭」就是个死控件)。
|
|
2069
|
+
var snapTurnCostCloseMs = Math.max(0, Math.round(Number(turnCostCloseMs) || 0))
|
|
2070
|
+
// 内存缓冲:窗口期间只改这里
|
|
2071
|
+
var sndBuf = {
|
|
2072
|
+
turnCost: soundEventCfg('turnCost'),
|
|
2073
|
+
question: soundEventCfg('question'),
|
|
2074
|
+
approval: soundEventCfg('approval'),
|
|
2075
|
+
}
|
|
2076
|
+
// v764:② 区「自动关闭」自己的缓冲(不再是 events.turnCost 的一部分)。初值直接取**真实值**
|
|
2077
|
+
// turnCostCloseMs —— 老实现显示的是 events.turnCost.ttlSec 里那个虚构的 180 秒,而消耗泡泡
|
|
2078
|
+
// 实际一直按 .dshw-size.json 的值关闭,那一行等于在骗人。窗口期间只改这个缓冲,
|
|
2079
|
+
// 真落盘仍由 saveAll() → setTurnCostClose() 负责(收「秒」)。
|
|
2080
|
+
var tcCloseBuf = {
|
|
2081
|
+
autoClose: snapTurnCostCloseMs > 0,
|
|
2082
|
+
ttlSec: Math.round(snapTurnCostCloseMs / 1000),
|
|
2083
|
+
}
|
|
2084
|
+
var teOld = (usageSet && usageSet.taskEnd) || {}
|
|
2085
|
+
var teOldSel = (typeof teOld.sel === 'string') ? teOld.sel : ''
|
|
2086
|
+
// 任务结束音:缓冲里存**真实选择**(taskEnd.on 的开关已由本行左侧的 [✓] 单独表达,
|
|
2087
|
+
// 不再像早期那样「on 为假就把下拉显示成静音」)。
|
|
2088
|
+
// v762:「静音」项已从下拉里删除,所以这里直接沿用已保存的 sel;空/失效值由
|
|
2089
|
+
// fillSoundSelect 按任务结束音原有的兜底逻辑挑一个默认音并写回 usageSet。
|
|
2090
|
+
var bufTaskEndSel = teOldSel
|
|
2091
|
+
var bufTaskEndKeep = teOldSel // 兜底:sel 万一被清空时保留原选择
|
|
2092
|
+
// v762:② 音效行的 [✓] = 既有的 usageSet.taskEnd.on(宿主 usageSettingsDefaults() 里
|
|
2093
|
+
// taskEnd 默认 { on: false } ⇒ 这里默认不勾)。**不引入 turnCost.soundOn**。
|
|
2094
|
+
var bufTaskEndOn = (teOld.on === true)
|
|
2095
|
+
// —— 窗口壳(与「提醒内容编辑器」同款卡片)——
|
|
2096
|
+
var mask = document.createElement('div')
|
|
2097
|
+
mask.className = 'dshwv-bubmask'
|
|
2098
|
+
// 必须高于「模型子菜单/模型设置」(29000) 才能正常操作
|
|
2099
|
+
mask.style.zIndex = '30000'
|
|
2100
|
+
var card = document.createElement('div')
|
|
2101
|
+
card.className = 'dshwv-bubcard'
|
|
2102
|
+
card.style.width = 'min(440px,calc(100vw - 16px))'
|
|
2103
|
+
card.style.maxHeight = '88vh'
|
|
2104
|
+
card.style.overflow = 'hidden auto'
|
|
2105
|
+
var title = document.createElement('div')
|
|
2106
|
+
title.className = 'dshwv-bubtitle'
|
|
2107
|
+
title.textContent = '提示与音效设置'
|
|
2108
|
+
card.appendChild(title)
|
|
2109
|
+
// 登记到运行时浮层表(issue #142 / CI 的 z 层审计):面板内的自绘下拉靠 visibleTopZ()
|
|
2110
|
+
// 抬层,不登记的话它们会算在遮罩下面(点开却看不见)
|
|
2111
|
+
var unregMask = null
|
|
2112
|
+
try { if (typeof window.dshwRegisterMask === 'function') unregMask = window.dshwRegisterMask(mask) } catch (err) {}
|
|
2113
|
+
// —— 面板内的小工具 ——
|
|
2114
|
+
// 分区表头:表头文字前一个开关(该事件总开关)。
|
|
2115
|
+
// v762:右侧那条分隔线(─)**去掉了**,改成上下的留白 —— 用户要求「加一些间隔距离,
|
|
2116
|
+
// 不加分割线了」;四个表头的 [✓] + 标题照旧保留。
|
|
2117
|
+
// ⚠️ .dshwv-bubsec 是多个窗口共用的小节标题类(泡泡编辑器、预览区等),所以这里只用
|
|
2118
|
+
// 行内样式覆盖本面板,绝不去改全局 CSS(否则会连带改掉别的窗口)。
|
|
2119
|
+
// v765:面板改成「四个入口行 + 展开体」的手风琴 —— 入口行常驻(开关 + 区名 + 当前值摘要 + ▸/▾),
|
|
2120
|
+
// 点入口行才展开该区的设置项,**可同时展开多个**;默认全部收起。
|
|
2121
|
+
// v767:入口行/折叠体的实现抽到模块级 `dshwvFoldEntry()`(与资源管理窗口共用),这里只做一层包装。
|
|
2122
|
+
// ⚠️ 四个区的控件**照旧全部创建**,只是藏在折叠体里:applyDefaults() / saveAll() 会逐个回填与读取,
|
|
2123
|
+
// 懒创建会让「恢复默认」静默失效(那些回填都包在 try/catch 里,出错看不出来)。
|
|
2124
|
+
var sndEntries = [] // [{ kind, summary, sel }],供 refreshSummaries() 更新入口行右侧的当前值
|
|
2125
|
+
function sndEntry(labelText, checked, onToggle, first) {
|
|
2126
|
+
return dshwvFoldEntry(card, labelText, {
|
|
2127
|
+
checked: checked,
|
|
2128
|
+
onToggle: onToggle,
|
|
2129
|
+
first: first,
|
|
2130
|
+
onRowClick: function () {
|
|
2131
|
+
// v766:点入口行(展开或收起)时,先把本面板可能开着的浮层收起来。为什么必须**显式**收:
|
|
2132
|
+
// · 「音效组」列表(.dshwv-audiolist)是 body 上的固定层,而全局 onDocPointerDown 见到
|
|
2133
|
+
// 本面板的遮罩(.dshwv-bubmask)就直接 return("面板内部交给面板自己处理")⇒ 不主动收,
|
|
2134
|
+
// 它就会一直飘在收起后的入口行上面(用户实测:收起设置项后已打开的下拉菜单没关)。
|
|
2135
|
+
// · 自绘音效下拉(.dshwv-custmenu)平时会被它自己的 document pointerdown 收掉,一起收更稳。
|
|
2136
|
+
// 与既有约定一致:菜单里点到别处也会 closeRolePanel() + closeAudioGroupPanel()。
|
|
2137
|
+
try { dshwCustSelClose() } catch (err) {}
|
|
2138
|
+
try { closeAudioGroupPanel() } catch (err) {}
|
|
2139
|
+
try { stopPanelPreview() } catch (err) {} // 正在试听的那一声也一起停(v766)
|
|
2140
|
+
},
|
|
2141
|
+
})
|
|
2142
|
+
}
|
|
2143
|
+
// 入口行右侧的「当前值摘要」:不展开也能看出这个事件现在是什么状态
|
|
2144
|
+
function soundNameOf(sel) {
|
|
2145
|
+
try {
|
|
2146
|
+
if (!sel || !sel.options || sel.selectedIndex < 0) return ''
|
|
2147
|
+
var o = sel.options[sel.selectedIndex]
|
|
2148
|
+
return String((o && o.textContent) || '')
|
|
2149
|
+
} catch (err) { return '' }
|
|
2150
|
+
}
|
|
2151
|
+
function sndPct(v) { var n = Number(v); if (!isFinite(n) || n < 0) n = 0; return Math.round(n * 100) + '%' }
|
|
2152
|
+
function refreshSummaries() {
|
|
2153
|
+
try {
|
|
2154
|
+
for (var i = 0; i < sndEntries.length; i++) {
|
|
2155
|
+
var e = sndEntries[i]
|
|
2156
|
+
var txt = ''
|
|
2157
|
+
if (e.kind === 'press') {
|
|
2158
|
+
if (soundOn === false) txt = '已关闭'
|
|
2159
|
+
else {
|
|
2160
|
+
var g = ''
|
|
2161
|
+
try { g = String(audioGroupBtnLabel.textContent || '') } catch (err) {}
|
|
2162
|
+
txt = (g || '默认音效组') + ' · ' + sndPct(soundVol)
|
|
2163
|
+
}
|
|
2164
|
+
} else if (e.kind === 'turnCost') {
|
|
2165
|
+
if (!turnCostOn) txt = '已关闭'
|
|
2166
|
+
else {
|
|
2167
|
+
var sec = Math.max(0, Math.round(Number(tcCloseBuf.ttlSec) || 0))
|
|
2168
|
+
var pre = (tcCloseBuf.autoClose !== false && sec > 0) ? ('自动关 ' + sec + 's') : '不自动关'
|
|
2169
|
+
// v768:静音时**不再显示已选的音效名**,直接告诉用户去哪儿改(用户要求的确切文案)
|
|
2170
|
+
txt = pre + ' · ' + (bufTaskEndOn ? (soundNameOf(e.sel) || '—') : '当前为静音,在下拉设置中修改')
|
|
2171
|
+
if (bufTaskEndOn && sndPct(sndBuf.turnCost.vol) !== '100%') txt += ' · ' + sndPct(sndBuf.turnCost.vol)
|
|
2172
|
+
}
|
|
2173
|
+
} else {
|
|
2174
|
+
var cfg = sndBuf[e.kind] || {}
|
|
2175
|
+
if (cfg.on === false) txt = '已关闭'
|
|
2176
|
+
else if (cfg.soundOn === false) txt = '当前为静音,在下拉设置中修改' // v768:静音时不显示已选音效
|
|
2177
|
+
else {
|
|
2178
|
+
txt = soundNameOf(e.sel) || '—'
|
|
2179
|
+
if (sndPct(cfg.vol) !== '100%') txt += ' · ' + sndPct(cfg.vol)
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
try { if (e.summary) e.summary.textContent = txt } catch (err) {}
|
|
2183
|
+
}
|
|
2184
|
+
// v773:摘要与"置灰状态"永远一起刷(所有既有刷新点都会走到这里:开面板 / 卡内 input·change /
|
|
2185
|
+
// 点文档任意处 / 「恢复默认」末尾)—— 所以开关一动,相关设置立刻跟着灰/亮,不需要另挂监听。
|
|
2186
|
+
applySndDisabled()
|
|
2187
|
+
} catch (err) {}
|
|
2188
|
+
}
|
|
2189
|
+
// v773:**音效没开 ⇒ 该区"音效相关设置"置灰且不可编辑**(用户要求)。
|
|
2190
|
+
// · 只动**设置类**控件:音效下拉(自绘那颗按钮 + 原生 select)与音量滑块,以及它们那一行的标签;
|
|
2191
|
+
// · **开关自己**(入口行 [✓] / 音效行 [✓])永远保持可点 —— 它是"开回来"的唯一入口,
|
|
2192
|
+
// 置灰它就等于把用户锁死在外面;
|
|
2193
|
+
// · **试听 ▶ 也保持可用** —— 它正好用来"先听一下再决定开不开"(不是设置,是预览);
|
|
2194
|
+
// · 与声音无关的行(冒泡提示 / 编辑提示内容 / 自动关闭)不动:它们归"事件总开关"管,各按各的。
|
|
2195
|
+
// 判定条件由各区给(读的都是**实时**状态:模块级 soundOn / turnCostOn、缓冲里的 bufTaskEndOn / cfg.soundOn)。
|
|
2196
|
+
var sndDisableGroups = []
|
|
2197
|
+
function sndReg(off, items) { sndDisableGroups.push({ off: off, items: items || [] }) }
|
|
2198
|
+
function sndDim(el, off) {
|
|
2199
|
+
try { if (el && el.style) el.style.opacity = off ? '.4' : '' } catch (err) {}
|
|
2200
|
+
}
|
|
2201
|
+
function applySndDisabled() {
|
|
2202
|
+
for (var i = 0; i < sndDisableGroups.length; i++) {
|
|
2203
|
+
var g = sndDisableGroups[i]
|
|
2204
|
+
var off = false
|
|
2205
|
+
try { off = !!g.off() } catch (err) { off = false }
|
|
2206
|
+
for (var j = 0; j < g.items.length; j++) {
|
|
2207
|
+
var it = g.items[j] || {}
|
|
2208
|
+
try { if (it.el) it.el.disabled = off } catch (err) {}
|
|
2209
|
+
// 自绘下拉:原生 select 是隐藏的,看得见的是它自己那颗按钮 ⇒ 两者都要处理
|
|
2210
|
+
try { if (it.drop && typeof it.drop.sync === 'function') it.drop.sync() } catch (err) {}
|
|
2211
|
+
sndDim(it.drop && it.drop.btn ? it.drop.btn : it.el, off)
|
|
2212
|
+
sndDim(it.label, off)
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
// 一行:标签 + 控件(控件由调用方 append 进来,沿用主菜单的行样式)
|
|
2217
|
+
// v765:多一个 host 参数 —— 行挂到哪个容器(默认 card;手风琴里传该区的折叠体)
|
|
2218
|
+
function row(labelText, host) {
|
|
2219
|
+
var r = menuRow()
|
|
2220
|
+
if (labelText) r.appendChild(menuLabel(labelText))
|
|
2221
|
+
;(host || card).appendChild(r)
|
|
2222
|
+
return r
|
|
2223
|
+
}
|
|
2224
|
+
// 试听按钮(沿用各窗口里的行内小图标按钮 .dshwv-bubmini)
|
|
2225
|
+
function playBtn(titleText, fn) {
|
|
2226
|
+
var b = document.createElement('button')
|
|
2227
|
+
b.type = 'button'
|
|
2228
|
+
b.className = 'dshwv-bubmini'
|
|
2229
|
+
b.textContent = '▶'
|
|
2230
|
+
b.title = titleText
|
|
2231
|
+
b.addEventListener('click', function (e) { e.stopPropagation(); try { fn() } catch (err) {} })
|
|
2232
|
+
return b
|
|
2233
|
+
}
|
|
2234
|
+
// 独立音量滑块 + 百分比(只改缓冲,点「保存」才落盘)
|
|
2235
|
+
function volSlider(getV, setV) {
|
|
2236
|
+
var inp = document.createElement('input')
|
|
2237
|
+
inp.type = 'range'
|
|
2238
|
+
inp.min = '0'
|
|
2239
|
+
inp.max = '1'
|
|
2240
|
+
inp.step = '0.05'
|
|
2241
|
+
inp.className = 'dshwv-range'
|
|
2242
|
+
inp.value = String(getV())
|
|
2243
|
+
var pct = document.createElement('span')
|
|
2244
|
+
pct.className = 'dshwv-volpct'
|
|
2245
|
+
pct.textContent = Math.round(getV() * 100) + '%'
|
|
2246
|
+
inp.addEventListener('input', function () {
|
|
2247
|
+
var v = Number(inp.value)
|
|
2248
|
+
if (!isFinite(v)) v = 0
|
|
2249
|
+
setV(v)
|
|
2250
|
+
pct.textContent = Math.round(v * 100) + '%'
|
|
2251
|
+
})
|
|
2252
|
+
return { input: inp, pct: pct }
|
|
2253
|
+
}
|
|
2254
|
+
// v766:试听包裹 —— 期间新建的音频元素会进"试听名单"(见 dshwvPreviewTrack),
|
|
2255
|
+
// 收起设置项时由 stopPanelPreview() 一次停掉(片段/预设/音效组三条路都覆盖)。
|
|
2256
|
+
function playPreview(fn) {
|
|
2257
|
+
try { dshwvPreviewOn() } catch (err) {}
|
|
2258
|
+
try { fn() } finally { try { dshwvPreviewOff() } catch (err) {} }
|
|
2259
|
+
}
|
|
2260
|
+
// v766:停掉本面板正在试听的声音。
|
|
2261
|
+
// · ① 的「试听按压音效」走的是**模块级** pressAudio / releaseAudio(复用本体那两个元素)⇒ 直接停;
|
|
2262
|
+
// · ②③④ 走 playBindingSound → playTaskEndSound(内部匿名新建元素)⇒ 靠试听名单停。
|
|
2263
|
+
// ⚠️ 只停试听,**不碰**真实的任务结束音 / 提问授权提示音(那是用户要听的事件音)。
|
|
2264
|
+
function stopPanelPreview() {
|
|
2265
|
+
try { if (pressAudio) dshwvSoundStop(pressAudio) } catch (err) {}
|
|
2266
|
+
try { if (releaseAudio) dshwvSoundStop(releaseAudio) } catch (err) {}
|
|
2267
|
+
try { dshwvStopPreviews() } catch (err) {}
|
|
2268
|
+
}
|
|
2269
|
+
// 自动关闭:开关 + 秒数(0 = 不自动关闭);host = 挂到哪个容器(手风琴的折叠体)
|
|
2270
|
+
function autoCloseRow(cfg, host) {
|
|
2271
|
+
var r = row('自动关闭', host)
|
|
2272
|
+
var ck = document.createElement('input')
|
|
2273
|
+
ck.type = 'checkbox'
|
|
2274
|
+
ck.className = 'dshwv-check'
|
|
2275
|
+
ck.checked = cfg.autoClose !== false
|
|
2276
|
+
ck.title = '开启后泡泡到时间自动关闭'
|
|
2277
|
+
ck.addEventListener('change', function () { cfg.autoClose = !!ck.checked })
|
|
2278
|
+
var num = document.createElement('input')
|
|
2279
|
+
num.type = 'number'
|
|
2280
|
+
num.min = '0'
|
|
2281
|
+
num.step = '1'
|
|
2282
|
+
num.className = 'dshwv-winput'
|
|
2283
|
+
num.value = String(Math.max(0, Math.round(Number(cfg.ttlSec) || 0)))
|
|
2284
|
+
num.title = '自动关闭秒数(0 = 不自动关闭)'
|
|
2285
|
+
num.addEventListener('input', function () { cfg.ttlSec = Math.max(0, Math.round(Number(num.value) || 0)) })
|
|
2286
|
+
num.addEventListener('change', function () {
|
|
2287
|
+
cfg.ttlSec = Math.max(0, Math.round(Number(num.value) || 0))
|
|
2288
|
+
num.value = String(cfg.ttlSec)
|
|
2289
|
+
})
|
|
2290
|
+
r.appendChild(ck)
|
|
2291
|
+
r.appendChild(num)
|
|
2292
|
+
r.appendChild(menuLabel('秒'))
|
|
2293
|
+
return { chk: ck, num: num }
|
|
2294
|
+
}
|
|
2295
|
+
// 冒泡提示:开关 + 编辑提示内容(editFn 为空时才置灰;提问/授权已接入各自的编辑器)
|
|
2296
|
+
function bubbleRow(cfg, editFn, editTip, host) {
|
|
2297
|
+
var r = row('冒泡提示', host)
|
|
2298
|
+
var ck = document.createElement('input')
|
|
2299
|
+
ck.type = 'checkbox'
|
|
2300
|
+
ck.className = 'dshwv-check'
|
|
2301
|
+
ck.checked = cfg.bubbleOn !== false
|
|
2302
|
+
ck.title = '该事件是否用泡泡提示'
|
|
2303
|
+
ck.addEventListener('change', function () { cfg.bubbleOn = !!ck.checked })
|
|
2304
|
+
var btn = document.createElement('button')
|
|
2305
|
+
btn.type = 'button'
|
|
2306
|
+
btn.className = 'dshwv-roleimport'
|
|
2307
|
+
btn.style.flex = '1'
|
|
2308
|
+
btn.textContent = '编辑提示内容'
|
|
2309
|
+
btn.title = editTip
|
|
2310
|
+
if (editFn) {
|
|
2311
|
+
btn.addEventListener('click', function (e) { e.stopPropagation(); try { editFn() } catch (err) {} })
|
|
2312
|
+
} else {
|
|
2313
|
+
btn.disabled = true
|
|
2314
|
+
btn.style.opacity = '.5'
|
|
2315
|
+
btn.style.cursor = 'not-allowed'
|
|
2316
|
+
}
|
|
2317
|
+
r.appendChild(ck)
|
|
2318
|
+
r.appendChild(btn)
|
|
2319
|
+
return ck
|
|
2320
|
+
}
|
|
2321
|
+
// —— ① 按压音效 ——
|
|
2322
|
+
// v762 行序:音效组 → 音量(用户要求把「按压音效组」挪到「按压音量」**之前**)
|
|
2323
|
+
var pressEnt = sndEntry('按压音效', soundOn !== false, function (b) {
|
|
2324
|
+
// 与音效总开关同一个状态(setSoundOn 会同步那只保留但不再挂到菜单行的开关)
|
|
2325
|
+
setSoundOn(b)
|
|
2326
|
+
}, true)
|
|
2327
|
+
var pressOnChk = pressEnt.chk
|
|
2328
|
+
sndEntries.push({ kind: 'press', summary: pressEnt.summary, sel: null })
|
|
2329
|
+
var rPressGrp = row('按压音效组', pressEnt.body)
|
|
2330
|
+
rPressGrp.appendChild(audioGroupBtn) // 复用原「音效组」按钮
|
|
2331
|
+
audioImportBtn.textContent = '新建音效组'
|
|
2332
|
+
audioImportBtn.title = '新建/编辑音效组'
|
|
2333
|
+
rPressGrp.appendChild(audioImportBtn)
|
|
2334
|
+
// 这两个弹层是 body 上的固定层(.dshwv-audiolist / .dshwv-audiomask 都低于 30000),
|
|
2335
|
+
// 从本面板打开时必须临时抬到面板之上,否则会被遮罩整层盖住(点了没反应)
|
|
2336
|
+
try {
|
|
2337
|
+
audioGroupBtn.addEventListener('click', function () { try { audioGroupPanel.style.zIndex = '31000' } catch (err) {} })
|
|
2338
|
+
audioImportBtn.addEventListener('click', function () { try { audioEditMask.style.zIndex = '31000' } catch (err) {} })
|
|
2339
|
+
} catch (err) {}
|
|
2340
|
+
var rPressVol = row('按压音量', pressEnt.body)
|
|
2341
|
+
rPressVol.appendChild(volInput) // 复用主菜单搬出来的滑块(input 事件已接 setVol;取消时按快照还原)
|
|
2342
|
+
rPressVol.appendChild(volPct)
|
|
2343
|
+
// 本区**只保留音量行这一个试听按钮**(音量行自己不加 ▶,避免一个模块两个入口)
|
|
2344
|
+
rPressVol.appendChild(playBtn('试听按压音效', function () {
|
|
2345
|
+
playPreview(function () {
|
|
2346
|
+
playPress()
|
|
2347
|
+
// 音效组把按压槽留空时 playPress() 只置状态不出声 → 补一次松开音,保证试听总能听到该组
|
|
2348
|
+
if (!pressAudio) playRelease()
|
|
2349
|
+
})
|
|
2350
|
+
}))
|
|
2351
|
+
// v773:① 的"音效没开"= 入口行 [✓](soundOn)没勾 ⇒ 音效组 / 按压音量 置灰不可编辑
|
|
2352
|
+
sndReg(function () { return soundOn === false }, [
|
|
2353
|
+
{ el: audioGroupBtn, label: rPressGrp.firstChild },
|
|
2354
|
+
{ el: audioImportBtn, label: null },
|
|
2355
|
+
{ el: volInput, label: rPressVol.firstChild },
|
|
2356
|
+
])
|
|
2357
|
+
// —— ② 每轮消耗提示 ——
|
|
2358
|
+
// v762 行序(与 ③④ 统一):冒泡提示 → 自动关闭 → 音效行 → 提示音量
|
|
2359
|
+
// (DOM 顺序 = 创建顺序,所以这里按目标顺序调用即完成重排)
|
|
2360
|
+
// v763:② 表头的 [✓] 留个句柄,供 applyDefaults() 同步显示(直接改 .checked 不会触发 change ⇒ 不落盘)
|
|
2361
|
+
var tcEnt = sndEntry('每轮消耗提示', !!turnCostOn, function (b) { setTurnCostOn(b) })
|
|
2362
|
+
var tcOnChk = tcEnt.chk
|
|
2363
|
+
var tcBub = bubbleRow(sndBuf.turnCost, function () { usageAlertBudgetEditor('cost', null) }, '编辑每轮消耗提示的泡泡内容(金额用 {cost})', tcEnt.body)
|
|
2364
|
+
var tcAc = autoCloseRow(tcCloseBuf, tcEnt.body)
|
|
2365
|
+
var rTe = row('任务结束音', tcEnt.body)
|
|
2366
|
+
// 音效行的 [✓] = 该事件是否播这个音效。② 直接绑**既有的** usageSet.taskEnd.on
|
|
2367
|
+
//(宿主 usageSettingsDefaults() 里 taskEnd 默认 { on: false } ⇒ 这里默认不勾)。
|
|
2368
|
+
var teSoundChk = document.createElement('input')
|
|
2369
|
+
teSoundChk.type = 'checkbox'
|
|
2370
|
+
teSoundChk.className = 'dshwv-check'
|
|
2371
|
+
teSoundChk.checked = bufTaskEndOn
|
|
2372
|
+
teSoundChk.title = '该事件是否播这个音效(关掉 = 不响;下拉里选的音效仍保留,下次勾上即用)'
|
|
2373
|
+
teSoundChk.addEventListener('change', function () { bufTaskEndOn = !!teSoundChk.checked })
|
|
2374
|
+
rTe.appendChild(teSoundChk)
|
|
2375
|
+
var teSel = document.createElement('select')
|
|
2376
|
+
teSel.className = 'dshwv-sound'
|
|
2377
|
+
teSel.title = '任务结束音:音效组(点按整组)/ 单个音频;播不播由左边的 [✓] 决定'
|
|
2378
|
+
teSel.addEventListener('change', function () { bufTaskEndSel = teSel.value })
|
|
2379
|
+
rTe.appendChild(teSel)
|
|
2380
|
+
rTe.appendChild(playBtn('试听任务结束音', function () {
|
|
2381
|
+
playPreview(function () { playBindingSound({ on: true, sel: bufTaskEndSel }, { vol: sndBuf.turnCost.vol }) })
|
|
2382
|
+
}))
|
|
2383
|
+
var teDrop = dshwCustSel(teSel, { scrollNames: true })
|
|
2384
|
+
try { teSel.__dshwDrop = teDrop } catch (err) {}
|
|
2385
|
+
fillSoundSelect(teSel, bufTaskEndSel)
|
|
2386
|
+
// 「静音」项删除后下拉不该留空:把兜底后的真实值回写缓冲(旧数据 sel 为空串时兜底成列表里的默认音)
|
|
2387
|
+
if (teSel.value) bufTaskEndSel = teSel.value
|
|
2388
|
+
var tcVol = volSlider(function () { return sndBuf.turnCost.vol }, function (v) { sndBuf.turnCost.vol = v })
|
|
2389
|
+
var rTcVol = row('提示音量', tcEnt.body)
|
|
2390
|
+
rTcVol.appendChild(tcVol.input)
|
|
2391
|
+
rTcVol.appendChild(tcVol.pct)
|
|
2392
|
+
// v773:② 的"音效没开"= 音效行 [✓] 没勾(bufTaskEndOn)**或**整个事件关着(turnCostOn)⇒
|
|
2393
|
+
// 音效下拉 + 提示音量置灰;只有"下拉 + 音量"灰,[✓] 与试听 ▶ 仍可用。
|
|
2394
|
+
// v773b:**「任务结束音」这行字不置灰**(用户要求"这三行字不用变灰")—— 行里还有可用的
|
|
2395
|
+
// [✓] 与 ▶,标签一起灰会让人以为整行都废了。label 传 null = 只灰控件、不动标签。
|
|
2396
|
+
sndReg(function () { return !turnCostOn || !bufTaskEndOn }, [
|
|
2397
|
+
{ el: teSel, drop: teDrop, label: null },
|
|
2398
|
+
{ el: tcVol.input, label: rTcVol.firstChild },
|
|
2399
|
+
])
|
|
2400
|
+
sndEntries.push({ kind: 'turnCost', summary: tcEnt.summary, sel: teSel })
|
|
2401
|
+
// —— ③④ 提问提示 / 授权提示(结构完全相同)——
|
|
2402
|
+
var waitUI = {}
|
|
2403
|
+
function buildWait(kind, labelText, selLabel) {
|
|
2404
|
+
var cfg = sndBuf[kind]
|
|
2405
|
+
var wEnt = sndEntry(labelText, cfg.on !== false, function (b) { cfg.on = !!b })
|
|
2406
|
+
var onChk = wEnt.chk
|
|
2407
|
+
// v762 行序(与 ② 统一):冒泡提示 → 自动关闭 → 音效行 → 提示音量
|
|
2408
|
+
// v761(#161 C5):提问/授权的内容编辑器已接入 —— usageAlertBudgetEditor 新增了
|
|
2409
|
+
// 'question' / 'approval' 两个模式,内容落在 events.<kind>.lines。**不要**拿 'question'
|
|
2410
|
+
// 去打开预警/预算编辑器(那会改错内容,正是上一轮把它置灰的原因)。
|
|
2411
|
+
var bub = bubbleRow(cfg, function () { usageAlertBudgetEditor(kind, null) },
|
|
2412
|
+
'编辑' + (kind === 'approval' ? '授权' : '提问') + '提示的泡泡内容({session} = 当前对话名,超长自动截断)', wEnt.body)
|
|
2413
|
+
// v764:**不再挂「自动关闭」那一行** —— 提问/授权的提示气泡是**常驻**的(宿主给的是挂起状态,
|
|
2414
|
+
// 前端 whaleSysPush({kind:'wait', ttlMs: 0}) 不布自动关闭计时器),所以那行改了也没有任何消费方,
|
|
2415
|
+
// 属于「看起来能设、其实什么都不做」的死控件。行序因此是:冒泡提示 → 音效行 → 提示音量。
|
|
2416
|
+
var rSel = row(selLabel, wEnt.body)
|
|
2417
|
+
// 音效行的 [✓] = 是否播这个音效,键 = events.<kind>.soundOn(v762 新增,默认 true)。
|
|
2418
|
+
// 提问/授权本来就被表头总开关默认关着(on: false),所以默认 true 不带来任何行为变化。
|
|
2419
|
+
// 位置:标签之后、下拉之前。
|
|
2420
|
+
var soundChk = document.createElement('input')
|
|
2421
|
+
soundChk.type = 'checkbox'
|
|
2422
|
+
soundChk.className = 'dshwv-check'
|
|
2423
|
+
soundChk.checked = cfg.soundOn !== false
|
|
2424
|
+
soundChk.title = '该事件是否播这个音效(关掉 = 不响;下拉里选的音效仍保留,下次勾上即用)'
|
|
2425
|
+
soundChk.addEventListener('change', function () { cfg.soundOn = !!soundChk.checked })
|
|
2426
|
+
rSel.appendChild(soundChk)
|
|
2427
|
+
var sel = document.createElement('select')
|
|
2428
|
+
sel.className = 'dshwv-sound'
|
|
2429
|
+
sel.title = selLabel + ':音效组(点按整组)/ 单个音频;播不播由左边的 [✓] 决定'
|
|
2430
|
+
sel.addEventListener('change', function () { cfg.sel = sel.value })
|
|
2431
|
+
rSel.appendChild(sel)
|
|
2432
|
+
rSel.appendChild(playBtn('试听' + selLabel, function () {
|
|
2433
|
+
playPreview(function () { playBindingSound({ on: true, sel: cfg.sel }, { vol: cfg.vol }) })
|
|
2434
|
+
}))
|
|
2435
|
+
var drop = dshwCustSel(sel, { scrollNames: true })
|
|
2436
|
+
try { sel.__dshwDrop = drop } catch (err) {}
|
|
2437
|
+
fillSoundSelect(sel, cfg.sel)
|
|
2438
|
+
// 同 ②:「静音」项删除后下拉不该留空,把兜底后的真实值写回缓冲
|
|
2439
|
+
if (sel.value) cfg.sel = sel.value
|
|
2440
|
+
var vs = volSlider(function () { return cfg.vol }, function (v) { cfg.vol = v })
|
|
2441
|
+
var rVol = row('提示音量', wEnt.body)
|
|
2442
|
+
rVol.appendChild(vs.input)
|
|
2443
|
+
rVol.appendChild(vs.pct)
|
|
2444
|
+
// v773:③④ 的"音效没开"= 音效行 [✓](cfg.soundOn)没勾**或**该事件总开关(cfg.on)关着
|
|
2445
|
+
// v773b:**「提问提示音效」/「授权提示音」这两行字不置灰**(用户要求),只灰下拉与音量
|
|
2446
|
+
sndReg(function () { return cfg.on === false || cfg.soundOn === false }, [
|
|
2447
|
+
{ el: sel, drop: drop, label: null },
|
|
2448
|
+
{ el: vs.input, label: rVol.firstChild },
|
|
2449
|
+
])
|
|
2450
|
+
sndEntries.push({ kind: kind, summary: wEnt.summary, sel: sel })
|
|
2451
|
+
waitUI[kind] = { on: onChk, sel: sel, drop: drop, vol: vs, bub: bub, sound: soundChk }
|
|
2452
|
+
}
|
|
2453
|
+
buildWait('question', '提问提示', '提问提示音效')
|
|
2454
|
+
buildWait('approval', '授权提示', '授权提示音')
|
|
2455
|
+
// v765:入口行右侧摘要的刷新 —— ① 打开面板时一次;② 卡片里任何 input/change 都刷新(控件都在卡内);
|
|
2456
|
+
// ③ 点文档任意处也刷新一次(覆盖「音效组」这种从 body 级子面板里改、不发卡内事件的路径),
|
|
2457
|
+
// 该监听在 cleanup 里摘掉。摘要只写 textContent,成本很低。
|
|
2458
|
+
function sndDocRefresh() { refreshSummaries() }
|
|
2459
|
+
try { document.addEventListener('click', sndDocRefresh, true) } catch (err) {}
|
|
2460
|
+
try { card.addEventListener('input', refreshSummaries) } catch (err) {}
|
|
2461
|
+
try { card.addEventListener('change', refreshSummaries) } catch (err) {}
|
|
2462
|
+
refreshSummaries()
|
|
2463
|
+
// —— 底部按钮 ——
|
|
2464
|
+
var closed = false
|
|
2465
|
+
function cleanup(restore) {
|
|
2466
|
+
if (closed) return
|
|
2467
|
+
closed = true
|
|
2468
|
+
try { dshwCustSelClose() } catch (err) {} // 收回可能还开着的自绘下拉(它是 body 上的固定层)
|
|
2469
|
+
try { closeAudioGroupPanel() } catch (err) {}
|
|
2470
|
+
try { stopPanelPreview() } catch (err) {} // v766:关窗时也停掉试听(别让声音跟着窗口一起"留在后台")
|
|
2471
|
+
if (restore) {
|
|
2472
|
+
// 复用自菜单的控件在输入时就已提交 → 取消时按快照回滚
|
|
2473
|
+
try { if (Math.abs(Number(soundVol) - snapVol) > 1e-9) setVol(snapVol) } catch (err) {}
|
|
2474
|
+
try { if ((soundOn !== false) !== snapSoundOn) setSoundOn(snapSoundOn) } catch (err) {}
|
|
2475
|
+
try { if (!!turnCostOn !== snapTurnCostOn) setTurnCostOn(snapTurnCostOn) } catch (err) {}
|
|
2476
|
+
// v763:④「自动关闭」(turnCostCloseMs,存在 .dsh-size.json)同款处理 —— 回滚内存并写回磁盘。
|
|
2477
|
+
// setTurnCostClose 收秒,且没有「值未变就跳过」的短路,所以这里自己按快照比较,没变就一个字节都不写。
|
|
2478
|
+
try {
|
|
2479
|
+
if (Math.abs(Number(turnCostCloseMs) - snapTurnCostCloseMs) > 1e-9) {
|
|
2480
|
+
setTurnCostClose(Math.round(snapTurnCostCloseMs / 1000))
|
|
2481
|
+
}
|
|
2482
|
+
} catch (err) {}
|
|
2483
|
+
}
|
|
2484
|
+
try { audioGroupPanel.style.zIndex = '' } catch (err) {}
|
|
2485
|
+
try { audioEditMask.style.zIndex = '' } catch (err) {}
|
|
2486
|
+
try { document.removeEventListener('click', sndDocRefresh, true) } catch (err) {}
|
|
2487
|
+
try { if (unregMask) unregMask() } catch (err) {}
|
|
2488
|
+
dshwBodyDetach(mask) // 走 detach 注销登记,否则 DOM 守护会把这个刚关掉的面板补挂回来
|
|
2489
|
+
}
|
|
2490
|
+
// 本面板会写的那四个事件的载荷(只带 schema 里的键)
|
|
2491
|
+
function eventsPayload() {
|
|
2492
|
+
return {
|
|
2493
|
+
// 按压音量仍在 .dsh-size.json(滑块已即时落盘),这里按宿主 schema 再记一份
|
|
2494
|
+
press: { vol: Number(soundVol) || 0 },
|
|
2495
|
+
turnCost: soundEventOut(sndBuf.turnCost, false),
|
|
2496
|
+
question: soundEventOut(sndBuf.question, true),
|
|
2497
|
+
approval: soundEventOut(sndBuf.approval, true),
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
// 本地合并一份(与既有编辑器一致:先乐观写内存,宿主返回后以宿主为准)——
|
|
2501
|
+
// 逐字段合并不整块替换,免得把宿主那侧的 lines 等内容从内存里抹掉。
|
|
2502
|
+
function mergeEventsLocal(ev) {
|
|
2503
|
+
try {
|
|
2504
|
+
usageSet = usageSet || {}
|
|
2505
|
+
usageSet.events = usageSet.events || {}
|
|
2506
|
+
for (var ek in ev) {
|
|
2507
|
+
var m = {}
|
|
2508
|
+
for (var mk in (usageSet.events[ek] || {})) m[mk] = usageSet.events[ek][mk]
|
|
2509
|
+
for (var nk in ev[ek]) m[nk] = ev[ek][nk]
|
|
2510
|
+
usageSet.events[ek] = m
|
|
2511
|
+
}
|
|
2512
|
+
} catch (err) {}
|
|
2513
|
+
}
|
|
2514
|
+
// v763:面板 ②区「自动关闭」→ 消耗泡泡停留时长(毫秒)。为什么要这根线:
|
|
2515
|
+
// · v764:events.turnCost 的 autoClose / ttlSec 已**摘除**(它们从来没有消费方,只有面板自己在
|
|
2516
|
+
// 读写,等于同一件事有两个键);现在 ② 区那行只改面板自己的 tcCloseBuf;
|
|
2517
|
+
// · 真正决定消耗泡泡停多久的是 .dsh-size.json 的 turnCostCloseMs(消费点见上面 sceneOpen);
|
|
2518
|
+
// · 而唯一能改 turnCostCloseMs 的 turnCostCloseInput 已经不再挂到任何窗口(主菜单那行 v762 删了、
|
|
2519
|
+
// 本面板这轮也没搬它)⇒ 不接这根线,面板上那行「自动关闭」就是个死控件。
|
|
2520
|
+
// ⚠️ setTurnCostClose(v) 收的是**秒**(内部 n = round(v); turnCostCloseMs = n * 1000),
|
|
2521
|
+
// 并且**没有**「值未变就跳过」的短路 —— 值没变也会 saveConfig() 写一次 .dsh-size.json(幂等)。
|
|
2522
|
+
function closeMsFromBuf() {
|
|
2523
|
+
// v764:读 ② 区自己的缓冲(tcCloseBuf),不再读 events.turnCost
|
|
2524
|
+
var c = tcCloseBuf || {}
|
|
2525
|
+
var s = Number(c.ttlSec)
|
|
2526
|
+
if (!isFinite(s) || s < 0) s = 0
|
|
2527
|
+
// 不勾「自动关闭」⇒ 0(= 不自动关闭),与既有 turnCostCloseMs > 0 的判定一致
|
|
2528
|
+
return c.autoClose !== false ? Math.round(s) * 1000 : 0
|
|
2529
|
+
}
|
|
2530
|
+
function saveAll() {
|
|
2531
|
+
try {
|
|
2532
|
+
var ev = eventsPayload()
|
|
2533
|
+
// 任务结束音:沿用既有绑定键 {on, sel}。v762 起「播不播」由 ② 音效行的 [✓]
|
|
2534
|
+
// (= usageSet.taskEnd.on)表达,下拉只负责选**哪一个**音效(已无「静音」项)。
|
|
2535
|
+
usageSet = usageSet || {}
|
|
2536
|
+
var te = {}
|
|
2537
|
+
try { for (var k in (usageSet.taskEnd || {})) te[k] = usageSet.taskEnd[k] } catch (err) {}
|
|
2538
|
+
te.on = !!bufTaskEndOn
|
|
2539
|
+
te.sel = bufTaskEndSel || bufTaskEndKeep
|
|
2540
|
+
usageSet.taskEnd = te
|
|
2541
|
+
try { applyTaskEndLocal(te.on, te.sel) } catch (err) {}
|
|
2542
|
+
mergeEventsLocal(ev)
|
|
2543
|
+
saveUsageSettings({ events: ev, taskEnd: te })
|
|
2544
|
+
// v763:按压音量 / 音效总开关 / 每轮消耗提示开关这三项**不在**上面这次 PUT 的覆盖范围里
|
|
2545
|
+
//(它们存在 .dsh-size.json,走 saveConfig())⇒ 保存时必须各自按**当前内存值**提交一次,
|
|
2546
|
+
// 否则「恢复默认」对它们不生效(用户会觉得恢复默认对音量/开关没作用)。
|
|
2547
|
+
// 三个 setter 各自都有「值没变就写一遍同样的值」的幂等行为,无副作用。
|
|
2548
|
+
try { setVol(soundVol) } catch (err) {}
|
|
2549
|
+
try { setSoundOn(soundOn) } catch (err) {}
|
|
2550
|
+
try { setTurnCostOn(turnCostOn) } catch (err) {}
|
|
2551
|
+
// v763:② 的「自动关闭」同样按**当前内存值**提交 —— setTurnCostClose() 收秒、写的是
|
|
2552
|
+
// .dsh-size.json 的 turnCostCloseMs(消耗泡泡停留时长);0 = 不自动关闭。
|
|
2553
|
+
try { setTurnCostClose(Math.round(closeMsFromBuf() / 1000)) } catch (err) {}
|
|
2554
|
+
} catch (err) {}
|
|
2555
|
+
cleanup(false)
|
|
2556
|
+
}
|
|
2557
|
+
function applyDefaults() {
|
|
2558
|
+
try {
|
|
2559
|
+
// v763(真机反馈修):**本函数只改内存,绝不落盘** —— 与窗口里其它改动完全同语义。
|
|
2560
|
+
// 旧实现调了 setVol/setSoundOn/setTurnCostOn(这三者都立即写 .dsh-size.json),又把
|
|
2561
|
+
// 「取消快照」整体换成默认值、最后还 saveUsageSettings({resetEvents:true}) 立刻 PUT
|
|
2562
|
+
// ⇒ 点「恢复默认」再点「取消」时磁盘上早已是默认值,取消等于没撤销(用户实测的 bug)。
|
|
2563
|
+
// 现在:这里只改内存缓冲 + 控件显示(直接改 .value/.checked 不派发 input/change,
|
|
2564
|
+
// 所以碰不到那三个落盘 setter);点「保存」由 saveAll() 提交,「取消」由 cleanup(true)
|
|
2565
|
+
// 按**打开面板时**的快照回滚(因此下面不再重写 snapVol/snapSoundOn/snapTurnCostOn)。
|
|
2566
|
+
// ① 按压音量 + 按压音效总开关(都是内存标量)
|
|
2567
|
+
var dPressVol = Number(DEF.press.vol)
|
|
2568
|
+
dPressVol = isFinite(dPressVol) ? Math.round(Math.min(1, Math.max(0, dPressVol)) * 100) / 100 : 0
|
|
2569
|
+
soundVol = dPressVol // 与 setVol() 同一套规范化(取消时的快照比较是 1e-9,精度必须一致)
|
|
2570
|
+
var sndWasOff = (soundOn === false)
|
|
2571
|
+
soundOn = true
|
|
2572
|
+
try { volInput.value = String(soundVol) } catch (err) {}
|
|
2573
|
+
try { volPct.textContent = Math.round(soundVol * 100) + '%' } catch (err) {}
|
|
2574
|
+
try {
|
|
2575
|
+
if (pressAudio) pressAudio.volume = soundVol
|
|
2576
|
+
if (releaseAudio) releaseAudio.volume = soundVol
|
|
2577
|
+
} catch (err) {}
|
|
2578
|
+
// 由「关」到「开」时补上 setSoundOn(true) 的**非落盘**副作用(复位本轮播放状态 + 重新预热):
|
|
2579
|
+
// 否则音频上下文还停在 suspend,面板里的试听按钮会没声音(取消时 setSoundOn(快照) 会回去)。
|
|
2580
|
+
if (sndWasOff) { try { applySoundSet() } catch (err) {} }
|
|
2581
|
+
// ② 「每轮消耗提示」总开关(同样只改内存)
|
|
2582
|
+
turnCostOn = true
|
|
2583
|
+
try { tcOnChk.checked = true } catch (err) {}
|
|
2584
|
+
try { turnCostToggle.checked = true } catch (err) {}
|
|
2585
|
+
try { turnCostCloseInput.disabled = false } catch (err) {}
|
|
2586
|
+
sndBuf.turnCost = JSON.parse(JSON.stringify(DEF.turnCost))
|
|
2587
|
+
sndBuf.question = JSON.parse(JSON.stringify(DEF.question))
|
|
2588
|
+
sndBuf.approval = JSON.parse(JSON.stringify(DEF.approval))
|
|
2589
|
+
// v764:② 「自动关闭」的默认值 = 出厂值(5 秒 / 开,与 .dshw-size.json 的 turnCostCloseMs 出厂
|
|
2590
|
+
// 默认一致)—— 它现在住在 tcCloseBuf 里,**不再**跟着 DEF.turnCost 走。同样只改内存
|
|
2591
|
+
// (**不调 setTurnCostClose** ⇒ 不落盘),并把(已创建、未挂载的)秒数输入框显示同步好 ——
|
|
2592
|
+
// 否则之后从本面板打开「自定义提示(每轮消耗)」窗口时,那个窗口的「保存」会按陈旧的输入框值
|
|
2593
|
+
// 把 turnCostCloseMs 又写回去。
|
|
2594
|
+
tcCloseBuf.autoClose = true
|
|
2595
|
+
tcCloseBuf.ttlSec = TC_CLOSE_DEFAULT_SEC
|
|
2596
|
+
turnCostCloseMs = closeMsFromBuf()
|
|
2597
|
+
try { turnCostCloseInput.value = String(Math.round(turnCostCloseMs / 1000)) } catch (err) {}
|
|
2598
|
+
// v762:「静音」项已删除 ⇒ 默认仍是「默认已选中内置音」。
|
|
2599
|
+
// v774:「恢复默认」的目标 = 新的出厂默认(宿主 usageSettingsDefaults().taskEnd):
|
|
2600
|
+
// on = **true**、sel = 内置 A(frag:end_a)。
|
|
2601
|
+
bufTaskEndSel = 'frag:end_a' // = 宿主 usageSettingsDefaults().taskEnd.sel
|
|
2602
|
+
bufTaskEndKeep = 'frag:end_a' // = 宿主 usageSettingsDefaults().taskEnd.sel
|
|
2603
|
+
bufTaskEndOn = true // v774:② 音效行的 [✓] 复位(宿主 taskEnd.on 默认 true)
|
|
2604
|
+
// 控件回填
|
|
2605
|
+
try { pressOnChk.checked = true } catch (err) {}
|
|
2606
|
+
try { soundToggle.checked = true } catch (err) {} // v763:与 setSoundOn() 同步的那只开关(只改内存显示)
|
|
2607
|
+
try { teSoundChk.checked = true } catch (err) {} // v774:② 音效行 [✓] 复位(出厂默认已改成"开")
|
|
2608
|
+
try { tcAc.chk.checked = tcCloseBuf.autoClose !== false } catch (err) {}
|
|
2609
|
+
try { tcAc.num.value = String(tcCloseBuf.ttlSec) } catch (err) {}
|
|
2610
|
+
try { tcBub.checked = sndBuf.turnCost.bubbleOn !== false } catch (err) {}
|
|
2611
|
+
try {
|
|
2612
|
+
tcVol.input.value = String(sndBuf.turnCost.vol)
|
|
2613
|
+
tcVol.pct.textContent = Math.round(sndBuf.turnCost.vol * 100) + '%'
|
|
2614
|
+
} catch (err) {}
|
|
2615
|
+
fillSoundSelect(teSel, bufTaskEndSel)
|
|
2616
|
+
;['question', 'approval'].forEach(function (kind) {
|
|
2617
|
+
var u = waitUI[kind]
|
|
2618
|
+
var cfg = sndBuf[kind]
|
|
2619
|
+
if (!u) return
|
|
2620
|
+
try { u.on.checked = cfg.on !== false } catch (err) {}
|
|
2621
|
+
try {
|
|
2622
|
+
u.vol.input.value = String(cfg.vol)
|
|
2623
|
+
u.vol.pct.textContent = Math.round(cfg.vol * 100) + '%'
|
|
2624
|
+
} catch (err) {}
|
|
2625
|
+
try { u.bub.checked = cfg.bubbleOn !== false } catch (err) {}
|
|
2626
|
+
// v762:③④ 音效行的 [✓] 复位(DEF 里 soundOn: true)
|
|
2627
|
+
try { if (u.sound) u.sound.checked = cfg.soundOn !== false } catch (err) {}
|
|
2628
|
+
fillSoundSelect(u.sel, cfg.sel)
|
|
2629
|
+
})
|
|
2630
|
+
// v765:入口行摘要也要跟着「恢复默认」刷新
|
|
2631
|
+
refreshSummaries()
|
|
2632
|
+
// v763:到这里结束 —— **不再写 usageSet、不调 applyTaskEndLocal、也绝不 PUT**。
|
|
2633
|
+
// 旧实现在这里乐观写了内存镜像(events + taskEnd)并立刻发 resetEvents;而「取消」只能
|
|
2634
|
+
// 回滚那三个标量 ⇒ 内存镜像停在默认值、磁盘却是旧值,表现为「取消后重新打开面板看到
|
|
2635
|
+
// 的还是默认值」。现在整套动作都只是缓冲:
|
|
2636
|
+
// · 事件类键 = sndBuf(saveAll 里由 eventsPayload() 取,保存时 mergeEventsLocal + PUT)
|
|
2637
|
+
// · 任务结束音 = bufTaskEndOn / bufTaskEndSel(saveAll 里写回 usageSet.taskEnd + applyTaskEndLocal)
|
|
2638
|
+
// · 按压音量 = soundVol(saveAll 里 setVol() 落 .dsh-size.json,并按宿主 schema 记一份 press.vol)
|
|
2639
|
+
// · usageSet.events 的 lines(提问/授权提示内容)不再被「恢复默认」清空(与按钮 title 一致)
|
|
2640
|
+
} catch (err) {}
|
|
2641
|
+
}
|
|
2642
|
+
var btns = document.createElement('div')
|
|
2643
|
+
btns.className = 'dshwv-bubbtns'
|
|
2644
|
+
var cancelBtn = document.createElement('button')
|
|
2645
|
+
cancelBtn.type = 'button'
|
|
2646
|
+
cancelBtn.className = 'dshwv-bubbtn dshwv-bubbtn-no'
|
|
2647
|
+
cancelBtn.textContent = '取消'
|
|
2648
|
+
cancelBtn.addEventListener('click', function () { cleanup(true) })
|
|
2649
|
+
btns.appendChild(cancelBtn)
|
|
2650
|
+
var resetBtn = document.createElement('button')
|
|
2651
|
+
resetBtn.type = 'button'
|
|
2652
|
+
resetBtn.className = 'dshwv-bubbtn dshwv-bubbtn-no'
|
|
2653
|
+
resetBtn.textContent = '恢复默认'
|
|
2654
|
+
resetBtn.title = '把四个事件的音量/自动关闭/冒泡/音效选择恢复为默认值(不动外观、位置、账本、角色与自定义泡泡)'
|
|
2655
|
+
resetBtn.addEventListener('click', function () { applyDefaults() })
|
|
2656
|
+
btns.appendChild(resetBtn)
|
|
2657
|
+
var okBtn = document.createElement('button')
|
|
2658
|
+
okBtn.type = 'button'
|
|
2659
|
+
okBtn.className = 'dshwv-bubbtn dshwv-bubbtn-ok'
|
|
2660
|
+
okBtn.textContent = '保存'
|
|
2661
|
+
okBtn.addEventListener('click', function () { saveAll() })
|
|
2662
|
+
btns.appendChild(okBtn)
|
|
2663
|
+
card.appendChild(btns)
|
|
2664
|
+
mask.appendChild(card)
|
|
2665
|
+
mask.addEventListener('click', function (e) { if (e.target === mask) cleanup(true) })
|
|
2666
|
+
dshwBodyAppend(mask)
|
|
2667
|
+
} catch (err) {}
|
|
2668
|
+
}
|
|
1866
2669
|
function usageAlertBudgetEditor(key, onSave) {
|
|
1867
2670
|
try {
|
|
1868
2671
|
var isAlert = key === 'alert'
|
|
1869
2672
|
// v720:第三个模式 cost —— 主菜单「每轮消耗提示」后的「自定义提示」窗口。
|
|
1870
2673
|
// 内容与预警/预算共用同一套模块机制;头部不是"触发条件"而是自动关闭秒数 + 任务结束音效(从主菜单搬来)。
|
|
1871
2674
|
var isCost = key === 'cost'
|
|
2675
|
+
// v761(#161 C5):第四/第五个模式 question / approval —— 「全局音效设置」面板里
|
|
2676
|
+
// 「提问音效 / 授权音效」两区的「编辑提示内容」入口。内容落在 usageSet.events.<kind>.lines
|
|
2677
|
+
// (宿主 waitQuestion.content / waitApproval.content 那套键),与 alert/budget 的 lines 完全分开,
|
|
2678
|
+
// 所以**绝不能**落到 isAlert/isBudget 分支去(那会改错内容 —— 上一轮就是因此把它置灰的)。
|
|
2679
|
+
var isWait = (key === 'question' || key === 'approval')
|
|
2680
|
+
var waitKind = isWait ? key : ''
|
|
1872
2681
|
var costCommitted = false // 保存过就不再走"取消还原"
|
|
2682
|
+
var waitCommitted = false // v761:等待提示内容"保存过就不再走取消还原"的旗标
|
|
2683
|
+
var waitSnapLines = null // v761:等待提示内容打开窗口时的快照(取消/点遮罩关闭时还原缓冲)
|
|
1873
2684
|
var cfg = isCost
|
|
1874
2685
|
? (((usageSet || {}).turnCost) || {})
|
|
1875
|
-
: (((usageSet || {})[isAlert ? 'alert' : 'budget']) || {})
|
|
2686
|
+
: (isWait ? ((((usageSet || {}).events || {})[waitKind]) || {}) : (((usageSet || {})[isAlert ? 'alert' : 'budget']) || {}))
|
|
1876
2687
|
var numDef = isAlert ? 50 : 20
|
|
1877
2688
|
var numInit = isAlert ? (cfg.below != null ? cfg.below : numDef) : (cfg.amount != null ? cfg.amount : numDef)
|
|
1878
|
-
|
|
2689
|
+
// 三种自带内容各自取自己的默认模板;等待两模式取宿主 waitDefaultLines() 的逐字段一致版
|
|
2690
|
+
var stepLines0 = isCost ? usageTurnCostLines() : (isWait ? usageWaitLinesOf(cfg, waitKind) : usageRemindLinesOf(cfg, isAlert))
|
|
2691
|
+
if (isWait) {
|
|
2692
|
+
try { waitSnapLines = stepLines0 ? JSON.parse(JSON.stringify(stepLines0)) : [] } catch (err) { waitSnapLines = [] }
|
|
2693
|
+
}
|
|
2694
|
+
var step = { kind: 'custom', modules: JSON.parse(JSON.stringify(stepLines0)) }
|
|
1879
2695
|
// 备份被临时替换的全局状态/元素/函数,关闭后还原
|
|
1880
2696
|
var bkEditItems = bubbleEditItems
|
|
1881
2697
|
var bkEditorSnap = bubbleEditorSnap
|
|
@@ -1905,20 +2721,25 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
1905
2721
|
var title = document.createElement('div')
|
|
1906
2722
|
title.className = 'dshwv-bubtitle'
|
|
1907
2723
|
title.textContent = isCost
|
|
1908
|
-
? '
|
|
1909
|
-
: (
|
|
2724
|
+
? '自定义提示(每轮消耗 · 内容)'
|
|
2725
|
+
: (isWait
|
|
2726
|
+
? ('编辑 ' + (waitKind === 'approval' ? '授权' : '提问') + '提示内容(可拖动下方模块入框)')
|
|
2727
|
+
: ('编辑 ' + (isAlert ? '余额预警' : '今日预算') + '提醒内容(可拖动下方模块入框)'))
|
|
1910
2728
|
card.appendChild(title)
|
|
1911
|
-
// —— 头部:预警/预算=触发条件;cost=自动关闭 +
|
|
2729
|
+
// —— 头部:预警/预算=触发条件;cost=自动关闭 + 任务结束音效;wait=说明 + 占位符提示 ——
|
|
1912
2730
|
var secCond = document.createElement('div')
|
|
1913
2731
|
secCond.className = 'dshwv-bubsec dshwv-bubsec-first'
|
|
1914
2732
|
secCond.textContent = isCost
|
|
1915
2733
|
? '每轮消耗提示(内容里用 {cost} 引用本轮消耗金额)'
|
|
1916
|
-
: (
|
|
2734
|
+
: (isWait
|
|
2735
|
+
? ((waitKind === 'approval' ? '授权' : '提问') + '提示内容({session} = 对话名)')
|
|
2736
|
+
: (isAlert ? '触发条件(余额低于该值时提醒)' : '触发条件(今日已用达到该值时提醒)'))
|
|
1917
2737
|
card.appendChild(secCond)
|
|
1918
2738
|
// 触发条件控件(启用 + 阈值):cost 模式没有触发条件,这两个控件不创建
|
|
2739
|
+
// v761:wait 模式同样没有"触发条件"(挂起音效/开关/自动关闭/冒泡都在「全局音效设置」面板那一区里)
|
|
1919
2740
|
var chk = null
|
|
1920
2741
|
var numInp = null
|
|
1921
|
-
if (!isCost) {
|
|
2742
|
+
if (!isCost && !isWait) {
|
|
1922
2743
|
chk = document.createElement('input')
|
|
1923
2744
|
chk.type = 'checkbox'
|
|
1924
2745
|
chk.className = 'dshwv-check'
|
|
@@ -1949,32 +2770,36 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
1949
2770
|
return s
|
|
1950
2771
|
}
|
|
1951
2772
|
if (isCost) {
|
|
1952
|
-
//
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
//
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2773
|
+
// v763(用户要求):本窗口**不再挂这三行** —— 「自动关闭 [秒数] 秒(0=不自动关闭)」、
|
|
2774
|
+
// 「任务结束音效 [音效名 ▾]」(含左边的 [✓])以及下面的小字提示
|
|
2775
|
+
// 「每轮回复完成时播放;这两项点「保存」后才生效」。它们现在都在「提示与音效设置」面板的
|
|
2776
|
+
// 「每轮消耗提示」区里(那一区才是这两项的正式入口),留在本窗口属于重复。
|
|
2777
|
+
// ⚠️ 这里**只是不 append**,元素的创建与相关缓冲/快照/取消还原一律原样保留:
|
|
2778
|
+
// · turnCostCloseInput:仍创建(见文件上方),仍走 turnCostCloseDefer / -Snap 那套
|
|
2779
|
+
// 「窗口内只改内存 → 点「保存」才 setTurnCostClose 落 .dsh-size.json → 取消按快照还原」;
|
|
2780
|
+
// · taskEndSel:仍创建,并**继续留在他原本的宿主 taskEndRowHost 里**(那个宿主本来就不插入
|
|
2781
|
+
// 文档;dshwCustSel 只要求 select 有父节点,所以自绘下拉的 refresh/fill/sync 照常工作,
|
|
2782
|
+
// 不依赖是否挂进文档);
|
|
2783
|
+
// · taskEndToggle(「每轮回复完成时播放」的 [✓]):仍创建、仍被 applyTaskEndLocal 同步,
|
|
2784
|
+
// 只是本窗口不再展示(面板 ② 区的 teSoundChk 是它的替代入口)。
|
|
2785
|
+
// 保存/取消语义不变:本窗口的「保存」照样提交缓冲里的 taskEnd 与秒数,「取消」照样按快照还原。
|
|
2786
|
+
turnCostCloseInput.disabled = false // 保留原有的「窗口内不受菜单禁用态影响」语义(取消路径也看它)
|
|
2787
|
+
} else if (isWait) {
|
|
2788
|
+
// v761(#161 C5):等待提示没有触发条件。这里只放说明 —— 音效 / 音量 / 自动关闭 / 冒泡开关
|
|
2789
|
+
// 都在「全局音效设置」面板里那一区,本窗口只管内容(改动仍遵循"保存才提交")。
|
|
2790
|
+
var wHint = qLabel(waitKind === 'approval'
|
|
2791
|
+
? '授权挂起时显示这条提示(开关与时长在「授权音效」那一区设置)'
|
|
2792
|
+
: '提问挂起时显示这条提示(开关与时长在「提问音效」那一区设置)')
|
|
2793
|
+
wHint.style.fontSize = '11px'
|
|
2794
|
+
wHint.style.color = '#203170'
|
|
2795
|
+
wHint.style.opacity = '.85'
|
|
2796
|
+
condBox.appendChild(wHint)
|
|
2797
|
+
condBox.appendChild(document.createElement('br'))
|
|
2798
|
+
var wHint2 = qLabel('{session} 会替换为当前对话名,超长自动截断(读不到时显示「当前对话」)')
|
|
2799
|
+
wHint2.style.fontSize = '11px'
|
|
2800
|
+
wHint2.style.color = '#203170'
|
|
2801
|
+
wHint2.style.opacity = '.75'
|
|
2802
|
+
condBox.appendChild(wHint2)
|
|
1978
2803
|
} else {
|
|
1979
2804
|
// 启用提醒: 口 启用提醒 · 阈值: 余额 ≤ [数值] 元时提醒
|
|
1980
2805
|
var gOn = segCond()
|
|
@@ -2018,7 +2843,8 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2018
2843
|
gSec.appendChild(lSec)
|
|
2019
2844
|
condBox.appendChild(gSec)
|
|
2020
2845
|
}
|
|
2021
|
-
card
|
|
2846
|
+
// v761:wait 模式 condBox 里只有说明(上面那一支已直接挂进 card),不重复挂一个空的盒子
|
|
2847
|
+
if (!isWait) card.appendChild(condBox)
|
|
2022
2848
|
// —— 以下完全复用 W2 组件(仅换宿主元素与目标步骤) ——
|
|
2023
2849
|
var secPal = document.createElement('div')
|
|
2024
2850
|
secPal.className = 'dshwv-bubsec dshwv-bubsec-first dshwv-bubsec-withq'
|
|
@@ -2033,7 +2859,9 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2033
2859
|
'<div>· 拖 ⠿ 手柄 = 整行排序</div>' +
|
|
2034
2860
|
(isCost
|
|
2035
2861
|
? '<div style="margin-top:4px">{cost} 在触发时替换为本轮消耗金额(默认内容请保持 {cost})</div>'
|
|
2036
|
-
:
|
|
2862
|
+
: (isWait
|
|
2863
|
+
? '<div style="margin-top:4px">{session} 在挂起时替换为当前对话名(读不到显示「当前对话」,超长按 12 字符截断并追加 ...)</div>'
|
|
2864
|
+
: '<div style="margin-top:4px">{below} / {amount} 在触发时替换为实际数值</div>')) +
|
|
2037
2865
|
'<div style="margin-top:4px;opacity:.75">手机端:长按约 0.4 秒进入拖动</div>'
|
|
2038
2866
|
), secPal.firstChild)
|
|
2039
2867
|
card.appendChild(secPal)
|
|
@@ -2042,7 +2870,9 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2042
2870
|
card.appendChild(bubblePalEl)
|
|
2043
2871
|
var secPv = document.createElement('div')
|
|
2044
2872
|
secPv.className = 'dshwv-bubsec'
|
|
2045
|
-
secPv.textContent = isCost
|
|
2873
|
+
secPv.textContent = isCost
|
|
2874
|
+
? '提示内容(每轮消耗)'
|
|
2875
|
+
: (isWait ? ('提示内容(' + (waitKind === 'approval' ? '授权' : '提问') + ')') : '提醒内容')
|
|
2046
2876
|
card.appendChild(secPv)
|
|
2047
2877
|
bubblePvEl = document.createElement('div')
|
|
2048
2878
|
bubblePvEl.className = 'dshwv-bubpvbox'
|
|
@@ -2083,7 +2913,10 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2083
2913
|
var snap = taskEndDeferSnap || { on: false, sel: '' }
|
|
2084
2914
|
usageSet = usageSet || {}
|
|
2085
2915
|
usageSet.taskEnd = { on: !!snap.on, sel: String(snap.sel || '') }
|
|
2086
|
-
|
|
2916
|
+
// v763:v762 起 taskEndSel 不再挂进任何窗口(它留在不插入文档的宿主 taskEndRowHost 里),
|
|
2917
|
+
// 所以这里对它的同步改成与下一行同款的 try 包裹 —— 万一自绘下拉在未挂载状态下出问题,
|
|
2918
|
+
// 也不能把后面「秒数还原」和 dshwBodyDetach(mask)(关窗)一起带崩(本文件踩过静默 TypeError)。
|
|
2919
|
+
try { applyTaskEndLocal(usageSet.taskEnd.on, usageSet.taskEnd.sel) } catch (err) {}
|
|
2087
2920
|
try { fillTaskEndOptions(usageSet.taskEnd) } catch (err) {}
|
|
2088
2921
|
// 秒数:窗口内没落盘,还原内存与输入框即可
|
|
2089
2922
|
turnCostCloseMs = turnCostCloseDeferSnap
|
|
@@ -2095,6 +2928,14 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2095
2928
|
turnCostCloseDefer = false
|
|
2096
2929
|
turnCostCloseInput.disabled = !turnCostOn // 回到菜单态的禁用逻辑
|
|
2097
2930
|
}
|
|
2931
|
+
// v761(#161 C5):wait 模式收尾 —— 没点「保存」就把缓冲还原(取消语义)。
|
|
2932
|
+
// 窗口期间用户拖入/删除的模块只改 step.modules(缓冲),内存里的 events.<kind>.lines 一直没动,
|
|
2933
|
+
// 所以这里只需把缓冲还原回打开时的快照,再把预览重画一次。
|
|
2934
|
+
if (isWait && !waitCommitted) {
|
|
2935
|
+
var wsnap = Array.isArray(waitSnapLines) ? waitSnapLines : []
|
|
2936
|
+
step.modules = JSON.parse(JSON.stringify(wsnap))
|
|
2937
|
+
try { renderBubblePv() } catch (err) {}
|
|
2938
|
+
}
|
|
2098
2939
|
dshwBodyDetach(mask) // v744:走 detach 注销登记,否则 DOM 守护会把这个刚关掉的编辑器补挂回来
|
|
2099
2940
|
bubbleEditItems = bkEditItems
|
|
2100
2941
|
bubbleEditorSnap = bkEditorSnap
|
|
@@ -2122,9 +2963,16 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2122
2963
|
resBtn.type = 'button'
|
|
2123
2964
|
resBtn.className = 'dshwv-bubbtn dshwv-bubbtn-no'
|
|
2124
2965
|
resBtn.textContent = '恢复默认'
|
|
2125
|
-
resBtn.title = isCost
|
|
2966
|
+
resBtn.title = isCost
|
|
2967
|
+
? '恢复为默认提示内容(自动关闭与任务结束音效保持不变)'
|
|
2968
|
+
: (isWait
|
|
2969
|
+
? ('恢复为默认' + (waitKind === 'approval' ? '授权' : '提问') + '提示内容(音效/音量/自动关闭/冒泡开关保持不变)')
|
|
2970
|
+
: '恢复为默认提醒内容(触发条件保持不变)')
|
|
2126
2971
|
resBtn.addEventListener('click', function () {
|
|
2127
|
-
|
|
2972
|
+
// v761:wait 模式的默认模板 = 宿主 waitDefaultLines() 的逐字段一致版
|
|
2973
|
+
step.modules = JSON.parse(JSON.stringify(isCost
|
|
2974
|
+
? usageTurnCostDefaultLines()
|
|
2975
|
+
: (isWait ? usageWaitDefaultLines(waitKind) : usageRemindDefaultLines(isAlert))))
|
|
2128
2976
|
renderBubblePv()
|
|
2129
2977
|
})
|
|
2130
2978
|
btns.appendChild(resBtn)
|
|
@@ -2153,6 +3001,26 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2153
3001
|
if (onSave) onSave(oCost)
|
|
2154
3002
|
return
|
|
2155
3003
|
}
|
|
3004
|
+
// v761(#161 C5):等待提示内容保存 —— 落在 usageSet.events.<kind>.lines(宿主 waitQuestion.content /
|
|
3005
|
+
// waitApproval.content 的那套键),**只**发 { events: { <kind>: { lines } } },
|
|
3006
|
+
// 不碰 alert/budget/turnCost 的任何键(宿主那侧是逐键合并,其余字段原样保留)。
|
|
3007
|
+
if (isWait) {
|
|
3008
|
+
waitCommitted = true
|
|
3009
|
+
usageSet = usageSet || {}
|
|
3010
|
+
usageSet.events = usageSet.events || {}
|
|
3011
|
+
var prevEv = usageSet.events[waitKind] || {}
|
|
3012
|
+
var evNext = {}
|
|
3013
|
+
try { for (var pk in prevEv) evNext[pk] = prevEv[pk] } catch (err) {}
|
|
3014
|
+
evNext.lines = lines
|
|
3015
|
+
usageSet.events[waitKind] = evNext
|
|
3016
|
+
var patchEv = {}
|
|
3017
|
+
patchEv[waitKind] = { lines: lines }
|
|
3018
|
+
saveUsageSettings({ events: patchEv })
|
|
3019
|
+
var oWait = { lines: lines }
|
|
3020
|
+
cleanup()
|
|
3021
|
+
if (onSave) onSave(oWait)
|
|
3022
|
+
return
|
|
3023
|
+
}
|
|
2156
3024
|
var o = { on: chk.checked, lines: lines, autoClose: acChk.checked, ttlSec: Math.max(0, Number(secInp.value) || 0) }
|
|
2157
3025
|
if (isAlert) o.below = Math.max(0, Number(numInp.value) || 0)
|
|
2158
3026
|
else o.amount = Math.max(0, Number(numInp.value) || 0)
|
|
@@ -2182,17 +3050,20 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2182
3050
|
return el
|
|
2183
3051
|
}
|
|
2184
3052
|
try { if (qeditEl) qeditEl.style.zIndex = '31000' } catch (err) {}
|
|
2185
|
-
// 包装 renderBubblePv
|
|
2186
|
-
// ({below} 余额阈值 / {amount} 预算阈值 / {cost} 本轮消耗金额)
|
|
3053
|
+
// 包装 renderBubblePv:列表/预览后,鲸鱼预览里的占位符用实际值实时替换
|
|
3054
|
+
// ({below} 余额阈值 / {amount} 预算阈值 / {cost} 本轮消耗金额 / {session} 当前对话名)
|
|
2187
3055
|
var renderPvSuper = bkRenderPv
|
|
2188
3056
|
renderBubblePv = function () {
|
|
2189
3057
|
try { renderPvSuper() } catch (err) {}
|
|
2190
3058
|
try {
|
|
2191
3059
|
var it = bubbleEditTarget()
|
|
2192
3060
|
var below = (isAlert && numInp) ? Math.max(0, Number(numInp.value) || 0) : null
|
|
2193
|
-
var amount = (!isAlert && !isCost && numInp) ? Math.max(0, Number(numInp.value) || 0) : null
|
|
3061
|
+
var amount = (!isAlert && !isCost && !isWait && numInp) ? Math.max(0, Number(numInp.value) || 0) : null
|
|
2194
3062
|
var cost = isCost ? '0.00' : null // 预览用示例金额(与 usageCostValue 的格式一致)
|
|
2195
|
-
|
|
3063
|
+
// v761(#161 C5):{session} 要在这里就换成对话名(读不到显示「当前对话」,超长按 12 字符截断),
|
|
3064
|
+
// 这样编辑器预览与真实泡泡看到的完全一致,而不是原样显示 `{session}`。
|
|
3065
|
+
var sess = isWait ? soundSessionLabel() : null
|
|
3066
|
+
if (it && Array.isArray(it.modules) && bubblePvPrevEl) bubblePreviewInto(bubblePvPrevEl, usageAlertModsResolved(it.modules, below, amount, cost, sess))
|
|
2196
3067
|
} catch (err) {}
|
|
2197
3068
|
}
|
|
2198
3069
|
if (numInp) {
|
|
@@ -2205,7 +3076,9 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
2205
3076
|
taskEndDefer = true
|
|
2206
3077
|
var teCur = (usageSet && usageSet.taskEnd) || { on: false, sel: '' }
|
|
2207
3078
|
taskEndDeferSnap = JSON.parse(JSON.stringify({ on: !!teCur.on, sel: String(teCur.sel || '') }))
|
|
2208
|
-
|
|
3079
|
+
// v763:taskEndSel 现在不挂进本窗口了(留在不插入文档的宿主里)⇒ 这两处同步都包 try,
|
|
3080
|
+
// 免得自绘下拉在未挂载状态下万一抛错,把整个窗口的渲染(mask.appendChild 等)一起带崩。
|
|
3081
|
+
try { applyTaskEndLocal(!!teCur.on, String(teCur.sel || '')) } catch (err) {}
|
|
2209
3082
|
try { fillTaskEndOptions(teCur) } catch (err) {}
|
|
2210
3083
|
turnCostCloseDefer = true
|
|
2211
3084
|
turnCostCloseDeferSnap = turnCostCloseMs
|
|
@@ -3763,6 +4636,100 @@ function openUsageRecordsWindow() {
|
|
|
3763
4636
|
.catch(function () { usageMoreCard.innerHTML = '<div style="padding:10px;color:#203170">加载失败</div>' })
|
|
3764
4637
|
}
|
|
3765
4638
|
function closeUsageRecordsWindow() { usageMoreMask.style.display = 'none' }
|
|
4639
|
+
// ===== v767:通用「入口行 + 可折叠体」组件(提示与音效设置面板 / 资源管理窗口共用)=====
|
|
4640
|
+
// 入口行常驻:可选 [✓] 开关 + 区名 + 当前值摘要 + ▸/▾;点整行才展开它下面的折叠体,可同时展开多个。
|
|
4641
|
+
// 折叠用 max-height 过渡(与「自定义提示」窗口里的 advBox 同一套路),展开动画结束后置 'none' —— 之后
|
|
4642
|
+
// 窗口缩放换行也不会被截断;flexShrink:0 是坑 14 的规矩(flex 容器里的折叠框必须给)。
|
|
4643
|
+
// ⚠️ 折叠体里的控件**照旧全部创建**(只是藏起来):懒创建会让「恢复默认」这类逐个回填的逻辑静默失效。
|
|
4644
|
+
// instant = true 时不做过渡、直接到位(资源窗口重渲染时用它保持用户已展开的区,别闪动画)。
|
|
4645
|
+
function collapseSet(body, open, instant) {
|
|
4646
|
+
try {
|
|
4647
|
+
var arrow = body.__dshwArrow
|
|
4648
|
+
if (open) {
|
|
4649
|
+
body.setAttribute('data-open', '1')
|
|
4650
|
+
body.style.maxHeight = instant ? 'none' : (body.scrollHeight + 'px')
|
|
4651
|
+
body.style.opacity = '1'
|
|
4652
|
+
if (arrow) arrow.textContent = '▾'
|
|
4653
|
+
if (!instant) {
|
|
4654
|
+
setTimeout(function () { try { if (body.getAttribute('data-open') === '1') body.style.maxHeight = 'none' } catch (err) {} }, 260)
|
|
4655
|
+
}
|
|
4656
|
+
} else {
|
|
4657
|
+
body.setAttribute('data-open', '0')
|
|
4658
|
+
body.style.maxHeight = body.scrollHeight + 'px' // 先给起始值,才收得回去
|
|
4659
|
+
if (!instant) { try { void body.offsetHeight } catch (err) {} }
|
|
4660
|
+
body.style.maxHeight = '0px'
|
|
4661
|
+
body.style.opacity = '0'
|
|
4662
|
+
if (arrow) arrow.textContent = '▸'
|
|
4663
|
+
}
|
|
4664
|
+
} catch (err) {}
|
|
4665
|
+
}
|
|
4666
|
+
// 建一个入口行 + 它下面的折叠体,都挂进 host;返回 { row, body, summary, arrow, chk }
|
|
4667
|
+
// opts: { checked, onToggle, first, summary, titleTail, onRowClick(willOpen) }
|
|
4668
|
+
function dshwvFoldEntry(host, labelText, opts) {
|
|
4669
|
+
opts = opts || {}
|
|
4670
|
+
var rowEl = document.createElement('div')
|
|
4671
|
+
rowEl.className = 'dshwv-menu-row' // 与主菜单/设置面板入口行同款排版
|
|
4672
|
+
rowEl.style.cursor = 'pointer'
|
|
4673
|
+
rowEl.style.margin = opts.first ? '12px 0 2px' : '10px 0 2px'
|
|
4674
|
+
rowEl.style.padding = '6px 6px'
|
|
4675
|
+
rowEl.style.borderRadius = '6px'
|
|
4676
|
+
rowEl.style.background = 'rgba(32,49,112,.05)' // 常驻淡底:与折叠体里的内容行区分开
|
|
4677
|
+
rowEl.title = '点这一行展开 / 收起「' + labelText + '」' + String(opts.titleTail || '')
|
|
4678
|
+
var ck = null
|
|
4679
|
+
if (typeof opts.onToggle === 'function') {
|
|
4680
|
+
ck = document.createElement('input')
|
|
4681
|
+
ck.type = 'checkbox'
|
|
4682
|
+
ck.className = 'dshwv-check'
|
|
4683
|
+
ck.checked = !!opts.checked
|
|
4684
|
+
ck.title = labelText + '开关(只管开不开;下面的设置项不受影响)'
|
|
4685
|
+
// 点开关**不**展开/收起(否则想关掉该事件时会顺手把区拉开)
|
|
4686
|
+
ck.addEventListener('click', function (e) { try { e.stopPropagation() } catch (err) {} })
|
|
4687
|
+
ck.addEventListener('change', function () { try { opts.onToggle(!!ck.checked) } catch (err) {} })
|
|
4688
|
+
rowEl.appendChild(ck)
|
|
4689
|
+
}
|
|
4690
|
+
var tx = document.createElement('span')
|
|
4691
|
+
tx.textContent = labelText
|
|
4692
|
+
rowEl.appendChild(tx)
|
|
4693
|
+
var sp = document.createElement('span') // 撑开,把摘要与箭头推到右侧
|
|
4694
|
+
sp.style.flex = '1'
|
|
4695
|
+
rowEl.appendChild(sp)
|
|
4696
|
+
var sum = document.createElement('span')
|
|
4697
|
+
sum.style.fontSize = '11px'
|
|
4698
|
+
sum.style.color = '#203170'
|
|
4699
|
+
sum.style.opacity = '.7'
|
|
4700
|
+
sum.style.whiteSpace = 'nowrap'
|
|
4701
|
+
sum.style.overflow = 'hidden'
|
|
4702
|
+
sum.style.textOverflow = 'ellipsis'
|
|
4703
|
+
sum.style.maxWidth = '46%'
|
|
4704
|
+
sum.textContent = String(opts.summary == null ? '' : opts.summary)
|
|
4705
|
+
rowEl.appendChild(sum)
|
|
4706
|
+
var arrow = document.createElement('span')
|
|
4707
|
+
arrow.textContent = '▸'
|
|
4708
|
+
arrow.style.opacity = '.6'
|
|
4709
|
+
arrow.style.marginLeft = '2px'
|
|
4710
|
+
rowEl.appendChild(arrow)
|
|
4711
|
+
host.appendChild(rowEl)
|
|
4712
|
+
var body = document.createElement('div')
|
|
4713
|
+
body.style.overflow = 'hidden'
|
|
4714
|
+
body.style.maxHeight = '0px'
|
|
4715
|
+
body.style.opacity = '0'
|
|
4716
|
+
body.style.transition = 'max-height .24s ease, opacity .18s ease'
|
|
4717
|
+
body.style.flexShrink = '0'
|
|
4718
|
+
body.style.paddingLeft = '10px' // 轻微缩进:看得出下面这些行属于上面那个入口
|
|
4719
|
+
body.setAttribute('data-open', '0')
|
|
4720
|
+
body.__dshwArrow = arrow
|
|
4721
|
+
host.appendChild(body)
|
|
4722
|
+
rowEl.addEventListener('click', function () {
|
|
4723
|
+
var willOpen = body.getAttribute('data-open') !== '1'
|
|
4724
|
+
// onRowClick 先跑:调用方要在"切换"之前收掉自己的浮层 / 停掉试听(见坑 53)
|
|
4725
|
+
try { if (typeof opts.onRowClick === 'function') opts.onRowClick(willOpen) } catch (err) {}
|
|
4726
|
+
collapseSet(body, willOpen)
|
|
4727
|
+
})
|
|
4728
|
+
// 悬停反馈:本项目约定「只用行内样式、不动全局 CSS」,所以用两个监听器代替 :hover
|
|
4729
|
+
rowEl.addEventListener('mouseenter', function () { try { rowEl.style.background = 'rgba(32,49,112,.09)' } catch (err) {} })
|
|
4730
|
+
rowEl.addEventListener('mouseleave', function () { try { rowEl.style.background = 'rgba(32,49,112,.05)' } catch (err) {} })
|
|
4731
|
+
return { row: rowEl, body: body, summary: sum, arrow: arrow, chk: ck }
|
|
4732
|
+
}
|
|
3766
4733
|
// —— 资源管理窗口:集中查看/删除已导入插件的图片与音频 ——
|
|
3767
4734
|
var resMaskEl = null
|
|
3768
4735
|
var resCardEl = null
|
|
@@ -3785,9 +4752,7 @@ function resMaskOpen() {
|
|
|
3785
4752
|
function resManagerClose() {
|
|
3786
4753
|
try { if (resMaskEl) resMaskEl.style.display = 'none' } catch (err) {}
|
|
3787
4754
|
// 窗口消失时结束正在预览的音频,避免后台继续响
|
|
3788
|
-
try {
|
|
3789
|
-
if (resAudEl) { resAudEl.pause(); resAudEl = null }
|
|
3790
|
-
} catch (err) {}
|
|
4755
|
+
try { resStopPreview() } catch (err) {}
|
|
3791
4756
|
}
|
|
3792
4757
|
function openResManager() { resMaskOpen() }
|
|
3793
4758
|
function resMkTag(text, built) {
|
|
@@ -3881,12 +4846,15 @@ function resMkBtn(cls, label, disabled, fn) {
|
|
|
3881
4846
|
return b
|
|
3882
4847
|
}
|
|
3883
4848
|
var resAudEl = null // 资源窗口音频片段试听元素(复用,避免并发)
|
|
4849
|
+
function resStopPreview() {
|
|
4850
|
+
try { if (resAudEl) { resAudEl.pause(); resAudEl = null } } catch (err) {}
|
|
4851
|
+
}
|
|
3884
4852
|
function resPlayFragment(fid) {
|
|
3885
4853
|
try {
|
|
3886
4854
|
if (!fid) return
|
|
3887
4855
|
if (resAudEl) { try { resAudEl.pause() } catch (err) {} resAudEl = null }
|
|
3888
4856
|
var a = dshwvSound('/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(fid))
|
|
3889
|
-
try { a.volume =
|
|
4857
|
+
try { a.volume = soundVolClamped(0.9) } catch (err) {}
|
|
3890
4858
|
a.onended = function () { resAudEl = null }
|
|
3891
4859
|
resAudEl = a
|
|
3892
4860
|
a.play().catch(function () { resAudEl = null })
|
|
@@ -4035,43 +5003,58 @@ function resDelAudioFrag(id) {
|
|
|
4035
5003
|
} catch (err) {}
|
|
4036
5004
|
})
|
|
4037
5005
|
}
|
|
5006
|
+
// v767:资源窗口的两个分区(图片 / 音频)改成与「提示与音效设置」同一套折叠入口(`dshwvFoldEntry`)。
|
|
5007
|
+
// 折叠态**跨重渲染保留** —— 删一张图 / 导一段音频后本窗口会整窗重渲染,不能让用户刚展开的区自己收回去。
|
|
5008
|
+
var resFoldOpen = { img: false, audio: false }
|
|
5009
|
+
function resFoldEntry(wrap, key, labelText, summaryText, first) {
|
|
5010
|
+
var e = dshwvFoldEntry(wrap, labelText, {
|
|
5011
|
+
first: first,
|
|
5012
|
+
summary: summaryText,
|
|
5013
|
+
titleTail: '的资源',
|
|
5014
|
+
onRowClick: function (willOpen) {
|
|
5015
|
+
resFoldOpen[key] = !!willOpen
|
|
5016
|
+
resStopPreview() // 收起/展开时停掉正在试听的那一段(与设置面板同一约定)
|
|
5017
|
+
},
|
|
5018
|
+
})
|
|
5019
|
+
if (resFoldOpen[key]) collapseSet(e.body, true, true) // 直接到位不播动画:重渲染时保持用户已展开的区
|
|
5020
|
+
return e
|
|
5021
|
+
}
|
|
4038
5022
|
function resRenderData(wrap, roles, bubbleImgs, audio) {
|
|
4039
5023
|
try {
|
|
4040
5024
|
wrap.innerHTML = ''
|
|
4041
|
-
|
|
4042
|
-
var
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
wrap.
|
|
5025
|
+
var groups = (audio && Array.isArray(audio.groups)) ? audio.groups : []
|
|
5026
|
+
var fragsAll = (audio && Array.isArray(audio.fragments)) ? audio.fragments : []
|
|
5027
|
+
var frags = fragsAll.filter(function (f) { return f && !f.preset })
|
|
5028
|
+
// —— 图片(入口行 + 折叠体;摘要 = 数量,不展开也知道有多少)——
|
|
5029
|
+
var imgEnt = resFoldEntry(wrap, 'img', '图片', '角色 ' + roles.length + ' · 泡泡图 ' + bubbleImgs.length, true)
|
|
4046
5030
|
var anyImg = false
|
|
4047
5031
|
// 角色图(默认角色只读展示)
|
|
4048
5032
|
roles.forEach(function (r) {
|
|
4049
5033
|
anyImg = true
|
|
4050
5034
|
var isDefault = r.id === 'default'
|
|
4051
5035
|
var tag = resMkTag(isDefault ? '默认角色' : '自定义角色', isDefault)
|
|
4052
|
-
|
|
5036
|
+
imgEnt.body.appendChild(resImgRow(r.url, r.name || r.id, '', [tag, resMkDel('删除', isDefault, function () { resDelRole(r.id) })]))
|
|
4053
5037
|
})
|
|
4054
5038
|
bubbleImgs.forEach(function (im) {
|
|
4055
5039
|
anyImg = true
|
|
4056
5040
|
var tag = resMkTag(im.builtin ? '内置图' : '泡泡图', !!im.builtin)
|
|
4057
|
-
|
|
5041
|
+
imgEnt.body.appendChild(resImgRow('/dsh-whale/bubble-img.png?id=' + encodeURIComponent(im.id), im.name || im.id, '', [tag, resMkDel('删除', !!im.builtin, function () { resDelBubbleImg(im.id) })]))
|
|
4058
5042
|
})
|
|
4059
5043
|
if (!anyImg) {
|
|
4060
5044
|
var empty = document.createElement('div')
|
|
4061
5045
|
empty.className = 'dshwv-resempty'
|
|
4062
5046
|
empty.textContent = '暂无自定义图片(角色/泡泡图)'
|
|
4063
|
-
|
|
5047
|
+
imgEnt.body.appendChild(empty)
|
|
4064
5048
|
}
|
|
4065
|
-
// ——
|
|
4066
|
-
var
|
|
4067
|
-
|
|
4068
|
-
|
|
5049
|
+
// —— 音频(入口行 + 折叠体;「导入片段」放进折叠体第一行,免得入口行太挤)——
|
|
5050
|
+
var auEnt = resFoldEntry(wrap, 'audio', '音频', '音效组 ' + groups.length + ' · 片段 ' + frags.length)
|
|
5051
|
+
var impRow = document.createElement('div')
|
|
5052
|
+
impRow.className = 'dshwv-resrow'
|
|
4069
5053
|
var catAuBtn = resMkBtn('dshwv-resimp', '导入片段', false, resImportAudioFragment)
|
|
4070
5054
|
catAuBtn.title = '导入并裁剪一段音频到片段库(可被音效组引用)'
|
|
4071
|
-
|
|
4072
|
-
|
|
5055
|
+
impRow.appendChild(catAuBtn)
|
|
5056
|
+
auEnt.body.appendChild(impRow)
|
|
4073
5057
|
var anyAu = false
|
|
4074
|
-
var groups = (audio && Array.isArray(audio.groups)) ? audio.groups : []
|
|
4075
5058
|
groups.forEach(function (g) {
|
|
4076
5059
|
anyAu = true
|
|
4077
5060
|
var preset = !!g.preset
|
|
@@ -4079,22 +5062,20 @@ function resRenderData(wrap, roles, bubbleImgs, audio) {
|
|
|
4079
5062
|
var meta = ''
|
|
4080
5063
|
if (!preset && g.press && g.release) meta = '按压:' + g.press + ' 松开:' + g.release
|
|
4081
5064
|
// 音效组不提供播放(按整组点按发声,需与点按动作绑定),只展示/删除
|
|
4082
|
-
|
|
5065
|
+
auEnt.body.appendChild(resIconRow(preset ? '🎧' : '🎵', g.name || g.id, meta, [tag, resMkDel('删除', preset, function () { resDelAudioGroup(g.id) })]))
|
|
4083
5066
|
})
|
|
4084
|
-
var frags = (audio && Array.isArray(audio.fragments)) ? audio.fragments : []
|
|
4085
5067
|
frags.forEach(function (f) {
|
|
4086
|
-
if (f.preset) return
|
|
4087
5068
|
anyAu = true
|
|
4088
5069
|
var tag = resMkTag('音频片段', false)
|
|
4089
5070
|
var play = resMkBtn('dshwv-resplay', '播放', false, function () { resPlayFragment(f.id) })
|
|
4090
5071
|
play.title = '试听该音频片段'
|
|
4091
|
-
|
|
5072
|
+
auEnt.body.appendChild(resIconRow('🎶', f.name || f.id, '', [tag, play, resMkDel('删除', false, function () { resDelAudioFrag(f.id) })]))
|
|
4092
5073
|
})
|
|
4093
5074
|
if (!anyAu) {
|
|
4094
5075
|
var empty2 = document.createElement('div')
|
|
4095
5076
|
empty2.className = 'dshwv-resempty'
|
|
4096
5077
|
empty2.textContent = '暂无自定义音频(片段/音效组)'
|
|
4097
|
-
|
|
5078
|
+
auEnt.body.appendChild(empty2)
|
|
4098
5079
|
}
|
|
4099
5080
|
} catch (err) {}
|
|
4100
5081
|
}
|
|
@@ -4163,12 +5144,32 @@ function usageRemindLinesOf(cfg, isAlert) {
|
|
|
4163
5144
|
if (Array.isArray(cfg.lines) && cfg.lines.length) return cfg.lines
|
|
4164
5145
|
return usageRemindDefaultLines(isAlert)
|
|
4165
5146
|
}
|
|
5147
|
+
// —— 等待交互(提问 / 授权)提示内容(v761 / #161 C5)——
|
|
5148
|
+
// 默认内容必须与宿主 whale-balance.mjs 的 waitDefaultLines(kind) **逐字段一致**
|
|
5149
|
+
// (宿主 = 新用户默认;这里 = 编辑器「恢复默认」的目标,改一处必须同步另一处)。
|
|
5150
|
+
function usageWaitDefaultLines(kind) {
|
|
5151
|
+
// v774:出厂默认内容 = 作者当前实际使用的那一套(对话名模块「[ 对话名 ]」+ 一句提示语,都带跑马灯配色)
|
|
5152
|
+
var isApproval = (kind === 'approval')
|
|
5153
|
+
return [
|
|
5154
|
+
{ type: 'session', size: 10, bold: true, tpl: '[ {session} ]', len: 5, rgb: 'champagne', color: '' },
|
|
5155
|
+
{ type: 'text', text: (isApproval ? '正在等待老大授权' : '正在等待老大回答'), size: 7, bold: true, bgRgb: '', bg: '', rgb: 'indigo', color: '' },
|
|
5156
|
+
]
|
|
5157
|
+
}
|
|
5158
|
+
// 等待提示内容读取:events.<kind>.lines[] 优先;缺失/空 → 默认模板
|
|
5159
|
+
function usageWaitLinesOf(cfg, kind) {
|
|
5160
|
+
cfg = cfg || {}
|
|
5161
|
+
if (Array.isArray(cfg.lines) && cfg.lines.length) return cfg.lines
|
|
5162
|
+
return usageWaitDefaultLines(kind)
|
|
5163
|
+
}
|
|
4166
5164
|
// 占位替换:{below} 余额预警阈值 / {amount} 今日预算阈值 / {cost} 本轮消耗金额
|
|
4167
|
-
function usageFillText(txt, below, amount, cost) {
|
|
5165
|
+
function usageFillText(txt, below, amount, cost, session) {
|
|
4168
5166
|
return String(txt || '')
|
|
4169
5167
|
.replace(/\{below\}/g, below != null ? String(below) : '')
|
|
4170
5168
|
.replace(/\{amount\}/g, amount != null ? String(amount) : '')
|
|
4171
5169
|
.replace(/\{cost\}/g, cost != null ? String(cost) : '')
|
|
5170
|
+
// v761:{session} = 当前对话名(wait.json 轮询回填;读不到显示「当前对话」;超长截断)
|
|
5171
|
+
.replace(/\{session\}/g, session != null ? String(session) : soundSessionLabel())
|
|
5172
|
+
|
|
4172
5173
|
}
|
|
4173
5174
|
function usageLineFontPx(level) {
|
|
4174
5175
|
var n = Math.max(1, Math.min(50, Math.round(Number(level) || 7)))
|
|
@@ -4317,14 +5318,18 @@ function runApiModelAlerts() {
|
|
|
4317
5318
|
} catch (err) {}
|
|
4318
5319
|
}
|
|
4319
5320
|
// 占位替换后的展示模块列表(深拷贝,不改配置)
|
|
4320
|
-
|
|
5321
|
+
// v761(#161 C5):第 5 个参数 session 原样透传给 usageFillText —— 等待交互的提示内容里有
|
|
5322
|
+
// `{session}`,要在这里(渲染/预览时)就换成对话名,否则真实泡泡与编辑器预览都会**原样显示 `{session}`**。
|
|
5323
|
+
// 不传(既有 alert / budget / cost 三处调用)时 usageFillText 会自己回落 soundSessionLabel(),
|
|
5324
|
+
// 因此这三处的行为逐字未变(它们的 lines 里本来就没有 {session})。
|
|
5325
|
+
function usageAlertModsResolved(mods, below, amount, cost, session) {
|
|
4321
5326
|
var out = []
|
|
4322
5327
|
try {
|
|
4323
5328
|
for (var i = 0; i < mods.length; i++) {
|
|
4324
5329
|
var m0 = mods[i] || {}
|
|
4325
5330
|
var cp = JSON.parse(JSON.stringify(m0))
|
|
4326
5331
|
var raw = String(m0.text != null ? m0.text : '')
|
|
4327
|
-
cp.text = raw.length ? usageFillText(raw, below, amount, cost) : raw
|
|
5332
|
+
cp.text = raw.length ? usageFillText(raw, below, amount, cost, session) : raw
|
|
4328
5333
|
out.push(cp)
|
|
4329
5334
|
}
|
|
4330
5335
|
} catch (err) {}
|
|
@@ -4381,6 +5386,29 @@ function usagePopupCard(title, content, below, amount) {
|
|
|
4381
5386
|
}
|
|
4382
5387
|
var USAGE_PALETTE = ['#203170', '#e0433f', '#2fa24c', '#b060c8', '#e89a2e', '#3aa6c8', '#d06a8a', '#7a8b2f', '#6a6ad0', '#c84a8a']
|
|
4383
5388
|
// 统计一组天数里的模型合计(输入 days:[{models:[{model,cost}]}])
|
|
5389
|
+
// v776(issue #163):换/删 API key 后,宿主按密钥指纹分本记账(换 key = 换一本)。
|
|
5390
|
+
// 界面侧给一句说明:否则旧账本的日期看起来像"丢了"(数据其实一直在 .dshw-usage.json 里)。
|
|
5391
|
+
// 宿主在 daySummary() 上补了 bookCount / historyHint / source='balance-observed-other-account',
|
|
5392
|
+
// 这里只负责把话讲清楚 —— **不做相加**(无法判断两次是不是同一个账户)。
|
|
5393
|
+
function usageMultiBookNote(allDays, today) {
|
|
5394
|
+
try {
|
|
5395
|
+
var maxCount = 0, maxHint = '', otherDays = 0, todayCount = 0
|
|
5396
|
+
for (var i = 0; i < (allDays || []).length; i++) {
|
|
5397
|
+
var dx = allDays[i] || {}
|
|
5398
|
+
var n = Number(dx.bookCount) || 0
|
|
5399
|
+
if (n > maxCount) { maxCount = n; maxHint = String(dx.historyHint || '') }
|
|
5400
|
+
if (dx.source === 'balance-observed-other-account') otherDays++
|
|
5401
|
+
}
|
|
5402
|
+
todayCount = Number(today && today.bookCount) || 0
|
|
5403
|
+
if (maxCount <= 1 && !otherDays) return ''
|
|
5404
|
+
var parts = []
|
|
5405
|
+
if (maxCount > 1 || todayCount > 1) {
|
|
5406
|
+
parts.push('检测到 ' + Math.max(maxCount, todayCount) + ' 个记账本(换过 API key):' + (maxHint || '同一天在多个本里都有观测'))
|
|
5407
|
+
}
|
|
5408
|
+
if (otherDays > 0) parts.push('另有 ' + otherDays + ' 天来自历史记账本,已按「已观测消费 · 历史账户」显示,未与当前账户相加')
|
|
5409
|
+
return parts.join(';')
|
|
5410
|
+
} catch (err) { return '' }
|
|
5411
|
+
}
|
|
4384
5412
|
function usageAggModels(daysArr) {
|
|
4385
5413
|
var map = {}
|
|
4386
5414
|
;(daysArr || []).forEach(function (day) {
|
|
@@ -4704,6 +5732,15 @@ function fillUsageRecordsWindow(d) {
|
|
|
4704
5732
|
sourceNote.className = 'dshwv-usage-hint'
|
|
4705
5733
|
sourceNote.textContent = '按各日标注口径汇总;本机模型明细为估算,与账户消费覆盖范围不同。'
|
|
4706
5734
|
ov.appendChild(sourceNote)
|
|
5735
|
+
// v776(issue #163):多记账本(换过 API key)时补一句说明,让旧账本的记录有入口可查
|
|
5736
|
+
var bookNote = usageMultiBookNote(allDays, d.today)
|
|
5737
|
+
if (bookNote) {
|
|
5738
|
+
var bn = document.createElement('div')
|
|
5739
|
+
bn.className = 'dshwv-usage-hint'
|
|
5740
|
+
bn.style.marginTop = '2px'
|
|
5741
|
+
bn.textContent = bookNote
|
|
5742
|
+
ov.appendChild(bn)
|
|
5743
|
+
}
|
|
4707
5744
|
body.appendChild(ov)
|
|
4708
5745
|
var detailBox = null
|
|
4709
5746
|
// ② 统计图表(默认折叠,懒渲染)
|
|
@@ -4772,7 +5809,9 @@ function fillUsageRecordsWindow(d) {
|
|
|
4772
5809
|
if ((dayV === undefined || dayV === 0) && day === todayKeyStr2 && d.today && isFinite(Number(d.today.total))) dayV = Number(d.today.total)
|
|
4773
5810
|
if (dayV === undefined || dayV === null) dayV = evs.reduce(function (a, x) { return a + (Number(x.cost) || 0) }, 0)
|
|
4774
5811
|
c.textContent = usageMoney(dayV, dayMeta[day] && dayMeta[day].currency)
|
|
4775
|
-
|
|
5812
|
+
// v776(issue #163):悬停提示带上"历史记账本"说明(旧 key 的记账不再看着像丢了)
|
|
5813
|
+
var dayM = dayMeta[day] || {}
|
|
5814
|
+
name.title = (dayM.label || '本地估算') + (dayM.historyHint ? ' · ' + dayM.historyHint : '')
|
|
4776
5815
|
row.appendChild(c)
|
|
4777
5816
|
var chev = document.createElement('span')
|
|
4778
5817
|
chev.className = 'dshwv-usage-chev'
|
|
@@ -6432,6 +7471,7 @@ function bubbleModuleSummary(m) {
|
|
|
6432
7471
|
m = m || {}
|
|
6433
7472
|
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '余额数值'
|
|
6434
7473
|
if (m.type === 'today') return bubbleIsModelMod(m) ? ('今日已用·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '今日已用'
|
|
7474
|
+
if (m.type === 'session') return '对话名' + (Number(m.len) > 0 ? '(保留 ' + Math.round(Number(m.len)) + ' 字)' : '(不截断)')
|
|
6435
7475
|
if (m.type === 'quota') return '额度·' + ((apiModelById(m.modelId) || {}).name || m.modelId)
|
|
6436
7476
|
if (m.type === 'plan') return bubblePlanModuleLabel(m)
|
|
6437
7477
|
if (m.type === 'peak' || m.type === 'nextpeak') return bubblePeakModuleLabel(m)
|
|
@@ -6449,6 +7489,7 @@ function bubbleModuleListLabel(m) {
|
|
|
6449
7489
|
if (m.type === 'random') return m.name || '随机语句' // 只显示模块名,不展示内部句子
|
|
6450
7490
|
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '余额数值'
|
|
6451
7491
|
if (m.type === 'today') return bubbleIsModelMod(m) ? ('今日已用·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '今日已用'
|
|
7492
|
+
if (m.type === 'session') return '对话名' + (Number(m.len) > 0 ? '(保留 ' + Math.round(Number(m.len)) + ' 字)' : '(不截断)')
|
|
6452
7493
|
if (m.type === 'quota') return '额度·' + ((apiModelById(m.modelId) || {}).name || m.modelId)
|
|
6453
7494
|
if (m.type === 'plan') return bubblePlanModuleLabel(m)
|
|
6454
7495
|
if (m.type === 'peak' || m.type === 'nextpeak') return bubblePeakModuleLabel(m)
|
|
@@ -7148,6 +8189,7 @@ var QC_SCHEMES = [
|
|
|
7148
8189
|
['macaron', '马卡龙'], ['candy', '糖果'], ['rouge', '酒红'], ['bamboo', '翠青'], ['aurora', '极光幻彩'],
|
|
7149
8190
|
['deepsea', '深海蓝调'], ['sunset', '落日熔金'], ['forest', '森林秘语'], ['champagne', '香槟鎏金'],
|
|
7150
8191
|
['lavender', '薰衣草梦境'], ['mint', '薄荷汽水'], ['lava', '岩浆熔岩'], ['galaxy', '银河星紫'], ['ink', '墨韵黑白'], ['indigo', '靛蓝夜曲'],
|
|
8192
|
+
['blaze', '火红烈焰'], ['amber', '警示橙黄'],
|
|
7151
8193
|
]
|
|
7152
8194
|
function qeditEnsure() {
|
|
7153
8195
|
if (qeditEl) return qeditEl
|
|
@@ -7421,6 +8463,38 @@ function openQuickTextEditor(m, anchorBtn) {
|
|
|
7421
8463
|
} catch (err) { qeditPlace({ left: 40, right: 360, top: 200, bottom: 300, width: 320 }, 320, false) }
|
|
7422
8464
|
}
|
|
7423
8465
|
// —— 特殊内容模块(balance/today/peak/nextpeak):悬浮编辑,内容与原先编辑窗口一致 ——
|
|
8466
|
+
// v769:对话名模块的「保留长度」行 —— 悬浮编辑器(qRow/qLabel)与整窗编辑器(.dshwv-audiorow/span)
|
|
8467
|
+
// **共用同一份实现**,只有外层行/标签的构造方式不同(各自的样式才一致)。
|
|
8468
|
+
// rowFactory() 造一行、labelFactory(text) 造标签;changed() 在输入时回调(悬浮窗用它做实时预览)。
|
|
8469
|
+
function sessionLenRowBuild(rowFactory, labelFactory, m, changed) {
|
|
8470
|
+
var r = rowFactory()
|
|
8471
|
+
r.appendChild(labelFactory('保留长度'))
|
|
8472
|
+
var inp = document.createElement('input')
|
|
8473
|
+
inp.type = 'number'
|
|
8474
|
+
inp.min = '0'
|
|
8475
|
+
inp.max = '120'
|
|
8476
|
+
inp.step = '1'
|
|
8477
|
+
inp.className = 'dshwv-winput'
|
|
8478
|
+
if (m.len === undefined || m.len === null) m.len = WAIT_SESSION_MAX
|
|
8479
|
+
inp.value = String(Math.max(0, Math.round(Number(m.len) || 0)))
|
|
8480
|
+
inp.title = '对话名超过这个长度时截断成「前N字...」;0 = 不截断'
|
|
8481
|
+
inp.addEventListener('input', function () {
|
|
8482
|
+
var n = Math.round(Number(inp.value) || 0)
|
|
8483
|
+
m.len = Math.max(0, Math.min(120, n))
|
|
8484
|
+
try { if (typeof changed === 'function') changed() } catch (err) {}
|
|
8485
|
+
})
|
|
8486
|
+
inp.addEventListener('change', function () {
|
|
8487
|
+
inp.value = String(Math.max(0, Math.round(Number(m.len) || 0)))
|
|
8488
|
+
})
|
|
8489
|
+
r.appendChild(inp)
|
|
8490
|
+
var tail = labelFactory('字,超出截断为 ...(0 = 不截断)')
|
|
8491
|
+
try {
|
|
8492
|
+
tail.style.opacity = '.75'
|
|
8493
|
+
tail.style.fontSize = '11px'
|
|
8494
|
+
} catch (err) {}
|
|
8495
|
+
r.appendChild(tail)
|
|
8496
|
+
return r
|
|
8497
|
+
}
|
|
7424
8498
|
function openQuickModuleEditor(m, anchorBtn) {
|
|
7425
8499
|
if (!m || bubbleIsImgMod(m) || m.type === 'random' || m.type === 'text') return
|
|
7426
8500
|
if (qeditToggleClose(anchorBtn)) return // v723:同一按钮再点 = 关闭
|
|
@@ -7461,8 +8535,8 @@ function openQuickModuleEditor(m, anchorBtn) {
|
|
|
7461
8535
|
var isModelBal = bubbleIsModelMod(m) && (m.type === 'balance' || m.type === 'today')
|
|
7462
8536
|
var isModelQuota = bubbleIsModelMod(m) && m.type === 'quota'
|
|
7463
8537
|
var isModelPlan = bubbleIsModelMod(m) && m.type === 'plan'
|
|
7464
|
-
inp.placeholder = isModelPlan ? '例: {plan} 额度 · {plan_reset} 刷新时间' : (isModelQuota ? '例: 额度 {quota} · 剩 {quota_left}' : (isModelBal ? '例: {balance} 或 今日 {today}' : (m.type === 'balance' ? '例: {balance_ds}' : (m.type === 'today' ? '例: 今日已用 {expense_ds}' : (bubbleIsPeakCount(m) ? '例: 距空闲 {countdown}' : '例: 当前 {status}')))))
|
|
7465
|
-
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}' : '{expense_ds}')))))
|
|
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}')))))))
|
|
7466
8540
|
inp.addEventListener('input', function () { m.tpl = inp.value; changed() })
|
|
7467
8541
|
r.appendChild(inp)
|
|
7468
8542
|
var qb2 = document.createElement('button')
|
|
@@ -7476,6 +8550,8 @@ function openQuickModuleEditor(m, anchorBtn) {
|
|
|
7476
8550
|
box.appendChild(r)
|
|
7477
8551
|
}
|
|
7478
8552
|
tplRow()
|
|
8553
|
+
// v769:对话名模块的「保留长度」(与整窗编辑器共用同一份实现;这里改了会实时刷新预览)
|
|
8554
|
+
if (m.type === 'session') box.appendChild(sessionLenRowBuild(qRow, qLabel, m, changed))
|
|
7479
8555
|
// 订阅额度模块的「显示样式」= 选时间窗口(多窗口厂商,如 OpenCode Go 的 5h / 周 / 月)
|
|
7480
8556
|
if (m.type === 'plan' && (apiPlanMultiWin(m.modelId) || apiPlanWinList(m.modelId))) {
|
|
7481
8557
|
var wrow = qRow()
|
|
@@ -7661,6 +8737,8 @@ function renderBubblePal() {
|
|
|
7661
8737
|
{ key: 'today', label: '今日已用', pin: true, cb: function () { bubbleModuleAdd({ type: 'today', size: 1, tpl: '今日已用 {expense_ds}' }) } },
|
|
7662
8738
|
{ key: 'peak', label: '峰谷时段', pin: true, cb: function () { bubbleModuleAdd({ type: 'peak', size: 4, peakColor: '#e0433f', offColor: '#2fa24c', tpl: '{status}' }) } },
|
|
7663
8739
|
{ key: 'nextpeak', label: '时段倒计时', pin: true, cb: function () { bubbleModuleAdd({ type: 'peak', size: 6, bold: true, peakStyle: 'count', peakColor: '#e0433f', offColor: '#2fa24c', tpl: '{countdown}' }) } },
|
|
8740
|
+
// v768:对话名模块(内容 = 当前对话标题;「保留长度」可在模块编辑器里改)
|
|
8741
|
+
{ key: 'session', label: '对话名', pin: true, cb: function () { bubbleModuleAdd({ type: 'session', size: 4, bold: true, tpl: '{session}', len: WAIT_SESSION_MAX }) } },
|
|
7664
8742
|
{ key: 'random', label: '随机语句', cb: function () { bubbleModuleAdd(bubbleCloneModule(bubbleDefaultSecondModules()[0])) } },
|
|
7665
8743
|
{ key: 'link', label: '超链接', cb: function () { bubbleModuleAdd(bubblePaletteModule('link')) } },
|
|
7666
8744
|
{ key: 'image', label: '图片/动图', cb: function () { bubblePickImageToAdd() } },
|
|
@@ -7878,7 +8956,8 @@ function bubblePvRowCommit(rows) {
|
|
|
7878
8956
|
function bubbleModuleEdit(m, anchorBtn) {
|
|
7879
8957
|
try {
|
|
7880
8958
|
if (m && (m.type === 'text' || m.type === 'link')) { openQuickTextEditor(m, anchorBtn); return }
|
|
7881
|
-
|
|
8959
|
+
// v769:对话名也走悬浮编辑(像文本那样边改边看效果),不再开整窗编辑器
|
|
8960
|
+
if (m && (m.type === 'balance' || m.type === 'today' || m.type === 'peak' || m.type === 'nextpeak' || m.type === 'session')) { openQuickModuleEditor(m, anchorBtn); return }
|
|
7882
8961
|
openModuleEditor(m, function (saved) { if (saved) renderBubblePv() })
|
|
7883
8962
|
} catch (err) {}
|
|
7884
8963
|
}
|
|
@@ -7980,6 +9059,7 @@ function bubblePaletteModule(key) {
|
|
|
7980
9059
|
if (key === 'today') return { type: 'today', size: 1, tpl: '今日已用 {expense_ds}' }
|
|
7981
9060
|
if (key === 'peak') return { type: 'peak', size: 4, peakColor: '#e0433f', offColor: '#2fa24c', tpl: '{status}' }
|
|
7982
9061
|
if (key === 'nextpeak') return { type: 'peak', size: 6, bold: true, peakStyle: 'count', peakColor: '#e0433f', offColor: '#2fa24c', tpl: '{countdown}' }
|
|
9062
|
+
if (key === 'session') return { type: 'session', size: 4, bold: true, tpl: '{session}', len: WAIT_SESSION_MAX }
|
|
7983
9063
|
if (key === 'link') return { type: 'link', text: '打开链接', url: '', size: 6, color: '#2f4488' }
|
|
7984
9064
|
if (key === 'randimg') return { type: 'randimg', imgs: [], imgScale: 1 }
|
|
7985
9065
|
// 自定义 API 模型的余额模块(palette key = bal:<modelId>)
|
|
@@ -8869,6 +9949,8 @@ function bubbleRgbSelect(current, cb) {
|
|
|
8869
9949
|
['galaxy', '银河星紫'],
|
|
8870
9950
|
['ink', '墨韵黑白'],
|
|
8871
9951
|
['indigo', '靛蓝夜曲'],
|
|
9952
|
+
['blaze', '火红烈焰'],
|
|
9953
|
+
['amber', '警示橙黄'],
|
|
8872
9954
|
]
|
|
8873
9955
|
function labelOf(v) {
|
|
8874
9956
|
for (var i = 0; i < opts.length; i++) if (opts[i][0] === v) return opts[i][1]
|
|
@@ -9045,6 +10127,7 @@ function bubbleFontEditRow(getVal, setVal) {
|
|
|
9045
10127
|
function moduleTypeName(t, m) {
|
|
9046
10128
|
if (t === 'balance') return '余额数值'
|
|
9047
10129
|
if (t === 'today') return '今日已用'
|
|
10130
|
+
if (t === 'session') return '对话名'
|
|
9048
10131
|
if (t === 'peak' || t === 'nextpeak') {
|
|
9049
10132
|
// 峰谷模块按显示样式给名(倒计时/简洁等);无模块对象时退回通用名
|
|
9050
10133
|
if (m) return bubblePeakModuleLabel(m)
|
|
@@ -9091,6 +10174,7 @@ function renderModuleEditor() {
|
|
|
9091
10174
|
function hintOf() {
|
|
9092
10175
|
if (m.type === 'balance') return '例: {balance_ds}'
|
|
9093
10176
|
if (m.type === 'today') return '例: 今日已用 {expense_ds}'
|
|
10177
|
+
if (m.type === 'session') return '例: {session} 或 当前对话 {session}'
|
|
9094
10178
|
if (bubbleIsPeakCount(m)) return '例: 距空闲 {countdown}'
|
|
9095
10179
|
return '例: 当前 {status}'
|
|
9096
10180
|
}
|
|
@@ -9667,10 +10751,26 @@ function renderModuleEditor() {
|
|
|
9667
10751
|
} else {
|
|
9668
10752
|
var note = document.createElement('div')
|
|
9669
10753
|
note.className = 'dshwv-bubhint'
|
|
9670
|
-
note.textContent = '
|
|
10754
|
+
note.textContent = m.type === 'session'
|
|
10755
|
+
? '该模块自动显示**当前对话名**(占位符 {session}),可调下方颜色 / 字号'
|
|
10756
|
+
: '该模块为内置数值,内容自动获取,可调下方颜色/字号'
|
|
9671
10757
|
moduleBodyEl.appendChild(note)
|
|
9672
10758
|
// F1:整句文案模板(如「今日已用 {值}」;留空=默认)
|
|
9673
10759
|
moduleTplRow()
|
|
10760
|
+
// v768/v769:对话名模块额外给「保留长度」(与悬浮编辑器共用同一份实现;
|
|
10761
|
+
// 悬浮窗改了会实时刷新预览,这里点保存才落盘)
|
|
10762
|
+
if (m.type === 'session') {
|
|
10763
|
+
moduleBodyEl.appendChild(sessionLenRowBuild(function () {
|
|
10764
|
+
var d = document.createElement('div')
|
|
10765
|
+
d.className = 'dshwv-audiorow'
|
|
10766
|
+
return d
|
|
10767
|
+
}, function (t) {
|
|
10768
|
+
var s = document.createElement('span')
|
|
10769
|
+
s.textContent = t
|
|
10770
|
+
s.style.flex = '0 0 auto'
|
|
10771
|
+
return s
|
|
10772
|
+
}, m, null))
|
|
10773
|
+
}
|
|
9674
10774
|
}
|
|
9675
10775
|
}
|
|
9676
10776
|
// 公共样式区(图片/动图、随机语句模块不显示:随机语句样式在每句单独编辑中设置)
|
|
@@ -11048,6 +12148,10 @@ function bubbleClearAll() {
|
|
|
11048
12148
|
try { if (settleTimer) { clearTimeout(settleTimer); settleTimer = null } } catch (err) {}
|
|
11049
12149
|
}
|
|
11050
12150
|
function bubbleCloseVisual() {
|
|
12151
|
+
// v771:泡泡一旦真的收起,等待标记必须一起复位。`waitShown` 平时由 bubbleRenderModules 按场景重算,
|
|
12152
|
+
// 但"整泡关闭"(hideBubble/hideCostBubble/hideWaitBubble…)不走渲染 ⇒ 不复位就会残留成 true,
|
|
12153
|
+
// 之后 showWaitBubble 会误判"已经在显示等待泡泡"(历史上正是这类不同步把等待泡泡卡死的)。
|
|
12154
|
+
waitShown = false
|
|
11051
12155
|
try { bubbleBox.classList.remove('dshwv-pop-open') } catch (err) {}
|
|
11052
12156
|
try { textBox.style.transition = ''; textBox.style.opacity = '' } catch (err) {}
|
|
11053
12157
|
try { hintEl.style.transition = ''; hintEl.style.opacity = '' } catch (err) {}
|
|
@@ -11114,6 +12218,9 @@ function bubbleAutoClose() {
|
|
|
11114
12218
|
bubbleTtlTimer = null
|
|
11115
12219
|
if (bubbleScene && bubbleScene.kind === 'cost') { if (whaleSysSwapNext()) return; hideCostBubble(); return }
|
|
11116
12220
|
if (bubbleScene && bubbleScene.kind === 'alert') { if (whaleSysSwapNext()) return; hideUsageAlertBubble(); return }
|
|
12221
|
+
// v761(#161 C5):等待交互泡泡是**常驻**的(sceneOpen 收到 ttl 0 不布计时器),正常永远走不到这里;
|
|
12222
|
+
// 这一支是防御性的(例如用户先点开等待泡泡再改设置导致 ttl 被重新布上),语义仍与其他系统泡泡一致。
|
|
12223
|
+
if (bubbleScene && bubbleScene.kind === 'wait') { if (whaleSysSwapNext()) return; hideWaitBubble(); return }
|
|
11117
12224
|
hideBubble()
|
|
11118
12225
|
}
|
|
11119
12226
|
// 重置当前泡泡的留存计时(点鲸鱼给第 1 泡续时,不清内容)
|
|
@@ -11143,6 +12250,15 @@ function bubbleRenderRandom(lines) {
|
|
|
11143
12250
|
function bubbleRenderCostMods(amount) {
|
|
11144
12251
|
bubbleRenderModules(usageAlertModsResolved(usageTurnCostLines(), null, null, usageCostValue(amount)))
|
|
11145
12252
|
}
|
|
12253
|
+
// 等待交互提示(v761 / #161 C5):内容取 events.<kind>.lines(缺失/空 → 默认模板),
|
|
12254
|
+
// 走与预警/预算/消耗同一条模块渲染链路。`{session}` 在这里就替换成当前对话名
|
|
12255
|
+
// (usageAlertModsResolved → usageFillText 的会话名回落),所以真实泡泡显示的是对话名而不是 `{session}`。
|
|
12256
|
+
function bubbleRenderWaitMods(kind) {
|
|
12257
|
+
var k = (kind === 'approval') ? 'approval' : 'question'
|
|
12258
|
+
var cfg = {}
|
|
12259
|
+
try { cfg = ((usageSet || {}).events || {})[k] || {} } catch (err) { cfg = {} }
|
|
12260
|
+
bubbleRenderModules(usageAlertModsResolved(usageWaitLinesOf(cfg, k), null, null, null))
|
|
12261
|
+
}
|
|
11146
12262
|
// 并列步骤:每轮到这一步时独立按权重抽一个候选泡(不记忆上次,允许连续几轮同泡)
|
|
11147
12263
|
function bubblePickChoiceStep(step) {
|
|
11148
12264
|
var opts = bubbleChoiceOptions(step)
|
|
@@ -11175,8 +12291,8 @@ function bubbleShowSeqNext() {
|
|
|
11175
12291
|
}
|
|
11176
12292
|
}
|
|
11177
12293
|
// ===== 模块渲染引擎(B1) =====
|
|
11178
|
-
// 模块:{type:'text'|'balance'|'today'|'peak'|'image'|'random', text?, imgId?, color?, size?(1..8 档),
|
|
11179
|
-
// lines?:[{t,w}] (random 自带句子列表)}
|
|
12294
|
+
// 模块:{type:'text'|'balance'|'today'|'peak'|'session'|'image'|'random', text?, imgId?, color?, size?(1..8 档),
|
|
12295
|
+
// lines?:[{t,w}] (random 自带句子列表), tpl?(占位符模板), len?(session 的「保留长度」,0=不截断)}
|
|
11180
12296
|
// 字号档位 1..50,线性细分(1→40u … 50→240u,相对 --dshw-u 的倍数)
|
|
11181
12297
|
function bubbleModuleFontU(level) {
|
|
11182
12298
|
var n = Number(level) || 6
|
|
@@ -11563,6 +12679,9 @@ function bubbleContentTokenMap(m) {
|
|
|
11563
12679
|
} else if (m.type === 'today') {
|
|
11564
12680
|
v = (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
11565
12681
|
map['expense_ds'] = v
|
|
12682
|
+
} else if (m.type === 'session') {
|
|
12683
|
+
// v768:对话名模块 —— {session} = 当前对话标题(按模块的「保留长度」截断)
|
|
12684
|
+
map['session'] = bubbleSessionText(m)
|
|
11566
12685
|
} else if (bubbleIsPeakCount(m)) {
|
|
11567
12686
|
v = bubbleCountdownText()
|
|
11568
12687
|
map['countdown'] = v
|
|
@@ -11596,6 +12715,7 @@ function bubbleTplHelpItems(m) {
|
|
|
11596
12715
|
}
|
|
11597
12716
|
if (m.type === 'balance') add('balance_ds', '余额数值')
|
|
11598
12717
|
else if (m.type === 'today') add('expense_ds', '今日已用金额')
|
|
12718
|
+
else if (m.type === 'session') add('session', '当前对话名(超过「保留长度」会截断为 前N字...)')
|
|
11599
12719
|
else if (m.type === 'peak' || m.type === 'nextpeak') {
|
|
11600
12720
|
if (bubbleIsPeakCount(m)) add('countdown', '距下一时段倒计时 (HH:MM:SS)')
|
|
11601
12721
|
else add('status', '高峰/空闲 状态文字(随显示样式变化)')
|
|
@@ -11870,7 +12990,7 @@ function bubbleCountdownApplyStyle(el, mod, peak) {
|
|
|
11870
12990
|
if (rgb) {
|
|
11871
12991
|
var scheme = rgb === true ? 'macaron' : String(rgb || 'macaron')
|
|
11872
12992
|
el.classList.add('dshwv-rgb')
|
|
11873
|
-
if (scheme
|
|
12993
|
+
if (bubbleRgbSchemeOk(scheme)) el.classList.add('dshwv-rgb-' + scheme)
|
|
11874
12994
|
} else if (col) {
|
|
11875
12995
|
el.style.color = col
|
|
11876
12996
|
}
|
|
@@ -11932,10 +13052,12 @@ function bubblePeakRowRegister(row, tx, mod, peakNow) {
|
|
|
11932
13052
|
} catch (err) {}
|
|
11933
13053
|
}
|
|
11934
13054
|
// 跑马灯配色方案白名单(与 blockOf() / bubbleCountdownApplyStyle 用的那套一致)
|
|
13055
|
+
// v770:新增配色时**只改这一处**(其余调用点已改成调它,不再各自维护一串 || 判断)
|
|
11935
13056
|
function bubbleRgbSchemeOk(s) {
|
|
11936
13057
|
return s === 'candy' || s === 'rouge' || s === 'bamboo' || s === 'aurora' || s === 'deepsea' ||
|
|
11937
13058
|
s === 'sunset' || s === 'forest' || s === 'champagne' || s === 'lavender' || s === 'mint' ||
|
|
11938
|
-
s === 'lava' || s === 'galaxy' || s === 'ink' || s === 'indigo'
|
|
13059
|
+
s === 'lava' || s === 'galaxy' || s === 'ink' || s === 'indigo' ||
|
|
13060
|
+
s === 'blaze' || s === 'amber'
|
|
11939
13061
|
}
|
|
11940
13062
|
function bubblePeakRowClearClass(el, prefix) {
|
|
11941
13063
|
try {
|
|
@@ -11993,6 +13115,10 @@ function bubbleRowContentOf(mod) {
|
|
|
11993
13115
|
var tv2 = bubbleIsModelMod(mod) ? apiModelTodayText(mod.modelId) : (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
11994
13116
|
return { txt: bubbleContentText(mod, '今日已用 ' + tv2), line: null }
|
|
11995
13117
|
}
|
|
13118
|
+
if (mod.type === 'session') {
|
|
13119
|
+
// v768:对话名模块(自动内容 = 截断后的对话名;留空 tpl 时直接用自动内容)
|
|
13120
|
+
return { txt: bubbleContentText(mod, bubbleSessionText(mod)), line: null }
|
|
13121
|
+
}
|
|
11996
13122
|
if (mod.type === 'quota') {
|
|
11997
13123
|
var qi = apiQuotaInfo(mod.modelId)
|
|
11998
13124
|
var qt = qi
|
|
@@ -12108,7 +13234,7 @@ function bubbleRowsTo(parentEl, mods) {
|
|
|
12108
13234
|
// 跑马灯渐变:true 兼容旧数据=马卡龙;支持 macaron/candy/rouge/bamboo…
|
|
12109
13235
|
target.classList.add('dshwv-rgb')
|
|
12110
13236
|
var scheme = g === true ? 'macaron' : String(g || 'macaron')
|
|
12111
|
-
if (scheme
|
|
13237
|
+
if (bubbleRgbSchemeOk(scheme)) target.classList.add('dshwv-rgb-' + scheme)
|
|
12112
13238
|
}
|
|
12113
13239
|
if (marquee) {
|
|
12114
13240
|
// 文字跑马灯装在内层 span(有底色时)或行上;底色在 row 层,两者可叠加;
|
|
@@ -12127,7 +13253,7 @@ function bubbleRowsTo(parentEl, mods) {
|
|
|
12127
13253
|
if (needBg) {
|
|
12128
13254
|
if (effBgRgb) {
|
|
12129
13255
|
row.classList.add('dshwv-bgrgb')
|
|
12130
|
-
if (effBgRgb
|
|
13256
|
+
if (bubbleRgbSchemeOk(effBgRgb) || effBgRgb === 'macaron') row.classList.add('dshwv-bgrgb-' + effBgRgb)
|
|
12131
13257
|
row.style.animationDuration = bubbleMarqueeDur()
|
|
12132
13258
|
} else if (effBg) {
|
|
12133
13259
|
row.style.background = effBg
|
|
@@ -12285,8 +13411,15 @@ function bubbleRowsTo(parentEl, mods) {
|
|
|
12285
13411
|
// 当前泡泡正在显示的模块列表(供场景切换时记录;显示期间不再即时整泡重绘,
|
|
12286
13412
|
// 避免用户观看时内容被刷新打断——更新统一在“泡泡消失→下一次显示”的渲染采用最新 state)
|
|
12287
13413
|
var bubbleLiveMods = null
|
|
13414
|
+
// v761(#161 C5):当前是否正显示「等待交互」的常驻泡泡。只有真正渲染出等待内容时才置位,
|
|
13415
|
+
// 因此拿它做幂等判断(同名再次入队直接丢弃)不会把"排了队但还没轮到"的项误判成已显示。
|
|
13416
|
+
var waitShown = false
|
|
12288
13417
|
function bubbleRenderModules(mods) {
|
|
12289
13418
|
try {
|
|
13419
|
+
// v761(#161 C5):等待交互泡泡(kind:'wait')从布场景到渲染都是同步完成的,所以真正的
|
|
13420
|
+
// "已显示"标记放在这里取;其它场景/关闭路径会把 bubbleScene 置空或换 kind,因此
|
|
13421
|
+
// 该标记不可能残留成"其实没显示却以为在显示"。
|
|
13422
|
+
waitShown = !!(bubbleScene && bubbleScene.kind === 'wait')
|
|
12290
13423
|
bubbleLiveMods = Array.isArray(mods) ? mods.slice() : null
|
|
12291
13424
|
// 清掉旧模块行,隐藏老三行与 gif(它们仍保留在 DOM 供普通场景使用)
|
|
12292
13425
|
gifEl.style.display = 'none'
|
|
@@ -12390,6 +13523,9 @@ function bubbleNext() {
|
|
|
12390
13523
|
if (!bubbleShown) return
|
|
12391
13524
|
if (bubbleScene && bubbleScene.kind === 'cost') { hideCostBubble(); return }
|
|
12392
13525
|
if (bubbleScene && bubbleScene.kind === 'alert') { hideUsageAlertBubble(); return }
|
|
13526
|
+
// v761(#161 C5):等待交互泡泡必须**常驻到被回答/批准**——点它不关(点击泡泡 = 推进/收起是给
|
|
13527
|
+
// 手动轮的语义);避免用户误点把「正在等待你的回答」收掉、而后台其实还挂着。
|
|
13528
|
+
if (bubbleScene && bubbleScene.kind === 'wait') return
|
|
12393
13529
|
if (bubbleRoundOn && bubbleSeqIdx < bubbleSeq.length) { bubbleShowSeqNext(); return }
|
|
12394
13530
|
hideBubble()
|
|
12395
13531
|
} catch (err) {}
|
|
@@ -12437,20 +13573,71 @@ var USAGE_ALERT_TTL = 6500 // 提醒泡泡停留毫秒数
|
|
|
12437
13573
|
var whaleSysQueue = []
|
|
12438
13574
|
var whaleSysItem = null // 当前正展示的 {kind, mods/amount, rank}
|
|
12439
13575
|
var whaleSysTimer = null
|
|
13576
|
+
// 队列插入(v772 起抽成函数,供 push / 抢占退回 共用):
|
|
13577
|
+
// rank 小的先出(1 今日预算 / 2 余额预警 / 3 每轮消耗 / 4 等待交互);
|
|
13578
|
+
// **同档的「每轮消耗」后入先出** —— 新的一轮消耗插到已有消耗项之前 ⇒ 新的置顶,旧的排队;
|
|
13579
|
+
// 其它档位保持先入先出(预警/预算按发生顺序看更合理),等待交互项永远排在最前。
|
|
13580
|
+
function whaleSysQueueInsert(item) {
|
|
13581
|
+
var rank = Number(item.rank)
|
|
13582
|
+
if (!(rank >= 1)) rank = 2
|
|
13583
|
+
item.rank = rank
|
|
13584
|
+
var pos = whaleSysQueue.length
|
|
13585
|
+
for (var i = 0; i < whaleSysQueue.length; i++) {
|
|
13586
|
+
var q = whaleSysQueue[i] || {}
|
|
13587
|
+
var qr = Number(q.rank)
|
|
13588
|
+
if (!(qr >= 1)) qr = 2
|
|
13589
|
+
if (qr > rank || (item.kind === 'cost' && q.kind === 'cost' && qr === rank)) { pos = i; break }
|
|
13590
|
+
}
|
|
13591
|
+
whaleSysQueue.splice(pos, 0, item)
|
|
13592
|
+
return pos
|
|
13593
|
+
}
|
|
13594
|
+
// 展示一项(v772 抽成函数:tick / 淡切下一项 / 抢占都走这一份,避免三处各写一遍又不一致)
|
|
13595
|
+
function whaleSysOpenItem(item) {
|
|
13596
|
+
whaleSysItem = item
|
|
13597
|
+
if (item.kind === 'cost') {
|
|
13598
|
+
sceneOpen('cost', function () { bubbleRenderCostMods(item.amount) }, turnCostCloseMs > 0 ? turnCostCloseMs : 0)
|
|
13599
|
+
} else if (item.kind === 'wait') {
|
|
13600
|
+
// 等待交互:常驻(ttl 0 = sceneOpen 不布自动关闭计时器),内容在渲染时按当前设置取
|
|
13601
|
+
sceneOpen('wait', function () { bubbleRenderWaitMods(item.waitKind) }, 0)
|
|
13602
|
+
} else {
|
|
13603
|
+
sceneOpen('alert', function () { bubbleRenderModules(item.mods || []) }, (item && item.ttlMs != null) ? item.ttlMs : USAGE_ALERT_TTL)
|
|
13604
|
+
}
|
|
13605
|
+
}
|
|
12440
13606
|
function whaleSysPush(item) {
|
|
12441
13607
|
try {
|
|
12442
13608
|
if (!bubbleOn || !bubbleBox || !textBox) return false
|
|
13609
|
+
if (!item || !item.kind) return false
|
|
13610
|
+
// v772:**等待交互(授权/提问)享有最高优先级,永远不会被"消耗泡泡正开着"挡住**。
|
|
13611
|
+
// 整轮对话都卡在等你回答/批准,它必须立刻可见;正在展示的其它系统泡泡(消耗/预警)退回队列,
|
|
13612
|
+
// 等挂起解除后照旧继续 —— 用户真机反馈「已有每轮消耗提示时,授权与提问不会显示」。
|
|
13613
|
+
// 顺带兜住历史上的一个坑:`costBubbleActive` 万一残留成 true 而 `whaleSysItem` 为空,
|
|
13614
|
+
// 下面的 `costBubbleActive && !whaleSysItem` 早退会把所有入队都吃掉(等待泡泡一起被丢)。
|
|
13615
|
+
if (item.kind === 'wait') {
|
|
13616
|
+
if (whaleSysItem && whaleSysItem.kind === 'wait') {
|
|
13617
|
+
if (whaleSysItem.waitKind === item.waitKind) return true // 已在显示同一类:内容无需重渲染
|
|
13618
|
+
whaleSysItem = item
|
|
13619
|
+
sceneOpen('wait', function () { bubbleRenderWaitMods(item.waitKind) }, 0)
|
|
13620
|
+
return true
|
|
13621
|
+
}
|
|
13622
|
+
if (whaleSysItem) {
|
|
13623
|
+
var pre = whaleSysItem
|
|
13624
|
+
whaleSysItem = null
|
|
13625
|
+
whaleSysQueueInsert(pre) // 退回队列(按 rank + 消耗 LIFO 规则就位)
|
|
13626
|
+
}
|
|
13627
|
+
whaleSysOpenItem(item)
|
|
13628
|
+
return true
|
|
13629
|
+
}
|
|
12443
13630
|
// 正在展示消耗泡泡时,新提醒退化为居中卡片(避免与消耗内容抢层);排队中的则按序等
|
|
12444
13631
|
if (costBubbleActive && !whaleSysItem) return false
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
}
|
|
12453
|
-
|
|
13632
|
+
// v772:**每轮消耗改后入先出** —— 新一轮消耗直接置顶,原来那条退回队列(点击时按"新→旧"回看)。
|
|
13633
|
+
if (item.kind === 'cost' && whaleSysItem && whaleSysItem.kind === 'cost') {
|
|
13634
|
+
var pre2 = whaleSysItem
|
|
13635
|
+
whaleSysItem = null
|
|
13636
|
+
whaleSysQueueInsert(pre2)
|
|
13637
|
+
whaleSysOpenItem(item)
|
|
13638
|
+
return true
|
|
13639
|
+
}
|
|
13640
|
+
whaleSysQueueInsert(item)
|
|
12454
13641
|
if (whaleSysTimer) { clearTimeout(whaleSysTimer); whaleSysTimer = null }
|
|
12455
13642
|
whaleSysTimer = setTimeout(whaleSysTick, 30)
|
|
12456
13643
|
return true
|
|
@@ -12463,15 +13650,12 @@ function whaleSysTick() {
|
|
|
12463
13650
|
if (whaleSysItem) return
|
|
12464
13651
|
if (!whaleSysQueue.length) return
|
|
12465
13652
|
// 存在尚未入队的正展示系统泡?仅在鲸鱼空闲且无我们自己队列项时取下一个
|
|
12466
|
-
|
|
13653
|
+
// v761(#161 C5):waitShown 也必须算"正被占着"——等待泡泡是常驻的(whaleSysItem 会一直是它),
|
|
13654
|
+
// 正常走不到这里;但万一 item 被别的路径清掉而场景还停在等待内容上,也不能抢它的层。
|
|
13655
|
+
if (costBubbleActive || waitShown || (bubbleScene && bubbleScene.kind === 'alert')) return
|
|
12467
13656
|
var item = whaleSysQueue.shift()
|
|
12468
13657
|
if (!item) return
|
|
12469
|
-
|
|
12470
|
-
if (item.kind === 'cost') {
|
|
12471
|
-
sceneOpen('cost', function () { bubbleRenderCostMods(item.amount) }, turnCostCloseMs > 0 ? turnCostCloseMs : 0)
|
|
12472
|
-
} else {
|
|
12473
|
-
sceneOpen('alert', function () { bubbleRenderModules(item.mods || []) }, (item && item.ttlMs != null) ? item.ttlMs : USAGE_ALERT_TTL)
|
|
12474
|
-
}
|
|
13658
|
+
whaleSysOpenItem(item) // v772:展示逻辑抽成一份(tick / 淡切 / 抢占共用)
|
|
12475
13659
|
} catch (err) {}
|
|
12476
13660
|
}
|
|
12477
13661
|
function whaleSysDone() {
|
|
@@ -12487,12 +13671,7 @@ function whaleSysSwapNext() {
|
|
|
12487
13671
|
if (!whaleSysQueue.length || !bubbleOn || !bubbleShown) return false
|
|
12488
13672
|
var item = whaleSysQueue.shift()
|
|
12489
13673
|
if (!item) return false
|
|
12490
|
-
|
|
12491
|
-
if (item.kind === 'cost') {
|
|
12492
|
-
sceneOpen('cost', function () { bubbleRenderCostMods(item.amount) }, turnCostCloseMs > 0 ? turnCostCloseMs : 0)
|
|
12493
|
-
} else {
|
|
12494
|
-
sceneOpen('alert', function () { bubbleRenderModules(item.mods || []) }, (item && item.ttlMs != null) ? item.ttlMs : USAGE_ALERT_TTL)
|
|
12495
|
-
}
|
|
13674
|
+
whaleSysOpenItem(item) // v772:与 tick / 抢占同一份展示逻辑
|
|
12496
13675
|
return true
|
|
12497
13676
|
} catch (err) { return false }
|
|
12498
13677
|
}
|
|
@@ -12508,7 +13687,88 @@ function hideUsageAlertBubble() {
|
|
|
12508
13687
|
bubbleCloseVisual()
|
|
12509
13688
|
whaleSysDone()
|
|
12510
13689
|
}
|
|
12511
|
-
|
|
13690
|
+
// ===== v761(#161 C5):等待交互的**常驻**泡泡(提问 / 授权)=====
|
|
13691
|
+
// 语义与消耗(3)/预警(2)/预算(1)三档系统泡泡不同:那几档是"显示 N 秒自动关",而挂起泡泡要
|
|
13692
|
+
// **在挂起存在期间一直显示**,直到宿主在 wait.json 里说挂起解除了 → pollWaitState() 调 hideWaitBubble()。
|
|
13693
|
+
// 复核后的实现方式(改动面最小的那一版):
|
|
13694
|
+
// · 复用既有系统泡泡链路(whaleSysPush / whaleSysTick / whaleSysSwapNext / whaleSysDone),
|
|
13695
|
+
// 不另造浮层;新增第四档 rank = 4(排在消耗之后,理由见 showWaitBubble 注释);
|
|
13696
|
+
// · 常驻 = **ttlMs 传 0**。sceneOpen 里 `if (ttlMs > 0) bubbleTtlTimer = setTimeout(...)`,
|
|
13697
|
+
// 所以 0 天然"不布自动关闭计时器";bubbleResetTtl 也带 `ttlMs > 0` 守卫,不会给它补上计时器。
|
|
13698
|
+
// 既有 cost/alert 的取值(turnCostCloseMs / usageRemindTtlMs)完全没动。
|
|
13699
|
+
// · 队列项 kind = 'cost' | 'alert' | 'wait';'wait' 这一支只出现在 whaleSysTick /
|
|
13700
|
+
// whaleSysSwapNext / bubbleAutoClose / bubbleNext 的**新增分支**里,alert/budget/cost 路径逐字未改。
|
|
13701
|
+
// 幂等 + 换类型(**v771 修掉真机卡死**):等待泡泡**永远只有一条**,内容 = 宿主当前上报的那一条挂起。
|
|
13702
|
+
// 依据:宿主 `waitState` 也只有一个槽位(`notePendingEvent` 里每次 `waitState.pending = {...}` 覆盖),
|
|
13703
|
+
// 所以客户端"镜像最新一条"就与宿主同口径,不需要(也不能)排队多条常驻泡泡。
|
|
13704
|
+
// ⚠️ 旧实现遇到"正在显示 A 类、又来了 B 类"时:把 `whaleSysItem` 置空 → 把 A 项 `unshift` 回队首 → 再 push B。
|
|
13705
|
+
// 可屏幕上**仍在显示等待场景**(`waitShown` 依旧 true)⇒
|
|
13706
|
+
// ① `whaleSysTick` 的 `if (waitShown) return` 守卫**永远**拦住队列(B 永不上屏、A 也不换);
|
|
13707
|
+
// ② `whaleSysItem` 已是 null ⇒ `hideWaitBubble()` 的 `if (whaleSysItem && kind==='wait')` 分支走不到
|
|
13708
|
+
// ⇒ 之后宿主说"没有挂起了"也**关不掉** ⇒ 泡泡一直停在 A,直到刷新页面。
|
|
13709
|
+
// 真机复现:先授权、再提问(1 秒轮询很可能错过中间"无挂起"的那一瞬)⇒ 泡泡停在授权、提问泡泡不出现、
|
|
13710
|
+
// 对话结束也不消失。
|
|
13711
|
+
function showWaitBubble(kind, p) {
|
|
13712
|
+
try {
|
|
13713
|
+
if (!bubbleOn) return false // 全局「泡泡总开关」为假 → 一律不显示
|
|
13714
|
+
var k = (kind === 'approval') ? 'approval' : 'question'
|
|
13715
|
+
var cfg = waitEventCfg(k)
|
|
13716
|
+
// 门控:该事件总开关 + 该事件「冒泡提示」开关(任一为假都不冒泡);pollWaitState 也会先判一次
|
|
13717
|
+
if (cfg.on === false || cfg.bubbleOn === false) return false
|
|
13718
|
+
if (!bubbleBox || !textBox) return false
|
|
13719
|
+
var pid = (p && p.id) ? String(p.id) : ''
|
|
13720
|
+
// "正显示等待泡泡"看**场景**(权威)或已占位的队列项(次之)—— 二者取或,避免状态半同步时漏判
|
|
13721
|
+
var showingWait = !!((bubbleScene && bubbleScene.kind === 'wait') || (whaleSysItem && whaleSysItem.kind === 'wait'))
|
|
13722
|
+
if (showingWait) {
|
|
13723
|
+
// 同一类:幂等丢弃(内容一样)。只是 pendingId 变了就顺手更新,便于排查。
|
|
13724
|
+
if (whaleSysItem && whaleSysItem.kind === 'wait' && whaleSysItem.waitKind === k) {
|
|
13725
|
+
if (pid) whaleSysItem.pendingId = pid
|
|
13726
|
+
return false
|
|
13727
|
+
}
|
|
13728
|
+
// 不同类(或状态半同步):**原地替换** —— 不动 waitShown、不置空 whaleSysItem、不往队列塞,
|
|
13729
|
+
// 直接复用 sceneOpen 的内容淡切把这一条常驻泡泡换成新的挂起内容。
|
|
13730
|
+
whaleSysQueue = whaleSysQueue.filter(function (q) { return !(q && q.kind === 'wait') })
|
|
13731
|
+
whaleSysItem = { kind: 'wait', waitKind: k, rank: 4, ttlMs: 0, pendingId: pid }
|
|
13732
|
+
sceneOpen('wait', function () { bubbleRenderWaitMods(k) }, 0)
|
|
13733
|
+
return true
|
|
13734
|
+
}
|
|
13735
|
+
// 还没在显示等待泡泡(可能正在放消耗/预警,或压根没泡泡):队列里只保留"最新一条"等待项
|
|
13736
|
+
whaleSysQueue = whaleSysQueue.filter(function (q) { return !(q && q.kind === 'wait') })
|
|
13737
|
+
// rank 4:1 今日预算 / 2 余额预警 / 3 每轮消耗 / 4 等待交互。挂在消耗之后是刻意的 ——
|
|
13738
|
+
// 等待泡泡一旦显示就**常驻**(不会自己让位),所以必须让"来了就要看、会自己消失"的消耗泡泡先走完;
|
|
13739
|
+
// 否则一次消耗提醒会被常驻的等待泡泡永久挡在队里。既有 30ms 合批排序逻辑不用改。
|
|
13740
|
+
return whaleSysPush({ kind: 'wait', waitKind: k, rank: 4, ttlMs: 0, pendingId: pid })
|
|
13741
|
+
} catch (err) { return false }
|
|
13742
|
+
}
|
|
13743
|
+
// 挂起解除(被回答 / 被批准 / 切换对话):收起等待泡泡。
|
|
13744
|
+
// 队列里还有别的项时保持打开、淡切到下一项 —— 与 hideCostBubble / hideUsageAlertBubble 同语义,
|
|
13745
|
+
// 复用它们的做法:**不**把正在显示的那一项摘掉,交给 whaleSysSwapNext() 去换或去清场。
|
|
13746
|
+
function hideWaitBubble() {
|
|
13747
|
+
try {
|
|
13748
|
+
// v771:判断"当前是不是正显示等待泡泡"必须**同时看场景与队列项**。历史上只看 whaleSysItem,
|
|
13749
|
+
// 一旦它与场景不同步(showWaitBubble 换类型的旧实现就会造成)就永远走不到关闭分支 ⇒ 泡泡卡死。
|
|
13750
|
+
var showingWait = !!((bubbleScene && bubbleScene.kind === 'wait') || (whaleSysItem && whaleSysItem.kind === 'wait') || waitShown)
|
|
13751
|
+
var beforeLen = whaleSysQueue.length
|
|
13752
|
+
// 排队中但还没轮到的等待项作废(等待项永远只保留最新一条,见 showWaitBubble)
|
|
13753
|
+
whaleSysQueue = whaleSysQueue.filter(function (q) { return !(q && q.kind === 'wait') })
|
|
13754
|
+
if (showingWait) {
|
|
13755
|
+
if (whaleSysSwapNext()) return // 队列里还有别的项 → 淡切过去(waitShown 由渲染侧重算)
|
|
13756
|
+
waitShown = false
|
|
13757
|
+
whaleSysItem = null
|
|
13758
|
+
bubbleClearAll()
|
|
13759
|
+
bubbleScene = null
|
|
13760
|
+
bubbleShown = false
|
|
13761
|
+
bubbleRandomActive = false
|
|
13762
|
+
bubbleRandomLines = null
|
|
13763
|
+
bubbleLiveMods = null
|
|
13764
|
+
bubbleCloseVisual()
|
|
13765
|
+
whaleSysDone()
|
|
13766
|
+
return
|
|
13767
|
+
}
|
|
13768
|
+
// 没在显示等待泡泡:只清掉了排队中的等待项 → 补一次 tick,让别的项继续走
|
|
13769
|
+
if (whaleSysQueue.length !== beforeLen) whaleSysTick()
|
|
13770
|
+
} catch (err) {}
|
|
13771
|
+
}
|
|
12512
13772
|
function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v) }
|
|
12513
13773
|
function viewport() {
|
|
12514
13774
|
return {
|
|
@@ -15501,6 +16761,56 @@ try {
|
|
|
15501
16761
|
var lastCostStored = Number(localStorage.getItem('dshw-last-seq') || 0)
|
|
15502
16762
|
if (isFinite(lastCostStored) && lastCostStored >= 0) lastCostSeq = lastCostStored
|
|
15503
16763
|
} catch (err) {}
|
|
16764
|
+
// v761(issue #161 / 全局音效设置):**等待用户交互**的轮询。
|
|
16765
|
+
// 并入既有的每秒周期(不新增 interval);宿主 wait.json 给出「有没有挂起 / 哪一类 / 对话名」。这里负责:
|
|
16766
|
+
// ① 记住对话名,供提示内容里的 {session} 占位符使用;
|
|
16767
|
+
// ② 挂起**首次出现**时播对应音效(提问音 / 授权音),并在开启「冒泡提示」时弹泡泡;
|
|
16768
|
+
// ③ 用 localStorage 记住已响过的挂起 id ⇒ **刷新页面不会为同一个未回答的提问重复响**。
|
|
16769
|
+
var WAIT_URL = '/dsh-whale/wait.json'
|
|
16770
|
+
var WAIT_SOUND_KEY = 'dshw-wait-sound'
|
|
16771
|
+
var waitSeenId = ''
|
|
16772
|
+
function waitEventCfg(kind) {
|
|
16773
|
+
try { return ((usageSet || {}).events || {})[kind] || {} } catch (err) { return {} }
|
|
16774
|
+
}
|
|
16775
|
+
function pollWaitState() {
|
|
16776
|
+
try {
|
|
16777
|
+
fetch(WAIT_URL, { cache: 'no-store' })
|
|
16778
|
+
.then(function (r) { return r.json() })
|
|
16779
|
+
.then(function (d) {
|
|
16780
|
+
if (!d || !d.ok) return
|
|
16781
|
+
if (typeof d.sessionName === 'string') waitSessionName = d.sessionName
|
|
16782
|
+
var p = d.pending || null
|
|
16783
|
+
if (!p || !p.kind) {
|
|
16784
|
+
// 挂起已解除:收起泡泡,并允许下一次挂起照常响
|
|
16785
|
+
waitSeenId = ''
|
|
16786
|
+
if (typeof hideWaitBubble === 'function') hideWaitBubble()
|
|
16787
|
+
return
|
|
16788
|
+
}
|
|
16789
|
+
var kind = p.kind === 'approval' ? 'approval' : 'question'
|
|
16790
|
+
var ev = waitEventCfg(kind)
|
|
16791
|
+
// v771:**每秒都把泡泡与宿主的挂起状态对齐一次**(showWaitBubble 幂等:同类直接返回 false,
|
|
16792
|
+
// 不会重渲染)。旧实现只在"新 id"时才调显示,于是一旦出现瞬时不同步(换类型 / 泡泡被别的
|
|
16793
|
+
// 场景顶掉 / 某次渲染失败),泡泡就会卡在那个状态再也回不来 —— 这正是真机那次卡死的另一半原因。
|
|
16794
|
+
// 现在任何不同步都会在 1 秒内自愈:该换内容就换、该收就收(收在下面 !p 分支)。
|
|
16795
|
+
// 门控照旧:该事件的「冒泡提示」关掉就不冒泡(与声音各自独立判)。
|
|
16796
|
+
if (ev.bubbleOn !== false && typeof showWaitBubble === 'function') showWaitBubble(kind, p)
|
|
16797
|
+
// 声音:同一个未回答的挂起只响一次(刷新页面也不重复响)
|
|
16798
|
+
var key = kind + ':' + String(p.id || '')
|
|
16799
|
+
var remembered = ''
|
|
16800
|
+
try { remembered = localStorage.getItem(WAIT_SOUND_KEY) || '' } catch (err) {}
|
|
16801
|
+
if (key === waitSeenId || key === remembered) return
|
|
16802
|
+
waitSeenId = key
|
|
16803
|
+
try { localStorage.setItem(WAIT_SOUND_KEY, key) } catch (err) {}
|
|
16804
|
+
var on = ev.on !== false
|
|
16805
|
+
var sel = ev.sel || ''
|
|
16806
|
+
// v762:音效行新增的「是否播这个音效」开关(events.<kind>.soundOn,缺省视为开)。
|
|
16807
|
+
// 门控 = 该事件总开关 on + 选了音效 sel(下拉已无「静音」,正常不为空)+ soundOn。
|
|
16808
|
+
// 任务结束音的播放判定不在这里,仍读既有的 usageSet.taskEnd.on(见 playTaskEndSound)。
|
|
16809
|
+
if (on && sel && ev.soundOn !== false) playBindingSound({ on: true, sel: sel }, ev)
|
|
16810
|
+
})
|
|
16811
|
+
.catch(function () {})
|
|
16812
|
+
} catch (err) {}
|
|
16813
|
+
}
|
|
15504
16814
|
function pollLastTurn() {
|
|
15505
16815
|
try {
|
|
15506
16816
|
fetch(LAST_TURN_URL, { cache: 'no-store' })
|
|
@@ -15536,7 +16846,7 @@ function pollLastTurn() {
|
|
|
15536
16846
|
.catch(function () {})
|
|
15537
16847
|
} catch (err) {}
|
|
15538
16848
|
}
|
|
15539
|
-
setInterval(pollLastTurn, 1000)
|
|
16849
|
+
setInterval(function () { pollLastTurn(); pollWaitState() }, 1000)
|
|
15540
16850
|
}
|
|
15541
16851
|
// 主界面检测通过(或稍后由 MutationObserver 检测到)后执行挂件初始化;非主界面不启动
|
|
15542
16852
|
try { dshwTryStart(true) } catch (err) {}
|