dsh-remote-plugin 0.6.5 → 0.6.6
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 +1 -1
- package/README.md +1 -1
- package/apk/dsh-remote.apk +0 -0
- package/gateway-stats.cjs +9 -2
- package/gateway.cjs +173 -6
- package/index.mjs +52 -12
- package/package.json +1 -1
- package/public/app.js +241 -28
- package/public/desktop/desktop.css +34 -3
- package/public/desktop/desktop.html +35 -7
- package/public/desktop/desktop.js +149 -19
- package/public/index.html +50 -11
- package/public/styles.css +38 -4
- package/public/update.json +8 -4
- package/public/version.json +1 -1
package/public/app.js
CHANGED
|
@@ -46,6 +46,7 @@ const state = {
|
|
|
46
46
|
serverLatency: {}, // url -> 最近一次 /health 测速毫秒数
|
|
47
47
|
selectingServer: false, // 防重入: 测速/切换中
|
|
48
48
|
sessions: [],
|
|
49
|
+
sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
|
|
49
50
|
byId: new Map(),
|
|
50
51
|
current: null, // 当前打开的 sessionId
|
|
51
52
|
hostInfo: null,
|
|
@@ -183,6 +184,24 @@ function apiUrl(path) {
|
|
|
183
184
|
return (state.server || '') + path
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
function updateBase() {
|
|
188
|
+
const configured = String(state.server || '').replace(/\/+$/, '')
|
|
189
|
+
if (configured) return configured
|
|
190
|
+
if (!/^https?:$/.test(location.protocol)) return ''
|
|
191
|
+
const origin = location.origin.replace(/\/+$/, '')
|
|
192
|
+
return location.pathname === '/remote' || location.pathname.startsWith('/remote/')
|
|
193
|
+
? origin + '/remote'
|
|
194
|
+
: origin
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function adminApiUrl(path) {
|
|
198
|
+
const base = String(state.server || '').replace(/\/+$/, '')
|
|
199
|
+
if (base) return base + path
|
|
200
|
+
// DSH 抽屉页面同源运行在 /remote/ 前缀下,管理接口也必须带此前缀;
|
|
201
|
+
// 独立网关根页面则继续使用 /admin/api/*。
|
|
202
|
+
return location.pathname.startsWith('/remote/') ? '/remote' + path : path
|
|
203
|
+
}
|
|
204
|
+
|
|
186
205
|
function fmtCost(n) {
|
|
187
206
|
return '¥' + (Number(n) || 0).toFixed(2)
|
|
188
207
|
}
|
|
@@ -268,14 +287,14 @@ function renderStats(days) {
|
|
|
268
287
|
</div>`
|
|
269
288
|
}).join('')
|
|
270
289
|
}
|
|
271
|
-
async function rpc(method, payload = {}) {
|
|
290
|
+
async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
272
291
|
const opts = {
|
|
273
292
|
method: 'POST',
|
|
274
293
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' },
|
|
275
294
|
body: JSON.stringify({ type: 'client-request', rpcId: uuid(), method, payload })
|
|
276
295
|
}
|
|
277
296
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
278
|
-
opts.signal = AbortSignal.timeout(
|
|
297
|
+
opts.signal = AbortSignal.timeout(timeoutMs)
|
|
279
298
|
}
|
|
280
299
|
const res = await fetch(apiUrl('/api/' + method), opts)
|
|
281
300
|
if (res.status === 401) throw new Error('AUTH')
|
|
@@ -290,11 +309,15 @@ async function rpc(method, payload = {}) {
|
|
|
290
309
|
}
|
|
291
310
|
|
|
292
311
|
async function respond(rpcId, value) {
|
|
293
|
-
const
|
|
312
|
+
const opts = {
|
|
294
313
|
method: 'POST',
|
|
295
314
|
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': CAP?.isNativePlatform?.() ? 'app' : 'web' },
|
|
296
315
|
body: JSON.stringify({ type: 'client-response', rpcId, result: { ok: true, value } })
|
|
297
|
-
}
|
|
316
|
+
}
|
|
317
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
318
|
+
opts.signal = AbortSignal.timeout(15000)
|
|
319
|
+
}
|
|
320
|
+
const res = await fetch(apiUrl('/api/respond'), opts)
|
|
298
321
|
if (res.status === 401) throw new Error('AUTH')
|
|
299
322
|
const receipt = await res.json()
|
|
300
323
|
return receipt?.accepted === true
|
|
@@ -430,7 +453,8 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
|
|
|
430
453
|
let ms = Infinity
|
|
431
454
|
|
|
432
455
|
if (state.autoSelect[state.activeGroup] !== false) {
|
|
433
|
-
|
|
456
|
+
const measured = await Promise.all(candidates.map(async (u) => [u, await pingServer(u)]))
|
|
457
|
+
for (const [u, latency] of measured) state.serverLatency[u] = latency
|
|
434
458
|
best = candidates
|
|
435
459
|
.filter(u => Number.isFinite(state.serverLatency[u]))
|
|
436
460
|
.sort((a, b) => state.serverLatency[a] - state.serverLatency[b])[0] || null
|
|
@@ -898,8 +922,18 @@ async function pollKind(kind) {
|
|
|
898
922
|
let data
|
|
899
923
|
try { data = await res.json() } catch { return }
|
|
900
924
|
if (!data || !Array.isArray(data.events)) return
|
|
901
|
-
//
|
|
902
|
-
|
|
925
|
+
// 网关重启或客户端离线过久后,游标可能落后于内存环形缓冲;
|
|
926
|
+
// 从当前缓冲重新接收,并刷新权威会话列表,避免静默漏事件。
|
|
927
|
+
const reset = data.truncated === true || (typeof data.latestSeq === 'number' && data.latestSeq < since)
|
|
928
|
+
if (reset) {
|
|
929
|
+
state.pollSeq[kind] = 0
|
|
930
|
+
if (kind === 'mux') {
|
|
931
|
+
state.approvals = []
|
|
932
|
+
state.questions = []
|
|
933
|
+
renderPending()
|
|
934
|
+
}
|
|
935
|
+
scheduleRefresh()
|
|
936
|
+
}
|
|
903
937
|
for (const item of data.events) {
|
|
904
938
|
if (item.seq > (state.pollSeq[kind] || 0)) {
|
|
905
939
|
state.pollSeq[kind] = item.seq
|
|
@@ -1070,7 +1104,8 @@ function applyProjection(sessionId, key, value, seq) {
|
|
|
1070
1104
|
if (state.current === sessionId) { renderSessionTitle(); renderSessionCards() }
|
|
1071
1105
|
if (['title', 'goal', 'todos', 'plan', 'sessionListMetadata'].includes(key)) scheduleRefresh()
|
|
1072
1106
|
else renderSessions()
|
|
1073
|
-
}
|
|
1107
|
+
}
|
|
1108
|
+
function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('session.unknown')) }
|
|
1074
1109
|
function short(id) { return '…' + String(id).slice(-8) }
|
|
1075
1110
|
const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
|
|
1076
1111
|
function isGoalTerminal(goal) {
|
|
@@ -1088,10 +1123,42 @@ function updatePendingBadge() {
|
|
|
1088
1123
|
if (pending) $('nav-pending').textContent = pending
|
|
1089
1124
|
}
|
|
1090
1125
|
|
|
1126
|
+
function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
|
|
1127
|
+
function sessionWorkspaceLabel(s) {
|
|
1128
|
+
const cwd = sessionCwd(s)
|
|
1129
|
+
return cwd || t('sessions.workspaceUnknown')
|
|
1130
|
+
}
|
|
1131
|
+
function workspaceDisplayName(label) {
|
|
1132
|
+
const value = String(label || '').trim()
|
|
1133
|
+
if (!value || value === t('sessions.workspaceUnknown')) return value || t('sessions.workspaceUnknown')
|
|
1134
|
+
const clean = value.replace(/[\\/]+$/, '')
|
|
1135
|
+
const parts = clean.split(/[\\/]/).filter(Boolean)
|
|
1136
|
+
return parts[parts.length - 1] || value
|
|
1137
|
+
}
|
|
1138
|
+
function sortedSessions() {
|
|
1139
|
+
const items = [...state.sessions]
|
|
1140
|
+
if (state.sessionSort === 'workspace') {
|
|
1141
|
+
return items.sort((a, b) => {
|
|
1142
|
+
const aw = sessionCwd(a) || '\uffff'
|
|
1143
|
+
const bw = sessionCwd(b) || '\uffff'
|
|
1144
|
+
const byWorkspace = aw.localeCompare(bw, undefined, { numeric: true, sensitivity: 'base' })
|
|
1145
|
+
return byWorkspace || ((b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1146
|
+
})
|
|
1147
|
+
}
|
|
1148
|
+
return items.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
|
1149
|
+
}
|
|
1091
1150
|
function renderSessions() {
|
|
1092
1151
|
const list = $('session-list')
|
|
1093
|
-
const items =
|
|
1094
|
-
|
|
1152
|
+
const items = sortedSessions()
|
|
1153
|
+
let lastWorkspace = null
|
|
1154
|
+
const rows = []
|
|
1155
|
+
for (const s of items) {
|
|
1156
|
+
const workspace = sessionWorkspaceLabel(s)
|
|
1157
|
+
const workspaceName = workspaceDisplayName(workspace)
|
|
1158
|
+
if (state.sessionSort === 'workspace' && workspace !== lastWorkspace) {
|
|
1159
|
+
rows.push(`<div class="session-group-label" title="${esc(workspace)}"><span class="session-group-icon" aria-hidden="true">⌂</span><span class="session-group-name">${esc(workspaceName)}</span></div>`)
|
|
1160
|
+
lastWorkspace = workspace
|
|
1161
|
+
}
|
|
1095
1162
|
const title = titleOf(s)
|
|
1096
1163
|
const goal = goalOf(s)
|
|
1097
1164
|
const pending = (state.approvals.some(a => a.sessionId === s.sessionId) || state.questions.some(q => q.sessionId === s.sessionId)) ? 'pending' : ''
|
|
@@ -1101,7 +1168,7 @@ function renderSessions() {
|
|
|
1101
1168
|
if (pending) dots.push('pending')
|
|
1102
1169
|
const badge = goal ? `<span class="sc-badge ${goal.phase === 'active' ? 'goal-active' : ''}">${esc(t('sessions.goalBadge', { phase: goal.phase || '?' }))}</span>` : ''
|
|
1103
1170
|
const queueBadge = queueN ? `<span class="sc-badge">${esc(t('sessions.queueBadge', { n: queueN }))}</span>` : ''
|
|
1104
|
-
|
|
1171
|
+
rows.push(`<div class="session-card ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1105
1172
|
<div class="sc-title">${esc(title)}</div>
|
|
1106
1173
|
<div class="sc-meta">
|
|
1107
1174
|
<span class="sc-dot ${dots.join(' ')}"></span>
|
|
@@ -1109,9 +1176,14 @@ function renderSessions() {
|
|
|
1109
1176
|
${s.running ? '<span>' + t('sessions.running') + '</span>' : ''}
|
|
1110
1177
|
${badge}${queueBadge}
|
|
1111
1178
|
</div>
|
|
1179
|
+
<div class="sc-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</div>
|
|
1112
1180
|
<span class="sc-arrow">›</span>
|
|
1113
|
-
</div>`
|
|
1114
|
-
}
|
|
1181
|
+
</div>`)
|
|
1182
|
+
}
|
|
1183
|
+
list.innerHTML = rows.join('')
|
|
1184
|
+
list.classList.toggle('workspace-sorted', state.sessionSort === 'workspace')
|
|
1185
|
+
const sort = $('session-sort')
|
|
1186
|
+
if (sort) sort.value = state.sessionSort
|
|
1115
1187
|
$('home-empty').classList.toggle('hidden', items.length > 0)
|
|
1116
1188
|
const running = state.sessions.filter(s => s.running).length
|
|
1117
1189
|
const pending = state.approvals.length + state.questions.length
|
|
@@ -1245,7 +1317,7 @@ function restoreCachedHistory() {
|
|
|
1245
1317
|
if (!cached?.events?.length) return false
|
|
1246
1318
|
const h = emptyHistory()
|
|
1247
1319
|
for (const e of cached.events) {
|
|
1248
|
-
if (
|
|
1320
|
+
if (e?.seq == null) continue
|
|
1249
1321
|
h.seqs.add(e.seq)
|
|
1250
1322
|
h.visible.push(e)
|
|
1251
1323
|
}
|
|
@@ -1613,7 +1685,8 @@ async function goalAction(kind) {
|
|
|
1613
1685
|
if (!method) return
|
|
1614
1686
|
if (kind === 'clear' && !confirm(t('goal.confirmClear'))) return
|
|
1615
1687
|
if (kind === 'complete' && !confirm(t('goal.confirmComplete'))) return
|
|
1616
|
-
await safeRpc(method, { sessionId: state.current, ref }, t('goal.actionFailed'))
|
|
1688
|
+
const result = await safeRpc(method, { sessionId: state.current, ref }, t('goal.actionFailed'))
|
|
1689
|
+
if (result == null) return
|
|
1617
1690
|
if (kind === 'complete') setGoalPhaseLocal('complete')
|
|
1618
1691
|
if (kind === 'clear') setGoalPhaseLocal('cleared')
|
|
1619
1692
|
toast(t('goal.actionSubmitted'), 'ok')
|
|
@@ -1622,7 +1695,8 @@ async function goalAction(kind) {
|
|
|
1622
1695
|
|
|
1623
1696
|
async function interruptSubagent(childId) {
|
|
1624
1697
|
if (!confirm(t('subagent.confirmInterrupt'))) return
|
|
1625
|
-
await safeRpc('subagent.interrupt', { parentSessionId: state.current, childSessionId: childId, mode: 'continuable' }, t('subagent.interruptFailed'))
|
|
1698
|
+
const result = await safeRpc('subagent.interrupt', { parentSessionId: state.current, childSessionId: childId, mode: 'continuable' }, t('subagent.interruptFailed'))
|
|
1699
|
+
if (result == null) return
|
|
1626
1700
|
toast(t('subagent.interruptSubmitted'), 'ok')
|
|
1627
1701
|
setTimeout(renderSessionCards, 600)
|
|
1628
1702
|
}
|
|
@@ -1784,7 +1858,18 @@ async function cancelSession() {
|
|
|
1784
1858
|
}
|
|
1785
1859
|
|
|
1786
1860
|
async function newSession() {
|
|
1787
|
-
|
|
1861
|
+
let payload = {}
|
|
1862
|
+
// DSH 的 host.describe 返回当前工作目录。每次创建前短暂刷新一次,
|
|
1863
|
+
// 避免用户在桌面端切换工作区后,手机仍沿用启动时的旧 cwd。
|
|
1864
|
+
try {
|
|
1865
|
+
const host = await rpc('host.describe', {}, 5000)
|
|
1866
|
+
const cwd = typeof host?.cwd === 'string' ? host.cwd.trim() : ''
|
|
1867
|
+
if (cwd) {
|
|
1868
|
+
state.hostInfo = host
|
|
1869
|
+
payload = { cwd }
|
|
1870
|
+
}
|
|
1871
|
+
} catch {}
|
|
1872
|
+
const v = await safeRpc('session.create', payload, t('home.createFailed'))
|
|
1788
1873
|
if (!v?.sessionId) return
|
|
1789
1874
|
toast(t('home.created'), 'ok')
|
|
1790
1875
|
await refreshSessions()
|
|
@@ -1831,7 +1916,14 @@ function renderPending() {
|
|
|
1831
1916
|
async function approveApproval(id, allow) {
|
|
1832
1917
|
const a = state.approvals.find(x => x.approvalId === id)
|
|
1833
1918
|
if (!a) return
|
|
1834
|
-
|
|
1919
|
+
let ok
|
|
1920
|
+
try {
|
|
1921
|
+
ok = await respond(a.rpcId, { sessionId: a.sessionId, approvalId: a.approvalId, outcome: allow ? 'allowed-once' : 'rejected' })
|
|
1922
|
+
} catch (e) {
|
|
1923
|
+
if (e.message === 'AUTH') authFailure()
|
|
1924
|
+
else toast(t('pending.submitFailed', { msg: e.message || t('fs.networkError') }), 'err')
|
|
1925
|
+
return
|
|
1926
|
+
}
|
|
1835
1927
|
toast(ok ? (allow ? t('pending.allowed') : t('pending.rejected')) : t('pending.stale'), ok ? 'ok' : 'err')
|
|
1836
1928
|
state.approvals = state.approvals.filter(x => x.approvalId !== id)
|
|
1837
1929
|
renderPending()
|
|
@@ -1862,7 +1954,14 @@ async function submitQuestion() {
|
|
|
1862
1954
|
return ans
|
|
1863
1955
|
}).filter(Boolean)
|
|
1864
1956
|
if (!answers.length) return toast(t('question.needAnswer'), 'err')
|
|
1865
|
-
|
|
1957
|
+
let ok
|
|
1958
|
+
try {
|
|
1959
|
+
ok = await respond(q.rpcId, { sessionId: q.sessionId, answer: { answers } })
|
|
1960
|
+
} catch (e) {
|
|
1961
|
+
if (e.message === 'AUTH') authFailure()
|
|
1962
|
+
else toast(t('question.submitFailed', { msg: e.message || t('fs.networkError') }), 'err')
|
|
1963
|
+
return
|
|
1964
|
+
}
|
|
1866
1965
|
if (ok) { toast(t('question.submitted'), 'ok'); $('modal-question').classList.add('hidden'); state.questions = state.questions.filter(x => x.rpcId !== q.rpcId); renderPending() }
|
|
1867
1966
|
else toast(t('question.stale'), 'err')
|
|
1868
1967
|
}
|
|
@@ -1910,6 +2009,50 @@ function fsParent(p) {
|
|
|
1910
2009
|
return clean.slice(0, idx)
|
|
1911
2010
|
}
|
|
1912
2011
|
|
|
2012
|
+
async function openWorkspaceModal() {
|
|
2013
|
+
if (!state.token) { toast(t('fs.noTokenToast'), 'err'); showView('view-settings'); return }
|
|
2014
|
+
if (!state.fs.path) await loadFs(null, { silent: true })
|
|
2015
|
+
$('workspace-parent-path').textContent = state.fs.path || '~'
|
|
2016
|
+
$('workspace-name').value = ''
|
|
2017
|
+
$('modal-workspace').classList.remove('hidden')
|
|
2018
|
+
setTimeout(() => $('workspace-name').focus(), 50)
|
|
2019
|
+
}
|
|
2020
|
+
function closeWorkspaceModal() { $('modal-workspace').classList.add('hidden') }
|
|
2021
|
+
async function createWorkspace() {
|
|
2022
|
+
if (createWorkspace.busy) return
|
|
2023
|
+
const name = $('workspace-name').value.trim()
|
|
2024
|
+
if (!name) { toast(t('workspace.nameRequired'), 'err'); $('workspace-name').focus(); return }
|
|
2025
|
+
createWorkspace.busy = true
|
|
2026
|
+
const parent = state.fs.path || ''
|
|
2027
|
+
const button = $('workspace-create')
|
|
2028
|
+
button.disabled = true
|
|
2029
|
+
try {
|
|
2030
|
+
const res = await fetch(fsApiUrl('/mkdir', { path: parent, name }), { method: 'POST', headers: fsHeaders() })
|
|
2031
|
+
if (res.status === 401) { fsAuthError(401); return }
|
|
2032
|
+
const data = await res.json().catch(() => ({}))
|
|
2033
|
+
if (!res.ok) {
|
|
2034
|
+
const msg = data.error === 'exists' ? t('workspace.exists') : data.error === 'bad-name' ? t('workspace.invalidName') : data.error || ('HTTP ' + res.status)
|
|
2035
|
+
throw new Error(msg)
|
|
2036
|
+
}
|
|
2037
|
+
closeWorkspaceModal()
|
|
2038
|
+
await loadFs(parent || null, { silent: true })
|
|
2039
|
+
const v = await safeRpc('session.create', { cwd: data.path }, t('home.createFailed'))
|
|
2040
|
+
await refreshSessions()
|
|
2041
|
+
if (v?.sessionId) {
|
|
2042
|
+
toast(t('workspace.created'), 'ok')
|
|
2043
|
+
openSession(v.sessionId)
|
|
2044
|
+
} else {
|
|
2045
|
+
toast(t('workspace.createdNoSession'), 'ok')
|
|
2046
|
+
}
|
|
2047
|
+
} catch (e) {
|
|
2048
|
+
if (e.message === 'AUTH') fsAuthError(401)
|
|
2049
|
+
else toast(t('workspace.createFailed', { msg: e.message || t('fs.networkError') }), 'err')
|
|
2050
|
+
} finally {
|
|
2051
|
+
createWorkspace.busy = false
|
|
2052
|
+
button.disabled = false
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1913
2056
|
function fsApiUrl(sub, params = {}) {
|
|
1914
2057
|
const u = new URL(apiUrl('/fs' + sub), location.href)
|
|
1915
2058
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -2333,7 +2476,8 @@ async function submitGoalEdit() {
|
|
|
2333
2476
|
if (!goal) return
|
|
2334
2477
|
const objective = $('goal-edit-text')?.value?.trim()
|
|
2335
2478
|
if (!objective) return toast(t('goal.cannotEmpty'), 'err')
|
|
2336
|
-
await safeRpc('goal.edit', { sessionId: state.current, ref: { id: goal.id, revision: goal.revision }, objective }, t('goal.updateFailed'))
|
|
2479
|
+
const result = await safeRpc('goal.edit', { sessionId: state.current, ref: { id: goal.id, revision: goal.revision }, objective }, t('goal.updateFailed'))
|
|
2480
|
+
if (result == null) return
|
|
2337
2481
|
$('modal-goal').classList.add('hidden')
|
|
2338
2482
|
toast(t('goal.updated'), 'ok')
|
|
2339
2483
|
scheduleRefresh()
|
|
@@ -2477,7 +2621,7 @@ async function checkUpdate(silent) {
|
|
|
2477
2621
|
if (!silent) toast(t('update.noVersion'), 'err')
|
|
2478
2622
|
return
|
|
2479
2623
|
}
|
|
2480
|
-
const base =
|
|
2624
|
+
const base = updateBase()
|
|
2481
2625
|
if (!base) {
|
|
2482
2626
|
if (!silent) toast(t('update.needServer'), 'err')
|
|
2483
2627
|
$('update-desc').textContent = state.localVersion ? `${t('update.currentV', { version: state.localVersion })} · ${t('update.needServer')}` : t('update.needServer')
|
|
@@ -2554,7 +2698,7 @@ async function verifyUpdateApk(info, url) {
|
|
|
2554
2698
|
async function downloadUpdate() {
|
|
2555
2699
|
const info = state.updateInfo
|
|
2556
2700
|
if (!info) return
|
|
2557
|
-
const base =
|
|
2701
|
+
const base = updateBase()
|
|
2558
2702
|
let url
|
|
2559
2703
|
try { url = new URL(info.apkUrl || 'dsh-remote.apk', base + '/').href }
|
|
2560
2704
|
catch { url = base + '/' + (info.apkUrl || 'dsh-remote.apk') }
|
|
@@ -2769,8 +2913,7 @@ async function schedulePeakReminders() {
|
|
|
2769
2913
|
const b = bgBridge()
|
|
2770
2914
|
if (!b?.startPeakReminder) return false
|
|
2771
2915
|
try {
|
|
2772
|
-
b.startPeakReminder()
|
|
2773
|
-
return true
|
|
2916
|
+
return b.startPeakReminder() !== false
|
|
2774
2917
|
} catch { return false }
|
|
2775
2918
|
}
|
|
2776
2919
|
|
|
@@ -2779,8 +2922,7 @@ async function cancelPeakReminders() {
|
|
|
2779
2922
|
const b = bgBridge()
|
|
2780
2923
|
if (!b?.stopPeakReminder) return false
|
|
2781
2924
|
try {
|
|
2782
|
-
b.stopPeakReminder()
|
|
2783
|
-
return true
|
|
2925
|
+
return b.stopPeakReminder() !== false
|
|
2784
2926
|
} catch { return false }
|
|
2785
2927
|
}
|
|
2786
2928
|
|
|
@@ -2973,6 +3115,55 @@ function initToken() {
|
|
|
2973
3115
|
$('server-desc').textContent = state.server || t('servers.defaultDesc')
|
|
2974
3116
|
}
|
|
2975
3117
|
|
|
3118
|
+
function renderDshControlStatus(v) {
|
|
3119
|
+
const desc = $('dsh-control-desc')
|
|
3120
|
+
if (!desc || !v) return
|
|
3121
|
+
const buttons = [$('btn-dsh-start'), $('btn-dsh-restart')].filter(Boolean)
|
|
3122
|
+
if (v.supported === false) {
|
|
3123
|
+
desc.textContent = v.message || t('settings.dshUnsupported')
|
|
3124
|
+
buttons.forEach(b => { b.disabled = true; b.classList.add('hidden') })
|
|
3125
|
+
return
|
|
3126
|
+
}
|
|
3127
|
+
buttons.forEach(b => { b.disabled = false; b.classList.remove('hidden') })
|
|
3128
|
+
desc.textContent = `${t(v.running ? 'settings.dshRunning' : 'settings.dshStopped')} · ${v.service || 'dsh-web'}`
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
async function loadDshControl() {
|
|
3132
|
+
if (!state.token || !$('dsh-control-desc')) return
|
|
3133
|
+
try {
|
|
3134
|
+
const res = await fetch(adminApiUrl('/admin/api/dsh'), { headers: { authorization: 'Bearer ' + state.token } })
|
|
3135
|
+
if (res.status === 401) return authFailure()
|
|
3136
|
+
renderDshControlStatus(await res.json())
|
|
3137
|
+
} catch {
|
|
3138
|
+
$('dsh-control-desc').textContent = t('settings.dshFailed', { msg: t('conn.off') })
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
async function controlDsh(action) {
|
|
3143
|
+
const label = action === 'start' ? t('settings.dshStart') : t('settings.dshRestart')
|
|
3144
|
+
const buttons = [$('btn-dsh-start'), $('btn-dsh-restart')].filter(Boolean)
|
|
3145
|
+
buttons.forEach(b => { b.disabled = true })
|
|
3146
|
+
const desc = $('dsh-control-desc')
|
|
3147
|
+
if (desc) desc.textContent = t('settings.dshStarting', { action: label })
|
|
3148
|
+
try {
|
|
3149
|
+
const res = await fetch(adminApiUrl('/admin/api/dsh'), {
|
|
3150
|
+
method: 'POST',
|
|
3151
|
+
headers: { authorization: 'Bearer ' + state.token, 'content-type': 'application/json' },
|
|
3152
|
+
body: JSON.stringify({ action }),
|
|
3153
|
+
})
|
|
3154
|
+
if (res.status === 401) return authFailure()
|
|
3155
|
+
const v = await res.json().catch(() => ({}))
|
|
3156
|
+
if (!res.ok || v.ok === false) throw new Error(v.error || v.message || `HTTP ${res.status}`)
|
|
3157
|
+
renderDshControlStatus(v)
|
|
3158
|
+
toast(t('settings.dshStarted', { action: label }), 'ok')
|
|
3159
|
+
} catch (e) {
|
|
3160
|
+
if (desc) desc.textContent = t('settings.dshFailed', { msg: e?.message || e })
|
|
3161
|
+
toast(t('settings.dshFailed', { msg: e?.message || e }), 'err')
|
|
3162
|
+
} finally {
|
|
3163
|
+
buttons.forEach(b => { b.disabled = false })
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
|
|
2976
3167
|
function renderLangBtn() {
|
|
2977
3168
|
const btn = $('btn-lang')
|
|
2978
3169
|
if (btn) btn.textContent = I18N.lang === 'zh' ? 'EN' : '中文'
|
|
@@ -3089,6 +3280,12 @@ function bindUi() {
|
|
|
3089
3280
|
if (e.key === 'Escape' && !$('feedback-sheet').classList.contains('hidden')) { closeFeedbackSheet(); $('btn-feedback').focus() }
|
|
3090
3281
|
})
|
|
3091
3282
|
$('btn-new-session').addEventListener('click', newSession)
|
|
3283
|
+
$('btn-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
3284
|
+
$('session-sort')?.addEventListener('change', (e) => {
|
|
3285
|
+
state.sessionSort = e.target.value === 'workspace' ? 'workspace' : 'time'
|
|
3286
|
+
LS.set('sessionSort', state.sessionSort)
|
|
3287
|
+
renderSessions()
|
|
3288
|
+
})
|
|
3092
3289
|
$('btn-cancel').addEventListener('click', cancelSession)
|
|
3093
3290
|
$('btn-send').addEventListener('click', sendMessage)
|
|
3094
3291
|
$('btn-plus').addEventListener('click', toggleComposerMenu)
|
|
@@ -3136,6 +3333,10 @@ function bindUi() {
|
|
|
3136
3333
|
// goal
|
|
3137
3334
|
$('goal-close').addEventListener('click', () => $('modal-goal').classList.add('hidden'))
|
|
3138
3335
|
$('goal-edit').addEventListener('click', submitGoalEdit)
|
|
3336
|
+
$('workspace-cancel').addEventListener('click', closeWorkspaceModal)
|
|
3337
|
+
$('workspace-create').addEventListener('click', createWorkspace)
|
|
3338
|
+
$('workspace-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') createWorkspace() })
|
|
3339
|
+
$('modal-workspace').addEventListener('click', (e) => { if (e.target === $('modal-workspace')) closeWorkspaceModal() })
|
|
3139
3340
|
// 设置
|
|
3140
3341
|
$('view-settings').addEventListener('click', (e) => {
|
|
3141
3342
|
const group = e.target.closest('[data-settings-group]')
|
|
@@ -3165,6 +3366,8 @@ function bindUi() {
|
|
|
3165
3366
|
$('host-desc').textContent = t('settings.hostDesc', { version: v.version, cwd: v.cwd, n: v.attachedSessions })
|
|
3166
3367
|
}
|
|
3167
3368
|
})
|
|
3369
|
+
$('btn-dsh-start')?.addEventListener('click', () => controlDsh('start'))
|
|
3370
|
+
$('btn-dsh-restart')?.addEventListener('click', () => controlDsh('restart'))
|
|
3168
3371
|
$('btn-check-update').addEventListener('click', () => checkUpdate(false))
|
|
3169
3372
|
$('btn-download-update').addEventListener('click', downloadUpdate)
|
|
3170
3373
|
$('btn-update-expand').addEventListener('click', toggleUpdateExpand)
|
|
@@ -3191,10 +3394,18 @@ function bindUi() {
|
|
|
3191
3394
|
}
|
|
3192
3395
|
const ok = await ensureNotify()
|
|
3193
3396
|
if (!ok) { e.target.checked = false; return toast(t('settings.notifyDenied')) }
|
|
3194
|
-
await schedulePeakReminders()
|
|
3397
|
+
const started = await schedulePeakReminders()
|
|
3398
|
+
if (!started) {
|
|
3399
|
+
e.target.checked = false
|
|
3400
|
+
return toast(t('peakRemind.failed'), 'err')
|
|
3401
|
+
}
|
|
3195
3402
|
toast(t('peakRemind.on'), 'ok')
|
|
3196
3403
|
} else {
|
|
3197
|
-
await cancelPeakReminders()
|
|
3404
|
+
const stopped = await cancelPeakReminders()
|
|
3405
|
+
if (!stopped) {
|
|
3406
|
+
e.target.checked = true
|
|
3407
|
+
return toast(t('peakRemind.failed'), 'err')
|
|
3408
|
+
}
|
|
3198
3409
|
toast(t('peakRemind.off'), 'ok')
|
|
3199
3410
|
}
|
|
3200
3411
|
LS.set('peakRemind', e.target.checked ? '1' : '0')
|
|
@@ -3240,6 +3451,7 @@ function bindUi() {
|
|
|
3240
3451
|
|
|
3241
3452
|
// 文件页
|
|
3242
3453
|
$('fs-up').addEventListener('click', fsUp)
|
|
3454
|
+
$('fs-new-workspace').addEventListener('click', openWorkspaceModal)
|
|
3243
3455
|
$('fs-refresh').addEventListener('click', () => { toast(t('common.refreshing')); loadFs() })
|
|
3244
3456
|
$('fs-upload-btn').addEventListener('click', () => $('fs-file-input').click())
|
|
3245
3457
|
$('fs-file-input').addEventListener('change', (e) => {
|
|
@@ -3306,6 +3518,7 @@ async function boot() {
|
|
|
3306
3518
|
await refreshAll()
|
|
3307
3519
|
const host = await safeRpc('host.describe', {}, '')
|
|
3308
3520
|
if (host) { state.hostInfo = host; $('host-desc').textContent = t('settings.hostDesc', { version: host.version, cwd: host.cwd, n: host.attachedSessions }) }
|
|
3521
|
+
loadDshControl()
|
|
3309
3522
|
// 启动后自动检查一次更新(静默)
|
|
3310
3523
|
setTimeout(() => checkUpdate(true), 4000)
|
|
3311
3524
|
}
|
|
@@ -15,12 +15,29 @@ html, body { height: 100%; margin: 0; overflow: hidden; overscroll-behavior: non
|
|
|
15
15
|
.ds-btn:active { filter: brightness(.96); }
|
|
16
16
|
a.ds-btn { text-decoration: none; }
|
|
17
17
|
.ds-new-session .ds-btn { width: 100%; justify-content: center; }
|
|
18
|
-
.ds-section-
|
|
18
|
+
.ds-section-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; min-width: 0; padding-right: 10px; }
|
|
19
|
+
.ds-section-label { min-width: 0; flex: 1; font-size: 11px; color: var(--dsr-muted); letter-spacing: .8px; padding: 10px 14px 6px; font-weight: 700; }
|
|
20
|
+
.ds-session-sort {
|
|
21
|
+
width: 92px; min-width: 0; height: 26px; padding: 0 5px; color: var(--dsr-muted);
|
|
22
|
+
background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 7px;
|
|
23
|
+
font: inherit; font-size: 10.5px; outline: none;
|
|
24
|
+
}
|
|
25
|
+
.ds-session-sort:focus { border-color: var(--dsr-accent-line); color: var(--dsr-text); }
|
|
19
26
|
.ds-session-list { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 8px; display: flex; flex-direction: column; gap: 3px; }
|
|
20
|
-
.ds-session-
|
|
27
|
+
.ds-session-group {
|
|
28
|
+
display: flex; align-items: center; gap: 6px; flex: 0 0 auto; min-width: 0; min-height: 28px;
|
|
29
|
+
box-sizing: border-box; padding: 8px 10px 4px; border-bottom: 1px solid var(--dsr-line);
|
|
30
|
+
color: var(--dsr-accent-strong); font-size: 10px; font-weight: 700; line-height: 1.2;
|
|
31
|
+
}
|
|
32
|
+
.ds-session-group-icon { flex: 0 0 auto; color: var(--dsr-accent-2); font-size: 11px; }
|
|
33
|
+
.ds-session-group-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
34
|
+
.ds-session-item { width: 100%; flex: 0 0 auto; text-align: left; border: none; background: transparent; color: var(--dsr-text); font: inherit; font-size: 13px; padding: 8px 9px; border-radius: 9px; cursor: pointer; display: flex; flex-direction: column; gap: 3px; }
|
|
21
35
|
.ds-session-item:hover { background: var(--dsr-bg-2); }
|
|
22
36
|
.ds-session-item.current { background: var(--dsr-accent-soft); color: var(--dsr-accent-strong); }
|
|
23
37
|
.ds-session-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
38
|
+
.ds-session-workspace { min-width: 0; color: var(--dsr-muted); font-size: 10.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
39
|
+
.ds-session-list.workspace-sorted .ds-session-workspace { display: none; }
|
|
40
|
+
.ds-session-list.workspace-sorted .ds-session-item { padding-top: 9px; padding-bottom: 9px; }
|
|
24
41
|
.ds-session-meta { display: flex; gap: 7px; font-size: 11px; color: var(--dsr-muted); align-items: center; }
|
|
25
42
|
.ds-session-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dsr-line); }
|
|
26
43
|
.ds-session-dot.running { background: var(--dsr-warning); }
|
|
@@ -146,7 +163,7 @@ a.ds-btn { text-decoration: none; }
|
|
|
146
163
|
.ds-composer { flex: none; display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--dsr-line); background: var(--dsr-panel); }
|
|
147
164
|
.ds-composer textarea { width: 100%; min-width: 0; resize: none; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 14px; font: inherit; font-size: 13.5px; line-height: 1.5; outline: none; min-height: 44px; max-height: 120px; box-sizing: border-box; }
|
|
148
165
|
.ds-composer-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
149
|
-
.ds-composer-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
166
|
+
.ds-composer-left { display: flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; }
|
|
150
167
|
.ds-composer-right { display: flex; align-items: center; flex: none; }
|
|
151
168
|
.ds-composer .ds-btn { min-height: 34px; box-sizing: border-box; }
|
|
152
169
|
.ds-send-btn { width: 36px; height: 36px; min-height: 36px; padding: 0; border-radius: 50%; background: var(--dsr-accent-strong); border-color: var(--dsr-accent-strong); color: var(--dsr-on-accent); font-size: 18px; line-height: 1; justify-content: center; }
|
|
@@ -269,6 +286,11 @@ a.ds-btn { text-decoration: none; }
|
|
|
269
286
|
.ds-modal-card { width: min(520px, 100%); background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 14px; padding: 16px; }
|
|
270
287
|
.ds-modal-title { font-weight: 700; margin-bottom: 10px; }
|
|
271
288
|
.ds-modal-body { display: flex; flex-direction: column; gap: 10px; max-height: 60vh; overflow-y: auto; }
|
|
289
|
+
.ds-workspace-create-desc { margin: 0; color: var(--dsr-muted); line-height: 1.55; }
|
|
290
|
+
.ds-workspace-create-location { display: flex; align-items: center; gap: 8px; min-width: 0; color: var(--dsr-muted); font-size: 12px; }
|
|
291
|
+
.ds-workspace-create-location code { min-width: 0; flex: 1; padding: 7px 9px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsr-text); background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 9px; }
|
|
292
|
+
.ds-workspace-name { width: 100%; box-sizing: border-box; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 9px; padding: 8px 10px; font: inherit; outline: none; }
|
|
293
|
+
.ds-workspace-name:focus { border-color: var(--dsr-accent-line); }
|
|
272
294
|
.ds-modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
|
|
273
295
|
.ds-q-item { border: 1px solid var(--dsr-line); border-radius: 10px; padding: 9px 11px; }
|
|
274
296
|
.ds-q-text { font-size: 13.5px; margin-bottom: 6px; }
|
|
@@ -332,3 +354,12 @@ a.ds-btn { text-decoration: none; }
|
|
|
332
354
|
.ds-toast-stack { left: 10px; right: 10px; width: auto; }
|
|
333
355
|
.mobile-only { display: block; }
|
|
334
356
|
}
|
|
357
|
+
|
|
358
|
+
@media (max-width: 520px) {
|
|
359
|
+
.ds-topbar { gap: 6px; padding: 0 8px; }
|
|
360
|
+
.ds-top-right { gap: 5px; }
|
|
361
|
+
.ds-composer-actions { align-items: stretch; flex-wrap: wrap; gap: 6px; }
|
|
362
|
+
.ds-composer-left { flex: 1 1 100%; gap: 5px; }
|
|
363
|
+
.ds-composer-left .ds-btn { flex: 1 1 0; min-width: 0; padding-left: 7px; padding-right: 7px; overflow: hidden; text-overflow: ellipsis; }
|
|
364
|
+
.ds-composer-right { margin-left: auto; }
|
|
365
|
+
}
|
|
@@ -20,7 +20,14 @@
|
|
|
20
20
|
<div class="ds-new-session">
|
|
21
21
|
<button id="btn-new-session" class="ds-btn primary" data-i18n="ds.newSession">+ 新会话</button>
|
|
22
22
|
</div>
|
|
23
|
-
<div class="ds-section-
|
|
23
|
+
<div class="ds-section-head">
|
|
24
|
+
<div class="ds-section-label" data-i18n="ds.sessions">会话</div>
|
|
25
|
+
<select id="session-sort" class="ds-session-sort" data-i18n-aria="ds.sortLabel">
|
|
26
|
+
<option value="time" data-i18n="ds.sortTime">按时间</option>
|
|
27
|
+
<option value="workspace" data-i18n="ds.sortWorkspace">按工作区</option>
|
|
28
|
+
</select>
|
|
29
|
+
<button id="btn-new-workspace" class="ds-icon-btn ds-workspace-create" data-i18n-title="ds.newWorkspace" data-i18n-aria="ds.newWorkspace">+</button>
|
|
30
|
+
</div>
|
|
24
31
|
<div class="ds-session-list" id="session-list"></div>
|
|
25
32
|
<div class="ds-sidebar-foot">
|
|
26
33
|
<button class="ds-nav-item" data-view="view-files"><span class="ds-nav-ico">⇅</span><span data-i18n="ds.files">文件传输</span></button>
|
|
@@ -116,6 +123,7 @@
|
|
|
116
123
|
<div class="ds-fs-bar">
|
|
117
124
|
<button id="fs-up" class="ds-btn" data-i18n="ds.fsUp">上级</button>
|
|
118
125
|
<span id="fs-path" class="ds-fs-path">/</span>
|
|
126
|
+
<button id="fs-new-workspace" class="ds-btn" data-i18n="ds.fsNewWorkspace">新建工作区</button>
|
|
119
127
|
<button id="fs-refresh" class="ds-btn" data-i18n="ds.fsRefresh">刷新</button>
|
|
120
128
|
</div>
|
|
121
129
|
<div id="fs-list" class="ds-fs-list"></div>
|
|
@@ -266,6 +274,22 @@
|
|
|
266
274
|
</div>
|
|
267
275
|
</div>
|
|
268
276
|
|
|
277
|
+
<!-- 新建工作区模态 -->
|
|
278
|
+
<div id="modal-workspace" class="ds-modal hidden">
|
|
279
|
+
<div class="ds-modal-card">
|
|
280
|
+
<div class="ds-modal-title" data-i18n="ds.workspaceCreateTitle">新建工作区</div>
|
|
281
|
+
<div class="ds-modal-body">
|
|
282
|
+
<p class="ds-workspace-create-desc" data-i18n="ds.workspaceCreateDesc">将在当前文件目录下创建文件夹,并自动打开新会话。</p>
|
|
283
|
+
<div class="ds-workspace-create-location"><span data-i18n="ds.workspaceParent">父目录</span><code id="workspace-parent-path">~</code></div>
|
|
284
|
+
<input id="workspace-name" class="ds-workspace-name" maxlength="120" autocomplete="off" data-i18n-placeholder="ds.workspaceNamePlaceholder" placeholder="例如:my-project">
|
|
285
|
+
</div>
|
|
286
|
+
<div class="ds-modal-actions">
|
|
287
|
+
<button id="workspace-cancel" class="ds-btn" data-i18n="ds.cancel">取消</button>
|
|
288
|
+
<button id="workspace-create" class="ds-btn primary" data-i18n="ds.workspaceCreate">创建并打开</button>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
269
293
|
<!-- 预设提示词管理模态 -->
|
|
270
294
|
<div id="modal-presets" class="ds-modal hidden">
|
|
271
295
|
<div class="ds-modal-card">
|
|
@@ -321,14 +345,16 @@
|
|
|
321
345
|
<script>
|
|
322
346
|
window.DESKTOP_STR = {
|
|
323
347
|
zh: {
|
|
324
|
-
'ds.repo': 'GitHub 仓库', 'ds.newSession': '+ 新会话', 'ds.sessions': '会话',
|
|
348
|
+
'ds.repo': 'GitHub 仓库', 'ds.newSession': '+ 新会话', 'ds.newWorkspace': '新建工作区', 'ds.sessions': '会话',
|
|
349
|
+
'ds.sortLabel': '会话排序', 'ds.sortTime': '按时间', 'ds.sortWorkspace': '按工作区', 'ds.workspace': '工作区', 'ds.workspaceUnknown': '未指定工作区',
|
|
325
350
|
'ds.files': '文件传输', 'ds.settings': '设置', 'ds.stats': '统计', 'ds.menu': '导航',
|
|
326
351
|
'ds.send': '发送', 'ds.composerPlaceholder': '输入消息,Enter 发送…', 'ds.presets': '预设',
|
|
327
352
|
'ds.commands': '指令',
|
|
328
353
|
'ds.cmdCompact': '/compact 压缩对话历史', 'ds.cmdExport': '/export 导出会话日志 ZIP',
|
|
329
354
|
'ds.cmdFeedback': '/feedback 反馈当前会话', 'ds.cmdGoal': '/goal 设置/查看任务目标',
|
|
330
355
|
'ds.cmdPermission': '/permission 切换权限预设', 'ds.cmdPlan': '/plan 进入/退出计划模式',
|
|
331
|
-
'ds.fsUp': '上级', 'ds.fsRefresh': '刷新', 'ds.fsEmpty': '目录为空',
|
|
356
|
+
'ds.fsUp': '上级', 'ds.fsNewWorkspace': '新建工作区', 'ds.fsRefresh': '刷新', 'ds.fsEmpty': '目录为空',
|
|
357
|
+
'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': '工作区已创建,但新会话未能打开',
|
|
332
358
|
'ds.groupGeneral': '通用', 'ds.groupGeneralDesc': '工具调用、预设提示词',
|
|
333
359
|
'ds.groupServers': '服务器', 'ds.groupServersDesc': '服务器地址',
|
|
334
360
|
'ds.groupNotify': '通知', 'ds.groupNotifyDesc': '通知与提醒',
|
|
@@ -365,7 +391,7 @@
|
|
|
365
391
|
'ds.approvalTitle': '工具审批', 'ds.questionNotify': '需要回答',
|
|
366
392
|
'ds.approvalBody': '{tool} · {server}', 'ds.approvalReason': '原因:{reason}',
|
|
367
393
|
'ds.allowed': '已允许', 'ds.rejected': '已拒绝', 'ds.ignored': '已忽略',
|
|
368
|
-
'ds.stale': '请求已失效',
|
|
394
|
+
'ds.stale': '请求已失效', 'ds.pendingSubmitFailed': '提交失败:{msg}',
|
|
369
395
|
'ds.statsTodayTokens': '今日 Token', 'ds.statsTodayCost': '今日费用', 'ds.statsPeakShare': '高峰占比',
|
|
370
396
|
'ds.statsInput': '未缓存输入', 'ds.statsCacheRead': '缓存命中', 'ds.statsCacheWrite': '缓存写入', 'ds.statsOutput': '输出',
|
|
371
397
|
'ds.statsPeak': '高峰', 'ds.statsOff': '空闲', 'ds.statsDays': '近 {n} 天',
|
|
@@ -416,14 +442,16 @@
|
|
|
416
442
|
'menu.modelTitle': '模型切换', 'menu.effortTitle': '思考深度',
|
|
417
443
|
},
|
|
418
444
|
en: {
|
|
419
|
-
'ds.repo': 'GitHub repo', 'ds.newSession': '+ New session', 'ds.sessions': 'Sessions',
|
|
445
|
+
'ds.repo': 'GitHub repo', 'ds.newSession': '+ New session', 'ds.newWorkspace': 'New workspace', 'ds.sessions': 'Sessions',
|
|
446
|
+
'ds.sortLabel': 'Session sort', 'ds.sortTime': 'By time', 'ds.sortWorkspace': 'By workspace', 'ds.workspace': 'Workspace', 'ds.workspaceUnknown': 'No workspace',
|
|
420
447
|
'ds.files': 'Files', 'ds.settings': 'Settings', 'ds.stats': 'Stats', 'ds.menu': 'Menu',
|
|
421
448
|
'ds.send': 'Send', 'ds.composerPlaceholder': 'Type a message, Enter to send…', 'ds.presets': 'Presets',
|
|
422
449
|
'ds.commands': 'Commands',
|
|
423
450
|
'ds.cmdCompact': '/compact Compress conversation history', 'ds.cmdExport': '/export Export session log ZIP',
|
|
424
451
|
'ds.cmdFeedback': '/feedback Feedback current session', 'ds.cmdGoal': '/goal Set/view task goal',
|
|
425
452
|
'ds.cmdPermission': '/permission Switch permission preset', 'ds.cmdPlan': '/plan Enter/exit plan mode',
|
|
426
|
-
'ds.fsUp': 'Up', 'ds.fsRefresh': 'Refresh', 'ds.fsEmpty': 'Empty directory',
|
|
453
|
+
'ds.fsUp': 'Up', 'ds.fsNewWorkspace': 'New workspace', 'ds.fsRefresh': 'Refresh', 'ds.fsEmpty': 'Empty directory',
|
|
454
|
+
'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',
|
|
427
455
|
'ds.groupGeneral': 'General', 'ds.groupGeneralDesc': 'Tool calls, prompt presets',
|
|
428
456
|
'ds.groupServers': 'Servers', 'ds.groupServersDesc': 'Server address',
|
|
429
457
|
'ds.groupNotify': 'Notifications', 'ds.groupNotifyDesc': 'Notifications & reminders',
|
|
@@ -460,7 +488,7 @@
|
|
|
460
488
|
'ds.approvalTitle': 'Tool approval', 'ds.questionNotify': 'Question',
|
|
461
489
|
'ds.approvalBody': '{tool} · {server}', 'ds.approvalReason': 'Reason: {reason}',
|
|
462
490
|
'ds.allowed': 'Allowed', 'ds.rejected': 'Rejected', 'ds.ignored': 'Ignored',
|
|
463
|
-
'ds.stale': 'Request is stale',
|
|
491
|
+
'ds.stale': 'Request is stale', 'ds.pendingSubmitFailed': 'Submit failed: {msg}',
|
|
464
492
|
'ds.statsTodayTokens': 'Tokens today', 'ds.statsTodayCost': 'Cost today', 'ds.statsPeakShare': 'Peak share',
|
|
465
493
|
'ds.statsInput': 'Uncached input', 'ds.statsCacheRead': 'Cache read', 'ds.statsCacheWrite': 'Cache write', 'ds.statsOutput': 'Output',
|
|
466
494
|
'ds.statsPeak': 'Peak', 'ds.statsOff': 'Off-peak', 'ds.statsDays': 'Last {n} days',
|