dsh-remote-plugin 0.6.21 → 0.6.23

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.
@@ -79,6 +79,10 @@ const state = {
79
79
  questions: [],
80
80
  queues: {},
81
81
  queueSteering: {},
82
+ compactions: {},
83
+ pendingCommands: {},
84
+ compactionPollTimer: null,
85
+ compactionClockTimer: null,
82
86
  sessionTurnTimes: {},
83
87
  questionModal: null,
84
88
  streamsOk: { mux: false, host: false },
@@ -381,6 +385,7 @@ function openFeedbackModal() {
381
385
  document.querySelectorAll('#fb-chips .ds-fb-chip').forEach(b => b.classList.toggle('current', b.dataset.fbType === 'bug'))
382
386
  $('fb-msg').value = ''
383
387
  $('fb-contact').value = ''
388
+ $('fb-include-diagnostics').checked = false
384
389
  $('modal-feedback').classList.remove('hidden')
385
390
  setTimeout(() => $('fb-msg').focus(), 50)
386
391
  }
@@ -503,6 +508,7 @@ async function submitFeedback() {
503
508
  const type = document.querySelector('#fb-chips .ds-fb-chip.current')?.dataset.fbType || 'bug'
504
509
  const message = $('fb-msg').value.trim()
505
510
  const contact = $('fb-contact').value.trim()
511
+ const includeDiagnostics = $('fb-include-diagnostics').checked
506
512
  if (!message) { toast(t('ds.feedbackEmpty'), 'err'); return }
507
513
  if (message.length > 2000) { toast(t('ds.feedbackTooLong'), 'err'); return }
508
514
  const btn = $('fb-submit')
@@ -511,7 +517,7 @@ async function submitFeedback() {
511
517
  const res = await fetch(apiUrl('/feedback'), {
512
518
  method: 'POST',
513
519
  headers: { 'content-type': 'application/json', authorization: 'Bearer ' + state.token },
514
- body: JSON.stringify({ type, message, contact, appVersion: '' })
520
+ body: JSON.stringify({ type, message, contact, appVersion: '', includeDiagnostics })
515
521
  })
516
522
  let json = {}
517
523
  try { json = await res.json() } catch {}
@@ -1370,11 +1376,98 @@ function onHostFrame(full) {
1370
1376
  const f = full.payload
1371
1377
  if (!f) return
1372
1378
  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()
1379
+ if (f.type === 'host/session-activity') {
1380
+ const session = state.byId.get(f.sessionId)
1381
+ if (session) { session.updatedAt = Number(f.updatedAt) || Date.now(); renderSessions(); renderOverviewDesktop() }
1382
+ return
1383
+ }
1373
1384
  if (f.type === 'host/session-status') {
1374
1385
  const s = state.byId.get(f.sessionId)
1375
1386
  if (s) { s.running = f.running; if (state.current === f.sessionId) { renderSessions(); renderQueue(); updateComposerStatus() } renderOverviewDesktop() }
1376
1387
  }
1377
1388
  }
1389
+ function activeCompaction(sessionId = state.current) {
1390
+ const compact = state.compactions[sessionId]
1391
+ return compact?.active === true ? compact : null
1392
+ }
1393
+ function compactElapsed(startedAt) {
1394
+ const seconds = Math.max(0, Math.floor((Date.now() - Number(startedAt || Date.now())) / 1000))
1395
+ const minutes = Math.floor(seconds / 60)
1396
+ const remain = seconds % 60
1397
+ return minutes > 0 ? `${minutes}:${String(remain).padStart(2, '0')}` : `${remain}s`
1398
+ }
1399
+ function setCompactionStatus(sessionId, next) {
1400
+ if (!sessionId) return
1401
+ const previous = state.compactions[sessionId]
1402
+ if (next?.active === true) {
1403
+ state.compactions[sessionId] = {
1404
+ active: true,
1405
+ phase: next.phase || previous?.phase || 'running',
1406
+ command: String(next.command || previous?.command || 'compact'),
1407
+ startedAt: Number(next.startedAt) || previous?.startedAt || Date.now(),
1408
+ message: String(next.message || ''),
1409
+ source: next.source || previous?.source || 'event',
1410
+ }
1411
+ } else {
1412
+ delete state.compactions[sessionId]
1413
+ if (previous?.active) {
1414
+ const command = previous.command || 'compact'
1415
+ if (next?.phase === 'failed') toast(command === 'compact'
1416
+ ? t('ds.compactFailed', { msg: next.message || t('ds.sessionRecoveryFailed') })
1417
+ : t('ds.commandFailed', { command, msg: next.message || t('ds.sessionRecoveryFailed') }), 'err')
1418
+ else toast(command === 'compact' ? t('ds.compactComplete') : t('ds.commandComplete', { command }), 'ok')
1419
+ }
1420
+ }
1421
+ ensureCompactionMonitoring()
1422
+ if (state.current === sessionId) updateComposerStatus()
1423
+ }
1424
+ async function refreshCompactionStatus(sessionId = state.current) {
1425
+ if (!sessionId) return
1426
+ try {
1427
+ const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(8000) : undefined
1428
+ const url = new URL(apiUrl('/remote/api/command-status'), location.href)
1429
+ url.searchParams.set('sessionId', sessionId)
1430
+ const res = await fetch(url, { headers: { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() }, ...(signal ? { signal } : {}) })
1431
+ if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
1432
+ if (!res.ok) return
1433
+ const body = await res.json().catch(() => null)
1434
+ const operation = body?.operation || body?.compact
1435
+ if (!operation) return
1436
+ const pending = state.pendingCommands[sessionId]
1437
+ if (operation.active) {
1438
+ delete state.pendingCommands[sessionId]
1439
+ setCompactionStatus(sessionId, { ...operation, source: 'status' })
1440
+ } else if (activeCompaction(sessionId) && activeCompaction(sessionId).source !== 'event') {
1441
+ setCompactionStatus(sessionId, operation)
1442
+ } else if (pending) {
1443
+ if (operation.phase === 'failed') toast(t('ds.commandFailed', { command: pending, msg: operation.message || t('ds.sessionRecoveryFailed') }), 'err')
1444
+ delete state.pendingCommands[sessionId]
1445
+ }
1446
+ } catch {}
1447
+ }
1448
+ function ensureCompactionMonitoring() {
1449
+ const active = Object.values(state.compactions).some(compact => compact?.active)
1450
+ if (!active) {
1451
+ if (state.compactionPollTimer) clearInterval(state.compactionPollTimer)
1452
+ if (state.compactionClockTimer) clearInterval(state.compactionClockTimer)
1453
+ state.compactionPollTimer = null
1454
+ state.compactionClockTimer = null
1455
+ return
1456
+ }
1457
+ if (!state.compactionClockTimer) state.compactionClockTimer = setInterval(() => { if (activeCompaction()) updateComposerStatus() }, 1000)
1458
+ if (!state.compactionPollTimer) state.compactionPollTimer = setInterval(() => {
1459
+ const compact = activeCompaction()
1460
+ if (compact && compact.source !== 'event') void refreshCompactionStatus()
1461
+ }, 3000)
1462
+ }
1463
+ function observeCompactionEvent(sessionId, event) {
1464
+ if (event?.type === 'compaction/start') {
1465
+ const existing = activeCompaction(sessionId)
1466
+ setCompactionStatus(sessionId, { active: true, phase: 'running', startedAt: existing?.startedAt || Date.now(), source: existing?.source || 'event' })
1467
+ } else if (event?.type === 'compaction/end' && activeCompaction(sessionId)?.source === 'event') {
1468
+ setCompactionStatus(sessionId, { active: false, phase: 'complete' })
1469
+ }
1470
+ }
1378
1471
  function hydrateSessionProjections(sessionId, projections) {
1379
1472
  const s = state.byId.get(sessionId)
1380
1473
  if (!s || !projections || typeof projections !== 'object') return
@@ -1470,6 +1563,7 @@ function setGoalCollapsed(sessionId, goal, collapsed) {
1470
1563
  LS.set(COLLAPSED_GOALS_KEY, JSON.stringify(next.slice(-100)))
1471
1564
  }
1472
1565
  function onSessionEvent(sessionId, event) {
1566
+ observeCompactionEvent(sessionId, event)
1473
1567
  if (event?.type === 'turn/start' || event?.type === 'turn/end') {
1474
1568
  noteSessionTurnTime(sessionId, event)
1475
1569
  renderSessions()
@@ -1637,6 +1731,7 @@ async function openSession(id) {
1637
1731
  renderQueue()
1638
1732
  renderSessionPendingDesktop()
1639
1733
  updateComposerStatus()
1734
+ void refreshCompactionStatus(id)
1640
1735
  await loadHistory()
1641
1736
  }
1642
1737
  function closeSession() {
@@ -1938,12 +2033,55 @@ async function interruptSubagent(childId) {
1938
2033
  setTimeout(renderSessionCards, 600)
1939
2034
  }
1940
2035
 
2036
+ const NO_FALLBACK_SLASH_COMMANDS = new Set(['compact', 'export'])
2037
+ const SLASH_COMMAND_TIMEOUT_MS = 20_000
2038
+ // 比插件端的 120 秒多留 5 秒,让服务端能返回确定的失败结果而非客户端先中断。
2039
+ const LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS = 125_000
2040
+
2041
+ function slashCommandName(text) {
2042
+ const match = /^\/+([^\s/]+)/.exec(String(text || '').trim())
2043
+ return match ? match[1].toLowerCase() : ''
2044
+ }
2045
+
2046
+ function sessionLogFilename(sessionId) {
2047
+ return `dsh-session-${String(sessionId).replace(/[^A-Za-z0-9_-]/g, '_')}.zip`
2048
+ }
2049
+
2050
+ async function downloadSessionExport(sessionId) {
2051
+ const url = new URL(apiUrl('/api/session.export'), location.href)
2052
+ url.searchParams.set('sessionId', sessionId)
2053
+ url.searchParams.set('includeDescendants', 'true')
2054
+ if (state.server && state.token) url.searchParams.set('token', state.token)
2055
+ const headers = state.token ? { authorization: 'Bearer ' + state.token, 'x-dsh-remote-client': 'web', ...clientIdHeaders() } : {}
2056
+ try {
2057
+ const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
2058
+ ? AbortSignal.timeout(LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS)
2059
+ : undefined
2060
+ const preflight = await fetch(url, { method: 'HEAD', headers, ...(signal ? { signal } : {}) })
2061
+ if (preflight.status === 401) { toast(t('ds.toastAuth'), 'err'); return }
2062
+ if (!preflight.ok) throw new Error('HTTP ' + preflight.status)
2063
+ const anchor = document.createElement('a')
2064
+ anchor.href = url.href
2065
+ anchor.download = sessionLogFilename(sessionId)
2066
+ document.body.appendChild(anchor)
2067
+ anchor.click()
2068
+ anchor.remove()
2069
+ toast(t('ds.exportStarted'), 'ok')
2070
+ } catch (e) {
2071
+ console.error('session export download failed', e)
2072
+ toast(t('ds.exportFailed', { msg: e?.message || '' }), 'err')
2073
+ }
2074
+ }
2075
+
1941
2076
  async function runSlashCommand(text) {
1942
2077
  const clean = String(text || '').trim()
1943
2078
  if (!clean.startsWith('/') || !state.current) return false
2079
+ const command = slashCommandName(clean)
2080
+ const noFallback = NO_FALLBACK_SLASH_COMMANDS.has(command)
2081
+ const longRunning = command === 'export'
1944
2082
  try {
1945
2083
  const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
1946
- ? AbortSignal.timeout(20000)
2084
+ ? AbortSignal.timeout(longRunning ? LONG_RUNNING_SLASH_COMMAND_TIMEOUT_MS : SLASH_COMMAND_TIMEOUT_MS)
1947
2085
  : undefined
1948
2086
  const res = await fetch(apiUrl('/remote/api/command'), {
1949
2087
  method: 'POST',
@@ -1952,12 +2090,30 @@ async function runSlashCommand(text) {
1952
2090
  ...(signal ? { signal } : {})
1953
2091
  })
1954
2092
  if (res.status === 401) { toast(t('ds.toastAuth'), 'err'); return true }
1955
- if (!res.ok) return false
2093
+ if (!res.ok) {
2094
+ if (noFallback) toast(t('ds.commandTimedOut'), 'err')
2095
+ return noFallback
2096
+ }
1956
2097
  const data = await res.json().catch(() => null)
1957
2098
  if (data?.ok === false) return true
1958
- return data?.ok === true && data.executed === true
2099
+ if (data?.ok === true && data.executed === true) {
2100
+ if (data.accepted) {
2101
+ if (command === 'compact') {
2102
+ setCompactionStatus(state.current, { ...(data.operation || data.compact), active: true, command, source: 'command' })
2103
+ } else {
2104
+ const sessionId = state.current
2105
+ state.pendingCommands[sessionId] = command
2106
+ setTimeout(() => { if (state.current === sessionId) void refreshCompactionStatus(sessionId) }, 600)
2107
+ }
2108
+ } else if (command === 'export') await downloadSessionExport(state.current)
2109
+ return true
2110
+ }
1959
2111
  } catch (e) {
1960
2112
  console.error('slash command bridge failed', e)
2113
+ if (noFallback) {
2114
+ toast(t('ds.commandTimedOut'), 'err')
2115
+ return true
2116
+ }
1961
2117
  }
1962
2118
  return false
1963
2119
  }
@@ -2033,7 +2189,15 @@ async function archiveCurrentSession() {
2033
2189
  function updateComposerStatus() {
2034
2190
  const status = $('composer-status')
2035
2191
  if (!status) return
2036
- status.classList.toggle('hidden', !state.byId.get(state.current)?.running)
2192
+ const compact = activeCompaction()
2193
+ status.classList.toggle('hidden', !state.byId.get(state.current)?.running && !compact)
2194
+ status.classList.toggle('compacting', !!compact)
2195
+ const text = $('composer-status-text')
2196
+ if (text) text.textContent = compact
2197
+ ? (compact.command === 'compact'
2198
+ ? t('ds.compacting', { elapsed: compactElapsed(compact.startedAt) })
2199
+ : t('ds.commandRunning', { command: compact.command, elapsed: compactElapsed(compact.startedAt) }))
2200
+ : t('ds.composerRunning')
2037
2201
  updateSessionActions()
2038
2202
  }
2039
2203
  function queuePreview(item) {
package/public/index.html CHANGED
@@ -135,7 +135,7 @@
135
135
  </div>
136
136
 
137
137
  <div id="composer-wrap" class="composer-wrap">
138
- <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>
138
+ <div id="composer-status" class="composer-status hidden" role="status" aria-live="polite"><span class="composer-status-dot" aria-hidden="true"></span><span id="composer-status-text" data-i18n="composer.running">运行中…</span></div>
139
139
  <div id="composer-menu" class="composer-menu hidden">
140
140
  <div class="menu-group">
141
141
  <div class="menu-title" data-i18n="menu.commandsTitle">输入指令</div>
@@ -665,6 +665,7 @@
665
665
  </div>
666
666
  <textarea id="fb-msg" class="fb-textarea" rows="5" maxlength="2000" data-i18n-placeholder="feedback.messagePlaceholder" placeholder="请描述遇到的问题或建议(必填,≤2000 字)"></textarea>
667
667
  <input id="fb-contact" class="fb-input" maxlength="200" data-i18n-placeholder="feedback.contactPlaceholder" placeholder="联系方式(可选):邮箱 / 微信 / B站 ID">
668
+ <label class="fb-diagnostics"><input id="fb-include-diagnostics" type="checkbox"> <span data-i18n="feedback.includeDiagnostics">附带兼容性诊断日志(可选)</span><small data-i18n="feedback.diagnosticsPrivacy">仅上传网关/协议版本、接口失败摘要和实时链路状态;不包含令牌、Cookie、对话内容或文件路径。</small></label>
668
669
  </div>
669
670
  <div class="modal-actions">
670
671
  <button id="fb-cancel" class="btn subtle" data-i18n="feedback.cancel">取消</button>
@@ -881,6 +882,11 @@
881
882
  <div id="modal-announcement" class="modal hidden" role="dialog" aria-modal="true">
882
883
  <div class="modal-card announcement-card">
883
884
  <div class="announcement-kicker" data-i18n="announcement.label">公告</div>
885
+ <div id="announcement-pagination" class="announcement-pagination hidden" aria-live="polite">
886
+ <button id="announcement-prev" class="mini-btn" type="button" data-i18n-aria="announcement.previous" data-i18n-title="announcement.previous" aria-label="上一条公告" title="上一条公告">‹</button>
887
+ <span id="announcement-page"></span>
888
+ <button id="announcement-next" class="mini-btn" type="button" data-i18n-aria="announcement.next" data-i18n-title="announcement.next" aria-label="下一条公告" title="下一条公告">›</button>
889
+ </div>
884
890
  <div id="announcement-title" class="modal-title"></div>
885
891
  <div id="announcement-content" class="modal-body announcement-content"></div>
886
892
  <div id="announcement-poll" class="announcement-poll hidden">
@@ -1003,6 +1009,7 @@
1003
1009
  'subagent.confirmInterrupt': '中断这个子代理当前回合?', 'subagent.interruptFailed': '中断失败', 'subagent.interruptSubmitted': '中断请求已提交',
1004
1010
  'queue.title': '排队消息', 'queue.steer': '插话', 'queue.steerUnavailable': '仅运行中可插话', 'queue.steerSubmitted': '插话请求已提交', 'queue.steerFailed': '插话失败:{msg}', 'queue.image': '图片消息',
1005
1011
  'send.failed': '发送失败', 'send.commandSent': '指令已发送', 'send.sent': '已发送', 'send.imageSent': '图片已发送', 'send.commandExecuted': '命令已执行',
1012
+ 'session.commandTimedOut': '命令执行超时,未作为普通消息发送', 'session.exportStarted': '会话日志已开始下载,请在浏览器下载中查看', 'session.exportFailed': '无法导出会话日志:{msg}', 'session.compacting': '正在压缩对话 · 已用时 {elapsed}', 'session.compactComplete': '对话压缩完成', 'session.compactFailed': '对话压缩未完成:{msg}', 'session.commandRunning': '正在执行 /{command} · 已用时 {elapsed}', 'session.commandComplete': '/{command} 已完成', 'session.commandFailed': '/{command} 未完成:{msg}',
1006
1013
  'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
1007
1014
  'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
1008
1015
  'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}', 'models.effortLow': '低', 'models.effortHigh': '高', 'models.effortMax': '极高', 'models.effortOff': '关闭', 'models.effortCustomHint': '该路由未公布档位,按 DSH 兼容值尝试;不支持时不会更改当前设置。',
@@ -1123,7 +1130,7 @@
1123
1130
  'feedback.title': '反馈', 'feedback.githubDesc': '反馈 bug / 提建议', 'feedback.giteeDesc': '国内镜像,无需代理',
1124
1131
  'feedback.biliDesc': 'UP 动态页交流', 'feedback.copyLink': '复制项目链接', 'feedback.copyDesc': '手动分享给朋友',
1125
1132
  'feedback.copied': '项目链接已复制', 'feedback.copyFailed': '复制失败,请手动复制',
1126
- 'feedback.write': '写反馈', 'feedback.writeDesc': 'App 内直接提交', 'feedback.modalTitle': '写反馈',
1133
+ 'feedback.write': '写反馈', 'feedback.writeDesc': 'App 内直接提交', 'feedback.modalTitle': '写反馈', 'feedback.includeDiagnostics': '附带兼容性诊断日志(可选)', 'feedback.diagnosticsPrivacy': '仅上传网关/协议版本、接口失败摘要和实时链路状态;不包含令牌、Cookie、对话内容或文件路径。',
1127
1134
  'feedback.typeBug': 'Bug', 'feedback.typeSuggestion': '建议', 'feedback.typeOther': '其他',
1128
1135
  'feedback.messagePlaceholder': '请描述遇到的问题或建议(必填,≤2000 字)',
1129
1136
  'feedback.contactPlaceholder': '联系方式(可选):邮箱 / 微信 / B站 ID',
@@ -1139,7 +1146,7 @@
1139
1146
  'modal.approvalTitle': '工具审批', 'modal.reject': '拒绝', 'modal.allowOnce': '允许一次',
1140
1147
  'modal.questionTitle': 'DSH 需要你回答', 'modal.later': '稍后', 'modal.submit': '提交',
1141
1148
  'modal.goalTitle': '目标控制', 'modal.close': '关闭', 'modal.updateGoal': '更新目标',
1142
- 'announcement.label': '公告', 'announcement.later': '稍后再看', 'announcement.gotIt': '知道了', 'announcement.open': '查看详情', 'announcement.historyLabel': '公告记录', 'announcement.historyTitle': '历史公告', 'announcement.historyEmpty': '暂无历史公告', 'announcement.close': '关闭', 'announcement.noDate': '未标注日期',
1149
+ 'announcement.label': '公告', 'announcement.later': '稍后再看', 'announcement.gotIt': '全部知道了', 'announcement.open': '查看详情', 'announcement.previous': '上一条公告', 'announcement.next': '下一条公告', 'announcement.page': '第 {current} / {total} 条', 'announcement.historyLabel': '公告记录', 'announcement.historyTitle': '历史公告', 'announcement.historyEmpty': '暂无历史公告', 'announcement.close': '关闭', 'announcement.noDate': '未标注日期',
1143
1150
  'announcement.voteSubmit': '提交投票', 'announcement.voteChoose': '请先选择一项', 'announcement.voteThanks': '已投票:{option}', 'announcement.voteFailed': '投票失败:{msg}', 'announcement.voteNetworkError': '网络错误', 'announcement.voteAgainLater': '提交太频繁,请稍后再试', 'announcement.voteFromHistory': '参与投票',
1144
1151
  'modal.statsTitle': '本轮统计',
1145
1152
  'time.justNow': '刚刚', 'time.minAgo': ' 分钟前', 'time.hourAgo': ' 小时前'
@@ -1235,6 +1242,7 @@
1235
1242
  'subagent.confirmInterrupt': 'Interrupt this subagent\'s current turn?', 'subagent.interruptFailed': 'Interrupt failed', 'subagent.interruptSubmitted': 'Interrupt requested',
1236
1243
  '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',
1237
1244
  'send.failed': 'Send failed', 'send.commandSent': 'Command sent', 'send.sent': 'Sent', 'send.imageSent': 'Image sent', 'send.commandExecuted': 'Command executed',
1245
+ 'session.commandTimedOut': 'Command timed out and was not sent as a chat message', 'session.exportStarted': 'Session log download started; check your browser downloads', 'session.exportFailed': 'Could not export session log: {msg}', 'session.compacting': 'Compressing conversation · {elapsed} elapsed', 'session.compactComplete': 'Conversation compression complete', 'session.compactFailed': 'Conversation compression did not finish: {msg}', 'session.commandRunning': 'Running /{command} · {elapsed} elapsed', 'session.commandComplete': '/{command} complete', 'session.commandFailed': '/{command} did not finish: {msg}',
1238
1246
  'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
1239
1247
  'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
1240
1248
  '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.',
@@ -1355,7 +1363,7 @@
1355
1363
  'feedback.title': 'Feedback', 'feedback.githubDesc': 'Report bugs · suggest features', 'feedback.giteeDesc': 'Mirror in China, no proxy needed',
1356
1364
  'feedback.biliDesc': 'Chat on the UP\'s Bilibili page', 'feedback.copyLink': 'Copy project link', 'feedback.copyDesc': 'Share it manually',
1357
1365
  'feedback.copied': 'Project link copied', 'feedback.copyFailed': 'Copy failed, copy manually',
1358
- 'feedback.write': 'Write feedback', 'feedback.writeDesc': 'Submit from the app', 'feedback.modalTitle': 'Write feedback',
1366
+ 'feedback.write': 'Write feedback', 'feedback.writeDesc': 'Submit from the app', 'feedback.modalTitle': 'Write feedback', 'feedback.includeDiagnostics': 'Include compatibility diagnostics (optional)', 'feedback.diagnosticsPrivacy': 'Uploads only gateway/protocol versions, interface failure summaries, and realtime status; no token, cookie, conversation content, or file path.',
1359
1367
  'feedback.typeBug': 'Bug', 'feedback.typeSuggestion': 'Suggestion', 'feedback.typeOther': 'Other',
1360
1368
  'feedback.messagePlaceholder': 'Describe the bug or suggestion (required, ≤2000 chars)',
1361
1369
  'feedback.contactPlaceholder': 'Contact (optional): email / WeChat / Bilibili ID',
@@ -1371,7 +1379,7 @@
1371
1379
  'modal.approvalTitle': 'Tool approval', 'modal.reject': 'Reject', 'modal.allowOnce': 'Allow once',
1372
1380
  'modal.questionTitle': 'DSH needs your answer', 'modal.later': 'Later', 'modal.submit': 'Submit',
1373
1381
  'modal.goalTitle': 'Goal control', 'modal.close': 'Close', 'modal.updateGoal': 'Update goal',
1374
- 'announcement.label': 'Announcement', 'announcement.later': 'Later', 'announcement.gotIt': 'Got it', 'announcement.open': 'View details', 'announcement.historyLabel': 'Announcement archive', 'announcement.historyTitle': 'Announcement history', 'announcement.historyEmpty': 'No announcement history', 'announcement.close': 'Close', 'announcement.noDate': 'No date',
1382
+ 'announcement.label': 'Announcement', 'announcement.later': 'Later', 'announcement.gotIt': 'Mark all read', 'announcement.open': 'View details', 'announcement.previous': 'Previous announcement', 'announcement.next': 'Next announcement', 'announcement.page': 'Announcement {current} of {total}', 'announcement.historyLabel': 'Announcement archive', 'announcement.historyTitle': 'Announcement history', 'announcement.historyEmpty': 'No announcement history', 'announcement.close': 'Close', 'announcement.noDate': 'No date',
1375
1383
  'announcement.voteSubmit': 'Submit vote', 'announcement.voteChoose': 'Choose an option first', 'announcement.voteThanks': 'Voted: {option}', 'announcement.voteFailed': 'Vote failed: {msg}', 'announcement.voteNetworkError': 'Network error', 'announcement.voteAgainLater': 'Too many submissions; try again later', 'announcement.voteFromHistory': 'Vote now',
1376
1384
  'modal.statsTitle': 'This round',
1377
1385
  'time.justNow': 'just now', 'time.minAgo': ' min ago', 'time.hourAgo': ' hr ago'
package/public/styles.css CHANGED
@@ -1151,6 +1151,9 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
1151
1151
  }
1152
1152
  .fb-textarea:focus, .fb-input:focus { border-color: var(--dsr-accent-line); }
1153
1153
  .fb-input { min-height: 42px; }
1154
+ .fb-diagnostics { display:grid; grid-template-columns:auto minmax(0,1fr); gap:5px 8px; align-items:start; margin:-1px 1px 4px; color:var(--dsr-text); font-size:13px; line-height:1.45; cursor:pointer; }
1155
+ .fb-diagnostics input { width:17px; height:17px; margin:1px 0 0; accent-color:var(--dsr-accent); }
1156
+ .fb-diagnostics small { grid-column:2; color:var(--dsr-muted); font-size:11px; line-height:1.45; }
1154
1157
  .feedback-success-card { max-width: 360px; text-align: center; padding: 22px 20px 18px; }
1155
1158
  .feedback-success-icon {
1156
1159
  width: 52px; height: 52px; display: grid; place-items: center; margin: 0 auto 13px; border-radius: 50%;
@@ -1215,6 +1218,9 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
1215
1218
  .archive-confirm-btn:active { filter: brightness(.9); }
1216
1219
  .announcement-card { border-top: 3px solid var(--dsr-accent); }
1217
1220
  .announcement-kicker { color: var(--dsr-accent-strong); font-size: 12px; font-weight: 700; letter-spacing: .04em; margin-bottom: 6px; }
1221
+ .announcement-pagination { display:flex; align-items:center; justify-content:flex-end; gap:8px; margin:-2px 0 8px; color:var(--dsr-muted); font-size:12px; font-weight:650; }
1222
+ .announcement-pagination .mini-btn { min-width:34px; min-height:30px; padding:2px 9px; font-size:19px; line-height:1; }
1223
+ .announcement-pagination .mini-btn:disabled { opacity:.38; cursor:default; }
1218
1224
  .announcement-content { line-height: 1.7; overflow-wrap: anywhere; word-break: break-word; }
1219
1225
  .announcement-poll { display: grid; gap: 10px; margin-top: 14px; padding: 12px; border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-panel); }
1220
1226
  .announcement-poll-question { font-size: 14px; font-weight: 700; line-height: 1.5; }
@@ -1,10 +1,18 @@
1
1
  {
2
- "version": "0.6.21",
2
+ "version": "0.6.23",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "46fc5409dbe59300e352ae6dddfbc001856a349a85b81be025e958316a1c5dc0",
5
- "releasedAt": "2026-08-31T04:10:09.396Z",
6
- "notes": "0.6.21:左上角 DSH Remote 名称可展开服务器组快捷切换抽屉,当前组、组内服务器数量和管理入口一目了然,手机与桌面端均可一键切换;修复部分新版 DSH 命令执行成功返回 void 时被误判为未执行、继而把 /命令作为普通文本发送的问题,同时兼容旧版三参数、新版四参数及默认参数签名。",
4
+ "sha256": "947d138637edf0b300e9db3e024ac9b983f62e825847a24327bf7399edd56c01",
5
+ "releasedAt": "2026-09-03T01:19:16.166Z",
6
+ "notes": "0.6.23:加强 DSH 版本兼容。旧版点号 RPC 与新版 slash Remote API 可自动协商和回退,修复部分新版 DSH host.describe、创建会话等请求返回 404 的问题;DSH 重启或升级后会重新识别协议并恢复实时通道。反馈时可由用户选择附带脱敏兼容性诊断,便于定位接口问题;诊断不包含令牌、Cookie、对话内容或文件路径。",
7
7
  "history": [
8
+ {
9
+ "version": "0.6.23",
10
+ "notes": "0.6.23:加强 DSH 版本兼容。旧版点号 RPC 与新版 slash Remote API 可自动协商和回退,修复部分新版 DSH 中 host.describe、创建会话等请求返回 404 的问题;DSH 重启或升级后会重新识别协议并恢复实时通道。反馈时可由用户选择附带脱敏兼容性诊断,便于定位接口问题;诊断不包含令牌、Cookie、对话内容或文件路径。"
11
+ },
12
+ {
13
+ "version": "0.6.22",
14
+ "notes": "0.6.22:斜杠命令统一后台受理并显示运行状态与耗时,/compact 不再受两分钟请求等待限制;/export 可在手机保存到系统下载目录、在浏览器按下载设置保存会话 ZIP;修复命令桥接超时后被误作为普通消息发送的问题。首次有多条未读公告时改为单一分页窗口,可左右切换,确认一次即可全部标为已读。"
15
+ },
8
16
  {
9
17
  "version": "0.6.21",
10
18
  "notes": "0.6.21:左上角 DSH Remote 名称可展开服务器组快捷切换抽屉,当前组、组内服务器数量和管理入口一目了然,手机与桌面端均可一键切换;修复部分新版 DSH 命令执行成功返回 void 时被误判为未执行、继而把 /命令作为普通文本发送的问题,同时兼容旧版三参数、新版四参数及默认参数签名。"
@@ -36,14 +44,6 @@
36
44
  {
37
45
  "version": "0.6.14",
38
46
  "notes": "0.6.14-rc.7:将 Android 后台轮询服务接入与前台 App 相同的 client ID,修复 Dalvik 后台请求被显示为第二台未知设备;保留旧记录的顺序无关迁移;补齐文件上传请求的持久设备 ID;实时扫码继续使用 Worker 解码并降低取样负载;修复非用户来源的 DSH user/message 被误显示为用户输入;新增子代理折叠、排队消息插话、运行中提示和按轮次稳定排序;Markdown 支持 GFM 表格。"
39
- },
40
- {
41
- "version": "0.6.13",
42
- "notes": "0.6.13 正式版:新增首次连接 Doctor,集中检查 DSH 服务、远程网关、局域网地址、防火墙、终端配对和实时消息通道;网关控制台新增可选的独立设备密钥,支持设备备注、最近 IP、二维码、令牌轮换、复制和退出,共享令牌在启用后仅保留管理权限;手机端和桌面端支持实时思考内容,并为未声明推理档位的模型提供 low、high、max 三档选择;普通会话列表、工作区树和主页统计不再混入子代理内部会话;Android App 落后于网关版本时显示明确更新提醒;新增网关协议与能力协商并兼容旧网关,同时补充设备隔离、重启持久化、推理显示、会话过滤和版本差异回归测试。"
43
- },
44
- {
45
- "version": "0.6.12",
46
- "notes": "0.6.12:修复远程启动和重启 DSH 时统一报 HTTP 502 且无法确认结果的问题;改为异步追踪 systemd 服务检查、命令提交、进程启动、DSH HTTP 恢复和 mux/host 实时通道重连阶段,成功时显示 PID、HTTP 状态和用时,失败时区分服务不存在、systemd 不可用、权限不足、命令失败、服务失败、启动超时、HTTP 恢复超时和实时通道恢复超时;优化工作区会话筛选、文件预览和应用内选择器;新增中央投票公告与反馈成功确认;主页公告栏常驻并在无未读公告时显示空状态,同时修正设置页异常右箭头、主页刷新图标居中和周末谷时提醒。"
47
47
  }
48
48
  ]
49
49
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.6.21"
2
+ "version": "0.6.23"
3
3
  }