@wenbin_wb/dsh-bridge 2.5.0 → 2.5.2

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
@@ -1,5 +1,26 @@
1
1
  // dsh-bridge 客户端插件:设置页「远程访问」面板
2
2
 
3
+ // 兼容非 HTTPS 环境(如手机局域网 HTTP 访问):为非安全上下文补齐 crypto.randomUUID
4
+ if (typeof window !== 'undefined') {
5
+ if (!window.crypto) {
6
+ window.crypto = {};
7
+ }
8
+ if (!window.crypto.randomUUID) {
9
+ window.crypto.randomUUID = function() {
10
+ if (typeof window.crypto.getRandomValues === 'function') {
11
+ return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) {
12
+ return (c ^ window.crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> (c / 4)).toString(16);
13
+ });
14
+ }
15
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
16
+ var r = (Math.random() * 16) | 0;
17
+ var v = c === 'x' ? r : (r & 0x3) | 0x8;
18
+ return v.toString(16);
19
+ });
20
+ };
21
+ }
22
+ }
23
+
3
24
  import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from '../lib/bridge-rpc-constants.js';
4
25
 
5
26
  const GITHUB_URL = 'https://github.com/wenbin-wb/dsh-bridge';
@@ -270,18 +291,23 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
270
291
  }, [initUrl, initToken]);
271
292
 
272
293
  const dirty = serverUrl !== (initUrl ?? '') || accessToken !== (initToken ?? '');
294
+ const [saveErr, setSaveErr] = React.useState(null);
273
295
  const handleSave = React.useCallback(async () => {
274
296
  setSaving(true);
275
297
  setSaveSuccess(false);
298
+ setSaveErr(null);
276
299
  try {
277
300
  await onSave(serverUrl, accessToken);
278
301
  setSaveSuccess(true);
279
302
  setTimeout(() => setSaveSuccess(false), 2500);
303
+ } catch (e) {
304
+ setSaveErr(e.message || '保存配置失败');
305
+ } finally {
306
+ setSaving(false);
280
307
  }
281
- finally { setSaving(false); }
282
308
  }, [onSave, serverUrl, accessToken]);
283
- const handleUrlChange = React.useCallback((e) => { setServerUrl(e.target.value); setSaveSuccess(false); }, []);
284
- const handleTokenChange = React.useCallback((e) => { setAccessToken(e.target.value); setSaveSuccess(false); }, []);
309
+ const handleUrlChange = React.useCallback((e) => { setServerUrl(e.target.value); setSaveSuccess(false); setSaveErr(null); }, []);
310
+ const handleTokenChange = React.useCallback((e) => { setAccessToken(e.target.value); setSaveSuccess(false); setSaveErr(null); }, []);
285
311
 
286
312
  return React.createElement('div', { style: s.block },
287
313
  React.createElement('div', { style: { ...s.muted, marginBottom: 8 } }, '隧道服务器配置'),
@@ -303,6 +329,7 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
303
329
  onKeyDown: (e) => { if (e.key === 'Enter' && dirty && !saving) handleSave(); },
304
330
  disabled: saving,
305
331
  }),
332
+ saveErr && React.createElement('div', { style: s.err }, `❌ ${saveErr}`),
306
333
  React.createElement('div', { style: { ...s.muted, fontSize: 11 } },
307
334
  '💡 用于与您的 VPS 隧道服务端建立反向通道(与 Web 网页访客访问密码互相独立)。'
308
335
  ),
@@ -388,46 +415,58 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
388
415
  }, [auth]);
389
416
 
390
417
  const handleToggleEnabled = async () => {
418
+ const prev = enabled;
391
419
  const next = !enabled;
392
420
  setEnabled(next);
393
421
  try {
394
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
422
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
423
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
395
424
  setTopMsg({ ok: true, text: next ? '✓ 访问安全认证已开启(现有登录态已刷新)' : '✓ 访问安全认证已关闭' });
396
425
  onUpdate?.();
397
426
  } catch (e) {
427
+ setEnabled(prev);
398
428
  setTopMsg({ ok: false, text: e.message || '更新失败' });
399
429
  }
400
430
  };
401
431
 
402
432
  const handleChangeMode = async (m) => {
433
+ const prev = mode;
403
434
  setMode(m);
404
435
  try {
405
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
436
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
437
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
406
438
  setTopMsg({ ok: true, text: '✓ 外部验证模式已切换,已刷新全域登录态' });
407
439
  onUpdate?.();
408
440
  } catch (e) {
441
+ setMode(prev);
409
442
  setTopMsg({ ok: false, text: e.message || '更新失败' });
410
443
  }
411
444
  };
412
445
 
413
446
  const handleChangeScope = async (sc) => {
447
+ const prev = scope;
414
448
  setScope(sc);
415
449
  try {
416
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
450
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
451
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
417
452
  setTopMsg({ ok: true, text: '✓ 防护生效范围已更新' });
418
453
  onUpdate?.();
419
454
  } catch (e) {
455
+ setScope(prev);
420
456
  setTopMsg({ ok: false, text: e.message || '更新失败' });
421
457
  }
422
458
  };
423
459
 
424
460
  const handleChangeAdminPolicy = async (pol) => {
461
+ const prev = adminPolicy;
425
462
  setAdminPolicy(pol);
426
463
  try {
427
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
464
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
465
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
428
466
  setTopMsg({ ok: true, text: '✓ 远程管理防篡改策略已更新' });
429
467
  onUpdate?.();
430
468
  } catch (e) {
469
+ setAdminPolicy(prev);
431
470
  setTopMsg({ ok: false, text: e.message || '更新失败' });
432
471
  }
433
472
  };
@@ -479,7 +518,8 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
479
518
  const handleRegenerateToken = async () => {
480
519
  if (!confirm('重置后,之前包含旧 Token 的二维码和分享链接将立即失效。是否确认重置?')) return;
481
520
  try {
482
- await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
521
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
522
+ if (!res?.ok) throw new Error(res?.error?.message || '重置失败');
483
523
  setTopMsg({ ok: true, text: '✓ 安全 Token 已重置,二维码与专属链接已刷新' });
484
524
  onUpdate?.();
485
525
  } catch (e) {
@@ -804,16 +844,24 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
804
844
  onStatusChange?.(connected);
805
845
  }, [platform?.status, onStatusChange]);
806
846
 
847
+ const loadInFlightRef = React.useRef(false);
848
+ const seqRef = React.useRef(0);
807
849
  const load = React.useCallback(async (quiet = false) => {
850
+ if (loadInFlightRef.current) return;
851
+ loadInFlightRef.current = true;
852
+ const currentSeq = ++seqRef.current;
808
853
  try {
809
854
  // 用通用端点读取平台状态(不执行登录操作,只获取状态)
810
855
  const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
856
+ if (currentSeq !== seqRef.current) return; // 丢弃过时响应
811
857
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
812
858
  const allPlatforms = r.value ?? {};
813
859
  setPlatform(allPlatforms[platformId] ?? null);
814
860
  if (!quiet) setErr(null);
815
861
  } catch (e) {
816
- if (!quiet) setErr(e.message);
862
+ if (currentSeq === seqRef.current && !quiet) setErr(e.message);
863
+ } finally {
864
+ loadInFlightRef.current = false;
817
865
  }
818
866
  }, [rpcCall, platformId]);
819
867
 
@@ -850,9 +898,20 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
850
898
  const id = newId.trim();
851
899
  if (!id) return;
852
900
  const list = [...(platform?.allowFrom ?? []), id];
853
- await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
854
- setNewId('');
855
- }, [act, newId, platform?.allowFrom]);
901
+ setBusy(true);
902
+ try {
903
+ const r = await rpcCall(BRIDGE_ENDPOINTS.platformSetAllowFrom, { platformId, allowFrom: list });
904
+ if (!r?.ok) throw new Error(r?.error?.message ?? '添加白名单失败');
905
+ setPlatform(r.value);
906
+ setNewId('');
907
+ setErr(null);
908
+ await load(true);
909
+ } catch (e) {
910
+ setErr(e.message);
911
+ } finally {
912
+ setBusy(false);
913
+ }
914
+ }, [rpcCall, platformId, newId, platform?.allowFrom, load]);
856
915
  const removeAllow = React.useCallback(async (id) => {
857
916
  const list = (platform?.allowFrom ?? []).filter((x) => x !== id);
858
917
  await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
@@ -922,7 +981,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
922
981
  );
923
982
  }
924
983
 
925
- const connected = platform?.status === 'connected' || platform?.status === 'starting';
984
+ const connected = platform?.status === 'connected' || platform?.status === 'starting' || platform?.status === 'reconnecting';
926
985
  const login = platform?.login ?? {};
927
986
  const showQr = login.phase === 'qr' || login.phase === 'scaned';
928
987
  const statusLabel = platform?.status === 'connected' ? '已连接'
@@ -1580,11 +1639,69 @@ function BridgePanel({ rpcCall }) {
1580
1639
  window.location.protocol === 'app:' ||
1581
1640
  window.location.hostname.endsWith('.local')
1582
1641
  );
1642
+ const [adminToken, setAdminToken] = React.useState('');
1583
1643
  const [adminUnlocked, setAdminUnlocked] = React.useState(false);
1584
1644
  const [unlockPassword, setUnlockPassword] = React.useState('');
1585
- const [unlockErr, setUnlockErr] = React.useState(null);
1586
- const [unlocking, setUnlocking] = React.useState(false);
1645
+ const [unlockErr, setUnlockErr] = React.useState(null);
1646
+ const [unlocking, setUnlocking] = React.useState(false);
1587
1647
  const [showForgotGuide, setShowForgotGuide] = React.useState(false);
1648
+ const [showUnlockModal, setShowUnlockModal] = React.useState(false);
1649
+
1650
+ // 本机物理访问自动静默获取 adminToken,免输密码直通管理(支持 3080 原生端口与 3082 代理端口)
1651
+ const fetchLoopbackToken = React.useCallback(async () => {
1652
+ if (!isLocalhost) return null;
1653
+ const currentPort = typeof window !== 'undefined' ? (window.location.port || (window.location.protocol === 'https:' ? '443' : '80')) : '3082';
1654
+ const proxyPort = status?.proxy?.port || 3082;
1655
+ const candidateUrls = [
1656
+ '/__dsh_bridge__/loopback-token',
1657
+ `http://127.0.0.1:${proxyPort}/__dsh_bridge__/loopback-token`,
1658
+ `http://localhost:${proxyPort}/__dsh_bridge__/loopback-token`,
1659
+ 'http://127.0.0.1:3082/__dsh_bridge__/loopback-token',
1660
+ ];
1661
+ const uniqueUrls = [...new Set(candidateUrls)];
1662
+
1663
+ for (const url of uniqueUrls) {
1664
+ try {
1665
+ const res = await fetch(url, { method: 'POST' });
1666
+ if (res.ok) {
1667
+ const data = await res.json();
1668
+ if (data?.ok && data.adminToken) {
1669
+ setAdminToken(data.adminToken);
1670
+ setAdminUnlocked(true);
1671
+ return data.adminToken;
1672
+ }
1673
+ }
1674
+ } catch {}
1675
+ }
1676
+ return null;
1677
+ }, [isLocalhost, status?.proxy?.port]);
1678
+
1679
+ React.useEffect(() => {
1680
+ if (isLocalhost && !adminUnlocked) {
1681
+ fetchLoopbackToken();
1682
+ }
1683
+ }, [isLocalhost, adminUnlocked, fetchLoopbackToken]);
1684
+
1685
+ const authRpcCall = React.useCallback(async (endpoint, payload = {}, signal) => {
1686
+ let token = adminToken;
1687
+ if (isLocalhost && !token) {
1688
+ token = await fetchLoopbackToken();
1689
+ }
1690
+ const enriched = {
1691
+ ...payload,
1692
+ ...(token ? { adminToken: token } : {}),
1693
+ ...(isLocalhost ? { isLocalhost: true } : {}),
1694
+ };
1695
+ const res = await rpcCall(endpoint, enriched, signal);
1696
+ if (res?.ok === false) {
1697
+ const msg = res?.error?.message || '';
1698
+ if (msg.includes('管理员权限') || msg.includes('管理密码解锁')) {
1699
+ setUnlockErr(msg);
1700
+ setShowUnlockModal(true);
1701
+ }
1702
+ }
1703
+ return res;
1704
+ }, [rpcCall, adminToken, isLocalhost, fetchLoopbackToken]);
1588
1705
 
1589
1706
  const handleUnlockAdmin = React.useCallback(async (e) => {
1590
1707
  e?.preventDefault?.();
@@ -1593,8 +1710,11 @@ function BridgePanel({ rpcCall }) {
1593
1710
  try {
1594
1711
  const res = await rpcCall(BRIDGE_ENDPOINTS.authAdminUnlock, { password: unlockPassword });
1595
1712
  if (res?.ok) {
1713
+ setAdminToken(res.value?.adminToken || '');
1596
1714
  setAdminUnlocked(true);
1597
1715
  setUnlockPassword('');
1716
+ setShowUnlockModal(false);
1717
+ setErr(null);
1598
1718
  } else {
1599
1719
  setUnlockErr(res?.error?.message || '管理员密码错误');
1600
1720
  }
@@ -1605,32 +1725,58 @@ function BridgePanel({ rpcCall }) {
1605
1725
  }
1606
1726
  }, [rpcCall, unlockPassword]);
1607
1727
 
1728
+ const handleLockAdmin = React.useCallback(async () => {
1729
+ try {
1730
+ if (adminToken) {
1731
+ await rpcCall(BRIDGE_ENDPOINTS.authAdminLock, { adminToken });
1732
+ }
1733
+ } catch {}
1734
+ setAdminToken('');
1735
+ setAdminUnlocked(false);
1736
+ }, [rpcCall, adminToken]);
1737
+
1738
+ const loadInFlightRef = React.useRef(false);
1739
+ const loadSeqRef = React.useRef(0);
1608
1740
  const load = React.useCallback(async (quiet = false) => {
1741
+ if (loadInFlightRef.current) return;
1742
+ loadInFlightRef.current = true;
1743
+ const currentSeq = ++loadSeqRef.current;
1609
1744
  try {
1610
- const r = await rpcCall(BRIDGE_ENDPOINTS.getStatus, {});
1745
+ const r = await authRpcCall(BRIDGE_ENDPOINTS.getStatus, {});
1746
+ if (currentSeq !== loadSeqRef.current) return;
1611
1747
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
1612
1748
  setStatus(r.value);
1613
1749
  if (!quiet) setErr(null);
1614
1750
  } catch (e) {
1615
- setErr(e.message);
1751
+ if (currentSeq === loadSeqRef.current) setErr(e.message);
1752
+ } finally {
1753
+ loadInFlightRef.current = false;
1616
1754
  }
1617
- }, [rpcCall]);
1755
+ }, [authRpcCall]);
1618
1756
 
1619
- // 独立轮询所有平台状态(Tab 未选中时也能更新)
1757
+ // 独立轮询所有平台状态(Tab 未选中时也能更新),带 in-flight 锁与序列号防乱序
1758
+ const pollPlatformsSeqRef = React.useRef(0);
1620
1759
  React.useEffect(() => {
1621
1760
  let alive = true;
1761
+ let inFlight = false;
1622
1762
  const poll = async () => {
1763
+ if (inFlight || !alive) return;
1764
+ inFlight = true;
1765
+ const currentSeq = ++pollPlatformsSeqRef.current;
1623
1766
  try {
1624
- const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1625
- if (alive && r?.ok) {
1767
+ const r = await authRpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1768
+ if (alive && currentSeq === pollPlatformsSeqRef.current && r?.ok) {
1626
1769
  setPlatforms(r.value ?? {});
1627
1770
  }
1628
1771
  } catch { /* 忽略,不影响主面板 */ }
1772
+ finally {
1773
+ inFlight = false;
1774
+ }
1629
1775
  };
1630
1776
  poll();
1631
1777
  const t = setInterval(poll, 4000);
1632
1778
  return () => { alive = false; clearInterval(t); };
1633
- }, [rpcCall]);
1779
+ }, [authRpcCall]);
1634
1780
 
1635
1781
  React.useEffect(() => {
1636
1782
  load();
@@ -1640,14 +1786,14 @@ function BridgePanel({ rpcCall }) {
1640
1786
 
1641
1787
  const act = React.useCallback(async (endpoint, payload) => {
1642
1788
  try {
1643
- const r = await rpcCall(endpoint, payload ?? {});
1789
+ const r = await authRpcCall(endpoint, payload ?? {});
1644
1790
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
1645
1791
  setStatus(r.value);
1646
1792
  setErr(null);
1647
1793
  } catch (e) {
1648
1794
  setErr(e.message);
1649
1795
  }
1650
- }, [rpcCall]);
1796
+ }, [authRpcCall]);
1651
1797
 
1652
1798
  const onStartCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCloudflared), [act]);
1653
1799
  const onStopCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCloudflared), [act]);
@@ -1734,7 +1880,7 @@ function BridgePanel({ rpcCall }) {
1734
1880
  } else if (activeTab === 'security') {
1735
1881
  tabContent = React.createElement(AccessAuthCard, {
1736
1882
  auth: status?.auth,
1737
- rpcCall,
1883
+ rpcCall: authRpcCall,
1738
1884
  onUpdate: () => load(true),
1739
1885
  });
1740
1886
  } else if (activeTab === 'im') {
@@ -1790,12 +1936,13 @@ function BridgePanel({ rpcCall }) {
1790
1936
  );
1791
1937
  }),
1792
1938
  ),
1793
- // 显示选中的平台卡片
1939
+ // 显示选中的平台卡片(带有 key 保证切换时重置表单状态)
1794
1940
  selectedPlatform && platforms?.[selectedPlatform] && React.createElement(PlatformCard, {
1941
+ key: selectedPlatform,
1795
1942
  platformId: selectedPlatform,
1796
1943
  platformName: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.label ?? selectedPlatform,
1797
1944
  platformDesc: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.desc ?? '',
1798
- rpcCall,
1945
+ rpcCall: authRpcCall,
1799
1946
  onStatusChange: () => {}, // 状态变化已由 listPlatforms 轮询处理,不需要回调
1800
1947
  }),
1801
1948
  );
@@ -1891,11 +2038,36 @@ function BridgePanel({ rpcCall }) {
1891
2038
  );
1892
2039
  }
1893
2040
 
1894
- return React.createElement('div', { style: { maxWidth: 620 } },
2041
+ const isInterceptionErr = err && (err.includes('管理员权限') || err.includes('管理密码解锁'));
2042
+
2043
+ return React.createElement('div', { style: { maxWidth: 620, position: 'relative' } },
2044
+ // 错误横幅(如果是权限拦截,直接提供醒目的输入密码解锁按钮)
1895
2045
  err && React.createElement('div', {
1896
- style: { ...s.card, background: 'var(--dsw-alias-state-error-bg,#fef2f2)', color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 13, marginBottom: 16 },
1897
- }, err),
2046
+ style: {
2047
+ ...s.card,
2048
+ background: 'var(--dsw-alias-state-error-bg,#fef2f2)',
2049
+ color: 'var(--dsw-alias-state-error-primary,#dc2626)',
2050
+ fontSize: 13,
2051
+ marginBottom: 16,
2052
+ display: 'flex',
2053
+ alignItems: 'center',
2054
+ justifyContent: 'space-between',
2055
+ flexWrap: 'wrap',
2056
+ gap: 10,
2057
+ },
2058
+ },
2059
+ React.createElement('span', { style: { flex: '1 1 auto' } }, err),
2060
+ isInterceptionErr && React.createElement('button', {
2061
+ type: 'button',
2062
+ style: { ...s.btnPri, background: '#dc2626', color: '#ffffff', height: 26, fontSize: 12, padding: '0 10px', flexShrink: 0 },
2063
+ onClick: () => {
2064
+ setUnlockErr(err);
2065
+ setShowUnlockModal(true);
2066
+ },
2067
+ }, '🔑 立即输入管理密码解锁'),
2068
+ ),
1898
2069
 
2070
+ // 管理员解锁状态提示条
1899
2071
  !isLocalhost && adminUnlocked && React.createElement('div', {
1900
2072
  style: {
1901
2073
  display: 'flex', alignItems: 'center', justifyContent: 'space-between',
@@ -1907,15 +2079,94 @@ function BridgePanel({ rpcCall }) {
1907
2079
  React.createElement('span', null, '🔓 管理员权限已解锁(当前临时会话有效)'),
1908
2080
  React.createElement('button', {
1909
2081
  style: { ...s.btnGhost, height: 24, fontSize: 11, padding: '0 8px' },
1910
- onClick: () => setAdminUnlocked(false),
2082
+ onClick: handleLockAdmin,
1911
2083
  }, '🔒 重新锁定后台'),
1912
2084
  ),
1913
2085
 
1914
- React.createElement(VersionBanner, { rpcCall }),
2086
+ // 未解锁时的顶部引导条
2087
+ !isLocalhost && !adminUnlocked && auth?.enabled && policy !== 'open' && React.createElement('div', {
2088
+ style: {
2089
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
2090
+ padding: '8px 14px', background: 'var(--dsw-alias-state-warn-bg,#fffbeb)',
2091
+ border: '1px solid var(--dsw-alias-state-warn-border,#fde68a)', borderRadius: 8,
2092
+ marginBottom: 14, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#92400e)',
2093
+ },
2094
+ },
2095
+ React.createElement('span', null, '🔒 后台管理权限未解锁(修改敏感配置需先解锁)'),
2096
+ React.createElement('button', {
2097
+ type: 'button',
2098
+ style: { ...s.btnPri, height: 24, fontSize: 11, padding: '0 10px', background: '#d97706' },
2099
+ onClick: () => setShowUnlockModal(true),
2100
+ }, '🔑 解锁管理权限'),
2101
+ ),
2102
+
2103
+ React.createElement(VersionBanner, { rpcCall: authRpcCall }),
1915
2104
 
1916
2105
  React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
1917
2106
 
1918
2107
  tabContent,
2108
+
2109
+ // 全局交互式解锁弹窗 Modal
2110
+ showUnlockModal && React.createElement('div', {
2111
+ style: {
2112
+ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 99999,
2113
+ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
2114
+ },
2115
+ onClick: (e) => { if (e.target === e.currentTarget) setShowUnlockModal(false); },
2116
+ },
2117
+ React.createElement('div', {
2118
+ style: {
2119
+ background: 'var(--dsw-alias-bg-layer-1,#ffffff)', borderRadius: 14,
2120
+ padding: '24px 24px', maxWidth: 420, width: '100%',
2121
+ boxShadow: '0 20px 25px -5px rgba(0,0,0,0.2)', border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
2122
+ },
2123
+ },
2124
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 } },
2125
+ React.createElement('div', { style: { fontSize: 16, fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', display: 'flex', alignItems: 'center', gap: 8 } },
2126
+ '🔒 解锁后台管理权限'
2127
+ ),
2128
+ React.createElement('button', {
2129
+ type: 'button',
2130
+ style: { border: 'none', background: 'none', cursor: 'pointer', fontSize: 18, color: 'var(--dsw-alias-label-tertiary,#9ca3af)', padding: 0 },
2131
+ onClick: () => setShowUnlockModal(false),
2132
+ }, '✕'),
2133
+ ),
2134
+ React.createElement('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary,#4b5563)', marginBottom: 16, lineHeight: 1.5 } },
2135
+ '当前操作需要后台管理员权限。为保护您的网络配置与机器人平台安全,请输入管理密码解锁:'
2136
+ ),
2137
+ React.createElement('form', {
2138
+ onSubmit: handleUnlockAdmin,
2139
+ style: { display: 'flex', flexDirection: 'column', gap: 12 },
2140
+ },
2141
+ React.createElement('input', {
2142
+ type: 'password',
2143
+ style: s.input,
2144
+ placeholder: '请输入后台管理密码',
2145
+ value: unlockPassword,
2146
+ onChange: (e) => setUnlockPassword(e.target.value),
2147
+ autoFocus: true,
2148
+ }),
2149
+ unlockErr && React.createElement('div', {
2150
+ style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' },
2151
+ }, unlockErr),
2152
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 4 } },
2153
+ React.createElement('button', {
2154
+ type: 'button',
2155
+ style: s.btnGhost,
2156
+ onClick: () => setShowUnlockModal(false),
2157
+ }, '取消'),
2158
+ React.createElement('button', {
2159
+ type: 'submit',
2160
+ style: { ...s.btnPri, background: '#4f6ef7', color: '#fff' },
2161
+ disabled: unlocking || !unlockPassword,
2162
+ }, unlocking ? '验证中…' : '立即解锁'),
2163
+ ),
2164
+ ),
2165
+ React.createElement('div', { style: { marginTop: 14, paddingTop: 10, borderTop: '1px solid var(--dsw-alias-border-l2,#f3f4f6)', fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#9ca3af)', textAlign: 'center', lineHeight: 1.5 } },
2166
+ '💡 提示:若未单独配置管理密码,请输入初次设置的访问密码;电脑本机(127.0.0.1)访问享有免密管理特权。'
2167
+ ),
2168
+ ),
2169
+ ),
1919
2170
  );
1920
2171
  }
1921
2172
 
package/docs/banner.jpg CHANGED
Binary file
@@ -69,6 +69,8 @@
69
69
  3. 点击 **「保存并连接」**;
70
70
  4. 状态显示为绿色 **「已连接」** 即表示长连接成功建立!
71
71
 
72
+ ![飞书机器人配置](screenshots/feishu-bot-config.jpg)
73
+
72
74
  ---
73
75
 
74
76
  ## 💬 常用操作与指令
@@ -91,3 +93,6 @@
91
93
  当 Agent 尝试执行需授权的操作(如终端命令、写敏感文件)时,飞书端会自动推送 **原生交互卡片**:
92
94
  - 点击卡片上的 **「✓ 批准执行」** 或 **「✕ 拒绝执行」** 按钮即可一键处理;
93
95
  - 亦可直接回复文字 `/yes` (或 `1`) / `/no` (或 `2`)。
96
+
97
+ ![飞书对话与卡片审批](screenshots/feishu-chat.jpg)
98
+