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

@@ -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');
}