dsh-remote-plugin 0.6.13 → 0.6.14
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.cjs +59 -4
- package/package.json +1 -1
- package/public/announcements.json +25 -0
- package/public/app.js +313 -31
- package/public/desktop/desktop.css +20 -0
- package/public/desktop/desktop.html +8 -4
- package/public/desktop/desktop.js +124 -24
- package/public/index.html +32 -12
- package/public/md.js +71 -1
- package/public/styles.css +27 -0
- package/public/transcribe-core.js +74 -0
- package/public/update.json +8 -8
- package/public/version.json +1 -1
|
@@ -146,8 +146,10 @@
|
|
|
146
146
|
<!-- 会话详情 -->
|
|
147
147
|
<section id="view-chat" class="ds-view">
|
|
148
148
|
<div id="session-cards" class="ds-session-cards"></div>
|
|
149
|
+
<div id="queue-dock" class="ds-queue-dock hidden" aria-live="polite"></div>
|
|
149
150
|
<div id="history" class="ds-history" aria-live="polite"></div>
|
|
150
151
|
<div class="ds-composer">
|
|
152
|
+
<div id="composer-status" class="ds-composer-status hidden" role="status" aria-live="polite"><span class="ds-composer-status-dot" aria-hidden="true"></span><span data-i18n="ds.composerRunning">运行中…</span></div>
|
|
151
153
|
<textarea id="composer" rows="1" data-i18n-placeholder="ds.composerPlaceholder" placeholder="输入消息…"></textarea>
|
|
152
154
|
<div class="ds-composer-actions">
|
|
153
155
|
<div class="ds-composer-left">
|
|
@@ -434,7 +436,7 @@
|
|
|
434
436
|
'ds.attention': '需要你处理', 'ds.runtime': '运行指标', 'ds.recentActivity': '近期活动', 'ds.dshVersion': 'DSH 版本', 'ds.gatewayStatus': '网关状态', 'ds.activeSessions': '活跃会话', 'ds.connectionMode': '连接模式', 'ds.liveWs': '实时 WS', 'ds.poll': '降级轮询', 'ds.running': '运行中', 'ds.noReason': '无说明', 'ds.noActivity': '暂无后台活动', 'ds.noSessions': '暂无会话', 'ds.refreshOverview': '刷新总览', 'ds.pendingCount': '{n} 项待处理', 'ds.activeCount': '{n} 个活跃会话', 'ds.nothingPending': '暂无事项', 'ds.action.openSession': '打开最近会话', 'ds.action.newSession': '新建会话', 'ds.action.attention': '处理待办', 'ds.action.connect': '去设置连接', 'ds.action.refresh': '重新检查连接',
|
|
435
437
|
'ds.sortLabel': '会话排序', 'ds.sortTime': '按时间', 'ds.sortWorkspace': '按工作区', 'ds.workspace': '工作区', 'ds.workspaceUnknown': '未指定工作区',
|
|
436
438
|
'ds.files': '文件传输', 'ds.settings': '设置', 'ds.stats': '统计', 'ds.menu': '导航',
|
|
437
|
-
'ds.send': '发送', 'ds.composerPlaceholder': '输入消息,Enter 发送…', 'ds.presets': '预设',
|
|
439
|
+
'ds.send': '发送', 'ds.composerPlaceholder': '输入消息,Enter 发送…', 'ds.composerRunning': '运行中…', 'ds.presets': '预设',
|
|
438
440
|
'ds.commands': '指令',
|
|
439
441
|
'ds.cmdCompact': '/compact 压缩对话历史', 'ds.cmdExport': '/export 导出会话日志 ZIP',
|
|
440
442
|
'ds.cmdFeedback': '/feedback 反馈当前会话', 'ds.cmdGoal': '/goal 设置/查看任务目标',
|
|
@@ -520,8 +522,9 @@
|
|
|
520
522
|
'goal.actionFailed': '目标操作失败', 'goal.actionSubmitted': '目标操作已提交',
|
|
521
523
|
'goal.cannotEmpty': '目标不能为空', 'goal.updateFailed': '更新失败', 'goal.updated': '目标已更新', 'goal.editPrompt': '修改目标:',
|
|
522
524
|
'todos.title': '任务清单',
|
|
523
|
-
'subagent.title': '子代理', 'subagent.diagnostic': '诊断项', 'subagent.running': '· 运行中', 'subagent.interrupt': '中断',
|
|
525
|
+
'subagent.title': '子代理', 'subagent.count': '子代理 · {n} 个', 'subagent.expand': '展开子代理', 'subagent.collapse': '收起子代理', 'subagent.diagnostic': '诊断项', 'subagent.running': '· 运行中', 'subagent.interrupt': '中断',
|
|
524
526
|
'subagent.confirmInterrupt': '中断这个子代理当前回合?', 'subagent.interruptFailed': '中断失败', 'subagent.interruptSubmitted': '中断请求已提交',
|
|
527
|
+
'queue.title': '排队消息', 'queue.steer': '插话', 'queue.steerUnavailable': '仅运行中可插话', 'queue.steerSubmitted': '插话请求已提交', 'queue.steerFailed': '插话失败:{msg}', 'queue.image': '图片消息',
|
|
525
528
|
'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
|
|
526
529
|
'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
|
|
527
530
|
'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}', 'models.effortLow': '低', 'models.effortHigh': '高', 'models.effortMax': '极高', 'models.effortOff': '关闭', 'models.effortCustomHint': '该路由未公布档位,按 DSH 兼容值尝试;不支持时不会更改当前设置。',
|
|
@@ -535,7 +538,7 @@
|
|
|
535
538
|
'ds.attention': 'Needs attention', 'ds.runtime': 'Runtime metrics', 'ds.recentActivity': 'Recent activity', 'ds.dshVersion': 'DSH version', 'ds.gatewayStatus': 'Gateway status', 'ds.activeSessions': 'Active sessions', 'ds.connectionMode': 'Connection mode', 'ds.liveWs': 'Live WS', 'ds.poll': 'Degraded polling', 'ds.running': 'Running', 'ds.noReason': 'No reason given', 'ds.noActivity': 'No background activity', 'ds.noSessions': 'No sessions yet', 'ds.refreshOverview': 'Refresh overview', 'ds.pendingCount': '{n} pending', 'ds.activeCount': '{n} active sessions', 'ds.nothingPending': 'Nothing pending', 'ds.action.openSession': 'Open recent session', 'ds.action.newSession': 'New session', 'ds.action.attention': 'Review pending', 'ds.action.connect': 'Set up connection', 'ds.action.refresh': 'Check connection',
|
|
536
539
|
'ds.sortLabel': 'Session sort', 'ds.sortTime': 'By time', 'ds.sortWorkspace': 'By workspace', 'ds.workspace': 'Workspace', 'ds.workspaceUnknown': 'No workspace',
|
|
537
540
|
'ds.files': 'Files', 'ds.settings': 'Settings', 'ds.stats': 'Stats', 'ds.menu': 'Menu',
|
|
538
|
-
'ds.send': 'Send', 'ds.composerPlaceholder': 'Type a message, Enter to send…', 'ds.presets': 'Presets',
|
|
541
|
+
'ds.send': 'Send', 'ds.composerPlaceholder': 'Type a message, Enter to send…', 'ds.composerRunning': 'Running…', 'ds.presets': 'Presets',
|
|
539
542
|
'ds.commands': 'Commands',
|
|
540
543
|
'ds.cmdCompact': '/compact Compress conversation history', 'ds.cmdExport': '/export Export session log ZIP',
|
|
541
544
|
'ds.cmdFeedback': '/feedback Feedback current session', 'ds.cmdGoal': '/goal Set/view task goal',
|
|
@@ -621,8 +624,9 @@
|
|
|
621
624
|
'goal.actionFailed': 'Goal action failed', 'goal.actionSubmitted': 'Goal action submitted',
|
|
622
625
|
'goal.cannotEmpty': 'Goal cannot be empty', 'goal.updateFailed': 'Update failed', 'goal.updated': 'Goal updated', 'goal.editPrompt': 'Edit goal:',
|
|
623
626
|
'todos.title': 'Todos',
|
|
624
|
-
'subagent.title': 'Subagents', 'subagent.diagnostic': 'Diagnostic', 'subagent.running': '· running', 'subagent.interrupt': 'Interrupt',
|
|
627
|
+
'subagent.title': 'Subagents', 'subagent.count': 'Subagents · {n}', 'subagent.expand': 'Expand subagents', 'subagent.collapse': 'Collapse subagents', 'subagent.diagnostic': 'Diagnostic', 'subagent.running': '· running', 'subagent.interrupt': 'Interrupt',
|
|
625
628
|
'subagent.confirmInterrupt': 'Interrupt this subagent\'s current turn?', 'subagent.interruptFailed': 'Interrupt failed', 'subagent.interruptSubmitted': 'Interrupt requested',
|
|
629
|
+
'queue.title': 'Queued messages', 'queue.steer': 'Interject', 'queue.steerUnavailable': 'Only available while running', 'queue.steerSubmitted': 'Interjection requested', 'queue.steerFailed': 'Interjection failed: {msg}', 'queue.image': 'Image message',
|
|
626
630
|
'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
|
|
627
631
|
'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
|
|
628
632
|
'models.effortFailed': 'Failed to switch reasoning effort', 'models.effortSwitched': 'Reasoning effort: {effort}', 'models.effortLow': 'Low', 'models.effortHigh': 'High', 'models.effortMax': 'Max', 'models.effortOff': 'Off', 'models.effortCustomHint': 'This route does not publish effort metadata. DSH compatibility values are tried; unsupported values leave the current setting unchanged.',
|
|
@@ -13,14 +13,16 @@ const LS = {
|
|
|
13
13
|
}
|
|
14
14
|
const CLIENT_ID = (() => {
|
|
15
15
|
try {
|
|
16
|
-
|
|
16
|
+
const key = 'dshRemoteClientIdV2'
|
|
17
|
+
let id = localStorage.getItem(key)
|
|
17
18
|
if (!id) {
|
|
18
19
|
id = (globalThis.crypto?.randomUUID?.() || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`)
|
|
19
|
-
|
|
20
|
+
localStorage.setItem(key, id)
|
|
20
21
|
}
|
|
21
22
|
return id
|
|
22
23
|
} catch { return '' }
|
|
23
24
|
})()
|
|
25
|
+
function clientIdHeaders() { return CLIENT_ID ? { 'x-dsh-remote-client-id': CLIENT_ID } : {} }
|
|
24
26
|
const CAP = window.Capacitor || null
|
|
25
27
|
|
|
26
28
|
/* ---------------- 皮肤 ---------------- */
|
|
@@ -71,6 +73,9 @@ const state = {
|
|
|
71
73
|
history: emptyDesktopHistory(),
|
|
72
74
|
approvals: [],
|
|
73
75
|
questions: [],
|
|
76
|
+
queues: {},
|
|
77
|
+
queueSteering: {},
|
|
78
|
+
sessionTurnTimes: {},
|
|
74
79
|
questionModal: null,
|
|
75
80
|
streamsOk: { mux: false, host: false },
|
|
76
81
|
errCount: 0,
|
|
@@ -84,7 +89,8 @@ const state = {
|
|
|
84
89
|
models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
|
|
85
90
|
wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
|
|
86
91
|
archivedIds: [],
|
|
87
|
-
view: 'sessions'
|
|
92
|
+
view: 'sessions',
|
|
93
|
+
subagentExpandedSession: ''
|
|
88
94
|
}
|
|
89
95
|
const streams = {}
|
|
90
96
|
let pollTimer = null
|
|
@@ -545,7 +551,7 @@ async function getWsTicket() {
|
|
|
545
551
|
if (activeGatewayCapability('wsTicket') === false) throw new Error('ws ticket unsupported')
|
|
546
552
|
const res = await fetch(apiUrl('/api/ws-ticket'), {
|
|
547
553
|
method: 'POST',
|
|
548
|
-
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web' }
|
|
554
|
+
headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
549
555
|
})
|
|
550
556
|
if (!res.ok) throw new Error('ws ticket HTTP ' + res.status)
|
|
551
557
|
const data = await res.json()
|
|
@@ -558,7 +564,7 @@ async function getWsTicket() {
|
|
|
558
564
|
async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
559
565
|
const opts = {
|
|
560
566
|
method: 'POST',
|
|
561
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
567
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
562
568
|
body: JSON.stringify({ type: 'client-request', rpcId: uuid(), method, payload })
|
|
563
569
|
}
|
|
564
570
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
@@ -575,7 +581,7 @@ async function rpc(method, payload = {}, timeoutMs = 45000) {
|
|
|
575
581
|
async function respond(rpcId, value) {
|
|
576
582
|
const opts = {
|
|
577
583
|
method: 'POST',
|
|
578
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
584
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
579
585
|
body: JSON.stringify({ type: 'client-response', rpcId, result: { ok: true, value } })
|
|
580
586
|
}
|
|
581
587
|
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
@@ -1094,7 +1100,7 @@ async function pollKind(kind) {
|
|
|
1094
1100
|
let res
|
|
1095
1101
|
try {
|
|
1096
1102
|
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(5000) : undefined
|
|
1097
|
-
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' }
|
|
1103
|
+
const headers = { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
1098
1104
|
res = signal ? await fetch(apiUrl(`/api/events.poll?kind=${kind}&since=${since}`), { signal, headers }) : await fetch(apiUrl(`/api/events.poll?kind=${kind}&since=${since}`), { headers })
|
|
1099
1105
|
} catch { return }
|
|
1100
1106
|
if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
|
|
@@ -1164,6 +1170,7 @@ function onMuxFrame(full) {
|
|
|
1164
1170
|
return
|
|
1165
1171
|
}
|
|
1166
1172
|
if (f.type === 'question/resolved') { state.questions = state.questions.filter(q => q.rpcId !== f.questionRpcId); renderNotifStack(); return }
|
|
1173
|
+
if (f.type === 'session/queue') { state.queues[f.sessionId] = f.items || []; renderQueue(); return }
|
|
1167
1174
|
if (f.type === 'session/projection') { applyProjection(f.sessionId, f.key, f.value, f.seq); return }
|
|
1168
1175
|
if (f.type === 'stream/error') toast(f.error?.message || 'stream error', 'err')
|
|
1169
1176
|
}
|
|
@@ -1173,7 +1180,7 @@ function onHostFrame(full) {
|
|
|
1173
1180
|
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()
|
|
1174
1181
|
if (f.type === 'host/session-status') {
|
|
1175
1182
|
const s = state.byId.get(f.sessionId)
|
|
1176
|
-
if (s) { s.running = f.running; if (state.current === f.sessionId) renderSessions(); renderOverviewDesktop() }
|
|
1183
|
+
if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessions(); renderQueue(); updateComposerStatus() } renderOverviewDesktop() }
|
|
1177
1184
|
}
|
|
1178
1185
|
}
|
|
1179
1186
|
function applyProjection(sessionId, key, value, seq) {
|
|
@@ -1206,6 +1213,15 @@ function goalOf(s) {
|
|
|
1206
1213
|
return p.goal && typeof p.goal === 'object' ? p.goal : p
|
|
1207
1214
|
}
|
|
1208
1215
|
function onSessionEvent(sessionId, event) {
|
|
1216
|
+
if (event?.type === 'turn/start' || event?.type === 'turn/end') {
|
|
1217
|
+
noteSessionTurnTime(sessionId, event)
|
|
1218
|
+
renderSessions()
|
|
1219
|
+
}
|
|
1220
|
+
const session = state.byId.get(sessionId)
|
|
1221
|
+
if (event?.type === 'agent/status' && session) {
|
|
1222
|
+
session.running = !!event.data?.running
|
|
1223
|
+
if (state.current === sessionId) { renderQueue(); updateComposerStatus() }
|
|
1224
|
+
}
|
|
1209
1225
|
if (state.current === sessionId && event) {
|
|
1210
1226
|
const h = state.history
|
|
1211
1227
|
const reasoningChanged = applyReasoningStreamEvent(event)
|
|
@@ -1214,7 +1230,7 @@ function onSessionEvent(sessionId, event) {
|
|
|
1214
1230
|
return
|
|
1215
1231
|
}
|
|
1216
1232
|
const seq = event.seq
|
|
1217
|
-
if (seq != null && !h.seqs.has(seq) && shouldShowEvent(event.type)) {
|
|
1233
|
+
if (seq != null && !h.seqs.has(seq) && shouldShowEvent(event.type, event)) {
|
|
1218
1234
|
h.seqs.add(seq)
|
|
1219
1235
|
h.visible.push({ seq, event })
|
|
1220
1236
|
h.visible.sort((a, b) => a.seq - b.seq)
|
|
@@ -1239,6 +1255,15 @@ function sessionWorkspaceLabel(s) {
|
|
|
1239
1255
|
const cwd = sessionCwd(s)
|
|
1240
1256
|
return cwd || t('ds.workspaceUnknown')
|
|
1241
1257
|
}
|
|
1258
|
+
function sessionSortTime(s) {
|
|
1259
|
+
return Math.max(Number(state.sessionTurnTimes[s?.sessionId]) || 0, Number(s?.updatedAt) || 0, Number(s?.createdAt) || 0)
|
|
1260
|
+
}
|
|
1261
|
+
function noteSessionTurnTime(sessionId, eventOrTime) {
|
|
1262
|
+
const raw = typeof eventOrTime === 'object' ? eventOrTime?.time : eventOrTime
|
|
1263
|
+
const time = Number(raw) > 0 ? Number(raw) : Date.now()
|
|
1264
|
+
if (!sessionId || !Number.isFinite(time)) return
|
|
1265
|
+
state.sessionTurnTimes[sessionId] = Math.max(Number(state.sessionTurnTimes[sessionId]) || 0, time)
|
|
1266
|
+
}
|
|
1242
1267
|
function workspaceDisplayName(label) {
|
|
1243
1268
|
const value = String(label || '').trim()
|
|
1244
1269
|
if (!value || value === t('ds.workspaceUnknown')) return value || t('ds.workspaceUnknown')
|
|
@@ -1253,10 +1278,10 @@ function sortedSessions() {
|
|
|
1253
1278
|
const aw = sessionCwd(a) || '\uffff'
|
|
1254
1279
|
const bw = sessionCwd(b) || '\uffff'
|
|
1255
1280
|
const byWorkspace = aw.localeCompare(bw, undefined, { numeric: true, sensitivity: 'base' })
|
|
1256
|
-
return byWorkspace || ((b
|
|
1281
|
+
return byWorkspace || (sessionSortTime(b) - sessionSortTime(a))
|
|
1257
1282
|
})
|
|
1258
1283
|
}
|
|
1259
|
-
return items.sort((a, b) => (b
|
|
1284
|
+
return items.sort((a, b) => sessionSortTime(b) - sessionSortTime(a))
|
|
1260
1285
|
}
|
|
1261
1286
|
function renderSessions() {
|
|
1262
1287
|
const allItems = sortedSessions()
|
|
@@ -1286,7 +1311,7 @@ function renderSessions() {
|
|
|
1286
1311
|
rows.push(`<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
|
|
1287
1312
|
<span class="ds-session-title">${esc(title)}</span>
|
|
1288
1313
|
<span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
|
|
1289
|
-
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(s
|
|
1314
|
+
<span class="ds-session-meta"><span class="ds-session-dot ${s.running ? 'running' : ''}"></span>${fmtTime(sessionSortTime(s))}</span>
|
|
1290
1315
|
</button>`)
|
|
1291
1316
|
}
|
|
1292
1317
|
return rows.join('')
|
|
@@ -1317,6 +1342,8 @@ async function openSession(id) {
|
|
|
1317
1342
|
$('history').innerHTML = `<div class="ds-empty">${t('ds.historyLoading')}</div>`
|
|
1318
1343
|
renderSessions()
|
|
1319
1344
|
renderSessionCards()
|
|
1345
|
+
renderQueue()
|
|
1346
|
+
updateComposerStatus()
|
|
1320
1347
|
await loadHistory()
|
|
1321
1348
|
}
|
|
1322
1349
|
function closeSession() {
|
|
@@ -1324,6 +1351,8 @@ function closeSession() {
|
|
|
1324
1351
|
state.history = emptyDesktopHistory()
|
|
1325
1352
|
const cards = $('session-cards')
|
|
1326
1353
|
if (cards) cards.innerHTML = ''
|
|
1354
|
+
renderQueue()
|
|
1355
|
+
updateComposerStatus()
|
|
1327
1356
|
showView('view-sessions')
|
|
1328
1357
|
}
|
|
1329
1358
|
async function loadHistory() {
|
|
@@ -1341,9 +1370,10 @@ async function loadHistory() {
|
|
|
1341
1370
|
for (const entry of v.events || []) {
|
|
1342
1371
|
const ev = entry?.event
|
|
1343
1372
|
const seq = ev?.seq
|
|
1373
|
+
if (ev?.type === 'turn/start' || ev?.type === 'turn/end') noteSessionTurnTime(id, ev)
|
|
1344
1374
|
applyReasoningStreamEvent(ev)
|
|
1345
1375
|
if (seq == null || state.history.seqs.has(seq)) continue
|
|
1346
|
-
if (!shouldShowEvent(ev.type)) continue
|
|
1376
|
+
if (!shouldShowEvent(ev.type, ev)) continue
|
|
1347
1377
|
state.history.seqs.add(seq)
|
|
1348
1378
|
state.history.visible.push({ seq, event: ev })
|
|
1349
1379
|
}
|
|
@@ -1360,7 +1390,25 @@ const INTERESTING_EVENTS = new Set([
|
|
|
1360
1390
|
'todo/updated', 'plan/updated', 'question/asked', 'question/resolved',
|
|
1361
1391
|
'approval/asked', 'approval/resolved', 'session/title', 'title'
|
|
1362
1392
|
])
|
|
1363
|
-
function
|
|
1393
|
+
function messageSource(data) {
|
|
1394
|
+
const source = data?.source ?? data?.message?.source
|
|
1395
|
+
return source && typeof source === 'object' ? source : null
|
|
1396
|
+
}
|
|
1397
|
+
function isHumanUserMessage(event) {
|
|
1398
|
+
if (event?.type !== 'user/message') return false
|
|
1399
|
+
const source = messageSource(event.data || {})
|
|
1400
|
+
// Older DSH events may not carry source metadata; keep those visible for compatibility.
|
|
1401
|
+
return !source || source.kind === 'user'
|
|
1402
|
+
}
|
|
1403
|
+
function shouldShowEvent(type, event) {
|
|
1404
|
+
if (!INTERESTING_EVENTS.has(type)) return false
|
|
1405
|
+
if (type === 'user/message' && !isHumanUserMessage(event)) {
|
|
1406
|
+
const data = event?.data || {}
|
|
1407
|
+
const blocks = data.message?.content || data.content || []
|
|
1408
|
+
return systemReminderText(blocks).length > 0
|
|
1409
|
+
}
|
|
1410
|
+
return true
|
|
1411
|
+
}
|
|
1364
1412
|
function reasoningStreamKey(data, index) { return `${data?.turn ?? '?'}:${data?.step ?? '?'}:${index ?? '?'}` }
|
|
1365
1413
|
function applyReasoningStreamEvent(event) {
|
|
1366
1414
|
const h = state.history
|
|
@@ -1430,7 +1478,7 @@ function eventHtml(entry) {
|
|
|
1430
1478
|
const ev = entry.event || {}
|
|
1431
1479
|
const data = ev.data || {}
|
|
1432
1480
|
const type = ev.type || 'event'
|
|
1433
|
-
if (!shouldShowEvent(type)) return ''
|
|
1481
|
+
if (!shouldShowEvent(type, ev)) return ''
|
|
1434
1482
|
if (type === 'user/message' || type === 'assistant/message') {
|
|
1435
1483
|
const msg = data.message || {}
|
|
1436
1484
|
const role = data.role || msg.role || (type.startsWith('user') ? 'user' : 'assistant')
|
|
@@ -1440,6 +1488,7 @@ function eventHtml(entry) {
|
|
|
1440
1488
|
const shown = sysText.length > 400 ? sysText.slice(0, 400) + '…' : sysText
|
|
1441
1489
|
return `<details class="event ds-tool ds-event-detail"><summary>${esc(t('ds.eventSystemReminder'))}</summary><pre>${esc(shown)}</pre></details>`
|
|
1442
1490
|
}
|
|
1491
|
+
if (type === 'user/message' && !isHumanUserMessage(ev)) return ''
|
|
1443
1492
|
const text = blocks.map(blockHtml).join('')
|
|
1444
1493
|
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>`
|
|
1445
1494
|
}
|
|
@@ -1500,13 +1549,19 @@ async function renderSessionCards() {
|
|
|
1500
1549
|
const sub = await safeRpc('subagent.list', { parentSessionId: sessionId }, '')
|
|
1501
1550
|
if (renderGeneration !== sessionCardsRenderGeneration || state.current !== sessionId) return
|
|
1502
1551
|
if (sub?.entries?.length) {
|
|
1552
|
+
const expanded = state.subagentExpandedSession === sessionId
|
|
1553
|
+
const toggleLabel = expanded ? t('subagent.collapse') : t('subagent.expand')
|
|
1503
1554
|
const rows = sub.entries.map(e => {
|
|
1504
1555
|
if (e.kind === 'diagnostic') return `<div class="ds-card-row"><span class="ds-card-k">${t('subagent.diagnostic')}</span><span class="ds-card-v">${esc(e.reason)}</span></div>`
|
|
1505
1556
|
const label = e.label || short(e.id)
|
|
1506
1557
|
const running = e.activity === 'running'
|
|
1507
1558
|
return `<div class="ds-card-row"><span class="ds-card-k">${running ? '▶ ' : ''}${esc(label)}</span><span class="ds-card-v">${esc(e.mode)} ${running ? t('subagent.running') : ''}${e.mode === 'continuable' && running ? ` <button class="ds-mini-btn" data-sub-interrupt="${esc(e.id)}">${t('subagent.interrupt')}</button>` : ''}</span></div>`
|
|
1508
1559
|
}).join('')
|
|
1509
|
-
box.insertAdjacentHTML('beforeend', `<div class="ds-card"><
|
|
1560
|
+
box.insertAdjacentHTML('beforeend', `<div class="ds-card ds-subagent-card"><button type="button" class="ds-subagent-toggle" data-subagent-toggle aria-expanded="${expanded}" aria-label="${esc(toggleLabel)}" title="${esc(toggleLabel)}"><span class="ds-card-title">${esc(t('subagent.count', { n: sub.entries.length }))}</span><span class="ds-subagent-toggle-icon" aria-hidden="true">${expanded ? '⌃' : '⌄'}</span></button><div class="ds-subagent-list${expanded ? '' : ' hidden'}">${rows}</div></div>`)
|
|
1561
|
+
box.querySelector('[data-subagent-toggle]')?.addEventListener('click', () => {
|
|
1562
|
+
state.subagentExpandedSession = expanded ? '' : sessionId
|
|
1563
|
+
renderSessionCards()
|
|
1564
|
+
})
|
|
1510
1565
|
box.querySelectorAll('[data-sub-interrupt]').forEach(btn =>
|
|
1511
1566
|
btn.addEventListener('click', () => interruptSubagent(btn.dataset.subInterrupt)))
|
|
1512
1567
|
}
|
|
@@ -1569,7 +1624,7 @@ async function runSlashCommand(text) {
|
|
|
1569
1624
|
: undefined
|
|
1570
1625
|
const res = await fetch(apiUrl('/remote/api/command'), {
|
|
1571
1626
|
method: 'POST',
|
|
1572
|
-
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' },
|
|
1627
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() },
|
|
1573
1628
|
body: JSON.stringify({ sessionId: state.current, line: clean }),
|
|
1574
1629
|
...(signal ? { signal } : {})
|
|
1575
1630
|
})
|
|
@@ -1595,7 +1650,52 @@ async function sendMessage() {
|
|
|
1595
1650
|
mode: 'queue',
|
|
1596
1651
|
content: [{ type: 'text', text }]
|
|
1597
1652
|
}, '')
|
|
1598
|
-
if (v) toast(t('ds.toastSent'), 'ok')
|
|
1653
|
+
if (v) { noteSessionTurnTime(state.current, Date.now()); renderSessions(); toast(t('ds.toastSent'), 'ok') }
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function updateComposerStatus() {
|
|
1657
|
+
const status = $('composer-status')
|
|
1658
|
+
if (!status) return
|
|
1659
|
+
status.classList.toggle('hidden', !state.byId.get(state.current)?.running)
|
|
1660
|
+
}
|
|
1661
|
+
function queuePreview(item) {
|
|
1662
|
+
const blocks = item?.message?.content || item?.content || []
|
|
1663
|
+
const text = Array.isArray(blocks)
|
|
1664
|
+
? blocks.filter(block => block?.type === 'text').map(block => String(block.text || '')).join(' ').trim()
|
|
1665
|
+
: ''
|
|
1666
|
+
return text || (Array.isArray(blocks) && blocks.some(block => block?.type === 'image') ? t('queue.image') : '…')
|
|
1667
|
+
}
|
|
1668
|
+
async function steerQueueItem(itemId) {
|
|
1669
|
+
const sessionId = state.current
|
|
1670
|
+
const key = `${sessionId}:${itemId}`
|
|
1671
|
+
const s = state.byId.get(sessionId)
|
|
1672
|
+
if (!sessionId || !s?.running || state.queueSteering[key]) return
|
|
1673
|
+
state.queueSteering[key] = true
|
|
1674
|
+
renderQueue()
|
|
1675
|
+
try {
|
|
1676
|
+
const v = await safeRpc('session.updateQueue', { sessionId, itemId, action: { kind: 'steer' } }, t('queue.steerFailed', { msg: '' }).replace(/:$/, '').replace(/: $/, ''))
|
|
1677
|
+
if (v?.accepted) toast(t('queue.steerSubmitted'), 'ok')
|
|
1678
|
+
} finally {
|
|
1679
|
+
delete state.queueSteering[key]
|
|
1680
|
+
renderQueue()
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
function renderQueue() {
|
|
1684
|
+
const box = $('queue-dock')
|
|
1685
|
+
if (!box) return
|
|
1686
|
+
const sessionId = state.current
|
|
1687
|
+
const s = state.byId.get(sessionId)
|
|
1688
|
+
const items = (state.queues[sessionId] || []).filter(item => item?.placement === 'queued')
|
|
1689
|
+
box.classList.toggle('hidden', !items.length)
|
|
1690
|
+
box.innerHTML = items.length ? `<div class="ds-queue-dock-head"><span>⌁</span><span>${esc(t('queue.title'))} · ${items.length}</span></div><div class="ds-queue-dock-list">${items.map(item => {
|
|
1691
|
+
const key = `${sessionId}:${item.id}`
|
|
1692
|
+
const busy = !!state.queueSteering[key]
|
|
1693
|
+
return `<div class="ds-queue-dock-item"><span class="ds-queue-dock-preview" title="${esc(queuePreview(item))}">${esc(queuePreview(item))}</span><button type="button" class="ds-mini-btn ds-queue-dock-action" data-queue-steer="${esc(item.id)}" title="${esc(s?.running ? t('queue.steer') : t('queue.steerUnavailable'))}" ${s?.running && !busy ? '' : 'disabled'}>${busy ? '…' : esc(t('queue.steer'))}</button></div>`
|
|
1694
|
+
}).join('')}</div>` : ''
|
|
1695
|
+
box.querySelectorAll('[data-queue-steer]').forEach(button => {
|
|
1696
|
+
button.addEventListener('click', () => steerQueueItem(button.dataset.queueSteer))
|
|
1697
|
+
})
|
|
1698
|
+
updateComposerStatus()
|
|
1599
1699
|
}
|
|
1600
1700
|
|
|
1601
1701
|
/* ---------------- 审批/提问通知卡片栈 ---------------- */
|
|
@@ -1709,7 +1809,7 @@ function fsApiUrl(sub, params = {}) {
|
|
|
1709
1809
|
return u.href
|
|
1710
1810
|
}
|
|
1711
1811
|
function fsHeaders() {
|
|
1712
|
-
return { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' }
|
|
1812
|
+
return { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }
|
|
1713
1813
|
}
|
|
1714
1814
|
function fsParent(p) {
|
|
1715
1815
|
if (!p) return null
|
|
@@ -1837,7 +1937,7 @@ function wbFsParent(p) {
|
|
|
1837
1937
|
return raw.slice(0, index)
|
|
1838
1938
|
}
|
|
1839
1939
|
async function wbGateway(method, pathname, body) {
|
|
1840
|
-
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web' } }
|
|
1940
|
+
const options = { method, headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() } }
|
|
1841
1941
|
if (body !== undefined) {
|
|
1842
1942
|
options.headers['content-type'] = 'application/json'
|
|
1843
1943
|
options.body = JSON.stringify(body)
|
|
@@ -1926,7 +2026,7 @@ function renderWorkbench() {
|
|
|
1926
2026
|
let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
|
|
1927
2027
|
html += projects.length ? projects.map(w => {
|
|
1928
2028
|
const id = String(w.workspaceId || '')
|
|
1929
|
-
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => (b
|
|
2029
|
+
const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => sessionSortTime(b) - sessionSortTime(a))
|
|
1930
2030
|
const open = state.wb.open === id
|
|
1931
2031
|
return `<div class="ds-wb-project ${open ? 'open' : ''}">
|
|
1932
2032
|
<button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
|
|
@@ -2052,7 +2152,7 @@ async function loadStats() {
|
|
|
2052
2152
|
return
|
|
2053
2153
|
}
|
|
2054
2154
|
try {
|
|
2055
|
-
const res = await fetch(apiUrl('/stats/summary?days=7'), { headers: { authorization: 'Bearer ' + state.token } })
|
|
2155
|
+
const res = await fetch(apiUrl('/stats/summary?days=7'), { headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() } })
|
|
2056
2156
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
2057
2157
|
const json = await res.json()
|
|
2058
2158
|
renderStats(json.days || [])
|
|
@@ -2150,7 +2250,7 @@ function renderOverviewDesktop() {
|
|
|
2150
2250
|
|
|
2151
2251
|
const topSessions = topLevelSessions()
|
|
2152
2252
|
const running = topSessions.filter(s => s.running).length
|
|
2153
|
-
const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (
|
|
2253
|
+
const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (sessionSortTime(b) - sessionSortTime(a))).slice(0, 6)
|
|
2154
2254
|
const primary = $('ds-overview-primary-action')
|
|
2155
2255
|
if (primary) {
|
|
2156
2256
|
let action = 'new'
|
|
@@ -2180,7 +2280,7 @@ function renderOverviewDesktop() {
|
|
|
2180
2280
|
$('ds-overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'ds.poll' : 'ds.liveWs') : '—'
|
|
2181
2281
|
$('ds-overview-active-count').textContent = running ? t('ds.activeCount', { n: running }) : ''
|
|
2182
2282
|
$('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)}">
|
|
2183
|
-
<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
|
|
2283
|
+
<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(sessionSortTime(s)))}</span></span><span class="ds-overview-arrow">›</span>
|
|
2184
2284
|
</button>`).join('') : `<div class="ds-overview-empty">${t('ds.noSessions')}</div>`
|
|
2185
2285
|
$('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
|
|
2186
2286
|
}
|
package/public/index.html
CHANGED
|
@@ -100,6 +100,8 @@
|
|
|
100
100
|
|
|
101
101
|
<div id="session-cards" class="cards"></div>
|
|
102
102
|
|
|
103
|
+
<div id="queue-dock" class="queue-dock hidden" aria-live="polite"></div>
|
|
104
|
+
|
|
103
105
|
<div class="section-head small">
|
|
104
106
|
<span data-i18n="session.history">对话</span>
|
|
105
107
|
<span id="history-hint" class="muted"></span>
|
|
@@ -114,6 +116,7 @@
|
|
|
114
116
|
</div>
|
|
115
117
|
|
|
116
118
|
<div id="composer-wrap" class="composer-wrap">
|
|
119
|
+
<div id="composer-status" class="composer-status hidden" role="status" aria-live="polite"><span class="composer-status-dot" aria-hidden="true"></span><span data-i18n="composer.running">运行中…</span></div>
|
|
117
120
|
<div id="composer-menu" class="composer-menu hidden">
|
|
118
121
|
<div class="menu-group">
|
|
119
122
|
<div class="menu-title" data-i18n="menu.commandsTitle">输入指令</div>
|
|
@@ -346,9 +349,9 @@
|
|
|
346
349
|
<button id="btn-server-add" class="mini-btn" data-i18n="settings.add">添加</button>
|
|
347
350
|
</div>
|
|
348
351
|
<div class="setting-row">
|
|
349
|
-
<div><div class="setting-name" data-i18n="settings.scanTitle">扫码连接</div><div class="setting-desc" data-i18n="settings.scanDesc"
|
|
352
|
+
<div><div class="setting-name" data-i18n="settings.scanTitle">扫码连接</div><div class="setting-desc" data-i18n="settings.scanDesc">实时相机识别二维码,也可拍照/相册本地解码;无需谷歌服务</div></div>
|
|
350
353
|
<div class="setting-actions">
|
|
351
|
-
<button id="btn-scan-camera" class="mini-btn" data-i18n="scan.
|
|
354
|
+
<button id="btn-scan-camera" class="mini-btn" data-i18n="scan.promptLive">实时扫码</button>
|
|
352
355
|
<button id="btn-scan-gallery" class="mini-btn" data-i18n="scan.promptGallery">从相册选择</button>
|
|
353
356
|
</div>
|
|
354
357
|
</div>
|
|
@@ -717,6 +720,21 @@
|
|
|
717
720
|
</div>
|
|
718
721
|
</div>
|
|
719
722
|
|
|
723
|
+
<!-- 实时扫码模态 -->
|
|
724
|
+
<div id="modal-scan-live" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="scan-live-title">
|
|
725
|
+
<div class="modal-card scan-live-card">
|
|
726
|
+
<div id="scan-live-title" class="modal-title" data-i18n="scan.liveTitle">实时扫码连接</div>
|
|
727
|
+
<div class="scan-live-preview">
|
|
728
|
+
<video id="scan-live-video" autoplay playsinline muted></video>
|
|
729
|
+
<div class="scan-live-frame" aria-hidden="true"><span></span></div>
|
|
730
|
+
</div>
|
|
731
|
+
<div id="scan-live-status" class="scan-live-status" data-i18n="scan.liveHint">将二维码放入框内,识别后自动连接</div>
|
|
732
|
+
<div class="modal-actions">
|
|
733
|
+
<button id="scan-live-cancel" class="btn subtle" data-i18n="scan.liveCancel">取消</button>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
</div>
|
|
737
|
+
|
|
720
738
|
<!-- 更新内容模态 -->
|
|
721
739
|
<div id="modal-notes" class="modal hidden" role="dialog" aria-modal="true">
|
|
722
740
|
<div class="modal-card">
|
|
@@ -852,8 +870,9 @@
|
|
|
852
870
|
'goal.actionFailed': '目标操作失败', 'goal.actionSubmitted': '目标操作已提交',
|
|
853
871
|
'goal.cannotEmpty': '目标不能为空', 'goal.updateFailed': '更新失败', 'goal.updated': '目标已更新',
|
|
854
872
|
'todos.title': '任务清单',
|
|
855
|
-
'subagent.title': '子代理', 'subagent.diagnostic': '诊断项', 'subagent.running': '· 运行中', 'subagent.interrupt': '中断',
|
|
873
|
+
'subagent.title': '子代理', 'subagent.count': '子代理 · {n} 个', 'subagent.expand': '展开子代理', 'subagent.collapse': '收起子代理', 'subagent.diagnostic': '诊断项', 'subagent.running': '· 运行中', 'subagent.interrupt': '中断',
|
|
856
874
|
'subagent.confirmInterrupt': '中断这个子代理当前回合?', 'subagent.interruptFailed': '中断失败', 'subagent.interruptSubmitted': '中断请求已提交',
|
|
875
|
+
'queue.title': '排队消息', 'queue.steer': '插话', 'queue.steerUnavailable': '仅运行中可插话', 'queue.steerSubmitted': '插话请求已提交', 'queue.steerFailed': '插话失败:{msg}', 'queue.image': '图片消息',
|
|
857
876
|
'send.failed': '发送失败', 'send.commandSent': '指令已发送', 'send.sent': '已发送', 'send.imageSent': '图片已发送', 'send.commandExecuted': '命令已执行',
|
|
858
877
|
'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
|
|
859
878
|
'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
|
|
@@ -864,7 +883,7 @@
|
|
|
864
883
|
'menu.permission': '/permission 切换权限预设', 'menu.plan': '/plan 进入/退出计划模式',
|
|
865
884
|
'perm.readOnly': 'read-only', 'perm.workspaceWrite': 'workspace-write', 'perm.dangerFullAccess': 'danger-full-access',
|
|
866
885
|
'menu.modelTitle': '模型切换', 'menu.refresh': '刷新', 'menu.modelAuto': '点开时自动加载', 'menu.effortTitle': '思考深度',
|
|
867
|
-
'composer.placeholder': '给 DSH 发消息…', 'composer.send': '发送', 'composer.fullscreen': '全屏编辑', 'composer.exitFullscreen': '退出全屏编辑', 'composer.addImage': '添加图片', 'composer.takePhoto': '拍照', 'composer.choosePhoto': '相册', 'composer.imageAdded': '已添加图片', 'composer.imageRemoved': '已移除图片', 'composer.removeImage': '移除图片', 'composer.imageUnsupported': '仅支持 PNG、JPEG、WebP 或 GIF 图片', 'composer.imageTooLarge': '单张图片不能超过 {size}', 'composer.imageLimit': '最多添加 {count} 张图片', 'composer.imageSlashUnsupported': '带图片时不能发送斜杠命令', 'composer.imageReadFailed': '图片读取失败:{msg}',
|
|
886
|
+
'composer.placeholder': '给 DSH 发消息…', 'composer.running': '运行中…', 'composer.send': '发送', 'composer.fullscreen': '全屏编辑', 'composer.exitFullscreen': '退出全屏编辑', 'composer.addImage': '添加图片', 'composer.takePhoto': '拍照', 'composer.choosePhoto': '相册', 'composer.imageAdded': '已添加图片', 'composer.imageRemoved': '已移除图片', 'composer.removeImage': '移除图片', 'composer.imageUnsupported': '仅支持 PNG、JPEG、WebP 或 GIF 图片', 'composer.imageTooLarge': '单张图片不能超过 {size}', 'composer.imageLimit': '最多添加 {count} 张图片', 'composer.imageSlashUnsupported': '带图片时不能发送斜杠命令', 'composer.imageReadFailed': '图片读取失败:{msg}',
|
|
868
887
|
'pending.title': '待处理', 'pending.count': '{n} 项', 'pending.approvalTitle': '🔧 {tool} 请求批准', 'pending.noReason': '无说明',
|
|
869
888
|
'pending.allow': '允许', 'pending.reject': '拒绝', 'pending.question': '❓ {question}',
|
|
870
889
|
'pending.questionCount': '共 {n} 个问题', 'pending.answer': '去回答', 'pending.empty': '暂无待处理事项',
|
|
@@ -903,10 +922,10 @@
|
|
|
903
922
|
'update.appVersion': 'App 版本', 'update.gatewayVersion': '网关版本', 'update.gatewayAheadHint': '建议同步更新 App,以保持与网关功能一致。',
|
|
904
923
|
'update.remindLater': '稍后提醒', 'update.checkNow': '立即检查更新',
|
|
905
924
|
'notes.title': '更新内容', 'notes.close': '关闭', 'notes.page': '版本号 {current}/{total}',
|
|
906
|
-
'scan.imageLoadFailed': '图片加载失败', 'scan.decodeUnsupported': '当前设备不支持图片解码',
|
|
925
|
+
'scan.imageLoadFailed': '图片加载失败', 'scan.decodeUnsupported': '当前设备不支持图片解码', 'scan.liveTitle': '实时扫码连接', 'scan.liveHint': '将二维码放入框内,识别后自动连接', 'scan.liveStarting': '正在打开摄像头…', 'scan.liveCancel': '取消',
|
|
907
926
|
'scan.browserHint': '浏览器请打开主机管理页,用手机相机扫码', 'scan.unsupported': '当前 App 版本不支持拍照扫码,请先更新 App',
|
|
908
927
|
'scan.permissionDenied': '未授予相机权限',
|
|
909
|
-
'scan.promptHeader': '扫描 DSH Remote 配对二维码', 'scan.promptPhoto': '拍照扫描', 'scan.promptGallery': '从相册选择',
|
|
928
|
+
'scan.promptHeader': '扫描 DSH Remote 配对二维码', 'scan.promptLive': '实时扫码', 'scan.promptPhoto': '拍照扫描', 'scan.promptGallery': '从相册选择',
|
|
910
929
|
'scan.noPhoto': '没有拿到图片,请重试', 'scan.noQr': '未识别到二维码,请对准后重试',
|
|
911
930
|
'scan.paired': '配对成功,正在连接', 'scan.pairedLink': '已通过二维码配对', 'scan.notPair': '这不是 DSH Remote 的配对二维码',
|
|
912
931
|
'scan.cancelled': '已取消扫码', 'scan.failed': '扫码失败:{msg}',
|
|
@@ -918,7 +937,7 @@
|
|
|
918
937
|
'settings.donate': '赞赏支持', 'settings.donateDesc': '如果 dsh-remote 帮到了你,欢迎赞赏支持开发 ☕', 'settings.donateThanks': '感谢你的支持 ☕',
|
|
919
938
|
'settings.back': '‹ 返回',
|
|
920
939
|
'settings.servers': '服务器地址(可多个)', 'settings.speedTest': '测速', 'settings.add': '添加',
|
|
921
|
-
'settings.scanTitle': '扫码连接', 'settings.scanDesc': '
|
|
940
|
+
'settings.scanTitle': '扫码连接', 'settings.scanDesc': '实时相机识别二维码,也可拍照/相册本地解码;无需谷歌服务', 'settings.scan': '扫码',
|
|
922
941
|
'settings.notifyTitle': '通知', 'settings.notifyDesc': '收到审批/提问时推送', 'settings.announcementHistoryTitle': '历史公告', 'settings.announcementHistoryDesc': '查看以前收到的公告', 'settings.announcementHistory': '查看记录',
|
|
923
942
|
'settings.testNotifyTitle': '测试通知', 'settings.testNotifyDesc': '立即排一条 3 秒后的本地通知,用于排查系统通知限制', 'settings.testNotify': '发送测试通知',
|
|
924
943
|
'settings.testNotifySent': '测试通知已排定,3 秒后到达', 'settings.testNotifyFailed': '发送测试通知失败:{msg}', 'settings.testNotifyUnavailable': '测试通知仅在 App 内可用',
|
|
@@ -1063,8 +1082,9 @@
|
|
|
1063
1082
|
'goal.actionFailed': 'Goal action failed', 'goal.actionSubmitted': 'Goal action submitted',
|
|
1064
1083
|
'goal.cannotEmpty': 'Goal cannot be empty', 'goal.updateFailed': 'Update failed', 'goal.updated': 'Goal updated',
|
|
1065
1084
|
'todos.title': 'Todos',
|
|
1066
|
-
'subagent.title': 'Subagents', 'subagent.diagnostic': 'Diagnostic', 'subagent.running': '· running', 'subagent.interrupt': 'Interrupt',
|
|
1085
|
+
'subagent.title': 'Subagents', 'subagent.count': 'Subagents · {n}', 'subagent.expand': 'Expand subagents', 'subagent.collapse': 'Collapse subagents', 'subagent.diagnostic': 'Diagnostic', 'subagent.running': '· running', 'subagent.interrupt': 'Interrupt',
|
|
1067
1086
|
'subagent.confirmInterrupt': 'Interrupt this subagent\'s current turn?', 'subagent.interruptFailed': 'Interrupt failed', 'subagent.interruptSubmitted': 'Interrupt requested',
|
|
1087
|
+
'queue.title': 'Queued messages', 'queue.steer': 'Interject', 'queue.steerUnavailable': 'Only available while running', 'queue.steerSubmitted': 'Interjection requested', 'queue.steerFailed': 'Interjection failed: {msg}', 'queue.image': 'Image message',
|
|
1068
1088
|
'send.failed': 'Send failed', 'send.commandSent': 'Command sent', 'send.sent': 'Sent', 'send.imageSent': 'Image sent', 'send.commandExecuted': 'Command executed',
|
|
1069
1089
|
'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
|
|
1070
1090
|
'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
|
|
@@ -1075,7 +1095,7 @@
|
|
|
1075
1095
|
'menu.permission': '/permission Switch permission preset', 'menu.plan': '/plan Enter/exit plan mode',
|
|
1076
1096
|
'perm.readOnly': 'read-only', 'perm.workspaceWrite': 'workspace-write', 'perm.dangerFullAccess': 'danger-full-access',
|
|
1077
1097
|
'menu.modelTitle': 'Model', 'menu.refresh': 'Refresh', 'menu.modelAuto': 'Auto-loads when opened', 'menu.effortTitle': 'Reasoning effort',
|
|
1078
|
-
'composer.placeholder': 'Message DSH…', 'composer.send': 'Send', 'composer.fullscreen': 'Fullscreen editor', 'composer.exitFullscreen': 'Exit fullscreen editor', 'composer.addImage': 'Add image', 'composer.takePhoto': 'Camera', 'composer.choosePhoto': 'Gallery', 'composer.imageAdded': 'Image added', 'composer.imageRemoved': 'Image removed', 'composer.removeImage': 'Remove image', 'composer.imageUnsupported': 'Only PNG, JPEG, WebP or GIF images are supported', 'composer.imageTooLarge': 'Each image must be under {size}', 'composer.imageLimit': 'You can add up to {count} images', 'composer.imageSlashUnsupported': 'Slash commands cannot include images', 'composer.imageReadFailed': 'Could not read image: {msg}',
|
|
1098
|
+
'composer.placeholder': 'Message DSH…', 'composer.running': 'Running…', 'composer.send': 'Send', 'composer.fullscreen': 'Fullscreen editor', 'composer.exitFullscreen': 'Exit fullscreen editor', 'composer.addImage': 'Add image', 'composer.takePhoto': 'Camera', 'composer.choosePhoto': 'Gallery', 'composer.imageAdded': 'Image added', 'composer.imageRemoved': 'Image removed', 'composer.removeImage': 'Remove image', 'composer.imageUnsupported': 'Only PNG, JPEG, WebP or GIF images are supported', 'composer.imageTooLarge': 'Each image must be under {size}', 'composer.imageLimit': 'You can add up to {count} images', 'composer.imageSlashUnsupported': 'Slash commands cannot include images', 'composer.imageReadFailed': 'Could not read image: {msg}',
|
|
1079
1099
|
'pending.title': 'Inbox', 'pending.count': '{n} items', 'pending.approvalTitle': '🔧 {tool} requests approval', 'pending.noReason': 'No reason given',
|
|
1080
1100
|
'pending.allow': 'Allow', 'pending.reject': 'Reject', 'pending.question': '❓ {question}',
|
|
1081
1101
|
'pending.questionCount': '{n} questions', 'pending.answer': 'Answer', 'pending.empty': 'Nothing pending',
|
|
@@ -1114,10 +1134,10 @@
|
|
|
1114
1134
|
'update.appVersion': 'App version', 'update.gatewayVersion': 'Gateway version', 'update.gatewayAheadHint': 'Update the App to keep its features aligned with the gateway.',
|
|
1115
1135
|
'update.remindLater': 'Remind me later', 'update.checkNow': 'Check for updates',
|
|
1116
1136
|
'notes.title': 'What\'s New', 'notes.close': 'Close', 'notes.page': 'Version {current}/{total}',
|
|
1117
|
-
'scan.imageLoadFailed': 'Image failed to load', 'scan.decodeUnsupported': 'This device cannot decode images',
|
|
1137
|
+
'scan.imageLoadFailed': 'Image failed to load', 'scan.decodeUnsupported': 'This device cannot decode images', 'scan.liveTitle': 'Live QR scanner', 'scan.liveHint': 'Place the QR code inside the frame to connect automatically', 'scan.liveStarting': 'Opening camera…', 'scan.liveCancel': 'Cancel',
|
|
1118
1138
|
'scan.browserHint': 'In a browser, open the host admin page and scan with your phone camera', 'scan.unsupported': 'This app version cannot scan, please update first',
|
|
1119
1139
|
'scan.permissionDenied': 'Camera permission not granted',
|
|
1120
|
-
'scan.promptHeader': 'Scan DSH Remote pairing QR', 'scan.promptPhoto': 'Take photo', 'scan.promptGallery': 'Choose from gallery',
|
|
1140
|
+
'scan.promptHeader': 'Scan DSH Remote pairing QR', 'scan.promptLive': 'Live scan', 'scan.promptPhoto': 'Take photo', 'scan.promptGallery': 'Choose from gallery',
|
|
1121
1141
|
'scan.noPhoto': 'No image received, try again', 'scan.noQr': 'No QR code detected, aim and try again',
|
|
1122
1142
|
'scan.paired': 'Paired, connecting', 'scan.pairedLink': 'Paired via QR code', 'scan.notPair': 'Not a DSH Remote pairing QR code',
|
|
1123
1143
|
'scan.cancelled': 'Scan cancelled', 'scan.failed': 'Scan failed: {msg}',
|
|
@@ -1129,7 +1149,7 @@
|
|
|
1129
1149
|
'settings.donate': 'Support', 'settings.donateDesc': 'If dsh-remote has been helpful to you, feel free to support development with a coffee ☕', 'settings.donateThanks': 'Thanks for your support ☕',
|
|
1130
1150
|
'settings.back': '‹ Back',
|
|
1131
1151
|
'settings.servers': 'Servers (one or more)', 'settings.speedTest': 'Test', 'settings.add': 'Add',
|
|
1132
|
-
'settings.scanTitle': 'Scan to pair', 'settings.scanDesc': '
|
|
1152
|
+
'settings.scanTitle': 'Scan to pair', 'settings.scanDesc': 'Use the live camera or photo/gallery QR decoding; no Google services required', 'settings.scan': 'Scan',
|
|
1133
1153
|
'settings.notifyTitle': 'Notifications', 'settings.notifyDesc': 'Push on approvals / questions', 'settings.announcementHistoryTitle': 'Announcement history', 'settings.announcementHistoryDesc': 'Review announcements you received before', 'settings.announcementHistory': 'View history',
|
|
1134
1154
|
'settings.testNotifyTitle': 'Test notification', 'settings.testNotifyDesc': 'Schedule a local notification in 3 seconds to verify the notification pipeline', 'settings.testNotify': 'Send test notification',
|
|
1135
1155
|
'settings.testNotifySent': 'Test notification scheduled, arriving in 3s', 'settings.testNotifyFailed': 'Failed to send test notification: {msg}', 'settings.testNotifyUnavailable': 'Test notification is only available in the app',
|