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.
- package/apk/dsh-remote.apk +0 -0
- package/gateway-stats.cjs +3 -2
- package/package.json +1 -1
- package/public/admin.html +16 -17
- package/public/admin.js +6 -5
- package/public/app.js +293 -75
- package/public/desktop/desktop.css +52 -7
- package/public/desktop/desktop.html +39 -32
- package/public/desktop/desktop.js +186 -48
- package/public/index.html +89 -48
- package/public/motion.js +105 -17
- package/public/plugin.html +6 -5
- package/public/styles.css +143 -43
- package/public/theme-vars.css +244 -167
- package/public/theme.js +2 -2
- package/public/update.json +12 -12
- package/public/version.json +1 -1
|
@@ -27,10 +27,11 @@ const CAP = window.Capacitor || null
|
|
|
27
27
|
|
|
28
28
|
/* ---------------- 皮肤 ---------------- */
|
|
29
29
|
const THEME_META = [
|
|
30
|
-
{ id: 'default', sw: ['#
|
|
31
|
-
{ id: 'dark', sw: ['#
|
|
32
|
-
{ id: 'light', sw: ['#
|
|
33
|
-
{ id: 'neutral', sw: ['#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
709
|
-
if (location.origin && !list.
|
|
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 (
|
|
756
|
-
for (const [
|
|
757
|
-
best = candidates.filter(
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
860
|
+
const ms = state.serverLatency[serverKey(s)]
|
|
822
861
|
let badge = `<span class="ds-server-badge">${t('ds.serversUntested')}</span>`
|
|
823
|
-
|
|
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 ?
|
|
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 =
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1695
|
-
|
|
1696
|
-
<div class="ds-goal-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
<
|
|
1701
|
-
<
|
|
1702
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
2765
|
-
|
|
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
|
-
|
|
2953
|
-
|
|
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) }
|