1
This commit is contained in:
321
server-node/src/services/customWorldAgentTestHelpers.ts
Normal file
321
server-node/src/services/customWorldAgentTestHelpers.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user