dsh-mesh-ui 0.0.7 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +44 -8
- package/lib/delegations.js +62 -39
- package/lib/index.js +2 -0
- package/package.json +5 -5
package/lib/client.js
CHANGED
|
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
var type = deviceType(name)
|
|
43
43
|
return type ? label + ' · ' + type : label
|
|
44
44
|
}
|
|
45
|
-
// 同一台设备可能有多个 peer 记录(不同 token/入口:
|
|
45
|
+
// 同一台设备可能有多个 peer 记录(不同 token/入口:peer-node / peer-node-lan / peer-node-exec)。
|
|
46
46
|
// AI 网络拓扑按「设备」展示:去掉后缀归并,取可达且延迟最优的那条。
|
|
47
47
|
var PEER_SUFFIX = /-(lan|wan|exec|exe|import|local|ctl)$/i
|
|
48
48
|
function peerDeviceKey(name) {
|
|
@@ -96,6 +96,7 @@ function nodeLabel(name) { return NODE_LABELS[String(name || '').toLowerCase()]
|
|
|
96
96
|
return keys
|
|
97
97
|
}
|
|
98
98
|
// 2026-11-10:用户手工调整后认可的默认布局(世界坐标,10000×6000 画布内)
|
|
99
|
+
// 设备按设备名(home/peer-node/company/peer-node/peer-node)映射,各机通用;服务器两张卡单独一张表
|
|
99
100
|
var DEFAULT_DEVICE_POS = {}
|
|
100
101
|
var DEFAULT_INFRA_POS = {
|
|
101
102
|
aliyun: [4887.59, 2856.78],
|
|
@@ -677,7 +678,7 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
|
|
|
677
678
|
h('span', null, '⋯')
|
|
678
679
|
)
|
|
679
680
|
),
|
|
680
|
-
h('div', { className: 'dm-hint' }, '拓扑
|
|
681
|
+
h('div', { className: 'dm-hint' }, '拓扑 v21 · 内容 ' + Math.round(layout.W) + '×' + Math.round(layout.H) + 'px · 卡 ' + Math.round(layout.cardW) + '×' + Math.round(layout.cardH) + ' · 缩放 ' + Math.round(view.scale * 100) + '% · 滚轮缩放 / 拖拽平移')
|
|
681
682
|
)
|
|
682
683
|
}
|
|
683
684
|
|
|
@@ -811,6 +812,7 @@ var DOWN_COLOR = "rgba(209,75,65,.7)"
|
|
|
811
812
|
// 各机 DSH 安装形态(实测/档案)
|
|
812
813
|
var DEVICE_DSH = {}
|
|
813
814
|
function dshTypesFor(devKey) { return DEVICE_DSH[peerDeviceKey(devKey)] || [] }
|
|
815
|
+
// LAN peer 判定:-lan 后缀 / 内网 IP / kind=lan(实际数据里 peer-node-lan 的 kind 是 frp)
|
|
814
816
|
function isLanPeer(peer) {
|
|
815
817
|
var t = (peer && peer.tunnel) || {}
|
|
816
818
|
var name = String((peer && peer.name) || '')
|
|
@@ -1291,6 +1293,32 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1291
1293
|
return v
|
|
1292
1294
|
}
|
|
1293
1295
|
|
|
1296
|
+
// 2026-11-10:设备状态与拓扑同口径 —— 任一链路可达即算在线(HTTP 探测偶发超时不再误判红点)
|
|
1297
|
+
var lanUpByDev = {}
|
|
1298
|
+
;(data.peers || []).forEach(function (p) {
|
|
1299
|
+
if (!isLanPeer(p)) return
|
|
1300
|
+
var tu = p.tunnel || {}
|
|
1301
|
+
lanUpByDev[peerDeviceKey(p.name)] = tu.probe ? tu.probe.up === true : Boolean(p.up)
|
|
1302
|
+
})
|
|
1303
|
+
function peerLinkState(peer) {
|
|
1304
|
+
var t = peer.tunnel || {}
|
|
1305
|
+
var httpUp = Boolean(peer.up)
|
|
1306
|
+
var tcpUp = t.probe ? t.probe.up === true : false
|
|
1307
|
+
var sk = sakuraTunnelOf(data, peerDeviceKey(peer.name))
|
|
1308
|
+
var sakuraUp = sk ? sk.online === true : null
|
|
1309
|
+
var lanUp = lanUpByDev[peerDeviceKey(peer.name)] === true
|
|
1310
|
+
return {
|
|
1311
|
+
httpUp: httpUp, tcpUp: tcpUp, sakuraUp: sakuraUp, lanUp: lanUp,
|
|
1312
|
+
up: httpUp || tcpUp || sakuraUp === true || lanUp
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function linkStateText(ls) {
|
|
1316
|
+
return "公网 " + ((ls.httpUp || ls.tcpUp) ? "✅" : "❌")
|
|
1317
|
+
+ " / 樱花 " + (ls.sakuraUp == null ? "—" : (ls.sakuraUp ? "✅" : "❌"))
|
|
1318
|
+
+ (ls.lanUp ? " / LAN ✅" : "")
|
|
1319
|
+
+ (ls.httpUp ? "" : "(HTTP 超时,隧道" + (ls.tcpUp ? "通" : "断") + ")")
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1294
1322
|
var devices = [{
|
|
1295
1323
|
key: 'local',
|
|
1296
1324
|
name: nodeDisplay(data.nodeName) + '(本机)',
|
|
@@ -1308,11 +1336,12 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1308
1336
|
]
|
|
1309
1337
|
}].concat(peers.map(function (peer) {
|
|
1310
1338
|
var t = peer.tunnel || {}
|
|
1339
|
+
var ls = peerLinkState(peer)
|
|
1311
1340
|
var hwp = peer.hardware || {}
|
|
1312
1341
|
return {
|
|
1313
1342
|
key: 'peer:' + peer.name,
|
|
1314
1343
|
name: nodeDisplay(peerDeviceKey(peer.name)),
|
|
1315
|
-
up:
|
|
1344
|
+
up: ls.up,
|
|
1316
1345
|
rows: [
|
|
1317
1346
|
['设备类型', formFactorOf(hwp)],
|
|
1318
1347
|
['操作系统', hwp.os || peer.os || '—'],
|
|
@@ -1322,7 +1351,8 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1322
1351
|
['DSH 版本', dshVerOf(peer)],
|
|
1323
1352
|
['IP 类型', ipKindOf(peer)],
|
|
1324
1353
|
['端口号', t.port ? String(t.port) : '—'],
|
|
1325
|
-
['公网穿透类型', relayKindOf(peer)]
|
|
1354
|
+
['公网穿透类型', relayKindOf(peer)],
|
|
1355
|
+
['链路状态', linkStateText(ls)]
|
|
1326
1356
|
]
|
|
1327
1357
|
}
|
|
1328
1358
|
}))
|
|
@@ -1899,6 +1929,11 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
1899
1929
|
|
|
1900
1930
|
function loadSession() {
|
|
1901
1931
|
if (!selected || !selected.sessionId) return
|
|
1932
|
+
// 2026-11-10:本机任务(local)没有对端会话,不要发 peer call
|
|
1933
|
+
if (/^(local|self|localhost)$/i.test(String(selected.node || ''))) {
|
|
1934
|
+
flash('bad', '本机任务(local)没有对端会话;请在 DSH 侧栏查看本机会话')
|
|
1935
|
+
return
|
|
1936
|
+
}
|
|
1902
1937
|
setSessionLoading(true)
|
|
1903
1938
|
dgFetch(DG_API_SESSION + '?node=' + encodeURIComponent(selected.node) + '&sessionId=' + encodeURIComponent(selected.sessionId) + '&limit=60')
|
|
1904
1939
|
.then(function (json) {
|
|
@@ -2019,6 +2054,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2019
2054
|
loading ? h('span', { className: 'dm-dg-loading' }, h('span', { className: 'dm-dg-spin' }), '刷新中') : '刷新')
|
|
2020
2055
|
),
|
|
2021
2056
|
notice ? h('div', { className: 'dm-dg-notice ' + notice.type }, notice.text) : null,
|
|
2057
|
+
selected && selected.staleActive ? h('div', { className: 'dm-dg-notice warn' }, '该任务已超过 ' + Math.round(selected.staleAfterSec / 60) + ' 分钟没有更新,可能已失联;可点「刷新状态」再查对端,或重新发起委托。') : null,
|
|
2022
2058
|
h('div', { className: 'dm-dg-body', style: { '--dm-dg-list-w': listWidth + 'px' } },
|
|
2023
2059
|
h('section', { className: 'dm-dg-pane' },
|
|
2024
2060
|
h('div', { className: 'dm-dg-pane-head' }, '委托列表', h('span', { className: 'dm-dg-sub' }, '(' + visibleTasks.length + (visibleTasks.length !== tasks.length ? '/' + tasks.length : '') + ')')),
|
|
@@ -2040,7 +2076,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2040
2076
|
key: task.taskId,
|
|
2041
2077
|
className: 'dm-dg-row ' + tone + (task.taskId === selectedId ? ' active' : ''),
|
|
2042
2078
|
onClick: function () { if (task.taskId !== selectedId) selectTask(task) },
|
|
2043
|
-
title: task.goal || ''
|
|
2079
|
+
title: task.staleActive ? '超过 ' + Math.round(task.staleAfterSec / 60) + ' 分钟无更新(疑似失联,可点刷新状态)' : (task.goal || '')
|
|
2044
2080
|
},
|
|
2045
2081
|
h('div', { className: 'dm-dg-row-top' },
|
|
2046
2082
|
task.activeFresh ? h(LoadingSpinner, { size: 12 }) : null,
|
|
@@ -2050,7 +2086,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2050
2086
|
h('span', { className: 'dm-dg-arrow' }, '→'),
|
|
2051
2087
|
h('span', { className: 'dm-dg-to' }, (task.nodeLabel || task.node || '?') + (task.instance ? ' · ' + task.instance : ''))
|
|
2052
2088
|
),
|
|
2053
|
-
dgChip(task.state),
|
|
2089
|
+
dgChip(task.displayState || task.state),
|
|
2054
2090
|
h('span', { className: 'dm-dg-row-time' }, (task.isActive ? '更新 ' : '完成 ') + dgClock(task.displayAt || task.updatedAt))
|
|
2055
2091
|
),
|
|
2056
2092
|
h('div', { className: 'dm-dg-goal' }, task.goal || '(无目标)'),
|
|
@@ -2084,7 +2120,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2084
2120
|
h('span', { className: 'dm-dg-k' }, '状态'),
|
|
2085
2121
|
h('span', { className: 'dm-dg-v' },
|
|
2086
2122
|
selected.activeFresh ? h(LoadingSpinner, { size: 13 }) : null,
|
|
2087
|
-
dgChip(selected.state)
|
|
2123
|
+
dgChip(selected.displayState || selected.state)
|
|
2088
2124
|
)
|
|
2089
2125
|
),
|
|
2090
2126
|
kv('委托方', (selected.fromLabel || selected.from || '—') + ((selected.fromIsLocal || (localNode && selected.from === localNode)) ? '(本机)' : '')),
|
|
@@ -2163,7 +2199,7 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2163
2199
|
'对端会话',
|
|
2164
2200
|
h('span', { className: 'dm-dg-sub' }, session ? ('轮次 ' + (session.totalMessages || sessionMessages.length) + ' · 帧 ' + (session.frameCount || '—') + ' · ' + Math.round((session.bytes || 0) / 1024) + ' KB') : '只读'),
|
|
2165
2201
|
h('span', { className: 'dm-dg-spacer' }),
|
|
2166
|
-
h('button', { type: 'button', className: 'dm-dg-btn primary', onClick: loadSession, disabled: sessionLoading || !selected.sessionId },
|
|
2202
|
+
h('button', { type: 'button', className: 'dm-dg-btn primary', onClick: loadSession, disabled: sessionLoading || !selected.sessionId || /^(local|self|localhost)$/i.test(String(selected.node || '')) },
|
|
2167
2203
|
sessionLoading ? h('span', { className: 'dm-dg-loading' }, h('span', { className: 'dm-dg-spin' }), '加载中') : (session ? '重新加载' : '加载会话'))
|
|
2168
2204
|
),
|
|
2169
2205
|
h('div', { className: 'dm-dg-sec-body' },
|
package/lib/delegations.js
CHANGED
|
@@ -26,53 +26,76 @@ function readJson(file, fallback = null) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** 委托列表(本地台账,按更新时间倒序)。
|
|
29
|
+
/** 委托列表(本地台账,按更新时间倒序)。
|
|
30
|
+
* - 过滤 selftest 污染记录(selftest 曾把测试任务写进真实台账)
|
|
31
|
+
* - 活跃任务超过阈值未更新 → displayState=stalled(UI 不再显示无限 loading)
|
|
32
|
+
*/
|
|
30
33
|
export function listDelegations(limit = 50) {
|
|
31
34
|
const data = readJson(join(meshHome(), 'delegations.json'), { tasks: {} })
|
|
32
35
|
const localNode = readJson(join(meshHome(), 'config.json'), {})?.node?.name || ''
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
36
|
+
const ACTIVE_STATES = ['working', 'submitted', 'accepted', 'question', 'provisioning', 'running']
|
|
37
|
+
// 超过这些时间没有 updatedAt 更新,就认为“疑似失联”(不直接改文件,UI 只展示)
|
|
38
|
+
const STALE_AFTER_MS = {
|
|
39
|
+
submitted: 15 * 60 * 1000,
|
|
40
|
+
accepted: 15 * 60 * 1000,
|
|
41
|
+
provisioning: 15 * 60 * 1000,
|
|
42
|
+
working: 60 * 60 * 1000,
|
|
43
|
+
running: 60 * 60 * 1000,
|
|
44
|
+
question: 6 * 60 * 60 * 1000
|
|
45
|
+
}
|
|
46
|
+
const nowMs = Date.now()
|
|
47
|
+
const tasks = Object.values(data?.tasks || {})
|
|
48
|
+
.filter((task) => task && task.from !== 'selftest')
|
|
49
|
+
.map((task) => {
|
|
50
|
+
const start = Date.parse(task.createdAt || '')
|
|
51
|
+
const end = Date.parse(task.updatedAt || '') || nowMs
|
|
52
|
+
const from = task.from || ''
|
|
53
|
+
const state = task.state || 'unknown'
|
|
54
|
+
const isActive = ACTIVE_STATES.indexOf(state) >= 0
|
|
55
|
+
const updatedMs = Date.parse(task.updatedAt || task.createdAt || '')
|
|
56
|
+
const ageMs = Number.isFinite(updatedMs) ? nowMs - updatedMs : Infinity
|
|
57
|
+
const staleAfter = STALE_AFTER_MS[state] ?? 60 * 60 * 1000
|
|
58
|
+
const staleActive = isActive && ageMs > staleAfter
|
|
59
|
+
return {
|
|
60
|
+
taskId: task.taskId || '',
|
|
61
|
+
conversationId: task.conversationId || '',
|
|
62
|
+
sessionId: task.sessionId || '',
|
|
63
|
+
from,
|
|
64
|
+
fromLabel: nodeLabel(from),
|
|
65
|
+
fromIsLocal: Boolean(from) && from === localNode,
|
|
66
|
+
fromInstance: task.fromInstance || '',
|
|
67
|
+
node: task.node || '',
|
|
68
|
+
nodeLabel: nodeLabel(task.node),
|
|
69
|
+
toIsLocal: Boolean(task.node) && task.node === localNode,
|
|
70
|
+
instance: task.instance || '',
|
|
71
|
+
workspace: task.workspace || '',
|
|
72
|
+
dedicated: Boolean(task.dedicated),
|
|
73
|
+
state,
|
|
74
|
+
displayState: staleActive ? 'stalled' : state,
|
|
75
|
+
isActive,
|
|
76
|
+
staleActive,
|
|
77
|
+
staleAfterSec: Math.round(staleAfter / 1000),
|
|
78
|
+
activeFresh: isActive && !staleActive,
|
|
79
|
+
displayAt: task.finishedAt || task.updatedAt || '',
|
|
80
|
+
policy: task.requestedPolicy || task.policy || '',
|
|
81
|
+
goal: String(task.goal || ''),
|
|
82
|
+
createdAt: task.createdAt || '',
|
|
83
|
+
updatedAt: task.updatedAt || '',
|
|
84
|
+
durationSec: (() => {
|
|
85
|
+
if (!Number.isFinite(start)) return null
|
|
86
|
+
const value = Math.max(0, Math.round((end - start) / 1000))
|
|
87
|
+
return value > 12 * 3600 ? null : value // 台账 updatedAt 会被轮询刷新,超长值不可信
|
|
88
|
+
})(),
|
|
89
|
+
stalledReason: task.stalledReason || '',
|
|
90
|
+
lastSummary: task.summary || ''
|
|
91
|
+
}
|
|
92
|
+
})
|
|
70
93
|
tasks.sort((a, b) => {
|
|
71
94
|
if (a.activeFresh !== b.activeFresh) return a.activeFresh ? -1 : 1
|
|
72
95
|
return String(b.displayAt || b.updatedAt || '').localeCompare(String(a.displayAt || a.updatedAt || ''))
|
|
73
96
|
})
|
|
74
97
|
const byState = {}
|
|
75
|
-
for (const task of tasks) byState[task.
|
|
98
|
+
for (const task of tasks) byState[task.displayState] = (byState[task.displayState] || 0) + 1
|
|
76
99
|
return { total: tasks.length, byState, localNode, tasks: tasks.slice(0, Math.max(1, Math.min(limit, 200))) }
|
|
77
100
|
}
|
|
78
101
|
|
package/lib/index.js
CHANGED
|
@@ -176,6 +176,8 @@ export function apply(ctx) {
|
|
|
176
176
|
}),
|
|
177
177
|
readonlyRoute('/api/dsh-mesh/session', async (url) => {
|
|
178
178
|
const node = String(url.searchParams.get('node') || '')
|
|
179
|
+
// 2026-11-10:本机任务(node=local)没有对端会话,直接给出明确错误,避免 peer call --to local 报 unknown peer
|
|
180
|
+
if (/^(local|self|localhost)$/i.test(node)) return { ok: false, code: "E_LOCAL_NODE", error: "本机任务(local)没有对端会话;请在 DSH 侧栏查看本机会话" }
|
|
179
181
|
const sessionId = String(url.searchParams.get('sessionId') || '')
|
|
180
182
|
const limit = Number(url.searchParams.get('limit')) || 40
|
|
181
183
|
if (!node || !sessionId) return { ok: false, error: 'node and sessionId required' }
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mesh-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "DSH settings-page panel for dsh-mesh: AI network topology, peer health, and delegation panel. Read-only; data is collected from the local mesh CLI.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
8
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
9
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
9
10
|
},
|
|
10
11
|
"main": "lib/index.js",
|
|
11
12
|
"exports": {
|
|
@@ -14,9 +15,7 @@
|
|
|
14
15
|
"./package.json": "./package.json"
|
|
15
16
|
},
|
|
16
17
|
"dsh": {
|
|
17
|
-
"
|
|
18
|
-
"dsh": ">=0.2.0-rc.2"
|
|
19
|
-
},
|
|
18
|
+
"manifestVersion": 1,
|
|
20
19
|
"bundle": {
|
|
21
20
|
"patch": "./cordis.patch.yml"
|
|
22
21
|
},
|
|
@@ -29,6 +28,7 @@
|
|
|
29
28
|
}
|
|
30
29
|
},
|
|
31
30
|
"peerDependencies": {
|
|
31
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0",
|
|
32
32
|
"react": "^18.2.0",
|
|
33
33
|
"react-dom": "^18.2.0"
|
|
34
34
|
},
|