dsh-mesh-ui 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +8 -2
- package/cordis.patch.yml +4 -0
- package/icon.svg +6 -0
- package/lib/client.js +2199 -0
- package/lib/collect.js +513 -0
- package/lib/delegations.js +240 -0
- package/lib/index.js +199 -0
- package/package.json +45 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,2199 @@
|
|
|
1
|
+
// dsh-mesh-ui — 浏览器半(client plugin bundle)v3
|
|
2
|
+
//
|
|
3
|
+
// 手写成 DSH 期望的形状:window.__ModuleLoader__.load({ id, factory(require) }),react 由平台提供。
|
|
4
|
+
// 设置窗口 settings.section(契约:id 必填 + label)→「AI 网络」:
|
|
5
|
+
//
|
|
6
|
+
// ┌ 头部:标题 / verdict / 版本 / 时间 / 刷新 / 复制诊断 / 原始 JSON
|
|
7
|
+
// ├ 画布(viewport):滚轮缩放(以鼠标为中心)、拖拽平移、双击复位、缩放按钮、自适应 fit
|
|
8
|
+
// │ 画布内 = 世界坐标(1080×680):本机居中 + peer 卡片 + 曲线连线 + 延迟标签
|
|
9
|
+
// └ 折叠面板:本机实例 / 委托台账 / 告警历史 / 诊断建议(默认收起,省垂直空间)
|
|
10
|
+
//
|
|
11
|
+
// 2026-11-06 v3 重做(用户反馈:v2 卡片/连线糟糕、设置面板窄导致内容看不见、缺缩放拖拽)。
|
|
12
|
+
//
|
|
13
|
+
// 归宿:reusable(dsh-mesh/plugin/dsh-mesh-ui/lib/)
|
|
14
|
+
|
|
15
|
+
window.__ModuleLoader__.load({
|
|
16
|
+
id: 'dsh-mesh-ui',
|
|
17
|
+
factory: (require) => {
|
|
18
|
+
var module = { exports: {} }
|
|
19
|
+
var exports = module.exports
|
|
20
|
+
var React = require('react')
|
|
21
|
+
var h = React.createElement
|
|
22
|
+
var useState = React.useState
|
|
23
|
+
var useEffect = React.useEffect
|
|
24
|
+
var useRef = React.useRef
|
|
25
|
+
var useCallback = React.useCallback
|
|
26
|
+
|
|
27
|
+
var API = '/api/dsh-mesh/status'
|
|
28
|
+
var REFRESH_MS = 5000
|
|
29
|
+
var SUPPORTS_ZOOM = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('zoom', '1.5')
|
|
30
|
+
|
|
31
|
+
// 2026-11-07 用户要求:卡片标题只用固定中文叫法,不显示电脑设备名
|
|
32
|
+
var NODE_LABELS = {}
|
|
33
|
+
// 2026-11-09 用户要求:名称后显示设备类型。类型依据设备快照(CPU 形态)判断,可直接改这张表:
|
|
34
|
+
var DEVICE_TYPES = {}
|
|
35
|
+
function deviceType(name) {
|
|
36
|
+
var raw = String(name || '').toLowerCase()
|
|
37
|
+
var key = peerDeviceKey(raw)
|
|
38
|
+
return DEVICE_TYPES[key] || DEVICE_TYPES[raw] || ''
|
|
39
|
+
}
|
|
40
|
+
function nodeDisplay(name) {
|
|
41
|
+
var label = nodeLabel(name)
|
|
42
|
+
var type = deviceType(name)
|
|
43
|
+
return type ? label + ' · ' + type : label
|
|
44
|
+
}
|
|
45
|
+
// 同一台设备可能有多个 peer 记录(不同 token/入口:phone-node / phone-node-lan / phone-node-exec)。
|
|
46
|
+
// AI 网络拓扑按「设备」展示:去掉后缀归并,取可达且延迟最优的那条。
|
|
47
|
+
var PEER_SUFFIX = /-(lan|wan|exec|exe|import|local|ctl)$/i
|
|
48
|
+
function peerDeviceKey(name) {
|
|
49
|
+
var n = String(name || '').trim().toLowerCase()
|
|
50
|
+
return n.replace(PEER_SUFFIX, '')
|
|
51
|
+
}
|
|
52
|
+
function dedupePeers(list) {
|
|
53
|
+
var byKey = {}
|
|
54
|
+
var order = []
|
|
55
|
+
;(list || []).forEach(function (peer) {
|
|
56
|
+
var key = peerDeviceKey(peer.name)
|
|
57
|
+
if (!byKey[key]) { byKey[key] = peer; order.push(key); return }
|
|
58
|
+
var cur = byKey[key]
|
|
59
|
+
var better = false
|
|
60
|
+
if (!cur.up && peer.up) better = true
|
|
61
|
+
else if (Boolean(cur.up) === Boolean(peer.up)) {
|
|
62
|
+
var a = cur.latencyMs == null ? 1e9 : cur.latencyMs
|
|
63
|
+
var b = peer.latencyMs == null ? 1e9 : peer.latencyMs
|
|
64
|
+
if (b < a) better = true
|
|
65
|
+
else if (b === a && (peer.instancesCount || 0) > (cur.instancesCount || 0)) better = true
|
|
66
|
+
}
|
|
67
|
+
if (better) byKey[key] = peer
|
|
68
|
+
})
|
|
69
|
+
return order.map(function (k) { return byKey[k] })
|
|
70
|
+
}
|
|
71
|
+
function filterSelfPeers(list, nodeName) {
|
|
72
|
+
var selfKey = peerDeviceKey(nodeName)
|
|
73
|
+
return (list || []).filter(function (peer) { return !selfKey || peerDeviceKey(peer.name) !== selfKey })
|
|
74
|
+
}
|
|
75
|
+
function nodeLabel(name) { return NODE_LABELS[String(name || '').toLowerCase()] || String(name || '未知节点') }
|
|
76
|
+
// [layout-helpers-begin] 2026-11-09 拓扑自定义布局:节点 key 顺序 + localStorage 持久化 + clamp
|
|
77
|
+
var LAYOUT_STORE_KEY = 'dsh-mesh-ui.topology.layout.v1'
|
|
78
|
+
function topologyNodeKeys(data) {
|
|
79
|
+
var d = data || {}
|
|
80
|
+
var peers = filterSelfPeers(dedupePeers(d.peers || []), d.nodeName)
|
|
81
|
+
return ['local'].concat(peers.map(function (peer) { return 'peer:' + peer.name }))
|
|
82
|
+
}
|
|
83
|
+
// 2026-11-10:设备显示顺序(左→右 / 逆时针)——拓扑渲染与拖拽位置表必须共用同一顺序
|
|
84
|
+
var FAN_ORDER = []
|
|
85
|
+
function fanRankOfNodeKey(key, data) {
|
|
86
|
+
var dev = key === 'local' ? peerDeviceKey(data && data.nodeName) : peerDeviceKey(String(key || '').replace(/^peer:/, ''))
|
|
87
|
+
var idx = FAN_ORDER.indexOf(dev)
|
|
88
|
+
return idx < 0 ? 99 : idx
|
|
89
|
+
}
|
|
90
|
+
function orderedNodeKeys(data) {
|
|
91
|
+
var keys = topologyNodeKeys(data).slice()
|
|
92
|
+
keys.sort(function (a, b) {
|
|
93
|
+
var d = fanRankOfNodeKey(a, data) - fanRankOfNodeKey(b, data)
|
|
94
|
+
return d !== 0 ? d : String(a).localeCompare(String(b))
|
|
95
|
+
})
|
|
96
|
+
return keys
|
|
97
|
+
}
|
|
98
|
+
// 2026-11-10:用户手工调整后认可的默认布局(世界坐标,10000×6000 画布内)
|
|
99
|
+
var DEFAULT_DEVICE_POS = {}
|
|
100
|
+
var DEFAULT_INFRA_POS = {
|
|
101
|
+
aliyun: [4887.59, 2856.78],
|
|
102
|
+
sakura: [5177.39, 2858.40]
|
|
103
|
+
}
|
|
104
|
+
function devKeyOfNodeKey(key, data) {
|
|
105
|
+
return key === 'local' ? peerDeviceKey(data && data.nodeName) : peerDeviceKey(String(key || '').replace(/^peer:/, ''))
|
|
106
|
+
}
|
|
107
|
+
function loadMovedLayout() {
|
|
108
|
+
try {
|
|
109
|
+
var raw = window.localStorage.getItem(LAYOUT_STORE_KEY)
|
|
110
|
+
if (!raw) return {}
|
|
111
|
+
var obj = JSON.parse(raw)
|
|
112
|
+
return obj && typeof obj === 'object' ? obj : {}
|
|
113
|
+
} catch (_) { return {} }
|
|
114
|
+
}
|
|
115
|
+
function saveMovedLayout(obj) {
|
|
116
|
+
try { window.localStorage.setItem(LAYOUT_STORE_KEY, JSON.stringify(obj || {})) } catch (_) { /* ignore */ }
|
|
117
|
+
}
|
|
118
|
+
function clampNum(v, min, max) { return Math.max(min, Math.min(max, v)) }
|
|
119
|
+
// [layout-helpers-end]
|
|
120
|
+
// [relay-helpers-begin] 2026-11-09 架构层:识别公网中转站(frps)与通信方式(纯函数,可单测)
|
|
121
|
+
// 判定:同一公网 host 被 >=2 个 peer 使用 → 视为共享中转站;LAN/loopback 不算。
|
|
122
|
+
var RELAY_PUBLIC_KINDS = { wan: 1, frp: 1 }
|
|
123
|
+
function relayGroups(peers) {
|
|
124
|
+
var groups = {}
|
|
125
|
+
var order = []
|
|
126
|
+
;(peers || []).forEach(function (peer) {
|
|
127
|
+
var t = peer.tunnel || {}
|
|
128
|
+
var host = String(t.host || '').trim().toLowerCase()
|
|
129
|
+
if (!host || !RELAY_PUBLIC_KINDS[t.kind]) return
|
|
130
|
+
var g = groups[host]
|
|
131
|
+
if (!g) { g = groups[host] = { host: t.host || host, hostKey: host, kind: t.kind, peers: [], ports: {}, up: 0 }; order.push(host) }
|
|
132
|
+
g.peers.push(peer)
|
|
133
|
+
if (t.port) g.ports[String(t.port)] = (g.ports[String(t.port)] || 0) + 1
|
|
134
|
+
if (peer.up) g.up++
|
|
135
|
+
})
|
|
136
|
+
return order.map(function (k) { return groups[k] })
|
|
137
|
+
.filter(function (g) { return g.peers.length >= 2 })
|
|
138
|
+
.sort(function (a, b) { return b.peers.length - a.peers.length })
|
|
139
|
+
}
|
|
140
|
+
function relayHostSet(peers) {
|
|
141
|
+
var set = {}
|
|
142
|
+
relayGroups(peers).forEach(function (g) { set[g.hostKey] = g })
|
|
143
|
+
return set
|
|
144
|
+
}
|
|
145
|
+
function commLabel(peer, relayHosts) {
|
|
146
|
+
var t = peer.tunnel || {}
|
|
147
|
+
var host = String(t.host || '').toLowerCase()
|
|
148
|
+
if (!host) return { text: '—', short: '—' }
|
|
149
|
+
var relay = relayHosts && relayHosts[host]
|
|
150
|
+
var port = t.port ? ':' + t.port : ''
|
|
151
|
+
if (relay) return { text: (relay.kind === 'frp' ? '域名穿透' : '自建 frps 中转') + ' · ' + relay.host + port, short: '中转' + (t.port ? ' ' + t.port : '') }
|
|
152
|
+
if (t.kind === 'loopback') return { text: '本机回环', short: '本机' }
|
|
153
|
+
if (t.kind === 'lan') return { text: '局域网直连 · ' + t.host + port, short: 'LAN' }
|
|
154
|
+
if (t.kind === 'frp') return { text: '公网穿透 · ' + t.host + port, short: '穿透' }
|
|
155
|
+
if (t.kind === 'wan') return { text: '公网直连 · ' + t.host + port, short: '公网' }
|
|
156
|
+
return { text: (t.label || t.kind || '未知') + ' · ' + t.host + port, short: t.label || '未知' }
|
|
157
|
+
}
|
|
158
|
+
// [relay-helpers-end]
|
|
159
|
+
var W = 560
|
|
160
|
+
var H = 400
|
|
161
|
+
var MIN_SCALE = 0.18
|
|
162
|
+
var MAX_SCALE = 2.6
|
|
163
|
+
|
|
164
|
+
var CSS = `
|
|
165
|
+
.dm-root { display:flex; flex-direction:column; gap:12px; font-size:13px; line-height:1.5; color:inherit; }
|
|
166
|
+
.dm-head { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
167
|
+
.dm-title { font-size:15.5px; font-weight:650; letter-spacing:.2px; }
|
|
168
|
+
.dm-sub { font-size:11.5px; opacity:.55; }
|
|
169
|
+
.dm-pill { padding:1px 9px; border-radius:999px; font-size:11.5px; font-weight:600; border:1px solid transparent; }
|
|
170
|
+
.dm-pill.ok { background:rgba(63,185,80,.14); color:#2e9e4f; border-color:rgba(63,185,80,.35); }
|
|
171
|
+
.dm-pill.bad { background:rgba(248,81,73,.14); color:#c9362c; border-color:rgba(248,81,73,.35); }
|
|
172
|
+
.dm-pill.warn { background:rgba(210,153,34,.16); color:#9a6c12; border-color:rgba(210,153,34,.4); }
|
|
173
|
+
.dm-pill.muted { background:rgba(128,128,128,.12); color:inherit; border-color:rgba(128,128,128,.28); opacity:.8; }
|
|
174
|
+
.dm-btn { font-size:11.5px; padding:3px 10px; border-radius:7px; border:1px solid rgba(128,128,128,.38); background:transparent; color:inherit; cursor:pointer; white-space:nowrap; }
|
|
175
|
+
.dm-btn:hover { background:rgba(128,128,128,.12); }
|
|
176
|
+
.dm-btn.primary { border-color:rgba(88,166,255,.55); color:#3b82d6; }
|
|
177
|
+
|
|
178
|
+
/* 画布视口 */
|
|
179
|
+
.dm-viewport { position:relative; width:100%; height:clamp(240px, 34vh, 380px); min-height:220px; overflow:hidden; border-radius:12px;
|
|
180
|
+
border:1px solid rgba(128,128,128,.22); background:
|
|
181
|
+
radial-gradient(ellipse at 50% 45%, rgba(88,166,255,.07), transparent 60%),
|
|
182
|
+
linear-gradient(rgba(128,128,128,.05) 1px, transparent 1px) 0 0/26px 26px,
|
|
183
|
+
linear-gradient(90deg, rgba(128,128,128,.05) 1px, transparent 1px) 0 0/26px 26px;
|
|
184
|
+
cursor:grab; touch-action:none; user-select:none; }
|
|
185
|
+
.dm-viewport.dragging { cursor:grabbing; }
|
|
186
|
+
.dm-viewport.resizing { cursor:ns-resize; }
|
|
187
|
+
/* 2026-11-09:画布高度可拖拽调整 */
|
|
188
|
+
.dm-resize { position:absolute; left:50%; bottom:0; transform:translateX(-50%); width:64px; height:12px; z-index:6; cursor:ns-resize; display:flex; align-items:center; justify-content:center; border-radius:6px 6px 0 0; background:rgba(128,128,128,.18); color:rgba(128,128,128,.9); font-size:12px; line-height:1; user-select:none; }
|
|
189
|
+
.dm-resize:hover { background:rgba(88,166,255,.28); color:#7fb4ff; }
|
|
190
|
+
/* 2026-11-09:悬浮节点高亮 + 连线上显示完整地址 */
|
|
191
|
+
.dm-edge-label { paint-order: stroke; stroke: rgba(15,17,21,.9); stroke-width: 3px; font-family: ui-monospace,Consolas,monospace; pointer-events: none; }
|
|
192
|
+
body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
|
|
193
|
+
.dm-card-node.hovered { border-color: rgba(88,166,255,.95) !important; box-shadow: 0 0 0 2px rgba(88,166,255,.35), 0 10px 26px rgba(0,0,0,.25); z-index: 5; }
|
|
194
|
+
.dm-world-outer { position:absolute; left:0; top:0; }
|
|
195
|
+
.dm-world { position:absolute; left:0; top:0; transform-origin:0 0; }
|
|
196
|
+
.dm-lines { position:absolute; left:0; top:0; overflow:visible; pointer-events:none; }
|
|
197
|
+
.dm-world-label { position:absolute; left:10px; top:8px; font-size:11px; opacity:.5; font-variant-numeric:tabular-nums; }
|
|
198
|
+
|
|
199
|
+
/* 节点卡片(世界坐标里) */
|
|
200
|
+
.dm-card-node { position:absolute; box-sizing:border-box; overflow:visible; border-radius:12px; padding:10px 12px;
|
|
201
|
+
border:1px solid rgba(128,128,128,.3); background:rgba(128,128,128,.06); color:inherit; box-shadow:0 6px 22px rgba(0,0,0,.10); }
|
|
202
|
+
.dm-card-node.local { border-width:1.6px; border-color:rgba(88,166,255,.55); background:rgba(88,166,255,.10); }
|
|
203
|
+
.dm-card-node.down { border-style:dashed; border-color:rgba(248,81,73,.6); background:rgba(248,81,73,.07); }
|
|
204
|
+
.dm-card-node.down .dm-n-name { opacity:1; }
|
|
205
|
+
.dm-card-node.down .dm-card-domain, .dm-card-node.down .dm-card-dsh { opacity:.62; }
|
|
206
|
+
.dm-card-node.down .dm-v, .dm-card-node.down .dm-big, .dm-card-node.down .dm-extra { opacity:.6; }
|
|
207
|
+
.dm-card-node.down .dm-chip { opacity:.72; }
|
|
208
|
+
.dm-offline-row { margin-top:5px; font-size:10.5px; color:#c9362c; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
209
|
+
.dm-n-head { display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
|
210
|
+
.dm-n-name { font-weight:650; font-size:13px; display:flex; align-items:center; gap:7px; min-width:0; }
|
|
211
|
+
.dm-n-name span.dm-ell { overflow:visible; text-overflow:clip; white-space:nowrap; }
|
|
212
|
+
.dm-dot { width:9px; height:9px; border-radius:50%; display:inline-block; background:#8b949e; flex:none; }
|
|
213
|
+
.dm-dot.ok { background:#2e9e4f; box-shadow:0 0 0 3px rgba(63,185,80,.18); }
|
|
214
|
+
.dm-dot.bad { background:#d14b41; box-shadow:0 0 0 3px rgba(248,81,73,.18); }
|
|
215
|
+
.dm-big { font-size:17px; font-weight:650; font-variant-numeric:tabular-nums; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
216
|
+
.dm-unit { font-size:11px; opacity:.55; margin-left:2px; }
|
|
217
|
+
.dm-row { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:5px; font-size:11.5px; }
|
|
218
|
+
.dm-k { opacity:.58; font-size:11px; }
|
|
219
|
+
.dm-v { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11.5px; font-variant-numeric:tabular-nums; }
|
|
220
|
+
.dm-mono { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
221
|
+
.dm-mini { display:flex; gap:4px; flex-wrap:wrap; margin-top:6px; }
|
|
222
|
+
.dm-extra { font-size:11px; opacity:.6; font-variant-numeric:tabular-nums; }
|
|
223
|
+
.dm-card-node.compact { padding:9px 11px; cursor:pointer; transition:border-color .12s, box-shadow .12s; }
|
|
224
|
+
.dm-card-node.selected { border-color:rgba(88,166,255,.85) !important; box-shadow:0 0 0 2px rgba(88,166,255,.35), 0 6px 18px rgba(0,0,0,.12) !important; }
|
|
225
|
+
.dm-card-domain { margin-top:6px; font-size:10px; opacity:.72; overflow:visible; text-overflow:clip; white-space:nowrap; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
226
|
+
.dm-card-dsh { margin-top:6px; }
|
|
227
|
+
/* 2026-11-09-v6:详情卡列数自适应(宽屏 3 列),不再固定 2 列 */
|
|
228
|
+
.dm-devices { display:grid; grid-template-columns:repeat(auto-fit, minmax(300px, 1fr)); gap:10px; align-items:start; }
|
|
229
|
+
/* 2026-11-09:三通道筛选浮层(画布内左上) */
|
|
230
|
+
.dm-topo-wrap { display:flex; flex-direction:column; gap:6px; }
|
|
231
|
+
.dm-topbar { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
|
232
|
+
.dm-topbar-info { font-size:10.5px; opacity:.55; margin-left:auto; white-space:nowrap; }
|
|
233
|
+
.dm-filters { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
234
|
+
.dm-flt { display:flex; align-items:center; gap:5px; font-size:11.5px; opacity:.55; border:1px solid rgba(128,128,128,.3); border-radius:999px; padding:1px 7px; cursor:pointer; user-select:none; }
|
|
235
|
+
.dm-flt.on { opacity:1; border-color:var(--c); }
|
|
236
|
+
.dm-flt input { margin:0; accent-color:var(--c); }
|
|
237
|
+
.dm-card-node.server { border-radius:12px; background:rgba(128,128,128,.06); }
|
|
238
|
+
.dm-chip.type { border-color:rgba(88,166,255,.5); color:#7fb4ff; }
|
|
239
|
+
/* 2026-11-09:三通道虚线流动动画(位移量必须是 dash 周期 11px 的整数倍,否则循环会抽搐) */
|
|
240
|
+
@keyframes dm-dashflow { to { stroke-dashoffset: -22; } }
|
|
241
|
+
.dm-lines path.dm-edge-frp { animation: dm-dashflow 1.15s linear infinite; stroke-linecap: butt; }
|
|
242
|
+
@media (prefers-reduced-motion: reduce) { .dm-lines path.dm-edge-frp { animation: none; } }
|
|
243
|
+
.dm-infra-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(340px, 1fr)); gap:10px; margin-top:10px; }
|
|
244
|
+
.dm-infra-card { border:1px solid rgba(128,128,128,.3); border-radius:11px; padding:9px 11px; background:rgba(128,128,128,.05); }
|
|
245
|
+
.dm-infra-head { display:flex; align-items:center; gap:7px; font-weight:650; font-size:12.5px; margin-bottom:5px; }
|
|
246
|
+
.dm-device { border:1px solid rgba(128,128,128,.22); border-radius:11px; padding:9px 11px; background:rgba(128,128,128,.035); min-width:0; overflow:hidden; }
|
|
247
|
+
.dm-device.selected { border-color:rgba(88,166,255,.6); background:rgba(88,166,255,.07); }
|
|
248
|
+
.dm-device-head { display:flex; align-items:center; gap:7px; font-weight:650; font-size:12.5px; margin-bottom:6px; }
|
|
249
|
+
.dm-device-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
250
|
+
.dm-device-body { display:flex; flex-direction:column; gap:3px; }
|
|
251
|
+
.dm-device-row { display:flex; align-items:baseline; justify-content:space-between; gap:10px; font-size:11px; min-width:0; }
|
|
252
|
+
.dm-device-k { opacity:.55; flex:0 0 auto; }
|
|
253
|
+
.dm-device-v { flex:1 1 auto; min-width:0; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:right; }
|
|
254
|
+
@media (max-width:720px) { .dm-devices { grid-template-columns:1fr; } }
|
|
255
|
+
.dm-fields { display:grid; grid-template-columns:1fr 1fr; gap:4px 10px; margin-top:6px; }
|
|
256
|
+
.dm-field { min-width:0; }
|
|
257
|
+
.dm-field-k { font-size:9.5px; opacity:.5; line-height:1.2; }
|
|
258
|
+
.dm-field-v { font-size:11px; line-height:1.35; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-variant-numeric:tabular-nums; }
|
|
259
|
+
.dm-tunnel { margin-top:6px; font-size:10px; opacity:.7; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
260
|
+
.dm-chip { font-size:9.5px; padding:0 6px; border-radius:999px; border:1px solid rgba(128,128,128,.3); opacity:.85; white-space:nowrap; }
|
|
261
|
+
.dm-chip.run { border-color:rgba(63,185,80,.45); color:#2e9e4f; opacity:1; }
|
|
262
|
+
.dm-chip.warn { border-color:rgba(210,153,34,.5); color:#9a6c12; opacity:1; }
|
|
263
|
+
.dm-chip.bad { border-color:rgba(248,81,73,.5); color:#c9362c; opacity:1; }
|
|
264
|
+
.dm-chip.ok { border-color:rgba(63,185,80,.4); color:#2e9e4f; opacity:1; }
|
|
265
|
+
|
|
266
|
+
/* 画布浮层控件 */
|
|
267
|
+
.dm-zoom { position:absolute; right:8px; bottom:8px; display:flex; gap:5px; padding:4px; border-radius:9px;
|
|
268
|
+
background:rgba(128,128,128,.12); backdrop-filter:blur(4px); z-index:5; }
|
|
269
|
+
.dm-zoom button { width:22px; height:22px; border-radius:6px; border:1px solid rgba(128,128,128,.3); background:transparent; color:inherit; cursor:pointer; font-size:13px; line-height:1; padding:0; display:flex; align-items:center; justify-content:center; }
|
|
270
|
+
.dm-zoom button:hover { background:rgba(128,128,128,.16); }
|
|
271
|
+
.dm-zoom .pct { font-size:10.5px; opacity:.6; align-self:center; padding:0 2px; font-variant-numeric:tabular-nums; }
|
|
272
|
+
.dm-hint { font-size:10.5px; opacity:.45; padding:0 2px; }
|
|
273
|
+
|
|
274
|
+
/* 折叠面板 */
|
|
275
|
+
.dm-details { border:1px solid rgba(128,128,128,.22); border-radius:11px; background:rgba(128,128,128,.035); }
|
|
276
|
+
.dm-details > summary { cursor:pointer; padding:9px 12px; font-weight:600; font-size:12.5px; list-style:none; display:flex; align-items:center; gap:8px; }
|
|
277
|
+
.dm-details > summary::-webkit-details-marker { display:none; }
|
|
278
|
+
.dm-details > summary::before { content:'▸'; font-size:10px; opacity:.5; transition:transform .12s; }
|
|
279
|
+
.dm-details[open] > summary::before { transform:rotate(90deg); }
|
|
280
|
+
.dm-details > .dm-body { padding:0 12px 11px; }
|
|
281
|
+
.dm-table { width:100%; border-collapse:collapse; }
|
|
282
|
+
.dm-table th { text-align:left; font-weight:600; font-size:11px; opacity:.5; padding:2px 8px 5px 0; border-bottom:1px solid rgba(128,128,128,.2); white-space:nowrap; }
|
|
283
|
+
.dm-table td { padding:5px 8px 5px 0; border-bottom:1px solid rgba(128,128,128,.1); font-size:12px; }
|
|
284
|
+
.dm-table tr:last-child td { border-bottom:none; }
|
|
285
|
+
.dm-trunc { max-width:190px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:inline-block; vertical-align:bottom; }
|
|
286
|
+
.dm-instrow { display:flex; align-items:center; justify-content:space-between; gap:10px; padding:6px 0; border-bottom:1px solid rgba(128,128,128,.1); }
|
|
287
|
+
.dm-instrow:last-child { border-bottom:none; }
|
|
288
|
+
.dm-alertrow { display:flex; gap:8px; align-items:baseline; padding:5px 0; border-bottom:1px dashed rgba(128,128,128,.16); font-size:12px; }
|
|
289
|
+
.dm-alertrow:last-child { border-bottom:none; }
|
|
290
|
+
.dm-empty { opacity:.55; font-size:12px; padding:4px 0; }
|
|
291
|
+
.dm-err { color:#c9362c; font-size:12px; }
|
|
292
|
+
.dm-foot { font-size:11px; opacity:.45; }
|
|
293
|
+
@media (prefers-color-scheme: dark) {
|
|
294
|
+
.dm-pill.ok { color:#3fb950; } .dm-pill.bad { color:#ff7b72; } .dm-pill.warn { color:#d29922; }
|
|
295
|
+
.dm-btn.primary { color:#79b8ff; }
|
|
296
|
+
.dm-card-node { background:#1d2025; box-shadow:0 8px 26px rgba(0,0,0,.45); }
|
|
297
|
+
.dm-card-node.local { background:#19222e; }
|
|
298
|
+
.dm-card-node.down { background:rgba(90,32,32,.45); border-color:rgba(248,81,73,.65); }
|
|
299
|
+
.dm-offline-row { color:#ff7b72; }
|
|
300
|
+
.dm-dot.ok { background:#3fb950; } .dm-dot.bad { background:#ff7b72; }
|
|
301
|
+
.dm-chip.run, .dm-chip.ok { color:#3fb950; } .dm-chip.warn { color:#d29922; } .dm-chip.bad { color:#ff7b72; }
|
|
302
|
+
.dm-err { color:#ff7b72; }
|
|
303
|
+
.dm-details { background:rgba(255,255,255,.025); }
|
|
304
|
+
}
|
|
305
|
+
/* 2026-11-09 架构层:公网中转站(frps) */
|
|
306
|
+
/* 2026-11-09 拓扑自定义布局:卡片可拖拽 */
|
|
307
|
+
.dm-card-node.draggable { cursor:grab; touch-action:none; }
|
|
308
|
+
.dm-card-node.draggable:active { cursor:grabbing; }
|
|
309
|
+
.dm-card-node.dragging { z-index:20; border-color:rgba(88,166,255,.85) !important; box-shadow:0 12px 34px rgba(0,0,0,.30) !important; }
|
|
310
|
+
.dm-layout-btn.active { border-color:rgba(88,166,255,.6); color:#3b82d6; }
|
|
311
|
+
.dm-relay { border:1px solid rgba(128,128,128,.22); border-radius:12px; padding:10px 12px; background:rgba(128,128,128,.035); display:flex; flex-direction:column; gap:10px; }
|
|
312
|
+
.dm-relay-head { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; }
|
|
313
|
+
.dm-relay-title { font-size:12.5px; font-weight:650; }
|
|
314
|
+
.dm-relay-row { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:8px; border:1px dashed rgba(128,128,128,.26); border-radius:10px; }
|
|
315
|
+
.dm-relay-end { min-width:120px; display:flex; flex-direction:column; gap:2px; }
|
|
316
|
+
.dm-relay-name { font-size:12px; font-weight:650; }
|
|
317
|
+
.dm-relay-sub { font-size:10.5px; opacity:.55; }
|
|
318
|
+
.dm-relay-hub { min-width:190px; display:flex; flex-direction:column; gap:4px; padding:8px 10px; border:1.4px solid rgba(210,153,34,.55); border-radius:10px; background:rgba(210,153,34,.10); }
|
|
319
|
+
.dm-relay-chips { display:flex; gap:5px; flex-wrap:wrap; }
|
|
320
|
+
.dm-relay-link { display:flex; align-items:center; gap:5px; min-width:56px; color:rgba(128,128,128,.75); }
|
|
321
|
+
.dm-relay-line { flex:1 1 auto; height:0; border-top:1.6px dashed rgba(63,185,80,.55); }
|
|
322
|
+
.dm-relay-dirlabel { font-size:10px; opacity:.6; white-space:nowrap; }
|
|
323
|
+
.dm-relay-peers { display:flex; flex-direction:column; gap:4px; min-width:150px; flex:1 1 auto; }
|
|
324
|
+
.dm-relay-peer { display:flex; align-items:center; gap:6px; font-size:11.5px; }
|
|
325
|
+
.dm-relay-peer.down { opacity:.55; }
|
|
326
|
+
.dm-relay-peer-name { font-weight:600; }
|
|
327
|
+
.dm-relay-peer-meta { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:10.5px; opacity:.7; }
|
|
328
|
+
.dm-relay-direct { display:flex; align-items:center; gap:6px; flex-wrap:wrap; font-size:11px; }
|
|
329
|
+
.dm-relay-tag { margin-top:5px; font-size:10px; opacity:.75; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
330
|
+
`
|
|
331
|
+
|
|
332
|
+
function fetchStatus(force) {
|
|
333
|
+
return fetch(force ? API + '?force=1' : API, { headers: { accept: 'application/json' } }).then(function (res) { return res.json() })
|
|
334
|
+
}
|
|
335
|
+
function tone(verdict) {
|
|
336
|
+
if (verdict === 'ok') return 'ok'
|
|
337
|
+
if (verdict === 'down') return 'bad'
|
|
338
|
+
if (verdict === 'degraded') return 'warn'
|
|
339
|
+
return 'muted'
|
|
340
|
+
}
|
|
341
|
+
function ago(iso) {
|
|
342
|
+
if (!iso) return '—'
|
|
343
|
+
var diff = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000))
|
|
344
|
+
if (diff < 60) return diff + ' 秒前'
|
|
345
|
+
if (diff < 3600) return Math.round(diff / 60) + ' 分钟前'
|
|
346
|
+
if (diff < 86400) return Math.round(diff / 3600) + ' 小时前'
|
|
347
|
+
return Math.round(diff / 86400) + ' 天前'
|
|
348
|
+
}
|
|
349
|
+
function clockText(iso) {
|
|
350
|
+
if (!iso) return '—'
|
|
351
|
+
var d = new Date(iso)
|
|
352
|
+
if (isNaN(d.getTime())) return '—'
|
|
353
|
+
function p2(n) { return (n < 10 ? '0' : '') + n }
|
|
354
|
+
return p2(d.getHours()) + ':' + p2(d.getMinutes()) + ':' + p2(d.getSeconds())
|
|
355
|
+
}
|
|
356
|
+
function uptimeText(sec) {
|
|
357
|
+
if (sec == null) return '—'
|
|
358
|
+
var s = Math.max(0, Number(sec))
|
|
359
|
+
var days = Math.floor(s / 86400)
|
|
360
|
+
var hours = Math.floor((s % 86400) / 3600)
|
|
361
|
+
var mins = Math.floor((s % 3600) / 60)
|
|
362
|
+
if (days > 0) return days + '天' + hours + '小时'
|
|
363
|
+
if (hours > 0) return hours + '小时' + mins + '分'
|
|
364
|
+
return mins + '分'
|
|
365
|
+
}
|
|
366
|
+
function dur(sec) {
|
|
367
|
+
if (sec == null) return '—'
|
|
368
|
+
if (sec < 60) return sec + 's'
|
|
369
|
+
if (sec < 3600) return Math.floor(sec / 60) + 'm' + (sec % 60) + 's'
|
|
370
|
+
return Math.floor(sec / 3600) + 'h' + Math.floor((sec % 3600) / 60) + 'm'
|
|
371
|
+
}
|
|
372
|
+
function stateChip(state) {
|
|
373
|
+
// 设置页表格里的状态也统一中文
|
|
374
|
+
var map = { done: 'ok', failed: 'bad', cancelled: 'muted', stalled: 'warn', working: 'run', question: 'warn', submitted: 'muted', accepted: 'muted' }
|
|
375
|
+
return h('span', { className: 'dm-chip ' + (map[state] || 'muted') }, DG_STATE_LABELS[state] || state || '未知')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function Spark(props) {
|
|
379
|
+
var values = props.values || []
|
|
380
|
+
var pts = values.filter(function (v) { return v != null })
|
|
381
|
+
if (pts.length < 2) return h('span', { className: 'dm-sub' }, '—')
|
|
382
|
+
var min = Math.min.apply(null, pts)
|
|
383
|
+
var max = Math.max.apply(null, pts)
|
|
384
|
+
var span = max - min || 1
|
|
385
|
+
var SW = 54
|
|
386
|
+
var SH = 15
|
|
387
|
+
var d = []
|
|
388
|
+
var open = false
|
|
389
|
+
values.forEach(function (v, i) {
|
|
390
|
+
if (v == null) { open = false; return }
|
|
391
|
+
var x = (i / Math.max(1, values.length - 1)) * SW
|
|
392
|
+
var y = SH - ((v - min) / span) * (SH - 4) - 2
|
|
393
|
+
d.push((open ? 'L' : 'M') + x.toFixed(1) + ' ' + y.toFixed(1))
|
|
394
|
+
open = true
|
|
395
|
+
})
|
|
396
|
+
return h('svg', { width: SW, height: SH, viewBox: '0 0 ' + SW + ' ' + SH, style: { overflow: 'visible' } },
|
|
397
|
+
h('path', { d: d.join(' '), fill: 'none', stroke: props.up ? '#2e9e4f' : '#d14b41', strokeWidth: 1.6, strokeLinejoin: 'round', strokeLinecap: 'round' })
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ---------- 画布:缩放 / 平移 ----------
|
|
402
|
+
function Viewport(props) {
|
|
403
|
+
var layout = props.layout
|
|
404
|
+
// 2026-11-09-v5:卡片宽度自适应 —— 子组件测量真实卡片宽高后回传,按实际尺寸重算多边形布局
|
|
405
|
+
var sizeState = useState(null)
|
|
406
|
+
var measuredSize = sizeState[0]
|
|
407
|
+
var setMeasuredSize = sizeState[1]
|
|
408
|
+
if (measuredSize && measuredSize.w > 0 && measuredSize.h > 0) {
|
|
409
|
+
layout = computeLayout(layout.count, { cardW: measuredSize.w, cardH: measuredSize.h })
|
|
410
|
+
}
|
|
411
|
+
var infraBoundsRef = useRef([])
|
|
412
|
+
// 2026-11-09:重置拓扑时同时通知 Topology 复位服务器卡片
|
|
413
|
+
var resetNonceState = useState(0)
|
|
414
|
+
var resetNonce = resetNonceState[0]
|
|
415
|
+
var bumpResetNonce = resetNonceState[1]
|
|
416
|
+
function onInfraBounds(boxes) { infraBoundsRef.current = boxes || [] }
|
|
417
|
+
function onCardSize(w, h) {
|
|
418
|
+
setMeasuredSize(function (prev) {
|
|
419
|
+
if (prev && prev.w === w && prev.h === h) return prev
|
|
420
|
+
return { w: w, h: h }
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
// 2026-11-09 自定义布局:moved[key] = [x,y](绝对世界坐标;localStorage 持久化,重置清空)
|
|
424
|
+
// 2026-11-09-v2:用户反馈「拖不远」→ 世界画布固定放大(可拖很开),自适应按实际内容 bbox 缩放,
|
|
425
|
+
// 而不是按初始画布尺寸;卡片只能在世界边界内移动(防止拖丢),但世界足够大。
|
|
426
|
+
var WORLD_MIN_W = 10000
|
|
427
|
+
var WORLD_MIN_H = 6000
|
|
428
|
+
var WORLD_PAD = 16
|
|
429
|
+
var nodeKeys = orderedNodeKeys(props.data)
|
|
430
|
+
var layoutState = useState(loadMovedLayout)
|
|
431
|
+
var movedLayout = layoutState[0]
|
|
432
|
+
var setMovedLayout = layoutState[1]
|
|
433
|
+
var worldW = Math.max(layout.W, WORLD_MIN_W)
|
|
434
|
+
var worldH = Math.max(layout.H, WORLD_MIN_H)
|
|
435
|
+
var baseOffsetX = (worldW - layout.W) / 2
|
|
436
|
+
var baseOffsetY = (worldH - layout.H) / 2
|
|
437
|
+
var positions = layout.positions.map(function (p, idx) {
|
|
438
|
+
var m = movedLayout[nodeKeys[idx]]
|
|
439
|
+
if (m && m.length === 2) return [Number(m[0]), Number(m[1])]
|
|
440
|
+
var devKey = devKeyOfNodeKey(nodeKeys[idx], props.data)
|
|
441
|
+
var def = DEFAULT_DEVICE_POS[devKey]
|
|
442
|
+
if (def) return [def[0], def[1]]
|
|
443
|
+
return [p[0] + baseOffsetX, p[1] + baseOffsetY]
|
|
444
|
+
})
|
|
445
|
+
var effLayout = Object.assign({}, layout, { W: worldW, H: worldH, positions: positions, baseOffsetX: baseOffsetX, baseOffsetY: baseOffsetY, contentW: layout.W, contentH: layout.H })
|
|
446
|
+
function moveNode(key, pos) {
|
|
447
|
+
setMovedLayout(function (prev) {
|
|
448
|
+
var next = Object.assign({}, prev)
|
|
449
|
+
next[key] = pos
|
|
450
|
+
saveMovedLayout(next)
|
|
451
|
+
return next
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
function resetLayout() {
|
|
455
|
+
setMovedLayout({})
|
|
456
|
+
saveMovedLayout({})
|
|
457
|
+
// 2026-11-09:重置也恢复画布高度(用户反馈重置后"高度拉得很长"其实是之前拖拽的高度被保留)
|
|
458
|
+
setCustomHeight(0)
|
|
459
|
+
try { window.localStorage.removeItem('dsh-mesh-ui.viewportH.v1') } catch (_) { /* ignore */ }
|
|
460
|
+
interactedRef.current = false
|
|
461
|
+
bumpResetNonce(function (x) { return x + 1 })
|
|
462
|
+
setTimeout(fit, 120)
|
|
463
|
+
}
|
|
464
|
+
// 2026-11-09-v3:拖卡片到画布边缘时自动平移视角(不用手动挪画面就能拖很远)
|
|
465
|
+
function autoPanAt(clientX, clientY) {
|
|
466
|
+
var el = viewRef.current
|
|
467
|
+
if (!el) return
|
|
468
|
+
var rect = el.getBoundingClientRect()
|
|
469
|
+
var MARGIN = 60
|
|
470
|
+
var SPEED = 16
|
|
471
|
+
var dx = 0
|
|
472
|
+
var dy = 0
|
|
473
|
+
if (clientX < rect.left + MARGIN) dx = SPEED * Math.min(1, (rect.left + MARGIN - clientX) / MARGIN)
|
|
474
|
+
else if (clientX > rect.right - MARGIN) dx = -SPEED * Math.min(1, (clientX - (rect.right - MARGIN)) / MARGIN)
|
|
475
|
+
if (clientY < rect.top + MARGIN) dy = SPEED * Math.min(1, (rect.top + MARGIN - clientY) / MARGIN)
|
|
476
|
+
else if (clientY > rect.bottom - MARGIN) dy = -SPEED * Math.min(1, (clientY - (rect.bottom - MARGIN)) / MARGIN)
|
|
477
|
+
if (!dx && !dy) return
|
|
478
|
+
interactedRef.current = true
|
|
479
|
+
setView(function (prev) { return { scale: prev.scale, x: prev.x + dx, y: prev.y + dy } })
|
|
480
|
+
}
|
|
481
|
+
var viewRef = useRef(null)
|
|
482
|
+
// 2026-11-09:画布高度可上下拖拽调整(持久化;双击恢复默认)
|
|
483
|
+
var heightState = useState(function () { try { var v = Number(window.localStorage.getItem('dsh-mesh-ui.viewportH.v1') || 0); return v > 0 ? v : 0 } catch (e) { return 0 } })
|
|
484
|
+
var customHeight = heightState[0]
|
|
485
|
+
var setCustomHeight = heightState[1]
|
|
486
|
+
// 2026-11-10:画布默认高度自动跟随内容(避免固定 34vh 导致"拓扑区域又高又空");手动拖过则以手动为准
|
|
487
|
+
var autoHState = useState(0)
|
|
488
|
+
var autoH = autoHState[0]
|
|
489
|
+
var setAutoH = autoHState[1]
|
|
490
|
+
var customHeightRef = useRef(0)
|
|
491
|
+
var resizeRef = useRef(null)
|
|
492
|
+
var resizingState = useState(false); var resizing = resizingState[0]; var setResizing = resizingState[1]
|
|
493
|
+
function onResizeDown(e) {
|
|
494
|
+
if (e.button !== 0) return
|
|
495
|
+
e.preventDefault()
|
|
496
|
+
e.stopPropagation()
|
|
497
|
+
var el = viewRef.current
|
|
498
|
+
if (!el) return
|
|
499
|
+
resizeRef.current = { sy: e.clientY, h: el.getBoundingClientRect().height }
|
|
500
|
+
interactedRef.current = true
|
|
501
|
+
setResizing(true)
|
|
502
|
+
try { e.currentTarget.setPointerCapture(e.pointerId) } catch (_) { /* ignore */ }
|
|
503
|
+
}
|
|
504
|
+
function onResizeMove(e) {
|
|
505
|
+
var r = resizeRef.current
|
|
506
|
+
if (!r) return
|
|
507
|
+
e.stopPropagation()
|
|
508
|
+
var h2 = Math.max(240, Math.min(1600, Math.round(r.h + (e.clientY - r.sy))))
|
|
509
|
+
setCustomHeight(h2)
|
|
510
|
+
try { window.localStorage.setItem('dsh-mesh-ui.viewportH.v1', String(h2)) } catch (_) { /* ignore */ }
|
|
511
|
+
}
|
|
512
|
+
function onResizeUp(e) {
|
|
513
|
+
if (!resizeRef.current) return
|
|
514
|
+
e.stopPropagation()
|
|
515
|
+
resizeRef.current = null
|
|
516
|
+
setResizing(false)
|
|
517
|
+
interactedRef.current = false
|
|
518
|
+
setTimeout(fit, 0)
|
|
519
|
+
}
|
|
520
|
+
function resetHeight() {
|
|
521
|
+
setCustomHeight(0)
|
|
522
|
+
try { window.localStorage.removeItem('dsh-mesh-ui.viewportH.v1') } catch (_) { /* ignore */ }
|
|
523
|
+
setTimeout(fit, 0)
|
|
524
|
+
}
|
|
525
|
+
var dragRef = useRef(null)
|
|
526
|
+
var interactedRef = useRef(false)
|
|
527
|
+
var s0 = useState({ scale: 1, x: 0, y: 0 }); var view = s0[0]; var setView = s0[1]
|
|
528
|
+
var s1 = useState(false); var dragging = s1[0]; var setDragging = s1[1]
|
|
529
|
+
// 2026-11-09:三通道筛选(局域网/公网/樱花)
|
|
530
|
+
var filterState = useState({ lan: true, public: true, sakura: true })
|
|
531
|
+
var showFilters = filterState[0]
|
|
532
|
+
var setShowFilters = filterState[1]
|
|
533
|
+
function toggleFilter(k) { setShowFilters(function (prev) { var n = Object.assign({}, prev); n[k] = !n[k]; return n }) }
|
|
534
|
+
// fit 用最新布局的 ref(拖拽中不重算视角;只有初始化 / 窗口变化 / ⟲ 重置 / ⤢ 自适应才重算)
|
|
535
|
+
var effRef = useRef(effLayout)
|
|
536
|
+
effRef.current = effLayout
|
|
537
|
+
|
|
538
|
+
var fit = useCallback(function () {
|
|
539
|
+
var el = viewRef.current
|
|
540
|
+
if (!el) return
|
|
541
|
+
var rect = el.getBoundingClientRect()
|
|
542
|
+
if (!rect.width || !rect.height) return
|
|
543
|
+
var cur = effRef.current || layout
|
|
544
|
+
var halfW = cur.cardW / 2
|
|
545
|
+
var halfH = cur.cardH / 2
|
|
546
|
+
var xs = []
|
|
547
|
+
var ys = []
|
|
548
|
+
;(cur.positions || []).forEach(function (p) { xs.push(p[0]); ys.push(p[1]) })
|
|
549
|
+
if (!xs.length) { xs = [cur.W / 2]; ys = [cur.H / 2] }
|
|
550
|
+
var minX = Math.min.apply(null, xs) - halfW - WORLD_PAD
|
|
551
|
+
var maxX = Math.max.apply(null, xs) + halfW + WORLD_PAD
|
|
552
|
+
var minY = Math.min.apply(null, ys) - halfH - WORLD_PAD
|
|
553
|
+
var maxY = Math.max.apply(null, ys) + halfH + WORLD_PAD
|
|
554
|
+
// 2026-11-09:中转服务器卡片(阿里云/樱花)位置由 Topology 维护,按真实包围盒纳入自适应
|
|
555
|
+
;(infraBoundsRef.current || []).forEach(function (b) {
|
|
556
|
+
if (!b) return
|
|
557
|
+
minX = Math.min(minX, b.x - b.w / 2 - WORLD_PAD)
|
|
558
|
+
maxX = Math.max(maxX, b.x + b.w / 2 + WORLD_PAD)
|
|
559
|
+
minY = Math.min(minY, b.y - b.h / 2 - WORLD_PAD)
|
|
560
|
+
maxY = Math.max(maxY, b.y + b.h / 2 + WORLD_PAD)
|
|
561
|
+
})
|
|
562
|
+
var bw = Math.max(1, maxX - minX)
|
|
563
|
+
var bh = Math.max(1, maxY - minY)
|
|
564
|
+
var scale = Math.min(rect.width / bw, rect.height / bh) * 0.94
|
|
565
|
+
scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale))
|
|
566
|
+
setView({ scale: scale, x: (rect.width - bw * scale) / 2 - minX * scale, y: (rect.height - bh * scale) / 2 - minY * scale })
|
|
567
|
+
// 画布默认高度 = 内容渲染高度 + 余量(仅当用户没手动调过高度)
|
|
568
|
+
if (!customHeightRef.current) {
|
|
569
|
+
var wantH = Math.max(150, Math.min(380, Math.round(bh * scale + 64)))
|
|
570
|
+
setAutoH(function (prev) { return Math.abs(prev - wantH) >= 10 ? wantH : prev })
|
|
571
|
+
}
|
|
572
|
+
}, [layout.W, layout.H])
|
|
573
|
+
|
|
574
|
+
useEffect(function () {
|
|
575
|
+
fit()
|
|
576
|
+
var el = viewRef.current
|
|
577
|
+
if (!el || typeof ResizeObserver === 'undefined') return
|
|
578
|
+
var ro = new ResizeObserver(function () {
|
|
579
|
+
if (!interactedRef.current) fit()
|
|
580
|
+
})
|
|
581
|
+
ro.observe(el)
|
|
582
|
+
return function () { try { ro.disconnect() } catch (_) { /* ignore */ } }
|
|
583
|
+
}, [fit])
|
|
584
|
+
|
|
585
|
+
// 2026-11-10:布局变化(节点数/卡片尺寸)后自动 fit(用户未手动缩放/平移时),避免停留在旧的缩放比例
|
|
586
|
+
useEffect(function () {
|
|
587
|
+
if (!interactedRef.current) setTimeout(fit, 40)
|
|
588
|
+
}, [layout.count, layout.cardW, layout.cardH])
|
|
589
|
+
|
|
590
|
+
useEffect(function () {
|
|
591
|
+
var el = viewRef.current
|
|
592
|
+
if (!el) return
|
|
593
|
+
function onWheel(event) {
|
|
594
|
+
event.preventDefault()
|
|
595
|
+
var rect = el.getBoundingClientRect()
|
|
596
|
+
var px = event.clientX - rect.left
|
|
597
|
+
var py = event.clientY - rect.top
|
|
598
|
+
var factor = Math.pow(1.0015, -event.deltaY)
|
|
599
|
+
interactedRef.current = true
|
|
600
|
+
setView(function (prev) {
|
|
601
|
+
var next = Math.max(MIN_SCALE, Math.min(MAX_SCALE, prev.scale * factor))
|
|
602
|
+
var k = next / prev.scale
|
|
603
|
+
return { scale: next, x: px - (px - prev.x) * k, y: py - (py - prev.y) * k }
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
el.addEventListener('wheel', onWheel, { passive: false })
|
|
607
|
+
return function () { el.removeEventListener('wheel', onWheel) }
|
|
608
|
+
}, [])
|
|
609
|
+
|
|
610
|
+
function onPointerDown(event) {
|
|
611
|
+
if (event.button !== 0) return
|
|
612
|
+
if (event.target && event.target.closest && event.target.closest('button,a,summary,.dm-filters,.dm-zoom,.dm-resize')) return
|
|
613
|
+
dragRef.current = { x: event.clientX, y: event.clientY, vx: view.x, vy: view.y }
|
|
614
|
+
setDragging(true)
|
|
615
|
+
event.currentTarget.setPointerCapture && event.currentTarget.setPointerCapture(event.pointerId)
|
|
616
|
+
}
|
|
617
|
+
function onPointerMove(event) {
|
|
618
|
+
var drag = dragRef.current
|
|
619
|
+
if (!drag) return
|
|
620
|
+
interactedRef.current = true
|
|
621
|
+
setView(function (prev) { return { scale: prev.scale, x: drag.vx + (event.clientX - drag.x), y: drag.vy + (event.clientY - drag.y) } })
|
|
622
|
+
}
|
|
623
|
+
function endDrag() { dragRef.current = null; setDragging(false) }
|
|
624
|
+
|
|
625
|
+
function zoomBy(multiplier) {
|
|
626
|
+
var el = viewRef.current
|
|
627
|
+
if (!el) return
|
|
628
|
+
var rect = el.getBoundingClientRect()
|
|
629
|
+
var px = rect.width / 2
|
|
630
|
+
var py = rect.height / 2
|
|
631
|
+
interactedRef.current = true
|
|
632
|
+
setView(function (prev) {
|
|
633
|
+
var next = Math.max(MIN_SCALE, Math.min(MAX_SCALE, prev.scale * multiplier))
|
|
634
|
+
var k = next / prev.scale
|
|
635
|
+
return { scale: next, x: px - (px - prev.x) * k, y: py - (py - prev.y) * k }
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
customHeightRef.current = customHeight
|
|
640
|
+
customHeightRef.current = customHeight
|
|
641
|
+
// 2026-11-10:画布内的文字/勾选项全部移到画布外(上方工具栏 = 勾选 + 链路信息;下方 = 缩放/说明)
|
|
642
|
+
return h('div', { className: 'dm-topo-wrap' },
|
|
643
|
+
h('div', { className: 'dm-topbar' },
|
|
644
|
+
h('div', { className: 'dm-filters' },
|
|
645
|
+
[['lan', '局域网'], ['public', '阿里云'], ['sakura', '樱花穿透']].map(function (it) {
|
|
646
|
+
var tr = TRANSPORTS[it[0]]
|
|
647
|
+
return h('label', { key: it[0], className: 'dm-flt' + (showFilters[it[0]] ? ' on' : ''), style: { '--c': tr.color } },
|
|
648
|
+
h('input', { type: 'checkbox', checked: showFilters[it[0]], onChange: function () { toggleFilter(it[0]) } }),
|
|
649
|
+
h('span', null, it[1])
|
|
650
|
+
)
|
|
651
|
+
})
|
|
652
|
+
),
|
|
653
|
+
h('span', { className: 'dm-topbar-info' }, effLayout.count + ' 节点 · ' + effLayout.edges.length + ' 条链路 · 拖拽卡片自定义布局')
|
|
654
|
+
),
|
|
655
|
+
h('div', { className: 'dm-viewport' + (dragging ? ' dragging' : '') + (resizing ? ' resizing' : ''), ref: viewRef,
|
|
656
|
+
style: (customHeight || autoH) ? { height: (customHeight || autoH) + 'px' } : undefined,
|
|
657
|
+
onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endDrag, onPointerCancel: endDrag, onPointerLeave: endDrag,
|
|
658
|
+
onDoubleClick: function () { interactedRef.current = false; fit() } },
|
|
659
|
+
// 2026-11-07:缩放改用 CSS zoom(重新布局、文字矢量重绘 → 放大不糊);
|
|
660
|
+
// 平移用外层 transform(不参与光栅缓存)。不再使用 will-change,避免整块被缓存成位图后放大发虚。
|
|
661
|
+
h('div', { className: 'dm-world-outer', style: { transform: 'translate(' + view.x + 'px,' + view.y + 'px)' } },
|
|
662
|
+
h('div', { className: 'dm-world', style: Object.assign({ width: effLayout.W + 'px', height: effLayout.H + 'px' },
|
|
663
|
+
SUPPORTS_ZOOM ? { zoom: view.scale } : { transform: 'scale(' + view.scale + ')' }) },
|
|
664
|
+
h(Topology, { data: props.data, layout: effLayout, selectedKey: props.selectedKey, onSelect: props.onSelect, scale: view.scale, onNodeMove: moveNode, onDragEdge: autoPanAt, onCardSize: onCardSize, show: showFilters, onInfraBounds: onInfraBounds, resetNonce: resetNonce })
|
|
665
|
+
)
|
|
666
|
+
),
|
|
667
|
+
h('div', { className: 'dm-zoom' },
|
|
668
|
+
h('button', { title: '放大', onClick: function () { zoomBy(1.22) } }, '+'),
|
|
669
|
+
h('button', { title: '缩小', onClick: function () { zoomBy(1 / 1.22) } }, '−'),
|
|
670
|
+
h('button', { title: '自适应', onClick: function () { interactedRef.current = false; fit() } }, '⤢'),
|
|
671
|
+
h('button', { className: 'dm-layout-btn', title: '重置为默认拓扑布局', onClick: resetLayout }, '↺'),
|
|
672
|
+
h('span', { className: 'pct' }, Math.round(view.scale * 100) + '%')
|
|
673
|
+
),
|
|
674
|
+
h('div', { className: 'dm-resize', title: '上下拖拽调整画布高度(双击恢复默认)',
|
|
675
|
+
onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp,
|
|
676
|
+
onDoubleClick: resetHeight },
|
|
677
|
+
h('span', null, '⋯')
|
|
678
|
+
)
|
|
679
|
+
),
|
|
680
|
+
h('div', { className: 'dm-hint' }, '拓扑 v17 · 内容 ' + Math.round(layout.W) + '×' + Math.round(layout.H) + 'px · 卡 ' + Math.round(layout.cardW) + '×' + Math.round(layout.cardH) + ' · 缩放 ' + Math.round(view.scale * 100) + '% · 滚轮缩放 / 拖拽平移')
|
|
681
|
+
)
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ---------- 布局计算 ----------
|
|
685
|
+
// 用户偏好(2026-11-07 明确):
|
|
686
|
+
// 4 节点 = 菱形(上/右/下/左);5 节点 = 顶点朝上的五边形;6 = 六边形;n 节点 = 顶点朝上的正 n 边形。
|
|
687
|
+
// 相邻节点的「轴对齐包围盒」必须留出 GAP,否则菱形斜向两张卡会贴住/重叠。
|
|
688
|
+
// 2026-11-09-v6:默认布局 = 顶部「阿里云 / 樱花」服务器一排 + 设备沿下方弧线扇形展开(用户指定)
|
|
689
|
+
function computeLayout(count, override) {
|
|
690
|
+
var GAP = 30
|
|
691
|
+
var cardW = count <= 4 ? 210 : count <= 6 ? 196 : 176
|
|
692
|
+
var cardH = count <= 4 ? 112 : count <= 6 ? 104 : 96
|
|
693
|
+
if (override && override.cardW > 0) cardW = Math.ceil(override.cardW)
|
|
694
|
+
if (override && override.cardH > 0) cardH = Math.ceil(override.cardH)
|
|
695
|
+
var PAD = 16
|
|
696
|
+
var topTierH = Math.round(cardH * 1.1)
|
|
697
|
+
var W, H, positions
|
|
698
|
+
if (count <= 1) {
|
|
699
|
+
W = cardW + PAD * 2
|
|
700
|
+
H = topTierH + cardH + PAD * 2
|
|
701
|
+
positions = [[W / 2, topTierH + cardH / 2 + PAD]]
|
|
702
|
+
} else if (count === 2) {
|
|
703
|
+
W = cardW * 2 + GAP + PAD * 2
|
|
704
|
+
H = topTierH + cardH + PAD * 2
|
|
705
|
+
positions = [[PAD + cardW / 2, topTierH + cardH / 2 + PAD], [PAD + cardW * 1.5 + GAP, topTierH + cardH / 2 + PAD]]
|
|
706
|
+
} else {
|
|
707
|
+
// 2026-11-09-v7:浅扁椭圆弧扇形(横向铺开、纵向贴近顶部服务器,压缩中间空白)
|
|
708
|
+
var START = 150 * Math.PI / 180
|
|
709
|
+
var END = 30 * Math.PI / 180
|
|
710
|
+
var stepA = (START - END) / (count - 1)
|
|
711
|
+
var MARGIN = 22
|
|
712
|
+
var A = (cardW + MARGIN) / (2 * Math.sin(stepA / 2))
|
|
713
|
+
for (var step = 0; step < 400; step++) {
|
|
714
|
+
var ok = true
|
|
715
|
+
for (var i = 0; i < count - 1 && ok; i++) {
|
|
716
|
+
var a1 = START - i * stepA
|
|
717
|
+
var a2 = START - (i + 1) * stepA
|
|
718
|
+
var dx = Math.abs(A * Math.cos(a1) - A * Math.cos(a2))
|
|
719
|
+
var dy = Math.abs(0.35 * A * Math.sin(a1) - 0.35 * A * Math.sin(a2))
|
|
720
|
+
if (dx < cardW + MARGIN * 0.5 && dy < cardH + MARGIN * 0.5) ok = false
|
|
721
|
+
}
|
|
722
|
+
if (ok) break
|
|
723
|
+
A += 4
|
|
724
|
+
}
|
|
725
|
+
var B = A * 0.35
|
|
726
|
+
var halfW = A * Math.cos(END) + cardW / 2
|
|
727
|
+
W = Math.ceil(2 * halfW + PAD * 2)
|
|
728
|
+
var cx = W / 2
|
|
729
|
+
var cy = topTierH + PAD
|
|
730
|
+
H = Math.ceil(cy + B + cardH / 2 + PAD)
|
|
731
|
+
positions = []
|
|
732
|
+
for (var j = 0; j < count; j++) {
|
|
733
|
+
var aj = START - j * stepA
|
|
734
|
+
positions.push([cx + A * Math.cos(aj), cy + B * Math.sin(aj)])
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
var edges = []
|
|
738
|
+
for (var m = 0; m < count; m++) for (var n = m + 1; n < count; n++) edges.push([m, n])
|
|
739
|
+
var topDeviceY = positions.length ? Math.min.apply(null, positions.map(function (p) { return p[1] })) : 0
|
|
740
|
+
return { count: count, cardW: cardW, cardH: cardH, gap: GAP, W: W, H: H, positions: positions, edges: edges, topDeviceY: topDeviceY }
|
|
741
|
+
}
|
|
742
|
+
// ---------- 架构层:公网中转站(frps)与各节点通信关系(2026-11-09) ----------
|
|
743
|
+
function RelayLayer(props) {
|
|
744
|
+
var data = props.data || {}
|
|
745
|
+
var peers = filterSelfPeers(dedupePeers(data.peers || []), data.nodeName)
|
|
746
|
+
var groups = relayGroups(peers)
|
|
747
|
+
var relayHosts = relayHostSet(peers)
|
|
748
|
+
var direct = peers.filter(function (p) { var t = p.tunnel || {}; return t.kind === 'lan' || t.kind === 'loopback' })
|
|
749
|
+
var instances = (data.local && data.local.instances) || []
|
|
750
|
+
var localUp = instances.filter(function (x) { return x.up }).length
|
|
751
|
+
// 2026-11-09:樱花账号隧道(PC 端 API 采集,跨设备含手机)
|
|
752
|
+
var sakuraAccount = (data.tunnels && data.tunnels.sakuraAccount) || null
|
|
753
|
+
if (!groups.length && !direct.length && !sakuraAccount) return null
|
|
754
|
+
return h('div', { className: 'dm-relay' },
|
|
755
|
+
h('div', { className: 'dm-relay-head' },
|
|
756
|
+
h('span', { className: 'dm-relay-title' }, '架构层 · 公网中转站'),
|
|
757
|
+
h('span', { className: 'dm-sub' }, groups.length ? '虚线 = 经中转;实线/标签 = 直连。端口是各节点在中转站上的隧道入口' : '当前没有共享公网中转的 peer,全部为直连')
|
|
758
|
+
),
|
|
759
|
+
groups.map(function (g, gi) {
|
|
760
|
+
var ports = Object.keys(g.ports).sort()
|
|
761
|
+
return h('div', { className: 'dm-relay-row', key: 'g' + gi },
|
|
762
|
+
h('div', { className: 'dm-relay-end' },
|
|
763
|
+
h('div', { className: 'dm-relay-name' }, nodeDisplay(data.nodeName) + '(本机)'),
|
|
764
|
+
h('div', { className: 'dm-relay-sub' }, 'DSH ' + localUp + '/' + instances.length + ' 在线')
|
|
765
|
+
),
|
|
766
|
+
h('div', { className: 'dm-relay-link' }, h('span', { className: 'dm-relay-line' }), h('span', { className: 'dm-relay-dirlabel' }, '出站')),
|
|
767
|
+
h('div', { className: 'dm-relay-hub' },
|
|
768
|
+
h('div', { className: 'dm-relay-name' }, '公网中转站 · frps'),
|
|
769
|
+
h('div', { className: 'dm-relay-sub dm-mono' }, g.host),
|
|
770
|
+
h('div', { className: 'dm-relay-chips' },
|
|
771
|
+
ports.map(function (p) { return h('span', { className: 'dm-chip', key: p, title: '隧道入口 :' + p }, ':' + p + ' ×' + g.ports[p]) }),
|
|
772
|
+
h('span', { className: 'dm-chip ' + (g.up === g.peers.length ? 'run' : 'bad') }, '在线 ' + g.up + '/' + g.peers.length)
|
|
773
|
+
)
|
|
774
|
+
),
|
|
775
|
+
h('div', { className: 'dm-relay-link' }, h('span', { className: 'dm-relay-line' }), h('span', { className: 'dm-relay-dirlabel' }, '中转')),
|
|
776
|
+
h('div', { className: 'dm-relay-peers' }, g.peers.map(function (peer) {
|
|
777
|
+
var t = peer.tunnel || {}
|
|
778
|
+
return h('div', { className: 'dm-relay-peer' + (peer.up ? '' : ' down'), key: peer.name },
|
|
779
|
+
h('span', { className: 'dm-dot ' + (peer.up ? 'ok' : 'bad') }),
|
|
780
|
+
h('span', { className: 'dm-relay-peer-name' }, nodeDisplay(peerDeviceKey(peer.name))),
|
|
781
|
+
h('span', { className: 'dm-relay-peer-meta' }, (t.port ? ':' + t.port : '') + (peer.up && peer.latencyMs != null ? ' · ' + peer.latencyMs + 'ms' : ' · 离线'))
|
|
782
|
+
)
|
|
783
|
+
}))
|
|
784
|
+
)
|
|
785
|
+
}),
|
|
786
|
+
sakuraAccount && sakuraAccount.available ? h('div', { className: 'dm-relay-direct' },
|
|
787
|
+
h('span', { className: 'dm-k' }, '樱花隧道:'),
|
|
788
|
+
h('span', { className: 'dm-chip ' + (sakuraAccount.online === sakuraAccount.total ? 'run' : 'bad') }, sakuraAccount.online + '/' + sakuraAccount.total + ' 在线'),
|
|
789
|
+
(sakuraAccount.tunnels || []).map(function (t) {
|
|
790
|
+
return h('span', { className: 'dm-chip ' + (t.online ? 'run' : 'bad'), key: 'st' + t.id, title: (t.nodeName || '') + ' ' + t.addr + (t.online ? '' : '(离线)') },
|
|
791
|
+
(t.deviceKey || t.name) + ' ' + t.addr)
|
|
792
|
+
})
|
|
793
|
+
) : null,
|
|
794
|
+
direct.length ? h('div', { className: 'dm-relay-direct' },
|
|
795
|
+
h('span', { className: 'dm-k' }, '直连:'),
|
|
796
|
+
direct.map(function (peer) {
|
|
797
|
+
var c = commLabel(peer, relayHosts)
|
|
798
|
+
return h('span', { className: 'dm-chip ' + (peer.up ? 'run' : 'bad'), key: peer.name, title: c.text }, nodeDisplay(peerDeviceKey(peer.name)) + ' · ' + c.text)
|
|
799
|
+
})
|
|
800
|
+
) : null
|
|
801
|
+
)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ===== 2026-11-09 传输层封装:局域网 / 阿里云(公网) / 樱花穿透 三通道 =====
|
|
805
|
+
var TRANSPORTS = {
|
|
806
|
+
lan: { label: '局域网直连', short: 'LAN', color: '#2e9e4f', dash: false },
|
|
807
|
+
public: { label: '公网服务器', short: 'PUB', color: '#4d9bff', dash: true },
|
|
808
|
+
sakura: { label: '樱花穿透', short: 'SAK', color: '#a371f7', dash: true }
|
|
809
|
+
}
|
|
810
|
+
var DOWN_COLOR = "rgba(209,75,65,.7)"
|
|
811
|
+
// 各机 DSH 安装形态(实测/档案)
|
|
812
|
+
var DEVICE_DSH = {}
|
|
813
|
+
function dshTypesFor(devKey) { return DEVICE_DSH[peerDeviceKey(devKey)] || [] }
|
|
814
|
+
function isLanPeer(peer) {
|
|
815
|
+
var t = (peer && peer.tunnel) || {}
|
|
816
|
+
var name = String((peer && peer.name) || '')
|
|
817
|
+
var host = String(t.host || '')
|
|
818
|
+
if (/-lan$/i.test(name)) return true
|
|
819
|
+
if (t.kind === 'lan' || t.kind === 'loopback') return true
|
|
820
|
+
if (/^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(host)) return true
|
|
821
|
+
return false
|
|
822
|
+
}
|
|
823
|
+
function sakuraAccountOf(data) {
|
|
824
|
+
var acc = data && data.tunnels && data.tunnels.sakuraAccount
|
|
825
|
+
return acc && acc.available === true ? acc : null
|
|
826
|
+
}
|
|
827
|
+
function sakuraTunnelOf(data, devKey) {
|
|
828
|
+
var acc = sakuraAccountOf(data)
|
|
829
|
+
if (!acc) return null
|
|
830
|
+
var k = peerDeviceKey(devKey)
|
|
831
|
+
for (var i = 0; i < (acc.tunnels || []).length; i++) if (acc.tunnels[i].deviceKey === k) return acc.tunnels[i]
|
|
832
|
+
return null
|
|
833
|
+
}
|
|
834
|
+
// 设备状态 = 任一链路存活(全断才离线/红)
|
|
835
|
+
function deviceLinkState(data, peer, lanMap) {
|
|
836
|
+
var dk = peerDeviceKey(peer.name)
|
|
837
|
+
var t = peer.tunnel || {}
|
|
838
|
+
var publicUp = Boolean(peer.up)
|
|
839
|
+
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)
|
|
841
|
+
var st = sakuraTunnelOf(data, dk)
|
|
842
|
+
var sakuraUp = st ? st.online === true : null
|
|
843
|
+
var anyUp = publicUp || sakuraUp === true || lanUp
|
|
844
|
+
return { devKey: dk, publicUp: publicUp, lanUp: lanUp, lanTunnel: lan, sakuraUp: sakuraUp, sakuraTunnel: st, anyUp: anyUp }
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ---------- 2026-11-09 画布下方:中转服务器情况(阿里云 / 樱花) ----------
|
|
848
|
+
function InfraPanels(props) {
|
|
849
|
+
var data = props.data || {}
|
|
850
|
+
var acc = sakuraAccountOf(data)
|
|
851
|
+
var peers = filterSelfPeers(dedupePeers(data.peers || []), data.nodeName)
|
|
852
|
+
// 2026-11-10:本机自身也有一条阿里云入口(publicEntry/selfHosted 的 7012),之前漏掉了
|
|
853
|
+
var lt = data.localTunnel || {}
|
|
854
|
+
var selfHost = lt.selfHosted || {}
|
|
855
|
+
var entry = lt.publicEntry || selfHost || {}
|
|
856
|
+
var localPort = entry.port || selfHost.port || null
|
|
857
|
+
var localHost = entry.host || selfHost.host || "relay.example.com"
|
|
858
|
+
var localKey = peerDeviceKey(data.nodeName)
|
|
859
|
+
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
|
+
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' }
|
|
862
|
+
}))
|
|
863
|
+
var aliyunRows = devs.filter(function (d) { return d.port }).map(function (d) {
|
|
864
|
+
return h('div', { key: d.devKey, className: 'dm-device-row' },
|
|
865
|
+
h('span', { className: 'dm-device-k' }, d.name),
|
|
866
|
+
h('span', { className: 'dm-device-v' }, (d.up ? '✅ ' : '❌ ') + (d.host || 'relay.example.com') + ':' + d.port)
|
|
867
|
+
)
|
|
868
|
+
})
|
|
869
|
+
var tunnelRows = acc ? (acc.tunnels || []).map(function (x, i) {
|
|
870
|
+
return h('div', { key: 'st' + i, className: 'dm-device-row' },
|
|
871
|
+
h('span', { className: 'dm-device-k' }, (x.deviceKey ? nodeDisplay(peerDeviceKey(x.deviceKey)) : (x.name || '未归类'))),
|
|
872
|
+
h('span', { className: 'dm-device-v' }, (x.online ? '✅ ' : '❌ ') + (x.addr || ''))
|
|
873
|
+
)
|
|
874
|
+
}) : null
|
|
875
|
+
if (!aliyunRows.length && !tunnelRows) return null
|
|
876
|
+
return h('div', { className: 'dm-infra-grid' },
|
|
877
|
+
h('div', { className: 'dm-infra-card' },
|
|
878
|
+
h('div', { className: 'dm-infra-head' },
|
|
879
|
+
h('span', { className: 'dm-dot ok', style: { background: TRANSPORTS.public.color } }),
|
|
880
|
+
h('span', null, '阿里云服务器 · ECS'),
|
|
881
|
+
h('span', { className: 'dm-sub', style: { marginLeft: 'auto' } }, '自建 frps')
|
|
882
|
+
),
|
|
883
|
+
h('div', { className: 'dm-sub dm-mono', style: { marginBottom: 4 } }, 'relay.example.com · 隧道 ' + (devs.filter(function (d) { return d.port }).map(function (d) { return d.port }).sort().join(' / ') || '—')),
|
|
884
|
+
aliyunRows
|
|
885
|
+
),
|
|
886
|
+
acc ? h('div', { className: 'dm-infra-card' },
|
|
887
|
+
h('div', { className: 'dm-infra-head' },
|
|
888
|
+
h('span', { className: 'dm-dot', style: { background: TRANSPORTS.sakura.color } }),
|
|
889
|
+
h('span', null, '樱花穿透服务器'),
|
|
890
|
+
h('span', { className: 'dm-sub', style: { marginLeft: 'auto' } }, acc.online + '/' + acc.total + ' 在线')
|
|
891
|
+
),
|
|
892
|
+
h('div', { className: 'dm-sub dm-mono', style: { marginBottom: 4 } }, 'relay.example.com / relay.example.com / relay.example.com'),
|
|
893
|
+
tunnelRows
|
|
894
|
+
) : null
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ---------- 拓扑:极简卡片(名称 / 穿透域名 / DSH 数)+ 点击选中 ----------
|
|
899
|
+
function Topology(props) {
|
|
900
|
+
var data = props.data
|
|
901
|
+
var layout = props.layout
|
|
902
|
+
// 2026-11-09-v5:卡片宽度自适应 —— 每次渲染后测量真实尺寸,回传父组件重算布局;
|
|
903
|
+
// 同时把每张卡的实际尺寸给连线用(不同卡片宽窄不同,连线端点贴各自边缘)。
|
|
904
|
+
var cardRefs = useRef({})
|
|
905
|
+
var cardSizeState = useState({})
|
|
906
|
+
var cardSizes = cardSizeState[0]
|
|
907
|
+
var setCardSizes = cardSizeState[1]
|
|
908
|
+
useEffect(function () {
|
|
909
|
+
var next = {}
|
|
910
|
+
var maxW = 0
|
|
911
|
+
var maxH = 0
|
|
912
|
+
var changed = false
|
|
913
|
+
Object.keys(cardRefs.current).forEach(function (key) {
|
|
914
|
+
var el = cardRefs.current[key]
|
|
915
|
+
if (!el) return
|
|
916
|
+
var w = el.offsetWidth
|
|
917
|
+
var h = el.offsetHeight
|
|
918
|
+
if (!w || !h) return
|
|
919
|
+
next[key] = { w: w, h: h }
|
|
920
|
+
if (w > maxW) maxW = w
|
|
921
|
+
if (h > maxH) maxH = h
|
|
922
|
+
var old = cardSizes[key]
|
|
923
|
+
if (!old || Math.abs(old.w - w) > 1 || Math.abs(old.h - h) > 1) changed = true
|
|
924
|
+
})
|
|
925
|
+
if (!maxW) return
|
|
926
|
+
if (changed) setCardSizes(next)
|
|
927
|
+
if (props.onCardSize) props.onCardSize(maxW, maxH)
|
|
928
|
+
})
|
|
929
|
+
// 2026-11-09 卡片拖拽:在世界坐标里移动;连线由 layout.positions 派生,移动即自动跟随。
|
|
930
|
+
var dragState = useState(null)
|
|
931
|
+
var dragKey = dragState[0]
|
|
932
|
+
var setDragKey = dragState[1]
|
|
933
|
+
var dragRef = useRef(null)
|
|
934
|
+
function cardPointerDown(node, pos, event) {
|
|
935
|
+
if (event.button !== 0) return
|
|
936
|
+
event.stopPropagation()
|
|
937
|
+
dragRef.current = { key: node.key, sx: event.clientX, sy: event.clientY, ox: pos[0], oy: pos[1], moved: false }
|
|
938
|
+
setDragKey(node.key)
|
|
939
|
+
try { event.currentTarget.setPointerCapture(event.pointerId) } catch (_) { /* ignore */ }
|
|
940
|
+
}
|
|
941
|
+
function cardPointerMove(node, event) {
|
|
942
|
+
var drag = dragRef.current
|
|
943
|
+
if (!drag || drag.key !== node.key) return
|
|
944
|
+
event.stopPropagation()
|
|
945
|
+
var scale = props.scale || 1
|
|
946
|
+
var dx = (event.clientX - drag.sx) / scale
|
|
947
|
+
var dy = (event.clientY - drag.sy) / scale
|
|
948
|
+
if (!drag.moved && (Math.abs(dx) + Math.abs(dy)) > 3) drag.moved = true
|
|
949
|
+
if (!drag.moved) return
|
|
950
|
+
if (String(node.key).indexOf('server-') === 0) { infraMove(node.key, [drag.ox + dx, drag.oy + dy]); return }
|
|
951
|
+
// 2026-11-09-v3:不再限制在画布边界内(用户反馈左侧有「空气墙」);
|
|
952
|
+
// 坐标允许任意值,.dm-world/.dm-lines 均 overflow:visible,配合拖到边缘自动平移可无限拖。
|
|
953
|
+
var nx = drag.ox + dx
|
|
954
|
+
var ny = drag.oy + dy
|
|
955
|
+
if (props.onNodeMove) props.onNodeMove(node.key, [nx, ny])
|
|
956
|
+
if (props.onDragEdge) props.onDragEdge(event.clientX, event.clientY)
|
|
957
|
+
}
|
|
958
|
+
function cardPointerUp(node, event) {
|
|
959
|
+
var drag = dragRef.current
|
|
960
|
+
if (!drag || drag.key !== node.key) return
|
|
961
|
+
event.stopPropagation()
|
|
962
|
+
var wasDrag = drag.moved
|
|
963
|
+
dragRef.current = null
|
|
964
|
+
setDragKey(null)
|
|
965
|
+
if (!wasDrag && props.onSelect) props.onSelect(node.key)
|
|
966
|
+
}
|
|
967
|
+
function cardPointerCancel() { dragRef.current = null; setDragKey(null) }
|
|
968
|
+
var peers = filterSelfPeers(dedupePeers(data.peers || []), data.nodeName)
|
|
969
|
+
var relayHosts = relayHostSet(peers)
|
|
970
|
+
var endpoint = data.endpoint || {}
|
|
971
|
+
var instances = (data.local && data.local.instances) || []
|
|
972
|
+
var hw = data.hardware || {}
|
|
973
|
+
var localTunnel = data.localTunnel || {}
|
|
974
|
+
var localUp = instances.filter(function (x) { return x.up }).length
|
|
975
|
+
var localTunnelText = localTunnel.port
|
|
976
|
+
? (localTunnel.frpRunning ? (localTunnel.frpCount ? localTunnel.frpCount : '') : '') || ''
|
|
977
|
+
: ''
|
|
978
|
+
|
|
979
|
+
function localDomain() {
|
|
980
|
+
var entry = localTunnel.publicEntry
|
|
981
|
+
var src = localTunnel.mainSource || (entry && entry.source === 'self-hosted-frps' ? 'self-hosted' : '')
|
|
982
|
+
if (entry && entry.host) {
|
|
983
|
+
var tag = src === 'self-hosted' ? '(自建 frps)' : (src === 'sakura' ? '(Sakura 备用)' : '')
|
|
984
|
+
return entry.host + ':' + entry.port + tag
|
|
985
|
+
}
|
|
986
|
+
if (localTunnel.frpRunning) return 'SakuraFrp 已开 · ' + (localTunnel.bind || '?') + ':' + localTunnel.port
|
|
987
|
+
if (localTunnel.port) return '未开穿透 · ' + (localTunnel.bind || '?') + ':' + localTunnel.port
|
|
988
|
+
return '未配置'
|
|
989
|
+
}
|
|
990
|
+
function peerDomain(peer) {
|
|
991
|
+
var t = peer.tunnel || {}
|
|
992
|
+
if (!t.host) return '—'
|
|
993
|
+
return t.host + (t.port ? ':' + t.port : '') + (t.kind === 'lan' ? '(内网)' : '')
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// LAN peer 单独收集(dedupe 后可能被更快的 frp 记录覆盖,但 LAN 线仍要画)
|
|
997
|
+
var lanMap = {}
|
|
998
|
+
filterSelfPeers(data.peers || [], data.nodeName).forEach(function (p) {
|
|
999
|
+
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 || '' }
|
|
1001
|
+
})
|
|
1002
|
+
var nodes = [{
|
|
1003
|
+
key: 'local',
|
|
1004
|
+
local: true,
|
|
1005
|
+
devKey: peerDeviceKey(data.nodeName),
|
|
1006
|
+
up: true,
|
|
1007
|
+
name: nodeDisplay(data.nodeName),
|
|
1008
|
+
dsh: localUp + '/' + instances.length,
|
|
1009
|
+
ports: instances.map(function (inst) { return { port: inst.port, up: inst.up } }),
|
|
1010
|
+
extra: ''
|
|
1011
|
+
}].concat(peers.map(function (peer) {
|
|
1012
|
+
var ls = deviceLinkState(data, peer, lanMap)
|
|
1013
|
+
return {
|
|
1014
|
+
key: 'peer:' + peer.name,
|
|
1015
|
+
local: false,
|
|
1016
|
+
devKey: ls.devKey,
|
|
1017
|
+
up: ls.anyUp,
|
|
1018
|
+
publicUp: ls.publicUp,
|
|
1019
|
+
sakuraUp: ls.sakuraUp,
|
|
1020
|
+
sakuraTunnel: ls.sakuraTunnel,
|
|
1021
|
+
lanUp: ls.lanUp,
|
|
1022
|
+
lanTunnel: ls.lanTunnel,
|
|
1023
|
+
name: nodeDisplay(ls.devKey),
|
|
1024
|
+
dsh: String(peer.instancesCount || 0),
|
|
1025
|
+
ports: peer.instancesList || [],
|
|
1026
|
+
latencyMs: peer.latencyMs,
|
|
1027
|
+
tunnel: peer.tunnel || {},
|
|
1028
|
+
error: peer.error || ''
|
|
1029
|
+
}
|
|
1030
|
+
}))
|
|
1031
|
+
// 2026-11-10:按共用顺序排序(与 Viewport 拖拽位置表一致,修复"拖 A 动 B"的 bug)
|
|
1032
|
+
var orderKeys = orderedNodeKeys(data)
|
|
1033
|
+
nodes.sort(function (a, b) { return orderKeys.indexOf(a.key) - orderKeys.indexOf(b.key) })
|
|
1034
|
+
// 基础设施节点(中转服务器):阿里云 / 樱花
|
|
1035
|
+
var sakuraAcc = sakuraAccountOf(data)
|
|
1036
|
+
var aliyunPorts = peers.map(function (p) { return isLanPeer(p) ? null : (p.tunnel || {}).port }).filter(Boolean)
|
|
1037
|
+
var infra = [
|
|
1038
|
+
{ key: 'server-aliyun', transport: 'public', name: '阿里云服务器 · ECS', host: 'relay.example.com', color: TRANSPORTS.public.color, detail: '隧道 ' + (aliyunPorts.length ? aliyunPorts.sort().join('/') : '—'), up: true },
|
|
1039
|
+
{ key: 'server-sakura', transport: 'sakura', name: '樱花穿透服务器', host: 'relay.example.com / relay.example.com / relay.example.com', color: TRANSPORTS.sakura.color, detail: sakuraAcc ? (sakuraAcc.online + '/' + sakuraAcc.total + ' 在线') : '未采集', up: Boolean(sakuraAcc) },
|
|
1040
|
+
]
|
|
1041
|
+
// 2026-11-09-v8:服务器默认位置贴住扇形最高一排设备(用户要求缩小服务器↔设备的垂直间隔)
|
|
1042
|
+
function infraDefaultPos(idx) {
|
|
1043
|
+
var dx = layout.cardW * 1.08
|
|
1044
|
+
var ox = layout.baseOffsetX || 0
|
|
1045
|
+
var oy = layout.baseOffsetY || 0
|
|
1046
|
+
var cx = ox + (layout.contentW || layout.W) / 2
|
|
1047
|
+
var topYAbs = oy + (layout.topDeviceY || 0)
|
|
1048
|
+
return [Math.round(cx + (idx === 0 ? -dx : dx)), Math.round(topYAbs - layout.cardH * 1.05)]
|
|
1049
|
+
}
|
|
1050
|
+
var infraState = useState(function () { try { return JSON.parse(window.localStorage.getItem('dsh-mesh-ui.infra.v2') || '{}') || {} } catch (e) { return {} } })
|
|
1051
|
+
var infraPos = infraState[0]
|
|
1052
|
+
var setInfraPos = infraState[1]
|
|
1053
|
+
function infraPosition(idx) { var k = infra[idx].key; var v = infraPos[k]; if (v && v.length === 2) return v; var def = DEFAULT_INFRA_POS[infra[idx].transport === 'public' ? 'aliyun' : 'sakura']; if (def) return [def[0], def[1]]; return infraDefaultPos(idx) }
|
|
1054
|
+
useEffect(function () {
|
|
1055
|
+
if (!props.resetNonce) return
|
|
1056
|
+
try { window.localStorage.removeItem('dsh-mesh-ui.infra.v2') } catch (e) { /* ignore */ }
|
|
1057
|
+
setInfraPos({})
|
|
1058
|
+
}, [props.resetNonce])
|
|
1059
|
+
function infraMove(key, pos) { setInfraPos(function (prev) { var next = Object.assign({}, prev); next[key] = pos; try { window.localStorage.setItem('dsh-mesh-ui.infra.v2', JSON.stringify(next)) } catch (e) { /* ignore */ } return next }) }
|
|
1060
|
+
|
|
1061
|
+
var GAP_EDGE = 6
|
|
1062
|
+
function edgePoint(from, to, hw, hh) {
|
|
1063
|
+
var dx = to[0] - from[0]
|
|
1064
|
+
var dy = to[1] - from[1]
|
|
1065
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1
|
|
1066
|
+
var ux = dx / len
|
|
1067
|
+
var uy = dy / len
|
|
1068
|
+
var tx = ux === 0 ? Infinity : (hw + GAP_EDGE) / Math.abs(ux)
|
|
1069
|
+
var ty = uy === 0 ? Infinity : (hh + GAP_EDGE) / Math.abs(uy)
|
|
1070
|
+
var t = Math.min(tx, ty)
|
|
1071
|
+
return [from[0] + ux * t, from[1] + uy * t]
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// 2026-11-09:三通道连线(设备→阿里云 / 设备→樱花 / 局域网直连)+ 中转服务器卡片 + 筛选
|
|
1075
|
+
var show = props.show || { lan: true, public: true, sakura: true }
|
|
1076
|
+
// 2026-11-09:悬浮节点 → 只高亮它的连线并显示完整地址(其它线变淡)
|
|
1077
|
+
var hoverState = useState(null)
|
|
1078
|
+
var hoverKey = hoverState[0]
|
|
1079
|
+
var setHoverKey = hoverState[1]
|
|
1080
|
+
function edgeRelated(aKey, bKey) { return hoverKey ? (hoverKey === aKey || hoverKey === bKey) : true }
|
|
1081
|
+
var infraRefs = useRef({})
|
|
1082
|
+
var infraSizeState = useState({})
|
|
1083
|
+
var infraSizes = infraSizeState[0]
|
|
1084
|
+
var setInfraSizes = infraSizeState[1]
|
|
1085
|
+
useEffect(function () {
|
|
1086
|
+
var next = {}
|
|
1087
|
+
var changed = false
|
|
1088
|
+
Object.keys(infraRefs.current).forEach(function (key) {
|
|
1089
|
+
var el = infraRefs.current[key]
|
|
1090
|
+
if (!el) return
|
|
1091
|
+
var w = el.offsetWidth
|
|
1092
|
+
var h2 = el.offsetHeight
|
|
1093
|
+
if (!w || !h2) return
|
|
1094
|
+
next[key] = { w: w, h: h2 }
|
|
1095
|
+
var old = infraSizes[key]
|
|
1096
|
+
if (!old || Math.abs(old.w - w) > 1 || Math.abs(old.h - h2) > 1) changed = true
|
|
1097
|
+
})
|
|
1098
|
+
if (!Object.keys(next).length) return
|
|
1099
|
+
if (changed) setInfraSizes(next)
|
|
1100
|
+
})
|
|
1101
|
+
function sizeOf(key) { return cardSizes[key] || infraSizes[key] || { w: layout.cardW, h: layout.cardH } }
|
|
1102
|
+
function curve(key, from, to, up, transport, label, fromSize, toSize, highlight) {
|
|
1103
|
+
var a = edgePoint(from, to, fromSize.w / 2, fromSize.h / 2)
|
|
1104
|
+
var b = edgePoint(to, from, toSize.w / 2, toSize.h / 2)
|
|
1105
|
+
var mx = (a[0] + b[0]) / 2
|
|
1106
|
+
var my = (a[1] + b[1]) / 2
|
|
1107
|
+
var dx = b[0] - a[0]
|
|
1108
|
+
var dy = b[1] - a[1]
|
|
1109
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1
|
|
1110
|
+
var bow = Math.min(12, len * 0.07)
|
|
1111
|
+
var color = up ? transport.color : DOWN_COLOR
|
|
1112
|
+
var dash = up ? (transport.dash ? '6 5' : 'none') : '6 5'
|
|
1113
|
+
var related = highlight !== false
|
|
1114
|
+
var els = [h('path', {
|
|
1115
|
+
key: key,
|
|
1116
|
+
d: 'M ' + a[0] + ' ' + a[1] + ' Q ' + (mx + (-dy / len) * bow) + ' ' + (my + (dx / len) * bow) + ' ' + b[0] + ' ' + b[1],
|
|
1117
|
+
fill: 'none', stroke: color, strokeWidth: 1.8, strokeDasharray: dash,
|
|
1118
|
+
className: up ? (transport.dash ? 'dm-edge-frp' : 'dm-edge-solid') : 'dm-edge-down',
|
|
1119
|
+
opacity: related ? 1 : 0.12
|
|
1120
|
+
})]
|
|
1121
|
+
if (label) {
|
|
1122
|
+
els.push(h('text', { key: key + '-t', className: 'dm-edge-label', x: mx + (-dy / len) * bow, y: my + (dx / len) * bow - 4, fill: color, fontSize: 9, textAnchor: 'middle', style: { display: (hoverKey && related) ? 'block' : 'none' } }, label))
|
|
1123
|
+
}
|
|
1124
|
+
return els
|
|
1125
|
+
}
|
|
1126
|
+
var svgEls = []
|
|
1127
|
+
var aliyunPos = infraPosition(0)
|
|
1128
|
+
var sakuraPos = infraPosition(1)
|
|
1129
|
+
var aliyunSize = infraSizes['server-aliyun'] || { w: 300, h: 90 }
|
|
1130
|
+
var sakuraSize = infraSizes['server-sakura'] || { w: 300, h: 90 }
|
|
1131
|
+
var infraBoxes = []
|
|
1132
|
+
if (show.public) infraBoxes.push({ x: aliyunPos[0], y: aliyunPos[1], w: aliyunSize.w, h: aliyunSize.h })
|
|
1133
|
+
if (show.sakura) infraBoxes.push({ x: sakuraPos[0], y: sakuraPos[1], w: sakuraSize.w, h: sakuraSize.h })
|
|
1134
|
+
if (props.onInfraBounds) props.onInfraBounds(infraBoxes)
|
|
1135
|
+
var localKey = peerDeviceKey(data.nodeName)
|
|
1136
|
+
var localPub = localTunnel.publicEntry || localTunnel.selfHosted || null
|
|
1137
|
+
var localSak = sakuraTunnelOf(data, localKey)
|
|
1138
|
+
// 2026-11-10:LAN 线必须从本机节点出发(排序后 local 可能不在 index 0,之前误连到peer-x)
|
|
1139
|
+
var localNodeIdx = 0
|
|
1140
|
+
nodes.forEach(function (n, ii) { if (n.local) localNodeIdx = ii })
|
|
1141
|
+
nodes.forEach(function (n, i) {
|
|
1142
|
+
var pa = layout.positions[i]
|
|
1143
|
+
if (n.local) {
|
|
1144
|
+
// 主机自己的两条上级链路:自建 frps(阿里云) + 本机樱花隧道
|
|
1145
|
+
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')))
|
|
1146
|
+
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')))
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
var sizeN = sizeOf(n.key)
|
|
1150
|
+
if (show.public) svgEls = svgEls.concat(curve('pub' + i, pa, aliyunPos, n.publicUp, TRANSPORTS.public, 'relay.example.com' + ((n.tunnel && n.tunnel.port) ? ':' + n.tunnel.port : ''), sizeN, aliyunSize, edgeRelated(n.key, 'server-aliyun')))
|
|
1151
|
+
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')))
|
|
1152
|
+
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))) }
|
|
1153
|
+
})
|
|
1154
|
+
return h('div', null,
|
|
1155
|
+
h('svg', { className: 'dm-lines', width: layout.W, height: layout.H, viewBox: '0 0 ' + layout.W + ' ' + layout.H }, svgEls),
|
|
1156
|
+
nodes.map(function (node, i) {
|
|
1157
|
+
var pos = layout.positions[i]
|
|
1158
|
+
var selected = props.selectedKey === node.key
|
|
1159
|
+
var types = dshTypesFor(node.devKey)
|
|
1160
|
+
return h('div', {
|
|
1161
|
+
key: node.key,
|
|
1162
|
+
className: 'dm-card-node compact draggable' + (node.local ? ' local' : '') + (node.up ? '' : ' down') + (selected ? ' selected' : '') + (hoverKey === node.key ? ' hovered' : '') + (dragKey === node.key ? ' dragging' : ''),
|
|
1163
|
+
onMouseEnter: function () { setHoverKey(node.key) },
|
|
1164
|
+
onMouseLeave: function () { setHoverKey(null) },
|
|
1165
|
+
style: { left: pos[0] + 'px', top: pos[1] + 'px', maxWidth: '380px', transform: 'translate(-50%,-50%)' },
|
|
1166
|
+
ref: function (el) { if (el) cardRefs.current[node.key] = el; else delete cardRefs.current[node.key] },
|
|
1167
|
+
onPointerDown: function (event) { cardPointerDown(node, pos, event) },
|
|
1168
|
+
onPointerMove: function (event) { cardPointerMove(node, event) },
|
|
1169
|
+
onPointerUp: function (event) { cardPointerUp(node, event) },
|
|
1170
|
+
onPointerCancel: cardPointerCancel,
|
|
1171
|
+
title: '拖拽调整位置 · 单击查看 ' + node.name + ' 的详情'
|
|
1172
|
+
},
|
|
1173
|
+
h('div', { className: 'dm-n-head' },
|
|
1174
|
+
h('div', { className: 'dm-n-name' }, h('span', { className: 'dm-dot ' + (node.up ? 'ok' : 'bad') }), h('span', { className: 'dm-ell' }, node.name)),
|
|
1175
|
+
node.local ? h('span', { className: 'dm-chip ok' }, '本机')
|
|
1176
|
+
: (node.up ? h('span', { className: 'dm-extra' }, node.latencyMs == null ? '' : node.latencyMs + 'ms') : h('span', { className: 'dm-chip bad' }, '离线'))
|
|
1177
|
+
),
|
|
1178
|
+
node.up ? null : h('div', { className: 'dm-offline-row' }, '全部链路不可用' + (node.error ? ' · ' + node.error : '')),
|
|
1179
|
+
h('div', { className: 'dm-card-dsh' },
|
|
1180
|
+
types.map(function (tp, ti) { return h('span', { key: 't' + ti, className: 'dm-chip type' }, tp) }),
|
|
1181
|
+
(node.ports || []).map(function (item, pi) {
|
|
1182
|
+
return h('span', { key: pi, className: 'dm-chip' + (item.up ? ' run' : ' bad'), title: (item.up ? '在线' : '离线') }, String(item.port))
|
|
1183
|
+
})
|
|
1184
|
+
)
|
|
1185
|
+
)
|
|
1186
|
+
}),
|
|
1187
|
+
(show.public ? h('div', {
|
|
1188
|
+
key: 'server-aliyun',
|
|
1189
|
+
className: 'dm-card-node server' + (infra[0].up ? '' : ' down') + (hoverKey === 'server-aliyun' ? ' hovered' : ''),
|
|
1190
|
+
onMouseEnter: function () { setHoverKey('server-aliyun') },
|
|
1191
|
+
onMouseLeave: function () { setHoverKey(null) },
|
|
1192
|
+
style: { left: aliyunPos[0] + 'px', top: aliyunPos[1] + 'px', maxWidth: '360px', transform: 'translate(-50%,-50%)', borderColor: TRANSPORTS.public.color },
|
|
1193
|
+
ref: function (el) { if (el) infraRefs.current['server-aliyun'] = el; else delete infraRefs.current['server-aliyun'] },
|
|
1194
|
+
onPointerDown: function (event) { cardPointerDown({ key: 'server-aliyun' }, aliyunPos, event) },
|
|
1195
|
+
onPointerMove: function (event) { cardPointerMove({ key: 'server-aliyun' }, event) },
|
|
1196
|
+
onPointerUp: function (event) { cardPointerUp({ key: 'server-aliyun' }, event) },
|
|
1197
|
+
onPointerCancel: cardPointerCancel
|
|
1198
|
+
},
|
|
1199
|
+
h('div', { className: 'dm-n-head' }, h('div', { className: 'dm-n-name' }, h('span', { className: 'dm-dot ok' }), h('span', {}, infra[0].name))),
|
|
1200
|
+
h('div', { className: 'dm-card-domain' }, infra[0].host),
|
|
1201
|
+
h('div', { className: 'dm-card-dsh' }, h('span', { className: 'dm-chip', style: { borderColor: TRANSPORTS.public.color, color: TRANSPORTS.public.color } }, 'frps'), h('span', { className: 'dm-chip' }, infra[0].detail))
|
|
1202
|
+
) : null),
|
|
1203
|
+
(show.sakura ? h('div', {
|
|
1204
|
+
key: 'server-sakura',
|
|
1205
|
+
className: 'dm-card-node server' + (infra[1].up ? '' : ' down') + (hoverKey === 'server-sakura' ? ' hovered' : ''),
|
|
1206
|
+
onMouseEnter: function () { setHoverKey('server-sakura') },
|
|
1207
|
+
onMouseLeave: function () { setHoverKey(null) },
|
|
1208
|
+
style: { left: sakuraPos[0] + 'px', top: sakuraPos[1] + 'px', maxWidth: '360px', transform: 'translate(-50%,-50%)', borderColor: TRANSPORTS.sakura.color },
|
|
1209
|
+
ref: function (el) { if (el) infraRefs.current['server-sakura'] = el; else delete infraRefs.current['server-sakura'] },
|
|
1210
|
+
onPointerDown: function (event) { cardPointerDown({ key: 'server-sakura' }, sakuraPos, event) },
|
|
1211
|
+
onPointerMove: function (event) { cardPointerMove({ key: 'server-sakura' }, event) },
|
|
1212
|
+
onPointerUp: function (event) { cardPointerUp({ key: 'server-sakura' }, event) },
|
|
1213
|
+
onPointerCancel: cardPointerCancel
|
|
1214
|
+
},
|
|
1215
|
+
h('div', { className: 'dm-n-head' }, h('div', { className: 'dm-n-name' }, h('span', { className: 'dm-dot ' + (infra[1].up ? 'ok' : 'bad') }), h('span', {}, infra[1].name))),
|
|
1216
|
+
h('div', { className: 'dm-card-domain' }, infra[1].host),
|
|
1217
|
+
h('div', { className: 'dm-card-dsh' }, h('span', { className: 'dm-chip', style: { borderColor: TRANSPORTS.sakura.color, color: TRANSPORTS.sakura.color } }, 'Sakura'), h('span', { className: 'dm-chip' }, infra[1].detail))
|
|
1218
|
+
) : null)
|
|
1219
|
+
)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// ---------- 画布下方:设备详情(默认全展示,点卡片高亮对应项) ----------
|
|
1223
|
+
function DeviceDetails(props) {
|
|
1224
|
+
var data = props.data
|
|
1225
|
+
var peers = filterSelfPeers(dedupePeers(data.peers || []), data.nodeName)
|
|
1226
|
+
var relayHosts = relayHostSet(peers)
|
|
1227
|
+
var hw = data.hardware || {}
|
|
1228
|
+
var instances = (data.local && data.local.instances) || []
|
|
1229
|
+
var endpoint = data.endpoint || {}
|
|
1230
|
+
var localTunnel = data.localTunnel || {}
|
|
1231
|
+
var localUp = instances.filter(function (x) { return x.up }).length
|
|
1232
|
+
var sessions = instances.reduce(function (n, x) { return n + (x.sessions || 0) }, 0)
|
|
1233
|
+
var running = instances.reduce(function (n, x) { return n + (x.running || 0) }, 0)
|
|
1234
|
+
var model = instances.length && instances[0].model ? (instances[0].model.provider || '') + ' / ' + (instances[0].model.model || '') : '—'
|
|
1235
|
+
var localTunnelText = null
|
|
1236
|
+
var tunnels = localTunnel.tunnels || []
|
|
1237
|
+
var entry = localTunnel.publicEntry || null
|
|
1238
|
+
var tunnelNames = tunnels.map(function (x) { return x.name + '→' + x.host + ':' + x.port }).join(' / ')
|
|
1239
|
+
var mainSrc = localTunnel.mainSource || (entry && entry.source === 'self-hosted-frps' ? 'self-hosted' : (entry ? 'sakura' : 'none'))
|
|
1240
|
+
var entryText = entry && entry.host
|
|
1241
|
+
? entry.host + ':' + entry.port + (mainSrc === 'self-hosted' ? '(自建 frps · 主用)' : (mainSrc === 'sakura' ? '(Sakura · 备用)' : ''))
|
|
1242
|
+
: '未配置'
|
|
1243
|
+
var localTunnelRows = [
|
|
1244
|
+
['入口', entryText],
|
|
1245
|
+
['监听 / frpc', (localTunnel.bind || '?') + ':' + (localTunnel.port || '?') + (localTunnel.tls ? ' (TLS)' : '') + ' · ' + (localTunnel.frpRunning ? 'frpc ×' + (localTunnel.frpcCount || 1) : 'frpc 未运行')]
|
|
1246
|
+
]
|
|
1247
|
+
if (tunnels.length) localTunnelRows.push(['Sakura 备用', tunnels.map(function (x) { return x.host + ':' + x.port }).join(' / ')])
|
|
1248
|
+
if (mainSrc === 'self-hosted' && localTunnel.selfHosted && localTunnel.selfHosted.configPath) localTunnelRows.push(['frpc 配置', localTunnel.selfHosted.configPath])
|
|
1249
|
+
var localTunnelAll = tunnelNames
|
|
1250
|
+
var memText = hw.memTotalGB ? ((hw.memFreeGB != null ? hw.memFreeGB : '?') + ' / ' + hw.memTotalGB + ' GB') : '—'
|
|
1251
|
+
var cpuText = hw.cpuModel ? hw.cpuModel + ' · ' + (hw.cpuCores || '?') + ' 线程' : '—'
|
|
1252
|
+
|
|
1253
|
+
var devices = [{
|
|
1254
|
+
key: 'local',
|
|
1255
|
+
name: nodeDisplay(data.nodeName) + '(本机)',
|
|
1256
|
+
up: Boolean(endpoint.up),
|
|
1257
|
+
rows: [
|
|
1258
|
+
['操作系统', hw.os || '—'],
|
|
1259
|
+
['CPU', cpuText],
|
|
1260
|
+
['内存', memText],
|
|
1261
|
+
['DSH 实例', localUp + '/' + instances.length + ' 在线'],
|
|
1262
|
+
['会话 / 运行', sessions + ' / ' + running],
|
|
1263
|
+
['模型', model],
|
|
1264
|
+
...localTunnelRows,
|
|
1265
|
+
['端点', (endpoint.pid != null ? 'pid ' + endpoint.pid : '—') + ' · tools ' + (endpoint.tools != null ? endpoint.tools : '—') + ' · mesh ' + (data.meshVersion || '?')],
|
|
1266
|
+
['检测时间', clockText(data.at)]
|
|
1267
|
+
]
|
|
1268
|
+
}].concat(peers.map(function (peer) {
|
|
1269
|
+
var t = peer.tunnel || {}
|
|
1270
|
+
return {
|
|
1271
|
+
key: 'peer:' + peer.name,
|
|
1272
|
+
name: nodeDisplay(peerDeviceKey(peer.name)),
|
|
1273
|
+
up: Boolean(peer.up),
|
|
1274
|
+
rows: [
|
|
1275
|
+
['操作系统', peer.os || '—'],
|
|
1276
|
+
['运行时长', uptimeText(peer.uptimeSec)],
|
|
1277
|
+
['DSH 实例', (peer.instancesCount || 0) + ' 个(' + ((peer.instancesList || []).map(function (x) { return x.port + (x.up ? '✓' : '✗') }).join(' ') || '—') + ')'],
|
|
1278
|
+
['会话 / 运行', (peer.sessions || 0) + ' / ' + (peer.running || 0)],
|
|
1279
|
+
['任务(进行中)', peer.tasksActive == null ? '—' : String(peer.tasksActive)],
|
|
1280
|
+
['入口', t.host ? t.host + (t.port ? ':' + t.port : '') : '—'],
|
|
1281
|
+
['链路', commLabel(peer, relayHosts).text + ' · ' + (peer.up ? (peer.latencyMs == null ? '—' : peer.latencyMs + 'ms') : '离线') + ' · ' + (peer.tls ? 'TLS ✓' : 'TLS ✗') + ' · v' + (peer.version || '?')],
|
|
1282
|
+
['节点 pid / tools', (peer.nodePid == null ? '—' : peer.nodePid) + ' / ' + (peer.nodeTools == null ? '—' : peer.nodeTools)],
|
|
1283
|
+
['检测时间', clockText(peer.detectedAt || data.at)]
|
|
1284
|
+
]
|
|
1285
|
+
}
|
|
1286
|
+
}))
|
|
1287
|
+
|
|
1288
|
+
return h('div', { className: 'dm-devices' },
|
|
1289
|
+
devices.map(function (device) {
|
|
1290
|
+
return h('div', { key: device.key, className: 'dm-device' + (props.selectedKey === device.key ? ' selected' : '') },
|
|
1291
|
+
h('div', { className: 'dm-device-head' },
|
|
1292
|
+
h('span', { className: 'dm-dot ' + (device.up ? 'ok' : 'bad') }),
|
|
1293
|
+
h('span', { className: 'dm-device-name' }, device.name),
|
|
1294
|
+
device.key === 'local' ? h('span', { className: 'dm-chip ok' }, '本机') : null
|
|
1295
|
+
),
|
|
1296
|
+
h('div', { className: 'dm-device-body' }, device.rows.map(function (row, i) {
|
|
1297
|
+
return h('div', { className: 'dm-device-row', key: i },
|
|
1298
|
+
h('span', { className: 'dm-device-k' }, row[0]),
|
|
1299
|
+
h('span', { className: 'dm-device-v', title: String(row[1]) }, String(row[1]))
|
|
1300
|
+
)
|
|
1301
|
+
}))
|
|
1302
|
+
)
|
|
1303
|
+
})
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function Details(props) {
|
|
1308
|
+
return h('details', { className: 'dm-details', open: Boolean(props.open) },
|
|
1309
|
+
h('summary', null, props.title, props.badge != null ? h('span', { className: 'dm-pill muted' }, String(props.badge)) : null),
|
|
1310
|
+
h('div', { className: 'dm-body' }, props.children)
|
|
1311
|
+
)
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function InstanceList(props) {
|
|
1315
|
+
var instances = (props.data.local && props.data.local.instances) || []
|
|
1316
|
+
if (!instances.length) return h('div', { className: 'dm-empty' }, '无本机实例数据')
|
|
1317
|
+
return h('div', null, instances.map(function (inst) {
|
|
1318
|
+
var model = inst.model ? (inst.model.provider || '') + ' / ' + (inst.model.model || '') : '—'
|
|
1319
|
+
return h('div', { className: 'dm-instrow', key: inst.id },
|
|
1320
|
+
h('div', { style: { minWidth: 0 } },
|
|
1321
|
+
h('div', { style: { fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6 } }, h('span', { className: 'dm-dot ' + (inst.up ? 'ok' : 'bad') }), inst.id),
|
|
1322
|
+
h('div', { className: 'dm-sub', style: { marginTop: 1 } }, model)
|
|
1323
|
+
),
|
|
1324
|
+
h('div', { className: 'dm-mini', style: { marginTop: 0 } },
|
|
1325
|
+
h('span', { className: 'dm-chip' }, 'pid ' + (inst.pid == null ? '—' : inst.pid)),
|
|
1326
|
+
h('span', { className: 'dm-chip' }, 'tools ' + (inst.tools == null ? '—' : inst.tools)),
|
|
1327
|
+
h('span', { className: 'dm-chip' }, '会话 ' + (inst.sessions || 0)),
|
|
1328
|
+
h('span', { className: 'dm-chip' + (inst.running ? ' run' : '') }, '运行 ' + (inst.running || 0))
|
|
1329
|
+
)
|
|
1330
|
+
)
|
|
1331
|
+
}))
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function DelegationTable(props) {
|
|
1335
|
+
var d = props.data.delegations || {}
|
|
1336
|
+
var list = (d.recent || []).slice(0, 6)
|
|
1337
|
+
var stats = d.byState || {}
|
|
1338
|
+
if (!list.length) return h('div', { className: 'dm-empty' }, '暂无委托记录')
|
|
1339
|
+
return h('div', null,
|
|
1340
|
+
h('div', { className: 'dm-sub', style: { marginBottom: 5 } }, '共 ' + (d.total || 0) + ' 条 · ' + Object.keys(stats).map(function (k) { return k + ' ' + stats[k] }).join(' · ')),
|
|
1341
|
+
h('table', { className: 'dm-table' },
|
|
1342
|
+
h('thead', null, h('tr', null, h('th', null, '状态'), h('th', null, '节点'), h('th', null, '任务'), h('th', null, '耗时'), h('th', null, '目标'))),
|
|
1343
|
+
h('tbody', null, list.map(function (task) {
|
|
1344
|
+
return h('tr', { key: task.taskId },
|
|
1345
|
+
h('td', null, stateChip(task.state)),
|
|
1346
|
+
h('td', null, nodeLabel(task.node)),
|
|
1347
|
+
h('td', { className: 'dm-mono', style: { fontSize: 11 } }, String(task.taskId || '').replace(/^T-\d{8}-/, 'T-…')),
|
|
1348
|
+
h('td', { className: 'dm-mono' }, dur(task.durationSec)),
|
|
1349
|
+
h('td', null, h('span', { className: 'dm-trunc', title: task.goal }, task.goal || ''))
|
|
1350
|
+
)
|
|
1351
|
+
}))
|
|
1352
|
+
)
|
|
1353
|
+
)
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function AlertList(props) {
|
|
1357
|
+
var alerts = props.data.alerts || {}
|
|
1358
|
+
var latest = alerts.latest
|
|
1359
|
+
var history = (alerts.history || []).filter(function (item) { return !latest || item.at !== latest.at }).slice().reverse()
|
|
1360
|
+
if (!latest && !history.length) return h('div', { className: 'dm-empty' }, '没有告警记录')
|
|
1361
|
+
return h('div', null,
|
|
1362
|
+
latest ? h('div', { className: 'dm-alertrow' },
|
|
1363
|
+
h('span', { className: 'dm-chip bad' }, latest.kind || 'alert'),
|
|
1364
|
+
h('span', null, h('span', { className: 'dm-mono' }, latest.at), ' · ', latest.summary || '', latest.consecutiveBad ? ' · 连续 ' + latest.consecutiveBad + ' 次' : '')
|
|
1365
|
+
) : null,
|
|
1366
|
+
history.slice(0, 6).map(function (item, i) {
|
|
1367
|
+
return h('div', { className: 'dm-alertrow', key: i, style: { opacity: 0.75 } },
|
|
1368
|
+
h('span', { className: 'dm-chip' }, item.kind || '?'),
|
|
1369
|
+
h('span', null, h('span', { className: 'dm-mono' }, item.at || ''), ' · ', item.summary || '')
|
|
1370
|
+
)
|
|
1371
|
+
})
|
|
1372
|
+
)
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function MeshStatusSection() {
|
|
1376
|
+
var s0 = useState(null); var data = s0[0]; var setData = s0[1]
|
|
1377
|
+
var s1 = useState(true); var loading = s1[0]; var setLoading = s1[1]
|
|
1378
|
+
var s2 = useState(''); var error = s2[0]; var setError = s2[1]
|
|
1379
|
+
var s3 = useState(0); var tick = s3[0]; var setTick = s3[1]
|
|
1380
|
+
var s4 = useState(''); var copied = s4[0]; var setCopied = s4[1]
|
|
1381
|
+
var s5 = useState('local'); var selectedKey = s5[0]; var setSelectedKey = s5[1]
|
|
1382
|
+
|
|
1383
|
+
function load(force) {
|
|
1384
|
+
setLoading(true)
|
|
1385
|
+
fetchStatus(Boolean(force))
|
|
1386
|
+
.then(function (json) {
|
|
1387
|
+
setData(json)
|
|
1388
|
+
setError(json && json.ok === false && !json.warming ? json.error || 'mesh-health error' : '')
|
|
1389
|
+
})
|
|
1390
|
+
.catch(function (err) { setError(String((err && err.message) || err)) })
|
|
1391
|
+
.finally(function () { setLoading(false) })
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
useEffect(function () {
|
|
1395
|
+
load(false)
|
|
1396
|
+
var timer = setInterval(function () { setTick(function (n) { return n + 1 }) }, REFRESH_MS)
|
|
1397
|
+
return function () { clearInterval(timer) }
|
|
1398
|
+
}, [])
|
|
1399
|
+
|
|
1400
|
+
useEffect(function () { if (tick > 0) load(false) }, [tick])
|
|
1401
|
+
|
|
1402
|
+
function copyJson() {
|
|
1403
|
+
var text = JSON.stringify(data || {}, null, 2)
|
|
1404
|
+
var done = function () { setCopied('已复制'); setTimeout(function () { setCopied('') }, 1600) }
|
|
1405
|
+
try {
|
|
1406
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, done)
|
|
1407
|
+
else done()
|
|
1408
|
+
} catch (_) { done() }
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
if (!data) {
|
|
1412
|
+
return h('div', { className: 'dm-root' },
|
|
1413
|
+
h('div', { className: 'dm-head' }, h('span', { className: 'dm-title' }, 'AI 网络'), h('span', { className: 'dm-pill muted' }, loading ? '加载中…' : '无数据')),
|
|
1414
|
+
error ? h('div', { className: 'dm-err' }, error) : null
|
|
1415
|
+
)
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
var versions = data.versionMismatch || []
|
|
1419
|
+
var issues = data.issues || []
|
|
1420
|
+
var hints = data.hints || []
|
|
1421
|
+
|
|
1422
|
+
return h('div', { className: 'dm-root' },
|
|
1423
|
+
h('style', null, CSS),
|
|
1424
|
+
h('div', { className: 'dm-head' },
|
|
1425
|
+
h('span', { className: 'dm-title' }, 'AI 网络 · dsh-mesh'),
|
|
1426
|
+
h('span', { className: 'dm-pill ' + (data.warming ? 'muted' : tone(data.verdict)) }, data.warming ? '预热中' : (data.verdict || '?')),
|
|
1427
|
+
h('span', { className: 'dm-sub' }, (data.meshVersion ? 'mesh ' + data.meshVersion + ' · ' : '') + (data.at ? ago(data.at) : '')),
|
|
1428
|
+
h('span', { style: { flex: 1 } }),
|
|
1429
|
+
h('a', { className: 'dm-btn', href: API, target: '_blank', rel: 'noreferrer', style: { textDecoration: 'none' } }, '原始 JSON'),
|
|
1430
|
+
h('button', { className: 'dm-btn', onClick: copyJson }, copied || '复制诊断'),
|
|
1431
|
+
h('button', { className: 'dm-btn primary', onClick: function () { load(true) } }, loading ? '刷新中…' : '刷新')
|
|
1432
|
+
),
|
|
1433
|
+
error ? h('div', { className: 'dm-err' }, error) : null,
|
|
1434
|
+
|
|
1435
|
+
h(Viewport, { data: data, layout: computeLayout(1 + filterSelfPeers(dedupePeers(data.peers || []), data.nodeName).length), selectedKey: selectedKey, onSelect: setSelectedKey }),
|
|
1436
|
+
h(InfraPanels, { data: data }),
|
|
1437
|
+
h(DeviceDetails, { data: data, selectedKey: selectedKey }),
|
|
1438
|
+
h(Details, { title: '本机实例', badge: ((data.local && data.local.instances) || []).length, open: true }, h(InstanceList, { data: data })),
|
|
1439
|
+
h(Details, { title: '版本一致性', badge: versions.length ? versions.length + ' 处升级' : '一致' },
|
|
1440
|
+
versions.length
|
|
1441
|
+
? versions.map(function (item, i) { return h('div', { key: i, className: 'dm-sub', style: { marginTop: 4 } }, '· ' + (typeof item === 'string' ? item : JSON.stringify(item))) })
|
|
1442
|
+
: h('div', { className: 'dm-empty' }, '所有节点/实例版本一致')
|
|
1443
|
+
),
|
|
1444
|
+
h(Details, { title: '委托台账', badge: (data.delegations && data.delegations.total) || 0 }, h(DelegationTable, { data: data })),
|
|
1445
|
+
h(Details, { title: '告警历史', badge: ((data.alerts && data.alerts.history) || []).length }, h(AlertList, { data: data })),
|
|
1446
|
+
(issues.length || hints.length) ? h(Details, { title: '诊断建议', badge: issues.length + hints.length },
|
|
1447
|
+
h('div', null,
|
|
1448
|
+
issues.slice(0, 6).map(function (issue, i) { return h('div', { key: 'i' + i, className: 'dm-err', style: { marginTop: 3 } }, '· ' + issue) }),
|
|
1449
|
+
hints.slice(0, 6).map(function (hint, i) { return h('div', { key: 'h' + i, className: 'dm-sub', style: { marginTop: 3 } }, '· ' + hint) })
|
|
1450
|
+
)
|
|
1451
|
+
) : null,
|
|
1452
|
+
h('div', { className: 'dm-foot' }, '数据源:mesh-health / host status / peer host_self / 委托台账 / 告警历史 · ' + (data.repo || '') + ' · node ' + (data.nodeVersion || '') + ' · ' + REFRESH_MS / 1000 + 's 自动刷新(只读)')
|
|
1453
|
+
)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// ================= 委托面板(只读,方案 A / v2 视觉重做) =================
|
|
1457
|
+
var DG_API_LIST = '/api/dsh-mesh/delegations'
|
|
1458
|
+
var DG_API_TASK = '/api/dsh-mesh/task'
|
|
1459
|
+
var DG_API_SESSION = '/api/dsh-mesh/session'
|
|
1460
|
+
|
|
1461
|
+
var DG_STATE_TONE = {
|
|
1462
|
+
done: 'ok', working: 'run', accepted: 'run', submitted: 'run',
|
|
1463
|
+
question: 'warn', stalled: 'warn', failed: 'bad', cancelled: 'muted'
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
var DG_CSS = `
|
|
1467
|
+
.dm-dg-root { display:flex; flex-direction:column; gap:12px; height:100%; min-height:0; padding:16px; box-sizing:border-box; font-size:13px; color:inherit; }
|
|
1468
|
+
.dm-dg-head { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
|
1469
|
+
.dm-dg-title { font-size:16px; font-weight:650; letter-spacing:.2px; }
|
|
1470
|
+
.dm-dg-sub { font-size:11.5px; opacity:.55; }
|
|
1471
|
+
.dm-dg-pill { font-size:10.5px; padding:1px 8px; border-radius:999px; border:1px solid rgba(128,128,128,.32); opacity:.85; }
|
|
1472
|
+
.dm-dg-pill.readonly { border-color:rgba(88,166,255,.5); color:#3b82d6; opacity:1; }
|
|
1473
|
+
.dm-dg-stats { display:flex; gap:6px; flex-wrap:wrap; }
|
|
1474
|
+
.dm-dg-stat { font-size:10.5px; padding:1px 8px; border-radius:999px; border:1px solid rgba(128,128,128,.28); display:flex; gap:5px; align-items:center; }
|
|
1475
|
+
.dm-dg-stat b { font-weight:650; }
|
|
1476
|
+
.dm-dg-spacer { flex:1; }
|
|
1477
|
+
.dm-dg-btn { font-size:11.5px; padding:4px 11px; border-radius:8px; border:1px solid rgba(128,128,128,.4); background:transparent; color:inherit; cursor:pointer; white-space:nowrap; display:inline-flex; align-items:center; gap:5px; }
|
|
1478
|
+
.dm-dg-btn:hover:not(:disabled) { background:rgba(128,128,128,.12); }
|
|
1479
|
+
.dm-dg-btn:disabled { opacity:.45; cursor:default; }
|
|
1480
|
+
.dm-dg-btn.primary { border-color:rgba(88,166,255,.6); color:#3b82d6; }
|
|
1481
|
+
.dm-dg-notice { font-size:11.5px; padding:7px 10px; border-radius:8px; border:1px solid transparent; display:flex; gap:8px; align-items:center; }
|
|
1482
|
+
.dm-dg-notice.info { background:rgba(88,166,255,.1); border-color:rgba(88,166,255,.35); }
|
|
1483
|
+
.dm-dg-notice.ok { background:rgba(63,185,80,.12); border-color:rgba(63,185,80,.4); color:#2e9e4f; }
|
|
1484
|
+
.dm-dg-notice.bad { background:rgba(248,81,73,.12); border-color:rgba(248,81,73,.4); color:#c9362c; }
|
|
1485
|
+
.dm-dg-body { display:grid; grid-template-columns:var(--dm-dg-list-w, 360px) 7px minmax(0,1fr); gap:10px; min-height:0; flex:1; }
|
|
1486
|
+
@media (max-width: 960px) { .dm-dg-body { grid-template-columns:1fr; } .dm-dg-divider { display:none; } }
|
|
1487
|
+
.dm-dg-pane { display:flex; flex-direction:column; min-height:0; border:1px solid rgba(128,128,128,.25); border-radius:12px; background:rgba(128,128,128,.035); overflow:hidden; }
|
|
1488
|
+
.dm-dg-pane-head { flex:none; display:flex; align-items:center; gap:8px; padding:9px 12px; border-bottom:1px solid rgba(128,128,128,.2); background:rgba(128,128,128,.05); font-weight:650; font-size:12px; }
|
|
1489
|
+
.dm-dg-pane-head .dm-dg-sub { font-weight:400; }
|
|
1490
|
+
.dm-dg-scroll { flex:1; min-height:0; overflow:auto; }
|
|
1491
|
+
.dm-dg-row { position:relative; display:flex; flex-direction:column; gap:5px; padding:10px 12px 10px 15px; border-bottom:1px solid rgba(128,128,128,.14); cursor:pointer; }
|
|
1492
|
+
.dm-dg-row:last-child { border-bottom:none; }
|
|
1493
|
+
.dm-dg-row::before { content:''; position:absolute; left:0; top:0; bottom:0; width:3px; background:transparent; }
|
|
1494
|
+
.dm-dg-row.ok::before { background:#2e9e4f; }
|
|
1495
|
+
.dm-dg-row.run::before { background:#3b82d6; }
|
|
1496
|
+
.dm-dg-row.warn::before { background:#d29922; }
|
|
1497
|
+
.dm-dg-row.bad::before { background:#d14b41; }
|
|
1498
|
+
.dm-dg-row:hover { background:rgba(128,128,128,.09); }
|
|
1499
|
+
.dm-dg-row.active { background:rgba(88,166,255,.13); }
|
|
1500
|
+
.dm-dg-divider { position:relative; cursor:col-resize; min-height:0; border-radius:4px; }
|
|
1501
|
+
.dm-dg-divider::before { content:''; position:absolute; top:0; bottom:0; left:3px; width:1px; background:rgba(128,128,128,.28); }
|
|
1502
|
+
.dm-dg-divider:hover::before, .dm-dg-divider:active::before { background:rgba(88,166,255,.7); width:2px; left:2.5px; }
|
|
1503
|
+
.dm-dg-spinner { flex:none; color:var(--dsw-alias-label-tertiary, rgba(128,128,128,.85)); }
|
|
1504
|
+
.dm-dg-spinner-motion { transform-origin:center; animation:dm-dg-spin 1.5s linear infinite; }
|
|
1505
|
+
.dm-dg-spinner-track, .dm-dg-spinner-arc { fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; }
|
|
1506
|
+
.dm-dg-spinner-track { opacity:.25; }
|
|
1507
|
+
.dm-dg-spinner-arc { stroke-dasharray:12 150; animation:dm-dg-dash 1.5s ease-in-out infinite; }
|
|
1508
|
+
@keyframes dm-dg-spin { to { transform:rotate(360deg); } }
|
|
1509
|
+
@keyframes dm-dg-dash {
|
|
1510
|
+
0% { stroke-dasharray:12 150; stroke-dashoffset:0; }
|
|
1511
|
+
50% { stroke-dasharray:24 150; stroke-dashoffset:-6; }
|
|
1512
|
+
100% { stroke-dasharray:12 150; stroke-dashoffset:0; }
|
|
1513
|
+
}
|
|
1514
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1515
|
+
.dm-dg-spinner-motion, .dm-dg-spinner-arc { animation:none; }
|
|
1516
|
+
.dm-dg-spinner-arc { stroke-dasharray:18 150; stroke-dashoffset:-3; }
|
|
1517
|
+
}
|
|
1518
|
+
.dm-dg-row-top { display:flex; align-items:center; gap:8px; min-width:0; }
|
|
1519
|
+
.dm-dg-row-node { font-weight:650; white-space:nowrap; }
|
|
1520
|
+
.dm-dg-route { display:inline-flex; align-items:center; gap:6px; min-width:0; }
|
|
1521
|
+
.dm-dg-from, .dm-dg-to { font-weight:650; font-size:12.5px; white-space:nowrap; }
|
|
1522
|
+
.dm-dg-from { color:inherit; opacity:.85; }
|
|
1523
|
+
.dm-dg-to { color:#3b82d6; }
|
|
1524
|
+
.dm-dg-arrow { opacity:.5; font-size:11px; }
|
|
1525
|
+
.dm-dg-me { font-size:9.5px; padding:0 5px; border-radius:999px; border:1px solid rgba(63,185,80,.55); color:#2e9e4f; line-height:15px; }
|
|
1526
|
+
.dm-dg-filters { position:relative; z-index:5; display:flex; align-items:center; gap:8px; flex-wrap:wrap; padding:7px 10px; border-bottom:1px solid rgba(128,128,128,.16); background:rgba(128,128,128,.03); }
|
|
1527
|
+
.dm-dg-fgroup { display:flex; align-items:center; gap:5px; flex-wrap:wrap; font-size:10px; opacity:.8; }
|
|
1528
|
+
.dm-dg-fs { position:relative; display:inline-flex; align-items:center; gap:4px; }
|
|
1529
|
+
.dm-dg-fs-label { font-size:10px; opacity:.6; }
|
|
1530
|
+
.dm-dg-fs-btn { display:inline-flex; align-items:center; gap:6px; font-size:10.5px; padding:2px 8px; border-radius:6px; border:1px solid rgba(128,128,128,.35); background:rgba(128,128,128,.08); color:inherit; cursor:pointer; }
|
|
1531
|
+
.dm-dg-fs-btn:hover { background:rgba(128,128,128,.16); }
|
|
1532
|
+
.dm-dg-fs-caret { font-size:8px; opacity:.55; }
|
|
1533
|
+
.dm-dg-fs-menu { position:absolute; top:calc(100% + 4px); left:0; min-width:132px; padding:4px; border-radius:8px; border:1px solid rgba(128,128,128,.3); background:#ffffff; color:#1a1a1a; box-shadow:0 8px 24px rgba(0,0,0,.18); z-index:90; display:flex; flex-direction:column; gap:2px; }
|
|
1534
|
+
.dm-dg-fs-item { text-align:left; font-size:11px; padding:4px 9px; border-radius:5px; border:none; background:transparent; color:inherit; cursor:pointer; white-space:nowrap; }
|
|
1535
|
+
.dm-dg-fs-item:hover { background:rgba(128,128,128,.18); }
|
|
1536
|
+
.dm-dg-fs-item.active { background:rgba(88,166,255,.2); color:#2b6fc4; }
|
|
1537
|
+
body[data-ds-dark-theme] .dm-dg-fs-menu { background:#24262b; color:#e6e6e6; border-color:rgba(255,255,255,.16); box-shadow:0 8px 24px rgba(0,0,0,.55); }
|
|
1538
|
+
body[data-ds-dark-theme] .dm-dg-fs-item:hover { background:rgba(255,255,255,.1); }
|
|
1539
|
+
body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28); color:#79b8ff; }
|
|
1540
|
+
.dm-dg-fbtn { font-size:10px; padding:2px 8px; border-radius:6px; border:1px solid rgba(128,128,128,.35); background:transparent; color:inherit; cursor:pointer; }
|
|
1541
|
+
.dm-dg-fhint { font-size:9.5px; opacity:.45; }
|
|
1542
|
+
.dm-dg-fchip { font-size:10px; padding:0 8px; line-height:16px; border-radius:999px; border:1px solid rgba(128,128,128,.3); background:transparent; color:inherit; cursor:pointer; }
|
|
1543
|
+
.dm-dg-fchip.active { border-color:rgba(88,166,255,.7); color:#3b82d6; background:rgba(88,166,255,.12); }
|
|
1544
|
+
.dm-dg-badge { padding:0 6px; border-radius:5px; border:1px solid rgba(128,128,128,.32); font-size:10px; opacity:.8; }
|
|
1545
|
+
.dm-dg-row-time { margin-left:auto; font-size:10.5px; opacity:.5; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
1546
|
+
.dm-dg-chip { font-size:10px; padding:0 8px; border-radius:999px; border:1px solid rgba(128,128,128,.32); white-space:nowrap; line-height:17px; display:inline-block; }
|
|
1547
|
+
.dm-dg-chip.ok { border-color:rgba(63,185,80,.55); color:#2e9e4f; }
|
|
1548
|
+
.dm-dg-chip.bad { border-color:rgba(248,81,73,.55); color:#c9362c; }
|
|
1549
|
+
.dm-dg-chip.run { border-color:rgba(88,166,255,.55); color:#3b82d6; }
|
|
1550
|
+
.dm-dg-chip.warn { border-color:rgba(210,153,34,.6); color:#9a6c12; }
|
|
1551
|
+
.dm-dg-chip.muted { opacity:.7; }
|
|
1552
|
+
.dm-dg-goal { font-size:11.5px; opacity:.8; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1553
|
+
.dm-dg-meta { display:flex; gap:8px; flex-wrap:wrap; font-size:10px; opacity:.5; font-variant-numeric:tabular-nums; }
|
|
1554
|
+
.dm-dg-empty { padding:22px 14px; text-align:center; opacity:.55; font-size:12px; line-height:1.7; }
|
|
1555
|
+
.dm-dg-skel { padding:10px 12px; display:flex; flex-direction:column; gap:8px; }
|
|
1556
|
+
.dm-dg-skel i { display:block; height:10px; border-radius:6px; background:rgba(128,128,128,.18); animation:dm-dg-pulse 1.2s ease-in-out infinite; }
|
|
1557
|
+
.dm-dg-skel i:nth-child(2) { width:70%; } .dm-dg-skel i:nth-child(3) { width:45%; }
|
|
1558
|
+
@keyframes dm-dg-pulse { 0%,100% { opacity:.45 } 50% { opacity:.9 } }
|
|
1559
|
+
.dm-dg-detail { display:flex; flex-direction:column; gap:12px; padding:12px; }
|
|
1560
|
+
.dm-dg-sec { border:1px solid rgba(128,128,128,.22); border-radius:10px; overflow:hidden; }
|
|
1561
|
+
.dm-dg-sec-head { display:flex; align-items:center; gap:8px; padding:7px 11px; border-bottom:1px solid rgba(128,128,128,.18); background:rgba(128,128,128,.05); font-weight:650; font-size:11.5px; }
|
|
1562
|
+
.dm-dg-sec-body { padding:10px 11px; display:flex; flex-direction:column; gap:7px; }
|
|
1563
|
+
.dm-dg-kv { display:grid; grid-template-columns:92px minmax(0,1fr); gap:6px 10px; align-items:baseline; font-size:11.5px; }
|
|
1564
|
+
.dm-dg-k { opacity:.55; }
|
|
1565
|
+
.dm-dg-v { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; display:flex; gap:6px; align-items:center; }
|
|
1566
|
+
.dm-dg-copy { font-size:10px; padding:0 6px; border-radius:6px; border:1px solid rgba(128,128,128,.35); background:transparent; color:inherit; cursor:pointer; opacity:.6; flex:none; }
|
|
1567
|
+
.dm-dg-copy:hover { opacity:1; }
|
|
1568
|
+
.dm-dg-pre { white-space:pre-wrap; word-break:break-word; font-size:11px; line-height:1.55; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; max-height:220px; overflow:auto; border:1px dashed rgba(128,128,128,.25); border-radius:8px; padding:8px; background:rgba(128,128,128,.04); }
|
|
1569
|
+
.dm-dg-msgs { display:flex; flex-direction:column; gap:8px; max-height:380px; overflow:auto; padding-right:2px; }
|
|
1570
|
+
.dm-dg-msg { border:1px solid rgba(128,128,128,.18); border-radius:9px; padding:8px 10px; background:rgba(128,128,128,.03); }
|
|
1571
|
+
.dm-dg-msg.user { border-left:3px solid rgba(63,185,80,.6); }
|
|
1572
|
+
.dm-dg-msg.assistant { border-left:3px solid rgba(88,166,255,.6); }
|
|
1573
|
+
.dm-dg-msg-head { display:flex; gap:8px; align-items:baseline; margin-bottom:5px; font-size:10px; opacity:.6; text-transform:uppercase; letter-spacing:.4px; }
|
|
1574
|
+
.dm-dg-msg-text { white-space:pre-wrap; word-break:break-word; font-size:11.5px; line-height:1.55; }
|
|
1575
|
+
.dm-dg-loading { display:inline-flex; gap:6px; align-items:center; font-size:11.5px; opacity:.7; }
|
|
1576
|
+
.dm-dg-spin { width:11px; height:11px; border:2px solid rgba(128,128,128,.35); border-top-color:currentColor; border-radius:50%; animation:dm-dg-rot .8s linear infinite; }
|
|
1577
|
+
@keyframes dm-dg-rot { to { transform:rotate(360deg) } }
|
|
1578
|
+
@media (prefers-color-scheme: dark) {
|
|
1579
|
+
.dm-dg-pill.readonly { color:#79b8ff; }
|
|
1580
|
+
.dm-dg-btn.primary { color:#79b8ff; }
|
|
1581
|
+
.dm-dg-notice.ok { color:#3fb950; } .dm-dg-notice.bad { color:#ff7b72; }
|
|
1582
|
+
.dm-dg-chip.ok { color:#3fb950; } .dm-dg-to { color:#79b8ff; } .dm-dg-chip.run { color:#79b8ff; } .dm-dg-chip.warn { color:#d29922; } .dm-dg-chip.bad { color:#ff7b72; }
|
|
1583
|
+
}
|
|
1584
|
+
`
|
|
1585
|
+
|
|
1586
|
+
// 仿 DSH StateDot 的 ongoing 转圈环(1.5s 转 + 弧线呼吸)
|
|
1587
|
+
function LoadingSpinner(props) {
|
|
1588
|
+
var size = (props && props.size) || 14
|
|
1589
|
+
return h('svg', {
|
|
1590
|
+
className: 'dm-dg-spinner',
|
|
1591
|
+
width: size,
|
|
1592
|
+
height: size,
|
|
1593
|
+
viewBox: '0 0 24 24',
|
|
1594
|
+
'aria-hidden': 'true',
|
|
1595
|
+
'data-state': 'ongoing'
|
|
1596
|
+
},
|
|
1597
|
+
h('g', { className: 'dm-dg-spinner-motion' },
|
|
1598
|
+
h('circle', { className: 'dm-dg-spinner-track', cx: 12, cy: 12, r: 9.5 }),
|
|
1599
|
+
h('circle', { className: 'dm-dg-spinner-arc', cx: 12, cy: 12, r: 9.5 })
|
|
1600
|
+
)
|
|
1601
|
+
)
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
function dgFetch(path) {
|
|
1605
|
+
return fetch(path, { headers: { accept: 'application/json' } })
|
|
1606
|
+
.then(function (res) {
|
|
1607
|
+
return res.text().then(function (text) {
|
|
1608
|
+
var type = res.headers.get('content-type') || ''
|
|
1609
|
+
if (res.status === 404 || !/json/i.test(type)) {
|
|
1610
|
+
var routeError = new Error('route unavailable (HTTP ' + res.status + ')')
|
|
1611
|
+
routeError.code = 'E_NO_ROUTE'
|
|
1612
|
+
routeError.body = String(text || '').slice(0, 80)
|
|
1613
|
+
throw routeError
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
return JSON.parse(text)
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
var parseError = new Error('服务端返回的不是 JSON(可能需要重启该 DSH 实例)')
|
|
1619
|
+
parseError.code = 'E_PARSE'
|
|
1620
|
+
parseError.body = String(text || '').slice(0, 80)
|
|
1621
|
+
throw parseError
|
|
1622
|
+
}
|
|
1623
|
+
})
|
|
1624
|
+
})
|
|
1625
|
+
}
|
|
1626
|
+
function dgFriendly(err) {
|
|
1627
|
+
if (err && (err.code === 'E_NO_ROUTE' || err.code === 'E_PARSE')) return '当前 DSH 实例的服务端还是旧版本(缺接口),重启该实例后可用'
|
|
1628
|
+
return String((err && err.message) || err)
|
|
1629
|
+
}
|
|
1630
|
+
function dgDur(sec) {
|
|
1631
|
+
if (sec == null) return '—'
|
|
1632
|
+
if (sec < 60) return sec + 's'
|
|
1633
|
+
if (sec < 3600) return Math.floor(sec / 60) + 'm' + (sec % 60) + 's'
|
|
1634
|
+
return Math.floor(sec / 3600) + 'h' + Math.floor((sec % 3600) / 60) + 'm'
|
|
1635
|
+
}
|
|
1636
|
+
function dgClock(iso) {
|
|
1637
|
+
if (!iso) return '—'
|
|
1638
|
+
var d = new Date(iso)
|
|
1639
|
+
if (isNaN(d.getTime())) return '—'
|
|
1640
|
+
function p2(n) { return (n < 10 ? '0' : '') + n }
|
|
1641
|
+
return p2(d.getMonth() + 1) + '-' + p2(d.getDate()) + ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes())
|
|
1642
|
+
}
|
|
1643
|
+
function dgTone(state) { return DG_STATE_TONE[state] || 'muted' }
|
|
1644
|
+
// 状态/策略展示统一中文(内部值保持英文,避免影响筛选与接口)
|
|
1645
|
+
var DG_STATE_LABELS = {
|
|
1646
|
+
done: '已完成', working: '执行中', submitted: '已提交', accepted: '已受理',
|
|
1647
|
+
question: '待回复', stalled: '疑似卡住', failed: '失败', cancelled: '已取消',
|
|
1648
|
+
timeout: '超时', running: '运行中', provisioning: '准备中', inactive: '空闲', unknown: '未知'
|
|
1649
|
+
}
|
|
1650
|
+
var DG_POLICY_LABELS = { new: '新会话', continue: '续会话', auto: '自动', session: '指定会话' }
|
|
1651
|
+
function dgStateLabel(state) { return DG_STATE_LABELS[String(state || '')] || String(state || '未知') }
|
|
1652
|
+
function dgPolicyLabel(policy) { return DG_POLICY_LABELS[String(policy || '')] || String(policy || '—') }
|
|
1653
|
+
function dgChip(state) { return h('span', { className: 'dm-dg-chip ' + dgTone(state) }, dgStateLabel(state)) }
|
|
1654
|
+
|
|
1655
|
+
// 必须是 React 组件(返回元素),不能返回 DOM 节点
|
|
1656
|
+
function meshPanelIcon(props) {
|
|
1657
|
+
var size = (props && props.size) || 16
|
|
1658
|
+
return h('svg', {
|
|
1659
|
+
'data-dsh-panel-entry': 'mesh-delegations', viewBox: '0 0 16 16', width: size, height: size,
|
|
1660
|
+
fill: 'none', stroke: 'currentColor', strokeWidth: '1.4', strokeLinecap: 'round', 'aria-hidden': 'true',
|
|
1661
|
+
style: { color: 'var(--dsh-brand-color, currentColor)' }
|
|
1662
|
+
},
|
|
1663
|
+
h('path', { d: 'M8 3.2 L3.2 8 M8 3.2 L12.8 8 M3.2 8 L12.8 8', opacity: '.7' }),
|
|
1664
|
+
h('circle', { cx: 8, cy: 3.2, r: 1.6, fill: 'currentColor', stroke: 'none' }),
|
|
1665
|
+
h('circle', { cx: 3.2, cy: 8, r: 1.6, fill: 'currentColor', stroke: 'none' }),
|
|
1666
|
+
h('circle', { cx: 12.8, cy: 8, r: 1.6, fill: 'currentColor', stroke: 'none' })
|
|
1667
|
+
)
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function MeshDelegationPanel() {
|
|
1671
|
+
var s0 = useState(null); var list = s0[0]; var setList = s0[1]
|
|
1672
|
+
var s1 = useState(true); var loading = s1[0]; var setLoading = s1[1]
|
|
1673
|
+
var s2 = useState(null); var notice = s2[0]; var setNotice = s2[1]
|
|
1674
|
+
var s3 = useState(null); var selectedId = s3[0]; var setSelectedId = s3[1]
|
|
1675
|
+
var s4 = useState(null); var detail = s4[0]; var setDetail = s4[1]
|
|
1676
|
+
var s5 = useState(false); var taskLoading = s5[0]; var setTaskLoading = s5[1]
|
|
1677
|
+
var s6 = useState(null); var session = s6[0]; var setSession = s6[1]
|
|
1678
|
+
var s7 = useState(false); var sessionLoading = s7[0]; var setSessionLoading = s7[1]
|
|
1679
|
+
var s8 = useState(''); var copied = s8[0]; var setCopied = s8[1]
|
|
1680
|
+
var s10 = useState(null); var artifacts = s10[0]; var setArtifacts = s10[1]
|
|
1681
|
+
var s11 = useState(''); var localNode = s11[0]; var setLocalNode = s11[1]
|
|
1682
|
+
var s12 = useState(''); var lastListAt = s12[0]; var setLastListAt = s12[1]
|
|
1683
|
+
var s13 = useState(''); var lastTaskAt = s13[0]; var setLastTaskAt = s13[1]
|
|
1684
|
+
var s14 = useState(''); var filterFrom = s14[0]; var setFilterFrom = s14[1]
|
|
1685
|
+
var s15 = useState(''); var filterTo = s15[0]; var setFilterTo = s15[1]
|
|
1686
|
+
var s16 = useState(''); var filterFromNode = s16[0]; var setFilterFromNode = s16[1]
|
|
1687
|
+
var s17 = useState(''); var filterToNode = s17[0]; var setFilterToNode = s17[1]
|
|
1688
|
+
var s19 = useState(null); var rich = s19[0]; var setRich = s19[1]
|
|
1689
|
+
var s18 = useState(function () {
|
|
1690
|
+
try {
|
|
1691
|
+
var saved = window.localStorage.getItem('dsh-mesh-list-width')
|
|
1692
|
+
var value = saved ? Number(saved) : NaN
|
|
1693
|
+
if (Number.isFinite(value)) return Math.max(260, Math.min(640, value))
|
|
1694
|
+
} catch (error) { /* ignore */ }
|
|
1695
|
+
return 360
|
|
1696
|
+
})
|
|
1697
|
+
var listWidth = s18[0]; var setListWidth = s18[1]
|
|
1698
|
+
function startResize(event) {
|
|
1699
|
+
event.preventDefault()
|
|
1700
|
+
var startX = event.clientX
|
|
1701
|
+
var startW = listWidth
|
|
1702
|
+
var currentW = startW
|
|
1703
|
+
function onMove(moveEvent) {
|
|
1704
|
+
currentW = Math.max(260, Math.min(640, startW + (moveEvent.clientX - startX)))
|
|
1705
|
+
setListWidth(currentW)
|
|
1706
|
+
}
|
|
1707
|
+
function onUp() {
|
|
1708
|
+
window.removeEventListener('pointermove', onMove)
|
|
1709
|
+
window.removeEventListener('pointerup', onUp)
|
|
1710
|
+
document.body.style.cursor = ''
|
|
1711
|
+
document.body.style.userSelect = ''
|
|
1712
|
+
try { window.localStorage.setItem('dsh-mesh-list-width', String(currentW)) } catch (error) { /* ignore */ }
|
|
1713
|
+
}
|
|
1714
|
+
window.addEventListener('pointermove', onMove)
|
|
1715
|
+
window.addEventListener('pointerup', onUp)
|
|
1716
|
+
document.body.style.cursor = 'col-resize'
|
|
1717
|
+
document.body.style.userSelect = 'none'
|
|
1718
|
+
}
|
|
1719
|
+
function resetResize() {
|
|
1720
|
+
setListWidth(360)
|
|
1721
|
+
try { window.localStorage.setItem('dsh-mesh-list-width', '360') } catch (error) { /* ignore */ }
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// 只保留「错误」提示(持久显示,下一次成功操作/切换任务时清除);成功不再弹自动消失的提示
|
|
1725
|
+
function flash(type, text) {
|
|
1726
|
+
if (type === 'bad') setNotice({ type: 'bad', text: text })
|
|
1727
|
+
else setNotice(null)
|
|
1728
|
+
}
|
|
1729
|
+
function clearNotice() { setNotice(null) }
|
|
1730
|
+
|
|
1731
|
+
function loadList(silent) {
|
|
1732
|
+
if (!silent) setLoading(true)
|
|
1733
|
+
dgFetch(DG_API_LIST + '?limit=100')
|
|
1734
|
+
.then(function (json) {
|
|
1735
|
+
if (!json || !json.ok) throw new Error((json && json.error) || 'load failed')
|
|
1736
|
+
setList(Object.assign({}, json, { degraded: false }))
|
|
1737
|
+
if (json.localNode) setLocalNode(json.localNode)
|
|
1738
|
+
setLastListAt(new Date().toISOString())
|
|
1739
|
+
})
|
|
1740
|
+
.catch(function (err) {
|
|
1741
|
+
// 2026-11-07 兼容:旧实例没有 /delegations 路由 → 用 /status 里的 delegations.recent 降级
|
|
1742
|
+
if (err && (err.code === 'E_NO_ROUTE' || err.code === 'E_PARSE')) {
|
|
1743
|
+
return dgFetch('/api/dsh-mesh/status')
|
|
1744
|
+
.then(function (st) {
|
|
1745
|
+
var recent = (st && st.delegations && st.delegations.recent) || []
|
|
1746
|
+
var tasks = recent.map(function (item) {
|
|
1747
|
+
return {
|
|
1748
|
+
taskId: item.taskId,
|
|
1749
|
+
conversationId: item.conversationId || '',
|
|
1750
|
+
sessionId: item.sessionId || '',
|
|
1751
|
+
from: '',
|
|
1752
|
+
fromLabel: '',
|
|
1753
|
+
node: item.node,
|
|
1754
|
+
nodeLabel: nodeLabel(item.node),
|
|
1755
|
+
instance: '',
|
|
1756
|
+
state: item.state,
|
|
1757
|
+
policy: item.policy || '',
|
|
1758
|
+
goal: item.goal || '',
|
|
1759
|
+
createdAt: item.createdAt || '',
|
|
1760
|
+
updatedAt: item.updatedAt || '',
|
|
1761
|
+
durationSec: item.durationSec == null ? null : (item.durationSec > 43200 ? null : item.durationSec)
|
|
1762
|
+
}
|
|
1763
|
+
})
|
|
1764
|
+
if (st && st.nodeName) setLocalNode(st.nodeName)
|
|
1765
|
+
setList({
|
|
1766
|
+
total: (st && st.delegations && st.delegations.total) || tasks.length,
|
|
1767
|
+
byState: (st && st.delegations && st.delegations.byState) || {},
|
|
1768
|
+
tasks: tasks,
|
|
1769
|
+
degraded: true
|
|
1770
|
+
})
|
|
1771
|
+
flash('bad', '当前实例服务端未重启:已降级显示最近 ' + tasks.length + ' 条(重启后显示完整列表/详情)')
|
|
1772
|
+
})
|
|
1773
|
+
.catch(function () { throw err })
|
|
1774
|
+
}
|
|
1775
|
+
throw err
|
|
1776
|
+
})
|
|
1777
|
+
.catch(function (err) { flash('bad', '列表加载失败:' + dgFriendly(err)) })
|
|
1778
|
+
.finally(function () { setLoading(false) })
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
useEffect(function () {
|
|
1782
|
+
loadList(true)
|
|
1783
|
+
var timer = setInterval(function () { loadList(true) }, 20000)
|
|
1784
|
+
return function () { clearInterval(timer) }
|
|
1785
|
+
}, [])
|
|
1786
|
+
|
|
1787
|
+
var tasks = (list && list.tasks) || []
|
|
1788
|
+
var selected = null
|
|
1789
|
+
for (var i = 0; i < tasks.length; i++) { if (tasks[i].taskId === selectedId) { selected = tasks[i]; break } }
|
|
1790
|
+
|
|
1791
|
+
function selectTask(task) {
|
|
1792
|
+
setSelectedId(task.taskId)
|
|
1793
|
+
clearNotice()
|
|
1794
|
+
setDetail(null)
|
|
1795
|
+
setArtifacts(null)
|
|
1796
|
+
setRich(null)
|
|
1797
|
+
setSession(null)
|
|
1798
|
+
setTaskLoading(true)
|
|
1799
|
+
dgFetch(DG_API_TASK + '?taskId=' + encodeURIComponent(task.taskId))
|
|
1800
|
+
.then(function (json) {
|
|
1801
|
+
if (!json || !json.ok) throw new Error((json && json.error) || 'task status failed')
|
|
1802
|
+
setDetail(json.status || null)
|
|
1803
|
+
setArtifacts(json.artifacts || null)
|
|
1804
|
+
setRich(json.detail || null)
|
|
1805
|
+
setLastTaskAt(new Date().toISOString())
|
|
1806
|
+
clearNotice()
|
|
1807
|
+
})
|
|
1808
|
+
.catch(function (err) { flash('bad', '查询任务状态失败:' + dgFriendly(err)) })
|
|
1809
|
+
.finally(function () { setTaskLoading(false) })
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function loadSession() {
|
|
1813
|
+
if (!selected || !selected.sessionId) return
|
|
1814
|
+
setSessionLoading(true)
|
|
1815
|
+
dgFetch(DG_API_SESSION + '?node=' + encodeURIComponent(selected.node) + '&sessionId=' + encodeURIComponent(selected.sessionId) + '&limit=60')
|
|
1816
|
+
.then(function (json) {
|
|
1817
|
+
if (!json || !json.ok) throw new Error((json && json.error) || 'session history failed')
|
|
1818
|
+
setSession(json.payload || null)
|
|
1819
|
+
clearNotice()
|
|
1820
|
+
})
|
|
1821
|
+
.catch(function (err) { flash('bad', '加载对端会话失败:' + dgFriendly(err)) })
|
|
1822
|
+
.finally(function () { setSessionLoading(false) })
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function copyText(key, text) {
|
|
1826
|
+
try {
|
|
1827
|
+
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(function () { setCopied(key) }, function () { flash('bad', '复制失败') })
|
|
1828
|
+
else { setCopied(key) }
|
|
1829
|
+
} catch (_) { flash('bad', '复制失败') }
|
|
1830
|
+
setTimeout(function () { setCopied('') }, 1500)
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
var byState = (list && list.byState) || {}
|
|
1834
|
+
// 2026-11-07:节点筛选(委托方/接收方)+ 实例筛选(源实例仅新委托记录;旧数据为空 → 未记录)
|
|
1835
|
+
var fromNodeOptions = []
|
|
1836
|
+
var toNodeOptions = []
|
|
1837
|
+
var fromOptions = []
|
|
1838
|
+
var toOptions = []
|
|
1839
|
+
for (var ti = 0; ti < tasks.length; ti++) {
|
|
1840
|
+
var task0 = tasks[ti]
|
|
1841
|
+
if (fromNodeOptions.indexOf(task0.from || '') < 0) fromNodeOptions.push(task0.from || '')
|
|
1842
|
+
if (toNodeOptions.indexOf(task0.node || '') < 0) toNodeOptions.push(task0.node || '')
|
|
1843
|
+
var fi = task0.fromInstance || ''
|
|
1844
|
+
if (fromOptions.indexOf(fi) < 0) fromOptions.push(fi)
|
|
1845
|
+
var ii = task0.instance || ''
|
|
1846
|
+
if (toOptions.indexOf(ii) < 0) toOptions.push(ii)
|
|
1847
|
+
}
|
|
1848
|
+
fromNodeOptions.sort()
|
|
1849
|
+
toNodeOptions.sort()
|
|
1850
|
+
fromOptions.sort()
|
|
1851
|
+
toOptions.sort()
|
|
1852
|
+
function nodeOptionLabel(value, isFrom) {
|
|
1853
|
+
if (!value) return '未知节点'
|
|
1854
|
+
for (var k = 0; k < tasks.length; k++) {
|
|
1855
|
+
var item = tasks[k]
|
|
1856
|
+
if (isFrom && (item.from || '') === value) return item.fromLabel || value
|
|
1857
|
+
if (!isFrom && (item.node || '') === value) return (item.nodeLabel || value) + (item.fromIsLocal !== undefined ? '' : '')
|
|
1858
|
+
}
|
|
1859
|
+
return value
|
|
1860
|
+
}
|
|
1861
|
+
var visibleTasks = tasks.filter(function (task) {
|
|
1862
|
+
if (filterFromNode && (task.from || '') !== filterFromNode) return false
|
|
1863
|
+
if (filterToNode && (task.node || '') !== filterToNode) return false
|
|
1864
|
+
if (filterFrom && (task.fromInstance || '') !== filterFrom) return false
|
|
1865
|
+
if (filterTo && (task.instance || '') !== filterTo) return false
|
|
1866
|
+
return true
|
|
1867
|
+
})
|
|
1868
|
+
function resetFilters() { setFilterFromNode(''); setFilterToNode(''); setFilterFrom(''); setFilterTo('') }
|
|
1869
|
+
// 自绘下拉:不用原生 select(暗色主题下原生弹层会保持白底)
|
|
1870
|
+
function FilterSelect(props) {
|
|
1871
|
+
var fs = useState(false)
|
|
1872
|
+
var open = fs[0]
|
|
1873
|
+
var setOpen = fs[1]
|
|
1874
|
+
useEffect(function () {
|
|
1875
|
+
if (!open) return
|
|
1876
|
+
function close() { setOpen(false) }
|
|
1877
|
+
document.addEventListener('mousedown', close)
|
|
1878
|
+
return function () { document.removeEventListener('mousedown', close) }
|
|
1879
|
+
}, [open])
|
|
1880
|
+
return h('div', { className: 'dm-dg-fs' },
|
|
1881
|
+
h('span', { className: 'dm-dg-fs-label' }, props.label),
|
|
1882
|
+
h('button', {
|
|
1883
|
+
type: 'button',
|
|
1884
|
+
className: 'dm-dg-fs-btn',
|
|
1885
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
1886
|
+
onClick: function (event) { event.stopPropagation(); setOpen(!open) }
|
|
1887
|
+
},
|
|
1888
|
+
h('span', { className: 'dm-dg-fs-val' }, props.renderLabel(props.value)),
|
|
1889
|
+
h('span', { className: 'dm-dg-fs-caret' }, '▾')
|
|
1890
|
+
),
|
|
1891
|
+
open ? h('div', { className: 'dm-dg-fs-menu', onMouseDown: function (event) { event.stopPropagation() } },
|
|
1892
|
+
props.options.map(function (opt) {
|
|
1893
|
+
return h('button', {
|
|
1894
|
+
key: opt,
|
|
1895
|
+
type: 'button',
|
|
1896
|
+
className: 'dm-dg-fs-item' + (opt === props.value ? ' active' : ''),
|
|
1897
|
+
onClick: function (event) { event.stopPropagation(); props.onChange(opt); setOpen(false) }
|
|
1898
|
+
}, props.renderLabel(opt))
|
|
1899
|
+
})
|
|
1900
|
+
) : null
|
|
1901
|
+
)
|
|
1902
|
+
}
|
|
1903
|
+
var stats = ['done', 'working', 'submitted', 'question', 'stalled', 'failed'].filter(function (key) { return byState[key] })
|
|
1904
|
+
var selectedTone = selected ? dgTone(selected.state) : 'muted'
|
|
1905
|
+
var sessionMessages = (session && session.messages) || []
|
|
1906
|
+
var st = detail || {}
|
|
1907
|
+
|
|
1908
|
+
function copyBtn(key, text) {
|
|
1909
|
+
if (!text) return null
|
|
1910
|
+
return h('button', { type: 'button', className: 'dm-dg-copy', title: '复制 ' + key, onClick: function (e) { e.stopPropagation(); copyText(key, text) } }, copied === key ? '已复制' : '复制')
|
|
1911
|
+
}
|
|
1912
|
+
function kv(label, value, copyKey) {
|
|
1913
|
+
return h('div', { className: 'dm-dg-kv' },
|
|
1914
|
+
h('span', { className: 'dm-dg-k' }, label),
|
|
1915
|
+
h('span', { className: 'dm-dg-v', title: String(value == null ? '' : value) }, String(value == null || value === '' ? '—' : value), copyKey ? copyBtn(copyKey, String(value || '')) : null)
|
|
1916
|
+
)
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
return h('div', { className: 'dm-dg-root' },
|
|
1920
|
+
h('style', null, DG_CSS),
|
|
1921
|
+
h('div', { className: 'dm-dg-head' },
|
|
1922
|
+
h('span', { className: 'dm-dg-title' }, '委托'),
|
|
1923
|
+
h('span', { className: 'dm-dg-pill readonly' }, '只读'),
|
|
1924
|
+
h('div', { className: 'dm-dg-stats' },
|
|
1925
|
+
h('span', { className: 'dm-dg-stat' }, '共 ', h('b', null, String((list && list.total) || tasks.length || 0)), ' 条'),
|
|
1926
|
+
stats.map(function (key) { return h('span', { key: key, className: 'dm-dg-stat' }, h('b', null, String(byState[key])), ' ', dgStateLabel(key)) })
|
|
1927
|
+
),
|
|
1928
|
+
h('span', { className: 'dm-dg-spacer' }),
|
|
1929
|
+
h('span', { className: 'dm-dg-sub' }, '20s 自动刷新 · 数据来自本地台账' + (lastListAt ? ' · 上次 ' + dgClock(lastListAt) : '')),
|
|
1930
|
+
h('button', { type: 'button', className: 'dm-dg-btn', onClick: function () { loadList(false) }, disabled: loading },
|
|
1931
|
+
loading ? h('span', { className: 'dm-dg-loading' }, h('span', { className: 'dm-dg-spin' }), '刷新中') : '刷新')
|
|
1932
|
+
),
|
|
1933
|
+
notice ? h('div', { className: 'dm-dg-notice ' + notice.type }, notice.text) : null,
|
|
1934
|
+
h('div', { className: 'dm-dg-body', style: { '--dm-dg-list-w': listWidth + 'px' } },
|
|
1935
|
+
h('section', { className: 'dm-dg-pane' },
|
|
1936
|
+
h('div', { className: 'dm-dg-pane-head' }, '委托列表', h('span', { className: 'dm-dg-sub' }, '(' + visibleTasks.length + (visibleTasks.length !== tasks.length ? '/' + tasks.length : '') + ')')),
|
|
1937
|
+
h('div', { className: 'dm-dg-filters' },
|
|
1938
|
+
h(FilterSelect, { label: '委托方', value: filterFromNode, options: [''].concat(fromNodeOptions), onChange: setFilterFromNode, renderLabel: function (opt) { return opt === '' ? '全部节点' : nodeOptionLabel(opt, true) } }),
|
|
1939
|
+
h(FilterSelect, { label: '接收方', value: filterToNode, options: [''].concat(toNodeOptions), onChange: setFilterToNode, renderLabel: function (opt) { return opt === '' ? '全部节点' : nodeOptionLabel(opt, false) } }),
|
|
1940
|
+
h(FilterSelect, { label: '源实例', value: filterFrom, options: [''].concat(fromOptions), onChange: setFilterFrom, renderLabel: function (opt) { return opt === '' ? '全部' : (opt || '未记录') } }),
|
|
1941
|
+
h(FilterSelect, { label: '目标实例', value: filterTo, options: [''].concat(toOptions), onChange: setFilterTo, renderLabel: function (opt) { return opt === '' ? '全部' : (opt || '未记录') } }),
|
|
1942
|
+
h('button', { type: 'button', className: 'dm-dg-fbtn', onClick: resetFilters }, '重置筛选'),
|
|
1943
|
+
h('span', { className: 'dm-dg-fhint' }, '源实例仅对 2026-11-07 之后新发的委托有效,历史记录显示未记录')
|
|
1944
|
+
),
|
|
1945
|
+
h('div', { className: 'dm-dg-scroll' },
|
|
1946
|
+
loading && tasks.length === 0 ? h('div', { className: 'dm-dg-skel' }, h('i'), h('i'), h('i'), h('i')) : null,
|
|
1947
|
+
!loading && tasks.length === 0 ? h('div', { className: 'dm-dg-empty' }, '暂无委托记录', h('br'), '有委托后这里会自动出现') : null,
|
|
1948
|
+
!loading && tasks.length > 0 && visibleTasks.length === 0 ? h('div', { className: 'dm-dg-empty' }, '当前筛选无匹配委托') : null,
|
|
1949
|
+
visibleTasks.map(function (task) {
|
|
1950
|
+
var tone = dgTone(task.state)
|
|
1951
|
+
return h('div', {
|
|
1952
|
+
key: task.taskId,
|
|
1953
|
+
className: 'dm-dg-row ' + tone + (task.taskId === selectedId ? ' active' : ''),
|
|
1954
|
+
onClick: function () { if (task.taskId !== selectedId) selectTask(task) },
|
|
1955
|
+
title: task.goal || ''
|
|
1956
|
+
},
|
|
1957
|
+
h('div', { className: 'dm-dg-row-top' },
|
|
1958
|
+
task.activeFresh ? h(LoadingSpinner, { size: 12 }) : null,
|
|
1959
|
+
h('span', { className: 'dm-dg-route' },
|
|
1960
|
+
h('span', { className: 'dm-dg-from' }, task.fromLabel || task.from || '?'),
|
|
1961
|
+
(task.fromIsLocal || (localNode && task.from === localNode)) ? h('span', { className: 'dm-dg-me' }, task.fromInstance ? '本机·' + task.fromInstance : '本机') : null,
|
|
1962
|
+
h('span', { className: 'dm-dg-arrow' }, '→'),
|
|
1963
|
+
h('span', { className: 'dm-dg-to' }, (task.nodeLabel || task.node || '?') + (task.instance ? ' · ' + task.instance : ''))
|
|
1964
|
+
),
|
|
1965
|
+
dgChip(task.state),
|
|
1966
|
+
h('span', { className: 'dm-dg-row-time' }, (task.isActive ? '更新 ' : '完成 ') + dgClock(task.displayAt || task.updatedAt))
|
|
1967
|
+
),
|
|
1968
|
+
h('div', { className: 'dm-dg-goal' }, task.goal || '(无目标)'),
|
|
1969
|
+
h('div', { className: 'dm-dg-meta' },
|
|
1970
|
+
task.instance ? h('span', { className: 'dm-dg-badge' }, task.instance) : null,
|
|
1971
|
+
h('span', null, '历时 ' + dgDur(task.durationSec)),
|
|
1972
|
+
h('span', null, task.taskId),
|
|
1973
|
+
task.policy ? h('span', null, dgPolicyLabel(task.policy)) : null
|
|
1974
|
+
)
|
|
1975
|
+
)
|
|
1976
|
+
})
|
|
1977
|
+
)
|
|
1978
|
+
),
|
|
1979
|
+
h('div', { className: 'dm-dg-divider', title: '拖动调整列表宽度,双击复位', onPointerDown: startResize, onDoubleClick: resetResize }),
|
|
1980
|
+
h('section', { className: 'dm-dg-pane' },
|
|
1981
|
+
h('div', { className: 'dm-dg-pane-head' },
|
|
1982
|
+
'任务详情',
|
|
1983
|
+
h('span', { className: 'dm-dg-sub' }, selected ? selected.taskId + (lastTaskAt ? ' · 状态更新于 ' + dgClock(lastTaskAt) : '') : '未选择'),
|
|
1984
|
+
h('span', { className: 'dm-dg-spacer' }),
|
|
1985
|
+
selected ? h('button', { type: 'button', className: 'dm-dg-btn', onClick: function () { selectTask(selected) }, disabled: taskLoading },
|
|
1986
|
+
taskLoading ? h('span', { className: 'dm-dg-loading' }, h('span', { className: 'dm-dg-spin' }), '查询中') : '刷新状态') : null
|
|
1987
|
+
),
|
|
1988
|
+
h('div', { className: 'dm-dg-scroll' },
|
|
1989
|
+
selected === null
|
|
1990
|
+
? h('div', { className: 'dm-dg-empty' }, '在左侧选择一条委托', h('br'), '可查看任务详情与对端会话(只读)')
|
|
1991
|
+
: h('div', { className: 'dm-dg-detail' },
|
|
1992
|
+
h('div', { className: 'dm-dg-sec' },
|
|
1993
|
+
h('div', { className: 'dm-dg-sec-head' }, '概要'),
|
|
1994
|
+
h('div', { className: 'dm-dg-sec-body' },
|
|
1995
|
+
h('div', { className: 'dm-dg-kv' },
|
|
1996
|
+
h('span', { className: 'dm-dg-k' }, '状态'),
|
|
1997
|
+
h('span', { className: 'dm-dg-v' },
|
|
1998
|
+
selected.activeFresh ? h(LoadingSpinner, { size: 13 }) : null,
|
|
1999
|
+
dgChip(selected.state)
|
|
2000
|
+
)
|
|
2001
|
+
),
|
|
2002
|
+
kv('委托方', (selected.fromLabel || selected.from || '—') + ((selected.fromIsLocal || (localNode && selected.from === localNode)) ? '(本机)' : '')),
|
|
2003
|
+
kv('接收方', (selected.nodeLabel || selected.node || '—') + (selected.instance ? ' · ' + selected.instance : '')),
|
|
2004
|
+
kv('目标实例', selected.instance || '—'),
|
|
2005
|
+
kv('策略', dgPolicyLabel(selected.policy)),
|
|
2006
|
+
kv('历时' + (artifacts && artifacts.durationSec != null ? '(实测)' : ''), artifacts && artifacts.durationSec != null ? dgDur(artifacts.durationSec) : dgDur(selected.durationSec)),
|
|
2007
|
+
kv('开始时间', artifacts && artifacts.requestedAt ? dgClock(artifacts.requestedAt) : dgClock(selected.createdAt)),
|
|
2008
|
+
kv('结束时间', artifacts && artifacts.finishedAt ? dgClock(artifacts.finishedAt) : '—'),
|
|
2009
|
+
kv('台账更新', dgClock(selected.updatedAt))
|
|
2010
|
+
)
|
|
2011
|
+
),
|
|
2012
|
+
h('div', { className: 'dm-dg-sec' },
|
|
2013
|
+
h('div', { className: 'dm-dg-sec-head' }, '标识'),
|
|
2014
|
+
h('div', { className: 'dm-dg-sec-body' },
|
|
2015
|
+
kv('taskId', selected.taskId, 'taskId'),
|
|
2016
|
+
kv('conversation', selected.conversationId, 'conversationId'),
|
|
2017
|
+
kv('sessionId', selected.sessionId, 'sessionId')
|
|
2018
|
+
)
|
|
2019
|
+
),
|
|
2020
|
+
h('div', { className: 'dm-dg-sec' },
|
|
2021
|
+
h('div', { className: 'dm-dg-sec-head' }, '目标'),
|
|
2022
|
+
h('div', { className: 'dm-dg-sec-body' }, h('div', { className: 'dm-dg-pre' }, selected.goal || '(无目标)'))
|
|
2023
|
+
),
|
|
2024
|
+
rich ? h('div', { className: 'dm-dg-sec' },
|
|
2025
|
+
h('div', { className: 'dm-dg-sec-head' }, '时间线'),
|
|
2026
|
+
h('div', { className: 'dm-dg-sec-body' },
|
|
2027
|
+
kv('委托发出', rich.timeline ? dgClock(rich.timeline.requestedAt) : '—'),
|
|
2028
|
+
kv('对端心跳', rich.timeline ? dgClock(rich.timeline.heartbeatAt) : '—'),
|
|
2029
|
+
kv('结束时间', rich.timeline ? dgClock(rich.timeline.finishedAt) : '—'),
|
|
2030
|
+
kv('响应耗时', rich.timeline && rich.timeline.responseSec != null ? dgDur(rich.timeline.responseSec) : '—'),
|
|
2031
|
+
kv('执行耗时', rich.timeline && rich.timeline.execSec != null ? dgDur(rich.timeline.execSec) : '—'),
|
|
2032
|
+
kv('总耗时', rich.timeline && rich.timeline.totalSec != null ? dgDur(rich.timeline.totalSec) : dgDur(selected.durationSec))
|
|
2033
|
+
)
|
|
2034
|
+
) : null,
|
|
2035
|
+
rich && (rich.operations && (rich.operations.commandsRun.length > 0 || rich.operations.evidence.length > 0 || rich.operations.notes)) ? h('div', { className: 'dm-dg-sec' },
|
|
2036
|
+
h('div', { className: 'dm-dg-sec-head' }, '对端操作',
|
|
2037
|
+
rich.operations.readonly ? h('span', { className: 'dm-dg-chip ok' }, '只读') : null
|
|
2038
|
+
),
|
|
2039
|
+
h('div', { className: 'dm-dg-sec-body' },
|
|
2040
|
+
rich.operations.notes ? h('div', { className: 'dm-dg-pre' }, rich.operations.notes) : null,
|
|
2041
|
+
rich.operations.commandsRun.length > 0 ? h('div', null,
|
|
2042
|
+
h('div', { className: 'dm-dg-sub' }, '执行过的命令(对端回报)'),
|
|
2043
|
+
h('div', { className: 'dm-dg-pre' }, rich.operations.commandsRun.map(function (item) { return '· ' + item }).join('\n'))
|
|
2044
|
+
) : null,
|
|
2045
|
+
rich.operations.evidence.length > 0 ? h('div', null,
|
|
2046
|
+
h('div', { className: 'dm-dg-sub' }, '产物/证据'),
|
|
2047
|
+
h('div', { className: 'dm-dg-pre' }, rich.operations.evidence.map(function (item) { return '· ' + item }).join('\n'))
|
|
2048
|
+
) : null
|
|
2049
|
+
)
|
|
2050
|
+
) : null,
|
|
2051
|
+
rich && (rich.operations && (rich.operations.scope || rich.operations.acceptance || rich.operations.sessionPolicy)) ? h('div', { className: 'dm-dg-sec' },
|
|
2052
|
+
h('div', { className: 'dm-dg-sec-head' }, '权限 / 范围'),
|
|
2053
|
+
h('div', { className: 'dm-dg-sec-body' },
|
|
2054
|
+
kv('会话策略', rich.operations.sessionPolicy ? dgPolicyLabel(rich.operations.sessionPolicy) : '—'),
|
|
2055
|
+
kv('目标实例', rich.operations.targetInstance || '—'),
|
|
2056
|
+
kv('工作区', rich.operations.workspace || '—'),
|
|
2057
|
+
kv('授权范围', rich.operations.scope || '(未声明)'),
|
|
2058
|
+
kv('验收标准', rich.operations.acceptance || '(未声明)')
|
|
2059
|
+
)
|
|
2060
|
+
) : null,
|
|
2061
|
+
h('div', { className: 'dm-dg-sec' },
|
|
2062
|
+
h('div', { className: 'dm-dg-sec-head' }, '对端状态',
|
|
2063
|
+
h('span', { className: 'dm-dg-sub' }, taskLoading ? '查询中…' : detail ? '已取到 status.json' : '未查询')),
|
|
2064
|
+
h('div', { className: 'dm-dg-sec-body' },
|
|
2065
|
+
st.degraded ? h('div', { className: 'dm-dg-notice bad' }, '对端查询失败,已降级显示本地台账:' + String(st.error || '')) : null,
|
|
2066
|
+
kv('状态', st.state ? dgStateLabel(st.state) : '—'),
|
|
2067
|
+
kv('会话状态', st.sessionState ? dgStateLabel(st.sessionState) : '—'),
|
|
2068
|
+
kv('心跳延迟', st.staleSeconds == null ? '—' : String(st.staleSeconds) + ' 秒'),
|
|
2069
|
+
kv('error', st.error || '—'),
|
|
2070
|
+
st.result ? h('div', { className: 'dm-dg-pre' }, JSON.stringify(st.result, null, 2)) : null
|
|
2071
|
+
)
|
|
2072
|
+
),
|
|
2073
|
+
h('div', { className: 'dm-dg-sec' },
|
|
2074
|
+
h('div', { className: 'dm-dg-sec-head' },
|
|
2075
|
+
'对端会话',
|
|
2076
|
+
h('span', { className: 'dm-dg-sub' }, session ? ('轮次 ' + (session.totalMessages || sessionMessages.length) + ' · 帧 ' + (session.frameCount || '—') + ' · ' + Math.round((session.bytes || 0) / 1024) + ' KB') : '只读'),
|
|
2077
|
+
h('span', { className: 'dm-dg-spacer' }),
|
|
2078
|
+
h('button', { type: 'button', className: 'dm-dg-btn primary', onClick: loadSession, disabled: sessionLoading || !selected.sessionId },
|
|
2079
|
+
sessionLoading ? h('span', { className: 'dm-dg-loading' }, h('span', { className: 'dm-dg-spin' }), '加载中') : (session ? '重新加载' : '加载会话'))
|
|
2080
|
+
),
|
|
2081
|
+
h('div', { className: 'dm-dg-sec-body' },
|
|
2082
|
+
session ? (session.title ? h('div', { className: 'dm-dg-sub' }, '标题:' + session.title) : null) : null,
|
|
2083
|
+
session
|
|
2084
|
+
? h('div', { className: 'dm-dg-msgs' },
|
|
2085
|
+
sessionMessages.length === 0 ? h('div', { className: 'dm-dg-empty' }, '无消息') : null,
|
|
2086
|
+
sessionMessages.map(function (msg, idx) {
|
|
2087
|
+
return h('div', { key: idx, className: 'dm-dg-msg ' + (msg.role || '') },
|
|
2088
|
+
h('div', { className: 'dm-dg-msg-head' }, h('span', null, msg.role || '?'), msg.time ? h('span', null, msg.time) : null),
|
|
2089
|
+
h('div', { className: 'dm-dg-msg-text' }, String(msg.text || ''))
|
|
2090
|
+
)
|
|
2091
|
+
})
|
|
2092
|
+
)
|
|
2093
|
+
: h('div', { className: 'dm-dg-empty' }, selected.sessionId ? '点击右上「加载会话」查看对端会话正文' : '该委托没有 sessionId')
|
|
2094
|
+
)
|
|
2095
|
+
)
|
|
2096
|
+
)
|
|
2097
|
+
)
|
|
2098
|
+
)
|
|
2099
|
+
)
|
|
2100
|
+
)
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
var SECTION = { name: 'settings.section', id: 'dsh-mesh', order: 90, label: 'AI 网络' }
|
|
2104
|
+
var inject = ['slots']
|
|
2105
|
+
|
|
2106
|
+
// 2026-11-07:设置左侧导航里「AI 网络」这一项,把壳的默认齿轮换成三节点图标,颜色跟随 --dsh-brand-color
|
|
2107
|
+
function navMeshIconDom(referenceClass) {
|
|
2108
|
+
var NS = 'http://www.w3.org/2000/svg'
|
|
2109
|
+
var svg = document.createElementNS(NS, 'svg')
|
|
2110
|
+
if (referenceClass) svg.setAttribute('class', referenceClass)
|
|
2111
|
+
svg.setAttribute('width', '16')
|
|
2112
|
+
svg.setAttribute('height', '16')
|
|
2113
|
+
svg.setAttribute('viewBox', '0 0 16 16')
|
|
2114
|
+
svg.setAttribute('fill', 'none')
|
|
2115
|
+
svg.setAttribute('stroke', 'currentColor')
|
|
2116
|
+
svg.setAttribute('stroke-width', '1.4')
|
|
2117
|
+
svg.setAttribute('stroke-linecap', 'round')
|
|
2118
|
+
svg.setAttribute('aria-hidden', 'true')
|
|
2119
|
+
svg.setAttribute('data-dsh-mesh-nav-icon', '1')
|
|
2120
|
+
svg.style.color = 'var(--dsh-brand-color, currentColor)'
|
|
2121
|
+
var line = document.createElementNS(NS, 'path')
|
|
2122
|
+
line.setAttribute('d', 'M8 3.2 L3.2 8 M8 3.2 L12.8 8 M3.2 8 L12.8 8')
|
|
2123
|
+
line.setAttribute('opacity', '.7')
|
|
2124
|
+
svg.appendChild(line)
|
|
2125
|
+
var dots = [[8, 3.2], [3.2, 8], [12.8, 8]]
|
|
2126
|
+
for (var i = 0; i < dots.length; i++) {
|
|
2127
|
+
var circle = document.createElementNS(NS, 'circle')
|
|
2128
|
+
circle.setAttribute('cx', dots[i][0])
|
|
2129
|
+
circle.setAttribute('cy', dots[i][1])
|
|
2130
|
+
circle.setAttribute('r', '1.6')
|
|
2131
|
+
circle.setAttribute('fill', 'currentColor')
|
|
2132
|
+
circle.setAttribute('stroke', 'none')
|
|
2133
|
+
svg.appendChild(circle)
|
|
2134
|
+
}
|
|
2135
|
+
return svg
|
|
2136
|
+
}
|
|
2137
|
+
function decorateMeshNav() {
|
|
2138
|
+
try {
|
|
2139
|
+
var buttons = document.querySelectorAll('[role="dialog"] nav button')
|
|
2140
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
2141
|
+
var button = buttons[i]
|
|
2142
|
+
var text = (button.textContent || '').trim()
|
|
2143
|
+
if (text !== 'AI 网络' && text !== 'AI 网络(dsh-mesh)') continue
|
|
2144
|
+
if (button.querySelector('svg[data-dsh-mesh-nav-icon]')) continue
|
|
2145
|
+
var gear = button.querySelector('svg')
|
|
2146
|
+
var className = gear ? gear.getAttribute('class') || '' : ''
|
|
2147
|
+
var icon = navMeshIconDom(className)
|
|
2148
|
+
if (gear && gear.parentNode === button) button.replaceChild(icon, gear)
|
|
2149
|
+
else button.insertBefore(icon, button.firstChild)
|
|
2150
|
+
}
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
/* ignore */
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function apply(ctx) {
|
|
2157
|
+
var disposers = []
|
|
2158
|
+
try {
|
|
2159
|
+
disposers.push(ctx.slots.inject('settings.section', function () { return ctx.slots.register(SECTION, MeshStatusSection) }))
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
console.warn('[dsh-mesh-ui] settings section registration failed:', error)
|
|
2162
|
+
}
|
|
2163
|
+
// 2026-11-07 方案 A:左侧栏「委托」面板(sidebar.panellist + main)
|
|
2164
|
+
try {
|
|
2165
|
+
disposers.push(ctx.slots.inject('sidebar.panellist', function () {
|
|
2166
|
+
return ctx.slots.register({ name: 'sidebar.panellist', id: 'mesh-delegations', order: 55, label: function () { return '委托' } }, meshPanelIcon)
|
|
2167
|
+
}))
|
|
2168
|
+
disposers.push(ctx.slots.inject('main', function () {
|
|
2169
|
+
return ctx.slots.register({ name: 'main', key: 'mesh-delegations', inject: function () { return {} } }, MeshDelegationPanel)
|
|
2170
|
+
}))
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
console.warn('[dsh-mesh-ui] delegation panel registration failed:', error)
|
|
2173
|
+
}
|
|
2174
|
+
var navObserver = null
|
|
2175
|
+
var navTimer = null
|
|
2176
|
+
try {
|
|
2177
|
+
if (typeof MutationObserver !== 'undefined' && document.body) {
|
|
2178
|
+
navObserver = new MutationObserver(function () { decorateMeshNav() })
|
|
2179
|
+
navObserver.observe(document.body, { childList: true, subtree: true })
|
|
2180
|
+
}
|
|
2181
|
+
navTimer = setInterval(decorateMeshNav, 1500)
|
|
2182
|
+
if (navTimer && typeof navTimer.unref === 'function') navTimer.unref()
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
/* ignore */
|
|
2185
|
+
}
|
|
2186
|
+
ctx.effect(function () {
|
|
2187
|
+
return function () {
|
|
2188
|
+
for (var i = 0; i < disposers.length; i++) { try { disposers[i]() } catch (_) { /* ignore */ } }
|
|
2189
|
+
try { if (navObserver) navObserver.disconnect() } catch (_) { /* ignore */ }
|
|
2190
|
+
try { if (navTimer) clearInterval(navTimer) } catch (_) { /* ignore */ }
|
|
2191
|
+
}
|
|
2192
|
+
}, 'dsh-mesh-ui: settings section + nav icon')
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
exports.apply = apply
|
|
2196
|
+
exports.inject = inject
|
|
2197
|
+
return module.exports
|
|
2198
|
+
}
|
|
2199
|
+
})
|