dsh-mesh-ui 0.0.11 → 0.0.13

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 CHANGED
@@ -678,7 +678,7 @@ body.light .dm-edge-label { stroke: rgba(255,255,255,.92); }
678
678
  h('span', null, '⋯')
679
679
  )
680
680
  ),
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
+ h('div', { className: 'dm-hint' }, '拓扑 v23 · 内容 ' + Math.round(layout.W) + '×' + Math.round(layout.H) + 'px · 卡 ' + Math.round(layout.cardW) + '×' + Math.round(layout.cardH) + ' · 缩放 ' + Math.round(view.scale * 100) + '% · 滚轮缩放 / 拖拽平移')
682
682
  )
683
683
  }
684
684
 
@@ -1453,7 +1453,40 @@ function deviceLinkState(data, peer, lanMap) {
1453
1453
  // 2026-11-10:真刷新 —— 点击后等待后台采集完成再渲染;本地冷却 + 忙锁防止连点刷爆请求
1454
1454
  var s6 = useState(""); var refreshHint = s6[0]; var setRefreshHint = s6[1]
1455
1455
  var forceRef = useRef({ at: 0, busy: false })
1456
+ // 2026-11-10:verdict 防抖 —— 单次探测失败(远端忙/超时)只显示 degraded,连续 2 次才显示 down
1457
+ var verdictState = useState(''); var verdictShown = verdictState[0]; var setVerdictShown = verdictState[1]
1458
+ var verdictRef = useRef({ raw: '', streak: 0 })
1456
1459
  var s5 = useState('local'); var selectedKey = s5[0]; var setSelectedKey = s5[1]
1460
+ // 双栏宽度:拖动分隔条调整并记忆,窄屏由 CSS 媒体查询自动堆叠。
1461
+ var dragState = useState(function () {
1462
+ try { var saved = Number(window.localStorage.getItem('dsh-mesh-split-w')); return Number.isFinite(saved) && saved > 0 ? saved : 0 } catch (error) { return 0 }
1463
+ })
1464
+ var splitW = dragState[0]; var setSplitW = dragState[1]
1465
+
1466
+ function startSplit(event) {
1467
+ event.preventDefault()
1468
+ var body = event.currentTarget.parentElement
1469
+ if (!body) return
1470
+ var rect = body.getBoundingClientRect()
1471
+ var startW = splitW > 0 ? splitW : Math.round(rect.width * 0.62)
1472
+ var currentW = startW
1473
+ function onMove(moveEvent) {
1474
+ var width = Math.max(300, Math.min(moveEvent.clientX - rect.left, Math.max(300, rect.width - 360)))
1475
+ currentW = width
1476
+ setSplitW(width)
1477
+ }
1478
+ function onUp() {
1479
+ window.removeEventListener('pointermove', onMove)
1480
+ window.removeEventListener('pointerup', onUp)
1481
+ document.body.style.cursor = ''
1482
+ document.body.style.userSelect = ''
1483
+ try { window.localStorage.setItem('dsh-mesh-split-w', String(currentW)) } catch (error) { /* ignore */ }
1484
+ }
1485
+ document.body.style.cursor = 'col-resize'
1486
+ document.body.style.userSelect = 'none'
1487
+ window.addEventListener('pointermove', onMove)
1488
+ window.addEventListener('pointerup', onUp)
1489
+ }
1457
1490
 
1458
1491
  function load(force) {
1459
1492
  if (!force) {
@@ -1515,6 +1548,24 @@ function deviceLinkState(data, peer, lanMap) {
1515
1548
  }, [])
1516
1549
 
1517
1550
  useEffect(function () { if (tick > 0 && !forceRef.current.busy) load(false) }, [tick])
1551
+ useEffect(function () {
1552
+ var raw = String((data && data.verdict) || '')
1553
+ if (!raw) return
1554
+ var st = verdictRef.current
1555
+ var at = String((data && data.at) || '')
1556
+ var fresh = Boolean(at) && at !== st.lastAt
1557
+ if (raw === 'down') {
1558
+ if (fresh && !st.downAt) st.downAt = Date.now()
1559
+ st.lastAt = at
1560
+ var persisted = Boolean(st.downAt) && Date.now() - st.downAt >= 15000
1561
+ setVerdictShown(persisted ? 'down' : 'degraded')
1562
+ } else {
1563
+ st.downAt = 0
1564
+ st.lastAt = at
1565
+ setVerdictShown(raw)
1566
+ }
1567
+ st.raw = raw
1568
+ }, [data])
1518
1569
 
1519
1570
  function copyJson() {
1520
1571
  var text = JSON.stringify(data || {}, null, 2)
@@ -1540,7 +1591,11 @@ function deviceLinkState(data, peer, lanMap) {
1540
1591
  h('style', null, CSS),
1541
1592
  h('div', { className: 'dm-head' },
1542
1593
  h('span', { className: 'dm-title' }, 'AI 网络 · dsh-mesh'),
1543
- h('span', { className: 'dm-pill ' + (data.warming ? 'muted' : tone(data.verdict)) }, data.warming ? '预热中' : (data.verdict || '?')),
1594
+ h('span', {
1595
+ className: 'dm-pill ' + (data.warming ? 'muted' : tone(verdictShown || data.verdict)),
1596
+ title: data.warming ? '' : (verdictShown === 'degraded' ? '探测波动:需持续 ≥15s 且新快照仍 down 才显示 down' : ''),
1597
+ style: verdictShown === 'degraded' ? { opacity: 0.75 } : undefined
1598
+ }, data.warming ? '预热中' : (verdictShown || data.verdict || '?')),
1544
1599
  h('span', { className: 'dm-sub' }, (data.meshVersion ? 'mesh ' + data.meshVersion + ' · ' : '') + (data.at ? ago(data.at) : '')),
1545
1600
  h('span', { style: { flex: 1 } }),
1546
1601
  h('a', { className: 'dm-btn', href: API, target: '_blank', rel: 'noreferrer', style: { textDecoration: 'none' } }, '原始 JSON'),
@@ -1550,23 +1605,30 @@ function deviceLinkState(data, peer, lanMap) {
1550
1605
  ),
1551
1606
  error ? h('div', { className: 'dm-err' }, error) : null,
1552
1607
 
1553
- h(Viewport, { data: data, layout: computeLayout(1 + filterSelfPeers(dedupePeers(data.peers || []), data.nodeName).length), selectedKey: selectedKey, onSelect: setSelectedKey }),
1554
- h(InfraPanels, { data: data }),
1555
- h(DeviceDetails, { data: data, selectedKey: selectedKey }),
1556
- h(Details, { title: '本机实例', badge: ((data.local && data.local.instances) || []).length, open: true }, h(InstanceList, { data: data })),
1557
- h(Details, { title: '版本一致性', badge: versions.length ? versions.length + ' 处升级' : '一致' },
1558
- versions.length
1559
- ? versions.map(function (item, i) { return h('div', { key: i, className: 'dm-sub', style: { marginTop: 4 } }, '· ' + (typeof item === 'string' ? item : JSON.stringify(item))) })
1560
- : h('div', { className: 'dm-empty' }, '所有节点/实例版本一致')
1561
- ),
1562
- h(Details, { title: '委托台账', badge: (data.delegations && data.delegations.total) || 0 }, h(DelegationTable, { data: data })),
1563
- h(Details, { title: '告警历史', badge: ((data.alerts && data.alerts.history) || []).length }, h(AlertList, { data: data })),
1564
- (issues.length || hints.length) ? h(Details, { title: '诊断建议', badge: issues.length + hints.length },
1565
- h('div', null,
1566
- issues.slice(0, 6).map(function (issue, i) { return h('div', { key: 'i' + i, className: 'dm-err', style: { marginTop: 3 } }, '· ' + issue) }),
1567
- hints.slice(0, 6).map(function (hint, i) { return h('div', { key: 'h' + i, className: 'dm-sub', style: { marginTop: 3 } }, '· ' + hint) })
1608
+ h('div', { className: 'dm-body', style: splitW > 0 ? { '--dm-main-w': splitW + 'px' } : undefined },
1609
+ h('div', { className: 'dm-pane' },
1610
+ h(Viewport, { data: data, layout: computeLayout(1 + filterSelfPeers(dedupePeers(data.peers || []), data.nodeName).length), selectedKey: selectedKey, onSelect: setSelectedKey }),
1611
+ h(InfraPanels, { data: data }),
1612
+ h(DeviceDetails, { data: data, selectedKey: selectedKey })
1613
+ ),
1614
+ h('div', { className: 'dm-divider', onPointerDown: startSplit, title: '↔ 拖拽调整左右宽度(双击恢复默认)', onDoubleClick: function () { setSplitW(0); try { window.localStorage.removeItem('dsh-mesh-split-w') } catch (error) { /* ignore */ } } }),
1615
+ h('div', { className: 'dm-pane' },
1616
+ h(Details, { title: '本机实例', badge: ((data.local && data.local.instances) || []).length, open: true }, h(InstanceList, { data: data })),
1617
+ h(Details, { title: '版本一致性', badge: versions.length ? versions.length + ' 处升级' : '一致' },
1618
+ versions.length
1619
+ ? versions.map(function (item, i) { return h('div', { key: i, className: 'dm-sub', style: { marginTop: 4 } }, '· ' + (typeof item === 'string' ? item : JSON.stringify(item))) })
1620
+ : h('div', { className: 'dm-empty' }, '所有节点/实例版本一致')
1621
+ ),
1622
+ h(Details, { title: '委托台账', badge: (data.delegations && data.delegations.total) || 0 }, h(DelegationTable, { data: data })),
1623
+ h(Details, { title: '告警历史', badge: ((data.alerts && data.alerts.history) || []).length }, h(AlertList, { data: data })),
1624
+ (issues.length || hints.length) ? h(Details, { title: '诊断建议', badge: issues.length + hints.length },
1625
+ h('div', null,
1626
+ issues.slice(0, 6).map(function (issue, i) { return h('div', { key: 'i' + i, className: 'dm-err', style: { marginTop: 3 } }, '· ' + issue) }),
1627
+ hints.slice(0, 6).map(function (hint, i) { return h('div', { key: 'h' + i, className: 'dm-sub', style: { marginTop: 3 } }, '· ' + hint) })
1628
+ )
1629
+ ) : null
1568
1630
  )
1569
- ) : null,
1631
+ ),
1570
1632
  h('div', { className: 'dm-foot' }, '数据源:mesh-health / host status / peer host_self / 委托台账 / 告警历史 · ' + (data.repo || '') + ' · node ' + (data.nodeVersion || '') + ' · ' + REFRESH_MS / 1000 + 's 自动刷新(只读)')
1571
1633
  )
1572
1634
  }
@@ -1785,6 +1847,23 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
1785
1847
  )
1786
1848
  }
1787
1849
 
1850
+ /** 左侧栏「AI 网络」专用图标:中心节点 + 四向设备,和「委托」区分开。 */
1851
+ function networkPanelIcon(props) {
1852
+ var size = (props && props.size) || 16
1853
+ return h('svg', {
1854
+ 'data-dsh-panel-entry': 'mesh-network', viewBox: '0 0 16 16', width: size, height: size,
1855
+ fill: 'none', stroke: 'currentColor', strokeWidth: '1.3', strokeLinecap: 'round', 'aria-hidden': 'true',
1856
+ style: { color: 'var(--dsh-brand-color, currentColor)' }
1857
+ },
1858
+ h('path', { d: 'M8 8 L8 2.6 M8 8 L13.4 8 M8 8 L8 13.4 M8 8 L2.6 8', opacity: '.55' }),
1859
+ h('circle', { cx: 8, cy: 8, r: 2.1, fill: 'none', strokeWidth: '1.4' }),
1860
+ h('circle', { cx: 8, cy: 2.6, r: 1.25, fill: 'currentColor', stroke: 'none' }),
1861
+ h('circle', { cx: 13.4, cy: 8, r: 1.25, fill: 'currentColor', stroke: 'none' }),
1862
+ h('circle', { cx: 8, cy: 13.4, r: 1.25, fill: 'currentColor', stroke: 'none' }),
1863
+ h('circle', { cx: 2.6, cy: 8, r: 1.25, fill: 'currentColor', stroke: 'none' })
1864
+ )
1865
+ }
1866
+
1788
1867
  function MeshDelegationPanel() {
1789
1868
  var s0 = useState(null); var list = s0[0]; var setList = s0[1]
1790
1869
  var s1 = useState(true); var loading = s1[0]; var setLoading = s1[1]
@@ -2224,6 +2303,111 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
2224
2303
  )
2225
2304
  }
2226
2305
 
2306
+ // 2026-11-10:AI 网络现在是左侧栏主页面,不再受设置面板的窄容器约束。
2307
+ // 主区域自身没有内边距,这里给页面统一留白并限制最大阅读宽度。
2308
+ var PAGE_CSS = `
2309
+ .dm-page { box-sizing:border-box; width:100%; height:100%; overflow:hidden; padding:clamp(16px, 2.2vw, 30px) clamp(18px, 2.6vw, 42px) 0; }
2310
+ .dm-content-shell { width:100%; height:100%; position:relative; --dm-content-w:920px; }
2311
+ .dm-content-scroll { height:100%; overflow:auto; padding-bottom:36px; scrollbar-gutter:stable; }
2312
+ .dm-content { width:min(100%, var(--dm-content-w)); max-width:100%; margin:0 auto; box-sizing:border-box; container-type:inline-size; container-name:dm-content; }
2313
+ .dm-content > .dm-root { width:100%; max-width:100%; margin:0 auto; }
2314
+ /* 与 DSH 对话区一致的宽度手柄:位于内容两侧外 24px,平时隐藏,hover/拖拽显示 2px 渐变线 */
2315
+ .dm-width-handle { z-index:0; position:absolute; top:0; bottom:0; cursor:col-resize; width:clamp(0px, calc((100% - var(--dm-content-w)) / 2 - 48px), 10px); }
2316
+ .dm-width-handle[data-side="left"] { right:calc(50% + var(--dm-content-w) / 2 + 24px); }
2317
+ .dm-width-handle[data-side="right"] { left:calc(50% + var(--dm-content-w) / 2 + 24px); }
2318
+ .dm-width-handle::after { content:''; pointer-events:none; opacity:0; position:absolute; top:0; bottom:0; width:2px; border-radius:2px; background:linear-gradient(to bottom, transparent calc(var(--dm-width-handle-pointer-y, 50%) - 36px), var(--dsw-alias-scrollbar-bg-l1, rgba(128,128,128,.55)) calc(var(--dm-width-handle-pointer-y, 50%) - 8px), var(--dsw-alias-scrollbar-bg-l1, rgba(128,128,128,.55)) calc(var(--dm-width-handle-pointer-y, 50%) + 8px), transparent calc(var(--dm-width-handle-pointer-y, 50%) + 36px)); }
2319
+ .dm-width-handle[data-side="left"]::after { right:4px; }
2320
+ .dm-width-handle[data-side="right"]::after { left:4px; }
2321
+ .dm-width-handle[data-dragging]::after { opacity:1; }
2322
+ @media (hover:hover) { .dm-width-handle:hover::after { opacity:1; } }
2323
+ @media (max-width: 1000px) { .dm-width-handle { display:none; } }
2324
+ .dm-body { display:grid; grid-template-columns:minmax(300px, var(--dm-main-w, 62%)) 8px minmax(0,1fr); gap:10px; align-items:start; }
2325
+ .dm-pane { min-width:0; display:flex; flex-direction:column; gap:12px; }
2326
+ .dm-divider { cursor:col-resize; align-self:stretch; min-height:120px; border-radius:4px; position:relative; }
2327
+ .dm-divider::before { content:''; position:absolute; top:0; bottom:0; left:3px; width:1px; background:rgba(128,128,128,.28); }
2328
+ .dm-divider:hover::before, .dm-divider:active::before { background:rgba(88,166,255,.7); width:2px; left:2.5px; }
2329
+ @container dm-content (max-width: 860px) { .dm-body { grid-template-columns:1fr; } .dm-divider { display:none; } }
2330
+ `
2331
+ function MeshNetworkPage() {
2332
+ // 宽度偏好:与 DSH 对话区相同语义——空 = 默认宽度,数字 = 用户拖拽固定值。
2333
+ var prefState = useState(function () {
2334
+ try { var saved = Number(window.localStorage.getItem('dsh-mesh.contentWidth')); return Number.isFinite(saved) && saved > 0 ? saved : 0 } catch (error) { return 0 }
2335
+ })
2336
+ var pref = prefState[0]; var setPref = prefState[1]
2337
+ var shellState = useState(0); var containerW = shellState[0]; var setContainerW = shellState[1]
2338
+ var shellRef = useRef(null)
2339
+
2340
+ useEffect(function () {
2341
+ var el = shellRef.current
2342
+ if (!el) return
2343
+ function measure() { setContainerW(el.clientWidth || 0) }
2344
+ measure()
2345
+ if (typeof ResizeObserver !== 'undefined') {
2346
+ var observer = new ResizeObserver(measure)
2347
+ observer.observe(el)
2348
+ return function () { observer.disconnect() }
2349
+ }
2350
+ window.addEventListener('resize', measure)
2351
+ return function () { window.removeEventListener('resize', measure) }
2352
+ }, [])
2353
+
2354
+ // DSH 对话区公式:默认 clamp(680, 64% 容器, 920);用户值 [640, 容器-176]。
2355
+ var widthNow = containerW || 920
2356
+ var maxW = Math.max(640, widthNow - 176)
2357
+ var contentW = pref > 0 ? Math.min(Math.max(pref, 640), maxW) : Math.max(680, Math.min(widthNow * 0.64, 920))
2358
+
2359
+ function beginWidthDrag(side, event) {
2360
+ event.preventDefault()
2361
+ var handle = event.currentTarget
2362
+ var shell = handle.closest ? handle.closest('.dm-content-shell') : (handle.parentElement && handle.parentElement.parentElement)
2363
+ if (!shell) return
2364
+ var max = Math.max(640, (shell.clientWidth || 920) - 176)
2365
+ var base = contentW
2366
+ var originX = event.clientX
2367
+ var latest = contentW
2368
+ var firstBox = handle.getBoundingClientRect()
2369
+ handle.style.setProperty('--dm-width-handle-pointer-y', (event.clientY - firstBox.top) + 'px')
2370
+ handle.toggleAttribute('data-dragging', true)
2371
+ document.body.style.cursor = 'col-resize'
2372
+ document.body.style.userSelect = 'none'
2373
+ function onMove(moveEvent) {
2374
+ var dx = moveEvent.clientX - originX
2375
+ var outward = side === 'right' ? dx : -dx
2376
+ // 只有拖拽时才更新高亮位置(与 DSH 一致);hover 时保持手柄高度的 50%。
2377
+ var box = handle.getBoundingClientRect()
2378
+ handle.style.setProperty('--dm-width-handle-pointer-y', (moveEvent.clientY - box.top) + 'px')
2379
+ // 两侧同时外扩/内收,所以内容宽度变化是手柄位移的 2 倍(与 DSH 一致)。
2380
+ latest = Math.min(Math.max(base + outward * 2, 640), max)
2381
+ setPref(latest)
2382
+ }
2383
+ function onUp() {
2384
+ handle.toggleAttribute('data-dragging', false)
2385
+ window.removeEventListener('pointermove', onMove)
2386
+ window.removeEventListener('pointerup', onUp)
2387
+ document.body.style.cursor = ''
2388
+ document.body.style.userSelect = ''
2389
+ try { window.localStorage.setItem('dsh-mesh.contentWidth', String(latest)) } catch (error) { /* ignore */ }
2390
+ }
2391
+ window.addEventListener('pointermove', onMove)
2392
+ window.addEventListener('pointerup', onUp)
2393
+ }
2394
+ function resetContentWidth() {
2395
+ setPref(0)
2396
+ try { window.localStorage.removeItem('dsh-mesh.contentWidth') } catch (error) { /* ignore */ }
2397
+ }
2398
+
2399
+ return h('div', { className: 'dm-page' },
2400
+ h('style', null, PAGE_CSS),
2401
+ h('div', { className: 'dm-content-shell', ref: shellRef, style: { '--dm-content-w': contentW + 'px' } },
2402
+ h('div', { className: 'dm-width-handle', 'data-side': 'left', title: '拖拽调整内容宽度(双击恢复默认)', onPointerDown: function (event) { beginWidthDrag('left', event) }, onDoubleClick: resetContentWidth }),
2403
+ h('div', { className: 'dm-width-handle', 'data-side': 'right', title: '拖拽调整内容宽度(双击恢复默认)', onPointerDown: function (event) { beginWidthDrag('right', event) }, onDoubleClick: resetContentWidth }),
2404
+ h('div', { className: 'dm-content-scroll' },
2405
+ h('div', { className: 'dm-content', style: { width: 'min(100%, ' + contentW + 'px)' } }, h(MeshStatusSection))
2406
+ )
2407
+ )
2408
+ )
2409
+ }
2410
+
2227
2411
  var SECTION = { name: 'settings.section', id: 'dsh-mesh', order: 90, label: 'AI 网络' }
2228
2412
  var inject = ['slots']
2229
2413
 
@@ -2265,12 +2449,22 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
2265
2449
  var button = buttons[i]
2266
2450
  var text = (button.textContent || '').trim()
2267
2451
  if (text !== 'AI 网络' && text !== 'AI 网络(dsh-mesh)') continue
2268
- if (button.querySelector('svg[data-dsh-mesh-nav-icon]')) continue
2269
- var gear = button.querySelector('svg')
2270
- var className = gear ? gear.getAttribute('class') || '' : ''
2271
- var icon = navMeshIconDom(className)
2272
- if (gear && gear.parentNode === button) button.replaceChild(icon, gear)
2273
- else button.insertBefore(icon, button.firstChild)
2452
+ // 0.2.2 的默认图标可能包在 span 里;在默认图标的父容器里原地替换,
2453
+ // 保留间距/对齐,并清掉所有旧 SVG,避免齿轮 + 自绘图标双图标。
2454
+ var existingIcon = button.querySelector('svg[data-dsh-mesh-nav-icon]')
2455
+ var old = []
2456
+ var all = button.querySelectorAll('svg')
2457
+ for (var j = 0; j < all.length; j++) if (all[j] !== existingIcon) old.push(all[j])
2458
+ var reference = old[0] || null
2459
+ var parent = reference ? reference.parentNode : button
2460
+ var className = reference ? reference.getAttribute('class') || '' : ''
2461
+ if (!existingIcon) {
2462
+ var icon = navMeshIconDom(className)
2463
+ if (reference) parent.insertBefore(icon, reference)
2464
+ else if (button.firstChild) button.insertBefore(icon, button.firstChild)
2465
+ else button.appendChild(icon)
2466
+ }
2467
+ for (var k = 0; k < old.length; k++) if (old[k].parentNode) old[k].parentNode.removeChild(old[k])
2274
2468
  }
2275
2469
  } catch (error) {
2276
2470
  /* ignore */
@@ -2279,10 +2473,16 @@ body[data-ds-dark-theme] .dm-dg-fs-item.active { background:rgba(88,166,255,.28)
2279
2473
 
2280
2474
  function apply(ctx) {
2281
2475
  var disposers = []
2476
+ // 2026-11-10:AI 网络从「设置」迁到左侧栏独立页面,使用更大的主区域。
2282
2477
  try {
2283
- disposers.push(ctx.slots.inject('settings.section', function () { return ctx.slots.register(SECTION, MeshStatusSection) }))
2478
+ disposers.push(ctx.slots.inject('sidebar.panellist', function () {
2479
+ return ctx.slots.register({ name: 'sidebar.panellist', id: 'mesh-network', order: 54, label: function () { return 'AI 网络' } }, networkPanelIcon)
2480
+ }))
2481
+ disposers.push(ctx.slots.inject('main', function () {
2482
+ return ctx.slots.register({ name: 'main', key: 'mesh-network', inject: function () { return {} } }, MeshNetworkPage)
2483
+ }))
2284
2484
  } catch (error) {
2285
- console.warn('[dsh-mesh-ui] settings section registration failed:', error)
2485
+ console.warn('[dsh-mesh-ui] sidebar network page registration failed:', error)
2286
2486
  }
2287
2487
  // 2026-11-07 方案 A:左侧栏「委托」面板(sidebar.panellist + main)
2288
2488
  try {
package/lib/collect.js CHANGED
@@ -15,10 +15,11 @@
15
15
 
16
16
  import { execFile } from 'node:child_process'
17
17
  import { existsSync, readdirSync, readFileSync, statfsSync, writeFileSync } from 'node:fs'
18
+ import { createRequire } from 'node:module'
18
19
  import { lookup as dnsLookup } from 'node:dns/promises'
19
20
  import { connect as netConnect } from 'node:net'
20
21
  import { arch as osArch, cpus as osCpus, freemem as osFreemem, homedir, platform as osPlatform, release as osRelease, totalmem as osTotalmem } from 'node:os'
21
- import { join } from 'node:path'
22
+ import { dirname, join } from 'node:path'
22
23
 
23
24
  const CLI_TIMEOUT_MS = 25_000
24
25
  const ALERT_HISTORY_MAX = 12
@@ -29,9 +30,19 @@ export function meshHome() {
29
30
  }
30
31
 
31
32
  export function resolveRepo() {
33
+ // 1) 显式 DSH_MESH_REPO / 常见开发目录(源码/本地仓库)
32
34
  const candidates = [process.env.DSH_MESH_REPO, join(homedir(), 'Code', 'dsh-mesh'), join(homedir(), 'Desktop', 'dsh-mesh')].filter(Boolean)
33
35
  for (const candidate of candidates) {
34
- if (existsSync(join(candidate, 'scripts', 'mesh-health.mjs'))) return candidate
36
+ if (existsSync(join(candidate, 'scripts', 'mesh-health.mjs')) && existsSync(join(candidate, 'src', 'cli.js'))) return candidate
37
+ }
38
+ // 2) 用户正常安装路径:dsh-mesh-ui 声明了 dsh-mesh-node 依赖,
39
+ // 直接解析已安装的 npm 包(随包带 scripts/mesh-health.mjs)。
40
+ try {
41
+ const requireFromPlugin = createRequire(import.meta.url)
42
+ const nodeRoot = dirname(requireFromPlugin.resolve('dsh-mesh-node/package.json'))
43
+ if (existsSync(join(nodeRoot, 'scripts', 'mesh-health.mjs')) && existsSync(join(nodeRoot, 'src', 'cli.js'))) return nodeRoot
44
+ } catch {
45
+ /* package not installed next to this plugin */
35
46
  }
36
47
  return null
37
48
  }
@@ -391,7 +402,7 @@ async function overviewData(repo, force = false) {
391
402
  */
392
403
  export async function collect(repo) {
393
404
  const generatedAt = new Date().toISOString()
394
- if (!repo) return { ok: false, error: 'dsh-mesh repo not found (set DSH_MESH_REPO)', generatedAt }
405
+ if (!repo) return { ok: false, error: 'dsh-mesh-node is not installed for dsh-mesh-ui (install it with `npm i -g dsh-mesh-node`, or set DSH_MESH_REPO to a dsh-mesh checkout)', generatedAt }
395
406
 
396
407
  const meshPromise = meshHealthJson(repo)
397
408
  const overviewPromise = overviewData(repo)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-mesh-ui",
3
- "version": "0.0.11",
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.",
3
+ "version": "0.0.13",
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",
7
7
  "engines": {
@@ -27,6 +27,9 @@
27
27
  "platform": "web"
28
28
  }
29
29
  },
30
+ "dependencies": {
31
+ "dsh-mesh-node": "0.0.12"
32
+ },
30
33
  "peerDependencies": {
31
34
  "@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0",
32
35
  "react": "^18.2.0",