dsh-whale-widget 0.3.0 → 0.3.1
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 +717 -125
- package/lib/accounting.mjs +187 -0
- package/lib/index.js +228 -207
- package/package.json +3 -2
package/assets/whale-widget.js
CHANGED
|
@@ -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('-')
|
|
@@ -1901,7 +1901,11 @@ function apiFmtMoney(v, cur) {
|
|
|
1901
1901
|
function apiUsageSourceLabel(src) {
|
|
1902
1902
|
var s = String(src || '')
|
|
1903
1903
|
if (s === 'ledger' || s === 'balance') return '余额差记账'
|
|
1904
|
-
if (s === 'events') return '
|
|
1904
|
+
if (s === 'events') return '本地估算'
|
|
1905
|
+
if (s === 'balance-observed') return '已观测消费'
|
|
1906
|
+
if (s === 'balance-needs-review') return '待核对余额调整'
|
|
1907
|
+
if (s === 'balance-corrected') return '已校正消费'
|
|
1908
|
+
if (s === 'legacy') return '旧版记录 · 未校正'
|
|
1905
1909
|
return ''
|
|
1906
1910
|
}
|
|
1907
1911
|
// 模型列表加载:并发合并 + 失败退避重试 + 失败态可重试。
|
|
@@ -2977,9 +2981,9 @@ function refreshUsageMain() {
|
|
|
2977
2981
|
// 数值由“泡泡消失→下一次显示”的渲染自然采用最新 state
|
|
2978
2982
|
if (d && d.ok && d.today && isFinite(Number(d.today.total))) {
|
|
2979
2983
|
var recTotal = Number(d.today.total)
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2984
|
+
state.todayUsage = recTotal
|
|
2985
|
+
state.todayUsageCurrency = d.today.currency || 'CNY'
|
|
2986
|
+
state.usageLabel = d.today.label || '本地估算'
|
|
2983
2987
|
}
|
|
2984
2988
|
})
|
|
2985
2989
|
.catch(function () { if (usageMainEl && !usageMainEl.firstChild) usageMainEl.textContent = '记录加载失败' })
|
|
@@ -2996,6 +3000,228 @@ function uSectionTitle(leftTxt, rightTxt) {
|
|
|
2996
3000
|
h.appendChild(r)
|
|
2997
3001
|
return h
|
|
2998
3002
|
}
|
|
3003
|
+
function accountingTime(at) {
|
|
3004
|
+
try {
|
|
3005
|
+
return new Date(at).toLocaleString('zh-CN', {
|
|
3006
|
+
timeZone: 'Asia/Shanghai', hour12: false, year: 'numeric', month: '2-digit',
|
|
3007
|
+
day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'
|
|
3008
|
+
})
|
|
3009
|
+
} catch (err) { return String(at || '') }
|
|
3010
|
+
}
|
|
3011
|
+
var accountingMask = null
|
|
3012
|
+
function openBalanceAdjustment() {
|
|
3013
|
+
if (accountingMask) return
|
|
3014
|
+
var previousFocus = document.activeElement
|
|
3015
|
+
var mask = document.createElement('div')
|
|
3016
|
+
mask.className = 'dshwv-usage-mask'
|
|
3017
|
+
mask.style.display = 'flex'
|
|
3018
|
+
mask.style.zIndex = '2147483647'
|
|
3019
|
+
var card = document.createElement('form')
|
|
3020
|
+
card.className = 'dshwv-usage-card'
|
|
3021
|
+
card.style.cssText = 'width:min(520px,calc(100vw - 32px));box-sizing:border-box;max-height:calc(100vh - 40px);overflow:auto;padding:22px;color:#203170;background:#fff;border-radius:18px;line-height:1.65'
|
|
3022
|
+
card.setAttribute('role', 'dialog')
|
|
3023
|
+
card.setAttribute('aria-modal', 'true')
|
|
3024
|
+
card.setAttribute('aria-label', '充值与余额校正')
|
|
3025
|
+
var title = document.createElement('h2')
|
|
3026
|
+
title.textContent = '充值与余额校正'
|
|
3027
|
+
title.style.cssText = 'font-size:20px;margin:0 0 8px'
|
|
3028
|
+
card.appendChild(title)
|
|
3029
|
+
var introduction = document.createElement('p')
|
|
3030
|
+
introduction.textContent = '核对统计区间内的全部到账后,重新计算本地消费。此操作不会充值,也不会改变 DeepSeek 账户余额。'
|
|
3031
|
+
introduction.style.cssText = 'font-size:13px;margin:0 0 14px'
|
|
3032
|
+
card.appendChild(introduction)
|
|
3033
|
+
function field(labelText, input) {
|
|
3034
|
+
var label = document.createElement('label')
|
|
3035
|
+
label.style.cssText = 'display:block;font-size:13px;margin:12px 0'
|
|
3036
|
+
var text = document.createElement('span')
|
|
3037
|
+
text.textContent = labelText
|
|
3038
|
+
label.appendChild(text)
|
|
3039
|
+
input.style.cssText = 'display:block;width:100%;box-sizing:border-box;min-height:40px;margin-top:5px;border:1px solid #b9c5e7;border-radius:8px;padding:8px;background:#fff;color:#203170;font:inherit'
|
|
3040
|
+
label.appendChild(input)
|
|
3041
|
+
card.appendChild(label)
|
|
3042
|
+
return text
|
|
3043
|
+
}
|
|
3044
|
+
var dates = document.createElement('select')
|
|
3045
|
+
field('记账日期(北京时间)', dates)
|
|
3046
|
+
var interval = document.createElement('div')
|
|
3047
|
+
interval.style.cssText = 'font-size:13px;white-space:pre-line;background:#f1f5ff;padding:12px;border-radius:10px'
|
|
3048
|
+
card.appendChild(interval)
|
|
3049
|
+
var credits = document.createElement('input')
|
|
3050
|
+
credits.type = 'text'
|
|
3051
|
+
credits.inputMode = 'decimal'
|
|
3052
|
+
credits.required = true
|
|
3053
|
+
credits.autocomplete = 'off'
|
|
3054
|
+
var creditsLabel = field('本统计区间累计到账金额', credits)
|
|
3055
|
+
var debits = document.createElement('input')
|
|
3056
|
+
debits.type = 'text'
|
|
3057
|
+
debits.inputMode = 'decimal'
|
|
3058
|
+
debits.autocomplete = 'off'
|
|
3059
|
+
var debitsLabel = field('非调用造成的余额减少(没有请填 0)', debits)
|
|
3060
|
+
var help = document.createElement('p')
|
|
3061
|
+
help.style.cssText = 'font-size:12px;margin:8px 0;color:#4c5e87'
|
|
3062
|
+
help.textContent = '到账包括充值、赠金等;多次到账请填合计,不要只填最后一笔。非调用减少可包含到期赠金、余额退回等。仅填写上方统计起点之后的金额;保存会替换之前的校正值。'
|
|
3063
|
+
card.appendChild(help)
|
|
3064
|
+
var preview = document.createElement('div')
|
|
3065
|
+
preview.style.cssText = 'font-weight:600;font-size:15px;margin:14px 0'
|
|
3066
|
+
preview.setAttribute('aria-live', 'polite')
|
|
3067
|
+
card.appendChild(preview)
|
|
3068
|
+
var confirmRow = document.createElement('label')
|
|
3069
|
+
confirmRow.style.cssText = 'display:flex;gap:8px;align-items:flex-start;font-size:13px;margin:12px 0;cursor:pointer'
|
|
3070
|
+
var confirm = document.createElement('input')
|
|
3071
|
+
confirm.type = 'checkbox'
|
|
3072
|
+
confirm.style.marginTop = '5px'
|
|
3073
|
+
confirmRow.appendChild(confirm)
|
|
3074
|
+
confirmRow.appendChild(document.createTextNode('我已核对本统计区间的全部到账和非调用扣减'))
|
|
3075
|
+
card.appendChild(confirmRow)
|
|
3076
|
+
var status = document.createElement('div')
|
|
3077
|
+
status.setAttribute('role', 'status')
|
|
3078
|
+
status.style.cssText = 'font-size:13px;white-space:normal;color:#b33333;margin:8px 0'
|
|
3079
|
+
status.textContent = '正在刷新余额…'
|
|
3080
|
+
card.appendChild(status)
|
|
3081
|
+
var actions = document.createElement('div')
|
|
3082
|
+
actions.style.cssText = 'display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;margin-top:16px'
|
|
3083
|
+
function button(text, callback) {
|
|
3084
|
+
var btn = document.createElement('button')
|
|
3085
|
+
btn.type = 'button'
|
|
3086
|
+
btn.textContent = text
|
|
3087
|
+
btn.className = 'dshwv-bubbtn dshwv-bubbtn-no'
|
|
3088
|
+
btn.style.cssText = 'min-height:38px;padding:8px 12px;cursor:pointer'
|
|
3089
|
+
if (callback) btn.addEventListener('click', callback)
|
|
3090
|
+
actions.appendChild(btn)
|
|
3091
|
+
return btn
|
|
3092
|
+
}
|
|
3093
|
+
var busy = false
|
|
3094
|
+
function close() {
|
|
3095
|
+
if (busy) return
|
|
3096
|
+
document.removeEventListener('keydown', keyHandler)
|
|
3097
|
+
mask.remove()
|
|
3098
|
+
accountingMask = null
|
|
3099
|
+
try { previousFocus.focus() } catch (err) {}
|
|
3100
|
+
}
|
|
3101
|
+
function keyHandler(e) {
|
|
3102
|
+
if (e.key === 'Escape') { e.preventDefault(); close() }
|
|
3103
|
+
if (e.key === 'Tab') {
|
|
3104
|
+
var focusable = Array.prototype.filter.call(card.querySelectorAll('button,input,select'), function (el) {
|
|
3105
|
+
return !el.disabled && el.style.display !== 'none'
|
|
3106
|
+
})
|
|
3107
|
+
var first = focusable[0], last = focusable[focusable.length - 1]
|
|
3108
|
+
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus() }
|
|
3109
|
+
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
var reload = button('重新读取', function () { load() })
|
|
3113
|
+
var reset = button('撤销该日校正', function () { submit('reset') })
|
|
3114
|
+
button('取消', close)
|
|
3115
|
+
var save = button('保存校正')
|
|
3116
|
+
save.type = 'submit'
|
|
3117
|
+
save.style.background = '#315de8'
|
|
3118
|
+
save.style.color = '#fff'
|
|
3119
|
+
save.style.border = '1px solid #315de8'
|
|
3120
|
+
card.appendChild(actions)
|
|
3121
|
+
var scrollBody = document.createElement('div')
|
|
3122
|
+
scrollBody.style.cssText = 'min-height:0;overflow:auto;flex:1 1 auto;padding-right:3px'
|
|
3123
|
+
while (card.firstChild && card.firstChild !== actions) scrollBody.appendChild(card.firstChild)
|
|
3124
|
+
card.insertBefore(scrollBody, actions)
|
|
3125
|
+
card.style.display = 'flex'
|
|
3126
|
+
card.style.flexDirection = 'column'
|
|
3127
|
+
card.style.overflow = 'hidden'
|
|
3128
|
+
actions.style.flex = '0 0 auto'
|
|
3129
|
+
actions.style.paddingTop = '10px'
|
|
3130
|
+
actions.style.borderTop = '1px solid #e2e8f5'
|
|
3131
|
+
mask.appendChild(card)
|
|
3132
|
+
mask.addEventListener('click', function (e) { if (e.target === mask) close() })
|
|
3133
|
+
document.body.appendChild(mask)
|
|
3134
|
+
accountingMask = mask
|
|
3135
|
+
document.addEventListener('keydown', keyHandler)
|
|
3136
|
+
var rows = []
|
|
3137
|
+
var selected = null
|
|
3138
|
+
function renderSelection() {
|
|
3139
|
+
selected = rows.find(function (row) { return row.day === dates.value }) || null
|
|
3140
|
+
save.disabled = !selected || busy
|
|
3141
|
+
reset.disabled = !selected || !selected.correctedAt || busy
|
|
3142
|
+
if (!selected) {
|
|
3143
|
+
interval.textContent = '尚无可校正的余额观测。请先配置 DeepSeek API key 并成功刷新余额。'
|
|
3144
|
+
return
|
|
3145
|
+
}
|
|
3146
|
+
interval.textContent = '统计起点:' + accountingTime(selected.firstObservedAt) +
|
|
3147
|
+
'\n最近观测:' + accountingTime(selected.lastObservedAt) +
|
|
3148
|
+
'\n起点余额 ' + usageMoney(selected.openingBalance, selected.currency) +
|
|
3149
|
+
' → 当前余额 ' + usageMoney(selected.currentBalance, selected.currency) +
|
|
3150
|
+
'\n当前:' + selected.label + ' ' + usageMoney(selected.amount, selected.currency)
|
|
3151
|
+
creditsLabel.textContent = '本统计区间累计到账金额(' + selected.currency + ',未到账请填 0)'
|
|
3152
|
+
debitsLabel.textContent = '非调用造成的余额减少(' + selected.currency + ',没有请填 0)'
|
|
3153
|
+
credits.value = selected.credits == null ? '' : String(selected.credits)
|
|
3154
|
+
debits.value = selected.otherDebits == null ? '0' : String(selected.otherDebits)
|
|
3155
|
+
confirm.checked = false
|
|
3156
|
+
updatePreview()
|
|
3157
|
+
}
|
|
3158
|
+
function updatePreview() {
|
|
3159
|
+
if (!selected || credits.value.trim() === '' || !isFinite(Number(credits.value)) || !isFinite(Number(debits.value))) {
|
|
3160
|
+
preview.textContent = '填写完整金额后显示校正预览'
|
|
3161
|
+
return
|
|
3162
|
+
}
|
|
3163
|
+
var amount = selected.openingBalance + Number(credits.value) - Number(debits.value || 0) - selected.currentBalance
|
|
3164
|
+
preview.textContent = amount < -0.000000005 ? '校正后为负数,请核对金额和统计区间' : '校正后消费:' + usageMoney(Math.max(0, amount), selected.currency)
|
|
3165
|
+
}
|
|
3166
|
+
credits.addEventListener('input', updatePreview)
|
|
3167
|
+
debits.addEventListener('input', updatePreview)
|
|
3168
|
+
dates.addEventListener('change', renderSelection)
|
|
3169
|
+
function load() {
|
|
3170
|
+
if (busy) return
|
|
3171
|
+
busy = true
|
|
3172
|
+
save.disabled = true
|
|
3173
|
+
reset.disabled = true
|
|
3174
|
+
reload.disabled = true
|
|
3175
|
+
status.textContent = '正在刷新余额…'
|
|
3176
|
+
fetch('/dsh-whale/balance-adjustments.json', { cache: 'no-store' })
|
|
3177
|
+
.then(function (r) { return r.json() })
|
|
3178
|
+
.then(function (data) {
|
|
3179
|
+
if (!data.ok) throw new Error(data.error || '读取失败')
|
|
3180
|
+
rows = data.days || []
|
|
3181
|
+
dates.innerHTML = ''
|
|
3182
|
+
rows.forEach(function (row) {
|
|
3183
|
+
var option = document.createElement('option')
|
|
3184
|
+
option.value = row.day
|
|
3185
|
+
option.textContent = row.day + ' · ' + row.label
|
|
3186
|
+
dates.appendChild(option)
|
|
3187
|
+
})
|
|
3188
|
+
status.textContent = data.error ? '余额暂未刷新:' + data.error : ''
|
|
3189
|
+
})
|
|
3190
|
+
.catch(function (err) { status.textContent = err.message || '读取失败' })
|
|
3191
|
+
.finally(function () { busy = false; reload.disabled = false; renderSelection(); dates.focus() })
|
|
3192
|
+
}
|
|
3193
|
+
function submit(action) {
|
|
3194
|
+
if (busy || !selected) return
|
|
3195
|
+
if (action !== 'reset' && !confirm.checked) { status.textContent = '请先勾选确认,核对本统计区间的全部余额调整。'; return }
|
|
3196
|
+
busy = true
|
|
3197
|
+
save.disabled = true
|
|
3198
|
+
reset.disabled = true
|
|
3199
|
+
reload.disabled = true
|
|
3200
|
+
status.textContent = '正在保存…'
|
|
3201
|
+
fetch('/dsh-whale/balance-adjustments.json', {
|
|
3202
|
+
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
3203
|
+
body: JSON.stringify({
|
|
3204
|
+
day: selected.day, revision: selected.revision, action: action,
|
|
3205
|
+
credits: credits.value.trim(), otherDebits: debits.value.trim() || '0', confirmed: confirm.checked
|
|
3206
|
+
})
|
|
3207
|
+
})
|
|
3208
|
+
.then(function (r) { return r.json() })
|
|
3209
|
+
.then(function (data) {
|
|
3210
|
+
if (!data.ok) throw new Error(data.error || '保存失败')
|
|
3211
|
+
busy = false
|
|
3212
|
+
close()
|
|
3213
|
+
refresh(true)
|
|
3214
|
+
loadApiModels(function () {
|
|
3215
|
+
if (usagePanelOpen) rebuildUsageSubShell()
|
|
3216
|
+
}, true)
|
|
3217
|
+
})
|
|
3218
|
+
.catch(function (err) { status.textContent = err.message || '保存失败' })
|
|
3219
|
+
.finally(function () { busy = false; save.disabled = !selected; reset.disabled = !selected || !selected.correctedAt; reload.disabled = false })
|
|
3220
|
+
}
|
|
3221
|
+
card.addEventListener('submit', function (e) { e.preventDefault(); submit('save') })
|
|
3222
|
+
load()
|
|
3223
|
+
}
|
|
3224
|
+
|
|
2999
3225
|
function fillUsagePanel(d) {
|
|
3000
3226
|
var hostEl = usageMainEl || usagePanel
|
|
3001
3227
|
hostEl.innerHTML = ''
|
|
@@ -3010,9 +3236,26 @@ function fillUsagePanel(d) {
|
|
|
3010
3236
|
var todayModels = today.models || []
|
|
3011
3237
|
var hasEvToday = todayModels.length > 0
|
|
3012
3238
|
// ① 今日模型消费(独立容器+滚动);标题不带下分隔线
|
|
3013
|
-
var tToday = uSectionTitle('
|
|
3239
|
+
var tToday = uSectionTitle(today.label || '今日消费', usageMoney(today.total, today.currency))
|
|
3014
3240
|
tToday.style.borderBottom = 'none'
|
|
3015
3241
|
wrap.appendChild(tToday)
|
|
3242
|
+
var coverage = document.createElement('div')
|
|
3243
|
+
coverage.className = 'dshwv-usage-hint'
|
|
3244
|
+
coverage.style.cssText = 'line-height:1.65;white-space:normal;margin:4px 0'
|
|
3245
|
+
coverage.textContent = today.firstObservedAt
|
|
3246
|
+
? '统计起点(北京):' + accountingTime(today.firstObservedAt) + '。起点前的消费未计入。'
|
|
3247
|
+
: '尚无余额观测;此处仅显示本机会话估算或旧版记录。'
|
|
3248
|
+
if (today.needsReview) coverage.textContent += ' 检测到余额增加,请核对本区间累计到账。'
|
|
3249
|
+
wrap.appendChild(coverage)
|
|
3250
|
+
var adjustButton = document.createElement('button')
|
|
3251
|
+
adjustButton.type = 'button'
|
|
3252
|
+
adjustButton.className = 'dshwv-usage-more'
|
|
3253
|
+
adjustButton.textContent = '充值 / 余额校正'
|
|
3254
|
+
adjustButton.addEventListener('click', function (e) { e.stopPropagation(); openBalanceAdjustment() })
|
|
3255
|
+
wrap.appendChild(adjustButton)
|
|
3256
|
+
var modelTitle = uSectionTitle('本机模型费用 · 估算', usageMoney(today.modelTotal, 'CNY'))
|
|
3257
|
+
modelTitle.title = '按本机 DSH 会话计算,不按账户余额比例缩放;两者覆盖范围不同。'
|
|
3258
|
+
wrap.appendChild(modelTitle)
|
|
3016
3259
|
var todayBox = document.createElement('div')
|
|
3017
3260
|
todayBox.className = 'dshwv-usage-scroll dshwv-usage-today'
|
|
3018
3261
|
if (hasEvToday) {
|
|
@@ -3030,7 +3273,7 @@ function fillUsagePanel(d) {
|
|
|
3030
3273
|
} else if ((today.total || 0) > 0) {
|
|
3031
3274
|
var noM = document.createElement('div')
|
|
3032
3275
|
noM.className = 'dshwv-usage-hint'
|
|
3033
|
-
noM.textContent = '
|
|
3276
|
+
noM.textContent = '暂无本机会话费用明细;账户余额观测仍独立记账。'
|
|
3034
3277
|
todayBox.appendChild(noM)
|
|
3035
3278
|
} else {
|
|
3036
3279
|
var empty = document.createElement('div')
|
|
@@ -3045,7 +3288,7 @@ function fillUsagePanel(d) {
|
|
|
3045
3288
|
sep7.style.borderTop = '1px solid rgba(32,49,112,.15)'
|
|
3046
3289
|
sep7.style.margin = '6px 0'
|
|
3047
3290
|
wrap.appendChild(sep7)
|
|
3048
|
-
var t7 = uSectionTitle('近7天使用记录', usageMoney(d.total7))
|
|
3291
|
+
var t7 = uSectionTitle('近7天使用记录', usageMoney(d.total7, d.total7Currency))
|
|
3049
3292
|
t7.style.borderBottom = 'none'
|
|
3050
3293
|
wrap.appendChild(t7)
|
|
3051
3294
|
var daysBox = document.createElement('div')
|
|
@@ -3055,9 +3298,10 @@ function fillUsagePanel(d) {
|
|
|
3055
3298
|
r.className = 'dshwv-usage-row'
|
|
3056
3299
|
var n = document.createElement('span')
|
|
3057
3300
|
n.textContent = usageDayLabel(row.date)
|
|
3301
|
+
n.title = row.label || ''
|
|
3058
3302
|
r.appendChild(n)
|
|
3059
3303
|
var c = document.createElement('span')
|
|
3060
|
-
c.textContent = usageMoney(row.total)
|
|
3304
|
+
c.textContent = usageMoney(row.total, row.currency)
|
|
3061
3305
|
r.appendChild(c)
|
|
3062
3306
|
daysBox.appendChild(r)
|
|
3063
3307
|
})
|
|
@@ -3856,8 +4100,8 @@ function usageDrawBarChart(body, secTitle, days, opts) {
|
|
|
3856
4100
|
g.fillStyle = '#9fb0d9'
|
|
3857
4101
|
g.font = '10px sans-serif'
|
|
3858
4102
|
g.textAlign = 'right'
|
|
3859
|
-
g.fillText(usageMoney(max), padL - 4, padT + 8)
|
|
3860
|
-
g.fillText(usageMoney(max / 2), padL - 4, padT + ih / 2 + 3)
|
|
4103
|
+
g.fillText(usageMoney(max, opts.currency), padL - 4, padT + 8)
|
|
4104
|
+
g.fillText(usageMoney(max / 2, opts.currency), padL - 4, padT + ih / 2 + 3)
|
|
3861
4105
|
g.fillText('¥0', padL - 4, padT + ih + 4)
|
|
3862
4106
|
}
|
|
3863
4107
|
function move(ev) {
|
|
@@ -3870,7 +4114,7 @@ function usageDrawBarChart(body, secTitle, days, opts) {
|
|
|
3870
4114
|
paint(hover)
|
|
3871
4115
|
if (hover >= 0) {
|
|
3872
4116
|
tip.style.display = 'block'
|
|
3873
|
-
tip.textContent = bars[hover].day.date + ' ' + usageMoney(bars[hover].day.total)
|
|
4117
|
+
tip.textContent = bars[hover].day.date + ' ' + usageMoney(bars[hover].day.total, opts.currency)
|
|
3874
4118
|
var wr = wrap.getBoundingClientRect()
|
|
3875
4119
|
var tx = ev.clientX - wr.left + 10
|
|
3876
4120
|
if (tx + 130 > wr.width) tx = ev.clientX - wr.left - 140
|
|
@@ -4006,31 +4250,37 @@ function fillUsageRecordsWindow(d) {
|
|
|
4006
4250
|
var allDays = ((d.all && d.all.days) || []).slice().sort(function (a, b) { return a.date < b.date ? -1 : 1 })
|
|
4007
4251
|
var evAll = ((d.all && d.all.events) || []).slice()
|
|
4008
4252
|
// ① 概览头
|
|
4009
|
-
var
|
|
4253
|
+
var sumAllByCurrency = {}
|
|
4254
|
+
var chartCurrency = (d.today && d.today.currency) || 'CNY'
|
|
4010
4255
|
var maxDay = null
|
|
4011
4256
|
for (var s1 = 0; s1 < allDays.length; s1++) {
|
|
4012
|
-
|
|
4013
|
-
|
|
4257
|
+
var dayCurrency = allDays[s1].currency || 'CNY'
|
|
4258
|
+
sumAllByCurrency[dayCurrency] = (sumAllByCurrency[dayCurrency] || 0) + (Number(allDays[s1].total) || 0)
|
|
4259
|
+
if (dayCurrency === chartCurrency && (!maxDay || Number(allDays[s1].total) > Number(maxDay.total))) maxDay = allDays[s1]
|
|
4014
4260
|
}
|
|
4015
4261
|
var ov = document.createElement('div')
|
|
4016
4262
|
ov.className = 'dshwv-usage-oview'
|
|
4017
4263
|
var ovL = document.createElement('div')
|
|
4018
|
-
ovL.textContent = '
|
|
4264
|
+
ovL.textContent = '全部记录合计'
|
|
4019
4265
|
ov.appendChild(ovL)
|
|
4020
4266
|
var ovN = document.createElement('div')
|
|
4021
4267
|
ovN.className = 'dshwv-usage-oview-num'
|
|
4022
|
-
ovN.textContent = usageMoney(
|
|
4268
|
+
ovN.textContent = Object.keys(sumAllByCurrency).map(function (cur) { return usageMoney(sumAllByCurrency[cur], cur) }).join(' + ') || usageMoney(0, chartCurrency)
|
|
4023
4269
|
ov.appendChild(ovN)
|
|
4024
4270
|
var ovS = document.createElement('div')
|
|
4025
4271
|
ovS.className = 'dshwv-usage-hint'
|
|
4026
|
-
ovS.textContent = '共 ' + evAll.length + ' 笔明细' + (maxDay ? ' · 峰值 ' + maxDay.date + ' ' + usageMoney(maxDay.total) : '')
|
|
4272
|
+
ovS.textContent = '共 ' + evAll.length + ' 笔明细' + (maxDay ? ' · 峰值 ' + maxDay.date + ' ' + usageMoney(maxDay.total, maxDay.currency) : '')
|
|
4027
4273
|
ov.appendChild(ovS)
|
|
4274
|
+
var sourceNote = document.createElement('div')
|
|
4275
|
+
sourceNote.className = 'dshwv-usage-hint'
|
|
4276
|
+
sourceNote.textContent = '按各日标注口径汇总;本机模型明细为估算,与账户消费覆盖范围不同。'
|
|
4277
|
+
ov.appendChild(sourceNote)
|
|
4028
4278
|
body.appendChild(ov)
|
|
4029
4279
|
var detailBox = null
|
|
4030
4280
|
// ② 统计图表(默认折叠,懒渲染)
|
|
4031
4281
|
usageCollapseBlock(body, '统计图表(近30天 / 模型占比)', false, function (inner) {
|
|
4032
|
-
usageDrawBarChart(inner, '近30天消费
|
|
4033
|
-
today: usageTodayKeyStr(),
|
|
4282
|
+
usageDrawBarChart(inner, '近30天消费 · ' + chartCurrency + '(点柱查看当日)', allDays.filter(function (row) { return (row.currency || 'CNY') === chartCurrency }).slice(-30), {
|
|
4283
|
+
today: usageTodayKeyStr(), currency: chartCurrency,
|
|
4034
4284
|
onPick: function (date) {
|
|
4035
4285
|
try {
|
|
4036
4286
|
if (!detailBox) return
|
|
@@ -4047,9 +4297,9 @@ function fillUsageRecordsWindow(d) {
|
|
|
4047
4297
|
},
|
|
4048
4298
|
})
|
|
4049
4299
|
var todayAgg = usageAggModels(d.today && d.today.models ? [{ models: d.today.models }] : [])
|
|
4050
|
-
usageRatioRows(inner, '
|
|
4300
|
+
usageRatioRows(inner, '今日模型费用 · 估算', todayAgg, usageMoney((d.today && d.today.modelTotal) || 0))
|
|
4051
4301
|
var sevenAgg = usageAggModels(d.days7 || [])
|
|
4052
|
-
usageRatioRows(inner, '近7
|
|
4302
|
+
usageRatioRows(inner, '近7天模型费用 · 估算', sevenAgg, usageMoney((d.days7 || []).reduce(function (sum, row) { return sum + (Number(row.modelTotal) || 0) }, 0)))
|
|
4053
4303
|
})
|
|
4054
4304
|
// ③ 每日与逐条明细(默认折叠;搜索/限量加载)
|
|
4055
4305
|
detailBox = usageCollapseBlock(body, '每日与逐条明细', false, function (inner) {
|
|
@@ -4071,7 +4321,8 @@ function fillUsageRecordsWindow(d) {
|
|
|
4071
4321
|
;(evMap[day] = evMap[day] || []).push(ev)
|
|
4072
4322
|
})
|
|
4073
4323
|
var dayTot = {}
|
|
4074
|
-
|
|
4324
|
+
var dayMeta = {}
|
|
4325
|
+
allDays.forEach(function (dx) { dayTot[dx.date] = Number(dx.total) || 0; dayMeta[dx.date] = dx })
|
|
4075
4326
|
var todayKeyStr2 = usageTodayKeyStr()
|
|
4076
4327
|
function dayGroup(day, evs) {
|
|
4077
4328
|
var row = document.createElement('div')
|
|
@@ -4091,7 +4342,8 @@ function fillUsageRecordsWindow(d) {
|
|
|
4091
4342
|
var dayV = dayTot[day]
|
|
4092
4343
|
if ((dayV === undefined || dayV === 0) && day === todayKeyStr2 && d.today && isFinite(Number(d.today.total))) dayV = Number(d.today.total)
|
|
4093
4344
|
if (dayV === undefined || dayV === null) dayV = evs.reduce(function (a, x) { return a + (Number(x.cost) || 0) }, 0)
|
|
4094
|
-
c.textContent = usageMoney(dayV)
|
|
4345
|
+
c.textContent = usageMoney(dayV, dayMeta[day] && dayMeta[day].currency)
|
|
4346
|
+
name.title = (dayMeta[day] && dayMeta[day].label) || '本地估算'
|
|
4095
4347
|
row.appendChild(c)
|
|
4096
4348
|
var chev = document.createElement('span')
|
|
4097
4349
|
chev.className = 'dshwv-usage-chev'
|
|
@@ -4124,6 +4376,7 @@ function fillUsageRecordsWindow(d) {
|
|
|
4124
4376
|
var c2 = document.createElement('span')
|
|
4125
4377
|
c2.style.flex = '0 0 auto'
|
|
4126
4378
|
c2.textContent = usageMoney(ev.cost)
|
|
4379
|
+
c2.title = '本地估算:' + Number(ev.cost || 0).toFixed(8) + ' CNY'
|
|
4127
4380
|
r2.appendChild(c2)
|
|
4128
4381
|
detail.appendChild(r2)
|
|
4129
4382
|
}
|
|
@@ -5751,7 +6004,7 @@ function bubbleModuleSummary(m) {
|
|
|
5751
6004
|
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '余额数值'
|
|
5752
6005
|
if (m.type === 'today') return bubbleIsModelMod(m) ? ('今日已用·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '今日已用'
|
|
5753
6006
|
if (m.type === 'quota') return '额度·' + ((apiModelById(m.modelId) || {}).name || m.modelId)
|
|
5754
|
-
if (m.type === 'plan') return
|
|
6007
|
+
if (m.type === 'plan') return bubblePlanModuleLabel(m)
|
|
5755
6008
|
if (m.type === 'peak' || m.type === 'nextpeak') return bubblePeakModuleLabel(m)
|
|
5756
6009
|
if (m.type === 'image') return '图片/动图'
|
|
5757
6010
|
if (m.type === 'randimg') return '随机图片' + (m.imgs && m.imgs.length ? '(' + m.imgs.length + '张)' : '(空)')
|
|
@@ -5768,7 +6021,7 @@ function bubbleModuleListLabel(m) {
|
|
|
5768
6021
|
if (m.type === 'balance') return bubbleIsModelMod(m) ? ('余额·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '余额数值'
|
|
5769
6022
|
if (m.type === 'today') return bubbleIsModelMod(m) ? ('今日已用·' + ((apiModelBalanceInfo(m.modelId) || {}).name || m.modelId)) : '今日已用'
|
|
5770
6023
|
if (m.type === 'quota') return '额度·' + ((apiModelById(m.modelId) || {}).name || m.modelId)
|
|
5771
|
-
if (m.type === 'plan') return
|
|
6024
|
+
if (m.type === 'plan') return bubblePlanModuleLabel(m)
|
|
5772
6025
|
if (m.type === 'peak' || m.type === 'nextpeak') return bubblePeakModuleLabel(m)
|
|
5773
6026
|
if (m.type === 'image') return '图片/动图'
|
|
5774
6027
|
if (m.type === 'randimg') return '随机图片' + (m.imgs && m.imgs.length ? '(' + m.imgs.length + '张)' : '(空)')
|
|
@@ -6779,8 +7032,8 @@ function openQuickModuleEditor(m, anchorBtn) {
|
|
|
6779
7032
|
var isModelBal = bubbleIsModelMod(m) && (m.type === 'balance' || m.type === 'today')
|
|
6780
7033
|
var isModelQuota = bubbleIsModelMod(m) && m.type === 'quota'
|
|
6781
7034
|
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}')))))
|
|
7035
|
+
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}')))))
|
|
7036
|
+
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
7037
|
inp.addEventListener('input', function () { m.tpl = inp.value; changed() })
|
|
6785
7038
|
r.appendChild(inp)
|
|
6786
7039
|
var qb2 = document.createElement('button')
|
|
@@ -6794,6 +7047,25 @@ function openQuickModuleEditor(m, anchorBtn) {
|
|
|
6794
7047
|
box.appendChild(r)
|
|
6795
7048
|
}
|
|
6796
7049
|
tplRow()
|
|
7050
|
+
// 订阅额度模块的「显示样式」= 选时间窗口(多窗口厂商,如 OpenCode Go 的 5h / 周 / 月)
|
|
7051
|
+
if (m.type === 'plan' && (apiPlanMultiWin(m.modelId) || apiPlanWinList(m.modelId))) {
|
|
7052
|
+
var wrow = qRow()
|
|
7053
|
+
wrow.appendChild(qLabel('显示样式'))
|
|
7054
|
+
var wsel = document.createElement('select')
|
|
7055
|
+
for (var wi = 0; wi < BUBBLE_PLAN_WIN_OPTS.length; wi++) {
|
|
7056
|
+
var wo = document.createElement('option')
|
|
7057
|
+
wo.value = BUBBLE_PLAN_WIN_OPTS[wi][0]
|
|
7058
|
+
wo.textContent = BUBBLE_PLAN_WIN_OPTS[wi][1]
|
|
7059
|
+
wsel.appendChild(wo)
|
|
7060
|
+
}
|
|
7061
|
+
wsel.value = bubblePlanWinOf(m)
|
|
7062
|
+
wsel.style.flex = '1'
|
|
7063
|
+
wsel.style.minWidth = '0'
|
|
7064
|
+
wrow.appendChild(wsel)
|
|
7065
|
+
box.appendChild(wrow)
|
|
7066
|
+
dshwCustSel(wsel)
|
|
7067
|
+
wsel.addEventListener('change', function () { m.planWin = wsel.value || 'all'; changed() })
|
|
7068
|
+
}
|
|
6797
7069
|
if (m.type === 'peak' || m.type === 'nextpeak') {
|
|
6798
7070
|
// 显示样式(与编辑窗口一致)
|
|
6799
7071
|
var srow = qRow()
|
|
@@ -6985,6 +7257,32 @@ function renderBubblePal() {
|
|
|
6985
7257
|
if (apiModelsLoaded) {
|
|
6986
7258
|
apiModels.forEach(function (am) {
|
|
6987
7259
|
if (!am || !am.id || am.builtin) return
|
|
7260
|
+
var planSupported = apiPlanSupport(am.id)
|
|
7261
|
+
// 只有「一个接口返回多个窗口」的额度厂商(如 OpenCode Go)才把调色板收成一个「额度」模块;
|
|
7262
|
+
// 单窗口的既有额度厂商(智谱 / Kimi / MiniMax Coding)保持上游原有的三个模块不变
|
|
7263
|
+
if (planSupported && apiPlanMultiWin(am.id)) {
|
|
7264
|
+
// 订阅额度厂商(OpenCode Go / 智谱 / Kimi / MiniMax Coding 等):这类厂商本来就没有余额接口,
|
|
7265
|
+
// 只给一个「额度」模块 —— 时间窗口(5h / 周 / 月 / 全部)与显示内容都在模块编辑器里选,
|
|
7266
|
+
// 不再并列「余额 / 手动额度 / 订阅额度」三个模块。
|
|
7267
|
+
var key4 = 'pq:' + am.id
|
|
7268
|
+
var chip4 = document.createElement('div')
|
|
7269
|
+
chip4.className = 'dshwv-palchip'
|
|
7270
|
+
chip4.setAttribute('data-pal', key4)
|
|
7271
|
+
chip4.textContent = '额度·' + am.name
|
|
7272
|
+
var planWinHint = apiPlanMultiWin(am.id) ? ';「显示样式」可选时间窗口(全部 / 5h / 周 / 月)' : ''
|
|
7273
|
+
chip4.title = '该厂商的订阅额度(由厂商接口读取,非手动);模板变量 {plan} 额度 / {plan_reset} 刷新时间 / {plan_left} 剩余' + planWinHint
|
|
7274
|
+
chip4.draggable = true
|
|
7275
|
+
chip4.addEventListener('click', function (e) {
|
|
7276
|
+
e.stopPropagation()
|
|
7277
|
+
bubbleModuleAdd({ type: 'plan', modelId: am.id, size: 8, tpl: '{plan} · {plan_reset}', planWin: 'all' })
|
|
7278
|
+
})
|
|
7279
|
+
chip4.addEventListener('dragstart', function (e) {
|
|
7280
|
+
try { e.dataTransfer.setData('text/plain', key4) } catch (err) {}
|
|
7281
|
+
bubbleDragKey = key4
|
|
7282
|
+
})
|
|
7283
|
+
bubblePalEl.appendChild(chip4)
|
|
7284
|
+
return
|
|
7285
|
+
}
|
|
6988
7286
|
var key = 'bal:' + am.id
|
|
6989
7287
|
var chip2 = document.createElement('div')
|
|
6990
7288
|
chip2.className = 'dshwv-palchip'
|
|
@@ -7018,25 +7316,6 @@ function renderBubblePal() {
|
|
|
7018
7316
|
bubbleDragKey = key3
|
|
7019
7317
|
})
|
|
7020
7318
|
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
7319
|
})
|
|
7041
7320
|
} else if (apiModelsError) {
|
|
7042
7321
|
var chipErr = document.createElement('div')
|
|
@@ -7286,11 +7565,11 @@ function bubblePaletteModule(key) {
|
|
|
7286
7565
|
if (!am1) return null
|
|
7287
7566
|
return { type: 'quota', modelId: am1.id, size: 8, tpl: '已用 {quota} · 剩 {quota_left}' }
|
|
7288
7567
|
}
|
|
7289
|
-
// 厂商订阅额度模块(palette key = pq:<modelId
|
|
7568
|
+
// 厂商订阅额度模块(palette key = pq:<modelId>):时间窗口由模块里的「显示样式」决定
|
|
7290
7569
|
if (typeof key === 'string' && key.indexOf('pq:') === 0) {
|
|
7291
7570
|
var am2 = apiModelById(key.slice(3))
|
|
7292
7571
|
if (!am2) return null
|
|
7293
|
-
return { type: 'plan', modelId: am2.id, size: 8, tpl: '
|
|
7572
|
+
return { type: 'plan', modelId: am2.id, size: 8, tpl: '{plan} · {plan_reset}', planWin: 'all' }
|
|
7294
7573
|
}
|
|
7295
7574
|
if (key === 'random') return bubbleCloneModule(bubbleDefaultSecondModules()[0])
|
|
7296
7575
|
if (typeof key === 'string' && key.indexOf('lib:') === 0) {
|
|
@@ -9738,6 +10017,8 @@ var state = {
|
|
|
9738
10017
|
balance: null,
|
|
9739
10018
|
currency: null,
|
|
9740
10019
|
todayUsage: null,
|
|
10020
|
+
todayUsageCurrency: 'CNY',
|
|
10021
|
+
usageLabel: '本地估算',
|
|
9741
10022
|
isPeak: false,
|
|
9742
10023
|
status: 'loading',
|
|
9743
10024
|
message: '',
|
|
@@ -10347,7 +10628,7 @@ function bubbleAmountText() {
|
|
|
10347
10628
|
return fmt(v, state.currency)
|
|
10348
10629
|
}
|
|
10349
10630
|
function bubbleTodayText() {
|
|
10350
|
-
return '今日已用 ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.currency) : '--')
|
|
10631
|
+
return (state.usageLabel || '今日已用') + ' ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
10351
10632
|
}
|
|
10352
10633
|
// 模块「内容」模板:占位符统一英文(便于兼容其他模型 API 时区分来源/字段):
|
|
10353
10634
|
// {expense_ds} 今日已用金额 · {balance_ds} 余额 · {status} 高峰/空闲状态字 · {countdown} 倒计时
|
|
@@ -10520,36 +10801,160 @@ function apiPlanPctText(v) {
|
|
|
10520
10801
|
if (v === null || v === undefined) return '--'
|
|
10521
10802
|
return (Number(v) || 0).toFixed(1).replace(/\.0$/, '') + '%'
|
|
10522
10803
|
}
|
|
10523
|
-
|
|
10804
|
+
// —— 多窗口订阅额度(如 OpenCode Go 的 rolling / weekly / monthly)——
|
|
10805
|
+
// 模块的「显示样式」= 选哪个时间窗口:all 全部三窗口 / rolling 5h / weekly 周 / monthly 月。
|
|
10806
|
+
// 选中具体窗口时输出会带上窗口标签(如 `5h 2%`),避免看不出是哪个时间段。
|
|
10807
|
+
var BUBBLE_PLAN_WIN_OPTS = [
|
|
10808
|
+
['all', '全部(5h / 周 / 月)'],
|
|
10809
|
+
['rolling', '5h'],
|
|
10810
|
+
['weekly', '周'],
|
|
10811
|
+
['monthly', '月'],
|
|
10812
|
+
]
|
|
10813
|
+
function bubblePlanWinOf(m) {
|
|
10814
|
+
m = m || {}
|
|
10815
|
+
var w = String(m.planWin || 'all')
|
|
10816
|
+
for (var i = 0; i < BUBBLE_PLAN_WIN_OPTS.length; i++) { if (BUBBLE_PLAN_WIN_OPTS[i][0] === w) return w }
|
|
10817
|
+
return 'all'
|
|
10818
|
+
}
|
|
10819
|
+
function bubblePlanWinLabel(w) {
|
|
10820
|
+
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] }
|
|
10821
|
+
return BUBBLE_PLAN_WIN_OPTS[0][1]
|
|
10822
|
+
}
|
|
10823
|
+
// 订阅额度模块在编辑列表 / 模块库里的名字(带所选窗口,便于区分同一个模型的多个模块)
|
|
10824
|
+
function bubblePlanModuleLabel(m) {
|
|
10825
|
+
m = m || {}
|
|
10826
|
+
var nm = (apiModelById(m.modelId) || {}).name || m.modelId
|
|
10827
|
+
var w = bubblePlanWinOf(m)
|
|
10828
|
+
return '额度·' + nm + (w === 'all' ? '' : '(' + bubblePlanWinLabel(w) + ')')
|
|
10829
|
+
}
|
|
10830
|
+
// 该模型的厂商模板是否声明了多窗口额度(决定编辑器里要不要给「显示样式」下拉)
|
|
10831
|
+
function apiPlanMultiWin(modelId) {
|
|
10832
|
+
var am = apiModelById(modelId)
|
|
10833
|
+
if (!am) return false
|
|
10834
|
+
var list = apiTemplates || []
|
|
10835
|
+
for (var i = 0; i < list.length; i++) {
|
|
10836
|
+
if (list[i] && list[i].id === am.provider) {
|
|
10837
|
+
var q = list[i].quota
|
|
10838
|
+
return !!(q && q.json && q.json.windows && q.json.windows.length)
|
|
10839
|
+
}
|
|
10840
|
+
}
|
|
10841
|
+
return false
|
|
10842
|
+
}
|
|
10843
|
+
function apiPlanWinList(modelId) {
|
|
10844
|
+
var p = apiPlanOf(modelId)
|
|
10845
|
+
return (p && p.ok && p.windows && p.windows.length) ? p.windows : null
|
|
10846
|
+
}
|
|
10847
|
+
// 按窗口取「已用% / 剩余%」文本;非多窗口厂商返回 null(走原有单窗口逻辑)
|
|
10848
|
+
function apiPlanPctWinText(modelId, win, left) {
|
|
10849
|
+
var list = apiPlanWinList(modelId)
|
|
10850
|
+
if (!list) return null
|
|
10851
|
+
function one(w) {
|
|
10852
|
+
var v = w.usedPct
|
|
10853
|
+
if (v === null || v === undefined) return '--'
|
|
10854
|
+
var pct = left ? Math.max(0, 100 - Number(v)) : Number(v)
|
|
10855
|
+
return (w.label ? (w.label + ' ') : '') + apiPlanPctText(pct)
|
|
10856
|
+
}
|
|
10857
|
+
if (win && win !== 'all') {
|
|
10858
|
+
for (var i = 0; i < list.length; i++) { if (list[i].key === win) return one(list[i]) }
|
|
10859
|
+
return '--'
|
|
10860
|
+
}
|
|
10861
|
+
var parts = []
|
|
10862
|
+
for (var j = 0; j < list.length; j++) parts.push(one(list[j]))
|
|
10863
|
+
return parts.join(' · ')
|
|
10864
|
+
}
|
|
10865
|
+
// 按窗口取「重置倒计时」文本;非多窗口厂商返回 null
|
|
10866
|
+
function apiPlanResetWinText(modelId, win) {
|
|
10867
|
+
var list = apiPlanWinList(modelId)
|
|
10868
|
+
if (!list) return null
|
|
10869
|
+
function one(w, short) {
|
|
10870
|
+
var ms = apiPlanResetMs(w.resetAt)
|
|
10871
|
+
if (ms === null) return '--'
|
|
10872
|
+
var t = short ? apiPlanCountdownShortText(ms) : apiPlanCountdownText(ms)
|
|
10873
|
+
return t || '--'
|
|
10874
|
+
}
|
|
10875
|
+
if (win && win !== 'all') {
|
|
10876
|
+
for (var i = 0; i < list.length; i++) { if (list[i].key === win) return one(list[i], true) }
|
|
10877
|
+
return '--'
|
|
10878
|
+
}
|
|
10879
|
+
var parts = []
|
|
10880
|
+
for (var j = 0; j < list.length; j++) parts.push(one(list[j], true))
|
|
10881
|
+
return parts.join(' · ')
|
|
10882
|
+
}
|
|
10883
|
+
function apiPlanUsedText(modelId, win) {
|
|
10884
|
+
var t = apiPlanPctWinText(modelId, win, false)
|
|
10885
|
+
if (t !== null) return t
|
|
10524
10886
|
var p = apiPlanOf(modelId)
|
|
10525
10887
|
if (!p || !p.ok) return '--'
|
|
10526
10888
|
return apiPlanPctText(p.usedPct)
|
|
10527
10889
|
}
|
|
10528
|
-
function apiPlanLeftText(modelId) {
|
|
10890
|
+
function apiPlanLeftText(modelId, win) {
|
|
10891
|
+
var t = apiPlanPctWinText(modelId, win, true)
|
|
10892
|
+
if (t !== null) return t
|
|
10529
10893
|
var p = apiPlanOf(modelId)
|
|
10530
10894
|
if (!p || !p.ok) return '--'
|
|
10531
10895
|
return apiPlanPctText(p.remainPct)
|
|
10532
10896
|
}
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
if (
|
|
10536
|
-
|
|
10537
|
-
var
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10897
|
+
// 重置时间归一成毫秒(秒 / 毫秒 / 日期字符串都兼容);解析不出来返回 null
|
|
10898
|
+
function apiPlanResetMs(t) {
|
|
10899
|
+
if (t === null || t === undefined || t === '') return null
|
|
10900
|
+
if (typeof t === 'number') return t < 1e12 ? t * 1000 : t // 秒 / 毫秒都兼容
|
|
10901
|
+
var pd = Date.parse(String(t))
|
|
10902
|
+
return isFinite(pd) ? pd : null
|
|
10903
|
+
}
|
|
10904
|
+
// 倒计时文本(长写法,单窗口用)
|
|
10905
|
+
function apiPlanCountdownText(ms) {
|
|
10541
10906
|
var left = ms - Date.now()
|
|
10542
|
-
if (!isFinite(left)) return
|
|
10907
|
+
if (!isFinite(left)) return ''
|
|
10543
10908
|
if (left <= 0) return '即将重置'
|
|
10544
10909
|
var h = Math.floor(left / 3600000)
|
|
10545
10910
|
var d = Math.floor(h / 24)
|
|
10546
10911
|
if (d > 0) return d + '天' + (h % 24) + '小时后重置'
|
|
10547
10912
|
return h + '小时' + Math.floor((left % 3600000) / 60000) + '分后重置'
|
|
10548
10913
|
}
|
|
10914
|
+
// 倒计时文本(紧凑写法,多窗口用:5d21h / 3h53m)——单位统一用 d/h/m,不掺中文
|
|
10915
|
+
// 不带「后重置」字样:多窗口模块里窗口标签已说明它是什么(如 `5h 2% · 3h53m`)
|
|
10916
|
+
function apiPlanCountdownShortText(ms) {
|
|
10917
|
+
var left = ms - Date.now()
|
|
10918
|
+
if (!isFinite(left)) return ''
|
|
10919
|
+
if (left <= 0) return '即将重置'
|
|
10920
|
+
var m = Math.floor(left / 60000)
|
|
10921
|
+
var h = Math.floor(m / 60)
|
|
10922
|
+
var d = Math.floor(h / 24)
|
|
10923
|
+
if (d > 0) return d + 'd' + (h % 24) + 'h'
|
|
10924
|
+
if (h > 0) return h + 'h' + (m % 60) + 'm'
|
|
10925
|
+
return m + 'm'
|
|
10926
|
+
}
|
|
10927
|
+
function apiPlanResetText(modelId, win) {
|
|
10928
|
+
var t = apiPlanResetWinText(modelId, win)
|
|
10929
|
+
if (t !== null) return t
|
|
10930
|
+
var p = apiPlanOf(modelId)
|
|
10931
|
+
if (!p || !p.ok || p.resetAt === null || p.resetAt === undefined || p.resetAt === '') return '--'
|
|
10932
|
+
var ms = apiPlanResetMs(p.resetAt)
|
|
10933
|
+
if (ms === null) return String(p.resetAt)
|
|
10934
|
+
var txt = apiPlanCountdownText(ms)
|
|
10935
|
+
return txt || String(p.resetAt)
|
|
10936
|
+
}
|
|
10549
10937
|
function apiPlanSummary(modelId) {
|
|
10550
10938
|
var p = apiPlanOf(modelId)
|
|
10551
10939
|
if (!p) return '读取中…'
|
|
10552
10940
|
if (!p.ok) return p.hide ? '--' : (p.error || '读取失败')
|
|
10941
|
+
// v0.3.1:多窗口额度(如 OpenCode Go 的 5h / 周 / 月)逐窗口展示:`5h 12.5% · 2h55m后重置 | 周 …`
|
|
10942
|
+
if (p.windows && p.windows.length) {
|
|
10943
|
+
var parts = []
|
|
10944
|
+
for (var i = 0; i < p.windows.length; i++) {
|
|
10945
|
+
var w = p.windows[i] || {}
|
|
10946
|
+
var seg = w.label ? (w.label + ' ') : ''
|
|
10947
|
+
seg += apiPlanPctText(w.usedPct)
|
|
10948
|
+
var ms = apiPlanResetMs(w.resetAt)
|
|
10949
|
+
if (ms !== null) {
|
|
10950
|
+
var rt = apiPlanCountdownShortText(ms)
|
|
10951
|
+
if (rt) seg += ' · ' + rt
|
|
10952
|
+
}
|
|
10953
|
+
parts.push(seg)
|
|
10954
|
+
}
|
|
10955
|
+
if (p.level) parts.push(p.level)
|
|
10956
|
+
return parts.join(' | ')
|
|
10957
|
+
}
|
|
10553
10958
|
var s = '已用 ' + apiPlanUsedText(modelId) + ' · ' + apiPlanResetText(modelId)
|
|
10554
10959
|
if (p.weeklyUsedPct !== null && p.weeklyUsedPct !== undefined) s += ' · 周 ' + apiPlanPctText(p.weeklyUsedPct)
|
|
10555
10960
|
if (p.level) s += ' · ' + p.level
|
|
@@ -10573,18 +10978,19 @@ function bubbleContentTokenMap(m) {
|
|
|
10573
10978
|
map['quota_left'] = apiQuotaLeftText(m.modelId)
|
|
10574
10979
|
map['quota_total'] = apiQuotaTotalText(m.modelId)
|
|
10575
10980
|
map['quota_reset'] = apiQuotaResetText(m.modelId)
|
|
10576
|
-
// 厂商订阅额度占位符(kind='quota'
|
|
10577
|
-
|
|
10981
|
+
// 厂商订阅额度占位符(kind='quota' 的厂商):按模块「显示样式」选的窗口取数
|
|
10982
|
+
var pwin = bubblePlanWinOf(m)
|
|
10983
|
+
map['plan'] = apiPlanUsedText(m.modelId, pwin)
|
|
10578
10984
|
map['plan_used'] = map['plan']
|
|
10579
|
-
map['plan_left'] = apiPlanLeftText(m.modelId)
|
|
10580
|
-
map['plan_reset'] = apiPlanResetText(m.modelId)
|
|
10985
|
+
map['plan_left'] = apiPlanLeftText(m.modelId, pwin)
|
|
10986
|
+
map['plan_reset'] = apiPlanResetText(m.modelId, pwin)
|
|
10581
10987
|
return map
|
|
10582
10988
|
}
|
|
10583
10989
|
if (m.type === 'balance') {
|
|
10584
10990
|
v = bubbleAmountText()
|
|
10585
10991
|
map['balance_ds'] = v
|
|
10586
10992
|
} else if (m.type === 'today') {
|
|
10587
|
-
v = (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.currency) : '--')
|
|
10993
|
+
v = (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
10588
10994
|
map['expense_ds'] = v
|
|
10589
10995
|
} else if (bubbleIsPeakCount(m)) {
|
|
10590
10996
|
v = bubbleCountdownText()
|
|
@@ -10612,9 +11018,9 @@ function bubbleTplHelpItems(m) {
|
|
|
10612
11018
|
add('quota_total', '额度总量')
|
|
10613
11019
|
add('quota_reset', '额度重置倒计时')
|
|
10614
11020
|
}
|
|
10615
|
-
add('plan', '
|
|
10616
|
-
add('plan_left', '
|
|
10617
|
-
add('plan_reset', '
|
|
11021
|
+
add('plan', '订阅额度已用百分比(多窗口厂商按「显示样式」所选窗口,带窗口标签)')
|
|
11022
|
+
add('plan_left', '订阅额度剩余百分比(同上)')
|
|
11023
|
+
add('plan_reset', '订阅额度刷新倒计时(同上;全部窗口时为紧凑倒计时)')
|
|
10618
11024
|
return arr
|
|
10619
11025
|
}
|
|
10620
11026
|
if (m.type === 'balance') add('balance_ds', '余额数值')
|
|
@@ -10887,7 +11293,19 @@ function bubbleCountdownTick() {
|
|
|
10887
11293
|
bubbleCountdownApplyStyle(x.el, x.mod, cur)
|
|
10888
11294
|
} catch (err) {}
|
|
10889
11295
|
}
|
|
10890
|
-
|
|
11296
|
+
// v733:非倒计时的峰谷行共用这个 ticker(只在状态真的变了时才动 DOM,不打断跑马灯)
|
|
11297
|
+
bubblePeakRows = bubblePeakRows.filter(function (y) { return y && y.row && y.row.isConnected })
|
|
11298
|
+
if (bubblePeakRows.length) {
|
|
11299
|
+
var peakNow = bubbleIsPeakNow()
|
|
11300
|
+
for (var k = 0; k < bubblePeakRows.length; k++) {
|
|
11301
|
+
var y2 = bubblePeakRows[k]
|
|
11302
|
+
if (y2.peak !== peakNow) {
|
|
11303
|
+
y2.peak = peakNow
|
|
11304
|
+
try { bubblePeakRowApply(y2, peakNow) } catch (err) {}
|
|
11305
|
+
}
|
|
11306
|
+
}
|
|
11307
|
+
}
|
|
11308
|
+
if (!bubbleCountdownRows.length && !bubblePeakRows.length && bubbleCountdownTicker) {
|
|
10891
11309
|
clearInterval(bubbleCountdownTicker)
|
|
10892
11310
|
bubbleCountdownTicker = null
|
|
10893
11311
|
}
|
|
@@ -10897,6 +11315,78 @@ function bubbleCountdownRegister(el, mod) {
|
|
|
10897
11315
|
bubbleCountdownRows.push({ el: el, mod: mod })
|
|
10898
11316
|
if (!bubbleCountdownTicker) bubbleCountdownTicker = setInterval(bubbleCountdownTick, 1000)
|
|
10899
11317
|
}
|
|
11318
|
+
// —— v733:峰谷状态实时跟随 ——
|
|
11319
|
+
// count / nextpeak 样式由倒计时引擎逐秒刷新;其余峰谷样式(默认「高峰时段 / 空闲时段」、
|
|
11320
|
+
// 梁文峰谷、!?峰峰?!、简洁峰/谷)原来只在渲染那一刻取一次状态 —— 泡泡显示期间跨过峰谷切换点,
|
|
11321
|
+
// 文字与配色会一直停在旧状态。这里把这类行登记进同一个 1s ticker,状态变化时**原地**改写
|
|
11322
|
+
// 文字 / 配色 / 底色(不做整泡重绘,保持「泡泡显示期间内容稳定」的既有设计)。
|
|
11323
|
+
var bubblePeakRows = []
|
|
11324
|
+
// 文字是直接放在行上还是包一层内层 span,取决于该状态有没有底色 ——
|
|
11325
|
+
// 只有「高峰 / 空闲两态底色有无一致」时,DOM 结构才不随状态变化,才能原地改写
|
|
11326
|
+
function bubblePeakRowStable(m) {
|
|
11327
|
+
return !!(m.peakBg || m.peakBgRgb) === !!(m.offBg || m.offBgRgb)
|
|
11328
|
+
}
|
|
11329
|
+
function bubblePeakRowRegister(row, tx, mod, peakNow) {
|
|
11330
|
+
try {
|
|
11331
|
+
if (!mod || mod.type !== 'peak' || bubbleIsPeakCount(mod)) return
|
|
11332
|
+
if (!bubblePeakRowStable(mod)) return
|
|
11333
|
+
bubblePeakRows.push({
|
|
11334
|
+
row: row, tx: tx, mod: mod, peak: !!peakNow,
|
|
11335
|
+
gradEl: (mod.peakBg || mod.peakBgRgb) ? tx : row,
|
|
11336
|
+
})
|
|
11337
|
+
if (!bubbleCountdownTicker) bubbleCountdownTicker = setInterval(bubbleCountdownTick, 1000)
|
|
11338
|
+
} catch (err) {}
|
|
11339
|
+
}
|
|
11340
|
+
// 跑马灯配色方案白名单(与 blockOf() / bubbleCountdownApplyStyle 用的那套一致)
|
|
11341
|
+
function bubbleRgbSchemeOk(s) {
|
|
11342
|
+
return s === 'candy' || s === 'rouge' || s === 'bamboo' || s === 'aurora' || s === 'deepsea' ||
|
|
11343
|
+
s === 'sunset' || s === 'forest' || s === 'champagne' || s === 'lavender' || s === 'mint' ||
|
|
11344
|
+
s === 'lava' || s === 'galaxy' || s === 'ink' || s === 'indigo'
|
|
11345
|
+
}
|
|
11346
|
+
function bubblePeakRowClearClass(el, prefix) {
|
|
11347
|
+
try {
|
|
11348
|
+
var cls = Array.prototype.slice.call(el.classList || [])
|
|
11349
|
+
for (var i = 0; i < cls.length; i++) {
|
|
11350
|
+
if (String(cls[i]).indexOf(prefix) === 0) { try { el.classList.remove(cls[i]) } catch (err) {} }
|
|
11351
|
+
}
|
|
11352
|
+
} catch (err) {}
|
|
11353
|
+
}
|
|
11354
|
+
// 原地改写一行峰谷:顺序与 blockOf() 的峰谷分支一致(底色 → 文字跑马灯 → 纯色)
|
|
11355
|
+
function bubblePeakRowApply(x, peak) {
|
|
11356
|
+
var m = x.mod
|
|
11357
|
+
try { x.tx.textContent = bubbleContentText(m, bubblePeakText(m)) } catch (err) {}
|
|
11358
|
+
// ① 底色(高峰底色 / 空闲底色,跑马灯优先于纯色)
|
|
11359
|
+
try {
|
|
11360
|
+
var effBgRgb = peak ? String(m.peakBgRgb || '') : String(m.offBgRgb || '')
|
|
11361
|
+
var effBg = effBgRgb ? '' : (peak ? String(m.peakBg || '') : String(m.offBg || ''))
|
|
11362
|
+
if (effBgRgb === 'true') effBgRgb = 'macaron'
|
|
11363
|
+
bubblePeakRowClearClass(x.row, 'dshwv-bgrgb')
|
|
11364
|
+
x.row.style.background = ''
|
|
11365
|
+
if (effBgRgb) {
|
|
11366
|
+
x.row.classList.add('dshwv-bgrgb')
|
|
11367
|
+
if (bubbleRgbSchemeOk(effBgRgb) || effBgRgb === 'macaron') x.row.classList.add('dshwv-bgrgb-' + effBgRgb)
|
|
11368
|
+
x.row.style.animationDuration = bubbleMarqueeDur()
|
|
11369
|
+
} else if (effBg) {
|
|
11370
|
+
x.row.style.background = effBg
|
|
11371
|
+
}
|
|
11372
|
+
} catch (err) {}
|
|
11373
|
+
// ② 文字:本状态跑马灯 > 模块跑马灯 > 本状态纯色 > 模块纯色
|
|
11374
|
+
try {
|
|
11375
|
+
var marquee = (peak ? String(m.peakRgb || '') : String(m.offRgb || '')) || String(m.rgb || '')
|
|
11376
|
+
bubblePeakRowClearClass(x.gradEl, 'dshwv-rgb')
|
|
11377
|
+
x.row.style.color = ''
|
|
11378
|
+
if (marquee) {
|
|
11379
|
+
var scheme = marquee === true ? 'macaron' : String(marquee || 'macaron')
|
|
11380
|
+
x.gradEl.classList.add('dshwv-rgb')
|
|
11381
|
+
if (bubbleRgbSchemeOk(scheme)) x.gradEl.classList.add('dshwv-rgb-' + scheme)
|
|
11382
|
+
x.gradEl.style.animationDuration = bubbleMarqueeDur()
|
|
11383
|
+
} else {
|
|
11384
|
+
var pcol = peak ? String(m.peakColor || '') : String(m.offColor || '')
|
|
11385
|
+
if (pcol) x.row.style.color = pcol
|
|
11386
|
+
else if (m.color) x.row.style.color = String(m.color)
|
|
11387
|
+
}
|
|
11388
|
+
} catch (err) {}
|
|
11389
|
+
}
|
|
10900
11390
|
// v209: 计算单个模块要显示的行文本(与随机选中行),每次全新计算、不跨行复用状态
|
|
10901
11391
|
function bubbleRowContentOf(mod) {
|
|
10902
11392
|
mod = mod || {}
|
|
@@ -10906,7 +11396,7 @@ function bubbleRowContentOf(mod) {
|
|
|
10906
11396
|
return { txt: bubbleContentText(mod, bv), line: null }
|
|
10907
11397
|
}
|
|
10908
11398
|
if (mod.type === 'today') {
|
|
10909
|
-
var tv2 = bubbleIsModelMod(mod) ? apiModelTodayText(mod.modelId) : (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.currency) : '--')
|
|
11399
|
+
var tv2 = bubbleIsModelMod(mod) ? apiModelTodayText(mod.modelId) : (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
10910
11400
|
return { txt: bubbleContentText(mod, '今日已用 ' + tv2), line: null }
|
|
10911
11401
|
}
|
|
10912
11402
|
if (mod.type === 'quota') {
|
|
@@ -11049,7 +11539,7 @@ function bubbleRowsTo(parentEl, mods) {
|
|
|
11049
11539
|
row.style.background = effBg
|
|
11050
11540
|
}
|
|
11051
11541
|
}
|
|
11052
|
-
return { el: row, tx: tx, fSize: fSize, mod: m, peak: (m.type === 'peak' || m.type === 'nextpeak'), bg: needBg }
|
|
11542
|
+
return { el: row, tx: tx, fSize: fSize, mod: m, peak: (m.type === 'peak' || m.type === 'nextpeak'), bg: needBg, curPeak: curPeak }
|
|
11053
11543
|
}
|
|
11054
11544
|
// 超宽判定(与旧版一致):先单行渲染,测量实际超出泡泡内宽(560u)才允许该块内折行——
|
|
11055
11545
|
// 预览与真实共用此逻辑,保证两者一致
|
|
@@ -11072,6 +11562,8 @@ function bubbleRowsTo(parentEl, mods) {
|
|
|
11072
11562
|
// 倒计时块:注册文字节点(带底色时为内层 span),由每秒 ticker 刷新文案与配色
|
|
11073
11563
|
function registerIfCountdown(blk) {
|
|
11074
11564
|
if (bubbleIsPeakCount(blk.mod)) bubbleCountdownRegister(blk.tx, blk.mod)
|
|
11565
|
+
// v733:非 count 的峰谷行登记到同一个 ticker,状态切换时原地刷新(原来只在渲染时取一次)
|
|
11566
|
+
else if (blk.mod && blk.mod.type === 'peak') bubblePeakRowRegister(blk.el, blk.tx, blk.mod, blk.curPeak)
|
|
11075
11567
|
}
|
|
11076
11568
|
// 超链接模块:仅真实泡泡(textBox)内可点击,新标签页打开;预览/编辑不弹窗
|
|
11077
11569
|
function enableLinkRun(blk2) {
|
|
@@ -11479,7 +11971,7 @@ function render() {
|
|
|
11479
11971
|
hint = '加载中…'
|
|
11480
11972
|
} else {
|
|
11481
11973
|
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) : '--')
|
|
11974
|
+
hint = (state.usageLabel || '今日已用') + ' ' + (state.todayUsage !== null && state.todayUsage !== undefined ? fmt(state.todayUsage, state.todayUsageCurrency || state.currency) : '--')
|
|
11483
11975
|
}
|
|
11484
11976
|
amountEl.textContent = amount
|
|
11485
11977
|
if (bubbleRandomActive && bubbleRandomLines) {
|
|
@@ -11508,18 +12000,43 @@ function settle() {
|
|
|
11508
12000
|
express()
|
|
11509
12001
|
return
|
|
11510
12002
|
}
|
|
12003
|
+
// issue #102:锚点分支过去只有下限(左/顶锚甚至完全不夹),脏锚点或尺寸竞态会把挂件算到
|
|
12004
|
+
// 视口外 —— 症状是「启动闪一下 → 向下滑出/镜像 → 消失」,而且 saveConfig 会把负的离边距离
|
|
12005
|
+
// 写回 localStorage,于是刷新也恢复不了。四个锚点分支统一夹到可视区。
|
|
12006
|
+
var maxLFree = Math.max(0, vp.w - w - rightGap())
|
|
12007
|
+
var maxLAnchor = Math.max(0, vp.w - w)
|
|
12008
|
+
var maxT = Math.max(0, vp.h - h)
|
|
12009
|
+
var outOfRange = false
|
|
11511
12010
|
if (state.h === 'right') {
|
|
11512
|
-
|
|
12011
|
+
var rawR = vp.w - w - state.hOff - rightGap()
|
|
12012
|
+
state.left = clamp(rawR, 0, maxLAnchor)
|
|
12013
|
+
if (state.left !== rawR) outOfRange = true
|
|
11513
12014
|
} else if (state.h === 'left') {
|
|
11514
|
-
|
|
12015
|
+
var rawL = state.hOff
|
|
12016
|
+
state.left = clamp(rawL, 0, maxLAnchor)
|
|
12017
|
+
if (state.left !== rawL) outOfRange = true
|
|
11515
12018
|
} else {
|
|
11516
|
-
state.left = clamp(state.left, 0,
|
|
11517
|
-
}
|
|
11518
|
-
|
|
12019
|
+
state.left = clamp(state.left, 0, maxLFree)
|
|
12020
|
+
}
|
|
12021
|
+
if (state.v === 'bottom') {
|
|
12022
|
+
var rawB = vp.h - h - state.vOff
|
|
12023
|
+
state.top = clamp(rawB, 0, maxT)
|
|
12024
|
+
if (state.top !== rawB) outOfRange = true
|
|
11519
12025
|
} else if (state.v === 'top') {
|
|
11520
|
-
|
|
12026
|
+
var rawT = state.vOff
|
|
12027
|
+
state.top = clamp(rawT, 0, maxT)
|
|
12028
|
+
if (state.top !== rawT) outOfRange = true
|
|
11521
12029
|
} else {
|
|
11522
|
-
state.top = clamp(state.top, 0,
|
|
12030
|
+
state.top = clamp(state.top, 0, maxT)
|
|
12031
|
+
}
|
|
12032
|
+
// 偏移确实越界(脏数据 / 视口变小):先把偏移夹回合法范围再落盘,下次启动不再复现。
|
|
12033
|
+
// 只在真的越界时写,正常 resize 不会产生额外的 localStorage 写入。
|
|
12034
|
+
if (outOfRange) {
|
|
12035
|
+
if (state.h === 'right') state.hOff = clamp(state.hOff, 0, Math.max(0, vp.w - w - rightGap()))
|
|
12036
|
+
else if (state.h === 'left') state.hOff = clamp(state.hOff, 0, maxLAnchor)
|
|
12037
|
+
if (state.v === 'bottom') state.vOff = clamp(state.vOff, 0, maxT)
|
|
12038
|
+
else if (state.v === 'top') state.vOff = clamp(state.vOff, 0, maxT)
|
|
12039
|
+
try { saveAnchorPos() } catch (err) {}
|
|
11523
12040
|
}
|
|
11524
12041
|
refreshFlip()
|
|
11525
12042
|
}
|
|
@@ -11602,7 +12119,7 @@ function refresh(manual) {
|
|
|
11602
12119
|
ctrl = new AbortController()
|
|
11603
12120
|
timer = setTimeout(function () { try { ctrl.abort() } catch (err) {} }, FETCH_TIMEOUT_MS)
|
|
11604
12121
|
} catch (err) {}
|
|
11605
|
-
fetch(BALANCE_URL, { cache: 'no-store', signal: ctrl ? ctrl.signal : undefined })
|
|
12122
|
+
fetch(BALANCE_URL + (manual ? '?refresh=1' : ''), { cache: 'no-store', signal: ctrl ? ctrl.signal : undefined })
|
|
11606
12123
|
.then(function (r) { return r.json() })
|
|
11607
12124
|
.then(function (data) {
|
|
11608
12125
|
if (data && data.ok) {
|
|
@@ -11614,6 +12131,9 @@ function refresh(manual) {
|
|
|
11614
12131
|
state.currency = nc
|
|
11615
12132
|
state.message = ''
|
|
11616
12133
|
state.todayUsage = data.todayUsage !== undefined ? data.todayUsage : null
|
|
12134
|
+
state.todayUsageCurrency = data.todayUsageCurrency || data.currency || 'CNY'
|
|
12135
|
+
state.usageLabel = data.usageLabel || '本地估算'
|
|
12136
|
+
if (data.stale) state.usageLabel += ' · 余额未刷新'
|
|
11617
12137
|
state.isPeak = !!data.isPeak
|
|
11618
12138
|
checkUsageAlerts(nb, state.todayUsage)
|
|
11619
12139
|
if (changed && !currencyChanged) {
|
|
@@ -11669,27 +12189,90 @@ var costBubbleActive = false
|
|
|
11669
12189
|
var scrollGapOn = false
|
|
11670
12190
|
var scrollGapPx = 17
|
|
11671
12191
|
var menuBtnHide = false // 主菜单开关:隐藏挂件菜单按钮,改为右键小鲸鱼唤出菜单
|
|
12192
|
+
// —— v734(issue #97 / #88):设置保存的「防覆盖 + 失败可见」——
|
|
12193
|
+
// #97 根因:首次 GET 还没落地就 PUT,会把内存里的默认值整包写进服务端(重启后设置被洗成默认值)。
|
|
12194
|
+
// #88 根因:这个 PUT 以前是 fire-and-forget,服务端 500 / {ok:false} 完全没人读。
|
|
12195
|
+
var configLoaded = false // 首次 GET 应用完成前,一律不 PUT
|
|
12196
|
+
var configSavePending = false // 加载期间被挡下的保存,加载完成后补一次
|
|
12197
|
+
var dshwvToastEl = null
|
|
12198
|
+
var dshwvToastTimer = null
|
|
12199
|
+
// 固定定位的小提示条(自动消失;同一时刻只留一条)
|
|
12200
|
+
function dshwvToast(msg) {
|
|
12201
|
+
try {
|
|
12202
|
+
if (!dshwvToastEl || !dshwvToastEl.parentNode) {
|
|
12203
|
+
var el = document.createElement('div')
|
|
12204
|
+
el.style.cssText = 'position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:2147483600;' +
|
|
12205
|
+
'max-width:min(560px,calc(100vw - 32px));box-sizing:border-box;padding:10px 14px;border-radius:10px;' +
|
|
12206
|
+
'background:#8a1f1f;color:#fff;font-size:13px;line-height:1.6;box-shadow:0 6px 20px rgba(0,0,0,.28);' +
|
|
12207
|
+
'pointer-events:none;text-align:center'
|
|
12208
|
+
document.body.appendChild(el)
|
|
12209
|
+
dshwvToastEl = el
|
|
12210
|
+
}
|
|
12211
|
+
dshwvToastEl.innerHTML = msg
|
|
12212
|
+
if (dshwvToastTimer) clearTimeout(dshwvToastTimer)
|
|
12213
|
+
dshwvToastTimer = setTimeout(function () {
|
|
12214
|
+
try { if (dshwvToastEl && dshwvToastEl.parentNode) dshwvToastEl.parentNode.removeChild(dshwvToastEl) } catch (err) {}
|
|
12215
|
+
dshwvToastEl = null
|
|
12216
|
+
}, 8000)
|
|
12217
|
+
} catch (err) {}
|
|
12218
|
+
}
|
|
12219
|
+
function configSaveFailNotice(detail) {
|
|
12220
|
+
try { console.error('[dsh-whale] 设置保存失败:', detail) } catch (err) {}
|
|
12221
|
+
dshwvToast('⚠ 设置保存失败:' + String(detail || '').slice(0, 120) +
|
|
12222
|
+
'<br>已自动重试一次。若持续失败,请检查 DSH 数据目录是否可写。')
|
|
12223
|
+
}
|
|
12224
|
+
function configPayload() {
|
|
12225
|
+
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 })
|
|
12226
|
+
}
|
|
12227
|
+
// 真正的 PUT:读响应 → 失败(网络异常 / HTTP!=200 / {ok:false})静默重试一次 → 仍失败才提示
|
|
12228
|
+
function configPut(payload, retried) {
|
|
12229
|
+
return fetch(SIZE_URL, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: payload })
|
|
12230
|
+
.then(function (r) {
|
|
12231
|
+
return r.json().catch(function () { return null }).then(function (d) { return { ok: r.ok, status: r.status, d: d } })
|
|
12232
|
+
})
|
|
12233
|
+
.then(function (x) {
|
|
12234
|
+
if (x.ok && (!x.d || x.d.ok !== false)) return true
|
|
12235
|
+
if (!retried) return new Promise(function (res) { setTimeout(function () { res(configPut(payload, true)) }, 900) })
|
|
12236
|
+
configSaveFailNotice((x.d && x.d.error) || ('HTTP ' + x.status))
|
|
12237
|
+
return false
|
|
12238
|
+
})
|
|
12239
|
+
.catch(function (err) {
|
|
12240
|
+
if (!retried) return new Promise(function (res) { setTimeout(function () { res(configPut(payload, true)) }, 900) })
|
|
12241
|
+
configSaveFailNotice((err && err.message) || err)
|
|
12242
|
+
return false
|
|
12243
|
+
})
|
|
12244
|
+
}
|
|
11672
12245
|
function saveConfig() {
|
|
12246
|
+
// issue #97:加载完成前只记待办,绝不 PUT(否则把默认值整包写进服务端)
|
|
12247
|
+
if (!configLoaded) { configSavePending = true; return }
|
|
11673
12248
|
try {
|
|
11674
|
-
|
|
12249
|
+
configPut(configPayload(), false)
|
|
11675
12250
|
// 锚点位置记忆:记录相对边框的离边距离,窗口 resize 后保持(localStorage)。
|
|
11676
|
-
|
|
12251
|
+
saveAnchorPos()
|
|
12252
|
+
} catch (err) {}
|
|
12253
|
+
}
|
|
12254
|
+
// 单独抽出:只写 localStorage 锚点(不碰尺寸设置);applyAnchorPos / settle 自愈时也要用。
|
|
12255
|
+
// v:2 = 净距离格式(剥离避让距离),v:1 旧格式含避让距离,恢复时废弃旧格式。
|
|
12256
|
+
function saveAnchorPos() {
|
|
12257
|
+
try {
|
|
11677
12258
|
var vp = viewport()
|
|
11678
12259
|
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
11679
12260
|
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 -
|
|
12261
|
+
var leftDist = isFinite(state.left) ? state.left : 0
|
|
12262
|
+
var rightDist = vp.w - leftDist - w
|
|
12263
|
+
var topDist = isFinite(state.top) ? state.top : 0
|
|
12264
|
+
var bottomDist = vp.h - topDist - h
|
|
11684
12265
|
var hAnchor = leftDist <= rightDist ? 'left' : 'right'
|
|
11685
|
-
|
|
12266
|
+
// issue #102:离边距离必须非负。挂件一旦被算到屏幕外,min(leftDist, rightDist) 就是负数,
|
|
12267
|
+
// 存进去会变成"永久坏锚点",此后每次启动都复现(刷新也恢复不了)。
|
|
12268
|
+
var hDistRaw = Math.max(0, Math.round(Math.min(leftDist, rightDist)))
|
|
11686
12269
|
var hDist = hAnchor === 'right' && scrollGapOn ? Math.max(0, hDistRaw - rightGap()) : hDistRaw
|
|
11687
12270
|
localStorage.setItem('dshw-pos', JSON.stringify({
|
|
11688
12271
|
v: 2,
|
|
11689
12272
|
hAnchor: hAnchor,
|
|
11690
12273
|
hDist: hDist,
|
|
11691
12274
|
vAnchor: topDist <= bottomDist ? 'top' : 'bottom',
|
|
11692
|
-
vDist: Math.round(Math.min(topDist, bottomDist))
|
|
12275
|
+
vDist: Math.max(0, Math.round(Math.min(topDist, bottomDist)))
|
|
11693
12276
|
}))
|
|
11694
12277
|
} catch (err) {}
|
|
11695
12278
|
}
|
|
@@ -11818,6 +12401,11 @@ var releasePlayed = false
|
|
|
11818
12401
|
var releaseTimer = null
|
|
11819
12402
|
function applySoundSet() {
|
|
11820
12403
|
try {
|
|
12404
|
+
// v729:切音效组 / 开关音效时把本轮播放状态一并复位,
|
|
12405
|
+
// 避免残留 releasePlayed=true 把新组的松开音整体吃掉(与 playPress 的修复配套)
|
|
12406
|
+
if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null }
|
|
12407
|
+
pressEnded = false
|
|
12408
|
+
releasePlayed = false
|
|
11821
12409
|
// 槽位显式留空(该事件静音)时,对应音频元素置空;playPress/playRelease 已判空
|
|
11822
12410
|
var pEmpty = audioGroupSlotEmpty(soundSet, 'press')
|
|
11823
12411
|
var rEmpty = audioGroupSlotEmpty(soundSet, 'release')
|
|
@@ -11835,19 +12423,23 @@ function applySoundSet() {
|
|
|
11835
12423
|
}
|
|
11836
12424
|
function playPress() {
|
|
11837
12425
|
if (!soundOn) return
|
|
12426
|
+
// v729 修复:**本轮状态复位必须放在「按压槽留空」分支之前**。
|
|
12427
|
+
// 原实现里 pressAudio 为空时直接 return,跳过了 releasePlayed = false;而 playRelease()
|
|
12428
|
+
// 一旦把 releasePlayed 置为 true 就再没有任何地方复位它 → 结果是只有第一次松开有声音,
|
|
12429
|
+
// 之后每次点击都静音(用户实测:新建音效组只填松开音时复现)。
|
|
12430
|
+
if (releaseTimer) { clearTimeout(releaseTimer); releaseTimer = null }
|
|
12431
|
+
if (releaseAudio) {
|
|
12432
|
+
releaseAudio.pause()
|
|
12433
|
+
releaseAudio.currentTime = 0
|
|
12434
|
+
}
|
|
12435
|
+
pressEnded = false
|
|
12436
|
+
releasePlayed = false
|
|
11838
12437
|
if (!pressAudio) {
|
|
11839
|
-
//
|
|
12438
|
+
// 按压槽留空:按压事件静音,但状态已复位 → 每次松开都还能正常发声
|
|
11840
12439
|
pressEnded = true
|
|
11841
12440
|
return
|
|
11842
12441
|
}
|
|
11843
12442
|
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
12443
|
pressAudio.onended = function () {
|
|
11852
12444
|
pressEnded = true
|
|
11853
12445
|
// fallback (duration unknown): click → Ya2 right after Ya1 ends
|
|
@@ -13838,19 +14430,27 @@ function applyAnchorPos() {
|
|
|
13838
14430
|
var vp = viewport()
|
|
13839
14431
|
var w = root.offsetWidth || root.getBoundingClientRect().width || 0
|
|
13840
14432
|
var h = root.offsetHeight || root.getBoundingClientRect().height || 0
|
|
14433
|
+
var maxOffH = Math.max(0, vp.w - w)
|
|
14434
|
+
var maxOffV = Math.max(0, vp.h - h)
|
|
14435
|
+
// issue #102 自愈:非法距离(负数 = 存进去时挂件已在屏幕外;超出视口 = 窗口变小/脏数据)
|
|
14436
|
+
// 一律夹回合法范围并把修正结果落盘 —— 老用户中了脏数据也能自己恢复,无需手清 localStorage。
|
|
14437
|
+
var hDist = isFinite(a.hDist) ? clamp(a.hDist, 0, maxOffH) : 0
|
|
14438
|
+
var vDist = isFinite(a.vDist) ? clamp(a.vDist, 0, maxOffV) : 0
|
|
14439
|
+
var healed = (hDist !== a.hDist || vDist !== a.vDist)
|
|
13841
14440
|
// 与加载恢复一致:锚点存净距离,右锚点按当前避让开关叠加
|
|
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,
|
|
14441
|
+
var effectiveRightDist = a.hAnchor === 'right' ? hDist + (scrollGapOn ? rightGap() : 0) : hDist
|
|
14442
|
+
var l = a.hAnchor === 'left' ? hDist : vp.w - effectiveRightDist - w
|
|
14443
|
+
var t = a.vAnchor === 'top' ? vDist : vp.h - vDist - h
|
|
14444
|
+
state.left = clamp(l, 0, maxOffH)
|
|
14445
|
+
state.top = clamp(t, 0, maxOffV)
|
|
13847
14446
|
state.h = a.hAnchor
|
|
13848
14447
|
// 净距离直接还给 hOff/vOff:settle() 对锚定状态从偏移量重算,
|
|
13849
14448
|
// 若置 0 会把刚恢复的距离覆盖成贴边(issue #43)
|
|
13850
|
-
state.hOff =
|
|
14449
|
+
state.hOff = hDist
|
|
13851
14450
|
state.v = a.vAnchor
|
|
13852
|
-
state.vOff =
|
|
14451
|
+
state.vOff = vDist
|
|
13853
14452
|
refreshFlip()
|
|
14453
|
+
if (healed) { try { saveAnchorPos() } catch (err) {} }
|
|
13854
14454
|
return true
|
|
13855
14455
|
} catch (err) { return false }
|
|
13856
14456
|
}
|
|
@@ -13949,33 +14549,25 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
13949
14549
|
}
|
|
13950
14550
|
// 相对边框恢复(localStorage 锚点):窗口变化后保持离边距离。
|
|
13951
14551
|
// 仅认 v:2 净距离格式;旧格式(含避让距离)废弃,挂件保持默认右下角吸附。
|
|
13952
|
-
//
|
|
13953
|
-
//
|
|
14552
|
+
// issue #102:这里原与 applyAnchorPos() 各写了一份恢复逻辑(两份都只做下限夹紧),
|
|
14553
|
+
// 现在统一走 applyAnchorPos(),避免"只修一处、另一处仍复现"。
|
|
13954
14554
|
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
|
|
14555
|
+
if (applyAnchorPos()) {
|
|
13973
14556
|
settle()
|
|
14557
|
+
// 恢复时 offsetWidth 可能还是 0(字体/图片尚未就绪),尺寸就绪后再夹一次(issue #102)
|
|
14558
|
+
setTimeout(function () { try { settle() } catch (err) {} }, 500)
|
|
13974
14559
|
}
|
|
13975
14560
|
} catch (err) {}
|
|
13976
14561
|
refresh(false)
|
|
14562
|
+
// v734(issue #97):首次 GET 应用完成 —— 从这一刻起才允许 saveConfig() 落盘
|
|
14563
|
+
configLoaded = true
|
|
14564
|
+
if (configSavePending) { configSavePending = false; try { saveConfig() } catch (err) {} }
|
|
14565
|
+
})
|
|
14566
|
+
.catch(function () {
|
|
14567
|
+
// 读取失败:绝不能拿内存里的默认值去 PUT(那正是「设置被洗成默认值」)
|
|
14568
|
+
refresh(false)
|
|
14569
|
+
dshwvToast('⚠ 设置读取失败,已暂停保存以免覆盖你的原有设置<br>请刷新页面重试')
|
|
13977
14570
|
})
|
|
13978
|
-
.catch(function () { refresh(false) })
|
|
13979
14571
|
setInterval(function () { refresh(false); try { loadApiModels(null, true) } catch (err) {} }, REFRESH_MS)
|
|
13980
14572
|
|
|
13981
14573
|
// —— 每轮对话消耗检测:轮询 last-turn.json,出现新 seq 时弹消耗金额泡泡 ——
|