fix(ai): preserve relational image references

This commit is contained in:
134 2026-09-05 18:45:48 +08:00 committed by root
parent 9717196264
commit 6269aefee5
4 changed files with 96 additions and 73 deletions

View File

@ -0,0 +1,71 @@
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;
}
export function collectOrdinalImageIndexes(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*([一二两三四五六七八九十])(?![一二两三四五六七八九十\d维])/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);
}

View File

@ -10,6 +10,7 @@ import {
hasCanvasAgentDeicticTarget,
resolveCanvasAgentTargetPreference,
} from "@/lib/ai/runtime/canvas-agent-kernel";
import { collectOrdinalImageIndexes } from "@/lib/ai/context-memory/ordinal-image-references";
const ORIGINAL_IMAGE_REGEX =
/(原图|上传图|参考图|最初那张|第一张图|素材图|源图|原始图片|原始那张|我发的那张|上传的那张|原来的图)/i;
@ -33,40 +34,6 @@ function normalizeStringArray(values?: string[] | null): string[] {
);
}
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);
@ -98,46 +65,8 @@ function getPositionLabel(ref: ResolvedImageRef, refs: ResolvedImageRef[]): stri
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);
const ordinalIndexes = collectOrdinalImageIndexes(text);
if (ordinalIndexes.length === 0) return [];
const ordered = sortRefsForCanvasOrdering(refs);
return ordinalIndexes

View File

@ -30,6 +30,7 @@ export default defineConfig({
/routes\/hicad-manufacturing-extension\.test\.ts/,
/routes\/openapi-access-key\.test\.ts/,
/routes\/vip98k-reference-routing\.test\.ts/,
/routes\/ordinal-image-references\.test\.ts/,
],
fullyParallel: true,
forbidOnly: !!process.env.CI,

View File

@ -0,0 +1,22 @@
import { expect, test } from "@playwright/test";
import { collectOrdinalImageIndexes } from "@/lib/ai/context-memory/ordinal-image-references";
test.describe("ordinal image references", () => {
test("keeps both sides of a Chinese replacement relationship", () => {
expect(
collectOrdinalImageIndexes("把图二替换到图一的场景中,靠着左边放"),
).toEqual([0, 1]);
expect(collectOrdinalImageIndexes("用图三替换图一的产品")).toEqual([
0, 2,
]);
});
test("still resolves a single compact ordinal", () => {
expect(collectOrdinalImageIndexes("只调整图二")).toEqual([1]);
expect(collectOrdinalImageIndexes("修改第三张图")).toEqual([2]);
});
test("does not treat dimensional wording as an image ordinal", () => {
expect(collectOrdinalImageIndexes("保持图二维布局")).toEqual([]);
});
});