@gehennawu/dsh-service 0.13.2 → 0.14.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.en.md +3 -1
- package/README.md +3 -1
- package/client.js +288 -143
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -11,7 +11,7 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
11
11
|
### Version and updates
|
|
12
12
|
|
|
13
13
|
- Displays current DSH and plugin versions with links to GitHub Releases
|
|
14
|
-
- Automatically checks npm registry for stable and preview releases,
|
|
14
|
+
- Automatically checks npm registry for stable and preview releases; when a new version exists, the right-side status text (chevron + `New version`) is clickable to expand/collapse, showing the current/latest versions and both dist-tag versions inline, each with npmjs and npmmirror links (npmjs.com is blocked on some networks; npmmirror serves as the mirror entry)
|
|
15
15
|
- One-click plugin upgrade with automatic restart after completion
|
|
16
16
|
|
|
17
17
|
### Safe restart
|
|
@@ -19,6 +19,7 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
19
19
|
- Detects active agents, background jobs, and terminals before restart; lists them and requires explicit confirmation
|
|
20
20
|
- `/restart` command also available in conversations; automatically refuses when active work is detected
|
|
21
21
|
- Automatically probes for the new process after restart and reloads the page; manual reload available after 60 seconds
|
|
22
|
+
- Optional `Restart` entry at the bottom of the settings left navigation, enabled by a switch in the Restart tab (off by default), sharing the exact same confirmation flow as the Restart tab
|
|
22
23
|
|
|
23
24
|
### Health diagnostics
|
|
24
25
|
|
|
@@ -44,6 +45,7 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
44
45
|
### Task notifications
|
|
45
46
|
|
|
46
47
|
- Browser notification when a session finishes its turn, or when your approval, plan review, or answer is needed
|
|
48
|
+
- Clicking a notification focuses the DSH page and closes the popup
|
|
47
49
|
- Three toggle switches: master switch, task completion, and approvals & questions, each controlled independently
|
|
48
50
|
- Bell icon in the conversation input bar toggles the master switch quickly
|
|
49
51
|
- All toggles persist across page reloads
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
### 版本与更新
|
|
12
12
|
|
|
13
13
|
- 显示当前 DSH 和插件版本,版本号链接到 GitHub Releases
|
|
14
|
-
- 自动检查 npm registry
|
|
14
|
+
- 自动检查 npm registry 的正式版和预览版;有新版本时右侧「小三角 + 有新版本」状态文本可点击展开/收起,行内展示当前/最新版本与正式版/预览版双 tag,版本号分别带 npmjs 与 npmmirror 链接(部分网络会被 npmjs.com 拦截,npmmirror 作为镜像入口)
|
|
15
15
|
- 一键升级插件,升级后自动重启
|
|
16
16
|
|
|
17
17
|
### 安全重启
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
- 重启前检测活跃 Agent、后台任务和终端,展示清单并要求显式确认
|
|
20
20
|
- 对话中输入 `/restart` 也可触发,检测到运行中工作时自动拒绝
|
|
21
21
|
- 重启后自动探测新进程并刷新页面,60 秒未恢复时提供手动刷新
|
|
22
|
+
- 可在「重启」标签开启「设置页左列显示入口」开关(默认关闭),开启后在设置页左侧标签列底部显示「重启」快捷入口,与「重启」标签共用同一套确认流程
|
|
22
23
|
|
|
23
24
|
### 健康诊断
|
|
24
25
|
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
### 任务通知
|
|
45
46
|
|
|
46
47
|
- 会话完成一轮任务、或需要你授权/审阅计划/选择答案时发送浏览器通知
|
|
48
|
+
- 点击系统通知弹窗会聚焦到 DSH 页面并关闭该通知
|
|
47
49
|
- 三档开关条:总开关、任务结束通知、授权与提问通知,各自独立控制
|
|
48
50
|
- 对话栏内铃铛图标快速切换总开关
|
|
49
51
|
- 各开关在页面刷新后保持
|
package/client.js
CHANGED
|
@@ -10,6 +10,7 @@ window.__ModuleLoader__.load({
|
|
|
10
10
|
const NS = 'dsh-service'
|
|
11
11
|
const zh = {
|
|
12
12
|
'nav.label': '服务控制',
|
|
13
|
+
'nav.restart': '重启',
|
|
13
14
|
'overlay.label': '服务重启状态',
|
|
14
15
|
'recovery.waiting.title': '服务重启中…',
|
|
15
16
|
'recovery.waiting.body': '正在等待新的 DSH Web 进程启动,已等待 {seconds} 秒。',
|
|
@@ -52,6 +53,7 @@ window.__ModuleLoader__.load({
|
|
|
52
53
|
'health.detail.permissions.warning': '发现 {count} 个文件或目录权限异常',
|
|
53
54
|
'health.detail.generic': '{status}',
|
|
54
55
|
'tabs.overview': '概览',
|
|
56
|
+
'tabs.notify': '通知',
|
|
55
57
|
'tabs.health': '健康诊断',
|
|
56
58
|
'tabs.usage': '模型统计',
|
|
57
59
|
'overview.container': '容器信息',
|
|
@@ -106,6 +108,8 @@ window.__ModuleLoader__.load({
|
|
|
106
108
|
'update.checking': '检查中…',
|
|
107
109
|
'update.current': '已是最新版本',
|
|
108
110
|
'update.available': '有新版本:{version}',
|
|
111
|
+
'update.detailsButton': '查看详情',
|
|
112
|
+
'update.detailsHide': '收起',
|
|
109
113
|
'update.unavailable': '暂时无法检查最新版本',
|
|
110
114
|
'update.unpublished': '尚未发布可检查版本',
|
|
111
115
|
'health.recheck': '重新诊断',
|
|
@@ -113,7 +117,8 @@ window.__ModuleLoader__.load({
|
|
|
113
117
|
'update.details.title': 'DSH 更新可用',
|
|
114
118
|
'update.details.current': '当前版本:{version}',
|
|
115
119
|
'update.details.latest': '最新版本:{version}',
|
|
116
|
-
'update.
|
|
120
|
+
'update.channelStable': '正式版',
|
|
121
|
+
'update.channelPreview': '预览版',
|
|
117
122
|
'update.details.close': '关闭',
|
|
118
123
|
'update.upgrade': '升级插件',
|
|
119
124
|
'update.upgrading': '升级中…',
|
|
@@ -146,6 +151,8 @@ window.__ModuleLoader__.load({
|
|
|
146
151
|
'restart.sent': '重启指令已发出。',
|
|
147
152
|
'restart.sentHint': '页面连接即将断开,服务恢复后将自动刷新。',
|
|
148
153
|
'restart.idleHint': '当前没有检测到运行中的工作。确认后将断开连接,等待服务自动重启。',
|
|
154
|
+
'restart.navToggle': '设置页左列显示「重启」入口',
|
|
155
|
+
'restart.navToggleHint': '默认关闭;开启后在设置页左侧标签列底部显示快捷重启入口',
|
|
149
156
|
'activity.agent': 'Agent',
|
|
150
157
|
'activity.job': '后台任务',
|
|
151
158
|
'activity.terminal': '终端',
|
|
@@ -212,6 +219,7 @@ window.__ModuleLoader__.load({
|
|
|
212
219
|
}
|
|
213
220
|
const en = {
|
|
214
221
|
'nav.label': 'Service Control',
|
|
222
|
+
'nav.restart': 'Restart',
|
|
215
223
|
'overlay.label': 'Service restart status',
|
|
216
224
|
'recovery.waiting.title': 'Restarting service…',
|
|
217
225
|
'recovery.waiting.body': 'Waiting for a new DSH Web process. Elapsed: {seconds} seconds.',
|
|
@@ -254,6 +262,7 @@ window.__ModuleLoader__.load({
|
|
|
254
262
|
'health.detail.permissions.warning': 'Found {count} file or directory permission anomalies',
|
|
255
263
|
'health.detail.generic': '{status}',
|
|
256
264
|
'tabs.overview': 'Overview',
|
|
265
|
+
'tabs.notify': 'Notifications',
|
|
257
266
|
'tabs.health': 'Health',
|
|
258
267
|
'tabs.usage': 'Models',
|
|
259
268
|
'overview.container': 'Container info',
|
|
@@ -308,6 +317,8 @@ window.__ModuleLoader__.load({
|
|
|
308
317
|
'update.checking': 'Checking…',
|
|
309
318
|
'update.current': 'Up to date',
|
|
310
319
|
'update.available': 'New version: {version}',
|
|
320
|
+
'update.detailsButton': 'View details',
|
|
321
|
+
'update.detailsHide': 'Collapse',
|
|
311
322
|
'update.unavailable': 'Latest version is temporarily unavailable',
|
|
312
323
|
'update.unpublished': 'No published version is available to check',
|
|
313
324
|
'health.recheck': 'Run again',
|
|
@@ -315,7 +326,8 @@ window.__ModuleLoader__.load({
|
|
|
315
326
|
'update.details.title': 'DSH update available',
|
|
316
327
|
'update.details.current': 'Current version: {version}',
|
|
317
328
|
'update.details.latest': 'Latest version: {version}',
|
|
318
|
-
'update.
|
|
329
|
+
'update.channelStable': 'Stable',
|
|
330
|
+
'update.channelPreview': 'Preview',
|
|
319
331
|
'update.details.close': 'Close',
|
|
320
332
|
'update.upgrade': 'Upgrade plugin',
|
|
321
333
|
'update.upgrading': 'Upgrading…',
|
|
@@ -348,6 +360,8 @@ window.__ModuleLoader__.load({
|
|
|
348
360
|
'restart.sent': 'Restart request sent.',
|
|
349
361
|
'restart.sentHint': 'The connection will close shortly. The page will reload automatically after recovery.',
|
|
350
362
|
'restart.idleHint': 'No active work was detected. Confirm to disconnect and wait for the service to restart.',
|
|
363
|
+
'restart.navToggle': 'Show "Restart" entry in settings left nav',
|
|
364
|
+
'restart.navToggleHint': 'Off by default; when enabled, a quick-restart entry appears at the bottom of the settings left navigation',
|
|
351
365
|
'activity.agent': 'Agent',
|
|
352
366
|
'activity.job': 'Background job',
|
|
353
367
|
'activity.terminal': 'Terminal',
|
|
@@ -456,6 +470,36 @@ window.__ModuleLoader__.load({
|
|
|
456
470
|
}, [])
|
|
457
471
|
return { enabled, done, input, setEnabled: (v) => setNotifyEnabled(v), setDone: (v) => setNotifyDone(v), setInput: (v) => setNotifyInput(v) }
|
|
458
472
|
}
|
|
473
|
+
// 设置页左列「重启」入口显示开关:默认关闭,localStorage 持久化;
|
|
474
|
+
// 开启时才注册 settings.section 条目(导航列单元格由外壳渲染,开关不生效就不能用 null 内容占位)。
|
|
475
|
+
let restartNavEnabled = false
|
|
476
|
+
try { restartNavEnabled = localStorage.getItem('dsh-service-restart-nav') === 'true' } catch (_) {}
|
|
477
|
+
let restartNavDispose = null
|
|
478
|
+
const syncRestartNavEntry = () => {
|
|
479
|
+
if (restartNavDispose) { restartNavDispose(); restartNavDispose = null }
|
|
480
|
+
if (!restartNavEnabled) return
|
|
481
|
+
restartNavDispose = ctx.slots.register(
|
|
482
|
+
{ name: 'settings.section', id: 'dsh-service-restart', order: 499, label: () => t('nav.restart') },
|
|
483
|
+
() => React.createElement(RestartSection, null),
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
const restartNavListeners = new Set()
|
|
487
|
+
const setRestartNavEnabled = (value) => {
|
|
488
|
+
restartNavEnabled = value === true
|
|
489
|
+
try { localStorage.setItem('dsh-service-restart-nav', restartNavEnabled ? 'true' : 'false') } catch (_) {}
|
|
490
|
+
syncRestartNavEntry()
|
|
491
|
+
for (const listener of restartNavListeners) listener()
|
|
492
|
+
}
|
|
493
|
+
const useRestartNavEnabled = () => {
|
|
494
|
+
const [enabled, setEnabled] = useState(restartNavEnabled)
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
const update = () => setEnabled(restartNavEnabled)
|
|
497
|
+
restartNavListeners.add(update)
|
|
498
|
+
setEnabled(restartNavEnabled)
|
|
499
|
+
return () => restartNavListeners.delete(update)
|
|
500
|
+
}, [])
|
|
501
|
+
return [enabled, setRestartNavEnabled]
|
|
502
|
+
}
|
|
459
503
|
// 会话边沿通知:running→idle 记一次任务结束;pendingInteraction 出现记一次需要确认。
|
|
460
504
|
// 数据源是客户端运行时的会话列表快照(订阅推送);首个快照只建立基线,重连后重建基线,二者都不响铃。
|
|
461
505
|
const NOTIFY_KIND_KEYS = {
|
|
@@ -466,7 +510,15 @@ window.__ModuleLoader__.load({
|
|
|
466
510
|
const notifyPermissionGranted = () => typeof Notification !== 'undefined' && Notification.permission === 'granted'
|
|
467
511
|
const fireNotification = (title, body) => {
|
|
468
512
|
if (!notifyPermissionGranted()) return
|
|
469
|
-
try {
|
|
513
|
+
try {
|
|
514
|
+
const notification = new Notification(title, { body })
|
|
515
|
+
// 点击系统通知弹窗:聚焦 DSH 页面并关闭该通知(chrome/ff 从通知点击回调
|
|
516
|
+
// 视为用户手势,window.focus() 可把标签页带到前台)
|
|
517
|
+
notification.onclick = () => {
|
|
518
|
+
try { window.focus() } catch (_) {}
|
|
519
|
+
try { notification.close() } catch (_) {}
|
|
520
|
+
}
|
|
521
|
+
} catch (_) {}
|
|
470
522
|
}
|
|
471
523
|
if (ctx.sessions && typeof ctx.sessions.list?.subscribe === 'function') {
|
|
472
524
|
const observed = new Map()
|
|
@@ -540,6 +592,60 @@ window.__ModuleLoader__.load({
|
|
|
540
592
|
return snapshot
|
|
541
593
|
}
|
|
542
594
|
|
|
595
|
+
// 重启流程共享状态:设置面板「重启」标签与专属设置页共用同一份 stage/activity/busy/error,
|
|
596
|
+
// 触发路径(两段式确认、强制重启、恢复浮层)完全一致。
|
|
597
|
+
const restartFlowListeners = new Set()
|
|
598
|
+
let restartFlow = { stage: 0, activity: null, busy: false, error: null }
|
|
599
|
+
const setRestartFlow = (next) => {
|
|
600
|
+
restartFlow = next
|
|
601
|
+
for (const listener of restartFlowListeners) listener(next)
|
|
602
|
+
}
|
|
603
|
+
const useRestartFlow = () => {
|
|
604
|
+
const [snapshot, setSnapshot] = useState(restartFlow)
|
|
605
|
+
useEffect(() => {
|
|
606
|
+
restartFlowListeners.add(setSnapshot)
|
|
607
|
+
setSnapshot(restartFlow)
|
|
608
|
+
return () => restartFlowListeners.delete(setSnapshot)
|
|
609
|
+
}, [])
|
|
610
|
+
return snapshot
|
|
611
|
+
}
|
|
612
|
+
const checkRestart = async () => {
|
|
613
|
+
setRestartFlow({ ...restartFlow, busy: true, error: null })
|
|
614
|
+
try {
|
|
615
|
+
const res = await ctx.connection.rpc.call('/dsh-service', 'activity', {})
|
|
616
|
+
if (res && res.ok === false) {
|
|
617
|
+
console.error('dsh-service: activity check failed', res.error)
|
|
618
|
+
throw new Error(t('error.activity'))
|
|
619
|
+
}
|
|
620
|
+
const nextActivity = res && res.value ? res.value : { hasActive: false, items: [] }
|
|
621
|
+
setRestartFlow({ ...restartFlow, activity: nextActivity, stage: nextActivity.hasActive ? 3 : 1, busy: false, error: null })
|
|
622
|
+
} catch (err) {
|
|
623
|
+
setRestartFlow({ ...restartFlow, error: err && err.message ? String(err.message) : String(err), stage: 0, busy: false })
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const restartWeb = async (force) => {
|
|
627
|
+
setRestartFlow({ ...restartFlow, busy: true, error: null })
|
|
628
|
+
try {
|
|
629
|
+
const res = await ctx.connection.rpc.call('/dsh-service', 'web', { force: force === true })
|
|
630
|
+
if (res && res.ok === false) {
|
|
631
|
+
if (res.error === 'active-work' && res.value) {
|
|
632
|
+
setRestartFlow({ ...restartFlow, activity: res.value, stage: 3, busy: false, error: null })
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
console.error('dsh-service: restart failed', res.error)
|
|
636
|
+
throw new Error(t('error.restart'))
|
|
637
|
+
}
|
|
638
|
+
const previousInstanceId = res && res.value ? res.value.instanceId : undefined
|
|
639
|
+
if (typeof previousInstanceId !== 'string' || previousInstanceId.length === 0) {
|
|
640
|
+
throw new Error(t('error.instance'))
|
|
641
|
+
}
|
|
642
|
+
setRestartFlow({ ...restartFlow, stage: 2, busy: false, error: null })
|
|
643
|
+
startRecovery(previousInstanceId).catch((err) => console.error('dsh-service: recovery failed', err))
|
|
644
|
+
} catch (err) {
|
|
645
|
+
setRestartFlow({ ...restartFlow, error: err && err.message ? String(err.message) : String(err), stage: 0, busy: false })
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
543
649
|
const useRecoveryState = () => {
|
|
544
650
|
const [snapshot, setSnapshot] = useState(recoveryState)
|
|
545
651
|
useEffect(() => {
|
|
@@ -587,6 +693,7 @@ window.__ModuleLoader__.load({
|
|
|
587
693
|
ctx.effect(() => () => {
|
|
588
694
|
recoveryGeneration += 1
|
|
589
695
|
recoveryListeners.clear()
|
|
696
|
+
restartFlowListeners.clear()
|
|
590
697
|
updateListeners.clear()
|
|
591
698
|
}, 'dsh-service recovery')
|
|
592
699
|
|
|
@@ -601,6 +708,27 @@ window.__ModuleLoader__.load({
|
|
|
601
708
|
}, translate('update.badge'))
|
|
602
709
|
}
|
|
603
710
|
|
|
711
|
+
// 正式/预览通道行:版本号后跟 npmjs(版本页)与 npmmirror(镜像版本页)两个文字链接。
|
|
712
|
+
// 版本串嵌进 URL 前过安全字符集校验,不过校验的标签降级为纯文本。供更新详情浮层使用。
|
|
713
|
+
const NPM_DSH_PACKAGE = '@deepseek-ai/dsh'
|
|
714
|
+
const packageVersionHref = (base, version) => {
|
|
715
|
+
if (typeof version !== 'string' || version.length === 0 || !/^[0-9A-Za-z.+_-]+$/.test(version)) return null
|
|
716
|
+
return `${base}${version}`
|
|
717
|
+
}
|
|
718
|
+
const siteLabelLink = (kind, label, href) => {
|
|
719
|
+
const testid = `version-dsh-channel-${kind}-${label}`
|
|
720
|
+
if (!href) return React.createElement('span', { 'data-testid': testid, style: { marginLeft: '10px' } }, label)
|
|
721
|
+
return React.createElement('a', { 'data-testid': testid, href, target: '_blank', rel: 'noreferrer', style: { color: 'var(--dsw-alias-brand-primary)', textDecoration: 'underline', marginLeft: '10px' } }, label)
|
|
722
|
+
}
|
|
723
|
+
const channelLine = (translate, kind, version) => React.createElement('div', { style: { whiteSpace: 'nowrap', lineHeight: 1.7 } },
|
|
724
|
+
kind === 'latest' ? translate('update.channelStable') : translate('update.channelPreview'),
|
|
725
|
+
' ', React.createElement('span', { 'data-testid': `version-dsh-channel-${kind}`, style: { marginLeft: '4px' } }, version || '—'),
|
|
726
|
+
siteLabelLink(kind, 'npmjs', packageVersionHref(`https://www.npmjs.com/package/${NPM_DSH_PACKAGE}/v/`, version)),
|
|
727
|
+
siteLabelLink(kind, 'npmmirror', packageVersionHref(`https://www.npmmirror.com/package/${NPM_DSH_PACKAGE}/home?version=`, version)))
|
|
728
|
+
const channelLines = (translate, tags) => React.createElement('div', { style: { margin: '4px 0', fontSize: '12px', lineHeight: 1.7, color: 'var(--dsw-alias-label-secondary)', display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
729
|
+
channelLine(translate, 'latest', tags && tags.latest),
|
|
730
|
+
channelLine(translate, 'next', tags && tags.next))
|
|
731
|
+
|
|
604
732
|
function ServiceOverlay() {
|
|
605
733
|
const recovery = useRecoveryState()
|
|
606
734
|
const updateState = useUpdateState()
|
|
@@ -617,7 +745,7 @@ window.__ModuleLoader__.load({
|
|
|
617
745
|
React.createElement('div', { style: { fontSize: '18px', fontWeight: 700, marginBottom: '10px' } }, translate('update.details.title')),
|
|
618
746
|
React.createElement('p', { style: { margin: '4px 0', fontSize: '13px' } }, translate('update.details.current', { version: update.current })),
|
|
619
747
|
React.createElement('p', { style: { margin: '4px 0', fontSize: '13px' } }, translate('update.details.latest', { version: update.latest })),
|
|
620
|
-
|
|
748
|
+
channelLines(translate, update.tags),
|
|
621
749
|
React.createElement('button', { style: { marginTop: '16px', padding: '7px 16px', borderRadius: '6px', border: 0, background: 'var(--dsw-alias-brand-primary)', color: 'var(--dsw-alias-button-contrast-fill)', cursor: 'pointer' }, onClick: () => setUpdateDetailsOpen(false) }, translate('update.details.close'))))
|
|
622
750
|
}
|
|
623
751
|
|
|
@@ -666,6 +794,89 @@ window.__ModuleLoader__.load({
|
|
|
666
794
|
return React.createElement(ServiceOverlay, null)
|
|
667
795
|
}
|
|
668
796
|
|
|
797
|
+
// 重启区块:设置面板「重启」标签与设置页左列底部的专属入口共用,状态取自共享重启流。
|
|
798
|
+
// showNavToggle 仅「重启」标签传入:控制设置页左列入口是否显示(默认关闭)。
|
|
799
|
+
function RestartSection({ showNavToggle }) {
|
|
800
|
+
const translate = useTranslation()
|
|
801
|
+
const flow = useRestartFlow()
|
|
802
|
+
const [navEnabled, setNavEnabled] = useRestartNavEnabled()
|
|
803
|
+
const btn = { minHeight: '32px', padding: '6px 14px', borderRadius: '7px', border: '1px solid transparent', cursor: 'pointer', fontSize: '13px', fontWeight: 550, transition: 'border-color 120ms ease, color 120ms ease, background 120ms ease', lineHeight: '20px' }
|
|
804
|
+
const danger = { ...btn, background: 'var(--dsw-alias-state-error-primary)', color: '#fff', borderColor: 'var(--dsw-alias-state-error-primary)' }
|
|
805
|
+
const ghost = { ...btn, background: 'transparent', color: 'var(--dsw-alias-label-primary)', borderColor: 'var(--dsw-alias-border-l2)' }
|
|
806
|
+
const row = { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '12px', flexWrap: 'wrap' }
|
|
807
|
+
const hint = { color: 'var(--dsw-alias-label-secondary)', fontSize: '12px', marginTop: '8px', lineHeight: 1.5 }
|
|
808
|
+
const card = { padding: '4px 0 14px', marginBottom: '12px', color: 'var(--dsw-alias-label-primary)' }
|
|
809
|
+
const displaySurface = { background: 'var(--dsh-svc-surface-bg)', color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px', padding: '10px' }
|
|
810
|
+
const sectionTitle = { fontSize: '14px', fontWeight: 700, margin: '0 0 8px', color: 'var(--dsw-alias-label-primary)' }
|
|
811
|
+
|
|
812
|
+
// 重启后提示
|
|
813
|
+
if (flow.stage === 2) {
|
|
814
|
+
return React.createElement('div', { 'data-testid': 'restart-card', style: card },
|
|
815
|
+
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
816
|
+
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.sent')),
|
|
817
|
+
React.createElement('p', { style: hint }, translate('restart.sentHint')))
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const activityLabels = {
|
|
821
|
+
agent: translate('activity.agent'),
|
|
822
|
+
job: translate('activity.job'),
|
|
823
|
+
terminal: translate('activity.terminal'),
|
|
824
|
+
}
|
|
825
|
+
const activityItems = flow.activity && Array.isArray(flow.activity.items) ? flow.activity.items : []
|
|
826
|
+
const activityWarning = flow.stage === 3
|
|
827
|
+
? React.createElement('div', { style: { marginTop: '12px', padding: '10px 12px', borderRadius: '6px', background: 'rgba(211,51,51,0.1)', border: '1px solid rgba(211,51,51,0.35)' } },
|
|
828
|
+
React.createElement('p', { style: { margin: '0 0 8px', color: 'var(--dsw-alias-state-error-primary)', fontSize: '13px', fontWeight: 600 } },
|
|
829
|
+
translate('activity.warning', { count: activityItems.length })),
|
|
830
|
+
React.createElement('ul', { style: { margin: 0, paddingLeft: '20px', fontSize: '12px', lineHeight: 1.7 } },
|
|
831
|
+
activityItems.map((item) => React.createElement('li', { key: item.type + ':' + item.id },
|
|
832
|
+
translate('activity.item', {
|
|
833
|
+
type: activityLabels[item.type] || item.type,
|
|
834
|
+
label: item.label,
|
|
835
|
+
status: item.status,
|
|
836
|
+
}))))
|
|
837
|
+
)
|
|
838
|
+
: null
|
|
839
|
+
|
|
840
|
+
// 重启按钮区块
|
|
841
|
+
return React.createElement('div', { 'data-testid': 'restart-card', style: card },
|
|
842
|
+
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
843
|
+
React.createElement('div', { 'data-testid': 'restart-region', style: displaySurface },
|
|
844
|
+
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.description')),
|
|
845
|
+
activityWarning,
|
|
846
|
+
React.createElement('div', { style: row },
|
|
847
|
+
flow.stage === 0
|
|
848
|
+
? React.createElement('button', { style: danger, 'data-variant': 'danger', onClick: checkRestart, disabled: flow.busy }, translate(flow.busy ? 'update.checking' : 'restart.button'))
|
|
849
|
+
: flow.stage === 1
|
|
850
|
+
? [
|
|
851
|
+
React.createElement('button', { key: 'confirm', style: danger, onClick: () => restartWeb(false), disabled: flow.busy }, translate(flow.busy ? 'restart.sending' : 'restart.confirm')),
|
|
852
|
+
React.createElement('button', { key: 'cancel', style: ghost, onClick: () => setRestartFlow({ ...restartFlow, activity: null, stage: 0, busy: false, error: null }), disabled: flow.busy }, translate('restart.cancel')),
|
|
853
|
+
]
|
|
854
|
+
: flow.stage === 3
|
|
855
|
+
? [
|
|
856
|
+
React.createElement('button', { key: 'force', style: danger, onClick: () => restartWeb(true), disabled: flow.busy }, translate(flow.busy ? 'restart.sending' : 'restart.force')),
|
|
857
|
+
React.createElement('button', { key: 'cancel', style: ghost, onClick: () => setRestartFlow({ ...restartFlow, activity: null, stage: 0, busy: false, error: null }), disabled: flow.busy }, translate('restart.cancel')),
|
|
858
|
+
]
|
|
859
|
+
: null
|
|
860
|
+
),
|
|
861
|
+
flow.stage === 1 ? React.createElement('p', { style: hint }, translate('restart.idleHint')) : null,
|
|
862
|
+
flow.error ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, String(flow.error)) : null,
|
|
863
|
+
showNavToggle
|
|
864
|
+
? React.createElement('div', { style: { marginTop: '12px', paddingTop: '10px', borderTop: '1px solid var(--dsw-alias-border-l1)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px' } },
|
|
865
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
866
|
+
React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, translate('restart.navToggle')),
|
|
867
|
+
React.createElement('span', { style: hint }, translate('restart.navToggleHint'))),
|
|
868
|
+
React.createElement('button', {
|
|
869
|
+
type: 'button',
|
|
870
|
+
role: 'switch',
|
|
871
|
+
'data-testid': 'restart-nav-switch',
|
|
872
|
+
'aria-checked': String(navEnabled),
|
|
873
|
+
onClick: () => setNavEnabled(!navEnabled),
|
|
874
|
+
style: { width: '34px', height: '20px', borderRadius: '10px', padding: 0, flexShrink: 0, position: 'relative', border: `1px solid ${navEnabled ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-border-l2)'}`, background: navEnabled ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-layer-2)', cursor: 'pointer', lineHeight: 0 },
|
|
875
|
+
}, React.createElement('span', { style: { position: 'absolute', top: '1px', left: navEnabled ? '15px' : '1px', width: '16px', height: '16px', borderRadius: '50%', background: navEnabled ? '#fff' : 'var(--dsw-alias-label-tertiary)' } })))
|
|
876
|
+
: null)
|
|
877
|
+
)
|
|
878
|
+
}
|
|
879
|
+
|
|
669
880
|
function ServicePanel() {
|
|
670
881
|
const translate = useTranslation()
|
|
671
882
|
const [health, setHealth] = useState(null)
|
|
@@ -703,11 +914,10 @@ window.__ModuleLoader__.load({
|
|
|
703
914
|
const [toolErrorsOpen, setToolErrorsOpen] = useState(false)
|
|
704
915
|
const [modelsOpen, setModelsOpen] = useState(false)
|
|
705
916
|
const [activeTab, setActiveTab] = useState('overview')
|
|
706
|
-
//
|
|
707
|
-
const [
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
const [error, setError] = useState(null)
|
|
917
|
+
// 版本详情行内展开(不用浮层:弹层会被设置模态盖住)
|
|
918
|
+
const [channelOpen, setChannelOpen] = useState(false)
|
|
919
|
+
// 重启流程状态来自共享流(与设置页左列底部的专属入口同源)
|
|
920
|
+
const restartFlowState = useRestartFlow()
|
|
711
921
|
const usageRequestPayload = { timezoneOffsetMinutes: new Date().getTimezoneOffset() }
|
|
712
922
|
|
|
713
923
|
// 进入面板时拉取当前版本和健康快照;健康数据每 5 秒刷新,卸载即停止。
|
|
@@ -994,54 +1204,6 @@ window.__ModuleLoader__.load({
|
|
|
994
1204
|
}
|
|
995
1205
|
}
|
|
996
1206
|
|
|
997
|
-
const checkRestart = async () => {
|
|
998
|
-
setBusy(true)
|
|
999
|
-
setError(null)
|
|
1000
|
-
try {
|
|
1001
|
-
const res = await ctx.connection.rpc.call('/dsh-service', 'activity', {})
|
|
1002
|
-
if (res && res.ok === false) {
|
|
1003
|
-
console.error('dsh-service: activity check failed', res.error)
|
|
1004
|
-
throw new Error(translate('error.activity'))
|
|
1005
|
-
}
|
|
1006
|
-
const nextActivity = res && res.value ? res.value : { hasActive: false, items: [] }
|
|
1007
|
-
setActivity(nextActivity)
|
|
1008
|
-
setStage(nextActivity.hasActive ? 3 : 1)
|
|
1009
|
-
} catch (err) {
|
|
1010
|
-
setError(err && err.message ? String(err.message) : String(err))
|
|
1011
|
-
setStage(0)
|
|
1012
|
-
} finally {
|
|
1013
|
-
setBusy(false)
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
const restart = async (force) => {
|
|
1018
|
-
setBusy(true)
|
|
1019
|
-
setError(null)
|
|
1020
|
-
try {
|
|
1021
|
-
const res = await ctx.connection.rpc.call('/dsh-service', 'web', { force: force === true })
|
|
1022
|
-
if (res && res.ok === false) {
|
|
1023
|
-
if (res.error === 'active-work' && res.value) {
|
|
1024
|
-
setActivity(res.value)
|
|
1025
|
-
setStage(3)
|
|
1026
|
-
return
|
|
1027
|
-
}
|
|
1028
|
-
console.error('dsh-service: restart failed', res.error)
|
|
1029
|
-
throw new Error(translate('error.restart'))
|
|
1030
|
-
}
|
|
1031
|
-
setStage(2)
|
|
1032
|
-
const previousInstanceId = res && res.value ? res.value.instanceId : undefined
|
|
1033
|
-
if (typeof previousInstanceId !== 'string' || previousInstanceId.length === 0) {
|
|
1034
|
-
throw new Error(translate('error.instance'))
|
|
1035
|
-
}
|
|
1036
|
-
startRecovery(previousInstanceId).catch((err) => console.error('dsh-service: recovery failed', err))
|
|
1037
|
-
} catch (err) {
|
|
1038
|
-
setError(err && err.message ? String(err.message) : String(err))
|
|
1039
|
-
setStage(0)
|
|
1040
|
-
} finally {
|
|
1041
|
-
setBusy(false)
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
1207
|
// 样式
|
|
1046
1208
|
const btn = { minHeight: '32px', padding: '6px 14px', borderRadius: '7px', border: '1px solid transparent', cursor: 'pointer', fontSize: '13px', fontWeight: 550, transition: 'border-color 120ms ease, color 120ms ease, background 120ms ease', lineHeight: '20px' }
|
|
1047
1209
|
const primary = { ...btn, background: 'var(--dsw-alias-brand-primary)', color: 'var(--dsw-alias-button-contrast-fill)', borderColor: 'var(--dsw-alias-brand-primary)' }
|
|
@@ -1394,21 +1556,36 @@ window.__ModuleLoader__.load({
|
|
|
1394
1556
|
: null)))
|
|
1395
1557
|
: null))
|
|
1396
1558
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1559
|
+
// 正式/预览通道两行信息在版本卡内下拉展开(不弹浮层:弹层会被设置模态盖住)。
|
|
1560
|
+
// 有更新时状态文本本身可点击:小三角 + 「有新版本:…」整体切换展开/收起。
|
|
1561
|
+
const chevronIcon = (open) => React.createElement('svg', { xmlns: 'http://www.w3.org/2000/svg', width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 3, strokeLinecap: 'round', strokeLinejoin: 'round', style: { display: 'block', transition: 'transform 150ms ease', transform: open ? 'rotate(90deg)' : 'none' } },
|
|
1562
|
+
React.createElement('path', { d: 'M9 6l6 6-6 6' }))
|
|
1563
|
+
const versionRow = (id, label, fallbackVersion, state, action, expandable, topBorder) => {
|
|
1564
|
+
const statusText = !state
|
|
1565
|
+
? (updateError || translate('update.checking'))
|
|
1566
|
+
: state.status === 'unpublished' ? translate('update.unpublished')
|
|
1567
|
+
: state.status === 'unavailable' ? translate('update.unavailable')
|
|
1568
|
+
: state.upToDate ? translate('update.current')
|
|
1569
|
+
: translate('update.available', { version: state.latest })
|
|
1570
|
+
const statusColor = !state ? 'var(--dsw-alias-label-secondary)' : state.upToDate ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warn-primary)'
|
|
1571
|
+
const clickable = expandable && state && state.status !== 'unpublished' && state.status !== 'unavailable' && !state.upToDate
|
|
1572
|
+
const rightSide = clickable
|
|
1573
|
+
? React.createElement('button', { type: 'button', title: translate(channelOpen ? 'update.detailsHide' : 'update.detailsButton'), style: { background: 'transparent', border: 0, padding: 0, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: '5px', fontSize: '12px', fontWeight: 600, color: statusColor }, onClick: () => setChannelOpen((value) => !value) },
|
|
1574
|
+
chevronIcon(channelOpen),
|
|
1575
|
+
React.createElement('span', null, statusText))
|
|
1576
|
+
: React.createElement('div', { style: { color: statusColor, fontWeight: 600 } }, statusText)
|
|
1577
|
+
return React.createElement('div', { key: id, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '16px', padding: '10px 2px', borderTop: topBorder ? '1px solid var(--dsw-alias-border-l1)' : 0 } },
|
|
1578
|
+
React.createElement('div', { style: { whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1579
|
+
React.createElement('span', { style: { fontSize: '13px', fontWeight: 650 } }, `${label} `),
|
|
1580
|
+
state?.url
|
|
1581
|
+
? React.createElement('a', { 'data-testid': `version-${id}-link`, href: state.url, target: '_blank', rel: 'noreferrer', style: { color: 'var(--dsw-alias-label-primary)', textDecoration: 'underline', fontSize: '12px', whiteSpace: 'nowrap', marginLeft: '16px' } }, state.current || fallbackVersion || translate('version.loading'))
|
|
1582
|
+
: React.createElement('code', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary)', marginLeft: '16px' } }, state?.current || fallbackVersion || translate('version.loading'))),
|
|
1404
1583
|
action || null,
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
: state.upToDate ? translate('update.current') : translate('update.available', { version: state.latest }))))
|
|
1411
|
-
// 版本信息区块
|
|
1584
|
+
React.createElement('div', { style: { textAlign: 'right', fontSize: '12px' } }, rightSide))
|
|
1585
|
+
}
|
|
1586
|
+
// 版本信息区块:DSH 行在有更新时状态文本(小三角 + 有新版本)整体可点击,行内下拉展开
|
|
1587
|
+
const dshUpdate = updateInfo?.dsh
|
|
1588
|
+
const dshExpandable = dshUpdate && dshUpdate.status !== 'unpublished' && dshUpdate.status !== 'unavailable' && !dshUpdate.upToDate
|
|
1412
1589
|
const pluginUpdate = updateInfo?.plugin && !updateInfo.plugin.upToDate && updateInfo.plugin.status === 'available'
|
|
1413
1590
|
const pluginAction = pluginUpdate
|
|
1414
1591
|
? React.createElement('button', { style: Object.assign({}, neutral, { minHeight: '24px', padding: '2px 8px', fontSize: '11px' }), disabled: upgradeBusy, onClick: upgradePlugin }, translate(upgradeBusy ? 'update.upgrading' : 'update.upgrade'))
|
|
@@ -1416,67 +1593,18 @@ window.__ModuleLoader__.load({
|
|
|
1416
1593
|
const versionBlock = React.createElement('div', { key: 'version-card', 'data-testid': 'version-card', style: card },
|
|
1417
1594
|
React.createElement('div', { key: 'title', style: sectionTitle }, translate('version.title')),
|
|
1418
1595
|
React.createElement('div', { style: displaySurface },
|
|
1419
|
-
versionRow('
|
|
1420
|
-
versionRow('
|
|
1596
|
+
versionRow('plugin', 'dsh-service', pluginVersion, updateInfo?.plugin, pluginAction, false, false),
|
|
1597
|
+
versionRow('dsh', 'DSH', version, dshUpdate, null, dshExpandable === true, true),
|
|
1598
|
+
channelOpen
|
|
1599
|
+
? React.createElement('div', { 'data-testid': 'version-channel-details', style: { marginTop: '6px', paddingTop: '8px', borderTop: '1px solid var(--dsw-alias-border-l1)', fontSize: '12px', lineHeight: 1.7, color: 'var(--dsw-alias-label-secondary)', display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
1600
|
+
React.createElement('div', null, translate('update.details.current', { version: dshUpdate?.current || version || '—' })),
|
|
1601
|
+
React.createElement('div', null, translate('update.details.latest', { version: dshUpdate?.latest || '—' })),
|
|
1602
|
+
channelLines(translate, dshUpdate?.tags))
|
|
1603
|
+
: null,
|
|
1421
1604
|
upgradeError ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)', margin: '4px 0 0' }) }, upgradeError) : null))
|
|
1422
1605
|
|
|
1423
|
-
//
|
|
1424
|
-
|
|
1425
|
-
return React.createElement('div', null,
|
|
1426
|
-
healthBlock,
|
|
1427
|
-
backupBlock,
|
|
1428
|
-
versionBlock,
|
|
1429
|
-
React.createElement('div', { key: 'restart-section' },
|
|
1430
|
-
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
1431
|
-
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.sent')),
|
|
1432
|
-
React.createElement('p', { style: hint }, translate('restart.sentHint')))
|
|
1433
|
-
)
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
const activityLabels = {
|
|
1437
|
-
agent: translate('activity.agent'),
|
|
1438
|
-
job: translate('activity.job'),
|
|
1439
|
-
terminal: translate('activity.terminal'),
|
|
1440
|
-
}
|
|
1441
|
-
const activityItems = activity && Array.isArray(activity.items) ? activity.items : []
|
|
1442
|
-
const activityWarning = stage === 3
|
|
1443
|
-
? React.createElement('div', { style: { marginTop: '12px', padding: '10px 12px', borderRadius: '6px', background: 'rgba(211,51,51,0.1)', border: '1px solid rgba(211,51,51,0.35)' } },
|
|
1444
|
-
React.createElement('p', { style: { margin: '0 0 8px', color: 'var(--dsw-alias-state-error-primary)', fontSize: '13px', fontWeight: 600 } },
|
|
1445
|
-
translate('activity.warning', { count: activityItems.length })),
|
|
1446
|
-
React.createElement('ul', { style: { margin: 0, paddingLeft: '20px', fontSize: '12px', lineHeight: 1.7 } },
|
|
1447
|
-
activityItems.map((item) => React.createElement('li', { key: item.type + ':' + item.id },
|
|
1448
|
-
translate('activity.item', {
|
|
1449
|
-
type: activityLabels[item.type] || item.type,
|
|
1450
|
-
label: item.label,
|
|
1451
|
-
status: item.status,
|
|
1452
|
-
}))))
|
|
1453
|
-
)
|
|
1454
|
-
: null
|
|
1455
|
-
|
|
1456
|
-
// 重启按钮区块
|
|
1457
|
-
const restartBlock = React.createElement('div', { key: 'restart-section', 'data-testid': 'restart-card', style: card },
|
|
1458
|
-
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
1459
|
-
React.createElement('div', { 'data-testid': 'restart-region', style: displaySurface },
|
|
1460
|
-
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.description')),
|
|
1461
|
-
activityWarning,
|
|
1462
|
-
React.createElement('div', { style: row },
|
|
1463
|
-
stage === 0
|
|
1464
|
-
? React.createElement('button', { style: danger, 'data-variant': 'danger', onClick: checkRestart, disabled: busy }, translate(busy ? 'update.checking' : 'restart.button'))
|
|
1465
|
-
: stage === 1
|
|
1466
|
-
? [
|
|
1467
|
-
React.createElement('button', { key: 'confirm', style: danger, onClick: () => restart(false), disabled: busy }, translate(busy ? 'restart.sending' : 'restart.confirm')),
|
|
1468
|
-
React.createElement('button', { key: 'cancel', style: ghost, onClick: () => { setActivity(null); setStage(0) }, disabled: busy }, translate('restart.cancel')),
|
|
1469
|
-
]
|
|
1470
|
-
: stage === 3
|
|
1471
|
-
? [
|
|
1472
|
-
React.createElement('button', { key: 'force', style: danger, onClick: () => restart(true), disabled: busy }, translate(busy ? 'restart.sending' : 'restart.force')),
|
|
1473
|
-
React.createElement('button', { key: 'cancel', style: ghost, onClick: () => { setActivity(null); setStage(0) }, disabled: busy }, translate('restart.cancel')),
|
|
1474
|
-
]
|
|
1475
|
-
: null
|
|
1476
|
-
),
|
|
1477
|
-
stage === 1 ? React.createElement('p', { style: hint }, translate('restart.idleHint')) : null,
|
|
1478
|
-
error ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, String(error)) : null)
|
|
1479
|
-
)
|
|
1606
|
+
// 重启区块复用共享组件(「重启」标签还承载设置页左列入口的显示开关;左侧入口默认关闭)
|
|
1607
|
+
const restartBlock = React.createElement(RestartSection, { showNavToggle: true })
|
|
1480
1608
|
|
|
1481
1609
|
const { enabled: notifyOn, done: notifyDoneOn, input: notifyInputOn, setEnabled: setNotifyOn, setDone: setNotifyDoneOn, setInput: setNotifyInputOn } = useNotifyState()
|
|
1482
1610
|
const notifSupported = typeof Notification !== 'undefined'
|
|
@@ -1512,18 +1640,22 @@ window.__ModuleLoader__.load({
|
|
|
1512
1640
|
notifyRow(translate('notification.master'), translate('notification.masterHint'), notifyOn, setNotifyOn, false),
|
|
1513
1641
|
notifyRow(translate('notification.done'), translate('notification.doneHint'), notifyDoneOn, setNotifyDoneOn, !notifyOn),
|
|
1514
1642
|
notifyRow(translate('notification.input'), translate('notification.inputHint'), notifyInputOn, setNotifyInputOn, !notifyOn))))
|
|
1515
|
-
const overviewBlock = React.createElement('div', null, versionBlock,
|
|
1643
|
+
const overviewBlock = React.createElement('div', null, versionBlock, containerInfoBlock, overviewErrorsBlock)
|
|
1644
|
+
// 任务通知独立成顶部标签(v0.14 起不再混在概览里)
|
|
1645
|
+
const notifyBlock = React.createElement('div', null, notificationBlock)
|
|
1516
1646
|
const maintenanceBlock = React.createElement('div', { key: 'maintenance-card', 'data-testid': 'maintenance-card', style: card }, backupBlock)
|
|
1517
1647
|
const diagnosticFailure = diagnostics?.checks?.some((check) => check.status === 'error' || (check.status === 'warning' && !(check.id === 'backup-storage' && String(check.detail || '').startsWith('0:')))) === true
|
|
1518
1648
|
const tabWarnings = {
|
|
1519
1649
|
overview: false,
|
|
1650
|
+
notify: false,
|
|
1520
1651
|
health: Boolean(healthError || permissionError || diagnosticFailure || permissionAbnormal > 0),
|
|
1521
1652
|
usage: Boolean(usageError),
|
|
1522
1653
|
backup: Boolean(backupError),
|
|
1523
|
-
restart: Boolean(error),
|
|
1654
|
+
restart: Boolean(restartFlowState.error),
|
|
1524
1655
|
}
|
|
1525
1656
|
const tabs = [
|
|
1526
1657
|
['overview', 'tabs.overview'],
|
|
1658
|
+
['notify', 'tabs.notify'],
|
|
1527
1659
|
['health', 'tabs.health'],
|
|
1528
1660
|
['usage', 'tabs.usage'],
|
|
1529
1661
|
['backup', 'tabs.backup'],
|
|
@@ -1532,13 +1664,15 @@ window.__ModuleLoader__.load({
|
|
|
1532
1664
|
const warningTabs = tabs.filter(([id]) => tabWarnings[id]).map(([, label]) => translate(label))
|
|
1533
1665
|
const tabContent = activeTab === 'overview'
|
|
1534
1666
|
? overviewBlock
|
|
1535
|
-
: activeTab === '
|
|
1536
|
-
?
|
|
1537
|
-
: activeTab === '
|
|
1538
|
-
?
|
|
1539
|
-
: activeTab === '
|
|
1540
|
-
?
|
|
1541
|
-
:
|
|
1667
|
+
: activeTab === 'notify'
|
|
1668
|
+
? notifyBlock
|
|
1669
|
+
: activeTab === 'health'
|
|
1670
|
+
? healthBlock
|
|
1671
|
+
: activeTab === 'usage'
|
|
1672
|
+
? usageBlock
|
|
1673
|
+
: activeTab === 'backup'
|
|
1674
|
+
? maintenanceBlock
|
|
1675
|
+
: restartBlock
|
|
1542
1676
|
return React.createElement('div', null,
|
|
1543
1677
|
warningTabs.length > 0 ? React.createElement('div', { style: { marginBottom: '12px', padding: '11px 13px', borderRadius: '8px', background: 'rgba(198,128,0,0.16)', border: '1px solid rgba(198,128,0,0.48)', boxShadow: '0 2px 8px rgba(0,0,0,0.08)' } },
|
|
1544
1678
|
React.createElement('div', { style: { fontSize: '13px', fontWeight: 700 } }, translate('tabs.alert.title')),
|
|
@@ -1581,10 +1715,21 @@ window.__ModuleLoader__.load({
|
|
|
1581
1715
|
{ name: 'sidebar.footer.action', id: 'dsh-service-update', order: 90, label: () => t('update.badge') },
|
|
1582
1716
|
() => React.createElement(UpdateBadge, null),
|
|
1583
1717
|
))
|
|
1584
|
-
ctx.slots.inject('settings.section', () =>
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1718
|
+
ctx.slots.inject('settings.section', () => {
|
|
1719
|
+
const disposePanel = ctx.slots.register(
|
|
1720
|
+
{ name: 'settings.section', id: 'dsh-service', order: 99, label: () => t('nav.label') },
|
|
1721
|
+
() => React.createElement(ServicePanel, null),
|
|
1722
|
+
)
|
|
1723
|
+
// 左列「重启」入口由「重启」标签内的开关控制,默认不注册
|
|
1724
|
+
syncRestartNavEntry()
|
|
1725
|
+
return () => {
|
|
1726
|
+
disposePanel()
|
|
1727
|
+
if (restartNavDispose) {
|
|
1728
|
+
restartNavDispose()
|
|
1729
|
+
restartNavDispose = null
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
})
|
|
1588
1733
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register(
|
|
1589
1734
|
{ name: 'shell.overlay', id: 'dsh-service-restart', order: 100, label: () => t('overlay.label') },
|
|
1590
1735
|
() => React.createElement(RestartOverlay, null),
|