1
This commit is contained in:
@@ -1 +0,0 @@
|
||||
export { CustomWorldAgentOrchestrator as RpgAgentOrchestrator } from './customWorldAgentOrchestrator.js';
|
||||
@@ -1,5 +0,0 @@
|
||||
export type { CustomWorldAgentSessionRecord as RpgAgentSessionRecord } from './customWorldAgentSessionStore.js';
|
||||
export {
|
||||
CUSTOM_WORLD_AGENT_SESSION_ID_PREFIX as RPG_AGENT_SESSION_ID_PREFIX,
|
||||
CustomWorldAgentSessionStore as RpgAgentSessionStore,
|
||||
} from './customWorldAgentSessionStore.js';
|
||||
@@ -13,7 +13,7 @@ import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js'
|
||||
import { createInMemoryRpgWorldRepositoryPorts } from './customWorldAgentRepositoryTestHelpers.js';
|
||||
import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js';
|
||||
import { createTestCustomWorldAgentSingleTurnLlmClient } from './customWorldAgentTestHelpers.js';
|
||||
import { listCustomWorldWorkSummaries } from './customWorldWorkSummaryService.js';
|
||||
import { RpgWorldWorkSummaryService } from './RpgWorldWorkSummaryService.js';
|
||||
|
||||
async function waitForOperation(
|
||||
orchestrator: CustomWorldAgentOrchestrator,
|
||||
@@ -217,10 +217,10 @@ test('phase2 work summaries compile draft title and summary from creator intent'
|
||||
update.operation.operationId,
|
||||
);
|
||||
|
||||
const items = await listCustomWorldWorkSummaries(userId, {
|
||||
rpgWorldProfiles: rpgWorldProfileRepository,
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const items = await new RpgWorldWorkSummaryService(
|
||||
rpgWorldProfileRepository,
|
||||
sessionStore,
|
||||
).list(userId);
|
||||
const draft = items.find(
|
||||
(item) => item.sessionId === createdSession.sessionId,
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js'
|
||||
import { createInMemoryRpgWorldRepositoryPorts } from './customWorldAgentRepositoryTestHelpers.js';
|
||||
import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js';
|
||||
import { createTestCustomWorldAgentSingleTurnLlmClient } from './customWorldAgentTestHelpers.js';
|
||||
import { listCustomWorldWorkSummaries } from './customWorldWorkSummaryService.js';
|
||||
import { RpgWorldWorkSummaryService } from './RpgWorldWorkSummaryService.js';
|
||||
|
||||
function createAutoAssetTestConfig(testName: string): AppConfig {
|
||||
const projectRoot = fs.mkdtempSync(
|
||||
@@ -368,10 +368,10 @@ test('phase3 work summaries prefer compiled foundation draft fields', async () =
|
||||
response.operation.operationId,
|
||||
);
|
||||
|
||||
const items = await listCustomWorldWorkSummaries(userId, {
|
||||
rpgWorldProfiles: rpgWorldProfileRepository,
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const items = await new RpgWorldWorkSummaryService(
|
||||
rpgWorldProfileRepository,
|
||||
sessionStore,
|
||||
).list(userId);
|
||||
const draft = items.find((item) => item.sessionId === readySession.sessionId);
|
||||
const compiledProfile = normalizeFoundationDraftProfile(
|
||||
(
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CustomWorldAgentOrchestrator } from './customWorldAgentOrchestrator.js'
|
||||
import { createInMemoryRpgWorldRepositoryPorts } from './customWorldAgentRepositoryTestHelpers.js';
|
||||
import { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js';
|
||||
import { createTestCustomWorldAgentSingleTurnLlmClient } from './customWorldAgentTestHelpers.js';
|
||||
import { listCustomWorldWorkSummaries } from './customWorldWorkSummaryService.js';
|
||||
import { RpgWorldWorkSummaryService } from './RpgWorldWorkSummaryService.js';
|
||||
|
||||
async function waitForOperation(
|
||||
orchestrator: CustomWorldAgentOrchestrator,
|
||||
@@ -552,10 +552,10 @@ test('phase4 generate_characters appends story npcs and updates work summary cou
|
||||
[...profile.playableNpcs, ...profile.storyNpcs].map((entry) => entry.id),
|
||||
),
|
||||
].length;
|
||||
const workItems = await listCustomWorldWorkSummaries(userId, {
|
||||
rpgWorldProfiles: rpgWorldProfileRepository,
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const workItems = await new RpgWorldWorkSummaryService(
|
||||
rpgWorldProfileRepository,
|
||||
sessionStore,
|
||||
).list(userId);
|
||||
const draftItem = workItems.find((item) => item.sessionId === session.sessionId);
|
||||
|
||||
assert.equal(operation?.status, 'completed');
|
||||
@@ -641,10 +641,10 @@ test('phase4 work summaries exclude library draft entries after phase3 downgrade
|
||||
'玩家',
|
||||
);
|
||||
|
||||
const workItems = await listCustomWorldWorkSummaries(userId, {
|
||||
rpgWorldProfiles: rpgWorldProfileRepository,
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const workItems = await new RpgWorldWorkSummaryService(
|
||||
rpgWorldProfileRepository,
|
||||
sessionStore,
|
||||
).list(userId);
|
||||
|
||||
assert.ok(workItems.some((item) => item.sessionId === session.sessionId));
|
||||
assert.equal(
|
||||
@@ -688,10 +688,10 @@ test('phase4 work summaries hide published agent sessions from draft lane and ke
|
||||
'玩家',
|
||||
);
|
||||
|
||||
const workItems = await listCustomWorldWorkSummaries(userId, {
|
||||
rpgWorldProfiles: rpgWorldProfileRepository,
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const workItems = await new RpgWorldWorkSummaryService(
|
||||
rpgWorldProfileRepository,
|
||||
sessionStore,
|
||||
).list(userId);
|
||||
const draftItem = workItems.find((item) => item.sessionId === session.sessionId);
|
||||
const publishedItem = workItems.find(
|
||||
(item) => item.profileId === `agent-draft-${session.sessionId}`,
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import type {
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
|
||||
import { normalizeFoundationDraftProfile } from './customWorldAgentDraftCompiler.js';
|
||||
|
||||
export type CustomWorldAgentPublishGateResult = {
|
||||
blockers: CustomWorldAgentSessionSnapshot['qualityFindings'];
|
||||
warnings: CustomWorldAgentSessionSnapshot['qualityFindings'];
|
||||
};
|
||||
|
||||
function buildFinding(params: {
|
||||
id: string;
|
||||
code: string;
|
||||
severity: 'warning' | 'blocker';
|
||||
targetId?: string | null;
|
||||
message: string;
|
||||
}) {
|
||||
return {
|
||||
id: params.id,
|
||||
code: params.code,
|
||||
severity: params.severity,
|
||||
targetId: params.targetId ?? null,
|
||||
message: params.message,
|
||||
} satisfies CustomWorldAgentSessionSnapshot['qualityFindings'][number];
|
||||
}
|
||||
|
||||
export class CustomWorldAgentPublishGateService {
|
||||
evaluate(params: {
|
||||
draftProfile: unknown;
|
||||
qualityFindings: CustomWorldAgentSessionSnapshot['qualityFindings'];
|
||||
}): CustomWorldAgentPublishGateResult {
|
||||
const draftProfile = normalizeFoundationDraftProfile(params.draftProfile);
|
||||
const blockers = params.qualityFindings.filter(
|
||||
(entry) => entry.severity === 'blocker',
|
||||
);
|
||||
const warnings = params.qualityFindings.filter(
|
||||
(entry) => entry.severity === 'warning',
|
||||
);
|
||||
|
||||
if (!draftProfile) {
|
||||
return {
|
||||
blockers: [
|
||||
...blockers,
|
||||
buildFinding({
|
||||
id: 'publish-empty-draft',
|
||||
code: 'publish_empty_draft',
|
||||
severity: 'blocker',
|
||||
message: '当前还没有可发布的世界底稿,请先整理世界骨架。',
|
||||
}),
|
||||
],
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
if ((draftProfile.chapters?.length ?? 0) <= 0) {
|
||||
blockers.push(
|
||||
buildFinding({
|
||||
id: 'publish-missing-main-chapter',
|
||||
code: 'publish_missing_main_chapter',
|
||||
severity: 'blocker',
|
||||
message: '发布前至少需要保留主线第一幕,当前世界还缺少章节草稿。',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const missingRoleVisuals = [
|
||||
...draftProfile.playableNpcs,
|
||||
...draftProfile.storyNpcs,
|
||||
].filter(
|
||||
(entry) =>
|
||||
!entry.generatedVisualAssetId?.trim() ||
|
||||
!entry.generatedAnimationSetId?.trim(),
|
||||
);
|
||||
if (missingRoleVisuals.length > 0) {
|
||||
blockers.push(
|
||||
buildFinding({
|
||||
id: 'publish-role-assets-incomplete',
|
||||
code: 'publish_role_assets_incomplete',
|
||||
severity: 'blocker',
|
||||
targetId: missingRoleVisuals[0]?.id ?? null,
|
||||
message: '仍有角色缺少正式主图或动作资产,发布前需要先补齐。',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!draftProfile.camp?.imageSrc?.trim() ||
|
||||
!(draftProfile.camp as Record<string, unknown> | null)?.generatedSceneAssetId
|
||||
) {
|
||||
blockers.push(
|
||||
buildFinding({
|
||||
id: 'publish-camp-scene-missing',
|
||||
code: 'publish_camp_scene_missing',
|
||||
severity: 'blocker',
|
||||
targetId: draftProfile.camp?.id ?? null,
|
||||
message: '营地还缺少正式场景图资产,发布前需要先确认营地图。',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const missingLandmarkScenes = draftProfile.landmarks.filter((entry) => {
|
||||
const record = entry as Record<string, unknown>;
|
||||
return (
|
||||
!entry.imageSrc?.trim() || !String(record.generatedSceneAssetId ?? '').trim()
|
||||
);
|
||||
});
|
||||
if (missingLandmarkScenes.length > 0) {
|
||||
blockers.push(
|
||||
buildFinding({
|
||||
id: 'publish-landmark-scenes-missing',
|
||||
code: 'publish_landmark_scenes_missing',
|
||||
severity: 'blocker',
|
||||
targetId: missingLandmarkScenes[0]?.id ?? null,
|
||||
message: '仍有地点缺少正式场景图资产,发布前需要先补齐地点图。',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const invalidSceneChapters = draftProfile.sceneChapters.filter(
|
||||
(entry) =>
|
||||
entry.linkedThreadIds.length <= 0 ||
|
||||
entry.acts.every((act) => act.encounterNpcIds.length <= 0),
|
||||
);
|
||||
if (invalidSceneChapters.length > 0) {
|
||||
blockers.push(
|
||||
buildFinding({
|
||||
id: 'publish-scene-chapter-unbound',
|
||||
code: 'publish_scene_chapter_unbound',
|
||||
severity: 'blocker',
|
||||
targetId: invalidSceneChapters[0]?.id ?? null,
|
||||
message: '场景章节还没有绑定足够的线程或角色,发布前请先补齐主线挂钩。',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
blockers,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
import type {
|
||||
CustomWorldAgentSessionRecord,
|
||||
} from './customWorldAgentSessionStore.js';
|
||||
import {
|
||||
buildCompiledCustomWorldProfile,
|
||||
} from '../modules/custom-world/runtimeProfile.js';
|
||||
import type { CustomWorldProfile } from '../modules/custom-world/runtimeTypes.js';
|
||||
import type { RpgWorldProfileRepositoryPort } from '../repositories/RpgWorldProfileRepository.js';
|
||||
import type { CustomWorldProfileRecord } from '../../../packages/shared/src/contracts/runtime.js';
|
||||
|
||||
function toText(value: unknown, fallback = '') {
|
||||
return typeof value === 'string' ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
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> =>
|
||||
Boolean(item) && typeof item === 'object' && !Array.isArray(item),
|
||||
)
|
||||
: [];
|
||||
}
|
||||
|
||||
function toStringArray(value: unknown, max = 8) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...new Set(value.map((item) => toText(item)).filter(Boolean))].slice(
|
||||
0,
|
||||
max,
|
||||
);
|
||||
}
|
||||
|
||||
function buildSettingTextFromSession(session: CustomWorldAgentSessionRecord) {
|
||||
const anchorContent = session.anchorContent;
|
||||
|
||||
const anchorLines = [
|
||||
anchorContent.worldPromise
|
||||
? `世界承诺:${[
|
||||
anchorContent.worldPromise.hook,
|
||||
anchorContent.worldPromise.differentiator,
|
||||
anchorContent.worldPromise.desiredExperience,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';')}`
|
||||
: '',
|
||||
anchorContent.playerFantasy
|
||||
? `玩家幻想:${[
|
||||
anchorContent.playerFantasy.playerRole,
|
||||
anchorContent.playerFantasy.corePursuit,
|
||||
anchorContent.playerFantasy.fearOfLoss,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';')}`
|
||||
: '',
|
||||
anchorContent.coreConflict
|
||||
? `核心冲突:${[
|
||||
anchorContent.coreConflict.surfaceConflicts.join('、'),
|
||||
anchorContent.coreConflict.hiddenCrisis,
|
||||
anchorContent.coreConflict.firstTouchedConflict,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';')}`
|
||||
: '',
|
||||
anchorContent.iconicElements
|
||||
? `标志元素:${[
|
||||
anchorContent.iconicElements.iconicMotifs.join('、'),
|
||||
anchorContent.iconicElements.institutionsOrArtifacts.join('、'),
|
||||
anchorContent.iconicElements.hardRules.join('、'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';')}`
|
||||
: '',
|
||||
].filter(Boolean);
|
||||
|
||||
if (anchorLines.length > 0) {
|
||||
return anchorLines.join('\n');
|
||||
}
|
||||
|
||||
return session.seedText.trim() || '当前世界草稿已经进入发布阶段。';
|
||||
}
|
||||
|
||||
function buildRuntimeRoleFromDraft(
|
||||
draftRole: Record<string, unknown>,
|
||||
roleKind: 'playable' | 'story',
|
||||
index: number,
|
||||
) {
|
||||
const name = toText(draftRole.name) || `角色 ${index + 1}`;
|
||||
const title =
|
||||
toText(draftRole.title) ||
|
||||
toText(draftRole.role) ||
|
||||
(roleKind === 'playable' ? '关键角色' : '场景角色');
|
||||
const role = toText(draftRole.role) || title;
|
||||
|
||||
return {
|
||||
id: toText(draftRole.id) || `${roleKind}-draft-${index + 1}`,
|
||||
name,
|
||||
title,
|
||||
role,
|
||||
description:
|
||||
toText(draftRole.summary) ||
|
||||
toText(draftRole.publicIdentity) ||
|
||||
toText(draftRole.publicMask) ||
|
||||
toText(draftRole.currentPressure),
|
||||
backstory: [
|
||||
toText(draftRole.publicIdentity),
|
||||
toText(draftRole.currentPressure),
|
||||
toText(draftRole.hiddenHook)
|
||||
? `暗线:${toText(draftRole.hiddenHook)}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
personality:
|
||||
toText(draftRole.publicMask) ||
|
||||
toText(draftRole.publicIdentity) ||
|
||||
toText(draftRole.summary),
|
||||
motivation:
|
||||
toText(draftRole.relationToPlayer) ||
|
||||
toText(draftRole.currentPressure) ||
|
||||
toText(draftRole.hiddenHook),
|
||||
combatStyle: role,
|
||||
initialAffinity: roleKind === 'playable' ? 18 : 6,
|
||||
relationshipHooks: [
|
||||
toText(draftRole.relationToPlayer),
|
||||
toText(draftRole.currentPressure),
|
||||
toText(draftRole.hiddenHook),
|
||||
].filter(Boolean),
|
||||
tags: [
|
||||
...toStringArray(draftRole.threadIds, 4),
|
||||
roleKind === 'playable' ? '草稿主角' : '草稿角色',
|
||||
],
|
||||
imageSrc: toText(draftRole.imageSrc) || undefined,
|
||||
generatedVisualAssetId:
|
||||
toText(draftRole.generatedVisualAssetId) || undefined,
|
||||
generatedAnimationSetId:
|
||||
toText(draftRole.generatedAnimationSetId) || undefined,
|
||||
animationMap: isRecord(draftRole.animationMap)
|
||||
? draftRole.animationMap
|
||||
: undefined,
|
||||
} satisfies Record<string, unknown>;
|
||||
}
|
||||
|
||||
function buildRuntimeLandmarkFromDraft(
|
||||
draftLandmark: Record<string, unknown>,
|
||||
storyNpcIdSet: Set<string>,
|
||||
index: number,
|
||||
) {
|
||||
return {
|
||||
id: toText(draftLandmark.id) || `landmark-draft-${index + 1}`,
|
||||
name: toText(draftLandmark.name) || `关键地点 ${index + 1}`,
|
||||
description:
|
||||
toText(draftLandmark.description) ||
|
||||
toText(draftLandmark.summary) ||
|
||||
[toText(draftLandmark.purpose), toText(draftLandmark.mood)]
|
||||
.filter(Boolean)
|
||||
.join(';'),
|
||||
dangerLevel:
|
||||
toText(draftLandmark.dangerLevel) ||
|
||||
toText(draftLandmark.importance) ||
|
||||
toText(draftLandmark.mood) ||
|
||||
'medium',
|
||||
imageSrc: toText(draftLandmark.imageSrc) || undefined,
|
||||
generatedSceneAssetId:
|
||||
toText(draftLandmark.generatedSceneAssetId) || undefined,
|
||||
generatedScenePrompt:
|
||||
toText(draftLandmark.generatedScenePrompt) || undefined,
|
||||
generatedSceneModel:
|
||||
toText(draftLandmark.generatedSceneModel) || undefined,
|
||||
sceneNpcIds: toStringArray(draftLandmark.characterIds).filter((entry) =>
|
||||
storyNpcIdSet.has(entry),
|
||||
),
|
||||
connections: [],
|
||||
} satisfies Record<string, unknown>;
|
||||
}
|
||||
|
||||
function buildRuntimeCampFromDraft(draftCamp: Record<string, unknown> | null) {
|
||||
if (!draftCamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = toText(draftCamp.name);
|
||||
const description = toText(draftCamp.description);
|
||||
if (!name && !description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: toText(draftCamp.id) || 'camp-home',
|
||||
name: name || '开局营地',
|
||||
description: description || '当前世界的开局落脚点。',
|
||||
dangerLevel:
|
||||
toText(draftCamp.dangerLevel) || toText(draftCamp.mood) || 'low',
|
||||
imageSrc: toText(draftCamp.imageSrc) || undefined,
|
||||
generatedSceneAssetId:
|
||||
toText(draftCamp.generatedSceneAssetId) || undefined,
|
||||
generatedScenePrompt:
|
||||
toText(draftCamp.generatedScenePrompt) || undefined,
|
||||
generatedSceneModel:
|
||||
toText(draftCamp.generatedSceneModel) || undefined,
|
||||
sceneNpcIds: [],
|
||||
connections: [],
|
||||
} satisfies Record<string, unknown>;
|
||||
}
|
||||
|
||||
function buildRuntimeSceneChaptersFromDraft(
|
||||
draftProfile: Record<string, unknown>,
|
||||
storyNpcIdSet: Set<string>,
|
||||
landmarkIdSet: Set<string>,
|
||||
) {
|
||||
return toRecordArray(draftProfile.sceneChapters)
|
||||
.map((sceneChapter, chapterIndex) => {
|
||||
const sceneId = toText(sceneChapter.sceneId);
|
||||
if (!sceneId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const acts = toRecordArray(sceneChapter.acts)
|
||||
.map((act, actIndex) => {
|
||||
const encounterNpcIds = toStringArray(act.encounterNpcIds).filter(
|
||||
(entry) => storyNpcIdSet.has(entry),
|
||||
);
|
||||
const primaryNpcId =
|
||||
toText(act.primaryNpcId) || encounterNpcIds[0] || '';
|
||||
|
||||
return {
|
||||
id: toText(act.id) || `scene-act-${sceneId}-${actIndex + 1}`,
|
||||
sceneId,
|
||||
title: toText(act.title) || `第 ${actIndex + 1} 幕`,
|
||||
summary:
|
||||
toText(act.summary) ||
|
||||
toText(act.actGoal) ||
|
||||
`围绕${toText(sceneChapter.sceneName, sceneId)}继续推进`,
|
||||
stageCoverage:
|
||||
toStringArray(act.stageCoverage).length > 0
|
||||
? toStringArray(act.stageCoverage)
|
||||
: actIndex === 0
|
||||
? ['opening']
|
||||
: ['climax', 'aftermath'],
|
||||
backgroundImageSrc:
|
||||
toText(act.backgroundImageSrc) || undefined,
|
||||
backgroundAssetId: toText(act.backgroundAssetId) || undefined,
|
||||
encounterNpcIds,
|
||||
primaryNpcId,
|
||||
linkedThreadIds: toStringArray(act.linkedThreadIds, 8),
|
||||
advanceRule:
|
||||
toText(act.advanceRule) || 'after_active_step_complete',
|
||||
actGoal: toText(act.actGoal),
|
||||
transitionHook: toText(act.transitionHook),
|
||||
} satisfies Record<string, unknown>;
|
||||
})
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.encounterNpcIds.length > 0 || entry.backgroundImageSrc,
|
||||
);
|
||||
|
||||
return {
|
||||
id:
|
||||
toText(sceneChapter.id) ||
|
||||
`scene-chapter-${sceneId}-${chapterIndex + 1}`,
|
||||
sceneId,
|
||||
title:
|
||||
toText(sceneChapter.title) ||
|
||||
toText(sceneChapter.sceneName) ||
|
||||
sceneId,
|
||||
summary:
|
||||
toText(sceneChapter.summary) ||
|
||||
toText(sceneChapter.title) ||
|
||||
toText(sceneChapter.sceneName) ||
|
||||
sceneId,
|
||||
linkedThreadIds: toStringArray(sceneChapter.linkedThreadIds, 8),
|
||||
linkedLandmarkIds: toStringArray(
|
||||
sceneChapter.linkedLandmarkIds,
|
||||
8,
|
||||
).filter((entry) => landmarkIdSet.has(entry)),
|
||||
acts,
|
||||
} satisfies Record<string, unknown>;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildPublishRawProfile(
|
||||
session: CustomWorldAgentSessionRecord,
|
||||
profileId: string,
|
||||
) {
|
||||
const draftProfile = isRecord(session.draftProfile) ? session.draftProfile : {};
|
||||
const legacyResultProfile = isRecord(draftProfile.legacyResultProfile)
|
||||
? draftProfile.legacyResultProfile
|
||||
: null;
|
||||
const playableNpcs = toRecordArray(draftProfile.playableNpcs).map(
|
||||
(entry, index) => buildRuntimeRoleFromDraft(entry, 'playable', index),
|
||||
);
|
||||
const storyNpcs = toRecordArray(draftProfile.storyNpcs).map((entry, index) =>
|
||||
buildRuntimeRoleFromDraft(entry, 'story', index),
|
||||
);
|
||||
const storyNpcIdSet = new Set(
|
||||
storyNpcs.map((entry) => toText(entry.id)).filter(Boolean),
|
||||
);
|
||||
const landmarks = toRecordArray(draftProfile.landmarks).map((entry, index) =>
|
||||
buildRuntimeLandmarkFromDraft(entry, storyNpcIdSet, index),
|
||||
);
|
||||
const landmarkIdSet = new Set(
|
||||
landmarks.map((entry) => toText(entry.id)).filter(Boolean),
|
||||
);
|
||||
|
||||
return {
|
||||
...(legacyResultProfile ?? {}),
|
||||
id: profileId,
|
||||
settingText: buildSettingTextFromSession(session),
|
||||
name:
|
||||
toText(draftProfile.name) ||
|
||||
toText(legacyResultProfile?.name) ||
|
||||
'未命名世界底稿',
|
||||
subtitle:
|
||||
toText(draftProfile.subtitle) ||
|
||||
toText(legacyResultProfile?.subtitle) ||
|
||||
'已发布世界',
|
||||
summary:
|
||||
toText(draftProfile.summary) ||
|
||||
toText(legacyResultProfile?.summary) ||
|
||||
'当前世界已经进入发布态。',
|
||||
tone:
|
||||
toText(draftProfile.tone) ||
|
||||
toText(legacyResultProfile?.tone) ||
|
||||
'整体气质仍带着明显张力',
|
||||
playerGoal:
|
||||
toText(draftProfile.playerGoal) ||
|
||||
toText(legacyResultProfile?.playerGoal) ||
|
||||
'先站稳局势,再判断下一步',
|
||||
majorFactions:
|
||||
toStringArray(draftProfile.majorFactions, 6).length > 0
|
||||
? toStringArray(draftProfile.majorFactions, 6)
|
||||
: Array.isArray(legacyResultProfile?.majorFactions)
|
||||
? legacyResultProfile.majorFactions
|
||||
: [],
|
||||
coreConflicts:
|
||||
toStringArray(draftProfile.coreConflicts, 6).length > 0
|
||||
? toStringArray(draftProfile.coreConflicts, 6)
|
||||
: Array.isArray(legacyResultProfile?.coreConflicts)
|
||||
? legacyResultProfile.coreConflicts
|
||||
: [toText(draftProfile.summary) || '核心冲突仍待继续补强'],
|
||||
playableNpcs,
|
||||
storyNpcs,
|
||||
landmarks,
|
||||
camp: buildRuntimeCampFromDraft(
|
||||
isRecord(draftProfile.camp) ? draftProfile.camp : null,
|
||||
),
|
||||
sceneChapterBlueprints: buildRuntimeSceneChaptersFromDraft(
|
||||
draftProfile,
|
||||
storyNpcIdSet,
|
||||
landmarkIdSet,
|
||||
),
|
||||
anchorContent: session.anchorContent,
|
||||
creatorIntent: session.creatorIntent,
|
||||
anchorPack: session.anchorPack,
|
||||
lockState: session.lockState,
|
||||
generationMode: 'full',
|
||||
generationStatus: 'complete',
|
||||
} satisfies Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class CustomWorldAgentPublishService {
|
||||
buildProfileId(sessionId: string) {
|
||||
return `agent-draft-${sessionId}`;
|
||||
}
|
||||
|
||||
compilePublishedProfile(
|
||||
session: CustomWorldAgentSessionRecord,
|
||||
): CustomWorldProfile {
|
||||
const profileId = this.buildProfileId(session.sessionId);
|
||||
const rawProfile = buildPublishRawProfile(session, profileId);
|
||||
|
||||
return buildCompiledCustomWorldProfile(
|
||||
rawProfile,
|
||||
buildSettingTextFromSession(session),
|
||||
);
|
||||
}
|
||||
|
||||
async publish(params: {
|
||||
session: CustomWorldAgentSessionRecord;
|
||||
userId: string;
|
||||
authorDisplayName: string;
|
||||
profileRepository: RpgWorldProfileRepositoryPort;
|
||||
}) {
|
||||
const publishedProfile = this.compilePublishedProfile(params.session);
|
||||
const profileId = this.buildProfileId(params.session.sessionId);
|
||||
const mutation = await params.profileRepository.upsertOwnProfile(
|
||||
params.userId,
|
||||
profileId,
|
||||
publishedProfile as unknown as CustomWorldProfileRecord,
|
||||
params.authorDisplayName || '玩家',
|
||||
);
|
||||
const publishedMutation = await params.profileRepository.publishOwnProfile(
|
||||
params.userId,
|
||||
profileId,
|
||||
params.authorDisplayName || '玩家',
|
||||
);
|
||||
|
||||
return {
|
||||
profileId,
|
||||
publishedProfile,
|
||||
mutation: publishedMutation ?? mutation,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
} from '../../../packages/shared/src/contracts/rpgCreationFixtures.js';
|
||||
import type { CustomWorldSessionRecord } from '../../../packages/shared/src/contracts/runtime.js';
|
||||
import { createInMemoryRpgWorldRepositoryPorts } from './customWorldAgentRepositoryTestHelpers.js';
|
||||
import { listCustomWorldWorkSummaries } from './customWorldWorkSummaryService.js';
|
||||
import {
|
||||
CUSTOM_WORLD_AGENT_SESSION_ID_PREFIX,
|
||||
CustomWorldAgentSessionStore,
|
||||
} from './customWorldAgentSessionStore.js';
|
||||
import { RpgWorldWorkSummaryService } from './RpgWorldWorkSummaryService.js';
|
||||
|
||||
test('work summary service can aggregate shared RPG fixtures into draft and published entries', async () => {
|
||||
const sessionFixture = createRpgAgentSessionFixture();
|
||||
@@ -34,10 +34,10 @@ test('work summary service can aggregate shared RPG fixtures into draft and publ
|
||||
const sessionStore = new CustomWorldAgentSessionStore(
|
||||
rpgAgentSessionRepository,
|
||||
);
|
||||
const summaries = await listCustomWorldWorkSummaries('fixture-user', {
|
||||
rpgWorldProfiles: rpgWorldProfileRepository,
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const summaries = await new RpgWorldWorkSummaryService(
|
||||
rpgWorldProfileRepository,
|
||||
sessionStore,
|
||||
).list('fixture-user');
|
||||
const expected = createRpgCreationWorksResponseFixture();
|
||||
|
||||
assert.equal(summaries.length, expected.items.length);
|
||||
@@ -97,10 +97,10 @@ test('published agent sessions are filtered out after works unify to published p
|
||||
rpgAgentSessionRepository,
|
||||
);
|
||||
|
||||
const summaries = await listCustomWorldWorkSummaries('fixture-user', {
|
||||
rpgWorldProfiles: rpgWorldProfileRepository,
|
||||
customWorldAgentSessions: sessionStore,
|
||||
});
|
||||
const summaries = await new RpgWorldWorkSummaryService(
|
||||
rpgWorldProfileRepository,
|
||||
sessionStore,
|
||||
).list('fixture-user');
|
||||
|
||||
assert.equal(
|
||||
summaries.some((entry) => entry.sourceType === 'agent_session'),
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { RpgWorldProfileRepositoryPort } from '../repositories/RpgWorldProfileRepository.js';
|
||||
import type { CustomWorldAgentSessionStore } from './customWorldAgentSessionStore.js';
|
||||
import { RpgWorldWorkSummaryService } from './RpgWorldWorkSummaryService.js';
|
||||
|
||||
/**
|
||||
* 兼容服务入口保留旧文件名,内部则改走 RPG works 组装器,便于后续继续迁到新命名。
|
||||
*/
|
||||
export async function listCustomWorldWorkSummaries(
|
||||
userId: string,
|
||||
dependencies: {
|
||||
rpgWorldProfiles: RpgWorldProfileRepositoryPort;
|
||||
customWorldAgentSessions: CustomWorldAgentSessionStore;
|
||||
},
|
||||
) {
|
||||
const service = new RpgWorldWorkSummaryService(
|
||||
dependencies.rpgWorldProfiles,
|
||||
dependencies.customWorldAgentSessions,
|
||||
);
|
||||
return service.list(userId);
|
||||
}
|
||||
Reference in New Issue
Block a user