dsh-remote-plugin 0.6.9 → 0.6.10

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.
Binary file
package/gateway.cjs CHANGED
@@ -797,53 +797,70 @@ function startEventCollector(kind) {
797
797
  }
798
798
  const connect = () => {
799
799
  if (stopped) return
800
+ let current
800
801
  try {
801
- ws = new WebSocket(url)
802
+ current = new WebSocket(url)
803
+ ws = current
802
804
  } catch (err) {
803
805
  state.lastError = String(err?.message || err)
806
+ state.connected = false
807
+ state.reconnects++
804
808
  schedule()
805
809
  return
806
810
  }
807
- const current = ws
811
+ let finished = false
812
+ const clearConnectTimer = () => {
813
+ if (connectTimer) clearTimeout(connectTimer)
814
+ connectTimer = null
815
+ }
816
+ // Node WebSocket 在 CONNECTING 阶段失败时可能只触发 error,
817
+ // 调用 close() 后不保证再触发 close。每次尝试因此必须有一个
818
+ // 幂等的收口,任何 error/close/timeout 都只安排一次重连。
819
+ const finishAndRetry = (closeCode = 0, closeReason = '') => {
820
+ if (finished) return
821
+ finished = true
822
+ clearConnectTimer()
823
+ state.connected = false
824
+ state.lastCloseCode = Number(closeCode) || 0
825
+ state.lastCloseReason = String(closeReason || '')
826
+ if (ws === current) ws = null
827
+ if (stopped) return
828
+ state.reconnects++
829
+ schedule()
830
+ }
808
831
  connectTimer = setTimeout(() => {
809
- if (ws === current && current.readyState === 0) {
832
+ if (!finished && ws === current && current.readyState === 0) {
810
833
  state.lastError = 'websocket connect timeout'
834
+ finishAndRetry()
811
835
  try { current.close() } catch {}
812
836
  }
813
837
  }, WS_UPGRADE_TIMEOUT_MS)
814
838
  connectTimer.unref?.()
815
- ws.onopen = () => {
816
- if (connectTimer) clearTimeout(connectTimer)
817
- connectTimer = null
818
- if (state) {
819
- state.connected = true
820
- state.lastConnectAt = Date.now()
821
- state.lastError = ''
822
- state.attempt = 0
839
+ current.onopen = () => {
840
+ if (finished || ws !== current || stopped) {
841
+ try { current.close() } catch {}
842
+ return
823
843
  }
824
- if (stopped) { try { ws.close() } catch {} }
844
+ clearConnectTimer()
845
+ state.connected = true
846
+ state.lastConnectAt = Date.now()
847
+ state.lastError = ''
848
+ state.attempt = 0
825
849
  }
826
- ws.onmessage = (ev) => {
850
+ current.onmessage = (ev) => {
827
851
  if (stopped) return
828
852
  try {
829
853
  const data = typeof ev.data === 'string' ? ev.data : Buffer.isBuffer(ev.data) ? ev.data.toString() : String(ev.data)
830
854
  pushEvent(kind, JSON.parse(data), data)
831
855
  } catch {}
832
856
  }
833
- ws.onclose = () => {
834
- if (connectTimer) clearTimeout(connectTimer)
835
- connectTimer = null
836
- if (state) {
837
- state.connected = false
838
- state.reconnects++
839
- state.lastCloseCode = Number(current?.closeCode) || 0
840
- state.lastCloseReason = String(current?.closeReason || '')
841
- }
842
- ws = null
843
- schedule()
857
+ current.onclose = (ev) => {
858
+ finishAndRetry(ev?.code, ev?.reason)
844
859
  }
845
- ws.onerror = (err) => {
846
- if (state) state.lastError = String(err?.message || 'websocket error')
860
+ current.onerror = (err) => {
861
+ if (finished) return
862
+ state.lastError = String(err?.error?.message || err?.message || 'websocket error')
863
+ finishAndRetry()
847
864
  try { current.close() } catch {}
848
865
  }
849
866
  }
@@ -958,8 +975,9 @@ function serveStats(req, res, url) {
958
975
  // ---------- 反馈提交 ----------
959
976
  const feedbackThrottle = new Map() // ip -> 上次受理时间戳
960
977
  const FEEDBACK_WINDOW_MS = 60 * 1000
961
- // 反馈收集器: 环境变量可覆盖, 默认 Tailscale 内网地址
962
- const FEEDBACK_URL = process.env.DSH_REMOTE_FEEDBACK_URL || 'http://100.84.128.29/submit'
978
+ // 反馈收集器: 环境变量可覆盖, 默认使用 Tailscale Funnel 提供的公网 HTTPS
979
+ // 入口。这里是公开的 ts.net 域名,不要求提交反馈的用户加入 tailnet。
980
+ const FEEDBACK_URL = process.env.DSH_REMOTE_FEEDBACK_URL || 'https://vm-0-2-ubuntu.tail1f6fc4.ts.net/submit'
963
981
 
964
982
  function maskIp(ip) {
965
983
  if (!ip) return 'unknown'
@@ -2193,7 +2211,9 @@ async function serveHealth(res) {
2193
2211
 
2194
2212
  function lanAddresses() {
2195
2213
  const out = []
2196
- for (const infos of Object.values(os.networkInterfaces())) {
2214
+ let groups
2215
+ try { groups = Object.values(os.networkInterfaces()) } catch { return out }
2216
+ for (const infos of groups) {
2197
2217
  for (const info of infos || []) {
2198
2218
  if (info.family === 'IPv4' && !info.internal) out.push(info.address)
2199
2219
  }
package/index.mjs CHANGED
@@ -71,7 +71,9 @@ let dshListen = { host: '127.0.0.1', port: 3080 }
71
71
 
72
72
  function lanIPs() {
73
73
  const out = []
74
- for (const list of Object.values(networkInterfaces())) {
74
+ let groups
75
+ try { groups = Object.values(networkInterfaces()) } catch { return out }
76
+ for (const list of groups) {
75
77
  for (const it of list ?? []) {
76
78
  if (it.family === 'IPv4' && !it.internal) out.push(it.address)
77
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-plugin",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
@@ -7,6 +7,14 @@
7
7
  "minVersion": "0.6.7",
8
8
  "maxVersion": "",
9
9
  "publishedAt": "2026-08-21T15:00:00+08:00"
10
+ },
11
+ {
12
+ "id": "2026-08-22-0.6.9-progress",
13
+ "title": "dsh-Remote 0.6.9 更新公告",
14
+ "content": "各位开发者们下午好呀!在昨天的开发中,我对dsh-Remote的UI和网关部分进行了整体重构,目前的UI相比以往会更加简洁一些,同时网络连接稳定性也得到了增强,同时也吸取了PR中的部分功能,不知道大家使用后感觉体验如何呢?\n\n现在deepseek出了官方多模态模型,dsh的新版本也可以识别图片啦,所以我为app添加了插入图片附件的功能,以便更好地适配新版本。\n\n如果关于插件开发有更多的想法与建议,亦或是遇到了一些bug都可以通过各种渠道进行反馈,不过如果是在github上的话,由于目前版本更新较为频繁,更推荐各位开发者们提交issues进行反馈,提交PR可能由于版本不是最新还需要手动移植。感谢大家的支持与反馈!\n\n(设置中已新增往期公告功能,各位开发者可以查阅往期公告啦!)",
15
+ "minVersion": "0.6.9",
16
+ "maxVersion": "",
17
+ "publishedAt": "2026-08-22T12:00:00+08:00"
10
18
  }
11
19
  ]
12
20
  }
package/public/app.js CHANGED
@@ -1641,7 +1641,10 @@ function insertLiveEvent(event) {
1641
1641
  h.renderStart = Math.max(0, h.renderEnd - 200)
1642
1642
  renderHistory(false, 'bottom')
1643
1643
  } else {
1644
- renderHistory(false, 'keep')
1644
+ // 图片异步撑高历史区后,用户可能瞬间不再满足 nearBottom。新回复仍须
1645
+ // 扩展可见窗口,但保持当前阅读位置,不能等到重新进入会话才出现。
1646
+ h.renderEnd = h.visible.length
1647
+ renderHistory(false, 'fixed')
1645
1648
  }
1646
1649
  scheduleHistoryCacheSave()
1647
1650
  }
@@ -1684,6 +1687,7 @@ function renderHistory(reset, mode = 'bottom') {
1684
1687
  box.innerHTML = filtered.slice(start, end).map(e => eventHtml(e, { toolNames })).join('')
1685
1688
  if (reset || mode === 'bottom') box.scrollTop = box.scrollHeight
1686
1689
  else if (mode === 'keep') box.scrollTop = Math.max(0, oldTop + (box.scrollHeight - oldH))
1690
+ else if (mode === 'fixed') box.scrollTop = oldTop
1687
1691
  updateRail()
1688
1692
  }
1689
1693
 
@@ -1856,8 +1860,11 @@ function statsHtml(s) {
1856
1860
  return html || '<div class="empty">' + t('stats.empty') + '</div>'
1857
1861
  }
1858
1862
 
1863
+ let sessionCardsRenderGeneration = 0
1859
1864
  async function renderSessionCards() {
1860
- const s = state.byId.get(state.current)
1865
+ const renderGeneration = ++sessionCardsRenderGeneration
1866
+ const sessionId = state.current
1867
+ const s = state.byId.get(sessionId)
1861
1868
  const box = $('session-cards')
1862
1869
  const statsBox = $('stats-body')
1863
1870
  if (!s) { box.innerHTML = ''; if (statsBox) statsBox.innerHTML = ''; return }
@@ -1888,7 +1895,8 @@ async function renderSessionCards() {
1888
1895
  btn.addEventListener('click', () => goalAction(btn.dataset.goal)))
1889
1896
 
1890
1897
  // 子代理
1891
- const sub = await safeRpc('subagent.list', { parentSessionId: state.current })
1898
+ const sub = await safeRpc('subagent.list', { parentSessionId: sessionId })
1899
+ if (renderGeneration !== sessionCardsRenderGeneration || state.current !== sessionId) return
1892
1900
  if (sub?.entries?.length) {
1893
1901
  const rows = sub.entries.map(e => {
1894
1902
  if (e.kind === 'diagnostic') return `<div class="card-row"><span class="k">${t('subagent.diagnostic')}</span><span class="v">${esc(e.reason)}</span></div>`
@@ -2326,7 +2334,9 @@ function renderOverview() {
2326
2334
  const ring = $('overview-pulse-ring')
2327
2335
  if (!ring) return
2328
2336
  const checks = {
2329
- gateway: !!state.token && !!state.server,
2337
+ // 独立网关页面默认走同源,此时 state.server 合法地为空;不能因此把
2338
+ // 已连接网关误报为离线。Capacitor 等非 HTTP 页面仍要求显式服务器。
2339
+ gateway: !!state.token && (!!state.server || /^https?:$/.test(location.protocol)),
2330
2340
  dsh: !!state.hostInfo,
2331
2341
  mux: !!state.streamsOk?.mux,
2332
2342
  host: !!state.streamsOk?.host
@@ -3633,6 +3643,9 @@ function showSettingsPage(name) {
3633
3643
 
3634
3644
  function updateConn() {
3635
3645
  const el = $('conn-badge')
3646
+ // mux/host 的打开顺序不稳定;连接状态改变时同步重绘总览,避免后打开的
3647
+ // 通道只更新顶栏、总览却永久停留在 3/4。
3648
+ renderOverview()
3636
3649
  const cur = state.servers.find(s => s.url === state.server)
3637
3650
  const ms = state.serverLatency[state.server]
3638
3651
  const curGroup = cur ? cur.group : state.activeGroup
@@ -1378,9 +1378,12 @@ function renderHistory() {
1378
1378
  }
1379
1379
 
1380
1380
  /* ---------------- 会话信息卡(goal / todo / 子代理) ---------------- */
1381
+ let sessionCardsRenderGeneration = 0
1381
1382
  async function renderSessionCards() {
1383
+ const renderGeneration = ++sessionCardsRenderGeneration
1384
+ const sessionId = state.current
1382
1385
  const box = $('session-cards')
1383
- const s = state.byId.get(state.current)
1386
+ const s = state.byId.get(sessionId)
1384
1387
  if (!box) return
1385
1388
  if (!s) { box.innerHTML = ''; return }
1386
1389
  const goal = goalOf(s)
@@ -1406,7 +1409,8 @@ async function renderSessionCards() {
1406
1409
  box.querySelectorAll('[data-goal]').forEach(btn =>
1407
1410
  btn.addEventListener('click', () => goalAction(btn.dataset.goal)))
1408
1411
 
1409
- const sub = await safeRpc('subagent.list', { parentSessionId: state.current }, '')
1412
+ const sub = await safeRpc('subagent.list', { parentSessionId: sessionId }, '')
1413
+ if (renderGeneration !== sessionCardsRenderGeneration || state.current !== sessionId) return
1410
1414
  if (sub?.entries?.length) {
1411
1415
  const rows = sub.entries.map(e => {
1412
1416
  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>`
@@ -2011,7 +2015,8 @@ function renderOverviewDesktop() {
2011
2015
  const ring = $('ds-overview-pulse-ring')
2012
2016
  if (!ring) return
2013
2017
  const checks = {
2014
- gateway: !!state.token && !!state.server,
2018
+ // 桌面独立页面默认使用当前 origin,state.server 为空不代表网关离线。
2019
+ gateway: !!state.token && (!!state.server || /^https?:$/.test(location.protocol)),
2015
2020
  dsh: !!state.hostInfo,
2016
2021
  mux: !!state.streamsOk?.mux,
2017
2022
  host: !!state.streamsOk?.host
@@ -2119,6 +2124,8 @@ function showSettingsPage(name) {
2119
2124
  }
2120
2125
  function updateConn() {
2121
2126
  const el = $('conn-badge')
2127
+ // 双实时通道可能按任意顺序打开,连接刷新必须同时刷新系统总览。
2128
+ renderOverviewDesktop()
2122
2129
  const cur = state.servers.find(s => s.url === state.server)
2123
2130
  const group = cur ? cur.group : state.activeGroup
2124
2131
  const label = cur ? (cur.note || cur.url) : (state.server || t('ds.origin'))
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.6.9",
2
+ "version": "0.6.10",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "b8d869a6a6840261bc511005e4c86e3a2853df15f29ee33406e5c9e1f21f5796",
5
- "releasedAt": "2026-08-21T19:36:51.824Z",
6
- "notes": "0.6.9 正式版:全面优化网络连接稳定性,重构网关实时链路与连接管理;修复 downlink-only 心跳断联、网关重启风暴和 WebSocket 状态竞态;新增 VPN 友好 Ping/Pong、集中 mux/host collector、短时 WebSocket ticket、CORS 收紧与统计旁路优化。",
4
+ "sha256": "1aa45a7edcadbbfc5285d30308e6abf2fac7c7eaaf4ad6cf831df1a06a4f48b9",
5
+ "releasedAt": "2026-08-23T08:13:20.716Z",
6
+ "notes": "0.6.10 正式版:修复 DSH/网关重启后事件通道不自动恢复、启动阶段网卡读取异常、同源状态误报、双通道总览卡在 3/4、图片后实时回复不可见及子代理重复;新增完整用户链路与重启生命周期测试;公网反馈切换到安全的 HTTPS 临时入口并加强来源 IP 防伪造、节流与隐私保护。",
7
7
  "history": [
8
+ {
9
+ "version": "0.6.10",
10
+ "notes": "0.6.10 正式版:修复 DSH/网关重启后事件通道不自动恢复、启动阶段网卡读取异常、同源状态误报、双通道总览卡在 3/4、图片后实时回复不可见及子代理重复;新增完整用户链路与重启生命周期测试;公网反馈切换到安全的 HTTPS 临时入口并加强来源 IP 防伪造、节流与隐私保护。"
11
+ },
8
12
  {
9
13
  "version": "0.6.9",
10
14
  "notes": "0.6.9 正式版:全面优化网络连接稳定性,重构网关实时链路与连接管理;修复 downlink-only 心跳断联、网关重启风暴和 WebSocket 状态竞态;新增 VPN 友好 Ping/Pong、集中 mux/host collector、短时 WebSocket ticket、CORS 收紧与统计旁路优化。"
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.6.9"
2
+ "version": "0.6.10"
3
3
  }