dsh-mesh-ui 0.0.12 → 0.0.14
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/README.md +11 -1
- package/cordis.patch.yml +1 -1
- package/lib/client.js +236 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# dsh-mesh-ui
|
|
2
2
|
|
|
3
|
-
DSH
|
|
3
|
+
DSH left-sidebar page for a `dsh-mesh` AI network: topology, peer health, local instances, delegation ledger and alerts. Public addresses (IP/domain) are hidden by default; LAN addresses remain visible.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
4
6
|
|
|
5
7
|
```sh
|
|
6
8
|
dsh plugin --profile web add dsh-mesh-ui
|
|
7
9
|
```
|
|
8
10
|
|
|
11
|
+
This installs `dsh-mesh-node` as a dependency. The page is read-only; it reads the local `~/.dsh-mesh` mesh state and never uploads or hardcodes peer addresses.
|
|
12
|
+
|
|
13
|
+
To make this DSH instance a mesh node, also install the host bridge:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
dsh plugin --profile web add dsh-mesh-host
|
|
17
|
+
```
|
|
18
|
+
|
|
9
19
|
See https://github.com/LinYanZhi/dsh-mesh
|
package/cordis.patch.yml
CHANGED
package/lib/client.js
CHANGED
|
@@ -119,6 +119,37 @@ function nodeLabel(name) { return NODE_LABELS[String(name || '').toLowerCase()]
|
|
|
119
119
|
function clampNum(v, min, max) { return Math.max(min, Math.min(max, v)) }
|
|
120
120
|
// [layout-helpers-end]
|
|
121
121
|
// [relay-helpers-begin] 2026-11-09 架构层:识别公网中转站(frps)与通信方式(纯函数,可单测)
|
|
122
|
+
// 2026-11-10 隐私:默认不展示公网 IP/域名;局域网/回环地址保留。
|
|
123
|
+
function isPrivateNetworkHost(raw) {
|
|
124
|
+
var h = String(raw || '').trim().toLowerCase().replace(/^\[|\]$/g, '')
|
|
125
|
+
h = h.replace(/^https?:\/\//, '').split('/')[0].replace(/:\d+$/, '')
|
|
126
|
+
if (!h) return true
|
|
127
|
+
if (h === 'localhost' || h === '::1' || h === '0.0.0.0' || h === '127.0.0.1') return true
|
|
128
|
+
if (/(\.local|\.lan|\.home|\.internal)$/.test(h)) return true
|
|
129
|
+
var m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(h)
|
|
130
|
+
if (m) {
|
|
131
|
+
var a = Number(m[1]); var b = Number(m[2])
|
|
132
|
+
return a === 10 || a === 127 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31)
|
|
133
|
+
}
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
function maskHost(raw) {
|
|
137
|
+
var s = String(raw || '').trim()
|
|
138
|
+
if (!s) return '—'
|
|
139
|
+
var port = ''
|
|
140
|
+
var m = /^(.*):(\d+)$/.exec(s)
|
|
141
|
+
if (m && m[1].indexOf(']') < 0) { s = m[1]; port = ':' + m[2] }
|
|
142
|
+
return (isPrivateNetworkHost(s) ? s : '公网地址已隐藏') + port
|
|
143
|
+
}
|
|
144
|
+
function maskText(value) {
|
|
145
|
+
if (typeof value !== 'string') return value
|
|
146
|
+
return value
|
|
147
|
+
.replace(/https?:\/\/[^\s"'<>]+/gi, function (u) {
|
|
148
|
+
try { var x = new URL(u); return isPrivateNetworkHost(x.hostname) ? u : (x.protocol + '//公网地址已隐藏' + (x.port ? ':' + x.port : '')) } catch (e) { return u }
|
|
149
|
+
})
|
|
150
|
+
.replace(/\b(?!10\.|192\.168\.|127\.|172\.(?:1[6-9]|2\d|3[01])\.)(?:\d{1,3}\.){3}\d{1,3}\b/g, '公网地址已隐藏')
|
|
151
|
+
.replace(/\b(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?:com|net|org|io|dev|cn|top|xyz|me|cc|app|cloud)(?::\d+)?\b/gi, '公网地址已隐藏')
|
|
152
|
+
}
|
|
122
153
|
// 判定:同一公网 host 被 >=2 个 peer 使用 → 视为共享中转站;LAN/loopback 不算。
|
|
123
154
|
var RELAY_PUBLIC_KINDS = { wan: 1, frp: 1 }
|
|
124
155
|
function relayGroups(peers) {
|
|
@@ -149,12 +180,12 @@ function nodeLabel(name) { return NODE_LABELS[String(name || '').toLowerCase()]
|
|
|
149
180
|
if (!host) return { text: '—', short: '—' }
|
|
150
181
|
var relay = relayHosts && relayHosts[host]
|
|
151
182
|
var port = t.port ? ':' + t.port : ''
|
|
152
|
-
if (relay) return { text: (relay.kind === 'frp' ? '域名穿透' : '自建 frps 中转') + ' · ' + relay.host + port, short: '中转' + (t.port ? ' ' + t.port : '') }
|
|
183
|
+
if (relay) return { text: (relay.kind === 'frp' ? '域名穿透' : '自建 frps 中转') + ' · ' + maskHost(relay.host) + port, short: '中转' + (t.port ? ' ' + t.port : '') }
|
|
153
184
|
if (t.kind === 'loopback') return { text: '本机回环', short: '本机' }
|
|
154
|
-
if (t.kind === 'lan') return { text: '局域网直连 · ' + t.host + port, short: 'LAN' }
|
|
155
|
-
if (t.kind === 'frp') return { text: '公网穿透 · ' + t.host + port, short: '穿透' }
|
|
156
|
-
if (t.kind === 'wan') return { text: '公网直连 · ' + t.host + port, short: '公网' }
|
|
157
|
-
return { text: (t.label || t.kind || '未知') + ' · ' + t.host + port, short: t.label || '未知' }
|
|
185
|
+
if (t.kind === 'lan') return { text: '局域网直连 · ' + maskHost(t.host) + port, short: 'LAN' }
|
|
186
|
+
if (t.kind === 'frp') return { text: '公网穿透 · ' + maskHost(t.host) + port, short: '穿透' }
|
|
187
|
+
if (t.kind === 'wan') return { text: '公网直连 · ' + maskHost(t.host) + port, short: '公网' }
|
|
188
|
+
return { text: (t.label || t.kind || '未知') + ' · ' + maskHost(t.host) + port, short: t.label || '未知' }
|
|
158
189
|
}
|
|
159
190
|
// [relay-helpers-end]
|
|
160
191
|
var W = 560
|
|
@@ -643,7 +674,7 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
|
|
|
643
674
|
return h('div', { className: 'dm-topo-wrap' },
|
|
644
675
|
h('div', { className: 'dm-topbar' },
|
|
645
676
|
h('div', { className: 'dm-filters' },
|
|
646
|
-
[['lan', '局域网'], ['public', '
|
|
677
|
+
[['lan', '局域网'], ['public', '公网 frps'], ['sakura', '第三方穿透']].map(function (it) {
|
|
647
678
|
var tr = TRANSPORTS[it[0]]
|
|
648
679
|
return h('label', { key: it[0], className: 'dm-flt' + (showFilters[it[0]] ? ' on' : ''), style: { '--c': tr.color } },
|
|
649
680
|
h('input', { type: 'checkbox', checked: showFilters[it[0]], onChange: function () { toggleFilter(it[0]) } }),
|
|
@@ -767,7 +798,7 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
|
|
|
767
798
|
h('div', { className: 'dm-relay-link' }, h('span', { className: 'dm-relay-line' }), h('span', { className: 'dm-relay-dirlabel' }, '出站')),
|
|
768
799
|
h('div', { className: 'dm-relay-hub' },
|
|
769
800
|
h('div', { className: 'dm-relay-name' }, '公网中转站 · frps'),
|
|
770
|
-
h('div', { className: 'dm-relay-sub dm-mono' }, g.host),
|
|
801
|
+
h('div', { className: 'dm-relay-sub dm-mono' }, maskHost(g.host)),
|
|
771
802
|
h('div', { className: 'dm-relay-chips' },
|
|
772
803
|
ports.map(function (p) { return h('span', { className: 'dm-chip', key: p, title: '隧道入口 :' + p }, ':' + p + ' ×' + g.ports[p]) }),
|
|
773
804
|
h('span', { className: 'dm-chip ' + (g.up === g.peers.length ? 'run' : 'bad') }, '在线 ' + g.up + '/' + g.peers.length)
|
|
@@ -788,8 +819,8 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
|
|
|
788
819
|
h('span', { className: 'dm-k' }, '樱花隧道:'),
|
|
789
820
|
h('span', { className: 'dm-chip ' + (sakuraAccount.online === sakuraAccount.total ? 'run' : 'bad') }, sakuraAccount.online + '/' + sakuraAccount.total + ' 在线'),
|
|
790
821
|
(sakuraAccount.tunnels || []).map(function (t) {
|
|
791
|
-
return h('span', { className: 'dm-chip ' + (t.online ? 'run' : 'bad'), key: 'st' + t.id, title: (t.nodeName || '') + ' ' + t.addr + (t.online ? '' : '(离线)') },
|
|
792
|
-
(t.deviceKey || t.name) + ' ' + t.addr)
|
|
822
|
+
return h('span', { className: 'dm-chip ' + (t.online ? 'run' : 'bad'), key: 'st' + t.id, title: (t.nodeName || '') + ' ' + maskText(t.addr) + (t.online ? '' : '(离线)') },
|
|
823
|
+
(t.deviceKey || t.name) + ' ' + maskText(t.addr))
|
|
793
824
|
})
|
|
794
825
|
) : null,
|
|
795
826
|
direct.length ? h('div', { className: 'dm-relay-direct' },
|
|
@@ -858,22 +889,22 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
858
889
|
var selfHost = lt.selfHosted || {}
|
|
859
890
|
var entry = lt.publicEntry || selfHost || {}
|
|
860
891
|
var localPort = entry.port || selfHost.port || null
|
|
861
|
-
var localHost = entry.host || selfHost.host ||
|
|
892
|
+
var localHost = entry.host || selfHost.host || ''
|
|
862
893
|
var localKey = peerDeviceKey(data.nodeName)
|
|
863
894
|
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) {
|
|
864
895
|
var tu = p.tunnel || {}
|
|
865
|
-
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 || '
|
|
896
|
+
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 || '' }
|
|
866
897
|
}))
|
|
867
898
|
var aliyunRows = devs.filter(function (d) { return d.port }).map(function (d) {
|
|
868
899
|
return h('div', { key: d.devKey, className: 'dm-device-row' },
|
|
869
900
|
h('span', { className: 'dm-device-k' }, d.name),
|
|
870
|
-
h('span', { className: 'dm-device-v' }, (d.up ? '✅ ' : '❌ ') + (d.host
|
|
901
|
+
h('span', { className: 'dm-device-v' }, (d.up ? '✅ ' : '❌ ') + maskHost(d.host) + ':' + d.port)
|
|
871
902
|
)
|
|
872
903
|
})
|
|
873
904
|
var tunnelRows = acc ? (acc.tunnels || []).map(function (x, i) {
|
|
874
905
|
return h('div', { key: 'st' + i, className: 'dm-device-row' },
|
|
875
906
|
h('span', { className: 'dm-device-k' }, (x.deviceKey ? nodeDisplay(peerDeviceKey(x.deviceKey)) : (x.name || '未归类'))),
|
|
876
|
-
h('span', { className: 'dm-device-v' }, (x.online ? '✅ ' : '❌ ') + (x.addr || ''))
|
|
907
|
+
h('span', { className: 'dm-device-v' }, (x.online ? '✅ ' : '❌ ') + maskText(x.addr || ''))
|
|
877
908
|
)
|
|
878
909
|
}) : null
|
|
879
910
|
if (!aliyunRows.length && !tunnelRows) return null
|
|
@@ -881,19 +912,19 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
881
912
|
h('div', { className: 'dm-infra-card' },
|
|
882
913
|
h('div', { className: 'dm-infra-head' },
|
|
883
914
|
h('span', { className: 'dm-dot ok', style: { background: TRANSPORTS.public.color } }),
|
|
884
|
-
h('span', null, '
|
|
915
|
+
h('span', null, '公网 frps 中转'),
|
|
885
916
|
h('span', { className: 'dm-sub', style: { marginLeft: 'auto' } }, '自建 frps')
|
|
886
917
|
),
|
|
887
|
-
h('div', { className: 'dm-sub dm-mono', style: { marginBottom: 4 } }, '
|
|
918
|
+
h('div', { className: 'dm-sub dm-mono', style: { marginBottom: 4 } }, '隧道 ' + (devs.filter(function (d) { return d.port }).map(function (d) { return d.port }).sort().join(' / ') || '—')),
|
|
888
919
|
aliyunRows
|
|
889
920
|
),
|
|
890
921
|
acc ? h('div', { className: 'dm-infra-card' },
|
|
891
922
|
h('div', { className: 'dm-infra-head' },
|
|
892
923
|
h('span', { className: 'dm-dot', style: { background: TRANSPORTS.sakura.color } }),
|
|
893
|
-
h('span', null, '
|
|
924
|
+
h('span', null, '第三方 frps 穿透'),
|
|
894
925
|
h('span', { className: 'dm-sub', style: { marginLeft: 'auto' } }, acc.online + '/' + acc.total + ' 在线')
|
|
895
926
|
),
|
|
896
|
-
h('div', { className: 'dm-sub dm-mono', style: { marginBottom: 4 } }, '
|
|
927
|
+
h('div', { className: 'dm-sub dm-mono', style: { marginBottom: 4 } }, '公网地址已隐藏'),
|
|
897
928
|
tunnelRows
|
|
898
929
|
) : null
|
|
899
930
|
)
|
|
@@ -985,7 +1016,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
985
1016
|
var src = localTunnel.mainSource || (entry && entry.source === 'self-hosted-frps' ? 'self-hosted' : '')
|
|
986
1017
|
if (entry && entry.host) {
|
|
987
1018
|
var tag = src === 'self-hosted' ? '(自建 frps)' : (src === 'sakura' ? '(Sakura 备用)' : '')
|
|
988
|
-
return entry.host + ':' + entry.port + tag
|
|
1019
|
+
return maskHost(entry.host) + ':' + entry.port + tag
|
|
989
1020
|
}
|
|
990
1021
|
if (localTunnel.frpRunning) return 'SakuraFrp 已开 · ' + (localTunnel.bind || '?') + ':' + localTunnel.port
|
|
991
1022
|
if (localTunnel.port) return '未开穿透 · ' + (localTunnel.bind || '?') + ':' + localTunnel.port
|
|
@@ -994,7 +1025,7 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
994
1025
|
function peerDomain(peer) {
|
|
995
1026
|
var t = peer.tunnel || {}
|
|
996
1027
|
if (!t.host) return '—'
|
|
997
|
-
return t.host + (t.port ? ':' + t.port : '') + (t.kind === 'lan' ? '(内网)' : '')
|
|
1028
|
+
return maskHost(t.host) + (t.port ? ':' + t.port : '') + (t.kind === 'lan' ? '(内网)' : '')
|
|
998
1029
|
}
|
|
999
1030
|
|
|
1000
1031
|
// LAN peer 单独收集(dedupe 后可能被更快的 frp 记录覆盖,但 LAN 线仍要画)
|
|
@@ -1039,8 +1070,8 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1039
1070
|
var sakuraAcc = sakuraAccountOf(data)
|
|
1040
1071
|
var aliyunPorts = peers.map(function (p) { return isLanPeer(p) ? null : (p.tunnel || {}).port }).filter(Boolean)
|
|
1041
1072
|
var infra = [
|
|
1042
|
-
{ key: 'server-aliyun', transport: 'public', name: '
|
|
1043
|
-
{ key: 'server-sakura', transport: 'sakura', name: '
|
|
1073
|
+
{ key: 'server-aliyun', transport: 'public', name: '公网 frps 中转', host: '公网地址已隐藏', color: TRANSPORTS.public.color, detail: '隧道 ' + (aliyunPorts.length ? aliyunPorts.sort().join('/') : '—'), up: true },
|
|
1074
|
+
{ key: 'server-sakura', transport: 'sakura', name: '第三方 frps 穿透', host: '公网地址已隐藏', color: TRANSPORTS.sakura.color, detail: sakuraAcc ? (sakuraAcc.online + '/' + sakuraAcc.total + ' 在线') : '未采集', up: Boolean(sakuraAcc) },
|
|
1044
1075
|
]
|
|
1045
1076
|
// 2026-11-09-v8:服务器默认位置贴住扇形最高一排设备(用户要求缩小服务器↔设备的垂直间隔)
|
|
1046
1077
|
function infraDefaultPos(idx) {
|
|
@@ -1146,13 +1177,13 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1146
1177
|
var pa = layout.positions[i]
|
|
1147
1178
|
if (n.local) {
|
|
1148
1179
|
// 主机自己的两条上级链路:自建 frps(阿里云) + 本机樱花隧道
|
|
1149
|
-
if (show.public && localPub) svgEls = svgEls.concat(curve('pub-local', pa, aliyunPos, true, TRANSPORTS.public, localPub.host + ':' + localPub.port, sizeOf('local'), aliyunSize, edgeRelated('local', 'server-aliyun')))
|
|
1150
|
-
if (show.sakura && localSak) svgEls = svgEls.concat(curve('sak-local', pa, sakuraPos, localSak.online === true, TRANSPORTS.sakura, localSak.addr || '', sizeOf('local'), sakuraSize, edgeRelated('local', 'server-sakura')))
|
|
1180
|
+
if (show.public && localPub) svgEls = svgEls.concat(curve('pub-local', pa, aliyunPos, true, TRANSPORTS.public, maskHost(localPub.host) + ':' + localPub.port, sizeOf('local'), aliyunSize, edgeRelated('local', 'server-aliyun')))
|
|
1181
|
+
if (show.sakura && localSak) svgEls = svgEls.concat(curve('sak-local', pa, sakuraPos, localSak.online === true, TRANSPORTS.sakura, maskText(localSak.addr || ''), sizeOf('local'), sakuraSize, edgeRelated('local', 'server-sakura')))
|
|
1151
1182
|
return
|
|
1152
1183
|
}
|
|
1153
1184
|
var sizeN = sizeOf(n.key)
|
|
1154
|
-
if (show.public) svgEls = svgEls.concat(curve('pub' + i, pa, aliyunPos, n.publicUp, TRANSPORTS.public, '
|
|
1155
|
-
if (show.sakura && n.sakuraTunnel) svgEls = svgEls.concat(curve('sak' + i, pa, sakuraPos, n.sakuraUp === true, TRANSPORTS.sakura, n.sakuraTunnel.addr || '', sizeN, sakuraSize, edgeRelated(n.key, 'server-sakura')))
|
|
1185
|
+
if (show.public) svgEls = svgEls.concat(curve('pub' + i, pa, aliyunPos, n.publicUp, TRANSPORTS.public, '公网 frps' + ((n.tunnel && n.tunnel.port) ? ':' + n.tunnel.port : ''), sizeN, aliyunSize, edgeRelated(n.key, 'server-aliyun')))
|
|
1186
|
+
if (show.sakura && n.sakuraTunnel) svgEls = svgEls.concat(curve('sak' + i, pa, sakuraPos, n.sakuraUp === true, TRANSPORTS.sakura, maskText(n.sakuraTunnel.addr || ''), sizeN, sakuraSize, edgeRelated(n.key, 'server-sakura')))
|
|
1156
1187
|
if (show.lan && n.lanUp) { var lt = n.lanTunnel || {}; var lanAddr = (lt.ip || lt.host || 'LAN') + (lt.port ? ':' + lt.port : ''); svgEls = svgEls.concat(curve('lan' + i, layout.positions[localNodeIdx], pa, n.lanUp, TRANSPORTS.lan, lanAddr, sizeOf('local'), sizeN, edgeRelated('local', n.key))) }
|
|
1157
1188
|
})
|
|
1158
1189
|
return h('div', null,
|
|
@@ -1239,16 +1270,16 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1239
1270
|
var localTunnelText = null
|
|
1240
1271
|
var tunnels = localTunnel.tunnels || []
|
|
1241
1272
|
var entry = localTunnel.publicEntry || null
|
|
1242
|
-
var tunnelNames = tunnels.map(function (x) { return x.name + '→' + x.host + ':' + x.port }).join(' / ')
|
|
1273
|
+
var tunnelNames = tunnels.map(function (x) { return x.name + '→' + maskHost(x.host) + ':' + x.port }).join(' / ')
|
|
1243
1274
|
var mainSrc = localTunnel.mainSource || (entry && entry.source === 'self-hosted-frps' ? 'self-hosted' : (entry ? 'sakura' : 'none'))
|
|
1244
1275
|
var entryText = entry && entry.host
|
|
1245
|
-
? entry.host + ':' + entry.port + (mainSrc === 'self-hosted' ? '(自建 frps · 主用)' : (mainSrc === 'sakura' ? '(Sakura · 备用)' : ''))
|
|
1276
|
+
? maskHost(entry.host) + ':' + entry.port + (mainSrc === 'self-hosted' ? '(自建 frps · 主用)' : (mainSrc === 'sakura' ? '(Sakura · 备用)' : ''))
|
|
1246
1277
|
: '未配置'
|
|
1247
1278
|
var localTunnelRows = [
|
|
1248
1279
|
['入口', entryText],
|
|
1249
1280
|
['监听 / frpc', (localTunnel.bind || '?') + ':' + (localTunnel.port || '?') + (localTunnel.tls ? ' (TLS)' : '') + ' · ' + (localTunnel.frpRunning ? 'frpc ×' + (localTunnel.frpcCount || 1) : 'frpc 未运行')]
|
|
1250
1281
|
]
|
|
1251
|
-
if (tunnels.length) localTunnelRows.push(['Sakura 备用', tunnels.map(function (x) { return x.host + ':' + x.port }).join(' / ')])
|
|
1282
|
+
if (tunnels.length) localTunnelRows.push(['Sakura 备用', tunnels.map(function (x) { return maskHost(x.host) + ':' + x.port }).join(' / ')])
|
|
1252
1283
|
if (mainSrc === 'self-hosted' && localTunnel.selfHosted && localTunnel.selfHosted.configPath) localTunnelRows.push(['frpc 配置', localTunnel.selfHosted.configPath])
|
|
1253
1284
|
var localTunnelAll = tunnelNames
|
|
1254
1285
|
var memText = hw.memTotalGB ? ((hw.memFreeGB != null ? hw.memFreeGB : '?') + ' / ' + hw.memTotalGB + ' GB') : '—'
|
|
@@ -1457,6 +1488,36 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1457
1488
|
var verdictState = useState(''); var verdictShown = verdictState[0]; var setVerdictShown = verdictState[1]
|
|
1458
1489
|
var verdictRef = useRef({ raw: '', streak: 0 })
|
|
1459
1490
|
var s5 = useState('local'); var selectedKey = s5[0]; var setSelectedKey = s5[1]
|
|
1491
|
+
// 双栏宽度:拖动分隔条调整并记忆,窄屏由 CSS 媒体查询自动堆叠。
|
|
1492
|
+
var dragState = useState(function () {
|
|
1493
|
+
try { var saved = Number(window.localStorage.getItem('dsh-mesh-split-w')); return Number.isFinite(saved) && saved > 0 ? saved : 0 } catch (error) { return 0 }
|
|
1494
|
+
})
|
|
1495
|
+
var splitW = dragState[0]; var setSplitW = dragState[1]
|
|
1496
|
+
|
|
1497
|
+
function startSplit(event) {
|
|
1498
|
+
event.preventDefault()
|
|
1499
|
+
var body = event.currentTarget.parentElement
|
|
1500
|
+
if (!body) return
|
|
1501
|
+
var rect = body.getBoundingClientRect()
|
|
1502
|
+
var startW = splitW > 0 ? splitW : Math.round(rect.width * 0.62)
|
|
1503
|
+
var currentW = startW
|
|
1504
|
+
function onMove(moveEvent) {
|
|
1505
|
+
var width = Math.max(300, Math.min(moveEvent.clientX - rect.left, Math.max(300, rect.width - 360)))
|
|
1506
|
+
currentW = width
|
|
1507
|
+
setSplitW(width)
|
|
1508
|
+
}
|
|
1509
|
+
function onUp() {
|
|
1510
|
+
window.removeEventListener('pointermove', onMove)
|
|
1511
|
+
window.removeEventListener('pointerup', onUp)
|
|
1512
|
+
document.body.style.cursor = ''
|
|
1513
|
+
document.body.style.userSelect = ''
|
|
1514
|
+
try { window.localStorage.setItem('dsh-mesh-split-w', String(currentW)) } catch (error) { /* ignore */ }
|
|
1515
|
+
}
|
|
1516
|
+
document.body.style.cursor = 'col-resize'
|
|
1517
|
+
document.body.style.userSelect = 'none'
|
|
1518
|
+
window.addEventListener('pointermove', onMove)
|
|
1519
|
+
window.addEventListener('pointerup', onUp)
|
|
1520
|
+
}
|
|
1460
1521
|
|
|
1461
1522
|
function load(force) {
|
|
1462
1523
|
if (!force) {
|
|
@@ -1575,23 +1636,30 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1575
1636
|
),
|
|
1576
1637
|
error ? h('div', { className: 'dm-err' }, error) : null,
|
|
1577
1638
|
|
|
1578
|
-
h(
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1639
|
+
h('div', { className: 'dm-body', style: splitW > 0 ? { '--dm-main-w': splitW + 'px' } : undefined },
|
|
1640
|
+
h('div', { className: 'dm-pane' },
|
|
1641
|
+
h(Viewport, { data: data, layout: computeLayout(1 + filterSelfPeers(dedupePeers(data.peers || []), data.nodeName).length), selectedKey: selectedKey, onSelect: setSelectedKey }),
|
|
1642
|
+
h(InfraPanels, { data: data }),
|
|
1643
|
+
h(DeviceDetails, { data: data, selectedKey: selectedKey })
|
|
1644
|
+
),
|
|
1645
|
+
h('div', { className: 'dm-divider', onPointerDown: startSplit, title: '↔ 拖拽调整左右宽度(双击恢复默认)', onDoubleClick: function () { setSplitW(0); try { window.localStorage.removeItem('dsh-mesh-split-w') } catch (error) { /* ignore */ } } }),
|
|
1646
|
+
h('div', { className: 'dm-pane' },
|
|
1647
|
+
h(Details, { title: '本机实例', badge: ((data.local && data.local.instances) || []).length, open: true }, h(InstanceList, { data: data })),
|
|
1648
|
+
h(Details, { title: '版本一致性', badge: versions.length ? versions.length + ' 处升级' : '一致' },
|
|
1649
|
+
versions.length
|
|
1650
|
+
? versions.map(function (item, i) { return h('div', { key: i, className: 'dm-sub', style: { marginTop: 4 } }, '· ' + (typeof item === 'string' ? item : JSON.stringify(item))) })
|
|
1651
|
+
: h('div', { className: 'dm-empty' }, '所有节点/实例版本一致')
|
|
1652
|
+
),
|
|
1653
|
+
h(Details, { title: '委托台账', badge: (data.delegations && data.delegations.total) || 0 }, h(DelegationTable, { data: data })),
|
|
1654
|
+
h(Details, { title: '告警历史', badge: ((data.alerts && data.alerts.history) || []).length }, h(AlertList, { data: data })),
|
|
1655
|
+
(issues.length || hints.length) ? h(Details, { title: '诊断建议', badge: issues.length + hints.length },
|
|
1656
|
+
h('div', null,
|
|
1657
|
+
issues.slice(0, 6).map(function (issue, i) { return h('div', { key: 'i' + i, className: 'dm-err', style: { marginTop: 3 } }, '· ' + maskText(issue)) }),
|
|
1658
|
+
hints.slice(0, 6).map(function (hint, i) { return h('div', { key: 'h' + i, className: 'dm-sub', style: { marginTop: 3 } }, '· ' + maskText(hint)) })
|
|
1659
|
+
)
|
|
1660
|
+
) : null
|
|
1593
1661
|
)
|
|
1594
|
-
)
|
|
1662
|
+
),
|
|
1595
1663
|
h('div', { className: 'dm-foot' }, '数据源:mesh-health / host status / peer host_self / 委托台账 / 告警历史 · ' + (data.repo || '') + ' · node ' + (data.nodeVersion || '') + ' · ' + REFRESH_MS / 1000 + 's 自动刷新(只读)')
|
|
1596
1664
|
)
|
|
1597
1665
|
}
|
|
@@ -1810,6 +1878,23 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
1810
1878
|
)
|
|
1811
1879
|
}
|
|
1812
1880
|
|
|
1881
|
+
/** 左侧栏「AI 网络」专用图标:中心节点 + 四向设备,和「委托」区分开。 */
|
|
1882
|
+
function networkPanelIcon(props) {
|
|
1883
|
+
var size = (props && props.size) || 16
|
|
1884
|
+
return h('svg', {
|
|
1885
|
+
'data-dsh-panel-entry': 'mesh-network', viewBox: '0 0 16 16', width: size, height: size,
|
|
1886
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round', 'aria-hidden': 'true',
|
|
1887
|
+
style: { color: 'var(--dsh-brand-color, currentColor)' }
|
|
1888
|
+
},
|
|
1889
|
+
h('path', { d: 'M8 8 L8 2.6 M8 8 L13.4 8 M8 8 L8 13.4 M8 8 L2.6 8', opacity: '.55' }),
|
|
1890
|
+
h('circle', { cx: 8, cy: 8, r: 2.1, fill: 'none', strokeWidth: '1.4' }),
|
|
1891
|
+
h('circle', { cx: 8, cy: 2.6, r: 1.25, fill: 'currentColor', stroke: 'none' }),
|
|
1892
|
+
h('circle', { cx: 13.4, cy: 8, r: 1.25, fill: 'currentColor', stroke: 'none' }),
|
|
1893
|
+
h('circle', { cx: 8, cy: 13.4, r: 1.25, fill: 'currentColor', stroke: 'none' }),
|
|
1894
|
+
h('circle', { cx: 2.6, cy: 8, r: 1.25, fill: 'currentColor', stroke: 'none' })
|
|
1895
|
+
)
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1813
1898
|
function MeshDelegationPanel() {
|
|
1814
1899
|
var s0 = useState(null); var list = s0[0]; var setList = s0[1]
|
|
1815
1900
|
var s1 = useState(true); var loading = s1[0]; var setLoading = s1[1]
|
|
@@ -2249,6 +2334,111 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2249
2334
|
)
|
|
2250
2335
|
}
|
|
2251
2336
|
|
|
2337
|
+
// 2026-11-10:AI 网络现在是左侧栏主页面,不再受设置面板的窄容器约束。
|
|
2338
|
+
// 主区域自身没有内边距,这里给页面统一留白并限制最大阅读宽度。
|
|
2339
|
+
var PAGE_CSS = `
|
|
2340
|
+
.dm-page { box-sizing:border-box; width:100%; height:100%; overflow:hidden; padding:clamp(16px, 2.2vw, 30px) clamp(18px, 2.6vw, 42px) 0; }
|
|
2341
|
+
.dm-content-shell { width:100%; height:100%; position:relative; --dm-content-w:920px; }
|
|
2342
|
+
.dm-content-scroll { height:100%; overflow:auto; padding-bottom:36px; scrollbar-gutter:stable; }
|
|
2343
|
+
.dm-content { width:min(100%, var(--dm-content-w)); max-width:100%; margin:0 auto; box-sizing:border-box; container-type:inline-size; container-name:dm-content; }
|
|
2344
|
+
.dm-content > .dm-root { width:100%; max-width:100%; margin:0 auto; }
|
|
2345
|
+
/* 与 DSH 对话区一致的宽度手柄:位于内容两侧外 24px,平时隐藏,hover/拖拽显示 2px 渐变线 */
|
|
2346
|
+
.dm-width-handle { z-index:0; position:absolute; top:0; bottom:0; cursor:col-resize; width:clamp(0px, calc((100% - var(--dm-content-w)) / 2 - 48px), 10px); }
|
|
2347
|
+
.dm-width-handle[data-side="left"] { right:calc(50% + var(--dm-content-w) / 2 + 24px); }
|
|
2348
|
+
.dm-width-handle[data-side="right"] { left:calc(50% + var(--dm-content-w) / 2 + 24px); }
|
|
2349
|
+
.dm-width-handle::after { content:''; pointer-events:none; opacity:0; position:absolute; top:0; bottom:0; width:2px; border-radius:2px; background:linear-gradient(to bottom, transparent calc(var(--dm-width-handle-pointer-y, 50%) - 36px), var(--dsw-alias-scrollbar-bg-l1, rgba(128,128,128,.55)) calc(var(--dm-width-handle-pointer-y, 50%) - 8px), var(--dsw-alias-scrollbar-bg-l1, rgba(128,128,128,.55)) calc(var(--dm-width-handle-pointer-y, 50%) + 8px), transparent calc(var(--dm-width-handle-pointer-y, 50%) + 36px)); }
|
|
2350
|
+
.dm-width-handle[data-side="left"]::after { right:4px; }
|
|
2351
|
+
.dm-width-handle[data-side="right"]::after { left:4px; }
|
|
2352
|
+
.dm-width-handle[data-dragging]::after { opacity:1; }
|
|
2353
|
+
@media (hover:hover) { .dm-width-handle:hover::after { opacity:1; } }
|
|
2354
|
+
@media (max-width: 1000px) { .dm-width-handle { display:none; } }
|
|
2355
|
+
.dm-body { display:grid; grid-template-columns:minmax(300px, var(--dm-main-w, 62%)) 8px minmax(0,1fr); gap:10px; align-items:start; }
|
|
2356
|
+
.dm-pane { min-width:0; display:flex; flex-direction:column; gap:12px; }
|
|
2357
|
+
.dm-divider { cursor:col-resize; align-self:stretch; min-height:120px; border-radius:4px; position:relative; }
|
|
2358
|
+
.dm-divider::before { content:''; position:absolute; top:0; bottom:0; left:3px; width:1px; background:rgba(128,128,128,.28); }
|
|
2359
|
+
.dm-divider:hover::before, .dm-divider:active::before { background:rgba(88,166,255,.7); width:2px; left:2.5px; }
|
|
2360
|
+
@container dm-content (max-width: 860px) { .dm-body { grid-template-columns:1fr; } .dm-divider { display:none; } }
|
|
2361
|
+
`
|
|
2362
|
+
function MeshNetworkPage() {
|
|
2363
|
+
// 宽度偏好:与 DSH 对话区相同语义——空 = 默认宽度,数字 = 用户拖拽固定值。
|
|
2364
|
+
var prefState = useState(function () {
|
|
2365
|
+
try { var saved = Number(window.localStorage.getItem('dsh-mesh.contentWidth')); return Number.isFinite(saved) && saved > 0 ? saved : 0 } catch (error) { return 0 }
|
|
2366
|
+
})
|
|
2367
|
+
var pref = prefState[0]; var setPref = prefState[1]
|
|
2368
|
+
var shellState = useState(0); var containerW = shellState[0]; var setContainerW = shellState[1]
|
|
2369
|
+
var shellRef = useRef(null)
|
|
2370
|
+
|
|
2371
|
+
useEffect(function () {
|
|
2372
|
+
var el = shellRef.current
|
|
2373
|
+
if (!el) return
|
|
2374
|
+
function measure() { setContainerW(el.clientWidth || 0) }
|
|
2375
|
+
measure()
|
|
2376
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
2377
|
+
var observer = new ResizeObserver(measure)
|
|
2378
|
+
observer.observe(el)
|
|
2379
|
+
return function () { observer.disconnect() }
|
|
2380
|
+
}
|
|
2381
|
+
window.addEventListener('resize', measure)
|
|
2382
|
+
return function () { window.removeEventListener('resize', measure) }
|
|
2383
|
+
}, [])
|
|
2384
|
+
|
|
2385
|
+
// DSH 对话区公式:默认 clamp(680, 64% 容器, 920);用户值 [640, 容器-176]。
|
|
2386
|
+
var widthNow = containerW || 920
|
|
2387
|
+
var maxW = Math.max(640, widthNow - 176)
|
|
2388
|
+
var contentW = pref > 0 ? Math.min(Math.max(pref, 640), maxW) : Math.max(680, Math.min(widthNow * 0.64, 920))
|
|
2389
|
+
|
|
2390
|
+
function beginWidthDrag(side, event) {
|
|
2391
|
+
event.preventDefault()
|
|
2392
|
+
var handle = event.currentTarget
|
|
2393
|
+
var shell = handle.closest ? handle.closest('.dm-content-shell') : (handle.parentElement && handle.parentElement.parentElement)
|
|
2394
|
+
if (!shell) return
|
|
2395
|
+
var max = Math.max(640, (shell.clientWidth || 920) - 176)
|
|
2396
|
+
var base = contentW
|
|
2397
|
+
var originX = event.clientX
|
|
2398
|
+
var latest = contentW
|
|
2399
|
+
var firstBox = handle.getBoundingClientRect()
|
|
2400
|
+
handle.style.setProperty('--dm-width-handle-pointer-y', (event.clientY - firstBox.top) + 'px')
|
|
2401
|
+
handle.toggleAttribute('data-dragging', true)
|
|
2402
|
+
document.body.style.cursor = 'col-resize'
|
|
2403
|
+
document.body.style.userSelect = 'none'
|
|
2404
|
+
function onMove(moveEvent) {
|
|
2405
|
+
var dx = moveEvent.clientX - originX
|
|
2406
|
+
var outward = side === 'right' ? dx : -dx
|
|
2407
|
+
// 只有拖拽时才更新高亮位置(与 DSH 一致);hover 时保持手柄高度的 50%。
|
|
2408
|
+
var box = handle.getBoundingClientRect()
|
|
2409
|
+
handle.style.setProperty('--dm-width-handle-pointer-y', (moveEvent.clientY - box.top) + 'px')
|
|
2410
|
+
// 两侧同时外扩/内收,所以内容宽度变化是手柄位移的 2 倍(与 DSH 一致)。
|
|
2411
|
+
latest = Math.min(Math.max(base + outward * 2, 640), max)
|
|
2412
|
+
setPref(latest)
|
|
2413
|
+
}
|
|
2414
|
+
function onUp() {
|
|
2415
|
+
handle.toggleAttribute('data-dragging', false)
|
|
2416
|
+
window.removeEventListener('pointermove', onMove)
|
|
2417
|
+
window.removeEventListener('pointerup', onUp)
|
|
2418
|
+
document.body.style.cursor = ''
|
|
2419
|
+
document.body.style.userSelect = ''
|
|
2420
|
+
try { window.localStorage.setItem('dsh-mesh.contentWidth', String(latest)) } catch (error) { /* ignore */ }
|
|
2421
|
+
}
|
|
2422
|
+
window.addEventListener('pointermove', onMove)
|
|
2423
|
+
window.addEventListener('pointerup', onUp)
|
|
2424
|
+
}
|
|
2425
|
+
function resetContentWidth() {
|
|
2426
|
+
setPref(0)
|
|
2427
|
+
try { window.localStorage.removeItem('dsh-mesh.contentWidth') } catch (error) { /* ignore */ }
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
return h('div', { className: 'dm-page' },
|
|
2431
|
+
h('style', null, PAGE_CSS),
|
|
2432
|
+
h('div', { className: 'dm-content-shell', ref: shellRef, style: { '--dm-content-w': contentW + 'px' } },
|
|
2433
|
+
h('div', { className: 'dm-width-handle', 'data-side': 'left', title: '拖拽调整内容宽度(双击恢复默认)', onPointerDown: function (event) { beginWidthDrag('left', event) }, onDoubleClick: resetContentWidth }),
|
|
2434
|
+
h('div', { className: 'dm-width-handle', 'data-side': 'right', title: '拖拽调整内容宽度(双击恢复默认)', onPointerDown: function (event) { beginWidthDrag('right', event) }, onDoubleClick: resetContentWidth }),
|
|
2435
|
+
h('div', { className: 'dm-content-scroll' },
|
|
2436
|
+
h('div', { className: 'dm-content', style: { width: 'min(100%, ' + contentW + 'px)' } }, h(MeshStatusSection))
|
|
2437
|
+
)
|
|
2438
|
+
)
|
|
2439
|
+
)
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2252
2442
|
var SECTION = { name: 'settings.section', id: 'dsh-mesh', order: 90, label: 'AI 网络' }
|
|
2253
2443
|
var inject = ['slots']
|
|
2254
2444
|
|
|
@@ -2317,10 +2507,10 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2317
2507
|
// 2026-11-10:AI 网络从「设置」迁到左侧栏独立页面,使用更大的主区域。
|
|
2318
2508
|
try {
|
|
2319
2509
|
disposers.push(ctx.slots.inject('sidebar.panellist', function () {
|
|
2320
|
-
return ctx.slots.register({ name: 'sidebar.panellist', id: 'mesh-network', order: 54, label: function () { return 'AI 网络' } },
|
|
2510
|
+
return ctx.slots.register({ name: 'sidebar.panellist', id: 'mesh-network', order: 54, label: function () { return 'AI 网络' } }, networkPanelIcon)
|
|
2321
2511
|
}))
|
|
2322
2512
|
disposers.push(ctx.slots.inject('main', function () {
|
|
2323
|
-
return ctx.slots.register({ name: 'main', key: 'mesh-network', inject: function () { return {} } },
|
|
2513
|
+
return ctx.slots.register({ name: 'main', key: 'mesh-network', inject: function () { return {} } }, MeshNetworkPage)
|
|
2324
2514
|
}))
|
|
2325
2515
|
} catch (error) {
|
|
2326
2516
|
console.warn('[dsh-mesh-ui] sidebar network page registration failed:', error)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mesh-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "DSH sidebar page for dsh-mesh: AI network topology, peer health, and delegation panel. Read-only; data is collected from the installed dsh-mesh-node package.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|