Fix DashScope env loading for scene image generation

This commit is contained in:
2026-04-06 15:01:15 +08:00
parent fcd8d727b0
commit d678929064
23 changed files with 4943 additions and 138 deletions

View File

@@ -58,8 +58,10 @@ import {
buildCustomWorldRoleOutlineBatchPrompt,
buildCustomWorldSceneImagePrompt,
type CustomWorldGenerationFramework,
type CustomWorldGenerationLandmarkOutline,
type CustomWorldGenerationRoleBatchStage,
type CustomWorldGenerationRoleBatchType,
type CustomWorldGenerationRoleOutline,
DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT,
MIN_CUSTOM_WORLD_LANDMARK_COUNT,
MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
@@ -106,7 +108,7 @@ type RawOptionItem = {
type MergeableCustomWorldRoleEntry = {
name: string;
} & Record<string, unknown>;
};
const CUSTOM_WORLD_SCENE_IMAGE_API_BASE_URL =
import.meta.env.VITE_SCENE_IMAGE_PROXY_BASE_URL ||
@@ -159,6 +161,157 @@ export interface CustomWorldSceneImageResult {
actualPrompt?: string;
}
const CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS = [
{
id: 'framework',
label: '世界框架',
detail: '解析设定文本,确定世界主题、主目标与基础模板。',
total: 1,
weight: 1,
},
{
id: 'playable-outline',
label: '可扮演角色骨架',
detail: '先生成可扮演角色名单与核心定位。',
total: MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
weight: Math.max(
1,
Math.ceil(
MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT /
CUSTOM_WORLD_FRAMEWORK_PLAYABLE_OUTLINE_BATCH_SIZE,
),
),
},
{
id: 'story-outline',
label: '场景角色骨架',
detail: '补齐世界里的关键角色与势力关系。',
total: MIN_CUSTOM_WORLD_STORY_NPC_COUNT,
weight: Math.max(
1,
Math.ceil(
MIN_CUSTOM_WORLD_STORY_NPC_COUNT /
CUSTOM_WORLD_FRAMEWORK_STORY_OUTLINE_BATCH_SIZE,
),
),
},
{
id: 'landmark-seed',
label: '场景骨架',
detail: '生成地标、区域描述与危险等级。',
total: MIN_CUSTOM_WORLD_LANDMARK_COUNT,
weight: Math.max(
1,
Math.ceil(
MIN_CUSTOM_WORLD_LANDMARK_COUNT /
CUSTOM_WORLD_FRAMEWORK_LANDMARK_SEED_BATCH_SIZE,
),
),
},
{
id: 'landmark-network',
label: '场景连接',
detail: '建立场景连接关系与场景内角色分布。',
total: MIN_CUSTOM_WORLD_LANDMARK_COUNT,
weight: Math.max(
1,
Math.ceil(
MIN_CUSTOM_WORLD_LANDMARK_COUNT /
CUSTOM_WORLD_FRAMEWORK_LANDMARK_NETWORK_BATCH_SIZE,
),
),
},
{
id: 'playable-narrative',
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: 'playable-dossier',
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: '场景角色叙事',
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: '场景角色档案',
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: 'finalize',
label: '归档世界',
detail: '整理最终世界档案并做完整性校验。',
total: 1,
weight: 1,
},
] as const;
export type CustomWorldGenerationStageId =
(typeof CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS)[number]['id'];
export interface CustomWorldGenerationStep {
id: CustomWorldGenerationStageId;
label: string;
detail: string;
completed: number;
total: number;
status: 'pending' | 'active' | 'completed';
}
export interface CustomWorldGenerationProgress {
phaseId: CustomWorldGenerationStageId;
phaseLabel: string;
phaseDetail: string;
batchLabel?: string;
overallProgress: number;
completedWeight: number;
totalWeight: number;
elapsedMs: number;
estimatedRemainingMs: number | null;
activeStepIndex: number;
steps: CustomWorldGenerationStep[];
}
export interface GenerateCustomWorldProfileOptions {
onProgress?: (progress: CustomWorldGenerationProgress) => void;
signal?: AbortSignal;
}
class CustomWorldGenerationAbortedError extends Error {
constructor(message = '世界生成已中断。') {
super(message);
this.name = 'CustomWorldGenerationAbortedError';
}
}
function normalizeApiErrorMessage(
responseText: string,
fallbackMessage: string,
@@ -312,14 +465,212 @@ function appendUniqueNamedEntries<T extends MergeableCustomWorldRoleEntry>(
return merged;
}
const CUSTOM_WORLD_GENERATION_STAGE_MAP = new Map(
CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS.map((stage) => [stage.id, stage]),
);
const CUSTOM_WORLD_GENERATION_TOTAL_WEIGHT =
CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS.reduce(
(sum, stage) => sum + stage.weight,
0,
);
function getCustomWorldGenerationStageIdForRoleOutline(
roleType: CustomWorldGenerationRoleBatchType,
): CustomWorldGenerationStageId {
return roleType === 'playable' ? 'playable-outline' : 'story-outline';
}
function getCustomWorldGenerationStageIdForRoleExpansion(
roleType: CustomWorldGenerationRoleBatchType,
stage: CustomWorldGenerationRoleBatchStage,
): CustomWorldGenerationStageId {
if (roleType === 'playable') {
return stage === 'narrative'
? 'playable-narrative'
: 'playable-dossier';
}
return stage === 'narrative' ? 'story-narrative' : 'story-dossier';
}
function throwIfCustomWorldGenerationAborted(signal?: AbortSignal) {
if (!signal?.aborted) {
return;
}
throw signal.reason instanceof Error
? signal.reason
: new CustomWorldGenerationAbortedError();
}
function isCustomWorldGenerationAbortLikeError(error: unknown) {
return (
error instanceof CustomWorldGenerationAbortedError ||
(typeof DOMException !== 'undefined' &&
error instanceof DOMException &&
error.name === 'AbortError')
);
}
function createCustomWorldGenerationReporter(
onProgress?: GenerateCustomWorldProfileOptions['onProgress'],
) {
const startedAt = performance.now();
const completedByStage = Object.fromEntries(
CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS.map((stage) => [stage.id, 0]),
) as Record<CustomWorldGenerationStageId, number>;
const emit = (
stageId: CustomWorldGenerationStageId,
options: Partial<{
completed: number;
phaseDetail: string;
batchLabel: string;
}> = {},
) => {
const stage = CUSTOM_WORLD_GENERATION_STAGE_MAP.get(stageId);
if (!stage) {
return;
}
if (typeof options.completed === 'number') {
completedByStage[stageId] = Math.max(
0,
Math.min(stage.total, options.completed),
);
}
const steps = CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS.map((item) => {
const completed = Math.max(
0,
Math.min(item.total, completedByStage[item.id]),
);
return {
id: item.id,
label: item.label,
detail: item.detail,
completed,
total: item.total,
status:
completed >= item.total
? 'completed'
: item.id === stageId
? 'active'
: 'pending',
} satisfies CustomWorldGenerationStep;
});
const completedWeight = CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS.reduce(
(sum, item) =>
sum +
(completedByStage[item.id] / item.total || 0) * item.weight,
0,
);
const progressFraction =
CUSTOM_WORLD_GENERATION_TOTAL_WEIGHT > 0
? completedWeight / CUSTOM_WORLD_GENERATION_TOTAL_WEIGHT
: 0;
const elapsedMs = Math.max(0, performance.now() - startedAt);
const estimatedRemainingMs =
progressFraction > 0 && progressFraction < 1
? Math.max(
0,
Math.round(elapsedMs / progressFraction - elapsedMs),
)
: progressFraction >= 1
? 0
: null;
onProgress?.({
phaseId: stage.id,
phaseLabel: stage.label,
phaseDetail: options.phaseDetail ?? stage.detail,
batchLabel: options.batchLabel,
overallProgress: Math.max(
0,
Math.min(100, Math.round(progressFraction * 100)),
),
completedWeight,
totalWeight: CUSTOM_WORLD_GENERATION_TOTAL_WEIGHT,
elapsedMs: Math.round(elapsedMs),
estimatedRemainingMs,
activeStepIndex: CUSTOM_WORLD_GENERATION_STAGE_DEFINITIONS.findIndex(
(item) => item.id === stage.id,
),
steps,
});
};
return {
begin(
stageId: CustomWorldGenerationStageId,
options: Partial<{
phaseDetail: string;
batchLabel: string;
}> = {},
) {
emit(stageId, {
completed: completedByStage[stageId],
...options,
});
},
update(
stageId: CustomWorldGenerationStageId,
completed: number,
options: Partial<{
phaseDetail: string;
batchLabel: string;
}> = {},
) {
emit(stageId, {
completed,
...options,
});
},
complete(
stageId: CustomWorldGenerationStageId,
options: Partial<{
phaseDetail: string;
batchLabel: string;
}> = {},
) {
const stage = CUSTOM_WORLD_GENERATION_STAGE_MAP.get(stageId);
if (!stage) {
return;
}
emit(stageId, {
completed: stage.total,
...options,
});
},
};
}
type CustomWorldGenerationReporter = ReturnType<
typeof createCustomWorldGenerationReporter
>;
async function generateCustomWorldRoleOutlineEntries(params: {
framework: CustomWorldGenerationFramework;
roleType: CustomWorldGenerationRoleBatchType;
totalCount: number;
batchSize: number;
reporter?: CustomWorldGenerationReporter;
signal?: AbortSignal;
}) {
const { framework, roleType, totalCount, batchSize } = params;
let mergedEntries: MergeableCustomWorldRoleEntry[] = [];
const {
framework,
roleType,
totalCount,
batchSize,
reporter = createCustomWorldGenerationReporter(),
signal,
} = params;
const stageId = getCustomWorldGenerationStageIdForRoleOutline(roleType);
const plannedBatchCount = Math.max(1, Math.ceil(totalCount / batchSize));
const roleLabel = roleType === 'playable' ? '可扮演角色' : '场景角色';
let mergedEntries: CustomWorldGenerationRoleOutline[] = [];
const maxBatchAttempts = Math.max(2, Math.ceil(totalCount / batchSize) + 2);
for (
@@ -327,7 +678,12 @@ async function generateCustomWorldRoleOutlineEntries(params: {
batchIndex < maxBatchAttempts && mergedEntries.length < totalCount;
batchIndex += 1
) {
throwIfCustomWorldGenerationAborted(signal);
const batchCount = Math.min(batchSize, totalCount - mergedEntries.length);
reporter.update(stageId, mergedEntries.length, {
phaseDetail: `正在生成${roleLabel},已完成 ${mergedEntries.length}/${totalCount}`,
batchLabel: `${Math.min(batchIndex + 1, plannedBatchCount)} / ${plannedBatchCount}`,
});
const batchRaw = await requestCustomWorldJsonStage({
userPrompt: buildCustomWorldRoleOutlineBatchPrompt({
framework,
@@ -345,6 +701,7 @@ async function generateCustomWorldRoleOutlineEntries(params: {
}),
repairDebugLabel: `custom-world-${roleType}-outline-batch-${batchIndex + 1}-json-repair`,
emptyResponseMessage: `自定义世界${roleType === 'playable' ? '可扮演角色' : '场景角色'}名单批次 ${batchIndex + 1} 生成失败:模型没有返回有效内容。`,
signal,
});
mergedEntries = appendUniqueNamedEntries(
@@ -352,6 +709,10 @@ async function generateCustomWorldRoleOutlineEntries(params: {
normalizeCustomWorldGenerationRoleOutlineBatch(batchRaw, roleType),
totalCount,
);
reporter.update(stageId, mergedEntries.length, {
phaseDetail: `正在生成${roleLabel},已完成 ${mergedEntries.length}/${totalCount}`,
batchLabel: `${Math.min(batchIndex + 1, plannedBatchCount)} / ${plannedBatchCount}`,
});
if (batchCount <= 0) {
break;
@@ -365,9 +726,18 @@ async function generateCustomWorldLandmarkSeedEntries(params: {
framework: CustomWorldGenerationFramework;
totalCount: number;
batchSize: number;
reporter?: CustomWorldGenerationReporter;
signal?: AbortSignal;
}) {
const { framework, totalCount, batchSize } = params;
let mergedEntries: MergeableCustomWorldRoleEntry[] = [];
const {
framework,
totalCount,
batchSize,
reporter = createCustomWorldGenerationReporter(),
signal,
} = params;
const plannedBatchCount = Math.max(1, Math.ceil(totalCount / batchSize));
let mergedEntries: CustomWorldGenerationLandmarkOutline[] = [];
const maxBatchAttempts = Math.max(2, Math.ceil(totalCount / batchSize) + 2);
for (
@@ -375,7 +745,12 @@ async function generateCustomWorldLandmarkSeedEntries(params: {
batchIndex < maxBatchAttempts && mergedEntries.length < totalCount;
batchIndex += 1
) {
throwIfCustomWorldGenerationAborted(signal);
const batchCount = Math.min(batchSize, totalCount - mergedEntries.length);
reporter.update('landmark-seed', mergedEntries.length, {
phaseDetail: `正在生成场景骨架,已完成 ${mergedEntries.length}/${totalCount}`,
batchLabel: `${Math.min(batchIndex + 1, plannedBatchCount)} / ${plannedBatchCount}`,
});
const batchRaw = await requestCustomWorldJsonStage({
userPrompt: buildCustomWorldLandmarkSeedBatchPrompt({
framework,
@@ -391,6 +766,7 @@ async function generateCustomWorldLandmarkSeedEntries(params: {
}),
repairDebugLabel: `custom-world-landmark-seed-batch-${batchIndex + 1}-json-repair`,
emptyResponseMessage: `自定义世界场景骨架批次 ${batchIndex + 1} 生成失败:模型没有返回有效内容。`,
signal,
});
mergedEntries = appendUniqueNamedEntries(
@@ -398,6 +774,10 @@ async function generateCustomWorldLandmarkSeedEntries(params: {
normalizeCustomWorldGenerationLandmarkOutlineBatch(batchRaw),
totalCount,
);
reporter.update('landmark-seed', mergedEntries.length, {
phaseDetail: `正在生成场景骨架,已完成 ${mergedEntries.length}/${totalCount}`,
batchLabel: `${Math.min(batchIndex + 1, plannedBatchCount)} / ${plannedBatchCount}`,
});
if (batchCount <= 0) {
break;
@@ -410,16 +790,35 @@ async function generateCustomWorldLandmarkSeedEntries(params: {
async function expandCustomWorldLandmarkNetworkEntries(params: {
framework: CustomWorldGenerationFramework;
storyNpcs: CustomWorldGenerationFramework['storyNpcs'];
baseEntries: MergeableCustomWorldRoleEntry[];
baseEntries: CustomWorldGenerationLandmarkOutline[];
batchSize: number;
reporter?: CustomWorldGenerationReporter;
signal?: AbortSignal;
}) {
const { framework, storyNpcs, baseEntries, batchSize } = params;
const {
framework,
storyNpcs,
baseEntries,
batchSize,
reporter = createCustomWorldGenerationReporter(),
signal,
} = params;
const plannedBatchCount = Math.max(
1,
Math.ceil(framework.landmarks.length / batchSize),
);
let mergedEntries = baseEntries.map((entry) => ({ ...entry }));
let processedCount = 0;
for (const [batchIndex, landmarkBatch] of chunkArray(
framework.landmarks,
batchSize,
).entries()) {
throwIfCustomWorldGenerationAborted(signal);
reporter.update('landmark-network', processedCount, {
phaseDetail: `正在建立场景连接,已完成 ${processedCount}/${framework.landmarks.length}`,
batchLabel: `${batchIndex + 1} / ${plannedBatchCount}`,
});
const batchRaw = await requestCustomWorldJsonStage({
userPrompt: buildCustomWorldLandmarkNetworkBatchPrompt({
framework,
@@ -434,6 +833,7 @@ async function expandCustomWorldLandmarkNetworkEntries(params: {
}),
repairDebugLabel: `custom-world-landmark-network-batch-${batchIndex + 1}-json-repair`,
emptyResponseMessage: `自定义世界场景连接批次 ${batchIndex + 1} 生成失败:模型没有返回有效内容。`,
signal,
});
mergedEntries = mergeRoleBatchDetails(
@@ -442,6 +842,14 @@ async function expandCustomWorldLandmarkNetworkEntries(params: {
(entry) => ({ ...entry }),
),
);
processedCount = Math.min(
framework.landmarks.length,
processedCount + landmarkBatch.length,
);
reporter.update('landmark-network', processedCount, {
phaseDetail: `正在建立场景连接,已完成 ${processedCount}/${framework.landmarks.length}`,
batchLabel: `${batchIndex + 1} / ${plannedBatchCount}`,
});
}
return mergedEntries;
@@ -454,19 +862,45 @@ async function expandCustomWorldRoleEntries<
roleType: CustomWorldGenerationRoleBatchType;
baseEntries: T[];
batchSize: number;
reporter?: CustomWorldGenerationReporter;
signal?: AbortSignal;
}) {
const { framework, roleType, baseEntries, batchSize } = params;
const {
framework,
roleType,
baseEntries,
batchSize,
reporter = createCustomWorldGenerationReporter(),
signal,
} = params;
const roleBatchSource =
roleType === 'playable' ? framework.playableNpcs : framework.storyNpcs;
const roleLabel = roleType === 'playable' ? '可扮演角色' : '场景角色';
let mergedEntries = baseEntries.map((entry) => ({ ...entry })) as T[];
const plannedBatchCount = Math.max(
1,
Math.ceil(roleBatchSource.length / batchSize),
);
const processedByStage: Record<CustomWorldGenerationRoleBatchStage, number> = {
narrative: 0,
dossier: 0,
};
const requestBatchStage = async (
roleBatch: typeof roleBatchSource,
batchIndex: number,
stage: CustomWorldGenerationRoleBatchStage,
) => {
throwIfCustomWorldGenerationAborted(signal);
const stageLabel = stage === 'narrative' ? '叙事设定' : '档案补全';
const stageId = getCustomWorldGenerationStageIdForRoleExpansion(
roleType,
stage,
);
reporter.update(stageId, processedByStage[stage], {
phaseDetail: `正在补充${roleLabel}${stageLabel},已完成 ${processedByStage[stage]}/${roleBatchSource.length}`,
batchLabel: `${batchIndex + 1} / ${plannedBatchCount}`,
});
const stageRaw = await requestCustomWorldJsonStage({
userPrompt: buildCustomWorldRoleBatchPrompt({
framework,
@@ -484,6 +918,7 @@ async function expandCustomWorldRoleEntries<
}),
repairDebugLabel: `custom-world-${roleType}-${stage}-batch-${batchIndex + 1}-json-repair`,
emptyResponseMessage: `自定义世界${roleLabel}批次 ${batchIndex + 1}${stageLabel}生成失败:模型没有返回有效内容。`,
signal,
});
mergedEntries = mergeRoleBatchDetails(
@@ -493,9 +928,17 @@ async function expandCustomWorldRoleEntries<
? (stageRaw as Record<string, unknown>)[
roleType === 'playable' ? 'playableNpcs' : 'storyNpcs'
]
: [],
: []
),
);
processedByStage[stage] = Math.min(
roleBatchSource.length,
processedByStage[stage] + roleBatch.length,
);
reporter.update(stageId, processedByStage[stage], {
phaseDetail: `正在补充${roleLabel}${stageLabel},已完成 ${processedByStage[stage]}/${roleBatchSource.length}`,
batchLabel: `${batchIndex + 1} / ${plannedBatchCount}`,
});
};
for (const [batchIndex, roleBatch] of chunkArray(
@@ -513,8 +956,10 @@ async function parseCustomWorldStageResponseJson(params: {
responseText: string;
repairPrompt: string;
repairDebugLabel: string;
signal?: AbortSignal;
}) {
const { responseText, repairPrompt, repairDebugLabel } = params;
const { responseText, repairPrompt, repairDebugLabel, signal } = params;
throwIfCustomWorldGenerationAborted(signal);
try {
return parseJsonResponseTextFromParser(responseText);
} catch {
@@ -536,9 +981,11 @@ async function parseCustomWorldStageResponseJson(params: {
Math.min(90000, Math.round(CLIENT_CUSTOM_WORLD_REQUEST_TIMEOUT_MS / 2)),
),
debugLabel: repairDebugLabel,
signal,
},
);
throwIfCustomWorldGenerationAborted(signal);
return parseJsonResponseTextFromParser(
sanitizeJsonLikeText(repairedText) || repairedText,
);
@@ -551,6 +998,7 @@ async function requestCustomWorldJsonStage(params: {
repairPromptBuilder: (responseText: string) => string;
repairDebugLabel: string;
emptyResponseMessage: string;
signal?: AbortSignal;
}) {
const {
userPrompt,
@@ -558,6 +1006,7 @@ async function requestCustomWorldJsonStage(params: {
repairPromptBuilder,
repairDebugLabel,
emptyResponseMessage,
signal,
} = params;
const timeoutPlan = [
CLIENT_CUSTOM_WORLD_REQUEST_TIMEOUT_MS,
@@ -569,6 +1018,7 @@ async function requestCustomWorldJsonStage(params: {
for (const [attemptIndex, timeoutMs] of timeoutPlan.entries()) {
try {
throwIfCustomWorldGenerationAborted(signal);
const responseText = await requestPlainTextCompletionFromClient(
CUSTOM_WORLD_GENERATION_JSON_ONLY_SYSTEM_PROMPT,
userPrompt,
@@ -578,6 +1028,7 @@ async function requestCustomWorldJsonStage(params: {
attemptIndex === 0
? debugLabel
: `${debugLabel}-retry-${attemptIndex + 1}`,
signal,
},
);
text = typeof responseText === 'string' ? responseText : '';
@@ -602,6 +1053,7 @@ async function requestCustomWorldJsonStage(params: {
responseText: text,
repairPrompt: repairPromptBuilder(text),
repairDebugLabel,
signal,
});
}
@@ -1133,16 +1585,24 @@ export async function generateCustomWorldSceneImage({
export async function generateCustomWorldProfile(
settingText: string,
options: GenerateCustomWorldProfileOptions = {},
): Promise<CustomWorldProfile> {
const normalizedSettingText = settingText.trim();
const reporter = createCustomWorldGenerationReporter(options.onProgress);
const signal = options.signal;
try {
throwIfCustomWorldGenerationAborted(signal);
reporter.begin('framework', {
phaseDetail: '正在解析你的设定文本,准备搭建世界框架。',
});
const frameworkRaw = await requestCustomWorldJsonStage({
userPrompt: buildCustomWorldFrameworkPrompt(normalizedSettingText),
debugLabel: 'custom-world-framework',
repairPromptBuilder: buildCustomWorldFrameworkJsonRepairPrompt,
repairDebugLabel: 'custom-world-framework-json-repair',
emptyResponseMessage: '自定义世界框架生成失败:模型没有返回有效内容。',
signal,
});
const frameworkBase = {
...normalizeCustomWorldGenerationFramework(
@@ -1153,49 +1613,84 @@ export async function generateCustomWorldProfile(
storyNpcs: [],
landmarks: [],
} satisfies CustomWorldGenerationFramework;
reporter.complete('framework', {
phaseDetail: `世界框架已确定,基础模板锚定为${frameworkBase.templateWorldType === WorldType.WUXIA ? '武侠' : '仙侠'}`,
});
reporter.begin('playable-outline', {
phaseDetail: '正在生成可扮演角色骨架。',
});
const playableNpcs =
(await generateCustomWorldRoleOutlineEntries({
framework: frameworkBase,
roleType: 'playable',
totalCount: MIN_CUSTOM_WORLD_PLAYABLE_NPC_COUNT,
batchSize: CUSTOM_WORLD_FRAMEWORK_PLAYABLE_OUTLINE_BATCH_SIZE,
reporter,
signal,
})) as CustomWorldGenerationFramework['playableNpcs'];
reporter.complete('playable-outline', {
phaseDetail: `可扮演角色骨架已完成,共 ${playableNpcs.length} 名。`,
});
const frameworkWithPlayable = {
...frameworkBase,
playableNpcs,
} satisfies CustomWorldGenerationFramework;
reporter.begin('story-outline', {
phaseDetail: '正在生成场景角色骨架。',
});
const storyNpcs =
(await generateCustomWorldRoleOutlineEntries({
framework: frameworkWithPlayable,
roleType: 'story',
totalCount: MIN_CUSTOM_WORLD_STORY_NPC_COUNT,
batchSize: CUSTOM_WORLD_FRAMEWORK_STORY_OUTLINE_BATCH_SIZE,
reporter,
signal,
})) as CustomWorldGenerationFramework['storyNpcs'];
reporter.complete('story-outline', {
phaseDetail: `场景角色骨架已完成,共 ${storyNpcs.length} 名。`,
});
const frameworkWithStory = {
...frameworkWithPlayable,
storyNpcs,
} satisfies CustomWorldGenerationFramework;
reporter.begin('landmark-seed', {
phaseDetail: '正在生成场景骨架。',
});
const landmarkSeeds =
(await generateCustomWorldLandmarkSeedEntries({
framework: frameworkWithStory,
totalCount: MIN_CUSTOM_WORLD_LANDMARK_COUNT,
batchSize: CUSTOM_WORLD_FRAMEWORK_LANDMARK_SEED_BATCH_SIZE,
reporter,
signal,
})) as CustomWorldGenerationFramework['landmarks'];
reporter.complete('landmark-seed', {
phaseDetail: `场景骨架已完成,共 ${landmarkSeeds.length} 个地标。`,
});
const frameworkWithLandmarkSeeds = {
...frameworkWithStory,
landmarks: landmarkSeeds,
} satisfies CustomWorldGenerationFramework;
reporter.begin('landmark-network', {
phaseDetail: '正在建立场景连接与场景角色分布。',
});
const landmarks =
(await expandCustomWorldLandmarkNetworkEntries({
framework: frameworkWithLandmarkSeeds,
storyNpcs,
baseEntries: landmarkSeeds,
batchSize: CUSTOM_WORLD_FRAMEWORK_LANDMARK_NETWORK_BATCH_SIZE,
reporter,
signal,
})) as CustomWorldGenerationFramework['landmarks'];
reporter.complete('landmark-network', {
phaseDetail: `场景连接已完成,共整理 ${landmarks.length} 个地标网络。`,
});
const framework = {
...frameworkWithStory,
@@ -1204,19 +1699,34 @@ export async function generateCustomWorldProfile(
validateCustomWorldGenerationFramework(framework);
const baseRawProfile = buildCustomWorldRawProfileFromFramework(framework);
reporter.begin('playable-narrative', {
phaseDetail: '正在补充可扮演角色的叙事设定。',
});
const mergedPlayableNpcs = await expandCustomWorldRoleEntries({
framework,
roleType: 'playable',
baseEntries: baseRawProfile.playableNpcs.map((npc) => ({ ...npc })),
batchSize: CUSTOM_WORLD_PLAYABLE_BATCH_SIZE,
reporter,
signal,
});
reporter.begin('story-narrative', {
phaseDetail: '正在补充场景角色的叙事设定。',
});
const mergedStoryNpcs = await expandCustomWorldRoleEntries({
framework,
roleType: 'story',
baseEntries: baseRawProfile.storyNpcs.map((npc) => ({ ...npc })),
batchSize: CUSTOM_WORLD_STORY_BATCH_SIZE,
reporter,
signal,
});
reporter.begin('finalize', {
phaseDetail: '正在归档世界并做完整性校验。',
});
throwIfCustomWorldGenerationAborted(signal);
const profile = buildExpandedCustomWorldProfile(
{
...baseRawProfile,
@@ -1226,11 +1736,19 @@ export async function generateCustomWorldProfile(
normalizedSettingText,
);
validateGeneratedCustomWorldProfile(profile);
reporter.complete('finalize', {
phaseDetail: `世界“${profile.name}”已完成归档。`,
});
return {
...profile,
items: [],
};
} catch (error) {
if (isCustomWorldGenerationAbortLikeError(error) || signal?.aborted) {
throw error instanceof Error
? error
: new CustomWorldGenerationAbortedError();
}
if (error instanceof SyntaxError) {
throw new Error(
'自定义世界生成失败:模型返回了非严格 JSON且自动修复仍未成功请稍后重试。',