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

610 lines
17 KiB
TypeScript

import { apiFetch } from "@/core/api";
import { getBackendBaseURL } from "@/core/config";
import { OptType, writeYLog } from "@/core/log/y-log";
/** 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 AdminUserQaItem {
run_id: string;
thread_id: string;
thread_title: string;
assistant_id: string;
status: string;
model_name: string;
question: string;
answer: string;
total_tokens: number;
created_at: string;
updated_at: string;
}
export interface AdminUserQaPageResponse {
user_id: string;
page: number;
page_size: number;
total: number;
items: AdminUserQaItem[];
}
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 type AdminLeaderboardDetailKind = "skill" | "tool" | "agent";
export interface AdminLeaderboardDetailItem {
name: string;
count: number;
user_count: number;
total_tokens: number;
avg_duration_ms: number;
}
export interface AdminLeaderboardDetailSummary {
count: number;
item_count: number;
user_count: number;
total_tokens: number;
avg_duration_ms: number;
}
export interface AdminLeaderboardDetailResponse {
kind: AdminLeaderboardDetailKind;
range_days: number;
since: string;
until: string;
summary: AdminLeaderboardDetailSummary;
items: AdminLeaderboardDetailItem[];
}
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;
status?: "ready" | "running" | "refreshing";
generated_at?: string;
message?: 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[];
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[]> {
const url = `${base()}/${encodeURIComponent(userId)}/threads?limit=200`;
const result = await parse<AdminThread[]>(await apiFetch(url), "Failed to load threads");
writeYLog({
optType: OptType.search,
optDetail: "CM助手-全量-管理后台-查询用户会话列表",
url,
reqParam: { userId },
returnParam: { count: result.length },
});
return result;
}
export async function listUserThreadMessages(
userId: string,
threadId: string,
): Promise<AdminMessage[]> {
const url = `${base()}/${encodeURIComponent(userId)}/threads/${encodeURIComponent(threadId)}/messages`;
const result = await parse<AdminMessage[]>(await apiFetch(url), "Failed to load messages");
writeYLog({
optType: OptType.view,
optDetail: "CM助手-全量-管理后台-查看会话消息",
url,
reqParam: { userId, threadId },
returnParam: { count: result.length },
});
return result;
}
export async function listUserQa(
userId: string,
params: { page?: number; pageSize?: number; q?: string } = {},
): Promise<AdminUserQaPageResponse> {
const search = new URLSearchParams({
page: String(params.page ?? 1),
page_size: String(params.pageSize ?? 20),
});
const query = (params.q ?? "").trim();
if (query) search.set("q", query);
const url = `${base()}/${encodeURIComponent(userId)}/qa?${search}`;
const result = await parse<AdminUserQaPageResponse>(await apiFetch(url), "Failed to load user Q&A");
writeYLog({
optType: OptType.search,
optDetail: "CM助手-全量-管理后台-查询用户问答",
url,
reqParam: { userId, ...params },
returnParam: { total: result.total, count: result.items.length },
});
return result;
}
export async function getUserMemory(
userId: string,
): Promise<AdminMemoryResponse> {
const url = `${base()}/${encodeURIComponent(userId)}/memory`;
const result = await parse<AdminMemoryResponse>(await apiFetch(url), "Failed to load memory");
writeYLog({
optType: OptType.view,
optDetail: "CM助手-全量-管理后台-查看用户记忆",
url,
reqParam: { userId },
});
return result;
}
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),
});
const url = `${base()}/leaderboard?${params}`;
const result = await parse<AdminLeaderboardResponse>(await apiFetch(url), "Failed to load leaderboard");
writeYLog({
optType: OptType.search,
optDetail: "CM助手-全量-管理后台-查询用户排行榜",
url,
reqParam: { ...range, limit },
});
return result;
}
export async function getUserLeaderboardDetails(
kind: AdminLeaderboardDetailKind,
range: {
since: string;
until: string;
days?: number;
},
limit = 500,
): Promise<AdminLeaderboardDetailResponse> {
const params = new URLSearchParams({
kind,
days: String(range.days ?? 30),
since: range.since,
until: range.until,
limit: String(limit),
});
const url = `${base()}/leaderboard/details?${params}`;
const result = await parse<AdminLeaderboardDetailResponse>(
await apiFetch(url),
"Failed to load leaderboard details",
);
writeYLog({
optType: OptType.search,
optDetail: "CM助手-全量-管理后台-查询排行榜维度详情",
url,
reqParam: { kind, ...range, limit },
returnParam: { itemCount: result.items.length },
});
return result;
}
/**
* Force a synchronous rebuild of *today's* leaderboard snapshot so the latest
* real-time records are included, then return the leaderboard for `range`.
*/
export async function refreshTodayLeaderboard(
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),
});
const url = `${base()}/leaderboard/refresh-today?${params}`;
const result = await parse<AdminLeaderboardResponse>(
await apiFetch(url, { method: "POST" }),
"Failed to refresh today's leaderboard",
);
writeYLog({
optType: OptType.other,
optDetail: "CM助手-全量-管理后台-刷新今日排行榜",
url,
reqParam: { ...range, limit },
});
return result;
}
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);
const url = `${base()}/leaderboard/report?${params}`;
const result = await parse<AdminLeaderboardReportResponse>(
await apiFetch(url, { method: "POST" }),
"Failed to generate leaderboard report",
);
writeYLog({
optType: OptType.other,
optDetail: "CM助手-全量-管理后台-生成排行榜报告",
url,
reqParam: { ...range, limit, modelName },
returnParam: { generated_at: result.generated_at, model_name: result.model_name },
});
return result;
}
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 url = `${base()}/leaderboard/report/stream?${params}`;
const response = await apiFetch(url, {
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);
}
writeYLog({
optType: OptType.other,
optDetail: "CM助手-全量-管理后台-流式生成排行榜报告",
url,
reqParam: { ...range, limit, modelName },
});
}
export async function addAdminMemoryEntry(
userId: string,
content: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
const url = `${base()}/${encodeURIComponent(userId)}/memory/entries`;
const result = await parse<AdminMemoryBucket>(
await apiFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, agent }),
}),
"Failed to add memory entry",
);
writeYLog({
optType: OptType.create,
optDetail: "CM助手-全量-管理后台-新增记忆条目",
url,
reqParam: { userId, content, agent },
});
return result;
}
export async function replaceAdminMemoryEntry(
userId: string,
oldText: string,
content: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
const url = `${base()}/${encodeURIComponent(userId)}/memory/entries`;
const result = await parse<AdminMemoryBucket>(
await apiFetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ old_text: oldText, content, agent }),
}),
"Failed to replace memory entry",
);
writeYLog({
optType: OptType.edit,
optDetail: "CM助手-全量-管理后台-编辑记忆条目",
url,
reqParam: { userId, old_text: oldText, content, agent },
});
return result;
}
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);
const url = `${base()}/${encodeURIComponent(userId)}/memory/entries?${params}`;
const result = await parse<AdminMemoryBucket>(
await apiFetch(url, { method: "DELETE" }),
"Failed to delete memory entry",
);
writeYLog({
optType: OptType.delete,
optDetail: "CM助手-全量-管理后台-删除记忆条目",
url,
reqParam: { userId, old_text: oldText, agent },
});
return result;
}
// ── V1 Memory CRUD ──────────────────────────────────────────────────────────
export async function updateAdminV1Section(
userId: string,
sectionType: "user" | "history",
field: string,
summary: string,
agent: string | null,
): Promise<AdminMemoryBucket> {
const url = `${base()}/${encodeURIComponent(userId)}/memory/v1/sections`;
const result = await parse<AdminMemoryBucket>(
await apiFetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ section_type: sectionType, field, summary, agent }),
}),
"Failed to update V1 section",
);
writeYLog({
optType: OptType.edit,
optDetail: "CM助手-全量-管理后台-编辑记忆段落",
url,
reqParam: { userId, section_type: sectionType, field, summary, agent },
});
return result;
}
export async function addAdminV1Fact(
userId: string,
content: string,
category: string,
confidence: number,
agent: string | null,
): Promise<AdminMemoryBucket> {
const url = `${base()}/${encodeURIComponent(userId)}/memory/v1/facts`;
const result = await parse<AdminMemoryBucket>(
await apiFetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, category, confidence, agent }),
}),
"Failed to add V1 fact",
);
writeYLog({
optType: OptType.create,
optDetail: "CM助手-全量-管理后台-新增记忆事实",
url,
reqParam: { userId, content, category, confidence, agent },
});
return result;
}
export async function updateAdminV1Fact(
userId: string,
factId: string,
updates: { content?: string; category?: string; confidence?: number },
agent: string | null,
): Promise<AdminMemoryBucket> {
const url = `${base()}/${encodeURIComponent(userId)}/memory/v1/facts/${encodeURIComponent(factId)}`;
const result = await parse<AdminMemoryBucket>(
await apiFetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...updates, agent }),
}),
"Failed to update V1 fact",
);
writeYLog({
optType: OptType.edit,
optDetail: "CM助手-全量-管理后台-编辑记忆事实",
url,
reqParam: { userId, factId, ...updates, agent },
});
return result;
}
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}` : "";
const url = `${base()}/${encodeURIComponent(userId)}/memory/v1/facts/${encodeURIComponent(factId)}${qs}`;
const result = await parse<AdminMemoryBucket>(
await apiFetch(url, { method: "DELETE" }),
"Failed to delete V1 fact",
);
writeYLog({
optType: OptType.delete,
optDetail: "CM助手-全量-管理后台-删除记忆事实",
url,
reqParam: { userId, factId, agent },
});
return result;
}