196 lines
5.4 KiB
TypeScript
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;
|
|
}
|