This commit is contained in:
2026-04-18 13:05:29 +08:00
parent 09d4c0c31b
commit 5032701c38
77 changed files with 8538 additions and 2413 deletions

View File

@@ -1,4 +1,5 @@
import crypto from 'node:crypto';
import type { Request, Response } from 'express';
import type {
CreateCustomWorldAgentSessionRequest,
@@ -14,6 +15,7 @@ import type {
SendCustomWorldAgentMessageResponse,
} from '../../../packages/shared/src/contracts/customWorldAgent.js';
import { badRequest, notFound } from '../errors.js';
import { prepareEventStreamResponse } from '../http.js';
import { CustomWorldAgentAssetBridgeService } from './customWorldAgentAssetBridgeService.js';
import { CustomWorldAgentChangeSummaryService } from './customWorldAgentChangeSummaryService.js';
import {
@@ -31,7 +33,6 @@ import { CustomWorldAgentEntityGenerationService } from './customWorldAgentEntit
import { CustomWorldAgentFoundationDraftService } from './customWorldAgentFoundationDraftService.js';
import {
buildAnchorPackFromIntent,
buildCreatorIntentDisplayText,
buildDraftSummaryFromIntent,
buildDraftTitleFromIntent,
createEmptyCreatorIntentRecord,
@@ -49,11 +50,16 @@ import {
type CustomWorldAgentSessionRecord,
CustomWorldAgentSessionStore,
} from './customWorldAgentSessionStore.js';
import {
buildAnchorPackFromEightAnchorContent,
buildCreatorIntentFromEightAnchorContent,
buildEightAnchorContentFromCreatorIntent,
estimateProgressPercentFromAnchorContent,
} from './eightAnchorCompatibilityService.js';
import { EightAnchorSingleTurnService } from './eightAnchorSingleTurnService.js';
import type { UpstreamLlmClient } from './llmClient.js';
const PHASE2_FORCE_FAIL_TOKEN = '__phase1_force_fail__';
const AUTO_COMPLETE_PATTERN = /||/u;
function truncateText(value: string, maxLength: number) {
if (value.length <= maxLength) {
return value;
@@ -137,46 +143,10 @@ function buildSuggestedActions(
return actions;
}
function buildAutoCompletePatch(intent: CustomWorldCreatorIntentRecord) {
return {
worldHook:
intent.worldHook ||
intent.rawSettingText ||
'一个被未知异象改变秩序的边境世界。',
playerPremise: intent.playerPremise || '玩家是被卷入核心危机的返乡者。',
openingSituation:
intent.openingSituation || '开局时,玩家正抵达危机爆发的现场。',
themeKeywords:
intent.themeKeywords.length > 0 ? intent.themeKeywords : ['奇幻'],
toneDirectives:
intent.toneDirectives.length > 0 ? intent.toneDirectives : ['悬疑'],
coreConflicts:
intent.coreConflicts.length > 0
? intent.coreConflicts
: ['旧秩序与新威胁正在争夺世界的未来。'],
keyCharacters:
intent.keyCharacters.length > 0
? intent.keyCharacters
: [
{
id: 'auto-key-character-1',
name: '未命名关键人物',
role: '关键关系',
publicMask: '看似能帮助玩家的人。',
hiddenHook: '掌握一条会改变局势的暗线。',
relationToPlayer: '旧识',
notes: '自动补全,可继续修改。',
},
],
iconicElements:
intent.iconicElements.length > 0 ? intent.iconicElements : ['失落信标'],
};
}
function buildOperation(type: CustomWorldAgentOperationRecord['type']) {
const phaseDetail =
type === 'draft_foundation'
? '正在把已确认锚点编成第一版世界底稿。'
? '正在把已确认设定编成第一版世界底稿。'
: type === 'update_draft_card'
? '正在把这次设定改动写回草稿。'
: type === 'generate_characters'
@@ -184,10 +154,10 @@ function buildOperation(type: CustomWorldAgentOperationRecord['type']) {
: type === 'generate_landmarks'
? '正在围绕当前底稿补出新地点。'
: type === 'generate_role_assets'
? '正在准备角色资产工坊入口。'
: type === 'sync_role_assets'
? '正在把角色资产结果写回世界草稿。'
: '正在整理这一轮新增的世界锚点。';
? '正在准备角色资产工坊入口。'
: type === 'sync_role_assets'
? '正在把角色资产结果写回世界草稿。'
: '正在整理这一轮新增的世界设定。';
return {
operationId: `operation-${crypto.randomBytes(10).toString('hex')}`,
@@ -223,20 +193,10 @@ function buildRoleAssetSyncResultText(params: {
return `已把「${params.roleName}」的角色资产写回草稿,当前状态:${params.assetStatusLabel}`;
}
function getRecentUserMessages(session: CustomWorldAgentSessionRecord) {
return session.messages
.filter((message) => message.role === 'user')
.map((message) => message.text.trim())
.filter(Boolean)
.slice(-12);
}
function buildQuestionLines(
pendingClarifications: CustomWorldPendingClarification[],
) {
return pendingClarifications.map(
(entry, index) => `${index + 1}. ${entry.question}`,
);
return pendingClarifications.map((entry) => entry.question.trim());
}
function composeAssistantReply(params: {
@@ -250,7 +210,7 @@ function composeAssistantReply(params: {
return [
params.openingText,
params.isReady
? '最小锚点已经齐备。'
? '当前设定已经齐备。'
: questionLines.slice(0, 1).join('\n'),
].join('\n');
}
@@ -311,227 +271,6 @@ function buildWelcomeMessage(params: {
});
}
function buildAssistantMessage(params: {
latestUserText: string;
relatedOperationId: string;
intent: CustomWorldCreatorIntentRecord;
pendingClarifications: CustomWorldPendingClarification[];
isReady: boolean;
}) {
return {
id: `message-${crypto.randomBytes(8).toString('hex')}`,
role: 'assistant',
kind: params.isReady ? 'summary' : 'clarification',
text: composeAssistantReply({
openingText: `收到:${truncateText(params.latestUserText, 88)}`,
intent: params.intent,
pendingClarifications: params.pendingClarifications,
isReady: params.isReady,
}),
createdAt: new Date().toISOString(),
relatedOperationId: params.relatedOperationId,
} satisfies CustomWorldAgentMessage;
}
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;
pendingClarifications: CustomWorldPendingClarification[];
isReady: boolean;
}) {
const recentMessages = params.session.messages
.slice(-18)
.map((message) => `${message.role}: ${message.text}`)
.join('\n');
const pendingQuestions = params.pendingClarifications
.slice(0, 1)
.map((entry) => `${entry.label}: ${entry.question}`)
.join('\n');
return [
'# 当前结构化世界锚点',
buildCreatorIntentDisplayText(params.intent) || '暂无',
'',
`# 锚点是否齐备`,
params.isReady ? '是' : '否',
'',
pendingQuestions ? `# 待确认问题\n${pendingQuestions}\n` : '',
'# 最近对话',
recentMessages || '暂无',
'',
'# 用户最新输入',
params.latestUserText,
]
.filter(Boolean)
.join('\n');
}
function parseAssistantTurnJson(text: string) {
try {
const parsed = JSON.parse(text) as {
reply?: unknown;
recommendedReplies?: unknown;
};
const reply = typeof parsed.reply === 'string' ? parsed.reply.trim() : '';
const recommendedReplies = Array.isArray(parsed.recommendedReplies)
? parsed.recommendedReplies
.map((item) => (typeof item === 'string' ? item.trim() : ''))
.filter(Boolean)
.slice(0, 3)
: [];
return {
reply,
recommendedReplies,
};
} catch {
return {
reply: '',
recommendedReplies: [],
};
}
}
function buildFallbackRecommendedReplies(params: {
pendingClarifications: CustomWorldPendingClarification[];
isReady: boolean;
}) {
if (params.isReady) {
return ['现在开始生成草稿', '先总结一下当前设定', '我还想再补充一点'];
}
const nextQuestion = params.pendingClarifications[0];
if (!nextQuestion) {
return ['继续', '给我一个默认方案', '先总结一下'];
}
if (nextQuestion.targetKey === 'world_hook') {
return [
'一个被潮雾切开的列岛世界。',
'一个旧神遗产复苏的边境世界。',
'一个灯塔决定航路生死的海雾世界。',
];
}
if (nextQuestion.targetKey === 'player_premise') {
return [
'玩家是被迫返乡的失职守灯人。',
'玩家是背着旧案回来的流亡航海士。',
'玩家是被逐出组织的前探路员。',
];
}
if (nextQuestion.targetKey === 'theme_and_tone') {
return [
'整体偏冷峻、潮湿、悬疑。',
'气质偏压迫、克制、带一点宿命感。',
'我想要浪漫外壳下的阴冷悬疑。',
];
}
if (nextQuestion.targetKey === 'core_conflict') {
return [
'核心冲突是旧航路解释权之争。',
'主要危机是被封印的灾难正在重演。',
'核心矛盾是守旧势力和新秩序正面冲突。',
];
}
if (nextQuestion.targetKey === 'relationship_seed') {
return [
'关键人物是玩家的旧友兼宿敌。',
'她表面帮助玩家,其实另有立场。',
'关键钩子是玩家必须再次相信曾经背叛自己的人。',
];
}
return [
'标志性元素是潮雾钟声。',
'标志性规则是夜里不能出海。',
'地标意象是永不熄灭的盐火灯塔。',
];
}
function buildFoundationDraftAssistantMessage(params: {
relatedOperationId: string;
draftProfile: unknown;
@@ -555,31 +294,6 @@ function buildFoundationDraftAssistantMessage(params: {
} satisfies CustomWorldAgentMessage;
}
function buildObjectRefiningAssistantMessage(params: {
latestUserText: string;
relatedOperationId: string;
draftProfile: unknown;
}) {
const profile = normalizeFoundationDraftProfile(params.draftProfile);
const leadCharacter = profile?.playableNpcs[0];
const leadLandmark = profile?.landmarks[0];
return {
id: `message-${crypto.randomBytes(8).toString('hex')}`,
role: 'assistant',
kind: 'summary',
text: [
`我先把你这轮补充挂回当前底稿语境里:${truncateText(params.latestUserText, 88)}`,
'',
profile?.summary || '当前底稿仍然保留,你可以继续围绕已有卡片精修。',
'',
`现在更适合直接看卡继续收紧内容${leadCharacter ? `,角色建议先看「${leadCharacter.name}` : ''}${leadLandmark ? `,地点建议先看「${leadLandmark.name}` : ''}`,
].join('\n'),
createdAt: new Date().toISOString(),
relatedOperationId: params.relatedOperationId,
} satisfies CustomWorldAgentMessage;
}
function buildActionResultMessage(params: {
relatedOperationId: string;
text: string;
@@ -594,6 +308,19 @@ function buildActionResultMessage(params: {
} satisfies CustomWorldAgentMessage;
}
function writeSseEvent(
response: Response,
event: string,
data: unknown,
) {
if (response.writableEnded) {
return;
}
response.write(`event: ${event}\n`);
response.write(`data: ${JSON.stringify(data)}\n\n`);
}
export class CustomWorldAgentOrchestrator {
private readonly foundationDraftService: CustomWorldAgentFoundationDraftService;
@@ -605,9 +332,14 @@ export class CustomWorldAgentOrchestrator {
private readonly assetBridgeService: CustomWorldAgentAssetBridgeService;
private readonly eightAnchorSingleTurnService: EightAnchorSingleTurnService;
constructor(
private readonly sessionStore: CustomWorldAgentSessionStore,
private readonly llmClient: UpstreamLlmClient | null = null,
llmClient: UpstreamLlmClient | null = null,
options: {
singleTurnLlmClient?: UpstreamLlmClient | null;
} = {},
) {
this.foundationDraftService = new CustomWorldAgentFoundationDraftService(
llmClient,
@@ -618,6 +350,9 @@ export class CustomWorldAgentOrchestrator {
);
this.changeSummaryService = new CustomWorldAgentChangeSummaryService();
this.assetBridgeService = new CustomWorldAgentAssetBridgeService();
this.eightAnchorSingleTurnService = new EightAnchorSingleTurnService(
(options.singleTurnLlmClient ?? llmClient) ?? undefined,
);
}
async createSession(
@@ -634,59 +369,35 @@ export class CustomWorldAgentOrchestrator {
: {};
const creatorIntent = mergeCreatorIntentRecord(baseIntent, seedPatch);
const derivedState = buildDerivedState(creatorIntent, Boolean(seedText));
const anchorContent = buildEightAnchorContentFromCreatorIntent(creatorIntent);
const progressPercent = seedText
? estimateProgressPercentFromAnchorContent(anchorContent)
: 0;
const fallbackWelcomeMessage = buildWelcomeMessage({
seedText,
intent: creatorIntent,
pendingClarifications: derivedState.pendingClarifications,
isReady: derivedState.readiness.isReady,
});
const initialAssistantTurn = await this.generateAssistantTurn({
session: {
sessionId: 'preview',
userId,
seedText,
stage: derivedState.stage,
focusCardId: null,
creatorIntent,
creatorIntentReadiness: derivedState.readiness,
anchorPack: derivedState.anchorPack,
lockState: {},
draftProfile: derivedState.draftProfile,
messages: [],
draftCards: [],
pendingClarifications: derivedState.pendingClarifications,
suggestedActions: derivedState.suggestedActions,
recommendedReplies: [],
qualityFindings: [],
assetCoverage: {
roleAssets: [],
sceneAssets: [],
allRoleAssetsReady: false,
allSceneAssetsReady: false,
},
operations: [],
checkpoints: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
latestUserText: seedText,
fallbackReply: fallbackWelcomeMessage,
intent: creatorIntent,
pendingClarifications: derivedState.pendingClarifications,
isReady: derivedState.readiness.isReady,
});
const record = await this.sessionStore.create(userId, {
seedText,
welcomeMessage: initialAssistantTurn.reply,
welcomeMessage: fallbackWelcomeMessage,
currentTurn: 0,
anchorContent,
progressPercent,
lastAssistantReply: fallbackWelcomeMessage,
creatorIntent,
creatorIntentReadiness: derivedState.readiness,
anchorPack: derivedState.anchorPack,
anchorPack: buildAnchorPackFromEightAnchorContent(
anchorContent,
progressPercent,
),
draftProfile: derivedState.draftProfile,
pendingClarifications: derivedState.pendingClarifications,
stage: derivedState.stage,
stage: progressPercent >= 100 ? 'foundation_review' : 'collecting_intent',
suggestedActions: derivedState.suggestedActions,
recommendedReplies: initialAssistantTurn.recommendedReplies,
recommendedReplies: [],
});
return (await this.sessionStore.getSnapshot(
@@ -723,6 +434,7 @@ export class CustomWorldAgentOrchestrator {
sessionId,
operationId: operation.operationId,
latestUserText: trimmedText,
quickFillRequested: Boolean(payload.quickFillRequested),
});
return {
@@ -730,6 +442,68 @@ export class CustomWorldAgentOrchestrator {
};
}
async streamMessage(params: {
request: Request;
response: Response;
userId: string;
sessionId: string;
payload: SendCustomWorldAgentMessageRequest;
}) {
const session = await this.sessionStore.get(params.userId, params.sessionId);
if (!session) {
throw notFound('custom world agent session not found');
}
prepareEventStreamResponse(params.request, params.response);
const trimmedText = params.payload.text.trim();
const userMessage = buildUserMessage(
trimmedText,
params.payload.clientMessageId,
);
await this.sessionStore.appendMessage(
params.userId,
params.sessionId,
userMessage,
);
let latestReplyText = '';
try {
const nextSession = await this.applyMessageTurn({
userId: params.userId,
sessionId: params.sessionId,
latestUserText: trimmedText,
quickFillRequested: Boolean(params.payload.quickFillRequested),
relatedOperationId: null,
onReplyUpdate: (text) => {
if (!text.trim() || text === latestReplyText) {
return;
}
latestReplyText = text;
writeSseEvent(params.response, 'reply_delta', {
text,
});
},
});
writeSseEvent(params.response, 'session', {
session: nextSession,
});
writeSseEvent(params.response, 'done', {
ok: true,
});
} catch (error) {
writeSseEvent(params.response, 'error', {
message:
error instanceof Error ? error.message : 'stream custom world message failed',
});
} finally {
params.response.end();
}
}
async executeAction(
userId: string,
sessionId: string,
@@ -741,14 +515,8 @@ export class CustomWorldAgentOrchestrator {
}
if (payload.action === 'draft_foundation') {
if (session.stage !== 'foundation_review') {
throw badRequest(
'draft_foundation is only available during foundation_review',
);
}
if (!session.creatorIntentReadiness.isReady) {
throw badRequest('draft_foundation requires a ready session');
if (session.progressPercent < 100) {
throw badRequest('draft_foundation requires progressPercent >= 100');
}
const operation = buildOperation('draft_foundation');
@@ -919,57 +687,151 @@ export class CustomWorldAgentOrchestrator {
return this.draftCompiler.getDraftCardDetail(session.draftProfile, cardId);
}
private async generateAssistantTurn(params: {
session: CustomWorldAgentSessionRecord;
private async applyMessageTurn(params: {
userId: string;
sessionId: string;
latestUserText: string;
fallbackReply: string;
intent: CustomWorldCreatorIntentRecord;
pendingClarifications: CustomWorldPendingClarification[];
isReady: boolean;
quickFillRequested: boolean;
relatedOperationId?: string | null;
onReplyUpdate?: (text: string) => void;
}) {
const fallbackReplies = buildFallbackRecommendedReplies({
pendingClarifications: params.pendingClarifications,
isReady: params.isReady,
const latestSession = (await this.sessionStore.get(
params.userId,
params.sessionId,
)) as CustomWorldAgentSessionRecord | null;
if (!latestSession) {
throw new Error('custom world agent session not found');
}
const shouldPreserveDraftStage =
(latestSession.stage === 'object_refining' ||
latestSession.stage === 'visual_refining') &&
latestSession.draftCards.length > 0;
const assistantTurn = await this.eightAnchorSingleTurnService.streamTurn(
{
currentTurn: latestSession.currentTurn + 1,
progressPercent: latestSession.progressPercent,
quickFillRequested: params.quickFillRequested,
currentAnchorContent: latestSession.anchorContent,
chatHistory: latestSession.messages
.filter(
(message): message is CustomWorldAgentMessage =>
(message.role === 'user' || message.role === 'assistant') &&
Boolean(message.text.trim()),
)
.map((message) => ({
role: message.role,
content: message.text,
})),
},
{
onReplyUpdate: params.onReplyUpdate,
},
);
const nextCreatorIntent = buildCreatorIntentFromEightAnchorContent(
assistantTurn.nextAnchorContent,
);
const progressPercent = Math.max(
0,
Math.min(100, Math.round(assistantTurn.progressPercent)),
);
const creatorIntentReadiness =
progressPercent >= 100
? {
isReady: true,
completedKeys: [
'world_hook',
'player_premise',
'theme_and_tone',
'core_conflict',
'relationship_seed',
'iconic_element',
],
missingKeys: [],
}
: evaluateCreatorIntentReadiness(nextCreatorIntent);
const derivedState = buildDerivedState(nextCreatorIntent, true);
const preservedStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
: ('object_refining' as const);
const shouldStayInDraftStage =
shouldPreserveDraftStage && progressPercent >= 100;
const nextStage = shouldStayInDraftStage
? preservedStage
: derivedState.stage;
const assistantMessage = {
id: `message-${crypto.randomBytes(8).toString('hex')}`,
role: 'assistant',
kind: 'chat',
text: assistantTurn.replyText,
createdAt: new Date().toISOString(),
relatedOperationId: params.relatedOperationId ?? null,
} satisfies CustomWorldAgentMessage;
await this.sessionStore.replaceDerivedState(params.userId, params.sessionId, {
currentTurn: latestSession.currentTurn + 1,
anchorContent: assistantTurn.nextAnchorContent,
progressPercent,
lastAssistantReply: assistantTurn.replyText,
stage: nextStage,
focusCardId: shouldStayInDraftStage ? latestSession.focusCardId : null,
creatorIntent: nextCreatorIntent,
creatorIntentReadiness,
anchorPack: buildAnchorPackFromEightAnchorContent(
assistantTurn.nextAnchorContent,
progressPercent,
),
draftProfile: shouldStayInDraftStage
? latestSession.draftProfile
: progressPercent >= 100
? {
title: buildDraftTitleFromIntent(nextCreatorIntent),
summary: buildDraftSummaryFromIntent(nextCreatorIntent),
}
: derivedState.draftProfile,
draftCards: shouldStayInDraftStage ? latestSession.draftCards : [],
assetCoverage: shouldStayInDraftStage
? latestSession.assetCoverage
: rebuildRoleAssetCoverage(
progressPercent >= 100
? {
title: buildDraftTitleFromIntent(nextCreatorIntent),
summary: buildDraftSummaryFromIntent(nextCreatorIntent),
}
: derivedState.draftProfile,
),
pendingClarifications:
progressPercent >= 100 ? [] : derivedState.pendingClarifications,
suggestedActions: shouldStayInDraftStage
? buildSuggestedActions({
stage: preservedStage,
isReady: true,
draftProfile: latestSession.draftProfile,
draftCards: latestSession.draftCards,
})
: progressPercent >= 100
? [
{
id: 'draft_foundation',
type: 'draft_foundation',
label: '生成游戏设定草稿',
},
]
: [],
recommendedReplies: [],
});
await this.sessionStore.appendMessage(
params.userId,
params.sessionId,
assistantMessage,
);
if (!this.llmClient) {
return {
reply: params.fallbackReply,
recommendedReplies: fallbackReplies,
};
}
try {
const content = await this.llmClient.requestMessageContent({
systemPrompt: buildAgentSystemPrompt({
isReady: params.isReady,
hasAnyAnchors: hasMeaningfulCreatorIntentRecord(params.intent),
}),
userPrompt: buildAgentUserPrompt({
session: params.session,
latestUserText: params.latestUserText,
intent: params.intent,
pendingClarifications: params.pendingClarifications,
isReady: params.isReady,
}),
timeoutMs: 60000,
debugLabel: 'custom-world-agent-chat-turn',
});
const parsed = parseAssistantTurnJson(content);
return {
reply: parsed.reply || params.fallbackReply,
recommendedReplies:
parsed.recommendedReplies.length === 3
? parsed.recommendedReplies
: fallbackReplies,
};
} catch {
return {
reply: params.fallbackReply,
recommendedReplies: fallbackReplies,
};
}
return (await this.sessionStore.getSnapshot(
params.userId,
params.sessionId,
)) as CustomWorldAgentSessionSnapshot;
}
private async processDraftFoundationOperation(params: {
@@ -983,7 +845,7 @@ export class CustomWorldAgentOrchestrator {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '生成世界底稿',
phaseDetail: '正在根据已确认锚点编译第一版世界结构。',
phaseDetail: '正在根据已确认设定编译第一版世界结构。',
progress: 38,
});
@@ -997,16 +859,22 @@ export class CustomWorldAgentOrchestrator {
throw new Error('custom world agent session not found');
}
if (
latestSession.stage !== 'foundation_review' ||
!latestSession.creatorIntentReadiness.isReady
) {
throw new Error('session is not ready for draft_foundation');
if (latestSession.progressPercent < 100) {
throw new Error('session progressPercent is below 100');
}
const creatorIntent = buildCreatorIntentFromEightAnchorContent(
latestSession.anchorContent,
);
const anchorPack = buildAnchorPackFromEightAnchorContent(
latestSession.anchorContent,
latestSession.progressPercent,
);
const draftProfile = await this.foundationDraftService.generate({
creatorIntent: latestSession.creatorIntent,
anchorPack: latestSession.anchorPack,
creatorIntent,
anchorPack,
anchorContent: latestSession.anchorContent,
onProgress: async (progress) => {
await this.sessionStore.updateOperation(
userId,
@@ -1040,6 +908,8 @@ export class CustomWorldAgentOrchestrator {
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: nextStage,
creatorIntent,
anchorPack,
draftProfile: draftProfile as unknown as Record<string, unknown>,
draftCards,
assetCoverage,
@@ -1070,7 +940,7 @@ export class CustomWorldAgentOrchestrator {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '底稿生成失败',
phaseDetail: '这一轮没有成功把锚点编成世界底稿。',
phaseDetail: '这一轮没有成功把设定编成世界底稿。',
progress: 100,
error:
error instanceof Error ? error.message : 'draft foundation failed',
@@ -1596,14 +1466,23 @@ export class CustomWorldAgentOrchestrator {
sessionId: string;
operationId: string;
latestUserText: string;
quickFillRequested: boolean;
}) {
const { userId, sessionId, operationId, latestUserText } = params;
const {
userId,
sessionId,
operationId,
latestUserText,
quickFillRequested,
} = params;
try {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'running',
phaseLabel: '提取世界锚点',
phaseDetail: '正在把这轮自然语言补充整理成结构化创作意图。',
phaseLabel: quickFillRequested ? '补全剩余设定' : '整理当前设定',
phaseDetail: quickFillRequested
? '正在基于当前方向补齐剩余设定。'
: '正在把这轮输入沉淀成新的完整设定。',
progress: 45,
});
@@ -1621,107 +1500,27 @@ export class CustomWorldAgentOrchestrator {
throw new Error('custom world agent session not found');
}
const currentIntent =
normalizeCreatorIntentRecord(latestSession.creatorIntent) ??
createEmptyCreatorIntentRecord('freeform');
const recentMessages = getRecentUserMessages(latestSession).slice(0, -1);
const intentPatch = extractCreatorIntentPatch({
currentIntent,
latestUserMessage: latestUserText,
recentMessages,
});
const nextIntent = mergeCreatorIntentRecord(
currentIntent,
AUTO_COMPLETE_PATTERN.test(latestUserText)
? {
...intentPatch,
...buildAutoCompletePatch(currentIntent),
}
: intentPatch,
);
const derivedState = buildDerivedState(nextIntent, true);
const shouldPreserveDraftStage =
(latestSession.stage === 'object_refining' ||
latestSession.stage === 'visual_refining') &&
latestSession.draftCards.length > 0;
const preservedStage =
latestSession.stage === 'visual_refining'
? ('visual_refining' as const)
: ('object_refining' as const);
const nextSuggestedActions = shouldPreserveDraftStage
? buildSuggestedActions({
stage: preservedStage,
isReady: true,
draftProfile: latestSession.draftProfile,
draftCards: latestSession.draftCards,
})
: derivedState.suggestedActions;
await this.sessionStore.replaceDerivedState(userId, sessionId, {
stage: shouldPreserveDraftStage ? preservedStage : derivedState.stage,
creatorIntent: nextIntent,
creatorIntentReadiness: derivedState.readiness,
anchorPack: derivedState.anchorPack,
draftProfile: shouldPreserveDraftStage
? latestSession.draftProfile
: derivedState.draftProfile,
pendingClarifications: shouldPreserveDraftStage
? latestSession.pendingClarifications
: derivedState.pendingClarifications,
suggestedActions: nextSuggestedActions,
draftCards: shouldPreserveDraftStage
? latestSession.draftCards
: undefined,
});
const fallbackAssistantMessage = shouldPreserveDraftStage
? buildObjectRefiningAssistantMessage({
latestUserText,
relatedOperationId: operationId,
draftProfile: latestSession.draftProfile,
})
: buildAssistantMessage({
latestUserText,
relatedOperationId: operationId,
intent: nextIntent,
pendingClarifications: derivedState.pendingClarifications,
isReady: derivedState.readiness.isReady,
});
const assistantTurn = shouldPreserveDraftStage
? {
reply: fallbackAssistantMessage.text,
recommendedReplies: [] as string[],
}
: await this.generateAssistantTurn({
session: latestSession,
latestUserText,
fallbackReply: fallbackAssistantMessage.text,
intent: nextIntent,
pendingClarifications: derivedState.pendingClarifications,
isReady: derivedState.readiness.isReady,
});
const assistantMessage = {
...fallbackAssistantMessage,
text: assistantTurn.reply,
};
const recommendedReplies = assistantTurn.recommendedReplies;
await this.sessionStore.appendMessage(
await this.applyMessageTurn({
userId,
sessionId,
assistantMessage,
);
await this.sessionStore.replaceDerivedState(userId, sessionId, {
recommendedReplies,
latestUserText,
quickFillRequested,
relatedOperationId: operationId,
});
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'completed',
phaseLabel: '锚点已更新',
phaseLabel: '设定已更新',
phaseDetail: shouldPreserveDraftStage
? '这轮补充已挂回当前底稿语境,现有草稿卡保持可继续浏览。'
: derivedState.readiness.isReady
? '最小锚点已齐备,可以进入下一阶段。'
: '这一轮的创作锚点和澄清问题已经同步完成。',
: quickFillRequested
? '剩余设定已补全,现在可以进入游戏设定草稿生成。'
: '这一轮的设定更新已经完成。',
progress: 100,
error: null,
});
@@ -1729,7 +1528,7 @@ export class CustomWorldAgentOrchestrator {
await this.sessionStore.updateOperation(userId, sessionId, operationId, {
status: 'failed',
phaseLabel: '处理失败',
phaseDetail: '这一轮消息没有成功沉淀为创作锚点。',
phaseDetail: '这一轮消息没有成功沉淀为当前设定。',
progress: 100,
error:
error instanceof Error ? error.message : 'process message failed',