@wenbin_wb/dsh-bridge 2.6.1 → 2.7.0

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/client/index.js CHANGED
@@ -109,6 +109,10 @@ const Icons = {
109
109
  check: (props) => React.createElement('svg', { viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', strokeWidth: 2.5, strokeLinecap: 'round', strokeLinejoin: 'round', ...props },
110
110
  React.createElement('polyline', { points: '20 6 9 17 4 12' })
111
111
  ),
112
+ ops: (props) => React.createElement('svg', { viewBox: '0 0 24 24', width: 16, height: 16, fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', ...props },
113
+ React.createElement('circle', { cx: 12, cy: 12, r: 3 }),
114
+ React.createElement('path', { d: 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z' })
115
+ ),
112
116
  };
113
117
 
114
118
  // ---- 子组件 ----
@@ -257,6 +261,9 @@ function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
257
261
  showQr && qr && React.createElement('div', { style: { marginTop: 10 } },
258
262
  React.createElement('img', { src: qr, alt: 'QR', style: s.qr }),
259
263
  React.createElement('div', { style: { ...s.muted, marginTop: 4 } }, '请在私密环境下使用'),
264
+ React.createElement('div', { style: { ...s.muted, marginTop: 4, fontSize: 11, color: 'var(--dsw-alias-brand-primary, #4f6ef7)' } },
265
+ '📱 提示:手机浏览器扫码打开后,在菜单点击「添加到主屏幕」即可作为独立全屏 App 运行。'
266
+ ),
260
267
  ),
261
268
  onReset && React.createElement('div', { style: { marginTop: 8 } },
262
269
  React.createElement('button', {
@@ -1445,8 +1452,6 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1445
1452
  ),
1446
1453
  ),
1447
1454
 
1448
-
1449
-
1450
1455
  React.createElement('div', { style: s.block },
1451
1456
  React.createElement('div', { style: { ...s.tip, fontSize: 12 } },
1452
1457
  platformId === 'wechat'
@@ -1459,29 +1464,340 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1459
1464
  );
1460
1465
  }
1461
1466
 
1462
- // 单条升级命令行:命令文本 + 复制按钮
1463
- function UpgradeCommandRow({ cmd }) {
1464
- const [copied, copy] = useCopy();
1465
- return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
1466
- React.createElement('code', {
1467
+ // 宿主系统运行监控看板
1468
+ function SystemMetricsWidget({ metrics }) {
1469
+ if (!metrics) return null;
1470
+ const memUsedPercent = metrics.memory?.usedPercent ?? 0;
1471
+ const memUsedGb = (metrics.memory?.usedBytes / (1024 ** 3)).toFixed(1);
1472
+ const memTotalGb = (metrics.memory?.totalBytes / (1024 ** 3)).toFixed(1);
1473
+ const heapMb = Math.round((metrics.memory?.processHeapUsed || 0) / (1024 ** 2));
1474
+
1475
+ const formatUptime = (sec = 0) => {
1476
+ const days = Math.floor(sec / 86400);
1477
+ const hrs = Math.floor((sec % 86400) / 3600);
1478
+ const mins = Math.floor((sec % 3600) / 60);
1479
+ if (days > 0) return `${days}天 ${hrs}小时 ${mins}分`;
1480
+ if (hrs > 0) return `${hrs}小时 ${mins}分`;
1481
+ return `${mins}分钟`;
1482
+ };
1483
+
1484
+ const progressColor = memUsedPercent > 85 ? '#dc2626' : memUsedPercent > 70 ? '#d97706' : '#059669';
1485
+
1486
+ return React.createElement('div', {
1487
+ style: {
1488
+ ...s.card,
1489
+ marginBottom: 16,
1490
+ },
1491
+ },
1492
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, flexWrap: 'wrap', gap: 6 } },
1493
+ React.createElement('div', { style: { ...s.label, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 } },
1494
+ '📊 宿主系统与运行看板'
1495
+ ),
1496
+ React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary, #6b7280)' } },
1497
+ `Node ${metrics.os?.nodeVersion || ''} · ${metrics.os?.platform || ''} ${metrics.os?.arch || ''}`
1498
+ ),
1499
+ ),
1500
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(130px, 1fr))', gap: 12, marginBottom: 12 } },
1501
+ React.createElement('div', null,
1502
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #9ca3af)', fontSize: 11, marginBottom: 2 } }, 'CPU 核心与型号'),
1503
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary, currentColor)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, title: metrics.cpu?.model },
1504
+ `${metrics.cpu?.cores || 0} 核心 (${(metrics.cpu?.model || '').split('@')[0].trim()})`
1505
+ ),
1506
+ ),
1507
+ React.createElement('div', null,
1508
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #9ca3af)', fontSize: 11, marginBottom: 2 } }, 'DSH 运行时间 (Uptime)'),
1509
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-success-primary, #059669)' } },
1510
+ formatUptime(metrics.uptime?.processSec)
1511
+ ),
1512
+ ),
1513
+ React.createElement('div', null,
1514
+ React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary, #9ca3af)', fontSize: 11, marginBottom: 2 } }, 'Node 进程堆内存'),
1515
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary, currentColor)' } },
1516
+ `${heapMb} MB`
1517
+ ),
1518
+ ),
1519
+ ),
1520
+ React.createElement('div', null,
1521
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--dsw-alias-label-secondary, #6b7280)', marginBottom: 4 } },
1522
+ React.createElement('span', null, `系统内存占用: ${memUsedGb} GB / ${memTotalGb} GB`),
1523
+ React.createElement('span', { style: { fontWeight: 600, color: progressColor } }, `${memUsedPercent}%`),
1524
+ ),
1525
+ React.createElement('div', {
1526
+ style: {
1527
+ width: '100%', height: 6, background: 'var(--dsw-alias-border-l2, #e5e7eb)', borderRadius: 999, overflow: 'hidden',
1528
+ },
1529
+ },
1530
+ React.createElement('div', {
1531
+ style: {
1532
+ width: `${memUsedPercent}%`, height: '100%', background: progressColor, borderRadius: 999, transition: 'width .3s ease',
1533
+ },
1534
+ }),
1535
+ ),
1536
+ ),
1537
+ );
1538
+ }
1539
+
1540
+ // 网络连通性诊断小工具
1541
+ function NetworkDiagnosticWidget({ rpcCall }) {
1542
+ const [running, setRunning] = React.useState(false);
1543
+ const [result, setResult] = React.useState(null);
1544
+
1545
+ const runDiagnose = React.useCallback(async () => {
1546
+ setRunning(true);
1547
+ try {
1548
+ const r = await rpcCall(BRIDGE_ENDPOINTS.diagnoseNetwork, {});
1549
+ if (r?.ok) setResult(r.value);
1550
+ } catch (e) {
1551
+ setResult({ overall: 'warning', results: [{ item: 'err', name: '诊断请求异常', status: 'fail', detail: e.message }] });
1552
+ } finally {
1553
+ setRunning(false);
1554
+ }
1555
+ }, [rpcCall]);
1556
+
1557
+ return React.createElement('div', { style: { ...s.card, marginBottom: 16 } },
1558
+ React.createElement('div', { style: { marginBottom: 10 } },
1559
+ React.createElement('div', { style: { ...s.label, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 } },
1560
+ '🔍 网络连通性一键诊断'
1561
+ ),
1562
+ React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
1563
+ '一键检测本地反向代理端口、局域网 IPv4、Cloudflare Anycast 延迟以及国内 npmmirror 镜像源连通性。'
1564
+ ),
1565
+ ),
1566
+
1567
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: result ? 12 : 0 } },
1568
+ React.createElement('button', {
1569
+ type: 'button',
1570
+ style: { ...s.btnPri, height: 32, fontSize: 12, padding: '0 14px' },
1571
+ onClick: runDiagnose,
1572
+ disabled: running,
1573
+ },
1574
+ running ? '正在探测连通性…' : result ? '🔄 重新诊断网络' : '🔍 开始一键诊断'
1575
+ ),
1576
+ result && React.createElement('span', {
1577
+ style: {
1578
+ fontSize: 12,
1579
+ color: result.overall === 'healthy' ? 'var(--dsw-alias-state-success-primary, #059669)' : 'var(--dsw-alias-state-warn-primary, #d97706)',
1580
+ fontWeight: 600,
1581
+ },
1582
+ }, result.overall === 'healthy' ? '✓ 所有网络探测项正常' : '▲ 检测到部分延迟较高或异常'),
1583
+ ),
1584
+
1585
+ running && !result && React.createElement('div', {
1467
1586
  style: {
1468
- ...s.code,
1469
- fontSize: 11,
1470
- color: 'var(--dsw-alias-label-secondary,#6b7280)',
1471
- flex: 1,
1472
- minWidth: 0,
1473
- wordBreak: 'break-all',
1474
- background: 'var(--dsw-alias-bg-layer-1,#ffffff)',
1475
- padding: '4px 8px',
1476
- borderRadius: 6,
1477
- border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
1587
+ marginTop: 10, padding: '10px 14px', borderRadius: 8,
1588
+ background: 'var(--dsw-alias-bg-layer-2, #f9fafb)',
1589
+ display: 'flex', alignItems: 'center', gap: 8, color: 'var(--dsw-alias-brand-primary, #4f6ef7)',
1590
+ fontSize: 12,
1478
1591
  },
1479
- }, cmd),
1480
- React.createElement('button', {
1481
- style: { ...s.btnGhost, height: 26, padding: '0 10px', fontSize: 12, flexShrink: 0 },
1482
- onClick: () => copy(cmd),
1483
- title: '复制升级命令',
1484
- }, copied ? '✓ 已复制' : '复制'),
1592
+ },
1593
+ React.createElement('span', { style: { animation: 'spin 1s linear infinite', display: 'inline-flex' } }, React.createElement(Icons.refresh)),
1594
+ '正在执行网络端口与云端节点连通性探测…'
1595
+ ),
1596
+
1597
+ result?.results && React.createElement('div', {
1598
+ style: {
1599
+ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 10,
1600
+ paddingTop: 10, borderTop: '1px solid var(--dsw-alias-border-l2, #e5e7eb)',
1601
+ },
1602
+ },
1603
+ result.results.map((item, idx) => {
1604
+ const isPass = item.status === 'pass';
1605
+ const isWarn = item.status === 'warn';
1606
+ return React.createElement('div', {
1607
+ key: idx,
1608
+ style: {
1609
+ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8,
1610
+ padding: '8px 10px', borderRadius: 6,
1611
+ background: 'var(--dsw-alias-bg-layer-1, rgba(255,255,255,0.7))',
1612
+ border: `1px solid ${isPass ? 'var(--dsw-alias-state-success-border, #a7f3d0)' : isWarn ? 'var(--dsw-alias-state-warn-border, #fde68a)' : 'var(--dsw-alias-state-error-border, #fecaca)'}`,
1613
+ boxSizing: 'border-box',
1614
+ },
1615
+ },
1616
+ React.createElement('div', { style: { flex: 1, minWidth: 0 } },
1617
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary, currentColor)', marginBottom: 2 } },
1618
+ isPass ? '✓ ' : isWarn ? '▲ ' : '✕ ',
1619
+ item.name
1620
+ ),
1621
+ React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary, #6b7280)' } }, item.detail),
1622
+ ),
1623
+ item.latencyMs != null && React.createElement('span', {
1624
+ style: {
1625
+ fontSize: 11, fontWeight: 600, flexShrink: 0,
1626
+ color: item.latencyMs < 500 ? 'var(--dsw-alias-state-success-primary, #059669)' : 'var(--dsw-alias-state-warn-primary, #d97706)',
1627
+ },
1628
+ }, `${item.latencyMs}ms`),
1629
+ );
1630
+ })
1631
+ )
1632
+ );
1633
+ }
1634
+
1635
+ // 全局配置备份与恢复小卡片
1636
+ function BackupRestoreWidget({ rpcCall, onUpdate }) {
1637
+ const [exporting, setExporting] = React.useState(false);
1638
+ const [importing, setImporting] = React.useState(false);
1639
+ const [msg, setMsg] = React.useState(null);
1640
+ const fileInputRef = React.useRef(null);
1641
+
1642
+ const handleExport = async () => {
1643
+ setExporting(true);
1644
+ setMsg(null);
1645
+ try {
1646
+ const r = await rpcCall(BRIDGE_ENDPOINTS.exportBackup, {});
1647
+ if (r?.ok && r.value) {
1648
+ const jsonStr = JSON.stringify(r.value, null, 2);
1649
+ const blob = new Blob([jsonStr], { type: 'application/json' });
1650
+ const url = URL.createObjectURL(blob);
1651
+ const a = document.createElement('a');
1652
+ const now = new Date();
1653
+ const dateStr = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`;
1654
+ a.href = url;
1655
+ a.download = `dsh-bridge-backup-${dateStr}.json`;
1656
+ document.body.appendChild(a);
1657
+ a.click();
1658
+ document.body.removeChild(a);
1659
+ URL.revokeObjectURL(url);
1660
+ setMsg({ ok: true, text: '✓ 备份文件已成功导出并下载到本地!' });
1661
+ } else {
1662
+ setMsg({ ok: false, text: r?.error?.message || '导出备份失败' });
1663
+ }
1664
+ } catch (e) {
1665
+ setMsg({ ok: false, text: e.message || '导出异常' });
1666
+ } finally {
1667
+ setExporting(false);
1668
+ }
1669
+ };
1670
+
1671
+ const handleFileChange = async (e) => {
1672
+ const file = e.target?.files?.[0];
1673
+ if (!file) return;
1674
+ setImporting(true);
1675
+ setMsg(null);
1676
+ try {
1677
+ const text = await file.text();
1678
+ const backup = JSON.parse(text);
1679
+ const r = await rpcCall(BRIDGE_ENDPOINTS.importBackup, { backup });
1680
+ if (r?.ok) {
1681
+ setMsg({ ok: true, text: '✓ 配置已成功导入并刷新生效!' });
1682
+ onUpdate?.(r.value?.status);
1683
+ } else {
1684
+ setMsg({ ok: false, text: r?.error?.message || '导入配置失败' });
1685
+ }
1686
+ } catch (err) {
1687
+ setMsg({ ok: false, text: `导入解析失败: ${err.message}` });
1688
+ } finally {
1689
+ setImporting(false);
1690
+ if (fileInputRef.current) fileInputRef.current.value = '';
1691
+ }
1692
+ };
1693
+
1694
+ return React.createElement('div', { style: { ...s.card, marginBottom: 16 } },
1695
+ React.createElement('div', { style: { marginBottom: 10 } },
1696
+ React.createElement('div', { style: { ...s.label, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 } },
1697
+ '🗄️ 全局配置备份与恢复'
1698
+ ),
1699
+ React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
1700
+ '支持一键导出或导入恢复本插件所有配置(包含各 IM 平台凭证、授权白名单、公网隧道与安全认证规则)。'
1701
+ ),
1702
+ ),
1703
+ React.createElement('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
1704
+ React.createElement('button', {
1705
+ type: 'button',
1706
+ style: { ...s.btnPri, height: 32, fontSize: 12, padding: '0 14px' },
1707
+ onClick: handleExport,
1708
+ disabled: exporting || importing,
1709
+ }, exporting ? '正在导出…' : '📥 导出配置备份 (.json)'),
1710
+ React.createElement('button', {
1711
+ type: 'button',
1712
+ style: { ...s.btnGhost, height: 32, fontSize: 12, padding: '0 14px' },
1713
+ onClick: () => fileInputRef.current?.click(),
1714
+ disabled: exporting || importing,
1715
+ }, importing ? '正在导入…' : '📤 导入配置恢复'),
1716
+ React.createElement('input', {
1717
+ type: 'file',
1718
+ ref: fileInputRef,
1719
+ accept: '.json',
1720
+ style: { display: 'none' },
1721
+ onChange: handleFileChange,
1722
+ }),
1723
+ ),
1724
+ msg && React.createElement('div', {
1725
+ style: {
1726
+ marginTop: 10, padding: '6px 12px', borderRadius: 6, fontSize: 12,
1727
+ background: msg.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
1728
+ color: msg.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
1729
+ },
1730
+ }, msg.text),
1731
+ );
1732
+ }
1733
+
1734
+ // 运维 Tab 内的手动重启 DSH 服务小卡片
1735
+ function RestartDshCard({ rpcCall }) {
1736
+ const [restarting, setRestarting] = React.useState(false);
1737
+ const [status, setStatus] = React.useState(null);
1738
+
1739
+ const handleRestart = async () => {
1740
+ setRestarting(true);
1741
+ setStatus({ phase: 'restarting', text: '正在向 DSH 服务发送重启指令…' });
1742
+ try {
1743
+ await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
1744
+ } catch {}
1745
+
1746
+ setStatus({ phase: 'reconnecting', text: 'DSH 服务正在重启中,正在自动重新连接…' });
1747
+ await new Promise(r => setTimeout(r, 2000));
1748
+
1749
+ let attempts = 0;
1750
+ const maxAttempts = 30;
1751
+ const pollHealth = setInterval(async () => {
1752
+ attempts++;
1753
+ try {
1754
+ const r = await rpcCall(BRIDGE_ENDPOINTS.checkVersion, {});
1755
+ if (r?.ok) {
1756
+ clearInterval(pollHealth);
1757
+ setStatus({ phase: 'success', text: '🎉 重启成功!已重新建立连接,正在刷新页面…' });
1758
+ setTimeout(() => { window.location.reload(); }, 1000);
1759
+ return;
1760
+ }
1761
+ } catch {}
1762
+
1763
+ if (attempts >= maxAttempts) {
1764
+ clearInterval(pollHealth);
1765
+ setStatus({ phase: 'timeout', text: '重连等待超时,请手动刷新页面。' });
1766
+ setRestarting(false);
1767
+ }
1768
+ }, 1000);
1769
+ };
1770
+
1771
+ return React.createElement('div', { style: { ...s.card, marginBottom: 16 } },
1772
+ React.createElement('div', { style: { marginBottom: 10 } },
1773
+ React.createElement('div', { style: { ...s.label, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 } },
1774
+ '🔄 DSH 服务平滑重启'
1775
+ ),
1776
+ React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
1777
+ '优雅退出并重新拉起当前 DSH 进程与所有插件服务,前端将在几秒后自动探测重连并刷新页面。'
1778
+ ),
1779
+ ),
1780
+ !restarting && !status && React.createElement('button', {
1781
+ type: 'button',
1782
+ style: { ...s.btnGhost, height: 32, fontSize: 12, padding: '0 14px' },
1783
+ onClick: handleRestart,
1784
+ }, '🔄 立即重启 DSH 服务'),
1785
+ (restarting || status) && React.createElement('div', {
1786
+ style: {
1787
+ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12,
1788
+ color: status?.phase === 'success'
1789
+ ? 'var(--dsw-alias-state-success-primary, #059669)'
1790
+ : status?.phase === 'timeout'
1791
+ ? 'var(--dsw-alias-state-error-primary, #dc2626)'
1792
+ : 'var(--dsw-alias-state-info-primary, #2563eb)',
1793
+ fontWeight: 500,
1794
+ },
1795
+ },
1796
+ status?.phase !== 'success' && status?.phase !== 'timeout' && React.createElement('span', {
1797
+ style: { animation: 'spin 1s linear infinite', display: 'inline-flex' },
1798
+ }, React.createElement(Icons.refresh)),
1799
+ status?.text || '正在调度…',
1800
+ ),
1485
1801
  );
1486
1802
  }
1487
1803
 
@@ -1492,6 +1808,9 @@ function VersionBanner({ rpcCall }) {
1492
1808
  const [upgrading, setUpgrading] = React.useState(false);
1493
1809
  const [upgradeResult, setUpgradeResult] = React.useState(null);
1494
1810
  const [showManual, setShowManual] = React.useState(false);
1811
+ const [restarting, setRestarting] = React.useState(false);
1812
+ const [restartStatus, setRestartStatus] = React.useState(null);
1813
+ const [dismissRestart, setDismissRestart] = React.useState(false);
1495
1814
 
1496
1815
  const check = React.useCallback(async () => {
1497
1816
  setLoading(true);
@@ -1512,11 +1831,12 @@ function VersionBanner({ rpcCall }) {
1512
1831
  if (!info?.latest || upgrading) return;
1513
1832
  setUpgrading(true);
1514
1833
  setUpgradeResult(null);
1834
+ setDismissRestart(false);
1835
+ setRestartStatus(null);
1515
1836
  try {
1516
1837
  const r = await rpcCall(BRIDGE_ENDPOINTS.upgradePlugin, { version: info.latest });
1517
1838
  if (r?.ok && r.value?.ok) {
1518
- setUpgradeResult({ ok: true, message: `已成功升级到 v${info.latest}!请重启 DSH 服务使新版本生效。` });
1519
- setTimeout(() => check(), 3000);
1839
+ setUpgradeResult({ ok: true, message: `已成功升级到 v${info.latest}!` });
1520
1840
  } else {
1521
1841
  setUpgradeResult({ ok: false, message: r?.value?.error || r?.error?.message || '升级失败' });
1522
1842
  setShowManual(true);
@@ -1527,9 +1847,49 @@ function VersionBanner({ rpcCall }) {
1527
1847
  } finally {
1528
1848
  setUpgrading(false);
1529
1849
  }
1530
- }, [info?.latest, upgrading, rpcCall, check]);
1850
+ }, [info?.latest, upgrading, rpcCall]);
1851
+
1852
+ const handleRestart = React.useCallback(async () => {
1853
+ setRestarting(true);
1854
+ setRestartStatus({ phase: 'restarting', text: '正在调度 DSH 服务重启…' });
1855
+ try {
1856
+ await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
1857
+ } catch {
1858
+ // 忽略 RPC 错误(因为服务可能瞬间关闭导致网络连接断开)
1859
+ }
1860
+
1861
+ setRestartStatus({ phase: 'reconnecting', text: 'DSH 服务正在重启中,正在自动重新连接…' });
1862
+
1863
+ // 等待 2 秒后开始健康检查轮询
1864
+ await new Promise(r => setTimeout(r, 2000));
1865
+
1866
+ let attempts = 0;
1867
+ const maxAttempts = 30; // 最多探测 30 次(约 30 秒)
1868
+ const pollHealth = setInterval(async () => {
1869
+ attempts++;
1870
+ try {
1871
+ const r = await rpcCall(BRIDGE_ENDPOINTS.checkVersion, {});
1872
+ if (r?.ok) {
1873
+ clearInterval(pollHealth);
1874
+ setRestartStatus({ phase: 'success', text: '🎉 重启成功!已自动加载最新版本。正在刷新页面…' });
1875
+ setTimeout(() => {
1876
+ window.location.reload();
1877
+ }, 1000);
1878
+ return;
1879
+ }
1880
+ } catch {
1881
+ // 仍在启动中,继续等待
1882
+ }
1883
+
1884
+ if (attempts >= maxAttempts) {
1885
+ clearInterval(pollHealth);
1886
+ setRestartStatus({ phase: 'timeout', text: '重连等待超时,请手动刷新页面。' });
1887
+ setRestarting(false);
1888
+ }
1889
+ }, 1000);
1890
+ }, [rpcCall]);
1531
1891
 
1532
- const links = React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' } },
1892
+ const links = React.createElement('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
1533
1893
  React.createElement('a', {
1534
1894
  href: GITHUB_URL, target: '_blank', rel: 'noreferrer', style: s.btnLink,
1535
1895
  }, React.createElement(Icons.github), 'GitHub'),
@@ -1598,7 +1958,7 @@ function VersionBanner({ rpcCall }) {
1598
1958
  gap: 4,
1599
1959
  },
1600
1960
  onClick: check,
1601
- disabled: loading || upgrading,
1961
+ disabled: loading || upgrading || restarting,
1602
1962
  title: '重新检查 npm 线上版本',
1603
1963
  },
1604
1964
  React.createElement(Icons.refresh),
@@ -1639,10 +1999,10 @@ function VersionBanner({ rpcCall }) {
1639
1999
  background: upgradeResult?.ok
1640
2000
  ? 'var(--dsw-alias-state-success-primary,#059669)'
1641
2001
  : 'var(--dsw-alias-brand-primary,#4f6ef7)',
1642
- opacity: upgrading ? 0.6 : 1,
2002
+ opacity: (upgrading || restarting) ? 0.6 : 1,
1643
2003
  },
1644
2004
  onClick: handleUpgrade,
1645
- disabled: upgrading || upgradeResult?.ok,
2005
+ disabled: upgrading || restarting || upgradeResult?.ok,
1646
2006
  },
1647
2007
  upgrading
1648
2008
  ? React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } },
@@ -1650,7 +2010,7 @@ function VersionBanner({ rpcCall }) {
1650
2010
  '正在自动升级…',
1651
2011
  )
1652
2012
  : upgradeResult?.ok
1653
- ? '✓ 已完成升级'
2013
+ ? '✓ 升级完成'
1654
2014
  : `一键升级到 v${info.latest}`
1655
2015
  ),
1656
2016
  ),
@@ -1659,9 +2019,9 @@ function VersionBanner({ rpcCall }) {
1659
2019
  info?.releaseNotes && React.createElement('div', {
1660
2020
  style: {
1661
2021
  fontSize: 12,
1662
- color: 'var(--dsw-alias-label-secondary,#374151)',
1663
- background: 'rgba(255, 255, 255, 0.75)',
1664
- border: '1px solid rgba(191, 219, 254, 0.7)',
2022
+ color: 'var(--dsw-alias-label-primary, #374151)',
2023
+ background: 'var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.85))',
2024
+ border: '1px solid var(--dsw-alias-state-info-border, rgba(191, 219, 254, 0.8))',
1665
2025
  borderRadius: 8,
1666
2026
  padding: '8px 12px',
1667
2027
  marginBottom: 10,
@@ -1669,15 +2029,76 @@ function VersionBanner({ rpcCall }) {
1669
2029
  whiteSpace: 'pre-line',
1670
2030
  },
1671
2031
  },
1672
- React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-info-primary,#1e40af)', marginBottom: 2 } }, '✨ 更新亮点:'),
2032
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-info-primary, #2563eb)', marginBottom: 2 } }, '✨ 更新亮点:'),
1673
2033
  info.releaseNotes
1674
2034
  ),
1675
2035
 
1676
- upgradeResult && React.createElement('div', {
2036
+ // 升级成功后:引导重启 DSH 操作卡片
2037
+ upgradeResult?.ok && !dismissRestart && React.createElement('div', {
2038
+ style: {
2039
+ background: 'var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.95))',
2040
+ border: '1px solid var(--dsw-alias-state-success-border, #a7f3d0)',
2041
+ borderRadius: 8,
2042
+ padding: '12px 14px',
2043
+ marginBottom: 10,
2044
+ },
2045
+ },
2046
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 } },
2047
+ React.createElement('span', { style: { fontSize: 16 } }, '✨'),
2048
+ React.createElement('span', {
2049
+ style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-state-success-primary, #059669)' },
2050
+ }, `已成功升级到 v${info.latest}!需要重启 DSH 服务使新版本生效`),
2051
+ ),
2052
+ !restarting && !restartStatus && React.createElement('div', {
2053
+ style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
2054
+ },
2055
+ React.createElement('button', {
2056
+ style: {
2057
+ ...s.btnPri,
2058
+ height: 30,
2059
+ fontSize: 12,
2060
+ padding: '0 14px',
2061
+ background: 'var(--dsw-alias-state-success-primary, #059669)',
2062
+ },
2063
+ onClick: handleRestart,
2064
+ }, '🔄 立即重启 DSH 服务'),
2065
+ React.createElement('button', {
2066
+ style: {
2067
+ ...s.btnGhost,
2068
+ height: 30,
2069
+ fontSize: 12,
2070
+ padding: '0 12px',
2071
+ },
2072
+ onClick: () => setDismissRestart(true),
2073
+ }, '稍后手动重启'),
2074
+ ),
2075
+ (restarting || restartStatus) && React.createElement('div', {
2076
+ style: {
2077
+ display: 'flex',
2078
+ alignItems: 'center',
2079
+ gap: 8,
2080
+ fontSize: 12,
2081
+ color: restartStatus?.phase === 'success'
2082
+ ? 'var(--dsw-alias-state-success-primary, #059669)'
2083
+ : restartStatus?.phase === 'timeout'
2084
+ ? 'var(--dsw-alias-state-error-primary, #dc2626)'
2085
+ : 'var(--dsw-alias-state-info-primary, #2563eb)',
2086
+ fontWeight: 500,
2087
+ },
2088
+ },
2089
+ restartStatus?.phase !== 'success' && restartStatus?.phase !== 'timeout' && React.createElement('span', {
2090
+ style: { animation: 'spin 1s linear infinite', display: 'inline-flex' },
2091
+ }, React.createElement(Icons.refresh)),
2092
+ restartStatus?.text || '正在处理…',
2093
+ ),
2094
+ ),
2095
+
2096
+ // 失败提示
2097
+ upgradeResult && !upgradeResult.ok && React.createElement('div', {
1677
2098
  style: {
1678
- background: upgradeResult.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
1679
- border: `1px solid ${upgradeResult.ok ? 'var(--dsw-alias-state-success-border,#a7f3d0)' : 'var(--dsw-alias-state-error-border,#fecaca)'}`,
1680
- color: upgradeResult.ok ? 'var(--dsw-alias-state-success-primary,#065f46)' : 'var(--dsw-alias-state-error-primary,#991b1b)',
2099
+ background: 'var(--dsw-alias-state-error-bg,#fef2f2)',
2100
+ border: '1px solid var(--dsw-alias-state-error-border,#fecaca)',
2101
+ color: 'var(--dsw-alias-state-error-primary,#991b1b)',
1681
2102
  padding: '8px 12px',
1682
2103
  borderRadius: 6,
1683
2104
  fontSize: 12,
@@ -1713,15 +2134,19 @@ const TABS = [
1713
2134
  { id: 'tunnel', label: '公网隧道', icon: Icons.tunnel },
1714
2135
  { id: 'im', label: 'IM 机器人', icon: Icons.bot },
1715
2136
  { id: 'security', label: '安全认证', icon: Icons.security },
2137
+ { id: 'ops', label: '运维监控', icon: Icons.ops },
1716
2138
  ];
1717
2139
 
1718
2140
  function TabBar({ active, onChange, dots }) {
1719
2141
  return React.createElement('div', {
2142
+ className: 'dsh-tabbar-container',
1720
2143
  style: {
1721
2144
  display: 'flex', gap: 4, marginBottom: 20,
1722
2145
  borderBottom: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
1723
2146
  overflowX: 'auto', WebkitOverflowScrolling: 'touch',
1724
2147
  maxWidth: '100%', flexWrap: 'nowrap',
2148
+ scrollbarWidth: 'none',
2149
+ msOverflowStyle: 'none',
1725
2150
  },
1726
2151
  },
1727
2152
  TABS.map(({ id, label, icon: TabIcon }) => {
@@ -2052,6 +2477,16 @@ function BridgePanel({ rpcCall }) {
2052
2477
  rpcCall: authRpcCall,
2053
2478
  onUpdate: () => load(true),
2054
2479
  });
2480
+ } else if (activeTab === 'ops') {
2481
+ tabContent = React.createElement(React.Fragment, null,
2482
+ React.createElement(SystemMetricsWidget, { metrics: status?.system }),
2483
+ React.createElement(NetworkDiagnosticWidget, { rpcCall: authRpcCall }),
2484
+ React.createElement(BackupRestoreWidget, {
2485
+ rpcCall: authRpcCall,
2486
+ onUpdate: () => load(true),
2487
+ }),
2488
+ React.createElement(RestartDshCard, { rpcCall: authRpcCall }),
2489
+ );
2055
2490
  } else if (activeTab === 'im') {
2056
2491
  // 从 listPlatforms 动态生成平台列表
2057
2492
  const IM_PLATFORMS = [
@@ -2348,6 +2783,17 @@ function injectMobileStyles() {
2348
2783
  const style = document.createElement('style');
2349
2784
  style.id = 'dsh-bridge-mobile-styles';
2350
2785
  style.textContent = `
2786
+ /* DSH Bridge 隐藏 Tab 栏原生滚动条并保持平滑滑动 */
2787
+ .dsh-tabbar-container {
2788
+ scrollbar-width: none !important;
2789
+ -ms-overflow-style: none !important;
2790
+ }
2791
+ .dsh-tabbar-container::-webkit-scrollbar {
2792
+ display: none !important;
2793
+ width: 0 !important;
2794
+ height: 0 !important;
2795
+ }
2796
+
2351
2797
  /* DSH Bridge 移动端自适应与触控交互增强样式 */
2352
2798
  :root {
2353
2799
  --dsh-mobile-header-h: 52px;
@@ -84,6 +84,7 @@
84
84
  | `/use <编号>` | 切换到指定编号会话 | `/use 1` 或 `/resume 1` |
85
85
  | `/new <提示词>` | 在当前工作区创建新会话并开始 | `/new 帮我写个脚本` |
86
86
  | `/new <词> @N` | 在指定工作区新建会话 | `/new 帮我写个脚本 @1` |
87
+ | `/rename <新标题>` | 重命名当前活动会话 | `/rename 优化登录交互` |
87
88
  | `/workspaces` | 查看所有已注册的工作区列表 | `/workspaces` |
88
89
  | `/status` | 查看 Agent 运行状态看板 | `/status` |
89
90
  | `/stop` | 中断停止当前正在执行的任务 | `/stop` |
package/docs/qq-usage.md CHANGED
@@ -184,6 +184,7 @@ AI 生成过程中,QQ 会显示机器人的"正在输入"状态:
184
184
  - `/new <提示词>` - 新建会话并开始
185
185
  - `/sessions`(或 `/list`)- 列出所有会话(按工作区分组)
186
186
  - `/use N`(或 `/resume N`)- 切换到/恢复会话 N
187
+ - `/rename <新标题>` - 重命名当前活动会话
187
188
  - `/end` - 结束当前会话(清除活动会话并触发快捷按钮)
188
189
  - `/stop` - 停止当前任务
189
190
  - `/status` - 查看状态与会话摘要
@@ -69,6 +69,7 @@
69
69
  | *(普通文本)* | 发送给当前活动 Agent 执行任务 | 实时打字机流式输出 |
70
70
  | `/new <提示词>` | 在当前工作区新建会话并立即执行 | 启动全新轮次 |
71
71
  | `/new <提示词> @N` | 在指定工作区序号新建会话 | 多工作区调度 |
72
+ | `/rename <新标题>` | 重命名当前活动会话标题 | 修改会话名称 |
72
73
  | `/sessions`(或 `/list`) | 查看所有会话列表 | 附带一键切换按键 |
73
74
  | `/use N`(或 `/resume N`) | 切换活动会话至序号 N | 快速切换上下文 |
74
75
  | `/workspaces` | 列出本地所有可用项目工作区 | 查看工作区路径 |