dsh-remote-plugin 0.6.17 → 0.6.19

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.
@@ -27,10 +27,11 @@ const CAP = window.Capacitor || null
27
27
 
28
28
  /* ---------------- 皮肤 ---------------- */
29
29
  const THEME_META = [
30
- { id: 'default', sw: ['#0B0E1A', '#151B33', '#5B8CFF'] },
31
- { id: 'dark', sw: ['#05348B', '#0D438F', '#F9A647'] },
32
- { id: 'light', sw: ['#EFEEEC', '#FAF8F5', '#E6BC7B'] },
33
- { id: 'neutral', sw: ['#DDD4B8', '#585818', '#832D15'] }
30
+ { id: 'default', sw: ['#0D1117', '#21262D', '#58A6FF'] },
31
+ { id: 'dark', sw: ['#161316', '#302323', '#FFB86B'] },
32
+ { id: 'light', sw: ['#F5F7F8', '#FFFFFF', '#176B87'] },
33
+ { id: 'neutral', sw: ['#EEF1E8', '#FAFBF6', '#47643C'] },
34
+ { id: 'mono', sw: ['#050505', '#333333', '#F5F5F5'] }
34
35
  ]
35
36
  function themeGet() {
36
37
  let v = LS.get('dshTheme', '')
@@ -57,12 +58,12 @@ const state = {
57
58
  token: LS.get('token', ''),
58
59
  wsTicket: { token: '', server: '', value: '', expiresAt: 0 },
59
60
  server: '',
60
- servers: [],
61
+ servers: [], // { id, url, note, group, token }; connection identity = URL + token
61
62
  groups: ['默认'],
62
63
  activeGroup: '默认',
63
64
  autoSelect: { '默认': true },
64
65
  groupActive: { '默认': '' },
65
- serverLatency: {},
66
+ serverLatency: {}, // URL + token -> latest /health latency
66
67
  gatewayHealth: {},
67
68
  selectingServer: false,
68
69
  sessions: [],
@@ -661,6 +662,27 @@ function ensureGroup(name) {
661
662
  }
662
663
  function groupServers(g) { return state.servers.filter(s => s.group === g) }
663
664
  function activeServers() { return groupServers(state.activeGroup) }
665
+ function normalizedServerToken(token) { return typeof token === 'string' ? token.trim() : '' }
666
+ function serverIdentity(url, token) {
667
+ return `${String(url || '').replace(/\/+$/, '')}\u0000${normalizedServerToken(token)}`
668
+ }
669
+ function serverKey(server) { return serverIdentity(server?.url ?? server, server?.token) }
670
+ function serverToken(server) { return normalizedServerToken(server?.token) }
671
+ function currentServerEntry() {
672
+ return state.servers.find(s => s.url === state.server && serverToken(s) === state.token)
673
+ || state.servers.find(s => s.url === state.server)
674
+ || null
675
+ }
676
+ function setCurrentAccessToken(value) {
677
+ const token = normalizedServerToken(value)
678
+ if (!token) return false
679
+ const current = currentServerEntry()
680
+ if (current) current.token = token
681
+ state.token = token
682
+ LS.set('token', token)
683
+ saveServers()
684
+ return true
685
+ }
664
686
 
665
687
  function migrateServersV1() {
666
688
  if (LS.get('servers-v2', null) !== null) return
@@ -671,7 +693,7 @@ function migrateServersV1() {
671
693
  arr = legacy ? [legacy] : []
672
694
  }
673
695
  const urls = arr.map(s => String(s || '').trim().replace(/\/+$/, '')).filter(s => /^https?:\/\//i.test(s))
674
- state.servers = urls.map((url, i) => ({ id: 's' + (i + 1), url, note: '', group: '默认' }))
696
+ state.servers = urls.map((url, i) => ({ id: 's' + (i + 1), url, note: '', group: '默认', token: normalizedServerToken(state.token) }))
675
697
  state.groups = ['默认']; state.activeGroup = '默认'
676
698
  state.autoSelect = { '默认': true }; state.groupActive = { '默认': '' }
677
699
  const active = LS.get('activeServer', '')
@@ -687,7 +709,8 @@ function loadServers() {
687
709
  let data = null
688
710
  try { data = JSON.parse(LS.get('servers-v2', '')) } catch {}
689
711
  if (!data || !Array.isArray(data.servers)) { migrateServersV1(); return }
690
- state.servers = data.servers.filter(s => s && typeof s.url === 'string').map(s => ({ id: s.id || newServerId(), url: s.url.replace(/\/+$/, ''), note: s.note || '', group: s.group || '默认' }))
712
+ const legacyToken = normalizedServerToken(state.token || LS.get('token', ''))
713
+ state.servers = data.servers.filter(s => s && typeof s.url === 'string').map(s => ({ id: s.id || newServerId(), url: s.url.replace(/\/+$/, ''), note: s.note || '', group: s.group || '默认', token: s.token == null ? legacyToken : normalizedServerToken(s.token) }))
691
714
  state.groups = Array.isArray(data.groups) && data.groups.length ? data.groups : ['默认']
692
715
  state.activeGroup = state.groups.includes(data.activeGroup) ? data.activeGroup : '默认'
693
716
  state.autoSelect = data.autoSelect || {}
@@ -696,7 +719,12 @@ function loadServers() {
696
719
  for (const s of state.servers) ensureGroup(s.group)
697
720
  const manual = state.groupActive[state.activeGroup]
698
721
  const manualSrv = manual ? state.servers.find(s => s.id === manual) : null
699
- state.server = manualSrv ? manualSrv.url : (activeServers()[0]?.url || '')
722
+ const selected = manualSrv || activeServers()[0]
723
+ state.server = selected?.url || ''
724
+ if (selected) {
725
+ state.token = serverToken(selected)
726
+ LS.set('token', state.token)
727
+ }
700
728
  }
701
729
  function saveServers() {
702
730
  LS.set('servers-v2', JSON.stringify({
@@ -705,8 +733,8 @@ function saveServers() {
705
733
  }))
706
734
  }
707
735
  function serverCandidates() {
708
- const list = activeServers().map(s => s.url)
709
- if (location.origin && !list.includes(location.origin)) list.push(location.origin)
736
+ const list = activeServers().map(s => ({ ...s, token: serverToken(s) }))
737
+ if (location.origin && !list.some(s => s.url === location.origin)) list.push({ id: 'origin', url: location.origin, token: state.token, origin: true })
710
738
  return list
711
739
  }
712
740
  async function pingServer(base) {
@@ -749,36 +777,47 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
749
777
  if (!silent) toast(t('ds.speedTesting'))
750
778
  const candidates = serverCandidates()
751
779
  let chosen = ''
780
+ let chosenToken = state.token
752
781
  let best = null
753
782
  let ms = Infinity
754
783
  if (state.autoSelect[state.activeGroup] !== false) {
755
- const measured = await Promise.all(candidates.map(async (u) => [u, await pingServer(u)]))
756
- for (const [u, latency] of measured) state.serverLatency[u] = latency
757
- best = candidates.filter(u => Number.isFinite(state.serverLatency[u])).sort((a, b) => state.serverLatency[a] - state.serverLatency[b])[0] || null
758
- chosen = best || (state.server || '')
759
- ms = best ? state.serverLatency[best] : Infinity
784
+ const measured = await Promise.all(candidates.map(async (server) => [server, await pingServer(server.url)]))
785
+ for (const [server, latency] of measured) state.serverLatency[serverKey(server)] = latency
786
+ best = candidates.filter(server => Number.isFinite(state.serverLatency[serverKey(server)])).sort((a, b) => state.serverLatency[serverKey(a)] - state.serverLatency[serverKey(b)])[0] || null
787
+ const sameOrigin = best?.url === location.origin && best.origin
788
+ chosen = best ? (sameOrigin ? '' : best.url) : (state.server || '')
789
+ chosenToken = best ? serverToken(best) : state.token
790
+ ms = best ? state.serverLatency[serverKey(best)] : Infinity
760
791
  } else {
761
792
  const manual = state.groupActive[state.activeGroup]
762
793
  const manualSrv = manual ? state.servers.find(s => s.id === manual) : null
763
794
  chosen = manualSrv ? manualSrv.url : (activeServers()[0]?.url || '')
764
- if (chosen) { state.serverLatency[chosen] = await pingServer(chosen); ms = state.serverLatency[chosen] }
795
+ chosenToken = manualSrv ? serverToken(manualSrv) : state.token
796
+ if (chosen) {
797
+ const key = serverKey(manualSrv || { url: chosen, token: state.token })
798
+ state.serverLatency[key] = await pingServer(chosen)
799
+ ms = state.serverLatency[key]
800
+ }
765
801
  }
766
802
  renderServers()
767
- if (chosen !== state.server) {
803
+ if (chosen !== state.server || chosenToken !== state.token) {
768
804
  state.hostInfo = null
769
805
  hostDescribeFailures = 0
770
806
  if (hostDescribeRetryTimer) clearTimeout(hostDescribeRetryTimer)
771
807
  hostDescribeRetryTimer = null
772
808
  resetFsForServerDesktop()
773
809
  state.server = chosen
774
- if (best) { const srv = state.servers.find(s => s.url === best); if (srv) state.groupActive[state.activeGroup] = srv.id }
810
+ state.token = chosenToken
811
+ LS.set('token', state.token)
812
+ if (best) { const srv = state.servers.find(s => s.id === best.id); if (srv) state.groupActive[state.activeGroup] = srv.id }
775
813
  saveServers()
814
+ renderServers()
776
815
  if (!silent) {
777
816
  if (chosen) toast(t('ds.speedSwitched', { url: chosen, ms: Number.isFinite(ms) ? ms : 0 }), 'ok')
778
817
  }
779
818
  if (reconnect && state.token) { openStreams(); refreshSessions() }
780
819
  } else if (!silent) {
781
- if (best) toast(t('ds.speedAlreadyBest', { url: chosen, ms: state.serverLatency[best] }), 'ok')
820
+ if (best) toast(t('ds.speedAlreadyBest', { url: chosen, ms: state.serverLatency[serverKey(best)] }), 'ok')
782
821
  else if (chosen) toast(t('ds.speedManualUsing', { url: chosen, ms: Number.isFinite(ms) ? ms : '—' }), 'ok')
783
822
  else toast(t('ds.speedAllDown'), 'err')
784
823
  }
@@ -818,13 +857,14 @@ function renderServers() {
818
857
  </div>
819
858
  <div class="ds-srv-body ${g === state.activeGroup ? '' : 'hidden'}">
820
859
  ${list.map(s => {
821
- const ms = state.serverLatency[s.url]
860
+ const ms = state.serverLatency[serverKey(s)]
822
861
  let badge = `<span class="ds-server-badge">${t('ds.serversUntested')}</span>`
823
- if (Number.isFinite(ms)) badge = `<span class="ds-server-badge ${s.url === state.server ? 'good' : ''}">${ms}ms${s.url === state.server ? t('ds.serversCurrent') : ''}</span>`
862
+ const active = s.url === state.server && serverToken(s) === state.token
863
+ if (Number.isFinite(ms)) badge = `<span class="ds-server-badge ${active ? 'good' : ''}">${ms}ms${active ? t('ds.serversCurrent') : ''}</span>`
824
864
  else if (ms !== undefined) badge = `<span class="ds-server-badge bad">${t('ds.serversUnreachable')}</span>`
825
- const activeInGroup = auto ? s.url === state.server : s.id === activeManual
865
+ const activeInGroup = auto ? active : s.id === activeManual
826
866
  return `<div class="ds-server-row ${activeInGroup ? 'active' : ''}" data-use-server="${esc(s.id)}">
827
- <span class="ds-server-main"><span class="ds-server-note">${esc(serverTitle(s))}</span>${s.note ? `<span class="ds-server-url">${esc(s.url)}</span>` : ''}</span>${badge}
867
+ <span class="ds-server-main"><span class="ds-server-note">${esc(serverTitle(s))}</span>${s.note ? `<span class="ds-server-url">${esc(s.url)}</span>` : ''}<span class="ds-server-token-badge">${s.token ? t('ds.serversTokenSet') : t('ds.serversTokenMissing')}</span></span>${badge}
828
868
  <button class="ds-mini" data-edit-server="${esc(s.id)}" title="${t('ds.serversEdit')}">✎</button>
829
869
  <button class="ds-mini" data-del-server="${esc(s.id)}" title="${t('ds.serversDelete')}">✕</button>
830
870
  </div>`
@@ -857,15 +897,17 @@ function renderServers() {
857
897
  state.activeGroup = s.group
858
898
  if (state.server !== s.url) resetFsForServerDesktop()
859
899
  state.server = s.url
900
+ state.token = serverToken(s)
901
+ LS.set('token', state.token)
860
902
  saveServers()
861
903
  renderServers()
862
904
  toast(t('ds.serversManualSelected', { url: serverTitle(s) }), 'ok')
863
905
  if (state.token) { openStreams(); refreshSessions() }
864
906
  }))
865
- const cur = state.servers.find(s => s.url === state.server)
907
+ const cur = currentServerEntry()
866
908
  const curGroup = cur ? cur.group : state.activeGroup
867
909
  const curLabel = cur ? (cur.note || cur.url) : (state.server || t('ds.origin'))
868
- const curMs = state.serverLatency[state.server]
910
+ const curMs = cur ? state.serverLatency[serverKey(cur)] : undefined
869
911
  $('server-desc').textContent = t('ds.serversCurrentDesc', { group: curGroup, url: curLabel, ms: Number.isFinite(curMs) ? curMs + 'ms' : '—' })
870
912
  updateConn()
871
913
  }
@@ -876,9 +918,10 @@ async function addServer() {
876
918
  if (!raw) return toast(t('ds.serversNeedAddress'), 'err')
877
919
  try { const u = new URL(raw); if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad') }
878
920
  catch { return toast(t('ds.serversBadProtocol'), 'err') }
879
- if (state.servers.some(s => s.url === raw)) return toast(t('ds.serversDuplicate'), 'err')
921
+ const token = normalizedServerToken(state.token)
922
+ if (state.servers.some(s => serverIdentity(s.url, serverToken(s)) === serverIdentity(raw, token))) return toast(t('ds.serversDuplicate'), 'err')
880
923
  const note = (prompt(t('ds.serversPromptNote')) || '').trim()
881
- state.servers.push({ id: newServerId(), url: raw, note, group: state.activeGroup })
924
+ state.servers.push({ id: newServerId(), url: raw, note, group: state.activeGroup, token })
882
925
  saveServers()
883
926
  if (input) input.value = ''
884
927
  renderServers()
@@ -892,17 +935,22 @@ function editServer(id) {
892
935
  if (!raw) return
893
936
  try { const u = new URL(raw); if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad') }
894
937
  catch { return toast(t('ds.serversBadProtocol'), 'err') }
895
- if (state.servers.some(x => x.id !== id && x.url === raw)) return toast(t('ds.serversDuplicate'), 'err')
938
+ const tokenInput = prompt(t('ds.serversPromptEditToken'), '')
939
+ if (tokenInput === null) return
940
+ const token = normalizedServerToken(tokenInput) || serverToken(s)
941
+ if (state.servers.some(x => x.id !== id && serverIdentity(x.url, serverToken(x)) === serverIdentity(raw, token))) return toast(t('ds.serversDuplicate'), 'err')
896
942
  const note = prompt(t('ds.serversPromptEditNote', { url: raw }), s.note || '')
897
943
  if (note === null) return
898
944
  const group = prompt(t('ds.serversPromptEditGroup'), s.group || '默认')
899
945
  if (group === null) return
900
- const wasActive = state.server === s.url
946
+ const wasActive = state.server === s.url && state.token === serverToken(s)
901
947
  const changedActiveUrl = wasActive && state.server !== raw
902
- s.url = raw; s.note = note.trim(); s.group = ensureGroup(group.trim() || '默认')
948
+ s.url = raw; s.note = note.trim(); s.group = ensureGroup(group.trim() || '默认'); s.token = token
903
949
  if (wasActive) {
904
950
  if (changedActiveUrl) resetFsForServerDesktop()
905
951
  state.server = raw
952
+ state.token = token
953
+ LS.set('token', token)
906
954
  }
907
955
  saveServers(); renderServers(); toast(t('ds.serversEdited'), 'ok')
908
956
  if (wasActive && state.token) selectFastestServer({ silent: true })
@@ -911,7 +959,7 @@ function removeServer(id) {
911
959
  const s = state.servers.find(x => x.id === id)
912
960
  if (!s) return
913
961
  state.servers = state.servers.filter(x => x.id !== id)
914
- const wasActive = state.server === s.url
962
+ const wasActive = state.server === s.url && state.token === serverToken(s)
915
963
  for (const g of state.groups) if (state.groupActive[g] === id) state.groupActive[g] = ''
916
964
  saveServers(); renderServers()
917
965
  if (wasActive) { toast(t('ds.serversRemovedActive')); selectFastestServer({ silent: true }) }
@@ -1334,6 +1382,24 @@ function goalOf(s) {
1334
1382
  if (!p) return null
1335
1383
  return p.goal && typeof p.goal === 'object' ? p.goal : p
1336
1384
  }
1385
+ const COLLAPSED_GOALS_KEY = 'dshCollapsedGoalsV1'
1386
+ function collapsedGoals() {
1387
+ try {
1388
+ const value = JSON.parse(LS.get(COLLAPSED_GOALS_KEY, '[]'))
1389
+ return Array.isArray(value) ? value.filter(item => item && typeof item.sessionId === 'string' && typeof item.goalId === 'string') : []
1390
+ } catch { return [] }
1391
+ }
1392
+ function goalDisplayId(goal) { return String(goal?.id || '__current__') }
1393
+ function isGoalCollapsed(sessionId, goal) {
1394
+ const goalId = goalDisplayId(goal)
1395
+ return collapsedGoals().some(item => item.sessionId === sessionId && item.goalId === goalId)
1396
+ }
1397
+ function setGoalCollapsed(sessionId, goal, collapsed) {
1398
+ const goalId = goalDisplayId(goal)
1399
+ const next = collapsedGoals().filter(item => item.sessionId !== sessionId)
1400
+ if (collapsed) next.push({ sessionId, goalId })
1401
+ LS.set(COLLAPSED_GOALS_KEY, JSON.stringify(next.slice(-100)))
1402
+ }
1337
1403
  function onSessionEvent(sessionId, event) {
1338
1404
  if (event?.type === 'turn/start' || event?.type === 'turn/end') {
1339
1405
  noteSessionTurnTime(sessionId, event)
@@ -1691,15 +1757,23 @@ async function renderSessionCards() {
1691
1757
  const todos = proj(s, 'todos')
1692
1758
  let html = ''
1693
1759
  if (goal && !isGoalTerminal(goal)) {
1694
- html += `<div class="ds-card ds-goal-card"><div class="ds-card-title">${t('goal.title')}</div>
1695
- <div class="ds-goal-obj">${esc(goal.objective || '')}</div>
1696
- <div class="ds-goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
1697
- <div class="ds-goal-actions">
1698
- ${goal.phase === 'active' ? `<button class="ds-mini-btn" data-goal="pause">${t('goal.pause')}</button>` : `<button class="ds-mini-btn" data-goal="resume">${t('goal.resume')}</button>`}
1699
- <button class="ds-mini-btn" data-goal="complete">${t('goal.complete')}</button>
1700
- <button class="ds-mini-btn" data-goal="edit">${t('goal.edit')}</button>
1701
- <button class="ds-mini-btn" data-goal="clear">${t('goal.clear')}</button>
1702
- </div></div>`
1760
+ const collapsed = isGoalCollapsed(sessionId, goal)
1761
+ html += `<div class="ds-goal-disclosure${collapsed ? ' is-collapsed' : ''}">
1762
+ <div id="goal-panel-desktop" class="ds-card ds-goal-card${collapsed ? ' hidden' : ''}">
1763
+ <div class="ds-goal-card-head"><div class="ds-card-title">${t('goal.title')}</div>
1764
+ <button type="button" class="ds-goal-collapse-btn" data-goal-collapse="1" aria-expanded="true" aria-controls="goal-panel-desktop" title="${esc(t('goal.collapse'))}"><span aria-hidden="true">›</span>${t('goal.collapseShort')}</button>
1765
+ </div>
1766
+ <div class="ds-goal-obj">${esc(goal.objective || '')}</div>
1767
+ <div class="ds-goal-phase">phase: ${esc(goal.phase || '?')} · revision ${goal.revision ?? '?'}</div>
1768
+ <div class="ds-goal-actions">
1769
+ ${goal.phase === 'active' ? `<button class="ds-mini-btn" data-goal="pause">${t('goal.pause')}</button>` : `<button class="ds-mini-btn" data-goal="resume">${t('goal.resume')}</button>`}
1770
+ <button class="ds-mini-btn" data-goal="complete">${t('goal.complete')}</button>
1771
+ <button class="ds-mini-btn" data-goal="edit">${t('goal.edit')}</button>
1772
+ <button class="ds-mini-btn" data-goal="clear">${t('goal.clear')}</button>
1773
+ </div>
1774
+ </div>
1775
+ <button type="button" class="ds-goal-side-tab${collapsed ? '' : ' hidden'}" data-goal-collapse="0" aria-expanded="false" aria-controls="goal-panel-desktop" aria-label="${esc(t('goal.expand'))}" title="${esc(t('goal.expand'))}"><span aria-hidden="true">‹</span><span>${t('goal.title')}</span><small>${t('goal.collapsedHint')}</small></button>
1776
+ </div>`
1703
1777
  }
1704
1778
  if (todos?.items?.length) {
1705
1779
  html += `<div class="ds-card"><div class="ds-card-title">${t('todos.title')}</div>${todos.items.map(item =>
@@ -1709,6 +1783,15 @@ async function renderSessionCards() {
1709
1783
  box.innerHTML = html
1710
1784
  box.querySelectorAll('[data-goal]').forEach(btn =>
1711
1785
  btn.addEventListener('click', () => goalAction(btn.dataset.goal)))
1786
+ box.querySelectorAll('[data-goal-collapse]').forEach(btn =>
1787
+ btn.addEventListener('click', () => {
1788
+ const collapse = btn.dataset.goalCollapse === '1'
1789
+ const currentGoal = goalOf(state.byId.get(state.current))
1790
+ if (!currentGoal) return
1791
+ setGoalCollapsed(state.current, currentGoal, collapse)
1792
+ void renderSessionCards()
1793
+ requestAnimationFrame(() => box.querySelector(`[data-goal-collapse="${collapse ? '0' : '1'}"]`)?.focus())
1794
+ }))
1712
1795
 
1713
1796
  const sub = await safeRpc('subagent.list', { parentSessionId: sessionId }, '')
1714
1797
  if (renderGeneration !== sessionCardsRenderGeneration || state.current !== sessionId) return
@@ -1924,7 +2007,7 @@ function renderQueue() {
1924
2007
 
1925
2008
  /* ---------------- 审批/提问通知卡片栈 ---------------- */
1926
2009
  function serverLabel() {
1927
- const cur = state.servers.find(s => s.url === state.server)
2010
+ const cur = currentServerEntry()
1928
2011
  return cur ? (cur.note || cur.url) : (state.server || location.host)
1929
2012
  }
1930
2013
  function renderNotifStack() {
@@ -2518,6 +2601,8 @@ function toggleStatsDrawer() {
2518
2601
  async function loadStats() {
2519
2602
  if (!state.token) {
2520
2603
  $('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsGatewayDown')}</div>`
2604
+ $('stats-token-chart').innerHTML = ''
2605
+ $('stats-token-legend').innerHTML = ''
2521
2606
  return
2522
2607
  }
2523
2608
  try {
@@ -2527,11 +2612,15 @@ async function loadStats() {
2527
2612
  renderStats(json.days || [])
2528
2613
  } catch {
2529
2614
  $('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsGatewayDown')}</div>`
2615
+ $('stats-token-chart').innerHTML = ''
2616
+ $('stats-token-legend').innerHTML = ''
2530
2617
  }
2531
2618
  }
2532
2619
  function renderStats(days) {
2533
2620
  if (!days.length) {
2534
2621
  $('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsEmpty')}</div>`
2622
+ $('stats-token-chart').innerHTML = ''
2623
+ $('stats-token-legend').innerHTML = ''
2535
2624
  return
2536
2625
  }
2537
2626
  const today = days[days.length - 1]
@@ -2551,6 +2640,7 @@ function renderStats(days) {
2551
2640
  <div class="ds-stat-card"><div class="v">${fmtCost(totalCost)}</div><div class="k">${t('ds.statsTodayCost')}<br>${t('ds.statsPeak')} ${fmtCost(peakCost)} / ${t('ds.statsOff')} ${fmtCost(offCost)}</div></div>
2552
2641
  <div class="ds-stat-card"><div class="v">${peakShare}%</div><div class="k">${t('ds.statsPeakShare')}<br>${t('ds.statsDays', { n: days.length })}</div></div>`
2553
2642
  $('stats-legend').innerHTML = `<span class="sw peak"></span>${t('ds.statsPeak')} <span class="sw off"></span>${t('ds.statsOff')}`
2643
+ $('stats-token-legend').innerHTML = `<span class="sw token"></span>${t('ds.statsTokenTotal')}`
2554
2644
  $('stats-note').textContent = t('ds.statsNote')
2555
2645
  const maxCost = Math.max(...days.map(d => (d.total.cost || 0)), 0.0001)
2556
2646
  $('stats-chart').innerHTML = days.map(d => {
@@ -2565,6 +2655,17 @@ function renderStats(days) {
2565
2655
  <div class="lbl">${d.date.slice(5)}</div>
2566
2656
  </div>`
2567
2657
  }).join('')
2658
+ const maxTokens = Math.max(...days.map(d => bucketTokens(d.total)), 1)
2659
+ $('stats-token-chart').innerHTML = days.map(d => {
2660
+ const tokens = bucketTokens(d.total)
2661
+ const totalH = tokens > 0 ? Math.max(3, Math.round(tokens / maxTokens * 100)) : 0
2662
+ const tip = `${d.date}\n${t('ds.statsTokenTotal')} ${fmtTokens(tokens)}\n${t('ds.statsInput')} ${fmtTokens(d.total.input)} · ${t('ds.statsOutput')} ${fmtTokens(d.total.output)}`
2663
+ return `<div class="ds-stats-bar" data-tip="${esc(tip)}">
2664
+ <div class="bars token" style="height:${totalH}%"></div>
2665
+ <div class="val">${tokens > 0 ? fmtTokens(tokens) : ''}</div>
2666
+ <div class="lbl">${d.date.slice(5)}</div>
2667
+ </div>`
2668
+ }).join('')
2568
2669
  }
2569
2670
 
2570
2671
  /* ---------------- 视图与连接状态 ---------------- */
@@ -2654,10 +2755,25 @@ function renderOverviewDesktop() {
2654
2755
  $('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
2655
2756
  }
2656
2757
 
2758
+ const narrowSidebarQuery = window.matchMedia('(max-width: 1023px)')
2759
+ function setMobileSidebar(open) {
2760
+ const sidebar = $('ds-sidebar')
2761
+ const backdrop = $('ds-sidebar-backdrop')
2762
+ const trigger = $('btn-mobile-nav')
2763
+ if (!sidebar || !backdrop || !trigger) return
2764
+ const visible = !!open && narrowSidebarQuery.matches
2765
+ sidebar.classList.toggle('mobile-open', visible)
2766
+ backdrop.classList.toggle('hidden', !visible)
2767
+ trigger.setAttribute('aria-expanded', String(visible))
2768
+ sidebar.inert = narrowSidebarQuery.matches && !visible
2769
+ $('ds-app')?.classList.toggle('mobile-nav-open', visible)
2770
+ }
2771
+
2657
2772
  function showView(id) {
2658
2773
  state.view = id
2659
2774
  for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
2660
2775
  document.querySelectorAll('.ds-nav-item').forEach(b => b.classList.toggle('active', b.dataset.view === id))
2776
+ if (narrowSidebarQuery.matches) setMobileSidebar(false)
2661
2777
  window.DshMotion?.view($(id))
2662
2778
  const titles = { 'view-overview': 'ds.overview', 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
2663
2779
  if (id === 'view-chat') { const s = state.byId.get(state.current); $('ds-title').textContent = s ? titleOf(s) : t('ds.sessions') }
@@ -2700,7 +2816,7 @@ function updateConn() {
2700
2816
  const el = $('conn-badge')
2701
2817
  // 双实时通道可能按任意顺序打开,连接刷新必须同时刷新系统总览。
2702
2818
  renderOverviewDesktop()
2703
- const cur = state.servers.find(s => s.url === state.server)
2819
+ const cur = currentServerEntry()
2704
2820
  const group = cur ? cur.group : state.activeGroup
2705
2821
  const label = cur ? (cur.note || cur.url) : (state.server || t('ds.origin'))
2706
2822
  const serverText = t('ds.currentServer', { group, url: label })
@@ -2760,9 +2876,14 @@ function bindUi() {
2760
2876
  LS.set('sessionSort', state.sessionSort)
2761
2877
  renderSessions()
2762
2878
  })
2763
- $('btn-mobile-nav').addEventListener('click', () => {
2764
- const list = $('mobile-session-list')
2765
- list.style.display = list.style.display === 'none' ? 'flex' : 'none'
2879
+ $('btn-mobile-nav').addEventListener('click', () => setMobileSidebar(!$('ds-sidebar').classList.contains('mobile-open')))
2880
+ $('ds-sidebar-backdrop').addEventListener('click', () => setMobileSidebar(false))
2881
+ narrowSidebarQuery.addEventListener?.('change', () => setMobileSidebar(false))
2882
+ document.addEventListener('keydown', (event) => {
2883
+ if (event.key === 'Escape' && $('ds-sidebar').classList.contains('mobile-open')) {
2884
+ setMobileSidebar(false)
2885
+ $('btn-mobile-nav').focus()
2886
+ }
2766
2887
  })
2767
2888
  document.querySelectorAll('.ds-nav-item').forEach(b => b.addEventListener('click', () => showView(b.dataset.view)))
2768
2889
  $('ds-overview-refresh').addEventListener('click', async () => {
@@ -2901,6 +3022,11 @@ function bindUi() {
2901
3022
  if (bar && bar.dataset.tip) showTip(bar.dataset.tip, bar.getBoundingClientRect())
2902
3023
  })
2903
3024
  $('stats-chart').addEventListener('mouseleave', hideTip)
3025
+ $('stats-token-chart').addEventListener('mouseover', (e) => {
3026
+ const bar = e.target.closest('.ds-stats-bar')
3027
+ if (bar && bar.dataset.tip) showTip(bar.dataset.tip, bar.getBoundingClientRect())
3028
+ })
3029
+ $('stats-token-chart').addEventListener('mouseleave', hideTip)
2904
3030
 
2905
3031
  $('view-settings').addEventListener('click', (e) => {
2906
3032
  const group = e.target.closest('[data-settings-group]')
@@ -2946,11 +3072,23 @@ function bindUi() {
2946
3072
  }
2947
3073
 
2948
3074
  async function start() {
3075
+ const urlToken = new URLSearchParams(location.search).get('token')?.trim() || ''
3076
+ if (urlToken) {
3077
+ state.token = urlToken
3078
+ LS.set('token', urlToken)
3079
+ history.replaceState(null, '', location.pathname)
3080
+ }
2949
3081
  loadServers()
2950
3082
  renderServers()
2951
3083
  showView('view-overview')
2952
- const urlToken = new URLSearchParams(location.search).get('token')
2953
- if (urlToken) { state.token = urlToken; LS.set('token', urlToken); history.replaceState(null, '', location.pathname) }
3084
+ if (urlToken) {
3085
+ state.token = urlToken
3086
+ const currentId = state.groupActive[state.activeGroup]
3087
+ const current = state.servers.find(s => s.id === currentId) || state.servers.find(s => s.url === state.server)
3088
+ if (current) current.token = urlToken
3089
+ LS.set('token', urlToken)
3090
+ saveServers()
3091
+ }
2954
3092
  if (!state.token) {
2955
3093
  const input = prompt(t('ds.tokenTitle'))
2956
3094
  if (input && input.trim()) { state.token = input.trim(); LS.set('token', state.token) }