@gehennawu/dsh-service 0.13.2 → 0.15.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 +8 -3
- package/README.md +8 -3
- package/client.js +332 -172
- package/index.js +9 -11
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -8,10 +8,12 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
+
The Settings panel "Service Control" page has six top-level tabs: **Overview, Notifications, Health, Model stats, Backups, Restart**; the Restart tab can also enable a `Restart` quick entry at the bottom of the settings left navigation (off by default).
|
|
12
|
+
|
|
11
13
|
### Version and updates
|
|
12
14
|
|
|
13
15
|
- Displays current DSH and plugin versions with links to GitHub Releases
|
|
14
|
-
- Automatically checks npm registry for stable and preview releases,
|
|
16
|
+
- 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
17
|
- One-click plugin upgrade with automatic restart after completion
|
|
16
18
|
|
|
17
19
|
### Safe restart
|
|
@@ -19,6 +21,7 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
19
21
|
- Detects active agents, background jobs, and terminals before restart; lists them and requires explicit confirmation
|
|
20
22
|
- `/restart` command also available in conversations; automatically refuses when active work is detected
|
|
21
23
|
- Automatically probes for the new process after restart and reloads the page; manual reload available after 60 seconds
|
|
24
|
+
- 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
25
|
|
|
23
26
|
### Health diagnostics
|
|
24
27
|
|
|
@@ -30,7 +33,8 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
30
33
|
|
|
31
34
|
- 7-day stacked bar chart of input/output/cache tokens with blue/orange/teal legend
|
|
32
35
|
- Filter by project; hover for exact values
|
|
33
|
-
- Model breakdown sorted by
|
|
36
|
+
- Model breakdown as horizontal stacked bars (same legend colors), sorted by total tokens, largest first; each row keeps `x times · Cache hit x% · Input xM token · Output xM token`
|
|
37
|
+
- Steps whose provider reports no token usage are excluded from the statistics
|
|
34
38
|
- Last-24-hour model/tool error statistics, collapsed by default
|
|
35
39
|
|
|
36
40
|
### Backup management
|
|
@@ -43,7 +47,8 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
43
47
|
|
|
44
48
|
### Task notifications
|
|
45
49
|
|
|
46
|
-
-
|
|
50
|
+
- Notification settings live in the top-level Notifications tab: browser notification when a session finishes its turn, or when your approval, plan review, or answer is needed
|
|
51
|
+
- Clicking a notification focuses the DSH page and closes the popup
|
|
47
52
|
- Three toggle switches: master switch, task completion, and approvals & questions, each controlled independently
|
|
48
53
|
- Bell icon in the conversation input bar toggles the master switch quickly
|
|
49
54
|
- All toggles persist across page reloads
|
package/README.md
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
|
+
设置页「服务控制」面板包含六个顶部标签:**概览、通知、健康诊断、模型统计、备份维护、重启**;重启标签还可在设置页左侧标签列底部开启「重启」快捷入口(默认关闭)。
|
|
12
|
+
|
|
11
13
|
### 版本与更新
|
|
12
14
|
|
|
13
15
|
- 显示当前 DSH 和插件版本,版本号链接到 GitHub Releases
|
|
14
|
-
- 自动检查 npm registry
|
|
16
|
+
- 自动检查 npm registry 的正式版和预览版;有新版本时右侧「小三角 + 有新版本」状态文本可点击展开/收起,行内展示当前/最新版本与正式版/预览版双 tag,版本号分别带 npmjs 与 npmmirror 链接(部分网络会被 npmjs.com 拦截,npmmirror 作为镜像入口)
|
|
15
17
|
- 一键升级插件,升级后自动重启
|
|
16
18
|
|
|
17
19
|
### 安全重启
|
|
@@ -19,6 +21,7 @@
|
|
|
19
21
|
- 重启前检测活跃 Agent、后台任务和终端,展示清单并要求显式确认
|
|
20
22
|
- 对话中输入 `/restart` 也可触发,检测到运行中工作时自动拒绝
|
|
21
23
|
- 重启后自动探测新进程并刷新页面,60 秒未恢复时提供手动刷新
|
|
24
|
+
- 可在「重启」标签开启「设置页左列显示入口」开关(默认关闭),开启后在设置页左侧标签列底部显示「重启」快捷入口,与「重启」标签共用同一套确认流程
|
|
22
25
|
|
|
23
26
|
### 健康诊断
|
|
24
27
|
|
|
@@ -30,7 +33,8 @@
|
|
|
30
33
|
|
|
31
34
|
- 近 7 天输入/输出/缓存 token 堆叠柱图,蓝/橙/青图例
|
|
32
35
|
- 按项目筛选,鼠标悬停显示精确数值
|
|
33
|
-
-
|
|
36
|
+
- 模型明细为横向堆叠柱形图(沿用主图图例配色),按 token 总量从多到少排序;每行附「x次 · 缓存命中 x% · 输入 xM token · 输出 xM token」明细
|
|
37
|
+
- 提供方未上报 token 用量的模型步骤不纳入统计
|
|
34
38
|
- 最近 24 小时模型/工具报错统计,默认折叠
|
|
35
39
|
|
|
36
40
|
### 备份管理
|
|
@@ -43,7 +47,8 @@
|
|
|
43
47
|
|
|
44
48
|
### 任务通知
|
|
45
49
|
|
|
46
|
-
-
|
|
50
|
+
- 通知设置位于服务控制顶部的「通知」标签:会话完成一轮任务、或需要你授权/审阅计划/选择答案时发送浏览器通知
|
|
51
|
+
- 点击系统通知弹窗会聚焦到 DSH 页面并关闭该通知
|
|
47
52
|
- 三档开关条:总开关、任务结束通知、授权与提问通知,各自独立控制
|
|
48
53
|
- 对话栏内铃铛图标快速切换总开关
|
|
49
54
|
- 各开关在页面刷新后保持
|
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': '终端',
|
|
@@ -165,11 +172,14 @@ window.__ModuleLoader__.load({
|
|
|
165
172
|
'usage.axis': 'token 纵轴',
|
|
166
173
|
'usage.models.more': '展开其余 {count} 个模型',
|
|
167
174
|
'usage.models.less': '收起模型列表',
|
|
175
|
+
'usage.modelSortHint': '按 token 总量从多到少排列',
|
|
176
|
+
'usage.modelBar': '{model}:共 {total} token',
|
|
168
177
|
'usage.refresh': '刷新统计',
|
|
169
178
|
'usage.refreshing': '刷新中…',
|
|
170
179
|
'usage.empty': '尚未建立使用统计索引。点击刷新统计开始只读建立索引。',
|
|
171
180
|
'usage.error': '无法读取模型使用统计',
|
|
172
181
|
'usage.allProjects': '全部项目',
|
|
182
|
+
'usage.total': 'token 总量',
|
|
173
183
|
'usage.steps': '成功模型步骤',
|
|
174
184
|
'usage.stepsValue': '{count} 次',
|
|
175
185
|
'usage.modelLine': '{steps}次 · 缓存命中 {hitRate} · 输入 {input} token · 输出 {output} token',
|
|
@@ -179,7 +189,6 @@ window.__ModuleLoader__.load({
|
|
|
179
189
|
'usage.hitRate': '缓存命中率',
|
|
180
190
|
'usage.today': '今天',
|
|
181
191
|
'usage.sevenDays': '近 7 天',
|
|
182
|
-
'usage.missing': '{count} 个步骤没有 token 数据',
|
|
183
192
|
'usage.errors.title': '模型报错',
|
|
184
193
|
'usage.errors.toggle': '模型报错({count} 类)',
|
|
185
194
|
'usage.errors.recent': '最近 24 小时',
|
|
@@ -189,17 +198,14 @@ window.__ModuleLoader__.load({
|
|
|
189
198
|
'usage.toolErrors.empty': '最近 24 小时没有记录到工具报错。',
|
|
190
199
|
'usage.errors.count': '{count} 次',
|
|
191
200
|
'notification.title': '通知',
|
|
192
|
-
'notification.description': '
|
|
201
|
+
'notification.description': '任务结束或需要你授权、抉择时发送浏览器通知。需要授权浏览器通知权限。',
|
|
193
202
|
'notification.enable': '开启通知',
|
|
194
203
|
'notification.enabled': '通知已开启',
|
|
195
204
|
'notification.disable': '关闭通知',
|
|
196
205
|
'notification.denied': '通知权限被拒绝',
|
|
197
206
|
'notification.master': '通知总开关',
|
|
198
|
-
'notification.masterHint': '关闭时下面两个开关暂停生效',
|
|
199
207
|
'notification.done': '任务结束通知',
|
|
200
|
-
'notification.doneHint': '会话完成一轮任务时提醒',
|
|
201
208
|
'notification.input': '授权与提问通知',
|
|
202
|
-
'notification.inputHint': '需要授权、审阅计划或选择答案时提醒',
|
|
203
209
|
'notification.doneTitle': '任务完成',
|
|
204
210
|
'notification.doneBody': '{title} 已完成本轮任务',
|
|
205
211
|
'notification.inputTitle': '需要你的确认',
|
|
@@ -212,6 +218,7 @@ window.__ModuleLoader__.load({
|
|
|
212
218
|
}
|
|
213
219
|
const en = {
|
|
214
220
|
'nav.label': 'Service Control',
|
|
221
|
+
'nav.restart': 'Restart',
|
|
215
222
|
'overlay.label': 'Service restart status',
|
|
216
223
|
'recovery.waiting.title': 'Restarting service…',
|
|
217
224
|
'recovery.waiting.body': 'Waiting for a new DSH Web process. Elapsed: {seconds} seconds.',
|
|
@@ -254,6 +261,7 @@ window.__ModuleLoader__.load({
|
|
|
254
261
|
'health.detail.permissions.warning': 'Found {count} file or directory permission anomalies',
|
|
255
262
|
'health.detail.generic': '{status}',
|
|
256
263
|
'tabs.overview': 'Overview',
|
|
264
|
+
'tabs.notify': 'Notifications',
|
|
257
265
|
'tabs.health': 'Health',
|
|
258
266
|
'tabs.usage': 'Models',
|
|
259
267
|
'overview.container': 'Container info',
|
|
@@ -308,6 +316,8 @@ window.__ModuleLoader__.load({
|
|
|
308
316
|
'update.checking': 'Checking…',
|
|
309
317
|
'update.current': 'Up to date',
|
|
310
318
|
'update.available': 'New version: {version}',
|
|
319
|
+
'update.detailsButton': 'View details',
|
|
320
|
+
'update.detailsHide': 'Collapse',
|
|
311
321
|
'update.unavailable': 'Latest version is temporarily unavailable',
|
|
312
322
|
'update.unpublished': 'No published version is available to check',
|
|
313
323
|
'health.recheck': 'Run again',
|
|
@@ -315,7 +325,8 @@ window.__ModuleLoader__.load({
|
|
|
315
325
|
'update.details.title': 'DSH update available',
|
|
316
326
|
'update.details.current': 'Current version: {version}',
|
|
317
327
|
'update.details.latest': 'Latest version: {version}',
|
|
318
|
-
'update.
|
|
328
|
+
'update.channelStable': 'Stable',
|
|
329
|
+
'update.channelPreview': 'Preview',
|
|
319
330
|
'update.details.close': 'Close',
|
|
320
331
|
'update.upgrade': 'Upgrade plugin',
|
|
321
332
|
'update.upgrading': 'Upgrading…',
|
|
@@ -348,6 +359,8 @@ window.__ModuleLoader__.load({
|
|
|
348
359
|
'restart.sent': 'Restart request sent.',
|
|
349
360
|
'restart.sentHint': 'The connection will close shortly. The page will reload automatically after recovery.',
|
|
350
361
|
'restart.idleHint': 'No active work was detected. Confirm to disconnect and wait for the service to restart.',
|
|
362
|
+
'restart.navToggle': 'Show "Restart" entry in settings left nav',
|
|
363
|
+
'restart.navToggleHint': 'Off by default; when enabled, a quick-restart entry appears at the bottom of the settings left navigation',
|
|
351
364
|
'activity.agent': 'Agent',
|
|
352
365
|
'activity.job': 'Background job',
|
|
353
366
|
'activity.terminal': 'Terminal',
|
|
@@ -367,11 +380,14 @@ window.__ModuleLoader__.load({
|
|
|
367
380
|
'usage.axis': 'token vertical axis',
|
|
368
381
|
'usage.models.more': 'Show {count} more models',
|
|
369
382
|
'usage.models.less': 'Collapse model list',
|
|
383
|
+
'usage.modelSortHint': 'Sorted by total tokens, largest first',
|
|
384
|
+
'usage.modelBar': '{model}: {total} tokens in total',
|
|
370
385
|
'usage.refresh': 'Refresh usage',
|
|
371
386
|
'usage.refreshing': 'Refreshing…',
|
|
372
387
|
'usage.empty': 'No usage index yet. Select Refresh usage to build the read-only index.',
|
|
373
388
|
'usage.error': 'Could not read model usage',
|
|
374
389
|
'usage.allProjects': 'All projects',
|
|
390
|
+
'usage.total': 'Total tokens',
|
|
375
391
|
'usage.steps': 'Successful model steps',
|
|
376
392
|
'usage.stepsValue': '{count} times',
|
|
377
393
|
'usage.modelLine': '{steps} times · Cache hit {hitRate} · Input {input} token · Output {output} token',
|
|
@@ -381,7 +397,6 @@ window.__ModuleLoader__.load({
|
|
|
381
397
|
'usage.hitRate': 'Cache hit rate',
|
|
382
398
|
'usage.today': 'Today',
|
|
383
399
|
'usage.sevenDays': 'Last 7 days',
|
|
384
|
-
'usage.missing': '{count} step(s) have no token data',
|
|
385
400
|
'usage.errors.title': 'Model errors',
|
|
386
401
|
'usage.errors.toggle': 'Model errors ({count} types)',
|
|
387
402
|
'usage.errors.recent': 'Last 24 hours',
|
|
@@ -391,17 +406,14 @@ window.__ModuleLoader__.load({
|
|
|
391
406
|
'usage.toolErrors.empty': 'No tool errors were recorded in the last 24 hours.',
|
|
392
407
|
'usage.errors.count': '{count} occurrence(s)',
|
|
393
408
|
'notification.title': 'Notifications',
|
|
394
|
-
'notification.description': 'Send a browser notification when a task finishes or when your approval or
|
|
409
|
+
'notification.description': 'Send a browser notification when a task finishes or when your approval or decision is needed. Requires browser notification permission.',
|
|
395
410
|
'notification.enable': 'Enable notifications',
|
|
396
411
|
'notification.enabled': 'Notifications enabled',
|
|
397
412
|
'notification.disable': 'Disable notifications',
|
|
398
413
|
'notification.denied': 'Notification permission denied',
|
|
399
414
|
'notification.master': 'Master switch',
|
|
400
|
-
'notification.masterHint': 'Both kinds below stay paused while this is off',
|
|
401
415
|
'notification.done': 'Task completion',
|
|
402
|
-
'notification.doneHint': 'Notify when a session finishes its turn',
|
|
403
416
|
'notification.input': 'Approvals & questions',
|
|
404
|
-
'notification.inputHint': 'Notify when approval, plan review, or an answer is requested',
|
|
405
417
|
'notification.doneTitle': 'Task complete',
|
|
406
418
|
'notification.doneBody': '{title} has finished its turn',
|
|
407
419
|
'notification.inputTitle': 'Your attention needed',
|
|
@@ -456,6 +468,36 @@ window.__ModuleLoader__.load({
|
|
|
456
468
|
}, [])
|
|
457
469
|
return { enabled, done, input, setEnabled: (v) => setNotifyEnabled(v), setDone: (v) => setNotifyDone(v), setInput: (v) => setNotifyInput(v) }
|
|
458
470
|
}
|
|
471
|
+
// 设置页左列「重启」入口显示开关:默认关闭,localStorage 持久化;
|
|
472
|
+
// 开启时才注册 settings.section 条目(导航列单元格由外壳渲染,开关不生效就不能用 null 内容占位)。
|
|
473
|
+
let restartNavEnabled = false
|
|
474
|
+
try { restartNavEnabled = localStorage.getItem('dsh-service-restart-nav') === 'true' } catch (_) {}
|
|
475
|
+
let restartNavDispose = null
|
|
476
|
+
const syncRestartNavEntry = () => {
|
|
477
|
+
if (restartNavDispose) { restartNavDispose(); restartNavDispose = null }
|
|
478
|
+
if (!restartNavEnabled) return
|
|
479
|
+
restartNavDispose = ctx.slots.register(
|
|
480
|
+
{ name: 'settings.section', id: 'dsh-service-restart', order: 499, label: () => t('nav.restart') },
|
|
481
|
+
() => React.createElement(RestartSection, null),
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
const restartNavListeners = new Set()
|
|
485
|
+
const setRestartNavEnabled = (value) => {
|
|
486
|
+
restartNavEnabled = value === true
|
|
487
|
+
try { localStorage.setItem('dsh-service-restart-nav', restartNavEnabled ? 'true' : 'false') } catch (_) {}
|
|
488
|
+
syncRestartNavEntry()
|
|
489
|
+
for (const listener of restartNavListeners) listener()
|
|
490
|
+
}
|
|
491
|
+
const useRestartNavEnabled = () => {
|
|
492
|
+
const [enabled, setEnabled] = useState(restartNavEnabled)
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
const update = () => setEnabled(restartNavEnabled)
|
|
495
|
+
restartNavListeners.add(update)
|
|
496
|
+
setEnabled(restartNavEnabled)
|
|
497
|
+
return () => restartNavListeners.delete(update)
|
|
498
|
+
}, [])
|
|
499
|
+
return [enabled, setRestartNavEnabled]
|
|
500
|
+
}
|
|
459
501
|
// 会话边沿通知:running→idle 记一次任务结束;pendingInteraction 出现记一次需要确认。
|
|
460
502
|
// 数据源是客户端运行时的会话列表快照(订阅推送);首个快照只建立基线,重连后重建基线,二者都不响铃。
|
|
461
503
|
const NOTIFY_KIND_KEYS = {
|
|
@@ -466,7 +508,15 @@ window.__ModuleLoader__.load({
|
|
|
466
508
|
const notifyPermissionGranted = () => typeof Notification !== 'undefined' && Notification.permission === 'granted'
|
|
467
509
|
const fireNotification = (title, body) => {
|
|
468
510
|
if (!notifyPermissionGranted()) return
|
|
469
|
-
try {
|
|
511
|
+
try {
|
|
512
|
+
const notification = new Notification(title, { body })
|
|
513
|
+
// 点击系统通知弹窗:聚焦 DSH 页面并关闭该通知(chrome/ff 从通知点击回调
|
|
514
|
+
// 视为用户手势,window.focus() 可把标签页带到前台)
|
|
515
|
+
notification.onclick = () => {
|
|
516
|
+
try { window.focus() } catch (_) {}
|
|
517
|
+
try { notification.close() } catch (_) {}
|
|
518
|
+
}
|
|
519
|
+
} catch (_) {}
|
|
470
520
|
}
|
|
471
521
|
if (ctx.sessions && typeof ctx.sessions.list?.subscribe === 'function') {
|
|
472
522
|
const observed = new Map()
|
|
@@ -540,6 +590,60 @@ window.__ModuleLoader__.load({
|
|
|
540
590
|
return snapshot
|
|
541
591
|
}
|
|
542
592
|
|
|
593
|
+
// 重启流程共享状态:设置面板「重启」标签与专属设置页共用同一份 stage/activity/busy/error,
|
|
594
|
+
// 触发路径(两段式确认、强制重启、恢复浮层)完全一致。
|
|
595
|
+
const restartFlowListeners = new Set()
|
|
596
|
+
let restartFlow = { stage: 0, activity: null, busy: false, error: null }
|
|
597
|
+
const setRestartFlow = (next) => {
|
|
598
|
+
restartFlow = next
|
|
599
|
+
for (const listener of restartFlowListeners) listener(next)
|
|
600
|
+
}
|
|
601
|
+
const useRestartFlow = () => {
|
|
602
|
+
const [snapshot, setSnapshot] = useState(restartFlow)
|
|
603
|
+
useEffect(() => {
|
|
604
|
+
restartFlowListeners.add(setSnapshot)
|
|
605
|
+
setSnapshot(restartFlow)
|
|
606
|
+
return () => restartFlowListeners.delete(setSnapshot)
|
|
607
|
+
}, [])
|
|
608
|
+
return snapshot
|
|
609
|
+
}
|
|
610
|
+
const checkRestart = async () => {
|
|
611
|
+
setRestartFlow({ ...restartFlow, busy: true, error: null })
|
|
612
|
+
try {
|
|
613
|
+
const res = await ctx.connection.rpc.call('/dsh-service', 'activity', {})
|
|
614
|
+
if (res && res.ok === false) {
|
|
615
|
+
console.error('dsh-service: activity check failed', res.error)
|
|
616
|
+
throw new Error(t('error.activity'))
|
|
617
|
+
}
|
|
618
|
+
const nextActivity = res && res.value ? res.value : { hasActive: false, items: [] }
|
|
619
|
+
setRestartFlow({ ...restartFlow, activity: nextActivity, stage: nextActivity.hasActive ? 3 : 1, busy: false, error: null })
|
|
620
|
+
} catch (err) {
|
|
621
|
+
setRestartFlow({ ...restartFlow, error: err && err.message ? String(err.message) : String(err), stage: 0, busy: false })
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const restartWeb = async (force) => {
|
|
625
|
+
setRestartFlow({ ...restartFlow, busy: true, error: null })
|
|
626
|
+
try {
|
|
627
|
+
const res = await ctx.connection.rpc.call('/dsh-service', 'web', { force: force === true })
|
|
628
|
+
if (res && res.ok === false) {
|
|
629
|
+
if (res.error === 'active-work' && res.value) {
|
|
630
|
+
setRestartFlow({ ...restartFlow, activity: res.value, stage: 3, busy: false, error: null })
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
console.error('dsh-service: restart failed', res.error)
|
|
634
|
+
throw new Error(t('error.restart'))
|
|
635
|
+
}
|
|
636
|
+
const previousInstanceId = res && res.value ? res.value.instanceId : undefined
|
|
637
|
+
if (typeof previousInstanceId !== 'string' || previousInstanceId.length === 0) {
|
|
638
|
+
throw new Error(t('error.instance'))
|
|
639
|
+
}
|
|
640
|
+
setRestartFlow({ ...restartFlow, stage: 2, busy: false, error: null })
|
|
641
|
+
startRecovery(previousInstanceId).catch((err) => console.error('dsh-service: recovery failed', err))
|
|
642
|
+
} catch (err) {
|
|
643
|
+
setRestartFlow({ ...restartFlow, error: err && err.message ? String(err.message) : String(err), stage: 0, busy: false })
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
543
647
|
const useRecoveryState = () => {
|
|
544
648
|
const [snapshot, setSnapshot] = useState(recoveryState)
|
|
545
649
|
useEffect(() => {
|
|
@@ -587,6 +691,7 @@ window.__ModuleLoader__.load({
|
|
|
587
691
|
ctx.effect(() => () => {
|
|
588
692
|
recoveryGeneration += 1
|
|
589
693
|
recoveryListeners.clear()
|
|
694
|
+
restartFlowListeners.clear()
|
|
590
695
|
updateListeners.clear()
|
|
591
696
|
}, 'dsh-service recovery')
|
|
592
697
|
|
|
@@ -601,6 +706,27 @@ window.__ModuleLoader__.load({
|
|
|
601
706
|
}, translate('update.badge'))
|
|
602
707
|
}
|
|
603
708
|
|
|
709
|
+
// 正式/预览通道行:版本号后跟 npmjs(版本页)与 npmmirror(镜像版本页)两个文字链接。
|
|
710
|
+
// 版本串嵌进 URL 前过安全字符集校验,不过校验的标签降级为纯文本。供更新详情浮层使用。
|
|
711
|
+
const NPM_DSH_PACKAGE = '@deepseek-ai/dsh'
|
|
712
|
+
const packageVersionHref = (base, version) => {
|
|
713
|
+
if (typeof version !== 'string' || version.length === 0 || !/^[0-9A-Za-z.+_-]+$/.test(version)) return null
|
|
714
|
+
return `${base}${version}`
|
|
715
|
+
}
|
|
716
|
+
const siteLabelLink = (kind, label, href) => {
|
|
717
|
+
const testid = `version-dsh-channel-${kind}-${label}`
|
|
718
|
+
if (!href) return React.createElement('span', { 'data-testid': testid, style: { marginLeft: '10px' } }, label)
|
|
719
|
+
return React.createElement('a', { 'data-testid': testid, href, target: '_blank', rel: 'noreferrer', style: { color: 'var(--dsw-alias-brand-primary)', textDecoration: 'underline', marginLeft: '10px' } }, label)
|
|
720
|
+
}
|
|
721
|
+
const channelLine = (translate, kind, version) => React.createElement('div', { style: { whiteSpace: 'nowrap', lineHeight: 1.7 } },
|
|
722
|
+
kind === 'latest' ? translate('update.channelStable') : translate('update.channelPreview'),
|
|
723
|
+
' ', React.createElement('span', { 'data-testid': `version-dsh-channel-${kind}`, style: { marginLeft: '4px' } }, version || '—'),
|
|
724
|
+
siteLabelLink(kind, 'npmjs', packageVersionHref(`https://www.npmjs.com/package/${NPM_DSH_PACKAGE}/v/`, version)),
|
|
725
|
+
siteLabelLink(kind, 'npmmirror', packageVersionHref(`https://www.npmmirror.com/package/${NPM_DSH_PACKAGE}/home?version=`, version)))
|
|
726
|
+
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' } },
|
|
727
|
+
channelLine(translate, 'latest', tags && tags.latest),
|
|
728
|
+
channelLine(translate, 'next', tags && tags.next))
|
|
729
|
+
|
|
604
730
|
function ServiceOverlay() {
|
|
605
731
|
const recovery = useRecoveryState()
|
|
606
732
|
const updateState = useUpdateState()
|
|
@@ -617,7 +743,7 @@ window.__ModuleLoader__.load({
|
|
|
617
743
|
React.createElement('div', { style: { fontSize: '18px', fontWeight: 700, marginBottom: '10px' } }, translate('update.details.title')),
|
|
618
744
|
React.createElement('p', { style: { margin: '4px 0', fontSize: '13px' } }, translate('update.details.current', { version: update.current })),
|
|
619
745
|
React.createElement('p', { style: { margin: '4px 0', fontSize: '13px' } }, translate('update.details.latest', { version: update.latest })),
|
|
620
|
-
|
|
746
|
+
channelLines(translate, update.tags),
|
|
621
747
|
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
748
|
}
|
|
623
749
|
|
|
@@ -666,6 +792,89 @@ window.__ModuleLoader__.load({
|
|
|
666
792
|
return React.createElement(ServiceOverlay, null)
|
|
667
793
|
}
|
|
668
794
|
|
|
795
|
+
// 重启区块:设置面板「重启」标签与设置页左列底部的专属入口共用,状态取自共享重启流。
|
|
796
|
+
// showNavToggle 仅「重启」标签传入:控制设置页左列入口是否显示(默认关闭)。
|
|
797
|
+
function RestartSection({ showNavToggle }) {
|
|
798
|
+
const translate = useTranslation()
|
|
799
|
+
const flow = useRestartFlow()
|
|
800
|
+
const [navEnabled, setNavEnabled] = useRestartNavEnabled()
|
|
801
|
+
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' }
|
|
802
|
+
const danger = { ...btn, background: 'var(--dsw-alias-state-error-primary)', color: '#fff', borderColor: 'var(--dsw-alias-state-error-primary)' }
|
|
803
|
+
const ghost = { ...btn, background: 'transparent', color: 'var(--dsw-alias-label-primary)', borderColor: 'var(--dsw-alias-border-l2)' }
|
|
804
|
+
const row = { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '12px', flexWrap: 'wrap' }
|
|
805
|
+
const hint = { color: 'var(--dsw-alias-label-secondary)', fontSize: '12px', marginTop: '8px', lineHeight: 1.5 }
|
|
806
|
+
const card = { padding: '4px 0 14px', marginBottom: '12px', color: 'var(--dsw-alias-label-primary)' }
|
|
807
|
+
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' }
|
|
808
|
+
const sectionTitle = { fontSize: '14px', fontWeight: 700, margin: '0 0 8px', color: 'var(--dsw-alias-label-primary)' }
|
|
809
|
+
|
|
810
|
+
// 重启后提示
|
|
811
|
+
if (flow.stage === 2) {
|
|
812
|
+
return React.createElement('div', { 'data-testid': 'restart-card', style: card },
|
|
813
|
+
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
814
|
+
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.sent')),
|
|
815
|
+
React.createElement('p', { style: hint }, translate('restart.sentHint')))
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const activityLabels = {
|
|
819
|
+
agent: translate('activity.agent'),
|
|
820
|
+
job: translate('activity.job'),
|
|
821
|
+
terminal: translate('activity.terminal'),
|
|
822
|
+
}
|
|
823
|
+
const activityItems = flow.activity && Array.isArray(flow.activity.items) ? flow.activity.items : []
|
|
824
|
+
const activityWarning = flow.stage === 3
|
|
825
|
+
? 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)' } },
|
|
826
|
+
React.createElement('p', { style: { margin: '0 0 8px', color: 'var(--dsw-alias-state-error-primary)', fontSize: '13px', fontWeight: 600 } },
|
|
827
|
+
translate('activity.warning', { count: activityItems.length })),
|
|
828
|
+
React.createElement('ul', { style: { margin: 0, paddingLeft: '20px', fontSize: '12px', lineHeight: 1.7 } },
|
|
829
|
+
activityItems.map((item) => React.createElement('li', { key: item.type + ':' + item.id },
|
|
830
|
+
translate('activity.item', {
|
|
831
|
+
type: activityLabels[item.type] || item.type,
|
|
832
|
+
label: item.label,
|
|
833
|
+
status: item.status,
|
|
834
|
+
}))))
|
|
835
|
+
)
|
|
836
|
+
: null
|
|
837
|
+
|
|
838
|
+
// 重启按钮区块
|
|
839
|
+
return React.createElement('div', { 'data-testid': 'restart-card', style: card },
|
|
840
|
+
React.createElement('div', { style: sectionTitle }, translate('restart.title')),
|
|
841
|
+
React.createElement('div', { 'data-testid': 'restart-region', style: displaySurface },
|
|
842
|
+
React.createElement('p', { style: { margin: 0, fontSize: '13px' } }, translate('restart.description')),
|
|
843
|
+
activityWarning,
|
|
844
|
+
React.createElement('div', { style: row },
|
|
845
|
+
flow.stage === 0
|
|
846
|
+
? React.createElement('button', { style: danger, 'data-variant': 'danger', onClick: checkRestart, disabled: flow.busy }, translate(flow.busy ? 'update.checking' : 'restart.button'))
|
|
847
|
+
: flow.stage === 1
|
|
848
|
+
? [
|
|
849
|
+
React.createElement('button', { key: 'confirm', style: danger, onClick: () => restartWeb(false), disabled: flow.busy }, translate(flow.busy ? 'restart.sending' : 'restart.confirm')),
|
|
850
|
+
React.createElement('button', { key: 'cancel', style: ghost, onClick: () => setRestartFlow({ ...restartFlow, activity: null, stage: 0, busy: false, error: null }), disabled: flow.busy }, translate('restart.cancel')),
|
|
851
|
+
]
|
|
852
|
+
: flow.stage === 3
|
|
853
|
+
? [
|
|
854
|
+
React.createElement('button', { key: 'force', style: danger, onClick: () => restartWeb(true), disabled: flow.busy }, translate(flow.busy ? 'restart.sending' : 'restart.force')),
|
|
855
|
+
React.createElement('button', { key: 'cancel', style: ghost, onClick: () => setRestartFlow({ ...restartFlow, activity: null, stage: 0, busy: false, error: null }), disabled: flow.busy }, translate('restart.cancel')),
|
|
856
|
+
]
|
|
857
|
+
: null
|
|
858
|
+
),
|
|
859
|
+
flow.stage === 1 ? React.createElement('p', { style: hint }, translate('restart.idleHint')) : null,
|
|
860
|
+
flow.error ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, String(flow.error)) : null,
|
|
861
|
+
showNavToggle
|
|
862
|
+
? 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' } },
|
|
863
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
864
|
+
React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, translate('restart.navToggle')),
|
|
865
|
+
React.createElement('span', { style: hint }, translate('restart.navToggleHint'))),
|
|
866
|
+
React.createElement('button', {
|
|
867
|
+
type: 'button',
|
|
868
|
+
role: 'switch',
|
|
869
|
+
'data-testid': 'restart-nav-switch',
|
|
870
|
+
'aria-checked': String(navEnabled),
|
|
871
|
+
onClick: () => setNavEnabled(!navEnabled),
|
|
872
|
+
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 },
|
|
873
|
+
}, 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)' } })))
|
|
874
|
+
: null)
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
|
|
669
878
|
function ServicePanel() {
|
|
670
879
|
const translate = useTranslation()
|
|
671
880
|
const [health, setHealth] = useState(null)
|
|
@@ -703,11 +912,10 @@ window.__ModuleLoader__.load({
|
|
|
703
912
|
const [toolErrorsOpen, setToolErrorsOpen] = useState(false)
|
|
704
913
|
const [modelsOpen, setModelsOpen] = useState(false)
|
|
705
914
|
const [activeTab, setActiveTab] = useState('overview')
|
|
706
|
-
//
|
|
707
|
-
const [
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
const [error, setError] = useState(null)
|
|
915
|
+
// 版本详情行内展开(不用浮层:弹层会被设置模态盖住)
|
|
916
|
+
const [channelOpen, setChannelOpen] = useState(false)
|
|
917
|
+
// 重启流程状态来自共享流(与设置页左列底部的专属入口同源)
|
|
918
|
+
const restartFlowState = useRestartFlow()
|
|
711
919
|
const usageRequestPayload = { timezoneOffsetMinutes: new Date().getTimezoneOffset() }
|
|
712
920
|
|
|
713
921
|
// 进入面板时拉取当前版本和健康快照;健康数据每 5 秒刷新,卸载即停止。
|
|
@@ -994,54 +1202,6 @@ window.__ModuleLoader__.load({
|
|
|
994
1202
|
}
|
|
995
1203
|
}
|
|
996
1204
|
|
|
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
1205
|
// 样式
|
|
1046
1206
|
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
1207
|
const primary = { ...btn, background: 'var(--dsw-alias-brand-primary)', color: 'var(--dsw-alias-button-contrast-fill)', borderColor: 'var(--dsw-alias-brand-primary)' }
|
|
@@ -1102,15 +1262,19 @@ window.__ModuleLoader__.load({
|
|
|
1102
1262
|
]
|
|
1103
1263
|
const usageTotalsFor = (day) => {
|
|
1104
1264
|
const source = usage && usage.days ? usage.days[day] : null
|
|
1105
|
-
if (!source) return { steps: 0,
|
|
1265
|
+
if (!source) return { steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheHitRate: 0 }
|
|
1106
1266
|
if (usageProject === 'all') return source.totals
|
|
1107
1267
|
const project = source.projects.find((item) => item.id === usageProject)
|
|
1108
|
-
return project ? project.totals : { steps: 0,
|
|
1268
|
+
return project ? project.totals : { steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheHitRate: 0 }
|
|
1109
1269
|
}
|
|
1110
1270
|
const modelCacheHitRate = (model) => {
|
|
1111
1271
|
const denominator = Number(model.inputTokens || 0) + Number(model.cacheReadTokens || 0) + Number(model.cacheWriteTokens || 0)
|
|
1112
1272
|
return denominator === 0 ? 0 : Number(model.cacheReadTokens || 0) / denominator
|
|
1113
1273
|
}
|
|
1274
|
+
const modelTotalTokens = (model) => Number(model.inputTokens || 0) + Number(model.outputTokens || 0) + Number(model.cacheReadTokens || 0) + Number(model.cacheWriteTokens || 0)
|
|
1275
|
+
const modelSegmentValue = (model, metric) => metric === 'cacheTokens'
|
|
1276
|
+
? Number(model.cacheReadTokens || 0) + Number(model.cacheWriteTokens || 0)
|
|
1277
|
+
: Number(model[metric] || 0)
|
|
1114
1278
|
const usageValue = (totals, metricName) => metricName === 'cacheTokens'
|
|
1115
1279
|
? totals.cacheReadTokens + totals.cacheWriteTokens
|
|
1116
1280
|
: totals[metricName] || 0
|
|
@@ -1133,13 +1297,12 @@ window.__ModuleLoader__.load({
|
|
|
1133
1297
|
const sevenTotals = usageDays.reduce((total, day) => {
|
|
1134
1298
|
const source = usageTotalsFor(day.key)
|
|
1135
1299
|
total.steps += source.steps || 0
|
|
1136
|
-
total.missingUsage += source.missingUsage || 0
|
|
1137
1300
|
total.inputTokens += source.inputTokens || 0
|
|
1138
1301
|
total.outputTokens += source.outputTokens || 0
|
|
1139
1302
|
total.cacheReadTokens += source.cacheReadTokens || 0
|
|
1140
1303
|
total.cacheWriteTokens += source.cacheWriteTokens || 0
|
|
1141
1304
|
return total
|
|
1142
|
-
}, { steps: 0,
|
|
1305
|
+
}, { steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 })
|
|
1143
1306
|
const sevenDenominator = sevenTotals.inputTokens + sevenTotals.cacheReadTokens + sevenTotals.cacheWriteTokens
|
|
1144
1307
|
sevenTotals.cacheHitRate = sevenDenominator === 0 ? 0 : sevenTotals.cacheReadTokens / sevenDenominator
|
|
1145
1308
|
const selectedProjects = usageProject === 'all'
|
|
@@ -1178,6 +1341,7 @@ window.__ModuleLoader__.load({
|
|
|
1178
1341
|
return translate('health.detail.generic', { status: translate(`health.status.${check.status}`) })
|
|
1179
1342
|
}
|
|
1180
1343
|
const summaryItems = (totals) => [
|
|
1344
|
+
['usage.total', formatTokenValue(usageValue(totals, 'inputTokens') + usageValue(totals, 'outputTokens') + usageValue(totals, 'cacheTokens'))],
|
|
1181
1345
|
...usageSegments.map(([metricName, label]) => [label, formatTokenValue(usageValue(totals, metricName))]),
|
|
1182
1346
|
['usage.steps', formatUsageValue(totals.steps, 'steps')],
|
|
1183
1347
|
['usage.hitRate', formatUsageValue(totals.cacheHitRate, 'cacheHitRate')],
|
|
@@ -1188,9 +1352,10 @@ window.__ModuleLoader__.load({
|
|
|
1188
1352
|
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, translate(label)),
|
|
1189
1353
|
React.createElement('span', { style: { fontWeight: 650 } }, value)))
|
|
1190
1354
|
)
|
|
1191
|
-
const sortedModels = [...modelTotals.values()].sort((a, b) => b.steps - a.steps || a.id.localeCompare(b.id))
|
|
1355
|
+
const sortedModels = [...modelTotals.values()].sort((a, b) => modelTotalTokens(b) - modelTotalTokens(a) || b.steps - a.steps || a.id.localeCompare(b.id))
|
|
1192
1356
|
const visibleModels = modelsOpen ? sortedModels : sortedModels.slice(0, 3)
|
|
1193
1357
|
const hiddenModelCount = Math.max(0, sortedModels.length - 3)
|
|
1358
|
+
const maxModelTokens = Math.max(1, ...visibleModels.map((model) => modelTotalTokens(model)))
|
|
1194
1359
|
const selectedErrors = (list) => (list || [])
|
|
1195
1360
|
.filter((error) => usageProject === 'all' || error.projectId === usageProject)
|
|
1196
1361
|
.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
|
|
@@ -1249,16 +1414,28 @@ window.__ModuleLoader__.load({
|
|
|
1249
1414
|
React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '8px', marginTop: '10px' } },
|
|
1250
1415
|
summaryBlock('today', 'usage.today', todayTotals),
|
|
1251
1416
|
summaryBlock('seven', 'usage.sevenDays', sevenTotals)),
|
|
1252
|
-
sevenTotals.missingUsage > 0 ? React.createElement('p', { style: hint }, translate('usage.missing', { count: sevenTotals.missingUsage })) : null,
|
|
1253
1417
|
React.createElement('div', { 'data-testid': 'usage-model-list', style: { marginTop: '10px', padding: '8px 10px', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l1)' } },
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1418
|
+
React.createElement('div', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', marginBottom: '4px' } }, translate('usage.modelSortHint')),
|
|
1419
|
+
visibleModels.map((model, index) => {
|
|
1420
|
+
const total = modelTotalTokens(model)
|
|
1421
|
+
const fillWidth = `${Math.round(total / maxModelTokens * 10000) / 100}%`
|
|
1422
|
+
return React.createElement('div', { key: model.id, 'data-testid': `usage-model-row-${model.id}`, style: { padding: '8px 2px', borderTop: index === 0 ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
|
|
1423
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: '12px', alignItems: 'baseline', fontSize: '12px' } },
|
|
1424
|
+
React.createElement('span', { style: { overflowWrap: 'anywhere' } }, model.id),
|
|
1425
|
+
React.createElement('span', { style: { fontWeight: 650, whiteSpace: 'nowrap' } }, formatTokenValue(total))),
|
|
1426
|
+
React.createElement('div', { 'data-testid': `usage-model-bar-${model.id}`, 'data-value': total, 'aria-label': translate('usage.modelBar', { model: model.id, total: formatTokenValue(total) }), style: { height: '8px', borderRadius: '4px', marginTop: '6px', overflow: 'hidden', background: 'var(--dsw-alias-border-l1)' } },
|
|
1427
|
+
React.createElement('div', { style: { display: 'flex', height: '100%', width: fillWidth } },
|
|
1428
|
+
usageSegments.map(([metricName, , color]) => {
|
|
1429
|
+
const value = modelSegmentValue(model, metricName)
|
|
1430
|
+
return value > 0 ? React.createElement('div', { key: metricName, 'data-testid': `usage-model-segment-${model.id}-${metricName}`, style: { width: `${value / total * 100}%`, background: color } }) : null
|
|
1431
|
+
}))),
|
|
1432
|
+
React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', marginTop: '4px', textAlign: 'right' } }, translate('usage.modelLine', {
|
|
1433
|
+
steps: Number(model.steps || 0).toLocaleString(),
|
|
1434
|
+
hitRate: formatUsageValue(modelCacheHitRate(model), 'cacheHitRate'),
|
|
1435
|
+
input: formatTokenValue(model.inputTokens),
|
|
1436
|
+
output: formatTokenValue(model.outputTokens),
|
|
1437
|
+
})))
|
|
1438
|
+
}),
|
|
1262
1439
|
hiddenModelCount > 0 ? React.createElement('button', { style: Object.assign({}, toggle, { borderTop: '1px solid var(--dsw-alias-border-l1)', marginTop: '2px' }), onClick: () => setModelsOpen((value) => !value) }, `${modelsOpen ? '▾' : '▸'} ${translate(modelsOpen ? 'usage.models.less' : 'usage.models.more', { count: hiddenModelCount })}`) : null))
|
|
1263
1440
|
: React.createElement('p', { style: hint }, usageError || translate('usage.empty')),
|
|
1264
1441
|
React.createElement('div', { style: row }, React.createElement('button', { style: neutral, 'data-variant': 'neutral', onClick: refreshUsage, disabled: usageBusy }, translate(usageBusy ? 'usage.refreshing' : 'usage.refresh'))))
|
|
@@ -1394,21 +1571,36 @@ window.__ModuleLoader__.load({
|
|
|
1394
1571
|
: null)))
|
|
1395
1572
|
: null))
|
|
1396
1573
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1574
|
+
// 正式/预览通道两行信息在版本卡内下拉展开(不弹浮层:弹层会被设置模态盖住)。
|
|
1575
|
+
// 有更新时状态文本本身可点击:小三角 + 「有新版本:…」整体切换展开/收起。
|
|
1576
|
+
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' } },
|
|
1577
|
+
React.createElement('path', { d: 'M9 6l6 6-6 6' }))
|
|
1578
|
+
const versionRow = (id, label, fallbackVersion, state, action, expandable, topBorder) => {
|
|
1579
|
+
const statusText = !state
|
|
1580
|
+
? (updateError || translate('update.checking'))
|
|
1581
|
+
: state.status === 'unpublished' ? translate('update.unpublished')
|
|
1582
|
+
: state.status === 'unavailable' ? translate('update.unavailable')
|
|
1583
|
+
: state.upToDate ? translate('update.current')
|
|
1584
|
+
: translate('update.available', { version: state.latest })
|
|
1585
|
+
const statusColor = !state ? 'var(--dsw-alias-label-secondary)' : state.upToDate ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warn-primary)'
|
|
1586
|
+
const clickable = expandable && state && state.status !== 'unpublished' && state.status !== 'unavailable' && !state.upToDate
|
|
1587
|
+
const rightSide = clickable
|
|
1588
|
+
? 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) },
|
|
1589
|
+
chevronIcon(channelOpen),
|
|
1590
|
+
React.createElement('span', null, statusText))
|
|
1591
|
+
: React.createElement('div', { style: { color: statusColor, fontWeight: 600 } }, statusText)
|
|
1592
|
+
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 } },
|
|
1593
|
+
React.createElement('div', { style: { whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1594
|
+
React.createElement('span', { style: { fontSize: '13px', fontWeight: 650 } }, `${label} `),
|
|
1595
|
+
state?.url
|
|
1596
|
+
? 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'))
|
|
1597
|
+
: React.createElement('code', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary)', marginLeft: '16px' } }, state?.current || fallbackVersion || translate('version.loading'))),
|
|
1404
1598
|
action || null,
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
: state.upToDate ? translate('update.current') : translate('update.available', { version: state.latest }))))
|
|
1411
|
-
// 版本信息区块
|
|
1599
|
+
React.createElement('div', { style: { textAlign: 'right', fontSize: '12px' } }, rightSide))
|
|
1600
|
+
}
|
|
1601
|
+
// 版本信息区块:DSH 行在有更新时状态文本(小三角 + 有新版本)整体可点击,行内下拉展开
|
|
1602
|
+
const dshUpdate = updateInfo?.dsh
|
|
1603
|
+
const dshExpandable = dshUpdate && dshUpdate.status !== 'unpublished' && dshUpdate.status !== 'unavailable' && !dshUpdate.upToDate
|
|
1412
1604
|
const pluginUpdate = updateInfo?.plugin && !updateInfo.plugin.upToDate && updateInfo.plugin.status === 'available'
|
|
1413
1605
|
const pluginAction = pluginUpdate
|
|
1414
1606
|
? 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 +1608,18 @@ window.__ModuleLoader__.load({
|
|
|
1416
1608
|
const versionBlock = React.createElement('div', { key: 'version-card', 'data-testid': 'version-card', style: card },
|
|
1417
1609
|
React.createElement('div', { key: 'title', style: sectionTitle }, translate('version.title')),
|
|
1418
1610
|
React.createElement('div', { style: displaySurface },
|
|
1419
|
-
versionRow('
|
|
1420
|
-
versionRow('
|
|
1611
|
+
versionRow('plugin', 'dsh-service', pluginVersion, updateInfo?.plugin, pluginAction, false, false),
|
|
1612
|
+
versionRow('dsh', 'DSH', version, dshUpdate, null, dshExpandable === true, true),
|
|
1613
|
+
channelOpen
|
|
1614
|
+
? 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' } },
|
|
1615
|
+
React.createElement('div', null, translate('update.details.current', { version: dshUpdate?.current || version || '—' })),
|
|
1616
|
+
React.createElement('div', null, translate('update.details.latest', { version: dshUpdate?.latest || '—' })),
|
|
1617
|
+
channelLines(translate, dshUpdate?.tags))
|
|
1618
|
+
: null,
|
|
1421
1619
|
upgradeError ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)', margin: '4px 0 0' }) }, upgradeError) : null))
|
|
1422
1620
|
|
|
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
|
-
)
|
|
1621
|
+
// 重启区块复用共享组件(「重启」标签还承载设置页左列入口的显示开关;左侧入口默认关闭)
|
|
1622
|
+
const restartBlock = React.createElement(RestartSection, { showNavToggle: true })
|
|
1480
1623
|
|
|
1481
1624
|
const { enabled: notifyOn, done: notifyDoneOn, input: notifyInputOn, setEnabled: setNotifyOn, setDone: setNotifyDoneOn, setInput: setNotifyInputOn } = useNotifyState()
|
|
1482
1625
|
const notifSupported = typeof Notification !== 'undefined'
|
|
@@ -1494,10 +1637,10 @@ window.__ModuleLoader__.load({
|
|
|
1494
1637
|
cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.5 : 1, lineHeight: 0,
|
|
1495
1638
|
},
|
|
1496
1639
|
}, React.createElement('span', { style: { position: 'absolute', top: '1px', left: on ? '15px' : '1px', width: '16px', height: '16px', borderRadius: '50%', background: on ? '#fff' : 'var(--dsw-alias-label-tertiary)' } }))
|
|
1497
|
-
const notifyRow = (label, labelHint, on, onChange, disabled) => React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '5px 0' } },
|
|
1640
|
+
const notifyRow = (testId, label, labelHint, on, onChange, disabled) => React.createElement('div', { 'data-testid': testId, style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '5px 0' } },
|
|
1498
1641
|
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
1499
1642
|
React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, label),
|
|
1500
|
-
React.createElement('span', { style: hint }, labelHint)),
|
|
1643
|
+
labelHint ? React.createElement('span', { style: hint }, labelHint) : null),
|
|
1501
1644
|
notifySwitch(on, onChange, disabled))
|
|
1502
1645
|
const notificationBlock = !notifSupported ? null
|
|
1503
1646
|
: React.createElement('div', { style: { marginTop: '18px' } },
|
|
@@ -1509,21 +1652,25 @@ window.__ModuleLoader__.load({
|
|
|
1509
1652
|
React.createElement('button', { style: neutral, onClick: () => { Notification.requestPermission().then((p) => { if (p === 'granted') setNotifyOn(true) }) } }, translate('notification.enable')),
|
|
1510
1653
|
React.createElement('span', { style: hint }, notifPermission === 'denied' ? translate('notification.denied') : ''))
|
|
1511
1654
|
: React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column' } },
|
|
1512
|
-
notifyRow(
|
|
1513
|
-
notifyRow(
|
|
1514
|
-
notifyRow(
|
|
1515
|
-
const overviewBlock = React.createElement('div', null, versionBlock,
|
|
1655
|
+
notifyRow('notify-row-master', translate('notification.master'), null, notifyOn, setNotifyOn, false),
|
|
1656
|
+
notifyRow('notify-row-done', translate('notification.done'), null, notifyDoneOn, setNotifyDoneOn, !notifyOn),
|
|
1657
|
+
notifyRow('notify-row-input', translate('notification.input'), null, notifyInputOn, setNotifyInputOn, !notifyOn))))
|
|
1658
|
+
const overviewBlock = React.createElement('div', null, versionBlock, containerInfoBlock, overviewErrorsBlock)
|
|
1659
|
+
// 任务通知独立成顶部标签(v0.14 起不再混在概览里)
|
|
1660
|
+
const notifyBlock = React.createElement('div', null, notificationBlock)
|
|
1516
1661
|
const maintenanceBlock = React.createElement('div', { key: 'maintenance-card', 'data-testid': 'maintenance-card', style: card }, backupBlock)
|
|
1517
1662
|
const diagnosticFailure = diagnostics?.checks?.some((check) => check.status === 'error' || (check.status === 'warning' && !(check.id === 'backup-storage' && String(check.detail || '').startsWith('0:')))) === true
|
|
1518
1663
|
const tabWarnings = {
|
|
1519
1664
|
overview: false,
|
|
1665
|
+
notify: false,
|
|
1520
1666
|
health: Boolean(healthError || permissionError || diagnosticFailure || permissionAbnormal > 0),
|
|
1521
1667
|
usage: Boolean(usageError),
|
|
1522
1668
|
backup: Boolean(backupError),
|
|
1523
|
-
restart: Boolean(error),
|
|
1669
|
+
restart: Boolean(restartFlowState.error),
|
|
1524
1670
|
}
|
|
1525
1671
|
const tabs = [
|
|
1526
1672
|
['overview', 'tabs.overview'],
|
|
1673
|
+
['notify', 'tabs.notify'],
|
|
1527
1674
|
['health', 'tabs.health'],
|
|
1528
1675
|
['usage', 'tabs.usage'],
|
|
1529
1676
|
['backup', 'tabs.backup'],
|
|
@@ -1532,13 +1679,15 @@ window.__ModuleLoader__.load({
|
|
|
1532
1679
|
const warningTabs = tabs.filter(([id]) => tabWarnings[id]).map(([, label]) => translate(label))
|
|
1533
1680
|
const tabContent = activeTab === 'overview'
|
|
1534
1681
|
? overviewBlock
|
|
1535
|
-
: activeTab === '
|
|
1536
|
-
?
|
|
1537
|
-
: activeTab === '
|
|
1538
|
-
?
|
|
1539
|
-
: activeTab === '
|
|
1540
|
-
?
|
|
1541
|
-
:
|
|
1682
|
+
: activeTab === 'notify'
|
|
1683
|
+
? notifyBlock
|
|
1684
|
+
: activeTab === 'health'
|
|
1685
|
+
? healthBlock
|
|
1686
|
+
: activeTab === 'usage'
|
|
1687
|
+
? usageBlock
|
|
1688
|
+
: activeTab === 'backup'
|
|
1689
|
+
? maintenanceBlock
|
|
1690
|
+
: restartBlock
|
|
1542
1691
|
return React.createElement('div', null,
|
|
1543
1692
|
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
1693
|
React.createElement('div', { style: { fontSize: '13px', fontWeight: 700 } }, translate('tabs.alert.title')),
|
|
@@ -1581,10 +1730,21 @@ window.__ModuleLoader__.load({
|
|
|
1581
1730
|
{ name: 'sidebar.footer.action', id: 'dsh-service-update', order: 90, label: () => t('update.badge') },
|
|
1582
1731
|
() => React.createElement(UpdateBadge, null),
|
|
1583
1732
|
))
|
|
1584
|
-
ctx.slots.inject('settings.section', () =>
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1733
|
+
ctx.slots.inject('settings.section', () => {
|
|
1734
|
+
const disposePanel = ctx.slots.register(
|
|
1735
|
+
{ name: 'settings.section', id: 'dsh-service', order: 99, label: () => t('nav.label') },
|
|
1736
|
+
() => React.createElement(ServicePanel, null),
|
|
1737
|
+
)
|
|
1738
|
+
// 左列「重启」入口由「重启」标签内的开关控制,默认不注册
|
|
1739
|
+
syncRestartNavEntry()
|
|
1740
|
+
return () => {
|
|
1741
|
+
disposePanel()
|
|
1742
|
+
if (restartNavDispose) {
|
|
1743
|
+
restartNavDispose()
|
|
1744
|
+
restartNavDispose = null
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
})
|
|
1588
1748
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register(
|
|
1589
1749
|
{ name: 'shell.overlay', id: 'dsh-service-restart', order: 100, label: () => t('overlay.label') },
|
|
1590
1750
|
() => React.createElement(RestartOverlay, null),
|
package/index.js
CHANGED
|
@@ -21,7 +21,7 @@ const MAX_BACKUP_TRANSFER_BYTES = 128 * 1024 * 1024
|
|
|
21
21
|
const instanceId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
|
|
22
22
|
const backupIdSecret = randomBytes(32)
|
|
23
23
|
const BACKUP_NAME = /^dsh-backup-\d{8}-\d{6}\.tar\.gz$/
|
|
24
|
-
const USAGE_INDEX_VERSION =
|
|
24
|
+
const USAGE_INDEX_VERSION = 5
|
|
25
25
|
const USAGE_INDEX_FILE = 'dsh-service-usage-index.json'
|
|
26
26
|
|
|
27
27
|
// 升级目标白名单:命令与包名全部来自宿主常量,浏览器不传任何输入。
|
|
@@ -376,12 +376,11 @@ function localDayForHour(hour, timezoneOffsetMinutes = 0) {
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
function emptyUsageTotals() {
|
|
379
|
-
return { steps: 0,
|
|
379
|
+
return { steps: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
|
|
380
380
|
}
|
|
381
381
|
|
|
382
382
|
function addUsageTotals(target, source) {
|
|
383
383
|
target.steps += source.steps || 0
|
|
384
|
-
target.missingUsage += source.missingUsage || 0
|
|
385
384
|
target.inputTokens += source.inputTokens || 0
|
|
386
385
|
target.outputTokens += source.outputTokens || 0
|
|
387
386
|
target.cacheReadTokens += source.cacheReadTokens || 0
|
|
@@ -571,19 +570,18 @@ function foldUsageEvents(ctx, record, previous, events) {
|
|
|
571
570
|
if (addToolError(session, event)) continue
|
|
572
571
|
if (addUsageError(session, event, model)) continue
|
|
573
572
|
if (event.type !== 'assistant/message') continue
|
|
573
|
+
// 提供方未上报 token 用量的步骤直接忽略:不计步骤、不建模型桶、不提示。
|
|
574
|
+
if (event.data?.usage === undefined) continue
|
|
574
575
|
const hour = usageHour(event.time)
|
|
575
576
|
const bucket = session.hours[hour] || (session.hours[hour] = { totals: emptyUsageTotals(), models: {} })
|
|
576
577
|
const modelBucket = bucket.models[model.id] || (bucket.models[model.id] = { id: model.id, provider: model.provider, model: model.model, totals: emptyUsageTotals() })
|
|
577
|
-
const usage = event.data
|
|
578
|
+
const usage = event.data.usage
|
|
578
579
|
const delta = emptyUsageTotals()
|
|
579
580
|
delta.steps = 1
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
delta.cacheReadTokens = Number(usage.cacheReadTokens) || 0
|
|
585
|
-
delta.cacheWriteTokens = Number(usage.cacheWriteTokens) || 0
|
|
586
|
-
}
|
|
581
|
+
delta.inputTokens = Number(usage.inputTokens) || 0
|
|
582
|
+
delta.outputTokens = Number(usage.outputTokens) || 0
|
|
583
|
+
delta.cacheReadTokens = Number(usage.cacheReadTokens) || 0
|
|
584
|
+
delta.cacheWriteTokens = Number(usage.cacheWriteTokens) || 0
|
|
587
585
|
addUsageTotals(bucket.totals, delta)
|
|
588
586
|
addUsageTotals(modelBucket.totals, delta)
|
|
589
587
|
}
|