fix: parse text edit image results robustly

This commit is contained in:
Your Name 2026-05-28 20:43:07 +08:00
parent a15a26ba99
commit edcbddb0fa

View File

@ -15,10 +15,13 @@ import { createRunway } from "@/lib/ai/providers/runway";
import { createKrea } from "@/lib/ai/providers/krea";
import { createVolcanoVision } from "@/lib/ai/providers/volcano-vision";
import { isImageGenerationModelDescriptor } from "@/lib/ai/model-classification";
import { createLogger } from "@/lib/observability/logger";
export const runtime = "nodejs";
export const maxDuration = 120;
const logger = createLogger("canvas-text-edit");
const textItemSchema = z.object({
id: z.string().min(1),
originalText: z.string().min(1),
@ -115,20 +118,118 @@ function createTextEditProvider(
}
}
function parseImageFromMarkdown(markdown: string): { originalUrl: string; thumbnailUrl: string } {
function normalizeGeneratedImageUrl(value: unknown) {
const raw = String(value || "").trim();
if (!raw) return "";
return raw
.replace(/^["'([{<]+/, "")
.replace(/[)"'\]},;。]+$/, "")
.trim();
}
function isHttpImageUrl(value: unknown) {
const raw = normalizeGeneratedImageUrl(value);
if (!raw || !/^https?:\/\//i.test(raw)) {
return false;
}
try {
const parsed = new URL(raw);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}
function collectImageUrlsFromUnknown(value: unknown, output: Set<string>) {
if (value === null || value === undefined) return;
if (typeof value === "string") {
const raw = normalizeGeneratedImageUrl(value);
if (isHttpImageUrl(raw)) {
output.add(raw);
return;
}
const matches = raw.match(/https?:\/\/[^\s"'<>),\]}]+/gi) || [];
for (const match of matches) {
const normalized = normalizeGeneratedImageUrl(match);
if (isHttpImageUrl(normalized)) {
output.add(normalized);
}
}
return;
}
if (Array.isArray(value)) {
value.forEach((item) => collectImageUrlsFromUnknown(item, output));
return;
}
if (typeof value === "object") {
Object.entries(value as Record<string, unknown>).forEach(([key, entryValue]) => {
if (
/(url|uri|image|thumbnail|original|output|result|file)/i.test(key) ||
typeof entryValue === "object"
) {
collectImageUrlsFromUnknown(entryValue, output);
}
});
}
}
function pickGeneratedImageUrl(urls: Iterable<string>) {
const candidates = Array.from(new Set(Array.from(urls).map(normalizeGeneratedImageUrl)))
.filter(isHttpImageUrl);
const imageLike = candidates.find((url) =>
/\.(avif|gif|jpe?g|png|webp)(?:[?#]|$)/i.test(url)
);
return imageLike || candidates[0] || "";
}
function parseImageFromGenerationText(text: string): { originalUrl: string; thumbnailUrl: string } {
const markdown = String(text || "").trim();
const strictMatch = markdown.match(/!\[thumb\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/);
if (strictMatch) {
const originalUrl = normalizeGeneratedImageUrl(strictMatch[1]);
const thumbnailUrl = normalizeGeneratedImageUrl(strictMatch[2]);
if (isHttpImageUrl(originalUrl)) {
return {
originalUrl,
thumbnailUrl: isHttpImageUrl(thumbnailUrl) ? thumbnailUrl : originalUrl,
};
}
}
const markdownUrls = new Set<string>();
const markdownImagePattern = /!\[[^\]]*\]\(([^)]+)\)/g;
let markdownMatch: RegExpExecArray | null;
while ((markdownMatch = markdownImagePattern.exec(markdown)) !== null) {
const url = normalizeGeneratedImageUrl(markdownMatch[1]);
if (isHttpImageUrl(url)) {
markdownUrls.add(url);
}
}
const markdownUrl = pickGeneratedImageUrl(markdownUrls);
if (markdownUrl) {
return {
originalUrl: strictMatch[1],
thumbnailUrl: strictMatch[2],
originalUrl: markdownUrl,
thumbnailUrl: markdownUrl,
};
}
const simpleMatch = markdown.match(/!\[[^\]]*\]\(([^)]+)\)/);
if (simpleMatch) {
const collectedUrls = new Set<string>();
try {
collectImageUrlsFromUnknown(JSON.parse(markdown), collectedUrls);
} catch {
collectImageUrlsFromUnknown(markdown, collectedUrls);
}
const url = pickGeneratedImageUrl(collectedUrls);
if (url) {
return {
originalUrl: simpleMatch[1],
thumbnailUrl: simpleMatch[1],
originalUrl: url,
thumbnailUrl: url,
};
}
@ -238,7 +339,7 @@ export async function POST(request: Request) {
);
}
console.info("[Canvas Text Edit] selected model:", {
logger.info("selected_model", {
providerType: selected.provider.type,
providerName: selected.provider.name,
modelId: selected.model.modelId,
@ -270,7 +371,7 @@ export async function POST(request: Request) {
throw new Error("模型未返回结果");
}
const { originalUrl, thumbnailUrl } = parseImageFromMarkdown(result.text);
const { originalUrl, thumbnailUrl } = parseImageFromGenerationText(result.text);
return NextResponse.json({
success: true,
@ -281,7 +382,7 @@ export async function POST(request: Request) {
},
});
} catch (error) {
console.error("[Canvas Text Edit] Error:", error);
logger.error("request_failed", error);
return NextResponse.json(
{
success: false,