dsh-remote-plugin 0.5.0 → 0.5.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/client.js +0 -12
- package/gateway.cjs +41 -4
- package/package.json +1 -1
- package/public/admin.html +179 -29
- package/public/admin.js +102 -66
- package/public/app.js +427 -209
- package/public/i18n.js +52 -0
- package/public/index.html +321 -56
- package/public/jsqr.min.js +10104 -0
- package/public/styles.css +58 -6
- package/public/update.json +3 -3
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/* DSH Remote 移动控制台 · 零依赖 */
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
+
const I18N = window.I18N
|
|
5
|
+
const t = (k, v) => I18N.t(k, v)
|
|
6
|
+
I18N.init(window.APP_STR)
|
|
7
|
+
|
|
4
8
|
/* ---------------- 状态 ---------------- */
|
|
5
9
|
const LS = {
|
|
6
10
|
get(k, d) { try { return localStorage.getItem(k) ?? d } catch { return d } },
|
|
@@ -50,7 +54,8 @@ const state = {
|
|
|
50
54
|
history: emptyHistory(),
|
|
51
55
|
errCount: 0,
|
|
52
56
|
refreshTimer: null,
|
|
53
|
-
fs: { path: null, initial: null, loaded: false, upload: null }
|
|
57
|
+
fs: { path: null, initial: null, loaded: false, upload: null },
|
|
58
|
+
models: { loaded: false, loading: false, groups: [], current: null, failures: [] }
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
const $ = (id) => document.getElementById(id)
|
|
@@ -70,9 +75,9 @@ function toast(text, kind = '') {
|
|
|
70
75
|
function fmtTime(ts) {
|
|
71
76
|
if (!ts) return ''
|
|
72
77
|
const diff = Date.now() - ts
|
|
73
|
-
if (diff < 60e3) return '
|
|
74
|
-
if (diff < 3600e3) return Math.floor(diff / 60e3) + '
|
|
75
|
-
if (diff < 86400e3) return Math.floor(diff / 3600e3) + '
|
|
78
|
+
if (diff < 60e3) return t('time.justNow')
|
|
79
|
+
if (diff < 3600e3) return Math.floor(diff / 60e3) + t('time.minAgo')
|
|
80
|
+
if (diff < 86400e3) return Math.floor(diff / 3600e3) + t('time.hourAgo')
|
|
76
81
|
const d = new Date(ts)
|
|
77
82
|
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
|
78
83
|
}
|
|
@@ -112,10 +117,10 @@ async function rpc(method, payload = {}) {
|
|
|
112
117
|
if (res.status === 401) throw new Error('AUTH')
|
|
113
118
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
114
119
|
const full = await res.json()
|
|
115
|
-
if (!full?.result) throw new Error('
|
|
120
|
+
if (!full?.result) throw new Error(t('err.badResponse'))
|
|
116
121
|
if (!full.result.ok) {
|
|
117
122
|
const err = full.result.error || {}
|
|
118
|
-
throw new Error(err.message || '
|
|
123
|
+
throw new Error(err.message || t('err.dshError'))
|
|
119
124
|
}
|
|
120
125
|
return full.result.value
|
|
121
126
|
}
|
|
@@ -141,9 +146,9 @@ async function safeRpc(method, payload, errText) {
|
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
function authFailure() {
|
|
144
|
-
toast('
|
|
149
|
+
toast(t('err.accessDenied'), 'err')
|
|
145
150
|
showView('view-settings')
|
|
146
|
-
$('token-desc').textContent = '
|
|
151
|
+
$('token-desc').textContent = t('token.invalid')
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
/* ---------------- 多服务器 + 自动选优 ---------------- */
|
|
@@ -194,7 +199,7 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
|
194
199
|
if (state.selectingServer) return null
|
|
195
200
|
state.selectingServer = true
|
|
196
201
|
try {
|
|
197
|
-
if (!silent) toast('
|
|
202
|
+
if (!silent) toast(t('speed.testing'))
|
|
198
203
|
const candidates = serverCandidates()
|
|
199
204
|
for (const u of candidates) state.serverLatency[u] = await pingServer(u)
|
|
200
205
|
const best = candidates
|
|
@@ -207,11 +212,11 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
|
207
212
|
if (chosen !== state.server) {
|
|
208
213
|
state.server = chosen
|
|
209
214
|
saveServers()
|
|
210
|
-
if (!silent) toast(
|
|
215
|
+
if (!silent) toast(t('speed.switched', { url: chosen || t('speed.origin'), ms: state.serverLatency[best] }), 'ok')
|
|
211
216
|
if (reconnect && state.token) { openStreams(); refreshAll() }
|
|
212
217
|
} else if (!silent) {
|
|
213
|
-
if (best) toast(
|
|
214
|
-
else toast('
|
|
218
|
+
if (best) toast(t('speed.alreadyBest', { url: chosen || t('speed.origin'), ms: state.serverLatency[best] }), 'ok')
|
|
219
|
+
else toast(t('speed.allDown'), 'err')
|
|
215
220
|
}
|
|
216
221
|
return chosen
|
|
217
222
|
} finally {
|
|
@@ -223,41 +228,41 @@ function renderServers() {
|
|
|
223
228
|
const box = $('server-list')
|
|
224
229
|
if (!box) return
|
|
225
230
|
if (!state.servers.length) {
|
|
226
|
-
box.innerHTML = '<div class="server-empty"
|
|
231
|
+
box.innerHTML = '<div class="server-empty">' + t('servers.empty') + '</div>'
|
|
227
232
|
} else {
|
|
228
233
|
box.innerHTML = state.servers.map(u => {
|
|
229
234
|
const ms = state.serverLatency[u]
|
|
230
|
-
let badge = '<span class="server-badge"
|
|
231
|
-
if (Number.isFinite(ms)) badge = `<span class="server-badge ${u === state.server ? 'good' : ''}">${ms}ms${u === state.server ? '
|
|
232
|
-
else if (ms !== undefined) badge = '<span class="server-badge bad"
|
|
235
|
+
let badge = '<span class="server-badge">' + t('servers.untested') + '</span>'
|
|
236
|
+
if (Number.isFinite(ms)) badge = `<span class="server-badge ${u === state.server ? 'good' : ''}">${ms}ms${u === state.server ? t('servers.current') : ''}</span>`
|
|
237
|
+
else if (ms !== undefined) badge = '<span class="server-badge bad">' + t('servers.unreachable') + '</span>'
|
|
233
238
|
return `<div class="server-row ${u === state.server ? 'active' : ''}">
|
|
234
239
|
<span class="server-url">${esc(u)}</span>${badge}
|
|
235
|
-
<button class="server-del" data-del="${esc(u)}" aria-label="
|
|
240
|
+
<button class="server-del" data-del="${esc(u)}" aria-label="${t('servers.delete')}">✕</button>
|
|
236
241
|
</div>`
|
|
237
242
|
}).join('')
|
|
238
243
|
box.querySelectorAll('[data-del]').forEach(b => b.addEventListener('click', () => removeServer(b.dataset.del)))
|
|
239
244
|
}
|
|
240
245
|
$('server-desc').textContent = state.server
|
|
241
|
-
?
|
|
242
|
-
: (CAP?.isNativePlatform?.() ? '
|
|
246
|
+
? t('servers.currentDesc', { url: state.server })
|
|
247
|
+
: (CAP?.isNativePlatform?.() ? t('servers.notSet') : t('servers.defaultDesc'))
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
async function addServer() {
|
|
246
251
|
const input = $('server-input')
|
|
247
252
|
let raw = (input?.value || '').trim().replace(/\/+$/, '')
|
|
248
|
-
if (!raw) return toast('
|
|
253
|
+
if (!raw) return toast(t('servers.needAddress'), 'err')
|
|
249
254
|
try {
|
|
250
255
|
const u = new URL(raw)
|
|
251
256
|
if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad')
|
|
252
257
|
} catch {
|
|
253
|
-
return toast('
|
|
258
|
+
return toast(t('servers.badProtocol'), 'err')
|
|
254
259
|
}
|
|
255
|
-
if (state.servers.includes(raw)) return toast('
|
|
260
|
+
if (state.servers.includes(raw)) return toast(t('servers.duplicate'))
|
|
256
261
|
state.servers.push(raw)
|
|
257
262
|
saveServers()
|
|
258
263
|
if (input) input.value = ''
|
|
259
264
|
renderServers()
|
|
260
|
-
toast('
|
|
265
|
+
toast(t('servers.added'), 'ok')
|
|
261
266
|
if (state.token) selectFastestServer({ silent: false })
|
|
262
267
|
}
|
|
263
268
|
|
|
@@ -268,7 +273,7 @@ function removeServer(url) {
|
|
|
268
273
|
saveServers()
|
|
269
274
|
renderServers()
|
|
270
275
|
if (wasActive) {
|
|
271
|
-
toast('
|
|
276
|
+
toast(t('servers.removedActive'))
|
|
272
277
|
selectFastestServer({ silent: true })
|
|
273
278
|
}
|
|
274
279
|
}
|
|
@@ -278,11 +283,13 @@ const streams = {}
|
|
|
278
283
|
state.streamsOk = { mux: false, host: false }
|
|
279
284
|
|
|
280
285
|
function openStreams() {
|
|
286
|
+
if (!state.token) return
|
|
281
287
|
openStream('mux', onMuxFrame, true)
|
|
282
288
|
openStream('host', onHostFrame, false)
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
function openStream(kind, handler, refreshOnOpen) {
|
|
292
|
+
if (!state.token) return
|
|
286
293
|
let base
|
|
287
294
|
if (state.server) {
|
|
288
295
|
base = state.server.replace(/^http/, 'ws')
|
|
@@ -298,6 +305,13 @@ function openStream(kind, handler, refreshOnOpen) {
|
|
|
298
305
|
state.streamsOk[kind] = true
|
|
299
306
|
state.errCount = 0
|
|
300
307
|
updateConn()
|
|
308
|
+
// mux 每次(重)连接都会重放“仍待处理”的审批/提问基线:
|
|
309
|
+
// 先清空旧列表, 避免“桌面已自定义回答, 手机漏收 question/resolved”后永久残留。
|
|
310
|
+
if (kind === 'mux') {
|
|
311
|
+
state.approvals = []
|
|
312
|
+
state.questions = []
|
|
313
|
+
renderPending()
|
|
314
|
+
}
|
|
301
315
|
if (refreshOnOpen) refreshAll()
|
|
302
316
|
}
|
|
303
317
|
ws.onmessage = (msg) => {
|
|
@@ -313,7 +327,7 @@ function openStream(kind, handler, refreshOnOpen) {
|
|
|
313
327
|
state.streamsOk[kind] = false
|
|
314
328
|
state.errCount++
|
|
315
329
|
updateConn()
|
|
316
|
-
if (state.errCount === 3) toast('
|
|
330
|
+
if (state.errCount === 3) toast(t('conn.reconnecting'), 'err')
|
|
317
331
|
// 多服务器: 连续掉线若干次就重测速, 自动换到当前可达的最快地址
|
|
318
332
|
if (state.servers.length && state.errCount % 5 === 0) setTimeout(() => selectFastestServer({ silent: true }), 300)
|
|
319
333
|
// 无条件重连; 页面被挂起时定时器暂停, visibilitychange 会再触发一次
|
|
@@ -343,12 +357,12 @@ document.addEventListener('visibilitychange', () => {
|
|
|
343
357
|
if (document.visibilityState === 'visible') {
|
|
344
358
|
onResume()
|
|
345
359
|
if (state.servers.length) selectFastestServer({ silent: true })
|
|
346
|
-
else if (streams.mux?.readyState !== WebSocket.OPEN || streams.host?.readyState !== WebSocket.OPEN) openStreams()
|
|
360
|
+
else if (state.token && (streams.mux?.readyState !== WebSocket.OPEN || streams.host?.readyState !== WebSocket.OPEN)) openStreams()
|
|
347
361
|
}
|
|
348
362
|
})
|
|
349
363
|
window.addEventListener('pageshow', onResume)
|
|
350
364
|
setInterval(() => {
|
|
351
|
-
if (document.visibilityState === 'visible') {
|
|
365
|
+
if (document.visibilityState === 'visible' && state.token) {
|
|
352
366
|
if (streams.mux?.readyState !== WebSocket.OPEN || streams.host?.readyState !== WebSocket.OPEN) openStreams()
|
|
353
367
|
}
|
|
354
368
|
}, 15000)
|
|
@@ -364,21 +378,21 @@ function onMuxFrame(full) {
|
|
|
364
378
|
if (f.type === 'approval/requested') {
|
|
365
379
|
state.approvals = state.approvals.filter(a => a.approvalId !== f.approvalId)
|
|
366
380
|
state.approvals.push({ ...f, rpcId: full.rpcId })
|
|
367
|
-
notify('
|
|
381
|
+
notify(t('notify.approvalTitle'), t('notify.approvalBody', { tool: f.toolName || t('tool.unknown') }))
|
|
368
382
|
renderPending(); return
|
|
369
383
|
}
|
|
370
384
|
if (f.type === 'approval/resolved') { state.approvals = state.approvals.filter(a => a.approvalId !== f.approvalId); renderPending(); return }
|
|
371
385
|
if (f.type === 'question/requested') {
|
|
372
386
|
state.questions = state.questions.filter(q => q.rpcId !== full.rpcId)
|
|
373
387
|
state.questions.push({ ...f, rpcId: full.rpcId })
|
|
374
|
-
notify('
|
|
388
|
+
notify(t('notify.questionTitle'), f.questions?.map(q => q.question).join(' / ') || t('notify.questionBody'))
|
|
375
389
|
renderPending(); return
|
|
376
390
|
}
|
|
377
391
|
if (f.type === 'question/resolved') { state.questions = state.questions.filter(q => q.rpcId !== f.questionRpcId); renderPending(); return }
|
|
378
392
|
if (f.type === 'session/queue') { state.queues[f.sessionId] = f.items || []; renderQueue(); return }
|
|
379
393
|
if (f.type === 'session/jobs') { state.jobs[f.sessionId] = f.jobs || []; renderJobs(); return }
|
|
380
394
|
if (f.type === 'session/projection') { applyProjection(f.sessionId, f.key, f.value, f.seq); return }
|
|
381
|
-
if (f.type === 'stream/error') { toast('
|
|
395
|
+
if (f.type === 'stream/error') { toast(t('stream.error', { msg: f.error?.message || '' }), 'err') }
|
|
382
396
|
}
|
|
383
397
|
function onHostFrame(full) {
|
|
384
398
|
const f = full.payload
|
|
@@ -386,10 +400,15 @@ function onHostFrame(full) {
|
|
|
386
400
|
if (['host/session-added', 'host/session-removed', 'host/workspace-changed', 'host/workspace-removed', 'host/workspace-order-changed', 'host/archived-sessions-changed'].includes(f.type)) return scheduleRefresh()
|
|
387
401
|
if (f.type === 'host/session-status') {
|
|
388
402
|
const s = state.byId.get(f.sessionId)
|
|
389
|
-
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessionCards(); updateCancelBtn() } }
|
|
403
|
+
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessionCards(); updateCancelBtn(); renderSessionSub(); updateSessionStatus() } }
|
|
390
404
|
return
|
|
391
405
|
}
|
|
392
|
-
if (f.type === 'host/agent-error')
|
|
406
|
+
if (f.type === 'host/agent-error') {
|
|
407
|
+
const s = state.byId.get(f.sessionId)
|
|
408
|
+
if (s) { s.error = true; s.running = false }
|
|
409
|
+
if (state.current === f.sessionId) { renderSessionSub(); updateSessionStatus() }
|
|
410
|
+
return toast(t('session.errorMsg', { msg: f.message }), 'err')
|
|
411
|
+
}
|
|
393
412
|
if (f.type === 'host/remote-event') return scheduleRefresh()
|
|
394
413
|
}
|
|
395
414
|
|
|
@@ -398,8 +417,8 @@ function onSessionEvent(sessionId, event) {
|
|
|
398
417
|
const s = state.byId.get(sessionId)
|
|
399
418
|
if (s) s.updatedAt = Date.now()
|
|
400
419
|
if (event.type === 'agent/status') {
|
|
401
|
-
if (s) { s.running = !!event.data?.running; s.blank = false }
|
|
402
|
-
if (state.current === sessionId) { updateCancelBtn(); renderSessionSub() }
|
|
420
|
+
if (s) { s.running = !!event.data?.running; s.blank = false; if (s.running) s.error = false }
|
|
421
|
+
if (state.current === sessionId) { updateCancelBtn(); renderSessionSub(); updateSessionStatus() }
|
|
403
422
|
}
|
|
404
423
|
if (event.type === 'session/title' || event.type === 'title') {
|
|
405
424
|
if (event.data?.title && s) s.projections.values.title = event.data.title
|
|
@@ -417,12 +436,12 @@ function scheduleRefresh() {
|
|
|
417
436
|
|
|
418
437
|
async function refreshAll() {
|
|
419
438
|
await refreshSessions()
|
|
420
|
-
if (state.current) { renderSessionCards(); renderSessionSub(); updateCancelBtn() }
|
|
439
|
+
if (state.current) { renderSessionCards(); renderSessionSub(); updateCancelBtn(); updateSessionStatus() }
|
|
421
440
|
renderPending(); renderQueue(); renderJobs(); updateConn()
|
|
422
441
|
}
|
|
423
442
|
|
|
424
443
|
async function refreshSessions() {
|
|
425
|
-
const v = await safeRpc('session.list', {}, '
|
|
444
|
+
const v = await safeRpc('session.list', {}, t('err.sessionList'))
|
|
426
445
|
if (!v) {
|
|
427
446
|
// 网关不可达: 用上次成功的会话列表兜底, 用户仍能打开历史缓存
|
|
428
447
|
if (!state.sessions.length) {
|
|
@@ -431,7 +450,7 @@ async function refreshSessions() {
|
|
|
431
450
|
state.sessions = cached
|
|
432
451
|
state.byId = new Map(cached.map(s => [s.sessionId, s]))
|
|
433
452
|
renderSessions()
|
|
434
|
-
toast('
|
|
453
|
+
toast(t('sessions.cacheFallback'), 'ok')
|
|
435
454
|
}
|
|
436
455
|
}
|
|
437
456
|
return
|
|
@@ -479,14 +498,14 @@ function renderSessions() {
|
|
|
479
498
|
const dots = []
|
|
480
499
|
if (s.running) dots.push('running')
|
|
481
500
|
if (pending) dots.push('pending')
|
|
482
|
-
const badge = goal ? `<span class="sc-badge ${goal.phase === 'active' ? 'goal-active' : ''}"
|
|
483
|
-
const queueBadge = queueN ? `<span class="sc-badge"
|
|
501
|
+
const badge = goal ? `<span class="sc-badge ${goal.phase === 'active' ? 'goal-active' : ''}">${esc(t('sessions.goalBadge', { phase: goal.phase || '?' }))}</span>` : ''
|
|
502
|
+
const queueBadge = queueN ? `<span class="sc-badge">${esc(t('sessions.queueBadge', { n: queueN }))}</span>` : ''
|
|
484
503
|
return `<div class="session-card ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
485
504
|
<div class="sc-title">${esc(title)}</div>
|
|
486
505
|
<div class="sc-meta">
|
|
487
506
|
<span class="sc-dot ${dots.join(' ')}"></span>
|
|
488
507
|
<span>${fmtTime(s.updatedAt)}</span>
|
|
489
|
-
${s.running ? '<span
|
|
508
|
+
${s.running ? '<span>' + t('sessions.running') + '</span>' : ''}
|
|
490
509
|
${badge}${queueBadge}
|
|
491
510
|
</div>
|
|
492
511
|
<span class="sc-arrow">›</span>
|
|
@@ -496,9 +515,9 @@ function renderSessions() {
|
|
|
496
515
|
const running = state.sessions.filter(s => s.running).length
|
|
497
516
|
const pending = state.approvals.length + state.questions.length
|
|
498
517
|
$('stat-strip').innerHTML = `
|
|
499
|
-
<div class="stat running"><div class="v">${running}</div><div class="k"
|
|
500
|
-
<div class="stat pending"><div class="v">${pending}</div><div class="k"
|
|
501
|
-
<div class="stat ctx"><div class="v">${items.length}</div><div class="k"
|
|
518
|
+
<div class="stat running"><div class="v">${running}</div><div class="k">${t('sessions.statRunning')}</div></div>
|
|
519
|
+
<div class="stat pending"><div class="v">${pending}</div><div class="k">${t('sessions.statPending')}</div></div>
|
|
520
|
+
<div class="stat ctx"><div class="v">${items.length}</div><div class="k">${t('sessions.statTotal')}</div></div>`
|
|
502
521
|
updatePendingBadge()
|
|
503
522
|
}
|
|
504
523
|
|
|
@@ -508,8 +527,8 @@ async function openSession(id) {
|
|
|
508
527
|
state.history = emptyHistory()
|
|
509
528
|
document.body.classList.add('in-session')
|
|
510
529
|
showView('view-session')
|
|
511
|
-
renderSessionTitle(); renderSessionSub(); updateCancelBtn()
|
|
512
|
-
$('history').innerHTML = '<div class="empty"
|
|
530
|
+
renderSessionTitle(); renderSessionSub(); updateCancelBtn(); updateSessionStatus()
|
|
531
|
+
$('history').innerHTML = '<div class="empty">' + t('history.loading') + '</div>'
|
|
513
532
|
await loadHistory(true)
|
|
514
533
|
renderSessionCards()
|
|
515
534
|
refreshSessions()
|
|
@@ -519,6 +538,7 @@ function closeSession() {
|
|
|
519
538
|
state.current = null
|
|
520
539
|
state.history = emptyHistory()
|
|
521
540
|
document.body.classList.remove('in-session')
|
|
541
|
+
hideComposerMenu()
|
|
522
542
|
showView('view-home')
|
|
523
543
|
}
|
|
524
544
|
|
|
@@ -541,7 +561,7 @@ function bindNativeBack() {
|
|
|
541
561
|
|
|
542
562
|
function renderSessionTitle() {
|
|
543
563
|
const s = state.byId.get(state.current)
|
|
544
|
-
$('session-title').textContent = s ? titleOf(s) : '
|
|
564
|
+
$('session-title').textContent = s ? titleOf(s) : t('session.unknown')
|
|
545
565
|
}
|
|
546
566
|
|
|
547
567
|
function renderSessionSub() {
|
|
@@ -549,10 +569,22 @@ function renderSessionSub() {
|
|
|
549
569
|
if (!s) { $('session-sub').textContent = ''; return }
|
|
550
570
|
const parts = [short(s.sessionId)]
|
|
551
571
|
if (s.cwd) parts.push(s.cwd)
|
|
552
|
-
if (s.running) parts.push('
|
|
572
|
+
if (s.running) parts.push(t('session.running'))
|
|
573
|
+
else if (s.error) parts.push(t('session.interrupted'))
|
|
553
574
|
$('session-sub').textContent = parts.join(' · ')
|
|
554
575
|
}
|
|
555
576
|
|
|
577
|
+
/** 顶栏状态: 运行中=蓝色流动渐变, 中断/出错=橙红渐变, 空闲=原样式 */
|
|
578
|
+
function updateSessionStatus() {
|
|
579
|
+
const s = state.byId.get(state.current)
|
|
580
|
+
const head = $('session-head')
|
|
581
|
+
if (!head) return
|
|
582
|
+
head.classList.remove('running', 'interrupted')
|
|
583
|
+
const queued = (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
584
|
+
if (s?.running || queued) head.classList.add('running')
|
|
585
|
+
else if (s?.error) head.classList.add('interrupted')
|
|
586
|
+
}
|
|
587
|
+
|
|
556
588
|
function updateCancelBtn() {
|
|
557
589
|
const s = state.byId.get(state.current)
|
|
558
590
|
const running = s?.running || (state.queues[state.current] || []).some(i => i.placement !== 'context')
|
|
@@ -616,7 +648,7 @@ function restoreCachedHistory() {
|
|
|
616
648
|
}
|
|
617
649
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
618
650
|
state.history = h
|
|
619
|
-
$('history-hint').textContent =
|
|
651
|
+
$('history-hint').textContent = t('history.offlineCache', { n: h.visible.length })
|
|
620
652
|
renderHistory(true)
|
|
621
653
|
return true
|
|
622
654
|
}
|
|
@@ -636,8 +668,8 @@ async function loadHistory(reset) {
|
|
|
636
668
|
} catch (e) {
|
|
637
669
|
state.history.loading = false
|
|
638
670
|
if (e.message === 'AUTH') { authFailure(); return }
|
|
639
|
-
if (restoreCachedHistory()) toast('
|
|
640
|
-
else toast('
|
|
671
|
+
if (restoreCachedHistory()) toast(t('history.cacheFallback'), 'ok')
|
|
672
|
+
else toast(t('history.loadFailed', { msg: e.message }), 'err')
|
|
641
673
|
return
|
|
642
674
|
}
|
|
643
675
|
|
|
@@ -662,7 +694,7 @@ async function loadHistory(reset) {
|
|
|
662
694
|
if (reset) renderHistory(true)
|
|
663
695
|
else if (added) renderHistory(false, 'keep')
|
|
664
696
|
if (moreBtn) moreBtn.classList.toggle('hidden', !state.history.hasMore)
|
|
665
|
-
$('history-hint').textContent = state.history.visible.length ?
|
|
697
|
+
$('history-hint').textContent = state.history.visible.length ? t('history.count', { n: state.history.visible.length }) : ''
|
|
666
698
|
scheduleHistoryCacheSave()
|
|
667
699
|
}
|
|
668
700
|
|
|
@@ -701,7 +733,7 @@ function renderHistory(reset, mode = 'bottom') {
|
|
|
701
733
|
const filtered = filteredEntries()
|
|
702
734
|
const len = filtered.length
|
|
703
735
|
if (!len) {
|
|
704
|
-
box.innerHTML = '<div class="empty"
|
|
736
|
+
box.innerHTML = '<div class="empty">' + t('history.empty') + '</div>'
|
|
705
737
|
h.renderStart = 0; h.renderEnd = 0
|
|
706
738
|
updateRail()
|
|
707
739
|
return
|
|
@@ -817,19 +849,19 @@ function eventHtml(entry, ctx = {}) {
|
|
|
817
849
|
const msg = data.message || {}
|
|
818
850
|
const role = data.role || msg.role || (type.startsWith('user') ? 'user' : 'assistant')
|
|
819
851
|
const blocks = msg.content || data.content || []
|
|
820
|
-
inner = `<div class="msg ${esc(role)}" data-seq="${seq}"><div class="role">${esc(role === 'user' ? '
|
|
852
|
+
inner = `<div class="msg ${esc(role)}" data-seq="${seq}"><div class="role">${esc(role === 'user' ? t('role.me') : t('role.dsh'))}</div>${blocks.map(blockHtml).join('')}</div>`
|
|
821
853
|
} else if (type === 'tool/call') {
|
|
822
|
-
const name = data.name || data.toolName || '
|
|
854
|
+
const name = data.name || data.toolName || t('tool.default')
|
|
823
855
|
const step = (data.turn != null ? ` · turn ${data.turn}` : '') + (data.step != null ? `.${data.step}` : '')
|
|
824
856
|
inner = `<details class="tool" data-seq="${seq}"><summary>🔧 ${esc(name)}<span class="tool-meta-inline">${esc(step)}</span></summary><pre>${esc(safeJson(data.arguments ?? data.args ?? data.input ?? data))}</pre></details>`
|
|
825
857
|
} else if (type === 'tool/result') {
|
|
826
858
|
const callId = data.callId || data.message?.source?.callId
|
|
827
|
-
const name = (callId && ctx.toolNames?.get(callId)) || '
|
|
859
|
+
const name = (callId && ctx.toolNames?.get(callId)) || t('tool.result')
|
|
828
860
|
const err = data.error || data.ok === false
|
|
829
|
-
inner = `<details class="tool result ${err ? 'error' : ''}" data-seq="${seq}"><summary>📦 ${esc(name)}<span class="tool-meta-inline"
|
|
861
|
+
inner = `<details class="tool result ${err ? 'error' : ''}" data-seq="${seq}"><summary>📦 ${esc(name)}<span class="tool-meta-inline">${t('tool.result')}</span></summary><pre>${esc(truncate(safeJson(data.result ?? data.output ?? data.message ?? data), 4000))}</pre></details>`
|
|
830
862
|
} else if (type === 'agent/status') {
|
|
831
863
|
const running = !!data.running
|
|
832
|
-
inner = `<div class="event" data-seq="${seq}">${running ? '
|
|
864
|
+
inner = `<div class="event" data-seq="${seq}">${running ? t('event.taskStart') : t('event.taskEnd')}</div>`
|
|
833
865
|
} else if (type === 'llm/usage') {
|
|
834
866
|
inner = `<div class="event" data-seq="${seq}">tokens ${fmtTokens(data.inputTokens)} → ${fmtTokens(data.outputTokens)}</div>`
|
|
835
867
|
} else if (type === 'checkpoint/created' || type === 'compaction/complete' || type === 'compaction/summary') {
|
|
@@ -845,16 +877,16 @@ function blockHtml(b) {
|
|
|
845
877
|
if ((b.type === 'tool-call' || b.type === 'tool-result') && LS.get('showTools', '1') === '0') return ''
|
|
846
878
|
switch (b.type) {
|
|
847
879
|
case 'text': return `<div>${renderMarkdown(b.text ?? '')}</div>`
|
|
848
|
-
case 'image': return `<img alt="
|
|
880
|
+
case 'image': return `<img alt="${t('block.image')}" src="data:${esc(b.mediaType || 'image/png')};base64,${esc(b.data || '')}">`
|
|
849
881
|
case 'thinking':
|
|
850
882
|
case 'reasoning':
|
|
851
|
-
return `<details class="tool"><summary
|
|
883
|
+
return `<details class="tool"><summary>${t('block.thinking')}</summary><div class="tool-text">${esc(truncate(String(b.text ?? b.content ?? safeJson(b)), 6000))}</div></details>`
|
|
852
884
|
case 'code': return `<pre>${esc(b.content ?? b.code ?? '')}</pre>`
|
|
853
885
|
case 'tool-call':
|
|
854
|
-
return `<details class="tool"><summary>🔧 ${esc(b.name || b.toolName || '
|
|
886
|
+
return `<details class="tool"><summary>🔧 ${esc(b.name || b.toolName || t('block.toolCall'))}</summary><pre>${esc(truncate(safeJson(b.arguments ?? b), 4000))}</pre></details>`
|
|
855
887
|
case 'tool-result':
|
|
856
|
-
return `<details class="tool result"><summary>📦 ${esc(b.name || b.toolName || '
|
|
857
|
-
default: return `<details class="tool"><summary
|
|
888
|
+
return `<details class="tool result"><summary>📦 ${esc(b.name || b.toolName || t('block.toolResult'))}</summary><pre>${esc(truncate(safeJson(b.content ?? b), 4000))}</pre></details>`
|
|
889
|
+
default: return `<details class="tool"><summary>${esc(t('block.unknown', { type: b.type || '?' }))}</summary><pre>${esc(truncate(safeJson(b), 2000))}</pre></details>`
|
|
858
890
|
}
|
|
859
891
|
}
|
|
860
892
|
|
|
@@ -876,7 +908,7 @@ function safeJson(v) {
|
|
|
876
908
|
try { return typeof v === 'string' ? v : JSON.stringify(v, null, 2) }
|
|
877
909
|
catch { return String(v) }
|
|
878
910
|
}
|
|
879
|
-
function truncate(s, n) { return String(s).length > n ? String(s).slice(0, n) + '
|
|
911
|
+
function truncate(s, n) { return String(s).length > n ? String(s).slice(0, n) + t('truncated') : s }
|
|
880
912
|
|
|
881
913
|
/* 会话卡片(goal/todo/subagents); 统计进顶栏 📊 弹窗 */
|
|
882
914
|
function statsHtml(s) {
|
|
@@ -887,15 +919,15 @@ function statsHtml(s) {
|
|
|
887
919
|
let html = ''
|
|
888
920
|
if (stats) {
|
|
889
921
|
const llmMin = stats.llmMs ? (stats.llmMs / 60000).toFixed(1) : null
|
|
890
|
-
html += `<div class="card"><div class="card-title"
|
|
891
|
-
<div class="card-row"><span class="k"
|
|
892
|
-
<div class="card-row"><span class="k"
|
|
893
|
-
${usage ? `<div class="card-row"><span class="k"
|
|
894
|
-
${ctx ? `<div class="card-row"><span class="k"
|
|
895
|
-
${perms?.currentValue ? `<div class="card-row"><span class="k"
|
|
922
|
+
html += `<div class="card"><div class="card-title">${t('stats.roundTitle')}</div>
|
|
923
|
+
<div class="card-row"><span class="k">${t('stats.turnsSteps')}</span><span class="v">${stats.turns ?? '—'} / ${stats.steps ?? '—'}</span></div>
|
|
924
|
+
<div class="card-row"><span class="k">${t('stats.llmTime')}</span><span class="v">${llmMin ? llmMin + t('stats.minutes') : '—'}</span></div>
|
|
925
|
+
${usage ? `<div class="card-row"><span class="k">${t('stats.outputCache')}</span><span class="v">${fmtTokens(usage.outputTokens)} / ${fmtTokens(usage.cacheReadTokens)}</span></div>` : ''}
|
|
926
|
+
${ctx ? `<div class="card-row"><span class="k">${t('stats.ctxPressure')}</span><span class="v">${fmtTokens(ctx.pressureTokens)} / ${fmtTokens(ctx.contextWindow)}</span></div>` : ''}
|
|
927
|
+
${perms?.currentValue ? `<div class="card-row"><span class="k">${t('stats.permission')}</span><span class="v">${esc(perms.currentValue)}</span></div>` : ''}
|
|
896
928
|
</div>`
|
|
897
929
|
}
|
|
898
|
-
return html || '<div class="empty"
|
|
930
|
+
return html || '<div class="empty">' + t('stats.empty') + '</div>'
|
|
899
931
|
}
|
|
900
932
|
|
|
901
933
|
async function renderSessionCards() {
|
|
@@ -909,18 +941,18 @@ async function renderSessionCards() {
|
|
|
909
941
|
let html = ''
|
|
910
942
|
|
|
911
943
|
if (goal) {
|
|
912
|
-
html += `<div class="card"><div class="card-title"
|
|
944
|
+
html += `<div class="card"><div class="card-title">${t('goal.title')}</div>
|
|
913
945
|
<div class="goal-obj">${esc(goal.objective || '')}</div>
|
|
914
946
|
<div class="goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
|
|
915
947
|
<div class="goal-actions">
|
|
916
|
-
${goal.phase === 'active' ? '<button class="mini-btn" data-goal="pause"
|
|
917
|
-
<button class="mini-btn" data-goal="complete"
|
|
918
|
-
<button class="mini-btn" data-goal="edit"
|
|
919
|
-
<button class="mini-btn" data-goal="clear"
|
|
948
|
+
${goal.phase === 'active' ? '<button class="mini-btn" data-goal="pause">' + t('goal.pause') + '</button>' : '<button class="mini-btn" data-goal="resume">' + t('goal.resume') + '</button>'}
|
|
949
|
+
<button class="mini-btn" data-goal="complete">${t('goal.complete')}</button>
|
|
950
|
+
<button class="mini-btn" data-goal="edit">${t('goal.edit')}</button>
|
|
951
|
+
<button class="mini-btn" data-goal="clear">${t('goal.clear')}</button>
|
|
920
952
|
</div></div>`
|
|
921
953
|
}
|
|
922
954
|
if (todos?.items?.length) {
|
|
923
|
-
html += `<div class="card"><div class="card-title"
|
|
955
|
+
html += `<div class="card"><div class="card-title">${t('todos.title')}</div>${todos.items.map(t =>
|
|
924
956
|
`<div><span class="pill ${t.status === 'completed' ? 'done' : t.status === 'in_progress' ? 'active' : ''}">${esc(t.status || 'pending')}</span>${esc(t.content || '')}</div>`
|
|
925
957
|
).join('')}</div>`
|
|
926
958
|
}
|
|
@@ -932,12 +964,12 @@ async function renderSessionCards() {
|
|
|
932
964
|
const sub = await safeRpc('subagent.list', { parentSessionId: state.current })
|
|
933
965
|
if (sub?.entries?.length) {
|
|
934
966
|
const rows = sub.entries.map(e => {
|
|
935
|
-
if (e.kind === 'diagnostic') return `<div class="card-row"><span class="k"
|
|
967
|
+
if (e.kind === 'diagnostic') return `<div class="card-row"><span class="k">${t('subagent.diagnostic')}</span><span class="v">${esc(e.reason)}</span></div>`
|
|
936
968
|
const label = e.label || short(e.id)
|
|
937
969
|
const running = e.activity === 'running'
|
|
938
|
-
return `<div class="card-row"><span class="k">${running ? '▶ ' : ''}${esc(label)}</span><span class="v">${esc(e.mode)} ${running ? '
|
|
970
|
+
return `<div class="card-row"><span class="k">${running ? '▶ ' : ''}${esc(label)}</span><span class="v">${esc(e.mode)} ${running ? t('subagent.running') : ''}${e.mode === 'continuable' && running ? ` <button class="mini-btn" data-sub-interrupt="${esc(e.id)}">${t('subagent.interrupt')}</button>` : ''}</span></div>`
|
|
939
971
|
}).join('')
|
|
940
|
-
box.insertAdjacentHTML('beforeend', `<div class="card"><div class="card-title"
|
|
972
|
+
box.insertAdjacentHTML('beforeend', `<div class="card"><div class="card-title">${t('subagent.title')}</div>${rows}</div>`)
|
|
941
973
|
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
942
974
|
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
943
975
|
}
|
|
@@ -946,53 +978,161 @@ async function renderSessionCards() {
|
|
|
946
978
|
async function goalAction(kind) {
|
|
947
979
|
const s = state.byId.get(state.current)
|
|
948
980
|
const goal = goalOf(s)
|
|
949
|
-
if (!goal) return toast('
|
|
981
|
+
if (!goal) return toast(t('goal.none'))
|
|
950
982
|
const ref = { id: goal.id, revision: goal.revision }
|
|
951
983
|
if (kind === 'edit') return openGoalModal(goal)
|
|
952
984
|
const map = { pause: 'goal.pause', resume: 'goal.resume', complete: 'goal.complete', clear: 'goal.clear' }
|
|
953
985
|
const method = map[kind]
|
|
954
986
|
if (!method) return
|
|
955
|
-
if (kind === 'clear' && !confirm(
|
|
956
|
-
if (kind === 'complete' && !confirm('
|
|
957
|
-
await safeRpc(method, { sessionId: state.current, ref }, '
|
|
958
|
-
toast('
|
|
987
|
+
if (kind === 'clear' && !confirm(t('goal.confirmClear'))) return
|
|
988
|
+
if (kind === 'complete' && !confirm(t('goal.confirmComplete'))) return
|
|
989
|
+
await safeRpc(method, { sessionId: state.current, ref }, t('goal.actionFailed'))
|
|
990
|
+
toast(t('goal.actionSubmitted'), 'ok')
|
|
959
991
|
scheduleRefresh()
|
|
960
992
|
}
|
|
961
993
|
|
|
962
994
|
async function interruptSubagent(childId) {
|
|
963
|
-
if (!confirm('
|
|
964
|
-
await safeRpc('subagent.interrupt', { parentSessionId: state.current, childSessionId: childId, mode: 'continuable' }, '
|
|
965
|
-
toast('
|
|
995
|
+
if (!confirm(t('subagent.confirmInterrupt'))) return
|
|
996
|
+
await safeRpc('subagent.interrupt', { parentSessionId: state.current, childSessionId: childId, mode: 'continuable' }, t('subagent.interruptFailed'))
|
|
997
|
+
toast(t('subagent.interruptSubmitted'), 'ok')
|
|
966
998
|
setTimeout(renderSessionCards, 600)
|
|
967
999
|
}
|
|
968
1000
|
|
|
969
|
-
/* ---------------- 发送 / 取消 ---------------- */
|
|
970
|
-
async function
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
if (!text || !state.current) return
|
|
1001
|
+
/* ---------------- 发送 / 取消 / 快捷菜单 ---------------- */
|
|
1002
|
+
async function sendSessionText(text) {
|
|
1003
|
+
const clean = String(text || '').trim()
|
|
1004
|
+
if (!clean || !state.current) return false
|
|
974
1005
|
$('btn-send').disabled = true
|
|
975
1006
|
const v = await safeRpc('session.prompt', {
|
|
976
1007
|
sessionId: state.current,
|
|
977
1008
|
mode: 'queue',
|
|
978
|
-
content: [{ type: 'text', text }]
|
|
979
|
-
}, '
|
|
1009
|
+
content: [{ type: 'text', text: clean }]
|
|
1010
|
+
}, t('send.failed'))
|
|
980
1011
|
$('btn-send').disabled = false
|
|
981
|
-
if (v?.accepted) {
|
|
982
|
-
|
|
1012
|
+
if (v?.accepted) { toast(clean.startsWith('/') ? t('send.commandSent') : t('send.sent'), 'ok'); return true }
|
|
1013
|
+
if (v?.command?.text) { toast(t('send.commandExecuted'), 'ok'); return true }
|
|
1014
|
+
return false
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async function sendMessage() {
|
|
1018
|
+
const input = $('composer-input')
|
|
1019
|
+
const text = input.value.trim()
|
|
1020
|
+
if (!text || !state.current) return
|
|
1021
|
+
if (await sendSessionText(text)) { input.value = ''; autosize(input) }
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function hideComposerMenu() {
|
|
1025
|
+
$('composer-menu').classList.add('hidden')
|
|
1026
|
+
$('btn-plus').classList.remove('active')
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function toggleComposerMenu() {
|
|
1030
|
+
const menu = $('composer-menu')
|
|
1031
|
+
const show = menu.classList.contains('hidden')
|
|
1032
|
+
menu.classList.toggle('hidden', !show)
|
|
1033
|
+
$('btn-plus').classList.toggle('active', show)
|
|
1034
|
+
if (show && !state.models.loaded && !state.models.loading) loadSessionModels()
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function loadSessionModels() {
|
|
1038
|
+
if (!state.current || state.models.loading) return
|
|
1039
|
+
state.models.loading = true
|
|
1040
|
+
renderModelMenu()
|
|
1041
|
+
try {
|
|
1042
|
+
const v = await rpc('session.models', { sessionId: state.current })
|
|
1043
|
+
state.models.groups = v.groups || []
|
|
1044
|
+
state.models.current = v.current || null
|
|
1045
|
+
state.models.failures = v.failures || []
|
|
1046
|
+
state.models.loaded = true
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
if (e.message === 'AUTH') { authFailure(); return }
|
|
1049
|
+
toast(t('models.loadFailed', { msg: e.message }), 'err')
|
|
1050
|
+
}
|
|
1051
|
+
state.models.loading = false
|
|
1052
|
+
renderModelMenu()
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function renderModelMenu() {
|
|
1056
|
+
const box = $('menu-models')
|
|
1057
|
+
if (!box) return
|
|
1058
|
+
if (state.models.loading) { box.innerHTML = '<span>' + t('models.loading') + '</span>'; return }
|
|
1059
|
+
const groups = state.models.groups || []
|
|
1060
|
+
if (!groups.length) {
|
|
1061
|
+
box.innerHTML = '<span>' + ((state.models.failures || []).map(f => f.name + ' ' + t('models.unavailable')).join(';') || t('models.none')) + '</span>'
|
|
1062
|
+
const effortGroup = $('menu-effort-group')
|
|
1063
|
+
if (effortGroup) effortGroup.classList.add('hidden')
|
|
1064
|
+
return
|
|
1065
|
+
}
|
|
1066
|
+
const cur = state.models.current
|
|
1067
|
+
box.innerHTML = groups.map(g => `
|
|
1068
|
+
<div style="width:100%">
|
|
1069
|
+
<div class="model-provider">${esc(g.name || g.id)}</div>
|
|
1070
|
+
<div class="menu-chips">${(g.models || []).map(m => {
|
|
1071
|
+
const isCur = cur && cur.provider === g.id && cur.model === m.id
|
|
1072
|
+
return `<button class="model-chip ${isCur ? 'current' : ''}" data-model="${esc(m.id)}" data-provider="${esc(g.id)}">${esc(m.name || m.id)}</button>`
|
|
1073
|
+
}).join('')}</div>
|
|
1074
|
+
</div>`).join('')
|
|
1075
|
+
box.querySelectorAll('[data-model]').forEach(btn =>
|
|
1076
|
+
btn.addEventListener('click', () => selectSessionModel(btn.dataset.provider, btn.dataset.model)))
|
|
1077
|
+
renderEffortMenu()
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function renderEffortMenu() {
|
|
1081
|
+
const group = $('menu-effort-group')
|
|
1082
|
+
const box = $('menu-efforts')
|
|
1083
|
+
if (!group || !box) return
|
|
1084
|
+
const cur = state.models.current
|
|
1085
|
+
const provider = (state.models.groups || []).find(g => g.id === cur?.provider)
|
|
1086
|
+
const model = (provider?.models || []).find(m => m.id === cur?.model)
|
|
1087
|
+
const efforts = model?.reasoning?.efforts || []
|
|
1088
|
+
group.classList.toggle('hidden', !efforts.length)
|
|
1089
|
+
box.innerHTML = efforts.map(e => {
|
|
1090
|
+
const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === model.reasoning.defaultEffort)
|
|
1091
|
+
return `<button class="menu-chip ${isCur ? 'current' : ''}" data-effort="${esc(e.id)}" title="${esc(e.description || '')}">${esc(e.name || e.id)}</button>`
|
|
1092
|
+
}).join('')
|
|
1093
|
+
box.querySelectorAll('[data-effort]').forEach(btn =>
|
|
1094
|
+
btn.addEventListener('click', () => selectSessionEffort(btn.dataset.effort)))
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
async function selectSessionEffort(effortId) {
|
|
1098
|
+
const cur = state.models.current
|
|
1099
|
+
if (!state.current || !cur) return
|
|
1100
|
+
const v = await safeRpc('session.selectModel', {
|
|
1101
|
+
sessionId: state.current, provider: cur.provider, model: cur.model, reasoningEffort: effortId
|
|
1102
|
+
}, t('models.effortFailed'))
|
|
1103
|
+
if (v?.selected) {
|
|
1104
|
+
state.models.current = v.selected
|
|
1105
|
+
renderEffortMenu()
|
|
1106
|
+
toast(t('models.effortSwitched', { effort: effortId }), 'ok')
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async function selectSessionModel(provider, modelId) {
|
|
1111
|
+
if (!state.current) return
|
|
1112
|
+
const group = (state.models.groups || []).find(g => g.id === provider)
|
|
1113
|
+
const model = (group?.models || []).find(m => m.id === modelId)
|
|
1114
|
+
const payload = { sessionId: state.current, provider, model: modelId }
|
|
1115
|
+
const effort = model?.reasoning?.defaultEffort || model?.reasoning?.efforts?.[0]?.id
|
|
1116
|
+
if (effort) payload.reasoningEffort = effort
|
|
1117
|
+
const v = await safeRpc('session.selectModel', payload, t('models.switchFailed'))
|
|
1118
|
+
if (v?.selected) {
|
|
1119
|
+
state.models.current = v.selected
|
|
1120
|
+
renderModelMenu()
|
|
1121
|
+
toast(t('models.switched', { model: v.selected.model }), 'ok')
|
|
1122
|
+
}
|
|
983
1123
|
}
|
|
984
1124
|
|
|
985
1125
|
async function cancelSession() {
|
|
986
1126
|
if (!state.current) return
|
|
987
|
-
if (!confirm('
|
|
988
|
-
const v = await safeRpc('session.cancel', { sessionId: state.current }, '
|
|
989
|
-
if (v?.accepted) toast('
|
|
1127
|
+
if (!confirm(t('session.confirmStop'))) return
|
|
1128
|
+
const v = await safeRpc('session.cancel', { sessionId: state.current }, t('session.stopFailed'))
|
|
1129
|
+
if (v?.accepted) toast(t('session.stopRequested'), 'ok')
|
|
990
1130
|
}
|
|
991
1131
|
|
|
992
1132
|
async function newSession() {
|
|
993
|
-
const v = await safeRpc('session.create', {}, '
|
|
1133
|
+
const v = await safeRpc('session.create', {}, t('home.createFailed'))
|
|
994
1134
|
if (!v?.sessionId) return
|
|
995
|
-
toast('
|
|
1135
|
+
toast(t('home.created'), 'ok')
|
|
996
1136
|
await refreshSessions()
|
|
997
1137
|
openSession(v.sessionId)
|
|
998
1138
|
}
|
|
@@ -1004,27 +1144,27 @@ function renderPending() {
|
|
|
1004
1144
|
...state.approvals.map(a => ({ kind: 'approval', a })),
|
|
1005
1145
|
...state.questions.map(q => ({ kind: 'question', q }))
|
|
1006
1146
|
]
|
|
1007
|
-
$('pending-count').textContent = items.length ?
|
|
1147
|
+
$('pending-count').textContent = items.length ? t('pending.count', { n: items.length }) : ''
|
|
1008
1148
|
list.innerHTML = items.length ? items.map(it => {
|
|
1009
1149
|
if (it.kind === 'approval') {
|
|
1010
1150
|
const a = it.a
|
|
1011
1151
|
const title = titleOf(state.byId.get(a.sessionId))
|
|
1012
1152
|
return `<div class="pending-card approval" data-approval="${esc(a.approvalId)}">
|
|
1013
|
-
<div class="pc-title"
|
|
1014
|
-
<div class="pc-desc">${esc(a.reason || '
|
|
1153
|
+
<div class="pc-title">${esc(t('pending.approvalTitle', { tool: a.toolName || t('tool.default') }))}</div>
|
|
1154
|
+
<div class="pc-desc">${esc(a.reason || t('pending.noReason'))}</div>
|
|
1015
1155
|
<div class="pc-session">${esc(title)}</div>
|
|
1016
|
-
<div class="goal-actions"><button class="mini-btn" data-approve="1"
|
|
1156
|
+
<div class="goal-actions"><button class="mini-btn" data-approve="1">${t('pending.allow')}</button><button class="mini-btn" data-approve="0">${t('pending.reject')}</button></div>
|
|
1017
1157
|
</div>`
|
|
1018
1158
|
}
|
|
1019
1159
|
const q = it.q
|
|
1020
1160
|
const title = titleOf(state.byId.get(q.sessionId))
|
|
1021
1161
|
return `<div class="pending-card question" data-question="${esc(q.rpcId)}">
|
|
1022
|
-
<div class="pc-title">❓ ${esc(q.questions?.[0]?.question || '
|
|
1023
|
-
<div class="pc-desc">${q.questions?.length > 1 ?
|
|
1162
|
+
<div class="pc-title">❓ ${esc(q.questions?.[0]?.question || t('notify.questionTitle'))}</div>
|
|
1163
|
+
<div class="pc-desc">${q.questions?.length > 1 ? t('pending.questionCount', { n: q.questions.length }) : ''}</div>
|
|
1024
1164
|
<div class="pc-session">${esc(title)}</div>
|
|
1025
|
-
<div class="goal-actions"><button class="mini-btn" data-answer="1"
|
|
1165
|
+
<div class="goal-actions"><button class="mini-btn" data-answer="1">${t('pending.answer')}</button></div>
|
|
1026
1166
|
</div>`
|
|
1027
|
-
}).join('') : '<div class="empty"
|
|
1167
|
+
}).join('') : '<div class="empty">' + t('pending.empty') + '</div>'
|
|
1028
1168
|
list.querySelectorAll('[data-approve]').forEach(btn => {
|
|
1029
1169
|
const card = btn.closest('[data-approval]')
|
|
1030
1170
|
btn.addEventListener('click', () => approveApproval(card?.dataset.approval || '', btn.dataset.approve === '1'))
|
|
@@ -1038,7 +1178,7 @@ async function approveApproval(id, allow) {
|
|
|
1038
1178
|
const a = state.approvals.find(x => x.approvalId === id)
|
|
1039
1179
|
if (!a) return
|
|
1040
1180
|
const ok = await respond(a.rpcId, { sessionId: a.sessionId, approvalId: a.approvalId, outcome: allow ? 'allowed-once' : 'rejected' })
|
|
1041
|
-
toast(ok ? (allow ? '
|
|
1181
|
+
toast(ok ? (allow ? t('pending.allowed') : t('pending.rejected')) : t('pending.stale'), ok ? 'ok' : 'err')
|
|
1042
1182
|
state.approvals = state.approvals.filter(x => x.approvalId !== id)
|
|
1043
1183
|
renderPending()
|
|
1044
1184
|
}
|
|
@@ -1051,7 +1191,7 @@ function openQuestionModal(q) {
|
|
|
1051
1191
|
<div class="q-text">${esc(item.header ? item.header + ':' : '')}${esc(item.question)}</div>
|
|
1052
1192
|
${(item.options || []).map((o, j) => `
|
|
1053
1193
|
<label class="q-option"><input type="${item.multiSelect ? 'checkbox' : 'radio'}" name="q${i}" value="${esc(o.label)}" data-q="${i}"><span>${esc(o.label)}${o.description ? `<div class="muted">${esc(o.description)}</div>` : ''}</span></label>`).join('')}
|
|
1054
|
-
<textarea rows="2" placeholder="
|
|
1194
|
+
<textarea rows="2" placeholder="${t('question.customPlaceholder')}" data-qcustom="${i}"></textarea>
|
|
1055
1195
|
</div>`).join('')
|
|
1056
1196
|
$('modal-question').classList.remove('hidden')
|
|
1057
1197
|
}
|
|
@@ -1067,10 +1207,10 @@ async function submitQuestion() {
|
|
|
1067
1207
|
if (!sel.length && !custom) return null
|
|
1068
1208
|
return ans
|
|
1069
1209
|
}).filter(Boolean)
|
|
1070
|
-
if (!answers.length) return toast('
|
|
1210
|
+
if (!answers.length) return toast(t('question.needAnswer'), 'err')
|
|
1071
1211
|
const ok = await respond(q.rpcId, { sessionId: q.sessionId, answer: { answers } })
|
|
1072
|
-
if (ok) { toast('
|
|
1073
|
-
else toast('
|
|
1212
|
+
if (ok) { toast(t('question.submitted'), 'ok'); $('modal-question').classList.add('hidden'); state.questions = state.questions.filter(x => x.rpcId !== q.rpcId); renderPending() }
|
|
1213
|
+
else toast(t('question.stale'), 'err')
|
|
1074
1214
|
}
|
|
1075
1215
|
|
|
1076
1216
|
/* ---------------- 后台任务 ---------------- */
|
|
@@ -1080,14 +1220,14 @@ function renderQueue() {
|
|
|
1080
1220
|
const items = state.queues[state.current] || []
|
|
1081
1221
|
updateCancelBtn()
|
|
1082
1222
|
// 队列数量在会话列表已显示; 详情页不重复大 UI
|
|
1083
|
-
$('history-hint').textContent = items.length ?
|
|
1223
|
+
$('history-hint').textContent = items.length ? t('history.queueAndCount', { q: items.length, n: state.history.visible.length }) : t('history.countOnly', { n: state.history.visible.length })
|
|
1084
1224
|
renderSessions()
|
|
1085
1225
|
}
|
|
1086
1226
|
|
|
1087
1227
|
function renderJobs() {
|
|
1088
1228
|
const box = $('jobs-list')
|
|
1089
1229
|
const all = Object.entries(state.jobs).filter(([, jobs]) => jobs?.length)
|
|
1090
|
-
if (!all.length) { box.innerHTML = '<div class="empty"
|
|
1230
|
+
if (!all.length) { box.innerHTML = '<div class="empty">' + t('jobs.empty') + '</div>'; return }
|
|
1091
1231
|
box.innerHTML = all.flatMap(([sid, jobs]) => jobs.map(j => {
|
|
1092
1232
|
const title = titleOf(state.byId.get(sid))
|
|
1093
1233
|
return `<div class="job-card">
|
|
@@ -1130,21 +1270,21 @@ function fsAuthError(status) {
|
|
|
1130
1270
|
|
|
1131
1271
|
async function loadFs(dir, { silent = false, resetRoot = false } = {}) {
|
|
1132
1272
|
if (!state.token) {
|
|
1133
|
-
$('fs-path').textContent = '
|
|
1134
|
-
$('fs-list').innerHTML = '<div class="empty"
|
|
1273
|
+
$('fs-path').textContent = t('fs.noToken')
|
|
1274
|
+
$('fs-list').innerHTML = '<div class="empty">' + t('fs.goSettings') + '</div>'
|
|
1135
1275
|
return
|
|
1136
1276
|
}
|
|
1137
1277
|
if (resetRoot) { state.fs.initial = null; state.fs.path = null }
|
|
1138
1278
|
const target = dir ?? state.fs.path ?? ''
|
|
1139
1279
|
if (!silent) {
|
|
1140
|
-
$('fs-list').innerHTML = '<div class="empty"
|
|
1141
|
-
$('fs-path').textContent = target ? '…' + target.slice(-40) : '
|
|
1280
|
+
$('fs-list').innerHTML = '<div class="empty">' + t('fs.loading') + '</div>'
|
|
1281
|
+
$('fs-path').textContent = target ? '…' + target.slice(-40) : t('fs.loading')
|
|
1142
1282
|
}
|
|
1143
1283
|
try {
|
|
1144
1284
|
const res = await fetch(fsApiUrl('/list', target ? { path: target } : {}), { headers: fsHeaders() })
|
|
1145
1285
|
if (res.status === 401) { fsAuthError(401); return }
|
|
1146
1286
|
const data = await res.json().catch(() => ({}))
|
|
1147
|
-
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error === 'not-found' ? '
|
|
1287
|
+
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))
|
|
1148
1288
|
state.fs.path = data.path
|
|
1149
1289
|
if (!state.fs.initial) state.fs.initial = data.path
|
|
1150
1290
|
state.fs.loaded = true
|
|
@@ -1152,8 +1292,8 @@ async function loadFs(dir, { silent = false, resetRoot = false } = {}) {
|
|
|
1152
1292
|
} catch (e) {
|
|
1153
1293
|
if (e.message === 'AUTH') return
|
|
1154
1294
|
$('fs-path').textContent = target || '~'
|
|
1155
|
-
$('fs-list').innerHTML = `<div class="empty"
|
|
1156
|
-
if (!silent) toast('
|
|
1295
|
+
$('fs-list').innerHTML = `<div class="empty">${esc(t('fs.loadFailed', { msg: e.message || t('fs.networkError') }))}</div>`
|
|
1296
|
+
if (!silent) toast(t('fs.loadFailedToast', { msg: e.message }), 'err')
|
|
1157
1297
|
}
|
|
1158
1298
|
}
|
|
1159
1299
|
|
|
@@ -1161,7 +1301,7 @@ function renderFs(data) {
|
|
|
1161
1301
|
$('fs-path').textContent = data.path || '~'
|
|
1162
1302
|
const list = $('fs-list')
|
|
1163
1303
|
if (!data.entries.length) {
|
|
1164
|
-
list.innerHTML = '<div class="empty"
|
|
1304
|
+
list.innerHTML = '<div class="empty">' + t('fs.emptyDir') + '</div>'
|
|
1165
1305
|
return
|
|
1166
1306
|
}
|
|
1167
1307
|
list.innerHTML = data.entries.map(e => {
|
|
@@ -1170,7 +1310,7 @@ function renderFs(data) {
|
|
|
1170
1310
|
<span class="fs-ico">${isDir ? '📁' : '📄'}</span>
|
|
1171
1311
|
<span class="fs-meta">
|
|
1172
1312
|
<span class="fs-name">${esc(e.name)}</span>
|
|
1173
|
-
<span class="fs-sub">${isDir ? '
|
|
1313
|
+
<span class="fs-sub">${isDir ? t('fs.dir') : fmtSize(e.size)} · ${fmtFullTime(e.mtimeMs)}</span>
|
|
1174
1314
|
</span>
|
|
1175
1315
|
<span class="fs-arrow">${isDir ? '›' : '↓'}</span>
|
|
1176
1316
|
</div>`
|
|
@@ -1193,13 +1333,13 @@ function downloadFsFile(name) {
|
|
|
1193
1333
|
if (window.NativeFile?.downloadToDownloads) {
|
|
1194
1334
|
try {
|
|
1195
1335
|
window.NativeFile.downloadToDownloads(url, name, state.token)
|
|
1196
|
-
toast('
|
|
1336
|
+
toast(t('fs.downloadStarted'), 'ok')
|
|
1197
1337
|
} catch (e) {
|
|
1198
|
-
toast('
|
|
1338
|
+
toast(t('fs.downloadFailed', { msg: e?.message || '' }), 'err')
|
|
1199
1339
|
}
|
|
1200
1340
|
return
|
|
1201
1341
|
}
|
|
1202
|
-
toast('
|
|
1342
|
+
toast(t('fs.downloadUnsupported'), 'err')
|
|
1203
1343
|
return
|
|
1204
1344
|
}
|
|
1205
1345
|
// 浏览器控制台: <a download> + ?token= 兜底(主通道仍是 Bearer 头)
|
|
@@ -1234,7 +1374,7 @@ function setFsButtons(show, paused = false) {
|
|
|
1234
1374
|
if (!pauseBtn || !cancelBtn) return
|
|
1235
1375
|
pauseBtn.classList.toggle('hidden', !show)
|
|
1236
1376
|
cancelBtn.classList.toggle('hidden', !show)
|
|
1237
|
-
if (show) pauseBtn.textContent = paused ? '
|
|
1377
|
+
if (show) pauseBtn.textContent = paused ? t('fs.resume') : t('fs.pause')
|
|
1238
1378
|
}
|
|
1239
1379
|
|
|
1240
1380
|
function pauseFsUpload() {
|
|
@@ -1268,7 +1408,7 @@ async function cancelFsUpload() {
|
|
|
1268
1408
|
state.fs.upload = null
|
|
1269
1409
|
hideFsProgress()
|
|
1270
1410
|
setFsButtons(false)
|
|
1271
|
-
toast('
|
|
1411
|
+
toast(t('fs.uploadCancelled'))
|
|
1272
1412
|
}
|
|
1273
1413
|
|
|
1274
1414
|
const FS_CHUNK_SIZE = 4 * 1024 * 1024 // 4MB/块, 断线后重选同一文件自动续传
|
|
@@ -1279,7 +1419,7 @@ async function hashFsRange(file, hasher, start, end, up) {
|
|
|
1279
1419
|
for (let off = start; off < end; off += step) {
|
|
1280
1420
|
const buf = await file.slice(off, Math.min(off + step, end)).arrayBuffer()
|
|
1281
1421
|
if (up?.pauseRequested) {
|
|
1282
|
-
const err = new Error('
|
|
1422
|
+
const err = new Error(t('fs.pausedErr')); err.code = 'PAUSED'; throw err
|
|
1283
1423
|
}
|
|
1284
1424
|
hasher.update(new Uint8Array(buf))
|
|
1285
1425
|
}
|
|
@@ -1287,9 +1427,9 @@ async function hashFsRange(file, hasher, start, end, up) {
|
|
|
1287
1427
|
|
|
1288
1428
|
function uploadFsFile(file) {
|
|
1289
1429
|
if (!file) return
|
|
1290
|
-
if (!state.token) { toast('
|
|
1291
|
-
if (file.size > 2 * 1024 * 1024 * 1024) { toast('
|
|
1292
|
-
if (state.fs.upload?.active) { toast('
|
|
1430
|
+
if (!state.token) { toast(t('fs.noTokenToast'), 'err'); showView('view-settings'); return }
|
|
1431
|
+
if (file.size > 2 * 1024 * 1024 * 1024) { toast(t('fs.tooLarge'), 'err'); return }
|
|
1432
|
+
if (state.fs.upload?.active) { toast(t('fs.uploadBusy'), 'err'); return }
|
|
1293
1433
|
|
|
1294
1434
|
const prev = state.fs.upload
|
|
1295
1435
|
if (prev && prev.path === state.fs.path && prev.name === file.name && prev.size === file.size) {
|
|
@@ -1341,11 +1481,11 @@ async function runFsUpload(up) {
|
|
|
1341
1481
|
try { json = JSON.parse(xhr.responseText || '{}') } catch {}
|
|
1342
1482
|
resolve({ status: xhr.status, json })
|
|
1343
1483
|
}
|
|
1344
|
-
xhr.onerror = () => { if (up.xhr === xhr) up.xhr = null; reject(new Error('
|
|
1345
|
-
xhr.upload.onerror = () => { if (up.xhr === xhr) up.xhr = null; reject(new Error('
|
|
1484
|
+
xhr.onerror = () => { if (up.xhr === xhr) up.xhr = null; reject(new Error(t('fs.networkError'))) }
|
|
1485
|
+
xhr.upload.onerror = () => { if (up.xhr === xhr) up.xhr = null; reject(new Error(t('fs.networkInterrupt'))) }
|
|
1346
1486
|
xhr.onabort = () => {
|
|
1347
1487
|
if (up.xhr === xhr) up.xhr = null
|
|
1348
|
-
const err = new Error(up.cancelled ? '
|
|
1488
|
+
const err = new Error(t(up.cancelled ? 'fs.cancelledErr' : 'fs.pausedErr'))
|
|
1349
1489
|
err.code = up.cancelled ? 'CANCELLED' : 'PAUSED'
|
|
1350
1490
|
reject(err)
|
|
1351
1491
|
}
|
|
@@ -1368,7 +1508,7 @@ async function runFsUpload(up) {
|
|
|
1368
1508
|
if (info === null || up.cancelled) return
|
|
1369
1509
|
if (info.partialSize > 0) wasResumed = true
|
|
1370
1510
|
if (info.targetExists && info.targetSize === up.size && !overwrite) {
|
|
1371
|
-
if (!confirm('
|
|
1511
|
+
if (!confirm(t('fs.confirmOverwrite'))) { state.fs.upload = null; hideFsProgress(); setFsButtons(false); return }
|
|
1372
1512
|
overwrite = true
|
|
1373
1513
|
}
|
|
1374
1514
|
if (up.offset > 0) await hashFsRange(up.file, hasher, 0, up.offset, up)
|
|
@@ -1381,7 +1521,7 @@ async function runFsUpload(up) {
|
|
|
1381
1521
|
const before = hasher.clone()
|
|
1382
1522
|
const chunkBytes = new Uint8Array(await blob.arrayBuffer())
|
|
1383
1523
|
if (up.pauseRequested) {
|
|
1384
|
-
const err = new Error('
|
|
1524
|
+
const err = new Error(t('fs.pausedErr')); err.code = 'PAUSED'; throw err
|
|
1385
1525
|
}
|
|
1386
1526
|
hasher.update(chunkBytes)
|
|
1387
1527
|
const isLast = end >= up.size
|
|
@@ -1394,19 +1534,19 @@ async function runFsUpload(up) {
|
|
|
1394
1534
|
if (r.status === 201) {
|
|
1395
1535
|
const expected = params.sha256 || hasher.hex()
|
|
1396
1536
|
if (r.json.sha256 && r.json.sha256 !== expected) {
|
|
1397
|
-
const err = new Error('
|
|
1537
|
+
const err = new Error(t('fs.checksumMismatch')); err.checksum = true
|
|
1398
1538
|
throw err
|
|
1399
1539
|
}
|
|
1400
1540
|
hideFsProgress()
|
|
1401
1541
|
setFsButtons(false)
|
|
1402
1542
|
state.fs.upload = null
|
|
1403
|
-
toast(
|
|
1543
|
+
toast(t('fs.uploadDone', { name: up.name, resumed: wasResumed ? t('fs.resumedSuffix') : '' }), 'ok')
|
|
1404
1544
|
loadFs()
|
|
1405
1545
|
return
|
|
1406
1546
|
}
|
|
1407
1547
|
if (r.status === 409 && r.json.error === 'conflict') {
|
|
1408
1548
|
hasher = before // 这段数据没被写入, 回退哈希状态后带 overwrite=1 重发
|
|
1409
|
-
if (!confirm('
|
|
1549
|
+
if (!confirm(t('fs.confirmOverwrite2'))) { state.fs.upload = null; hideFsProgress(); setFsButtons(false); return }
|
|
1410
1550
|
overwrite = true
|
|
1411
1551
|
continue
|
|
1412
1552
|
}
|
|
@@ -1416,7 +1556,7 @@ async function runFsUpload(up) {
|
|
|
1416
1556
|
continue
|
|
1417
1557
|
}
|
|
1418
1558
|
if (r.status === 422 && r.json.error === 'checksum-mismatch') {
|
|
1419
|
-
const err = new Error('
|
|
1559
|
+
const err = new Error(t('fs.checksumCleared'))
|
|
1420
1560
|
err.checksum = true
|
|
1421
1561
|
throw err
|
|
1422
1562
|
}
|
|
@@ -1432,24 +1572,24 @@ async function runFsUpload(up) {
|
|
|
1432
1572
|
const r = await uploadChunk(params, new Blob([]))
|
|
1433
1573
|
if (r.status === 401) { fsAuthError(401); return }
|
|
1434
1574
|
if (r.status === 409 && r.json.error === 'conflict') {
|
|
1435
|
-
if (!confirm('
|
|
1575
|
+
if (!confirm(t('fs.confirmOverwrite2'))) { state.fs.upload = null; hideFsProgress(); setFsButtons(false); return }
|
|
1436
1576
|
params.overwrite = '1'
|
|
1437
1577
|
return runFsUpload(up) // 目标冲突未写入, 重新走 probe + 空 finish
|
|
1438
1578
|
}
|
|
1439
1579
|
if (r.status === 422 && r.json.error === 'checksum-mismatch') {
|
|
1440
|
-
const err = new Error('
|
|
1580
|
+
const err = new Error(t('fs.checksumCleared'))
|
|
1441
1581
|
err.checksum = true
|
|
1442
1582
|
throw err
|
|
1443
1583
|
}
|
|
1444
1584
|
if (r.status !== 201) throw new Error(r.json.error || ('HTTP ' + r.status))
|
|
1445
1585
|
if (r.json.sha256 && r.json.sha256 !== expected) {
|
|
1446
|
-
const err = new Error('
|
|
1586
|
+
const err = new Error(t('fs.checksumMismatch')); err.checksum = true
|
|
1447
1587
|
throw err
|
|
1448
1588
|
}
|
|
1449
1589
|
hideFsProgress()
|
|
1450
1590
|
setFsButtons(false)
|
|
1451
1591
|
state.fs.upload = null
|
|
1452
|
-
toast(
|
|
1592
|
+
toast(t('fs.uploadDone', { name: up.name, resumed: wasResumed ? t('fs.resumedSuffix') : '' }), 'ok')
|
|
1453
1593
|
loadFs()
|
|
1454
1594
|
return
|
|
1455
1595
|
}
|
|
@@ -1462,8 +1602,8 @@ async function runFsUpload(up) {
|
|
|
1462
1602
|
if (e?.code === 'PAUSED') {
|
|
1463
1603
|
up.paused = true
|
|
1464
1604
|
setFsButtons(true, true)
|
|
1465
|
-
setFsProgressText(
|
|
1466
|
-
toast('
|
|
1605
|
+
setFsProgressText(t('fs.pausedPct', { pct: Math.round(up.offset / Math.max(1, up.size) * 100) }))
|
|
1606
|
+
toast(t('fs.pausedToast'), 'ok')
|
|
1467
1607
|
return
|
|
1468
1608
|
}
|
|
1469
1609
|
if (e?.checksum) {
|
|
@@ -1476,20 +1616,20 @@ async function runFsUpload(up) {
|
|
|
1476
1616
|
})
|
|
1477
1617
|
} catch {}
|
|
1478
1618
|
setFsButtons(true, true)
|
|
1479
|
-
setFsProgressText('
|
|
1619
|
+
setFsProgressText(t('fs.checksumFailed'))
|
|
1480
1620
|
toast(e.message, 'err')
|
|
1481
1621
|
return
|
|
1482
1622
|
}
|
|
1483
1623
|
hideFsProgress()
|
|
1484
1624
|
setFsButtons(false)
|
|
1485
|
-
toast(
|
|
1625
|
+
toast(t('fs.uploadInterrupted', { msg: e.message }), 'err')
|
|
1486
1626
|
}
|
|
1487
1627
|
}
|
|
1488
1628
|
|
|
1489
1629
|
function fsUp() {
|
|
1490
1630
|
if (!state.fs.path || !state.fs.initial) return
|
|
1491
1631
|
if (state.fs.path === state.fs.initial) {
|
|
1492
|
-
toast('
|
|
1632
|
+
toast(t('fs.alreadyRoot'))
|
|
1493
1633
|
return
|
|
1494
1634
|
}
|
|
1495
1635
|
loadFs(fsParent(state.fs.path))
|
|
@@ -1508,7 +1648,7 @@ function bindFsPullRefresh() {
|
|
|
1508
1648
|
const dy = e.touches[0].clientY - startY
|
|
1509
1649
|
if (dy > 4 && window.scrollY <= 0) {
|
|
1510
1650
|
pull.style.height = Math.min(64, dy / 2) + 'px'
|
|
1511
|
-
pull.textContent = dy > 80 ? '
|
|
1651
|
+
pull.textContent = dy > 80 ? t('fs.pullRelease') : t('fs.pullDown')
|
|
1512
1652
|
}
|
|
1513
1653
|
}, { passive: true })
|
|
1514
1654
|
view.addEventListener('touchend', () => {
|
|
@@ -1516,7 +1656,7 @@ function bindFsPullRefresh() {
|
|
|
1516
1656
|
const h = parseFloat(pull.style.height || '0')
|
|
1517
1657
|
startY = null
|
|
1518
1658
|
if (h >= 40) {
|
|
1519
|
-
pull.textContent = '
|
|
1659
|
+
pull.textContent = t('fs.refreshing')
|
|
1520
1660
|
loadFs(null, { silent: true })
|
|
1521
1661
|
}
|
|
1522
1662
|
pull.style.height = '0px'
|
|
@@ -1538,21 +1678,38 @@ async function submitGoalEdit() {
|
|
|
1538
1678
|
const goal = state.goalEdit
|
|
1539
1679
|
if (!goal) return
|
|
1540
1680
|
const objective = $('goal-edit-text')?.value?.trim()
|
|
1541
|
-
if (!objective) return toast('
|
|
1542
|
-
await safeRpc('goal.edit', { sessionId: state.current, ref: { id: goal.id, revision: goal.revision }, objective }, '
|
|
1681
|
+
if (!objective) return toast(t('goal.cannotEmpty'), 'err')
|
|
1682
|
+
await safeRpc('goal.edit', { sessionId: state.current, ref: { id: goal.id, revision: goal.revision }, objective }, t('goal.updateFailed'))
|
|
1543
1683
|
$('modal-goal').classList.add('hidden')
|
|
1544
|
-
toast('
|
|
1684
|
+
toast(t('goal.updated'), 'ok')
|
|
1545
1685
|
scheduleRefresh()
|
|
1546
1686
|
}
|
|
1547
1687
|
|
|
1548
1688
|
/* ---------------- 检查更新 ---------------- */
|
|
1689
|
+
function parseVersion(v) {
|
|
1690
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(v || '').trim())
|
|
1691
|
+
if (!m) return { core: [0, 0, 0], pre: null }
|
|
1692
|
+
return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] || null }
|
|
1693
|
+
}
|
|
1549
1694
|
function cmpVersion(a, b) {
|
|
1550
|
-
const pa =
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
const d = (pa[i] || 0) - (pb[i] || 0)
|
|
1695
|
+
const pa = parseVersion(a), pb = parseVersion(b)
|
|
1696
|
+
for (let i = 0; i < 3; i++) {
|
|
1697
|
+
const d = pa.core[i] - pb.core[i]
|
|
1554
1698
|
if (d) return d
|
|
1555
1699
|
}
|
|
1700
|
+
// 无预发布后缀 = 正式版 > 任何 rc; 两个 rc 按段比较(数字段按数值, 字母段按字典序)
|
|
1701
|
+
if (!pa.pre && !pb.pre) return 0
|
|
1702
|
+
if (!pa.pre) return 1
|
|
1703
|
+
if (!pb.pre) return -1
|
|
1704
|
+
const sa = String(pa.pre).split('.'), sb = String(pb.pre).split('.')
|
|
1705
|
+
for (let i = 0; i < Math.max(sa.length, sb.length); i++) {
|
|
1706
|
+
const x = sa[i] ?? '', y = sb[i] ?? ''
|
|
1707
|
+
if (x === y) continue
|
|
1708
|
+
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y)
|
|
1709
|
+
if (nx && ny) { const d = Number(x) - Number(y); if (d) return d }
|
|
1710
|
+
else if (nx !== ny) return nx ? -1 : 1 // 数字段 < 字母段(semver 规则)
|
|
1711
|
+
else { const d = x.localeCompare(y); if (d) return d }
|
|
1712
|
+
}
|
|
1556
1713
|
return 0
|
|
1557
1714
|
}
|
|
1558
1715
|
|
|
@@ -1561,36 +1718,36 @@ async function loadLocalVersion() {
|
|
|
1561
1718
|
const res = await fetch('version.json?t=' + Date.now())
|
|
1562
1719
|
if (res.ok) state.localVersion = (await res.json())?.version || ''
|
|
1563
1720
|
} catch {}
|
|
1564
|
-
$('update-desc').textContent = state.localVersion ?
|
|
1721
|
+
$('update-desc').textContent = state.localVersion ? t('update.currentV', { version: state.localVersion }) : t('update.noVersion')
|
|
1565
1722
|
}
|
|
1566
1723
|
|
|
1567
1724
|
async function checkUpdate(silent) {
|
|
1568
1725
|
const base = state.server
|
|
1569
1726
|
if (!base) {
|
|
1570
|
-
if (!silent) toast('
|
|
1571
|
-
$('update-desc').textContent = state.localVersion ?
|
|
1727
|
+
if (!silent) toast(t('update.needServer'), 'err')
|
|
1728
|
+
$('update-desc').textContent = state.localVersion ? `${t('update.currentV', { version: state.localVersion })} · ${t('update.needServer')}` : t('update.needServer')
|
|
1572
1729
|
return
|
|
1573
1730
|
}
|
|
1574
|
-
if (!silent) toast('
|
|
1731
|
+
if (!silent) toast(t('update.checking'))
|
|
1575
1732
|
try {
|
|
1576
|
-
const res = await fetch(base + '/update.json?t=' + Date.now())
|
|
1733
|
+
const res = await fetch(base + '/update.json?t=' + Date.now() + '&local=' + encodeURIComponent(state.localVersion))
|
|
1577
1734
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
1578
1735
|
const info = await res.json()
|
|
1579
1736
|
if (info.version && cmpVersion(info.version, state.localVersion) > 0) {
|
|
1580
1737
|
state.updateInfo = info
|
|
1581
|
-
$('update-desc').textContent =
|
|
1738
|
+
$('update-desc').textContent = t('update.found', { version: info.version, notes: info.notes ? ':' + info.notes : '' })
|
|
1582
1739
|
$('btn-download-update').classList.remove('hidden')
|
|
1583
|
-
if (!silent) toast(
|
|
1584
|
-
else notify('
|
|
1740
|
+
if (!silent) toast(t('update.found', { version: info.version }), 'ok')
|
|
1741
|
+
else notify(t('update.foundTitle'), t('update.foundBody', { version: info.version }))
|
|
1585
1742
|
} else {
|
|
1586
1743
|
state.updateInfo = null
|
|
1587
|
-
$('update-desc').textContent = state.localVersion ?
|
|
1744
|
+
$('update-desc').textContent = state.localVersion ? t('update.latestV', { version: state.localVersion }) : t('update.latestRemote', { version: info.version || '?' })
|
|
1588
1745
|
$('btn-download-update').classList.add('hidden')
|
|
1589
|
-
if (!silent) toast('
|
|
1746
|
+
if (!silent) toast(t('update.latestToast'), 'ok')
|
|
1590
1747
|
}
|
|
1591
1748
|
} catch (e) {
|
|
1592
|
-
$('update-desc').textContent = '
|
|
1593
|
-
if (!silent) toast('
|
|
1749
|
+
$('update-desc').textContent = t('update.checkFailedDesc', { msg: e.message || t('fs.networkError') })
|
|
1750
|
+
if (!silent) toast(t('update.checkFailed', { msg: e.message }), 'err')
|
|
1594
1751
|
}
|
|
1595
1752
|
}
|
|
1596
1753
|
|
|
@@ -1606,14 +1763,14 @@ function downloadUpdate() {
|
|
|
1606
1763
|
if (window.NativeUpdate?.downloadAndInstall) {
|
|
1607
1764
|
try {
|
|
1608
1765
|
window.NativeUpdate.downloadAndInstall(url)
|
|
1609
|
-
toast('
|
|
1766
|
+
toast(t('update.downloadStarted'), 'ok')
|
|
1610
1767
|
} catch (e) {
|
|
1611
|
-
toast('
|
|
1768
|
+
toast(t('update.downloadFailed', { msg: e?.message || '' }), 'err')
|
|
1612
1769
|
}
|
|
1613
1770
|
return
|
|
1614
1771
|
}
|
|
1615
1772
|
// 兜底: 旧版 App 没有原生桥时用浏览器下载
|
|
1616
|
-
toast('
|
|
1773
|
+
toast(t('update.installUnsupported'), 'err')
|
|
1617
1774
|
}
|
|
1618
1775
|
// 浏览器: 直接触发下载
|
|
1619
1776
|
location.href = url
|
|
@@ -1630,7 +1787,7 @@ async function ensureNotify() {
|
|
|
1630
1787
|
const p = await L.requestPermissions()
|
|
1631
1788
|
return p?.display === 'granted'
|
|
1632
1789
|
} catch (e) {
|
|
1633
|
-
toast('
|
|
1790
|
+
toast(t('notify.permissionFailed', { msg: e?.message || '' }), 'err')
|
|
1634
1791
|
return false
|
|
1635
1792
|
}
|
|
1636
1793
|
}
|
|
@@ -1674,7 +1831,7 @@ function showView(id) {
|
|
|
1674
1831
|
function updateConn() {
|
|
1675
1832
|
const ok = !!state.streamsOk?.mux
|
|
1676
1833
|
const el = $('conn-badge')
|
|
1677
|
-
el.textContent = ok ? '
|
|
1834
|
+
el.textContent = ok ? t('conn.on') : t('conn.off')
|
|
1678
1835
|
el.className = 'conn-badge ' + (ok ? 'on' : 'off')
|
|
1679
1836
|
}
|
|
1680
1837
|
|
|
@@ -1689,46 +1846,81 @@ function applyPairUrl(url) {
|
|
|
1689
1846
|
try {
|
|
1690
1847
|
const u = new URL(String(url).trim())
|
|
1691
1848
|
if (u.protocol !== 'dshremote:' || u.hostname !== 'pair') return false
|
|
1692
|
-
const
|
|
1849
|
+
const tok = (u.searchParams.get('token') || '').trim()
|
|
1693
1850
|
const server = (u.searchParams.get('server') || '').trim().replace(/\/+$/, '')
|
|
1694
|
-
if (!
|
|
1695
|
-
state.token =
|
|
1696
|
-
LS.set('token',
|
|
1851
|
+
if (!tok || !/^https?:\/\//i.test(server)) return false
|
|
1852
|
+
state.token = tok
|
|
1853
|
+
LS.set('token', tok)
|
|
1697
1854
|
state.server = server
|
|
1698
1855
|
if (!state.servers.includes(server)) state.servers.unshift(server)
|
|
1699
1856
|
saveServers()
|
|
1700
1857
|
renderServers()
|
|
1701
|
-
$('token-desc').textContent = '
|
|
1858
|
+
$('token-desc').textContent = t('token.savedScan')
|
|
1702
1859
|
return true
|
|
1703
1860
|
} catch {
|
|
1704
1861
|
return false
|
|
1705
1862
|
}
|
|
1706
1863
|
}
|
|
1707
1864
|
|
|
1708
|
-
/**
|
|
1865
|
+
/** 拍照/相册得到的 dataUrl → 画到 canvas → jsQR 纯本地解码(不依赖任何谷歌服务) */
|
|
1866
|
+
async function decodeQrDataUrl(dataUrl) {
|
|
1867
|
+
const img = new Image()
|
|
1868
|
+
await new Promise((resolve, reject) => {
|
|
1869
|
+
img.onload = resolve
|
|
1870
|
+
img.onerror = () => reject(new Error(t('scan.imageLoadFailed')))
|
|
1871
|
+
img.src = dataUrl
|
|
1872
|
+
})
|
|
1873
|
+
const maxSide = 1600
|
|
1874
|
+
const scale = Math.min(1, maxSide / Math.max(img.naturalWidth || 1, img.naturalHeight || 1))
|
|
1875
|
+
const w = Math.max(1, Math.round((img.naturalWidth || 1) * scale))
|
|
1876
|
+
const h = Math.max(1, Math.round((img.naturalHeight || 1) * scale))
|
|
1877
|
+
const canvas = document.createElement('canvas')
|
|
1878
|
+
canvas.width = w
|
|
1879
|
+
canvas.height = h
|
|
1880
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true })
|
|
1881
|
+
if (!ctx) throw new Error(t('scan.decodeUnsupported'))
|
|
1882
|
+
ctx.drawImage(img, 0, 0, w, h)
|
|
1883
|
+
const imageData = ctx.getImageData(0, 0, w, h)
|
|
1884
|
+
const code = window.jsQR?.(imageData.data, w, h, { inversionAttempts: 'attemptBoth' })
|
|
1885
|
+
return code?.data || ''
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/** App 内扫码: 官方 Camera 拍照/相册 + jsQR 本地解码(无 Google ML Kit/GMS 依赖, 国内可用)。
|
|
1889
|
+
* 冗余路径 1: 系统相机扫 dshremote:// 二维码直接唤起 App(见 bindNativeLinks);
|
|
1890
|
+
* 冗余路径 2: 设置页手动粘贴令牌。 */
|
|
1709
1891
|
async function scanPair() {
|
|
1710
1892
|
if (!CAP?.isNativePlatform?.()) {
|
|
1711
|
-
toast('
|
|
1893
|
+
toast(t('scan.browserHint'), 'err')
|
|
1712
1894
|
return
|
|
1713
1895
|
}
|
|
1714
|
-
const
|
|
1715
|
-
if (!
|
|
1896
|
+
const camera = CAP.Plugins?.Camera
|
|
1897
|
+
if (!camera?.getPhoto) { toast(t('scan.unsupported'), 'err'); return }
|
|
1716
1898
|
try {
|
|
1717
|
-
const
|
|
1718
|
-
if (
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1899
|
+
const perm = await camera.requestPermissions?.({ permissions: ['camera'] })
|
|
1900
|
+
if (perm && perm.camera !== 'granted') { toast(t('scan.permissionDenied'), 'err'); return }
|
|
1901
|
+
const photo = await camera.getPhoto({
|
|
1902
|
+
resultType: 'dataUrl',
|
|
1903
|
+
source: 'PROMPT', // 拍照 / 从相册选择都行, 相册可扫截图
|
|
1904
|
+
quality: 85,
|
|
1905
|
+
correctOrientation: true,
|
|
1906
|
+
saveToGallery: false,
|
|
1907
|
+
promptLabelHeader: t('scan.promptHeader'),
|
|
1908
|
+
promptLabelPhoto: t('scan.promptPhoto'),
|
|
1909
|
+
promptLabelPicture: t('scan.promptGallery'),
|
|
1910
|
+
})
|
|
1911
|
+
if (!photo?.dataUrl) { toast(t('scan.noPhoto'), 'err'); return }
|
|
1912
|
+
const raw = await decodeQrDataUrl(photo.dataUrl)
|
|
1913
|
+
if (!raw) { toast(t('scan.noQr'), 'err'); return }
|
|
1722
1914
|
if (applyPairUrl(raw)) {
|
|
1723
|
-
toast('
|
|
1915
|
+
toast(t('scan.paired'), 'ok')
|
|
1724
1916
|
openStreams()
|
|
1725
1917
|
refreshAll()
|
|
1726
1918
|
} else {
|
|
1727
|
-
toast('
|
|
1919
|
+
toast(t('scan.notPair'), 'err')
|
|
1728
1920
|
}
|
|
1729
1921
|
} catch (e) {
|
|
1730
1922
|
const msg = String(e?.message || e || '')
|
|
1731
|
-
toast(
|
|
1923
|
+
toast(/cancel/i.test(msg) ? t('scan.cancelled') : t('scan.failed', { msg }), 'err')
|
|
1732
1924
|
}
|
|
1733
1925
|
}
|
|
1734
1926
|
|
|
@@ -1738,7 +1930,7 @@ function bindNativeLinks() {
|
|
|
1738
1930
|
try {
|
|
1739
1931
|
CAP.Plugins?.App?.addListener?.('appUrlOpen', (data) => {
|
|
1740
1932
|
if (data?.url && applyPairUrl(data.url)) {
|
|
1741
|
-
toast('
|
|
1933
|
+
toast(t('scan.pairedLink'), 'ok')
|
|
1742
1934
|
openStreams()
|
|
1743
1935
|
refreshAll()
|
|
1744
1936
|
}
|
|
@@ -1759,11 +1951,30 @@ function initToken() {
|
|
|
1759
1951
|
state.token = LS.get('token', '')
|
|
1760
1952
|
}
|
|
1761
1953
|
loadServers()
|
|
1762
|
-
$('token-desc').textContent = state.token ? '
|
|
1763
|
-
$('server-desc').textContent = state.server || '
|
|
1954
|
+
$('token-desc').textContent = state.token ? t('token.savedLocal') : t('token.notSet')
|
|
1955
|
+
$('server-desc').textContent = state.server || t('servers.defaultDesc')
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function renderLangBtn() {
|
|
1959
|
+
const btn = $('btn-lang')
|
|
1960
|
+
if (btn) btn.textContent = I18N.lang === 'zh' ? 'EN' : '中文'
|
|
1764
1961
|
}
|
|
1765
1962
|
|
|
1766
1963
|
function bindUi() {
|
|
1964
|
+
renderLangBtn()
|
|
1965
|
+
$('btn-lang').addEventListener('click', () => {
|
|
1966
|
+
I18N.setLang(I18N.lang === 'zh' ? 'en' : 'zh')
|
|
1967
|
+
renderLangBtn()
|
|
1968
|
+
renderServers()
|
|
1969
|
+
renderSessions()
|
|
1970
|
+
renderPending(); renderQueue(); renderJobs()
|
|
1971
|
+
updateConn()
|
|
1972
|
+
if (state.current) { renderSessionTitle(); renderSessionSub(); renderSessionCards(); renderHistory(true) }
|
|
1973
|
+
else renderModelMenu()
|
|
1974
|
+
loadLocalVersion()
|
|
1975
|
+
if (state.hostInfo) $('host-desc').textContent = t('settings.hostDesc', { version: state.hostInfo.version, cwd: state.hostInfo.cwd, n: state.hostInfo.attachedSessions })
|
|
1976
|
+
$('token-desc').textContent = state.token ? t('token.savedLocal') : t('token.notSet')
|
|
1977
|
+
})
|
|
1767
1978
|
renderServers()
|
|
1768
1979
|
// 底部导航
|
|
1769
1980
|
document.querySelectorAll('.nav-btn').forEach(b =>
|
|
@@ -1776,13 +1987,19 @@ function bindUi() {
|
|
|
1776
1987
|
$('btn-back').addEventListener('click', closeSession)
|
|
1777
1988
|
$('btn-stats').addEventListener('click', () => { renderSessionCards(); $('modal-stats').classList.remove('hidden') })
|
|
1778
1989
|
$('stats-close').addEventListener('click', () => $('modal-stats').classList.add('hidden'))
|
|
1779
|
-
$('btn-refresh').addEventListener('click', () => { toast('
|
|
1990
|
+
$('btn-refresh').addEventListener('click', () => { toast(t('common.refreshing')); openStreams(); refreshAll() })
|
|
1780
1991
|
$('btn-admin').addEventListener('click', () => {
|
|
1781
1992
|
location.href = state.server ? state.server.replace(/\/+$/, '') + '/admin' : 'admin'
|
|
1782
1993
|
})
|
|
1783
1994
|
$('btn-new-session').addEventListener('click', newSession)
|
|
1784
1995
|
$('btn-cancel').addEventListener('click', cancelSession)
|
|
1785
1996
|
$('btn-send').addEventListener('click', sendMessage)
|
|
1997
|
+
$('btn-plus').addEventListener('click', toggleComposerMenu)
|
|
1998
|
+
$('composer-menu').addEventListener('click', (e) => {
|
|
1999
|
+
const chip = e.target.closest('[data-cmd]')
|
|
2000
|
+
if (chip) { hideComposerMenu(); sendSessionText(chip.dataset.cmd) }
|
|
2001
|
+
})
|
|
2002
|
+
$('btn-model-refresh').addEventListener('click', loadSessionModels)
|
|
1786
2003
|
const input = $('composer-input')
|
|
1787
2004
|
input.addEventListener('input', () => autosize(input))
|
|
1788
2005
|
input.addEventListener('keydown', (e) => {
|
|
@@ -1807,8 +2024,8 @@ function bindUi() {
|
|
|
1807
2024
|
// 设置
|
|
1808
2025
|
$('btn-scan-pair').addEventListener('click', scanPair)
|
|
1809
2026
|
$('btn-change-token').addEventListener('click', () => {
|
|
1810
|
-
const
|
|
1811
|
-
if (
|
|
2027
|
+
const input = prompt(t('token.prompt'), state.token)
|
|
2028
|
+
if (input && input.trim()) { state.token = input.trim(); LS.set('token', input.trim()); $('token-desc').textContent = t('token.saved'); toast(t('token.savedReconnect'), 'ok'); openStreams(); refreshAll() }
|
|
1812
2029
|
})
|
|
1813
2030
|
$('btn-server-speed').addEventListener('click', () => selectFastestServer({ silent: false }))
|
|
1814
2031
|
$('btn-server-add').addEventListener('click', addServer)
|
|
@@ -1816,16 +2033,16 @@ function bindUi() {
|
|
|
1816
2033
|
if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); addServer() }
|
|
1817
2034
|
})
|
|
1818
2035
|
$('btn-host-describe').addEventListener('click', async () => {
|
|
1819
|
-
const v = await safeRpc('host.describe', {}, '
|
|
2036
|
+
const v = await safeRpc('host.describe', {}, t('settings.probeFailed'))
|
|
1820
2037
|
if (v) {
|
|
1821
2038
|
state.hostInfo = v
|
|
1822
|
-
$('host-desc').textContent =
|
|
2039
|
+
$('host-desc').textContent = t('settings.hostDesc', { version: v.version, cwd: v.cwd, n: v.attachedSessions })
|
|
1823
2040
|
}
|
|
1824
2041
|
})
|
|
1825
2042
|
$('btn-check-update').addEventListener('click', () => checkUpdate(false))
|
|
1826
2043
|
$('btn-download-update').addEventListener('click', downloadUpdate)
|
|
1827
2044
|
$('btn-reset').addEventListener('click', () => {
|
|
1828
|
-
if (!confirm('
|
|
2045
|
+
if (!confirm(t('settings.confirmReset'))) return
|
|
1829
2046
|
LS.del('token'); LS.del('notify'); LS.del('server')
|
|
1830
2047
|
location.reload()
|
|
1831
2048
|
})
|
|
@@ -1833,7 +2050,7 @@ function bindUi() {
|
|
|
1833
2050
|
$('opt-notify').addEventListener('change', async (e) => {
|
|
1834
2051
|
if (e.target.checked) {
|
|
1835
2052
|
const ok = await ensureNotify()
|
|
1836
|
-
if (!ok) { e.target.checked = false; return toast('
|
|
2053
|
+
if (!ok) { e.target.checked = false; return toast(t('settings.notifyDenied')) }
|
|
1837
2054
|
}
|
|
1838
2055
|
LS.set('notify', e.target.checked ? '1' : '0')
|
|
1839
2056
|
})
|
|
@@ -1841,12 +2058,12 @@ function bindUi() {
|
|
|
1841
2058
|
$('opt-tools').addEventListener('change', (e) => {
|
|
1842
2059
|
LS.set('showTools', e.target.checked ? '1' : '0')
|
|
1843
2060
|
if (state.current) renderHistory(true)
|
|
1844
|
-
toast(e.target.checked ? '
|
|
2061
|
+
toast(e.target.checked ? t('settings.toolsShown') : t('settings.toolsHidden'), 'ok')
|
|
1845
2062
|
})
|
|
1846
2063
|
|
|
1847
2064
|
// 文件页
|
|
1848
2065
|
$('fs-up').addEventListener('click', fsUp)
|
|
1849
|
-
$('fs-refresh').addEventListener('click', () => { toast('
|
|
2066
|
+
$('fs-refresh').addEventListener('click', () => { toast(t('common.refreshing')); loadFs() })
|
|
1850
2067
|
$('fs-upload-btn').addEventListener('click', () => $('fs-file-input').click())
|
|
1851
2068
|
$('fs-file-input').addEventListener('change', (e) => {
|
|
1852
2069
|
const f = e.target.files?.[0]
|
|
@@ -1896,6 +2113,7 @@ function applyNativeInsets() {
|
|
|
1896
2113
|
async function boot() {
|
|
1897
2114
|
initToken()
|
|
1898
2115
|
bindUi()
|
|
2116
|
+
renderLangBtn()
|
|
1899
2117
|
bindNativeBack()
|
|
1900
2118
|
bindNativeLinks()
|
|
1901
2119
|
applyNativeInsets()
|
|
@@ -1903,14 +2121,14 @@ async function boot() {
|
|
|
1903
2121
|
loadLocalVersion()
|
|
1904
2122
|
if (!state.token) {
|
|
1905
2123
|
showView('view-settings')
|
|
1906
|
-
$('token-desc').textContent = '
|
|
2124
|
+
$('token-desc').textContent = t('token.notSetHint')
|
|
1907
2125
|
} else {
|
|
1908
2126
|
// 多服务器: 启动时静默测速一次, 选最快的连接(同源页面也参与比较)
|
|
1909
2127
|
await selectFastestServer({ silent: true, reconnect: false })
|
|
1910
2128
|
openStreams()
|
|
1911
2129
|
await refreshAll()
|
|
1912
2130
|
const host = await safeRpc('host.describe', {}, '')
|
|
1913
|
-
if (host) { state.hostInfo = host; $('host-desc').textContent =
|
|
2131
|
+
if (host) { state.hostInfo = host; $('host-desc').textContent = t('settings.hostDesc', { version: host.version, cwd: host.cwd, n: host.attachedSessions }) }
|
|
1914
2132
|
// 启动后自动检查一次更新(静默)
|
|
1915
2133
|
setTimeout(() => checkUpdate(true), 4000)
|
|
1916
2134
|
}
|