1
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user