- 排行榜页顶部新增「实时并发监控」面板(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>
326 lines
9.0 KiB
TypeScript
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,
|
|
});
|
|
}
|