Files
Genarrative/server-node/src/services/customWorldAgentIntentExtractionService.ts

1129 lines
29 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.
type CustomWorldCreatorInputMode = 'freeform' | 'card';
export interface CreatorFactionSeedRecord {
id: string;
name: string;
publicGoal: string;
tension: string;
notes: string;
locked?: boolean;
}
export interface CreatorCharacterSeedRecord {
id: string;
name: string;
role: string;
publicMask: string;
hiddenHook: string;
relationToPlayer: string;
notes: string;
locked?: boolean;
}
export interface CreatorLandmarkSeedRecord {
id: string;
name: string;
purpose: string;
mood: string;
secret: string;
locked?: boolean;
}
export interface CustomWorldCreatorIntentRecord {
sourceMode: CustomWorldCreatorInputMode;
rawSettingText: string;
worldHook: string;
themeKeywords: string[];
toneDirectives: string[];
playerPremise: string;
openingSituation: string;
coreConflicts: string[];
keyFactions: CreatorFactionSeedRecord[];
keyCharacters: CreatorCharacterSeedRecord[];
keyLandmarks: CreatorLandmarkSeedRecord[];
iconicElements: string[];
forbiddenDirectives: string[];
}
export type ExtractedCreatorIntentPatch = Partial<
Pick<
CustomWorldCreatorIntentRecord,
| 'rawSettingText'
| 'worldHook'
| 'themeKeywords'
| 'toneDirectives'
| 'playerPremise'
| 'openingSituation'
| 'coreConflicts'
| 'keyFactions'
| 'keyCharacters'
| 'keyLandmarks'
| 'iconicElements'
| 'forbiddenDirectives'
>
> & {
replaceFields?: Array<
| 'rawSettingText'
| 'worldHook'
| 'themeKeywords'
| 'toneDirectives'
| 'playerPremise'
| 'openingSituation'
| 'coreConflicts'
| 'keyFactions'
| 'keyCharacters'
| 'keyLandmarks'
| 'iconicElements'
| 'forbiddenDirectives'
>;
};
const THEME_LEXICON = [
'武侠',
'修仙',
'仙侠',
'赛博',
'蒸汽',
'废土',
'悬疑',
'宫廷',
'海岛',
'边境',
'宗教',
'朝堂',
'奇谭',
'妖异',
'科幻',
'神秘',
'冒险',
'克苏鲁',
'侦探',
];
const TONE_LEXICON = [
'冷峻',
'克制',
'压抑',
'浪漫',
'潮湿',
'荒凉',
'悬疑',
'紧张',
'明快',
'史诗',
'残酷',
'诡异',
'黑暗',
'肃杀',
'温柔',
'宏大',
'宿命',
'神秘',
];
const RELATIONSHIP_TERMS = [
'宿敌',
'盟友',
'导师',
'师父',
'搭档',
'同伴',
'恋人',
'家人',
'兄弟',
'姐妹',
'父亲',
'母亲',
'向导',
'引路人',
'守望者',
'巡夜人',
];
const META_MESSAGE_PATTERN =
/^()?(||||)||/u;
function toText(value: unknown) {
return typeof value === 'string' ? value.replace(/\s+/gu, ' ').trim() : '';
}
function toStringArray(value: unknown, maxCount = 8) {
if (!Array.isArray(value)) {
return [];
}
return [...new Set(value.map((item) => toText(item)).filter(Boolean))].slice(
0,
maxCount,
);
}
function slugify(value: string) {
const normalized = value
.trim()
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/gu, '-')
.replace(/^-+|-+$/gu, '');
return normalized || 'entry';
}
function createSeedId(prefix: string, label: string, index: number) {
return `${prefix}-${slugify(label || `${prefix}-${index + 1}`)}-${index + 1}`;
}
function clampText(value: string, maxLength: number) {
const normalized = value.trim().replace(/\s+/gu, ' ');
if (!normalized) {
return '';
}
if (normalized.length <= maxLength) {
return normalized;
}
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}`;
}
function splitSentences(text: string) {
return text
.split(/[\n]/u)
.map((sentence) => sentence.trim())
.filter(Boolean);
}
function splitList(text: string, maxCount = 8) {
const normalized = text
.replace(/["'`]/gu, '')
.replace(/^(||||||)/u, '')
.replace(/^(||||)/u, '')
.replace(/^(||||||)/u, '')
.trim();
if (!normalized) {
return [];
}
return [
...new Set(
normalized
.split(/[,\/|;]/u)
.map((item) => item.trim())
.filter((item) => item.length >= 2 && item.length <= 24),
),
].slice(0, maxCount);
}
function mergeStringArray(
base: string[],
patch: string[] | undefined,
maxCount: number,
) {
if (!patch || patch.length === 0) {
return [...base];
}
return [
...new Set([...base, ...patch.map((item) => toText(item)).filter(Boolean)]),
].slice(0, maxCount);
}
function mergeNarrativeText(base: string, patch: string | undefined) {
const nextText = toText(patch);
if (!nextText) {
return base;
}
if (!base) {
return nextText;
}
if (base.includes(nextText)) {
return base;
}
return `${base}\n${nextText}`.trim();
}
function normalizeCreatorFactionSeed(
value: unknown,
index: number,
): CreatorFactionSeedRecord | null {
if (!value || typeof value !== 'object') {
return null;
}
const item = value as Record<string, unknown>;
const name = toText(item.name);
const publicGoal = toText(item.publicGoal);
const tension = toText(item.tension);
const notes = toText(item.notes);
if (!name && !publicGoal && !tension && !notes) {
return null;
}
return {
id:
toText(item.id) ||
createSeedId('creator-faction', name || publicGoal, index),
name,
publicGoal,
tension,
notes,
locked: Boolean(item.locked),
};
}
function normalizeCreatorCharacterSeed(
value: unknown,
index: number,
): CreatorCharacterSeedRecord | null {
if (!value || typeof value !== 'object') {
return null;
}
const item = value as Record<string, unknown>;
const name = toText(item.name);
const role = toText(item.role);
const publicMask = toText(item.publicMask);
const hiddenHook = toText(item.hiddenHook);
const relationToPlayer = toText(item.relationToPlayer);
const notes = toText(item.notes);
if (
!name &&
!role &&
!publicMask &&
!hiddenHook &&
!relationToPlayer &&
!notes
) {
return null;
}
return {
id:
toText(item.id) ||
createSeedId('creator-character', name || role || publicMask, index),
name,
role,
publicMask,
hiddenHook,
relationToPlayer,
notes,
locked: Boolean(item.locked),
};
}
function normalizeCreatorLandmarkSeed(
value: unknown,
index: number,
): CreatorLandmarkSeedRecord | null {
if (!value || typeof value !== 'object') {
return null;
}
const item = value as Record<string, unknown>;
const name = toText(item.name);
const purpose = toText(item.purpose);
const mood = toText(item.mood);
const secret = toText(item.secret);
if (!name && !purpose && !mood && !secret) {
return null;
}
return {
id:
toText(item.id) ||
createSeedId('creator-landmark', name || purpose || mood, index),
name,
purpose,
mood,
secret,
locked: Boolean(item.locked),
};
}
function normalizeAnchorArray<T>(
value: unknown,
normalizer: (value: unknown, index: number) => T | null,
maxCount: number,
) {
if (!Array.isArray(value)) {
return [];
}
return value
.map((item, index) => normalizer(item, index))
.filter((item): item is T => Boolean(item))
.slice(0, maxCount);
}
function mergeSeedArray<T extends { id: string; name?: string }>(
base: T[],
patch: T[] | undefined,
maxCount: number,
mergeEntry: (current: T, next: T) => T,
) {
if (!patch || patch.length === 0) {
return [...base];
}
const nextItems = [...base];
patch.forEach((entry) => {
const normalizedName = toText(entry.name);
const existingIndex = nextItems.findIndex(
(item) =>
item.id === entry.id ||
(normalizedName &&
toText(item.name).toLowerCase() === normalizedName.toLowerCase()),
);
if (existingIndex >= 0) {
nextItems[existingIndex] = mergeEntry(nextItems[existingIndex], entry);
return;
}
nextItems.push(entry);
});
return nextItems.slice(0, maxCount);
}
function mergeCharacterSeed(
current: CreatorCharacterSeedRecord,
next: CreatorCharacterSeedRecord,
): CreatorCharacterSeedRecord {
return {
...current,
...next,
id: next.id || current.id,
name: toText(next.name) || current.name,
role: toText(next.role) || current.role,
publicMask: toText(next.publicMask) || current.publicMask,
hiddenHook: toText(next.hiddenHook) || current.hiddenHook,
relationToPlayer: toText(next.relationToPlayer) || current.relationToPlayer,
notes: toText(next.notes) || current.notes,
locked:
typeof next.locked === 'boolean' ? next.locked : Boolean(current.locked),
};
}
function mergeFactionSeed(
current: CreatorFactionSeedRecord,
next: CreatorFactionSeedRecord,
): CreatorFactionSeedRecord {
return {
...current,
...next,
id: next.id || current.id,
name: toText(next.name) || current.name,
publicGoal: toText(next.publicGoal) || current.publicGoal,
tension: toText(next.tension) || current.tension,
notes: toText(next.notes) || current.notes,
locked:
typeof next.locked === 'boolean' ? next.locked : Boolean(current.locked),
};
}
function mergeLandmarkSeed(
current: CreatorLandmarkSeedRecord,
next: CreatorLandmarkSeedRecord,
): CreatorLandmarkSeedRecord {
return {
...current,
...next,
id: next.id || current.id,
name: toText(next.name) || current.name,
purpose: toText(next.purpose) || current.purpose,
mood: toText(next.mood) || current.mood,
secret: toText(next.secret) || current.secret,
locked:
typeof next.locked === 'boolean' ? next.locked : Boolean(current.locked),
};
}
export function createEmptyCreatorIntentRecord(
sourceMode: CustomWorldCreatorInputMode = 'freeform',
): CustomWorldCreatorIntentRecord {
return {
sourceMode,
rawSettingText: '',
worldHook: '',
themeKeywords: [],
toneDirectives: [],
playerPremise: '',
openingSituation: '',
coreConflicts: [],
keyFactions: [],
keyCharacters: [],
keyLandmarks: [],
iconicElements: [],
forbiddenDirectives: [],
};
}
export function normalizeCreatorIntentRecord(
value: unknown,
fallbackMode: CustomWorldCreatorInputMode = 'freeform',
): CustomWorldCreatorIntentRecord | null {
if (!value || typeof value !== 'object') {
return null;
}
const item = value as Record<string, unknown>;
const sourceMode =
item.sourceMode === 'card' || item.sourceMode === 'freeform'
? item.sourceMode
: fallbackMode;
const rawSettingText = toText(item.rawSettingText);
const worldHook = toText(item.worldHook);
const playerPremise = toText(item.playerPremise);
const openingSituation = toText(item.openingSituation);
const themeKeywords = toStringArray(item.themeKeywords, 8);
const toneDirectives = toStringArray(item.toneDirectives, 8);
const coreConflicts = toStringArray(item.coreConflicts, 6);
const iconicElements = toStringArray(item.iconicElements, 8);
const forbiddenDirectives = toStringArray(item.forbiddenDirectives, 8);
const keyFactions = normalizeAnchorArray(
item.keyFactions,
normalizeCreatorFactionSeed,
6,
);
const keyCharacters = normalizeAnchorArray(
item.keyCharacters,
normalizeCreatorCharacterSeed,
8,
);
const keyLandmarks = normalizeAnchorArray(
item.keyLandmarks,
normalizeCreatorLandmarkSeed,
8,
);
if (
!rawSettingText &&
!worldHook &&
themeKeywords.length === 0 &&
toneDirectives.length === 0 &&
!playerPremise &&
!openingSituation &&
coreConflicts.length === 0 &&
keyFactions.length === 0 &&
keyCharacters.length === 0 &&
keyLandmarks.length === 0 &&
iconicElements.length === 0 &&
forbiddenDirectives.length === 0
) {
return null;
}
return {
sourceMode,
rawSettingText,
worldHook,
themeKeywords,
toneDirectives,
playerPremise,
openingSituation,
coreConflicts,
keyFactions,
keyCharacters,
keyLandmarks,
iconicElements,
forbiddenDirectives,
};
}
export function mergeCreatorIntentRecord(
current: CustomWorldCreatorIntentRecord | null | undefined,
patch: ExtractedCreatorIntentPatch | null | undefined,
fallbackMode: CustomWorldCreatorInputMode = 'freeform',
) {
if (!patch) {
return (
normalizeCreatorIntentRecord(current, fallbackMode) ??
createEmptyCreatorIntentRecord(fallbackMode)
);
}
const base =
normalizeCreatorIntentRecord(current, fallbackMode) ??
createEmptyCreatorIntentRecord(fallbackMode);
const replaceFields = new Set(patch.replaceFields ?? []);
const patchIntent =
normalizeCreatorIntentRecord(
{
sourceMode: base.sourceMode,
...patch,
},
base.sourceMode,
) ?? createEmptyCreatorIntentRecord(base.sourceMode);
return {
...base,
rawSettingText: replaceFields.has('rawSettingText')
? toText(patchIntent.rawSettingText) || base.rawSettingText
: mergeNarrativeText(base.rawSettingText, patchIntent.rawSettingText),
worldHook: toText(patchIntent.worldHook) || base.worldHook,
themeKeywords: replaceFields.has('themeKeywords')
? [...patchIntent.themeKeywords]
: mergeStringArray(base.themeKeywords, patchIntent.themeKeywords, 8),
toneDirectives: replaceFields.has('toneDirectives')
? [...patchIntent.toneDirectives]
: mergeStringArray(base.toneDirectives, patchIntent.toneDirectives, 8),
playerPremise: toText(patchIntent.playerPremise) || base.playerPremise,
openingSituation:
toText(patchIntent.openingSituation) || base.openingSituation,
coreConflicts: replaceFields.has('coreConflicts')
? [...patchIntent.coreConflicts]
: mergeStringArray(base.coreConflicts, patchIntent.coreConflicts, 6),
keyFactions: replaceFields.has('keyFactions')
? [...patchIntent.keyFactions]
: mergeSeedArray(
base.keyFactions,
patchIntent.keyFactions,
6,
mergeFactionSeed,
),
keyCharacters: replaceFields.has('keyCharacters')
? [...patchIntent.keyCharacters]
: mergeSeedArray(
base.keyCharacters,
patchIntent.keyCharacters,
8,
mergeCharacterSeed,
),
keyLandmarks: replaceFields.has('keyLandmarks')
? [...patchIntent.keyLandmarks]
: mergeSeedArray(
base.keyLandmarks,
patchIntent.keyLandmarks,
8,
mergeLandmarkSeed,
),
iconicElements: replaceFields.has('iconicElements')
? [...patchIntent.iconicElements]
: mergeStringArray(base.iconicElements, patchIntent.iconicElements, 8),
forbiddenDirectives: replaceFields.has('forbiddenDirectives')
? [...patchIntent.forbiddenDirectives]
: mergeStringArray(
base.forbiddenDirectives,
patchIntent.forbiddenDirectives,
8,
),
} satisfies CustomWorldCreatorIntentRecord;
}
export function hasMeaningfulCreatorIntentRecord(
intent: CustomWorldCreatorIntentRecord | null | undefined,
) {
return Boolean(
intent &&
(intent.rawSettingText ||
intent.worldHook ||
intent.themeKeywords.length > 0 ||
intent.toneDirectives.length > 0 ||
intent.playerPremise ||
intent.openingSituation ||
intent.coreConflicts.length > 0 ||
intent.keyFactions.length > 0 ||
intent.keyCharacters.length > 0 ||
intent.keyLandmarks.length > 0 ||
intent.iconicElements.length > 0 ||
intent.forbiddenDirectives.length > 0),
);
}
function buildAnchorLine(label: string, content: string) {
return content ? `${label}${content}` : '';
}
export function buildCreatorIntentDisplayText(
intent: CustomWorldCreatorIntentRecord | null | undefined,
) {
if (!hasMeaningfulCreatorIntentRecord(intent)) {
return '';
}
const lines = [
intent?.worldHook ? `世界一句话:${intent.worldHook}` : '',
buildAnchorLine('玩家身份', intent?.playerPremise || ''),
buildAnchorLine('开局处境', intent?.openingSituation || ''),
buildAnchorLine('核心冲突', intent?.coreConflicts.join('、') || ''),
buildAnchorLine(
'主题气质',
[...(intent?.themeKeywords ?? []), ...(intent?.toneDirectives ?? [])]
.filter(Boolean)
.join('、'),
),
buildAnchorLine('标志性要素', intent?.iconicElements.join('、') || ''),
].filter(Boolean);
return lines.join('\n');
}
export function buildDraftTitleFromIntent(
intent: CustomWorldCreatorIntentRecord | null | undefined,
) {
return (
clampText(intent?.worldHook || '', 24) ||
clampText(intent?.rawSettingText || '', 24) ||
'未命名草稿'
);
}
export function buildDraftSummaryFromIntent(
intent: CustomWorldCreatorIntentRecord | null | undefined,
) {
const summary = buildCreatorIntentDisplayText(intent);
if (summary) {
return clampText(summary.replace(/\n+/gu, ' · '), 180);
}
return (
clampText(intent?.rawSettingText || '', 180) || '还在收集你的世界锚点。'
);
}
export function buildAnchorPackFromIntent(
intent: CustomWorldCreatorIntentRecord | null | undefined,
options: {
completedKeys?: string[];
missingKeys?: string[];
} = {},
) {
return {
worldSummary: clampText(
intent?.worldHook || intent?.rawSettingText || '',
96,
),
creatorIntentSummary: clampText(buildDraftSummaryFromIntent(intent), 180),
completedKeys: [...(options.completedKeys ?? [])],
missingKeys: [...(options.missingKeys ?? [])],
keyCharacterAnchors:
intent?.keyCharacters.map((entry) => ({
id: entry.id,
name: entry.name || '未命名关键人物',
summary: clampText(
[entry.role, entry.relationToPlayer, entry.hiddenHook]
.filter(Boolean)
.join(''),
60,
),
})) ?? [],
motifDirectives: [
...(intent?.themeKeywords ?? []),
...(intent?.toneDirectives ?? []),
...(intent?.iconicElements ?? []),
].slice(0, 12),
};
}
function findSentenceByPattern(text: string, pattern: RegExp) {
return splitSentences(text).find((sentence) => pattern.test(sentence)) ?? '';
}
function extractAfterCue(text: string, cues: string[]) {
const sentences = splitSentences(text);
for (const sentence of sentences) {
for (const cue of cues) {
const index = sentence.indexOf(cue);
if (index < 0) {
continue;
}
const candidate = sentence
.slice(index + cue.length)
.replace(/^[:,]+/u, '')
.trim();
if (candidate) {
return candidate;
}
}
}
return '';
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
}
function isExplicitRewrite(text: string, cues: string[]) {
const rewritePattern = /(||||||)/u;
return cues.some((cue) => {
const escapedCue = escapeRegExp(cue);
return (
new RegExp(
`${escapedCue}[^。!?;\\n]{0,28}${rewritePattern.source}`,
'u',
).test(text) ||
new RegExp(
`${rewritePattern.source}[^。!?;\\n]{0,28}${escapedCue}`,
'u',
).test(text)
);
});
}
function extractWorldHook(text: string, contextText: string) {
const explicit =
extractAfterCue(text, ['世界一句话', '核心幻想', '一句话概括', '一句话']) ||
extractAfterCue(text, ['这个世界', '整体设定', '世界设定']);
if (explicit) {
return clampText(explicit, 72);
}
const firstSentence = splitSentences(contextText)[0] ?? '';
if (firstSentence.length >= 8 && !META_MESSAGE_PATTERN.test(firstSentence)) {
return clampText(firstSentence, 72);
}
return '';
}
function extractThemeKeywords(text: string) {
const explicitSource = extractAfterCue(text, [
'主题关键词',
'关键词',
'主题',
'题材',
]).replace(
/(?:|,).*(|||||||||).*/u,
'',
);
const explicit = splitList(explicitSource);
const inferred = THEME_LEXICON.filter((entry) => text.includes(entry));
return [...new Set([...explicit, ...inferred])].slice(0, 8);
}
function extractToneDirectives(text: string) {
const explicit = splitList(
extractAfterCue(text, ['气质', '风格', '基调', '氛围', '风味']),
);
const inferred = TONE_LEXICON.filter((entry) => text.includes(entry));
return [...new Set([...explicit, ...inferred])].slice(0, 8);
}
function extractPlayerPremise(text: string) {
const explicit =
extractAfterCue(text, [
'玩家是',
'玩家身份是',
'主角是',
'你扮演',
'玩家身份',
]) || findSentenceByPattern(text, /(||||)/u);
return clampText(explicit, 96);
}
function extractOpeningSituation(text: string) {
const explicit =
extractAfterCue(text, [
'开局是',
'开局',
'故事开场',
'开场',
'一开始',
'起始',
]) || findSentenceByPattern(text, /(|||||)/u);
return clampText(explicit, 96);
}
function extractCoreConflicts(text: string) {
const explicit = splitList(
extractAfterCue(text, ['核心冲突', '冲突', '危机', '主要矛盾']),
6,
);
const inferred = splitSentences(text)
.filter((sentence) =>
/(||||||||||)/u.test(
sentence,
),
)
.map((sentence) => clampText(sentence, 72));
return [...new Set([...explicit, ...inferred])].slice(0, 6);
}
function extractForbiddenDirectives(text: string) {
return splitSentences(text)
.filter((sentence) => /(||||)/u.test(sentence))
.map((sentence) =>
clampText(
sentence.replace(/^(||||)/u, '').trim() ||
sentence,
48,
),
)
.filter(Boolean)
.slice(0, 8);
}
function extractIconicElements(text: string) {
const explicit = splitList(
extractAfterCue(text, [
'标志性元素',
'标志性要素',
'标志元素',
'视觉符号',
'核心意象',
'一眼能认出来的设定',
]),
);
return explicit.slice(0, 8);
}
function extractCharacterName(sentence: string) {
const matchers = [
/([A-Za-z0-9\u4e00-\u9fa5·-]{2,12})/u,
/([A-Za-z0-9\u4e00-\u9fa5·-]{2,12})/u,
/([A-Za-z0-9\u4e00-\u9fa5·-]{2,12})(?:||)/u,
];
for (const matcher of matchers) {
const matched = sentence.match(matcher);
const candidate = toText(matched?.[1]);
if (
candidate &&
!['玩家', '主角', '世界', '故事', '开局', '气质'].includes(candidate)
) {
return candidate;
}
}
return '';
}
function extractRelationToPlayer(sentence: string) {
const explicit = sentence.match(
/([^]+|[^]+|[^]+|[^]+)/u,
);
if (explicit?.[1]) {
return clampText(explicit[1], 48);
}
const relationKeyword = RELATIONSHIP_TERMS.find((entry) =>
sentence.includes(entry),
);
return relationKeyword ?? '';
}
function extractHiddenHook(sentence: string) {
const explicit = sentence.match(
/([^]+|[^]+|线[^]+|[^]+|[^]+|[^]+)/u,
);
return clampText(toText(explicit?.[1]), 64);
}
function extractRole(sentence: string, name: string) {
if (!name) {
return '';
}
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
const matcher = new RegExp(`${escapedName}(?:是|作为|担任)([^,。;]+)`, 'u');
const matched = sentence.match(matcher);
return clampText(toText(matched?.[1]), 48);
}
function extractCharacterSeeds(text: string) {
const candidateSentences = splitSentences(text).filter((sentence) =>
/(||||宿||||||||)/u.test(
sentence,
),
);
return candidateSentences
.map((sentence, index) => {
const name = extractCharacterName(sentence);
const relationToPlayer = extractRelationToPlayer(sentence);
const hiddenHook = extractHiddenHook(sentence);
const role = extractRole(sentence, name);
if (!name && !role && !relationToPlayer && !hiddenHook) {
return null;
}
return {
id: createSeedId(
'creator-character',
name || role || hiddenHook,
index,
),
name,
role,
publicMask: '',
hiddenHook,
relationToPlayer,
notes: '',
} satisfies CreatorCharacterSeedRecord;
})
.filter((entry): entry is CreatorCharacterSeedRecord => Boolean(entry))
.slice(0, 3);
}
function shouldAppendRawSettingText(text: string) {
return text.length >= 8 && !META_MESSAGE_PATTERN.test(text);
}
export function extractCreatorIntentPatch(params: {
currentIntent: CustomWorldCreatorIntentRecord | null | undefined;
latestUserMessage: string;
recentMessages?: string[];
}) {
const currentIntent =
normalizeCreatorIntentRecord(params.currentIntent) ??
createEmptyCreatorIntentRecord('freeform');
const latestUserMessage = toText(params.latestUserMessage);
const recentMessages = (params.recentMessages ?? [])
.map((entry) => toText(entry))
.filter(Boolean)
.slice(-10);
const contextText = [...recentMessages, latestUserMessage].join('\n');
if (!latestUserMessage) {
return {} satisfies ExtractedCreatorIntentPatch;
}
const patch: ExtractedCreatorIntentPatch = {};
const markReplace = (
field: NonNullable<ExtractedCreatorIntentPatch['replaceFields']>[number],
) => {
patch.replaceFields = [...new Set([...(patch.replaceFields ?? []), field])];
};
if (shouldAppendRawSettingText(latestUserMessage)) {
patch.rawSettingText = latestUserMessage;
}
const worldHook = extractWorldHook(
latestUserMessage,
currentIntent.worldHook ? latestUserMessage : contextText,
);
if (worldHook) {
patch.worldHook = worldHook;
if (
isExplicitRewrite(latestUserMessage, [
'世界一句话',
'核心幻想',
'一句话概括',
'这个世界',
'世界设定',
])
) {
markReplace('worldHook');
}
}
const themeKeywords = extractThemeKeywords(latestUserMessage);
if (themeKeywords.length > 0) {
patch.themeKeywords = themeKeywords;
if (
isExplicitRewrite(latestUserMessage, [
'主题关键词',
'关键词',
'主题',
'题材',
])
) {
markReplace('themeKeywords');
}
}
const toneDirectives = extractToneDirectives(latestUserMessage);
if (toneDirectives.length > 0) {
patch.toneDirectives = toneDirectives;
if (
isExplicitRewrite(latestUserMessage, ['气质', '风格', '基调', '氛围'])
) {
markReplace('toneDirectives');
}
}
const playerPremise = extractPlayerPremise(latestUserMessage);
if (playerPremise) {
patch.playerPremise = playerPremise;
if (
isExplicitRewrite(latestUserMessage, ['玩家', '玩家身份', '主角', '身份'])
) {
markReplace('playerPremise');
}
}
const openingSituation = extractOpeningSituation(latestUserMessage);
if (openingSituation) {
patch.openingSituation = openingSituation;
if (
isExplicitRewrite(latestUserMessage, ['开局', '故事开场', '开场', '起始'])
) {
markReplace('openingSituation');
}
}
const coreConflicts = extractCoreConflicts(latestUserMessage);
if (coreConflicts.length > 0) {
patch.coreConflicts = coreConflicts;
if (
isExplicitRewrite(latestUserMessage, [
'核心冲突',
'冲突',
'危机',
'主要矛盾',
])
) {
markReplace('coreConflicts');
}
}
const keyCharacters = extractCharacterSeeds(latestUserMessage);
if (keyCharacters.length > 0) {
patch.keyCharacters = keyCharacters;
if (
isExplicitRewrite(latestUserMessage, [
'关键人物',
'关键角色',
'人物',
'角色',
])
) {
markReplace('keyCharacters');
}
}
const iconicElements = extractIconicElements(latestUserMessage);
if (iconicElements.length > 0) {
patch.iconicElements = iconicElements;
if (
isExplicitRewrite(latestUserMessage, [
'标志性元素',
'标志性要素',
'标志元素',
'视觉符号',
'核心意象',
])
) {
markReplace('iconicElements');
}
}
const forbiddenDirectives = extractForbiddenDirectives(latestUserMessage);
if (forbiddenDirectives.length > 0) {
patch.forbiddenDirectives = forbiddenDirectives;
if (
isExplicitRewrite(latestUserMessage, ['禁忌', '禁止事项', '不要', '避免'])
) {
markReplace('forbiddenDirectives');
}
}
return patch;
}