@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/CHANGELOG.md +127 -0
- package/README.en.md +65 -33
- package/README.md +65 -33
- package/client/client.js +748 -81
- package/client/index.js +634 -43
- package/docs/feishu-usage.md +17 -0
- package/docs/qq-usage.md +1 -0
- package/docs/telegram-usage.md +1 -0
- package/docs/wechat-usage.md +1 -0
- package/lib/auth/login-template.js +4 -0
- package/lib/bridge-rpc-constants.js +7 -0
- package/lib/bridge-rpc.js +58 -1
- package/lib/cloudflared-manager.mjs +54 -8
- package/lib/index.js +361 -13
- package/lib/platform/conversation-bridge.js +92 -46
- package/lib/qq/gateway.js +5 -2
- package/lib/telegram/gateway.js +25 -17
- package/lib/wechat/gateway.js +27 -13
- package/package.json +3 -2
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({
|
|
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(
|
|
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
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1346
|
-
|
|
1347
|
-
color: 'var(--dsw-alias-
|
|
1348
|
-
|
|
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
|
-
},
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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}
|
|
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
|
|
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
|
-
|
|
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-
|
|
1540
|
-
background: 'rgba(255, 255, 255, 0.
|
|
1541
|
-
border: '1px solid rgba(191, 219, 254, 0.
|
|
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
|
|
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
|
-
|
|
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:
|
|
1556
|
-
border:
|
|
1557
|
-
color:
|
|
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:
|
|
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;
|