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/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
- const REFRESH_MS = 30_000
23
- const LATENCY_HISTORY_MAX = 30
24
- const REQUEST_REFRESH_COOLDOWN_MS = 8_000
25
-
26
- function isLoopback(req) {
27
- const raw = req?.socket?.remoteAddress || ''
28
- const addr = String(raw).replace(/^::ffff:/, '')
29
- return addr === '127.0.0.1' || addr === '::1' || addr === 'localhost' || addr.startsWith('127.')
30
- }
31
-
32
- function writeJson(res, status, body) {
33
- res.statusCode = status
34
- res.setHeader('content-type', 'application/json; charset=utf-8')
35
- res.setHeader('cache-control', 'no-store')
36
- res.end(JSON.stringify(body, null, 2))
37
- }
38
-
39
- /**
40
- * Cordis apply:注册只读状态路由 + 后台刷新循环。
41
- * @param {import('@deepseek-ai/cordis').Context} ctx host 插件上下文(已注入 webServer)
42
- */
43
- export function apply(ctx) {
44
- const repo = resolveRepo()
45
- const state = {
46
- repo,
47
- snapshot: {
48
- ok: false,
49
- warming: Boolean(repo),
50
- at: null,
51
- node: process.env.COMPUTERNAME || process.env.HOSTNAME || '',
52
- meshHome: meshHome(),
53
- nodeVersion: process.version,
54
- repo,
55
- error: repo ? 'warming up (first background refresh pending)' : 'dsh-mesh repo not found (set DSH_MESH_REPO or clone to ~/Code/dsh-mesh)'
56
- },
57
- refreshing: false,
58
- lastStartedAt: 0,
59
- latencyHistory: new Map()
60
- }
61
-
62
- /** 记录每个 peer 的延迟小趋势(在内存里滚动,不落盘)。 */
63
- function rememberLatency(peers) {
64
- for (const peer of peers || []) {
65
- const history = state.latencyHistory.get(peer.name) || []
66
- history.push(peer.up ? peer.latencyMs : null)
67
- while (history.length > LATENCY_HISTORY_MAX) history.shift()
68
- state.latencyHistory.set(peer.name, history)
69
- }
70
- return (peers || []).map((peer) => ({ ...peer, latencyHistory: state.latencyHistory.get(peer.name) || [] }))
71
- }
72
-
73
- async function refresh() {
74
- if (state.refreshing || !state.repo) return
75
- state.refreshing = true
76
- state.lastStartedAt = Date.now()
77
- try {
78
- const data = await collect(state.repo)
79
- state.snapshot = {
80
- ...data,
81
- warming: false,
82
- at: data.generatedAt || new Date().toISOString(),
83
- latencyMs: Date.now() - state.lastStartedAt,
84
- node: process.env.COMPUTERNAME || process.env.HOSTNAME || '',
85
- meshHome: meshHome(),
86
- nodeVersion: process.version,
87
- peers: data.ok ? rememberLatency(data.peers) : data.peers || []
88
- }
89
- } catch (error) {
90
- state.snapshot = { ...state.snapshot, ok: false, warming: false, at: new Date().toISOString(), error: String(error?.message || error) }
91
- } finally {
92
- state.refreshing = false
93
- }
94
- }
95
-
96
- const timer = setInterval(() => {
97
- refresh().catch(() => {})
98
- }, REFRESH_MS)
99
- if (typeof timer.unref === 'function') timer.unref()
100
- refresh().catch(() => {})
101
-
102
- // ---- 委托面板只读路由(2026-11-07 方案 A)----
103
- const delegationCache = { at: 0, data: null }
104
- const taskCache = new Map()
105
- const sessionCache = new Map()
106
- function cached(taskIdKey, map, ttlMs, producer) {
107
- const hit = map.get(taskIdKey)
108
- if (hit && Date.now() - hit.at < ttlMs) return hit.data
109
- const data = producer()
110
- 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)
111
- return settled
112
- }
113
-
114
- const route = {
115
- kind: 'exact',
116
- path: '/api/dsh-mesh/status',
117
- handler: async (req, res) => {
118
- if (!isLoopback(req)) {
119
- writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
120
- return
121
- }
122
- if ((req.method || 'GET').toUpperCase() !== 'GET') {
123
- writeJson(res, 405, { ok: false, error: 'method not allowed' })
124
- return
125
- }
126
- try {
127
- const url = new URL(req.url || '/', 'http://localhost')
128
- if (url.searchParams.get('force') === '1' && Date.now() - state.lastStartedAt > REQUEST_REFRESH_COOLDOWN_MS) {
129
- refresh().catch(() => {})
130
- }
131
- writeJson(res, 200, {
132
- ...state.snapshot,
133
- refreshing: state.refreshing,
134
- snapshotAgeSeconds: state.snapshot.at ? Math.round((Date.now() - Date.parse(state.snapshot.at)) / 1000) : null
135
- })
136
- } catch (error) {
137
- writeJson(res, 500, { ok: false, error: String(error?.message || error) })
138
- }
139
- }
140
- }
141
-
142
- /** 统一 GET 只读处理器。 */
143
- function readonlyRoute(path, handler) {
144
- return {
145
- kind: 'exact',
146
- path,
147
- handler: async (req, res) => {
148
- if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
149
- if ((req.method || 'GET').toUpperCase() !== 'GET') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
150
- try {
151
- const url = new URL(req.url || '/', 'http://localhost')
152
- writeJson(res, 200, await handler(url))
153
- } catch (error) {
154
- writeJson(res, 500, { ok: false, error: String(error?.message || error) })
155
- }
156
- }
157
- }
158
- }
159
-
160
- const delegateRoutes = [
161
- readonlyRoute('/api/dsh-mesh/delegations', async (url) => {
162
- // 缓存整份列表(与请求的 limit 无关),避免不同 limit 的请求互相污染缓存
163
- if (Date.now() - delegationCache.at > 15_000) {
164
- delegationCache.at = Date.now()
165
- delegationCache.data = listDelegations(200)
166
- }
167
- const data = delegationCache.data
168
- const limit = Math.max(1, Math.min(Number(url.searchParams.get('limit')) || 100, 200))
169
- return { ok: true, total: data.total, byState: data.byState, localNode: data.localNode, tasks: data.tasks.slice(0, limit) }
170
- }),
171
- readonlyRoute('/api/dsh-mesh/task', async (url) => {
172
- const taskId = String(url.searchParams.get('taskId') || '')
173
- if (!taskId) return { ok: false, error: 'taskId required' }
174
- return cached('task:' + taskId, taskCache, 60_000, () => taskStatus(taskId))
175
- }),
176
- readonlyRoute('/api/dsh-mesh/session', async (url) => {
177
- const node = String(url.searchParams.get('node') || '')
178
- const sessionId = String(url.searchParams.get('sessionId') || '')
179
- const limit = Number(url.searchParams.get('limit')) || 40
180
- if (!node || !sessionId) return { ok: false, error: 'node and sessionId required' }
181
- return cached('sess:' + node + ':' + sessionId + ':' + limit, sessionCache, 120_000, () => sessionHistory(node, sessionId, limit))
182
- })
183
- ]
184
-
185
- ctx.effect(() => {
186
- const disposeRoute = ctx.webServer.register(route)
187
- const disposeRoutes = delegateRoutes.map((item) => ctx.webServer.register(item))
188
- return () => {
189
- try {
190
- clearInterval(timer)
191
- } catch {
192
- /* ignore */
193
- }
194
- for (const dispose of disposeRoutes) { try { dispose() } catch { /* ignore */ } }
195
- if (typeof disposeRoute === 'function') disposeRoute()
196
- }
197
- }, 'dsh-mesh-ui: GET /api/dsh-mesh/status + background refresh')
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.1",
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",