WuYongJie 70f954382a 减轻风格化入口加载并防止新建项目超时重复创建
风格化入口并发下载完整配置时,新建项目的响应可能超过前端超时限制,重复点击会产生多个空项目。现在未选项目时只加载轻量配置,进入项目后再加载完整风格配置;配置未变化时按更新时间返回 304,复用已加载数据。配置下载超时覆盖响应正文读取,完整配置保留 30 秒读取时间。

新建请求使用稳定的项目编号,数据库按主键去重;丢失响应后按编号核对结果,尚未确认时保留编号供重试和刷新恢复。已有项目的内容与账号归属检查保持有效。

验证:8 项专项测试通过,辅助测试 633 项通过、1 项原有跳过,TypeScript、改动文件 lint、日志脱敏、凭据、环境、元数据、测试和制品检查通过。全量 lint 仍有 1005 项历史诊断;发布构建和线上验收另行核对。
2026-11-04 09:46:45 +08:00

188 lines
5.3 KiB
TypeScript

import type {
AgentGenerationProject,
AgentGenerationProjectPersistedState,
} from "@/types/agent";
interface ApiResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
status?: number;
}
const AGENT_PROJECT_REQUEST_TIMEOUT_MS = 15_000;
async function fetchAgentProjectApi<T>(
input: RequestInfo | URL,
init?: RequestInit
): Promise<ApiResponse<T>> {
const controller = new AbortController();
const timeoutId = globalThis.setTimeout(
() => controller.abort(),
AGENT_PROJECT_REQUEST_TIMEOUT_MS
);
try {
const response = await fetch(input, {
...init,
signal: controller.signal,
});
const result = await response.json();
if (!response.ok) result.status = response.status;
return result;
} finally {
globalThis.clearTimeout(timeoutId);
}
}
type CreateProjectPayload = {
title?: string;
description?: string | null;
thumbnail?: string | null;
projectState?: AgentGenerationProjectPersistedState;
};
const pendingCreations = new Map<string, {
id: string;
promise?: Promise<ApiResponse<AgentGenerationProject>>;
}>();
function pendingCreationStorage(key: string, value?: string | null) {
try {
if (typeof sessionStorage === "undefined") return null;
if (value === undefined) return sessionStorage.getItem(key);
if (value === null) sessionStorage.removeItem(key);
else sessionStorage.setItem(key, value);
} catch {
// Keep the in-memory request identity when browser storage is unavailable.
}
return null;
}
function getAgentProjectRequestErrorMessage(error: unknown, fallback: string) {
if (error instanceof DOMException && error.name === "AbortError") {
return `${fallback}:请求超时`;
}
return fallback;
}
export const agentProjectService = {
async getProjects(): Promise<ApiResponse<AgentGenerationProject[]>> {
try {
return await fetchAgentProjectApi<AgentGenerationProject[]>(
"/api/agent-projects",
{ cache: "no-store" }
);
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(
error,
"获取生图项目列表失败"
),
};
}
},
async getProject(id: string): Promise<ApiResponse<AgentGenerationProject>> {
try {
return await fetchAgentProjectApi<AgentGenerationProject>(
`/api/agent-projects/${id}`,
{ cache: "no-store" }
);
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "获取生图项目失败"),
};
}
},
async createProject(
payload: CreateProjectPayload,
options: { creationKey?: string } = {}
): Promise<ApiResponse<AgentGenerationProject>> {
const key = `agent-project:create:${options.creationKey || JSON.stringify(payload)}`;
let pending = pendingCreations.get(key);
if (!pending) {
pending = { id: pendingCreationStorage(key) || crypto.randomUUID() };
pendingCreations.set(key, pending);
pendingCreationStorage(key, pending.id);
}
if (pending.promise) return pending.promise;
const current = pending;
const clearPending = () => {
pendingCreations.delete(key);
pendingCreationStorage(key, null);
};
current.promise = (async () => {
try {
const result = await fetchAgentProjectApi<AgentGenerationProject>(
"/api/agent-projects",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...payload, id: current.id }),
}
);
if (result.success || (result.status && result.status < 500)) {
clearPending();
return result;
}
} catch {
// A lost response does not establish whether the project was created.
}
const recovered = await agentProjectService.getProject(current.id);
if (recovered.success && recovered.data) {
clearPending();
return recovered;
}
if (recovered.status === 401) return recovered;
return {
success: false,
error: "创建结果尚未确认,请重试核对或打开历史项目;重试不会重复创建。",
};
})().finally(() => { current.promise = undefined; });
return current.promise;
},
async updateProject(
id: string,
payload: {
title?: string;
description?: string | null;
thumbnail?: string | null;
projectState?: AgentGenerationProjectPersistedState;
}
): Promise<ApiResponse<AgentGenerationProject>> {
try {
return await fetchAgentProjectApi<AgentGenerationProject>(
`/api/agent-projects/${id}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
}
);
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "保存生图项目失败"),
};
}
},
async deleteProject(id: string): Promise<ApiResponse> {
try {
return await fetchAgentProjectApi(`/api/agent-projects/${id}`, {
method: "DELETE",
});
} catch (error) {
return {
success: false,
error: getAgentProjectRequestErrorMessage(error, "删除生图项目失败"),
};
}
},
};