@yyfather/dsh-balance 0.1.0 → 0.1.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/LICENSE +21 -21
- package/README.md +63 -55
- package/README_EN.md +63 -55
- package/client.js +253 -242
- package/cordis.patch.yml +3 -3
- package/index.js +337 -341
- package/package.json +40 -60
package/client.js
CHANGED
|
@@ -1,245 +1,256 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
import React from 'react'
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@yyfather/dsh-balance",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* dsh-balance — Client half (browser).
|
|
9
|
+
*
|
|
10
|
+
* Registers the ambient composition dock readout. All data comes from the
|
|
11
|
+
* Host's loopback-only same-origin routes; no credential ever reaches the page.
|
|
12
|
+
*/
|
|
13
|
+
const React = require("react");
|
|
14
|
+
|
|
15
|
+
var inject = ["slots"];
|
|
16
|
+
|
|
17
|
+
const STATE_ROUTE = '/dsh-balance/api/state'
|
|
18
|
+
const CONFIG_ROUTE = '/dsh-balance/api/config'
|
|
19
|
+
|
|
20
|
+
const CSS = [
|
|
21
|
+
'.dsh-balance-dock{position:relative;display:inline-flex}',
|
|
22
|
+
'.dsh-balance-dock-line{font-size:12px;opacity:.85;white-space:nowrap}',
|
|
23
|
+
'.dsh-balance-dock-line.warn{opacity:1;color:#f59e0b;font-weight:500;background:rgba(245,158,11,.13);border:1px solid rgba(245,158,11,.35);border-radius:999px;padding:1px 8px}',
|
|
24
|
+
'.dsh-balance-pop{position:absolute;bottom:calc(100% + 10px);left:0;z-index:60;background:var(--dsh-panel-bg,#1f1f1f);color:var(--dsh-text,#e8e8e8);border:1px solid rgba(128,128,128,.35);border-radius:10px;padding:12px;display:flex;flex-direction:column;gap:8px;font-size:12px;white-space:nowrap;min-width:280px;box-shadow:0 8px 28px rgba(0,0,0,.4)}',
|
|
25
|
+
'.dsh-balance-pop .title-row{display:flex;justify-content:space-between;align-items:center;font-weight:600;font-size:12.5px;opacity:.95}',
|
|
26
|
+
'.dsh-balance-pop .xbtn{border:none;background:none;color:inherit;opacity:.6;cursor:pointer;font-size:12px;padding:0}',
|
|
27
|
+
'.dsh-balance-pop label{display:flex;gap:8px;align-items:center;justify-content:space-between}',
|
|
28
|
+
'.dsh-balance-pop .field-name{opacity:.8}',
|
|
29
|
+
'.dsh-balance-pop input[type=number]{width:72px;font-size:12px;background:rgba(128,128,128,.08);color:inherit;border:1px solid rgba(128,128,128,.4);border-radius:6px;padding:3px 8px}',
|
|
30
|
+
'.dsh-balance-pop input[type=checkbox]{accent-color:#f59e0b}',
|
|
31
|
+
'.dsh-balance-pop select{font-size:12px;max-width:200px;background:rgba(128,128,128,.08);color:inherit;border:1px solid rgba(128,128,128,.4);border-radius:6px;padding:3px 6px}',
|
|
32
|
+
'.dsh-balance-bar{width:100%;height:5px;background:rgba(128,128,128,.22);border-radius:999px;overflow:hidden}',
|
|
33
|
+
'.dsh-balance-bar>div{height:100%;background:#f59e0b;border-radius:999px;transition:width .3s}',
|
|
34
|
+
'.dsh-balance-pop .actions{display:flex;gap:8px;align-items:center}',
|
|
35
|
+
'.dsh-balance-pop .actions button{font-size:12px;border-radius:6px;padding:3px 12px;cursor:pointer;border:1px solid rgba(128,128,128,.4);background:rgba(128,128,128,.1);color:inherit}',
|
|
36
|
+
'.dsh-balance-pop .actions button.primary{background:#f59e0b;color:#1a1a1a;font-weight:600;border:none}',
|
|
37
|
+
'.dsh-balance-pop .muted{opacity:.65}',
|
|
38
|
+
'.dsh-balance-pop .section{border-top:1px solid rgba(128,128,128,.25);padding-top:8px}',
|
|
39
|
+
].join('')
|
|
40
|
+
|
|
41
|
+
const symbolOf = (currency) => currency === 'CNY' ? '¥' : currency === 'USD' ? '$' : (currency || '') + ' '
|
|
42
|
+
|
|
43
|
+
async function loadState(sessionId, force, signal) {
|
|
44
|
+
const params = new URLSearchParams()
|
|
45
|
+
if (sessionId) params.set('session', sessionId)
|
|
46
|
+
if (force) params.set('refresh', '1')
|
|
47
|
+
const qs = params.toString()
|
|
48
|
+
const resp = await fetch(STATE_ROUTE + (qs ? '?' + qs : ''), {
|
|
49
|
+
headers: { accept: 'application/json' },
|
|
50
|
+
cache: 'no-store',
|
|
51
|
+
...signal ? { signal } : {},
|
|
52
|
+
})
|
|
53
|
+
const value = await resp.json()
|
|
54
|
+
if (value === null || typeof value !== 'object' || value.ok !== true) {
|
|
55
|
+
throw new Error((value && value.message) || 'Host 响应异常')
|
|
56
|
+
}
|
|
57
|
+
return value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function saveConfig(patch) {
|
|
61
|
+
const resp = await fetch(CONFIG_ROUTE, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
64
|
+
body: JSON.stringify(patch),
|
|
65
|
+
})
|
|
66
|
+
const value = await resp.json()
|
|
67
|
+
if (value === null || typeof value !== 'object' || value.ok !== true) {
|
|
68
|
+
throw new Error((value && value.message) || 'Host 响应异常')
|
|
69
|
+
}
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function BalancePanel(props) {
|
|
74
|
+
const data = props.data
|
|
75
|
+
const st = data.state || {}
|
|
76
|
+
const cfg = data.config || {}
|
|
77
|
+
const [spendThreshold, setSpendThreshold] = React.useState(String(cfg.spendThreshold ?? 1))
|
|
78
|
+
const [spendAlert, setSpendAlert] = React.useState(cfg.spendAlert !== false)
|
|
79
|
+
const [threshold, setThreshold] = React.useState(String(cfg.threshold ?? 10))
|
|
80
|
+
const [priceKey, setPriceKey] = React.useState('default')
|
|
81
|
+
const [tierEdit, setTierEdit] = React.useState('off')
|
|
82
|
+
const [priceIn, setPriceIn] = React.useState('1.5')
|
|
83
|
+
const [priceCache, setPriceCache] = React.useState('0.05')
|
|
84
|
+
const [priceOut, setPriceOut] = React.useState('4.5')
|
|
85
|
+
const [usdRate, setUsdRate] = React.useState(String(cfg.usdRate ?? 7.2))
|
|
86
|
+
const active = st.cost || 0
|
|
87
|
+
const thr = Math.max(0, Number(spendThreshold) || 1)
|
|
88
|
+
const pct = thr > 0 ? Math.min(100, Math.round(active / thr * 100)) : 0
|
|
89
|
+
const over = thr > 0 && active >= thr
|
|
90
|
+
const keys = cfg.prices ? Object.keys(cfg.prices) : ['default']
|
|
91
|
+
const labelOf = (k) => {
|
|
92
|
+
const p = (cfg.prices && (cfg.prices[k] || cfg.prices.default)) || {}
|
|
93
|
+
return k === 'default' ? '默认' : k + (p.currency === 'USD' ? ' ($)' : '')
|
|
94
|
+
}
|
|
95
|
+
const syncPrice = (key, tier) => {
|
|
96
|
+
const p = (cfg.prices && (cfg.prices[key] || cfg.prices.default)) || {}
|
|
97
|
+
const t = tier === 'peak' ? { in: p.peakIn, cache: p.peakCache, out: p.peakOut } : { in: p.in, cache: p.cache, out: p.out }
|
|
98
|
+
setPriceIn(String(t.in ?? 1.5)); setPriceCache(String(t.cache ?? 0.05)); setPriceOut(String(t.out ?? 4.5))
|
|
99
|
+
}
|
|
100
|
+
const num = (v, d) => { const n = Number(v); return Number.isFinite(n) && n >= 0 ? n : d }
|
|
101
|
+
const save = () => {
|
|
102
|
+
const rt = Number(usdRate)
|
|
103
|
+
saveConfig({
|
|
104
|
+
threshold: num(threshold, 10),
|
|
105
|
+
spendAlert,
|
|
106
|
+
spendThreshold: num(spendThreshold, 1),
|
|
107
|
+
usdRate: Number.isFinite(rt) && rt > 0 ? rt : 7.2,
|
|
108
|
+
priceKey: priceKey || 'default',
|
|
109
|
+
tier: tierEdit === 'peak' ? 'peak' : 'off',
|
|
110
|
+
price: { in: num(priceIn, 1.5), cache: num(priceCache, 0.05), out: num(priceOut, 4.5) },
|
|
111
|
+
}).then((v) => { if (props.onChanged) props.onChanged(v) }).catch(() => {})
|
|
112
|
+
}
|
|
113
|
+
const refresh = () => {
|
|
114
|
+
loadState(props.sessionId, true).then((v) => { if (props.onChanged) props.onChanged(v) }).catch(() => {})
|
|
115
|
+
}
|
|
116
|
+
return React.createElement('div', { className: 'dsh-balance-pop' },
|
|
117
|
+
React.createElement('div', { className: 'title-row' },
|
|
118
|
+
React.createElement('span', null, '余额开销设置'),
|
|
119
|
+
React.createElement('button', { className: 'xbtn', onClick: props.onClose }, '✕')
|
|
120
|
+
),
|
|
121
|
+
React.createElement('label', null,
|
|
122
|
+
React.createElement('span', { className: 'field-name' }, '花费提醒线 ¥'),
|
|
123
|
+
React.createElement('input', { type: 'number', min: 0, step: 0.1, value: spendThreshold, onChange: (e) => setSpendThreshold(e.target.value) })
|
|
124
|
+
),
|
|
125
|
+
React.createElement('label', null,
|
|
126
|
+
React.createElement('span', { className: 'field-name' }, '启用花费提醒(按本次活跃)'),
|
|
127
|
+
React.createElement('input', { type: 'checkbox', checked: spendAlert, onChange: (e) => setSpendAlert(e.target.checked) })
|
|
128
|
+
),
|
|
129
|
+
React.createElement('div', { className: 'dsh-balance-bar' }, React.createElement('div', { style: { width: pct + '%' } })),
|
|
130
|
+
React.createElement('span', { className: 'muted' }, '本次活跃 ' + symbolOf(st.currency || 'CNY') + String(active) + ' / ' + symbolOf(st.currency || 'CNY') + String(thr) + (over ? ' ⚠ 已超线' : '')),
|
|
131
|
+
React.createElement('label', null,
|
|
132
|
+
React.createElement('span', { className: 'field-name' }, '余额提醒线 ¥'),
|
|
133
|
+
React.createElement('input', { type: 'number', min: 0, step: 1, value: threshold, onChange: (e) => setThreshold(e.target.value) })
|
|
134
|
+
),
|
|
135
|
+
React.createElement('div', { className: 'section' },
|
|
136
|
+
React.createElement('label', null,
|
|
137
|
+
React.createElement('span', { className: 'field-name' }, '模型价目'),
|
|
138
|
+
React.createElement('select', { value: priceKey, onChange: (e) => { setPriceKey(e.target.value); syncPrice(e.target.value, tierEdit) } }, keys.map((k) => React.createElement('option', { value: k, key: k }, labelOf(k))))
|
|
139
|
+
),
|
|
140
|
+
React.createElement('label', null,
|
|
141
|
+
React.createElement('span', { className: 'field-name' }, '时段'),
|
|
142
|
+
React.createElement('select', { value: tierEdit, onChange: (e) => { setTierEdit(e.target.value); syncPrice(priceKey, e.target.value) } },
|
|
143
|
+
React.createElement('option', { value: 'off' }, '空闲'), React.createElement('option', { value: 'peak' }, '高峰'))
|
|
144
|
+
),
|
|
145
|
+
React.createElement('label', null,
|
|
146
|
+
React.createElement('span', { className: 'field-name' }, '输入/缓存/输出 ¥每百万'),
|
|
147
|
+
React.createElement('span', { style: { display: 'inline-flex', gap: 4 } },
|
|
148
|
+
React.createElement('input', { type: 'number', min: 0, step: 0.01, value: priceIn, onChange: (e) => setPriceIn(e.target.value) }),
|
|
149
|
+
React.createElement('input', { type: 'number', min: 0, step: 0.01, value: priceCache, onChange: (e) => setPriceCache(e.target.value) }),
|
|
150
|
+
React.createElement('input', { type: 'number', min: 0, step: 0.01, value: priceOut, onChange: (e) => setPriceOut(e.target.value) })
|
|
151
|
+
)
|
|
152
|
+
),
|
|
153
|
+
React.createElement('label', null,
|
|
154
|
+
React.createElement('span', { className: 'field-name' }, '美元汇率 ¥/$'),
|
|
155
|
+
React.createElement('input', { type: 'number', min: 0, step: 0.1, value: usdRate, onChange: (e) => setUsdRate(e.target.value) })
|
|
156
|
+
)
|
|
157
|
+
),
|
|
158
|
+
React.createElement('div', { className: 'actions' },
|
|
159
|
+
React.createElement('button', { className: 'primary', onClick: save }, '保存'),
|
|
160
|
+
React.createElement('button', { onClick: refresh }, '立即刷新')
|
|
161
|
+
),
|
|
162
|
+
React.createElement('span', { className: 'muted' }, '本次活跃 = 自本次插件启用以来的新花费;本会话 = 当前会话全部花费')
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function BalanceDock(props) {
|
|
167
|
+
const sessionId = props && props.sessionId
|
|
168
|
+
const [data, setData] = React.useState(null)
|
|
169
|
+
const [open, setOpen] = React.useState(false)
|
|
170
|
+
React.useEffect(() => {
|
|
171
|
+
let alive = true
|
|
172
|
+
const load = (force) => {
|
|
173
|
+
loadState(sessionId, force).then((v) => { if (alive) setData(v) }).catch(() => { if (alive) setData({ state: { status: 'error', error: '连接失败' } }) })
|
|
174
|
+
}
|
|
175
|
+
load(false)
|
|
176
|
+
const timer = window.setInterval(() => load(false), 5000)
|
|
177
|
+
return () => { alive = false; window.clearInterval(timer) }
|
|
178
|
+
}, [sessionId])
|
|
179
|
+
if (data === null) return null
|
|
180
|
+
const st = data.state || {}
|
|
181
|
+
const cfg = data.config || {}
|
|
182
|
+
const below = st.status === 'ok' && !st.external && typeof st.balance === 'number' && st.balance < (cfg.threshold ?? 10)
|
|
183
|
+
const costWarn = st.status === 'ok' && st.spendAlert === true
|
|
184
|
+
const warn = below || costWarn
|
|
185
|
+
let text
|
|
186
|
+
if (st.status === 'loading') text = '余额 …'
|
|
187
|
+
else if (st.status === 'unconfigured') text = '余额未配置'
|
|
188
|
+
else if (st.status === 'error') text = '余额获取失败'
|
|
189
|
+
else if (warn) {
|
|
190
|
+
const pieces = []
|
|
191
|
+
if (costWarn) pieces.push('本次活跃 ' + symbolOf(st.currency) + String(st.cost || 0) + '/' + symbolOf(st.currency) + String(cfg.spendThreshold ?? 1))
|
|
192
|
+
if (below) pieces.push('余额 ' + symbolOf(st.currency) + String(st.balance) + '/' + symbolOf(st.currency) + String(cfg.threshold ?? 10))
|
|
193
|
+
text = '⚠ ' + pieces.join(' · ')
|
|
194
|
+
} else {
|
|
195
|
+
const total = st.costTotal || 0
|
|
196
|
+
const active = st.cost || 0
|
|
197
|
+
const last = st.lastCost !== null && st.lastCost !== undefined ? st.lastCost : null
|
|
198
|
+
const prev = st.prevCost !== null && st.prevCost !== undefined ? st.prevCost : null
|
|
199
|
+
text = ''
|
|
200
|
+
if (!st.external) text += '余额 ' + symbolOf(st.currency) + String(st.balance) + ' · '
|
|
201
|
+
text += '本会话 ' + symbolOf(st.currency) + String(total)
|
|
202
|
+
text += ' · 本次活跃 ' + symbolOf(st.currency) + String(active)
|
|
203
|
+
if (last !== null) text += ' · 最近一次 ' + symbolOf(st.currency) + String(last)
|
|
204
|
+
if (prev !== null) text += ' · 上次对话 ' + symbolOf(st.currency) + String(prev)
|
|
205
|
+
}
|
|
206
|
+
let title = ''
|
|
207
|
+
if (st.status === 'ok') {
|
|
208
|
+
const total = st.costTotal || 0
|
|
209
|
+
const active = st.cost || 0
|
|
210
|
+
const last = st.lastCost !== null && st.lastCost !== undefined ? st.lastCost : null
|
|
211
|
+
const prev = st.prevCost !== null && st.prevCost !== undefined ? st.prevCost : null
|
|
212
|
+
title = st.external ? '当前会话为小米 MiMo 计费(不适用 DeepSeek 余额)' : 'DeepSeek 账户余额 ' + (st.currency || '') + ' · 更新于 ' + new Date(st.updatedAt || Date.now()).toLocaleTimeString()
|
|
213
|
+
if (st.tokens) title += ' · 本会话token: 输入 ' + st.tokens.input + ' · 缓存读 ' + st.tokens.cacheRead + ' · 输出 ' + st.tokens.output
|
|
214
|
+
title += ' · 本会话(' + (st.model || '默认') + ') ' + symbolOf(st.currency) + total + ' · 本次活跃 ' + symbolOf(st.currency) + active + ' (' + (st.samples || 0) + '次新请求)'
|
|
215
|
+
if (last !== null) title += ' · 最近一次 ' + symbolOf(st.currency) + last
|
|
216
|
+
if (prev !== null) title += ' · 上次对话(' + (st.prevModel || '默认') + ') ' + symbolOf(st.currency) + prev
|
|
217
|
+
if (costWarn) title += ' · ⚠ 本次活跃 ≥ ' + symbolOf(st.currency) + String(cfg.spendThreshold ?? 1)
|
|
218
|
+
if (below) title += ' · ⚠ 余额 < ' + symbolOf(st.currency) + String(cfg.threshold ?? 10)
|
|
219
|
+
} else if (st.status === 'unconfigured') {
|
|
220
|
+
title = '未找到 DEEPSEEK_API_KEY,请在 设置 → 模型 中配置'
|
|
221
|
+
} else if (st.status === 'error') {
|
|
222
|
+
title = st.error || '查询失败'
|
|
223
|
+
}
|
|
224
|
+
return React.createElement('div', { className: 'dsh-balance-dock', title },
|
|
225
|
+
React.createElement('span', {
|
|
226
|
+
className: 'dsh-balance-dock-line' + (warn ? ' warn' : ''),
|
|
227
|
+
onClick: () => setOpen(!open),
|
|
228
|
+
style: { cursor: 'pointer' },
|
|
229
|
+
}, text),
|
|
230
|
+
open ? React.createElement(BalancePanel, { data, sessionId, onChanged: setData, onClose: () => setOpen(false) }) : null
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function apply(ctx) {
|
|
235
|
+
ctx.effect(() => {
|
|
236
|
+
const style = document.createElement('style')
|
|
237
|
+
style.dataset.plugin = 'dsh-balance'
|
|
238
|
+
style.textContent = CSS
|
|
239
|
+
document.head.append(style)
|
|
240
|
+
return () => { style.remove() }
|
|
241
|
+
}, 'dsh-balance: styles')
|
|
242
|
+
|
|
243
|
+
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
244
|
+
name: 'conversation.composer.dock',
|
|
245
|
+
id: 'dsh-balance',
|
|
246
|
+
order: 10,
|
|
247
|
+
inject: () => ({ loadState, saveConfig }),
|
|
248
|
+
}, BalanceDock))
|
|
249
|
+
}
|
|
10
250
|
|
|
11
|
-
const STATE_ROUTE = '/dsh-balance/api/state'
|
|
12
|
-
const CONFIG_ROUTE = '/dsh-balance/api/config'
|
|
13
251
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'.dsh-balance-pop .title-row{display:flex;justify-content:space-between;align-items:center;font-weight:600;font-size:12.5px;opacity:.95}',
|
|
20
|
-
'.dsh-balance-pop .xbtn{border:none;background:none;color:inherit;opacity:.6;cursor:pointer;font-size:12px;padding:0}',
|
|
21
|
-
'.dsh-balance-pop label{display:flex;gap:8px;align-items:center;justify-content:space-between}',
|
|
22
|
-
'.dsh-balance-pop .field-name{opacity:.8}',
|
|
23
|
-
'.dsh-balance-pop input[type=number]{width:72px;font-size:12px;background:rgba(128,128,128,.08);color:inherit;border:1px solid rgba(128,128,128,.4);border-radius:6px;padding:3px 8px}',
|
|
24
|
-
'.dsh-balance-pop input[type=checkbox]{accent-color:#f59e0b}',
|
|
25
|
-
'.dsh-balance-pop select{font-size:12px;max-width:200px;background:rgba(128,128,128,.08);color:inherit;border:1px solid rgba(128,128,128,.4);border-radius:6px;padding:3px 6px}',
|
|
26
|
-
'.dsh-balance-bar{width:100%;height:5px;background:rgba(128,128,128,.22);border-radius:999px;overflow:hidden}',
|
|
27
|
-
'.dsh-balance-bar>div{height:100%;background:#f59e0b;border-radius:999px;transition:width .3s}',
|
|
28
|
-
'.dsh-balance-pop .actions{display:flex;gap:8px;align-items:center}',
|
|
29
|
-
'.dsh-balance-pop .actions button{font-size:12px;border-radius:6px;padding:3px 12px;cursor:pointer;border:1px solid rgba(128,128,128,.4);background:rgba(128,128,128,.1);color:inherit}',
|
|
30
|
-
'.dsh-balance-pop .actions button.primary{background:#f59e0b;color:#1a1a1a;font-weight:600;border:none}',
|
|
31
|
-
'.dsh-balance-pop .muted{opacity:.65}',
|
|
32
|
-
'.dsh-balance-pop .section{border-top:1px solid rgba(128,128,128,.25);padding-top:8px}',
|
|
33
|
-
].join('')
|
|
34
|
-
|
|
35
|
-
const symbolOf = (currency) => currency === 'CNY' ? '¥' : currency === 'USD' ? '$' : (currency || '') + ' '
|
|
36
|
-
const round2 = (n) => Math.round(n * 100) / 100
|
|
37
|
-
|
|
38
|
-
async function loadState(sessionId, force, signal) {
|
|
39
|
-
const params = new URLSearchParams()
|
|
40
|
-
if (sessionId) params.set('session', sessionId)
|
|
41
|
-
if (force) params.set('refresh', '1')
|
|
42
|
-
const qs = params.toString()
|
|
43
|
-
const resp = await fetch(STATE_ROUTE + (qs ? '?' + qs : ''), {
|
|
44
|
-
headers: { accept: 'application/json' },
|
|
45
|
-
cache: 'no-store',
|
|
46
|
-
...signal ? { signal } : {},
|
|
47
|
-
})
|
|
48
|
-
const value = await resp.json()
|
|
49
|
-
if (value === null || typeof value !== 'object' || value.ok !== true) {
|
|
50
|
-
throw new Error((value && value.message) || 'Host 响应异常')
|
|
51
|
-
}
|
|
52
|
-
return value
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function saveConfig(patch) {
|
|
56
|
-
const resp = await fetch(CONFIG_ROUTE, {
|
|
57
|
-
method: 'POST',
|
|
58
|
-
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
59
|
-
body: JSON.stringify(patch),
|
|
60
|
-
})
|
|
61
|
-
const value = await resp.json()
|
|
62
|
-
if (value === null || typeof value !== 'object' || value.ok !== true) {
|
|
63
|
-
throw new Error((value && value.message) || 'Host 响应异常')
|
|
64
|
-
}
|
|
65
|
-
return value
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Click-to-set panel under the dock readout: alert lines + per-model pricing. */
|
|
69
|
-
function BalancePanel(props) {
|
|
70
|
-
const data = props.data
|
|
71
|
-
const st = data.state || {}
|
|
72
|
-
const cfg = data.config || {}
|
|
73
|
-
const [spendThreshold, setSpendThreshold] = React.useState(String(cfg.spendThreshold ?? 1))
|
|
74
|
-
const [spendAlert, setSpendAlert] = React.useState(cfg.spendAlert !== false)
|
|
75
|
-
const [threshold, setThreshold] = React.useState(String(cfg.threshold ?? 10))
|
|
76
|
-
const [priceKey, setPriceKey] = React.useState('default')
|
|
77
|
-
const [tierEdit, setTierEdit] = React.useState('off')
|
|
78
|
-
const [priceIn, setPriceIn] = React.useState('1.5')
|
|
79
|
-
const [priceCache, setPriceCache] = React.useState('0.05')
|
|
80
|
-
const [priceOut, setPriceOut] = React.useState('4.5')
|
|
81
|
-
const [usdRate, setUsdRate] = React.useState(String(cfg.usdRate ?? 7.2))
|
|
82
|
-
const active = st.cost || 0
|
|
83
|
-
const thr = Math.max(0, Number(spendThreshold) || 1)
|
|
84
|
-
const pct = thr > 0 ? Math.min(100, Math.round(active / thr * 100)) : 0
|
|
85
|
-
const over = thr > 0 && active >= thr
|
|
86
|
-
const keys = cfg.prices ? Object.keys(cfg.prices) : ['default']
|
|
87
|
-
const labelOf = (k) => {
|
|
88
|
-
const p = (cfg.prices && (cfg.prices[k] || cfg.prices.default)) || {}
|
|
89
|
-
return k === 'default' ? '默认' : k + (p.currency === 'USD' ? ' ($)' : '')
|
|
90
|
-
}
|
|
91
|
-
const syncPrice = (key, tier) => {
|
|
92
|
-
const p = (cfg.prices && (cfg.prices[key] || cfg.prices.default)) || {}
|
|
93
|
-
const t = tier === 'peak' ? { in: p.peakIn, cache: p.peakCache, out: p.peakOut } : { in: p.in, cache: p.cache, out: p.out }
|
|
94
|
-
setPriceIn(String(t.in ?? 1.5)); setPriceCache(String(t.cache ?? 0.05)); setPriceOut(String(t.out ?? 4.5))
|
|
95
|
-
}
|
|
96
|
-
const num = (v, d) => { const n = Number(v); return Number.isFinite(n) && n >= 0 ? n : d }
|
|
97
|
-
const save = () => {
|
|
98
|
-
const rt = Number(usdRate)
|
|
99
|
-
saveConfig({
|
|
100
|
-
threshold: num(threshold, 10),
|
|
101
|
-
spendAlert,
|
|
102
|
-
spendThreshold: num(spendThreshold, 1),
|
|
103
|
-
usdRate: Number.isFinite(rt) && rt > 0 ? rt : 7.2,
|
|
104
|
-
priceKey: priceKey || 'default',
|
|
105
|
-
tier: tierEdit === 'peak' ? 'peak' : 'off',
|
|
106
|
-
price: { in: num(priceIn, 1.5), cache: num(priceCache, 0.05), out: num(priceOut, 4.5) },
|
|
107
|
-
}).then((v) => { if (props.onChanged) props.onChanged(v) }).catch(() => {})
|
|
108
|
-
}
|
|
109
|
-
const refresh = () => {
|
|
110
|
-
loadState(props.sessionId, true).then((v) => { if (props.onChanged) props.onChanged(v) }).catch(() => {})
|
|
111
|
-
}
|
|
112
|
-
return React.createElement('div', { className: 'dsh-balance-pop' },
|
|
113
|
-
React.createElement('div', { className: 'title-row' },
|
|
114
|
-
React.createElement('span', null, '余额开销设置'),
|
|
115
|
-
React.createElement('button', { className: 'xbtn', onClick: props.onClose }, '✕')
|
|
116
|
-
),
|
|
117
|
-
React.createElement('label', null,
|
|
118
|
-
React.createElement('span', { className: 'field-name' }, '花费提醒线 ¥'),
|
|
119
|
-
React.createElement('input', { type: 'number', min: 0, step: 0.1, value: spendThreshold, onChange: (e) => setSpendThreshold(e.target.value) })
|
|
120
|
-
),
|
|
121
|
-
React.createElement('label', null,
|
|
122
|
-
React.createElement('span', { className: 'field-name' }, '启用花费提醒(按本次活跃)'),
|
|
123
|
-
React.createElement('input', { type: 'checkbox', checked: spendAlert, onChange: (e) => setSpendAlert(e.target.checked) })
|
|
124
|
-
),
|
|
125
|
-
React.createElement('div', { className: 'dsh-balance-bar' }, React.createElement('div', { style: { width: pct + '%' } })),
|
|
126
|
-
React.createElement('span', { className: 'muted' }, '本次活跃 ' + symbolOf(st.currency || 'CNY') + String(active) + ' / ' + symbolOf(st.currency || 'CNY') + String(thr) + (over ? ' ⚠ 已超线' : '')),
|
|
127
|
-
React.createElement('label', null,
|
|
128
|
-
React.createElement('span', { className: 'field-name' }, '余额提醒线 ¥'),
|
|
129
|
-
React.createElement('input', { type: 'number', min: 0, step: 1, value: threshold, onChange: (e) => setThreshold(e.target.value) })
|
|
130
|
-
),
|
|
131
|
-
React.createElement('div', { className: 'section' },
|
|
132
|
-
React.createElement('label', null,
|
|
133
|
-
React.createElement('span', { className: 'field-name' }, '模型价目'),
|
|
134
|
-
React.createElement('select', { value: priceKey, onChange: (e) => { setPriceKey(e.target.value); syncPrice(e.target.value, tierEdit) } }, keys.map((k) => React.createElement('option', { value: k, key: k }, labelOf(k))))
|
|
135
|
-
),
|
|
136
|
-
React.createElement('label', null,
|
|
137
|
-
React.createElement('span', { className: 'field-name' }, '时段'),
|
|
138
|
-
React.createElement('select', { value: tierEdit, onChange: (e) => { setTierEdit(e.target.value); syncPrice(priceKey, e.target.value) } },
|
|
139
|
-
React.createElement('option', { value: 'off' }, '空闲'), React.createElement('option', { value: 'peak' }, '高峰'))
|
|
140
|
-
),
|
|
141
|
-
React.createElement('label', null,
|
|
142
|
-
React.createElement('span', { className: 'field-name' }, '输入/缓存/输出 ¥每百万'),
|
|
143
|
-
React.createElement('span', { style: { display: 'inline-flex', gap: 4 } },
|
|
144
|
-
React.createElement('input', { type: 'number', min: 0, step: 0.01, value: priceIn, onChange: (e) => setPriceIn(e.target.value) }),
|
|
145
|
-
React.createElement('input', { type: 'number', min: 0, step: 0.01, value: priceCache, onChange: (e) => setPriceCache(e.target.value) }),
|
|
146
|
-
React.createElement('input', { type: 'number', min: 0, step: 0.01, value: priceOut, onChange: (e) => setPriceOut(e.target.value) })
|
|
147
|
-
)
|
|
148
|
-
),
|
|
149
|
-
React.createElement('label', null,
|
|
150
|
-
React.createElement('span', { className: 'field-name' }, '美元汇率 ¥/$'),
|
|
151
|
-
React.createElement('input', { type: 'number', min: 0, step: 0.1, value: usdRate, onChange: (e) => setUsdRate(e.target.value) })
|
|
152
|
-
)
|
|
153
|
-
),
|
|
154
|
-
React.createElement('div', { className: 'actions' },
|
|
155
|
-
React.createElement('button', { className: 'primary', onClick: save }, '保存'),
|
|
156
|
-
React.createElement('button', { onClick: refresh }, '立即刷新')
|
|
157
|
-
),
|
|
158
|
-
React.createElement('span', { className: 'muted' }, '本次活跃 = 自本次插件启用以来的新花费;本会话 = 当前会话全部花费')
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function BalanceDock(props) {
|
|
163
|
-
const sessionId = props && props.sessionId
|
|
164
|
-
const [data, setData] = React.useState(null)
|
|
165
|
-
const [open, setOpen] = React.useState(false)
|
|
166
|
-
React.useEffect(() => {
|
|
167
|
-
let alive = true
|
|
168
|
-
const load = (force) => {
|
|
169
|
-
loadState(sessionId, force).then((v) => { if (alive) setData(v) }).catch(() => { if (alive) setData({ state: { status: 'error', error: '连接失败' } }) })
|
|
170
|
-
}
|
|
171
|
-
load(false)
|
|
172
|
-
const timer = window.setInterval(() => load(false), 5000)
|
|
173
|
-
return () => { alive = false; window.clearInterval(timer) }
|
|
174
|
-
}, [sessionId])
|
|
175
|
-
if (data === null) return null
|
|
176
|
-
const st = data.state || {}
|
|
177
|
-
const cfg = data.config || {}
|
|
178
|
-
const below = st.status === 'ok' && !st.external && typeof st.balance === 'number' && st.balance < (cfg.threshold ?? 10)
|
|
179
|
-
const costWarn = st.status === 'ok' && st.spendAlert === true
|
|
180
|
-
const warn = below || costWarn
|
|
181
|
-
let text
|
|
182
|
-
if (st.status === 'loading') text = '余额 …'
|
|
183
|
-
else if (st.status === 'unconfigured') text = '余额未配置'
|
|
184
|
-
else if (st.status === 'error') text = '余额获取失败'
|
|
185
|
-
else if (warn) {
|
|
186
|
-
const pieces = []
|
|
187
|
-
if (costWarn) pieces.push('本次活跃 ' + symbolOf(st.currency) + String(st.cost || 0) + '/' + symbolOf(st.currency) + String(cfg.spendThreshold ?? 1))
|
|
188
|
-
if (below) pieces.push('余额 ' + symbolOf(st.currency) + String(st.balance) + '/' + symbolOf(st.currency) + String(cfg.threshold ?? 10))
|
|
189
|
-
text = '⚠ ' + pieces.join(' · ')
|
|
190
|
-
} else {
|
|
191
|
-
const total = st.costTotal || 0
|
|
192
|
-
const active = st.cost || 0
|
|
193
|
-
const last = st.lastCost !== null && st.lastCost !== undefined ? st.lastCost : null
|
|
194
|
-
const prev = st.prevCost !== null && st.prevCost !== undefined ? st.prevCost : null
|
|
195
|
-
text = ''
|
|
196
|
-
if (!st.external) text += '余额 ' + symbolOf(st.currency) + String(st.balance) + ' · '
|
|
197
|
-
text += '本会话 ' + symbolOf(st.currency) + String(total)
|
|
198
|
-
text += ' · 本次活跃 ' + symbolOf(st.currency) + String(active)
|
|
199
|
-
if (last !== null) text += ' · 最近一次 ' + symbolOf(st.currency) + String(last)
|
|
200
|
-
if (prev !== null) text += ' · 上次对话 ' + symbolOf(st.currency) + String(prev)
|
|
201
|
-
}
|
|
202
|
-
let title = ''
|
|
203
|
-
if (st.status === 'ok') {
|
|
204
|
-
const total = st.costTotal || 0
|
|
205
|
-
const active = st.cost || 0
|
|
206
|
-
const last = st.lastCost !== null && st.lastCost !== undefined ? st.lastCost : null
|
|
207
|
-
const prev = st.prevCost !== null && st.prevCost !== undefined ? st.prevCost : null
|
|
208
|
-
title = st.external ? '当前会话为小米 MiMo 计费(不适用 DeepSeek 余额)' : 'DeepSeek 账户余额 ' + (st.currency || '') + ' · 更新于 ' + new Date(st.updatedAt || Date.now()).toLocaleTimeString()
|
|
209
|
-
if (st.tokens) title += ' · 本会话token: 输入 ' + st.tokens.input + ' · 缓存读 ' + st.tokens.cacheRead + ' · 输出 ' + st.tokens.output
|
|
210
|
-
title += ' · 本会话(' + (st.model || '默认') + ') ' + symbolOf(st.currency) + total + ' · 本次活跃 ' + symbolOf(st.currency) + active + ' (' + (st.samples || 0) + '次新请求)'
|
|
211
|
-
if (last !== null) title += ' · 最近一次 ' + symbolOf(st.currency) + last
|
|
212
|
-
if (prev !== null) title += ' · 上次对话(' + (st.prevModel || '默认') + ') ' + symbolOf(st.currency) + prev
|
|
213
|
-
if (costWarn) title += ' · ⚠ 本次活跃 ≥ ' + symbolOf(st.currency) + String(cfg.spendThreshold ?? 1)
|
|
214
|
-
if (below) title += ' · ⚠ 余额 < ' + symbolOf(st.currency) + String(cfg.threshold ?? 10)
|
|
215
|
-
} else if (st.status === 'unconfigured') {
|
|
216
|
-
title = '未找到 DEEPSEEK_API_KEY,请在 设置 → 模型 中配置'
|
|
217
|
-
} else if (st.status === 'error') {
|
|
218
|
-
title = st.error || '查询失败'
|
|
219
|
-
}
|
|
220
|
-
return React.createElement('div', { className: 'dsh-balance-dock', title },
|
|
221
|
-
React.createElement('span', {
|
|
222
|
-
className: 'dsh-balance-dock-line' + (warn ? ' warn' : ''),
|
|
223
|
-
onClick: () => setOpen(!open),
|
|
224
|
-
style: { cursor: 'pointer' },
|
|
225
|
-
}, text),
|
|
226
|
-
open ? React.createElement(BalancePanel, { data, sessionId, onChanged: setData, onClose: () => setOpen(false) }) : null
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function apply(ctx) {
|
|
231
|
-
ctx.effect(() => {
|
|
232
|
-
const style = document.createElement('style')
|
|
233
|
-
style.dataset.plugin = 'dsh-balance'
|
|
234
|
-
style.textContent = CSS
|
|
235
|
-
document.head.append(style)
|
|
236
|
-
return () => { style.remove() }
|
|
237
|
-
}, 'dsh-balance: styles')
|
|
238
|
-
|
|
239
|
-
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
240
|
-
name: 'conversation.composer.dock',
|
|
241
|
-
id: 'dsh-balance',
|
|
242
|
-
order: 10,
|
|
243
|
-
inject: () => ({ loadState, saveConfig }),
|
|
244
|
-
}, BalanceDock))
|
|
245
|
-
}
|
|
252
|
+
exports.apply = apply;
|
|
253
|
+
exports.inject = inject;
|
|
254
|
+
return module.exports;
|
|
255
|
+
},
|
|
256
|
+
});
|
package/cordis.patch.yml
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
- insert:
|
|
2
|
-
- id: dsh-balance
|
|
3
|
-
name: '@yyfather/dsh-balance'
|
|
1
|
+
- insert:
|
|
2
|
+
- id: dsh-balance
|
|
3
|
+
name: '@yyfather/dsh-balance'
|