@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/client.js CHANGED
@@ -1,245 +1,256 @@
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'
1
+ window.__ModuleLoader__.load({
2
+ id: "@yyfather/dsh-balance",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
8
6
 
9
- export const inject = ['slots']
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
- 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
- }
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'