import { useCallback, useEffect, useMemo, useState } from "react";
import { App as AntApp, Alert, Button, Card, Empty, Input, Select, Space, Statistic, Table, Tabs, Tag, Typography } from "antd";
import { ProCard, ProTable } from "@ant-design/pro-components";
import {
CheckCircleOutlined,
DesktopOutlined,
MessageOutlined,
ReloadOutlined,
SafetyCertificateOutlined,
SearchOutlined,
TeamOutlined,
UnorderedListOutlined,
WarningOutlined,
} from "@ant-design/icons";
import { api, activeAccount, dataPath, formatTime, isClientAvailable, normalizeContacts, shortId, statusColor, statusLabel } from "../api.js";
const { Text, Title } = Typography;
export function OverviewView({ nodes, tasks, events, onNavigate, onRefresh, refreshing }) {
const online = nodes.filter(isClientAvailable).length;
const pending = tasks.filter((task) => ["Pending", "WaitingForClient", "Accepted", "Running"].includes(task.status)).length;
const failed = tasks.filter((task) => ["Failed", "ResultUnconfirmed"].includes(task.status)).length;
const recent = [...events].sort((a, b) => new Date(b.received_at || 0) - new Date(a.received_at || 0)).slice(0, 5);
return (
实时运行状态真实连接状态 · 按用户分配范围隔离 · 管理操作留有审计记录
} loading={refreshing} onClick={onRefresh}>刷新概览
} />
} />
} valueStyle={{ color: failed ? "#cf1322" : undefined }} />
} />
onNavigate("agents")}>查看全部}>
{nodes.length === 0 ? : (
{nodes.slice(0, 6).map((node) =>
{node.node_id}{node.active_account_id || "尚未确认活动账号"}{statusLabel(node.status)}
)}
)}
} onClick={() => onNavigate("messages")}>消息工作台
} onClick={() => onNavigate("agents")}>Desktop Agent
} onClick={() => onNavigate("tasks")}>任务中心
} onClick={() => onNavigate("contacts")}>通讯录
最多展示 5 条}>
{recent.length ? {recent.map((event) =>
{event.chat_id || event.event_type || "授权事件"}{event.node_id} · {event.chat_type === "Group" ? "群聊" : "私聊"}
{formatTime(event.received_at)})}
: }
);
}
export function ContactsView({ token, client }) {
const { message } = AntApp.useApp();
const clientId = client?.node_id || "";
const accountId = activeAccount(client);
const [groupsOnly, setGroupsOnly] = useState(false);
const [query, setQuery] = useState("");
const [contacts, setContacts] = useState([]);
const [syncInfo, setSyncInfo] = useState(null);
const [hasMore, setHasMore] = useState(false);
const [syncing, setSyncing] = useState(false);
const [state, setState] = useState({ loading: false, error: "" });
const [requestVersion, setRequestVersion] = useState(0);
const load = useCallback(async (append = false) => {
if (!clientId || !accountId) return;
setState({ loading: true, error: "" });
try {
const params = new URLSearchParams({ limit: "200", offset: String(append ? contacts.length : 0), groups_only: String(groupsOnly) });
if (query.trim()) params.set("contains", query.trim());
const content = await api(dataPath(accountId, "contacts", `?${params.toString()}`), { token });
const next = normalizeContacts(content);
setContacts((current) => append ? [...current, ...next] : next);
setHasMore(Boolean(content.has_more));
setSyncInfo(content.sync || null);
setState({ loading: false, error: "" });
} catch (reason) {
setState({ loading: false, error: reason.message });
}
}, [accountId, clientId, contacts.length, groupsOnly, query, token]);
useEffect(() => {
setContacts([]);
setHasMore(false);
setSyncInfo(null);
if (clientId && accountId) void load();
}, [clientId, accountId, groupsOnly, requestVersion]);
const syncContacts = async () => {
if (!clientId || !accountId || syncing) return;
setSyncing(true);
try {
await api(dataPath(accountId, "refresh"), { token, method: "POST", body: { stream_key: "contacts" } });
message.success("通讯录同步任务已提交;完成后刷新缓存查看最新数据。");
} catch (reason) {
message.error(reason.message);
} finally {
setSyncing(false);
}
};
const visible = contacts.filter((contact) => !query || `${contact.title} ${contact.id} ${contact.detail}`.toLowerCase().includes(query.toLowerCase()));
const columns = [
{ title: "联系人 / 群聊", dataIndex: "title", render: (_, contact) => {contact.title?.slice(0, 1)}{contact.title}
{contact.type === "Group" ? "群聊" : "联系人"} },
{ title: "会话 ID", dataIndex: "id", render: (id) => {shortId(id, 32)} },
{ title: "备注 / 账号", dataIndex: "detail", render: (value) => value || "—" },
];
if (!client) return ;
return (
{syncInfo?.state || "尚未同步"}}>
setQuery(event.target.value)} onSearch={() => void load()} placeholder="搜索联系人或群聊" style={{ maxWidth: 360 }} />
} onClick={() => setRequestVersion((value) => value + 1)} loading={state.loading}>刷新缓存
{state.error && void load()}>重试} />}
最近同步:{formatTime(syncInfo?.last_success_at)} · {syncInfo?.backlog_count == null ? "积压未知" : `积压 ${syncInfo.backlog_count}`}
);
}
export function TasksView({ tasks, selectedNodeId }) {
const [filter, setFilter] = useState("all");
const filtered = tasks.filter((task) => task.node_id === selectedNodeId && (filter === "all" || task.status === filter));
const columns = [
{ title: "任务类型", dataIndex: "kind", width: 170, render: (value) => {value} },
{ title: "状态", dataIndex: "status", width: 140, render: (value) => {statusLabel(value)} },
{ title: "账号 / Node", dataIndex: "account_id", render: (value, task) => {value || "—"}
{task.node_id} },
{ title: "任务 ID", dataIndex: "task_id", render: (value) => {shortId(value, 24)} },
{ title: "执行说明", dataIndex: "result", render: (result, task) => task.status === "WaitingForClient" ? "Client 离线后等待显式恢复;不会自动重放写操作。" : result?.message || result?.error_code || "—" },
{ title: "更新时间", dataIndex: "updated_at", width: 175, render: formatTime },
];
if (!selectedNodeId) return ;
return [ ;
}
export function BroadcastView({ client }) {
return (
当前上下文:{client?.node_id || "未选择 Node"} · {activeAccount(client) || "未确认活动账号"}
冻结获准对象名单 → 预览确认 → 串行执行 → 查看逐项结果。消息发送仍需在 Windows 已登录会话完成真机验收。
}>范围约束任务 API 不自动重放等待 Web 验收
);
}
export function DiagnosticsView({ nodes, events, audit, selectedNodeId, onSelectNode }) {
const visibleEvents = events.filter((event) => !selectedNodeId || event.node_id === selectedNodeId);
const eventColumns = [
{ title: "时间", dataIndex: "received_at", width: 180, render: formatTime },
{ title: "Node", dataIndex: "node_id", width: 180 },
{ title: "会话", dataIndex: "chat_id", render: (value) => shortId(value, 28) },
{ title: "类型", dataIndex: "chat_type", width: 100, render: (value) => value === "Group" ? "群聊" : "私聊" },
{ title: "授权内容", dataIndex: "content", ellipsis: true },
];
const auditColumns = [
{ title: "时间", dataIndex: "at", width: 180, render: formatTime },
{ title: "操作者", dataIndex: "principal", width: 170 },
{ title: "操作", dataIndex: "action", width: 180 },
{ title: "资源", dataIndex: "resource" },
{ title: "结果", dataIndex: "result", width: 100 },
];
const nodeColumns = [
{ title: "Node", dataIndex: "node_id" },
{ title: "状态", dataIndex: "status", render: (value) => {statusLabel(value)} },
{ title: "活动账号", dataIndex: "active_account_id", render: (value) => value || "—" },
{ title: "最后心跳", dataIndex: "last_heartbeat_at", render: formatTime },
];
return (
},
{ key: "events", label: `授权事件 · ${visibleEvents.length}`, children: },
{ key: "audit", label: `操作审计 · ${audit.length}`, children: },
]} />
);
}