@wenbin_wb/dsh-bridge 2.10.4 → 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 +26 -0
- package/README.en.md +410 -410
- package/README.md +432 -432
- package/client/client.js +368 -66
- package/client/index.js +320 -66
- package/docs/CODE_REVIEW.md +220 -0
- package/docs/telegram-usage.md +1 -1
- package/lib/auth/manager.js +5 -4
- package/lib/bridge-rpc.js +19 -0
- 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
|
});
|
|
@@ -758,9 +903,15 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
|
|
|
758
903
|
const handleToggleEnabled = async () => {
|
|
759
904
|
const prev = enabled;
|
|
760
905
|
const next = !enabled;
|
|
761
|
-
//
|
|
906
|
+
// 开启安全防护但尚未设置任何密码
|
|
762
907
|
const noPasswordYet = !auth?.hasPassword && !auth?.hasAdminPassword;
|
|
763
908
|
if (next && noPasswordYet && mode !== 'token_only') {
|
|
909
|
+
if (mode === 'password_only') {
|
|
910
|
+
// password_only 模式必须已有密码才能开启(服务端同样强制),就地引导设密码
|
|
911
|
+
window.alert('「仅密码 / PIN 码登录」模式必须先设置访问密码才能开启安全防护。\n\n请先在下方「设置外部访客访问密码 / PIN 码」输入密码并点击「保存访问密码」,然后再开启。');
|
|
912
|
+
setTopMsg({ ok: false, text: '请先设置访客访问密码,再开启安全防护' });
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
764
915
|
const go = window.confirm(
|
|
765
916
|
'⚠️ 您尚未设置任何访问密码或管理密码。\n\n开启安全防护后,任何知道局域网 IP / 隧道地址的访客仍可直接进入(当前相当于"免密开放"状态)。\n\n是否仍要开启?建议先关闭,在下方「设置外部访客访问密码」处设置密码后再开启。'
|
|
766
917
|
);
|
|
@@ -798,6 +949,13 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
|
|
|
798
949
|
};
|
|
799
950
|
|
|
800
951
|
const handleChangeMode = async (m) => {
|
|
952
|
+
// 切换到「仅密码登录」但尚未设置任何密码:提前引导先设密码,避免切过去后
|
|
953
|
+
// 无密码可登录(服务端同样有守卫拒绝,双保险防自我锁死)
|
|
954
|
+
if (m === 'password_only' && !auth?.hasPassword && !auth?.hasAdminPassword) {
|
|
955
|
+
window.alert('「仅密码 / PIN 码登录」需要先设置访问密码。\n\n请在下方「设置外部访客访问密码 / PIN 码」处输入密码并点击「保存访问密码」,然后再切换到此模式或开启安全防护。');
|
|
956
|
+
setTopMsg({ ok: false, text: '请先在下方设置访客访问密码,再切换为「仅密码登录」模式' });
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
801
959
|
const prev = mode;
|
|
802
960
|
setMode(m);
|
|
803
961
|
try {
|
|
@@ -974,7 +1132,10 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
|
|
|
974
1132
|
// =========================================================================
|
|
975
1133
|
// ---- 第一道防线:外部访问门禁(控制谁能进入 Web 界面使用 AI) ----
|
|
976
1134
|
// =========================================================================
|
|
977
|
-
|
|
1135
|
+
// 第一道防线卡片:已开启防护,或【尚未设置任何密码】时始终显示——
|
|
1136
|
+
// 未设密码时必须给出密码输入框,否则用户开启防护(尤其 password_only)后
|
|
1137
|
+
// 会因无密码被锁在登录墙外且找不到设密码入口(自我锁死,v2.10.5 修复)。
|
|
1138
|
+
(enabled || !auth?.hasPassword) && React.createElement('div', { style: s.card },
|
|
978
1139
|
React.createElement('div', { style: { marginBottom: 14 } },
|
|
979
1140
|
React.createElement('div', { style: { ...s.label, fontSize: 14, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
980
1141
|
'🛡️ 第一道防线:外部访问门禁(控制谁能使用 AI)',
|
|
@@ -2579,6 +2740,20 @@ function BridgePanel({ rpcCall }) {
|
|
|
2579
2740
|
const [platforms, setPlatforms] = React.useState(null);
|
|
2580
2741
|
const [selectedPlatform, setSelectedPlatform] = React.useState('wechat');
|
|
2581
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
|
+
|
|
2582
2757
|
// 远程设备管理权限解锁状态
|
|
2583
2758
|
const isLocalhost = typeof window === 'undefined' || (
|
|
2584
2759
|
!window.location.hostname ||
|
|
@@ -2835,58 +3010,103 @@ function BridgePanel({ rpcCall }) {
|
|
|
2835
3010
|
);
|
|
2836
3011
|
} else if (activeTab === 'tunnel') {
|
|
2837
3012
|
const ext = status?.externalTunnel;
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
autoStart: status?.cloudflared?.autoStart,
|
|
2851
|
-
onToggleAutoStart: onToggleCloudflaredAutoStart,
|
|
2852
|
-
auth: status?.auth,
|
|
2853
|
-
onNavigateSecurity: navSecurity,
|
|
2854
|
-
onStart: onStartCloudflared,
|
|
2855
|
-
onStop: onStopCloudflared,
|
|
2856
|
-
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,
|
|
2857
3025
|
},
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
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,
|
|
2867
3052
|
}),
|
|
2868
|
-
React.createElement(
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
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,
|
|
2877
3076
|
},
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
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,
|
|
2890
3110
|
}),
|
|
2891
3111
|
),
|
|
2892
3112
|
);
|
|
@@ -2975,6 +3195,14 @@ function BridgePanel({ rpcCall }) {
|
|
|
2975
3195
|
const policy = auth?.adminPolicy ?? 'password_unlock';
|
|
2976
3196
|
// 系统是否已配置任何密码(访客访问密码 或 独立管理密码)。
|
|
2977
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
|
+
: '当前未设置独立管理密码,输入您的访问密码即可解锁。';
|
|
2978
3206
|
// 锁屏条件:远程 + 管理保护开启(adminProtection)+ 未解锁 + 非宽松策略。
|
|
2979
3207
|
// 不依赖 auth.enabled:即使访问认证关闭,管理保护仍独立生效,上锁后必须显示锁屏。
|
|
2980
3208
|
// local_only 也锁定(显示"仅限本机管理"专属锁屏)。
|
|
@@ -3024,8 +3252,32 @@ function BridgePanel({ rpcCall }) {
|
|
|
3024
3252
|
React.createElement('div', { style: { textAlign: 'center', marginBottom: 20 } },
|
|
3025
3253
|
React.createElement('div', { style: { fontSize: 40, marginBottom: 10 } }, '🔒'),
|
|
3026
3254
|
React.createElement('div', { style: { ...s.label, fontSize: 16, fontWeight: 600 } }, '管理控制台已锁定'),
|
|
3027
|
-
|
|
3028
|
-
|
|
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
|
|
3029
3281
|
),
|
|
3030
3282
|
),
|
|
3031
3283
|
React.createElement('form', {
|
|
@@ -3035,7 +3287,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3035
3287
|
React.createElement('input', {
|
|
3036
3288
|
type: 'password',
|
|
3037
3289
|
style: s.input,
|
|
3038
|
-
placeholder: '
|
|
3290
|
+
placeholder: '输入' + unlockPwdKind,
|
|
3039
3291
|
value: unlockPassword,
|
|
3040
3292
|
onChange: (e) => setUnlockPassword(e.target.value),
|
|
3041
3293
|
autoFocus: true,
|
|
@@ -3054,7 +3306,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3054
3306
|
type: 'button',
|
|
3055
3307
|
style: { ...s.btnLink, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' },
|
|
3056
3308
|
onClick: () => setShowForgotGuide(v => !v),
|
|
3057
|
-
}, '❓
|
|
3309
|
+
}, '❓ 忘记' + unlockPwdKind + '?'),
|
|
3058
3310
|
),
|
|
3059
3311
|
showForgotGuide && React.createElement('div', {
|
|
3060
3312
|
style: {
|
|
@@ -3063,8 +3315,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
3063
3315
|
color: 'var(--dsw-alias-label-secondary,#4b5563)', textAlign: 'left',
|
|
3064
3316
|
},
|
|
3065
3317
|
},
|
|
3066
|
-
React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟
|
|
3067
|
-
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 享有物理免锁特权),可随时修改或清除密码。'),
|
|
3068
3320
|
React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器 / 无头环境'), ':救急重置步骤参见 GitHub README 的「三重容灾保命体系」章节。'),
|
|
3069
3321
|
),
|
|
3070
3322
|
)
|
|
@@ -3098,7 +3350,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3098
3350
|
setUnlockErr(err);
|
|
3099
3351
|
setShowUnlockModal(true);
|
|
3100
3352
|
},
|
|
3101
|
-
}, '🔑
|
|
3353
|
+
}, '🔑 立即输入' + unlockPwdKind + '解锁'),
|
|
3102
3354
|
),
|
|
3103
3355
|
|
|
3104
3356
|
// 管理员解锁状态提示条
|
|
@@ -3174,7 +3426,9 @@ function BridgePanel({ rpcCall }) {
|
|
|
3174
3426
|
}, '✕'),
|
|
3175
3427
|
),
|
|
3176
3428
|
React.createElement('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary,#4b5563)', marginBottom: 16, lineHeight: 1.5 } },
|
|
3177
|
-
|
|
3429
|
+
unlockUsesAdmin
|
|
3430
|
+
? '当前操作需要后台管理员权限。为保护您的网络配置与机器人平台安全,请输入管理密码解锁:'
|
|
3431
|
+
: '当前操作需要后台管理权限。未设置独立管理密码,输入您的访问密码即可解锁:'
|
|
3178
3432
|
),
|
|
3179
3433
|
React.createElement('form', {
|
|
3180
3434
|
onSubmit: handleUnlockAdmin,
|
|
@@ -3183,7 +3437,7 @@ function BridgePanel({ rpcCall }) {
|
|
|
3183
3437
|
React.createElement('input', {
|
|
3184
3438
|
type: 'password',
|
|
3185
3439
|
style: s.input,
|
|
3186
|
-
placeholder: '
|
|
3440
|
+
placeholder: '输入' + unlockPwdKind,
|
|
3187
3441
|
value: unlockPassword,
|
|
3188
3442
|
onChange: (e) => setUnlockPassword(e.target.value),
|
|
3189
3443
|
autoFocus: true,
|