feat: cache asset visual memory summaries
This commit is contained in:
parent
17567df76b
commit
c1fea459cd
@ -1049,6 +1049,21 @@ function extractMessageImageUrls(parts: any[]): string[] {
|
||||
.filter((url: any): url is string => typeof url === "string" && /^https?:\/\//i.test(url));
|
||||
}
|
||||
|
||||
function extractMessageImageAssetIds(parts: any[]): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(parts || [])
|
||||
.map((part: any) => {
|
||||
if (!part || typeof part !== "object") return null;
|
||||
return typeof part.assetId === "string" && part.assetId.trim()
|
||||
? part.assetId.trim()
|
||||
: null;
|
||||
})
|
||||
.filter((assetId: any): assetId is string => typeof assetId === "string" && assetId.length > 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function shouldEnrichImageAnalysisText(message?: string): boolean {
|
||||
if (!message) return false;
|
||||
if (!/(###\s*图片分析|【\s*图片分析\s*】)/.test(message)) return false;
|
||||
@ -2242,6 +2257,12 @@ export async function POST(request: Request) {
|
||||
text: requestTextContent,
|
||||
hasImage,
|
||||
imageUrls,
|
||||
assetIds: Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
),
|
||||
imageFromHistory: smartAutoHistoryImages.length > 0,
|
||||
historyMessages,
|
||||
context: autoContextText || undefined,
|
||||
@ -2425,6 +2446,12 @@ export async function POST(request: Request) {
|
||||
rescueHasImage &&
|
||||
autoCurrentImageUrlsForFallback.length === 0 &&
|
||||
smartAutoHistoryImages.length > 0;
|
||||
const rescueAssetIds = Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
const rescueTimeoutMs = getBudgetedTimeout({
|
||||
startedAt: requestStartedAt,
|
||||
preferredMs: SMART_AUTO_FALLBACK_TIMEOUT_MS,
|
||||
@ -2436,6 +2463,7 @@ export async function POST(request: Request) {
|
||||
text: autoTextForFallback,
|
||||
hasImage: rescueHasImage,
|
||||
imageUrls: rescueImageUrls,
|
||||
assetIds: rescueAssetIds,
|
||||
imageFromHistory: rescueFromHistory || autoFromHistoryForFallback,
|
||||
historyMessages: [],
|
||||
context: undefined,
|
||||
@ -2486,6 +2514,12 @@ export async function POST(request: Request) {
|
||||
|
||||
if (summaryImageUrls.length > 0) {
|
||||
const { summarizeImageForExecutionReport } = await import("@/lib/ai/smart-analyzer");
|
||||
const summaryAssetIds = Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
const fallbackSummaryTimeoutMs = getBudgetedTimeout({
|
||||
startedAt: requestStartedAt,
|
||||
preferredMs: SMART_AUTO_IMAGE_SUMMARY_TIMEOUT_MS,
|
||||
@ -2498,6 +2532,7 @@ export async function POST(request: Request) {
|
||||
imageUrls: summaryImageUrls,
|
||||
context: typeof autoContext === "string" ? autoContext : undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
assetIds: summaryAssetIds,
|
||||
}),
|
||||
"fallbackVisualSummary",
|
||||
fallbackSummaryTimeoutMs,
|
||||
@ -2724,6 +2759,12 @@ export async function POST(request: Request) {
|
||||
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join(" ");
|
||||
const summaryAssetIds = Array.from(
|
||||
new Set([
|
||||
...extractMessageImageAssetIds(message.parts as any[]),
|
||||
...((contextPayload?.selectedAssetIds || []).filter(Boolean) as string[]),
|
||||
]),
|
||||
);
|
||||
|
||||
let summary: string | null = null;
|
||||
const summaryTimeoutMs = getBudgetedTimeout({
|
||||
@ -2738,6 +2779,7 @@ export async function POST(request: Request) {
|
||||
imageUrls: summaryImageUrls,
|
||||
context: typeof autoContext === "string" ? autoContext : undefined,
|
||||
preferredModelId: preferredAgentModelId,
|
||||
assetIds: summaryAssetIds,
|
||||
}),
|
||||
"enrichImageAnalysisSummary",
|
||||
summaryTimeoutMs,
|
||||
|
||||
@ -51,6 +51,7 @@ export interface AutoAgentInput {
|
||||
text: string;
|
||||
hasImage: boolean;
|
||||
imageUrls?: string[];
|
||||
assetIds?: string[];
|
||||
imageFromHistory?: boolean;
|
||||
historyMessages?: HistoryMessage[];
|
||||
context?: string;
|
||||
@ -70,6 +71,7 @@ const AutoAgentState = Annotation.Root({
|
||||
text: Annotation<string>,
|
||||
hasImage: Annotation<boolean>,
|
||||
imageUrls: Annotation<string[]>,
|
||||
assetIds: Annotation<string[]>,
|
||||
imageFromHistory: Annotation<boolean>,
|
||||
historyMessages: Annotation<HistoryMessage[]>,
|
||||
context: Annotation<string | null>,
|
||||
@ -1182,6 +1184,7 @@ const analyzeNode = async (state: AutoAgentStateType) => {
|
||||
text: state.text,
|
||||
hasImage: state.hasImage,
|
||||
imageUrls: state.imageUrls,
|
||||
assetIds: state.assetIds,
|
||||
imageFromHistory: state.imageFromHistory,
|
||||
historyMessages: state.historyMessages,
|
||||
context: state.context ?? undefined,
|
||||
@ -1332,6 +1335,7 @@ export async function runAutoAgentGraph(params: AutoAgentInput): Promise<AutoAge
|
||||
text: params.text,
|
||||
hasImage: params.hasImage,
|
||||
imageUrls: params.imageUrls ?? [],
|
||||
assetIds: params.assetIds ?? [],
|
||||
imageFromHistory: params.imageFromHistory ?? false,
|
||||
historyMessages: params.historyMessages ?? [],
|
||||
context: params.context ?? null,
|
||||
|
||||
@ -447,6 +447,7 @@ async function analyzeWithHardcodedQwen3VlFallback(params: {
|
||||
text: string;
|
||||
hasImage: boolean;
|
||||
imageUrls?: string[];
|
||||
assetIds?: string[];
|
||||
visionImages?: Array<{ type: "image"; image: string }>;
|
||||
historyMessages?: HistoryMessage[];
|
||||
context?: string;
|
||||
@ -905,16 +906,155 @@ async function summarizeWithZhipuOfficialApi(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function uniqueNonEmptyStrings(values?: string[] | null): string[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(values || [])
|
||||
.map((value) => String(value || "").trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function extractVisualSummarySection(summary: string, label: string): string {
|
||||
const normalized = summary.replace(/\s+/g, " ").trim();
|
||||
if (!normalized) return "";
|
||||
const match = normalized.match(new RegExp(`${label}\\s*[::]\\s*([^;;]+)`, "i"));
|
||||
return match?.[1]?.trim() || "";
|
||||
}
|
||||
|
||||
function tokenizeVisualSummarySection(section: string, maxTags = 6): string[] {
|
||||
const normalized = section
|
||||
.replace(/[()()]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (!normalized) return [];
|
||||
|
||||
const parts = normalized
|
||||
.split(/[、,,/|]/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return normalized ? [normalized.slice(0, 80)] : [];
|
||||
}
|
||||
|
||||
return Array.from(new Set(parts)).slice(0, maxTags);
|
||||
}
|
||||
|
||||
function buildVisualSummaryMemoryPayload(summary: string) {
|
||||
return {
|
||||
subjectTags: tokenizeVisualSummarySection(extractVisualSummarySection(summary, "画面")),
|
||||
sceneTags: tokenizeVisualSummarySection(extractVisualSummarySection(summary, "场景")),
|
||||
styleTags: tokenizeVisualSummarySection(extractVisualSummarySection(summary, "风格")),
|
||||
lightingTags: tokenizeVisualSummarySection(extractVisualSummarySection(summary, "光线")),
|
||||
};
|
||||
}
|
||||
|
||||
async function getCachedVisualSummaryByAssetIds(assetIds?: string[]): Promise<string | null> {
|
||||
const uniqueAssetIds = uniqueNonEmptyStrings(assetIds);
|
||||
if (uniqueAssetIds.length !== 1) return null;
|
||||
|
||||
try {
|
||||
const { getAssetMemorySnapshotsByAssetIds } = await import("@/lib/db/queries");
|
||||
const snapshots = await getAssetMemorySnapshotsByAssetIds({
|
||||
assetIds: uniqueAssetIds,
|
||||
snapshotType: "visual_summary",
|
||||
status: "ready",
|
||||
});
|
||||
const latest = snapshots.find(
|
||||
(snapshot) =>
|
||||
typeof snapshot.summaryText === "string" &&
|
||||
snapshot.summaryText.trim().length > 0,
|
||||
);
|
||||
const normalized = normalizeVisualSummaryText(latest?.summaryText);
|
||||
if (normalized && !isWeakVisualSummary(normalized)) {
|
||||
console.log("[Smart Analyzer] Asset memory cache hit:", uniqueAssetIds[0]);
|
||||
return normalized;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Smart Analyzer] Failed to read asset memory snapshot cache:", error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function persistVisualSummarySnapshot(params: {
|
||||
assetIds?: string[];
|
||||
summary: string;
|
||||
preferredModelId?: string;
|
||||
text?: string;
|
||||
context?: string;
|
||||
imageUrls?: string[];
|
||||
}) {
|
||||
const uniqueAssetIds = uniqueNonEmptyStrings(params.assetIds);
|
||||
if (uniqueAssetIds.length !== 1) return;
|
||||
|
||||
try {
|
||||
const { getMediaAssetsByIds, upsertAssetMemorySnapshot } = await import("@/lib/db/queries");
|
||||
const assets = await getMediaAssetsByIds({ ids: uniqueAssetIds });
|
||||
const asset = assets.find((item) => item.id === uniqueAssetIds[0]);
|
||||
if (!asset?.userId || !asset.normalizedUrl) return;
|
||||
|
||||
const normalizedSummary = normalizeVisualSummaryText(params.summary);
|
||||
if (!normalizedSummary || isWeakVisualSummary(normalizedSummary)) return;
|
||||
|
||||
const memoryPayload = buildVisualSummaryMemoryPayload(normalizedSummary);
|
||||
await upsertAssetMemorySnapshot({
|
||||
userId: asset.userId,
|
||||
assetId: asset.id,
|
||||
chatId: asset.chatId ?? null,
|
||||
projectId: asset.projectId ?? null,
|
||||
snapshotType: "visual_summary",
|
||||
status: "ready",
|
||||
normalizedUrl: asset.normalizedUrl,
|
||||
summaryText: normalizedSummary,
|
||||
subjectTags: memoryPayload.subjectTags,
|
||||
sceneTags: memoryPayload.sceneTags,
|
||||
styleTags: memoryPayload.styleTags,
|
||||
lightingTags: memoryPayload.lightingTags,
|
||||
confidence: 88,
|
||||
sourceModelId: params.preferredModelId || asset.modelId || null,
|
||||
metadata: {
|
||||
generator: "summarizeImageForExecutionReport",
|
||||
promptText: params.text || null,
|
||||
context: params.context || null,
|
||||
imageUrls: uniqueNonEmptyStrings(params.imageUrls),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[Smart Analyzer] Failed to persist asset memory snapshot:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function summarizeImageForExecutionReport(params: {
|
||||
text?: string;
|
||||
imageUrls?: string[];
|
||||
context?: string;
|
||||
preferredModelId?: string;
|
||||
assetIds?: string[];
|
||||
}): Promise<string | null> {
|
||||
const { text, imageUrls, context, preferredModelId } = params;
|
||||
const { text, imageUrls, context, preferredModelId, assetIds } = params;
|
||||
const uniqueAssetIds = uniqueNonEmptyStrings(assetIds);
|
||||
const cachedSummary = await getCachedVisualSummaryByAssetIds(uniqueAssetIds);
|
||||
if (cachedSummary) return cachedSummary;
|
||||
const visionImages = await buildAnalyzerVisionImages(imageUrls);
|
||||
if (visionImages.length === 0) return null;
|
||||
|
||||
const finalizeVisualSummary = async (summary?: string | null): Promise<string | null> => {
|
||||
const normalized = normalizeVisualSummaryText(summary);
|
||||
if (!normalized || isWeakVisualSummary(normalized)) return null;
|
||||
await persistVisualSummarySnapshot({
|
||||
assetIds: uniqueAssetIds,
|
||||
summary: normalized,
|
||||
preferredModelId,
|
||||
text,
|
||||
context,
|
||||
imageUrls,
|
||||
});
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const {
|
||||
getEnabledAiModelsWithProvider,
|
||||
getAgentAiModelWithProvider,
|
||||
@ -977,7 +1117,7 @@ export async function summarizeImageForExecutionReport(params: {
|
||||
imageUrls,
|
||||
visionImages,
|
||||
});
|
||||
if (forcedSummary) return forcedSummary;
|
||||
if (forcedSummary) return await finalizeVisualSummary(forcedSummary);
|
||||
}
|
||||
|
||||
if (zhipuProvider?.apiKey && zhipuProvider?.baseUrl) {
|
||||
@ -987,7 +1127,7 @@ export async function summarizeImageForExecutionReport(params: {
|
||||
promptText,
|
||||
visionImages,
|
||||
});
|
||||
if (summary) return summary;
|
||||
if (summary) return await finalizeVisualSummary(summary);
|
||||
}
|
||||
|
||||
if (volcanoProvider?.apiKey && volcanoProvider?.baseUrl) {
|
||||
@ -997,7 +1137,7 @@ export async function summarizeImageForExecutionReport(params: {
|
||||
promptText,
|
||||
imageUrls: (imageUrls || []).filter((url) => isHttpUrl(url)).slice(0, ANALYZER_MAX_IMAGE_COUNT),
|
||||
});
|
||||
if (summary) return summary;
|
||||
if (summary) return await finalizeVisualSummary(summary);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -1046,7 +1186,7 @@ export async function summarizeImageForExecutionReport(params: {
|
||||
console.log(
|
||||
`[Smart Analyzer] Visual summary model used: ${candidate.model.displayName} (${candidate.provider.type}), preferRemote=${preferRemoteUrls}`,
|
||||
);
|
||||
return normalized;
|
||||
return await finalizeVisualSummary(normalized);
|
||||
}
|
||||
if (normalized) {
|
||||
console.warn(
|
||||
@ -1072,7 +1212,7 @@ export async function summarizeImageForExecutionReport(params: {
|
||||
imageUrls,
|
||||
visionImages,
|
||||
});
|
||||
if (qwenFallbackSummary) return qwenFallbackSummary;
|
||||
if (qwenFallbackSummary) return await finalizeVisualSummary(qwenFallbackSummary);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -1264,12 +1404,13 @@ export async function analyzeIntent(params: {
|
||||
text: string;
|
||||
hasImage: boolean;
|
||||
imageUrls?: string[];
|
||||
assetIds?: string[];
|
||||
imageFromHistory?: boolean; // 🆕 图片是否来自历史记录
|
||||
historyMessages?: HistoryMessage[]; // 🆕 历史对话消息(用于上下文理解)
|
||||
context?: string; // 🆕 画布上下文(仅用于理解)
|
||||
preferredModelId?: string;
|
||||
}): Promise<AnalysisResult> {
|
||||
const { text, hasImage, imageUrls, imageFromHistory, historyMessages, context, preferredModelId } = params;
|
||||
const { text, hasImage, imageUrls, assetIds, imageFromHistory, historyMessages, context, preferredModelId } = params;
|
||||
|
||||
console.log("[Smart Analyzer] Analyzing intent:", {
|
||||
text,
|
||||
@ -1292,6 +1433,7 @@ export async function analyzeIntent(params: {
|
||||
text,
|
||||
hasImage,
|
||||
imageUrls,
|
||||
assetIds,
|
||||
historyMessages,
|
||||
context,
|
||||
preferredModelId,
|
||||
@ -1330,6 +1472,7 @@ async function analyzWithLLM(
|
||||
text: string,
|
||||
hasImage: boolean,
|
||||
imageUrls?: string[],
|
||||
assetIds?: string[],
|
||||
historyMessages?: HistoryMessage[],
|
||||
context?: string,
|
||||
preferredModelId?: string
|
||||
@ -1500,6 +1643,7 @@ async function analyzWithLLM(
|
||||
text,
|
||||
hasImage,
|
||||
imageUrls,
|
||||
assetIds,
|
||||
visionImages,
|
||||
historyMessages,
|
||||
context,
|
||||
@ -1509,11 +1653,17 @@ async function analyzWithLLM(
|
||||
|
||||
// 🆕 构建包含历史上下文的消息
|
||||
let contextMessage = "";
|
||||
const cachedVisualSummary = hasImage
|
||||
? await getCachedVisualSummaryByAssetIds(assetIds)
|
||||
: null;
|
||||
|
||||
// 🆕 画布上下文(仅用于理解,不要复述)
|
||||
if (context && context.trim().length > 0) {
|
||||
contextMessage = `## 画布上下文(仅用于理解,不要在对话回复中复述):\n${context.trim()}\n\n`;
|
||||
}
|
||||
if (cachedVisualSummary) {
|
||||
contextMessage += `## 已缓存图片摘要(可直接作为视觉上下文):\n${cachedVisualSummary}\n\n`;
|
||||
}
|
||||
|
||||
// 添加历史对话上下文(最多取最近5轮对话)
|
||||
if (historyMessages && historyMessages.length > 0) {
|
||||
@ -1550,6 +1700,7 @@ ${historyText}
|
||||
const forceSingleMultimodalAgent = Boolean(hasImage && multimodalAgentVisionModel);
|
||||
|
||||
if (
|
||||
!cachedVisualSummary &&
|
||||
!forceSingleMultimodalAgent &&
|
||||
hasImage &&
|
||||
visionImages.length > 0 &&
|
||||
@ -1579,6 +1730,7 @@ ${historyText}
|
||||
|
||||
// 调用模型分析(有图时携带图片,确保真实视觉分析)
|
||||
const requestPayload = hasImage && visionImages.length > 0
|
||||
&& !cachedVisualSummary
|
||||
? {
|
||||
model,
|
||||
system: ANALYZER_SYSTEM_PROMPT,
|
||||
@ -1615,6 +1767,7 @@ ${historyText}
|
||||
text,
|
||||
hasImage,
|
||||
imageUrls,
|
||||
assetIds,
|
||||
visionImages,
|
||||
historyMessages,
|
||||
context,
|
||||
@ -1633,6 +1786,7 @@ ${historyText}
|
||||
text,
|
||||
hasImage,
|
||||
imageUrls,
|
||||
assetIds,
|
||||
visionImages,
|
||||
historyMessages,
|
||||
context,
|
||||
|
||||
56
lib/db/migrations/0052_add_asset_memory_snapshots.sql
Normal file
56
lib/db/migrations/0052_add_asset_memory_snapshots.sql
Normal file
@ -0,0 +1,56 @@
|
||||
CREATE TABLE IF NOT EXISTS "AssetMemorySnapshot" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"assetId" uuid,
|
||||
"chatId" uuid,
|
||||
"projectId" uuid,
|
||||
"snapshotType" varchar(32) DEFAULT 'visual_summary' NOT NULL,
|
||||
"status" varchar(32) DEFAULT 'ready' NOT NULL,
|
||||
"normalizedUrl" text NOT NULL,
|
||||
"summaryText" text,
|
||||
"subjectTags" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"sceneTags" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"styleTags" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"lightingTags" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"confidence" integer DEFAULT 0 NOT NULL,
|
||||
"sourceModelId" varchar(128),
|
||||
"metadata" jsonb,
|
||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
||||
"updatedAt" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "AssetMemorySnapshot" ADD CONSTRAINT "AssetMemorySnapshot_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 "AssetMemorySnapshot" ADD CONSTRAINT "AssetMemorySnapshot_assetId_MediaAsset_id_fk" FOREIGN KEY ("assetId") REFERENCES "public"."MediaAsset"("id") ON DELETE cascade ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "AssetMemorySnapshot" ADD CONSTRAINT "AssetMemorySnapshot_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 "AssetMemorySnapshot" ADD CONSTRAINT "AssetMemorySnapshot_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
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "AssetMemorySnapshot_user_url_type_uidx"
|
||||
ON "AssetMemorySnapshot"("userId", "normalizedUrl", "snapshotType");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "AssetMemorySnapshot_asset_type_idx"
|
||||
ON "AssetMemorySnapshot"("assetId", "snapshotType");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "AssetMemorySnapshot_chat_created_idx"
|
||||
ON "AssetMemorySnapshot"("chatId", "createdAt");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "AssetMemorySnapshot_project_created_idx"
|
||||
ON "AssetMemorySnapshot"("projectId", "createdAt");
|
||||
@ -267,6 +267,13 @@
|
||||
"when": 1772792400000,
|
||||
"tag": "0051_add_media_assets",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 38,
|
||||
"version": "7",
|
||||
"when": 1772796000000,
|
||||
"tag": "0052_add_asset_memory_snapshots",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -60,6 +60,8 @@ import {
|
||||
type MediaAsset,
|
||||
mediaAssetRelation,
|
||||
type MediaAssetRelation,
|
||||
assetMemorySnapshot,
|
||||
type AssetMemorySnapshot,
|
||||
canvasNode,
|
||||
canvasProject,
|
||||
} from "./schema";
|
||||
@ -985,6 +987,28 @@ export async function getMediaAssetsByUserAndUrls(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMediaAssetsByIds(params: {
|
||||
ids: string[];
|
||||
}): Promise<MediaAsset[]> {
|
||||
const ids = Array.from(
|
||||
new Set(
|
||||
(params.ids || [])
|
||||
.map((id) => String(id || "").trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
try {
|
||||
return await db.select().from(mediaAsset).where(inArray(mediaAsset.id, ids));
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to get media assets by ids"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertMediaAsset(params: {
|
||||
userId: string;
|
||||
chatId?: string | null;
|
||||
@ -1126,6 +1150,160 @@ export async function createMediaAssetRelation(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAssetMemorySnapshotsByAssetIds(params: {
|
||||
assetIds: string[];
|
||||
snapshotType?: string | null;
|
||||
status?: string | null;
|
||||
}): Promise<AssetMemorySnapshot[]> {
|
||||
const assetIds = Array.from(
|
||||
new Set(
|
||||
(params.assetIds || [])
|
||||
.map((id) => String(id || "").trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
if (assetIds.length === 0) return [];
|
||||
|
||||
try {
|
||||
const conditions = [inArray(assetMemorySnapshot.assetId, assetIds)];
|
||||
if (params.snapshotType) {
|
||||
conditions.push(eq(assetMemorySnapshot.snapshotType, params.snapshotType));
|
||||
}
|
||||
if (params.status) {
|
||||
conditions.push(eq(assetMemorySnapshot.status, params.status));
|
||||
}
|
||||
|
||||
return await db
|
||||
.select()
|
||||
.from(assetMemorySnapshot)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(assetMemorySnapshot.updatedAt));
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to get asset memory snapshots by asset ids"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAssetMemorySnapshotsByUserAndUrls(params: {
|
||||
userId: string;
|
||||
urls: string[];
|
||||
snapshotType?: string | null;
|
||||
status?: string | null;
|
||||
}): Promise<AssetMemorySnapshot[]> {
|
||||
const normalizedUrls = Array.from(
|
||||
new Set(
|
||||
(params.urls || [])
|
||||
.map((url) => normalizeMediaUrl(String(url || "").trim()))
|
||||
.filter(Boolean),
|
||||
),
|
||||
);
|
||||
if (!params.userId || normalizedUrls.length === 0) return [];
|
||||
|
||||
try {
|
||||
const conditions = [
|
||||
eq(assetMemorySnapshot.userId, params.userId),
|
||||
inArray(assetMemorySnapshot.normalizedUrl, normalizedUrls),
|
||||
];
|
||||
if (params.snapshotType) {
|
||||
conditions.push(eq(assetMemorySnapshot.snapshotType, params.snapshotType));
|
||||
}
|
||||
if (params.status) {
|
||||
conditions.push(eq(assetMemorySnapshot.status, params.status));
|
||||
}
|
||||
|
||||
return await db
|
||||
.select()
|
||||
.from(assetMemorySnapshot)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(assetMemorySnapshot.updatedAt));
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to get asset memory snapshots by urls"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertAssetMemorySnapshot(params: {
|
||||
userId: string;
|
||||
assetId?: string | null;
|
||||
chatId?: string | null;
|
||||
projectId?: string | null;
|
||||
snapshotType?: string | null;
|
||||
status?: string | null;
|
||||
normalizedUrl: string;
|
||||
summaryText?: string | null;
|
||||
subjectTags?: string[] | null;
|
||||
sceneTags?: string[] | null;
|
||||
styleTags?: string[] | null;
|
||||
lightingTags?: string[] | null;
|
||||
confidence?: number | null;
|
||||
sourceModelId?: string | null;
|
||||
metadata?: any;
|
||||
}): Promise<AssetMemorySnapshot | null> {
|
||||
const normalizedUrl = normalizeMediaUrl(String(params.normalizedUrl || "").trim());
|
||||
if (!params.userId || !normalizedUrl) return null;
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
const snapshotType = String(params.snapshotType || "visual_summary").trim() || "visual_summary";
|
||||
const status = String(params.status || "ready").trim() || "ready";
|
||||
const [result] = await db
|
||||
.insert(assetMemorySnapshot)
|
||||
.values({
|
||||
userId: params.userId,
|
||||
assetId: params.assetId ?? null,
|
||||
chatId: params.chatId ?? null,
|
||||
projectId: params.projectId ?? null,
|
||||
snapshotType,
|
||||
status,
|
||||
normalizedUrl,
|
||||
summaryText: params.summaryText ?? null,
|
||||
subjectTags: normalizeStringArray(params.subjectTags),
|
||||
sceneTags: normalizeStringArray(params.sceneTags),
|
||||
styleTags: normalizeStringArray(params.styleTags),
|
||||
lightingTags: normalizeStringArray(params.lightingTags),
|
||||
confidence: Math.max(0, Math.min(100, Math.round(params.confidence ?? 0))),
|
||||
sourceModelId: params.sourceModelId ?? null,
|
||||
metadata: params.metadata ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
assetMemorySnapshot.userId,
|
||||
assetMemorySnapshot.normalizedUrl,
|
||||
assetMemorySnapshot.snapshotType,
|
||||
],
|
||||
set: {
|
||||
assetId: params.assetId ?? undefined,
|
||||
chatId: params.chatId ?? undefined,
|
||||
projectId: params.projectId ?? undefined,
|
||||
status,
|
||||
summaryText: params.summaryText ?? null,
|
||||
subjectTags: normalizeStringArray(params.subjectTags),
|
||||
sceneTags: normalizeStringArray(params.sceneTags),
|
||||
styleTags: normalizeStringArray(params.styleTags),
|
||||
lightingTags: normalizeStringArray(params.lightingTags),
|
||||
confidence: Math.max(0, Math.min(100, Math.round(params.confidence ?? 0))),
|
||||
sourceModelId: params.sourceModelId ?? null,
|
||||
metadata: params.metadata ?? null,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result ?? null;
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to upsert asset memory snapshot"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const UUID_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
|
||||
@ -810,3 +810,35 @@ export const mediaAssetRelation = pgTable("MediaAssetRelation", {
|
||||
});
|
||||
|
||||
export type MediaAssetRelation = InferSelectModel<typeof mediaAssetRelation>;
|
||||
|
||||
// 资产记忆快照表(视觉摘要 / 主体标签 / 场景标签等可复用缓存)
|
||||
export const assetMemorySnapshot = pgTable("AssetMemorySnapshot", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
assetId: uuid("assetId").references(() => mediaAsset.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
snapshotType: varchar("snapshotType", { length: 32 })
|
||||
.notNull()
|
||||
.default("visual_summary"),
|
||||
status: varchar("status", { length: 32 }).notNull().default("ready"),
|
||||
normalizedUrl: text("normalizedUrl").notNull(),
|
||||
summaryText: text("summaryText"),
|
||||
subjectTags: jsonb("subjectTags").$type<string[]>().notNull().default([]),
|
||||
sceneTags: jsonb("sceneTags").$type<string[]>().notNull().default([]),
|
||||
styleTags: jsonb("styleTags").$type<string[]>().notNull().default([]),
|
||||
lightingTags: jsonb("lightingTags").$type<string[]>().notNull().default([]),
|
||||
confidence: integer("confidence").notNull().default(0),
|
||||
sourceModelId: varchar("sourceModelId", { length: 128 }),
|
||||
metadata: jsonb("metadata"),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type AssetMemorySnapshot = InferSelectModel<typeof assetMemorySnapshot>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user