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