dsh-remote-plugin 0.6.12 → 0.6.13

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.
@@ -48,6 +48,9 @@ function themeSet(id) { LS.set('dshTheme', id); themeApply() }
48
48
  themeApply()
49
49
 
50
50
  /* ---------------- 状态 ---------------- */
51
+ function emptyDesktopHistory() {
52
+ return { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity, partialReasoning: new Map() }
53
+ }
51
54
  const state = {
52
55
  token: LS.get('token', ''),
53
56
  wsTicket: { token: '', server: '', value: '', expiresAt: 0 },
@@ -58,13 +61,14 @@ const state = {
58
61
  autoSelect: { '默认': true },
59
62
  groupActive: { '默认': '' },
60
63
  serverLatency: {},
64
+ gatewayHealth: {},
61
65
  selectingServer: false,
62
66
  sessions: [],
63
67
  sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
64
68
  byId: new Map(),
65
69
  current: null,
66
70
  hostInfo: null,
67
- history: { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity },
71
+ history: emptyDesktopHistory(),
68
72
  approvals: [],
69
73
  questions: [],
70
74
  questionModal: null,
@@ -280,16 +284,31 @@ function renderEffortMenu() {
280
284
  const cur = state.models.current
281
285
  const provider = (state.models.groups || []).find(g => g.id === cur?.provider)
282
286
  const model = (provider?.models || []).find(m => m.id === cur?.model)
283
- const efforts = model?.reasoning?.efforts || []
284
- group.classList.toggle('hidden', !efforts.length)
287
+ const { efforts, defaultEffort, custom } = reasoningEffortOptions(model)
288
+ group.classList.toggle('hidden', !cur || !efforts.length)
285
289
  box.innerHTML = efforts.map(e => {
286
- const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === model.reasoning.defaultEffort)
290
+ const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === defaultEffort)
287
291
  return `<button class="ds-model-chip ${isCur ? 'current' : ''}" data-effort="${esc(e.id)}" title="${esc(e.description || '')}">${esc(e.name || e.id)}</button>`
288
- }).join('')
292
+ }).join('') + (custom ? `<span class="ds-effort-hint">${esc(t('models.effortCustomHint'))}</span>` : '')
289
293
  box.querySelectorAll('[data-effort]').forEach(btn =>
290
294
  btn.addEventListener('click', () => selectSessionEffort(btn.dataset.effort)))
291
295
  }
292
296
 
297
+ function reasoningEffortOptions(model) {
298
+ const raw = Array.isArray(model?.reasoning?.efforts) && model.reasoning.efforts.length
299
+ ? model.reasoning.efforts
300
+ : (Array.isArray(model?.reasoningEfforts) && model.reasoningEfforts.length ? model.reasoningEfforts : null)
301
+ const names = { low: t('models.effortLow'), high: t('models.effortHigh'), max: t('models.effortMax'), off: t('models.effortOff') }
302
+ if (raw) {
303
+ return {
304
+ efforts: raw.map(e => typeof e === 'string' ? { id: e, name: names[e] || e } : e),
305
+ defaultEffort: model?.reasoning?.defaultEffort,
306
+ custom: !model?.reasoning?.efforts
307
+ }
308
+ }
309
+ return { efforts: ['low', 'high', 'max'].map(id => ({ id, name: names[id] })), defaultEffort: undefined, custom: true }
310
+ }
311
+
293
312
  async function selectSessionEffort(effortId) {
294
313
  const cur = state.models.current
295
314
  if (!state.current || !cur) return
@@ -523,6 +542,7 @@ async function getWsTicket() {
523
542
  const token = state.token
524
543
  const server = state.server
525
544
  wsTicketPromise = (async () => {
545
+ if (activeGatewayCapability('wsTicket') === false) throw new Error('ws ticket unsupported')
526
546
  const res = await fetch(apiUrl('/api/ws-ticket'), {
527
547
  method: 'POST',
528
548
  headers: { authorization: 'Bearer ' + token, 'x-dsh-remote-client': 'web' }
@@ -645,9 +665,18 @@ async function pingServer(base) {
645
665
  const timer = setTimeout(() => ctrl.abort(), 3500)
646
666
  try {
647
667
  const res = await fetch(u + '/health?t=' + Date.now(), { signal: ctrl.signal, cache: 'no-store' })
648
- return res.ok ? Math.round(performance.now() - t0) : Infinity
668
+ if (!res.ok) return Infinity
669
+ const health = await res.json().catch(() => null)
670
+ if (health && typeof health === 'object') state.gatewayHealth[u] = health
671
+ return Math.round(performance.now() - t0)
649
672
  } catch { return Infinity } finally { clearTimeout(timer) }
650
673
  }
674
+ function activeGatewayCapability(name) {
675
+ const key = String(state.server || location.origin || '').replace(/\/+$/, '')
676
+ const capabilities = state.gatewayHealth[key]?.capabilities
677
+ if (!capabilities || !Object.prototype.hasOwnProperty.call(capabilities, name)) return null
678
+ return Number(capabilities[name]) > 0
679
+ }
651
680
  async function selectFastestServer({ silent = false, reconnect = true } = {}) {
652
681
  if (state.selectingServer) return null
653
682
  state.selectingServer = true
@@ -1163,6 +1192,10 @@ function applyProjection(sessionId, key, value, seq) {
1163
1192
  }
1164
1193
  function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
1165
1194
  function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('ds.sessions')) }
1195
+ function isTopLevelSession(session) {
1196
+ return !!session && !session.parentSessionId && session.origin !== 'subagent'
1197
+ }
1198
+ function topLevelSessions() { return state.sessions.filter(isTopLevelSession) }
1166
1199
  const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
1167
1200
  function isGoalTerminal(goal) {
1168
1201
  return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
@@ -1175,6 +1208,11 @@ function goalOf(s) {
1175
1208
  function onSessionEvent(sessionId, event) {
1176
1209
  if (state.current === sessionId && event) {
1177
1210
  const h = state.history
1211
+ const reasoningChanged = applyReasoningStreamEvent(event)
1212
+ if (event.type === 'assistant/chunk' || event.type === 'reasoning-chunks') {
1213
+ if (reasoningChanged) scheduleReasoningRender()
1214
+ return
1215
+ }
1178
1216
  const seq = event.seq
1179
1217
  if (seq != null && !h.seqs.has(seq) && shouldShowEvent(event.type)) {
1180
1218
  h.seqs.add(seq)
@@ -1209,7 +1247,7 @@ function workspaceDisplayName(label) {
1209
1247
  return parts[parts.length - 1] || value
1210
1248
  }
1211
1249
  function sortedSessions() {
1212
- const items = [...state.sessions]
1250
+ const items = topLevelSessions()
1213
1251
  if (state.sessionSort === 'workspace') {
1214
1252
  return items.sort((a, b) => {
1215
1253
  const aw = sessionCwd(a) || '\uffff'
@@ -1272,7 +1310,7 @@ function renderSessions() {
1272
1310
 
1273
1311
  async function openSession(id) {
1274
1312
  state.current = id
1275
- state.history = { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity }
1313
+ state.history = emptyDesktopHistory()
1276
1314
  state.models = { loaded: false, loading: false, groups: [], current: null, failures: [] }
1277
1315
  showView('view-chat')
1278
1316
  $('ds-title').textContent = titleOf(state.byId.get(id)) || t('ds.sessions')
@@ -1283,7 +1321,7 @@ async function openSession(id) {
1283
1321
  }
1284
1322
  function closeSession() {
1285
1323
  state.current = null
1286
- state.history = { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity }
1324
+ state.history = emptyDesktopHistory()
1287
1325
  const cards = $('session-cards')
1288
1326
  if (cards) cards.innerHTML = ''
1289
1327
  showView('view-sessions')
@@ -1303,6 +1341,7 @@ async function loadHistory() {
1303
1341
  for (const entry of v.events || []) {
1304
1342
  const ev = entry?.event
1305
1343
  const seq = ev?.seq
1344
+ applyReasoningStreamEvent(ev)
1306
1345
  if (seq == null || state.history.seqs.has(seq)) continue
1307
1346
  if (!shouldShowEvent(ev.type)) continue
1308
1347
  state.history.seqs.add(seq)
@@ -1322,11 +1361,59 @@ const INTERESTING_EVENTS = new Set([
1322
1361
  'approval/asked', 'approval/resolved', 'session/title', 'title'
1323
1362
  ])
1324
1363
  function shouldShowEvent(type) { return INTERESTING_EVENTS.has(type) }
1364
+ function reasoningStreamKey(data, index) { return `${data?.turn ?? '?'}:${data?.step ?? '?'}:${index ?? '?'}` }
1365
+ function applyReasoningStreamEvent(event) {
1366
+ const h = state.history
1367
+ const data = event?.data || {}
1368
+ let changed = false
1369
+ if (event?.type === 'assistant/chunk') {
1370
+ const chunk = data.chunk || {}
1371
+ const key = reasoningStreamKey(data, chunk.index)
1372
+ if (chunk.type === 'block-start' && chunk.blockType === 'reasoning') {
1373
+ h.partialReasoning.set(key, { turn: data.turn, step: data.step, index: chunk.index, text: '' })
1374
+ changed = true
1375
+ } else if (chunk.type === 'reasoning-delta') {
1376
+ const item = h.partialReasoning.get(key) || { turn: data.turn, step: data.step, index: chunk.index, text: '' }
1377
+ item.text += String(chunk.text || '')
1378
+ h.partialReasoning.set(key, item)
1379
+ changed = true
1380
+ } else if (chunk.type === 'block-end' && chunk.block?.type === 'reasoning') {
1381
+ h.partialReasoning.set(key, { turn: data.turn, step: data.step, index: chunk.index, text: String(chunk.block.text ?? chunk.block.content ?? '') })
1382
+ changed = true
1383
+ }
1384
+ } else if (event?.type === 'reasoning-chunks') {
1385
+ const key = reasoningStreamKey(data, data.index)
1386
+ const item = h.partialReasoning.get(key) || { turn: data.turn, step: data.step, index: data.index, text: '' }
1387
+ item.text += Array.isArray(data.texts) ? data.texts.join('') : String(data.text || '')
1388
+ h.partialReasoning.set(key, item)
1389
+ changed = true
1390
+ } else if (event?.type === 'assistant/message') {
1391
+ for (const [key, item] of h.partialReasoning) {
1392
+ if (item.turn === data.turn && item.step === data.step) { h.partialReasoning.delete(key); changed = true }
1393
+ }
1394
+ }
1395
+ return changed
1396
+ }
1397
+ let reasoningRenderTimer = null
1398
+ function scheduleReasoningRender() {
1399
+ if (reasoningRenderTimer) return
1400
+ reasoningRenderTimer = setTimeout(() => {
1401
+ reasoningRenderTimer = null
1402
+ if (state.current) renderHistory()
1403
+ }, 80)
1404
+ }
1405
+ function partialReasoningHtml() {
1406
+ return [...state.history.partialReasoning.values()]
1407
+ .filter(item => item.text)
1408
+ .sort((a, b) => (a.turn ?? 0) - (b.turn ?? 0) || (a.step ?? 0) - (b.step ?? 0) || (a.index ?? 0) - (b.index ?? 0))
1409
+ .map(item => `<div class="ds-msg assistant ds-reasoning-live"><div class="role">${esc(t('ds.role.dsh'))}</div><details open><summary>${esc(t('block.thinkingLive'))}</summary><div>${esc(item.text)}</div></details></div>`)
1410
+ .join('')
1411
+ }
1325
1412
  function safeJson(v) { try { return JSON.stringify(v, null, 2) } catch { return String(v) } }
1326
1413
  function blockHtml(b) {
1327
1414
  if (!b) return ''
1328
1415
  if (b.type === 'text') return `<div class="md">${window.mdToHtml ? window.mdToHtml(b.text ?? '') : esc(b.text ?? '')}</div>`
1329
- if (b.type === 'reasoning') return `<span style="opacity:.75">${esc(b.text ?? '')}</span>`
1416
+ if (b.type === 'thinking' || b.type === 'reasoning') return `<details><summary>${esc(t('block.thinking'))}</summary><div style="opacity:.82">${esc(b.text ?? b.content ?? '')}</div></details>`
1330
1417
  if (b.type === 'tool-call') return `<div>🔧 ${esc(b.name || '')}</div>`
1331
1418
  if (b.type === 'tool-result') return `<div>📦</div>`
1332
1419
  if (b.type === 'image') return `<div>🖼</div>`
@@ -1373,7 +1460,8 @@ function eventHtml(entry) {
1373
1460
  function renderHistory() {
1374
1461
  const box = $('history')
1375
1462
  const items = state.history.visible
1376
- box.innerHTML = items.map(eventHtml).join('') || `<div class="ds-empty">${t('ds.historyEmpty')}</div>`
1463
+ const html = items.map(eventHtml).join('') + partialReasoningHtml()
1464
+ box.innerHTML = html || `<div class="ds-empty">${t('ds.historyEmpty')}</div>`
1377
1465
  box.scrollTop = box.scrollHeight
1378
1466
  }
1379
1467
 
@@ -1838,7 +1926,7 @@ function renderWorkbench() {
1838
1926
  let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
1839
1927
  html += projects.length ? projects.map(w => {
1840
1928
  const id = String(w.workspaceId || '')
1841
- const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
1929
+ const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
1842
1930
  const open = state.wb.open === id
1843
1931
  return `<div class="ds-wb-project ${open ? 'open' : ''}">
1844
1932
  <button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
@@ -2060,8 +2148,9 @@ function renderOverviewDesktop() {
2060
2148
  $('ds-overview-attention-list').querySelectorAll('[data-ds-overview-approve]').forEach(btn => btn.addEventListener('click', () => approveApproval(btn.closest('[data-ds-overview-approval]')?.dataset.dsOverviewApproval || '', btn.dataset.dsOverviewApprove === '1')))
2061
2149
  $('ds-overview-attention-list').querySelectorAll('[data-ds-overview-question]').forEach(btn => btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.dsOverviewQuestion))))
2062
2150
 
2063
- const running = state.sessions.filter(s => s.running).length
2064
- const sessions = [...state.sessions].sort((a, b) => Number(b.running) - Number(a.running) || (new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))).slice(0, 6)
2151
+ const topSessions = topLevelSessions()
2152
+ const running = topSessions.filter(s => s.running).length
2153
+ const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))).slice(0, 6)
2065
2154
  const primary = $('ds-overview-primary-action')
2066
2155
  if (primary) {
2067
2156
  let action = 'new'
package/public/index.html CHANGED
@@ -698,6 +698,25 @@
698
698
  </div>
699
699
  </div>
700
700
 
701
+ <!-- App / 网关版本差异提醒 -->
702
+ <div id="modal-app-version-warning" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="app-version-warning-title">
703
+ <div class="modal-card app-version-warning-card">
704
+ <div class="app-version-warning-icon" aria-hidden="true">↑</div>
705
+ <div id="app-version-warning-title" class="modal-title app-version-warning-title" data-i18n="update.gatewayAheadTitle">App 版本需要更新</div>
706
+ <div class="modal-body app-version-warning-body" data-i18n="update.gatewayAheadBody">网关已经更新,但当前 App 仍是较旧版本,部分功能可能缺失或显示异常。</div>
707
+ <div class="app-version-comparison" aria-label="App and gateway versions">
708
+ <div><span data-i18n="update.appVersion">App 版本</span><strong id="app-version-current">—</strong></div>
709
+ <i aria-hidden="true">→</i>
710
+ <div><span data-i18n="update.gatewayVersion">网关版本</span><strong id="app-version-gateway">—</strong></div>
711
+ </div>
712
+ <div class="app-version-warning-hint" data-i18n="update.gatewayAheadHint">建议同步更新 App,以保持与网关功能一致。</div>
713
+ <div class="modal-actions">
714
+ <button id="app-version-later" class="btn subtle" data-i18n="update.remindLater">稍后提醒</button>
715
+ <button id="app-version-update" class="btn primary" data-i18n="update.checkNow">立即检查更新</button>
716
+ </div>
717
+ </div>
718
+ </div>
719
+
701
720
  <!-- 更新内容模态 -->
702
721
  <div id="modal-notes" class="modal hidden" role="dialog" aria-modal="true">
703
722
  <div class="modal-card">
@@ -823,7 +842,7 @@
823
842
  'tool.default': '工具', 'tool.unknown': '未知工具', 'tool.result': '结果',
824
843
  'event.taskStart': '▶ 任务开始', 'event.taskEnd': '■ 任务结束',
825
844
  'event.systemReminder': '系统提示',
826
- 'block.image': '图片', 'block.thinking': '🧠 思考过程', 'block.toolCall': '工具调用', 'block.toolResult': '工具结果', 'block.unknown': '块 · {type}',
845
+ 'block.image': '图片', 'block.thinking': '🧠 思考过程', 'block.thinkingLive': '🧠 思考中…', 'block.toolCall': '工具调用', 'block.toolResult': '工具结果', 'block.unknown': '块 · {type}',
827
846
  'truncated': '…(截断)',
828
847
  'stats.title': '本轮统计', 'stats.roundTitle': '本轮统计', 'stats.turnsSteps': '轮次 / 步骤',
829
848
  'stats.llmTime': '模型耗时', 'stats.minutes': ' 分钟', 'stats.outputCache': '输出 / 缓存读',
@@ -838,7 +857,7 @@
838
857
  'send.failed': '发送失败', 'send.commandSent': '指令已发送', 'send.sent': '已发送', 'send.imageSent': '图片已发送', 'send.commandExecuted': '命令已执行',
839
858
  'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
840
859
  'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
841
- 'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}',
860
+ 'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}', 'models.effortLow': '低', 'models.effortHigh': '高', 'models.effortMax': '极高', 'models.effortOff': '关闭', 'models.effortCustomHint': '该路由未公布档位,按 DSH 兼容值尝试;不支持时不会更改当前设置。',
842
861
  'menu.commandsTitle': '输入指令',
843
862
  'menu.compact': '/compact 压缩对话历史', 'menu.export': '/export 导出会话日志 ZIP',
844
863
  'menu.feedback': '/feedback 反馈当前会话', 'menu.goal': '/goal 设置/查看任务目标',
@@ -880,6 +899,9 @@
880
899
  'update.corrupted': '下载文件损坏,请重试', 'update.serverFileMissing': '服务器上还没有对应版本的文件,请稍后再试',
881
900
  'update.installUnsupported': '当前版本不支持 App 内安装,已转浏览器下载',
882
901
  'update.expand': '展开', 'update.collapse': '收起',
902
+ 'update.gatewayAheadTitle': 'App 版本需要更新', 'update.gatewayAheadBody': '网关已经更新,但当前 App 仍是较旧版本,部分功能可能缺失或显示异常。',
903
+ 'update.appVersion': 'App 版本', 'update.gatewayVersion': '网关版本', 'update.gatewayAheadHint': '建议同步更新 App,以保持与网关功能一致。',
904
+ 'update.remindLater': '稍后提醒', 'update.checkNow': '立即检查更新',
883
905
  'notes.title': '更新内容', 'notes.close': '关闭', 'notes.page': '版本号 {current}/{total}',
884
906
  'scan.imageLoadFailed': '图片加载失败', 'scan.decodeUnsupported': '当前设备不支持图片解码',
885
907
  'scan.browserHint': '浏览器请打开主机管理页,用手机相机扫码', 'scan.unsupported': '当前 App 版本不支持拍照扫码,请先更新 App',
@@ -1031,7 +1053,7 @@
1031
1053
  'tool.default': 'tool', 'tool.unknown': 'Unknown tool', 'tool.result': 'Result',
1032
1054
  'event.taskStart': '▶ Task started', 'event.taskEnd': '■ Task finished',
1033
1055
  'event.systemReminder': 'System reminder',
1034
- 'block.image': 'Image', 'block.thinking': '🧠 Thinking', 'block.toolCall': 'Tool call', 'block.toolResult': 'Tool result', 'block.unknown': 'Block · {type}',
1056
+ 'block.image': 'Image', 'block.thinking': '🧠 Thinking', 'block.thinkingLive': '🧠 Thinking…', 'block.toolCall': 'Tool call', 'block.toolResult': 'Tool result', 'block.unknown': 'Block · {type}',
1035
1057
  'truncated': '…(truncated)',
1036
1058
  'stats.title': 'This round', 'stats.roundTitle': 'This round', 'stats.turnsSteps': 'Turns / steps',
1037
1059
  'stats.llmTime': 'LLM time', 'stats.minutes': ' min', 'stats.outputCache': 'Output / cache read',
@@ -1046,7 +1068,7 @@
1046
1068
  'send.failed': 'Send failed', 'send.commandSent': 'Command sent', 'send.sent': 'Sent', 'send.imageSent': 'Image sent', 'send.commandExecuted': 'Command executed',
1047
1069
  'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
1048
1070
  'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
1049
- 'models.effortFailed': 'Failed to switch reasoning effort', 'models.effortSwitched': 'Reasoning effort: {effort}',
1071
+ '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.',
1050
1072
  'menu.commandsTitle': 'Commands',
1051
1073
  'menu.compact': '/compact Compress conversation history', 'menu.export': '/export Export session log ZIP',
1052
1074
  'menu.feedback': '/feedback Feedback current session', 'menu.goal': '/goal Set/view task goal',
@@ -1088,6 +1110,9 @@
1088
1110
  'update.corrupted': 'Downloaded file is corrupted, please retry', 'update.serverFileMissing': 'The file for this version is not on the server yet, please try again later',
1089
1111
  'update.installUnsupported': 'This version cannot install in-app, opening browser download',
1090
1112
  'update.expand': 'Expand', 'update.collapse': 'Collapse',
1113
+ 'update.gatewayAheadTitle': 'App update recommended', 'update.gatewayAheadBody': 'The gateway is newer than this App. Some features may be missing or display incorrectly.',
1114
+ 'update.appVersion': 'App version', 'update.gatewayVersion': 'Gateway version', 'update.gatewayAheadHint': 'Update the App to keep its features aligned with the gateway.',
1115
+ 'update.remindLater': 'Remind me later', 'update.checkNow': 'Check for updates',
1091
1116
  'notes.title': 'What\'s New', 'notes.close': 'Close', 'notes.page': 'Version {current}/{total}',
1092
1117
  'scan.imageLoadFailed': 'Image failed to load', 'scan.decodeUnsupported': 'This device cannot decode images',
1093
1118
  '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',
package/public/styles.css CHANGED
@@ -3,6 +3,12 @@
3
3
 
4
4
  @import 'theme-vars.css';
5
5
 
6
+ /* 思考流与兼容档位提示 */
7
+ .effort-hint { flex-basis: 100%; color: var(--dsr-muted); font-size: 11px; line-height: 1.45; }
8
+ .msg.reasoning-live { border-color: var(--dsr-accent-line); box-shadow: 0 0 0 1px var(--dsr-accent-soft) inset; }
9
+ .msg.reasoning-live summary::after { content: ''; display: inline-block; width: 6px; height: 6px; margin-left: 7px; border-radius: 50%; background: var(--dsr-accent); animation: reasoning-pulse 1.2s ease-in-out infinite; }
10
+ @keyframes reasoning-pulse { 0%, 100% { opacity: .35; transform: scale(.8); } 50% { opacity: 1; transform: scale(1); } }
11
+
6
12
 
7
13
  /* ============ 基础 ============ */
8
14
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
@@ -900,6 +906,23 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
900
906
  .feedback-success-card .modal-actions { justify-content: stretch; }
901
907
  .feedback-success-card .modal-actions .btn { flex: 1; min-height: 42px; }
902
908
 
909
+ .app-version-warning-card { max-width: 390px; text-align: center; padding: 22px 20px 18px; border-top: 3px solid var(--dsr-accent); }
910
+ .app-version-warning-icon {
911
+ width: 50px; height: 50px; display: grid; place-items: center; margin: 0 auto 12px; border-radius: 15px;
912
+ color: var(--dsr-accent-strong); background: var(--dsr-accent-soft); border: 1px solid var(--dsr-accent-line);
913
+ font-size: 28px; font-weight: 800; line-height: 1;
914
+ }
915
+ .app-version-warning-title { margin-bottom: 7px; font-size: 18px; }
916
+ .app-version-warning-body, .app-version-warning-hint { color: var(--dsr-muted); line-height: 1.6; }
917
+ .app-version-comparison { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 10px; margin: 16px 0 12px; }
918
+ .app-version-comparison > div { display: grid; gap: 5px; min-width: 0; padding: 11px 8px; border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-panel); }
919
+ .app-version-comparison span { color: var(--dsr-muted); font-size: 11px; }
920
+ .app-version-comparison strong { overflow-wrap: anywhere; color: var(--dsr-text); font-size: 14px; }
921
+ .app-version-comparison > i { color: var(--dsr-accent-strong); font-style: normal; font-weight: 800; }
922
+ .app-version-warning-hint { font-size: 12px; }
923
+ .app-version-warning-card .modal-actions { justify-content: stretch; }
924
+ .app-version-warning-card .modal-actions .btn { flex: 1; min-height: 42px; }
925
+
903
926
  /* ---------- 模态 ---------- */
904
927
  .modal {
905
928
  position: fixed; inset: 0; z-index: 40; display: grid; place-items: center;
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.6.12",
2
+ "version": "0.6.13",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "99211eee40cd121fce38fb3a9a3b85a5fcfde28546bccdac246574b38d6adb3f",
5
- "releasedAt": "2026-08-23T14:05:30.412Z",
6
- "notes": "0.6.12:修复远程启动和重启 DSH 时统一报 HTTP 502 且无法确认结果的问题;改为异步追踪 systemd 服务检查、命令提交、进程启动、DSH HTTP 恢复和 mux/host 实时通道重连阶段,成功时显示 PIDHTTP 状态和用时,失败时区分服务不存在、systemd 不可用、权限不足、命令失败、服务失败、启动超时、HTTP 恢复超时和实时通道恢复超时;优化工作区会话筛选、文件预览和应用内选择器;新增中央投票公告与反馈成功确认;主页公告栏常驻并在无未读公告时显示空状态,同时修正设置页异常右箭头、主页刷新图标居中和周末谷时提醒。",
4
+ "sha256": "6cee7bfb1425fb26bcfbcd5c5f932b8ff26ca9d5026ac199b5393a00d0c343b1",
5
+ "releasedAt": "2026-08-24T05:49:23.534Z",
6
+ "notes": "0.6.13 正式版:新增首次连接 Doctor,集中检查 DSH 服务、远程网关、局域网地址、防火墙、终端配对和实时消息通道;网关控制台新增可选的独立设备密钥,支持设备备注、最近 IP、二维码、令牌轮换、复制和退出,共享令牌在启用后仅保留管理权限;手机端和桌面端支持实时思考内容,并为未声明推理档位的模型提供 lowhigh、max 三档选择;普通会话列表、工作区树和主页统计不再混入子代理内部会话;Android App 落后于网关版本时显示明确更新提醒;新增网关协议与能力协商并兼容旧网关,同时补充设备隔离、重启持久化、推理显示、会话过滤和版本差异回归测试。",
7
7
  "history": [
8
+ {
9
+ "version": "0.6.13",
10
+ "notes": "0.6.13 正式版:新增首次连接 Doctor,集中检查 DSH 服务、远程网关、局域网地址、防火墙、终端配对和实时消息通道;网关控制台新增可选的独立设备密钥,支持设备备注、最近 IP、二维码、令牌轮换、复制和退出,共享令牌在启用后仅保留管理权限;手机端和桌面端支持实时思考内容,并为未声明推理档位的模型提供 low、high、max 三档选择;普通会话列表、工作区树和主页统计不再混入子代理内部会话;Android App 落后于网关版本时显示明确更新提醒;新增网关协议与能力协商并兼容旧网关,同时补充设备隔离、重启持久化、推理显示、会话过滤和版本差异回归测试。"
11
+ },
8
12
  {
9
13
  "version": "0.6.12",
10
14
  "notes": "0.6.12:修复远程启动和重启 DSH 时统一报 HTTP 502 且无法确认结果的问题;改为异步追踪 systemd 服务检查、命令提交、进程启动、DSH HTTP 恢复和 mux/host 实时通道重连阶段,成功时显示 PID、HTTP 状态和用时,失败时区分服务不存在、systemd 不可用、权限不足、命令失败、服务失败、启动超时、HTTP 恢复超时和实时通道恢复超时;优化工作区会话筛选、文件预览和应用内选择器;新增中央投票公告与反馈成功确认;主页公告栏常驻并在无未读公告时显示空状态,同时修正设置页异常右箭头、主页刷新图标居中和周末谷时提醒。"
@@ -40,10 +44,6 @@
40
44
  {
41
45
  "version": "0.6.4",
42
46
  "notes": "扫码配对改为显式双按钮(拍照/相册),修复小米等 ROM 选择器错乱;会话历史加载超时放宽至 45 秒,失败可重试;网关端口自定义(插件管理页可改,环境变量仍优先)+ 启动前端口占用检测;峰谷计费提醒改前台服务驱动,后台按时必达;消息 Markdown 渲染(标题/代码块/列表/链接);修复 DSH 重启后网关上游端口不刷新(Issue #1);新增测试通知按钮。"
43
- },
44
- {
45
- "version": "0.6.3",
46
- "notes": "修复 DSH 重启后网关 upstream 端口不刷新(Issue #1):/health 增加 upstream/upstreamOk/pid 探测;插件 ensureGateway 检测上游变化或不可达时自动杀旧网关并按新 DSH_UPSTREAM 重启;启动/刷新写插件日志与 PID 文件。"
47
47
  }
48
48
  ]
49
49
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.6.12"
2
+ "version": "0.6.13"
3
3
  }