Implement scene-based chapter quest progression
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -101,6 +101,7 @@ import {
|
||||
parseJsonResponseText as parseJsonResponseTextFromParser,
|
||||
parseLineListContent as parseLineListContentFromParser,
|
||||
} from './llmParsers';
|
||||
import { hasMixedNarrativeLanguage } from './narrativeLanguage';
|
||||
import {
|
||||
buildNpcRecruitDialoguePrompt,
|
||||
buildStrictNpcChatDialoguePrompt,
|
||||
@@ -146,6 +147,11 @@ const CUSTOM_WORLD_JSON_REPAIR_SYSTEM_PROMPT = `你是 JSON 修复器。
|
||||
你的唯一任务是把它修复成能被 JSON.parse 直接解析的单个 JSON 对象。
|
||||
不要输出 Markdown、代码块、解释、注释或额外文字。
|
||||
尽量保留原始语义,只修复格式问题;必要时可以补齐缺失的引号、逗号、括号、数组闭合或缺失字段。`;
|
||||
const STORY_LANGUAGE_REPAIR_SYSTEM_PROMPT = `你是 RPG 中文叙事文本修复器。
|
||||
你会收到一个已经解析过的剧情 JSON 对象。
|
||||
你的唯一任务是把 storyText 和 options[].actionText 中的英文句子、中英混杂句式、英文解释改写成自然中文。
|
||||
必须保持 JSON 结构、encounter、options 数量、options 顺序以及每个 functionId 完全不变。
|
||||
只输出一个 JSON 对象,不要输出 Markdown、代码块、解释、注释或额外文字。`;
|
||||
const CUSTOM_WORLD_GENERATION_JSON_ONLY_SYSTEM_PROMPT = `你是严格的自定义世界 JSON 生成器。
|
||||
只输出一个 JSON 对象,不要输出 Markdown、代码块、解释或额外文字。`;
|
||||
const CUSTOM_WORLD_FRAMEWORK_PLAYABLE_OUTLINE_BATCH_SIZE = 5;
|
||||
@@ -1447,6 +1453,15 @@ function buildEncounterDrivenResolution(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSafeGeneratedActionText(actionText: string | undefined) {
|
||||
const trimmed = actionText?.trim();
|
||||
if (!trimmed || hasMixedNarrativeLanguage(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function resolveOptionsFromFunctionIds(
|
||||
items: RawOptionItem[],
|
||||
worldType: WorldType,
|
||||
@@ -1463,7 +1478,11 @@ function resolveOptionsFromFunctionIds(
|
||||
|
||||
return items
|
||||
.map((item) =>
|
||||
resolveFunctionOption(item.functionId, functionContext, item.actionText),
|
||||
resolveFunctionOption(
|
||||
item.functionId,
|
||||
functionContext,
|
||||
resolveSafeGeneratedActionText(item.actionText),
|
||||
),
|
||||
)
|
||||
.filter(Boolean) as StoryOption[];
|
||||
}
|
||||
@@ -1525,7 +1544,7 @@ function resolveOptionsFromProvidedOptions(
|
||||
if (!matchedOption) return;
|
||||
consumedOptions.add(matchedOption);
|
||||
|
||||
const rewrittenText = item.actionText?.trim();
|
||||
const rewrittenText = resolveSafeGeneratedActionText(item.actionText);
|
||||
resolved.push({
|
||||
...cloneStoryOption(matchedOption),
|
||||
actionText: rewrittenText || matchedOption.actionText,
|
||||
@@ -1566,7 +1585,7 @@ function resolveOptionsFromOptionCatalog(
|
||||
const matchedOption = bucket?.shift();
|
||||
if (!matchedOption) return;
|
||||
|
||||
const rewrittenText = item.actionText?.trim();
|
||||
const rewrittenText = resolveSafeGeneratedActionText(item.actionText);
|
||||
resolved.push({
|
||||
...cloneStoryOption(matchedOption),
|
||||
actionText: rewrittenText || matchedOption.actionText,
|
||||
@@ -1662,6 +1681,112 @@ function buildOfflineResponse(
|
||||
};
|
||||
}
|
||||
|
||||
function buildStoryLanguageRepairPrompt(response: AIResponse) {
|
||||
return [
|
||||
'请把下面 JSON 中的 storyText 与 options[].actionText 修复为自然中文。',
|
||||
'只改写叙事和选项文案,不要改变 encounter、options 数量、顺序或 functionId。',
|
||||
'如果英文只是角色名或地点名,可以保留名字本体;其余英文句子、英文解释和中英混杂表达都必须改成中文。',
|
||||
JSON.stringify(
|
||||
{
|
||||
storyText: response.storyText,
|
||||
encounter: response.encounter ?? null,
|
||||
options: response.options.map((option) => ({
|
||||
functionId: option.functionId,
|
||||
actionText: option.actionText,
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
].join('\n\n');
|
||||
}
|
||||
|
||||
function needsStoryLanguageRepair(response: AIResponse) {
|
||||
return hasMixedNarrativeLanguage(response.storyText);
|
||||
}
|
||||
|
||||
function buildStoryLanguageFallbackText(
|
||||
context: StoryGenerationContext,
|
||||
inBattle: boolean,
|
||||
) {
|
||||
if (inBattle) {
|
||||
return '敌意仍压在眼前,战斗局势还没有真正松开。';
|
||||
}
|
||||
|
||||
if (context.encounterName) {
|
||||
return `${context.encounterName}的态度与周围气氛都出现了新的变化,你需要立刻判断接下来如何应对。`;
|
||||
}
|
||||
|
||||
return `${context.sceneName || '眼前区域'}里的气氛又有了新的变化,你需要继续判断下一步。`;
|
||||
}
|
||||
|
||||
function finalizeStoryNarrativeLanguage(
|
||||
response: AIResponse,
|
||||
context: StoryGenerationContext,
|
||||
inBattle: boolean,
|
||||
): AIResponse {
|
||||
if (!needsStoryLanguageRepair(response)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
storyText: buildStoryLanguageFallbackText(context, inBattle),
|
||||
};
|
||||
}
|
||||
|
||||
async function repairStoryNarrativeLanguage(
|
||||
response: AIResponse,
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions,
|
||||
) {
|
||||
const responseBattleState = buildEncounterDrivenResolution(
|
||||
worldType,
|
||||
monsters,
|
||||
context,
|
||||
response.encounter,
|
||||
).inBattle;
|
||||
|
||||
if (!needsStoryLanguageRepair(response)) {
|
||||
return finalizeStoryNarrativeLanguage(response, context, responseBattleState);
|
||||
}
|
||||
|
||||
try {
|
||||
const repairedContent = await requestChatMessageContent(
|
||||
STORY_LANGUAGE_REPAIR_SYSTEM_PROMPT,
|
||||
buildStoryLanguageRepairPrompt(response),
|
||||
{
|
||||
debugLabel: 'story-language-repair',
|
||||
},
|
||||
);
|
||||
const repairedResponse = normalizeResponse(
|
||||
parseJsonResponseTextFromParser(repairedContent),
|
||||
worldType,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
const repairedBattleState = buildEncounterDrivenResolution(
|
||||
worldType,
|
||||
monsters,
|
||||
context,
|
||||
repairedResponse.encounter,
|
||||
).inBattle;
|
||||
return finalizeStoryNarrativeLanguage(
|
||||
repairedResponse,
|
||||
context,
|
||||
repairedBattleState,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('Failed to repair mixed-language story response:', error);
|
||||
return finalizeStoryNarrativeLanguage(response, context, responseBattleState);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeResponse(
|
||||
raw: unknown,
|
||||
worldType: WorldType,
|
||||
@@ -1766,7 +1891,7 @@ async function requestCompletion(
|
||||
debugLabel: 'story-completion',
|
||||
});
|
||||
|
||||
return normalizeResponse(
|
||||
const response = normalizeResponse(
|
||||
parseJsonResponseTextFromParser(content),
|
||||
worldType,
|
||||
character,
|
||||
@@ -1774,6 +1899,15 @@ async function requestCompletion(
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
|
||||
return repairStoryNarrativeLanguage(
|
||||
response,
|
||||
worldType,
|
||||
character,
|
||||
monsters,
|
||||
context,
|
||||
requestOptions,
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateCustomWorldSceneImage({
|
||||
@@ -1895,7 +2029,7 @@ export async function generateCustomWorldProfile(
|
||||
landmarks: [],
|
||||
} satisfies CustomWorldGenerationFramework;
|
||||
reporter.complete('framework', {
|
||||
phaseDetail: `世界框架已确定,基础模板锚定为${frameworkBase.templateWorldType === WorldType.WUXIA ? '武侠' : '仙侠'}。`,
|
||||
phaseDetail: '世界框架已确定,开始围绕你的设定继续编译题材层与关键对象。',
|
||||
});
|
||||
reporter.begin('theme-pack', {
|
||||
phaseDetail: '正在提炼题材适配层词汇与命名范式。',
|
||||
|
||||
Reference in New Issue
Block a user