fix(image-quality): always reuse original image URLs in follow-up edits
This commit is contained in:
parent
ec00b35658
commit
ef8326fd33
@ -50,6 +50,7 @@ import { db } from "@/lib/db";
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
import { generatedImage } from "@/lib/db/schema";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||
import { uploadFile } from "@/lib/storage/service";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import type { AppUsage } from "@/lib/usage";
|
||||
@ -498,36 +499,7 @@ function parseImageAltMetadata(altText: string, thumbnailUrl: string): {
|
||||
}
|
||||
|
||||
function extractOriginalImageUrlsFromText(text: string): string[] {
|
||||
const imageRegex = /!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g;
|
||||
const imageUrls: string[] = [];
|
||||
const added = new Set<string>();
|
||||
let hasMarkdownImages = false;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = imageRegex.exec(text)) !== null) {
|
||||
hasMarkdownImages = true;
|
||||
const altText = match[1] || "";
|
||||
const thumbnailUrl = match[2];
|
||||
const { originalUrl } = parseImageAltMetadata(altText, thumbnailUrl);
|
||||
const finalUrl = originalUrl || thumbnailUrl;
|
||||
if (finalUrl && /^https?:\/\//i.test(finalUrl) && !added.has(finalUrl)) {
|
||||
added.add(finalUrl);
|
||||
imageUrls.push(finalUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMarkdownImages) {
|
||||
const urlRegex = /https?:\/\/[^\s"'<>]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg)(?:\?[^\s"'<>]*)?/gi;
|
||||
const matchedUrls = text.match(urlRegex) || [];
|
||||
for (const url of matchedUrls) {
|
||||
if (!added.has(url)) {
|
||||
added.add(url);
|
||||
imageUrls.push(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imageUrls;
|
||||
return extractImageUrlsFromText(text);
|
||||
}
|
||||
|
||||
function mimeTypeToExtension(mimeType: string) {
|
||||
|
||||
@ -21,6 +21,7 @@ import { toast } from "sonner";
|
||||
import { useCanvasStore } from "@/store/canvas";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||
import { ImageGenerationLoading } from "@/components/image-generation-loading";
|
||||
@ -101,32 +102,7 @@ function getImageDimensions(url: string): Promise<{
|
||||
|
||||
// 从消息中提取图片 URL(只提取原始图片,避免重复)
|
||||
function extractImageUrls(text: string): string[] {
|
||||
const urls: string[] = [];
|
||||
|
||||
// 匹配 markdown 图片格式: 
|
||||
// 只提取原始图片 URL(第一个 ||| 和第二个 ||| 之间的 URL)
|
||||
const markdownRegex = /!\[(?:thumb)?\|\|\|([^|]+)\|\|\|[^\]]*\]\(([^)]+)\)/g;
|
||||
let match;
|
||||
let hasMarkdownImages = false;
|
||||
|
||||
while ((match = markdownRegex.exec(text)) !== null) {
|
||||
if (match[1]) {
|
||||
urls.push(match[1]); // 原始图片 URL
|
||||
hasMarkdownImages = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在没有 markdown 格式图片时,才匹配普通图片 URL
|
||||
// 这样可以避免重复匹配缩略图 URL
|
||||
if (!hasMarkdownImages) {
|
||||
const urlRegex = /https?:\/\/[^\s"'<>]+\.(?:jpg|jpeg|png|gif|webp)/gi;
|
||||
const urlMatches = text.match(urlRegex);
|
||||
if (urlMatches) {
|
||||
urls.push(...urlMatches.filter(url => !urls.includes(url)));
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
return extractImageUrlsFromText(text);
|
||||
}
|
||||
|
||||
function extractUserTextPrompt(message: ChatMessage): string {
|
||||
|
||||
@ -5,6 +5,7 @@ import { useSWRConfig } from "swr";
|
||||
import { useCopyToClipboard } from "usehooks-ts";
|
||||
import type { Vote } from "@/lib/db/schema";
|
||||
import type { ChatMessage } from "@/lib/types";
|
||||
import { extractImageUrlsFromText } from "@/lib/image-url";
|
||||
import { Action, Actions } from "./elements/actions";
|
||||
import { CopyIcon, PencilEditIcon, ThumbDownIcon, ThumbUpIcon } from "./icons";
|
||||
|
||||
@ -49,17 +50,8 @@ export function PureMessageActions({
|
||||
// 检查消息中是否包含图片
|
||||
const extractImageUrl = () => {
|
||||
if (!textFromParts) return null;
|
||||
|
||||
// 匹配 Markdown 图片格式: 或 
|
||||
const imageRegex = /!\[(?:thumb\|([^\]]+))?\]\(([^)]+)\)/;
|
||||
const match = textFromParts.match(imageRegex);
|
||||
|
||||
if (match) {
|
||||
// 如果有原图 URL,使用原图;否则使用缩略图
|
||||
return match[1] || match[2];
|
||||
}
|
||||
|
||||
return null;
|
||||
const imageUrls = extractImageUrlsFromText(textFromParts);
|
||||
return imageUrls[0] || null;
|
||||
};
|
||||
|
||||
const imageUrl = extractImageUrl();
|
||||
|
||||
125
lib/image-url.ts
Normal file
125
lib/image-url.ts
Normal file
@ -0,0 +1,125 @@
|
||||
const MARKDOWN_IMAGE_REGEX = /!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/gim;
|
||||
const PLAIN_IMAGE_URL_REGEX =
|
||||
/https?:\/\/[^\s"'<>]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg)(?:\?[^\s"'<>]*)?/gi;
|
||||
|
||||
function isHttpUrl(value: string): boolean {
|
||||
return /^https?:\/\//i.test(value);
|
||||
}
|
||||
|
||||
export function isLikelyThumbnailUrl(url: string): boolean {
|
||||
const lowerUrl = url.toLowerCase();
|
||||
if (
|
||||
lowerUrl.includes("/thumb/") ||
|
||||
lowerUrl.includes("thumbnail") ||
|
||||
lowerUrl.includes("gemini-thumb-") ||
|
||||
lowerUrl.includes("openai-thumb-") ||
|
||||
lowerUrl.includes("volcano-thumb-") ||
|
||||
lowerUrl.includes("alibaba-image-thumb-") ||
|
||||
lowerUrl.includes("ruxa-image-thumb-")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /(?:^|[-_/])thumb(?:[-_.\\/]|$)/i.test(url);
|
||||
}
|
||||
|
||||
export function inferOriginalImageUrl(url: string): string {
|
||||
if (!isHttpUrl(url)) return url;
|
||||
|
||||
if (url.includes("gemini-thumb-")) {
|
||||
return url.replace("gemini-thumb-", "gemini-orig-");
|
||||
}
|
||||
if (url.includes("openai-thumb-")) {
|
||||
return url.replace("openai-thumb-", "openai-orig-");
|
||||
}
|
||||
if (url.includes("volcano-thumb-")) {
|
||||
return url.replace("volcano-thumb-", "volcano-orig-");
|
||||
}
|
||||
if (url.includes("alibaba-image-thumb-")) {
|
||||
return url.replace("alibaba-image-thumb-", "alibaba-image-orig-");
|
||||
}
|
||||
if (url.includes("ruxa-image-thumb-")) {
|
||||
return url.replace("ruxa-image-thumb-", "ruxa-image-orig-");
|
||||
}
|
||||
|
||||
if (url.includes("/thumb/")) {
|
||||
return url.replace("/thumb/", "/");
|
||||
}
|
||||
|
||||
if (url.includes("-thumb.")) {
|
||||
return url.replace("-thumb.", ".");
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function extractOriginalImageUrlFromAlt(altText: string, fallbackUrl: string): string {
|
||||
let originalUrl = fallbackUrl;
|
||||
|
||||
if (altText.includes("thumb|||")) {
|
||||
const content = altText.substring(altText.indexOf("thumb|||") + 8);
|
||||
const parts = content.split("|||");
|
||||
if (parts[0] && isHttpUrl(parts[0])) {
|
||||
originalUrl = parts[0];
|
||||
}
|
||||
} else if (altText.includes("thumb$$")) {
|
||||
const content = altText.substring(altText.indexOf("thumb$$") + 7);
|
||||
const parts = content.split("$$");
|
||||
if (parts[0] && isHttpUrl(parts[0])) {
|
||||
originalUrl = parts[0];
|
||||
}
|
||||
} else if (altText.includes("thumb|")) {
|
||||
const content = altText.substring(altText.indexOf("thumb|") + 6);
|
||||
const parts = content.split("|");
|
||||
if (parts[0] && isHttpUrl(parts[0])) {
|
||||
originalUrl = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (originalUrl === fallbackUrl || !isHttpUrl(originalUrl)) {
|
||||
originalUrl = inferOriginalImageUrl(fallbackUrl);
|
||||
}
|
||||
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
export function extractImageUrlsFromText(text: string): string[] {
|
||||
if (!text) return [];
|
||||
|
||||
const urls: string[] = [];
|
||||
const added = new Set<string>();
|
||||
|
||||
const addUrl = (url: string) => {
|
||||
if (!url || !isHttpUrl(url) || added.has(url)) return;
|
||||
added.add(url);
|
||||
urls.push(url);
|
||||
};
|
||||
|
||||
let hasMarkdownImages = false;
|
||||
let markdownMatch: RegExpExecArray | null;
|
||||
MARKDOWN_IMAGE_REGEX.lastIndex = 0;
|
||||
|
||||
while ((markdownMatch = MARKDOWN_IMAGE_REGEX.exec(text)) !== null) {
|
||||
hasMarkdownImages = true;
|
||||
const altText = markdownMatch[1] || "";
|
||||
const thumbnailUrl = markdownMatch[2] || "";
|
||||
const originalUrl = extractOriginalImageUrlFromAlt(altText, thumbnailUrl);
|
||||
addUrl(originalUrl || thumbnailUrl);
|
||||
}
|
||||
|
||||
if (hasMarkdownImages) return urls;
|
||||
|
||||
const rawUrls = text.match(PLAIN_IMAGE_URL_REGEX) || [];
|
||||
if (!rawUrls.length) return urls;
|
||||
|
||||
const nonThumbnailUrls = rawUrls.filter((url) => !isLikelyThumbnailUrl(url));
|
||||
const candidates = nonThumbnailUrls.length
|
||||
? nonThumbnailUrls
|
||||
: rawUrls.map((url) => inferOriginalImageUrl(url));
|
||||
|
||||
for (const url of candidates) {
|
||||
addUrl(url);
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user