@wenbin_wb/dsh-bridge 1.2.5 → 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 CHANGED
@@ -33,8 +33,9 @@ Seamlessly extend your local DeepSeek Harness to mobile phones, tablets, public
33
33
 
34
34
  | Target | Description | Status |
35
35
  |--------|-------------|--------|
36
- | **WeChat** | Chat with your Agent directly in WeChat | Supported (workspaces / persisted sessions / media / approvals) |
37
- | **QQ Bot** | QQ bot integration for group/private chat | Planned |
36
+ | **Platform Abstraction** | Platform-agnostic core (sessions / approvals / commands / digest) shared across IM channels | **Completed** (v2.0.0) |
37
+ | **WeChat** | Chat with your Agent directly in WeChat | Supported (workspaces / persisted sessions / media / approvals) |
38
+ | **QQ Bot** | QQ bot integration for group/private chat | 🚧 Next (platform adapter implementation) |
38
39
  | **Feishu** | Feishu message/bot integration for workplace scenarios | Planned |
39
40
  | **OpenClaw** | Integration with OpenClaw ecosystem | Planned |
40
41
  | **Telegram** | Self-hosted IM channel | Planned |
package/README.zh-CN.md CHANGED
@@ -33,8 +33,9 @@
33
33
 
34
34
  | 目标 | 说明 | 状态 |
35
35
  |------|------|------|
36
- | **微信** | 在微信里直接与你的 Agent 对话 | 已支持(多工作区 / 会话持久化 / 媒体 / 审批) |
37
- | **QQ Bot** | 接入 QQ 机器人,群聊/私聊唤起 Agent | 规划中 |
36
+ | **平台抽象层** | 平台无关的核心(会话/审批/命令/digest)跨 IM 渠道复用 | **已完成**(v2.0.0) |
37
+ | **微信** | 在微信里直接与你的 Agent 对话 | 已支持(多工作区 / 会话持久化 / 媒体 / 审批) |
38
+ | **QQ Bot** | 接入 QQ 机器人,群聊/私聊唤起 Agent | 🚧 下一步(平台适配器实现) |
38
39
  | **飞书** | 飞书消息/机器人集成,办公场景直接调用 | 规划中 |
39
40
  | **OpenClaw** | 与 OpenClaw 生态打通 | 规划中 |
40
41
  | **Telegram** | 适合自托管的 IM 渠道 | 规划中 |
package/client/client.js CHANGED
@@ -42,6 +42,15 @@ var BRIDGE_ENDPOINTS = {
42
42
  resetCloudflared: "resetCloudflared",
43
43
  saveCustomTunnelConfig: "saveCustomTunnelConfig",
44
44
  checkVersion: "checkVersion",
45
+ // 平台管理器(多 IM 平台统一接口)
46
+ listPlatforms: "listPlatforms",
47
+ platformLogin: "platformLogin",
48
+ platformSetAllowFrom: "platformSetAllowFrom",
49
+ platformSetConfig: "platformSetConfig",
50
+ platformStop: "platformStop",
51
+ platformStart: "platformStart",
52
+ platformUnbind: "platformUnbind",
53
+ // 微信 Bot(v1.x 向后兼容别名,deprecated)
45
54
  wechatGetStatus: "wechatGetStatus",
46
55
  wechatLogin: "wechatLogin",
47
56
  wechatSetAllowFrom: "wechatSetAllowFrom",
@@ -267,50 +276,51 @@ var TunnelCard = React.memo(function TunnelCard2({ title, desc, data, onStart, o
267
276
  )
268
277
  );
269
278
  });
270
- function WechatCard({ rpcCall, onStatusChange }) {
271
- const [wx, setWx] = React.useState(null);
279
+ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatusChange }) {
280
+ const [platform, setPlatform] = React.useState(null);
272
281
  const [err, setErr] = React.useState(null);
273
282
  const [busy, setBusy] = React.useState(false);
274
283
  const [showAdvanced, setShowAdvanced] = React.useState(false);
275
284
  const [showHelp, setShowHelp] = React.useState(false);
276
285
  const [cfgDraft, setCfgDraft] = React.useState(null);
277
286
  React.useEffect(() => {
278
- if (wx?.config && !cfgDraft) {
287
+ if (platform?.config && !cfgDraft) {
279
288
  setCfgDraft({
280
- digestIntervalSec: String(wx.config.digestIntervalSec ?? 300),
281
- approvalTimeoutSec: String(wx.config.approvalTimeoutSec ?? 600),
282
- maxMessageChars: String(wx.config.maxMessageChars ?? 2e3),
283
- sendChunkDelayMs: String(wx.config.sendChunkDelayMs ?? 1500)
289
+ digestIntervalSec: String(platform.config.digestIntervalSec ?? 300),
290
+ approvalTimeoutSec: String(platform.config.approvalTimeoutSec ?? 600),
291
+ maxMessageChars: String(platform.config.maxMessageChars ?? 2e3),
292
+ sendChunkDelayMs: String(platform.config.sendChunkDelayMs ?? 1500)
284
293
  });
285
294
  }
286
- }, [wx?.config]);
295
+ }, [platform?.config]);
287
296
  React.useEffect(() => {
288
- const connected2 = wx?.status === "connected" || wx?.status === "starting" || wx?.status === "reconnecting";
297
+ const connected2 = platform?.status === "connected" || platform?.status === "starting" || platform?.status === "reconnecting";
289
298
  onStatusChange?.(connected2);
290
- }, [wx?.status, onStatusChange]);
299
+ }, [platform?.status, onStatusChange]);
291
300
  const load = React.useCallback(async (quiet = false) => {
292
301
  try {
293
- const r = await rpcCall(BRIDGE_ENDPOINTS.wechatGetStatus, {});
302
+ const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
294
303
  if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
295
- setWx(r.value);
304
+ const allPlatforms = r.value ?? {};
305
+ setPlatform(allPlatforms[platformId] ?? null);
296
306
  if (!quiet) setErr(null);
297
307
  } catch (e) {
298
308
  if (!quiet) setErr(e.message);
299
309
  }
300
- }, [rpcCall]);
310
+ }, [rpcCall, platformId]);
301
311
  React.useEffect(() => {
302
312
  load();
303
- const activeLogin = wx?.login && (wx.login.phase === "qr" || wx.login.phase === "scaned");
313
+ const activeLogin = platform?.login && (platform.login.phase === "qr" || platform.login.phase === "scaned");
304
314
  const interval = activeLogin ? 1500 : 3e3;
305
315
  const t = setInterval(() => load(true), interval);
306
316
  return () => clearInterval(t);
307
- }, [load, wx?.login?.phase]);
317
+ }, [load, platform?.login?.phase]);
308
318
  const act = React.useCallback(async (endpoint, payload) => {
309
319
  setBusy(true);
310
320
  try {
311
- const r = await rpcCall(endpoint, payload ?? {});
321
+ const r = await rpcCall(endpoint, { platformId, ...payload });
312
322
  if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
313
- setWx(r.value);
323
+ setPlatform(r.value);
314
324
  setErr(null);
315
325
  await load(true);
316
326
  } catch (e) {
@@ -318,44 +328,44 @@ function WechatCard({ rpcCall, onStatusChange }) {
318
328
  } finally {
319
329
  setBusy(false);
320
330
  }
321
- }, [rpcCall, load]);
322
- const onLogin = React.useCallback(() => act(BRIDGE_ENDPOINTS.wechatLogin, {}), [act]);
323
- const onStop = React.useCallback(() => act(BRIDGE_ENDPOINTS.wechatStop, {}), [act]);
331
+ }, [rpcCall, load, platformId]);
332
+ const onLogin = React.useCallback(() => act(BRIDGE_ENDPOINTS.platformLogin, {}), [act]);
333
+ const onStop = React.useCallback(() => act(BRIDGE_ENDPOINTS.platformStop, {}), [act]);
324
334
  const [newId, setNewId] = React.useState("");
325
335
  const addAllow = React.useCallback(async () => {
326
336
  const id = newId.trim();
327
337
  if (!id) return;
328
- const list = [...wx?.allowFrom ?? [], id];
329
- await act(BRIDGE_ENDPOINTS.wechatSetAllowFrom, { allowFrom: list });
338
+ const list = [...platform?.allowFrom ?? [], id];
339
+ await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
330
340
  setNewId("");
331
- }, [act, newId, wx?.allowFrom]);
341
+ }, [act, newId, platform?.allowFrom]);
332
342
  const removeAllow = React.useCallback(async (id) => {
333
- const list = (wx?.allowFrom ?? []).filter((x) => x !== id);
334
- await act(BRIDGE_ENDPOINTS.wechatSetAllowFrom, { allowFrom: list });
335
- }, [act, wx?.allowFrom]);
343
+ const list = (platform?.allowFrom ?? []).filter((x) => x !== id);
344
+ await act(BRIDGE_ENDPOINTS.platformSetAllowFrom, { allowFrom: list });
345
+ }, [act, platform?.allowFrom]);
336
346
  const handleNewId = React.useCallback((e) => setNewId(e.target.value), []);
337
347
  const saveConfig = React.useCallback(async () => {
338
348
  if (!cfgDraft) return;
339
- await act(BRIDGE_ENDPOINTS.wechatSetConfig, {
349
+ await act(BRIDGE_ENDPOINTS.platformSetConfig, {
340
350
  digestIntervalSec: Number(cfgDraft.digestIntervalSec),
341
351
  approvalTimeoutSec: Number(cfgDraft.approvalTimeoutSec),
342
352
  maxMessageChars: Number(cfgDraft.maxMessageChars),
343
353
  sendChunkDelayMs: Number(cfgDraft.sendChunkDelayMs)
344
354
  });
345
355
  }, [act, cfgDraft]);
346
- const cfgDirty = cfgDraft && wx?.config && (Number(cfgDraft.digestIntervalSec) !== wx.config.digestIntervalSec || Number(cfgDraft.approvalTimeoutSec) !== wx.config.approvalTimeoutSec || Number(cfgDraft.maxMessageChars) !== wx.config.maxMessageChars || Number(cfgDraft.sendChunkDelayMs) !== wx.config.sendChunkDelayMs);
347
- if (!wx && !err) {
356
+ const cfgDirty = cfgDraft && platform?.config && (Number(cfgDraft.digestIntervalSec) !== platform.config.digestIntervalSec || Number(cfgDraft.approvalTimeoutSec) !== platform.config.approvalTimeoutSec || Number(cfgDraft.maxMessageChars) !== platform.config.maxMessageChars || Number(cfgDraft.sendChunkDelayMs) !== platform.config.sendChunkDelayMs);
357
+ if (!platform && !err) {
348
358
  return React.createElement(
349
359
  "div",
350
360
  { style: s.card },
351
- React.createElement("div", { style: s.label }, "\u5FAE\u4FE1 Bot"),
361
+ React.createElement("div", { style: s.label }, platformName),
352
362
  React.createElement("div", { style: { ...s.muted, marginTop: 6 } }, "\u52A0\u8F7D\u4E2D\u2026")
353
363
  );
354
364
  }
355
- const connected = wx?.status === "connected" || wx?.status === "starting";
356
- const login = wx?.login ?? {};
365
+ const connected = platform?.status === "connected" || platform?.status === "starting";
366
+ const login = platform?.login ?? {};
357
367
  const showQr = login.phase === "qr" || login.phase === "scaned";
358
- const statusLabel = wx?.status === "connected" ? "\u5DF2\u8FDE\u63A5" : wx?.status === "starting" ? "\u8FDE\u63A5\u4E2D\u2026" : wx?.status === "reconnecting" ? "\u91CD\u8FDE\u4E2D\u2026" : wx?.status === "paused" ? "\u6682\u505C\uFF08\u4F1A\u8BDD\u8FC7\u671F\uFF09" : wx?.status === "error" ? "\u9519\u8BEF" : "\u672A\u8FDE\u63A5";
368
+ const statusLabel = platform?.status === "connected" ? "\u5DF2\u8FDE\u63A5" : platform?.status === "starting" ? "\u8FDE\u63A5\u4E2D\u2026" : platform?.status === "reconnecting" ? "\u91CD\u8FDE\u4E2D\u2026" : platform?.status === "paused" ? "\u6682\u505C\uFF08\u4F1A\u8BDD\u8FC7\u671F\uFF09" : platform?.status === "error" ? "\u9519\u8BEF" : "\u672A\u8FDE\u63A5";
359
369
  return React.createElement(
360
370
  "div",
361
371
  { style: s.card },
@@ -365,12 +375,8 @@ function WechatCard({ rpcCall, onStatusChange }) {
365
375
  React.createElement(
366
376
  "div",
367
377
  null,
368
- React.createElement("div", { style: s.label }, "\u5FAE\u4FE1 Bot"),
369
- React.createElement(
370
- "div",
371
- { style: { ...s.muted, marginTop: 2 } },
372
- "\u901A\u8FC7\u5FAE\u4FE1\u626B ClawBot \u4E8C\u7EF4\u7801\uFF0C\u5728\u5FAE\u4FE1\u91CC\u8FDC\u7A0B\u5BF9\u8BDD\u548C\u63A7\u5236 DSH agent"
373
- )
378
+ React.createElement("div", { style: s.label }, platformName),
379
+ React.createElement("div", { style: { ...s.muted, marginTop: 2 } }, platformDesc)
374
380
  ),
375
381
  React.createElement(StatusTag, { running: connected })
376
382
  ),
@@ -378,7 +384,7 @@ function WechatCard({ rpcCall, onStatusChange }) {
378
384
  React.createElement(
379
385
  "div",
380
386
  { style: { display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap", alignItems: "center" } },
381
- React.createElement("a", {
387
+ platformId === "wechat" && React.createElement("a", {
382
388
  href: "https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/wechat-usage.md",
383
389
  target: "_blank",
384
390
  rel: "noopener noreferrer",
@@ -387,7 +393,7 @@ function WechatCard({ rpcCall, onStatusChange }) {
387
393
  React.createElement("button", {
388
394
  style: s.btnGhost,
389
395
  onClick: () => setShowHelp((v) => !v)
390
- }, showHelp ? "\u6536\u8D77\u547D\u4EE4" : "\u5FAE\u4FE1\u547D\u4EE4")
396
+ }, showHelp ? "\u6536\u8D77\u547D\u4EE4" : "\u547D\u4EE4\u5217\u8868")
391
397
  ),
392
398
  // 命令速查
393
399
  showHelp && React.createElement(
@@ -405,25 +411,25 @@ function WechatCard({ rpcCall, onStatusChange }) {
405
411
  ),
406
412
  err && React.createElement("div", { style: { ...s.warn, marginTop: 10 } }, err),
407
413
  // 已配置:状态详情 + 白名单
408
- wx?.configured && React.createElement(
414
+ platform?.configured && React.createElement(
409
415
  "div",
410
416
  { style: s.block },
411
417
  React.createElement(
412
418
  "div",
413
419
  { style: { fontSize: 12, lineHeight: 1.7 } },
414
420
  React.createElement("div", null, `\u72B6\u6001: ${statusLabel}`),
415
- wx.accountId && React.createElement("div", null, `\u8D26\u53F7: ${wx.accountId}`),
416
- wx.sessionId && React.createElement("div", null, `\u5F53\u524D\u4F1A\u8BDD: ${wx.sessionId}`)
421
+ platform.accountId && React.createElement("div", null, `\u8D26\u53F7: ${platform.accountId}`),
422
+ platform.sessionId && React.createElement("div", null, `\u5F53\u524D\u4F1A\u8BDD: ${platform.sessionId}`)
417
423
  ),
418
424
  React.createElement(
419
425
  "div",
420
426
  { style: { ...s.muted, fontSize: 12, marginTop: 8, lineHeight: 1.6 } },
421
- "\u767D\u540D\u5355\uFF08\u4EC5\u8FD9\u4E9B\u5FAE\u4FE1\u7528\u6237\u53EF\u9A71\u52A8 agent\uFF09:"
427
+ "\u767D\u540D\u5355\uFF08\u4EC5\u8FD9\u4E9B\u7528\u6237\u53EF\u9A71\u52A8 agent\uFF09:"
422
428
  ),
423
429
  React.createElement(
424
430
  "div",
425
431
  { style: { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 6 } },
426
- wx.allowFrom?.length ? wx.allowFrom.map(
432
+ platform.allowFrom?.length ? platform.allowFrom.map(
427
433
  (id) => React.createElement(
428
434
  "span",
429
435
  { key: id, style: { ...s.tag, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", color: "var(--dsw-alias-label-primary,currentColor)", gap: 6 } },
@@ -434,14 +440,18 @@ function WechatCard({ rpcCall, onStatusChange }) {
434
440
  title: "\u79FB\u51FA\u767D\u540D\u5355"
435
441
  }, "\xD7")
436
442
  )
437
- ) : React.createElement("div", { style: { ...s.muted, fontSize: 12 } }, "(\u7A7A \u2014 \u626B\u7801\u540E\u9996\u4E2A\u53D1\u6D88\u606F\u7684\u5FAE\u4FE1\u7528\u6237\u5C06\u81EA\u52A8\u52A0\u5165)")
443
+ ) : React.createElement(
444
+ "div",
445
+ { style: { ...s.muted, fontSize: 12 } },
446
+ platformId === "wechat" ? "(\u7A7A \u2014 \u626B\u7801\u540E\u9996\u4E2A\u53D1\u6D88\u606F\u7684\u5FAE\u4FE1\u7528\u6237\u5C06\u81EA\u52A8\u52A0\u5165)" : "(\u7A7A \u2014 \u9996\u4E2A\u53D1\u6D88\u606F\u7684\u7528\u6237\u5C06\u81EA\u52A8\u52A0\u5165)"
447
+ )
438
448
  ),
439
449
  React.createElement(
440
450
  "div",
441
451
  { style: { display: "flex", gap: 8, marginTop: 8, alignItems: "center" } },
442
452
  React.createElement("input", {
443
453
  style: { ...s.input, flex: 1 },
444
- placeholder: "\u6DFB\u52A0\u5141\u8BB8\u7684\u5FAE\u4FE1 ID\uFF08\u5982 xxx@im.wechat\uFF09",
454
+ placeholder: platformId === "wechat" ? "\u6DFB\u52A0\u5141\u8BB8\u7684\u5FAE\u4FE1 ID\uFF08\u5982 xxx@im.wechat\uFF09" : "\u6DFB\u52A0\u5141\u8BB8\u7684\u7528\u6237 ID",
445
455
  value: newId,
446
456
  onChange: handleNewId
447
457
  }),
@@ -454,30 +464,30 @@ function WechatCard({ rpcCall, onStatusChange }) {
454
464
  React.createElement(
455
465
  "div",
456
466
  { style: { display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap", alignItems: "center" } },
457
- wx.status !== "connected" && wx.status !== "starting" && React.createElement("button", { style: s.btnPri, onClick: onLogin, disabled: busy }, "\u91CD\u65B0\u626B\u7801"),
458
- (wx.status === "connected" || wx.status === "starting") && React.createElement("button", { style: s.btnGhost, onClick: onStop, disabled: busy }, "\u65AD\u5F00"),
467
+ platform.status !== "connected" && platform.status !== "starting" && React.createElement("button", { style: s.btnPri, onClick: onLogin, disabled: busy }, "\u91CD\u65B0\u767B\u5F55"),
468
+ (platform.status === "connected" || platform.status === "starting") && React.createElement("button", { style: s.btnGhost, onClick: onStop, disabled: busy }, "\u65AD\u5F00"),
459
469
  React.createElement("button", {
460
470
  style: { ...s.btnGhost, color: "var(--dsw-alias-state-error-primary,#dc2626)", borderColor: "var(--dsw-alias-state-error-primary,#dc2626)", opacity: busy ? 0.5 : 1 },
461
471
  disabled: busy,
462
472
  onClick: () => {
463
- if (window.confirm("\u786E\u8BA4\u89E3\u7ED1\uFF1F\u8FD9\u5C06\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u626B\u7801\u767B\u5F55\u3002")) act(BRIDGE_ENDPOINTS.wechatUnbind, {});
473
+ if (window.confirm("\u786E\u8BA4\u89E3\u7ED1\uFF1F\u8FD9\u5C06\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u767B\u5F55\u3002")) act(BRIDGE_ENDPOINTS.platformUnbind, {});
464
474
  },
465
- title: "\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u626B\u7801"
475
+ title: "\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u767B\u5F55"
466
476
  }, "\u89E3\u7ED1\u8D26\u53F7")
467
477
  )
468
478
  ),
469
479
  // 未配置 / 登录中:二维码
470
- (!wx?.configured || showQr) && React.createElement(
480
+ (!platform?.configured || showQr) && React.createElement(
471
481
  "div",
472
482
  { style: s.block },
473
483
  showQr && login.qr ? React.createElement(
474
484
  "div",
475
485
  null,
476
- React.createElement("img", { src: login.qr, alt: "wechat QR", style: s.qr }),
486
+ React.createElement("img", { src: login.qr, alt: "login QR", style: s.qr }),
477
487
  React.createElement(
478
488
  "div",
479
489
  { style: { ...s.muted, marginTop: 4 } },
480
- login.phase === "scaned" ? "\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u624B\u673A\u4E0A\u786E\u8BA4\u2026" : "\u8BF7\u4F7F\u7528\u5FAE\u4FE1\u626B\u7801\u767B\u5F55\uFF08ClawBot\uFF09"
490
+ login.phase === "scaned" ? "\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u624B\u673A\u4E0A\u786E\u8BA4\u2026" : platformId === "wechat" ? "\u8BF7\u4F7F\u7528\u5FAE\u4FE1\u626B\u7801\u767B\u5F55\uFF08ClawBot\uFF09" : "\u8BF7\u626B\u7801\u767B\u5F55"
481
491
  ),
482
492
  login.error && React.createElement("div", { style: { ...s.muted, marginTop: 4, color: "var(--dsw-alias-state-warn-primary,#92400e)" } }, login.error)
483
493
  ) : React.createElement(
@@ -571,7 +581,7 @@ function WechatCard({ rpcCall, onStatusChange }) {
571
581
  React.createElement(
572
582
  "div",
573
583
  { style: { ...s.tip, fontSize: 12 } },
574
- "\u8BF4\u660E: \u626B\u7801\u6210\u529F\u540E\uFF0C\u5411\u8BE5\u5FAE\u4FE1 Bot \u53D1\u9001\u7B2C\u4E00\u6761\u6D88\u606F\u5373\u81EA\u52A8\u5B8C\u6210\u767D\u540D\u5355\u6388\u6743\u3002\u4EC5\u767D\u540D\u5355\u5185\u7684\u5FAE\u4FE1\u7528\u6237\u80FD\u9A71\u52A8 agent\uFF0C\u5176\u4ED6\u4EBA\u6D88\u606F\u4F1A\u88AB\u5FFD\u7565\u3002\u4F7F\u7528\u4E13\u7528\u5FAE\u4FE1\u53F7\uFF0C\u907F\u514D\u5F71\u54CD\u4E3B\u53F7\u3002"
584
+ platformId === "wechat" ? "\u8BF4\u660E: \u626B\u7801\u6210\u529F\u540E\uFF0C\u5411\u8BE5\u5FAE\u4FE1 Bot \u53D1\u9001\u7B2C\u4E00\u6761\u6D88\u606F\u5373\u81EA\u52A8\u5B8C\u6210\u767D\u540D\u5355\u6388\u6743\u3002\u4EC5\u767D\u540D\u5355\u5185\u7684\u5FAE\u4FE1\u7528\u6237\u80FD\u9A71\u52A8 agent\uFF0C\u5176\u4ED6\u4EBA\u6D88\u606F\u4F1A\u88AB\u5FFD\u7565\u3002\u4F7F\u7528\u4E13\u7528\u5FAE\u4FE1\u53F7\uFF0C\u907F\u514D\u5F71\u54CD\u4E3B\u53F7\u3002" : "\u8BF4\u660E: \u767B\u5F55\u6210\u529F\u540E\uFF0C\u53D1\u9001\u7B2C\u4E00\u6761\u6D88\u606F\u5373\u81EA\u52A8\u5B8C\u6210\u767D\u540D\u5355\u6388\u6743\u3002\u4EC5\u767D\u540D\u5355\u5185\u7684\u7528\u6237\u80FD\u9A71\u52A8 agent\uFF0C\u5176\u4ED6\u4EBA\u6D88\u606F\u4F1A\u88AB\u5FFD\u7565\u3002"
575
585
  )
576
586
  )
577
587
  );
@@ -740,7 +750,8 @@ function BridgePanel({ rpcCall }) {
740
750
  const [status, setStatus] = React.useState(null);
741
751
  const [err, setErr] = React.useState(null);
742
752
  const [activeTab, setActiveTab] = React.useState("lan");
743
- const [wechatConnected, setWechatConnected] = React.useState(false);
753
+ const [platforms, setPlatforms] = React.useState(null);
754
+ const [selectedPlatform, setSelectedPlatform] = React.useState("wechat");
744
755
  const load = React.useCallback(async (quiet = false) => {
745
756
  try {
746
757
  const r = await rpcCall(BRIDGE_ENDPOINTS.getStatus, {});
@@ -755,10 +766,9 @@ function BridgePanel({ rpcCall }) {
755
766
  let alive = true;
756
767
  const poll = async () => {
757
768
  try {
758
- const r = await rpcCall(BRIDGE_ENDPOINTS.wechatGetStatus, {});
769
+ const r = await rpcCall(BRIDGE_ENDPOINTS.listPlatforms, {});
759
770
  if (alive && r?.ok) {
760
- const s2 = r.value?.status;
761
- setWechatConnected(s2 === "connected" || s2 === "starting" || s2 === "reconnecting");
771
+ setPlatforms(r.value ?? {});
762
772
  }
763
773
  } catch {
764
774
  }
@@ -803,10 +813,13 @@ function BridgePanel({ rpcCall }) {
803
813
  }, "\u52A0\u8F7D\u4E2D\u2026");
804
814
  }
805
815
  const ct = status?.customTunnel;
816
+ const imConnected = platforms && Object.values(platforms).some(
817
+ (p) => p.status === "connected" || p.status === "starting" || p.status === "reconnecting"
818
+ );
806
819
  const dots = {
807
820
  lan: !!status?.proxy?.running,
808
821
  tunnel: !!(status?.cloudflared?.running || ct?.running),
809
- im: wechatConnected
822
+ im: !!imConnected
810
823
  };
811
824
  let tabContent;
812
825
  if (activeTab === "lan") {
@@ -857,33 +870,38 @@ function BridgePanel({ rpcCall }) {
857
870
  );
858
871
  } else if (activeTab === "im") {
859
872
  const IM_PLATFORMS = [
860
- { id: "wechat", label: "\u5FAE\u4FE1", desc: "iLink Bot API\uFF08ClawBot\uFF09", available: true, active: wechatConnected },
861
- { id: "qq", label: "QQ", desc: "NapCat / Mirai", available: false, active: false },
862
- { id: "feishu", label: "\u98DE\u4E66", desc: "\u5B98\u65B9\u4E8B\u4EF6\u56DE\u8C03 API", available: false, active: false }
873
+ { id: "wechat", label: "\u5FAE\u4FE1", desc: "iLink Bot API\uFF08ClawBot\uFF09" },
874
+ { id: "qq", label: "QQ", desc: "NapCat / Mirai" },
875
+ { id: "feishu", label: "\u98DE\u4E66", desc: "\u5B98\u65B9\u4E8B\u4EF6\u56DE\u8C03 API" }
863
876
  ];
864
877
  tabContent = React.createElement(
865
878
  "div",
866
879
  null,
867
- // 平台选择器
880
+ // 平台选择器(可点击切换)
868
881
  React.createElement(
869
882
  "div",
870
883
  {
871
884
  style: { display: "flex", gap: 10, marginBottom: 16, flexWrap: "wrap" }
872
885
  },
873
- IM_PLATFORMS.map(
874
- ({ id, label, desc, available, active }) => React.createElement(
886
+ IM_PLATFORMS.map(({ id, label, desc }) => {
887
+ const platformData = platforms?.[id];
888
+ const available = !!platformData;
889
+ const active = platformData?.status === "connected" || platformData?.status === "starting" || platformData?.status === "reconnecting";
890
+ return React.createElement(
875
891
  "div",
876
892
  {
877
893
  key: id,
878
894
  style: {
879
895
  flex: "1 1 140px",
880
- border: `1px solid ${active ? "var(--dsw-alias-state-success-primary,#10b981)" : "var(--dsw-alias-border-l2,#e5e7eb)"}`,
896
+ 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)"}`,
881
897
  borderRadius: 10,
882
898
  padding: "12px 14px",
883
899
  opacity: available ? 1 : 0.45,
884
- cursor: available ? "default" : "not-allowed",
885
- background: active ? "var(--dsw-alias-state-success-bg,#ecfdf5)" : available ? "var(--dsw-alias-bg-layer-1,transparent)" : "var(--dsw-alias-bg-layer-2,#f9fafb)"
886
- }
900
+ cursor: available ? "pointer" : "not-allowed",
901
+ 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)",
902
+ transition: "all 0.15s ease"
903
+ },
904
+ onClick: available ? () => setSelectedPlatform(id) : void 0
887
905
  },
888
906
  React.createElement(
889
907
  "div",
@@ -900,11 +918,19 @@ function BridgePanel({ rpcCall }) {
900
918
  }, "\u5373\u5C06\u652F\u6301")
901
919
  ),
902
920
  React.createElement("div", { style: { ...s.muted, marginTop: 3, fontSize: 11 } }, desc)
903
- )
904
- )
921
+ );
922
+ })
905
923
  ),
906
- // 微信卡片(onStatusChange 向上报连接状态)
907
- React.createElement(WechatCard, { rpcCall, onStatusChange: setWechatConnected })
924
+ // 显示选中的平台卡片
925
+ selectedPlatform && platforms?.[selectedPlatform] && React.createElement(PlatformCard, {
926
+ platformId: selectedPlatform,
927
+ platformName: IM_PLATFORMS.find((p) => p.id === selectedPlatform)?.label ?? selectedPlatform,
928
+ platformDesc: IM_PLATFORMS.find((p) => p.id === selectedPlatform)?.desc ?? "",
929
+ rpcCall,
930
+ onStatusChange: () => {
931
+ }
932
+ // 状态变化已由 listPlatforms 轮询处理,不需要回调
933
+ })
908
934
  );
909
935
  }
910
936
  return React.createElement(