Integrate role asset studio into custom world agent flow

This commit is contained in:
2026-04-14 20:16:41 +08:00
parent 0981d6ee1b
commit bc2999ffb9
118 changed files with 31211 additions and 1232 deletions

View File

@@ -5,12 +5,15 @@ import type {
CharacterChatSuggestionsRequest,
} from '../../../../packages/shared/src/contracts/story.js';
import { createTestPlayerCharacter } from '../../testFixtures/runtimeCharacter.js';
import { CHARACTER_PANEL_CHAT_SUGGESTION_SYSTEM_PROMPT } from './chatPromptBuilders.js';
import { SYSTEM_PROMPT } from './storyPromptBuilders.js';
import {
generateCharacterChatSuggestionsFromOrchestrator,
} from './chatOrchestrator.js';
import { CHARACTER_PANEL_CHAT_SUGGESTION_SYSTEM_PROMPT } from './chatPromptBuilders.js';
import {
generateCustomWorldProfileFromOrchestrator,
} from './customWorldOrchestrator.js';
import { generateInitialStoryFromOrchestrator } from './storyOrchestrator.js';
import { SYSTEM_PROMPT } from './storyPromptBuilders.js';
type TestStoryContext = Parameters<typeof generateInitialStoryFromOrchestrator>[4];
type TestStoryOption = Awaited<
@@ -191,3 +194,105 @@ test('chat orchestrator builds character suggestion prompts on the server side',
assert.match(capturedPrompts[0]?.userPrompt ?? '', //u);
assert.match(capturedPrompts[0]?.userPrompt ?? '', new RegExp(payload.targetCharacter.name, 'u'));
});
test('custom world orchestrator requests LLM content before compiling the profile', async () => {
const capturedPrompts: Array<{ systemPrompt: string; userPrompt: string }> = [];
const storyNpcNames = Array.from(
{ length: 8 },
(_, index) => `潮灯见证者${index + 1}`,
);
const llmClient = {
requestMessageContent: async ({
systemPrompt,
userPrompt,
}: {
systemPrompt: string;
userPrompt: string;
}) => {
capturedPrompts.push({ systemPrompt, userPrompt });
return JSON.stringify({
name: '潮灯列岛',
subtitle: '雾潮之下',
summary: '旧灯塔、潮雾与沉船盟约纠缠出的列岛冒险。',
tone: '潮湿、悬疑、克制',
playerGoal: '查明潮雾为何吞掉守灯人的名字',
templateWorldType: 'WUXIA',
majorFactions: ['守灯会', '沉船商盟', '潮雾祭司'],
coreConflicts: ['守灯会与沉船商盟争夺航道解释权'],
camp: {
name: '旧灯塔下层',
description: '潮水退去时才露出的临时据点。',
dangerLevel: 'low',
},
playableNpcs: Array.from({ length: 3 }, (_, index) => ({
name: `守灯旅人${index + 1}`,
title: `${index + 1}盏灯`,
role: '守灯同行者',
description: '在潮雾边缘辨认灯火与人声。',
backstory: '曾经守过一座被除名的灯塔。',
personality: '谨慎、沉静、记仇',
motivation: '找回被潮雾吞掉的名字。',
combatStyle: '短刃牵制后借灯火逼退敌人。',
initialAffinity: 18,
relationshipHooks: ['守灯', '旧名'],
tags: ['潮雾', '灯塔'],
})),
storyNpcs: storyNpcNames.map((name, index) => ({
name,
title: `${index + 1}位见证者`,
role: '潮雾见证者',
description: '知道一段被潮水洗掉的航线传闻。',
backstory: '在沉船夜里听见过不该出现的钟声。',
personality: '警觉、克制',
motivation: '确认下一次潮雾会带走谁。',
combatStyle: '先试探再撤入雾中。',
initialAffinity: 6,
relationshipHooks: ['沉船夜', '钟声'],
tags: ['潮雾', '线索'],
})),
landmarks: Array.from({ length: 4 }, (_, index) => ({
name: `潮灯地标${index + 1}`,
description: '潮雾会在这里折回,留下盐痕和旧灯影。',
dangerLevel: index === 0 ? 'medium' : 'high',
sceneNpcNames: storyNpcNames.slice(index, index + 3),
connections: [
{
targetLandmarkName: `潮灯地标${(index + 1) % 4 + 1}`,
relativePosition: 'forward',
summary: '沿潮痕继续前行即可抵达下一处灯影。',
},
],
})),
items: [],
});
},
} as const;
const progressEvents: Array<{ phaseId: string; overallProgress: number }> = [];
const profile = await generateCustomWorldProfileFromOrchestrator(
llmClient as never,
{
settingText: '一个被潮雾与失落列岛切碎的边境世界。',
generationMode: 'fast',
},
{
onProgress: (progress) => {
progressEvents.push({
phaseId: progress.phaseId,
overallProgress: progress.overallProgress,
});
},
},
);
assert.equal(capturedPrompts.length, 1);
assert.match(capturedPrompts[0]?.systemPrompt ?? '', /JSON /u);
assert.match(capturedPrompts[0]?.userPrompt ?? '', /fast/u);
assert.match(capturedPrompts[0]?.userPrompt ?? '', //u);
assert.equal(profile.name, '潮灯列岛');
assert.equal(profile.generationMode, 'fast');
assert.equal(profile.generationStatus, 'key_only');
assert.equal((profile.playableNpcs as unknown[]).length, 3);
assert.ok(progressEvents.some((event) => event.phaseId === 'llm-profile'));
assert.equal(progressEvents.at(-1)?.overallProgress, 100);
});