@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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // dsh-bridge 客户端插件:设置页「远程访问」面板
2
2
 
3
- import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from '../lib/bridge-rpc.js';
3
+ import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from '../lib/bridge-rpc-constants.js';
4
4
 
5
5
  const GITHUB_URL = 'https://github.com/wenbin-wb/dsh-bridge';
6
6
  const ISSUES_URL = 'https://github.com/wenbin-wb/dsh-bridge/issues/new';
@@ -190,9 +190,293 @@ const TunnelCard = React.memo(function TunnelCard({ title, desc, data, onStart,
190
190
  );
191
191
  });
192
192
 
193
+ // ---- 微信 Bot 卡片 ----
194
+
195
+ function WechatCard({ rpcCall, onStatusChange }) {
196
+ const [wx, setWx] = React.useState(null);
197
+ const [err, setErr] = React.useState(null);
198
+ const [busy, setBusy] = React.useState(false);
199
+ const [showAdvanced, setShowAdvanced] = React.useState(false);
200
+ const [showHelp, setShowHelp] = React.useState(false);
201
+
202
+ // 高级设置本地草稿
203
+ const [cfgDraft, setCfgDraft] = React.useState(null);
204
+ React.useEffect(() => {
205
+ if (wx?.config && !cfgDraft) {
206
+ setCfgDraft({
207
+ digestIntervalSec: String(wx.config.digestIntervalSec ?? 300),
208
+ approvalTimeoutSec: String(wx.config.approvalTimeoutSec ?? 600),
209
+ maxMessageChars: String(wx.config.maxMessageChars ?? 2000),
210
+ sendChunkDelayMs: String(wx.config.sendChunkDelayMs ?? 1500),
211
+ });
212
+ }
213
+ }, [wx?.config]);
214
+
215
+ // 向上传递连接状态(供平台列表卡片绿点使用)
216
+ React.useEffect(() => {
217
+ const connected = wx?.status === 'connected' || wx?.status === 'starting' || wx?.status === 'reconnecting';
218
+ onStatusChange?.(connected);
219
+ }, [wx?.status, onStatusChange]);
220
+
221
+ const load = React.useCallback(async (quiet = false) => {
222
+ try {
223
+ const r = await rpcCall(BRIDGE_ENDPOINTS.wechatGetStatus, {});
224
+ if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
225
+ setWx(r.value);
226
+ if (!quiet) setErr(null);
227
+ } catch (e) {
228
+ if (!quiet) setErr(e.message);
229
+ }
230
+ }, [rpcCall]);
231
+
232
+ // 轮询:登录中(qr/scaned)快速刷新,其余放慢
233
+ React.useEffect(() => {
234
+ load();
235
+ const activeLogin = wx?.login && (wx.login.phase === 'qr' || wx.login.phase === 'scaned');
236
+ const interval = activeLogin ? 1500 : 3000;
237
+ const t = setInterval(() => load(true), interval);
238
+ return () => clearInterval(t);
239
+ }, [load, wx?.login?.phase]);
240
+
241
+ const act = React.useCallback(async (endpoint, payload) => {
242
+ setBusy(true);
243
+ try {
244
+ const r = await rpcCall(endpoint, payload ?? {});
245
+ if (!r?.ok) throw new Error(r?.error?.message ?? 'RPC failed');
246
+ setWx(r.value);
247
+ setErr(null);
248
+ await load(true);
249
+ } catch (e) {
250
+ setErr(e.message);
251
+ } finally {
252
+ setBusy(false);
253
+ }
254
+ }, [rpcCall, load]);
255
+
256
+ const onLogin = React.useCallback(() => act(BRIDGE_ENDPOINTS.wechatLogin, {}), [act]);
257
+ const onStop = React.useCallback(() => act(BRIDGE_ENDPOINTS.wechatStop, {}), [act]);
258
+
259
+ // 白名单管理
260
+ const [newId, setNewId] = React.useState('');
261
+ const addAllow = React.useCallback(async () => {
262
+ const id = newId.trim();
263
+ if (!id) return;
264
+ const list = [...(wx?.allowFrom ?? []), id];
265
+ await act(BRIDGE_ENDPOINTS.wechatSetAllowFrom, { allowFrom: list });
266
+ setNewId('');
267
+ }, [act, newId, wx?.allowFrom]);
268
+ const removeAllow = React.useCallback(async (id) => {
269
+ const list = (wx?.allowFrom ?? []).filter((x) => x !== id);
270
+ await act(BRIDGE_ENDPOINTS.wechatSetAllowFrom, { allowFrom: list });
271
+ }, [act, wx?.allowFrom]);
272
+ const handleNewId = React.useCallback((e) => setNewId(e.target.value), []);
273
+
274
+ // 高级设置保存
275
+ const saveConfig = React.useCallback(async () => {
276
+ if (!cfgDraft) return;
277
+ await act(BRIDGE_ENDPOINTS.wechatSetConfig, {
278
+ digestIntervalSec: Number(cfgDraft.digestIntervalSec),
279
+ approvalTimeoutSec: Number(cfgDraft.approvalTimeoutSec),
280
+ maxMessageChars: Number(cfgDraft.maxMessageChars),
281
+ sendChunkDelayMs: Number(cfgDraft.sendChunkDelayMs),
282
+ });
283
+ }, [act, cfgDraft]);
284
+ const cfgDirty = cfgDraft && wx?.config && (
285
+ Number(cfgDraft.digestIntervalSec) !== wx.config.digestIntervalSec ||
286
+ Number(cfgDraft.approvalTimeoutSec) !== wx.config.approvalTimeoutSec ||
287
+ Number(cfgDraft.maxMessageChars) !== wx.config.maxMessageChars ||
288
+ Number(cfgDraft.sendChunkDelayMs) !== wx.config.sendChunkDelayMs
289
+ );
290
+
291
+ if (!wx && !err) {
292
+ return React.createElement('div', { style: s.card },
293
+ React.createElement('div', { style: s.label }, '微信 Bot'),
294
+ React.createElement('div', { style: { ...s.muted, marginTop: 6 } }, '加载中…'),
295
+ );
296
+ }
297
+
298
+ const connected = wx?.status === 'connected' || wx?.status === 'starting';
299
+ const login = wx?.login ?? {};
300
+ const showQr = login.phase === 'qr' || login.phase === 'scaned';
301
+ const statusLabel = wx?.status === 'connected' ? '已连接'
302
+ : wx?.status === 'starting' ? '连接中…'
303
+ : wx?.status === 'reconnecting' ? '重连中…'
304
+ : wx?.status === 'paused' ? '暂停(会话过期)'
305
+ : wx?.status === 'error' ? '错误'
306
+ : '未连接';
307
+
308
+ return React.createElement('div', { style: s.card },
309
+ React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
310
+ React.createElement('div', null,
311
+ React.createElement('div', { style: s.label }, '微信 Bot'),
312
+ React.createElement('div', { style: { ...s.muted, marginTop: 2 } },
313
+ '通过微信扫 ClawBot 二维码,在微信里远程对话和控制 DSH agent'
314
+ ),
315
+ ),
316
+ React.createElement(StatusTag, { running: connected }),
317
+ ),
318
+
319
+ // 快捷入口:使用说明 / 命令
320
+ React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap', alignItems: 'center' } },
321
+ React.createElement('a', {
322
+ href: 'https://github.com/wenbin-wb/dsh-bridge/blob/main/docs/wechat-usage.md',
323
+ target: '_blank', rel: 'noopener noreferrer',
324
+ style: s.btnGhost,
325
+ }, '📖 使用说明'),
326
+ React.createElement('button', {
327
+ style: s.btnGhost,
328
+ onClick: () => setShowHelp(v => !v),
329
+ }, showHelp ? '收起命令' : '微信命令'),
330
+ ),
331
+
332
+ // 命令速查
333
+ showHelp && React.createElement('div', { style: { ...s.block, fontSize: 12, lineHeight: 1.8, fontFamily: 'monospace' } },
334
+ React.createElement('div', null, '/new <提示词> — 新建会话(当前工作区)'),
335
+ React.createElement('div', null, '/new <提示词> @N — 在指定工作区新建'),
336
+ React.createElement('div', null, '/sessions — 按工作区分组列会话'),
337
+ React.createElement('div', null, '/use N — 切换到会话 N'),
338
+ React.createElement('div', null, '/workspaces — 列出工作区'),
339
+ React.createElement('div', null, '/stop — 停止任务'),
340
+ React.createElement('div', null, '/status — 查看状态'),
341
+ React.createElement('div', null, '/yes 或 /no — 回应审批'),
342
+ React.createElement('div', null, '/help — 全部命令'),
343
+ ),
344
+
345
+ err && React.createElement('div', { style: { ...s.warn, marginTop: 10 } }, err),
346
+
347
+ // 已配置:状态详情 + 白名单
348
+ wx?.configured && React.createElement('div', { style: s.block },
349
+ React.createElement('div', { style: { fontSize: 12, lineHeight: 1.7 } },
350
+ React.createElement('div', null, `状态: ${statusLabel}`),
351
+ wx.accountId && React.createElement('div', null, `账号: ${wx.accountId}`),
352
+ wx.sessionId && React.createElement('div', null, `当前会话: ${wx.sessionId}`),
353
+ ),
354
+ React.createElement('div', { style: { ...s.muted, fontSize: 12, marginTop: 8, lineHeight: 1.6 } },
355
+ '白名单(仅这些微信用户可驱动 agent):'
356
+ ),
357
+ React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 } },
358
+ (wx.allowFrom?.length
359
+ ? wx.allowFrom.map((id) =>
360
+ 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 } },
361
+ React.createElement('span', { style: { fontSize: 12, wordBreak: 'break-all' } }, id),
362
+ React.createElement('button', {
363
+ style: { cursor: 'pointer', border: 'none', background: 'none', color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, padding: 0 },
364
+ onClick: () => removeAllow(id), title: '移出白名单',
365
+ }, '×'),
366
+ )
367
+ )
368
+ : React.createElement('div', { style: { ...s.muted, fontSize: 12 } }, '(空 — 扫码后首个发消息的微信用户将自动加入)')),
369
+ ),
370
+ React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8, alignItems: 'center' } },
371
+ React.createElement('input', {
372
+ style: { ...s.input, flex: 1 },
373
+ placeholder: '添加允许的微信 ID(如 xxx@im.wechat)',
374
+ value: newId,
375
+ onChange: handleNewId,
376
+ }),
377
+ React.createElement('button', {
378
+ style: { ...s.btnGhost, whiteSpace: 'nowrap', opacity: (newId.trim() && !busy) ? 1 : 0.5 },
379
+ onClick: addAllow, disabled: busy || !newId.trim(),
380
+ }, '添加'),
381
+ ),
382
+ React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap', alignItems: 'center' } },
383
+ wx.status !== 'connected' && wx.status !== 'starting' &&
384
+ React.createElement('button', { style: s.btnPri, onClick: onLogin, disabled: busy }, '重新扫码'),
385
+ (wx.status === 'connected' || wx.status === 'starting') &&
386
+ React.createElement('button', { style: s.btnGhost, onClick: onStop, disabled: busy }, '断开'),
387
+ React.createElement('button', {
388
+ style: { ...s.btnGhost, color: 'var(--dsw-alias-state-error-primary,#dc2626)', borderColor: 'var(--dsw-alias-state-error-primary,#dc2626)', opacity: busy ? 0.5 : 1 },
389
+ disabled: busy,
390
+ onClick: () => { if (window.confirm('确认解绑?这将清除登录凭证,下次需重新扫码登录。')) act(BRIDGE_ENDPOINTS.wechatUnbind, {}); },
391
+ title: '清除登录凭证,下次需重新扫码',
392
+ }, '解绑账号'),
393
+ ),
394
+ ),
395
+
396
+ // 未配置 / 登录中:二维码
397
+ (!wx?.configured || showQr) && React.createElement('div', { style: s.block },
398
+ showQr && login.qr
399
+ ? React.createElement('div', null,
400
+ React.createElement('img', { src: login.qr, alt: 'wechat QR', style: s.qr }),
401
+ React.createElement('div', { style: { ...s.muted, marginTop: 4 } },
402
+ login.phase === 'scaned' ? '已扫码,请在手机上确认…' : '请使用微信扫码登录(ClawBot)'
403
+ ),
404
+ login.error && React.createElement('div', { style: { ...s.muted, marginTop: 4, color: 'var(--dsw-alias-state-warn-primary,#92400e)' } }, login.error),
405
+ )
406
+ : React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 4, flexWrap: 'wrap', alignItems: 'center' } },
407
+ React.createElement('button', {
408
+ style: { ...s.btnPri, opacity: busy ? 0.5 : 1 },
409
+ onClick: onLogin, disabled: busy,
410
+ }, busy ? '处理中…' : '扫码登录'),
411
+ login.phase === 'error' && React.createElement('div', { style: { ...s.muted, fontSize: 12 } }, login.error ?? '登录失败'),
412
+ ),
413
+ ),
414
+
415
+ // 高级设置(可折叠)
416
+ cfgDraft && React.createElement('div', { style: s.block },
417
+ React.createElement('button', {
418
+ style: { ...s.btnLink, fontSize: 12, marginBottom: showAdvanced ? 10 : 0 },
419
+ onClick: () => setShowAdvanced(v => !v),
420
+ }, showAdvanced ? '▾ 高级设置' : '▸ 高级设置'),
421
+ showAdvanced && React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
422
+ // 心跳间隔
423
+ React.createElement('div', null,
424
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '心跳间隔(秒)— 长任务处理中每隔多久发一次进度提示'),
425
+ React.createElement('input', {
426
+ style: { ...s.input, width: 120 },
427
+ type: 'number', min: 30, max: 3600,
428
+ value: cfgDraft.digestIntervalSec,
429
+ onChange: (e) => setCfgDraft(d => ({ ...d, digestIntervalSec: e.target.value })),
430
+ }),
431
+ ),
432
+ // 审批超时
433
+ React.createElement('div', null,
434
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '审批超时(秒)— 工具调用审批无响应后自动拒绝'),
435
+ React.createElement('input', {
436
+ style: { ...s.input, width: 120 },
437
+ type: 'number', min: 30, max: 86400,
438
+ value: cfgDraft.approvalTimeoutSec,
439
+ onChange: (e) => setCfgDraft(d => ({ ...d, approvalTimeoutSec: e.target.value })),
440
+ }),
441
+ ),
442
+ // 每气泡字数
443
+ React.createElement('div', null,
444
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '每条消息最大字数 — 超出时自动分多条发送'),
445
+ React.createElement('input', {
446
+ style: { ...s.input, width: 120 },
447
+ type: 'number', min: 100, max: 10000,
448
+ value: cfgDraft.maxMessageChars,
449
+ onChange: (e) => setCfgDraft(d => ({ ...d, maxMessageChars: e.target.value })),
450
+ }),
451
+ ),
452
+ // 分块延迟
453
+ React.createElement('div', null,
454
+ React.createElement('div', { style: { ...s.muted, marginBottom: 4 } }, '分块发送延迟(毫秒)— 多条消息之间的间隔'),
455
+ React.createElement('input', {
456
+ style: { ...s.input, width: 120 },
457
+ type: 'number', min: 0, max: 10000,
458
+ value: cfgDraft.sendChunkDelayMs,
459
+ onChange: (e) => setCfgDraft(d => ({ ...d, sendChunkDelayMs: e.target.value })),
460
+ }),
461
+ ),
462
+ React.createElement('button', {
463
+ style: { ...s.btnPri, alignSelf: 'flex-start', opacity: (cfgDirty && !busy) ? 1 : 0.5 },
464
+ disabled: !cfgDirty || busy,
465
+ onClick: saveConfig,
466
+ }, busy ? '保存中…' : '保存设置'),
467
+ ),
468
+ ),
469
+
470
+ React.createElement('div', { style: s.block },
471
+ React.createElement('div', { style: { ...s.tip, fontSize: 12 } },
472
+ '说明: 扫码成功后,向该微信 Bot 发送第一条消息即自动完成白名单授权。仅白名单内的微信用户能驱动 agent,其他人消息会被忽略。使用专用微信号,避免影响主号。'
473
+ ),
474
+ ),
475
+ );
476
+ }
477
+
193
478
  // 版本检查 + GitHub/反馈入口
194
- function VersionBanner({ rpcCall }) {
195
- const [info, setInfo] = React.useState(null);
479
+ function VersionBanner({ rpcCall }) { const [info, setInfo] = React.useState(null);
196
480
  const [loading, setLoading] = React.useState(false);
197
481
 
198
482
  const check = React.useCallback(async () => {
@@ -270,11 +554,63 @@ function VersionBanner({ rpcCall }) {
270
554
  );
271
555
  }
272
556
 
557
+ // ---- Tab Bar ----
558
+
559
+ const TABS = [
560
+ { id: 'lan', label: '局域网' },
561
+ { id: 'tunnel', label: '公网隧道' },
562
+ { id: 'im', label: 'IM 机器人' },
563
+ ];
564
+
565
+ function TabBar({ active, onChange, dots }) {
566
+ return React.createElement('div', {
567
+ style: {
568
+ display: 'flex', gap: 0, marginBottom: 20,
569
+ borderBottom: '1px solid var(--dsw-alias-border-l2,#e5e7eb)',
570
+ },
571
+ },
572
+ TABS.map(({ id, label }) => {
573
+ const isActive = active === id;
574
+ const hasDot = dots?.[id];
575
+ return React.createElement('button', {
576
+ key: id,
577
+ onClick: () => onChange(id),
578
+ style: {
579
+ font: 'inherit', cursor: 'pointer', border: 'none', background: 'none',
580
+ padding: '8px 16px', fontSize: 13, fontWeight: isActive ? 600 : 400,
581
+ color: isActive
582
+ ? 'var(--dsw-alias-brand-primary,#4f6ef7)'
583
+ : 'var(--dsw-alias-label-secondary,#6b7280)',
584
+ borderBottom: isActive
585
+ ? '2px solid var(--dsw-alias-brand-primary,#4f6ef7)'
586
+ : '2px solid transparent',
587
+ marginBottom: -1,
588
+ display: 'inline-flex', alignItems: 'center', gap: 6,
589
+ transition: 'color .15s, border-color .15s',
590
+ whiteSpace: 'nowrap',
591
+ },
592
+ },
593
+ label,
594
+ hasDot && React.createElement('span', {
595
+ style: {
596
+ width: 6, height: 6, borderRadius: '50%',
597
+ background: 'var(--dsw-alias-state-success-primary,#10b981)',
598
+ flexShrink: 0,
599
+ },
600
+ }),
601
+ );
602
+ }),
603
+ );
604
+ }
605
+
273
606
  // ---- 主面板 ----
274
607
 
275
608
  function BridgePanel({ rpcCall }) {
276
- const [status, setStatus] = React.useState(null);
277
- const [err, setErr] = React.useState(null);
609
+ const [status, setStatus] = React.useState(null);
610
+ const [err, setErr] = React.useState(null);
611
+ const [activeTab, setActiveTab] = React.useState('lan');
612
+ // 微信连接状态:独立轮询,不依赖 WechatCard 是否挂载
613
+ const [wechatConnected, setWechatConnected] = React.useState(false);
278
614
 
279
615
  const load = React.useCallback(async (quiet = false) => {
280
616
  try {
@@ -287,6 +623,23 @@ function BridgePanel({ rpcCall }) {
287
623
  }
288
624
  }, [rpcCall]);
289
625
 
626
+ // 独立轮询微信连接状态(与 getStatus 解耦,Tab 未选中时也能更新绿点)
627
+ React.useEffect(() => {
628
+ let alive = true;
629
+ const poll = async () => {
630
+ try {
631
+ const r = await rpcCall(BRIDGE_ENDPOINTS.wechatGetStatus, {});
632
+ if (alive && r?.ok) {
633
+ const s = r.value?.status;
634
+ setWechatConnected(s === 'connected' || s === 'starting' || s === 'reconnecting');
635
+ }
636
+ } catch { /* 忽略,不影响主面板 */ }
637
+ };
638
+ poll();
639
+ const t = setInterval(poll, 4000);
640
+ return () => { alive = false; clearInterval(t); };
641
+ }, [rpcCall]);
642
+
290
643
  React.useEffect(() => {
291
644
  load();
292
645
  const t = setInterval(() => load(true), 3000);
@@ -323,6 +676,104 @@ function BridgePanel({ rpcCall }) {
323
676
 
324
677
  const ct = status?.customTunnel;
325
678
 
679
+ // Tab 状态点:im 用 WechatCard 上报的准确状态,其余从 getStatus 读
680
+ const dots = {
681
+ lan: !!(status?.proxy?.running),
682
+ tunnel: !!(status?.cloudflared?.running || ct?.running),
683
+ im: wechatConnected,
684
+ };
685
+
686
+ // Tab 内容
687
+ let tabContent;
688
+ if (activeTab === 'lan') {
689
+ tabContent = React.createElement(TunnelCard, {
690
+ title: '局域网访问',
691
+ desc: '同一 Wi-Fi 下的设备可直接扫码访问',
692
+ data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
693
+ });
694
+ } else if (activeTab === 'tunnel') {
695
+ tabContent = React.createElement(React.Fragment, null,
696
+ React.createElement(TunnelCard, {
697
+ title: 'Cloudflare 隧道',
698
+ desc: '一键获取公网地址(重启后 URL 会变化)',
699
+ data: {
700
+ running: status?.cloudflared?.running,
701
+ url: status?.cloudflared?.url,
702
+ qr: status?.cloudflared?.qr,
703
+ state: status?.cloudflared?.state,
704
+ },
705
+ onStart: onStartCloudflared,
706
+ onStop: onStopCloudflared,
707
+ onReset: status?.cloudflared?.running ? onResetCloudflared : null,
708
+ }),
709
+ React.createElement(TunnelCard, {
710
+ title: '自建隧道',
711
+ desc: '连接自己部署的隧道服务器,获得固定域名',
712
+ data: {
713
+ configured: ct?.configured,
714
+ running: ct?.running,
715
+ url: ct?.url,
716
+ qr: ct?.qr,
717
+ state: ct?.state,
718
+ },
719
+ onStart: onStartCustom,
720
+ onStop: onStopCustom,
721
+ },
722
+ React.createElement(CustomTunnelGuide),
723
+ React.createElement(CustomTunnelConfigForm, {
724
+ serverUrl: ct?.serverUrl ?? '',
725
+ accessToken: ct?.accessToken ?? '',
726
+ onSave: saveConfig,
727
+ }),
728
+ ),
729
+ );
730
+ } else if (activeTab === 'im') {
731
+ // 平台列表:已接入的可点击,未接入的置灰
732
+ // wechatConnected 来自 WechatCard 的 onStatusChange 回调,状态准确
733
+ const IM_PLATFORMS = [
734
+ { id: 'wechat', label: '微信', desc: 'iLink Bot API(ClawBot)', available: true, active: wechatConnected },
735
+ { id: 'qq', label: 'QQ', desc: 'NapCat / Mirai', available: false, active: false },
736
+ { id: 'feishu', label: '飞书', desc: '官方事件回调 API', available: false, active: false },
737
+ ];
738
+ tabContent = React.createElement('div', null,
739
+ // 平台选择器
740
+ React.createElement('div', {
741
+ style: { display: 'flex', gap: 10, marginBottom: 16, flexWrap: 'wrap' },
742
+ },
743
+ IM_PLATFORMS.map(({ id, label, desc, available, active }) =>
744
+ React.createElement('div', {
745
+ key: id,
746
+ style: {
747
+ flex: '1 1 140px',
748
+ border: `1px solid ${active ? 'var(--dsw-alias-state-success-primary,#10b981)' : 'var(--dsw-alias-border-l2,#e5e7eb)'}`,
749
+ borderRadius: 10,
750
+ padding: '12px 14px',
751
+ opacity: available ? 1 : 0.45,
752
+ cursor: available ? 'default' : 'not-allowed',
753
+ background: active ? 'var(--dsw-alias-state-success-bg,#ecfdf5)' : available ? 'var(--dsw-alias-bg-layer-1,transparent)' : 'var(--dsw-alias-bg-layer-2,#f9fafb)',
754
+ },
755
+ },
756
+ React.createElement('div', { style: { ...s.label, fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 } },
757
+ label,
758
+ active && React.createElement('span', {
759
+ style: { width: 6, height: 6, borderRadius: '50%', background: 'var(--dsw-alias-state-success-primary,#10b981)', flexShrink: 0 },
760
+ }),
761
+ !active && available && React.createElement('span', {
762
+ style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#6b7280)', fontWeight: 400 },
763
+ }, '未连接'),
764
+ !available && React.createElement('span', {
765
+ style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#9ca3af)', fontWeight: 400 },
766
+ }, '即将支持'),
767
+ ),
768
+ React.createElement('div', { style: { ...s.muted, marginTop: 3, fontSize: 11 } }, desc),
769
+ )
770
+ ),
771
+ ),
772
+ // 微信卡片(onStatusChange 向上报连接状态)
773
+ React.createElement(WechatCard, { rpcCall, onStatusChange: setWechatConnected }),
774
+ );
775
+ }
776
+
326
777
  return React.createElement('div', { style: { maxWidth: 560 } },
327
778
  err && React.createElement('div', {
328
779
  style: { ...s.card, background: 'var(--dsw-alias-state-error-bg,#fef2f2)', color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 13, marginBottom: 16 },
@@ -330,46 +781,9 @@ function BridgePanel({ rpcCall }) {
330
781
 
331
782
  React.createElement(VersionBanner, { rpcCall }),
332
783
 
333
- React.createElement(TunnelCard, {
334
- title: '局域网访问',
335
- desc: '同一 Wi-Fi 下的设备可直接扫码访问',
336
- data: { running: status?.proxy?.running, url: status?.lan?.url, qr: status?.lan?.qr },
337
- }),
338
-
339
- React.createElement(TunnelCard, {
340
- title: 'Cloudflare 隧道',
341
- desc: '一键获取公网地址(重启后 URL 会变化)',
342
- data: {
343
- running: status?.cloudflared?.running,
344
- url: status?.cloudflared?.url,
345
- qr: status?.cloudflared?.qr,
346
- state: status?.cloudflared?.state,
347
- },
348
- onStart: onStartCloudflared,
349
- onStop: onStopCloudflared,
350
- onReset: status?.cloudflared?.running ? onResetCloudflared : null,
351
- }),
784
+ React.createElement(TabBar, { active: activeTab, onChange: setActiveTab, dots }),
352
785
 
353
- React.createElement(TunnelCard, {
354
- title: '自建隧道',
355
- desc: '连接自己部署的隧道服务器,获得固定域名',
356
- data: {
357
- configured: ct?.configured,
358
- running: ct?.running,
359
- url: ct?.url,
360
- qr: ct?.qr,
361
- state: ct?.state,
362
- },
363
- onStart: onStartCustom,
364
- onStop: onStopCustom,
365
- },
366
- React.createElement(CustomTunnelGuide),
367
- React.createElement(CustomTunnelConfigForm, {
368
- serverUrl: ct?.serverUrl ?? '',
369
- accessToken: ct?.accessToken ?? '',
370
- onSave: saveConfig,
371
- }),
372
- ),
786
+ tabContent,
373
787
  );
374
788
  }
375
789
 
Binary file