ai-chatbot/lib/ai/image-workflow-memory.ts
Your Name e7812060b8
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Update conversation memory and canvas persistence
2026-03-13 12:31:13 +08:00

160 lines
5.6 KiB
TypeScript

import "server-only";
import type { AgentThreadState } from "@/lib/ai/runtime/agent-thread-state";
export interface ImageWorkflowMemory {
lastIntent?: "image_gen" | "image_edit" | "vision";
lastOptimizedPrompt?: string;
lastUserPrompt?: string;
lastRequestedImageCount?: number;
lastAspectRatio?: string;
lastImageSize?: string;
lastSourceImageUrls?: string[];
lastResultImageUrls?: string[];
}
function normalizeText(value: unknown, max = 2000): string | undefined {
const normalized = String(value || "").trim();
if (!normalized) return undefined;
return normalized.slice(0, max);
}
function normalizeImageUrls(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined;
const urls = Array.from(
new Set(
value
.map((item) => String(item || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
)
);
return urls.length > 0 ? urls : undefined;
}
function normalizeImageCount(value: unknown): number | undefined {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return undefined;
const rounded = Math.round(parsed);
if (rounded < 1 || rounded > 12) return undefined;
return rounded;
}
export function extractImageWorkflowMemory(
workingState?: {
lastTurnSummary?: unknown;
lastResolvedTargetImageUrls?: string[] | null;
lastGeneratedImageUrls?: string[] | null;
} | null,
): ImageWorkflowMemory | null {
const summary = workingState?.lastTurnSummary;
let memory: ImageWorkflowMemory | null = null;
if (summary && typeof summary === "object") {
const summaryRecord = summary as Record<string, unknown>;
const imageWorkflowMemory = summaryRecord.imageWorkflowMemory;
if (imageWorkflowMemory && typeof imageWorkflowMemory === "object") {
const record = imageWorkflowMemory as Record<string, unknown>;
memory = {
lastIntent:
record.lastIntent === "image_gen" ||
record.lastIntent === "image_edit" ||
record.lastIntent === "vision"
? record.lastIntent
: undefined,
lastOptimizedPrompt: normalizeText(record.lastOptimizedPrompt, 4000),
lastUserPrompt: normalizeText(record.lastUserPrompt, 1000),
lastRequestedImageCount: normalizeImageCount(record.lastRequestedImageCount),
lastAspectRatio: normalizeText(record.lastAspectRatio, 32),
lastImageSize: normalizeText(record.lastImageSize, 32),
lastSourceImageUrls: normalizeImageUrls(record.lastSourceImageUrls),
lastResultImageUrls: normalizeImageUrls(record.lastResultImageUrls),
};
}
}
const fallbackTargetUrls = normalizeImageUrls(workingState?.lastResolvedTargetImageUrls);
const fallbackGeneratedUrls = normalizeImageUrls(workingState?.lastGeneratedImageUrls);
if (!memory) {
if (!fallbackTargetUrls && !fallbackGeneratedUrls) return null;
memory = {};
}
if (!memory.lastSourceImageUrls && fallbackTargetUrls) {
memory.lastSourceImageUrls = fallbackTargetUrls;
}
if (!memory.lastResultImageUrls && fallbackGeneratedUrls) {
memory.lastResultImageUrls = fallbackGeneratedUrls;
}
return memory;
}
export function buildImageWorkflowMemoryUpdate(params: {
existing?: ImageWorkflowMemory | null;
lastIntent?: "image_gen" | "image_edit" | "vision";
lastOptimizedPrompt?: string;
lastUserPrompt?: string;
lastRequestedImageCount?: number;
lastAspectRatio?: string;
lastImageSize?: string;
lastSourceImageUrls?: string[] | null;
lastResultImageUrls?: string[] | null;
}) {
return {
lastIntent: params.lastIntent ?? params.existing?.lastIntent,
lastOptimizedPrompt:
normalizeText(params.lastOptimizedPrompt, 4000) ??
params.existing?.lastOptimizedPrompt,
lastUserPrompt:
normalizeText(params.lastUserPrompt, 1000) ?? params.existing?.lastUserPrompt,
lastRequestedImageCount:
normalizeImageCount(params.lastRequestedImageCount) ??
params.existing?.lastRequestedImageCount,
lastAspectRatio:
normalizeText(params.lastAspectRatio, 32) ?? params.existing?.lastAspectRatio,
lastImageSize:
normalizeText(params.lastImageSize, 32) ?? params.existing?.lastImageSize,
lastSourceImageUrls:
normalizeImageUrls(params.lastSourceImageUrls) ??
params.existing?.lastSourceImageUrls,
lastResultImageUrls:
normalizeImageUrls(params.lastResultImageUrls) ??
params.existing?.lastResultImageUrls,
} satisfies ImageWorkflowMemory;
}
export function getImageWorkflowPlanningHints(
threadState?: AgentThreadState | null,
): string[] {
const metadata = threadState?.metadata;
const memory =
metadata && typeof metadata === "object" && !Array.isArray(metadata)
? ((metadata.imageWorkflowMemory as ImageWorkflowMemory | undefined) || undefined)
: undefined;
if (!memory) return [];
const hints: string[] = [];
if (memory.lastOptimizedPrompt) {
hints.push(
`上一轮可复用提示词摘要:${memory.lastOptimizedPrompt.replace(/\s+/g, " ").slice(0, 180)}`
);
}
if (memory.lastRequestedImageCount) {
hints.push(`用户上一轮期望出图数量:${memory.lastRequestedImageCount} 张。`);
}
if (memory.lastAspectRatio) {
hints.push(`用户上一轮常用画幅:${memory.lastAspectRatio}。`);
}
if (memory.lastImageSize) {
hints.push(`用户上一轮常用清晰度:${memory.lastImageSize}。`);
}
if (memory.lastSourceImageUrls?.length) {
hints.push(`上一轮存在 ${memory.lastSourceImageUrls.length} 张参考/目标图。`);
}
if (memory.lastResultImageUrls?.length) {
hints.push(`上一轮生成结果数:${memory.lastResultImageUrls.length} 张。`);
}
return hints;
}