import type { EightAnchorContent } from '../../../packages/shared/src/contracts/customWorldAgent.js'; import type { UpstreamLlmClient } from './llmClient.js'; import { extractCreatorIntentPatch, mergeCreatorIntentRecord, } from './customWorldAgentIntentExtractionService.js'; import { buildCreatorIntentFromEightAnchorContent, buildEightAnchorContentFromCreatorIntent, createEmptyEightAnchorContent, estimateProgressPercentFromAnchorContent, normalizeEightAnchorContent, } from './eightAnchorCompatibilityService.js'; type TestChatMessage = { role: 'user' | 'assistant'; content: string; }; function toText(value: unknown) { return typeof value === 'string' ? value.trim() : ''; } function shouldReplaceWorldPromise(params: { latestUserText: string; hasExistingWorldPromise: boolean; }) { if (!params.hasExistingWorldPromise) { return true; } return /(世界一句话|一句话概括|世界设定|这个世界|题材|主题|风格|改成|改为|换成)/u.test( params.latestUserText, ); } function buildAutoCompletePatch(intent: ReturnType< typeof buildCreatorIntentFromEightAnchorContent >) { 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 buildReplyText(params: { nextAnchorContent: EightAnchorContent; progressPercent: number; quickFillRequested: boolean; latestUserText: string; }) { if (params.quickFillRequested || params.progressPercent >= 100) { return '这一版已经收住了,现在可以直接生成游戏设定草稿。'; } if (/(改成|改为|换成|不是)/u.test(params.latestUserText)) { return '我已经按你刚刚修正后的方向重收了一版,现在这条主线会更稳。'; } if (!params.nextAnchorContent.worldPromise?.hook) { return '方向我先接住了一点。这个世界最抓人的那句核心设定,你想怎么钉住?'; } if (!params.nextAnchorContent.playerFantasy?.playerRole) { return '世界底色已经有了。你最想让玩家以什么身份卷进来?'; } if (!params.nextAnchorContent.playerEntryPoint?.openingProblem) { return '大方向先稳住了。故事开场时,玩家先撞上的麻烦是什么?'; } if (!params.nextAnchorContent.coreConflict?.surfaceConflicts.length) { return '现在气质和身份都更清楚了。接下来最值得钉住的,是这个世界正在爆开的主要冲突。'; } return '这轮信息我已经收进当前版本里了,你可以继续往下补,也可以让我顺着这条线继续收束。'; } function extractJsonBlock(text: string, marker: string) { const markerIndex = text.indexOf(marker); if (markerIndex < 0) { return null; } let startIndex = markerIndex + marker.length; while (startIndex < text.length && /\s/u.test(text[startIndex] ?? '')) { startIndex += 1; } const firstCharacter = text[startIndex]; if (firstCharacter !== '{' && firstCharacter !== '[') { return null; } const closingCharacter = firstCharacter === '{' ? '}' : ']'; let depth = 0; let insideString = false; let escaping = false; for (let index = startIndex; index < text.length; index += 1) { const character = text[index] ?? ''; if (insideString) { if (escaping) { escaping = false; continue; } if (character === '\\') { escaping = true; continue; } if (character === '"') { insideString = false; } continue; } if (character === '"') { insideString = true; continue; } if (character === firstCharacter) { depth += 1; continue; } if (character === closingCharacter) { depth -= 1; if (depth === 0) { return text.slice(startIndex, index + 1); } } } return null; } function parsePromptInput(text: string) { const anchorJson = extractJsonBlock(text, '当前完整设定结构:'); const chatJson = extractJsonBlock(text, '用户聊天记录:'); const currentAnchorContent = anchorJson ? normalizeEightAnchorContent(JSON.parse(anchorJson)) : createEmptyEightAnchorContent(); const chatHistory = chatJson ? (JSON.parse(chatJson) as TestChatMessage[]) : []; const quickFillRequested = text.includes('是否要求自动补全:是') || text.includes('conversationMode: force_complete') || text.includes('用户刚刚主动要求你自动补全剩余设定'); return { currentAnchorContent, chatHistory, quickFillRequested, }; } export function buildTestEightAnchorTurn(params: { currentAnchorContent: EightAnchorContent; chatHistory: TestChatMessage[]; quickFillRequested: boolean; }) { const latestUserText = [...params.chatHistory] .reverse() .find((entry) => entry.role === 'user' && entry.content.trim())?.content ?? ''; const currentIntent = buildCreatorIntentFromEightAnchorContent( params.currentAnchorContent, ); const intentPatch = extractCreatorIntentPatch({ currentIntent, latestUserMessage: latestUserText, recentMessages: params.chatHistory .filter((entry) => entry.role === 'user') .slice(-6, -1) .map((entry) => entry.content), }); const mergedIntent = mergeCreatorIntentRecord( currentIntent, params.quickFillRequested ? { ...intentPatch, ...buildAutoCompletePatch(currentIntent), } : intentPatch, ); if ( !shouldReplaceWorldPromise({ latestUserText, hasExistingWorldPromise: Boolean(currentIntent.worldHook), }) ) { mergedIntent.worldHook = currentIntent.worldHook; } const nextAnchorContent = buildEightAnchorContentFromCreatorIntent(mergedIntent); const progressPercent = params.quickFillRequested ? 100 : estimateProgressPercentFromAnchorContent(nextAnchorContent); return { replyText: buildReplyText({ nextAnchorContent, progressPercent, quickFillRequested: params.quickFillRequested, latestUserText, }), progressPercent, nextAnchorContent, }; } function buildStateInferenceFromPrompt(text: string) { const { chatHistory, quickFillRequested } = parsePromptInput(text); const latestUserText = [...chatHistory] .reverse() .find((entry) => entry.role === 'user' && entry.content.trim())?.content ?? ''; const correction = /(改成|改为|换成|不是|别走|不要)/u.test(latestUserText); const delegate = /(你来|你帮我|默认方案|自动补全|按你觉得合理)/u.test( latestUserText, ); if (quickFillRequested) { return { userInputSignal: delegate ? 'delegate' : 'normal', driftRisk: correction ? 'high' : 'medium', conversationMode: 'force_complete', judgementSummary: '用户希望系统直接补完,这一轮应优先补齐剩余设定并结束收集阶段。', }; } if (correction) { return { userInputSignal: 'correction', driftRisk: 'high', conversationMode: 'repair_direction', judgementSummary: '用户正在修正旧方向,正式生成时要让修正后的版本直接接管当前语境。', }; } if (latestUserText.length < 20) { return { userInputSignal: delegate ? 'delegate' : 'sparse', driftRisk: 'low', conversationMode: 'bootstrap', judgementSummary: '这轮新增信息较少,正式生成时应先低压力接住方向,再只推进一个最好回答的问题。', }; } return { userInputSignal: latestUserText.length >= 40 ? 'rich' : 'normal', driftRisk: 'low', conversationMode: 'expand', judgementSummary: '这轮是在顺着现有方向继续补充,正式生成时应吸收新增细节并往前推进一步。', }; } export function createTestCustomWorldAgentSingleTurnLlmClient() { return { requestMessageContent: async (params) => { if (params.systemPrompt.includes('创作状态识别器')) { return JSON.stringify(buildStateInferenceFromPrompt(params.userPrompt)); } const promptInput = parsePromptInput( [params.systemPrompt, params.userPrompt].join('\n\n'), ); return JSON.stringify(buildTestEightAnchorTurn(promptInput)); }, streamMessageContent: async (params) => { const promptInput = parsePromptInput( [params.systemPrompt, params.userPrompt].join('\n\n'), ); const output = buildTestEightAnchorTurn(promptInput); const jsonText = JSON.stringify({ replyText: output.replyText, progressPercent: output.progressPercent, nextAnchorContent: output.nextAnchorContent, }); params.onUpdate?.(jsonText); return jsonText; }, } as UpstreamLlmClient; }