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.
package/public/app.js CHANGED
@@ -55,6 +55,7 @@ const state = {
55
55
  autoSelect: { '默认': true }, // 组内自动测速选优 / 手动指定
56
56
  groupActive: { '默认': '' }, // 每组当前生效的 server id(手动模式)
57
57
  serverLatency: {}, // url -> 最近一次 /health 测速毫秒数
58
+ gatewayHealth: {}, // url -> /health 协议版本与能力声明
58
59
  selectingServer: false, // 防重入: 测速/切换中
59
60
  sessions: [],
60
61
  sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
@@ -64,6 +65,7 @@ const state = {
64
65
  hostInfo: null,
65
66
  localVersion: '',
66
67
  updateInfo: null,
68
+ warnedGatewayVersions: new Set(),
67
69
  announcement: null,
68
70
  announcements: [],
69
71
  approvals: [], // 待处理审批
@@ -355,6 +357,7 @@ async function getWsTicket() {
355
357
  const token = state.token
356
358
  const server = state.server
357
359
  wsTicketPromise = (async () => {
360
+ if (activeGatewayCapability('wsTicket') === false) throw new Error('ws ticket unsupported')
358
361
  const res = await fetch(apiUrl('/api/ws-ticket'), {
359
362
  method: 'POST',
360
363
  headers: {
@@ -621,7 +624,10 @@ async function pingServer(base) {
621
624
  const timer = setTimeout(() => ctrl.abort(), 3500)
622
625
  try {
623
626
  const res = await fetch(u + '/health?t=' + Date.now(), { signal: ctrl.signal, cache: 'no-store' })
624
- return res.ok ? Math.round(performance.now() - t0) : Infinity
627
+ if (!res.ok) return Infinity
628
+ const health = await res.json().catch(() => null)
629
+ if (health && typeof health === 'object') state.gatewayHealth[u] = health
630
+ return Math.round(performance.now() - t0)
625
631
  } catch {
626
632
  return Infinity
627
633
  } finally {
@@ -629,6 +635,13 @@ async function pingServer(base) {
629
635
  }
630
636
  }
631
637
 
638
+ function activeGatewayCapability(name) {
639
+ const key = String(state.server || location.origin || '').replace(/\/+$/, '')
640
+ const capabilities = state.gatewayHealth[key]?.capabilities
641
+ if (!capabilities || !Object.prototype.hasOwnProperty.call(capabilities, name)) return null
642
+ return Number(capabilities[name]) > 0
643
+ }
644
+
632
645
  async function selectFastestServer({ silent = false, reconnect = true } = {}) {
633
646
  if (state.selectingServer) return null
634
647
  state.selectingServer = true
@@ -677,6 +690,7 @@ async function selectFastestServer({ silent = false, reconnect = true } = {}) {
677
690
  else if (chosen) toast(t('speed.manualUsing', { url: chosen, ms: Number.isFinite(ms) ? ms : '—' }), 'ok')
678
691
  else toast(t('speed.allDown'), 'err')
679
692
  }
693
+ await maybeWarnAppBehindGateway()
680
694
  return chosen
681
695
  } finally {
682
696
  state.selectingServer = false
@@ -1343,6 +1357,10 @@ function applyProjection(sessionId, key, value, seq) {
1343
1357
  }
1344
1358
  function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('session.unknown')) }
1345
1359
  function short(id) { return '…' + String(id).slice(-8) }
1360
+ function isTopLevelSession(session) {
1361
+ return !!session && !session.parentSessionId && session.origin !== 'subagent'
1362
+ }
1363
+ function topLevelSessions() { return state.sessions.filter(isTopLevelSession) }
1346
1364
  const GOAL_TERMINAL_PHASES = new Set(['complete', 'cleared'])
1347
1365
  function isGoalTerminal(goal) {
1348
1366
  return !!goal && GOAL_TERMINAL_PHASES.has(goal.phase)
@@ -1523,7 +1541,7 @@ function renderWorkbench() {
1523
1541
  panel.innerHTML = projects.map(w => {
1524
1542
  const id = String(w.workspaceId || '')
1525
1543
  const open = !!state.wbOpenProjects[id]
1526
- const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).filter(s => !archivedSet.has(s.sessionId))
1544
+ const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(isTopLevelSession).filter(s => !archivedSet.has(s.sessionId))
1527
1545
  const body = open ? `<div class="wb-sessions">${sessions.length ? sessions.map(s => `
1528
1546
  <div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
1529
1547
  <button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}">
@@ -1559,7 +1577,7 @@ function workspaceDisplayName(label) {
1559
1577
  return parts[parts.length - 1] || value
1560
1578
  }
1561
1579
  function sortedSessions() {
1562
- const items = [...state.sessions]
1580
+ const items = topLevelSessions()
1563
1581
  if (state.sessionSort === 'workspace') {
1564
1582
  return items.sort((a, b) => {
1565
1583
  const aw = sessionCwd(a) || '\uffff'
@@ -1626,7 +1644,7 @@ function renderSessions() {
1626
1644
  const sort = $('session-sort')
1627
1645
  if (sort) { sort.value = state.sessionSort; syncCustomSelect(sort) }
1628
1646
  $('home-empty').classList.toggle('hidden', visible.length > 0)
1629
- const running = state.sessions.filter(s => s.running).length
1647
+ const running = topLevelSessions().filter(s => s.running).length
1630
1648
  const pending = state.approvals.length + state.questions.length
1631
1649
  $('stat-strip').innerHTML = `
1632
1650
  <div class="stat running"><div class="v">${running}</div><div class="k">${t('sessions.statRunning')}</div></div>
@@ -1668,7 +1686,7 @@ function bindNativeBack() {
1668
1686
  if ($('composer-wrap')?.classList.contains('fs')) { setComposerFullscreen(false); return }
1669
1687
  if (customSelectCurrent) { closeCustomSelect(); return }
1670
1688
  const openModal = [...document.querySelectorAll('.modal')].find(m => !m.classList.contains('hidden'))
1671
- if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else openModal.classList.add('hidden'); return } // 先关弹窗
1689
+ if (openModal) { if (openModal.id === 'modal-notes') closeNotesModal(); else if (openModal.id === 'modal-archive') closeArchiveConfirm(); else if (openModal.id === 'modal-app-version-warning') closeAppVersionWarning(false); else openModal.classList.add('hidden'); return } // 先关弹窗
1672
1690
  if (document.body.classList.contains('in-session')) { closeSession(); return } // 会话页 → 回主页
1673
1691
  if (!$('view-files').classList.contains('hidden')) { // 文件页 → 上级目录 → 主页
1674
1692
  if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) { fsUp(); return }
@@ -1716,10 +1734,78 @@ const HISTORY_MAX_VISIBLE = 5000 // 已加载的可显示事件上限(消息/
1716
1734
  function emptyHistory() {
1717
1735
  return {
1718
1736
  visible: [], seqs: new Set(), minSeq: Infinity,
1719
- hasMore: false, loading: false, renderStart: 0, renderEnd: 0
1737
+ hasMore: false, loading: false, renderStart: 0, renderEnd: 0,
1738
+ partialReasoning: new Map()
1720
1739
  }
1721
1740
  }
1722
1741
 
1742
+ function reasoningStreamKey(data, index) {
1743
+ return `${data?.turn ?? '?'}:${data?.step ?? '?'}:${index ?? '?'}`
1744
+ }
1745
+
1746
+ /**
1747
+ * DSH 的实时思考以 assistant/chunk 下发,历史尾页可能把增量压成
1748
+ * reasoning-chunks。最终 assistant/message 到达后再由正式消息接管展示。
1749
+ */
1750
+ function applyReasoningStreamEvent(event) {
1751
+ const h = state.history
1752
+ const data = event?.data || {}
1753
+ let changed = false
1754
+ if (event?.type === 'assistant/chunk') {
1755
+ const chunk = data.chunk || {}
1756
+ const key = reasoningStreamKey(data, chunk.index)
1757
+ if (chunk.type === 'block-start' && chunk.blockType === 'reasoning') {
1758
+ h.partialReasoning.set(key, { turn: data.turn, step: data.step, index: chunk.index, text: '' })
1759
+ changed = true
1760
+ } else if (chunk.type === 'reasoning-delta') {
1761
+ const item = h.partialReasoning.get(key) || { turn: data.turn, step: data.step, index: chunk.index, text: '' }
1762
+ item.text += String(chunk.text || '')
1763
+ h.partialReasoning.set(key, item)
1764
+ changed = true
1765
+ } else if (chunk.type === 'block-end' && chunk.block?.type === 'reasoning') {
1766
+ h.partialReasoning.set(key, {
1767
+ turn: data.turn, step: data.step, index: chunk.index,
1768
+ text: String(chunk.block.text ?? chunk.block.content ?? '')
1769
+ })
1770
+ changed = true
1771
+ }
1772
+ } else if (event?.type === 'reasoning-chunks') {
1773
+ const key = reasoningStreamKey(data, data.index)
1774
+ const item = h.partialReasoning.get(key) || { turn: data.turn, step: data.step, index: data.index, text: '' }
1775
+ item.text += Array.isArray(data.texts) ? data.texts.join('') : String(data.text || '')
1776
+ h.partialReasoning.set(key, item)
1777
+ changed = true
1778
+ } else if (event?.type === 'assistant/message') {
1779
+ for (const [key, item] of h.partialReasoning) {
1780
+ if (item.turn === data.turn && item.step === data.step) {
1781
+ h.partialReasoning.delete(key)
1782
+ changed = true
1783
+ }
1784
+ }
1785
+ }
1786
+ return changed
1787
+ }
1788
+
1789
+ let reasoningRenderTimer = null
1790
+ function scheduleReasoningRender() {
1791
+ if (reasoningRenderTimer) return
1792
+ reasoningRenderTimer = setTimeout(() => {
1793
+ reasoningRenderTimer = null
1794
+ if (!state.current) return
1795
+ const box = $('history')
1796
+ const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 240
1797
+ renderHistory(false, nearBottom ? 'bottom' : 'fixed')
1798
+ }, 80)
1799
+ }
1800
+
1801
+ function partialReasoningHtml() {
1802
+ return [...state.history.partialReasoning.values()]
1803
+ .filter(item => item.text)
1804
+ .sort((a, b) => (a.turn ?? 0) - (b.turn ?? 0) || (a.step ?? 0) - (b.step ?? 0) || (a.index ?? 0) - (b.index ?? 0))
1805
+ .map(item => `<div class="msg assistant reasoning-live"><div class="role">${esc(t('role.dsh'))}</div><details class="tool" open><summary>${esc(t('block.thinkingLive'))}</summary><div class="tool-text">${esc(truncate(item.text, 12000))}</div></details></div>`)
1806
+ .join('')
1807
+ }
1808
+
1723
1809
  function trimVisible() {
1724
1810
  const h = state.history
1725
1811
  if (h.visible.length <= HISTORY_MAX_VISIBLE) return
@@ -1806,9 +1892,11 @@ async function loadHistory(reset) {
1806
1892
 
1807
1893
  const incoming = v.events || []
1808
1894
  let added = 0
1895
+ if (reset) state.history.partialReasoning.clear()
1809
1896
  for (const entry of incoming) {
1810
1897
  const ev = entry?.event
1811
1898
  const seq = ev?.seq
1899
+ applyReasoningStreamEvent(ev)
1812
1900
  if (seq == null || state.history.seqs.has(seq)) continue
1813
1901
  if (!shouldShowEvent(ev.type)) continue // chunk 等内部事件不保留
1814
1902
  state.history.seqs.add(seq)
@@ -1835,8 +1923,16 @@ async function loadHistory(reset) {
1835
1923
 
1836
1924
  function insertLiveEvent(event) {
1837
1925
  const h = state.history
1926
+ const reasoningChanged = applyReasoningStreamEvent(event)
1927
+ if (event?.type === 'assistant/chunk' || event?.type === 'reasoning-chunks') {
1928
+ if (reasoningChanged) scheduleReasoningRender()
1929
+ return
1930
+ }
1838
1931
  const seq = event?.seq
1839
- if (seq == null || h.seqs.has(seq) || !shouldShowEvent(event.type)) return
1932
+ if (seq == null || h.seqs.has(seq) || !shouldShowEvent(event.type)) {
1933
+ if (reasoningChanged) scheduleReasoningRender()
1934
+ return
1935
+ }
1840
1936
  h.seqs.add(seq)
1841
1937
  h.visible.push({ seq, event })
1842
1938
  h.visible.sort((a, b) => a.seq - b.seq)
@@ -1870,7 +1966,8 @@ function renderHistory(reset, mode = 'bottom') {
1870
1966
  const h = state.history
1871
1967
  const filtered = filteredEntries()
1872
1968
  const len = filtered.length
1873
- if (!len) {
1969
+ const reasoningHtml = partialReasoningHtml()
1970
+ if (!len && !reasoningHtml) {
1874
1971
  box.innerHTML = '<div class="empty">' + t('history.empty') + '</div>'
1875
1972
  h.renderStart = 0; h.renderEnd = 0
1876
1973
  updateRail()
@@ -1891,7 +1988,7 @@ function renderHistory(reset, mode = 'bottom') {
1891
1988
  const d = e.event.data || {}
1892
1989
  if (d.callId && d.name) toolNames.set(d.callId, d.name)
1893
1990
  }
1894
- box.innerHTML = filtered.slice(start, end).map(e => eventHtml(e, { toolNames })).join('')
1991
+ box.innerHTML = filtered.slice(start, end).map(e => eventHtml(e, { toolNames })).join('') + reasoningHtml
1895
1992
  if (reset || mode === 'bottom') box.scrollTop = box.scrollHeight
1896
1993
  else if (mode === 'keep') box.scrollTop = Math.max(0, oldTop + (box.scrollHeight - oldH))
1897
1994
  else if (mode === 'fixed') box.scrollTop = oldTop
@@ -2393,16 +2490,37 @@ function renderEffortMenu() {
2393
2490
  const cur = state.models.current
2394
2491
  const provider = (state.models.groups || []).find(g => g.id === cur?.provider)
2395
2492
  const model = (provider?.models || []).find(m => m.id === cur?.model)
2396
- const efforts = model?.reasoning?.efforts || []
2397
- group.classList.toggle('hidden', !efforts.length)
2493
+ const { efforts, defaultEffort, custom } = reasoningEffortOptions(model)
2494
+ group.classList.toggle('hidden', !cur || !efforts.length)
2398
2495
  box.innerHTML = efforts.map(e => {
2399
- const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === model.reasoning.defaultEffort)
2496
+ const isCur = cur?.reasoningEffort === e.id || (!cur?.reasoningEffort && e.id === defaultEffort)
2400
2497
  return `<button class="menu-chip ${isCur ? 'current' : ''}" data-effort="${esc(e.id)}" title="${esc(e.description || '')}">${esc(e.name || e.id)}</button>`
2401
- }).join('')
2498
+ }).join('') + (custom ? `<span class="effort-hint">${esc(t('models.effortCustomHint'))}</span>` : '')
2402
2499
  box.querySelectorAll('[data-effort]').forEach(btn =>
2403
2500
  btn.addEventListener('click', () => selectSessionEffort(btn.dataset.effort)))
2404
2501
  }
2405
2502
 
2503
+ function reasoningEffortOptions(model) {
2504
+ const raw = Array.isArray(model?.reasoning?.efforts) && model.reasoning.efforts.length
2505
+ ? model.reasoning.efforts
2506
+ : (Array.isArray(model?.reasoningEfforts) && model.reasoningEfforts.length ? model.reasoningEfforts : null)
2507
+ const names = {
2508
+ low: t('models.effortLow'), high: t('models.effortHigh'), max: t('models.effortMax'), off: t('models.effortOff')
2509
+ }
2510
+ if (raw) {
2511
+ return {
2512
+ efforts: raw.map(e => typeof e === 'string' ? { id: e, name: names[e] || e } : e),
2513
+ defaultEffort: model?.reasoning?.defaultEffort,
2514
+ custom: !model?.reasoning?.efforts
2515
+ }
2516
+ }
2517
+ return {
2518
+ efforts: ['low', 'high', 'max'].map(id => ({ id, name: names[id] })),
2519
+ defaultEffort: undefined,
2520
+ custom: true
2521
+ }
2522
+ }
2523
+
2406
2524
  async function selectSessionEffort(effortId) {
2407
2525
  const cur = state.models.current
2408
2526
  if (!state.current || !cur) return
@@ -2615,8 +2733,9 @@ function renderOverview() {
2615
2733
  btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.overviewQuestion)))
2616
2734
  })
2617
2735
 
2618
- const running = state.sessions.filter(s => s.running).length
2619
- 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, 4)
2736
+ const topSessions = topLevelSessions()
2737
+ const running = topSessions.filter(s => s.running).length
2738
+ const sessions = topSessions.sort((a, b) => Number(b.running) - Number(a.running) || (new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))).slice(0, 4)
2620
2739
  const primary = $('overview-primary-action')
2621
2740
  if (primary) {
2622
2741
  let action = 'new'
@@ -3385,6 +3504,54 @@ function cmpVersion(a, b) {
3385
3504
  return 0
3386
3505
  }
3387
3506
 
3507
+ function isComparableVersion(value) {
3508
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(String(value || '').trim())
3509
+ }
3510
+
3511
+ function isAppBehindGateway(appVersion, gatewayVersion) {
3512
+ return isComparableVersion(appVersion) && isComparableVersion(gatewayVersion) && cmpVersion(gatewayVersion, appVersion) > 0
3513
+ }
3514
+
3515
+ function isAppVersionWarningOpen() {
3516
+ const modal = $('modal-app-version-warning')
3517
+ return !!modal && !modal.classList.contains('hidden')
3518
+ }
3519
+
3520
+ async function maybeWarnAppBehindGateway({ probe = false } = {}) {
3521
+ if (!CAP?.isNativePlatform?.() || !state.localVersion) return false
3522
+ const base = String(state.server || '').replace(/\/+$/, '')
3523
+ if (!base) return false
3524
+ let health = state.gatewayHealth[base]
3525
+ if (!health && probe) {
3526
+ await pingServer(base)
3527
+ health = state.gatewayHealth[base]
3528
+ }
3529
+ const gatewayVersion = String(health?.version || '').trim()
3530
+ if (!isAppBehindGateway(state.localVersion, gatewayVersion)) return false
3531
+ const warningKey = `${state.localVersion}\u0000${gatewayVersion}`
3532
+ if (state.warnedGatewayVersions.has(warningKey)) return false
3533
+ const anotherModal = [...document.querySelectorAll('.modal')]
3534
+ .some(modal => modal.id !== 'modal-app-version-warning' && !modal.classList.contains('hidden'))
3535
+ if (anotherModal) return false
3536
+ state.warnedGatewayVersions.add(warningKey)
3537
+ $('app-version-current').textContent = 'v' + state.localVersion
3538
+ $('app-version-gateway').textContent = 'v' + gatewayVersion
3539
+ $('modal-app-version-warning').classList.remove('hidden')
3540
+ setTimeout(() => $('app-version-update')?.focus(), 50)
3541
+ return true
3542
+ }
3543
+
3544
+ function closeAppVersionWarning(checkNow = false) {
3545
+ $('modal-app-version-warning').classList.add('hidden')
3546
+ if (checkNow) {
3547
+ showView('view-settings')
3548
+ showSettingsPage('about')
3549
+ void checkUpdate(false)
3550
+ } else {
3551
+ scheduleStartupNotices(150)
3552
+ }
3553
+ }
3554
+
3388
3555
  function resetUpdateExpand() {
3389
3556
  const desc = $('update-desc')
3390
3557
  if (desc) desc.classList.remove('expanded')
@@ -3672,7 +3839,7 @@ function closeAnnouncement(markSeen) {
3672
3839
  }
3673
3840
  async function fetchAnnouncements() {
3674
3841
  const base = updateBase()
3675
- if (!base || !state.localVersion) return false
3842
+ if (!base || !state.localVersion || isAppVersionWarningOpen()) return false
3676
3843
  try {
3677
3844
  const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(8000) : undefined
3678
3845
  const url = base + '/announcements.json?t=' + Date.now()
@@ -3713,6 +3880,16 @@ function startAnnouncementPolling() {
3713
3880
  window.addEventListener('online', () => { void checkAnnouncements() })
3714
3881
  }
3715
3882
 
3883
+ let startupNoticeTimer = null
3884
+ function scheduleStartupNotices(delay = 4000) {
3885
+ clearTimeout(startupNoticeTimer)
3886
+ startupNoticeTimer = setTimeout(async () => {
3887
+ if (isAppVersionWarningOpen()) return
3888
+ const shown = await checkAnnouncements()
3889
+ if (!shown && state.token && !isAppVersionWarningOpen()) checkUpdate(true)
3890
+ }, delay)
3891
+ }
3892
+
3716
3893
  /* ---------------- 更新内容弹窗 ---------------- */
3717
3894
  const NOTES_KEY = 'seenNotesVersion'
3718
3895
  let notesVersion = ''
@@ -4523,6 +4700,10 @@ function renderDshControlStatus(value) {
4523
4700
 
4524
4701
  async function loadDshControl() {
4525
4702
  if (!state.token || !$('dsh-control-desc')) return
4703
+ if (activeGatewayCapability('dshLifecycle') === false) {
4704
+ renderDshControlStatus({ supported: false, message: t('settings.dshUnsupported') })
4705
+ return
4706
+ }
4526
4707
  try {
4527
4708
  const res = await fetch(adminApiUrl('/admin/api/dsh'), { headers: { authorization: 'Bearer ' + state.token }, cache: 'no-store' })
4528
4709
  if (res.status === 401) return authFailure()
@@ -4775,6 +4956,7 @@ function bindUi() {
4775
4956
  document.addEventListener('keydown', (e) => {
4776
4957
  if (e.key === 'Escape' && !$('feedback-sheet').classList.contains('hidden')) { closeFeedbackSheet(); $('btn-feedback').focus() }
4777
4958
  else if (e.key === 'Escape' && !$('modal-feedback-success').classList.contains('hidden')) closeFeedbackSuccess()
4959
+ else if (e.key === 'Escape' && isAppVersionWarningOpen()) closeAppVersionWarning(false)
4778
4960
  })
4779
4961
  $('btn-new-session').addEventListener('click', newSession)
4780
4962
  $('session-workspace-filter').addEventListener('change', (e) => {
@@ -4965,6 +5147,11 @@ function bindUi() {
4965
5147
  $('btn-check-update').addEventListener('click', () => checkUpdate(false))
4966
5148
  $('btn-download-update').addEventListener('click', downloadUpdate)
4967
5149
  $('btn-update-expand').addEventListener('click', toggleUpdateExpand)
5150
+ $('app-version-later').addEventListener('click', () => closeAppVersionWarning(false))
5151
+ $('app-version-update').addEventListener('click', () => closeAppVersionWarning(true))
5152
+ $('modal-app-version-warning').addEventListener('click', (e) => {
5153
+ if (e.target === $('modal-app-version-warning')) closeAppVersionWarning(false)
5154
+ })
4968
5155
  $('btn-reset').addEventListener('click', () => {
4969
5156
  if (!confirm(t('settings.confirmReset'))) return
4970
5157
  LS.del('token'); LS.del('notify'); LS.del('server'); LS.del('mobileEnterAction'); LS.del(ANNOUNCEMENTS_KEY); LS.del(ANNOUNCEMENT_HISTORY_KEY); LS.del(ANNOUNCEMENT_VOTES_KEY)
@@ -5119,6 +5306,7 @@ async function boot() {
5119
5306
  } else {
5120
5307
  // 多服务器: 启动时静默测速一次, 选最快的连接(同源页面也参与比较)
5121
5308
  await selectFastestServer({ silent: true, reconnect: false })
5309
+ await maybeWarnAppBehindGateway({ probe: true })
5122
5310
  openStreams()
5123
5311
  await refreshAll()
5124
5312
  const host = await safeRpc('host.describe', {}, '')
@@ -5128,10 +5316,7 @@ async function boot() {
5128
5316
  // 网关从中央 HTTPS 公告源读取并在不可达时回退内置文件。前台每 30 秒检查,
5129
5317
  // 回到前台或网络恢复时立即补查;公告优先,避免启动时两个弹窗重叠。
5130
5318
  startAnnouncementPolling()
5131
- setTimeout(async () => {
5132
- const shown = await checkAnnouncements()
5133
- if (!shown && state.token) checkUpdate(true)
5134
- }, 4000)
5319
+ scheduleStartupNotices()
5135
5320
  renderPending()
5136
5321
  }
5137
5322
 
@@ -283,6 +283,11 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
283
283
  .ds-model-chip.current { background: var(--dsr-accent-soft); border-color: var(--dsr-accent-line); color: var(--dsr-accent-strong); font-weight: 600; }
284
284
  .ds-model-effort-group { padding-top: 2px; border-top: 1px solid var(--dsr-divider); margin-top: 2px; }
285
285
  .ds-model-effort-group.hidden { display: none; }
286
+ .ds-effort-hint { flex-basis: 100%; padding: 0 2px; color: var(--dsr-muted); font-size: 10.5px; line-height: 1.45; }
287
+ .ds-reasoning-live { border-color: var(--dsr-accent-line); }
288
+ .ds-reasoning-live summary { cursor: pointer; color: var(--dsr-accent-strong); }
289
+ .ds-reasoning-live summary::after { content: ''; display: inline-block; width: 6px; height: 6px; margin-left: 7px; border-radius: 50%; background: var(--dsr-accent-strong); animation: ds-reasoning-pulse 1.2s ease-in-out infinite; }
290
+ @keyframes ds-reasoning-pulse { 0%, 100% { opacity: .35; transform: scale(.8); } 50% { opacity: 1; transform: scale(1); } }
286
291
  .ds-composer { flex: none; display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--dsr-line); background: var(--dsr-panel); }
287
292
  .ds-composer textarea { width: 100%; min-width: 0; resize: none; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 12px; padding: 10px 14px; font: inherit; font-size: 13.5px; line-height: 1.5; outline: none; min-height: 44px; max-height: 120px; box-sizing: border-box; }
288
293
  .ds-composer-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
@@ -524,7 +524,8 @@
524
524
  'subagent.confirmInterrupt': '中断这个子代理当前回合?', 'subagent.interruptFailed': '中断失败', 'subagent.interruptSubmitted': '中断请求已提交',
525
525
  'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
526
526
  'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
527
- 'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}',
527
+ 'models.effortFailed': '切换思考深度失败', 'models.effortSwitched': '思考深度:{effort}', 'models.effortLow': '低', 'models.effortHigh': '高', 'models.effortMax': '极高', 'models.effortOff': '关闭', 'models.effortCustomHint': '该路由未公布档位,按 DSH 兼容值尝试;不支持时不会更改当前设置。',
528
+ 'block.thinking': '🧠 思考过程', 'block.thinkingLive': '🧠 思考中…',
528
529
  'menu.modelTitle': '模型切换', 'menu.effortTitle': '思考深度',
529
530
  },
530
531
  en: {
@@ -624,7 +625,8 @@
624
625
  'subagent.confirmInterrupt': 'Interrupt this subagent\'s current turn?', 'subagent.interruptFailed': 'Interrupt failed', 'subagent.interruptSubmitted': 'Interrupt requested',
625
626
  'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
626
627
  'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
627
- 'models.effortFailed': 'Failed to switch reasoning effort', 'models.effortSwitched': 'Reasoning effort: {effort}',
628
+ '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.',
629
+ 'block.thinking': '🧠 Thinking', 'block.thinkingLive': '🧠 Thinking…',
628
630
  'menu.modelTitle': 'Model', 'menu.effortTitle': 'Reasoning effort',
629
631
  }
630
632
  }