This commit is contained in:
2026-04-16 15:45:00 +08:00
parent 6363267bca
commit 91b63675eb
43 changed files with 5652 additions and 853 deletions

View File

@@ -37,17 +37,18 @@ import {
createEmptyCreatorIntentRecord,
type CustomWorldCreatorIntentRecord,
extractCreatorIntentPatch,
hasMeaningfulCreatorIntentRecord,
mergeCreatorIntentRecord,
normalizeCreatorIntentRecord,
} from './customWorldAgentIntentExtractionService.js';
import {
type CustomWorldAgentSessionRecord,
CustomWorldAgentSessionStore,
} from './customWorldAgentSessionStore.js';
import {
rebuildRoleAssetCoverage,
resolveRoleAssetStatusLabel,
} from './customWorldAgentRoleAssetStateService.js';
import {
type CustomWorldAgentSessionRecord,
CustomWorldAgentSessionStore,
} from './customWorldAgentSessionStore.js';
import type { UpstreamLlmClient } from './llmClient.js';
const PHASE2_FORCE_FAIL_TOKEN = '__phase1_force_fail__';
@@ -67,12 +68,14 @@ function sleep(ms: number) {
});
}
function buildSuggestedActions(params: {
stage?: CustomWorldAgentSessionRecord['stage'];
isReady?: boolean;
draftProfile?: unknown;
draftCards?: CustomWorldDraftCardSummary[];
} = {}): CustomWorldSuggestedAction[] {
function buildSuggestedActions(
params: {
stage?: CustomWorldAgentSessionRecord['stage'];
isReady?: boolean;
draftProfile?: unknown;
draftCards?: CustomWorldDraftCardSummary[];
} = {},
): CustomWorldSuggestedAction[] {
const profile = normalizeFoundationDraftProfile(params.draftProfile);
const actions: CustomWorldSuggestedAction[] = [
{
@@ -95,7 +98,8 @@ function buildSuggestedActions(params: {
}
if (
(params.stage === 'object_refining' || params.stage === 'visual_refining') &&
(params.stage === 'object_refining' ||
params.stage === 'visual_refining') &&
profile
) {
const worldCardId =
@@ -177,13 +181,13 @@ function buildOperation(type: CustomWorldAgentOperationRecord['type']) {
? '正在把这次设定改动写回草稿。'
: type === 'generate_characters'
? '正在围绕当前底稿补出新角色。'
: type === 'generate_landmarks'
? '正在围绕当前底稿补出新地点。'
: type === 'generate_role_assets'
? '正在准备角色资产工坊入口。'
: type === 'sync_role_assets'
? '正在把角色资产结果写回世界草稿。'
: '正在整理这一轮新增的世界锚点。';
: type === 'generate_landmarks'
? '正在围绕当前底稿补出新地点。'
: type === 'generate_role_assets'
? '正在准备角色资产工坊入口。'
: type === 'sync_role_assets'
? '正在把角色资产结果写回世界草稿。'
: '正在整理这一轮新增的世界锚点。';
return {
operationId: `operation-${crypto.randomBytes(10).toString('hex')}`,
@@ -287,9 +291,17 @@ function buildWelcomeMessage(params: {
pendingClarifications: CustomWorldPendingClarification[];
isReady: boolean;
}) {
const openingText = params.seedText
? `收到:${truncateText(params.seedText, 88)}`
: '想做一个什么样的世界?';
let openingText: string;
if (params.seedText) {
openingText = `收到:${truncateText(params.seedText, 88)}`;
} else {
// When user enters without saying anything, provide a welcoming introduction
const hasAnyAnchors = hasMeaningfulCreatorIntentRecord(params.intent);
openingText = hasAnyAnchors
? '继续聊聊你的世界设定吧。'
: '你好!我是你的世界设定助手,可以帮你一起构建游戏世界的核心设定。';
}
return composeAssistantReply({
openingText,
@@ -321,7 +333,86 @@ function buildAssistantMessage(params: {
} satisfies CustomWorldAgentMessage;
}
function buildAgentLlmPrompt(params: {
function buildAgentSystemPrompt(params: {
isReady: boolean;
hasAnyAnchors: boolean;
}) {
const baseInstructions = [
'你是一个专业的RPG游戏剧情策划通过对话帮助用户补全结构化世界锚点。',
'',
'# 核心原则',
'- 像创作者搭档,不要写系统说明,不要列规则,不要提到 JSON 或后端',
'- 用中文自然回复,语气专业但友好',
'- 不要重复追问用户已经明确回答过的信息',
'- 每次只聚焦一个关键问题,帮助用户高效推进',
'',
'# 输出格式',
'必须输出严格的 JSON 格式:{“reply”:”...”,”recommendedReplies”:[“...”,”...”,”...”]}',
'',
];
if (params.isReady) {
return [
...baseInstructions,
'# 当前阶段:设定已齐备',
'',
'## reply 字段要求',
'- 第一段:明确回应并收住用户刚刚给出的具体设定',
'- 第二段:明确告诉用户关键设定已经足够,可以生成第一版游戏草稿了',
'- 最后:自然询问是否现在开始生成草稿',
'- 整体要短,聚焦推进',
'',
'## recommendedReplies 字段要求',
'- 必须正好 3 条',
'- 每条都是用户下一句可以直接发送的话',
'- 第 1 条:表达开始生成草稿(例如:”现在开始生成草稿”)',
'- 第 2 条:让 Agent 总结当前设定(例如:”先总结一下当前设定”)',
'- 第 3 条:继续补充设定内容(例如:”我还想再补充一点”)',
].join('\n');
}
// When anchors are empty, use inspirational questioning strategy
if (!params.hasAnyAnchors) {
return [
...baseInstructions,
'# 当前阶段:初始启发',
'',
'## reply 字段要求',
'- 第一段:如果用户刚进入对话还没说话,用欢迎语气开场(例如:”想创造一个什么样的世界?”)',
'- 第一段:如果用户已经说了话,简短回应用户的输入',
'- 第二段:提出一个开放性、启发性的问题,帮助用户构思世界的核心概念',
'- 问题应该是高层次的,关于世界类型、主题、核心理念,而不是具体细节',
'- 例如:世界的整体风格、故事的核心主题、想传达的感觉',
'- 避免过早询问具体设定细节(如魔法系统、科技水平等)',
'',
'## recommendedReplies 字段要求',
'- 必须正好 3 条',
'- 3 条都是对当前问题的不同方向的回答',
'- 每条回答应该代表一种不同的世界类型或主题方向',
'- 回答要具体但不过于详细,给用户启发和选择空间',
].join('\n');
}
return [
...baseInstructions,
'# 当前阶段:收集设定中',
'',
'## reply 字段要求',
'- 第一段:明确回应并收住用户上一次给出的具体落地设定(不能只说”收到”)',
'- 第二段:固定只追问 1 个当前最关键、最能推进游戏设定的问题',
'- 这个问题必须帮助你更快拿到作品最核心的设定信息',
'- 必要时给一个很短的示例,帮助用户高效回答',
'',
'## recommendedReplies 字段要求',
'- 必须正好 3 条',
'- 3 条都必须是对当前这一个问题的直接回答',
'- 不允许继续提问',
'- 不允许写成”你先帮我””继续问我”这种让 Agent 行动的句子',
'- 回答要尽量具体,优先提供能推进作品设定的核心信息',
].join('\n');
}
function buildAgentUserPrompt(params: {
session: CustomWorldAgentSessionRecord;
latestUserText: string;
intent: CustomWorldCreatorIntentRecord;
@@ -338,52 +429,18 @@ function buildAgentLlmPrompt(params: {
.join('\n');
return [
'当前结构化世界锚点',
'# 当前结构化世界锚点',
buildCreatorIntentDisplayText(params.intent) || '暂无',
'',
'注意:上面这些已确认设定和下面的历史对话都算有效上下文。不要重复追问用户已经明确回答过的信息。',
`# 锚点是否齐备`,
params.isReady ? '是' : '否',
'',
`锚点是否齐备:${params.isReady ? '是' : '否'}`,
pendingQuestions ? `待确认问题:\n${pendingQuestions}` : '',
'',
'最近对话:',
pendingQuestions ? `# 待确认问题\n${pendingQuestions}\n` : '',
'# 最近对话',
recentMessages || '暂无',
'',
`用户最新输入${params.latestUserText}`,
'',
'请输出严格 JSON格式如下{"reply":"...","recommendedReplies":["...","...","..."]}',
'',
params.isReady
? [
'reply 字段要求:',
'- 用中文自然回复。',
'- 像创作者搭档,不要写系统说明,不要列规则,不要提到 JSON 或后端。',
'- 第一段先明确回应并收住用户刚刚给出的具体设定。',
'- 第二段明确告诉用户:关键设定已经足够,可以帮他生成第一版游戏草稿了。',
'- 最后固定补一句自然问题,询问是否现在开始生成草稿。',
'- 整体要短,聚焦推进。',
'recommendedReplies 字段要求:',
'- 必须正好 3 条。',
'- 每条都是用户下一句可以直接发送的话。',
'- 第 1 条必须表达开始生成草稿。',
'- 第 2 条应是让 Agent 先总结一下当前设定。',
'- 第 3 条应是用户还想再补充一点设定。',
].join('\n')
: [
'reply 字段要求:',
'- 用中文自然回复。',
'- 像创作者搭档,不要写系统说明,不要列规则,不要提到 JSON 或后端。',
'- 第一段必须明确回应并收住用户上一次给出的具体落地设定,不能只说“收到”。',
'- 第二段开始固定只追问 1 个当前最关键、最能推进游戏设定的问题。',
'- 这个问题必须帮助你更快拿到作品最核心的设定信息。',
'- 必要时给一个很短的示例,帮助用户高效回答。',
'recommendedReplies 字段要求:',
'- 必须正好 3 条。',
'- 3 条都必须是对当前这一个问题的直接回答。',
'- 不允许继续提问。',
'- 不允许写成“你先帮我”“继续问我”这种让 Agent 行动的句子。',
'- 回答要尽量具体,优先提供能推进作品设定的核心信息。',
].join('\n'),
'# 用户最新输入',
params.latestUserText,
]
.filter(Boolean)
.join('\n');
@@ -395,8 +452,7 @@ function parseAssistantTurnJson(text: string) {
reply?: unknown;
recommendedReplies?: unknown;
};
const reply =
typeof parsed.reply === 'string' ? parsed.reply.trim() : '';
const reply = typeof parsed.reply === 'string' ? parsed.reply.trim() : '';
const recommendedReplies = Array.isArray(parsed.recommendedReplies)
? parsed.recommendedReplies
.map((item) => (typeof item === 'string' ? item.trim() : ''))
@@ -553,7 +609,9 @@ export class CustomWorldAgentOrchestrator {
private readonly sessionStore: CustomWorldAgentSessionStore,
private readonly llmClient: UpstreamLlmClient | null = null,
) {
this.foundationDraftService = new CustomWorldAgentFoundationDraftService();
this.foundationDraftService = new CustomWorldAgentFoundationDraftService(
llmClient,
);
this.draftCompiler = new CustomWorldAgentDraftCompiler();
this.entityGenerationService = new CustomWorldAgentEntityGenerationService(
llmClient,
@@ -727,7 +785,9 @@ export class CustomWorldAgentOrchestrator {
session.draftCards.length > 0,
);
if (!hasDraftFoundation) {
throw badRequest(`${payload.action} requires an existing draft foundation`);
throw badRequest(
`${payload.action} requires an existing draft foundation`,
);
}
}
@@ -793,7 +853,9 @@ export class CustomWorldAgentOrchestrator {
if (payload.action === 'generate_role_assets') {
if (!Array.isArray(payload.roleIds) || payload.roleIds.length !== 1) {
throw badRequest('generate_role_assets currently requires exactly one roleId');
throw badRequest(
'generate_role_assets currently requires exactly one roleId',
);
}
const operation = buildOperation('generate_role_assets');
@@ -814,7 +876,10 @@ export class CustomWorldAgentOrchestrator {
if (!payload.roleId.trim()) {
throw badRequest('sync_role_assets requires roleId');
}
if (!payload.portraitPath.trim() || !payload.generatedVisualAssetId.trim()) {
if (
!payload.portraitPath.trim() ||
!payload.generatedVisualAssetId.trim()
) {
throw badRequest(
'sync_role_assets requires portraitPath and generatedVisualAssetId',
);
@@ -876,8 +941,11 @@ export class CustomWorldAgentOrchestrator {
try {
const content = await this.llmClient.requestMessageContent({
systemPrompt: '你只输出严格 JSON不输出 Markdown。',
userPrompt: buildAgentLlmPrompt({
systemPrompt: buildAgentSystemPrompt({
isReady: params.isReady,
hasAnyAnchors: hasMeaningfulCreatorIntentRecord(params.intent),
}),
userPrompt: buildAgentUserPrompt({
session: params.session,
latestUserText: params.latestUserText,
intent: params.intent,
@@ -936,15 +1004,28 @@ export class CustomWorldAgentOrchestrator {
throw new Error('session is not ready for draft_foundation');
}
const draftProfile = this.foundationDraftService.generate({
const draftProfile = await this.foundationDraftService.generate({
creatorIntent: latestSession.creatorIntent,
anchorPack: latestSession.anchorPack,
onProgress: async (progress) => {
await this.sessionStore.updateOperation(
userId,
sessionId,
operationId,
{
status: 'running',
phaseLabel: progress.phaseLabel,
phaseDetail: progress.phaseDetail,
progress: progress.progress,
},
);
},
});
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
phaseLabel: '编译草稿卡',
phaseDetail: '正在把世界底稿整理成可浏览的卡片摘要和详情结构。',
progress: 72,
progress: 98,
});
const draftCards = this.draftCompiler.compileDraftCards(draftProfile);
@@ -992,9 +1073,7 @@ export class CustomWorldAgentOrchestrator {
phaseDetail: '这一轮没有成功把锚点编成世界底稿。',
progress: 100,
error:
error instanceof Error
? error.message
: 'draft foundation failed',
error instanceof Error ? error.message : 'draft foundation failed',
});
}
}
@@ -1003,7 +1082,10 @@ export class CustomWorldAgentOrchestrator {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<CustomWorldAgentActionRequest, { action: 'update_draft_card' }>;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'update_draft_card' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
@@ -1024,7 +1106,10 @@ export class CustomWorldAgentOrchestrator {
}
const nextDraftProfile = updateDraftCardSections({
draftProfile: (latestSession.draftProfile ?? {}) as Record<string, unknown>,
draftProfile: (latestSession.draftProfile ?? {}) as Record<
string,
unknown
>,
cardId: payload.cardId,
sections: payload.sections,
});
@@ -1035,7 +1120,8 @@ export class CustomWorldAgentOrchestrator {
progress: 72,
});
const nextDraftCards = this.draftCompiler.compileDraftCards(nextDraftProfile);
const nextDraftCards =
this.draftCompiler.compileDraftCards(nextDraftProfile);
const assetCoverage = rebuildRoleAssetCoverage(nextDraftProfile);
const nextStage =
latestSession.stage === 'visual_refining'
@@ -1052,7 +1138,9 @@ export class CustomWorldAgentOrchestrator {
payload.cardId,
);
const changedSectionIds = new Set(
payload.sections.map((section) => section.sectionId.trim()).filter(Boolean),
payload.sections
.map((section) => section.sectionId.trim())
.filter(Boolean),
);
await this.sessionStore.replaceDerivedState(userId, sessionId, {
@@ -1107,7 +1195,10 @@ export class CustomWorldAgentOrchestrator {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<CustomWorldAgentActionRequest, { action: 'generate_characters' }>;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'generate_characters' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
@@ -1131,7 +1222,10 @@ export class CustomWorldAgentOrchestrator {
await this.entityGenerationService.generateAdditionalCharacters({
creatorIntent: latestSession.creatorIntent,
anchorPack: latestSession.anchorPack,
draftProfile: (latestSession.draftProfile ?? {}) as Record<string, unknown>,
draftProfile: (latestSession.draftProfile ?? {}) as Record<
string,
unknown
>,
count: payload.count,
promptText: payload.promptText,
anchorCardIds:
@@ -1151,7 +1245,9 @@ export class CustomWorldAgentOrchestrator {
const nextDraftCards = this.draftCompiler.compileDraftCards(
generationResult.draftProfile,
);
const assetCoverage = rebuildRoleAssetCoverage(generationResult.draftProfile);
const assetCoverage = rebuildRoleAssetCoverage(
generationResult.draftProfile,
);
const nextStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
@@ -1183,7 +1279,9 @@ export class CustomWorldAgentOrchestrator {
relatedOperationId: operationId,
text: this.changeSummaryService.buildSummary({
action: 'generate_characters',
names: generationResult.generatedCharacters.map((entry) => entry.name),
names: generationResult.generatedCharacters.map(
(entry) => entry.name,
),
draftProfile: generationResult.draftProfile,
}),
}),
@@ -1212,7 +1310,10 @@ export class CustomWorldAgentOrchestrator {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<CustomWorldAgentActionRequest, { action: 'generate_landmarks' }>;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'generate_landmarks' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
@@ -1236,7 +1337,10 @@ export class CustomWorldAgentOrchestrator {
await this.entityGenerationService.generateAdditionalLandmarks({
creatorIntent: latestSession.creatorIntent,
anchorPack: latestSession.anchorPack,
draftProfile: (latestSession.draftProfile ?? {}) as Record<string, unknown>,
draftProfile: (latestSession.draftProfile ?? {}) as Record<
string,
unknown
>,
count: payload.count,
promptText: payload.promptText,
anchorCardIds:
@@ -1256,7 +1360,9 @@ export class CustomWorldAgentOrchestrator {
const nextDraftCards = this.draftCompiler.compileDraftCards(
generationResult.draftProfile,
);
const assetCoverage = rebuildRoleAssetCoverage(generationResult.draftProfile);
const assetCoverage = rebuildRoleAssetCoverage(
generationResult.draftProfile,
);
const nextStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
@@ -1288,7 +1394,9 @@ export class CustomWorldAgentOrchestrator {
relatedOperationId: operationId,
text: this.changeSummaryService.buildSummary({
action: 'generate_landmarks',
names: generationResult.generatedLandmarks.map((entry) => entry.name),
names: generationResult.generatedLandmarks.map(
(entry) => entry.name,
),
draftProfile: generationResult.draftProfile,
}),
}),
@@ -1317,7 +1425,10 @@ export class CustomWorldAgentOrchestrator {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<CustomWorldAgentActionRequest, { action: 'generate_role_assets' }>;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'generate_role_assets' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
@@ -1379,7 +1490,9 @@ export class CustomWorldAgentOrchestrator {
phaseDetail: '这一轮没有成功进入角色资产工坊。',
progress: 100,
error:
error instanceof Error ? error.message : 'generate role assets failed',
error instanceof Error
? error.message
: 'generate role assets failed',
});
}
}
@@ -1388,7 +1501,10 @@ export class CustomWorldAgentOrchestrator {
userId: string;
sessionId: string;
operationId: string;
payload: Extract<CustomWorldAgentActionRequest, { action: 'sync_role_assets' }>;
payload: Extract<
CustomWorldAgentActionRequest,
{ action: 'sync_role_assets' }
>;
}) {
const { userId, sessionId, operationId, payload } = params;
@@ -1542,9 +1658,7 @@ export class CustomWorldAgentOrchestrator {
: derivedState.suggestedActions;
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: shouldPreserveDraftStage
? preservedStage
: derivedState.stage,
stage: shouldPreserveDraftStage ? preservedStage : derivedState.stage,
creatorIntent: nextIntent,
creatorIntentReadiness: derivedState.readiness,
anchorPack: derivedState.anchorPack,
@@ -1607,7 +1721,7 @@ export class CustomWorldAgentOrchestrator {
? '这轮补充已挂回当前底稿语境,现有草稿卡保持可继续浏览。'
: derivedState.readiness.isReady
? '最小锚点已齐备,可以进入下一阶段。'
: '这一轮的创作锚点和澄清问题已经同步完成。',
: '这一轮的创作锚点和澄清问题已经同步完成。',
progress: 100,
error: null,
});