dsh-remote-plugin 0.6.17 → 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/package.json +1 -1
- package/public/app.js +241 -61
- package/public/desktop/desktop.css +4 -0
- package/public/desktop/desktop.html +16 -12
- package/public/desktop/desktop.js +114 -32
- package/public/index.html +58 -19
- package/public/styles.css +87 -19
- package/public/update.json +8 -8
- package/public/version.json +1 -1
|
@@ -303,8 +303,12 @@
|
|
|
303
303
|
</div>
|
|
304
304
|
<div class="ds-drawer-body">
|
|
305
305
|
<div class="ds-stat-grid" id="stats-cards"></div>
|
|
306
|
+
<div class="ds-stats-chart-title" data-i18n="ds.statsCostTrend">费用趋势</div>
|
|
306
307
|
<div id="stats-legend" class="ds-stats-legend"></div>
|
|
307
308
|
<div id="stats-chart" class="ds-stats-chart"></div>
|
|
309
|
+
<div class="ds-stats-chart-title" data-i18n="ds.statsTokenTrend">Token 量趋势</div>
|
|
310
|
+
<div id="stats-token-legend" class="ds-stats-legend"></div>
|
|
311
|
+
<div id="stats-token-chart" class="ds-stats-chart"></div>
|
|
308
312
|
<div id="stats-note" class="ds-stats-note"></div>
|
|
309
313
|
</div>
|
|
310
314
|
</div>
|
|
@@ -462,7 +466,7 @@
|
|
|
462
466
|
'ds.fsUp': '上级', 'ds.fsRoot': '允许根目录', 'ds.fsNewWorkspace': '新建工作区', 'ds.fsRefresh': '刷新', 'ds.fsEmpty': '目录为空',
|
|
463
467
|
'ds.workspaceCreateTitle': '新建工作区', 'ds.workspaceCreateDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'ds.workspaceParent': '父目录', 'ds.workspaceNamePlaceholder': '例如:my-project', 'ds.workspaceCreate': '创建并打开', 'ds.cancel': '取消', 'ds.workspaceNameRequired': '请输入工作区名称', 'ds.workspaceExists': '该目录已存在', 'ds.workspaceInvalidName': '名称不能包含路径分隔符', 'ds.workspaceCreateFailed': '创建工作区失败', 'ds.workspaceCreated': '工作区已创建', 'ds.workspaceCreatedNoSession': '工作区已创建,但新会话未能打开',
|
|
464
468
|
'ds.groupGeneral': '通用', 'ds.groupGeneralDesc': '工具调用、预设提示词',
|
|
465
|
-
'ds.groupServers': '服务器', 'ds.groupServersDesc': '
|
|
469
|
+
'ds.groupServers': '服务器', 'ds.groupServersDesc': '服务器地址与令牌',
|
|
466
470
|
'ds.groupNotify': '通知', 'ds.groupNotifyDesc': '通知与提醒',
|
|
467
471
|
'ds.groupTheme': '皮肤', 'ds.groupThemeDesc': '配色主题',
|
|
468
472
|
'ds.groupAbout': '关于', 'ds.groupAboutDesc': '版本信息',
|
|
@@ -498,18 +502,18 @@
|
|
|
498
502
|
'ds.approvalBody': '{tool} · {server}', 'ds.approvalReason': '原因:{reason}',
|
|
499
503
|
'ds.allowed': '已允许', 'ds.rejected': '已拒绝', 'ds.ignored': '已忽略',
|
|
500
504
|
'ds.stale': '请求已失效', 'ds.pendingSubmitFailed': '提交失败:{msg}',
|
|
501
|
-
'ds.statsTodayTokens': '今日 Token', 'ds.statsTodayCost': '今日费用', 'ds.statsPeakShare': '高峰占比',
|
|
505
|
+
'ds.statsTodayTokens': '今日 Token', 'ds.statsTodayCost': '今日费用', 'ds.statsPeakShare': '高峰占比', 'ds.statsCostTrend': '费用趋势', 'ds.statsTokenTrend': 'Token 量趋势', 'ds.statsTokenTotal': 'Token 总量',
|
|
502
506
|
'ds.statsInput': '未缓存输入', 'ds.statsCacheRead': '缓存命中', 'ds.statsCacheWrite': '缓存写入', 'ds.statsOutput': '输出',
|
|
503
507
|
'ds.statsPeak': '高峰', 'ds.statsOff': '空闲', 'ds.statsDays': '近 {n} 天',
|
|
504
508
|
'ds.statsGatewayDown': '统计需要网关运行', 'ds.statsEmpty': '暂无统计',
|
|
505
|
-
'ds.statsNote': '
|
|
506
|
-
'ds.serversEmpty': '未添加备用服务器', 'ds.serversUntested': '未测速', 'ds.serversCurrent': ' · 当前',
|
|
509
|
+
'ds.statsNote': '注:Token 量按 DSH usage 事件记录,第三方 provider 需提供 usage 才能计入;费用仅对已知 DeepSeek 模型估算,与官网账单可能有出入,一切以官网为准。统计自 2026-08-17 定价生效日起。',
|
|
510
|
+
'ds.serversEmpty': '未添加备用服务器', 'ds.serversUntested': '未测速', 'ds.serversCurrent': ' · 当前', 'ds.serversTokenSet': '令牌已配置', 'ds.serversTokenMissing': '未配置令牌',
|
|
507
511
|
'ds.serversUnreachable': '不可达', 'ds.serversDelete': '删除', 'ds.serversEdit': '编辑',
|
|
508
512
|
'ds.serversNeedAddress': '请输入服务器地址', 'ds.serversBadProtocol': '地址需以 http:// 或 https:// 开头',
|
|
509
|
-
'ds.serversDuplicate': '
|
|
513
|
+
'ds.serversDuplicate': '相同地址和令牌已存在', 'ds.serversAdded': '已添加', 'ds.serversEdited': '已更新',
|
|
510
514
|
'ds.serversRemovedActive': '已删除当前服务器,重新测速…',
|
|
511
515
|
'ds.serversCurrentDesc': '当前:{group} · {url}({ms})',
|
|
512
|
-
'ds.serversPromptNote': '服务器备注(可留空):', 'ds.serversPromptEditUrl': '编辑服务器地址:',
|
|
516
|
+
'ds.serversPromptNote': '服务器备注(可留空):', 'ds.serversPromptEditUrl': '编辑服务器地址:', 'ds.serversPromptEditToken': '编辑服务器令牌(留空保持不变):',
|
|
513
517
|
'ds.serversPromptEditNote': '编辑备注({url}):', 'ds.serversPromptEditGroup': '所属组:',
|
|
514
518
|
'ds.serversManualSelected': '已切换至:{url}',
|
|
515
519
|
'ds.speedTesting': '正在测速…', 'ds.speedSwitched': '已切换到:{url}({ms}ms)',
|
|
@@ -564,7 +568,7 @@
|
|
|
564
568
|
'ds.fsUp': 'Up', 'ds.fsRoot': 'Allowed root', 'ds.fsNewWorkspace': 'New workspace', 'ds.fsRefresh': 'Refresh', 'ds.fsEmpty': 'Empty directory',
|
|
565
569
|
'ds.workspaceCreateTitle': 'New workspace', 'ds.workspaceCreateDesc': 'Create a folder in the current file directory and open a new session there.', 'ds.workspaceParent': 'Parent folder', 'ds.workspaceNamePlaceholder': 'For example: my-project', 'ds.workspaceCreate': 'Create & open', 'ds.cancel': 'Cancel', 'ds.workspaceNameRequired': 'Enter a workspace name', 'ds.workspaceExists': 'That folder already exists', 'ds.workspaceInvalidName': 'The name cannot contain path separators', 'ds.workspaceCreateFailed': 'Could not create workspace', 'ds.workspaceCreated': 'Workspace created', 'ds.workspaceCreatedNoSession': 'Workspace created, but the new session could not be opened',
|
|
566
570
|
'ds.groupGeneral': 'General', 'ds.groupGeneralDesc': 'Tool calls, prompt presets',
|
|
567
|
-
'ds.groupServers': 'Servers', 'ds.groupServersDesc': 'Server address',
|
|
571
|
+
'ds.groupServers': 'Servers', 'ds.groupServersDesc': 'Server address and token',
|
|
568
572
|
'ds.groupNotify': 'Notifications', 'ds.groupNotifyDesc': 'Notifications & reminders',
|
|
569
573
|
'ds.groupTheme': 'Theme', 'ds.groupThemeDesc': 'Color themes',
|
|
570
574
|
'ds.groupAbout': 'About', 'ds.groupAboutDesc': 'Version info',
|
|
@@ -600,18 +604,18 @@
|
|
|
600
604
|
'ds.approvalBody': '{tool} · {server}', 'ds.approvalReason': 'Reason: {reason}',
|
|
601
605
|
'ds.allowed': 'Allowed', 'ds.rejected': 'Rejected', 'ds.ignored': 'Ignored',
|
|
602
606
|
'ds.stale': 'Request is stale', 'ds.pendingSubmitFailed': 'Submit failed: {msg}',
|
|
603
|
-
'ds.statsTodayTokens': 'Tokens today', 'ds.statsTodayCost': 'Cost today', 'ds.statsPeakShare': 'Peak share',
|
|
607
|
+
'ds.statsTodayTokens': 'Tokens today', 'ds.statsTodayCost': 'Cost today', 'ds.statsPeakShare': 'Peak share', 'ds.statsCostTrend': 'Cost trend', 'ds.statsTokenTrend': 'Token volume trend', 'ds.statsTokenTotal': 'Total tokens',
|
|
604
608
|
'ds.statsInput': 'Uncached input', 'ds.statsCacheRead': 'Cache read', 'ds.statsCacheWrite': 'Cache write', 'ds.statsOutput': 'Output',
|
|
605
609
|
'ds.statsPeak': 'Peak', 'ds.statsOff': 'Off-peak', 'ds.statsDays': 'Last {n} days',
|
|
606
610
|
'ds.statsGatewayDown': 'Stats require the gateway', 'ds.statsEmpty': 'No stats yet',
|
|
607
|
-
'ds.statsNote': 'Note:
|
|
608
|
-
'ds.serversEmpty': 'No backup servers', 'ds.serversUntested': 'Untested', 'ds.serversCurrent': ' · current',
|
|
611
|
+
'ds.statsNote': 'Note: token volume uses DSH usage events; a third-party provider must expose usage to be counted. Costs are estimates for known DeepSeek models and may differ from the official bill; always defer to deepseek.com. Stats start from the 2026-08-17 pricing date.',
|
|
612
|
+
'ds.serversEmpty': 'No backup servers', 'ds.serversUntested': 'Untested', 'ds.serversCurrent': ' · current', 'ds.serversTokenSet': 'Token set', 'ds.serversTokenMissing': 'No token',
|
|
609
613
|
'ds.serversUnreachable': 'Unreachable', 'ds.serversDelete': 'Delete', 'ds.serversEdit': 'Edit',
|
|
610
614
|
'ds.serversNeedAddress': 'Enter a server address', 'ds.serversBadProtocol': 'Address must start with http:// or https://',
|
|
611
|
-
'ds.serversDuplicate': '
|
|
615
|
+
'ds.serversDuplicate': 'The same address and token already exist', 'ds.serversAdded': 'Added', 'ds.serversEdited': 'Updated',
|
|
612
616
|
'ds.serversRemovedActive': 'Removed active server, re-testing…',
|
|
613
617
|
'ds.serversCurrentDesc': 'Current: {group} · {url} ({ms})',
|
|
614
|
-
'ds.serversPromptNote': 'Server note (optional):', 'ds.serversPromptEditUrl': 'Edit server URL:',
|
|
618
|
+
'ds.serversPromptNote': 'Server note (optional):', 'ds.serversPromptEditUrl': 'Edit server URL:', 'ds.serversPromptEditToken': 'Edit server token (blank keeps the current token):',
|
|
615
619
|
'ds.serversPromptEditNote': 'Edit note ({url}):', 'ds.serversPromptEditGroup': 'Group:',
|
|
616
620
|
'ds.serversManualSelected': 'Switched to: {url}',
|
|
617
621
|
'ds.speedTesting': 'Testing servers…', 'ds.speedSwitched': 'Switched to: {url} ({ms}ms)',
|
|
@@ -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: [],
|
|
@@ -661,6 +661,27 @@ function ensureGroup(name) {
|
|
|
661
661
|
}
|
|
662
662
|
function groupServers(g) { return state.servers.filter(s => s.group === g) }
|
|
663
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
|
+
}
|
|
664
685
|
|
|
665
686
|
function migrateServersV1() {
|
|
666
687
|
if (LS.get('servers-v2', null) !== null) return
|
|
@@ -671,7 +692,7 @@ function migrateServersV1() {
|
|
|
671
692
|
arr = legacy ? [legacy] : []
|
|
672
693
|
}
|
|
673
694
|
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: '默认' }))
|
|
695
|
+
state.servers = urls.map((url, i) => ({ id: 's' + (i + 1), url, note: '', group: '默认', token: normalizedServerToken(state.token) }))
|
|
675
696
|
state.groups = ['默认']; state.activeGroup = '默认'
|
|
676
697
|
state.autoSelect = { '默认': true }; state.groupActive = { '默认': '' }
|
|
677
698
|
const active = LS.get('activeServer', '')
|
|
@@ -687,7 +708,8 @@ function loadServers() {
|
|
|
687
708
|
let data = null
|
|
688
709
|
try { data = JSON.parse(LS.get('servers-v2', '')) } catch {}
|
|
689
710
|
if (!data || !Array.isArray(data.servers)) { migrateServersV1(); return }
|
|
690
|
-
|
|
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) }))
|
|
691
713
|
state.groups = Array.isArray(data.groups) && data.groups.length ? data.groups : ['默认']
|
|
692
714
|
state.activeGroup = state.groups.includes(data.activeGroup) ? data.activeGroup : '默认'
|
|
693
715
|
state.autoSelect = data.autoSelect || {}
|
|
@@ -696,7 +718,12 @@ function loadServers() {
|
|
|
696
718
|
for (const s of state.servers) ensureGroup(s.group)
|
|
697
719
|
const manual = state.groupActive[state.activeGroup]
|
|
698
720
|
const manualSrv = manual ? state.servers.find(s => s.id === manual) : null
|
|
699
|
-
|
|
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
|
+
}
|
|
700
727
|
}
|
|
701
728
|
function saveServers() {
|
|
702
729
|
LS.set('servers-v2', JSON.stringify({
|
|
@@ -705,8 +732,8 @@ function saveServers() {
|
|
|
705
732
|
}))
|
|
706
733
|
}
|
|
707
734
|
function serverCandidates() {
|
|
708
|
-
const list = activeServers().map(s => s
|
|
709
|
-
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 })
|
|
710
737
|
return list
|
|
711
738
|
}
|
|
712
739
|
async function pingServer(base) {
|
|
@@ -749,36 +776,47 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
|
749
776
|
if (!silent) toast(t('ds.speedTesting'))
|
|
750
777
|
const candidates = serverCandidates()
|
|
751
778
|
let chosen = ''
|
|
779
|
+
let chosenToken = state.token
|
|
752
780
|
let best = null
|
|
753
781
|
let ms = Infinity
|
|
754
782
|
if (state.autoSelect[state.activeGroup] !== false) {
|
|
755
|
-
const measured = await Promise.all(candidates.map(async (
|
|
756
|
-
for (const [
|
|
757
|
-
best = candidates.filter(
|
|
758
|
-
|
|
759
|
-
|
|
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
|
|
760
790
|
} else {
|
|
761
791
|
const manual = state.groupActive[state.activeGroup]
|
|
762
792
|
const manualSrv = manual ? state.servers.find(s => s.id === manual) : null
|
|
763
793
|
chosen = manualSrv ? manualSrv.url : (activeServers()[0]?.url || '')
|
|
764
|
-
|
|
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
|
+
}
|
|
765
800
|
}
|
|
766
801
|
renderServers()
|
|
767
|
-
if (chosen !== state.server) {
|
|
802
|
+
if (chosen !== state.server || chosenToken !== state.token) {
|
|
768
803
|
state.hostInfo = null
|
|
769
804
|
hostDescribeFailures = 0
|
|
770
805
|
if (hostDescribeRetryTimer) clearTimeout(hostDescribeRetryTimer)
|
|
771
806
|
hostDescribeRetryTimer = null
|
|
772
807
|
resetFsForServerDesktop()
|
|
773
808
|
state.server = chosen
|
|
774
|
-
|
|
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 }
|
|
775
812
|
saveServers()
|
|
813
|
+
renderServers()
|
|
776
814
|
if (!silent) {
|
|
777
815
|
if (chosen) toast(t('ds.speedSwitched', { url: chosen, ms: Number.isFinite(ms) ? ms : 0 }), 'ok')
|
|
778
816
|
}
|
|
779
817
|
if (reconnect && state.token) { openStreams(); refreshSessions() }
|
|
780
818
|
} else if (!silent) {
|
|
781
|
-
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')
|
|
782
820
|
else if (chosen) toast(t('ds.speedManualUsing', { url: chosen, ms: Number.isFinite(ms) ? ms : '—' }), 'ok')
|
|
783
821
|
else toast(t('ds.speedAllDown'), 'err')
|
|
784
822
|
}
|
|
@@ -818,13 +856,14 @@ function renderServers() {
|
|
|
818
856
|
</div>
|
|
819
857
|
<div class="ds-srv-body ${g === state.activeGroup ? '' : 'hidden'}">
|
|
820
858
|
${list.map(s => {
|
|
821
|
-
const ms = state.serverLatency[s
|
|
859
|
+
const ms = state.serverLatency[serverKey(s)]
|
|
822
860
|
let badge = `<span class="ds-server-badge">${t('ds.serversUntested')}</span>`
|
|
823
|
-
|
|
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>`
|
|
824
863
|
else if (ms !== undefined) badge = `<span class="ds-server-badge bad">${t('ds.serversUnreachable')}</span>`
|
|
825
|
-
const activeInGroup = auto ?
|
|
864
|
+
const activeInGroup = auto ? active : s.id === activeManual
|
|
826
865
|
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}
|
|
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}
|
|
828
867
|
<button class="ds-mini" data-edit-server="${esc(s.id)}" title="${t('ds.serversEdit')}">✎</button>
|
|
829
868
|
<button class="ds-mini" data-del-server="${esc(s.id)}" title="${t('ds.serversDelete')}">✕</button>
|
|
830
869
|
</div>`
|
|
@@ -857,15 +896,17 @@ function renderServers() {
|
|
|
857
896
|
state.activeGroup = s.group
|
|
858
897
|
if (state.server !== s.url) resetFsForServerDesktop()
|
|
859
898
|
state.server = s.url
|
|
899
|
+
state.token = serverToken(s)
|
|
900
|
+
LS.set('token', state.token)
|
|
860
901
|
saveServers()
|
|
861
902
|
renderServers()
|
|
862
903
|
toast(t('ds.serversManualSelected', { url: serverTitle(s) }), 'ok')
|
|
863
904
|
if (state.token) { openStreams(); refreshSessions() }
|
|
864
905
|
}))
|
|
865
|
-
const cur =
|
|
906
|
+
const cur = currentServerEntry()
|
|
866
907
|
const curGroup = cur ? cur.group : state.activeGroup
|
|
867
908
|
const curLabel = cur ? (cur.note || cur.url) : (state.server || t('ds.origin'))
|
|
868
|
-
const curMs = state.serverLatency[
|
|
909
|
+
const curMs = cur ? state.serverLatency[serverKey(cur)] : undefined
|
|
869
910
|
$('server-desc').textContent = t('ds.serversCurrentDesc', { group: curGroup, url: curLabel, ms: Number.isFinite(curMs) ? curMs + 'ms' : '—' })
|
|
870
911
|
updateConn()
|
|
871
912
|
}
|
|
@@ -876,9 +917,10 @@ async function addServer() {
|
|
|
876
917
|
if (!raw) return toast(t('ds.serversNeedAddress'), 'err')
|
|
877
918
|
try { const u = new URL(raw); if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad') }
|
|
878
919
|
catch { return toast(t('ds.serversBadProtocol'), 'err') }
|
|
879
|
-
|
|
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')
|
|
880
922
|
const note = (prompt(t('ds.serversPromptNote')) || '').trim()
|
|
881
|
-
state.servers.push({ id: newServerId(), url: raw, note, group: state.activeGroup })
|
|
923
|
+
state.servers.push({ id: newServerId(), url: raw, note, group: state.activeGroup, token })
|
|
882
924
|
saveServers()
|
|
883
925
|
if (input) input.value = ''
|
|
884
926
|
renderServers()
|
|
@@ -892,17 +934,22 @@ function editServer(id) {
|
|
|
892
934
|
if (!raw) return
|
|
893
935
|
try { const u = new URL(raw); if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('bad') }
|
|
894
936
|
catch { return toast(t('ds.serversBadProtocol'), 'err') }
|
|
895
|
-
|
|
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')
|
|
896
941
|
const note = prompt(t('ds.serversPromptEditNote', { url: raw }), s.note || '')
|
|
897
942
|
if (note === null) return
|
|
898
943
|
const group = prompt(t('ds.serversPromptEditGroup'), s.group || '默认')
|
|
899
944
|
if (group === null) return
|
|
900
|
-
const wasActive = state.server === s.url
|
|
945
|
+
const wasActive = state.server === s.url && state.token === serverToken(s)
|
|
901
946
|
const changedActiveUrl = wasActive && state.server !== raw
|
|
902
|
-
s.url = raw; s.note = note.trim(); s.group = ensureGroup(group.trim() || '默认')
|
|
947
|
+
s.url = raw; s.note = note.trim(); s.group = ensureGroup(group.trim() || '默认'); s.token = token
|
|
903
948
|
if (wasActive) {
|
|
904
949
|
if (changedActiveUrl) resetFsForServerDesktop()
|
|
905
950
|
state.server = raw
|
|
951
|
+
state.token = token
|
|
952
|
+
LS.set('token', token)
|
|
906
953
|
}
|
|
907
954
|
saveServers(); renderServers(); toast(t('ds.serversEdited'), 'ok')
|
|
908
955
|
if (wasActive && state.token) selectFastestServer({ silent: true })
|
|
@@ -911,7 +958,7 @@ function removeServer(id) {
|
|
|
911
958
|
const s = state.servers.find(x => x.id === id)
|
|
912
959
|
if (!s) return
|
|
913
960
|
state.servers = state.servers.filter(x => x.id !== id)
|
|
914
|
-
const wasActive = state.server === s.url
|
|
961
|
+
const wasActive = state.server === s.url && state.token === serverToken(s)
|
|
915
962
|
for (const g of state.groups) if (state.groupActive[g] === id) state.groupActive[g] = ''
|
|
916
963
|
saveServers(); renderServers()
|
|
917
964
|
if (wasActive) { toast(t('ds.serversRemovedActive')); selectFastestServer({ silent: true }) }
|
|
@@ -1924,7 +1971,7 @@ function renderQueue() {
|
|
|
1924
1971
|
|
|
1925
1972
|
/* ---------------- 审批/提问通知卡片栈 ---------------- */
|
|
1926
1973
|
function serverLabel() {
|
|
1927
|
-
const cur =
|
|
1974
|
+
const cur = currentServerEntry()
|
|
1928
1975
|
return cur ? (cur.note || cur.url) : (state.server || location.host)
|
|
1929
1976
|
}
|
|
1930
1977
|
function renderNotifStack() {
|
|
@@ -2518,6 +2565,8 @@ function toggleStatsDrawer() {
|
|
|
2518
2565
|
async function loadStats() {
|
|
2519
2566
|
if (!state.token) {
|
|
2520
2567
|
$('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsGatewayDown')}</div>`
|
|
2568
|
+
$('stats-token-chart').innerHTML = ''
|
|
2569
|
+
$('stats-token-legend').innerHTML = ''
|
|
2521
2570
|
return
|
|
2522
2571
|
}
|
|
2523
2572
|
try {
|
|
@@ -2527,11 +2576,15 @@ async function loadStats() {
|
|
|
2527
2576
|
renderStats(json.days || [])
|
|
2528
2577
|
} catch {
|
|
2529
2578
|
$('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsGatewayDown')}</div>`
|
|
2579
|
+
$('stats-token-chart').innerHTML = ''
|
|
2580
|
+
$('stats-token-legend').innerHTML = ''
|
|
2530
2581
|
}
|
|
2531
2582
|
}
|
|
2532
2583
|
function renderStats(days) {
|
|
2533
2584
|
if (!days.length) {
|
|
2534
2585
|
$('stats-cards').innerHTML = `<div class="ds-empty">${t('ds.statsEmpty')}</div>`
|
|
2586
|
+
$('stats-token-chart').innerHTML = ''
|
|
2587
|
+
$('stats-token-legend').innerHTML = ''
|
|
2535
2588
|
return
|
|
2536
2589
|
}
|
|
2537
2590
|
const today = days[days.length - 1]
|
|
@@ -2551,6 +2604,7 @@ function renderStats(days) {
|
|
|
2551
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>
|
|
2552
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>`
|
|
2553
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')}`
|
|
2554
2608
|
$('stats-note').textContent = t('ds.statsNote')
|
|
2555
2609
|
const maxCost = Math.max(...days.map(d => (d.total.cost || 0)), 0.0001)
|
|
2556
2610
|
$('stats-chart').innerHTML = days.map(d => {
|
|
@@ -2565,6 +2619,17 @@ function renderStats(days) {
|
|
|
2565
2619
|
<div class="lbl">${d.date.slice(5)}</div>
|
|
2566
2620
|
</div>`
|
|
2567
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('')
|
|
2568
2633
|
}
|
|
2569
2634
|
|
|
2570
2635
|
/* ---------------- 视图与连接状态 ---------------- */
|
|
@@ -2700,7 +2765,7 @@ function updateConn() {
|
|
|
2700
2765
|
const el = $('conn-badge')
|
|
2701
2766
|
// 双实时通道可能按任意顺序打开,连接刷新必须同时刷新系统总览。
|
|
2702
2767
|
renderOverviewDesktop()
|
|
2703
|
-
const cur =
|
|
2768
|
+
const cur = currentServerEntry()
|
|
2704
2769
|
const group = cur ? cur.group : state.activeGroup
|
|
2705
2770
|
const label = cur ? (cur.note || cur.url) : (state.server || t('ds.origin'))
|
|
2706
2771
|
const serverText = t('ds.currentServer', { group, url: label })
|
|
@@ -2901,6 +2966,11 @@ function bindUi() {
|
|
|
2901
2966
|
if (bar && bar.dataset.tip) showTip(bar.dataset.tip, bar.getBoundingClientRect())
|
|
2902
2967
|
})
|
|
2903
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)
|
|
2904
2974
|
|
|
2905
2975
|
$('view-settings').addEventListener('click', (e) => {
|
|
2906
2976
|
const group = e.target.closest('[data-settings-group]')
|
|
@@ -2946,11 +3016,23 @@ function bindUi() {
|
|
|
2946
3016
|
}
|
|
2947
3017
|
|
|
2948
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
|
+
}
|
|
2949
3025
|
loadServers()
|
|
2950
3026
|
renderServers()
|
|
2951
3027
|
showView('view-overview')
|
|
2952
|
-
|
|
2953
|
-
|
|
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
|
+
}
|
|
2954
3036
|
if (!state.token) {
|
|
2955
3037
|
const input = prompt(t('ds.tokenTitle'))
|
|
2956
3038
|
if (input && input.trim()) { state.token = input.trim(); LS.set('token', state.token) }
|
package/public/index.html
CHANGED
|
@@ -229,12 +229,47 @@
|
|
|
229
229
|
</section>
|
|
230
230
|
|
|
231
231
|
<!-- Token 统计 -->
|
|
232
|
-
<section id="view-stats" class="view hidden">
|
|
233
|
-
<
|
|
232
|
+
<section id="view-stats" class="view hidden stats-view" aria-labelledby="stats-page-title">
|
|
233
|
+
<header class="stats-page-head">
|
|
234
|
+
<div class="stats-page-copy">
|
|
235
|
+
<div class="stats-eyebrow" data-i18n="statsPage.eyebrow">USAGE OVERVIEW</div>
|
|
236
|
+
<h1 id="stats-page-title" data-i18n="statsPage.title">Token 统计</h1>
|
|
237
|
+
<p class="stats-page-subtitle" data-i18n="statsPage.subtitle">按日查看 Token 用量与费用估算</p>
|
|
238
|
+
</div>
|
|
239
|
+
<div class="stats-date-wrap">
|
|
240
|
+
<span class="stats-date-label" data-i18n="statsPage.latest">最新日期</span>
|
|
241
|
+
<span id="stats-sub" class="stats-date">—</span>
|
|
242
|
+
</div>
|
|
243
|
+
</header>
|
|
244
|
+
<div class="stats-toolbar">
|
|
245
|
+
<span class="stats-toolbar-label" data-i18n="statsPage.chartMetric">图表指标</span>
|
|
246
|
+
<div id="stats-mode-picker" class="stats-mode-picker" role="group" data-i18n-aria="statsPage.chartMetric">
|
|
247
|
+
<button type="button" class="stats-mode-btn active" data-stats-mode="token" aria-pressed="true" data-i18n="statsPage.modeTokens">Token 数</button>
|
|
248
|
+
<button type="button" class="stats-mode-btn" data-stats-mode="cost" aria-pressed="false" data-i18n="statsPage.modeCost">费用(官方)</button>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
234
251
|
<div class="stats-cards" id="stats-cards"></div>
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
<section class="stats-panel" data-stats-panel="cost" aria-labelledby="stats-cost-title">
|
|
253
|
+
<div class="stats-panel-head">
|
|
254
|
+
<div>
|
|
255
|
+
<div class="stats-panel-kicker" data-i18n="statsPage.costKicker">ESTIMATED COST</div>
|
|
256
|
+
<h2 id="stats-cost-title" class="stats-panel-title" data-i18n="statsPage.costTrend">费用趋势</h2>
|
|
257
|
+
</div>
|
|
258
|
+
<div id="stats-legend" class="stats-legend"></div>
|
|
259
|
+
</div>
|
|
260
|
+
<div id="stats-chart" class="stats-chart" role="list" aria-live="polite" data-i18n-aria="statsPage.costChartLabel"></div>
|
|
261
|
+
</section>
|
|
262
|
+
<section class="stats-panel" data-stats-panel="token" aria-labelledby="stats-token-title">
|
|
263
|
+
<div class="stats-panel-head">
|
|
264
|
+
<div>
|
|
265
|
+
<div class="stats-panel-kicker" data-i18n="statsPage.tokenKicker">TOKEN VOLUME</div>
|
|
266
|
+
<h2 id="stats-token-title" class="stats-panel-title" data-i18n="statsPage.tokenTrend">Token 量趋势</h2>
|
|
267
|
+
</div>
|
|
268
|
+
<div id="stats-token-legend" class="stats-legend"></div>
|
|
269
|
+
</div>
|
|
270
|
+
<div id="stats-token-chart" class="stats-chart" role="list" aria-live="polite" data-i18n-aria="statsPage.tokenChartLabel"></div>
|
|
271
|
+
</section>
|
|
272
|
+
<p id="stats-note" class="stats-note"></p>
|
|
238
273
|
</section>
|
|
239
274
|
|
|
240
275
|
<!-- 设置 -->
|
|
@@ -877,12 +912,14 @@
|
|
|
877
912
|
'overview.action.openSession': '打开最近会话', 'overview.action.newSession': '新建会话', 'overview.action.attention': '处理待办', 'overview.action.connect': '去设置连接', 'overview.action.refresh': '重新检查连接',
|
|
878
913
|
'overview.communityEyebrow': 'COMMUNITY BOARD', 'overview.communityTitle': '公告与投票', 'overview.communityHistory': '历史公告', 'overview.communityEmpty': '暂无未读公告',
|
|
879
914
|
'overview.communityPoll': '投票', 'overview.communityNotice': '公告', 'overview.communityNew': '新', 'overview.communityVote': '参与投票', 'overview.communityView': '查看公告', 'overview.communityVoted': '已参与',
|
|
880
|
-
'statsPage.title': 'Token 统计',
|
|
881
|
-
'statsPage.
|
|
915
|
+
'statsPage.eyebrow': '使用概览', 'statsPage.title': 'Token 统计', 'statsPage.subtitle': '按日查看 Token 用量与费用估算', 'statsPage.latest': '最新日期', 'statsPage.chartMetric': '图表指标', 'statsPage.modeTokens': 'Token 数', 'statsPage.modeCost': '费用(官方)',
|
|
916
|
+
'statsPage.costKicker': 'ESTIMATED COST', 'statsPage.tokenKicker': 'TOKEN VOLUME', 'statsPage.costChartLabel': '近 7 天费用趋势', 'statsPage.tokenChartLabel': '近 7 天 Token 用量趋势',
|
|
917
|
+
'statsPage.today': '今日', 'statsPage.tokenUnit': 'Tokens', 'statsPage.breakdown': '今日用量构成', 'statsPage.estimated': '估算费用', 'statsPage.peakShareLabel': '高峰费用占比 {n}%',
|
|
918
|
+
'statsPage.todayTokens': '今日 Token', 'statsPage.todayCost': '今日费用', 'statsPage.peakShare': '高峰占比', 'statsPage.costTrend': '费用趋势', 'statsPage.tokenTrend': 'Token 量趋势', 'statsPage.tokenTotal': 'Token 总量',
|
|
882
919
|
'statsPage.input': '未缓存输入', 'statsPage.cacheRead': '缓存命中', 'statsPage.cacheWrite': '缓存写入', 'statsPage.output': '输出',
|
|
883
920
|
'statsPage.peak': '高峰', 'statsPage.off': '空闲', 'statsPage.days': '近 {n} 天',
|
|
884
921
|
'statsPage.gatewayDown': '统计需要网关运行', 'statsPage.empty': '暂无统计,产生会话后自动聚合',
|
|
885
|
-
'statsPage.note': '
|
|
922
|
+
'statsPage.note': '注:Token 量按 DSH usage 事件记录,第三方 provider 需提供 usage 才能计入;费用仅对已知 DeepSeek 模型估算,与官网账单可能有出入,一切以官网为准。统计自 2026-08-17 定价生效日起。',
|
|
886
923
|
'home.newSession': '+ 新会话', 'home.newWorkspace': '+ 工作区', 'home.empty': '暂无会话', 'home.createFailed': '新建会话失败', 'home.created': '会话已创建',
|
|
887
924
|
'wb.unbound': '未绑定工作台', 'wb.bound': '绑定 {title}', 'wb.noProjects': '暂无项目', 'wb.noSessions': '暂无会话', 'wb.newSession': '新会话', 'wb.orderSaved': '顺序已保存到当前设备', 'wb.flatHidden': '工作台会话已收起({n} 个)', 'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------',
|
|
888
925
|
'workspace.createTitle': '新建工作区', 'workspace.createDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'workspace.parent': '父目录', 'workspace.namePlaceholder': '例如:my-project', 'workspace.cancel': '取消', 'workspace.create': '创建并打开', 'workspace.nameRequired': '请输入工作区名称', 'workspace.exists': '该目录已存在', 'workspace.invalidName': '名称不能包含路径分隔符', 'workspace.createFailed': '创建工作区失败:{msg}', 'workspace.created': '工作区已创建', 'workspace.createdNoSession': '工作区已创建,但新会话未能打开',
|
|
@@ -903,13 +940,13 @@
|
|
|
903
940
|
'speed.switched': '已切换到最快服务器:{url}({ms}ms)', 'speed.alreadyBest': '当前已是最快:{url}({ms}ms)',
|
|
904
941
|
'speed.allDown': '全部服务器不可达',
|
|
905
942
|
'servers.empty': '未添加备用服务器 · 默认使用当前页面地址', 'servers.untested': '未测速',
|
|
906
|
-
'servers.current': ' · 当前', 'servers.unreachable': '不可达', 'servers.delete': '删除服务器',
|
|
943
|
+
'servers.current': ' · 当前', 'servers.unreachable': '不可达', 'servers.delete': '删除服务器', 'servers.tokenSet': '令牌已配置', 'servers.tokenMissing': '未配置令牌',
|
|
907
944
|
'servers.currentDesc': '当前: {url}', 'servers.notSet': '未设置服务器', 'servers.defaultDesc': '默认 = 当前页面地址',
|
|
908
945
|
'servers.needAddress': '请输入服务器地址', 'servers.badProtocol': '地址需以 http:// 或 https:// 开头',
|
|
909
|
-
'servers.duplicate': '
|
|
946
|
+
'servers.duplicate': '相同地址和令牌已在列表中', 'servers.added': '已添加服务器', 'servers.removedActive': '已删除当前服务器,重新测速…',
|
|
910
947
|
'servers.currentDescGroup': '当前:{group} · {url}({ms})',
|
|
911
948
|
'servers.edit': '编辑', 'servers.manualSelected': '已切换至:{url}',
|
|
912
|
-
'servers.promptNote': '服务器备注(可留空):', 'servers.promptEditNote': '编辑备注({url}):',
|
|
949
|
+
'servers.promptNote': '服务器备注(可留空):', 'servers.promptEditNote': '编辑备注({url}):', 'servers.promptEditToken': '编辑服务器令牌(留空保持不变):',
|
|
913
950
|
'servers.promptEditUrl': '编辑服务器地址:', 'servers.promptEditGroup': '所属组:', 'servers.edited': '服务器已更新',
|
|
914
951
|
'speed.switchedOrigin': '已切换到当前页面(同源)', 'speed.manualUsing': '手动模式:{url}({ms})',
|
|
915
952
|
'groups.current': '当前组', 'groups.add': '+ 组', 'groups.noServer': '组内暂无服务器',
|
|
@@ -1004,7 +1041,7 @@
|
|
|
1004
1041
|
'settings.groupGeneral': '通用', 'settings.groupGeneralDesc': '显示工具调用、预设提示词',
|
|
1005
1042
|
'settings.groupModel': '模型', 'settings.groupModelDesc': '提供方、API 密钥和模型目录',
|
|
1006
1043
|
'settings.groupTests': '功能测试', 'settings.groupTestsDesc': '验证 Android 和 DSH 原生能力',
|
|
1007
|
-
'settings.groupServers': '服务器', 'settings.groupServersDesc': '
|
|
1044
|
+
'settings.groupServers': '服务器', 'settings.groupServersDesc': '服务器地址、令牌与扫码连接',
|
|
1008
1045
|
'settings.groupNotify': '通知', 'settings.groupNotifyDesc': '通知、提醒、后台轮询',
|
|
1009
1046
|
'settings.groupTheme': '皮肤', 'settings.groupThemeDesc': '配色主题',
|
|
1010
1047
|
'settings.groupAbout': '关于', 'settings.groupAboutDesc': 'DSH 状态、检查更新',
|
|
@@ -1104,12 +1141,14 @@
|
|
|
1104
1141
|
'overview.action.openSession': 'Open recent session', 'overview.action.newSession': 'New session', 'overview.action.attention': 'Review pending', 'overview.action.connect': 'Set up connection', 'overview.action.refresh': 'Check connection',
|
|
1105
1142
|
'overview.communityEyebrow': 'COMMUNITY BOARD', 'overview.communityTitle': 'Announcements & polls', 'overview.communityHistory': 'History', 'overview.communityEmpty': 'No unread announcements',
|
|
1106
1143
|
'overview.communityPoll': 'Poll', 'overview.communityNotice': 'Notice', 'overview.communityNew': 'New', 'overview.communityVote': 'Vote now', 'overview.communityView': 'View notice', 'overview.communityVoted': 'Voted',
|
|
1107
|
-
'statsPage.title': 'Token stats',
|
|
1108
|
-
'statsPage.
|
|
1144
|
+
'statsPage.eyebrow': 'USAGE OVERVIEW', 'statsPage.title': 'Token stats', 'statsPage.subtitle': 'Daily token volume and estimated costs', 'statsPage.latest': 'Latest date', 'statsPage.chartMetric': 'Chart metric', 'statsPage.modeTokens': 'Token count', 'statsPage.modeCost': 'Cost (official)',
|
|
1145
|
+
'statsPage.costKicker': 'ESTIMATED COST', 'statsPage.tokenKicker': 'TOKEN VOLUME', 'statsPage.costChartLabel': 'Cost trend for the last 7 days', 'statsPage.tokenChartLabel': 'Token volume trend for the last 7 days',
|
|
1146
|
+
'statsPage.today': 'Today', 'statsPage.tokenUnit': 'Tokens', 'statsPage.breakdown': 'Today’s usage breakdown', 'statsPage.estimated': 'Estimated cost', 'statsPage.peakShareLabel': 'Peak cost share {n}%',
|
|
1147
|
+
'statsPage.todayTokens': 'Tokens today', 'statsPage.todayCost': 'Cost today', 'statsPage.peakShare': 'Peak share', 'statsPage.costTrend': 'Cost trend', 'statsPage.tokenTrend': 'Token volume trend', 'statsPage.tokenTotal': 'Total tokens',
|
|
1109
1148
|
'statsPage.input': 'Uncached input', 'statsPage.cacheRead': 'Cache read', 'statsPage.cacheWrite': 'Cache write', 'statsPage.output': 'Output',
|
|
1110
1149
|
'statsPage.peak': 'Peak', 'statsPage.off': 'Off-peak', 'statsPage.days': 'Last {n} days',
|
|
1111
1150
|
'statsPage.gatewayDown': 'Stats require the gateway', 'statsPage.empty': 'No stats yet — they aggregate as sessions happen',
|
|
1112
|
-
'statsPage.note': 'Note:
|
|
1151
|
+
'statsPage.note': 'Note: token volume uses DSH usage events; a third-party provider must expose usage to be counted. Costs are estimates for known DeepSeek models and may differ from the official bill; always defer to deepseek.com. Stats start from the 2026-08-17 pricing date.',
|
|
1113
1152
|
'home.newSession': '+ New session', 'home.newWorkspace': '+ Workspace', 'home.empty': 'No sessions yet', 'home.createFailed': 'Failed to create session', 'home.created': 'Session created',
|
|
1114
1153
|
'wb.unbound': 'No workbench bound', 'wb.bound': 'Bound {title}', 'wb.noProjects': 'No projects', 'wb.noSessions': 'No sessions', 'wb.newSession': 'New session', 'wb.orderSaved': 'Order saved on this device', 'wb.flatHidden': 'Workbench sessions folded ({n})', 'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------',
|
|
1115
1154
|
'workspace.createTitle': 'New workspace', 'workspace.createDesc': 'Create a folder in the current file directory and open a new session there.', 'workspace.parent': 'Parent folder', 'workspace.namePlaceholder': 'For example: my-project', 'workspace.cancel': 'Cancel', 'workspace.create': 'Create & open', 'workspace.nameRequired': 'Enter a workspace name', 'workspace.exists': 'That folder already exists', 'workspace.invalidName': 'The name cannot contain path separators', 'workspace.createFailed': 'Could not create workspace: {msg}', 'workspace.created': 'Workspace created', 'workspace.createdNoSession': 'Workspace created, but the new session could not be opened',
|
|
@@ -1130,13 +1169,13 @@
|
|
|
1130
1169
|
'speed.switched': 'Switched to fastest server: {url} ({ms}ms)', 'speed.alreadyBest': 'Already on fastest: {url} ({ms}ms)',
|
|
1131
1170
|
'speed.allDown': 'All servers unreachable',
|
|
1132
1171
|
'servers.empty': 'No backup servers · using the current page URL', 'servers.untested': 'Untested',
|
|
1133
|
-
'servers.current': ' · current', 'servers.unreachable': 'Unreachable', 'servers.delete': 'Remove server',
|
|
1172
|
+
'servers.current': ' · current', 'servers.unreachable': 'Unreachable', 'servers.delete': 'Remove server', 'servers.tokenSet': 'Token set', 'servers.tokenMissing': 'No token',
|
|
1134
1173
|
'servers.currentDesc': 'Current: {url}', 'servers.notSet': 'No server set', 'servers.defaultDesc': 'Default = current page URL',
|
|
1135
1174
|
'servers.needAddress': 'Enter a server address', 'servers.badProtocol': 'Address must start with http:// or https://',
|
|
1136
|
-
'servers.duplicate': '
|
|
1175
|
+
'servers.duplicate': 'The same address and token is already in the list', 'servers.added': 'Server added', 'servers.removedActive': 'Removed active server, re-testing…',
|
|
1137
1176
|
'servers.currentDescGroup': 'Current: {group} · {url} ({ms})',
|
|
1138
1177
|
'servers.edit': 'Edit', 'servers.manualSelected': 'Switched to: {url}',
|
|
1139
|
-
'servers.promptNote': 'Server note (optional):', 'servers.promptEditNote': 'Edit note ({url}):',
|
|
1178
|
+
'servers.promptNote': 'Server note (optional):', 'servers.promptEditNote': 'Edit note ({url}):', 'servers.promptEditToken': 'Edit server token (blank keeps the current token):',
|
|
1140
1179
|
'servers.promptEditUrl': 'Edit server URL:', 'servers.promptEditGroup': 'Group:', 'servers.edited': 'Server updated',
|
|
1141
1180
|
'speed.switchedOrigin': 'Switched to current page (same origin)', 'speed.manualUsing': 'Manual mode: {url} ({ms})',
|
|
1142
1181
|
'groups.current': 'Current group', 'groups.add': '+ Group', 'groups.noServer': 'No servers in this group',
|
|
@@ -1231,7 +1270,7 @@
|
|
|
1231
1270
|
'settings.groupGeneral': 'General', 'settings.groupGeneralDesc': 'Tool calls, prompt presets',
|
|
1232
1271
|
'settings.groupModel': 'Models', 'settings.groupModelDesc': 'Providers, API keys, and model catalogs',
|
|
1233
1272
|
'settings.groupTests': 'Feature tests', 'settings.groupTestsDesc': 'Check Android and DSH native capabilities',
|
|
1234
|
-
'settings.groupServers': 'Servers', 'settings.groupServersDesc': 'Server address, scan to pair',
|
|
1273
|
+
'settings.groupServers': 'Servers', 'settings.groupServersDesc': 'Server address, token, and scan to pair',
|
|
1235
1274
|
'settings.groupNotify': 'Notifications', 'settings.groupNotifyDesc': 'Notifications, reminders, background polling',
|
|
1236
1275
|
'settings.groupTheme': 'Theme', 'settings.groupThemeDesc': 'Color themes',
|
|
1237
1276
|
'settings.groupAbout': 'About', 'settings.groupAboutDesc': 'DSH status, updates',
|