Files
deerflow-code/frontend-web/src/core/admin/hooks.ts
T
吉鑫升andClaude Opus 4.8 26f8010a57 feat(监控): 管理员排行榜实时并发监控 + 真正停止对话 + 409 中文化
- 排行榜页顶部新增「实时并发监控」面板(admin-only):当前并发大模型调用数 +
  调用列表(用户/智能体/类型/已运行时长),每行「停止」+「全部停止」经
  RunManager.cancel 真正中断 LLM 流式调用,减轻大模型压力
- 并发量折线图(峰值/均值,按分钟/小时):新增轻量后台采样器写入新表
  concurrency_samples(create_all 自动建表),可按分/时聚合
- 大模型出字速度折线图(tokens/秒,按分钟/小时):复用 llm_call_metrics 聚合,
  便于定位是模型/供应商慢
- 后台采样可开关、默认关闭(opt-in)、运行时切换免重启、对线上零影响
  (单条极小 INSERT、保留期清理、查询有上限、全程 best-effort)
- 409 中文化:上一轮未结束又发新问题时返回中文提示,前端 onError 友好映射

后端:runtime/runs/manager.py(list_active)、persistence/concurrency(新模块)、
config/system_settings(ConcurrencyMonitorSettings)、gateway/routers/admin_active_runs、
app.py(采样后台任务)、services.py(409)。前端:core/admin/{active-runs,hooks}、
components/workspace/admin/concurrency-monitor、AdminUserLeaderboardPage、threads/hooks。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 20:29:26 +08:00

326 lines
9.0 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
addAdminMemoryEntry,
addAdminV1Fact,
deleteAdminMemoryEntry,
deleteAdminV1Fact,
generateUserLeaderboardReport,
getUserMemory,
getUserAnalytics,
getUserLeaderboard,
refreshTodayLeaderboard,
listUserQa,
listUserThreadMessages,
listUserThreads,
replaceAdminMemoryEntry,
updateAdminV1Fact,
updateAdminV1Section,
} from "./api";
import {
cancelActiveRun,
cancelAllActiveRuns,
getMonitorSettings,
loadActiveRuns,
loadConcurrencySeries,
loadTokenSpeedSeries,
updateMonitorSettings,
type Granularity,
} from "./active-runs";
// ── 实时并发监控 (admin) ─────────────────────────────────────────────────────
/** Live snapshot of in-flight runs. Polls every `intervalMs` while `enabled`. */
export function useActiveRuns(enabled = true, intervalMs = 4000) {
return useQuery({
queryKey: ["admin-active-runs", "live"],
queryFn: loadActiveRuns,
enabled,
refetchInterval: enabled ? intervalMs : false,
refetchOnWindowFocus: false,
});
}
/** 并发量时序 (peak/avg per minute|hour). */
export function useConcurrencySeries(
granularity: Granularity,
hours: number | undefined,
enabled = true,
intervalMs = 15000,
) {
return useQuery({
queryKey: ["admin-active-runs", "concurrency", granularity, hours ?? null],
queryFn: () => loadConcurrencySeries(granularity, hours),
enabled,
refetchInterval: enabled ? intervalMs : false,
refetchOnWindowFocus: false,
});
}
/** 大模型 token 出字速度时序 (tokens/sec per minute|hour). */
export function useTokenSpeedSeries(
granularity: Granularity,
hours: number | undefined,
enabled = true,
intervalMs = 15000,
) {
return useQuery({
queryKey: ["admin-active-runs", "token-speed", granularity, hours ?? null],
queryFn: () => loadTokenSpeedSeries(granularity, hours),
enabled,
refetchInterval: enabled ? intervalMs : false,
refetchOnWindowFocus: false,
});
}
export function useCancelActiveRun() {
const qc = useQueryClient();
return useMutation({
mutationFn: (runId: string) => cancelActiveRun(runId),
onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-active-runs", "live"] }),
});
}
export function useCancelAllActiveRuns() {
const qc = useQueryClient();
return useMutation({
mutationFn: () => cancelAllActiveRuns(),
onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-active-runs", "live"] }),
});
}
export function useMonitorSettings(enabled = true) {
return useQuery({
queryKey: ["admin-active-runs", "settings"],
queryFn: getMonitorSettings,
enabled,
refetchOnWindowFocus: false,
});
}
export function useUpdateMonitorSettings() {
const qc = useQueryClient();
return useMutation({
mutationFn: (nextEnabled: boolean) => updateMonitorSettings(nextEnabled),
onSuccess: (data) => {
qc.setQueryData(["admin-active-runs", "settings"], data);
},
});
}
export function useUserLeaderboard(
range: {
since: string;
until: string;
days?: number;
},
limit = 20,
) {
return useQuery({
queryKey: [
"admin-activity",
"leaderboard",
range.since,
range.until,
limit,
],
queryFn: () => getUserLeaderboard(range, limit),
refetchInterval: (query) => {
const status = query.state.data?.status;
return status === "running" || status === "refreshing" ? 3000 : false;
},
});
}
/**
* Force-rebuild today's snapshot, then prime + invalidate the matching
* leaderboard query so the page shows the freshly recomputed numbers.
*/
export function useRefreshTodayLeaderboard() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({
range,
limit = 20,
}: {
range: { since: string; until: string; days?: number };
limit?: number;
}) => refreshTodayLeaderboard(range, limit),
onSuccess: (data, variables) => {
qc.setQueryData(
[
"admin-activity",
"leaderboard",
variables.range.since,
variables.range.until,
variables.limit ?? 20,
],
data,
);
qc.invalidateQueries({ queryKey: ["admin-activity", "leaderboard"] });
},
});
}
export function useUserAnalytics(
userId: string | null,
range: { since: string; until: string; days?: number },
limit = 20,
) {
return useQuery({
queryKey: ["admin-activity", "analytics", userId, range.since, range.until, limit],
queryFn: () => getUserAnalytics(userId!, range, limit),
enabled: !!userId,
});
}
export function useGenerateUserLeaderboardReport() {
return useMutation({
mutationFn: ({
range,
limit,
modelName,
}: {
range: { since: string; until: string; days?: number };
limit?: number;
modelName?: string;
}) => generateUserLeaderboardReport(range, limit ?? 30, modelName),
});
}
/** Threads owned by a given user. Disabled until a user is picked. */
export function useUserThreads(userId: string | null) {
return useQuery({
queryKey: ["admin-activity", "threads", userId],
queryFn: () => listUserThreads(userId!),
enabled: !!userId,
});
}
export function useUserQa(
userId: string | null,
params: { page: number; pageSize: number; q?: string },
) {
return useQuery({
queryKey: ["admin-activity", "qa", userId, params.page, params.pageSize, params.q ?? ""],
queryFn: () => listUserQa(userId!, params),
enabled: !!userId,
});
}
/** Messages of a given thread. Disabled until a thread is picked. */
export function useUserThreadMessages(
userId: string | null,
threadId: string | null,
) {
return useQuery({
queryKey: ["admin-activity", "messages", userId, threadId],
queryFn: () => listUserThreadMessages(userId!, threadId!),
enabled: !!userId && !!threadId,
});
}
/** All memory buckets of a given user. Disabled until a user is picked. */
export function useUserMemory(userId: string | null) {
return useQuery({
queryKey: ["admin-activity", "memory", userId],
queryFn: () => getUserMemory(userId!),
enabled: !!userId,
});
}
function useMemoryInvalidate(userId: string) {
const qc = useQueryClient();
return () => qc.invalidateQueries({ queryKey: ["admin-activity", "memory", userId] });
}
export function useAdminAddMemoryEntry(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({ content, agent }: { content: string; agent: string | null }) =>
addAdminMemoryEntry(userId, content, agent),
onSuccess: invalidate,
});
}
export function useAdminReplaceMemoryEntry(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({ oldText, content, agent }: { oldText: string; content: string; agent: string | null }) =>
replaceAdminMemoryEntry(userId, oldText, content, agent),
onSuccess: invalidate,
});
}
export function useAdminDeleteMemoryEntry(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({ oldText, agent }: { oldText: string; agent: string | null }) =>
deleteAdminMemoryEntry(userId, oldText, agent),
onSuccess: invalidate,
});
}
// ── V1 CRUD hooks ──────────────────────────────────────────────────────────
export function useAdminUpdateV1Section(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({
sectionType,
field,
summary,
agent,
}: {
sectionType: "user" | "history";
field: string;
summary: string;
agent: string | null;
}) => updateAdminV1Section(userId, sectionType, field, summary, agent),
onSuccess: invalidate,
});
}
export function useAdminAddV1Fact(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({
content,
category,
confidence,
agent,
}: {
content: string;
category: string;
confidence: number;
agent: string | null;
}) => addAdminV1Fact(userId, content, category, confidence, agent),
onSuccess: invalidate,
});
}
export function useAdminUpdateV1Fact(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({
factId,
updates,
agent,
}: {
factId: string;
updates: { content?: string; category?: string; confidence?: number };
agent: string | null;
}) => updateAdminV1Fact(userId, factId, updates, agent),
onSuccess: invalidate,
});
}
export function useAdminDeleteV1Fact(userId: string) {
const invalidate = useMemoryInvalidate(userId);
return useMutation({
mutationFn: ({ factId, agent }: { factId: string; agent: string | null }) =>
deleteAdminV1Fact(userId, factId, agent),
onSuccess: invalidate,
});
}