@philomelcc/dsh-workbench 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/client.js +314 -15
- package/index.js +42 -6
- package/package.json +19 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philomelcc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
外壳本体:顶部第二行的标签切换、每个模块一整页,以及模块注册用的那套小 API。
|
|
4
4
|
|
|
5
|
+
## 0.1.9 的变化
|
|
6
|
+
|
|
7
|
+
- 新增**功能开关**和**工作台设置页**(系统栏最左侧的 mark 图标):主题切换、余额显示、重启按钮、进行中光效、单次消耗估计,都能单独关掉。
|
|
8
|
+
- 顶部第一行系统栏改为**镜像原生侧栏导航项**,任何注册 `sidebar.panellist` 的插件都会自动出现。
|
|
9
|
+
- 模块支持**无外壳运行**:没有工作台时自己注册到原生侧栏。
|
|
10
|
+
- 设置入口挪到左上角,工作台图标重绘为「带标签栏的面板」;`apiVersion` 仍是 `1`。
|
|
11
|
+
|
|
12
|
+
完整变更见 [CHANGELOG](https://github.com/philomelcc/dsh-workbench/blob/main/CHANGELOG.md)。
|
|
13
|
+
|
|
5
14
|
它单独装没什么用——至少配一个模块:
|
|
6
15
|
|
|
7
16
|
```powershell
|
package/client.js
CHANGED
|
@@ -21,6 +21,61 @@ window.__ModuleLoader__.load({
|
|
|
21
21
|
labelSecondary: 'var(--dsw-alias-label-secondary)',
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
// ---------- feature switches ----------
|
|
25
|
+
// The shell is optional decoration on top of the navigation layer: someone
|
|
26
|
+
// who only wants the tab row can turn the rest off and let another plugin
|
|
27
|
+
// own balances, themes or cost estimates. Every switch defaults to on, so
|
|
28
|
+
// installing the shell keeps the previous behaviour.
|
|
29
|
+
const FEATURE_KEYS = ['theme', 'balance', 'restart', 'glow', 'cost'];
|
|
30
|
+
const FEATURE_DEFAULTS = { theme: true, balance: true, restart: true, glow: true, cost: true };
|
|
31
|
+
const FEATURE_LABELS = {
|
|
32
|
+
theme: { title: '主题切换', hint: '顶部栏的深色 / 浅色 / 纸色三个色块' },
|
|
33
|
+
balance: { title: '余额显示', hint: '顶部栏的账户余额' },
|
|
34
|
+
restart: { title: '重启按钮', hint: '顶部栏的一键重启(需要外部管理器)' },
|
|
35
|
+
glow: { title: '进行中光效', hint: '会话运行中时列表行的流动光效' },
|
|
36
|
+
cost: { title: '单次消耗估计', hint: '每轮对话结束时显示的 token 与费用估算' },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function createFeaturesStore() {
|
|
40
|
+
const KEY = 'dsh-workbench:features';
|
|
41
|
+
const listeners = new Set();
|
|
42
|
+
let state = read();
|
|
43
|
+
|
|
44
|
+
function read() {
|
|
45
|
+
const next = { ...FEATURE_DEFAULTS };
|
|
46
|
+
try {
|
|
47
|
+
const raw = localStorage.getItem(KEY);
|
|
48
|
+
if (raw) {
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
for (const key of FEATURE_KEYS) {
|
|
51
|
+
if (typeof parsed[key] === 'boolean') next[key] = parsed[key];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (_error) { /* storage disabled */ }
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
getSnapshot() { return state; },
|
|
60
|
+
set(key, value) {
|
|
61
|
+
if (!FEATURE_KEYS.includes(key) || state[key] === value) return;
|
|
62
|
+
state = { ...state, [key]: value };
|
|
63
|
+
try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (_error) { /* storage disabled */ }
|
|
64
|
+
for (const fn of listeners) fn();
|
|
65
|
+
},
|
|
66
|
+
subscribe(fn) {
|
|
67
|
+
listeners.add(fn);
|
|
68
|
+
return () => { listeners.delete(fn); };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const features = createFeaturesStore();
|
|
74
|
+
|
|
75
|
+
function useFeatures() {
|
|
76
|
+
return React.useSyncExternalStore(features.subscribe, features.getSnapshot);
|
|
77
|
+
}
|
|
78
|
+
|
|
24
79
|
// ---------- shared UI primitives ----------
|
|
25
80
|
function StatusDot(props) {
|
|
26
81
|
const ok = props.ok !== false;
|
|
@@ -259,6 +314,33 @@ window.__ModuleLoader__.load({
|
|
|
259
314
|
);
|
|
260
315
|
}
|
|
261
316
|
|
|
317
|
+
// ---------- settings icon & page ----------
|
|
318
|
+
const SETTINGS_VIEW_ID = 'workbench-settings';
|
|
319
|
+
|
|
320
|
+
// A panel with its own tab strip: the workbench's mark, distinct from the
|
|
321
|
+
// native "More" grid (four squares) so the two do not read alike.
|
|
322
|
+
function IconWorkbenchTabs(props) {
|
|
323
|
+
const size = props.size || 20;
|
|
324
|
+
return h('svg', { viewBox: '0 0 24 24', width: size, height: size, 'aria-hidden': true, fill: 'none' },
|
|
325
|
+
h('rect', { x: 3.5, y: 4.5, width: 17, height: 15, rx: 2.5, stroke: 'currentColor', strokeWidth: 1.5 }),
|
|
326
|
+
h('path', { d: 'M3.5 9.2h17', stroke: 'currentColor', strokeWidth: 1.4 }),
|
|
327
|
+
h('circle', { cx: 6.4, cy: 6.85, r: 0.85, fill: 'currentColor' }),
|
|
328
|
+
h('circle', { cx: 9.4, cy: 6.85, r: 0.85, fill: 'currentColor' }),
|
|
329
|
+
h('path', { d: 'M6.2 13.2h5.6M6.2 16h8.4', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round' }),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function IconSettings(props) {
|
|
334
|
+
const size = props.size || 20;
|
|
335
|
+
return h('svg', { viewBox: '0 0 24 24', width: size, height: size, 'aria-hidden': true, fill: 'none' },
|
|
336
|
+
h('circle', { cx: 12, cy: 12, r: 3.2, stroke: 'currentColor', strokeWidth: 1.5 }),
|
|
337
|
+
h('path', {
|
|
338
|
+
d: 'M19.4 13.5a7.7 7.7 0 0 0 0-3l1.7-1.2-1.9-3.3-2 .8a7.6 7.6 0 0 0-2.6-1.5L14.3 3h-3.8l-.3 2.3a7.6 7.6 0 0 0-2.6 1.5l-2-.8-1.9 3.3 1.7 1.2a7.7 7.7 0 0 0 0 3l-1.7 1.2 1.9 3.3 2-.8a7.6 7.6 0 0 0 2.6 1.5l.3 2.3h3.8l.3-2.3a7.6 7.6 0 0 0 2.6-1.5l2 .8 1.9-3.3z',
|
|
339
|
+
stroke: 'currentColor', strokeWidth: 1.4, strokeLinejoin: 'round',
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
262
344
|
// ---------- overview page ----------
|
|
263
345
|
function WorkbenchOverview() {
|
|
264
346
|
const reg = useRegistry();
|
|
@@ -311,8 +393,110 @@ window.__ModuleLoader__.load({
|
|
|
311
393
|
// ────────────────────────── top bar (stage 1) ──────────────────────────
|
|
312
394
|
const TOPBAR_HEIGHT = 102;
|
|
313
395
|
|
|
396
|
+
// ---------- settings page ----------
|
|
397
|
+
// Reachable from the gear in the system bar. Two jobs: turn the shell's own
|
|
398
|
+
// extras on or off, and show which modules registered a page.
|
|
399
|
+
function WorkbenchSettings() {
|
|
400
|
+
const enabled = useFeatures();
|
|
401
|
+
const reg = useRegistry();
|
|
402
|
+
const views = Array.from(reg.views.values())
|
|
403
|
+
.filter((item) => item.id !== SETTINGS_VIEW_ID)
|
|
404
|
+
.sort((a, b) => a.order - b.order);
|
|
405
|
+
|
|
406
|
+
function toggleRow(key) {
|
|
407
|
+
const meta = FEATURE_LABELS[key];
|
|
408
|
+
const on = enabled[key];
|
|
409
|
+
return h('div', {
|
|
410
|
+
key,
|
|
411
|
+
style: {
|
|
412
|
+
display: 'flex', alignItems: 'center', gap: 14,
|
|
413
|
+
padding: '12px 14px', borderBottom: '1px solid ' + tokens.border1,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
h('div', { style: { flex: '1 1 auto', minWidth: 0 } },
|
|
417
|
+
h('div', { style: { fontWeight: 550 } }, meta.title),
|
|
418
|
+
h('div', { style: { fontSize: 11, color: tokens.labelSecondary } }, meta.hint),
|
|
419
|
+
),
|
|
420
|
+
h('button', {
|
|
421
|
+
type: 'button',
|
|
422
|
+
role: 'switch',
|
|
423
|
+
'aria-checked': on ? 'true' : 'false',
|
|
424
|
+
'aria-label': meta.title,
|
|
425
|
+
onClick: () => features.set(key, !on),
|
|
426
|
+
style: {
|
|
427
|
+
position: 'relative', width: 40, height: 22, borderRadius: 999,
|
|
428
|
+
border: '1px solid ' + (on ? 'var(--dsw-alias-brand-primary)' : tokens.border2),
|
|
429
|
+
background: on ? 'var(--dsw-alias-brand-primary)' : tokens.bgLayer2,
|
|
430
|
+
cursor: 'pointer', flex: '0 0 auto', padding: 0,
|
|
431
|
+
transition: 'background .15s, border-color .15s',
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
h('span', {
|
|
435
|
+
'aria-hidden': true,
|
|
436
|
+
style: {
|
|
437
|
+
position: 'absolute', top: 2, left: on ? 20 : 2,
|
|
438
|
+
width: 16, height: 16, borderRadius: '50%',
|
|
439
|
+
background: on ? '#fff' : 'var(--dsw-alias-label-secondary)',
|
|
440
|
+
transition: 'left .15s',
|
|
441
|
+
},
|
|
442
|
+
}),
|
|
443
|
+
),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function viewRow(item) {
|
|
448
|
+
return h('div', {
|
|
449
|
+
key: item.id,
|
|
450
|
+
style: {
|
|
451
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
452
|
+
padding: '10px 14px', borderBottom: '1px solid ' + tokens.border1,
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
h('div', { style: { width: 22, height: 22, color: tokens.labelSecondary, flex: '0 0 auto', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
|
|
456
|
+
item.icon ? h(item.icon, { size: 18, active: false }) : null),
|
|
457
|
+
h('div', { style: { flex: '1 1 auto', minWidth: 0 } },
|
|
458
|
+
h('div', { style: { fontWeight: 550, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.title),
|
|
459
|
+
h('div', { style: { fontSize: 11, color: tokens.labelSecondary } },
|
|
460
|
+
(item.tab === false ? '无标签页 · ' : '标签页 · ') + item.id + ' · order ' + item.order),
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return h('div', {
|
|
466
|
+
style: {
|
|
467
|
+
padding: '24px 28px', maxWidth: 920, margin: '0 auto', height: '100%',
|
|
468
|
+
boxSizing: 'border-box', overflowY: 'auto',
|
|
469
|
+
color: tokens.label, background: tokens.bgBase,
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
h('h2', { style: { margin: '0 0 4px', fontSize: 20, fontWeight: 650 } }, '工作台设置'),
|
|
473
|
+
h('div', { style: { fontSize: 13, color: tokens.labelSecondary, marginBottom: 20 } },
|
|
474
|
+
'只想要导航分层的话,上面这些都可以关掉,交给别的插件。'),
|
|
475
|
+
h('div', null,
|
|
476
|
+
h(SectionHeader, { title: '功能', subtitle: '外壳自带、可以单独关闭的额外功能' }),
|
|
477
|
+
h(Card, null, FEATURE_KEYS.map(toggleRow)),
|
|
478
|
+
),
|
|
479
|
+
h('div', { style: { marginTop: 20 } },
|
|
480
|
+
h(SectionHeader, { title: '已注册的模块', subtitle: '它们出现在顶部第二行的模块栏' }),
|
|
481
|
+
h(Card, null,
|
|
482
|
+
views.length === 0
|
|
483
|
+
? h(EmptyState, { text: '还没有模块注册页面。' })
|
|
484
|
+
: views.map(viewRow),
|
|
485
|
+
),
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
314
490
|
async function restartDsh() {
|
|
315
|
-
try {
|
|
491
|
+
try {
|
|
492
|
+
const res = await fetch('/wb/workbench/restart', { method: 'POST' });
|
|
493
|
+
const json = await res.json().catch(() => null);
|
|
494
|
+
if (json && json.ok === false) {
|
|
495
|
+
window.alert(json.hint || '这台机器上没有可用的重启管理器,请手动重启 DSH Web。');
|
|
496
|
+
}
|
|
497
|
+
} catch (_error) {
|
|
498
|
+
/* offline: nothing to do */
|
|
499
|
+
}
|
|
316
500
|
}
|
|
317
501
|
|
|
318
502
|
function createBalanceStore(ctx) {
|
|
@@ -665,8 +849,74 @@ window.__ModuleLoader__.load({
|
|
|
665
849
|
return ThemeSwitcher;
|
|
666
850
|
}
|
|
667
851
|
|
|
852
|
+
// ---------- native sidebar panel mirror ----------
|
|
853
|
+
// The workbench keeps no navigation list of its own: whatever the sidebar
|
|
854
|
+
// lists (Plugins, Automations, More — plus any plugin that registers a
|
|
855
|
+
// panel later) is mirrored into the top bar, click delegated to that row.
|
|
856
|
+
function createPanelMirror() {
|
|
857
|
+
function PanelMirror() {
|
|
858
|
+
const [items, setItems] = React.useState(function () { return []; });
|
|
859
|
+
|
|
860
|
+
React.useEffect(function () {
|
|
861
|
+
function sync() {
|
|
862
|
+
const list = document.querySelector('nav[class*="panelList"]');
|
|
863
|
+
if (!list) return;
|
|
864
|
+
const next = [];
|
|
865
|
+
Array.prototype.forEach.call(list.children, function (node) {
|
|
866
|
+
const btn = node.tagName === 'BUTTON'
|
|
867
|
+
? node
|
|
868
|
+
: (node.querySelector ? node.querySelector('button') : null);
|
|
869
|
+
if (!btn) return;
|
|
870
|
+
const label = btn.getAttribute('aria-label') || String(node.textContent || '').trim();
|
|
871
|
+
const svg = btn.querySelector('svg');
|
|
872
|
+
next.push({ label: label || '入口', html: svg ? svg.outerHTML : '', btn: btn });
|
|
873
|
+
});
|
|
874
|
+
setItems(function (prev) {
|
|
875
|
+
const same = prev.length === next.length && prev.every(function (item, index) {
|
|
876
|
+
return item.label === next[index].label && item.html === next[index].html;
|
|
877
|
+
});
|
|
878
|
+
return same ? prev : next;
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
sync();
|
|
882
|
+
const observer = new MutationObserver(sync);
|
|
883
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
884
|
+
return function () { observer.disconnect(); };
|
|
885
|
+
}, []);
|
|
886
|
+
|
|
887
|
+
return h('div', { className: 'wb-topbar-icons', style: { display: 'flex', gap: 2, flex: 1, overflow: 'hidden' } },
|
|
888
|
+
items.map(function (item, index) {
|
|
889
|
+
return h('button', {
|
|
890
|
+
key: item.label + ':' + index,
|
|
891
|
+
type: 'button',
|
|
892
|
+
className: 'wb-nav-item',
|
|
893
|
+
'aria-label': item.label,
|
|
894
|
+
onClick: function () {
|
|
895
|
+
try { item.btn.click(); } catch (_error) { /* row went away */ }
|
|
896
|
+
},
|
|
897
|
+
style: {
|
|
898
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
899
|
+
gap: 4, height: 34, padding: '0 9px', borderRadius: 8, border: 'none',
|
|
900
|
+
background: 'transparent', color: 'var(--dsw-alias-label-secondary)',
|
|
901
|
+
cursor: 'pointer', flex: '0 0 auto', transition: 'background .15s, color .15s',
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
h('span', {
|
|
905
|
+
'aria-hidden': true,
|
|
906
|
+
style: { display: 'inline-flex', width: 18, height: 18, flex: '0 0 auto' },
|
|
907
|
+
dangerouslySetInnerHTML: { __html: item.html },
|
|
908
|
+
}),
|
|
909
|
+
h('span', { className: 'wb-nav-label' }, item.label),
|
|
910
|
+
);
|
|
911
|
+
}),
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return PanelMirror;
|
|
915
|
+
}
|
|
916
|
+
|
|
668
917
|
function createTopBar(ctx, balance) {
|
|
669
918
|
const ThemeSwitcher = createThemeSwitcher(ctx);
|
|
919
|
+
const PanelMirror = createPanelMirror();
|
|
670
920
|
|
|
671
921
|
function BalanceChip() {
|
|
672
922
|
const snap = React.useSyncExternalStore(balance.subscribe, balance.getSnapshot);
|
|
@@ -731,6 +981,7 @@ window.__ModuleLoader__.load({
|
|
|
731
981
|
function TopBar(props) {
|
|
732
982
|
const rootRef = React.useRef(null);
|
|
733
983
|
const tabsRef = React.useRef(null);
|
|
984
|
+
const enabled = useFeatures();
|
|
734
985
|
const reg = useRegistry();
|
|
735
986
|
const usePanelInfo = props.usePanelInfo;
|
|
736
987
|
const panelInfo = typeof usePanelInfo === 'function' ? usePanelInfo((s) => s) : null;
|
|
@@ -887,12 +1138,25 @@ window.__ModuleLoader__.load({
|
|
|
887
1138
|
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)' } },
|
|
888
1139
|
h('div', { style: { height: 56, display: 'flex', alignItems: 'center', gap: 10, padding: '0 14px' } },
|
|
889
1140
|
h('div', { className: 'wb-topbar-brand', style: { width: 214, flex: '0 0 auto' } }),
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1141
|
+
// System-level entry, first in the top-left corner: the workbench's
|
|
1142
|
+
// own mark opens its settings (feature switches + module list).
|
|
1143
|
+
h('button', {
|
|
1144
|
+
type: 'button',
|
|
1145
|
+
className: 'wb-nav-item wb-settings-entry',
|
|
1146
|
+
'aria-label': '工作台设置',
|
|
1147
|
+
onClick: () => selectPanel(SETTINGS_VIEW_ID),
|
|
1148
|
+
style: {
|
|
1149
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
1150
|
+
gap: 4, height: 34, padding: '0 9px', borderRadius: 8, border: 'none',
|
|
1151
|
+
background: 'transparent', color: 'var(--dsw-alias-label-secondary)',
|
|
1152
|
+
cursor: 'pointer', flex: '0 0 auto', transition: 'background .15s, color .15s',
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
h(IconWorkbenchTabs, { size: 18 }),
|
|
1156
|
+
h('span', { className: 'wb-nav-label' }, '工作台'),
|
|
894
1157
|
),
|
|
895
|
-
h(
|
|
1158
|
+
h(PanelMirror),
|
|
1159
|
+
enabled.theme ? h(ThemeSwitcher) : null,
|
|
896
1160
|
),
|
|
897
1161
|
h('div', { style: { height: 45, display: 'flex', alignItems: 'center', gap: 8, padding: '0 14px', borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
898
1162
|
h('div', { ref: tabsRef, style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 4, flex: 1, overflowX: 'auto', overflowY: 'hidden' } },
|
|
@@ -915,12 +1179,12 @@ window.__ModuleLoader__.load({
|
|
|
915
1179
|
tabs.map(viewTab),
|
|
916
1180
|
),
|
|
917
1181
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flex: '0 0 auto' } },
|
|
918
|
-
h(BalanceChip, null),
|
|
919
|
-
h('button', {
|
|
1182
|
+
enabled.balance ? h(BalanceChip, null) : null,
|
|
1183
|
+
enabled.restart ? h('button', {
|
|
920
1184
|
type: 'button', title: '重启 DSH', className: 'wb-restart',
|
|
921
1185
|
onClick: () => { restartDsh(ctx); },
|
|
922
1186
|
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' },
|
|
923
|
-
}, h(IconRestart, { size: 15 })),
|
|
1187
|
+
}, h(IconRestart, { size: 15 })) : null,
|
|
924
1188
|
),
|
|
925
1189
|
),
|
|
926
1190
|
);
|
|
@@ -1001,9 +1265,16 @@ window.__ModuleLoader__.load({
|
|
|
1001
1265
|
return key.indexOf("v4-pro") >= 0 ? PRICING["deepseek-v4-pro"] : PRICING["deepseek-flash"];
|
|
1002
1266
|
}
|
|
1003
1267
|
|
|
1268
|
+
function timeMs(value) {
|
|
1269
|
+
var n = typeof value === "number" ? value : Number(value);
|
|
1270
|
+
if (!isFinite(n) || n <= 0) return undefined;
|
|
1271
|
+
// Turn times can arrive in seconds; anything below 1e12 ms is seconds.
|
|
1272
|
+
return n < 1e12 ? n * 1000 : n;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1004
1275
|
function computeCost(usage, ts) {
|
|
1005
1276
|
var model = modelOf(usage);
|
|
1006
|
-
var peak = isPeak(ts);
|
|
1277
|
+
var peak = isPeak(timeMs(ts));
|
|
1007
1278
|
var table = peak ? pricesFor(model).peak : pricesFor(model).offPeak;
|
|
1008
1279
|
var hit = num(usage && usage.cacheReadTokens);
|
|
1009
1280
|
var miss = num(usage && usage.uncachedInputTokens) + num(usage && usage.cacheWriteTokens);
|
|
@@ -1063,7 +1334,7 @@ window.__ModuleLoader__.load({
|
|
|
1063
1334
|
? entry.data.get("turn-tail")
|
|
1064
1335
|
: undefined;
|
|
1065
1336
|
if (tail && tail.tokenUsage && matchesTail(tail, seq, turnId)) {
|
|
1066
|
-
return tail
|
|
1337
|
+
return tail;
|
|
1067
1338
|
}
|
|
1068
1339
|
}
|
|
1069
1340
|
}
|
|
@@ -1072,7 +1343,7 @@ window.__ModuleLoader__.load({
|
|
|
1072
1343
|
var found;
|
|
1073
1344
|
snapshot.nodes.forEach(function (node) {
|
|
1074
1345
|
if (!node || node.kind !== "turn-tail" || !node.data || !node.data.tokenUsage) return;
|
|
1075
|
-
if (matchesTail(node.data, seq, turnId)) found = node.data
|
|
1346
|
+
if (matchesTail(node.data, seq, turnId)) found = node.data;
|
|
1076
1347
|
});
|
|
1077
1348
|
if (found) return found;
|
|
1078
1349
|
}
|
|
@@ -1127,7 +1398,9 @@ window.__ModuleLoader__.load({
|
|
|
1127
1398
|
});
|
|
1128
1399
|
}
|
|
1129
1400
|
if (!usage) return null;
|
|
1130
|
-
|
|
1401
|
+
// Price by the turn's own timestamp, not by when this row happens to
|
|
1402
|
+
// render: otherwise every historical turn reprices at the current hour.
|
|
1403
|
+
var entry = computeCost(usage.tokenUsage, usage.time);
|
|
1131
1404
|
return h(
|
|
1132
1405
|
"div",
|
|
1133
1406
|
{
|
|
@@ -1548,6 +1821,17 @@ body[data-wb-paper="1"] [class*="_hoverPath"] {
|
|
|
1548
1821
|
tab: false,
|
|
1549
1822
|
}), 'workbench: overview view');
|
|
1550
1823
|
|
|
1824
|
+
// Settings page: no tab — it is reached from the gear in the system bar.
|
|
1825
|
+
ctx.effect(() => api.registerView({
|
|
1826
|
+
id: SETTINGS_VIEW_ID,
|
|
1827
|
+
title: '工作台设置',
|
|
1828
|
+
subtitle: '功能开关与已注册的模块',
|
|
1829
|
+
icon: IconSettings,
|
|
1830
|
+
component: WorkbenchSettings,
|
|
1831
|
+
order: -20,
|
|
1832
|
+
tab: false,
|
|
1833
|
+
}), 'workbench: settings view');
|
|
1834
|
+
|
|
1551
1835
|
const balance = createBalanceStore(ctx);
|
|
1552
1836
|
try {
|
|
1553
1837
|
ctx.effect(() => { balance.start(); return () => balance.stop(); }, 'workbench: balance polling');
|
|
@@ -1571,15 +1855,30 @@ body[data-wb-paper="1"] [class*="_hoverPath"] {
|
|
|
1571
1855
|
}
|
|
1572
1856
|
|
|
1573
1857
|
const TurnCost = createTurnCost();
|
|
1858
|
+
function TurnCostGated(props) {
|
|
1859
|
+
const enabled = useFeatures();
|
|
1860
|
+
if (!enabled.cost) return null;
|
|
1861
|
+
return h(TurnCost, props);
|
|
1862
|
+
}
|
|
1574
1863
|
ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
|
|
1575
1864
|
name: 'conversation.chat.turnTail',
|
|
1576
1865
|
id: 'workbench-turn-cost',
|
|
1577
1866
|
order: 600,
|
|
1578
|
-
},
|
|
1867
|
+
}, TurnCostGated));
|
|
1579
1868
|
|
|
1580
1869
|
// 注意:ctx.effect 接收的是「返回 dispose 的函数」。
|
|
1581
1870
|
// 直接传 installGlow(ctx) 的返回值会让 effect 立刻执行清理,光效刚装上就被拆掉。
|
|
1582
|
-
|
|
1871
|
+
// The switch can turn it off at runtime, so mount/unmount follows the store.
|
|
1872
|
+
ctx.effect(() => {
|
|
1873
|
+
let dispose = null;
|
|
1874
|
+
const sync = () => {
|
|
1875
|
+
if (dispose) { dispose(); dispose = null; }
|
|
1876
|
+
if (features.getSnapshot().glow) dispose = installGlow(ctx);
|
|
1877
|
+
};
|
|
1878
|
+
sync();
|
|
1879
|
+
const unsubscribe = features.subscribe(sync);
|
|
1880
|
+
return () => { unsubscribe(); if (dispose) dispose(); };
|
|
1881
|
+
}, 'workbench: glow');
|
|
1583
1882
|
|
|
1584
1883
|
} catch (err) {
|
|
1585
1884
|
console.error('[workbench] apply failed:', err);
|
package/index.js
CHANGED
|
@@ -2,12 +2,39 @@
|
|
|
2
2
|
* Host half of the workbench shell.
|
|
3
3
|
*
|
|
4
4
|
* The workbench's value lives in the Client half (module API + overview page).
|
|
5
|
-
* The only Host duty is the one-click restart endpoint
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* The only Host duty is the one-click restart endpoint: it stops the running web
|
|
6
|
+
* host and lets an external supervisor start it again, so the current page
|
|
7
|
+
* simply reconnects. A restart is only offered when such a supervisor is
|
|
8
|
+
* actually present — exiting without one would strand the user with a dead
|
|
9
|
+
* server.
|
|
8
10
|
*/
|
|
9
11
|
import { execSync } from 'node:child_process';
|
|
10
12
|
|
|
13
|
+
/** Supervisor that can start the host again, resolved per platform. */
|
|
14
|
+
function resolveSupervisor() {
|
|
15
|
+
const command = process.env.DSH_RESTART_CMD || '';
|
|
16
|
+
if (command) return { kind: 'command', command };
|
|
17
|
+
if (process.platform !== 'win32') return null;
|
|
18
|
+
// Windows: a scheduled task. DSH_RESTART_TASK overrides the name, so the
|
|
19
|
+
// default stays useful for the author's machine without hardcoding it for
|
|
20
|
+
// everyone else.
|
|
21
|
+
const task = process.env.DSH_RESTART_TASK || 'philomel-dsh-web';
|
|
22
|
+
try {
|
|
23
|
+
execSync(`schtasks /query /tn ${JSON.stringify(task)}`, { windowsHide: true, stdio: 'ignore' });
|
|
24
|
+
return { kind: 'task', task };
|
|
25
|
+
} catch (_error) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function startSupervisor(supervisor) {
|
|
31
|
+
if (supervisor.kind === 'command') {
|
|
32
|
+
execSync(supervisor.command, { windowsHide: true, stdio: 'ignore' });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
execSync(`schtasks /run /tn ${JSON.stringify(supervisor.task)}`, { windowsHide: true, stdio: 'ignore' });
|
|
36
|
+
}
|
|
37
|
+
|
|
11
38
|
export const inject = ['webServer'];
|
|
12
39
|
|
|
13
40
|
export function apply(ctx) {
|
|
@@ -28,12 +55,21 @@ export function apply(ctx) {
|
|
|
28
55
|
res.end('method not allowed\n');
|
|
29
56
|
return;
|
|
30
57
|
}
|
|
31
|
-
//
|
|
32
|
-
|
|
58
|
+
// Refuse without a supervisor: exiting here would leave no server behind.
|
|
59
|
+
const supervisor = resolveSupervisor();
|
|
60
|
+
if (!supervisor) {
|
|
61
|
+
res.writeHead(409, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
62
|
+
res.end(JSON.stringify({
|
|
63
|
+
ok: false,
|
|
64
|
+
reason: 'no-supervisor',
|
|
65
|
+
hint: '这台机器上没有可用的重启管理器,请手动重启 DSH Web。',
|
|
66
|
+
}));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
33
69
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
34
70
|
res.end(JSON.stringify({ ok: true, restarting: true, etaSeconds: 300 }));
|
|
35
71
|
setTimeout(() => {
|
|
36
|
-
try {
|
|
72
|
+
try { startSupervisor(supervisor); } catch (_e) { /* best effort */ }
|
|
37
73
|
process.exit(0);
|
|
38
74
|
}, 300);
|
|
39
75
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@philomelcc/dsh-workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Workbench shell for DeepSeek Harness: a view/page module API, shared UI primitives and a top-bar switcher.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,20 +14,34 @@
|
|
|
14
14
|
"bugs": {
|
|
15
15
|
"url": "https://github.com/philomelcc/dsh-workbench/issues"
|
|
16
16
|
},
|
|
17
|
-
"keywords": [
|
|
17
|
+
"keywords": [
|
|
18
|
+
"dsh",
|
|
19
|
+
"deepseek-harness",
|
|
20
|
+
"workbench",
|
|
21
|
+
"plugin",
|
|
22
|
+
"web-ui"
|
|
23
|
+
],
|
|
18
24
|
"exports": {
|
|
19
25
|
".": "./index.js",
|
|
20
26
|
"./client": "./client.js",
|
|
21
27
|
"./locale/*.json": "./locale/*.json",
|
|
22
28
|
"./icon": "./icon.svg"
|
|
23
29
|
},
|
|
24
|
-
"files": [
|
|
30
|
+
"files": [
|
|
31
|
+
"index.js",
|
|
32
|
+
"client.js",
|
|
33
|
+
"cordis.patch.yml",
|
|
34
|
+
"locale/*.json",
|
|
35
|
+
"icon.svg"
|
|
36
|
+
],
|
|
25
37
|
"publishConfig": {
|
|
26
38
|
"access": "public",
|
|
27
39
|
"registry": "https://registry.npmjs.org/"
|
|
28
40
|
},
|
|
29
41
|
"dsh": {
|
|
30
|
-
"bundle": {
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
31
45
|
"client": {
|
|
32
46
|
"platform": "web",
|
|
33
47
|
"inject": [
|
|
@@ -38,4 +52,4 @@
|
|
|
38
52
|
]
|
|
39
53
|
}
|
|
40
54
|
}
|
|
41
|
-
}
|
|
55
|
+
}
|