@gehennawu/dsh-service 0.12.1 → 0.13.1
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 +4 -4
- package/README.md +4 -4
- package/client.js +138 -68
- package/index.js +16 -8
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -43,10 +43,10 @@ A service-control and operations plugin for self-hosted DSH Web. Provides safe r
|
|
|
43
43
|
|
|
44
44
|
### Task notifications
|
|
45
45
|
|
|
46
|
-
- Browser notification when
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
46
|
+
- Browser notification when a session finishes its turn, or when your approval, plan review, or answer is needed
|
|
47
|
+
- Three toggle switches: master switch, task completion, and approvals & questions, each controlled independently
|
|
48
|
+
- Bell icon in the conversation input bar toggles the master switch quickly
|
|
49
|
+
- All toggles persist across page reloads
|
|
50
50
|
|
|
51
51
|
### External liveness probe
|
|
52
52
|
|
package/README.md
CHANGED
package/client.js
CHANGED
|
@@ -118,6 +118,7 @@ window.__ModuleLoader__.load({
|
|
|
118
118
|
'update.upgrade': '升级插件',
|
|
119
119
|
'update.upgrading': '升级中…',
|
|
120
120
|
'update.upgradeError': '插件升级失败',
|
|
121
|
+
'update.upgradeErrorDetail': '插件升级失败({detail})',
|
|
121
122
|
'update.upgradeSuccess': '升级成功,服务重启中…',
|
|
122
123
|
'restart.title': '服务重启',
|
|
123
124
|
'restart.description': '重启 dsh web 进程。运行中的工作会中断,持久化会话可恢复。也可在对话中输入 /restart。',
|
|
@@ -171,18 +172,27 @@ window.__ModuleLoader__.load({
|
|
|
171
172
|
'usage.toolErrors.toggle': '工具报错({count} 类)',
|
|
172
173
|
'usage.toolErrors.empty': '最近 24 小时没有记录到工具报错。',
|
|
173
174
|
'usage.errors.count': '{count} 次',
|
|
174
|
-
'notification.title': '
|
|
175
|
-
'notification.description': '
|
|
175
|
+
'notification.title': '通知',
|
|
176
|
+
'notification.description': '任务结束或需要你授权、选择答案时发送浏览器通知。需要授权浏览器通知权限;各开关在页面刷新后保持。',
|
|
176
177
|
'notification.enable': '开启通知',
|
|
177
178
|
'notification.enabled': '通知已开启',
|
|
178
179
|
'notification.disable': '关闭通知',
|
|
179
180
|
'notification.denied': '通知权限被拒绝',
|
|
180
|
-
'notification.
|
|
181
|
-
'notification.
|
|
181
|
+
'notification.master': '通知总开关',
|
|
182
|
+
'notification.masterHint': '关闭时下面两个开关暂停生效',
|
|
183
|
+
'notification.done': '任务结束通知',
|
|
184
|
+
'notification.doneHint': '会话完成一轮任务时提醒',
|
|
185
|
+
'notification.input': '授权与提问通知',
|
|
186
|
+
'notification.inputHint': '需要授权、审阅计划或选择答案时提醒',
|
|
187
|
+
'notification.doneTitle': '任务完成',
|
|
188
|
+
'notification.doneBody': '{title} 已完成本轮任务',
|
|
189
|
+
'notification.inputTitle': '需要你的确认',
|
|
190
|
+
'notification.inputBody': '{title}({kind})',
|
|
191
|
+
'notification.kind.approval': '等待授权',
|
|
192
|
+
'notification.kind.plan-review': '等待审阅计划',
|
|
193
|
+
'notification.kind.question': '等待选择答案',
|
|
182
194
|
'notification.bellOn': '通知开启',
|
|
183
195
|
'notification.bellOff': '通知关闭',
|
|
184
|
-
'notification.interval': '轮询间隔',
|
|
185
|
-
'notification.intervalUnit': '秒(5-300)',
|
|
186
196
|
}
|
|
187
197
|
const en = {
|
|
188
198
|
'nav.label': 'Service Control',
|
|
@@ -294,6 +304,7 @@ window.__ModuleLoader__.load({
|
|
|
294
304
|
'update.upgrade': 'Upgrade plugin',
|
|
295
305
|
'update.upgrading': 'Upgrading…',
|
|
296
306
|
'update.upgradeError': 'Plugin upgrade failed',
|
|
307
|
+
'update.upgradeErrorDetail': 'Plugin upgrade failed ({detail})',
|
|
297
308
|
'update.upgradeSuccess': 'Upgrade successful, restarting…',
|
|
298
309
|
'restart.title': 'Service restart',
|
|
299
310
|
'restart.description': 'Restart the dsh web process. Active work will be interrupted; persisted sessions can be resumed. You can also type /restart in a conversation.',
|
|
@@ -347,21 +358,30 @@ window.__ModuleLoader__.load({
|
|
|
347
358
|
'usage.toolErrors.toggle': 'Tool errors ({count} types)',
|
|
348
359
|
'usage.toolErrors.empty': 'No tool errors were recorded in the last 24 hours.',
|
|
349
360
|
'usage.errors.count': '{count} occurrence(s)',
|
|
350
|
-
'notification.title': '
|
|
351
|
-
'notification.description': '
|
|
361
|
+
'notification.title': 'Notifications',
|
|
362
|
+
'notification.description': 'Send a browser notification when a task finishes or when your approval or answer is needed. Requires browser notification permission; the toggles persist across page reloads.',
|
|
352
363
|
'notification.enable': 'Enable notifications',
|
|
353
364
|
'notification.enabled': 'Notifications enabled',
|
|
354
365
|
'notification.disable': 'Disable notifications',
|
|
355
366
|
'notification.denied': 'Notification permission denied',
|
|
356
|
-
'notification.
|
|
357
|
-
'notification.
|
|
367
|
+
'notification.master': 'Master switch',
|
|
368
|
+
'notification.masterHint': 'Both kinds below stay paused while this is off',
|
|
369
|
+
'notification.done': 'Task completion',
|
|
370
|
+
'notification.doneHint': 'Notify when a session finishes its turn',
|
|
371
|
+
'notification.input': 'Approvals & questions',
|
|
372
|
+
'notification.inputHint': 'Notify when approval, plan review, or an answer is requested',
|
|
373
|
+
'notification.doneTitle': 'Task complete',
|
|
374
|
+
'notification.doneBody': '{title} has finished its turn',
|
|
375
|
+
'notification.inputTitle': 'Your attention needed',
|
|
376
|
+
'notification.inputBody': '{title} — {kind}',
|
|
377
|
+
'notification.kind.approval': 'approval requested',
|
|
378
|
+
'notification.kind.plan-review': 'plan review requested',
|
|
379
|
+
'notification.kind.question': 'answer requested',
|
|
358
380
|
'notification.bellOn': 'Notifications on',
|
|
359
381
|
'notification.bellOff': 'Notifications off',
|
|
360
|
-
'notification.interval': 'Polling interval',
|
|
361
|
-
'notification.intervalUnit': 'seconds (5–300)',
|
|
362
382
|
}
|
|
363
383
|
|
|
364
|
-
const inject = ['slots', 'connection', 'timer', 'locale']
|
|
384
|
+
const inject = ['slots', 'connection', 'timer', 'locale', 'sessions']
|
|
365
385
|
|
|
366
386
|
function apply(ctx) {
|
|
367
387
|
const { useState, useEffect } = React
|
|
@@ -379,58 +399,78 @@ window.__ModuleLoader__.load({
|
|
|
379
399
|
useEffect(() => ctx.locale.subscribe(() => setSnapshot(ctx.locale.getSnapshot())), [])
|
|
380
400
|
return t
|
|
381
401
|
}
|
|
382
|
-
//
|
|
402
|
+
// 全局通知:任务结束 + 需要授权/选择答案,两个独立子开关受总开关管辖
|
|
383
403
|
let notifyEnabled = false
|
|
384
|
-
let
|
|
385
|
-
|
|
386
|
-
try {
|
|
404
|
+
let notifyDone = true
|
|
405
|
+
let notifyInput = true
|
|
406
|
+
try { notifyEnabled = localStorage.getItem('dsh-service-notify') === 'true' } catch (_) {}
|
|
407
|
+
try { notifyDone = localStorage.getItem('dsh-service-notify-done') !== 'false' } catch (_) {}
|
|
408
|
+
try { notifyInput = localStorage.getItem('dsh-service-notify-input') !== 'false' } catch (_) {}
|
|
387
409
|
const notifyListeners = new Set()
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
const setNotifyInterval = (value) => {
|
|
394
|
-
const v = Math.max(5, Math.min(300, Math.round(Number(value) || 30)))
|
|
395
|
-
notifyInterval = v
|
|
396
|
-
try { localStorage.setItem('dsh-service-notify-interval', String(v)) } catch (_) {}
|
|
397
|
-
for (const listener of notifyListeners) listener()
|
|
398
|
-
}
|
|
410
|
+
const persistNotify = (key, value) => { try { localStorage.setItem(key, value ? 'true' : 'false') } catch (_) {} }
|
|
411
|
+
const publishNotify = () => { for (const listener of notifyListeners) listener() }
|
|
412
|
+
const setNotifyEnabled = (value) => { notifyEnabled = value; persistNotify('dsh-service-notify', value); publishNotify() }
|
|
413
|
+
const setNotifyDone = (value) => { notifyDone = value; persistNotify('dsh-service-notify-done', value); publishNotify() }
|
|
414
|
+
const setNotifyInput = (value) => { notifyInput = value; persistNotify('dsh-service-notify-input', value); publishNotify() }
|
|
399
415
|
const useNotifyState = () => {
|
|
400
416
|
const [, setTick] = useState(0)
|
|
401
417
|
const [enabled, setEnabled] = useState(notifyEnabled)
|
|
402
|
-
const [
|
|
418
|
+
const [done, setDone] = useState(notifyDone)
|
|
419
|
+
const [input, setInput] = useState(notifyInput)
|
|
403
420
|
React.useEffect(() => {
|
|
404
|
-
const update = () => { setEnabled(notifyEnabled);
|
|
421
|
+
const update = () => { setEnabled(notifyEnabled); setDone(notifyDone); setInput(notifyInput); setTick((t) => t + 1) }
|
|
405
422
|
notifyListeners.add(update)
|
|
406
423
|
return () => notifyListeners.delete(update)
|
|
407
424
|
}, [])
|
|
408
|
-
return { enabled,
|
|
425
|
+
return { enabled, done, input, setEnabled: (v) => setNotifyEnabled(v), setDone: (v) => setNotifyDone(v), setInput: (v) => setNotifyInput(v) }
|
|
409
426
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
427
|
+
// 会话边沿通知:running→idle 记一次任务结束;pendingInteraction 出现记一次需要确认。
|
|
428
|
+
// 数据源是客户端运行时的会话列表快照(订阅推送);首个快照只建立基线,重连后重建基线,二者都不响铃。
|
|
429
|
+
const NOTIFY_KIND_KEYS = {
|
|
430
|
+
approval: 'notification.kind.approval',
|
|
431
|
+
'plan-review': 'notification.kind.plan-review',
|
|
432
|
+
question: 'notification.kind.question',
|
|
433
|
+
}
|
|
434
|
+
const notifyPermissionGranted = () => typeof Notification !== 'undefined' && Notification.permission === 'granted'
|
|
435
|
+
const fireNotification = (title, body) => {
|
|
436
|
+
if (!notifyPermissionGranted()) return
|
|
437
|
+
try { new Notification(title, { body }) } catch (_) {}
|
|
438
|
+
}
|
|
439
|
+
if (ctx.sessions && typeof ctx.sessions.list?.subscribe === 'function') {
|
|
440
|
+
const observed = new Map()
|
|
441
|
+
let baselined = false
|
|
442
|
+
const observeSessions = () => {
|
|
443
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
444
|
+
if (!snapshot || !snapshot.byId) return
|
|
445
|
+
if (!baselined) {
|
|
446
|
+
baselined = true
|
|
447
|
+
for (const [id, summary] of Object.entries(snapshot.byId)) {
|
|
448
|
+
observed.set(id, { running: summary.running === true, pending: summary.pendingInteraction !== undefined })
|
|
449
|
+
}
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
for (const [id, summary] of Object.entries(snapshot.byId)) {
|
|
453
|
+
const next = { running: summary.running === true, pending: summary.pendingInteraction !== undefined }
|
|
454
|
+
const prev = observed.get(id)
|
|
455
|
+
if (prev !== undefined) {
|
|
456
|
+
if (prev.running && !next.running && notifyEnabled && notifyDone) {
|
|
457
|
+
fireNotification(t('notification.doneTitle'), t('notification.doneBody', { title: summary.displayTitle || id }))
|
|
458
|
+
}
|
|
459
|
+
if (!prev.pending && next.pending && notifyEnabled && notifyInput) {
|
|
460
|
+
const kindKey = NOTIFY_KIND_KEYS[summary.pendingInteraction]
|
|
461
|
+
const kind = kindKey ? t(kindKey) : String(summary.pendingInteraction)
|
|
462
|
+
fireNotification(t('notification.inputTitle'), t('notification.inputBody', { title: summary.displayTitle || id, kind }))
|
|
425
463
|
}
|
|
426
|
-
previousAgentIds.clear()
|
|
427
|
-
for (const id of currentIds) previousAgentIds.add(id)
|
|
428
|
-
initialized = true
|
|
429
464
|
}
|
|
430
|
-
|
|
431
|
-
|
|
465
|
+
observed.set(id, next)
|
|
466
|
+
}
|
|
467
|
+
for (const id of [...observed.keys()]) {
|
|
468
|
+
if (!(id in snapshot.byId)) observed.delete(id)
|
|
469
|
+
}
|
|
432
470
|
}
|
|
433
|
-
ctx.
|
|
471
|
+
ctx.effect(() => ctx.sessions.list.subscribe(() => observeSessions()), 'dsh-service: session notification observation')
|
|
472
|
+
ctx.effect(() => ctx.on('connection/reset', () => { observed.clear(); baselined = false }), 'dsh-service: notification rebaseline on reconnect')
|
|
473
|
+
observeSessions()
|
|
434
474
|
}
|
|
435
475
|
|
|
436
476
|
const recoveryListeners = new Set()
|
|
@@ -880,12 +920,18 @@ window.__ModuleLoader__.load({
|
|
|
880
920
|
const versionRes = await ctx.connection.rpc.call('/dsh-service', 'version', {})
|
|
881
921
|
const previousInstanceId = versionRes && versionRes.ok ? versionRes.value.instanceId : undefined
|
|
882
922
|
const res = await ctx.connection.rpc.call('/dsh-service', 'upgrade', {})
|
|
883
|
-
|
|
923
|
+
// 宿主错误详情(如 npm-failed: …、npm was not found on PATH)随通用文案透出,便于排查。
|
|
924
|
+
if (!res || res.ok === false) {
|
|
925
|
+
const detail = res && typeof res.error === 'string' ? res.error.trim() : ''
|
|
926
|
+
throw new Error(detail || 'upgrade failed')
|
|
927
|
+
}
|
|
884
928
|
if (typeof previousInstanceId === 'string' && previousInstanceId.length > 0) {
|
|
885
929
|
startRecovery(previousInstanceId).catch(() => {})
|
|
886
930
|
}
|
|
887
|
-
} catch (
|
|
888
|
-
|
|
931
|
+
} catch (err) {
|
|
932
|
+
const detail = err instanceof Error && typeof err.message === 'string' && err.message !== 'upgrade failed' ? err.message.trim() : ''
|
|
933
|
+
console.error('dsh-service: upgrade failed', detail || err)
|
|
934
|
+
setUpgradeError(detail ? translate('update.upgradeErrorDetail', { detail }) : translate('update.upgradeError'))
|
|
889
935
|
} finally {
|
|
890
936
|
setUpgradeBusy(false)
|
|
891
937
|
}
|
|
@@ -1113,14 +1159,14 @@ window.__ModuleLoader__.load({
|
|
|
1113
1159
|
: null,
|
|
1114
1160
|
usage && usage.indexedSessions > 0
|
|
1115
1161
|
? React.createElement('div', { 'data-testid': 'usage-statistics-region', style: Object.assign({}, displaySurface, { padding: '12px', borderRadius: '9px' }) },
|
|
1116
|
-
React.createElement('div', { 'data-testid': 'usage-chart', style: { position: 'relative', display: 'grid', gridTemplateColumns: '42px minmax(0, 1fr)', height: '180px', padding: '12px 10px
|
|
1117
|
-
React.createElement('div', { 'data-testid': 'usage-y-axis', 'aria-label': translate('usage.axis'), style: { position: 'relative', height: '
|
|
1162
|
+
React.createElement('div', { 'data-testid': 'usage-chart', style: { position: 'relative', display: 'grid', gridTemplateColumns: '42px minmax(0, 1fr)', height: '180px', padding: '12px 10px 4px', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', border: '1px solid var(--dsw-alias-border-l1)', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1163
|
+
React.createElement('div', { 'data-testid': 'usage-y-axis', 'aria-label': translate('usage.axis'), style: { position: 'relative', height: '144px', fontSize: '10px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
1118
1164
|
chartTicks.map((tick, index) => React.createElement('span', { key: index, style: { position: 'absolute', right: '7px', top: `${index * 25}%`, transform: index === 4 ? 'translateY(-100%)' : 'translateY(-50%)' } }, formatTokenValue(tick)))),
|
|
1119
|
-
React.createElement('div', { 'data-testid': 'usage-plot', style: { position: 'relative', height: '
|
|
1165
|
+
React.createElement('div', { 'data-testid': 'usage-plot', style: { position: 'relative', height: '164px' } },
|
|
1120
1166
|
React.createElement('div', { style: { position: 'absolute', inset: '0 0 20px', display: 'flex', alignItems: 'end', gap: '8px', borderBottom: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1121
1167
|
chartTicks.map((_, index) => React.createElement('div', { key: index, 'data-testid': `usage-grid-${index}`, style: { position: 'absolute', left: 0, right: 0, top: `${index * 25}%`, borderTop: '1px solid var(--dsw-alias-border-l1)', pointerEvents: 'none' } })),
|
|
1122
1168
|
usageDays.map((day, index) => React.createElement('div', { key: day.key, style: { position: 'relative', zIndex: 1, flex: 1, minWidth: 0, alignSelf: 'end' } },
|
|
1123
|
-
React.createElement('div', { 'data-testid': `usage-bar-${day.key}`, style: { height: `${Math.max(2, chartValues[index] / chartMax *
|
|
1169
|
+
React.createElement('div', { 'data-testid': `usage-bar-${day.key}`, style: { height: `${Math.max(2, chartValues[index] / chartMax * 144)}px`, display: 'flex', flexDirection: 'column-reverse', justifyContent: 'flex-start', borderRadius: '4px 4px 0 0', overflow: 'hidden' } },
|
|
1124
1170
|
usageSegments.map(([metricName, label, color]) => {
|
|
1125
1171
|
const value = usageValue(chartTotals[index], metricName)
|
|
1126
1172
|
const segmentHeight = chartValues[index] === 0 ? 0 : value / chartValues[index] * 100
|
|
@@ -1375,9 +1421,27 @@ window.__ModuleLoader__.load({
|
|
|
1375
1421
|
error ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, String(error)) : null)
|
|
1376
1422
|
)
|
|
1377
1423
|
|
|
1378
|
-
const { enabled: notifyOn,
|
|
1424
|
+
const { enabled: notifyOn, done: notifyDoneOn, input: notifyInputOn, setEnabled: setNotifyOn, setDone: setNotifyDoneOn, setInput: setNotifyInputOn } = useNotifyState()
|
|
1379
1425
|
const notifSupported = typeof Notification !== 'undefined'
|
|
1380
1426
|
const notifPermission = notifSupported ? Notification.permission : 'denied'
|
|
1427
|
+
const notifySwitch = (on, onChange, disabled) => React.createElement('button', {
|
|
1428
|
+
type: 'button',
|
|
1429
|
+
role: 'switch',
|
|
1430
|
+
'aria-checked': String(on === true),
|
|
1431
|
+
'aria-disabled': disabled ? 'true' : undefined,
|
|
1432
|
+
onClick: disabled ? undefined : () => onChange(!on),
|
|
1433
|
+
style: {
|
|
1434
|
+
width: '34px', height: '20px', borderRadius: '10px', padding: 0, flexShrink: 0, position: 'relative',
|
|
1435
|
+
border: `1px solid ${on ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-border-l2)'}`,
|
|
1436
|
+
background: on ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-layer-2)',
|
|
1437
|
+
cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.5 : 1, lineHeight: 0,
|
|
1438
|
+
},
|
|
1439
|
+
}, 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)' } }))
|
|
1440
|
+
const notifyRow = (label, labelHint, on, onChange, disabled) => React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '5px 0' } },
|
|
1441
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
1442
|
+
React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, label),
|
|
1443
|
+
React.createElement('span', { style: hint }, labelHint)),
|
|
1444
|
+
notifySwitch(on, onChange, disabled))
|
|
1381
1445
|
const notificationBlock = !notifSupported ? null
|
|
1382
1446
|
: React.createElement('div', { style: { marginTop: '18px' } },
|
|
1383
1447
|
React.createElement('div', { style: sectionTitle }, translate('notification.title')),
|
|
@@ -1387,12 +1451,10 @@ window.__ModuleLoader__.load({
|
|
|
1387
1451
|
? React.createElement('div', { style: { marginTop: '8px', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1388
1452
|
React.createElement('button', { style: neutral, onClick: () => { Notification.requestPermission().then((p) => { if (p === 'granted') setNotifyOn(true) }) } }, translate('notification.enable')),
|
|
1389
1453
|
React.createElement('span', { style: hint }, notifPermission === 'denied' ? translate('notification.denied') : ''))
|
|
1390
|
-
: React.createElement('div', { style: { marginTop: '8px', display: 'flex',
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
React.createElement('input', { type: 'number', min: 5, max: 300, step: 5, value: notifyIntv, onChange: (e) => setNotifyIntv(e.target.value), style: { width: '60px', padding: '4px 6px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', fontSize: '12px', textAlign: 'center' } }),
|
|
1395
|
-
React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, translate('notification.intervalUnit')))))
|
|
1454
|
+
: React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column' } },
|
|
1455
|
+
notifyRow(translate('notification.master'), translate('notification.masterHint'), notifyOn, setNotifyOn, false),
|
|
1456
|
+
notifyRow(translate('notification.done'), translate('notification.doneHint'), notifyDoneOn, setNotifyDoneOn, !notifyOn),
|
|
1457
|
+
notifyRow(translate('notification.input'), translate('notification.inputHint'), notifyInputOn, setNotifyInputOn, !notifyOn))))
|
|
1396
1458
|
const overviewBlock = React.createElement('div', null, versionBlock, notificationBlock, containerInfoBlock, overviewErrorsBlock)
|
|
1397
1459
|
const maintenanceBlock = React.createElement('div', { key: 'maintenance-card', 'data-testid': 'maintenance-card', style: card }, backupBlock)
|
|
1398
1460
|
const diagnosticFailure = diagnostics?.checks?.some((check) => check.status === 'error' || (check.status === 'warning' && !(check.id === 'backup-storage' && String(check.detail || '').startsWith('0:')))) === true
|
|
@@ -1429,13 +1491,21 @@ window.__ModuleLoader__.load({
|
|
|
1429
1491
|
React.createElement('div', { 'data-testid': 'tab-panel', style: tabPanel }, tabContent))
|
|
1430
1492
|
}
|
|
1431
1493
|
|
|
1494
|
+
const bellPaths = {
|
|
1495
|
+
on: ['M10.268 21a2 2 0 0 0 3.464 0', 'm15 8 2 2 4-4', 'M16.8607 4.4824A6 6 0 0 0 6 8C6 12.499 4.589 13.956 3.262 15.326', 'M3.262 15.326A1 1 0 0 0 4 17H20A1 1 0 0 0 20.74 15.327C20.209 14.779 19.665 14.218 19.203 13.454'],
|
|
1496
|
+
off: ['M10.268 21a2 2 0 0 0 3.464 0', 'M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742', 'm2 2 20 20', 'M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05'],
|
|
1497
|
+
}
|
|
1498
|
+
function BellIcon(on) {
|
|
1499
|
+
return React.createElement('svg', { xmlns: 'http://www.w3.org/2000/svg', width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { display: 'block' } },
|
|
1500
|
+
(on ? bellPaths.on : bellPaths.off).map(function (d, i) { return React.createElement('path', { key: i, d: d }) }))
|
|
1501
|
+
}
|
|
1432
1502
|
function InlineNotifyBell() {
|
|
1433
1503
|
const { enabled, setEnabled } = useNotifyState()
|
|
1434
1504
|
const translate = useTranslation()
|
|
1435
1505
|
return React.createElement('button', {
|
|
1436
1506
|
type: 'button',
|
|
1437
1507
|
title: translate(enabled ? 'notification.bellOn' : 'notification.bellOff'),
|
|
1438
|
-
style: { background: 'transparent', border: 0, cursor: 'pointer',
|
|
1508
|
+
style: { background: 'transparent', border: 0, cursor: 'pointer', padding: '2px 4px', color: 'inherit', opacity: enabled ? 1 : 0.45, lineHeight: 0 },
|
|
1439
1509
|
onClick: () => {
|
|
1440
1510
|
if (typeof Notification === 'undefined') return
|
|
1441
1511
|
if (Notification.permission !== 'granted') {
|
|
@@ -1444,7 +1514,7 @@ window.__ModuleLoader__.load({
|
|
|
1444
1514
|
}
|
|
1445
1515
|
setEnabled(!enabled)
|
|
1446
1516
|
},
|
|
1447
|
-
}, enabled
|
|
1517
|
+
}, BellIcon(enabled))
|
|
1448
1518
|
}
|
|
1449
1519
|
ctx.slots.inject('conversation.input.left', () => ctx.slots.register(
|
|
1450
1520
|
{ name: 'conversation.input.left', id: 'dsh-service-notify', order: 90, label: () => t('notification.bellOn') },
|
package/index.js
CHANGED
|
@@ -666,9 +666,18 @@ async function runFixedCommand(ctx, argv) {
|
|
|
666
666
|
const subprocess = ctx.get('subprocess')
|
|
667
667
|
if (subprocess === undefined) throw new Error('subprocess-unavailable')
|
|
668
668
|
const executable = await subprocess.resolveExecutable(argv[0])
|
|
669
|
+
let spawnArgv = [executable, ...argv.slice(1)]
|
|
670
|
+
// Windows 上白名单命令(如 npm)经 PATHEXT 解析为 .cmd/.bat 脚本;subprocess 服务的
|
|
671
|
+
// spawn 不带 shell,Node 对 .cmd/.bat 一律抛 EINVAL,无法直接执行。固定包一层
|
|
672
|
+
// cmd.exe /d /s /c + 已解析的绝对路径,全部参数仍是宿主白名单常量,无输入拼接。
|
|
673
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(executable)) {
|
|
674
|
+
const shell = await subprocess.resolveExecutable('cmd.exe')
|
|
675
|
+
spawnArgv = [shell, '/d', '/s', '/c', executable, ...argv.slice(1)]
|
|
676
|
+
}
|
|
669
677
|
const handle = subprocess.spawn({
|
|
670
|
-
argv:
|
|
671
|
-
|
|
678
|
+
argv: spawnArgv,
|
|
679
|
+
// '/' 不是合法的 Windows 目录路径;固定改用系统目录,POSIX 保持根目录。
|
|
680
|
+
cwd: process.platform === 'win32' ? (process.env.SystemRoot || 'C:\\Windows') : '/',
|
|
672
681
|
stdio: {
|
|
673
682
|
stdin: 'ignore',
|
|
674
683
|
stdout: { maxBytes: 16 * 1024 },
|
|
@@ -872,14 +881,13 @@ async function collectDiagnostics(ctx, dshHome) {
|
|
|
872
881
|
try { add('tar', 'ok', await subprocess.resolveExecutable('tar')) } catch (error) { add('tar', 'error', error?.message || error) }
|
|
873
882
|
}
|
|
874
883
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
else {
|
|
884
|
+
if (process.platform === 'linux' && typeof process.getuid === 'function' && typeof process.getgid === 'function') {
|
|
885
|
+
try {
|
|
886
|
+
const snapshot = await permissionSnapshot(ctx, dshHome, new Map())
|
|
879
887
|
const abnormal = snapshot.items.filter((item) => item.writable === false).length
|
|
880
888
|
add('permissions', abnormal === 0 ? 'ok' : 'warning', abnormal)
|
|
881
|
-
}
|
|
882
|
-
}
|
|
889
|
+
} catch (error) { add('permissions', 'warning', error?.message || error) }
|
|
890
|
+
}
|
|
883
891
|
|
|
884
892
|
let status = 'ok'
|
|
885
893
|
if (checks.some((check) => check.status === 'error')) status = 'error'
|