mirror of
https://github.com/Cola-Echo/memory-manager-concurrent.git
synced 2026-06-06 01:55:51 +00:00
Complete RMA system for tracking relationship dynamics in roleplay: - 10 new modules in src/rma/ (analyzer, memory-store, phase-manager, worldbook-sync, float-panel, confirmation-ui, timeline-view, etc.) - Three-layer model: phases → memories (4 types) → emotional texture - Post-processing AI analysis via CHARACTER_MESSAGE_RENDERED hook - Dynamic world book entry toggling/rewriting - Floating panel UI with 3 states (expanded/half/minimized) - User confirmation flow (every_turn/important_only/auto modes) - Settings panel integration with independent API config - chat_metadata.cola_rma persistence Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
264 lines
8.8 KiB
JavaScript
264 lines
8.8 KiB
JavaScript
/**
|
||
* RMA 后处理分析引擎
|
||
* 构建 prompt → 调用 AI → 解析结构化结果
|
||
* @module rma/analyzer
|
||
*/
|
||
|
||
import Logger from '@core/logger';
|
||
import { getCurrentCharacterDescription } from '@core/sillytavern-api';
|
||
import APIAdapter from '@api/adapter';
|
||
import { getRmaAnalysisApiConfig } from './index';
|
||
import { getCurrentRmaConfig, getSensitivities, getEmotionalProcessing, getSecrets, getPhaseDefinitions } from './config-loader';
|
||
import { getRmaState, getRelevantMemories, getUnresolvedThreads, getCurrentTexture, getPhase } from './memory-store';
|
||
|
||
const log = Logger.createModuleLogger('RMA-Analyzer');
|
||
|
||
const SYSTEM_PROMPT = `你是一个角色扮演分析引擎。你的任务是分析对话中发生的事件,评估它们对角色关系和情感状态的影响。
|
||
|
||
你需要输出两部分内容:
|
||
1. 一个 JSON 代码块(用 \`\`\`json 包裹),包含结构化的记忆/状态更新
|
||
2. 一个 [NARRATIVE] 标记后的自然语言段落,描述角色当前内心状态(200-400 token)
|
||
|
||
规则:
|
||
- 只在真正发生了对角色有意义的事时才创建记忆。日常寒暄不需要记忆。
|
||
- 阶段变化需要积累足够的记忆支持,不要轻易改变。
|
||
- 参考角色的敏感点来判断事件重要性。
|
||
- 参考情绪处理模式来区分表面反应和真实感受。
|
||
- crack 类型记忆必须说明 lasting_effect。
|
||
- NARRATIVE 不要使用"阶段""变量""系统"等元语言,用角色本人的语气写。`;
|
||
|
||
/**
|
||
* 构建用户 prompt
|
||
*/
|
||
function buildUserPrompt(recentMessages, latestResponse) {
|
||
const config = getCurrentRmaConfig();
|
||
if (!config) return '';
|
||
|
||
const charDesc = getCurrentCharacterDescription();
|
||
const sensitivities = getSensitivities(config);
|
||
const emotional = getEmotionalProcessing(config);
|
||
const secrets = getSecrets(config);
|
||
const state = getRmaState();
|
||
const currentPhase = getPhase();
|
||
const relevantMems = getRelevantMemories(latestResponse);
|
||
const threads = getUnresolvedThreads();
|
||
const texture = getCurrentTexture();
|
||
|
||
const parts = [];
|
||
|
||
parts.push(`## 角色设定\n${charDesc || '无'}`);
|
||
|
||
// 敏感点
|
||
if (Object.keys(sensitivities).length > 0) {
|
||
const lines = Object.entries(sensitivities).map(([id, s]) =>
|
||
`- ${id}: ${s.description}(反应: ${s.reaction},原因: ${s.why})`
|
||
);
|
||
parts.push(`## 敏感点\n${lines.join('\n')}`);
|
||
}
|
||
|
||
// 情感处理模式
|
||
if (emotional.style) {
|
||
parts.push(`## 情感处理模式\n风格: ${emotional.style}\n${emotional.description || ''}\n表达模式: ${emotional.expression_pattern || ''}\n防御机制: ${emotional.defense_mechanism || ''}`);
|
||
}
|
||
|
||
// 秘密路径
|
||
if (Object.keys(secrets).length > 0) {
|
||
const lines = Object.entries(secrets).map(([id, s]) => {
|
||
const currentStage = state?.secrets?.[id]?.current_stage || s.initial_stage;
|
||
const stages = s.stages?.map(st => st.id === currentStage ? `[${st.name}]` : st.name).join(' → ') || '';
|
||
return `- ${id}: ${s.description}(当前: ${currentStage},路径: ${stages})`;
|
||
});
|
||
parts.push(`## 秘密路径\n${lines.join('\n')}`);
|
||
}
|
||
|
||
// 当前阶段
|
||
const phaseDefs = getPhaseDefinitions(config);
|
||
const phaseNames = phaseDefs.map(p => p.name === currentPhase ? `[${p.name}]` : p.name).join(' → ');
|
||
parts.push(`## 当前关系阶段\n${currentPhase || '未知'}(路径: ${phaseNames})\n趋势: ${state?.phase?.tendency || 'stable'}`);
|
||
|
||
// 现有记忆
|
||
if (relevantMems.length > 0) {
|
||
const memLines = relevantMems.map(m =>
|
||
`- [${m.type}] 第${m.day}天: ${m.event}(影响: ${m.emotional_impact}${m.lasting_effect ? `,持续: ${m.lasting_effect}` : ''})`
|
||
);
|
||
parts.push(`## 现有记忆\n${memLines.join('\n')}`);
|
||
} else {
|
||
parts.push('## 现有记忆\n无');
|
||
}
|
||
|
||
// 当前质感
|
||
parts.push(`## 当前情感纹理\n${texture || '无'}`);
|
||
|
||
// 未解决事项
|
||
if (threads.length > 0) {
|
||
parts.push(`## 未解决的事\n${threads.map(t => `- ${t}`).join('\n')}`);
|
||
}
|
||
|
||
// 最近对话
|
||
if (recentMessages && recentMessages.length > 0) {
|
||
const msgLines = recentMessages.map(m =>
|
||
`${m.is_user ? '用户' : '角色'}: ${m.mes?.slice(0, 500) || ''}`
|
||
);
|
||
parts.push(`## 最近对话\n${msgLines.join('\n')}`);
|
||
}
|
||
|
||
// 最新回复
|
||
parts.push(`## AI 最新回复\n${latestResponse || '无'}`);
|
||
|
||
return parts.join('\n\n');
|
||
}
|
||
|
||
/**
|
||
* 解析 AI 输出,分离 JSON 和 NARRATIVE
|
||
* @param {string} text AI 原始输出
|
||
* @returns {{ json: object|null, narrative: string }}
|
||
*/
|
||
function parseAnalysisOutput(text) {
|
||
let json = null;
|
||
let narrative = '';
|
||
|
||
// 提取 JSON 代码块
|
||
const jsonMatch = text.match(/```json\s*([\s\S]*?)```/);
|
||
if (jsonMatch) {
|
||
try {
|
||
json = JSON.parse(jsonMatch[1].trim());
|
||
} catch (e) {
|
||
log.warn('JSON 解析失败:', e.message);
|
||
}
|
||
}
|
||
|
||
// 如果没有代码块格式,尝试直接找 JSON 对象
|
||
if (!json) {
|
||
const braceMatch = text.match(/\{[\s\S]*"new_memories"[\s\S]*\}/);
|
||
if (braceMatch) {
|
||
try {
|
||
json = JSON.parse(braceMatch[0]);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
}
|
||
|
||
// 提取 NARRATIVE
|
||
const narrativeMatch = text.match(/\[NARRATIVE\]\s*([\s\S]*?)(?:$|```)/i);
|
||
if (narrativeMatch) {
|
||
narrative = narrativeMatch[1].trim();
|
||
} else {
|
||
// 尝试取 JSON 代码块之后的所有内容
|
||
const afterJson = text.replace(/```json[\s\S]*?```/, '').trim();
|
||
if (afterJson.length > 50) {
|
||
narrative = afterJson;
|
||
}
|
||
}
|
||
|
||
return { json, narrative };
|
||
}
|
||
|
||
/**
|
||
* 验证分析结果
|
||
* @param {object} json 解析后的 JSON
|
||
* @param {object} config RMA 配置
|
||
* @returns {object} 清理后的结果
|
||
*/
|
||
function validateAnalysisResult(json, config) {
|
||
if (!json) return null;
|
||
|
||
const validTypes = ['breakthrough', 'warmth', 'crack', 'revelation'];
|
||
const phaseNames = getPhaseDefinitions(config).map(p => p.name || p.id);
|
||
const secretIds = Object.keys(getSecrets(config));
|
||
|
||
// 过滤非法记忆类型
|
||
if (json.new_memories) {
|
||
json.new_memories = json.new_memories.filter(m => {
|
||
if (!validTypes.includes(m.type)) {
|
||
log.warn(`过滤非法记忆类型: ${m.type}`);
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
// 验证阶段名
|
||
if (json.phase_assessment?.new_phase) {
|
||
if (!phaseNames.includes(json.phase_assessment.new_phase)) {
|
||
log.warn(`非法阶段名: ${json.phase_assessment.new_phase},已忽略`);
|
||
json.phase_assessment.phase_changed = false;
|
||
json.phase_assessment.new_phase = null;
|
||
}
|
||
}
|
||
|
||
// 验证秘密 ID
|
||
if (json.secret_updates) {
|
||
for (const id of Object.keys(json.secret_updates)) {
|
||
if (!secretIds.includes(id)) {
|
||
log.warn(`非法秘密 ID: ${id},已移除`);
|
||
delete json.secret_updates[id];
|
||
}
|
||
}
|
||
}
|
||
|
||
return json;
|
||
}
|
||
|
||
/**
|
||
* 执行后处理分析
|
||
* @param {Array} recentMessages 最近的聊天消息
|
||
* @param {string} latestResponse 最新 AI 回复
|
||
* @returns {Promise<{json: object, narrative: string}|null>} 分析结果
|
||
*/
|
||
export async function analyzeResponse(recentMessages, latestResponse) {
|
||
const config = getCurrentRmaConfig();
|
||
if (!config) return null;
|
||
|
||
const apiConfig = getRmaAnalysisApiConfig();
|
||
if (!apiConfig?.apiUrl || !apiConfig?.model) {
|
||
log.warn('RMA 分析 API 未配置');
|
||
return null;
|
||
}
|
||
|
||
const userPrompt = buildUserPrompt(recentMessages, latestResponse);
|
||
|
||
let retries = 0;
|
||
const maxRetries = 2;
|
||
|
||
while (retries <= maxRetries) {
|
||
try {
|
||
log.log(`发送后处理分析请求 (尝试 ${retries + 1}/${maxRetries + 1})`);
|
||
|
||
const result = await APIAdapter.call(
|
||
apiConfig,
|
||
SYSTEM_PROMPT,
|
||
userPrompt,
|
||
);
|
||
|
||
if (!result) {
|
||
log.warn('API 返回空结果');
|
||
retries++;
|
||
continue;
|
||
}
|
||
|
||
const { json, narrative } = parseAnalysisOutput(result);
|
||
|
||
if (!json && retries < maxRetries) {
|
||
log.warn('JSON 解析失败,重试...');
|
||
retries++;
|
||
continue;
|
||
}
|
||
|
||
const validated = validateAnalysisResult(json, config);
|
||
|
||
return {
|
||
json: validated,
|
||
narrative: narrative || '',
|
||
raw: result,
|
||
};
|
||
} catch (e) {
|
||
log.warn(`分析请求失败 (尝试 ${retries + 1}):`, e.message);
|
||
retries++;
|
||
}
|
||
}
|
||
|
||
log.warn('后处理分析失败,已达到最大重试次数');
|
||
return null;
|
||
}
|