dsh-mesh-ui 0.0.5 → 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 +109 -31
- 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',
|
|
@@ -1254,22 +1256,55 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1254
1256
|
var p = String(d.path || '').replace(/[\\/]+$/, '')
|
|
1255
1257
|
return (d.freeGB != null ? d.freeGB : '?') + ' / ' + d.totalGB + ' GB' + (p ? ' (' + p + ')' : '')
|
|
1256
1258
|
}
|
|
1259
|
+
// 设备类型:按平台/CPU 形态泛化推断(不依赖任何个人设备表)
|
|
1260
|
+
function formFactorOf(h) {
|
|
1261
|
+
if (!h) return '—'
|
|
1262
|
+
if (h.platform === 'linux' && String(h.arch || '').indexOf('arm') === 0) return '手机'
|
|
1263
|
+
var cpu = String(h.cpuModel || '')
|
|
1264
|
+
return cpu ? '主机' : '—'
|
|
1265
|
+
}
|
|
1266
|
+
function memTextOf(h) {
|
|
1267
|
+
return h && h.memTotalGB ? ((h.memFreeGB != null ? h.memFreeGB : '?') + ' / ' + h.memTotalGB + ' GB') : '—'
|
|
1268
|
+
}
|
|
1269
|
+
function cpuTextOf(h) {
|
|
1270
|
+
return h && h.cpuModel ? h.cpuModel + ' · ' + (h.cpuCores || '?') + ' 线程' : '—'
|
|
1271
|
+
}
|
|
1272
|
+
function ipKindOf(peer) {
|
|
1273
|
+
var t = peer.tunnel || {}
|
|
1274
|
+
if (t.kind === 'loopback') return '本机回环'
|
|
1275
|
+
if (t.kind === 'lan') return '局域网直连'
|
|
1276
|
+
if (t.kind === 'frp') return '公网穿透(域名)'
|
|
1277
|
+
if (t.kind === 'wan') return relayHosts[String(t.host || '').toLowerCase()] ? '公网(共享中转)' : '公网 IP 直连'
|
|
1278
|
+
return '—'
|
|
1279
|
+
}
|
|
1280
|
+
function relayKindOf(peer) {
|
|
1281
|
+
var t = peer.tunnel || {}
|
|
1282
|
+
if (t.kind === 'loopback') return '无(本机回环)'
|
|
1283
|
+
if (t.kind === 'lan') return '无(局域网直连)'
|
|
1284
|
+
if (t.kind === 'frp') return 'Sakura / 第三方 frp 穿透'
|
|
1285
|
+
if (t.kind === 'wan') return relayHosts[String(t.host || '').toLowerCase()] ? '自建公网中继(frps)' : '公网 IP 直连'
|
|
1286
|
+
return '—'
|
|
1287
|
+
}
|
|
1288
|
+
function dshVerOf(peer) {
|
|
1289
|
+
var v = peer.version ? 'v' + peer.version : '—'
|
|
1290
|
+
if (peer.plugin) v += ' · 插件 ' + peer.plugin
|
|
1291
|
+
return v
|
|
1292
|
+
}
|
|
1257
1293
|
|
|
1258
1294
|
var devices = [{
|
|
1259
1295
|
key: 'local',
|
|
1260
1296
|
name: nodeDisplay(data.nodeName) + '(本机)',
|
|
1261
1297
|
up: Boolean(endpoint.up),
|
|
1262
1298
|
rows: [
|
|
1299
|
+
['设备类型', formFactorOf(hw)],
|
|
1263
1300
|
['操作系统', hw.os || '—'],
|
|
1264
|
-
['
|
|
1265
|
-
['内存', memText],
|
|
1301
|
+
['内存', memTextOf(hw)],
|
|
1266
1302
|
['磁盘', diskText(hw.disk)],
|
|
1267
|
-
['
|
|
1268
|
-
['
|
|
1269
|
-
['
|
|
1270
|
-
|
|
1271
|
-
['
|
|
1272
|
-
['检测时间', clockText(data.at)]
|
|
1303
|
+
['CPU', cpuTextOf(hw)],
|
|
1304
|
+
['DSH 版本', data.meshVersion ? 'v' + data.meshVersion : '—'],
|
|
1305
|
+
['IP 类型', '本机回环'],
|
|
1306
|
+
['端口号', localTunnel.port != null ? String(localTunnel.port) : '—'],
|
|
1307
|
+
['公网穿透类型', mainSrc === 'self-hosted' ? '自建 frps 中继(主用)' : (mainSrc === 'sakura' ? 'Sakura 穿透(备用)' : '无(仅局域网)')]
|
|
1273
1308
|
]
|
|
1274
1309
|
}].concat(peers.map(function (peer) {
|
|
1275
1310
|
var t = peer.tunnel || {}
|
|
@@ -1279,18 +1314,15 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1279
1314
|
name: nodeDisplay(peerDeviceKey(peer.name)),
|
|
1280
1315
|
up: Boolean(peer.up),
|
|
1281
1316
|
rows: [
|
|
1317
|
+
['设备类型', formFactorOf(hwp)],
|
|
1282
1318
|
['操作系统', hwp.os || peer.os || '—'],
|
|
1283
|
-
['
|
|
1284
|
-
['内存', hwp.memTotalGB ? ((hwp.memFreeGB != null ? hwp.memFreeGB : '?') + ' / ' + hwp.memTotalGB + ' GB') : '—'],
|
|
1319
|
+
['内存', memTextOf(hwp)],
|
|
1285
1320
|
['磁盘', diskText(hwp.disk)],
|
|
1286
|
-
['
|
|
1287
|
-
['DSH
|
|
1288
|
-
['
|
|
1289
|
-
['
|
|
1290
|
-
['
|
|
1291
|
-
['链路', commLabel(peer, relayHosts).text + ' · ' + (peer.up ? (peer.latencyMs == null ? '—' : peer.latencyMs + 'ms') : '离线') + ' · ' + (peer.tls ? 'TLS ✓' : 'TLS ✗') + ' · v' + (peer.version || '?')],
|
|
1292
|
-
['节点 pid / tools', (peer.nodePid == null ? '—' : peer.nodePid) + ' / ' + (peer.nodeTools == null ? '—' : peer.nodeTools)],
|
|
1293
|
-
['检测时间', clockText(peer.detectedAt || data.at)]
|
|
1321
|
+
['CPU', cpuTextOf(hwp)],
|
|
1322
|
+
['DSH 版本', dshVerOf(peer)],
|
|
1323
|
+
['IP 类型', ipKindOf(peer)],
|
|
1324
|
+
['端口号', t.port ? String(t.port) : '—'],
|
|
1325
|
+
['公网穿透类型', relayKindOf(peer)]
|
|
1294
1326
|
]
|
|
1295
1327
|
}
|
|
1296
1328
|
}))
|
|
@@ -1388,17 +1420,62 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1388
1420
|
var s2 = useState(''); var error = s2[0]; var setError = s2[1]
|
|
1389
1421
|
var s3 = useState(0); var tick = s3[0]; var setTick = s3[1]
|
|
1390
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 })
|
|
1391
1426
|
var s5 = useState('local'); var selectedKey = s5[0]; var setSelectedKey = s5[1]
|
|
1392
1427
|
|
|
1393
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
|
|
1394
1451
|
setLoading(true)
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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)
|
|
1399
1474
|
})
|
|
1400
|
-
|
|
1401
|
-
|
|
1475
|
+
}
|
|
1476
|
+
fetchStatus(true)
|
|
1477
|
+
.catch(function () { /* continue polling */ })
|
|
1478
|
+
.then(function () { setTimeout(poll, 500) })
|
|
1402
1479
|
}
|
|
1403
1480
|
|
|
1404
1481
|
useEffect(function () {
|
|
@@ -1407,7 +1484,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1407
1484
|
return function () { clearInterval(timer) }
|
|
1408
1485
|
}, [])
|
|
1409
1486
|
|
|
1410
|
-
useEffect(function () { if (tick > 0) load(false) }, [tick])
|
|
1487
|
+
useEffect(function () { if (tick > 0 && !forceRef.current.busy) load(false) }, [tick])
|
|
1411
1488
|
|
|
1412
1489
|
function copyJson() {
|
|
1413
1490
|
var text = JSON.stringify(data || {}, null, 2)
|
|
@@ -1438,7 +1515,8 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1438
1515
|
h('span', { style: { flex: 1 } }),
|
|
1439
1516
|
h('a', { className: 'dm-btn', href: API, target: '_blank', rel: 'noreferrer', style: { textDecoration: 'none' } }, '原始 JSON'),
|
|
1440
1517
|
h('button', { className: 'dm-btn', onClick: copyJson }, copied || '复制诊断'),
|
|
1441
|
-
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
|
|
1442
1520
|
),
|
|
1443
1521
|
error ? h('div', { className: 'dm-err' }, error) : null,
|
|
1444
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",
|