This commit is contained in:
2026-04-21 10:30:12 +08:00
parent ae28dab032
commit 13bc79306f
49 changed files with 3691 additions and 1357 deletions

View File

@@ -198,6 +198,116 @@ function buildRoleAssetSyncResultText(params: {
return `已把「${params.roleName}」的角色资产写回草稿,当前状态:${params.assetStatusLabel}`;
}
function toText(value: unknown) {
return typeof value === 'string' ? value.trim() : '';
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function toRecordArray(value: unknown) {
return Array.isArray(value)
? value.filter((item): item is Record<string, unknown> => isRecord(item))
: [];
}
function cloneJsonRecord<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
function syncRoleAssetsFromResultProfile(params: {
currentRoles: unknown;
resultRoles: unknown;
}) {
const resultRoleById = new Map(
toRecordArray(params.resultRoles).map((role) => [toText(role.id), role]),
);
return toRecordArray(params.currentRoles).map((currentRole) => {
const resultRole = resultRoleById.get(toText(currentRole.id));
if (!resultRole) {
return currentRole;
}
return {
...currentRole,
imageSrc: toText(resultRole.imageSrc) || null,
generatedVisualAssetId: toText(resultRole.generatedVisualAssetId) || null,
generatedAnimationSetId:
toText(resultRole.generatedAnimationSetId) || null,
animationMap: isRecord(resultRole.animationMap)
? cloneJsonRecord(resultRole.animationMap)
: null,
} satisfies Record<string, unknown>;
});
}
function syncLandmarkAssetsFromResultProfile(params: {
currentLandmarks: unknown;
resultLandmarks: unknown;
}) {
const resultLandmarkById = new Map(
toRecordArray(params.resultLandmarks).map((landmark) => [
toText(landmark.id),
landmark,
]),
);
return toRecordArray(params.currentLandmarks).map((currentLandmark) => {
const resultLandmark = resultLandmarkById.get(toText(currentLandmark.id));
if (!resultLandmark) {
return currentLandmark;
}
return {
...currentLandmark,
imageSrc: toText(resultLandmark.imageSrc) || null,
} satisfies Record<string, unknown>;
});
}
function syncSceneChapterAssetsFromResultProfile(params: {
currentSceneChapters: unknown;
resultSceneChapters: unknown;
}) {
const resultSceneChapterBySceneId = new Map(
toRecordArray(params.resultSceneChapters).map((chapter) => [
toText(chapter.sceneId),
chapter,
]),
);
return toRecordArray(params.currentSceneChapters).map((currentChapter) => {
const resultChapter = resultSceneChapterBySceneId.get(
toText(currentChapter.sceneId),
);
if (!resultChapter) {
return currentChapter;
}
const resultActById = new Map(
toRecordArray(resultChapter.acts).map((act) => [toText(act.id), act]),
);
return {
...currentChapter,
acts: toRecordArray(currentChapter.acts).map((currentAct) => {
const resultAct = resultActById.get(toText(currentAct.id));
if (!resultAct) {
return currentAct;
}
return {
...currentAct,
backgroundImageSrc: toText(resultAct.backgroundImageSrc) || null,
backgroundAssetId: toText(resultAct.backgroundAssetId) || null,
} satisfies Record<string, unknown>;
}),
} satisfies Record<string, unknown>;
});
}
function syncResultProfileIntoDraftProfile(params: {
currentDraftProfile: Record<string, unknown> | null | undefined;
resultProfile: CustomWorldProfile;
@@ -215,6 +325,22 @@ function syncResultProfileIntoDraftProfile(params: {
playerGoal: resultProfile.playerGoal,
majorFactions: resultProfile.majorFactions,
coreConflicts: resultProfile.coreConflicts,
playableNpcs: syncRoleAssetsFromResultProfile({
currentRoles: currentDraftProfile.playableNpcs,
resultRoles: resultProfile.playableNpcs,
}),
storyNpcs: syncRoleAssetsFromResultProfile({
currentRoles: currentDraftProfile.storyNpcs,
resultRoles: resultProfile.storyNpcs,
}),
landmarks: syncLandmarkAssetsFromResultProfile({
currentLandmarks: currentDraftProfile.landmarks,
resultLandmarks: resultProfile.landmarks,
}),
sceneChapters: syncSceneChapterAssetsFromResultProfile({
currentSceneChapters: currentDraftProfile.sceneChapters,
resultSceneChapters: resultProfile.sceneChapterBlueprints,
}),
legacyResultProfile: resultProfile as unknown as Record<string, unknown>,
} satisfies Record<string, unknown>;
}

View File

@@ -421,6 +421,162 @@ test('phase4 sync_result_profile keeps existing foundation structure while updat
);
});
test('phase4 sync_result_profile also writes latest role and scene assets back into draft profile', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);
const orchestrator = new CustomWorldAgentOrchestrator(sessionStore, null, {
singleTurnLlmClient: createTestCustomWorldAgentSingleTurnLlmClient(),
});
const userId = 'user-phase4-sync-result-profile-assets';
const session = await createObjectRefiningSession(orchestrator, userId);
const baselineProfile = normalizeFoundationDraftProfile(session.draftProfile)!;
const playableRole = baselineProfile.playableNpcs[0]!;
const storyRole = baselineProfile.storyNpcs[0]!;
const landmark = baselineProfile.landmarks[0]!;
const response = await orchestrator.executeAction(userId, session.sessionId, {
action: 'sync_result_profile',
profile: {
id: `agent-draft-${session.sessionId}`,
settingText: '被海雾吞没的旧航路群岛',
name: '潮雾列岛·结果页精修版',
subtitle: '旧灯塔与失控航路',
summary: '结果页已经把最新图与动作一起确认。 ',
tone: '压抑、潮湿、悬疑',
playerGoal: '查清沉船夜与假航灯的真正操盘者。',
templateWorldType: 'WUXIA',
majorFactions: ['守灯会', '航运公会'],
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
attributeSchema: {
id: 'schema:test',
worldId: 'CUSTOM',
schemaVersion: 1,
schemaName: '测试',
generatedFrom: {
worldType: 'CUSTOM',
worldName: '潮雾列岛·结果页精修版',
settingSummary: '测试',
tone: '测试',
conflictCore: '测试',
},
slots: [],
},
playableNpcs: [
{
id: playableRole.id,
name: playableRole.name,
title: '结果页角色',
role: '关键同行者',
description: '结果页确认的最新角色资产。',
backstory: '测试',
personality: '冷静',
motivation: '验证资产回写',
combatStyle: '观察',
initialAffinity: 12,
relationshipHooks: [],
tags: [],
imageSrc: '/generated/playable/latest-master.png',
generatedVisualAssetId: 'visual-playable-latest',
generatedAnimationSetId: 'anim-playable-latest',
animationMap: {
idle: {
spriteSheetPath: '/generated/playable/idle.png',
},
},
},
],
storyNpcs: [
{
id: storyRole.id,
name: storyRole.name,
title: '结果页场景角色',
role: '场景关键角色',
description: '结果页确认的最新场景角色资产。',
backstory: '测试',
personality: '克制',
motivation: '验证资产回写',
combatStyle: '观察',
initialAffinity: 6,
relationshipHooks: [],
tags: [],
imageSrc: '/generated/story/latest-master.png',
generatedVisualAssetId: 'visual-story-latest',
},
],
items: [],
landmarks: [
{
id: landmark.id,
name: landmark.name,
description: '结果页确认的最新地点图。',
dangerLevel: '中',
sceneNpcIds: [],
connections: [],
imageSrc: '/generated/landmark/latest-scene.png',
},
],
sceneChapterBlueprints: [
{
id: 'scene-chapter-1',
sceneId: landmark.id,
title: '灯塔初章',
summary: '结果页确认最新分幕图。',
linkedThreadIds: [],
linkedLandmarkIds: [landmark.id],
acts: [
{
id: `${landmark.id}-act-1`,
sceneId: landmark.id,
title: '第一幕',
summary: '第一幕',
stageCoverage: ['opening'],
backgroundImageSrc: '/generated/scene/act-1-latest.png',
backgroundAssetId: 'scene-asset-latest',
encounterNpcIds: [],
primaryNpcId: '',
linkedThreadIds: [],
advanceRule: 'after_primary_contact',
actGoal: '验证分幕图回写',
transitionHook: '进入下一幕',
},
],
},
],
generationMode: 'full',
generationStatus: 'complete',
},
});
const operation = await waitForOperation(
orchestrator,
userId,
session.sessionId,
response.operation.operationId,
);
const snapshot = await orchestrator.getSessionSnapshot(userId, session.sessionId);
const profile = normalizeFoundationDraftProfile(snapshot?.draftProfile)!;
const syncedPlayable = profile.playableNpcs.find(
(entry) => entry.id === playableRole.id,
);
const syncedStory = profile.storyNpcs.find((entry) => entry.id === storyRole.id);
const syncedLandmark = profile.landmarks.find((entry) => entry.id === landmark.id);
const syncedSceneAct = profile.sceneChapters[0]?.acts[0];
assert.equal(operation?.status, 'completed');
assert.equal(syncedPlayable?.imageSrc, '/generated/playable/latest-master.png');
assert.equal(syncedPlayable?.generatedVisualAssetId, 'visual-playable-latest');
assert.equal(syncedPlayable?.generatedAnimationSetId, 'anim-playable-latest');
assert.deepEqual(syncedPlayable?.animationMap, {
idle: {
spriteSheetPath: '/generated/playable/idle.png',
},
});
assert.equal(syncedStory?.imageSrc, '/generated/story/latest-master.png');
assert.equal(syncedStory?.generatedVisualAssetId, 'visual-story-latest');
assert.equal(syncedLandmark?.imageSrc, '/generated/landmark/latest-scene.png');
assert.equal(syncedSceneAct?.backgroundImageSrc, '/generated/scene/act-1-latest.png');
assert.equal(syncedSceneAct?.backgroundAssetId, 'scene-asset-latest');
});
test('phase4 generate_characters appends story npcs and updates work summary counts', async () => {
const runtimeRepository = createRuntimeRepositoryStub();
const sessionStore = new CustomWorldAgentSessionStore(runtimeRepository);

View File

@@ -368,10 +368,15 @@ function buildCompatibleSuggestedActions(params: {
record: CustomWorldAgentSessionRecord;
stage: CustomWorldAgentStage;
readiness: CreatorIntentReadiness;
draftProfile: Record<string, unknown>;
}) {
if (params.record.suggestedActions.length > 0) {
return params.record.suggestedActions;
// 旧快照里可能带有“精修对象”的 Agent 建议动作;当前 Agent 对话框只服务八锚点收集。
const compatibleActions = params.record.suggestedActions.filter(
(action) => action.type !== 'refine_focus_target',
);
if (compatibleActions.length > 0) {
return compatibleActions;
}
}
const actions: CustomWorldSuggestedAction[] = [
@@ -384,16 +389,6 @@ function buildCompatibleSuggestedActions(params: {
: '总结当前设定',
},
];
const playableNpcs = Array.isArray(params.draftProfile.playableNpcs)
? params.draftProfile.playableNpcs
: [];
const storyNpcs = Array.isArray(params.draftProfile.storyNpcs)
? params.draftProfile.storyNpcs
: [];
const landmarks = Array.isArray(params.draftProfile.landmarks)
? params.draftProfile.landmarks
: [];
if (params.stage === 'foundation_review' && params.readiness.isReady) {
actions.push({
id: 'draft_foundation',
@@ -403,36 +398,6 @@ function buildCompatibleSuggestedActions(params: {
return actions;
}
if (params.stage === 'object_refining' || params.stage === 'visual_refining') {
const firstCharacter = toRecord([...playableNpcs, ...storyNpcs][0]);
const firstLandmark = toRecord(landmarks[0]);
actions.push({
id: 'refine_world',
type: 'refine_focus_target',
label: '先看世界总卡',
targetId: 'world-foundation',
});
if (firstCharacter) {
actions.push({
id: `refine-character-${toText(firstCharacter.id) || 'seed'}`,
type: 'refine_focus_target',
label: `精修角色:${toText(firstCharacter.name) || '关键角色'}`,
targetId: toText(firstCharacter.id) || null,
});
}
if (firstLandmark) {
actions.push({
id: `refine-landmark-${toText(firstLandmark.id) || 'seed'}`,
type: 'refine_focus_target',
label: `继续补地点:${toText(firstLandmark.name) || '关键地点'}`,
targetId: toText(firstLandmark.id) || null,
});
}
}
return actions;
}
@@ -533,7 +498,6 @@ function applyCompatibility(record: CustomWorldAgentSessionRecord) {
record,
stage,
readiness: creatorIntentReadiness,
draftProfile,
}),
assetCoverage: buildCompatibleAssetCoverage(record, draftProfile),
recommendedReplies: normalizeRecommendedReplies(

View File

@@ -55,7 +55,7 @@ function formatDraftStageLabel(stage: CustomWorldAgentStage) {
if (stage === 'collecting_intent') return '收集世界锚点';
if (stage === 'clarifying') return '补齐关键锚点';
if (stage === 'foundation_review') return '准备整理底稿';
if (stage === 'object_refining') return '精修对象';
if (stage === 'object_refining') return '待完善草稿';
if (stage === 'visual_refining') return '视觉工坊';
if (stage === 'long_tail_review') return '扩展长尾';
if (stage === 'ready_to_publish') return '准备发布';