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,处理消息、通讯录和任务结果。
会话只保存在当前浏览器标签页
);
}
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 (
);
}
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 ;
}
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 (
{!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 || "(无正文)"}
)}
}>}
);
}
function BroadcastView({ client, onSettings }) {
return
CONTROLLED ACTION
受控群发验证
单 Client 的 Client 端已提供 broadcast-text:冻结获准对象名单,逐项串行发送,返回每个对象的结果,并支持停止和幂等重放。
1冻结对象2预览确认3逐项发送4查看结果
;
}
function ContactsView({ token, client, onSettings }) {
const clientId = client?.node_id || "";
const accountId = activeAccount(client);
const [groupsOnly, setGroupsOnly] = useState(false);
const [query, setQuery] = useState("");
const [contacts, setContacts] = useState([]);
const [state, setState] = useState({ loading: false, error: "" });
const requestRef = useRef(0);
const load = useCallback(async () => {
if (!clientId || !accountId) return;
const requestId = ++requestRef.current;
setState({ loading: true, error: "" });
try {
const content = await readWithTask("/v1/reads/contacts", { node_id: clientId, account_id: accountId, limit: 200, groups_only: groupsOnly, contains: query }, token);
if (requestId !== requestRef.current) return;
setContacts(normalizeContacts(content));
setState({ loading: false, error: "" });
} catch (reason) {
if (requestId === requestRef.current) setState({ loading: false, error: reason.message });
}
}, [accountId, clientId, groupsOnly, query, token]);
useEffect(() => { setContacts([]); if (clientId && accountId) load(); }, [clientId, accountId, groupsOnly]);
if (!client) return ;
const visible = contacts.filter((contact) => !query || `${contact.title} ${contact.id} ${contact.detail}`.toLowerCase().includes(query.toLowerCase()));
return {state.loading ? : state.error ? : visible.length === 0 ? : {visible.map((contact) =>
{contact.title.slice(0, 1).toUpperCase()}{contact.title}{contact.type === "Group" ? "群聊" : "联系人"} · {shortId(contact.id, 28)}{contact.detail ? ` · ${contact.detail}` : ""}
)}
};
}
function TasksView({ tasks, selectedClientId }) {
const [filter, setFilter] = useState("all");
const visible = tasks.filter((task) => task.node_id === selectedClientId && (filter === "all" || task.status === filter));
return 任务结果
当前 Client 的任务状态和安全边界。
只读视图
{visible.length === 0 ? : {visible.map((task) =>
{task.kind}
{task.account_id} · {shortId(task.task_id, 22)}
{task.status === "WaitingForClient" ? "Client 离线后等待显式恢复;不会自动重放写操作。" : task.result?.message || task.result?.error_code || `更新于 ${formatTime(task.updated_at)}`}第 {task.lease_generation || 0} 代租约
)}
};
}
function NodesTable({ nodes }) {
return {nodes.length === 0 ?
: nodes.map((node) =>
{node.node_id.slice(0, 1).toUpperCase()}{node.node_id}{node.active_account_id || "尚未确认活动账号"} · 最近心跳 {formatTime(node.last_heartbeat_at)}
)}
;
}
function EventsTable({ events, selectedClientId }) {
const visible = events.filter((event) => !selectedClientId || event.node_id === selectedClientId);
return {visible.length === 0 ?
: visible.slice(0, 30).map((event) =>
{shortId(event.chat_id, 24)}{event.node_id} · {event.chat_type === "Group" ? "群聊" : "私聊"} · {formatTime(event.received_at)}
{event.content || "无正文"})}
;
}
function AuditTable({ audit }) {
return {audit.length === 0 ?
: audit.slice(0, 30).map((item) =>
{item.action}{item.principal} · {item.resource}
{formatTime(item.at)})}
;
}
function SettingsView({ nodes, events, audit, selectedClientId, onSelectClient }) {
const [tab, setTab] = useState("connection");
return {tab === "connection" ? <>
连接与账号
每个 Client 独立连接,单个 Client 离线不会阻断其它 Client。
安全边界
当前版本只开放真实后端已支持且可验证的只读能力。
- 消息、通讯录、任务结果按选定 Client 隔离读取
- Client 掉线只影响当前上下文,任务不会自动改投
- 发送、群发、添加好友等待 Windows 真机验收
> : <>
Client 诊断
技术状态只在这里展示,不干扰普通操作。
{nodes.map((node) => )}
AI 能力
AI 入口保留在高级诊断中;当前不自动调用,也不影响消息、通讯录和任务操作。
按能力矩阵开放>}
;
}
function App() {
const [token, setToken] = useState(() => sessionStorage.getItem("wxagent-token") || "");
const [username, setUsername] = useState(() => sessionStorage.getItem("wxagent-user") || "");
const [view, setView] = useState("messages");
const [nodes, setNodes] = useState([]);
const [selectedClientId, setSelectedClientId] = useState(() => sessionStorage.getItem("wxagent-client") || "");
const [tasks, setTasks] = useState([]);
const [events, setEvents] = useState([]);
const [audit, setAudit] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const logout = useCallback(() => {
sessionStorage.removeItem("wxagent-token");
sessionStorage.removeItem("wxagent-user");
sessionStorage.removeItem("wxagent-client");
setToken("");
setUsername("");
}, []);
const refresh = useCallback(async () => {
if (!token) return;
setLoading(true);
setError("");
try {
const [nodeData, taskData, eventData, auditData] = await Promise.all([
api("/v1/nodes", { token }),
api("/v1/tasks?limit=200", { token }),
api("/v1/events?limit=200", { token }),
api("/v1/audit?limit=200", { token }),
]);
setNodes(nodeData.nodes || []);
setTasks(taskData.tasks || []);
setEvents(eventData.events || []);
setAudit(auditData.audit || []);
} catch (reason) {
if (reason.status === 401) logout();
else setError(reason.message);
} finally {
setLoading(false);
}
}, [logout, token]);
useEffect(() => {
refresh();
if (!token) return undefined;
const id = window.setInterval(refresh, 15000);
return () => window.clearInterval(id);
}, [refresh, token]);
useEffect(() => {
if (!nodes.length) {
setSelectedClientId("");
return;
}
if (!nodes.some((node) => node.node_id === selectedClientId)) {
const next = nodes.find(isClientAvailable) || nodes[0];
setSelectedClientId(next.node_id);
sessionStorage.setItem("wxagent-client", next.node_id);
}
}, [nodes, selectedClientId]);
const handleClientChange = (clientId) => {
setSelectedClientId(clientId);
sessionStorage.setItem("wxagent-client", clientId);
};
const handleLogin = (nextToken, nextUser) => {
sessionStorage.setItem("wxagent-token", nextToken);
sessionStorage.setItem("wxagent-user", nextUser);
setToken(nextToken);
setUsername(nextUser);
};
const selectedNode = useMemo(() => nodes.find((node) => node.node_id === selectedClientId), [nodes, selectedClientId]);
const nav = NAV_ITEMS.find((item) => item.id === view);
const title = nav?.label || "设置与诊断";
const subtitle = nav?.description || "连接、账号和高级诊断信息";
if (!token) return ;
return {error && {error}
} setView("settings")} />{view === "messages" && setView("settings")} />}{view === "broadcast" && setView("settings")} />}{view === "contacts" && setView("settings")} />}{view === "tasks" && }{view === "settings" && }
;
}
createRoot(document.getElementById("root")).render();