Integrate role asset studio into custom world agent flow
This commit is contained in:
322
server-node/src/services/customWorldAgentDraftEditService.ts
Normal file
322
server-node/src/services/customWorldAgentDraftEditService.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { badRequest, notFound } from '../errors.js';
|
||||
import {
|
||||
getWorldFoundationCardId,
|
||||
normalizeFoundationDraftProfile,
|
||||
} from './customWorldAgentDraftCompiler.js';
|
||||
|
||||
type DraftSectionPatch = {
|
||||
sectionId: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type UpdateDraftCardSectionsParams = {
|
||||
draftProfile: Record<string, unknown>;
|
||||
cardId: string;
|
||||
sections: DraftSectionPatch[];
|
||||
};
|
||||
|
||||
const EDITABLE_SECTION_IDS = {
|
||||
world: new Set(['title', 'subtitle', 'summary', 'playerGoal', 'tone', 'coreConflicts']),
|
||||
faction: new Set(['title', 'subtitle', 'summary', 'publicGoal', 'tension']),
|
||||
character: new Set(['name', 'role', 'publicMask', 'hiddenHook', 'relationToPlayer', 'summary']),
|
||||
landmark: new Set(['name', 'purpose', 'mood', 'secret', 'summary']),
|
||||
thread: new Set(['title', 'summary', 'conflictType', 'stakes']),
|
||||
chapter: new Set(['title', 'summary', 'openingEvent', 'playerGoal', 'understandingShift']),
|
||||
camp: new Set(['name', 'description', 'dangerLevel']),
|
||||
} as const;
|
||||
|
||||
function normalizePatches(sections: DraftSectionPatch[]) {
|
||||
const normalized = sections
|
||||
.map((section) => ({
|
||||
sectionId: section.sectionId.trim(),
|
||||
value: section.value.trim(),
|
||||
}))
|
||||
.filter((section) => section.sectionId);
|
||||
|
||||
if (normalized.length === 0) {
|
||||
throw badRequest('update_draft_card requires at least one section patch');
|
||||
}
|
||||
|
||||
const deduped = new Map<string, string>();
|
||||
normalized.forEach((section) => {
|
||||
deduped.set(section.sectionId, section.value);
|
||||
});
|
||||
|
||||
return [...deduped.entries()].map(([sectionId, value]) => ({
|
||||
sectionId,
|
||||
value,
|
||||
}));
|
||||
}
|
||||
|
||||
function parseStringList(value: string) {
|
||||
return [...new Set(value.split(/[\n;;]+/u).map((item) => item.trim()).filter(Boolean))];
|
||||
}
|
||||
|
||||
function resolveThreadType(value: string) {
|
||||
if (value.includes('暗') || value.toLowerCase() === 'hidden') {
|
||||
return 'hidden' as const;
|
||||
}
|
||||
|
||||
return 'main' as const;
|
||||
}
|
||||
|
||||
export function updateDraftCardSections(params: UpdateDraftCardSectionsParams) {
|
||||
const draftProfile = normalizeFoundationDraftProfile(params.draftProfile);
|
||||
if (!draftProfile) {
|
||||
throw badRequest('draftProfile is empty');
|
||||
}
|
||||
|
||||
const patches = normalizePatches(params.sections);
|
||||
const worldCardId = getWorldFoundationCardId();
|
||||
|
||||
if (params.cardId === worldCardId) {
|
||||
patches.forEach(({ sectionId, value }) => {
|
||||
if (!EDITABLE_SECTION_IDS.world.has(sectionId as never)) {
|
||||
throw badRequest(`section ${sectionId} is not editable for world`);
|
||||
}
|
||||
|
||||
if (sectionId === 'title') {
|
||||
draftProfile.name = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'subtitle') {
|
||||
draftProfile.subtitle = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'summary') {
|
||||
draftProfile.summary = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'playerGoal') {
|
||||
draftProfile.playerGoal = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'tone') {
|
||||
draftProfile.tone = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'coreConflicts') {
|
||||
draftProfile.coreConflicts = parseStringList(value);
|
||||
}
|
||||
});
|
||||
|
||||
return draftProfile as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const faction = draftProfile.factions.find((entry) => entry.id === params.cardId);
|
||||
if (faction) {
|
||||
patches.forEach(({ sectionId, value }) => {
|
||||
if (!EDITABLE_SECTION_IDS.faction.has(sectionId as never)) {
|
||||
throw badRequest(`section ${sectionId} is not editable for faction`);
|
||||
}
|
||||
|
||||
if (sectionId === 'title') {
|
||||
faction.name = value;
|
||||
faction.title = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'subtitle') {
|
||||
faction.subtitle = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'summary') {
|
||||
faction.summary = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'publicGoal') {
|
||||
faction.publicGoal = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'tension') {
|
||||
faction.tension = value;
|
||||
faction.relatedConflict = value;
|
||||
}
|
||||
});
|
||||
|
||||
return draftProfile as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const character = [...draftProfile.playableNpcs, ...draftProfile.storyNpcs].find(
|
||||
(entry) => entry.id === params.cardId,
|
||||
);
|
||||
if (character) {
|
||||
patches.forEach(({ sectionId, value }) => {
|
||||
if (!EDITABLE_SECTION_IDS.character.has(sectionId as never)) {
|
||||
throw badRequest(`section ${sectionId} is not editable for character`);
|
||||
}
|
||||
|
||||
if (sectionId === 'name') {
|
||||
character.name = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'role') {
|
||||
character.role = value;
|
||||
character.title = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'publicMask') {
|
||||
character.publicMask = value;
|
||||
character.publicIdentity = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'hiddenHook') {
|
||||
character.hiddenHook = value;
|
||||
character.currentPressure = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'relationToPlayer') {
|
||||
character.relationToPlayer = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'summary') {
|
||||
character.summary = value;
|
||||
}
|
||||
});
|
||||
|
||||
return draftProfile as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const landmark = draftProfile.landmarks.find((entry) => entry.id === params.cardId);
|
||||
if (landmark) {
|
||||
patches.forEach(({ sectionId, value }) => {
|
||||
if (!EDITABLE_SECTION_IDS.landmark.has(sectionId as never)) {
|
||||
throw badRequest(`section ${sectionId} is not editable for landmark`);
|
||||
}
|
||||
|
||||
if (sectionId === 'name') {
|
||||
landmark.name = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'purpose') {
|
||||
landmark.purpose = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'mood') {
|
||||
landmark.mood = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'secret') {
|
||||
landmark.secret = value;
|
||||
landmark.importance = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'summary') {
|
||||
landmark.summary = value;
|
||||
}
|
||||
});
|
||||
|
||||
return draftProfile as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const thread = draftProfile.threads.find((entry) => entry.id === params.cardId);
|
||||
if (thread) {
|
||||
patches.forEach(({ sectionId, value }) => {
|
||||
if (!EDITABLE_SECTION_IDS.thread.has(sectionId as never)) {
|
||||
throw badRequest(`section ${sectionId} is not editable for thread`);
|
||||
}
|
||||
|
||||
if (sectionId === 'title') {
|
||||
thread.title = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'summary') {
|
||||
thread.summary = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'conflictType') {
|
||||
thread.conflictType = value;
|
||||
thread.type = resolveThreadType(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'stakes') {
|
||||
thread.stakes = value;
|
||||
thread.conflict = value;
|
||||
}
|
||||
});
|
||||
|
||||
return draftProfile as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const chapter = draftProfile.chapters.find((entry) => entry.id === params.cardId);
|
||||
if (chapter) {
|
||||
patches.forEach(({ sectionId, value }) => {
|
||||
if (!EDITABLE_SECTION_IDS.chapter.has(sectionId as never)) {
|
||||
throw badRequest(`section ${sectionId} is not editable for chapter`);
|
||||
}
|
||||
|
||||
if (sectionId === 'title') {
|
||||
chapter.title = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'summary') {
|
||||
chapter.summary = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'openingEvent') {
|
||||
chapter.openingEvent = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'playerGoal') {
|
||||
chapter.playerGoal = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'understandingShift') {
|
||||
chapter.understandingShift = value;
|
||||
}
|
||||
});
|
||||
|
||||
return draftProfile as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
if (draftProfile.camp?.id === params.cardId) {
|
||||
patches.forEach(({ sectionId, value }) => {
|
||||
if (!EDITABLE_SECTION_IDS.camp.has(sectionId as never)) {
|
||||
throw badRequest(`section ${sectionId} is not editable for camp`);
|
||||
}
|
||||
|
||||
if (sectionId === 'name') {
|
||||
draftProfile.camp!.name = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'description') {
|
||||
draftProfile.camp!.description = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sectionId === 'dangerLevel') {
|
||||
draftProfile.camp!.dangerLevel = value;
|
||||
draftProfile.camp!.mood = value;
|
||||
}
|
||||
});
|
||||
|
||||
return draftProfile as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
throw notFound('draft card not found');
|
||||
}
|
||||
Reference in New Issue
Block a user