@philomelcc/dsh-workbench 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/client.js +1316 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +6 -0
- package/index.js +41 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +39 -4
package/client.js
ADDED
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@philomelcc/dsh-workbench',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react');
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
|
|
7
|
+
// ---------- theme tokens (single source shared with modules) ----------
|
|
8
|
+
const tokens = {
|
|
9
|
+
success: 'var(--dsw-alias-state-success-primary)',
|
|
10
|
+
error: 'var(--dsw-alias-state-error-primary)',
|
|
11
|
+
warn: 'var(--dsw-alias-state-warn-primary)',
|
|
12
|
+
idle: 'var(--dsw-alias-state-idle-primary)',
|
|
13
|
+
brand: 'var(--dsw-alias-brand-primary)',
|
|
14
|
+
bgBase: 'var(--dsw-alias-bg-base)',
|
|
15
|
+
bgLayer1: 'var(--dsw-alias-bg-layer-1)',
|
|
16
|
+
bgLayer2: 'var(--dsw-alias-bg-layer-2)',
|
|
17
|
+
bgOverlay: 'var(--dsw-alias-bg-overlay)',
|
|
18
|
+
border1: 'var(--dsw-alias-border-l1)',
|
|
19
|
+
border2: 'var(--dsw-alias-border-l2)',
|
|
20
|
+
label: 'var(--dsw-alias-label-primary)',
|
|
21
|
+
labelSecondary: 'var(--dsw-alias-label-secondary)',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ---------- shared UI primitives ----------
|
|
25
|
+
function StatusDot(props) {
|
|
26
|
+
const ok = props.ok !== false;
|
|
27
|
+
const color = props.color || (ok ? tokens.success : tokens.error);
|
|
28
|
+
const glow = props.glow || (ok ? 'rgba(52,211,153,0.16)' : 'rgba(248,113,113,0.16)');
|
|
29
|
+
return h('span', {
|
|
30
|
+
title: props.title || (ok ? '正常' : '异常'),
|
|
31
|
+
style: {
|
|
32
|
+
display: 'inline-block',
|
|
33
|
+
width: props.size || 9,
|
|
34
|
+
height: props.size || 9,
|
|
35
|
+
borderRadius: '50%',
|
|
36
|
+
background: color,
|
|
37
|
+
boxShadow: '0 0 0 3px ' + glow,
|
|
38
|
+
flex: '0 0 auto',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function Card(props) {
|
|
44
|
+
return h('div', {
|
|
45
|
+
style: {
|
|
46
|
+
border: '1px solid ' + tokens.border1,
|
|
47
|
+
borderRadius: 12,
|
|
48
|
+
background: tokens.bgLayer1,
|
|
49
|
+
overflow: 'hidden',
|
|
50
|
+
...(props.style || {}),
|
|
51
|
+
},
|
|
52
|
+
}, props.children);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function SectionHeader(props) {
|
|
56
|
+
return h('div', {
|
|
57
|
+
style: {
|
|
58
|
+
display: 'flex',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
justifyContent: 'space-between',
|
|
61
|
+
gap: 12,
|
|
62
|
+
marginBottom: 12,
|
|
63
|
+
...(props.style || {}),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 } },
|
|
67
|
+
h('div', { style: { fontSize: 16, fontWeight: 650 } }, props.title),
|
|
68
|
+
props.subtitle ? h('div', { style: { fontSize: 12, color: tokens.labelSecondary } }, props.subtitle) : null,
|
|
69
|
+
),
|
|
70
|
+
props.actions
|
|
71
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flex: '0 0 auto' } }, props.actions)
|
|
72
|
+
: null,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function EmptyState(props) {
|
|
77
|
+
return h('div', {
|
|
78
|
+
style: {
|
|
79
|
+
padding: '28px 16px',
|
|
80
|
+
textAlign: 'center',
|
|
81
|
+
color: tokens.labelSecondary,
|
|
82
|
+
fontSize: 13,
|
|
83
|
+
lineHeight: 1.6,
|
|
84
|
+
whiteSpace: 'pre-line',
|
|
85
|
+
...(props.style || {}),
|
|
86
|
+
},
|
|
87
|
+
}, props.text || '暂无内容');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Spinner(props) {
|
|
91
|
+
return h('span', {
|
|
92
|
+
style: {
|
|
93
|
+
display: 'inline-block',
|
|
94
|
+
width: props.size || 12,
|
|
95
|
+
height: props.size || 12,
|
|
96
|
+
border: '2px solid ' + tokens.border2,
|
|
97
|
+
borderTopColor: tokens.brand,
|
|
98
|
+
borderRadius: '50%',
|
|
99
|
+
animation: 'wb-spin 0.8s linear infinite',
|
|
100
|
+
flex: '0 0 auto',
|
|
101
|
+
},
|
|
102
|
+
'aria-label': '加载中',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Button(props) {
|
|
107
|
+
const variant = props.variant || 'default';
|
|
108
|
+
const style = variant === 'primary'
|
|
109
|
+
? { background: tokens.brand, color: '#fff', border: '1px solid transparent' }
|
|
110
|
+
: variant === 'ghost'
|
|
111
|
+
? { background: 'transparent', color: tokens.labelSecondary, border: '1px solid transparent' }
|
|
112
|
+
: { background: tokens.bgLayer1, color: tokens.label, border: '1px solid ' + tokens.border2 };
|
|
113
|
+
return h('button', {
|
|
114
|
+
type: 'button',
|
|
115
|
+
disabled: props.disabled,
|
|
116
|
+
onClick: props.onClick,
|
|
117
|
+
title: props.title,
|
|
118
|
+
style: {
|
|
119
|
+
padding: props.small ? '4px 10px' : '6px 12px',
|
|
120
|
+
borderRadius: 8,
|
|
121
|
+
fontSize: props.small ? 12 : 13,
|
|
122
|
+
cursor: props.disabled ? 'not-allowed' : 'pointer',
|
|
123
|
+
opacity: props.disabled ? 0.5 : 1,
|
|
124
|
+
...style,
|
|
125
|
+
},
|
|
126
|
+
}, props.children);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function Field(props) {
|
|
130
|
+
return h('input', {
|
|
131
|
+
value: props.value,
|
|
132
|
+
defaultValue: props.defaultValue,
|
|
133
|
+
placeholder: props.placeholder,
|
|
134
|
+
onChange: props.onChange,
|
|
135
|
+
onBlur: props.onBlur,
|
|
136
|
+
onKeyDown: props.onKeyDown,
|
|
137
|
+
style: {
|
|
138
|
+
padding: '4px 8px',
|
|
139
|
+
borderRadius: 7,
|
|
140
|
+
border: '1px solid ' + tokens.border1,
|
|
141
|
+
background: tokens.bgBase,
|
|
142
|
+
color: tokens.label,
|
|
143
|
+
fontSize: props.fontSize || 12,
|
|
144
|
+
outline: 'none',
|
|
145
|
+
width: '100%',
|
|
146
|
+
boxSizing: 'border-box',
|
|
147
|
+
...(props.style || {}),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------- module registry ----------
|
|
153
|
+
// A "view" is a full workbench page: it owns a main-slot key, and the
|
|
154
|
+
// workbench switcher (the second top-bar row) shows one tab per view.
|
|
155
|
+
const registry = {
|
|
156
|
+
views: new Map(),
|
|
157
|
+
listeners: new Set(),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
function notify() {
|
|
161
|
+
registry.listeners.forEach((fn) => fn());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Captured in apply(); the tab row and the overview page both need it.
|
|
165
|
+
let layoutService = null;
|
|
166
|
+
let layoutGetter = null;
|
|
167
|
+
// Last measured position of the tab highlight plate (survives a remount).
|
|
168
|
+
let lastGlider = { left: 0, width: 0 };
|
|
169
|
+
|
|
170
|
+
function currentLayout() {
|
|
171
|
+
if (layoutService) return layoutService;
|
|
172
|
+
if (layoutGetter) {
|
|
173
|
+
try { layoutService = layoutGetter() || null; } catch (_error) { /* not ready */ }
|
|
174
|
+
}
|
|
175
|
+
return layoutService;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function openView(id) {
|
|
179
|
+
const layout = currentLayout();
|
|
180
|
+
if (!layout) return;
|
|
181
|
+
try {
|
|
182
|
+
layout.selectPanel(id);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.warn('[workbench] selectPanel failed for', id, err);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function useRegistry() {
|
|
189
|
+
const [, force] = React.useReducer((x) => x + 1, 0);
|
|
190
|
+
React.useEffect(() => {
|
|
191
|
+
const fn = () => force();
|
|
192
|
+
registry.listeners.add(fn);
|
|
193
|
+
return () => registry.listeners.delete(fn);
|
|
194
|
+
}, []);
|
|
195
|
+
return registry;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------- the public workbench API ----------
|
|
199
|
+
function createApi(ctx) {
|
|
200
|
+
const api = {
|
|
201
|
+
/**
|
|
202
|
+
* Register a workbench view: one full page, one tab in the workbench
|
|
203
|
+
* switcher. The view owns the `main` slot key `id`; pressing its tab
|
|
204
|
+
* selects that key, so the page takes over the central area.
|
|
205
|
+
* Pass `tab: false` to register the page without a tab (the shell itself
|
|
206
|
+
* uses that for its overview, which hangs off the top-bar nav instead).
|
|
207
|
+
* Returns a disposer; a module should keep it inside its own ctx.effect.
|
|
208
|
+
*/
|
|
209
|
+
registerView(opts) {
|
|
210
|
+
const { id, title, icon, component, order = 50, subtitle = '', badge, tab = true } = opts || {};
|
|
211
|
+
if (typeof id !== 'string' || id.length === 0) throw new Error('workbench.registerView: string `id` is required');
|
|
212
|
+
if (typeof component !== 'function') throw new Error('workbench.registerView: `component` is required');
|
|
213
|
+
if (id === 'conversation') throw new Error('workbench.registerView: `conversation` is reserved by DSH');
|
|
214
|
+
const label = title || id;
|
|
215
|
+
registry.views.set(id, { id, title: label, subtitle, icon, component, order, badge, tab: tab !== false });
|
|
216
|
+
notify();
|
|
217
|
+
const disposeMain = ctx.slots.inject('main', () => ctx.slots.register({
|
|
218
|
+
name: 'main', key: id,
|
|
219
|
+
}, component));
|
|
220
|
+
return () => {
|
|
221
|
+
try { disposeMain(); } catch (_error) { /* already gone */ }
|
|
222
|
+
registry.views.delete(id);
|
|
223
|
+
notify();
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/** Ask the shell to repaint the switcher (after a view badge changed). */
|
|
228
|
+
refresh() {
|
|
229
|
+
notify();
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Contract version this shell implements. Modules check it once in
|
|
234
|
+
* apply() and bail out on a mismatch instead of crashing later on a
|
|
235
|
+
* missing method. Bump only on a breaking change; see docs/SPEC.md.
|
|
236
|
+
*/
|
|
237
|
+
apiVersion: 1,
|
|
238
|
+
|
|
239
|
+
components: { StatusDot, Card, SectionHeader, EmptyState, Spinner, Button, Field },
|
|
240
|
+
tokens,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Deprecated alias kept for modules written against the old contract:
|
|
244
|
+
// a "panel" is just a view now, and the sidebar launcher is gone.
|
|
245
|
+
api.registerPanel = (opts) => api.registerView(opts);
|
|
246
|
+
return api;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------- workbench icon ----------
|
|
250
|
+
function WorkbenchIcon(props) {
|
|
251
|
+
const size = props.size || 20;
|
|
252
|
+
return h('svg', {
|
|
253
|
+
viewBox: '0 0 24 24', width: size, height: size, 'aria-hidden': true, fill: 'none',
|
|
254
|
+
},
|
|
255
|
+
h('rect', { x: 3.5, y: 3.5, width: 7, height: 7, rx: 1.6, stroke: 'currentColor', strokeWidth: 1.5 }),
|
|
256
|
+
h('rect', { x: 13.5, y: 3.5, width: 7, height: 7, rx: 1.6, stroke: 'currentColor', strokeWidth: 1.5 }),
|
|
257
|
+
h('rect', { x: 3.5, y: 13.5, width: 7, height: 7, rx: 1.6, stroke: 'currentColor', strokeWidth: 1.5 }),
|
|
258
|
+
h('rect', { x: 13.5, y: 13.5, width: 7, height: 7, rx: 1.6, stroke: 'currentColor', strokeWidth: 1.5 }),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------- overview page ----------
|
|
263
|
+
function WorkbenchOverview() {
|
|
264
|
+
const reg = useRegistry();
|
|
265
|
+
const views = Array.from(reg.views.values())
|
|
266
|
+
.filter((item) => item.tab !== false)
|
|
267
|
+
.sort((a, b) => a.order - b.order);
|
|
268
|
+
|
|
269
|
+
const viewRow = (item) => h('div', {
|
|
270
|
+
key: item.id,
|
|
271
|
+
style: {
|
|
272
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
273
|
+
padding: '10px 14px', borderBottom: '1px solid ' + tokens.border1,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
h('div', { style: { width: 22, height: 22, color: tokens.labelSecondary, flex: '0 0 auto', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
|
|
277
|
+
item.icon ? h(item.icon, { size: 18, active: false }) : null),
|
|
278
|
+
h('div', { style: { flex: '1 1 auto', minWidth: 0 } },
|
|
279
|
+
h('div', { style: { fontWeight: 550, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.title),
|
|
280
|
+
h('div', { style: { fontSize: 11, color: tokens.labelSecondary } }, '视图 · ' + item.id),
|
|
281
|
+
),
|
|
282
|
+
h(Button, {
|
|
283
|
+
small: true,
|
|
284
|
+
variant: 'ghost',
|
|
285
|
+
onClick: () => openView(item.id),
|
|
286
|
+
}, '打开'),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
return h('div', {
|
|
290
|
+
style: {
|
|
291
|
+
padding: '24px 28px', maxWidth: 920, margin: '0 auto', height: '100%',
|
|
292
|
+
boxSizing: 'border-box', overflowY: 'auto',
|
|
293
|
+
color: tokens.label, background: tokens.bgBase,
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
h('h2', { style: { margin: '0 0 4px', fontSize: 20, fontWeight: 650 } }, '工作台总览'),
|
|
297
|
+
h('div', { style: { fontSize: 13, color: tokens.labelSecondary, marginBottom: 20 } },
|
|
298
|
+
'已接入 ' + views.length + ' 个视图'),
|
|
299
|
+
|
|
300
|
+
h('div', null,
|
|
301
|
+
h(SectionHeader, { title: '视图', subtitle: '工作台切换页里的页面;每个模块对应一个页面' }),
|
|
302
|
+
h(Card, null,
|
|
303
|
+
views.length === 0
|
|
304
|
+
? h(EmptyState, { text: '还没有模块注册视图。\n模块会话按《工作台模块规范》接入后会出现在这里。' })
|
|
305
|
+
: views.map(viewRow),
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ────────────────────────── top bar (stage 1) ──────────────────────────
|
|
312
|
+
const TOPBAR_HEIGHT = 102;
|
|
313
|
+
|
|
314
|
+
async function restartDsh() {
|
|
315
|
+
try { await fetch('/wb/workbench/restart', { method: 'POST' }); } catch (_e) {}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function createBalanceStore(ctx) {
|
|
319
|
+
// `state` must be REPLACED on every write: useSyncExternalStore compares
|
|
320
|
+
// getSnapshot() results with Object.is, so mutating the same object in place
|
|
321
|
+
// makes React think nothing changed and the chip never repaints.
|
|
322
|
+
let state = { phase: 'loading', amount: null, error: null, updatedAt: 0 };
|
|
323
|
+
const listeners = new Set();
|
|
324
|
+
let inflight = null;
|
|
325
|
+
let timer = null;
|
|
326
|
+
const emit = () => listeners.forEach((fn) => fn());
|
|
327
|
+
const set = (next) => { state = { ...state, ...next }; emit(); };
|
|
328
|
+
function refresh() {
|
|
329
|
+
if (inflight) return inflight;
|
|
330
|
+
const remote = typeof ctx.get === 'function' ? ctx.get('remote') : ctx.remote;
|
|
331
|
+
const account = remote && remote.account;
|
|
332
|
+
if (!account || typeof account.getBalance !== 'function') {
|
|
333
|
+
set({ phase: 'failed', error: 'account 服务不可用' });
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const meta = { version: '1.0.0', locale: 'zh-CN', timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 };
|
|
337
|
+
inflight = account.getBalance(meta)
|
|
338
|
+
.then((result) => {
|
|
339
|
+
if (!result || result.ok !== true) throw new Error('余额读取被拒绝');
|
|
340
|
+
const value = result.value;
|
|
341
|
+
if (!value || value.status !== 'ready') throw new Error('余额不可用');
|
|
342
|
+
const wallets = Array.isArray(value.value) ? value.value : [];
|
|
343
|
+
const wallet = wallets.find((w) => w && w.currency === 'CNY') || wallets[0] || null;
|
|
344
|
+
set({ phase: 'ready', amount: wallet ? Number(wallet.balance) : null, error: null, updatedAt: Date.now() });
|
|
345
|
+
})
|
|
346
|
+
.catch((err) => {
|
|
347
|
+
set({ phase: state.phase === 'ready' ? 'stale' : 'failed', error: String((err && err.message) || err) });
|
|
348
|
+
})
|
|
349
|
+
.finally(() => { inflight = null; });
|
|
350
|
+
return inflight;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
refresh,
|
|
354
|
+
start() { refresh(); timer = setInterval(() => { if (document.hidden !== true) refresh(); }, 5000); },
|
|
355
|
+
stop() { if (timer) clearInterval(timer); timer = null; },
|
|
356
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
|
|
357
|
+
getSnapshot() { return state; },
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function createAccountStore(ctx) {
|
|
362
|
+
// Same rule as the balance store: replace the snapshot, never mutate it.
|
|
363
|
+
let state = { phase: 'loading', name: null, avatar: null, error: null };
|
|
364
|
+
const listeners = new Set();
|
|
365
|
+
let inflight = null;
|
|
366
|
+
const emit = () => listeners.forEach((fn) => fn());
|
|
367
|
+
const set = (next) => { state = { ...state, ...next }; emit(); };
|
|
368
|
+
function refresh() {
|
|
369
|
+
if (inflight) return inflight;
|
|
370
|
+
const account = ctx.remote && ctx.remote.account;
|
|
371
|
+
if (!account || typeof account.getProfile !== 'function') { set({ phase: 'failed' }); return null; }
|
|
372
|
+
const meta = { version: '1.0.0', locale: 'zh-CN', timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 };
|
|
373
|
+
inflight = account.getProfile(meta)
|
|
374
|
+
.then((result) => {
|
|
375
|
+
if (result && result.ok && result.value && result.value.status === 'ready') {
|
|
376
|
+
const p = result.value.value;
|
|
377
|
+
set({ phase: 'ready', name: p.name || p.contact || '已登录', avatar: p.avatarUrl || null, error: null });
|
|
378
|
+
} else {
|
|
379
|
+
set({ phase: 'failed', error: 'profile 不可用' });
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
.catch((err) => set({ phase: 'failed', error: String((err && err.message) || err) }))
|
|
383
|
+
.finally(() => { inflight = null; });
|
|
384
|
+
return inflight;
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
refresh,
|
|
388
|
+
start() { refresh(); },
|
|
389
|
+
stop() {},
|
|
390
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
|
|
391
|
+
getSnapshot() { return state; },
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function balanceTone(amount) {
|
|
396
|
+
if (typeof amount !== 'number' || !isFinite(amount)) return 'muted';
|
|
397
|
+
if (amount > 100) return 'green';
|
|
398
|
+
if (amount >= 10) return 'amber';
|
|
399
|
+
return 'red';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Linear icons for the top bar / workspaces.
|
|
403
|
+
const BRAND_PATH = 'M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z';
|
|
404
|
+
function IconBrand(props) {
|
|
405
|
+
const s = props.size || 20;
|
|
406
|
+
return h('svg', { viewBox: '0 0 50 50', width: s, height: s, fill: 'none', 'aria-hidden': true },
|
|
407
|
+
h('path', { d: BRAND_PATH, fill: '#4D6BFE' }),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
function IconGrid(props) {
|
|
411
|
+
const s = props.size || 18;
|
|
412
|
+
return h('svg', { viewBox: '0 0 24 24', width: s, height: s, fill: 'none', 'aria-hidden': true },
|
|
413
|
+
h('path', { d: 'M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z', stroke: 'currentColor', strokeWidth: 1.6, strokeLinejoin: 'round' }));
|
|
414
|
+
}
|
|
415
|
+
function IconPlugins(props) {
|
|
416
|
+
const s = props.size || 18;
|
|
417
|
+
return h('svg', { viewBox: '0 0 24 24', width: s, height: s, fill: 'none', 'aria-hidden': true },
|
|
418
|
+
h('path', { d: 'M12 3l2.2 5.8L20 11l-5.8 2.2L12 19l-2.2-5.8L4 11l5.8-2.2z', stroke: 'currentColor', strokeWidth: 1.6, strokeLinejoin: 'round' }));
|
|
419
|
+
}
|
|
420
|
+
function IconClock(props) {
|
|
421
|
+
const s = props.size || 18;
|
|
422
|
+
return h('svg', { viewBox: '0 0 24 24', width: s, height: s, fill: 'none', 'aria-hidden': true },
|
|
423
|
+
h('circle', { cx: 12, cy: 12, r: 8, stroke: 'currentColor', strokeWidth: 1.6 }),
|
|
424
|
+
h('path', { d: 'M12 7.5V12l3 2', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round' }));
|
|
425
|
+
}
|
|
426
|
+
function IconRestart(props) {
|
|
427
|
+
const s = props.size || 16;
|
|
428
|
+
return h('svg', { viewBox: '0 0 24 24', width: s, height: s, fill: 'none', 'aria-hidden': true },
|
|
429
|
+
h('path', { d: 'M20 12a8 8 0 1 1-2.34-5.66M20 4v5h-5', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round' }));
|
|
430
|
+
}
|
|
431
|
+
function IconChat(props) {
|
|
432
|
+
const s = props.size || 16;
|
|
433
|
+
return h('svg', { viewBox: '0 0 24 24', width: s, height: s, fill: 'none', 'aria-hidden': true },
|
|
434
|
+
h('path', { d: 'M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H9l-4 3.5V6z', stroke: 'currentColor', strokeWidth: 1.6, strokeLinejoin: 'round' }));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function createTopBar(ctx, balance) {
|
|
438
|
+
function BalanceChip() {
|
|
439
|
+
const snap = React.useSyncExternalStore(balance.subscribe, balance.getSnapshot);
|
|
440
|
+
const [hover, setHover] = React.useState(false);
|
|
441
|
+
const tone = snap.phase === 'ready' ? balanceTone(snap.amount) : 'muted';
|
|
442
|
+
const value = snap.phase === 'ready' && typeof snap.amount === 'number' ? snap.amount.toFixed(2) : (snap.phase === 'loading' ? '…' : '—');
|
|
443
|
+
const toneColor = tone === 'green' ? '#4ade80' : tone === 'amber' ? '#e3b341' : tone === 'red' ? '#f87171' : 'var(--dsw-alias-label-secondary)';
|
|
444
|
+
const borderBase = tone === 'green' ? '#2f4a37' : tone === 'amber' ? '#4a3d22' : tone === 'red' ? '#4a2a2a' : 'var(--dsw-alias-border-l2)';
|
|
445
|
+
const at = new Date(snap.updatedAt || Date.now()).toLocaleTimeString('zh-CN', { hour12: false });
|
|
446
|
+
return h('div', {
|
|
447
|
+
style: { position: 'relative', display: 'inline-flex' },
|
|
448
|
+
onMouseEnter: () => setHover(true),
|
|
449
|
+
onMouseLeave: () => setHover(false),
|
|
450
|
+
},
|
|
451
|
+
h('button', {
|
|
452
|
+
type: 'button',
|
|
453
|
+
className: 'wb-balance',
|
|
454
|
+
onClick: () => window.open('https://platform.deepseek.com/top_up', '_blank', 'noopener,noreferrer'),
|
|
455
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 3, height: 26, padding: '0 10px', borderRadius: 8, border: '1px solid ' + borderBase, background: 'transparent', color: toneColor, fontSize: 13, fontWeight: 600, fontVariantNumeric: 'tabular-nums', cursor: 'pointer', transition: 'background .12s, border-color .12s' },
|
|
456
|
+
}, h('span', { 'aria-hidden': true }, '¥'), h('span', null, value)),
|
|
457
|
+
hover && snap.phase === 'ready'
|
|
458
|
+
? h('div', { className: 'wb-balance-tip' },
|
|
459
|
+
h('div', null, '充值余额 ¥' + (snap.amount || 0).toFixed(2)),
|
|
460
|
+
h('div', { style: { opacity: 0.7 } }, '更新于 ' + at),
|
|
461
|
+
h('div', null, '点击前往充值'),
|
|
462
|
+
)
|
|
463
|
+
: null,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const accountStore = createAccountStore(ctx);
|
|
468
|
+
accountStore.start();
|
|
469
|
+
|
|
470
|
+
function AccountButton() {
|
|
471
|
+
const snap = React.useSyncExternalStore(accountStore.subscribe, accountStore.getSnapshot);
|
|
472
|
+
const [open, setOpen] = React.useState(false);
|
|
473
|
+
const name = snap.phase === 'ready' ? snap.name : (snap.phase === 'loading' ? '…' : '账号');
|
|
474
|
+
const menuItem = (label, onClick) => h('button', {
|
|
475
|
+
type: 'button',
|
|
476
|
+
onClick: () => { setOpen(false); onClick(); },
|
|
477
|
+
style: { display: 'block', width: '100%', textAlign: 'left', padding: '7px 12px', border: 'none', background: 'transparent', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer', fontSize: 12 },
|
|
478
|
+
}, label);
|
|
479
|
+
const account = ctx.remote && ctx.remote.account;
|
|
480
|
+
const meta = { version: '1.0.0', locale: 'zh-CN', timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60 };
|
|
481
|
+
return h('div', { style: { position: 'relative' } },
|
|
482
|
+
h('button', {
|
|
483
|
+
type: 'button', title: name, onClick: () => setOpen((v) => !v),
|
|
484
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, padding: '0 10px', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', background: 'transparent', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer', fontSize: 12 },
|
|
485
|
+
}, h('span', { style: { maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, name)),
|
|
486
|
+
open ? h('div', {
|
|
487
|
+
style: { position: 'absolute', right: 0, top: 'calc(100% + 6px)', minWidth: 160, background: 'var(--dsw-alias-bg-overlay)', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 10, boxShadow: '0 8px 24px rgba(0,0,0,0.2)', padding: '4px 0', zIndex: 200 },
|
|
488
|
+
},
|
|
489
|
+
menuItem('设置', () => { const layout = ctx.get('layout'); if (layout) { try { layout.selectPanel('settings'); } catch (_e) {} } }),
|
|
490
|
+
menuItem('意见反馈', () => window.open('https://github.com/deepseek-ai/deepseek-harness/issues', '_blank', 'noopener,noreferrer')),
|
|
491
|
+
menuItem('退出登录', () => { if (account && account.signOut) Promise.resolve(account.signOut(meta)).catch(() => {}); }),
|
|
492
|
+
) : null,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function TopBar(props) {
|
|
497
|
+
const rootRef = React.useRef(null);
|
|
498
|
+
const tabsRef = React.useRef(null);
|
|
499
|
+
const reg = useRegistry();
|
|
500
|
+
const usePanelInfo = props.usePanelInfo;
|
|
501
|
+
const panelInfo = typeof usePanelInfo === 'function' ? usePanelInfo((s) => s) : null;
|
|
502
|
+
const activePanelId = panelInfo ? panelInfo.activePanelId : null;
|
|
503
|
+
const views = Array.from(reg.views.values())
|
|
504
|
+
.filter((item) => item.tab !== false)
|
|
505
|
+
.sort((a, b) => a.order - b.order);
|
|
506
|
+
const tabs = [{ id: null, title: '会话', icon: IconChat, subtitle: '当前会话与对话记录' }].concat(views);
|
|
507
|
+
const tabKey = tabs.map((item) => (item.id === null ? '__conversation' : item.id)).join('|');
|
|
508
|
+
// Kept outside the component: if the shell ever remounts the top bar while
|
|
509
|
+
// switching panels, the plate still starts from where it was instead of
|
|
510
|
+
// teleporting to the new tab.
|
|
511
|
+
const [glider, setGlider] = React.useState(lastGlider.width > 0 ? lastGlider : null);
|
|
512
|
+
|
|
513
|
+
// The selected tab's grey plate: measured from the live DOM and animated
|
|
514
|
+
// between tabs, so the highlight slides instead of blinking tab to tab.
|
|
515
|
+
React.useLayoutEffect(() => {
|
|
516
|
+
const host = tabsRef.current;
|
|
517
|
+
if (!host) return undefined;
|
|
518
|
+
const measure = () => {
|
|
519
|
+
const el = host.querySelector('[data-active="1"]');
|
|
520
|
+
if (!el) { setGlider(null); return; }
|
|
521
|
+
const next = { left: el.offsetLeft, width: el.offsetWidth };
|
|
522
|
+
lastGlider = next;
|
|
523
|
+
setGlider(next);
|
|
524
|
+
};
|
|
525
|
+
measure();
|
|
526
|
+
const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(measure) : null;
|
|
527
|
+
if (observer) observer.observe(host);
|
|
528
|
+
window.addEventListener('resize', measure);
|
|
529
|
+
return () => {
|
|
530
|
+
if (observer) observer.disconnect();
|
|
531
|
+
window.removeEventListener('resize', measure);
|
|
532
|
+
};
|
|
533
|
+
}, [activePanelId, tabKey]);
|
|
534
|
+
|
|
535
|
+
React.useEffect(() => {
|
|
536
|
+
const el = rootRef.current;
|
|
537
|
+
if (!el) return undefined;
|
|
538
|
+
let frame = el.parentElement;
|
|
539
|
+
while (frame && frame !== document.body && getComputedStyle(frame).display !== 'grid') frame = frame.parentElement;
|
|
540
|
+
const cleanup = [];
|
|
541
|
+
if (frame && frame !== document.body) {
|
|
542
|
+
frame.style.paddingTop = TOPBAR_HEIGHT + 'px';
|
|
543
|
+
frame.style.boxSizing = 'border-box';
|
|
544
|
+
cleanup.push(() => {
|
|
545
|
+
frame.style.paddingTop = '';
|
|
546
|
+
frame.style.boxSizing = '';
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const wsRoot = document.querySelector('[class*="sidebarCol"] [class*="workspaces"]');
|
|
550
|
+
if (wsRoot) {
|
|
551
|
+
Array.from(wsRoot.children).forEach((g, i) => { if (i > 0) g.style.display = 'none'; });
|
|
552
|
+
}
|
|
553
|
+
document.querySelectorAll('button[aria-label]').forEach((b) => {
|
|
554
|
+
const label = b.getAttribute('aria-label') || '';
|
|
555
|
+
if (label.includes('侧栏') || label.includes('侧边栏') || label.includes('折叠') || label.includes('collapse') || label.includes('toggle')) {
|
|
556
|
+
b.style.display = 'none';
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
const settings = document.querySelector('[class*="settingsArea"]');
|
|
560
|
+
if (settings) {
|
|
561
|
+
settings.style.position = 'fixed';
|
|
562
|
+
settings.style.top = '2px';
|
|
563
|
+
settings.style.right = '16px';
|
|
564
|
+
settings.style.left = 'auto';
|
|
565
|
+
settings.style.bottom = 'auto';
|
|
566
|
+
settings.style.width = 'max-content';
|
|
567
|
+
settings.style.zIndex = '300';
|
|
568
|
+
cleanup.push(() => {
|
|
569
|
+
settings.style.position = '';
|
|
570
|
+
settings.style.top = '';
|
|
571
|
+
settings.style.right = '';
|
|
572
|
+
settings.style.left = '';
|
|
573
|
+
settings.style.bottom = '';
|
|
574
|
+
settings.style.width = '';
|
|
575
|
+
settings.style.zIndex = '';
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
const menuObserver = new MutationObserver(() => {
|
|
579
|
+
document.querySelectorAll('[role="menu"]').forEach((menu) => {
|
|
580
|
+
const txt = menu.textContent || '';
|
|
581
|
+
if (txt.includes('退出登录')) menu.style.top = '56px';
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
menuObserver.observe(document.body, { childList: true, subtree: true });
|
|
585
|
+
return () => {
|
|
586
|
+
cleanup.forEach((fn) => fn());
|
|
587
|
+
menuObserver.disconnect();
|
|
588
|
+
};
|
|
589
|
+
}, []);
|
|
590
|
+
|
|
591
|
+
const selectPanel = (id) => openView(id);
|
|
592
|
+
const iconBtn = (id, label, Icon) => h('button', {
|
|
593
|
+
key: id, type: 'button', onClick: () => selectPanel(id),
|
|
594
|
+
className: 'wb-nav-item',
|
|
595
|
+
style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 4, height: 34, padding: '0 9px', borderRadius: 8, border: 'none', background: 'transparent', color: 'var(--dsw-alias-label-secondary)', cursor: 'pointer', flex: '0 0 auto', transition: 'background .15s, color .15s' },
|
|
596
|
+
}, h(Icon, { size: 18 }), h('span', { className: 'wb-nav-label' }, label));
|
|
597
|
+
|
|
598
|
+
const viewTab = (item) => {
|
|
599
|
+
const active = item.id === null
|
|
600
|
+
? (activePanelId === null || activePanelId === undefined)
|
|
601
|
+
: activePanelId === item.id;
|
|
602
|
+
let badge = null;
|
|
603
|
+
if (typeof item.badge === 'function') {
|
|
604
|
+
try { badge = item.badge(); } catch (_error) { badge = null; }
|
|
605
|
+
}
|
|
606
|
+
const showBadge = badge !== null && badge !== undefined && badge !== '' && badge !== 0 && badge !== false;
|
|
607
|
+
return h('button', {
|
|
608
|
+
key: item.id === null ? '__conversation' : item.id,
|
|
609
|
+
type: 'button',
|
|
610
|
+
className: 'wb-view-tab',
|
|
611
|
+
'data-active': active ? '1' : '0',
|
|
612
|
+
onClick: () => selectPanel(item.id),
|
|
613
|
+
style: {
|
|
614
|
+
position: 'relative', zIndex: 1,
|
|
615
|
+
display: 'inline-flex', alignItems: 'center', gap: 6,
|
|
616
|
+
height: 31, padding: '0 11px', borderRadius: 7,
|
|
617
|
+
border: 'none',
|
|
618
|
+
background: 'transparent',
|
|
619
|
+
color: active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
620
|
+
fontWeight: 400,
|
|
621
|
+
fontSize: 12,
|
|
622
|
+
cursor: 'pointer', flex: '0 0 auto',
|
|
623
|
+
transition: 'color .12s',
|
|
624
|
+
},
|
|
625
|
+
},
|
|
626
|
+
item.icon ? h(item.icon, { size: 15, active }) : null,
|
|
627
|
+
h('span', null, item.title),
|
|
628
|
+
showBadge ? h('span', { className: 'wb-view-tab-badge' }, String(badge)) : null,
|
|
629
|
+
);
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
return h('div', { ref: rootRef, style: { position: 'fixed', top: 0, left: 0, right: 0, height: TOPBAR_HEIGHT, zIndex: 100, pointerEvents: 'auto', display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base)', borderBottom: '1px solid var(--dsw-alias-border-l1)' } },
|
|
633
|
+
h('div', { style: { height: 56, display: 'flex', alignItems: 'center', gap: 10, padding: '0 14px' } },
|
|
634
|
+
h('div', { style: { width: 214, flex: '0 0 auto' } }),
|
|
635
|
+
h('div', { style: { display: 'flex', gap: 2, flex: 1, overflow: 'hidden' } },
|
|
636
|
+
iconBtn('workbench', '工作台', IconGrid),
|
|
637
|
+
iconBtn('plugins', '插件', IconPlugins),
|
|
638
|
+
iconBtn('schedules', '自动化任务', IconClock),
|
|
639
|
+
),
|
|
640
|
+
null,
|
|
641
|
+
),
|
|
642
|
+
h('div', { style: { height: 45, display: 'flex', alignItems: 'center', gap: 8, padding: '0 14px', borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
643
|
+
h('div', { ref: tabsRef, style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 4, flex: 1, overflowX: 'auto', overflowY: 'hidden' } },
|
|
644
|
+
glider
|
|
645
|
+
? h('div', {
|
|
646
|
+
className: 'wb-tab-glider',
|
|
647
|
+
'aria-hidden': true,
|
|
648
|
+
style: {
|
|
649
|
+
position: 'absolute', top: 0, height: 31, borderRadius: 7,
|
|
650
|
+
// translateX keeps the slide on the compositor: switching back
|
|
651
|
+
// to the (heavy) conversation page must not drop its frames.
|
|
652
|
+
left: 0, width: glider.width,
|
|
653
|
+
transform: 'translateX(' + glider.left + 'px)',
|
|
654
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
655
|
+
boxShadow: 'inset 0 0 0 1px var(--dsw-alias-border-l2)',
|
|
656
|
+
pointerEvents: 'none', zIndex: 0,
|
|
657
|
+
},
|
|
658
|
+
})
|
|
659
|
+
: null,
|
|
660
|
+
tabs.map(viewTab),
|
|
661
|
+
),
|
|
662
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flex: '0 0 auto' } },
|
|
663
|
+
h(BalanceChip, null),
|
|
664
|
+
h('button', {
|
|
665
|
+
type: 'button', title: '重启 DSH', className: 'wb-restart',
|
|
666
|
+
onClick: () => { restartDsh(ctx); },
|
|
667
|
+
style: { width: 28, height: 28, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', background: 'transparent', color: 'var(--dsw-alias-label-secondary)', cursor: 'pointer', flex: '0 0 auto', transition: 'background .12s, color .12s, border-color .12s' },
|
|
668
|
+
}, h(IconRestart, { size: 15 })),
|
|
669
|
+
),
|
|
670
|
+
),
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return TopBar;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ───────────────────────────── 每轮费用 ─────────────────────────────
|
|
677
|
+
// 官方价目(2026-10-18,元 / 百万 tokens):{ 空闲, 高峰 }
|
|
678
|
+
var PRICING = {
|
|
679
|
+
"deepseek-flash": {
|
|
680
|
+
offPeak: { cacheHit: 0.02, cacheMiss: 1.0, output: 4.0 },
|
|
681
|
+
peak: { cacheHit: 0.04, cacheMiss: 2.0, output: 8.0 },
|
|
682
|
+
},
|
|
683
|
+
"deepseek-v4-pro": {
|
|
684
|
+
offPeak: { cacheHit: 0.15, cacheMiss: 4.5, output: 13.5 },
|
|
685
|
+
peak: { cacheHit: 0.3, cacheMiss: 9.0, output: 27.0 },
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
var BJ_FORMAT = null;
|
|
690
|
+
function beijingParts(ts) {
|
|
691
|
+
try {
|
|
692
|
+
if (BJ_FORMAT === null) {
|
|
693
|
+
BJ_FORMAT = new Intl.DateTimeFormat("en-US", {
|
|
694
|
+
timeZone: "Asia/Shanghai",
|
|
695
|
+
weekday: "short",
|
|
696
|
+
hour: "2-digit",
|
|
697
|
+
minute: "2-digit",
|
|
698
|
+
hour12: false,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
var parts = BJ_FORMAT.formatToParts(new Date(ts));
|
|
702
|
+
var out = {};
|
|
703
|
+
for (var i = 0; i < parts.length; i += 1) out[parts[i].type] = parts[i].value;
|
|
704
|
+
var hour = Number(out.hour);
|
|
705
|
+
if (hour === 24) hour = 0;
|
|
706
|
+
return { weekday: out.weekday, hour: hour, minute: Number(out.minute || 0) };
|
|
707
|
+
} catch (_error) {
|
|
708
|
+
var d = new Date(ts);
|
|
709
|
+
var shifted = new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 60000);
|
|
710
|
+
return {
|
|
711
|
+
weekday: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][shifted.getDay()],
|
|
712
|
+
hour: shifted.getHours(),
|
|
713
|
+
minute: shifted.getMinutes(),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function isPeak(ts) {
|
|
719
|
+
var p = beijingParts(ts == null ? Date.now() : ts);
|
|
720
|
+
if (p.weekday === "Sat" || p.weekday === "Sun") return false;
|
|
721
|
+
var minutes = p.hour * 60 + p.minute;
|
|
722
|
+
return (
|
|
723
|
+
(minutes >= 9 * 60 && minutes < 12 * 60) ||
|
|
724
|
+
(minutes >= 14 * 60 && minutes < 18 * 60)
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function num(value) {
|
|
729
|
+
return typeof value === "number" && isFinite(value) && value > 0 ? value : 0;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function modelOf(usage) {
|
|
733
|
+
var routes = usage && usage.routes;
|
|
734
|
+
if (Array.isArray(routes)) {
|
|
735
|
+
for (var i = 0; i < routes.length; i += 1) {
|
|
736
|
+
if (routes[i] && typeof routes[i].model === "string" && routes[i].model) {
|
|
737
|
+
return routes[i].model;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return "deepseek-flash";
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function pricesFor(model) {
|
|
745
|
+
var key = String(model || "").toLowerCase();
|
|
746
|
+
return key.indexOf("v4-pro") >= 0 ? PRICING["deepseek-v4-pro"] : PRICING["deepseek-flash"];
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function computeCost(usage, ts) {
|
|
750
|
+
var model = modelOf(usage);
|
|
751
|
+
var peak = isPeak(ts);
|
|
752
|
+
var table = peak ? pricesFor(model).peak : pricesFor(model).offPeak;
|
|
753
|
+
var hit = num(usage && usage.cacheReadTokens);
|
|
754
|
+
var miss = num(usage && usage.uncachedInputTokens) + num(usage && usage.cacheWriteTokens);
|
|
755
|
+
var output = num(usage && usage.outputTokens);
|
|
756
|
+
return {
|
|
757
|
+
cost: (hit * table.cacheHit + miss * table.cacheMiss + output * table.output) / 1e6,
|
|
758
|
+
peak: peak,
|
|
759
|
+
table: table,
|
|
760
|
+
hit: hit,
|
|
761
|
+
miss: miss,
|
|
762
|
+
output: output,
|
|
763
|
+
model: model,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function formatMoney(value) {
|
|
768
|
+
if (typeof value !== "number" || !isFinite(value)) return "—";
|
|
769
|
+
if (value === 0) return "0.00";
|
|
770
|
+
var abs = Math.abs(value);
|
|
771
|
+
if (abs < 0.01) return value.toFixed(4);
|
|
772
|
+
if (abs < 1) return value.toFixed(3);
|
|
773
|
+
return value.toFixed(2);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function formatTokens(value) {
|
|
777
|
+
if (typeof value !== "number" || !isFinite(value)) return "0";
|
|
778
|
+
try {
|
|
779
|
+
return value.toLocaleString("zh-CN");
|
|
780
|
+
} catch (_error) {
|
|
781
|
+
return String(value);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function same(left, right) {
|
|
786
|
+
return left !== undefined && right !== undefined && String(left) === String(right);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function matchesTail(data, seq, turnId) {
|
|
790
|
+
if (!data) return false;
|
|
791
|
+
if (same(data.seq, seq)) return true;
|
|
792
|
+
if (data.closing && data.closing.finalNode && same(data.closing.finalNode.seq, seq)) return true;
|
|
793
|
+
if (turnId !== undefined && same(data.turn, turnId)) return true;
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function findUsage(snapshot, seq, turn) {
|
|
798
|
+
if (!snapshot) return undefined;
|
|
799
|
+
var turnId = turn && turn.id !== undefined ? turn.id : undefined;
|
|
800
|
+
|
|
801
|
+
var turns = snapshot.timeline && snapshot.timeline.turns;
|
|
802
|
+
if (turns && typeof turns.values === "function") {
|
|
803
|
+
var iterator = turns.values();
|
|
804
|
+
for (var step = iterator.next(); !step.done; step = iterator.next()) {
|
|
805
|
+
var entry = step.value;
|
|
806
|
+
var tail =
|
|
807
|
+
entry && entry.data && typeof entry.data.get === "function"
|
|
808
|
+
? entry.data.get("turn-tail")
|
|
809
|
+
: undefined;
|
|
810
|
+
if (tail && tail.tokenUsage && matchesTail(tail, seq, turnId)) {
|
|
811
|
+
return tail.tokenUsage;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (snapshot.nodes && typeof snapshot.nodes.forEach === "function") {
|
|
817
|
+
var found;
|
|
818
|
+
snapshot.nodes.forEach(function (node) {
|
|
819
|
+
if (!node || node.kind !== "turn-tail" || !node.data || !node.data.tokenUsage) return;
|
|
820
|
+
if (matchesTail(node.data, seq, turnId)) found = node.data.tokenUsage;
|
|
821
|
+
});
|
|
822
|
+
if (found) return found;
|
|
823
|
+
}
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function detailText(entry) {
|
|
828
|
+
return [
|
|
829
|
+
"模型 " + entry.model,
|
|
830
|
+
"时段 " + (entry.peak ? "高峰" : "空闲") + "(周一至周五 9–12、14–18 为高峰)",
|
|
831
|
+
"缓存命中 " + formatTokens(entry.hit) + " tok × ¥" + entry.table.cacheHit + "/M",
|
|
832
|
+
"缓存未命中 " + formatTokens(entry.miss) + " tok × ¥" + entry.table.cacheMiss + "/M",
|
|
833
|
+
"输出 " + formatTokens(entry.output) + " tok × ¥" + entry.table.output + "/M",
|
|
834
|
+
"合计 ¥" + formatMoney(entry.cost),
|
|
835
|
+
].join("\n");
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
var COST_STYLE = `
|
|
839
|
+
.dsh-cost {
|
|
840
|
+
display: inline-flex;
|
|
841
|
+
align-items: center;
|
|
842
|
+
gap: 4px;
|
|
843
|
+
align-self: flex-end;
|
|
844
|
+
margin: 4px 2px 0 0;
|
|
845
|
+
padding: 2px 7px;
|
|
846
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.24));
|
|
847
|
+
border-radius: 6px;
|
|
848
|
+
color: var(--dsw-alias-label-tertiary, #8b949e);
|
|
849
|
+
background: transparent;
|
|
850
|
+
font: inherit;
|
|
851
|
+
font-size: 11px;
|
|
852
|
+
line-height: 1.6;
|
|
853
|
+
font-variant-numeric: tabular-nums;
|
|
854
|
+
white-space: nowrap;
|
|
855
|
+
cursor: default;
|
|
856
|
+
user-select: none;
|
|
857
|
+
}
|
|
858
|
+
.dsh-cost[data-peak="1"] {
|
|
859
|
+
color: #d29922;
|
|
860
|
+
border-color: color-mix(in srgb, #d29922 40%, transparent);
|
|
861
|
+
}
|
|
862
|
+
.dsh-cost-symbol { opacity: 0.6; }
|
|
863
|
+
`;
|
|
864
|
+
|
|
865
|
+
function createTurnCost() {
|
|
866
|
+
return function TurnCost(props) {
|
|
867
|
+
var useChat = props.useChat;
|
|
868
|
+
var usage;
|
|
869
|
+
if (typeof useChat === "function") {
|
|
870
|
+
usage = useChat(function (snapshot) {
|
|
871
|
+
return findUsage(snapshot, props.seq, props.turn);
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
if (!usage) return null;
|
|
875
|
+
var entry = computeCost(usage, Date.now());
|
|
876
|
+
return h(
|
|
877
|
+
"div",
|
|
878
|
+
{
|
|
879
|
+
className: "dsh-cost",
|
|
880
|
+
"data-peak": entry.peak ? "1" : "0",
|
|
881
|
+
title: detailText(entry),
|
|
882
|
+
},
|
|
883
|
+
h("span", { className: "dsh-cost-symbol", "aria-hidden": "true" }, "≈"),
|
|
884
|
+
h("span", null, "¥" + formatMoney(entry.cost))
|
|
885
|
+
);
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// ───────────────────────────── 运行光效 ─────────────────────────────
|
|
890
|
+
var ORBIT_MS = 4800;
|
|
891
|
+
var CORNER = 8;
|
|
892
|
+
var SIGMA = 0.05;
|
|
893
|
+
var STEP_PX = 2;
|
|
894
|
+
|
|
895
|
+
function radiusOf(width, height) {
|
|
896
|
+
return Math.max(0, Math.min(CORNER, width / 2, height / 2));
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function perimeter(width, height) {
|
|
900
|
+
var r = radiusOf(width, height);
|
|
901
|
+
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 4 * ((Math.PI * r) / 2);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function pointOnRect(width, height, t) {
|
|
905
|
+
var r = radiusOf(width, height);
|
|
906
|
+
var straightX = Math.max(0, width - 2 * r);
|
|
907
|
+
var straightY = Math.max(0, height - 2 * r);
|
|
908
|
+
var arc = (Math.PI * r) / 2;
|
|
909
|
+
var total = 2 * straightX + 2 * straightY + 4 * arc;
|
|
910
|
+
if (total === 0) return { x: 0, y: 0 };
|
|
911
|
+
var d = (((t % 1) + 1) % 1) * total;
|
|
912
|
+
|
|
913
|
+
if (d < straightX) return { x: r + d, y: 0 };
|
|
914
|
+
d -= straightX;
|
|
915
|
+
if (d < arc) {
|
|
916
|
+
var a1 = -Math.PI / 2 + (d / arc) * (Math.PI / 2);
|
|
917
|
+
return { x: width - r + r * Math.cos(a1), y: r + r * Math.sin(a1) };
|
|
918
|
+
}
|
|
919
|
+
d -= arc;
|
|
920
|
+
if (d < straightY) return { x: width, y: r + d };
|
|
921
|
+
d -= straightY;
|
|
922
|
+
if (d < arc) {
|
|
923
|
+
var a2 = (d / arc) * (Math.PI / 2);
|
|
924
|
+
return { x: width - r + r * Math.cos(a2), y: height - r + r * Math.sin(a2) };
|
|
925
|
+
}
|
|
926
|
+
d -= arc;
|
|
927
|
+
if (d < straightX) return { x: width - r - d, y: height };
|
|
928
|
+
d -= straightX;
|
|
929
|
+
if (d < arc) {
|
|
930
|
+
var a3 = Math.PI / 2 + (d / arc) * (Math.PI / 2);
|
|
931
|
+
return { x: r + r * Math.cos(a3), y: height - r + r * Math.sin(a3) };
|
|
932
|
+
}
|
|
933
|
+
d -= arc;
|
|
934
|
+
if (d < straightY) return { x: 0, y: height - r - d };
|
|
935
|
+
d -= straightY;
|
|
936
|
+
var a4 = Math.PI + (Math.min(d, arc) / arc) * (Math.PI / 2);
|
|
937
|
+
return { x: r + r * Math.cos(a4), y: r + r * Math.sin(a4) };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function hotWeight(phase, t) {
|
|
941
|
+
var delta = Math.abs(phase - t);
|
|
942
|
+
var dist = Math.min(delta, 1 - delta);
|
|
943
|
+
return Math.exp(-(dist * dist) / (2 * SIGMA * SIGMA));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
var GLOW_STYLE = `
|
|
947
|
+
.dsh-glow-on {
|
|
948
|
+
position: relative;
|
|
949
|
+
border-radius: var(--dsw-radius-md, 8px);
|
|
950
|
+
animation: dsh-glow-breathe 2.8s ease-in-out infinite;
|
|
951
|
+
}
|
|
952
|
+
.dsh-glow-canvas {
|
|
953
|
+
position: absolute;
|
|
954
|
+
inset: 0;
|
|
955
|
+
width: 100%;
|
|
956
|
+
height: 100%;
|
|
957
|
+
pointer-events: none;
|
|
958
|
+
z-index: 1;
|
|
959
|
+
filter: drop-shadow(0 0 2px rgba(170, 200, 255, 0.65));
|
|
960
|
+
}
|
|
961
|
+
/* 隐藏运行中会话左侧的转圈:不依赖 JS 加的 class,React 重建行时也能兜底 */
|
|
962
|
+
div[data-row-key^="session:"]:has(svg[data-state="ongoing"]) svg[data-state="ongoing"],
|
|
963
|
+
.dsh-glow-on svg[data-state="ongoing"],
|
|
964
|
+
.dsh-glow-on [data-state="ongoing"],
|
|
965
|
+
.dsh-glow-on > span:first-child > svg {
|
|
966
|
+
display: none !important;
|
|
967
|
+
}
|
|
968
|
+
@keyframes dsh-glow-breathe {
|
|
969
|
+
0%, 100% {
|
|
970
|
+
box-shadow:
|
|
971
|
+
0 0 0 1px rgba(99, 149, 255, 0.5),
|
|
972
|
+
0 0 8px 1px rgba(99, 149, 255, 0.2);
|
|
973
|
+
}
|
|
974
|
+
50% {
|
|
975
|
+
box-shadow:
|
|
976
|
+
0 0 0 1px rgba(130, 178, 255, 0.85),
|
|
977
|
+
0 0 16px 2px rgba(99, 149, 255, 0.42);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/* 暗档:只有后台任务时,呼吸光也压到约 35% */
|
|
981
|
+
.dsh-glow-on[data-glow-dim="1"] {
|
|
982
|
+
animation: dsh-glow-breathe-dim 2.8s ease-in-out infinite;
|
|
983
|
+
}
|
|
984
|
+
@keyframes dsh-glow-breathe-dim {
|
|
985
|
+
0%, 100% {
|
|
986
|
+
box-shadow:
|
|
987
|
+
0 0 0 1px rgba(99, 149, 255, 0.18),
|
|
988
|
+
0 0 4px 0 rgba(99, 149, 255, 0.07);
|
|
989
|
+
}
|
|
990
|
+
50% {
|
|
991
|
+
box-shadow:
|
|
992
|
+
0 0 0 1px rgba(130, 178, 255, 0.32),
|
|
993
|
+
0 0 8px 1px rgba(99, 149, 255, 0.15);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
@media (prefers-reduced-motion: reduce) {
|
|
997
|
+
.dsh-glow-on { animation: none; }
|
|
998
|
+
}
|
|
999
|
+
`;
|
|
1000
|
+
|
|
1001
|
+
function installGlow(ctx) {
|
|
1002
|
+
var active = new Map();
|
|
1003
|
+
var watched = new Map();
|
|
1004
|
+
var unsubscribeJobs = null;
|
|
1005
|
+
var frame = null;
|
|
1006
|
+
var observer = null;
|
|
1007
|
+
var scheduled = false;
|
|
1008
|
+
var disposed = false;
|
|
1009
|
+
|
|
1010
|
+
function sessionIdOf(row) {
|
|
1011
|
+
var key = row.getAttribute("data-row-key");
|
|
1012
|
+
return key && key.indexOf("session:") === 0 ? key.slice(8) : null;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function hasRunningJob(sessionId) {
|
|
1016
|
+
var snapshot = ctx.jobs.state.getSnapshot();
|
|
1017
|
+
var jobs = snapshot.rows && snapshot.rows[sessionId];
|
|
1018
|
+
if (!jobs) return false;
|
|
1019
|
+
for (var i = 0; i < jobs.length; i += 1) {
|
|
1020
|
+
if (jobs[i].status === "running") return true;
|
|
1021
|
+
}
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function glowState(row) {
|
|
1026
|
+
// 2 = 双光(会话运行中),1 = 单光(仅后台任务),0 = 不亮
|
|
1027
|
+
if (row.querySelector('svg[data-state="ongoing"]') !== null) return 2;
|
|
1028
|
+
var sessionId = sessionIdOf(row);
|
|
1029
|
+
if (!sessionId) return 0;
|
|
1030
|
+
if (!watched.has(sessionId)) {
|
|
1031
|
+
watched.set(sessionId, ctx.jobs.watchRows(sessionId));
|
|
1032
|
+
}
|
|
1033
|
+
return hasRunningJob(sessionId) ? 1 : 0;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function currentSessionId() {
|
|
1037
|
+
var row = document.querySelector('div[data-row-key^="session:"][aria-selected="true"]');
|
|
1038
|
+
return row ? sessionIdOf(row) : null;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function activate(row, lights) {
|
|
1042
|
+
row.classList.add("dsh-glow-on");
|
|
1043
|
+
if (lights === 1) row.setAttribute("data-glow-dim", "1");
|
|
1044
|
+
else row.removeAttribute("data-glow-dim");
|
|
1045
|
+
var canvas = document.createElement("canvas");
|
|
1046
|
+
canvas.className = "dsh-glow-canvas";
|
|
1047
|
+
row.appendChild(canvas);
|
|
1048
|
+
active.set(row, { canvas: canvas, lights: lights });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function deactivate(row) {
|
|
1052
|
+
var entry = active.get(row);
|
|
1053
|
+
if (!entry) return;
|
|
1054
|
+
entry.canvas.remove();
|
|
1055
|
+
row.classList.remove("dsh-glow-on");
|
|
1056
|
+
row.removeAttribute("data-glow-dim");
|
|
1057
|
+
active.delete(row);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function sync() {
|
|
1061
|
+
if (disposed) return;
|
|
1062
|
+
var rows = document.querySelectorAll('div[data-row-key^="session:"]');
|
|
1063
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
1064
|
+
var row = rows[i];
|
|
1065
|
+
var lights = glowState(row);
|
|
1066
|
+
if (lights > 0) {
|
|
1067
|
+
var entry = active.get(row);
|
|
1068
|
+
if (entry === undefined) {
|
|
1069
|
+
activate(row, lights);
|
|
1070
|
+
} else if (
|
|
1071
|
+
row.classList.contains("dsh-glow-on") !== true ||
|
|
1072
|
+
row.contains(entry.canvas) !== true
|
|
1073
|
+
) {
|
|
1074
|
+
// React 重渲染会话行时会覆盖 className、删掉手动 append 的 canvas;
|
|
1075
|
+
// active 里还记着已激活,所以这里要检测并重挂。
|
|
1076
|
+
deactivate(row);
|
|
1077
|
+
activate(row, lights);
|
|
1078
|
+
} else if (entry.lights !== lights) {
|
|
1079
|
+
entry.lights = lights;
|
|
1080
|
+
if (lights === 1) row.setAttribute("data-glow-dim", "1");
|
|
1081
|
+
else row.removeAttribute("data-glow-dim");
|
|
1082
|
+
}
|
|
1083
|
+
} else if (active.has(row)) {
|
|
1084
|
+
deactivate(row);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// 后台任务按钮:有任务运行就亮双光,并隐藏它自己的圆环。
|
|
1089
|
+
var button =
|
|
1090
|
+
document.querySelector('button[aria-label*="后台任务"]') ||
|
|
1091
|
+
document.querySelector('button[aria-label*="background task"]');
|
|
1092
|
+
if (button !== null) {
|
|
1093
|
+
var currentId = currentSessionId();
|
|
1094
|
+
var jobLights = currentId && hasRunningJob(currentId) ? 1 : 0;
|
|
1095
|
+
if (jobLights > 0) {
|
|
1096
|
+
var buttonEntry = active.get(button);
|
|
1097
|
+
if (buttonEntry === undefined) {
|
|
1098
|
+
activate(button, jobLights);
|
|
1099
|
+
} else if (
|
|
1100
|
+
button.classList.contains("dsh-glow-on") !== true ||
|
|
1101
|
+
button.contains(buttonEntry.canvas) !== true
|
|
1102
|
+
) {
|
|
1103
|
+
deactivate(button);
|
|
1104
|
+
activate(button, jobLights);
|
|
1105
|
+
} else if (buttonEntry.lights !== jobLights) {
|
|
1106
|
+
buttonEntry.lights = jobLights;
|
|
1107
|
+
if (jobLights === 1) button.setAttribute("data-glow-dim", "1");
|
|
1108
|
+
else button.removeAttribute("data-glow-dim");
|
|
1109
|
+
}
|
|
1110
|
+
} else if (active.has(button)) {
|
|
1111
|
+
deactivate(button);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
active.forEach(function (_entry, row) {
|
|
1116
|
+
if (row.isConnected !== true) deactivate(row);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function schedule() {
|
|
1121
|
+
if (scheduled) return;
|
|
1122
|
+
scheduled = true;
|
|
1123
|
+
requestAnimationFrame(function () {
|
|
1124
|
+
scheduled = false;
|
|
1125
|
+
sync();
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function paint(entry, row, now) {
|
|
1130
|
+
var width = row.clientWidth;
|
|
1131
|
+
var height = row.clientHeight;
|
|
1132
|
+
if (width === 0 || height === 0) return;
|
|
1133
|
+
var canvas = entry.canvas;
|
|
1134
|
+
var dpr = window.devicePixelRatio || 1;
|
|
1135
|
+
if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
|
|
1136
|
+
canvas.width = Math.round(width * dpr);
|
|
1137
|
+
canvas.height = Math.round(height * dpr);
|
|
1138
|
+
canvas.style.width = width + "px";
|
|
1139
|
+
canvas.style.height = height + "px";
|
|
1140
|
+
}
|
|
1141
|
+
var context = canvas.getContext("2d");
|
|
1142
|
+
if (!context) return;
|
|
1143
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1144
|
+
context.clearRect(0, 0, width, height);
|
|
1145
|
+
context.lineWidth = 1;
|
|
1146
|
+
context.lineCap = "round";
|
|
1147
|
+
|
|
1148
|
+
var r = radiusOf(width, height);
|
|
1149
|
+
var total = perimeter(width, height);
|
|
1150
|
+
var t = (now % ORBIT_MS) / ORBIT_MS;
|
|
1151
|
+
|
|
1152
|
+
context.strokeStyle = "rgba(99, 149, 255, 0.07)";
|
|
1153
|
+
context.beginPath();
|
|
1154
|
+
if (typeof context.roundRect === "function") {
|
|
1155
|
+
context.roundRect(0.5, 0.5, width - 1, height - 1, r);
|
|
1156
|
+
} else {
|
|
1157
|
+
context.rect(0.5, 0.5, width - 1, height - 1);
|
|
1158
|
+
}
|
|
1159
|
+
context.stroke();
|
|
1160
|
+
|
|
1161
|
+
var count = Math.max(24, Math.ceil(total / STEP_PX));
|
|
1162
|
+
for (var i = 0; i < count; i += 1) {
|
|
1163
|
+
var phase = i / count;
|
|
1164
|
+
var weight = hotWeight(phase, t);
|
|
1165
|
+
if (weight < 0.02) continue;
|
|
1166
|
+
var p0 = pointOnRect(width, height, phase);
|
|
1167
|
+
var p1 = pointOnRect(width, height, (i + 1) / count);
|
|
1168
|
+
var rChannel = Math.round(99 + (205 - 99) * weight);
|
|
1169
|
+
var gChannel = Math.round(149 + (180 - 149) * weight);
|
|
1170
|
+
var bChannel = 255;
|
|
1171
|
+
// lights === 1 表示「只有后台任务」,整体压暗。
|
|
1172
|
+
var alpha = (0.05 + 0.95 * weight) * (entry.lights === 1 ? 0.35 : 1);
|
|
1173
|
+
context.strokeStyle =
|
|
1174
|
+
"rgba(" + rChannel + "," + gChannel + "," + bChannel + "," + alpha.toFixed(3) + ")";
|
|
1175
|
+
context.beginPath();
|
|
1176
|
+
context.moveTo(p0.x, p0.y);
|
|
1177
|
+
context.lineTo(p1.x, p1.y);
|
|
1178
|
+
context.stroke();
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function tick(now) {
|
|
1183
|
+
if (disposed) return;
|
|
1184
|
+
active.forEach(function (entry, row) {
|
|
1185
|
+
paint(entry, row, now);
|
|
1186
|
+
});
|
|
1187
|
+
frame = requestAnimationFrame(tick);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (typeof MutationObserver !== "undefined" && document.body !== null) {
|
|
1191
|
+
observer = new MutationObserver(schedule);
|
|
1192
|
+
observer.observe(document.body, {
|
|
1193
|
+
childList: true,
|
|
1194
|
+
subtree: true,
|
|
1195
|
+
attributes: true,
|
|
1196
|
+
attributeFilter: ["data-state"],
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
window.addEventListener("resize", schedule);
|
|
1200
|
+
unsubscribeJobs = ctx.jobs.state.subscribe(schedule);
|
|
1201
|
+
sync();
|
|
1202
|
+
frame = requestAnimationFrame(tick);
|
|
1203
|
+
|
|
1204
|
+
return function () {
|
|
1205
|
+
disposed = true;
|
|
1206
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
1207
|
+
if (observer !== null) observer.disconnect();
|
|
1208
|
+
window.removeEventListener("resize", schedule);
|
|
1209
|
+
if (unsubscribeJobs !== null) unsubscribeJobs();
|
|
1210
|
+
watched.forEach(function (release) {
|
|
1211
|
+
release();
|
|
1212
|
+
});
|
|
1213
|
+
watched.clear();
|
|
1214
|
+
active.forEach(function (_entry, row) {
|
|
1215
|
+
deactivate(row);
|
|
1216
|
+
});
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// 费用与光效样式(迁移自 dsh-usage-hud)
|
|
1221
|
+
var USAGE_STYLE = COST_STYLE + "\n" + GLOW_STYLE;
|
|
1222
|
+
|
|
1223
|
+
return {
|
|
1224
|
+
inject: ['slots', 'remote', 'remote.account', 'jobs'],
|
|
1225
|
+
apply(ctx) {
|
|
1226
|
+
try {
|
|
1227
|
+
if (typeof document !== 'undefined') {
|
|
1228
|
+
ctx.effect(() => {
|
|
1229
|
+
const tag = document.createElement('style');
|
|
1230
|
+
tag.textContent = '@keyframes wb-spin { to { transform: rotate(360deg); } }'
|
|
1231
|
+
+ '\n[class*="sidebarCol"] [class*="panelList"], [class*="sidebarCol"] [class*="newSession"], [class*="sidebarCol"] [class*="topStrip"] { display: none !important; }'
|
|
1232
|
+
+ '\n[class*="sidebarCol"] [class*="logoRow"] { height: 0 !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; overflow: visible !important; }'
|
|
1233
|
+
+ '\n[class*="sidebarCol"] [class*="workspaces"] { background: var(--dsw-alias-bg-base) !important; border: 1px solid var(--dsw-alias-border-l1) !important; border-radius: 12px !important; margin: 0 12px 8px 0 !important; overflow: hidden !important; }'
|
|
1234
|
+
+ '\n[class*="sidebarCol"] [class*="workspaces"] > *:nth-child(n+2) { display: none !important; }'
|
|
1235
|
+
+ '\n[class*="sidebarCol"] [class*="brand"]:not([class*="Identity"]):not([class*="Mark"]):not([class*="Name"]) { position: fixed !important; top: 14px !important; left: 16px !important; z-index: 300 !important; width: 260px !important; max-width: 260px !important; pointer-events: none !important; }'
|
|
1236
|
+
+ '\n[class*="controls"] { display: none !important; }'
|
|
1237
|
+
+ '\n[class*="sidebarCol"] [class*="brandIdentity"] { display: flex !important; align-items: center !important; gap: 6px !important; }'
|
|
1238
|
+
+ '\n[role="menu"][style*="--dsh-menu-r3v"] { top: 56px !important; }'
|
|
1239
|
+
+ '\n[class*="sidebarCol"] { border: none !important; }'
|
|
1240
|
+
+ '\n.wb-session-row { display: flex; align-items: center; gap: 6px; width: 100%; padding: 5px 8px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-primary); cursor: pointer; text-align: left; font-size: 12px; }'
|
|
1241
|
+
+ '\n.wb-session-row:hover { background: var(--dsw-alias-bg-layer-2); }'
|
|
1242
|
+
+ '\n.wb-session-row .wb-session-more { opacity: 0; transition: opacity .12s; }'
|
|
1243
|
+
+ '\n.wb-balance-tip { position: absolute; top: calc(100% + 6px); right: 0; z-index: 500; padding: 8px 10px; border-radius: 8px; background: #23262c; border: 1px solid #2f333b; color: var(--dsw-alias-label-primary); font-size: 11px; line-height: 1.7; white-space: nowrap; max-width: 240px; box-sizing: border-box; box-shadow: 0 6px 20px rgba(0,0,0,.28); pointer-events: none; }'
|
|
1244
|
+
+ '\n.wb-balance:hover { border-color: #4ade80 !important; background: rgba(74,222,128,.12) !important; }'
|
|
1245
|
+
+ '\n.wb-nav-item { position: relative; }'
|
|
1246
|
+
+ '\n.wb-nav-label { max-width: 0; opacity: 0; overflow: hidden; white-space: nowrap; font-size: 12px; transition: max-width .18s ease, opacity .18s ease; }'
|
|
1247
|
+
+ '\n.wb-nav-item:hover .wb-nav-label { max-width: 110px; opacity: 1; }'
|
|
1248
|
+
+ '\n.wb-nav-item:hover { background: var(--dsw-alias-bg-layer-2) !important; color: var(--dsw-alias-label-primary) !important; }'
|
|
1249
|
+
+ '\n.wb-view-tab[data-active="0"]:hover { background: var(--dsw-alias-bg-layer-1) !important; color: var(--dsw-alias-label-primary) !important; }'
|
|
1250
|
+
+ '\n.wb-tab-glider { transition: transform .26s cubic-bezier(.4,0,.2,1), width .26s cubic-bezier(.4,0,.2,1); will-change: transform; }'
|
|
1251
|
+
+ '\n@media (prefers-reduced-motion: reduce) { .wb-tab-glider { transition: none; } }'
|
|
1252
|
+
+ '\n.wb-view-tab-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--dsw-alias-brand-primary); color: #fff; font-size: 10px; font-weight: 650; font-variant-numeric: tabular-nums; box-sizing: border-box; }'
|
|
1253
|
+
+ '\n.wb-restart:hover { background: var(--dsw-alias-bg-layer-2) !important; color: var(--dsw-alias-label-primary) !important; border-color: var(--dsw-alias-border-l1) !important; }';
|
|
1254
|
+
document.head.append(tag);
|
|
1255
|
+
return () => tag.remove();
|
|
1256
|
+
}, 'workbench: spinner keyframes');
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const api = createApi(ctx);
|
|
1260
|
+
ctx.effect(() => ctx.provide('workbench', api), 'workbench: provide api');
|
|
1261
|
+
|
|
1262
|
+
layoutService = ctx.get('layout') || null;
|
|
1263
|
+
layoutGetter = () => ctx.get('layout') || null;
|
|
1264
|
+
|
|
1265
|
+
// The shell's own overview lives above the tab row: it is reachable from
|
|
1266
|
+
// the top-bar "工作台" icon, so it registers a page without a tab.
|
|
1267
|
+
ctx.effect(() => api.registerView({
|
|
1268
|
+
id: 'workbench',
|
|
1269
|
+
title: '工作台',
|
|
1270
|
+
subtitle: '工作台总览:已接入的视图与浮窗',
|
|
1271
|
+
icon: WorkbenchIcon,
|
|
1272
|
+
component: WorkbenchOverview,
|
|
1273
|
+
order: -10,
|
|
1274
|
+
tab: false,
|
|
1275
|
+
}), 'workbench: overview view');
|
|
1276
|
+
|
|
1277
|
+
const balance = createBalanceStore(ctx);
|
|
1278
|
+
try {
|
|
1279
|
+
ctx.effect(() => { balance.start(); return () => balance.stop(); }, 'workbench: balance polling');
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
console.warn('[workbench] balance polling failed', err);
|
|
1282
|
+
}
|
|
1283
|
+
const TopBar = createTopBar(ctx, balance);
|
|
1284
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
1285
|
+
name: 'shell.overlay', id: 'workbench-topbar', order: -100,
|
|
1286
|
+
}, TopBar));
|
|
1287
|
+
|
|
1288
|
+
// ── 每轮费用 + 运行光效(迁移自 dsh-usage-hud) ──
|
|
1289
|
+
if (typeof document !== 'undefined') {
|
|
1290
|
+
ctx.effect(() => {
|
|
1291
|
+
const tag = document.createElement('style');
|
|
1292
|
+
tag.dataset.plugin = 'dsh-workbench:usage';
|
|
1293
|
+
tag.textContent = USAGE_STYLE;
|
|
1294
|
+
document.head.append(tag);
|
|
1295
|
+
return () => tag.remove();
|
|
1296
|
+
}, 'workbench: usage styles');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const TurnCost = createTurnCost();
|
|
1300
|
+
ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
|
|
1301
|
+
name: 'conversation.chat.turnTail',
|
|
1302
|
+
id: 'workbench-turn-cost',
|
|
1303
|
+
order: 600,
|
|
1304
|
+
}, TurnCost));
|
|
1305
|
+
|
|
1306
|
+
// 注意:ctx.effect 接收的是「返回 dispose 的函数」。
|
|
1307
|
+
// 直接传 installGlow(ctx) 的返回值会让 effect 立刻执行清理,光效刚装上就被拆掉。
|
|
1308
|
+
ctx.effect(() => installGlow(ctx), 'workbench: glow');
|
|
1309
|
+
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
console.error('[workbench] apply failed:', err);
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
};
|
|
1315
|
+
},
|
|
1316
|
+
});
|