477 lines
15 KiB
TypeScript
477 lines
15 KiB
TypeScript
import "server-only";
|
|
|
|
import { eq, like, or, sql } from "drizzle-orm";
|
|
import { db } from "@/lib/db";
|
|
import {
|
|
assetMemorySnapshot,
|
|
canvasDetailArchive,
|
|
canvasNode,
|
|
canvasProject,
|
|
conversationWorkingState,
|
|
externalImageApiRequestLog,
|
|
generatedImage,
|
|
mediaAsset,
|
|
message,
|
|
messageDeprecated,
|
|
model3dAsset,
|
|
} from "@/lib/db/schema";
|
|
import { inferOriginalImageUrl } from "@/lib/image-url";
|
|
|
|
function normalizeMediaUrl(value: string): string {
|
|
const raw = String(value || "").trim();
|
|
if (!raw) return "";
|
|
const canonical = inferOriginalImageUrl(raw);
|
|
|
|
try {
|
|
const parsed = new URL(canonical);
|
|
return `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "").toLowerCase();
|
|
} catch {
|
|
return canonical
|
|
.split("#")[0]
|
|
.split("?")[0]
|
|
.replace(/\/+$/, "")
|
|
.toLowerCase();
|
|
}
|
|
}
|
|
|
|
export function replaceDeepUrls(
|
|
value: unknown,
|
|
sourceUrl: string,
|
|
targetUrl: string
|
|
): { value: unknown; changed: boolean } {
|
|
if (typeof value === "string") {
|
|
if (!value.includes(sourceUrl)) {
|
|
return { value, changed: false };
|
|
}
|
|
return { value: value.split(sourceUrl).join(targetUrl), changed: true };
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
let changed = false;
|
|
const next = value.map((item) => {
|
|
const result = replaceDeepUrls(item, sourceUrl, targetUrl);
|
|
if (result.changed) changed = true;
|
|
return result.value;
|
|
});
|
|
return { value: changed ? next : value, changed };
|
|
}
|
|
|
|
if (value && typeof value === "object") {
|
|
let changed = false;
|
|
const next: Record<string, unknown> = {};
|
|
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
const result = replaceDeepUrls(item, sourceUrl, targetUrl);
|
|
next[key] = result.value;
|
|
if (result.changed) changed = true;
|
|
}
|
|
return { value: changed ? next : value, changed };
|
|
}
|
|
|
|
return { value, changed: false };
|
|
}
|
|
|
|
export async function replaceImageUrlInDatabase(
|
|
sourceUrl: string,
|
|
targetUrl: string
|
|
) {
|
|
if (!sourceUrl || !targetUrl || sourceUrl === targetUrl) {
|
|
return { changed: 0 };
|
|
}
|
|
|
|
let changed = 0;
|
|
const sourceNormalizedUrl = normalizeMediaUrl(sourceUrl);
|
|
const targetNormalizedUrl = normalizeMediaUrl(targetUrl);
|
|
|
|
const generatedThumb = await db
|
|
.update(generatedImage)
|
|
.set({ thumbnailUrl: targetUrl })
|
|
.where(eq(generatedImage.thumbnailUrl, sourceUrl))
|
|
.returning({ id: generatedImage.id });
|
|
changed += generatedThumb.length;
|
|
|
|
const generatedOrig = await db
|
|
.update(generatedImage)
|
|
.set({ originalUrl: targetUrl })
|
|
.where(eq(generatedImage.originalUrl, sourceUrl))
|
|
.returning({ id: generatedImage.id });
|
|
changed += generatedOrig.length;
|
|
|
|
const assets = await db
|
|
.select()
|
|
.from(mediaAsset)
|
|
.where(
|
|
or(
|
|
eq(mediaAsset.originalUrl, sourceUrl),
|
|
eq(mediaAsset.previewUrl, sourceUrl),
|
|
eq(mediaAsset.thumbnailUrl, sourceUrl),
|
|
eq(mediaAsset.normalizedUrl, sourceNormalizedUrl),
|
|
sql`${mediaAsset.metadata}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${mediaAsset.summary}::text like ${`%${sourceUrl}%`}`
|
|
)
|
|
)
|
|
.limit(500);
|
|
|
|
for (const item of assets) {
|
|
const original = replaceDeepUrls(item.originalUrl, sourceUrl, targetUrl);
|
|
const preview = replaceDeepUrls(item.previewUrl, sourceUrl, targetUrl);
|
|
const thumbnail = replaceDeepUrls(item.thumbnailUrl, sourceUrl, targetUrl);
|
|
const metadata = replaceDeepUrls(item.metadata, sourceUrl, targetUrl);
|
|
const summary = replaceDeepUrls(item.summary, sourceUrl, targetUrl);
|
|
const shouldNormalize =
|
|
item.normalizedUrl === sourceNormalizedUrl || original.changed;
|
|
const nextOriginal = (original.value as string) || item.originalUrl;
|
|
|
|
if (
|
|
!original.changed &&
|
|
!preview.changed &&
|
|
!thumbnail.changed &&
|
|
!metadata.changed &&
|
|
!summary.changed &&
|
|
!shouldNormalize
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await db
|
|
.update(mediaAsset)
|
|
.set({
|
|
originalUrl: nextOriginal,
|
|
normalizedUrl: shouldNormalize ? targetNormalizedUrl : item.normalizedUrl,
|
|
previewUrl: (preview.value as string | null | undefined) ?? item.previewUrl,
|
|
thumbnailUrl:
|
|
(thumbnail.value as string | null | undefined) ?? item.thumbnailUrl,
|
|
metadata: (metadata.value ?? item.metadata) as any,
|
|
summary: (summary.value ?? item.summary) as any,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(mediaAsset.id, item.id));
|
|
} catch (error) {
|
|
await db
|
|
.update(mediaAsset)
|
|
.set({
|
|
originalUrl: nextOriginal,
|
|
previewUrl: (preview.value as string | null | undefined) ?? item.previewUrl,
|
|
thumbnailUrl:
|
|
(thumbnail.value as string | null | undefined) ?? item.thumbnailUrl,
|
|
metadata: (metadata.value ?? item.metadata) as any,
|
|
summary: (summary.value ?? item.summary) as any,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(mediaAsset.id, item.id));
|
|
}
|
|
changed += 1;
|
|
}
|
|
|
|
const snapshots = await db
|
|
.select()
|
|
.from(assetMemorySnapshot)
|
|
.where(
|
|
or(
|
|
eq(assetMemorySnapshot.normalizedUrl, sourceNormalizedUrl),
|
|
sql`${assetMemorySnapshot.metadata}::text like ${`%${sourceUrl}%`}`,
|
|
like(assetMemorySnapshot.summaryText, `%${sourceUrl}%`)
|
|
)
|
|
)
|
|
.limit(500);
|
|
|
|
for (const item of snapshots) {
|
|
const metadata = replaceDeepUrls(item.metadata, sourceUrl, targetUrl);
|
|
const summary = replaceDeepUrls(item.summaryText, sourceUrl, targetUrl);
|
|
const shouldNormalize = item.normalizedUrl === sourceNormalizedUrl;
|
|
if (!metadata.changed && !summary.changed && !shouldNormalize) continue;
|
|
|
|
await db
|
|
.update(assetMemorySnapshot)
|
|
.set({
|
|
normalizedUrl: shouldNormalize ? targetNormalizedUrl : item.normalizedUrl,
|
|
metadata: (metadata.value ?? item.metadata) as any,
|
|
summaryText: (summary.value as string | null | undefined) ?? item.summaryText,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(assetMemorySnapshot.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
const messages = await db
|
|
.select({
|
|
id: message.id,
|
|
parts: message.parts,
|
|
attachments: message.attachments,
|
|
})
|
|
.from(message)
|
|
.where(
|
|
or(
|
|
sql`${message.parts}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${message.attachments}::text like ${`%${sourceUrl}%`}`
|
|
)
|
|
)
|
|
.limit(500);
|
|
|
|
for (const item of messages) {
|
|
const partsResult = replaceDeepUrls(item.parts, sourceUrl, targetUrl);
|
|
const attachmentsResult = replaceDeepUrls(item.attachments, sourceUrl, targetUrl);
|
|
if (!partsResult.changed && !attachmentsResult.changed) continue;
|
|
|
|
await db
|
|
.update(message)
|
|
.set({
|
|
parts: (partsResult.value ?? item.parts) as any,
|
|
attachments: (attachmentsResult.value ?? item.attachments) as any,
|
|
})
|
|
.where(eq(message.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
const legacyMessages = await db
|
|
.select({
|
|
id: messageDeprecated.id,
|
|
content: messageDeprecated.content,
|
|
})
|
|
.from(messageDeprecated)
|
|
.where(sql`${messageDeprecated.content}::text like ${`%${sourceUrl}%`}`)
|
|
.limit(500);
|
|
|
|
for (const item of legacyMessages) {
|
|
const replaced = replaceDeepUrls(item.content, sourceUrl, targetUrl);
|
|
if (!replaced.changed) continue;
|
|
|
|
await db
|
|
.update(messageDeprecated)
|
|
.set({ content: replaced.value as any })
|
|
.where(eq(messageDeprecated.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
const projects = await db
|
|
.select({
|
|
id: canvasProject.id,
|
|
canvasData: canvasProject.canvasData,
|
|
})
|
|
.from(canvasProject)
|
|
.where(sql`${canvasProject.canvasData}::text like ${`%${sourceUrl}%`}`)
|
|
.limit(500);
|
|
|
|
for (const item of projects) {
|
|
const replaced = replaceDeepUrls(item.canvasData, sourceUrl, targetUrl);
|
|
if (!replaced.changed) continue;
|
|
|
|
await db
|
|
.update(canvasProject)
|
|
.set({
|
|
canvasData: replaced.value as any,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(canvasProject.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
const nodes = await db
|
|
.select({
|
|
id: canvasNode.id,
|
|
data: canvasNode.data,
|
|
})
|
|
.from(canvasNode)
|
|
.where(sql`${canvasNode.data}::text like ${`%${sourceUrl}%`}`)
|
|
.limit(500);
|
|
|
|
for (const item of nodes) {
|
|
const replaced = replaceDeepUrls(item.data, sourceUrl, targetUrl);
|
|
if (!replaced.changed) continue;
|
|
|
|
await db
|
|
.update(canvasNode)
|
|
.set({
|
|
data: replaced.value as any,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(canvasNode.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
const workingStates = await db
|
|
.select()
|
|
.from(conversationWorkingState)
|
|
.where(
|
|
or(
|
|
sql`${conversationWorkingState.currentFocusImageUrls}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${conversationWorkingState.selectedImageUrls}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${conversationWorkingState.lastUploadedImageUrls}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${conversationWorkingState.lastGeneratedImageUrls}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${conversationWorkingState.lastReferencedImageUrls}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${conversationWorkingState.lastResolvedTargetImageUrls}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${conversationWorkingState.lastCanvasSnapshot}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${conversationWorkingState.lastTurnSummary}::text like ${`%${sourceUrl}%`}`
|
|
)
|
|
)
|
|
.limit(500);
|
|
|
|
for (const item of workingStates) {
|
|
const currentFocus = replaceDeepUrls(
|
|
item.currentFocusImageUrls,
|
|
sourceUrl,
|
|
targetUrl
|
|
);
|
|
const selected = replaceDeepUrls(item.selectedImageUrls, sourceUrl, targetUrl);
|
|
const uploaded = replaceDeepUrls(item.lastUploadedImageUrls, sourceUrl, targetUrl);
|
|
const generated = replaceDeepUrls(item.lastGeneratedImageUrls, sourceUrl, targetUrl);
|
|
const referenced = replaceDeepUrls(item.lastReferencedImageUrls, sourceUrl, targetUrl);
|
|
const resolved = replaceDeepUrls(
|
|
item.lastResolvedTargetImageUrls,
|
|
sourceUrl,
|
|
targetUrl
|
|
);
|
|
const canvasSnapshot = replaceDeepUrls(
|
|
item.lastCanvasSnapshot,
|
|
sourceUrl,
|
|
targetUrl
|
|
);
|
|
const turnSummary = replaceDeepUrls(item.lastTurnSummary, sourceUrl, targetUrl);
|
|
|
|
if (
|
|
!currentFocus.changed &&
|
|
!selected.changed &&
|
|
!uploaded.changed &&
|
|
!generated.changed &&
|
|
!referenced.changed &&
|
|
!resolved.changed &&
|
|
!canvasSnapshot.changed &&
|
|
!turnSummary.changed
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
await db
|
|
.update(conversationWorkingState)
|
|
.set({
|
|
currentFocusImageUrls: currentFocus.value as any,
|
|
selectedImageUrls: selected.value as any,
|
|
lastUploadedImageUrls: uploaded.value as any,
|
|
lastGeneratedImageUrls: generated.value as any,
|
|
lastReferencedImageUrls: referenced.value as any,
|
|
lastResolvedTargetImageUrls: resolved.value as any,
|
|
lastCanvasSnapshot: (canvasSnapshot.value ?? item.lastCanvasSnapshot) as any,
|
|
lastTurnSummary: (turnSummary.value ?? item.lastTurnSummary) as any,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(conversationWorkingState.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
const detailArchives = await db
|
|
.select({
|
|
id: canvasDetailArchive.id,
|
|
coverUrl: canvasDetailArchive.coverUrl,
|
|
brief: canvasDetailArchive.brief,
|
|
slots: canvasDetailArchive.slots,
|
|
uiSurfaceSnapshot: canvasDetailArchive.uiSurfaceSnapshot,
|
|
uiSurfaceLatestReview: canvasDetailArchive.uiSurfaceLatestReview,
|
|
})
|
|
.from(canvasDetailArchive)
|
|
.where(
|
|
or(
|
|
like(canvasDetailArchive.coverUrl, `%${sourceUrl}%`),
|
|
sql`${canvasDetailArchive.brief}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${canvasDetailArchive.slots}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${canvasDetailArchive.uiSurfaceSnapshot}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${canvasDetailArchive.uiSurfaceLatestReview}::text like ${`%${sourceUrl}%`}`
|
|
)
|
|
)
|
|
.limit(500);
|
|
|
|
for (const item of detailArchives) {
|
|
const cover = replaceDeepUrls(item.coverUrl, sourceUrl, targetUrl);
|
|
const brief = replaceDeepUrls(item.brief, sourceUrl, targetUrl);
|
|
const slots = replaceDeepUrls(item.slots, sourceUrl, targetUrl);
|
|
const snapshot = replaceDeepUrls(item.uiSurfaceSnapshot, sourceUrl, targetUrl);
|
|
const review = replaceDeepUrls(item.uiSurfaceLatestReview, sourceUrl, targetUrl);
|
|
|
|
if (
|
|
!cover.changed &&
|
|
!brief.changed &&
|
|
!slots.changed &&
|
|
!snapshot.changed &&
|
|
!review.changed
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
await db
|
|
.update(canvasDetailArchive)
|
|
.set({
|
|
coverUrl: (cover.value as string | null | undefined) ?? item.coverUrl,
|
|
brief: (brief.value ?? item.brief) as any,
|
|
slots: (slots.value ?? item.slots) as any,
|
|
uiSurfaceSnapshot: (snapshot.value ?? item.uiSurfaceSnapshot) as any,
|
|
uiSurfaceLatestReview: (review.value ?? item.uiSurfaceLatestReview) as any,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(canvasDetailArchive.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
const modelAssets = await db
|
|
.select()
|
|
.from(model3dAsset)
|
|
.where(
|
|
or(
|
|
like(model3dAsset.previewImageUrl, `%${sourceUrl}%`),
|
|
sql`${model3dAsset.assetPayload}::text like ${`%${sourceUrl}%`}`
|
|
)
|
|
)
|
|
.limit(500);
|
|
|
|
for (const item of modelAssets) {
|
|
const preview = replaceDeepUrls(item.previewImageUrl, sourceUrl, targetUrl);
|
|
const payload = replaceDeepUrls(item.assetPayload, sourceUrl, targetUrl);
|
|
if (!preview.changed && !payload.changed) continue;
|
|
|
|
await db
|
|
.update(model3dAsset)
|
|
.set({
|
|
previewImageUrl:
|
|
(preview.value as string | null | undefined) ?? item.previewImageUrl,
|
|
assetPayload: (payload.value ?? item.assetPayload) as any,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(model3dAsset.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
const apiLogs = await db
|
|
.select()
|
|
.from(externalImageApiRequestLog)
|
|
.where(
|
|
or(
|
|
sql`${externalImageApiRequestLog.responsePayload}::text like ${`%${sourceUrl}%`}`,
|
|
sql`${externalImageApiRequestLog.generatedImageUrls}::text like ${`%${sourceUrl}%`}`
|
|
)
|
|
)
|
|
.limit(500);
|
|
|
|
for (const item of apiLogs) {
|
|
const responsePayload = replaceDeepUrls(
|
|
item.responsePayload,
|
|
sourceUrl,
|
|
targetUrl
|
|
);
|
|
const generatedUrls = replaceDeepUrls(
|
|
item.generatedImageUrls,
|
|
sourceUrl,
|
|
targetUrl
|
|
);
|
|
if (!responsePayload.changed && !generatedUrls.changed) continue;
|
|
|
|
await db
|
|
.update(externalImageApiRequestLog)
|
|
.set({
|
|
responsePayload: (responsePayload.value ?? item.responsePayload) as any,
|
|
generatedImageUrls: (generatedUrls.value ?? item.generatedImageUrls) as any,
|
|
})
|
|
.where(eq(externalImageApiRequestLog.id, item.id));
|
|
changed += 1;
|
|
}
|
|
|
|
return { changed };
|
|
}
|