@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 +1 -1
- package/README.md +1 -1
- package/client/client.js +98 -23
- package/client/index.js +105 -29
- package/docs/platform-abstraction-design.md +473 -0
- package/docs/screenshots/qq-chat.jpg +0 -0
- package/docs/screenshots/qq-group.jpg +0 -0
- package/docs/screenshots/qr-scan.jpg +0 -0
- package/docs/screenshots/wechat-chat.jpg +0 -0
- package/docs/wechat-bot-plan.md +294 -0
- package/lib/auth/manager.js +63 -18
- package/lib/bridge-rpc.js +95 -8
- package/lib/feishu/node.js +4 -4
- package/lib/index.js +59 -22
- package/lib/qq/node.js +9 -2
- package/lib/telegram/node.js +4 -4
- package/lib/tunnel-client.mjs +11 -2
- package/lib/wechat/media.js +8 -2
- package/lib/wechat/node.js +2 -2
- package/package.json +6 -9
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
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
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:
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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]
|
|
1586
|
-
const [unlocking, setUnlocking]
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
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:
|
|
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
|
|