@gehennawu/dsh-service 0.12.0 → 0.13.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 +4 -4
- package/README.md +4 -4
- package/client.js +135 -73
- package/index.js +5 -6
- 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
|
@@ -171,18 +171,27 @@ window.__ModuleLoader__.load({
|
|
|
171
171
|
'usage.toolErrors.toggle': '工具报错({count} 类)',
|
|
172
172
|
'usage.toolErrors.empty': '最近 24 小时没有记录到工具报错。',
|
|
173
173
|
'usage.errors.count': '{count} 次',
|
|
174
|
-
'notification.title': '
|
|
175
|
-
'notification.description': '
|
|
174
|
+
'notification.title': '通知',
|
|
175
|
+
'notification.description': '任务结束或需要你授权、选择答案时发送浏览器通知。需要授权浏览器通知权限;各开关在页面刷新后保持。',
|
|
176
176
|
'notification.enable': '开启通知',
|
|
177
177
|
'notification.enabled': '通知已开启',
|
|
178
178
|
'notification.disable': '关闭通知',
|
|
179
179
|
'notification.denied': '通知权限被拒绝',
|
|
180
|
-
'notification.
|
|
181
|
-
'notification.
|
|
180
|
+
'notification.master': '通知总开关',
|
|
181
|
+
'notification.masterHint': '关闭时下面两个开关暂停生效',
|
|
182
|
+
'notification.done': '任务结束通知',
|
|
183
|
+
'notification.doneHint': '会话完成一轮任务时提醒',
|
|
184
|
+
'notification.input': '授权与提问通知',
|
|
185
|
+
'notification.inputHint': '需要授权、审阅计划或选择答案时提醒',
|
|
186
|
+
'notification.doneTitle': '任务完成',
|
|
187
|
+
'notification.doneBody': '{title} 已完成本轮任务',
|
|
188
|
+
'notification.inputTitle': '需要你的确认',
|
|
189
|
+
'notification.inputBody': '{title}({kind})',
|
|
190
|
+
'notification.kind.approval': '等待授权',
|
|
191
|
+
'notification.kind.plan-review': '等待审阅计划',
|
|
192
|
+
'notification.kind.question': '等待选择答案',
|
|
182
193
|
'notification.bellOn': '通知开启',
|
|
183
194
|
'notification.bellOff': '通知关闭',
|
|
184
|
-
'notification.interval': '轮询间隔',
|
|
185
|
-
'notification.intervalUnit': '秒(5-300)',
|
|
186
195
|
}
|
|
187
196
|
const en = {
|
|
188
197
|
'nav.label': 'Service Control',
|
|
@@ -347,21 +356,30 @@ window.__ModuleLoader__.load({
|
|
|
347
356
|
'usage.toolErrors.toggle': 'Tool errors ({count} types)',
|
|
348
357
|
'usage.toolErrors.empty': 'No tool errors were recorded in the last 24 hours.',
|
|
349
358
|
'usage.errors.count': '{count} occurrence(s)',
|
|
350
|
-
'notification.title': '
|
|
351
|
-
'notification.description': '
|
|
359
|
+
'notification.title': 'Notifications',
|
|
360
|
+
'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
361
|
'notification.enable': 'Enable notifications',
|
|
353
362
|
'notification.enabled': 'Notifications enabled',
|
|
354
363
|
'notification.disable': 'Disable notifications',
|
|
355
364
|
'notification.denied': 'Notification permission denied',
|
|
356
|
-
'notification.
|
|
357
|
-
'notification.
|
|
365
|
+
'notification.master': 'Master switch',
|
|
366
|
+
'notification.masterHint': 'Both kinds below stay paused while this is off',
|
|
367
|
+
'notification.done': 'Task completion',
|
|
368
|
+
'notification.doneHint': 'Notify when a session finishes its turn',
|
|
369
|
+
'notification.input': 'Approvals & questions',
|
|
370
|
+
'notification.inputHint': 'Notify when approval, plan review, or an answer is requested',
|
|
371
|
+
'notification.doneTitle': 'Task complete',
|
|
372
|
+
'notification.doneBody': '{title} has finished its turn',
|
|
373
|
+
'notification.inputTitle': 'Your attention needed',
|
|
374
|
+
'notification.inputBody': '{title} — {kind}',
|
|
375
|
+
'notification.kind.approval': 'approval requested',
|
|
376
|
+
'notification.kind.plan-review': 'plan review requested',
|
|
377
|
+
'notification.kind.question': 'answer requested',
|
|
358
378
|
'notification.bellOn': 'Notifications on',
|
|
359
379
|
'notification.bellOff': 'Notifications off',
|
|
360
|
-
'notification.interval': 'Polling interval',
|
|
361
|
-
'notification.intervalUnit': 'seconds (5–300)',
|
|
362
380
|
}
|
|
363
381
|
|
|
364
|
-
const inject = ['slots', 'connection', 'timer', 'locale']
|
|
382
|
+
const inject = ['slots', 'connection', 'timer', 'locale', 'sessions']
|
|
365
383
|
|
|
366
384
|
function apply(ctx) {
|
|
367
385
|
const { useState, useEffect } = React
|
|
@@ -379,58 +397,78 @@ window.__ModuleLoader__.load({
|
|
|
379
397
|
useEffect(() => ctx.locale.subscribe(() => setSnapshot(ctx.locale.getSnapshot())), [])
|
|
380
398
|
return t
|
|
381
399
|
}
|
|
382
|
-
//
|
|
400
|
+
// 全局通知:任务结束 + 需要授权/选择答案,两个独立子开关受总开关管辖
|
|
383
401
|
let notifyEnabled = false
|
|
384
|
-
let
|
|
385
|
-
|
|
386
|
-
try {
|
|
402
|
+
let notifyDone = true
|
|
403
|
+
let notifyInput = true
|
|
404
|
+
try { notifyEnabled = localStorage.getItem('dsh-service-notify') === 'true' } catch (_) {}
|
|
405
|
+
try { notifyDone = localStorage.getItem('dsh-service-notify-done') !== 'false' } catch (_) {}
|
|
406
|
+
try { notifyInput = localStorage.getItem('dsh-service-notify-input') !== 'false' } catch (_) {}
|
|
387
407
|
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
|
-
}
|
|
408
|
+
const persistNotify = (key, value) => { try { localStorage.setItem(key, value ? 'true' : 'false') } catch (_) {} }
|
|
409
|
+
const publishNotify = () => { for (const listener of notifyListeners) listener() }
|
|
410
|
+
const setNotifyEnabled = (value) => { notifyEnabled = value; persistNotify('dsh-service-notify', value); publishNotify() }
|
|
411
|
+
const setNotifyDone = (value) => { notifyDone = value; persistNotify('dsh-service-notify-done', value); publishNotify() }
|
|
412
|
+
const setNotifyInput = (value) => { notifyInput = value; persistNotify('dsh-service-notify-input', value); publishNotify() }
|
|
399
413
|
const useNotifyState = () => {
|
|
400
414
|
const [, setTick] = useState(0)
|
|
401
415
|
const [enabled, setEnabled] = useState(notifyEnabled)
|
|
402
|
-
const [
|
|
416
|
+
const [done, setDone] = useState(notifyDone)
|
|
417
|
+
const [input, setInput] = useState(notifyInput)
|
|
403
418
|
React.useEffect(() => {
|
|
404
|
-
const update = () => { setEnabled(notifyEnabled);
|
|
419
|
+
const update = () => { setEnabled(notifyEnabled); setDone(notifyDone); setInput(notifyInput); setTick((t) => t + 1) }
|
|
405
420
|
notifyListeners.add(update)
|
|
406
421
|
return () => notifyListeners.delete(update)
|
|
407
422
|
}, [])
|
|
408
|
-
return { enabled,
|
|
423
|
+
return { enabled, done, input, setEnabled: (v) => setNotifyEnabled(v), setDone: (v) => setNotifyDone(v), setInput: (v) => setNotifyInput(v) }
|
|
409
424
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
+
// 会话边沿通知:running→idle 记一次任务结束;pendingInteraction 出现记一次需要确认。
|
|
426
|
+
// 数据源是客户端运行时的会话列表快照(订阅推送);首个快照只建立基线,重连后重建基线,二者都不响铃。
|
|
427
|
+
const NOTIFY_KIND_KEYS = {
|
|
428
|
+
approval: 'notification.kind.approval',
|
|
429
|
+
'plan-review': 'notification.kind.plan-review',
|
|
430
|
+
question: 'notification.kind.question',
|
|
431
|
+
}
|
|
432
|
+
const notifyPermissionGranted = () => typeof Notification !== 'undefined' && Notification.permission === 'granted'
|
|
433
|
+
const fireNotification = (title, body) => {
|
|
434
|
+
if (!notifyPermissionGranted()) return
|
|
435
|
+
try { new Notification(title, { body }) } catch (_) {}
|
|
436
|
+
}
|
|
437
|
+
if (ctx.sessions && typeof ctx.sessions.list?.subscribe === 'function') {
|
|
438
|
+
const observed = new Map()
|
|
439
|
+
let baselined = false
|
|
440
|
+
const observeSessions = () => {
|
|
441
|
+
const snapshot = ctx.sessions.list.getSnapshot()
|
|
442
|
+
if (!snapshot || !snapshot.byId) return
|
|
443
|
+
if (!baselined) {
|
|
444
|
+
baselined = true
|
|
445
|
+
for (const [id, summary] of Object.entries(snapshot.byId)) {
|
|
446
|
+
observed.set(id, { running: summary.running === true, pending: summary.pendingInteraction !== undefined })
|
|
447
|
+
}
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
for (const [id, summary] of Object.entries(snapshot.byId)) {
|
|
451
|
+
const next = { running: summary.running === true, pending: summary.pendingInteraction !== undefined }
|
|
452
|
+
const prev = observed.get(id)
|
|
453
|
+
if (prev !== undefined) {
|
|
454
|
+
if (prev.running && !next.running && notifyEnabled && notifyDone) {
|
|
455
|
+
fireNotification(t('notification.doneTitle'), t('notification.doneBody', { title: summary.displayTitle || id }))
|
|
456
|
+
}
|
|
457
|
+
if (!prev.pending && next.pending && notifyEnabled && notifyInput) {
|
|
458
|
+
const kindKey = NOTIFY_KIND_KEYS[summary.pendingInteraction]
|
|
459
|
+
const kind = kindKey ? t(kindKey) : String(summary.pendingInteraction)
|
|
460
|
+
fireNotification(t('notification.inputTitle'), t('notification.inputBody', { title: summary.displayTitle || id, kind }))
|
|
425
461
|
}
|
|
426
|
-
previousAgentIds.clear()
|
|
427
|
-
for (const id of currentIds) previousAgentIds.add(id)
|
|
428
|
-
initialized = true
|
|
429
462
|
}
|
|
430
|
-
|
|
431
|
-
|
|
463
|
+
observed.set(id, next)
|
|
464
|
+
}
|
|
465
|
+
for (const id of [...observed.keys()]) {
|
|
466
|
+
if (!(id in snapshot.byId)) observed.delete(id)
|
|
467
|
+
}
|
|
432
468
|
}
|
|
433
|
-
ctx.
|
|
469
|
+
ctx.effect(() => ctx.sessions.list.subscribe(() => observeSessions()), 'dsh-service: session notification observation')
|
|
470
|
+
ctx.effect(() => ctx.on('connection/reset', () => { observed.clear(); baselined = false }), 'dsh-service: notification rebaseline on reconnect')
|
|
471
|
+
observeSessions()
|
|
434
472
|
}
|
|
435
473
|
|
|
436
474
|
const recoveryListeners = new Set()
|
|
@@ -1113,14 +1151,14 @@ window.__ModuleLoader__.load({
|
|
|
1113
1151
|
: null,
|
|
1114
1152
|
usage && usage.indexedSessions > 0
|
|
1115
1153
|
? 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: '
|
|
1154
|
+
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)' } },
|
|
1155
|
+
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
1156
|
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: '
|
|
1157
|
+
React.createElement('div', { 'data-testid': 'usage-plot', style: { position: 'relative', height: '164px' } },
|
|
1120
1158
|
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
1159
|
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
1160
|
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 *
|
|
1161
|
+
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
1162
|
usageSegments.map(([metricName, label, color]) => {
|
|
1125
1163
|
const value = usageValue(chartTotals[index], metricName)
|
|
1126
1164
|
const segmentHeight = chartValues[index] === 0 ? 0 : value / chartValues[index] * 100
|
|
@@ -1291,13 +1329,14 @@ window.__ModuleLoader__.load({
|
|
|
1291
1329
|
: null)))
|
|
1292
1330
|
: null))
|
|
1293
1331
|
|
|
1294
|
-
const versionRow = (id, label, fallbackVersion, state) => React.createElement('div', { key: id, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '16px', padding: '10px 2px', borderTop: id === 'dsh' ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
|
|
1295
|
-
React.createElement('div', { style: { whiteSpace: 'nowrap' } },
|
|
1332
|
+
const versionRow = (id, label, fallbackVersion, state, action) => React.createElement('div', { key: id, style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '16px', padding: '10px 2px', borderTop: id === 'dsh' ? 0 : '1px solid var(--dsw-alias-border-l1)' } },
|
|
1333
|
+
React.createElement('div', { style: { whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1296
1334
|
React.createElement('span', { style: { fontSize: '13px', fontWeight: 650 } }, `${label} `),
|
|
1297
1335
|
state?.url
|
|
1298
1336
|
? 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'))
|
|
1299
1337
|
: React.createElement('code', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-primary)', marginLeft: '16px' } }, state?.current || fallbackVersion || translate('version.loading'))),
|
|
1300
1338
|
state?.tags && id === 'dsh' ? React.createElement('span', { style: { marginLeft: '8px', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)', whiteSpace: 'nowrap' } }, translate('update.channels', { latest: state.tags.latest || '—', next: state.tags.next || '—' })) : null,
|
|
1339
|
+
action || null,
|
|
1301
1340
|
React.createElement('div', { style: { textAlign: 'right', fontSize: '12px' } },
|
|
1302
1341
|
React.createElement('div', { style: { color: !state ? 'var(--dsw-alias-label-secondary)' : state.upToDate ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warn-primary)', fontWeight: 600 } }, !state
|
|
1303
1342
|
? (updateError || translate('update.checking'))
|
|
@@ -1306,16 +1345,15 @@ window.__ModuleLoader__.load({
|
|
|
1306
1345
|
: state.upToDate ? translate('update.current') : translate('update.available', { version: state.latest }))))
|
|
1307
1346
|
// 版本信息区块
|
|
1308
1347
|
const pluginUpdate = updateInfo?.plugin && !updateInfo.plugin.upToDate && updateInfo.plugin.status === 'available'
|
|
1348
|
+
const pluginAction = pluginUpdate
|
|
1349
|
+
? React.createElement('button', { style: Object.assign({}, neutral, { minHeight: '24px', padding: '2px 8px', fontSize: '11px' }), disabled: upgradeBusy, onClick: upgradePlugin }, translate(upgradeBusy ? 'update.upgrading' : 'update.upgrade'))
|
|
1350
|
+
: null
|
|
1309
1351
|
const versionBlock = React.createElement('div', { key: 'version-card', 'data-testid': 'version-card', style: card },
|
|
1310
1352
|
React.createElement('div', { key: 'title', style: sectionTitle }, translate('version.title')),
|
|
1311
1353
|
React.createElement('div', { style: displaySurface },
|
|
1312
1354
|
versionRow('dsh', 'DSH', version, updateInfo?.dsh),
|
|
1313
|
-
versionRow('plugin', 'dsh-service', pluginVersion, updateInfo?.plugin),
|
|
1314
|
-
|
|
1315
|
-
? React.createElement('div', { style: { marginTop: '8px', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1316
|
-
pluginUpdate ? React.createElement('button', { style: primary, disabled: upgradeBusy, onClick: upgradePlugin }, translate(upgradeBusy ? 'update.upgrading' : 'update.upgrade')) : null,
|
|
1317
|
-
upgradeError ? React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-state-error-primary)' } }, upgradeError) : null)
|
|
1318
|
-
: null))
|
|
1355
|
+
versionRow('plugin', 'dsh-service', pluginVersion, updateInfo?.plugin, pluginAction),
|
|
1356
|
+
upgradeError ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)', margin: '4px 0 0' }) }, upgradeError) : null))
|
|
1319
1357
|
|
|
1320
1358
|
// 重启后提示
|
|
1321
1359
|
if (stage === 2) {
|
|
@@ -1375,9 +1413,27 @@ window.__ModuleLoader__.load({
|
|
|
1375
1413
|
error ? React.createElement('p', { style: Object.assign({}, hint, { color: 'var(--dsw-alias-state-error-primary)' }) }, String(error)) : null)
|
|
1376
1414
|
)
|
|
1377
1415
|
|
|
1378
|
-
const { enabled: notifyOn,
|
|
1416
|
+
const { enabled: notifyOn, done: notifyDoneOn, input: notifyInputOn, setEnabled: setNotifyOn, setDone: setNotifyDoneOn, setInput: setNotifyInputOn } = useNotifyState()
|
|
1379
1417
|
const notifSupported = typeof Notification !== 'undefined'
|
|
1380
1418
|
const notifPermission = notifSupported ? Notification.permission : 'denied'
|
|
1419
|
+
const notifySwitch = (on, onChange, disabled) => React.createElement('button', {
|
|
1420
|
+
type: 'button',
|
|
1421
|
+
role: 'switch',
|
|
1422
|
+
'aria-checked': String(on === true),
|
|
1423
|
+
'aria-disabled': disabled ? 'true' : undefined,
|
|
1424
|
+
onClick: disabled ? undefined : () => onChange(!on),
|
|
1425
|
+
style: {
|
|
1426
|
+
width: '34px', height: '20px', borderRadius: '10px', padding: 0, flexShrink: 0, position: 'relative',
|
|
1427
|
+
border: `1px solid ${on ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-border-l2)'}`,
|
|
1428
|
+
background: on ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-layer-2)',
|
|
1429
|
+
cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.5 : 1, lineHeight: 0,
|
|
1430
|
+
},
|
|
1431
|
+
}, 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)' } }))
|
|
1432
|
+
const notifyRow = (label, labelHint, on, onChange, disabled) => React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', padding: '5px 0' } },
|
|
1433
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
1434
|
+
React.createElement('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-primary)' } }, label),
|
|
1435
|
+
React.createElement('span', { style: hint }, labelHint)),
|
|
1436
|
+
notifySwitch(on, onChange, disabled))
|
|
1381
1437
|
const notificationBlock = !notifSupported ? null
|
|
1382
1438
|
: React.createElement('div', { style: { marginTop: '18px' } },
|
|
1383
1439
|
React.createElement('div', { style: sectionTitle }, translate('notification.title')),
|
|
@@ -1387,12 +1443,10 @@ window.__ModuleLoader__.load({
|
|
|
1387
1443
|
? React.createElement('div', { style: { marginTop: '8px', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1388
1444
|
React.createElement('button', { style: neutral, onClick: () => { Notification.requestPermission().then((p) => { if (p === 'granted') setNotifyOn(true) }) } }, translate('notification.enable')),
|
|
1389
1445
|
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')))))
|
|
1446
|
+
: React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column' } },
|
|
1447
|
+
notifyRow(translate('notification.master'), translate('notification.masterHint'), notifyOn, setNotifyOn, false),
|
|
1448
|
+
notifyRow(translate('notification.done'), translate('notification.doneHint'), notifyDoneOn, setNotifyDoneOn, !notifyOn),
|
|
1449
|
+
notifyRow(translate('notification.input'), translate('notification.inputHint'), notifyInputOn, setNotifyInputOn, !notifyOn))))
|
|
1396
1450
|
const overviewBlock = React.createElement('div', null, versionBlock, notificationBlock, containerInfoBlock, overviewErrorsBlock)
|
|
1397
1451
|
const maintenanceBlock = React.createElement('div', { key: 'maintenance-card', 'data-testid': 'maintenance-card', style: card }, backupBlock)
|
|
1398
1452
|
const diagnosticFailure = diagnostics?.checks?.some((check) => check.status === 'error' || (check.status === 'warning' && !(check.id === 'backup-storage' && String(check.detail || '').startsWith('0:')))) === true
|
|
@@ -1429,13 +1483,21 @@ window.__ModuleLoader__.load({
|
|
|
1429
1483
|
React.createElement('div', { 'data-testid': 'tab-panel', style: tabPanel }, tabContent))
|
|
1430
1484
|
}
|
|
1431
1485
|
|
|
1486
|
+
const bellPaths = {
|
|
1487
|
+
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'],
|
|
1488
|
+
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'],
|
|
1489
|
+
}
|
|
1490
|
+
function BellIcon(on) {
|
|
1491
|
+
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' } },
|
|
1492
|
+
(on ? bellPaths.on : bellPaths.off).map(function (d, i) { return React.createElement('path', { key: i, d: d }) }))
|
|
1493
|
+
}
|
|
1432
1494
|
function InlineNotifyBell() {
|
|
1433
1495
|
const { enabled, setEnabled } = useNotifyState()
|
|
1434
1496
|
const translate = useTranslation()
|
|
1435
1497
|
return React.createElement('button', {
|
|
1436
1498
|
type: 'button',
|
|
1437
1499
|
title: translate(enabled ? 'notification.bellOn' : 'notification.bellOff'),
|
|
1438
|
-
style: { background: 'transparent', border: 0, cursor: 'pointer',
|
|
1500
|
+
style: { background: 'transparent', border: 0, cursor: 'pointer', padding: '2px 4px', color: 'inherit', opacity: enabled ? 1 : 0.45, lineHeight: 0 },
|
|
1439
1501
|
onClick: () => {
|
|
1440
1502
|
if (typeof Notification === 'undefined') return
|
|
1441
1503
|
if (Notification.permission !== 'granted') {
|
|
@@ -1444,7 +1506,7 @@ window.__ModuleLoader__.load({
|
|
|
1444
1506
|
}
|
|
1445
1507
|
setEnabled(!enabled)
|
|
1446
1508
|
},
|
|
1447
|
-
}, enabled
|
|
1509
|
+
}, BellIcon(enabled))
|
|
1448
1510
|
}
|
|
1449
1511
|
ctx.slots.inject('conversation.input.left', () => ctx.slots.register(
|
|
1450
1512
|
{ name: 'conversation.input.left', id: 'dsh-service-notify', order: 90, label: () => t('notification.bellOn') },
|
package/index.js
CHANGED
|
@@ -872,14 +872,13 @@ async function collectDiagnostics(ctx, dshHome) {
|
|
|
872
872
|
try { add('tar', 'ok', await subprocess.resolveExecutable('tar')) } catch (error) { add('tar', 'error', error?.message || error) }
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
else {
|
|
875
|
+
if (process.platform === 'linux' && typeof process.getuid === 'function' && typeof process.getgid === 'function') {
|
|
876
|
+
try {
|
|
877
|
+
const snapshot = await permissionSnapshot(ctx, dshHome, new Map())
|
|
879
878
|
const abnormal = snapshot.items.filter((item) => item.writable === false).length
|
|
880
879
|
add('permissions', abnormal === 0 ? 'ok' : 'warning', abnormal)
|
|
881
|
-
}
|
|
882
|
-
}
|
|
880
|
+
} catch (error) { add('permissions', 'warning', error?.message || error) }
|
|
881
|
+
}
|
|
883
882
|
|
|
884
883
|
let status = 'ok'
|
|
885
884
|
if (checks.some((check) => check.status === 'error')) status = 'error'
|