@philomelcc/dsh-workbench 0.1.7 → 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 +365 -27
- 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) {
|
|
@@ -470,6 +654,11 @@ window.__ModuleLoader__.load({
|
|
|
470
654
|
'--dsw-specific-input-major': '#f2ece0',
|
|
471
655
|
'--dsw-specific-tip': '#f7f2e7',
|
|
472
656
|
'--dsw-specific-menu': '#fcf9f3',
|
|
657
|
+
// Scrollbars read their own alias pair and default to neutral grey.
|
|
658
|
+
'--dsw-alias-scrollbar-bg-l1': '#d8cfba',
|
|
659
|
+
'--dsw-alias-scrollbar-bg-l2': '#d8cfba',
|
|
660
|
+
'--dsw-alias-scrollbar-hover-l1': '#c6bba2',
|
|
661
|
+
'--dsw-alias-scrollbar-hover-l2': '#c6bba2',
|
|
473
662
|
// The neutral greys are blue-tinted. Buttons ("load earlier"), inputs,
|
|
474
663
|
// chips and cards read their pale surfaces from this ramp, so warm it up
|
|
475
664
|
// too; deep steps stay untouched because they are text/borders.
|
|
@@ -555,12 +744,15 @@ window.__ModuleLoader__.load({
|
|
|
555
744
|
return;
|
|
556
745
|
}
|
|
557
746
|
const rect = settings.getBoundingClientRect();
|
|
558
|
-
if (rect.width > 0)
|
|
559
|
-
|
|
560
|
-
// the account's optical center instead of a guessed offset.
|
|
561
|
-
if (rect.height > 0) {
|
|
747
|
+
if (rect.width > 0) {
|
|
748
|
+
box.style.right = (rect.width + 16 + 12) + 'px';
|
|
562
749
|
box.style.top = rect.top + 'px';
|
|
563
750
|
box.style.height = rect.height + 'px';
|
|
751
|
+
} else {
|
|
752
|
+
// Narrow screens hide the account block; pin the swatches right.
|
|
753
|
+
box.style.right = '12px';
|
|
754
|
+
box.style.top = '2px';
|
|
755
|
+
box.style.height = '40px';
|
|
564
756
|
}
|
|
565
757
|
};
|
|
566
758
|
place();
|
|
@@ -657,8 +849,74 @@ window.__ModuleLoader__.load({
|
|
|
657
849
|
return ThemeSwitcher;
|
|
658
850
|
}
|
|
659
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
|
+
|
|
660
917
|
function createTopBar(ctx, balance) {
|
|
661
918
|
const ThemeSwitcher = createThemeSwitcher(ctx);
|
|
919
|
+
const PanelMirror = createPanelMirror();
|
|
662
920
|
|
|
663
921
|
function BalanceChip() {
|
|
664
922
|
const snap = React.useSyncExternalStore(balance.subscribe, balance.getSnapshot);
|
|
@@ -723,6 +981,7 @@ window.__ModuleLoader__.load({
|
|
|
723
981
|
function TopBar(props) {
|
|
724
982
|
const rootRef = React.useRef(null);
|
|
725
983
|
const tabsRef = React.useRef(null);
|
|
984
|
+
const enabled = useFeatures();
|
|
726
985
|
const reg = useRegistry();
|
|
727
986
|
const usePanelInfo = props.usePanelInfo;
|
|
728
987
|
const panelInfo = typeof usePanelInfo === 'function' ? usePanelInfo((s) => s) : null;
|
|
@@ -777,12 +1036,32 @@ window.__ModuleLoader__.load({
|
|
|
777
1036
|
if (wsRoot) {
|
|
778
1037
|
Array.from(wsRoot.children).forEach((g, i) => { if (i > 0) g.style.display = 'none'; });
|
|
779
1038
|
}
|
|
1039
|
+
// Narrow screens keep the official sidebar toggle: the workbench has no
|
|
1040
|
+
// session list of its own, so hiding it strands phone users.
|
|
1041
|
+
const narrowScreen = typeof window !== 'undefined' && window.innerWidth < 768;
|
|
780
1042
|
document.querySelectorAll('button[aria-label]').forEach((b) => {
|
|
781
1043
|
const label = b.getAttribute('aria-label') || '';
|
|
782
|
-
if (label.includes('侧栏') || label.includes('侧边栏') || label.includes('折叠') || label.includes('collapse') || label.includes('toggle')) {
|
|
1044
|
+
if (!narrowScreen && (label.includes('侧栏') || label.includes('侧边栏') || label.includes('折叠') || label.includes('collapse') || label.includes('toggle'))) {
|
|
783
1045
|
b.style.display = 'none';
|
|
784
1046
|
}
|
|
785
1047
|
});
|
|
1048
|
+
if (narrowScreen) {
|
|
1049
|
+
// The official toggle lives inside the translated sidebar container,
|
|
1050
|
+
// so a fixed copy would ride along with it. Own button in <body>,
|
|
1051
|
+
// clicks delegated to the official one.
|
|
1052
|
+
const original = Array.from(document.querySelectorAll('button[aria-label]'))
|
|
1053
|
+
.find((b) => b.getAttribute('aria-label') === '打开侧边栏');
|
|
1054
|
+
if (original) {
|
|
1055
|
+
const toggle = document.createElement('button');
|
|
1056
|
+
toggle.type = 'button';
|
|
1057
|
+
toggle.className = 'wb-mobile-sidebar-toggle';
|
|
1058
|
+
toggle.setAttribute('aria-label', '显示会话列表');
|
|
1059
|
+
toggle.textContent = '☰';
|
|
1060
|
+
toggle.addEventListener('click', () => original.click());
|
|
1061
|
+
document.body.appendChild(toggle);
|
|
1062
|
+
cleanup.push(() => toggle.remove());
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
786
1065
|
const settings = document.querySelector('[class*="settingsArea"]');
|
|
787
1066
|
if (settings) {
|
|
788
1067
|
settings.style.position = 'fixed';
|
|
@@ -858,13 +1137,26 @@ window.__ModuleLoader__.load({
|
|
|
858
1137
|
|
|
859
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)' } },
|
|
860
1139
|
h('div', { style: { height: 56, display: 'flex', alignItems: 'center', gap: 10, padding: '0 14px' } },
|
|
861
|
-
h('div', { style: { width: 214, flex: '0 0 auto' } }),
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1140
|
+
h('div', { className: 'wb-topbar-brand', style: { width: 214, flex: '0 0 auto' } }),
|
|
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' }, '工作台'),
|
|
866
1157
|
),
|
|
867
|
-
h(
|
|
1158
|
+
h(PanelMirror),
|
|
1159
|
+
enabled.theme ? h(ThemeSwitcher) : null,
|
|
868
1160
|
),
|
|
869
1161
|
h('div', { style: { height: 45, display: 'flex', alignItems: 'center', gap: 8, padding: '0 14px', borderTop: '1px solid var(--dsw-alias-border-l1)' } },
|
|
870
1162
|
h('div', { ref: tabsRef, style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 4, flex: 1, overflowX: 'auto', overflowY: 'hidden' } },
|
|
@@ -887,12 +1179,12 @@ window.__ModuleLoader__.load({
|
|
|
887
1179
|
tabs.map(viewTab),
|
|
888
1180
|
),
|
|
889
1181
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flex: '0 0 auto' } },
|
|
890
|
-
h(BalanceChip, null),
|
|
891
|
-
h('button', {
|
|
1182
|
+
enabled.balance ? h(BalanceChip, null) : null,
|
|
1183
|
+
enabled.restart ? h('button', {
|
|
892
1184
|
type: 'button', title: '重启 DSH', className: 'wb-restart',
|
|
893
1185
|
onClick: () => { restartDsh(ctx); },
|
|
894
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' },
|
|
895
|
-
}, h(IconRestart, { size: 15 })),
|
|
1187
|
+
}, h(IconRestart, { size: 15 })) : null,
|
|
896
1188
|
),
|
|
897
1189
|
),
|
|
898
1190
|
);
|
|
@@ -973,9 +1265,16 @@ window.__ModuleLoader__.load({
|
|
|
973
1265
|
return key.indexOf("v4-pro") >= 0 ? PRICING["deepseek-v4-pro"] : PRICING["deepseek-flash"];
|
|
974
1266
|
}
|
|
975
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
|
+
|
|
976
1275
|
function computeCost(usage, ts) {
|
|
977
1276
|
var model = modelOf(usage);
|
|
978
|
-
var peak = isPeak(ts);
|
|
1277
|
+
var peak = isPeak(timeMs(ts));
|
|
979
1278
|
var table = peak ? pricesFor(model).peak : pricesFor(model).offPeak;
|
|
980
1279
|
var hit = num(usage && usage.cacheReadTokens);
|
|
981
1280
|
var miss = num(usage && usage.uncachedInputTokens) + num(usage && usage.cacheWriteTokens);
|
|
@@ -1035,7 +1334,7 @@ window.__ModuleLoader__.load({
|
|
|
1035
1334
|
? entry.data.get("turn-tail")
|
|
1036
1335
|
: undefined;
|
|
1037
1336
|
if (tail && tail.tokenUsage && matchesTail(tail, seq, turnId)) {
|
|
1038
|
-
return tail
|
|
1337
|
+
return tail;
|
|
1039
1338
|
}
|
|
1040
1339
|
}
|
|
1041
1340
|
}
|
|
@@ -1044,7 +1343,7 @@ window.__ModuleLoader__.load({
|
|
|
1044
1343
|
var found;
|
|
1045
1344
|
snapshot.nodes.forEach(function (node) {
|
|
1046
1345
|
if (!node || node.kind !== "turn-tail" || !node.data || !node.data.tokenUsage) return;
|
|
1047
|
-
if (matchesTail(node.data, seq, turnId)) found = node.data
|
|
1346
|
+
if (matchesTail(node.data, seq, turnId)) found = node.data;
|
|
1048
1347
|
});
|
|
1049
1348
|
if (found) return found;
|
|
1050
1349
|
}
|
|
@@ -1099,7 +1398,9 @@ window.__ModuleLoader__.load({
|
|
|
1099
1398
|
});
|
|
1100
1399
|
}
|
|
1101
1400
|
if (!usage) return null;
|
|
1102
|
-
|
|
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);
|
|
1103
1404
|
return h(
|
|
1104
1405
|
"div",
|
|
1105
1406
|
{
|
|
@@ -1184,6 +1485,9 @@ window.__ModuleLoader__.load({
|
|
|
1184
1485
|
z-index: 1;
|
|
1185
1486
|
filter: drop-shadow(0 0 2px rgba(170, 200, 255, 0.65));
|
|
1186
1487
|
}
|
|
1488
|
+
body[data-wb-paper="1"] .dsh-glow-canvas {
|
|
1489
|
+
filter: drop-shadow(0 0 2px rgba(198, 156, 96, 0.6));
|
|
1490
|
+
}
|
|
1187
1491
|
/* 隐藏运行中会话左侧的转圈:不依赖 JS 加的 class,React 重建行时也能兜底 */
|
|
1188
1492
|
div[data-row-key^="session:"]:has(svg[data-state="ongoing"]) svg[data-state="ongoing"],
|
|
1189
1493
|
.dsh-glow-on svg[data-state="ongoing"],
|
|
@@ -1378,12 +1682,18 @@ body[data-wb-paper="1"] [class*="_hoverPath"] {
|
|
|
1378
1682
|
var breath = 0.5 + 0.5 * Math.sin((now / 2800) * Math.PI * 2);
|
|
1379
1683
|
var dim = entry.lights === 1 ? 0.35 : 1;
|
|
1380
1684
|
var edgeAlpha = (0.12 + breath * 0.33) * dim;
|
|
1685
|
+
// Paper turns the blue streak into a bronze one; blue reads as ink
|
|
1686
|
+
// smear on a warm sheet.
|
|
1687
|
+
var paperNow = false;
|
|
1688
|
+
try { paperNow = document.body.hasAttribute("data-wb-paper"); } catch (_error) { paperNow = false; }
|
|
1689
|
+
var glowBase = paperNow ? [138, 106, 69] : [99, 149, 255];
|
|
1690
|
+
var glowHot = paperNow ? [198, 156, 96] : [205, 180, 255];
|
|
1381
1691
|
|
|
1382
1692
|
context.save();
|
|
1383
1693
|
context.lineWidth = 1;
|
|
1384
|
-
context.strokeStyle = "rgba(
|
|
1694
|
+
context.strokeStyle = "rgba(" + glowBase[0] + ", " + glowBase[1] + ", " + glowBase[2] + ", " + edgeAlpha.toFixed(3) + ")";
|
|
1385
1695
|
context.shadowBlur = (4 + breath * 10) * dim;
|
|
1386
|
-
context.shadowColor = "rgba(
|
|
1696
|
+
context.shadowColor = "rgba(" + glowBase[0] + ", " + glowBase[1] + ", " + glowBase[2] + ", " + ((0.2 + breath * 0.35) * dim).toFixed(3) + ")";
|
|
1387
1697
|
context.beginPath();
|
|
1388
1698
|
if (typeof context.roundRect === "function") {
|
|
1389
1699
|
context.roundRect(0.5, 0.5, width - 1, height - 1, r);
|
|
@@ -1400,9 +1710,9 @@ body[data-wb-paper="1"] [class*="_hoverPath"] {
|
|
|
1400
1710
|
if (weight < 0.02) continue;
|
|
1401
1711
|
var p0 = pointOnRect(width, height, phase);
|
|
1402
1712
|
var p1 = pointOnRect(width, height, (i + 1) / count);
|
|
1403
|
-
var rChannel = Math.round(
|
|
1404
|
-
var gChannel = Math.round(
|
|
1405
|
-
var bChannel =
|
|
1713
|
+
var rChannel = Math.round(glowBase[0] + (glowHot[0] - glowBase[0]) * weight);
|
|
1714
|
+
var gChannel = Math.round(glowBase[1] + (glowHot[1] - glowBase[1]) * weight);
|
|
1715
|
+
var bChannel = Math.round(glowBase[2] + (glowHot[2] - glowBase[2]) * weight);
|
|
1406
1716
|
// lights === 1 表示「只有后台任务」,整体压暗。
|
|
1407
1717
|
var alpha = (0.05 + 0.95 * weight) * (entry.lights === 1 ? 0.35 : 1);
|
|
1408
1718
|
context.strokeStyle =
|
|
@@ -1475,7 +1785,9 @@ body[data-wb-paper="1"] [class*="_hoverPath"] {
|
|
|
1475
1785
|
+ '\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; }'
|
|
1476
1786
|
+ '\n.wb-session-row:hover { background: var(--dsw-alias-bg-layer-2); }'
|
|
1477
1787
|
+ '\n.wb-session-row .wb-session-more { opacity: 0; transition: opacity .12s; }'
|
|
1478
|
-
+ '\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; }'
|
|
1788
|
+
+ '\n.wb-balance-tip { position: absolute; top: calc(100% + 6px); right: 0; z-index: 500; padding: 8px 10px; border-radius: 8px; background: var(--dsw-alias-bg-overlay, #23262c); border: 1px solid var(--dsw-alias-border-l2, #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; }'
|
|
1789
|
+
+ '\n.wb-mobile-sidebar-toggle { position: fixed; left: 10px; top: 10px; z-index: 5000; width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-size: 15px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; box-shadow: 0 1px 4px rgba(0,0,0,.12); }'
|
|
1790
|
+
+ '\n@media (max-width: 767px) { .wb-topbar-brand, [class*="settingsArea"] { display: none !important; } .wb-topbar-icons { margin-left: 42px; } [class*="sidebarCol"] [class*="brand"] { display: none !important; } [class*="sidebarCol"] { padding-top: 44px !important; } [aria-label="收起侧边栏"] { display: none !important; } }'
|
|
1479
1791
|
+ '\n.wb-balance:hover { border-color: #4ade80 !important; background: rgba(74,222,128,.12) !important; }'
|
|
1480
1792
|
+ '\n.wb-nav-item { position: relative; }'
|
|
1481
1793
|
+ '\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; }'
|
|
@@ -1509,6 +1821,17 @@ body[data-wb-paper="1"] [class*="_hoverPath"] {
|
|
|
1509
1821
|
tab: false,
|
|
1510
1822
|
}), 'workbench: overview view');
|
|
1511
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
|
+
|
|
1512
1835
|
const balance = createBalanceStore(ctx);
|
|
1513
1836
|
try {
|
|
1514
1837
|
ctx.effect(() => { balance.start(); return () => balance.stop(); }, 'workbench: balance polling');
|
|
@@ -1532,15 +1855,30 @@ body[data-wb-paper="1"] [class*="_hoverPath"] {
|
|
|
1532
1855
|
}
|
|
1533
1856
|
|
|
1534
1857
|
const TurnCost = createTurnCost();
|
|
1858
|
+
function TurnCostGated(props) {
|
|
1859
|
+
const enabled = useFeatures();
|
|
1860
|
+
if (!enabled.cost) return null;
|
|
1861
|
+
return h(TurnCost, props);
|
|
1862
|
+
}
|
|
1535
1863
|
ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
|
|
1536
1864
|
name: 'conversation.chat.turnTail',
|
|
1537
1865
|
id: 'workbench-turn-cost',
|
|
1538
1866
|
order: 600,
|
|
1539
|
-
},
|
|
1867
|
+
}, TurnCostGated));
|
|
1540
1868
|
|
|
1541
1869
|
// 注意:ctx.effect 接收的是「返回 dispose 的函数」。
|
|
1542
1870
|
// 直接传 installGlow(ctx) 的返回值会让 effect 立刻执行清理,光效刚装上就被拆掉。
|
|
1543
|
-
|
|
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');
|
|
1544
1882
|
|
|
1545
1883
|
} catch (err) {
|
|
1546
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
|
+
}
|