dsh-dock 0.9.6 → 0.9.7

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.
Files changed (35) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +325 -325
  3. package/client.js +42 -8
  4. package/cordis.patch.yml +2 -2
  5. package/features/animation/host.js +660 -660
  6. package/features/animation/view.jsx +2247 -2247
  7. package/features/balance/host.js +494 -494
  8. package/features/balance/view.js +351 -351
  9. package/features/games/games/breakout.jsx +227 -227
  10. package/features/games/games/gomoku.jsx +244 -244
  11. package/features/games/games/racer.jsx +169 -169
  12. package/features/games/games/shared.jsx +118 -118
  13. package/features/games/games/snake.jsx +144 -144
  14. package/features/games/games/sokoban.jsx +180 -180
  15. package/features/games/games/tank.jsx +128 -128
  16. package/features/games/games/tetris.jsx +308 -308
  17. package/features/games/games/xiangqi.jsx +277 -277
  18. package/features/games/tank-core.js +152 -152
  19. package/features/games/view.jsx +639 -639
  20. package/features/games/xiangqi-core.js +299 -299
  21. package/features/heartbeat/view.js +35 -35
  22. package/features/mobile-relay/gateway.js +472 -472
  23. package/features/mobile-relay/host.js +570 -569
  24. package/features/mobile-relay/view.jsx +300 -300
  25. package/features/modelconfig/host.js +403 -403
  26. package/features/modelconfig/view.js +490 -490
  27. package/features/theme/view.js +61 -61
  28. package/features/tokenlog/host.js +678 -678
  29. package/features/tokenlog/view.jsx +561 -561
  30. package/features/visionproxy/host.js +315 -315
  31. package/index.js +175 -118
  32. package/package.json +59 -59
  33. package/src/client.jsx +668 -668
  34. package/src/host-core.js +98 -95
  35. package/src/shared.js +167 -130
@@ -1,351 +1,351 @@
1
- // dsh-dock · 功能模块【模型余额】· 客户端视图(v0.2.0 迁自 client.js,行为不变)
2
- // 数据与刷新走共享 balanceStore:首页总揽、弹层与设置页共用同一份快照(挂载即拉取,5 分钟自动刷新)
3
- import react from "react";
4
- import { openPanel } from "../../src/shared.js";
5
-
6
- // ---- 模型余额视图的配色与工具(沿用 dsh-balance-panel@0.1.1,MIT 同作者)----
7
- // 调色板按色相取「亮暗主题都可读」的中深色:亮色白底下够深(可作文字色),暗色下也够亮。
8
- // (旧调色板全是 300–400 级浅色,亮色主题下 Provider 名/模型 chips 看不清。)
9
- const ACCENT_PALETTE = ['#2563eb', '#0d9488', '#b45309', '#7c3aed', '#be185d', '#0369a1', '#c2410c', '#15803d', '#a21caf', '#0e7490'];
10
- const CURATED_ACCENTS = {
11
- 'deepseek-official': '#2563eb',
12
- deepseek: '#2563eb',
13
- 'qwen-token-plan-cn': '#b45309',
14
- fangzhou: '#7c3aed',
15
- openai: '#15803d',
16
- anthropic: '#c2410c',
17
- 'google-gemini': '#1d4ed8'
18
- };
19
- const C_TOTAL = '#0d9488';
20
- const C_GRANTED = '#0369a1';
21
- const C_TOPUP = '#b45309';
22
- const C_OK = '#0d9488';
23
- const C_ERR = '#dc2626';
24
- // 余额接口有些 Provider 仅返回美元。功能坞统一以人民币展示,汇率与用量模块默认值保持一致。
25
- const USD_CNY_RATE = 7.2;
26
-
27
- function accentOf(id) {
28
- if (CURATED_ACCENTS[id]) return CURATED_ACCENTS[id];
29
- let h = 0;
30
- const s = String(id);
31
- for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
32
- return ACCENT_PALETTE[h % ACCENT_PALETTE.length];
33
- }
34
- const fmt = (v) => (v == null || v === '' ? '—' : String(v));
35
- function cnyBalanceInfo(info) {
36
- if (!info) return info;
37
- if (info.currency === 'CNY') return Object.assign({}, info, { currency: '人民币' });
38
- if (info.currency !== 'USD') return info;
39
- const convert = (value) => {
40
- if (value == null) return value;
41
- const n = Number(value);
42
- return Number.isFinite(n) ? n * USD_CNY_RATE : value;
43
- };
44
- return Object.assign({}, info, {
45
- currency: '人民币(估算)',
46
- totalBalance: convert(info.totalBalance),
47
- grantedBalance: convert(info.grantedBalance),
48
- toppedUpBalance: convert(info.toppedUpBalance),
49
- });
50
- }
51
-
52
- // ---- 共享快照:弹层/设置页/首页总揽共用一份数据与刷新 ----
53
- const balanceStore = {
54
- snap: { data: null, loading: false, error: null },
55
- listeners: new Set(),
56
- subscribe(fn) { this.listeners.add(fn); return () => { this.listeners.delete(fn); }; },
57
- set(patch) { this.snap = Object.assign({}, this.snap, patch); for (const fn of this.listeners) fn(); },
58
- load() {
59
- if (this.snap.loading) return;
60
- this.set({ loading: true });
61
- fetch("/dsh-dock/balance", { signal: AbortSignal.timeout(20000) })
62
- .then((res) => (res.ok ? res.json() : Promise.reject(new Error("余额接口 HTTP " + res.status))))
63
- .then((data) => this.set({ data: data, loading: false, error: null }))
64
- .catch((e) => this.set({ loading: false, error: (e && e.message) || String(e) }));
65
- }
66
- };
67
- function useBalance(ctx) {
68
- const [snap, setSnap] = react.useState(balanceStore.snap);
69
- react.useEffect(() => {
70
- const off = balanceStore.subscribe(() => setSnap(balanceStore.snap));
71
- if (!balanceStore.snap.data && !balanceStore.snap.loading) balanceStore.load();
72
- const disposer = ctx && typeof ctx.interval === "function" ? ctx.interval(() => balanceStore.load(), 5 * 60 * 1000) : null;
73
- return () => { off(); if (disposer) disposer(); };
74
- }, []);
75
- return snap;
76
- }
77
-
78
- function BalanceView(props) {
79
- // 数据与刷新走共享 balanceStore(挂载即拉取,5 分钟自动刷新)
80
- // props.params.provider(chips 点击带入):滚动到并高亮该 Provider 行
81
- const snap = useBalance(props && props.ctx);
82
- const load = () => balanceStore.load();
83
- react.useEffect(() => {
84
- const pid = props && props.params && props.params.provider;
85
- if (!pid || typeof document === "undefined") return;
86
- const t = setTimeout(() => {
87
- try {
88
- const el = document.querySelector('[data-dock-provider="' + pid + '"]');
89
- if (el) {
90
- el.scrollIntoView({ block: "center", behavior: "smooth" });
91
- el.classList.remove("dkb-flash");
92
- void el.offsetWidth; // 重启动画
93
- el.classList.add("dkb-flash");
94
- }
95
- } catch { /* 高亮失败不影响视图 */ }
96
- }, 80);
97
- return () => clearTimeout(t);
98
- }, [props && props.params]);
99
-
100
- const data = snap.data;
101
- const providers = data && Array.isArray(data.providers) ? data.providers : [];
102
- const okCount = providers.filter((p) => p.balance && p.balance.status === "ok").length;
103
- const def = data && data.default ? data.default.provider : null;
104
- const body = [];
105
-
106
- if (snap.error) {
107
- body.push(react.createElement("div", { key: "err", className: "dkb-note dkb-error" }, "余额查询失败:" + snap.error));
108
- } else if (!data) {
109
- body.push(react.createElement("div", { key: "loading", className: "dkb-note" }, snap.loading ? "正在拉取余额…" : "还没有数据,点击下方刷新"));
110
- } else if (providers.length === 0) {
111
- body.push(react.createElement("div", { key: "empty", className: "dkb-note" }, "没有找到已配置的模型 Provider"));
112
- } else {
113
- // Provider 分组:可查余额的排前面(不可查/需登录的往后)
114
- const rank = (p) => (p.balance && p.balance.status === "ok" ? 0 : p.balance && p.balance.status === "login-required" ? 1 : 2);
115
- const sorted = providers.slice().sort((a, b) => rank(a) - rank(b));
116
- body.push(react.createElement("div", { key: "rows", className: "dkb-rows" },
117
- sorted.map((p) => {
118
- const accent = accentOf(p.id);
119
- const b = p.balance;
120
- let badge = ["未知", ""];
121
- if (b) {
122
- if (b.status === "ok") badge = ["可用", "ok"];
123
- else if (b.status === "unsupported") badge = ["不支持", ""];
124
- else if (b.status === "no-credential") badge = ["未配置密钥", "warn"];
125
- else if (b.status === "login-required") badge = ["需登录", "warn"];
126
- else badge = ["查询失败", "err"];
127
- }
128
- // 主体内容按状态分三型:余额(数字大卡)/ 登录跳转 / 说明
129
- let balBody = null;
130
- if (b && b.status === "ok" && b.kind === "quota") {
131
- const dims = Array.isArray(b.dims) ? b.dims : [];
132
- balBody = react.createElement(react.Fragment, null,
133
- react.createElement("div", { className: "dkb-main" },
134
- react.createElement("span", { className: "dkb-main-label" }, "剩余额度"),
135
- react.createElement("span", { className: "dkb-main-value" }, fmt(b.remaining) + (b.unit ? " " + b.unit : "")),
136
- react.createElement("span", { className: "dkb-main-part" }, "已用 " + fmt(b.used) + " / 总 " + fmt(b.limit),
137
- b.resetTime ? react.createElement("span", null, " · 重置 " + String(b.resetTime).slice(0, 10)) : null)),
138
- dims.map((d, i) => react.createElement("div", { key: "d" + i, className: "dkb-dim" },
139
- react.createElement("span", { className: "dkb-dim-label" }, d.window === "weekly" ? "周额度" : "小时额度"),
140
- react.createElement("span", { className: "dkb-dim-bar" },
141
- react.createElement("span", { className: "dkb-dim-fill", style: { width: (d.limit > 0 ? Math.min(100, Math.round((d.remaining / d.limit) * 100)) : 0) + "%" } })),
142
- react.createElement("span", { className: "dkb-dim-text" }, "剩 " + fmt(d.remaining) + " / " + fmt(d.limit)))));
143
- } else if (b && b.status === "ok") {
144
- const infos = Array.isArray(b.infos) ? b.infos : [];
145
- balBody = react.createElement("div", { className: "dkb-mains" },
146
- infos.map((rawInfo, idx) => {
147
- const i = cnyBalanceInfo(rawInfo);
148
- return react.createElement("div", { key: idx, className: "dkb-main" },
149
- react.createElement("span", { className: "dkb-main-label", style: { color: accent } }, i.currency),
150
- react.createElement("span", { className: "dkb-main-value" }, fmt(i.totalBalance)),
151
- react.createElement("span", { className: "dkb-main-parts" },
152
- i.grantedBalance != null ? react.createElement("span", { className: "dkb-main-part", style: { color: C_GRANTED } }, "赠送 " + fmt(i.grantedBalance)) : null,
153
- i.toppedUpBalance != null ? react.createElement("span", { className: "dkb-main-part", style: { color: C_TOPUP } }, "充值 " + fmt(i.toppedUpBalance)) : null));
154
- }));
155
- } else if (b && b.status === "login-required" && b.consoleUrl) {
156
- balBody = react.createElement("div", { className: "dkb-main" },
157
- react.createElement("a", { href: b.consoleUrl, target: "_blank", rel: "noreferrer", className: "dkb-link", style: { color: accent } }, "去控制台查看余额 →"));
158
- } else {
159
- balBody = b && b.message
160
- ? react.createElement("div", { className: "dkb-note" }, b.message)
161
- : react.createElement("div", { className: "dkb-note" }, "该 Provider 没有已知的余额查询接口");
162
- }
163
- // 长连接信息弱化成单行小字(ID · api · baseURL),模型 chips 收敛到折叠区
164
- const sub = ["ID " + p.id, p.api, p.baseURL ? p.baseURL.replace(/^https?:\/\//, "") : null].filter(Boolean).join(" · ");
165
- return react.createElement("div", { key: p.id, className: "dkb-row", "data-dock-provider": p.id },
166
- react.createElement("div", { className: "dkb-row-head" },
167
- react.createElement("span", { className: "dkb-dot", style: { background: accent } }),
168
- react.createElement("span", { className: "dkb-name", style: { color: accent } }, p.displayName),
169
- def === p.id ? react.createElement("span", { className: "dkb-default" }, "默认") : null,
170
- react.createElement("span", { className: "dkb-badge" + (badge[1] ? " " + badge[1] : ""), style: badge[1] === "err" ? { color: C_ERR, borderColor: C_ERR } : null }, badge[0]),
171
- // 模型数折叠提示(点开展开 chips)
172
- p.models && p.models.length > 0
173
- ? react.createElement("button", {
174
- type: "button", className: "dkb-models-toggle",
175
- title: p.models.join(", "),
176
- onClick: (e) => {
177
- const row = e.currentTarget.closest(".dkb-row");
178
- if (row) row.classList.toggle("dkb-models-open");
179
- }
180
- }, p.models.length + " 个模型 ▾")
181
- : null),
182
- sub ? react.createElement("div", { className: "dkb-sub" }, sub) : null,
183
- p.apiKeyEnv
184
- ? react.createElement("div", { className: "dkb-sub" }, "密钥: " + p.apiKeyEnv + (p.credentialConfigured ? " ✓" : " ✗"))
185
- : null,
186
- p.models && p.models.length > 0
187
- ? react.createElement("div", { className: "dkb-chips" },
188
- p.models.slice(0, 8).map((m) =>
189
- react.createElement("span", { key: m, className: "dkb-chip", style: { border: "1px solid " + accent, color: accent, background: accent + "1a" } }, m)),
190
- p.models.length > 8 ? react.createElement("span", { key: "more", className: "dkb-chip" }, "+" + (p.models.length - 8)) : null)
191
- : null,
192
- balBody);
193
- })));
194
- }
195
-
196
- const now = new Date(data && data.generatedAt ? data.generatedAt : Date.now());
197
- return react.createElement("div", { className: "dkb-rows" },
198
- react.createElement("div", { className: "dkb-note" },
199
- data && !snap.error && providers.length > 0
200
- ? okCount + "/" + providers.length + " 个 Provider 可查余额(可查的排前面)"
201
- : "模型余额 · Host 半部实时拉取"),
202
- body,
203
- react.createElement("div", { className: "dkb-foot" },
204
- react.createElement("span", null, snap.loading ? "刷新中…" : "更新于 " + now.toLocaleTimeString()),
205
- react.createElement("button", { type: "button", className: "dkb-refresh", onClick: load }, "刷新")));
206
- }
207
-
208
- function BalanceStat(props) {
209
- const snap = useBalance(props && props.ctx);
210
- const data = snap.data;
211
- const providers = data && Array.isArray(data.providers) ? data.providers : [];
212
- if (snap.error) return react.createElement("span", { className: "dockm-err" }, "余额查询失败(点击进入查看详情)");
213
- if (!data) return react.createElement("span", null, snap.loading ? "正在拉取余额…" : "等待拉取余额");
214
- const ok = providers.filter((p) => p.balance && p.balance.status === "ok").length;
215
- return react.createElement("span", null, providers.length + " 个 Provider · " + ok + " 个可查余额");
216
- }
217
-
218
- // ---- 会话输入区 chip:显示当前选中 Provider 的账户余额 ----
219
- // 当前选中以会话级模型选择为准(client 服务 modelDirectories:选择器切换即时推送),
220
- // 无该服务时回退余额快照里的默认选中(agentDefaultModel)。
221
- // 点击打开功能坞并定位到模型余额页、高亮该 Provider 行。
222
- function useCurrentProvider(ctx, sessionId) {
223
- const [cur, setCur] = react.useState(null);
224
- react.useEffect(() => {
225
- let alive = true, off = null;
226
- try {
227
- const models = ctx && ctx.get ? ctx.get("modelDirectories") : undefined;
228
- if (models && typeof models.directoryFor === "function" && sessionId) {
229
- const dir = models.directoryFor(sessionId);
230
- const face = dir && typeof dir.subscribe === "function" ? dir : (dir && dir.store);
231
- const read = () => {
232
- if (!alive) return;
233
- try {
234
- const snap = face && typeof face.getSnapshot === "function" ? face.getSnapshot() : null;
235
- setCur(snap && snap.current ? snap.current : null);
236
- } catch { /* 快照读取失败保持旧值 */ }
237
- };
238
- if (face && typeof face.subscribe === "function") {
239
- read();
240
- off = face.subscribe(read);
241
- }
242
- }
243
- } catch { /* 服务不可用回退默认 */ }
244
- return () => { alive = false; if (typeof off === "function") off(); };
245
- }, [sessionId]);
246
- return cur;
247
- }
248
- function chipBalanceText(p) {
249
- const b = p && p.balance;
250
- if (!b) return "余额 …";
251
- if (b.status !== "ok") {
252
- if (b.status === "login-required") return "余额·需登录";
253
- if (b.status === "no-credential") return "余额·无密钥";
254
- if (b.status === "unsupported") return "余额·不支持";
255
- return "余额·查询失败";
256
- }
257
- if (b.kind === "quota") {
258
- const v = Number(b.remaining);
259
- const s = Number.isFinite(v)
260
- ? (v >= 10000 ? (v / 10000).toFixed(1) + "万" : String(Math.round(v)))
261
- : fmt(b.remaining);
262
- return "余额 剩 " + s + (b.unit ? " " + b.unit : "");
263
- }
264
- const info = cnyBalanceInfo(Array.isArray(b.infos) && b.infos[0] ? b.infos[0] : null);
265
- if (!info) return "余额 …";
266
- const cur = info.currency === "CNY" || info.currency === "人民币(估算)" ? "¥" : "";
267
- const v = Number(info.totalBalance);
268
- const s = Number.isFinite(v) ? (v >= 10000 ? (v / 10000).toFixed(2) + "万" : String(Math.round(v * 100) / 100)) : fmt(info.totalBalance);
269
- return "余额 " + cur + s;
270
- }
271
- function BalanceChip(props) {
272
- const snap = useBalance(props && props.ctx);
273
- const data = snap.data;
274
- const providers = data && Array.isArray(data.providers) ? data.providers : [];
275
- // 会话级当前选中(模型选择器切换即时推送);无该服务时回退余额快照里的默认选中
276
- const sel = useCurrentProvider(props && props.ctx, props && props.sessionId);
277
- const selProvider = sel && (sel.provider || (sel.selected && sel.selected.provider)) || null;
278
- const def = selProvider || (data && data.default ? data.default.provider : null);
279
- const cur = def ? providers.find((p) => p.id === def) : null;
280
- const accent = cur ? accentOf(cur.id) : "#64748b";
281
- const text = snap.error ? "余额·失败" : (!data ? (snap.loading ? "余额 …" : "余额 —") : (cur ? chipBalanceText(cur) : "余额 —"));
282
- const selModel = sel && (sel.model || (sel.selected && sel.selected.model)) || null;
283
- const title = cur
284
- ? (cur.displayName || cur.id) + (selModel ? " · 当前模型 " + selModel : "") + " · 点击在功能坞查看余额详情"
285
- : "点击打开功能坞 · 模型余额";
286
- return react.createElement("button", {
287
- type: "button",
288
- className: "dockchip" + (snap.error || (cur && cur.balance && cur.balance.status === "error") ? " err" : ""),
289
- title: title,
290
- "aria-label": "模型余额",
291
- onClick: () => openPanel("balance", def ? { provider: def } : null),
292
- },
293
- react.createElement("span", { className: "dockchip-dot", style: { background: accent } }),
294
- react.createElement("span", null, text));
295
- }
296
-
297
- export const feature = {
298
- id: "balance",
299
- name: "模型余额",
300
- order: 120,
301
- accent: "#4d9fff",
302
- description: "展示所有模型 Provider 账户余额(Host 拉取,5 分钟自动刷新)",
303
- css: [
304
- ".dkb-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
305
- ".dkb-error{color:var(--dsw-alias-state-error-primary);}",
306
- ".dkb-rows{display:flex;flex-direction:column;gap:10px;}",
307
- // Provider 卡片:行距收窄、留白分层
308
- ".dkb-row{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 14px 12px;display:flex;flex-direction:column;gap:6px;}",
309
- ".dkb-row-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
310
- ".dkb-dot{width:8px;height:8px;border-radius:50%;flex:none;}",
311
- ".dkb-name{font-weight:600;font-size:13px;color:var(--dsw-alias-label-primary);}",
312
- ".dkb-default{color:var(--dk-accent);font-size:11px;border:1px solid currentColor;border-radius:999px;padding:0 6px;}",
313
- ".dkb-badge{font-size:11px;border-radius:999px;padding:1px 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
314
- ".dkb-badge.ok{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
315
- ".dkb-badge.warn{color:var(--dk-warn);border-color:currentColor;}",
316
- ".dkb-badge.err{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
317
- // 模型数折叠开关(点行头右侧"N 个模型 ▾"展开/收起 chips)
318
- ".dkb-models-toggle{margin-left:auto;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);font-family:inherit;font-size:11px;padding:1px 6px;border-radius:6px;white-space:nowrap;}",
319
- ".dkb-models-toggle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
320
- ".dkb-row:not(.dkb-models-open) .dkb-chips{display:none;}",
321
- ".dkb-row.dkb-models-open .dkb-models-toggle::after{content:\"\";}",
322
- // 连接信息弱化单行(省略 URL 协议头、超出省略号)
323
- ".dkb-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
324
- ".dkb-chips{display:flex;flex-wrap:wrap;gap:4px;padding:2px 0 4px;}",
325
- ".dkb-chip{font-size:10px;border-radius:6px;padding:1px 6px;}",
326
- // 余额主数字卡(大号数字 + 小字辅助信息;多币种横排多卡)
327
- ".dkb-mains{display:flex;gap:12px;flex-wrap:wrap;}",
328
- ".dkb-main{display:flex;flex-direction:column;gap:2px;padding:8px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);min-width:150px;}",
329
- ".dkb-main-label{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
330
- ".dkb-main-value{font-size:20px;font-weight:700;color:var(--dsw-alias-state-success-primary);font-variant-numeric:tabular-nums;line-height:1.2;}",
331
- ".dkb-main-parts{display:flex;gap:10px;flex-wrap:wrap;}",
332
- ".dkb-main-part{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
333
- // 配额维度条(周/小时额度:进度条 + 剩余/总量)
334
- ".dkb-dim{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--dsw-alias-label-secondary);}",
335
- ".dkb-dim-label{flex:none;min-width:44px;color:var(--dsw-alias-label-tertiary);}",
336
- ".dkb-dim-bar{flex:1;max-width:220px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow:hidden;}",
337
- ".dkb-dim-fill{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,var(--dsw-alias-state-success-primary),color-mix(in srgb,var(--dsw-alias-state-success-primary) 60%,#fff));}",
338
- ".dkb-dim-text{flex:none;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;}",
339
- ".dkb-link{font-size:12px;text-decoration:none;}",
340
- ".dkb-link:hover{text-decoration:underline;}",
341
- ".dkb-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;}",
342
- ".dkb-refresh{cursor:pointer;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:2px 10px;font-family:inherit;font-size:12px;}",
343
- ".dkb-refresh:hover{background:var(--dsw-alias-interactive-bg-hover);}",
344
- // chips 点击定位后的行高亮闪烁
345
- ".dkb-row.dkb-flash{animation:dkb-flash 1.8s var(--ds-ease-in-out);}",
346
- "@keyframes dkb-flash{0%,55%{border-color:var(--dk-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--dk-accent) 25%,transparent);}100%{border-color:var(--dsw-alias-border-l1);box-shadow:none;}}"
347
- ].join("\n"),
348
- View: BalanceView,
349
- HomeStat: BalanceStat,
350
- Chip: BalanceChip,
351
- };
1
+ // dsh-dock · 功能模块【模型余额】· 客户端视图(v0.2.0 迁自 client.js,行为不变)
2
+ // 数据与刷新走共享 balanceStore:首页总揽、弹层与设置页共用同一份快照(挂载即拉取,5 分钟自动刷新)
3
+ import react from "react";
4
+ import { openPanel } from "../../src/shared.js";
5
+
6
+ // ---- 模型余额视图的配色与工具(沿用 dsh-balance-panel@0.1.1,MIT 同作者)----
7
+ // 调色板按色相取「亮暗主题都可读」的中深色:亮色白底下够深(可作文字色),暗色下也够亮。
8
+ // (旧调色板全是 300–400 级浅色,亮色主题下 Provider 名/模型 chips 看不清。)
9
+ const ACCENT_PALETTE = ['#2563eb', '#0d9488', '#b45309', '#7c3aed', '#be185d', '#0369a1', '#c2410c', '#15803d', '#a21caf', '#0e7490'];
10
+ const CURATED_ACCENTS = {
11
+ 'deepseek-official': '#2563eb',
12
+ deepseek: '#2563eb',
13
+ 'qwen-token-plan-cn': '#b45309',
14
+ fangzhou: '#7c3aed',
15
+ openai: '#15803d',
16
+ anthropic: '#c2410c',
17
+ 'google-gemini': '#1d4ed8'
18
+ };
19
+ const C_TOTAL = '#0d9488';
20
+ const C_GRANTED = '#0369a1';
21
+ const C_TOPUP = '#b45309';
22
+ const C_OK = '#0d9488';
23
+ const C_ERR = '#dc2626';
24
+ // 余额接口有些 Provider 仅返回美元。功能坞统一以人民币展示,汇率与用量模块默认值保持一致。
25
+ const USD_CNY_RATE = 7.2;
26
+
27
+ function accentOf(id) {
28
+ if (CURATED_ACCENTS[id]) return CURATED_ACCENTS[id];
29
+ let h = 0;
30
+ const s = String(id);
31
+ for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
32
+ return ACCENT_PALETTE[h % ACCENT_PALETTE.length];
33
+ }
34
+ const fmt = (v) => (v == null || v === '' ? '—' : String(v));
35
+ function cnyBalanceInfo(info) {
36
+ if (!info) return info;
37
+ if (info.currency === 'CNY') return Object.assign({}, info, { currency: '人民币' });
38
+ if (info.currency !== 'USD') return info;
39
+ const convert = (value) => {
40
+ if (value == null) return value;
41
+ const n = Number(value);
42
+ return Number.isFinite(n) ? n * USD_CNY_RATE : value;
43
+ };
44
+ return Object.assign({}, info, {
45
+ currency: '人民币(估算)',
46
+ totalBalance: convert(info.totalBalance),
47
+ grantedBalance: convert(info.grantedBalance),
48
+ toppedUpBalance: convert(info.toppedUpBalance),
49
+ });
50
+ }
51
+
52
+ // ---- 共享快照:弹层/设置页/首页总揽共用一份数据与刷新 ----
53
+ const balanceStore = {
54
+ snap: { data: null, loading: false, error: null },
55
+ listeners: new Set(),
56
+ subscribe(fn) { this.listeners.add(fn); return () => { this.listeners.delete(fn); }; },
57
+ set(patch) { this.snap = Object.assign({}, this.snap, patch); for (const fn of this.listeners) fn(); },
58
+ load() {
59
+ if (this.snap.loading) return;
60
+ this.set({ loading: true });
61
+ fetch("/dsh-dock/balance", { signal: AbortSignal.timeout(20000) })
62
+ .then((res) => (res.ok ? res.json() : Promise.reject(new Error("余额接口 HTTP " + res.status))))
63
+ .then((data) => this.set({ data: data, loading: false, error: null }))
64
+ .catch((e) => this.set({ loading: false, error: (e && e.message) || String(e) }));
65
+ }
66
+ };
67
+ function useBalance(ctx) {
68
+ const [snap, setSnap] = react.useState(balanceStore.snap);
69
+ react.useEffect(() => {
70
+ const off = balanceStore.subscribe(() => setSnap(balanceStore.snap));
71
+ if (!balanceStore.snap.data && !balanceStore.snap.loading) balanceStore.load();
72
+ const disposer = ctx && typeof ctx.interval === "function" ? ctx.interval(() => balanceStore.load(), 5 * 60 * 1000) : null;
73
+ return () => { off(); if (disposer) disposer(); };
74
+ }, []);
75
+ return snap;
76
+ }
77
+
78
+ function BalanceView(props) {
79
+ // 数据与刷新走共享 balanceStore(挂载即拉取,5 分钟自动刷新)
80
+ // props.params.provider(chips 点击带入):滚动到并高亮该 Provider 行
81
+ const snap = useBalance(props && props.ctx);
82
+ const load = () => balanceStore.load();
83
+ react.useEffect(() => {
84
+ const pid = props && props.params && props.params.provider;
85
+ if (!pid || typeof document === "undefined") return;
86
+ const t = setTimeout(() => {
87
+ try {
88
+ const el = document.querySelector('[data-dock-provider="' + pid + '"]');
89
+ if (el) {
90
+ el.scrollIntoView({ block: "center", behavior: "smooth" });
91
+ el.classList.remove("dkb-flash");
92
+ void el.offsetWidth; // 重启动画
93
+ el.classList.add("dkb-flash");
94
+ }
95
+ } catch { /* 高亮失败不影响视图 */ }
96
+ }, 80);
97
+ return () => clearTimeout(t);
98
+ }, [props && props.params]);
99
+
100
+ const data = snap.data;
101
+ const providers = data && Array.isArray(data.providers) ? data.providers : [];
102
+ const okCount = providers.filter((p) => p.balance && p.balance.status === "ok").length;
103
+ const def = data && data.default ? data.default.provider : null;
104
+ const body = [];
105
+
106
+ if (snap.error) {
107
+ body.push(react.createElement("div", { key: "err", className: "dkb-note dkb-error" }, "余额查询失败:" + snap.error));
108
+ } else if (!data) {
109
+ body.push(react.createElement("div", { key: "loading", className: "dkb-note" }, snap.loading ? "正在拉取余额…" : "还没有数据,点击下方刷新"));
110
+ } else if (providers.length === 0) {
111
+ body.push(react.createElement("div", { key: "empty", className: "dkb-note" }, "没有找到已配置的模型 Provider"));
112
+ } else {
113
+ // Provider 分组:可查余额的排前面(不可查/需登录的往后)
114
+ const rank = (p) => (p.balance && p.balance.status === "ok" ? 0 : p.balance && p.balance.status === "login-required" ? 1 : 2);
115
+ const sorted = providers.slice().sort((a, b) => rank(a) - rank(b));
116
+ body.push(react.createElement("div", { key: "rows", className: "dkb-rows" },
117
+ sorted.map((p) => {
118
+ const accent = accentOf(p.id);
119
+ const b = p.balance;
120
+ let badge = ["未知", ""];
121
+ if (b) {
122
+ if (b.status === "ok") badge = ["可用", "ok"];
123
+ else if (b.status === "unsupported") badge = ["不支持", ""];
124
+ else if (b.status === "no-credential") badge = ["未配置密钥", "warn"];
125
+ else if (b.status === "login-required") badge = ["需登录", "warn"];
126
+ else badge = ["查询失败", "err"];
127
+ }
128
+ // 主体内容按状态分三型:余额(数字大卡)/ 登录跳转 / 说明
129
+ let balBody = null;
130
+ if (b && b.status === "ok" && b.kind === "quota") {
131
+ const dims = Array.isArray(b.dims) ? b.dims : [];
132
+ balBody = react.createElement(react.Fragment, null,
133
+ react.createElement("div", { className: "dkb-main" },
134
+ react.createElement("span", { className: "dkb-main-label" }, "剩余额度"),
135
+ react.createElement("span", { className: "dkb-main-value" }, fmt(b.remaining) + (b.unit ? " " + b.unit : "")),
136
+ react.createElement("span", { className: "dkb-main-part" }, "已用 " + fmt(b.used) + " / 总 " + fmt(b.limit),
137
+ b.resetTime ? react.createElement("span", null, " · 重置 " + String(b.resetTime).slice(0, 10)) : null)),
138
+ dims.map((d, i) => react.createElement("div", { key: "d" + i, className: "dkb-dim" },
139
+ react.createElement("span", { className: "dkb-dim-label" }, d.window === "weekly" ? "周额度" : "小时额度"),
140
+ react.createElement("span", { className: "dkb-dim-bar" },
141
+ react.createElement("span", { className: "dkb-dim-fill", style: { width: (d.limit > 0 ? Math.min(100, Math.round((d.remaining / d.limit) * 100)) : 0) + "%" } })),
142
+ react.createElement("span", { className: "dkb-dim-text" }, "剩 " + fmt(d.remaining) + " / " + fmt(d.limit)))));
143
+ } else if (b && b.status === "ok") {
144
+ const infos = Array.isArray(b.infos) ? b.infos : [];
145
+ balBody = react.createElement("div", { className: "dkb-mains" },
146
+ infos.map((rawInfo, idx) => {
147
+ const i = cnyBalanceInfo(rawInfo);
148
+ return react.createElement("div", { key: idx, className: "dkb-main" },
149
+ react.createElement("span", { className: "dkb-main-label", style: { color: accent } }, i.currency),
150
+ react.createElement("span", { className: "dkb-main-value" }, fmt(i.totalBalance)),
151
+ react.createElement("span", { className: "dkb-main-parts" },
152
+ i.grantedBalance != null ? react.createElement("span", { className: "dkb-main-part", style: { color: C_GRANTED } }, "赠送 " + fmt(i.grantedBalance)) : null,
153
+ i.toppedUpBalance != null ? react.createElement("span", { className: "dkb-main-part", style: { color: C_TOPUP } }, "充值 " + fmt(i.toppedUpBalance)) : null));
154
+ }));
155
+ } else if (b && b.status === "login-required" && b.consoleUrl) {
156
+ balBody = react.createElement("div", { className: "dkb-main" },
157
+ react.createElement("a", { href: b.consoleUrl, target: "_blank", rel: "noreferrer", className: "dkb-link", style: { color: accent } }, "去控制台查看余额 →"));
158
+ } else {
159
+ balBody = b && b.message
160
+ ? react.createElement("div", { className: "dkb-note" }, b.message)
161
+ : react.createElement("div", { className: "dkb-note" }, "该 Provider 没有已知的余额查询接口");
162
+ }
163
+ // 长连接信息弱化成单行小字(ID · api · baseURL),模型 chips 收敛到折叠区
164
+ const sub = ["ID " + p.id, p.api, p.baseURL ? p.baseURL.replace(/^https?:\/\//, "") : null].filter(Boolean).join(" · ");
165
+ return react.createElement("div", { key: p.id, className: "dkb-row", "data-dock-provider": p.id },
166
+ react.createElement("div", { className: "dkb-row-head" },
167
+ react.createElement("span", { className: "dkb-dot", style: { background: accent } }),
168
+ react.createElement("span", { className: "dkb-name", style: { color: accent } }, p.displayName),
169
+ def === p.id ? react.createElement("span", { className: "dkb-default" }, "默认") : null,
170
+ react.createElement("span", { className: "dkb-badge" + (badge[1] ? " " + badge[1] : ""), style: badge[1] === "err" ? { color: C_ERR, borderColor: C_ERR } : null }, badge[0]),
171
+ // 模型数折叠提示(点开展开 chips)
172
+ p.models && p.models.length > 0
173
+ ? react.createElement("button", {
174
+ type: "button", className: "dkb-models-toggle",
175
+ title: p.models.join(", "),
176
+ onClick: (e) => {
177
+ const row = e.currentTarget.closest(".dkb-row");
178
+ if (row) row.classList.toggle("dkb-models-open");
179
+ }
180
+ }, p.models.length + " 个模型 ▾")
181
+ : null),
182
+ sub ? react.createElement("div", { className: "dkb-sub" }, sub) : null,
183
+ p.apiKeyEnv
184
+ ? react.createElement("div", { className: "dkb-sub" }, "密钥: " + p.apiKeyEnv + (p.credentialConfigured ? " ✓" : " ✗"))
185
+ : null,
186
+ p.models && p.models.length > 0
187
+ ? react.createElement("div", { className: "dkb-chips" },
188
+ p.models.slice(0, 8).map((m) =>
189
+ react.createElement("span", { key: m, className: "dkb-chip", style: { border: "1px solid " + accent, color: accent, background: accent + "1a" } }, m)),
190
+ p.models.length > 8 ? react.createElement("span", { key: "more", className: "dkb-chip" }, "+" + (p.models.length - 8)) : null)
191
+ : null,
192
+ balBody);
193
+ })));
194
+ }
195
+
196
+ const now = new Date(data && data.generatedAt ? data.generatedAt : Date.now());
197
+ return react.createElement("div", { className: "dkb-rows" },
198
+ react.createElement("div", { className: "dkb-note" },
199
+ data && !snap.error && providers.length > 0
200
+ ? okCount + "/" + providers.length + " 个 Provider 可查余额(可查的排前面)"
201
+ : "模型余额 · Host 半部实时拉取"),
202
+ body,
203
+ react.createElement("div", { className: "dkb-foot" },
204
+ react.createElement("span", null, snap.loading ? "刷新中…" : "更新于 " + now.toLocaleTimeString()),
205
+ react.createElement("button", { type: "button", className: "dkb-refresh", onClick: load }, "刷新")));
206
+ }
207
+
208
+ function BalanceStat(props) {
209
+ const snap = useBalance(props && props.ctx);
210
+ const data = snap.data;
211
+ const providers = data && Array.isArray(data.providers) ? data.providers : [];
212
+ if (snap.error) return react.createElement("span", { className: "dockm-err" }, "余额查询失败(点击进入查看详情)");
213
+ if (!data) return react.createElement("span", null, snap.loading ? "正在拉取余额…" : "等待拉取余额");
214
+ const ok = providers.filter((p) => p.balance && p.balance.status === "ok").length;
215
+ return react.createElement("span", null, providers.length + " 个 Provider · " + ok + " 个可查余额");
216
+ }
217
+
218
+ // ---- 会话输入区 chip:显示当前选中 Provider 的账户余额 ----
219
+ // 当前选中以会话级模型选择为准(client 服务 modelDirectories:选择器切换即时推送),
220
+ // 无该服务时回退余额快照里的默认选中(agentDefaultModel)。
221
+ // 点击打开功能坞并定位到模型余额页、高亮该 Provider 行。
222
+ function useCurrentProvider(ctx, sessionId) {
223
+ const [cur, setCur] = react.useState(null);
224
+ react.useEffect(() => {
225
+ let alive = true, off = null;
226
+ try {
227
+ const models = ctx && ctx.get ? ctx.get("modelDirectories") : undefined;
228
+ if (models && typeof models.directoryFor === "function" && sessionId) {
229
+ const dir = models.directoryFor(sessionId);
230
+ const face = dir && typeof dir.subscribe === "function" ? dir : (dir && dir.store);
231
+ const read = () => {
232
+ if (!alive) return;
233
+ try {
234
+ const snap = face && typeof face.getSnapshot === "function" ? face.getSnapshot() : null;
235
+ setCur(snap && snap.current ? snap.current : null);
236
+ } catch { /* 快照读取失败保持旧值 */ }
237
+ };
238
+ if (face && typeof face.subscribe === "function") {
239
+ read();
240
+ off = face.subscribe(read);
241
+ }
242
+ }
243
+ } catch { /* 服务不可用回退默认 */ }
244
+ return () => { alive = false; if (typeof off === "function") off(); };
245
+ }, [sessionId]);
246
+ return cur;
247
+ }
248
+ function chipBalanceText(p) {
249
+ const b = p && p.balance;
250
+ if (!b) return "余额 …";
251
+ if (b.status !== "ok") {
252
+ if (b.status === "login-required") return "余额·需登录";
253
+ if (b.status === "no-credential") return "余额·无密钥";
254
+ if (b.status === "unsupported") return "余额·不支持";
255
+ return "余额·查询失败";
256
+ }
257
+ if (b.kind === "quota") {
258
+ const v = Number(b.remaining);
259
+ const s = Number.isFinite(v)
260
+ ? (v >= 10000 ? (v / 10000).toFixed(1) + "万" : String(Math.round(v)))
261
+ : fmt(b.remaining);
262
+ return "余额 剩 " + s + (b.unit ? " " + b.unit : "");
263
+ }
264
+ const info = cnyBalanceInfo(Array.isArray(b.infos) && b.infos[0] ? b.infos[0] : null);
265
+ if (!info) return "余额 …";
266
+ const cur = info.currency === "CNY" || info.currency === "人民币(估算)" ? "¥" : "";
267
+ const v = Number(info.totalBalance);
268
+ const s = Number.isFinite(v) ? (v >= 10000 ? (v / 10000).toFixed(2) + "万" : String(Math.round(v * 100) / 100)) : fmt(info.totalBalance);
269
+ return "余额 " + cur + s;
270
+ }
271
+ function BalanceChip(props) {
272
+ const snap = useBalance(props && props.ctx);
273
+ const data = snap.data;
274
+ const providers = data && Array.isArray(data.providers) ? data.providers : [];
275
+ // 会话级当前选中(模型选择器切换即时推送);无该服务时回退余额快照里的默认选中
276
+ const sel = useCurrentProvider(props && props.ctx, props && props.sessionId);
277
+ const selProvider = sel && (sel.provider || (sel.selected && sel.selected.provider)) || null;
278
+ const def = selProvider || (data && data.default ? data.default.provider : null);
279
+ const cur = def ? providers.find((p) => p.id === def) : null;
280
+ const accent = cur ? accentOf(cur.id) : "#64748b";
281
+ const text = snap.error ? "余额·失败" : (!data ? (snap.loading ? "余额 …" : "余额 —") : (cur ? chipBalanceText(cur) : "余额 —"));
282
+ const selModel = sel && (sel.model || (sel.selected && sel.selected.model)) || null;
283
+ const title = cur
284
+ ? (cur.displayName || cur.id) + (selModel ? " · 当前模型 " + selModel : "") + " · 点击在功能坞查看余额详情"
285
+ : "点击打开功能坞 · 模型余额";
286
+ return react.createElement("button", {
287
+ type: "button",
288
+ className: "dockchip" + (snap.error || (cur && cur.balance && cur.balance.status === "error") ? " err" : ""),
289
+ title: title,
290
+ "aria-label": "模型余额",
291
+ onClick: () => openPanel("balance", def ? { provider: def } : null),
292
+ },
293
+ react.createElement("span", { className: "dockchip-dot", style: { background: accent } }),
294
+ react.createElement("span", null, text));
295
+ }
296
+
297
+ export const feature = {
298
+ id: "balance",
299
+ name: "模型余额",
300
+ order: 120,
301
+ accent: "#4d9fff",
302
+ description: "展示所有模型 Provider 账户余额(Host 拉取,5 分钟自动刷新)",
303
+ css: [
304
+ ".dkb-note{color:var(--dsw-alias-label-secondary);font-size:12px;}",
305
+ ".dkb-error{color:var(--dsw-alias-state-error-primary);}",
306
+ ".dkb-rows{display:flex;flex-direction:column;gap:10px;}",
307
+ // Provider 卡片:行距收窄、留白分层
308
+ ".dkb-row{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:10px 14px 12px;display:flex;flex-direction:column;gap:6px;}",
309
+ ".dkb-row-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
310
+ ".dkb-dot{width:8px;height:8px;border-radius:50%;flex:none;}",
311
+ ".dkb-name{font-weight:600;font-size:13px;color:var(--dsw-alias-label-primary);}",
312
+ ".dkb-default{color:var(--dk-accent);font-size:11px;border:1px solid currentColor;border-radius:999px;padding:0 6px;}",
313
+ ".dkb-badge{font-size:11px;border-radius:999px;padding:1px 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
314
+ ".dkb-badge.ok{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
315
+ ".dkb-badge.warn{color:var(--dk-warn);border-color:currentColor;}",
316
+ ".dkb-badge.err{color:var(--dsw-alias-state-error-primary);border-color:currentColor;}",
317
+ // 模型数折叠开关(点行头右侧"N 个模型 ▾"展开/收起 chips)
318
+ ".dkb-models-toggle{margin-left:auto;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);font-family:inherit;font-size:11px;padding:1px 6px;border-radius:6px;white-space:nowrap;}",
319
+ ".dkb-models-toggle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
320
+ ".dkb-row:not(.dkb-models-open) .dkb-chips{display:none;}",
321
+ ".dkb-row.dkb-models-open .dkb-models-toggle::after{content:\"\";}",
322
+ // 连接信息弱化单行(省略 URL 协议头、超出省略号)
323
+ ".dkb-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
324
+ ".dkb-chips{display:flex;flex-wrap:wrap;gap:4px;padding:2px 0 4px;}",
325
+ ".dkb-chip{font-size:10px;border-radius:6px;padding:1px 6px;}",
326
+ // 余额主数字卡(大号数字 + 小字辅助信息;多币种横排多卡)
327
+ ".dkb-mains{display:flex;gap:12px;flex-wrap:wrap;}",
328
+ ".dkb-main{display:flex;flex-direction:column;gap:2px;padding:8px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);min-width:150px;}",
329
+ ".dkb-main-label{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
330
+ ".dkb-main-value{font-size:20px;font-weight:700;color:var(--dsw-alias-state-success-primary);font-variant-numeric:tabular-nums;line-height:1.2;}",
331
+ ".dkb-main-parts{display:flex;gap:10px;flex-wrap:wrap;}",
332
+ ".dkb-main-part{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
333
+ // 配额维度条(周/小时额度:进度条 + 剩余/总量)
334
+ ".dkb-dim{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--dsw-alias-label-secondary);}",
335
+ ".dkb-dim-label{flex:none;min-width:44px;color:var(--dsw-alias-label-tertiary);}",
336
+ ".dkb-dim-bar{flex:1;max-width:220px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow:hidden;}",
337
+ ".dkb-dim-fill{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,var(--dsw-alias-state-success-primary),color-mix(in srgb,var(--dsw-alias-state-success-primary) 60%,#fff));}",
338
+ ".dkb-dim-text{flex:none;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;}",
339
+ ".dkb-link{font-size:12px;text-decoration:none;}",
340
+ ".dkb-link:hover{text-decoration:underline;}",
341
+ ".dkb-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;}",
342
+ ".dkb-refresh{cursor:pointer;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:2px 10px;font-family:inherit;font-size:12px;}",
343
+ ".dkb-refresh:hover{background:var(--dsw-alias-interactive-bg-hover);}",
344
+ // chips 点击定位后的行高亮闪烁
345
+ ".dkb-row.dkb-flash{animation:dkb-flash 1.8s var(--ds-ease-in-out);}",
346
+ "@keyframes dkb-flash{0%,55%{border-color:var(--dk-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--dk-accent) 25%,transparent);}100%{border-color:var(--dsw-alias-border-l1);box-shadow:none;}}"
347
+ ].join("\n"),
348
+ View: BalanceView,
349
+ HomeStat: BalanceStat,
350
+ Chip: BalanceChip,
351
+ };