dsh-whale-widget 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,8 +18,17 @@ function dshwIsChatRoot(r) {
18
18
  return !!(
19
19
  r.querySelector('textarea') ||
20
20
  r.querySelector('[contenteditable="true"]') ||
21
+ // DSH 0.1.6-alpha.1 起的新版 composer:data-composer-input 在整个 DSH 前端产物里
22
+ // **只出现在对话组件**(v742 时实测:1515 个前端文件里仅 dsh-client-ui-conversation 命中),
23
+ // 所以它是最精确的判据。
21
24
  r.querySelector('[data-composer-input]') ||
22
- r.querySelector('[role="textbox"]')
25
+ // 同一层的 composer 容器属性(data-composer-seat / -card 也各只有一个出处),
26
+ // 万一将来输入框本身的属性改名,容器还在就仍能识别。
27
+ r.querySelector('[data-composer-seat],[data-composer-card]') ||
28
+ // 通用兜底:多行 textbox。注意**不要**用裸的 [role="textbox"] —— pdf.js 的批注编辑器
29
+ // (dsh-client-ui-sidebar-documentpreview)也会设 role=textbox,用它会把判定放宽到
30
+ // 非输入框的元素;加上 aria-multiline 至少排除掉单行输入框。
31
+ r.querySelector('[role="textbox"][aria-multiline="true"]')
23
32
  )
24
33
  }
25
34
  var dshwStarted = false
@@ -690,6 +699,38 @@ styleEl.setAttribute('data-plugin', 'dsh-whale-widget')
690
699
  styleEl.textContent = css
691
700
  document.head.appendChild(styleEl)
692
701
 
702
+ // ===== v743:body 挂载登记器(DOM 守护的基础设施,见下面 dshwReattachRoot)=====
703
+ // 挂件会把 30 多个节点挂到 document.body 上(主节点、菜单、各种遮罩/面板、隐藏的 file input…)。
704
+ // 早先的守护只补挂 root + menuBox:SPA 切路由或别的插件整体替换 body 子树后,其余节点会变成
705
+ // "存在但不在文档里"的孤儿 —— 界面看起来恢复了,可一旦点开对应功能就静默失效。
706
+ // 所以这里统一登记:所有挂到 body 的节点都走 dshwBodyAppend(),守护时逐个补挂;
707
+ // 主动移除(目前只有一处)走 dshwBodyDetach(),避免被守护逻辑"复活"。
708
+ var dshwBodyNodes = []
709
+ function dshwBodyAppend(el) {
710
+ try {
711
+ if (!el) return el
712
+ // 注意:这里必须是**原始**的 document.body.appendChild —— 不能走 dshwBodyAppend 自己
713
+ // (v743 批量改写时曾误替换成自我递归,被 try/catch 吞掉后表现为"登记了但从未挂上")
714
+ document.body.appendChild(el)
715
+ if (dshwBodyNodes.indexOf(el) < 0) dshwBodyNodes.push(el)
716
+ } catch (err) {}
717
+ return el
718
+ }
719
+ function dshwBodyDetach(el) {
720
+ try {
721
+ var i = dshwBodyNodes.indexOf(el)
722
+ if (i >= 0) dshwBodyNodes.splice(i, 1)
723
+ if (el && el.parentNode) el.parentNode.removeChild(el)
724
+ } catch (err) {}
725
+ }
726
+ // 兼容性:老 WebView 可能没有 Element.isConnected(Chrome 51+ 才有)。
727
+ // 若直接 `!el.isConnected`,在那种环境里会恒为 true → 每个 DOM 变更批次都会重复 appendChild。
728
+ function dshwConnected(el) {
729
+ if (!el) return false
730
+ try { if (typeof el.isConnected === 'boolean') return el.isConnected } catch (err) {}
731
+ try { return document.documentElement.contains(el) } catch (err) { return true }
732
+ }
733
+
693
734
  var root = document.createElement('div')
694
735
  root.className = 'dshwv-root'
695
736
 
@@ -774,7 +815,7 @@ audioImportBtn.textContent = '导入'
774
815
  audioImportBtn.title = '新建/编辑音效组'
775
816
  audioGroupBtn.addEventListener('click', function (e) { e.stopPropagation(); toggleAudioGroupPanel() })
776
817
  audioImportBtn.addEventListener('click', function (e) { e.stopPropagation(); openAudioGroupEditor(null) })
777
- document.body.appendChild(audioGroupPanel)
818
+ dshwBodyAppend(audioGroupPanel)
778
819
  function soundOpt(value, label) {
779
820
  var o = document.createElement('option')
780
821
  o.value = value
@@ -910,7 +951,7 @@ function dshwCustSel(sel, opts) {
910
951
  dshwCustSelClose()
911
952
  fill()
912
953
  sync()
913
- if (menu.parentNode !== document.body) document.body.appendChild(menu)
954
+ if (menu.parentNode !== document.body) dshwBodyAppend(menu)
914
955
  dshwDropOpen(menu, btn)
915
956
  // 可选底部参照元素:展开高度不超过该元素的上缘(内部滚动),用于
916
957
  // 主菜单内“任务结束音效”等下拉,避免列表盖住底部「小鲸鱼记账」按钮
@@ -1991,7 +2032,7 @@ function usageAlertBudgetEditor(key, onSave) {
1991
2032
  mask.appendChild(card)
1992
2033
  mask.addEventListener('click', function (e) { if (e.target === mask) cleanup() })
1993
2034
  window.__dshwRemindMask = mask
1994
- document.body.appendChild(mask)
2035
+ dshwBodyAppend(mask)
1995
2036
  renderBubblePal()
1996
2037
  renderBubblePv()
1997
2038
  } catch (err) {}
@@ -2563,7 +2604,7 @@ function openApiModelPanel(modelId) {
2563
2604
  } catch (err) {}
2564
2605
  mask.appendChild(card)
2565
2606
  mask.addEventListener('click', function (e) { if (e.target === mask) closeApiModelPanel() })
2566
- document.body.appendChild(mask)
2607
+ dshwBodyAppend(mask)
2567
2608
  apiModelMaskEl = mask
2568
2609
  // 模板切换:把模板默认值填进各字段(仅新增态)
2569
2610
  if (isNew) {
@@ -2914,7 +2955,7 @@ function openModelQuotaEditor(modelId) {
2914
2955
  card.appendChild(btns)
2915
2956
  mask.appendChild(card)
2916
2957
  mask.addEventListener('click', function (e) { if (e.target === mask) closeApiModelPanel() })
2917
- document.body.appendChild(mask)
2958
+ dshwBodyAppend(mask)
2918
2959
  apiModelMaskEl = mask
2919
2960
  } catch (err) {}
2920
2961
  }
@@ -3102,7 +3143,7 @@ function openApiModelMenu(modelId) {
3102
3143
  card.appendChild(btns)
3103
3144
  mask.appendChild(card)
3104
3145
  mask.addEventListener('click', function (e) { if (e.target === mask) closeApiModelPanel() })
3105
- document.body.appendChild(mask)
3146
+ dshwBodyAppend(mask)
3106
3147
  apiModelMaskEl = mask
3107
3148
  } catch (err) {}
3108
3149
  }
@@ -3260,7 +3301,8 @@ function openBalanceAdjustment(modelId) {
3260
3301
  function close() {
3261
3302
  if (busy) return
3262
3303
  document.removeEventListener('keydown', keyHandler)
3263
- mask.remove()
3304
+ // v743:主动移除要走 dshwBodyDetach —— 否则它已被登记,DOM 守护会把这个刚关掉的窗口"复活"
3305
+ dshwBodyDetach(mask)
3264
3306
  accountingMask = null
3265
3307
  // 关掉校正窗口后回到同一个设置菜单,并把焦点放回「校正」按钮
3266
3308
  openApiModelMenu(modelId)
@@ -3293,7 +3335,7 @@ function openBalanceAdjustment(modelId) {
3293
3335
  card.appendChild(actions)
3294
3336
  mask.appendChild(card)
3295
3337
  mask.addEventListener('click', function (e) { if (e.target === mask) close() })
3296
- document.body.appendChild(mask)
3338
+ dshwBodyAppend(mask)
3297
3339
  accountingMask = mask
3298
3340
  document.addEventListener('keydown', keyHandler)
3299
3341
  var rows = []
@@ -3489,7 +3531,7 @@ var usageMoreCard = document.createElement('div')
3489
3531
  usageMoreCard.className = 'dshwv-usage-card'
3490
3532
  usageMoreMask.appendChild(usageMoreCard)
3491
3533
  usageMoreMask.addEventListener('click', function (e) { if (e.target === usageMoreMask) closeUsageRecordsWindow() })
3492
- document.body.appendChild(usageMoreMask)
3534
+ dshwBodyAppend(usageMoreMask)
3493
3535
  function openUsageRecordsWindow() {
3494
3536
  usageMoreCard.innerHTML = '<div style="padding:10px;color:#203170">加载中…</div>'
3495
3537
  usageMoreMask.style.display = 'flex'
@@ -3512,7 +3554,7 @@ function resMaskOpen() {
3512
3554
  resCardEl.className = 'dshwv-usage-card dshwv-rescard'
3513
3555
  resMaskEl.appendChild(resCardEl)
3514
3556
  resMaskEl.addEventListener('click', function (e) { if (e.target === resMaskEl) resManagerClose() })
3515
- document.body.appendChild(resMaskEl)
3557
+ dshwBodyAppend(resMaskEl)
3516
3558
  }
3517
3559
  resManagerRender()
3518
3560
  resMaskEl.style.display = 'flex'
@@ -4112,7 +4154,7 @@ function usagePopupCard(title, content, below, amount) {
4112
4154
  card.appendChild(btns)
4113
4155
  mask.appendChild(card)
4114
4156
  mask.addEventListener('click', function (e) { if (e.target === mask) { try { document.body.removeChild(mask) } catch (err) {} } })
4115
- document.body.appendChild(mask)
4157
+ dshwBodyAppend(mask)
4116
4158
  } catch (err) {}
4117
4159
  }
4118
4160
  var USAGE_PALETTE = ['#203170', '#e0433f', '#2fa24c', '#b060c8', '#e89a2e', '#3aa6c8', '#d06a8a', '#7a8b2f', '#6a6ad0', '#c84a8a']
@@ -4617,7 +4659,7 @@ function fillUsageRecordsWindow(d) {
4617
4659
  renderGroups('')
4618
4660
  })
4619
4661
  }
4620
- document.body.appendChild(rolePanel)
4662
+ dshwBodyAppend(rolePanel)
4621
4663
 
4622
4664
  // —— 吸附与翻转自定义弹窗 ——
4623
4665
  // 预览方框内五条可拖线:左/右/上/下四条吸附区边界 + 红色翻转线(竖中线位置)。
@@ -5034,7 +5076,7 @@ snapBtns.appendChild(snapBtn('重置', 'dshwv-snapbtn-no', resetSnapEdit))
5034
5076
  snapBtns.appendChild(snapBtn('确认', 'dshwv-snapbtn-ok', function () { closeSnapModal(true) }))
5035
5077
  snapCard.appendChild(snapBtns)
5036
5078
  snapMask.appendChild(snapCard)
5037
- document.body.appendChild(snapMask)
5079
+ dshwBodyAppend(snapMask)
5038
5080
 
5039
5081
  // —— 自定义泡泡:主编辑窗口(点击队列) + 单泡模块编辑 + 模块编辑 ——
5040
5082
  var bubbleMask = null
@@ -6890,7 +6932,7 @@ function qeditEnsure() {
6890
6932
  qeditEl = document.createElement('div')
6891
6933
  qeditEl.className = 'dshwv-qedit'
6892
6934
  qeditEl.style.display = 'none'
6893
- document.body.appendChild(qeditEl)
6935
+ dshwBodyAppend(qeditEl)
6894
6936
  if (!window.__dshwQeditBound) {
6895
6937
  window.__dshwQeditBound = true
6896
6938
  document.addEventListener('pointerdown', function (e) {
@@ -8212,7 +8254,7 @@ bubbleBtns.appendChild(bubbleBtn('重置', 'dshwv-bubbtn-no', bubbleEditorReset)
8212
8254
  bubbleBtns.appendChild(bubbleBtn('保存', 'dshwv-bubbtn-ok', bubbleEditorSave))
8213
8255
  bubbleCard.appendChild(bubbleBtns)
8214
8256
  bubbleMask.appendChild(bubbleCard)
8215
- document.body.appendChild(bubbleMask)
8257
+ dshwBodyAppend(bubbleMask)
8216
8258
 
8217
8259
  // ===== W2 单泡编辑窗口 =====
8218
8260
  bubbleItemMask = document.createElement('div')
@@ -8296,7 +8338,7 @@ bubbleItemBtns.appendChild(bubbleBtn('恢复默认', 'dshwv-bubbtn-no', bubbleIt
8296
8338
  bubbleItemBtns.appendChild(bubbleBtn('保存', 'dshwv-bubbtn-ok', bubbleItemSave))
8297
8339
  bubbleItemCard.appendChild(bubbleItemBtns)
8298
8340
  bubbleItemMask.appendChild(bubbleItemCard)
8299
- document.body.appendChild(bubbleItemMask)
8341
+ dshwBodyAppend(bubbleItemMask)
8300
8342
 
8301
8343
  // ===== W3 模块编辑器(文本/随机/图片 + 样式) =====
8302
8344
  var moduleMask = null
@@ -8432,7 +8474,7 @@ function whaleZClean() {
8432
8474
  // 菜单宽度与触发按钮保持一致(原样式 min-width:100% 在 body 下会按视口撑满,须归零)
8433
8475
  function dshwDropOpen(menuEl, anchorEl) {
8434
8476
  try {
8435
- if (menuEl.parentNode !== document.body) document.body.appendChild(menuEl)
8477
+ if (menuEl.parentNode !== document.body) dshwBodyAppend(menuEl)
8436
8478
  menuEl.style.position = 'fixed'
8437
8479
  menuEl.style.minWidth = '0px'
8438
8480
  menuEl.style.left = '0px'
@@ -9483,7 +9525,7 @@ moduleBtns.appendChild(saveAsBtn)
9483
9525
  moduleBtns.appendChild(bubbleBtn('保存', 'dshwv-bubbtn-ok', function () { closeModuleEditor(true) }))
9484
9526
  moduleCard.appendChild(moduleBtns)
9485
9527
  moduleMask.appendChild(moduleCard)
9486
- document.body.appendChild(moduleMask)
9528
+ dshwBodyAppend(moduleMask)
9487
9529
  // 存为可选模块:名称输入弹窗
9488
9530
  var moduleNamePromptMask = document.createElement('div')
9489
9531
  moduleNamePromptMask.className = 'dshwv-confirmmask'
@@ -9516,7 +9558,7 @@ moduleNameBtns.appendChild(moduleNameCancel)
9516
9558
  moduleNameBtns.appendChild(moduleNameOk)
9517
9559
  moduleNamePromptCard.appendChild(moduleNameBtns)
9518
9560
  moduleNamePromptMask.appendChild(moduleNamePromptCard)
9519
- document.body.appendChild(moduleNamePromptMask)
9561
+ dshwBodyAppend(moduleNamePromptMask)
9520
9562
  // 回车保存/关闭
9521
9563
  moduleNameInput.addEventListener('keydown', function (e) {
9522
9564
  try {
@@ -9629,7 +9671,7 @@ cropCard.appendChild(cropZoomWrap)
9629
9671
  cropCard.appendChild(cropAngleWrap)
9630
9672
  cropCard.appendChild(cropBtns)
9631
9673
  cropMask.appendChild(cropCard)
9632
- document.body.appendChild(cropMask)
9674
+ dshwBodyAppend(cropMask)
9633
9675
  cropBox.addEventListener('pointerdown', onCropDown)
9634
9676
  cropBox.addEventListener('pointermove', onCropMove)
9635
9677
  cropBox.addEventListener('pointerup', onCropUp)
@@ -9729,7 +9771,7 @@ gifCard.appendChild(gifHint)
9729
9771
  gifCard.appendChild(gifNameInput)
9730
9772
  gifCard.appendChild(gifBtns)
9731
9773
  gifMask.appendChild(gifCard)
9732
- document.body.appendChild(gifMask)
9774
+ dshwBodyAppend(gifMask)
9733
9775
  gifCancelBtn.addEventListener('click', hideGifRoleModal)
9734
9776
  gifOkBtn.addEventListener('click', confirmGifRole)
9735
9777
  var gifRoleDataUrl = null
@@ -9810,7 +9852,7 @@ confirmBtns.appendChild(confirmYesBtn)
9810
9852
  confirmCard.appendChild(confirmText)
9811
9853
  confirmCard.appendChild(confirmBtns)
9812
9854
  confirmMask.appendChild(confirmCard)
9813
- document.body.appendChild(confirmMask)
9855
+ dshwBodyAppend(confirmMask)
9814
9856
  confirmNoBtn.addEventListener('click', hideConfirm)
9815
9857
  confirmYesBtn.addEventListener('click', function () {
9816
9858
  var cb = confirmCb
@@ -9902,7 +9944,7 @@ audioEditCard.appendChild(audioEditPressRow)
9902
9944
  audioEditCard.appendChild(audioEditReleaseRow)
9903
9945
  audioEditCard.appendChild(audioEditBtns)
9904
9946
  audioEditMask.appendChild(audioEditCard)
9905
- document.body.appendChild(audioEditMask)
9947
+ dshwBodyAppend(audioEditMask)
9906
9948
  audioEditCancel.addEventListener('click', hideAudioEditor)
9907
9949
  audioEditPlay.addEventListener('pointerdown', function (e) { e.stopPropagation(); audioEditPreviewDown() })
9908
9950
  audioEditPlay.addEventListener('pointerup', function (e) { e.stopPropagation(); audioEditPreviewUp() })
@@ -10055,7 +10097,7 @@ audioCropCard.appendChild(audioCropTime)
10055
10097
  audioCropCard.appendChild(audioCropNameRow)
10056
10098
  audioCropCard.appendChild(audioCropBtns)
10057
10099
  audioCropMask.appendChild(audioCropCard)
10058
- document.body.appendChild(audioCropMask)
10100
+ dshwBodyAppend(audioCropMask)
10059
10101
  audioCropCancel.addEventListener('click', hideAudioCrop)
10060
10102
  audioCropOk.addEventListener('click', confirmAudioCrop)
10061
10103
  audioCropPlay.addEventListener('click', previewAudioCrop)
@@ -10126,8 +10168,8 @@ body.appendChild(img)
10126
10168
  body.appendChild(bubbleBox)
10127
10169
  root.appendChild(body)
10128
10170
  root.appendChild(menuBtn)
10129
- document.body.appendChild(root)
10130
- document.body.appendChild(menuBox)
10171
+ dshwBodyAppend(root)
10172
+ dshwBodyAppend(menuBox)
10131
10173
 
10132
10174
  // ===== PR #105 后半:DOM 守护(SPA 切路由 / 别的插件替换 body 子树时把节点摘掉)=====
10133
10175
  // 背景:DSH 是 SPA,切到会话列表 / 设置 / 插件市场再回来、或其它客户端插件整体替换
@@ -10135,19 +10177,29 @@ document.body.appendChild(menuBox)
10135
10177
  // 做法:暴露 window.__dshWhaleRoot 供外部定位/调试,并用一个 MutationObserver 盯着;
10136
10178
  // 一旦发现节点已不在文档里就**把同一个节点补挂回 body**(不重建、不重新初始化,
10137
10179
  // 位置/设置/状态全部保留)。
10180
+ // v743:补挂范围从 root+menuBox 扩到**全部登记过的 body 节点**(见 dshwBodyNodes)——
10181
+ // 否则被整体替换后,遮罩/面板/隐藏 file input 会变成孤儿,功能静默失效;
10182
+ // 另外 root 还在、只有个别节点被摘掉的"局部移除"也要能自愈,所以做了节流的全量核对。
10138
10183
  try { window.__dshWhaleRoot = root } catch (err) {}
10139
10184
  function dshwReattachRoot() {
10140
10185
  try {
10141
- if (!root || root.isConnected) return
10142
- document.body.appendChild(root)
10143
- // 菜单是独立挂在 body 上的浮层:它也被摘掉且当前正打开时一并补回,否则菜单会"消失"
10144
- if (menuBox && menuOpen && !menuBox.isConnected) document.body.appendChild(menuBox)
10186
+ for (var i = 0; i < dshwBodyNodes.length; i++) {
10187
+ var el = dshwBodyNodes[i]
10188
+ if (el && !dshwConnected(el)) dshwBodyAppend(el)
10189
+ }
10145
10190
  } catch (err) {}
10146
10191
  }
10147
10192
  try {
10148
10193
  if (typeof MutationObserver === 'function') {
10194
+ var dshwGuardLastFull = 0
10149
10195
  var dshwRootGuard = new MutationObserver(function () {
10150
- if (root && !root.isConnected) dshwReattachRoot()
10196
+ try {
10197
+ if (!root) return
10198
+ if (!dshwConnected(root)) { dshwReattachRoot(); dshwGuardLastFull = Date.now(); return }
10199
+ // root 正常时也定期全量核对一次(最多 1.5 秒一次):覆盖"只有个别浮层被摘掉"的情况
10200
+ var now = Date.now()
10201
+ if (now - dshwGuardLastFull > 1500) { dshwGuardLastFull = now; dshwReattachRoot() }
10202
+ } catch (err) {}
10151
10203
  })
10152
10204
  dshwRootGuard.observe(document.documentElement, { childList: true, subtree: true })
10153
10205
  }
@@ -11236,7 +11288,7 @@ function bubbleTplHelpToggle(m, anchor) {
11236
11288
  if (!dshwvTplHelpEl) {
11237
11289
  dshwvTplHelpEl = document.createElement('div')
11238
11290
  dshwvTplHelpEl.className = 'dshwv-tplhelp'
11239
- document.body.appendChild(dshwvTplHelpEl)
11291
+ dshwBodyAppend(dshwvTplHelpEl)
11240
11292
  document.addEventListener('pointerdown', function (e) {
11241
11293
  if (!dshwvTplHelpEl || dshwvTplHelpEl.style.display === 'none') return
11242
11294
  try {
@@ -11280,7 +11332,7 @@ function dshwvHintEnsure() {
11280
11332
  dshwvHintEl = document.createElement('div')
11281
11333
  dshwvHintEl.className = 'dshwv-tplhelp dshwv-hintbox'
11282
11334
  dshwvHintEl.style.display = 'none'
11283
- document.body.appendChild(dshwvHintEl)
11335
+ dshwBodyAppend(dshwvHintEl)
11284
11336
  document.addEventListener('pointerdown', function (e) {
11285
11337
  if (!dshwvHintEl || dshwvHintEl.style.display === 'none') return
11286
11338
  try {
@@ -12420,7 +12472,7 @@ function dshwvToast(msg) {
12420
12472
  'max-width:min(560px,calc(100vw - 32px));box-sizing:border-box;padding:10px 14px;border-radius:10px;' +
12421
12473
  'background:#8a1f1f;color:#fff;font-size:13px;line-height:1.6;box-shadow:0 6px 20px rgba(0,0,0,.28);' +
12422
12474
  'pointer-events:none;text-align:center'
12423
- document.body.appendChild(el)
12475
+ dshwBodyAppend(el)
12424
12476
  dshwvToastEl = el
12425
12477
  }
12426
12478
  dshwvToastEl.innerHTML = msg
@@ -12459,12 +12511,15 @@ function configPut(payload, retried) {
12459
12511
  }
12460
12512
  function saveConfig() {
12461
12513
  // issue #97:加载完成前只记待办,绝不 PUT(否则把默认值整包写进服务端)
12462
- if (!configLoaded) { configSavePending = true; return }
12514
+ if (!configLoaded) { configSavePending = true; return null }
12463
12515
  try {
12464
- configPut(configPayload(), false)
12516
+ // 返回 Promise(v743:Codex 统计开关需要"等服务端确认后再刷新")。
12517
+ // configPut 自带重试与失败提示、且链尾有 catch,所以不会有 unhandled rejection。
12518
+ var p = configPut(configPayload(), false)
12465
12519
  // 锚点位置记忆:记录相对边框的离边距离,窗口 resize 后保持(localStorage)。
12466
12520
  saveAnchorPos()
12467
- } catch (err) {}
12521
+ return p
12522
+ } catch (err) { return null }
12468
12523
  }
12469
12524
  // 单独抽出:只写 localStorage 锚点(不碰尺寸设置);applyAnchorPos / settle 自愈时也要用。
12470
12525
  // v:2 = 净距离格式(剥离避让距离),v:1 旧格式含避让距离,恢复时废弃旧格式。
@@ -12572,8 +12627,12 @@ function setMenuBtnHide(v) {
12572
12627
  function setCodexStatsOn(v) {
12573
12628
  codexStatsOn = v !== false
12574
12629
  if (codexStatsToggle) codexStatsToggle.checked = codexStatsOn
12575
- saveConfig()
12576
- try { refreshModelList() } catch (err) {}
12630
+ var after = function () { try { refreshModelList() } catch (err) {} }
12631
+ var p = null
12632
+ try { p = saveConfig() } catch (err) {}
12633
+ // 等服务端确认落盘后再重取模型列表:否则可能读到旧配置,那一行会先显示上一次的状态再跳变
12634
+ if (p && typeof p.then === 'function') p.then(after, after)
12635
+ else after()
12577
12636
  }
12578
12637
  function scaleToDisplay(s) {
12579
12638
  return Math.round((s - MIN_SCALE) / ((MAX_SCALE - MIN_SCALE) / 19)) + 1
@@ -13819,7 +13878,7 @@ var audioCropFileInput = document.createElement('input')
13819
13878
  audioCropFileInput.type = 'file'
13820
13879
  audioCropFileInput.accept = 'audio/*'
13821
13880
  audioCropFileInput.style.display = 'none'
13822
- document.body.appendChild(audioCropFileInput)
13881
+ dshwBodyAppend(audioCropFileInput)
13823
13882
  var audioCropTarget = null // 'press' | 'release'
13824
13883
  var audioCropCtx = null // AudioContext
13825
13884
  var audioCropBuffer = null // AudioBuffer
package/lib/index.js CHANGED
@@ -929,7 +929,7 @@ export default {
929
929
  const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
930
930
  for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
931
931
  return {
932
- ok: true, version: '0.3.6', today: todayData, days7,
932
+ ok: true, version: '0.3.7', today: todayData, days7,
933
933
  total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
934
934
  all: {
935
935
  days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
@@ -1312,20 +1312,30 @@ export default {
1312
1312
  let codexCatchupT = null
1313
1313
  // 配置变更(例如用户在设置里关掉 Codex 统计)后必须让快照失效,
1314
1314
  // 否则下一次请求还会命中关闭前的旧快照、开关看起来"没生效"。
1315
- function codexInvalidate() { codexSnap = null; codexSnapAt = 0 }
1315
+ function codexInvalidate() { codexSnap = null; codexSnapAt = 0; codexStatsOnV = null; codexStatsOnAt = 0 }
1316
+ // 开关读取加 2 秒缓存:codexBackgroundRefresh 可能被高频调用(快照过期时每次 payload
1317
+ // 构建都会试一次),而 readSizeConfig() 是同步文件读 —— 不能放在热路径上。
1318
+ // 用户改设置时 codexInvalidate() 会清掉缓存,所以开关仍然是"立刻生效"。
1319
+ let codexStatsOnV = null, codexStatsOnAt = 0
1316
1320
  function codexStatsOn() {
1321
+ const now = Date.now()
1322
+ if (codexStatsOnV !== null && now - codexStatsOnAt < 2000) return codexStatsOnV
1323
+ let v = true
1317
1324
  try {
1318
1325
  const cfg = readSizeConfig()
1319
- return !cfg || cfg.codexStatsOn !== false
1320
- } catch (err) { return true }
1326
+ v = !cfg || cfg.codexStatsOn !== false
1327
+ } catch (err) { v = true }
1328
+ codexStatsOnV = v
1329
+ codexStatsOnAt = now
1330
+ return v
1321
1331
  }
1322
1332
  function codexBackgroundRefresh(delayMs) {
1333
+ if (codexScanP) return codexScanP // 已在扫:先返回,避免重复读配置/重复排队
1323
1334
  if (!codexStatsOn()) {
1324
1335
  codexSnap = { ok: false, disabled: true, error: 'Codex 统计已在设置里关闭' }
1325
1336
  codexSnapAt = Date.now()
1326
1337
  return null
1327
1338
  }
1328
- if (codexScanP) return codexScanP
1329
1339
  if (delayMs > 0) {
1330
1340
  if (codexCatchupT) return null
1331
1341
  codexCatchupT = setTimeout(() => { codexCatchupT = null; codexBackgroundRefresh(0) }, delayMs)
@@ -2094,7 +2104,7 @@ export default {
2094
2104
  const led = readUsageLedger()
2095
2105
  const summary = daySummary(led, todayKey())
2096
2106
  return {
2097
- ...visible, version: '0.3.6', isPeak: isPeakTime(Math.floor(Date.now() / 1000)),
2107
+ ...visible, version: '0.3.7', isPeak: isPeakTime(Math.floor(Date.now() / 1000)),
2098
2108
  todayUsage: summary.amount, todayUsageCurrency: summary.currency,
2099
2109
  usageSource: summary.source, usageLabel: summary.label, usageMode: 'ledger',
2100
2110
  accounting: balanceSummary(led, todayKey()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-widget",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义",
5
5
  "keywords": [
6
6
  "dsh",