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.
- package/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +49 -29
- package/index.mjs +3 -1
- package/package.json +1 -1
- package/public/announcements.json +8 -0
- package/public/app.js +17 -4
- package/public/desktop/desktop.js +10 -3
- package/public/update.json +8 -4
- package/public/version.json +1 -1
package/apk/dsh-remote.apk
CHANGED
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
816
|
-
if (
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
844
|
+
clearConnectTimer()
|
|
845
|
+
state.connected = true
|
|
846
|
+
state.lastConnectAt = Date.now()
|
|
847
|
+
state.lastError = ''
|
|
848
|
+
state.attempt = 0
|
|
825
849
|
}
|
|
826
|
-
|
|
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
|
-
|
|
834
|
-
|
|
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
|
-
|
|
846
|
-
if (
|
|
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
|
-
// 反馈收集器: 环境变量可覆盖,
|
|
962
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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'))
|
package/public/update.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.6.
|
|
2
|
+
"version": "0.6.10",
|
|
3
3
|
"apkUrl": "dsh-remote.apk",
|
|
4
|
-
"sha256": "
|
|
5
|
-
"releasedAt": "2026-08-
|
|
6
|
-
"notes": "0.6.
|
|
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 收紧与统计旁路优化。"
|
package/public/version.json
CHANGED