dsh-remote-plugin 0.6.11 → 0.6.13
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 +760 -82
- package/index.mjs +32 -6
- package/package.json +1 -1
- package/public/admin.html +157 -8
- package/public/admin.js +264 -5
- package/public/announcements.json +35 -0
- package/public/app.js +475 -55
- package/public/desktop/desktop.css +5 -0
- package/public/desktop/desktop.html +4 -2
- package/public/desktop/desktop.js +103 -14
- package/public/index.html +79 -16
- package/public/styles.css +64 -2
- package/public/update.json +13 -13
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -55,6 +55,7 @@ const state = {
|
|
|
55
55
|
autoSelect: { '默认': true }, // 组内自动测速选优 / 手动指定
|
|
56
56
|
groupActive: { '默认': '' }, // 每组当前生效的 server id(手动模式)
|
|
57
57
|
serverLatency: {}, // url -> 最近一次 /health 测速毫秒数
|
|
58
|
+
gatewayHealth: {}, // url -> /health 协议版本与能力声明
|
|
58
59
|
selectingServer: false, // 防重入: 测速/切换中
|
|
59
60
|
sessions: [],
|
|
60
61
|
sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
|
|
@@ -64,7 +65,9 @@ const state = {
|
|
|
64
65
|
hostInfo: null,
|
|
65
66
|
localVersion: '',
|
|
66
67
|
updateInfo: null,
|
|
68
|
+
warnedGatewayVersions: new Set(),
|
|
67
69
|
announcement: null,
|
|
70
|
+
announcements: [],
|
|
68
71
|
approvals: [], // 待处理审批
|
|
69
72
|
questions: [], // 待处理提问
|
|
70
73
|
queues: {}, // sessionId -> queue items
|
|
@@ -272,6 +275,14 @@ function openFeedbackModal() {
|
|
|
272
275
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
273
276
|
}
|
|
274
277
|
function closeFeedbackModal() { $('modal-feedback').classList.add('hidden') }
|
|
278
|
+
function openFeedbackSuccess() {
|
|
279
|
+
$('modal-feedback-success').classList.remove('hidden')
|
|
280
|
+
setTimeout(() => $('feedback-success-confirm').focus(), 50)
|
|
281
|
+
}
|
|
282
|
+
function closeFeedbackSuccess() {
|
|
283
|
+
$('modal-feedback-success').classList.add('hidden')
|
|
284
|
+
$('btn-feedback')?.focus()
|
|
285
|
+
}
|
|
275
286
|
async function submitFeedback() {
|
|
276
287
|
const type = state.feedbackType || 'bug'
|
|
277
288
|
const message = $('fb-msg').value.trim()
|
|
@@ -290,7 +301,7 @@ async function submitFeedback() {
|
|
|
290
301
|
})
|
|
291
302
|
let json = {}
|
|
292
303
|
try { json = await res.json() } catch {}
|
|
293
|
-
if (res.ok && json.ok) {
|
|
304
|
+
if (res.ok && json.ok) { closeFeedbackModal(); openFeedbackSuccess() }
|
|
294
305
|
else if (res.status === 429) { toast(json.retryAfter ? t('feedback.rateLimitedAt', { n: json.retryAfter }) : t('feedback.rateLimited'), 'err') }
|
|
295
306
|
else { toast(t('feedback.submitFailed', { msg: json.error || res.status }), 'err') }
|
|
296
307
|
} catch {
|
|
@@ -346,6 +357,7 @@ async function getWsTicket() {
|
|
|
346
357
|
const token = state.token
|
|
347
358
|
const server = state.server
|
|
348
359
|
wsTicketPromise = (async () => {
|
|
360
|
+
if (activeGatewayCapability('wsTicket') === false) throw new Error('ws ticket unsupported')
|
|
349
361
|
const res = await fetch(apiUrl('/api/ws-ticket'), {
|
|
350
362
|
method: 'POST',
|
|
351
363
|
headers: {
|
|
@@ -612,7 +624,10 @@ async function pingServer(base) {
|
|
|
612
624
|
const timer = setTimeout(() => ctrl.abort(), 3500)
|
|
613
625
|
try {
|
|
614
626
|
const res = await fetch(u + '/health?t=' + Date.now(), { signal: ctrl.signal, cache: 'no-store' })
|
|
615
|
-
|
|
627
|
+
if (!res.ok) return Infinity
|
|
628
|
+
const health = await res.json().catch(() => null)
|
|
629
|
+
if (health && typeof health === 'object') state.gatewayHealth[u] = health
|
|
630
|
+
return Math.round(performance.now() - t0)
|
|
616
631
|
} catch {
|
|
617
632
|
return Infinity
|
|
618
633
|
} finally {
|
|
@@ -620,6 +635,13 @@ async function pingServer(base) {
|
|
|
620
635
|
}
|
|
621
636
|
}
|
|
622
637
|
|
|
638
|
+
function activeGatewayCapability(name) {
|
|
639
|
+
const key = String(state.server || location.origin || '').replace(/\/+$/, '')
|
|
640
|
+
const capabilities = state.gatewayHealth[key]?.capabilities
|
|
641
|
+
if (!capabilities || !Object.prototype.hasOwnProperty.call(capabilities, name)) return null
|
|
642
|
+
return Number(capabilities[name]) > 0
|
|
643
|
+
}
|
|
644
|
+
|
|
623
645
|
async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
624
646
|
if (state.selectingServer) return null
|
|
625
647
|
state.selectingServer = true
|
|
@@ -668,6 +690,7 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
|
668
690
|
else if (chosen) toast(t('speed.manualUsing', { url: chosen, ms: Number.isFinite(ms) ? ms : '—' }), 'ok')
|
|
669
691
|
else toast(t('speed.allDown'), 'err')
|
|
670
692
|
}
|
|
693
|
+
await maybeWarnAppBehindGateway()
|
|
671
694
|
return chosen
|
|
672
695
|
} finally {
|
|
673
696
|
state.selectingServer = false
|
|
@@ -1334,6 +1357,10 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
1334
1357
|
}
|
|
1335
1358
|
function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('session.unknown')) }
|
|
1336
1359
|
function short(id) { return '…' + String(id).slice(-8) }
|
|
1360
|
+
function isTopLevelSession(session) {
|
|
1361
|
+
return !!session && !session.parentSessionId && session.origin !== 'subagent'
|
|
1362
|
+
}
|
|
1363
|
+
function topLevelSessions() { return state.sessions.filter(isTopLevelSession) }
|
|
1337
1364
|
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
1338
1365
|
function isGoalTerminal(goal) {
|
|
1339
1366
|
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
@@ -1382,12 +1409,17 @@ function workspaceItems() {
|
|
|
1382
1409
|
function workspaceById(workspaceId) {
|
|
1383
1410
|
return workspaceItems().find(w => w.workspaceId === workspaceId) || null
|
|
1384
1411
|
}
|
|
1412
|
+
function workspaceOwnsSession(workspace, session) {
|
|
1413
|
+
if (!workspace || !session?.sessionId) return false
|
|
1414
|
+
if (String(session.workspaceId || '') === workspace.workspaceId) return true
|
|
1415
|
+
if (Array.isArray(workspace.sessionIds) && workspace.sessionIds.some(id => String(id) === String(session.sessionId))) return true
|
|
1416
|
+
const cwdKey = wbPathKey(sessionCwd(session))
|
|
1417
|
+
const workspaceKey = wbPathKey(workspace.path)
|
|
1418
|
+
return !!cwdKey && !!workspaceKey && (cwdKey === workspaceKey || cwdKey.startsWith(workspaceKey.endsWith('/') ? workspaceKey : workspaceKey + '/'))
|
|
1419
|
+
}
|
|
1385
1420
|
function workspaceForSession(session) {
|
|
1386
1421
|
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
|
|
1422
|
+
return workspaceItems().find(workspace => workspaceOwnsSession(workspace, session)) || null
|
|
1391
1423
|
}
|
|
1392
1424
|
function workspaceName(workspace) {
|
|
1393
1425
|
return String(workspace?.title || wbBaseName(workspace?.path) || workspace?.path || '').trim()
|
|
@@ -1411,7 +1443,10 @@ function renderWorkspaceNavigation() {
|
|
|
1411
1443
|
LS.del('workspaceFilterV1')
|
|
1412
1444
|
}
|
|
1413
1445
|
const sessionSelect = $('session-workspace-filter')
|
|
1414
|
-
if (sessionSelect)
|
|
1446
|
+
if (sessionSelect) {
|
|
1447
|
+
sessionSelect.innerHTML = workspaceOptionsHtml({ all: true, ungrouped: true, selected: state.workspaceFilter })
|
|
1448
|
+
syncCustomSelect(sessionSelect)
|
|
1449
|
+
}
|
|
1415
1450
|
const selectedWorkspace = workspaceById(state.workspaceFilter)
|
|
1416
1451
|
const pathBox = $('session-workspace-path')
|
|
1417
1452
|
if (pathBox) pathBox.textContent = selectedWorkspace?.path || (state.workspaceFilter === WORKSPACE_UNGROUPED ? t('workspace.ungrouped') : t('workspace.allPath'))
|
|
@@ -1423,7 +1458,10 @@ function renderWorkspaceNavigation() {
|
|
|
1423
1458
|
LS.del('fsWorkspaceIdV1')
|
|
1424
1459
|
}
|
|
1425
1460
|
const fsSelect = $('fs-workspace')
|
|
1426
|
-
if (fsSelect)
|
|
1461
|
+
if (fsSelect) {
|
|
1462
|
+
fsSelect.innerHTML = workspaceOptionsHtml({ root: true, selected: state.fs.workspaceId })
|
|
1463
|
+
syncCustomSelect(fsSelect)
|
|
1464
|
+
}
|
|
1427
1465
|
if ($('modal-new-session') && !$('modal-new-session').classList.contains('hidden')) renderNewSessionWorkspace()
|
|
1428
1466
|
return items
|
|
1429
1467
|
}
|
|
@@ -1503,7 +1541,7 @@ function renderWorkbench() {
|
|
|
1503
1541
|
panel.innerHTML = projects.map(w => {
|
|
1504
1542
|
const id = String(w.workspaceId || '')
|
|
1505
1543
|
const open = !!state.wbOpenProjects[id]
|
|
1506
|
-
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(
|
|
1544
|
+
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId))
|
|
1507
1545
|
const body = open ? `<div class="wb-sessions">${sessions.length ? sessions.map(s => `
|
|
1508
1546
|
<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
1509
1547
|
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}">
|
|
@@ -1539,7 +1577,7 @@ function workspaceDisplayName(label) {
|
|
|
1539
1577
|
return parts[parts.length - 1] || value
|
|
1540
1578
|
}
|
|
1541
1579
|
function sortedSessions() {
|
|
1542
|
-
const items =
|
|
1580
|
+
const items = topLevelSessions()
|
|
1543
1581
|
if (state.sessionSort === 'workspace') {
|
|
1544
1582
|
return items.sort((a, b) => {
|
|
1545
1583
|
const aw = sessionCwd(a) || '\uffff'
|
|
@@ -1556,9 +1594,8 @@ function renderSessions() {
|
|
|
1556
1594
|
const archivedSet = new Set(state.wbArchived || [])
|
|
1557
1595
|
const visible = allItems.filter(s => {
|
|
1558
1596
|
if (!state.workspaceFilter) return true
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
return workspace?.workspaceId === state.workspaceFilter
|
|
1597
|
+
if (state.workspaceFilter === WORKSPACE_UNGROUPED) return !workspaceForSession(s)
|
|
1598
|
+
return workspaceOwnsSession(workspaceById(state.workspaceFilter), s)
|
|
1562
1599
|
})
|
|
1563
1600
|
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1564
1601
|
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
@@ -1607,7 +1644,7 @@ function renderSessions() {
|
|
|
1607
1644
|
const sort = $('session-sort')
|
|
1608
1645
|
if (sort) { sort.value = state.sessionSort; syncCustomSelect(sort) }
|
|
1609
1646
|
$('home-empty').classList.toggle('hidden', visible.length > 0)
|
|
1610
|
-
const running =
|
|
1647
|
+
const running = topLevelSessions().filter(s => s.running).length
|
|
1611
1648
|
const pending = state.approvals.length + state.questions.length
|
|
1612
1649
|
$('stat-strip').innerHTML = `
|
|
1613
1650
|
<div class="stat running"><div class="v">${running}</div><div class="k">${t('sessions.statRunning')}</div></div>
|
|
@@ -1649,7 +1686,7 @@ function bindNativeBack() {
|
|
|
1649
1686
|
if ($('composer-wrap')?.classList.contains('fs')) { setComposerFullscreen(false); return }
|
|
1650
1687
|
if (customSelectCurrent) { closeCustomSelect(); return }
|
|
1651
1688
|
const openModal = [...document.querySelectorAll('.modal')].find(m => !m.classList.contains('hidden'))
|
|
1652
|
-
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1689
|
+
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else if (openModal.id === 'modal-app-version-warning') closeAppVersionWarning(false); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1653
1690
|
if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
|
|
1654
1691
|
if (!$('view-files').classList.contains('hidden')) { // 文件页 → 上级目录 → 主页
|
|
1655
1692
|
if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) { fsUp(); return }
|
|
@@ -1697,10 +1734,78 @@ const HISTORY_MAX_VISIBLE = 5000 // 已加载的可显示事件上限(消息/
|
|
|
1697
1734
|
function emptyHistory() {
|
|
1698
1735
|
return {
|
|
1699
1736
|
visible: [], seqs: new Set(), minSeq: Infinity,
|
|
1700
|
-
hasMore: false, loading: false, renderStart: 0, renderEnd: 0
|
|
1737
|
+
hasMore: false, loading: false, renderStart: 0, renderEnd: 0,
|
|
1738
|
+
partialReasoning: new Map()
|
|
1701
1739
|
}
|
|
1702
1740
|
}
|
|
1703
1741
|
|
|
1742
|
+
function reasoningStreamKey(data, index) {
|
|
1743
|
+
return `${data?.turn ?? '?'}:${data?.step ?? '?'}:${index ?? '?'}`
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* DSH 的实时思考以 assistant/chunk 下发,历史尾页可能把增量压成
|
|
1748
|
+
* reasoning-chunks。最终 assistant/message 到达后再由正式消息接管展示。
|
|
1749
|
+
*/
|
|
1750
|
+
function applyReasoningStreamEvent(event) {
|
|
1751
|
+
const h = state.history
|
|
1752
|
+
const data = event?.data || {}
|
|
1753
|
+
let changed = false
|
|
1754
|
+
if (event?.type === 'assistant/chunk') {
|
|
1755
|
+
const chunk = data.chunk || {}
|
|
1756
|
+
const key = reasoningStreamKey(data, chunk.index)
|
|
1757
|
+
if (chunk.type === 'block-start' && chunk.blockType === 'reasoning') {
|
|
1758
|
+
h.partialReasoning.set(key, { turn: data.turn, step: data.step, index: chunk.index, text: '' })
|
|
1759
|
+
changed = true
|
|
1760
|
+
} else if (chunk.type === 'reasoning-delta') {
|
|
1761
|
+
const item = h.partialReasoning.get(key) || { turn: data.turn, step: data.step, index: chunk.index, text: '' }
|
|
1762
|
+
item.text += String(chunk.text || '')
|
|
1763
|
+
h.partialReasoning.set(key, item)
|
|
1764
|
+
changed = true
|
|
1765
|
+
} else if (chunk.type === 'block-end' && chunk.block?.type === 'reasoning') {
|
|
1766
|
+
h.partialReasoning.set(key, {
|
|
1767
|
+
turn: data.turn, step: data.step, index: chunk.index,
|
|
1768
|
+
text: String(chunk.block.text ?? chunk.block.content ?? '')
|
|
1769
|
+
})
|
|
1770
|
+
changed = true
|
|
1771
|
+
}
|
|
1772
|
+
} else if (event?.type === 'reasoning-chunks') {
|
|
1773
|
+
const key = reasoningStreamKey(data, data.index)
|
|
1774
|
+
const item = h.partialReasoning.get(key) || { turn: data.turn, step: data.step, index: data.index, text: '' }
|
|
1775
|
+
item.text += Array.isArray(data.texts) ? data.texts.join('') : String(data.text || '')
|
|
1776
|
+
h.partialReasoning.set(key, item)
|
|
1777
|
+
changed = true
|
|
1778
|
+
} else if (event?.type === 'assistant/message') {
|
|
1779
|
+
for (const [key, item] of h.partialReasoning) {
|
|
1780
|
+
if (item.turn === data.turn && item.step === data.step) {
|
|
1781
|
+
h.partialReasoning.delete(key)
|
|
1782
|
+
changed = true
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return changed
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
let reasoningRenderTimer = null
|
|
1790
|
+
function scheduleReasoningRender() {
|
|
1791
|
+
if (reasoningRenderTimer) return
|
|
1792
|
+
reasoningRenderTimer = setTimeout(() => {
|
|
1793
|
+
reasoningRenderTimer = null
|
|
1794
|
+
if (!state.current) return
|
|
1795
|
+
const box = $('history')
|
|
1796
|
+
const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 240
|
|
1797
|
+
renderHistory(false, nearBottom ? 'bottom' : 'fixed')
|
|
1798
|
+
}, 80)
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function partialReasoningHtml() {
|
|
1802
|
+
return [...state.history.partialReasoning.values()]
|
|
1803
|
+
.filter(item => item.text)
|
|
1804
|
+
.sort((a, b) => (a.turn ?? 0) - (b.turn ?? 0) || (a.step ?? 0) - (b.step ?? 0) || (a.index ?? 0) - (b.index ?? 0))
|
|
1805
|
+
.map(item => `<div class="msg assistant reasoning-live"><div class="role">${esc(t('role.dsh'))}</div><details class="tool" open><summary>${esc(t('block.thinkingLive'))}</summary><div class="tool-text">${esc(truncate(item.text, 12000))}</div></details></div>`)
|
|
1806
|
+
.join('')
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1704
1809
|
function trimVisible() {
|
|
1705
1810
|
const h = state.history
|
|
1706
1811
|
if (h.visible.length <= HISTORY_MAX_VISIBLE) return
|
|
@@ -1787,9 +1892,11 @@ async function loadHistory(reset) {
|
|
|
1787
1892
|
|
|
1788
1893
|
const incoming = v.events || []
|
|
1789
1894
|
let added = 0
|
|
1895
|
+
if (reset) state.history.partialReasoning.clear()
|
|
1790
1896
|
for (const entry of incoming) {
|
|
1791
1897
|
const ev = entry?.event
|
|
1792
1898
|
const seq = ev?.seq
|
|
1899
|
+
applyReasoningStreamEvent(ev)
|
|
1793
1900
|
if (seq == null || state.history.seqs.has(seq)) continue
|
|
1794
1901
|
if (!shouldShowEvent(ev.type)) continue // chunk 等内部事件不保留
|
|
1795
1902
|
state.history.seqs.add(seq)
|
|
@@ -1816,8 +1923,16 @@ async function loadHistory(reset) {
|
|
|
1816
1923
|
|
|
1817
1924
|
function insertLiveEvent(event) {
|
|
1818
1925
|
const h = state.history
|
|
1926
|
+
const reasoningChanged = applyReasoningStreamEvent(event)
|
|
1927
|
+
if (event?.type === 'assistant/chunk' || event?.type === 'reasoning-chunks') {
|
|
1928
|
+
if (reasoningChanged) scheduleReasoningRender()
|
|
1929
|
+
return
|
|
1930
|
+
}
|
|
1819
1931
|
const seq = event?.seq
|
|
1820
|
-
if (seq == null || h.seqs.has(seq) || !shouldShowEvent(event.type))
|
|
1932
|
+
if (seq == null || h.seqs.has(seq) || !shouldShowEvent(event.type)) {
|
|
1933
|
+
if (reasoningChanged) scheduleReasoningRender()
|
|
1934
|
+
return
|
|
1935
|
+
}
|
|
1821
1936
|
h.seqs.add(seq)
|
|
1822
1937
|
h.visible.push({ seq, event })
|
|
1823
1938
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
@@ -1851,7 +1966,8 @@ function renderHistory(reset, mode = 'bottom') {
|
|
|
1851
1966
|
const h = state.history
|
|
1852
1967
|
const filtered = filteredEntries()
|
|
1853
1968
|
const len = filtered.length
|
|
1854
|
-
|
|
1969
|
+
const reasoningHtml = partialReasoningHtml()
|
|
1970
|
+
if (!len && !reasoningHtml) {
|
|
1855
1971
|
box.innerHTML = '<div class="empty">' + t('history.empty') + '</div>'
|
|
1856
1972
|
h.renderStart = 0; h.renderEnd = 0
|
|
1857
1973
|
updateRail()
|
|
@@ -1872,7 +1988,7 @@ function renderHistory(reset, mode = 'bottom') {
|
|
|
1872
1988
|
const d = e.event.data || {}
|
|
1873
1989
|
if (d.callId && d.name) toolNames.set(d.callId, d.name)
|
|
1874
1990
|
}
|
|
1875
|
-
box.innerHTML = filtered.slice(start, end).map(e => eventHtml(e, { toolNames })).join('')
|
|
1991
|
+
box.innerHTML = filtered.slice(start, end).map(e => eventHtml(e, { toolNames })).join('') + reasoningHtml
|
|
1876
1992
|
if (reset || mode === 'bottom') box.scrollTop = box.scrollHeight
|
|
1877
1993
|
else if (mode === 'keep') box.scrollTop = Math.max(0, oldTop + (box.scrollHeight - oldH))
|
|
1878
1994
|
else if (mode === 'fixed') box.scrollTop = oldTop
|
|
@@ -2374,16 +2490,37 @@ function renderEffortMenu() {
|
|
|
2374
2490
|
const cur = state.models.current
|
|
2375
2491
|
const provider = (state.models.groups || []).find(g => g.id === cur?.provider)
|
|
2376
2492
|
const model = (provider?.models || []).find(m => m.id === cur?.model)
|
|
2377
|
-
const efforts = model
|
|
2378
|
-
group.classList.toggle('hidden', !efforts.length)
|
|
2493
|
+
const { efforts, defaultEffort, custom } = reasoningEffortOptions(model)
|
|
2494
|
+
group.classList.toggle('hidden', !cur || !efforts.length)
|
|
2379
2495
|
box.innerHTML = efforts.map(e => {
|
|
2380
|
-
const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id ===
|
|
2496
|
+
const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === defaultEffort)
|
|
2381
2497
|
return `<button class="menu-chip ${isCur ? 'current' : ''}" data-effort="${esc(e.id)}" title="${esc(e.description || '')}">${esc(e.name || e.id)}</button>`
|
|
2382
|
-
}).join('')
|
|
2498
|
+
}).join('') + (custom ? `<span class="effort-hint">${esc(t('models.effortCustomHint'))}</span>` : '')
|
|
2383
2499
|
box.querySelectorAll('[data-effort]').forEach(btn =>
|
|
2384
2500
|
btn.addEventListener('click', () => selectSessionEffort(btn.dataset.effort)))
|
|
2385
2501
|
}
|
|
2386
2502
|
|
|
2503
|
+
function reasoningEffortOptions(model) {
|
|
2504
|
+
const raw = Array.isArray(model?.reasoning?.efforts) && model.reasoning.efforts.length
|
|
2505
|
+
? model.reasoning.efforts
|
|
2506
|
+
: (Array.isArray(model?.reasoningEfforts) && model.reasoningEfforts.length ? model.reasoningEfforts : null)
|
|
2507
|
+
const names = {
|
|
2508
|
+
low: t('models.effortLow'), high: t('models.effortHigh'), max: t('models.effortMax'), off: t('models.effortOff')
|
|
2509
|
+
}
|
|
2510
|
+
if (raw) {
|
|
2511
|
+
return {
|
|
2512
|
+
efforts: raw.map(e => typeof e === 'string' ? { id: e, name: names[e] || e } : e),
|
|
2513
|
+
defaultEffort: model?.reasoning?.defaultEffort,
|
|
2514
|
+
custom: !model?.reasoning?.efforts
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
return {
|
|
2518
|
+
efforts: ['low', 'high', 'max'].map(id => ({ id, name: names[id] })),
|
|
2519
|
+
defaultEffort: undefined,
|
|
2520
|
+
custom: true
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2387
2524
|
async function selectSessionEffort(effortId) {
|
|
2388
2525
|
const cur = state.models.current
|
|
2389
2526
|
if (!state.current || !cur) return
|
|
@@ -2433,7 +2570,9 @@ function renderNewSessionWorkspace(preferredId = '') {
|
|
|
2433
2570
|
const current = workspaceById(preferredId || select.value)?.workspaceId || items[0]?.workspaceId || ''
|
|
2434
2571
|
select.innerHTML = workspaceOptionsHtml({ selected: current })
|
|
2435
2572
|
select.disabled = !items.length
|
|
2573
|
+
syncCustomSelect(select)
|
|
2436
2574
|
const workspace = workspaceById(select.value)
|
|
2575
|
+
$('new-session-workspace-name').textContent = workspace ? workspaceName(workspace) : ''
|
|
2437
2576
|
$('new-session-workspace-path').textContent = workspace?.path || ''
|
|
2438
2577
|
$('new-session-empty').classList.toggle('hidden', !!items.length)
|
|
2439
2578
|
$('new-session-create').disabled = !workspace
|
|
@@ -2594,8 +2733,9 @@ function renderOverview() {
|
|
|
2594
2733
|
btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.overviewQuestion)))
|
|
2595
2734
|
})
|
|
2596
2735
|
|
|
2597
|
-
const
|
|
2598
|
-
const
|
|
2736
|
+
const topSessions = topLevelSessions()
|
|
2737
|
+
const running = topSessions.filter(s => s.running).length
|
|
2738
|
+
const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))).slice(0, 4)
|
|
2599
2739
|
const primary = $('overview-primary-action')
|
|
2600
2740
|
if (primary) {
|
|
2601
2741
|
let action = 'new'
|
|
@@ -3364,6 +3504,54 @@ function cmpVersion(a, b) {
|
|
|
3364
3504
|
return 0
|
|
3365
3505
|
}
|
|
3366
3506
|
|
|
3507
|
+
function isComparableVersion(value) {
|
|
3508
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(String(value || '').trim())
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function isAppBehindGateway(appVersion, gatewayVersion) {
|
|
3512
|
+
return isComparableVersion(appVersion) && isComparableVersion(gatewayVersion) && cmpVersion(gatewayVersion, appVersion) > 0
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
function isAppVersionWarningOpen() {
|
|
3516
|
+
const modal = $('modal-app-version-warning')
|
|
3517
|
+
return !!modal && !modal.classList.contains('hidden')
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
async function maybeWarnAppBehindGateway({ probe = false } = {}) {
|
|
3521
|
+
if (!CAP?.isNativePlatform?.() || !state.localVersion) return false
|
|
3522
|
+
const base = String(state.server || '').replace(/\/+$/, '')
|
|
3523
|
+
if (!base) return false
|
|
3524
|
+
let health = state.gatewayHealth[base]
|
|
3525
|
+
if (!health && probe) {
|
|
3526
|
+
await pingServer(base)
|
|
3527
|
+
health = state.gatewayHealth[base]
|
|
3528
|
+
}
|
|
3529
|
+
const gatewayVersion = String(health?.version || '').trim()
|
|
3530
|
+
if (!isAppBehindGateway(state.localVersion, gatewayVersion)) return false
|
|
3531
|
+
const warningKey = `${state.localVersion}\u0000${gatewayVersion}`
|
|
3532
|
+
if (state.warnedGatewayVersions.has(warningKey)) return false
|
|
3533
|
+
const anotherModal = [...document.querySelectorAll('.modal')]
|
|
3534
|
+
.some(modal => modal.id !== 'modal-app-version-warning' && !modal.classList.contains('hidden'))
|
|
3535
|
+
if (anotherModal) return false
|
|
3536
|
+
state.warnedGatewayVersions.add(warningKey)
|
|
3537
|
+
$('app-version-current').textContent = 'v' + state.localVersion
|
|
3538
|
+
$('app-version-gateway').textContent = 'v' + gatewayVersion
|
|
3539
|
+
$('modal-app-version-warning').classList.remove('hidden')
|
|
3540
|
+
setTimeout(() => $('app-version-update')?.focus(), 50)
|
|
3541
|
+
return true
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
function closeAppVersionWarning(checkNow = false) {
|
|
3545
|
+
$('modal-app-version-warning').classList.add('hidden')
|
|
3546
|
+
if (checkNow) {
|
|
3547
|
+
showView('view-settings')
|
|
3548
|
+
showSettingsPage('about')
|
|
3549
|
+
void checkUpdate(false)
|
|
3550
|
+
} else {
|
|
3551
|
+
scheduleStartupNotices(150)
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3367
3555
|
function resetUpdateExpand() {
|
|
3368
3556
|
const desc = $('update-desc')
|
|
3369
3557
|
if (desc) desc.classList.remove('expanded')
|
|
@@ -3397,6 +3585,10 @@ async function loadLocalVersion() {
|
|
|
3397
3585
|
const ANNOUNCEMENTS_KEY = 'seenAnnouncementsV1'
|
|
3398
3586
|
const ANNOUNCEMENT_HISTORY_KEY = 'announcementHistoryV1'
|
|
3399
3587
|
const ANNOUNCEMENT_VOTES_KEY = 'announcementVotesV1'
|
|
3588
|
+
const ANNOUNCEMENTS_POLL_MS = 30 * 1000
|
|
3589
|
+
let announcementCheckPromise = null
|
|
3590
|
+
let announcementPollTimer = null
|
|
3591
|
+
let announcementPollingStarted = false
|
|
3400
3592
|
function readSeenAnnouncements() {
|
|
3401
3593
|
try {
|
|
3402
3594
|
const value = JSON.parse(LS.get(ANNOUNCEMENTS_KEY, '{}'))
|
|
@@ -3413,6 +3605,7 @@ function markAnnouncementSeen(id) {
|
|
|
3413
3605
|
for (const key of keys.slice(0, keys.length - 100)) delete seen[key]
|
|
3414
3606
|
}
|
|
3415
3607
|
LS.set(ANNOUNCEMENTS_KEY, JSON.stringify(seen))
|
|
3608
|
+
renderAnnouncementBoard()
|
|
3416
3609
|
}
|
|
3417
3610
|
function readAnnouncementVotes() {
|
|
3418
3611
|
try {
|
|
@@ -3432,6 +3625,7 @@ function storeAnnouncementVote(announcementId, pollId, optionId) {
|
|
|
3432
3625
|
for (const key of keys.slice(0, keys.length - 100)) delete votes[key]
|
|
3433
3626
|
}
|
|
3434
3627
|
LS.set(ANNOUNCEMENT_VOTES_KEY, JSON.stringify(votes))
|
|
3628
|
+
renderAnnouncementBoard()
|
|
3435
3629
|
}
|
|
3436
3630
|
function announcementVote(item) {
|
|
3437
3631
|
if (!item?.poll?.id) return null
|
|
@@ -3453,6 +3647,36 @@ function storeAnnouncementHistory(items) {
|
|
|
3453
3647
|
LS.set(ANNOUNCEMENT_HISTORY_KEY, JSON.stringify(list))
|
|
3454
3648
|
return list
|
|
3455
3649
|
}
|
|
3650
|
+
function announcementBoardItems() {
|
|
3651
|
+
const seen = readSeenAnnouncements()
|
|
3652
|
+
const merged = new Map(readAnnouncementHistory().map(item => [item.id, item]))
|
|
3653
|
+
for (const item of state.announcements) if (item?.id) merged.set(item.id, item)
|
|
3654
|
+
return [...merged.values()]
|
|
3655
|
+
.filter(item => !seen[item.id])
|
|
3656
|
+
.sort((a, b) => Number(b.publishedAt || 0) - Number(a.publishedAt || 0))
|
|
3657
|
+
.slice(0, 1)
|
|
3658
|
+
}
|
|
3659
|
+
function renderAnnouncementBoard() {
|
|
3660
|
+
const box = $('overview-announcement-list')
|
|
3661
|
+
if (!box) return
|
|
3662
|
+
const items = announcementBoardItems()
|
|
3663
|
+
if (!items.length) {
|
|
3664
|
+
box.innerHTML = `<div class="overview-empty">${esc(t('overview.communityEmpty'))}</div>`
|
|
3665
|
+
return
|
|
3666
|
+
}
|
|
3667
|
+
box.innerHTML = items.map(item => {
|
|
3668
|
+
const poll = !!item.poll
|
|
3669
|
+
const vote = announcementVote(item)
|
|
3670
|
+
const date = Number(item.publishedAt) > 0 ? fmtFullTime(item.publishedAt) : t('announcement.noDate')
|
|
3671
|
+
const action = poll ? (vote ? t('overview.communityVoted') : t('overview.communityVote')) : t('overview.communityView')
|
|
3672
|
+
return `<button class="overview-announcement-card ${poll ? 'poll' : 'notice'}" type="button" data-home-announcement="${esc(item.id)}">
|
|
3673
|
+
<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>
|
|
3674
|
+
<span class="overview-announcement-title">${esc(item.title)}</span>
|
|
3675
|
+
<span class="overview-announcement-copy">${esc(item.content)}</span>
|
|
3676
|
+
<span class="overview-announcement-footer"><span>${esc(action)}</span><span aria-hidden="true">›</span></span>
|
|
3677
|
+
</button>`
|
|
3678
|
+
}).join('')
|
|
3679
|
+
}
|
|
3456
3680
|
function renderAnnouncementHistory() {
|
|
3457
3681
|
const box = $('announcement-history-list')
|
|
3458
3682
|
if (!box) return
|
|
@@ -3613,9 +3837,9 @@ function closeAnnouncement(markSeen) {
|
|
|
3613
3837
|
state.announcement = null
|
|
3614
3838
|
$('modal-announcement').classList.add('hidden')
|
|
3615
3839
|
}
|
|
3616
|
-
async function
|
|
3840
|
+
async function fetchAnnouncements() {
|
|
3617
3841
|
const base = updateBase()
|
|
3618
|
-
if (!base || !state.localVersion) return false
|
|
3842
|
+
if (!base || !state.localVersion || isAppVersionWarningOpen()) return false
|
|
3619
3843
|
try {
|
|
3620
3844
|
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(8000) : undefined
|
|
3621
3845
|
const url = base + '/announcements.json?t=' + Date.now()
|
|
@@ -3626,16 +3850,46 @@ async function checkAnnouncements() {
|
|
|
3626
3850
|
const data = JSON.parse(raw)
|
|
3627
3851
|
const source = Array.isArray(data) ? data : (Array.isArray(data?.items) ? data.items : [data])
|
|
3628
3852
|
const normalized = source.map(item => normalizeAnnouncement(item, base)).filter(Boolean)
|
|
3853
|
+
state.announcements = normalized.sort((a, b) => Number(b.publishedAt || 0) - Number(a.publishedAt || 0))
|
|
3629
3854
|
storeAnnouncementHistory(normalized)
|
|
3855
|
+
renderAnnouncementBoard()
|
|
3630
3856
|
const seen = readSeenAnnouncements()
|
|
3631
3857
|
const items = normalized.filter(item => !seen[item.id])
|
|
3632
3858
|
.sort((a, b) => b.publishedAt - a.publishedAt)
|
|
3633
|
-
if (!items.length) return false
|
|
3859
|
+
if (!items.length || state.announcement) return false
|
|
3634
3860
|
openAnnouncementModal(items[0])
|
|
3635
3861
|
return true
|
|
3636
3862
|
} catch { return false }
|
|
3637
3863
|
}
|
|
3638
3864
|
|
|
3865
|
+
function checkAnnouncements() {
|
|
3866
|
+
if (announcementCheckPromise) return announcementCheckPromise
|
|
3867
|
+
announcementCheckPromise = fetchAnnouncements().finally(() => { announcementCheckPromise = null })
|
|
3868
|
+
return announcementCheckPromise
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
function startAnnouncementPolling() {
|
|
3872
|
+
if (announcementPollingStarted) return
|
|
3873
|
+
announcementPollingStarted = true
|
|
3874
|
+
announcementPollTimer = setInterval(() => {
|
|
3875
|
+
if (!document.hidden) void checkAnnouncements()
|
|
3876
|
+
}, ANNOUNCEMENTS_POLL_MS)
|
|
3877
|
+
document.addEventListener('visibilitychange', () => {
|
|
3878
|
+
if (!document.hidden) void checkAnnouncements()
|
|
3879
|
+
})
|
|
3880
|
+
window.addEventListener('online', () => { void checkAnnouncements() })
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
let startupNoticeTimer = null
|
|
3884
|
+
function scheduleStartupNotices(delay = 4000) {
|
|
3885
|
+
clearTimeout(startupNoticeTimer)
|
|
3886
|
+
startupNoticeTimer = setTimeout(async () => {
|
|
3887
|
+
if (isAppVersionWarningOpen()) return
|
|
3888
|
+
const shown = await checkAnnouncements()
|
|
3889
|
+
if (!shown && state.token && !isAppVersionWarningOpen()) checkUpdate(true)
|
|
3890
|
+
}, delay)
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3639
3893
|
/* ---------------- 更新内容弹窗 ---------------- */
|
|
3640
3894
|
const NOTES_KEY = 'seenNotesVersion'
|
|
3641
3895
|
let notesVersion = ''
|
|
@@ -4321,36 +4575,159 @@ function initToken() {
|
|
|
4321
4575
|
$('server-desc').textContent = state.server || t('servers.defaultDesc')
|
|
4322
4576
|
}
|
|
4323
4577
|
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4578
|
+
let dshControlPollPromise = null
|
|
4579
|
+
|
|
4580
|
+
function dshControlButtonsBusy(busy, supported = true) {
|
|
4327
4581
|
const buttons = [$('btn-dsh-start'), $('btn-dsh-restart')].filter(Boolean)
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4582
|
+
buttons.forEach(button => {
|
|
4583
|
+
button.disabled = busy || !supported
|
|
4584
|
+
button.classList.toggle('hidden', !supported)
|
|
4585
|
+
})
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
function dshControlFailureText(value) {
|
|
4589
|
+
const key = {
|
|
4590
|
+
SERVICE_NOT_FOUND: 'settings.dshErrorServiceNotFound',
|
|
4591
|
+
INVALID_SERVICE: 'settings.dshErrorInvalidService',
|
|
4592
|
+
SYSTEMCTL_NOT_FOUND: 'settings.dshErrorSystemctlNotFound',
|
|
4593
|
+
SYSTEMD_UNAVAILABLE: 'settings.dshErrorSystemdUnavailable',
|
|
4594
|
+
PERMISSION_DENIED: 'settings.dshErrorPermissionDenied',
|
|
4595
|
+
COMMAND_TIMEOUT: 'settings.dshErrorCommandTimeout',
|
|
4596
|
+
COMMAND_FAILED: 'settings.dshErrorCommandFailed',
|
|
4597
|
+
SERVICE_FAILED: 'settings.dshErrorServiceFailed',
|
|
4598
|
+
SERVICE_TIMEOUT: 'settings.dshErrorServiceTimeout',
|
|
4599
|
+
UPSTREAM_TIMEOUT: 'settings.dshErrorUpstreamTimeout',
|
|
4600
|
+
EVENTS_TIMEOUT: 'settings.dshErrorEventsTimeout',
|
|
4601
|
+
OPERATION_NOT_FOUND: 'settings.dshErrorOperationNotFound',
|
|
4602
|
+
OPERATION_IN_PROGRESS: 'settings.dshErrorOperationProgress',
|
|
4603
|
+
}[value?.code]
|
|
4604
|
+
return key
|
|
4605
|
+
? t(key, { service: value?.service || value?.status?.service || 'dsh-web' })
|
|
4606
|
+
: t('settings.dshErrorGeneric', { msg: value?.error || value?.message || value?.code || t('conn.off') })
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
function dshControlStageText(operation, step = operation) {
|
|
4610
|
+
const seconds = Math.max(0, Math.round(Number(step?.elapsedMs ?? operation?.elapsedMs ?? 0) / 1000))
|
|
4611
|
+
const action = operation?.action === 'start' ? t('settings.dshStart') : t('settings.dshRestart')
|
|
4612
|
+
const service = operation?.service || operation?.status?.service || 'dsh-web'
|
|
4613
|
+
if (step?.stage === 'queued') return t('settings.dshStageQueued')
|
|
4614
|
+
if (step?.stage === 'checking') return t('settings.dshStageChecking', { service })
|
|
4615
|
+
if (step?.stage === 'command') return t('settings.dshStageCommand', { action })
|
|
4616
|
+
if (step?.stage === 'waiting-service') return t('settings.dshStageWaitingService', { seconds })
|
|
4617
|
+
if (step?.stage === 'waiting-upstream') return t('settings.dshStageWaitingUpstream', { seconds })
|
|
4618
|
+
if (step?.stage === 'waiting-events') return t('settings.dshStageWaitingEvents', { seconds })
|
|
4619
|
+
if (step?.stage === 'complete') {
|
|
4620
|
+
if (operation?.code === 'ALREADY_RUNNING') return t('settings.dshAlreadyRunning', { pid: operation?.status?.mainPid || '—' })
|
|
4621
|
+
return t('settings.dshSuccessDetail', {
|
|
4622
|
+
action,
|
|
4623
|
+
pid: operation?.status?.mainPid || '—',
|
|
4624
|
+
status: operation?.upstream?.status || '—',
|
|
4625
|
+
seconds: Math.max(0, Math.round(Number(operation?.elapsedMs || step?.elapsedMs || 0) / 1000)),
|
|
4626
|
+
})
|
|
4627
|
+
}
|
|
4628
|
+
if (step?.stage === 'failed') return dshControlFailureText(operation)
|
|
4629
|
+
return step?.message || operation?.message || '—'
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
function renderDshControlOperation(operation) {
|
|
4633
|
+
const desc = $('dsh-control-desc')
|
|
4634
|
+
const box = $('dsh-control-steps')
|
|
4635
|
+
if (!desc || !box || !operation) return
|
|
4636
|
+
const failed = operation.stage === 'failed' || operation.done && operation.ok === false
|
|
4637
|
+
desc.textContent = failed ? t('settings.dshFailed', { msg: dshControlFailureText(operation) }) : dshControlStageText(operation)
|
|
4638
|
+
const steps = Array.isArray(operation.steps) && operation.steps.length
|
|
4639
|
+
? operation.steps
|
|
4640
|
+
: [{ stage: operation.stage || 'queued', elapsedMs: operation.elapsedMs || 0, message: operation.message || '' }]
|
|
4641
|
+
box.classList.remove('hidden')
|
|
4642
|
+
box.innerHTML = steps.map((step, index) => {
|
|
4643
|
+
const current = !operation.done && index === steps.length - 1
|
|
4644
|
+
const isFailed = step.stage === 'failed'
|
|
4645
|
+
const success = step.stage === 'complete'
|
|
4646
|
+
const mark = isFailed ? '×' : success ? '✓' : current ? '…' : '✓'
|
|
4647
|
+
const cls = isFailed ? 'failed' : success ? 'success' : current ? 'current' : ''
|
|
4648
|
+
const detail = isFailed && operation.detail
|
|
4649
|
+
? `<span class="dsh-control-step-detail">${esc(t('settings.dshErrorDetail', { detail: operation.detail }))}</span>`
|
|
4650
|
+
: ''
|
|
4651
|
+
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>`
|
|
4652
|
+
}).join('')
|
|
4653
|
+
dshControlButtonsBusy(!operation.done, true)
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
async function readDshControlResponse(res) {
|
|
4657
|
+
const text = await res.text()
|
|
4658
|
+
if (!text) return {}
|
|
4659
|
+
try { return JSON.parse(text) } catch { return { error: `HTTP ${res.status}`, detail: text.slice(0, 500) } }
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
async function pollDshControlOperation(operationId, initial) {
|
|
4663
|
+
let value = initial
|
|
4664
|
+
while (true) {
|
|
4665
|
+
renderDshControlOperation(value)
|
|
4666
|
+
if (value.done) return value
|
|
4667
|
+
await new Promise(resolvePromise => setTimeout(resolvePromise, 700))
|
|
4668
|
+
const res = await fetch(adminApiUrl(`/admin/api/dsh?operation=${encodeURIComponent(operationId)}`), {
|
|
4669
|
+
headers: { authorization: 'Bearer ' + state.token }, cache: 'no-store'
|
|
4670
|
+
})
|
|
4671
|
+
if (res.status === 401) { authFailure(); throw Object.assign(new Error('unauthorized'), { auth: true }) }
|
|
4672
|
+
const next = await readDshControlResponse(res)
|
|
4673
|
+
if (!res.ok) throw Object.assign(new Error(next.error || next.message || `HTTP ${res.status}`), { dshPayload: { ...next, httpStatus: res.status } })
|
|
4674
|
+
value = next
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
|
|
4678
|
+
function renderDshControlStatus(value) {
|
|
4679
|
+
const desc = $('dsh-control-desc')
|
|
4680
|
+
const box = $('dsh-control-steps')
|
|
4681
|
+
if (!desc || !value) return
|
|
4682
|
+
if (value.supported === false) {
|
|
4683
|
+
desc.textContent = value.code ? dshControlFailureText(value) : value.message || t('settings.dshUnsupported')
|
|
4684
|
+
dshControlButtonsBusy(false, false)
|
|
4685
|
+
return
|
|
4686
|
+
}
|
|
4687
|
+
if (value.ok === false) {
|
|
4688
|
+
desc.textContent = t('settings.dshStatusFailed', { msg: dshControlFailureText(value) })
|
|
4689
|
+
dshControlButtonsBusy(false, false)
|
|
4331
4690
|
return
|
|
4332
4691
|
}
|
|
4333
|
-
|
|
4334
|
-
|
|
4692
|
+
dshControlButtonsBusy(false, true)
|
|
4693
|
+
if (box && !dshControlPollPromise) box.classList.add('hidden')
|
|
4694
|
+
const service = value.service || 'dsh-web'
|
|
4695
|
+
const serviceState = `${value.activeState || value.state || 'unknown'}/${value.subState || 'unknown'}`
|
|
4696
|
+
desc.textContent = value.running
|
|
4697
|
+
? t('settings.dshRunningDetail', { service, pid: value.mainPid || '—', state: serviceState })
|
|
4698
|
+
: t('settings.dshStoppedDetail', { service, state: serviceState })
|
|
4335
4699
|
}
|
|
4336
4700
|
|
|
4337
4701
|
async function loadDshControl() {
|
|
4338
4702
|
if (!state.token || !$('dsh-control-desc')) return
|
|
4703
|
+
if (activeGatewayCapability('dshLifecycle') === false) {
|
|
4704
|
+
renderDshControlStatus({ supported: false, message: t('settings.dshUnsupported') })
|
|
4705
|
+
return
|
|
4706
|
+
}
|
|
4339
4707
|
try {
|
|
4340
|
-
const res = await fetch(adminApiUrl('/admin/api/dsh'), { headers: { authorization: 'Bearer ' + state.token } })
|
|
4708
|
+
const res = await fetch(adminApiUrl('/admin/api/dsh'), { headers: { authorization: 'Bearer ' + state.token }, cache: 'no-store' })
|
|
4341
4709
|
if (res.status === 401) return authFailure()
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4710
|
+
const value = await readDshControlResponse(res)
|
|
4711
|
+
if (!res.ok) throw Object.assign(new Error(value.error || value.message || `HTTP ${res.status}`), { dshPayload: value })
|
|
4712
|
+
renderDshControlStatus(value)
|
|
4713
|
+
if (value.operation?.operationId && !value.operation.done && !dshControlPollPromise) {
|
|
4714
|
+
dshControlPollPromise = pollDshControlOperation(value.operation.operationId, value.operation)
|
|
4715
|
+
.then(renderDshControlOperation)
|
|
4716
|
+
.catch(error => {
|
|
4717
|
+
if (!error?.auth) $('dsh-control-desc').textContent = t('settings.dshFailed', { msg: error?.dshPayload ? dshControlFailureText(error.dshPayload) : t('settings.dshResultUnknown') })
|
|
4718
|
+
})
|
|
4719
|
+
.finally(() => { dshControlPollPromise = null; dshControlButtonsBusy(false, true) })
|
|
4720
|
+
}
|
|
4721
|
+
} catch (error) {
|
|
4722
|
+
$('dsh-control-desc').textContent = t('settings.dshStatusFailed', { msg: error?.dshPayload ? dshControlFailureText(error.dshPayload) : t('conn.off') })
|
|
4345
4723
|
}
|
|
4346
4724
|
}
|
|
4347
4725
|
|
|
4348
4726
|
async function controlDsh(action) {
|
|
4727
|
+
if (dshControlPollPromise) return
|
|
4349
4728
|
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 })
|
|
4729
|
+
dshControlButtonsBusy(true, true)
|
|
4730
|
+
renderDshControlOperation({ action, service: 'dsh-web', accepted: true, done: false, stage: 'queued', elapsedMs: 0, steps: [] })
|
|
4354
4731
|
try {
|
|
4355
4732
|
const res = await fetch(adminApiUrl('/admin/api/dsh'), {
|
|
4356
4733
|
method: 'POST',
|
|
@@ -4358,15 +4735,33 @@ async function controlDsh(action) {
|
|
|
4358
4735
|
body: JSON.stringify({ action }),
|
|
4359
4736
|
})
|
|
4360
4737
|
if (res.status === 401) return authFailure()
|
|
4361
|
-
|
|
4362
|
-
if (
|
|
4738
|
+
let v = await readDshControlResponse(res)
|
|
4739
|
+
if (res.status === 409 && v.operation?.operationId) v = v.operation
|
|
4740
|
+
else if (!res.ok) throw Object.assign(new Error(v.error || v.message || `HTTP ${res.status}`), { dshPayload: { ...v, httpStatus: res.status } })
|
|
4741
|
+
|
|
4742
|
+
if (v.accepted && v.operationId) {
|
|
4743
|
+
dshControlPollPromise = pollDshControlOperation(v.operationId, v)
|
|
4744
|
+
v = await dshControlPollPromise
|
|
4745
|
+
if (!v.ok) throw Object.assign(new Error(dshControlFailureText(v)), { dshPayload: v })
|
|
4746
|
+
renderDshControlOperation(v)
|
|
4747
|
+
toast(dshControlStageText(v), 'ok')
|
|
4748
|
+
return
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
// 兼容旧网关:它会在单个 POST 中直接返回最终状态。
|
|
4752
|
+
if (v.ok === false) throw Object.assign(new Error(v.error || v.message || `HTTP ${res.status}`), { dshPayload: v })
|
|
4363
4753
|
renderDshControlStatus(v)
|
|
4364
4754
|
toast(t('settings.dshStarted', { action: label }), 'ok')
|
|
4365
|
-
} catch (
|
|
4366
|
-
if (
|
|
4367
|
-
|
|
4755
|
+
} catch (error) {
|
|
4756
|
+
if (error?.auth) return
|
|
4757
|
+
const payload = error?.dshPayload
|
|
4758
|
+
const message = payload ? dshControlFailureText(payload) : t('settings.dshResultUnknown')
|
|
4759
|
+
if (payload) renderDshControlOperation({ action, service: payload.service || 'dsh-web', done: true, ok: false, stage: 'failed', steps: payload.steps || [], ...payload })
|
|
4760
|
+
else $('dsh-control-desc').textContent = t('settings.dshFailed', { msg: message })
|
|
4761
|
+
toast(t('settings.dshFailed', { msg: message }), 'err')
|
|
4368
4762
|
} finally {
|
|
4369
|
-
|
|
4763
|
+
dshControlPollPromise = null
|
|
4764
|
+
dshControlButtonsBusy(false, true)
|
|
4370
4765
|
}
|
|
4371
4766
|
}
|
|
4372
4767
|
|
|
@@ -4427,6 +4822,7 @@ function bindUi() {
|
|
|
4427
4822
|
renderServers()
|
|
4428
4823
|
renderSessions()
|
|
4429
4824
|
renderWorkbench()
|
|
4825
|
+
renderAnnouncementBoard()
|
|
4430
4826
|
renderPending(); renderQueue(); renderJobs()
|
|
4431
4827
|
updateConn()
|
|
4432
4828
|
if (state.current) { renderSessionTitle(); renderSessionSub(); renderSessionCards(); renderHistory(true) }
|
|
@@ -4526,6 +4922,11 @@ function bindUi() {
|
|
|
4526
4922
|
$('btn-stats').addEventListener('click', () => { renderSessionCards(); $('modal-stats').classList.remove('hidden') })
|
|
4527
4923
|
$('stats-close').addEventListener('click', () => $('modal-stats').classList.add('hidden'))
|
|
4528
4924
|
$('btn-refresh').addEventListener('click', () => { toast(t('common.refreshing')); openStreams(); refreshAll() })
|
|
4925
|
+
$('overview-refresh').addEventListener('click', () => {
|
|
4926
|
+
toast(t('common.refreshing'))
|
|
4927
|
+
openStreams()
|
|
4928
|
+
void Promise.all([refreshAll(), checkAnnouncements()])
|
|
4929
|
+
})
|
|
4529
4930
|
// 反馈
|
|
4530
4931
|
$('btn-feedback').addEventListener('click', (e) => { e.stopPropagation(); toggleFeedbackSheet() })
|
|
4531
4932
|
$('feedback-backdrop').addEventListener('click', closeFeedbackSheet)
|
|
@@ -4540,6 +4941,10 @@ function bindUi() {
|
|
|
4540
4941
|
$('btn-write-feedback').addEventListener('click', () => { closeFeedbackSheet(); openFeedbackModal() })
|
|
4541
4942
|
$('fb-cancel').addEventListener('click', closeFeedbackModal)
|
|
4542
4943
|
$('fb-submit').addEventListener('click', submitFeedback)
|
|
4944
|
+
$('feedback-success-confirm').addEventListener('click', closeFeedbackSuccess)
|
|
4945
|
+
$('modal-feedback-success').addEventListener('click', (e) => {
|
|
4946
|
+
if (e.target === $('modal-feedback-success')) closeFeedbackSuccess()
|
|
4947
|
+
})
|
|
4543
4948
|
document.querySelectorAll('#fb-chips .fb-chip').forEach(btn =>
|
|
4544
4949
|
btn.addEventListener('click', () => {
|
|
4545
4950
|
state.feedbackType = btn.dataset.fbType
|
|
@@ -4550,6 +4955,8 @@ function bindUi() {
|
|
|
4550
4955
|
})
|
|
4551
4956
|
document.addEventListener('keydown', (e) => {
|
|
4552
4957
|
if (e.key === 'Escape' && !$('feedback-sheet').classList.contains('hidden')) { closeFeedbackSheet(); $('btn-feedback').focus() }
|
|
4958
|
+
else if (e.key === 'Escape' && !$('modal-feedback-success').classList.contains('hidden')) closeFeedbackSuccess()
|
|
4959
|
+
else if (e.key === 'Escape' && isAppVersionWarningOpen()) closeAppVersionWarning(false)
|
|
4553
4960
|
})
|
|
4554
4961
|
$('btn-new-session').addEventListener('click', newSession)
|
|
4555
4962
|
$('session-workspace-filter').addEventListener('change', (e) => {
|
|
@@ -4699,6 +5106,13 @@ function bindUi() {
|
|
|
4699
5106
|
$('modal-announcement-history').addEventListener('click', (e) => {
|
|
4700
5107
|
if (e.target === $('modal-announcement-history')) closeAnnouncementHistory()
|
|
4701
5108
|
})
|
|
5109
|
+
$('overview-announcement-history').addEventListener('click', openAnnouncementHistory)
|
|
5110
|
+
$('overview-announcement-list').addEventListener('click', (e) => {
|
|
5111
|
+
const button = e.target.closest('[data-home-announcement]')
|
|
5112
|
+
if (!button) return
|
|
5113
|
+
const item = announcementBoardItems().find(entry => entry.id === button.dataset.homeAnnouncement)
|
|
5114
|
+
if (item) openAnnouncementModal(item)
|
|
5115
|
+
})
|
|
4702
5116
|
// 设置
|
|
4703
5117
|
$('view-settings').addEventListener('click', (e) => {
|
|
4704
5118
|
const group = e.target.closest('[data-settings-group]')
|
|
@@ -4733,6 +5147,11 @@ function bindUi() {
|
|
|
4733
5147
|
$('btn-check-update').addEventListener('click', () => checkUpdate(false))
|
|
4734
5148
|
$('btn-download-update').addEventListener('click', downloadUpdate)
|
|
4735
5149
|
$('btn-update-expand').addEventListener('click', toggleUpdateExpand)
|
|
5150
|
+
$('app-version-later').addEventListener('click', () => closeAppVersionWarning(false))
|
|
5151
|
+
$('app-version-update').addEventListener('click', () => closeAppVersionWarning(true))
|
|
5152
|
+
$('modal-app-version-warning').addEventListener('click', (e) => {
|
|
5153
|
+
if (e.target === $('modal-app-version-warning')) closeAppVersionWarning(false)
|
|
5154
|
+
})
|
|
4736
5155
|
$('btn-reset').addEventListener('click', () => {
|
|
4737
5156
|
if (!confirm(t('settings.confirmReset'))) return
|
|
4738
5157
|
LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction'); LS.del(ANNOUNCEMENTS_KEY); LS.del(ANNOUNCEMENT_HISTORY_KEY); LS.del(ANNOUNCEMENT_VOTES_KEY)
|
|
@@ -4873,6 +5292,7 @@ function applyNativeInsets() {
|
|
|
4873
5292
|
async function boot() {
|
|
4874
5293
|
initToken()
|
|
4875
5294
|
bindUi()
|
|
5295
|
+
renderAnnouncementBoard()
|
|
4876
5296
|
renderLangBtn()
|
|
4877
5297
|
bindNativeBack()
|
|
4878
5298
|
bindNativeLinks()
|
|
@@ -4886,17 +5306,17 @@ async function boot() {
|
|
|
4886
5306
|
} else {
|
|
4887
5307
|
// 多服务器: 启动时静默测速一次, 选最快的连接(同源页面也参与比较)
|
|
4888
5308
|
await selectFastestServer({ silent: true, reconnect: false })
|
|
5309
|
+
await maybeWarnAppBehindGateway({ probe: true })
|
|
4889
5310
|
openStreams()
|
|
4890
5311
|
await refreshAll()
|
|
4891
5312
|
const host = await safeRpc('host.describe', {}, '')
|
|
4892
5313
|
if (host) { state.hostInfo = host; $('host-desc').textContent = t('settings.hostDesc', { version: host.version, cwd: host.cwd, n: host.attachedSessions }) }
|
|
4893
5314
|
loadDshControl()
|
|
4894
5315
|
}
|
|
4895
|
-
//
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
}, 4000)
|
|
5316
|
+
// 网关从中央 HTTPS 公告源读取并在不可达时回退内置文件。前台每 30 秒检查,
|
|
5317
|
+
// 回到前台或网络恢复时立即补查;公告优先,避免启动时两个弹窗重叠。
|
|
5318
|
+
startAnnouncementPolling()
|
|
5319
|
+
scheduleStartupNotices()
|
|
4900
5320
|
renderPending()
|
|
4901
5321
|
}
|
|
4902
5322
|
|