dsh-remote-plugin 0.6.24 → 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 +153 -244
- package/public/desktop/desktop.html +2 -0
- package/public/desktop/desktop.js +50 -2
- package/public/genui.css +2 -0
- package/public/genui.js +144 -0
- package/public/index.html +11 -26
- package/public/md.js +16 -3
- package/public/styles.css +4 -11
- package/public/update.json +8 -8
- 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') {
|
|
@@ -2285,7 +2288,31 @@ function renderSessions() {
|
|
|
2285
2288
|
}
|
|
2286
2289
|
|
|
2287
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
|
+
|
|
2288
2314
|
async function openSession(id) {
|
|
2315
|
+
if (state.current && state.current !== id) await archiveEmptySessionOnLeave(state.current)
|
|
2289
2316
|
state.current = id
|
|
2290
2317
|
setSessionRecovery('loading')
|
|
2291
2318
|
state.history = emptyHistory()
|
|
@@ -2328,7 +2355,7 @@ async function closeSession() {
|
|
|
2328
2355
|
const sessionId = state.current
|
|
2329
2356
|
if (!sessionId) return
|
|
2330
2357
|
const task = (async () => {
|
|
2331
|
-
const discard = await shouldDiscardEmptySession(sessionId)
|
|
2358
|
+
const discard = await shouldDiscardEmptySession(sessionId) && await archiveEmptySessionOnLeave(sessionId)
|
|
2332
2359
|
if (state.current !== sessionId) return
|
|
2333
2360
|
state.current = null
|
|
2334
2361
|
renderSessionPending()
|
|
@@ -2442,6 +2469,13 @@ function reasoningStreamKey(data, index) {
|
|
|
2442
2469
|
* DSH 的实时思考以 assistant/chunk 下发,历史尾页可能把增量压成
|
|
2443
2470
|
* reasoning-chunks。最终 assistant/message 到达后再由正式消息接管展示。
|
|
2444
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
|
+
|
|
2445
2479
|
function applyReasoningStreamEvent(event) {
|
|
2446
2480
|
const h = state.history
|
|
2447
2481
|
const data = event?.data || {}
|
|
@@ -2470,7 +2504,7 @@ function applyReasoningStreamEvent(event) {
|
|
|
2470
2504
|
item.text += Array.isArray(data.texts) ? data.texts.join('') : String(data.text || '')
|
|
2471
2505
|
h.partialReasoning.set(key, item)
|
|
2472
2506
|
changed = true
|
|
2473
|
-
} else if (event?.type === 'assistant/message') {
|
|
2507
|
+
} else if (event?.type === 'assistant/message' || event?.type === 'assistant/attempt') {
|
|
2474
2508
|
for (const [key, item] of h.partialReasoning) {
|
|
2475
2509
|
if (item.turn === data.turn && item.step === data.step) {
|
|
2476
2510
|
h.partialReasoning.delete(key)
|
|
@@ -2560,6 +2594,7 @@ async function loadHistory(reset) {
|
|
|
2560
2594
|
const id = state.current
|
|
2561
2595
|
if (!id || state.history.loading) return
|
|
2562
2596
|
const history = state.history
|
|
2597
|
+
const reasoningVersion = history.reasoningVersion || 0
|
|
2563
2598
|
history.loading = true
|
|
2564
2599
|
if (reset) setSessionRecovery('loading')
|
|
2565
2600
|
const moreBtn = $('history-more')
|
|
@@ -2593,6 +2628,7 @@ async function loadHistory(reset) {
|
|
|
2593
2628
|
}
|
|
2594
2629
|
|
|
2595
2630
|
if (state.current !== id || state.history !== history) return
|
|
2631
|
+
const liveReasoning = (history.reasoningVersion || 0) !== reasoningVersion ? new Map(history.partialReasoning) : null
|
|
2596
2632
|
hydrateSessionProjections(id, v.projections)
|
|
2597
2633
|
history.loaded = true
|
|
2598
2634
|
const incoming = v.events || []
|
|
@@ -2610,6 +2646,8 @@ async function loadHistory(reset) {
|
|
|
2610
2646
|
added++
|
|
2611
2647
|
}
|
|
2612
2648
|
// 向前翻页游标 = 本页最旧的 raw seq(即使它本身被过滤)
|
|
2649
|
+
if (liveReasoning) history.partialReasoning = liveReasoning
|
|
2650
|
+
else applyReasoningBaseline(v.partialReasoning)
|
|
2613
2651
|
const firstSeq = incoming[0]?.event?.seq
|
|
2614
2652
|
if (firstSeq != null) state.history.minSeq = Math.min(state.history.minSeq, firstSeq)
|
|
2615
2653
|
state.history.visible.sort((a, b) => a.seq - b.seq)
|
|
@@ -2850,6 +2888,7 @@ function blockHtml(b) {
|
|
|
2850
2888
|
if ((b.type === 'tool-call' || b.type === 'tool-result') && LS.get('showTools', '1') === '0') return ''
|
|
2851
2889
|
switch (b.type) {
|
|
2852
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>`
|
|
2853
2892
|
case 'image': return `<img alt="${t('block.image')}" src="data:${esc(b.mediaType || 'image/png')};base64,${esc(b.data || '')}">`
|
|
2854
2893
|
case 'thinking':
|
|
2855
2894
|
case 'reasoning':
|
|
@@ -4163,9 +4202,11 @@ async function openFsPreview(pathValue, name) {
|
|
|
4163
4202
|
$('file-preview-loading').classList.add('hidden')
|
|
4164
4203
|
$('file-preview-source').textContent = data.content || ''
|
|
4165
4204
|
const markdown = data.extension === '.md' || data.extension === '.markdown'
|
|
4166
|
-
|
|
4205
|
+
const htmlPreview = (data.extension === '.html' || data.extension === '.htm') && !!window.DshGenUi
|
|
4206
|
+
$('file-preview-tabs').classList.toggle('hidden', !markdown && !htmlPreview)
|
|
4167
4207
|
if (markdown) $('file-preview-rendered').innerHTML = window.mdToHtml(data.content || '')
|
|
4168
|
-
|
|
4208
|
+
if (htmlPreview) $('file-preview-rendered').innerHTML = window.DshGenUi.html(data.content || '')
|
|
4209
|
+
showFsPreviewMode(markdown || htmlPreview ? 'rendered' : 'source')
|
|
4169
4210
|
} catch (e) {
|
|
4170
4211
|
if (generation !== fsPreviewGeneration) return
|
|
4171
4212
|
$('file-preview-loading').textContent = e.message || t('fs.previewFailed', { msg: t('fs.networkError') })
|
|
@@ -5111,77 +5152,123 @@ async function sha256Hex(buffer) {
|
|
|
5111
5152
|
return [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, '0')).join('')
|
|
5112
5153
|
}
|
|
5113
5154
|
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
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
|
+
|
|
5119
5185
|
async function verifyUpdateApk(info, url) {
|
|
5120
|
-
const
|
|
5121
|
-
|
|
5122
|
-
let
|
|
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) }
|
|
5123
5191
|
try {
|
|
5124
|
-
|
|
5125
|
-
res =
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
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) }
|
|
5144
5224
|
}
|
|
5145
5225
|
|
|
5146
5226
|
async function downloadUpdate() {
|
|
5147
5227
|
const info = state.updateInfo
|
|
5148
|
-
if (!info) return
|
|
5149
|
-
const
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
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() }
|
|
5164
5247
|
return
|
|
5165
5248
|
}
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
if (window.NativeUpdate?.downloadAndInstall) {
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
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 })
|
|
5177
5266
|
}
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
}
|
|
5181
|
-
// 浏览器: 直接触发下载
|
|
5182
|
-
location.href = url
|
|
5267
|
+
} catch (error) { updateDownloadProgress({ phase: 'error', error: error.message }) }
|
|
5268
|
+
finally { finishUpdateDownload() }
|
|
5183
5269
|
}
|
|
5184
5270
|
|
|
5271
|
+
|
|
5185
5272
|
/* ---------------- 通知 ---------------- */
|
|
5186
5273
|
const CAP = window.Capacitor || null
|
|
5187
5274
|
async function ensureNotify() {
|
|
@@ -5399,177 +5486,6 @@ async function restorePeakReminders() {
|
|
|
5399
5486
|
if (peakRemindOn() && legacyCleaned) await schedulePeakReminders({ legacyCleaned: true })
|
|
5400
5487
|
}
|
|
5401
5488
|
|
|
5402
|
-
/* ---------------- 功能测试 / Android ASR ---------------- */
|
|
5403
|
-
function asrTestBridge() { return window.NativeAsrTest }
|
|
5404
|
-
|
|
5405
|
-
function emptyAsrTest() {
|
|
5406
|
-
return { running: false, status: 'idle', meta: null, summary: null, events: [], lastError: '' }
|
|
5407
|
-
}
|
|
5408
|
-
|
|
5409
|
-
function asrTestEvent(event) {
|
|
5410
|
-
if (!event || typeof event !== 'object') return
|
|
5411
|
-
const current = state.asrTest
|
|
5412
|
-
const data = event.data && typeof event.data === 'object' ? event.data : {}
|
|
5413
|
-
if (event.type === 'meta') current.meta = data
|
|
5414
|
-
if (event.type === 'summary') {
|
|
5415
|
-
current.summary = data
|
|
5416
|
-
current.running = false
|
|
5417
|
-
}
|
|
5418
|
-
if (event.type === 'status') {
|
|
5419
|
-
current.status = String(data.status || 'unknown')
|
|
5420
|
-
if (current.status === 'listening' || current.status === 'starting' || current.status === 'restarting') current.running = true
|
|
5421
|
-
if (['stopped', 'unsupported', 'permission-denied'].includes(current.status)) current.running = false
|
|
5422
|
-
}
|
|
5423
|
-
if (event.type === 'error') current.lastError = String(data.name || data.message || 'error')
|
|
5424
|
-
current.events.push({ type: event.type, atMs: Number(event.atMs) || 0, data })
|
|
5425
|
-
if (current.events.length > 500) current.events.splice(0, current.events.length - 500)
|
|
5426
|
-
renderAsrTest()
|
|
5427
|
-
}
|
|
5428
|
-
window.__dshAsrEvent = asrTestEvent
|
|
5429
|
-
|
|
5430
|
-
function asrTestStatusText(status) {
|
|
5431
|
-
const labels = {
|
|
5432
|
-
idle: t('settings.asrTestNativeOnly'),
|
|
5433
|
-
starting: t('settings.asrTestStarted'),
|
|
5434
|
-
listening: t('settings.asrTestStarted'),
|
|
5435
|
-
restarting: t('settings.asrTestRestarting'),
|
|
5436
|
-
'permission-requesting': t('settings.asrTestPermission'),
|
|
5437
|
-
'permission-denied': t('settings.asrTestPermissionDenied'),
|
|
5438
|
-
'permission-error': t('settings.asrTestPermissionError'),
|
|
5439
|
-
unsupported: t('settings.asrTestUnavailable'),
|
|
5440
|
-
busy: t('settings.asrTestBusy'),
|
|
5441
|
-
stopped: t('settings.asrTestStopped')
|
|
5442
|
-
}
|
|
5443
|
-
return labels[status] || t('settings.asrTestStatus', { status })
|
|
5444
|
-
}
|
|
5445
|
-
|
|
5446
|
-
function asrTestLogLines() {
|
|
5447
|
-
const current = state.asrTest
|
|
5448
|
-
const lines = []
|
|
5449
|
-
for (const event of current.events) {
|
|
5450
|
-
const data = event.data || {}
|
|
5451
|
-
const at = `${event.atMs}ms`
|
|
5452
|
-
if (event.type === 'meta') {
|
|
5453
|
-
lines.push(`[${at}] meta brand=${data.brand || '—'} manufacturer=${data.manufacturer || '—'} model=${data.model || '—'} Android=${data.androidVersion || '—'} API=${data.apiLevel || '—'}`)
|
|
5454
|
-
lines.push(`[${at}] recordAudioPermission=${data.recordAudioPermission ?? 'unknown'} recordAudioAppOp=${data.recordAudioAppOp || 'unknown'} microphoneMuted=${data.microphoneMuted ?? 'unknown'}`)
|
|
5455
|
-
lines.push(`[${at}] recognitionAvailable=${data.recognitionAvailable === true} onDeviceAvailable=${data.onDeviceAvailable === true} path=${data.networkPath || '—'}`)
|
|
5456
|
-
for (const service of data.recognitionServices || []) lines.push(`[${at}] service ${service.packageName || '—'} / ${service.serviceName || '—'} xiaomiLike=${service.xiaomiLike === true}`)
|
|
5457
|
-
} else if (event.type === 'status') {
|
|
5458
|
-
lines.push(`[${at}] status=${data.status || '—'} session=${data.session ?? '—'} reason=${data.reason || '—'} ${data.message || ''}`.trim())
|
|
5459
|
-
} else if (event.type === 'partial' || event.type === 'final') {
|
|
5460
|
-
lines.push(`[${at}] ${event.type}#${data.count ?? '—'} session=${data.session ?? '—'} +${data.elapsedMs ?? '—'}ms: ${data.text || '(empty)'}`)
|
|
5461
|
-
} else if (event.type === 'callback') {
|
|
5462
|
-
lines.push(`[${at}] callback=${data.name || '—'} session=${data.session ?? '—'} +${data.elapsedMs ?? '—'}ms${data.bytes >= 0 ? ` bytes=${data.bytes}` : ''}`)
|
|
5463
|
-
} else if (event.type === 'error') {
|
|
5464
|
-
lines.push(`[${at}] error=${data.name || '—'} code=${data.code ?? '—'} session=${data.session ?? '—'} ${data.message || ''}`.trim())
|
|
5465
|
-
} else if (event.type === 'summary') {
|
|
5466
|
-
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 ?? '—'}`)
|
|
5467
|
-
}
|
|
5468
|
-
}
|
|
5469
|
-
return lines
|
|
5470
|
-
}
|
|
5471
|
-
|
|
5472
|
-
function asrTestReport() {
|
|
5473
|
-
const current = state.asrTest
|
|
5474
|
-
const meta = current.meta || {}
|
|
5475
|
-
const summary = current.summary || {}
|
|
5476
|
-
const lines = [
|
|
5477
|
-
'DSH Remote Android ASR 测试报告',
|
|
5478
|
-
`生成时间: ${new Date().toISOString()}`,
|
|
5479
|
-
`设备: ${meta.brand || '—'} / ${meta.manufacturer || '—'} / ${meta.model || '—'}`,
|
|
5480
|
-
`Android: ${meta.androidVersion || '—'} (API ${meta.apiLevel || '—'})`,
|
|
5481
|
-
`识别可用: ${meta.recognitionAvailable === true ? 'yes' : meta.recognitionAvailable === false ? 'no' : 'unknown'}`,
|
|
5482
|
-
`端侧识别可用: ${meta.onDeviceAvailable === true ? 'yes' : meta.onDeviceAvailable === false ? 'no' : 'unknown'}`,
|
|
5483
|
-
`路径: ${meta.networkPath || 'system-default-recognition-service'}`,
|
|
5484
|
-
`测试结束原因: ${summary.reason || current.status || '—'}`,
|
|
5485
|
-
`总时长: ${summary.durationMs ?? '—'}ms`,
|
|
5486
|
-
`session: ${summary.sessionCount ?? '—'} / 重建: ${summary.restartCount ?? '—'} / partial: ${summary.partialCount ?? '—'} / final: ${summary.finalCount ?? '—'} / errors: ${summary.errorCount ?? '—'}`,
|
|
5487
|
-
'',
|
|
5488
|
-
'事件日志:',
|
|
5489
|
-
...asrTestLogLines()
|
|
5490
|
-
]
|
|
5491
|
-
return lines.join('\n')
|
|
5492
|
-
}
|
|
5493
|
-
|
|
5494
|
-
function renderAsrTest() {
|
|
5495
|
-
const start = $('btn-asr-test-start')
|
|
5496
|
-
const stop = $('btn-asr-test-stop')
|
|
5497
|
-
const copy = $('btn-asr-test-copy')
|
|
5498
|
-
const permission = $('btn-asr-test-permission')
|
|
5499
|
-
const engine = $('btn-asr-test-engine')
|
|
5500
|
-
const status = $('asr-test-status')
|
|
5501
|
-
const summary = $('asr-test-summary')
|
|
5502
|
-
const log = $('asr-test-log')
|
|
5503
|
-
if (!start || !stop || !copy || !permission || !engine || !status || !summary || !log) return
|
|
5504
|
-
const current = state.asrTest
|
|
5505
|
-
const native = !!(CAP?.isNativePlatform?.() && asrTestBridge()?.startAsrTest)
|
|
5506
|
-
start.disabled = current.running || !native
|
|
5507
|
-
stop.disabled = !current.running || !native
|
|
5508
|
-
copy.disabled = !current.events.length
|
|
5509
|
-
const permissionError = current.status === 'permission-error' || current.status === 'permission-denied' || current.summary?.reason === 'permission-error'
|
|
5510
|
-
status.className = 'feature-test-status ' + (permissionError || current.status === 'unsupported' ? 'error' : current.status === 'stopped' ? 'ok' : 'muted')
|
|
5511
|
-
status.textContent = native ? (permissionError ? t('settings.asrTestPermissionError') : asrTestStatusText(current.status)) : t('settings.asrTestWebUnsupported')
|
|
5512
|
-
permission.classList.toggle('hidden', !native || !permissionError)
|
|
5513
|
-
engine.classList.toggle('hidden', !native || !permissionError)
|
|
5514
|
-
const meta = current.meta || {}
|
|
5515
|
-
const s = current.summary
|
|
5516
|
-
summary.textContent = [
|
|
5517
|
-
meta.model ? `${t('settings.asrTestMeta')}: ${meta.brand || '—'} / ${meta.manufacturer || '—'} / ${meta.model}` : '',
|
|
5518
|
-
s ? `${t('settings.asrTestSummary')}: ${t('settings.asrTestStatus', { status: s.reason || 'done' })} · session ${s.sessionCount ?? '—'} · partial ${s.partialCount ?? '—'} · final ${s.finalCount ?? '—'} · error ${s.errorCount ?? '—'}` : ''
|
|
5519
|
-
].filter(Boolean).join('\n')
|
|
5520
|
-
log.textContent = current.events.length ? asrTestLogLines().join('\n') : t('settings.asrTestLogEmpty')
|
|
5521
|
-
log.scrollTop = log.scrollHeight
|
|
5522
|
-
}
|
|
5523
|
-
|
|
5524
|
-
function clearAsrTest() {
|
|
5525
|
-
if (state.asrTest.running) return toast(t('settings.asrTestBusy'), 'err')
|
|
5526
|
-
state.asrTest = emptyAsrTest()
|
|
5527
|
-
renderAsrTest()
|
|
5528
|
-
}
|
|
5529
|
-
|
|
5530
|
-
async function startAsrTest() {
|
|
5531
|
-
const native = asrTestBridge()
|
|
5532
|
-
if (!CAP?.isNativePlatform?.() || !native?.startAsrTest) return toast(t('settings.asrTestWebUnsupported'), 'err')
|
|
5533
|
-
if (state.asrTest.running) return toast(t('settings.asrTestBusy'), 'err')
|
|
5534
|
-
if (!confirm(t('settings.asrTestConsent'))) return
|
|
5535
|
-
state.asrTest = { ...emptyAsrTest(), running: true, status: 'starting' }
|
|
5536
|
-
renderAsrTest()
|
|
5537
|
-
try {
|
|
5538
|
-
if (native.startAsrTest() === false) throw new Error(t('settings.asrTestUnavailable'))
|
|
5539
|
-
} catch (error) {
|
|
5540
|
-
state.asrTest.running = false
|
|
5541
|
-
state.asrTest.status = 'error'
|
|
5542
|
-
state.asrTest.lastError = error?.message || String(error)
|
|
5543
|
-
renderAsrTest()
|
|
5544
|
-
toast(state.asrTest.lastError, 'err')
|
|
5545
|
-
}
|
|
5546
|
-
}
|
|
5547
|
-
|
|
5548
|
-
function stopAsrTest() {
|
|
5549
|
-
try { asrTestBridge()?.stopAsrTest?.() } catch {}
|
|
5550
|
-
}
|
|
5551
|
-
|
|
5552
|
-
function openAsrPermissionSettings() {
|
|
5553
|
-
try {
|
|
5554
|
-
if (asrTestBridge()?.openAsrPermissionSettings?.() === false) throw new Error('permission settings unavailable')
|
|
5555
|
-
} catch (error) {
|
|
5556
|
-
toast(error?.message || String(error), 'err')
|
|
5557
|
-
}
|
|
5558
|
-
}
|
|
5559
|
-
|
|
5560
|
-
function openAsrEngineSettings() {
|
|
5561
|
-
try {
|
|
5562
|
-
if (asrTestBridge()?.openAsrEngineSettings?.() === false) throw new Error('voice engine settings unavailable')
|
|
5563
|
-
} catch (error) {
|
|
5564
|
-
toast(error?.message || String(error), 'err')
|
|
5565
|
-
}
|
|
5566
|
-
}
|
|
5567
|
-
|
|
5568
|
-
async function copyAsrTestLog() {
|
|
5569
|
-
const ok = await copyText(asrTestReport())
|
|
5570
|
-
toast(t(ok ? 'settings.asrTestCopyOk' : 'settings.asrTestCopyFailed'), ok ? 'ok' : 'err')
|
|
5571
|
-
}
|
|
5572
|
-
|
|
5573
5489
|
/* ---------------- 模型设置 ---------------- */
|
|
5574
5490
|
const MODEL_SETTINGS_FIELDS = ['baseURL', 'api', 'apiKeyEnv', 'displayName', 'models']
|
|
5575
5491
|
const MODEL_REASONING_LIMIT = 12
|
|
@@ -6111,6 +6027,7 @@ async function openModelConfigDocument() {
|
|
|
6111
6027
|
}
|
|
6112
6028
|
/* ---------------- 视图切换 ---------------- */
|
|
6113
6029
|
function showView(id) {
|
|
6030
|
+
if (id !== 'view-session' && document.body.classList.contains('in-session') && state.current) void archiveEmptySessionOnLeave(state.current)
|
|
6114
6031
|
for (const v of ['view-home', 'view-files', 'view-session', 'view-activity', 'view-stats', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
6115
6032
|
// 离开会话页必须清掉 in-session, 否则其他页面顶栏被 body 样式隐藏
|
|
6116
6033
|
document.body.classList.toggle('in-session', id === 'view-session')
|
|
@@ -6856,7 +6773,6 @@ function bindUi() {
|
|
|
6856
6773
|
renderPending(); renderQueue(); renderJobs()
|
|
6857
6774
|
updateConn()
|
|
6858
6775
|
if (state.modelSettings.status === 'ready' || state.modelSettings.status === 'error' || state.modelSettings.status === 'loading') renderModelSettings()
|
|
6859
|
-
renderAsrTest()
|
|
6860
6776
|
if (state.current) { renderSessionTitle(); renderSessionSub(); renderSessionCards(); renderHistory(true) }
|
|
6861
6777
|
else renderModelMenu()
|
|
6862
6778
|
loadLocalVersion()
|
|
@@ -7174,13 +7090,6 @@ function bindUi() {
|
|
|
7174
7090
|
$('btn-model-settings-open')?.addEventListener('click', openModelConfigDocument)
|
|
7175
7091
|
$('model-settings-list')?.addEventListener('click', handleModelSettingsClick)
|
|
7176
7092
|
$('model-settings-list')?.addEventListener('input', handleModelSettingsInput)
|
|
7177
|
-
$('btn-asr-test-start')?.addEventListener('click', startAsrTest)
|
|
7178
|
-
$('btn-asr-test-stop')?.addEventListener('click', stopAsrTest)
|
|
7179
|
-
$('btn-asr-test-copy')?.addEventListener('click', copyAsrTestLog)
|
|
7180
|
-
$('btn-asr-test-clear')?.addEventListener('click', clearAsrTest)
|
|
7181
|
-
$('btn-asr-test-permission')?.addEventListener('click', openAsrPermissionSettings)
|
|
7182
|
-
$('btn-asr-test-engine')?.addEventListener('click', openAsrEngineSettings)
|
|
7183
|
-
renderAsrTest()
|
|
7184
7093
|
$('btn-scan-camera').addEventListener('click', () => scanPair('CAMERA'))
|
|
7185
7094
|
$('btn-scan-gallery').addEventListener('click', () => scanPair('PHOTOS'))
|
|
7186
7095
|
$('scan-live-cancel')?.addEventListener('click', () => closeLiveScan(''))
|
|
@@ -703,6 +703,8 @@
|
|
|
703
703
|
}
|
|
704
704
|
</script>
|
|
705
705
|
<script src="i18n.js"></script>
|
|
706
|
+
<link rel="stylesheet" href="../genui.css">
|
|
707
|
+
<script src="../genui.js"></script>
|
|
706
708
|
<script src="../md.js"></script>
|
|
707
709
|
<script src="../vendor/gsap/gsap.min.js"></script>
|
|
708
710
|
<script src="../motion.js"></script>
|