dsh-mesh-ui 0.0.6 → 0.0.7
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/lib/client.js +61 -13
- package/lib/collect.js +31 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -677,7 +677,7 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
|
|
|
677
677
|
h('span', null, '⋯')
|
|
678
678
|
)
|
|
679
679
|
),
|
|
680
|
-
h('div', { className: 'dm-hint' }, '拓扑
|
|
680
|
+
h('div', { className: 'dm-hint' }, '拓扑 v19 · 内容 ' + Math.round(layout.W) + '×' + Math.round(layout.H) + 'px · 卡 ' + Math.round(layout.cardW) + '×' + Math.round(layout.cardH) + ' · 缩放 ' + Math.round(view.scale * 100) + '% · 滚轮缩放 / 拖拽平移')
|
|
681
681
|
)
|
|
682
682
|
}
|
|
683
683
|
|
|
@@ -835,9 +835,11 @@ function sakuraTunnelOf(data, devKey) {
|
|
|
835
835
|
function deviceLinkState(data, peer, lanMap) {
|
|
836
836
|
var dk = peerDeviceKey(peer.name)
|
|
837
837
|
var t = peer.tunnel || {}
|
|
838
|
-
|
|
838
|
+
// 2026-11-10:蓝线状态以隧道端口 TCP 探测为准(HTTP /health 对高延迟节点会误判断连)
|
|
839
|
+
var probe = t.probe && typeof t.probe.up === "boolean" ? t.probe : null
|
|
840
|
+
var publicUp = (probe ? probe.up === true : false) || Boolean(peer.up)
|
|
839
841
|
var lan = lanMap ? lanMap[dk] : (t.kind === 'lan' ? { up: Boolean(peer.up), host: t.host, port: t.port, ip: t.ip || '' } : null)
|
|
840
|
-
var lanUp = Boolean(lan && lan.up)
|
|
842
|
+
var lanUp = Boolean(lan && (lan.probe && lan.probe.up === true ? true : lan.up))
|
|
841
843
|
var st = sakuraTunnelOf(data, dk)
|
|
842
844
|
var sakuraUp = st ? st.online === true : null
|
|
843
845
|
var anyUp = publicUp || sakuraUp === true || lanUp
|
|
@@ -858,7 +860,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
858
860
|
var localKey = peerDeviceKey(data.nodeName)
|
|
859
861
|
var devs = [{ devKey: localKey, name: nodeDisplay(data.nodeName) + "(本机)", up: true, port: localPort, host: localHost }].concat(peers.filter(function (p) { return !isLanPeer(p) }).map(function (p) {
|
|
860
862
|
var tu = p.tunnel || {}
|
|
861
|
-
return { devKey: peerDeviceKey(p.name), name: nodeDisplay(peerDeviceKey(p.name)), up: Boolean(p.up), port: tu.port || null, host: tu.host || 'relay.example.com' }
|
|
863
|
+
return { devKey: peerDeviceKey(p.name), name: nodeDisplay(peerDeviceKey(p.name)), up: (tu.probe ? Boolean(tu.probe.up) : Boolean(p.up)), port: tu.port || null, host: tu.host || 'relay.example.com' }
|
|
862
864
|
}))
|
|
863
865
|
var aliyunRows = devs.filter(function (d) { return d.port }).map(function (d) {
|
|
864
866
|
return h('div', { key: d.devKey, className: 'dm-device-row' },
|
|
@@ -997,7 +999,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
997
999
|
var lanMap = {}
|
|
998
1000
|
filterSelfPeers(data.peers || [], data.nodeName).forEach(function (p) {
|
|
999
1001
|
var t = p.tunnel || {}
|
|
1000
|
-
if (isLanPeer(p)) lanMap[peerDeviceKey(p.name)] = { up: Boolean(p.up), host: t.host, port: t.port, ip: t.ip || '' }
|
|
1002
|
+
if (isLanPeer(p)) lanMap[peerDeviceKey(p.name)] = { up: Boolean(p.up), host: t.host, port: t.port, ip: t.ip || '', probe: t.probe || null }
|
|
1001
1003
|
})
|
|
1002
1004
|
var nodes = [{
|
|
1003
1005
|
key: 'local',
|
|
@@ -1418,17 +1420,62 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1418
1420
|
var s2 = useState(''); var error = s2[0]; var setError = s2[1]
|
|
1419
1421
|
var s3 = useState(0); var tick = s3[0]; var setTick = s3[1]
|
|
1420
1422
|
var s4 = useState(''); var copied = s4[0]; var setCopied = s4[1]
|
|
1423
|
+
// 2026-11-10:真刷新 —— 点击后等待后台采集完成再渲染;本地冷却 + 忙锁防止连点刷爆请求
|
|
1424
|
+
var s6 = useState(""); var refreshHint = s6[0]; var setRefreshHint = s6[1]
|
|
1425
|
+
var forceRef = useRef({ at: 0, busy: false })
|
|
1421
1426
|
var s5 = useState('local'); var selectedKey = s5[0]; var setSelectedKey = s5[1]
|
|
1422
1427
|
|
|
1423
1428
|
function load(force) {
|
|
1429
|
+
if (!force) {
|
|
1430
|
+
setLoading(true)
|
|
1431
|
+
fetchStatus(false)
|
|
1432
|
+
.then(function (json) {
|
|
1433
|
+
setData(json)
|
|
1434
|
+
setError(json && json.ok === false && !json.warming ? json.error || "mesh-health error" : "")
|
|
1435
|
+
})
|
|
1436
|
+
.catch(function (err) { setError(String((err && err.message) || err)) })
|
|
1437
|
+
.finally(function () { setLoading(false) })
|
|
1438
|
+
return
|
|
1439
|
+
}
|
|
1440
|
+
var st = forceRef.current
|
|
1441
|
+
var now = Date.now()
|
|
1442
|
+
if (st.busy) return
|
|
1443
|
+
if (now - st.at < 8000) {
|
|
1444
|
+
var waitSec = Math.ceil((8000 - (now - st.at)) / 1000)
|
|
1445
|
+
setRefreshHint("刚采集过," + waitSec + "s 后可再刷新")
|
|
1446
|
+
setTimeout(function () { setRefreshHint("") }, 2200)
|
|
1447
|
+
return
|
|
1448
|
+
}
|
|
1449
|
+
st.busy = true
|
|
1450
|
+
st.at = now
|
|
1424
1451
|
setLoading(true)
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1452
|
+
setRefreshHint("采集中…")
|
|
1453
|
+
var prevAt = data && data.at
|
|
1454
|
+
var attempts = 0
|
|
1455
|
+
function poll() {
|
|
1456
|
+
attempts++
|
|
1457
|
+
fetchStatus(false).then(function (json) {
|
|
1458
|
+
var fresh = json && json.refreshing === false && json.at && json.at !== prevAt
|
|
1459
|
+
if (fresh || attempts >= 15) {
|
|
1460
|
+
setData(json)
|
|
1461
|
+
setError(json && json.ok === false && !json.warming ? json.error || "mesh-health error" : "")
|
|
1462
|
+
setLoading(false)
|
|
1463
|
+
st.busy = false
|
|
1464
|
+
setRefreshHint(fresh ? "已刷新" : "无新数据(采集超时)")
|
|
1465
|
+
setTimeout(function () { setRefreshHint("") }, 2000)
|
|
1466
|
+
return
|
|
1467
|
+
}
|
|
1468
|
+
setTimeout(poll, 800)
|
|
1469
|
+
}).catch(function () {
|
|
1470
|
+
st.busy = false
|
|
1471
|
+
setLoading(false)
|
|
1472
|
+
setRefreshHint("刷新失败")
|
|
1473
|
+
setTimeout(function () { setRefreshHint("") }, 2500)
|
|
1429
1474
|
})
|
|
1430
|
-
|
|
1431
|
-
|
|
1475
|
+
}
|
|
1476
|
+
fetchStatus(true)
|
|
1477
|
+
.catch(function () { /* continue polling */ })
|
|
1478
|
+
.then(function () { setTimeout(poll, 500) })
|
|
1432
1479
|
}
|
|
1433
1480
|
|
|
1434
1481
|
useEffect(function () {
|
|
@@ -1437,7 +1484,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1437
1484
|
return function () { clearInterval(timer) }
|
|
1438
1485
|
}, [])
|
|
1439
1486
|
|
|
1440
|
-
useEffect(function () { if (tick > 0) load(false) }, [tick])
|
|
1487
|
+
useEffect(function () { if (tick > 0 && !forceRef.current.busy) load(false) }, [tick])
|
|
1441
1488
|
|
|
1442
1489
|
function copyJson() {
|
|
1443
1490
|
var text = JSON.stringify(data || {}, null, 2)
|
|
@@ -1468,7 +1515,8 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1468
1515
|
h('span', { style: { flex: 1 } }),
|
|
1469
1516
|
h('a', { className: 'dm-btn', href: API, target: '_blank', rel: 'noreferrer', style: { textDecoration: 'none' } }, '原始 JSON'),
|
|
1470
1517
|
h('button', { className: 'dm-btn', onClick: copyJson }, copied || '复制诊断'),
|
|
1471
|
-
h('button', { className: 'dm-btn primary', onClick: function () { load(true) } }, loading ? '
|
|
1518
|
+
h('button', { className: 'dm-btn primary', onClick: function () { load(true) }, title: '立即采集一次并等待新快照(8s 冷却)' }, loading ? '采集中…' : '刷新'),
|
|
1519
|
+
refreshHint ? h('span', { className: 'dm-sub' }, refreshHint) : null
|
|
1472
1520
|
),
|
|
1473
1521
|
error ? h('div', { className: 'dm-err' }, error) : null,
|
|
1474
1522
|
|
package/lib/collect.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { execFile } from 'node:child_process'
|
|
17
17
|
import { existsSync, readdirSync, readFileSync, statfsSync, writeFileSync } from 'node:fs'
|
|
18
18
|
import { lookup as dnsLookup } from 'node:dns/promises'
|
|
19
|
+
import { connect as netConnect } from 'node:net'
|
|
19
20
|
import { arch as osArch, cpus as osCpus, freemem as osFreemem, homedir, platform as osPlatform, release as osRelease, totalmem as osTotalmem } from 'node:os'
|
|
20
21
|
import { join } from 'node:path'
|
|
21
22
|
|
|
@@ -505,6 +506,36 @@ export async function collect(repo) {
|
|
|
505
506
|
} catch { /* 解析失败则保持主机名 */ }
|
|
506
507
|
}))
|
|
507
508
|
|
|
509
|
+
// 2026-11-10:为每条 peer 隧道做 TCP 端口探测(阿里云 701x / LAN 8124),面板连线状态以此为准,
|
|
510
|
+
// 而不是用 peer.up(HTTP /health 探测,高延迟节点偶发超时会造成"隧道通却显示断连"的误判)
|
|
511
|
+
function tcpProbe(host, port, timeoutMs) {
|
|
512
|
+
return new Promise((resolve) => {
|
|
513
|
+
const started = Date.now()
|
|
514
|
+
let done = false
|
|
515
|
+
const socket = netConnect({ host, port })
|
|
516
|
+
const finish = (up, error) => {
|
|
517
|
+
if (done) return
|
|
518
|
+
done = true
|
|
519
|
+
try { socket.destroy() } catch { /* ignore */ }
|
|
520
|
+
resolve({ up, latencyMs: Date.now() - started, error: up ? null : error || "timeout" })
|
|
521
|
+
}
|
|
522
|
+
socket.setTimeout(timeoutMs)
|
|
523
|
+
socket.once("connect", () => finish(true, null))
|
|
524
|
+
socket.once("timeout", () => finish(false, "timeout"))
|
|
525
|
+
socket.once("error", (err) => finish(false, err?.code || "error"))
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
await Promise.all(peers.map(async (peer) => {
|
|
529
|
+
const tunnel = peer.tunnel || {}
|
|
530
|
+
if (!tunnel.host || !tunnel.port) return
|
|
531
|
+
tunnel.probe = await tcpProbe(tunnel.host, Number(tunnel.port), 3000)
|
|
532
|
+
if (!tunnel.probe.up) {
|
|
533
|
+
const retry = await tcpProbe(tunnel.host, Number(tunnel.port), 3000)
|
|
534
|
+
if (retry.up) tunnel.probe = retry
|
|
535
|
+
}
|
|
536
|
+
}))
|
|
537
|
+
|
|
538
|
+
|
|
508
539
|
return {
|
|
509
540
|
ok: true,
|
|
510
541
|
generatedAt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mesh-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "DSH settings-page panel for dsh-mesh: AI network topology, peer health, and delegation panel. Read-only; data is collected from the local mesh CLI.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|