@wenbin_wb/dsh-bridge 2.4.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 +46 -9
- package/README.md +46 -9
- package/client/client.js +821 -28
- package/client/index.js +711 -35
- 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/login-template.js +378 -0
- package/lib/auth/manager.js +470 -0
- package/lib/bridge-rpc-constants.js +6 -0
- package/lib/bridge-rpc.js +126 -6
- package/lib/feishu/node.js +4 -4
- package/lib/index.js +258 -32
- 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/client/index.js
CHANGED
|
@@ -72,6 +72,9 @@ const Icons = {
|
|
|
72
72
|
tunnel: (props) => React.createElement('svg', { viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', ...props },
|
|
73
73
|
React.createElement('path', { d: 'M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3z' })
|
|
74
74
|
),
|
|
75
|
+
security: (props) => React.createElement('svg', { viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', ...props },
|
|
76
|
+
React.createElement('path', { d: 'M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z' })
|
|
77
|
+
),
|
|
75
78
|
bot: (props) => React.createElement('svg', { viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', ...props },
|
|
76
79
|
React.createElement('path', { d: 'M20 9V7c0-1.1-.9-2-2-2h-3c0-1.66-1.34-3-3-3S9 3.34 9 5H6c-1.1 0-2 .9-2 2v2c-1.66 0-3 1.34-3 3s1.34 3 3 3v4c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-4c1.66 0 3-1.34 3-3s-1.34-3-3-3zm-2 10H6V7h12v12zm-9-6c-.83 0-1.5-.67-1.5-1.5S8.17 10 9 10s1.5.67 1.5 1.5S9.83 13 9 13zm6 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z' })
|
|
77
80
|
),
|
|
@@ -154,7 +157,7 @@ function StatusTag({ running, status }) {
|
|
|
154
157
|
}, text);
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
function QrBlock({ url, qr, onReset }) {
|
|
160
|
+
function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
|
|
158
161
|
const [copied, setCopied] = React.useState(false);
|
|
159
162
|
const [showQr, setShowQr] = React.useState(true);
|
|
160
163
|
|
|
@@ -191,10 +194,31 @@ function QrBlock({ url, qr, onReset }) {
|
|
|
191
194
|
const toggleQr = React.useCallback(() => setShowQr(v => !v), []);
|
|
192
195
|
|
|
193
196
|
return React.createElement('div', { style: { marginTop: 10 } },
|
|
194
|
-
|
|
195
|
-
'
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
auth?.enabled
|
|
198
|
+
? React.createElement('div', {
|
|
199
|
+
style: {
|
|
200
|
+
display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 10px',
|
|
201
|
+
background: 'var(--dsw-alias-state-success-bg,#ecfdf5)',
|
|
202
|
+
border: '1px solid var(--dsw-alias-state-success-primary,#10b981)',
|
|
203
|
+
borderRadius: 8, fontSize: 12, color: 'var(--dsw-alias-state-success-primary,#059669)',
|
|
204
|
+
marginBottom: 8, fontWeight: 500,
|
|
205
|
+
cursor: onNavigateSecurity ? 'pointer' : 'default',
|
|
206
|
+
},
|
|
207
|
+
onClick: onNavigateSecurity,
|
|
208
|
+
title: onNavigateSecurity ? '点击前往「安全认证」配置' : undefined,
|
|
209
|
+
},
|
|
210
|
+
'🛡️ 访问安全认证已生效 · 扫码设备自动完成免密授权',
|
|
211
|
+
onNavigateSecurity && React.createElement('span', { style: { textDecoration: 'underline', fontSize: 11, marginLeft: 4 } }, '设置 ➔')
|
|
212
|
+
)
|
|
213
|
+
: React.createElement('div', {
|
|
214
|
+
style: { ...s.warn, cursor: onNavigateSecurity ? 'pointer' : 'default', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 6 },
|
|
215
|
+
onClick: onNavigateSecurity,
|
|
216
|
+
title: onNavigateSecurity ? '点击前往「安全认证」开启访问保护' : undefined,
|
|
217
|
+
},
|
|
218
|
+
React.createElement('span', null, '⚠️ 当前未开启访问认证,建议在「安全认证」开启密码或扫码保护。'),
|
|
219
|
+
onNavigateSecurity && React.createElement('span', { style: { fontWeight: 600, textDecoration: 'underline', fontSize: 12, color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } }, '去开启 ➔')
|
|
220
|
+
),
|
|
221
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 } },
|
|
198
222
|
React.createElement('code', { style: { ...s.code, flex: 1 } }, url),
|
|
199
223
|
React.createElement('button', {
|
|
200
224
|
style: { ...s.btnGhost, height: 26, padding: '0 10px', fontSize: 12, flexShrink: 0 },
|
|
@@ -246,21 +270,26 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
|
|
|
246
270
|
}, [initUrl, initToken]);
|
|
247
271
|
|
|
248
272
|
const dirty = serverUrl !== (initUrl ?? '') || accessToken !== (initToken ?? '');
|
|
273
|
+
const [saveErr, setSaveErr] = React.useState(null);
|
|
249
274
|
const handleSave = React.useCallback(async () => {
|
|
250
275
|
setSaving(true);
|
|
251
276
|
setSaveSuccess(false);
|
|
277
|
+
setSaveErr(null);
|
|
252
278
|
try {
|
|
253
279
|
await onSave(serverUrl, accessToken);
|
|
254
280
|
setSaveSuccess(true);
|
|
255
281
|
setTimeout(() => setSaveSuccess(false), 2500);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
setSaveErr(e.message || '保存配置失败');
|
|
284
|
+
} finally {
|
|
285
|
+
setSaving(false);
|
|
256
286
|
}
|
|
257
|
-
finally { setSaving(false); }
|
|
258
287
|
}, [onSave, serverUrl, accessToken]);
|
|
259
|
-
const handleUrlChange = React.useCallback((e) => { setServerUrl(e.target.value); setSaveSuccess(false); }, []);
|
|
260
|
-
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); }, []);
|
|
261
290
|
|
|
262
291
|
return React.createElement('div', { style: s.block },
|
|
263
|
-
React.createElement('div', { style: { ...s.muted, marginBottom: 8 } }, '
|
|
292
|
+
React.createElement('div', { style: { ...s.muted, marginBottom: 8 } }, '隧道服务器配置'),
|
|
264
293
|
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
265
294
|
React.createElement('input', {
|
|
266
295
|
style: s.input,
|
|
@@ -273,12 +302,16 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
|
|
|
273
302
|
React.createElement('input', {
|
|
274
303
|
style: s.input,
|
|
275
304
|
type: 'password',
|
|
276
|
-
placeholder: '
|
|
305
|
+
placeholder: '隧道服务端连接令牌(Tunnel Access Token)',
|
|
277
306
|
value: accessToken,
|
|
278
307
|
onChange: handleTokenChange,
|
|
279
308
|
onKeyDown: (e) => { if (e.key === 'Enter' && dirty && !saving) handleSave(); },
|
|
280
309
|
disabled: saving,
|
|
281
310
|
}),
|
|
311
|
+
saveErr && React.createElement('div', { style: s.err }, `❌ ${saveErr}`),
|
|
312
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11 } },
|
|
313
|
+
'💡 用于与您的 VPS 隧道服务端建立反向通道(与 Web 网页访客访问密码互相独立)。'
|
|
314
|
+
),
|
|
282
315
|
React.createElement('button', {
|
|
283
316
|
style: {
|
|
284
317
|
...s.btnPri,
|
|
@@ -293,7 +326,7 @@ const CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm({ serv
|
|
|
293
326
|
);
|
|
294
327
|
});
|
|
295
328
|
|
|
296
|
-
const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart, onStop, onReset, children }) {
|
|
329
|
+
const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart, onStop, onReset, auth, onNavigateSecurity, children }) {
|
|
297
330
|
const { running, configured, url, qr, state } = data ?? {};
|
|
298
331
|
const phase = state?.phase ?? 'idle';
|
|
299
332
|
|
|
@@ -312,7 +345,7 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
|
|
|
312
345
|
color: phase === 'error' ? 'var(--dsw-alias-state-error-primary,#dc2626)' : 'var(--dsw-alias-label-secondary,#6b7280)',
|
|
313
346
|
},
|
|
314
347
|
}, state?.detail ?? phase),
|
|
315
|
-
url && React.createElement(QrBlock, { url, qr, onReset }),
|
|
348
|
+
url && React.createElement(QrBlock, { url, qr, onReset, auth, onNavigateSecurity }),
|
|
316
349
|
(onStart || onStop) && React.createElement('div', {
|
|
317
350
|
style: { ...s.block, display: 'flex', gap: 8, flexWrap: 'wrap' },
|
|
318
351
|
},
|
|
@@ -327,6 +360,434 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
|
|
|
327
360
|
);
|
|
328
361
|
});
|
|
329
362
|
|
|
363
|
+
// ---- 访问安全认证卡片 ----
|
|
364
|
+
|
|
365
|
+
const AccessAuthCard = React.memo(function AccessAuthCard({ auth, rpcCall, onUpdate }) {
|
|
366
|
+
const [enabled, setEnabled] = React.useState(auth?.enabled ?? false);
|
|
367
|
+
const [mode, setMode] = React.useState(auth?.mode ?? 'token_and_password');
|
|
368
|
+
const [scope, setScope] = React.useState(auth?.scope ?? 'all');
|
|
369
|
+
const [adminPolicy, setAdminPolicy] = React.useState(auth?.adminPolicy ?? 'password_unlock');
|
|
370
|
+
|
|
371
|
+
// 1. 访客访问密码状态
|
|
372
|
+
const [accessPassword, setAccessPassword] = React.useState('');
|
|
373
|
+
const [showAccessPassword, setShowAccessPassword] = React.useState(false);
|
|
374
|
+
const [savingAccess, setSavingAccess] = React.useState(false);
|
|
375
|
+
const [saveAccessSuccess, setSaveAccessSuccess] = React.useState(false);
|
|
376
|
+
const [msgAccess, setMsgAccess] = React.useState(null);
|
|
377
|
+
|
|
378
|
+
// 2. 后台管理密码状态
|
|
379
|
+
const [adminPassword, setAdminPassword] = React.useState('');
|
|
380
|
+
const [showAdminPassword, setShowAdminPassword] = React.useState(false);
|
|
381
|
+
const [savingAdmin, setSavingAdmin] = React.useState(false);
|
|
382
|
+
const [saveAdminSuccess, setSaveAdminSuccess] = React.useState(false);
|
|
383
|
+
const [msgAdmin, setMsgAdmin] = React.useState(null);
|
|
384
|
+
|
|
385
|
+
const [topMsg, setTopMsg] = React.useState(null);
|
|
386
|
+
|
|
387
|
+
React.useEffect(() => {
|
|
388
|
+
if (auth) {
|
|
389
|
+
setEnabled(auth.enabled ?? false);
|
|
390
|
+
setMode(auth.mode ?? 'token_and_password');
|
|
391
|
+
setScope(auth.scope ?? 'all');
|
|
392
|
+
setAdminPolicy(auth.adminPolicy ?? 'password_unlock');
|
|
393
|
+
}
|
|
394
|
+
}, [auth]);
|
|
395
|
+
|
|
396
|
+
const handleToggleEnabled = async () => {
|
|
397
|
+
const prev = enabled;
|
|
398
|
+
const next = !enabled;
|
|
399
|
+
setEnabled(next);
|
|
400
|
+
try {
|
|
401
|
+
const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { enabled: next });
|
|
402
|
+
if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
|
|
403
|
+
setTopMsg({ ok: true, text: next ? '✓ 访问安全认证已开启(现有登录态已刷新)' : '✓ 访问安全认证已关闭' });
|
|
404
|
+
onUpdate?.();
|
|
405
|
+
} catch (e) {
|
|
406
|
+
setEnabled(prev);
|
|
407
|
+
setTopMsg({ ok: false, text: e.message || '更新失败' });
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const handleChangeMode = async (m) => {
|
|
412
|
+
const prev = mode;
|
|
413
|
+
setMode(m);
|
|
414
|
+
try {
|
|
415
|
+
const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { mode: m });
|
|
416
|
+
if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
|
|
417
|
+
setTopMsg({ ok: true, text: '✓ 外部验证模式已切换,已刷新全域登录态' });
|
|
418
|
+
onUpdate?.();
|
|
419
|
+
} catch (e) {
|
|
420
|
+
setMode(prev);
|
|
421
|
+
setTopMsg({ ok: false, text: e.message || '更新失败' });
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const handleChangeScope = async (sc) => {
|
|
426
|
+
const prev = scope;
|
|
427
|
+
setScope(sc);
|
|
428
|
+
try {
|
|
429
|
+
const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { scope: sc });
|
|
430
|
+
if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
|
|
431
|
+
setTopMsg({ ok: true, text: '✓ 防护生效范围已更新' });
|
|
432
|
+
onUpdate?.();
|
|
433
|
+
} catch (e) {
|
|
434
|
+
setScope(prev);
|
|
435
|
+
setTopMsg({ ok: false, text: e.message || '更新失败' });
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const handleChangeAdminPolicy = async (pol) => {
|
|
440
|
+
const prev = adminPolicy;
|
|
441
|
+
setAdminPolicy(pol);
|
|
442
|
+
try {
|
|
443
|
+
const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPolicy: pol });
|
|
444
|
+
if (!res?.ok) throw new Error(res?.error?.message || '更新失败');
|
|
445
|
+
setTopMsg({ ok: true, text: '✓ 远程管理防篡改策略已更新' });
|
|
446
|
+
onUpdate?.();
|
|
447
|
+
} catch (e) {
|
|
448
|
+
setAdminPolicy(prev);
|
|
449
|
+
setTopMsg({ ok: false, text: e.message || '更新失败' });
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// 保存访客访问密码
|
|
454
|
+
const handleSaveAccessPassword = async () => {
|
|
455
|
+
setSavingAccess(true);
|
|
456
|
+
setMsgAccess(null);
|
|
457
|
+
try {
|
|
458
|
+
const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { password: accessPassword });
|
|
459
|
+
if (res?.ok) {
|
|
460
|
+
setSaveAccessSuccess(true);
|
|
461
|
+
setMsgAccess({ ok: true, text: '✓ 访客访问密码已成功保存!原有的历史访客会话已全部安全刷新。' });
|
|
462
|
+
setAccessPassword('');
|
|
463
|
+
setTimeout(() => setSaveAccessSuccess(false), 3500);
|
|
464
|
+
onUpdate?.();
|
|
465
|
+
} else {
|
|
466
|
+
setMsgAccess({ ok: false, text: res?.error?.message || '保存失败' });
|
|
467
|
+
}
|
|
468
|
+
} catch (e) {
|
|
469
|
+
setMsgAccess({ ok: false, text: e.message || '保存失败' });
|
|
470
|
+
} finally {
|
|
471
|
+
setSavingAccess(false);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// 保存后台管理密码
|
|
476
|
+
const handleSaveAdminPassword = async () => {
|
|
477
|
+
setSavingAdmin(true);
|
|
478
|
+
setMsgAdmin(null);
|
|
479
|
+
try {
|
|
480
|
+
const res = await rpcCall(BRIDGE_ENDPOINTS.authUpdateConfig, { adminPassword });
|
|
481
|
+
if (res?.ok) {
|
|
482
|
+
setSaveAdminSuccess(true);
|
|
483
|
+
setMsgAdmin({ ok: true, text: '✓ 后台管理密码已成功保存!远程管理解锁状态已重置生效。' });
|
|
484
|
+
setAdminPassword('');
|
|
485
|
+
setTimeout(() => setSaveAdminSuccess(false), 3500);
|
|
486
|
+
onUpdate?.();
|
|
487
|
+
} else {
|
|
488
|
+
setMsgAdmin({ ok: false, text: res?.error?.message || '保存失败' });
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
setMsgAdmin({ ok: false, text: e.message || '保存失败' });
|
|
492
|
+
} finally {
|
|
493
|
+
setSavingAdmin(false);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const handleRegenerateToken = async () => {
|
|
498
|
+
if (!confirm('重置后,之前包含旧 Token 的二维码和分享链接将立即失效。是否确认重置?')) return;
|
|
499
|
+
try {
|
|
500
|
+
const res = await rpcCall(BRIDGE_ENDPOINTS.authRegenerateToken, {});
|
|
501
|
+
if (!res?.ok) throw new Error(res?.error?.message || '重置失败');
|
|
502
|
+
setTopMsg({ ok: true, text: '✓ 安全 Token 已重置,二维码与专属链接已刷新' });
|
|
503
|
+
onUpdate?.();
|
|
504
|
+
} catch (e) {
|
|
505
|
+
setTopMsg({ ok: false, text: e.message || '重置失败' });
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const scopeLabel = scope === 'all' ? '全部通道 (局域网+公网)' : scope === 'public_only' ? '仅公网隧道' : '仅局域网';
|
|
510
|
+
const modeLabel = mode === 'token_and_password' ? '扫码免密 + 密码' : mode === 'password_only' ? '仅密码登录' : '仅安全 Token';
|
|
511
|
+
const adminLabel = adminPolicy === 'password_unlock' ? '需密码解锁' : adminPolicy === 'local_only' ? '仅限电脑本机管理' : '宽松模式';
|
|
512
|
+
|
|
513
|
+
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } },
|
|
514
|
+
// ---- 顶部总控与状态概览卡片 ----
|
|
515
|
+
React.createElement('div', { style: s.card },
|
|
516
|
+
React.createElement('div', {
|
|
517
|
+
style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 },
|
|
518
|
+
},
|
|
519
|
+
React.createElement('div', { style: { flex: '1 1 260px' } },
|
|
520
|
+
React.createElement('div', { style: { ...s.label, fontSize: 15, display: 'flex', alignItems: 'center', gap: 8 } },
|
|
521
|
+
'🔐 全局访问安全防护体系',
|
|
522
|
+
),
|
|
523
|
+
React.createElement('div', { style: { ...s.muted, marginTop: 4 } },
|
|
524
|
+
'集成外部访问门禁拦截与管理后台防篡改控制,双重守护远程会话与网络配置安全',
|
|
525
|
+
),
|
|
526
|
+
),
|
|
527
|
+
React.createElement('button', {
|
|
528
|
+
style: { ...(enabled ? s.btnPri : s.btnGhost), whiteSpace: 'nowrap', flexShrink: 0 },
|
|
529
|
+
onClick: handleToggleEnabled,
|
|
530
|
+
}, enabled ? '✓ 已启用安全防护' : '未开启安全防护'),
|
|
531
|
+
),
|
|
532
|
+
|
|
533
|
+
enabled && React.createElement('div', {
|
|
534
|
+
style: {
|
|
535
|
+
display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 14, paddingTop: 12,
|
|
536
|
+
borderTop: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
React.createElement('div', {
|
|
540
|
+
style: {
|
|
541
|
+
display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
|
|
542
|
+
borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
|
|
543
|
+
color: 'var(--dsw-alias-label-secondary,#4b5563)',
|
|
544
|
+
},
|
|
545
|
+
}, '🌐 保护范围: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } }, scopeLabel)),
|
|
546
|
+
React.createElement('div', {
|
|
547
|
+
style: {
|
|
548
|
+
display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
|
|
549
|
+
borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
|
|
550
|
+
color: 'var(--dsw-alias-label-secondary,#4b5563)',
|
|
551
|
+
},
|
|
552
|
+
}, '🔑 外部验证: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } }, modeLabel)),
|
|
553
|
+
React.createElement('div', {
|
|
554
|
+
style: {
|
|
555
|
+
display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px',
|
|
556
|
+
borderRadius: 16, fontSize: 11, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)',
|
|
557
|
+
color: 'var(--dsw-alias-label-secondary,#4b5563)',
|
|
558
|
+
},
|
|
559
|
+
}, '🔒 后台防篡改: ', React.createElement('strong', { style: { color: 'var(--dsw-alias-state-success-primary,#059669)' } }, adminLabel)),
|
|
560
|
+
),
|
|
561
|
+
|
|
562
|
+
topMsg && React.createElement('div', {
|
|
563
|
+
style: {
|
|
564
|
+
marginTop: 12, padding: '8px 12px', borderRadius: 6, fontSize: 12,
|
|
565
|
+
background: topMsg.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
|
|
566
|
+
color: topMsg.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
|
|
567
|
+
},
|
|
568
|
+
}, topMsg.text),
|
|
569
|
+
),
|
|
570
|
+
|
|
571
|
+
enabled && React.createElement(React.Fragment, null,
|
|
572
|
+
// =========================================================================
|
|
573
|
+
// ---- 第一道防线:外部访问门禁(控制谁能进入 Web 界面使用 AI) ----
|
|
574
|
+
// =========================================================================
|
|
575
|
+
React.createElement('div', { style: s.card },
|
|
576
|
+
React.createElement('div', { style: { marginBottom: 14 } },
|
|
577
|
+
React.createElement('div', { style: { ...s.label, fontSize: 14, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
578
|
+
'🛡️ 第一道防线:外部访问门禁(控制谁能使用 AI)',
|
|
579
|
+
),
|
|
580
|
+
React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
|
|
581
|
+
'控制外部设备通过局域网 IP 或公网隧道(Cloudflare/自建隧道)进入 DSH 聊天界面时的身份验证方式',
|
|
582
|
+
),
|
|
583
|
+
),
|
|
584
|
+
|
|
585
|
+
// 验证模式选择
|
|
586
|
+
React.createElement('div', { style: { marginBottom: 16 } },
|
|
587
|
+
React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '验证模式选择'),
|
|
588
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
589
|
+
[
|
|
590
|
+
{ id: 'token_and_password', title: '🟢 扫码免密 + 密码认证 (推荐)', desc: '二维码自带专属 Token 扫码秒进;直接输 IP/公网域名需输密码' },
|
|
591
|
+
{ id: 'password_only', title: '🔑 仅密码 / PIN 码登录', desc: '所有外部访问必须手动输入访问密码方可进入' },
|
|
592
|
+
{ id: 'token_only', title: '🎫 仅安全 Token 免密', desc: '仅持有带安全 Token 的二维码或专属分享链接方可进入' },
|
|
593
|
+
].map(opt => {
|
|
594
|
+
const isSel = mode === opt.id;
|
|
595
|
+
return React.createElement('div', {
|
|
596
|
+
key: opt.id,
|
|
597
|
+
onClick: () => handleChangeMode(opt.id),
|
|
598
|
+
style: {
|
|
599
|
+
flex: '1 1 200px',
|
|
600
|
+
padding: '10px 12px',
|
|
601
|
+
borderRadius: 8,
|
|
602
|
+
border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
|
|
603
|
+
background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
|
|
604
|
+
cursor: 'pointer',
|
|
605
|
+
transition: 'all 0.15s ease',
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
React.createElement('div', { style: { fontSize: 13, fontWeight: isSel ? 600 : 500, color: isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-label-primary,currentColor)' } }, opt.title),
|
|
609
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } }, opt.desc),
|
|
610
|
+
);
|
|
611
|
+
})
|
|
612
|
+
),
|
|
613
|
+
),
|
|
614
|
+
|
|
615
|
+
// 防护生效范围
|
|
616
|
+
React.createElement('div', { style: { marginBottom: 16 } },
|
|
617
|
+
React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '防护生效通道'),
|
|
618
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
619
|
+
[
|
|
620
|
+
{ id: 'all', title: '全部通道防护 (推荐)', desc: '局域网 IP 直连与公网隧道全部受安全保护' },
|
|
621
|
+
{ id: 'public_only', title: '仅公网隧道开启防护', desc: '局域网内设备直接免密直连,公网隧道强制验证' },
|
|
622
|
+
{ id: 'lan_only', title: '仅局域网开启防护', desc: '仅局域网直连需验证,公网隧道不开启' },
|
|
623
|
+
].map(opt => {
|
|
624
|
+
const isSel = scope === opt.id;
|
|
625
|
+
return React.createElement('div', {
|
|
626
|
+
key: opt.id,
|
|
627
|
+
onClick: () => handleChangeScope(opt.id),
|
|
628
|
+
style: {
|
|
629
|
+
flex: '1 1 180px',
|
|
630
|
+
padding: '9px 12px',
|
|
631
|
+
borderRadius: 8,
|
|
632
|
+
border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
|
|
633
|
+
background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
|
|
634
|
+
cursor: 'pointer',
|
|
635
|
+
transition: 'all 0.15s ease',
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
React.createElement('div', { style: { fontSize: 13, fontWeight: isSel ? 600 : 500, color: isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-label-primary,currentColor)' } }, opt.title),
|
|
639
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 3 } }, opt.desc),
|
|
640
|
+
);
|
|
641
|
+
})
|
|
642
|
+
),
|
|
643
|
+
),
|
|
644
|
+
|
|
645
|
+
// 访客访问密码输入框 (当非 token_only 时展示)
|
|
646
|
+
mode !== 'token_only' && React.createElement('div', { style: { marginBottom: 16 } },
|
|
647
|
+
React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } },
|
|
648
|
+
`设置外部访客访问密码 ${auth?.hasPassword ? '(✓ 已设置访客密码)' : '(⚠️ 尚未设置密码,直接输入 IP 将免密)'}`,
|
|
649
|
+
),
|
|
650
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
651
|
+
React.createElement('input', {
|
|
652
|
+
type: showAccessPassword ? 'text' : 'password',
|
|
653
|
+
style: { ...s.input, flex: 1 },
|
|
654
|
+
placeholder: auth?.hasPassword ? '输入新密码以修改(留空保存可清除访客密码)' : '设置外部访客访问密码 / PIN 码',
|
|
655
|
+
value: accessPassword,
|
|
656
|
+
onChange: e => setAccessPassword(e.target.value),
|
|
657
|
+
}),
|
|
658
|
+
React.createElement('button', {
|
|
659
|
+
style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
|
|
660
|
+
onClick: () => setShowAccessPassword(v => !v),
|
|
661
|
+
}, showAccessPassword ? '隐藏' : '显示'),
|
|
662
|
+
React.createElement('button', {
|
|
663
|
+
style: {
|
|
664
|
+
...s.btnPri, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0,
|
|
665
|
+
background: saveAccessSuccess ? '#059669' : 'var(--dsw-alias-brand-primary, #4f6ef7)',
|
|
666
|
+
color: '#ffffff',
|
|
667
|
+
cursor: savingAccess ? 'wait' : 'pointer',
|
|
668
|
+
},
|
|
669
|
+
onClick: handleSaveAccessPassword,
|
|
670
|
+
disabled: savingAccess,
|
|
671
|
+
}, savingAccess ? '保存中…' : saveAccessSuccess ? '✓ 已成功保存!' : '保存访问密码'),
|
|
672
|
+
),
|
|
673
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
|
|
674
|
+
'💡 当外部朋友或同事未通过二维码扫码,而是直接输入 IP 或公网域名访问时,需输入此密码登录。'
|
|
675
|
+
),
|
|
676
|
+
msgAccess && React.createElement('div', {
|
|
677
|
+
style: {
|
|
678
|
+
marginTop: 8, padding: '6px 12px', borderRadius: 6, fontSize: 12,
|
|
679
|
+
background: msgAccess.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
|
|
680
|
+
color: msgAccess.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
|
|
681
|
+
},
|
|
682
|
+
}, msgAccess.text),
|
|
683
|
+
),
|
|
684
|
+
|
|
685
|
+
// 免密 Token 管理
|
|
686
|
+
mode !== 'password_only' && React.createElement('div', { style: s.block },
|
|
687
|
+
React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } }, '免密扫码 Token (专属访问凭据)'),
|
|
688
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
689
|
+
React.createElement('code', { style: { ...s.code, flex: 1, padding: '6px 10px', background: 'var(--dsw-alias-bg-layer-1,#fff)', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)' } },
|
|
690
|
+
auth?.secretToken ? `${auth.secretToken.slice(0, 10)}****************` : '未生成'
|
|
691
|
+
),
|
|
692
|
+
React.createElement('button', {
|
|
693
|
+
style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
|
|
694
|
+
onClick: handleRegenerateToken,
|
|
695
|
+
title: '重新生成 Token,使之前分享的旧二维码和链接立即失效',
|
|
696
|
+
}, '🔄 重置安全 Token'),
|
|
697
|
+
),
|
|
698
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } },
|
|
699
|
+
'💡 控制台生成的局域网与公网二维码已自动嵌入此 Token,手机扫码即可免密进入聊天界面(但不赋予后台管理设置权限)。'
|
|
700
|
+
),
|
|
701
|
+
),
|
|
702
|
+
),
|
|
703
|
+
|
|
704
|
+
// =========================================================================
|
|
705
|
+
// ---- 第二道防线:后台管理防篡改(控制谁能修改本插件所有设置) ----
|
|
706
|
+
// =========================================================================
|
|
707
|
+
React.createElement('div', { style: s.card },
|
|
708
|
+
React.createElement('div', { style: { marginBottom: 14 } },
|
|
709
|
+
React.createElement('div', { style: { ...s.label, fontSize: 14, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
710
|
+
'🔒 第二道防线:管理后台防篡改(控制谁能修改本插件所有设置)',
|
|
711
|
+
),
|
|
712
|
+
React.createElement('div', { style: { ...s.muted, marginTop: 3 } },
|
|
713
|
+
'锁定整个插件设置后台(包含局域网、公网隧道、IM 机器人密钥与安全设置),防止他人随意篡改配置',
|
|
714
|
+
),
|
|
715
|
+
),
|
|
716
|
+
|
|
717
|
+
// 管理员密码设置
|
|
718
|
+
React.createElement('div', { style: { marginBottom: 16 } },
|
|
719
|
+
React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 6, fontSize: 12 } },
|
|
720
|
+
`设置独立管理员密码 ${auth?.hasAdminPassword ? '(✓ 已设置独立管理密码)' : '(未单独设置,默认使用上述访客访问密码)'}`,
|
|
721
|
+
),
|
|
722
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
723
|
+
React.createElement('input', {
|
|
724
|
+
type: showAdminPassword ? 'text' : 'password',
|
|
725
|
+
style: { ...s.input, flex: 1 },
|
|
726
|
+
placeholder: auth?.hasAdminPassword ? '输入新密码以修改(留空保存可清除独立管理密码)' : '设置后台管理解锁密码(建议与访客密码不同)',
|
|
727
|
+
value: adminPassword,
|
|
728
|
+
onChange: e => setAdminPassword(e.target.value),
|
|
729
|
+
}),
|
|
730
|
+
React.createElement('button', {
|
|
731
|
+
style: { ...s.btnGhost, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0 },
|
|
732
|
+
onClick: () => setShowAdminPassword(v => !v),
|
|
733
|
+
}, showAdminPassword ? '隐藏' : '显示'),
|
|
734
|
+
React.createElement('button', {
|
|
735
|
+
style: {
|
|
736
|
+
...s.btnPri, height: 32, fontSize: 12, whiteSpace: 'nowrap', flexShrink: 0,
|
|
737
|
+
background: saveAdminSuccess ? '#059669' : 'var(--dsw-alias-brand-primary, #4f6ef7)',
|
|
738
|
+
color: '#ffffff',
|
|
739
|
+
cursor: savingAdmin ? 'wait' : 'pointer',
|
|
740
|
+
},
|
|
741
|
+
onClick: handleSaveAdminPassword,
|
|
742
|
+
disabled: savingAdmin,
|
|
743
|
+
}, savingAdmin ? '保存中…' : saveAdminSuccess ? '✓ 已成功保存!' : '保存管理密码'),
|
|
744
|
+
),
|
|
745
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4, color: 'var(--dsw-alias-brand-primary,#4f6ef7)' } },
|
|
746
|
+
'🔑 核心作用:用于远程设备进入设置后台时的解锁验证。设置后,即便把访问密码告知他人,他人也无法进入设置后台改配置。'
|
|
747
|
+
),
|
|
748
|
+
msgAdmin && React.createElement('div', {
|
|
749
|
+
style: {
|
|
750
|
+
marginTop: 8, padding: '6px 12px', borderRadius: 6, fontSize: 12,
|
|
751
|
+
background: msgAdmin.ok ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : 'var(--dsw-alias-state-error-bg,#fef2f2)',
|
|
752
|
+
color: msgAdmin.ok ? 'var(--dsw-alias-state-success-primary,#059669)' : 'var(--dsw-alias-state-error-primary,#dc2626)',
|
|
753
|
+
},
|
|
754
|
+
}, msgAdmin.text),
|
|
755
|
+
),
|
|
756
|
+
|
|
757
|
+
// 远程管理权限控制策略
|
|
758
|
+
React.createElement('div', { style: s.block },
|
|
759
|
+
React.createElement('label', { style: { ...s.label, display: 'block', marginBottom: 8, fontSize: 12 } }, '远程设备管理权限策略'),
|
|
760
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } },
|
|
761
|
+
[
|
|
762
|
+
{ id: 'password_unlock', title: '🔒 需密码解锁 (推荐)', desc: '远程手机/外网打开本插件设置时默认全局锁定,输入管理密码解锁后方可使用' },
|
|
763
|
+
{ id: 'local_only', title: '🚫 仅限电脑本机管理 (最严格)', desc: '远程设备彻底锁定整个设置后台,仅允许在 127.0.0.1 电脑本机上操作' },
|
|
764
|
+
{ id: 'open', title: '🔓 宽松模式', desc: '任何已通过第一道防线登录的设备均可直接修改所有配置' },
|
|
765
|
+
].map(opt => {
|
|
766
|
+
const isSel = adminPolicy === opt.id;
|
|
767
|
+
return React.createElement('div', {
|
|
768
|
+
key: opt.id,
|
|
769
|
+
onClick: () => handleChangeAdminPolicy(opt.id),
|
|
770
|
+
style: {
|
|
771
|
+
flex: '1 1 180px',
|
|
772
|
+
padding: '10px 12px',
|
|
773
|
+
borderRadius: 8,
|
|
774
|
+
border: `1px solid ${isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
|
|
775
|
+
background: isSel ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
|
|
776
|
+
cursor: 'pointer',
|
|
777
|
+
transition: 'all 0.15s ease',
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
React.createElement('div', { style: { fontSize: 13, fontWeight: isSel ? 600 : 500, color: isSel ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-label-primary,currentColor)' } }, opt.title),
|
|
781
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 11, marginTop: 4 } }, opt.desc),
|
|
782
|
+
);
|
|
783
|
+
})
|
|
784
|
+
),
|
|
785
|
+
),
|
|
786
|
+
),
|
|
787
|
+
),
|
|
788
|
+
);
|
|
789
|
+
});
|
|
790
|
+
|
|
330
791
|
// ---- 通用 IM 平台卡片 ----
|
|
331
792
|
|
|
332
793
|
function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatusChange }) {
|
|
@@ -362,16 +823,24 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
|
|
|
362
823
|
onStatusChange?.(connected);
|
|
363
824
|
}, [platform?.status, onStatusChange]);
|
|
364
825
|
|
|
826
|
+
const loadInFlightRef = React.useRef(false);
|
|
827
|
+
const seqRef = React.useRef(0);
|
|
365
828
|
const load = React.useCallback(async (quiet = false) => {
|
|
829
|
+
if (loadInFlightRef.current) return;
|
|
830
|
+
loadInFlightRef.current = true;
|
|
831
|
+
const currentSeq = ++seqRef.current;
|
|
366
832
|
try {
|
|
367
833
|
// 用通用端点读取平台状态(不执行登录操作,只获取状态)
|
|
368
834
|
const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
|
|
835
|
+
if (currentSeq !== seqRef.current) return; // 丢弃过时响应
|
|
369
836
|
if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
|
|
370
837
|
const allPlatforms = r.value ?? {};
|
|
371
838
|
setPlatform(allPlatforms[platformId] ?? null);
|
|
372
839
|
if (!quiet) setErr(null);
|
|
373
840
|
} catch (e) {
|
|
374
|
-
if (!quiet) setErr(e.message);
|
|
841
|
+
if (currentSeq === seqRef.current && !quiet) setErr(e.message);
|
|
842
|
+
} finally {
|
|
843
|
+
loadInFlightRef.current = false;
|
|
375
844
|
}
|
|
376
845
|
}, [rpcCall, platformId]);
|
|
377
846
|
|
|
@@ -408,9 +877,20 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
|
|
|
408
877
|
const id = newId.trim();
|
|
409
878
|
if (!id) return;
|
|
410
879
|
const list = [...(platform?.allowFrom ?? []), id];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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]);
|
|
414
894
|
const removeAllow = React.useCallback(async (id) => {
|
|
415
895
|
const list = (platform?.allowFrom ?? []).filter((x) => x !== id);
|
|
416
896
|
await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
|
|
@@ -480,7 +960,7 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
|
|
|
480
960
|
);
|
|
481
961
|
}
|
|
482
962
|
|
|
483
|
-
const connected = platform?.status === 'connected' || platform?.status === 'starting';
|
|
963
|
+
const connected = platform?.status === 'connected' || platform?.status === 'starting' || platform?.status === 'reconnecting';
|
|
484
964
|
const login = platform?.login ?? {};
|
|
485
965
|
const showQr = login.phase === 'qr' || login.phase === 'scaned';
|
|
486
966
|
const statusLabel = platform?.status === 'connected' ? '已连接'
|
|
@@ -1063,9 +1543,10 @@ function VersionBanner({ rpcCall }) {
|
|
|
1063
1543
|
// ---- Tab Bar ----
|
|
1064
1544
|
|
|
1065
1545
|
const TABS = [
|
|
1066
|
-
{ id: 'lan',
|
|
1067
|
-
{ id: 'tunnel',
|
|
1068
|
-
{ id: 'im',
|
|
1546
|
+
{ id: 'lan', label: '局域网', icon: Icons.lan },
|
|
1547
|
+
{ id: 'tunnel', label: '公网隧道', icon: Icons.tunnel },
|
|
1548
|
+
{ id: 'im', label: 'IM 机器人', icon: Icons.bot },
|
|
1549
|
+
{ id: 'security', label: '安全认证', icon: Icons.security },
|
|
1069
1550
|
];
|
|
1070
1551
|
|
|
1071
1552
|
function TabBar({ active, onChange, dots }) {
|
|
@@ -1125,32 +1606,106 @@ function BridgePanel({ rpcCall }) {
|
|
|
1125
1606
|
const [platforms, setPlatforms] = React.useState(null);
|
|
1126
1607
|
const [selectedPlatform, setSelectedPlatform] = React.useState('wechat');
|
|
1127
1608
|
|
|
1609
|
+
// 远程设备管理权限解锁状态
|
|
1610
|
+
const isLocalhost = typeof window === 'undefined' || (
|
|
1611
|
+
!window.location.hostname ||
|
|
1612
|
+
window.location.hostname === '127.0.0.1' ||
|
|
1613
|
+
window.location.hostname === 'localhost' ||
|
|
1614
|
+
window.location.hostname === '::1' ||
|
|
1615
|
+
window.location.hostname === '' ||
|
|
1616
|
+
window.location.protocol === 'file:' ||
|
|
1617
|
+
window.location.protocol === 'vscode-webview:' ||
|
|
1618
|
+
window.location.protocol === 'app:' ||
|
|
1619
|
+
window.location.hostname.endsWith('.local')
|
|
1620
|
+
);
|
|
1621
|
+
const [adminToken, setAdminToken] = React.useState('');
|
|
1622
|
+
const [adminUnlocked, setAdminUnlocked] = React.useState(false);
|
|
1623
|
+
const [unlockPassword, setUnlockPassword] = React.useState('');
|
|
1624
|
+
const [unlockErr, setUnlockErr] = React.useState(null);
|
|
1625
|
+
const [unlocking, setUnlocking] = React.useState(false);
|
|
1626
|
+
const [showForgotGuide, setShowForgotGuide] = React.useState(false);
|
|
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
|
+
|
|
1637
|
+
const handleUnlockAdmin = React.useCallback(async (e) => {
|
|
1638
|
+
e?.preventDefault?.();
|
|
1639
|
+
setUnlocking(true);
|
|
1640
|
+
setUnlockErr(null);
|
|
1641
|
+
try {
|
|
1642
|
+
const res = await rpcCall(BRIDGE_ENDPOINTS.authAdminUnlock, { password: unlockPassword });
|
|
1643
|
+
if (res?.ok) {
|
|
1644
|
+
setAdminToken(res.value?.adminToken || '');
|
|
1645
|
+
setAdminUnlocked(true);
|
|
1646
|
+
setUnlockPassword('');
|
|
1647
|
+
} else {
|
|
1648
|
+
setUnlockErr(res?.error?.message || '管理员密码错误');
|
|
1649
|
+
}
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
setUnlockErr(err.message || '解锁请求失败');
|
|
1652
|
+
} finally {
|
|
1653
|
+
setUnlocking(false);
|
|
1654
|
+
}
|
|
1655
|
+
}, [rpcCall, unlockPassword]);
|
|
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);
|
|
1128
1669
|
const load = React.useCallback(async (quiet = false) => {
|
|
1670
|
+
if (loadInFlightRef.current) return;
|
|
1671
|
+
loadInFlightRef.current = true;
|
|
1672
|
+
const currentSeq = ++loadSeqRef.current;
|
|
1129
1673
|
try {
|
|
1130
|
-
const r = await
|
|
1674
|
+
const r = await authRpcCall(BRIDGE_ENDPOINTS.getStatus, {});
|
|
1675
|
+
if (currentSeq !== loadSeqRef.current) return;
|
|
1131
1676
|
if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
|
|
1132
1677
|
setStatus(r.value);
|
|
1133
1678
|
if (!quiet) setErr(null);
|
|
1134
1679
|
} catch (e) {
|
|
1135
|
-
setErr(e.message);
|
|
1680
|
+
if (currentSeq === loadSeqRef.current) setErr(e.message);
|
|
1681
|
+
} finally {
|
|
1682
|
+
loadInFlightRef.current = false;
|
|
1136
1683
|
}
|
|
1137
|
-
}, [
|
|
1684
|
+
}, [authRpcCall]);
|
|
1138
1685
|
|
|
1139
|
-
// 独立轮询所有平台状态(Tab
|
|
1686
|
+
// 独立轮询所有平台状态(Tab 未选中时也能更新),带 in-flight 锁与序列号防乱序
|
|
1687
|
+
const pollPlatformsSeqRef = React.useRef(0);
|
|
1140
1688
|
React.useEffect(() => {
|
|
1141
1689
|
let alive = true;
|
|
1690
|
+
let inFlight = false;
|
|
1142
1691
|
const poll = async () => {
|
|
1692
|
+
if (inFlight || !alive) return;
|
|
1693
|
+
inFlight = true;
|
|
1694
|
+
const currentSeq = ++pollPlatformsSeqRef.current;
|
|
1143
1695
|
try {
|
|
1144
|
-
const r = await
|
|
1145
|
-
if (alive && r?.ok) {
|
|
1696
|
+
const r = await authRpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
|
|
1697
|
+
if (alive && currentSeq === pollPlatformsSeqRef.current && r?.ok) {
|
|
1146
1698
|
setPlatforms(r.value ?? {});
|
|
1147
1699
|
}
|
|
1148
1700
|
} catch { /* 忽略,不影响主面板 */ }
|
|
1701
|
+
finally {
|
|
1702
|
+
inFlight = false;
|
|
1703
|
+
}
|
|
1149
1704
|
};
|
|
1150
1705
|
poll();
|
|
1151
1706
|
const t = setInterval(poll, 4000);
|
|
1152
1707
|
return () => { alive = false; clearInterval(t); };
|
|
1153
|
-
}, [
|
|
1708
|
+
}, [authRpcCall]);
|
|
1154
1709
|
|
|
1155
1710
|
React.useEffect(() => {
|
|
1156
1711
|
load();
|
|
@@ -1160,14 +1715,14 @@ function BridgePanel({ rpcCall }) {
|
|
|
1160
1715
|
|
|
1161
1716
|
const act = React.useCallback(async (endpoint, payload) => {
|
|
1162
1717
|
try {
|
|
1163
|
-
const r = await
|
|
1718
|
+
const r = await authRpcCall(endpoint, payload ?? {});
|
|
1164
1719
|
if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
|
|
1165
1720
|
setStatus(r.value);
|
|
1166
1721
|
setErr(null);
|
|
1167
1722
|
} catch (e) {
|
|
1168
1723
|
setErr(e.message);
|
|
1169
1724
|
}
|
|
1170
|
-
}, [
|
|
1725
|
+
}, [authRpcCall]);
|
|
1171
1726
|
|
|
1172
1727
|
const onStartCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCloudflared), [act]);
|
|
1173
1728
|
const onStopCloudflared = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCloudflared), [act]);
|
|
@@ -1180,6 +1735,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
1180
1735
|
act(BRIDGE_ENDPOINTS.saveCustomTunnelConfig, { serverUrl, accessToken })
|
|
1181
1736
|
, [act]);
|
|
1182
1737
|
|
|
1738
|
+
const navSecurity = React.useCallback(() => setActiveTab('security'), []);
|
|
1739
|
+
|
|
1183
1740
|
if (!status && !err) {
|
|
1184
1741
|
return React.createElement('div', {
|
|
1185
1742
|
style: { padding: 32, color: 'var(--dsw-alias-label-tertiary,#9ca3af)', fontSize: 13 },
|
|
@@ -1193,9 +1750,10 @@ function BridgePanel({ rpcCall }) {
|
|
|
1193
1750
|
p.status === 'connected' || p.status === 'starting' || p.status === 'reconnecting'
|
|
1194
1751
|
);
|
|
1195
1752
|
const dots = {
|
|
1196
|
-
lan:
|
|
1197
|
-
tunnel:
|
|
1198
|
-
im:
|
|
1753
|
+
lan: !!(status?.proxy?.running),
|
|
1754
|
+
tunnel: !!(status?.cloudflared?.running || ct?.running),
|
|
1755
|
+
im: !!imConnected,
|
|
1756
|
+
security: !!(status?.auth?.enabled),
|
|
1199
1757
|
};
|
|
1200
1758
|
|
|
1201
1759
|
// Tab 内容
|
|
@@ -1205,6 +1763,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
1205
1763
|
title: '局域网访问',
|
|
1206
1764
|
desc: '同一 Wi-Fi 下的设备可直接扫码访问',
|
|
1207
1765
|
data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
|
|
1766
|
+
auth: status?.auth,
|
|
1767
|
+
onNavigateSecurity: navSecurity,
|
|
1208
1768
|
});
|
|
1209
1769
|
} else if (activeTab === 'tunnel') {
|
|
1210
1770
|
tabContent = React.createElement(React.Fragment, null,
|
|
@@ -1217,6 +1777,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
1217
1777
|
qr: status?.cloudflared?.qr,
|
|
1218
1778
|
state: status?.cloudflared?.state,
|
|
1219
1779
|
},
|
|
1780
|
+
auth: status?.auth,
|
|
1781
|
+
onNavigateSecurity: navSecurity,
|
|
1220
1782
|
onStart: onStartCloudflared,
|
|
1221
1783
|
onStop: onStopCloudflared,
|
|
1222
1784
|
onReset: status?.cloudflared?.running ? onResetCloudflared : null,
|
|
@@ -1231,6 +1793,8 @@ function BridgePanel({ rpcCall }) {
|
|
|
1231
1793
|
qr: ct?.qr,
|
|
1232
1794
|
state: ct?.state,
|
|
1233
1795
|
},
|
|
1796
|
+
auth: status?.auth,
|
|
1797
|
+
onNavigateSecurity: navSecurity,
|
|
1234
1798
|
onStart: onStartCustom,
|
|
1235
1799
|
onStop: onStopCustom,
|
|
1236
1800
|
},
|
|
@@ -1242,6 +1806,12 @@ function BridgePanel({ rpcCall }) {
|
|
|
1242
1806
|
}),
|
|
1243
1807
|
),
|
|
1244
1808
|
);
|
|
1809
|
+
} else if (activeTab === 'security') {
|
|
1810
|
+
tabContent = React.createElement(AccessAuthCard, {
|
|
1811
|
+
auth: status?.auth,
|
|
1812
|
+
rpcCall: authRpcCall,
|
|
1813
|
+
onUpdate: () => load(true),
|
|
1814
|
+
});
|
|
1245
1815
|
} else if (activeTab === 'im') {
|
|
1246
1816
|
// 从 listPlatforms 动态生成平台列表
|
|
1247
1817
|
const IM_PLATFORMS = [
|
|
@@ -1295,23 +1865,129 @@ function BridgePanel({ rpcCall }) {
|
|
|
1295
1865
|
);
|
|
1296
1866
|
}),
|
|
1297
1867
|
),
|
|
1298
|
-
//
|
|
1868
|
+
// 显示选中的平台卡片(带有 key 保证切换时重置表单状态)
|
|
1299
1869
|
selectedPlatform && platforms?.[selectedPlatform] && React.createElement(PlatformCard, {
|
|
1870
|
+
key: selectedPlatform,
|
|
1300
1871
|
platformId: selectedPlatform,
|
|
1301
1872
|
platformName: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.label ?? selectedPlatform,
|
|
1302
1873
|
platformDesc: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.desc ?? '',
|
|
1303
|
-
rpcCall,
|
|
1874
|
+
rpcCall: authRpcCall,
|
|
1304
1875
|
onStatusChange: () => {}, // 状态变化已由 listPlatforms 轮询处理,不需要回调
|
|
1305
1876
|
}),
|
|
1306
1877
|
);
|
|
1307
1878
|
}
|
|
1308
1879
|
|
|
1880
|
+
const auth = status?.auth;
|
|
1881
|
+
const policy = auth?.adminPolicy ?? 'password_unlock';
|
|
1882
|
+
const isLocked = !isLocalhost && auth?.enabled && policy !== 'open' && !adminUnlocked;
|
|
1883
|
+
|
|
1884
|
+
// 远程设备被锁定:全局展示锁定页面,阻断所有 Tab 的查看与操作
|
|
1885
|
+
if (isLocked) {
|
|
1886
|
+
return React.createElement('div', { style: { maxWidth: 620 } },
|
|
1887
|
+
policy === 'local_only' ? (
|
|
1888
|
+
React.createElement('div', {
|
|
1889
|
+
style: { ...s.card, textAlign: 'center', padding: '36px 20px', marginTop: 10 },
|
|
1890
|
+
},
|
|
1891
|
+
React.createElement('div', { style: { fontSize: 40, marginBottom: 12 } }, '🛡️'),
|
|
1892
|
+
React.createElement('div', { style: { ...s.label, fontSize: 16, fontWeight: 600, marginBottom: 8 } }, '管理控制台已锁定(仅限电脑本机管理)'),
|
|
1893
|
+
React.createElement('div', { style: { ...s.muted, maxWidth: 420, margin: '0 auto', lineHeight: 1.6, fontSize: 13 } },
|
|
1894
|
+
'当前设备通过远程局域网或公网接入。已开启「仅限电脑本机管理」最高安全策略,远程设备禁止查看与修改任何网络与机器人配置。如需管理请在电脑本机(127.0.0.1)上操作。'
|
|
1895
|
+
),
|
|
1896
|
+
React.createElement('div', { style: { marginTop: 20 } },
|
|
1897
|
+
React.createElement('button', {
|
|
1898
|
+
type: 'button',
|
|
1899
|
+
style: { ...s.btnLink, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' },
|
|
1900
|
+
onClick: () => setShowForgotGuide(v => !v),
|
|
1901
|
+
}, '❓ 远程如何救急解除锁定?'),
|
|
1902
|
+
),
|
|
1903
|
+
showForgotGuide && React.createElement('div', {
|
|
1904
|
+
style: {
|
|
1905
|
+
marginTop: 14, padding: '12px 14px', borderRadius: 8, fontSize: 12, lineHeight: 1.6,
|
|
1906
|
+
background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
|
|
1907
|
+
color: 'var(--dsw-alias-label-secondary,#4b5563)', textAlign: 'left', maxWidth: 420, margin: '14px auto 0',
|
|
1908
|
+
},
|
|
1909
|
+
},
|
|
1910
|
+
React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 救急解除锁定指引:'),
|
|
1911
|
+
React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改策略或清除密码。'),
|
|
1912
|
+
React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器救急指令'), ':在宿主电脑/服务器终端执行 ', React.createElement('code', { style: s.code }, 'touch ~/.dsh/dsh-bridge/reset-auth'), ' 即可瞬间清空密码恢复初始状态。'),
|
|
1913
|
+
),
|
|
1914
|
+
)
|
|
1915
|
+
) : (
|
|
1916
|
+
React.createElement('div', {
|
|
1917
|
+
style: { ...s.card, maxWidth: 440, margin: '20px auto', padding: '32px 24px' },
|
|
1918
|
+
},
|
|
1919
|
+
React.createElement('div', { style: { textAlign: 'center', marginBottom: 20 } },
|
|
1920
|
+
React.createElement('div', { style: { fontSize: 40, marginBottom: 10 } }, '🔒'),
|
|
1921
|
+
React.createElement('div', { style: { ...s.label, fontSize: 16, fontWeight: 600 } }, '管理控制台已锁定'),
|
|
1922
|
+
React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 6, lineHeight: 1.5 } },
|
|
1923
|
+
'当前设备为远程访问。为保护您的网络与平台配置安全,请输入管理员密码解锁管理权限。'
|
|
1924
|
+
),
|
|
1925
|
+
),
|
|
1926
|
+
React.createElement('form', {
|
|
1927
|
+
onSubmit: handleUnlockAdmin,
|
|
1928
|
+
style: { display: 'flex', flexDirection: 'column', gap: 12 },
|
|
1929
|
+
},
|
|
1930
|
+
React.createElement('input', {
|
|
1931
|
+
type: 'password',
|
|
1932
|
+
style: s.input,
|
|
1933
|
+
placeholder: '输入后台管理密码',
|
|
1934
|
+
value: unlockPassword,
|
|
1935
|
+
onChange: (e) => setUnlockPassword(e.target.value),
|
|
1936
|
+
autoFocus: true,
|
|
1937
|
+
}),
|
|
1938
|
+
unlockErr && React.createElement('div', {
|
|
1939
|
+
style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' },
|
|
1940
|
+
}, unlockErr),
|
|
1941
|
+
React.createElement('button', {
|
|
1942
|
+
type: 'submit',
|
|
1943
|
+
style: { ...s.btnPri, width: '100%', justifyContent: 'center', height: 36, background: '#4f6ef7', color: '#ffffff' },
|
|
1944
|
+
disabled: unlocking,
|
|
1945
|
+
}, unlocking ? '验证中…' : '解锁管理权限'),
|
|
1946
|
+
),
|
|
1947
|
+
React.createElement('div', { style: { marginTop: 16, textAlign: 'center' } },
|
|
1948
|
+
React.createElement('button', {
|
|
1949
|
+
type: 'button',
|
|
1950
|
+
style: { ...s.btnLink, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' },
|
|
1951
|
+
onClick: () => setShowForgotGuide(v => !v),
|
|
1952
|
+
}, '❓ 忘记后台管理密码?'),
|
|
1953
|
+
),
|
|
1954
|
+
showForgotGuide && React.createElement('div', {
|
|
1955
|
+
style: {
|
|
1956
|
+
marginTop: 12, padding: '12px 14px', borderRadius: 8, fontSize: 12, lineHeight: 1.6,
|
|
1957
|
+
background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
|
|
1958
|
+
color: 'var(--dsw-alias-label-secondary,#4b5563)', textAlign: 'left',
|
|
1959
|
+
},
|
|
1960
|
+
},
|
|
1961
|
+
React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary,currentColor)', marginBottom: 4 } }, '🛟 找回与重置密码指引:'),
|
|
1962
|
+
React.createElement('div', null, '1. ', React.createElement('strong', null, '电脑本机直连修改'), ':直接在运行本程序的电脑本机打开本控制台(127.0.0.1 享有物理免锁特权),可随时修改管理密码。'),
|
|
1963
|
+
React.createElement('div', { style: { marginTop: 4 } }, '2. ', React.createElement('strong', null, '服务器救急指令'), ':在宿主电脑终端执行 ', React.createElement('code', { style: s.code }, 'touch ~/.dsh/dsh-bridge/reset-auth'), ' 即可瞬间清空密码恢复初始状态。'),
|
|
1964
|
+
),
|
|
1965
|
+
)
|
|
1966
|
+
)
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1309
1970
|
return React.createElement('div', { style: { maxWidth: 620 } },
|
|
1310
1971
|
err && React.createElement('div', {
|
|
1311
1972
|
style: { ...s.card, background: 'var(--dsw-alias-state-error-bg,#fef2f2)', color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 13, marginBottom: 16 },
|
|
1312
1973
|
}, err),
|
|
1313
1974
|
|
|
1314
|
-
React.createElement(
|
|
1975
|
+
!isLocalhost && adminUnlocked && React.createElement('div', {
|
|
1976
|
+
style: {
|
|
1977
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
1978
|
+
padding: '8px 14px', background: 'var(--dsw-alias-state-info-bg,#eff6ff)',
|
|
1979
|
+
border: '1px solid var(--dsw-alias-brand-primary,#4f6ef7)', borderRadius: 8,
|
|
1980
|
+
marginBottom: 14, fontSize: 12, color: 'var(--dsw-alias-brand-primary,#4f6ef7)',
|
|
1981
|
+
},
|
|
1982
|
+
},
|
|
1983
|
+
React.createElement('span', null, '🔓 管理员权限已解锁(当前临时会话有效)'),
|
|
1984
|
+
React.createElement('button', {
|
|
1985
|
+
style: { ...s.btnGhost, height: 24, fontSize: 11, padding: '0 8px' },
|
|
1986
|
+
onClick: handleLockAdmin,
|
|
1987
|
+
}, '🔒 重新锁定后台'),
|
|
1988
|
+
),
|
|
1989
|
+
|
|
1990
|
+
React.createElement(VersionBanner, { rpcCall: authRpcCall }),
|
|
1315
1991
|
|
|
1316
1992
|
React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
|
|
1317
1993
|
|