@wenbin_wb/dsh-bridge 1.2.4 → 2.0.0
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.md +3 -2
- package/README.zh-CN.md +3 -2
- package/client/client.js +102 -76
- package/client/index.js +112 -89
- package/lib/bridge-rpc-constants.js +9 -0
- package/lib/bridge-rpc.js +93 -3
- package/lib/index.js +28 -10
- package/lib/platform/base.js +143 -0
- package/lib/platform/conversation-bridge.js +972 -0
- package/lib/platform/index.js +10 -0
- package/lib/platform/manager.js +74 -0
- package/lib/wechat/index.js +82 -37
- package/lib/wechat/node.js +64 -886
- package/package.json +3 -2
package/client/index.js
CHANGED
|
@@ -31,7 +31,7 @@ const s = {
|
|
|
31
31
|
muted: { color: 'var(--dsw-alias-label-tertiary,#8b93a1)', fontSize: 12, lineHeight: 1.5 },
|
|
32
32
|
label: { color: 'var(--dsw-alias-label-primary,currentColor)', fontSize: 13, fontWeight: 500 },
|
|
33
33
|
code: { fontFamily: 'ui-monospace,Menlo,monospace', fontSize: 12, wordBreak: 'break-all', color: 'var(--dsw-alias-label-primary,currentColor)' },
|
|
34
|
-
btnPri: { font: 'inherit', cursor: 'pointer', border: 'none', background: 'var(--dsw-alias-
|
|
34
|
+
btnPri: { font: 'inherit', cursor: 'pointer', border: 'none', background: 'var(--dsw-alias-brand-primary,#4f6ef7)', color: 'var(--dsw-alias-label-primary-foreground,#fff)', height: 32, padding: '0 14px', borderRadius: 999, fontSize: 13, fontWeight: 500, display: 'inline-flex', alignItems: 'center', gap: 4 },
|
|
35
35
|
btnGhost: { font: 'inherit', cursor: 'pointer', border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', background: 'var(--dsw-alias-bg-layer-1,transparent)', color: 'var(--dsw-alias-label-primary,currentColor)', height: 32, padding: '0 14px', borderRadius: 999, fontSize: 13, display: 'inline-flex', alignItems: 'center', gap: 4, textDecoration: 'none' },
|
|
36
36
|
btnLink: { font: 'inherit', cursor: 'pointer', border: 'none', background: 'none', color: 'var(--dsw-alias-brand-primary,#4f6ef7)', fontSize: 12, padding: 0, display: 'inline-flex', alignItems: 'center', gap: 3, textDecoration: 'none' },
|
|
37
37
|
qr: { width: 200, height: 200, borderRadius: 10, border: '1px solid var(--dsw-alias-border-l2,#e5e7eb)', margin: '8px 0', display: 'block' },
|
|
@@ -213,10 +213,10 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
|
|
|
213
213
|
);
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
// ----
|
|
216
|
+
// ---- 通用 IM 平台卡片 ----
|
|
217
217
|
|
|
218
|
-
function
|
|
219
|
-
const [
|
|
218
|
+
function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatusChange }) {
|
|
219
|
+
const [platform, setPlatform] = React.useState(null);
|
|
220
220
|
const [err, setErr] = React.useState(null);
|
|
221
221
|
const [busy, setBusy] = React.useState(false);
|
|
222
222
|
const [showAdvanced, setShowAdvanced] = React.useState(false);
|
|
@@ -225,48 +225,50 @@ function WechatCard({ rpcCall, onStatusChange }) {
|
|
|
225
225
|
// 高级设置本地草稿
|
|
226
226
|
const [cfgDraft, setCfgDraft] = React.useState(null);
|
|
227
227
|
React.useEffect(() => {
|
|
228
|
-
if (
|
|
228
|
+
if (platform?.config && !cfgDraft) {
|
|
229
229
|
setCfgDraft({
|
|
230
|
-
digestIntervalSec: String(
|
|
231
|
-
approvalTimeoutSec: String(
|
|
232
|
-
maxMessageChars: String(
|
|
233
|
-
sendChunkDelayMs: String(
|
|
230
|
+
digestIntervalSec: String(platform.config.digestIntervalSec ?? 300),
|
|
231
|
+
approvalTimeoutSec: String(platform.config.approvalTimeoutSec ?? 600),
|
|
232
|
+
maxMessageChars: String(platform.config.maxMessageChars ?? 2000),
|
|
233
|
+
sendChunkDelayMs: String(platform.config.sendChunkDelayMs ?? 1500),
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
|
-
}, [
|
|
236
|
+
}, [platform?.config]);
|
|
237
237
|
|
|
238
238
|
// 向上传递连接状态(供平台列表卡片绿点使用)
|
|
239
239
|
React.useEffect(() => {
|
|
240
|
-
const connected =
|
|
240
|
+
const connected = platform?.status === 'connected' || platform?.status === 'starting' || platform?.status === 'reconnecting';
|
|
241
241
|
onStatusChange?.(connected);
|
|
242
|
-
}, [
|
|
242
|
+
}, [platform?.status, onStatusChange]);
|
|
243
243
|
|
|
244
244
|
const load = React.useCallback(async (quiet = false) => {
|
|
245
245
|
try {
|
|
246
|
-
|
|
246
|
+
// 用通用端点读取平台状态(不执行登录操作,只获取状态)
|
|
247
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
|
|
247
248
|
if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
|
|
248
|
-
|
|
249
|
+
const allPlatforms = r.value ?? {};
|
|
250
|
+
setPlatform(allPlatforms[platformId] ?? null);
|
|
249
251
|
if (!quiet) setErr(null);
|
|
250
252
|
} catch (e) {
|
|
251
253
|
if (!quiet) setErr(e.message);
|
|
252
254
|
}
|
|
253
|
-
}, [rpcCall]);
|
|
255
|
+
}, [rpcCall, platformId]);
|
|
254
256
|
|
|
255
257
|
// 轮询:登录中(qr/scaned)快速刷新,其余放慢
|
|
256
258
|
React.useEffect(() => {
|
|
257
259
|
load();
|
|
258
|
-
const activeLogin =
|
|
260
|
+
const activeLogin = platform?.login && (platform.login.phase === 'qr' || platform.login.phase === 'scaned');
|
|
259
261
|
const interval = activeLogin ? 1500 : 3000;
|
|
260
262
|
const t = setInterval(() => load(true), interval);
|
|
261
263
|
return () => clearInterval(t);
|
|
262
|
-
}, [load,
|
|
264
|
+
}, [load, platform?.login?.phase]);
|
|
263
265
|
|
|
264
266
|
const act = React.useCallback(async (endpoint, payload) => {
|
|
265
267
|
setBusy(true);
|
|
266
268
|
try {
|
|
267
|
-
const r = await rpcCall(endpoint, payload
|
|
269
|
+
const r = await rpcCall(endpoint, { platformId, ...payload });
|
|
268
270
|
if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
|
|
269
|
-
|
|
271
|
+
setPlatform(r.value);
|
|
270
272
|
setErr(null);
|
|
271
273
|
await load(true);
|
|
272
274
|
} catch (e) {
|
|
@@ -274,74 +276,72 @@ function WechatCard({ rpcCall, onStatusChange }) {
|
|
|
274
276
|
} finally {
|
|
275
277
|
setBusy(false);
|
|
276
278
|
}
|
|
277
|
-
}, [rpcCall, load]);
|
|
279
|
+
}, [rpcCall, load, platformId]);
|
|
278
280
|
|
|
279
|
-
const onLogin = React.useCallback(() => act(BRIDGE_ENDPOINTS.
|
|
280
|
-
const onStop = React.useCallback(() => act(BRIDGE_ENDPOINTS.
|
|
281
|
+
const onLogin = React.useCallback(() => act(BRIDGE_ENDPOINTS.platformLogin, {}), [act]);
|
|
282
|
+
const onStop = React.useCallback(() => act(BRIDGE_ENDPOINTS.platformStop, {}), [act]);
|
|
281
283
|
|
|
282
284
|
// 白名单管理
|
|
283
285
|
const [newId, setNewId] = React.useState('');
|
|
284
286
|
const addAllow = React.useCallback(async () => {
|
|
285
287
|
const id = newId.trim();
|
|
286
288
|
if (!id) return;
|
|
287
|
-
const list = [...(
|
|
288
|
-
await act(BRIDGE_ENDPOINTS.
|
|
289
|
+
const list = [...(platform?.allowFrom ?? []), id];
|
|
290
|
+
await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
|
|
289
291
|
setNewId('');
|
|
290
|
-
}, [act, newId,
|
|
292
|
+
}, [act, newId, platform?.allowFrom]);
|
|
291
293
|
const removeAllow = React.useCallback(async (id) => {
|
|
292
|
-
const list = (
|
|
293
|
-
await act(BRIDGE_ENDPOINTS.
|
|
294
|
-
}, [act,
|
|
294
|
+
const list = (platform?.allowFrom ?? []).filter((x) => x !== id);
|
|
295
|
+
await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
|
|
296
|
+
}, [act, platform?.allowFrom]);
|
|
295
297
|
const handleNewId = React.useCallback((e) => setNewId(e.target.value), []);
|
|
296
298
|
|
|
297
299
|
// 高级设置保存
|
|
298
300
|
const saveConfig = React.useCallback(async () => {
|
|
299
301
|
if (!cfgDraft) return;
|
|
300
|
-
await act(BRIDGE_ENDPOINTS.
|
|
302
|
+
await act(BRIDGE_ENDPOINTS.platformSetConfig, {
|
|
301
303
|
digestIntervalSec: Number(cfgDraft.digestIntervalSec),
|
|
302
304
|
approvalTimeoutSec: Number(cfgDraft.approvalTimeoutSec),
|
|
303
305
|
maxMessageChars: Number(cfgDraft.maxMessageChars),
|
|
304
306
|
sendChunkDelayMs: Number(cfgDraft.sendChunkDelayMs),
|
|
305
307
|
});
|
|
306
308
|
}, [act, cfgDraft]);
|
|
307
|
-
const cfgDirty = cfgDraft &&
|
|
308
|
-
Number(cfgDraft.digestIntervalSec) !==
|
|
309
|
-
Number(cfgDraft.approvalTimeoutSec) !==
|
|
310
|
-
Number(cfgDraft.maxMessageChars) !==
|
|
311
|
-
Number(cfgDraft.sendChunkDelayMs) !==
|
|
309
|
+
const cfgDirty = cfgDraft && platform?.config && (
|
|
310
|
+
Number(cfgDraft.digestIntervalSec) !== platform.config.digestIntervalSec ||
|
|
311
|
+
Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec ||
|
|
312
|
+
Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars ||
|
|
313
|
+
Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs
|
|
312
314
|
);
|
|
313
315
|
|
|
314
|
-
if (!
|
|
316
|
+
if (!platform && !err) {
|
|
315
317
|
return React.createElement('div', { style: s.card },
|
|
316
|
-
React.createElement('div', { style: s.label },
|
|
318
|
+
React.createElement('div', { style: s.label }, platformName),
|
|
317
319
|
React.createElement('div', { style: { ...s.muted, marginTop: 6 } }, '加载中…'),
|
|
318
320
|
);
|
|
319
321
|
}
|
|
320
322
|
|
|
321
|
-
const connected =
|
|
322
|
-
const login =
|
|
323
|
+
const connected = platform?.status === 'connected' || platform?.status === 'starting';
|
|
324
|
+
const login = platform?.login ?? {};
|
|
323
325
|
const showQr = login.phase === 'qr' || login.phase === 'scaned';
|
|
324
|
-
const statusLabel =
|
|
325
|
-
:
|
|
326
|
-
:
|
|
327
|
-
:
|
|
328
|
-
:
|
|
326
|
+
const statusLabel = platform?.status === 'connected' ? '已连接'
|
|
327
|
+
: platform?.status === 'starting' ? '连接中…'
|
|
328
|
+
: platform?.status === 'reconnecting' ? '重连中…'
|
|
329
|
+
: platform?.status === 'paused' ? '暂停(会话过期)'
|
|
330
|
+
: platform?.status === 'error' ? '错误'
|
|
329
331
|
: '未连接';
|
|
330
332
|
|
|
331
333
|
return React.createElement('div', { style: s.card },
|
|
332
334
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
333
335
|
React.createElement('div', null,
|
|
334
|
-
React.createElement('div', { style: s.label },
|
|
335
|
-
React.createElement('div', { style: { ...s.muted, marginTop: 2 } },
|
|
336
|
-
'通过微信扫 ClawBot 二维码,在微信里远程对话和控制 DSH agent'
|
|
337
|
-
),
|
|
336
|
+
React.createElement('div', { style: s.label }, platformName),
|
|
337
|
+
React.createElement('div', { style: { ...s.muted, marginTop: 2 } }, platformDesc),
|
|
338
338
|
),
|
|
339
339
|
React.createElement(StatusTag, { running: connected }),
|
|
340
340
|
),
|
|
341
341
|
|
|
342
342
|
// 快捷入口:使用说明 / 命令
|
|
343
343
|
React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap', alignItems: 'center' } },
|
|
344
|
-
React.createElement('a', {
|
|
344
|
+
platformId === 'wechat' && React.createElement('a', {
|
|
345
345
|
href: 'https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/wechat-usage.md',
|
|
346
346
|
target: '_blank', rel: 'noopener noreferrer',
|
|
347
347
|
style: s.btnGhost,
|
|
@@ -349,7 +349,7 @@ function WechatCard({ rpcCall, onStatusChange }) {
|
|
|
349
349
|
React.createElement('button', {
|
|
350
350
|
style: s.btnGhost,
|
|
351
351
|
onClick: () => setShowHelp(v => !v),
|
|
352
|
-
}, showHelp ? '收起命令' : '
|
|
352
|
+
}, showHelp ? '收起命令' : '命令列表'),
|
|
353
353
|
),
|
|
354
354
|
|
|
355
355
|
// 命令速查
|
|
@@ -368,18 +368,18 @@ function WechatCard({ rpcCall, onStatusChange }) {
|
|
|
368
368
|
err && React.createElement('div', { style: { ...s.warn, marginTop: 10 } }, err),
|
|
369
369
|
|
|
370
370
|
// 已配置:状态详情 + 白名单
|
|
371
|
-
|
|
371
|
+
platform?.configured && React.createElement('div', { style: s.block },
|
|
372
372
|
React.createElement('div', { style: { fontSize: 12, lineHeight: 1.7 } },
|
|
373
373
|
React.createElement('div', null, `状态: ${statusLabel}`),
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
platform.accountId && React.createElement('div', null, `账号: ${platform.accountId}`),
|
|
375
|
+
platform.sessionId && React.createElement('div', null, `当前会话: ${platform.sessionId}`),
|
|
376
376
|
),
|
|
377
377
|
React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 8, lineHeight: 1.6 } },
|
|
378
|
-
'
|
|
378
|
+
'白名单(仅这些用户可驱动 agent):'
|
|
379
379
|
),
|
|
380
380
|
React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 } },
|
|
381
|
-
(
|
|
382
|
-
?
|
|
381
|
+
(platform.allowFrom?.length
|
|
382
|
+
? platform.allowFrom.map((id) =>
|
|
383
383
|
React.createElement('span', { key: id, style: { ...s.tag, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', color: 'var(--dsw-alias-label-primary,currentColor)', gap: 6 } },
|
|
384
384
|
React.createElement('span', { style: { fontSize: 12, wordBreak: 'break-all' } }, id),
|
|
385
385
|
React.createElement('button', {
|
|
@@ -388,12 +388,16 @@ function WechatCard({ rpcCall, onStatusChange }) {
|
|
|
388
388
|
}, '×'),
|
|
389
389
|
)
|
|
390
390
|
)
|
|
391
|
-
: React.createElement('div', { style: { ...s.muted, fontSize: 12 } },
|
|
391
|
+
: React.createElement('div', { style: { ...s.muted, fontSize: 12 } },
|
|
392
|
+
platformId === 'wechat'
|
|
393
|
+
? '(空 — 扫码后首个发消息的微信用户将自动加入)'
|
|
394
|
+
: '(空 — 首个发消息的用户将自动加入)'
|
|
395
|
+
)),
|
|
392
396
|
),
|
|
393
397
|
React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8, alignItems: 'center' } },
|
|
394
398
|
React.createElement('input', {
|
|
395
399
|
style: { ...s.input, flex: 1 },
|
|
396
|
-
placeholder: '添加允许的微信 ID(如 xxx@im.wechat)',
|
|
400
|
+
placeholder: platformId === 'wechat' ? '添加允许的微信 ID(如 xxx@im.wechat)' : '添加允许的用户 ID',
|
|
397
401
|
value: newId,
|
|
398
402
|
onChange: handleNewId,
|
|
399
403
|
}),
|
|
@@ -403,26 +407,28 @@ function WechatCard({ rpcCall, onStatusChange }) {
|
|
|
403
407
|
}, '添加'),
|
|
404
408
|
),
|
|
405
409
|
React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap', alignItems: 'center' } },
|
|
406
|
-
|
|
407
|
-
React.createElement('button', { style: s.btnPri, onClick: onLogin, disabled: busy }, '
|
|
408
|
-
(
|
|
410
|
+
platform.status !== 'connected' && platform.status !== 'starting' &&
|
|
411
|
+
React.createElement('button', { style: s.btnPri, onClick: onLogin, disabled: busy }, '重新登录'),
|
|
412
|
+
(platform.status === 'connected' || platform.status === 'starting') &&
|
|
409
413
|
React.createElement('button', { style: s.btnGhost, onClick: onStop, disabled: busy }, '断开'),
|
|
410
414
|
React.createElement('button', {
|
|
411
415
|
style: { ...s.btnGhost, color: 'var(--dsw-alias-state-error-primary,#dc2626)', borderColor: 'var(--dsw-alias-state-error-primary,#dc2626)', opacity: busy ? 0.5 : 1 },
|
|
412
416
|
disabled: busy,
|
|
413
|
-
onClick: () => { if (window.confirm('
|
|
414
|
-
title: '
|
|
417
|
+
onClick: () => { if (window.confirm('确认解绑?这将清除登录凭证,下次需重新登录。')) act(BRIDGE_ENDPOINTS.platformUnbind, {}); },
|
|
418
|
+
title: '清除登录凭证,下次需重新登录',
|
|
415
419
|
}, '解绑账号'),
|
|
416
420
|
),
|
|
417
421
|
),
|
|
418
422
|
|
|
419
423
|
// 未配置 / 登录中:二维码
|
|
420
|
-
(!
|
|
424
|
+
(!platform?.configured || showQr) && React.createElement('div', { style: s.block },
|
|
421
425
|
showQr && login.qr
|
|
422
426
|
? React.createElement('div', null,
|
|
423
|
-
React.createElement('img', { src: login.qr, alt: '
|
|
427
|
+
React.createElement('img', { src: login.qr, alt: 'login QR', style: s.qr }),
|
|
424
428
|
React.createElement('div', { style: { ...s.muted, marginTop: 4 } },
|
|
425
|
-
login.phase === 'scaned'
|
|
429
|
+
login.phase === 'scaned'
|
|
430
|
+
? '已扫码,请在手机上确认…'
|
|
431
|
+
: (platformId === 'wechat' ? '请使用微信扫码登录(ClawBot)' : '请扫码登录')
|
|
426
432
|
),
|
|
427
433
|
login.error && React.createElement('div', { style: { ...s.muted, marginTop: 4, color: 'var(--dsw-alias-state-warn-primary,#92400e)' } }, login.error),
|
|
428
434
|
)
|
|
@@ -492,7 +498,9 @@ function WechatCard({ rpcCall, onStatusChange }) {
|
|
|
492
498
|
|
|
493
499
|
React.createElement('div', { style: s.block },
|
|
494
500
|
React.createElement('div', { style: { ...s.tip, fontSize: 12 } },
|
|
495
|
-
|
|
501
|
+
platformId === 'wechat'
|
|
502
|
+
? '说明: 扫码成功后,向该微信 Bot 发送第一条消息即自动完成白名单授权。仅白名单内的微信用户能驱动 agent,其他人消息会被忽略。使用专用微信号,避免影响主号。'
|
|
503
|
+
: '说明: 登录成功后,发送第一条消息即自动完成白名单授权。仅白名单内的用户能驱动 agent,其他人消息会被忽略。'
|
|
496
504
|
),
|
|
497
505
|
),
|
|
498
506
|
);
|
|
@@ -649,8 +657,9 @@ function BridgePanel({ rpcCall }) {
|
|
|
649
657
|
const [status, setStatus] = React.useState(null);
|
|
650
658
|
const [err, setErr] = React.useState(null);
|
|
651
659
|
const [activeTab, setActiveTab] = React.useState('lan');
|
|
652
|
-
//
|
|
653
|
-
const [
|
|
660
|
+
// 平台列表和连接状态
|
|
661
|
+
const [platforms, setPlatforms] = React.useState(null);
|
|
662
|
+
const [selectedPlatform, setSelectedPlatform] = React.useState('wechat');
|
|
654
663
|
|
|
655
664
|
const load = React.useCallback(async (quiet = false) => {
|
|
656
665
|
try {
|
|
@@ -663,15 +672,14 @@ function BridgePanel({ rpcCall }) {
|
|
|
663
672
|
}
|
|
664
673
|
}, [rpcCall]);
|
|
665
674
|
|
|
666
|
-
//
|
|
675
|
+
// 独立轮询所有平台状态(Tab 未选中时也能更新)
|
|
667
676
|
React.useEffect(() => {
|
|
668
677
|
let alive = true;
|
|
669
678
|
const poll = async () => {
|
|
670
679
|
try {
|
|
671
|
-
const r = await rpcCall(BRIDGE_ENDPOINTS.
|
|
680
|
+
const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
|
|
672
681
|
if (alive && r?.ok) {
|
|
673
|
-
|
|
674
|
-
setWechatConnected(s === 'connected' || s === 'starting' || s === 'reconnecting');
|
|
682
|
+
setPlatforms(r.value ?? {});
|
|
675
683
|
}
|
|
676
684
|
} catch { /* 忽略,不影响主面板 */ }
|
|
677
685
|
};
|
|
@@ -716,11 +724,14 @@ function BridgePanel({ rpcCall }) {
|
|
|
716
724
|
|
|
717
725
|
const ct = status?.customTunnel;
|
|
718
726
|
|
|
719
|
-
// Tab
|
|
727
|
+
// Tab 状态点:从各自数据源计算
|
|
728
|
+
const imConnected = platforms && Object.values(platforms).some(p =>
|
|
729
|
+
p.status === 'connected' || p.status === 'starting' || p.status === 'reconnecting'
|
|
730
|
+
);
|
|
720
731
|
const dots = {
|
|
721
732
|
lan: !!(status?.proxy?.running),
|
|
722
733
|
tunnel: !!(status?.cloudflared?.running || ct?.running),
|
|
723
|
-
im:
|
|
734
|
+
im: !!imConnected,
|
|
724
735
|
};
|
|
725
736
|
|
|
726
737
|
// Tab 内容
|
|
@@ -768,30 +779,36 @@ function BridgePanel({ rpcCall }) {
|
|
|
768
779
|
),
|
|
769
780
|
);
|
|
770
781
|
} else if (activeTab === 'im') {
|
|
771
|
-
//
|
|
772
|
-
// wechatConnected 来自 WechatCard 的 onStatusChange 回调,状态准确
|
|
782
|
+
// 从 listPlatforms 动态生成平台列表
|
|
773
783
|
const IM_PLATFORMS = [
|
|
774
|
-
{ id: 'wechat', label: '微信', desc: 'iLink Bot API(ClawBot)'
|
|
775
|
-
{ id: 'qq', label: 'QQ', desc: 'NapCat / Mirai'
|
|
776
|
-
{ id: 'feishu', label: '飞书', desc: '官方事件回调 API'
|
|
784
|
+
{ id: 'wechat', label: '微信', desc: 'iLink Bot API(ClawBot)' },
|
|
785
|
+
{ id: 'qq', label: 'QQ', desc: 'NapCat / Mirai' },
|
|
786
|
+
{ id: 'feishu', label: '飞书', desc: '官方事件回调 API' },
|
|
777
787
|
];
|
|
788
|
+
|
|
778
789
|
tabContent = React.createElement('div', null,
|
|
779
|
-
//
|
|
790
|
+
// 平台选择器(可点击切换)
|
|
780
791
|
React.createElement('div', {
|
|
781
792
|
style: { display: 'flex', gap: 10, marginBottom: 16, flexWrap: 'wrap' },
|
|
782
793
|
},
|
|
783
|
-
IM_PLATFORMS.map(({ id, label, desc
|
|
784
|
-
|
|
794
|
+
IM_PLATFORMS.map(({ id, label, desc }) => {
|
|
795
|
+
const platformData = platforms?.[id];
|
|
796
|
+
const available = !!platformData;
|
|
797
|
+
const active = platformData?.status === 'connected' || platformData?.status === 'starting' || platformData?.status === 'reconnecting';
|
|
798
|
+
|
|
799
|
+
return React.createElement('div', {
|
|
785
800
|
key: id,
|
|
786
801
|
style: {
|
|
787
802
|
flex: '1 1 140px',
|
|
788
|
-
border: `1px solid ${active ? 'var(--dsw-alias-state-success-primary,#10b981)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
|
|
803
|
+
border: `1px solid ${selectedPlatform === id ? 'var(--dsw-alias-state-info-primary,#3b82f6)' : active ? 'var(--dsw-alias-state-success-primary,#10b981)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
|
|
789
804
|
borderRadius: 10,
|
|
790
805
|
padding: '12px 14px',
|
|
791
806
|
opacity: available ? 1 : 0.45,
|
|
792
|
-
cursor: available ? '
|
|
793
|
-
background: active ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : available ? 'var(--dsw-alias-bg-layer-1,transparent)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
|
|
807
|
+
cursor: available ? 'pointer' : 'not-allowed',
|
|
808
|
+
background: selectedPlatform === id ? 'var(--dsw-alias-state-info-bg,#eff6ff)' : active ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : available ? 'var(--dsw-alias-bg-layer-1,transparent)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
|
|
809
|
+
transition: 'all 0.15s ease',
|
|
794
810
|
},
|
|
811
|
+
onClick: available ? () => setSelectedPlatform(id) : undefined,
|
|
795
812
|
},
|
|
796
813
|
React.createElement('div', { style: { ...s.label, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 } },
|
|
797
814
|
label,
|
|
@@ -806,11 +823,17 @@ function BridgePanel({ rpcCall }) {
|
|
|
806
823
|
}, '即将支持'),
|
|
807
824
|
),
|
|
808
825
|
React.createElement('div', { style: { ...s.muted, marginTop: 3, fontSize: 11 } }, desc),
|
|
809
|
-
)
|
|
810
|
-
),
|
|
826
|
+
);
|
|
827
|
+
}),
|
|
811
828
|
),
|
|
812
|
-
//
|
|
813
|
-
React.createElement(
|
|
829
|
+
// 显示选中的平台卡片
|
|
830
|
+
selectedPlatform && platforms?.[selectedPlatform] && React.createElement(PlatformCard, {
|
|
831
|
+
platformId: selectedPlatform,
|
|
832
|
+
platformName: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.label ?? selectedPlatform,
|
|
833
|
+
platformDesc: IM_PLATFORMS.find(p => p.id === selectedPlatform)?.desc ?? '',
|
|
834
|
+
rpcCall,
|
|
835
|
+
onStatusChange: () => {}, // 状态变化已由 listPlatforms 轮询处理,不需要回调
|
|
836
|
+
}),
|
|
814
837
|
);
|
|
815
838
|
}
|
|
816
839
|
|
|
@@ -11,6 +11,15 @@ export const BRIDGE_ENDPOINTS = {
|
|
|
11
11
|
resetCloudflared: 'resetCloudflared',
|
|
12
12
|
saveCustomTunnelConfig: 'saveCustomTunnelConfig',
|
|
13
13
|
checkVersion: 'checkVersion',
|
|
14
|
+
// 平台管理器(多 IM 平台统一接口)
|
|
15
|
+
listPlatforms: 'listPlatforms',
|
|
16
|
+
platformLogin: 'platformLogin',
|
|
17
|
+
platformSetAllowFrom: 'platformSetAllowFrom',
|
|
18
|
+
platformSetConfig: 'platformSetConfig',
|
|
19
|
+
platformStop: 'platformStop',
|
|
20
|
+
platformStart: 'platformStart',
|
|
21
|
+
platformUnbind: 'platformUnbind',
|
|
22
|
+
// 微信 Bot(v1.x 向后兼容别名,deprecated)
|
|
14
23
|
wechatGetStatus: 'wechatGetStatus',
|
|
15
24
|
wechatLogin: 'wechatLogin',
|
|
16
25
|
wechatSetAllowFrom: 'wechatSetAllowFrom',
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -54,7 +54,7 @@ async function wechatStatusValue(wechatService, logger) {
|
|
|
54
54
|
return { ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function installBridgeRpc(ctx, { service, wechat, logger, saveCustomTunnelConfig }) {
|
|
57
|
+
export function installBridgeRpc(ctx, { service, wechat, platformManager, logger, saveCustomTunnelConfig }) {
|
|
58
58
|
if (!ctx?.connection?.rpc?.handle) {
|
|
59
59
|
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
60
60
|
return () => {};
|
|
@@ -123,7 +123,97 @@ export function installBridgeRpc(ctx, { service, wechat, logger, saveCustomTunne
|
|
|
123
123
|
return ok(result);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// ----
|
|
126
|
+
// ---- 平台管理器(多 IM 平台)----
|
|
127
|
+
|
|
128
|
+
if (endpoint === BRIDGE_ENDPOINTS.listPlatforms) {
|
|
129
|
+
if (!platformManager) return ok({});
|
|
130
|
+
// 每个平台的 login.qrPayload 渲染为 dataURL 后返回
|
|
131
|
+
const raw = platformManager.getStatus();
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const [id, status] of Object.entries(raw)) {
|
|
134
|
+
let qr = null;
|
|
135
|
+
try { qr = await renderQr(status.login).catch(() => null); } catch { /* ignore */ }
|
|
136
|
+
out[id] = { ...status, login: { ...(status.login ?? {}), qr, qrPayload: undefined, qrKind: undefined } };
|
|
137
|
+
}
|
|
138
|
+
return ok(out);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---- 平台操作(统一接口)----
|
|
142
|
+
|
|
143
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformLogin) {
|
|
144
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
145
|
+
const { platformId, qrType } = payload;
|
|
146
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
147
|
+
const platform = platformManager.get(platformId);
|
|
148
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
149
|
+
const result = await platform.login({ qrType });
|
|
150
|
+
if (!result.ok) return fail('bad-request', result.error ?? '登录启动失败');
|
|
151
|
+
const status = platform.getStatus();
|
|
152
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
153
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformSetAllowFrom) {
|
|
157
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
158
|
+
const { platformId, allowFrom } = payload;
|
|
159
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
160
|
+
const platform = platformManager.get(platformId);
|
|
161
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
162
|
+
await platform.setAllowFrom(allowFrom);
|
|
163
|
+
const status = platform.getStatus();
|
|
164
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
165
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformSetConfig) {
|
|
169
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
170
|
+
const { platformId, ...config } = payload;
|
|
171
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
172
|
+
const platform = platformManager.get(platformId);
|
|
173
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
174
|
+
await platform.setConfig(config);
|
|
175
|
+
const status = platform.getStatus();
|
|
176
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
177
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformStop) {
|
|
181
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
182
|
+
const { platformId } = payload;
|
|
183
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
184
|
+
const platform = platformManager.get(platformId);
|
|
185
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
186
|
+
await platform.stop();
|
|
187
|
+
const status = platform.getStatus();
|
|
188
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
189
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformStart) {
|
|
193
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
194
|
+
const { platformId } = payload;
|
|
195
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
196
|
+
const platform = platformManager.get(platformId);
|
|
197
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
198
|
+
await platform.start();
|
|
199
|
+
const status = platform.getStatus();
|
|
200
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
201
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformUnbind) {
|
|
205
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
206
|
+
const { platformId } = payload;
|
|
207
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
208
|
+
const platform = platformManager.get(platformId);
|
|
209
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
210
|
+
await platform.unbind();
|
|
211
|
+
const status = platform.getStatus();
|
|
212
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
213
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---- 微信 Bot(v1.x 向后兼容别名,deprecated)----
|
|
127
217
|
|
|
128
218
|
if (endpoint === BRIDGE_ENDPOINTS.wechatGetStatus) {
|
|
129
219
|
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
@@ -164,7 +254,7 @@ export function installBridgeRpc(ctx, { service, wechat, logger, saveCustomTunne
|
|
|
164
254
|
if (endpoint === BRIDGE_ENDPOINTS.wechatStart) {
|
|
165
255
|
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
166
256
|
await wechat.gateway.start().catch((err) => {
|
|
167
|
-
logger.error('wechat start
|
|
257
|
+
logger.error('wechat start enabled: %s', err?.message ?? err);
|
|
168
258
|
});
|
|
169
259
|
const value = await wechatStatusValue(wechat, logger);
|
|
170
260
|
return ok(value);
|
package/lib/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import QRCode from 'qrcode';
|
|
|
16
16
|
import { installBridgeRpc } from './bridge-rpc.js';
|
|
17
17
|
import { CustomTunnelClient } from './tunnel-client.mjs';
|
|
18
18
|
import { CloudflaredManager } from './cloudflared-manager.mjs';
|
|
19
|
+
import { PlatformManager } from './platform/manager.js';
|
|
19
20
|
import { WechatService } from './wechat/index.js';
|
|
20
21
|
|
|
21
22
|
const name = 'dsh-bridge';
|
|
@@ -483,7 +484,10 @@ function apply(ctx, config = {}) {
|
|
|
483
484
|
}
|
|
484
485
|
}).catch(() => {});
|
|
485
486
|
|
|
486
|
-
//
|
|
487
|
+
// 平台管理器:注册/协调所有 IM 平台适配器
|
|
488
|
+
const platformManager = new PlatformManager({ logger });
|
|
489
|
+
|
|
490
|
+
// 微信 Bot(ClawBot/iLink)—— 作为 Platform 子类注册进平台管理器
|
|
487
491
|
const wechat = new WechatService({
|
|
488
492
|
ctx,
|
|
489
493
|
logger,
|
|
@@ -494,20 +498,32 @@ function apply(ctx, config = {}) {
|
|
|
494
498
|
await saveConfig(stored);
|
|
495
499
|
},
|
|
496
500
|
});
|
|
501
|
+
platformManager.register(wechat);
|
|
497
502
|
|
|
498
503
|
// 启动时读取已保存的微信 Bot 配置(凭证 + 白名单 + 活动会话)
|
|
499
|
-
loadConfig().then((stored) => {
|
|
504
|
+
loadConfig().then(async (stored) => {
|
|
500
505
|
if (stored?.wechat) {
|
|
501
506
|
const cfg = stored.wechat;
|
|
502
507
|
wechat.node.config.allowFrom = Array.isArray(cfg.allowFrom) ? cfg.allowFrom : [];
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
508
|
+
|
|
509
|
+
// 用 Promise 包装配置恢复过程,防止 handleInbound 竞态
|
|
510
|
+
wechat.node._restoringConfig = (async () => {
|
|
511
|
+
try {
|
|
512
|
+
// 恢复活动会话 ID(直接覆盖,不判断当前值)
|
|
513
|
+
if (cfg.activeSessionId) {
|
|
514
|
+
wechat.node.activeSessionId = cfg.activeSessionId;
|
|
515
|
+
logger.info('dsh-bridge: restored wechat active session: %s', cfg.activeSessionId);
|
|
516
|
+
} else {
|
|
517
|
+
// 没有持久化的会话时,回退到选第一个
|
|
518
|
+
await wechat.node._pickDefaultSession().catch(() => {});
|
|
519
|
+
}
|
|
520
|
+
} finally {
|
|
521
|
+
wechat.node._configRestored = true;
|
|
522
|
+
}
|
|
523
|
+
})();
|
|
524
|
+
|
|
525
|
+
await wechat.node._restoringConfig;
|
|
526
|
+
|
|
511
527
|
if (cfg.token && cfg.accountId) {
|
|
512
528
|
wechat.gateway.setCredentials({
|
|
513
529
|
token: cfg.token,
|
|
@@ -522,6 +538,7 @@ function apply(ctx, config = {}) {
|
|
|
522
538
|
const disposeRpc = installBridgeRpc(ctx, {
|
|
523
539
|
service,
|
|
524
540
|
wechat,
|
|
541
|
+
platformManager,
|
|
525
542
|
logger,
|
|
526
543
|
saveCustomTunnelConfig: async (serverUrl, accessToken) => {
|
|
527
544
|
const stored = await loadConfig();
|
|
@@ -539,6 +556,7 @@ function apply(ctx, config = {}) {
|
|
|
539
556
|
ctx.effect(() => async () => {
|
|
540
557
|
try { disposeRpc(); } catch {}
|
|
541
558
|
await wechat.destroy();
|
|
559
|
+
platformManager.dispose();
|
|
542
560
|
await service.dispose();
|
|
543
561
|
}, 'dsh-bridge: stop wechat, proxy and tunnels');
|
|
544
562
|
}
|