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 { 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( baseUrl: string, operation: string, request: (signal: AbortSignal) => Promise, signal?: AbortSignal ): Promise { const deadline = AbortSignal.timeout(PROJECT_REQUEST_TIMEOUT_MS); const combined = signal ? AbortSignal.any([signal, deadline]) : deadline; try { combined.throwIfAborted(); return await request(combined); } catch (error) { if (signal?.aborted && !isTimeout(signal.reason)) throw signal.reason; if (isTimeout(error) || (combined.aborted && isTimeout(combined.reason))) { const diagnosis = await describeServerReadiness(baseUrl); throw new Error(`${operation}超时(已等待 15 秒)。${diagnosis}`, { cause: error }); } if (error instanceof TypeError || (error instanceof DOMException && error.name === "NetworkError")) { throw new Error(`${operation}失败:无法连接后端服务。请检查网络和后端服务状态后重试。`, { cause: error }); } throw error; } }