feat(canvas): 智能引用 - 支持素材自动补全与全文引用
This commit is contained in:
@@ -51,6 +51,105 @@ export function normalizeCanvasNodeMentionTokens(prompt: string, references: Can
|
||||
}, prompt);
|
||||
}
|
||||
|
||||
/** 将提示词内已连接素材的名称替换为对应引用;同名的每处指代都会保留为独立引用。 */
|
||||
export function autoMentionCanvasResourceReferences(prompt: string, references: CanvasResourceReference[]) {
|
||||
const referenceByName = new Map<string, CanvasResourceReference | null>();
|
||||
references
|
||||
.filter((reference) => reference.active && !reference.assetId && reference.kind !== "skill")
|
||||
.forEach((reference, index) => {
|
||||
canvasResourceMentionNames(reference, index, true).forEach((name) => {
|
||||
if (!referenceByName.has(name)) {
|
||||
referenceByName.set(name, reference);
|
||||
return;
|
||||
}
|
||||
if (referenceByName.get(name)?.id !== reference.id) referenceByName.set(name, null);
|
||||
});
|
||||
});
|
||||
const names = [...referenceByName]
|
||||
.filter(([, reference]) => Boolean(reference))
|
||||
.map(([name]) => name)
|
||||
.sort((left, right) => right.length - left.length);
|
||||
if (!names.length) return prompt;
|
||||
|
||||
const matcher = new RegExp(`(^|[^A-Za-z0-9_@])(${names.map(escapeRegExp).join("|")})(?![A-Za-z0-9_])`, "gu");
|
||||
return prompt
|
||||
.split(/(@\[[^\]]+\]|@[^\s@,.;:!?,。;:!?、)\]}】)]+)/gu)
|
||||
.map((part) => {
|
||||
if (part.startsWith("@")) return part;
|
||||
return part.replace(matcher, (match, prefix: string, name: string, offset: number, source: string) => {
|
||||
const nextCharacter = source[offset + match.length];
|
||||
if (/^\d+$/u.test(name) && ((prefix && !/[\s,.!?;:,。!?;:、)\]}】)]/u.test(prefix)) || (nextCharacter && !/[\s,.!?;:,。!?;:、)\]}】)]/u.test(nextCharacter)))) return match;
|
||||
const separator = needsAutoMentionSeparator(nextCharacter) ? " " : "";
|
||||
return `${prefix}${canvasResourceMentionToken(referenceByName.get(name)!)}${separator}`;
|
||||
});
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
export type CanvasResourceAutoLinkMatch = {
|
||||
reference: CanvasResourceReference;
|
||||
start: number;
|
||||
end: number;
|
||||
query: string;
|
||||
};
|
||||
|
||||
export function findCanvasResourceAutoLinkMatch(prompt: string, cursor: number, references: CanvasResourceReference[]): CanvasResourceAutoLinkMatch | null {
|
||||
if (cursor <= 0 || cursor > prompt.length) return null;
|
||||
const prefix = prompt.slice(0, cursor);
|
||||
if (/@(?:\[[^\]]*|[^\s@,.;:!?,。;:!?、)\]}】)]*)$/u.test(prefix)) return null;
|
||||
const aliases = new Map<string, CanvasResourceReference | null>();
|
||||
references
|
||||
.filter((reference) => reference.active && !reference.assetId && reference.kind !== "skill")
|
||||
.forEach((reference, index) => {
|
||||
canvasResourceMentionNames(reference, index, true).forEach((name) => {
|
||||
if (!aliases.has(name)) aliases.set(name, reference);
|
||||
else if (aliases.get(name)?.id !== reference.id) aliases.set(name, null);
|
||||
});
|
||||
});
|
||||
const candidates = [...aliases]
|
||||
.filter(([, reference]) => Boolean(reference))
|
||||
.map(([name, reference]) => ({ name, reference: reference!, isBareOrder: /^\d+$/u.test(name) }))
|
||||
.sort((left, right) => right.name.length - left.name.length);
|
||||
for (const candidate of candidates) {
|
||||
if (!prefix.endsWith(candidate.name)) continue;
|
||||
const start = cursor - candidate.name.length;
|
||||
const previous = prompt[start - 1];
|
||||
const next = prompt[cursor];
|
||||
if (previous === "@") continue;
|
||||
if (next && /[A-Za-z0-9_]/u.test(next)) continue;
|
||||
if (candidate.isBareOrder && previous && !/[\s,.!?;:,。!?;:、)\]}】)]/u.test(previous)) continue;
|
||||
if (candidate.isBareOrder && next && !/[\s,.!?;:,。!?;:、)\]}】)]/u.test(next)) continue;
|
||||
if (/^[A-Za-z0-9_]+$/u.test(candidate.name) && previous && /[A-Za-z0-9_]/u.test(previous)) continue;
|
||||
return { reference: candidate.reference, start, end: cursor, query: candidate.name };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function canvasResourceMentionNames(reference: CanvasResourceReference, index: number, includeBareNumber: boolean) {
|
||||
const names = new Set([reference.label.trim(), reference.title.trim()].filter(Boolean));
|
||||
const order = index + 1;
|
||||
names.add(`图片${order}`);
|
||||
names.add(`图${order}`);
|
||||
names.add(`image${order}`);
|
||||
names.add(`image ${order}`);
|
||||
const imageLabelMatch = /^图片(\d+)$/u.exec(reference.label.trim());
|
||||
if (imageLabelMatch) {
|
||||
names.add(`图${imageLabelMatch[1]}`);
|
||||
names.add(`image${imageLabelMatch[1]}`);
|
||||
names.add(`image ${imageLabelMatch[1]}`);
|
||||
}
|
||||
if (includeBareNumber) names.add(String(order));
|
||||
return names;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function needsAutoMentionSeparator(nextCharacter: string | undefined) {
|
||||
return Boolean(nextCharacter && !/[\s,.!?;:,。!?;:、)\]}】)]/u.test(nextCharacter));
|
||||
}
|
||||
|
||||
const CANVAS_RESOURCE_MENTION_BOUNDARY = /(?:$|\s|[,.!?;:,。!?;:、)\]}】)])/;
|
||||
|
||||
function canvasResourceReferenceMentionTokens(reference: CanvasResourceReference) {
|
||||
|
||||
Reference in New Issue
Block a user