dsh-whale-widget 0.3.1 → 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/README.md CHANGED
@@ -10,7 +10,7 @@ DeepSeek Harness(DSH)Web 界面右下角的常驻挂件:小鲸鱼气泡图
10
10
 
11
11
  - 🐋 **常驻自启**:随 DSH Web 界面每次打开自动出现(标准 DSH bundle 插件)
12
12
  - 💰 **余额**:60 秒自动刷新 + 点击鲸鱼手动刷新;余额变化时数字**滚动动画**;瞬时网络抖动自动沿用最近余额不报错
13
- - 📊 **今日已用(小鲸鱼记账)**:不需要任何令牌。余额**下降**按观测累计为消费,余额**上升**(充值 / 赠金)单独记录、不会冲掉已有消费;检测到余额增加会提示「待核对余额调整」,可在「小鲸鱼记账 → 充值 / 余额校正」按实际累计到账金额与非调用扣减校正(逐轮明细保留 90 天或最多 2 万条、逐日归档保留 365 天,超期数据归档到 `.dshw-usage-archive.json`);金额按 8 位小数记账、显示保留两位
13
+ - 📊 **今日已用(小鲸鱼记账)**:不需要任何令牌。余额**下降**按观测累计为消费,余额**上升**(充值 / 赠金)单独记录、不会冲掉已有消费;检测到余额增加会提示「待核对余额调整」,可在「小鲸鱼记账 → **DeepSeek(内置)→ 设置 余额校正**」按实际累计到账金额与非调用扣减校正 —— **「已观测消费」与「余额校正」都是 DeepSeek 账户口径**(由该 API key 的余额观测得出,不含其它厂商;「本机模型费用」才是所有模型的本地估算)(逐轮明细保留 90 天或最多 2 万条、逐日归档保留 365 天,超期数据归档到 `.dshw-usage-archive.json`);金额按 8 位小数记账、显示保留两位
14
14
  - 💬 **每轮对话消耗**:监听 DSH 本机会话事件,按模型**真实 usage**(input / cache / output / reasoning tokens)换算金额,每轮结束弹出消耗泡泡;可开关、自动关闭秒数可设(0 = 不自动关闭),**泡泡内容可自定义**(模块化,金额用 `{cost}` 引用),入口:菜单 → 每轮消耗提示 → 「自定义提示」
15
15
  - ⛰️ **峰谷定价**:工作日高峰 9:00–12:00、14:00–18:00(北京时间),其余空闲;2026-08-23 起**周末全天谷价**。峰谷模块支持状态文字、倒计时、"梁文峰谷 / !?强强?! / 简洁"等多种样式
16
16
  - 📒 **用量记录窗口**:今日模型消费、近 7 天、全部记录;模型占比条;按日期展开逐条明细(含时间与金额);支持按日期或**模型名搜索**;模型名带友好标注(`deepseek-flash` → `DeepSeek-V4.1-Flash`,旧名标注"同 V4.1 Flash")
@@ -341,7 +341,7 @@ curl http://127.0.0.1:3080/dsh-whale/audio.json
341
341
  - **图片/音效不显示、没声音**:确认插件包内 `assets/` 完整(`DSniang1.png`、`*.mp3`、`minecraft-exp-orb.wav` 等);缺失时相关功能静默降级。
342
342
  - **余额报「未配置 DEEPSEEK_API_KEY」**:去 DSH 凭据里配置。
343
343
  - **今日已用显示 `--`**:先等一次成功的余额观测;统计从该观测时刻开始,起点之前的消费不在此区间内。
344
- - **今日已用与官网有差异**:先核对同一账户、同一币种与同一统计区间;若有充值或其它余额调整,用「小鲸鱼记账 → 充值 / 余额校正」填写实际到账金额。余额接口只返回余额快照、不提供充值流水,充值与消费发生在同一次刷新间隔时需要实际到账金额才能校正。
344
+ - **今日已用与官网有差异**:先核对同一账户、同一币种与同一统计区间;若有充值或其它余额调整,用「小鲸鱼记账 → DeepSeek(内置)→ 设置 余额校正」填写实际到账金额。余额接口只返回余额快照、不提供充值流水,充值与消费发生在同一次刷新间隔时需要实际到账金额才能校正。
345
345
  - **充值后消费数字没变**:这是预期行为——充值不会增加消费;数字上方会出现「待核对余额调整」,提示你补充本统计区间的累计到账金额。
346
346
  - **每轮消耗不显示**:确认菜单里「每轮对话后自动显示消耗金额」已勾选;一轮对话要完整结束(`turn/end`)才结算。余额变化泡泡与消耗泡泡抢层时,提醒会退化为居中卡片。
347
347
  - **每轮消耗泡泡的内容**:菜单 → 每轮消耗提示 → 「自定义提示」里编辑(模块化,金额用 `{cost}`);这里同时能设自动关闭秒数与任务结束音效。
@@ -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}',
@@ -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 单价的模型会把人民币数值渲染成 $。
@@ -2800,7 +2807,7 @@ function openApiModelMenu(modelId) {
2800
2807
  else st.textContent = '余额 ' + apiFmtMoney(m.balance, m.currency) + ' · 今日已用 ' + apiFmtMoney(m.todayUsage, apiTodayCur(m))
2801
2808
  card.appendChild(st)
2802
2809
  var ms = (usageSet && usageSet.models && usageSet.models[modelId]) || {}
2803
- function rowOf(label, stateFn, onEdit) {
2810
+ function rowOf(label, stateFn, onEdit, buttonLabel) {
2804
2811
  var r = document.createElement('div')
2805
2812
  r.className = 'dshwv-audiorow'
2806
2813
  var l = document.createElement('span')
@@ -2817,8 +2824,9 @@ function openApiModelMenu(modelId) {
2817
2824
  info.style.textOverflow = 'ellipsis'
2818
2825
  info.textContent = stateFn()
2819
2826
  r.appendChild(info)
2820
- r.appendChild(apiBtn('编辑', 'dshwv-roleimport', onEdit))
2827
+ r.appendChild(apiBtn(buttonLabel || '编辑', 'dshwv-roleimport', onEdit))
2821
2828
  card.appendChild(r)
2829
+ return r
2822
2830
  }
2823
2831
  rowOf('余额预警', function () {
2824
2832
  var a = ms.alert
@@ -2875,13 +2883,15 @@ function openApiModelMenu(modelId) {
2875
2883
  card.appendChild(pr)
2876
2884
  }
2877
2885
  // 只读行(复用现有 .dshwv-audiorow / .dshwv-usage-hint,不引入新颜色与字体)
2878
- function readonlyRow(label, text) {
2886
+ function readonlyRow(label, text, hintHtml) {
2879
2887
  var r = document.createElement('div')
2880
2888
  r.className = 'dshwv-audiorow'
2881
2889
  var l = document.createElement('span')
2882
2890
  l.textContent = label
2883
2891
  l.style.flex = '0 0 auto'
2884
2892
  r.appendChild(l)
2893
+ // 需要「?」说明时,说明收进圆圈(悬停显示、点击钉住),与挂件其它说明圈同一套组件
2894
+ if (hintHtml) { try { r.appendChild(dshwvAskDot(hintHtml)) } catch (err) {} }
2885
2895
  var v = document.createElement('span')
2886
2896
  v.className = 'dshwv-usage-hint'
2887
2897
  v.style.flex = '1'
@@ -2914,6 +2924,32 @@ function openApiModelMenu(modelId) {
2914
2924
  pTxt = '未设置(沿用内置价目表)→ 在「密钥 / 接口」里填写'
2915
2925
  }
2916
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
+ }
2917
2953
  // 币种不一致提示:今日已用按「自带币种」显示(会话事件为 CNY)。若与模型币种不同且没填汇率,
2918
2954
  // 今日预算提醒会被跳过(见 A 方案),这里给出可见的补救提示。
2919
2955
  var tuc = String((m && m.todayUsageCurrency) || '').toUpperCase()
@@ -3009,94 +3045,92 @@ function accountingTime(at) {
3009
3045
  } catch (err) { return String(at || '') }
3010
3046
  }
3011
3047
  var accountingMask = null
3012
- function openBalanceAdjustment() {
3013
- if (accountingMask) return
3014
- var previousFocus = document.activeElement
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
3015
3055
  var mask = document.createElement('div')
3016
3056
  mask.className = 'dshwv-usage-mask'
3017
- mask.style.display = 'flex'
3018
- mask.style.zIndex = '2147483647'
3057
+ mask.style.zIndex = '30000'
3019
3058
  var card = document.createElement('form')
3020
3059
  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'
3060
+ card.style.width = 'min(430px,94vw)'
3061
+ card.style.padding = '14px 16px'
3062
+ card.style.boxSizing = 'border-box'
3063
+ card.style.textAlign = 'left'
3022
3064
  card.setAttribute('role', 'dialog')
3023
3065
  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')
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'
3030
3077
  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)
3078
+ body.appendChild(introduction)
3079
+ var dates = apiSelectEl([], '')
3080
+ body.appendChild(apiPanelRow('日期', dates))
3046
3081
  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'
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')
3051
3086
  credits.inputMode = 'decimal'
3052
3087
  credits.required = true
3053
3088
  credits.autocomplete = 'off'
3054
- var creditsLabel = field('本统计区间累计到账金额', credits)
3055
- var debits = document.createElement('input')
3056
- debits.type = 'text'
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')
3057
3095
  debits.inputMode = 'decimal'
3058
3096
  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)
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)
3064
3102
  var preview = document.createElement('div')
3065
- preview.style.cssText = 'font-weight:600;font-size:15px;margin:14px 0'
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'
3066
3105
  preview.setAttribute('aria-live', 'polite')
3067
- card.appendChild(preview)
3106
+ body.appendChild(preview)
3068
3107
  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'
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'
3070
3109
  var confirm = document.createElement('input')
3071
3110
  confirm.type = 'checkbox'
3072
- confirm.style.marginTop = '5px'
3111
+ confirm.style.marginTop = '1px'
3073
3112
  confirmRow.appendChild(confirm)
3074
3113
  confirmRow.appendChild(document.createTextNode('我已核对本统计区间的全部到账和非调用扣减'))
3075
- card.appendChild(confirmRow)
3114
+ body.appendChild(apiPanelRow('确认', confirmRow))
3076
3115
  var status = document.createElement('div')
3116
+ status.className = 'dshwv-bubhint'
3077
3117
  status.setAttribute('role', 'status')
3078
- status.style.cssText = 'font-size:13px;white-space:normal;color:#b33333;margin:8px 0'
3118
+ status.style.cssText = 'margin:0 0 6px;white-space:normal;line-height:1.6;text-align:left;color:#b33333'
3079
3119
  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
- }
3120
+ body.appendChild(status)
3121
+ card.appendChild(body)
3093
3122
  var busy = false
3094
3123
  function close() {
3095
3124
  if (busy) return
3096
3125
  document.removeEventListener('keydown', keyHandler)
3097
3126
  mask.remove()
3098
3127
  accountingMask = null
3099
- try { previousFocus.focus() } catch (err) {}
3128
+ // 关掉校正窗口后回到同一个设置菜单,并把焦点放回「校正」按钮
3129
+ openApiModelMenu(modelId)
3130
+ try {
3131
+ var returnButton = apiModelMaskEl && apiModelMaskEl.querySelector('[data-action="balance-adjustment"]')
3132
+ if (returnButton) returnButton.focus()
3133
+ } catch (err) {}
3100
3134
  }
3101
3135
  function keyHandler(e) {
3102
3136
  if (e.key === 'Escape') { e.preventDefault(); close() }
@@ -3109,25 +3143,17 @@ function openBalanceAdjustment() {
3109
3143
  else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
3110
3144
  }
3111
3145
  }
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'
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)
3120
3156
  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
3157
  mask.appendChild(card)
3132
3158
  mask.addEventListener('click', function (e) { if (e.target === mask) close() })
3133
3159
  document.body.appendChild(mask)
@@ -3141,6 +3167,8 @@ function openBalanceAdjustment() {
3141
3167
  reset.disabled = !selected || !selected.correctedAt || busy
3142
3168
  if (!selected) {
3143
3169
  interval.textContent = '尚无可校正的余额观测。请先配置 DeepSeek API key 并成功刷新余额。'
3170
+ creditsHint.textContent = ''
3171
+ debitsHint.textContent = ''
3144
3172
  return
3145
3173
  }
3146
3174
  interval.textContent = '统计起点:' + accountingTime(selected.firstObservedAt) +
@@ -3148,8 +3176,8 @@ function openBalanceAdjustment() {
3148
3176
  '\n起点余额 ' + usageMoney(selected.openingBalance, selected.currency) +
3149
3177
  ' → 当前余额 ' + usageMoney(selected.currentBalance, selected.currency) +
3150
3178
  '\n当前:' + selected.label + ' ' + usageMoney(selected.amount, selected.currency)
3151
- creditsLabel.textContent = '本统计区间累计到账金额(' + selected.currency + ',未到账请填 0'
3152
- debitsLabel.textContent = '非调用造成的余额减少(' + selected.currency + ',没有请填 0'
3179
+ creditsHint.textContent = '本统计区间累计到账金额(' + selected.currency + ',未到账请填 0):包括充值、赠金等;多次到账请填合计,不要只填最后一笔。'
3180
+ debitsHint.textContent = '非调用造成的余额减少(' + selected.currency + ',没有请填 0):到期赠金、余额退回等。仅填写统计起点之后的金额;保存会替换之前的校正值。'
3153
3181
  credits.value = selected.credits == null ? '' : String(selected.credits)
3154
3182
  debits.value = selected.otherDebits == null ? '0' : String(selected.otherDebits)
3155
3183
  confirm.checked = false
@@ -3173,7 +3201,7 @@ function openBalanceAdjustment() {
3173
3201
  reset.disabled = true
3174
3202
  reload.disabled = true
3175
3203
  status.textContent = '正在刷新余额…'
3176
- fetch('/dsh-whale/balance-adjustments.json', { cache: 'no-store' })
3204
+ fetch(adjustmentUrl, { cache: 'no-store' })
3177
3205
  .then(function (r) { return r.json() })
3178
3206
  .then(function (data) {
3179
3207
  if (!data.ok) throw new Error(data.error || '读取失败')
@@ -3198,16 +3226,29 @@ function openBalanceAdjustment() {
3198
3226
  reset.disabled = true
3199
3227
  reload.disabled = true
3200
3228
  status.textContent = '正在保存…'
3201
- fetch('/dsh-whale/balance-adjustments.json', {
3229
+ fetch(adjustmentUrl, {
3202
3230
  method: 'PUT', headers: { 'Content-Type': 'application/json' },
3203
3231
  body: JSON.stringify({
3204
- day: selected.day, revision: selected.revision, action: action,
3232
+ modelId: modelId, day: selected.day, revision: selected.revision, action: action,
3205
3233
  credits: credits.value.trim(), otherDebits: debits.value.trim() || '0', confirmed: confirm.checked
3206
3234
  })
3207
3235
  })
3208
3236
  .then(function (r) { return r.json() })
3209
3237
  .then(function (data) {
3210
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
+ }
3211
3252
  busy = false
3212
3253
  close()
3213
3254
  refresh(true)
@@ -3235,25 +3276,12 @@ function fillUsagePanel(d) {
3235
3276
  var today = d.today || {}
3236
3277
  var todayModels = today.models || []
3237
3278
  var hasEvToday = todayModels.length > 0
3238
- // ① 今日模型消费(独立容器+滚动);标题不带下分隔线
3239
- var tToday = uSectionTitle(today.label || '今日消费', usageMoney(today.total, today.currency))
3240
- tToday.style.borderBottom = 'none'
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'))
3279
+ // ① 本机模型费用(所有模型的本地会话估算)
3280
+ // 「已观测消费」是 DeepSeek 账户口径(余额观测),已移到 小鲸鱼记账 DeepSeek(内置)→ 设置 里,
3281
+ // 与「余额校正」放在一起,避免在这里被误读成"全模型合计"。
3282
+ var modelTitle = uSectionTitle('本机模型费用', usageMoney(today.modelTotal, 'CNY'))
3283
+ // 标题与下方模型列表之间不再画分隔线(列表本身有边框,够了)
3284
+ modelTitle.style.borderBottom = 'none'
3257
3285
  modelTitle.title = '按本机 DSH 会话计算,不按账户余额比例缩放;两者覆盖范围不同。'
3258
3286
  wrap.appendChild(modelTitle)
3259
3287
  var todayBox = document.createElement('div')
@@ -8232,7 +8260,7 @@ var bubbleColorOpenMenu = null // 当前展开的颜色下拉(纯色/跑马灯,
8232
8260
  // —— 弹层/菜单层级统一助手(所有下拉与弹窗据此取高于当前可见层,避免互相压制) ——
8233
8261
  function visibleTopZ() {
8234
8262
  var top = 20500
8235
- var cand = [bubbleMask, bubbleItemMask, moduleMask, usageMoreMask, qeditEl, window.__dshwRemindMask]
8263
+ var cand = [bubbleMask, bubbleItemMask, moduleMask, usageMoreMask, qeditEl, window.__dshwRemindMask, apiModelMaskEl, accountingMask]
8236
8264
  function eff(el) {
8237
8265
  try {
8238
8266
  if (!el) return 0
package/lib/index.js CHANGED
@@ -75,6 +75,11 @@ const API_FILE_CANDIDATES = [
75
75
  path.join(DSH_HOME, 'profiles', 'web', '.dshw-api.json'),
76
76
  ]
77
77
  const API_BUILTIN_ID = 'deepseek'
78
+ // 「充值 / 余额校正」只属于固定的 DeepSeek(内置):模型条目下发 canAdjustBalance,路由层再校验一次。
79
+ // 新增厂商模板 / 手动新增的同名模型 / Kimi 等其它厂商都不会继承这个能力。
80
+ function canAdjustBuiltinBalance(model) {
81
+ return !!(model && model.id === API_BUILTIN_ID && model.builtin === true && model.provider === 'deepseek')
82
+ }
78
83
  // 内置厂商模板:balance 描述「怎么取余额」。json 路径支持 a.b[0].c;scale 为取值后的乘数。
79
84
  // 余额接口已按官方文档 / 社区参考核对(DeepSeek、OpenRouter、Kimi、阶跃、Novita、OpenAI 兼容中转站);
80
85
  // 硅基流动的余额接口已官方下线(410),故改为「无余额接口 + /v1/models 探活」;
@@ -916,7 +921,7 @@ export default {
916
921
  const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
917
922
  for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
918
923
  return {
919
- ok: true, version: '0.3.1', today: todayData, days7,
924
+ ok: true, version: '0.3.2', today: todayData, days7,
920
925
  total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
921
926
  all: {
922
927
  days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
@@ -1824,6 +1829,7 @@ export default {
1824
1829
  let entry = {
1825
1830
  id: m.id, name: m.name, provider: m.provider, currency: m.currency,
1826
1831
  keyRef: m.keyRef, builtin: !!m.builtin, baseUrl: m.baseUrl || '',
1832
+ canAdjustBalance: canAdjustBuiltinBalance(m),
1827
1833
  matchIds: m.matchIds || [], settings: settings.models && settings.models[m.id] ? settings.models[m.id] : null,
1828
1834
  price: m.price || null,
1829
1835
  // 手动额度(订阅/资源包):总量、单位、已用、重置周期;前端据此显示额度模块
@@ -1959,7 +1965,7 @@ export default {
1959
1965
  const led = readUsageLedger()
1960
1966
  const summary = daySummary(led, todayKey())
1961
1967
  return {
1962
- ...visible, version: '0.3.1', isPeak: isPeakTime(Math.floor(Date.now() / 1000)),
1968
+ ...visible, version: '0.3.2', isPeak: isPeakTime(Math.floor(Date.now() / 1000)),
1963
1969
  todayUsage: summary.amount, todayUsageCurrency: summary.currency,
1964
1970
  usageSource: summary.source, usageLabel: summary.label, usageMode: 'ledger',
1965
1971
  accounting: balanceSummary(led, todayKey()),
@@ -2498,9 +2504,23 @@ export default {
2498
2504
  path: '/dsh-whale/balance-adjustments.json',
2499
2505
  handler: async (req, res) => {
2500
2506
  try {
2507
+ // 先核对身份再碰余额或账本:必须是唯一且明确的 DeepSeek(内置)。
2508
+ // 新增厂商模板不会自动继承这个能力,也拒绝其它模型读写校正。
2509
+ const targetIds = new URL(req.url || '/', 'http://localhost').searchParams.getAll('modelId')
2510
+ const targetModelId = targetIds.length === 1 ? targetIds[0] : null
2511
+ if (targetModelId !== API_BUILTIN_ID || !canAdjustBuiltinBalance(apiModelById(targetModelId))) {
2512
+ const error = new Error('余额校正仅支持 DeepSeek(内置),请从该模型的设置菜单进入')
2513
+ error.status = 403
2514
+ throw error
2515
+ }
2501
2516
  if (req.method === 'PUT') {
2502
2517
  const input = JSON.parse(await readBodyMax(req, 8192))
2503
2518
  if (!input || typeof input !== 'object') throw new Error('校正内容无效')
2519
+ if (input.modelId !== targetModelId) {
2520
+ const error = new Error('校正请求的模型不匹配,仅支持 DeepSeek(内置)')
2521
+ error.status = 403
2522
+ throw error
2523
+ }
2504
2524
  if (input.day === todayKey()) {
2505
2525
  const fresh = await getBalance(true)
2506
2526
  if (!fresh.ok || fresh.stale) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-widget",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "DSH Web 界面右下角的 DeepSeek 余额小鲸鱼挂件:余额/今日已用/峰谷定价、自定义泡泡点击序列(文本/余额/今日/峰谷/图片/随机语句与并列加权选择)、逐行样式与字体、悬浮快捷编辑、音效与每轮消耗、自定义角色/动图/音效、吸附与翻转自定义",
5
5
  "keywords": [
6
6
  "dsh",