dsh-remote-plugin 0.7.2 → 0.7.3
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/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +313 -18
- package/index.mjs +31 -9
- package/package.json +1 -1
- package/public/app.js +310 -303
- package/public/desktop/desktop.css +101 -0
- package/public/desktop/desktop.html +59 -21
- package/public/desktop/desktop.js +260 -101
- package/public/history.js +160 -0
- package/public/index.html +61 -22
- package/public/link-check.js +109 -0
- package/public/plugin-center.css +11 -0
- package/public/plugin-center.js +92 -39
- package/public/styles.css +107 -2
- package/public/update.json +8 -8
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -27,7 +27,7 @@ const CLIENT_ID = (() => {
|
|
|
27
27
|
})()
|
|
28
28
|
function clientIdHeaders() { return CLIENT_ID ? { 'x-dsh-remote-client-id': CLIENT_ID } : {} }
|
|
29
29
|
|
|
30
|
-
/*
|
|
30
|
+
/* 按连接身份隔离:本地缓存先显示,远端历史在后台校验与合并。 */
|
|
31
31
|
const CACHE = {
|
|
32
32
|
get sessions() { return connectionCacheKey('sessions') },
|
|
33
33
|
get history() { return connectionCacheKey('history') }
|
|
@@ -43,18 +43,6 @@ function cacheRead(key, d = null) {
|
|
|
43
43
|
function cacheWrite(key, value) {
|
|
44
44
|
try { LS.set(key, JSON.stringify(value)) } catch { LS.del(key) }
|
|
45
45
|
}
|
|
46
|
-
function readHistoryCache() { return cacheRead(CACHE.history, {}) || {} }
|
|
47
|
-
function writeHistoryCache(cache) {
|
|
48
|
-
try { LS.set(CACHE.history, JSON.stringify(cache)); return }
|
|
49
|
-
catch {
|
|
50
|
-
// localStorage 配额不足: 每会话只留最近 50 条再试一次
|
|
51
|
-
try {
|
|
52
|
-
for (const k of Object.keys(cache)) cache[k].events = (cache[k].events || []).slice(-50)
|
|
53
|
-
LS.set(CACHE.history, JSON.stringify(cache))
|
|
54
|
-
} catch { LS.del(CACHE.history) }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
46
|
const state = {
|
|
59
47
|
token: '',
|
|
60
48
|
wsTicket: { token: '', server: '', value: '', expiresAt: 0 },
|
|
@@ -128,11 +116,20 @@ for (const key of ['server', 'token']) {
|
|
|
128
116
|
if (next === value) return
|
|
129
117
|
value = next; connectionGeneration++
|
|
130
118
|
state.sessions = []; state.byId.clear(); state.pendingProjections.clear()
|
|
119
|
+
state.hostInfo = null
|
|
120
|
+
if(typeof closeStream==='function'&&typeof streamMeta!=='undefined'){closeStream('mux');closeStream('host')}
|
|
131
121
|
state.current = null; state.history = emptyHistory()
|
|
132
122
|
state.fs.path = null; state.fs.initial = null; state.fs.loaded = false; state.fs.roots = []; state.fs.rootIndex = 0
|
|
133
123
|
state.wb = null; state.wbProjects = []; state.wbArchived = []; state.workspaceFilter = ''; state.fs.workspaceId = ''; clearTimeout(scheduleHistoryCacheSave._t)
|
|
134
124
|
window.DshPluginCenter?.close()
|
|
135
125
|
window.DshInsights?.close()
|
|
126
|
+
if(typeof document!=='undefined') {
|
|
127
|
+
const identityGeneration=connectionGeneration
|
|
128
|
+
queueMicrotask(()=>{if(identityGeneration===connectionGeneration&&typeof startOverviewDetection==='function')startOverviewDetection(true,true)})
|
|
129
|
+
document.getElementById('history')?.replaceChildren?.()
|
|
130
|
+
const generation=connectionGeneration
|
|
131
|
+
queueMicrotask(()=>{if(generation!==connectionGeneration)return;if(typeof conversationView!=='undefined')conversationView?.reset();restoreCachedSessionList()})
|
|
132
|
+
}
|
|
136
133
|
if (typeof document !== 'undefined') {
|
|
137
134
|
document.getElementById('modal-workspace')?.classList.add('hidden')
|
|
138
135
|
const generation = connectionGeneration
|
|
@@ -331,6 +328,9 @@ function openFeedbackModal() {
|
|
|
331
328
|
$('fb-msg').value = ''
|
|
332
329
|
$('fb-contact').value = ''
|
|
333
330
|
$('fb-include-diagnostics').checked = false
|
|
331
|
+
$('fb-include-crashes').checked = false
|
|
332
|
+
$('fb-crash-details').open = false
|
|
333
|
+
void loadCrashPreview()
|
|
334
334
|
$('modal-feedback').classList.remove('hidden')
|
|
335
335
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
336
336
|
}
|
|
@@ -343,11 +343,22 @@ function closeFeedbackSuccess() {
|
|
|
343
343
|
$('modal-feedback-success').classList.add('hidden')
|
|
344
344
|
$('btn-feedback')?.focus()
|
|
345
345
|
}
|
|
346
|
+
async function loadCrashPreview() {
|
|
347
|
+
const connection=captureConnection(),preview=$('fb-crash-preview')
|
|
348
|
+
preview.textContent=t('feedback.crashLoading')
|
|
349
|
+
try {
|
|
350
|
+
const response=await fetch(updateBase()+'/crash-logs',{headers:{authorization:'Bearer '+connection.token},signal:AbortSignal.timeout(5000),cache:'no-store'})
|
|
351
|
+
if(!response.ok)throw new Error('unavailable')
|
|
352
|
+
const data=await response.json()
|
|
353
|
+
if(connection.valid())preview.textContent=JSON.stringify(data.upload,null,2)
|
|
354
|
+
}catch{if(connection.valid())preview.textContent=t('feedback.crashUnavailable')}
|
|
355
|
+
}
|
|
346
356
|
async function submitFeedback() {
|
|
347
357
|
const type = state.feedbackType || 'bug'
|
|
348
358
|
const message = $('fb-msg').value.trim()
|
|
349
359
|
const contact = $('fb-contact').value.trim()
|
|
350
360
|
const includeDiagnostics = $('fb-include-diagnostics').checked
|
|
361
|
+
const includeCrashLogs = $('fb-include-crashes').checked
|
|
351
362
|
if (!message) { toast(t('feedback.empty'), 'err'); return }
|
|
352
363
|
if (message.length > 2000) { toast(t('feedback.tooLong'), 'err'); return }
|
|
353
364
|
const btn = $('fb-submit')
|
|
@@ -358,7 +369,7 @@ async function submitFeedback() {
|
|
|
358
369
|
const res = await fetch(base + '/feedback', {
|
|
359
370
|
method: 'POST',
|
|
360
371
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
|
|
361
|
-
body: JSON.stringify({ type, message, contact, appVersion: state.localVersion, includeDiagnostics })
|
|
372
|
+
body: JSON.stringify({ type, message, contact, appVersion: state.localVersion, includeDiagnostics, includeCrashLogs })
|
|
362
373
|
})
|
|
363
374
|
let json = {}
|
|
364
375
|
try { json = await res.json() } catch {}
|
|
@@ -524,6 +535,7 @@ async function safeRpc(method, payload, errText) {
|
|
|
524
535
|
}
|
|
525
536
|
|
|
526
537
|
let hostDescribePromise = null
|
|
538
|
+
let hostDescribeConnection = null
|
|
527
539
|
let hostDescribeRetryTimer = null
|
|
528
540
|
let hostDescribeFailures = 0
|
|
529
541
|
|
|
@@ -539,12 +551,13 @@ function scheduleHostDescribeRetry() {
|
|
|
539
551
|
|
|
540
552
|
async function refreshHostDescription({ notify = false } = {}) {
|
|
541
553
|
if (!state.token) return null
|
|
542
|
-
if (hostDescribePromise) return hostDescribePromise
|
|
543
|
-
const
|
|
544
|
-
|
|
554
|
+
if (hostDescribePromise && hostDescribeConnection?.valid()) return hostDescribePromise
|
|
555
|
+
const connection = captureConnection()
|
|
556
|
+
hostDescribeConnection = connection
|
|
557
|
+
const job = (async () => {
|
|
545
558
|
try {
|
|
546
559
|
const host = await rpc('host.describe', {}, 5000)
|
|
547
|
-
if (
|
|
560
|
+
if (!connection.valid()) return null
|
|
548
561
|
state.hostInfo = host
|
|
549
562
|
const health = activeGatewayHealth()
|
|
550
563
|
if (health) health.upstreamReachable = true
|
|
@@ -556,7 +569,7 @@ async function refreshHostDescription({ notify = false } = {}) {
|
|
|
556
569
|
renderOverview()
|
|
557
570
|
return host
|
|
558
571
|
} catch (error) {
|
|
559
|
-
if (
|
|
572
|
+
if (!connection.valid()) return null
|
|
560
573
|
if (error.message === 'AUTH') authFailure()
|
|
561
574
|
else {
|
|
562
575
|
hostDescribeFailures++
|
|
@@ -565,11 +578,11 @@ async function refreshHostDescription({ notify = false } = {}) {
|
|
|
565
578
|
}
|
|
566
579
|
return null
|
|
567
580
|
} finally {
|
|
568
|
-
hostDescribePromise = null
|
|
569
|
-
if (server !== state.server) scheduleHostDescribeRetry()
|
|
581
|
+
if (hostDescribePromise === job) { hostDescribePromise = null; hostDescribeConnection = null }
|
|
570
582
|
}
|
|
571
583
|
})()
|
|
572
|
-
|
|
584
|
+
hostDescribePromise = job
|
|
585
|
+
return job
|
|
573
586
|
}
|
|
574
587
|
|
|
575
588
|
function authFailure() {
|
|
@@ -1210,12 +1223,12 @@ function openStreams() {
|
|
|
1210
1223
|
function openStream(kind, handler, refreshOnOpen, isRestore, ticket = null) {
|
|
1211
1224
|
if (!state.token) return
|
|
1212
1225
|
if (ticket === null) {
|
|
1213
|
-
const token = state.token
|
|
1226
|
+
const token = state.token, connection=captureConnection()
|
|
1214
1227
|
void getWsTicket().then((value) => {
|
|
1215
|
-
if (
|
|
1228
|
+
if (connection.valid()) openStream(kind, handler, refreshOnOpen, isRestore, value)
|
|
1216
1229
|
}).catch(() => {
|
|
1217
1230
|
// 兼容旧网关/插件副本: ticket 接口不可用时临时回退旧 token 握手。
|
|
1218
|
-
if (
|
|
1231
|
+
if (connection.valid()) openStream(kind, handler, refreshOnOpen, isRestore, '')
|
|
1219
1232
|
})
|
|
1220
1233
|
return
|
|
1221
1234
|
}
|
|
@@ -1420,13 +1433,13 @@ document.addEventListener('visibilitychange', () => {
|
|
|
1420
1433
|
if (document.visibilityState === 'visible') {
|
|
1421
1434
|
onResume()
|
|
1422
1435
|
if (state.servers.length) selectFastestServer({ silent: true })
|
|
1423
|
-
else if (state.token
|
|
1436
|
+
else if (state.token) repairMissingStreams()
|
|
1424
1437
|
}
|
|
1425
1438
|
})
|
|
1426
1439
|
window.addEventListener('pageshow', onResume)
|
|
1427
1440
|
setInterval(() => {
|
|
1428
1441
|
if (document.visibilityState === 'visible' && state.token) {
|
|
1429
|
-
|
|
1442
|
+
repairMissingStreams()
|
|
1430
1443
|
}
|
|
1431
1444
|
}, 15000)
|
|
1432
1445
|
// 多服务器: 每 5 分钟重测一次延迟, 网络环境变化(离开 Wi-Fi / 挂上 Tailscale)时自动换线
|
|
@@ -1628,6 +1641,7 @@ async function refreshAll() {
|
|
|
1628
1641
|
}
|
|
1629
1642
|
|
|
1630
1643
|
async function refreshSessions() {
|
|
1644
|
+
if(typeof restoreCachedSessionList==='function')restoreCachedSessionList()
|
|
1631
1645
|
const connection = captureConnection(), request = ++sessionsRequest
|
|
1632
1646
|
const v = await safeRpc('session.list', {}, t('err.sessionList'))
|
|
1633
1647
|
if (!connection.valid() || request !== sessionsRequest) return
|
|
@@ -1654,6 +1668,7 @@ async function refreshSessions() {
|
|
|
1654
1668
|
|
|
1655
1669
|
function removeLocalSessionRecord(sessionId) {
|
|
1656
1670
|
if (!sessionId) return
|
|
1671
|
+
void window.DshHistory.cacheRemove(historyCacheScope(),sessionId)
|
|
1657
1672
|
state.sessions = state.sessions.filter(session => session?.sessionId !== sessionId)
|
|
1658
1673
|
state.byId.delete(sessionId)
|
|
1659
1674
|
state.pendingProjections.delete(sessionId)
|
|
@@ -1661,11 +1676,7 @@ function removeLocalSessionRecord(sessionId) {
|
|
|
1661
1676
|
state.pendingPrompts.delete(sessionId)
|
|
1662
1677
|
delete state.queues[sessionId]
|
|
1663
1678
|
delete state.jobs[sessionId]
|
|
1664
|
-
|
|
1665
|
-
if (Object.prototype.hasOwnProperty.call(historyCache, sessionId)) {
|
|
1666
|
-
delete historyCache[sessionId]
|
|
1667
|
-
writeHistoryCache(historyCache)
|
|
1668
|
-
}
|
|
1679
|
+
LS.del(historyCacheScope()) // Also discard the obsolete monolithic cache.
|
|
1669
1680
|
cacheWrite(CACHE.sessions, state.sessions.slice(0, 80))
|
|
1670
1681
|
}
|
|
1671
1682
|
|
|
@@ -1749,7 +1760,11 @@ function short(id) { return '…' + String(id).slice(-8) }
|
|
|
1749
1760
|
function isTopLevelSession(session) {
|
|
1750
1761
|
return !!session && !session.parentSessionId && session.origin !== 'subagent'
|
|
1751
1762
|
}
|
|
1752
|
-
|
|
1763
|
+
// DSH marks sessions created via "new session" but never prompted as blank and hides
|
|
1764
|
+
// them in its own UI; keep the same semantics here so they do not occupy the list,
|
|
1765
|
+
// the workspace tree or the home stats.
|
|
1766
|
+
function isVisibleSession(session) { return isTopLevelSession(session) && !session?.blank }
|
|
1767
|
+
function topLevelSessions() { return state.sessions.filter(isVisibleSession) }
|
|
1753
1768
|
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
1754
1769
|
function isGoalTerminal(goal) {
|
|
1755
1770
|
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
@@ -2025,7 +2040,7 @@ function renderWorkbench() {
|
|
|
2025
2040
|
const projectHtml = projects.map(w => {
|
|
2026
2041
|
const id = String(w.workspaceId || '')
|
|
2027
2042
|
const open = !!state.wbOpenProjects[id]
|
|
2028
|
-
const sessions = orderedWorkspaceSessions(id, (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(
|
|
2043
|
+
const sessions = orderedWorkspaceSessions(id, (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isVisibleSession).filter(s => !archivedSet.has(s.sessionId)))
|
|
2029
2044
|
const body = open ? `<div class="wb-sessions">${sessions.length ? sessions.map(s => `
|
|
2030
2045
|
<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
2031
2046
|
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}">
|
|
@@ -2196,9 +2211,9 @@ function renderSessions() {
|
|
|
2196
2211
|
|
|
2197
2212
|
/* ---------------- 会话详情 ---------------- */
|
|
2198
2213
|
const emptySessionCleanup = new Set()
|
|
2199
|
-
async function archiveEmptySessionOnLeave(sessionId) {
|
|
2200
|
-
const base = state.server
|
|
2201
|
-
const protectedSession = () => state.server !== base || state.current !== sessionId
|
|
2214
|
+
async function archiveEmptySessionOnLeave(sessionId, leaving = false) {
|
|
2215
|
+
const base = state.server, connection=captureConnection()
|
|
2216
|
+
const protectedSession = () => !connection.valid() || state.server !== base || (!leaving && state.current !== sessionId)
|
|
2202
2217
|
|| !state.byId.has(sessionId) || state.byId.get(sessionId)?.running
|
|
2203
2218
|
|| state.sessionActivity?.has(sessionId) || state.pendingPrompts?.has(sessionId)
|
|
2204
2219
|
|| (state.queues[sessionId] || []).length > 0
|
|
@@ -2219,10 +2234,11 @@ async function archiveEmptySessionOnLeave(sessionId) {
|
|
|
2219
2234
|
}
|
|
2220
2235
|
|
|
2221
2236
|
async function openSession(id) {
|
|
2222
|
-
if (state.current && state.current !== id)
|
|
2237
|
+
if (state.current && state.current !== id) void archiveEmptySessionOnLeave(state.current,true)
|
|
2223
2238
|
state.current = id
|
|
2224
2239
|
setSessionRecovery('loading')
|
|
2225
2240
|
state.history = emptyHistory()
|
|
2241
|
+
const openingHistory=state.history, openingConnection=captureConnection()
|
|
2226
2242
|
document.body.classList.add('in-session')
|
|
2227
2243
|
showView('view-session')
|
|
2228
2244
|
$('btn-rename-session').classList.remove('hidden')
|
|
@@ -2233,7 +2249,9 @@ async function openSession(id) {
|
|
|
2233
2249
|
renderQueue()
|
|
2234
2250
|
renderSessionPending()
|
|
2235
2251
|
void refreshCompactionStatus(id)
|
|
2236
|
-
|
|
2252
|
+
getConversationView().reset()
|
|
2253
|
+
await restoreCachedHistory()
|
|
2254
|
+
if(state.current!==id||state.history!==openingHistory||!openingConnection.valid())return
|
|
2237
2255
|
await loadHistory(true)
|
|
2238
2256
|
renderSessionCards()
|
|
2239
2257
|
refreshSessions()
|
|
@@ -2312,13 +2330,10 @@ function renderSessionTitle() {
|
|
|
2312
2330
|
function renderSessionSub() {
|
|
2313
2331
|
const s = state.byId.get(state.current)
|
|
2314
2332
|
if (!s) { $('session-sub').textContent = ''; return }
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
const recovery = recoveryLabel()
|
|
2320
|
-
if (recovery) parts.push(recovery)
|
|
2321
|
-
$('session-sub').textContent = parts.join(' · ')
|
|
2333
|
+
const workspace = s.cwd || ''
|
|
2334
|
+
const name = workspace.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || workspace
|
|
2335
|
+
$('session-sub').textContent = '📁 ' + (name || t('session.noWorkspace')) + ' ›'
|
|
2336
|
+
$('session-sub').title = workspace || t('session.noWorkspace')
|
|
2322
2337
|
}
|
|
2323
2338
|
|
|
2324
2339
|
/** 顶栏状态: 运行中=蓝色流动渐变, 中断/出错=橙红渐变, 空闲=原样式 */
|
|
@@ -2330,7 +2345,8 @@ function updateSessionStatus() {
|
|
|
2330
2345
|
const composerStatus = $('composer-status')
|
|
2331
2346
|
const compact = activeCompaction()
|
|
2332
2347
|
if (composerStatus) {
|
|
2333
|
-
|
|
2348
|
+
const queued = (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
2349
|
+
composerStatus.classList.toggle('hidden', !s?.running && !compact && !queued)
|
|
2334
2350
|
composerStatus.classList.toggle('compacting', !!compact)
|
|
2335
2351
|
}
|
|
2336
2352
|
const composerText = $('composer-status-text')
|
|
@@ -2349,6 +2365,7 @@ function updateCancelBtn() {
|
|
|
2349
2365
|
const s = state.byId.get(state.current)
|
|
2350
2366
|
const running = s?.running || (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
2351
2367
|
$('btn-cancel').classList.toggle('hidden', !running)
|
|
2368
|
+
$('composer-status')?.classList.toggle('hidden', !running && !activeCompaction())
|
|
2352
2369
|
}
|
|
2353
2370
|
|
|
2354
2371
|
const HISTORY_MAX_VISIBLE = 5000 // 已加载的可显示事件上限(消息/工具/状态, 不含 chunk)
|
|
@@ -2443,137 +2460,102 @@ function partialReasoningHtml() {
|
|
|
2443
2460
|
.join('')
|
|
2444
2461
|
}
|
|
2445
2462
|
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
if
|
|
2449
|
-
const
|
|
2450
|
-
for (const e of drop) h.seqs.delete(e.seq)
|
|
2451
|
-
h.renderStart = Math.max(0, h.renderStart - drop.length)
|
|
2452
|
-
h.renderEnd = Math.max(h.renderStart, h.renderEnd - drop.length)
|
|
2463
|
+
|
|
2464
|
+
function restoreCachedSessionList() {
|
|
2465
|
+
if(state.sessions.length)return
|
|
2466
|
+
try{const items=JSON.parse(LS.get(CACHE.sessions,''));if(!Array.isArray(items))return;state.sessions=items.filter(item=>typeof item?.sessionId==='string').slice(0,80);state.byId=new Map(state.sessions.map(item=>[item.sessionId,item]));renderSessions()}catch{}
|
|
2453
2467
|
}
|
|
2454
2468
|
|
|
2455
|
-
|
|
2469
|
+
let conversationView = null
|
|
2470
|
+
function historyCacheScope() { return CACHE.history }
|
|
2471
|
+
function historySource(value) {
|
|
2472
|
+
state.history.source=value
|
|
2473
|
+
const el=$('history-source')
|
|
2474
|
+
if(el)el.textContent=t(({cache:'history.cached',syncing:'history.syncing',synced:'history.synced',saved:'history.saved',offline:'history.offline',saveFailed:'history.cacheFailed'})[value]||'history.synced')
|
|
2475
|
+
}
|
|
2476
|
+
function getConversationView() {
|
|
2477
|
+
if(!conversationView)conversationView=new window.DshHistory.HistoryView($('history'),{
|
|
2478
|
+
html:entry=>eventHtml(entry, {toolNames:state.history.toolNames}),
|
|
2479
|
+
older:()=>loadHistory(false),olderLabel:()=>t('history.earlier'),loadingLabel:()=>t('history.loadingEarlier'),live:partialReasoningHtml,
|
|
2480
|
+
changed:view=>{state.history.renderStart=view.start;state.history.renderEnd=view.end;$('btn-history-latest')?.classList.toggle('hidden',!view.reading&&view.end>=view.entries.length&&!state.history.tailEvicted);updateRail(view.domChanged)},
|
|
2481
|
+
scrolled:()=>{updateRail()}
|
|
2482
|
+
})
|
|
2483
|
+
return conversationView
|
|
2484
|
+
}
|
|
2456
2485
|
function scheduleHistoryCacheSave() {
|
|
2457
2486
|
clearTimeout(scheduleHistoryCacheSave._t)
|
|
2458
|
-
const connection
|
|
2459
|
-
scheduleHistoryCacheSave._t
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
const
|
|
2464
|
-
if
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
const pruned = {}
|
|
2477
|
-
for (const k of keys) pruned[k] = cache[k]
|
|
2478
|
-
writeHistoryCache(pruned)
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
/** 网关不可达时回填本地缓存的历史; 返回是否命中。 */
|
|
2482
|
-
function restoreCachedHistory() {
|
|
2483
|
-
const id = state.current
|
|
2484
|
-
if (!id) return false
|
|
2485
|
-
const cached = readHistoryCache()[id]
|
|
2486
|
-
if (!cached?.events?.length) return false
|
|
2487
|
-
if (cached.title) hydrateSessionProjections(id, { values: { title: cached.title }, asOfSeq: 0 })
|
|
2488
|
-
const h = emptyHistory()
|
|
2489
|
-
for (const e of cached.events) {
|
|
2490
|
-
if (e?.seq == null) continue
|
|
2491
|
-
h.seqs.add(e.seq)
|
|
2492
|
-
h.visible.push(e)
|
|
2493
|
-
}
|
|
2494
|
-
h.visible.sort((a, b) => a.seq - b.seq)
|
|
2495
|
-
h.loaded = true
|
|
2496
|
-
state.history = h
|
|
2497
|
-
$('history-hint').textContent = t('history.offlineCache', { n: h.visible.length })
|
|
2498
|
-
renderHistory(true)
|
|
2487
|
+
const connection=captureConnection(),id=state.current,history=state.history,scope=historyCacheScope()
|
|
2488
|
+
scheduleHistoryCacheSave._t=setTimeout(()=>{if(connection.valid()&&state.current===id&&state.history===history)void saveHistoryCache(scope,id,history)},500)
|
|
2489
|
+
}
|
|
2490
|
+
async function saveHistoryCache(scope=historyCacheScope(),id=state.current,history=state.history) {
|
|
2491
|
+
if(!id||!history.visible.length||history.tailEvicted)return
|
|
2492
|
+
const saved=await window.DshHistory.cacheSave(scope,id,{title:titleOf(state.byId.get(id)),events:history.visible.slice(-250),minSeq:history.visible.slice(-250)[0]?.seq??history.minSeq,hasMore:history.hasMore||history.visible.length>250,partialReasoning:[...history.partialReasoning.values()]})
|
|
2493
|
+
if(!saved&&state.current===id&&state.history===history&&scope===historyCacheScope())historySource('saveFailed')
|
|
2494
|
+
if(saved&&state.current===id&&state.history===history&&scope===historyCacheScope()&&['synced','saveFailed'].includes(history.source))historySource('saved')
|
|
2495
|
+
}
|
|
2496
|
+
async function restoreCachedHistory() {
|
|
2497
|
+
const connection=captureConnection(),id=state.current,history=state.history,scope=historyCacheScope()
|
|
2498
|
+
if(!id)return false
|
|
2499
|
+
const cached=await window.DshHistory.cacheLoad(scope,id,legacyId=>window.DshHistory.decodeLegacy(LS.get(scope,''),legacyId))
|
|
2500
|
+
if(!connection.valid()||state.current!==id||state.history!==history||!cached?.events?.length)return false
|
|
2501
|
+
history.visible=cached.events.slice(-250).filter(entry=>entry?.seq!=null&&shouldShowEvent(entry.event?.type,entry.event)).sort((a,b)=>a.seq-b.seq)
|
|
2502
|
+
history.seqs=new Set(history.visible.map(entry=>entry.seq));history.loaded=true;history.cached=true;history.hasMore=cached.events.length>250||cached.hasMore!==false;history.minSeq=cached.events.length>250?(history.visible[0]?.seq??Infinity):Number.isFinite(cached.minSeq)?cached.minSeq:history.visible[0]?.seq??Infinity
|
|
2503
|
+
if(cached.title&&!hasSessionTitle(state.byId.get(id)))hydrateSessionProjections(id,{values:{title:cached.title},asOfSeq:0})
|
|
2504
|
+
applyReasoningBaseline(cached.partialReasoning||[]);historySource('cache');setSessionRecovery('cached');renderHistory(true)
|
|
2499
2505
|
return true
|
|
2500
2506
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
const
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
const
|
|
2513
|
-
if
|
|
2514
|
-
|
|
2507
|
+
function trimVisible(direction='tail') {
|
|
2508
|
+
const h=state.history,excess=h.visible.length-5000
|
|
2509
|
+
if(excess<=0)return
|
|
2510
|
+
const older=direction==='older'||conversationView?.reading
|
|
2511
|
+
const removed=older?h.visible.splice(5000):h.visible.splice(0,excess)
|
|
2512
|
+
for(const entry of removed)h.seqs.delete(entry.seq)
|
|
2513
|
+
if(older)h.tailEvicted=true
|
|
2514
|
+
else{h.minSeq=h.visible[0]?.seq??Infinity;h.headEvicted=true;h.hasMore=true}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
async function loadHistory(reset = true) {
|
|
2518
|
+
const connection=captureConnection(),id=state.current,history=state.history
|
|
2519
|
+
if(!id||history.loading||(!reset&&Date.now()<(history.retryAfter||0)))return
|
|
2520
|
+
const reasoningVersion=history.reasoningVersion||0,hadContent=history.visible.length>0
|
|
2521
|
+
history.loading=true
|
|
2522
|
+
if(reset){if(hadContent)historySource('syncing');else setSessionRecovery('loading')}
|
|
2523
|
+
const payload={sessionId:id,maxMessages:60}
|
|
2524
|
+
if(!reset&&Number.isFinite(history.minSeq))payload.beforeSeq=history.minSeq
|
|
2525
|
+
getConversationView().more.disabled=true
|
|
2526
|
+
getConversationView().more.textContent=getConversationView().options.loadingLabel()
|
|
2515
2527
|
let v
|
|
2516
|
-
try {
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
if
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
toast(t('history.cacheFallback'), 'ok')
|
|
2525
|
-
return
|
|
2526
|
-
}
|
|
2527
|
-
const msg = e.message || t('err.dshError')
|
|
2528
|
-
setSessionRecovery('error', msg)
|
|
2529
|
-
const box = $('history')
|
|
2530
|
-
if (box && (reset || !state.history.visible.length)) {
|
|
2531
|
-
box.innerHTML = `<div class="empty"><div>${esc(t('history.loadFailed', { msg }))}</div><button type="button" class="mini-btn" id="btn-history-retry" style="margin-top:10px">${esc(t('history.retry'))}</button></div>`
|
|
2532
|
-
const retry = $('btn-history-retry')
|
|
2533
|
-
if (retry) retry.addEventListener('click', () => loadHistory(true))
|
|
2534
|
-
} else {
|
|
2535
|
-
toast(t('history.loadFailed', { msg }), 'err')
|
|
2536
|
-
}
|
|
2528
|
+
try{v=await rpc('session.history',payload)}catch(error){
|
|
2529
|
+
if(!connection.valid()||state.current!==id||state.history!==history)return
|
|
2530
|
+
history.loading=false;history.retryAfter=Date.now()+1500
|
|
2531
|
+
if(error.message==='AUTH'){authFailure();return}
|
|
2532
|
+
if(hadContent){historySource('offline');setSessionRecovery('cached',error.message);renderHistory(false,'fixed');return}
|
|
2533
|
+
setSessionRecovery('error',error.message)
|
|
2534
|
+
$('history').innerHTML='<div class="history-empty">'+esc(error.message)+'</div>'
|
|
2535
|
+
const retry=document.createElement('button');retry.type='button';retry.className='history-page-control';retry.textContent=t('history.retry');retry.addEventListener('click',()=>{void loadHistory(true)});$('history').append(retry)
|
|
2537
2536
|
return
|
|
2538
2537
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
history.loaded = true
|
|
2544
|
-
const incoming = v.events || []
|
|
2545
|
-
let added = 0
|
|
2546
|
-
if (reset) state.history.partialReasoning.clear()
|
|
2538
|
+
if(!connection.valid()||state.current!==id||state.history!==history)return
|
|
2539
|
+
const liveReasoning=(history.reasoningVersion||0)!==reasoningVersion?new Map(history.partialReasoning):null
|
|
2540
|
+
hydrateSessionProjections(id,v.projections);history.loaded=true
|
|
2541
|
+
const incoming=v.events||[],prepared=[]
|
|
2547
2542
|
for (const entry of incoming) {
|
|
2548
|
-
const ev
|
|
2549
|
-
const seq = ev?.seq
|
|
2550
|
-
if (ev?.type === 'turn/start' || ev?.type === 'turn/end') noteSessionTurnTime(id, ev)
|
|
2543
|
+
const ev=entry?.event
|
|
2551
2544
|
applyReasoningStreamEvent(ev)
|
|
2552
|
-
if
|
|
2553
|
-
if
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
else applyReasoningBaseline(v.partialReasoning)
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
history.loading = false
|
|
2567
|
-
setSessionRecovery('ready')
|
|
2568
|
-
renderSessionTitle(); renderSessionSub(); renderSessionCards()
|
|
2569
|
-
try {
|
|
2570
|
-
if (reset) renderHistory(true)
|
|
2571
|
-
else if (added) renderHistory(false, 'keep')
|
|
2572
|
-
} catch (e) {
|
|
2573
|
-
console.error('renderHistory failed', e)
|
|
2574
|
-
}
|
|
2575
|
-
if (moreBtn) moreBtn.classList.toggle('hidden', !state.history.hasMore)
|
|
2576
|
-
$('history-hint').textContent = state.history.visible.length ? t('history.count', { n: state.history.visible.length }) : ''
|
|
2545
|
+
if(ev?.type==='turn/start'||ev?.type==='turn/end')noteSessionTurnTime(id,ev)
|
|
2546
|
+
if(ev?.seq==null||!shouldShowEvent(ev.type,ev))continue
|
|
2547
|
+
prepared.push({seq:ev.seq,event:ev,view:entry.view})
|
|
2548
|
+
}
|
|
2549
|
+
const rawSeqs=incoming.map(entry=>entry?.event?.seq).filter(Number.isFinite),floor=rawSeqs.length?Math.min(...rawSeqs):Infinity
|
|
2550
|
+
if(reset){const retainedOlder=!!v.hasMore&&history.visible.some(entry=>entry.seq<floor);const ceiling=rawSeqs.length?Math.max(...rawSeqs)+1:Infinity;history.visible=rawSeqs.length?window.DshHistory.reconcile(history.visible,prepared,floor,!!v.hasMore,ceiling):[];history.minSeq=retainedOlder?Math.min(history.minSeq,floor):floor;history.tailEvicted=false}
|
|
2551
|
+
else{history.visible=window.DshHistory.reconcile(history.visible,prepared,floor,true,payload.beforeSeq);history.minSeq=Math.min(history.minSeq,floor)}
|
|
2552
|
+
history.seqs=new Set(history.visible.map(entry=>entry.seq));history.headEvicted=false;trimVisible(reset?'tail':'older')
|
|
2553
|
+
if(liveReasoning)history.partialReasoning=liveReasoning;else applyReasoningBaseline(v.partialReasoning)
|
|
2554
|
+
history.hasMore=(!!v.hasMore||(reset&&history.headEvicted)) && rawSeqs.length>0 && (reset || floor<payload.beforeSeq)
|
|
2555
|
+
history.loading=false;history.cached=false;historySource('synced');setSessionRecovery('ready')
|
|
2556
|
+
renderSessionTitle();renderSessionSub();renderSessionCards()
|
|
2557
|
+
renderHistory(!hadContent,reset?'fixed':'keep')
|
|
2558
|
+
$('history-hint').textContent=t('history.count',{n:history.visible.length})
|
|
2577
2559
|
scheduleHistoryCacheSave()
|
|
2578
2560
|
}
|
|
2579
2561
|
|
|
@@ -2593,18 +2575,7 @@ function insertLiveEvent(event) {
|
|
|
2593
2575
|
h.visible.push({ seq, event })
|
|
2594
2576
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
2595
2577
|
trimVisible()
|
|
2596
|
-
|
|
2597
|
-
const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 240
|
|
2598
|
-
if (nearBottom) {
|
|
2599
|
-
h.renderEnd = h.visible.length
|
|
2600
|
-
h.renderStart = Math.max(0, h.renderEnd - 200)
|
|
2601
|
-
renderHistory(false, 'bottom')
|
|
2602
|
-
} else {
|
|
2603
|
-
// 图片异步撑高历史区后,用户可能瞬间不再满足 nearBottom。新回复仍须
|
|
2604
|
-
// 扩展可见窗口,但保持当前阅读位置,不能等到重新进入会话才出现。
|
|
2605
|
-
h.renderEnd = h.visible.length
|
|
2606
|
-
renderHistory(false, 'fixed')
|
|
2607
|
-
}
|
|
2578
|
+
renderHistory(false, 'fixed')
|
|
2608
2579
|
scheduleHistoryCacheSave()
|
|
2609
2580
|
}
|
|
2610
2581
|
|
|
@@ -2617,74 +2588,33 @@ function filteredEntries() {
|
|
|
2617
2588
|
return f
|
|
2618
2589
|
}
|
|
2619
2590
|
|
|
2620
|
-
function renderHistory(reset,
|
|
2621
|
-
const
|
|
2622
|
-
const
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
if (!len && !reasoningHtml) {
|
|
2627
|
-
box.innerHTML = '<div class="empty">' + t('history.empty') + '</div>'
|
|
2628
|
-
h.renderStart = 0; h.renderEnd = 0
|
|
2629
|
-
updateRail()
|
|
2630
|
-
return
|
|
2631
|
-
}
|
|
2632
|
-
if (reset) {
|
|
2633
|
-
h.renderEnd = len
|
|
2634
|
-
h.renderStart = Math.max(0, len - 200)
|
|
2635
|
-
}
|
|
2636
|
-
const start = Math.min(h.renderStart, len)
|
|
2637
|
-
const end = Math.min(h.renderEnd, len) || len
|
|
2638
|
-
const oldH = box.scrollHeight
|
|
2639
|
-
const oldTop = box.scrollTop
|
|
2640
|
-
// callId → 工具名, 供 tool/result 折叠标题显示
|
|
2641
|
-
const toolNames = new Map()
|
|
2642
|
-
for (const e of state.history.visible) {
|
|
2643
|
-
if (e.event?.type !== 'tool/call') continue
|
|
2644
|
-
const d = e.event.data || {}
|
|
2645
|
-
if (d.callId && d.name) toolNames.set(d.callId, d.name)
|
|
2646
|
-
}
|
|
2647
|
-
box.innerHTML = filtered.slice(start, end).map(e => eventHtml(e, { toolNames })).join('') + reasoningHtml
|
|
2648
|
-
if (reset || mode === 'bottom') box.scrollTop = box.scrollHeight
|
|
2649
|
-
else if (mode === 'keep') box.scrollTop = Math.max(0, oldTop + (box.scrollHeight - oldH))
|
|
2650
|
-
else if (mode === 'fixed') box.scrollTop = oldTop
|
|
2651
|
-
updateRail()
|
|
2591
|
+
function renderHistory(reset=false,mode='auto') {
|
|
2592
|
+
const filtered=filteredEntries(),toolNames=new Map()
|
|
2593
|
+
for(const entry of state.history.visible){const d=entry.event?.data;if(entry.event?.type==='tool/call'&&d?.callId&&d.name)toolNames.set(d.callId,d.name)}
|
|
2594
|
+
state.history.toolNames=toolNames
|
|
2595
|
+
$('history-hint').textContent=t('history.count',{n:state.history.visible.length})
|
|
2596
|
+
getConversationView().set(filtered,{reset,follow:mode==='bottom',live:partialReasoningHtml(),hasMore:state.history.hasMore,loading:state.history.loading,emptyLabel:t('history.empty')})
|
|
2652
2597
|
}
|
|
2653
2598
|
|
|
2654
2599
|
/* 右侧导航条: 用户发言节点 + 拖动快速定位 */
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
const thumb
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
thumb.style.top = (4 + ratio * maxTop) + 'px'
|
|
2674
|
-
|
|
2675
|
-
const boxTop = box.getBoundingClientRect().top
|
|
2676
|
-
const userNodes = [...box.querySelectorAll('.msg.user')]
|
|
2677
|
-
nodesBox.innerHTML = userNodes.map(el => {
|
|
2678
|
-
const off = el.getBoundingClientRect().top - boxTop + box.scrollTop
|
|
2679
|
-
const pos = Math.min(4 + trackH, 4 + off / Math.max(1, sh) * trackH)
|
|
2680
|
-
return `<div class="rail-node" data-offset="${Math.round(off)}" style="top:${pos}px"></div>`
|
|
2681
|
-
}).join('')
|
|
2682
|
-
let activeIdx = -1
|
|
2683
|
-
userNodes.forEach((el, i) => {
|
|
2684
|
-
const off = el.getBoundingClientRect().top - boxTop + box.scrollTop
|
|
2685
|
-
if (off <= box.scrollTop + 60) activeIdx = i
|
|
2686
|
-
})
|
|
2687
|
-
if (activeIdx >= 0) nodesBox.children[activeIdx]?.classList.add('active')
|
|
2600
|
+
let historyRailLayout=null
|
|
2601
|
+
function updateRail(rebuild=false) {
|
|
2602
|
+
const box=$('history'),thumb=$('rail-thumb'),nodesBox=$('rail-nodes')
|
|
2603
|
+
if(!box||!thumb||!nodesBox)return
|
|
2604
|
+
const sh=box.scrollHeight,ch=box.clientHeight
|
|
2605
|
+
if(sh<=ch){thumb.style.display='none';nodesBox.replaceChildren();historyRailLayout=null;return}
|
|
2606
|
+
thumb.style.display=''
|
|
2607
|
+
const trackH=Math.max(1,ch-8),thumbH=Math.max(32,ch/sh*trackH),ratio=box.scrollTop/Math.max(1,sh-ch)
|
|
2608
|
+
thumb.style.height=thumbH+'px';thumb.style.top=(4+ratio*(trackH-thumbH))+'px'
|
|
2609
|
+
if(rebuild||!historyRailLayout||historyRailLayout.height!==sh||historyRailLayout.width!==box.clientWidth){
|
|
2610
|
+
const top=box.getBoundingClientRect().top
|
|
2611
|
+
const allOffsets=[...box.querySelectorAll('.msg.user')].map(el=>el.getBoundingClientRect().top-top+box.scrollTop)
|
|
2612
|
+
const count=Math.min(24,allOffsets.length),offsets=Array.from({length:count},(_,i)=>allOffsets[Math.round(i*(allOffsets.length-1)/Math.max(1,count-1))])
|
|
2613
|
+
nodesBox.innerHTML=offsets.map(off=>'<div class="rail-node" data-offset="'+Math.round(off)+'" style="top:'+Math.min(4+trackH,4+off/Math.max(1,sh)*trackH)+'px"></div>').join('')
|
|
2614
|
+
historyRailLayout={height:sh,width:box.clientWidth,offsets}
|
|
2615
|
+
}
|
|
2616
|
+
let active=-1;historyRailLayout.offsets.forEach((offset,i)=>{if(offset<=box.scrollTop+60)active=i})
|
|
2617
|
+
for(let i=0;i<nodesBox.children.length;i++)nodesBox.children[i].classList.toggle('active',i===active)
|
|
2688
2618
|
}
|
|
2689
2619
|
|
|
2690
2620
|
function bindRail() {
|
|
@@ -2695,6 +2625,8 @@ function bindRail() {
|
|
|
2695
2625
|
nodesBox.addEventListener('click', (e) => {
|
|
2696
2626
|
const node = e.target.closest('.rail-node')
|
|
2697
2627
|
if (!node) return
|
|
2628
|
+
getConversationView().intentUntil=Date.now()+1500
|
|
2629
|
+
getConversationView().reading=true
|
|
2698
2630
|
box.scrollTo({ top: Math.max(0, Number(node.dataset.offset) - 10), behavior: 'smooth' })
|
|
2699
2631
|
})
|
|
2700
2632
|
let drag = null
|
|
@@ -2704,6 +2636,8 @@ function bindRail() {
|
|
|
2704
2636
|
})
|
|
2705
2637
|
thumb.addEventListener('pointermove', (e) => {
|
|
2706
2638
|
if (!drag) return
|
|
2639
|
+
getConversationView().intentUntil=Date.now()+1500
|
|
2640
|
+
getConversationView().reading=true
|
|
2707
2641
|
const trackH = Math.max(1, box.clientHeight - 8)
|
|
2708
2642
|
const delta = (e.clientY - drag.y) / trackH * Math.max(1, box.scrollHeight - box.clientHeight)
|
|
2709
2643
|
box.scrollTop = Math.max(0, Math.min(box.scrollHeight - box.clientHeight, drag.top + delta))
|
|
@@ -2722,8 +2656,7 @@ const INTERESTING_EVENTS = new Set([
|
|
|
2722
2656
|
'goal/created', 'goal/updated', 'goal/completed', 'goal/cleared',
|
|
2723
2657
|
'todo/updated', 'plan/updated',
|
|
2724
2658
|
'question/asked', 'question/resolved',
|
|
2725
|
-
'approval/asked', 'approval/resolved'
|
|
2726
|
-
'session/title', 'title'
|
|
2659
|
+
'approval/asked', 'approval/resolved'
|
|
2727
2660
|
])
|
|
2728
2661
|
function messageSource(data) {
|
|
2729
2662
|
const source = data?.source ?? data?.message?.source
|
|
@@ -3648,36 +3581,88 @@ function bindSessionSwipe() {
|
|
|
3648
3581
|
}
|
|
3649
3582
|
|
|
3650
3583
|
/* ---------------- 系统总览 / 待办 ---------------- */
|
|
3584
|
+
let overviewDetector = null
|
|
3585
|
+
let overviewPulse = null
|
|
3586
|
+
function setOverviewText(id,value){const node=$(id);if(overviewPulse)overviewPulse.text(node,value);else if(node)node.textContent=value}
|
|
3587
|
+
let overviewStarting = true
|
|
3588
|
+
function overviewChecks() {
|
|
3589
|
+
const confirmed=overviewDetector?.model.gateway===true,health=activeGatewayHealth()
|
|
3590
|
+
return {
|
|
3591
|
+
gateway: !!state.token && (!!state.server || /^https?:$/.test(location.protocol)) && confirmed,
|
|
3592
|
+
dsh: dshReachable() && confirmed,
|
|
3593
|
+
mux: !!state.streamsOk?.mux && confirmed && health?.events?.mux?.connected!==false,
|
|
3594
|
+
host: !!state.streamsOk?.host && confirmed && health?.events?.host?.connected!==false
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
function repairMissingStreams() {
|
|
3598
|
+
if(!state.token||!navigator.onLine||state.selectingServer||overviewStarting)return
|
|
3599
|
+
if(state.streamMode==='poll'){tryRestoreWs();return}
|
|
3600
|
+
for(const [kind,handler,refresh] of [['mux',onMuxFrame,true],['host',onHostFrame,false]]) {
|
|
3601
|
+
const ws=streams[kind]
|
|
3602
|
+
if((!ws||ws.readyState===WebSocket.CLOSED)&&!streamMeta[kind].retryTimer)openStream(kind,handler,refresh)
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
async function probeOverviewLinks(signal) {
|
|
3606
|
+
const connection=captureConnection(),base=String(connection.server||location.origin).replace(/\/+$/,'')
|
|
3607
|
+
let timer
|
|
3608
|
+
const controller=new AbortController(),cancel=()=>controller.abort()
|
|
3609
|
+
signal.addEventListener('abort',cancel,{once:true});if(signal.aborted)controller.abort()
|
|
3610
|
+
timer=setTimeout(cancel,4000)
|
|
3611
|
+
const healthRequest=fetch(base+'/health?t='+Date.now(),{signal:controller.signal,cache:'no-store'}).then(async response=>{
|
|
3612
|
+
if(!response.ok)throw Error('health')
|
|
3613
|
+
const health=await response.json();if(health?.ok!==true)throw Error('health')
|
|
3614
|
+
return health
|
|
3615
|
+
}).catch(()=>null).finally(()=>{clearTimeout(timer);signal.removeEventListener('abort',cancel)})
|
|
3616
|
+
const [health,host]=await Promise.all([healthRequest,refreshHostDescription()])
|
|
3617
|
+
if(!connection.valid()||signal.aborted)throw Error('stale check')
|
|
3618
|
+
if(health){if(host)health.upstreamReachable=true;state.gatewayHealth[base]=health}
|
|
3619
|
+
else delete state.gatewayHealth[base]
|
|
3620
|
+
return {gateway:!!health}
|
|
3621
|
+
}
|
|
3622
|
+
function startOverviewDetection(force=false,identityChanged=false) {
|
|
3623
|
+
if(identityChanged)overviewDetector?.pause()
|
|
3624
|
+
if(!window.DshLinkCheck)return
|
|
3625
|
+
if(!overviewDetector)overviewDetector=new window.DshLinkCheck.LinkCheck({
|
|
3626
|
+
active:()=>document.visibilityState==='visible'&&!$('view-activity').classList.contains('hidden'),
|
|
3627
|
+
configured:()=>!!state.token&&(!!state.server||/^https?:$/.test(location.protocol)),online:()=>navigator.onLine,
|
|
3628
|
+
snapshot:overviewChecks,repair:repairMissingStreams,probe:probeOverviewLinks,changed:renderOverview
|
|
3629
|
+
})
|
|
3630
|
+
overviewDetector.start(force)
|
|
3631
|
+
}
|
|
3632
|
+
window.addEventListener('online',()=>startOverviewDetection(true))
|
|
3633
|
+
window.addEventListener('offline',()=>startOverviewDetection(true))
|
|
3634
|
+
document.addEventListener('visibilitychange',()=>{if(document.visibilityState==='visible')startOverviewDetection(true);else {overviewDetector?.pause();overviewPulse?.pause()}})
|
|
3635
|
+
|
|
3651
3636
|
function renderOverview() {
|
|
3652
3637
|
const ring = $('overview-pulse-ring')
|
|
3653
3638
|
if (!ring) return
|
|
3654
|
-
const checks =
|
|
3655
|
-
|
|
3656
|
-
// 已连接网关误报为离线。Capacitor 等非 HTTP 页面仍要求显式服务器。
|
|
3657
|
-
gateway: !!state.token && (!!state.server || /^https?:$/.test(location.protocol)),
|
|
3658
|
-
dsh: dshReachable(),
|
|
3659
|
-
mux: !!state.streamsOk?.mux,
|
|
3660
|
-
host: !!state.streamsOk?.host
|
|
3661
|
-
}
|
|
3639
|
+
const checks = overviewChecks()
|
|
3640
|
+
const detection=overviewDetector?.model,checking=detection?.phase==='checking'||(detection?.phase==='degraded'&&detection?.probing===true)
|
|
3662
3641
|
const online = Object.values(checks).filter(Boolean).length
|
|
3642
|
+
$('overview-refresh').disabled=detection?.probing===true
|
|
3663
3643
|
const status = online === 4 ? 'nominal' : online > 0 ? 'degraded' : 'offline'
|
|
3664
3644
|
const pulseCard = document.querySelector('.overview-pulse-card')
|
|
3665
3645
|
if (pulseCard) {
|
|
3646
|
+
if(!overviewPulse&&window.DshLinkCheck?.PulseVisual)overviewPulse=new window.DshLinkCheck.PulseVisual({card:pulseCard,active:()=>document.visibilityState==='visible'&&!$('view-activity').classList.contains('hidden')})
|
|
3647
|
+
overviewPulse?.setChecking(checking)
|
|
3648
|
+
overviewPulse?.setLinks(checks)
|
|
3666
3649
|
pulseCard.classList.remove('status-nominal', 'status-degraded', 'status-offline')
|
|
3650
|
+
pulseCard.classList.toggle('status-checking',checking)
|
|
3651
|
+
pulseCard.setAttribute('aria-busy',String(checking))
|
|
3667
3652
|
pulseCard.classList.add('status-' + status)
|
|
3668
3653
|
}
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
$('overview-status-desc').textContent = t('overview.components', { n: online })
|
|
3654
|
+
setOverviewText('overview-health',checking ? t('overview.checking') : online === 4 ? t('overview.live') : online ? `${online}/4` : t('overview.offlineCore'))
|
|
3655
|
+
setOverviewText('overview-health-caption',checking ? `${online}/4` : online === 4 ? t('overview.allLinked') : online ? t('overview.confirmedShort') : t('overview.offlineShort'))
|
|
3656
|
+
setOverviewText('overview-status',checking ? t('overview.checking') : t(`overview.${status}`))
|
|
3657
|
+
setOverviewText('overview-status-desc',checking ? t('overview.autoChecking',{n:online}) : detection?.phase==='degraded' ? t('overview.autoRetry',{n:online}) : t('overview.components',{n:online}))
|
|
3674
3658
|
for (const [name, ok] of Object.entries(checks)) {
|
|
3675
3659
|
const item = document.querySelector(`[data-overview-link="${name}"]`)
|
|
3676
3660
|
if (!item) continue
|
|
3677
3661
|
item.classList.toggle('ok', ok)
|
|
3678
|
-
item.classList.toggle('off', !ok)
|
|
3662
|
+
item.classList.toggle('off', !ok&&!checking)
|
|
3663
|
+
item.classList.toggle('pending',!ok&&checking)
|
|
3679
3664
|
const value = item.querySelector('b')
|
|
3680
|
-
if (value) value.
|
|
3665
|
+
if (value) setOverviewText(value.id || (value.id=`overview-link-${name}`), ok ? t('overview.online') : checking ? t('overview.checkingShort') : t('overview.offlineShort'))
|
|
3681
3666
|
}
|
|
3682
3667
|
|
|
3683
3668
|
const pending = [
|
|
@@ -5266,11 +5251,9 @@ async function verifyUpdateApk(info, url) {
|
|
|
5266
5251
|
phase = 'verifying'
|
|
5267
5252
|
updateDownloadProgress({ phase, received, total: total || received })
|
|
5268
5253
|
const expected = String(info.sha256 || '').trim().toLowerCase()
|
|
5269
|
-
if (expected) {
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
if (actual !== expected) return { ok: false, corrupted: true }
|
|
5273
|
-
}
|
|
5254
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) return { ok: false, corrupted: true }
|
|
5255
|
+
const actual = await sha256Hex(await blob.arrayBuffer())
|
|
5256
|
+
if (actual !== expected) return { ok: false, corrupted: true }
|
|
5274
5257
|
return { ok: true, blob }
|
|
5275
5258
|
} catch (err) { return { ok: false, network: true, msg: err?.message || '' } }
|
|
5276
5259
|
finally { clearTimeout(timer); clearInterval(progressTimer) }
|
|
@@ -5280,13 +5263,19 @@ async function downloadUpdate() {
|
|
|
5280
5263
|
const info = state.updateInfo
|
|
5281
5264
|
if (!info || updateDownloadBusy) return
|
|
5282
5265
|
const url = new URL(info.apkUrl || 'dsh-remote.apk', updateBase() + '/').href
|
|
5266
|
+
const sha256 = String(info.sha256 || '').trim().toLowerCase()
|
|
5267
|
+
if (!/^[0-9a-f]{64}$/.test(sha256)) {
|
|
5268
|
+
// 无 SHA-256 的更新包一律不下载、不安装
|
|
5269
|
+
updateDownloadProgress({ phase: 'error', error: t('update.hashMissing') })
|
|
5270
|
+
return
|
|
5271
|
+
}
|
|
5283
5272
|
updateDownloadBusy = true
|
|
5284
5273
|
updateDownloadSample = null
|
|
5285
5274
|
$('btn-download-update').disabled = true
|
|
5286
5275
|
updateDownloadProgress({ phase: 'downloading', received: 0, total: 0 })
|
|
5287
5276
|
if (CAP?.isNativePlatform?.() && window.NativeUpdate?.downloadVerifiedAndInstall && window.NativeUpdate?.getDownloadStatus) {
|
|
5288
5277
|
try {
|
|
5289
|
-
if (!window.NativeUpdate.downloadVerifiedAndInstall(url,
|
|
5278
|
+
if (!window.NativeUpdate.downloadVerifiedAndInstall(url, sha256)) throw new Error(t('update.busy'))
|
|
5290
5279
|
const poll = () => {
|
|
5291
5280
|
try {
|
|
5292
5281
|
const value = JSON.parse(window.NativeUpdate.getDownloadStatus())
|
|
@@ -5302,11 +5291,8 @@ async function downloadUpdate() {
|
|
|
5302
5291
|
try {
|
|
5303
5292
|
const result = await verifyUpdateApk(info, url)
|
|
5304
5293
|
if (!result.ok) throw new Error(result.corrupted ? t('update.corrupted') : result.status ? t('update.serverFileMissing') : result.msg || t('fs.networkError'))
|
|
5305
|
-
|
|
5306
|
-
//
|
|
5307
|
-
window.NativeUpdate.downloadAndInstall(url)
|
|
5308
|
-
updateDownloadProgress({ phase: 'legacy', received: 0, total: 0 })
|
|
5309
|
-
} else {
|
|
5294
|
+
{
|
|
5295
|
+
// 无校验的原生安装入口已移除: 新旧壳统一走下方"下载并本地校验"路径。
|
|
5310
5296
|
const objectUrl = URL.createObjectURL(result.blob)
|
|
5311
5297
|
const link = document.createElement('a')
|
|
5312
5298
|
link.href = objectUrl
|
|
@@ -6084,6 +6070,8 @@ function showView(id) {
|
|
|
6084
6070
|
for (const v of ['view-home', 'view-files', 'view-session', 'view-activity', 'view-plugins', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
6085
6071
|
// 离开会话页必须清掉 in-session, 否则其他页面顶栏被 body 样式隐藏
|
|
6086
6072
|
document.body.classList.toggle('in-session', id === 'view-session')
|
|
6073
|
+
$('session-actions')?.close()
|
|
6074
|
+
$('session-info')?.close()
|
|
6087
6075
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
6088
6076
|
// 主页已有上下文明确的刷新按钮,避免顶栏出现第二个同义入口。
|
|
6089
6077
|
$('btn-refresh')?.classList.toggle('hidden', id === 'view-activity')
|
|
@@ -6096,6 +6084,7 @@ function showView(id) {
|
|
|
6096
6084
|
if (id === 'view-plugins') openPluginPage()
|
|
6097
6085
|
else window.DshPluginCenter?.close()
|
|
6098
6086
|
if (id === 'view-settings') showSettingsHome()
|
|
6087
|
+
if(id==='view-activity')startOverviewDetection(true);else {overviewDetector?.pause();overviewPulse?.pause()}
|
|
6099
6088
|
}
|
|
6100
6089
|
|
|
6101
6090
|
const SETTINGS_GROUPS = ['general', 'model', 'tests', 'servers', 'notify', 'theme', 'about']
|
|
@@ -6116,6 +6105,7 @@ function showSettingsPage(name) {
|
|
|
6116
6105
|
}
|
|
6117
6106
|
|
|
6118
6107
|
function updateConn() {
|
|
6108
|
+
overviewDetector?.observe()
|
|
6119
6109
|
const el = $('conn-badge')
|
|
6120
6110
|
// mux/host 的打开顺序不稳定;连接状态改变时同步重绘总览,避免后打开的
|
|
6121
6111
|
// 通道只更新顶栏、总览却永久停留在 3/4。
|
|
@@ -6186,8 +6176,8 @@ function updateComposerFullscreenButton() {
|
|
|
6186
6176
|
function setComposerFullscreen(on) {
|
|
6187
6177
|
const wrap = $('composer-wrap')
|
|
6188
6178
|
if (!wrap) return
|
|
6189
|
-
$('btn-
|
|
6190
|
-
$('
|
|
6179
|
+
$('btn-session-more')?.classList.toggle('hidden', !!on)
|
|
6180
|
+
$('session-actions')?.close()
|
|
6191
6181
|
$('btn-fs-send')?.classList.toggle('hidden', !on)
|
|
6192
6182
|
if (on) {
|
|
6193
6183
|
$('composer-image-menu')?.classList.add('hidden')
|
|
@@ -6239,7 +6229,7 @@ function bindComposerFullscreenGesture() {
|
|
|
6239
6229
|
|
|
6240
6230
|
/* ---------------- 初始化 ---------------- */
|
|
6241
6231
|
/** 解析 dshremote://pair?token=..&server=.. 配对二维码;server 可重复以携带多个主机地址。 */
|
|
6242
|
-
function applyPairUrl(url) {
|
|
6232
|
+
function applyPairUrl(url, options = {}) {
|
|
6243
6233
|
try {
|
|
6244
6234
|
const u = new URL(String(url).trim())
|
|
6245
6235
|
if (u.protocol !== 'dshremote:' || u.hostname !== 'pair') return false
|
|
@@ -6248,6 +6238,13 @@ function applyPairUrl(url) {
|
|
|
6248
6238
|
.map(value => value.trim().replace(/\/+$/, ''))
|
|
6249
6239
|
.filter(value => /^https?:\/\//i.test(value)))]
|
|
6250
6240
|
if (!tok || !servers.length) return false
|
|
6241
|
+
const alreadyPaired = (state.servers || []).some(s => normalizedServerToken(s.token) === tok
|
|
6242
|
+
&& servers.includes(String(s.url || '').replace(/\/+$/, '')))
|
|
6243
|
+
if (options.confirmExisting && !alreadyPaired && (state.token || (state.servers || []).length)) {
|
|
6244
|
+
// 深链可被任意应用/网页触发: 已有配置时必须由用户确认,不能静默换服务器和令牌
|
|
6245
|
+
const confirmed = window.confirm(t('scan.pairConfirm', { servers: servers.join('\n') }))
|
|
6246
|
+
if (!confirmed) return false
|
|
6247
|
+
}
|
|
6251
6248
|
const previousServer = state.server
|
|
6252
6249
|
const previousToken = state.token
|
|
6253
6250
|
state.token = tok
|
|
@@ -6535,14 +6532,14 @@ function bindNativeLinks() {
|
|
|
6535
6532
|
if (!CAP?.isNativePlatform?.()) return
|
|
6536
6533
|
try {
|
|
6537
6534
|
CAP.Plugins?.App?.addListener?.('appUrlOpen', (data) => {
|
|
6538
|
-
if (data?.url && applyPairUrl(data.url)) {
|
|
6535
|
+
if (data?.url && applyPairUrl(data.url, { confirmExisting: true })) {
|
|
6539
6536
|
toast(t('scan.pairedLink'), 'ok')
|
|
6540
6537
|
openStreams()
|
|
6541
6538
|
refreshAll()
|
|
6542
6539
|
}
|
|
6543
6540
|
})
|
|
6544
6541
|
CAP.Plugins?.App?.getLaunchUrl?.().then((data) => {
|
|
6545
|
-
if (data?.url) applyPairUrl(data.url)
|
|
6542
|
+
if (data?.url) applyPairUrl(data.url, { confirmExisting: true })
|
|
6546
6543
|
}).catch(() => {})
|
|
6547
6544
|
} catch {}
|
|
6548
6545
|
}
|
|
@@ -6688,7 +6685,10 @@ function renderDshControlStatus(value) {
|
|
|
6688
6685
|
return
|
|
6689
6686
|
}
|
|
6690
6687
|
dshControlButtonsBusy(false, true)
|
|
6688
|
+
$('btn-dsh-start').disabled = value.running === true || value.canStart === false
|
|
6689
|
+
$('btn-dsh-restart').disabled = value.canRestart === false || value.running === false
|
|
6691
6690
|
if (box && !dshControlPollPromise) box.classList.add('hidden')
|
|
6691
|
+
if (value.canStart === false && value.code) { desc.textContent=dshControlFailureText(value); return }
|
|
6692
6692
|
const service = value.service || 'dsh-web'
|
|
6693
6693
|
const serviceState = `${value.activeState || value.state || 'unknown'}/${value.subState || 'unknown'}`
|
|
6694
6694
|
desc.textContent = value.running
|
|
@@ -6813,7 +6813,31 @@ function openDonateModal() {
|
|
|
6813
6813
|
if (m) m.classList.remove('hidden')
|
|
6814
6814
|
}
|
|
6815
6815
|
|
|
6816
|
+
|
|
6817
|
+
function bindSessionDialogs() {
|
|
6818
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
6819
|
+
const wrap=$('composer-wrap')
|
|
6820
|
+
const observer=new ResizeObserver(()=>{
|
|
6821
|
+
if(!wrap.classList.contains('fs'))document.documentElement.style.setProperty('--composer-height', wrap.getBoundingClientRect().height+'px')
|
|
6822
|
+
if(document.body.classList.contains('in-session'))document.documentElement.style.setProperty('--composer-top', $('session-sub').getBoundingClientRect().bottom+'px')
|
|
6823
|
+
})
|
|
6824
|
+
observer.observe(wrap)
|
|
6825
|
+
observer.observe($('session-head'))
|
|
6826
|
+
observer.observe($('session-sub'))
|
|
6827
|
+
}
|
|
6828
|
+
const actions=$('session-actions'), info=$('session-info');
|
|
6829
|
+
$('btn-session-more').addEventListener('click',()=>actions.showModal());
|
|
6830
|
+
actions.addEventListener('click',e=>{if(e.target===actions)actions.close();else if(e.target.closest('button'))actions.close()},true);
|
|
6831
|
+
const openInfo=()=>{const s=state.byId.get(state.current);if(!s)return; $('session-info-name').textContent=titleOf(s);$('session-info-path').textContent=s.cwd||t('session.noWorkspace');$('session-info-id').textContent=s.sessionId;$('btn-copy-session-workspace').disabled=!s.cwd;info.showModal()};
|
|
6832
|
+
$('session-title').addEventListener('click',openInfo);
|
|
6833
|
+
$('session-sub').addEventListener('click',openInfo);
|
|
6834
|
+
$('btn-session-info').addEventListener('click',openInfo);
|
|
6835
|
+
info.addEventListener('click',e=>{if(e.target===info||e.target.closest('[data-session-dialog-close]'))info.close()});
|
|
6836
|
+
$('btn-copy-session-workspace').addEventListener('click',async()=>{const value=state.byId.get(state.current)?.cwd;if(value&&await copyText(value))toast(t('feedback.copied'),'ok')});
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6816
6839
|
function bindUi() {
|
|
6840
|
+
bindSessionDialogs()
|
|
6817
6841
|
renderLangBtn()
|
|
6818
6842
|
renderThemeBtn()
|
|
6819
6843
|
$('btn-lang').addEventListener('click', () => {
|
|
@@ -6928,7 +6952,7 @@ function bindUi() {
|
|
|
6928
6952
|
$('btn-refresh').addEventListener('click', () => { toast(t('common.refreshing')); openStreams(); refreshAll() })
|
|
6929
6953
|
$('overview-refresh').addEventListener('click', () => {
|
|
6930
6954
|
toast(t('common.refreshing'))
|
|
6931
|
-
|
|
6955
|
+
startOverviewDetection(true)
|
|
6932
6956
|
void Promise.all([refreshAll(), checkAnnouncements()])
|
|
6933
6957
|
})
|
|
6934
6958
|
// 反馈
|
|
@@ -7190,8 +7214,10 @@ function bindUi() {
|
|
|
7190
7214
|
$('modal-app-version-warning').addEventListener('click', (e) => {
|
|
7191
7215
|
if (e.target === $('modal-app-version-warning')) closeAppVersionWarning(false)
|
|
7192
7216
|
})
|
|
7193
|
-
$('btn-reset').addEventListener('click', () => {
|
|
7217
|
+
$('btn-reset').addEventListener('click', async () => {
|
|
7194
7218
|
if (!confirm(t('settings.confirmReset'))) return
|
|
7219
|
+
clearTimeout(scheduleHistoryCacheSave._t)
|
|
7220
|
+
await window.DshHistory.cacheClear()
|
|
7195
7221
|
LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction'); LS.del('steerSendingEnabled'); LS.del('busySendMode'); LS.del(ANNOUNCEMENTS_KEY); LS.del(ANNOUNCEMENT_HISTORY_KEY); LS.del(ANNOUNCEMENT_VOTES_KEY)
|
|
7196
7222
|
if (bgBridge()?.saveBackgroundConfig) saveBgConfig(false)
|
|
7197
7223
|
location.reload()
|
|
@@ -7305,27 +7331,8 @@ function bindUi() {
|
|
|
7305
7331
|
|
|
7306
7332
|
initCustomSelects()
|
|
7307
7333
|
|
|
7308
|
-
|
|
7309
|
-
$('history').addEventListener('
|
|
7310
|
-
const box = $('history')
|
|
7311
|
-
const h = state.history
|
|
7312
|
-
updateRail()
|
|
7313
|
-
if (!state.current || !h.filtered?.length) return
|
|
7314
|
-
if (box.scrollTop < 80) {
|
|
7315
|
-
if (h.renderStart > 0) {
|
|
7316
|
-
h.renderStart = Math.max(0, h.renderStart - 100)
|
|
7317
|
-
renderHistory(false, 'keep')
|
|
7318
|
-
} else if (h.hasMore && !h.loading) {
|
|
7319
|
-
loadHistory(false)
|
|
7320
|
-
}
|
|
7321
|
-
} else if (box.scrollHeight - box.scrollTop - box.clientHeight < 240) {
|
|
7322
|
-
if (h.renderEnd < h.filtered.length) {
|
|
7323
|
-
h.renderEnd = h.filtered.length
|
|
7324
|
-
h.renderStart = Math.max(0, h.renderEnd - 200)
|
|
7325
|
-
renderHistory(false, 'bottom')
|
|
7326
|
-
}
|
|
7327
|
-
}
|
|
7328
|
-
})
|
|
7334
|
+
$('btn-history-latest').addEventListener('click',()=>{getConversationView().latest();if(state.history.tailEvicted)void loadHistory(true)})
|
|
7335
|
+
$('history').addEventListener('load',()=>updateRail(true),true)
|
|
7329
7336
|
}
|
|
7330
7337
|
|
|
7331
7338
|
/* App 内真实系统栏 inset(刘海/状态栏/手势条) */
|
|
@@ -7352,14 +7359,15 @@ async function boot() {
|
|
|
7352
7359
|
await loadLocalVersion()
|
|
7353
7360
|
if (!state.token) {
|
|
7354
7361
|
showView('view-activity')
|
|
7362
|
+
overviewStarting=false
|
|
7355
7363
|
$('token-desc').textContent = t('token.notSetHint')
|
|
7356
7364
|
} else {
|
|
7357
7365
|
// 多服务器: 启动时静默测速一次, 选最快的连接(同源页面也参与比较)
|
|
7358
7366
|
await selectFastestServer({ silent: true, reconnect: false })
|
|
7359
7367
|
await maybeWarnAppBehindGateway({ probe: true })
|
|
7368
|
+
overviewStarting=false
|
|
7360
7369
|
openStreams()
|
|
7361
|
-
await refreshAll()
|
|
7362
|
-
await refreshHostDescription()
|
|
7370
|
+
await Promise.all([refreshAll(),refreshHostDescription()])
|
|
7363
7371
|
loadDshControl()
|
|
7364
7372
|
}
|
|
7365
7373
|
// 网关从中央 HTTPS 公告源读取并在不可达时回退内置文件。前台每 30 秒检查,
|
|
@@ -7383,4 +7391,3 @@ function openPluginPage() {
|
|
|
7383
7391
|
valid: connection.valid,
|
|
7384
7392
|
}, $('plugin-page'))
|
|
7385
7393
|
}
|
|
7386
|
-
document.getElementById('btn-plugin-center')?.addEventListener('click', () => showView('view-plugins'))
|