import "server-only"; import { upsertSystemSetting } from "@/lib/db/queries"; import { AGENT_PSD_TEMPLATES_KEY, type AgentPsdTemplate, } from "@/lib/agents/psd-template-config"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; function getEnvPositiveInt(name: string, fallback: number) { const parsed = Number.parseInt(process.env[name] || "", 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } export const AGENT_PSD_TEMPLATE_QUICK_PARSE_TIMEOUT_MS = getEnvPositiveInt( "AGENT_PSD_TEMPLATE_QUICK_PARSE_TIMEOUT_MS", 15 * 60 * 1000 ); export const AGENT_PSD_TEMPLATE_ASSET_BACKFILL_TIMEOUT_MS = getEnvPositiveInt( "AGENT_PSD_TEMPLATE_ASSET_BACKFILL_TIMEOUT_MS", 20 * 60 * 1000 ); export const AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS = getEnvPositiveInt( "AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS", 45 * 60 * 1000 ); function getIsoTime(value?: string) { const time = new Date(value || "").getTime(); return Number.isFinite(time) ? time : null; } function getTemplateHeartbeatTime(template: AgentPsdTemplate) { const heartbeatAt = getIsoTime(template.parseHeartbeatAt); if (heartbeatAt) { return heartbeatAt; } const updatedAt = getIsoTime(template.updatedAt); if (updatedAt) { return updatedAt; } return getIsoTime(template.createdAt); } function getTemplateParseStartedAtTime(template: AgentPsdTemplate) { const startedAt = getIsoTime(template.parseStartedAt); if (startedAt) { return startedAt; } const updatedAt = getIsoTime(template.updatedAt); if (updatedAt) { return updatedAt; } return getIsoTime(template.createdAt); } function hasPassedFastParsePhase(template: AgentPsdTemplate) { return ( Number(template.parseProgress || 0) >= 48 || template.layers.length > 0 || (Number(template.width || 0) > 1 && Number(template.height || 0) > 1) ); } export function getAgentPsdTemplateParseTimeoutMs(template: AgentPsdTemplate) { return hasPassedFastParsePhase(template) ? AGENT_PSD_TEMPLATE_ASSET_BACKFILL_TIMEOUT_MS : AGENT_PSD_TEMPLATE_QUICK_PARSE_TIMEOUT_MS; } export function getAgentPsdTemplateParseAgeMs( template: AgentPsdTemplate, now = Date.now() ) { const heartbeatAt = getTemplateHeartbeatTime(template); if (!heartbeatAt) { return Number.POSITIVE_INFINITY; } return Math.max(0, now - heartbeatAt); } export function getAgentPsdTemplateParseDurationMs( template: AgentPsdTemplate, now = Date.now() ) { const startedAt = getTemplateParseStartedAtTime(template); if (!startedAt) { return Number.POSITIVE_INFINITY; } return Math.max(0, now - startedAt); } export function isAgentPsdTemplateParseTimedOut( template: AgentPsdTemplate, now = Date.now() ) { if (template.parseStatus !== "processing") { return false; } return ( getAgentPsdTemplateParseAgeMs(template, now) > getAgentPsdTemplateParseTimeoutMs(template) || getAgentPsdTemplateParseDurationMs(template, now) > AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS ); } function buildTimeoutMessage(template: AgentPsdTemplate, now = Date.now()) { const phase = hasPassedFastParsePhase(template) ? "后台图层资产补全" : "PSD 快速解析"; const stage = template.parseStage ? `,最后阶段:${template.parseStage}` : ""; if ( getAgentPsdTemplateParseDurationMs(template, now) > AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS ) { const timeoutMinutes = Math.round( AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS / 60_000 ); return `${phase}总耗时超过 ${timeoutMinutes} 分钟,系统已自动结束本次任务${stage}。请检查 PSD 是否过大、图层是否异常,或重新上传后再解析。`; } const timeoutMinutes = Math.round( getAgentPsdTemplateParseTimeoutMs(template) / 60_000 ); return `${phase}超过 ${timeoutMinutes} 分钟没有收到解析心跳,系统已自动结束本次任务${stage}。可检查 PSD 文件后重新解析。`; } export function failTimedOutAgentPsdTemplatesInMemory(params: { templates: AgentPsdTemplate[]; now?: Date; }) { const now = params.now || new Date(); let changed = false; const timedOutTemplates: AgentPsdTemplate[] = []; const templates = params.templates.map((template) => { if (!isAgentPsdTemplateParseTimedOut(template, now.getTime())) { return template; } changed = true; const nextTemplate: AgentPsdTemplate = { ...template, parseStatus: "failed", parseProgress: Math.min( 95, Math.max(0, Math.round(Number(template.parseProgress || 0))) ), parseStage: hasPassedFastParsePhase(template) ? "后台图层资产补全超时" : "PSD 快速解析超时", parseError: buildTimeoutMessage(template, now.getTime()), parseHeartbeatAt: now.toISOString(), updatedAt: now.toISOString(), }; timedOutTemplates.push(nextTemplate); return nextTemplate; }); return { changed, templates, timedOutTemplates, }; } export async function failTimedOutAgentPsdTemplates(params: { updatedBy: string; now?: Date; }) { const currentTemplates = await readAgentPsdTemplates(); const result = failTimedOutAgentPsdTemplatesInMemory({ templates: currentTemplates, now: params.now, }); if (result.changed) { await upsertSystemSetting({ key: AGENT_PSD_TEMPLATES_KEY, value: result.templates, updatedBy: params.updatedBy, }); } return result; }