@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/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-button-primary-fill,var(--dsw-alias-brand-primary,#4f6ef7))', color: 'var(--dsw-alias-button-primary-label,#fff)', height: 32, padding: '0 14px', borderRadius: 999, fontSize: 13, fontWeight: 500, display: 'inline-flex', alignItems: 'center', gap: 4 },
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
- // ---- 微信 Bot 卡片 ----
216
+ // ---- 通用 IM 平台卡片 ----
217
217
 
218
- function WechatCard({ rpcCall, onStatusChange }) {
219
- const [wx, setWx] = React.useState(null);
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 (wx?.config && !cfgDraft) {
228
+ if (platform?.config && !cfgDraft) {
229
229
  setCfgDraft({
230
- digestIntervalSec: String(wx.config.digestIntervalSec ?? 300),
231
- approvalTimeoutSec: String(wx.config.approvalTimeoutSec ?? 600),
232
- maxMessageChars: String(wx.config.maxMessageChars ?? 2000),
233
- sendChunkDelayMs: String(wx.config.sendChunkDelayMs ?? 1500),
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
- }, [wx?.config]);
236
+ }, [platform?.config]);
237
237
 
238
238
  // 向上传递连接状态(供平台列表卡片绿点使用)
239
239
  React.useEffect(() => {
240
- const connected = wx?.status === 'connected' || wx?.status === 'starting' || wx?.status === 'reconnecting';
240
+ const connected = platform?.status === 'connected' || platform?.status === 'starting' || platform?.status === 'reconnecting';
241
241
  onStatusChange?.(connected);
242
- }, [wx?.status, onStatusChange]);
242
+ }, [platform?.status, onStatusChange]);
243
243
 
244
244
  const load = React.useCallback(async (quiet = false) => {
245
245
  try {
246
- const r = await rpcCall(BRIDGE_ENDPOINTS.wechatGetStatus, {});
246
+ // 用通用端点读取平台状态(不执行登录操作,只获取状态)
247
+ const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
247
248
  if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
248
- setWx(r.value);
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 = wx?.login && (wx.login.phase === 'qr' || wx.login.phase === 'scaned');
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, wx?.login?.phase]);
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
- setWx(r.value);
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.wechatLogin, {}), [act]);
280
- const onStop = React.useCallback(() => act(BRIDGE_ENDPOINTS.wechatStop, {}), [act]);
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 = [...(wx?.allowFrom ?? []), id];
288
- await act(BRIDGE_ENDPOINTS.wechatSetAllowFrom, { allowFrom: list });
289
+ const list = [...(platform?.allowFrom ?? []), id];
290
+ await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
289
291
  setNewId('');
290
- }, [act, newId, wx?.allowFrom]);
292
+ }, [act, newId, platform?.allowFrom]);
291
293
  const removeAllow = React.useCallback(async (id) => {
292
- const list = (wx?.allowFrom ?? []).filter((x) => x !== id);
293
- await act(BRIDGE_ENDPOINTS.wechatSetAllowFrom, { allowFrom: list });
294
- }, [act, wx?.allowFrom]);
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.wechatSetConfig, {
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 && wx?.config && (
308
- Number(cfgDraft.digestIntervalSec) !== wx.config.digestIntervalSec ||
309
- Number(cfgDraft.approvalTimeoutSec) !== wx.config.approvalTimeoutSec ||
310
- Number(cfgDraft.maxMessageChars) !== wx.config.maxMessageChars ||
311
- Number(cfgDraft.sendChunkDelayMs) !== wx.config.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 (!wx && !err) {
316
+ if (!platform && !err) {
315
317
  return React.createElement('div', { style: s.card },
316
- React.createElement('div', { style: s.label }, '微信 Bot'),
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 = wx?.status === 'connected' || wx?.status === 'starting';
322
- const login = wx?.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 = wx?.status === 'connected' ? '已连接'
325
- : wx?.status === 'starting' ? '连接中…'
326
- : wx?.status === 'reconnecting' ? '重连中…'
327
- : wx?.status === 'paused' ? '暂停(会话过期)'
328
- : wx?.status === 'error' ? '错误'
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 }, '微信 Bot'),
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
- wx?.configured && React.createElement('div', { style: s.block },
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
- wx.accountId && React.createElement('div', null, `账号: ${wx.accountId}`),
375
- wx.sessionId && React.createElement('div', null, `当前会话: ${wx.sessionId}`),
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
- '白名单(仅这些微信用户可驱动 agent):'
378
+ '白名单(仅这些用户可驱动 agent):'
379
379
  ),
380
380
  React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 } },
381
- (wx.allowFrom?.length
382
- ? wx.allowFrom.map((id) =>
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
- wx.status !== 'connected' && wx.status !== 'starting' &&
407
- React.createElement('button', { style: s.btnPri, onClick: onLogin, disabled: busy }, '重新扫码'),
408
- (wx.status === 'connected' || wx.status === 'starting') &&
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('确认解绑?这将清除登录凭证,下次需重新扫码登录。')) act(BRIDGE_ENDPOINTS.wechatUnbind, {}); },
414
- title: '清除登录凭证,下次需重新扫码',
417
+ onClick: () => { if (window.confirm('确认解绑?这将清除登录凭证,下次需重新登录。')) act(BRIDGE_ENDPOINTS.platformUnbind, {}); },
418
+ title: '清除登录凭证,下次需重新登录',
415
419
  }, '解绑账号'),
416
420
  ),
417
421
  ),
418
422
 
419
423
  // 未配置 / 登录中:二维码
420
- (!wx?.configured || showQr) && React.createElement('div', { style: s.block },
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: 'wechat QR', style: s.qr }),
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' ? '已扫码,请在手机上确认…' : '请使用微信扫码登录(ClawBot)'
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
- '说明: 扫码成功后,向该微信 Bot 发送第一条消息即自动完成白名单授权。仅白名单内的微信用户能驱动 agent,其他人消息会被忽略。使用专用微信号,避免影响主号。'
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
- // 微信连接状态:独立轮询,不依赖 WechatCard 是否挂载
653
- const [wechatConnected, setWechatConnected] = React.useState(false);
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
- // 独立轮询微信连接状态(与 getStatus 解耦,Tab 未选中时也能更新绿点)
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.wechatGetStatus, {});
680
+ const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
672
681
  if (alive && r?.ok) {
673
- const s = r.value?.status;
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 状态点:im 用 WechatCard 上报的准确状态,其余从 getStatus 读
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: wechatConnected,
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)', available: true, active: wechatConnected },
775
- { id: 'qq', label: 'QQ', desc: 'NapCat / Mirai', available: false, active: false },
776
- { id: 'feishu', label: '飞书', desc: '官方事件回调 API', available: false, active: false },
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, available, active }) =>
784
- React.createElement('div', {
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 ? 'default' : 'not-allowed',
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
- // 微信卡片(onStatusChange 向上报连接状态)
813
- React.createElement(WechatCard, { rpcCall, onStatusChange: setWechatConnected }),
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
- // ---- 微信 Bot ----
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 failed: %s', err?.message ?? err);
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
- // 微信 Bot(ClawBot/iLink)
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
- // 恢复活动会话 ID(直接覆盖,不判断当前值)
504
- if (cfg.activeSessionId) {
505
- wechat.node.activeSessionId = cfg.activeSessionId;
506
- logger.info('dsh-bridge: restored wechat active session: %s', cfg.activeSessionId);
507
- } else {
508
- // 没有持久化的会话时,回退到选第一个
509
- wechat.node._pickDefaultSession().catch(() => {});
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
  }