@wenbin_wb/dsh-bridge 2.6.0 → 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', {
@@ -351,7 +358,19 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
351
358
  );
352
359
  });
353
360
 
354
- const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart, onStop, onReset, auth, onNavigateSecurity, children }) {
361
+ const TunnelCard = React.memo(function TunnelCard({
362
+ title,
363
+ desc,
364
+ data,
365
+ autoStart,
366
+ onToggleAutoStart,
367
+ onStart,
368
+ onStop,
369
+ onReset,
370
+ auth,
371
+ onNavigateSecurity,
372
+ children
373
+ }) {
355
374
  const { running, configured, url, qr, state } = data ?? {};
356
375
  const phase = state?.phase ?? 'idle';
357
376
 
@@ -361,7 +380,28 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
361
380
  React.createElement('div', { style: s.label }, title),
362
381
  React.createElement('div', { style: { ...s.muted, marginTop: 2 } }, desc),
363
382
  ),
364
- React.createElement(StatusTag, { running }),
383
+ React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 } },
384
+ React.createElement(StatusTag, { running }),
385
+ onToggleAutoStart && React.createElement('label', {
386
+ style: {
387
+ display: 'flex',
388
+ alignItems: 'center',
389
+ gap: 4,
390
+ fontSize: 11,
391
+ color: 'var(--dsw-alias-label-secondary,#6b7280)',
392
+ cursor: 'pointer',
393
+ userSelect: 'none',
394
+ },
395
+ title: 'DSH 启动时自动恢复该隧道的运行状态',
396
+ },
397
+ React.createElement('input', {
398
+ type: 'checkbox',
399
+ checked: Boolean(autoStart),
400
+ onChange: (e) => onToggleAutoStart(e.target.checked),
401
+ }),
402
+ '随 DSH 启动自动开启',
403
+ ),
404
+ ),
365
405
  ),
366
406
  children,
367
407
  phase !== 'idle' && phase !== 'ready' && React.createElement('div', {
@@ -385,6 +425,96 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
385
425
  );
386
426
  });
387
427
 
428
+ // Cloudflare 命名隧道(固定域名 / Token)高级配置表单
429
+ const CloudflareConfigForm = React.memo(function CloudflareConfigForm({ token, hostname, onSave }) {
430
+ const [open, setOpen] = React.useState(Boolean(token || hostname));
431
+ const [tokenVal, setTokenVal] = React.useState(token || '');
432
+ const [hostnameVal, setHostnameVal] = React.useState(hostname || '');
433
+ const [saving, setSaving] = React.useState(false);
434
+ const [msg, setMsg] = React.useState(null);
435
+
436
+ React.useEffect(() => {
437
+ setTokenVal(token || '');
438
+ setHostnameVal(hostname || '');
439
+ }, [token, hostname]);
440
+
441
+ const handleSave = async (e) => {
442
+ e.preventDefault();
443
+ setSaving(true);
444
+ setMsg(null);
445
+ try {
446
+ await onSave({ token: tokenVal, hostname: hostnameVal });
447
+ setMsg({ ok: true, text: '✓ 固定域名配置已保存' });
448
+ } catch (err) {
449
+ setMsg({ ok: false, text: err.message || '保存失败' });
450
+ } finally {
451
+ setSaving(false);
452
+ }
453
+ };
454
+
455
+ return React.createElement('div', {
456
+ style: {
457
+ ...s.block,
458
+ borderTop: '1px solid var(--dsw-alias-border-secondary, #e5e7eb)',
459
+ paddingTop: 10,
460
+ marginTop: 10,
461
+ },
462
+ },
463
+ React.createElement('div', {
464
+ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', userSelect: 'none' },
465
+ onClick: () => setOpen(v => !v),
466
+ },
467
+ React.createElement('div', { style: { fontSize: 12, fontWeight: 500, color: 'var(--dsw-alias-brand-primary, #3b82f6)' } },
468
+ '⚙️ 高级配置:固定域名 (Cloudflare Token) ',
469
+ (token || hostname) && React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-state-success-primary, #059669)', fontWeight: 400 } }, '● 已配置固定域名')
470
+ ),
471
+ React.createElement('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary, #9ca3af)' } }, open ? '▴ 折叠' : '▾ 展开'),
472
+ ),
473
+ open && React.createElement('form', { onSubmit: handleSave, style: { marginTop: 10 } },
474
+ React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary, #6b7280)', marginBottom: 8, lineHeight: 1.5 } },
475
+ '在 Cloudflare Zero Trust 控制台创建 Tunnel 即可获取专属 Token 并绑定自己的域名(如 dsh.yourname.com),每次重启 URL 永不变更。不填则使用默认免登录临时随机域名。'
476
+ ),
477
+ React.createElement('div', { style: { marginBottom: 8 } },
478
+ React.createElement('input', {
479
+ style: s.input,
480
+ type: 'text',
481
+ placeholder: '自定义固定域名 (例如: dsh.yourdomain.com)',
482
+ value: hostnameVal,
483
+ onChange: (e) => setHostnameVal(e.target.value),
484
+ }),
485
+ ),
486
+ React.createElement('div', { style: { marginBottom: 8 } },
487
+ React.createElement('input', {
488
+ style: s.input,
489
+ type: 'password',
490
+ placeholder: 'Tunnel Token (例如: eyJhIjoi...)',
491
+ value: tokenVal,
492
+ onChange: (e) => setTokenVal(e.target.value),
493
+ }),
494
+ ),
495
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
496
+ React.createElement('button', {
497
+ type: 'submit',
498
+ style: { ...s.btnPri, height: 28, fontSize: 12, padding: '0 12px' },
499
+ disabled: saving,
500
+ }, saving ? '保存中…' : '保存固定域名配置'),
501
+ (tokenVal || hostnameVal) && React.createElement('button', {
502
+ type: 'button',
503
+ style: { ...s.btnGhost, height: 28, fontSize: 12, padding: '0 10px' },
504
+ onClick: () => {
505
+ setTokenVal('');
506
+ setHostnameVal('');
507
+ onSave({ token: '', hostname: '' });
508
+ },
509
+ }, '清除'),
510
+ msg && React.createElement('span', {
511
+ style: { fontSize: 12, color: msg.ok ? 'var(--dsw-alias-state-success-primary, #059669)' : 'var(--dsw-alias-state-error-primary, #dc2626)' },
512
+ }, msg.text),
513
+ ),
514
+ ),
515
+ );
516
+ });
517
+
388
518
  // ---- 访问安全认证卡片 ----
389
519
 
390
520
  const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpdate }) {
@@ -1322,8 +1452,6 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1322
1452
  ),
1323
1453
  ),
1324
1454
 
1325
-
1326
-
1327
1455
  React.createElement('div', { style: s.block },
1328
1456
  React.createElement('div', { style: { ...s.tip, fontSize: 12 } },
1329
1457
  platformId === 'wechat'
@@ -1336,29 +1464,340 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1336
1464
  );
1337
1465
  }
1338
1466
 
1339
- // 单条升级命令行:命令文本 + 复制按钮
1340
- function UpgradeCommandRow({ cmd }) {
1341
- const [copied, copy] = useCopy();
1342
- return React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
1343
- 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', {
1344
1586
  style: {
1345
- ...s.code,
1346
- fontSize: 11,
1347
- color: 'var(--dsw-alias-label-secondary,#6b7280)',
1348
- flex: 1,
1349
- minWidth: 0,
1350
- wordBreak: 'break-all',
1351
- background: 'var(--dsw-alias-bg-layer-1,#ffffff)',
1352
- padding: '4px 8px',
1353
- borderRadius: 6,
1354
- 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,
1355
1591
  },
1356
- }, cmd),
1357
- React.createElement('button', {
1358
- style: { ...s.btnGhost, height: 26, padding: '0 10px', fontSize: 12, flexShrink: 0 },
1359
- onClick: () => copy(cmd),
1360
- title: '复制升级命令',
1361
- }, 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
+ ),
1362
1801
  );
1363
1802
  }
1364
1803
 
@@ -1369,6 +1808,9 @@ function VersionBanner({ rpcCall }) {
1369
1808
  const [upgrading, setUpgrading] = React.useState(false);
1370
1809
  const [upgradeResult, setUpgradeResult] = React.useState(null);
1371
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);
1372
1814
 
1373
1815
  const check = React.useCallback(async () => {
1374
1816
  setLoading(true);
@@ -1389,11 +1831,12 @@ function VersionBanner({ rpcCall }) {
1389
1831
  if (!info?.latest || upgrading) return;
1390
1832
  setUpgrading(true);
1391
1833
  setUpgradeResult(null);
1834
+ setDismissRestart(false);
1835
+ setRestartStatus(null);
1392
1836
  try {
1393
1837
  const r = await rpcCall(BRIDGE_ENDPOINTS.upgradePlugin, { version: info.latest });
1394
1838
  if (r?.ok && r.value?.ok) {
1395
- setUpgradeResult({ ok: true, message: `已成功升级到 v${info.latest}!请重启 DSH 服务使新版本生效。` });
1396
- setTimeout(() => check(), 3000);
1839
+ setUpgradeResult({ ok: true, message: `已成功升级到 v${info.latest}!` });
1397
1840
  } else {
1398
1841
  setUpgradeResult({ ok: false, message: r?.value?.error || r?.error?.message || '升级失败' });
1399
1842
  setShowManual(true);
@@ -1404,9 +1847,49 @@ function VersionBanner({ rpcCall }) {
1404
1847
  } finally {
1405
1848
  setUpgrading(false);
1406
1849
  }
1407
- }, [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));
1408
1865
 
1409
- const links = React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' } },
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]);
1891
+
1892
+ const links = React.createElement('div', { style: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } },
1410
1893
  React.createElement('a', {
1411
1894
  href: GITHUB_URL, target: '_blank', rel: 'noreferrer', style: s.btnLink,
1412
1895
  }, React.createElement(Icons.github), 'GitHub'),
@@ -1475,7 +1958,7 @@ function VersionBanner({ rpcCall }) {
1475
1958
  gap: 4,
1476
1959
  },
1477
1960
  onClick: check,
1478
- disabled: loading || upgrading,
1961
+ disabled: loading || upgrading || restarting,
1479
1962
  title: '重新检查 npm 线上版本',
1480
1963
  },
1481
1964
  React.createElement(Icons.refresh),
@@ -1516,10 +1999,10 @@ function VersionBanner({ rpcCall }) {
1516
1999
  background: upgradeResult?.ok
1517
2000
  ? 'var(--dsw-alias-state-success-primary,#059669)'
1518
2001
  : 'var(--dsw-alias-brand-primary,#4f6ef7)',
1519
- opacity: upgrading ? 0.6 : 1,
2002
+ opacity: (upgrading || restarting) ? 0.6 : 1,
1520
2003
  },
1521
2004
  onClick: handleUpgrade,
1522
- disabled: upgrading || upgradeResult?.ok,
2005
+ disabled: upgrading || restarting || upgradeResult?.ok,
1523
2006
  },
1524
2007
  upgrading
1525
2008
  ? React.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6 } },
@@ -1527,7 +2010,7 @@ function VersionBanner({ rpcCall }) {
1527
2010
  '正在自动升级…',
1528
2011
  )
1529
2012
  : upgradeResult?.ok
1530
- ? '✓ 已完成升级'
2013
+ ? '✓ 升级完成'
1531
2014
  : `一键升级到 v${info.latest}`
1532
2015
  ),
1533
2016
  ),
@@ -1536,9 +2019,9 @@ function VersionBanner({ rpcCall }) {
1536
2019
  info?.releaseNotes && React.createElement('div', {
1537
2020
  style: {
1538
2021
  fontSize: 12,
1539
- color: 'var(--dsw-alias-label-secondary,#374151)',
1540
- background: 'rgba(255, 255, 255, 0.75)',
1541
- 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))',
1542
2025
  borderRadius: 8,
1543
2026
  padding: '8px 12px',
1544
2027
  marginBottom: 10,
@@ -1546,15 +2029,76 @@ function VersionBanner({ rpcCall }) {
1546
2029
  whiteSpace: 'pre-line',
1547
2030
  },
1548
2031
  },
1549
- 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 } }, '✨ 更新亮点:'),
1550
2033
  info.releaseNotes
1551
2034
  ),
1552
2035
 
1553
- 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', {
1554
2098
  style: {
1555
- background: upgradeResult.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
1556
- border: `1px solid ${upgradeResult.ok ? 'var(--dsw-alias-state-success-border,#a7f3d0)' : 'var(--dsw-alias-state-error-border,#fecaca)'}`,
1557
- 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)',
1558
2102
  padding: '8px 12px',
1559
2103
  borderRadius: 6,
1560
2104
  fontSize: 12,
@@ -1590,15 +2134,19 @@ const TABS = [
1590
2134
  { id: 'tunnel', label: '公网隧道', icon: Icons.tunnel },
1591
2135
  { id: 'im', label: 'IM 机器人', icon: Icons.bot },
1592
2136
  { id: 'security', label: '安全认证', icon: Icons.security },
2137
+ { id: 'ops', label: '运维监控', icon: Icons.ops },
1593
2138
  ];
1594
2139
 
1595
2140
  function TabBar({ active, onChange, dots }) {
1596
2141
  return React.createElement('div', {
2142
+ className: 'dsh-tabbar-container',
1597
2143
  style: {
1598
2144
  display: 'flex', gap: 4, marginBottom: 20,
1599
2145
  borderBottom: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
1600
2146
  overflowX: 'auto', WebkitOverflowScrolling: 'touch',
1601
2147
  maxWidth: '100%', flexWrap: 'nowrap',
2148
+ scrollbarWidth: 'none',
2149
+ msOverflowStyle: 'none',
1602
2150
  },
1603
2151
  },
1604
2152
  TABS.map(({ id, label, icon: TabIcon }) => {
@@ -1824,8 +2372,18 @@ function BridgePanel({ rpcCall }) {
1824
2372
  const onResetCloudflared = React.useCallback(() =>
1825
2373
  act(BRIDGE_ENDPOINTS.stopCloudflared).then(() => act(BRIDGE_ENDPOINTS.startCloudflared))
1826
2374
  , [act]);
2375
+ const onToggleCloudflaredAutoStart = React.useCallback((autoStart) =>
2376
+ act(BRIDGE_ENDPOINTS.setTunnelAutoStart, { tunnel: 'cloudflared', autoStart })
2377
+ , [act]);
2378
+ const saveCloudflaredConfig = React.useCallback(({ token, hostname }) =>
2379
+ act(BRIDGE_ENDPOINTS.saveCloudflaredConfig, { token, hostname })
2380
+ , [act]);
2381
+
1827
2382
  const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
1828
2383
  const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
2384
+ const onToggleCustomAutoStart = React.useCallback((autoStart) =>
2385
+ act(BRIDGE_ENDPOINTS.setTunnelAutoStart, { tunnel: 'customTunnel', autoStart })
2386
+ , [act]);
1829
2387
  const saveConfig = React.useCallback((serverUrl, accessToken) =>
1830
2388
  act(BRIDGE_ENDPOINTS.saveCustomTunnelConfig, { serverUrl, accessToken })
1831
2389
  , [act]);
@@ -1865,19 +2423,29 @@ function BridgePanel({ rpcCall }) {
1865
2423
  tabContent = React.createElement(React.Fragment, null,
1866
2424
  React.createElement(TunnelCard, {
1867
2425
  title: 'Cloudflare 隧道',
1868
- desc: '一键获取公网地址(重启后 URL 会变化)',
2426
+ desc: status?.cloudflared?.tokenConfigured
2427
+ ? '固定域名模式(Token 运行 · 重启 URL 保持不变)'
2428
+ : '一键获取公网地址(免登录临时随机域名)',
1869
2429
  data: {
1870
2430
  running: status?.cloudflared?.running,
1871
2431
  url: status?.cloudflared?.url,
1872
2432
  qr: status?.cloudflared?.qr,
1873
2433
  state: status?.cloudflared?.state,
1874
2434
  },
2435
+ autoStart: status?.cloudflared?.autoStart,
2436
+ onToggleAutoStart: onToggleCloudflaredAutoStart,
1875
2437
  auth: status?.auth,
1876
2438
  onNavigateSecurity: navSecurity,
1877
2439
  onStart: onStartCloudflared,
1878
2440
  onStop: onStopCloudflared,
1879
2441
  onReset: status?.cloudflared?.running ? onResetCloudflared : null,
1880
- }),
2442
+ },
2443
+ React.createElement(CloudflareConfigForm, {
2444
+ token: status?.cloudflared?.token ?? '',
2445
+ hostname: status?.cloudflared?.hostname ?? '',
2446
+ onSave: saveCloudflaredConfig,
2447
+ }),
2448
+ ),
1881
2449
  React.createElement(TunnelCard, {
1882
2450
  title: '自建隧道',
1883
2451
  desc: '连接自己部署的隧道服务器,获得固定域名',
@@ -1888,6 +2456,8 @@ function BridgePanel({ rpcCall }) {
1888
2456
  qr: ct?.qr,
1889
2457
  state: ct?.state,
1890
2458
  },
2459
+ autoStart: ct?.autoStart,
2460
+ onToggleAutoStart: onToggleCustomAutoStart,
1891
2461
  auth: status?.auth,
1892
2462
  onNavigateSecurity: navSecurity,
1893
2463
  onStart: onStartCustom,
@@ -1907,6 +2477,16 @@ function BridgePanel({ rpcCall }) {
1907
2477
  rpcCall: authRpcCall,
1908
2478
  onUpdate: () => load(true),
1909
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
+ );
1910
2490
  } else if (activeTab === 'im') {
1911
2491
  // 从 listPlatforms 动态生成平台列表
1912
2492
  const IM_PLATFORMS = [
@@ -2203,6 +2783,17 @@ function injectMobileStyles() {
2203
2783
  const style = document.createElement('style');
2204
2784
  style.id = 'dsh-bridge-mobile-styles';
2205
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
+
2206
2797
  /* DSH Bridge 移动端自适应与触控交互增强样式 */
2207
2798
  :root {
2208
2799
  --dsh-mobile-header-h: 52px;