fix(canvas): restore reliable text editing

This commit is contained in:
134 2026-08-25 14:44:50 +08:00
parent a0999bfcde
commit 3a88fb0df0
2 changed files with 23 additions and 2 deletions

View File

@ -29,8 +29,9 @@ export const runtime = "nodejs";
export const maxDuration = 1200;
const logger = createLogger("canvas-text-edit");
const TEXT_EDIT_SOURCE_ARCHIVE_TIMEOUT_MS = 20_000;
const TEXT_EDIT_SOURCE_ARCHIVE_TIMEOUT_MS = 120_000;
const TEXT_EDIT_MODEL_FALLBACK_LIMIT = 3;
const TEXT_EDIT_GENERATION_TIMEOUT_MS = 180_000;
const textItemSchema = z.object({
id: z.string().min(1),
@ -68,6 +69,13 @@ const SUPPORTED_TEXT_EDIT_PROVIDER_TYPES = new Set([
type TextEditModelCandidate = Awaited<ReturnType<typeof getEnabledAiModelsWithProvider>>[number];
function isExplicitTextEditModel(candidate: TextEditModelCandidate) {
const capabilities = candidate.model.capabilities as Record<string, unknown> | null;
const kind = String(capabilities?.kind || "").toLowerCase();
const modelId = String(candidate.model.modelId || "").toLowerCase();
return kind === "image-edit" || /(?:^|[/_-])edit(?:$|[/_-])/.test(modelId);
}
function getTextEditModelCandidates(
models: Awaited<ReturnType<typeof getEnabledAiModelsWithProvider>>
) {
@ -87,11 +95,13 @@ function getTextEditModelCandidates(
.sort((a, b) => {
const aScore =
(a.model.isDefaultTextEditModel ? 100 : 0) +
(isExplicitTextEditModel(a) ? 30 : 0) +
(a.model.isDefault ? 10 : 0) +
(a.model.modelId?.toLowerCase().includes("seedream") ? 5 : 0) +
(a.model.displayName?.includes("火山") ? 3 : 0);
const bScore =
(b.model.isDefaultTextEditModel ? 100 : 0) +
(isExplicitTextEditModel(b) ? 30 : 0) +
(b.model.isDefault ? 10 : 0) +
(b.model.modelId?.toLowerCase().includes("seedream") ? 5 : 0) +
(b.model.displayName?.includes("火山") ? 3 : 0);
@ -220,6 +230,13 @@ async function resolveTextEditSourceImageUrl(imageUrl: string) {
} catch (error) {
logger.warn("source_image_archive_failed", {
error: error instanceof Error ? error.message : String(error),
sourceHost: (() => {
try {
return new URL(imageUrl).host;
} catch {
return "invalid-url";
}
})(),
});
return imageUrl;
}
@ -495,6 +512,7 @@ async function generateTextEditImageWithFailover(params: {
],
imageParams: {
imageCount: 1,
timeoutMs: TEXT_EDIT_GENERATION_TIMEOUT_MS,
},
};
if (selected.provider.type === "atlascloud" || selected.provider.type === "vip98k") {

View File

@ -56,6 +56,7 @@ const DEFAULT_SAFETY_TOLERANCE = "4";
const DEFAULT_TIMEOUT_MS = 600000;
const MIN_TIMEOUT_MS = 30_000;
const MAX_TIMEOUT_MS = 600_000;
const FAL_INPUT_IMAGE_DOWNLOAD_TIMEOUT_MS = 120_000;
const FAL_GENERIC_MAX_EDGE = 4096;
const FAL_GENERIC_MAX_PIXELS = 4096 * 4096;
const GPT_IMAGE_2_MAX_EDGE = 3840;
@ -679,7 +680,9 @@ async function uploadImageUrlToFalStorage(params: {
const { fal, url } = params;
if (/fal\.(?:media|run)\//i.test(url) || /fal-ai/i.test(url)) return url;
const response = await fetch(url, { signal: AbortSignal.timeout(60_000) });
const response = await fetch(url, {
signal: AbortSignal.timeout(FAL_INPUT_IMAGE_DOWNLOAD_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`FAL input image download failed before storage upload: ${response.status}`);
}