Rework story engine flow and reorganize project docs
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { createSceneNpcMonstersFromEncounters } from '../data/hostileNpcs';
|
||||
import { createSceneHostileNpcsFromEncounters } from '../data/hostileNpcs';
|
||||
import {
|
||||
buildEncounterFromSceneNpc,
|
||||
getScenePresetById,
|
||||
@@ -13,13 +13,17 @@ import {
|
||||
AIResponse,
|
||||
Character,
|
||||
CharacterChatTurn,
|
||||
CustomWorldCreatorIntent,
|
||||
CustomWorldGenerationMode,
|
||||
CustomWorldProfile,
|
||||
Encounter,
|
||||
SceneEncounterResult,
|
||||
SceneMonster,
|
||||
SceneHostileNpc,
|
||||
SceneNpc,
|
||||
StoryMoment,
|
||||
StoryOption,
|
||||
ThemePack,
|
||||
WorldStoryGraph,
|
||||
WorldType,
|
||||
} from '../types';
|
||||
import {
|
||||
@@ -45,6 +49,8 @@ import {
|
||||
CharacterChatTargetStatus,
|
||||
} from './characterChatPrompt';
|
||||
import {
|
||||
buildCustomWorldActorNarrativeProfileBatchJsonRepairPrompt,
|
||||
buildCustomWorldActorNarrativeProfileBatchPrompt,
|
||||
buildCustomWorldFrameworkJsonRepairPrompt,
|
||||
buildCustomWorldFrameworkPrompt,
|
||||
buildCustomWorldLandmarkNetworkBatchJsonRepairPrompt,
|
||||
@@ -57,6 +63,10 @@ import {
|
||||
buildCustomWorldRoleOutlineBatchJsonRepairPrompt,
|
||||
buildCustomWorldRoleOutlineBatchPrompt,
|
||||
buildCustomWorldSceneImagePrompt,
|
||||
buildCustomWorldStoryGraphJsonRepairPrompt,
|
||||
buildCustomWorldStoryGraphPrompt,
|
||||
buildCustomWorldThemePackJsonRepairPrompt,
|
||||
buildCustomWorldThemePackPrompt,
|
||||
type CustomWorldGenerationFramework,
|
||||
type CustomWorldGenerationLandmarkOutline,
|
||||
type CustomWorldGenerationRoleBatchStage,
|
||||
@@ -73,6 +83,12 @@ import {
|
||||
validateGeneratedCustomWorldProfile,
|
||||
} from './customWorld';
|
||||
import { buildExpandedCustomWorldProfile } from './customWorldBuilder';
|
||||
import {
|
||||
buildCustomWorldAnchorPackFromIntent,
|
||||
buildCustomWorldCreatorIntentGenerationText,
|
||||
deriveCustomWorldLockStateFromIntent,
|
||||
hasMeaningfulCustomWorldCreatorIntent,
|
||||
} from './customWorldCreatorIntent';
|
||||
import {
|
||||
CUSTOM_WORLD_REQUEST_TIMEOUT_MS as CLIENT_CUSTOM_WORLD_REQUEST_TIMEOUT_MS,
|
||||
isLlmConnectivityError as isLlmConnectivityErrorFromClient,
|
||||
@@ -94,6 +110,18 @@ import {
|
||||
NPC_RECRUIT_DIALOGUE_SYSTEM_PROMPT,
|
||||
SYSTEM_PROMPT,
|
||||
} from './prompt';
|
||||
import {
|
||||
buildFallbackActorNarrativeProfile,
|
||||
normalizeActorNarrativeProfile,
|
||||
} from './storyEngine/actorNarrativeProfile';
|
||||
import {
|
||||
buildThemePackFromWorldProfile,
|
||||
normalizeThemePack,
|
||||
} from './storyEngine/themePack';
|
||||
import {
|
||||
buildFallbackWorldStoryGraph,
|
||||
normalizeWorldStoryGraph,
|
||||
} from './storyEngine/worldStoryGraph';
|
||||
|
||||
export type {
|
||||
StoryGenerationContext,
|
||||
@@ -169,6 +197,20 @@ const CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS = [
|
||||
total: 1,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
id: 'theme-pack',
|
||||
label: '题材适配层',
|
||||
detail: '提炼制度词汇、禁忌词与命名范式。',
|
||||
total: 1,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
id: 'story-graph',
|
||||
label: '世界线程图谱',
|
||||
detail: '补出明线、暗线、旧伤与意象母题。',
|
||||
total: 1,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
id: 'playable-outline',
|
||||
label: '可扮演角色骨架',
|
||||
@@ -245,6 +287,18 @@ const CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS = [
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'playable-profile',
|
||||
label: '可扮演角色叙事档案',
|
||||
detail: '为可扮演角色生成首遇面具、当前压力和暗线钩子。',
|
||||
total: MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
|
||||
weight: Math.max(
|
||||
1,
|
||||
Math.ceil(
|
||||
MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT / CUSTOM_WORLD_PLAYABLE_BATCH_SIZE,
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'story-narrative',
|
||||
label: '场景角色叙事',
|
||||
@@ -255,6 +309,18 @@ const CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS = [
|
||||
Math.ceil(MIN_CUSTOM_WORLD_STORY_NPC_COUNT / CUSTOM_WORLD_STORY_BATCH_SIZE),
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'story-profile',
|
||||
label: '场景角色叙事档案',
|
||||
detail: '为场景角色生成首遇面具、当前压力和暗线钩子。',
|
||||
total: MIN_CUSTOM_WORLD_STORY_NPC_COUNT,
|
||||
weight: Math.max(
|
||||
1,
|
||||
Math.ceil(
|
||||
MIN_CUSTOM_WORLD_STORY_NPC_COUNT / CUSTOM_WORLD_STORY_BATCH_SIZE,
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'story-dossier',
|
||||
label: '场景角色档案',
|
||||
@@ -305,6 +371,16 @@ export interface GenerateCustomWorldProfileOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface GenerateCustomWorldProfileInput {
|
||||
settingText: string;
|
||||
creatorIntent?: CustomWorldCreatorIntent | null;
|
||||
generationMode?: CustomWorldGenerationMode;
|
||||
}
|
||||
|
||||
const FAST_CUSTOM_WORLD_PLAYABLE_COUNT = 3;
|
||||
const FAST_CUSTOM_WORLD_STORY_COUNT = 8;
|
||||
const FAST_CUSTOM_WORLD_LANDMARK_COUNT = 4;
|
||||
|
||||
class CustomWorldGenerationAbortedError extends Error {
|
||||
constructor(message = '世界生成已中断。') {
|
||||
super(message);
|
||||
@@ -341,6 +417,60 @@ function normalizeApiErrorMessage(
|
||||
return responseText;
|
||||
}
|
||||
|
||||
function resolveCustomWorldGenerationInput(
|
||||
input: string | GenerateCustomWorldProfileInput,
|
||||
): {
|
||||
settingText: string;
|
||||
generationSeedText: string;
|
||||
creatorIntent: CustomWorldCreatorIntent | null;
|
||||
generationMode: CustomWorldGenerationMode;
|
||||
} {
|
||||
if (typeof input === 'string') {
|
||||
return {
|
||||
settingText: input.trim(),
|
||||
generationSeedText: input.trim(),
|
||||
creatorIntent: null as CustomWorldCreatorIntent | null,
|
||||
generationMode: 'full' as CustomWorldGenerationMode,
|
||||
};
|
||||
}
|
||||
|
||||
const normalizedSettingText = input.settingText.trim();
|
||||
const creatorIntent = input.creatorIntent ?? null;
|
||||
const generationSeedText =
|
||||
creatorIntent && hasMeaningfulCustomWorldCreatorIntent(creatorIntent)
|
||||
? buildCustomWorldCreatorIntentGenerationText(creatorIntent)
|
||||
: normalizedSettingText;
|
||||
|
||||
return {
|
||||
settingText: normalizedSettingText,
|
||||
generationSeedText: generationSeedText.trim(),
|
||||
creatorIntent,
|
||||
generationMode: input.generationMode === 'fast'
|
||||
? ('fast' as const)
|
||||
: ('full' as const),
|
||||
};
|
||||
}
|
||||
|
||||
function getCustomWorldGenerationTargets(
|
||||
generationMode: CustomWorldGenerationMode,
|
||||
) {
|
||||
if (generationMode === 'fast') {
|
||||
return {
|
||||
playableCount: FAST_CUSTOM_WORLD_PLAYABLE_COUNT,
|
||||
storyCount: FAST_CUSTOM_WORLD_STORY_COUNT,
|
||||
landmarkCount: FAST_CUSTOM_WORLD_LANDMARK_COUNT,
|
||||
generationStatus: 'key_only' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
playableCount: MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
|
||||
storyCount: MIN_CUSTOM_WORLD_STORY_NPC_COUNT,
|
||||
landmarkCount: MIN_CUSTOM_WORLD_LANDMARK_COUNT,
|
||||
generationStatus: 'complete' as const,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeJsonLikeText(text: string) {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) {
|
||||
@@ -493,6 +623,12 @@ function getCustomWorldGenerationStageIdForRoleExpansion(
|
||||
return stage === 'narrative' ? 'story-narrative' : 'story-dossier';
|
||||
}
|
||||
|
||||
function getCustomWorldGenerationStageIdForActorProfile(
|
||||
roleType: CustomWorldGenerationRoleBatchType,
|
||||
): CustomWorldGenerationStageId {
|
||||
return roleType === 'playable' ? 'playable-profile' : 'story-profile';
|
||||
}
|
||||
|
||||
function throwIfCustomWorldGenerationAborted(signal?: AbortSignal) {
|
||||
if (!signal?.aborted) {
|
||||
return;
|
||||
@@ -952,6 +1088,154 @@ async function expandCustomWorldRoleEntries<
|
||||
return mergedEntries;
|
||||
}
|
||||
|
||||
async function generateCustomWorldThemePackWithAi(params: {
|
||||
framework: CustomWorldGenerationFramework;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const { framework, signal } = params;
|
||||
const fallback = buildThemePackFromWorldProfile({
|
||||
...framework,
|
||||
templateWorldType:
|
||||
framework.templateWorldType === WorldType.XIANXIA
|
||||
? WorldType.XIANXIA
|
||||
: WorldType.WUXIA,
|
||||
});
|
||||
const raw = await requestCustomWorldJsonStage({
|
||||
userPrompt: buildCustomWorldThemePackPrompt({ framework }),
|
||||
debugLabel: 'custom-world-theme-pack',
|
||||
repairPromptBuilder: (responseText) =>
|
||||
buildCustomWorldThemePackJsonRepairPrompt({ responseText }),
|
||||
repairDebugLabel: 'custom-world-theme-pack-json-repair',
|
||||
emptyResponseMessage: '自定义世界 ThemePack 生成失败:模型没有返回有效内容。',
|
||||
signal,
|
||||
});
|
||||
|
||||
return normalizeThemePack(raw, fallback);
|
||||
}
|
||||
|
||||
async function generateCustomWorldStoryGraphWithAi(params: {
|
||||
framework: CustomWorldGenerationFramework;
|
||||
themePack: ThemePack;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const { framework, themePack, signal } = params;
|
||||
const profileSeed = buildExpandedCustomWorldProfile(
|
||||
buildCustomWorldRawProfileFromFramework(framework),
|
||||
framework.settingText,
|
||||
);
|
||||
const fallback = buildFallbackWorldStoryGraph(
|
||||
{
|
||||
...profileSeed,
|
||||
themePack,
|
||||
},
|
||||
themePack,
|
||||
);
|
||||
const raw = await requestCustomWorldJsonStage({
|
||||
userPrompt: buildCustomWorldStoryGraphPrompt({
|
||||
framework,
|
||||
themePack,
|
||||
}),
|
||||
debugLabel: 'custom-world-story-graph',
|
||||
repairPromptBuilder: (responseText) =>
|
||||
buildCustomWorldStoryGraphJsonRepairPrompt({ responseText }),
|
||||
repairDebugLabel: 'custom-world-story-graph-json-repair',
|
||||
emptyResponseMessage: '自定义世界 StoryGraph 生成失败:模型没有返回有效内容。',
|
||||
signal,
|
||||
});
|
||||
|
||||
return normalizeWorldStoryGraph(raw, fallback);
|
||||
}
|
||||
|
||||
async function expandCustomWorldActorNarrativeProfiles<
|
||||
T extends MergeableCustomWorldRoleEntry,
|
||||
>(params: {
|
||||
framework: CustomWorldGenerationFramework;
|
||||
roleType: CustomWorldGenerationRoleBatchType;
|
||||
baseEntries: T[];
|
||||
batchSize: number;
|
||||
themePack: ThemePack;
|
||||
storyGraph: WorldStoryGraph;
|
||||
reporter?: CustomWorldGenerationReporter;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
const {
|
||||
framework,
|
||||
roleType,
|
||||
baseEntries,
|
||||
batchSize,
|
||||
themePack,
|
||||
storyGraph,
|
||||
reporter = createCustomWorldGenerationReporter(),
|
||||
signal,
|
||||
} = params;
|
||||
const roleBatchSource = baseEntries;
|
||||
const roleLabel = roleType === 'playable' ? '可扮演角色' : '场景角色';
|
||||
const stageId = getCustomWorldGenerationStageIdForActorProfile(roleType);
|
||||
const plannedBatchCount = Math.max(1, Math.ceil(roleBatchSource.length / batchSize));
|
||||
let mergedEntries = baseEntries.map((entry) => ({ ...entry })) as T[];
|
||||
let processedCount = 0;
|
||||
|
||||
for (const [batchIndex, roleBatch] of chunkArray(roleBatchSource, batchSize).entries()) {
|
||||
throwIfCustomWorldGenerationAborted(signal);
|
||||
reporter.update(stageId, processedCount, {
|
||||
phaseDetail: `正在补充${roleLabel}叙事档案,已完成 ${processedCount}/${roleBatchSource.length}。`,
|
||||
batchLabel: `第 ${batchIndex + 1} / ${plannedBatchCount} 批`,
|
||||
});
|
||||
const stageRaw = await requestCustomWorldJsonStage({
|
||||
userPrompt: buildCustomWorldActorNarrativeProfileBatchPrompt({
|
||||
framework,
|
||||
roleType,
|
||||
roleBatch: roleBatch as Array<Record<string, unknown>>,
|
||||
themePack,
|
||||
storyGraph,
|
||||
}),
|
||||
debugLabel: `custom-world-${roleType}-actor-profile-batch-${batchIndex + 1}`,
|
||||
repairPromptBuilder: (responseText) =>
|
||||
buildCustomWorldActorNarrativeProfileBatchJsonRepairPrompt({
|
||||
responseText,
|
||||
roleType,
|
||||
expectedNames: roleBatch.map((role) => getNamedRecordKey(role.name)),
|
||||
}),
|
||||
repairDebugLabel: `custom-world-${roleType}-actor-profile-batch-${batchIndex + 1}-json-repair`,
|
||||
emptyResponseMessage: `自定义世界${roleLabel}叙事档案批次 ${batchIndex + 1} 生成失败:模型没有返回有效内容。`,
|
||||
signal,
|
||||
});
|
||||
|
||||
mergedEntries = mergeRoleBatchDetails(
|
||||
mergedEntries,
|
||||
toRecordArray(
|
||||
stageRaw && typeof stageRaw === 'object'
|
||||
? (stageRaw as Record<string, unknown>)[
|
||||
roleType === 'playable' ? 'playableNpcs' : 'storyNpcs'
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
processedCount = Math.min(roleBatchSource.length, processedCount + roleBatch.length);
|
||||
reporter.update(stageId, processedCount, {
|
||||
phaseDetail: `正在补充${roleLabel}叙事档案,已完成 ${processedCount}/${roleBatchSource.length}。`,
|
||||
batchLabel: `第 ${batchIndex + 1} / ${plannedBatchCount} 批`,
|
||||
});
|
||||
}
|
||||
|
||||
return mergedEntries.map((entry) => {
|
||||
const item = entry as Record<string, unknown>;
|
||||
const fallbackProfile = buildFallbackActorNarrativeProfile(
|
||||
entry as unknown as CustomWorldProfile['storyNpcs'][number],
|
||||
storyGraph,
|
||||
themePack,
|
||||
);
|
||||
|
||||
return {
|
||||
...entry,
|
||||
narrativeProfile: normalizeActorNarrativeProfile(
|
||||
item.narrativeProfile,
|
||||
fallbackProfile,
|
||||
),
|
||||
} as T;
|
||||
});
|
||||
}
|
||||
|
||||
async function parseCustomWorldStageResponseJson(params: {
|
||||
responseText: string;
|
||||
repairPrompt: string;
|
||||
@@ -1060,7 +1344,7 @@ async function requestCustomWorldJsonStage(params: {
|
||||
function buildFunctionContext(
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
): FunctionAvailabilityContext {
|
||||
return {
|
||||
@@ -1089,17 +1373,8 @@ function normalizeEncounterResult(
|
||||
const kind = typeof item.kind === 'string' ? item.kind.trim() : '';
|
||||
|
||||
if (kind === 'monster') {
|
||||
const rawMonsterIds = Array.isArray(item.monsterIds) ? item.monsterIds : [];
|
||||
const fallbackHostileNpc =
|
||||
scene?.npcs.find(
|
||||
(npc: SceneNpc) =>
|
||||
isHostileSceneNpc(npc) &&
|
||||
rawMonsterIds.some(
|
||||
(monsterId) =>
|
||||
typeof monsterId === 'string' &&
|
||||
npc.monsterPresetId === monsterId,
|
||||
),
|
||||
) ?? scene?.npcs.find((npc: SceneNpc) => isHostileSceneNpc(npc));
|
||||
scene?.npcs.find((npc: SceneNpc) => isHostileSceneNpc(npc));
|
||||
|
||||
return fallbackHostileNpc
|
||||
? { kind: 'npc', npcId: fallbackHostileNpc.id }
|
||||
@@ -1128,7 +1403,7 @@ function normalizeEncounterResult(
|
||||
|
||||
function buildEncounterDrivenResolution(
|
||||
worldType: WorldType,
|
||||
inputMonsters: SceneMonster[],
|
||||
inputMonsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
encounter: SceneEncounterResult | undefined,
|
||||
) {
|
||||
@@ -1148,7 +1423,7 @@ function buildEncounterDrivenResolution(
|
||||
);
|
||||
if (sceneNpc?.monsterPresetId && isHostileSceneNpc(sceneNpc)) {
|
||||
return {
|
||||
monsters: createSceneNpcMonstersFromEncounters(
|
||||
monsters: createSceneHostileNpcsFromEncounters(
|
||||
worldType,
|
||||
[buildEncounterFromSceneNpc(sceneNpc, context.playerX)],
|
||||
context.playerX,
|
||||
@@ -1176,7 +1451,7 @@ function resolveOptionsFromFunctionIds(
|
||||
items: RawOptionItem[],
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
): StoryOption[] {
|
||||
const functionContext = buildFunctionContext(
|
||||
@@ -1305,7 +1580,7 @@ function resolveOptionsFromOptionCatalog(
|
||||
function getFallbackOptions(
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
): StoryOption[] {
|
||||
const functionContext = buildFunctionContext(
|
||||
@@ -1329,7 +1604,7 @@ function getFallbackOptions(
|
||||
function buildOfflineResponse(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
choice?: string,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
@@ -1391,7 +1666,7 @@ function normalizeResponse(
|
||||
raw: unknown,
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): AIResponse {
|
||||
@@ -1483,7 +1758,7 @@ async function requestCompletion(
|
||||
userPrompt: string,
|
||||
worldType: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): Promise<AIResponse> {
|
||||
@@ -1584,10 +1859,16 @@ export async function generateCustomWorldSceneImage({
|
||||
}
|
||||
|
||||
export async function generateCustomWorldProfile(
|
||||
settingText: string,
|
||||
input: string | GenerateCustomWorldProfileInput,
|
||||
options: GenerateCustomWorldProfileOptions = {},
|
||||
): Promise<CustomWorldProfile> {
|
||||
const normalizedSettingText = settingText.trim();
|
||||
const {
|
||||
settingText: normalizedSettingText,
|
||||
generationSeedText,
|
||||
creatorIntent,
|
||||
generationMode,
|
||||
} = resolveCustomWorldGenerationInput(input);
|
||||
const generationTargets = getCustomWorldGenerationTargets(generationMode);
|
||||
const reporter = createCustomWorldGenerationReporter(options.onProgress);
|
||||
const signal = options.signal;
|
||||
|
||||
@@ -1597,7 +1878,7 @@ export async function generateCustomWorldProfile(
|
||||
phaseDetail: '正在解析你的设定文本,准备搭建世界框架。',
|
||||
});
|
||||
const frameworkRaw = await requestCustomWorldJsonStage({
|
||||
userPrompt: buildCustomWorldFrameworkPrompt(normalizedSettingText),
|
||||
userPrompt: buildCustomWorldFrameworkPrompt(generationSeedText),
|
||||
debugLabel: 'custom-world-framework',
|
||||
repairPromptBuilder: buildCustomWorldFrameworkJsonRepairPrompt,
|
||||
repairDebugLabel: 'custom-world-framework-json-repair',
|
||||
@@ -1607,7 +1888,7 @@ export async function generateCustomWorldProfile(
|
||||
const frameworkBase = {
|
||||
...normalizeCustomWorldGenerationFramework(
|
||||
frameworkRaw,
|
||||
normalizedSettingText,
|
||||
generationSeedText,
|
||||
),
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
@@ -1616,6 +1897,16 @@ export async function generateCustomWorldProfile(
|
||||
reporter.complete('framework', {
|
||||
phaseDetail: `世界框架已确定,基础模板锚定为${frameworkBase.templateWorldType === WorldType.WUXIA ? '武侠' : '仙侠'}。`,
|
||||
});
|
||||
reporter.begin('theme-pack', {
|
||||
phaseDetail: '正在提炼题材适配层词汇与命名范式。',
|
||||
});
|
||||
const themePack = await generateCustomWorldThemePackWithAi({
|
||||
framework: frameworkBase,
|
||||
signal,
|
||||
});
|
||||
reporter.complete('theme-pack', {
|
||||
phaseDetail: `题材适配层已完成,当前题材包为“${themePack.displayName}”。`,
|
||||
});
|
||||
|
||||
reporter.begin('playable-outline', {
|
||||
phaseDetail: '正在生成可扮演角色骨架。',
|
||||
@@ -1624,7 +1915,7 @@ export async function generateCustomWorldProfile(
|
||||
(await generateCustomWorldRoleOutlineEntries({
|
||||
framework: frameworkBase,
|
||||
roleType: 'playable',
|
||||
totalCount: MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
|
||||
totalCount: generationTargets.playableCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_PLAYABLE_OUTLINE_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
@@ -1644,7 +1935,7 @@ export async function generateCustomWorldProfile(
|
||||
(await generateCustomWorldRoleOutlineEntries({
|
||||
framework: frameworkWithPlayable,
|
||||
roleType: 'story',
|
||||
totalCount: MIN_CUSTOM_WORLD_STORY_NPC_COUNT,
|
||||
totalCount: generationTargets.storyCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_STORY_OUTLINE_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
@@ -1663,7 +1954,7 @@ export async function generateCustomWorldProfile(
|
||||
const landmarkSeeds =
|
||||
(await generateCustomWorldLandmarkSeedEntries({
|
||||
framework: frameworkWithStory,
|
||||
totalCount: MIN_CUSTOM_WORLD_LANDMARK_COUNT,
|
||||
totalCount: generationTargets.landmarkCount,
|
||||
batchSize: CUSTOM_WORLD_FRAMEWORK_LANDMARK_SEED_BATCH_SIZE,
|
||||
reporter,
|
||||
signal,
|
||||
@@ -1696,7 +1987,20 @@ export async function generateCustomWorldProfile(
|
||||
...frameworkWithStory,
|
||||
landmarks,
|
||||
} satisfies CustomWorldGenerationFramework;
|
||||
validateCustomWorldGenerationFramework(framework);
|
||||
if (generationMode === 'full') {
|
||||
validateCustomWorldGenerationFramework(framework);
|
||||
}
|
||||
reporter.begin('story-graph', {
|
||||
phaseDetail: '正在生成世界线程、旧伤与意象母题。',
|
||||
});
|
||||
const storyGraph = await generateCustomWorldStoryGraphWithAi({
|
||||
framework,
|
||||
themePack,
|
||||
signal,
|
||||
});
|
||||
reporter.complete('story-graph', {
|
||||
phaseDetail: `世界线程图谱已完成,当前可见线程 ${storyGraph.visibleThreads.length} 条,暗线 ${storyGraph.hiddenThreads.length} 条。`,
|
||||
});
|
||||
|
||||
const baseRawProfile = buildCustomWorldRawProfileFromFramework(framework);
|
||||
reporter.begin('playable-narrative', {
|
||||
@@ -1722,6 +2026,52 @@ export async function generateCustomWorldProfile(
|
||||
reporter,
|
||||
signal,
|
||||
});
|
||||
const profileSeed = buildExpandedCustomWorldProfile(
|
||||
{
|
||||
...baseRawProfile,
|
||||
playableNpcs: mergedPlayableNpcs,
|
||||
storyNpcs: mergedStoryNpcs,
|
||||
themePack,
|
||||
storyGraph,
|
||||
creatorIntent,
|
||||
anchorPack: buildCustomWorldAnchorPackFromIntent(creatorIntent),
|
||||
generationMode,
|
||||
generationStatus: generationTargets.generationStatus,
|
||||
},
|
||||
generationSeedText,
|
||||
);
|
||||
reporter.begin('playable-profile', {
|
||||
phaseDetail: '正在补充可扮演角色的叙事档案。',
|
||||
});
|
||||
const playableNpcsWithNarrativeProfile = await expandCustomWorldActorNarrativeProfiles({
|
||||
framework,
|
||||
roleType: 'playable',
|
||||
baseEntries: profileSeed.playableNpcs.map((npc) => ({ ...npc })),
|
||||
batchSize: CUSTOM_WORLD_PLAYABLE_BATCH_SIZE,
|
||||
themePack,
|
||||
storyGraph,
|
||||
reporter,
|
||||
signal,
|
||||
});
|
||||
reporter.complete('playable-profile', {
|
||||
phaseDetail: `可扮演角色叙事档案已完成,共 ${playableNpcsWithNarrativeProfile.length} 名。`,
|
||||
});
|
||||
reporter.begin('story-profile', {
|
||||
phaseDetail: '正在补充场景角色的叙事档案。',
|
||||
});
|
||||
const storyNpcsWithNarrativeProfile = await expandCustomWorldActorNarrativeProfiles({
|
||||
framework,
|
||||
roleType: 'story',
|
||||
baseEntries: profileSeed.storyNpcs.map((npc) => ({ ...npc })),
|
||||
batchSize: CUSTOM_WORLD_STORY_BATCH_SIZE,
|
||||
themePack,
|
||||
storyGraph,
|
||||
reporter,
|
||||
signal,
|
||||
});
|
||||
reporter.complete('story-profile', {
|
||||
phaseDetail: `场景角色叙事档案已完成,共 ${storyNpcsWithNarrativeProfile.length} 名。`,
|
||||
});
|
||||
|
||||
reporter.begin('finalize', {
|
||||
phaseDetail: '正在归档世界并做完整性校验。',
|
||||
@@ -1730,17 +2080,33 @@ export async function generateCustomWorldProfile(
|
||||
const profile = buildExpandedCustomWorldProfile(
|
||||
{
|
||||
...baseRawProfile,
|
||||
playableNpcs: mergedPlayableNpcs,
|
||||
storyNpcs: mergedStoryNpcs,
|
||||
playableNpcs: playableNpcsWithNarrativeProfile,
|
||||
storyNpcs: storyNpcsWithNarrativeProfile,
|
||||
themePack,
|
||||
storyGraph,
|
||||
creatorIntent,
|
||||
anchorPack: buildCustomWorldAnchorPackFromIntent(creatorIntent),
|
||||
generationMode,
|
||||
generationStatus: generationTargets.generationStatus,
|
||||
},
|
||||
normalizedSettingText,
|
||||
generationSeedText,
|
||||
);
|
||||
validateGeneratedCustomWorldProfile(profile);
|
||||
if (generationMode === 'full') {
|
||||
validateGeneratedCustomWorldProfile(profile);
|
||||
}
|
||||
reporter.complete('finalize', {
|
||||
phaseDetail: `世界“${profile.name}”已完成归档。`,
|
||||
});
|
||||
return {
|
||||
...profile,
|
||||
settingText: normalizedSettingText || profile.settingText,
|
||||
creatorIntent,
|
||||
anchorPack:
|
||||
profile.anchorPack ?? buildCustomWorldAnchorPackFromIntent(creatorIntent),
|
||||
lockState:
|
||||
profile.lockState ?? deriveCustomWorldLockStateFromIntent(creatorIntent),
|
||||
generationMode,
|
||||
generationStatus: generationTargets.generationStatus,
|
||||
items: [],
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -1904,7 +2270,7 @@ export async function generateCharacterPanelChatSummary(
|
||||
export async function generateInitialStory(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
context: StoryGenerationContext,
|
||||
requestOptions: StoryRequestOptions = {},
|
||||
): Promise<AIResponse> {
|
||||
@@ -1944,7 +2310,7 @@ export async function generateInitialStory(
|
||||
export async function generateNextStep(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
history: StoryMoment[],
|
||||
choice: string,
|
||||
context: StoryGenerationContext,
|
||||
@@ -1987,7 +2353,7 @@ export async function streamNpcChatDialogue(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
encounter: Encounter,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
history: StoryMoment[],
|
||||
context: StoryGenerationContext,
|
||||
topic: string,
|
||||
@@ -2028,7 +2394,7 @@ export async function streamNpcRecruitDialogue(
|
||||
world: WorldType,
|
||||
character: Character,
|
||||
encounter: Encounter,
|
||||
monsters: SceneMonster[],
|
||||
monsters: SceneHostileNpc[],
|
||||
history: StoryMoment[],
|
||||
context: StoryGenerationContext,
|
||||
invitationText: string,
|
||||
|
||||
Reference in New Issue
Block a user