dsh-remote-plugin 0.6.23 → 0.6.25
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-stats.cjs +106 -84
- package/gateway.cjs +78 -7
- package/package.json +1 -1
- package/public/app.js +169 -250
- package/public/desktop/desktop.css +7 -0
- package/public/desktop/desktop.html +27 -4
- package/public/desktop/desktop.js +145 -21
- package/public/genui.css +2 -0
- package/public/genui.js +144 -0
- package/public/index.html +13 -28
- package/public/md.js +16 -3
- package/public/styles.css +4 -11
- package/public/update.json +12 -12
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -104,7 +104,6 @@ const state = {
|
|
|
104
104
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
105
105
|
modelSettings: { status: 'idle', error: '', writable: false, hasDocument: false, providers: [], namespaces: [], credentials: {} },
|
|
106
106
|
modelEditor: null,
|
|
107
|
-
asrTest: { running: false, status: 'idle', meta: null, summary: null, events: [] },
|
|
108
107
|
wb: null,
|
|
109
108
|
wbProjects: [],
|
|
110
109
|
wbArchived: [],
|
|
@@ -1566,6 +1565,10 @@ window.addEventListener('online', () => {
|
|
|
1566
1565
|
function onMuxFrame(full) {
|
|
1567
1566
|
const f = full.payload
|
|
1568
1567
|
if (!f) return
|
|
1568
|
+
if (f.type === 'session/reasoning') {
|
|
1569
|
+
if (f.sessionId === state.current) { applyReasoningBaseline(f.partialReasoning); scheduleReasoningRender() }
|
|
1570
|
+
return
|
|
1571
|
+
}
|
|
1569
1572
|
if (f.type === 'session/event') return onSessionEvent(f.sessionId, f.event)
|
|
1570
1573
|
if (f.type === 'session/subscribed') return
|
|
1571
1574
|
if (f.type === 'approval/requested') {
|
|
@@ -1837,7 +1840,17 @@ function resyncAfterStreamOpen() {
|
|
|
1837
1840
|
state.lastStreamResyncAt = Date.now()
|
|
1838
1841
|
void refreshAll().then(() => resyncCurrentSession())
|
|
1839
1842
|
}
|
|
1840
|
-
function
|
|
1843
|
+
function sessionTitleValue(s) {
|
|
1844
|
+
const value = proj(s, 'title', '')
|
|
1845
|
+
return value == null ? '' : String(value).trim()
|
|
1846
|
+
}
|
|
1847
|
+
function hasSessionTitle(s) { return !!sessionTitleValue(s) }
|
|
1848
|
+
function titleOf(s) { return sessionTitleValue(s) || (s?.sessionId ? t('session.untitled') : t('session.unknown')) }
|
|
1849
|
+
function sessionLabelOf(s) {
|
|
1850
|
+
const title = titleOf(s)
|
|
1851
|
+
if (!s?.sessionId || hasSessionTitle(s)) return title
|
|
1852
|
+
return `${title} · ${String(s.sessionId).slice(-8)}`
|
|
1853
|
+
}
|
|
1841
1854
|
function short(id) { return '…' + String(id).slice(-8) }
|
|
1842
1855
|
function isTopLevelSession(session) {
|
|
1843
1856
|
return !!session && !session.parentSessionId && session.origin !== 'subagent'
|
|
@@ -2110,7 +2123,7 @@ function renderWorkbench() {
|
|
|
2110
2123
|
<div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
|
|
2111
2124
|
<button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}">
|
|
2112
2125
|
<span class="wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span>
|
|
2113
|
-
<span class="wb-session-title">${esc(
|
|
2126
|
+
<span class="wb-session-title">${esc(sessionLabelOf(s))}</span>
|
|
2114
2127
|
<span class="wb-session-meta">${s.running ? esc(t('sessions.running')) : esc(fmtTime(sessionSortTime(s)))}</span>
|
|
2115
2128
|
</button>
|
|
2116
2129
|
<button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>
|
|
@@ -2202,7 +2215,7 @@ function renderSessions() {
|
|
|
2202
2215
|
const renderSession = s => {
|
|
2203
2216
|
const workspace = sessionWorkspaceLabel(s)
|
|
2204
2217
|
const workspaceTitle = sessionWorkspaceName(s)
|
|
2205
|
-
const title =
|
|
2218
|
+
const title = sessionLabelOf(s)
|
|
2206
2219
|
const goal = goalOf(s)
|
|
2207
2220
|
const pending = (state.approvals.some(a => a.sessionId === s.sessionId) || state.questions.some(q => q.sessionId === s.sessionId)) ? 'pending' : ''
|
|
2208
2221
|
const queueN = (state.queues[s.sessionId] || []).filter(i => i.placement === 'queued').length
|
|
@@ -2275,7 +2288,31 @@ function renderSessions() {
|
|
|
2275
2288
|
}
|
|
2276
2289
|
|
|
2277
2290
|
/* ---------------- 会话详情 ---------------- */
|
|
2291
|
+
const emptySessionCleanup = new Set()
|
|
2292
|
+
async function archiveEmptySessionOnLeave(sessionId) {
|
|
2293
|
+
const base = state.server
|
|
2294
|
+
const protectedSession = () => state.server !== base || state.current !== sessionId
|
|
2295
|
+
|| !state.byId.has(sessionId) || state.byId.get(sessionId)?.running
|
|
2296
|
+
|| state.sessionActivity?.has(sessionId) || state.pendingPrompts?.has(sessionId)
|
|
2297
|
+
|| (state.queues[sessionId] || []).length > 0
|
|
2298
|
+
|| (state.composerImages || []).length > 0 || !!$('composer-input')?.value?.trim()
|
|
2299
|
+
if (!sessionId || emptySessionCleanup.has(sessionId) || protectedSession()) return false
|
|
2300
|
+
emptySessionCleanup.add(sessionId)
|
|
2301
|
+
try {
|
|
2302
|
+
const history = await rpc('session.history', { sessionId, maxMessages: 1 })
|
|
2303
|
+
if (protectedSession() || !Array.isArray(history?.events) || history.events.some(item => !['permission/preset', 'sandbox/mode', 'approval/policy'].includes(item?.event?.type || item?.type))
|
|
2304
|
+
|| history.hasMore || (history.partialReasoning || []).length) return false
|
|
2305
|
+
const result = await rpc('workspace.archiveSession', { sessionId })
|
|
2306
|
+
if (!Array.isArray(result?.archivedSessionIds) || !result.archivedSessionIds.includes(sessionId)) return false
|
|
2307
|
+
if (state.server !== base) return false
|
|
2308
|
+
await refreshSessions()
|
|
2309
|
+
return true
|
|
2310
|
+
} catch { return false } // 离线、历史未知或归档失败时保留会话。
|
|
2311
|
+
finally { emptySessionCleanup.delete(sessionId) }
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2278
2314
|
async function openSession(id) {
|
|
2315
|
+
if (state.current && state.current !== id) await archiveEmptySessionOnLeave(state.current)
|
|
2279
2316
|
state.current = id
|
|
2280
2317
|
setSessionRecovery('loading')
|
|
2281
2318
|
state.history = emptyHistory()
|
|
@@ -2318,7 +2355,7 @@ async function closeSession() {
|
|
|
2318
2355
|
const sessionId = state.current
|
|
2319
2356
|
if (!sessionId) return
|
|
2320
2357
|
const task = (async () => {
|
|
2321
|
-
const discard = await shouldDiscardEmptySession(sessionId)
|
|
2358
|
+
const discard = await shouldDiscardEmptySession(sessionId) && await archiveEmptySessionOnLeave(sessionId)
|
|
2322
2359
|
if (state.current !== sessionId) return
|
|
2323
2360
|
state.current = null
|
|
2324
2361
|
renderSessionPending()
|
|
@@ -2432,6 +2469,13 @@ function reasoningStreamKey(data, index) {
|
|
|
2432
2469
|
* DSH 的实时思考以 assistant/chunk 下发,历史尾页可能把增量压成
|
|
2433
2470
|
* reasoning-chunks。最终 assistant/message 到达后再由正式消息接管展示。
|
|
2434
2471
|
*/
|
|
2472
|
+
function applyReasoningBaseline(items) {
|
|
2473
|
+
if (!Array.isArray(items)) return
|
|
2474
|
+
state.history.reasoningVersion = (state.history.reasoningVersion || 0) + 1
|
|
2475
|
+
state.history.partialReasoning.clear()
|
|
2476
|
+
for (const item of items) state.history.partialReasoning.set(reasoningStreamKey(item, item.index), item)
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2435
2479
|
function applyReasoningStreamEvent(event) {
|
|
2436
2480
|
const h = state.history
|
|
2437
2481
|
const data = event?.data || {}
|
|
@@ -2460,7 +2504,7 @@ function applyReasoningStreamEvent(event) {
|
|
|
2460
2504
|
item.text += Array.isArray(data.texts) ? data.texts.join('') : String(data.text || '')
|
|
2461
2505
|
h.partialReasoning.set(key, item)
|
|
2462
2506
|
changed = true
|
|
2463
|
-
} else if (event?.type === 'assistant/message') {
|
|
2507
|
+
} else if (event?.type === 'assistant/message' || event?.type === 'assistant/attempt') {
|
|
2464
2508
|
for (const [key, item] of h.partialReasoning) {
|
|
2465
2509
|
if (item.turn === data.turn && item.step === data.step) {
|
|
2466
2510
|
h.partialReasoning.delete(key)
|
|
@@ -2550,6 +2594,7 @@ async function loadHistory(reset) {
|
|
|
2550
2594
|
const id = state.current
|
|
2551
2595
|
if (!id || state.history.loading) return
|
|
2552
2596
|
const history = state.history
|
|
2597
|
+
const reasoningVersion = history.reasoningVersion || 0
|
|
2553
2598
|
history.loading = true
|
|
2554
2599
|
if (reset) setSessionRecovery('loading')
|
|
2555
2600
|
const moreBtn = $('history-more')
|
|
@@ -2583,6 +2628,7 @@ async function loadHistory(reset) {
|
|
|
2583
2628
|
}
|
|
2584
2629
|
|
|
2585
2630
|
if (state.current !== id || state.history !== history) return
|
|
2631
|
+
const liveReasoning = (history.reasoningVersion || 0) !== reasoningVersion ? new Map(history.partialReasoning) : null
|
|
2586
2632
|
hydrateSessionProjections(id, v.projections)
|
|
2587
2633
|
history.loaded = true
|
|
2588
2634
|
const incoming = v.events || []
|
|
@@ -2600,6 +2646,8 @@ async function loadHistory(reset) {
|
|
|
2600
2646
|
added++
|
|
2601
2647
|
}
|
|
2602
2648
|
// 向前翻页游标 = 本页最旧的 raw seq(即使它本身被过滤)
|
|
2649
|
+
if (liveReasoning) history.partialReasoning = liveReasoning
|
|
2650
|
+
else applyReasoningBaseline(v.partialReasoning)
|
|
2603
2651
|
const firstSeq = incoming[0]?.event?.seq
|
|
2604
2652
|
if (firstSeq != null) state.history.minSeq = Math.min(state.history.minSeq, firstSeq)
|
|
2605
2653
|
state.history.visible.sort((a, b) => a.seq - b.seq)
|
|
@@ -2840,6 +2888,7 @@ function blockHtml(b) {
|
|
|
2840
2888
|
if ((b.type === 'tool-call' || b.type === 'tool-result') && LS.get('showTools', '1') === '0') return ''
|
|
2841
2889
|
switch (b.type) {
|
|
2842
2890
|
case 'text': return `<div class="md">${window.mdToHtml ? window.mdToHtml(b.text ?? '') : esc(b.text ?? '')}</div>`
|
|
2891
|
+
case 'file': return `<div class="tool">📎 ${esc(b.attachment?.name || b.name || 'file')} <small>${esc(b.attachment?.bytes ?? b.bytes ?? '')} bytes</small></div>`
|
|
2843
2892
|
case 'image': return `<img alt="${t('block.image')}" src="data:${esc(b.mediaType || 'image/png')};base64,${esc(b.data || '')}">`
|
|
2844
2893
|
case 'thinking':
|
|
2845
2894
|
case 'reasoning':
|
|
@@ -3410,7 +3459,7 @@ function renameSession(sessionId = state.current) {
|
|
|
3410
3459
|
const session = state.byId.get(sessionId)
|
|
3411
3460
|
if (!session) return
|
|
3412
3461
|
renamePendingSessionId = sessionId
|
|
3413
|
-
$('rename-session-input').value =
|
|
3462
|
+
$('rename-session-input').value = sessionTitleValue(session)
|
|
3414
3463
|
$('modal-rename').classList.remove('hidden')
|
|
3415
3464
|
setTimeout(() => { $('rename-session-input').focus(); $('rename-session-input').select() }, 40)
|
|
3416
3465
|
}
|
|
@@ -3601,7 +3650,7 @@ function renderOverview() {
|
|
|
3601
3650
|
]
|
|
3602
3651
|
$('overview-attention-count').textContent = pending.length ? t('overview.pendingCount', { n: pending.length }) : '—'
|
|
3603
3652
|
$('overview-attention-list').innerHTML = pending.length ? pending.slice(0, 3).map(({ kind, item }) => {
|
|
3604
|
-
const title =
|
|
3653
|
+
const title = sessionLabelOf(state.byId.get(item.sessionId))
|
|
3605
3654
|
if (kind === 'approval') return `<div class="overview-attention-item" data-overview-approval="${esc(item.approvalId)}">
|
|
3606
3655
|
<span class="overview-item-mark">⌁</span><span class="overview-item-copy"><span class="overview-item-title">${esc(item.toolName || t('tool.default'))}</span><span class="overview-item-desc">${esc(approvalDetail(item))} · ${esc(title)}</span></span>
|
|
3607
3656
|
<span class="overview-item-actions"><button type="button" class="mini-btn" data-overview-approve="1">${t('pending.allow')}</button><button type="button" class="mini-btn" data-overview-approve="0">${t('pending.reject')}</button></span>
|
|
@@ -3650,7 +3699,7 @@ function renderOverview() {
|
|
|
3650
3699
|
$('overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'overview.poll' : 'overview.ws') : '—'
|
|
3651
3700
|
$('overview-active-count').textContent = running ? t('overview.activeCount', { n: running }) : ''
|
|
3652
3701
|
$('overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="overview-session-item ${s.running ? 'running' : ''}" data-overview-session="${esc(s.sessionId)}">
|
|
3653
|
-
<span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(
|
|
3702
|
+
<span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(sessionLabelOf(s))}</span><span class="overview-item-desc">${s.running ? esc(t('sessions.running')) + ' · ' : ''}${esc(fmtTime(sessionSortTime(s)))}</span></span><span class="overview-item-arrow">›</span>
|
|
3654
3703
|
</button>`).join('') : `<div class="overview-empty">${t('overview.noSession')}</div>`
|
|
3655
3704
|
$('overview-session-list').querySelectorAll('[data-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.overviewSession)))
|
|
3656
3705
|
}
|
|
@@ -4153,9 +4202,11 @@ async function openFsPreview(pathValue, name) {
|
|
|
4153
4202
|
$('file-preview-loading').classList.add('hidden')
|
|
4154
4203
|
$('file-preview-source').textContent = data.content || ''
|
|
4155
4204
|
const markdown = data.extension === '.md' || data.extension === '.markdown'
|
|
4156
|
-
|
|
4205
|
+
const htmlPreview = (data.extension === '.html' || data.extension === '.htm') && !!window.DshGenUi
|
|
4206
|
+
$('file-preview-tabs').classList.toggle('hidden', !markdown && !htmlPreview)
|
|
4157
4207
|
if (markdown) $('file-preview-rendered').innerHTML = window.mdToHtml(data.content || '')
|
|
4158
|
-
|
|
4208
|
+
if (htmlPreview) $('file-preview-rendered').innerHTML = window.DshGenUi.html(data.content || '')
|
|
4209
|
+
showFsPreviewMode(markdown || htmlPreview ? 'rendered' : 'source')
|
|
4159
4210
|
} catch (e) {
|
|
4160
4211
|
if (generation !== fsPreviewGeneration) return
|
|
4161
4212
|
$('file-preview-loading').textContent = e.message || t('fs.previewFailed', { msg: t('fs.networkError') })
|
|
@@ -5101,77 +5152,123 @@ async function sha256Hex(buffer) {
|
|
|
5101
5152
|
return [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, '0')).join('')
|
|
5102
5153
|
}
|
|
5103
5154
|
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5155
|
+
let updateDownloadBusy = false
|
|
5156
|
+
let updateDownloadTimer = null
|
|
5157
|
+
let updateDownloadSample = null
|
|
5158
|
+
|
|
5159
|
+
function updateDownloadProgress(value) {
|
|
5160
|
+
const box = $('update-download-progress')
|
|
5161
|
+
if (!box) return
|
|
5162
|
+
const received = Math.max(0, Number(value.received) || 0)
|
|
5163
|
+
const total = Math.max(0, Number(value.total) || 0)
|
|
5164
|
+
const now = performance.now()
|
|
5165
|
+
if (!updateDownloadSample || received < updateDownloadSample.received) updateDownloadSample = { time: now, received, speed: 0 }
|
|
5166
|
+
const elapsed = now - updateDownloadSample.time
|
|
5167
|
+
if (elapsed >= 800) updateDownloadSample = { time: now, received, speed: Math.max(0, received - updateDownloadSample.received) * 1000 / elapsed }
|
|
5168
|
+
const format = n => n >= 1048576 ? `${(n / 1048576).toFixed(1)} MB` : `${(n / 1024).toFixed(1)} KB`
|
|
5169
|
+
const bar = $('update-download-bar')
|
|
5170
|
+
box.classList.remove('hidden')
|
|
5171
|
+
if (total > 0) bar.value = Math.min(100, received * 100 / total)
|
|
5172
|
+
else bar.removeAttribute('value')
|
|
5173
|
+
const phase = value.phase || 'downloading'
|
|
5174
|
+
$('update-download-status').textContent = phase === 'error' ? t('update.downloadFailed', { msg: value.error || t('fs.networkError') }) : t(`update.phase.${phase}`)
|
|
5175
|
+
$('update-download-detail').textContent = `${total > 0 ? `${Math.min(100, received * 100 / total).toFixed(0)}% · ` : ''}${format(received)}${total > 0 ? ` / ${format(total)}` : ''} · ${format(phase === 'downloading' ? updateDownloadSample.speed : 0)}/s`
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
function finishUpdateDownload() {
|
|
5179
|
+
updateDownloadBusy = false
|
|
5180
|
+
clearInterval(updateDownloadTimer)
|
|
5181
|
+
updateDownloadTimer = null
|
|
5182
|
+
$('btn-download-update').disabled = false
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5109
5185
|
async function verifyUpdateApk(info, url) {
|
|
5110
|
-
const
|
|
5111
|
-
|
|
5112
|
-
let
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
res = signal ? await fetch(url, { signal }) : await fetch(url)
|
|
5116
|
-
} catch (err) {
|
|
5117
|
-
return { ok: false, network: true, msg: err?.message || '' }
|
|
5118
|
-
}
|
|
5119
|
-
if (!res.ok) return { ok: false, status: res.status }
|
|
5120
|
-
let buf
|
|
5121
|
-
try {
|
|
5122
|
-
buf = await res.arrayBuffer()
|
|
5123
|
-
} catch (err) {
|
|
5124
|
-
return { ok: false, network: true, msg: err?.message || '' }
|
|
5125
|
-
}
|
|
5126
|
-
let actual
|
|
5186
|
+
const controller = new AbortController()
|
|
5187
|
+
let timer
|
|
5188
|
+
let received = 0, total = 0, phase = 'downloading'
|
|
5189
|
+
const progressTimer = setInterval(() => updateDownloadProgress({ phase, received, total }), 500)
|
|
5190
|
+
const resetTimeout = () => { clearTimeout(timer); timer = setTimeout(() => controller.abort(), 60000) }
|
|
5127
5191
|
try {
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
return { ok:
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5192
|
+
resetTimeout()
|
|
5193
|
+
const res = await fetch(url, { signal: controller.signal })
|
|
5194
|
+
if (!res.ok) return { ok: false, status: res.status }
|
|
5195
|
+
total = Number(res.headers.get('content-length')) || 0
|
|
5196
|
+
const chunks = []
|
|
5197
|
+
if (res.body?.getReader) {
|
|
5198
|
+
const reader = res.body.getReader()
|
|
5199
|
+
for (;;) {
|
|
5200
|
+
const { value, done } = await reader.read()
|
|
5201
|
+
if (done) break
|
|
5202
|
+
chunks.push(value)
|
|
5203
|
+
received += value.byteLength
|
|
5204
|
+
resetTimeout()
|
|
5205
|
+
updateDownloadProgress({ phase: 'downloading', received, total })
|
|
5206
|
+
}
|
|
5207
|
+
} else {
|
|
5208
|
+
chunks.push(new Uint8Array(await res.arrayBuffer()))
|
|
5209
|
+
received = chunks[0].byteLength
|
|
5210
|
+
}
|
|
5211
|
+
if (total > 0 && total !== received) return { ok: false, corrupted: true }
|
|
5212
|
+
const blob = new Blob(chunks, { type: 'application/vnd.android.package-archive' })
|
|
5213
|
+
phase = 'verifying'
|
|
5214
|
+
updateDownloadProgress({ phase, received, total: total || received })
|
|
5215
|
+
const expected = String(info.sha256 || '').trim().toLowerCase()
|
|
5216
|
+
if (expected) {
|
|
5217
|
+
if (!/^[0-9a-f]{64}$/.test(expected)) return { ok: false, corrupted: true }
|
|
5218
|
+
const actual = await sha256Hex(await blob.arrayBuffer())
|
|
5219
|
+
if (actual !== expected) return { ok: false, corrupted: true }
|
|
5220
|
+
}
|
|
5221
|
+
return { ok: true, blob }
|
|
5222
|
+
} catch (err) { return { ok: false, network: true, msg: err?.message || '' } }
|
|
5223
|
+
finally { clearTimeout(timer); clearInterval(progressTimer) }
|
|
5134
5224
|
}
|
|
5135
5225
|
|
|
5136
5226
|
async function downloadUpdate() {
|
|
5137
5227
|
const info = state.updateInfo
|
|
5138
|
-
if (!info) return
|
|
5139
|
-
const
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5228
|
+
if (!info || updateDownloadBusy) return
|
|
5229
|
+
const url = new URL(info.apkUrl || 'dsh-remote.apk', updateBase() + '/').href
|
|
5230
|
+
updateDownloadBusy = true
|
|
5231
|
+
updateDownloadSample = null
|
|
5232
|
+
$('btn-download-update').disabled = true
|
|
5233
|
+
updateDownloadProgress({ phase: 'downloading', received: 0, total: 0 })
|
|
5234
|
+
if (CAP?.isNativePlatform?.() && window.NativeUpdate?.downloadVerifiedAndInstall && window.NativeUpdate?.getDownloadStatus) {
|
|
5235
|
+
try {
|
|
5236
|
+
if (!window.NativeUpdate.downloadVerifiedAndInstall(url, String(info.sha256 || '').trim())) throw new Error(t('update.busy'))
|
|
5237
|
+
const poll = () => {
|
|
5238
|
+
try {
|
|
5239
|
+
const value = JSON.parse(window.NativeUpdate.getDownloadStatus())
|
|
5240
|
+
updateDownloadProgress(value)
|
|
5241
|
+
if (['complete', 'error'].includes(value.phase)) finishUpdateDownload()
|
|
5242
|
+
} catch (error) { updateDownloadProgress({ phase: 'error', error: error.message }); finishUpdateDownload() }
|
|
5243
|
+
}
|
|
5244
|
+
updateDownloadTimer = setInterval(poll, 500)
|
|
5245
|
+
poll()
|
|
5246
|
+
} catch (error) { updateDownloadProgress({ phase: 'error', error: error.message }); finishUpdateDownload() }
|
|
5154
5247
|
return
|
|
5155
5248
|
}
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
if (window.NativeUpdate?.downloadAndInstall) {
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5249
|
+
try {
|
|
5250
|
+
const result = await verifyUpdateApk(info, url)
|
|
5251
|
+
if (!result.ok) throw new Error(result.corrupted ? t('update.corrupted') : result.status ? t('update.serverFileMissing') : result.msg || t('fs.networkError'))
|
|
5252
|
+
if (CAP?.isNativePlatform?.() && window.NativeUpdate?.downloadAndInstall) {
|
|
5253
|
+
// 旧壳不具备进度桥;安装本次新版后即可使用单次下载和完整进度。
|
|
5254
|
+
window.NativeUpdate.downloadAndInstall(url)
|
|
5255
|
+
updateDownloadProgress({ phase: 'legacy', received: 0, total: 0 })
|
|
5256
|
+
} else {
|
|
5257
|
+
const objectUrl = URL.createObjectURL(result.blob)
|
|
5258
|
+
const link = document.createElement('a')
|
|
5259
|
+
link.href = objectUrl
|
|
5260
|
+
link.download = 'dsh-remote.apk'
|
|
5261
|
+
document.body.appendChild(link)
|
|
5262
|
+
link.click()
|
|
5263
|
+
link.remove()
|
|
5264
|
+
setTimeout(() => URL.revokeObjectURL(objectUrl), 60000)
|
|
5265
|
+
updateDownloadProgress({ phase: 'complete', received: result.blob.size, total: result.blob.size })
|
|
5167
5266
|
}
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
}
|
|
5171
|
-
// 浏览器: 直接触发下载
|
|
5172
|
-
location.href = url
|
|
5267
|
+
} catch (error) { updateDownloadProgress({ phase: 'error', error: error.message }) }
|
|
5268
|
+
finally { finishUpdateDownload() }
|
|
5173
5269
|
}
|
|
5174
5270
|
|
|
5271
|
+
|
|
5175
5272
|
/* ---------------- 通知 ---------------- */
|
|
5176
5273
|
const CAP = window.Capacitor || null
|
|
5177
5274
|
async function ensureNotify() {
|
|
@@ -5389,177 +5486,6 @@ async function restorePeakReminders() {
|
|
|
5389
5486
|
if (peakRemindOn() && legacyCleaned) await schedulePeakReminders({ legacyCleaned: true })
|
|
5390
5487
|
}
|
|
5391
5488
|
|
|
5392
|
-
/* ---------------- 功能测试 / Android ASR ---------------- */
|
|
5393
|
-
function asrTestBridge() { return window.NativeAsrTest }
|
|
5394
|
-
|
|
5395
|
-
function emptyAsrTest() {
|
|
5396
|
-
return { running: false, status: 'idle', meta: null, summary: null, events: [], lastError: '' }
|
|
5397
|
-
}
|
|
5398
|
-
|
|
5399
|
-
function asrTestEvent(event) {
|
|
5400
|
-
if (!event || typeof event !== 'object') return
|
|
5401
|
-
const current = state.asrTest
|
|
5402
|
-
const data = event.data && typeof event.data === 'object' ? event.data : {}
|
|
5403
|
-
if (event.type === 'meta') current.meta = data
|
|
5404
|
-
if (event.type === 'summary') {
|
|
5405
|
-
current.summary = data
|
|
5406
|
-
current.running = false
|
|
5407
|
-
}
|
|
5408
|
-
if (event.type === 'status') {
|
|
5409
|
-
current.status = String(data.status || 'unknown')
|
|
5410
|
-
if (current.status === 'listening' || current.status === 'starting' || current.status === 'restarting') current.running = true
|
|
5411
|
-
if (['stopped', 'unsupported', 'permission-denied'].includes(current.status)) current.running = false
|
|
5412
|
-
}
|
|
5413
|
-
if (event.type === 'error') current.lastError = String(data.name || data.message || 'error')
|
|
5414
|
-
current.events.push({ type: event.type, atMs: Number(event.atMs) || 0, data })
|
|
5415
|
-
if (current.events.length > 500) current.events.splice(0, current.events.length - 500)
|
|
5416
|
-
renderAsrTest()
|
|
5417
|
-
}
|
|
5418
|
-
window.__dshAsrEvent = asrTestEvent
|
|
5419
|
-
|
|
5420
|
-
function asrTestStatusText(status) {
|
|
5421
|
-
const labels = {
|
|
5422
|
-
idle: t('settings.asrTestNativeOnly'),
|
|
5423
|
-
starting: t('settings.asrTestStarted'),
|
|
5424
|
-
listening: t('settings.asrTestStarted'),
|
|
5425
|
-
restarting: t('settings.asrTestRestarting'),
|
|
5426
|
-
'permission-requesting': t('settings.asrTestPermission'),
|
|
5427
|
-
'permission-denied': t('settings.asrTestPermissionDenied'),
|
|
5428
|
-
'permission-error': t('settings.asrTestPermissionError'),
|
|
5429
|
-
unsupported: t('settings.asrTestUnavailable'),
|
|
5430
|
-
busy: t('settings.asrTestBusy'),
|
|
5431
|
-
stopped: t('settings.asrTestStopped')
|
|
5432
|
-
}
|
|
5433
|
-
return labels[status] || t('settings.asrTestStatus', { status })
|
|
5434
|
-
}
|
|
5435
|
-
|
|
5436
|
-
function asrTestLogLines() {
|
|
5437
|
-
const current = state.asrTest
|
|
5438
|
-
const lines = []
|
|
5439
|
-
for (const event of current.events) {
|
|
5440
|
-
const data = event.data || {}
|
|
5441
|
-
const at = `${event.atMs}ms`
|
|
5442
|
-
if (event.type === 'meta') {
|
|
5443
|
-
lines.push(`[${at}] meta brand=${data.brand || '—'} manufacturer=${data.manufacturer || '—'} model=${data.model || '—'} Android=${data.androidVersion || '—'} API=${data.apiLevel || '—'}`)
|
|
5444
|
-
lines.push(`[${at}] recordAudioPermission=${data.recordAudioPermission ?? 'unknown'} recordAudioAppOp=${data.recordAudioAppOp || 'unknown'} microphoneMuted=${data.microphoneMuted ?? 'unknown'}`)
|
|
5445
|
-
lines.push(`[${at}] recognitionAvailable=${data.recognitionAvailable === true} onDeviceAvailable=${data.onDeviceAvailable === true} path=${data.networkPath || '—'}`)
|
|
5446
|
-
for (const service of data.recognitionServices || []) lines.push(`[${at}] service ${service.packageName || '—'} / ${service.serviceName || '—'} xiaomiLike=${service.xiaomiLike === true}`)
|
|
5447
|
-
} else if (event.type === 'status') {
|
|
5448
|
-
lines.push(`[${at}] status=${data.status || '—'} session=${data.session ?? '—'} reason=${data.reason || '—'} ${data.message || ''}`.trim())
|
|
5449
|
-
} else if (event.type === 'partial' || event.type === 'final') {
|
|
5450
|
-
lines.push(`[${at}] ${event.type}#${data.count ?? '—'} session=${data.session ?? '—'} +${data.elapsedMs ?? '—'}ms: ${data.text || '(empty)'}`)
|
|
5451
|
-
} else if (event.type === 'callback') {
|
|
5452
|
-
lines.push(`[${at}] callback=${data.name || '—'} session=${data.session ?? '—'} +${data.elapsedMs ?? '—'}ms${data.bytes >= 0 ? ` bytes=${data.bytes}` : ''}`)
|
|
5453
|
-
} else if (event.type === 'error') {
|
|
5454
|
-
lines.push(`[${at}] error=${data.name || '—'} code=${data.code ?? '—'} session=${data.session ?? '—'} ${data.message || ''}`.trim())
|
|
5455
|
-
} else if (event.type === 'summary') {
|
|
5456
|
-
lines.push(`[${at}] summary reason=${data.reason || '—'} duration=${data.durationMs ?? '—'}ms sessions=${data.sessionCount ?? '—'} restarts=${data.restartCount ?? '—'} partial=${data.partialCount ?? '—'} final=${data.finalCount ?? '—'} errors=${data.errorCount ?? '—'}`)
|
|
5457
|
-
}
|
|
5458
|
-
}
|
|
5459
|
-
return lines
|
|
5460
|
-
}
|
|
5461
|
-
|
|
5462
|
-
function asrTestReport() {
|
|
5463
|
-
const current = state.asrTest
|
|
5464
|
-
const meta = current.meta || {}
|
|
5465
|
-
const summary = current.summary || {}
|
|
5466
|
-
const lines = [
|
|
5467
|
-
'DSH Remote Android ASR 测试报告',
|
|
5468
|
-
`生成时间: ${new Date().toISOString()}`,
|
|
5469
|
-
`设备: ${meta.brand || '—'} / ${meta.manufacturer || '—'} / ${meta.model || '—'}`,
|
|
5470
|
-
`Android: ${meta.androidVersion || '—'} (API ${meta.apiLevel || '—'})`,
|
|
5471
|
-
`识别可用: ${meta.recognitionAvailable === true ? 'yes' : meta.recognitionAvailable === false ? 'no' : 'unknown'}`,
|
|
5472
|
-
`端侧识别可用: ${meta.onDeviceAvailable === true ? 'yes' : meta.onDeviceAvailable === false ? 'no' : 'unknown'}`,
|
|
5473
|
-
`路径: ${meta.networkPath || 'system-default-recognition-service'}`,
|
|
5474
|
-
`测试结束原因: ${summary.reason || current.status || '—'}`,
|
|
5475
|
-
`总时长: ${summary.durationMs ?? '—'}ms`,
|
|
5476
|
-
`session: ${summary.sessionCount ?? '—'} / 重建: ${summary.restartCount ?? '—'} / partial: ${summary.partialCount ?? '—'} / final: ${summary.finalCount ?? '—'} / errors: ${summary.errorCount ?? '—'}`,
|
|
5477
|
-
'',
|
|
5478
|
-
'事件日志:',
|
|
5479
|
-
...asrTestLogLines()
|
|
5480
|
-
]
|
|
5481
|
-
return lines.join('\n')
|
|
5482
|
-
}
|
|
5483
|
-
|
|
5484
|
-
function renderAsrTest() {
|
|
5485
|
-
const start = $('btn-asr-test-start')
|
|
5486
|
-
const stop = $('btn-asr-test-stop')
|
|
5487
|
-
const copy = $('btn-asr-test-copy')
|
|
5488
|
-
const permission = $('btn-asr-test-permission')
|
|
5489
|
-
const engine = $('btn-asr-test-engine')
|
|
5490
|
-
const status = $('asr-test-status')
|
|
5491
|
-
const summary = $('asr-test-summary')
|
|
5492
|
-
const log = $('asr-test-log')
|
|
5493
|
-
if (!start || !stop || !copy || !permission || !engine || !status || !summary || !log) return
|
|
5494
|
-
const current = state.asrTest
|
|
5495
|
-
const native = !!(CAP?.isNativePlatform?.() && asrTestBridge()?.startAsrTest)
|
|
5496
|
-
start.disabled = current.running || !native
|
|
5497
|
-
stop.disabled = !current.running || !native
|
|
5498
|
-
copy.disabled = !current.events.length
|
|
5499
|
-
const permissionError = current.status === 'permission-error' || current.status === 'permission-denied' || current.summary?.reason === 'permission-error'
|
|
5500
|
-
status.className = 'feature-test-status ' + (permissionError || current.status === 'unsupported' ? 'error' : current.status === 'stopped' ? 'ok' : 'muted')
|
|
5501
|
-
status.textContent = native ? (permissionError ? t('settings.asrTestPermissionError') : asrTestStatusText(current.status)) : t('settings.asrTestWebUnsupported')
|
|
5502
|
-
permission.classList.toggle('hidden', !native || !permissionError)
|
|
5503
|
-
engine.classList.toggle('hidden', !native || !permissionError)
|
|
5504
|
-
const meta = current.meta || {}
|
|
5505
|
-
const s = current.summary
|
|
5506
|
-
summary.textContent = [
|
|
5507
|
-
meta.model ? `${t('settings.asrTestMeta')}: ${meta.brand || '—'} / ${meta.manufacturer || '—'} / ${meta.model}` : '',
|
|
5508
|
-
s ? `${t('settings.asrTestSummary')}: ${t('settings.asrTestStatus', { status: s.reason || 'done' })} · session ${s.sessionCount ?? '—'} · partial ${s.partialCount ?? '—'} · final ${s.finalCount ?? '—'} · error ${s.errorCount ?? '—'}` : ''
|
|
5509
|
-
].filter(Boolean).join('\n')
|
|
5510
|
-
log.textContent = current.events.length ? asrTestLogLines().join('\n') : t('settings.asrTestLogEmpty')
|
|
5511
|
-
log.scrollTop = log.scrollHeight
|
|
5512
|
-
}
|
|
5513
|
-
|
|
5514
|
-
function clearAsrTest() {
|
|
5515
|
-
if (state.asrTest.running) return toast(t('settings.asrTestBusy'), 'err')
|
|
5516
|
-
state.asrTest = emptyAsrTest()
|
|
5517
|
-
renderAsrTest()
|
|
5518
|
-
}
|
|
5519
|
-
|
|
5520
|
-
async function startAsrTest() {
|
|
5521
|
-
const native = asrTestBridge()
|
|
5522
|
-
if (!CAP?.isNativePlatform?.() || !native?.startAsrTest) return toast(t('settings.asrTestWebUnsupported'), 'err')
|
|
5523
|
-
if (state.asrTest.running) return toast(t('settings.asrTestBusy'), 'err')
|
|
5524
|
-
if (!confirm(t('settings.asrTestConsent'))) return
|
|
5525
|
-
state.asrTest = { ...emptyAsrTest(), running: true, status: 'starting' }
|
|
5526
|
-
renderAsrTest()
|
|
5527
|
-
try {
|
|
5528
|
-
if (native.startAsrTest() === false) throw new Error(t('settings.asrTestUnavailable'))
|
|
5529
|
-
} catch (error) {
|
|
5530
|
-
state.asrTest.running = false
|
|
5531
|
-
state.asrTest.status = 'error'
|
|
5532
|
-
state.asrTest.lastError = error?.message || String(error)
|
|
5533
|
-
renderAsrTest()
|
|
5534
|
-
toast(state.asrTest.lastError, 'err')
|
|
5535
|
-
}
|
|
5536
|
-
}
|
|
5537
|
-
|
|
5538
|
-
function stopAsrTest() {
|
|
5539
|
-
try { asrTestBridge()?.stopAsrTest?.() } catch {}
|
|
5540
|
-
}
|
|
5541
|
-
|
|
5542
|
-
function openAsrPermissionSettings() {
|
|
5543
|
-
try {
|
|
5544
|
-
if (asrTestBridge()?.openAsrPermissionSettings?.() === false) throw new Error('permission settings unavailable')
|
|
5545
|
-
} catch (error) {
|
|
5546
|
-
toast(error?.message || String(error), 'err')
|
|
5547
|
-
}
|
|
5548
|
-
}
|
|
5549
|
-
|
|
5550
|
-
function openAsrEngineSettings() {
|
|
5551
|
-
try {
|
|
5552
|
-
if (asrTestBridge()?.openAsrEngineSettings?.() === false) throw new Error('voice engine settings unavailable')
|
|
5553
|
-
} catch (error) {
|
|
5554
|
-
toast(error?.message || String(error), 'err')
|
|
5555
|
-
}
|
|
5556
|
-
}
|
|
5557
|
-
|
|
5558
|
-
async function copyAsrTestLog() {
|
|
5559
|
-
const ok = await copyText(asrTestReport())
|
|
5560
|
-
toast(t(ok ? 'settings.asrTestCopyOk' : 'settings.asrTestCopyFailed'), ok ? 'ok' : 'err')
|
|
5561
|
-
}
|
|
5562
|
-
|
|
5563
5489
|
/* ---------------- 模型设置 ---------------- */
|
|
5564
5490
|
const MODEL_SETTINGS_FIELDS = ['baseURL', 'api', 'apiKeyEnv', 'displayName', 'models']
|
|
5565
5491
|
const MODEL_REASONING_LIMIT = 12
|
|
@@ -6101,6 +6027,7 @@ async function openModelConfigDocument() {
|
|
|
6101
6027
|
}
|
|
6102
6028
|
/* ---------------- 视图切换 ---------------- */
|
|
6103
6029
|
function showView(id) {
|
|
6030
|
+
if (id !== 'view-session' && document.body.classList.contains('in-session') && state.current) void archiveEmptySessionOnLeave(state.current)
|
|
6104
6031
|
for (const v of ['view-home', 'view-files', 'view-session', 'view-activity', 'view-stats', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
6105
6032
|
// 离开会话页必须清掉 in-session, 否则其他页面顶栏被 body 样式隐藏
|
|
6106
6033
|
document.body.classList.toggle('in-session', id === 'view-session')
|
|
@@ -6846,7 +6773,6 @@ function bindUi() {
|
|
|
6846
6773
|
renderPending(); renderQueue(); renderJobs()
|
|
6847
6774
|
updateConn()
|
|
6848
6775
|
if (state.modelSettings.status === 'ready' || state.modelSettings.status === 'error' || state.modelSettings.status === 'loading') renderModelSettings()
|
|
6849
|
-
renderAsrTest()
|
|
6850
6776
|
if (state.current) { renderSessionTitle(); renderSessionSub(); renderSessionCards(); renderHistory(true) }
|
|
6851
6777
|
else renderModelMenu()
|
|
6852
6778
|
loadLocalVersion()
|
|
@@ -7164,13 +7090,6 @@ function bindUi() {
|
|
|
7164
7090
|
$('btn-model-settings-open')?.addEventListener('click', openModelConfigDocument)
|
|
7165
7091
|
$('model-settings-list')?.addEventListener('click', handleModelSettingsClick)
|
|
7166
7092
|
$('model-settings-list')?.addEventListener('input', handleModelSettingsInput)
|
|
7167
|
-
$('btn-asr-test-start')?.addEventListener('click', startAsrTest)
|
|
7168
|
-
$('btn-asr-test-stop')?.addEventListener('click', stopAsrTest)
|
|
7169
|
-
$('btn-asr-test-copy')?.addEventListener('click', copyAsrTestLog)
|
|
7170
|
-
$('btn-asr-test-clear')?.addEventListener('click', clearAsrTest)
|
|
7171
|
-
$('btn-asr-test-permission')?.addEventListener('click', openAsrPermissionSettings)
|
|
7172
|
-
$('btn-asr-test-engine')?.addEventListener('click', openAsrEngineSettings)
|
|
7173
|
-
renderAsrTest()
|
|
7174
7093
|
$('btn-scan-camera').addEventListener('click', () => scanPair('CAMERA'))
|
|
7175
7094
|
$('btn-scan-gallery').addEventListener('click', () => scanPair('PHOTOS'))
|
|
7176
7095
|
$('scan-live-cancel')?.addEventListener('click', () => closeLiveScan(''))
|
|
@@ -528,6 +528,13 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
|
|
|
528
528
|
.ds-workspace-create-location code { min-width: 0; flex: 1; padding: 7px 9px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsr-text); background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 9px; }
|
|
529
529
|
.ds-workspace-name { width: 100%; box-sizing: border-box; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 9px; padding: 8px 10px; font: inherit; outline: none; }
|
|
530
530
|
.ds-workspace-name:focus { border-color: var(--dsr-accent-line); }
|
|
531
|
+
.ds-new-session-desc { margin: 0; color: var(--dsr-muted); line-height: 1.55; }
|
|
532
|
+
.ds-new-session-label { font-size: 12px; color: var(--dsr-muted); }
|
|
533
|
+
.ds-new-session-select { width: 100%; box-sizing: border-box; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 9px; padding: 8px 10px; font: inherit; outline: none; }
|
|
534
|
+
.ds-new-session-select:focus { border-color: var(--dsr-accent-line); }
|
|
535
|
+
.ds-new-session-details { display: flex; flex-direction: column; gap: 4px; min-width: 0; padding: 8px 10px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 9px; }
|
|
536
|
+
.ds-new-session-name { font-size: 13px; font-weight: 600; }
|
|
537
|
+
.ds-new-session-path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsr-muted); font-size: 11.5px; }
|
|
531
538
|
.ds-modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
|
|
532
539
|
.ds-q-item { border: 1px solid var(--dsr-line); border-radius: 10px; padding: 9px 11px; }
|
|
533
540
|
.ds-q-text { font-size: 13.5px; margin-bottom: 6px; }
|