import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import { AppShell } from "../components/AppShell"; import { Chat } from "../components/icons"; import { Button, EmptyState, InlineAlert, PageHeader, SkeletonRows } from "../components/ui"; import { useAuth } from "../App"; import { api, ApiError } from "../lib/client"; import type { BotChannel, BotMessage, WechatQr } from "../lib/types"; function ago(iso: string | null): string { if (!iso) return "还没收到过消息"; const diff = Math.max(0, Date.now() - new Date(iso).getTime()); const min = Math.floor(diff / 60_000); if (min < 1) return "刚刚收到消息"; if (min < 60) return `${min} 分钟前收到消息`; const hour = Math.floor(min / 60); if (hour < 24) return `${hour} 小时前收到消息`; return `${Math.floor(hour / 24)} 天前收到消息`; } const ACTION_TEXT: Record = { task: "建了任务", knowledge: "建了知识", help: "回了用法", ignored: "没处理", error: "出错", }; /** 扫码的几种回话,说给用户听的话。 */ const QR_TEXT: Record = { waiting: "用微信扫这张码,扫完在手机上点确认。", wait: "用微信扫这张码,扫完在手机上点确认。", scaned: "已经扫到了,在手机上点一下「确认登录」。", confirmed: "配好了,现在可以直接在微信里发消息。", expired: "二维码过期了,重新拿一张。", error: "问登录结果时出错了,稍等再试。", }; function ChannelStatus({ channel }: { channel: BotChannel }) { if (channel.status === "active") { return ( 接收中 ); } if (channel.status === "expired") { return 登录已失效; } if (channel.status === "disabled") { return 已停用; } return 待扫码; } /** * 机器人页:把微信(clawbot)接进这个工作区。 * * 走的是腾讯 iLink Bot 协议:**扫码登录 → 后端长轮询收消息 → 用 context_token 回复**。 * 页面这一侧只做两件事:把二维码画出来、反复问「扫了没」。 */ export function IntegrationsPage() { const { wsId = "" } = useParams(); const { workspaces } = useAuth(); const ws = workspaces.find((w) => w.id === wsId); const canManage = ws?.role === "owner" || ws?.role === "admin"; const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [busy, setBusy] = useState(null); const [notice, setNotice] = useState(null); const [qr, setQr] = useState(null); const [qrStatus, setQrStatus] = useState("waiting"); const [messages, setMessages] = useState>({}); /** 关掉轮询:确认成功、过期、或者用户离开这一屏。 */ const stopPolling = useRef(false); const load = useCallback(async () => { setLoading(true); setError(null); try { const data = await api.get<{ items: BotChannel[] }>(`/workspaces/${wsId}/integrations`); setChannels(data.items); } catch (err) { setError(err instanceof ApiError ? err.message : "读不到机器人配置"); } finally { setLoading(false); } }, [wsId]); useEffect(() => { void load(); }, [load]); const channel = channels[0] ?? null; const startQr = useCallback(async () => { setBusy("qr"); setError(null); setNotice(null); setMessages({}); stopPolling.current = false; try { const data = await api.post(`/workspaces/${wsId}/integrations/wechat/qr`, {}); setQr(data); setQrStatus("waiting"); } catch (err) { setError(err instanceof ApiError ? err.message : "取二维码失败,稍后再试"); setQr(null); } finally { setBusy(null); } }, [wsId]); // 每 2 秒问一次扫码进度;确认之后换成"已连接"并停手。 useEffect(() => { if (!qr || stopPolling.current) return; let alive = true; const timer = window.setTimeout(async () => { try { const data = await api.get<{ status: string; channel?: BotChannel }>( `/workspaces/${wsId}/integrations/wechat/qr/${qr.qrcode_id}`, ); if (!alive) return; setQrStatus(data.status); if (data.status === "confirmed") { stopPolling.current = true; setQr(null); setNotice("微信已经连上,可以直接在对话里发「任务:…」或「知识:…」。"); void load(); } else if (data.status === "expired") { stopPolling.current = true; } } catch { if (alive) setQrStatus("error"); } }, 2000); return () => { alive = false; window.clearTimeout(timer); }; }, [qr, qrStatus, wsId, load]); const disconnect = useCallback( async (id: string) => { setBusy(id); setError(null); try { await api.del(`/workspaces/${wsId}/integrations/${id}?confirm=true`); setQr(null); stopPolling.current = true; setNotice("已断开,微信里再发消息就不会进来了。"); await load(); } catch (err) { setError(err instanceof ApiError ? err.message : "断开失败"); } finally { setBusy(null); } }, [wsId, load], ); const showMessages = useCallback( async (id: string) => { if (messages[id]) { setMessages((prev) => { const next = { ...prev }; delete next[id]; return next; }); return; } try { const data = await api.get<{ items: BotMessage[] }>( `/workspaces/${wsId}/integrations/${id}/messages?limit=5`, ); setMessages((prev) => ({ ...prev, [id]: data.items })); } catch (err) { setError(err instanceof ApiError ? err.message : "读不到消息记录"); } }, [wsId, messages], ); return ( void startQr()} disabled={busy === "qr"}> {busy === "qr" ? "正在取二维码…" : qr ? "换一张二维码" : "扫码连接微信"} ) : null } /> {error ? (
{error}
) : null} {notice ? (
{notice}
) : null}

怎么用

  1. 点右上「扫码连接微信」,用手机微信扫出现的二维码并在手机上确认。
  2. 连上之后直接给这个微信机器人发消息:默认记成任务,「知识:」开头记成知识草稿。
  3. 第一行当标题,其余当正文;标题或正文里带 #标签 会一起加上。
  4. 直接发一条链接(公众号文章也行),正文和配图会抓下来存成知识草稿。
{loading ? (
) : ( <> {qr ? (
微信扫码登录二维码

{QR_TEXT[qrStatus] ?? "等扫码结果…"}

二维码 5 分钟内有效。扫完手机上会问「是否允许登录」,点确认就配好了。

{qrStatus === "expired" || qrStatus === "error" ? ( ) : null}
) : null} {channel ? (
{channel.name || "微信"} {channel.polling ? 正在接收 : null}

{channel.status === "active" ? ago(channel.last_seen_at) : "还没连上微信,扫码之后才收得到消息"} {channel.ilink_user_id ? ` · 微信用户 ${channel.ilink_user_id}` : ""}

{channel.last_error ? (

{channel.last_error}

) : null}
{canManage ? ( <> ) : null}
{messages[channel.id] ? (
{messages[channel.id].length === 0 ? (

还没收到过消息。

) : ( messages[channel.id].map((m) => (

{m.text || "(没有文字)"}

{m.sender}

{ACTION_TEXT[m.action] ?? m.action}
)) )}
) : null}
) : (
} title="还没连微信" hint={ canManage ? "点右上「扫码连接微信」,用手机扫一下就行。" : "只有工作区管理员能连微信,让他扫一下。" } />
)} {!canManage ? (

你不是这个工作区的管理员,能看到机器人的状态,但不能扫码或断开。

) : null} )}
); }