@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/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)' : 'var(--dsw-alias-label-secondary,#6b7280)',
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', { style: s.btnGhost, onClick: onStop }, '关闭'),
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
- enabled && React.createElement('div', { style: s.card },
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
- tabContent = React.createElement(React.Fragment, null,
2839
- React.createElement(TunnelCard, {
2840
- title: 'Cloudflare 隧道',
2841
- desc: status?.cloudflared?.tokenConfigured
2842
- ? '固定域名模式(Token 运行 · 重启 URL 保持不变)'
2843
- : '一键获取公网地址(免登录临时随机域名)',
2844
- data: {
2845
- running: status?.cloudflared?.running,
2846
- url: status?.cloudflared?.url,
2847
- qr: status?.cloudflared?.qr,
2848
- state: status?.cloudflared?.state,
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
- React.createElement(CloudflareConfigForm, {
2859
- token: status?.cloudflared?.token ?? '',
2860
- hostname: status?.cloudflared?.hostname ?? '',
2861
- onSave: saveCloudflaredConfig,
2862
- }),
2863
- ),
2864
- React.createElement(ExternalTunnelCard, {
2865
- ext,
2866
- onSave: saveExternalTunnel,
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(TunnelCard, {
2869
- title: '自建隧道',
2870
- desc: '连接自己部署的隧道服务器,获得固定域名',
2871
- data: {
2872
- configured: ct?.configured,
2873
- running: ct?.running,
2874
- url: ct?.url,
2875
- qr: ct?.qr,
2876
- state: ct?.state,
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
- autoStart: ct?.autoStart,
2879
- onToggleAutoStart: onToggleCustomAutoStart,
2880
- auth: status?.auth,
2881
- onNavigateSecurity: navSecurity,
2882
- onStart: onStartCustom,
2883
- onStop: onStopCustom,
2884
- },
2885
- React.createElement(CustomTunnelGuide),
2886
- React.createElement(CustomTunnelConfigForm, {
2887
- serverUrl: ct?.serverUrl ?? '',
2888
- accessToken: ct?.accessToken ?? '',
2889
- onSave: saveConfig,
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
- React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 6, lineHeight: 1.5 } },
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,