风格化入口并发下载完整配置时,新建项目的响应可能超过前端超时限制,重复点击会产生多个空项目。现在未选项目时只加载轻量配置,进入项目后再加载完整风格配置;配置未变化时按更新时间返回 304,复用已加载数据。配置下载超时覆盖响应正文读取,完整配置保留 30 秒读取时间。 新建请求使用稳定的项目编号,数据库按主键去重;丢失响应后按编号核对结果,尚未确认时保留编号供重试和刷新恢复。已有项目的内容与账号归属检查保持有效。 验证:8 项专项测试通过,辅助测试 633 项通过、1 项原有跳过,TypeScript、改动文件 lint、日志脱敏、凭据、环境、元数据、测试和制品检查通过。全量 lint 仍有 1005 项历史诊断;发布构建和线上验收另行核对。
188 lines
5.3 KiB
TypeScript
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, "删除生图项目失败"),
|
|
};
|
|
}
|
|
},
|
|
};
|