1
This commit is contained in:
@@ -3,6 +3,12 @@ import type {
|
||||
CustomWorldFoundationDraftLandmark,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
|
||||
import { badRequest } from '../errors.js';
|
||||
import {
|
||||
buildCustomWorldAgentCharacterExpansionPrompt,
|
||||
buildCustomWorldAgentLandmarkExpansionPrompt,
|
||||
CUSTOM_WORLD_AGENT_CHARACTER_EXPANSION_SYSTEM_PROMPT,
|
||||
CUSTOM_WORLD_AGENT_LANDMARK_EXPANSION_SYSTEM_PROMPT,
|
||||
} from '../prompts/customWorldAgentPrompts.js';
|
||||
import {
|
||||
getWorldFoundationCardId,
|
||||
normalizeFoundationDraftProfile,
|
||||
@@ -438,22 +444,18 @@ async function requestCharacterSuggestionsFromLlm(params: {
|
||||
params.profile.summary;
|
||||
|
||||
const content = await params.llmClient.requestMessageContent({
|
||||
systemPrompt:
|
||||
'你负责为当前游戏世界底稿补 1 到 3 个新角色。只能输出 JSON 数组,不要输出任何额外说明。',
|
||||
userPrompt: [
|
||||
`当前世界:${params.profile.name}`,
|
||||
`世界摘要:${params.profile.summary}`,
|
||||
`创作意图摘要:${creatorIntentSummary}`,
|
||||
`参考锚点:${anchorSummary}`,
|
||||
`已有角色:${getAllCharacters(params.profile)
|
||||
systemPrompt: CUSTOM_WORLD_AGENT_CHARACTER_EXPANSION_SYSTEM_PROMPT,
|
||||
userPrompt: buildCustomWorldAgentCharacterExpansionPrompt({
|
||||
worldName: params.profile.name,
|
||||
worldSummary: params.profile.summary,
|
||||
creatorIntentSummary,
|
||||
anchorSummary,
|
||||
existingNames: getAllCharacters(params.profile)
|
||||
.slice(0, 10)
|
||||
.map((entry) => entry.name)
|
||||
.join('、') || '暂无'}`,
|
||||
`数量:${params.count}`,
|
||||
`补充要求:${params.promptSeed || '没有额外要求,围绕当前底稿自然扩展。'}`,
|
||||
'返回 JSON 数组。每个对象字段只允许包含:name, role, publicMask, hiddenHook, relationToPlayer, summary, threadIds。',
|
||||
'threadIds 必须优先引用现有线程 id。',
|
||||
].join('\n'),
|
||||
.map((entry) => entry.name),
|
||||
count: params.count,
|
||||
promptSeed: params.promptSeed,
|
||||
}),
|
||||
timeoutMs: 45000,
|
||||
debugLabel: 'custom-world-agent-generate-characters',
|
||||
});
|
||||
@@ -478,22 +480,18 @@ async function requestLandmarkSuggestionsFromLlm(params: {
|
||||
params.profile.summary;
|
||||
|
||||
const content = await params.llmClient.requestMessageContent({
|
||||
systemPrompt:
|
||||
'你负责为当前游戏世界底稿补 1 到 3 个新地点。只能输出 JSON 数组,不要输出任何额外说明。',
|
||||
userPrompt: [
|
||||
`当前世界:${params.profile.name}`,
|
||||
`世界摘要:${params.profile.summary}`,
|
||||
`创作意图摘要:${creatorIntentSummary}`,
|
||||
`参考锚点:${anchorSummary}`,
|
||||
`已有地点:${params.profile.landmarks
|
||||
systemPrompt: CUSTOM_WORLD_AGENT_LANDMARK_EXPANSION_SYSTEM_PROMPT,
|
||||
userPrompt: buildCustomWorldAgentLandmarkExpansionPrompt({
|
||||
worldName: params.profile.name,
|
||||
worldSummary: params.profile.summary,
|
||||
creatorIntentSummary,
|
||||
anchorSummary,
|
||||
existingNames: params.profile.landmarks
|
||||
.slice(0, 10)
|
||||
.map((entry) => entry.name)
|
||||
.join('、') || '暂无'}`,
|
||||
`数量:${params.count}`,
|
||||
`补充要求:${params.promptSeed || '没有额外要求,围绕当前底稿自然扩展。'}`,
|
||||
'返回 JSON 数组。每个对象字段只允许包含:name, purpose, mood, dangerLevel, secret, summary, threadIds, characterIds。',
|
||||
'threadIds / characterIds 必须优先引用现有对象 id。',
|
||||
].join('\n'),
|
||||
.map((entry) => entry.name),
|
||||
count: params.count,
|
||||
promptSeed: params.promptSeed,
|
||||
}),
|
||||
timeoutMs: 45000,
|
||||
debugLabel: 'custom-world-agent-generate-landmarks',
|
||||
});
|
||||
|
||||
@@ -8,6 +8,10 @@ import type {
|
||||
EightAnchorContent,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
|
||||
import { parseJsonResponseText } from '../../../packages/shared/src/llm/parsers.js';
|
||||
import {
|
||||
FOUNDATION_JSON_ONLY_SYSTEM_PROMPT,
|
||||
FOUNDATION_JSON_REPAIR_SYSTEM_PROMPT,
|
||||
} from '../prompts/customWorldAgentPrompts.js';
|
||||
import {
|
||||
buildCustomWorldFrameworkJsonRepairPrompt,
|
||||
buildCustomWorldFrameworkPrompt,
|
||||
@@ -770,13 +774,6 @@ function buildChapter(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const FOUNDATION_JSON_ONLY_SYSTEM_PROMPT = `你是严格的世界草稿 JSON 生成器。
|
||||
只输出一个 JSON 对象,不要输出 Markdown、代码块、解释或额外文字。`;
|
||||
const FOUNDATION_JSON_REPAIR_SYSTEM_PROMPT = `你是 JSON 修复器。
|
||||
你会收到一段本应为单个 JSON 对象的文本。
|
||||
你的唯一任务是把它修复成能被 JSON.parse 直接解析的单个 JSON 对象。
|
||||
不要输出 Markdown、代码块、解释、注释或额外文字。`;
|
||||
|
||||
const FOUNDATION_DRAFT_PLAYABLE_COUNT = 3;
|
||||
const FOUNDATION_DRAFT_STORY_COUNT = 6;
|
||||
const FOUNDATION_DRAFT_LANDMARK_COUNT = 4;
|
||||
|
||||
@@ -47,6 +47,7 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
|
||||
async getSettings() {
|
||||
return {
|
||||
musicVolume: 0.42,
|
||||
platformTheme: 'light',
|
||||
};
|
||||
},
|
||||
async putSettings(_userId, settings) {
|
||||
|
||||
@@ -39,6 +39,7 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
|
||||
async getSettings() {
|
||||
return {
|
||||
musicVolume: 0.42,
|
||||
platformTheme: 'light',
|
||||
};
|
||||
},
|
||||
async putSettings(_userId, settings) {
|
||||
|
||||
@@ -40,6 +40,7 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
|
||||
async getSettings() {
|
||||
return {
|
||||
musicVolume: 0.42,
|
||||
platformTheme: 'light',
|
||||
};
|
||||
},
|
||||
async putSettings(_userId, settings) {
|
||||
|
||||
@@ -39,6 +39,7 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
|
||||
async getSettings() {
|
||||
return {
|
||||
musicVolume: 0.42,
|
||||
platformTheme: 'light',
|
||||
};
|
||||
},
|
||||
async putSettings(_userId, settings) {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { parseJsonResponseText } from '../../../packages/shared/src/llm/parsers.js';
|
||||
import { badRequest } from '../errors.js';
|
||||
import {
|
||||
buildLandmarkPrompt,
|
||||
buildPlayablePrompt,
|
||||
buildStoryPrompt,
|
||||
CUSTOM_WORLD_ENTITY_GENERATOR_SYSTEM_PROMPT,
|
||||
} from '../prompts/customWorldEntityPrompts.js';
|
||||
import type { UpstreamLlmClient } from './llmClient.js';
|
||||
|
||||
type CustomWorldEntityKind = 'playable' | 'story' | 'landmark';
|
||||
@@ -319,69 +325,6 @@ function normalizeProfile(value: unknown): ParsedProfile {
|
||||
};
|
||||
}
|
||||
|
||||
function buildRoleReferenceText(roles: ParsedRole[], emptyText: string) {
|
||||
if (roles.length === 0) {
|
||||
return emptyText;
|
||||
}
|
||||
|
||||
return roles
|
||||
.slice(0, 12)
|
||||
.map(
|
||||
(role, index) =>
|
||||
`${index + 1}. ${role.name} / ${role.title || role.role} / 身份:${
|
||||
role.role || '未写'
|
||||
} / 描述:${role.description || '未写'} / 背景:${
|
||||
role.backstory || '未写'
|
||||
} / 性格:${role.personality || '未写'} / 动机:${
|
||||
role.motivation || '未写'
|
||||
} / 形象:${role.visualDescription || '未写'} / 动作表现:${
|
||||
role.actionDescription || '未写'
|
||||
} / 场景画面:${role.sceneVisualDescription || '未写'} / 标签:${
|
||||
role.tags.join('、') || '暂无'
|
||||
}`,
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function buildLandmarkReferenceText(profile: ParsedProfile) {
|
||||
if (profile.landmarks.length === 0) {
|
||||
return '当前还没有场景设定。';
|
||||
}
|
||||
|
||||
const storyNpcById = new Map(profile.storyNpcs.map((npc) => [npc.id, npc]));
|
||||
const landmarkById = new Map(
|
||||
profile.landmarks.map((landmark) => [landmark.id, landmark]),
|
||||
);
|
||||
|
||||
return profile.landmarks
|
||||
.slice(0, 12)
|
||||
.map((landmark, index) => {
|
||||
const sceneNpcNames = landmark.sceneNpcIds
|
||||
.map((npcId) => storyNpcById.get(npcId)?.name ?? '')
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
const connectionNames = landmark.connections
|
||||
.map((connection) => {
|
||||
const targetName =
|
||||
landmarkById.get(connection.targetLandmarkId)?.name ||
|
||||
connection.targetLandmarkId;
|
||||
return `${targetName}(${connection.relativePosition} / ${
|
||||
connection.summary || '无说明'
|
||||
})`;
|
||||
})
|
||||
.join('、');
|
||||
|
||||
return `${index + 1}. ${landmark.name} / 危险度:${
|
||||
landmark.dangerLevel || 'medium'
|
||||
} / 描述:${landmark.description || '未写'} / 画面:${
|
||||
landmark.visualDescription || '未写'
|
||||
} / 场景角色:${
|
||||
sceneNpcNames || '暂无'
|
||||
} / 连接:${connectionNames || '暂无'}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function buildUniqueRoleName(existingNames: Set<string>, startIndex: number) {
|
||||
for (let attempt = 0; attempt < 120; attempt += 1) {
|
||||
const index = startIndex + attempt;
|
||||
@@ -563,148 +506,6 @@ function buildFallbackLandmarkDraft(profile: ParsedProfile) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildPlayablePrompt(profile: ParsedProfile) {
|
||||
return [
|
||||
`世界名:${profile.name}`,
|
||||
`当前世界设定:${profile.settingText || profile.summary || '未提供额外设定。'}`,
|
||||
`世界摘要:${profile.summary || '未填写'}`,
|
||||
`世界基调:${profile.tone || '未填写'}`,
|
||||
`玩家主线目标:${profile.playerGoal || '未填写'}`,
|
||||
`当前可扮演角色设定:\n${buildRoleReferenceText(profile.playableNpcs, '当前还没有可扮演角色。')}`,
|
||||
`当前场景角色设定:\n${buildRoleReferenceText(profile.storyNpcs, '当前还没有场景角色。')}`,
|
||||
`当前场景设定:\n${buildLandmarkReferenceText(profile)}`,
|
||||
'请基于上面全部上下文,生成 1 名新的“可扮演角色”。',
|
||||
'要求:',
|
||||
'- 必须与当前世界设定、已有可扮演角色、已有场景角色、已有场景形成互补,不要重复定位。',
|
||||
'- 必须保留明确的协作价值、成长空间和入队理由。',
|
||||
'- 不要生成泛用模板角色,必须让角色与当前世界的具体势力、地点、冲突或禁忌发生绑定。',
|
||||
'- visualDescription 只写与角色设定相关的外形、服装、材质、武器、体态、色彩和识别特征,禁止写角色以外的周边环境等与角色不想管的设定。',
|
||||
'- actionDescription 只写这个角色的动作表现与战斗气质,不要写镜头切换或参数。',
|
||||
'- sceneVisualDescription 只写该角色首次登场或主要活动区域的场景画面感,不要写“提示词”字样。',
|
||||
'- 只返回 JSON,不要输出解释或 Markdown。',
|
||||
'JSON 结构:',
|
||||
'{',
|
||||
' "playableNpc": {',
|
||||
' "name": "角色名",',
|
||||
' "title": "称号",',
|
||||
' "role": "身份",',
|
||||
' "description": "一句到两句定位描述",',
|
||||
' "visualDescription": "角色形象描述",',
|
||||
' "actionDescription": "动作表现描述",',
|
||||
' "sceneVisualDescription": "角色关联场景画面描述",',
|
||||
' "backstory": "背景经历",',
|
||||
' "personality": "性格特点",',
|
||||
' "motivation": "当前动机",',
|
||||
' "combatStyle": "战斗风格",',
|
||||
' "initialAffinity": 22,',
|
||||
' "relationshipHooks": ["关系钩子1", "关系钩子2", "关系钩子3"],',
|
||||
' "tags": ["标签1", "标签2", "标签3"],',
|
||||
' "publicSummary": "公开背景摘要",',
|
||||
' "chapterTeasers": ["表层来意", "旧事裂痕", "隐藏执念", "最终底牌"],',
|
||||
' "chapterContents": ["对应正文1", "对应正文2", "对应正文3", "对应正文4"],',
|
||||
' "skills": [',
|
||||
' { "name": "技能1", "summary": "说明", "style": "风格" },',
|
||||
' { "name": "技能2", "summary": "说明", "style": "风格" },',
|
||||
' { "name": "技能3", "summary": "说明", "style": "风格" }',
|
||||
' ],',
|
||||
' "initialItems": [',
|
||||
' { "name": "物品1", "category": "武器", "quantity": 1, "rarity": "rare", "description": "说明", "tags": ["标签"] },',
|
||||
' { "name": "物品2", "category": "专属物品", "quantity": 1, "rarity": "rare", "description": "说明", "tags": ["标签"] },',
|
||||
' { "name": "物品3", "category": "消耗品", "quantity": 2, "rarity": "uncommon", "description": "说明", "tags": ["标签"] }',
|
||||
' ]',
|
||||
' }',
|
||||
'}',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildStoryPrompt(profile: ParsedProfile) {
|
||||
return [
|
||||
`世界名:${profile.name}`,
|
||||
`当前世界设定:${profile.settingText || profile.summary || '未提供额外设定。'}`,
|
||||
`世界摘要:${profile.summary || '未填写'}`,
|
||||
`世界基调:${profile.tone || '未填写'}`,
|
||||
`玩家主线目标:${profile.playerGoal || '未填写'}`,
|
||||
`当前可扮演角色设定:\n${buildRoleReferenceText(profile.playableNpcs, '当前还没有可扮演角色。')}`,
|
||||
`当前场景角色设定:\n${buildRoleReferenceText(profile.storyNpcs, '当前还没有场景角色。')}`,
|
||||
`当前场景设定:\n${buildLandmarkReferenceText(profile)}`,
|
||||
'请基于上面全部上下文,生成 1 名新的“场景角色”。',
|
||||
'要求:',
|
||||
'- 必须与当前世界设定、已有可扮演角色、已有场景角色、已有场景形成互补,不要重复定位。',
|
||||
'- 必须像能直接进入游戏的场景角色,而不是抽象设定条目。',
|
||||
'- 角色应与具体场景、关系链或局势变化发生绑定。',
|
||||
'- visualDescription 只写与角色设定匹配的外形、服装、材质、武器、体态、色彩和识别特征,不要写“提示词”、镜头参数或构图规则。',
|
||||
'- actionDescription 只写这个角色的动作表现与战斗气质,不要写镜头切换或参数。',
|
||||
'- sceneVisualDescription 只写该角色首次登场或主要活动区域的场景画面感,不要写“提示词”字样。',
|
||||
'- 只返回 JSON,不要输出解释或 Markdown。',
|
||||
'JSON 结构:',
|
||||
'{',
|
||||
' "storyNpc": {',
|
||||
' "name": "角色名",',
|
||||
' "title": "称号",',
|
||||
' "role": "身份",',
|
||||
' "description": "一句到两句定位描述",',
|
||||
' "visualDescription": "角色形象描述",',
|
||||
' "actionDescription": "动作表现描述",',
|
||||
' "sceneVisualDescription": "角色关联场景画面描述",',
|
||||
' "backstory": "背景经历",',
|
||||
' "personality": "性格特点",',
|
||||
' "motivation": "当前动机",',
|
||||
' "combatStyle": "战斗风格",',
|
||||
' "initialAffinity": 6,',
|
||||
' "relationshipHooks": ["关系钩子1", "关系钩子2", "关系钩子3"],',
|
||||
' "tags": ["标签1", "标签2", "标签3"],',
|
||||
' "publicSummary": "公开背景摘要",',
|
||||
' "chapterTeasers": ["表层来意", "旧事裂痕", "隐藏执念", "最终底牌"],',
|
||||
' "chapterContents": ["对应正文1", "对应正文2", "对应正文3", "对应正文4"],',
|
||||
' "skills": [',
|
||||
' { "name": "技能1", "summary": "说明", "style": "风格" },',
|
||||
' { "name": "技能2", "summary": "说明", "style": "风格" },',
|
||||
' { "name": "技能3", "summary": "说明", "style": "风格" }',
|
||||
' ],',
|
||||
' "initialItems": [',
|
||||
' { "name": "物品1", "category": "武器", "quantity": 1, "rarity": "rare", "description": "说明", "tags": ["标签"] },',
|
||||
' { "name": "物品2", "category": "专属物品", "quantity": 1, "rarity": "rare", "description": "说明", "tags": ["标签"] },',
|
||||
' { "name": "物品3", "category": "消耗品", "quantity": 2, "rarity": "uncommon", "description": "说明", "tags": ["标签"] }',
|
||||
' ]',
|
||||
' }',
|
||||
'}',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildLandmarkPrompt(profile: ParsedProfile) {
|
||||
return [
|
||||
`世界名:${profile.name}`,
|
||||
`当前世界设定:${profile.settingText || profile.summary || '未提供额外设定。'}`,
|
||||
`世界摘要:${profile.summary || '未填写'}`,
|
||||
`世界基调:${profile.tone || '未填写'}`,
|
||||
`玩家主线目标:${profile.playerGoal || '未填写'}`,
|
||||
`当前可扮演角色设定:\n${buildRoleReferenceText(profile.playableNpcs, '当前还没有可扮演角色。')}`,
|
||||
`当前场景角色设定:\n${buildRoleReferenceText(profile.storyNpcs, '当前还没有场景角色。')}`,
|
||||
`当前场景设定:\n${buildLandmarkReferenceText(profile)}`,
|
||||
'请基于上面全部上下文,生成 1 个新的“场景”。',
|
||||
'要求:',
|
||||
'- 必须与当前世界设定、已有可扮演角色、已有场景角色、已有场景形成互补,不要重复。',
|
||||
'- 必须给出适合出现在这个新场景里的 sceneNpcNames,且只能从已有场景角色里选择至少 3 个名字。',
|
||||
'- 必须给出 connections,且 targetLandmarkName 只能引用已有场景名,不要连向自己。',
|
||||
'- visualDescription 只写这个场景的空间层次、地面、主体建筑或自然景观、氛围、色彩和可见装置,不要写“提示词”、镜头参数或构图规则。',
|
||||
'- 只返回 JSON,不要输出解释或 Markdown。',
|
||||
'JSON 结构:',
|
||||
'{',
|
||||
' "landmark": {',
|
||||
' "name": "场景名",',
|
||||
' "description": "场景描述",',
|
||||
' "visualDescription": "场景画面描述",',
|
||||
' "dangerLevel": "low|medium|high|extreme",',
|
||||
' "sceneNpcNames": ["场景角色1", "场景角色2", "场景角色3"],',
|
||||
' "connections": [',
|
||||
' { "targetLandmarkName": "已有场景名", "relativePosition": "forward", "summary": "通路说明" },',
|
||||
' { "targetLandmarkName": "已有场景名", "relativePosition": "inside", "summary": "通路说明" }',
|
||||
' ]',
|
||||
' }',
|
||||
'}',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function ensureUniqueName(name: string, existingNames: string[], fallbackName: string) {
|
||||
const normalized = name.trim() || fallbackName;
|
||||
if (!existingNames.includes(normalized)) {
|
||||
@@ -1040,8 +841,7 @@ async function requestGeneratedEntity(
|
||||
: buildLandmarkPrompt(profile);
|
||||
|
||||
const content = await llmClient.requestMessageContent({
|
||||
systemPrompt:
|
||||
'你是游戏世界编辑器的实体生成器。你必须只返回可解析 JSON,不要输出解释、前言或 Markdown。',
|
||||
systemPrompt: CUSTOM_WORLD_ENTITY_GENERATOR_SYSTEM_PROMPT,
|
||||
userPrompt,
|
||||
timeoutMs: 45000,
|
||||
debugLabel: `custom-world-generate-${kind}`,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { parseJsonResponseText } from '../../../packages/shared/src/llm/parsers.js';
|
||||
import { badRequest } from '../errors.js';
|
||||
import {
|
||||
buildCustomWorldSceneNpcPrompt,
|
||||
CUSTOM_WORLD_SCENE_NPC_SYSTEM_PROMPT,
|
||||
} from '../prompts/customWorldSceneNpcPrompts.js';
|
||||
import type { UpstreamLlmClient } from './llmClient.js';
|
||||
|
||||
type SceneNpcGenerationInput = {
|
||||
@@ -288,86 +292,6 @@ function buildFallbackDraft(
|
||||
};
|
||||
}
|
||||
|
||||
function buildPrompt(
|
||||
profile: ParsedProfile,
|
||||
landmark: ParsedLandmark,
|
||||
sceneNpcs: ParsedStoryNpc[],
|
||||
otherNpcs: ParsedStoryNpc[],
|
||||
) {
|
||||
const sceneNpcSummary = sceneNpcs.length
|
||||
? sceneNpcs
|
||||
.map(
|
||||
(npc, index) =>
|
||||
`${index + 1}. ${npc.name} / ${npc.title || npc.role} / ${npc.description || '无描述'} / 性格:${npc.personality || '未写'} / 动机:${npc.motivation || '未写'}`,
|
||||
)
|
||||
.join('\n')
|
||||
: '当前场景还没有已加入 NPC。';
|
||||
|
||||
const reserveNpcSummary = otherNpcs.length
|
||||
? otherNpcs
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
(npc, index) =>
|
||||
`${index + 1}. ${npc.name} / ${npc.title || npc.role} / ${npc.description || '无描述'}`,
|
||||
)
|
||||
.join('\n')
|
||||
: '暂无其他场景角色参考。';
|
||||
|
||||
const landmarkSummary = profile.landmarks
|
||||
.slice(0, 10)
|
||||
.map(
|
||||
(entry, index) =>
|
||||
`${index + 1}. ${entry.name} / 危险度:${entry.dangerLevel || '中'} / ${entry.description || '无描述'}`,
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
return [
|
||||
`世界名:${profile.name}`,
|
||||
`世界设定:${profile.settingText || '未提供额外设定文本。'}`,
|
||||
`当前目标场景:${landmark.name}`,
|
||||
`场景描述:${landmark.description || '未填写'}`,
|
||||
`危险度:${landmark.dangerLevel || '中'}`,
|
||||
`当前场景已加入 NPC:\n${sceneNpcSummary}`,
|
||||
`其他可参考 NPC:\n${reserveNpcSummary}`,
|
||||
`世界内其他场景概览:\n${landmarkSummary}`,
|
||||
'请生成 1 名适合加入当前场景的新 NPC。',
|
||||
'要求:',
|
||||
'- 必须与当前场景气质、危险度、已有 NPC 分工互补,不要和已有 NPC 重复。',
|
||||
'- 角色要像真正可落地到游戏里的场景角色,不要写成抽象设定。',
|
||||
'- 关系钩子、技能、初始物品都要可直接进入编辑器。',
|
||||
'- 返回 JSON,不要额外解释。',
|
||||
'JSON 结构:',
|
||||
'{',
|
||||
' "npc": {',
|
||||
' "name": "角色名",',
|
||||
' "title": "头衔",',
|
||||
' "role": "身份",',
|
||||
' "description": "一句到两句角色描述",',
|
||||
' "backstory": "背景",',
|
||||
' "personality": "性格",',
|
||||
' "motivation": "动机",',
|
||||
' "combatStyle": "战斗风格",',
|
||||
' "initialAffinity": 6,',
|
||||
' "relationshipHooks": ["关系钩子1", "关系钩子2", "关系钩子3"],',
|
||||
' "tags": ["标签1", "标签2", "标签3"],',
|
||||
' "publicSummary": "公开背景摘要",',
|
||||
' "chapterTeasers": ["表层来意", "旧事裂痕", "隐藏执念", "最终底牌"],',
|
||||
' "chapterContents": ["对应正文1", "对应正文2", "对应正文3", "对应正文4"],',
|
||||
' "skills": [',
|
||||
' { "name": "技能1", "summary": "说明", "style": "风格" },',
|
||||
' { "name": "技能2", "summary": "说明", "style": "风格" },',
|
||||
' { "name": "技能3", "summary": "说明", "style": "风格" }',
|
||||
' ],',
|
||||
' "initialItems": [',
|
||||
' { "name": "物品1", "category": "分类", "quantity": 1, "rarity": "rare", "description": "说明", "tags": ["标签"] },',
|
||||
' { "name": "物品2", "category": "分类", "quantity": 1, "rarity": "uncommon", "description": "说明", "tags": ["标签"] },',
|
||||
' { "name": "物品3", "category": "分类", "quantity": 1, "rarity": "rare", "description": "说明", "tags": ["标签"] }',
|
||||
' ]',
|
||||
' }',
|
||||
'}',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function sanitizeGeneratedNpc(
|
||||
rawValue: unknown,
|
||||
profile: ParsedProfile,
|
||||
@@ -571,9 +495,13 @@ export async function generateSceneNpcForLandmark(
|
||||
|
||||
try {
|
||||
const content = await llmClient.requestMessageContent({
|
||||
systemPrompt:
|
||||
'你是游戏世界编辑器的角色生成器。你必须只返回可解析 JSON,不要输出解释、前言或 markdown 代码块之外的额外内容。',
|
||||
userPrompt: buildPrompt(profile, landmark, sceneNpcs, otherNpcs),
|
||||
systemPrompt: CUSTOM_WORLD_SCENE_NPC_SYSTEM_PROMPT,
|
||||
userPrompt: buildCustomWorldSceneNpcPrompt(
|
||||
profile,
|
||||
landmark,
|
||||
sceneNpcs,
|
||||
otherNpcs,
|
||||
),
|
||||
debugLabel: 'custom-world-scene-npc',
|
||||
});
|
||||
const parsed = parseJsonResponseText(content);
|
||||
|
||||
Reference in New Issue
Block a user