This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

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

View File

@@ -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;

View File

@@ -47,6 +47,7 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
async getSettings() {
return {
musicVolume: 0.42,
platformTheme: 'light',
};
},
async putSettings(_userId, settings) {

View File

@@ -39,6 +39,7 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
async getSettings() {
return {
musicVolume: 0.42,
platformTheme: 'light',
};
},
async putSettings(_userId, settings) {

View File

@@ -40,6 +40,7 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
async getSettings() {
return {
musicVolume: 0.42,
platformTheme: 'light',
};
},
async putSettings(_userId, settings) {

View File

@@ -39,6 +39,7 @@ function createRuntimeRepositoryStub(): RuntimeRepositoryPort {
async getSettings() {
return {
musicVolume: 0.42,
platformTheme: 'light',
};
},
async putSettings(_userId, settings) {

View File

@@ -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}`,

View File

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