@wenbin_wb/dsh-bridge 1.0.8 → 1.2.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/client.js CHANGED
@@ -31,7 +31,7 @@ __export(index_exports, {
31
31
  });
32
32
  module.exports = __toCommonJS(index_exports);
33
33
 
34
- // lib/bridge-rpc.js
34
+ // lib/bridge-rpc-constants.js
35
35
  var BRIDGE_RPC_CHANNEL = "/dsh-bridge";
36
36
  var BRIDGE_ENDPOINTS = {
37
37
  getStatus: "getStatus",
@@ -41,7 +41,14 @@ var BRIDGE_ENDPOINTS = {
41
41
  stopCloudflared: "stopCloudflared",
42
42
  resetCloudflared: "resetCloudflared",
43
43
  saveCustomTunnelConfig: "saveCustomTunnelConfig",
44
- checkVersion: "checkVersion"
44
+ checkVersion: "checkVersion",
45
+ wechatGetStatus: "wechatGetStatus",
46
+ wechatLogin: "wechatLogin",
47
+ wechatSetAllowFrom: "wechatSetAllowFrom",
48
+ wechatSetConfig: "wechatSetConfig",
49
+ wechatStop: "wechatStop",
50
+ wechatStart: "wechatStart",
51
+ wechatUnbind: "wechatUnbind"
45
52
  };
46
53
 
47
54
  // client/index.js
@@ -243,6 +250,315 @@ var TunnelCard = React.memo(function TunnelCard2({ title, desc, data, onStart, o
243
250
  )
244
251
  );
245
252
  });
253
+ function WechatCard({ rpcCall, onStatusChange }) {
254
+ const [wx, setWx] = React.useState(null);
255
+ const [err, setErr] = React.useState(null);
256
+ const [busy, setBusy] = React.useState(false);
257
+ const [showAdvanced, setShowAdvanced] = React.useState(false);
258
+ const [showHelp, setShowHelp] = React.useState(false);
259
+ const [cfgDraft, setCfgDraft] = React.useState(null);
260
+ React.useEffect(() => {
261
+ if (wx?.config && !cfgDraft) {
262
+ setCfgDraft({
263
+ digestIntervalSec: String(wx.config.digestIntervalSec ?? 300),
264
+ approvalTimeoutSec: String(wx.config.approvalTimeoutSec ?? 600),
265
+ maxMessageChars: String(wx.config.maxMessageChars ?? 2e3),
266
+ sendChunkDelayMs: String(wx.config.sendChunkDelayMs ?? 1500)
267
+ });
268
+ }
269
+ }, [wx?.config]);
270
+ React.useEffect(() => {
271
+ const connected2 = wx?.status === "connected" || wx?.status === "starting" || wx?.status === "reconnecting";
272
+ onStatusChange?.(connected2);
273
+ }, [wx?.status, onStatusChange]);
274
+ const load = React.useCallback(async (quiet = false) => {
275
+ try {
276
+ const r = await rpcCall(BRIDGE_ENDPOINTS.wechatGetStatus, {});
277
+ if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
278
+ setWx(r.value);
279
+ if (!quiet) setErr(null);
280
+ } catch (e) {
281
+ if (!quiet) setErr(e.message);
282
+ }
283
+ }, [rpcCall]);
284
+ React.useEffect(() => {
285
+ load();
286
+ const activeLogin = wx?.login && (wx.login.phase === "qr" || wx.login.phase === "scaned");
287
+ const interval = activeLogin ? 1500 : 3e3;
288
+ const t = setInterval(() => load(true), interval);
289
+ return () => clearInterval(t);
290
+ }, [load, wx?.login?.phase]);
291
+ const act = React.useCallback(async (endpoint, payload) => {
292
+ setBusy(true);
293
+ try {
294
+ const r = await rpcCall(endpoint, payload ?? {});
295
+ if (!r?.ok) throw new Error(r?.error?.message ?? "RPC failed");
296
+ setWx(r.value);
297
+ setErr(null);
298
+ await load(true);
299
+ } catch (e) {
300
+ setErr(e.message);
301
+ } finally {
302
+ setBusy(false);
303
+ }
304
+ }, [rpcCall, load]);
305
+ const onLogin = React.useCallback(() => act(BRIDGE_ENDPOINTS.wechatLogin, {}), [act]);
306
+ const onStop = React.useCallback(() => act(BRIDGE_ENDPOINTS.wechatStop, {}), [act]);
307
+ const [newId, setNewId] = React.useState("");
308
+ const addAllow = React.useCallback(async () => {
309
+ const id = newId.trim();
310
+ if (!id) return;
311
+ const list = [...wx?.allowFrom ?? [], id];
312
+ await act(BRIDGE_ENDPOINTS.wechatSetAllowFrom, { allowFrom: list });
313
+ setNewId("");
314
+ }, [act, newId, wx?.allowFrom]);
315
+ const removeAllow = React.useCallback(async (id) => {
316
+ const list = (wx?.allowFrom ?? []).filter((x) => x !== id);
317
+ await act(BRIDGE_ENDPOINTS.wechatSetAllowFrom, { allowFrom: list });
318
+ }, [act, wx?.allowFrom]);
319
+ const handleNewId = React.useCallback((e) => setNewId(e.target.value), []);
320
+ const saveConfig = React.useCallback(async () => {
321
+ if (!cfgDraft) return;
322
+ await act(BRIDGE_ENDPOINTS.wechatSetConfig, {
323
+ digestIntervalSec: Number(cfgDraft.digestIntervalSec),
324
+ approvalTimeoutSec: Number(cfgDraft.approvalTimeoutSec),
325
+ maxMessageChars: Number(cfgDraft.maxMessageChars),
326
+ sendChunkDelayMs: Number(cfgDraft.sendChunkDelayMs)
327
+ });
328
+ }, [act, cfgDraft]);
329
+ 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);
330
+ if (!wx && !err) {
331
+ return React.createElement(
332
+ "div",
333
+ { style: s.card },
334
+ React.createElement("div", { style: s.label }, "\u5FAE\u4FE1 Bot"),
335
+ React.createElement("div", { style: { ...s.muted, marginTop: 6 } }, "\u52A0\u8F7D\u4E2D\u2026")
336
+ );
337
+ }
338
+ const connected = wx?.status === "connected" || wx?.status === "starting";
339
+ const login = wx?.login ?? {};
340
+ const showQr = login.phase === "qr" || login.phase === "scaned";
341
+ 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";
342
+ return React.createElement(
343
+ "div",
344
+ { style: s.card },
345
+ React.createElement(
346
+ "div",
347
+ { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } },
348
+ React.createElement(
349
+ "div",
350
+ null,
351
+ React.createElement("div", { style: s.label }, "\u5FAE\u4FE1 Bot"),
352
+ React.createElement(
353
+ "div",
354
+ { style: { ...s.muted, marginTop: 2 } },
355
+ "\u901A\u8FC7\u5FAE\u4FE1\u626B ClawBot \u4E8C\u7EF4\u7801\uFF0C\u5728\u5FAE\u4FE1\u91CC\u8FDC\u7A0B\u5BF9\u8BDD\u548C\u63A7\u5236 DSH agent"
356
+ )
357
+ ),
358
+ React.createElement(StatusTag, { running: connected })
359
+ ),
360
+ // 快捷入口:使用说明 / 命令
361
+ React.createElement(
362
+ "div",
363
+ { style: { display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap", alignItems: "center" } },
364
+ React.createElement("a", {
365
+ href: "https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/wechat-usage.md",
366
+ target: "_blank",
367
+ rel: "noopener noreferrer",
368
+ style: s.btnGhost
369
+ }, "\u{1F4D6} \u4F7F\u7528\u8BF4\u660E"),
370
+ React.createElement("button", {
371
+ style: s.btnGhost,
372
+ onClick: () => setShowHelp((v) => !v)
373
+ }, showHelp ? "\u6536\u8D77\u547D\u4EE4" : "\u5FAE\u4FE1\u547D\u4EE4")
374
+ ),
375
+ // 命令速查
376
+ showHelp && React.createElement(
377
+ "div",
378
+ { style: { ...s.block, fontSize: 12, lineHeight: 1.8, fontFamily: "monospace" } },
379
+ React.createElement("div", null, "/new <\u63D0\u793A\u8BCD> \u2014 \u65B0\u5EFA\u4F1A\u8BDD\uFF08\u5F53\u524D\u5DE5\u4F5C\u533A\uFF09"),
380
+ React.createElement("div", null, "/new <\u63D0\u793A\u8BCD> @N \u2014 \u5728\u6307\u5B9A\u5DE5\u4F5C\u533A\u65B0\u5EFA"),
381
+ React.createElement("div", null, "/sessions \u2014 \u6309\u5DE5\u4F5C\u533A\u5206\u7EC4\u5217\u4F1A\u8BDD"),
382
+ React.createElement("div", null, "/use N \u2014 \u5207\u6362\u5230\u4F1A\u8BDD N"),
383
+ React.createElement("div", null, "/workspaces \u2014 \u5217\u51FA\u5DE5\u4F5C\u533A"),
384
+ React.createElement("div", null, "/stop \u2014 \u505C\u6B62\u4EFB\u52A1"),
385
+ React.createElement("div", null, "/status \u2014 \u67E5\u770B\u72B6\u6001"),
386
+ React.createElement("div", null, "/yes \u6216 /no \u2014 \u56DE\u5E94\u5BA1\u6279"),
387
+ React.createElement("div", null, "/help \u2014 \u5168\u90E8\u547D\u4EE4")
388
+ ),
389
+ err && React.createElement("div", { style: { ...s.warn, marginTop: 10 } }, err),
390
+ // 已配置:状态详情 + 白名单
391
+ wx?.configured && React.createElement(
392
+ "div",
393
+ { style: s.block },
394
+ React.createElement(
395
+ "div",
396
+ { style: { fontSize: 12, lineHeight: 1.7 } },
397
+ React.createElement("div", null, `\u72B6\u6001: ${statusLabel}`),
398
+ wx.accountId && React.createElement("div", null, `\u8D26\u53F7: ${wx.accountId}`),
399
+ wx.sessionId && React.createElement("div", null, `\u5F53\u524D\u4F1A\u8BDD: ${wx.sessionId}`)
400
+ ),
401
+ React.createElement(
402
+ "div",
403
+ { style: { ...s.muted, fontSize: 12, marginTop: 8, lineHeight: 1.6 } },
404
+ "\u767D\u540D\u5355\uFF08\u4EC5\u8FD9\u4E9B\u5FAE\u4FE1\u7528\u6237\u53EF\u9A71\u52A8 agent\uFF09:"
405
+ ),
406
+ React.createElement(
407
+ "div",
408
+ { style: { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 6 } },
409
+ wx.allowFrom?.length ? wx.allowFrom.map(
410
+ (id) => React.createElement(
411
+ "span",
412
+ { key: id, style: { ...s.tag, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", color: "var(--dsw-alias-label-primary,currentColor)", gap: 6 } },
413
+ React.createElement("span", { style: { fontSize: 12, wordBreak: "break-all" } }, id),
414
+ React.createElement("button", {
415
+ style: { cursor: "pointer", border: "none", background: "none", color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, padding: 0 },
416
+ onClick: () => removeAllow(id),
417
+ title: "\u79FB\u51FA\u767D\u540D\u5355"
418
+ }, "\xD7")
419
+ )
420
+ ) : 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)")
421
+ ),
422
+ React.createElement(
423
+ "div",
424
+ { style: { display: "flex", gap: 8, marginTop: 8, alignItems: "center" } },
425
+ React.createElement("input", {
426
+ style: { ...s.input, flex: 1 },
427
+ placeholder: "\u6DFB\u52A0\u5141\u8BB8\u7684\u5FAE\u4FE1 ID\uFF08\u5982 xxx@im.wechat\uFF09",
428
+ value: newId,
429
+ onChange: handleNewId
430
+ }),
431
+ React.createElement("button", {
432
+ style: { ...s.btnGhost, whiteSpace: "nowrap", opacity: newId.trim() && !busy ? 1 : 0.5 },
433
+ onClick: addAllow,
434
+ disabled: busy || !newId.trim()
435
+ }, "\u6DFB\u52A0")
436
+ ),
437
+ React.createElement(
438
+ "div",
439
+ { style: { display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap", alignItems: "center" } },
440
+ wx.status !== "connected" && wx.status !== "starting" && React.createElement("button", { style: s.btnPri, onClick: onLogin, disabled: busy }, "\u91CD\u65B0\u626B\u7801"),
441
+ (wx.status === "connected" || wx.status === "starting") && React.createElement("button", { style: s.btnGhost, onClick: onStop, disabled: busy }, "\u65AD\u5F00"),
442
+ React.createElement("button", {
443
+ style: { ...s.btnGhost, color: "var(--dsw-alias-state-error-primary,#dc2626)", borderColor: "var(--dsw-alias-state-error-primary,#dc2626)", opacity: busy ? 0.5 : 1 },
444
+ disabled: busy,
445
+ onClick: () => {
446
+ 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, {});
447
+ },
448
+ title: "\u6E05\u9664\u767B\u5F55\u51ED\u8BC1\uFF0C\u4E0B\u6B21\u9700\u91CD\u65B0\u626B\u7801"
449
+ }, "\u89E3\u7ED1\u8D26\u53F7")
450
+ )
451
+ ),
452
+ // 未配置 / 登录中:二维码
453
+ (!wx?.configured || showQr) && React.createElement(
454
+ "div",
455
+ { style: s.block },
456
+ showQr && login.qr ? React.createElement(
457
+ "div",
458
+ null,
459
+ React.createElement("img", { src: login.qr, alt: "wechat QR", style: s.qr }),
460
+ React.createElement(
461
+ "div",
462
+ { style: { ...s.muted, marginTop: 4 } },
463
+ 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"
464
+ ),
465
+ login.error && React.createElement("div", { style: { ...s.muted, marginTop: 4, color: "var(--dsw-alias-state-warn-primary,#92400e)" } }, login.error)
466
+ ) : React.createElement(
467
+ "div",
468
+ { style: { display: "flex", gap: 8, marginTop: 4, flexWrap: "wrap", alignItems: "center" } },
469
+ React.createElement("button", {
470
+ style: { ...s.btnPri, opacity: busy ? 0.5 : 1 },
471
+ onClick: onLogin,
472
+ disabled: busy
473
+ }, busy ? "\u5904\u7406\u4E2D\u2026" : "\u626B\u7801\u767B\u5F55"),
474
+ login.phase === "error" && React.createElement("div", { style: { ...s.muted, fontSize: 12 } }, login.error ?? "\u767B\u5F55\u5931\u8D25")
475
+ )
476
+ ),
477
+ // 高级设置(可折叠)
478
+ cfgDraft && React.createElement(
479
+ "div",
480
+ { style: s.block },
481
+ React.createElement("button", {
482
+ style: { ...s.btnLink, fontSize: 12, marginBottom: showAdvanced ? 10 : 0 },
483
+ onClick: () => setShowAdvanced((v) => !v)
484
+ }, showAdvanced ? "\u25BE \u9AD8\u7EA7\u8BBE\u7F6E" : "\u25B8 \u9AD8\u7EA7\u8BBE\u7F6E"),
485
+ showAdvanced && React.createElement(
486
+ "div",
487
+ { style: { display: "flex", flexDirection: "column", gap: 10 } },
488
+ // 心跳间隔
489
+ React.createElement(
490
+ "div",
491
+ null,
492
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5FC3\u8DF3\u95F4\u9694\uFF08\u79D2\uFF09\u2014 \u957F\u4EFB\u52A1\u5904\u7406\u4E2D\u6BCF\u9694\u591A\u4E45\u53D1\u4E00\u6B21\u8FDB\u5EA6\u63D0\u793A"),
493
+ React.createElement("input", {
494
+ style: { ...s.input, width: 120 },
495
+ type: "number",
496
+ min: 30,
497
+ max: 3600,
498
+ value: cfgDraft.digestIntervalSec,
499
+ onChange: (e) => setCfgDraft((d) => ({ ...d, digestIntervalSec: e.target.value }))
500
+ })
501
+ ),
502
+ // 审批超时
503
+ React.createElement(
504
+ "div",
505
+ null,
506
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5BA1\u6279\u8D85\u65F6\uFF08\u79D2\uFF09\u2014 \u5DE5\u5177\u8C03\u7528\u5BA1\u6279\u65E0\u54CD\u5E94\u540E\u81EA\u52A8\u62D2\u7EDD"),
507
+ React.createElement("input", {
508
+ style: { ...s.input, width: 120 },
509
+ type: "number",
510
+ min: 30,
511
+ max: 86400,
512
+ value: cfgDraft.approvalTimeoutSec,
513
+ onChange: (e) => setCfgDraft((d) => ({ ...d, approvalTimeoutSec: e.target.value }))
514
+ })
515
+ ),
516
+ // 每气泡字数
517
+ React.createElement(
518
+ "div",
519
+ null,
520
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u6BCF\u6761\u6D88\u606F\u6700\u5927\u5B57\u6570 \u2014 \u8D85\u51FA\u65F6\u81EA\u52A8\u5206\u591A\u6761\u53D1\u9001"),
521
+ React.createElement("input", {
522
+ style: { ...s.input, width: 120 },
523
+ type: "number",
524
+ min: 100,
525
+ max: 1e4,
526
+ value: cfgDraft.maxMessageChars,
527
+ onChange: (e) => setCfgDraft((d) => ({ ...d, maxMessageChars: e.target.value }))
528
+ })
529
+ ),
530
+ // 分块延迟
531
+ React.createElement(
532
+ "div",
533
+ null,
534
+ React.createElement("div", { style: { ...s.muted, marginBottom: 4 } }, "\u5206\u5757\u53D1\u9001\u5EF6\u8FDF\uFF08\u6BEB\u79D2\uFF09\u2014 \u591A\u6761\u6D88\u606F\u4E4B\u95F4\u7684\u95F4\u9694"),
535
+ React.createElement("input", {
536
+ style: { ...s.input, width: 120 },
537
+ type: "number",
538
+ min: 0,
539
+ max: 1e4,
540
+ value: cfgDraft.sendChunkDelayMs,
541
+ onChange: (e) => setCfgDraft((d) => ({ ...d, sendChunkDelayMs: e.target.value }))
542
+ })
543
+ ),
544
+ React.createElement("button", {
545
+ style: { ...s.btnPri, alignSelf: "flex-start", opacity: cfgDirty && !busy ? 1 : 0.5 },
546
+ disabled: !cfgDirty || busy,
547
+ onClick: saveConfig
548
+ }, busy ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u8BBE\u7F6E")
549
+ )
550
+ ),
551
+ React.createElement(
552
+ "div",
553
+ { style: s.block },
554
+ React.createElement(
555
+ "div",
556
+ { style: { ...s.tip, fontSize: 12 } },
557
+ "\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"
558
+ )
559
+ )
560
+ );
561
+ }
246
562
  function VersionBanner({ rpcCall }) {
247
563
  const [info, setInfo] = React.useState(null);
248
564
  const [loading, setLoading] = React.useState(false);
@@ -345,9 +661,67 @@ function VersionBanner({ rpcCall }) {
345
661
  links
346
662
  );
347
663
  }
664
+ var TABS = [
665
+ { id: "lan", label: "\u5C40\u57DF\u7F51" },
666
+ { id: "tunnel", label: "\u516C\u7F51\u96A7\u9053" },
667
+ { id: "im", label: "IM \u673A\u5668\u4EBA" }
668
+ ];
669
+ function TabBar({ active, onChange, dots }) {
670
+ return React.createElement(
671
+ "div",
672
+ {
673
+ style: {
674
+ display: "flex",
675
+ gap: 0,
676
+ marginBottom: 20,
677
+ borderBottom: "1px solid var(--dsw-alias-border-l2,#e5e7eb)"
678
+ }
679
+ },
680
+ TABS.map(({ id, label }) => {
681
+ const isActive = active === id;
682
+ const hasDot = dots?.[id];
683
+ return React.createElement(
684
+ "button",
685
+ {
686
+ key: id,
687
+ onClick: () => onChange(id),
688
+ style: {
689
+ font: "inherit",
690
+ cursor: "pointer",
691
+ border: "none",
692
+ background: "none",
693
+ padding: "8px 16px",
694
+ fontSize: 13,
695
+ fontWeight: isActive ? 600 : 400,
696
+ color: isActive ? "var(--dsw-alias-brand-primary,#4f6ef7)" : "var(--dsw-alias-label-secondary,#6b7280)",
697
+ borderBottom: isActive ? "2px solid var(--dsw-alias-brand-primary,#4f6ef7)" : "2px solid transparent",
698
+ marginBottom: -1,
699
+ display: "inline-flex",
700
+ alignItems: "center",
701
+ gap: 6,
702
+ transition: "color .15s, border-color .15s",
703
+ whiteSpace: "nowrap"
704
+ }
705
+ },
706
+ label,
707
+ hasDot && React.createElement("span", {
708
+ style: {
709
+ width: 6,
710
+ height: 6,
711
+ borderRadius: "50%",
712
+ background: "var(--dsw-alias-state-success-primary,#10b981)",
713
+ flexShrink: 0
714
+ }
715
+ })
716
+ );
717
+ })
718
+ );
719
+ }
348
720
  function BridgePanel({ rpcCall }) {
349
721
  const [status, setStatus] = React.useState(null);
350
722
  const [err, setErr] = React.useState(null);
723
+ const [activeTab, setActiveTab] = React.useState("lan");
724
+ const [wechatConnected, setWechatConnected] = React.useState(false);
351
725
  const load = React.useCallback(async (quiet = false) => {
352
726
  try {
353
727
  const r = await rpcCall(BRIDGE_ENDPOINTS.getStatus, {});
@@ -358,6 +732,25 @@ function BridgePanel({ rpcCall }) {
358
732
  setErr(e.message);
359
733
  }
360
734
  }, [rpcCall]);
735
+ React.useEffect(() => {
736
+ let alive = true;
737
+ const poll = async () => {
738
+ try {
739
+ const r = await rpcCall(BRIDGE_ENDPOINTS.wechatGetStatus, {});
740
+ if (alive && r?.ok) {
741
+ const s2 = r.value?.status;
742
+ setWechatConnected(s2 === "connected" || s2 === "starting" || s2 === "reconnecting");
743
+ }
744
+ } catch {
745
+ }
746
+ };
747
+ poll();
748
+ const t = setInterval(poll, 4e3);
749
+ return () => {
750
+ alive = false;
751
+ clearInterval(t);
752
+ };
753
+ }, [rpcCall]);
361
754
  React.useEffect(() => {
362
755
  load();
363
756
  const t = setInterval(() => load(true), 3e3);
@@ -391,6 +784,110 @@ function BridgePanel({ rpcCall }) {
391
784
  }, "\u52A0\u8F7D\u4E2D\u2026");
392
785
  }
393
786
  const ct = status?.customTunnel;
787
+ const dots = {
788
+ lan: !!status?.proxy?.running,
789
+ tunnel: !!(status?.cloudflared?.running || ct?.running),
790
+ im: wechatConnected
791
+ };
792
+ let tabContent;
793
+ if (activeTab === "lan") {
794
+ tabContent = React.createElement(TunnelCard, {
795
+ title: "\u5C40\u57DF\u7F51\u8BBF\u95EE",
796
+ desc: "\u540C\u4E00 Wi-Fi \u4E0B\u7684\u8BBE\u5907\u53EF\u76F4\u63A5\u626B\u7801\u8BBF\u95EE",
797
+ data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr }
798
+ });
799
+ } else if (activeTab === "tunnel") {
800
+ tabContent = React.createElement(
801
+ React.Fragment,
802
+ null,
803
+ React.createElement(TunnelCard, {
804
+ title: "Cloudflare \u96A7\u9053",
805
+ desc: "\u4E00\u952E\u83B7\u53D6\u516C\u7F51\u5730\u5740\uFF08\u91CD\u542F\u540E URL \u4F1A\u53D8\u5316\uFF09",
806
+ data: {
807
+ running: status?.cloudflared?.running,
808
+ url: status?.cloudflared?.url,
809
+ qr: status?.cloudflared?.qr,
810
+ state: status?.cloudflared?.state
811
+ },
812
+ onStart: onStartCloudflared,
813
+ onStop: onStopCloudflared,
814
+ onReset: status?.cloudflared?.running ? onResetCloudflared : null
815
+ }),
816
+ React.createElement(
817
+ TunnelCard,
818
+ {
819
+ title: "\u81EA\u5EFA\u96A7\u9053",
820
+ desc: "\u8FDE\u63A5\u81EA\u5DF1\u90E8\u7F72\u7684\u96A7\u9053\u670D\u52A1\u5668\uFF0C\u83B7\u5F97\u56FA\u5B9A\u57DF\u540D",
821
+ data: {
822
+ configured: ct?.configured,
823
+ running: ct?.running,
824
+ url: ct?.url,
825
+ qr: ct?.qr,
826
+ state: ct?.state
827
+ },
828
+ onStart: onStartCustom,
829
+ onStop: onStopCustom
830
+ },
831
+ React.createElement(CustomTunnelGuide),
832
+ React.createElement(CustomTunnelConfigForm, {
833
+ serverUrl: ct?.serverUrl ?? "",
834
+ accessToken: ct?.accessToken ?? "",
835
+ onSave: saveConfig
836
+ })
837
+ )
838
+ );
839
+ } else if (activeTab === "im") {
840
+ const IM_PLATFORMS = [
841
+ { id: "wechat", label: "\u5FAE\u4FE1", desc: "iLink Bot API\uFF08ClawBot\uFF09", available: true, active: wechatConnected },
842
+ { id: "qq", label: "QQ", desc: "NapCat / Mirai", available: false, active: false },
843
+ { id: "feishu", label: "\u98DE\u4E66", desc: "\u5B98\u65B9\u4E8B\u4EF6\u56DE\u8C03 API", available: false, active: false }
844
+ ];
845
+ tabContent = React.createElement(
846
+ "div",
847
+ null,
848
+ // 平台选择器
849
+ React.createElement(
850
+ "div",
851
+ {
852
+ style: { display: "flex", gap: 10, marginBottom: 16, flexWrap: "wrap" }
853
+ },
854
+ IM_PLATFORMS.map(
855
+ ({ id, label, desc, available, active }) => React.createElement(
856
+ "div",
857
+ {
858
+ key: id,
859
+ style: {
860
+ flex: "1 1 140px",
861
+ border: `1px solid ${active ? "var(--dsw-alias-state-success-primary,#10b981)" : "var(--dsw-alias-border-l2,#e5e7eb)"}`,
862
+ borderRadius: 10,
863
+ padding: "12px 14px",
864
+ opacity: available ? 1 : 0.45,
865
+ cursor: available ? "default" : "not-allowed",
866
+ background: active ? "var(--dsw-alias-state-success-bg,#ecfdf5)" : available ? "var(--dsw-alias-bg-layer-1,transparent)" : "var(--dsw-alias-bg-layer-2,#f9fafb)"
867
+ }
868
+ },
869
+ React.createElement(
870
+ "div",
871
+ { style: { ...s.label, fontSize: 13, display: "flex", alignItems: "center", gap: 6 } },
872
+ label,
873
+ active && React.createElement("span", {
874
+ style: { width: 6, height: 6, borderRadius: "50%", background: "var(--dsw-alias-state-success-primary,#10b981)", flexShrink: 0 }
875
+ }),
876
+ !active && available && React.createElement("span", {
877
+ style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#6b7280)", fontWeight: 400 }
878
+ }, "\u672A\u8FDE\u63A5"),
879
+ !available && React.createElement("span", {
880
+ style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#9ca3af)", fontWeight: 400 }
881
+ }, "\u5373\u5C06\u652F\u6301")
882
+ ),
883
+ React.createElement("div", { style: { ...s.muted, marginTop: 3, fontSize: 11 } }, desc)
884
+ )
885
+ )
886
+ ),
887
+ // 微信卡片(onStatusChange 向上报连接状态)
888
+ React.createElement(WechatCard, { rpcCall, onStatusChange: setWechatConnected })
889
+ );
890
+ }
394
891
  return React.createElement(
395
892
  "div",
396
893
  { style: { maxWidth: 560 } },
@@ -398,46 +895,8 @@ function BridgePanel({ rpcCall }) {
398
895
  style: { ...s.card, background: "var(--dsw-alias-state-error-bg,#fef2f2)", color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 13, marginBottom: 16 }
399
896
  }, err),
400
897
  React.createElement(VersionBanner, { rpcCall }),
401
- React.createElement(TunnelCard, {
402
- title: "\u5C40\u57DF\u7F51\u8BBF\u95EE",
403
- desc: "\u540C\u4E00 Wi-Fi \u4E0B\u7684\u8BBE\u5907\u53EF\u76F4\u63A5\u626B\u7801\u8BBF\u95EE",
404
- data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr }
405
- }),
406
- React.createElement(TunnelCard, {
407
- title: "Cloudflare \u96A7\u9053",
408
- desc: "\u4E00\u952E\u83B7\u53D6\u516C\u7F51\u5730\u5740\uFF08\u91CD\u542F\u540E URL \u4F1A\u53D8\u5316\uFF09",
409
- data: {
410
- running: status?.cloudflared?.running,
411
- url: status?.cloudflared?.url,
412
- qr: status?.cloudflared?.qr,
413
- state: status?.cloudflared?.state
414
- },
415
- onStart: onStartCloudflared,
416
- onStop: onStopCloudflared,
417
- onReset: status?.cloudflared?.running ? onResetCloudflared : null
418
- }),
419
- React.createElement(
420
- TunnelCard,
421
- {
422
- title: "\u81EA\u5EFA\u96A7\u9053",
423
- desc: "\u8FDE\u63A5\u81EA\u5DF1\u90E8\u7F72\u7684\u96A7\u9053\u670D\u52A1\u5668\uFF0C\u83B7\u5F97\u56FA\u5B9A\u57DF\u540D",
424
- data: {
425
- configured: ct?.configured,
426
- running: ct?.running,
427
- url: ct?.url,
428
- qr: ct?.qr,
429
- state: ct?.state
430
- },
431
- onStart: onStartCustom,
432
- onStop: onStopCustom
433
- },
434
- React.createElement(CustomTunnelGuide),
435
- React.createElement(CustomTunnelConfigForm, {
436
- serverUrl: ct?.serverUrl ?? "",
437
- accessToken: ct?.accessToken ?? "",
438
- onSave: saveConfig
439
- })
440
- )
898
+ React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
899
+ tabContent
441
900
  );
442
901
  }
443
902
  function apply(ctx) {