import type { CustomWorldDraftCardDetail, CustomWorldDraftCardDetailSection, CustomWorldDraftCardKind, CustomWorldDraftCardSummary, CustomWorldRoleAssetStatus, CustomWorldFoundationDraftCamp, CustomWorldFoundationDraftChapter, CustomWorldFoundationDraftCharacter, CustomWorldFoundationDraftFaction, CustomWorldFoundationDraftLandmark, CustomWorldFoundationDraftProfile, CustomWorldFoundationDraftThread, } from '../../../packages/shared/src/contracts/customWorldAgent.js'; import { buildRoleAssetSummary, resolveRoleAssetStatusLabel, } from './customWorldAgentRoleAssetStateService.js'; const WORLD_CARD_ID = 'world-foundation'; const EDITABLE_WORLD_SECTION_IDS = [ 'title', 'subtitle', 'summary', 'playerGoal', 'tone', 'coreConflicts', ] as const; const EDITABLE_FACTION_SECTION_IDS = [ 'title', 'subtitle', 'summary', 'publicGoal', 'tension', ] as const; const EDITABLE_CHARACTER_SECTION_IDS = [ 'name', 'role', 'publicMask', 'hiddenHook', 'relationToPlayer', 'summary', ] as const; const EDITABLE_LANDMARK_SECTION_IDS = [ 'name', 'purpose', 'mood', 'secret', 'summary', ] as const; const EDITABLE_THREAD_SECTION_IDS = [ 'title', 'summary', 'conflictType', 'stakes', ] as const; const EDITABLE_CHAPTER_SECTION_IDS = [ 'title', 'summary', 'openingEvent', 'playerGoal', 'understandingShift', ] as const; const EDITABLE_CAMP_SECTION_IDS = [ 'name', 'description', 'dangerLevel', ] as const; function toText(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } function toRecord(value: unknown) { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : null; } function toRecordArray(value: unknown) { return Array.isArray(value) ? value.filter((item) => item && typeof item === 'object') : []; } function toStringArray(value: unknown, maxCount = 8) { if (!Array.isArray(value)) { return []; } return [...new Set(value.map((item) => toText(item)).filter(Boolean))].slice( 0, maxCount, ); } function slugify(value: string) { const normalized = value .trim() .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]+/gu, '-') .replace(/^-+|-+$/gu, ''); return normalized || 'entry'; } function createId(prefix: string, label: string, index: number) { return `${prefix}-${slugify(label || `${prefix}-${index + 1}`)}-${index + 1}`; } function clampText(value: string, maxLength: number) { const normalized = value.replace(/\s+/gu, ' ').trim(); if (!normalized) { return ''; } if (normalized.length <= maxLength) { return normalized; } return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}…`; } function dedupeById(items: T[]) { const seen = new Set(); return items.filter((item) => { const key = item.id.trim(); if (!key || seen.has(key)) { return false; } seen.add(key); return true; }); } function resolveEditableSectionIds(kind: CustomWorldDraftCardKind) { if (kind === 'world') return [...EDITABLE_WORLD_SECTION_IDS]; if (kind === 'faction') return [...EDITABLE_FACTION_SECTION_IDS]; if (kind === 'character') return [...EDITABLE_CHARACTER_SECTION_IDS]; if (kind === 'landmark') return [...EDITABLE_LANDMARK_SECTION_IDS]; if (kind === 'thread') return [...EDITABLE_THREAD_SECTION_IDS]; if (kind === 'chapter') return [...EDITABLE_CHAPTER_SECTION_IDS]; if (kind === 'camp') return [...EDITABLE_CAMP_SECTION_IDS]; return []; } function normalizeFaction( value: unknown, index: number, ): CustomWorldFoundationDraftFaction | null { const record = toRecord(value); if (!record) { return null; } const title = toText(record.title) || toText(record.name); const subtitle = toText(record.subtitle); const publicGoal = toText(record.publicGoal); const tension = toText(record.tension) || toText(record.relatedConflict); const playerRelation = toText(record.playerRelation); const summary = toText(record.summary); if (!title && !publicGoal && !tension && !summary) { return null; } return { id: toText(record.id) || createId('faction', title || publicGoal, index), name: title || `关键势力 ${index + 1}`, title: title || `关键势力 ${index + 1}`, subtitle: subtitle || clampText( [publicGoal || '关键势力', tension || '当前张力仍在升级'] .filter(Boolean) .join(' · '), 40, ), publicGoal: publicGoal || '稳住自己在当前局势中的位置', relatedConflict: tension || '局势仍在快速失衡', tension: tension || '局势仍在快速失衡', playerRelation: playerRelation || '玩家迟早要和它发生直接关系', summary: summary || clampText( [ publicGoal || '正在抢夺当前局势的主动权', tension || '和主线冲突直接相连', playerRelation || '会逼玩家选边', ].join(';'), 120, ), }; } function normalizeCharacter( value: unknown, index: number, ): CustomWorldFoundationDraftCharacter | null { const record = toRecord(value); if (!record) { return null; } const name = toText(record.name); const title = toText(record.title); const role = toText(record.role); const publicMask = toText(record.publicMask) || toText(record.publicIdentity); const hiddenHook = toText(record.hiddenHook) || toText(record.currentPressure); const relationToPlayer = toText(record.relationToPlayer); const summary = toText(record.summary); if (!name && !title && !role && !summary) { return null; } return { id: toText(record.id) || createId('character', name || title || role, index), name: name || `关键角色 ${index + 1}`, title: title || role || '关键角色', role: role || title || '关键角色', publicIdentity: publicMask || title || role || '正在局势前台行动的人', publicMask: publicMask || title || role || '正在局势前台行动的人', currentPressure: hiddenHook || '必须立刻回应眼前的局势压力', hiddenHook: hiddenHook || '必须立刻回应眼前的局势压力', relationToPlayer: relationToPlayer || '和玩家存在尚待精修的关系钩子', threadIds: toStringArray(record.threadIds, 6), summary: summary || clampText( [ publicMask || title || role || '处在局势前台', hiddenHook || '眼下压力仍在加码', relationToPlayer || '与玩家关系待细化', ].join(';'), 120, ), imageSrc: toText(record.imageSrc) || null, generatedVisualAssetId: toText(record.generatedVisualAssetId) || null, generatedAnimationSetId: toText(record.generatedAnimationSetId) || null, animationMap: toRecord(record.animationMap), }; } function normalizeLandmark( value: unknown, index: number, ): CustomWorldFoundationDraftLandmark | null { const record = toRecord(value); if (!record) { return null; } const name = toText(record.name); const description = toText(record.description); const purpose = toText(record.purpose); const mood = toText(record.mood); const secret = toText(record.secret) || toText(record.importance); const dangerLevel = toText(record.dangerLevel); const summary = toText(record.summary); if (!name && !purpose && !mood && !secret && !summary) { return null; } return { id: toText(record.id) || createId('landmark', name || purpose, index), name: name || `关键地点 ${index + 1}`, description: description || clampText( [purpose || '承接关键冲突', mood || '整体情绪仍在发酵'] .filter(Boolean) .join(';'), 96, ), purpose: purpose || '承接主线推进的关键地点', mood: mood || '带着明显张力与未明感', importance: secret || '玩家第一次抵达就会意识到它不只是背景', secret: secret || '玩家第一次抵达就会意识到它不只是背景', dangerLevel: dangerLevel || '中', characterIds: toStringArray(record.characterIds, 8), threadIds: toStringArray(record.threadIds, 8), summary: summary || clampText( [ purpose || '承担关键戏剧功能', secret || '和当前冲突直接相连', mood || '会立刻形成情绪印象', ].join(';'), 120, ), }; } function normalizeThread( value: unknown, index: number, ): CustomWorldFoundationDraftThread | null { const record = toRecord(value); if (!record) { return null; } const title = toText(record.title); const conflictTypeText = toText(record.conflictType); const type = record.type === 'hidden' || conflictTypeText.includes('暗') || conflictTypeText.toLowerCase() === 'hidden' ? 'hidden' : 'main'; const stakes = toText(record.stakes) || toText(record.conflict); const summary = toText(record.summary); if (!title && !stakes && !summary) { return null; } return { id: toText(record.id) || createId('thread', title || stakes, index), title: title || `世界线程 ${index + 1}`, type, conflictType: conflictTypeText || (type === 'hidden' ? '暗线' : '明线'), conflict: stakes || '这条线仍在等待进一步精修', stakes: stakes || '这条线仍在等待进一步精修', characterIds: toStringArray(record.characterIds, 8), landmarkIds: toStringArray(record.landmarkIds, 8), summary: summary || clampText( [ type === 'hidden' ? '暗线' : '明线', stakes || '主要冲突待细化', ].join(':'), 120, ), }; } function normalizeChapter( value: unknown, index: number, ): CustomWorldFoundationDraftChapter | null { const record = toRecord(value); if (!record) { return null; } const title = toText(record.title); const openingEvent = toText(record.openingEvent); const playerGoal = toText(record.playerGoal); const understandingShift = toText(record.understandingShift); const summary = toText(record.summary); if (!title && !openingEvent && !playerGoal && !summary) { return null; } return { id: toText(record.id) || createId('chapter', title || openingEvent, index), title: title || '第一幕', openingEvent: openingEvent || '局势在开幕时突然失控', playerGoal: playerGoal || '先稳住开局并找到下一步目标', characterIds: toStringArray(record.characterIds, 8), landmarkIds: toStringArray(record.landmarkIds, 8), understandingShift: understandingShift || '玩家会意识到这场冲突远不止表面那一层', summary: summary || clampText( [ openingEvent || '开幕事件已逼近', playerGoal || '玩家需要尽快立住脚跟', understandingShift || '第一幕会改写玩家对世界的理解', ].join(';'), 140, ), }; } function normalizeCamp(value: unknown): CustomWorldFoundationDraftCamp | null { const record = toRecord(value); if (!record) { return null; } const name = toText(record.name); const description = toText(record.description); const dangerLevel = toText(record.dangerLevel) || toText(record.mood); const summary = toText(record.summary); if (!name && !description && !summary) { return null; } return { id: toText(record.id) || 'camp-home', name: name || '临时落脚处', description: description || '玩家暂时还能整顿情报和喘口气的地方', mood: dangerLevel || '克制、紧绷,但还能暂时收拢局势', dangerLevel: dangerLevel || '克制、紧绷,但还能暂时收拢局势', summary: summary || clampText( [ description || '这是玩家当前最稳的回气点', dangerLevel || '它承担落脚与整理线索的功能', ].join(';'), 120, ), }; } export function normalizeFoundationDraftProfile( value: unknown, ): CustomWorldFoundationDraftProfile | null { const record = toRecord(value); if (!record) { return null; } const name = toText(record.name) || toText(record.title); const summary = toText(record.summary); const playableNpcs = dedupeById( toRecordArray(record.playableNpcs) .map((item, index) => normalizeCharacter(item, index)) .filter((item): item is CustomWorldFoundationDraftCharacter => Boolean(item), ), ); const storyNpcs = dedupeById( toRecordArray(record.storyNpcs) .map((item, index) => normalizeCharacter(item, index)) .filter((item): item is CustomWorldFoundationDraftCharacter => Boolean(item), ), ); const landmarks = dedupeById( toRecordArray(record.landmarks) .map((item, index) => normalizeLandmark(item, index)) .filter((item): item is CustomWorldFoundationDraftLandmark => Boolean(item), ), ); const factions = dedupeById( toRecordArray(record.factions) .map((item, index) => normalizeFaction(item, index)) .filter((item): item is CustomWorldFoundationDraftFaction => Boolean(item), ), ); const threads = dedupeById( toRecordArray(record.threads) .map((item, index) => normalizeThread(item, index)) .filter((item): item is CustomWorldFoundationDraftThread => Boolean(item), ), ); const chapters = dedupeById( toRecordArray(record.chapters) .map((item, index) => normalizeChapter(item, index)) .filter((item): item is CustomWorldFoundationDraftChapter => Boolean(item), ), ); const camp = normalizeCamp(record.camp); const hasStructuredFoundationContent = playableNpcs.length > 0 || storyNpcs.length > 0 || landmarks.length > 0 || factions.length > 0 || threads.length > 0 || chapters.length > 0 || Boolean(camp); if (!hasStructuredFoundationContent) { return null; } const mergedCharacters = dedupeById([...playableNpcs, ...storyNpcs]); const coreConflicts = toStringArray(record.coreConflicts, 6); return { name: name || '未命名世界底稿', subtitle: toText(record.subtitle) || clampText( [toText(record.playerPremise), coreConflicts[0] ?? '核心冲突仍在整理'] .filter(Boolean) .join(' · '), 40, ) || '第一版世界底稿', summary: summary || clampText( [ toText(record.worldHook), toText(record.playerPremise), coreConflicts[0] ?? '', ] .filter(Boolean) .join(' '), 160, ) || '第一版世界底稿已经整理完成。', tone: toText(record.tone) || '整体气质仍可继续精修', playerGoal: toText(record.playerGoal) || '先站稳开局,再判断下一步', majorFactions: toStringArray(record.majorFactions, 6).length > 0 ? toStringArray(record.majorFactions, 6) : factions.map((entry) => entry.name), coreConflicts, playableNpcs: playableNpcs.length > 0 ? playableNpcs : mergedCharacters.slice(0, Math.max(3, mergedCharacters.length)), storyNpcs: storyNpcs.length > 0 ? storyNpcs : mergedCharacters.filter( (entry) => !playableNpcs.some((npc) => npc.id === entry.id), ), landmarks, camp, themePack: toRecord(record.themePack), storyGraph: toRecord(record.storyGraph), factions, threads, chapters, worldHook: toText(record.worldHook) || name || summary, playerPremise: toText(record.playerPremise), openingSituation: toText(record.openingSituation), iconicElements: toStringArray(record.iconicElements, 8), sourceAnchorSummary: toText(record.sourceAnchorSummary) || summary, }; } function buildSection( id: string, label: string, value: string, ): CustomWorldDraftCardDetailSection { return { id, label, value: value.trim() || '待继续精修', }; } function resolveThreadTypeLabel(type: CustomWorldFoundationDraftThread['type']) { return type === 'hidden' ? '暗线' : '明线'; } function buildWorldWarnings(profile: CustomWorldFoundationDraftProfile) { const warnings: string[] = []; const totalCharacters = dedupeById([ ...profile.playableNpcs, ...profile.storyNpcs, ]).length; if (profile.iconicElements.length === 0) { warnings.push('标志性要素还偏少,后续可以补 1 到 2 个记忆点。'); } if (totalCharacters < 3) { warnings.push('关键角色数量还偏少,建议继续补角色关系网。'); } if (profile.landmarks.length < 4) { warnings.push('关键地点仍然偏少,第一版游历路径还不够饱满。'); } return warnings; } function buildFactionWarnings(faction: CustomWorldFoundationDraftFaction) { const warnings: string[] = []; if (!faction.playerRelation.trim()) { warnings.push('这个势力和玩家的关系仍可更具体。'); } if (!faction.relatedConflict.trim()) { warnings.push('这个势力还缺少更明确的冲突挂钩。'); } return warnings; } function buildCharacterWarnings(character: CustomWorldFoundationDraftCharacter) { const warnings: string[] = []; if (!character.relationToPlayer.trim()) { warnings.push('和玩家的关系钩子还不够明确。'); } if (character.threadIds.length === 0) { warnings.push('这个角色尚未绑定到明确线程。'); } return warnings; } function buildLandmarkWarnings(landmark: CustomWorldFoundationDraftLandmark) { const warnings: string[] = []; if (landmark.characterIds.length === 0) { warnings.push('这个地点还没有挂住足够明确的角色。'); } if (landmark.threadIds.length === 0) { warnings.push('这个地点还缺少更清楚的线程挂钩。'); } return warnings; } function buildThreadWarnings(thread: CustomWorldFoundationDraftThread) { const warnings: string[] = []; if (thread.characterIds.length === 0) { warnings.push('这条线还缺少更明确的角色挂点。'); } if (thread.landmarkIds.length === 0) { warnings.push('这条线还缺少更明确的地点挂点。'); } return warnings; } function buildChapterWarnings(chapter: CustomWorldFoundationDraftChapter) { const warnings: string[] = []; if (chapter.characterIds.length < 2) { warnings.push('第一幕涉及的关键角色还偏少。'); } if (chapter.landmarkIds.length < 2) { warnings.push('第一幕涉及的关键地点还偏少。'); } return warnings; } function buildCampWarnings() { return [] as string[]; } function buildCharacterAssetHeadline(character: CustomWorldFoundationDraftCharacter) { const assetSummary = buildRoleAssetSummary({ role: { id: character.id, name: character.name, threadIds: character.threadIds, imageSrc: character.imageSrc, generatedVisualAssetId: character.generatedVisualAssetId, generatedAnimationSetId: character.generatedAnimationSetId, animationMap: character.animationMap, }, roleKind: 'story', }); return { status: assetSummary.status, label: resolveRoleAssetStatusLabel(assetSummary.status), }; } type CompiledCard = { summary: CustomWorldDraftCardSummary; detail: CustomWorldDraftCardDetail; }; export class CustomWorldAgentDraftCompiler { compileDraftCards(profileInput: unknown) { return this.compile(profileInput).map((entry) => entry.summary); } getDraftCardDetail(profileInput: unknown, cardId: string) { return ( this.compile(profileInput).find((entry) => entry.summary.id === cardId) ?.detail ?? null ); } private compile(profileInput: unknown): CompiledCard[] { const profile = normalizeFoundationDraftProfile(profileInput); if (!profile) { return []; } const characters = dedupeById([ ...profile.playableNpcs, ...profile.storyNpcs, ]); const characterById = new Map(characters.map((entry) => [entry.id, entry])); const landmarkById = new Map(profile.landmarks.map((entry) => [entry.id, entry])); const threadById = new Map(profile.threads.map((entry) => [entry.id, entry])); const resolveCharacterNames = (ids: string[]) => ids .map((id) => characterById.get(id)?.name) .filter((entry): entry is string => Boolean(entry)) .join('、'); const resolveLandmarkNames = (ids: string[]) => ids .map((id) => landmarkById.get(id)?.name) .filter((entry): entry is string => Boolean(entry)) .join('、'); const resolveThreadTitles = (ids: string[]) => ids .map((id) => threadById.get(id)?.title) .filter((entry): entry is string => Boolean(entry)) .join('、'); const cards: CompiledCard[] = []; const pushCard = (params: { id: string; kind: CustomWorldDraftCardKind; title: string; subtitle: string; summary: string; linkedIds: string[]; sections: CustomWorldDraftCardDetailSection[]; editableSectionIds?: string[]; warningMessages: string[]; assetStatus?: CustomWorldRoleAssetStatus | null; assetStatusLabel?: string | null; }) => { const warningMessages = [...new Set(params.warningMessages.filter(Boolean))]; const editableSectionIds = params.editableSectionIds ?? []; cards.push({ summary: { id: params.id, kind: params.kind, title: params.title, subtitle: params.subtitle, summary: clampText(params.summary, 180), status: warningMessages.length > 0 ? 'warning' : 'suggested', linkedIds: [...new Set(params.linkedIds.filter(Boolean))], warningCount: warningMessages.length, assetStatus: params.assetStatus ?? null, assetStatusLabel: params.assetStatusLabel ?? null, }, detail: { id: params.id, kind: params.kind, title: params.title, sections: params.sections, linkedIds: [...new Set(params.linkedIds.filter(Boolean))], locked: false, editable: editableSectionIds.length > 0, editableSectionIds, warningMessages, assetStatus: params.assetStatus ?? null, assetStatusLabel: params.assetStatusLabel ?? null, }, }); }; const worldWarnings = buildWorldWarnings(profile); pushCard({ id: WORLD_CARD_ID, kind: 'world', title: profile.name, subtitle: clampText( [profile.playerPremise, profile.coreConflicts[0] ?? '核心冲突待继续精修'] .filter(Boolean) .join(' · '), 40, ) || profile.subtitle, summary: profile.summary, linkedIds: [ ...(profile.camp ? [profile.camp.id] : []), ...profile.factions.map((entry) => entry.id), ...characters.map((entry) => entry.id), ...profile.landmarks.map((entry) => entry.id), ...profile.threads.map((entry) => entry.id), ...profile.chapters.map((entry) => entry.id), ].slice(0, 12), sections: [ buildSection('title', '标题', profile.name), buildSection('subtitle', '副标题', profile.subtitle), buildSection('summary', '摘要', profile.summary), buildSection('playerGoal', '玩家目标', profile.playerGoal), buildSection( 'tone', '世界气质', [profile.tone, profile.iconicElements.join('、')] .filter(Boolean) .join(' / '), ), buildSection( 'coreConflicts', '核心冲突', profile.coreConflicts.join(';') || profile.summary, ), buildSection('worldHook', '世界一句话', profile.worldHook || profile.summary), buildSection( 'playerPremise', '玩家是谁', profile.playerPremise, ), ], editableSectionIds: resolveEditableSectionIds('world'), warningMessages: worldWarnings, }); if (profile.camp) { const campWarnings = buildCampWarnings(); pushCard({ id: profile.camp.id, kind: 'camp', title: profile.camp.name, subtitle: clampText(profile.camp.mood || '开局落脚处', 28), summary: profile.camp.summary, linkedIds: [ ...profile.landmarks.slice(0, 2).map((entry) => entry.id), ...characters.slice(0, 2).map((entry) => entry.id), ...profile.chapters.slice(0, 1).map((entry) => entry.id), ], sections: [ buildSection('name', '营地名称', profile.camp.name), buildSection('description', '当前定位', profile.camp.description), buildSection( 'dangerLevel', '危险等级', profile.camp.dangerLevel || profile.camp.mood, ), buildSection( 'linkedObjects', '关联对象', [ resolveLandmarkNames( profile.landmarks.slice(0, 2).map((entry) => entry.id), ), resolveCharacterNames( characters.slice(0, 2).map((entry) => entry.id), ), ] .filter(Boolean) .join(';'), ), ], editableSectionIds: resolveEditableSectionIds('camp'), warningMessages: campWarnings, }); } profile.threads.forEach((thread) => { const warnings = buildThreadWarnings(thread); pushCard({ id: thread.id, kind: 'thread', title: thread.title, subtitle: resolveThreadTypeLabel(thread.type), summary: thread.summary, linkedIds: [...thread.characterIds, ...thread.landmarkIds], sections: [ buildSection('title', '线程标题', thread.title), buildSection('summary', '线程摘要', thread.summary), buildSection( 'conflictType', '冲突类型', thread.conflictType || resolveThreadTypeLabel(thread.type), ), buildSection('stakes', '冲突内容', thread.stakes || thread.conflict), buildSection( 'relatedObjects', '相关对象', [ resolveCharacterNames(thread.characterIds), resolveLandmarkNames(thread.landmarkIds), ] .filter(Boolean) .join(';'), ), ], editableSectionIds: resolveEditableSectionIds('thread'), warningMessages: warnings, }); }); profile.factions.forEach((faction) => { const warnings = buildFactionWarnings(faction); const linkedThreadIds = profile.threads .filter( (thread) => thread.conflict.includes(faction.name) || thread.conflict.includes(faction.relatedConflict) || thread.summary.includes(faction.name), ) .map((entry) => entry.id) .slice(0, 3); pushCard({ id: faction.id, kind: 'faction', title: faction.title || faction.name, subtitle: clampText(faction.subtitle || faction.publicGoal, 28), summary: faction.summary, linkedIds: linkedThreadIds, sections: [ buildSection('title', '势力标题', faction.title || faction.name), buildSection( 'subtitle', '副标题', faction.subtitle || clampText(faction.publicGoal, 40), ), buildSection('summary', '势力摘要', faction.summary), buildSection('publicGoal', '公开目标', faction.publicGoal), buildSection('tension', '当前张力', faction.tension || faction.relatedConflict), buildSection('playerRelation', '玩家关系', faction.playerRelation), ], editableSectionIds: resolveEditableSectionIds('faction'), warningMessages: warnings, }); }); characters.forEach((character) => { const warnings = buildCharacterWarnings(character); const assetHeadline = buildCharacterAssetHeadline(character); const linkedLandmarks = profile.landmarks .filter((landmark) => landmark.characterIds.includes(character.id)) .map((entry) => entry.id) .slice(0, 3); pushCard({ id: character.id, kind: 'character', title: character.name, subtitle: [ clampText(character.publicMask || character.publicIdentity, 18), assetHeadline.label, ] .filter(Boolean) .join(' / '), summary: clampText(character.summary, 180), linkedIds: [...character.threadIds, ...linkedLandmarks].slice(0, 6), sections: [ buildSection('name', '角色名', character.name), buildSection('role', '角色定位', character.role || character.title), buildSection( 'publicMask', '外显身份', character.publicMask || character.publicIdentity, ), buildSection( 'hiddenHook', '隐藏钩子', character.hiddenHook || character.currentPressure, ), buildSection('relationToPlayer', '玩家关系', character.relationToPlayer), buildSection('summary', '角色摘要', character.summary), buildSection( 'threadIds', '关联线程', resolveThreadTitles(character.threadIds), ), ], editableSectionIds: resolveEditableSectionIds('character'), warningMessages: warnings, assetStatus: assetHeadline.status, assetStatusLabel: assetHeadline.label, }); }); profile.landmarks.forEach((landmark) => { const warnings = buildLandmarkWarnings(landmark); pushCard({ id: landmark.id, kind: 'landmark', title: landmark.name, subtitle: clampText(landmark.purpose || landmark.mood, 28), summary: landmark.summary, linkedIds: [...landmark.characterIds, ...landmark.threadIds].slice(0, 8), sections: [ buildSection('name', '地点名', landmark.name), buildSection('purpose', '地点定位', landmark.purpose), buildSection('mood', '场景情绪', landmark.mood), buildSection('secret', '隐藏秘密', landmark.secret || landmark.importance), buildSection('summary', '地点摘要', landmark.summary), buildSection( 'characterIds', '关联角色', resolveCharacterNames(landmark.characterIds), ), buildSection( 'threadIds', '关联线程', resolveThreadTitles(landmark.threadIds), ), ], editableSectionIds: resolveEditableSectionIds('landmark'), warningMessages: warnings, }); }); profile.chapters.forEach((chapter) => { const warnings = buildChapterWarnings(chapter); pushCard({ id: chapter.id, kind: 'chapter', title: chapter.title, subtitle: clampText(chapter.playerGoal, 28), summary: chapter.summary, linkedIds: [...chapter.characterIds, ...chapter.landmarkIds].slice(0, 10), sections: [ buildSection('title', '章节标题', chapter.title), buildSection('summary', '章节摘要', chapter.summary), buildSection('openingEvent', '开幕事件', chapter.openingEvent), buildSection('playerGoal', '玩家目标', chapter.playerGoal), buildSection( 'characterIds', '第一批角色', resolveCharacterNames(chapter.characterIds), ), buildSection( 'landmarkIds', '第一批地点', resolveLandmarkNames(chapter.landmarkIds), ), buildSection( 'understandingShift', '第一幕理解变化', chapter.understandingShift, ), ], editableSectionIds: resolveEditableSectionIds('chapter'), warningMessages: warnings, }); }); return cards; } } export function getWorldFoundationCardId() { return WORLD_CARD_ID; }