@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/README.en.md +122 -19
- package/README.md +122 -21
- package/client/client.js +308 -28
- package/client/index.js +283 -32
- package/docs/banner.jpg +0 -0
- package/docs/feishu-usage.md +5 -0
- package/docs/platform-abstraction-design.md +473 -0
- package/docs/screenshots/admin-lock-screen.jpg +0 -0
- package/docs/screenshots/feishu-bot-config.jpg +0 -0
- package/docs/screenshots/feishu-chat.jpg +0 -0
- package/docs/screenshots/lan-access.jpg +0 -0
- package/docs/screenshots/qq-bot-config.jpg +0 -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/remote-auth-login.jpg +0 -0
- package/docs/screenshots/remote-web-mobile.jpg +0 -0
- package/docs/screenshots/security-auth-config.jpg +0 -0
- package/docs/screenshots/telegram-bot-config.jpg +0 -0
- package/docs/screenshots/tunnel-access.jpg +0 -0
- package/docs/screenshots/wechat-bot-config.jpg +0 -0
- package/docs/screenshots/wechat-chat.jpg +0 -0
- package/docs/telegram-usage.md +2 -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 +91 -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 +11 -12
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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]
|
|
1586
|
-
const [unlocking, setUnlocking]
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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: {
|
|
1897
|
-
|
|
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:
|
|
2082
|
+
onClick: handleLockAdmin,
|
|
1911
2083
|
}, '🔒 重新锁定后台'),
|
|
1912
2084
|
),
|
|
1913
2085
|
|
|
1914
|
-
|
|
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
|
package/docs/feishu-usage.md
CHANGED
|
@@ -69,6 +69,8 @@
|
|
|
69
69
|
3. 点击 **「保存并连接」**;
|
|
70
70
|
4. 状态显示为绿色 **「已连接」** 即表示长连接成功建立!
|
|
71
71
|
|
|
72
|
+

|
|
73
|
+
|
|
72
74
|
---
|
|
73
75
|
|
|
74
76
|
## 💬 常用操作与指令
|
|
@@ -91,3 +93,6 @@
|
|
|
91
93
|
当 Agent 尝试执行需授权的操作(如终端命令、写敏感文件)时,飞书端会自动推送 **原生交互卡片**:
|
|
92
94
|
- 点击卡片上的 **「✓ 批准执行」** 或 **「✕ 拒绝执行」** 按钮即可一键处理;
|
|
93
95
|
- 亦可直接回复文字 `/yes` (或 `1`) / `/no` (或 `2`)。
|
|
96
|
+
|
|
97
|
+

|
|
98
|
+
|