215 lines
7.5 KiB
TypeScript
215 lines
7.5 KiB
TypeScript
import "server-only";
|
||
|
||
import type { AgentThreadState } from "@/lib/ai/runtime/agent-thread-state";
|
||
import type { ImageWorkflowMemory } from "@/lib/ai/image-workflow-memory";
|
||
import type { PromptMemoryEntry } from "@/lib/ai/prompt-memory";
|
||
import {
|
||
applyContextLineLimit,
|
||
applyMemoryHintLimit,
|
||
type MemoryRelevancePolicy,
|
||
} from "@/lib/ai/runtime/memory-relevance";
|
||
import type { StoryboardPreferenceMemory } from "@/lib/video-agent/storyboard-preference-memory";
|
||
import type {
|
||
ResolvedChatMediaInput,
|
||
ResolvedTargetAssets,
|
||
} from "@/lib/ai/context-memory/types";
|
||
|
||
export interface ComposedConversationMemory {
|
||
contextSummary: string;
|
||
memoryHints: string[];
|
||
metadata: Record<string, unknown>;
|
||
}
|
||
|
||
function normalizeText(value: unknown, max = 400): string | undefined {
|
||
const normalized = String(value || "").trim();
|
||
if (!normalized) return undefined;
|
||
return normalized.slice(0, max);
|
||
}
|
||
|
||
function uniqueStrings(values: Array<string | undefined>) {
|
||
return Array.from(
|
||
new Set(
|
||
values
|
||
.map((item) => normalizeText(item, 240))
|
||
.filter((item): item is string => Boolean(item))
|
||
)
|
||
);
|
||
}
|
||
|
||
function summarizeTargetResolution(target?: ResolvedTargetAssets | null) {
|
||
if (!target) return { lines: [] as string[], metadata: {} as Record<string, unknown> };
|
||
const lines: string[] = [];
|
||
lines.push(
|
||
`当前目标解析:mode=${target.resolutionMode}; confidence=${target.confidence}; fromHistory=${
|
||
target.fromHistory ? "yes" : "no"
|
||
}`
|
||
);
|
||
if (target.targetImageUrls.length > 0) {
|
||
lines.push(`当前命中的目标图片数:${target.targetImageUrls.length}`);
|
||
}
|
||
if (target.reason) {
|
||
lines.push(`目标解析原因:${target.reason}`);
|
||
}
|
||
return {
|
||
lines,
|
||
metadata: {
|
||
targetResolutionMode: target.resolutionMode,
|
||
targetResolutionConfidence: target.confidence,
|
||
targetImageCount: target.targetImageUrls.length,
|
||
targetReason: target.reason,
|
||
},
|
||
};
|
||
}
|
||
|
||
function summarizeResolvedMedia(media?: ResolvedChatMediaInput | null) {
|
||
if (!media) return { lines: [] as string[], metadata: {} as Record<string, unknown> };
|
||
return {
|
||
lines: [
|
||
`当前媒体输入:source=${media.source}; hasImage=${media.hasImage ? "yes" : "no"}`,
|
||
media.currentMessageImageUrls.length > 0
|
||
? `本轮上传图片数:${media.currentMessageImageUrls.length}`
|
||
: "",
|
||
media.selectedImageUrls.length > 0
|
||
? `当前选中图片数:${media.selectedImageUrls.length}`
|
||
: "",
|
||
media.historyAssistantImageUrls.length + media.historyUserImageUrls.length > 0
|
||
? `历史可参考图片数:${
|
||
media.historyAssistantImageUrls.length + media.historyUserImageUrls.length
|
||
}`
|
||
: "",
|
||
].filter(Boolean),
|
||
metadata: {
|
||
mediaSource: media.source,
|
||
hasImage: media.hasImage,
|
||
currentImageCount: media.currentMessageImageUrls.length,
|
||
selectedImageCount: media.selectedImageUrls.length,
|
||
historyImageCount:
|
||
media.historyAssistantImageUrls.length + media.historyUserImageUrls.length,
|
||
},
|
||
};
|
||
}
|
||
|
||
function summarizePromptMemory(promptMemory?: PromptMemoryEntry | null) {
|
||
if (!promptMemory?.optimizedPrompt) {
|
||
return { lines: [] as string[], hints: [] as string[], metadata: {} as Record<string, unknown> };
|
||
}
|
||
const preview = promptMemory.optimizedPrompt.replace(/\s+/g, " ").trim().slice(0, 220);
|
||
return {
|
||
lines: [`上一轮可复用提示词:${preview}`],
|
||
hints: [
|
||
"如果用户明确要求沿用上一轮提示词继续做图,优先把这段提示词作为本轮生成基础。",
|
||
],
|
||
metadata: {
|
||
promptMemory,
|
||
},
|
||
};
|
||
}
|
||
|
||
function summarizeImageWorkflow(memory?: ImageWorkflowMemory | null) {
|
||
if (!memory) {
|
||
return { lines: [] as string[], hints: [] as string[], metadata: {} as Record<string, unknown> };
|
||
}
|
||
const lines = [
|
||
memory.lastOptimizedPrompt
|
||
? `上一轮优化提示词摘要:${memory.lastOptimizedPrompt.replace(/\s+/g, " ").slice(0, 180)}`
|
||
: "",
|
||
memory.lastRequestedImageCount
|
||
? `上一轮目标出图数:${memory.lastRequestedImageCount} 张`
|
||
: "",
|
||
memory.lastAspectRatio ? `上一轮画幅:${memory.lastAspectRatio}` : "",
|
||
memory.lastImageSize ? `上一轮清晰度:${memory.lastImageSize}` : "",
|
||
memory.lastSourceImageUrls?.length
|
||
? `上一轮参考/目标图数量:${memory.lastSourceImageUrls.length}`
|
||
: "",
|
||
memory.lastResultImageUrls?.length
|
||
? `上一轮生成结果数量:${memory.lastResultImageUrls.length}`
|
||
: "",
|
||
].filter(Boolean);
|
||
const hints = memory.lastOptimizedPrompt
|
||
? ["如果用户要求沿用上一轮提示词继续做图,应优先复用这段提示词。"]
|
||
: [];
|
||
return {
|
||
lines,
|
||
hints,
|
||
metadata: {
|
||
imageWorkflowMemory: memory,
|
||
},
|
||
};
|
||
}
|
||
|
||
function summarizeStoryboardPreference(memory?: StoryboardPreferenceMemory | null) {
|
||
if (!memory) {
|
||
return { lines: [] as string[], hints: [] as string[], metadata: {} as Record<string, unknown> };
|
||
}
|
||
const lines = [
|
||
memory.preferredGridType ? `用户偏好宫格:${memory.preferredGridType}` : "",
|
||
memory.preferredShotCount ? `用户偏好镜头数:${memory.preferredShotCount}` : "",
|
||
memory.prefersConfirmation === true ? "用户偏好先确认再执行" : "",
|
||
memory.lastUsedGridType && memory.lastUsedShotCount
|
||
? `最近一次分镜任务:${memory.lastUsedGridType} / ${memory.lastUsedShotCount} 段`
|
||
: "",
|
||
].filter(Boolean);
|
||
const hints = memory.prefersConfirmation
|
||
? ["如果当前任务涉及分镜或高成本执行,优先先确认再推进。"]
|
||
: [];
|
||
return {
|
||
lines,
|
||
hints,
|
||
metadata: {
|
||
storyboardPreferenceMemory: memory,
|
||
},
|
||
};
|
||
}
|
||
|
||
export function composeConversationMemory(params: {
|
||
autoContextText?: string;
|
||
resolvedMediaInput?: ResolvedChatMediaInput | null;
|
||
targetResolution?: ResolvedTargetAssets | null;
|
||
promptMemory?: PromptMemoryEntry | null;
|
||
imageWorkflowMemory?: ImageWorkflowMemory | null;
|
||
storyboardPreferenceMemory?: StoryboardPreferenceMemory | null;
|
||
threadState?: AgentThreadState | null;
|
||
memoryRelevancePolicy?: MemoryRelevancePolicy;
|
||
}) {
|
||
const targetSummary = summarizeTargetResolution(params.targetResolution);
|
||
const mediaSummary = summarizeResolvedMedia(params.resolvedMediaInput);
|
||
const promptSummary = summarizePromptMemory(params.promptMemory);
|
||
const imageWorkflowSummary = summarizeImageWorkflow(params.imageWorkflowMemory);
|
||
const storyboardSummary = summarizeStoryboardPreference(params.storyboardPreferenceMemory);
|
||
|
||
const contextSummary = uniqueStrings([
|
||
params.autoContextText,
|
||
...mediaSummary.lines,
|
||
...targetSummary.lines,
|
||
...promptSummary.lines,
|
||
...imageWorkflowSummary.lines,
|
||
...storyboardSummary.lines,
|
||
]).join("\n");
|
||
|
||
const memoryHints = uniqueStrings([
|
||
...(params.threadState?.memoryHints || []),
|
||
...promptSummary.hints,
|
||
...imageWorkflowSummary.hints,
|
||
...storyboardSummary.hints,
|
||
]);
|
||
|
||
const metadata: Record<string, unknown> = {
|
||
...(params.threadState?.metadata || {}),
|
||
...mediaSummary.metadata,
|
||
...targetSummary.metadata,
|
||
...promptSummary.metadata,
|
||
...imageWorkflowSummary.metadata,
|
||
...storyboardSummary.metadata,
|
||
};
|
||
|
||
return {
|
||
contextSummary: params.memoryRelevancePolicy
|
||
? applyContextLineLimit(contextSummary, params.memoryRelevancePolicy.maxContextLines)
|
||
: contextSummary,
|
||
memoryHints: params.memoryRelevancePolicy
|
||
? applyMemoryHintLimit(memoryHints, params.memoryRelevancePolicy.maxMemoryHints)
|
||
: memoryHints,
|
||
metadata,
|
||
} satisfies ComposedConversationMemory;
|
||
}
|