dsh-mesh-ui 0.0.1 → 0.0.3
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 +752 -172
- package/lib/collect.js +512 -457
- package/lib/delegations.js +240 -233
- package/lib/index.js +199 -198
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,198 +1,199 @@
|
|
|
1
|
-
// dsh-mesh-ui — 服务端半(host 进程)
|
|
2
|
-
//
|
|
3
|
-
// 注册只读路由 GET /api/dsh-mesh/status(设置页「AI 网络」用)+ 后台刷新循环。
|
|
4
|
-
// 数据由 lib/collect.js 采集(mesh-health / host status / peer host_self / 台账 / 告警历史)。
|
|
5
|
-
//
|
|
6
|
-
// 2026-11-06 修正(三台实测死锁):v1 在 request handler 里 spawnSync 跑 mesh-health,
|
|
7
|
-
// 同步阻塞 DSH host 事件循环,而 mesh-health 要探测同进程的 host bridge(9100/9101)→ 自死锁 20s。
|
|
8
|
-
// 现在:后台 execFile 异步刷新 + 内存缓存,handler 只读缓存(0ms,不阻塞)。
|
|
9
|
-
//
|
|
10
|
-
// 安全:仅 loopback;不含 token/凭据;纯只读(只读文件 + 只调只读 CLI)。
|
|
11
|
-
//
|
|
12
|
-
// 归宿:reusable(dsh-mesh/plugin/dsh-mesh-ui/lib/)
|
|
13
|
-
|
|
14
|
-
import { collect, resolveRepo, meshHome } from './collect.js'
|
|
15
|
-
import { listDelegations, sessionHistory, taskStatus } from './delegations.js'
|
|
16
|
-
|
|
17
|
-
/** Cordis inject 列表:只需要 webServer。 */
|
|
18
|
-
export const inject = ['webServer']
|
|
19
|
-
|
|
20
|
-
export const name = 'dsh-mesh-ui'
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
res.
|
|
35
|
-
res.setHeader('
|
|
36
|
-
res.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
history.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
state.refreshing
|
|
76
|
-
state.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if ((req
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
delegationCache.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
1
|
+
// dsh-mesh-ui — 服务端半(host 进程)
|
|
2
|
+
//
|
|
3
|
+
// 注册只读路由 GET /api/dsh-mesh/status(设置页「AI 网络」用)+ 后台刷新循环。
|
|
4
|
+
// 数据由 lib/collect.js 采集(mesh-health / host status / peer host_self / 台账 / 告警历史)。
|
|
5
|
+
//
|
|
6
|
+
// 2026-11-06 修正(三台实测死锁):v1 在 request handler 里 spawnSync 跑 mesh-health,
|
|
7
|
+
// 同步阻塞 DSH host 事件循环,而 mesh-health 要探测同进程的 host bridge(9100/9101)→ 自死锁 20s。
|
|
8
|
+
// 现在:后台 execFile 异步刷新 + 内存缓存,handler 只读缓存(0ms,不阻塞)。
|
|
9
|
+
//
|
|
10
|
+
// 安全:仅 loopback;不含 token/凭据;纯只读(只读文件 + 只调只读 CLI)。
|
|
11
|
+
//
|
|
12
|
+
// 归宿:reusable(dsh-mesh/plugin/dsh-mesh-ui/lib/)
|
|
13
|
+
|
|
14
|
+
import { collect, resolveRepo, meshHome } from './collect.js'
|
|
15
|
+
import { listDelegations, sessionHistory, taskStatus } from './delegations.js'
|
|
16
|
+
|
|
17
|
+
/** Cordis inject 列表:只需要 webServer。 */
|
|
18
|
+
export const inject = ['webServer']
|
|
19
|
+
|
|
20
|
+
export const name = 'dsh-mesh-ui'
|
|
21
|
+
|
|
22
|
+
// 2026-11-09:默认 10s(可用 DSH_MESH_UI_REFRESH_MS 覆盖);隧道/节点变化更快可见
|
|
23
|
+
const REFRESH_MS = Math.max(3_000, Number(process.env.DSH_MESH_UI_REFRESH_MS || 5_000))
|
|
24
|
+
const LATENCY_HISTORY_MAX = 30
|
|
25
|
+
const REQUEST_REFRESH_COOLDOWN_MS = 8_000
|
|
26
|
+
|
|
27
|
+
function isLoopback(req) {
|
|
28
|
+
const raw = req?.socket?.remoteAddress || ''
|
|
29
|
+
const addr = String(raw).replace(/^::ffff:/, '')
|
|
30
|
+
return addr === '127.0.0.1' || addr === '::1' || addr === 'localhost' || addr.startsWith('127.')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeJson(res, status, body) {
|
|
34
|
+
res.statusCode = status
|
|
35
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
36
|
+
res.setHeader('cache-control', 'no-store')
|
|
37
|
+
res.end(JSON.stringify(body, null, 2))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Cordis apply:注册只读状态路由 + 后台刷新循环。
|
|
42
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx host 插件上下文(已注入 webServer)
|
|
43
|
+
*/
|
|
44
|
+
export function apply(ctx) {
|
|
45
|
+
const repo = resolveRepo()
|
|
46
|
+
const state = {
|
|
47
|
+
repo,
|
|
48
|
+
snapshot: {
|
|
49
|
+
ok: false,
|
|
50
|
+
warming: Boolean(repo),
|
|
51
|
+
at: null,
|
|
52
|
+
node: process.env.COMPUTERNAME || process.env.HOSTNAME || '',
|
|
53
|
+
meshHome: meshHome(),
|
|
54
|
+
nodeVersion: process.version,
|
|
55
|
+
repo,
|
|
56
|
+
error: repo ? 'warming up (first background refresh pending)' : 'dsh-mesh repo not found (set DSH_MESH_REPO or clone to ~/Code/dsh-mesh)'
|
|
57
|
+
},
|
|
58
|
+
refreshing: false,
|
|
59
|
+
lastStartedAt: 0,
|
|
60
|
+
latencyHistory: new Map()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 记录每个 peer 的延迟小趋势(在内存里滚动,不落盘)。 */
|
|
64
|
+
function rememberLatency(peers) {
|
|
65
|
+
for (const peer of peers || []) {
|
|
66
|
+
const history = state.latencyHistory.get(peer.name) || []
|
|
67
|
+
history.push(peer.up ? peer.latencyMs : null)
|
|
68
|
+
while (history.length > LATENCY_HISTORY_MAX) history.shift()
|
|
69
|
+
state.latencyHistory.set(peer.name, history)
|
|
70
|
+
}
|
|
71
|
+
return (peers || []).map((peer) => ({ ...peer, latencyHistory: state.latencyHistory.get(peer.name) || [] }))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function refresh() {
|
|
75
|
+
if (state.refreshing || !state.repo) return
|
|
76
|
+
state.refreshing = true
|
|
77
|
+
state.lastStartedAt = Date.now()
|
|
78
|
+
try {
|
|
79
|
+
const data = await collect(state.repo)
|
|
80
|
+
state.snapshot = {
|
|
81
|
+
...data,
|
|
82
|
+
warming: false,
|
|
83
|
+
at: data.generatedAt || new Date().toISOString(),
|
|
84
|
+
latencyMs: Date.now() - state.lastStartedAt,
|
|
85
|
+
node: process.env.COMPUTERNAME || process.env.HOSTNAME || '',
|
|
86
|
+
meshHome: meshHome(),
|
|
87
|
+
nodeVersion: process.version,
|
|
88
|
+
peers: data.ok ? rememberLatency(data.peers) : data.peers || []
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
state.snapshot = { ...state.snapshot, ok: false, warming: false, at: new Date().toISOString(), error: String(error?.message || error) }
|
|
92
|
+
} finally {
|
|
93
|
+
state.refreshing = false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const timer = setInterval(() => {
|
|
98
|
+
refresh().catch(() => {})
|
|
99
|
+
}, REFRESH_MS)
|
|
100
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
101
|
+
refresh().catch(() => {})
|
|
102
|
+
|
|
103
|
+
// ---- 委托面板只读路由(2026-11-07 方案 A)----
|
|
104
|
+
const delegationCache = { at: 0, data: null }
|
|
105
|
+
const taskCache = new Map()
|
|
106
|
+
const sessionCache = new Map()
|
|
107
|
+
function cached(taskIdKey, map, ttlMs, producer) {
|
|
108
|
+
const hit = map.get(taskIdKey)
|
|
109
|
+
if (hit && Date.now() - hit.at < ttlMs) return hit.data
|
|
110
|
+
const data = producer()
|
|
111
|
+
const settled = data instanceof Promise ? data.then((value) => { map.set(taskIdKey, { at: Date.now(), data: value }); return value }) : (map.set(taskIdKey, { at: Date.now(), data }), data)
|
|
112
|
+
return settled
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const route = {
|
|
116
|
+
kind: 'exact',
|
|
117
|
+
path: '/api/dsh-mesh/status',
|
|
118
|
+
handler: async (req, res) => {
|
|
119
|
+
if (!isLoopback(req)) {
|
|
120
|
+
writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') {
|
|
124
|
+
writeJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const url = new URL(req.url || '/', 'http://localhost')
|
|
129
|
+
if (url.searchParams.get('force') === '1' && Date.now() - state.lastStartedAt > REQUEST_REFRESH_COOLDOWN_MS) {
|
|
130
|
+
refresh().catch(() => {})
|
|
131
|
+
}
|
|
132
|
+
writeJson(res, 200, {
|
|
133
|
+
...state.snapshot,
|
|
134
|
+
refreshing: state.refreshing,
|
|
135
|
+
snapshotAgeSeconds: state.snapshot.at ? Math.round((Date.now() - Date.parse(state.snapshot.at)) / 1000) : null
|
|
136
|
+
})
|
|
137
|
+
} catch (error) {
|
|
138
|
+
writeJson(res, 500, { ok: false, error: String(error?.message || error) })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 统一 GET 只读处理器。 */
|
|
144
|
+
function readonlyRoute(path, handler) {
|
|
145
|
+
return {
|
|
146
|
+
kind: 'exact',
|
|
147
|
+
path,
|
|
148
|
+
handler: async (req, res) => {
|
|
149
|
+
if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
|
|
150
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
151
|
+
try {
|
|
152
|
+
const url = new URL(req.url || '/', 'http://localhost')
|
|
153
|
+
writeJson(res, 200, await handler(url))
|
|
154
|
+
} catch (error) {
|
|
155
|
+
writeJson(res, 500, { ok: false, error: String(error?.message || error) })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const delegateRoutes = [
|
|
162
|
+
readonlyRoute('/api/dsh-mesh/delegations', async (url) => {
|
|
163
|
+
// 缓存整份列表(与请求的 limit 无关),避免不同 limit 的请求互相污染缓存
|
|
164
|
+
if (Date.now() - delegationCache.at > 15_000) {
|
|
165
|
+
delegationCache.at = Date.now()
|
|
166
|
+
delegationCache.data = listDelegations(200)
|
|
167
|
+
}
|
|
168
|
+
const data = delegationCache.data
|
|
169
|
+
const limit = Math.max(1, Math.min(Number(url.searchParams.get('limit')) || 100, 200))
|
|
170
|
+
return { ok: true, total: data.total, byState: data.byState, localNode: data.localNode, tasks: data.tasks.slice(0, limit) }
|
|
171
|
+
}),
|
|
172
|
+
readonlyRoute('/api/dsh-mesh/task', async (url) => {
|
|
173
|
+
const taskId = String(url.searchParams.get('taskId') || '')
|
|
174
|
+
if (!taskId) return { ok: false, error: 'taskId required' }
|
|
175
|
+
return cached('task:' + taskId, taskCache, 60_000, () => taskStatus(taskId))
|
|
176
|
+
}),
|
|
177
|
+
readonlyRoute('/api/dsh-mesh/session', async (url) => {
|
|
178
|
+
const node = String(url.searchParams.get('node') || '')
|
|
179
|
+
const sessionId = String(url.searchParams.get('sessionId') || '')
|
|
180
|
+
const limit = Number(url.searchParams.get('limit')) || 40
|
|
181
|
+
if (!node || !sessionId) return { ok: false, error: 'node and sessionId required' }
|
|
182
|
+
return cached('sess:' + node + ':' + sessionId + ':' + limit, sessionCache, 120_000, () => sessionHistory(node, sessionId, limit))
|
|
183
|
+
})
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
ctx.effect(() => {
|
|
187
|
+
const disposeRoute = ctx.webServer.register(route)
|
|
188
|
+
const disposeRoutes = delegateRoutes.map((item) => ctx.webServer.register(item))
|
|
189
|
+
return () => {
|
|
190
|
+
try {
|
|
191
|
+
clearInterval(timer)
|
|
192
|
+
} catch {
|
|
193
|
+
/* ignore */
|
|
194
|
+
}
|
|
195
|
+
for (const dispose of disposeRoutes) { try { dispose() } catch { /* ignore */ } }
|
|
196
|
+
if (typeof disposeRoute === 'function') disposeRoute()
|
|
197
|
+
}
|
|
198
|
+
}, 'dsh-mesh-ui: GET /api/dsh-mesh/status + background refresh')
|
|
199
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mesh-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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",
|