431 lines
12 KiB
TypeScript
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",
|
|
);
|
|
}
|