dsh-remote-plugin 0.6.6 → 0.6.8
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/README.en.md +31 -25
- package/README.md +30 -24
- package/apk/dsh-remote.apk +0 -0
- package/client.js +11 -9
- package/gateway.cjs +119 -1
- package/index.mjs +10 -1
- package/package.json +1 -1
- package/public/admin.html +103 -7
- package/public/admin.js +24 -0
- package/public/announcements.json +12 -0
- package/public/app.js +850 -57
- package/public/desktop/desktop.css +170 -20
- package/public/desktop/desktop.html +102 -13
- package/public/desktop/desktop.js +422 -26
- package/public/desktop/i18n.js +33 -1
- package/public/index.html +200 -40
- package/public/plugin.html +151 -0
- package/public/plugin.js +179 -0
- package/public/styles.css +220 -15
- package/public/theme-vars.css +9 -0
- package/public/update.json +12 -4
- package/public/version.json +1 -1
|
@@ -52,6 +52,7 @@ const state = {
|
|
|
52
52
|
sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
|
|
53
53
|
byId: new Map(),
|
|
54
54
|
current: null,
|
|
55
|
+
hostInfo: null,
|
|
55
56
|
history: { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity },
|
|
56
57
|
approvals: [],
|
|
57
58
|
questions: [],
|
|
@@ -62,6 +63,8 @@ const state = {
|
|
|
62
63
|
pollSeq: { mux: 0, host: 0 },
|
|
63
64
|
fs: { path: null, initial: null, loaded: false },
|
|
64
65
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
66
|
+
wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
|
|
67
|
+
archivedIds: [],
|
|
65
68
|
view: 'sessions'
|
|
66
69
|
}
|
|
67
70
|
const streams = {}
|
|
@@ -1053,7 +1056,7 @@ function onHostFrame(full) {
|
|
|
1053
1056
|
if (['host/session-added', 'host/session-removed', 'host/workspace-changed', 'host/workspace-removed', 'host/workspace-order-changed', 'host/archived-sessions-changed'].includes(f.type)) refreshSessions()
|
|
1054
1057
|
if (f.type === 'host/session-status') {
|
|
1055
1058
|
const s = state.byId.get(f.sessionId)
|
|
1056
|
-
if (s) { s.running = f.running; if (state.current === f.sessionId) renderSessions() }
|
|
1059
|
+
if (s) { s.running = f.running; if (state.current === f.sessionId) renderSessions(); renderOverviewDesktop() }
|
|
1057
1060
|
}
|
|
1058
1061
|
}
|
|
1059
1062
|
function applyProjection(sessionId, key, value, seq) {
|
|
@@ -1098,10 +1101,12 @@ function onSessionEvent(sessionId, event) {
|
|
|
1098
1101
|
/* ---------------- 会话 ---------------- */
|
|
1099
1102
|
async function refreshSessions() {
|
|
1100
1103
|
const v = await safeRpc('session.list', {}, '')
|
|
1101
|
-
if (!v) { renderSessions(); return }
|
|
1104
|
+
if (!v) { renderSessions(); renderOverviewDesktop(); return }
|
|
1102
1105
|
state.sessions = v.items || []
|
|
1103
1106
|
state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
|
|
1104
1107
|
renderSessions()
|
|
1108
|
+
scheduleWorkbenchRefresh()
|
|
1109
|
+
renderOverviewDesktop()
|
|
1105
1110
|
}
|
|
1106
1111
|
function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
|
|
1107
1112
|
function sessionWorkspaceLabel(s) {
|
|
@@ -1128,31 +1133,53 @@ function sortedSessions() {
|
|
|
1128
1133
|
return items.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1129
1134
|
}
|
|
1130
1135
|
function renderSessions() {
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
if (state.
|
|
1138
|
-
|
|
1139
|
-
|
|
1136
|
+
const allItems = sortedSessions()
|
|
1137
|
+
const wbIds = new Set()
|
|
1138
|
+
if (state.wb.bound && state.wb.projects) for (const w of state.wb.projects) for (const id of (w.sessionIds || [])) wbIds.add(id)
|
|
1139
|
+
const root = state.wb.bound ? state.wb.path : ''
|
|
1140
|
+
const archivedSet = new Set(state.archivedIds || [])
|
|
1141
|
+
const visible = allItems.filter(s => {
|
|
1142
|
+
if (!state.wb.bound) return true
|
|
1143
|
+
if (archivedSet.has(s.sessionId)) return true
|
|
1144
|
+
return !(wbIds.has(s.sessionId) || wbStrictInside(s.cwd, root))
|
|
1145
|
+
})
|
|
1146
|
+
const archived = visible.filter(s => archivedSet.has(s.sessionId))
|
|
1147
|
+
const main = visible.filter(s => !archivedSet.has(s.sessionId))
|
|
1148
|
+
const showArchived = LS.get('dsShowArchivedV1', '0') === '1'
|
|
1149
|
+
const renderItems = (items) => {
|
|
1150
|
+
let lastWorkspace = null
|
|
1151
|
+
const rows = []
|
|
1152
|
+
for (const s of items) {
|
|
1153
|
+
const workspace = sessionWorkspaceLabel(s)
|
|
1154
|
+
const workspaceName = workspaceDisplayName(workspace)
|
|
1155
|
+
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1156
|
+
rows.push(`<div class="ds-session-group" title="${esc(workspace)}"><span class="ds-session-group-icon" aria-hidden="true">⌂</span><span class="ds-session-group-name">${esc(workspaceName)}</span></div>`)
|
|
1157
|
+
lastWorkspace = workspace
|
|
1158
|
+
}
|
|
1159
|
+
const title = titleOf(s)
|
|
1160
|
+
rows.push(`<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1161
|
+
<span class="ds-session-title">${esc(title)}</span>
|
|
1162
|
+
<span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
|
|
1163
|
+
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(s.updatedAt)}</span>
|
|
1164
|
+
</button>`)
|
|
1140
1165
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
</button>`)
|
|
1147
|
-
}
|
|
1148
|
-
const html = rows.join('') || `<div class="ds-empty">${t('ds.sessionsEmpty')}</div>`
|
|
1166
|
+
return rows.join('')
|
|
1167
|
+
}
|
|
1168
|
+
const divider = archived.length ? `<button class="ds-archived-toggle" type="button" data-archived-toggle>${esc(showArchived ? t('wb.archivedShown') : t('wb.archivedHidden'))}</button>` : ''
|
|
1169
|
+
const hiddenByWorkbench = allItems.length - visible.length
|
|
1170
|
+
const html = renderItems(main) + divider + (showArchived ? renderItems(archived) : '') || `<div class="ds-empty">${esc(hiddenByWorkbench ? t('wb.flatHidden', { n: hiddenByWorkbench }) : t('ds.sessionsEmpty'))}</div>`
|
|
1149
1171
|
$('session-list').innerHTML = html
|
|
1150
1172
|
$('mobile-session-list').innerHTML = html
|
|
1151
1173
|
$('session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1152
1174
|
$('mobile-session-list').classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1153
1175
|
const sort = $('session-sort')
|
|
1154
1176
|
if (sort) sort.value = state.sessionSort
|
|
1177
|
+
document.querySelectorAll('[data-archived-toggle]').forEach(b => b.addEventListener('click', () => {
|
|
1178
|
+
LS.set('dsShowArchivedV1', LS.get('dsShowArchivedV1', '0') === '1' ? '0' : '1')
|
|
1179
|
+
renderSessions()
|
|
1180
|
+
}))
|
|
1155
1181
|
document.querySelectorAll('[data-id]').forEach(b => b.addEventListener('click', () => openSession(b.dataset.id)))
|
|
1182
|
+
renderWorkbench()
|
|
1156
1183
|
}
|
|
1157
1184
|
|
|
1158
1185
|
async function openSession(id) {
|
|
@@ -1236,7 +1263,7 @@ function eventHtml(entry) {
|
|
|
1236
1263
|
const sysText = type === 'user/message' ? systemReminderText(blocks) : ''
|
|
1237
1264
|
if (sysText) {
|
|
1238
1265
|
const shown = sysText.length > 400 ? sysText.slice(0, 400) + '…' : sysText
|
|
1239
|
-
return `<details class="event ds-tool"><summary>${esc(t('ds.eventSystemReminder'))}</summary><pre>${esc(shown)}</pre></details>`
|
|
1266
|
+
return `<details class="event ds-tool ds-event-detail"><summary>${esc(t('ds.eventSystemReminder'))}</summary><pre>${esc(shown)}</pre></details>`
|
|
1240
1267
|
}
|
|
1241
1268
|
const text = blocks.map(blockHtml).join('')
|
|
1242
1269
|
return `<div class="ds-msg ${esc(role)}"><div class="role">${esc(role === 'user' ? t('ds.role.me') : t('ds.role.dsh'))}</div>${text || '<span style="opacity:.6">…</span>'}</div>`
|
|
@@ -1436,6 +1463,7 @@ function renderNotifStack() {
|
|
|
1436
1463
|
stack.querySelectorAll('.ds-notif-card').forEach(card => card.addEventListener('keydown', (e) => {
|
|
1437
1464
|
if (e.key === 'Escape') toast(t('ds.ignored'), 'ok')
|
|
1438
1465
|
}))
|
|
1466
|
+
renderOverviewDesktop()
|
|
1439
1467
|
}
|
|
1440
1468
|
async function approveApproval(id, allow) {
|
|
1441
1469
|
const a = state.approvals.find(x => x.approvalId === id)
|
|
@@ -1573,7 +1601,7 @@ async function loadFs(dir, silent) {
|
|
|
1573
1601
|
$('fs-path').textContent = data.path
|
|
1574
1602
|
$('fs-list').innerHTML = (data.entries || []).map(e => `
|
|
1575
1603
|
<div class="ds-fs-row" data-fs-path="${esc(e.path)}" data-fs-dir="${e.type === 'dir' ? '1' : '0'}">
|
|
1576
|
-
<span>${e.type === 'dir'
|
|
1604
|
+
<span class="ds-fs-type">${desktopFsIconSvg(e.type === 'dir')}</span>
|
|
1577
1605
|
<span class="ds-fs-name">${esc(e.name)}</span>
|
|
1578
1606
|
<span class="ds-fs-size">${e.type === 'dir' ? '' : fmtSize(e.size)}</span>
|
|
1579
1607
|
</div>`).join('') || `<div class="ds-empty">${t('ds.fsEmpty')}</div>`
|
|
@@ -1586,12 +1614,247 @@ async function loadFs(dir, silent) {
|
|
|
1586
1614
|
$('fs-list').innerHTML = `<div class="ds-empty">${esc(e.message || t('ds.toastConnFailed'))}</div>`
|
|
1587
1615
|
}
|
|
1588
1616
|
}
|
|
1617
|
+
|
|
1618
|
+
function desktopFsIconSvg(isDir) {
|
|
1619
|
+
return isDir
|
|
1620
|
+
? '<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>'
|
|
1621
|
+
: '<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>'
|
|
1622
|
+
}
|
|
1589
1623
|
function fsUp() {
|
|
1590
1624
|
if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) {
|
|
1591
1625
|
loadFs(fsParent(state.fs.path))
|
|
1592
1626
|
}
|
|
1593
1627
|
}
|
|
1594
1628
|
|
|
1629
|
+
/* ---------------- 工作台绑定 / 项目会话 ---------------- */
|
|
1630
|
+
function wbPathKey(p) {
|
|
1631
|
+
let value = String(p || '').replace(/\\/g, '/').replace(/\/+/g, '/')
|
|
1632
|
+
if (value.length > 1) value = value.replace(/\/+$/, '')
|
|
1633
|
+
const windows = /^[A-Za-z]:\//.test(value) || /Windows/i.test(navigator.platform || navigator.userAgent || '')
|
|
1634
|
+
return windows ? value.toLowerCase() : value
|
|
1635
|
+
}
|
|
1636
|
+
function wbBaseName(p) {
|
|
1637
|
+
const value = String(p || '').replace(/[\\/]+$/, '')
|
|
1638
|
+
return value.split(/[\\/]/).pop() || value
|
|
1639
|
+
}
|
|
1640
|
+
function wbStrictInside(pathValue, rootValue) {
|
|
1641
|
+
const pathKey = wbPathKey(pathValue)
|
|
1642
|
+
const rootKey = wbPathKey(rootValue)
|
|
1643
|
+
if (!pathKey || !rootKey || pathKey === rootKey) return false
|
|
1644
|
+
return pathKey.startsWith(rootKey.endsWith('/') ? rootKey : rootKey + '/')
|
|
1645
|
+
}
|
|
1646
|
+
function wbJoin(root, name) {
|
|
1647
|
+
const raw = String(root || '')
|
|
1648
|
+
const separator = raw.includes('\\') ? '\\' : '/'
|
|
1649
|
+
return raw.replace(/[\\/]+$/, '') + separator + String(name || '')
|
|
1650
|
+
}
|
|
1651
|
+
function wbFsParent(p) {
|
|
1652
|
+
if (!p) return null
|
|
1653
|
+
const raw = String(p)
|
|
1654
|
+
const separator = raw.includes('\\') ? '\\' : '/'
|
|
1655
|
+
const index = raw.lastIndexOf(separator)
|
|
1656
|
+
if (index <= 0 || /^[A-Za-z]:$/.test(raw.slice(0, index))) return null
|
|
1657
|
+
return raw.slice(0, index)
|
|
1658
|
+
}
|
|
1659
|
+
async function wbGateway(method, pathname, body) {
|
|
1660
|
+
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' } }
|
|
1661
|
+
if (body !== undefined) {
|
|
1662
|
+
options.headers['content-type'] = 'application/json'
|
|
1663
|
+
options.body = JSON.stringify(body)
|
|
1664
|
+
}
|
|
1665
|
+
const res = await fetch(apiUrl(pathname), options)
|
|
1666
|
+
if (res.status === 401) throw new Error('AUTH')
|
|
1667
|
+
const data = await res.json().catch(() => ({}))
|
|
1668
|
+
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
1669
|
+
return data
|
|
1670
|
+
}
|
|
1671
|
+
async function refreshWorkbench({ silent = false } = {}) {
|
|
1672
|
+
if (!state.token) { renderWorkbench(); return }
|
|
1673
|
+
let wb = null
|
|
1674
|
+
try {
|
|
1675
|
+
wb = await wbGateway('GET', '/workbench')
|
|
1676
|
+
state.wb.apiMissing = false
|
|
1677
|
+
} catch (e) {
|
|
1678
|
+
if (e.message === 'AUTH') { toast(t('ds.toastAuth'), 'err'); return }
|
|
1679
|
+
if (!silent) toast(t('wb.loadFailed', { msg: e.message }), 'err')
|
|
1680
|
+
if (!state.wb.bound && /404/.test(e.message)) state.wb.apiMissing = true
|
|
1681
|
+
}
|
|
1682
|
+
const wl = await safeRpc('workspace.list', {}, '')
|
|
1683
|
+
state.archivedIds = wl && Array.isArray(wl.archivedSessionIds) ? wl.archivedSessionIds : []
|
|
1684
|
+
if (!wb) { renderWorkbench(); renderSessions(); return }
|
|
1685
|
+
if (!wb.bound) {
|
|
1686
|
+
state.wb = { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false }
|
|
1687
|
+
renderWorkbench()
|
|
1688
|
+
renderSessions()
|
|
1689
|
+
return
|
|
1690
|
+
}
|
|
1691
|
+
state.wb.bound = true
|
|
1692
|
+
state.wb.path = wb.path || ''
|
|
1693
|
+
state.wb.title = wb.title || ''
|
|
1694
|
+
if (!wl) { state.wb.projects = []; renderWorkbench(); renderSessions(); return }
|
|
1695
|
+
const items = Array.isArray(wl.items) ? wl.items.slice() : []
|
|
1696
|
+
try {
|
|
1697
|
+
const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
1698
|
+
if (listRes.ok) {
|
|
1699
|
+
const listData = await listRes.json().catch(() => ({}))
|
|
1700
|
+
if (Array.isArray(listData.entries)) {
|
|
1701
|
+
const diskDirs = new Set(listData.entries.filter(e => e.type === 'dir').map(e => wbPathKey(wbJoin(state.wb.path, e.name))))
|
|
1702
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
1703
|
+
if (!diskDirs.has(wbPathKey(items[i].path))) items.splice(i, 1)
|
|
1704
|
+
}
|
|
1705
|
+
const have = new Set(items.map(w => wbPathKey(w.path)))
|
|
1706
|
+
for (const entry of listData.entries) {
|
|
1707
|
+
if (entry.type !== 'dir') continue
|
|
1708
|
+
const projectPath = wbJoin(state.wb.path, entry.name)
|
|
1709
|
+
if (have.has(wbPathKey(projectPath))) continue
|
|
1710
|
+
try {
|
|
1711
|
+
const created = await rpc('workspace.create', { path: projectPath })
|
|
1712
|
+
if (created?.workspace) { items.push(created.workspace); have.add(wbPathKey(projectPath)) }
|
|
1713
|
+
} catch {}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
} catch {}
|
|
1718
|
+
state.wb.projects = items
|
|
1719
|
+
.filter(w => wbStrictInside(w.path, state.wb.path))
|
|
1720
|
+
.sort((a, b) => String(a.title || wbBaseName(a.path)).localeCompare(String(b.title || wbBaseName(b.path)), 'zh-CN', { numeric: true }))
|
|
1721
|
+
renderWorkbench()
|
|
1722
|
+
renderSessions()
|
|
1723
|
+
}
|
|
1724
|
+
function renderWorkbench() {
|
|
1725
|
+
const box = $('workbench-box')
|
|
1726
|
+
if (!box) return
|
|
1727
|
+
const unbound = $('wb-unbound')
|
|
1728
|
+
const bound = $('wb-bound')
|
|
1729
|
+
const hint = $('wb-api-hint')
|
|
1730
|
+
if (!state.wb.bound) {
|
|
1731
|
+
unbound.classList.remove('hidden')
|
|
1732
|
+
bound.classList.add('hidden')
|
|
1733
|
+
hint?.classList.toggle('hidden', !state.wb.apiMissing)
|
|
1734
|
+
return
|
|
1735
|
+
}
|
|
1736
|
+
unbound.classList.add('hidden')
|
|
1737
|
+
bound.classList.remove('hidden')
|
|
1738
|
+
$('wb-head-text').textContent = t('wb.bound', { title: state.wb.title || wbBaseName(state.wb.path) })
|
|
1739
|
+
$('wb-head').setAttribute('aria-expanded', state.wb.expanded ? 'true' : 'false')
|
|
1740
|
+
$('wb-caret').textContent = state.wb.expanded ? '▾' : '▸'
|
|
1741
|
+
const panel = $('wb-panel')
|
|
1742
|
+
panel.classList.toggle('hidden', !state.wb.expanded)
|
|
1743
|
+
if (!state.wb.expanded) return
|
|
1744
|
+
const projects = state.wb.projects || []
|
|
1745
|
+
const archivedSet = new Set(state.archivedIds || [])
|
|
1746
|
+
let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
|
|
1747
|
+
html += projects.length ? projects.map(w => {
|
|
1748
|
+
const id = String(w.workspaceId || '')
|
|
1749
|
+
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1750
|
+
const open = state.wb.open === id
|
|
1751
|
+
return `<div class="ds-wb-project ${open ? 'open' : ''}">
|
|
1752
|
+
<button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
|
|
1753
|
+
<span class="ds-wb-caret" aria-hidden="true">${open ? '▾' : '▸'}</span>
|
|
1754
|
+
<span class="ds-wb-project-title" title="${esc(w.path)}">${esc(w.title || wbBaseName(w.path) || short(id))}</span>
|
|
1755
|
+
<span class="ds-wb-project-count">${sessions.length}</span>
|
|
1756
|
+
</button>
|
|
1757
|
+
<div class="ds-wb-project-body ${open ? '' : 'hidden'}">
|
|
1758
|
+
<button type="button" class="ds-mini-btn ds-wb-new-session" data-wb-new="${esc(id)}">+ ${esc(t('wb.newSession'))}</button>
|
|
1759
|
+
${sessions.length ? sessions.map(s => `<button type="button" class="ds-wb-session ${state.current === s.sessionId ? 'current' : ''}" data-wb-session="${esc(s.sessionId)}"><span class="ds-wb-session-dot ${s.running ? 'running' : ''}"></span><span class="ds-wb-session-title">${esc(titleOf(s))}</span></button>`).join('') : `<div class="ds-wb-session-empty">${esc(t('wb.noSessions'))}</div>`}
|
|
1760
|
+
</div>
|
|
1761
|
+
</div>`
|
|
1762
|
+
}).join('') : `<div class="ds-wb-empty">${esc(t('wb.noProjects'))}</div>`
|
|
1763
|
+
html += `<button type="button" class="ds-mini-btn ds-wb-unbind-panel" data-wb-unbind-panel>${esc(t('wb.unbind'))}</button>`
|
|
1764
|
+
panel.innerHTML = html
|
|
1765
|
+
panel.querySelectorAll('[data-wb-head]').forEach(button => button.addEventListener('click', () => {
|
|
1766
|
+
state.wb.open = state.wb.open === button.dataset.wbHead ? null : button.dataset.wbHead
|
|
1767
|
+
renderWorkbench()
|
|
1768
|
+
}))
|
|
1769
|
+
panel.querySelectorAll('[data-wb-new]').forEach(button => button.addEventListener('click', async () => {
|
|
1770
|
+
const value = await safeRpc('session.create', { workspaceId: button.dataset.wbNew }, '')
|
|
1771
|
+
if (value?.sessionId) { await refreshSessions(); openSession(value.sessionId) }
|
|
1772
|
+
}))
|
|
1773
|
+
panel.querySelectorAll('[data-wb-session]').forEach(button => button.addEventListener('click', () => openSession(button.dataset.wbSession)))
|
|
1774
|
+
panel.querySelectorAll('[data-wb-unbind-panel]').forEach(button => button.addEventListener('click', unbindWorkbench))
|
|
1775
|
+
}
|
|
1776
|
+
const wbFs = { path: null, initial: null }
|
|
1777
|
+
function openWorkbenchModal() {
|
|
1778
|
+
$('modal-workbench').classList.remove('hidden')
|
|
1779
|
+
wbFs.path = null
|
|
1780
|
+
wbFs.initial = null
|
|
1781
|
+
wbFsLoad(null)
|
|
1782
|
+
setTimeout(() => $('wb-path-input').focus(), 50)
|
|
1783
|
+
}
|
|
1784
|
+
function closeWorkbenchModal() { $('modal-workbench').classList.add('hidden') }
|
|
1785
|
+
async function wbFsLoad(dir) {
|
|
1786
|
+
const box = $('wb-fs-list')
|
|
1787
|
+
const target = dir ?? wbFs.path ?? ''
|
|
1788
|
+
box.innerHTML = `<div class="ds-empty">${esc(t('ds.loading'))}</div>`
|
|
1789
|
+
$('wb-fs-path').textContent = target ? '…' + target.slice(-40) : '~'
|
|
1790
|
+
try {
|
|
1791
|
+
const res = await fetch(fsApiUrl('/list', target ? { path: target } : {}), { headers: fsHeaders() })
|
|
1792
|
+
const data = await res.json().catch(() => ({}))
|
|
1793
|
+
if (!res.ok || !Array.isArray(data.entries)) throw new Error(data.error || ('HTTP ' + res.status))
|
|
1794
|
+
wbFs.path = data.path
|
|
1795
|
+
if (!wbFs.initial) wbFs.initial = data.path
|
|
1796
|
+
$('wb-fs-path').textContent = data.path
|
|
1797
|
+
const dirs = (data.entries || []).filter(e => e.type === 'dir')
|
|
1798
|
+
box.innerHTML = dirs.length ? dirs.map(e => {
|
|
1799
|
+
const p = wbJoin(data.path, e.name)
|
|
1800
|
+
return `<div class="ds-wb-fs-row" data-wb-dir="${esc(p)}"><span class="ds-fs-type">${desktopFsIconSvg(true)}</span><span class="ds-wb-fs-name">${esc(e.name)}</span><button type="button" class="ds-btn ds-wb-select" data-wb-select="${esc(p)}">${esc(t('wb.selectDir'))}</button></div>`
|
|
1801
|
+
}).join('') : `<div class="ds-empty">${esc(t('wb.empty'))}</div>`
|
|
1802
|
+
box.querySelectorAll('[data-wb-dir]').forEach(row => row.addEventListener('click', e => { if (!e.target.closest('[data-wb-select]')) wbFsLoad(row.dataset.wbDir) }))
|
|
1803
|
+
box.querySelectorAll('[data-wb-select]').forEach(button => button.addEventListener('click', () => bindWorkbench(button.dataset.wbSelect)))
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
$('wb-fs-path').textContent = target || '~'
|
|
1806
|
+
box.innerHTML = `<div class="ds-empty">${esc(e.message || t('ds.toastConnFailed'))}</div>`
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function wbFsUp() {
|
|
1810
|
+
if (wbFs.path && wbFs.initial && wbFs.path !== wbFs.initial) {
|
|
1811
|
+
const parent = wbFsParent(wbFs.path)
|
|
1812
|
+
if (parent) wbFsLoad(parent)
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
async function bindWorkbench(rawPath) {
|
|
1816
|
+
const value = String(rawPath || '').trim()
|
|
1817
|
+
if (!value) return toast(t('wb.pathEmpty'), 'err')
|
|
1818
|
+
try {
|
|
1819
|
+
const wb = await wbGateway('POST', '/workbench/bind', { path: value })
|
|
1820
|
+
state.wb = { bound: true, path: wb.path || value, title: wb.title || '', expanded: true, projects: null, open: null, apiMissing: false }
|
|
1821
|
+
const paths = [state.wb.path]
|
|
1822
|
+
try {
|
|
1823
|
+
const res = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
|
|
1824
|
+
const data = await res.json().catch(() => ({}))
|
|
1825
|
+
for (const e of data.entries || []) if (e.type === 'dir') paths.push(wbJoin(state.wb.path, e.name))
|
|
1826
|
+
} catch {}
|
|
1827
|
+
for (const projectPath of paths) {
|
|
1828
|
+
try { await rpc('workspace.create', { path: projectPath }) } catch {}
|
|
1829
|
+
}
|
|
1830
|
+
closeWorkbenchModal()
|
|
1831
|
+
await refreshWorkbench({ silent: true })
|
|
1832
|
+
await refreshSessions()
|
|
1833
|
+
toast(t('wb.boundOk', { path: state.wb.path }), 'ok')
|
|
1834
|
+
} catch (e) {
|
|
1835
|
+
if (e.message === 'AUTH') return toast(t('ds.toastAuth'), 'err')
|
|
1836
|
+
toast(t('wb.bindFailed', { msg: e.message }), 'err')
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
async function unbindWorkbench() {
|
|
1840
|
+
if (!confirm(t('wb.unbindConfirm'))) return
|
|
1841
|
+
try {
|
|
1842
|
+
await wbGateway('POST', '/workbench/unbind')
|
|
1843
|
+
state.wb = { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false }
|
|
1844
|
+
renderWorkbench()
|
|
1845
|
+
renderSessions()
|
|
1846
|
+
toast(t('wb.unboundOk'), 'ok')
|
|
1847
|
+
} catch (e) {
|
|
1848
|
+
if (e.message === 'AUTH') return toast(t('ds.toastAuth'), 'err')
|
|
1849
|
+
toast(t('wb.unbindFailed', { msg: e.message }), 'err')
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
let wbRefreshTimer = null
|
|
1853
|
+
function scheduleWorkbenchRefresh() {
|
|
1854
|
+
clearTimeout(wbRefreshTimer)
|
|
1855
|
+
wbRefreshTimer = setTimeout(() => refreshWorkbench({ silent: true }), 400)
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1595
1858
|
/* ---------------- 统计 ---------------- */
|
|
1596
1859
|
function bucketTokens(b) { return (b.input || 0) + (b.cacheRead || 0) + (b.cacheWrite || 0) + (b.output || 0) }
|
|
1597
1860
|
let statsDrawerOpened = false
|
|
@@ -1656,13 +1919,98 @@ function renderStats(days) {
|
|
|
1656
1919
|
}
|
|
1657
1920
|
|
|
1658
1921
|
/* ---------------- 视图与连接状态 ---------------- */
|
|
1922
|
+
function renderOverviewDesktop() {
|
|
1923
|
+
const ring = $('ds-overview-pulse-ring')
|
|
1924
|
+
if (!ring) return
|
|
1925
|
+
const checks = {
|
|
1926
|
+
gateway: !!state.token && !!state.server,
|
|
1927
|
+
dsh: !!state.hostInfo,
|
|
1928
|
+
mux: !!state.streamsOk?.mux,
|
|
1929
|
+
host: !!state.streamsOk?.host
|
|
1930
|
+
}
|
|
1931
|
+
const online = Object.values(checks).filter(Boolean).length
|
|
1932
|
+
const status = online === 4 ? 'Nominal' : online > 0 ? 'Degraded' : 'Offline'
|
|
1933
|
+
const pulseCard = document.querySelector('.ds-overview-pulse-card')
|
|
1934
|
+
if (pulseCard) {
|
|
1935
|
+
pulseCard.classList.remove('status-nominal', 'status-degraded', 'status-offline')
|
|
1936
|
+
pulseCard.classList.add('status-' + status.toLowerCase())
|
|
1937
|
+
}
|
|
1938
|
+
ring.style.setProperty('--pulse-pct', `${online / 4 * 100}%`)
|
|
1939
|
+
$('ds-overview-health').textContent = online === 4 ? t('ds.live') : online ? `${online}/4` : t('ds.offlineCore')
|
|
1940
|
+
$('ds-overview-health-caption').textContent = online === 4 ? t('ds.allLinked') : online ? t('ds.components', { n: online }) : t('ds.offlineShort')
|
|
1941
|
+
$('ds-overview-status').textContent = t(`ds.system${status}`)
|
|
1942
|
+
$('ds-overview-status-desc').textContent = t('ds.components', { n: online })
|
|
1943
|
+
for (const [name, ok] of Object.entries(checks)) {
|
|
1944
|
+
const item = document.querySelector(`[data-ds-overview-link="${name}"]`)
|
|
1945
|
+
if (!item) continue
|
|
1946
|
+
item.classList.toggle('ok', ok)
|
|
1947
|
+
item.classList.toggle('off', !ok)
|
|
1948
|
+
const value = item.querySelector('b')
|
|
1949
|
+
if (value) value.textContent = ok ? t('ds.online') : t('ds.offlineShort')
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
const pending = [
|
|
1953
|
+
...state.approvals.map(a => ({ kind: 'approval', item: a })),
|
|
1954
|
+
...state.questions.map(q => ({ kind: 'question', item: q }))
|
|
1955
|
+
]
|
|
1956
|
+
$('ds-overview-attention-count').textContent = pending.length ? t('ds.pendingCount', { n: pending.length }) : '—'
|
|
1957
|
+
$('ds-overview-attention-list').innerHTML = pending.length ? pending.slice(0, 4).map(({ kind, item }) => {
|
|
1958
|
+
const title = titleOf(state.byId.get(item.sessionId))
|
|
1959
|
+
if (kind === 'approval') return `<div class="ds-overview-attention-item" data-ds-overview-approval="${esc(item.approvalId)}">
|
|
1960
|
+
<span class="ds-overview-mark">⌁</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(item.toolName || t('ds.toolDefault'))}</span><span class="ds-overview-item-desc">${esc(item.reason || t('ds.approvalReason', { reason: '' }))} · ${esc(title)}</span></span>
|
|
1961
|
+
<span class="ds-overview-actions"><button class="ds-btn allow" data-ds-overview-approve="1">${t('ds.allow')}</button><button class="ds-btn reject" data-ds-overview-approve="0">${t('ds.reject')}</button></span>
|
|
1962
|
+
</div>`
|
|
1963
|
+
return `<button type="button" class="ds-overview-attention-item question" data-ds-overview-question="${esc(item.rpcId)}">
|
|
1964
|
+
<span class="ds-overview-mark">?</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(item.questions?.[0]?.question || t('ds.questionNotify'))}</span><span class="ds-overview-item-desc">${esc(title)}</span></span><span class="ds-overview-arrow">›</span>
|
|
1965
|
+
</button>`
|
|
1966
|
+
}).join('') : `<div class="ds-overview-empty">${t('ds.nothingPending')}</div>`
|
|
1967
|
+
$('ds-overview-attention-list').querySelectorAll('[data-ds-overview-approve]').forEach(btn => btn.addEventListener('click', () => approveApproval(btn.closest('[data-ds-overview-approval]')?.dataset.dsOverviewApproval || '', btn.dataset.dsOverviewApprove === '1')))
|
|
1968
|
+
$('ds-overview-attention-list').querySelectorAll('[data-ds-overview-question]').forEach(btn => btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.dsOverviewQuestion))))
|
|
1969
|
+
|
|
1970
|
+
const running = state.sessions.filter(s => s.running).length
|
|
1971
|
+
const sessions = [...state.sessions].sort((a, b) => Number(b.running) - Number(a.running) || (new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))).slice(0, 6)
|
|
1972
|
+
const primary = $('ds-overview-primary-action')
|
|
1973
|
+
if (primary) {
|
|
1974
|
+
let action = 'new'
|
|
1975
|
+
let label = t('ds.action.newSession')
|
|
1976
|
+
let sessionId = ''
|
|
1977
|
+
if (!state.token) {
|
|
1978
|
+
action = 'settings'
|
|
1979
|
+
label = t('ds.action.connect')
|
|
1980
|
+
} else if (online > 0 && online < 4) {
|
|
1981
|
+
action = 'refresh'
|
|
1982
|
+
label = t('ds.action.refresh')
|
|
1983
|
+
} else if (pending.length) {
|
|
1984
|
+
action = 'attention'
|
|
1985
|
+
label = t('ds.action.attention')
|
|
1986
|
+
} else if (sessions.length) {
|
|
1987
|
+
action = 'session'
|
|
1988
|
+
sessionId = sessions[0].sessionId
|
|
1989
|
+
label = t('ds.action.openSession')
|
|
1990
|
+
}
|
|
1991
|
+
primary.textContent = label
|
|
1992
|
+
primary.dataset.dsOverviewAction = action
|
|
1993
|
+
primary.dataset.dsOverviewSession = sessionId
|
|
1994
|
+
}
|
|
1995
|
+
$('ds-overview-dsh-version').textContent = state.hostInfo?.version || '—'
|
|
1996
|
+
$('ds-overview-gateway-version').textContent = checks.gateway ? t('ds.online') : t('ds.offlineShort')
|
|
1997
|
+
$('ds-overview-active-sessions').textContent = String(running)
|
|
1998
|
+
$('ds-overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'ds.poll' : 'ds.liveWs') : '—'
|
|
1999
|
+
$('ds-overview-active-count').textContent = running ? t('ds.activeCount', { n: running }) : ''
|
|
2000
|
+
$('ds-overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="ds-overview-session-item ${s.running ? 'running' : ''}" data-ds-overview-session="${esc(s.sessionId)}">
|
|
2001
|
+
<span class="ds-overview-mark">${s.running ? '●' : '○'}</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(titleOf(s))}</span><span class="ds-overview-item-desc">${s.running ? esc(t('ds.running')) + ' · ' : ''}${esc(fmtTime(s.updatedAt))}</span></span><span class="ds-overview-arrow">›</span>
|
|
2002
|
+
</button>`).join('') : `<div class="ds-overview-empty">${t('ds.noSessions')}</div>`
|
|
2003
|
+
$('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1659
2006
|
function showView(id) {
|
|
1660
2007
|
state.view = id
|
|
1661
|
-
for (const v of ['view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
2008
|
+
for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
|
|
1662
2009
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.classList.toggle('active', b.dataset.view === id))
|
|
1663
|
-
const titles = { 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
|
|
2010
|
+
const titles = { 'view-overview': 'ds.overview', 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
|
|
1664
2011
|
if (id === 'view-chat') { const s = state.byId.get(state.current); $('ds-title').textContent = s ? titleOf(s) : t('ds.sessions') }
|
|
1665
2012
|
else $('ds-title').textContent = t(titles[id])
|
|
2013
|
+
if (id === 'view-overview') renderOverviewDesktop()
|
|
1666
2014
|
if (id === 'view-files' && !state.fs.loaded) loadFs(null, true)
|
|
1667
2015
|
if (id === 'view-settings') showSettingsHome()
|
|
1668
2016
|
}
|
|
@@ -1748,10 +2096,53 @@ function bindUi() {
|
|
|
1748
2096
|
list.style.display = list.style.display === 'none' ? 'flex' : 'none'
|
|
1749
2097
|
})
|
|
1750
2098
|
document.querySelectorAll('.ds-nav-item').forEach(b => b.addEventListener('click', () => showView(b.dataset.view)))
|
|
2099
|
+
$('ds-overview-refresh').addEventListener('click', async () => {
|
|
2100
|
+
toast(t('ds.loading'))
|
|
2101
|
+
if (state.token) {
|
|
2102
|
+
await refreshSessions()
|
|
2103
|
+
const host = await safeRpc('host.describe', {}, '')
|
|
2104
|
+
if (host) state.hostInfo = host
|
|
2105
|
+
}
|
|
2106
|
+
renderOverviewDesktop()
|
|
2107
|
+
})
|
|
2108
|
+
$('ds-overview-primary-action').addEventListener('click', () => {
|
|
2109
|
+
const button = $('ds-overview-primary-action')
|
|
2110
|
+
const action = button.dataset.dsOverviewAction
|
|
2111
|
+
if (action === 'session' && button.dataset.dsOverviewSession) return openSession(button.dataset.dsOverviewSession)
|
|
2112
|
+
if (action === 'new') return $('btn-new-session').click()
|
|
2113
|
+
if (action === 'settings') return showView('view-settings')
|
|
2114
|
+
if (action === 'refresh') return $('ds-overview-refresh').click()
|
|
2115
|
+
const first = document.querySelector('.ds-overview-attention-item')
|
|
2116
|
+
if (first) {
|
|
2117
|
+
first.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
2118
|
+
if (first.matches('button')) first.focus({ preventScroll: true })
|
|
2119
|
+
}
|
|
2120
|
+
})
|
|
1751
2121
|
$('session-list').addEventListener('click', (e) => {
|
|
2122
|
+
if (e.target.closest('[data-archived-toggle]')) {
|
|
2123
|
+
LS.set('dsShowArchivedV1', LS.get('dsShowArchivedV1', '0') === '1' ? '0' : '1')
|
|
2124
|
+
renderSessions()
|
|
2125
|
+
return
|
|
2126
|
+
}
|
|
1752
2127
|
const item = e.target.closest('[data-id]')
|
|
1753
2128
|
if (item) openSession(item.dataset.id)
|
|
1754
2129
|
})
|
|
2130
|
+
$('btn-wb-bind').addEventListener('click', openWorkbenchModal)
|
|
2131
|
+
$('btn-wb-bind-manual').addEventListener('click', () => bindWorkbench($('wb-path-input').value))
|
|
2132
|
+
$('wb-path-input').addEventListener('keydown', e => {
|
|
2133
|
+
if (e.key === 'Enter' && !e.isComposing) { e.preventDefault(); bindWorkbench($('wb-path-input').value) }
|
|
2134
|
+
})
|
|
2135
|
+
$('wb-fs-up').addEventListener('click', wbFsUp)
|
|
2136
|
+
$('wb-fs-home').addEventListener('click', () => wbFsLoad(wbFs.initial || null))
|
|
2137
|
+
$('btn-wb-modal-close').addEventListener('click', closeWorkbenchModal)
|
|
2138
|
+
$('modal-workbench').addEventListener('click', e => { if (e.target === $('modal-workbench')) closeWorkbenchModal() })
|
|
2139
|
+
$('wb-head').addEventListener('click', () => {
|
|
2140
|
+
state.wb.expanded = !state.wb.expanded
|
|
2141
|
+
if (state.wb.expanded && !state.wb.projects) refreshWorkbench({ silent: false })
|
|
2142
|
+
else renderWorkbench()
|
|
2143
|
+
})
|
|
2144
|
+
$('btn-wb-path').addEventListener('click', () => { if (state.wb.path) toast(t('wb.boundPath', { path: state.wb.path }), 'ok') })
|
|
2145
|
+
$('btn-wb-unbind').addEventListener('click', unbindWorkbench)
|
|
1755
2146
|
$('btn-send').addEventListener('click', sendMessage)
|
|
1756
2147
|
$('composer').addEventListener('keydown', (e) => {
|
|
1757
2148
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage() }
|
|
@@ -1864,7 +2255,7 @@ function bindUi() {
|
|
|
1864
2255
|
$('btn-lang').addEventListener('click', () => {
|
|
1865
2256
|
I18N.setLang(I18N.lang === 'zh' ? 'en' : 'zh')
|
|
1866
2257
|
$('btn-lang').textContent = I18N.lang === 'zh' ? 'EN' : '中文'
|
|
1867
|
-
renderServers(); renderSessions(); updateConn(); themeApply()
|
|
2258
|
+
renderServers(); renderSessions(); renderNotifStack(); renderOverviewDesktop(); updateConn(); themeApply()
|
|
1868
2259
|
})
|
|
1869
2260
|
$('fs-up').addEventListener('click', fsUp)
|
|
1870
2261
|
$('fs-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
@@ -1876,7 +2267,7 @@ function bindUi() {
|
|
|
1876
2267
|
async function start() {
|
|
1877
2268
|
loadServers()
|
|
1878
2269
|
renderServers()
|
|
1879
|
-
showView('view-
|
|
2270
|
+
showView('view-overview')
|
|
1880
2271
|
const urlToken = new URLSearchParams(location.search).get('token')
|
|
1881
2272
|
if (urlToken) { state.token = urlToken; LS.set('token', urlToken); history.replaceState(null, '', location.pathname) }
|
|
1882
2273
|
if (!state.token) {
|
|
@@ -1885,13 +2276,18 @@ async function start() {
|
|
|
1885
2276
|
}
|
|
1886
2277
|
$('token-desc').textContent = state.token ? '● ' + state.token.slice(0, 12) + '…' : t('ds.toastAuth')
|
|
1887
2278
|
bindUi()
|
|
2279
|
+
renderWorkbench()
|
|
1888
2280
|
updateConn()
|
|
1889
2281
|
checkNotesOnStart()
|
|
1890
2282
|
if (state.token) {
|
|
1891
2283
|
if (state.servers.length) await selectFastestServer({ silent: true, reconnect: false })
|
|
1892
2284
|
openStreams()
|
|
1893
|
-
refreshSessions()
|
|
2285
|
+
await refreshSessions()
|
|
2286
|
+
const host = await safeRpc('host.describe', {}, '')
|
|
2287
|
+
if (host) state.hostInfo = host
|
|
2288
|
+
refreshWorkbench({ silent: true })
|
|
1894
2289
|
}
|
|
2290
|
+
renderOverviewDesktop()
|
|
1895
2291
|
}
|
|
1896
2292
|
|
|
1897
2293
|
start()
|
package/public/desktop/i18n.js
CHANGED
|
@@ -10,6 +10,38 @@
|
|
|
10
10
|
if (saved === 'zh' || saved === 'en') return saved
|
|
11
11
|
return (navigator.language || 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
12
12
|
}
|
|
13
|
+
const BUILTIN = {
|
|
14
|
+
zh: {
|
|
15
|
+
'wb.unbound': 'DSH Remote(未绑定)', 'wb.bind': '绑定工作台', 'wb.bound': 'DSH Remote(绑定 {title})',
|
|
16
|
+
'wb.viewPath': '已绑定 workspace', 'wb.unbind': '解绑', 'wb.unbindConfirm': '确定解绑当前工作台?解绑后这些会话将回到扁平会话列表。',
|
|
17
|
+
'wb.modalTitle': '绑定工作台', 'wb.modalDesc': '选择工作台根目录,其下每个子文件夹会自动成为项目工作区。',
|
|
18
|
+
'wb.up': '上级', 'wb.home': '根目录', 'wb.selectDir': '选择此目录', 'wb.pathPlaceholder': '或手动输入绝对路径…',
|
|
19
|
+
'wb.pathEmpty': '请输入要绑定的路径', 'wb.empty': '此目录没有子文件夹', 'wb.boundOk': '工作台已绑定:{path}',
|
|
20
|
+
'wb.bindFailed': '绑定失败:{msg}', 'wb.unboundOk': '已解绑工作台', 'wb.unbindFailed': '解绑失败:{msg}',
|
|
21
|
+
'wb.boundPath': '绑定路径:{path}', 'wb.projects': '项目', 'wb.noProjects': '暂无项目(根目录下新建文件夹后会自动出现)',
|
|
22
|
+
'wb.newSession': '新会话', 'wb.noSessions': '暂无会话', 'wb.flatHidden': '工作台会话已收起({n} 个在工作台面板)',
|
|
23
|
+
'wb.apiMissing': '工作台接口不可用(需更新网关)', 'wb.loadFailed': '工作台加载失败:{msg}',
|
|
24
|
+
'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------'
|
|
25
|
+
},
|
|
26
|
+
en: {
|
|
27
|
+
'wb.unbound': 'DSH Remote (unbound)', 'wb.bind': 'Bind workbench', 'wb.bound': 'DSH Remote (bound {title})',
|
|
28
|
+
'wb.viewPath': 'Bound workspace', 'wb.unbind': 'Unbind', 'wb.unbindConfirm': 'Unbind the current workbench? These sessions will return to the flat list.',
|
|
29
|
+
'wb.modalTitle': 'Bind workbench', 'wb.modalDesc': 'Choose the workbench root; each subfolder becomes a project workspace automatically.',
|
|
30
|
+
'wb.up': 'Up', 'wb.home': 'Root', 'wb.selectDir': 'Select this folder', 'wb.pathPlaceholder': 'Or type an absolute path…',
|
|
31
|
+
'wb.pathEmpty': 'Enter a path to bind', 'wb.empty': 'No subfolders in this folder', 'wb.boundOk': 'Workbench bound: {path}',
|
|
32
|
+
'wb.bindFailed': 'Bind failed: {msg}', 'wb.unboundOk': 'Workbench unbound', 'wb.unbindFailed': 'Unbind failed: {msg}',
|
|
33
|
+
'wb.boundPath': 'Bound path: {path}', 'wb.projects': 'Projects', 'wb.noProjects': 'No projects yet',
|
|
34
|
+
'wb.newSession': 'New session', 'wb.noSessions': 'No sessions', 'wb.flatHidden': 'Workbench sessions folded ({n} in the workbench panel)',
|
|
35
|
+
'wb.apiMissing': 'Workbench API unavailable (update gateway)', 'wb.loadFailed': 'Workbench load failed: {msg}',
|
|
36
|
+
'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function mergeDicts(strings) {
|
|
40
|
+
return {
|
|
41
|
+
zh: Object.assign({}, BUILTIN.zh, strings?.zh || {}),
|
|
42
|
+
en: Object.assign({}, BUILTIN.en, strings?.en || {})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
13
45
|
let dict = null
|
|
14
46
|
let lang = detect()
|
|
15
47
|
|
|
@@ -46,7 +78,7 @@
|
|
|
46
78
|
}
|
|
47
79
|
|
|
48
80
|
window.I18N = {
|
|
49
|
-
init(strings) { dict = strings || dict; return apply(document) },
|
|
81
|
+
init(strings) { dict = mergeDicts(strings || dict || {}); return apply(document) },
|
|
50
82
|
t, setLang, apply,
|
|
51
83
|
get lang() { return lang }
|
|
52
84
|
}
|