dsh-remote-plugin 0.6.16 → 0.6.18
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/gateway.cjs +87 -8
- package/index.mjs +21 -3
- package/package.json +1 -1
- package/public/admin.html +11 -6
- package/public/admin.js +88 -14
- package/public/app.js +430 -86
- package/public/desktop/desktop.css +5 -0
- package/public/desktop/desktop.html +20 -15
- package/public/desktop/desktop.js +281 -52
- package/public/index.html +58 -19
- package/public/styles.css +87 -19
- package/public/update.json +12 -12
- package/public/version.json +1 -1
|
@@ -57,12 +57,12 @@ const state = {
|
|
|
57
57
|
token: LS.get('token', ''),
|
|
58
58
|
wsTicket: { token: '', server: '', value: '', expiresAt: 0 },
|
|
59
59
|
server: '',
|
|
60
|
-
servers: [],
|
|
60
|
+
servers: [], // { id, url, note, group, token }; connection identity = URL + token
|
|
61
61
|
groups: ['默认'],
|
|
62
62
|
activeGroup: '默认',
|
|
63
63
|
autoSelect: { '默认': true },
|
|
64
64
|
groupActive: { '默认': '' },
|
|
65
|
-
serverLatency: {},
|
|
65
|
+
serverLatency: {}, // URL + token -> latest /health latency
|
|
66
66
|
gatewayHealth: {},
|
|
67
67
|
selectingServer: false,
|
|
68
68
|
sessions: [],
|
|
@@ -88,7 +88,7 @@ const state = {
|
|
|
88
88
|
},
|
|
89
89
|
streamMode: 'ws', // 'ws' | 'poll'
|
|
90
90
|
pollSeq: { mux: 0, host: 0 },
|
|
91
|
-
fs: { path: null, initial: null, loaded: false },
|
|
91
|
+
fs: { path: null, initial: null, loaded: false, roots: [], rootIndex: 0 },
|
|
92
92
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
93
93
|
wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
|
|
94
94
|
archivedIds: [],
|
|
@@ -603,6 +603,49 @@ async function safeRpc(method, payload, errText) {
|
|
|
603
603
|
return null
|
|
604
604
|
}
|
|
605
605
|
}
|
|
606
|
+
let hostDescribePromise = null
|
|
607
|
+
let hostDescribeRetryTimer = null
|
|
608
|
+
let hostDescribeFailures = 0
|
|
609
|
+
|
|
610
|
+
function scheduleHostDescribeRetryDesktop() {
|
|
611
|
+
if (!state.token || hostDescribeRetryTimer) return
|
|
612
|
+
const delays = [2000, 5000, 15000, 30000, 60000]
|
|
613
|
+
const delay = delays[Math.min(Math.max(0, hostDescribeFailures - 1), delays.length - 1)]
|
|
614
|
+
hostDescribeRetryTimer = setTimeout(() => {
|
|
615
|
+
hostDescribeRetryTimer = null
|
|
616
|
+
void refreshHostDescriptionDesktop()
|
|
617
|
+
}, delay)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function refreshHostDescriptionDesktop({ notify = false } = {}) {
|
|
621
|
+
if (!state.token) return null
|
|
622
|
+
if (hostDescribePromise) return hostDescribePromise
|
|
623
|
+
const server = state.server
|
|
624
|
+
hostDescribePromise = (async () => {
|
|
625
|
+
try {
|
|
626
|
+
const host = await rpc('host.describe', {}, 5000)
|
|
627
|
+
if (server !== state.server) return null
|
|
628
|
+
state.hostInfo = host
|
|
629
|
+
const health = activeGatewayHealth()
|
|
630
|
+
if (health) health.upstreamReachable = true
|
|
631
|
+
hostDescribeFailures = 0
|
|
632
|
+
if (hostDescribeRetryTimer) clearTimeout(hostDescribeRetryTimer)
|
|
633
|
+
hostDescribeRetryTimer = null
|
|
634
|
+
renderOverviewDesktop()
|
|
635
|
+
return host
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (server !== state.server) return null
|
|
638
|
+
hostDescribeFailures++
|
|
639
|
+
scheduleHostDescribeRetryDesktop()
|
|
640
|
+
if (notify) toast(error.message === 'AUTH' ? t('ds.toastAuth') : error.message, 'err')
|
|
641
|
+
return null
|
|
642
|
+
} finally {
|
|
643
|
+
hostDescribePromise = null
|
|
644
|
+
if (server !== state.server) scheduleHostDescribeRetryDesktop()
|
|
645
|
+
}
|
|
646
|
+
})()
|
|
647
|
+
return hostDescribePromise
|
|
648
|
+
}
|
|
606
649
|
function uuid() {
|
|
607
650
|
try { return crypto.randomUUID() } catch { return 'id-' + Date.now() + '-' + Math.random().toString(36).slice(2) }
|
|
608
651
|
}
|
|
@@ -618,6 +661,27 @@ function ensureGroup(name) {
|
|
|
618
661
|
}
|
|
619
662
|
function groupServers(g) { return state.servers.filter(s => s.group === g) }
|
|
620
663
|
function activeServers() { return groupServers(state.activeGroup) }
|
|
664
|
+
function normalizedServerToken(token) { return typeof token === 'string' ? token.trim() : '' }
|
|
665
|
+
function serverIdentity(url, token) {
|
|
666
|
+
return `${String(url || '').replace(/\/+$/, '')}\u0000${normalizedServerToken(token)}`
|
|
667
|
+
}
|
|
668
|
+
function serverKey(server) { return serverIdentity(server?.url ?? server, server?.token) }
|
|
669
|
+
function serverToken(server) { return normalizedServerToken(server?.token) }
|
|
670
|
+
function currentServerEntry() {
|
|
671
|
+
return state.servers.find(s => s.url === state.server && serverToken(s) === state.token)
|
|
672
|
+
|| state.servers.find(s => s.url === state.server)
|
|
673
|
+
|| null
|
|
674
|
+
}
|
|
675
|
+
function setCurrentAccessToken(value) {
|
|
676
|
+
const token = normalizedServerToken(value)
|
|
677
|
+
if (!token) return false
|
|
678
|
+
const current = currentServerEntry()
|
|
679
|
+
if (current) current.token = token
|
|
680
|
+
state.token = token
|
|
681
|
+
LS.set('token', token)
|
|
682
|
+
saveServers()
|
|
683
|
+
return true
|
|
684
|
+
}
|
|
621
685
|
|
|
622
686
|
function migrateServersV1() {
|
|
623
687
|
if (LS.get('servers-v2', null) !== null) return
|
|
@@ -628,7 +692,7 @@ function migrateServersV1() {
|
|
|
628
692
|
arr = legacy ? [legacy] : []
|
|
629
693
|
}
|
|
630
694
|
const urls = arr.map(s => String(s || '').trim().replace(/\/+$/, '')).filter(s => /^https?:\/\//i.test(s))
|
|
631
|
-
state.servers = urls.map((url, i) => ({ id: 's' + (i + 1), url, note: '', group: '默认' }))
|
|
695
|
+
state.servers = urls.map((url, i) => ({ id: 's' + (i + 1), url, note: '', group: '默认', token: normalizedServerToken(state.token) }))
|
|
632
696
|
state.groups = ['默认']; state.activeGroup = '默认'
|
|
633
697
|
state.autoSelect = { '默认': true }; state.groupActive = { '默认': '' }
|
|
634
698
|
const active = LS.get('activeServer', '')
|
|
@@ -644,7 +708,8 @@ function loadServers() {
|
|
|
644
708
|
let data = null
|
|
645
709
|
try { data = JSON.parse(LS.get('servers-v2', '')) } catch {}
|
|
646
710
|
if (!data || !Array.isArray(data.servers)) { migrateServersV1(); return }
|
|
647
|
-
|
|
711
|
+
const legacyToken = normalizedServerToken(state.token || LS.get('token', ''))
|
|
712
|
+
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) }))
|
|
648
713
|
state.groups = Array.isArray(data.groups) && data.groups.length ? data.groups : ['默认']
|
|
649
714
|
state.activeGroup = state.groups.includes(data.activeGroup) ? data.activeGroup : '默认'
|
|
650
715
|
state.autoSelect = data.autoSelect || {}
|
|
@@ -653,7 +718,12 @@ function loadServers() {
|
|
|
653
718
|
for (const s of state.servers) ensureGroup(s.group)
|
|
654
719
|
const manual = state.groupActive[state.activeGroup]
|
|
655
720
|
const manualSrv = manual ? state.servers.find(s => s.id === manual) : null
|
|
656
|
-
|
|
721
|
+
const selected = manualSrv || activeServers()[0]
|
|
722
|
+
state.server = selected?.url || ''
|
|
723
|
+
if (selected) {
|
|
724
|
+
state.token = serverToken(selected)
|
|
725
|
+
LS.set('token', state.token)
|
|
726
|
+
}
|
|
657
727
|
}
|
|
658
728
|
function saveServers() {
|
|
659
729
|
LS.set('servers-v2', JSON.stringify({
|
|
@@ -662,8 +732,8 @@ function saveServers() {
|
|
|
662
732
|
}))
|
|
663
733
|
}
|
|
664
734
|
function serverCandidates() {
|
|
665
|
-
const list = activeServers().map(s => s
|
|
666
|
-
if (location.origin && !list.
|
|
735
|
+
const list = activeServers().map(s => ({ ...s, token: serverToken(s) }))
|
|
736
|
+
if (location.origin && !list.some(s => s.url === location.origin)) list.push({ id: 'origin', url: location.origin, token: state.token, origin: true })
|
|
667
737
|
return list
|
|
668
738
|
}
|
|
669
739
|
async function pingServer(base) {
|
|
@@ -676,16 +746,29 @@ async function pingServer(base) {
|
|
|
676
746
|
const res = await fetch(u + '/health?t=' + Date.now(), { signal: ctrl.signal, cache: 'no-store' })
|
|
677
747
|
if (!res.ok) return Infinity
|
|
678
748
|
const health = await res.json().catch(() => null)
|
|
679
|
-
if (health && typeof health === 'object')
|
|
749
|
+
if (health && typeof health === 'object') {
|
|
750
|
+
state.gatewayHealth[u] = health
|
|
751
|
+
renderOverviewDesktop()
|
|
752
|
+
}
|
|
680
753
|
return Math.round(performance.now() - t0)
|
|
681
754
|
} catch { return Infinity } finally { clearTimeout(timer) }
|
|
682
755
|
}
|
|
683
756
|
function activeGatewayCapability(name) {
|
|
684
|
-
const
|
|
685
|
-
const capabilities = state.gatewayHealth[key]?.capabilities
|
|
757
|
+
const capabilities = activeGatewayHealth()?.capabilities
|
|
686
758
|
if (!capabilities || !Object.prototype.hasOwnProperty.call(capabilities, name)) return null
|
|
687
759
|
return Number(capabilities[name]) > 0
|
|
688
760
|
}
|
|
761
|
+
|
|
762
|
+
function activeGatewayHealth() {
|
|
763
|
+
const key = String(state.server || location.origin || '').replace(/\/+$/, '')
|
|
764
|
+
return state.gatewayHealth[key] || null
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function dshReachable() {
|
|
768
|
+
const health = activeGatewayHealth()
|
|
769
|
+
if (health && typeof health.upstreamReachable === 'boolean') return health.upstreamReachable
|
|
770
|
+
return !!state.hostInfo
|
|
771
|
+
}
|
|
689
772
|
async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
690
773
|
if (state.selectingServer) return null
|
|
691
774
|
state.selectingServer = true
|
|
@@ -693,31 +776,47 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
|
693
776
|
if (!silent) toast(t('ds.speedTesting'))
|
|
694
777
|
const candidates = serverCandidates()
|
|
695
778
|
let chosen = ''
|
|
779
|
+
let chosenToken = state.token
|
|
696
780
|
let best = null
|
|
697
781
|
let ms = Infinity
|
|
698
782
|
if (state.autoSelect[state.activeGroup] !== false) {
|
|
699
|
-
const measured = await Promise.all(candidates.map(async (
|
|
700
|
-
for (const [
|
|
701
|
-
best = candidates.filter(
|
|
702
|
-
|
|
703
|
-
|
|
783
|
+
const measured = await Promise.all(candidates.map(async (server) => [server, await pingServer(server.url)]))
|
|
784
|
+
for (const [server, latency] of measured) state.serverLatency[serverKey(server)] = latency
|
|
785
|
+
best = candidates.filter(server => Number.isFinite(state.serverLatency[serverKey(server)])).sort((a, b) => state.serverLatency[serverKey(a)] - state.serverLatency[serverKey(b)])[0] || null
|
|
786
|
+
const sameOrigin = best?.url === location.origin && best.origin
|
|
787
|
+
chosen = best ? (sameOrigin ? '' : best.url) : (state.server || '')
|
|
788
|
+
chosenToken = best ? serverToken(best) : state.token
|
|
789
|
+
ms = best ? state.serverLatency[serverKey(best)] : Infinity
|
|
704
790
|
} else {
|
|
705
791
|
const manual = state.groupActive[state.activeGroup]
|
|
706
792
|
const manualSrv = manual ? state.servers.find(s => s.id === manual) : null
|
|
707
793
|
chosen = manualSrv ? manualSrv.url : (activeServers()[0]?.url || '')
|
|
708
|
-
|
|
794
|
+
chosenToken = manualSrv ? serverToken(manualSrv) : state.token
|
|
795
|
+
if (chosen) {
|
|
796
|
+
const key = serverKey(manualSrv || { url: chosen, token: state.token })
|
|
797
|
+
state.serverLatency[key] = await pingServer(chosen)
|
|
798
|
+
ms = state.serverLatency[key]
|
|
799
|
+
}
|
|
709
800
|
}
|
|
710
801
|
renderServers()
|
|
711
|
-
if (chosen !== state.server) {
|
|
802
|
+
if (chosen !== state.server || chosenToken !== state.token) {
|
|
803
|
+
state.hostInfo = null
|
|
804
|
+
hostDescribeFailures = 0
|
|
805
|
+
if (hostDescribeRetryTimer) clearTimeout(hostDescribeRetryTimer)
|
|
806
|
+
hostDescribeRetryTimer = null
|
|
807
|
+
resetFsForServerDesktop()
|
|
712
808
|
state.server = chosen
|
|
713
|
-
|
|
809
|
+
state.token = chosenToken
|
|
810
|
+
LS.set('token', state.token)
|
|
811
|
+
if (best) { const srv = state.servers.find(s => s.id === best.id); if (srv) state.groupActive[state.activeGroup] = srv.id }
|
|
714
812
|
saveServers()
|
|
813
|
+
renderServers()
|
|
715
814
|
if (!silent) {
|
|
716
815
|
if (chosen) toast(t('ds.speedSwitched', { url: chosen, ms: Number.isFinite(ms) ? ms : 0 }), 'ok')
|
|
717
816
|
}
|
|
718
817
|
if (reconnect && state.token) { openStreams(); refreshSessions() }
|
|
719
818
|
} else if (!silent) {
|
|
720
|
-
if (best) toast(t('ds.speedAlreadyBest', { url: chosen, ms: state.serverLatency[best] }), 'ok')
|
|
819
|
+
if (best) toast(t('ds.speedAlreadyBest', { url: chosen, ms: state.serverLatency[serverKey(best)] }), 'ok')
|
|
721
820
|
else if (chosen) toast(t('ds.speedManualUsing', { url: chosen, ms: Number.isFinite(ms) ? ms : '—' }), 'ok')
|
|
722
821
|
else toast(t('ds.speedAllDown'), 'err')
|
|
723
822
|
}
|
|
@@ -757,13 +856,14 @@ function renderServers() {
|
|
|
757
856
|
</div>
|
|
758
857
|
<div class="ds-srv-body ${g === state.activeGroup ? '' : 'hidden'}">
|
|
759
858
|
${list.map(s => {
|
|
760
|
-
const ms = state.serverLatency[s
|
|
859
|
+
const ms = state.serverLatency[serverKey(s)]
|
|
761
860
|
let badge = `<span class="ds-server-badge">${t('ds.serversUntested')}</span>`
|
|
762
|
-
|
|
861
|
+
const active = s.url === state.server && serverToken(s) === state.token
|
|
862
|
+
if (Number.isFinite(ms)) badge = `<span class="ds-server-badge ${active ? 'good' : ''}">${ms}ms${active ? t('ds.serversCurrent') : ''}</span>`
|
|
763
863
|
else if (ms !== undefined) badge = `<span class="ds-server-badge bad">${t('ds.serversUnreachable')}</span>`
|
|
764
|
-
const activeInGroup = auto ?
|
|
864
|
+
const activeInGroup = auto ? active : s.id === activeManual
|
|
765
865
|
return `<div class="ds-server-row ${activeInGroup ? 'active' : ''}" data-use-server="${esc(s.id)}">
|
|
766
|
-
<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}
|
|
866
|
+
<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}
|
|
767
867
|
<button class="ds-mini" data-edit-server="${esc(s.id)}" title="${t('ds.serversEdit')}">✎</button>
|
|
768
868
|
<button class="ds-mini" data-del-server="${esc(s.id)}" title="${t('ds.serversDelete')}">✕</button>
|
|
769
869
|
</div>`
|
|
@@ -794,16 +894,19 @@ function renderServers() {
|
|
|
794
894
|
if (state.autoSelect[s.group] !== false) { editServer(id); return }
|
|
795
895
|
state.groupActive[s.group] = id
|
|
796
896
|
state.activeGroup = s.group
|
|
897
|
+
if (state.server !== s.url) resetFsForServerDesktop()
|
|
797
898
|
state.server = s.url
|
|
899
|
+
state.token = serverToken(s)
|
|
900
|
+
LS.set('token', state.token)
|
|
798
901
|
saveServers()
|
|
799
902
|
renderServers()
|
|
800
903
|
toast(t('ds.serversManualSelected', { url: serverTitle(s) }), 'ok')
|
|
801
904
|
if (state.token) { openStreams(); refreshSessions() }
|
|
802
905
|
}))
|
|
803
|
-
const cur =
|
|
906
|
+
const cur = currentServerEntry()
|
|
804
907
|
const curGroup = cur ? cur.group : state.activeGroup
|
|
805
908
|
const curLabel = cur ? (cur.note || cur.url) : (state.server || t('ds.origin'))
|
|
806
|
-
const curMs = state.serverLatency[
|
|
909
|
+
const curMs = cur ? state.serverLatency[serverKey(cur)] : undefined
|
|
807
910
|
$('server-desc').textContent = t('ds.serversCurrentDesc', { group: curGroup, url: curLabel, ms: Number.isFinite(curMs) ? curMs + 'ms' : '—' })
|
|
808
911
|
updateConn()
|
|
809
912
|
}
|
|
@@ -814,9 +917,10 @@ async function addServer() {
|
|
|
814
917
|
if (!raw) return toast(t('ds.serversNeedAddress'), 'err')
|
|
815
918
|
try { const u = new URL(raw); if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad') }
|
|
816
919
|
catch { return toast(t('ds.serversBadProtocol'), 'err') }
|
|
817
|
-
|
|
920
|
+
const token = normalizedServerToken(state.token)
|
|
921
|
+
if (state.servers.some(s => serverIdentity(s.url, serverToken(s)) === serverIdentity(raw, token))) return toast(t('ds.serversDuplicate'), 'err')
|
|
818
922
|
const note = (prompt(t('ds.serversPromptNote')) || '').trim()
|
|
819
|
-
state.servers.push({ id: newServerId(), url: raw, note, group: state.activeGroup })
|
|
923
|
+
state.servers.push({ id: newServerId(), url: raw, note, group: state.activeGroup, token })
|
|
820
924
|
saveServers()
|
|
821
925
|
if (input) input.value = ''
|
|
822
926
|
renderServers()
|
|
@@ -830,14 +934,23 @@ function editServer(id) {
|
|
|
830
934
|
if (!raw) return
|
|
831
935
|
try { const u = new URL(raw); if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad') }
|
|
832
936
|
catch { return toast(t('ds.serversBadProtocol'), 'err') }
|
|
833
|
-
|
|
937
|
+
const tokenInput = prompt(t('ds.serversPromptEditToken'), '')
|
|
938
|
+
if (tokenInput === null) return
|
|
939
|
+
const token = normalizedServerToken(tokenInput) || serverToken(s)
|
|
940
|
+
if (state.servers.some(x => x.id !== id && serverIdentity(x.url, serverToken(x)) === serverIdentity(raw, token))) return toast(t('ds.serversDuplicate'), 'err')
|
|
834
941
|
const note = prompt(t('ds.serversPromptEditNote', { url: raw }), s.note || '')
|
|
835
942
|
if (note === null) return
|
|
836
943
|
const group = prompt(t('ds.serversPromptEditGroup'), s.group || '默认')
|
|
837
944
|
if (group === null) return
|
|
838
|
-
const wasActive = state.server === s.url
|
|
839
|
-
|
|
840
|
-
|
|
945
|
+
const wasActive = state.server === s.url && state.token === serverToken(s)
|
|
946
|
+
const changedActiveUrl = wasActive && state.server !== raw
|
|
947
|
+
s.url = raw; s.note = note.trim(); s.group = ensureGroup(group.trim() || '默认'); s.token = token
|
|
948
|
+
if (wasActive) {
|
|
949
|
+
if (changedActiveUrl) resetFsForServerDesktop()
|
|
950
|
+
state.server = raw
|
|
951
|
+
state.token = token
|
|
952
|
+
LS.set('token', token)
|
|
953
|
+
}
|
|
841
954
|
saveServers(); renderServers(); toast(t('ds.serversEdited'), 'ok')
|
|
842
955
|
if (wasActive && state.token) selectFastestServer({ silent: true })
|
|
843
956
|
}
|
|
@@ -845,7 +958,7 @@ function removeServer(id) {
|
|
|
845
958
|
const s = state.servers.find(x => x.id === id)
|
|
846
959
|
if (!s) return
|
|
847
960
|
state.servers = state.servers.filter(x => x.id !== id)
|
|
848
|
-
const wasActive = state.server === s.url
|
|
961
|
+
const wasActive = state.server === s.url && state.token === serverToken(s)
|
|
849
962
|
for (const g of state.groups) if (state.groupActive[g] === id) state.groupActive[g] = ''
|
|
850
963
|
saveServers(); renderServers()
|
|
851
964
|
if (wasActive) { toast(t('ds.serversRemovedActive')); selectFastestServer({ silent: true }) }
|
|
@@ -1020,7 +1133,11 @@ function openStream(kind, handler, refreshOnOpen, isRestore, ticket = null) {
|
|
|
1020
1133
|
updateConn()
|
|
1021
1134
|
if (kind === 'mux') { state.approvals = []; state.questions = []; renderNotifStack() }
|
|
1022
1135
|
if (refreshOnOpen) refreshSessions()
|
|
1023
|
-
if (allStreamsOpen())
|
|
1136
|
+
if (allStreamsOpen()) {
|
|
1137
|
+
resyncAfterStreamOpen()
|
|
1138
|
+
void pingServer(state.server || location.origin)
|
|
1139
|
+
void refreshHostDescriptionDesktop()
|
|
1140
|
+
}
|
|
1024
1141
|
}
|
|
1025
1142
|
ws.onmessage = (msg) => {
|
|
1026
1143
|
if (!streamIsCurrent(kind, ws, generation)) return
|
|
@@ -1854,7 +1971,7 @@ function renderQueue() {
|
|
|
1854
1971
|
|
|
1855
1972
|
/* ---------------- 审批/提问通知卡片栈 ---------------- */
|
|
1856
1973
|
function serverLabel() {
|
|
1857
|
-
const cur =
|
|
1974
|
+
const cur = currentServerEntry()
|
|
1858
1975
|
return cur ? (cur.note || cur.url) : (state.server || location.host)
|
|
1859
1976
|
}
|
|
1860
1977
|
function renderNotifStack() {
|
|
@@ -1966,10 +2083,70 @@ function fsHeaders() {
|
|
|
1966
2083
|
return { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
1967
2084
|
}
|
|
1968
2085
|
function fsParent(p) {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2086
|
+
const meta = fsPathMeta(p)
|
|
2087
|
+
if (!meta.value) return null
|
|
2088
|
+
if (meta.root && fsPathEqual(meta.value, meta.root)) return meta.root
|
|
2089
|
+
const idx = meta.value.lastIndexOf(meta.separator)
|
|
2090
|
+
if (idx < 0) return meta.root || null
|
|
2091
|
+
const parent = meta.value.slice(0, idx)
|
|
2092
|
+
return meta.root && parent.length < meta.root.length ? meta.root : (parent || meta.root)
|
|
2093
|
+
}
|
|
2094
|
+
function fsJoin(dir, name) {
|
|
2095
|
+
const meta = fsPathMeta(dir)
|
|
2096
|
+
if (!meta.value) return String(name || '')
|
|
2097
|
+
return meta.value.endsWith(meta.separator) ? meta.value + name : meta.value + meta.separator + name
|
|
2098
|
+
}
|
|
2099
|
+
function fsPathMeta(input) {
|
|
2100
|
+
const source = String(input || '').trim()
|
|
2101
|
+
const windows = /^[A-Za-z]:(?:[\\/]|$)/.test(source) || /^[\\/]{2}[^\\/]+[\\/][^\\/]+/.test(source) || source.includes('\\')
|
|
2102
|
+
if (!windows) {
|
|
2103
|
+
let value = source.replace(/\/+/g, '/')
|
|
2104
|
+
const root = value.startsWith('/') ? '/' : ''
|
|
2105
|
+
if (root && value.length > root.length) value = value.replace(/\/+$/, '')
|
|
2106
|
+
return { value, root, separator: '/', windows: false }
|
|
2107
|
+
}
|
|
2108
|
+
let value = source.replace(/\//g, '\\')
|
|
2109
|
+
const unc = /^\\{2,}/.test(value)
|
|
2110
|
+
value = unc
|
|
2111
|
+
? '\\\\' + value.replace(/^\\+/, '').replace(/\\+/g, '\\')
|
|
2112
|
+
: value.replace(/\\+/g, '\\')
|
|
2113
|
+
let root = ''
|
|
2114
|
+
if (unc) {
|
|
2115
|
+
const parts = value.slice(2).split('\\').filter(Boolean)
|
|
2116
|
+
root = parts.length >= 2 ? `\\\\${parts[0]}\\${parts[1]}` : value
|
|
2117
|
+
} else {
|
|
2118
|
+
const drive = /^([A-Za-z]:)/.exec(value)
|
|
2119
|
+
if (drive) {
|
|
2120
|
+
root = drive[1] + '\\'
|
|
2121
|
+
if (value === drive[1]) value = root
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
if (root && value.length > root.length) value = value.replace(/\\+$/, '')
|
|
2125
|
+
return { value, root, separator: '\\', windows: true }
|
|
2126
|
+
}
|
|
2127
|
+
function fsPathKey(value) {
|
|
2128
|
+
const meta = fsPathMeta(value)
|
|
2129
|
+
return meta.windows ? meta.value.toLowerCase() : meta.value
|
|
2130
|
+
}
|
|
2131
|
+
function fsPathEqual(left, right) { return fsPathKey(left) === fsPathKey(right) }
|
|
2132
|
+
function fsPathInside(candidate, root) {
|
|
2133
|
+
const child = fsPathMeta(candidate)
|
|
2134
|
+
const boundary = fsPathMeta(root)
|
|
2135
|
+
if (!child.value || !boundary.value || child.windows !== boundary.windows) return false
|
|
2136
|
+
const childKey = child.windows ? child.value.toLowerCase() : child.value
|
|
2137
|
+
const rootKey = boundary.windows ? boundary.value.toLowerCase() : boundary.value
|
|
2138
|
+
if (childKey === rootKey) return true
|
|
2139
|
+
const prefix = rootKey.endsWith(boundary.separator) ? rootKey : rootKey + boundary.separator
|
|
2140
|
+
return childKey.startsWith(prefix)
|
|
2141
|
+
}
|
|
2142
|
+
function resetFsForServerDesktop() {
|
|
2143
|
+
state.fs.path = null
|
|
2144
|
+
state.fs.initial = null
|
|
2145
|
+
state.fs.loaded = false
|
|
2146
|
+
state.fs.roots = []
|
|
2147
|
+
state.fs.rootIndex = 0
|
|
2148
|
+
const select = $('fs-root')
|
|
2149
|
+
if (select) select.classList.add('hidden')
|
|
1973
2150
|
}
|
|
1974
2151
|
async function openWorkspaceModal() {
|
|
1975
2152
|
if (!state.token) { toast(t('ds.toastAuth'), 'err'); showView('view-settings'); return }
|
|
@@ -2031,10 +2208,15 @@ async function loadFs(dir, silent) {
|
|
|
2031
2208
|
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error || ('HTTP ' + res.status))
|
|
2032
2209
|
state.fs.path = data.path
|
|
2033
2210
|
if (!state.fs.initial) state.fs.initial = data.path
|
|
2211
|
+
if (Array.isArray(data.roots) && data.roots.length) state.fs.roots = data.roots.map(value => String(value || '')).filter(Boolean)
|
|
2212
|
+
if (!state.fs.roots.length) state.fs.roots = [data.path]
|
|
2213
|
+
const rootIndex = state.fs.roots.findIndex(root => fsPathInside(data.path, root))
|
|
2214
|
+
if (rootIndex >= 0) state.fs.rootIndex = rootIndex
|
|
2034
2215
|
state.fs.loaded = true
|
|
2216
|
+
renderFsRootsDesktop()
|
|
2035
2217
|
$('fs-path').textContent = data.path
|
|
2036
2218
|
$('fs-list').innerHTML = (data.entries || []).map(e => `
|
|
2037
|
-
<div class="ds-fs-row" data-fs-path="${esc(e.path)}" data-fs-dir="${e.type === 'dir' ? '1' : '0'}">
|
|
2219
|
+
<div class="ds-fs-row" data-fs-path="${esc(e.path || fsJoin(data.path, e.name))}" data-fs-dir="${e.type === 'dir' ? '1' : '0'}">
|
|
2038
2220
|
<span class="ds-fs-type">${desktopFsIconSvg(e.type === 'dir')}</span>
|
|
2039
2221
|
<span class="ds-fs-name">${esc(e.name)}</span>
|
|
2040
2222
|
<span class="ds-fs-size">${e.type === 'dir' ? '' : fmtSize(e.size)}</span>
|
|
@@ -2049,15 +2231,23 @@ async function loadFs(dir, silent) {
|
|
|
2049
2231
|
}
|
|
2050
2232
|
}
|
|
2051
2233
|
|
|
2234
|
+
function renderFsRootsDesktop() {
|
|
2235
|
+
const select = $('fs-root')
|
|
2236
|
+
if (!select) return
|
|
2237
|
+
select.classList.toggle('hidden', state.fs.roots.length <= 1)
|
|
2238
|
+
select.innerHTML = state.fs.roots.map((root, index) => `<option value="${index}"${index === state.fs.rootIndex ? ' selected' : ''}>${esc(root)}</option>`).join('')
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2052
2241
|
function desktopFsIconSvg(isDir) {
|
|
2053
2242
|
return isDir
|
|
2054
2243
|
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6.5h6l2 2H20a1 1 0 0 1 1 1v8.5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7.5a1 1 0 0 1 .5-1Z"/></svg>'
|
|
2055
2244
|
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3.5h8l4 4V20a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1Z"/><path d="M14 3.5v4h4M8 13h8M8 16h6"/></svg>'
|
|
2056
2245
|
}
|
|
2057
2246
|
function fsUp() {
|
|
2058
|
-
if (state.fs.path
|
|
2059
|
-
|
|
2060
|
-
|
|
2247
|
+
if (!state.fs.path || !state.fs.initial) return
|
|
2248
|
+
const parent = fsParent(state.fs.path)
|
|
2249
|
+
if (fsPathEqual(state.fs.path, state.fs.initial) || !fsPathInside(parent, state.fs.initial)) return
|
|
2250
|
+
loadFs(parent)
|
|
2061
2251
|
}
|
|
2062
2252
|
|
|
2063
2253
|
/* ---------------- 工作台绑定 / 项目会话 ---------------- */
|
|
@@ -2375,6 +2565,8 @@ function toggleStatsDrawer() {
|
|
|
2375
2565
|
async function loadStats() {
|
|
2376
2566
|
if (!state.token) {
|
|
2377
2567
|
$('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsGatewayDown')}</div>`
|
|
2568
|
+
$('stats-token-chart').innerHTML = ''
|
|
2569
|
+
$('stats-token-legend').innerHTML = ''
|
|
2378
2570
|
return
|
|
2379
2571
|
}
|
|
2380
2572
|
try {
|
|
@@ -2384,11 +2576,15 @@ async function loadStats() {
|
|
|
2384
2576
|
renderStats(json.days || [])
|
|
2385
2577
|
} catch {
|
|
2386
2578
|
$('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsGatewayDown')}</div>`
|
|
2579
|
+
$('stats-token-chart').innerHTML = ''
|
|
2580
|
+
$('stats-token-legend').innerHTML = ''
|
|
2387
2581
|
}
|
|
2388
2582
|
}
|
|
2389
2583
|
function renderStats(days) {
|
|
2390
2584
|
if (!days.length) {
|
|
2391
2585
|
$('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsEmpty')}</div>`
|
|
2586
|
+
$('stats-token-chart').innerHTML = ''
|
|
2587
|
+
$('stats-token-legend').innerHTML = ''
|
|
2392
2588
|
return
|
|
2393
2589
|
}
|
|
2394
2590
|
const today = days[days.length - 1]
|
|
@@ -2408,6 +2604,7 @@ function renderStats(days) {
|
|
|
2408
2604
|
<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>
|
|
2409
2605
|
<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>`
|
|
2410
2606
|
$('stats-legend').innerHTML = `<span class="sw peak"></span>${t('ds.statsPeak')} <span class="sw off"></span>${t('ds.statsOff')}`
|
|
2607
|
+
$('stats-token-legend').innerHTML = `<span class="sw token"></span>${t('ds.statsTokenTotal')}`
|
|
2411
2608
|
$('stats-note').textContent = t('ds.statsNote')
|
|
2412
2609
|
const maxCost = Math.max(...days.map(d => (d.total.cost || 0)), 0.0001)
|
|
2413
2610
|
$('stats-chart').innerHTML = days.map(d => {
|
|
@@ -2422,6 +2619,17 @@ function renderStats(days) {
|
|
|
2422
2619
|
<div class="lbl">${d.date.slice(5)}</div>
|
|
2423
2620
|
</div>`
|
|
2424
2621
|
}).join('')
|
|
2622
|
+
const maxTokens = Math.max(...days.map(d => bucketTokens(d.total)), 1)
|
|
2623
|
+
$('stats-token-chart').innerHTML = days.map(d => {
|
|
2624
|
+
const tokens = bucketTokens(d.total)
|
|
2625
|
+
const totalH = tokens > 0 ? Math.max(3, Math.round(tokens / maxTokens * 100)) : 0
|
|
2626
|
+
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)}`
|
|
2627
|
+
return `<div class="ds-stats-bar" data-tip="${esc(tip)}">
|
|
2628
|
+
<div class="bars token" style="height:${totalH}%"></div>
|
|
2629
|
+
<div class="val">${tokens > 0 ? fmtTokens(tokens) : ''}</div>
|
|
2630
|
+
<div class="lbl">${d.date.slice(5)}</div>
|
|
2631
|
+
</div>`
|
|
2632
|
+
}).join('')
|
|
2425
2633
|
}
|
|
2426
2634
|
|
|
2427
2635
|
/* ---------------- 视图与连接状态 ---------------- */
|
|
@@ -2431,7 +2639,7 @@ function renderOverviewDesktop() {
|
|
|
2431
2639
|
const checks = {
|
|
2432
2640
|
// 桌面独立页面默认使用当前 origin,state.server 为空不代表网关离线。
|
|
2433
2641
|
gateway: !!state.token && (!!state.server || /^https?:$/.test(location.protocol)),
|
|
2434
|
-
dsh:
|
|
2642
|
+
dsh: dshReachable(),
|
|
2435
2643
|
mux: !!state.streamsOk?.mux,
|
|
2436
2644
|
host: !!state.streamsOk?.host
|
|
2437
2645
|
}
|
|
@@ -2557,7 +2765,7 @@ function updateConn() {
|
|
|
2557
2765
|
const el = $('conn-badge')
|
|
2558
2766
|
// 双实时通道可能按任意顺序打开,连接刷新必须同时刷新系统总览。
|
|
2559
2767
|
renderOverviewDesktop()
|
|
2560
|
-
const cur =
|
|
2768
|
+
const cur = currentServerEntry()
|
|
2561
2769
|
const group = cur ? cur.group : state.activeGroup
|
|
2562
2770
|
const label = cur ? (cur.note || cur.url) : (state.server || t('ds.origin'))
|
|
2563
2771
|
const serverText = t('ds.currentServer', { group, url: label })
|
|
@@ -2626,8 +2834,7 @@ function bindUi() {
|
|
|
2626
2834
|
toast(t('ds.loading'))
|
|
2627
2835
|
if (state.token) {
|
|
2628
2836
|
await refreshSessions()
|
|
2629
|
-
|
|
2630
|
-
if (host) state.hostInfo = host
|
|
2837
|
+
await refreshHostDescriptionDesktop({ notify: true })
|
|
2631
2838
|
}
|
|
2632
2839
|
renderOverviewDesktop()
|
|
2633
2840
|
})
|
|
@@ -2759,6 +2966,11 @@ function bindUi() {
|
|
|
2759
2966
|
if (bar && bar.dataset.tip) showTip(bar.dataset.tip, bar.getBoundingClientRect())
|
|
2760
2967
|
})
|
|
2761
2968
|
$('stats-chart').addEventListener('mouseleave', hideTip)
|
|
2969
|
+
$('stats-token-chart').addEventListener('mouseover', (e) => {
|
|
2970
|
+
const bar = e.target.closest('.ds-stats-bar')
|
|
2971
|
+
if (bar && bar.dataset.tip) showTip(bar.dataset.tip, bar.getBoundingClientRect())
|
|
2972
|
+
})
|
|
2973
|
+
$('stats-token-chart').addEventListener('mouseleave', hideTip)
|
|
2762
2974
|
|
|
2763
2975
|
$('view-settings').addEventListener('click', (e) => {
|
|
2764
2976
|
const group = e.target.closest('[data-settings-group]')
|
|
@@ -2791,6 +3003,12 @@ function bindUi() {
|
|
|
2791
3003
|
renderServers(); renderSessions(); renderNotifStack(); renderOverviewDesktop(); updateConn(); themeApply()
|
|
2792
3004
|
})
|
|
2793
3005
|
$('fs-up').addEventListener('click', fsUp)
|
|
3006
|
+
$('fs-root').addEventListener('change', (event) => {
|
|
3007
|
+
const index = Math.min(Number(event.target.value) || 0, Math.max(0, state.fs.roots.length - 1))
|
|
3008
|
+
state.fs.rootIndex = index
|
|
3009
|
+
state.fs.initial = null
|
|
3010
|
+
loadFs(state.fs.roots[index] || null, true)
|
|
3011
|
+
})
|
|
2794
3012
|
$('fs-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
2795
3013
|
$('fs-refresh').addEventListener('click', () => loadFs(state.fs.path || null))
|
|
2796
3014
|
$('btn-question-submit').addEventListener('click', submitQuestion)
|
|
@@ -2798,11 +3016,23 @@ function bindUi() {
|
|
|
2798
3016
|
}
|
|
2799
3017
|
|
|
2800
3018
|
async function start() {
|
|
3019
|
+
const urlToken = new URLSearchParams(location.search).get('token')?.trim() || ''
|
|
3020
|
+
if (urlToken) {
|
|
3021
|
+
state.token = urlToken
|
|
3022
|
+
LS.set('token', urlToken)
|
|
3023
|
+
history.replaceState(null, '', location.pathname)
|
|
3024
|
+
}
|
|
2801
3025
|
loadServers()
|
|
2802
3026
|
renderServers()
|
|
2803
3027
|
showView('view-overview')
|
|
2804
|
-
|
|
2805
|
-
|
|
3028
|
+
if (urlToken) {
|
|
3029
|
+
state.token = urlToken
|
|
3030
|
+
const currentId = state.groupActive[state.activeGroup]
|
|
3031
|
+
const current = state.servers.find(s => s.id === currentId) || state.servers.find(s => s.url === state.server)
|
|
3032
|
+
if (current) current.token = urlToken
|
|
3033
|
+
LS.set('token', urlToken)
|
|
3034
|
+
saveServers()
|
|
3035
|
+
}
|
|
2806
3036
|
if (!state.token) {
|
|
2807
3037
|
const input = prompt(t('ds.tokenTitle'))
|
|
2808
3038
|
if (input && input.trim()) { state.token = input.trim(); LS.set('token', state.token) }
|
|
@@ -2813,11 +3043,10 @@ async function start() {
|
|
|
2813
3043
|
updateConn()
|
|
2814
3044
|
checkNotesOnStart()
|
|
2815
3045
|
if (state.token) {
|
|
2816
|
-
|
|
3046
|
+
await selectFastestServer({ silent: true, reconnect: false })
|
|
2817
3047
|
openStreams()
|
|
2818
3048
|
await refreshSessions()
|
|
2819
|
-
|
|
2820
|
-
if (host) state.hostInfo = host
|
|
3049
|
+
await refreshHostDescriptionDesktop()
|
|
2821
3050
|
refreshWorkbench({ silent: true })
|
|
2822
3051
|
}
|
|
2823
3052
|
renderOverviewDesktop()
|