dsh-whale-widget 0.3.6 → 0.3.8
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/assets/whale-widget.js +343 -108
- package/lib/index.js +78 -8
- package/package.json +1 -1
package/assets/whale-widget.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -70,21 +79,34 @@ function dshwInit() {
|
|
|
70
79
|
if (window.__dshWhaleInit) return
|
|
71
80
|
window.__dshWhaleInit = true
|
|
72
81
|
|
|
73
|
-
// =====
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
82
|
+
// ===== 音效播放(v745:Web Audio + 预解码 + 同步起播 + 可调衔接)=====
|
|
83
|
+
// 目标:既要 0.3.0 那种"贴手"的响应,又不让 macOS 把音效注册进系统「正在播放」(Touch Bar 播放条 + 卡顿)。
|
|
84
|
+
// 之前 0.3.3~0.3.7 换成 Web Audio 后手感变钝,不是引擎的问题,而是丢了四样里的三样:
|
|
85
|
+
// ① 垫片的 preload='auto' 只是占位 → 不预取、不解码 → 第一次点按要现 fetch+decode;
|
|
86
|
+
// ② 垫片没有 duration、currentTime 也不前进 → 点按退化成 onended 兜底「按压音放完才接松开音」;
|
|
87
|
+
// ③ 起播要经过一次 Promise/微任务。
|
|
88
|
+
// 现在四件齐备:
|
|
89
|
+
// ① 引擎仍是不经过 media element 的 AudioBufferSourceNode(系统媒体控件不会出现 → 无 Touch Bar 条);
|
|
90
|
+
// ② dshwvWarm() 在切音效组 / 打开试听 / 页面初始化时就**预取 + 预解码**(URL 级缓存,只解一次);
|
|
91
|
+
// ③ 补齐 duration / currentTime(播放中真实前进)→ 能算"按压音还剩多久";
|
|
92
|
+
// ④ **同步起播**:缓冲区已预热时,start() 在 pointerdown 的**同一个任务**里调用(不再经过 Promise)。
|
|
93
|
+
// 衔接时机由 RELEASE_LEAD_MS 决定(0 = 正好接上;30/50 = 轻微交叠)—— 改这个数字即可按耳朵微调,
|
|
94
|
+
// 不用动任何逻辑。
|
|
79
95
|
var dshwvAudioCtx = null
|
|
80
96
|
function dshwvAudio() {
|
|
81
97
|
try {
|
|
82
|
-
if (!dshwvAudioCtx)
|
|
98
|
+
if (!dshwvAudioCtx) {
|
|
99
|
+
var AC = window.AudioContext || window.webkitAudioContext
|
|
100
|
+
// 显式用 'interactive'(该 API 的最低延迟档),起播尽量贴手
|
|
101
|
+
dshwvAudioCtx = new AC({ latencyHint: 'interactive' })
|
|
102
|
+
}
|
|
83
103
|
if (dshwvAudioCtx.state === 'suspended') { try { dshwvAudioCtx.resume() } catch (err) {} }
|
|
84
104
|
return dshwvAudioCtx
|
|
85
105
|
} catch (err) { return null }
|
|
86
106
|
}
|
|
87
|
-
var dshwvAudioBuffers = {} // url -> Promise<AudioBuffer
|
|
107
|
+
var dshwvAudioBuffers = {} // url -> Promise<AudioBuffer>(同一片段不重复下载/解码)
|
|
108
|
+
var dshwvAudioDecoded = {} // url -> AudioBuffer(解码完成后**同步可读**:起播走同步路径的关键)
|
|
109
|
+
function nowMs() { try { return (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now() } catch (err) { return Date.now() } }
|
|
88
110
|
function dshwvAudioBuffer(url) {
|
|
89
111
|
if (!url) return Promise.reject(new Error('empty url'))
|
|
90
112
|
if (!dshwvAudioBuffers[url]) {
|
|
@@ -95,62 +117,99 @@ function dshwvAudioBuffer(url) {
|
|
|
95
117
|
if (!c) throw new Error('no audio context')
|
|
96
118
|
return new Promise(function (res, rej) { c.decodeAudioData(raw, res, rej) })
|
|
97
119
|
})
|
|
98
|
-
.
|
|
120
|
+
.then(function (buf) { dshwvAudioDecoded[url] = buf; return buf })
|
|
121
|
+
.catch(function (err) { delete dshwvAudioBuffers[url]; delete dshwvAudioDecoded[url]; throw err })
|
|
99
122
|
}
|
|
100
123
|
return dshwvAudioBuffers[url]
|
|
101
124
|
}
|
|
125
|
+
// 预热:垫片的 preload='auto' 在 Web Audio 下不解码,必须显式预取+预解码。
|
|
126
|
+
// 不预热 → 第一次点按要现 fetch+decodeAudioData,表现就是"按下音慢半拍、中间衔接发飘"。
|
|
127
|
+
// URL 级缓存保证同一片段只解一次;失败静默吞掉(真正播放时还会自己重试一次)。
|
|
128
|
+
function dshwvWarm(urls) {
|
|
129
|
+
try { dshwvAudio() } catch (err) {}
|
|
130
|
+
for (var i = 0; i < (urls || []).length; i++) {
|
|
131
|
+
var u = urls[i]
|
|
132
|
+
if (!u) continue
|
|
133
|
+
try { dshwvAudioBuffer(u).catch(function () {}) } catch (err) {}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
102
136
|
function dshwvSoundStop(el) {
|
|
103
137
|
el._token = (el._token || 0) + 1
|
|
104
138
|
var node = el._node
|
|
105
139
|
el._node = null
|
|
140
|
+
el._playingSince = 0
|
|
106
141
|
if (node) {
|
|
107
142
|
try { node.onended = null } catch (err) {}
|
|
108
143
|
try { node.stop() } catch (err) {}
|
|
109
144
|
}
|
|
110
145
|
}
|
|
111
146
|
function dshwvSound(url) {
|
|
112
|
-
var el = { preload: 'auto', volume: 1, onended: null, loop: false, _url: String(url || ''), _node: null, _gain: null, _offset: 0, _token: 0 }
|
|
147
|
+
var el = { preload: 'auto', volume: 1, onended: null, loop: false, _url: String(url || ''), _node: null, _gain: null, _offset: 0, _token: 0, _playingSince: 0 }
|
|
113
148
|
Object.defineProperty(el, 'src', {
|
|
114
149
|
get: function () { return el._url },
|
|
115
150
|
set: function (v) { dshwvSoundStop(el); el._url = String(v || ''); el._offset = 0 },
|
|
116
151
|
})
|
|
117
152
|
Object.defineProperty(el, 'currentTime', {
|
|
118
|
-
|
|
153
|
+
// 播放中真实前进:pressUp 靠它算"按压音还剩多久",据此把松开音排到按压音结束(或提前 lead)那一刻
|
|
154
|
+
get: function () {
|
|
155
|
+
if (el._node && el._playingSince) {
|
|
156
|
+
var t = el._offset + (nowMs() - el._playingSince) / 1000
|
|
157
|
+
var d = el.duration
|
|
158
|
+
return Math.max(0, isFinite(d) && d > 0 ? Math.min(t, d) : t)
|
|
159
|
+
}
|
|
160
|
+
return el._offset
|
|
161
|
+
},
|
|
119
162
|
// 既有逻辑用「currentTime = 0」表示重播 → 这里顺手停掉正在播的那一份
|
|
120
|
-
set: function (v) { el._offset = Number(v) || 0; dshwvSoundStop(el) },
|
|
163
|
+
set: function (v) { el._offset = Number(v) || 0; el._playingSince = 0; dshwvSoundStop(el) },
|
|
121
164
|
})
|
|
122
|
-
el
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
Object.defineProperty(el, 'duration', {
|
|
166
|
+
// 与 HTMLAudioElement 对齐:未解码时 NaN(调用方用 isFinite 判定),解码后是真实时长
|
|
167
|
+
get: function () {
|
|
168
|
+
var b = dshwvAudioDecoded[el._url]
|
|
169
|
+
return b && isFinite(b.duration) && b.duration > 0 ? b.duration : NaN
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
// 拿到 buffer 后真正起播。delay>0 时用音频线程时间轴(start(when))排期,不受主线程抖动影响。
|
|
173
|
+
function beginWithBuffer(c, buf, delay) {
|
|
125
174
|
try {
|
|
126
175
|
if (!el._gain) { el._gain = c.createGain(); el._gain.connect(c.destination) }
|
|
127
176
|
el._gain.gain.value = Math.max(0, Math.min(1, Number(el.volume) || 0))
|
|
128
|
-
|
|
177
|
+
var src = c.createBufferSource()
|
|
178
|
+
src.buffer = buf
|
|
179
|
+
src.connect(el._gain)
|
|
180
|
+
src.onended = function () {
|
|
181
|
+
if (el._node !== src) return
|
|
182
|
+
el._node = null
|
|
183
|
+
el._playingSince = 0
|
|
184
|
+
if (typeof el.onended === 'function') { try { el.onended() } catch (err) {} }
|
|
185
|
+
}
|
|
186
|
+
el._node = src
|
|
187
|
+
el._playingSince = nowMs() + delay * 1000
|
|
188
|
+
var dur = Math.max(0.001, buf.duration)
|
|
189
|
+
src.start(delay > 0 ? c.currentTime + delay : 0, Math.max(0, el._offset) % dur)
|
|
190
|
+
} catch (err) {}
|
|
191
|
+
}
|
|
192
|
+
function startSound(delaySec) {
|
|
193
|
+
var c = dshwvAudio()
|
|
194
|
+
if (!c || !el._url) return Promise.resolve()
|
|
129
195
|
var token = (el._token = (el._token || 0) + 1)
|
|
130
|
-
var
|
|
131
|
-
|
|
196
|
+
var delay = Math.max(0, Number(delaySec) || 0)
|
|
197
|
+
// ④ 同步起播:缓冲区已预热(dshwvWarm)过 → 直接在当前任务里 start(),不再等 Promise
|
|
198
|
+
var cached = dshwvAudioDecoded[el._url]
|
|
199
|
+
if (cached) { beginWithBuffer(c, cached, delay); return Promise.resolve() }
|
|
200
|
+
dshwvAudioBuffer(el._url).then(function (buf) {
|
|
132
201
|
if (token !== el._token) return // 期间被重播/暂停/换源 → 丢弃这次
|
|
133
|
-
|
|
134
|
-
var src = c.createBufferSource()
|
|
135
|
-
src.buffer = buf
|
|
136
|
-
src.connect(el._gain)
|
|
137
|
-
src.onended = function () {
|
|
138
|
-
if (el._node !== src) return
|
|
139
|
-
el._node = null
|
|
140
|
-
if (typeof el.onended === 'function') { try { el.onended() } catch (err) {} }
|
|
141
|
-
}
|
|
142
|
-
el._node = src
|
|
143
|
-
var dur = Math.max(0.001, buf.duration)
|
|
144
|
-
src.start(0, Math.max(0, el._offset) % dur)
|
|
145
|
-
} catch (err) {}
|
|
202
|
+
beginWithBuffer(c, buf, delay)
|
|
146
203
|
}).catch(function () {})
|
|
147
204
|
return Promise.resolve()
|
|
148
205
|
}
|
|
206
|
+
el.play = function () { return startSound(0) }
|
|
207
|
+
el.playAt = function (delaySec) { return startSound(delaySec) }
|
|
149
208
|
el.pause = function () { dshwvSoundStop(el) }
|
|
150
209
|
return el
|
|
151
210
|
}
|
|
152
|
-
// 自动播放策略:AudioContext 初始是 suspended
|
|
153
|
-
//
|
|
211
|
+
// 自动播放策略:AudioContext 初始是 suspended,要有一次用户手势才能出声;任务结束音不是手势触发的,
|
|
212
|
+
// 所以挂一次性解锁(首次点击/按键后移除)。
|
|
154
213
|
try {
|
|
155
214
|
var dshwvAudioUnlock = function () {
|
|
156
215
|
dshwvAudio()
|
|
@@ -690,6 +749,38 @@ styleEl.setAttribute('data-plugin', 'dsh-whale-widget')
|
|
|
690
749
|
styleEl.textContent = css
|
|
691
750
|
document.head.appendChild(styleEl)
|
|
692
751
|
|
|
752
|
+
// ===== v743:body 挂载登记器(DOM 守护的基础设施,见下面 dshwReattachRoot)=====
|
|
753
|
+
// 挂件会把 30 多个节点挂到 document.body 上(主节点、菜单、各种遮罩/面板、隐藏的 file input…)。
|
|
754
|
+
// 早先的守护只补挂 root + menuBox:SPA 切路由或别的插件整体替换 body 子树后,其余节点会变成
|
|
755
|
+
// "存在但不在文档里"的孤儿 —— 界面看起来恢复了,可一旦点开对应功能就静默失效。
|
|
756
|
+
// 所以这里统一登记:所有挂到 body 的节点都走 dshwBodyAppend(),守护时逐个补挂;
|
|
757
|
+
// 主动移除(目前只有一处)走 dshwBodyDetach(),避免被守护逻辑"复活"。
|
|
758
|
+
var dshwBodyNodes = []
|
|
759
|
+
function dshwBodyAppend(el) {
|
|
760
|
+
try {
|
|
761
|
+
if (!el) return el
|
|
762
|
+
// 注意:这里必须是**原始**的 document.body.appendChild —— 不能走 dshwBodyAppend 自己
|
|
763
|
+
// (v743 批量改写时曾误替换成自我递归,被 try/catch 吞掉后表现为"登记了但从未挂上")
|
|
764
|
+
document.body.appendChild(el)
|
|
765
|
+
if (dshwBodyNodes.indexOf(el) < 0) dshwBodyNodes.push(el)
|
|
766
|
+
} catch (err) {}
|
|
767
|
+
return el
|
|
768
|
+
}
|
|
769
|
+
function dshwBodyDetach(el) {
|
|
770
|
+
try {
|
|
771
|
+
var i = dshwBodyNodes.indexOf(el)
|
|
772
|
+
if (i >= 0) dshwBodyNodes.splice(i, 1)
|
|
773
|
+
if (el && el.parentNode) el.parentNode.removeChild(el)
|
|
774
|
+
} catch (err) {}
|
|
775
|
+
}
|
|
776
|
+
// 兼容性:老 WebView 可能没有 Element.isConnected(Chrome 51+ 才有)。
|
|
777
|
+
// 若直接 `!el.isConnected`,在那种环境里会恒为 true → 每个 DOM 变更批次都会重复 appendChild。
|
|
778
|
+
function dshwConnected(el) {
|
|
779
|
+
if (!el) return false
|
|
780
|
+
try { if (typeof el.isConnected === 'boolean') return el.isConnected } catch (err) {}
|
|
781
|
+
try { return document.documentElement.contains(el) } catch (err) { return true }
|
|
782
|
+
}
|
|
783
|
+
|
|
693
784
|
var root = document.createElement('div')
|
|
694
785
|
root.className = 'dshwv-root'
|
|
695
786
|
|
|
@@ -774,7 +865,7 @@ audioImportBtn.textContent = '导入'
|
|
|
774
865
|
audioImportBtn.title = '新建/编辑音效组'
|
|
775
866
|
audioGroupBtn.addEventListener('click', function (e) { e.stopPropagation(); toggleAudioGroupPanel() })
|
|
776
867
|
audioImportBtn.addEventListener('click', function (e) { e.stopPropagation(); openAudioGroupEditor(null) })
|
|
777
|
-
|
|
868
|
+
dshwBodyAppend(audioGroupPanel)
|
|
778
869
|
function soundOpt(value, label) {
|
|
779
870
|
var o = document.createElement('option')
|
|
780
871
|
o.value = value
|
|
@@ -792,7 +883,7 @@ function dshwCustSelClose() {
|
|
|
792
883
|
if (!o) return
|
|
793
884
|
try {
|
|
794
885
|
o.menu.classList.remove('dshwv-rgbopen')
|
|
795
|
-
|
|
886
|
+
dshwBodyDetach(o.menu) // v744:登记过的 body 节点必须走 detach,否则会被 DOM 守护补挂回来
|
|
796
887
|
} catch (err) {}
|
|
797
888
|
}
|
|
798
889
|
if (!window.__dshwCustBound) {
|
|
@@ -910,7 +1001,7 @@ function dshwCustSel(sel, opts) {
|
|
|
910
1001
|
dshwCustSelClose()
|
|
911
1002
|
fill()
|
|
912
1003
|
sync()
|
|
913
|
-
if (menu.parentNode !== document.body)
|
|
1004
|
+
if (menu.parentNode !== document.body) dshwBodyAppend(menu)
|
|
914
1005
|
dshwDropOpen(menu, btn)
|
|
915
1006
|
// 可选底部参照元素:展开高度不超过该元素的上缘(内部滚动),用于
|
|
916
1007
|
// 主菜单内“任务结束音效”等下拉,避免列表盖住底部「小鲸鱼记账」按钮
|
|
@@ -1148,6 +1239,7 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1148
1239
|
// 按压留空:无按下音,直接播松开(模拟按下即松开的完整点按);松开留空:只播按压
|
|
1149
1240
|
if (pressEmpty) {
|
|
1150
1241
|
if (!releaseEmpty) {
|
|
1242
|
+
dshwvWarm(['/dsh-whale/sound/release.mp3?set=' + encodeURIComponent(groupId)]) // v745:先预热
|
|
1151
1243
|
var relOnly = dshwvSound('/dsh-whale/sound/release.mp3?set=' + encodeURIComponent(groupId))
|
|
1152
1244
|
try { relOnly.volume = vol } catch (err) {}
|
|
1153
1245
|
relOnly.currentTime = 0
|
|
@@ -1158,6 +1250,11 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1158
1250
|
}
|
|
1159
1251
|
var press = dshwvSound('/dsh-whale/sound/press.mp3?set=' + encodeURIComponent(groupId))
|
|
1160
1252
|
try { press.volume = vol } catch (err) {}
|
|
1253
|
+
// v745:菜单里的"点一下试听"同样先预热解码,否则第一次听有明显延迟
|
|
1254
|
+
dshwvWarm([
|
|
1255
|
+
'/dsh-whale/sound/press.mp3?set=' + encodeURIComponent(groupId),
|
|
1256
|
+
releaseEmpty ? '' : '/dsh-whale/sound/release.mp3?set=' + encodeURIComponent(groupId),
|
|
1257
|
+
])
|
|
1161
1258
|
if (releaseEmpty) {
|
|
1162
1259
|
press.currentTime = 0
|
|
1163
1260
|
var pp = press.play()
|
|
@@ -1872,7 +1969,7 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
1872
1969
|
turnCostCloseDefer = false
|
|
1873
1970
|
turnCostCloseInput.disabled = !turnCostOn // 回到菜单态的禁用逻辑
|
|
1874
1971
|
}
|
|
1875
|
-
|
|
1972
|
+
dshwBodyDetach(mask) // v744:走 detach 注销登记,否则 DOM 守护会把这个刚关掉的编辑器补挂回来
|
|
1876
1973
|
bubbleEditItems = bkEditItems
|
|
1877
1974
|
bubbleEditorSnap = bkEditorSnap
|
|
1878
1975
|
bubbleItemSnap = bkItemSnap
|
|
@@ -1991,7 +2088,7 @@ function usageAlertBudgetEditor(key, onSave) {
|
|
|
1991
2088
|
mask.appendChild(card)
|
|
1992
2089
|
mask.addEventListener('click', function (e) { if (e.target === mask) cleanup() })
|
|
1993
2090
|
window.__dshwRemindMask = mask
|
|
1994
|
-
|
|
2091
|
+
dshwBodyAppend(mask)
|
|
1995
2092
|
renderBubblePal()
|
|
1996
2093
|
renderBubblePv()
|
|
1997
2094
|
} catch (err) {}
|
|
@@ -2455,7 +2552,9 @@ function openApiModelPanel(modelId) {
|
|
|
2455
2552
|
// 新建模型:保存后给一个明确的收尾——弹「保存成功」,点确认即关掉本面板。
|
|
2456
2553
|
// (原先是重新打开为编辑态,新建流程会停在一个没有明显关闭入口的面板上)
|
|
2457
2554
|
if (isNew) {
|
|
2458
|
-
|
|
2555
|
+
// v744 清理:这里原先有 `confirmMask.style.zIndex = '29500' !important`,但紧接着的
|
|
2556
|
+
// showConfirm() 会把确认框统一提到 40000 !important,所以那行是**无效设置**(立刻被覆盖),
|
|
2557
|
+
// 只会让"层级"更难读。已删除 —— 确认框永远由 showConfirm 统一抬到 40000。
|
|
2459
2558
|
showConfirm('✓ 保存成功:' + (nm || '新模型') + '\n已加入模型列表', function () {
|
|
2460
2559
|
try { closeApiModelPanel() } catch (err) {}
|
|
2461
2560
|
// 背后的「- = 小鲸鱼记账 = -」列表同步刷新(模型行在静态区,需重建子界面)
|
|
@@ -2563,7 +2662,7 @@ function openApiModelPanel(modelId) {
|
|
|
2563
2662
|
} catch (err) {}
|
|
2564
2663
|
mask.appendChild(card)
|
|
2565
2664
|
mask.addEventListener('click', function (e) { if (e.target === mask) closeApiModelPanel() })
|
|
2566
|
-
|
|
2665
|
+
dshwBodyAppend(mask)
|
|
2567
2666
|
apiModelMaskEl = mask
|
|
2568
2667
|
// 模板切换:把模板默认值填进各字段(仅新增态)
|
|
2569
2668
|
if (isNew) {
|
|
@@ -2614,7 +2713,10 @@ function openApiModelPanel(modelId) {
|
|
|
2614
2713
|
}
|
|
2615
2714
|
var apiModelMaskEl = null
|
|
2616
2715
|
function closeApiModelPanel() {
|
|
2617
|
-
|
|
2716
|
+
// v744:必须走 dshwBodyDetach —— 这个遮罩是登记在案的 body 节点,
|
|
2717
|
+
// 若用 parentNode.removeChild 直接摘掉,DOM 守护会认为"被别的插件摘走了"并把它补挂回来,
|
|
2718
|
+
// 表现就是**点「取消」关不掉这个窗口**(0.3.7 引入的回归)。
|
|
2719
|
+
try { if (apiModelMaskEl) dshwBodyDetach(apiModelMaskEl) } catch (err) {}
|
|
2618
2720
|
apiModelMaskEl = null
|
|
2619
2721
|
}
|
|
2620
2722
|
// 列宽受限 + 悬浮滚动:内容超出列宽时,鼠标移上去文字自动横向滚动,移开回到起点。
|
|
@@ -2914,7 +3016,7 @@ function openModelQuotaEditor(modelId) {
|
|
|
2914
3016
|
card.appendChild(btns)
|
|
2915
3017
|
mask.appendChild(card)
|
|
2916
3018
|
mask.addEventListener('click', function (e) { if (e.target === mask) closeApiModelPanel() })
|
|
2917
|
-
|
|
3019
|
+
dshwBodyAppend(mask)
|
|
2918
3020
|
apiModelMaskEl = mask
|
|
2919
3021
|
} catch (err) {}
|
|
2920
3022
|
}
|
|
@@ -3102,7 +3204,7 @@ function openApiModelMenu(modelId) {
|
|
|
3102
3204
|
card.appendChild(btns)
|
|
3103
3205
|
mask.appendChild(card)
|
|
3104
3206
|
mask.addEventListener('click', function (e) { if (e.target === mask) closeApiModelPanel() })
|
|
3105
|
-
|
|
3207
|
+
dshwBodyAppend(mask)
|
|
3106
3208
|
apiModelMaskEl = mask
|
|
3107
3209
|
} catch (err) {}
|
|
3108
3210
|
}
|
|
@@ -3260,7 +3362,8 @@ function openBalanceAdjustment(modelId) {
|
|
|
3260
3362
|
function close() {
|
|
3261
3363
|
if (busy) return
|
|
3262
3364
|
document.removeEventListener('keydown', keyHandler)
|
|
3263
|
-
|
|
3365
|
+
// v743:主动移除要走 dshwBodyDetach —— 否则它已被登记,DOM 守护会把这个刚关掉的窗口"复活"
|
|
3366
|
+
dshwBodyDetach(mask)
|
|
3264
3367
|
accountingMask = null
|
|
3265
3368
|
// 关掉校正窗口后回到同一个设置菜单,并把焦点放回「校正」按钮
|
|
3266
3369
|
openApiModelMenu(modelId)
|
|
@@ -3293,7 +3396,7 @@ function openBalanceAdjustment(modelId) {
|
|
|
3293
3396
|
card.appendChild(actions)
|
|
3294
3397
|
mask.appendChild(card)
|
|
3295
3398
|
mask.addEventListener('click', function (e) { if (e.target === mask) close() })
|
|
3296
|
-
|
|
3399
|
+
dshwBodyAppend(mask)
|
|
3297
3400
|
accountingMask = mask
|
|
3298
3401
|
document.addEventListener('keydown', keyHandler)
|
|
3299
3402
|
var rows = []
|
|
@@ -3489,7 +3592,7 @@ var usageMoreCard = document.createElement('div')
|
|
|
3489
3592
|
usageMoreCard.className = 'dshwv-usage-card'
|
|
3490
3593
|
usageMoreMask.appendChild(usageMoreCard)
|
|
3491
3594
|
usageMoreMask.addEventListener('click', function (e) { if (e.target === usageMoreMask) closeUsageRecordsWindow() })
|
|
3492
|
-
|
|
3595
|
+
dshwBodyAppend(usageMoreMask)
|
|
3493
3596
|
function openUsageRecordsWindow() {
|
|
3494
3597
|
usageMoreCard.innerHTML = '<div style="padding:10px;color:#203170">加载中…</div>'
|
|
3495
3598
|
usageMoreMask.style.display = 'flex'
|
|
@@ -3512,7 +3615,7 @@ function resMaskOpen() {
|
|
|
3512
3615
|
resCardEl.className = 'dshwv-usage-card dshwv-rescard'
|
|
3513
3616
|
resMaskEl.appendChild(resCardEl)
|
|
3514
3617
|
resMaskEl.addEventListener('click', function (e) { if (e.target === resMaskEl) resManagerClose() })
|
|
3515
|
-
|
|
3618
|
+
dshwBodyAppend(resMaskEl)
|
|
3516
3619
|
}
|
|
3517
3620
|
resManagerRender()
|
|
3518
3621
|
resMaskEl.style.display = 'flex'
|
|
@@ -4107,12 +4210,12 @@ function usagePopupCard(title, content, below, amount) {
|
|
|
4107
4210
|
ok.type = 'button'
|
|
4108
4211
|
ok.className = 'dshwv-bubbtn dshwv-bubbtn-ok'
|
|
4109
4212
|
ok.textContent = '知道了'
|
|
4110
|
-
ok.addEventListener('click', function () { try {
|
|
4213
|
+
ok.addEventListener('click', function () { try { dshwBodyDetach(mask) } catch (err) {} })
|
|
4111
4214
|
btns.appendChild(ok)
|
|
4112
4215
|
card.appendChild(btns)
|
|
4113
4216
|
mask.appendChild(card)
|
|
4114
|
-
mask.addEventListener('click', function (e) { if (e.target === mask) { try {
|
|
4115
|
-
|
|
4217
|
+
mask.addEventListener('click', function (e) { if (e.target === mask) { try { dshwBodyDetach(mask) } catch (err) {} } })
|
|
4218
|
+
dshwBodyAppend(mask)
|
|
4116
4219
|
} catch (err) {}
|
|
4117
4220
|
}
|
|
4118
4221
|
var USAGE_PALETTE = ['#203170', '#e0433f', '#2fa24c', '#b060c8', '#e89a2e', '#3aa6c8', '#d06a8a', '#7a8b2f', '#6a6ad0', '#c84a8a']
|
|
@@ -4617,7 +4720,7 @@ function fillUsageRecordsWindow(d) {
|
|
|
4617
4720
|
renderGroups('')
|
|
4618
4721
|
})
|
|
4619
4722
|
}
|
|
4620
|
-
|
|
4723
|
+
dshwBodyAppend(rolePanel)
|
|
4621
4724
|
|
|
4622
4725
|
// —— 吸附与翻转自定义弹窗 ——
|
|
4623
4726
|
// 预览方框内五条可拖线:左/右/上/下四条吸附区边界 + 红色翻转线(竖中线位置)。
|
|
@@ -5034,7 +5137,7 @@ snapBtns.appendChild(snapBtn('重置', 'dshwv-snapbtn-no', resetSnapEdit))
|
|
|
5034
5137
|
snapBtns.appendChild(snapBtn('确认', 'dshwv-snapbtn-ok', function () { closeSnapModal(true) }))
|
|
5035
5138
|
snapCard.appendChild(snapBtns)
|
|
5036
5139
|
snapMask.appendChild(snapCard)
|
|
5037
|
-
|
|
5140
|
+
dshwBodyAppend(snapMask)
|
|
5038
5141
|
|
|
5039
5142
|
// —— 自定义泡泡:主编辑窗口(点击队列) + 单泡模块编辑 + 模块编辑 ——
|
|
5040
5143
|
var bubbleMask = null
|
|
@@ -6890,7 +6993,7 @@ function qeditEnsure() {
|
|
|
6890
6993
|
qeditEl = document.createElement('div')
|
|
6891
6994
|
qeditEl.className = 'dshwv-qedit'
|
|
6892
6995
|
qeditEl.style.display = 'none'
|
|
6893
|
-
|
|
6996
|
+
dshwBodyAppend(qeditEl)
|
|
6894
6997
|
if (!window.__dshwQeditBound) {
|
|
6895
6998
|
window.__dshwQeditBound = true
|
|
6896
6999
|
document.addEventListener('pointerdown', function (e) {
|
|
@@ -8212,7 +8315,7 @@ bubbleBtns.appendChild(bubbleBtn('重置', 'dshwv-bubbtn-no', bubbleEditorReset)
|
|
|
8212
8315
|
bubbleBtns.appendChild(bubbleBtn('保存', 'dshwv-bubbtn-ok', bubbleEditorSave))
|
|
8213
8316
|
bubbleCard.appendChild(bubbleBtns)
|
|
8214
8317
|
bubbleMask.appendChild(bubbleCard)
|
|
8215
|
-
|
|
8318
|
+
dshwBodyAppend(bubbleMask)
|
|
8216
8319
|
|
|
8217
8320
|
// ===== W2 单泡编辑窗口 =====
|
|
8218
8321
|
bubbleItemMask = document.createElement('div')
|
|
@@ -8296,7 +8399,7 @@ bubbleItemBtns.appendChild(bubbleBtn('恢复默认', 'dshwv-bubbtn-no', bubbleIt
|
|
|
8296
8399
|
bubbleItemBtns.appendChild(bubbleBtn('保存', 'dshwv-bubbtn-ok', bubbleItemSave))
|
|
8297
8400
|
bubbleItemCard.appendChild(bubbleItemBtns)
|
|
8298
8401
|
bubbleItemMask.appendChild(bubbleItemCard)
|
|
8299
|
-
|
|
8402
|
+
dshwBodyAppend(bubbleItemMask)
|
|
8300
8403
|
|
|
8301
8404
|
// ===== W3 模块编辑器(文本/随机/图片 + 样式) =====
|
|
8302
8405
|
var moduleMask = null
|
|
@@ -8395,9 +8498,46 @@ var bubbleRgbOpenMenu = null // 当前展开的 .dshwv-rgbmenu
|
|
|
8395
8498
|
var bubbleFontOpenMenu = null // 当前展开的字体下拉(复用 rgbmenu 类,额外限高)
|
|
8396
8499
|
var bubbleColorOpenMenu = null // 当前展开的颜色下拉(纯色/跑马灯,限高)
|
|
8397
8500
|
// —— 弹层/菜单层级统一助手(所有下拉与弹窗据此取高于当前可见层,避免互相压制) ——
|
|
8501
|
+
// ============================================================================
|
|
8502
|
+
// 层级(z-index)分层表 —— **改层之前先读这里**
|
|
8503
|
+
// 所有浮层都直接挂在 <body> 上,谁盖谁完全由 z-index 决定。历史上出过
|
|
8504
|
+
// 「子窗口被父窗口盖住点不到」「窗口关不掉」这类问题,所以这里把层段定死,并约定三条铁律:
|
|
8505
|
+
// ① 新增浮层**先在下面的表里选一个层段**,不要随手写数字;
|
|
8506
|
+
// ② 凡是「可能从别的窗口里被打开」的窗口,必须用 dshwLayerUp(el, 本段起点),
|
|
8507
|
+
// 这样父窗口在 29000 时子窗口自动落到 29010,永远不会被盖住;
|
|
8508
|
+
// ③ visibleTopZ() 的候选表必须包含**全部**浮层节点(漏一个就会低估"当前最高层"),
|
|
8509
|
+
// 唯一例外是 toast —— 它是刻意的最顶层,不参与追赶。
|
|
8510
|
+
//
|
|
8511
|
+
// 层段 用途 载体
|
|
8512
|
+
// ---------------------------------------------------------------------------
|
|
8513
|
+
// 1 – 999 挂件本体内部元素 .dshwv-pop(1) / .dshwv-menu-btn(2) / 拖拽把手(3-5)
|
|
8514
|
+
// 30 / 60 面板内的自绘下拉 .dshwv-colpop(30) / .dshwv-rgbmenu(60)
|
|
8515
|
+
// 9999 挂件本体 .dshwv-root
|
|
8516
|
+
// 10000 – 19999 主菜单与其列表 .dshwv-menu(10000) / .dshwv-rolelist·audiolist(10001)
|
|
8517
|
+
// 20000 – 20999 一级编辑器 .dshwv-cropmask·gifmask(20000) / .dshwv-resmask(20300) /
|
|
8518
|
+
// .dshwv-audiomask·bubmask(20500) / .dshwv-slotlist(20600)
|
|
8519
|
+
// 21000 – 21999 对话框基础层 .dshwv-confirmmask(21000,实际被 showConfirm 提到 40000)
|
|
8520
|
+
// 22000 – 22999 记账 / 吸附窗口 .dshwv-snapmask·usage-mask(22000)
|
|
8521
|
+
// 26000 – 26999 小浮层 .dshwv-qedit(26000) / .dshwv-usagepanel(26020) /
|
|
8522
|
+
// .dshwv-tplhelp·动态提示(26080+)
|
|
8523
|
+
// 29000 – 29999 模型子菜单 / 模型设置 JS 显式写入(refreshModelList / openApiModelMenu)
|
|
8524
|
+
// 30000 – 30999 提醒·额度·余额校正编辑器 JS 显式写入(含 openModelQuotaEditor / openBalanceAdjustment)
|
|
8525
|
+
// 31000 – 31999 提醒编辑期间需提到顶层的浮层 JS 显式写入(moduleMask / qedit / token 提示)
|
|
8526
|
+
// 32000 – 32999 提醒编辑期间统一置顶的遮罩 remindZStyle 的 !important 规则
|
|
8527
|
+
// 40000 确认对话框(永远在所有窗口之上) showConfirm() 的 !important
|
|
8528
|
+
// 2147483600 提示条 toast(最高,且不参与 visibleTopZ)
|
|
8529
|
+
// ============================================================================
|
|
8398
8530
|
function visibleTopZ() {
|
|
8399
8531
|
var top = 20500
|
|
8400
|
-
|
|
8532
|
+
// 注意:这里必须列全 —— 少一个浮层,dshwLayerUp/下拉/提示就会低估"当前最高层"而被盖住。
|
|
8533
|
+
// toast 刻意不列入(它是永远的最顶层,不该让别的层去追它)。
|
|
8534
|
+
var cand = [
|
|
8535
|
+
bubbleMask, bubbleItemMask, moduleMask, moduleNamePromptMask,
|
|
8536
|
+
cropMask, gifMask, audioCropMask, audioEditMask, resMaskEl,
|
|
8537
|
+
confirmMask, snapMask, usageMask, usageMoreMask,
|
|
8538
|
+
qeditEl, dshwvTplHelpEl, dshwvHintEl,
|
|
8539
|
+
apiModelMaskEl, accountingMask, window.__dshwRemindMask
|
|
8540
|
+
]
|
|
8401
8541
|
function eff(el) {
|
|
8402
8542
|
try {
|
|
8403
8543
|
if (!el) return 0
|
|
@@ -8417,6 +8557,13 @@ function visibleTopZ() {
|
|
|
8417
8557
|
}
|
|
8418
8558
|
return top
|
|
8419
8559
|
}
|
|
8560
|
+
// 「永远在打开它的那个窗口之上」:取 本层段起点 与 当前可见最高层+10 的较大值。
|
|
8561
|
+
// 用在裁剪 / GIF / 音频裁剪 / 模块编辑器这些**既可能从主菜单(10000)打开、也可能从资源管理(20300)、
|
|
8562
|
+
// 泡泡编辑器(20500)、模型设置(29000)里打开**的窗口上 —— 固定层号在后者场景会被父窗口盖住。
|
|
8563
|
+
function dshwLayerUp(el, base) {
|
|
8564
|
+
try { if (el && el.style) el.style.zIndex = String(Math.max(base, Math.round(visibleTopZ()) + 10)) } catch (err) {}
|
|
8565
|
+
return el
|
|
8566
|
+
}
|
|
8420
8567
|
// 打开主要编辑器前清理可能残留的临时层级(提醒会话遗留的 moduleMask/qedit 提升与样式)
|
|
8421
8568
|
function whaleZClean() {
|
|
8422
8569
|
try {
|
|
@@ -8432,7 +8579,7 @@ function whaleZClean() {
|
|
|
8432
8579
|
// 菜单宽度与触发按钮保持一致(原样式 min-width:100% 在 body 下会按视口撑满,须归零)
|
|
8433
8580
|
function dshwDropOpen(menuEl, anchorEl) {
|
|
8434
8581
|
try {
|
|
8435
|
-
if (menuEl.parentNode !== document.body)
|
|
8582
|
+
if (menuEl.parentNode !== document.body) dshwBodyAppend(menuEl)
|
|
8436
8583
|
menuEl.style.position = 'fixed'
|
|
8437
8584
|
menuEl.style.minWidth = '0px'
|
|
8438
8585
|
menuEl.style.left = '0px'
|
|
@@ -9428,6 +9575,9 @@ function openModuleEditor(m, onSave, isNew) {
|
|
|
9428
9575
|
moduleOnSave = onSave || null
|
|
9429
9576
|
moduleEditNew = !!isNew
|
|
9430
9577
|
renderModuleEditor()
|
|
9578
|
+
// v744:模块编辑器从泡泡编辑器(20500)/资源管理(20300)里打开时,固定 20500 会与父窗口同层
|
|
9579
|
+
// (只靠 DOM 顺序决定谁在上面),这里统一抬到"当前最高层之上"
|
|
9580
|
+
dshwLayerUp(moduleMask, 20500)
|
|
9431
9581
|
moduleMask.style.display = 'flex'
|
|
9432
9582
|
} catch (err) {}
|
|
9433
9583
|
}
|
|
@@ -9483,7 +9633,7 @@ moduleBtns.appendChild(saveAsBtn)
|
|
|
9483
9633
|
moduleBtns.appendChild(bubbleBtn('保存', 'dshwv-bubbtn-ok', function () { closeModuleEditor(true) }))
|
|
9484
9634
|
moduleCard.appendChild(moduleBtns)
|
|
9485
9635
|
moduleMask.appendChild(moduleCard)
|
|
9486
|
-
|
|
9636
|
+
dshwBodyAppend(moduleMask)
|
|
9487
9637
|
// 存为可选模块:名称输入弹窗
|
|
9488
9638
|
var moduleNamePromptMask = document.createElement('div')
|
|
9489
9639
|
moduleNamePromptMask.className = 'dshwv-confirmmask'
|
|
@@ -9516,7 +9666,7 @@ moduleNameBtns.appendChild(moduleNameCancel)
|
|
|
9516
9666
|
moduleNameBtns.appendChild(moduleNameOk)
|
|
9517
9667
|
moduleNamePromptCard.appendChild(moduleNameBtns)
|
|
9518
9668
|
moduleNamePromptMask.appendChild(moduleNamePromptCard)
|
|
9519
|
-
|
|
9669
|
+
dshwBodyAppend(moduleNamePromptMask)
|
|
9520
9670
|
// 回车保存/关闭
|
|
9521
9671
|
moduleNameInput.addEventListener('keydown', function (e) {
|
|
9522
9672
|
try {
|
|
@@ -9629,7 +9779,7 @@ cropCard.appendChild(cropZoomWrap)
|
|
|
9629
9779
|
cropCard.appendChild(cropAngleWrap)
|
|
9630
9780
|
cropCard.appendChild(cropBtns)
|
|
9631
9781
|
cropMask.appendChild(cropCard)
|
|
9632
|
-
|
|
9782
|
+
dshwBodyAppend(cropMask)
|
|
9633
9783
|
cropBox.addEventListener('pointerdown', onCropDown)
|
|
9634
9784
|
cropBox.addEventListener('pointermove', onCropMove)
|
|
9635
9785
|
cropBox.addEventListener('pointerup', onCropUp)
|
|
@@ -9729,7 +9879,7 @@ gifCard.appendChild(gifHint)
|
|
|
9729
9879
|
gifCard.appendChild(gifNameInput)
|
|
9730
9880
|
gifCard.appendChild(gifBtns)
|
|
9731
9881
|
gifMask.appendChild(gifCard)
|
|
9732
|
-
|
|
9882
|
+
dshwBodyAppend(gifMask)
|
|
9733
9883
|
gifCancelBtn.addEventListener('click', hideGifRoleModal)
|
|
9734
9884
|
gifOkBtn.addEventListener('click', confirmGifRole)
|
|
9735
9885
|
var gifRoleDataUrl = null
|
|
@@ -9749,6 +9899,7 @@ function openGifRoleModal(dataUrl, fileName, animType) {
|
|
|
9749
9899
|
}
|
|
9750
9900
|
gifNameInput.value = ''
|
|
9751
9901
|
gifPreviewImg.src = dataUrl
|
|
9902
|
+
dshwLayerUp(gifMask, 20000) // v744:同上,GIF 窗口也可能从资源管理/泡泡编辑器里打开
|
|
9752
9903
|
gifMask.style.display = 'flex'
|
|
9753
9904
|
}
|
|
9754
9905
|
function hideGifRoleModal() {
|
|
@@ -9810,7 +9961,7 @@ confirmBtns.appendChild(confirmYesBtn)
|
|
|
9810
9961
|
confirmCard.appendChild(confirmText)
|
|
9811
9962
|
confirmCard.appendChild(confirmBtns)
|
|
9812
9963
|
confirmMask.appendChild(confirmCard)
|
|
9813
|
-
|
|
9964
|
+
dshwBodyAppend(confirmMask)
|
|
9814
9965
|
confirmNoBtn.addEventListener('click', hideConfirm)
|
|
9815
9966
|
confirmYesBtn.addEventListener('click', function () {
|
|
9816
9967
|
var cb = confirmCb
|
|
@@ -9902,7 +10053,7 @@ audioEditCard.appendChild(audioEditPressRow)
|
|
|
9902
10053
|
audioEditCard.appendChild(audioEditReleaseRow)
|
|
9903
10054
|
audioEditCard.appendChild(audioEditBtns)
|
|
9904
10055
|
audioEditMask.appendChild(audioEditCard)
|
|
9905
|
-
|
|
10056
|
+
dshwBodyAppend(audioEditMask)
|
|
9906
10057
|
audioEditCancel.addEventListener('click', hideAudioEditor)
|
|
9907
10058
|
audioEditPlay.addEventListener('pointerdown', function (e) { e.stopPropagation(); audioEditPreviewDown() })
|
|
9908
10059
|
audioEditPlay.addEventListener('pointerup', function (e) { e.stopPropagation(); audioEditPreviewUp() })
|
|
@@ -10055,7 +10206,7 @@ audioCropCard.appendChild(audioCropTime)
|
|
|
10055
10206
|
audioCropCard.appendChild(audioCropNameRow)
|
|
10056
10207
|
audioCropCard.appendChild(audioCropBtns)
|
|
10057
10208
|
audioCropMask.appendChild(audioCropCard)
|
|
10058
|
-
|
|
10209
|
+
dshwBodyAppend(audioCropMask)
|
|
10059
10210
|
audioCropCancel.addEventListener('click', hideAudioCrop)
|
|
10060
10211
|
audioCropOk.addEventListener('click', confirmAudioCrop)
|
|
10061
10212
|
audioCropPlay.addEventListener('click', previewAudioCrop)
|
|
@@ -10126,8 +10277,8 @@ body.appendChild(img)
|
|
|
10126
10277
|
body.appendChild(bubbleBox)
|
|
10127
10278
|
root.appendChild(body)
|
|
10128
10279
|
root.appendChild(menuBtn)
|
|
10129
|
-
|
|
10130
|
-
|
|
10280
|
+
dshwBodyAppend(root)
|
|
10281
|
+
dshwBodyAppend(menuBox)
|
|
10131
10282
|
|
|
10132
10283
|
// ===== PR #105 后半:DOM 守护(SPA 切路由 / 别的插件替换 body 子树时把节点摘掉)=====
|
|
10133
10284
|
// 背景:DSH 是 SPA,切到会话列表 / 设置 / 插件市场再回来、或其它客户端插件整体替换
|
|
@@ -10135,19 +10286,29 @@ document.body.appendChild(menuBox)
|
|
|
10135
10286
|
// 做法:暴露 window.__dshWhaleRoot 供外部定位/调试,并用一个 MutationObserver 盯着;
|
|
10136
10287
|
// 一旦发现节点已不在文档里就**把同一个节点补挂回 body**(不重建、不重新初始化,
|
|
10137
10288
|
// 位置/设置/状态全部保留)。
|
|
10289
|
+
// v743:补挂范围从 root+menuBox 扩到**全部登记过的 body 节点**(见 dshwBodyNodes)——
|
|
10290
|
+
// 否则被整体替换后,遮罩/面板/隐藏 file input 会变成孤儿,功能静默失效;
|
|
10291
|
+
// 另外 root 还在、只有个别节点被摘掉的"局部移除"也要能自愈,所以做了节流的全量核对。
|
|
10138
10292
|
try { window.__dshWhaleRoot = root } catch (err) {}
|
|
10139
10293
|
function dshwReattachRoot() {
|
|
10140
10294
|
try {
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10295
|
+
for (var i = 0; i < dshwBodyNodes.length; i++) {
|
|
10296
|
+
var el = dshwBodyNodes[i]
|
|
10297
|
+
if (el && !dshwConnected(el)) dshwBodyAppend(el)
|
|
10298
|
+
}
|
|
10145
10299
|
} catch (err) {}
|
|
10146
10300
|
}
|
|
10147
10301
|
try {
|
|
10148
10302
|
if (typeof MutationObserver === 'function') {
|
|
10303
|
+
var dshwGuardLastFull = 0
|
|
10149
10304
|
var dshwRootGuard = new MutationObserver(function () {
|
|
10150
|
-
|
|
10305
|
+
try {
|
|
10306
|
+
if (!root) return
|
|
10307
|
+
if (!dshwConnected(root)) { dshwReattachRoot(); dshwGuardLastFull = Date.now(); return }
|
|
10308
|
+
// root 正常时也定期全量核对一次(最多 1.5 秒一次):覆盖"只有个别浮层被摘掉"的情况
|
|
10309
|
+
var now = Date.now()
|
|
10310
|
+
if (now - dshwGuardLastFull > 1500) { dshwGuardLastFull = now; dshwReattachRoot() }
|
|
10311
|
+
} catch (err) {}
|
|
10151
10312
|
})
|
|
10152
10313
|
dshwRootGuard.observe(document.documentElement, { childList: true, subtree: true })
|
|
10153
10314
|
}
|
|
@@ -10209,6 +10370,8 @@ var state = {
|
|
|
10209
10370
|
todayUsageCurrency: 'CNY',
|
|
10210
10371
|
usageLabel: '本地估算',
|
|
10211
10372
|
isPeak: false,
|
|
10373
|
+
peakNextChangeAt: null,
|
|
10374
|
+
peakHolidays: null,
|
|
10212
10375
|
status: 'loading',
|
|
10213
10376
|
message: '',
|
|
10214
10377
|
flip: false
|
|
@@ -11236,7 +11399,7 @@ function bubbleTplHelpToggle(m, anchor) {
|
|
|
11236
11399
|
if (!dshwvTplHelpEl) {
|
|
11237
11400
|
dshwvTplHelpEl = document.createElement('div')
|
|
11238
11401
|
dshwvTplHelpEl.className = 'dshwv-tplhelp'
|
|
11239
|
-
|
|
11402
|
+
dshwBodyAppend(dshwvTplHelpEl)
|
|
11240
11403
|
document.addEventListener('pointerdown', function (e) {
|
|
11241
11404
|
if (!dshwvTplHelpEl || dshwvTplHelpEl.style.display === 'none') return
|
|
11242
11405
|
try {
|
|
@@ -11280,7 +11443,7 @@ function dshwvHintEnsure() {
|
|
|
11280
11443
|
dshwvHintEl = document.createElement('div')
|
|
11281
11444
|
dshwvHintEl.className = 'dshwv-tplhelp dshwv-hintbox'
|
|
11282
11445
|
dshwvHintEl.style.display = 'none'
|
|
11283
|
-
|
|
11446
|
+
dshwBodyAppend(dshwvHintEl)
|
|
11284
11447
|
document.addEventListener('pointerdown', function (e) {
|
|
11285
11448
|
if (!dshwvHintEl || dshwvHintEl.style.display === 'none') return
|
|
11286
11449
|
try {
|
|
@@ -11423,9 +11586,28 @@ function bubbleModuleText(m, avoidIdx) {
|
|
|
11423
11586
|
return t
|
|
11424
11587
|
}
|
|
11425
11588
|
// —— 下个时段倒计时模块:按“工作日9-12/14-18为高峰,周末全天谷价”推算下一时段切换 ——
|
|
11589
|
+
// v746:法定节假日同样全天谷价。前端无法自己知道放假安排,节假日清单由宿主随余额接口下发
|
|
11590
|
+
// (state.peakHolidays,单一来源),切换点也优先用宿主算好的绝对值(state.peakNextChangeAt)。
|
|
11591
|
+
var bubbleHolidaySet = null
|
|
11592
|
+
var bubbleHolidaySetKey = ''
|
|
11593
|
+
function bubbleHolidaySetOf() {
|
|
11594
|
+
var list = (state && state.peakHolidays) || null
|
|
11595
|
+
var key = Array.isArray(list) ? list.join(',') : ''
|
|
11596
|
+
if (key !== bubbleHolidaySetKey) {
|
|
11597
|
+
bubbleHolidaySetKey = key
|
|
11598
|
+
bubbleHolidaySet = {}
|
|
11599
|
+
if (Array.isArray(list)) for (var i = 0; i < list.length; i++) bubbleHolidaySet[String(list[i])] = 1
|
|
11600
|
+
}
|
|
11601
|
+
return bubbleHolidaySet
|
|
11602
|
+
}
|
|
11603
|
+
function bubbleBJHolidayKey(bj) {
|
|
11604
|
+
try { return bj.toISOString().slice(0, 10) } catch (err) { return '' }
|
|
11605
|
+
}
|
|
11426
11606
|
function bubbleCountdownIsPeak(sec) {
|
|
11427
11607
|
sec = isFinite(Number(sec)) ? Number(sec) : Math.floor(Date.now() / 1000)
|
|
11428
11608
|
var bj = new Date((sec + 8 * 3600) * 1000)
|
|
11609
|
+
var hs = bubbleHolidaySetOf()
|
|
11610
|
+
if (hs && hs[bubbleBJHolidayKey(bj)]) return false // 法定节假日全天谷价
|
|
11429
11611
|
var dow = bj.getUTCDay()
|
|
11430
11612
|
var h = bj.getUTCHours()
|
|
11431
11613
|
if (dow === 0 || dow === 6) return false
|
|
@@ -11433,10 +11615,14 @@ function bubbleCountdownIsPeak(sec) {
|
|
|
11433
11615
|
}
|
|
11434
11616
|
function bubbleCountdownNextChange(sec) {
|
|
11435
11617
|
sec = isFinite(Number(sec)) ? Number(sec) : Math.floor(Date.now() / 1000)
|
|
11618
|
+
// 宿主已就绪时直接用其算好的切换点(与计费同源,含法定节假日);
|
|
11619
|
+
// 只接受未来 12 天内的值——切过去之后该值会过期,回退到本地推算,避免倒计时卡在 00:00:00
|
|
11620
|
+
var hostCand = Number(state && state.peakNextChangeAt)
|
|
11621
|
+
if (isFinite(hostCand) && hostCand > sec + 1 && hostCand - sec <= 12 * 86400) return hostCand
|
|
11436
11622
|
var cur = bubbleCountdownIsPeak(sec)
|
|
11437
11623
|
var bjDay0 = Math.floor((sec + 8 * 3600) / 86400) * 86400
|
|
11438
11624
|
var best = null
|
|
11439
|
-
for (var d = 0; d <=
|
|
11625
|
+
for (var d = 0; d <= 12 && best === null; d++) {
|
|
11440
11626
|
var dayStartBj = bjDay0 + d * 86400
|
|
11441
11627
|
var edges = [0, 9 * 3600, 12 * 3600, 14 * 3600, 18 * 3600]
|
|
11442
11628
|
for (var i = 0; i < edges.length; i++) {
|
|
@@ -12345,6 +12531,8 @@ function refresh(manual) {
|
|
|
12345
12531
|
state.usageLabel = data.usageLabel || '本地估算'
|
|
12346
12532
|
if (data.stale) state.usageLabel += ' · 余额未刷新'
|
|
12347
12533
|
state.isPeak = !!data.isPeak
|
|
12534
|
+
state.peakNextChangeAt = isFinite(Number(data.peakNextChangeAt)) ? Number(data.peakNextChangeAt) : null
|
|
12535
|
+
state.peakHolidays = Array.isArray(data.peakHolidays) ? data.peakHolidays : null
|
|
12348
12536
|
checkUsageAlerts(nb, state.todayUsage)
|
|
12349
12537
|
if (changed && !currencyChanged) {
|
|
12350
12538
|
if (!manual) {
|
|
@@ -12420,13 +12608,14 @@ function dshwvToast(msg) {
|
|
|
12420
12608
|
'max-width:min(560px,calc(100vw - 32px));box-sizing:border-box;padding:10px 14px;border-radius:10px;' +
|
|
12421
12609
|
'background:#8a1f1f;color:#fff;font-size:13px;line-height:1.6;box-shadow:0 6px 20px rgba(0,0,0,.28);' +
|
|
12422
12610
|
'pointer-events:none;text-align:center'
|
|
12423
|
-
|
|
12611
|
+
dshwBodyAppend(el)
|
|
12424
12612
|
dshwvToastEl = el
|
|
12425
12613
|
}
|
|
12426
12614
|
dshwvToastEl.innerHTML = msg
|
|
12427
12615
|
if (dshwvToastTimer) clearTimeout(dshwvToastTimer)
|
|
12428
12616
|
dshwvToastTimer = setTimeout(function () {
|
|
12429
|
-
|
|
12617
|
+
// v744:toast 也是登记过的 body 节点,必须 detach(否则 DOM 守护会把它补挂回来 → 提示条永不消失)
|
|
12618
|
+
try { if (dshwvToastEl) dshwBodyDetach(dshwvToastEl) } catch (err) {}
|
|
12430
12619
|
dshwvToastEl = null
|
|
12431
12620
|
}, 8000)
|
|
12432
12621
|
} catch (err) {}
|
|
@@ -12459,12 +12648,15 @@ function configPut(payload, retried) {
|
|
|
12459
12648
|
}
|
|
12460
12649
|
function saveConfig() {
|
|
12461
12650
|
// issue #97:加载完成前只记待办,绝不 PUT(否则把默认值整包写进服务端)
|
|
12462
|
-
if (!configLoaded) { configSavePending = true; return }
|
|
12651
|
+
if (!configLoaded) { configSavePending = true; return null }
|
|
12463
12652
|
try {
|
|
12464
|
-
|
|
12653
|
+
// 返回 Promise(v743:Codex 统计开关需要"等服务端确认后再刷新")。
|
|
12654
|
+
// configPut 自带重试与失败提示、且链尾有 catch,所以不会有 unhandled rejection。
|
|
12655
|
+
var p = configPut(configPayload(), false)
|
|
12465
12656
|
// 锚点位置记忆:记录相对边框的离边距离,窗口 resize 后保持(localStorage)。
|
|
12466
12657
|
saveAnchorPos()
|
|
12467
|
-
|
|
12658
|
+
return p
|
|
12659
|
+
} catch (err) { return null }
|
|
12468
12660
|
}
|
|
12469
12661
|
// 单独抽出:只写 localStorage 锚点(不碰尺寸设置);applyAnchorPos / settle 自愈时也要用。
|
|
12470
12662
|
// v:2 = 净距离格式(剥离避让距离),v:1 旧格式含避让距离,恢复时废弃旧格式。
|
|
@@ -12572,8 +12764,12 @@ function setMenuBtnHide(v) {
|
|
|
12572
12764
|
function setCodexStatsOn(v) {
|
|
12573
12765
|
codexStatsOn = v !== false
|
|
12574
12766
|
if (codexStatsToggle) codexStatsToggle.checked = codexStatsOn
|
|
12575
|
-
|
|
12576
|
-
|
|
12767
|
+
var after = function () { try { refreshModelList() } catch (err) {} }
|
|
12768
|
+
var p = null
|
|
12769
|
+
try { p = saveConfig() } catch (err) {}
|
|
12770
|
+
// 等服务端确认落盘后再重取模型列表:否则可能读到旧配置,那一行会先显示上一次的状态再跳变
|
|
12771
|
+
if (p && typeof p.then === 'function') p.then(after, after)
|
|
12772
|
+
else after()
|
|
12577
12773
|
}
|
|
12578
12774
|
function scaleToDisplay(s) {
|
|
12579
12775
|
return Math.round((s - MIN_SCALE) / ((MAX_SCALE - MIN_SCALE) / 19)) + 1
|
|
@@ -12639,12 +12835,11 @@ var releaseAudio = null
|
|
|
12639
12835
|
var pressing = false
|
|
12640
12836
|
var pressEnded = false
|
|
12641
12837
|
var releasePlayed = false
|
|
12642
|
-
|
|
12838
|
+
// v745:不再需要 releaseTimer —— 点按时由 playReleaseAt() 在**音频线程**排期(见 pressUp)
|
|
12643
12839
|
function applySoundSet() {
|
|
12644
12840
|
try {
|
|
12645
12841
|
// v729:切音效组 / 开关音效时把本轮播放状态一并复位,
|
|
12646
12842
|
// 避免残留 releasePlayed=true 把新组的松开音整体吃掉(与 playPress 的修复配套)
|
|
12647
|
-
if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null }
|
|
12648
12843
|
pressEnded = false
|
|
12649
12844
|
releasePlayed = false
|
|
12650
12845
|
// 槽位显式留空(该事件静音)时,对应音频元素置空;playPress/playRelease 已判空
|
|
@@ -12660,6 +12855,12 @@ function applySoundSet() {
|
|
|
12660
12855
|
releaseAudio.preload = 'auto'
|
|
12661
12856
|
releaseAudio.volume = soundVol
|
|
12662
12857
|
}
|
|
12858
|
+
// v745:把这组的按压/松开音**预取+预解码**(Web Audio 下 preload='auto' 不解码)。
|
|
12859
|
+
// 预热过之后,起播走 dshwvSound 的同步路径(pointerdown 同一任务里 start),手感才贴手。
|
|
12860
|
+
dshwvWarm([
|
|
12861
|
+
pEmpty ? '' : '/dsh-whale/sound/press.mp3?set=' + soundSet,
|
|
12862
|
+
rEmpty ? '' : '/dsh-whale/sound/release.mp3?set=' + soundSet,
|
|
12863
|
+
])
|
|
12663
12864
|
} catch (err) {}
|
|
12664
12865
|
}
|
|
12665
12866
|
function playPress() {
|
|
@@ -12668,7 +12869,6 @@ function playPress() {
|
|
|
12668
12869
|
// 原实现里 pressAudio 为空时直接 return,跳过了 releasePlayed = false;而 playRelease()
|
|
12669
12870
|
// 一旦把 releasePlayed 置为 true 就再没有任何地方复位它 → 结果是只有第一次松开有声音,
|
|
12670
12871
|
// 之后每次点击都静音(用户实测:新建音效组只填松开音时复现)。
|
|
12671
|
-
if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null }
|
|
12672
12872
|
if (releaseAudio) {
|
|
12673
12873
|
releaseAudio.pause()
|
|
12674
12874
|
releaseAudio.currentTime = 0
|
|
@@ -12692,6 +12892,13 @@ function playPress() {
|
|
|
12692
12892
|
if (p && typeof p.catch === 'function') p.catch(function () {})
|
|
12693
12893
|
} catch (err) {}
|
|
12694
12894
|
}
|
|
12895
|
+
// 点按时"松开音提前多少毫秒进场"(v745 把它做成**可调参数**,按耳朵微调即可):
|
|
12896
|
+
// 0 = 松开音正好接在按压音结束那一刻(无缝、不重叠)
|
|
12897
|
+
// 30 / 50 = 轻微交叠(更"黏")
|
|
12898
|
+
// 100 = 明显重叠(听感上可能像"重复播放"。0.3.0 代码里写的是 100,但它的主线程 setTimeout
|
|
12899
|
+
// 经常迟到、实际几乎听不到重叠;我们用音频线程精确排期,所以别照抄 100)
|
|
12900
|
+
// 当前取值:**40**(用户在 30/50 之间试听后选定)
|
|
12901
|
+
var RELEASE_LEAD_MS = 40
|
|
12695
12902
|
function playRelease() {
|
|
12696
12903
|
if (releasePlayed || !releaseAudio || !soundOn) return
|
|
12697
12904
|
releasePlayed = true
|
|
@@ -12701,6 +12908,20 @@ function playRelease() {
|
|
|
12701
12908
|
if (p && typeof p.catch === 'function') p.catch(function () {})
|
|
12702
12909
|
} catch (err) {}
|
|
12703
12910
|
}
|
|
12911
|
+
// 把松开音**排期到 delaySec 秒之后**(音频线程时间轴):起播时刻精确、不受主线程抖动影响。
|
|
12912
|
+
function playReleaseAt(delaySec) {
|
|
12913
|
+
if (releasePlayed || !releaseAudio || !soundOn) return
|
|
12914
|
+
releasePlayed = true
|
|
12915
|
+
try {
|
|
12916
|
+
releaseAudio.currentTime = 0 // 复位(同时停掉上一条已排期/在播的松开音)
|
|
12917
|
+
if (delaySec > 0 && typeof releaseAudio.playAt === 'function') {
|
|
12918
|
+
releaseAudio.playAt(delaySec)
|
|
12919
|
+
return
|
|
12920
|
+
}
|
|
12921
|
+
var p = releaseAudio.play()
|
|
12922
|
+
if (p && typeof p.catch === 'function') p.catch(function () {})
|
|
12923
|
+
} catch (err) {}
|
|
12924
|
+
}
|
|
12704
12925
|
function pressDown() {
|
|
12705
12926
|
body.style.transform = SQUISH
|
|
12706
12927
|
pressing = true
|
|
@@ -12714,23 +12935,21 @@ function pressUp() {
|
|
|
12714
12935
|
playRelease()
|
|
12715
12936
|
return
|
|
12716
12937
|
}
|
|
12717
|
-
// click
|
|
12938
|
+
// click:把松开音排到"按压音结束前 RELEASE_LEAD_MS 毫秒"(默认 0 = 正好接上)
|
|
12718
12939
|
var durKnown = false
|
|
12719
|
-
var
|
|
12940
|
+
var remainSec = 0
|
|
12720
12941
|
try {
|
|
12721
12942
|
var dur = pressAudio ? pressAudio.duration : 0
|
|
12722
12943
|
if (isFinite(dur) && dur > 0) {
|
|
12723
12944
|
durKnown = true
|
|
12724
|
-
|
|
12945
|
+
remainSec = Math.max(0, dur - pressAudio.currentTime)
|
|
12725
12946
|
}
|
|
12726
12947
|
} catch (err) {}
|
|
12727
12948
|
if (durKnown) {
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
playRelease()
|
|
12731
|
-
}, Math.max(0, remainMs - 100))
|
|
12949
|
+
playReleaseAt(Math.max(0, remainSec - RELEASE_LEAD_MS / 1000))
|
|
12950
|
+
return
|
|
12732
12951
|
}
|
|
12733
|
-
//
|
|
12952
|
+
// 时长未知(预热失败/解码异常)→ 交给 pressAudio.onended 兜底(见 playPress)
|
|
12734
12953
|
}
|
|
12735
12954
|
var menuOpen = false
|
|
12736
12955
|
var menuClosedAt = 0 // 最近一次关闭菜单的时刻(用于避免"关掉后同一次手势又把它长按打开")
|
|
@@ -13218,6 +13437,9 @@ function openCropModal(dataUrl, fileName) {
|
|
|
13218
13437
|
cropAngle.value = '0'
|
|
13219
13438
|
cropAngleNum.value = '0'
|
|
13220
13439
|
positionCrop()
|
|
13440
|
+
// v744:裁剪窗口既可能从主菜单打开,也可能从资源管理(20300)/泡泡编辑器(20500)里打开,
|
|
13441
|
+
// 固定 20000 在后者会被父窗口盖住 → 动态抬层(见 visibleTopZ 上方的分层表)
|
|
13442
|
+
dshwLayerUp(cropMask, 20000)
|
|
13221
13443
|
cropMask.style.display = 'flex'
|
|
13222
13444
|
}
|
|
13223
13445
|
imgEl.onerror = function () {}
|
|
@@ -13668,10 +13890,10 @@ function hideAudioEditor() {
|
|
|
13668
13890
|
}
|
|
13669
13891
|
// 组编辑弹窗试听:完全模拟挂件按压交互。
|
|
13670
13892
|
// pointerdown(按下)→ 播放按压片段,进入"按住"状态(按压音播完仍按着则静默等待,不重复);
|
|
13671
|
-
// pointerup/cancel/leave(松开)→
|
|
13893
|
+
// pointerup/cancel/leave(松开)→ 若按压音已结束则立即播松开片段,
|
|
13894
|
+
// 否则把松开音**排期到按压音正好放完**的那一刻(音频线程排期;与挂件本体同一套做法,不重叠)。
|
|
13672
13895
|
var audioEditPreviewEl = null
|
|
13673
13896
|
var audioEditPreviewRelease = null
|
|
13674
|
-
var audioEditPreviewTimer = null
|
|
13675
13897
|
var audioEditPreviewReady = false
|
|
13676
13898
|
var audioEditPreviewPressing = false // 按住状态
|
|
13677
13899
|
var audioEditPreviewPressEnded = false // 按压音已播完
|
|
@@ -13699,6 +13921,11 @@ function audioEditPreviewEnsure(force) {
|
|
|
13699
13921
|
audioEditPreviewRelease.preload = 'auto'
|
|
13700
13922
|
audioEditPreviewRelease.volume = soundVol
|
|
13701
13923
|
}
|
|
13924
|
+
// v745:试听也要预热(否则第一次点按试听同样是"按下音慢半拍")
|
|
13925
|
+
dshwvWarm([
|
|
13926
|
+
pressId ? '/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(pressId) : '',
|
|
13927
|
+
releaseId ? '/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(releaseId) : '',
|
|
13928
|
+
])
|
|
13702
13929
|
audioEditPreviewReady = true
|
|
13703
13930
|
return true
|
|
13704
13931
|
} catch (err) { return false }
|
|
@@ -13715,8 +13942,11 @@ function audioEditPreviewDown() {
|
|
|
13715
13942
|
// 按下:播放按压音
|
|
13716
13943
|
audioEditPreviewEl.onended = function () {
|
|
13717
13944
|
audioEditPreviewPressEnded = true
|
|
13718
|
-
// fallback
|
|
13719
|
-
|
|
13945
|
+
// fallback:松手发生在按压音结束**之前**(= 直接点按)时,按压音播完立刻补上松开音。
|
|
13946
|
+
// ⚠️ 这里必须直接调 audioEditPreviewPlayRelease(),**不能**调 audioEditPreviewUp():
|
|
13947
|
+
// 后者开头是 `if (!audioEditPreviewPressing) return`,而点按松手时 pressing 已经是 false
|
|
13948
|
+
// → 直接 return,松开音永远不响(这就是"直接点按只有按下音、长按才听到松开音"的根因)。
|
|
13949
|
+
if (!audioEditPreviewPressing && !audioEditPreviewReleasePlayed) audioEditPreviewPlayRelease()
|
|
13720
13950
|
}
|
|
13721
13951
|
var p = audioEditPreviewEl.play()
|
|
13722
13952
|
if (p && typeof p.catch === 'function') p.catch(function () {})
|
|
@@ -13732,38 +13962,42 @@ function audioEditPreviewUp() {
|
|
|
13732
13962
|
audioEditPreviewPlayRelease()
|
|
13733
13963
|
return
|
|
13734
13964
|
}
|
|
13735
|
-
//
|
|
13965
|
+
// 快速点击(松手时按压音未播完):与本体一致 —— 排到"按压音结束前 RELEASE_LEAD_MS"
|
|
13966
|
+
// (默认 0 = 正好接上;音频线程排期,不依赖主线程定时器)
|
|
13736
13967
|
var durKnown = false
|
|
13737
|
-
var
|
|
13968
|
+
var remainSec = 0
|
|
13738
13969
|
try {
|
|
13739
13970
|
var dur = audioEditPreviewEl ? audioEditPreviewEl.duration : 0
|
|
13740
13971
|
if (isFinite(dur) && dur > 0) {
|
|
13741
13972
|
durKnown = true
|
|
13742
|
-
|
|
13973
|
+
remainSec = Math.max(0, dur - audioEditPreviewEl.currentTime)
|
|
13743
13974
|
}
|
|
13744
13975
|
} catch (err) {}
|
|
13745
13976
|
if (durKnown) {
|
|
13746
|
-
|
|
13747
|
-
|
|
13748
|
-
audioEditPreviewPlayRelease()
|
|
13749
|
-
}, Math.max(0, remainMs - 100))
|
|
13977
|
+
audioEditPreviewPlayRelease(Math.max(0, remainSec - RELEASE_LEAD_MS / 1000))
|
|
13978
|
+
return
|
|
13750
13979
|
}
|
|
13751
13980
|
// duration 未知 → onended fallback 播放松开
|
|
13752
13981
|
} catch (err) {}
|
|
13753
13982
|
}
|
|
13754
|
-
|
|
13983
|
+
// 兜底里必须**直接**调它(不能调 audioEditPreviewUp() —— 后者以 pressing 为前置,
|
|
13984
|
+
// 点按松手时已为 false 会直接 return,导致"直接点按只有按下音")。delaySec>0 时走音频线程排期。
|
|
13985
|
+
function audioEditPreviewPlayRelease(delaySec) {
|
|
13755
13986
|
try {
|
|
13756
13987
|
if (audioEditPreviewReleasePlayed || !audioEditPreviewRelease) return
|
|
13757
13988
|
audioEditPreviewReleasePlayed = true
|
|
13758
13989
|
audioEditPreviewRelease.currentTime = 0
|
|
13990
|
+
if (delaySec > 0 && typeof audioEditPreviewRelease.playAt === 'function') {
|
|
13991
|
+
audioEditPreviewRelease.playAt(delaySec)
|
|
13992
|
+
return
|
|
13993
|
+
}
|
|
13759
13994
|
var p = audioEditPreviewRelease.play()
|
|
13760
13995
|
if (p && typeof p.catch === 'function') p.catch(function () {})
|
|
13761
13996
|
} catch (err) {}
|
|
13762
13997
|
}
|
|
13763
13998
|
function stopAudioEditPreview() {
|
|
13764
13999
|
try {
|
|
13765
|
-
|
|
13766
|
-
// 不销毁 Audio 元素(保持预加载状态,与挂件持久 Audio 一致),只暂停复位
|
|
14000
|
+
// v745:松开音改由音频线程排期,没有主线程定时器要清
|
|
13767
14001
|
if (audioEditPreviewEl) {
|
|
13768
14002
|
audioEditPreviewEl.pause()
|
|
13769
14003
|
audioEditPreviewEl.currentTime = 0
|
|
@@ -13819,7 +14053,7 @@ var audioCropFileInput = document.createElement('input')
|
|
|
13819
14053
|
audioCropFileInput.type = 'file'
|
|
13820
14054
|
audioCropFileInput.accept = 'audio/*'
|
|
13821
14055
|
audioCropFileInput.style.display = 'none'
|
|
13822
|
-
|
|
14056
|
+
dshwBodyAppend(audioCropFileInput)
|
|
13823
14057
|
var audioCropTarget = null // 'press' | 'release'
|
|
13824
14058
|
var audioCropCtx = null // AudioContext
|
|
13825
14059
|
var audioCropBuffer = null // AudioBuffer
|
|
@@ -13866,6 +14100,7 @@ function openAudioCrop(arrayBuf, fileName) {
|
|
|
13866
14100
|
drawAudioCrop()
|
|
13867
14101
|
try { audioCropName.value = '' } catch (err) {}
|
|
13868
14102
|
updateAudioCropOkState()
|
|
14103
|
+
dshwLayerUp(audioCropMask, 20500) // v744:音频裁剪也可能从资源管理(20300)里打开
|
|
13869
14104
|
audioCropMask.style.display = 'flex'
|
|
13870
14105
|
}, function () { alert('音频解码失败') })
|
|
13871
14106
|
} catch (err) {}
|
package/lib/index.js
CHANGED
|
@@ -434,10 +434,15 @@ const RUA_GIF_CANDIDATES = [
|
|
|
434
434
|
path.join(PACKAGE_ROOT, 'assets', 'rua.gif'),
|
|
435
435
|
]
|
|
436
436
|
// DeepSeek CNY prices per million tokens: [空闲时段价, 高峰时段价].
|
|
437
|
-
//
|
|
437
|
+
// 高峰时段(官方 2026-09-19 说明,见 docs 脚注 (2)):**北京时间周一至周五(不含中国法定节假日)
|
|
438
|
+
// 9:00–12:00、14:00–18:00**;其余时段 —— 包括**周末、调休上班的周末、以及中国法定节假日全天** ——
|
|
439
|
+
// 一律按空闲时段计费。
|
|
440
|
+
// 时间线:2026-08-17 峰谷定价正式实施 → 2026-08-23 00:00 起周末全天谷价 →
|
|
441
|
+
// 2026-09-19 明确「调休上班的周末 + 法定节假日全天」也按空闲计。
|
|
438
442
|
// 价格来源:官方 https://api-docs.deepseek.com/zh-cn/quick_start/pricing
|
|
439
443
|
// 2026-09-10 起 Flash 系列降价:缓存命中 0.05→0.02、未命中 1.5→1、输出 4.5→4(高峰=空闲×2)。
|
|
440
|
-
// 2026-09-
|
|
444
|
+
// 2026-09-19 复核:价格数字与上表一致(Flash 0.02/1/4 与 0.04/2/8;Pro 0.15/4.5/13.5 与 0.30/9/27),
|
|
445
|
+
// 峰谷规则新增「法定节假日全天谷价」这一条,见下方 HOLIDAY_VALLEY。
|
|
441
446
|
const PEAK_HOURS = [
|
|
442
447
|
[9, 12],
|
|
443
448
|
[14, 18],
|
|
@@ -483,6 +488,37 @@ function priceFor(model) {
|
|
|
483
488
|
// 2026-08-23 起(北京时间)周末(周六/周日)全天按谷价;生效时刻之前的历史
|
|
484
489
|
// 分桶仍按旧规则计价,所以周末判定带生效分界。
|
|
485
490
|
const WEEKEND_VALLEY_FROM_SEC = Math.floor(Date.UTC(2026, 7, 22, 16, 0, 0) / 1000) // = 北京时间 2026-08-23 00:00
|
|
491
|
+
// ===== 法定节假日全天谷价(官方 2026-09-19 说明)=====
|
|
492
|
+
// 依据:《国务院办公厅关于 2026 年部分节假日安排的通知》(国办发明电〔2025〕7 号,2025-11-04)
|
|
493
|
+
// + DeepSeek API 官方计费脚注「周一至周五(不含中国法定节假日)…其余时段,包括周末及
|
|
494
|
+
// 中国法定节假日全天均为空闲时段」。
|
|
495
|
+
// 只需列**放假**的日期:调休上班日全部落在周末(2026 年为 1/4、2/14、2/28、5/9、9/20、10/10),
|
|
496
|
+
// 按"周末也算谷价"的规则本来就是谷价,无需单列。
|
|
497
|
+
// ⚠️ **每年 11 月国务院发布次年安排后,必须在这里补下一年的日期**(探针 `_peak-holiday-check.mjs`
|
|
498
|
+
// 会检查当年是否已覆盖并给出提醒)。
|
|
499
|
+
const HOLIDAY_VALLEY = {
|
|
500
|
+
'2026-01-01': 1, '2026-01-02': 1, '2026-01-03': 1, // 元旦 1/1–1/3(1/4 周日上班)
|
|
501
|
+
'2026-02-15': 1, '2026-02-16': 1, '2026-02-17': 1, '2026-02-18': 1, '2026-02-19': 1, // 春节 2/15–2/23(9 天)
|
|
502
|
+
'2026-02-20': 1, '2026-02-21': 1, '2026-02-22': 1, '2026-02-23': 1,
|
|
503
|
+
'2026-04-04': 1, '2026-04-05': 1, '2026-04-06': 1, // 清明 4/4–4/6
|
|
504
|
+
'2026-05-01': 1, '2026-05-02': 1, '2026-05-03': 1, '2026-05-04': 1, '2026-05-05': 1, // 劳动节 5/1–5/5(5/9 周六上班)
|
|
505
|
+
'2026-06-19': 1, '2026-06-20': 1, '2026-06-21': 1, // 端午 6/19–6/21
|
|
506
|
+
'2026-09-25': 1, '2026-09-26': 1, '2026-09-27': 1, // 中秋 9/25–9/27
|
|
507
|
+
'2026-10-01': 1, '2026-10-02': 1, '2026-10-03': 1, '2026-10-04': 1, // 国庆 10/1–10/7(9/20 周日、10/10 周六上班)
|
|
508
|
+
'2026-10-05': 1, '2026-10-06': 1, '2026-10-07': 1,
|
|
509
|
+
}
|
|
510
|
+
// 这条规则自 2026-09-19 起生效(此前历史分桶按旧规则计价)。2026 年内落在 8/17(峰谷机制实施)
|
|
511
|
+
// 与 9/19 之间没有任何"工作日的法定节假日",所以对 2026 年的账目没有影响;次年(2027)之后
|
|
512
|
+
// 整年的节假日都会走到这个分支。
|
|
513
|
+
const HOLIDAY_VALLEY_FROM_SEC = Math.floor(Date.UTC(2026, 8, 18, 16, 0, 0) / 1000) // = 北京时间 2026-09-19 00:00
|
|
514
|
+
// 下发给前端(挂件)的节假日清单:前端「时段倒计时」也要按节假日算下一切换点,
|
|
515
|
+
// 而前端无法自己知道法定安排,所以由宿主单一来源下发。每次 /dsh-whale/balance 都会带上。
|
|
516
|
+
const HOLIDAY_VALLEY_LIST = Object.keys(HOLIDAY_VALLEY).sort()
|
|
517
|
+
function isHolidayValley(bjDate) {
|
|
518
|
+
try {
|
|
519
|
+
return !!HOLIDAY_VALLEY[bjDate.toISOString().slice(0, 10)]
|
|
520
|
+
} catch (err) { return false }
|
|
521
|
+
}
|
|
486
522
|
function isPeakTime(timeSec) {
|
|
487
523
|
if (!isFinite(Number(timeSec))) return false
|
|
488
524
|
const n = Number(timeSec)
|
|
@@ -491,12 +527,32 @@ function isPeakTime(timeSec) {
|
|
|
491
527
|
const dow = bj.getUTCDay() // 0=周日 6=周六(bj 按 UTC 读即为北京日历日)
|
|
492
528
|
if (dow === 0 || dow === 6) return false
|
|
493
529
|
}
|
|
530
|
+
// 法定节假日全天谷价(含调休放假的工作日)
|
|
531
|
+
if (n >= HOLIDAY_VALLEY_FROM_SEC && isHolidayValley(bj)) return false
|
|
494
532
|
const hour = bj.getUTCHours()
|
|
495
533
|
for (const [start, end] of PEAK_HOURS) {
|
|
496
534
|
if (hour >= start && hour < end) return true
|
|
497
535
|
}
|
|
498
536
|
return false
|
|
499
537
|
}
|
|
538
|
+
// 下一个峰谷切换时刻(epoch 秒):与 isPeakTime 完全同源(含周末 + 法定节假日规则)。
|
|
539
|
+
// 只需扫北京时间的小时边界 0/9/12/14/18 点;最长假期(春节 9 天)也只到第 9 天,
|
|
540
|
+
// 这里扫 12 天留足余量。返回 null 表示 12 天内没有切换点(当前规则下不会发生),
|
|
541
|
+
// 由前端回退到自己的本地推算。
|
|
542
|
+
function nextPeakChangeAt(timeSec) {
|
|
543
|
+
const n = Number(timeSec)
|
|
544
|
+
if (!isFinite(n)) return null
|
|
545
|
+
const cur = isPeakTime(n)
|
|
546
|
+
const day0 = Math.floor((n + 8 * 3600) / 86400) * 86400 // 北京当日 00:00(在 +8h 平移坐标系里)
|
|
547
|
+
for (let d = 0; d <= 12; d++) {
|
|
548
|
+
for (const edge of [0, 9, 12, 14, 18]) {
|
|
549
|
+
const cand = day0 + d * 86400 + edge * 3600 - 8 * 3600
|
|
550
|
+
if (cand <= n + 1) continue
|
|
551
|
+
if (isPeakTime(cand) !== cur) return cand
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return null
|
|
555
|
+
}
|
|
500
556
|
|
|
501
557
|
const JSON_HEADERS = {
|
|
502
558
|
'Content-Type': 'application/json; charset=utf-8',
|
|
@@ -929,7 +985,7 @@ export default {
|
|
|
929
985
|
const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
|
|
930
986
|
for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
|
|
931
987
|
return {
|
|
932
|
-
ok: true, version: '0.3.
|
|
988
|
+
ok: true, version: '0.3.8', today: todayData, days7,
|
|
933
989
|
total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
|
|
934
990
|
all: {
|
|
935
991
|
days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
|
|
@@ -1312,20 +1368,30 @@ export default {
|
|
|
1312
1368
|
let codexCatchupT = null
|
|
1313
1369
|
// 配置变更(例如用户在设置里关掉 Codex 统计)后必须让快照失效,
|
|
1314
1370
|
// 否则下一次请求还会命中关闭前的旧快照、开关看起来"没生效"。
|
|
1315
|
-
function codexInvalidate() { codexSnap = null; codexSnapAt = 0 }
|
|
1371
|
+
function codexInvalidate() { codexSnap = null; codexSnapAt = 0; codexStatsOnV = null; codexStatsOnAt = 0 }
|
|
1372
|
+
// 开关读取加 2 秒缓存:codexBackgroundRefresh 可能被高频调用(快照过期时每次 payload
|
|
1373
|
+
// 构建都会试一次),而 readSizeConfig() 是同步文件读 —— 不能放在热路径上。
|
|
1374
|
+
// 用户改设置时 codexInvalidate() 会清掉缓存,所以开关仍然是"立刻生效"。
|
|
1375
|
+
let codexStatsOnV = null, codexStatsOnAt = 0
|
|
1316
1376
|
function codexStatsOn() {
|
|
1377
|
+
const now = Date.now()
|
|
1378
|
+
if (codexStatsOnV !== null && now - codexStatsOnAt < 2000) return codexStatsOnV
|
|
1379
|
+
let v = true
|
|
1317
1380
|
try {
|
|
1318
1381
|
const cfg = readSizeConfig()
|
|
1319
|
-
|
|
1320
|
-
} catch (err) {
|
|
1382
|
+
v = !cfg || cfg.codexStatsOn !== false
|
|
1383
|
+
} catch (err) { v = true }
|
|
1384
|
+
codexStatsOnV = v
|
|
1385
|
+
codexStatsOnAt = now
|
|
1386
|
+
return v
|
|
1321
1387
|
}
|
|
1322
1388
|
function codexBackgroundRefresh(delayMs) {
|
|
1389
|
+
if (codexScanP) return codexScanP // 已在扫:先返回,避免重复读配置/重复排队
|
|
1323
1390
|
if (!codexStatsOn()) {
|
|
1324
1391
|
codexSnap = { ok: false, disabled: true, error: 'Codex 统计已在设置里关闭' }
|
|
1325
1392
|
codexSnapAt = Date.now()
|
|
1326
1393
|
return null
|
|
1327
1394
|
}
|
|
1328
|
-
if (codexScanP) return codexScanP
|
|
1329
1395
|
if (delayMs > 0) {
|
|
1330
1396
|
if (codexCatchupT) return null
|
|
1331
1397
|
codexCatchupT = setTimeout(() => { codexCatchupT = null; codexBackgroundRefresh(0) }, delayMs)
|
|
@@ -2093,8 +2159,12 @@ export default {
|
|
|
2093
2159
|
const { accountTag, ...visible } = payload
|
|
2094
2160
|
const led = readUsageLedger()
|
|
2095
2161
|
const summary = daySummary(led, todayKey())
|
|
2162
|
+
const nowSec = Math.floor(Date.now() / 1000)
|
|
2096
2163
|
return {
|
|
2097
|
-
...visible, version: '0.3.
|
|
2164
|
+
...visible, version: '0.3.8', isPeak: isPeakTime(nowSec),
|
|
2165
|
+
// 峰谷切换点与节假日清单:前端倒计时要与宿主同源(否则法定节假日会算错切换点)
|
|
2166
|
+
peakNextChangeAt: nextPeakChangeAt(nowSec),
|
|
2167
|
+
peakHolidays: HOLIDAY_VALLEY_LIST,
|
|
2098
2168
|
todayUsage: summary.amount, todayUsageCurrency: summary.currency,
|
|
2099
2169
|
usageSource: summary.source, usageLabel: summary.label, usageMode: 'ledger',
|
|
2100
2170
|
accounting: balanceSummary(led, todayKey()),
|
package/package.json
CHANGED