@wenbin_wb/dsh-bridge 2.3.3 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 },
@@ -260,7 +284,7 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
260
284
  const handleTokenChange = React.useCallback((e) => { setAccessToken(e.target.value); setSaveSuccess(false); }, []);
261
285
 
262
286
  return React.createElement('div', { style: s.block },
263
- React.createElement('div', { style: { ...s.muted, marginBottom: 8 } }, '服务器配置'),
287
+ React.createElement('div', { style: { ...s.muted, marginBottom: 8 } }, '隧道服务器配置'),
264
288
  React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
265
289
  React.createElement('input', {
266
290
  style: s.input,
@@ -273,12 +297,15 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
273
297
  React.createElement('input', {
274
298
  style: s.input,
275
299
  type: 'password',
276
- placeholder: '访问令牌(Access Token)',
300
+ placeholder: '隧道服务端连接令牌(Tunnel Access Token)',
277
301
  value: accessToken,
278
302
  onChange: handleTokenChange,
279
303
  onKeyDown: (e) => { if (e.key === 'Enter' && dirty && !saving) handleSave(); },
280
304
  disabled: saving,
281
305
  }),
306
+ React.createElement('div', { style: { ...s.muted, fontSize: 11 } },
307
+ '💡 用于与您的 VPS 隧道服务端建立反向通道(与 Web 网页访客访问密码互相独立)。'
308
+ ),
282
309
  React.createElement('button', {
283
310
  style: {
284
311
  ...s.btnPri,
@@ -293,7 +320,7 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
293
320
  );
294
321
  });
295
322
 
296
- const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart, onStop, onReset, children }) {
323
+ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart, onStop, onReset, auth, onNavigateSecurity, children }) {
297
324
  const { running, configured, url, qr, state } = data ?? {};
298
325
  const phase = state?.phase ?? 'idle';
299
326
 
@@ -312,7 +339,7 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
312
339
  color: phase === 'error' ? 'var(--dsw-alias-state-error-primary,#dc2626)' : 'var(--dsw-alias-label-secondary,#6b7280)',
313
340
  },
314
341
  }, state?.detail ?? phase),
315
- url && React.createElement(QrBlock, { url, qr, onReset }),
342
+ url && React.createElement(QrBlock, { url, qr, onReset, auth, onNavigateSecurity }),
316
343
  (onStart || onStop) && React.createElement('div', {
317
344
  style: { ...s.block, display: 'flex', gap: 8, flexWrap: 'wrap' },
318
345
  },
@@ -327,6 +354,421 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
327
354
  );
328
355
  });
329
356
 
357
+ // ---- 访问安全认证卡片 ----
358
+
359
+ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpdate }) {
360
+ const [enabled, setEnabled] = React.useState(auth?.enabled ?? false);
361
+ const [mode, setMode] = React.useState(auth?.mode ?? 'token_and_password');
362
+ const [scope, setScope] = React.useState(auth?.scope ?? 'all');
363
+ const [adminPolicy, setAdminPolicy] = React.useState(auth?.adminPolicy ?? 'password_unlock');
364
+
365
+ // 1. 访客访问密码状态
366
+ const [accessPassword, setAccessPassword] = React.useState('');
367
+ const [showAccessPassword, setShowAccessPassword] = React.useState(false);
368
+ const [savingAccess, setSavingAccess] = React.useState(false);
369
+ const [saveAccessSuccess, setSaveAccessSuccess] = React.useState(false);
370
+ const [msgAccess, setMsgAccess] = React.useState(null);
371
+
372
+ // 2. 后台管理密码状态
373
+ const [adminPassword, setAdminPassword] = React.useState('');
374
+ const [showAdminPassword, setShowAdminPassword] = React.useState(false);
375
+ const [savingAdmin, setSavingAdmin] = React.useState(false);
376
+ const [saveAdminSuccess, setSaveAdminSuccess] = React.useState(false);
377
+ const [msgAdmin, setMsgAdmin] = React.useState(null);
378
+
379
+ const [topMsg, setTopMsg] = React.useState(null);
380
+
381
+ React.useEffect(() => {
382
+ if (auth) {
383
+ setEnabled(auth.enabled ?? false);
384
+ setMode(auth.mode ?? 'token_and_password');
385
+ setScope(auth.scope ?? 'all');
386
+ setAdminPolicy(auth.adminPolicy ?? 'password_unlock');
387
+ }
388
+ }, [auth]);
389
+
390
+ const handleToggleEnabled = async () => {
391
+ const next = !enabled;
392
+ setEnabled(next);
393
+ try {
394
+ await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
395
+ setTopMsg({ ok: true, text: next ? '✓ 访问安全认证已开启(现有登录态已刷新)' : '✓ 访问安全认证已关闭' });
396
+ onUpdate?.();
397
+ } catch (e) {
398
+ setTopMsg({ ok: false, text: e.message || '更新失败' });
399
+ }
400
+ };
401
+
402
+ const handleChangeMode = async (m) => {
403
+ setMode(m);
404
+ try {
405
+ await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
406
+ setTopMsg({ ok: true, text: '✓ 外部验证模式已切换,已刷新全域登录态' });
407
+ onUpdate?.();
408
+ } catch (e) {
409
+ setTopMsg({ ok: false, text: e.message || '更新失败' });
410
+ }
411
+ };
412
+
413
+ const handleChangeScope = async (sc) => {
414
+ setScope(sc);
415
+ try {
416
+ await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
417
+ setTopMsg({ ok: true, text: '✓ 防护生效范围已更新' });
418
+ onUpdate?.();
419
+ } catch (e) {
420
+ setTopMsg({ ok: false, text: e.message || '更新失败' });
421
+ }
422
+ };
423
+
424
+ const handleChangeAdminPolicy = async (pol) => {
425
+ setAdminPolicy(pol);
426
+ try {
427
+ await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
428
+ setTopMsg({ ok: true, text: '✓ 远程管理防篡改策略已更新' });
429
+ onUpdate?.();
430
+ } catch (e) {
431
+ setTopMsg({ ok: false, text: e.message || '更新失败' });
432
+ }
433
+ };
434
+
435
+ // 保存访客访问密码
436
+ const handleSaveAccessPassword = async () => {
437
+ setSavingAccess(true);
438
+ setMsgAccess(null);
439
+ try {
440
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { password: accessPassword });
441
+ if (res?.ok) {
442
+ setSaveAccessSuccess(true);
443
+ setMsgAccess({ ok: true, text: '✓ 访客访问密码已成功保存!原有的历史访客会话已全部安全刷新。' });
444
+ setAccessPassword('');
445
+ setTimeout(() => setSaveAccessSuccess(false), 3500);
446
+ onUpdate?.();
447
+ } else {
448
+ setMsgAccess({ ok: false, text: res?.error?.message || '保存失败' });
449
+ }
450
+ } catch (e) {
451
+ setMsgAccess({ ok: false, text: e.message || '保存失败' });
452
+ } finally {
453
+ setSavingAccess(false);
454
+ }
455
+ };
456
+
457
+ // 保存后台管理密码
458
+ const handleSaveAdminPassword = async () => {
459
+ setSavingAdmin(true);
460
+ setMsgAdmin(null);
461
+ try {
462
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPassword });
463
+ if (res?.ok) {
464
+ setSaveAdminSuccess(true);
465
+ setMsgAdmin({ ok: true, text: '✓ 后台管理密码已成功保存!远程管理解锁状态已重置生效。' });
466
+ setAdminPassword('');
467
+ setTimeout(() => setSaveAdminSuccess(false), 3500);
468
+ onUpdate?.();
469
+ } else {
470
+ setMsgAdmin({ ok: false, text: res?.error?.message || '保存失败' });
471
+ }
472
+ } catch (e) {
473
+ setMsgAdmin({ ok: false, text: e.message || '保存失败' });
474
+ } finally {
475
+ setSavingAdmin(false);
476
+ }
477
+ };
478
+
479
+ const handleRegenerateToken = async () => {
480
+ if (!confirm('重置后,之前包含旧 Token 的二维码和分享链接将立即失效。是否确认重置?')) return;
481
+ try {
482
+ await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
483
+ setTopMsg({ ok: true, text: '✓ 安全 Token 已重置,二维码与专属链接已刷新' });
484
+ onUpdate?.();
485
+ } catch (e) {
486
+ setTopMsg({ ok: false, text: e.message || '重置失败' });
487
+ }
488
+ };
489
+
490
+ const scopeLabel = scope === 'all' ? '全部通道 (局域网+公网)' : scope === 'public_only' ? '仅公网隧道' : '仅局域网';
491
+ const modeLabel = mode === 'token_and_password' ? '扫码免密 + 密码' : mode === 'password_only' ? '仅密码登录' : '仅安全 Token';
492
+ const adminLabel = adminPolicy === 'password_unlock' ? '需密码解锁' : adminPolicy === 'local_only' ? '仅限电脑本机管理' : '宽松模式';
493
+
494
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } },
495
+ // ---- 顶部总控与状态概览卡片 ----
496
+ React.createElement('div', { style: s.card },
497
+ React.createElement('div', {
498
+ style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 },
499
+ },
500
+ React.createElement('div', { style: { flex: '1 1 260px' } },
501
+ React.createElement('div', { style: { ...s.label, fontSize: 15, display: 'flex', alignItems: 'center', gap: 8 } },
502
+ '🔐 全局访问安全防护体系',
503
+ ),
504
+ React.createElement('div', { style: { ...s.muted, marginTop: 4 } },
505
+ '集成外部访问门禁拦截与管理后台防篡改控制,双重守护远程会话与网络配置安全',
506
+ ),
507
+ ),
508
+ React.createElement('button', {
509
+ style: { ...(enabled ? s.btnPri : s.btnGhost), whiteSpace: 'nowrap', flexShrink: 0 },
510
+ onClick: handleToggleEnabled,
511
+ }, enabled ? '✓ 已启用安全防护' : '未开启安全防护'),
512
+ ),
513
+
514
+ enabled && React.createElement('div', {
515
+ style: {
516
+ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 14, paddingTop: 12,
517
+ borderTop: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
518
+ },
519
+ },
520
+ React.createElement('div', {
521
+ style: {
522
+ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
523
+ borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
524
+ color: 'var(--dsw-alias-label-secondary,#4b5563)',
525
+ },
526
+ }, '🌐 保护范围: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } }, scopeLabel)),
527
+ React.createElement('div', {
528
+ style: {
529
+ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
530
+ borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
531
+ color: 'var(--dsw-alias-label-secondary,#4b5563)',
532
+ },
533
+ }, '🔑 外部验证: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } }, modeLabel)),
534
+ React.createElement('div', {
535
+ style: {
536
+ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
537
+ borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
538
+ color: 'var(--dsw-alias-label-secondary,#4b5563)',
539
+ },
540
+ }, '🔒 后台防篡改: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-state-success-primary,#059669)' } }, adminLabel)),
541
+ ),
542
+
543
+ topMsg && React.createElement('div', {
544
+ style: {
545
+ marginTop: 12, padding: '8px 12px', borderRadius: 6, fontSize: 12,
546
+ background: topMsg.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
547
+ color: topMsg.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
548
+ },
549
+ }, topMsg.text),
550
+ ),
551
+
552
+ enabled && React.createElement(React.Fragment, null,
553
+ // =========================================================================
554
+ // ---- 第一道防线:外部访问门禁(控制谁能进入 Web 界面使用 AI) ----
555
+ // =========================================================================
556
+ React.createElement('div', { style: s.card },
557
+ React.createElement('div', { style: { marginBottom: 14 } },
558
+ React.createElement('div', { style: { ...s.label, fontSize: 14, display: 'flex', alignItems: 'center', gap: 6 } },
559
+ '🛡️ 第一道防线:外部访问门禁(控制谁能使用 AI)',
560
+ ),
561
+ React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
562
+ '控制外部设备通过局域网 IP 或公网隧道(Cloudflare/自建隧道)进入 DSH 聊天界面时的身份验证方式',
563
+ ),
564
+ ),
565
+
566
+ // 验证模式选择
567
+ React.createElement('div', { style: { marginBottom: 16 } },
568
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '验证模式选择'),
569
+ React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
570
+ [
571
+ { id: 'token_and_password', title: '🟢 扫码免密 + 密码认证 (推荐)', desc: '二维码自带专属 Token 扫码秒进;直接输 IP/公网域名需输密码' },
572
+ { id: 'password_only', title: '🔑 仅密码 / PIN 码登录', desc: '所有外部访问必须手动输入访问密码方可进入' },
573
+ { id: 'token_only', title: '🎫 仅安全 Token 免密', desc: '仅持有带安全 Token 的二维码或专属分享链接方可进入' },
574
+ ].map(opt => {
575
+ const isSel = mode === opt.id;
576
+ return React.createElement('div', {
577
+ key: opt.id,
578
+ onClick: () => handleChangeMode(opt.id),
579
+ style: {
580
+ flex: '1 1 200px',
581
+ padding: '10px 12px',
582
+ borderRadius: 8,
583
+ border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
584
+ background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
585
+ cursor: 'pointer',
586
+ transition: 'all 0.15s ease',
587
+ },
588
+ },
589
+ 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),
590
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } }, opt.desc),
591
+ );
592
+ })
593
+ ),
594
+ ),
595
+
596
+ // 防护生效范围
597
+ React.createElement('div', { style: { marginBottom: 16 } },
598
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '防护生效通道'),
599
+ React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
600
+ [
601
+ { id: 'all', title: '全部通道防护 (推荐)', desc: '局域网 IP 直连与公网隧道全部受安全保护' },
602
+ { id: 'public_only', title: '仅公网隧道开启防护', desc: '局域网内设备直接免密直连,公网隧道强制验证' },
603
+ { id: 'lan_only', title: '仅局域网开启防护', desc: '仅局域网直连需验证,公网隧道不开启' },
604
+ ].map(opt => {
605
+ const isSel = scope === opt.id;
606
+ return React.createElement('div', {
607
+ key: opt.id,
608
+ onClick: () => handleChangeScope(opt.id),
609
+ style: {
610
+ flex: '1 1 180px',
611
+ padding: '9px 12px',
612
+ borderRadius: 8,
613
+ border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
614
+ background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
615
+ cursor: 'pointer',
616
+ transition: 'all 0.15s ease',
617
+ },
618
+ },
619
+ 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),
620
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 3 } }, opt.desc),
621
+ );
622
+ })
623
+ ),
624
+ ),
625
+
626
+ // 访客访问密码输入框 (当非 token_only 时展示)
627
+ mode !== 'token_only' && React.createElement('div', { style: { marginBottom: 16 } },
628
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } },
629
+ `设置外部访客访问密码 ${auth?.hasPassword ? '(✓ 已设置访客密码)' : '(⚠️ 尚未设置密码,直接输入 IP 将免密)'}`,
630
+ ),
631
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
632
+ React.createElement('input', {
633
+ type: showAccessPassword ? 'text' : 'password',
634
+ style: { ...s.input, flex: 1 },
635
+ placeholder: auth?.hasPassword ? '输入新密码以修改(留空保存可清除访客密码)' : '设置外部访客访问密码 / PIN 码',
636
+ value: accessPassword,
637
+ onChange: e => setAccessPassword(e.target.value),
638
+ }),
639
+ React.createElement('button', {
640
+ style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
641
+ onClick: () => setShowAccessPassword(v => !v),
642
+ }, showAccessPassword ? '隐藏' : '显示'),
643
+ React.createElement('button', {
644
+ style: {
645
+ ...s.btnPri, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0,
646
+ background: saveAccessSuccess ? '#059669' : 'var(--dsw-alias-brand-primary, #4f6ef7)',
647
+ color: '#ffffff',
648
+ cursor: savingAccess ? 'wait' : 'pointer',
649
+ },
650
+ onClick: handleSaveAccessPassword,
651
+ disabled: savingAccess,
652
+ }, savingAccess ? '保存中…' : saveAccessSuccess ? '✓ 已成功保存!' : '保存访问密码'),
653
+ ),
654
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
655
+ '💡 当外部朋友或同事未通过二维码扫码,而是直接输入 IP 或公网域名访问时,需输入此密码登录。'
656
+ ),
657
+ msgAccess && React.createElement('div', {
658
+ style: {
659
+ marginTop: 8, padding: '6px 12px', borderRadius: 6, fontSize: 12,
660
+ background: msgAccess.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
661
+ color: msgAccess.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
662
+ },
663
+ }, msgAccess.text),
664
+ ),
665
+
666
+ // 免密 Token 管理
667
+ mode !== 'password_only' && React.createElement('div', { style: s.block },
668
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } }, '免密扫码 Token (专属访问凭据)'),
669
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
670
+ 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)' } },
671
+ auth?.secretToken ? `${auth.secretToken.slice(0, 10)}****************` : '未生成'
672
+ ),
673
+ React.createElement('button', {
674
+ style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
675
+ onClick: handleRegenerateToken,
676
+ title: '重新生成 Token,使之前分享的旧二维码和链接立即失效',
677
+ }, '🔄 重置安全 Token'),
678
+ ),
679
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
680
+ '💡 控制台生成的局域网与公网二维码已自动嵌入此 Token,手机扫码即可免密进入聊天界面(但不赋予后台管理设置权限)。'
681
+ ),
682
+ ),
683
+ ),
684
+
685
+ // =========================================================================
686
+ // ---- 第二道防线:后台管理防篡改(控制谁能修改本插件所有设置) ----
687
+ // =========================================================================
688
+ React.createElement('div', { style: s.card },
689
+ React.createElement('div', { style: { marginBottom: 14 } },
690
+ React.createElement('div', { style: { ...s.label, fontSize: 14, display: 'flex', alignItems: 'center', gap: 6 } },
691
+ '🔒 第二道防线:管理后台防篡改(控制谁能修改本插件所有设置)',
692
+ ),
693
+ React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
694
+ '锁定整个插件设置后台(包含局域网、公网隧道、IM 机器人密钥与安全设置),防止他人随意篡改配置',
695
+ ),
696
+ ),
697
+
698
+ // 管理员密码设置
699
+ React.createElement('div', { style: { marginBottom: 16 } },
700
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } },
701
+ `设置独立管理员密码 ${auth?.hasAdminPassword ? '(✓ 已设置独立管理密码)' : '(未单独设置,默认使用上述访客访问密码)'}`,
702
+ ),
703
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
704
+ React.createElement('input', {
705
+ type: showAdminPassword ? 'text' : 'password',
706
+ style: { ...s.input, flex: 1 },
707
+ placeholder: auth?.hasAdminPassword ? '输入新密码以修改(留空保存可清除独立管理密码)' : '设置后台管理解锁密码(建议与访客密码不同)',
708
+ value: adminPassword,
709
+ onChange: e => setAdminPassword(e.target.value),
710
+ }),
711
+ React.createElement('button', {
712
+ style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
713
+ onClick: () => setShowAdminPassword(v => !v),
714
+ }, showAdminPassword ? '隐藏' : '显示'),
715
+ React.createElement('button', {
716
+ style: {
717
+ ...s.btnPri, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0,
718
+ background: saveAdminSuccess ? '#059669' : 'var(--dsw-alias-brand-primary, #4f6ef7)',
719
+ color: '#ffffff',
720
+ cursor: savingAdmin ? 'wait' : 'pointer',
721
+ },
722
+ onClick: handleSaveAdminPassword,
723
+ disabled: savingAdmin,
724
+ }, savingAdmin ? '保存中…' : saveAdminSuccess ? '✓ 已成功保存!' : '保存管理密码'),
725
+ ),
726
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4, color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } },
727
+ '🔑 核心作用:用于远程设备进入设置后台时的解锁验证。设置后,即便把访问密码告知他人,他人也无法进入设置后台改配置。'
728
+ ),
729
+ msgAdmin && React.createElement('div', {
730
+ style: {
731
+ marginTop: 8, padding: '6px 12px', borderRadius: 6, fontSize: 12,
732
+ background: msgAdmin.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
733
+ color: msgAdmin.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
734
+ },
735
+ }, msgAdmin.text),
736
+ ),
737
+
738
+ // 远程管理权限控制策略
739
+ React.createElement('div', { style: s.block },
740
+ React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '远程设备管理权限策略'),
741
+ React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
742
+ [
743
+ { id: 'password_unlock', title: '🔒 需密码解锁 (推荐)', desc: '远程手机/外网打开本插件设置时默认全局锁定,输入管理密码解锁后方可使用' },
744
+ { id: 'local_only', title: '🚫 仅限电脑本机管理 (最严格)', desc: '远程设备彻底锁定整个设置后台,仅允许在 127.0.0.1 电脑本机上操作' },
745
+ { id: 'open', title: '🔓 宽松模式', desc: '任何已通过第一道防线登录的设备均可直接修改所有配置' },
746
+ ].map(opt => {
747
+ const isSel = adminPolicy === opt.id;
748
+ return React.createElement('div', {
749
+ key: opt.id,
750
+ onClick: () => handleChangeAdminPolicy(opt.id),
751
+ style: {
752
+ flex: '1 1 180px',
753
+ padding: '10px 12px',
754
+ borderRadius: 8,
755
+ border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
756
+ background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
757
+ cursor: 'pointer',
758
+ transition: 'all 0.15s ease',
759
+ },
760
+ },
761
+ 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),
762
+ React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } }, opt.desc),
763
+ );
764
+ })
765
+ ),
766
+ ),
767
+ ),
768
+ ),
769
+ );
770
+ });
771
+
330
772
  // ---- 通用 IM 平台卡片 ----
331
773
 
332
774
  function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatusChange }) {
@@ -343,16 +785,18 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
343
785
  setCfgDraft({
344
786
  digestIntervalSec: String(platform.config.digestIntervalSec ?? 300),
345
787
  approvalTimeoutSec: String(platform.config.approvalTimeoutSec ?? 600),
346
- maxMessageChars: String(platform.config.maxMessageChars ?? 2000),
788
+ maxMessageChars: String(platform.config.maxMessageChars ?? (platformId === 'telegram' ? 4096 : 2000)),
347
789
  sendChunkDelayMs: String(platform.config.sendChunkDelayMs ?? 1500),
348
790
  appId: platform.config.appId ?? '',
349
791
  // Secret 不由后端回传;空值表示沿用已保存密钥
350
792
  clientSecret: '',
351
793
  appSecret: '',
352
794
  domain: platform.config.domain ?? 'feishu',
795
+ botToken: '',
796
+ proxy: platform.config.proxy ?? '',
353
797
  });
354
798
  }
355
- }, [platform?.config]);
799
+ }, [platform?.config, platformId]);
356
800
 
357
801
  // 向上传递连接状态(供平台列表卡片绿点使用)
358
802
  React.useEffect(() => {
@@ -424,10 +868,10 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
424
868
  ...d,
425
869
  digestIntervalSec: '300',
426
870
  approvalTimeoutSec: '600',
427
- maxMessageChars: '2000',
871
+ maxMessageChars: platformId === 'telegram' ? '4096' : '2000',
428
872
  sendChunkDelayMs: '1500',
429
873
  }));
430
- }, []);
874
+ }, [platformId]);
431
875
 
432
876
  // 高级设置保存
433
877
  const saveConfig = React.useCallback(async () => {
@@ -438,7 +882,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
438
882
  maxMessageChars: Number(cfgDraft.maxMessageChars),
439
883
  sendChunkDelayMs: Number(cfgDraft.sendChunkDelayMs),
440
884
  };
441
- // QQ / 飞书 平台额外携带凭证
885
+ // QQ / 飞书 / Telegram 平台额外携带凭证
442
886
  if (platformId === 'qq') {
443
887
  payload.appId = cfgDraft.appId.trim();
444
888
  payload.clientSecret = cfgDraft.clientSecret.trim();
@@ -446,6 +890,9 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
446
890
  payload.appId = cfgDraft.appId.trim();
447
891
  payload.appSecret = cfgDraft.appSecret.trim();
448
892
  payload.domain = cfgDraft.domain || 'feishu';
893
+ } else if (platformId === 'telegram') {
894
+ payload.botToken = cfgDraft.botToken.trim();
895
+ payload.proxy = cfgDraft.proxy.trim();
449
896
  }
450
897
  await act(BRIDGE_ENDPOINTS.platformSetConfig, payload);
451
898
  }, [act, cfgDraft, platformId]);
@@ -461,6 +908,10 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
461
908
  (platformId === 'feishu' && (
462
909
  cfgDraft.appId !== (platform.config.appId ?? '') ||
463
910
  cfgDraft.appSecret !== (platform.config.appSecret ?? '')
911
+ )) ||
912
+ (platformId === 'telegram' && (
913
+ cfgDraft.botToken !== '' ||
914
+ cfgDraft.proxy !== (platform.config.proxy ?? '')
464
915
  ))
465
916
  );
466
917
 
@@ -523,6 +974,16 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
523
974
  target: '_blank', rel: 'noopener noreferrer',
524
975
  style: s.btnGhost,
525
976
  }, '🌐 飞书开放平台'),
977
+ platformId === 'telegram' && React.createElement('a', {
978
+ href: 'https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/telegram-usage.md',
979
+ target: '_blank', rel: 'noopener noreferrer',
980
+ style: s.btnGhost,
981
+ }, '📖 Telegram 使用说明'),
982
+ platformId === 'telegram' && React.createElement('a', {
983
+ href: 'https://t.me/BotFather',
984
+ target: '_blank', rel: 'noopener noreferrer',
985
+ style: s.btnGhost,
986
+ }, '🌐 @BotFather 申请 Bot'),
526
987
  React.createElement('button', {
527
988
  style: s.btnGhost,
528
989
  onClick: () => setShowHelp(v => !v),
@@ -643,8 +1104,8 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
643
1104
  title: '清除登录凭证,下次需重新配置',
644
1105
  }, '解绑账号'),
645
1106
  ),
646
- // 飞书专属:手机扫码直达 Bot 对话
647
- platformId === 'feishu' && platform.botQr && React.createElement('div', {
1107
+ // 飞书 / Telegram 扫码直达对话引导卡片
1108
+ (platformId === 'feishu' || platformId === 'telegram') && platform.botQr && React.createElement('div', {
648
1109
  style: {
649
1110
  marginTop: 12,
650
1111
  padding: 12,
@@ -657,11 +1118,11 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
657
1118
  flexWrap: 'wrap',
658
1119
  },
659
1120
  },
660
- React.createElement('img', { src: platform.botQr, alt: 'Feishu Bot QR', style: { width: 110, height: 110, borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', padding: 4, background: '#fff' } }),
1121
+ React.createElement('img', { src: platform.botQr, alt: `${platformName} Bot QR`, style: { width: 110, height: 110, borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', padding: 4, background: '#fff' } }),
661
1122
  React.createElement('div', { style: { flex: 1, minWidth: 160 } },
662
- React.createElement('div', { style: { ...s.label, fontSize: 13, fontWeight: 600 } }, '📱 手机飞书扫码直达对话'),
1123
+ React.createElement('div', { style: { ...s.label, fontSize: 13, fontWeight: 600 } }, `📱 手机 ${platformName} 扫码直达对话`),
663
1124
  React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 4, lineHeight: 1.5 } },
664
- '用飞书手机 App 扫描左侧二维码,立即打开与 Bot 对话;发送首条消息自动完成白名单授权。'
1125
+ `用 ${platformName} 扫描左侧二维码,立即打开与 Bot 对话;发送首条消息自动完成白名单授权。`
665
1126
  ),
666
1127
  platform.botLink && React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8 } },
667
1128
  React.createElement('a', {
@@ -669,13 +1130,13 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
669
1130
  target: '_blank',
670
1131
  rel: 'noopener noreferrer',
671
1132
  style: { ...s.btnGhost, textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, padding: '4px 10px', fontSize: 12 },
672
- }, '在飞书客户端打开 ↗'),
1133
+ }, `在 ${platformName} 客户端打开 ↗`),
673
1134
  ),
674
1135
  ),
675
1136
  ),
676
1137
  ),
677
1138
 
678
- // 未配置 / 登录中:表单(QQ / 飞书)或二维码(微信)
1139
+ // 未配置 / 登录中:表单(QQ / 飞书 / Telegram)或二维码(微信)
679
1140
  (!platform?.configured || showQr) && React.createElement('div', { style: s.block },
680
1141
  showQr && login.qr
681
1142
  ? React.createElement('div', null,
@@ -687,7 +1148,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
687
1148
  ),
688
1149
  login.error && React.createElement('div', { style: { ...s.muted, marginTop: 4, color: 'var(--dsw-alias-state-warn-primary,#92400e)' } }, login.error),
689
1150
  )
690
- : (platformId === 'qq' || platformId === 'feishu')
1151
+ : (platformId === 'qq' || platformId === 'feishu' || platformId === 'telegram')
691
1152
  ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10, marginTop: 4 } },
692
1153
  platformId === 'feishu' && React.createElement('div', {
693
1154
  style: {
@@ -704,40 +1165,76 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
704
1165
  React.createElement('code', { style: { ...s.code, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '2px 4px', borderRadius: 4 } }, 'npx feishu-bot-bootstrap'),
705
1166
  React.createElement('span', { style: s.muted }, ' 手机扫码一键自动创建应用并输出凭证;或在下方手动填入凭证。')
706
1167
  ),
707
- React.createElement('div', null,
708
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } },
709
- platformId === 'qq' ? 'AppID — QQ 开放平台机器人应用 ID' : 'App ID — 飞书开放平台自建应用 ID (cli_xxx)'
710
- ),
711
- React.createElement('input', {
712
- style: { ...s.input, width: '100%' },
713
- placeholder: platformId === 'qq' ? '请输入 AppID' : '请输入 App ID (如 cli_a1b2c3d4...)',
714
- value: cfgDraft?.appId ?? '',
715
- onChange: (e) => setCfgDraft(d => ({ ...d, appId: e.target.value })),
716
- }),
717
- ),
718
- React.createElement('div', null,
719
- React.createElement('div', { style: { ...s.muted, marginBottom: 4 } },
720
- platformId === 'qq' ? 'ClientSecret QQ 开放平台机器人密钥' : 'App Secret 飞书开放平台应用密钥'
721
- ),
722
- React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
723
- React.createElement('input', {
724
- style: { ...s.input, flex: 1 },
725
- type: showSecret ? 'text' : 'password',
726
- placeholder: platformId === 'qq' ? '请输入 ClientSecret' : '请输入 App Secret',
727
- value: platformId === 'qq' ? (cfgDraft?.clientSecret ?? '') : (cfgDraft?.appSecret ?? ''),
728
- onChange: (e) => setCfgDraft(d => platformId === 'qq' ? ({ ...d, clientSecret: e.target.value }) : ({ ...d, appSecret: e.target.value })),
729
- }),
730
- React.createElement('button', {
731
- style: { ...s.btnGhost, height: 32, padding: '0 10px', fontSize: 13, flexShrink: 0 },
732
- onClick: () => setShowSecret(v => !v),
733
- type: 'button',
734
- title: showSecret ? '隐藏密钥' : '显示明文',
735
- }, showSecret ? '🙈 隐藏' : '👁️ 显示'),
736
- ),
737
- ),
1168
+ platformId === 'telegram'
1169
+ ? React.createElement(React.Fragment, null,
1170
+ React.createElement('div', null,
1171
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, 'Bot Token — Telegram @BotFather 下发的机器人 Token'),
1172
+ React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
1173
+ React.createElement('input', {
1174
+ style: { ...s.input, flex: 1 },
1175
+ type: showSecret ? 'text' : 'password',
1176
+ placeholder: '请输入 Telegram Bot Token ( 123456789:ABCdef...)',
1177
+ value: cfgDraft?.botToken ?? '',
1178
+ onChange: (e) => setCfgDraft(d => ({ ...d, botToken: e.target.value })),
1179
+ }),
1180
+ React.createElement('button', {
1181
+ style: { ...s.btnGhost, height: 32, padding: '0 10px', fontSize: 13, flexShrink: 0 },
1182
+ onClick: () => setShowSecret(v => !v),
1183
+ type: 'button',
1184
+ title: showSecret ? '隐藏密钥' : '显示明文',
1185
+ }, showSecret ? '🙈 隐藏' : '👁️ 显示'),
1186
+ ),
1187
+ ),
1188
+ React.createElement('div', null,
1189
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '网络代理 (可选) 支持国内 HTTP / HTTPS 代理'),
1190
+ React.createElement('input', {
1191
+ style: { ...s.input, width: '100%' },
1192
+ placeholder: '可选,例如 http://127.0.0.1:7890(为空则直连或读取环境变量)',
1193
+ value: cfgDraft?.proxy ?? '',
1194
+ onChange: (e) => setCfgDraft(d => ({ ...d, proxy: e.target.value })),
1195
+ }),
1196
+ ),
1197
+ )
1198
+ : React.createElement(React.Fragment, null,
1199
+ React.createElement('div', null,
1200
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } },
1201
+ platformId === 'qq' ? 'AppID — QQ 开放平台机器人应用 ID' : 'App ID — 飞书开放平台自建应用 ID (cli_xxx)'
1202
+ ),
1203
+ React.createElement('input', {
1204
+ style: { ...s.input, width: '100%' },
1205
+ placeholder: platformId === 'qq' ? '请输入 AppID' : '请输入 App ID (如 cli_a1b2c3d4...)',
1206
+ value: cfgDraft?.appId ?? '',
1207
+ onChange: (e) => setCfgDraft(d => ({ ...d, appId: e.target.value })),
1208
+ }),
1209
+ ),
1210
+ React.createElement('div', null,
1211
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } },
1212
+ platformId === 'qq' ? 'ClientSecret — QQ 开放平台机器人密钥' : 'App Secret — 飞书开放平台应用密钥'
1213
+ ),
1214
+ React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
1215
+ React.createElement('input', {
1216
+ style: { ...s.input, flex: 1 },
1217
+ type: showSecret ? 'text' : 'password',
1218
+ placeholder: platformId === 'qq' ? '请输入 ClientSecret' : '请输入 App Secret',
1219
+ value: platformId === 'qq' ? (cfgDraft?.clientSecret ?? '') : (cfgDraft?.appSecret ?? ''),
1220
+ onChange: (e) => setCfgDraft(d => platformId === 'qq' ? ({ ...d, clientSecret: e.target.value }) : ({ ...d, appSecret: e.target.value })),
1221
+ }),
1222
+ React.createElement('button', {
1223
+ style: { ...s.btnGhost, height: 32, padding: '0 10px', fontSize: 13, flexShrink: 0 },
1224
+ onClick: () => setShowSecret(v => !v),
1225
+ type: 'button',
1226
+ title: showSecret ? '隐藏密钥' : '显示明文',
1227
+ }, showSecret ? '🙈 隐藏' : '👁️ 显示'),
1228
+ ),
1229
+ ),
1230
+ ),
738
1231
  React.createElement('div', null,
739
1232
  React.createElement('a', {
740
- href: platformId === 'qq' ? 'https://bot.q.qq.com/wiki/develop/api-v2/' : 'https://open.feishu.cn/app',
1233
+ href: platformId === 'qq'
1234
+ ? 'https://bot.q.qq.com/wiki/develop/api-v2/'
1235
+ : platformId === 'feishu'
1236
+ ? 'https://open.feishu.cn/app'
1237
+ : 'https://t.me/BotFather',
741
1238
  target: '_blank', rel: 'noopener noreferrer',
742
1239
  style: s.btnLink,
743
1240
  }, platformId === 'qq' ? '📖 前往 QQ 开放平台申请机器人' : '📖 前往飞书开放平台创建企业自建应用'),
@@ -746,7 +1243,9 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
746
1243
  React.createElement('button', {
747
1244
  style: { ...s.btnPri, opacity: busy ? 0.5 : 1 },
748
1245
  onClick: saveConfig,
749
- disabled: busy || !cfgDraft?.appId?.trim() || (platformId === 'qq' ? !cfgDraft?.clientSecret?.trim() : !cfgDraft?.appSecret?.trim()),
1246
+ disabled: busy || (platformId === 'telegram'
1247
+ ? !cfgDraft?.botToken?.trim()
1248
+ : (!cfgDraft?.appId?.trim() || (platformId === 'qq' ? !cfgDraft?.clientSecret?.trim() : !cfgDraft?.appSecret?.trim()))),
750
1249
  }, busy ? '保存中…' : '保存并连接'),
751
1250
  login.phase === 'error' && React.createElement('div', { style: { ...s.muted, fontSize: 12 } }, login.error ?? '连接失败'),
752
1251
  ),
@@ -1006,9 +1505,10 @@ function VersionBanner({ rpcCall }) {
1006
1505
  // ---- Tab Bar ----
1007
1506
 
1008
1507
  const TABS = [
1009
- { id: 'lan', label: '局域网', icon: Icons.lan },
1010
- { id: 'tunnel', label: '公网隧道', icon: Icons.tunnel },
1011
- { id: 'im', label: 'IM 机器人', icon: Icons.bot },
1508
+ { id: 'lan', label: '局域网', icon: Icons.lan },
1509
+ { id: 'tunnel', label: '公网隧道', icon: Icons.tunnel },
1510
+ { id: 'im', label: 'IM 机器人', icon: Icons.bot },
1511
+ { id: 'security', label: '安全认证', icon: Icons.security },
1012
1512
  ];
1013
1513
 
1014
1514
  function TabBar({ active, onChange, dots }) {
@@ -1068,6 +1568,43 @@ function BridgePanel({ rpcCall }) {
1068
1568
  const [platforms, setPlatforms] = React.useState(null);
1069
1569
  const [selectedPlatform, setSelectedPlatform] = React.useState('wechat');
1070
1570
 
1571
+ // 远程设备管理权限解锁状态
1572
+ const isLocalhost = typeof window === 'undefined' || (
1573
+ !window.location.hostname ||
1574
+ window.location.hostname === '127.0.0.1' ||
1575
+ window.location.hostname === 'localhost' ||
1576
+ window.location.hostname === '::1' ||
1577
+ window.location.hostname === '' ||
1578
+ window.location.protocol === 'file:' ||
1579
+ window.location.protocol === 'vscode-webview:' ||
1580
+ window.location.protocol === 'app:' ||
1581
+ window.location.hostname.endsWith('.local')
1582
+ );
1583
+ const [adminUnlocked, setAdminUnlocked] = React.useState(false);
1584
+ const [unlockPassword, setUnlockPassword] = React.useState('');
1585
+ const [unlockErr, setUnlockErr] = React.useState(null);
1586
+ const [unlocking, setUnlocking] = React.useState(false);
1587
+ const [showForgotGuide, setShowForgotGuide] = React.useState(false);
1588
+
1589
+ const handleUnlockAdmin = React.useCallback(async (e) => {
1590
+ e?.preventDefault?.();
1591
+ setUnlocking(true);
1592
+ setUnlockErr(null);
1593
+ try {
1594
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authAdminUnlock, { password: unlockPassword });
1595
+ if (res?.ok) {
1596
+ setAdminUnlocked(true);
1597
+ setUnlockPassword('');
1598
+ } else {
1599
+ setUnlockErr(res?.error?.message || '管理员密码错误');
1600
+ }
1601
+ } catch (err) {
1602
+ setUnlockErr(err.message || '解锁请求失败');
1603
+ } finally {
1604
+ setUnlocking(false);
1605
+ }
1606
+ }, [rpcCall, unlockPassword]);
1607
+
1071
1608
  const load = React.useCallback(async (quiet = false) => {
1072
1609
  try {
1073
1610
  const r = await rpcCall(BRIDGE_ENDPOINTS.getStatus, {});
@@ -1123,6 +1660,8 @@ function BridgePanel({ rpcCall }) {
1123
1660
  act(BRIDGE_ENDPOINTS.saveCustomTunnelConfig, { serverUrl, accessToken })
1124
1661
  , [act]);
1125
1662
 
1663
+ const navSecurity = React.useCallback(() => setActiveTab('security'), []);
1664
+
1126
1665
  if (!status && !err) {
1127
1666
  return React.createElement('div', {
1128
1667
  style: { padding: 32, color: 'var(--dsw-alias-label-tertiary,#9ca3af)', fontSize: 13 },
@@ -1136,9 +1675,10 @@ function BridgePanel({ rpcCall }) {
1136
1675
  p.status === 'connected' || p.status === 'starting' || p.status === 'reconnecting'
1137
1676
  );
1138
1677
  const dots = {
1139
- lan: !!(status?.proxy?.running),
1140
- tunnel: !!(status?.cloudflared?.running || ct?.running),
1141
- im: !!imConnected,
1678
+ lan: !!(status?.proxy?.running),
1679
+ tunnel: !!(status?.cloudflared?.running || ct?.running),
1680
+ im: !!imConnected,
1681
+ security: !!(status?.auth?.enabled),
1142
1682
  };
1143
1683
 
1144
1684
  // Tab 内容
@@ -1148,6 +1688,8 @@ function BridgePanel({ rpcCall }) {
1148
1688
  title: '局域网访问',
1149
1689
  desc: '同一 Wi-Fi 下的设备可直接扫码访问',
1150
1690
  data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
1691
+ auth: status?.auth,
1692
+ onNavigateSecurity: navSecurity,
1151
1693
  });
1152
1694
  } else if (activeTab === 'tunnel') {
1153
1695
  tabContent = React.createElement(React.Fragment, null,
@@ -1160,6 +1702,8 @@ function BridgePanel({ rpcCall }) {
1160
1702
  qr: status?.cloudflared?.qr,
1161
1703
  state: status?.cloudflared?.state,
1162
1704
  },
1705
+ auth: status?.auth,
1706
+ onNavigateSecurity: navSecurity,
1163
1707
  onStart: onStartCloudflared,
1164
1708
  onStop: onStopCloudflared,
1165
1709
  onReset: status?.cloudflared?.running ? onResetCloudflared : null,
@@ -1174,6 +1718,8 @@ function BridgePanel({ rpcCall }) {
1174
1718
  qr: ct?.qr,
1175
1719
  state: ct?.state,
1176
1720
  },
1721
+ auth: status?.auth,
1722
+ onNavigateSecurity: navSecurity,
1177
1723
  onStart: onStartCustom,
1178
1724
  onStop: onStopCustom,
1179
1725
  },
@@ -1185,6 +1731,12 @@ function BridgePanel({ rpcCall }) {
1185
1731
  }),
1186
1732
  ),
1187
1733
  );
1734
+ } else if (activeTab === 'security') {
1735
+ tabContent = React.createElement(AccessAuthCard, {
1736
+ auth: status?.auth,
1737
+ rpcCall,
1738
+ onUpdate: () => load(true),
1739
+ });
1188
1740
  } else if (activeTab === 'im') {
1189
1741
  // 从 listPlatforms 动态生成平台列表
1190
1742
  const IM_PLATFORMS = [
@@ -1249,11 +1801,116 @@ function BridgePanel({ rpcCall }) {
1249
1801
  );
1250
1802
  }
1251
1803
 
1804
+ const auth = status?.auth;
1805
+ const policy = auth?.adminPolicy ?? 'password_unlock';
1806
+ const isLocked = !isLocalhost && auth?.enabled && policy !== 'open' && !adminUnlocked;
1807
+
1808
+ // 远程设备被锁定:全局展示锁定页面,阻断所有 Tab 的查看与操作
1809
+ if (isLocked) {
1810
+ return React.createElement('div', { style: { maxWidth: 620 } },
1811
+ policy === 'local_only' ? (
1812
+ React.createElement('div', {
1813
+ style: { ...s.card, textAlign: 'center', padding: '36px 20px', marginTop: 10 },
1814
+ },
1815
+ React.createElement('div', { style: { fontSize: 40, marginBottom: 12 } }, '🛡️'),
1816
+ React.createElement('div', { style: { ...s.label, fontSize: 16, fontWeight: 600, marginBottom: 8 } }, '管理控制台已锁定(仅限电脑本机管理)'),
1817
+ React.createElement('div', { style: { ...s.muted, maxWidth: 420, margin: '0 auto', lineHeight: 1.6, fontSize: 13 } },
1818
+ '当前设备通过远程局域网或公网接入。已开启「仅限电脑本机管理」最高安全策略,远程设备禁止查看与修改任何网络与机器人配置。如需管理请在电脑本机(127.0.0.1)上操作。'
1819
+ ),
1820
+ React.createElement('div', { style: { marginTop: 20 } },
1821
+ React.createElement('button', {
1822
+ type: 'button',
1823
+ style: { ...s.btnLink, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' },
1824
+ onClick: () => setShowForgotGuide(v => !v),
1825
+ }, '❓ 远程如何救急解除锁定?'),
1826
+ ),
1827
+ showForgotGuide && React.createElement('div', {
1828
+ style: {
1829
+ marginTop: 14, padding: '12px 14px', borderRadius: 8, fontSize: 12, lineHeight: 1.6,
1830
+ background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
1831
+ color: 'var(--dsw-alias-label-secondary,#4b5563)', textAlign: 'left', maxWidth: 420, margin: '14px auto 0',
1832
+ },
1833
+ },
1834
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 救急解除锁定指引:'),
1835
+ React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改策略或清除密码。'),
1836
+ React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器救急指令'), ':在宿主电脑/服务器终端执行 ', React.createElement('code', { style: s.code }, 'touch ~/.dsh/dsh-bridge/reset-auth'), ' 即可瞬间清空密码恢复初始状态。'),
1837
+ ),
1838
+ )
1839
+ ) : (
1840
+ React.createElement('div', {
1841
+ style: { ...s.card, maxWidth: 440, margin: '20px auto', padding: '32px 24px' },
1842
+ },
1843
+ React.createElement('div', { style: { textAlign: 'center', marginBottom: 20 } },
1844
+ React.createElement('div', { style: { fontSize: 40, marginBottom: 10 } }, '🔒'),
1845
+ React.createElement('div', { style: { ...s.label, fontSize: 16, fontWeight: 600 } }, '管理控制台已锁定'),
1846
+ React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 6, lineHeight: 1.5 } },
1847
+ '当前设备为远程访问。为保护您的网络与平台配置安全,请输入管理员密码解锁管理权限。'
1848
+ ),
1849
+ ),
1850
+ React.createElement('form', {
1851
+ onSubmit: handleUnlockAdmin,
1852
+ style: { display: 'flex', flexDirection: 'column', gap: 12 },
1853
+ },
1854
+ React.createElement('input', {
1855
+ type: 'password',
1856
+ style: s.input,
1857
+ placeholder: '输入后台管理密码',
1858
+ value: unlockPassword,
1859
+ onChange: (e) => setUnlockPassword(e.target.value),
1860
+ autoFocus: true,
1861
+ }),
1862
+ unlockErr && React.createElement('div', {
1863
+ style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' },
1864
+ }, unlockErr),
1865
+ React.createElement('button', {
1866
+ type: 'submit',
1867
+ style: { ...s.btnPri, width: '100%', justifyContent: 'center', height: 36, background: '#4f6ef7', color: '#ffffff' },
1868
+ disabled: unlocking,
1869
+ }, unlocking ? '验证中…' : '解锁管理权限'),
1870
+ ),
1871
+ React.createElement('div', { style: { marginTop: 16, textAlign: 'center' } },
1872
+ React.createElement('button', {
1873
+ type: 'button',
1874
+ style: { ...s.btnLink, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' },
1875
+ onClick: () => setShowForgotGuide(v => !v),
1876
+ }, '❓ 忘记后台管理密码?'),
1877
+ ),
1878
+ showForgotGuide && React.createElement('div', {
1879
+ style: {
1880
+ marginTop: 12, padding: '12px 14px', borderRadius: 8, fontSize: 12, lineHeight: 1.6,
1881
+ background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
1882
+ color: 'var(--dsw-alias-label-secondary,#4b5563)', textAlign: 'left',
1883
+ },
1884
+ },
1885
+ React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 找回与重置密码指引:'),
1886
+ React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改管理密码。'),
1887
+ React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器救急指令'), ':在宿主电脑终端执行 ', React.createElement('code', { style: s.code }, 'touch ~/.dsh/dsh-bridge/reset-auth'), ' 即可瞬间清空密码恢复初始状态。'),
1888
+ ),
1889
+ )
1890
+ )
1891
+ );
1892
+ }
1893
+
1252
1894
  return React.createElement('div', { style: { maxWidth: 620 } },
1253
1895
  err && React.createElement('div', {
1254
1896
  style: { ...s.card, background: 'var(--dsw-alias-state-error-bg,#fef2f2)', color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 13, marginBottom: 16 },
1255
1897
  }, err),
1256
1898
 
1899
+ !isLocalhost && adminUnlocked && React.createElement('div', {
1900
+ style: {
1901
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
1902
+ padding: '8px 14px', background: 'var(--dsw-alias-state-info-bg,#eff6ff)',
1903
+ border: '1px solid var(--dsw-alias-brand-primary,#4f6ef7)', borderRadius: 8,
1904
+ marginBottom: 14, fontSize: 12, color: 'var(--dsw-alias-brand-primary,#4f6ef7)',
1905
+ },
1906
+ },
1907
+ React.createElement('span', null, '🔓 管理员权限已解锁(当前临时会话有效)'),
1908
+ React.createElement('button', {
1909
+ style: { ...s.btnGhost, height: 24, fontSize: 11, padding: '0 8px' },
1910
+ onClick: () => setAdminUnlocked(false),
1911
+ }, '🔒 重新锁定后台'),
1912
+ ),
1913
+
1257
1914
  React.createElement(VersionBanner, { rpcCall }),
1258
1915
 
1259
1916
  React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),