dsh-remote-plugin 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +5 -0
- package/index.mjs +68 -3
- package/package.json +1 -1
- package/public/admin.html +17 -0
- package/public/admin.js +9 -0
- package/public/app.js +314 -11
- package/public/desktop/desktop.html +158 -15
- package/public/desktop/desktop.js +183 -0
- package/public/donate.png +0 -0
- package/public/index.html +267 -85
- package/public/update.json +4 -4
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -269,11 +269,15 @@ function renderStats(days) {
|
|
|
269
269
|
}).join('')
|
|
270
270
|
}
|
|
271
271
|
async function rpc(method, payload = {}) {
|
|
272
|
-
const
|
|
272
|
+
const opts = {
|
|
273
273
|
method: 'POST',
|
|
274
274
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' },
|
|
275
275
|
body: JSON.stringify({ type: 'client-request', rpcId: uuid(), method, payload })
|
|
276
|
-
}
|
|
276
|
+
}
|
|
277
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
278
|
+
opts.signal = AbortSignal.timeout(20000)
|
|
279
|
+
}
|
|
280
|
+
const res = await fetch(apiUrl('/api/' + method), opts)
|
|
277
281
|
if (res.status === 401) throw new Error('AUTH')
|
|
278
282
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
279
283
|
const full = await res.json()
|
|
@@ -451,6 +455,7 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
|
451
455
|
if (srv) state.groupActive[state.activeGroup] = srv.id
|
|
452
456
|
}
|
|
453
457
|
saveServers()
|
|
458
|
+
syncBgConfig()
|
|
454
459
|
if (!silent) {
|
|
455
460
|
if (chosen) toast(t('speed.switched', { url: chosen, ms: Number.isFinite(ms) ? ms : 0 }), 'ok')
|
|
456
461
|
else toast(t('speed.switchedOrigin'), 'ok')
|
|
@@ -976,6 +981,10 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
976
981
|
else renderSessions()
|
|
977
982
|
}function titleOf(s) { return proj(s, 'title') || short(s.sessionId) }
|
|
978
983
|
function short(id) { return '…' + String(id).slice(-8) }
|
|
984
|
+
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
985
|
+
function isGoalTerminal(goal) {
|
|
986
|
+
return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
|
|
987
|
+
}
|
|
979
988
|
function goalOf(s) {
|
|
980
989
|
const p = proj(s, 'goal')
|
|
981
990
|
if (!p) return null
|
|
@@ -1028,8 +1037,10 @@ async function openSession(id) {
|
|
|
1028
1037
|
state.history = emptyHistory()
|
|
1029
1038
|
document.body.classList.add('in-session')
|
|
1030
1039
|
showView('view-session')
|
|
1040
|
+
$('session-cards').innerHTML = ''
|
|
1031
1041
|
renderSessionTitle(); renderSessionSub(); updateCancelBtn(); updateSessionStatus()
|
|
1032
1042
|
$('history').innerHTML = '<div class="empty">' + t('history.loading') + '</div>'
|
|
1043
|
+
restoreCachedHistory()
|
|
1033
1044
|
await loadHistory(true)
|
|
1034
1045
|
renderSessionCards()
|
|
1035
1046
|
refreshSessions()
|
|
@@ -1049,7 +1060,7 @@ function bindNativeBack() {
|
|
|
1049
1060
|
try {
|
|
1050
1061
|
CAP.Plugins?.App?.addListener?.('backButton', () => {
|
|
1051
1062
|
const openModal = [...document.querySelectorAll('.modal')].find(m => !m.classList.contains('hidden'))
|
|
1052
|
-
if (openModal) { openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1063
|
+
if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else openModal.classList.add('hidden'); return } // 先关弹窗
|
|
1053
1064
|
if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
|
|
1054
1065
|
if (!$('view-files').classList.contains('hidden')) { // 文件页 → 上级目录 → 主页
|
|
1055
1066
|
if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) { fsUp(); return }
|
|
@@ -1192,8 +1203,12 @@ async function loadHistory(reset) {
|
|
|
1192
1203
|
trimVisible()
|
|
1193
1204
|
state.history.hasMore = !!v.hasMore
|
|
1194
1205
|
state.history.loading = false
|
|
1195
|
-
|
|
1196
|
-
|
|
1206
|
+
try {
|
|
1207
|
+
if (reset) renderHistory(true)
|
|
1208
|
+
else if (added) renderHistory(false, 'keep')
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
console.error('renderHistory failed', e)
|
|
1211
|
+
}
|
|
1197
1212
|
if (moreBtn) moreBtn.classList.toggle('hidden', !state.history.hasMore)
|
|
1198
1213
|
$('history-hint').textContent = state.history.visible.length ? t('history.count', { n: state.history.visible.length }) : ''
|
|
1199
1214
|
scheduleHistoryCacheSave()
|
|
@@ -1338,6 +1353,13 @@ function shouldShowEvent(type) {
|
|
|
1338
1353
|
if (INTERESTING_EVENTS.has(type)) return true
|
|
1339
1354
|
return false
|
|
1340
1355
|
}
|
|
1356
|
+
function systemReminderText(blocks) {
|
|
1357
|
+
if (!Array.isArray(blocks)) return ''
|
|
1358
|
+
return blocks
|
|
1359
|
+
.filter(b => b && typeof b === 'object' && b.type === 'text' && String(b.text ?? '').trimStart().startsWith('<system-reminder>'))
|
|
1360
|
+
.map(b => String(b.text ?? ''))
|
|
1361
|
+
.join('\n')
|
|
1362
|
+
}
|
|
1341
1363
|
function eventHtml(entry, ctx = {}) {
|
|
1342
1364
|
const seq = entry.seq
|
|
1343
1365
|
const ev = entry.event || {}
|
|
@@ -1350,7 +1372,12 @@ function eventHtml(entry, ctx = {}) {
|
|
|
1350
1372
|
const msg = data.message || {}
|
|
1351
1373
|
const role = data.role || msg.role || (type.startsWith('user') ? 'user' : 'assistant')
|
|
1352
1374
|
const blocks = msg.content || data.content || []
|
|
1353
|
-
|
|
1375
|
+
const sysText = type === 'user/message' ? systemReminderText(blocks) : ''
|
|
1376
|
+
if (sysText) {
|
|
1377
|
+
inner = `<details class="event" data-seq="${seq}"><summary>${esc(t('event.systemReminder'))}</summary><pre>${esc(truncate(sysText, 400))}</pre></details>`
|
|
1378
|
+
} else {
|
|
1379
|
+
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>`
|
|
1380
|
+
}
|
|
1354
1381
|
} else if (type === 'tool/call') {
|
|
1355
1382
|
const name = data.name || data.toolName || t('tool.default')
|
|
1356
1383
|
const step = (data.turn != null ? ` · turn ${data.turn}` : '') + (data.step != null ? `.${data.step}` : '')
|
|
@@ -1436,12 +1463,13 @@ async function renderSessionCards() {
|
|
|
1436
1463
|
const box = $('session-cards')
|
|
1437
1464
|
const statsBox = $('stats-body')
|
|
1438
1465
|
if (!s) { box.innerHTML = ''; if (statsBox) statsBox.innerHTML = ''; return }
|
|
1439
|
-
if (statsBox) statsBox.innerHTML = statsHtml(s)
|
|
1466
|
+
if (statsBox) { try { statsBox.innerHTML = statsHtml(s) } catch {} }
|
|
1467
|
+
box.innerHTML = ''
|
|
1440
1468
|
const goal = goalOf(s)
|
|
1441
1469
|
const todos = proj(s, 'todos')
|
|
1442
1470
|
let html = ''
|
|
1443
1471
|
|
|
1444
|
-
if (goal) {
|
|
1472
|
+
if (goal && !isGoalTerminal(goal)) {
|
|
1445
1473
|
html += `<div class="card"><div class="card-title">${t('goal.title')}</div>
|
|
1446
1474
|
<div class="goal-obj">${esc(goal.objective || '')}</div>
|
|
1447
1475
|
<div class="goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
|
|
@@ -1476,6 +1504,16 @@ async function renderSessionCards() {
|
|
|
1476
1504
|
}
|
|
1477
1505
|
}
|
|
1478
1506
|
|
|
1507
|
+
function setGoalPhaseLocal(phase) {
|
|
1508
|
+
const s = state.byId.get(state.current)
|
|
1509
|
+
const p = s && proj(s, 'goal')
|
|
1510
|
+
const goal = p && typeof p === 'object' && p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
1511
|
+
if (!goal) return
|
|
1512
|
+
goal.phase = phase
|
|
1513
|
+
renderSessions()
|
|
1514
|
+
renderSessionCards()
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1479
1517
|
async function goalAction(kind) {
|
|
1480
1518
|
const s = state.byId.get(state.current)
|
|
1481
1519
|
const goal = goalOf(s)
|
|
@@ -1488,6 +1526,8 @@ async function goalAction(kind) {
|
|
|
1488
1526
|
if (kind === 'clear' && !confirm(t('goal.confirmClear'))) return
|
|
1489
1527
|
if (kind === 'complete' && !confirm(t('goal.confirmComplete'))) return
|
|
1490
1528
|
await safeRpc(method, { sessionId: state.current, ref }, t('goal.actionFailed'))
|
|
1529
|
+
if (kind === 'complete') setGoalPhaseLocal('complete')
|
|
1530
|
+
if (kind === 'clear') setGoalPhaseLocal('cleared')
|
|
1491
1531
|
toast(t('goal.actionSubmitted'), 'ok')
|
|
1492
1532
|
scheduleRefresh()
|
|
1493
1533
|
}
|
|
@@ -1500,9 +1540,34 @@ async function interruptSubagent(childId) {
|
|
|
1500
1540
|
}
|
|
1501
1541
|
|
|
1502
1542
|
/* ---------------- 发送 / 取消 / 快捷菜单 ---------------- */
|
|
1543
|
+
async function runSlashCommand(text) {
|
|
1544
|
+
const clean = String(text || '').trim()
|
|
1545
|
+
if (!clean.startsWith('/') || !state.current) return false
|
|
1546
|
+
try {
|
|
1547
|
+
const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
1548
|
+
? AbortSignal.timeout(20000)
|
|
1549
|
+
: undefined
|
|
1550
|
+
const res = await fetch(apiUrl('/remote/api/command'), {
|
|
1551
|
+
method: 'POST',
|
|
1552
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' },
|
|
1553
|
+
body: JSON.stringify({ sessionId: state.current, line: clean }),
|
|
1554
|
+
...(signal ? { signal } : {})
|
|
1555
|
+
})
|
|
1556
|
+
if (res.status === 401) { authFailure(); return true }
|
|
1557
|
+
if (!res.ok) return false
|
|
1558
|
+
const data = await res.json().catch(() => null)
|
|
1559
|
+
if (data?.ok === false) { toast(data.message || t('send.failed'), 'err'); return true }
|
|
1560
|
+
if (data?.ok && data.executed === true) { toast(t('send.commandExecuted'), 'ok'); return true }
|
|
1561
|
+
} catch (e) {
|
|
1562
|
+
console.error('slash command bridge failed', e)
|
|
1563
|
+
}
|
|
1564
|
+
return false
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1503
1567
|
async function sendSessionText(text) {
|
|
1504
1568
|
const clean = String(text || '').trim()
|
|
1505
1569
|
if (!clean || !state.current) return false
|
|
1570
|
+
if (await runSlashCommand(clean)) return true
|
|
1506
1571
|
$('btn-send').disabled = true
|
|
1507
1572
|
const v = await safeRpc('session.prompt', {
|
|
1508
1573
|
sessionId: state.current,
|
|
@@ -2240,6 +2305,54 @@ async function loadLocalVersion() {
|
|
|
2240
2305
|
$('update-desc').textContent = state.localVersion ? t('update.currentV', { version: state.localVersion }) : t('update.noVersion')
|
|
2241
2306
|
}
|
|
2242
2307
|
|
|
2308
|
+
/* ---------------- 更新内容弹窗 ---------------- */
|
|
2309
|
+
const NOTES_KEY = 'seenNotesVersion'
|
|
2310
|
+
let notesVersion = ''
|
|
2311
|
+
let notesPages = []
|
|
2312
|
+
let notesPage = 0
|
|
2313
|
+
function splitNotes(notes) {
|
|
2314
|
+
return String(notes || '').split(/[;;]/).map(s => s.trim()).filter(Boolean)
|
|
2315
|
+
}
|
|
2316
|
+
function renderNotesPages(items) {
|
|
2317
|
+
const box = $('notes-pages')
|
|
2318
|
+
if (!box) return
|
|
2319
|
+
const pages = []
|
|
2320
|
+
for (let i = 0; i < items.length; i += 3) pages.push(items.slice(i, i + 3))
|
|
2321
|
+
notesPages = pages
|
|
2322
|
+
notesPage = 0
|
|
2323
|
+
box.innerHTML = pages.map(page => `<div class="notes-page" style="flex:0 0 100%;scroll-snap-align:start;box-sizing:border-box;min-width:0;">${page.map(item => `<div class="notes-item" style="padding:6px 0;line-height:1.5;">${esc(item)}</div>`).join('')}</div>`).join('')
|
|
2324
|
+
box.scrollLeft = 0
|
|
2325
|
+
updateNotesPage()
|
|
2326
|
+
}
|
|
2327
|
+
function updateNotesPage() {
|
|
2328
|
+
const box = $('notes-pages')
|
|
2329
|
+
const pageEl = $('notes-page')
|
|
2330
|
+
if (!box || !pageEl) return
|
|
2331
|
+
const total = notesPages.length || 1
|
|
2332
|
+
const idx = Math.min(Math.max(0, Math.round(box.scrollLeft / Math.max(1, box.clientWidth))), total - 1)
|
|
2333
|
+
notesPage = idx
|
|
2334
|
+
pageEl.textContent = t('notes.page', { current: idx + 1, total })
|
|
2335
|
+
}
|
|
2336
|
+
function scrollNotes(dir) {
|
|
2337
|
+
const box = $('notes-pages')
|
|
2338
|
+
if (box) box.scrollBy({ left: dir * box.clientWidth, behavior: 'smooth' })
|
|
2339
|
+
}
|
|
2340
|
+
function openNotesModal(info) {
|
|
2341
|
+
if (!info?.version || String(info.version).includes('-rc')) return
|
|
2342
|
+
if (LS.get(NOTES_KEY) === info.version) return
|
|
2343
|
+
const items = splitNotes(info.notes)
|
|
2344
|
+
if (!items.length) return
|
|
2345
|
+
notesVersion = info.version
|
|
2346
|
+
const vEl = $('notes-version')
|
|
2347
|
+
if (vEl) vEl.textContent = 'v' + info.version
|
|
2348
|
+
renderNotesPages(items)
|
|
2349
|
+
$('modal-notes').classList.remove('hidden')
|
|
2350
|
+
}
|
|
2351
|
+
function closeNotesModal() {
|
|
2352
|
+
$('modal-notes').classList.add('hidden')
|
|
2353
|
+
if (notesVersion) { LS.set(NOTES_KEY, notesVersion); notesVersion = '' }
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2243
2356
|
async function checkUpdate(silent) {
|
|
2244
2357
|
if (!state.localVersion) {
|
|
2245
2358
|
$('update-desc').textContent = t('update.noVersion')
|
|
@@ -2259,6 +2372,7 @@ async function checkUpdate(silent) {
|
|
|
2259
2372
|
const res = await fetch(base + '/update.json?t=' + Date.now() + '&local=' + encodeURIComponent(state.localVersion))
|
|
2260
2373
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
2261
2374
|
const info = await res.json()
|
|
2375
|
+
openNotesModal(info)
|
|
2262
2376
|
if (info.version && cmpVersion(info.version, state.localVersion) > 0) {
|
|
2263
2377
|
state.updateInfo = info
|
|
2264
2378
|
const hasNotes = !!(info.notes && String(info.notes).trim())
|
|
@@ -2401,6 +2515,108 @@ function notify(title, body) {
|
|
|
2401
2515
|
} catch {}
|
|
2402
2516
|
}
|
|
2403
2517
|
|
|
2518
|
+
/* ---------------- 后台轮询(Android 前台服务) ---------------- */
|
|
2519
|
+
function bgBridge() { return window.NativeBackground }
|
|
2520
|
+
function bgBase() { return (state.server || location.origin || '').replace(/\/+$/, '') }
|
|
2521
|
+
function applyBgConfigFromNative() {
|
|
2522
|
+
const b = bgBridge()
|
|
2523
|
+
if (!b?.getBackgroundConfig) return
|
|
2524
|
+
try {
|
|
2525
|
+
const cfg = JSON.parse(b.getBackgroundConfig() || '{}')
|
|
2526
|
+
$('opt-bg-poll').checked = !!cfg.enabled
|
|
2527
|
+
const v = String(cfg.intervalMin ?? 1)
|
|
2528
|
+
const opts = Array.from($('bg-interval')?.options || [])
|
|
2529
|
+
if (opts.some(o => o.value === v)) $('bg-interval').value = v
|
|
2530
|
+
if ($('opt-task-done')) $('opt-task-done').checked = cfg.notifyTaskDone !== false
|
|
2531
|
+
$('bg-auth-status')?.classList.toggle('hidden', !cfg.loginExpired)
|
|
2532
|
+
} catch {}
|
|
2533
|
+
}
|
|
2534
|
+
function saveBgConfig(enabled) {
|
|
2535
|
+
const b = bgBridge()
|
|
2536
|
+
if (!b?.saveBackgroundConfig) return false
|
|
2537
|
+
const base = bgBase()
|
|
2538
|
+
const intervalMin = parseFloat($('bg-interval')?.value || '1') || 1
|
|
2539
|
+
const notifyTaskDone = $('opt-task-done')?.checked !== false
|
|
2540
|
+
b.saveBackgroundConfig(JSON.stringify({ enabled, intervalMin, base, token: state.token || '', notifyTaskDone }))
|
|
2541
|
+
if (enabled) $('bg-auth-status')?.classList.add('hidden')
|
|
2542
|
+
return true
|
|
2543
|
+
}
|
|
2544
|
+
function syncBgConfig() {
|
|
2545
|
+
if ($('opt-bg-poll')?.checked && state.token) saveBgConfig(true)
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/* ---------------- 预设提示词 ---------------- */
|
|
2549
|
+
const PRESETS_KEY = 'dshPromptPresets'
|
|
2550
|
+
const PRESET_NAME_MAX = 20
|
|
2551
|
+
const PRESET_TEXT_MAX = 2000
|
|
2552
|
+
const PRESET_LIMIT = 20
|
|
2553
|
+
function readPresets() {
|
|
2554
|
+
try {
|
|
2555
|
+
const v = JSON.parse(LS.get(PRESETS_KEY, '[]') || '[]')
|
|
2556
|
+
return Array.isArray(v) ? v.filter(p => p && typeof p.id === 'string' && typeof p.name === 'string' && typeof p.text === 'string') : []
|
|
2557
|
+
} catch { return [] }
|
|
2558
|
+
}
|
|
2559
|
+
function writePresets(list) {
|
|
2560
|
+
LS.set(PRESETS_KEY, JSON.stringify(list))
|
|
2561
|
+
renderPresets()
|
|
2562
|
+
renderPresetMenu()
|
|
2563
|
+
}
|
|
2564
|
+
function renderPresets() {
|
|
2565
|
+
const box = $('preset-list')
|
|
2566
|
+
if (!box) return
|
|
2567
|
+
const list = readPresets()
|
|
2568
|
+
if (!list.length) {
|
|
2569
|
+
box.innerHTML = `<div class="server-empty">${esc(t('presets.empty'))}</div>`
|
|
2570
|
+
return
|
|
2571
|
+
}
|
|
2572
|
+
box.innerHTML = list.map(p => `<div class="server-row">
|
|
2573
|
+
<div class="server-main"><div class="server-note">${esc(p.name)}</div><div class="server-url">${esc((p.text || '').slice(0, 60))}</div></div>
|
|
2574
|
+
<button class="mini-btn" data-preset-edit="${esc(p.id)}">${t('presets.edit')}</button>
|
|
2575
|
+
<button class="mini-btn" data-preset-del="${esc(p.id)}">${t('presets.delete')}</button>
|
|
2576
|
+
</div>`).join('')
|
|
2577
|
+
box.querySelectorAll('[data-preset-edit]').forEach(b => b.addEventListener('click', () => editPreset(b.dataset.presetEdit)))
|
|
2578
|
+
box.querySelectorAll('[data-preset-del]').forEach(b => b.addEventListener('click', () => deletePreset(b.dataset.presetDel)))
|
|
2579
|
+
}
|
|
2580
|
+
function renderPresetMenu() {
|
|
2581
|
+
const group = $('preset-menu-group')
|
|
2582
|
+
const listBox = $('preset-menu-list')
|
|
2583
|
+
if (!group || !listBox) return
|
|
2584
|
+
const list = readPresets()
|
|
2585
|
+
group.classList.toggle('hidden', !list.length)
|
|
2586
|
+
listBox.innerHTML = list.map(p => `<button class="menu-chip" data-preset="${esc(p.id)}">${esc(p.name)}</button>`).join('')
|
|
2587
|
+
}
|
|
2588
|
+
function promptPreset(id) {
|
|
2589
|
+
const list = readPresets()
|
|
2590
|
+
const existing = id ? list.find(p => p.id === id) : null
|
|
2591
|
+
const name = prompt(t('presets.namePrompt'), existing?.name || '')
|
|
2592
|
+
if (name == null) return
|
|
2593
|
+
const text = prompt(t('presets.textPrompt'), existing?.text || '')
|
|
2594
|
+
if (text == null) return
|
|
2595
|
+
const n = (name || '').trim()
|
|
2596
|
+
if (!n) return toast(t('presets.nameEmpty'), 'err')
|
|
2597
|
+
if (n.length > PRESET_NAME_MAX) return toast(t('presets.nameTooLong'), 'err')
|
|
2598
|
+
if (text.length > PRESET_TEXT_MAX) return toast(t('presets.textTooLong'), 'err')
|
|
2599
|
+
if (existing) {
|
|
2600
|
+
existing.name = n
|
|
2601
|
+
existing.text = text
|
|
2602
|
+
} else {
|
|
2603
|
+
if (list.length >= PRESET_LIMIT) return toast(t('presets.limit'), 'err')
|
|
2604
|
+
list.push({ id: 'p' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), name: n, text })
|
|
2605
|
+
}
|
|
2606
|
+
writePresets(list)
|
|
2607
|
+
toast(existing ? t('presets.saved') : t('presets.added'), 'ok')
|
|
2608
|
+
}
|
|
2609
|
+
function addPreset() { promptPreset(null) }
|
|
2610
|
+
function editPreset(id) { promptPreset(id) }
|
|
2611
|
+
function deletePreset(id) {
|
|
2612
|
+
const list = readPresets()
|
|
2613
|
+
const p = list.find(x => x.id === id)
|
|
2614
|
+
if (!p) return
|
|
2615
|
+
if (!confirm(t('presets.confirmDelete', { name: p.name }))) return
|
|
2616
|
+
writePresets(list.filter(x => x.id !== id))
|
|
2617
|
+
toast(t('presets.deleted'), 'ok')
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2404
2620
|
/* ---------------- 峰谷计费提醒(每天 9/12/14/18 点本地通知) ---------------- */
|
|
2405
2621
|
const PEAK_REMIND_NOTIFS = [
|
|
2406
2622
|
{ id: 8801, hour: 9, periodKey: 'peak0912', enterKey: 'enterPeak' },
|
|
@@ -2446,6 +2662,23 @@ function showView(id) {
|
|
|
2446
2662
|
window.scrollTo(0, 0)
|
|
2447
2663
|
if (id === 'view-files' && !state.fs.loaded) loadFs(null, { silent: true })
|
|
2448
2664
|
if (id === 'view-stats') loadStats()
|
|
2665
|
+
if (id === 'view-settings') showSettingsHome()
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
const SETTINGS_GROUPS = ['general', 'servers', 'notify', 'theme', 'about']
|
|
2669
|
+
function showSettingsHome() {
|
|
2670
|
+
const home = $('settings-home')
|
|
2671
|
+
if (!home) return
|
|
2672
|
+
home.classList.remove('hidden')
|
|
2673
|
+
for (const name of SETTINGS_GROUPS) $('settings-page-' + name)?.classList.add('hidden')
|
|
2674
|
+
window.scrollTo(0, 0)
|
|
2675
|
+
}
|
|
2676
|
+
function showSettingsPage(name) {
|
|
2677
|
+
const home = $('settings-home')
|
|
2678
|
+
if (!home || !SETTINGS_GROUPS.includes(name)) return
|
|
2679
|
+
home.classList.add('hidden')
|
|
2680
|
+
for (const g of SETTINGS_GROUPS) $('settings-page-' + g)?.classList.toggle('hidden', g !== name)
|
|
2681
|
+
window.scrollTo(0, 0)
|
|
2449
2682
|
}
|
|
2450
2683
|
|
|
2451
2684
|
function updateConn() {
|
|
@@ -2489,6 +2722,7 @@ function applyPairUrl(url) {
|
|
|
2489
2722
|
saveServers()
|
|
2490
2723
|
renderServers()
|
|
2491
2724
|
$('token-desc').textContent = t('token.savedScan')
|
|
2725
|
+
syncBgConfig()
|
|
2492
2726
|
return true
|
|
2493
2727
|
} catch {
|
|
2494
2728
|
return false
|
|
@@ -2629,6 +2863,11 @@ function openThemePanel() {
|
|
|
2629
2863
|
$('modal-theme').classList.remove('hidden')
|
|
2630
2864
|
}
|
|
2631
2865
|
|
|
2866
|
+
function openDonateModal() {
|
|
2867
|
+
const m = $('modal-donate')
|
|
2868
|
+
if (m) m.classList.remove('hidden')
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2632
2871
|
function bindUi() {
|
|
2633
2872
|
renderLangBtn()
|
|
2634
2873
|
renderThemeBtn()
|
|
@@ -2649,6 +2888,17 @@ function bindUi() {
|
|
|
2649
2888
|
})
|
|
2650
2889
|
$('btn-theme').addEventListener('click', openThemePanel)
|
|
2651
2890
|
$('theme-close').addEventListener('click', () => $('modal-theme').classList.add('hidden'))
|
|
2891
|
+
$('btn-donate').addEventListener('click', openDonateModal)
|
|
2892
|
+
$('donate-close').addEventListener('click', () => $('modal-donate').classList.add('hidden'))
|
|
2893
|
+
document.addEventListener('click', (e) => {
|
|
2894
|
+
if (e.target.closest('[data-donate-open]')) openDonateModal()
|
|
2895
|
+
})
|
|
2896
|
+
// 更新内容弹窗
|
|
2897
|
+
$('notes-close').addEventListener('click', closeNotesModal)
|
|
2898
|
+
$('notes-prev').addEventListener('click', () => scrollNotes(-1))
|
|
2899
|
+
$('notes-next').addEventListener('click', () => scrollNotes(1))
|
|
2900
|
+
$('notes-pages').addEventListener('scroll', updateNotesPage)
|
|
2901
|
+
$('modal-notes').addEventListener('click', (e) => { if (e.target === $('modal-notes')) closeNotesModal() })
|
|
2652
2902
|
renderServers()
|
|
2653
2903
|
// 底部导航
|
|
2654
2904
|
document.querySelectorAll('.nav-btn').forEach(b =>
|
|
@@ -2691,9 +2941,27 @@ function bindUi() {
|
|
|
2691
2941
|
$('btn-cancel').addEventListener('click', cancelSession)
|
|
2692
2942
|
$('btn-send').addEventListener('click', sendMessage)
|
|
2693
2943
|
$('btn-plus').addEventListener('click', toggleComposerMenu)
|
|
2694
|
-
$('composer-menu').addEventListener('click', (e) => {
|
|
2944
|
+
$('composer-menu').addEventListener('click', async (e) => {
|
|
2695
2945
|
const chip = e.target.closest('[data-cmd]')
|
|
2696
|
-
if (chip) {
|
|
2946
|
+
if (chip) {
|
|
2947
|
+
const input = $('composer-input')
|
|
2948
|
+
input.value = chip.dataset.cmd + ' '
|
|
2949
|
+
input.focus()
|
|
2950
|
+
autosize(input)
|
|
2951
|
+
hideComposerMenu()
|
|
2952
|
+
return
|
|
2953
|
+
}
|
|
2954
|
+
const preset = e.target.closest('[data-preset]')
|
|
2955
|
+
if (preset) {
|
|
2956
|
+
const found = readPresets().find(x => x.id === preset.dataset.preset)
|
|
2957
|
+
if (found) {
|
|
2958
|
+
const input = $('composer-input')
|
|
2959
|
+
input.value = found.text
|
|
2960
|
+
input.focus()
|
|
2961
|
+
autosize(input)
|
|
2962
|
+
}
|
|
2963
|
+
hideComposerMenu()
|
|
2964
|
+
}
|
|
2697
2965
|
})
|
|
2698
2966
|
$('btn-model-refresh').addEventListener('click', loadSessionModels)
|
|
2699
2967
|
const input = $('composer-input')
|
|
@@ -2718,10 +2986,15 @@ function bindUi() {
|
|
|
2718
2986
|
$('goal-close').addEventListener('click', () => $('modal-goal').classList.add('hidden'))
|
|
2719
2987
|
$('goal-edit').addEventListener('click', submitGoalEdit)
|
|
2720
2988
|
// 设置
|
|
2989
|
+
$('view-settings').addEventListener('click', (e) => {
|
|
2990
|
+
const group = e.target.closest('[data-settings-group]')
|
|
2991
|
+
if (group) { showSettingsPage(group.dataset.settingsGroup); return }
|
|
2992
|
+
if (e.target.closest('[data-settings-back]')) { showSettingsHome(); return }
|
|
2993
|
+
})
|
|
2721
2994
|
$('btn-scan-pair').addEventListener('click', scanPair)
|
|
2722
2995
|
$('btn-change-token').addEventListener('click', () => {
|
|
2723
2996
|
const input = prompt(t('token.prompt'), state.token)
|
|
2724
|
-
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() }
|
|
2997
|
+
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(); syncBgConfig() }
|
|
2725
2998
|
})
|
|
2726
2999
|
$('btn-server-speed').addEventListener('click', () => selectFastestServer({ silent: false }))
|
|
2727
3000
|
$('btn-server-add').addEventListener('click', addServer)
|
|
@@ -2746,6 +3019,7 @@ function bindUi() {
|
|
|
2746
3019
|
$('btn-reset').addEventListener('click', () => {
|
|
2747
3020
|
if (!confirm(t('settings.confirmReset'))) return
|
|
2748
3021
|
LS.del('token'); LS.del('notify'); LS.del('server')
|
|
3022
|
+
if (bgBridge()?.saveBackgroundConfig) saveBgConfig(false)
|
|
2749
3023
|
location.reload()
|
|
2750
3024
|
})
|
|
2751
3025
|
$('opt-notify').checked = LS.get('notify', '0') === '1'
|
|
@@ -2775,6 +3049,35 @@ function bindUi() {
|
|
|
2775
3049
|
})
|
|
2776
3050
|
// 已开启则启动时重新调度, 防止系统清理后丢失
|
|
2777
3051
|
if (peakRemindOn() && CAP?.isNativePlatform?.()) schedulePeakReminders()
|
|
3052
|
+
applyBgConfigFromNative()
|
|
3053
|
+
$('opt-bg-poll').addEventListener('change', async (e) => {
|
|
3054
|
+
if (e.target.checked) {
|
|
3055
|
+
if (!CAP?.isNativePlatform?.() || !bgBridge()?.saveBackgroundConfig) {
|
|
3056
|
+
e.target.checked = false
|
|
3057
|
+
return toast(t('settings.bgNativeOnly'), 'err')
|
|
3058
|
+
}
|
|
3059
|
+
if (!state.token) {
|
|
3060
|
+
e.target.checked = false
|
|
3061
|
+
return toast(t('settings.bgNeedToken'), 'err')
|
|
3062
|
+
}
|
|
3063
|
+
const ok = await ensureNotify()
|
|
3064
|
+
if (!ok) { e.target.checked = false; return toast(t('settings.notifyDenied')) }
|
|
3065
|
+
saveBgConfig(true)
|
|
3066
|
+
toast(t('settings.bgOn'), 'ok')
|
|
3067
|
+
} else {
|
|
3068
|
+
saveBgConfig(false)
|
|
3069
|
+
toast(t('settings.bgOff'), 'ok')
|
|
3070
|
+
}
|
|
3071
|
+
})
|
|
3072
|
+
$('bg-interval').addEventListener('change', () => {
|
|
3073
|
+
if ($('opt-bg-poll')?.checked) saveBgConfig(true)
|
|
3074
|
+
})
|
|
3075
|
+
$('opt-task-done')?.addEventListener('change', () => {
|
|
3076
|
+
if (bgBridge()?.saveBackgroundConfig) saveBgConfig($('opt-bg-poll')?.checked)
|
|
3077
|
+
})
|
|
3078
|
+
renderPresets()
|
|
3079
|
+
renderPresetMenu()
|
|
3080
|
+
$('btn-preset-add').addEventListener('click', addPreset)
|
|
2778
3081
|
$('opt-tools').checked = LS.get('showTools', '1') !== '0'
|
|
2779
3082
|
$('opt-tools').addEventListener('change', (e) => {
|
|
2780
3083
|
LS.set('showTools', e.target.checked ? '1' : '0')
|