dsh-remote-plugin 0.7.1 → 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 +322 -19
- package/index.mjs +41 -9
- package/insights.mjs +73 -0
- package/package.json +3 -2
- package/plugin-center.mjs +118 -21
- package/public/admin.html +3 -0
- package/public/admin.js +1 -0
- package/public/app.js +403 -470
- package/public/desktop/desktop.css +101 -0
- package/public/desktop/desktop.html +63 -20
- package/public/desktop/desktop.js +324 -99
- package/public/history.js +160 -0
- package/public/index.html +73 -84
- package/public/insights.css +14 -0
- package/public/insights.js +228 -0
- package/public/link-check.js +109 -0
- package/public/plugin-center.css +100 -1
- package/public/plugin-center.js +222 -60
- package/public/plugin.js +7 -1
- package/public/styles.css +107 -95
- package/public/update.json +12 -12
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -27,10 +27,15 @@ 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
|
-
sessions
|
|
33
|
-
history
|
|
32
|
+
get sessions() { return connectionCacheKey('sessions') },
|
|
33
|
+
get history() { return connectionCacheKey('history') }
|
|
34
|
+
}
|
|
35
|
+
function connectionCacheKey(kind) {
|
|
36
|
+
const hash = new SHA256()
|
|
37
|
+
hash.update(new TextEncoder().encode(JSON.stringify([state.server || location.origin, state.token])))
|
|
38
|
+
return kind + 'CacheV2:' + hash.hex()
|
|
34
39
|
}
|
|
35
40
|
function cacheRead(key, d = null) {
|
|
36
41
|
try { return JSON.parse(LS.get(key, '')) || d } catch { return d }
|
|
@@ -38,18 +43,6 @@ function cacheRead(key, d = null) {
|
|
|
38
43
|
function cacheWrite(key, value) {
|
|
39
44
|
try { LS.set(key, JSON.stringify(value)) } catch { LS.del(key) }
|
|
40
45
|
}
|
|
41
|
-
function readHistoryCache() { return cacheRead(CACHE.history, {}) || {} }
|
|
42
|
-
function writeHistoryCache(cache) {
|
|
43
|
-
try { LS.set(CACHE.history, JSON.stringify(cache)); return }
|
|
44
|
-
catch {
|
|
45
|
-
// localStorage 配额不足: 每会话只留最近 50 条再试一次
|
|
46
|
-
try {
|
|
47
|
-
for (const k of Object.keys(cache)) cache[k].events = (cache[k].events || []).slice(-50)
|
|
48
|
-
LS.set(CACHE.history, JSON.stringify(cache))
|
|
49
|
-
} catch { LS.del(CACHE.history) }
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
46
|
const state = {
|
|
54
47
|
token: '',
|
|
55
48
|
wsTicket: { token: '', server: '', value: '', expiresAt: 0 },
|
|
@@ -112,6 +105,47 @@ const state = {
|
|
|
112
105
|
subagentExpandedSession: ''
|
|
113
106
|
}
|
|
114
107
|
|
|
108
|
+
// Increment on every identity transition, including A -> B -> A and token edits.
|
|
109
|
+
let connectionGeneration = 0, sessionsRequest = 0, filesRequest = 0, workbenchRequest = 0
|
|
110
|
+
for (const key of ['server', 'token']) {
|
|
111
|
+
let value = state[key]
|
|
112
|
+
Object.defineProperty(state, key, {
|
|
113
|
+
enumerable: true,
|
|
114
|
+
get: () => value,
|
|
115
|
+
set(next) {
|
|
116
|
+
if (next === value) return
|
|
117
|
+
value = next; connectionGeneration++
|
|
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')}
|
|
121
|
+
state.current = null; state.history = emptyHistory()
|
|
122
|
+
state.fs.path = null; state.fs.initial = null; state.fs.loaded = false; state.fs.roots = []; state.fs.rootIndex = 0
|
|
123
|
+
state.wb = null; state.wbProjects = []; state.wbArchived = []; state.workspaceFilter = ''; state.fs.workspaceId = ''; clearTimeout(scheduleHistoryCacheSave._t)
|
|
124
|
+
window.DshPluginCenter?.close()
|
|
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
|
+
}
|
|
133
|
+
if (typeof document !== 'undefined') {
|
|
134
|
+
document.getElementById('modal-workspace')?.classList.add('hidden')
|
|
135
|
+
const generation = connectionGeneration
|
|
136
|
+
queueMicrotask(() => {
|
|
137
|
+
if (generation === connectionGeneration && document.getElementById('view-plugins')?.classList.contains('hidden') === false) openPluginPage()
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
function captureConnection() {
|
|
144
|
+
const generation = connectionGeneration, server = state.server, token = state.token
|
|
145
|
+
return { server, token, valid: () => generation === connectionGeneration && server === state.server && token === state.token }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
115
149
|
const $ = (id) => document.getElementById(id)
|
|
116
150
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) =>
|
|
117
151
|
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
@@ -294,6 +328,9 @@ function openFeedbackModal() {
|
|
|
294
328
|
$('fb-msg').value = ''
|
|
295
329
|
$('fb-contact').value = ''
|
|
296
330
|
$('fb-include-diagnostics').checked = false
|
|
331
|
+
$('fb-include-crashes').checked = false
|
|
332
|
+
$('fb-crash-details').open = false
|
|
333
|
+
void loadCrashPreview()
|
|
297
334
|
$('modal-feedback').classList.remove('hidden')
|
|
298
335
|
setTimeout(() => $('fb-msg').focus(), 50)
|
|
299
336
|
}
|
|
@@ -306,11 +343,22 @@ function closeFeedbackSuccess() {
|
|
|
306
343
|
$('modal-feedback-success').classList.add('hidden')
|
|
307
344
|
$('btn-feedback')?.focus()
|
|
308
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
|
+
}
|
|
309
356
|
async function submitFeedback() {
|
|
310
357
|
const type = state.feedbackType || 'bug'
|
|
311
358
|
const message = $('fb-msg').value.trim()
|
|
312
359
|
const contact = $('fb-contact').value.trim()
|
|
313
360
|
const includeDiagnostics = $('fb-include-diagnostics').checked
|
|
361
|
+
const includeCrashLogs = $('fb-include-crashes').checked
|
|
314
362
|
if (!message) { toast(t('feedback.empty'), 'err'); return }
|
|
315
363
|
if (message.length > 2000) { toast(t('feedback.tooLong'), 'err'); return }
|
|
316
364
|
const btn = $('fb-submit')
|
|
@@ -321,7 +369,7 @@ async function submitFeedback() {
|
|
|
321
369
|
const res = await fetch(base + '/feedback', {
|
|
322
370
|
method: 'POST',
|
|
323
371
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
|
|
324
|
-
body: JSON.stringify({ type, message, contact, appVersion: state.localVersion, includeDiagnostics })
|
|
372
|
+
body: JSON.stringify({ type, message, contact, appVersion: state.localVersion, includeDiagnostics, includeCrashLogs })
|
|
325
373
|
})
|
|
326
374
|
let json = {}
|
|
327
375
|
try { json = await res.json() } catch {}
|
|
@@ -437,160 +485,6 @@ function bucketTokens(b) {
|
|
|
437
485
|
return (b.input || 0) + (b.cacheRead || 0) + (b.cacheWrite || 0) + (b.output || 0)
|
|
438
486
|
}
|
|
439
487
|
|
|
440
|
-
/* ---------------- Token 统计页 ---------------- */
|
|
441
|
-
let statsChartMode = LS.get('statsChartModeV1', 'token') === 'cost' ? 'cost' : 'token'
|
|
442
|
-
|
|
443
|
-
function applyStatsChartMode() {
|
|
444
|
-
document.querySelectorAll('[data-stats-panel]').forEach(panel => {
|
|
445
|
-
const active = panel.dataset.statsPanel === statsChartMode
|
|
446
|
-
panel.classList.toggle('hidden', !active)
|
|
447
|
-
panel.setAttribute('aria-hidden', active ? 'false' : 'true')
|
|
448
|
-
})
|
|
449
|
-
document.querySelectorAll('[data-stats-mode]').forEach(button => {
|
|
450
|
-
const active = button.dataset.statsMode === statsChartMode
|
|
451
|
-
button.classList.toggle('active', active)
|
|
452
|
-
button.setAttribute('aria-pressed', active ? 'true' : 'false')
|
|
453
|
-
})
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function setStatsChartMode(mode) {
|
|
457
|
-
if (mode !== 'token' && mode !== 'cost') return
|
|
458
|
-
statsChartMode = mode
|
|
459
|
-
LS.set('statsChartModeV1', mode)
|
|
460
|
-
applyStatsChartMode()
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
async function loadStats() {
|
|
464
|
-
const cards = $('stats-cards')
|
|
465
|
-
const chart = $('stats-chart')
|
|
466
|
-
const tokenChart = $('stats-token-chart')
|
|
467
|
-
const note = $('stats-note')
|
|
468
|
-
if (!state.token) {
|
|
469
|
-
cards.innerHTML = ''
|
|
470
|
-
chart.innerHTML = `<div class="stats-empty">${t('statsPage.gatewayDown')}</div>`
|
|
471
|
-
tokenChart.innerHTML = `<div class="stats-empty">${t('statsPage.gatewayDown')}</div>`
|
|
472
|
-
note.textContent = ''
|
|
473
|
-
$('stats-sub').textContent = ''
|
|
474
|
-
$('stats-sub').textContent = ''
|
|
475
|
-
$('stats-legend').innerHTML = ''
|
|
476
|
-
$('stats-token-legend').innerHTML = ''
|
|
477
|
-
return
|
|
478
|
-
}
|
|
479
|
-
try {
|
|
480
|
-
const res = await fetch(apiUrl('/stats/summary?days=7'), {
|
|
481
|
-
headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() }
|
|
482
|
-
})
|
|
483
|
-
if (res.status === 401) { authFailure(); return }
|
|
484
|
-
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
485
|
-
const json = await res.json()
|
|
486
|
-
renderStats(json.days || [])
|
|
487
|
-
} catch (e) {
|
|
488
|
-
cards.innerHTML = ''
|
|
489
|
-
chart.innerHTML = `<div class="stats-empty">${t('statsPage.gatewayDown')}</div>`
|
|
490
|
-
tokenChart.innerHTML = `<div class="stats-empty">${t('statsPage.gatewayDown')}</div>`
|
|
491
|
-
note.textContent = ''
|
|
492
|
-
$('stats-sub').textContent = ''
|
|
493
|
-
$('stats-sub').textContent = ''
|
|
494
|
-
$('stats-legend').innerHTML = ''
|
|
495
|
-
$('stats-token-legend').innerHTML = ''
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function renderStats(days) {
|
|
500
|
-
const cards = $('stats-cards')
|
|
501
|
-
const chart = $('stats-chart')
|
|
502
|
-
const tokenChart = $('stats-token-chart')
|
|
503
|
-
const note = $('stats-note')
|
|
504
|
-
if (!days.length) {
|
|
505
|
-
cards.innerHTML = ''
|
|
506
|
-
chart.innerHTML = `<div class="stats-empty">${t('statsPage.empty')}</div>`
|
|
507
|
-
tokenChart.innerHTML = `<div class="stats-empty">${t('statsPage.empty')}</div>`
|
|
508
|
-
note.textContent = t('statsPage.note')
|
|
509
|
-
$('stats-sub').textContent = ''
|
|
510
|
-
$('stats-legend').innerHTML = ''
|
|
511
|
-
$('stats-token-legend').innerHTML = ''
|
|
512
|
-
return
|
|
513
|
-
}
|
|
514
|
-
const today = days[days.length - 1]
|
|
515
|
-
const totalTokens = bucketTokens(today.total)
|
|
516
|
-
const peakCost = today.peak.cost || 0
|
|
517
|
-
const offCost = today.off.cost || 0
|
|
518
|
-
const totalCost = peakCost + offCost
|
|
519
|
-
const peakShare = totalCost > 0 ? Math.round(peakCost / totalCost * 100) : 0
|
|
520
|
-
const todayLabel = fmtStatsDate(today.date)
|
|
521
|
-
const todayFullLabel = fmtStatsDate(today.date, true)
|
|
522
|
-
cards.innerHTML = `
|
|
523
|
-
<article class="scard scard-primary">
|
|
524
|
-
<div class="scard-head">
|
|
525
|
-
<div class="scard-label"><span class="scard-icon token" aria-hidden="true">↗</span><span>${t('statsPage.todayTokens')}</span></div>
|
|
526
|
-
<span class="scard-badge">${esc(t('statsPage.today'))} · ${esc(todayLabel)}</span>
|
|
527
|
-
</div>
|
|
528
|
-
<div class="scard-value">${fmtTokens(totalTokens)}<span class="scard-unit">${esc(t('statsPage.tokenUnit'))}</span></div>
|
|
529
|
-
<div class="scard-helper">${esc(t('statsPage.breakdown'))}</div>
|
|
530
|
-
<div class="bucket-grid">
|
|
531
|
-
<div class="b"><span class="bucket-dot input" aria-hidden="true"></span><span class="n">${t('statsPage.input')}</span><strong class="t">${fmtTokens(today.total.input)}</strong></div>
|
|
532
|
-
<div class="b"><span class="bucket-dot cache-read" aria-hidden="true"></span><span class="n">${t('statsPage.cacheRead')}</span><strong class="t">${fmtTokens(today.total.cacheRead)}</strong></div>
|
|
533
|
-
<div class="b"><span class="bucket-dot cache-write" aria-hidden="true"></span><span class="n">${t('statsPage.cacheWrite')}</span><strong class="t">${fmtTokens(today.total.cacheWrite)}</strong></div>
|
|
534
|
-
<div class="b"><span class="bucket-dot output" aria-hidden="true"></span><span class="n">${t('statsPage.output')}</span><strong class="t">${fmtTokens(today.total.output)}</strong></div>
|
|
535
|
-
</div>
|
|
536
|
-
</article>
|
|
537
|
-
<article class="scard scard-cost">
|
|
538
|
-
<div class="scard-head">
|
|
539
|
-
<div class="scard-label"><span class="scard-icon cost" aria-hidden="true">¥</span><span>${t('statsPage.todayCost')}</span></div>
|
|
540
|
-
<span class="scard-badge">${esc(t('statsPage.estimated'))}</span>
|
|
541
|
-
</div>
|
|
542
|
-
<div class="scard-value">${fmtCost(totalCost)}</div>
|
|
543
|
-
<div class="stats-split">
|
|
544
|
-
<div class="stats-split-item"><span><i class="split-dot peak" aria-hidden="true"></i>${t('statsPage.peak')}</span><strong>${fmtCost(peakCost)}</strong></div>
|
|
545
|
-
<div class="stats-split-item"><span><i class="split-dot off" aria-hidden="true"></i>${t('statsPage.off')}</span><strong>${fmtCost(offCost)}</strong></div>
|
|
546
|
-
</div>
|
|
547
|
-
</article>
|
|
548
|
-
<article class="scard scard-share">
|
|
549
|
-
<div class="scard-head">
|
|
550
|
-
<div class="scard-label"><span class="scard-icon share" aria-hidden="true">%</span><span>${t('statsPage.peakShare')}</span></div>
|
|
551
|
-
</div>
|
|
552
|
-
<div class="share-layout">
|
|
553
|
-
<div class="share-ring" style="--share:${peakShare}%" role="img" aria-label="${esc(t('statsPage.peakShareLabel', { n: peakShare }))}"><span>${peakShare}%</span></div>
|
|
554
|
-
<div class="share-copy"><strong>${t('statsPage.peak')}</strong><span>${t('statsPage.days', { n: days.length })}</span><small>${esc(todayFullLabel)}</small></div>
|
|
555
|
-
</div>
|
|
556
|
-
</article>`
|
|
557
|
-
$('stats-sub').textContent = todayFullLabel
|
|
558
|
-
note.textContent = t('statsPage.note')
|
|
559
|
-
$('stats-legend').innerHTML = `<span class="lg"><span class="sw peak"></span>${t('statsPage.peak')}</span><span class="lg"><span class="sw off"></span>${t('statsPage.off')}</span>`
|
|
560
|
-
$('stats-token-legend').innerHTML = `<span class="lg"><span class="sw token"></span>${t('statsPage.tokenTotal')}</span>`
|
|
561
|
-
chart.setAttribute('aria-label', t('statsPage.costChartLabel'))
|
|
562
|
-
tokenChart.setAttribute('aria-label', t('statsPage.tokenChartLabel'))
|
|
563
|
-
const maxCost = Math.max(...days.map(d => (d.total.cost || 0)), 0.0001)
|
|
564
|
-
chart.innerHTML = days.map(d => {
|
|
565
|
-
const cost = d.total.cost || 0
|
|
566
|
-
// 峰/谷按该日实际费用占比堆叠, 柱总高按当日费用相对窗口最大值; 不再加最小高度, 保证占比真实
|
|
567
|
-
const peakH = cost > 0 ? Math.round((d.peak.cost || 0) / cost * 100) : 0
|
|
568
|
-
const offH = cost > 0 ? Math.max(0, 100 - peakH) : 0
|
|
569
|
-
const totalH = cost > 0 ? Math.max(3, Math.round(cost / maxCost * 100)) : 0
|
|
570
|
-
const label = fmtStatsDate(d.date)
|
|
571
|
-
const fullLabel = `${fmtStatsDate(d.date, true)} · ${t('statsPage.peak')} ${fmtCost(d.peak.cost)} · ${t('statsPage.off')} ${fmtCost(d.off.cost)} · ${t('statsPage.tokenTotal')} ${fmtTokens(bucketTokens(d.total))}`
|
|
572
|
-
return `<div class="stats-bar" role="listitem" title="${esc(fullLabel)}" aria-label="${esc(fullLabel)}">
|
|
573
|
-
<div class="bars" style="height:${totalH}%">
|
|
574
|
-
<div class="seg peak" style="height:${peakH}%"></div>
|
|
575
|
-
<div class="seg off" style="height:${offH}%"></div>
|
|
576
|
-
</div>
|
|
577
|
-
<div class="val">${cost > 0 ? fmtCost(cost) : ''}</div>
|
|
578
|
-
<div class="lbl">${label}</div>
|
|
579
|
-
</div>`
|
|
580
|
-
}).join('')
|
|
581
|
-
const maxTokens = Math.max(...days.map(d => bucketTokens(d.total)), 1)
|
|
582
|
-
tokenChart.innerHTML = days.map(d => {
|
|
583
|
-
const tokens = bucketTokens(d.total)
|
|
584
|
-
const totalH = tokens > 0 ? Math.max(3, Math.round(tokens / maxTokens * 100)) : 0
|
|
585
|
-
const label = fmtStatsDate(d.date)
|
|
586
|
-
const tip = `${fmtStatsDate(d.date, true)} · ${t('statsPage.tokenTotal')} ${fmtTokens(tokens)} · ${t('statsPage.input')} ${fmtTokens(d.total.input)} · ${t('statsPage.output')} ${fmtTokens(d.total.output)}`
|
|
587
|
-
return `<div class="stats-bar" role="listitem" title="${esc(tip)}" aria-label="${esc(tip)}">
|
|
588
|
-
<div class="bars token" style="height:${totalH}%"></div>
|
|
589
|
-
<div class="val">${tokens > 0 ? fmtTokens(tokens) : ''}</div>
|
|
590
|
-
<div class="lbl">${label}</div>
|
|
591
|
-
</div>`
|
|
592
|
-
}).join('')
|
|
593
|
-
}
|
|
594
488
|
async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
595
489
|
const opts = {
|
|
596
490
|
method: 'POST',
|
|
@@ -630,8 +524,10 @@ async function respond(rpcId, value) {
|
|
|
630
524
|
}
|
|
631
525
|
|
|
632
526
|
async function safeRpc(method, payload, errText) {
|
|
527
|
+
const connection = captureConnection()
|
|
633
528
|
try { return await rpc(method, payload) }
|
|
634
529
|
catch (e) {
|
|
530
|
+
if (!connection.valid()) return null
|
|
635
531
|
if (e.message === 'AUTH') authFailure()
|
|
636
532
|
else toast(errText ? `${errText}:${e.message}` : e.message, 'err')
|
|
637
533
|
return null
|
|
@@ -639,6 +535,7 @@ async function safeRpc(method, payload, errText) {
|
|
|
639
535
|
}
|
|
640
536
|
|
|
641
537
|
let hostDescribePromise = null
|
|
538
|
+
let hostDescribeConnection = null
|
|
642
539
|
let hostDescribeRetryTimer = null
|
|
643
540
|
let hostDescribeFailures = 0
|
|
644
541
|
|
|
@@ -654,12 +551,13 @@ function scheduleHostDescribeRetry() {
|
|
|
654
551
|
|
|
655
552
|
async function refreshHostDescription({ notify = false } = {}) {
|
|
656
553
|
if (!state.token) return null
|
|
657
|
-
if (hostDescribePromise) return hostDescribePromise
|
|
658
|
-
const
|
|
659
|
-
|
|
554
|
+
if (hostDescribePromise && hostDescribeConnection?.valid()) return hostDescribePromise
|
|
555
|
+
const connection = captureConnection()
|
|
556
|
+
hostDescribeConnection = connection
|
|
557
|
+
const job = (async () => {
|
|
660
558
|
try {
|
|
661
559
|
const host = await rpc('host.describe', {}, 5000)
|
|
662
|
-
if (
|
|
560
|
+
if (!connection.valid()) return null
|
|
663
561
|
state.hostInfo = host
|
|
664
562
|
const health = activeGatewayHealth()
|
|
665
563
|
if (health) health.upstreamReachable = true
|
|
@@ -671,7 +569,7 @@ async function refreshHostDescription({ notify = false } = {}) {
|
|
|
671
569
|
renderOverview()
|
|
672
570
|
return host
|
|
673
571
|
} catch (error) {
|
|
674
|
-
if (
|
|
572
|
+
if (!connection.valid()) return null
|
|
675
573
|
if (error.message === 'AUTH') authFailure()
|
|
676
574
|
else {
|
|
677
575
|
hostDescribeFailures++
|
|
@@ -680,11 +578,11 @@ async function refreshHostDescription({ notify = false } = {}) {
|
|
|
680
578
|
}
|
|
681
579
|
return null
|
|
682
580
|
} finally {
|
|
683
|
-
hostDescribePromise = null
|
|
684
|
-
if (server !== state.server) scheduleHostDescribeRetry()
|
|
581
|
+
if (hostDescribePromise === job) { hostDescribePromise = null; hostDescribeConnection = null }
|
|
685
582
|
}
|
|
686
583
|
})()
|
|
687
|
-
|
|
584
|
+
hostDescribePromise = job
|
|
585
|
+
return job
|
|
688
586
|
}
|
|
689
587
|
|
|
690
588
|
function authFailure() {
|
|
@@ -1325,12 +1223,12 @@ function openStreams() {
|
|
|
1325
1223
|
function openStream(kind, handler, refreshOnOpen, isRestore, ticket = null) {
|
|
1326
1224
|
if (!state.token) return
|
|
1327
1225
|
if (ticket === null) {
|
|
1328
|
-
const token = state.token
|
|
1226
|
+
const token = state.token, connection=captureConnection()
|
|
1329
1227
|
void getWsTicket().then((value) => {
|
|
1330
|
-
if (
|
|
1228
|
+
if (connection.valid()) openStream(kind, handler, refreshOnOpen, isRestore, value)
|
|
1331
1229
|
}).catch(() => {
|
|
1332
1230
|
// 兼容旧网关/插件副本: ticket 接口不可用时临时回退旧 token 握手。
|
|
1333
|
-
if (
|
|
1231
|
+
if (connection.valid()) openStream(kind, handler, refreshOnOpen, isRestore, '')
|
|
1334
1232
|
})
|
|
1335
1233
|
return
|
|
1336
1234
|
}
|
|
@@ -1535,13 +1433,13 @@ document.addEventListener('visibilitychange', () => {
|
|
|
1535
1433
|
if (document.visibilityState === 'visible') {
|
|
1536
1434
|
onResume()
|
|
1537
1435
|
if (state.servers.length) selectFastestServer({ silent: true })
|
|
1538
|
-
else if (state.token
|
|
1436
|
+
else if (state.token) repairMissingStreams()
|
|
1539
1437
|
}
|
|
1540
1438
|
})
|
|
1541
1439
|
window.addEventListener('pageshow', onResume)
|
|
1542
1440
|
setInterval(() => {
|
|
1543
1441
|
if (document.visibilityState === 'visible' && state.token) {
|
|
1544
|
-
|
|
1442
|
+
repairMissingStreams()
|
|
1545
1443
|
}
|
|
1546
1444
|
}, 15000)
|
|
1547
1445
|
// 多服务器: 每 5 分钟重测一次延迟, 网络环境变化(离开 Wi-Fi / 挂上 Tailscale)时自动换线
|
|
@@ -1743,7 +1641,10 @@ async function refreshAll() {
|
|
|
1743
1641
|
}
|
|
1744
1642
|
|
|
1745
1643
|
async function refreshSessions() {
|
|
1644
|
+
if(typeof restoreCachedSessionList==='function')restoreCachedSessionList()
|
|
1645
|
+
const connection = captureConnection(), request = ++sessionsRequest
|
|
1746
1646
|
const v = await safeRpc('session.list', {}, t('err.sessionList'))
|
|
1647
|
+
if (!connection.valid() || request !== sessionsRequest) return
|
|
1747
1648
|
if (!v) {
|
|
1748
1649
|
// 网关不可达: 用上次成功的会话列表兜底, 用户仍能打开历史缓存
|
|
1749
1650
|
if (!state.sessions.length) {
|
|
@@ -1767,6 +1668,7 @@ async function refreshSessions() {
|
|
|
1767
1668
|
|
|
1768
1669
|
function removeLocalSessionRecord(sessionId) {
|
|
1769
1670
|
if (!sessionId) return
|
|
1671
|
+
void window.DshHistory.cacheRemove(historyCacheScope(),sessionId)
|
|
1770
1672
|
state.sessions = state.sessions.filter(session => session?.sessionId !== sessionId)
|
|
1771
1673
|
state.byId.delete(sessionId)
|
|
1772
1674
|
state.pendingProjections.delete(sessionId)
|
|
@@ -1774,15 +1676,15 @@ function removeLocalSessionRecord(sessionId) {
|
|
|
1774
1676
|
state.pendingPrompts.delete(sessionId)
|
|
1775
1677
|
delete state.queues[sessionId]
|
|
1776
1678
|
delete state.jobs[sessionId]
|
|
1777
|
-
|
|
1778
|
-
if (Object.prototype.hasOwnProperty.call(historyCache, sessionId)) {
|
|
1779
|
-
delete historyCache[sessionId]
|
|
1780
|
-
writeHistoryCache(historyCache)
|
|
1781
|
-
}
|
|
1679
|
+
LS.del(historyCacheScope()) // Also discard the obsolete monolithic cache.
|
|
1782
1680
|
cacheWrite(CACHE.sessions, state.sessions.slice(0, 80))
|
|
1783
1681
|
}
|
|
1784
1682
|
|
|
1785
1683
|
function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
|
|
1684
|
+
function openInsights(tab = 'context') {
|
|
1685
|
+
window.DshInsights?.open({ tab, connection: captureConnection(), url: apiUrl,
|
|
1686
|
+
getSession: () => state.byId.get(state.current), getTitle: () => sessionTitleValue(state.byId.get(state.current)) })
|
|
1687
|
+
}
|
|
1786
1688
|
function hydrateSessionProjections(sessionId, projections) {
|
|
1787
1689
|
const s = state.byId.get(sessionId)
|
|
1788
1690
|
if (!s || !projections || typeof projections !== 'object') return
|
|
@@ -1858,7 +1760,11 @@ function short(id) { return '…' + String(id).slice(-8) }
|
|
|
1858
1760
|
function isTopLevelSession(session) {
|
|
1859
1761
|
return !!session && !session.parentSessionId && session.origin !== 'subagent'
|
|
1860
1762
|
}
|
|
1861
|
-
|
|
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) }
|
|
1862
1768
|
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
1863
1769
|
function isGoalTerminal(goal) {
|
|
1864
1770
|
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
@@ -2046,21 +1952,28 @@ function renderWorkspaceNavigation() {
|
|
|
2046
1952
|
return items
|
|
2047
1953
|
}
|
|
2048
1954
|
async function refreshWorkbench() {
|
|
1955
|
+
const connection = captureConnection(), request = ++workbenchRequest
|
|
1956
|
+
const valid = () => connection.valid() && request === workbenchRequest
|
|
2049
1957
|
if (!state.token) return
|
|
2050
1958
|
try {
|
|
2051
1959
|
const res = await fetch(apiUrl('/workbench'), {
|
|
2052
1960
|
headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() }
|
|
2053
1961
|
})
|
|
1962
|
+
if (!valid()) return
|
|
2054
1963
|
if (res.ok) {
|
|
2055
1964
|
const value = await res.json().catch(() => null)
|
|
1965
|
+
if (!valid()) return
|
|
2056
1966
|
if (value && typeof value.bound === 'boolean') state.wb = value
|
|
2057
1967
|
}
|
|
2058
1968
|
} catch {}
|
|
1969
|
+
if (!valid()) return
|
|
2059
1970
|
try {
|
|
2060
1971
|
const value = await rpc('workspace.list', {})
|
|
1972
|
+
if (!valid()) return
|
|
2061
1973
|
state.wbProjects = Array.isArray(value?.items) ? value.items : []
|
|
2062
1974
|
state.wbArchived = Array.isArray(value?.archivedSessionIds) ? value.archivedSessionIds : []
|
|
2063
1975
|
} catch {
|
|
1976
|
+
if (!valid()) return
|
|
2064
1977
|
state.wbProjects = []
|
|
2065
1978
|
state.wbArchived = []
|
|
2066
1979
|
}
|
|
@@ -2068,8 +1981,10 @@ async function refreshWorkbench() {
|
|
|
2068
1981
|
if (state.wb?.bound && state.wb.path) {
|
|
2069
1982
|
try {
|
|
2070
1983
|
const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
1984
|
+
if (!valid()) return
|
|
2071
1985
|
if (listRes.ok) {
|
|
2072
1986
|
const listData = await listRes.json().catch(() => ({}))
|
|
1987
|
+
if (!valid()) return
|
|
2073
1988
|
if (Array.isArray(listData.entries)) {
|
|
2074
1989
|
const diskDirs = new Set(listData.entries.filter(e => e.type === 'dir').map(e => wbPathKey(wbJoin(state.wb.path, e.name))))
|
|
2075
1990
|
// 工作台只管理自己根目录下的项目;DSH 中位于其他路径的工作区必须保留,
|
|
@@ -2080,14 +1995,18 @@ async function refreshWorkbench() {
|
|
|
2080
1995
|
if (entry.type !== 'dir') continue
|
|
2081
1996
|
const projectPath = wbJoin(state.wb.path, entry.name)
|
|
2082
1997
|
if (have.has(wbPathKey(projectPath))) continue
|
|
1998
|
+
if (!valid()) return
|
|
2083
1999
|
try {
|
|
2084
2000
|
const created = await rpc('workspace.create', { path: projectPath })
|
|
2001
|
+
if (!valid()) return
|
|
2085
2002
|
if (created?.workspace) { state.wbProjects.push(created.workspace); have.add(wbPathKey(projectPath)) }
|
|
2086
2003
|
} catch {}
|
|
2004
|
+
if (!valid()) return
|
|
2087
2005
|
}
|
|
2088
2006
|
}
|
|
2089
2007
|
}
|
|
2090
2008
|
} catch {}
|
|
2009
|
+
if (!valid()) return
|
|
2091
2010
|
}
|
|
2092
2011
|
renderWorkspaceNavigation()
|
|
2093
2012
|
renderWorkbench()
|
|
@@ -2121,7 +2040,7 @@ function renderWorkbench() {
|
|
|
2121
2040
|
const projectHtml = projects.map(w => {
|
|
2122
2041
|
const id = String(w.workspaceId || '')
|
|
2123
2042
|
const open = !!state.wbOpenProjects[id]
|
|
2124
|
-
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)))
|
|
2125
2044
|
const body = open ? `<div class="wb-sessions">${sessions.length ? sessions.map(s => `
|
|
2126
2045
|
<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
2127
2046
|
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}">
|
|
@@ -2292,9 +2211,9 @@ function renderSessions() {
|
|
|
2292
2211
|
|
|
2293
2212
|
/* ---------------- 会话详情 ---------------- */
|
|
2294
2213
|
const emptySessionCleanup = new Set()
|
|
2295
|
-
async function archiveEmptySessionOnLeave(sessionId) {
|
|
2296
|
-
const base = state.server
|
|
2297
|
-
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)
|
|
2298
2217
|
|| !state.byId.has(sessionId) || state.byId.get(sessionId)?.running
|
|
2299
2218
|
|| state.sessionActivity?.has(sessionId) || state.pendingPrompts?.has(sessionId)
|
|
2300
2219
|
|| (state.queues[sessionId] || []).length > 0
|
|
@@ -2315,10 +2234,11 @@ async function archiveEmptySessionOnLeave(sessionId) {
|
|
|
2315
2234
|
}
|
|
2316
2235
|
|
|
2317
2236
|
async function openSession(id) {
|
|
2318
|
-
if (state.current && state.current !== id)
|
|
2237
|
+
if (state.current && state.current !== id) void archiveEmptySessionOnLeave(state.current,true)
|
|
2319
2238
|
state.current = id
|
|
2320
2239
|
setSessionRecovery('loading')
|
|
2321
2240
|
state.history = emptyHistory()
|
|
2241
|
+
const openingHistory=state.history, openingConnection=captureConnection()
|
|
2322
2242
|
document.body.classList.add('in-session')
|
|
2323
2243
|
showView('view-session')
|
|
2324
2244
|
$('btn-rename-session').classList.remove('hidden')
|
|
@@ -2329,7 +2249,9 @@ async function openSession(id) {
|
|
|
2329
2249
|
renderQueue()
|
|
2330
2250
|
renderSessionPending()
|
|
2331
2251
|
void refreshCompactionStatus(id)
|
|
2332
|
-
|
|
2252
|
+
getConversationView().reset()
|
|
2253
|
+
await restoreCachedHistory()
|
|
2254
|
+
if(state.current!==id||state.history!==openingHistory||!openingConnection.valid())return
|
|
2333
2255
|
await loadHistory(true)
|
|
2334
2256
|
renderSessionCards()
|
|
2335
2257
|
refreshSessions()
|
|
@@ -2408,13 +2330,10 @@ function renderSessionTitle() {
|
|
|
2408
2330
|
function renderSessionSub() {
|
|
2409
2331
|
const s = state.byId.get(state.current)
|
|
2410
2332
|
if (!s) { $('session-sub').textContent = ''; return }
|
|
2411
|
-
const
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
const recovery = recoveryLabel()
|
|
2416
|
-
if (recovery) parts.push(recovery)
|
|
2417
|
-
$('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')
|
|
2418
2337
|
}
|
|
2419
2338
|
|
|
2420
2339
|
/** 顶栏状态: 运行中=蓝色流动渐变, 中断/出错=橙红渐变, 空闲=原样式 */
|
|
@@ -2426,7 +2345,8 @@ function updateSessionStatus() {
|
|
|
2426
2345
|
const composerStatus = $('composer-status')
|
|
2427
2346
|
const compact = activeCompaction()
|
|
2428
2347
|
if (composerStatus) {
|
|
2429
|
-
|
|
2348
|
+
const queued = (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
2349
|
+
composerStatus.classList.toggle('hidden', !s?.running && !compact && !queued)
|
|
2430
2350
|
composerStatus.classList.toggle('compacting', !!compact)
|
|
2431
2351
|
}
|
|
2432
2352
|
const composerText = $('composer-status-text')
|
|
@@ -2445,6 +2365,7 @@ function updateCancelBtn() {
|
|
|
2445
2365
|
const s = state.byId.get(state.current)
|
|
2446
2366
|
const running = s?.running || (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
2447
2367
|
$('btn-cancel').classList.toggle('hidden', !running)
|
|
2368
|
+
$('composer-status')?.classList.toggle('hidden', !running && !activeCompaction())
|
|
2448
2369
|
}
|
|
2449
2370
|
|
|
2450
2371
|
const HISTORY_MAX_VISIBLE = 5000 // 已加载的可显示事件上限(消息/工具/状态, 不含 chunk)
|
|
@@ -2539,135 +2460,102 @@ function partialReasoningHtml() {
|
|
|
2539
2460
|
.join('')
|
|
2540
2461
|
}
|
|
2541
2462
|
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
if
|
|
2545
|
-
const
|
|
2546
|
-
for (const e of drop) h.seqs.delete(e.seq)
|
|
2547
|
-
h.renderStart = Math.max(0, h.renderStart - drop.length)
|
|
2548
|
-
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{}
|
|
2549
2467
|
}
|
|
2550
2468
|
|
|
2551
|
-
|
|
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
|
+
}
|
|
2552
2485
|
function scheduleHistoryCacheSave() {
|
|
2553
2486
|
clearTimeout(scheduleHistoryCacheSave._t)
|
|
2554
|
-
|
|
2555
|
-
}
|
|
2556
|
-
|
|
2557
|
-
function saveHistoryCache() {
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
for (const k of keys) pruned[k] = cache[k]
|
|
2573
|
-
writeHistoryCache(pruned)
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
/** 网关不可达时回填本地缓存的历史; 返回是否命中。 */
|
|
2577
|
-
function restoreCachedHistory() {
|
|
2578
|
-
const id = state.current
|
|
2579
|
-
if (!id) return false
|
|
2580
|
-
const cached = readHistoryCache()[id]
|
|
2581
|
-
if (!cached?.events?.length) return false
|
|
2582
|
-
if (cached.title) hydrateSessionProjections(id, { values: { title: cached.title }, asOfSeq: 0 })
|
|
2583
|
-
const h = emptyHistory()
|
|
2584
|
-
for (const e of cached.events) {
|
|
2585
|
-
if (e?.seq == null) continue
|
|
2586
|
-
h.seqs.add(e.seq)
|
|
2587
|
-
h.visible.push(e)
|
|
2588
|
-
}
|
|
2589
|
-
h.visible.sort((a, b) => a.seq - b.seq)
|
|
2590
|
-
h.loaded = true
|
|
2591
|
-
state.history = h
|
|
2592
|
-
$('history-hint').textContent = t('history.offlineCache', { n: h.visible.length })
|
|
2593
|
-
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)
|
|
2594
2505
|
return true
|
|
2595
2506
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
const
|
|
2601
|
-
const
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
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()
|
|
2609
2527
|
let v
|
|
2610
|
-
try {
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
if
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
toast(t('history.cacheFallback'), 'ok')
|
|
2619
|
-
return
|
|
2620
|
-
}
|
|
2621
|
-
const msg = e.message || t('err.dshError')
|
|
2622
|
-
setSessionRecovery('error', msg)
|
|
2623
|
-
const box = $('history')
|
|
2624
|
-
if (box && (reset || !state.history.visible.length)) {
|
|
2625
|
-
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>`
|
|
2626
|
-
const retry = $('btn-history-retry')
|
|
2627
|
-
if (retry) retry.addEventListener('click', () => loadHistory(true))
|
|
2628
|
-
} else {
|
|
2629
|
-
toast(t('history.loadFailed', { msg }), 'err')
|
|
2630
|
-
}
|
|
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)
|
|
2631
2536
|
return
|
|
2632
2537
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
history.loaded = true
|
|
2638
|
-
const incoming = v.events || []
|
|
2639
|
-
let added = 0
|
|
2640
|
-
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=[]
|
|
2641
2542
|
for (const entry of incoming) {
|
|
2642
|
-
const ev
|
|
2643
|
-
const seq = ev?.seq
|
|
2644
|
-
if (ev?.type === 'turn/start' || ev?.type === 'turn/end') noteSessionTurnTime(id, ev)
|
|
2543
|
+
const ev=entry?.event
|
|
2645
2544
|
applyReasoningStreamEvent(ev)
|
|
2646
|
-
if
|
|
2647
|
-
if
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
else applyReasoningBaseline(v.partialReasoning)
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
history.loading = false
|
|
2661
|
-
setSessionRecovery('ready')
|
|
2662
|
-
renderSessionTitle(); renderSessionSub(); renderSessionCards()
|
|
2663
|
-
try {
|
|
2664
|
-
if (reset) renderHistory(true)
|
|
2665
|
-
else if (added) renderHistory(false, 'keep')
|
|
2666
|
-
} catch (e) {
|
|
2667
|
-
console.error('renderHistory failed', e)
|
|
2668
|
-
}
|
|
2669
|
-
if (moreBtn) moreBtn.classList.toggle('hidden', !state.history.hasMore)
|
|
2670
|
-
$('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})
|
|
2671
2559
|
scheduleHistoryCacheSave()
|
|
2672
2560
|
}
|
|
2673
2561
|
|
|
@@ -2687,18 +2575,7 @@ function insertLiveEvent(event) {
|
|
|
2687
2575
|
h.visible.push({ seq, event })
|
|
2688
2576
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
2689
2577
|
trimVisible()
|
|
2690
|
-
|
|
2691
|
-
const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 240
|
|
2692
|
-
if (nearBottom) {
|
|
2693
|
-
h.renderEnd = h.visible.length
|
|
2694
|
-
h.renderStart = Math.max(0, h.renderEnd - 200)
|
|
2695
|
-
renderHistory(false, 'bottom')
|
|
2696
|
-
} else {
|
|
2697
|
-
// 图片异步撑高历史区后,用户可能瞬间不再满足 nearBottom。新回复仍须
|
|
2698
|
-
// 扩展可见窗口,但保持当前阅读位置,不能等到重新进入会话才出现。
|
|
2699
|
-
h.renderEnd = h.visible.length
|
|
2700
|
-
renderHistory(false, 'fixed')
|
|
2701
|
-
}
|
|
2578
|
+
renderHistory(false, 'fixed')
|
|
2702
2579
|
scheduleHistoryCacheSave()
|
|
2703
2580
|
}
|
|
2704
2581
|
|
|
@@ -2711,74 +2588,33 @@ function filteredEntries() {
|
|
|
2711
2588
|
return f
|
|
2712
2589
|
}
|
|
2713
2590
|
|
|
2714
|
-
function renderHistory(reset,
|
|
2715
|
-
const
|
|
2716
|
-
const
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
if (!len && !reasoningHtml) {
|
|
2721
|
-
box.innerHTML = '<div class="empty">' + t('history.empty') + '</div>'
|
|
2722
|
-
h.renderStart = 0; h.renderEnd = 0
|
|
2723
|
-
updateRail()
|
|
2724
|
-
return
|
|
2725
|
-
}
|
|
2726
|
-
if (reset) {
|
|
2727
|
-
h.renderEnd = len
|
|
2728
|
-
h.renderStart = Math.max(0, len - 200)
|
|
2729
|
-
}
|
|
2730
|
-
const start = Math.min(h.renderStart, len)
|
|
2731
|
-
const end = Math.min(h.renderEnd, len) || len
|
|
2732
|
-
const oldH = box.scrollHeight
|
|
2733
|
-
const oldTop = box.scrollTop
|
|
2734
|
-
// callId → 工具名, 供 tool/result 折叠标题显示
|
|
2735
|
-
const toolNames = new Map()
|
|
2736
|
-
for (const e of state.history.visible) {
|
|
2737
|
-
if (e.event?.type !== 'tool/call') continue
|
|
2738
|
-
const d = e.event.data || {}
|
|
2739
|
-
if (d.callId && d.name) toolNames.set(d.callId, d.name)
|
|
2740
|
-
}
|
|
2741
|
-
box.innerHTML = filtered.slice(start, end).map(e => eventHtml(e, { toolNames })).join('') + reasoningHtml
|
|
2742
|
-
if (reset || mode === 'bottom') box.scrollTop = box.scrollHeight
|
|
2743
|
-
else if (mode === 'keep') box.scrollTop = Math.max(0, oldTop + (box.scrollHeight - oldH))
|
|
2744
|
-
else if (mode === 'fixed') box.scrollTop = oldTop
|
|
2745
|
-
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')})
|
|
2746
2597
|
}
|
|
2747
2598
|
|
|
2748
2599
|
/* 右侧导航条: 用户发言节点 + 拖动快速定位 */
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
const thumb
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
thumb.style.top = (4 + ratio * maxTop) + 'px'
|
|
2768
|
-
|
|
2769
|
-
const boxTop = box.getBoundingClientRect().top
|
|
2770
|
-
const userNodes = [...box.querySelectorAll('.msg.user')]
|
|
2771
|
-
nodesBox.innerHTML = userNodes.map(el => {
|
|
2772
|
-
const off = el.getBoundingClientRect().top - boxTop + box.scrollTop
|
|
2773
|
-
const pos = Math.min(4 + trackH, 4 + off / Math.max(1, sh) * trackH)
|
|
2774
|
-
return `<div class="rail-node" data-offset="${Math.round(off)}" style="top:${pos}px"></div>`
|
|
2775
|
-
}).join('')
|
|
2776
|
-
let activeIdx = -1
|
|
2777
|
-
userNodes.forEach((el, i) => {
|
|
2778
|
-
const off = el.getBoundingClientRect().top - boxTop + box.scrollTop
|
|
2779
|
-
if (off <= box.scrollTop + 60) activeIdx = i
|
|
2780
|
-
})
|
|
2781
|
-
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)
|
|
2782
2618
|
}
|
|
2783
2619
|
|
|
2784
2620
|
function bindRail() {
|
|
@@ -2789,6 +2625,8 @@ function bindRail() {
|
|
|
2789
2625
|
nodesBox.addEventListener('click', (e) => {
|
|
2790
2626
|
const node = e.target.closest('.rail-node')
|
|
2791
2627
|
if (!node) return
|
|
2628
|
+
getConversationView().intentUntil=Date.now()+1500
|
|
2629
|
+
getConversationView().reading=true
|
|
2792
2630
|
box.scrollTo({ top: Math.max(0, Number(node.dataset.offset) - 10), behavior: 'smooth' })
|
|
2793
2631
|
})
|
|
2794
2632
|
let drag = null
|
|
@@ -2798,6 +2636,8 @@ function bindRail() {
|
|
|
2798
2636
|
})
|
|
2799
2637
|
thumb.addEventListener('pointermove', (e) => {
|
|
2800
2638
|
if (!drag) return
|
|
2639
|
+
getConversationView().intentUntil=Date.now()+1500
|
|
2640
|
+
getConversationView().reading=true
|
|
2801
2641
|
const trackH = Math.max(1, box.clientHeight - 8)
|
|
2802
2642
|
const delta = (e.clientY - drag.y) / trackH * Math.max(1, box.scrollHeight - box.clientHeight)
|
|
2803
2643
|
box.scrollTop = Math.max(0, Math.min(box.scrollHeight - box.clientHeight, drag.top + delta))
|
|
@@ -2816,8 +2656,7 @@ const INTERESTING_EVENTS = new Set([
|
|
|
2816
2656
|
'goal/created', 'goal/updated', 'goal/completed', 'goal/cleared',
|
|
2817
2657
|
'todo/updated', 'plan/updated',
|
|
2818
2658
|
'question/asked', 'question/resolved',
|
|
2819
|
-
'approval/asked', 'approval/resolved'
|
|
2820
|
-
'session/title', 'title'
|
|
2659
|
+
'approval/asked', 'approval/resolved'
|
|
2821
2660
|
])
|
|
2822
2661
|
function messageSource(data) {
|
|
2823
2662
|
const source = data?.source ?? data?.message?.source
|
|
@@ -2934,6 +2773,7 @@ function statsHtml(s) {
|
|
|
2934
2773
|
|
|
2935
2774
|
let sessionCardsRenderGeneration = 0
|
|
2936
2775
|
async function renderSessionCards() {
|
|
2776
|
+
window.DshInsights?.update()
|
|
2937
2777
|
const renderGeneration = ++sessionCardsRenderGeneration
|
|
2938
2778
|
const sessionId = state.current
|
|
2939
2779
|
const s = state.byId.get(sessionId)
|
|
@@ -3741,36 +3581,88 @@ function bindSessionSwipe() {
|
|
|
3741
3581
|
}
|
|
3742
3582
|
|
|
3743
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
|
+
|
|
3744
3636
|
function renderOverview() {
|
|
3745
3637
|
const ring = $('overview-pulse-ring')
|
|
3746
3638
|
if (!ring) return
|
|
3747
|
-
const checks =
|
|
3748
|
-
|
|
3749
|
-
// 已连接网关误报为离线。Capacitor 等非 HTTP 页面仍要求显式服务器。
|
|
3750
|
-
gateway: !!state.token && (!!state.server || /^https?:$/.test(location.protocol)),
|
|
3751
|
-
dsh: dshReachable(),
|
|
3752
|
-
mux: !!state.streamsOk?.mux,
|
|
3753
|
-
host: !!state.streamsOk?.host
|
|
3754
|
-
}
|
|
3639
|
+
const checks = overviewChecks()
|
|
3640
|
+
const detection=overviewDetector?.model,checking=detection?.phase==='checking'||(detection?.phase==='degraded'&&detection?.probing===true)
|
|
3755
3641
|
const online = Object.values(checks).filter(Boolean).length
|
|
3642
|
+
$('overview-refresh').disabled=detection?.probing===true
|
|
3756
3643
|
const status = online === 4 ? 'nominal' : online > 0 ? 'degraded' : 'offline'
|
|
3757
3644
|
const pulseCard = document.querySelector('.overview-pulse-card')
|
|
3758
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)
|
|
3759
3649
|
pulseCard.classList.remove('status-nominal', 'status-degraded', 'status-offline')
|
|
3650
|
+
pulseCard.classList.toggle('status-checking',checking)
|
|
3651
|
+
pulseCard.setAttribute('aria-busy',String(checking))
|
|
3760
3652
|
pulseCard.classList.add('status-' + status)
|
|
3761
3653
|
}
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
$('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}))
|
|
3767
3658
|
for (const [name, ok] of Object.entries(checks)) {
|
|
3768
3659
|
const item = document.querySelector(`[data-overview-link="${name}"]`)
|
|
3769
3660
|
if (!item) continue
|
|
3770
3661
|
item.classList.toggle('ok', ok)
|
|
3771
|
-
item.classList.toggle('off', !ok)
|
|
3662
|
+
item.classList.toggle('off', !ok&&!checking)
|
|
3663
|
+
item.classList.toggle('pending',!ok&&checking)
|
|
3772
3664
|
const value = item.querySelector('b')
|
|
3773
|
-
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'))
|
|
3774
3666
|
}
|
|
3775
3667
|
|
|
3776
3668
|
const pending = [
|
|
@@ -4134,8 +4026,10 @@ function openWorkspaceFiles(workspaceId) {
|
|
|
4134
4026
|
}
|
|
4135
4027
|
|
|
4136
4028
|
async function openWorkspaceModal() {
|
|
4029
|
+
const connection = captureConnection()
|
|
4137
4030
|
if (!state.token) { toast(t('fs.noTokenToast'), 'err'); showView('view-settings'); return }
|
|
4138
4031
|
if (!state.fs.path) await loadFs(null, { silent: true })
|
|
4032
|
+
if (!connection.valid()) return
|
|
4139
4033
|
$('workspace-parent-path').textContent = state.fs.path || '~'
|
|
4140
4034
|
$('workspace-name').value = ''
|
|
4141
4035
|
$('modal-workspace').classList.remove('hidden')
|
|
@@ -4146,27 +4040,34 @@ async function createWorkspace() {
|
|
|
4146
4040
|
if (createWorkspace.busy) return
|
|
4147
4041
|
const name = $('workspace-name').value.trim()
|
|
4148
4042
|
if (!name) { toast(t('workspace.nameRequired'), 'err'); $('workspace-name').focus(); return }
|
|
4043
|
+
const connection = captureConnection()
|
|
4149
4044
|
createWorkspace.busy = true
|
|
4150
4045
|
const parent = state.fs.path || ''
|
|
4151
4046
|
const button = $('workspace-create')
|
|
4152
4047
|
button.disabled = true
|
|
4153
4048
|
try {
|
|
4154
4049
|
const res = await fetch(fsApiUrl('/mkdir', { path: parent, name }), { method: 'POST', headers: fsHeaders() })
|
|
4050
|
+
if (!connection.valid()) return
|
|
4155
4051
|
if (res.status === 401) { fsAuthError(401); return }
|
|
4156
4052
|
const data = await res.json().catch(() => ({}))
|
|
4053
|
+
if (!connection.valid()) return
|
|
4157
4054
|
if (!res.ok) {
|
|
4158
4055
|
const msg = data.error === 'exists' ? t('workspace.exists') : data.error === 'bad-name' ? t('workspace.invalidName') : data.error || ('HTTP ' + res.status)
|
|
4159
4056
|
throw new Error(msg)
|
|
4160
4057
|
}
|
|
4161
4058
|
closeWorkspaceModal()
|
|
4162
4059
|
await loadFs(parent || null, { silent: true })
|
|
4060
|
+
if (!connection.valid()) return
|
|
4163
4061
|
let workspace = null
|
|
4164
4062
|
try {
|
|
4165
4063
|
const created = await rpc('workspace.create', { path: data.path })
|
|
4064
|
+
if (!connection.valid()) return
|
|
4166
4065
|
workspace = created?.workspace || null
|
|
4167
4066
|
} catch {}
|
|
4067
|
+
if (!connection.valid()) return
|
|
4168
4068
|
const sessionPayload = workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: data.path }
|
|
4169
4069
|
const v = await safeRpc('session.create', sessionPayload, t('home.createFailed'))
|
|
4070
|
+
if (!connection.valid()) return
|
|
4170
4071
|
if (workspace?.workspaceId) {
|
|
4171
4072
|
state.workspaceFilter = workspace.workspaceId
|
|
4172
4073
|
state.fs.workspaceId = workspace.workspaceId
|
|
@@ -4174,6 +4075,7 @@ async function createWorkspace() {
|
|
|
4174
4075
|
LS.set('fsWorkspaceIdV1', workspace.workspaceId)
|
|
4175
4076
|
}
|
|
4176
4077
|
await refreshSessions()
|
|
4078
|
+
if (!connection.valid()) return
|
|
4177
4079
|
if (v?.sessionId) {
|
|
4178
4080
|
toast(t('workspace.created'), 'ok')
|
|
4179
4081
|
openSession(v.sessionId)
|
|
@@ -4181,6 +4083,7 @@ async function createWorkspace() {
|
|
|
4181
4083
|
toast(t('workspace.createdNoSession'), 'ok')
|
|
4182
4084
|
}
|
|
4183
4085
|
} catch (e) {
|
|
4086
|
+
if (!connection.valid()) return
|
|
4184
4087
|
if (e.message === 'AUTH') fsAuthError(401)
|
|
4185
4088
|
else toast(t('workspace.createFailed', { msg: e.message || t('fs.networkError') }), 'err')
|
|
4186
4089
|
} finally {
|
|
@@ -4202,6 +4105,8 @@ function fsAuthError(status) {
|
|
|
4202
4105
|
}
|
|
4203
4106
|
|
|
4204
4107
|
async function loadFs(dir, { silent = false, resetRoot = false } = {}) {
|
|
4108
|
+
const connection = captureConnection(), request = ++filesRequest
|
|
4109
|
+
const valid = () => connection.valid() && request === filesRequest
|
|
4205
4110
|
if (!state.token) {
|
|
4206
4111
|
$('fs-path').textContent = t('fs.noToken')
|
|
4207
4112
|
$('fs-list').innerHTML = '<div class="empty">' + t('fs.goSettings') + '</div>'
|
|
@@ -4215,8 +4120,10 @@ async function loadFs(dir, { silent = false, resetRoot = false } = {}) {
|
|
|
4215
4120
|
}
|
|
4216
4121
|
try {
|
|
4217
4122
|
const res = await fetch(fsApiUrl('/list', target ? { path: target } : {}), { headers: fsHeaders() })
|
|
4123
|
+
if (!valid()) return
|
|
4218
4124
|
if (res.status === 401) { fsAuthError(401); return }
|
|
4219
4125
|
const data = await res.json().catch(() => ({}))
|
|
4126
|
+
if (!valid()) return
|
|
4220
4127
|
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error === 'not-found' ? t('fs.notFound') : data.error === 'forbidden' ? t('fs.forbidden') : data.error || ('HTTP ' + res.status))
|
|
4221
4128
|
state.fs.path = data.path
|
|
4222
4129
|
if (!state.fs.initial) state.fs.initial = data.path
|
|
@@ -4230,6 +4137,7 @@ async function loadFs(dir, { silent = false, resetRoot = false } = {}) {
|
|
|
4230
4137
|
renderWorkspaceNavigation()
|
|
4231
4138
|
renderFs(data)
|
|
4232
4139
|
} catch (e) {
|
|
4140
|
+
if (!valid()) return
|
|
4233
4141
|
if (e.message === 'AUTH') return
|
|
4234
4142
|
$('fs-path').textContent = target || '~'
|
|
4235
4143
|
$('fs-list').innerHTML = `<div class="empty">${esc(t('fs.loadFailed', { msg: e.message || t('fs.networkError') }))}</div>`
|
|
@@ -5343,11 +5251,9 @@ async function verifyUpdateApk(info, url) {
|
|
|
5343
5251
|
phase = 'verifying'
|
|
5344
5252
|
updateDownloadProgress({ phase, received, total: total || received })
|
|
5345
5253
|
const expected = String(info.sha256 || '').trim().toLowerCase()
|
|
5346
|
-
if (expected) {
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
if (actual !== expected) return { ok: false, corrupted: true }
|
|
5350
|
-
}
|
|
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 }
|
|
5351
5257
|
return { ok: true, blob }
|
|
5352
5258
|
} catch (err) { return { ok: false, network: true, msg: err?.message || '' } }
|
|
5353
5259
|
finally { clearTimeout(timer); clearInterval(progressTimer) }
|
|
@@ -5357,13 +5263,19 @@ async function downloadUpdate() {
|
|
|
5357
5263
|
const info = state.updateInfo
|
|
5358
5264
|
if (!info || updateDownloadBusy) return
|
|
5359
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
|
+
}
|
|
5360
5272
|
updateDownloadBusy = true
|
|
5361
5273
|
updateDownloadSample = null
|
|
5362
5274
|
$('btn-download-update').disabled = true
|
|
5363
5275
|
updateDownloadProgress({ phase: 'downloading', received: 0, total: 0 })
|
|
5364
5276
|
if (CAP?.isNativePlatform?.() && window.NativeUpdate?.downloadVerifiedAndInstall && window.NativeUpdate?.getDownloadStatus) {
|
|
5365
5277
|
try {
|
|
5366
|
-
if (!window.NativeUpdate.downloadVerifiedAndInstall(url,
|
|
5278
|
+
if (!window.NativeUpdate.downloadVerifiedAndInstall(url, sha256)) throw new Error(t('update.busy'))
|
|
5367
5279
|
const poll = () => {
|
|
5368
5280
|
try {
|
|
5369
5281
|
const value = JSON.parse(window.NativeUpdate.getDownloadStatus())
|
|
@@ -5379,11 +5291,8 @@ async function downloadUpdate() {
|
|
|
5379
5291
|
try {
|
|
5380
5292
|
const result = await verifyUpdateApk(info, url)
|
|
5381
5293
|
if (!result.ok) throw new Error(result.corrupted ? t('update.corrupted') : result.status ? t('update.serverFileMissing') : result.msg || t('fs.networkError'))
|
|
5382
|
-
|
|
5383
|
-
//
|
|
5384
|
-
window.NativeUpdate.downloadAndInstall(url)
|
|
5385
|
-
updateDownloadProgress({ phase: 'legacy', received: 0, total: 0 })
|
|
5386
|
-
} else {
|
|
5294
|
+
{
|
|
5295
|
+
// 无校验的原生安装入口已移除: 新旧壳统一走下方"下载并本地校验"路径。
|
|
5387
5296
|
const objectUrl = URL.createObjectURL(result.blob)
|
|
5388
5297
|
const link = document.createElement('a')
|
|
5389
5298
|
link.href = objectUrl
|
|
@@ -6158,9 +6067,11 @@ async function openModelConfigDocument() {
|
|
|
6158
6067
|
/* ---------------- 视图切换 ---------------- */
|
|
6159
6068
|
function showView(id) {
|
|
6160
6069
|
if (id !== 'view-session' && document.body.classList.contains('in-session') && state.current) void archiveEmptySessionOnLeave(state.current)
|
|
6161
|
-
for (const v of ['view-home', 'view-files', 'view-session', 'view-activity', 'view-
|
|
6070
|
+
for (const v of ['view-home', 'view-files', 'view-session', 'view-activity', 'view-plugins', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
6162
6071
|
// 离开会话页必须清掉 in-session, 否则其他页面顶栏被 body 样式隐藏
|
|
6163
6072
|
document.body.classList.toggle('in-session', id === 'view-session')
|
|
6073
|
+
$('session-actions')?.close()
|
|
6074
|
+
$('session-info')?.close()
|
|
6164
6075
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
6165
6076
|
// 主页已有上下文明确的刷新按钮,避免顶栏出现第二个同义入口。
|
|
6166
6077
|
$('btn-refresh')?.classList.toggle('hidden', id === 'view-activity')
|
|
@@ -6170,8 +6081,10 @@ function showView(id) {
|
|
|
6170
6081
|
const workspace = workspaceById(state.fs.workspaceId)
|
|
6171
6082
|
loadFs(workspace?.path || null, { silent: true, resetRoot: true })
|
|
6172
6083
|
}
|
|
6173
|
-
if (id === 'view-
|
|
6084
|
+
if (id === 'view-plugins') openPluginPage()
|
|
6085
|
+
else window.DshPluginCenter?.close()
|
|
6174
6086
|
if (id === 'view-settings') showSettingsHome()
|
|
6087
|
+
if(id==='view-activity')startOverviewDetection(true);else {overviewDetector?.pause();overviewPulse?.pause()}
|
|
6175
6088
|
}
|
|
6176
6089
|
|
|
6177
6090
|
const SETTINGS_GROUPS = ['general', 'model', 'tests', 'servers', 'notify', 'theme', 'about']
|
|
@@ -6192,6 +6105,7 @@ function showSettingsPage(name) {
|
|
|
6192
6105
|
}
|
|
6193
6106
|
|
|
6194
6107
|
function updateConn() {
|
|
6108
|
+
overviewDetector?.observe()
|
|
6195
6109
|
const el = $('conn-badge')
|
|
6196
6110
|
// mux/host 的打开顺序不稳定;连接状态改变时同步重绘总览,避免后打开的
|
|
6197
6111
|
// 通道只更新顶栏、总览却永久停留在 3/4。
|
|
@@ -6262,7 +6176,8 @@ function updateComposerFullscreenButton() {
|
|
|
6262
6176
|
function setComposerFullscreen(on) {
|
|
6263
6177
|
const wrap = $('composer-wrap')
|
|
6264
6178
|
if (!wrap) return
|
|
6265
|
-
$('btn-
|
|
6179
|
+
$('btn-session-more')?.classList.toggle('hidden', !!on)
|
|
6180
|
+
$('session-actions')?.close()
|
|
6266
6181
|
$('btn-fs-send')?.classList.toggle('hidden', !on)
|
|
6267
6182
|
if (on) {
|
|
6268
6183
|
$('composer-image-menu')?.classList.add('hidden')
|
|
@@ -6314,7 +6229,7 @@ function bindComposerFullscreenGesture() {
|
|
|
6314
6229
|
|
|
6315
6230
|
/* ---------------- 初始化 ---------------- */
|
|
6316
6231
|
/** 解析 dshremote://pair?token=..&server=.. 配对二维码;server 可重复以携带多个主机地址。 */
|
|
6317
|
-
function applyPairUrl(url) {
|
|
6232
|
+
function applyPairUrl(url, options = {}) {
|
|
6318
6233
|
try {
|
|
6319
6234
|
const u = new URL(String(url).trim())
|
|
6320
6235
|
if (u.protocol !== 'dshremote:' || u.hostname !== 'pair') return false
|
|
@@ -6323,6 +6238,13 @@ function applyPairUrl(url) {
|
|
|
6323
6238
|
.map(value => value.trim().replace(/\/+$/, ''))
|
|
6324
6239
|
.filter(value => /^https?:\/\//i.test(value)))]
|
|
6325
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
|
+
}
|
|
6326
6248
|
const previousServer = state.server
|
|
6327
6249
|
const previousToken = state.token
|
|
6328
6250
|
state.token = tok
|
|
@@ -6610,14 +6532,14 @@ function bindNativeLinks() {
|
|
|
6610
6532
|
if (!CAP?.isNativePlatform?.()) return
|
|
6611
6533
|
try {
|
|
6612
6534
|
CAP.Plugins?.App?.addListener?.('appUrlOpen', (data) => {
|
|
6613
|
-
if (data?.url && applyPairUrl(data.url)) {
|
|
6535
|
+
if (data?.url && applyPairUrl(data.url, { confirmExisting: true })) {
|
|
6614
6536
|
toast(t('scan.pairedLink'), 'ok')
|
|
6615
6537
|
openStreams()
|
|
6616
6538
|
refreshAll()
|
|
6617
6539
|
}
|
|
6618
6540
|
})
|
|
6619
6541
|
CAP.Plugins?.App?.getLaunchUrl?.().then((data) => {
|
|
6620
|
-
if (data?.url) applyPairUrl(data.url)
|
|
6542
|
+
if (data?.url) applyPairUrl(data.url, { confirmExisting: true })
|
|
6621
6543
|
}).catch(() => {})
|
|
6622
6544
|
} catch {}
|
|
6623
6545
|
}
|
|
@@ -6763,7 +6685,10 @@ function renderDshControlStatus(value) {
|
|
|
6763
6685
|
return
|
|
6764
6686
|
}
|
|
6765
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
|
|
6766
6690
|
if (box && !dshControlPollPromise) box.classList.add('hidden')
|
|
6691
|
+
if (value.canStart === false && value.code) { desc.textContent=dshControlFailureText(value); return }
|
|
6767
6692
|
const service = value.service || 'dsh-web'
|
|
6768
6693
|
const serviceState = `${value.activeState || value.state || 'unknown'}/${value.subState || 'unknown'}`
|
|
6769
6694
|
desc.textContent = value.running
|
|
@@ -6888,7 +6813,31 @@ function openDonateModal() {
|
|
|
6888
6813
|
if (m) m.classList.remove('hidden')
|
|
6889
6814
|
}
|
|
6890
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
|
+
|
|
6891
6839
|
function bindUi() {
|
|
6840
|
+
bindSessionDialogs()
|
|
6892
6841
|
renderLangBtn()
|
|
6893
6842
|
renderThemeBtn()
|
|
6894
6843
|
$('btn-lang').addEventListener('click', () => {
|
|
@@ -6923,9 +6872,6 @@ function bindUi() {
|
|
|
6923
6872
|
$('notes-pages').addEventListener('scroll', updateNotesPage)
|
|
6924
6873
|
$('modal-notes').addEventListener('click', (e) => { if (e.target === $('modal-notes')) closeNotesModal() })
|
|
6925
6874
|
renderServers()
|
|
6926
|
-
document.querySelectorAll('[data-stats-mode]').forEach(button =>
|
|
6927
|
-
button.addEventListener('click', () => setStatsChartMode(button.dataset.statsMode)))
|
|
6928
|
-
applyStatsChartMode()
|
|
6929
6875
|
// 底部导航
|
|
6930
6876
|
document.querySelectorAll('.nav-btn').forEach(b =>
|
|
6931
6877
|
b.addEventListener('click', () => showView(b.dataset.view)))
|
|
@@ -7001,11 +6947,12 @@ function bindUi() {
|
|
|
7001
6947
|
})
|
|
7002
6948
|
$('btn-back').addEventListener('click', () => { void closeSession() })
|
|
7003
6949
|
$('btn-stats').addEventListener('click', () => { renderSessionCards(); $('modal-stats').classList.remove('hidden') })
|
|
6950
|
+
$('btn-insights').addEventListener('click', () => openInsights())
|
|
7004
6951
|
$('stats-close').addEventListener('click', () => $('modal-stats').classList.add('hidden'))
|
|
7005
6952
|
$('btn-refresh').addEventListener('click', () => { toast(t('common.refreshing')); openStreams(); refreshAll() })
|
|
7006
6953
|
$('overview-refresh').addEventListener('click', () => {
|
|
7007
6954
|
toast(t('common.refreshing'))
|
|
7008
|
-
|
|
6955
|
+
startOverviewDetection(true)
|
|
7009
6956
|
void Promise.all([refreshAll(), checkAnnouncements()])
|
|
7010
6957
|
})
|
|
7011
6958
|
// 反馈
|
|
@@ -7267,8 +7214,10 @@ function bindUi() {
|
|
|
7267
7214
|
$('modal-app-version-warning').addEventListener('click', (e) => {
|
|
7268
7215
|
if (e.target === $('modal-app-version-warning')) closeAppVersionWarning(false)
|
|
7269
7216
|
})
|
|
7270
|
-
$('btn-reset').addEventListener('click', () => {
|
|
7217
|
+
$('btn-reset').addEventListener('click', async () => {
|
|
7271
7218
|
if (!confirm(t('settings.confirmReset'))) return
|
|
7219
|
+
clearTimeout(scheduleHistoryCacheSave._t)
|
|
7220
|
+
await window.DshHistory.cacheClear()
|
|
7272
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)
|
|
7273
7222
|
if (bgBridge()?.saveBackgroundConfig) saveBgConfig(false)
|
|
7274
7223
|
location.reload()
|
|
@@ -7382,27 +7331,8 @@ function bindUi() {
|
|
|
7382
7331
|
|
|
7383
7332
|
initCustomSelects()
|
|
7384
7333
|
|
|
7385
|
-
|
|
7386
|
-
$('history').addEventListener('
|
|
7387
|
-
const box = $('history')
|
|
7388
|
-
const h = state.history
|
|
7389
|
-
updateRail()
|
|
7390
|
-
if (!state.current || !h.filtered?.length) return
|
|
7391
|
-
if (box.scrollTop < 80) {
|
|
7392
|
-
if (h.renderStart > 0) {
|
|
7393
|
-
h.renderStart = Math.max(0, h.renderStart - 100)
|
|
7394
|
-
renderHistory(false, 'keep')
|
|
7395
|
-
} else if (h.hasMore && !h.loading) {
|
|
7396
|
-
loadHistory(false)
|
|
7397
|
-
}
|
|
7398
|
-
} else if (box.scrollHeight - box.scrollTop - box.clientHeight < 240) {
|
|
7399
|
-
if (h.renderEnd < h.filtered.length) {
|
|
7400
|
-
h.renderEnd = h.filtered.length
|
|
7401
|
-
h.renderStart = Math.max(0, h.renderEnd - 200)
|
|
7402
|
-
renderHistory(false, 'bottom')
|
|
7403
|
-
}
|
|
7404
|
-
}
|
|
7405
|
-
})
|
|
7334
|
+
$('btn-history-latest').addEventListener('click',()=>{getConversationView().latest();if(state.history.tailEvicted)void loadHistory(true)})
|
|
7335
|
+
$('history').addEventListener('load',()=>updateRail(true),true)
|
|
7406
7336
|
}
|
|
7407
7337
|
|
|
7408
7338
|
/* App 内真实系统栏 inset(刘海/状态栏/手势条) */
|
|
@@ -7429,14 +7359,15 @@ async function boot() {
|
|
|
7429
7359
|
await loadLocalVersion()
|
|
7430
7360
|
if (!state.token) {
|
|
7431
7361
|
showView('view-activity')
|
|
7362
|
+
overviewStarting=false
|
|
7432
7363
|
$('token-desc').textContent = t('token.notSetHint')
|
|
7433
7364
|
} else {
|
|
7434
7365
|
// 多服务器: 启动时静默测速一次, 选最快的连接(同源页面也参与比较)
|
|
7435
7366
|
await selectFastestServer({ silent: true, reconnect: false })
|
|
7436
7367
|
await maybeWarnAppBehindGateway({ probe: true })
|
|
7368
|
+
overviewStarting=false
|
|
7437
7369
|
openStreams()
|
|
7438
|
-
await refreshAll()
|
|
7439
|
-
await refreshHostDescription()
|
|
7370
|
+
await Promise.all([refreshAll(),refreshHostDescription()])
|
|
7440
7371
|
loadDshControl()
|
|
7441
7372
|
}
|
|
7442
7373
|
// 网关从中央 HTTPS 公告源读取并在不可达时回退内置文件。前台每 30 秒检查,
|
|
@@ -7448,13 +7379,15 @@ async function boot() {
|
|
|
7448
7379
|
|
|
7449
7380
|
document.addEventListener('DOMContentLoaded', boot)
|
|
7450
7381
|
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7382
|
+
function openPluginPage() {
|
|
7383
|
+
const connection = captureConnection()
|
|
7384
|
+
if (!connection.token) {
|
|
7385
|
+
$('plugin-page').textContent = '请先在设置中连接 DSH 主机。'
|
|
7386
|
+
return
|
|
7387
|
+
}
|
|
7388
|
+
window.DshPluginCenter.mount({
|
|
7389
|
+
url: path => connection.server + path,
|
|
7390
|
+
headers: { authorization: 'Bearer ' + connection.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web', ...clientIdHeaders() },
|
|
7391
|
+
valid: connection.valid,
|
|
7392
|
+
}, $('plugin-page'))
|
|
7393
|
+
}
|