ai-chatbot/lib/ai/context-memory/target-resolution.ts

610 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import "server-only";
import type {
ResolveTargetAssetsParams,
ResolvedImageRef,
ResolvedTargetAssets,
} from "@/lib/ai/context-memory/types";
import {
hasCanvasAgentActionableImageEditIntent,
hasCanvasAgentDeicticTarget,
resolveCanvasAgentTargetPreference,
} from "@/lib/ai/runtime/canvas-agent-kernel";
const ORIGINAL_IMAGE_REGEX =
/(原图|上传图|参考图|最初那张|第一张图|素材图|源图|原始图片|原始那张|我发的那张|上传的那张|原来的图)/i;
const LATEST_RESULT_REGEX =
/(上一张|上一轮|刚生成|最新生成|这个结果|该结果|按这个结果|继续在这张|基于这张结果|延续上一次结果|上一版|刚才那张结果|生成的图|生成图|结果图|出图结果|刚出的图|上一张生成图|上一轮结果图|现在这张生成图)/i;
const SELECTION_REGEX = /(选中的|当前这张|我选的|这几张|这些图|选中图)/i;
const ACTIONABLE_GENERATION_REGEX =
/(生图|生成|画一张|出图|来一张|再来一张|做一张|做图|做视频|视频生成|生成视频|动起来)/i;
const IMPLICIT_HISTORY_TARGET_REGEX =
/(继续|接着|沿用|按刚才|照刚才|上次|上一轮|上一版|刚才|刚生成|最新生成|结果图|生成图|这张|这个|当前|原图|上传图|选中)/i;
function normalizeStringArray(values?: string[] | null): string[] {
if (!Array.isArray(values)) return [];
return Array.from(
new Set(
values
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean),
),
);
}
function chineseNumberToInt(value: string): number | null {
const normalized = value.trim();
if (!normalized) return null;
if (/^\d+$/.test(normalized)) return Number.parseInt(normalized, 10);
const map: Record<string, number> = {
一: 1,
二: 2,
两: 2,
三: 3,
四: 4,
五: 5,
六: 6,
七: 7,
八: 8,
九: 9,
十: 10,
};
if (normalized === "十") return 10;
if (normalized.length === 1) return map[normalized] ?? null;
if (normalized.startsWith("十")) {
return 10 + (map[normalized.slice(1)] ?? 0);
}
if (normalized.endsWith("十")) {
return (map[normalized.slice(0, 1)] ?? 0) * 10;
}
if (normalized.includes("十")) {
const [head, tail] = normalized.split("十");
return (map[head] ?? 0) * 10 + (map[tail] ?? 0);
}
return null;
}
function sortRefsForCanvasOrdering(refs: ResolvedImageRef[]) {
return [...refs].sort((left, right) => {
const leftY = Number(left.y ?? 0);
const rightY = Number(right.y ?? 0);
if (Math.abs(leftY - rightY) > 12) {
return leftY - rightY;
}
return Number(left.x ?? 0) - Number(right.x ?? 0);
});
}
function getPositionLabel(ref: ResolvedImageRef, refs: ResolvedImageRef[]): string {
if (refs.length <= 1) return "中间";
const xs = refs.map((item) => Number(item.x ?? 0));
const ys = refs.map((item) => Number(item.y ?? 0));
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const minY = Math.min(...ys);
const maxY = Math.max(...ys);
const spanX = Math.max(1, maxX - minX);
const spanY = Math.max(1, maxY - minY);
const relX = (Number(ref.x ?? 0) - minX) / spanX;
const relY = (Number(ref.y ?? 0) - minY) / spanY;
const horizontal = relX <= 0.33 ? "左" : relX >= 0.66 ? "右" : "中";
const vertical = relY <= 0.33 ? "上" : relY >= 0.66 ? "下" : "中";
if (horizontal === "中" && vertical === "中") return "中间";
return `${vertical}${horizontal}`;
}
function collectOrdinalIndexes(text: string): number[] {
const indexes = new Set<number>();
const normalizedText = String(text || "");
const chineseOrdinalPattern =
/第\s*([一二三四五六七八九十两\d]+)\s*(?:张|个|幅)?\s*(?:图|图片|照片)?/gi;
const numberedImagePattern = /(?:图|图片|照片|image|photo|reference)\s*([1-9]\d*)\b/gi;
const compactChineseImagePattern =
/(?:图片|照片|图)\s*([一二两三四五六七八九十])(?=\s*(?:的|上|中|里|内|和|与|及|、|,|,|。|;|;|$))/g;
const englishOrdinals: Array<{ index: number; pattern: RegExp }> = [
{ index: 0, pattern: /\b(?:first|1st)\s+(?:image|photo|reference)\b/i },
{ index: 1, pattern: /\b(?:second|2nd)\s+(?:image|photo|reference)\b/i },
{ index: 2, pattern: /\b(?:third|3rd)\s+(?:image|photo|reference)\b/i },
{ index: 3, pattern: /\b(?:fourth|4th)\s+(?:image|photo|reference)\b/i },
{ index: 4, pattern: /\b(?:fifth|5th)\s+(?:image|photo|reference)\b/i },
{ index: 5, pattern: /\b(?:sixth|6th)\s+(?:image|photo|reference)\b/i },
{ index: 6, pattern: /\b(?:seventh|7th)\s+(?:image|photo|reference)\b/i },
{ index: 7, pattern: /\b(?:eighth|8th)\s+(?:image|photo|reference)\b/i },
];
for (const match of normalizedText.matchAll(chineseOrdinalPattern)) {
const numeric = chineseNumberToInt(match[1] || "");
if (numeric && numeric > 0) indexes.add(numeric - 1);
}
for (const match of normalizedText.matchAll(numberedImagePattern)) {
const numeric = Number.parseInt(match[1] || "", 10);
if (Number.isFinite(numeric) && numeric > 0) indexes.add(numeric - 1);
}
for (const match of normalizedText.matchAll(compactChineseImagePattern)) {
const numeric = chineseNumberToInt(match[1] || "");
if (numeric && numeric > 0) indexes.add(numeric - 1);
}
for (const item of englishOrdinals) {
if (item.pattern.test(normalizedText)) indexes.add(item.index);
}
return Array.from(indexes).sort((left, right) => left - right);
}
function pickByOrdinal(text: string, refs: ResolvedImageRef[]): ResolvedImageRef[] {
const ordinalIndexes = collectOrdinalIndexes(text);
if (ordinalIndexes.length === 0) return [];
const ordered = sortRefsForCanvasOrdering(refs);
return ordinalIndexes
.map((index) => ordered[index])
.filter((ref): ref is ResolvedImageRef => Boolean(ref));
}
function pickBySpatialHint(text: string, refs: ResolvedImageRef[]): ResolvedImageRef[] {
if (!refs.length) return [];
const normalizedText = text.replace(/\s+/g, "");
const hasLeftTop = /左上/.test(normalizedText);
const hasLeftBottom = /左下/.test(normalizedText);
const hasRightTop = /右上/.test(normalizedText);
const hasRightBottom = /右下/.test(normalizedText);
const hasLeft = /左图|左边|左侧|左面|靠左/.test(normalizedText);
const hasRight = /右图|右边|右侧|右面|靠右/.test(normalizedText);
const hasTop = /上图|上面|上方|上边|上侧|顶部|靠上|最上/.test(normalizedText);
const hasBottom = /下图|下面|下方|下边|下侧|底部|靠下|最下/.test(normalizedText);
const hasCenter = /中间|居中|正中|中央/.test(normalizedText);
if (
!(
hasLeftTop ||
hasLeftBottom ||
hasRightTop ||
hasRightBottom ||
hasLeft ||
hasRight ||
hasTop ||
hasBottom ||
hasCenter
)
) {
return [];
}
const withLabels = refs.map((ref) => ({
ref,
label: getPositionLabel(ref, refs),
}));
return withLabels
.filter(({ label }) => {
const matchesCompound =
(hasLeftTop && label.includes("左") && label.includes("上")) ||
(hasLeftBottom && label.includes("左") && label.includes("下")) ||
(hasRightTop && label.includes("右") && label.includes("上")) ||
(hasRightBottom && label.includes("右") && label.includes("下"));
const matchesAxis =
(hasLeft && label.includes("左")) ||
(hasRight && label.includes("右")) ||
(hasTop && label.includes("上")) ||
(hasBottom && label.includes("下")) ||
(hasCenter && (label.includes("中") || label === "中间"));
return matchesCompound || matchesAxis;
})
.map(({ ref }) => ref);
}
function buildResolvedTargetAssets(params: {
targetRefs: ResolvedImageRef[];
candidateRefs: ResolvedImageRef[];
resolutionMode: ResolvedTargetAssets["resolutionMode"];
confidence: number;
needsClarification: boolean;
reason: string;
fromHistory: boolean;
}): ResolvedTargetAssets {
const targetRefs = params.targetRefs;
const candidateRefs = params.candidateRefs;
return {
targetRefs,
candidateRefs,
targetImageUrls: targetRefs.map((ref) => ref.url),
targetAssetIds: normalizeStringArray(targetRefs.map((ref) => ref.assetId || "")),
candidateImageUrls: candidateRefs.map((ref) => ref.url),
candidateAssetIds: normalizeStringArray(candidateRefs.map((ref) => ref.assetId || "")),
resolutionMode: params.resolutionMode,
confidence: Math.max(0, Math.min(100, Math.round(params.confidence))),
needsClarification: params.needsClarification,
reason: params.reason,
fromHistory: params.fromHistory,
};
}
function toWorkingMemoryRefs(urls: string[]): ResolvedImageRef[] {
return urls.map((url) => ({
url,
normalizedUrl: url,
source: "working_memory" as const,
role: "working_memory" as const,
}));
}
function buildNormalizedUrlSet(refs: ResolvedImageRef[]): Set<string> {
return new Set(
refs
.map((ref) => String(ref.normalizedUrl || ref.url || "").trim())
.filter(Boolean),
);
}
export function resolveTargetAssets(
params: ResolveTargetAssetsParams,
): ResolvedTargetAssets {
const text = String(params.text || "").trim();
const mediaInput = params.mediaInput;
const canvasRefs = mediaInput.canvasImageRefs;
const selectedRefs = mediaInput.selectedImageRefs;
const currentRefs = mediaInput.currentMessageImageRefs;
const assistantRefs = mediaInput.historyAssistantImageRefs;
const userRefs = mediaInput.historyUserImageRefs;
const candidateRefs = mediaInput.imageRefs;
const workingState = params.workingState;
const focusPolicy = params.contextPayload?.focusPolicy || "auto";
const workingStateGeneratedUrls = normalizeStringArray(workingState?.lastGeneratedImageUrls);
const workingStateUploadedUrls = normalizeStringArray(workingState?.lastUploadedImageUrls);
const workingStateResolvedUrls = normalizeStringArray(workingState?.lastResolvedTargetImageUrls);
const generatedRefs =
workingStateGeneratedUrls.length > 0 ? toWorkingMemoryRefs(workingStateGeneratedUrls) : assistantRefs;
const originalRefs =
workingStateUploadedUrls.length > 0 ? toWorkingMemoryRefs(workingStateUploadedUrls) : userRefs;
const selectedLooksLikeOriginal = (() => {
if (selectedRefs.length === 0 || originalRefs.length === 0) return false;
const originalKeys = buildNormalizedUrlSet(originalRefs);
return selectedRefs.every((ref) =>
originalKeys.has(String(ref.normalizedUrl || ref.url || "").trim()),
);
})();
const currentOrdinalRefs = pickByOrdinal(text, currentRefs);
if (currentOrdinalRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: currentOrdinalRefs,
candidateRefs,
resolutionMode: "current_upload",
confidence: currentOrdinalRefs.length > 1 ? 96 : 94,
needsClarification: false,
reason:
currentOrdinalRefs.length > 1
? "根据用户文本中的多个序号定位到本轮上传的多张图片"
: "根据用户文本中的序号定位到本轮上传图片",
fromHistory: false,
});
}
if (currentRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: currentRefs,
candidateRefs,
resolutionMode: "current_upload",
confidence: 93,
needsClarification: false,
reason: "本轮消息包含图片输入,默认优先处理当前上传图片",
fromHistory: false,
});
}
const ordinalRefs = pickByOrdinal(text, canvasRefs);
if (ordinalRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: ordinalRefs,
candidateRefs,
resolutionMode: "text_reference",
confidence: ordinalRefs.length > 1 ? 91 : 88,
needsClarification: false,
reason:
ordinalRefs.length > 1
? "根据用户文本中的多个序号定位到多张画布图片"
: "根据用户文本中的序号定位到画布图片",
fromHistory: true,
});
}
if (ORIGINAL_IMAGE_REGEX.test(text) && originalRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: originalRefs,
candidateRefs,
resolutionMode: "text_reference",
confidence: 95,
needsClarification: false,
reason: "用户文本明确指向原图/上传图",
fromHistory: true,
});
}
if (LATEST_RESULT_REGEX.test(text) && generatedRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: generatedRefs,
candidateRefs,
resolutionMode: "latest_result",
confidence: 95,
needsClarification: false,
reason: "用户文本明确指向上一轮结果/生成图",
fromHistory: true,
});
}
if (focusPolicy === "original" && originalRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: originalRefs,
candidateRefs,
resolutionMode: "text_reference",
confidence: 96,
needsClarification: false,
reason: "前端上下文指定优先使用原图",
fromHistory: true,
});
}
if (focusPolicy === "latest_result" && generatedRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: generatedRefs,
candidateRefs,
resolutionMode: "latest_result",
confidence: 96,
needsClarification: false,
reason: "前端上下文指定优先使用上一轮结果",
fromHistory: true,
});
}
const spatialRefs = pickBySpatialHint(text, canvasRefs);
if (spatialRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: spatialRefs,
candidateRefs,
resolutionMode: "text_reference",
confidence: spatialRefs.length === 1 ? 90 : 76,
needsClarification: false,
reason: "根据用户文本中的方位指向定位到画布图片",
fromHistory: true,
});
}
if (focusPolicy === "selected" && selectedRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: selectedRefs,
candidateRefs,
resolutionMode: "explicit_selection",
confidence: 97,
needsClarification: false,
reason: "前端已明确选中目标图片",
fromHistory: true,
});
}
const canvasAgentTargetPreference = resolveCanvasAgentTargetPreference({
text,
selectedImageCount: selectedRefs.length,
latestResultImageCount: generatedRefs.length,
workingMemoryTargetImageCount: workingStateResolvedUrls.length,
});
if (canvasAgentTargetPreference.shouldPreferLatestResult) {
return buildResolvedTargetAssets({
targetRefs: generatedRefs,
candidateRefs,
resolutionMode:
workingStateGeneratedUrls.length > 0 ? "working_memory" : "latest_result",
confidence: canvasAgentTargetPreference.confidence,
needsClarification: false,
reason: "用户使用指代表达并提出图片编辑意图,默认续接最近生成结果",
fromHistory: true,
});
}
if (canvasAgentTargetPreference.shouldPreferWorkingMemoryTarget) {
const refs = toWorkingMemoryRefs(workingStateResolvedUrls);
return buildResolvedTargetAssets({
targetRefs: refs,
candidateRefs,
resolutionMode: "working_memory",
confidence: canvasAgentTargetPreference.confidence,
needsClarification: false,
reason: "用户使用指代表达并提出图片编辑意图,使用工作记忆中的上一轮目标图",
fromHistory: true,
});
}
if (hasCanvasAgentDeicticTarget(text)) {
if (selectedRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: selectedRefs,
candidateRefs,
resolutionMode: "explicit_selection",
confidence: 93,
needsClarification: false,
reason: "用户使用“这张/这只/这个”等指代表达,且当前存在明确选中目标图",
fromHistory: true,
});
}
if (candidateRefs.length === 1) {
return buildResolvedTargetAssets({
targetRefs: [candidateRefs[0]],
candidateRefs,
resolutionMode: "text_reference",
confidence: 84,
needsClarification: false,
reason: "用户使用指代表达,且当前仅有一张候选图",
fromHistory: true,
});
}
return buildResolvedTargetAssets({
targetRefs: [],
candidateRefs,
resolutionMode: "clarification",
confidence: 0,
needsClarification: true,
reason: "用户使用“这张/这只/这个”等指代表达,但当前没有明确选中唯一目标图",
fromHistory: true,
});
}
if (
hasCanvasAgentActionableImageEditIntent(text) &&
currentRefs.length === 0 &&
generatedRefs.length > 0 &&
selectedLooksLikeOriginal &&
!SELECTION_REGEX.test(text) &&
!ORIGINAL_IMAGE_REGEX.test(text)
) {
return buildResolvedTargetAssets({
targetRefs: generatedRefs,
candidateRefs,
resolutionMode: "latest_result",
confidence: 89,
needsClarification: false,
reason: "连续对话默认续接上一轮生成结果,避免误回退到原始选中图",
fromHistory: true,
});
}
if ((SELECTION_REGEX.test(text) || focusPolicy === "selected") && selectedRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: selectedRefs,
candidateRefs,
resolutionMode: "explicit_selection",
confidence: 92,
needsClarification: false,
reason: "用户文本或上下文明确要求使用当前选中的图片",
fromHistory: true,
});
}
if (selectedRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: selectedRefs,
candidateRefs,
resolutionMode: "explicit_selection",
confidence: 86,
needsClarification: false,
reason: "当前存在画布选中图片,默认优先继续处理选中目标",
fromHistory: true,
});
}
if (
canvasRefs.length === 1 &&
(hasCanvasAgentActionableImageEditIntent(text) || focusPolicy === "auto")
) {
return buildResolvedTargetAssets({
targetRefs: [canvasRefs[0]],
candidateRefs,
resolutionMode: "text_reference",
confidence: 84,
needsClarification: false,
reason: "用户使用画布/图片指代表达,且当前画布仅有一张可处理图片",
fromHistory: true,
});
}
const allowsImplicitHistoryTarget =
IMPLICIT_HISTORY_TARGET_REGEX.test(text) &&
(hasCanvasAgentActionableImageEditIntent(text) ||
ACTIONABLE_GENERATION_REGEX.test(text) ||
LATEST_RESULT_REGEX.test(text) ||
ORIGINAL_IMAGE_REGEX.test(text) ||
hasCanvasAgentDeicticTarget(text) ||
SELECTION_REGEX.test(text));
if (allowsImplicitHistoryTarget) {
if (workingStateResolvedUrls.length > 0) {
const refs = toWorkingMemoryRefs(workingStateResolvedUrls);
return buildResolvedTargetAssets({
targetRefs: refs,
candidateRefs,
resolutionMode: "working_memory",
confidence: 82,
needsClarification: false,
reason: "用户表达了连续处理意图,使用会话工作记忆中的上一轮已解析目标图",
fromHistory: true,
});
}
if (workingStateGeneratedUrls.length > 0) {
const refs = toWorkingMemoryRefs(workingStateGeneratedUrls);
return buildResolvedTargetAssets({
targetRefs: refs,
candidateRefs,
resolutionMode: "working_memory",
confidence: 78,
needsClarification: false,
reason: "用户表达了连续处理意图,使用会话工作记忆中的最近生成结果",
fromHistory: true,
});
}
if (generatedRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: generatedRefs,
candidateRefs,
resolutionMode: "latest_result",
confidence: 74,
needsClarification: false,
reason: "用户表达了连续处理意图,续接最近一轮生成结果",
fromHistory: true,
});
}
if (originalRefs.length > 0) {
return buildResolvedTargetAssets({
targetRefs: originalRefs,
candidateRefs,
resolutionMode: "working_memory",
confidence: 68,
needsClarification: false,
reason: "用户表达了连续处理意图,回退使用最近一次用户上传图片",
fromHistory: true,
});
}
}
if (hasCanvasAgentActionableImageEditIntent(text)) {
if (canvasRefs.length === 1) {
return buildResolvedTargetAssets({
targetRefs: [canvasRefs[0]],
candidateRefs,
resolutionMode: "text_reference",
confidence: 82,
needsClarification: false,
reason: "用户表达了图片编辑意图,且当前画布仅有一张可处理图片",
fromHistory: true,
});
}
return buildResolvedTargetAssets({
targetRefs: [],
candidateRefs,
resolutionMode: "clarification",
confidence: 0,
needsClarification: true,
reason: "用户表达了编辑意图,但没有明确选中或文本定位目标图;不会自动关联历史图片",
fromHistory: false,
});
}
return buildResolvedTargetAssets({
targetRefs: [],
candidateRefs,
resolutionMode: "none",
confidence: 0,
needsClarification: false,
reason: "当前请求没有可定位的图片目标",
fromHistory: false,
});
}