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/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 res = await fetch(apiUrl('/api/' + method), {
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
- if (reset) renderHistory(true)
1196
- else if (added) renderHistory(false, 'keep')
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
- 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>`
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) { hideComposerMenu(); sendSessionText(chip.dataset.cmd) }
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')