@wenbin_wb/dsh-bridge 2.5.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/README.en.md CHANGED
@@ -179,7 +179,7 @@ Automatically active when the plugin starts, zero configuration needed. Open Set
179
179
 
180
180
  ### Custom Tunnel
181
181
 
182
- Requires a server with a public IP. See the [Custom Tunnel Setup Guide](docs/custom-tunnel.md) for detailed steps.
182
+ Requires a server with a public IP (server environment requires Node.js >= 18, recommended Node.js 22 LTS). See the [Custom Tunnel Setup Guide](docs/custom-tunnel.md) for detailed steps.
183
183
 
184
184
  1. Deploy the tunnel server on your server following the guide
185
185
  2. Enter the WebSocket URL (`wss://...`) and access token in the "Custom Tunnel" card
package/README.md CHANGED
@@ -179,7 +179,7 @@ dsh plugin --profile web add @wenbin_wb/dsh-bridge@latest
179
179
 
180
180
  ### 自建隧道
181
181
 
182
- 需要一台有公网 IP 的服务器。详细搭建步骤见 [自建隧道教程](docs/custom-tunnel.md)。
182
+ 需要一台有公网 IP 的服务器(服务端环境要求 Node.js >= 18,推荐 Node.js 22 LTS)。详细搭建步骤见 [自建隧道教程](docs/custom-tunnel.md)。
183
183
 
184
184
  1. 按教程在服务器上部署隧道服务端
185
185
  2. 在「自建隧道」卡片中填写 WebSocket 地址(`wss://...`)和访问令牌
package/client/client.js CHANGED
@@ -346,13 +346,17 @@ var CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm2({ serve
346
346
  }
347
347
  }, [initUrl, initToken]);
348
348
  const dirty = serverUrl !== (initUrl ?? "") || accessToken !== (initToken ?? "");
349
+ const [saveErr, setSaveErr] = React.useState(null);
349
350
  const handleSave = React.useCallback(async () => {
350
351
  setSaving(true);
351
352
  setSaveSuccess(false);
353
+ setSaveErr(null);
352
354
  try {
353
355
  await onSave(serverUrl, accessToken);
354
356
  setSaveSuccess(true);
355
357
  setTimeout(() => setSaveSuccess(false), 2500);
358
+ } catch (e) {
359
+ setSaveErr(e.message || "\u4FDD\u5B58\u914D\u7F6E\u5931\u8D25");
356
360
  } finally {
357
361
  setSaving(false);
358
362
  }
@@ -360,10 +364,12 @@ var CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm2({ serve
360
364
  const handleUrlChange = React.useCallback((e) => {
361
365
  setServerUrl(e.target.value);
362
366
  setSaveSuccess(false);
367
+ setSaveErr(null);
363
368
  }, []);
364
369
  const handleTokenChange = React.useCallback((e) => {
365
370
  setAccessToken(e.target.value);
366
371
  setSaveSuccess(false);
372
+ setSaveErr(null);
367
373
  }, []);
368
374
  return React.createElement(
369
375
  "div",
@@ -393,6 +399,7 @@ var CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm2({ serve
393
399
  },
394
400
  disabled: saving
395
401
  }),
402
+ saveErr && React.createElement("div", { style: s.err }, `\u274C ${saveErr}`),
396
403
  React.createElement(
397
404
  "div",
398
405
  { style: { ...s.muted, fontSize: 11 } },
@@ -477,43 +484,55 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
477
484
  }
478
485
  }, [auth]);
479
486
  const handleToggleEnabled = async () => {
487
+ const prev = enabled;
480
488
  const next = !enabled;
481
489
  setEnabled(next);
482
490
  try {
483
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
491
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
492
+ if (!res?.ok) throw new Error(res?.error?.message || "\u66F4\u65B0\u5931\u8D25");
484
493
  setTopMsg({ ok: true, text: next ? "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5F00\u542F\uFF08\u73B0\u6709\u767B\u5F55\u6001\u5DF2\u5237\u65B0\uFF09" : "\u2713 \u8BBF\u95EE\u5B89\u5168\u8BA4\u8BC1\u5DF2\u5173\u95ED" });
485
494
  onUpdate?.();
486
495
  } catch (e) {
496
+ setEnabled(prev);
487
497
  setTopMsg({ ok: false, text: e.message || "\u66F4\u65B0\u5931\u8D25" });
488
498
  }
489
499
  };
490
500
  const handleChangeMode = async (m) => {
501
+ const prev = mode;
491
502
  setMode(m);
492
503
  try {
493
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
504
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
505
+ if (!res?.ok) throw new Error(res?.error?.message || "\u66F4\u65B0\u5931\u8D25");
494
506
  setTopMsg({ ok: true, text: "\u2713 \u5916\u90E8\u9A8C\u8BC1\u6A21\u5F0F\u5DF2\u5207\u6362\uFF0C\u5DF2\u5237\u65B0\u5168\u57DF\u767B\u5F55\u6001" });
495
507
  onUpdate?.();
496
508
  } catch (e) {
509
+ setMode(prev);
497
510
  setTopMsg({ ok: false, text: e.message || "\u66F4\u65B0\u5931\u8D25" });
498
511
  }
499
512
  };
500
513
  const handleChangeScope = async (sc) => {
514
+ const prev = scope;
501
515
  setScope(sc);
502
516
  try {
503
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
517
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
518
+ if (!res?.ok) throw new Error(res?.error?.message || "\u66F4\u65B0\u5931\u8D25");
504
519
  setTopMsg({ ok: true, text: "\u2713 \u9632\u62A4\u751F\u6548\u8303\u56F4\u5DF2\u66F4\u65B0" });
505
520
  onUpdate?.();
506
521
  } catch (e) {
522
+ setScope(prev);
507
523
  setTopMsg({ ok: false, text: e.message || "\u66F4\u65B0\u5931\u8D25" });
508
524
  }
509
525
  };
510
526
  const handleChangeAdminPolicy = async (pol) => {
527
+ const prev = adminPolicy;
511
528
  setAdminPolicy(pol);
512
529
  try {
513
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
530
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
531
+ if (!res?.ok) throw new Error(res?.error?.message || "\u66F4\u65B0\u5931\u8D25");
514
532
  setTopMsg({ ok: true, text: "\u2713 \u8FDC\u7A0B\u7BA1\u7406\u9632\u7BE1\u6539\u7B56\u7565\u5DF2\u66F4\u65B0" });
515
533
  onUpdate?.();
516
534
  } catch (e) {
535
+ setAdminPolicy(prev);
517
536
  setTopMsg({ ok: false, text: e.message || "\u66F4\u65B0\u5931\u8D25" });
518
537
  }
519
538
  };
@@ -560,7 +579,8 @@ var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpda
560
579
  const handleRegenerateToken = async () => {
561
580
  if (!confirm("\u91CD\u7F6E\u540E\uFF0C\u4E4B\u524D\u5305\u542B\u65E7 Token \u7684\u4E8C\u7EF4\u7801\u548C\u5206\u4EAB\u94FE\u63A5\u5C06\u7ACB\u5373\u5931\u6548\u3002\u662F\u5426\u786E\u8BA4\u91CD\u7F6E\uFF1F")) return;
562
581
  try {
563
- await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
582
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
583
+ if (!res?.ok) throw new Error(res?.error?.message || "\u91CD\u7F6E\u5931\u8D25");
564
584
  setTopMsg({ ok: true, text: "\u2713 \u5B89\u5168 Token \u5DF2\u91CD\u7F6E\uFF0C\u4E8C\u7EF4\u7801\u4E0E\u4E13\u5C5E\u94FE\u63A5\u5DF2\u5237\u65B0" });
565
585
  onUpdate?.();
566
586
  } catch (e) {
@@ -975,15 +995,23 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
975
995
  const connected2 = platform?.status === "connected" || platform?.status === "starting" || platform?.status === "reconnecting";
976
996
  onStatusChange?.(connected2);
977
997
  }, [platform?.status, onStatusChange]);
998
+ const loadInFlightRef = React.useRef(false);
999
+ const seqRef = React.useRef(0);
978
1000
  const load = React.useCallback(async (quiet = false) => {
1001
+ if (loadInFlightRef.current) return;
1002
+ loadInFlightRef.current = true;
1003
+ const currentSeq = ++seqRef.current;
979
1004
  try {
980
1005
  const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1006
+ if (currentSeq !== seqRef.current) return;
981
1007
  if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
982
1008
  const allPlatforms = r.value ?? {};
983
1009
  setPlatform(allPlatforms[platformId] ?? null);
984
1010
  if (!quiet) setErr(null);
985
1011
  } catch (e) {
986
- if (!quiet) setErr(e.message);
1012
+ if (currentSeq === seqRef.current && !quiet) setErr(e.message);
1013
+ } finally {
1014
+ loadInFlightRef.current = false;
987
1015
  }
988
1016
  }, [rpcCall, platformId]);
989
1017
  React.useEffect(() => {
@@ -1014,9 +1042,20 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1014
1042
  const id = newId.trim();
1015
1043
  if (!id) return;
1016
1044
  const list = [...platform?.allowFrom ?? [], id];
1017
- await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
1018
- setNewId("");
1019
- }, [act, newId, platform?.allowFrom]);
1045
+ setBusy(true);
1046
+ try {
1047
+ const r = await rpcCall(BRIDGE_ENDPOINTS.platformSetAllowFrom, { platformId, allowFrom: list });
1048
+ if (!r?.ok) throw new Error(r?.error?.message ?? "\u6DFB\u52A0\u767D\u540D\u5355\u5931\u8D25");
1049
+ setPlatform(r.value);
1050
+ setNewId("");
1051
+ setErr(null);
1052
+ await load(true);
1053
+ } catch (e) {
1054
+ setErr(e.message);
1055
+ } finally {
1056
+ setBusy(false);
1057
+ }
1058
+ }, [rpcCall, platformId, newId, platform?.allowFrom, load]);
1020
1059
  const removeAllow = React.useCallback(async (id) => {
1021
1060
  const list = (platform?.allowFrom ?? []).filter((x) => x !== id);
1022
1061
  await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
@@ -1062,7 +1101,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1062
1101
  React.createElement("div", { style: { ...s.muted, marginTop: 6 } }, "\u52A0\u8F7D\u4E2D\u2026")
1063
1102
  );
1064
1103
  }
1065
- const connected = platform?.status === "connected" || platform?.status === "starting";
1104
+ const connected = platform?.status === "connected" || platform?.status === "starting" || platform?.status === "reconnecting";
1066
1105
  const login = platform?.login ?? {};
1067
1106
  const showQr = login.phase === "qr" || login.phase === "scaned";
1068
1107
  const statusLabel = platform?.status === "connected" ? "\u5DF2\u8FDE\u63A5" : platform?.status === "starting" ? "\u8FDE\u63A5\u4E2D\u2026" : platform?.status === "reconnecting" ? "\u91CD\u8FDE\u4E2D\u2026" : platform?.status === "paused" ? "\u6682\u505C\uFF08\u4F1A\u8BDD\u8FC7\u671F\uFF09" : platform?.status === "error" ? "\u9519\u8BEF" : "\u672A\u8FDE\u63A5";
@@ -1785,11 +1824,20 @@ function BridgePanel({ rpcCall }) {
1785
1824
  const [platforms, setPlatforms] = React.useState(null);
1786
1825
  const [selectedPlatform, setSelectedPlatform] = React.useState("wechat");
1787
1826
  const isLocalhost = typeof window === "undefined" || (!window.location.hostname || window.location.hostname === "127.0.0.1" || window.location.hostname === "localhost" || window.location.hostname === "::1" || window.location.hostname === "" || window.location.protocol === "file:" || window.location.protocol === "vscode-webview:" || window.location.protocol === "app:" || window.location.hostname.endsWith(".local"));
1827
+ const [adminToken, setAdminToken] = React.useState("");
1788
1828
  const [adminUnlocked, setAdminUnlocked] = React.useState(false);
1789
1829
  const [unlockPassword, setUnlockPassword] = React.useState("");
1790
1830
  const [unlockErr, setUnlockErr] = React.useState(null);
1791
1831
  const [unlocking, setUnlocking] = React.useState(false);
1792
1832
  const [showForgotGuide, setShowForgotGuide] = React.useState(false);
1833
+ const authRpcCall = React.useCallback((endpoint, payload = {}, signal) => {
1834
+ const enriched = {
1835
+ ...payload,
1836
+ ...adminToken ? { adminToken } : {},
1837
+ ...isLocalhost ? { isLocalhost: true } : {}
1838
+ };
1839
+ return rpcCall(endpoint, enriched, signal);
1840
+ }, [rpcCall, adminToken, isLocalhost]);
1793
1841
  const handleUnlockAdmin = React.useCallback(async (e) => {
1794
1842
  e?.preventDefault?.();
1795
1843
  setUnlocking(true);
@@ -1797,6 +1845,7 @@ function BridgePanel({ rpcCall }) {
1797
1845
  try {
1798
1846
  const res = await rpcCall(BRIDGE_ENDPOINTS.authAdminUnlock, { password: unlockPassword });
1799
1847
  if (res?.ok) {
1848
+ setAdminToken(res.value?.adminToken || "");
1800
1849
  setAdminUnlocked(true);
1801
1850
  setUnlockPassword("");
1802
1851
  } else {
@@ -1808,25 +1857,50 @@ function BridgePanel({ rpcCall }) {
1808
1857
  setUnlocking(false);
1809
1858
  }
1810
1859
  }, [rpcCall, unlockPassword]);
1860
+ const handleLockAdmin = React.useCallback(async () => {
1861
+ try {
1862
+ if (adminToken) {
1863
+ await rpcCall(BRIDGE_ENDPOINTS.authAdminLock, { adminToken });
1864
+ }
1865
+ } catch {
1866
+ }
1867
+ setAdminToken("");
1868
+ setAdminUnlocked(false);
1869
+ }, [rpcCall, adminToken]);
1870
+ const loadInFlightRef = React.useRef(false);
1871
+ const loadSeqRef = React.useRef(0);
1811
1872
  const load = React.useCallback(async (quiet = false) => {
1873
+ if (loadInFlightRef.current) return;
1874
+ loadInFlightRef.current = true;
1875
+ const currentSeq = ++loadSeqRef.current;
1812
1876
  try {
1813
- const r = await rpcCall(BRIDGE_ENDPOINTS.getStatus, {});
1877
+ const r = await authRpcCall(BRIDGE_ENDPOINTS.getStatus, {});
1878
+ if (currentSeq !== loadSeqRef.current) return;
1814
1879
  if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
1815
1880
  setStatus(r.value);
1816
1881
  if (!quiet) setErr(null);
1817
1882
  } catch (e) {
1818
- setErr(e.message);
1883
+ if (currentSeq === loadSeqRef.current) setErr(e.message);
1884
+ } finally {
1885
+ loadInFlightRef.current = false;
1819
1886
  }
1820
- }, [rpcCall]);
1887
+ }, [authRpcCall]);
1888
+ const pollPlatformsSeqRef = React.useRef(0);
1821
1889
  React.useEffect(() => {
1822
1890
  let alive = true;
1891
+ let inFlight = false;
1823
1892
  const poll = async () => {
1893
+ if (inFlight || !alive) return;
1894
+ inFlight = true;
1895
+ const currentSeq = ++pollPlatformsSeqRef.current;
1824
1896
  try {
1825
- const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1826
- if (alive && r?.ok) {
1897
+ const r = await authRpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1898
+ if (alive && currentSeq === pollPlatformsSeqRef.current && r?.ok) {
1827
1899
  setPlatforms(r.value ?? {});
1828
1900
  }
1829
1901
  } catch {
1902
+ } finally {
1903
+ inFlight = false;
1830
1904
  }
1831
1905
  };
1832
1906
  poll();
@@ -1835,7 +1909,7 @@ function BridgePanel({ rpcCall }) {
1835
1909
  alive = false;
1836
1910
  clearInterval(t);
1837
1911
  };
1838
- }, [rpcCall]);
1912
+ }, [authRpcCall]);
1839
1913
  React.useEffect(() => {
1840
1914
  load();
1841
1915
  const t = setInterval(() => load(true), 3e3);
@@ -1843,14 +1917,14 @@ function BridgePanel({ rpcCall }) {
1843
1917
  }, [load]);
1844
1918
  const act = React.useCallback(async (endpoint, payload) => {
1845
1919
  try {
1846
- const r = await rpcCall(endpoint, payload ?? {});
1920
+ const r = await authRpcCall(endpoint, payload ?? {});
1847
1921
  if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
1848
1922
  setStatus(r.value);
1849
1923
  setErr(null);
1850
1924
  } catch (e) {
1851
1925
  setErr(e.message);
1852
1926
  }
1853
- }, [rpcCall]);
1927
+ }, [authRpcCall]);
1854
1928
  const onStartCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCloudflared), [act]);
1855
1929
  const onStopCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCloudflared), [act]);
1856
1930
  const onResetCloudflared = React.useCallback(
@@ -1935,7 +2009,7 @@ function BridgePanel({ rpcCall }) {
1935
2009
  } else if (activeTab === "security") {
1936
2010
  tabContent = React.createElement(AccessAuthCard, {
1937
2011
  auth: status?.auth,
1938
- rpcCall,
2012
+ rpcCall: authRpcCall,
1939
2013
  onUpdate: () => load(true)
1940
2014
  });
1941
2015
  } else if (activeTab === "im") {
@@ -1998,12 +2072,13 @@ function BridgePanel({ rpcCall }) {
1998
2072
  );
1999
2073
  })
2000
2074
  ),
2001
- // 显示选中的平台卡片
2075
+ // 显示选中的平台卡片(带有 key 保证切换时重置表单状态)
2002
2076
  selectedPlatform && platforms?.[selectedPlatform] && React.createElement(PlatformCard, {
2077
+ key: selectedPlatform,
2003
2078
  platformId: selectedPlatform,
2004
2079
  platformName: IM_PLATFORMS.find((p) => p.id === selectedPlatform)?.label ?? selectedPlatform,
2005
2080
  platformDesc: IM_PLATFORMS.find((p) => p.id === selectedPlatform)?.desc ?? "",
2006
- rpcCall,
2081
+ rpcCall: authRpcCall,
2007
2082
  onStatusChange: () => {
2008
2083
  }
2009
2084
  // 状态变化已由 listPlatforms 轮询处理,不需要回调
@@ -2154,10 +2229,10 @@ function BridgePanel({ rpcCall }) {
2154
2229
  React.createElement("span", null, "\u{1F513} \u7BA1\u7406\u5458\u6743\u9650\u5DF2\u89E3\u9501\uFF08\u5F53\u524D\u4E34\u65F6\u4F1A\u8BDD\u6709\u6548\uFF09"),
2155
2230
  React.createElement("button", {
2156
2231
  style: { ...s.btnGhost, height: 24, fontSize: 11, padding: "0 8px" },
2157
- onClick: () => setAdminUnlocked(false)
2232
+ onClick: handleLockAdmin
2158
2233
  }, "\u{1F512} \u91CD\u65B0\u9501\u5B9A\u540E\u53F0")
2159
2234
  ),
2160
- React.createElement(VersionBanner, { rpcCall }),
2235
+ React.createElement(VersionBanner, { rpcCall: authRpcCall }),
2161
2236
  React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
2162
2237
  tabContent
2163
2238
  );
package/client/index.js CHANGED
@@ -270,18 +270,23 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
270
270
  }, [initUrl, initToken]);
271
271
 
272
272
  const dirty = serverUrl !== (initUrl ?? '') || accessToken !== (initToken ?? '');
273
+ const [saveErr, setSaveErr] = React.useState(null);
273
274
  const handleSave = React.useCallback(async () => {
274
275
  setSaving(true);
275
276
  setSaveSuccess(false);
277
+ setSaveErr(null);
276
278
  try {
277
279
  await onSave(serverUrl, accessToken);
278
280
  setSaveSuccess(true);
279
281
  setTimeout(() => setSaveSuccess(false), 2500);
282
+ } catch (e) {
283
+ setSaveErr(e.message || '保存配置失败');
284
+ } finally {
285
+ setSaving(false);
280
286
  }
281
- finally { setSaving(false); }
282
287
  }, [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); }, []);
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); }, []);
285
290
 
286
291
  return React.createElement('div', { style: s.block },
287
292
  React.createElement('div', { style: { ...s.muted, marginBottom: 8 } }, '隧道服务器配置'),
@@ -303,6 +308,7 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
303
308
  onKeyDown: (e) => { if (e.key === 'Enter' && dirty && !saving) handleSave(); },
304
309
  disabled: saving,
305
310
  }),
311
+ saveErr && React.createElement('div', { style: s.err }, `❌ ${saveErr}`),
306
312
  React.createElement('div', { style: { ...s.muted, fontSize: 11 } },
307
313
  '💡 用于与您的 VPS 隧道服务端建立反向通道(与 Web 网页访客访问密码互相独立)。'
308
314
  ),
@@ -388,46 +394,58 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
388
394
  }, [auth]);
389
395
 
390
396
  const handleToggleEnabled = async () => {
397
+ const prev = enabled;
391
398
  const next = !enabled;
392
399
  setEnabled(next);
393
400
  try {
394
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
401
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
402
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
395
403
  setTopMsg({ ok: true, text: next ? '✓ 访问安全认证已开启(现有登录态已刷新)' : '✓ 访问安全认证已关闭' });
396
404
  onUpdate?.();
397
405
  } catch (e) {
406
+ setEnabled(prev);
398
407
  setTopMsg({ ok: false, text: e.message || '更新失败' });
399
408
  }
400
409
  };
401
410
 
402
411
  const handleChangeMode = async (m) => {
412
+ const prev = mode;
403
413
  setMode(m);
404
414
  try {
405
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
415
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
416
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
406
417
  setTopMsg({ ok: true, text: '✓ 外部验证模式已切换,已刷新全域登录态' });
407
418
  onUpdate?.();
408
419
  } catch (e) {
420
+ setMode(prev);
409
421
  setTopMsg({ ok: false, text: e.message || '更新失败' });
410
422
  }
411
423
  };
412
424
 
413
425
  const handleChangeScope = async (sc) => {
426
+ const prev = scope;
414
427
  setScope(sc);
415
428
  try {
416
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
429
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
430
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
417
431
  setTopMsg({ ok: true, text: '✓ 防护生效范围已更新' });
418
432
  onUpdate?.();
419
433
  } catch (e) {
434
+ setScope(prev);
420
435
  setTopMsg({ ok: false, text: e.message || '更新失败' });
421
436
  }
422
437
  };
423
438
 
424
439
  const handleChangeAdminPolicy = async (pol) => {
440
+ const prev = adminPolicy;
425
441
  setAdminPolicy(pol);
426
442
  try {
427
- await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
443
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
444
+ if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
428
445
  setTopMsg({ ok: true, text: '✓ 远程管理防篡改策略已更新' });
429
446
  onUpdate?.();
430
447
  } catch (e) {
448
+ setAdminPolicy(prev);
431
449
  setTopMsg({ ok: false, text: e.message || '更新失败' });
432
450
  }
433
451
  };
@@ -479,7 +497,8 @@ const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpd
479
497
  const handleRegenerateToken = async () => {
480
498
  if (!confirm('重置后,之前包含旧 Token 的二维码和分享链接将立即失效。是否确认重置?')) return;
481
499
  try {
482
- await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
500
+ const res = await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
501
+ if (!res?.ok) throw new Error(res?.error?.message || '重置失败');
483
502
  setTopMsg({ ok: true, text: '✓ 安全 Token 已重置,二维码与专属链接已刷新' });
484
503
  onUpdate?.();
485
504
  } catch (e) {
@@ -804,16 +823,24 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
804
823
  onStatusChange?.(connected);
805
824
  }, [platform?.status, onStatusChange]);
806
825
 
826
+ const loadInFlightRef = React.useRef(false);
827
+ const seqRef = React.useRef(0);
807
828
  const load = React.useCallback(async (quiet = false) => {
829
+ if (loadInFlightRef.current) return;
830
+ loadInFlightRef.current = true;
831
+ const currentSeq = ++seqRef.current;
808
832
  try {
809
833
  // 用通用端点读取平台状态(不执行登录操作,只获取状态)
810
834
  const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
835
+ if (currentSeq !== seqRef.current) return; // 丢弃过时响应
811
836
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
812
837
  const allPlatforms = r.value ?? {};
813
838
  setPlatform(allPlatforms[platformId] ?? null);
814
839
  if (!quiet) setErr(null);
815
840
  } catch (e) {
816
- if (!quiet) setErr(e.message);
841
+ if (currentSeq === seqRef.current && !quiet) setErr(e.message);
842
+ } finally {
843
+ loadInFlightRef.current = false;
817
844
  }
818
845
  }, [rpcCall, platformId]);
819
846
 
@@ -850,9 +877,20 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
850
877
  const id = newId.trim();
851
878
  if (!id) return;
852
879
  const list = [...(platform?.allowFrom ?? []), id];
853
- await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
854
- setNewId('');
855
- }, [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]);
856
894
  const removeAllow = React.useCallback(async (id) => {
857
895
  const list = (platform?.allowFrom ?? []).filter((x) => x !== id);
858
896
  await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
@@ -922,7 +960,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
922
960
  );
923
961
  }
924
962
 
925
- const connected = platform?.status === 'connected' || platform?.status === 'starting';
963
+ const connected = platform?.status === 'connected' || platform?.status === 'starting' || platform?.status === 'reconnecting';
926
964
  const login = platform?.login ?? {};
927
965
  const showQr = login.phase === 'qr' || login.phase === 'scaned';
928
966
  const statusLabel = platform?.status === 'connected' ? '已连接'
@@ -1580,12 +1618,22 @@ function BridgePanel({ rpcCall }) {
1580
1618
  window.location.protocol === 'app:' ||
1581
1619
  window.location.hostname.endsWith('.local')
1582
1620
  );
1621
+ const [adminToken, setAdminToken] = React.useState('');
1583
1622
  const [adminUnlocked, setAdminUnlocked] = React.useState(false);
1584
1623
  const [unlockPassword, setUnlockPassword] = React.useState('');
1585
- const [unlockErr, setUnlockErr] = React.useState(null);
1586
- const [unlocking, setUnlocking] = React.useState(false);
1624
+ const [unlockErr, setUnlockErr] = React.useState(null);
1625
+ const [unlocking, setUnlocking] = React.useState(false);
1587
1626
  const [showForgotGuide, setShowForgotGuide] = React.useState(false);
1588
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
+
1589
1637
  const handleUnlockAdmin = React.useCallback(async (e) => {
1590
1638
  e?.preventDefault?.();
1591
1639
  setUnlocking(true);
@@ -1593,6 +1641,7 @@ function BridgePanel({ rpcCall }) {
1593
1641
  try {
1594
1642
  const res = await rpcCall(BRIDGE_ENDPOINTS.authAdminUnlock, { password: unlockPassword });
1595
1643
  if (res?.ok) {
1644
+ setAdminToken(res.value?.adminToken || '');
1596
1645
  setAdminUnlocked(true);
1597
1646
  setUnlockPassword('');
1598
1647
  } else {
@@ -1605,32 +1654,58 @@ function BridgePanel({ rpcCall }) {
1605
1654
  }
1606
1655
  }, [rpcCall, unlockPassword]);
1607
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);
1608
1669
  const load = React.useCallback(async (quiet = false) => {
1670
+ if (loadInFlightRef.current) return;
1671
+ loadInFlightRef.current = true;
1672
+ const currentSeq = ++loadSeqRef.current;
1609
1673
  try {
1610
- const r = await rpcCall(BRIDGE_ENDPOINTS.getStatus, {});
1674
+ const r = await authRpcCall(BRIDGE_ENDPOINTS.getStatus, {});
1675
+ if (currentSeq !== loadSeqRef.current) return;
1611
1676
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
1612
1677
  setStatus(r.value);
1613
1678
  if (!quiet) setErr(null);
1614
1679
  } catch (e) {
1615
- setErr(e.message);
1680
+ if (currentSeq === loadSeqRef.current) setErr(e.message);
1681
+ } finally {
1682
+ loadInFlightRef.current = false;
1616
1683
  }
1617
- }, [rpcCall]);
1684
+ }, [authRpcCall]);
1618
1685
 
1619
- // 独立轮询所有平台状态(Tab 未选中时也能更新)
1686
+ // 独立轮询所有平台状态(Tab 未选中时也能更新),带 in-flight 锁与序列号防乱序
1687
+ const pollPlatformsSeqRef = React.useRef(0);
1620
1688
  React.useEffect(() => {
1621
1689
  let alive = true;
1690
+ let inFlight = false;
1622
1691
  const poll = async () => {
1692
+ if (inFlight || !alive) return;
1693
+ inFlight = true;
1694
+ const currentSeq = ++pollPlatformsSeqRef.current;
1623
1695
  try {
1624
- const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1625
- if (alive && r?.ok) {
1696
+ const r = await authRpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
1697
+ if (alive && currentSeq === pollPlatformsSeqRef.current && r?.ok) {
1626
1698
  setPlatforms(r.value ?? {});
1627
1699
  }
1628
1700
  } catch { /* 忽略,不影响主面板 */ }
1701
+ finally {
1702
+ inFlight = false;
1703
+ }
1629
1704
  };
1630
1705
  poll();
1631
1706
  const t = setInterval(poll, 4000);
1632
1707
  return () => { alive = false; clearInterval(t); };
1633
- }, [rpcCall]);
1708
+ }, [authRpcCall]);
1634
1709
 
1635
1710
  React.useEffect(() => {
1636
1711
  load();
@@ -1640,14 +1715,14 @@ function BridgePanel({ rpcCall }) {
1640
1715
 
1641
1716
  const act = React.useCallback(async (endpoint, payload) => {
1642
1717
  try {
1643
- const r = await rpcCall(endpoint, payload ?? {});
1718
+ const r = await authRpcCall(endpoint, payload ?? {});
1644
1719
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
1645
1720
  setStatus(r.value);
1646
1721
  setErr(null);
1647
1722
  } catch (e) {
1648
1723
  setErr(e.message);
1649
1724
  }
1650
- }, [rpcCall]);
1725
+ }, [authRpcCall]);
1651
1726
 
1652
1727
  const onStartCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCloudflared), [act]);
1653
1728
  const onStopCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCloudflared), [act]);
@@ -1734,7 +1809,7 @@ function BridgePanel({ rpcCall }) {
1734
1809
  } else if (activeTab === 'security') {
1735
1810
  tabContent = React.createElement(AccessAuthCard, {
1736
1811
  auth: status?.auth,
1737
- rpcCall,
1812
+ rpcCall: authRpcCall,
1738
1813
  onUpdate: () => load(true),
1739
1814
  });
1740
1815
  } else if (activeTab === 'im') {
@@ -1790,12 +1865,13 @@ function BridgePanel({ rpcCall }) {
1790
1865
  );
1791
1866
  }),
1792
1867
  ),
1793
- // 显示选中的平台卡片
1868
+ // 显示选中的平台卡片(带有 key 保证切换时重置表单状态)
1794
1869
  selectedPlatform && platforms?.[selectedPlatform] && React.createElement(PlatformCard, {
1870
+ key: selectedPlatform,
1795
1871
  platformId: selectedPlatform,
1796
1872
  platformName: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.label ?? selectedPlatform,
1797
1873
  platformDesc: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.desc ?? '',
1798
- rpcCall,
1874
+ rpcCall: authRpcCall,
1799
1875
  onStatusChange: () => {}, // 状态变化已由 listPlatforms 轮询处理,不需要回调
1800
1876
  }),
1801
1877
  );
@@ -1907,11 +1983,11 @@ function BridgePanel({ rpcCall }) {
1907
1983
  React.createElement('span', null, '🔓 管理员权限已解锁(当前临时会话有效)'),
1908
1984
  React.createElement('button', {
1909
1985
  style: { ...s.btnGhost, height: 24, fontSize: 11, padding: '0 8px' },
1910
- onClick: () => setAdminUnlocked(false),
1986
+ onClick: handleLockAdmin,
1911
1987
  }, '🔒 重新锁定后台'),
1912
1988
  ),
1913
1989
 
1914
- React.createElement(VersionBanner, { rpcCall }),
1990
+ React.createElement(VersionBanner, { rpcCall: authRpcCall }),
1915
1991
 
1916
1992
  React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
1917
1993