dsh-remote-plugin 0.6.11 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -2
- package/README.md +3 -2
- package/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +466 -65
- package/index.mjs +3 -2
- package/package.json +1 -1
- package/public/announcements.json +8 -0
- package/public/app.js +270 -35
- package/public/index.html +50 -12
- package/public/styles.css +41 -2
- package/public/update.json +9 -9
- package/public/version.json +1 -1
package/index.mjs
CHANGED
|
@@ -151,8 +151,9 @@ function runExit(cmd, args) {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
const GATEWAY_ENV_KEYS = [
|
|
154
|
-
'TOKEN', 'TOKEN_FILE', 'DSH_REMOTE_TOKEN', 'DSH_REMOTE_FS_ROOT', 'DSH_REMOTE_FS_MAX_UPLOAD',
|
|
155
|
-
'DSH_REMOTE_NOTES', 'DSH_REMOTE_WORKBENCH', 'DSH_REMOTE_DSH_SERVICE', '
|
|
154
|
+
'TOKEN', 'TOKEN_FILE', 'DSH_REMOTE_TOKEN', 'DSH_REMOTE_FS_ROOT', 'DSH_REMOTE_FS_WORKSPACE_CACHE_MS', 'DSH_REMOTE_FS_MAX_UPLOAD',
|
|
155
|
+
'DSH_REMOTE_NOTES', 'DSH_REMOTE_WORKBENCH', 'DSH_REMOTE_DSH_SERVICE', 'DSH_REMOTE_SYSTEMCTL',
|
|
156
|
+
'DSH_REMOTE_DSH_CONTROL_TIMEOUT_MS', 'DSH_REMOTE_DSH_CONTROL_POLL_MS', 'DSH_REMOTE_FEEDBACK_URL',
|
|
156
157
|
'UPDATE_CHECK_URL', 'UPDATE_INTERVAL_MS', 'UPDATE_PROXY', 'DSH_HEALTH_PATH',
|
|
157
158
|
'GATEWAY_WS_IDLE_MS', 'GATEWAY_WS_PING_MS', 'GATEWAY_WS_PONG_TIMEOUT_MS',
|
|
158
159
|
'GATEWAY_WS_UPGRADE_TIMEOUT_MS', 'GATEWAY_UPSTREAM_TIMEOUT_MS', 'GATEWAY_EVENT_BUFFER_MAX',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
4
4
|
"description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"minVersion": "0.6.9",
|
|
16
16
|
"maxVersion": "",
|
|
17
17
|
"publishedAt": "2026-08-22T12:00:00+08:00"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "2026-08-23-feedback-polls",
|
|
21
|
+
"title": "反馈系统更新与投票公告上线",
|
|
22
|
+
"content": "大家好,DSH Remote 的反馈系统已完成修复,并新增了投票类公告功能。后续我们会通过投票收集大家对新功能、优化方向和更新优先级的意见,让版本规划更贴近实际使用需求。\n\n欢迎各位用户和开发者提交更多反馈,包括功能建议、异常问题、兼容性情况以及使用体验等。\n\n目前应用内的“直接反馈”功能仍处于测试阶段,稳定性还在持续改进。如果是重要问题或需要持续跟进的 Bug,建议优先通过 GitHub Issues 或 B 站相关渠道反馈,这两种方式目前比直接反馈更加稳定。\n\n提交日志或截图前,请注意隐藏 Token、服务器地址等敏感信息。感谢大家的支持与参与!",
|
|
23
|
+
"minVersion": "",
|
|
24
|
+
"maxVersion": "",
|
|
25
|
+
"publishedAt": "2026-08-23T19:04:26+08:00"
|
|
18
26
|
}
|
|
19
27
|
]
|
|
20
28
|
}
|
package/public/app.js
CHANGED
|
@@ -65,6 +65,7 @@ const state = {
|
|
|
65
65
|
localVersion: '',
|
|
66
66
|
updateInfo: null,
|
|
67
67
|
announcement: null,
|
|
68
|
+
announcements: [],
|
|
68
69
|
approvals: [], // 待处理审批
|
|
69
70
|
questions: [], // 待处理提问
|
|
70
71
|
queues: {}, // sessionId -> queue items
|
|
@@ -272,6 +273,14 @@ function openFeedbackModal() {
|
|
|
272
273
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
273
274
|
}
|
|
274
275
|
function closeFeedbackModal() { $('modal-feedback').classList.add('hidden') }
|
|
276
|
+
function openFeedbackSuccess() {
|
|
277
|
+
$('modal-feedback-success').classList.remove('hidden')
|
|
278
|
+
setTimeout(() => $('feedback-success-confirm').focus(), 50)
|
|
279
|
+
}
|
|
280
|
+
function closeFeedbackSuccess() {
|
|
281
|
+
$('modal-feedback-success').classList.add('hidden')
|
|
282
|
+
$('btn-feedback')?.focus()
|
|
283
|
+
}
|
|
275
284
|
async function submitFeedback() {
|
|
276
285
|
const type = state.feedbackType || 'bug'
|
|
277
286
|
const message = $('fb-msg').value.trim()
|
|
@@ -290,7 +299,7 @@ async function submitFeedback() {
|
|
|
290
299
|
})
|
|
291
300
|
let json = {}
|
|
292
301
|
try { json = await res.json() } catch {}
|
|
293
|
-
if (res.ok && json.ok) {
|
|
302
|
+
if (res.ok && json.ok) { closeFeedbackModal(); openFeedbackSuccess() }
|
|
294
303
|
else if (res.status === 429) { toast(json.retryAfter ? t('feedback.rateLimitedAt', { n: json.retryAfter }) : t('feedback.rateLimited'), 'err') }
|
|
295
304
|
else { toast(t('feedback.submitFailed', { msg: json.error || res.status }), 'err') }
|
|
296
305
|
} catch {
|
|
@@ -1382,12 +1391,17 @@ function workspaceItems() {
|
|
|
1382
1391
|
function workspaceById(workspaceId) {
|
|
1383
1392
|
return workspaceItems().find(w => w.workspaceId === workspaceId) || null
|
|
1384
1393
|
}
|
|
1394
|
+
function workspaceOwnsSession(workspace, session) {
|
|
1395
|
+
if (!workspace || !session?.sessionId) return false
|
|
1396
|
+
if (String(session.workspaceId || '') === workspace.workspaceId) return true
|
|
1397
|
+
if (Array.isArray(workspace.sessionIds) && workspace.sessionIds.some(id => String(id) === String(session.sessionId))) return true
|
|
1398
|
+
const cwdKey = wbPathKey(sessionCwd(session))
|
|
1399
|
+
const workspaceKey = wbPathKey(workspace.path)
|
|
1400
|
+
return !!cwdKey && !!workspaceKey && (cwdKey === workspaceKey || cwdKey.startsWith(workspaceKey.endsWith('/') ? workspaceKey : workspaceKey + '/'))
|
|
1401
|
+
}
|
|
1385
1402
|
function workspaceForSession(session) {
|
|
1386
1403
|
if (!session) return null
|
|
1387
|
-
|
|
1388
|
-
if (byMembership) return byMembership
|
|
1389
|
-
const cwdKey = wbPathKey(sessionCwd(session))
|
|
1390
|
-
return cwdKey ? workspaceItems().find(w => wbPathKey(w.path) === cwdKey) || null : null
|
|
1404
|
+
return workspaceItems().find(workspace => workspaceOwnsSession(workspace, session)) || null
|
|
1391
1405
|
}
|
|
1392
1406
|
function workspaceName(workspace) {
|
|
1393
1407
|
return String(workspace?.title || wbBaseName(workspace?.path) || workspace?.path || '').trim()
|
|
@@ -1411,7 +1425,10 @@ function renderWorkspaceNavigation() {
|
|
|
1411
1425
|
LS.del('workspaceFilterV1')
|
|
1412
1426
|
}
|
|
1413
1427
|
const sessionSelect = $('session-workspace-filter')
|
|
1414
|
-
if (sessionSelect)
|
|
1428
|
+
if (sessionSelect) {
|
|
1429
|
+
sessionSelect.innerHTML = workspaceOptionsHtml({ all: true, ungrouped: true, selected: state.workspaceFilter })
|
|
1430
|
+
syncCustomSelect(sessionSelect)
|
|
1431
|
+
}
|
|
1415
1432
|
const selectedWorkspace = workspaceById(state.workspaceFilter)
|
|
1416
1433
|
const pathBox = $('session-workspace-path')
|
|
1417
1434
|
if (pathBox) pathBox.textContent = selectedWorkspace?.path || (state.workspaceFilter === WORKSPACE_UNGROUPED ? t('workspace.ungrouped') : t('workspace.allPath'))
|
|
@@ -1423,7 +1440,10 @@ function renderWorkspaceNavigation() {
|
|
|
1423
1440
|
LS.del('fsWorkspaceIdV1')
|
|
1424
1441
|
}
|
|
1425
1442
|
const fsSelect = $('fs-workspace')
|
|
1426
|
-
if (fsSelect)
|
|
1443
|
+
if (fsSelect) {
|
|
1444
|
+
fsSelect.innerHTML = workspaceOptionsHtml({ root: true, selected: state.fs.workspaceId })
|
|
1445
|
+
syncCustomSelect(fsSelect)
|
|
1446
|
+
}
|
|
1427
1447
|
if ($('modal-new-session') && !$('modal-new-session').classList.contains('hidden')) renderNewSessionWorkspace()
|
|
1428
1448
|
return items
|
|
1429
1449
|
}
|
|
@@ -1556,9 +1576,8 @@ function renderSessions() {
|
|
|
1556
1576
|
const archivedSet = new Set(state.wbArchived || [])
|
|
1557
1577
|
const visible = allItems.filter(s => {
|
|
1558
1578
|
if (!state.workspaceFilter) return true
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
return workspace?.workspaceId === state.workspaceFilter
|
|
1579
|
+
if (state.workspaceFilter === WORKSPACE_UNGROUPED) return !workspaceForSession(s)
|
|
1580
|
+
return workspaceOwnsSession(workspaceById(state.workspaceFilter), s)
|
|
1562
1581
|
})
|
|
1563
1582
|
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1564
1583
|
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
@@ -2433,7 +2452,9 @@ function renderNewSessionWorkspace(preferredId = '') {
|
|
|
2433
2452
|
const current = workspaceById(preferredId || select.value)?.workspaceId || items[0]?.workspaceId || ''
|
|
2434
2453
|
select.innerHTML = workspaceOptionsHtml({ selected: current })
|
|
2435
2454
|
select.disabled = !items.length
|
|
2455
|
+
syncCustomSelect(select)
|
|
2436
2456
|
const workspace = workspaceById(select.value)
|
|
2457
|
+
$('new-session-workspace-name').textContent = workspace ? workspaceName(workspace) : ''
|
|
2437
2458
|
$('new-session-workspace-path').textContent = workspace?.path || ''
|
|
2438
2459
|
$('new-session-empty').classList.toggle('hidden', !!items.length)
|
|
2439
2460
|
$('new-session-create').disabled = !workspace
|
|
@@ -3397,6 +3418,10 @@ async function loadLocalVersion() {
|
|
|
3397
3418
|
const ANNOUNCEMENTS_KEY = 'seenAnnouncementsV1'
|
|
3398
3419
|
const ANNOUNCEMENT_HISTORY_KEY = 'announcementHistoryV1'
|
|
3399
3420
|
const ANNOUNCEMENT_VOTES_KEY = 'announcementVotesV1'
|
|
3421
|
+
const ANNOUNCEMENTS_POLL_MS = 30 * 1000
|
|
3422
|
+
let announcementCheckPromise = null
|
|
3423
|
+
let announcementPollTimer = null
|
|
3424
|
+
let announcementPollingStarted = false
|
|
3400
3425
|
function readSeenAnnouncements() {
|
|
3401
3426
|
try {
|
|
3402
3427
|
const value = JSON.parse(LS.get(ANNOUNCEMENTS_KEY, '{}'))
|
|
@@ -3413,6 +3438,7 @@ function markAnnouncementSeen(id) {
|
|
|
3413
3438
|
for (const key of keys.slice(0, keys.length - 100)) delete seen[key]
|
|
3414
3439
|
}
|
|
3415
3440
|
LS.set(ANNOUNCEMENTS_KEY, JSON.stringify(seen))
|
|
3441
|
+
renderAnnouncementBoard()
|
|
3416
3442
|
}
|
|
3417
3443
|
function readAnnouncementVotes() {
|
|
3418
3444
|
try {
|
|
@@ -3432,6 +3458,7 @@ function storeAnnouncementVote(announcementId, pollId, optionId) {
|
|
|
3432
3458
|
for (const key of keys.slice(0, keys.length - 100)) delete votes[key]
|
|
3433
3459
|
}
|
|
3434
3460
|
LS.set(ANNOUNCEMENT_VOTES_KEY, JSON.stringify(votes))
|
|
3461
|
+
renderAnnouncementBoard()
|
|
3435
3462
|
}
|
|
3436
3463
|
function announcementVote(item) {
|
|
3437
3464
|
if (!item?.poll?.id) return null
|
|
@@ -3453,6 +3480,36 @@ function storeAnnouncementHistory(items) {
|
|
|
3453
3480
|
LS.set(ANNOUNCEMENT_HISTORY_KEY, JSON.stringify(list))
|
|
3454
3481
|
return list
|
|
3455
3482
|
}
|
|
3483
|
+
function announcementBoardItems() {
|
|
3484
|
+
const seen = readSeenAnnouncements()
|
|
3485
|
+
const merged = new Map(readAnnouncementHistory().map(item => [item.id, item]))
|
|
3486
|
+
for (const item of state.announcements) if (item?.id) merged.set(item.id, item)
|
|
3487
|
+
return [...merged.values()]
|
|
3488
|
+
.filter(item => !seen[item.id])
|
|
3489
|
+
.sort((a, b) => Number(b.publishedAt || 0) - Number(a.publishedAt || 0))
|
|
3490
|
+
.slice(0, 1)
|
|
3491
|
+
}
|
|
3492
|
+
function renderAnnouncementBoard() {
|
|
3493
|
+
const box = $('overview-announcement-list')
|
|
3494
|
+
if (!box) return
|
|
3495
|
+
const items = announcementBoardItems()
|
|
3496
|
+
if (!items.length) {
|
|
3497
|
+
box.innerHTML = `<div class="overview-empty">${esc(t('overview.communityEmpty'))}</div>`
|
|
3498
|
+
return
|
|
3499
|
+
}
|
|
3500
|
+
box.innerHTML = items.map(item => {
|
|
3501
|
+
const poll = !!item.poll
|
|
3502
|
+
const vote = announcementVote(item)
|
|
3503
|
+
const date = Number(item.publishedAt) > 0 ? fmtFullTime(item.publishedAt) : t('announcement.noDate')
|
|
3504
|
+
const action = poll ? (vote ? t('overview.communityVoted') : t('overview.communityVote')) : t('overview.communityView')
|
|
3505
|
+
return `<button class="overview-announcement-card ${poll ? 'poll' : 'notice'}" type="button" data-home-announcement="${esc(item.id)}">
|
|
3506
|
+
<span class="overview-announcement-meta"><span class="overview-announcement-badge">${esc(t(poll ? 'overview.communityPoll' : 'overview.communityNotice'))}</span><span class="overview-announcement-new">${esc(t('overview.communityNew'))}</span><span class="overview-announcement-date">${esc(date)}</span></span>
|
|
3507
|
+
<span class="overview-announcement-title">${esc(item.title)}</span>
|
|
3508
|
+
<span class="overview-announcement-copy">${esc(item.content)}</span>
|
|
3509
|
+
<span class="overview-announcement-footer"><span>${esc(action)}</span><span aria-hidden="true">›</span></span>
|
|
3510
|
+
</button>`
|
|
3511
|
+
}).join('')
|
|
3512
|
+
}
|
|
3456
3513
|
function renderAnnouncementHistory() {
|
|
3457
3514
|
const box = $('announcement-history-list')
|
|
3458
3515
|
if (!box) return
|
|
@@ -3613,7 +3670,7 @@ function closeAnnouncement(markSeen) {
|
|
|
3613
3670
|
state.announcement = null
|
|
3614
3671
|
$('modal-announcement').classList.add('hidden')
|
|
3615
3672
|
}
|
|
3616
|
-
async function
|
|
3673
|
+
async function fetchAnnouncements() {
|
|
3617
3674
|
const base = updateBase()
|
|
3618
3675
|
if (!base || !state.localVersion) return false
|
|
3619
3676
|
try {
|
|
@@ -3626,16 +3683,36 @@ async function checkAnnouncements() {
|
|
|
3626
3683
|
const data = JSON.parse(raw)
|
|
3627
3684
|
const source = Array.isArray(data) ? data : (Array.isArray(data?.items) ? data.items : [data])
|
|
3628
3685
|
const normalized = source.map(item => normalizeAnnouncement(item, base)).filter(Boolean)
|
|
3686
|
+
state.announcements = normalized.sort((a, b) => Number(b.publishedAt || 0) - Number(a.publishedAt || 0))
|
|
3629
3687
|
storeAnnouncementHistory(normalized)
|
|
3688
|
+
renderAnnouncementBoard()
|
|
3630
3689
|
const seen = readSeenAnnouncements()
|
|
3631
3690
|
const items = normalized.filter(item => !seen[item.id])
|
|
3632
3691
|
.sort((a, b) => b.publishedAt - a.publishedAt)
|
|
3633
|
-
if (!items.length) return false
|
|
3692
|
+
if (!items.length || state.announcement) return false
|
|
3634
3693
|
openAnnouncementModal(items[0])
|
|
3635
3694
|
return true
|
|
3636
3695
|
} catch { return false }
|
|
3637
3696
|
}
|
|
3638
3697
|
|
|
3698
|
+
function checkAnnouncements() {
|
|
3699
|
+
if (announcementCheckPromise) return announcementCheckPromise
|
|
3700
|
+
announcementCheckPromise = fetchAnnouncements().finally(() => { announcementCheckPromise = null })
|
|
3701
|
+
return announcementCheckPromise
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
function startAnnouncementPolling() {
|
|
3705
|
+
if (announcementPollingStarted) return
|
|
3706
|
+
announcementPollingStarted = true
|
|
3707
|
+
announcementPollTimer = setInterval(() => {
|
|
3708
|
+
if (!document.hidden) void checkAnnouncements()
|
|
3709
|
+
}, ANNOUNCEMENTS_POLL_MS)
|
|
3710
|
+
document.addEventListener('visibilitychange', () => {
|
|
3711
|
+
if (!document.hidden) void checkAnnouncements()
|
|
3712
|
+
})
|
|
3713
|
+
window.addEventListener('online', () => { void checkAnnouncements() })
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3639
3716
|
/* ---------------- 更新内容弹窗 ---------------- */
|
|
3640
3717
|
const NOTES_KEY = 'seenNotesVersion'
|
|
3641
3718
|
let notesVersion = ''
|
|
@@ -4321,36 +4398,155 @@ function initToken() {
|
|
|
4321
4398
|
$('server-desc').textContent = state.server || t('servers.defaultDesc')
|
|
4322
4399
|
}
|
|
4323
4400
|
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4401
|
+
let dshControlPollPromise = null
|
|
4402
|
+
|
|
4403
|
+
function dshControlButtonsBusy(busy, supported = true) {
|
|
4327
4404
|
const buttons = [$('btn-dsh-start'), $('btn-dsh-restart')].filter(Boolean)
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4405
|
+
buttons.forEach(button => {
|
|
4406
|
+
button.disabled = busy || !supported
|
|
4407
|
+
button.classList.toggle('hidden', !supported)
|
|
4408
|
+
})
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
function dshControlFailureText(value) {
|
|
4412
|
+
const key = {
|
|
4413
|
+
SERVICE_NOT_FOUND: 'settings.dshErrorServiceNotFound',
|
|
4414
|
+
INVALID_SERVICE: 'settings.dshErrorInvalidService',
|
|
4415
|
+
SYSTEMCTL_NOT_FOUND: 'settings.dshErrorSystemctlNotFound',
|
|
4416
|
+
SYSTEMD_UNAVAILABLE: 'settings.dshErrorSystemdUnavailable',
|
|
4417
|
+
PERMISSION_DENIED: 'settings.dshErrorPermissionDenied',
|
|
4418
|
+
COMMAND_TIMEOUT: 'settings.dshErrorCommandTimeout',
|
|
4419
|
+
COMMAND_FAILED: 'settings.dshErrorCommandFailed',
|
|
4420
|
+
SERVICE_FAILED: 'settings.dshErrorServiceFailed',
|
|
4421
|
+
SERVICE_TIMEOUT: 'settings.dshErrorServiceTimeout',
|
|
4422
|
+
UPSTREAM_TIMEOUT: 'settings.dshErrorUpstreamTimeout',
|
|
4423
|
+
EVENTS_TIMEOUT: 'settings.dshErrorEventsTimeout',
|
|
4424
|
+
OPERATION_NOT_FOUND: 'settings.dshErrorOperationNotFound',
|
|
4425
|
+
OPERATION_IN_PROGRESS: 'settings.dshErrorOperationProgress',
|
|
4426
|
+
}[value?.code]
|
|
4427
|
+
return key
|
|
4428
|
+
? t(key, { service: value?.service || value?.status?.service || 'dsh-web' })
|
|
4429
|
+
: t('settings.dshErrorGeneric', { msg: value?.error || value?.message || value?.code || t('conn.off') })
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
function dshControlStageText(operation, step = operation) {
|
|
4433
|
+
const seconds = Math.max(0, Math.round(Number(step?.elapsedMs ?? operation?.elapsedMs ?? 0) / 1000))
|
|
4434
|
+
const action = operation?.action === 'start' ? t('settings.dshStart') : t('settings.dshRestart')
|
|
4435
|
+
const service = operation?.service || operation?.status?.service || 'dsh-web'
|
|
4436
|
+
if (step?.stage === 'queued') return t('settings.dshStageQueued')
|
|
4437
|
+
if (step?.stage === 'checking') return t('settings.dshStageChecking', { service })
|
|
4438
|
+
if (step?.stage === 'command') return t('settings.dshStageCommand', { action })
|
|
4439
|
+
if (step?.stage === 'waiting-service') return t('settings.dshStageWaitingService', { seconds })
|
|
4440
|
+
if (step?.stage === 'waiting-upstream') return t('settings.dshStageWaitingUpstream', { seconds })
|
|
4441
|
+
if (step?.stage === 'waiting-events') return t('settings.dshStageWaitingEvents', { seconds })
|
|
4442
|
+
if (step?.stage === 'complete') {
|
|
4443
|
+
if (operation?.code === 'ALREADY_RUNNING') return t('settings.dshAlreadyRunning', { pid: operation?.status?.mainPid || '—' })
|
|
4444
|
+
return t('settings.dshSuccessDetail', {
|
|
4445
|
+
action,
|
|
4446
|
+
pid: operation?.status?.mainPid || '—',
|
|
4447
|
+
status: operation?.upstream?.status || '—',
|
|
4448
|
+
seconds: Math.max(0, Math.round(Number(operation?.elapsedMs || step?.elapsedMs || 0) / 1000)),
|
|
4449
|
+
})
|
|
4450
|
+
}
|
|
4451
|
+
if (step?.stage === 'failed') return dshControlFailureText(operation)
|
|
4452
|
+
return step?.message || operation?.message || '—'
|
|
4453
|
+
}
|
|
4454
|
+
|
|
4455
|
+
function renderDshControlOperation(operation) {
|
|
4456
|
+
const desc = $('dsh-control-desc')
|
|
4457
|
+
const box = $('dsh-control-steps')
|
|
4458
|
+
if (!desc || !box || !operation) return
|
|
4459
|
+
const failed = operation.stage === 'failed' || operation.done && operation.ok === false
|
|
4460
|
+
desc.textContent = failed ? t('settings.dshFailed', { msg: dshControlFailureText(operation) }) : dshControlStageText(operation)
|
|
4461
|
+
const steps = Array.isArray(operation.steps) && operation.steps.length
|
|
4462
|
+
? operation.steps
|
|
4463
|
+
: [{ stage: operation.stage || 'queued', elapsedMs: operation.elapsedMs || 0, message: operation.message || '' }]
|
|
4464
|
+
box.classList.remove('hidden')
|
|
4465
|
+
box.innerHTML = steps.map((step, index) => {
|
|
4466
|
+
const current = !operation.done && index === steps.length - 1
|
|
4467
|
+
const isFailed = step.stage === 'failed'
|
|
4468
|
+
const success = step.stage === 'complete'
|
|
4469
|
+
const mark = isFailed ? '×' : success ? '✓' : current ? '…' : '✓'
|
|
4470
|
+
const cls = isFailed ? 'failed' : success ? 'success' : current ? 'current' : ''
|
|
4471
|
+
const detail = isFailed && operation.detail
|
|
4472
|
+
? `<span class="dsh-control-step-detail">${esc(t('settings.dshErrorDetail', { detail: operation.detail }))}</span>`
|
|
4473
|
+
: ''
|
|
4474
|
+
return `<div class="dsh-control-step ${cls}"><span class="dsh-control-step-mark" aria-hidden="true">${mark}</span><span>${esc(dshControlStageText(operation, step))}</span>${detail}</div>`
|
|
4475
|
+
}).join('')
|
|
4476
|
+
dshControlButtonsBusy(!operation.done, true)
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
async function readDshControlResponse(res) {
|
|
4480
|
+
const text = await res.text()
|
|
4481
|
+
if (!text) return {}
|
|
4482
|
+
try { return JSON.parse(text) } catch { return { error: `HTTP ${res.status}`, detail: text.slice(0, 500) } }
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
async function pollDshControlOperation(operationId, initial) {
|
|
4486
|
+
let value = initial
|
|
4487
|
+
while (true) {
|
|
4488
|
+
renderDshControlOperation(value)
|
|
4489
|
+
if (value.done) return value
|
|
4490
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 700))
|
|
4491
|
+
const res = await fetch(adminApiUrl(`/admin/api/dsh?operation=${encodeURIComponent(operationId)}`), {
|
|
4492
|
+
headers: { authorization: 'Bearer ' + state.token }, cache: 'no-store'
|
|
4493
|
+
})
|
|
4494
|
+
if (res.status === 401) { authFailure(); throw Object.assign(new Error('unauthorized'), { auth: true }) }
|
|
4495
|
+
const next = await readDshControlResponse(res)
|
|
4496
|
+
if (!res.ok) throw Object.assign(new Error(next.error || next.message || `HTTP ${res.status}`), { dshPayload: { ...next, httpStatus: res.status } })
|
|
4497
|
+
value = next
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
function renderDshControlStatus(value) {
|
|
4502
|
+
const desc = $('dsh-control-desc')
|
|
4503
|
+
const box = $('dsh-control-steps')
|
|
4504
|
+
if (!desc || !value) return
|
|
4505
|
+
if (value.supported === false) {
|
|
4506
|
+
desc.textContent = value.code ? dshControlFailureText(value) : value.message || t('settings.dshUnsupported')
|
|
4507
|
+
dshControlButtonsBusy(false, false)
|
|
4331
4508
|
return
|
|
4332
4509
|
}
|
|
4333
|
-
|
|
4334
|
-
|
|
4510
|
+
if (value.ok === false) {
|
|
4511
|
+
desc.textContent = t('settings.dshStatusFailed', { msg: dshControlFailureText(value) })
|
|
4512
|
+
dshControlButtonsBusy(false, false)
|
|
4513
|
+
return
|
|
4514
|
+
}
|
|
4515
|
+
dshControlButtonsBusy(false, true)
|
|
4516
|
+
if (box && !dshControlPollPromise) box.classList.add('hidden')
|
|
4517
|
+
const service = value.service || 'dsh-web'
|
|
4518
|
+
const serviceState = `${value.activeState || value.state || 'unknown'}/${value.subState || 'unknown'}`
|
|
4519
|
+
desc.textContent = value.running
|
|
4520
|
+
? t('settings.dshRunningDetail', { service, pid: value.mainPid || '—', state: serviceState })
|
|
4521
|
+
: t('settings.dshStoppedDetail', { service, state: serviceState })
|
|
4335
4522
|
}
|
|
4336
4523
|
|
|
4337
4524
|
async function loadDshControl() {
|
|
4338
4525
|
if (!state.token || !$('dsh-control-desc')) return
|
|
4339
4526
|
try {
|
|
4340
|
-
const res = await fetch(adminApiUrl('/admin/api/dsh'), { headers: { authorization: 'Bearer ' + state.token } })
|
|
4527
|
+
const res = await fetch(adminApiUrl('/admin/api/dsh'), { headers: { authorization: 'Bearer ' + state.token }, cache: 'no-store' })
|
|
4341
4528
|
if (res.status === 401) return authFailure()
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4529
|
+
const value = await readDshControlResponse(res)
|
|
4530
|
+
if (!res.ok) throw Object.assign(new Error(value.error || value.message || `HTTP ${res.status}`), { dshPayload: value })
|
|
4531
|
+
renderDshControlStatus(value)
|
|
4532
|
+
if (value.operation?.operationId && !value.operation.done && !dshControlPollPromise) {
|
|
4533
|
+
dshControlPollPromise = pollDshControlOperation(value.operation.operationId, value.operation)
|
|
4534
|
+
.then(renderDshControlOperation)
|
|
4535
|
+
.catch(error => {
|
|
4536
|
+
if (!error?.auth) $('dsh-control-desc').textContent = t('settings.dshFailed', { msg: error?.dshPayload ? dshControlFailureText(error.dshPayload) : t('settings.dshResultUnknown') })
|
|
4537
|
+
})
|
|
4538
|
+
.finally(() => { dshControlPollPromise = null; dshControlButtonsBusy(false, true) })
|
|
4539
|
+
}
|
|
4540
|
+
} catch (error) {
|
|
4541
|
+
$('dsh-control-desc').textContent = t('settings.dshStatusFailed', { msg: error?.dshPayload ? dshControlFailureText(error.dshPayload) : t('conn.off') })
|
|
4345
4542
|
}
|
|
4346
4543
|
}
|
|
4347
4544
|
|
|
4348
4545
|
async function controlDsh(action) {
|
|
4546
|
+
if (dshControlPollPromise) return
|
|
4349
4547
|
const label = action === 'start' ? t('settings.dshStart') : t('settings.dshRestart')
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
const desc = $('dsh-control-desc')
|
|
4353
|
-
if (desc) desc.textContent = t('settings.dshStarting', { action: label })
|
|
4548
|
+
dshControlButtonsBusy(true, true)
|
|
4549
|
+
renderDshControlOperation({ action, service: 'dsh-web', accepted: true, done: false, stage: 'queued', elapsedMs: 0, steps: [] })
|
|
4354
4550
|
try {
|
|
4355
4551
|
const res = await fetch(adminApiUrl('/admin/api/dsh'), {
|
|
4356
4552
|
method: 'POST',
|
|
@@ -4358,15 +4554,33 @@ async function controlDsh(action) {
|
|
|
4358
4554
|
body: JSON.stringify({ action }),
|
|
4359
4555
|
})
|
|
4360
4556
|
if (res.status === 401) return authFailure()
|
|
4361
|
-
|
|
4362
|
-
if (
|
|
4557
|
+
let v = await readDshControlResponse(res)
|
|
4558
|
+
if (res.status === 409 && v.operation?.operationId) v = v.operation
|
|
4559
|
+
else if (!res.ok) throw Object.assign(new Error(v.error || v.message || `HTTP ${res.status}`), { dshPayload: { ...v, httpStatus: res.status } })
|
|
4560
|
+
|
|
4561
|
+
if (v.accepted && v.operationId) {
|
|
4562
|
+
dshControlPollPromise = pollDshControlOperation(v.operationId, v)
|
|
4563
|
+
v = await dshControlPollPromise
|
|
4564
|
+
if (!v.ok) throw Object.assign(new Error(dshControlFailureText(v)), { dshPayload: v })
|
|
4565
|
+
renderDshControlOperation(v)
|
|
4566
|
+
toast(dshControlStageText(v), 'ok')
|
|
4567
|
+
return
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
// 兼容旧网关:它会在单个 POST 中直接返回最终状态。
|
|
4571
|
+
if (v.ok === false) throw Object.assign(new Error(v.error || v.message || `HTTP ${res.status}`), { dshPayload: v })
|
|
4363
4572
|
renderDshControlStatus(v)
|
|
4364
4573
|
toast(t('settings.dshStarted', { action: label }), 'ok')
|
|
4365
|
-
} catch (
|
|
4366
|
-
if (
|
|
4367
|
-
|
|
4574
|
+
} catch (error) {
|
|
4575
|
+
if (error?.auth) return
|
|
4576
|
+
const payload = error?.dshPayload
|
|
4577
|
+
const message = payload ? dshControlFailureText(payload) : t('settings.dshResultUnknown')
|
|
4578
|
+
if (payload) renderDshControlOperation({ action, service: payload.service || 'dsh-web', done: true, ok: false, stage: 'failed', steps: payload.steps || [], ...payload })
|
|
4579
|
+
else $('dsh-control-desc').textContent = t('settings.dshFailed', { msg: message })
|
|
4580
|
+
toast(t('settings.dshFailed', { msg: message }), 'err')
|
|
4368
4581
|
} finally {
|
|
4369
|
-
|
|
4582
|
+
dshControlPollPromise = null
|
|
4583
|
+
dshControlButtonsBusy(false, true)
|
|
4370
4584
|
}
|
|
4371
4585
|
}
|
|
4372
4586
|
|
|
@@ -4427,6 +4641,7 @@ function bindUi() {
|
|
|
4427
4641
|
renderServers()
|
|
4428
4642
|
renderSessions()
|
|
4429
4643
|
renderWorkbench()
|
|
4644
|
+
renderAnnouncementBoard()
|
|
4430
4645
|
renderPending(); renderQueue(); renderJobs()
|
|
4431
4646
|
updateConn()
|
|
4432
4647
|
if (state.current) { renderSessionTitle(); renderSessionSub(); renderSessionCards(); renderHistory(true) }
|
|
@@ -4526,6 +4741,11 @@ function bindUi() {
|
|
|
4526
4741
|
$('btn-stats').addEventListener('click', () => { renderSessionCards(); $('modal-stats').classList.remove('hidden') })
|
|
4527
4742
|
$('stats-close').addEventListener('click', () => $('modal-stats').classList.add('hidden'))
|
|
4528
4743
|
$('btn-refresh').addEventListener('click', () => { toast(t('common.refreshing')); openStreams(); refreshAll() })
|
|
4744
|
+
$('overview-refresh').addEventListener('click', () => {
|
|
4745
|
+
toast(t('common.refreshing'))
|
|
4746
|
+
openStreams()
|
|
4747
|
+
void Promise.all([refreshAll(), checkAnnouncements()])
|
|
4748
|
+
})
|
|
4529
4749
|
// 反馈
|
|
4530
4750
|
$('btn-feedback').addEventListener('click', (e) => { e.stopPropagation(); toggleFeedbackSheet() })
|
|
4531
4751
|
$('feedback-backdrop').addEventListener('click', closeFeedbackSheet)
|
|
@@ -4540,6 +4760,10 @@ function bindUi() {
|
|
|
4540
4760
|
$('btn-write-feedback').addEventListener('click', () => { closeFeedbackSheet(); openFeedbackModal() })
|
|
4541
4761
|
$('fb-cancel').addEventListener('click', closeFeedbackModal)
|
|
4542
4762
|
$('fb-submit').addEventListener('click', submitFeedback)
|
|
4763
|
+
$('feedback-success-confirm').addEventListener('click', closeFeedbackSuccess)
|
|
4764
|
+
$('modal-feedback-success').addEventListener('click', (e) => {
|
|
4765
|
+
if (e.target === $('modal-feedback-success')) closeFeedbackSuccess()
|
|
4766
|
+
})
|
|
4543
4767
|
document.querySelectorAll('#fb-chips .fb-chip').forEach(btn =>
|
|
4544
4768
|
btn.addEventListener('click', () => {
|
|
4545
4769
|
state.feedbackType = btn.dataset.fbType
|
|
@@ -4550,6 +4774,7 @@ function bindUi() {
|
|
|
4550
4774
|
})
|
|
4551
4775
|
document.addEventListener('keydown', (e) => {
|
|
4552
4776
|
if (e.key === 'Escape' && !$('feedback-sheet').classList.contains('hidden')) { closeFeedbackSheet(); $('btn-feedback').focus() }
|
|
4777
|
+
else if (e.key === 'Escape' && !$('modal-feedback-success').classList.contains('hidden')) closeFeedbackSuccess()
|
|
4553
4778
|
})
|
|
4554
4779
|
$('btn-new-session').addEventListener('click', newSession)
|
|
4555
4780
|
$('session-workspace-filter').addEventListener('change', (e) => {
|
|
@@ -4699,6 +4924,13 @@ function bindUi() {
|
|
|
4699
4924
|
$('modal-announcement-history').addEventListener('click', (e) => {
|
|
4700
4925
|
if (e.target === $('modal-announcement-history')) closeAnnouncementHistory()
|
|
4701
4926
|
})
|
|
4927
|
+
$('overview-announcement-history').addEventListener('click', openAnnouncementHistory)
|
|
4928
|
+
$('overview-announcement-list').addEventListener('click', (e) => {
|
|
4929
|
+
const button = e.target.closest('[data-home-announcement]')
|
|
4930
|
+
if (!button) return
|
|
4931
|
+
const item = announcementBoardItems().find(entry => entry.id === button.dataset.homeAnnouncement)
|
|
4932
|
+
if (item) openAnnouncementModal(item)
|
|
4933
|
+
})
|
|
4702
4934
|
// 设置
|
|
4703
4935
|
$('view-settings').addEventListener('click', (e) => {
|
|
4704
4936
|
const group = e.target.closest('[data-settings-group]')
|
|
@@ -4873,6 +5105,7 @@ function applyNativeInsets() {
|
|
|
4873
5105
|
async function boot() {
|
|
4874
5106
|
initToken()
|
|
4875
5107
|
bindUi()
|
|
5108
|
+
renderAnnouncementBoard()
|
|
4876
5109
|
renderLangBtn()
|
|
4877
5110
|
bindNativeBack()
|
|
4878
5111
|
bindNativeLinks()
|
|
@@ -4892,7 +5125,9 @@ async function boot() {
|
|
|
4892
5125
|
if (host) { state.hostInfo = host; $('host-desc').textContent = t('settings.hostDesc', { version: host.version, cwd: host.cwd, n: host.attachedSessions }) }
|
|
4893
5126
|
loadDshControl()
|
|
4894
5127
|
}
|
|
4895
|
-
//
|
|
5128
|
+
// 网关从中央 HTTPS 公告源读取并在不可达时回退内置文件。前台每 30 秒检查,
|
|
5129
|
+
// 回到前台或网络恢复时立即补查;公告优先,避免启动时两个弹窗重叠。
|
|
5130
|
+
startAnnouncementPolling()
|
|
4896
5131
|
setTimeout(async () => {
|
|
4897
5132
|
const shown = await checkAnnouncements()
|
|
4898
5133
|
if (!shown && state.token) checkUpdate(true)
|