@wenbin_wb/dsh-bridge 2.10.5 → 2.10.6
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 +16 -0
- package/README.en.md +410 -410
- package/README.md +432 -432
- package/client/client.js +354 -65
- package/client/index.js +302 -64
- package/docs/CODE_REVIEW.md +220 -0
- package/docs/telegram-usage.md +1 -1
- package/lib/cloudflared-manager.mjs +513 -361
- package/lib/compat.js +129 -129
- package/lib/feishu/index.js +225 -225
- package/lib/feishu/node.js +439 -439
- package/lib/index.js +43 -8
- package/lib/platform/base.js +147 -147
- package/lib/platform/commands.js +221 -221
- package/lib/platform/conversation-bridge.js +821 -821
- package/lib/platform/dsh-storage.js +117 -117
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +229 -229
- package/lib/platform/session-catalog.js +372 -372
- package/lib/platform/stream-slices.js +21 -21
- package/lib/qq/index.js +312 -312
- package/lib/telegram/index.js +216 -216
- package/lib/telegram/node.js +322 -322
- package/lib/wechat/gateway.js +986 -986
- package/lib/wechat/index.js +244 -244
- package/lib/wechat/media.js +285 -285
- package/lib/wechat/node.js +352 -352
- package/package.json +2 -2
- package/docs/fix-plan-202608.md +0 -108
- package/docs/fix-tunnel-sse-and-session-list.md +0 -134
- package/docs/platform-abstraction-design.md +0 -473
- package/docs/wechat-bot-plan.md +0 -294
package/client/index.js
CHANGED
|
@@ -213,14 +213,14 @@ function StatusTag({ running, status }) {
|
|
|
213
213
|
bg = 'var(--dsw-alias-state-success-bg,#ecfdf5)';
|
|
214
214
|
color = 'var(--dsw-alias-state-success-primary,#059669)';
|
|
215
215
|
text = '已连接';
|
|
216
|
-
} else if (status === 'starting') {
|
|
216
|
+
} else if (status === 'starting' || status === 'connecting' || status === 'downloading') {
|
|
217
217
|
bg = 'var(--dsw-alias-state-info-bg,#eff6ff)';
|
|
218
218
|
color = 'var(--dsw-alias-state-info-primary,#3b82f6)';
|
|
219
|
-
text = '连接中…';
|
|
219
|
+
text = status === 'downloading' ? '下载中…' : '连接中…';
|
|
220
220
|
} else if (status === 'reconnecting') {
|
|
221
221
|
bg = 'var(--dsw-alias-state-warn-bg,#fffbeb)';
|
|
222
222
|
color = 'var(--dsw-alias-state-warn-primary,#d97706)';
|
|
223
|
-
text = '
|
|
223
|
+
text = '自动重连中…';
|
|
224
224
|
} else if (status === 'paused') {
|
|
225
225
|
bg = 'var(--dsw-alias-state-warn-bg,#fffbeb)';
|
|
226
226
|
color = 'var(--dsw-alias-state-warn-primary,#d97706)';
|
|
@@ -416,6 +416,146 @@ const CustomTunnelGuide = React.memo(function CustomTunnelGuide() {
|
|
|
416
416
|
);
|
|
417
417
|
});
|
|
418
418
|
|
|
419
|
+
// ── 公网入口总览卡:把"当前生效的访问地址"提到页面主角位置 ──────────────
|
|
420
|
+
const TunnelEntryCard = React.memo(function TunnelEntryCard({
|
|
421
|
+
entry, onCopy, copied, autoStart, onToggleAutoStart, onStart, onStop, onReset,
|
|
422
|
+
}) {
|
|
423
|
+
const [showQr, setShowQr] = React.useState(false);
|
|
424
|
+
const hasUrl = Boolean(entry && entry.url);
|
|
425
|
+
const active = Boolean(entry && entry.running);
|
|
426
|
+
|
|
427
|
+
return React.createElement('div', {
|
|
428
|
+
style: {
|
|
429
|
+
...s.card,
|
|
430
|
+
borderColor: active ? 'var(--dsw-alias-state-success-border,#a7f3d0)' : undefined,
|
|
431
|
+
background: active ? 'linear-gradient(180deg, var(--dsw-alias-bg-layer-2,#f9fafb), var(--dsw-alias-bg-layer-1,#ffffff))' : undefined,
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 10 } },
|
|
435
|
+
React.createElement('div', { style: { flex: '1 1 auto', minWidth: 0 } },
|
|
436
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
437
|
+
React.createElement('span', { style: { fontSize: 16 } }, '🌐'),
|
|
438
|
+
React.createElement('div', { style: s.label }, '公网访问入口'),
|
|
439
|
+
),
|
|
440
|
+
React.createElement('div', { style: { ...s.muted, marginTop: 4 } },
|
|
441
|
+
entry ? (entry.title + ' · ' + entry.desc) : '尚未配置任何公网隧道,可在下方开启 Cloudflare 隧道或配置自建隧道'
|
|
442
|
+
),
|
|
443
|
+
),
|
|
444
|
+
React.createElement(StatusTag, {
|
|
445
|
+
running: active,
|
|
446
|
+
status: (entry && entry.phase && entry.phase !== 'ready') ? entry.phase : undefined,
|
|
447
|
+
}),
|
|
448
|
+
),
|
|
449
|
+
|
|
450
|
+
React.createElement('div', { style: { ...s.block, display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
451
|
+
// 有地址:大号 URL + 复制
|
|
452
|
+
hasUrl && React.createElement('div', {
|
|
453
|
+
style: {
|
|
454
|
+
padding: '10px 12px',
|
|
455
|
+
background: 'var(--dsw-alias-bg-layer-1,#ffffff)',
|
|
456
|
+
border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
|
|
457
|
+
borderRadius: 10,
|
|
458
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
React.createElement('code', {
|
|
462
|
+
style: { ...s.code, flex: '1 1 auto', fontSize: 13.5, wordBreak: 'break-all', lineHeight: 1.5 },
|
|
463
|
+
}, entry.url),
|
|
464
|
+
React.createElement('button', {
|
|
465
|
+
style: { ...s.btnGhost, flexShrink: 0, height: 28, padding: '0 12px', fontSize: 12 },
|
|
466
|
+
onClick: () => onCopy && onCopy(entry.url),
|
|
467
|
+
}, copied ? '✓ 已复制' : '复制'),
|
|
468
|
+
),
|
|
469
|
+
// 状态细节(重连/错误/连接中)——无 URL 时格外重要,让用户知道隧道在自愈而非消失
|
|
470
|
+
entry && entry.stateDetail && React.createElement('div', {
|
|
471
|
+
style: {
|
|
472
|
+
fontSize: 12, lineHeight: 1.5,
|
|
473
|
+
color: entry.phase === 'error' ? 'var(--dsw-alias-state-error-primary,#dc2626)'
|
|
474
|
+
: entry.phase === 'reconnecting' ? 'var(--dsw-alias-state-warn-primary,#d97706)'
|
|
475
|
+
: 'var(--dsw-alias-label-secondary,#6b7280)',
|
|
476
|
+
},
|
|
477
|
+
}, entry.stateDetail),
|
|
478
|
+
// 操作行:running → 关闭/停止重连(+ 重置);!running → 开启
|
|
479
|
+
active && onStop && React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
480
|
+
React.createElement('button', {
|
|
481
|
+
style: s.btnGhost,
|
|
482
|
+
onClick: onStop,
|
|
483
|
+
}, entry && entry.phase === 'reconnecting' ? '停止重连' : '关闭'),
|
|
484
|
+
onReset && React.createElement('button', {
|
|
485
|
+
style: { ...s.btnGhost, height: 28, padding: '0 12px', fontSize: 12 },
|
|
486
|
+
onClick: onReset,
|
|
487
|
+
title: '关闭并重新开启,更换临时地址',
|
|
488
|
+
}, '🔄 重置链接'),
|
|
489
|
+
),
|
|
490
|
+
// running 但无 onStop 的入口(如外部登记):只给重置/二维码辅助,无开关
|
|
491
|
+
active && !onStop && onReset && React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
492
|
+
React.createElement('button', {
|
|
493
|
+
style: { ...s.btnGhost, height: 28, padding: '0 12px', fontSize: 12 },
|
|
494
|
+
onClick: onReset,
|
|
495
|
+
title: '关闭并重新开启,更换临时地址',
|
|
496
|
+
}, '🔄 重置链接'),
|
|
497
|
+
),
|
|
498
|
+
// 未运行:引导开启(连接中/下载中禁用)
|
|
499
|
+
!active && onStart && React.createElement('button', {
|
|
500
|
+
style: {
|
|
501
|
+
...s.btnPri, alignSelf: 'flex-start',
|
|
502
|
+
opacity: (entry && entry.configured === false) ? 0.4 : 1,
|
|
503
|
+
background: (entry && entry.phase === 'connecting') ? 'var(--dsw-alias-state-info-primary,#3b82f6)' : undefined,
|
|
504
|
+
},
|
|
505
|
+
onClick: onStart,
|
|
506
|
+
disabled: Boolean((entry && entry.configured === false) || (entry && (entry.phase === 'connecting' || entry.phase === 'downloading'))),
|
|
507
|
+
title: (entry && entry.configured === false) ? '请先在「隧道配置」中保存服务器配置' : '',
|
|
508
|
+
}, (entry && entry.phase === 'connecting') ? '连接中…' : (entry && entry.phase === 'downloading') ? '下载中…' : '开启公网隧道'),
|
|
509
|
+
// 二维码辅助按钮:有地址未运行时也可查看(外部登记等)
|
|
510
|
+
hasUrl && entry.qr && React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
511
|
+
React.createElement('button', {
|
|
512
|
+
style: { ...s.btnGhost, height: 28, padding: '0 12px', fontSize: 12 },
|
|
513
|
+
onClick: () => setShowQr((v) => !v),
|
|
514
|
+
}, showQr ? '隐藏二维码' : '显示二维码'),
|
|
515
|
+
),
|
|
516
|
+
showQr && hasUrl && entry.qr && React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 6 } },
|
|
517
|
+
React.createElement('img', { src: entry.qr, alt: 'QR', style: { ...s.qr, margin: 0 } }),
|
|
518
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11 } }, '请在私密环境下扫码使用'),
|
|
519
|
+
),
|
|
520
|
+
),
|
|
521
|
+
|
|
522
|
+
onToggleAutoStart && React.createElement('label', {
|
|
523
|
+
style: {
|
|
524
|
+
display: 'flex', alignItems: 'center', gap: 6, marginTop: 12, paddingTop: 10,
|
|
525
|
+
borderTop: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
|
|
526
|
+
fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', cursor: 'pointer', userSelect: 'none',
|
|
527
|
+
},
|
|
528
|
+
title: 'DSH 启动时自动恢复该隧道的运行状态',
|
|
529
|
+
},
|
|
530
|
+
React.createElement('input', {
|
|
531
|
+
type: 'checkbox',
|
|
532
|
+
checked: Boolean(autoStart),
|
|
533
|
+
onChange: (e) => onToggleAutoStart(e.target.checked),
|
|
534
|
+
}),
|
|
535
|
+
React.createElement('span', null, '随 DSH 启动自动开启'),
|
|
536
|
+
),
|
|
537
|
+
);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// 隧道配置折叠分组:把 CF / 自建 / 外部 三套"不常用"配置收进一处
|
|
541
|
+
const TunnelConfigGroup = React.memo(function TunnelConfigGroup({ children }) {
|
|
542
|
+
const [open, setOpen] = React.useState(false);
|
|
543
|
+
return React.createElement('div', { style: s.card },
|
|
544
|
+
React.createElement('div', {
|
|
545
|
+
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', userSelect: 'none', gap: 8 },
|
|
546
|
+
onClick: () => setOpen((v) => !v),
|
|
547
|
+
},
|
|
548
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
549
|
+
React.createElement('span', { style: { fontSize: 14 } }, '⚙️'),
|
|
550
|
+
React.createElement('div', { style: s.label }, '隧道配置'),
|
|
551
|
+
React.createElement('span', { style: { ...s.muted, fontSize: 11 } }, 'Token · 固定域名 · 自建服务器 · 外部登记'),
|
|
552
|
+
),
|
|
553
|
+
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary,#9ca3af)', flexShrink: 0 } }, open ? '收起 ▴' : '展开 ▾'),
|
|
554
|
+
),
|
|
555
|
+
open && React.createElement('div', { style: { marginTop: 4 } }, children),
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
|
|
419
559
|
const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serverUrl: initUrl, accessToken: initToken, onSave }) {
|
|
420
560
|
const [serverUrl, setServerUrl] = React.useState(initUrl ?? '');
|
|
421
561
|
const [accessToken, setAccessToken] = React.useState(initToken ?? '');
|
|
@@ -511,7 +651,7 @@ const TunnelCard = React.memo(function TunnelCard({
|
|
|
511
651
|
React.createElement('div', { style: { ...s.muted, marginTop: 2 } }, desc),
|
|
512
652
|
),
|
|
513
653
|
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 } },
|
|
514
|
-
React.createElement(StatusTag, { running }),
|
|
654
|
+
React.createElement(StatusTag, { running, status: phase === 'ready' ? undefined : phase }),
|
|
515
655
|
onToggleAutoStart && React.createElement('label', {
|
|
516
656
|
style: {
|
|
517
657
|
display: 'flex',
|
|
@@ -537,7 +677,9 @@ const TunnelCard = React.memo(function TunnelCard({
|
|
|
537
677
|
phase !== 'idle' && phase !== 'ready' && React.createElement('div', {
|
|
538
678
|
style: {
|
|
539
679
|
...s.block, fontSize: 12,
|
|
540
|
-
color: phase === 'error' ? 'var(--dsw-alias-state-error-primary,#dc2626)'
|
|
680
|
+
color: phase === 'error' ? 'var(--dsw-alias-state-error-primary,#dc2626)'
|
|
681
|
+
: phase === 'reconnecting' ? 'var(--dsw-alias-state-warn-primary,#d97706)'
|
|
682
|
+
: 'var(--dsw-alias-label-secondary,#6b7280)',
|
|
541
683
|
},
|
|
542
684
|
}, state?.detail ?? phase),
|
|
543
685
|
url && React.createElement(QrBlock, { url, qr, onReset, auth, onNavigateSecurity }),
|
|
@@ -550,7 +692,10 @@ const TunnelCard = React.memo(function TunnelCard({
|
|
|
550
692
|
disabled: configured === false || phase === 'connecting' || phase === 'downloading',
|
|
551
693
|
title: configured === false ? '请先保存服务器配置' : '',
|
|
552
694
|
}, phase === 'connecting' ? '连接中…' : phase === 'downloading' ? '下载中…' : '开启'),
|
|
553
|
-
running && onStop && React.createElement('button', {
|
|
695
|
+
running && onStop && React.createElement('button', {
|
|
696
|
+
style: s.btnGhost,
|
|
697
|
+
onClick: onStop,
|
|
698
|
+
}, phase === 'reconnecting' ? '停止重连' : '关闭'),
|
|
554
699
|
),
|
|
555
700
|
);
|
|
556
701
|
});
|
|
@@ -2595,6 +2740,20 @@ function BridgePanel({ rpcCall }) {
|
|
|
2595
2740
|
const [platforms, setPlatforms] = React.useState(null);
|
|
2596
2741
|
const [selectedPlatform, setSelectedPlatform] = React.useState('wechat');
|
|
2597
2742
|
|
|
2743
|
+
// 隧道页"主入口"地址复制反馈
|
|
2744
|
+
const [copiedUrl, setCopiedUrl] = React.useState('');
|
|
2745
|
+
const copyEntryUrl = React.useCallback((url) => {
|
|
2746
|
+
const done = () => {
|
|
2747
|
+
setCopiedUrl(url);
|
|
2748
|
+
setTimeout(() => setCopiedUrl(''), 2000);
|
|
2749
|
+
};
|
|
2750
|
+
if (navigator.clipboard?.writeText) {
|
|
2751
|
+
navigator.clipboard.writeText(url).then(done).catch(() => done());
|
|
2752
|
+
} else {
|
|
2753
|
+
done();
|
|
2754
|
+
}
|
|
2755
|
+
}, []);
|
|
2756
|
+
|
|
2598
2757
|
// 远程设备管理权限解锁状态
|
|
2599
2758
|
const isLocalhost = typeof window === 'undefined' || (
|
|
2600
2759
|
!window.location.hostname ||
|
|
@@ -2851,58 +3010,103 @@ function BridgePanel({ rpcCall }) {
|
|
|
2851
3010
|
);
|
|
2852
3011
|
} else if (activeTab === 'tunnel') {
|
|
2853
3012
|
const ext = status?.externalTunnel;
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
autoStart: status?.cloudflared?.autoStart,
|
|
2867
|
-
onToggleAutoStart: onToggleCloudflaredAutoStart,
|
|
2868
|
-
auth: status?.auth,
|
|
2869
|
-
onNavigateSecurity: navSecurity,
|
|
2870
|
-
onStart: onStartCloudflared,
|
|
2871
|
-
onStop: onStopCloudflared,
|
|
2872
|
-
onReset: status?.cloudflared?.running ? onResetCloudflared : null,
|
|
3013
|
+
const cf = status?.cloudflared;
|
|
3014
|
+
|
|
3015
|
+
// 计算"当前主入口":优先自建隧道(固定地址)> Cloudflare > 外部登记。
|
|
3016
|
+
// running(含重连中)即入列——重连时 url 可能暂时为空,但应让用户看到状态。
|
|
3017
|
+
const cfDesc = cf?.tokenConfigured ? '固定域名模式' : '免登录临时域名';
|
|
3018
|
+
const entries = [
|
|
3019
|
+
ct && ct.running && {
|
|
3020
|
+
key: 'custom', title: '自建隧道', desc: 'VPS 自建 · 固定地址',
|
|
3021
|
+
url: ct.url || null, qr: ct.qr, running: true,
|
|
3022
|
+
phase: ct.state && ct.state.phase, stateDetail: ct.state && ct.state.detail,
|
|
3023
|
+
autoStart: ct.autoStart, onToggleAutoStart: onToggleCustomAutoStart,
|
|
3024
|
+
onStart: onStartCustom, onStop: onStopCustom,
|
|
2873
3025
|
},
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3026
|
+
cf && cf.running && {
|
|
3027
|
+
key: 'cloudflared', title: 'Cloudflare 隧道', desc: cfDesc,
|
|
3028
|
+
url: cf.url || null, qr: cf.qr, running: true,
|
|
3029
|
+
phase: cf.state && cf.state.phase, stateDetail: cf.state && cf.state.detail,
|
|
3030
|
+
autoStart: cf.autoStart, onToggleAutoStart: onToggleCloudflaredAutoStart,
|
|
3031
|
+
onStart: onStartCloudflared, onStop: onStopCloudflared,
|
|
3032
|
+
onReset: onResetCloudflared,
|
|
3033
|
+
},
|
|
3034
|
+
ext && ext.configured && ext.url && {
|
|
3035
|
+
key: 'external', title: '外部已部署隧道', desc: '自行部署登记',
|
|
3036
|
+
url: ext.url, qr: ext.qr, running: true,
|
|
3037
|
+
},
|
|
3038
|
+
].filter(Boolean);
|
|
3039
|
+
const primary = entries[0] || null;
|
|
3040
|
+
const otherCount = primary ? entries.length - 1 : entries.length;
|
|
3041
|
+
|
|
3042
|
+
tabContent = React.createElement(React.Fragment, null,
|
|
3043
|
+
React.createElement(TunnelEntryCard, {
|
|
3044
|
+
entry: primary,
|
|
3045
|
+
onCopy: copyEntryUrl,
|
|
3046
|
+
copied: Boolean(copiedUrl && primary && copiedUrl === primary.url),
|
|
3047
|
+
autoStart: primary ? primary.autoStart : undefined,
|
|
3048
|
+
onToggleAutoStart: primary ? primary.onToggleAutoStart : undefined,
|
|
3049
|
+
onStart: primary ? primary.onStart : (cf ? onStartCloudflared : null),
|
|
3050
|
+
onStop: primary ? primary.onStop : undefined,
|
|
3051
|
+
onReset: primary ? primary.onReset : undefined,
|
|
2883
3052
|
}),
|
|
2884
|
-
React.createElement(
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
3053
|
+
otherCount > 0 && React.createElement('div', {
|
|
3054
|
+
style: { ...s.muted, fontSize: 11, marginBottom: 8, textAlign: 'center' },
|
|
3055
|
+
}, '另有 ' + otherCount + ' 个隧道入口在运行,可在下方「隧道配置」中查看与管理'),
|
|
3056
|
+
|
|
3057
|
+
React.createElement(TunnelConfigGroup, null,
|
|
3058
|
+
React.createElement(TunnelCard, {
|
|
3059
|
+
title: 'Cloudflare 隧道',
|
|
3060
|
+
desc: cf && cf.tokenConfigured
|
|
3061
|
+
? '固定域名模式(Token 运行 · 重启 URL 保持不变)'
|
|
3062
|
+
: '一键获取公网地址(免登录临时随机域名)',
|
|
3063
|
+
data: {
|
|
3064
|
+
running: cf && cf.running,
|
|
3065
|
+
url: cf && cf.url,
|
|
3066
|
+
qr: cf && cf.qr,
|
|
3067
|
+
state: cf && cf.state,
|
|
3068
|
+
},
|
|
3069
|
+
autoStart: cf && cf.autoStart,
|
|
3070
|
+
onToggleAutoStart: onToggleCloudflaredAutoStart,
|
|
3071
|
+
auth: status && status.auth,
|
|
3072
|
+
onNavigateSecurity: navSecurity,
|
|
3073
|
+
onStart: onStartCloudflared,
|
|
3074
|
+
onStop: onStopCloudflared,
|
|
3075
|
+
onReset: (cf && cf.running) ? onResetCloudflared : null,
|
|
2893
3076
|
},
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
3077
|
+
React.createElement(CloudflareConfigForm, {
|
|
3078
|
+
token: (cf && cf.token) || '',
|
|
3079
|
+
hostname: (cf && cf.hostname) || '',
|
|
3080
|
+
onSave: saveCloudflaredConfig,
|
|
3081
|
+
}),
|
|
3082
|
+
),
|
|
3083
|
+
React.createElement(TunnelCard, {
|
|
3084
|
+
title: '自建隧道',
|
|
3085
|
+
desc: '连接自己部署的隧道服务器,获得固定域名',
|
|
3086
|
+
data: {
|
|
3087
|
+
configured: ct && ct.configured,
|
|
3088
|
+
running: ct && ct.running,
|
|
3089
|
+
url: ct && ct.url,
|
|
3090
|
+
qr: ct && ct.qr,
|
|
3091
|
+
state: ct && ct.state,
|
|
3092
|
+
},
|
|
3093
|
+
autoStart: ct && ct.autoStart,
|
|
3094
|
+
onToggleAutoStart: onToggleCustomAutoStart,
|
|
3095
|
+
auth: status && status.auth,
|
|
3096
|
+
onNavigateSecurity: navSecurity,
|
|
3097
|
+
onStart: onStartCustom,
|
|
3098
|
+
onStop: onStopCustom,
|
|
3099
|
+
},
|
|
3100
|
+
React.createElement(CustomTunnelGuide),
|
|
3101
|
+
React.createElement(CustomTunnelConfigForm, {
|
|
3102
|
+
serverUrl: (ct && ct.serverUrl) || '',
|
|
3103
|
+
accessToken: (ct && ct.accessToken) || '',
|
|
3104
|
+
onSave: saveConfig,
|
|
3105
|
+
}),
|
|
3106
|
+
),
|
|
3107
|
+
React.createElement(ExternalTunnelCard, {
|
|
3108
|
+
ext,
|
|
3109
|
+
onSave: saveExternalTunnel,
|
|
2906
3110
|
}),
|
|
2907
3111
|
),
|
|
2908
3112
|
);
|
|
@@ -2991,6 +3195,14 @@ function BridgePanel({ rpcCall }) {
|
|
|
2991
3195
|
const policy = auth?.adminPolicy ?? 'password_unlock';
|
|
2992
3196
|
// 系统是否已配置任何密码(访客访问密码 或 独立管理密码)。
|
|
2993
3197
|
const hasAnyPassword = !!(auth?.hasPassword || auth?.hasAdminPassword);
|
|
3198
|
+
// 解锁密码类型:有独立管理密码 → 用管理密码;否则(只有访问密码/都没有)
|
|
3199
|
+
// 服务端以 adminPasswordHash || passwordHash 兜底,实际校验的是访问密码。
|
|
3200
|
+
// UI 需如实告诉用户该输哪个密码,避免"哪来的管理密码"的困惑。
|
|
3201
|
+
const unlockUsesAdmin = !!auth?.hasAdminPassword;
|
|
3202
|
+
const unlockPwdKind = unlockUsesAdmin ? '管理密码' : '访问密码';
|
|
3203
|
+
const unlockPwdHint = unlockUsesAdmin
|
|
3204
|
+
? '请输入后台管理密码解锁管理权限。'
|
|
3205
|
+
: '当前未设置独立管理密码,输入您的访问密码即可解锁。';
|
|
2994
3206
|
// 锁屏条件:远程 + 管理保护开启(adminProtection)+ 未解锁 + 非宽松策略。
|
|
2995
3207
|
// 不依赖 auth.enabled:即使访问认证关闭,管理保护仍独立生效,上锁后必须显示锁屏。
|
|
2996
3208
|
// local_only 也锁定(显示"仅限本机管理"专属锁屏)。
|
|
@@ -3040,8 +3252,32 @@ function BridgePanel({ rpcCall }) {
|
|
|
3040
3252
|
React.createElement('div', { style: { textAlign: 'center', marginBottom: 20 } },
|
|
3041
3253
|
React.createElement('div', { style: { fontSize: 40, marginBottom: 10 } }, '🔒'),
|
|
3042
3254
|
React.createElement('div', { style: { ...s.label, fontSize: 16, fontWeight: 600 } }, '管理控制台已锁定'),
|
|
3043
|
-
|
|
3044
|
-
|
|
3255
|
+
// 醒目提示该输入哪种密码
|
|
3256
|
+
React.createElement('div', { style: { marginTop: 10, display: 'flex', justifyContent: 'center' } },
|
|
3257
|
+
unlockUsesAdmin ? (
|
|
3258
|
+
React.createElement('span', {
|
|
3259
|
+
style: {
|
|
3260
|
+
display: 'inline-flex', alignItems: 'center', gap: 5,
|
|
3261
|
+
padding: '4px 12px', borderRadius: 999, fontSize: 12, fontWeight: 600,
|
|
3262
|
+
background: 'var(--dsw-alias-state-info-bg,#eff6ff)',
|
|
3263
|
+
color: 'var(--dsw-alias-state-info-primary,#2563eb)',
|
|
3264
|
+
border: '1px solid var(--dsw-alias-state-info-border,#bfdbfe)',
|
|
3265
|
+
},
|
|
3266
|
+
}, '🔑 使用管理密码解锁')
|
|
3267
|
+
) : (
|
|
3268
|
+
React.createElement('span', {
|
|
3269
|
+
style: {
|
|
3270
|
+
display: 'inline-flex', alignItems: 'center', gap: 5,
|
|
3271
|
+
padding: '4px 12px', borderRadius: 999, fontSize: 12, fontWeight: 600,
|
|
3272
|
+
background: 'var(--dsw-alias-state-success-bg,#ecfdf5)',
|
|
3273
|
+
color: 'var(--dsw-alias-state-success-primary,#059669)',
|
|
3274
|
+
border: '1px solid var(--dsw-alias-state-success-border,#a7f3d0)',
|
|
3275
|
+
},
|
|
3276
|
+
}, '🔐 使用访问密码解锁')
|
|
3277
|
+
),
|
|
3278
|
+
),
|
|
3279
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 10, lineHeight: 1.6 } },
|
|
3280
|
+
'当前设备为远程访问。为保护您的网络与平台配置安全,' + unlockPwdHint
|
|
3045
3281
|
),
|
|
3046
3282
|
),
|
|
3047
3283
|
React.createElement('form', {
|
|
@@ -3051,7 +3287,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3051
3287
|
React.createElement('input', {
|
|
3052
3288
|
type: 'password',
|
|
3053
3289
|
style: s.input,
|
|
3054
|
-
placeholder: '
|
|
3290
|
+
placeholder: '输入' + unlockPwdKind,
|
|
3055
3291
|
value: unlockPassword,
|
|
3056
3292
|
onChange: (e) => setUnlockPassword(e.target.value),
|
|
3057
3293
|
autoFocus: true,
|
|
@@ -3070,7 +3306,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3070
3306
|
type: 'button',
|
|
3071
3307
|
style: { ...s.btnLink, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' },
|
|
3072
3308
|
onClick: () => setShowForgotGuide(v => !v),
|
|
3073
|
-
}, '❓
|
|
3309
|
+
}, '❓ 忘记' + unlockPwdKind + '?'),
|
|
3074
3310
|
),
|
|
3075
3311
|
showForgotGuide && React.createElement('div', {
|
|
3076
3312
|
style: {
|
|
@@ -3079,8 +3315,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
3079
3315
|
color: 'var(--dsw-alias-label-secondary,#4b5563)', textAlign: 'left',
|
|
3080
3316
|
},
|
|
3081
3317
|
},
|
|
3082
|
-
React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟
|
|
3083
|
-
React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1
|
|
3318
|
+
React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 找回与重置' + unlockPwdKind + '指引:'),
|
|
3319
|
+
React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改或清除密码。'),
|
|
3084
3320
|
React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器 / 无头环境'), ':救急重置步骤参见 GitHub README 的「三重容灾保命体系」章节。'),
|
|
3085
3321
|
),
|
|
3086
3322
|
)
|
|
@@ -3114,7 +3350,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3114
3350
|
setUnlockErr(err);
|
|
3115
3351
|
setShowUnlockModal(true);
|
|
3116
3352
|
},
|
|
3117
|
-
}, '🔑
|
|
3353
|
+
}, '🔑 立即输入' + unlockPwdKind + '解锁'),
|
|
3118
3354
|
),
|
|
3119
3355
|
|
|
3120
3356
|
// 管理员解锁状态提示条
|
|
@@ -3190,7 +3426,9 @@ function BridgePanel({ rpcCall }) {
|
|
|
3190
3426
|
}, '✕'),
|
|
3191
3427
|
),
|
|
3192
3428
|
React.createElement('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary,#4b5563)', marginBottom: 16, lineHeight: 1.5 } },
|
|
3193
|
-
|
|
3429
|
+
unlockUsesAdmin
|
|
3430
|
+
? '当前操作需要后台管理员权限。为保护您的网络配置与机器人平台安全,请输入管理密码解锁:'
|
|
3431
|
+
: '当前操作需要后台管理权限。未设置独立管理密码,输入您的访问密码即可解锁:'
|
|
3194
3432
|
),
|
|
3195
3433
|
React.createElement('form', {
|
|
3196
3434
|
onSubmit: handleUnlockAdmin,
|
|
@@ -3199,7 +3437,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3199
3437
|
React.createElement('input', {
|
|
3200
3438
|
type: 'password',
|
|
3201
3439
|
style: s.input,
|
|
3202
|
-
placeholder: '
|
|
3440
|
+
placeholder: '输入' + unlockPwdKind,
|
|
3203
3441
|
value: unlockPassword,
|
|
3204
3442
|
onChange: (e) => setUnlockPassword(e.target.value),
|
|
3205
3443
|
autoFocus: true,
|