Files
Genarrative/server-node/src/prompts/customWorldPrompts.ts
高物 50759f3c1e
Some checks failed
CI / verify (push) Has been cancelled
1
2026-04-20 09:54:17 +08:00

646 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
CustomWorldGenerationFramework,
CustomWorldGenerationLandmarkOutline,
CustomWorldGenerationRoleBatchStage,
CustomWorldGenerationRoleBatchType,
CustomWorldGenerationRoleOutline,
CustomWorldLandmark,
CustomWorldProfile,
} from '../modules/custom-world/runtimeTypes.js';
const MIN_CUSTOM_WORLD_LANDMARK_COUNT = 10;
const CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES = [15, 30, 60, 90] as const;
const CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS = [
'forward',
'back',
'left',
'right',
'north',
'south',
'east',
'west',
'up',
'down',
'inside',
'outside',
'portal',
] as const;
function buildFrameworkSummaryText(
framework: CustomWorldGenerationFramework,
options: {
maxLandmarks?: number;
} = {},
) {
const maxLandmarks = options.maxLandmarks ?? MIN_CUSTOM_WORLD_LANDMARK_COUNT;
const landmarkText = framework.landmarks
.slice(0, maxLandmarks)
.map(
(landmark) =>
`${landmark.name}${landmark.dangerLevel}${landmark.description}`,
)
.join('、');
return [
`世界:${framework.name}`,
`副标题:${framework.subtitle}`,
`世界概述:${framework.summary}`,
`世界基调:${framework.tone}`,
`玩家核心目标:${framework.playerGoal}`,
framework.majorFactions.length > 0
? `主要势力:${framework.majorFactions.join('、')}`
: '',
framework.coreConflicts.length > 0
? `核心冲突:${framework.coreConflicts.join('、')}`
: '',
`开局归处:${framework.camp.name}${framework.camp.description}`,
landmarkText ? `关键场景:${landmarkText}` : '',
]
.filter(Boolean)
.join('\n');
}
function buildLandmarkAppearanceLookup(
framework: CustomWorldGenerationFramework,
) {
const lookup = new Map<string, string[]>();
framework.landmarks.forEach((landmark) => {
landmark.sceneNpcNames.forEach((npcName) => {
const key = npcName.trim();
if (!key) {
return;
}
const current = lookup.get(key) ?? [];
if (!current.includes(landmark.name)) {
current.push(landmark.name);
}
lookup.set(key, current);
});
});
return lookup;
}
function buildRoleOutlinePromptLines(
roleBatch: CustomWorldGenerationRoleOutline[],
options: {
framework: CustomWorldGenerationFramework;
roleType: CustomWorldGenerationRoleBatchType;
},
) {
const appearanceLookup =
options.roleType === 'story'
? buildLandmarkAppearanceLookup(options.framework)
: new Map<string, string[]>();
return roleBatch
.map((role) => {
const appearanceText =
options.roleType === 'story'
? (appearanceLookup.get(role.name)?.join('、') ?? '未指定')
: '';
return [
`- ${role.name} / ${role.title}`,
`身份:${role.role}`,
`框架描述:${role.description}`,
`预设好感:${role.initialAffinity}`,
role.relationshipHooks.length > 0
? `关系切入口:${role.relationshipHooks.join('、')}`
: '',
role.tags.length > 0 ? `标签:${role.tags.join('、')}` : '',
appearanceText ? `出现场景:${appearanceText}` : '',
]
.filter(Boolean)
.join('');
})
.join('\n');
}
export function buildCustomWorldFrameworkPrompt(settingText: string) {
return [
'请先根据下面的玩家设定创建一份“世界核心骨架”,后续我会分步骤生成角色名单、场景名单和详细档案。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'这一步只保留世界顶层信息与一个开局归处场景,不要输出 playableNpcs、storyNpcs、landmarks也不要展开人物和地图细节。',
'玩家设定:',
settingText.trim(),
'',
'输出 JSON 模板:',
'{',
' "name": "世界名称",',
' "subtitle": "世界副标题",',
' "summary": "世界概述",',
' "tone": "世界基调",',
' "playerGoal": "玩家核心目标",',
' "templateWorldType": "WUXIA|XIANXIA",',
' "majorFactions": ["势力甲", "势力乙"],',
' "coreConflicts": ["冲突甲", "冲突乙"],',
' "camp": {',
' "name": "开局归处名称",',
' "description": "这是玩家进入世界后的第一处落脚点描述",',
' "dangerLevel": "low|medium|high|extreme"',
' }',
'}',
'',
'要求:',
'- 所有生成文本都必须使用中文。',
'- 这一步只输出顶层 9 个字段name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、camp。',
'- 这是一个完全独立的自定义世界;不要在任何正文里直接写出“武侠世界”“仙侠世界”等现成世界名。',
'- templateWorldType 只是系统兼容字段,不代表正文应当引用的世界名称。',
'- camp 必须表示玩家开局时的落脚处,名字不要直接写成“某某营地”,更接近归舍、住处、栖居、前哨居所这类“家/归处”的概念。',
'- 不要输出 playableNpcs、storyNpcs、landmarks、items也不要输出任何角色和地图细节。',
'- majorFactions 保持 2 到 3 个coreConflicts 保持 2 到 3 个。',
'- 世界设定必须直接源自玩家输入,不要脱离主题乱扩写。',
'- 每个字符串尽量简洁subtitle 控制在 8 到 18 个汉字内summary 控制在 16 到 32 个汉字内tone 控制在 6 到 16 个汉字内playerGoal 控制在 16 到 32 个汉字内camp.description 控制在 18 到 40 个汉字内。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
export function buildCustomWorldFrameworkJsonRepairPrompt(
responseText: string,
) {
return [
'下面这段文本本应是自定义世界核心骨架的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。',
'请只输出修复后的 JSON 对象。',
'顶层必须只包含name、subtitle、summary、tone、playerGoal、templateWorldType、majorFactions、coreConflicts、camp。',
'不要输出 playableNpcs、storyNpcs、landmarks、items 或任何其他字段。',
'majorFactions 与 coreConflicts 必须是字符串数组。',
'camp 必须是对象且包含name、description、dangerLevel。',
'原始文本:',
responseText.trim(),
].join('\n');
}
export function buildCustomWorldRoleOutlineBatchPrompt(params: {
framework: CustomWorldGenerationFramework;
roleType: CustomWorldGenerationRoleBatchType;
batchCount: number;
forbiddenNames?: string[];
}) {
const { framework, roleType, batchCount, forbiddenNames = [] } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
const label = roleType === 'playable' ? '可扮演角色' : '场景角色';
return [
`请根据下面的世界核心信息,生成一批${label}框架名单。`,
'后续我会继续补全人物档案,所以这一步每个角色只保留最少字段。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'世界核心信息:',
buildFrameworkSummaryText(framework, { maxLandmarks: 0 }),
forbiddenNames.length > 0
? `这些名字已经生成,禁止重复:${forbiddenNames.join('、')}`
: '',
'',
'输出 JSON 模板:',
'{',
` "${key}": [`,
' {',
' "name": "角色名称",',
' "title": "称号",',
' "role": "身份",',
' "description": "极简定位描述",',
' "initialAffinity": 18,',
' "relationshipHooks": ["一个关系切入口"],',
' "tags": ["标签1", "标签2"]',
' }',
' ]',
'}',
'',
'要求:',
`- 必须生成恰好 ${batchCount}${label}`,
'- 这是一个完全独立的自定义世界;不要把角色写成来自“武侠世界”“仙侠世界”等现成世界。',
'- 名称必须具体且互不重复,不要使用 角色1、NPC1、场景角色1 之类的占位名。',
'- 只保留name、title、role、description、initialAffinity、relationshipHooks、tags。',
'- relationshipHooks 最多 1 条tags 保持 1 到 2 个。',
'- description 控制在 8 到 18 个汉字内title 和 role 也尽量短。',
'- initialAffinity 必须是 -40 到 90 的整数。',
roleType === 'playable'
? '- 可扮演角色的定位必须明显不同,通常使用 18 到 40 的初始好感。'
: '- 场景角色要覆盖势力成员、居民、异类或怪物,不要全是同一种身份;敌对或怪物型角色可以使用负好感。',
'- 所有生成文本都必须使用中文。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldRoleOutlineBatchJsonRepairPrompt(params: {
responseText: string;
roleType: CustomWorldGenerationRoleBatchType;
expectedCount: number;
forbiddenNames?: string[];
}) {
const { responseText, roleType, expectedCount, forbiddenNames = [] } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
return [
`下面这段文本本应是自定义世界${roleType === 'playable' ? '可扮演角色' : '场景角色'}框架名单批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。`,
'请只输出修复后的 JSON 对象。',
`顶层必须只包含一个 ${key} 数组。`,
`必须保留恰好 ${expectedCount} 个角色对象。`,
forbiddenNames.length > 0
? `禁止使用这些重复名:${forbiddenNames.join('、')}`
: '',
'每个角色只包含name、title、role、description、initialAffinity、relationshipHooks、tags。',
'如果缺少字段字符串补空字符串relationshipHooks 和 tags 补空数组initialAffinity 补默认整数。',
'不要输出 backstory、skills、landmarks 或任何其他字段。',
'原始文本:',
responseText.trim(),
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldLandmarkSeedBatchPrompt(params: {
framework: CustomWorldGenerationFramework;
batchCount: number;
forbiddenNames?: string[];
}) {
const { framework, batchCount, forbiddenNames = [] } = params;
return [
'请根据下面的世界核心信息,生成一批场景地标骨架。',
'后续我会继续补全场景角色分布和连接关系,所以这一步只保留最少字段。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'世界核心信息:',
buildFrameworkSummaryText(framework, { maxLandmarks: 0 }),
forbiddenNames.length > 0
? `这些场景名已经生成,禁止重复:${forbiddenNames.join('、')}`
: '',
'',
'输出 JSON 模板:',
'{',
' "landmarks": [',
' {',
' "name": "场景名称",',
' "description": "极简场景描述",',
' "dangerLevel": "low|medium|high|extreme"',
' }',
' ]',
'}',
'',
'要求:',
`- 必须生成恰好 ${batchCount} 个 landmarks。`,
'- 这是一个完全独立的自定义世界;不要在场景描述里直接写“武侠世界”“仙侠世界”等现成世界名。',
'- 这一步只保留name、description、dangerLevel。',
'- 不要输出 sceneNpcNames、connections、imageSrc 或其他字段。',
'- 名称必须具体且互不重复,不要使用 场景1、地点1 之类的占位名。',
'- description 控制在 8 到 18 个汉字内。',
'- 所有生成文本都必须使用中文。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldLandmarkSeedBatchJsonRepairPrompt(params: {
responseText: string;
expectedCount: number;
forbiddenNames?: string[];
}) {
const { responseText, expectedCount, forbiddenNames = [] } = params;
return [
'下面这段文本本应是自定义世界场景地标骨架批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。',
'请只输出修复后的 JSON 对象。',
'顶层必须只包含一个 landmarks 数组。',
`必须保留恰好 ${expectedCount} 个地标对象。`,
forbiddenNames.length > 0
? `禁止使用这些重复场景名:${forbiddenNames.join('、')}`
: '',
'每个地标只包含name、description、dangerLevel。',
'不要输出 sceneNpcNames、connections 或其他字段。',
'原始文本:',
responseText.trim(),
]
.filter(Boolean)
.join('\n');
}
export function buildCustomWorldLandmarkNetworkBatchPrompt(params: {
framework: CustomWorldGenerationFramework;
landmarkBatch: CustomWorldGenerationLandmarkOutline[];
storyNpcs: CustomWorldGenerationRoleOutline[];
}) {
const { framework, landmarkBatch, storyNpcs } = params;
const relativePositionValues =
CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS.join('|');
const allLandmarkNames = framework.landmarks.map((landmark) => landmark.name);
const storyNpcNames = storyNpcs.map((npc) => npc.name);
return [
'请根据下面的世界信息,为这一批场景补全“出现场景角色”和“地图连接关系”。',
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'世界核心信息:',
buildFrameworkSummaryText(framework, { maxLandmarks: 0 }),
`全部场景名:${allLandmarkNames.join('、')}`,
`可用场景角色名:${storyNpcNames.join('、')}`,
'本批次场景骨架:',
landmarkBatch
.map(
(landmark) =>
`- ${landmark.name} / 危险度:${landmark.dangerLevel} / 描述:${landmark.description}`,
)
.join('\n'),
'',
'输出 JSON 模板:',
'{',
' "landmarks": [',
' {',
' "name": "场景名称",',
' "sceneNpcNames": ["场景角色1", "场景角色2", "场景角色3"],',
' "connections": [',
' {',
' "targetLandmarkName": "其他场景名称",',
` "relativePosition": "${CUSTOM_WORLD_SCENE_RELATIVE_POSITION_OPTIONS[0] ?? 'forward'}",`,
' "summary": "极简通路说明"',
' }',
' ]',
' }',
' ]',
'}',
'',
'要求:',
`- 只输出这批 ${landmarkBatch.length} 个场景,不要输出其他场景。`,
'- 这是一个完全独立的自定义世界summary 不要带入“武侠”“仙侠”等现成世界名称。',
'- 名称必须与本批次场景骨架完全一致,不得改名。',
'- 每个场景必须提供恰好 3 个唯一 sceneNpcNames且只能从可用场景角色名里选择。',
`- 每个场景必须提供恰好 2 条 connectionsrelativePosition 只能使用:${relativePositionValues}`,
'- targetLandmarkName 必须来自全部场景名,且不能连接自己,两个目标场景不能重复。',
'- summary 控制在 4 到 10 个汉字内。',
'- 不要输出 description、dangerLevel、backstory 或其他字段。',
'- 所有生成文本都必须使用中文。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
export function buildCustomWorldLandmarkNetworkBatchJsonRepairPrompt(params: {
responseText: string;
expectedNames: string[];
}) {
const { responseText, expectedNames } = params;
return [
'下面这段文本本应是自定义世界场景连接补全批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。',
'请只输出修复后的 JSON 对象。',
'顶层必须只包含一个 landmarks 数组。',
`landmarks 里只能保留这些场景名:${expectedNames.join('、')}`,
'每个场景对象只包含name、sceneNpcNames、connections。',
'connections 里的每个对象必须包含targetLandmarkName、relativePosition、summary。',
'不要输出 description、dangerLevel 或其他字段。',
'原始文本:',
responseText.trim(),
].join('\n');
}
export function buildCustomWorldRoleBatchPrompt(params: {
framework: CustomWorldGenerationFramework;
roleType: CustomWorldGenerationRoleBatchType;
roleBatch: CustomWorldGenerationRoleOutline[];
stage: CustomWorldGenerationRoleBatchStage;
}) {
const { framework, roleType, roleBatch, stage } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
const label = roleType === 'playable' ? '可扮演角色' : '场景角色';
const roleOutlineText = buildRoleOutlinePromptLines(roleBatch, {
framework,
roleType,
});
if (stage === 'narrative') {
return [
`请根据下面的世界框架,补全这一批${label}的叙事基础设定。`,
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'玩家原始设定:',
framework.settingText,
'',
'世界框架摘要:',
buildFrameworkSummaryText(framework, { maxLandmarks: 8 }),
'',
`本批次需要补全的${label}(名称必须原样保留):`,
roleOutlineText,
'',
'输出 JSON 模板:',
'{',
` "${key}": [`,
' {',
' "name": "角色名称",',
' "backstory": "背景经历",',
' "personality": "性格特点",',
' "motivation": "当前动机",',
' "combatStyle": "战斗风格"',
' }',
' ]',
'}',
'',
'要求:',
`- 只输出这批${label},不要输出其他角色、场景或额外顶层字段。`,
'- 这是一个完全独立的自定义世界;不要在角色背景、性格、动机或战斗风格里直接写“武侠世界”“仙侠世界”等现成世界名。',
`- ${key} 的数量必须与本批次名单完全一致。`,
'- 名称必须与批次名单完全一致,不得增删改名。',
'- 只补全 backstory、personality、motivation、combatStyle 这 4 个字段,不要输出 backstoryReveal、skills、initialItems。',
'- 必须严格沿用框架中的 title、role、description、initialAffinity、relationshipHooks、tags 所表达的角色定位,不要改名,不要改阵营。',
'- backstory 必须写出角色和当前世界的具体关系,至少落到一个势力、一个地点、一个正在发生的局势变化,不要只写抽象气质或泛泛成长史。',
'- personality 不能只写单个形容词,要体现角色在这个世界里的处事习惯、应对压力的方式和与人相处的锋面。',
'- motivation 必须是“此刻正在推动角色行动”的现实目标,而不是空泛理想;它要和玩家目标、核心冲突或开局处境形成直接拉扯。',
'- combatStyle 要体现角色为什么会这样战斗,它最好能反映其身份、经历、所属势力或长期栖身的场景环境。',
roleType === 'story'
? '- 怪物型场景角色要在 backstory 或 combatStyle 中直接写出怪物特征、栖息环境或攻击方式。'
: '- 可扮演角色要体现成长空间、协作价值和明确的入队理由。',
'- 所有生成文本都必须使用中文。',
'- 每个字符串尽量简洁但不能空泛backstory/personality/motivation/combatStyle 控制在 18 到 56 个汉字内。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
return [
`请根据下面的世界框架,补全这一批${label}的背景章节、技能和初始物品。`,
'你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。',
'玩家原始设定:',
framework.settingText,
'',
'世界框架摘要:',
buildFrameworkSummaryText(framework, { maxLandmarks: 8 }),
'',
`本批次需要补全的${label}(名称必须原样保留):`,
roleOutlineText,
'',
'输出 JSON 模板:',
'{',
` "${key}": [`,
' {',
' "name": "角色名称",',
' "backstoryReveal": {',
' "publicSummary": "公开可见的背景摘要",',
' "chapters": [',
` { "id": "surface", "title": "表层来意", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[0]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[0]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[0]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "scar", "title": "旧事裂痕", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[1]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "hidden", "title": "隐藏执念", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[2]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" },`,
` { "id": "final", "title": "最终底牌", "affinityRequired": ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}, "teaser": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}好感可见的提示", "content": "${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES[3]}好感时解锁的背景内容", "contextSnippet": "可供后续剧情引用的摘要" }`,
' ]',
' },',
' "skills": [',
' { "name": "技能1", "summary": "技能说明", "style": "起手压制" },',
' { "name": "技能2", "summary": "技能说明", "style": "机动周旋" },',
' { "name": "技能3", "summary": "技能说明", "style": "爆发终结" }',
' ],',
' "initialItems": [',
' { "name": "初始物品1", "category": "武器", "quantity": 1, "rarity": "rare", "description": "物品说明", "tags": ["标签1", "标签2"] },',
' { "name": "初始物品2", "category": "消耗品", "quantity": 2, "rarity": "uncommon", "description": "物品说明", "tags": ["标签1", "标签2"] },',
' { "name": "初始物品3", "category": "专属物品", "quantity": 1, "rarity": "rare", "description": "物品说明", "tags": ["标签1", "标签2"] }',
' ]',
' }',
' ]',
'}',
'',
'要求:',
`- 只输出这批${label},不要输出其他角色、场景或额外顶层字段。`,
'- 这是一个完全独立的自定义世界;不要在公开背景、技能名、物品名或说明里直接带入“武侠”“仙侠”等现成世界名。',
`- ${key} 的数量必须与本批次名单完全一致。`,
'- 名称必须与批次名单完全一致,不得增删改名。',
'- 这一阶段只补全 backstoryReveal、skills、initialItems不要重复输出 title、role、description、backstory、personality、motivation、combatStyle、initialAffinity、relationshipHooks、tags。',
'- 背景章节、技能和初始物品必须严格围绕框架中的角色定位来写,不要改变阵营、身份或出现场景。',
'- backstoryReveal 的 4 章必须形成明显递进:第 1 章写表层来意与第一印象,第 2 章写旧伤或代价,第 3 章写角色真正隐瞒的线索,第 4 章写最终底牌或不可回避的真相。',
'- 每一章都必须紧贴当前世界设定,至少落到具体势力、地点、事件、制度、禁忌或关系链中的一项,不要写成可套用到任何世界的空泛心情。',
'- teaser 必须像“继续相处后能戳到的钩子”content 必须像“真正解锁后得到的新信息”contextSnippet 必须可直接被后续剧情复用,三者不要只是同一句话改写。',
'- skills 不只是职业标签,要体现角色的个人经历、所属阵营、地理环境或禁忌系统影响,尽量写出这个世界独有的招式语感。',
'- initialItems 不只是常规装备清单,至少要有一件能反映角色背景、关系或任务压力的私人物件。',
`- backstoryReveal.chapters 必须恰好 4 章affinityRequired 固定使用 ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES.join('、')}`,
'- 每个角色必须提供恰好 3 个 skills 和恰好 3 个 initialItems。',
'- initialItems.category 只能使用:武器、护甲、饰品、消耗品、材料、稀有品、专属物品。',
roleType === 'story'
? '- 怪物型角色仍然放进 storyNpcs并在 role、description、backstory、combatStyle、tags、backstoryReveal、skills 或 initialItems 中明确写出怪物特征、栖息环境、攻击方式或异形外观。'
: '- 可扮演角色要保持明确的成长空间、协作价值和入队理由。',
'- 所有生成文本都必须使用中文。',
'- 每个字符串尽量简洁但要有信息量backstoryReveal.publicSummary 控制在 14 到 36 个汉字内backstoryReveal.teaser 控制在 12 到 28 个汉字内backstoryReveal.content 控制在 20 到 64 个汉字内contextSnippet 控制在 12 到 36 个汉字内skills.summary 和 initialItems.description 控制在 12 到 32 个汉字内。',
'- 返回前自检:必须是一个能被 JSON.parse 直接解析的单个 JSON 对象。',
].join('\n');
}
export function buildCustomWorldRoleBatchJsonRepairPrompt(params: {
responseText: string;
roleType: CustomWorldGenerationRoleBatchType;
expectedNames: string[];
stage: CustomWorldGenerationRoleBatchStage;
}) {
const { responseText, roleType, expectedNames, stage } = params;
const key = roleType === 'playable' ? 'playableNpcs' : 'storyNpcs';
if (stage === 'narrative') {
return [
`下面这段文本本应是自定义世界${roleType === 'playable' ? '可扮演角色' : '场景角色'}叙事设定批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。`,
'请只输出修复后的 JSON 对象。',
`顶层必须只包含一个 ${key} 数组。`,
`这个数组里只能保留这些角色名:${expectedNames.join('、')}`,
'名称必须与名单完全一致,不得增删改名;如果原文遗漏,可按名单顺序补齐占位对象。',
'每个角色都必须包含name、backstory、personality、motivation、combatStyle。',
'如果缺少字段:字符串补空字符串。',
'不要输出 backstoryReveal、skills、initialItems也不要新增名单外的角色。',
'原始文本:',
responseText.trim(),
].join('\n');
}
return [
`下面这段文本本应是自定义世界${roleType === 'playable' ? '可扮演角色' : '场景角色'}档案补全批次的单个 JSON 对象,但当前不能被 JSON.parse 直接解析。`,
'请只输出修复后的 JSON 对象。',
`顶层必须只包含一个 ${key} 数组。`,
`这个数组里只能保留这些角色名:${expectedNames.join('、')}`,
'名称必须与名单完全一致,不得增删改名;如果原文遗漏,可按名单顺序补齐占位对象。',
'每个角色都必须包含name、backstoryReveal、skills、initialItems。',
`backstoryReveal 必须包含 publicSummary 和 4 个 chapterschapters.affinityRequired 固定为 ${CUSTOM_WORLD_BACKSTORY_CHAPTER_AFFINITIES.join('、')}`,
'skills 默认补成 3 个对象,每个对象包含 name、summary、styleinitialItems 默认补成 3 个对象,每个对象包含 name、category、quantity、rarity、description、tags。',
'不要输出 backstory、personality、motivation、combatStyle、landmarks也不要新增名单外的角色。',
'原始文本:',
responseText.trim(),
].join('\n');
}
function clampSceneImageText(value: string, maxLength: number) {
const normalized = value.trim().replace(/\s+/g, ' ');
if (!normalized) {
return '';
}
if (normalized.length <= maxLength) {
return normalized;
}
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trim()}`;
}
function describeDangerLevel(dangerLevel: string) {
const normalized = dangerLevel.trim().toLowerCase();
if (normalized === 'low' || normalized === '低')
return '气氛相对平静,但暗藏细节张力';
if (normalized === 'medium' || normalized === '中')
return '带有明确的探索压力与潜在威胁';
if (normalized === 'high' || normalized === '高')
return '危险感强烈,空间中有明显压迫感';
if (normalized === 'extreme' || normalized === '极高')
return '极端危险,环境本身就像会吞没闯入者';
return dangerLevel.trim()
? `危险氛围:${dangerLevel.trim()}`
: '危险气质保持克制但不可忽视';
}
export const DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT = [
'文字',
'水印',
'logo',
'UI界面',
'对话框',
'边框',
'人物近景特写',
'多人合照',
'模糊',
'低清晰度',
'畸形建筑',
'现代车辆',
'监控摄像头',
].join('');
export function buildCustomWorldSceneImagePrompt(
profile: Pick<
CustomWorldProfile,
'name' | 'subtitle' | 'summary' | 'tone' | 'playerGoal' | 'settingText'
>,
landmark: Pick<CustomWorldLandmark, 'name' | 'description' | 'dangerLevel'>,
userPrompt = '',
options: {
hasReferenceImage?: boolean;
} = {},
) {
const worldName = clampSceneImageText(profile.name, 18) || '未命名世界';
const worldSubtitle = clampSceneImageText(profile.subtitle, 18);
const worldTone = clampSceneImageText(profile.tone, 48);
const worldGoal = clampSceneImageText(profile.playerGoal, 48);
const worldSummary = clampSceneImageText(profile.summary, 72);
const worldSetting = clampSceneImageText(profile.settingText, 72);
const landmarkName = clampSceneImageText(landmark.name, 18) || '未命名场景';
const landmarkDescription = clampSceneImageText(landmark.description, 96);
const requestedVisual = clampSceneImageText(userPrompt, 120);
const dangerMood = describeDangerLevel(landmark.dangerLevel);
return [
'为横版 16:9 2D RPG 生成高完成度像素风场景背景,适合作为剧情探索与战斗底图。',
'画面构图必须严格按上下 1:1 分区:上半部分严格控制在整张图的 1/2 高度内,只描绘场景远景与中远景轮廓,不要让背景内容向下侵占超过半屏。',
'下半部分严格占据整张图的 1/2 高度,用于玩家角色站位与展示,必须是模拟 3D 游戏视角的地面近景,有明确的透视延伸和近大远小关系,不是平铺的 2D 侧视地面。',
'下半部分的内容必须是明确可站立的地面本体,例如道路、石板、平台、广场、甲板、沙地或草地,要有连续、稳定、可落脚的站位逻辑,不能只是装饰性前景、坑洞、障碍堆、栏杆带或不可通行的景物。',
'下半部分地面近景要保持相对简洁、低细节、轮廓清楚、便于角色站立,不要堆满道具、植被、碎石、栏杆或复杂装饰。',
options.hasReferenceImage
? '已提供一张自定义参考图,请沿用其构图、镜头或氛围线索,同时继续满足本次场景需求。'
: '',
`世界:${worldName}${worldSubtitle ? `${worldSubtitle}` : ''}`,
worldSetting ? `玩家设定:${worldSetting}` : '',
worldSummary ? `世界概述:${worldSummary}` : '',
worldTone ? `整体基调:${worldTone}` : '',
worldGoal ? `玩家目标关联:${worldGoal}` : '',
`场景名称:${landmarkName}`,
landmarkDescription ? `场景描述:${landmarkDescription}` : '',
requestedVisual ? `本次想要生成的画面内容:${requestedVisual}` : '',
`${dangerMood}`,
'不要出现 UI、字幕、文字、水印、logo 或装饰边框,人物仅可作为很小的远景剪影,画面重点放在场景本身,不要遮挡下半部分的角色展示区域。',
]
.filter(Boolean)
.join('');
}