Files
next-tjwater-frontend/src/features/workbench/api/project-request.ts
T
jiang 692bbf609c refactor: consolidate requests and finalize agent tool states
Share project JSON transport, interaction replies and report selectors.
Settle tool progress consistently for direct streams, reconnects and history, preserving failure reasons and stopped state.
2026-11-09 17:55:00 +08:00

71 lines
2.9 KiB
TypeScript

import { z } from "zod";
export const PROJECT_REQUEST_TIMEOUT_MS = 15_000;
const READINESS_TIMEOUT_MS = 3_000;
const readinessSchema = z.object({
status: z.enum(["ready", "not_ready"]),
checks: z.object({
startup: z.string(),
metadata_database: z.string(),
mcp_transport: z.string()
})
});
const isTimeout = (error: unknown) => Boolean(error && typeof error === "object"
&& "name" in error && error.name === "TimeoutError");
async function describeServerReadiness(baseUrl: string): Promise<string> {
try {
const response = await fetch(`${baseUrl}/ready`, {
signal: AbortSignal.timeout(READINESS_TIMEOUT_MS),
headers: { Accept: "application/json" },
cache: "no-store"
});
const parsed = readinessSchema.safeParse(await response.json());
if (parsed.success) {
const { status, checks } = parsed.data;
if (checks.startup === "not_ready") {
return "后端服务正在启动,尚未准备好处理项目请求。请稍后重新加载。";
}
if (checks.metadata_database === "unavailable") {
return "后端项目元数据库暂不可用。请稍后重试,若持续失败,请检查后端数据库连接。";
}
if (checks.mcp_transport === "not_ready") {
return "后端查询服务尚未就绪。请稍后重新加载。";
}
if (response.ok && status === "ready") {
return "后端当前已就绪,请重新加载项目。";
}
}
return "暂时无法确认后端就绪状态。服务可能正在重启或响应缓慢,请稍后重新加载。";
} catch {
return "后端服务未响应,可能正在重启或热重载。请稍后重新加载,若持续失败,请检查后端服务。";
}
}
/** Keep caller cancellation distinct from a request deadline or unavailable backend. */
export async function withProjectRequest<T>(
baseUrl: string,
operation: string,
request: (signal: AbortSignal) => Promise<T>,
signal?: AbortSignal,
timeoutMs: number | null = PROJECT_REQUEST_TIMEOUT_MS
): Promise<T> {
const deadline = timeoutMs == null ? undefined : AbortSignal.timeout(timeoutMs);
const combined = signal && deadline ? AbortSignal.any([signal, deadline]) : signal ?? deadline ?? new AbortController().signal;
try {
combined.throwIfAborted();
return await request(combined);
} catch (error) {
if (signal?.aborted && !isTimeout(signal.reason)) throw signal.reason;
if (timeoutMs != null && (isTimeout(error) || (combined.aborted && isTimeout(combined.reason)))) {
const diagnosis = await describeServerReadiness(baseUrl);
throw new Error(`${operation}超时(已等待 ${timeoutMs / 1000} 秒)。${diagnosis}`, { cause: error });
}
if (error instanceof TypeError || (error instanceof DOMException && error.name === "NetworkError")) {
throw new Error(`${operation}失败:无法连接后端服务。请检查网络和后端服务状态后重试。`, { cause: error });
}
throw error;
}
}