ai-chatbot/lib/agents/psd-template-timeout.ts
2026-06-04 11:39:53 +08:00

196 lines
5.4 KiB
TypeScript

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;
}