@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/README.md +53 -5
- package/README.zh-CN.md +53 -5
- package/client/client.js +501 -42
- package/client/index.js +458 -44
- package/docs/banner.jpg +0 -0
- package/docs/custom-tunnel.md +220 -0
- package/docs/wechat-usage.md +101 -0
- package/lib/bridge-rpc-constants.js +21 -0
- package/lib/bridge-rpc.js +89 -13
- package/lib/index.js +45 -2
- package/lib/wechat/gateway.js +807 -0
- package/lib/wechat/index.js +189 -0
- package/lib/wechat/media.js +275 -0
- package/lib/wechat/node.js +1040 -0
- package/package.json +9 -4
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]
|
|
277
|
-
const [err, setErr]
|
|
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(
|
|
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
|
-
|
|
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
|
|
package/docs/banner.jpg
ADDED
|
Binary file
|