Files
memory-manager-concurrent/src/rma/analyzer.js
Cola-Echo 10ea8cc1f4 feat: add RMA (Relationship Memory Architecture) module v0.6.0
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>
2026-03-31 23:23:41 +08:00

264 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}