import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import "./styles.css"; const NAV_ITEMS = [ { id: "messages", label: "消息", description: "查找会话、阅读并回复", icon: "message" }, { id: "broadcast", label: "群发", description: "创建受控群发任务", icon: "send" }, { id: "contacts", label: "通讯录", description: "查找联系人和群聊", icon: "contacts" }, { id: "tasks", label: "任务", description: "查看执行结果", icon: "tasks" }, ]; const STATUS_LABELS = { Online: "在线", Degraded: "需要注意", Offline: "已离线", Registered: "已注册", SessionLocked: "桌面已锁定", WechatNotRunning: "微信未运行", WechatNotLoggedIn: "微信未登录", Pending: "待处理", WaitingForClient: "等待 Client", Accepted: "已接收", Running: "执行中", Succeeded: "已完成", Failed: "失败", Cancelled: "已取消", Expired: "已过期", ResultUnconfirmed: "待核对", }; const TERMINAL_TASKS = new Set([ "Succeeded", "Failed", "Cancelled", "Expired", "ResultUnconfirmed", ]); class ApiError extends Error { constructor(message, status, code = "RequestFailed") { super(message); this.status = status; this.code = code; } } async function api(path, { token, method = "GET", body } = {}) { const headers = {}; if (token) headers.Authorization = `Bearer ${token}`; if (body !== undefined) headers["Content-Type"] = "application/json"; const response = await fetch(path, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); const payload = await response.json().catch(() => ({})); if (!response.ok) { throw new ApiError( payload.error?.message || `请求失败(${response.status})`, response.status, payload.error?.code, ); } return payload; } const wait = (duration) => new Promise((resolve) => window.setTimeout(resolve, duration)); async function waitForTask(token, taskId) { const deadline = Date.now() + 30000; let delay = 250; while (Date.now() < deadline) { const task = await api(`/v1/tasks/${encodeURIComponent(taskId)}`, { token }); if (task.status === "Succeeded") return task.result?.content || {}; if (task.status === "WaitingForClient") { throw new ApiError("当前 Client 暂不可用,请切换 Client 后重试。", 409, "ClientNotReady"); } if (TERMINAL_TASKS.has(task.status)) { throw new ApiError( task.result?.message || `读取任务${STATUS_LABELS[task.status] || "未完成"}。`, 409, task.result?.error_code || task.status, ); } await wait(delay); delay = Math.min(1000, delay + 150); } throw new ApiError("读取超时,请稍后重试。", 408, "ReadTimeout"); } async function readWithTask(path, body, token) { const created = await api(path, { token, method: "POST", body: { ...body, idempotency_key: `${path}-${Date.now()}-${Math.random().toString(36).slice(2)}`, }, }); if (created.status === "WaitingForClient") { throw new ApiError("当前 Client 暂不可用,请切换 Client 后重试。", 409, "ClientNotReady"); } return waitForTask(token, created.task_id); } function formatTime(value) { if (!value) return "—"; const date = new Date(value); return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString("zh-CN", { hour12: false }); } function shortId(value, length = 16) { if (!value) return "—"; return value.length > length ? `${value.slice(0, length)}…` : value; } function statusTone(status) { if (["Online", "Succeeded", "Accepted"].includes(status)) return "positive"; if (["Running", "Pending", "WaitingForClient", "Degraded"].includes(status)) return "warning"; if ( [ "Failed", "Cancelled", "Expired", "ResultUnconfirmed", "Offline", "SessionLocked", "WechatNotRunning", "WechatNotLoggedIn", ].includes(status) ) return "negative"; return "neutral"; } function statusLabel(status) { return STATUS_LABELS[status] || status || "未知"; } function isClientAvailable(node) { return Boolean(node && ["Online", "Degraded", "Registered"].includes(node.status)); } function activeAccount(node) { if (!node) return ""; if (node.active_account_id) return node.active_account_id; return node.accounts?.find((account) => account.active && account.verified)?.account_id || ""; } function extractItems(content, keys) { if (Array.isArray(content)) return content; for (const key of keys) { if (Array.isArray(content?.[key])) return content[key]; } return []; } function normalizeSessions(content) { return extractItems(content, ["items", "sessions", "chats"]).map((item, index) => ({ id: String(item.chat_id ?? item.chatId ?? item.session_id ?? item.id ?? index), title: String(item.name ?? item.display_name ?? item.displayName ?? item.nickname ?? item.chat_id ?? item.id ?? "未命名会话"), preview: String(item.last_message ?? item.lastMessage ?? item.preview ?? "暂无最近消息"), unread: Number(item.unread_count ?? item.unreadCount ?? 0), type: item.chat_type ?? item.chatType ?? "Private", })); } function normalizeMessages(content) { return extractItems(content, ["items", "messages"]).map((item, index) => ({ id: String(item.message_id ?? item.messageId ?? item.id ?? index), sender: String(item.sender ?? item.sender_name ?? item.senderName ?? "对方"), text: typeof item === "string" ? item : String(item.content ?? item.text ?? item.message ?? ""), at: item.occurred_at ?? item.occurredAt ?? item.created_at ?? item.createdAt, })); } function normalizeContacts(content) { return extractItems(content, ["items", "contacts", "sessions"]).map((item, index) => ({ id: String(item.contact_id ?? item.contactId ?? item.chat_id ?? item.chatId ?? item.id ?? index), title: String(item.name ?? item.display_name ?? item.displayName ?? item.nickname ?? item.contact_id ?? item.id ?? "未命名联系人"), type: item.chat_type ?? item.chatType ?? (item.is_group ? "Group" : "Private"), detail: String(item.remark ?? item.alias ?? item.account_id ?? item.accountId ?? ""), })); } function Icon({ name, size = 18 }) { const paths = { message: <>, send: <>, contacts: <>, tasks: <>, settings: <>, refresh: <>, chevron: , search: <>, shield: <>, alert: <>, close: <>, logout: <>, arrow: <>, check: , }; return ( ); } function StatusBadge({ value }) { return ( {statusLabel(value)} ); } function Login({ onLogin }) { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const submit = async (event) => { event.preventDefault(); setError(""); setBusy(true); try { const result = await api("/v1/auth/login", { method: "POST", body: { username, password } }); onLogin(result.access_token, username); } catch (reason) { setError(reason.message); } finally { setBusy(false); } }; return (
W

WXAGENT WORKSPACE

进入工作台

连接已授权的 Client,处理消息、通讯录和任务结果。

{error &&
{error}
}

会话只保存在当前浏览器标签页

); } function Sidebar({ view, setView, onLogout, username }) { return ( ); } function Topbar({ title, subtitle, nodes, selectedClientId, onClientChange, onRefresh, loading }) { const selected = nodes.find((node) => node.node_id === selectedClientId); return (

{title}

{subtitle}

{selected && }
); } function ConnectionBanner({ nodes, selectedNode, onSettings }) { if (!nodes.length) { return
还没有可用 Client请启动已配置的 Desktop Agent;浏览器不会模拟连接状态。
; } if (!isClientAvailable(selectedNode)) { const other = nodes.some(isClientAvailable); return
当前 Client 不可用{other ? "其它 Client 不受影响,请从顶部切换。" : "所有 Client 当前都不可用,请先恢复连接。"}
; } if (!activeAccount(selectedNode)) { return
当前 Client 尚未确认微信账号只读数据需要已验证的活动账号,先完成账号绑定后再操作。
; } return null; } function ContextEmpty({ title = "没有选中的 Client", text = "请先连接并选择一个可用 Client。", onSettings }) { return

{title}

{text}

{onSettings && }
; } function LoadingState({ text = "正在读取…" }) { return
{text}
; } function Empty({ text, action }) { return

{text}

{action}
; } function ReadError({ error, onRetry }) { return
{error}{onRetry && }
; } function MessagesView({ token, client, onSettings }) { const clientId = client?.node_id || ""; const accountId = activeAccount(client); const [sessions, setSessions] = useState([]); const [sessionsState, setSessionsState] = useState({ loading: false, error: "" }); const [query, setQuery] = useState(""); const [selectedChat, setSelectedChat] = useState(null); const [messages, setMessages] = useState([]); const [messagesState, setMessagesState] = useState({ loading: false, error: "" }); const requestRef = useRef(0); const loadSessions = useCallback(async () => { if (!clientId || !accountId) return; const requestId = ++requestRef.current; setSessionsState({ loading: true, error: "" }); try { const content = await readWithTask("/v1/reads/sessions", { node_id: clientId, account_id: accountId, limit: 100 }, token); if (requestId !== requestRef.current) return; setSessions(normalizeSessions(content)); } catch (reason) { if (requestId === requestRef.current) setSessionsState({ loading: false, error: reason.message }); return; } setSessionsState({ loading: false, error: "" }); }, [accountId, clientId, token]); useEffect(() => { setSelectedChat(null); setSessions([]); setMessages([]); if (!clientId || !accountId) return; loadSessions(); }, [clientId, accountId, loadSessions]); useEffect(() => { if (!selectedChat || selectedChat.clientId !== clientId || !clientId || !accountId) return; const requestId = ++requestRef.current; setMessagesState({ loading: true, error: "" }); readWithTask("/v1/reads/messages", { node_id: clientId, account_id: accountId, chat_id: selectedChat.id, limit: 100, include_content: true }, token) .then((content) => { if (requestId !== requestRef.current) return; setMessages(normalizeMessages(content)); setMessagesState({ loading: false, error: "" }); }) .catch((reason) => { if (requestId === requestRef.current) setMessagesState({ loading: false, error: reason.message }); }); }, [accountId, clientId, selectedChat, token]); if (!client) return ; const visibleSessions = sessions.filter((session) => !query || `${session.title} ${session.id}`.toLowerCase().includes(query.toLowerCase())); return (

会话

{sessions.length ? `${sessions.length} 个会话` : "当前 Client 的会话"}

{sessionsState.loading ? : sessionsState.error ? : visibleSessions.length === 0 ? :
{visibleSessions.map((session) => )}
}
{!selectedChat ? : <>

当前 Client · {clientId}

{selectedChat.title}

{selectedChat.type === "Group" ? "群聊" : "私聊"} · {shortId(selectedChat.id, 28)}
{messagesState.loading ? : messagesState.error ? setSelectedChat({ ...selectedChat })} /> : messages.length === 0 ? :
{messages.map((message) =>
{message.sender.slice(0, 1).toUpperCase()}
{message.sender}{formatTime(message.at)}

{message.text || "(无正文)"}

)}
}