dsh-mesh-ui 0.0.14 → 0.0.15
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 +67 -0
- package/lib/index.js +79 -2
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -1475,6 +1475,61 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1475
1475
|
)
|
|
1476
1476
|
}
|
|
1477
1477
|
|
|
1478
|
+
// 2026-11-10:局域网首次接入(扫描 / 生成配对码 / 配对)。
|
|
1479
|
+
function LanOnboarding() {
|
|
1480
|
+
var sNodes = useState(null); var nodes = sNodes[0]; var setNodes = sNodes[1]
|
|
1481
|
+
var sBusy = useState(false); var busy = sBusy[0]; var setBusy = sBusy[1]
|
|
1482
|
+
var sMsg = useState(''); var msg = sMsg[0]; var setMsg = sMsg[1]
|
|
1483
|
+
var sCode = useState(''); var code = sCode[0]; var setCode = sCode[1]
|
|
1484
|
+
var sMine = useState(null); var mine = sMine[0]; var setMine = sMine[1]
|
|
1485
|
+
|
|
1486
|
+
function scan() {
|
|
1487
|
+
setBusy(true); setMsg('')
|
|
1488
|
+
fetch('/api/dsh-mesh/lan/scan').then(function (r) { return r.json() }).then(function (j) {
|
|
1489
|
+
if (!j.ok) { setMsg(j.error || '扫描失败'); return }
|
|
1490
|
+
setNodes(j.nodes || [])
|
|
1491
|
+
setMsg((j.nodes || []).length ? '发现 ' + j.nodes.length + ' 个节点' : '没有发现局域网节点(对方需 bind 0.0.0.0 并放行 8124)')
|
|
1492
|
+
}).catch(function (e) { setMsg(String(e.message || e)) }).finally(function () { setBusy(false) })
|
|
1493
|
+
}
|
|
1494
|
+
function myCode() {
|
|
1495
|
+
setBusy(true); setMine(null); setMsg('')
|
|
1496
|
+
fetch('/api/dsh-mesh/lan/code').then(function (r) { return r.json() }).then(function (j) {
|
|
1497
|
+
if (!j.ok) { setMsg(j.error || '生成配对码失败'); return }
|
|
1498
|
+
setMine(j)
|
|
1499
|
+
}).catch(function (e) { setMsg(String(e.message || e)) }).finally(function () { setBusy(false) })
|
|
1500
|
+
}
|
|
1501
|
+
function pairWith(node) {
|
|
1502
|
+
var c = String(code || '').trim()
|
|
1503
|
+
if (!c) { setMsg('先填入对方显示的配对码'); return }
|
|
1504
|
+
setBusy(true); setMsg('')
|
|
1505
|
+
fetch('/api/dsh-mesh/lan/pair', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ url: String(node.url || '').replace(/\/mcp$/, ''), code: c, name: (node.node || 'lan') + '-lan', fingerprint: node.certFingerprint || '' }) })
|
|
1506
|
+
.then(function (r) { return r.json() }).then(function (j) {
|
|
1507
|
+
setMsg(j.ok ? '配对成功:' + (node.node || '') + ',稍后刷新网络' : ('配对失败:' + (j.stderr || j.error || j.stdout || '').slice(0, 300)))
|
|
1508
|
+
}).catch(function (e) { setMsg(String(e.message || e)) }).finally(function () { setBusy(false) })
|
|
1509
|
+
}
|
|
1510
|
+
return h('div', null,
|
|
1511
|
+
h('div', { className: 'dm-lan-row' },
|
|
1512
|
+
h('button', { className: 'dm-btn', disabled: busy, onClick: scan }, busy ? '处理中…' : '扫描局域网'),
|
|
1513
|
+
h('button', { className: 'dm-btn', disabled: busy, onClick: myCode }, '生成我的配对码'),
|
|
1514
|
+
h('span', { className: 'dm-sub' }, '对方的 mesh 需 bind 0.0.0.0;第一次可能需放行 8124')
|
|
1515
|
+
),
|
|
1516
|
+
msg ? h('div', { className: 'dm-sub', style: { marginTop: 6 } }, msg) : null,
|
|
1517
|
+
mine ? h('div', { className: 'dm-lan-code', style: { marginTop: 8 } }, '配对码 ' + mine.pairingCode + ' · ' + (mine.endpoint || '') + (mine.certFingerprint ? ' · 指纹 ' + mine.certFingerprint : '')) : null,
|
|
1518
|
+
nodes && nodes.length ? h('div', { style: { marginTop: 8 } },
|
|
1519
|
+
nodes.map(function (node, i) {
|
|
1520
|
+
return h('div', { className: 'dm-lan-node', key: i },
|
|
1521
|
+
h('span', { className: 'dm-dot ok' }),
|
|
1522
|
+
h('span', null, (node.node || '?') + ' · ' + (node.url || '').replace('/mcp', '')),
|
|
1523
|
+
h('button', { className: 'dm-btn primary', disabled: busy, onClick: function () { pairWith(node) } }, '用此节点配对')
|
|
1524
|
+
)
|
|
1525
|
+
})
|
|
1526
|
+
) : null,
|
|
1527
|
+
nodes && nodes.length ? h('div', { className: 'dm-lan-row' },
|
|
1528
|
+
h('input', { placeholder: '对方显示的配对码,如 H5K4-9RXV', value: code, onChange: function (e) { setCode(e.target.value) } })
|
|
1529
|
+
) : null
|
|
1530
|
+
)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1478
1533
|
function MeshStatusSection() {
|
|
1479
1534
|
var s0 = useState(null); var data = s0[0]; var setData = s0[1]
|
|
1480
1535
|
var s1 = useState(true); var loading = s1[0]; var setLoading = s1[1]
|
|
@@ -1636,6 +1691,11 @@ function deviceLinkState(data, peer, lanMap) {
|
|
|
1636
1691
|
),
|
|
1637
1692
|
error ? h('div', { className: 'dm-err' }, error) : null,
|
|
1638
1693
|
|
|
1694
|
+
h('details', { className: 'dm-details dm-lan' },
|
|
1695
|
+
h('summary', null, '局域网发现 / 配对(首次使用)'),
|
|
1696
|
+
h(LanOnboarding)
|
|
1697
|
+
),
|
|
1698
|
+
|
|
1639
1699
|
h('div', { className: 'dm-body', style: splitW > 0 ? { '--dm-main-w': splitW + 'px' } : undefined },
|
|
1640
1700
|
h('div', { className: 'dm-pane' },
|
|
1641
1701
|
h(Viewport, { data: data, layout: computeLayout(1 + filterSelfPeers(dedupePeers(data.peers || []), data.nodeName).length), selectedKey: selectedKey, onSelect: setSelectedKey }),
|
|
@@ -2353,6 +2413,13 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
|
|
|
2353
2413
|
@media (hover:hover) { .dm-width-handle:hover::after { opacity:1; } }
|
|
2354
2414
|
@media (max-width: 1000px) { .dm-width-handle { display:none; } }
|
|
2355
2415
|
.dm-body { display:grid; grid-template-columns:minmax(300px, var(--dm-main-w, 62%)) 8px minmax(0,1fr); gap:10px; align-items:start; }
|
|
2416
|
+
.dm-lan { border:1px solid rgba(128,128,128,.25); border-radius:12px; padding:10px 12px; background:rgba(128,128,128,.035); }
|
|
2417
|
+
.dm-lan-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:8px; }
|
|
2418
|
+
.dm-lan input { font:inherit; font-size:12px; padding:5px 8px; border:1px solid rgba(128,128,128,.35); border-radius:7px; background:transparent; color:inherit; min-width:180px; }
|
|
2419
|
+
.dm-lan-node { display:flex; gap:8px; align-items:center; border-top:1px solid rgba(128,128,128,.16); padding:7px 0; font-size:12px; }
|
|
2420
|
+
.dm-lan-node:first-child { border-top:none; }
|
|
2421
|
+
.dm-lan-code { font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:12px; padding:6px 8px; border-radius:8px; background:rgba(88,166,255,.1); border:1px solid rgba(88,166,255,.35); word-break:break-all; }
|
|
2422
|
+
|
|
2356
2423
|
.dm-pane { min-width:0; display:flex; flex-direction:column; gap:12px; }
|
|
2357
2424
|
.dm-divider { cursor:col-resize; align-self:stretch; min-height:120px; border-radius:4px; position:relative; }
|
|
2358
2425
|
.dm-divider::before { content:''; position:absolute; top:0; bottom:0; left:3px; width:1px; background:rgba(128,128,128,.28); }
|
package/lib/index.js
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
//
|
|
12
12
|
// 归宿:reusable(dsh-mesh/plugin/dsh-mesh-ui/lib/)
|
|
13
13
|
|
|
14
|
-
import { collect, resolveRepo, meshHome } from './collect.js'
|
|
14
|
+
import { collect, resolveRepo, meshHome, run, nodeExec } from './collect.js'
|
|
15
15
|
import { listDelegations, sessionHistory, taskStatus } from './delegations.js'
|
|
16
|
+
import { networkInterfaces } from 'node:os'
|
|
17
|
+
import { join } from 'node:path'
|
|
16
18
|
|
|
17
19
|
/** Cordis inject 列表:只需要 webServer。 */
|
|
18
20
|
export const inject = ['webServer']
|
|
@@ -37,6 +39,32 @@ function writeJson(res, status, body) {
|
|
|
37
39
|
res.end(JSON.stringify(body, null, 2))
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function readBody(req, limit = 16 * 1024) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
let size = 0
|
|
45
|
+
const chunks = []
|
|
46
|
+
req.on('data', (chunk) => {
|
|
47
|
+
size += chunk.length
|
|
48
|
+
if (size > limit) { resolve(null); req.destroy(); return }
|
|
49
|
+
chunks.push(chunk)
|
|
50
|
+
})
|
|
51
|
+
req.on('end', () => {
|
|
52
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')) } catch { resolve(null) }
|
|
53
|
+
})
|
|
54
|
+
req.on('error', () => resolve(null))
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function lanIPv4() {
|
|
59
|
+
const ifaces = networkInterfaces()
|
|
60
|
+
for (const list of Object.values(ifaces)) {
|
|
61
|
+
for (const item of list || []) {
|
|
62
|
+
if (item.family === 'IPv4' && !item.internal && !/^169\.254\./.test(item.address)) return item.address
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return '127.0.0.1'
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
/**
|
|
41
69
|
* Cordis apply:注册只读状态路由 + 后台刷新循环。
|
|
42
70
|
* @param {import('@deepseek-ai/cordis').Context} ctx host 插件上下文(已注入 webServer)
|
|
@@ -158,6 +186,54 @@ export function apply(ctx) {
|
|
|
158
186
|
}
|
|
159
187
|
}
|
|
160
188
|
|
|
189
|
+
async function runCli(args, timeout = 30_000) {
|
|
190
|
+
if (!state.repo) return { error: 'dsh-mesh-node is not installed', stdout: '', stderr: '' }
|
|
191
|
+
return run(nodeExec(), [join(state.repo, 'src', 'cli.js'), ...args], { cwd: state.repo, timeout })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const lanScanRoute = readonlyRoute('/api/dsh-mesh/lan/scan', async () => {
|
|
195
|
+
const r = await runCli(['discover', '--json', '--timeout-ms', '1500'], 20_000)
|
|
196
|
+
if (r.error) return { ok: false, error: r.error, stderr: r.stderr }
|
|
197
|
+
try {
|
|
198
|
+
const data = JSON.parse(r.stdout)
|
|
199
|
+
return { ok: true, localAddress: lanIPv4(), nodes: data.nodes || [] }
|
|
200
|
+
} catch {
|
|
201
|
+
return { ok: false, error: 'invalid discover output', stdout: r.stdout, stderr: r.stderr }
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const lanCodeRoute = readonlyRoute('/api/dsh-mesh/lan/code', async () => {
|
|
206
|
+
const host = `${lanIPv4()}:8124`
|
|
207
|
+
const r = await runCli(['pair', '--code', '--ttl', '600', '--role', 'node', '--host', host])
|
|
208
|
+
if (r.error) return { ok: false, error: r.error, stderr: r.stderr }
|
|
209
|
+
try {
|
|
210
|
+
return { ok: true, ...JSON.parse(r.stdout) }
|
|
211
|
+
} catch {
|
|
212
|
+
return { ok: false, error: 'invalid pair output', stdout: r.stdout, stderr: r.stderr }
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const lanPairRoute = {
|
|
217
|
+
kind: 'exact',
|
|
218
|
+
path: '/api/dsh-mesh/lan/pair',
|
|
219
|
+
handler: async (req, res) => {
|
|
220
|
+
if (!isLoopback(req)) return writeJson(res, 403, { ok: false, error: 'forbidden: loopback-only' })
|
|
221
|
+
if ((req.method || 'GET').toUpperCase() !== 'POST') return writeJson(res, 405, { ok: false, error: 'method not allowed' })
|
|
222
|
+
const body = await readBody(req)
|
|
223
|
+
if (!body || typeof body.url !== 'string' || typeof body.code !== 'string') {
|
|
224
|
+
return writeJson(res, 400, { ok: false, error: 'url and code required' })
|
|
225
|
+
}
|
|
226
|
+
const args = ['peer', 'add', '--url', String(body.url), '--code', String(body.code), '--name', String(body.name || 'lan-peer')]
|
|
227
|
+
if (body.fingerprint) args.push('--fingerprint', String(body.fingerprint))
|
|
228
|
+
try {
|
|
229
|
+
const r = await runCli(args)
|
|
230
|
+
writeJson(res, 200, { ok: !r.error, error: r.error, stdout: r.stdout, stderr: r.stderr })
|
|
231
|
+
} catch (error) {
|
|
232
|
+
writeJson(res, 500, { ok: false, error: String(error?.message || error) })
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
161
237
|
const delegateRoutes = [
|
|
162
238
|
readonlyRoute('/api/dsh-mesh/delegations', async (url) => {
|
|
163
239
|
// 缓存整份列表(与请求的 limit 无关),避免不同 limit 的请求互相污染缓存
|
|
@@ -187,6 +263,7 @@ export function apply(ctx) {
|
|
|
187
263
|
|
|
188
264
|
ctx.effect(() => {
|
|
189
265
|
const disposeRoute = ctx.webServer.register(route)
|
|
266
|
+
const disposeLanRoutes = [lanScanRoute, lanCodeRoute, lanPairRoute].map((item) => ctx.webServer.register(item))
|
|
190
267
|
const disposeRoutes = delegateRoutes.map((item) => ctx.webServer.register(item))
|
|
191
268
|
return () => {
|
|
192
269
|
try {
|
|
@@ -194,7 +271,7 @@ export function apply(ctx) {
|
|
|
194
271
|
} catch {
|
|
195
272
|
/* ignore */
|
|
196
273
|
}
|
|
197
|
-
for (const dispose of disposeRoutes) { try { dispose() } catch { /* ignore */ } }
|
|
274
|
+
for (const dispose of disposeLanRoutes.concat(disposeRoutes)) { try { dispose() } catch { /* ignore */ } }
|
|
198
275
|
if (typeof disposeRoute === 'function') disposeRoute()
|
|
199
276
|
}
|
|
200
277
|
}, 'dsh-mesh-ui: GET /api/dsh-mesh/status + background refresh')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mesh-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "DSH sidebar page for dsh-mesh: AI network topology, peer health, and delegation panel. Read-only; data is collected from the installed dsh-mesh-node package.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"dsh-mesh-node": "0.0.
|
|
31
|
+
"dsh-mesh-node": "0.0.13"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0",
|