feat(roundtable): 会商诊断日志 + 后台挂起卡死兜底(idle 超时)
- 新增会商全流程诊断日志:新表 roundtable_diagnostics + 仓储 + admin API (/api/roundtable-diagnostics) + 管理员诊断页(会商页右上角设置按钮入口), 覆盖第一/二/三步及后台执行器/进程内网关各处报错与关键事件,支持按 范围/阶段/级别/事件/关键词筛选 + 分页。诊断写入全程 best-effort,绝不打断会商。 - 修复后台挂起「执行完第一个席位后永久卡死」:进程内网关每轮 agent run 改为 idle(无活动)超时(默认 20min)——只要还在持续产出 token/工具事件就一直等, 仅当连续无任何输出才判卡死并取消+报错+落 seat_turn_timeout 诊断;慢但活着的 生成绝不打断。可经 ROUNDTABLE_SEAT_IDLE_TIMEOUT_SECONDS 调整。 - 一并提交进行中的会商 seat-skill 注入 / 业务链条编辑器相关改动。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8ca16c74bc
commit
4a403c95c5
27 files changed
+1873
-61
No files matched your search
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 圆桌会商诊断日志(管理员)API 客户端 + React Query hooks。
|
||||
*
|
||||
* 后端:`/api/roundtable-diagnostics`(见 app/gateway/routers/roundtable_diagnostics.py)。
|
||||
* 全部 admin-gated。snake_case ⇄ camelCase 在此边界收敛。
|
||||
*/
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/roundtable-diagnostics`;
|
||||
|
||||
export interface DiagnosticItem {
|
||||
id: string;
|
||||
scope: string;
|
||||
stage: string;
|
||||
level: string;
|
||||
event: string;
|
||||
message: string | null;
|
||||
detail: unknown;
|
||||
jobId: string | null;
|
||||
draftId: string | null;
|
||||
taskId: string | null;
|
||||
userId: string | null;
|
||||
agentId: string | null;
|
||||
agentName: string | null;
|
||||
cycle: number | null;
|
||||
createdAt: string | null;
|
||||
}
|
||||
|
||||
export interface DiagnosticListResponse {
|
||||
items: DiagnosticItem[];
|
||||
total: number;
|
||||
limit: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export interface DiagnosticFacets {
|
||||
scope: string[];
|
||||
stage: string[];
|
||||
level: string[];
|
||||
event: string[];
|
||||
}
|
||||
|
||||
export interface DiagnosticFilters {
|
||||
scope?: string;
|
||||
stage?: string;
|
||||
level?: string;
|
||||
event?: string;
|
||||
jobId?: string;
|
||||
draftId?: string;
|
||||
taskId?: string;
|
||||
userId?: string;
|
||||
q?: string;
|
||||
since?: string;
|
||||
until?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
async function readJson<T>(res: Response, ctx: string): Promise<T> {
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => null as unknown);
|
||||
const detail =
|
||||
data && typeof data === "object" && "detail" in data
|
||||
? String((data as { detail: unknown }).detail)
|
||||
: `${ctx} failed (${res.status})`;
|
||||
throw new Error(detail);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
function buildQuery(f: DiagnosticFilters): string {
|
||||
const p = new URLSearchParams();
|
||||
const map: Record<string, string | number | undefined> = {
|
||||
scope: f.scope,
|
||||
stage: f.stage,
|
||||
level: f.level,
|
||||
event: f.event,
|
||||
job_id: f.jobId,
|
||||
draft_id: f.draftId,
|
||||
task_id: f.taskId,
|
||||
user_id: f.userId,
|
||||
q: f.q,
|
||||
since: f.since,
|
||||
until: f.until,
|
||||
limit: f.limit,
|
||||
offset: f.offset,
|
||||
};
|
||||
for (const [k, v] of Object.entries(map)) {
|
||||
if (v !== undefined && v !== null && String(v).length > 0) p.set(k, String(v));
|
||||
}
|
||||
const qs = p.toString();
|
||||
return qs ? `?${qs}` : "";
|
||||
}
|
||||
|
||||
export async function listDiagnostics(f: DiagnosticFilters): Promise<DiagnosticListResponse> {
|
||||
const res = await apiFetch(`${BASE()}${buildQuery(f)}`, { method: "GET" });
|
||||
return readJson<DiagnosticListResponse>(res, "list diagnostics");
|
||||
}
|
||||
|
||||
export async function getDiagnosticFacets(): Promise<DiagnosticFacets> {
|
||||
const res = await apiFetch(`${BASE()}/facets`, { method: "GET" });
|
||||
return readJson<DiagnosticFacets>(res, "diagnostic facets");
|
||||
}
|
||||
|
||||
// ── React Query hooks ──────────────────────────────────────────────────────
|
||||
|
||||
const KEY = "roundtable-diagnostics";
|
||||
|
||||
export function useRoundtableDiagnostics(filters: DiagnosticFilters, enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: [KEY, filters],
|
||||
queryFn: () => listDiagnostics(filters),
|
||||
enabled,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRoundtableDiagnosticFacets(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: [KEY, "facets"],
|
||||
queryFn: getDiagnosticFacets,
|
||||
enabled,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user