feat: sync latest canvas and media updates
This commit is contained in:
parent
5f849bf0b7
commit
12583cdd10
@ -50,8 +50,11 @@ import {
|
|||||||
checkUserImageQuota,
|
checkUserImageQuota,
|
||||||
consumeUserPoints,
|
consumeUserPoints,
|
||||||
createMessageTargetResolution,
|
createMessageTargetResolution,
|
||||||
|
createMediaAssetRelation,
|
||||||
getConversationWorkingStateByChatId,
|
getConversationWorkingStateByChatId,
|
||||||
|
getMediaAssetsByUserAndUrls,
|
||||||
upsertConversationWorkingState,
|
upsertConversationWorkingState,
|
||||||
|
upsertMediaAsset,
|
||||||
} from "@/lib/db/queries";
|
} from "@/lib/db/queries";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import type { DBMessage } from "@/lib/db/schema";
|
import type { DBMessage } from "@/lib/db/schema";
|
||||||
@ -1628,14 +1631,41 @@ async function saveGeneratedImages(
|
|||||||
chatId: string,
|
chatId: string,
|
||||||
messageId: string,
|
messageId: string,
|
||||||
prompt: string,
|
prompt: string,
|
||||||
modelId: string
|
modelId: string,
|
||||||
|
options?: {
|
||||||
|
sourceAssetIds?: string[];
|
||||||
|
sourceImageUrls?: string[];
|
||||||
|
relationType?: "generated_from" | "edited_from";
|
||||||
|
sourceType?: string;
|
||||||
|
projectId?: string | null;
|
||||||
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 匹配 Markdown 图片格式: 
|
// 匹配 Markdown 图片格式: 
|
||||||
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||||||
const matches = [...imageMarkdown.matchAll(imageRegex)];
|
const matches = [...imageMarkdown.matchAll(imageRegex)];
|
||||||
|
|
||||||
let savedImageCount = 0;
|
let savedImageCount = 0;
|
||||||
|
const savedAssetIds: string[] = [];
|
||||||
const savedOriginalUrls: string[] = [];
|
const savedOriginalUrls: string[] = [];
|
||||||
|
let relationSourceAssetIds = Array.isArray(options?.sourceAssetIds)
|
||||||
|
? options.sourceAssetIds.filter((assetId): assetId is string => Boolean(assetId))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
relationSourceAssetIds.length === 0 &&
|
||||||
|
Array.isArray(options?.sourceImageUrls) &&
|
||||||
|
options.sourceImageUrls.length > 0
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const sourceAssets = await getMediaAssetsByUserAndUrls({
|
||||||
|
userId,
|
||||||
|
urls: options.sourceImageUrls,
|
||||||
|
});
|
||||||
|
relationSourceAssetIds = sourceAssets.map((asset) => asset.id);
|
||||||
|
} catch (sourceLookupError) {
|
||||||
|
console.error("[Gallery] Failed to resolve source assets by URL:", sourceLookupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const altText = match[1] || "";
|
const altText = match[1] || "";
|
||||||
@ -1643,7 +1673,7 @@ async function saveGeneratedImages(
|
|||||||
const { originalUrl, imageSize } = parseImageAltMetadata(altText, thumbnailUrl);
|
const { originalUrl, imageSize } = parseImageAltMetadata(altText, thumbnailUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.insert(generatedImage).values({
|
const [savedImage] = await db.insert(generatedImage).values({
|
||||||
userId,
|
userId,
|
||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
@ -1652,11 +1682,54 @@ async function saveGeneratedImages(
|
|||||||
imageSize,
|
imageSize,
|
||||||
prompt,
|
prompt,
|
||||||
modelId,
|
modelId,
|
||||||
});
|
}).returning();
|
||||||
savedImageCount += 1;
|
savedImageCount += 1;
|
||||||
if (!savedOriginalUrls.includes(originalUrl)) {
|
if (!savedOriginalUrls.includes(originalUrl)) {
|
||||||
savedOriginalUrls.push(originalUrl);
|
savedOriginalUrls.push(originalUrl);
|
||||||
}
|
}
|
||||||
|
const media = await upsertMediaAsset({
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
projectId: options?.projectId ?? null,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: options?.sourceType || "generated",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl,
|
||||||
|
previewUrl: originalUrl,
|
||||||
|
thumbnailUrl,
|
||||||
|
displayName: prompt?.slice(0, 80) || "AI 生成图片",
|
||||||
|
prompt,
|
||||||
|
modelId,
|
||||||
|
sourceMessageId: messageId,
|
||||||
|
sourceTable: "GeneratedImage",
|
||||||
|
sourceRowId: savedImage?.id || null,
|
||||||
|
primaryParentAssetId: relationSourceAssetIds[0] || null,
|
||||||
|
metadata: {
|
||||||
|
imageSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (media?.id && !savedAssetIds.includes(media.id)) {
|
||||||
|
savedAssetIds.push(media.id);
|
||||||
|
}
|
||||||
|
if (media && relationSourceAssetIds.length > 0) {
|
||||||
|
for (const sourceAssetId of relationSourceAssetIds) {
|
||||||
|
if (!sourceAssetId) continue;
|
||||||
|
await createMediaAssetRelation({
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
projectId: options?.projectId ?? null,
|
||||||
|
fromAssetId: sourceAssetId,
|
||||||
|
toAssetId: media.id,
|
||||||
|
relationType: options?.relationType || "generated_from",
|
||||||
|
sourceMessageId: messageId,
|
||||||
|
metadata: {
|
||||||
|
modelId,
|
||||||
|
prompt,
|
||||||
|
sourceImageUrls: options?.sourceImageUrls || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
console.log(`[Gallery] Saved image: ${originalUrl.substring(0, 80)}...`);
|
console.log(`[Gallery] Saved image: ${originalUrl.substring(0, 80)}...`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Gallery] Failed to save image record:", error);
|
console.error("[Gallery] Failed to save image record:", error);
|
||||||
@ -1668,8 +1741,11 @@ async function saveGeneratedImages(
|
|||||||
await upsertConversationWorkingState({
|
await upsertConversationWorkingState({
|
||||||
userId,
|
userId,
|
||||||
chatId,
|
chatId,
|
||||||
|
currentFocusAssetIds: savedAssetIds,
|
||||||
currentFocusImageUrls: savedOriginalUrls,
|
currentFocusImageUrls: savedOriginalUrls,
|
||||||
|
lastGeneratedAssetIds: savedAssetIds,
|
||||||
lastGeneratedImageUrls: savedOriginalUrls,
|
lastGeneratedImageUrls: savedOriginalUrls,
|
||||||
|
lastResolvedTargetAssetIds: savedAssetIds,
|
||||||
lastResolvedTargetImageUrls: savedOriginalUrls,
|
lastResolvedTargetImageUrls: savedOriginalUrls,
|
||||||
lastMessageId: messageId,
|
lastMessageId: messageId,
|
||||||
});
|
});
|
||||||
@ -3411,7 +3487,14 @@ export async function POST(request: Request) {
|
|||||||
id,
|
id,
|
||||||
messageId,
|
messageId,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
finalModelId
|
finalModelId,
|
||||||
|
{
|
||||||
|
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||||
|
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||||
|
relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from",
|
||||||
|
sourceType: requestResolvedMediaInput?.hasImage ? "generated" : "generated",
|
||||||
|
projectId: contextPayload?.projectId || null,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (saveError) {
|
} catch (saveError) {
|
||||||
console.error("[Chat API] ❌ Failed to save Gemini message to database:", saveError);
|
console.error("[Chat API] ❌ Failed to save Gemini message to database:", saveError);
|
||||||
@ -3515,7 +3598,14 @@ export async function POST(request: Request) {
|
|||||||
id,
|
id,
|
||||||
messageId,
|
messageId,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
volcanoModel.model.id
|
volcanoModel.model.id,
|
||||||
|
{
|
||||||
|
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||||
|
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||||
|
relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from",
|
||||||
|
sourceType: "generated",
|
||||||
|
projectId: contextPayload?.projectId || null,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (saveError) {
|
} catch (saveError) {
|
||||||
console.error("[Chat API] Failed to save failover message:", saveError);
|
console.error("[Chat API] Failed to save failover message:", saveError);
|
||||||
@ -3869,7 +3959,14 @@ export async function POST(request: Request) {
|
|||||||
id,
|
id,
|
||||||
messageId,
|
messageId,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
finalModelId
|
finalModelId,
|
||||||
|
{
|
||||||
|
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||||
|
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||||
|
relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from",
|
||||||
|
sourceType: "generated",
|
||||||
|
projectId: contextPayload?.projectId || null,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (saveError) {
|
} catch (saveError) {
|
||||||
console.error("[Chat API] ❌ Failed to save Alibaba message:", saveError);
|
console.error("[Chat API] ❌ Failed to save Alibaba message:", saveError);
|
||||||
@ -4211,7 +4308,14 @@ export async function POST(request: Request) {
|
|||||||
id,
|
id,
|
||||||
messageId,
|
messageId,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
finalModelId
|
finalModelId,
|
||||||
|
{
|
||||||
|
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||||
|
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||||
|
relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from",
|
||||||
|
sourceType: "generated",
|
||||||
|
projectId: contextPayload?.projectId || null,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (saveError) {
|
} catch (saveError) {
|
||||||
console.error("[Chat API] ❌ Failed to save Volcano Vision message to database:", saveError);
|
console.error("[Chat API] ❌ Failed to save Volcano Vision message to database:", saveError);
|
||||||
@ -4834,7 +4938,14 @@ export async function POST(request: Request) {
|
|||||||
id,
|
id,
|
||||||
messageId,
|
messageId,
|
||||||
userPrompt,
|
userPrompt,
|
||||||
finalModelId
|
finalModelId,
|
||||||
|
{
|
||||||
|
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||||
|
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||||
|
relationType: requestResolvedMediaInput?.hasImage ? "edited_from" : "generated_from",
|
||||||
|
sourceType: "generated",
|
||||||
|
projectId: contextPayload?.projectId || null,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const filePartSchema = z.object({
|
|||||||
mediaType: z.enum(["image/jpeg", "image/png", "image/webp", "image/gif", "image/jpg", "image/bmp", "image/svg+xml"]),
|
mediaType: z.enum(["image/jpeg", "image/png", "image/webp", "image/gif", "image/jpg", "image/bmp", "image/svg+xml"]),
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
url: z.string().url(),
|
url: z.string().url(),
|
||||||
|
assetId: z.string().uuid().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const partSchema = z.union([textPartSchema, filePartSchema]);
|
const partSchema = z.union([textPartSchema, filePartSchema]);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
import { upsertMediaAsset } from "@/lib/db/queries";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -167,8 +168,34 @@ export async function POST(request: Request) {
|
|||||||
// 使用优化后的buffer上传
|
// 使用优化后的buffer上传
|
||||||
const data = await uploadFile(filename, optimizedArrayBuffer);
|
const data = await uploadFile(filename, optimizedArrayBuffer);
|
||||||
|
|
||||||
|
let assetId: string | null = null;
|
||||||
|
try {
|
||||||
|
const asset = await upsertMediaAsset({
|
||||||
|
userId: session.user.id,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: "upload",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl: data.url,
|
||||||
|
previewUrl: data.url,
|
||||||
|
thumbnailUrl: data.url,
|
||||||
|
mimeType: data.contentType || file.type || null,
|
||||||
|
displayName: filename,
|
||||||
|
sourceTable: "upload",
|
||||||
|
metadata: {
|
||||||
|
pathname: data.pathname,
|
||||||
|
size: optimizedBuffer.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assetId = asset?.id ?? null;
|
||||||
|
} catch (assetError) {
|
||||||
|
console.warn("[Upload API] Failed to register uploaded image as media asset:", assetError);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("[Upload API] Upload successful:", data.url);
|
console.log("[Upload API] Upload successful:", data.url);
|
||||||
return NextResponse.json(data);
|
return NextResponse.json({
|
||||||
|
...data,
|
||||||
|
assetId,
|
||||||
|
});
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
console.error("[Upload API] Upload failed:", _error);
|
console.error("[Upload API] Upload failed:", _error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getSystemSetting } from "@/lib/db/queries";
|
import {
|
||||||
|
createMediaAssetRelation,
|
||||||
|
getMediaAssetsByUserAndUrls,
|
||||||
|
getSystemSetting,
|
||||||
|
upsertMediaAsset,
|
||||||
|
} from "@/lib/db/queries";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
@ -214,14 +219,61 @@ export async function POST(request: NextRequest) {
|
|||||||
// 8. 保存到数据库
|
// 8. 保存到数据库
|
||||||
const processingTime = (Date.now() - startTime) / 1000;
|
const processingTime = (Date.now() - startTime) / 1000;
|
||||||
|
|
||||||
await db.insert(generatedImage).values({
|
const [savedImage] = await db.insert(generatedImage).values({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
thumbnailUrl: s3Result.thumbnail,
|
thumbnailUrl: s3Result.thumbnail,
|
||||||
originalUrl: s3Result.original,
|
originalUrl: s3Result.original,
|
||||||
prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`,
|
prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`,
|
||||||
modelId: "qwen-image-edit-2511-multiple-angles",
|
modelId: "qwen-image-edit-2511-multiple-angles",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
});
|
}).returning();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceAssets =
|
||||||
|
typeof image_url === "string" && image_url.trim()
|
||||||
|
? await getMediaAssetsByUserAndUrls({
|
||||||
|
userId: session.user.id,
|
||||||
|
urls: [image_url],
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const media = await upsertMediaAsset({
|
||||||
|
userId: session.user.id,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: "edited",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl: s3Result.original,
|
||||||
|
previewUrl: s3Result.original,
|
||||||
|
thumbnailUrl: s3Result.thumbnail,
|
||||||
|
displayName: `角度编辑 ${finalHorizontalAngle}/${finalVerticalAngle}`.slice(0, 80),
|
||||||
|
prompt: `角度编辑: H${finalHorizontalAngle}° V${finalVerticalAngle}° Z${finalZoom}`,
|
||||||
|
modelId: "qwen-image-edit-2511-multiple-angles",
|
||||||
|
sourceTable: "GeneratedImage",
|
||||||
|
sourceRowId: savedImage.id,
|
||||||
|
primaryParentAssetId: sourceAssets[0]?.id ?? null,
|
||||||
|
metadata: {
|
||||||
|
horizontalAngle: finalHorizontalAngle,
|
||||||
|
verticalAngle: finalVerticalAngle,
|
||||||
|
zoom: finalZoom,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (media) {
|
||||||
|
for (const sourceAsset of sourceAssets) {
|
||||||
|
await createMediaAssetRelation({
|
||||||
|
userId: session.user.id,
|
||||||
|
fromAssetId: sourceAsset.id,
|
||||||
|
toAssetId: media.id,
|
||||||
|
relationType: "edited_from",
|
||||||
|
metadata: {
|
||||||
|
horizontalAngle: finalHorizontalAngle,
|
||||||
|
verticalAngle: finalVerticalAngle,
|
||||||
|
zoom: finalZoom,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (assetError) {
|
||||||
|
console.error("[Angle-Edit] ⚠️ MediaAsset 注册失败:", assetError);
|
||||||
|
}
|
||||||
|
|
||||||
console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒");
|
console.log("[Angle-Edit] ✅ 处理完成,耗时:", processingTime.toFixed(2), "秒");
|
||||||
|
|
||||||
@ -270,4 +322,3 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { auth } from '@/app/(auth)/auth';
|
import { auth } from '@/app/(auth)/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { canvasProject } from '@/lib/db/schema';
|
import { canvasProject } from '@/lib/db/schema';
|
||||||
|
import { syncCanvasNodesForProject } from '@/lib/db/queries';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
// 获取单个画布
|
// 获取单个画布
|
||||||
@ -52,6 +53,14 @@ export async function PUT(
|
|||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { title, description, thumbnail, canvasData } = body;
|
const { title, description, thumbnail, canvasData } = body;
|
||||||
|
const syncedCanvasData =
|
||||||
|
canvasData !== undefined
|
||||||
|
? await syncCanvasNodesForProject({
|
||||||
|
projectId: id,
|
||||||
|
userId: session.user.id,
|
||||||
|
canvasData,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// 先检查画布是否存在
|
// 先检查画布是否存在
|
||||||
const [existingProject] = await db
|
const [existingProject] = await db
|
||||||
@ -70,7 +79,7 @@ export async function PUT(
|
|||||||
if (title !== undefined) updateData.title = title;
|
if (title !== undefined) updateData.title = title;
|
||||||
if (description !== undefined) updateData.description = description;
|
if (description !== undefined) updateData.description = description;
|
||||||
if (thumbnail !== undefined) updateData.thumbnail = thumbnail;
|
if (thumbnail !== undefined) updateData.thumbnail = thumbnail;
|
||||||
if (canvasData !== undefined) updateData.canvasData = canvasData;
|
if (syncedCanvasData !== undefined) updateData.canvasData = syncedCanvasData;
|
||||||
|
|
||||||
const [updatedProject] = await db
|
const [updatedProject] = await db
|
||||||
.update(canvasProject)
|
.update(canvasProject)
|
||||||
@ -93,7 +102,7 @@ export async function PUT(
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
title: title || '未命名画布',
|
title: title || '未命名画布',
|
||||||
description,
|
description,
|
||||||
canvasData: canvasData || {
|
canvasData: syncedCanvasData || {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
edges: [],
|
edges: [],
|
||||||
viewport: { x: 0, y: 0, zoom: 1 },
|
viewport: { x: 0, y: 0, zoom: 1 },
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { auth } from '@/app/(auth)/auth';
|
import { auth } from '@/app/(auth)/auth';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { canvasProject } from '@/lib/db/schema';
|
import { canvasProject } from '@/lib/db/schema';
|
||||||
|
import { syncCanvasNodesForProject } from '@/lib/db/queries';
|
||||||
import { eq, desc } from 'drizzle-orm';
|
import { eq, desc } from 'drizzle-orm';
|
||||||
|
|
||||||
// 获取画布列表
|
// 获取画布列表
|
||||||
@ -36,7 +37,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { title, description, canvasData } = body;
|
const { title, description, canvasData } = body;
|
||||||
|
|
||||||
const [newProject] = await db
|
const [insertedProject] = await db
|
||||||
.insert(canvasProject)
|
.insert(canvasProject)
|
||||||
.values({
|
.values({
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
@ -50,6 +51,26 @@ export async function POST(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
let newProject = insertedProject;
|
||||||
|
if (canvasData) {
|
||||||
|
const syncedCanvasData = await syncCanvasNodesForProject({
|
||||||
|
projectId: insertedProject.id,
|
||||||
|
userId: session.user.id,
|
||||||
|
canvasData,
|
||||||
|
});
|
||||||
|
const [updatedProject] = await db
|
||||||
|
.update(canvasProject)
|
||||||
|
.set({
|
||||||
|
canvasData: syncedCanvasData,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(canvasProject.id, insertedProject.id))
|
||||||
|
.returning();
|
||||||
|
if (updatedProject) {
|
||||||
|
newProject = updatedProject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true, data: newProject });
|
return NextResponse.json({ success: true, data: newProject });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建画布失败:', error);
|
console.error('创建画布失败:', error);
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getSystemSetting } from "@/lib/db/queries";
|
import {
|
||||||
|
createMediaAssetRelation,
|
||||||
|
getMediaAssetsByUserAndUrls,
|
||||||
|
getSystemSetting,
|
||||||
|
upsertMediaAsset,
|
||||||
|
} from "@/lib/db/queries";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
@ -407,6 +412,7 @@ async function finalizeEnhanceTaskResult(params: {
|
|||||||
chatId?: string | null;
|
chatId?: string | null;
|
||||||
messageId?: string | null;
|
messageId?: string | null;
|
||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
|
sourceImageUrls?: string[];
|
||||||
}) {
|
}) {
|
||||||
if (params.taskResult?.status !== 200 || !params.taskResult?.data) {
|
if (params.taskResult?.status !== 200 || !params.taskResult?.data) {
|
||||||
throw new Error(params.taskResult?.message || "未知错误");
|
throw new Error(params.taskResult?.message || "未知错误");
|
||||||
@ -438,9 +444,14 @@ async function finalizeEnhanceTaskResult(params: {
|
|||||||
|
|
||||||
let saved = false;
|
let saved = false;
|
||||||
let saveError: unknown = null;
|
let saveError: unknown = null;
|
||||||
|
let savedImage:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| null = null;
|
||||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
await db.insert(generatedImage).values({
|
([savedImage] = await db.insert(generatedImage).values({
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
chatId: params.chatId || null,
|
chatId: params.chatId || null,
|
||||||
messageId: params.messageId || null,
|
messageId: params.messageId || null,
|
||||||
@ -449,7 +460,7 @@ async function finalizeEnhanceTaskResult(params: {
|
|||||||
imageSize: "2K",
|
imageSize: "2K",
|
||||||
prompt: finalPrompt,
|
prompt: finalPrompt,
|
||||||
modelId: "enhance-1.0",
|
modelId: "enhance-1.0",
|
||||||
});
|
}).returning());
|
||||||
saved = true;
|
saved = true;
|
||||||
break;
|
break;
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
@ -464,6 +475,55 @@ async function finalizeEnhanceTaskResult(params: {
|
|||||||
throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
|
throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedImage) {
|
||||||
|
try {
|
||||||
|
const sourceAssets =
|
||||||
|
Array.isArray(params.sourceImageUrls) && params.sourceImageUrls.length > 0
|
||||||
|
? await getMediaAssetsByUserAndUrls({
|
||||||
|
userId: params.userId,
|
||||||
|
urls: params.sourceImageUrls,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const media = await upsertMediaAsset({
|
||||||
|
userId: params.userId,
|
||||||
|
chatId: params.chatId || null,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: "edited",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl: uploadResult.original,
|
||||||
|
previewUrl: uploadResult.original,
|
||||||
|
thumbnailUrl: uploadResult.thumbnail,
|
||||||
|
displayName: finalPrompt.slice(0, 80),
|
||||||
|
prompt: finalPrompt,
|
||||||
|
modelId: "enhance-1.0",
|
||||||
|
sourceMessageId: params.messageId || null,
|
||||||
|
sourceTable: "GeneratedImage",
|
||||||
|
sourceRowId: savedImage.id,
|
||||||
|
primaryParentAssetId: sourceAssets[0]?.id ?? null,
|
||||||
|
metadata: {
|
||||||
|
imageSize: "2K",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (media) {
|
||||||
|
for (const sourceAsset of sourceAssets) {
|
||||||
|
await createMediaAssetRelation({
|
||||||
|
userId: params.userId,
|
||||||
|
chatId: params.chatId || null,
|
||||||
|
fromAssetId: sourceAsset.id,
|
||||||
|
toAssetId: media.id,
|
||||||
|
relationType: "edited_from",
|
||||||
|
sourceMessageId: params.messageId || null,
|
||||||
|
metadata: {
|
||||||
|
prompt: finalPrompt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (assetError) {
|
||||||
|
console.error("[Enhance] ⚠️ MediaAsset 注册失败:", assetError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const processingTime =
|
const processingTime =
|
||||||
typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined;
|
typeof params.startedAt === "number" ? (Date.now() - params.startedAt) / 1000 : undefined;
|
||||||
|
|
||||||
@ -621,6 +681,7 @@ export async function POST(request: NextRequest) {
|
|||||||
let prompt = ""; // 用户提示词(可选)
|
let prompt = ""; // 用户提示词(可选)
|
||||||
let chatId: string | null = null; // 聊天ID(可选)
|
let chatId: string | null = null; // 聊天ID(可选)
|
||||||
let messageId: string | null = null; // 消息ID(可选)
|
let messageId: string | null = null; // 消息ID(可选)
|
||||||
|
let sourceImageUrl: string | null = null;
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
if (contentType.includes("application/json")) {
|
||||||
console.log("[Enhance API] 解析JSON请求体...");
|
console.log("[Enhance API] 解析JSON请求体...");
|
||||||
@ -631,6 +692,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// 优先使用 imageUrl,其次使用 image_base64
|
// 优先使用 imageUrl,其次使用 image_base64
|
||||||
if (body.imageUrl) {
|
if (body.imageUrl) {
|
||||||
|
sourceImageUrl = String(body.imageUrl);
|
||||||
// 从 URL 获取图片并转换为 Base64
|
// 从 URL 获取图片并转换为 Base64
|
||||||
console.log(`[Enhance API] 从 URL 获取图片: ${body.imageUrl}`);
|
console.log(`[Enhance API] 从 URL 获取图片: ${body.imageUrl}`);
|
||||||
try {
|
try {
|
||||||
@ -729,9 +791,14 @@ export async function POST(request: NextRequest) {
|
|||||||
// 步骤4: 保存到数据库
|
// 步骤4: 保存到数据库
|
||||||
let saved = false;
|
let saved = false;
|
||||||
let saveError: unknown = null;
|
let saveError: unknown = null;
|
||||||
|
let savedImage:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| null = null;
|
||||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const [savedImage] = await db.insert(generatedImage).values({
|
([savedImage] = await db.insert(generatedImage).values({
|
||||||
userId,
|
userId,
|
||||||
chatId: chatId || null,
|
chatId: chatId || null,
|
||||||
messageId: messageId || null,
|
messageId: messageId || null,
|
||||||
@ -740,7 +807,7 @@ export async function POST(request: NextRequest) {
|
|||||||
imageSize: `${config.backupScale}x放大`,
|
imageSize: `${config.backupScale}x放大`,
|
||||||
prompt: prompt || "Real-ESRGAN图片变清晰处理",
|
prompt: prompt || "Real-ESRGAN图片变清晰处理",
|
||||||
modelId: `realesrgan-${config.backupModel}`,
|
modelId: `realesrgan-${config.backupModel}`,
|
||||||
}).returning();
|
}).returning());
|
||||||
console.log(`[Enhance-RealESRGAN] ✅ 保存到数据库成功,ID: ${savedImage.id}`);
|
console.log(`[Enhance-RealESRGAN] ✅ 保存到数据库成功,ID: ${savedImage.id}`);
|
||||||
saved = true;
|
saved = true;
|
||||||
break;
|
break;
|
||||||
@ -756,6 +823,56 @@ export async function POST(request: NextRequest) {
|
|||||||
throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
|
throw new Error("图片变清晰成功但保存到相册失败,请稍后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedImage) {
|
||||||
|
try {
|
||||||
|
const sourceAssets =
|
||||||
|
sourceImageUrl
|
||||||
|
? await getMediaAssetsByUserAndUrls({
|
||||||
|
userId,
|
||||||
|
urls: [sourceImageUrl],
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const media = await upsertMediaAsset({
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: "edited",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl,
|
||||||
|
previewUrl: originalUrl,
|
||||||
|
thumbnailUrl,
|
||||||
|
displayName: (prompt || "Real-ESRGAN图片变清晰处理").slice(0, 80),
|
||||||
|
prompt: prompt || "Real-ESRGAN图片变清晰处理",
|
||||||
|
modelId: `realesrgan-${config.backupModel}`,
|
||||||
|
sourceMessageId: messageId,
|
||||||
|
sourceTable: "GeneratedImage",
|
||||||
|
sourceRowId: savedImage.id,
|
||||||
|
primaryParentAssetId: sourceAssets[0]?.id ?? null,
|
||||||
|
metadata: {
|
||||||
|
imageSize: `${config.backupScale}x放大`,
|
||||||
|
apiType: "realesrgan-url",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (media) {
|
||||||
|
for (const sourceAsset of sourceAssets) {
|
||||||
|
await createMediaAssetRelation({
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
fromAssetId: sourceAsset.id,
|
||||||
|
toAssetId: media.id,
|
||||||
|
relationType: "edited_from",
|
||||||
|
sourceMessageId: messageId,
|
||||||
|
metadata: {
|
||||||
|
prompt: prompt || "Real-ESRGAN图片变清晰处理",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (assetError) {
|
||||||
|
console.error("[Enhance-RealESRGAN] ⚠️ MediaAsset 注册失败:", assetError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 返回结果
|
// 返回结果
|
||||||
console.log(`[Enhance-RealESRGAN] ✅ 全部完成,总耗时: ${result.processingTime.toFixed(2)}秒`);
|
console.log(`[Enhance-RealESRGAN] ✅ 全部完成,总耗时: ${result.processingTime.toFixed(2)}秒`);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@ -997,6 +1114,7 @@ export async function POST(request: NextRequest) {
|
|||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
startedAt: startTime,
|
startedAt: startTime,
|
||||||
|
sourceImageUrls: sourceImageUrl ? [sourceImageUrl] : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(responsePayload);
|
return NextResponse.json(responsePayload);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { auth } from "@/app/(auth)/auth";
|
|||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { generatedImage } from "@/lib/db/schema";
|
import { generatedImage } from "@/lib/db/schema";
|
||||||
|
import { upsertMediaAsset } from "@/lib/db/queries";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 压缩图片生成缩略图(使用 sharp)
|
* 压缩图片生成缩略图(使用 sharp)
|
||||||
@ -174,6 +175,32 @@ export async function POST(request: NextRequest) {
|
|||||||
modelId: "resize-1.0", // 标记为尺寸调整处理
|
modelId: "resize-1.0", // 标记为尺寸调整处理
|
||||||
}).returning();
|
}).returning();
|
||||||
console.log(`[Resize] ✅ 图片已保存到相册和生图记录,ID: ${savedImage.id}`);
|
console.log(`[Resize] ✅ 图片已保存到相册和生图记录,ID: ${savedImage.id}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await upsertMediaAsset({
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: "edited",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl,
|
||||||
|
previewUrl: originalUrl,
|
||||||
|
thumbnailUrl,
|
||||||
|
displayName: (prompt || "尺寸调整处理").slice(0, 80),
|
||||||
|
prompt: prompt || "尺寸调整处理",
|
||||||
|
modelId: "resize-1.0",
|
||||||
|
sourceMessageId: messageId,
|
||||||
|
sourceTable: "GeneratedImage",
|
||||||
|
sourceRowId: savedImage.id,
|
||||||
|
metadata: {
|
||||||
|
imageSize: `${canvasWidth}x${canvasHeight}`,
|
||||||
|
canvasWidth,
|
||||||
|
canvasHeight,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (assetError) {
|
||||||
|
console.error("[Resize] ⚠️ MediaAsset 注册失败:", assetError);
|
||||||
|
}
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.error("[Resize] ❌ 保存到数据库失败:", dbError);
|
console.error("[Resize] ❌ 保存到数据库失败:", dbError);
|
||||||
// 数据库保存失败时仍然返回成功,但记录错误(图片已上传到S3,用户可以访问)
|
// 数据库保存失败时仍然返回成功,但记录错误(图片已上传到S3,用户可以访问)
|
||||||
@ -196,4 +223,3 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getSystemSetting } from "@/lib/db/queries";
|
import {
|
||||||
|
createMediaAssetRelation,
|
||||||
|
getMediaAssetsByUserAndUrls,
|
||||||
|
getSystemSetting,
|
||||||
|
upsertMediaAsset,
|
||||||
|
} from "@/lib/db/queries";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
@ -502,6 +507,7 @@ export async function POST(request: NextRequest) {
|
|||||||
let chatId: string | null = null;
|
let chatId: string | null = null;
|
||||||
let messageId: string | null = null;
|
let messageId: string | null = null;
|
||||||
let skipGallerySave = false;
|
let skipGallerySave = false;
|
||||||
|
let sourceImageUrl: string | null = null;
|
||||||
|
|
||||||
if (contentType.includes("application/json")) {
|
if (contentType.includes("application/json")) {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
@ -512,6 +518,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const imageUrl = body.imageUrl || body.image_url;
|
const imageUrl = body.imageUrl || body.image_url;
|
||||||
|
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
|
sourceImageUrl = String(imageUrl);
|
||||||
console.log(`[RMBG] 从 URL 获取图片: ${imageUrl}`);
|
console.log(`[RMBG] 从 URL 获取图片: ${imageUrl}`);
|
||||||
try {
|
try {
|
||||||
const imageResponse = await fetch(imageUrl);
|
const imageResponse = await fetch(imageUrl);
|
||||||
@ -650,9 +657,14 @@ export async function POST(request: NextRequest) {
|
|||||||
if (!skipGallerySave) {
|
if (!skipGallerySave) {
|
||||||
let saved = false;
|
let saved = false;
|
||||||
let saveError: unknown = null;
|
let saveError: unknown = null;
|
||||||
|
let savedImage:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| null = null;
|
||||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const [savedImage] = await db.insert(generatedImage).values({
|
([savedImage] = await db.insert(generatedImage).values({
|
||||||
userId,
|
userId,
|
||||||
chatId: chatId || null,
|
chatId: chatId || null,
|
||||||
messageId: messageId || null,
|
messageId: messageId || null,
|
||||||
@ -661,7 +673,7 @@ export async function POST(request: NextRequest) {
|
|||||||
imageSize: "2K",
|
imageSize: "2K",
|
||||||
prompt: prompt || "抠图处理",
|
prompt: prompt || "抠图处理",
|
||||||
modelId: provider === "photoroom" ? "photoroom-hd" : provider === "aoscdn" ? "aos-segmentation" : "rmbg-2.0",
|
modelId: provider === "photoroom" ? "photoroom-hd" : provider === "aoscdn" ? "aos-segmentation" : "rmbg-2.0",
|
||||||
}).returning();
|
}).returning());
|
||||||
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
|
console.log(`[RMBG] ✅ 图片已保存到相册,ID: ${savedImage.id}`);
|
||||||
saved = true;
|
saved = true;
|
||||||
break;
|
break;
|
||||||
@ -676,6 +688,62 @@ export async function POST(request: NextRequest) {
|
|||||||
console.error("[RMBG] ❌ 保存到数据库失败:", saveError);
|
console.error("[RMBG] ❌ 保存到数据库失败:", saveError);
|
||||||
throw new Error("图片处理成功但保存到相册失败,请稍后重试");
|
throw new Error("图片处理成功但保存到相册失败,请稍后重试");
|
||||||
}
|
}
|
||||||
|
if (savedImage) {
|
||||||
|
try {
|
||||||
|
const sourceAssets =
|
||||||
|
sourceImageUrl
|
||||||
|
? await getMediaAssetsByUserAndUrls({
|
||||||
|
userId,
|
||||||
|
urls: [sourceImageUrl],
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const modelId =
|
||||||
|
provider === "photoroom"
|
||||||
|
? "photoroom-hd"
|
||||||
|
: provider === "aoscdn"
|
||||||
|
? "aos-segmentation"
|
||||||
|
: "rmbg-2.0";
|
||||||
|
const media = await upsertMediaAsset({
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: "edited",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl,
|
||||||
|
previewUrl: originalUrl,
|
||||||
|
thumbnailUrl,
|
||||||
|
displayName: (prompt || "抠图处理").slice(0, 80),
|
||||||
|
prompt: prompt || "抠图处理",
|
||||||
|
modelId,
|
||||||
|
sourceMessageId: messageId,
|
||||||
|
sourceTable: "GeneratedImage",
|
||||||
|
sourceRowId: savedImage.id,
|
||||||
|
primaryParentAssetId: sourceAssets[0]?.id ?? null,
|
||||||
|
metadata: {
|
||||||
|
provider,
|
||||||
|
imageSize: "2K",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (media) {
|
||||||
|
for (const sourceAsset of sourceAssets) {
|
||||||
|
await createMediaAssetRelation({
|
||||||
|
userId,
|
||||||
|
chatId,
|
||||||
|
fromAssetId: sourceAsset.id,
|
||||||
|
toAssetId: media.id,
|
||||||
|
relationType: "edited_from",
|
||||||
|
sourceMessageId: messageId,
|
||||||
|
metadata: {
|
||||||
|
prompt: prompt || "抠图处理",
|
||||||
|
provider,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (assetError) {
|
||||||
|
console.error("[RMBG] ⚠️ MediaAsset 注册失败:", assetError);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("[RMBG] Skip gallery save by request flag");
|
console.log("[RMBG] Skip gallery save by request flag");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,13 @@ import { z } from "zod";
|
|||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { generatedImage } from "@/lib/db/schema";
|
import { generatedImage } from "@/lib/db/schema";
|
||||||
import { getEnabledAiModelsWithProvider, getSystemSetting } from "@/lib/db/queries";
|
import {
|
||||||
|
createMediaAssetRelation,
|
||||||
|
getEnabledAiModelsWithProvider,
|
||||||
|
getMediaAssetsByUserAndUrls,
|
||||||
|
getSystemSetting,
|
||||||
|
upsertMediaAsset,
|
||||||
|
} from "@/lib/db/queries";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { createGeminiImage } from "@/lib/ai/providers/gemini-image";
|
import { createGeminiImage } from "@/lib/ai/providers/gemini-image";
|
||||||
@ -921,11 +927,17 @@ async function saveSceneFusionImageRecord(params: {
|
|||||||
imageSize: string;
|
imageSize: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
modelId: string;
|
modelId: string;
|
||||||
|
sourceImageUrls?: string[];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
|
let savedImage:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| null = null;
|
||||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
await db.insert(generatedImage).values({
|
([savedImage] = await db.insert(generatedImage).values({
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
chatId: null,
|
chatId: null,
|
||||||
messageId: null,
|
messageId: null,
|
||||||
@ -934,8 +946,8 @@ async function saveSceneFusionImageRecord(params: {
|
|||||||
imageSize: params.imageSize,
|
imageSize: params.imageSize,
|
||||||
prompt: params.prompt,
|
prompt: params.prompt,
|
||||||
modelId: params.modelId,
|
modelId: params.modelId,
|
||||||
});
|
}).returning());
|
||||||
return;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
if (attempt < 3) {
|
if (attempt < 3) {
|
||||||
@ -944,7 +956,53 @@ async function saveSceneFusionImageRecord(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw lastError instanceof Error ? lastError : new Error("保存到相册失败");
|
if (!savedImage) {
|
||||||
|
throw lastError instanceof Error ? lastError : new Error("保存到相册失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceAssets =
|
||||||
|
Array.isArray(params.sourceImageUrls) && params.sourceImageUrls.length > 0
|
||||||
|
? await getMediaAssetsByUserAndUrls({
|
||||||
|
userId: params.userId,
|
||||||
|
urls: params.sourceImageUrls,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const media = await upsertMediaAsset({
|
||||||
|
userId: params.userId,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: "edited",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl: params.originalUrl,
|
||||||
|
previewUrl: params.originalUrl,
|
||||||
|
thumbnailUrl: params.thumbnailUrl,
|
||||||
|
displayName: params.prompt.slice(0, 80),
|
||||||
|
prompt: params.prompt,
|
||||||
|
modelId: params.modelId,
|
||||||
|
sourceTable: "GeneratedImage",
|
||||||
|
sourceRowId: savedImage.id,
|
||||||
|
primaryParentAssetId: sourceAssets[0]?.id ?? null,
|
||||||
|
metadata: {
|
||||||
|
imageSize: params.imageSize,
|
||||||
|
sourceImageUrls: params.sourceImageUrls ?? [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (media) {
|
||||||
|
for (const sourceAsset of sourceAssets) {
|
||||||
|
await createMediaAssetRelation({
|
||||||
|
userId: params.userId,
|
||||||
|
fromAssetId: sourceAsset.id,
|
||||||
|
toAssetId: media.id,
|
||||||
|
relationType: "edited_from",
|
||||||
|
metadata: {
|
||||||
|
prompt: params.prompt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (assetError) {
|
||||||
|
console.error("[SceneFusion] ⚠️ MediaAsset 注册失败:", assetError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1856,6 +1914,7 @@ export async function POST(request: Request) {
|
|||||||
imageSize: dbImageSize,
|
imageSize: dbImageSize,
|
||||||
prompt: `[场景裂变] ${result.prompt}`,
|
prompt: `[场景裂变] ${result.prompt}`,
|
||||||
modelId: candidate.model.modelId,
|
modelId: candidate.model.modelId,
|
||||||
|
sourceImageUrls: [fusionProductImageUrl, resolvedScene.imageUrl],
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`
|
`[SceneFusion] ✅ Saved image to gallery: ${result.original.substring(0, 80)}...`
|
||||||
|
|||||||
@ -4,6 +4,11 @@ import { db } from "@/lib/db";
|
|||||||
import { systemSettings, generatedImage } from "@/lib/db/schema";
|
import { systemSettings, generatedImage } from "@/lib/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
|
import {
|
||||||
|
createMediaAssetRelation,
|
||||||
|
getMediaAssetsByUserAndUrls,
|
||||||
|
upsertMediaAsset,
|
||||||
|
} from "@/lib/db/queries";
|
||||||
|
|
||||||
// PicWish API 需要公网可访问的 HTTP/HTTPS URL
|
// PicWish API 需要公网可访问的 HTTP/HTTPS URL
|
||||||
const WATERMARK_SYNC_WAIT_TIMEOUT_MS = 20_000;
|
const WATERMARK_SYNC_WAIT_TIMEOUT_MS = 20_000;
|
||||||
@ -112,6 +117,7 @@ async function finalizeWatermarkTaskResult(params: {
|
|||||||
userId: string;
|
userId: string;
|
||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
sourceImageUrls?: string[];
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
original_url: string;
|
original_url: string;
|
||||||
@ -138,16 +144,21 @@ async function finalizeWatermarkTaskResult(params: {
|
|||||||
|
|
||||||
let saved = false;
|
let saved = false;
|
||||||
let saveError: unknown = null;
|
let saveError: unknown = null;
|
||||||
|
let savedImage:
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| null = null;
|
||||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
await db.insert(generatedImage).values({
|
([savedImage] = await db.insert(generatedImage).values({
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
thumbnailUrl: uploadResult.url,
|
thumbnailUrl: uploadResult.url,
|
||||||
originalUrl: uploadResult.url,
|
originalUrl: uploadResult.url,
|
||||||
imageSize: "2K",
|
imageSize: "2K",
|
||||||
prompt: "去水印处理",
|
prompt: "去水印处理",
|
||||||
modelId: "watermark-remove",
|
modelId: "watermark-remove",
|
||||||
});
|
}).returning());
|
||||||
saved = true;
|
saved = true;
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -162,6 +173,51 @@ async function finalizeWatermarkTaskResult(params: {
|
|||||||
throw new Error("去水印成功但保存到相册失败,请稍后重试");
|
throw new Error("去水印成功但保存到相册失败,请稍后重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedImage) {
|
||||||
|
try {
|
||||||
|
const sourceAssets =
|
||||||
|
Array.isArray(params.sourceImageUrls) && params.sourceImageUrls.length > 0
|
||||||
|
? await getMediaAssetsByUserAndUrls({
|
||||||
|
userId: params.userId,
|
||||||
|
urls: params.sourceImageUrls,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const media = await upsertMediaAsset({
|
||||||
|
userId: params.userId,
|
||||||
|
mediaType: "image",
|
||||||
|
sourceType: "edited",
|
||||||
|
status: "ready",
|
||||||
|
originalUrl: uploadResult.url,
|
||||||
|
previewUrl: uploadResult.url,
|
||||||
|
thumbnailUrl: uploadResult.url,
|
||||||
|
displayName: "去水印处理",
|
||||||
|
prompt: "去水印处理",
|
||||||
|
modelId: "watermark-remove",
|
||||||
|
sourceTable: "GeneratedImage",
|
||||||
|
sourceRowId: savedImage.id,
|
||||||
|
primaryParentAssetId: sourceAssets[0]?.id ?? null,
|
||||||
|
metadata: {
|
||||||
|
imageSize: "2K",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (media) {
|
||||||
|
for (const sourceAsset of sourceAssets) {
|
||||||
|
await createMediaAssetRelation({
|
||||||
|
userId: params.userId,
|
||||||
|
fromAssetId: sourceAsset.id,
|
||||||
|
toAssetId: media.id,
|
||||||
|
relationType: "edited_from",
|
||||||
|
metadata: {
|
||||||
|
prompt: "去水印处理",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (assetError) {
|
||||||
|
console.error("[WatermarkRemove] ⚠️ MediaAsset 注册失败:", assetError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
original_url: uploadResult.url,
|
original_url: uploadResult.url,
|
||||||
@ -358,6 +414,10 @@ export async function POST(request: NextRequest) {
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
startedAt: startTime,
|
startedAt: startTime,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
|
sourceImageUrls:
|
||||||
|
typeof image_url === "string" && /^https?:\/\//i.test(image_url)
|
||||||
|
? [image_url]
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
return NextResponse.json(result);
|
return NextResponse.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -2024,6 +2024,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
url: src,
|
url: src,
|
||||||
name: nodeTitle,
|
name: nodeTitle,
|
||||||
contentType: "image/jpeg",
|
contentType: "image/jpeg",
|
||||||
|
assetId:
|
||||||
|
typeof (node?.data as any)?.assetId === "string" && (node?.data as any)?.assetId.trim()
|
||||||
|
? String((node?.data as any)?.assetId).trim()
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@ -2146,6 +2150,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
url: String(item.url),
|
url: String(item.url),
|
||||||
name: String(item.name || "参考图"),
|
name: String(item.name || "参考图"),
|
||||||
contentType: String(item.contentType || "image/jpeg"),
|
contentType: String(item.contentType || "image/jpeg"),
|
||||||
|
assetId:
|
||||||
|
typeof item.assetId === "string" && item.assetId.trim()
|
||||||
|
? item.assetId.trim()
|
||||||
|
: undefined,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
const normalizedPrompt = String(parsed.prompt || "").trim();
|
const normalizedPrompt = String(parsed.prompt || "").trim();
|
||||||
@ -3432,6 +3440,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
mediaType: attachment.contentType || "image/jpeg",
|
mediaType: attachment.contentType || "image/jpeg",
|
||||||
name: attachment.name || "参考图",
|
name: attachment.name || "参考图",
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
|
assetId: attachment.assetId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (textContent) {
|
if (textContent) {
|
||||||
@ -4077,11 +4086,14 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const imageUrl = data.url as string;
|
const imageUrl = data.url as string;
|
||||||
|
const assetId =
|
||||||
|
typeof data.assetId === "string" && data.assetId.trim() ? data.assetId.trim() : undefined;
|
||||||
|
|
||||||
updateAttachmentAtVideoSlot(slotIndex, {
|
updateAttachmentAtVideoSlot(slotIndex, {
|
||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
contentType: file.type || "image/jpeg",
|
contentType: file.type || "image/jpeg",
|
||||||
|
assetId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dimensions = await getImageDimensions(imageUrl);
|
const dimensions = await getImageDimensions(imageUrl);
|
||||||
@ -4244,6 +4256,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const imageUrl = String(data.url || "").trim();
|
const imageUrl = String(data.url || "").trim();
|
||||||
|
const assetId =
|
||||||
|
typeof data.assetId === "string" && data.assetId.trim() ? data.assetId.trim() : undefined;
|
||||||
if (!imageUrl) {
|
if (!imageUrl) {
|
||||||
throw new Error("未获取到图片地址");
|
throw new Error("未获取到图片地址");
|
||||||
}
|
}
|
||||||
@ -4252,6 +4266,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
contentType: file.type || "image/jpeg",
|
contentType: file.type || "image/jpeg",
|
||||||
|
assetId,
|
||||||
});
|
});
|
||||||
toast.success(`${DETAIL_SLOT_CONFIGS[slotIndex]?.label || "图片"}上传成功`);
|
toast.success(`${DETAIL_SLOT_CONFIGS[slotIndex]?.label || "图片"}上传成功`);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
@ -5933,6 +5948,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
mediaType: a.contentType,
|
mediaType: a.contentType,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
url: a.url,
|
url: a.url,
|
||||||
|
assetId: a.assetId,
|
||||||
hasAnnotations: hasAnnotatedImages, // 🆕 标记图片是否有标注
|
hasAnnotations: hasAnnotatedImages, // 🆕 标记图片是否有标注
|
||||||
pickedObject: att.pickedObject, // 🆕 包含物品信息
|
pickedObject: att.pickedObject, // 🆕 包含物品信息
|
||||||
});
|
});
|
||||||
@ -6142,7 +6158,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
const uploadImageFileAndAddToCanvas = useCallback(async (
|
const uploadImageFileAndAddToCanvas = useCallback(async (
|
||||||
file: File,
|
file: File,
|
||||||
nodeIndex: number
|
nodeIndex: number
|
||||||
): Promise<{ imageUrl: string; name: string; contentType: string } | null> => {
|
): Promise<{ imageUrl: string; name: string; contentType: string; assetId?: string } | null> => {
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
toast.error('请选择图片文件');
|
toast.error('请选择图片文件');
|
||||||
return null;
|
return null;
|
||||||
@ -6168,6 +6184,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const imageUrl = data.url as string;
|
const imageUrl = data.url as string;
|
||||||
|
const assetId =
|
||||||
|
typeof data.assetId === "string" && data.assetId.trim() ? data.assetId.trim() : undefined;
|
||||||
const dimensions = await getImageDimensions(imageUrl);
|
const dimensions = await getImageDimensions(imageUrl);
|
||||||
const spacing = 20;
|
const spacing = 20;
|
||||||
const x = 100 + (nodeIndex % 3) * (dimensions.width + spacing);
|
const x = 100 + (nodeIndex % 3) * (dimensions.width + spacing);
|
||||||
@ -6179,6 +6197,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
size: { width: dimensions.width, height: dimensions.height },
|
size: { width: dimensions.width, height: dimensions.height },
|
||||||
data: {
|
data: {
|
||||||
src: imageUrl,
|
src: imageUrl,
|
||||||
|
assetId,
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
title: fileName.replace(/\.[^.]+$/, ""),
|
title: fileName.replace(/\.[^.]+$/, ""),
|
||||||
prompt: fileName.replace(/\.[^.]+$/, ""),
|
prompt: fileName.replace(/\.[^.]+$/, ""),
|
||||||
@ -6194,6 +6213,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
contentType: file.type || 'image/jpeg',
|
contentType: file.type || 'image/jpeg',
|
||||||
|
assetId,
|
||||||
};
|
};
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error('图片上传失败');
|
toast.error('图片上传失败');
|
||||||
@ -6323,6 +6343,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const imageUrl = data.url;
|
const imageUrl = data.url;
|
||||||
|
const assetId =
|
||||||
|
typeof data.assetId === "string" && data.assetId.trim() ? data.assetId.trim() : undefined;
|
||||||
|
|
||||||
// 记录第一张图片的 URL(用于自动命名和缩略图)
|
// 记录第一张图片的 URL(用于自动命名和缩略图)
|
||||||
if (!firstImageUrl) {
|
if (!firstImageUrl) {
|
||||||
@ -6341,6 +6363,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
url: imageUrl,
|
url: imageUrl,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
contentType: file.type,
|
contentType: file.type,
|
||||||
|
assetId,
|
||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -6359,6 +6382,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
|||||||
size: { width: dimensions.width, height: dimensions.height },
|
size: { width: dimensions.width, height: dimensions.height },
|
||||||
data: {
|
data: {
|
||||||
src: imageUrl,
|
src: imageUrl,
|
||||||
|
assetId,
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
title: file.name.replace(/\.[^.]+$/, ""),
|
title: file.name.replace(/\.[^.]+$/, ""),
|
||||||
prompt: file.name.replace(/\.[^.]+$/, ""),
|
prompt: file.name.replace(/\.[^.]+$/, ""),
|
||||||
|
|||||||
@ -647,6 +647,7 @@ export function CanvasWorkspace({ occlusionRight = 0 }: CanvasWorkspaceProps) {
|
|||||||
updateNode(placeholderId, {
|
updateNode(placeholderId, {
|
||||||
data: {
|
data: {
|
||||||
src: result.data.url,
|
src: result.data.url,
|
||||||
|
assetId: result.data.assetId,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
prompt: cleanName || "拖拽上传",
|
prompt: cleanName || "拖拽上传",
|
||||||
title: cleanName || "拖拽上传",
|
title: cleanName || "拖拽上传",
|
||||||
|
|||||||
@ -1270,6 +1270,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
|||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
src: result.data.url,
|
src: result.data.url,
|
||||||
|
assetId: result.data.assetId,
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
title: normalizeImageTitle(data.title) || fileBaseName,
|
title: normalizeImageTitle(data.title) || fileBaseName,
|
||||||
},
|
},
|
||||||
@ -1581,7 +1582,6 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
|||||||
|
|
||||||
const handleResizeEditorComplete = useCallback(
|
const handleResizeEditorComplete = useCallback(
|
||||||
async (resizedImageUrl: string) => {
|
async (resizedImageUrl: string) => {
|
||||||
setResizeAnnotatorOpen(false);
|
|
||||||
let finalUrl = resizedImageUrl;
|
let finalUrl = resizedImageUrl;
|
||||||
|
|
||||||
if (resizedImageUrl.startsWith("data:")) {
|
if (resizedImageUrl.startsWith("data:")) {
|
||||||
@ -1613,6 +1613,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
|||||||
connections: [node.id],
|
connections: [node.id],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setResizeAnnotatorOpen(false);
|
||||||
toast.success("尺寸调整结果已发送到画布");
|
toast.success("尺寸调整结果已发送到画布");
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
|||||||
@ -1248,6 +1248,8 @@ export function Chat({
|
|||||||
url: uploadResult.url,
|
url: uploadResult.url,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
contentType: "image/png",
|
contentType: "image/png",
|
||||||
|
assetId:
|
||||||
|
typeof uploadResult.assetId === "string" ? uploadResult.assetId : undefined,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -845,6 +845,7 @@ export function LovartHome({
|
|||||||
url: data.url,
|
url: data.url,
|
||||||
name: item.file.name,
|
name: item.file.name,
|
||||||
contentType: item.file.type || "image/jpeg",
|
contentType: item.file.type || "image/jpeg",
|
||||||
|
assetId: typeof data.assetId === "string" ? data.assetId : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,7 +47,7 @@ type DrawingAction = {
|
|||||||
|
|
||||||
interface ImageAnnotatorProps {
|
interface ImageAnnotatorProps {
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
onConfirm: (annotatedImageUrl: string) => void;
|
onConfirm: (annotatedImageUrl: string) => void | Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onHomeClick?: () => void;
|
onHomeClick?: () => void;
|
||||||
initialResizeMode?: boolean; // 是否直接进入尺寸调整模式
|
initialResizeMode?: boolean; // 是否直接进入尺寸调整模式
|
||||||
@ -1144,7 +1144,8 @@ export function ImageAnnotator({
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 保存处理后的图片URL和Base64(优先使用original_url,确保下载的是原图)
|
const finalResultUrl = result.original_url || dataUrl;
|
||||||
|
|
||||||
if (result.original_url) {
|
if (result.original_url) {
|
||||||
console.log("[尺寸调整] 原图URL:", result.original_url);
|
console.log("[尺寸调整] 原图URL:", result.original_url);
|
||||||
setProcessedImageUrl(result.original_url);
|
setProcessedImageUrl(result.original_url);
|
||||||
@ -1153,10 +1154,8 @@ export function ImageAnnotator({
|
|||||||
}
|
}
|
||||||
setProcessedImageBase64(base64Data);
|
setProcessedImageBase64(base64Data);
|
||||||
setProcessType("resize");
|
setProcessType("resize");
|
||||||
// 不退出尺寸调整模式,保持原图显示
|
|
||||||
// setIsResizeMode(false);
|
|
||||||
|
|
||||||
toast.success(`尺寸调整完成!图片已调整为 ${resizeCanvasWidth}x${resizeCanvasHeight} 像素,已保存到相册`);
|
await Promise.resolve(onConfirm(finalResultUrl));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || "尺寸调整处理失败");
|
throw new Error(result.error || "尺寸调整处理失败");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -430,6 +430,7 @@ function PureMultimodalInput({
|
|||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
name: attachment.name,
|
name: attachment.name,
|
||||||
mediaType: attachment.contentType,
|
mediaType: attachment.contentType,
|
||||||
|
assetId: attachment.assetId,
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
@ -594,13 +595,13 @@ function PureMultimodalInput({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const uploadFile = useCallback(async (file: File) => {
|
const uploadFile = useCallback(async (file: File): Promise<Attachment | null> => {
|
||||||
try {
|
try {
|
||||||
// 先压缩图片
|
// 先压缩图片
|
||||||
console.log(`[上传] 开始处理文件: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB`);
|
console.log(`[上传] 开始处理文件: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)}MB`);
|
||||||
const processedFile = await compressImage(file);
|
const processedFile = await compressImage(file);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", processedFile);
|
formData.append("file", processedFile);
|
||||||
|
|
||||||
const response = await fetchUploadWithRetry(formData);
|
const response = await fetchUploadWithRetry(formData);
|
||||||
@ -616,14 +617,17 @@ function PureMultimodalInput({
|
|||||||
url, // 完整的可访问 URL
|
url, // 完整的可访问 URL
|
||||||
name: file.name, // 使用原始文件名,不是 pathname
|
name: file.name, // 使用原始文件名,不是 pathname
|
||||||
contentType,
|
contentType,
|
||||||
|
assetId: typeof data.assetId === "string" ? data.assetId : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { error } = await response.json();
|
const { error } = await response.json();
|
||||||
console.error(`[上传] 服务器返回错误:`, error);
|
console.error(`[上传] 服务器返回错误:`, error);
|
||||||
toast.error(error);
|
toast.error(error);
|
||||||
|
return null;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
console.error("[上传] 失败:", _error);
|
console.error("[上传] 失败:", _error);
|
||||||
toast.error(_error instanceof Error ? _error.message : "文件上传失败,请稍后重试");
|
toast.error(_error instanceof Error ? _error.message : "文件上传失败,请稍后重试");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}, [compressImage]);
|
}, [compressImage]);
|
||||||
|
|
||||||
@ -799,7 +803,7 @@ function PureMultimodalInput({
|
|||||||
const uploadPromises = filesToUpload.map((file) => uploadFile(file));
|
const uploadPromises = filesToUpload.map((file) => uploadFile(file));
|
||||||
const uploadedAttachments = await Promise.all(uploadPromises);
|
const uploadedAttachments = await Promise.all(uploadPromises);
|
||||||
const successfullyUploadedAttachments = uploadedAttachments.filter(
|
const successfullyUploadedAttachments = uploadedAttachments.filter(
|
||||||
(attachment) => attachment !== undefined
|
(attachment): attachment is Attachment => attachment !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
setAttachments((currentAttachments) => [
|
setAttachments((currentAttachments) => [
|
||||||
@ -852,15 +856,15 @@ function PureMultimodalInput({
|
|||||||
|
|
||||||
const uploadedAttachments = await Promise.all(uploadPromises);
|
const uploadedAttachments = await Promise.all(uploadPromises);
|
||||||
const successfullyUploadedAttachments = uploadedAttachments.filter(
|
const successfullyUploadedAttachments = uploadedAttachments.filter(
|
||||||
(attachment) =>
|
(attachment): attachment is Attachment =>
|
||||||
attachment !== undefined &&
|
attachment !== null &&
|
||||||
attachment.url !== undefined &&
|
attachment.url !== undefined &&
|
||||||
attachment.contentType !== undefined
|
attachment.contentType !== undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
setAttachments((curr) => [
|
setAttachments((curr) => [
|
||||||
...curr,
|
...curr,
|
||||||
...(successfullyUploadedAttachments as Attachment[]),
|
...successfullyUploadedAttachments,
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading pasted images:", error);
|
console.error("Error uploading pasted images:", error);
|
||||||
@ -952,7 +956,7 @@ function PureMultimodalInput({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const validAttachments = uploadedAttachments.filter(
|
const validAttachments = uploadedAttachments.filter(
|
||||||
(attachment): attachment is Attachment => attachment !== undefined
|
(attachment): attachment is Attachment => attachment !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
setAttachments((currentAttachments) => [
|
setAttachments((currentAttachments) => [
|
||||||
@ -1157,6 +1161,7 @@ function PureMultimodalInput({
|
|||||||
|
|
||||||
// 如果仍然是 base64 格式,先上传到服务器
|
// 如果仍然是 base64 格式,先上传到服务器
|
||||||
let finalUrl = annotatedImageUrl;
|
let finalUrl = annotatedImageUrl;
|
||||||
|
let assetId: string | undefined;
|
||||||
if (annotatedImageUrl.startsWith("data:")) {
|
if (annotatedImageUrl.startsWith("data:")) {
|
||||||
try {
|
try {
|
||||||
toast.info("正在上传图片...");
|
toast.info("正在上传图片...");
|
||||||
@ -1168,6 +1173,10 @@ function PureMultimodalInput({
|
|||||||
if (uploadResponse.ok) {
|
if (uploadResponse.ok) {
|
||||||
const result = await uploadResponse.json();
|
const result = await uploadResponse.json();
|
||||||
finalUrl = result.url;
|
finalUrl = result.url;
|
||||||
|
assetId =
|
||||||
|
typeof result.assetId === "string" && result.assetId.trim()
|
||||||
|
? result.assetId.trim()
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("上传标注图片失败:", error);
|
console.error("上传标注图片失败:", error);
|
||||||
@ -1182,6 +1191,7 @@ function PureMultimodalInput({
|
|||||||
url: finalUrl,
|
url: finalUrl,
|
||||||
name: fileName,
|
name: fileName,
|
||||||
contentType: "image/png",
|
contentType: "image/png",
|
||||||
|
assetId,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
// 如果输入框为空,自动填入提示词
|
// 如果输入框为空,自动填入提示词
|
||||||
|
|||||||
@ -53,42 +53,45 @@ function normalizeImageInputRef(rawValue: unknown, requestOrigin?: string): stri
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectImageInputsFromMessageParts(
|
function collectImageRefsFromMessageParts(
|
||||||
parts: Array<Record<string, any>> | undefined,
|
parts: Array<Record<string, any>> | undefined,
|
||||||
requestOrigin?: string,
|
requestOrigin?: string,
|
||||||
): string[] {
|
): Array<{ url: string; assetId?: string }> {
|
||||||
if (!Array.isArray(parts) || parts.length === 0) return [];
|
if (!Array.isArray(parts) || parts.length === 0) return [];
|
||||||
|
|
||||||
const result: string[] = [];
|
const result: Array<{ url: string; assetId?: string }> = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const pushInput = (rawValue: unknown) => {
|
const pushInput = (rawValue: unknown, assetId?: unknown) => {
|
||||||
const normalized = normalizeImageInputRef(rawValue, requestOrigin);
|
const normalized = normalizeImageInputRef(rawValue, requestOrigin);
|
||||||
if (!normalized) return;
|
if (!normalized) return;
|
||||||
const key = normalizeMediaUrl(normalized);
|
const key = normalizeMediaUrl(normalized);
|
||||||
if (!key || seen.has(key)) return;
|
if (!key || seen.has(key)) return;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
result.push(normalized);
|
result.push({
|
||||||
|
url: normalized,
|
||||||
|
assetId: typeof assetId === "string" && assetId.trim() ? assetId.trim() : undefined,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (!part) continue;
|
if (!part) continue;
|
||||||
|
|
||||||
if (part.type === "file") {
|
if (part.type === "file") {
|
||||||
pushInput(part.url);
|
pushInput(part.url, part.assetId);
|
||||||
pushInput(part.data);
|
pushInput(part.data, part.assetId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.type === "image") {
|
if (part.type === "image") {
|
||||||
pushInput(part.url);
|
pushInput(part.url, part.assetId);
|
||||||
pushInput(part.image);
|
pushInput(part.image, part.assetId);
|
||||||
pushInput(part.data);
|
pushInput(part.data, part.assetId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.type === "image_url") {
|
if (part.type === "image_url") {
|
||||||
pushInput(part.image_url?.url);
|
pushInput(part.image_url?.url, part.assetId);
|
||||||
pushInput(part.url);
|
pushInput(part.url, part.assetId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,12 +178,12 @@ function extractHistoryImageRefs(params: {
|
|||||||
const parts = historyMessage.parts as Array<Record<string, any>> | undefined;
|
const parts = historyMessage.parts as Array<Record<string, any>> | undefined;
|
||||||
if (!Array.isArray(parts) || parts.length === 0) continue;
|
if (!Array.isArray(parts) || parts.length === 0) continue;
|
||||||
|
|
||||||
const urls = collectImageInputsFromMessageParts(parts, params.requestOrigin);
|
const refs = collectImageRefsFromMessageParts(parts, params.requestOrigin);
|
||||||
if (!urls.length) continue;
|
if (!refs.length) continue;
|
||||||
|
|
||||||
if (historyMessage.role === "assistant" && latestAssistant.length < params.maxImages) {
|
if (historyMessage.role === "assistant" && latestAssistant.length < params.maxImages) {
|
||||||
for (const url of urls) {
|
for (const ref of refs) {
|
||||||
pushRef(latestAssistant, seenAssistant, url, {
|
pushRef(latestAssistant, seenAssistant, ref.url, {
|
||||||
source: "history_assistant_result",
|
source: "history_assistant_result",
|
||||||
role: "history_result",
|
role: "history_result",
|
||||||
messageId: historyMessage.id,
|
messageId: historyMessage.id,
|
||||||
@ -189,8 +192,8 @@ function extractHistoryImageRefs(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (historyMessage.role === "user" && latestUser.length < params.maxImages) {
|
if (historyMessage.role === "user" && latestUser.length < params.maxImages) {
|
||||||
for (const url of urls) {
|
for (const ref of refs) {
|
||||||
pushRef(latestUser, seenUser, url, {
|
pushRef(latestUser, seenUser, ref.url, {
|
||||||
source: "history_user_upload",
|
source: "history_user_upload",
|
||||||
role: "reference",
|
role: "reference",
|
||||||
messageId: historyMessage.id,
|
messageId: historyMessage.id,
|
||||||
@ -214,9 +217,10 @@ export function resolveChatMediaInput(
|
|||||||
): ResolvedChatMediaInput {
|
): ResolvedChatMediaInput {
|
||||||
const maxImages = Math.max(1, Math.min(12, Math.round(params.maxImages || 4)));
|
const maxImages = Math.max(1, Math.min(12, Math.round(params.maxImages || 4)));
|
||||||
const currentMessageImageRefs = dedupeRefs(
|
const currentMessageImageRefs = dedupeRefs(
|
||||||
collectImageInputsFromMessageParts(params.messageParts, params.requestOrigin).map((url) => ({
|
collectImageRefsFromMessageParts(params.messageParts, params.requestOrigin).map((ref) => ({
|
||||||
url,
|
url: ref.url,
|
||||||
normalizedUrl: normalizeMediaUrl(url),
|
normalizedUrl: normalizeMediaUrl(ref.url),
|
||||||
|
assetId: ref.assetId,
|
||||||
source: "current_message",
|
source: "current_message",
|
||||||
role: "upload",
|
role: "upload",
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export interface CanvasLaunchAttachment {
|
|||||||
url: string;
|
url: string;
|
||||||
name: string;
|
name: string;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
|
assetId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CanvasLaunchImageParams {
|
export interface CanvasLaunchImageParams {
|
||||||
|
|||||||
123
lib/db/migrations/0051_add_media_assets.sql
Normal file
123
lib/db/migrations/0051_add_media_assets.sql
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "MediaAsset" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"userId" uuid NOT NULL,
|
||||||
|
"chatId" uuid,
|
||||||
|
"projectId" uuid,
|
||||||
|
"mediaType" varchar(32) NOT NULL,
|
||||||
|
"sourceType" varchar(32) NOT NULL,
|
||||||
|
"status" varchar(32) DEFAULT 'ready' NOT NULL,
|
||||||
|
"originalUrl" text NOT NULL,
|
||||||
|
"normalizedUrl" text NOT NULL,
|
||||||
|
"previewUrl" text,
|
||||||
|
"thumbnailUrl" text,
|
||||||
|
"mimeType" varchar(128),
|
||||||
|
"width" integer,
|
||||||
|
"height" integer,
|
||||||
|
"durationMs" integer,
|
||||||
|
"displayName" varchar(256),
|
||||||
|
"prompt" text,
|
||||||
|
"modelId" varchar(128),
|
||||||
|
"sourceMessageId" uuid,
|
||||||
|
"sourceTable" varchar(64),
|
||||||
|
"sourceRowId" varchar(128),
|
||||||
|
"sourceNodeId" varchar(128),
|
||||||
|
"rootAssetId" uuid,
|
||||||
|
"primaryParentAssetId" uuid,
|
||||||
|
"summary" jsonb,
|
||||||
|
"metadata" jsonb,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "MediaAssetRelation" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"userId" uuid NOT NULL,
|
||||||
|
"chatId" uuid,
|
||||||
|
"projectId" uuid,
|
||||||
|
"fromAssetId" uuid NOT NULL,
|
||||||
|
"toAssetId" uuid NOT NULL,
|
||||||
|
"relationType" varchar(64) NOT NULL,
|
||||||
|
"sourceMessageId" uuid,
|
||||||
|
"sourceNodeId" varchar(128),
|
||||||
|
"confidence" integer DEFAULT 100 NOT NULL,
|
||||||
|
"metadata" jsonb,
|
||||||
|
"createdAt" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAsset" ADD CONSTRAINT "MediaAsset_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAsset" ADD CONSTRAINT "MediaAsset_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAsset" ADD CONSTRAINT "MediaAsset_projectId_CanvasProject_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."CanvasProject"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAsset" ADD CONSTRAINT "MediaAsset_sourceMessageId_Message_v2_id_fk" FOREIGN KEY ("sourceMessageId") REFERENCES "public"."Message_v2"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAssetRelation" ADD CONSTRAINT "MediaAssetRelation_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAssetRelation" ADD CONSTRAINT "MediaAssetRelation_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAssetRelation" ADD CONSTRAINT "MediaAssetRelation_projectId_CanvasProject_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."CanvasProject"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAssetRelation" ADD CONSTRAINT "MediaAssetRelation_fromAssetId_MediaAsset_id_fk" FOREIGN KEY ("fromAssetId") REFERENCES "public"."MediaAsset"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAssetRelation" ADD CONSTRAINT "MediaAssetRelation_toAssetId_MediaAsset_id_fk" FOREIGN KEY ("toAssetId") REFERENCES "public"."MediaAsset"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MediaAssetRelation" ADD CONSTRAINT "MediaAssetRelation_sourceMessageId_Message_v2_id_fk" FOREIGN KEY ("sourceMessageId") REFERENCES "public"."Message_v2"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "MediaAsset_user_normalized_uidx"
|
||||||
|
ON "MediaAsset"("userId", "normalizedUrl");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "MediaAsset_chat_created_idx"
|
||||||
|
ON "MediaAsset"("chatId", "createdAt");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "MediaAsset_project_created_idx"
|
||||||
|
ON "MediaAsset"("projectId", "createdAt");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "MediaAsset_source_message_idx"
|
||||||
|
ON "MediaAsset"("sourceMessageId");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "MediaAssetRelation_project_type_idx"
|
||||||
|
ON "MediaAssetRelation"("projectId", "relationType");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "MediaAssetRelation_message_type_idx"
|
||||||
|
ON "MediaAssetRelation"("sourceMessageId", "relationType");
|
||||||
@ -260,6 +260,13 @@
|
|||||||
"when": 1772788800000,
|
"when": 1772788800000,
|
||||||
"tag": "0050_add_context_memory_tables",
|
"tag": "0050_add_context_memory_tables",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 37,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1772792400000,
|
||||||
|
"tag": "0051_add_media_assets",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,12 @@ import {
|
|||||||
type ConversationWorkingState,
|
type ConversationWorkingState,
|
||||||
messageTargetResolution,
|
messageTargetResolution,
|
||||||
type MessageTargetResolution,
|
type MessageTargetResolution,
|
||||||
|
mediaAsset,
|
||||||
|
type MediaAsset,
|
||||||
|
mediaAssetRelation,
|
||||||
|
type MediaAssetRelation,
|
||||||
|
canvasNode,
|
||||||
|
canvasProject,
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
import { generateHashedPassword } from "./utils";
|
import { generateHashedPassword } from "./utils";
|
||||||
import {
|
import {
|
||||||
@ -69,6 +75,7 @@ import {
|
|||||||
normalizePointsSystemConfig,
|
normalizePointsSystemConfig,
|
||||||
POINTS_SYSTEM_CONFIG_KEY,
|
POINTS_SYSTEM_CONFIG_KEY,
|
||||||
} from "../points/config";
|
} from "../points/config";
|
||||||
|
import { inferOriginalImageUrl } from "../image-url";
|
||||||
|
|
||||||
// Optionally, if not using email/pass login, you can
|
// Optionally, if not using email/pass login, you can
|
||||||
// use the Drizzle adapter for Auth.js / NextAuth
|
// use the Drizzle adapter for Auth.js / NextAuth
|
||||||
@ -682,6 +689,23 @@ function normalizeStringArray(value?: string[] | null): string[] {
|
|||||||
return Array.from(deduped);
|
return Array.from(deduped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 async function getConversationWorkingStateByChatId({
|
export async function getConversationWorkingStateByChatId({
|
||||||
chatId,
|
chatId,
|
||||||
}: {
|
}: {
|
||||||
@ -909,6 +933,316 @@ export async function createMessageTargetResolution(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMediaAssetByUserAndUrl(params: {
|
||||||
|
userId: string;
|
||||||
|
url: string;
|
||||||
|
}): Promise<MediaAsset | null> {
|
||||||
|
const normalizedUrl = normalizeMediaUrl(params.url);
|
||||||
|
if (!normalizedUrl) return null;
|
||||||
|
try {
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(mediaAsset)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(mediaAsset.userId, params.userId),
|
||||||
|
eq(mediaAsset.normalizedUrl, normalizedUrl),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
} catch (_error) {
|
||||||
|
throw new ChatSDKError(
|
||||||
|
"bad_request:database",
|
||||||
|
"Failed to get media asset by url"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMediaAssetsByUserAndUrls(params: {
|
||||||
|
userId: string;
|
||||||
|
urls: string[];
|
||||||
|
}): Promise<MediaAsset[]> {
|
||||||
|
const normalizedUrls = Array.from(
|
||||||
|
new Set(params.urls.map((url) => normalizeMediaUrl(url)).filter(Boolean)),
|
||||||
|
);
|
||||||
|
if (!normalizedUrls.length) return [];
|
||||||
|
try {
|
||||||
|
return await db
|
||||||
|
.select()
|
||||||
|
.from(mediaAsset)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(mediaAsset.userId, params.userId),
|
||||||
|
inArray(mediaAsset.normalizedUrl, normalizedUrls),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (_error) {
|
||||||
|
throw new ChatSDKError(
|
||||||
|
"bad_request:database",
|
||||||
|
"Failed to get media assets by urls"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsertMediaAsset(params: {
|
||||||
|
userId: string;
|
||||||
|
chatId?: string | null;
|
||||||
|
projectId?: string | null;
|
||||||
|
mediaType: string;
|
||||||
|
sourceType: string;
|
||||||
|
status?: string;
|
||||||
|
originalUrl: string;
|
||||||
|
previewUrl?: string | null;
|
||||||
|
thumbnailUrl?: string | null;
|
||||||
|
mimeType?: string | null;
|
||||||
|
width?: number | null;
|
||||||
|
height?: number | null;
|
||||||
|
durationMs?: number | null;
|
||||||
|
displayName?: string | null;
|
||||||
|
prompt?: string | null;
|
||||||
|
modelId?: string | null;
|
||||||
|
sourceMessageId?: string | null;
|
||||||
|
sourceTable?: string | null;
|
||||||
|
sourceRowId?: string | null;
|
||||||
|
sourceNodeId?: string | null;
|
||||||
|
rootAssetId?: string | null;
|
||||||
|
primaryParentAssetId?: string | null;
|
||||||
|
summary?: any;
|
||||||
|
metadata?: any;
|
||||||
|
}): Promise<MediaAsset | null> {
|
||||||
|
const originalUrl = String(params.originalUrl || "").trim();
|
||||||
|
const normalizedUrl = normalizeMediaUrl(originalUrl);
|
||||||
|
if (!originalUrl || !normalizedUrl) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const [result] = await db
|
||||||
|
.insert(mediaAsset)
|
||||||
|
.values({
|
||||||
|
userId: params.userId,
|
||||||
|
chatId: params.chatId ?? null,
|
||||||
|
projectId: params.projectId ?? null,
|
||||||
|
mediaType: params.mediaType,
|
||||||
|
sourceType: params.sourceType,
|
||||||
|
status: params.status || "ready",
|
||||||
|
originalUrl,
|
||||||
|
normalizedUrl,
|
||||||
|
previewUrl: params.previewUrl ?? null,
|
||||||
|
thumbnailUrl: params.thumbnailUrl ?? null,
|
||||||
|
mimeType: params.mimeType ?? null,
|
||||||
|
width: params.width ?? null,
|
||||||
|
height: params.height ?? null,
|
||||||
|
durationMs: params.durationMs ?? null,
|
||||||
|
displayName: params.displayName ?? null,
|
||||||
|
prompt: params.prompt ?? null,
|
||||||
|
modelId: params.modelId ?? null,
|
||||||
|
sourceMessageId: params.sourceMessageId ?? null,
|
||||||
|
sourceTable: params.sourceTable ?? null,
|
||||||
|
sourceRowId: params.sourceRowId ?? null,
|
||||||
|
sourceNodeId: params.sourceNodeId ?? null,
|
||||||
|
rootAssetId: params.rootAssetId ?? null,
|
||||||
|
primaryParentAssetId: params.primaryParentAssetId ?? null,
|
||||||
|
summary: params.summary ?? null,
|
||||||
|
metadata: params.metadata ?? null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [mediaAsset.userId, mediaAsset.normalizedUrl],
|
||||||
|
set: {
|
||||||
|
chatId: params.chatId ?? undefined,
|
||||||
|
projectId: params.projectId ?? undefined,
|
||||||
|
mediaType: params.mediaType,
|
||||||
|
sourceType: params.sourceType,
|
||||||
|
status: params.status || "ready",
|
||||||
|
originalUrl,
|
||||||
|
previewUrl: params.previewUrl ?? null,
|
||||||
|
thumbnailUrl: params.thumbnailUrl ?? null,
|
||||||
|
mimeType: params.mimeType ?? null,
|
||||||
|
width: params.width ?? null,
|
||||||
|
height: params.height ?? null,
|
||||||
|
durationMs: params.durationMs ?? null,
|
||||||
|
displayName: params.displayName ?? null,
|
||||||
|
prompt: params.prompt ?? null,
|
||||||
|
modelId: params.modelId ?? null,
|
||||||
|
sourceMessageId: params.sourceMessageId ?? null,
|
||||||
|
sourceTable: params.sourceTable ?? null,
|
||||||
|
sourceRowId: params.sourceRowId ?? null,
|
||||||
|
sourceNodeId: params.sourceNodeId ?? null,
|
||||||
|
rootAssetId: params.rootAssetId ?? null,
|
||||||
|
primaryParentAssetId: params.primaryParentAssetId ?? null,
|
||||||
|
summary: params.summary ?? null,
|
||||||
|
metadata: params.metadata ?? null,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return result ?? null;
|
||||||
|
} catch (_error) {
|
||||||
|
throw new ChatSDKError(
|
||||||
|
"bad_request:database",
|
||||||
|
"Failed to upsert media asset"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMediaAssetRelation(params: {
|
||||||
|
userId: string;
|
||||||
|
fromAssetId: string;
|
||||||
|
toAssetId: string;
|
||||||
|
relationType: string;
|
||||||
|
chatId?: string | null;
|
||||||
|
projectId?: string | null;
|
||||||
|
sourceMessageId?: string | null;
|
||||||
|
sourceNodeId?: string | null;
|
||||||
|
confidence?: number | null;
|
||||||
|
metadata?: any;
|
||||||
|
}): Promise<MediaAssetRelation | null> {
|
||||||
|
try {
|
||||||
|
const [result] = await db
|
||||||
|
.insert(mediaAssetRelation)
|
||||||
|
.values({
|
||||||
|
userId: params.userId,
|
||||||
|
chatId: params.chatId ?? null,
|
||||||
|
projectId: params.projectId ?? null,
|
||||||
|
fromAssetId: params.fromAssetId,
|
||||||
|
toAssetId: params.toAssetId,
|
||||||
|
relationType: params.relationType,
|
||||||
|
sourceMessageId: params.sourceMessageId ?? null,
|
||||||
|
sourceNodeId: params.sourceNodeId ?? null,
|
||||||
|
confidence: Math.max(0, Math.min(100, Math.round(params.confidence ?? 100))),
|
||||||
|
metadata: params.metadata ?? null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
return result ?? null;
|
||||||
|
} catch (_error) {
|
||||||
|
throw new ChatSDKError(
|
||||||
|
"bad_request:database",
|
||||||
|
"Failed to create media asset relation"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const UUID_REGEX =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
|
export async function syncCanvasNodesForProject(params: {
|
||||||
|
projectId: string;
|
||||||
|
userId: string;
|
||||||
|
canvasData: {
|
||||||
|
nodes?: any[];
|
||||||
|
edges?: any[];
|
||||||
|
viewport?: { x: number; y: number; zoom: number };
|
||||||
|
} | null | undefined;
|
||||||
|
}) {
|
||||||
|
const nodes = Array.isArray(params.canvasData?.nodes) ? params.canvasData?.nodes : [];
|
||||||
|
const assetUrls = Array.from(
|
||||||
|
new Set(
|
||||||
|
nodes
|
||||||
|
.map((node) => String(node?.data?.src || "").trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const matchedAssets = assetUrls.length
|
||||||
|
? await getMediaAssetsByUserAndUrls({
|
||||||
|
userId: params.userId,
|
||||||
|
urls: assetUrls,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const assetByUrl = new Map<string, MediaAsset>();
|
||||||
|
for (const asset of matchedAssets) {
|
||||||
|
assetByUrl.set(asset.normalizedUrl, asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedNodes = nodes.map((node) => {
|
||||||
|
const src = String(node?.data?.src || "").trim();
|
||||||
|
const normalizedUrl = normalizeMediaUrl(src);
|
||||||
|
const matchedAsset = normalizedUrl ? assetByUrl.get(normalizedUrl) : null;
|
||||||
|
const nextData =
|
||||||
|
matchedAsset && !node?.data?.assetId
|
||||||
|
? {
|
||||||
|
...(node?.data || {}),
|
||||||
|
assetId: matchedAsset.id,
|
||||||
|
}
|
||||||
|
: node?.data;
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
data: nextData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.delete(mediaAssetRelation).where(
|
||||||
|
and(
|
||||||
|
eq(mediaAssetRelation.projectId, params.projectId),
|
||||||
|
eq(mediaAssetRelation.relationType, "located_on_canvas"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await db.delete(canvasNode).where(eq(canvasNode.projectId, params.projectId));
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const insertableNodes = enrichedNodes.map((node) => {
|
||||||
|
const nodeId =
|
||||||
|
typeof node?.id === "string" && UUID_REGEX.test(node.id)
|
||||||
|
? node.id
|
||||||
|
: generateUUID();
|
||||||
|
return {
|
||||||
|
id: nodeId,
|
||||||
|
projectId: params.projectId,
|
||||||
|
type: String(node?.type || "text").slice(0, 32),
|
||||||
|
position: node?.position || null,
|
||||||
|
size: node?.size || null,
|
||||||
|
data: node?.data || null,
|
||||||
|
connections: Array.isArray(node?.connections)
|
||||||
|
? node.connections.filter((value: unknown) => typeof value === "string")
|
||||||
|
: [],
|
||||||
|
parentId:
|
||||||
|
typeof node?.parentId === "string" && UUID_REGEX.test(node.parentId)
|
||||||
|
? node.parentId
|
||||||
|
: null,
|
||||||
|
createdAt:
|
||||||
|
node?.createdAt instanceof Date
|
||||||
|
? node.createdAt
|
||||||
|
: typeof node?.createdAt === "string"
|
||||||
|
? new Date(node.createdAt)
|
||||||
|
: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (insertableNodes.length > 0) {
|
||||||
|
await db.insert(canvasNode).values(insertableNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of enrichedNodes) {
|
||||||
|
const assetId =
|
||||||
|
typeof node?.data?.assetId === "string" && node.data.assetId.trim()
|
||||||
|
? node.data.assetId.trim()
|
||||||
|
: null;
|
||||||
|
if (!assetId) continue;
|
||||||
|
await createMediaAssetRelation({
|
||||||
|
userId: params.userId,
|
||||||
|
fromAssetId: assetId,
|
||||||
|
toAssetId: assetId,
|
||||||
|
relationType: "located_on_canvas",
|
||||||
|
projectId: params.projectId,
|
||||||
|
sourceNodeId: typeof node.id === "string" ? node.id : null,
|
||||||
|
metadata: {
|
||||||
|
nodeType: node.type,
|
||||||
|
position: node.position || null,
|
||||||
|
size: node.size || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...params.canvasData,
|
||||||
|
nodes: enrichedNodes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMessageCountByUserId({
|
export async function getMessageCountByUserId({
|
||||||
id,
|
id,
|
||||||
differenceInHours,
|
differenceInHours,
|
||||||
|
|||||||
@ -742,3 +742,71 @@ export const messageTargetResolution = pgTable("MessageTargetResolution", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type MessageTargetResolution = InferSelectModel<typeof messageTargetResolution>;
|
export type MessageTargetResolution = InferSelectModel<typeof messageTargetResolution>;
|
||||||
|
|
||||||
|
// 统一媒体资产表(上传图 / 生成图 / 视频 / 3D 预览的统一索引)
|
||||||
|
export const mediaAsset = pgTable("MediaAsset", {
|
||||||
|
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||||
|
userId: uuid("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||||||
|
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
mediaType: varchar("mediaType", { length: 32 }).notNull(),
|
||||||
|
sourceType: varchar("sourceType", { length: 32 }).notNull(),
|
||||||
|
status: varchar("status", { length: 32 }).notNull().default("ready"),
|
||||||
|
originalUrl: text("originalUrl").notNull(),
|
||||||
|
normalizedUrl: text("normalizedUrl").notNull(),
|
||||||
|
previewUrl: text("previewUrl"),
|
||||||
|
thumbnailUrl: text("thumbnailUrl"),
|
||||||
|
mimeType: varchar("mimeType", { length: 128 }),
|
||||||
|
width: integer("width"),
|
||||||
|
height: integer("height"),
|
||||||
|
durationMs: integer("durationMs"),
|
||||||
|
displayName: varchar("displayName", { length: 256 }),
|
||||||
|
prompt: text("prompt"),
|
||||||
|
modelId: varchar("modelId", { length: 128 }),
|
||||||
|
sourceMessageId: uuid("sourceMessageId").references(() => message.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
sourceTable: varchar("sourceTable", { length: 64 }),
|
||||||
|
sourceRowId: varchar("sourceRowId", { length: 128 }),
|
||||||
|
sourceNodeId: varchar("sourceNodeId", { length: 128 }),
|
||||||
|
rootAssetId: uuid("rootAssetId"),
|
||||||
|
primaryParentAssetId: uuid("primaryParentAssetId"),
|
||||||
|
summary: jsonb("summary"),
|
||||||
|
metadata: jsonb("metadata"),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type MediaAsset = InferSelectModel<typeof mediaAsset>;
|
||||||
|
|
||||||
|
// 媒体资产关系表(版本链 / 画布节点绑定 / 来源关系)
|
||||||
|
export const mediaAssetRelation = pgTable("MediaAssetRelation", {
|
||||||
|
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||||
|
userId: uuid("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||||||
|
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
fromAssetId: uuid("fromAssetId").notNull().references(() => mediaAsset.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
toAssetId: uuid("toAssetId").notNull().references(() => mediaAsset.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
relationType: varchar("relationType", { length: 64 }).notNull(),
|
||||||
|
sourceMessageId: uuid("sourceMessageId").references(() => message.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
sourceNodeId: varchar("sourceNodeId", { length: 128 }),
|
||||||
|
confidence: integer("confidence").notNull().default(100),
|
||||||
|
metadata: jsonb("metadata"),
|
||||||
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type MediaAssetRelation = InferSelectModel<typeof mediaAssetRelation>;
|
||||||
|
|||||||
@ -55,4 +55,5 @@ export type Attachment = {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
|
assetId?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -137,7 +137,7 @@ export const canvasService = {
|
|||||||
// 图片生成服务
|
// 图片生成服务
|
||||||
export const imageGenerationService = {
|
export const imageGenerationService = {
|
||||||
// 上传图片到存储桶
|
// 上传图片到存储桶
|
||||||
async uploadImage(file: File): Promise<ApiResponse<{ url: string; pathname: string }>> {
|
async uploadImage(file: File): Promise<ApiResponse<{ url: string; pathname: string; assetId?: string }>> {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
@ -157,7 +157,8 @@ export const imageGenerationService = {
|
|||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
url: data.url,
|
url: data.url,
|
||||||
pathname: data.pathname
|
pathname: data.pathname,
|
||||||
|
assetId: typeof data.assetId === "string" ? data.assetId : undefined,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user