@wenbin_wb/dsh-bridge 2.4.0 → 2.5.1

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
@@ -72,6 +72,9 @@ const Icons = {
72
72
  tunnel: (props) => React.createElement('svg', { viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', ...props },
73
73
  React.createElement('path', { d: 'M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3z' })
74
74
  ),
75
+ security: (props) => React.createElement('svg', { viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', ...props },
76
+ React.createElement('path', { d: 'M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z' })
77
+ ),
75
78
  bot: (props) => React.createElement('svg', { viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', ...props },
76
79
  React.createElement('path', { d: 'M20 9V7c0-1.1-.9-2-2-2h-3c0-1.66-1.34-3-3-3S9 3.34 9 5H6c-1.1 0-2 .9-2 2v2c-1.66 0-3 1.34-3 3s1.34 3 3 3v4c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-4c1.66 0 3-1.34 3-3s-1.34-3-3-3zm-2 10H6V7h12v12zm-9-6c-.83 0-1.5-.67-1.5-1.5S8.17 10 9 10s1.5.67 1.5 1.5S9.83 13 9 13zm6 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z' })
77
80
  ),
@@ -154,7 +157,7 @@ function StatusTag({ running, status }) {
154
157
  }, text);
155
158
  }
156
159
 
157
- function QrBlock({ url, qr, onReset }) {
160
+ function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
158
161
  const [copied, setCopied] = React.useState(false);
159
162
  const [showQr, setShowQr] = React.useState(true);
160
163
 
@@ -191,10 +194,31 @@ function QrBlock({ url, qr, onReset }) {
191
194
  const toggleQr = React.useCallback(() => setShowQr(v => !v), []);
192
195
 
193
196
  return React.createElement('div', { style: { marginTop: 10 } },
194
- React.createElement('div', { style: s.warn },
195
- '⚠️ 请勿将此链接或二维码分享给他人,任何人扫码后都可直接访问您的 DSH。'
196
- ),
197
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 10 } },
197
+ auth?.enabled
198
+ ? React.createElement('div', {
199
+ style: {
200
+ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 10px',
201
+ background: 'var(--dsw-alias-state-success-bg,#ecfdf5)',
202
+ border: '1px solid var(--dsw-alias-state-success-primary,#10b981)',
203
+ borderRadius: 8, fontSize: 12, color: 'var(--dsw-alias-state-success-primary,#059669)',
204
+ marginBottom: 8, fontWeight: 500,
205
+ cursor: onNavigateSecurity ? 'pointer' : 'default',
206
+ },
207
+ onClick: onNavigateSecurity,
208
+ title: onNavigateSecurity ? '点击前往「安全认证」配置' : undefined,
209
+ },
210
+ '🛡️ 访问安全认证已生效 · 扫码设备自动完成免密授权',
211
+ onNavigateSecurity && React.createElement('span', { style: { textDecoration: 'underline', fontSize: 11, marginLeft: 4 } }, '设置 ➔')
212
+ )
213
+ : React.createElement('div', {
214
+ style: { ...s.warn, cursor: onNavigateSecurity ? 'pointer' : 'default', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 6 },
215
+ onClick: onNavigateSecurity,
216
+ title: onNavigateSecurity ? '点击前往「安全认证」开启访问保护' : undefined,
217
+ },
218
+ React.createElement('span', null, '⚠️ 当前未开启访问认证,建议在「安全认证」开启密码或扫码保护。'),
219
+ onNavigateSecurity && React.createElement('span', { style: { fontWeight: 600, textDecoration: 'underline', fontSize: 12, color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } }, '去开启 ➔')
220
+ ),
221
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 } },
198
222
  React.createElement('code', { style: { ...s.code, flex: 1 } }, url),
199
223
  React.createElement('button', {
200
224
  style: { ...s.btnGhost, height: 26, padding: '0 10px', fontSize: 12, flexShrink: 0 },
@@ -246,21 +270,26 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
246
270
  }, [initUrl, initToken]);
247
271
 
248
272
  const dirty = serverUrl !== (initUrl ?? '') || accessToken !== (initToken ?? '');
273
+ const [saveErr, setSaveErr] = React.useState(null);
249
274
  const handleSave = React.useCallback(async () => {
250
275
  setSaving(true);
251
276
  setSaveSuccess(false);
277
+ setSaveErr(null);
252
278
  try {
253
279
  await onSave(serverUrl, accessToken);
254
280
  setSaveSuccess(true);
255
281
  setTimeout(() => setSaveSuccess(false), 2500);
282
+ } catch (e) {
283
+ setSaveErr(e.message || '保存配置失败');
284
+ } finally {
285
+ setSaving(false);
256
286
  }
257
- finally { setSaving(false); }
258
287
  }, [onSave, serverUrl, accessToken]);
259
- const handleUrlChange = React.useCallback((e) => { setServerUrl(e.target.value); setSaveSuccess(false); }, []);
260
- const handleTokenChange = React.useCallback((e) => { setAccessToken(e.target.value); setSaveSuccess(false); }, []);
288
+ const handleUrlChange = React.useCallback((e) => { setServerUrl(e.target.value); setSaveSuccess(false); setSaveErr(null); }, []);
289
+ const handleTokenChange = React.useCallback((e) => { setAccessToken(e.target.value); setSaveSuccess(false); setSaveErr(null); }, []);
261
290
 
262
291
  return React.createElement('div', { style: s.block },
263
- React.createElement('div', { style: { ...s.muted, marginBottom: 8 } }, '服务器配置'),
292
+ React.createElement('div', { style: { ...s.muted, marginBottom: 8 } }, '隧道服务器配置'),
264
293
  React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
265
294
  React.createElement('input', {
266
295
  style: s.input,
@@ -273,12 +302,16 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
273
302
  React.createElement('input', {
274
303
  style: s.input,
275
304
  type: 'password',
276
- placeholder: '访问令牌(Access Token)',
305
+ placeholder: '隧道服务端连接令牌(Tunnel Access Token)',
277
306
  value: accessToken,
278
307
  onChange: handleTokenChange,
279
308
  onKeyDown: (e) => { if (e.key === 'Enter' && dirty && !saving) handleSave(); },
280
309
  disabled: saving,
281
310
  }),
311
+ saveErr && React.createElement('div', { style: s.err }, `❌ ${saveErr}`),
312
+ React.createElement('div', { style: { ...s.muted, fontSize: 11 } },
313
+ '💡 用于与您的 VPS 隧道服务端建立反向通道(与 Web 网页访客访问密码互相独立)。'
314
+ ),
282
315
  React.createElement('button', {
283
316
  style: {
284
317
  ...s.btnPri,
@@ -293,7 +326,7 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
293
326
  );
294
327
  });
295
328
 
296
- const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart, onStop, onReset, children }) {
329
+ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart, onStop, onReset, auth, onNavigateSecurity, children }) {
297
330
  const { running, configured, url, qr, state } = data ?? {};
298
331
  const phase = state?.phase ?? 'idle';
299
332
 
@@ -312,7 +345,7 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
312
345
  color: phase === 'error' ? 'var(--dsw-alias-state-error-primary,#dc2626)' : 'var(--dsw-alias-label-secondary,#6b7280)',
313
346
  },
314
347
  }, state?.detail ?? phase),
315
- url && React.createElement(QrBlock, { url, qr, onReset }),
348
+ url && React.createElement(QrBlock, { url, qr, onReset, auth, onNavigateSecurity }),
316
349
  (onStart || onStop) && React.createElement('div', {
317
350
  style: { ...s.block, display: 'flex', gap: 8, flexWrap: 'wrap' },
318
351
  },
@@ -327,6 +360,434 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
327
360
  );
328
361
  });
329
362
 
363
+ // ---- 访问安全认证卡片 ----
364
+
365
+ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpdate }) {
366
+ const [enabled, setEnabled] = React.useState(auth?.enabled ?? false);
367
+ const [mode, setMode] = React.useState(auth?.mode ?? 'token_and_password');
368
+ const [scope, setScope] = React.useState(auth?.scope ?? 'all');
369
+ const [adminPolicy, setAdminPolicy] = React.useState(auth?.adminPolicy ?? 'password_unlock');
370
+
371
+ // 1. 访客访问密码状态
372
+ const [accessPassword, setAccessPassword] = React.useState('');
373
+ const [showAccessPassword, setShowAccessPassword] = React.useState(false);
374
+ const [savingAccess, setSavingAccess] = React.useState(false);
375
+ const [saveAccessSuccess, setSaveAccessSuccess] = React.useState(false);
376
+ const [msgAccess, setMsgAccess] = React.useState(null);
377
+
378
+ // 2. 后台管理密码状态
379
+ const [adminPassword, setAdminPassword] = React.useState('');
380
+ const [showAdminPassword, setShowAdminPassword] = React.useState(false);
381
+ const [savingAdmin, setSavingAdmin] = React.useState(false);
382
+ const [saveAdminSuccess, setSaveAdminSuccess] = React.useState(false);
383
+ const [msgAdmin, setMsgAdmin] = React.useState(null);
384
+
385
+ const [topMsg, setTopMsg] = React.useState(null);
386
+
387
+ React.useEffect(() => {
388
+ if (auth) {
389
+ setEnabled(auth.enabled ?? false);
390
+ setMode(auth.mode ?? 'token_and_password');
391
+ setScope(auth.scope ?? 'all');
392
+ setAdminPolicy(auth.adminPolicy ?? 'password_unlock');
393
+ }
394
+ }, [auth]);
395
+
396
+ const handleToggleEnabled = async () => {
397
+ const prev = enabled;
398
+ const next = !enabled;
399
+ setEnabled(next);
400
+ try {
401
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
402
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
403
+ setTopMsg({ ok: true, text: next ? '✓ 访问安全认证已开启(现有登录态已刷新)' : '✓ 访问安全认证已关闭' });
404
+ onUpdate?.();
405
+ } catch (e) {
406
+ setEnabled(prev);
407
+ setTopMsg({ ok: false, text: e.message || '更新失败' });
408
+ }
409
+ };
410
+
411
+ const handleChangeMode = async (m) => {
412
+ const prev = mode;
413
+ setMode(m);
414
+ try {
415
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
416
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
417
+ setTopMsg({ ok: true, text: '✓ 外部验证模式已切换,已刷新全域登录态' });
418
+ onUpdate?.();
419
+ } catch (e) {
420
+ setMode(prev);
421
+ setTopMsg({ ok: false, text: e.message || '更新失败' });
422
+ }
423
+ };
424
+
425
+ const handleChangeScope = async (sc) => {
426
+ const prev = scope;
427
+ setScope(sc);
428
+ try {
429
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
430
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
431
+ setTopMsg({ ok: true, text: '✓ 防护生效范围已更新' });
432
+ onUpdate?.();
433
+ } catch (e) {
434
+ setScope(prev);
435
+ setTopMsg({ ok: false, text: e.message || '更新失败' });
436
+ }
437
+ };
438
+
439
+ const handleChangeAdminPolicy = async (pol) => {
440
+ const prev = adminPolicy;
441
+ setAdminPolicy(pol);
442
+ try {
443
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
444
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
445
+ setTopMsg({ ok: true, text: '✓ 远程管理防篡改策略已更新' });
446
+ onUpdate?.();
447
+ } catch (e) {
448
+ setAdminPolicy(prev);
449
+ setTopMsg({ ok: false, text: e.message || '更新失败' });
450
+ }
451
+ };
452
+
453
+ // 保存访客访问密码
454
+ const handleSaveAccessPassword = async () => {
455
+ setSavingAccess(true);
456
+ setMsgAccess(null);
457
+ try {
458
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { password: accessPassword });
459
+ if (res?.ok) {
460
+ setSaveAccessSuccess(true);
461
+ setMsgAccess({ ok: true, text: '✓ 访客访问密码已成功保存!原有的历史访客会话已全部安全刷新。' });
462
+ setAccessPassword('');
463
+ setTimeout(() => setSaveAccessSuccess(false), 3500);
464
+ onUpdate?.();
465
+ } else {
466
+ setMsgAccess({ ok: false, text: res?.error?.message || '保存失败' });
467
+ }
468
+ } catch (e) {
469
+ setMsgAccess({ ok: false, text: e.message || '保存失败' });
470
+ } finally {
471
+ setSavingAccess(false);
472
+ }
473
+ };
474
+
475
+ // 保存后台管理密码
476
+ const handleSaveAdminPassword = async () => {
477
+ setSavingAdmin(true);
478
+ setMsgAdmin(null);
479
+ try {
480
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPassword });
481
+ if (res?.ok) {
482
+ setSaveAdminSuccess(true);
483
+ setMsgAdmin({ ok: true, text: '✓ 后台管理密码已成功保存!远程管理解锁状态已重置生效。' });
484
+ setAdminPassword('');
485
+ setTimeout(() => setSaveAdminSuccess(false), 3500);
486
+ onUpdate?.();
487
+ } else {
488
+ setMsgAdmin({ ok: false, text: res?.error?.message || '保存失败' });
489
+ }
490
+ } catch (e) {
491
+ setMsgAdmin({ ok: false, text: e.message || '保存失败' });
492
+ } finally {
493
+ setSavingAdmin(false);
494
+ }
495
+ };
496
+
497
+ const handleRegenerateToken = async () => {
498
+ if (!confirm('重置后,之前包含旧 Token 的二维码和分享链接将立即失效。是否确认重置?')) return;
499
+ try {
500
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
501
+ if (!res?.ok) throw new Error(res?.error?.message || '重置失败');
502
+ setTopMsg({ ok: true, text: '✓ 安全 Token 已重置,二维码与专属链接已刷新' });
503
+ onUpdate?.();
504
+ } catch (e) {
505
+ setTopMsg({ ok: false, text: e.message || '重置失败' });
506
+ }
507
+ };
508
+
509
+ const scopeLabel = scope === 'all' ? '全部通道 (局域网+公网)' : scope === 'public_only' ? '仅公网隧道' : '仅局域网';
510
+ const modeLabel = mode === 'token_and_password' ? '扫码免密 + 密码' : mode === 'password_only' ? '仅密码登录' : '仅安全 Token';
511
+ const adminLabel = adminPolicy === 'password_unlock' ? '需密码解锁' : adminPolicy === 'local_only' ? '仅限电脑本机管理' : '宽松模式';
512
+
513
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } },
514
+ // ---- 顶部总控与状态概览卡片 ----
515
+ React.createElement('div', { style: s.card },
516
+ React.createElement('div', {
517
+ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 },
518
+ },
519
+ React.createElement('div', { style: { flex: '1 1 260px' } },
520
+ React.createElement('div', { style: { ...s.label, fontSize: 15, display: 'flex', alignItems: 'center', gap: 8 } },
521
+ '🔐 全局访问安全防护体系',
522
+ ),
523
+ React.createElement('div', { style: { ...s.muted, marginTop: 4 } },
524
+ '集成外部访问门禁拦截与管理后台防篡改控制,双重守护远程会话与网络配置安全',
525
+ ),
526
+ ),
527
+ React.createElement('button', {
528
+ style: { ...(enabled ? s.btnPri : s.btnGhost), whiteSpace: 'nowrap', flexShrink: 0 },
529
+ onClick: handleToggleEnabled,
530
+ }, enabled ? '✓ 已启用安全防护' : '未开启安全防护'),
531
+ ),
532
+
533
+ enabled && React.createElement('div', {
534
+ style: {
535
+ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 14, paddingTop: 12,
536
+ borderTop: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
537
+ },
538
+ },
539
+ React.createElement('div', {
540
+ style: {
541
+ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
542
+ borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
543
+ color: 'var(--dsw-alias-label-secondary,#4b5563)',
544
+ },
545
+ }, '🌐 保护范围: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } }, scopeLabel)),
546
+ React.createElement('div', {
547
+ style: {
548
+ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
549
+ borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
550
+ color: 'var(--dsw-alias-label-secondary,#4b5563)',
551
+ },
552
+ }, '🔑 外部验证: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } }, modeLabel)),
553
+ React.createElement('div', {
554
+ style: {
555
+ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
556
+ borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
557
+ color: 'var(--dsw-alias-label-secondary,#4b5563)',
558
+ },
559
+ }, '🔒 后台防篡改: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-state-success-primary,#059669)' } }, adminLabel)),
560
+ ),
561
+
562
+ topMsg && React.createElement('div', {
563
+ style: {
564
+ marginTop: 12, padding: '8px 12px', borderRadius: 6, fontSize: 12,
565
+ background: topMsg.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
566
+ color: topMsg.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
567
+ },
568
+ }, topMsg.text),
569
+ ),
570
+
571
+ enabled && React.createElement(React.Fragment, null,
572
+ // =========================================================================
573
+ // ---- 第一道防线:外部访问门禁(控制谁能进入 Web 界面使用 AI) ----
574
+ // =========================================================================
575
+ React.createElement('div', { style: s.card },
576
+ React.createElement('div', { style: { marginBottom: 14 } },
577
+ React.createElement('div', { style: { ...s.label, fontSize: 14, display: 'flex', alignItems: 'center', gap: 6 } },
578
+ '🛡️ 第一道防线:外部访问门禁(控制谁能使用 AI)',
579
+ ),
580
+ React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
581
+ '控制外部设备通过局域网 IP 或公网隧道(Cloudflare/自建隧道)进入 DSH 聊天界面时的身份验证方式',
582
+ ),
583
+ ),
584
+
585
+ // 验证模式选择
586
+ React.createElement('div', { style: { marginBottom: 16 } },
587
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '验证模式选择'),
588
+ React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
589
+ [
590
+ { id: 'token_and_password', title: '🟢 扫码免密 + 密码认证 (推荐)', desc: '二维码自带专属 Token 扫码秒进;直接输 IP/公网域名需输密码' },
591
+ { id: 'password_only', title: '🔑 仅密码 / PIN 码登录', desc: '所有外部访问必须手动输入访问密码方可进入' },
592
+ { id: 'token_only', title: '🎫 仅安全 Token 免密', desc: '仅持有带安全 Token 的二维码或专属分享链接方可进入' },
593
+ ].map(opt => {
594
+ const isSel = mode === opt.id;
595
+ return React.createElement('div', {
596
+ key: opt.id,
597
+ onClick: () => handleChangeMode(opt.id),
598
+ style: {
599
+ flex: '1 1 200px',
600
+ padding: '10px 12px',
601
+ borderRadius: 8,
602
+ border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
603
+ background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
604
+ cursor: 'pointer',
605
+ transition: 'all 0.15s ease',
606
+ },
607
+ },
608
+ React.createElement('div', { style: { fontSize: 13, fontWeight: isSel ? 600 : 500, color: isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-label-primary,currentColor)' } }, opt.title),
609
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } }, opt.desc),
610
+ );
611
+ })
612
+ ),
613
+ ),
614
+
615
+ // 防护生效范围
616
+ React.createElement('div', { style: { marginBottom: 16 } },
617
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '防护生效通道'),
618
+ React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
619
+ [
620
+ { id: 'all', title: '全部通道防护 (推荐)', desc: '局域网 IP 直连与公网隧道全部受安全保护' },
621
+ { id: 'public_only', title: '仅公网隧道开启防护', desc: '局域网内设备直接免密直连,公网隧道强制验证' },
622
+ { id: 'lan_only', title: '仅局域网开启防护', desc: '仅局域网直连需验证,公网隧道不开启' },
623
+ ].map(opt => {
624
+ const isSel = scope === opt.id;
625
+ return React.createElement('div', {
626
+ key: opt.id,
627
+ onClick: () => handleChangeScope(opt.id),
628
+ style: {
629
+ flex: '1 1 180px',
630
+ padding: '9px 12px',
631
+ borderRadius: 8,
632
+ border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
633
+ background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
634
+ cursor: 'pointer',
635
+ transition: 'all 0.15s ease',
636
+ },
637
+ },
638
+ React.createElement('div', { style: { fontSize: 13, fontWeight: isSel ? 600 : 500, color: isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-label-primary,currentColor)' } }, opt.title),
639
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 3 } }, opt.desc),
640
+ );
641
+ })
642
+ ),
643
+ ),
644
+
645
+ // 访客访问密码输入框 (当非 token_only 时展示)
646
+ mode !== 'token_only' && React.createElement('div', { style: { marginBottom: 16 } },
647
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } },
648
+ `设置外部访客访问密码 ${auth?.hasPassword ? '(✓ 已设置访客密码)' : '(⚠️ 尚未设置密码,直接输入 IP 将免密)'}`,
649
+ ),
650
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
651
+ React.createElement('input', {
652
+ type: showAccessPassword ? 'text' : 'password',
653
+ style: { ...s.input, flex: 1 },
654
+ placeholder: auth?.hasPassword ? '输入新密码以修改(留空保存可清除访客密码)' : '设置外部访客访问密码 / PIN 码',
655
+ value: accessPassword,
656
+ onChange: e => setAccessPassword(e.target.value),
657
+ }),
658
+ React.createElement('button', {
659
+ style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
660
+ onClick: () => setShowAccessPassword(v => !v),
661
+ }, showAccessPassword ? '隐藏' : '显示'),
662
+ React.createElement('button', {
663
+ style: {
664
+ ...s.btnPri, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0,
665
+ background: saveAccessSuccess ? '#059669' : 'var(--dsw-alias-brand-primary, #4f6ef7)',
666
+ color: '#ffffff',
667
+ cursor: savingAccess ? 'wait' : 'pointer',
668
+ },
669
+ onClick: handleSaveAccessPassword,
670
+ disabled: savingAccess,
671
+ }, savingAccess ? '保存中…' : saveAccessSuccess ? '✓ 已成功保存!' : '保存访问密码'),
672
+ ),
673
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
674
+ '💡 当外部朋友或同事未通过二维码扫码,而是直接输入 IP 或公网域名访问时,需输入此密码登录。'
675
+ ),
676
+ msgAccess && React.createElement('div', {
677
+ style: {
678
+ marginTop: 8, padding: '6px 12px', borderRadius: 6, fontSize: 12,
679
+ background: msgAccess.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
680
+ color: msgAccess.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
681
+ },
682
+ }, msgAccess.text),
683
+ ),
684
+
685
+ // 免密 Token 管理
686
+ mode !== 'password_only' && React.createElement('div', { style: s.block },
687
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } }, '免密扫码 Token (专属访问凭据)'),
688
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
689
+ React.createElement('code', { style: { ...s.code, flex: 1, padding: '6px 10px', background: 'var(--dsw-alias-bg-layer-1,#fff)', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)' } },
690
+ auth?.secretToken ? `${auth.secretToken.slice(0, 10)}****************` : '未生成'
691
+ ),
692
+ React.createElement('button', {
693
+ style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
694
+ onClick: handleRegenerateToken,
695
+ title: '重新生成 Token,使之前分享的旧二维码和链接立即失效',
696
+ }, '🔄 重置安全 Token'),
697
+ ),
698
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
699
+ '💡 控制台生成的局域网与公网二维码已自动嵌入此 Token,手机扫码即可免密进入聊天界面(但不赋予后台管理设置权限)。'
700
+ ),
701
+ ),
702
+ ),
703
+
704
+ // =========================================================================
705
+ // ---- 第二道防线:后台管理防篡改(控制谁能修改本插件所有设置) ----
706
+ // =========================================================================
707
+ React.createElement('div', { style: s.card },
708
+ React.createElement('div', { style: { marginBottom: 14 } },
709
+ React.createElement('div', { style: { ...s.label, fontSize: 14, display: 'flex', alignItems: 'center', gap: 6 } },
710
+ '🔒 第二道防线:管理后台防篡改(控制谁能修改本插件所有设置)',
711
+ ),
712
+ React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
713
+ '锁定整个插件设置后台(包含局域网、公网隧道、IM 机器人密钥与安全设置),防止他人随意篡改配置',
714
+ ),
715
+ ),
716
+
717
+ // 管理员密码设置
718
+ React.createElement('div', { style: { marginBottom: 16 } },
719
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } },
720
+ `设置独立管理员密码 ${auth?.hasAdminPassword ? '(✓ 已设置独立管理密码)' : '(未单独设置,默认使用上述访客访问密码)'}`,
721
+ ),
722
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
723
+ React.createElement('input', {
724
+ type: showAdminPassword ? 'text' : 'password',
725
+ style: { ...s.input, flex: 1 },
726
+ placeholder: auth?.hasAdminPassword ? '输入新密码以修改(留空保存可清除独立管理密码)' : '设置后台管理解锁密码(建议与访客密码不同)',
727
+ value: adminPassword,
728
+ onChange: e => setAdminPassword(e.target.value),
729
+ }),
730
+ React.createElement('button', {
731
+ style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
732
+ onClick: () => setShowAdminPassword(v => !v),
733
+ }, showAdminPassword ? '隐藏' : '显示'),
734
+ React.createElement('button', {
735
+ style: {
736
+ ...s.btnPri, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0,
737
+ background: saveAdminSuccess ? '#059669' : 'var(--dsw-alias-brand-primary, #4f6ef7)',
738
+ color: '#ffffff',
739
+ cursor: savingAdmin ? 'wait' : 'pointer',
740
+ },
741
+ onClick: handleSaveAdminPassword,
742
+ disabled: savingAdmin,
743
+ }, savingAdmin ? '保存中…' : saveAdminSuccess ? '✓ 已成功保存!' : '保存管理密码'),
744
+ ),
745
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4, color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } },
746
+ '🔑 核心作用:用于远程设备进入设置后台时的解锁验证。设置后,即便把访问密码告知他人,他人也无法进入设置后台改配置。'
747
+ ),
748
+ msgAdmin && React.createElement('div', {
749
+ style: {
750
+ marginTop: 8, padding: '6px 12px', borderRadius: 6, fontSize: 12,
751
+ background: msgAdmin.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
752
+ color: msgAdmin.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
753
+ },
754
+ }, msgAdmin.text),
755
+ ),
756
+
757
+ // 远程管理权限控制策略
758
+ React.createElement('div', { style: s.block },
759
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '远程设备管理权限策略'),
760
+ React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
761
+ [
762
+ { id: 'password_unlock', title: '🔒 需密码解锁 (推荐)', desc: '远程手机/外网打开本插件设置时默认全局锁定,输入管理密码解锁后方可使用' },
763
+ { id: 'local_only', title: '🚫 仅限电脑本机管理 (最严格)', desc: '远程设备彻底锁定整个设置后台,仅允许在 127.0.0.1 电脑本机上操作' },
764
+ { id: 'open', title: '🔓 宽松模式', desc: '任何已通过第一道防线登录的设备均可直接修改所有配置' },
765
+ ].map(opt => {
766
+ const isSel = adminPolicy === opt.id;
767
+ return React.createElement('div', {
768
+ key: opt.id,
769
+ onClick: () => handleChangeAdminPolicy(opt.id),
770
+ style: {
771
+ flex: '1 1 180px',
772
+ padding: '10px 12px',
773
+ borderRadius: 8,
774
+ border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
775
+ background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
776
+ cursor: 'pointer',
777
+ transition: 'all 0.15s ease',
778
+ },
779
+ },
780
+ React.createElement('div', { style: { fontSize: 13, fontWeight: isSel ? 600 : 500, color: isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-label-primary,currentColor)' } }, opt.title),
781
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } }, opt.desc),
782
+ );
783
+ })
784
+ ),
785
+ ),
786
+ ),
787
+ ),
788
+ );
789
+ });
790
+
330
791
  // ---- 通用 IM 平台卡片 ----
331
792
 
332
793
  function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatusChange }) {
@@ -362,16 +823,24 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
362
823
  onStatusChange?.(connected);
363
824
  }, [platform?.status, onStatusChange]);
364
825
 
826
+ const loadInFlightRef = React.useRef(false);
827
+ const seqRef = React.useRef(0);
365
828
  const load = React.useCallback(async (quiet = false) => {
829
+ if (loadInFlightRef.current) return;
830
+ loadInFlightRef.current = true;
831
+ const currentSeq = ++seqRef.current;
366
832
  try {
367
833
  // 用通用端点读取平台状态(不执行登录操作,只获取状态)
368
834
  const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
835
+ if (currentSeq !== seqRef.current) return; // 丢弃过时响应
369
836
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
370
837
  const allPlatforms = r.value ?? {};
371
838
  setPlatform(allPlatforms[platformId] ?? null);
372
839
  if (!quiet) setErr(null);
373
840
  } catch (e) {
374
- if (!quiet) setErr(e.message);
841
+ if (currentSeq === seqRef.current && !quiet) setErr(e.message);
842
+ } finally {
843
+ loadInFlightRef.current = false;
375
844
  }
376
845
  }, [rpcCall, platformId]);
377
846
 
@@ -408,9 +877,20 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
408
877
  const id = newId.trim();
409
878
  if (!id) return;
410
879
  const list = [...(platform?.allowFrom ?? []), id];
411
- await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
412
- setNewId('');
413
- }, [act, newId, platform?.allowFrom]);
880
+ setBusy(true);
881
+ try {
882
+ const r = await rpcCall(BRIDGE_ENDPOINTS.platformSetAllowFrom, { platformId, allowFrom: list });
883
+ if (!r?.ok) throw new Error(r?.error?.message ?? '添加白名单失败');
884
+ setPlatform(r.value);
885
+ setNewId('');
886
+ setErr(null);
887
+ await load(true);
888
+ } catch (e) {
889
+ setErr(e.message);
890
+ } finally {
891
+ setBusy(false);
892
+ }
893
+ }, [rpcCall, platformId, newId, platform?.allowFrom, load]);
414
894
  const removeAllow = React.useCallback(async (id) => {
415
895
  const list = (platform?.allowFrom ?? []).filter((x) => x !== id);
416
896
  await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
@@ -480,7 +960,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
480
960
  );
481
961
  }
482
962
 
483
- const connected = platform?.status === 'connected' || platform?.status === 'starting';
963
+ const connected = platform?.status === 'connected' || platform?.status === 'starting' || platform?.status === 'reconnecting';
484
964
  const login = platform?.login ?? {};
485
965
  const showQr = login.phase === 'qr' || login.phase === 'scaned';
486
966
  const statusLabel = platform?.status === 'connected' ? '已连接'
@@ -1063,9 +1543,10 @@ function VersionBanner({ rpcCall }) {
1063
1543
  // ---- Tab Bar ----
1064
1544
 
1065
1545
  const TABS = [
1066
- { id: 'lan', label: '局域网', icon: Icons.lan },
1067
- { id: 'tunnel', label: '公网隧道', icon: Icons.tunnel },
1068
- { id: 'im', label: 'IM 机器人', icon: Icons.bot },
1546
+ { id: 'lan', label: '局域网', icon: Icons.lan },
1547
+ { id: 'tunnel', label: '公网隧道', icon: Icons.tunnel },
1548
+ { id: 'im', label: 'IM 机器人', icon: Icons.bot },
1549
+ { id: 'security', label: '安全认证', icon: Icons.security },
1069
1550
  ];
1070
1551
 
1071
1552
  function TabBar({ active, onChange, dots }) {
@@ -1125,32 +1606,106 @@ function BridgePanel({ rpcCall }) {
1125
1606
  const [platforms, setPlatforms] = React.useState(null);
1126
1607
  const [selectedPlatform, setSelectedPlatform] = React.useState('wechat');
1127
1608
 
1609
+ // 远程设备管理权限解锁状态
1610
+ const isLocalhost = typeof window === 'undefined' || (
1611
+ !window.location.hostname ||
1612
+ window.location.hostname === '127.0.0.1' ||
1613
+ window.location.hostname === 'localhost' ||
1614
+ window.location.hostname === '::1' ||
1615
+ window.location.hostname === '' ||
1616
+ window.location.protocol === 'file:' ||
1617
+ window.location.protocol === 'vscode-webview:' ||
1618
+ window.location.protocol === 'app:' ||
1619
+ window.location.hostname.endsWith('.local')
1620
+ );
1621
+ const [adminToken, setAdminToken] = React.useState('');
1622
+ const [adminUnlocked, setAdminUnlocked] = React.useState(false);
1623
+ const [unlockPassword, setUnlockPassword] = React.useState('');
1624
+ const [unlockErr, setUnlockErr] = React.useState(null);
1625
+ const [unlocking, setUnlocking] = React.useState(false);
1626
+ const [showForgotGuide, setShowForgotGuide] = React.useState(false);
1627
+
1628
+ const authRpcCall = React.useCallback((endpoint, payload = {}, signal) => {
1629
+ const enriched = {
1630
+ ...payload,
1631
+ ...(adminToken ? { adminToken } : {}),
1632
+ ...(isLocalhost ? { isLocalhost: true } : {}),
1633
+ };
1634
+ return rpcCall(endpoint, enriched, signal);
1635
+ }, [rpcCall, adminToken, isLocalhost]);
1636
+
1637
+ const handleUnlockAdmin = React.useCallback(async (e) => {
1638
+ e?.preventDefault?.();
1639
+ setUnlocking(true);
1640
+ setUnlockErr(null);
1641
+ try {
1642
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authAdminUnlock, { password: unlockPassword });
1643
+ if (res?.ok) {
1644
+ setAdminToken(res.value?.adminToken || '');
1645
+ setAdminUnlocked(true);
1646
+ setUnlockPassword('');
1647
+ } else {
1648
+ setUnlockErr(res?.error?.message || '管理员密码错误');
1649
+ }
1650
+ } catch (err) {
1651
+ setUnlockErr(err.message || '解锁请求失败');
1652
+ } finally {
1653
+ setUnlocking(false);
1654
+ }
1655
+ }, [rpcCall, unlockPassword]);
1656
+
1657
+ const handleLockAdmin = React.useCallback(async () => {
1658
+ try {
1659
+ if (adminToken) {
1660
+ await rpcCall(BRIDGE_ENDPOINTS.authAdminLock, { adminToken });
1661
+ }
1662
+ } catch {}
1663
+ setAdminToken('');
1664
+ setAdminUnlocked(false);
1665
+ }, [rpcCall, adminToken]);
1666
+
1667
+ const loadInFlightRef = React.useRef(false);
1668
+ const loadSeqRef = React.useRef(0);
1128
1669
  const load = React.useCallback(async (quiet = false) => {
1670
+ if (loadInFlightRef.current) return;
1671
+ loadInFlightRef.current = true;
1672
+ const currentSeq = ++loadSeqRef.current;
1129
1673
  try {
1130
- const r = await rpcCall(BRIDGE_ENDPOINTS.getStatus, {});
1674
+ const r = await authRpcCall(BRIDGE_ENDPOINTS.getStatus, {});
1675
+ if (currentSeq !== loadSeqRef.current) return;
1131
1676
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
1132
1677
  setStatus(r.value);
1133
1678
  if (!quiet) setErr(null);
1134
1679
  } catch (e) {
1135
- setErr(e.message);
1680
+ if (currentSeq === loadSeqRef.current) setErr(e.message);
1681
+ } finally {
1682
+ loadInFlightRef.current = false;
1136
1683
  }
1137
- }, [rpcCall]);
1684
+ }, [authRpcCall]);
1138
1685
 
1139
- // 独立轮询所有平台状态(Tab 未选中时也能更新)
1686
+ // 独立轮询所有平台状态(Tab 未选中时也能更新),带 in-flight 锁与序列号防乱序
1687
+ const pollPlatformsSeqRef = React.useRef(0);
1140
1688
  React.useEffect(() => {
1141
1689
  let alive = true;
1690
+ let inFlight = false;
1142
1691
  const poll = async () => {
1692
+ if (inFlight || !alive) return;
1693
+ inFlight = true;
1694
+ const currentSeq = ++pollPlatformsSeqRef.current;
1143
1695
  try {
1144
- const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1145
- if (alive && r?.ok) {
1696
+ const r = await authRpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1697
+ if (alive && currentSeq === pollPlatformsSeqRef.current && r?.ok) {
1146
1698
  setPlatforms(r.value ?? {});
1147
1699
  }
1148
1700
  } catch { /* 忽略,不影响主面板 */ }
1701
+ finally {
1702
+ inFlight = false;
1703
+ }
1149
1704
  };
1150
1705
  poll();
1151
1706
  const t = setInterval(poll, 4000);
1152
1707
  return () => { alive = false; clearInterval(t); };
1153
- }, [rpcCall]);
1708
+ }, [authRpcCall]);
1154
1709
 
1155
1710
  React.useEffect(() => {
1156
1711
  load();
@@ -1160,14 +1715,14 @@ function BridgePanel({ rpcCall }) {
1160
1715
 
1161
1716
  const act = React.useCallback(async (endpoint, payload) => {
1162
1717
  try {
1163
- const r = await rpcCall(endpoint, payload ?? {});
1718
+ const r = await authRpcCall(endpoint, payload ?? {});
1164
1719
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
1165
1720
  setStatus(r.value);
1166
1721
  setErr(null);
1167
1722
  } catch (e) {
1168
1723
  setErr(e.message);
1169
1724
  }
1170
- }, [rpcCall]);
1725
+ }, [authRpcCall]);
1171
1726
 
1172
1727
  const onStartCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCloudflared), [act]);
1173
1728
  const onStopCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCloudflared), [act]);
@@ -1180,6 +1735,8 @@ function BridgePanel({ rpcCall }) {
1180
1735
  act(BRIDGE_ENDPOINTS.saveCustomTunnelConfig, { serverUrl, accessToken })
1181
1736
  , [act]);
1182
1737
 
1738
+ const navSecurity = React.useCallback(() => setActiveTab('security'), []);
1739
+
1183
1740
  if (!status && !err) {
1184
1741
  return React.createElement('div', {
1185
1742
  style: { padding: 32, color: 'var(--dsw-alias-label-tertiary,#9ca3af)', fontSize: 13 },
@@ -1193,9 +1750,10 @@ function BridgePanel({ rpcCall }) {
1193
1750
  p.status === 'connected' || p.status === 'starting' || p.status === 'reconnecting'
1194
1751
  );
1195
1752
  const dots = {
1196
- lan: !!(status?.proxy?.running),
1197
- tunnel: !!(status?.cloudflared?.running || ct?.running),
1198
- im: !!imConnected,
1753
+ lan: !!(status?.proxy?.running),
1754
+ tunnel: !!(status?.cloudflared?.running || ct?.running),
1755
+ im: !!imConnected,
1756
+ security: !!(status?.auth?.enabled),
1199
1757
  };
1200
1758
 
1201
1759
  // Tab 内容
@@ -1205,6 +1763,8 @@ function BridgePanel({ rpcCall }) {
1205
1763
  title: '局域网访问',
1206
1764
  desc: '同一 Wi-Fi 下的设备可直接扫码访问',
1207
1765
  data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
1766
+ auth: status?.auth,
1767
+ onNavigateSecurity: navSecurity,
1208
1768
  });
1209
1769
  } else if (activeTab === 'tunnel') {
1210
1770
  tabContent = React.createElement(React.Fragment, null,
@@ -1217,6 +1777,8 @@ function BridgePanel({ rpcCall }) {
1217
1777
  qr: status?.cloudflared?.qr,
1218
1778
  state: status?.cloudflared?.state,
1219
1779
  },
1780
+ auth: status?.auth,
1781
+ onNavigateSecurity: navSecurity,
1220
1782
  onStart: onStartCloudflared,
1221
1783
  onStop: onStopCloudflared,
1222
1784
  onReset: status?.cloudflared?.running ? onResetCloudflared : null,
@@ -1231,6 +1793,8 @@ function BridgePanel({ rpcCall }) {
1231
1793
  qr: ct?.qr,
1232
1794
  state: ct?.state,
1233
1795
  },
1796
+ auth: status?.auth,
1797
+ onNavigateSecurity: navSecurity,
1234
1798
  onStart: onStartCustom,
1235
1799
  onStop: onStopCustom,
1236
1800
  },
@@ -1242,6 +1806,12 @@ function BridgePanel({ rpcCall }) {
1242
1806
  }),
1243
1807
  ),
1244
1808
  );
1809
+ } else if (activeTab === 'security') {
1810
+ tabContent = React.createElement(AccessAuthCard, {
1811
+ auth: status?.auth,
1812
+ rpcCall: authRpcCall,
1813
+ onUpdate: () => load(true),
1814
+ });
1245
1815
  } else if (activeTab === 'im') {
1246
1816
  // 从 listPlatforms 动态生成平台列表
1247
1817
  const IM_PLATFORMS = [
@@ -1295,23 +1865,129 @@ function BridgePanel({ rpcCall }) {
1295
1865
  );
1296
1866
  }),
1297
1867
  ),
1298
- // 显示选中的平台卡片
1868
+ // 显示选中的平台卡片(带有 key 保证切换时重置表单状态)
1299
1869
  selectedPlatform && platforms?.[selectedPlatform] && React.createElement(PlatformCard, {
1870
+ key: selectedPlatform,
1300
1871
  platformId: selectedPlatform,
1301
1872
  platformName: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.label ?? selectedPlatform,
1302
1873
  platformDesc: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.desc ?? '',
1303
- rpcCall,
1874
+ rpcCall: authRpcCall,
1304
1875
  onStatusChange: () => {}, // 状态变化已由 listPlatforms 轮询处理,不需要回调
1305
1876
  }),
1306
1877
  );
1307
1878
  }
1308
1879
 
1880
+ const auth = status?.auth;
1881
+ const policy = auth?.adminPolicy ?? 'password_unlock';
1882
+ const isLocked = !isLocalhost && auth?.enabled && policy !== 'open' && !adminUnlocked;
1883
+
1884
+ // 远程设备被锁定:全局展示锁定页面,阻断所有 Tab 的查看与操作
1885
+ if (isLocked) {
1886
+ return React.createElement('div', { style: { maxWidth: 620 } },
1887
+ policy === 'local_only' ? (
1888
+ React.createElement('div', {
1889
+ style: { ...s.card, textAlign: 'center', padding: '36px 20px', marginTop: 10 },
1890
+ },
1891
+ React.createElement('div', { style: { fontSize: 40, marginBottom: 12 } }, '🛡️'),
1892
+ React.createElement('div', { style: { ...s.label, fontSize: 16, fontWeight: 600, marginBottom: 8 } }, '管理控制台已锁定(仅限电脑本机管理)'),
1893
+ React.createElement('div', { style: { ...s.muted, maxWidth: 420, margin: '0 auto', lineHeight: 1.6, fontSize: 13 } },
1894
+ '当前设备通过远程局域网或公网接入。已开启「仅限电脑本机管理」最高安全策略,远程设备禁止查看与修改任何网络与机器人配置。如需管理请在电脑本机(127.0.0.1)上操作。'
1895
+ ),
1896
+ React.createElement('div', { style: { marginTop: 20 } },
1897
+ React.createElement('button', {
1898
+ type: 'button',
1899
+ style: { ...s.btnLink, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' },
1900
+ onClick: () => setShowForgotGuide(v => !v),
1901
+ }, '❓ 远程如何救急解除锁定?'),
1902
+ ),
1903
+ showForgotGuide && React.createElement('div', {
1904
+ style: {
1905
+ marginTop: 14, padding: '12px 14px', borderRadius: 8, fontSize: 12, lineHeight: 1.6,
1906
+ background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
1907
+ color: 'var(--dsw-alias-label-secondary,#4b5563)', textAlign: 'left', maxWidth: 420, margin: '14px auto 0',
1908
+ },
1909
+ },
1910
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 救急解除锁定指引:'),
1911
+ React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改策略或清除密码。'),
1912
+ React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器救急指令'), ':在宿主电脑/服务器终端执行 ', React.createElement('code', { style: s.code }, 'touch ~/.dsh/dsh-bridge/reset-auth'), ' 即可瞬间清空密码恢复初始状态。'),
1913
+ ),
1914
+ )
1915
+ ) : (
1916
+ React.createElement('div', {
1917
+ style: { ...s.card, maxWidth: 440, margin: '20px auto', padding: '32px 24px' },
1918
+ },
1919
+ React.createElement('div', { style: { textAlign: 'center', marginBottom: 20 } },
1920
+ React.createElement('div', { style: { fontSize: 40, marginBottom: 10 } }, '🔒'),
1921
+ React.createElement('div', { style: { ...s.label, fontSize: 16, fontWeight: 600 } }, '管理控制台已锁定'),
1922
+ React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 6, lineHeight: 1.5 } },
1923
+ '当前设备为远程访问。为保护您的网络与平台配置安全,请输入管理员密码解锁管理权限。'
1924
+ ),
1925
+ ),
1926
+ React.createElement('form', {
1927
+ onSubmit: handleUnlockAdmin,
1928
+ style: { display: 'flex', flexDirection: 'column', gap: 12 },
1929
+ },
1930
+ React.createElement('input', {
1931
+ type: 'password',
1932
+ style: s.input,
1933
+ placeholder: '输入后台管理密码',
1934
+ value: unlockPassword,
1935
+ onChange: (e) => setUnlockPassword(e.target.value),
1936
+ autoFocus: true,
1937
+ }),
1938
+ unlockErr && React.createElement('div', {
1939
+ style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' },
1940
+ }, unlockErr),
1941
+ React.createElement('button', {
1942
+ type: 'submit',
1943
+ style: { ...s.btnPri, width: '100%', justifyContent: 'center', height: 36, background: '#4f6ef7', color: '#ffffff' },
1944
+ disabled: unlocking,
1945
+ }, unlocking ? '验证中…' : '解锁管理权限'),
1946
+ ),
1947
+ React.createElement('div', { style: { marginTop: 16, textAlign: 'center' } },
1948
+ React.createElement('button', {
1949
+ type: 'button',
1950
+ style: { ...s.btnLink, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' },
1951
+ onClick: () => setShowForgotGuide(v => !v),
1952
+ }, '❓ 忘记后台管理密码?'),
1953
+ ),
1954
+ showForgotGuide && React.createElement('div', {
1955
+ style: {
1956
+ marginTop: 12, padding: '12px 14px', borderRadius: 8, fontSize: 12, lineHeight: 1.6,
1957
+ background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
1958
+ color: 'var(--dsw-alias-label-secondary,#4b5563)', textAlign: 'left',
1959
+ },
1960
+ },
1961
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 找回与重置密码指引:'),
1962
+ React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改管理密码。'),
1963
+ React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器救急指令'), ':在宿主电脑终端执行 ', React.createElement('code', { style: s.code }, 'touch ~/.dsh/dsh-bridge/reset-auth'), ' 即可瞬间清空密码恢复初始状态。'),
1964
+ ),
1965
+ )
1966
+ )
1967
+ );
1968
+ }
1969
+
1309
1970
  return React.createElement('div', { style: { maxWidth: 620 } },
1310
1971
  err && React.createElement('div', {
1311
1972
  style: { ...s.card, background: 'var(--dsw-alias-state-error-bg,#fef2f2)', color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 13, marginBottom: 16 },
1312
1973
  }, err),
1313
1974
 
1314
- React.createElement(VersionBanner, { rpcCall }),
1975
+ !isLocalhost && adminUnlocked && React.createElement('div', {
1976
+ style: {
1977
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
1978
+ padding: '8px 14px', background: 'var(--dsw-alias-state-info-bg,#eff6ff)',
1979
+ border: '1px solid var(--dsw-alias-brand-primary,#4f6ef7)', borderRadius: 8,
1980
+ marginBottom: 14, fontSize: 12, color: 'var(--dsw-alias-brand-primary,#4f6ef7)',
1981
+ },
1982
+ },
1983
+ React.createElement('span', null, '🔓 管理员权限已解锁(当前临时会话有效)'),
1984
+ React.createElement('button', {
1985
+ style: { ...s.btnGhost, height: 24, fontSize: 11, padding: '0 8px' },
1986
+ onClick: handleLockAdmin,
1987
+ }, '🔒 重新锁定后台'),
1988
+ ),
1989
+
1990
+ React.createElement(VersionBanner, { rpcCall: authRpcCall }),
1315
1991
 
1316
1992
  React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
1317
1993