322 lines
9.7 KiB
TypeScript
322 lines
9.7 KiB
TypeScript
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;
|
|
}
|