dsh-whale-widget 0.3.0 → 0.3.2
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/PROVENANCE.md +35 -0
- package/README.md +29 -11
- package/assets/DSH2.png +0 -0
- package/assets/whale-widget.js +753 -133
- package/lib/accounting.mjs +187 -0
- package/lib/index.js +248 -207
- package/package.json +3 -2
package/assets/whale-widget.js
CHANGED
|
@@ -293,7 +293,7 @@ var css = [
|
|
|
293
293
|
'.dshwv-usagepanel{position:fixed;z-index:26020;background:#fff;border:1px solid rgba(32,49,112,.35);border-radius:10px;box-shadow:0 8px 22px rgba(15,23,42,.22);padding:10px 12px;color-scheme:light;max-height:70vh;overflow-y:auto}',
|
|
294
294
|
// 用量作为主菜单内的子界面
|
|
295
295
|
'.dshwv-menuview{display:block}',
|
|
296
|
-
'.dshwv-usage-sub{display:none;max-height:min(70vh,560px);overflow-y:auto;padding-right:2px;width:100%;box-sizing:border-box}',
|
|
296
|
+
'.dshwv-usage-sub{display:none;max-height:min(70vh,560px);overflow-y:auto;padding-right:2px;padding-bottom:12px;width:100%;box-sizing:border-box}',
|
|
297
297
|
'.dshwv-usage-back{border:none;background:none;color:#203170;font-size:12px;font-weight:600;cursor:pointer;padding:0 0 2px;text-align:left;width:100%}',
|
|
298
298
|
'.dshwv-usage-back:hover{color:#2f4488;text-decoration:underline}',
|
|
299
299
|
'.dshwv-usagebody{display:flex;flex-direction:column;gap:2px;color:#203170;font-size:12px;min-width:0;overflow-x:hidden}',
|
|
@@ -1475,7 +1475,7 @@ function hideUsageSub() {
|
|
|
1475
1475
|
}, 230)
|
|
1476
1476
|
}
|
|
1477
1477
|
function closeUsagePanel() { hideUsageSub() }
|
|
1478
|
-
function usageMoney(x) { return
|
|
1478
|
+
function usageMoney(x, currency) { return apiFmtMoney(x, currency || 'CNY') }
|
|
1479
1479
|
function usageDayLabel(day) {
|
|
1480
1480
|
try {
|
|
1481
1481
|
var d = day.split('-')
|
|
@@ -1870,6 +1870,13 @@ function apiModelById(id) {
|
|
|
1870
1870
|
for (var i = 0; i < apiModels.length; i++) if (apiModels[i] && apiModels[i].id === id) return apiModels[i]
|
|
1871
1871
|
return null
|
|
1872
1872
|
}
|
|
1873
|
+
// 「充值 / 余额校正」只属于固定的 DeepSeek(内置):宿主在模型条目里下发 canAdjustBalance,
|
|
1874
|
+
// 前端据此决定要不要在设置菜单里给出入口 —— 手动新增的同名模型、Kimi 等其它厂商都不会有,
|
|
1875
|
+
// 新增模板也不会自动继承(宿主还会在路由层再校验一次,见 balance-adjustments.json)。
|
|
1876
|
+
function apiCanAdjustBalance(model) {
|
|
1877
|
+
return !!(model && model.id === 'deepseek' && model.builtin === true &&
|
|
1878
|
+
model.provider === 'deepseek' && model.canAdjustBalance === true)
|
|
1879
|
+
}
|
|
1873
1880
|
// 今日已用金额自带的币种(host 下发的 todayUsageCurrency):
|
|
1874
1881
|
// 会话事件金额在 host 已按自定义单价折算成人民币,余额差则是厂商币种 → 显示必须按各自的币种,
|
|
1875
1882
|
// 否则 USD 单价的模型会把人民币数值渲染成 $。
|
|
@@ -1901,7 +1908,11 @@ function apiFmtMoney(v, cur) {
|
|
|
1901
1908
|
function apiUsageSourceLabel(src) {
|
|
1902
1909
|
var s = String(src || '')
|
|
1903
1910
|
if (s === 'ledger' || s === 'balance') return '余额差记账'
|
|
1904
|
-
if (s === 'events') return '
|
|
1911
|
+
if (s === 'events') return '本地估算'
|
|
1912
|
+
if (s === 'balance-observed') return '已观测消费'
|
|
1913
|
+
if (s === 'balance-needs-review') return '待核对余额调整'
|
|
1914
|
+
if (s === 'balance-corrected') return '已校正消费'
|
|
1915
|
+
if (s === 'legacy') return '旧版记录 · 未校正'
|
|
1905
1916
|
return ''
|
|
1906
1917
|
}
|
|
1907
1918
|
// 模型列表加载:并发合并 + 失败退避重试 + 失败态可重试。
|
|
@@ -2796,7 +2807,7 @@ function openApiModelMenu(modelId) {
|
|
|
2796
2807
|
else st.textContent = '余额 ' + apiFmtMoney(m.balance, m.currency) + ' · 今日已用 ' + apiFmtMoney(m.todayUsage, apiTodayCur(m))
|
|
2797
2808
|
card.appendChild(st)
|
|
2798
2809
|
var ms = (usageSet && usageSet.models && usageSet.models[modelId]) || {}
|
|
2799
|
-
function rowOf(label, stateFn, onEdit) {
|
|
2810
|
+
function rowOf(label, stateFn, onEdit, buttonLabel) {
|
|
2800
2811
|
var r = document.createElement('div')
|
|
2801
2812
|
r.className = 'dshwv-audiorow'
|
|
2802
2813
|
var l = document.createElement('span')
|
|
@@ -2813,8 +2824,9 @@ function openApiModelMenu(modelId) {
|
|
|
2813
2824
|
info.style.textOverflow = 'ellipsis'
|
|
2814
2825
|
info.textContent = stateFn()
|
|
2815
2826
|
r.appendChild(info)
|
|
2816
|
-
r.appendChild(apiBtn('编辑', 'dshwv-roleimport', onEdit))
|
|
2827
|
+
r.appendChild(apiBtn(buttonLabel || '编辑', 'dshwv-roleimport', onEdit))
|
|
2817
2828
|
card.appendChild(r)
|
|
2829
|
+
return r
|
|
2818
2830
|
}
|
|
2819
2831
|
rowOf('余额预警', function () {
|
|
2820
2832
|
var a = ms.alert
|
|
@@ -2871,13 +2883,15 @@ function openApiModelMenu(modelId) {
|
|
|
2871
2883
|
card.appendChild(pr)
|
|
2872
2884
|
}
|
|
2873
2885
|
// 只读行(复用现有 .dshwv-audiorow / .dshwv-usage-hint,不引入新颜色与字体)
|
|
2874
|
-
function readonlyRow(label, text) {
|
|
2886
|
+
function readonlyRow(label, text, hintHtml) {
|
|
2875
2887
|
var r = document.createElement('div')
|
|
2876
2888
|
r.className = 'dshwv-audiorow'
|
|
2877
2889
|
var l = document.createElement('span')
|
|
2878
2890
|
l.textContent = label
|
|
2879
2891
|
l.style.flex = '0 0 auto'
|
|
2880
2892
|
r.appendChild(l)
|
|
2893
|
+
// 需要「?」说明时,说明收进圆圈(悬停显示、点击钉住),与挂件其它说明圈同一套组件
|
|
2894
|
+
if (hintHtml) { try { r.appendChild(dshwvAskDot(hintHtml)) } catch (err) {} }
|
|
2881
2895
|
var v = document.createElement('span')
|
|
2882
2896
|
v.className = 'dshwv-usage-hint'
|
|
2883
2897
|
v.style.flex = '1'
|
|
@@ -2910,6 +2924,32 @@ function openApiModelMenu(modelId) {
|
|
|
2910
2924
|
pTxt = '未设置(沿用内置价目表)→ 在「密钥 / 接口」里填写'
|
|
2911
2925
|
}
|
|
2912
2926
|
readonlyRow('单价', pTxt)
|
|
2927
|
+
// 「已观测消费」= DeepSeek 账户口径(余额观测),只在内置项的设置里显示,与下面的「余额校正」配套。
|
|
2928
|
+
if (apiCanAdjustBalance(m)) {
|
|
2929
|
+
var acc = m.accounting || null
|
|
2930
|
+
var accAmt = (acc && typeof acc.amount === 'number') ? acc.amount : m.todayUsage
|
|
2931
|
+
var accInfo = (acc && acc.firstObservedAt)
|
|
2932
|
+
? '统计起点(北京):' + accountingTime(acc.firstObservedAt) + '。起点前的消费未计入;该账户的观测包含同一个 key 在别处的消费。'
|
|
2933
|
+
: '尚无余额观测:先配置 DeepSeek API key 并成功刷新一次余额。'
|
|
2934
|
+
readonlyRow('已观测消费', ((acc && acc.label) || m.usageLabel || '已观测消费') + ' ' +
|
|
2935
|
+
(isFinite(Number(accAmt)) ? apiFmtMoney(accAmt, apiTodayCur(m)) : '--'), accInfo)
|
|
2936
|
+
// 需要用户动手的提示仍然直接显示(不藏进「?」里)
|
|
2937
|
+
if (acc && acc.needsReview) {
|
|
2938
|
+
var accWarn = document.createElement('div')
|
|
2939
|
+
accWarn.className = 'dshwv-usage-hint'
|
|
2940
|
+
accWarn.style.cssText = 'line-height:1.65;white-space:normal;margin:2px 0 4px'
|
|
2941
|
+
accWarn.textContent = '检测到余额增加,请用下面的「余额校正」核对本区间累计到账。'
|
|
2942
|
+
card.appendChild(accWarn)
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
// 「充值 / 余额校正」入口:只给固定的 DeepSeek(内置),放在「单价」下方、沿用同一行布局与按钮样式。
|
|
2946
|
+
// 其它厂商、手动新增的 DeepSeek、仅改名为「DeepSeek(内置)」的模型都不会走到这里。
|
|
2947
|
+
if (apiCanAdjustBalance(m)) {
|
|
2948
|
+
var adjustmentRow = rowOf('余额校正', function () {
|
|
2949
|
+
return (m.accounting && m.accounting.label) || '充值与余额调整'
|
|
2950
|
+
}, function () { openBalanceAdjustment(modelId) }, '校正')
|
|
2951
|
+
adjustmentRow.lastElementChild.setAttribute('data-action', 'balance-adjustment')
|
|
2952
|
+
}
|
|
2913
2953
|
// 币种不一致提示:今日已用按「自带币种」显示(会话事件为 CNY)。若与模型币种不同且没填汇率,
|
|
2914
2954
|
// 今日预算提醒会被跳过(见 A 方案),这里给出可见的补救提示。
|
|
2915
2955
|
var tuc = String((m && m.todayUsageCurrency) || '').toUpperCase()
|
|
@@ -2977,9 +3017,9 @@ function refreshUsageMain() {
|
|
|
2977
3017
|
// 数值由“泡泡消失→下一次显示”的渲染自然采用最新 state
|
|
2978
3018
|
if (d && d.ok && d.today && isFinite(Number(d.today.total))) {
|
|
2979
3019
|
var recTotal = Number(d.today.total)
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3020
|
+
state.todayUsage = recTotal
|
|
3021
|
+
state.todayUsageCurrency = d.today.currency || 'CNY'
|
|
3022
|
+
state.usageLabel = d.today.label || '本地估算'
|
|
2983
3023
|
}
|
|
2984
3024
|
})
|
|
2985
3025
|
.catch(function () { if (usageMainEl && !usageMainEl.firstChild) usageMainEl.textContent = '记录加载失败' })
|
|
@@ -2996,6 +3036,233 @@ function uSectionTitle(leftTxt, rightTxt) {
|
|
|
2996
3036
|
h.appendChild(r)
|
|
2997
3037
|
return h
|
|
2998
3038
|
}
|
|
3039
|
+
function accountingTime(at) {
|
|
3040
|
+
try {
|
|
3041
|
+
return new Date(at).toLocaleString('zh-CN', {
|
|
3042
|
+
timeZone: 'Asia/Shanghai', hour12: false, year: 'numeric', month: '2-digit',
|
|
3043
|
+
day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
3044
|
+
})
|
|
3045
|
+
} catch (err) { return String(at || '') }
|
|
3046
|
+
}
|
|
3047
|
+
var accountingMask = null
|
|
3048
|
+
function openBalanceAdjustment(modelId) {
|
|
3049
|
+
// 只有固定的 DeepSeek(内置)能打开:入口由宿主下发的 canAdjustBalance 控制,这里再挡一次
|
|
3050
|
+
if (!apiCanAdjustBalance(apiModelById(modelId)) || accountingMask) return
|
|
3051
|
+
var adjustmentUrl = '/dsh-whale/balance-adjustments.json?modelId=' + encodeURIComponent(modelId)
|
|
3052
|
+
closeApiModelPanel()
|
|
3053
|
+
// 与「额度」「余额预警 / 今日预算」等窗口共用同一套骨架与样式:
|
|
3054
|
+
// dshwv-usage-mask + dshwv-usage-card + dshwv-bubtitle/bubhint + apiPanelRow/apiTextInput + dshwv-bubbtns
|
|
3055
|
+
var mask = document.createElement('div')
|
|
3056
|
+
mask.className = 'dshwv-usage-mask'
|
|
3057
|
+
mask.style.zIndex = '30000'
|
|
3058
|
+
var card = document.createElement('form')
|
|
3059
|
+
card.className = 'dshwv-usage-card'
|
|
3060
|
+
card.style.width = 'min(430px,94vw)'
|
|
3061
|
+
card.style.padding = '14px 16px'
|
|
3062
|
+
card.style.boxSizing = 'border-box'
|
|
3063
|
+
card.style.textAlign = 'left'
|
|
3064
|
+
card.setAttribute('role', 'dialog')
|
|
3065
|
+
card.setAttribute('aria-modal', 'true')
|
|
3066
|
+
card.setAttribute('aria-label', 'DeepSeek(内置)余额校正')
|
|
3067
|
+
// 内容可能比 82vh 高:中间这段自己滚,按钮行固定在底部(与 .dshwv-reswrap 同一做法)
|
|
3068
|
+
var body = document.createElement('div')
|
|
3069
|
+
body.style.cssText = 'min-height:0;overflow-y:auto;flex:1 1 auto'
|
|
3070
|
+
var title = document.createElement('div')
|
|
3071
|
+
title.className = 'dshwv-bubtitle'
|
|
3072
|
+
title.textContent = '余额校正 · DeepSeek(内置)'
|
|
3073
|
+
body.appendChild(title)
|
|
3074
|
+
var introduction = document.createElement('div')
|
|
3075
|
+
introduction.className = 'dshwv-bubhint'
|
|
3076
|
+
introduction.style.cssText = 'margin:0 0 8px;white-space:normal;line-height:1.6;text-align:left'
|
|
3077
|
+
introduction.textContent = '核对统计区间内的全部到账后,重新计算本地消费。此操作不会充值,也不会改变 DeepSeek 账户余额。'
|
|
3078
|
+
body.appendChild(introduction)
|
|
3079
|
+
var dates = apiSelectEl([], '')
|
|
3080
|
+
body.appendChild(apiPanelRow('日期', dates))
|
|
3081
|
+
var interval = document.createElement('div')
|
|
3082
|
+
interval.className = 'dshwv-bubhint'
|
|
3083
|
+
interval.style.cssText = 'margin:0 0 8px;white-space:pre-line;line-height:1.6;text-align:left'
|
|
3084
|
+
body.appendChild(interval)
|
|
3085
|
+
var credits = apiTextInput('', '未到账请填 0')
|
|
3086
|
+
credits.inputMode = 'decimal'
|
|
3087
|
+
credits.required = true
|
|
3088
|
+
credits.autocomplete = 'off'
|
|
3089
|
+
body.appendChild(apiPanelRow('累计到账', credits))
|
|
3090
|
+
var creditsHint = document.createElement('div')
|
|
3091
|
+
creditsHint.className = 'dshwv-bubhint'
|
|
3092
|
+
creditsHint.style.cssText = 'margin:0 0 8px;white-space:normal;line-height:1.6;text-align:left'
|
|
3093
|
+
body.appendChild(creditsHint)
|
|
3094
|
+
var debits = apiTextInput('0', '没有请填 0')
|
|
3095
|
+
debits.inputMode = 'decimal'
|
|
3096
|
+
debits.autocomplete = 'off'
|
|
3097
|
+
body.appendChild(apiPanelRow('非调用扣减', debits))
|
|
3098
|
+
var debitsHint = document.createElement('div')
|
|
3099
|
+
debitsHint.className = 'dshwv-bubhint'
|
|
3100
|
+
debitsHint.style.cssText = 'margin:0 0 8px;white-space:normal;line-height:1.6;text-align:left'
|
|
3101
|
+
body.appendChild(debitsHint)
|
|
3102
|
+
var preview = document.createElement('div')
|
|
3103
|
+
preview.className = 'dshwv-bubhint'
|
|
3104
|
+
preview.style.cssText = 'margin:0 0 8px;white-space:normal;line-height:1.6;text-align:left;font-weight:700;color:#203170'
|
|
3105
|
+
preview.setAttribute('aria-live', 'polite')
|
|
3106
|
+
body.appendChild(preview)
|
|
3107
|
+
var confirmRow = document.createElement('label')
|
|
3108
|
+
confirmRow.style.cssText = 'display:flex;gap:6px;align-items:flex-start;flex:1;min-width:0;font-size:12px;color:#203170;cursor:pointer;line-height:1.5'
|
|
3109
|
+
var confirm = document.createElement('input')
|
|
3110
|
+
confirm.type = 'checkbox'
|
|
3111
|
+
confirm.style.marginTop = '1px'
|
|
3112
|
+
confirmRow.appendChild(confirm)
|
|
3113
|
+
confirmRow.appendChild(document.createTextNode('我已核对本统计区间的全部到账和非调用扣减'))
|
|
3114
|
+
body.appendChild(apiPanelRow('确认', confirmRow))
|
|
3115
|
+
var status = document.createElement('div')
|
|
3116
|
+
status.className = 'dshwv-bubhint'
|
|
3117
|
+
status.setAttribute('role', 'status')
|
|
3118
|
+
status.style.cssText = 'margin:0 0 6px;white-space:normal;line-height:1.6;text-align:left;color:#b33333'
|
|
3119
|
+
status.textContent = '正在刷新余额…'
|
|
3120
|
+
body.appendChild(status)
|
|
3121
|
+
card.appendChild(body)
|
|
3122
|
+
var busy = false
|
|
3123
|
+
function close() {
|
|
3124
|
+
if (busy) return
|
|
3125
|
+
document.removeEventListener('keydown', keyHandler)
|
|
3126
|
+
mask.remove()
|
|
3127
|
+
accountingMask = null
|
|
3128
|
+
// 关掉校正窗口后回到同一个设置菜单,并把焦点放回「校正」按钮
|
|
3129
|
+
openApiModelMenu(modelId)
|
|
3130
|
+
try {
|
|
3131
|
+
var returnButton = apiModelMaskEl && apiModelMaskEl.querySelector('[data-action="balance-adjustment"]')
|
|
3132
|
+
if (returnButton) returnButton.focus()
|
|
3133
|
+
} catch (err) {}
|
|
3134
|
+
}
|
|
3135
|
+
function keyHandler(e) {
|
|
3136
|
+
if (e.key === 'Escape') { e.preventDefault(); close() }
|
|
3137
|
+
if (e.key === 'Tab') {
|
|
3138
|
+
var focusable = Array.prototype.filter.call(card.querySelectorAll('button,input,select'), function (el) {
|
|
3139
|
+
return !el.disabled && el.style.display !== 'none'
|
|
3140
|
+
})
|
|
3141
|
+
var first = focusable[0], last = focusable[focusable.length - 1]
|
|
3142
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus() }
|
|
3143
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
// 按钮行与其它窗口完全一致:.dshwv-bubbtns + apiBtn 的标准按钮样式(不再自绘尺寸/配色)
|
|
3147
|
+
var actions = document.createElement('div')
|
|
3148
|
+
actions.className = 'dshwv-bubbtns'
|
|
3149
|
+
var reload = apiBtn('重新读取', 'dshwv-bubbtn dshwv-bubbtn-no', function () { load() })
|
|
3150
|
+
var reset = apiBtn('撤销该日校正', 'dshwv-bubbtn dshwv-bubbtn-no', function () { submit('reset') })
|
|
3151
|
+
var save = apiBtn('保存校正', 'dshwv-bubbtn dshwv-bubbtn-ok', function () { submit('save') })
|
|
3152
|
+
actions.appendChild(reload)
|
|
3153
|
+
actions.appendChild(reset)
|
|
3154
|
+
actions.appendChild(apiBtn('取消', 'dshwv-bubbtn dshwv-bubbtn-no', function () { close() }))
|
|
3155
|
+
actions.appendChild(save)
|
|
3156
|
+
card.appendChild(actions)
|
|
3157
|
+
mask.appendChild(card)
|
|
3158
|
+
mask.addEventListener('click', function (e) { if (e.target === mask) close() })
|
|
3159
|
+
document.body.appendChild(mask)
|
|
3160
|
+
accountingMask = mask
|
|
3161
|
+
document.addEventListener('keydown', keyHandler)
|
|
3162
|
+
var rows = []
|
|
3163
|
+
var selected = null
|
|
3164
|
+
function renderSelection() {
|
|
3165
|
+
selected = rows.find(function (row) { return row.day === dates.value }) || null
|
|
3166
|
+
save.disabled = !selected || busy
|
|
3167
|
+
reset.disabled = !selected || !selected.correctedAt || busy
|
|
3168
|
+
if (!selected) {
|
|
3169
|
+
interval.textContent = '尚无可校正的余额观测。请先配置 DeepSeek API key 并成功刷新余额。'
|
|
3170
|
+
creditsHint.textContent = ''
|
|
3171
|
+
debitsHint.textContent = ''
|
|
3172
|
+
return
|
|
3173
|
+
}
|
|
3174
|
+
interval.textContent = '统计起点:' + accountingTime(selected.firstObservedAt) +
|
|
3175
|
+
'\n最近观测:' + accountingTime(selected.lastObservedAt) +
|
|
3176
|
+
'\n起点余额 ' + usageMoney(selected.openingBalance, selected.currency) +
|
|
3177
|
+
' → 当前余额 ' + usageMoney(selected.currentBalance, selected.currency) +
|
|
3178
|
+
'\n当前:' + selected.label + ' ' + usageMoney(selected.amount, selected.currency)
|
|
3179
|
+
creditsHint.textContent = '本统计区间累计到账金额(' + selected.currency + ',未到账请填 0):包括充值、赠金等;多次到账请填合计,不要只填最后一笔。'
|
|
3180
|
+
debitsHint.textContent = '非调用造成的余额减少(' + selected.currency + ',没有请填 0):到期赠金、余额退回等。仅填写统计起点之后的金额;保存会替换之前的校正值。'
|
|
3181
|
+
credits.value = selected.credits == null ? '' : String(selected.credits)
|
|
3182
|
+
debits.value = selected.otherDebits == null ? '0' : String(selected.otherDebits)
|
|
3183
|
+
confirm.checked = false
|
|
3184
|
+
updatePreview()
|
|
3185
|
+
}
|
|
3186
|
+
function updatePreview() {
|
|
3187
|
+
if (!selected || credits.value.trim() === '' || !isFinite(Number(credits.value)) || !isFinite(Number(debits.value))) {
|
|
3188
|
+
preview.textContent = '填写完整金额后显示校正预览'
|
|
3189
|
+
return
|
|
3190
|
+
}
|
|
3191
|
+
var amount = selected.openingBalance + Number(credits.value) - Number(debits.value || 0) - selected.currentBalance
|
|
3192
|
+
preview.textContent = amount < -0.000000005 ? '校正后为负数,请核对金额和统计区间' : '校正后消费:' + usageMoney(Math.max(0, amount), selected.currency)
|
|
3193
|
+
}
|
|
3194
|
+
credits.addEventListener('input', updatePreview)
|
|
3195
|
+
debits.addEventListener('input', updatePreview)
|
|
3196
|
+
dates.addEventListener('change', renderSelection)
|
|
3197
|
+
function load() {
|
|
3198
|
+
if (busy) return
|
|
3199
|
+
busy = true
|
|
3200
|
+
save.disabled = true
|
|
3201
|
+
reset.disabled = true
|
|
3202
|
+
reload.disabled = true
|
|
3203
|
+
status.textContent = '正在刷新余额…'
|
|
3204
|
+
fetch(adjustmentUrl, { cache: 'no-store' })
|
|
3205
|
+
.then(function (r) { return r.json() })
|
|
3206
|
+
.then(function (data) {
|
|
3207
|
+
if (!data.ok) throw new Error(data.error || '读取失败')
|
|
3208
|
+
rows = data.days || []
|
|
3209
|
+
dates.innerHTML = ''
|
|
3210
|
+
rows.forEach(function (row) {
|
|
3211
|
+
var option = document.createElement('option')
|
|
3212
|
+
option.value = row.day
|
|
3213
|
+
option.textContent = row.day + ' · ' + row.label
|
|
3214
|
+
dates.appendChild(option)
|
|
3215
|
+
})
|
|
3216
|
+
status.textContent = data.error ? '余额暂未刷新:' + data.error : ''
|
|
3217
|
+
})
|
|
3218
|
+
.catch(function (err) { status.textContent = err.message || '读取失败' })
|
|
3219
|
+
.finally(function () { busy = false; reload.disabled = false; renderSelection(); dates.focus() })
|
|
3220
|
+
}
|
|
3221
|
+
function submit(action) {
|
|
3222
|
+
if (busy || !selected) return
|
|
3223
|
+
if (action !== 'reset' && !confirm.checked) { status.textContent = '请先勾选确认,核对本统计区间的全部余额调整。'; return }
|
|
3224
|
+
busy = true
|
|
3225
|
+
save.disabled = true
|
|
3226
|
+
reset.disabled = true
|
|
3227
|
+
reload.disabled = true
|
|
3228
|
+
status.textContent = '正在保存…'
|
|
3229
|
+
fetch(adjustmentUrl, {
|
|
3230
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
3231
|
+
body: JSON.stringify({
|
|
3232
|
+
modelId: modelId, day: selected.day, revision: selected.revision, action: action,
|
|
3233
|
+
credits: credits.value.trim(), otherDebits: debits.value.trim() || '0', confirmed: confirm.checked
|
|
3234
|
+
})
|
|
3235
|
+
})
|
|
3236
|
+
.then(function (r) { return r.json() })
|
|
3237
|
+
.then(function (data) {
|
|
3238
|
+
if (!data.ok) throw new Error(data.error || '保存失败')
|
|
3239
|
+
// 保存后立刻用返回的摘要刷新该模型条目:关窗回到设置菜单时显示的就是新金额
|
|
3240
|
+
var model = apiModelById(modelId)
|
|
3241
|
+
var currentDay = new Date(Date.now() + 8 * 3600000).toISOString().slice(0, 10)
|
|
3242
|
+
if (apiCanAdjustBalance(model) && data.summary && data.summary.day === currentDay) {
|
|
3243
|
+
model.balance = data.summary.currentBalance
|
|
3244
|
+
model.currency = data.summary.currency
|
|
3245
|
+
model.todayUsage = data.summary.amount
|
|
3246
|
+
model.todayUsageCurrency = data.summary.currency
|
|
3247
|
+
model.usageSource = data.summary.source
|
|
3248
|
+
model.usageLabel = data.summary.label
|
|
3249
|
+
model.accounting = data.summary
|
|
3250
|
+
model.error = null
|
|
3251
|
+
}
|
|
3252
|
+
busy = false
|
|
3253
|
+
close()
|
|
3254
|
+
refresh(true)
|
|
3255
|
+
loadApiModels(function () {
|
|
3256
|
+
if (usagePanelOpen) rebuildUsageSubShell()
|
|
3257
|
+
}, true)
|
|
3258
|
+
})
|
|
3259
|
+
.catch(function (err) { status.textContent = err.message || '保存失败' })
|
|
3260
|
+
.finally(function () { busy = false; save.disabled = !selected; reset.disabled = !selected || !selected.correctedAt; reload.disabled = false })
|
|
3261
|
+
}
|
|
3262
|
+
card.addEventListener('submit', function (e) { e.preventDefault(); submit('save') })
|
|
3263
|
+
load()
|
|
3264
|
+
}
|
|
3265
|
+
|
|
2999
3266
|
function fillUsagePanel(d) {
|
|
3000
3267
|
var hostEl = usageMainEl || usagePanel
|
|
3001
3268
|
hostEl.innerHTML = ''
|
|
@@ -3009,10 +3276,14 @@ function fillUsagePanel(d) {
|
|
|
3009
3276
|
var today = d.today || {}
|
|
3010
3277
|
var todayModels = today.models || []
|
|
3011
3278
|
var hasEvToday = todayModels.length > 0
|
|
3012
|
-
// ①
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3279
|
+
// ① 本机模型费用(所有模型的本地会话估算)
|
|
3280
|
+
// 「已观测消费」是 DeepSeek 账户口径(余额观测),已移到 小鲸鱼记账 → DeepSeek(内置)→ 设置 里,
|
|
3281
|
+
// 与「余额校正」放在一起,避免在这里被误读成"全模型合计"。
|
|
3282
|
+
var modelTitle = uSectionTitle('本机模型费用', usageMoney(today.modelTotal, 'CNY'))
|
|
3283
|
+
// 标题与下方模型列表之间不再画分隔线(列表本身有边框,够了)
|
|
3284
|
+
modelTitle.style.borderBottom = 'none'
|
|
3285
|
+
modelTitle.title = '按本机 DSH 会话计算,不按账户余额比例缩放;两者覆盖范围不同。'
|
|
3286
|
+
wrap.appendChild(modelTitle)
|
|
3016
3287
|
var todayBox = document.createElement('div')
|
|
3017
3288
|
todayBox.className = 'dshwv-usage-scroll dshwv-usage-today'
|
|
3018
3289
|
if (hasEvToday) {
|
|
@@ -3030,7 +3301,7 @@ function fillUsagePanel(d) {
|
|
|
3030
3301
|
} else if ((today.total || 0) > 0) {
|
|
3031
3302
|
var noM = document.createElement('div')
|
|
3032
3303
|
noM.className = 'dshwv-usage-hint'
|
|
3033
|
-
noM.textContent = '
|
|
3304
|
+
noM.textContent = '暂无本机会话费用明细;账户余额观测仍独立记账。'
|
|
3034
3305
|
todayBox.appendChild(noM)
|
|
3035
3306
|
} else {
|
|
3036
3307
|
var empty = document.createElement('div')
|
|
@@ -3045,7 +3316,7 @@ function fillUsagePanel(d) {
|
|
|
3045
3316
|
sep7.style.borderTop = '1px solid rgba(32,49,112,.15)'
|
|
3046
3317
|
sep7.style.margin = '6px 0'
|
|
3047
3318
|
wrap.appendChild(sep7)
|
|
3048
|
-
var t7 = uSectionTitle('近7天使用记录', usageMoney(d.total7))
|
|
3319
|
+
var t7 = uSectionTitle('近7天使用记录', usageMoney(d.total7, d.total7Currency))
|
|
3049
3320
|
t7.style.borderBottom = 'none'
|
|
3050
3321
|
wrap.appendChild(t7)
|
|
3051
3322
|
var daysBox = document.createElement('div')
|
|
@@ -3055,9 +3326,10 @@ function fillUsagePanel(d) {
|
|
|
3055
3326
|
r.className = 'dshwv-usage-row'
|
|
3056
3327
|
var n = document.createElement('span')
|
|
3057
3328
|
n.textContent = usageDayLabel(row.date)
|
|
3329
|
+
n.title = row.label || ''
|
|
3058
3330
|
r.appendChild(n)
|
|
3059
3331
|
var c = document.createElement('span')
|
|
3060
|
-
c.textContent = usageMoney(row.total)
|
|
3332
|
+
c.textContent = usageMoney(row.total, row.currency)
|
|
3061
3333
|
r.appendChild(c)
|
|
3062
3334
|
daysBox.appendChild(r)
|
|
3063
3335
|
})
|
|
@@ -3856,8 +4128,8 @@ function usageDrawBarChart(body, secTitle, days, opts) {
|
|
|
3856
4128
|
g.fillStyle = '#9fb0d9'
|
|
3857
4129
|
g.font = '10px sans-serif'
|
|
3858
4130
|
g.textAlign = 'right'
|
|
3859
|
-
g.fillText(usageMoney(max), padL - 4, padT + 8)
|
|
3860
|
-
g.fillText(usageMoney(max / 2), padL - 4, padT + ih / 2 + 3)
|
|
4131
|
+
g.fillText(usageMoney(max, opts.currency), padL - 4, padT + 8)
|
|
4132
|
+
g.fillText(usageMoney(max / 2, opts.currency), padL - 4, padT + ih / 2 + 3)
|
|
3861
4133
|
g.fillText('¥0', padL - 4, padT + ih + 4)
|
|
3862
4134
|
}
|
|
3863
4135
|
function move(ev) {
|
|
@@ -3870,7 +4142,7 @@ function usageDrawBarChart(body, secTitle, days, opts) {
|
|
|
3870
4142
|
paint(hover)
|
|
3871
4143
|
if (hover >= 0) {
|
|
3872
4144
|
tip.style.display = 'block'
|
|
3873
|
-
tip.textContent = bars[hover].day.date + ' ' + usageMoney(bars[hover].day.total)
|
|
4145
|
+
tip.textContent = bars[hover].day.date + ' ' + usageMoney(bars[hover].day.total, opts.currency)
|
|
3874
4146
|
var wr = wrap.getBoundingClientRect()
|
|
3875
4147
|
var tx = ev.clientX - wr.left + 10
|
|
3876
4148
|
if (tx + 130 > wr.width) tx = ev.clientX - wr.left - 140
|
|
@@ -4006,31 +4278,37 @@ function fillUsageRecordsWindow(d) {
|
|
|
4006
4278
|
var allDays = ((d.all && d.all.days) || []).slice().sort(function (a, b) { return a.date < b.date ? -1 : 1 })
|
|
4007
4279
|
var evAll = ((d.all && d.all.events) || []).slice()
|
|
4008
4280
|
// ① 概览头
|
|
4009
|
-
var
|
|
4281
|
+
var sumAllByCurrency = {}
|
|
4282
|
+
var chartCurrency = (d.today && d.today.currency) || 'CNY'
|
|
4010
4283
|
var maxDay = null
|
|
4011
4284
|
for (var s1 = 0; s1 < allDays.length; s1++) {
|
|
4012
|
-
|
|
4013
|
-
|
|
4285
|
+
var dayCurrency = allDays[s1].currency || 'CNY'
|
|
4286
|
+
sumAllByCurrency[dayCurrency] = (sumAllByCurrency[dayCurrency] || 0) + (Number(allDays[s1].total) || 0)
|
|
4287
|
+
if (dayCurrency === chartCurrency && (!maxDay || Number(allDays[s1].total) > Number(maxDay.total))) maxDay = allDays[s1]
|
|
4014
4288
|
}
|
|
4015
4289
|
var ov = document.createElement('div')
|
|
4016
4290
|
ov.className = 'dshwv-usage-oview'
|
|
4017
4291
|
var ovL = document.createElement('div')
|
|
4018
|
-
ovL.textContent = '
|
|
4292
|
+
ovL.textContent = '全部记录合计'
|
|
4019
4293
|
ov.appendChild(ovL)
|
|
4020
4294
|
var ovN = document.createElement('div')
|
|
4021
4295
|
ovN.className = 'dshwv-usage-oview-num'
|
|
4022
|
-
ovN.textContent = usageMoney(
|
|
4296
|
+
ovN.textContent = Object.keys(sumAllByCurrency).map(function (cur) { return usageMoney(sumAllByCurrency[cur], cur) }).join(' + ') || usageMoney(0, chartCurrency)
|
|
4023
4297
|
ov.appendChild(ovN)
|
|
4024
4298
|
var ovS = document.createElement('div')
|
|
4025
4299
|
ovS.className = 'dshwv-usage-hint'
|
|
4026
|
-
ovS.textContent = '共 ' + evAll.length + ' 笔明细' + (maxDay ? ' · 峰值 ' + maxDay.date + ' ' + usageMoney(maxDay.total) : '')
|
|
4300
|
+
ovS.textContent = '共 ' + evAll.length + ' 笔明细' + (maxDay ? ' · 峰值 ' + maxDay.date + ' ' + usageMoney(maxDay.total, maxDay.currency) : '')
|
|
4027
4301
|
ov.appendChild(ovS)
|
|
4302
|
+
var sourceNote = document.createElement('div')
|
|
4303
|
+
sourceNote.className = 'dshwv-usage-hint'
|
|
4304
|
+
sourceNote.textContent = '按各日标注口径汇总;本机模型明细为估算,与账户消费覆盖范围不同。'
|
|
4305
|
+
ov.appendChild(sourceNote)
|
|
4028
4306
|
body.appendChild(ov)
|
|
4029
4307
|
var detailBox = null
|
|
4030
4308
|
// ② 统计图表(默认折叠,懒渲染)
|
|
4031
4309
|
usageCollapseBlock(body, '统计图表(近30天 / 模型占比)', false, function (inner) {
|
|
4032
|
-
usageDrawBarChart(inner, '近30天消费
|
|
4033
|
-
today: usageTodayKeyStr(),
|
|
4310
|
+
usageDrawBarChart(inner, '近30天消费 · ' + chartCurrency + '(点柱查看当日)', allDays.filter(function (row) { return (row.currency || 'CNY') === chartCurrency }).slice(-30), {
|
|
4311
|
+
today: usageTodayKeyStr(), currency: chartCurrency,
|
|
4034
4312
|
onPick: function (date) {
|
|
4035
4313
|
try {
|
|
4036
4314
|
if (!detailBox) return
|
|
@@ -4047,9 +4325,9 @@ function fillUsageRecordsWindow(d) {
|
|
|
4047
4325
|
},
|
|
4048
4326
|
})
|
|
4049
4327
|
var todayAgg = usageAggModels(d.today && d.today.models ? [{ models: d.today.models }] : [])
|
|
4050
|
-
usageRatioRows(inner, '
|
|
4328
|
+
usageRatioRows(inner, '今日模型费用 · 估算', todayAgg, usageMoney((d.today && d.today.modelTotal) || 0))
|
|
4051
4329
|
var sevenAgg = usageAggModels(d.days7 || [])
|
|
4052
|
-
usageRatioRows(inner, '近7
|
|
4330
|
+
usageRatioRows(inner, '近7天模型费用 · 估算', sevenAgg, usageMoney((d.days7 || []).reduce(function (sum, row) { return sum + (Number(row.modelTotal) || 0) }, 0)))
|
|
4053
4331
|
})
|
|
4054
4332
|
// ③ 每日与逐条明细(默认折叠;搜索/限量加载)
|
|
4055
4333
|
detailBox = usageCollapseBlock(body, '每日与逐条明细', false, function (inner) {
|
|
@@ -4071,7 +4349,8 @@ function fillUsageRecordsWindow(d) {
|
|
|
4071
4349
|
;(evMap[day] = evMap[day] || []).push(ev)
|
|
4072
4350
|
})
|
|
4073
4351
|
var dayTot = {}
|
|
4074
|
-
|
|
4352
|
+
var dayMeta = {}
|
|
4353
|
+
allDays.forEach(function (dx) { dayTot[dx.date] = Number(dx.total) || 0; dayMeta[dx.date] = dx })
|
|
4075
4354
|
var todayKeyStr2 = usageTodayKeyStr()
|
|
4076
4355
|
function dayGroup(day, evs) {
|
|
4077
4356
|
var row = document.createElement('div')
|
|
@@ -4091,7 +4370,8 @@ function fillUsageRecordsWindow(d) {
|
|
|
4091
4370
|
var dayV = dayTot[day]
|
|
4092
4371
|
if ((dayV === undefined || dayV === 0) && day === todayKeyStr2 && d.today && isFinite(Number(d.today.total))) dayV = Number(d.today.total)
|
|
4093
4372
|
if (dayV === undefined || dayV === null) dayV = evs.reduce(function (a, x) { return a + (Number(x.cost) || 0) }, 0)
|
|
4094
|
-
c.textContent = usageMoney(dayV)
|
|
4373
|
+
c.textContent = usageMoney(dayV, dayMeta[day] && dayMeta[day].currency)
|
|
4374
|
+
name.title = (dayMeta[day] && dayMeta[day].label) || '本地估算'
|
|
4095
4375
|
row.appendChild(c)
|
|
4096
4376
|
var chev = document.createElement('span')
|
|
4097
4377
|
chev.className = 'dshwv-usage-chev'
|
|
@@ -4124,6 +4404,7 @@ function fillUsageRecordsWindow(d) {
|
|
|
4124
4404
|
var c2 = document.createElement('span')
|
|
4125
4405
|
c2.style.flex = '0 0 auto'
|
|
4126
4406
|
c2.textContent = usageMoney(ev.cost)
|
|
4407
|
+
c2.title = '本地估算:' + Number(ev.cost || 0).toFixed(8) + ' CNY'
|
|
4127
4408
|
r2.appendChild(c2)
|
|
4128
4409
|
detail.appendChild(r2)
|
|
4129
4410
|
}
|
|
@@ -5751,7 +6032,7 @@ function bubbleModuleSummary(m) {
|
|
|
5751
6032
|
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '余额数值'
|
|
5752
6033
|
if (m.type === 'today') return bubbleIsModelMod(m) ? ('今日已用·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '今日已用'
|
|
5753
6034
|
if (m.type === 'quota') return '额度·' + ((apiModelById(m.modelId) || {}).name || m.modelId)
|
|
5754
|
-
if (m.type === 'plan') return
|
|
6035
|
+
if (m.type === 'plan') return bubblePlanModuleLabel(m)
|
|
5755
6036
|
if (m.type === 'peak' || m.type === 'nextpeak') return bubblePeakModuleLabel(m)
|
|
5756
6037
|
if (m.type === 'image') return '图片/动图'
|
|
5757
6038
|
if (m.type === 'randimg') return '随机图片' + (m.imgs && m.imgs.length ? '(' + m.imgs.length + '张)' : '(空)')
|
|
@@ -5768,7 +6049,7 @@ function bubbleModuleListLabel(m) {
|
|
|
5768
6049
|
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '余额数值'
|
|
5769
6050
|
if (m.type === 'today') return bubbleIsModelMod(m) ? ('今日已用·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '今日已用'
|
|
5770
6051
|
if (m.type === 'quota') return '额度·' + ((apiModelById(m.modelId) || {}).name || m.modelId)
|
|
5771
|
-
if (m.type === 'plan') return
|
|
6052
|
+
if (m.type === 'plan') return bubblePlanModuleLabel(m)
|
|
5772
6053
|
if (m.type === 'peak' || m.type === 'nextpeak') return bubblePeakModuleLabel(m)
|
|
5773
6054
|
if (m.type === 'image') return '图片/动图'
|
|
5774
6055
|
if (m.type === 'randimg') return '随机图片' + (m.imgs && m.imgs.length ? '(' + m.imgs.length + '张)' : '(空)')
|
|
@@ -6779,8 +7060,8 @@ function openQuickModuleEditor(m, anchorBtn) {
|
|
|
6779
7060
|
var isModelBal = bubbleIsModelMod(m) && (m.type === 'balance' || m.type === 'today')
|
|
6780
7061
|
var isModelQuota = bubbleIsModelMod(m) && m.type === 'quota'
|
|
6781
7062
|
var isModelPlan = bubbleIsModelMod(m) && m.type === 'plan'
|
|
6782
|
-
inp.placeholder = isModelPlan ? '例:
|
|
6783
|
-
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}')))))
|
|
7063
|
+
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}')))))
|
|
7064
|
+
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}')))))
|
|
6784
7065
|
inp.addEventListener('input', function () { m.tpl = inp.value; changed() })
|
|
6785
7066
|
r.appendChild(inp)
|
|
6786
7067
|
var qb2 = document.createElement('button')
|
|
@@ -6794,6 +7075,25 @@ function openQuickModuleEditor(m, anchorBtn) {
|
|
|
6794
7075
|
box.appendChild(r)
|
|
6795
7076
|
}
|
|
6796
7077
|
tplRow()
|
|
7078
|
+
// 订阅额度模块的「显示样式」= 选时间窗口(多窗口厂商,如 OpenCode Go 的 5h / 周 / 月)
|
|
7079
|
+
if (m.type === 'plan' && (apiPlanMultiWin(m.modelId) || apiPlanWinList(m.modelId))) {
|
|
7080
|
+
var wrow = qRow()
|
|
7081
|
+
wrow.appendChild(qLabel('显示样式'))
|
|
7082
|
+
var wsel = document.createElement('select')
|
|
7083
|
+
for (var wi = 0; wi < BUBBLE_PLAN_WIN_OPTS.length; wi++) {
|
|
7084
|
+
var wo = document.createElement('option')
|
|
7085
|
+
wo.value = BUBBLE_PLAN_WIN_OPTS[wi][0]
|
|
7086
|
+
wo.textContent = BUBBLE_PLAN_WIN_OPTS[wi][1]
|
|
7087
|
+
wsel.appendChild(wo)
|
|
7088
|
+
}
|
|
7089
|
+
wsel.value = bubblePlanWinOf(m)
|
|
7090
|
+
wsel.style.flex = '1'
|
|
7091
|
+
wsel.style.minWidth = '0'
|
|
7092
|
+
wrow.appendChild(wsel)
|
|
7093
|
+
box.appendChild(wrow)
|
|
7094
|
+
dshwCustSel(wsel)
|
|
7095
|
+
wsel.addEventListener('change', function () { m.planWin = wsel.value || 'all'; changed() })
|
|
7096
|
+
}
|
|
6797
7097
|
if (m.type === 'peak' || m.type === 'nextpeak') {
|
|
6798
7098
|
// 显示样式(与编辑窗口一致)
|
|
6799
7099
|
var srow = qRow()
|
|
@@ -6985,6 +7285,32 @@ function renderBubblePal() {
|
|
|
6985
7285
|
if (apiModelsLoaded) {
|
|
6986
7286
|
apiModels.forEach(function (am) {
|
|
6987
7287
|
if (!am || !am.id || am.builtin) return
|
|
7288
|
+
var planSupported = apiPlanSupport(am.id)
|
|
7289
|
+
// 只有「一个接口返回多个窗口」的额度厂商(如 OpenCode Go)才把调色板收成一个「额度」模块;
|
|
7290
|
+
// 单窗口的既有额度厂商(智谱 / Kimi / MiniMax Coding)保持上游原有的三个模块不变
|
|
7291
|
+
if (planSupported && apiPlanMultiWin(am.id)) {
|
|
7292
|
+
// 订阅额度厂商(OpenCode Go / 智谱 / Kimi / MiniMax Coding 等):这类厂商本来就没有余额接口,
|
|
7293
|
+
// 只给一个「额度」模块 —— 时间窗口(5h / 周 / 月 / 全部)与显示内容都在模块编辑器里选,
|
|
7294
|
+
// 不再并列「余额 / 手动额度 / 订阅额度」三个模块。
|
|
7295
|
+
var key4 = 'pq:' + am.id
|
|
7296
|
+
var chip4 = document.createElement('div')
|
|
7297
|
+
chip4.className = 'dshwv-palchip'
|
|
7298
|
+
chip4.setAttribute('data-pal', key4)
|
|
7299
|
+
chip4.textContent = '额度·' + am.name
|
|
7300
|
+
var planWinHint = apiPlanMultiWin(am.id) ? ';「显示样式」可选时间窗口(全部 / 5h / 周 / 月)' : ''
|
|
7301
|
+
chip4.title = '该厂商的订阅额度(由厂商接口读取,非手动);模板变量 {plan} 额度 / {plan_reset} 刷新时间 / {plan_left} 剩余' + planWinHint
|
|
7302
|
+
chip4.draggable = true
|
|
7303
|
+
chip4.addEventListener('click', function (e) {
|
|
7304
|
+
e.stopPropagation()
|
|
7305
|
+
bubbleModuleAdd({ type: 'plan', modelId: am.id, size: 8, tpl: '{plan} · {plan_reset}', planWin: 'all' })
|
|
7306
|
+
})
|
|
7307
|
+
chip4.addEventListener('dragstart', function (e) {
|
|
7308
|
+
try { e.dataTransfer.setData('text/plain', key4) } catch (err) {}
|
|
7309
|
+
bubbleDragKey = key4
|
|
7310
|
+
})
|
|
7311
|
+
bubblePalEl.appendChild(chip4)
|
|
7312
|
+
return
|
|
7313
|
+
}
|
|
6988
7314
|
var key = 'bal:' + am.id
|
|
6989
7315
|
var chip2 = document.createElement('div')
|
|
6990
7316
|
chip2.className = 'dshwv-palchip'
|
|
@@ -7018,25 +7344,6 @@ function renderBubblePal() {
|
|
|
7018
7344
|
bubbleDragKey = key3
|
|
7019
7345
|
})
|
|
7020
7346
|
bubblePalEl.appendChild(chip3)
|
|
7021
|
-
// 厂商订阅额度模块(kind='quota' 的厂商,如智谱/Kimi/MiniMax Coding):palette key = pq:<modelId>
|
|
7022
|
-
if (apiPlanSupport(am.id)) {
|
|
7023
|
-
var key4 = 'pq:' + am.id
|
|
7024
|
-
var chip4 = document.createElement('div')
|
|
7025
|
-
chip4.className = 'dshwv-palchip'
|
|
7026
|
-
chip4.setAttribute('data-pal', key4)
|
|
7027
|
-
chip4.textContent = '订阅额度·' + am.name
|
|
7028
|
-
chip4.title = '该厂商的订阅额度(由厂商接口读取,非手动);模板变量 {plan} / {plan_left} / {plan_reset}'
|
|
7029
|
-
chip4.draggable = true
|
|
7030
|
-
chip4.addEventListener('click', function (e) {
|
|
7031
|
-
e.stopPropagation()
|
|
7032
|
-
bubbleModuleAdd({ type: 'plan', modelId: am.id, size: 8, tpl: '额度 {plan}' })
|
|
7033
|
-
})
|
|
7034
|
-
chip4.addEventListener('dragstart', function (e) {
|
|
7035
|
-
try { e.dataTransfer.setData('text/plain', key4) } catch (err) {}
|
|
7036
|
-
bubbleDragKey = key4
|
|
7037
|
-
})
|
|
7038
|
-
bubblePalEl.appendChild(chip4)
|
|
7039
|
-
}
|
|
7040
7347
|
})
|
|
7041
7348
|
} else if (apiModelsError) {
|
|
7042
7349
|
var chipErr = document.createElement('div')
|
|
@@ -7286,11 +7593,11 @@ function bubblePaletteModule(key) {
|
|
|
7286
7593
|
if (!am1) return null
|
|
7287
7594
|
return { type: 'quota', modelId: am1.id, size: 8, tpl: '已用 {quota} · 剩 {quota_left}' }
|
|
7288
7595
|
}
|
|
7289
|
-
// 厂商订阅额度模块(palette key = pq:<modelId
|
|
7596
|
+
// 厂商订阅额度模块(palette key = pq:<modelId>):时间窗口由模块里的「显示样式」决定
|
|
7290
7597
|
if (typeof key === 'string' && key.indexOf('pq:') === 0) {
|
|
7291
7598
|
var am2 = apiModelById(key.slice(3))
|
|
7292
7599
|
if (!am2) return null
|
|
7293
|
-
return { type: 'plan', modelId: am2.id, size: 8, tpl: '
|
|
7600
|
+
return { type: 'plan', modelId: am2.id, size: 8, tpl: '{plan} · {plan_reset}', planWin: 'all' }
|
|
7294
7601
|
}
|
|
7295
7602
|
if (key === 'random') return bubbleCloneModule(bubbleDefaultSecondModules()[0])
|
|
7296
7603
|
if (typeof key === 'string' && key.indexOf('lib:') === 0) {
|
|
@@ -7953,7 +8260,7 @@ var bubbleColorOpenMenu = null // 当前展开的颜色下拉(纯色/跑马灯,
|
|
|
7953
8260
|
// —— 弹层/菜单层级统一助手(所有下拉与弹窗据此取高于当前可见层,避免互相压制) ——
|
|
7954
8261
|
function visibleTopZ() {
|
|
7955
8262
|
var top = 20500
|
|
7956
|
-
var cand = [bubbleMask, bubbleItemMask, moduleMask, usageMoreMask, qeditEl, window.__dshwRemindMask]
|
|
8263
|
+
var cand = [bubbleMask, bubbleItemMask, moduleMask, usageMoreMask, qeditEl, window.__dshwRemindMask, apiModelMaskEl, accountingMask]
|
|
7957
8264
|
function eff(el) {
|
|
7958
8265
|
try {
|
|
7959
8266
|
if (!el) return 0
|
|
@@ -9738,6 +10045,8 @@ var state = {
|
|
|
9738
10045
|
balance: null,
|
|
9739
10046
|
currency: null,
|
|
9740
10047
|
todayUsage: null,
|
|
10048
|
+
todayUsageCurrency: 'CNY',
|
|
10049
|
+
usageLabel: '本地估算',
|
|
9741
10050
|
isPeak: false,
|
|
9742
10051
|
status: 'loading',
|
|
9743
10052
|
message: '',
|
|
@@ -10347,7 +10656,7 @@ function bubbleAmountText() {
|
|
|
10347
10656
|
return fmt(v, state.currency)
|
|
10348
10657
|
}
|
|
10349
10658
|
function bubbleTodayText() {
|
|
10350
|
-
return '今日已用 ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.currency) : '--')
|
|
10659
|
+
return (state.usageLabel || '今日已用') + ' ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
10351
10660
|
}
|
|
10352
10661
|
// 模块「内容」模板:占位符统一英文(便于兼容其他模型 API 时区分来源/字段):
|
|
10353
10662
|
// {expense_ds} 今日已用金额 · {balance_ds} 余额 · {status} 高峰/空闲状态字 · {countdown} 倒计时
|
|
@@ -10520,36 +10829,160 @@ function apiPlanPctText(v) {
|
|
|
10520
10829
|
if (v === null || v === undefined) return '--'
|
|
10521
10830
|
return (Number(v) || 0).toFixed(1).replace(/\.0$/, '') + '%'
|
|
10522
10831
|
}
|
|
10523
|
-
|
|
10832
|
+
// —— 多窗口订阅额度(如 OpenCode Go 的 rolling / weekly / monthly)——
|
|
10833
|
+
// 模块的「显示样式」= 选哪个时间窗口:all 全部三窗口 / rolling 5h / weekly 周 / monthly 月。
|
|
10834
|
+
// 选中具体窗口时输出会带上窗口标签(如 `5h 2%`),避免看不出是哪个时间段。
|
|
10835
|
+
var BUBBLE_PLAN_WIN_OPTS = [
|
|
10836
|
+
['all', '全部(5h / 周 / 月)'],
|
|
10837
|
+
['rolling', '5h'],
|
|
10838
|
+
['weekly', '周'],
|
|
10839
|
+
['monthly', '月'],
|
|
10840
|
+
]
|
|
10841
|
+
function bubblePlanWinOf(m) {
|
|
10842
|
+
m = m || {}
|
|
10843
|
+
var w = String(m.planWin || 'all')
|
|
10844
|
+
for (var i = 0; i < BUBBLE_PLAN_WIN_OPTS.length; i++) { if (BUBBLE_PLAN_WIN_OPTS[i][0] === w) return w }
|
|
10845
|
+
return 'all'
|
|
10846
|
+
}
|
|
10847
|
+
function bubblePlanWinLabel(w) {
|
|
10848
|
+
for (var i = 0; i < BUBBLE_PLAN_WIN_OPTS.length; i++) { if (BUBBLE_PLAN_WIN_OPTS[i][0] === w) return BUBBLE_PLAN_WIN_OPTS[i][1] }
|
|
10849
|
+
return BUBBLE_PLAN_WIN_OPTS[0][1]
|
|
10850
|
+
}
|
|
10851
|
+
// 订阅额度模块在编辑列表 / 模块库里的名字(带所选窗口,便于区分同一个模型的多个模块)
|
|
10852
|
+
function bubblePlanModuleLabel(m) {
|
|
10853
|
+
m = m || {}
|
|
10854
|
+
var nm = (apiModelById(m.modelId) || {}).name || m.modelId
|
|
10855
|
+
var w = bubblePlanWinOf(m)
|
|
10856
|
+
return '额度·' + nm + (w === 'all' ? '' : '(' + bubblePlanWinLabel(w) + ')')
|
|
10857
|
+
}
|
|
10858
|
+
// 该模型的厂商模板是否声明了多窗口额度(决定编辑器里要不要给「显示样式」下拉)
|
|
10859
|
+
function apiPlanMultiWin(modelId) {
|
|
10860
|
+
var am = apiModelById(modelId)
|
|
10861
|
+
if (!am) return false
|
|
10862
|
+
var list = apiTemplates || []
|
|
10863
|
+
for (var i = 0; i < list.length; i++) {
|
|
10864
|
+
if (list[i] && list[i].id === am.provider) {
|
|
10865
|
+
var q = list[i].quota
|
|
10866
|
+
return !!(q && q.json && q.json.windows && q.json.windows.length)
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
return false
|
|
10870
|
+
}
|
|
10871
|
+
function apiPlanWinList(modelId) {
|
|
10872
|
+
var p = apiPlanOf(modelId)
|
|
10873
|
+
return (p && p.ok && p.windows && p.windows.length) ? p.windows : null
|
|
10874
|
+
}
|
|
10875
|
+
// 按窗口取「已用% / 剩余%」文本;非多窗口厂商返回 null(走原有单窗口逻辑)
|
|
10876
|
+
function apiPlanPctWinText(modelId, win, left) {
|
|
10877
|
+
var list = apiPlanWinList(modelId)
|
|
10878
|
+
if (!list) return null
|
|
10879
|
+
function one(w) {
|
|
10880
|
+
var v = w.usedPct
|
|
10881
|
+
if (v === null || v === undefined) return '--'
|
|
10882
|
+
var pct = left ? Math.max(0, 100 - Number(v)) : Number(v)
|
|
10883
|
+
return (w.label ? (w.label + ' ') : '') + apiPlanPctText(pct)
|
|
10884
|
+
}
|
|
10885
|
+
if (win && win !== 'all') {
|
|
10886
|
+
for (var i = 0; i < list.length; i++) { if (list[i].key === win) return one(list[i]) }
|
|
10887
|
+
return '--'
|
|
10888
|
+
}
|
|
10889
|
+
var parts = []
|
|
10890
|
+
for (var j = 0; j < list.length; j++) parts.push(one(list[j]))
|
|
10891
|
+
return parts.join(' · ')
|
|
10892
|
+
}
|
|
10893
|
+
// 按窗口取「重置倒计时」文本;非多窗口厂商返回 null
|
|
10894
|
+
function apiPlanResetWinText(modelId, win) {
|
|
10895
|
+
var list = apiPlanWinList(modelId)
|
|
10896
|
+
if (!list) return null
|
|
10897
|
+
function one(w, short) {
|
|
10898
|
+
var ms = apiPlanResetMs(w.resetAt)
|
|
10899
|
+
if (ms === null) return '--'
|
|
10900
|
+
var t = short ? apiPlanCountdownShortText(ms) : apiPlanCountdownText(ms)
|
|
10901
|
+
return t || '--'
|
|
10902
|
+
}
|
|
10903
|
+
if (win && win !== 'all') {
|
|
10904
|
+
for (var i = 0; i < list.length; i++) { if (list[i].key === win) return one(list[i], true) }
|
|
10905
|
+
return '--'
|
|
10906
|
+
}
|
|
10907
|
+
var parts = []
|
|
10908
|
+
for (var j = 0; j < list.length; j++) parts.push(one(list[j], true))
|
|
10909
|
+
return parts.join(' · ')
|
|
10910
|
+
}
|
|
10911
|
+
function apiPlanUsedText(modelId, win) {
|
|
10912
|
+
var t = apiPlanPctWinText(modelId, win, false)
|
|
10913
|
+
if (t !== null) return t
|
|
10524
10914
|
var p = apiPlanOf(modelId)
|
|
10525
10915
|
if (!p || !p.ok) return '--'
|
|
10526
10916
|
return apiPlanPctText(p.usedPct)
|
|
10527
10917
|
}
|
|
10528
|
-
function apiPlanLeftText(modelId) {
|
|
10918
|
+
function apiPlanLeftText(modelId, win) {
|
|
10919
|
+
var t = apiPlanPctWinText(modelId, win, true)
|
|
10920
|
+
if (t !== null) return t
|
|
10529
10921
|
var p = apiPlanOf(modelId)
|
|
10530
10922
|
if (!p || !p.ok) return '--'
|
|
10531
10923
|
return apiPlanPctText(p.remainPct)
|
|
10532
10924
|
}
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
if (
|
|
10536
|
-
|
|
10537
|
-
var
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10925
|
+
// 重置时间归一成毫秒(秒 / 毫秒 / 日期字符串都兼容);解析不出来返回 null
|
|
10926
|
+
function apiPlanResetMs(t) {
|
|
10927
|
+
if (t === null || t === undefined || t === '') return null
|
|
10928
|
+
if (typeof t === 'number') return t < 1e12 ? t * 1000 : t // 秒 / 毫秒都兼容
|
|
10929
|
+
var pd = Date.parse(String(t))
|
|
10930
|
+
return isFinite(pd) ? pd : null
|
|
10931
|
+
}
|
|
10932
|
+
// 倒计时文本(长写法,单窗口用)
|
|
10933
|
+
function apiPlanCountdownText(ms) {
|
|
10541
10934
|
var left = ms - Date.now()
|
|
10542
|
-
if (!isFinite(left)) return
|
|
10935
|
+
if (!isFinite(left)) return ''
|
|
10543
10936
|
if (left <= 0) return '即将重置'
|
|
10544
10937
|
var h = Math.floor(left / 3600000)
|
|
10545
10938
|
var d = Math.floor(h / 24)
|
|
10546
10939
|
if (d > 0) return d + '天' + (h % 24) + '小时后重置'
|
|
10547
10940
|
return h + '小时' + Math.floor((left % 3600000) / 60000) + '分后重置'
|
|
10548
10941
|
}
|
|
10942
|
+
// 倒计时文本(紧凑写法,多窗口用:5d21h / 3h53m)——单位统一用 d/h/m,不掺中文
|
|
10943
|
+
// 不带「后重置」字样:多窗口模块里窗口标签已说明它是什么(如 `5h 2% · 3h53m`)
|
|
10944
|
+
function apiPlanCountdownShortText(ms) {
|
|
10945
|
+
var left = ms - Date.now()
|
|
10946
|
+
if (!isFinite(left)) return ''
|
|
10947
|
+
if (left <= 0) return '即将重置'
|
|
10948
|
+
var m = Math.floor(left / 60000)
|
|
10949
|
+
var h = Math.floor(m / 60)
|
|
10950
|
+
var d = Math.floor(h / 24)
|
|
10951
|
+
if (d > 0) return d + 'd' + (h % 24) + 'h'
|
|
10952
|
+
if (h > 0) return h + 'h' + (m % 60) + 'm'
|
|
10953
|
+
return m + 'm'
|
|
10954
|
+
}
|
|
10955
|
+
function apiPlanResetText(modelId, win) {
|
|
10956
|
+
var t = apiPlanResetWinText(modelId, win)
|
|
10957
|
+
if (t !== null) return t
|
|
10958
|
+
var p = apiPlanOf(modelId)
|
|
10959
|
+
if (!p || !p.ok || p.resetAt === null || p.resetAt === undefined || p.resetAt === '') return '--'
|
|
10960
|
+
var ms = apiPlanResetMs(p.resetAt)
|
|
10961
|
+
if (ms === null) return String(p.resetAt)
|
|
10962
|
+
var txt = apiPlanCountdownText(ms)
|
|
10963
|
+
return txt || String(p.resetAt)
|
|
10964
|
+
}
|
|
10549
10965
|
function apiPlanSummary(modelId) {
|
|
10550
10966
|
var p = apiPlanOf(modelId)
|
|
10551
10967
|
if (!p) return '读取中…'
|
|
10552
10968
|
if (!p.ok) return p.hide ? '--' : (p.error || '读取失败')
|
|
10969
|
+
// v0.3.1:多窗口额度(如 OpenCode Go 的 5h / 周 / 月)逐窗口展示:`5h 12.5% · 2h55m后重置 | 周 …`
|
|
10970
|
+
if (p.windows && p.windows.length) {
|
|
10971
|
+
var parts = []
|
|
10972
|
+
for (var i = 0; i < p.windows.length; i++) {
|
|
10973
|
+
var w = p.windows[i] || {}
|
|
10974
|
+
var seg = w.label ? (w.label + ' ') : ''
|
|
10975
|
+
seg += apiPlanPctText(w.usedPct)
|
|
10976
|
+
var ms = apiPlanResetMs(w.resetAt)
|
|
10977
|
+
if (ms !== null) {
|
|
10978
|
+
var rt = apiPlanCountdownShortText(ms)
|
|
10979
|
+
if (rt) seg += ' · ' + rt
|
|
10980
|
+
}
|
|
10981
|
+
parts.push(seg)
|
|
10982
|
+
}
|
|
10983
|
+
if (p.level) parts.push(p.level)
|
|
10984
|
+
return parts.join(' | ')
|
|
10985
|
+
}
|
|
10553
10986
|
var s = '已用 ' + apiPlanUsedText(modelId) + ' · ' + apiPlanResetText(modelId)
|
|
10554
10987
|
if (p.weeklyUsedPct !== null && p.weeklyUsedPct !== undefined) s += ' · 周 ' + apiPlanPctText(p.weeklyUsedPct)
|
|
10555
10988
|
if (p.level) s += ' · ' + p.level
|
|
@@ -10573,18 +11006,19 @@ function bubbleContentTokenMap(m) {
|
|
|
10573
11006
|
map['quota_left'] = apiQuotaLeftText(m.modelId)
|
|
10574
11007
|
map['quota_total'] = apiQuotaTotalText(m.modelId)
|
|
10575
11008
|
map['quota_reset'] = apiQuotaResetText(m.modelId)
|
|
10576
|
-
// 厂商订阅额度占位符(kind='quota'
|
|
10577
|
-
|
|
11009
|
+
// 厂商订阅额度占位符(kind='quota' 的厂商):按模块「显示样式」选的窗口取数
|
|
11010
|
+
var pwin = bubblePlanWinOf(m)
|
|
11011
|
+
map['plan'] = apiPlanUsedText(m.modelId, pwin)
|
|
10578
11012
|
map['plan_used'] = map['plan']
|
|
10579
|
-
map['plan_left'] = apiPlanLeftText(m.modelId)
|
|
10580
|
-
map['plan_reset'] = apiPlanResetText(m.modelId)
|
|
11013
|
+
map['plan_left'] = apiPlanLeftText(m.modelId, pwin)
|
|
11014
|
+
map['plan_reset'] = apiPlanResetText(m.modelId, pwin)
|
|
10581
11015
|
return map
|
|
10582
11016
|
}
|
|
10583
11017
|
if (m.type === 'balance') {
|
|
10584
11018
|
v = bubbleAmountText()
|
|
10585
11019
|
map['balance_ds'] = v
|
|
10586
11020
|
} else if (m.type === 'today') {
|
|
10587
|
-
v = (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.currency) : '--')
|
|
11021
|
+
v = (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
10588
11022
|
map['expense_ds'] = v
|
|
10589
11023
|
} else if (bubbleIsPeakCount(m)) {
|
|
10590
11024
|
v = bubbleCountdownText()
|
|
@@ -10612,9 +11046,9 @@ function bubbleTplHelpItems(m) {
|
|
|
10612
11046
|
add('quota_total', '额度总量')
|
|
10613
11047
|
add('quota_reset', '额度重置倒计时')
|
|
10614
11048
|
}
|
|
10615
|
-
add('plan', '
|
|
10616
|
-
add('plan_left', '
|
|
10617
|
-
add('plan_reset', '
|
|
11049
|
+
add('plan', '订阅额度已用百分比(多窗口厂商按「显示样式」所选窗口,带窗口标签)')
|
|
11050
|
+
add('plan_left', '订阅额度剩余百分比(同上)')
|
|
11051
|
+
add('plan_reset', '订阅额度刷新倒计时(同上;全部窗口时为紧凑倒计时)')
|
|
10618
11052
|
return arr
|
|
10619
11053
|
}
|
|
10620
11054
|
if (m.type === 'balance') add('balance_ds', '余额数值')
|
|
@@ -10887,7 +11321,19 @@ function bubbleCountdownTick() {
|
|
|
10887
11321
|
bubbleCountdownApplyStyle(x.el, x.mod, cur)
|
|
10888
11322
|
} catch (err) {}
|
|
10889
11323
|
}
|
|
10890
|
-
|
|
11324
|
+
// v733:非倒计时的峰谷行共用这个 ticker(只在状态真的变了时才动 DOM,不打断跑马灯)
|
|
11325
|
+
bubblePeakRows = bubblePeakRows.filter(function (y) { return y && y.row && y.row.isConnected })
|
|
11326
|
+
if (bubblePeakRows.length) {
|
|
11327
|
+
var peakNow = bubbleIsPeakNow()
|
|
11328
|
+
for (var k = 0; k < bubblePeakRows.length; k++) {
|
|
11329
|
+
var y2 = bubblePeakRows[k]
|
|
11330
|
+
if (y2.peak !== peakNow) {
|
|
11331
|
+
y2.peak = peakNow
|
|
11332
|
+
try { bubblePeakRowApply(y2, peakNow) } catch (err) {}
|
|
11333
|
+
}
|
|
11334
|
+
}
|
|
11335
|
+
}
|
|
11336
|
+
if (!bubbleCountdownRows.length && !bubblePeakRows.length && bubbleCountdownTicker) {
|
|
10891
11337
|
clearInterval(bubbleCountdownTicker)
|
|
10892
11338
|
bubbleCountdownTicker = null
|
|
10893
11339
|
}
|
|
@@ -10897,6 +11343,78 @@ function bubbleCountdownRegister(el, mod) {
|
|
|
10897
11343
|
bubbleCountdownRows.push({ el: el, mod: mod })
|
|
10898
11344
|
if (!bubbleCountdownTicker) bubbleCountdownTicker = setInterval(bubbleCountdownTick, 1000)
|
|
10899
11345
|
}
|
|
11346
|
+
// —— v733:峰谷状态实时跟随 ——
|
|
11347
|
+
// count / nextpeak 样式由倒计时引擎逐秒刷新;其余峰谷样式(默认「高峰时段 / 空闲时段」、
|
|
11348
|
+
// 梁文峰谷、!?峰峰?!、简洁峰/谷)原来只在渲染那一刻取一次状态 —— 泡泡显示期间跨过峰谷切换点,
|
|
11349
|
+
// 文字与配色会一直停在旧状态。这里把这类行登记进同一个 1s ticker,状态变化时**原地**改写
|
|
11350
|
+
// 文字 / 配色 / 底色(不做整泡重绘,保持「泡泡显示期间内容稳定」的既有设计)。
|
|
11351
|
+
var bubblePeakRows = []
|
|
11352
|
+
// 文字是直接放在行上还是包一层内层 span,取决于该状态有没有底色 ——
|
|
11353
|
+
// 只有「高峰 / 空闲两态底色有无一致」时,DOM 结构才不随状态变化,才能原地改写
|
|
11354
|
+
function bubblePeakRowStable(m) {
|
|
11355
|
+
return !!(m.peakBg || m.peakBgRgb) === !!(m.offBg || m.offBgRgb)
|
|
11356
|
+
}
|
|
11357
|
+
function bubblePeakRowRegister(row, tx, mod, peakNow) {
|
|
11358
|
+
try {
|
|
11359
|
+
if (!mod || mod.type !== 'peak' || bubbleIsPeakCount(mod)) return
|
|
11360
|
+
if (!bubblePeakRowStable(mod)) return
|
|
11361
|
+
bubblePeakRows.push({
|
|
11362
|
+
row: row, tx: tx, mod: mod, peak: !!peakNow,
|
|
11363
|
+
gradEl: (mod.peakBg || mod.peakBgRgb) ? tx : row,
|
|
11364
|
+
})
|
|
11365
|
+
if (!bubbleCountdownTicker) bubbleCountdownTicker = setInterval(bubbleCountdownTick, 1000)
|
|
11366
|
+
} catch (err) {}
|
|
11367
|
+
}
|
|
11368
|
+
// 跑马灯配色方案白名单(与 blockOf() / bubbleCountdownApplyStyle 用的那套一致)
|
|
11369
|
+
function bubbleRgbSchemeOk(s) {
|
|
11370
|
+
return s === 'candy' || s === 'rouge' || s === 'bamboo' || s === 'aurora' || s === 'deepsea' ||
|
|
11371
|
+
s === 'sunset' || s === 'forest' || s === 'champagne' || s === 'lavender' || s === 'mint' ||
|
|
11372
|
+
s === 'lava' || s === 'galaxy' || s === 'ink' || s === 'indigo'
|
|
11373
|
+
}
|
|
11374
|
+
function bubblePeakRowClearClass(el, prefix) {
|
|
11375
|
+
try {
|
|
11376
|
+
var cls = Array.prototype.slice.call(el.classList || [])
|
|
11377
|
+
for (var i = 0; i < cls.length; i++) {
|
|
11378
|
+
if (String(cls[i]).indexOf(prefix) === 0) { try { el.classList.remove(cls[i]) } catch (err) {} }
|
|
11379
|
+
}
|
|
11380
|
+
} catch (err) {}
|
|
11381
|
+
}
|
|
11382
|
+
// 原地改写一行峰谷:顺序与 blockOf() 的峰谷分支一致(底色 → 文字跑马灯 → 纯色)
|
|
11383
|
+
function bubblePeakRowApply(x, peak) {
|
|
11384
|
+
var m = x.mod
|
|
11385
|
+
try { x.tx.textContent = bubbleContentText(m, bubblePeakText(m)) } catch (err) {}
|
|
11386
|
+
// ① 底色(高峰底色 / 空闲底色,跑马灯优先于纯色)
|
|
11387
|
+
try {
|
|
11388
|
+
var effBgRgb = peak ? String(m.peakBgRgb || '') : String(m.offBgRgb || '')
|
|
11389
|
+
var effBg = effBgRgb ? '' : (peak ? String(m.peakBg || '') : String(m.offBg || ''))
|
|
11390
|
+
if (effBgRgb === 'true') effBgRgb = 'macaron'
|
|
11391
|
+
bubblePeakRowClearClass(x.row, 'dshwv-bgrgb')
|
|
11392
|
+
x.row.style.background = ''
|
|
11393
|
+
if (effBgRgb) {
|
|
11394
|
+
x.row.classList.add('dshwv-bgrgb')
|
|
11395
|
+
if (bubbleRgbSchemeOk(effBgRgb) || effBgRgb === 'macaron') x.row.classList.add('dshwv-bgrgb-' + effBgRgb)
|
|
11396
|
+
x.row.style.animationDuration = bubbleMarqueeDur()
|
|
11397
|
+
} else if (effBg) {
|
|
11398
|
+
x.row.style.background = effBg
|
|
11399
|
+
}
|
|
11400
|
+
} catch (err) {}
|
|
11401
|
+
// ② 文字:本状态跑马灯 > 模块跑马灯 > 本状态纯色 > 模块纯色
|
|
11402
|
+
try {
|
|
11403
|
+
var marquee = (peak ? String(m.peakRgb || '') : String(m.offRgb || '')) || String(m.rgb || '')
|
|
11404
|
+
bubblePeakRowClearClass(x.gradEl, 'dshwv-rgb')
|
|
11405
|
+
x.row.style.color = ''
|
|
11406
|
+
if (marquee) {
|
|
11407
|
+
var scheme = marquee === true ? 'macaron' : String(marquee || 'macaron')
|
|
11408
|
+
x.gradEl.classList.add('dshwv-rgb')
|
|
11409
|
+
if (bubbleRgbSchemeOk(scheme)) x.gradEl.classList.add('dshwv-rgb-' + scheme)
|
|
11410
|
+
x.gradEl.style.animationDuration = bubbleMarqueeDur()
|
|
11411
|
+
} else {
|
|
11412
|
+
var pcol = peak ? String(m.peakColor || '') : String(m.offColor || '')
|
|
11413
|
+
if (pcol) x.row.style.color = pcol
|
|
11414
|
+
else if (m.color) x.row.style.color = String(m.color)
|
|
11415
|
+
}
|
|
11416
|
+
} catch (err) {}
|
|
11417
|
+
}
|
|
10900
11418
|
// v209: 计算单个模块要显示的行文本(与随机选中行),每次全新计算、不跨行复用状态
|
|
10901
11419
|
function bubbleRowContentOf(mod) {
|
|
10902
11420
|
mod = mod || {}
|
|
@@ -10906,7 +11424,7 @@ function bubbleRowContentOf(mod) {
|
|
|
10906
11424
|
return { txt: bubbleContentText(mod, bv), line: null }
|
|
10907
11425
|
}
|
|
10908
11426
|
if (mod.type === 'today') {
|
|
10909
|
-
var tv2 = bubbleIsModelMod(mod) ? apiModelTodayText(mod.modelId) : (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.currency) : '--')
|
|
11427
|
+
var tv2 = bubbleIsModelMod(mod) ? apiModelTodayText(mod.modelId) : (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
10910
11428
|
return { txt: bubbleContentText(mod, '今日已用 ' + tv2), line: null }
|
|
10911
11429
|
}
|
|
10912
11430
|
if (mod.type === 'quota') {
|
|
@@ -11049,7 +11567,7 @@ function bubbleRowsTo(parentEl, mods) {
|
|
|
11049
11567
|
row.style.background = effBg
|
|
11050
11568
|
}
|
|
11051
11569
|
}
|
|
11052
|
-
return { el: row, tx: tx, fSize: fSize, mod: m, peak: (m.type === 'peak' || m.type === 'nextpeak'), bg: needBg }
|
|
11570
|
+
return { el: row, tx: tx, fSize: fSize, mod: m, peak: (m.type === 'peak' || m.type === 'nextpeak'), bg: needBg, curPeak: curPeak }
|
|
11053
11571
|
}
|
|
11054
11572
|
// 超宽判定(与旧版一致):先单行渲染,测量实际超出泡泡内宽(560u)才允许该块内折行——
|
|
11055
11573
|
// 预览与真实共用此逻辑,保证两者一致
|
|
@@ -11072,6 +11590,8 @@ function bubbleRowsTo(parentEl, mods) {
|
|
|
11072
11590
|
// 倒计时块:注册文字节点(带底色时为内层 span),由每秒 ticker 刷新文案与配色
|
|
11073
11591
|
function registerIfCountdown(blk) {
|
|
11074
11592
|
if (bubbleIsPeakCount(blk.mod)) bubbleCountdownRegister(blk.tx, blk.mod)
|
|
11593
|
+
// v733:非 count 的峰谷行登记到同一个 ticker,状态切换时原地刷新(原来只在渲染时取一次)
|
|
11594
|
+
else if (blk.mod && blk.mod.type === 'peak') bubblePeakRowRegister(blk.el, blk.tx, blk.mod, blk.curPeak)
|
|
11075
11595
|
}
|
|
11076
11596
|
// 超链接模块:仅真实泡泡(textBox)内可点击,新标签页打开;预览/编辑不弹窗
|
|
11077
11597
|
function enableLinkRun(blk2) {
|
|
@@ -11479,7 +11999,7 @@ function render() {
|
|
|
11479
11999
|
hint = '加载中…'
|
|
11480
12000
|
} else {
|
|
11481
12001
|
amount = shown !== null ? fmt(shown, state.currency) : fmt(state.balance, state.currency)
|
|
11482
|
-
hint = '今日已用 ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.currency) : '--')
|
|
12002
|
+
hint = (state.usageLabel || '今日已用') + ' ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
11483
12003
|
}
|
|
11484
12004
|
amountEl.textContent = amount
|
|
11485
12005
|
if (bubbleRandomActive && bubbleRandomLines) {
|
|
@@ -11508,18 +12028,43 @@ function settle() {
|
|
|
11508
12028
|
express()
|
|
11509
12029
|
return
|
|
11510
12030
|
}
|
|
12031
|
+
// issue #102:锚点分支过去只有下限(左/顶锚甚至完全不夹),脏锚点或尺寸竞态会把挂件算到
|
|
12032
|
+
// 视口外 —— 症状是「启动闪一下 → 向下滑出/镜像 → 消失」,而且 saveConfig 会把负的离边距离
|
|
12033
|
+
// 写回 localStorage,于是刷新也恢复不了。四个锚点分支统一夹到可视区。
|
|
12034
|
+
var maxLFree = Math.max(0, vp.w - w - rightGap())
|
|
12035
|
+
var maxLAnchor = Math.max(0, vp.w - w)
|
|
12036
|
+
var maxT = Math.max(0, vp.h - h)
|
|
12037
|
+
var outOfRange = false
|
|
11511
12038
|
if (state.h === 'right') {
|
|
11512
|
-
|
|
12039
|
+
var rawR = vp.w - w - state.hOff - rightGap()
|
|
12040
|
+
state.left = clamp(rawR, 0, maxLAnchor)
|
|
12041
|
+
if (state.left !== rawR) outOfRange = true
|
|
11513
12042
|
} else if (state.h === 'left') {
|
|
11514
|
-
|
|
12043
|
+
var rawL = state.hOff
|
|
12044
|
+
state.left = clamp(rawL, 0, maxLAnchor)
|
|
12045
|
+
if (state.left !== rawL) outOfRange = true
|
|
11515
12046
|
} else {
|
|
11516
|
-
state.left = clamp(state.left, 0,
|
|
11517
|
-
}
|
|
11518
|
-
|
|
12047
|
+
state.left = clamp(state.left, 0, maxLFree)
|
|
12048
|
+
}
|
|
12049
|
+
if (state.v === 'bottom') {
|
|
12050
|
+
var rawB = vp.h - h - state.vOff
|
|
12051
|
+
state.top = clamp(rawB, 0, maxT)
|
|
12052
|
+
if (state.top !== rawB) outOfRange = true
|
|
11519
12053
|
} else if (state.v === 'top') {
|
|
11520
|
-
|
|
12054
|
+
var rawT = state.vOff
|
|
12055
|
+
state.top = clamp(rawT, 0, maxT)
|
|
12056
|
+
if (state.top !== rawT) outOfRange = true
|
|
11521
12057
|
} else {
|
|
11522
|
-
state.top = clamp(state.top, 0,
|
|
12058
|
+
state.top = clamp(state.top, 0, maxT)
|
|
12059
|
+
}
|
|
12060
|
+
// 偏移确实越界(脏数据 / 视口变小):先把偏移夹回合法范围再落盘,下次启动不再复现。
|
|
12061
|
+
// 只在真的越界时写,正常 resize 不会产生额外的 localStorage 写入。
|
|
12062
|
+
if (outOfRange) {
|
|
12063
|
+
if (state.h === 'right') state.hOff = clamp(state.hOff, 0, Math.max(0, vp.w - w - rightGap()))
|
|
12064
|
+
else if (state.h === 'left') state.hOff = clamp(state.hOff, 0, maxLAnchor)
|
|
12065
|
+
if (state.v === 'bottom') state.vOff = clamp(state.vOff, 0, maxT)
|
|
12066
|
+
else if (state.v === 'top') state.vOff = clamp(state.vOff, 0, maxT)
|
|
12067
|
+
try { saveAnchorPos() } catch (err) {}
|
|
11523
12068
|
}
|
|
11524
12069
|
refreshFlip()
|
|
11525
12070
|
}
|
|
@@ -11602,7 +12147,7 @@ function refresh(manual) {
|
|
|
11602
12147
|
ctrl = new AbortController()
|
|
11603
12148
|
timer = setTimeout(function () { try { ctrl.abort() } catch (err) {} }, FETCH_TIMEOUT_MS)
|
|
11604
12149
|
} catch (err) {}
|
|
11605
|
-
fetch(BALANCE_URL, { cache: 'no-store', signal: ctrl ? ctrl.signal : undefined })
|
|
12150
|
+
fetch(BALANCE_URL + (manual ? '?refresh=1' : ''), { cache: 'no-store', signal: ctrl ? ctrl.signal : undefined })
|
|
11606
12151
|
.then(function (r) { return r.json() })
|
|
11607
12152
|
.then(function (data) {
|
|
11608
12153
|
if (data && data.ok) {
|
|
@@ -11614,6 +12159,9 @@ function refresh(manual) {
|
|
|
11614
12159
|
state.currency = nc
|
|
11615
12160
|
state.message = ''
|
|
11616
12161
|
state.todayUsage = data.todayUsage !== undefined ? data.todayUsage : null
|
|
12162
|
+
state.todayUsageCurrency = data.todayUsageCurrency || data.currency || 'CNY'
|
|
12163
|
+
state.usageLabel = data.usageLabel || '本地估算'
|
|
12164
|
+
if (data.stale) state.usageLabel += ' · 余额未刷新'
|
|
11617
12165
|
state.isPeak = !!data.isPeak
|
|
11618
12166
|
checkUsageAlerts(nb, state.todayUsage)
|
|
11619
12167
|
if (changed && !currencyChanged) {
|
|
@@ -11669,27 +12217,90 @@ var costBubbleActive = false
|
|
|
11669
12217
|
var scrollGapOn = false
|
|
11670
12218
|
var scrollGapPx = 17
|
|
11671
12219
|
var menuBtnHide = false // 主菜单开关:隐藏挂件菜单按钮,改为右键小鲸鱼唤出菜单
|
|
12220
|
+
// —— v734(issue #97 / #88):设置保存的「防覆盖 + 失败可见」——
|
|
12221
|
+
// #97 根因:首次 GET 还没落地就 PUT,会把内存里的默认值整包写进服务端(重启后设置被洗成默认值)。
|
|
12222
|
+
// #88 根因:这个 PUT 以前是 fire-and-forget,服务端 500 / {ok:false} 完全没人读。
|
|
12223
|
+
var configLoaded = false // 首次 GET 应用完成前,一律不 PUT
|
|
12224
|
+
var configSavePending = false // 加载期间被挡下的保存,加载完成后补一次
|
|
12225
|
+
var dshwvToastEl = null
|
|
12226
|
+
var dshwvToastTimer = null
|
|
12227
|
+
// 固定定位的小提示条(自动消失;同一时刻只留一条)
|
|
12228
|
+
function dshwvToast(msg) {
|
|
12229
|
+
try {
|
|
12230
|
+
if (!dshwvToastEl || !dshwvToastEl.parentNode) {
|
|
12231
|
+
var el = document.createElement('div')
|
|
12232
|
+
el.style.cssText = 'position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:2147483600;' +
|
|
12233
|
+
'max-width:min(560px,calc(100vw - 32px));box-sizing:border-box;padding:10px 14px;border-radius:10px;' +
|
|
12234
|
+
'background:#8a1f1f;color:#fff;font-size:13px;line-height:1.6;box-shadow:0 6px 20px rgba(0,0,0,.28);' +
|
|
12235
|
+
'pointer-events:none;text-align:center'
|
|
12236
|
+
document.body.appendChild(el)
|
|
12237
|
+
dshwvToastEl = el
|
|
12238
|
+
}
|
|
12239
|
+
dshwvToastEl.innerHTML = msg
|
|
12240
|
+
if (dshwvToastTimer) clearTimeout(dshwvToastTimer)
|
|
12241
|
+
dshwvToastTimer = setTimeout(function () {
|
|
12242
|
+
try { if (dshwvToastEl && dshwvToastEl.parentNode) dshwvToastEl.parentNode.removeChild(dshwvToastEl) } catch (err) {}
|
|
12243
|
+
dshwvToastEl = null
|
|
12244
|
+
}, 8000)
|
|
12245
|
+
} catch (err) {}
|
|
12246
|
+
}
|
|
12247
|
+
function configSaveFailNotice(detail) {
|
|
12248
|
+
try { console.error('[dsh-whale] 设置保存失败:', detail) } catch (err) {}
|
|
12249
|
+
dshwvToast('⚠ 设置保存失败:' + String(detail || '').slice(0, 120) +
|
|
12250
|
+
'<br>已自动重试一次。若持续失败,请检查 DSH 数据目录是否可写。')
|
|
12251
|
+
}
|
|
12252
|
+
function configPayload() {
|
|
12253
|
+
return JSON.stringify({ scale: state.scale, sound: soundOn, vol: soundVol, soundSet: soundSet, usageMode: usageMode, peakMode: peakMode, bubbleOn: bubbleOn, turnCostOn: turnCostOn, turnCostCloseMs: turnCostCloseMs, scrollGapOn: scrollGapOn, scrollGapPx: scrollGapPx, menuBtnHide: menuBtnHide })
|
|
12254
|
+
}
|
|
12255
|
+
// 真正的 PUT:读响应 → 失败(网络异常 / HTTP!=200 / {ok:false})静默重试一次 → 仍失败才提示
|
|
12256
|
+
function configPut(payload, retried) {
|
|
12257
|
+
return fetch(SIZE_URL, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: payload })
|
|
12258
|
+
.then(function (r) {
|
|
12259
|
+
return r.json().catch(function () { return null }).then(function (d) { return { ok: r.ok, status: r.status, d: d } })
|
|
12260
|
+
})
|
|
12261
|
+
.then(function (x) {
|
|
12262
|
+
if (x.ok && (!x.d || x.d.ok !== false)) return true
|
|
12263
|
+
if (!retried) return new Promise(function (res) { setTimeout(function () { res(configPut(payload, true)) }, 900) })
|
|
12264
|
+
configSaveFailNotice((x.d && x.d.error) || ('HTTP ' + x.status))
|
|
12265
|
+
return false
|
|
12266
|
+
})
|
|
12267
|
+
.catch(function (err) {
|
|
12268
|
+
if (!retried) return new Promise(function (res) { setTimeout(function () { res(configPut(payload, true)) }, 900) })
|
|
12269
|
+
configSaveFailNotice((err && err.message) || err)
|
|
12270
|
+
return false
|
|
12271
|
+
})
|
|
12272
|
+
}
|
|
11672
12273
|
function saveConfig() {
|
|
12274
|
+
// issue #97:加载完成前只记待办,绝不 PUT(否则把默认值整包写进服务端)
|
|
12275
|
+
if (!configLoaded) { configSavePending = true; return }
|
|
11673
12276
|
try {
|
|
11674
|
-
|
|
12277
|
+
configPut(configPayload(), false)
|
|
11675
12278
|
// 锚点位置记忆:记录相对边框的离边距离,窗口 resize 后保持(localStorage)。
|
|
11676
|
-
|
|
12279
|
+
saveAnchorPos()
|
|
12280
|
+
} catch (err) {}
|
|
12281
|
+
}
|
|
12282
|
+
// 单独抽出:只写 localStorage 锚点(不碰尺寸设置);applyAnchorPos / settle 自愈时也要用。
|
|
12283
|
+
// v:2 = 净距离格式(剥离避让距离),v:1 旧格式含避让距离,恢复时废弃旧格式。
|
|
12284
|
+
function saveAnchorPos() {
|
|
12285
|
+
try {
|
|
11677
12286
|
var vp = viewport()
|
|
11678
12287
|
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
11679
12288
|
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
11680
|
-
var leftDist = state.left
|
|
11681
|
-
var rightDist = vp.w -
|
|
11682
|
-
var topDist = state.top
|
|
11683
|
-
var bottomDist = vp.h -
|
|
12289
|
+
var leftDist = isFinite(state.left) ? state.left : 0
|
|
12290
|
+
var rightDist = vp.w - leftDist - w
|
|
12291
|
+
var topDist = isFinite(state.top) ? state.top : 0
|
|
12292
|
+
var bottomDist = vp.h - topDist - h
|
|
11684
12293
|
var hAnchor = leftDist <= rightDist ? 'left' : 'right'
|
|
11685
|
-
|
|
12294
|
+
// issue #102:离边距离必须非负。挂件一旦被算到屏幕外,min(leftDist, rightDist) 就是负数,
|
|
12295
|
+
// 存进去会变成"永久坏锚点",此后每次启动都复现(刷新也恢复不了)。
|
|
12296
|
+
var hDistRaw = Math.max(0, Math.round(Math.min(leftDist, rightDist)))
|
|
11686
12297
|
var hDist = hAnchor === 'right' && scrollGapOn ? Math.max(0, hDistRaw - rightGap()) : hDistRaw
|
|
11687
12298
|
localStorage.setItem('dshw-pos', JSON.stringify({
|
|
11688
12299
|
v: 2,
|
|
11689
12300
|
hAnchor: hAnchor,
|
|
11690
12301
|
hDist: hDist,
|
|
11691
12302
|
vAnchor: topDist <= bottomDist ? 'top' : 'bottom',
|
|
11692
|
-
vDist: Math.round(Math.min(topDist, bottomDist))
|
|
12303
|
+
vDist: Math.max(0, Math.round(Math.min(topDist, bottomDist)))
|
|
11693
12304
|
}))
|
|
11694
12305
|
} catch (err) {}
|
|
11695
12306
|
}
|
|
@@ -11818,6 +12429,11 @@ var releasePlayed = false
|
|
|
11818
12429
|
var releaseTimer = null
|
|
11819
12430
|
function applySoundSet() {
|
|
11820
12431
|
try {
|
|
12432
|
+
// v729:切音效组 / 开关音效时把本轮播放状态一并复位,
|
|
12433
|
+
// 避免残留 releasePlayed=true 把新组的松开音整体吃掉(与 playPress 的修复配套)
|
|
12434
|
+
if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null }
|
|
12435
|
+
pressEnded = false
|
|
12436
|
+
releasePlayed = false
|
|
11821
12437
|
// 槽位显式留空(该事件静音)时,对应音频元素置空;playPress/playRelease 已判空
|
|
11822
12438
|
var pEmpty = audioGroupSlotEmpty(soundSet, 'press')
|
|
11823
12439
|
var rEmpty = audioGroupSlotEmpty(soundSet, 'release')
|
|
@@ -11835,19 +12451,23 @@ function applySoundSet() {
|
|
|
11835
12451
|
}
|
|
11836
12452
|
function playPress() {
|
|
11837
12453
|
if (!soundOn) return
|
|
12454
|
+
// v729 修复:**本轮状态复位必须放在「按压槽留空」分支之前**。
|
|
12455
|
+
// 原实现里 pressAudio 为空时直接 return,跳过了 releasePlayed = false;而 playRelease()
|
|
12456
|
+
// 一旦把 releasePlayed 置为 true 就再没有任何地方复位它 → 结果是只有第一次松开有声音,
|
|
12457
|
+
// 之后每次点击都静音(用户实测:新建音效组只填松开音时复现)。
|
|
12458
|
+
if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null }
|
|
12459
|
+
if (releaseAudio) {
|
|
12460
|
+
releaseAudio.pause()
|
|
12461
|
+
releaseAudio.currentTime = 0
|
|
12462
|
+
}
|
|
12463
|
+
pressEnded = false
|
|
12464
|
+
releasePlayed = false
|
|
11838
12465
|
if (!pressAudio) {
|
|
11839
|
-
//
|
|
12466
|
+
// 按压槽留空:按压事件静音,但状态已复位 → 每次松开都还能正常发声
|
|
11840
12467
|
pressEnded = true
|
|
11841
12468
|
return
|
|
11842
12469
|
}
|
|
11843
12470
|
try {
|
|
11844
|
-
if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null }
|
|
11845
|
-
if (releaseAudio) {
|
|
11846
|
-
releaseAudio.pause()
|
|
11847
|
-
releaseAudio.currentTime = 0
|
|
11848
|
-
}
|
|
11849
|
-
pressEnded = false
|
|
11850
|
-
releasePlayed = false
|
|
11851
12471
|
pressAudio.onended = function () {
|
|
11852
12472
|
pressEnded = true
|
|
11853
12473
|
// fallback (duration unknown): click → Ya2 right after Ya1 ends
|
|
@@ -13838,19 +14458,27 @@ function applyAnchorPos() {
|
|
|
13838
14458
|
var vp = viewport()
|
|
13839
14459
|
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
13840
14460
|
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
14461
|
+
var maxOffH = Math.max(0, vp.w - w)
|
|
14462
|
+
var maxOffV = Math.max(0, vp.h - h)
|
|
14463
|
+
// issue #102 自愈:非法距离(负数 = 存进去时挂件已在屏幕外;超出视口 = 窗口变小/脏数据)
|
|
14464
|
+
// 一律夹回合法范围并把修正结果落盘 —— 老用户中了脏数据也能自己恢复,无需手清 localStorage。
|
|
14465
|
+
var hDist = isFinite(a.hDist) ? clamp(a.hDist, 0, maxOffH) : 0
|
|
14466
|
+
var vDist = isFinite(a.vDist) ? clamp(a.vDist, 0, maxOffV) : 0
|
|
14467
|
+
var healed = (hDist !== a.hDist || vDist !== a.vDist)
|
|
13841
14468
|
// 与加载恢复一致:锚点存净距离,右锚点按当前避让开关叠加
|
|
13842
|
-
var effectiveRightDist = a.hAnchor === 'right' ?
|
|
13843
|
-
var l = a.hAnchor === 'left' ?
|
|
13844
|
-
var t = a.vAnchor === 'top' ?
|
|
13845
|
-
state.left = clamp(l, 0,
|
|
13846
|
-
state.top = clamp(t, 0,
|
|
14469
|
+
var effectiveRightDist = a.hAnchor === 'right' ? hDist + (scrollGapOn ? rightGap() : 0) : hDist
|
|
14470
|
+
var l = a.hAnchor === 'left' ? hDist : vp.w - effectiveRightDist - w
|
|
14471
|
+
var t = a.vAnchor === 'top' ? vDist : vp.h - vDist - h
|
|
14472
|
+
state.left = clamp(l, 0, maxOffH)
|
|
14473
|
+
state.top = clamp(t, 0, maxOffV)
|
|
13847
14474
|
state.h = a.hAnchor
|
|
13848
14475
|
// 净距离直接还给 hOff/vOff:settle() 对锚定状态从偏移量重算,
|
|
13849
14476
|
// 若置 0 会把刚恢复的距离覆盖成贴边(issue #43)
|
|
13850
|
-
state.hOff =
|
|
14477
|
+
state.hOff = hDist
|
|
13851
14478
|
state.v = a.vAnchor
|
|
13852
|
-
state.vOff =
|
|
14479
|
+
state.vOff = vDist
|
|
13853
14480
|
refreshFlip()
|
|
14481
|
+
if (healed) { try { saveAnchorPos() } catch (err) {} }
|
|
13854
14482
|
return true
|
|
13855
14483
|
} catch (err) { return false }
|
|
13856
14484
|
}
|
|
@@ -13949,33 +14577,25 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
13949
14577
|
}
|
|
13950
14578
|
// 相对边框恢复(localStorage 锚点):窗口变化后保持离边距离。
|
|
13951
14579
|
// 仅认 v:2 净距离格式;旧格式(含避让距离)废弃,挂件保持默认右下角吸附。
|
|
13952
|
-
//
|
|
13953
|
-
//
|
|
14580
|
+
// issue #102:这里原与 applyAnchorPos() 各写了一份恢复逻辑(两份都只做下限夹紧),
|
|
14581
|
+
// 现在统一走 applyAnchorPos(),避免"只修一处、另一处仍复现"。
|
|
13954
14582
|
try {
|
|
13955
|
-
|
|
13956
|
-
if (a && a.v === 2 && (a.hAnchor === 'left' || a.hAnchor === 'right') && typeof a.hDist === 'number' &&
|
|
13957
|
-
(a.vAnchor === 'top' || a.vAnchor === 'bottom') && typeof a.vDist === 'number') {
|
|
13958
|
-
var vpA = viewport()
|
|
13959
|
-
var wA = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
13960
|
-
var hA = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
13961
|
-
// 锚点存的是净距离:右锚点按当前避让开关叠加避让距离
|
|
13962
|
-
var effectiveRightDist = a.hAnchor === 'right' ? a.hDist + (scrollGapOn ? rightGap() : 0) : a.hDist
|
|
13963
|
-
var lA = a.hAnchor === 'left' ? a.hDist : vpA.w - effectiveRightDist - wA
|
|
13964
|
-
var tA = a.vAnchor === 'top' ? a.vDist : vpA.h - a.vDist - hA
|
|
13965
|
-
state.left = clamp(lA, 0, Math.max(0, vpA.w - wA))
|
|
13966
|
-
state.top = clamp(tA, 0, Math.max(0, vpA.h - hA))
|
|
13967
|
-
// 按锚点还原吸附状态(贴边锚点 → 吸附;自由位锚点 → 自由)。
|
|
13968
|
-
// 净距离还给 hOff/vOff,否则 settle() 按偏移量重算成贴边(issue #43)
|
|
13969
|
-
state.h = a.hAnchor
|
|
13970
|
-
state.hOff = a.hDist
|
|
13971
|
-
state.v = a.vAnchor
|
|
13972
|
-
state.vOff = a.vDist
|
|
14583
|
+
if (applyAnchorPos()) {
|
|
13973
14584
|
settle()
|
|
14585
|
+
// 恢复时 offsetWidth 可能还是 0(字体/图片尚未就绪),尺寸就绪后再夹一次(issue #102)
|
|
14586
|
+
setTimeout(function () { try { settle() } catch (err) {} }, 500)
|
|
13974
14587
|
}
|
|
13975
14588
|
} catch (err) {}
|
|
13976
14589
|
refresh(false)
|
|
14590
|
+
// v734(issue #97):首次 GET 应用完成 —— 从这一刻起才允许 saveConfig() 落盘
|
|
14591
|
+
configLoaded = true
|
|
14592
|
+
if (configSavePending) { configSavePending = false; try { saveConfig() } catch (err) {} }
|
|
14593
|
+
})
|
|
14594
|
+
.catch(function () {
|
|
14595
|
+
// 读取失败:绝不能拿内存里的默认值去 PUT(那正是「设置被洗成默认值」)
|
|
14596
|
+
refresh(false)
|
|
14597
|
+
dshwvToast('⚠ 设置读取失败,已暂停保存以免覆盖你的原有设置<br>请刷新页面重试')
|
|
13977
14598
|
})
|
|
13978
|
-
.catch(function () { refresh(false) })
|
|
13979
14599
|
setInterval(function () { refresh(false); try { loadApiModels(null, true) } catch (err) {} }, REFRESH_MS)
|
|
13980
14600
|
|
|
13981
14601
|
// —— 每轮对话消耗检测:轮询 last-turn.json,出现新 seq 时弹消耗金额泡泡 ——
|