Files
deerflow-code/frontend-web/src/core/admin/api.ts
T

431 lines
12 KiB
TypeScript

import { apiFetch } from "@/core/api";
import { getBackendBaseURL } from "@/core/config";
/** A conversation thread owned by some user (admin view). */
export interface AdminThread {
thread_id: string;
display_name: string;
status: string;
created_at: string;
updated_at: string;
}
/**
* One conversation message, as serialized from the LangGraph checkpoint —
* a LangChain message dump: ``type`` is "human" | "ai" | "tool" | "system",
* ``content`` is a string or a content-block list.
*/
export interface AdminMessage {
type?: string;
content?: unknown;
id?: string | null;
name?: string | null;
}
/** One memory bucket: the global file or a per-agent file. */
export interface AdminMemoryBucket {
name: string;
display_name?: string;
data: Record<string, unknown>;
}
export interface AdminMemoryResponse {
user_id: string;
version: "v1" | "v2";
buckets: AdminMemoryBucket[];
}
export interface AdminLeaderboardUser {
user_id: string;
email: string;
system_role: string;
thread_count: number;
run_count: number;
qa_count: number;
llm_call_count: number;
tool_call_count: number;
skill_call_count: number;
total_tokens: number;
input_tokens: number;
output_tokens: number;
last_active_at: string;
}
export interface AdminLeaderboardSkill {
name: string;
call_count: number;
user_count: number;
success_count: number;
error_count: number;
avg_duration_ms: number;
last_used_at: string;
}
export interface AdminLeaderboardDimension {
name: string;
count: number;
user_count: number;
success_count: number;
error_count: number;
total_tokens: number;
last_seen_at: string;
}
export interface AdminLeaderboardQuestion {
text: string;
user_id: string;
email: string;
thread_id: string;
run_id: string;
asked_at: string;
}
export interface AdminLeaderboardTrendPoint {
date: string;
runs: number;
questions: number;
tokens: number;
users: number;
}
export interface AdminLeaderboardOverview {
total_users: number;
active_users: number;
total_threads: number;
total_runs: number;
total_questions: number;
total_tokens: number;
total_skill_calls: number;
total_tool_calls: number;
error_runs: number;
}
export interface AdminLeaderboardResponse {
range_days: number;
since: string;
until: string;
overview: AdminLeaderboardOverview;
users_by_activity: AdminLeaderboardUser[];
users_by_questions: AdminLeaderboardUser[];
users_by_tokens: AdminLeaderboardUser[];
skills: AdminLeaderboardSkill[];
tools: AdminLeaderboardSkill[];
models: AdminLeaderboardDimension[];
agents: AdminLeaderboardDimension[];
run_statuses: AdminLeaderboardDimension[];
recent_questions: AdminLeaderboardQuestion[];
frequent_questions: AdminLeaderboardQuestion[];
trends: AdminLeaderboardTrendPoint[];
}
export interface AdminUserAnalyticsResponse {
range_days: number;
since: string;
until: string;
user: AdminLeaderboardUser;
overview: AdminLeaderboardOverview;
skills: AdminLeaderboardSkill[];
tools: AdminLeaderboardSkill[];
models: AdminLeaderboardDimension[];
agents: AdminLeaderboardDimension[];
run_statuses: AdminLeaderboardDimension[];
recent_questions: AdminLeaderboardQuestion[];
trends: AdminLeaderboardTrendPoint[];
}
export interface AdminLeaderboardReportResponse {
report: string;
generated_at: string;
cached?: boolean;
model_name?: string | null;
}
export type AdminLeaderboardReportStreamEvent =
| { event: "meta"; data: { cached?: boolean; model_name?: string | null } }
| { event: "cached"; data: { report: string; generated_at?: string | null; model_name?: string | null } }
| { event: "thinking"; data: { chunk: string } }
| { event: "text"; data: { chunk: string } }
| { event: "done"; data: { cached?: boolean; generated_at?: string | null; model_name?: string | null; report?: string } }
| { event: "error"; data: { message: string } };
const base = () => `${getBackendBaseURL()}/api/admin/users`;
async function parse<T>(res: Response, what: string): Promise<T> {
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `${what} (HTTP ${res.status})`);
}
return res.json() as Promise<T>;
}
export async function listUserThreads(userId: string): Promise<AdminThread[]> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/threads?limit=200`),
"Failed to load threads",
);
}
export async function listUserThreadMessages(
userId: string,
threadId: string,
): Promise<AdminMessage[]> {
return parse(
await apiFetch(
`${base()}/${encodeURIComponent(userId)}/threads/${encodeURIComponent(threadId)}/messages`,
),
"Failed to load messages",
);
}
export async function getUserMemory(
userId: string,
): Promise<AdminMemoryResponse> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory`),
"Failed to load memory",
);
}
export async function getUserLeaderboard(
range: {
since: string;
until: string;
days?: number;
},
limit = 20,
): Promise<AdminLeaderboardResponse> {
const params = new URLSearchParams({
days: String(range.days ?? 30),
since: range.since,
until: range.until,
limit: String(limit),
});
return parse(
await apiFetch(`${base()}/leaderboard?${params}`),
"Failed to load leaderboard",
);
}
export async function getUserAnalytics(
userId: string,
range: { since: string; until: string; days?: number },
limit = 20,
): Promise<AdminUserAnalyticsResponse> {
const params = new URLSearchParams({
days: String(range.days ?? 30),
since: range.since,
until: range.until,
limit: String(limit),
});
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/analytics?${params}`),
"Failed to load user analytics",
);
}
export async function generateUserLeaderboardReport(
range: { since: string; until: string; days?: number },
limit = 30,
modelName?: string,
): Promise<AdminLeaderboardReportResponse> {
const params = new URLSearchParams({
days: String(range.days ?? 30),
since: range.since,
until: range.until,
limit: String(limit),
});
if (modelName) params.set("model_name", modelName);
return parse(
await apiFetch(`${base()}/leaderboard/report?${params}`, {
method: "POST",
}),
"Failed to generate leaderboard report",
);
}
function parseSseFrame(frame: string): AdminLeaderboardReportStreamEvent | null {
let event = "message";
const dataLines: string[] = [];
for (const rawLine of frame.split(/\r?\n/)) {
const line = rawLine.trimEnd();
if (!line || line.startsWith(":")) continue;
if (line.startsWith("event:")) {
event = line.slice("event:".length).trim();
} else if (line.startsWith("data:")) {
dataLines.push(line.slice("data:".length).trimStart());
}
}
if (!dataLines.length) return null;
const data = JSON.parse(dataLines.join("\n")) as AdminLeaderboardReportStreamEvent["data"];
return { event, data } as AdminLeaderboardReportStreamEvent;
}
export async function streamUserLeaderboardReport({
range,
limit = 30,
modelName,
signal,
onEvent,
}: {
range: { since: string; until: string; days?: number };
limit?: number;
modelName?: string;
signal?: AbortSignal;
onEvent: (event: AdminLeaderboardReportStreamEvent) => void;
}) {
const params = new URLSearchParams({
days: String(range.days ?? 30),
since: range.since,
until: range.until,
limit: String(limit),
});
if (modelName) params.set("model_name", modelName);
const response = await apiFetch(`${base()}/leaderboard/report/stream?${params}`, {
method: "POST",
signal,
});
if (!response.ok) {
const err = (await response.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to stream leaderboard report (HTTP ${response.status})`);
}
if (!response.body) {
throw new Error("当前浏览器不支持流式报告读取");
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
for (;;) {
const { done, value } = await reader.read();
buffer += decoder.decode(value ?? new Uint8Array(), { stream: !done });
const frames = buffer.split(/\r?\n\r?\n/);
buffer = frames.pop() ?? "";
for (const frame of frames) {
const parsed = parseSseFrame(frame);
if (parsed) onEvent(parsed);
}
if (done) break;
}
const tail = buffer.trim();
if (tail) {
const parsed = parseSseFrame(tail);
if (parsed) onEvent(parsed);
}
}
export async function addAdminMemoryEntry(
userId: string,
content: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/entries`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, agent }),
}),
"Failed to add memory entry",
);
}
export async function replaceAdminMemoryEntry(
userId: string,
oldText: string,
content: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/entries`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ old_text: oldText, content, agent }),
}),
"Failed to replace memory entry",
);
}
export async function deleteAdminMemoryEntry(
userId: string,
oldText: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
const params = new URLSearchParams({ old_text: oldText });
if (agent) params.set("agent", agent);
return parse(
await apiFetch(
`${base()}/${encodeURIComponent(userId)}/memory/entries?${params}`,
{ method: "DELETE" },
),
"Failed to delete memory entry",
);
}
// ── V1 Memory CRUD ──────────────────────────────────────────────────────────
export async function updateAdminV1Section(
userId: string,
sectionType: "user" | "history",
field: string,
summary: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/v1/sections`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ section_type: sectionType, field, summary, agent }),
}),
"Failed to update V1 section",
);
}
export async function addAdminV1Fact(
userId: string,
content: string,
category: string,
confidence: number,
agent: string | null,
): Promise<AdminMemoryBucket> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/v1/facts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, category, confidence, agent }),
}),
"Failed to add V1 fact",
);
}
export async function updateAdminV1Fact(
userId: string,
factId: string,
updates: { content?: string; category?: string; confidence?: number },
agent: string | null,
): Promise<AdminMemoryBucket> {
return parse(
await apiFetch(`${base()}/${encodeURIComponent(userId)}/memory/v1/facts/${encodeURIComponent(factId)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...updates, agent }),
}),
"Failed to update V1 fact",
);
}
export async function deleteAdminV1Fact(
userId: string,
factId: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
const params = new URLSearchParams();
if (agent) params.set("agent", agent);
const qs = params.toString() ? `?${params}` : "";
return parse(
await apiFetch(
`${base()}/${encodeURIComponent(userId)}/memory/v1/facts/${encodeURIComponent(factId)}${qs}`,
{ method: "DELETE" },
),
"Failed to delete V1 fact",
);
}