ai-chatbot/lib/agents/psd-remote-asset.ts
Your Name 02f13a045a
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: stabilize psd template export rendering
2026-04-10 14:28:19 +08:00

116 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
const DEFAULT_TIMEOUT_MS = 30_000;
const DEFAULT_RETRY_COUNT = 3;
const RETRY_DELAY_MS = 1_000;
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function describeRemoteAssetLocation(url: string) {
const normalizedUrl = String(url || "").trim();
if (!normalizedUrl) {
return "未知地址";
}
try {
const parsed = new URL(normalizedUrl);
return parsed.host || parsed.hostname || normalizedUrl;
} catch {
return normalizedUrl.length > 80 ? `${normalizedUrl.slice(0, 77)}...` : normalizedUrl;
}
}
function isRetryableStatus(status: number) {
return status === 408 || status === 425 || status === 429 || status >= 500;
}
function isRetryableFetchError(error: unknown) {
if (!(error instanceof Error)) {
return true;
}
const message = `${error.name} ${error.message}`.toLowerCase();
return (
error.name === "AbortError" ||
message.includes("fetch failed") ||
message.includes("timeout") ||
message.includes("timed out") ||
message.includes("econnreset") ||
message.includes("socket") ||
message.includes("network") ||
message.includes("terminated") ||
message.includes("und_err")
);
}
export async function fetchRemoteAssetBuffer(
url: string,
options?: {
label?: string;
timeoutMs?: number;
retries?: number;
}
) {
const normalizedUrl = String(url || "").trim();
const label = String(options?.label || "远程资源").trim() || "远程资源";
if (!normalizedUrl) {
throw new Error(`${label}地址为空`);
}
const attempts = Math.max(1, Math.round(Number(options?.retries || DEFAULT_RETRY_COUNT)));
const timeoutMs = Math.max(5_000, Math.round(Number(options?.timeoutMs || DEFAULT_TIMEOUT_MS)));
const location = describeRemoteAssetLocation(normalizedUrl);
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const response = await fetch(normalizedUrl, {
cache: "no-store",
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) {
const statusError = new Error(`${label}下载失败(${response.status})`);
if (attempt < attempts && isRetryableStatus(response.status)) {
console.warn("[PSD Remote Asset] transient status, retrying download:", {
label,
location,
status: response.status,
attempt,
attempts,
});
await sleep(RETRY_DELAY_MS * attempt);
continue;
}
throw statusError;
}
return Buffer.from(await response.arrayBuffer());
} catch (error) {
if (attempt < attempts && isRetryableFetchError(error)) {
console.warn("[PSD Remote Asset] transient fetch error, retrying download:", {
label,
location,
attempt,
attempts,
error,
});
await sleep(RETRY_DELAY_MS * attempt);
continue;
}
console.error("[PSD Remote Asset] failed to download asset:", {
label,
location,
attemptsTried: attempt,
error,
});
throw new Error(`${label}下载失败,请稍后重试或检查资源是否可访问(${location})`);
}
}
throw new Error(`${label}下载失败,请稍后重试或检查资源是否可访问(${location})`);
}