mirror of
https://github.com/Cola-Echo/memory-manager-concurrent.git
synced 2026-06-06 03:05:51 +00:00
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>
This commit is contained in:
20
CHANGELOG.md
20
CHANGELOG.md
@@ -11,6 +11,26 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.6.0] - 2026-03-31
|
||||||
|
|
||||||
|
### RMA 关系记忆系统(全新模块)
|
||||||
|
- **新增**:`src/rma/` 模块(10个文件),实现完整的关系记忆架构
|
||||||
|
- `config-loader.js` — 从角色卡 `extensions.cola_rma_config` 加载 RMA 配置
|
||||||
|
- `memory-store.js` — 基于 `chat_metadata.cola_rma` 的记忆存储管理
|
||||||
|
- `analyzer.js` — 后处理 AI 分析引擎,独立 API 调用提取关系变化
|
||||||
|
- `response-hook.js` — 监听 `CHARACTER_MESSAGE_RENDERED` / `MESSAGE_DELETED` 事件
|
||||||
|
- `phase-manager.js` — 六阶段关系管理(stranger→bond),支持回退
|
||||||
|
- `worldbook-sync.js` — 世界书条目同步(阶段互斥切换、质感改写、事件解锁)
|
||||||
|
- `confirmation-ui.js` — 用户确认流程(每轮/仅重要/全自动三种模式)
|
||||||
|
- `float-panel.js` — 悬浮面板 UI(展开/半折叠/最小化三态)
|
||||||
|
- `timeline-view.js` — 记忆时间线视图
|
||||||
|
- **新增**:四种记忆类型 — breakthrough(⭐)、warmth(💛)、crack(⚡)、revelation(👁)
|
||||||
|
- **新增**:RMA 设置面板 — 启用开关、确认模式选择、面板状态偏好、独立 API 配置
|
||||||
|
- **新增**:`ui/rma-panel.html` — RMA 悬浮面板 HTML 模板
|
||||||
|
- **新增**:RMA 面板 CSS 样式,兼容已有多主题系统
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.5.0] - 2025-02-13
|
## [0.5.0] - 2025-02-13
|
||||||
|
|
||||||
### 总结世界书拆分功能优化
|
### 总结世界书拆分功能优化
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "memory-manager-concurrent",
|
"name": "memory-manager-concurrent",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "memory-manager-concurrent",
|
"name": "memory-manager-concurrent",
|
||||||
"version": "0.4.7",
|
"version": "0.5.0",
|
||||||
"license": "AGPL-3.0",
|
"license": "CC-BY-NC-ND-4.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
"webpack": "^5.104.1",
|
"webpack": "^5.104.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "memory-manager-concurrent",
|
"name": "memory-manager-concurrent",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"description": "SillyTavern 记忆管理并发系统 - 智能记忆检索与注入系统",
|
"description": "SillyTavern 记忆管理并发系统 - 智能记忆检索与注入系统",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -113,6 +113,25 @@ export const defaultConfig = Object.freeze({
|
|||||||
minChars: 40000, // 最小字符数(确保段落完整)
|
minChars: 40000, // 最小字符数(确保段落完整)
|
||||||
maxChars: 60000, // 最大字符数(确保段落完整)
|
maxChars: 60000, // 最大字符数(确保段落完整)
|
||||||
},
|
},
|
||||||
|
// RMA 关系记忆系统配置
|
||||||
|
rmaConfig: {
|
||||||
|
enabled: false, // 全局开关
|
||||||
|
confirmationMode: 'every_turn', // every_turn | important_only | auto
|
||||||
|
analysisApi: {
|
||||||
|
apiFormat: 'openai',
|
||||||
|
apiUrl: '',
|
||||||
|
apiKey: '',
|
||||||
|
model: '',
|
||||||
|
maxTokens: 1500,
|
||||||
|
temperature: 0.3,
|
||||||
|
customTemplate: '',
|
||||||
|
responsePath: 'choices.0.message.content',
|
||||||
|
},
|
||||||
|
floatPanel: {
|
||||||
|
defaultState: 'half_collapsed', // expanded | half_collapsed | minimized
|
||||||
|
position: { x: 'right', y: 'top' },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
memoryConfigs: {},
|
memoryConfigs: {},
|
||||||
summaryConfigs: {},
|
summaryConfigs: {},
|
||||||
|
|||||||
63
src/index.js
63
src/index.js
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* 记忆管理并发系统 - 主入口
|
* 记忆管理并发系统 - 主入口
|
||||||
* @version 0.4.9
|
* @version 0.6.0
|
||||||
* @author 可乐、繁华
|
* @author 可乐、繁华
|
||||||
* @license CC BY-NC-ND 4.0
|
* @license CC BY-NC-ND 4.0
|
||||||
* @see https://github.com/Cola-Echo/memory-manager-concurrent
|
* @see https://github.com/Cola-Echo/memory-manager-concurrent
|
||||||
@@ -69,6 +69,7 @@ import {
|
|||||||
setMessageProgressPanel,
|
setMessageProgressPanel,
|
||||||
setOpenIndexMergeConfigModalFunction,
|
setOpenIndexMergeConfigModalFunction,
|
||||||
setOpenPlotOptimizeConfigModalFunction,
|
setOpenPlotOptimizeConfigModalFunction,
|
||||||
|
setOpenRmaConfigModalFunction,
|
||||||
setPlotPanelProgressTracker,
|
setPlotPanelProgressTracker,
|
||||||
setPromptEditorFunctions,
|
setPromptEditorFunctions,
|
||||||
setRefreshAIConfigListFunction,
|
setRefreshAIConfigListFunction,
|
||||||
@@ -96,6 +97,8 @@ import {
|
|||||||
// 模型显示更新
|
// 模型显示更新
|
||||||
updateIndexMergeModelDisplay,
|
updateIndexMergeModelDisplay,
|
||||||
updatePlotOptimizeModelDisplay,
|
updatePlotOptimizeModelDisplay,
|
||||||
|
// RMA 关系记忆
|
||||||
|
updateRmaModelDisplay,
|
||||||
// 总结世界书拆分配置弹窗
|
// 总结世界书拆分配置弹窗
|
||||||
setSummaryPartConfigModalFunction,
|
setSummaryPartConfigModalFunction,
|
||||||
showSummaryPartConfigModal,
|
showSummaryPartConfigModal,
|
||||||
@@ -129,8 +132,19 @@ import {
|
|||||||
// 表格填表模块
|
// 表格填表模块
|
||||||
import { initTableFiller } from "@table-filler/index";
|
import { initTableFiller } from "@table-filler/index";
|
||||||
|
|
||||||
|
// RMA 关系记忆系统
|
||||||
|
import {
|
||||||
|
isRmaEnabled,
|
||||||
|
hasRmaConfig,
|
||||||
|
initRmaState,
|
||||||
|
getCurrentRmaConfig,
|
||||||
|
registerRmaEventListeners,
|
||||||
|
initRmaPanel,
|
||||||
|
showRmaPanel,
|
||||||
|
} from "@rma";
|
||||||
|
|
||||||
// 版本信息
|
// 版本信息
|
||||||
const VERSION = "0.4.9";
|
const VERSION = "0.6.0";
|
||||||
|
|
||||||
// 面板状态
|
// 面板状态
|
||||||
let isPanelVisible = false;
|
let isPanelVisible = false;
|
||||||
@@ -252,6 +266,9 @@ async function initPlugin() {
|
|||||||
setOpenPlotOptimizeConfigModalFunction(() =>
|
setOpenPlotOptimizeConfigModalFunction(() =>
|
||||||
showConfigModal("剧情优化", "plot"),
|
showConfigModal("剧情优化", "plot"),
|
||||||
);
|
);
|
||||||
|
setOpenRmaConfigModalFunction(() =>
|
||||||
|
showConfigModal("RMA分析", "rma"),
|
||||||
|
);
|
||||||
|
|
||||||
// 设置更新列表清空函数
|
// 设置更新列表清空函数
|
||||||
setClearUpdatesListFunction(clearUpdatesList);
|
setClearUpdatesListFunction(clearUpdatesList);
|
||||||
@@ -264,6 +281,7 @@ async function initPlugin() {
|
|||||||
updateIndexMergeModelDisplay,
|
updateIndexMergeModelDisplay,
|
||||||
updatePlotOptimizeModelDisplay,
|
updatePlotOptimizeModelDisplay,
|
||||||
refreshAIConfigList,
|
refreshAIConfigList,
|
||||||
|
updateRmaModelDisplay,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 设置总结世界书拆分配置弹窗函数
|
// 设置总结世界书拆分配置弹窗函数
|
||||||
@@ -359,6 +377,13 @@ async function initUI() {
|
|||||||
});
|
});
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
|
// 初始化 RMA 关系记忆系统(延迟以确保角色数据加载完成)
|
||||||
|
setTimeout(() => {
|
||||||
|
initRmaModule().catch((e) => {
|
||||||
|
Logger.debug("RMA 模块初始化失败:", e);
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
Logger.log("UI 初始化完成");
|
Logger.log("UI 初始化完成");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error("UI 初始化失败:", error);
|
Logger.error("UI 初始化失败:", error);
|
||||||
@@ -431,6 +456,9 @@ function registerEventListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.log("已注册事件监听");
|
Logger.log("已注册事件监听");
|
||||||
|
|
||||||
|
// 注册 RMA 事件监听(MESSAGE_RECEIVED / MESSAGE_DELETED)
|
||||||
|
registerRmaEventListeners();
|
||||||
} else {
|
} else {
|
||||||
Logger.warn("事件系统不可用,使用延迟初始化");
|
Logger.warn("事件系统不可用,使用延迟初始化");
|
||||||
// 延迟安装钩子
|
// 延迟安装钩子
|
||||||
@@ -442,6 +470,37 @@ function registerEventListeners() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 RMA 关系记忆系统
|
||||||
|
*/
|
||||||
|
async function initRmaModule() {
|
||||||
|
try {
|
||||||
|
if (!isRmaEnabled()) {
|
||||||
|
Logger.debug("RMA 未启用");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRmaConfig()) {
|
||||||
|
Logger.debug("当前角色无 RMA 配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getCurrentRmaConfig();
|
||||||
|
if (config) {
|
||||||
|
// 初始化 RMA 状态(如果首次使用)
|
||||||
|
initRmaState(config);
|
||||||
|
|
||||||
|
// 初始化悬浮面板
|
||||||
|
await initRmaPanel();
|
||||||
|
showRmaPanel();
|
||||||
|
|
||||||
|
Logger.log("RMA 模块初始化完成");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.debug("RMA 模块初始化失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 启动插件
|
// 启动插件
|
||||||
if (typeof jQuery !== "undefined") {
|
if (typeof jQuery !== "undefined") {
|
||||||
jQuery(async () => {
|
jQuery(async () => {
|
||||||
|
|||||||
263
src/rma/analyzer.js
Normal file
263
src/rma/analyzer.js
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
140
src/rma/config-loader.js
Normal file
140
src/rma/config-loader.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* RMA 配置加载器
|
||||||
|
* 从角色卡 extensions.cola_rma_config 读取 RMA 配置
|
||||||
|
* @module rma/config-loader
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from '@core/logger';
|
||||||
|
import { getContext } from '@core/sillytavern-api';
|
||||||
|
|
||||||
|
const log = Logger.createModuleLogger('RMA-ConfigLoader');
|
||||||
|
|
||||||
|
let _cachedConfig = null;
|
||||||
|
let _cachedCharId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从当前角色卡加载 RMA 配置
|
||||||
|
* @returns {object|null} RMA 配置对象,无配置返回 null
|
||||||
|
*/
|
||||||
|
export function loadRmaConfigFromCharacter() {
|
||||||
|
try {
|
||||||
|
const context = getContext();
|
||||||
|
if (!context) return null;
|
||||||
|
|
||||||
|
const chid = context.characterId;
|
||||||
|
if (chid === undefined || chid < 0) return null;
|
||||||
|
|
||||||
|
// 使用缓存避免重复读取
|
||||||
|
if (_cachedConfig && _cachedCharId === chid) {
|
||||||
|
return _cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const charData = context.characters?.[chid]?.data;
|
||||||
|
if (!charData) return null;
|
||||||
|
|
||||||
|
const rmaConfig = charData.extensions?.cola_rma_config;
|
||||||
|
if (!rmaConfig) {
|
||||||
|
_cachedConfig = null;
|
||||||
|
_cachedCharId = chid;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.log(`加载角色 RMA 配置: ${rmaConfig.character_name || '未知'} (v${rmaConfig.version || '?'})`);
|
||||||
|
_cachedConfig = rmaConfig;
|
||||||
|
_cachedCharId = chid;
|
||||||
|
return rmaConfig;
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('加载 RMA 配置失败:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测当前角色是否有 RMA 配置
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function hasRmaConfig() {
|
||||||
|
return loadRmaConfigFromCharacter() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的 RMA 配置
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
export function getCurrentRmaConfig() {
|
||||||
|
const context = getContext();
|
||||||
|
const chid = context?.characterId;
|
||||||
|
|
||||||
|
// 角色切换时清除缓存
|
||||||
|
if (chid !== _cachedCharId) {
|
||||||
|
_cachedConfig = null;
|
||||||
|
_cachedCharId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_cachedConfig) {
|
||||||
|
return loadRmaConfigFromCharacter();
|
||||||
|
}
|
||||||
|
return _cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除配置缓存(角色切换时调用)
|
||||||
|
*/
|
||||||
|
export function clearRmaConfigCache() {
|
||||||
|
_cachedConfig = null;
|
||||||
|
_cachedCharId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 RMA 配置中的阶段定义列表
|
||||||
|
* @param {object} config RMA 配置
|
||||||
|
* @returns {Array<{id: string, name: string, order: number}>}
|
||||||
|
*/
|
||||||
|
export function getPhaseDefinitions(config) {
|
||||||
|
return config?.phases?.definitions || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 RMA 配置中的初始阶段
|
||||||
|
* @param {object} config RMA 配置
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
export function getInitialPhase(config) {
|
||||||
|
return config?.phases?.initial_phase || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 RMA 配置中的敏感点
|
||||||
|
* @param {object} config RMA 配置
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function getSensitivities(config) {
|
||||||
|
return config?.sensitivities || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 RMA 配置中的情感处理模式
|
||||||
|
* @param {object} config RMA 配置
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function getEmotionalProcessing(config) {
|
||||||
|
return config?.emotional_processing || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 RMA 配置中的秘密路径
|
||||||
|
* @param {object} config RMA 配置
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function getSecrets(config) {
|
||||||
|
return config?.secrets || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取世界书条目映射配置
|
||||||
|
* @param {object} config RMA 配置
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function getWorldbookEntryConfig(config) {
|
||||||
|
return config?.worldbook_entries || {};
|
||||||
|
}
|
||||||
245
src/rma/confirmation-ui.js
Normal file
245
src/rma/confirmation-ui.js
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
/**
|
||||||
|
* RMA 确认 UI
|
||||||
|
* 处理用户对分析结果的确认/编辑/拒绝
|
||||||
|
* @module rma/confirmation-ui
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from '@core/logger';
|
||||||
|
import { getCurrentRmaConfig, getPhaseDefinitions } from './config-loader';
|
||||||
|
import { addMemory, setPhase, setCurrentTexture, updateSecretStage, addThread, resolveThread, updateStoryState, saveRmaState, getPhase } from './memory-store';
|
||||||
|
import { assessPhaseChange, executePhaseChange, updateTendency } from './phase-manager';
|
||||||
|
import { switchPhaseEntry, rewriteTextureEntry, checkAndUnlockEntries } from './worldbook-sync';
|
||||||
|
import { getRmaState } from './memory-store';
|
||||||
|
import { clearPendingAnalysis } from './response-hook';
|
||||||
|
|
||||||
|
const log = Logger.createModuleLogger('RMA-ConfirmationUI');
|
||||||
|
|
||||||
|
let _onConfirmComplete = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置确认完成回调
|
||||||
|
* @param {Function} fn
|
||||||
|
*/
|
||||||
|
export function setOnConfirmCompleteCallback(fn) {
|
||||||
|
_onConfirmComplete = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染待确认区域 HTML
|
||||||
|
* @param {object} analysisResult 分析结果 { json, narrative }
|
||||||
|
* @returns {string} HTML 字符串
|
||||||
|
*/
|
||||||
|
export function renderPendingConfirmation(analysisResult) {
|
||||||
|
if (!analysisResult?.json) {
|
||||||
|
return '<div class="rma-pending-empty">无分析结果</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { json, narrative } = analysisResult;
|
||||||
|
const config = getCurrentRmaConfig();
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
// 新记忆
|
||||||
|
if (json.new_memories?.length > 0) {
|
||||||
|
const memHtml = json.new_memories.map((m, i) => {
|
||||||
|
const typeLabel = { breakthrough: '⭐ 破防', warmth: '💛 暖意', crack: '⚡ 裂痕', revelation: '👁 揭示' }[m.type] || m.type;
|
||||||
|
return `<div class="rma-pending-item" data-index="${i}">
|
||||||
|
<span class="rma-type-badge rma-type-${m.type}">${typeLabel}</span>
|
||||||
|
<span class="rma-pending-event">${escapeHtml(m.event)}</span>
|
||||||
|
<div class="rma-pending-actions">
|
||||||
|
<button class="rma-btn-sm rma-edit-mem" data-index="${i}" title="编辑">✏️</button>
|
||||||
|
<button class="rma-btn-sm rma-delete-mem" data-index="${i}" title="删除">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
parts.push(`<div class="rma-pending-section">
|
||||||
|
<div class="rma-pending-label">新记忆</div>
|
||||||
|
${memHtml}
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 阶段变化
|
||||||
|
if (json.phase_assessment) {
|
||||||
|
const pa = json.phase_assessment;
|
||||||
|
const tendencyIcon = { warming: '🔥', stable: '➡️', cooling: '❄️' }[pa.tendency] || '➡️';
|
||||||
|
let phaseHtml = `<span>${tendencyIcon} 趋势: ${pa.tendency || 'stable'}</span>`;
|
||||||
|
if (pa.phase_changed && pa.new_phase) {
|
||||||
|
phaseHtml += `<div class="rma-phase-change">阶段变化: ${getPhase()} → <strong>${pa.new_phase}</strong>
|
||||||
|
<button class="rma-btn-sm rma-reject-phase" title="拒绝变化">✕</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
parts.push(`<div class="rma-pending-section">
|
||||||
|
<div class="rma-pending-label">关系评估</div>
|
||||||
|
${phaseHtml}
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrative
|
||||||
|
if (narrative) {
|
||||||
|
parts.push(`<div class="rma-pending-section">
|
||||||
|
<div class="rma-pending-label">AI 将看到的情绪质感</div>
|
||||||
|
<div class="rma-narrative-preview" contenteditable="true">${escapeHtml(narrative)}</div>
|
||||||
|
</div>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作按钮
|
||||||
|
parts.push(`<div class="rma-pending-actions-bar">
|
||||||
|
<button class="rma-btn rma-btn-confirm" id="rma-confirm-btn">✅ 确认</button>
|
||||||
|
<button class="rma-btn rma-btn-reanalyze" id="rma-reanalyze-btn">🔄 重新分析</button>
|
||||||
|
</div>`);
|
||||||
|
|
||||||
|
return parts.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定确认区域事件
|
||||||
|
* @param {HTMLElement} container 确认区域容器
|
||||||
|
* @param {object} analysisResult 分析结果
|
||||||
|
*/
|
||||||
|
export function bindConfirmationEvents(container, analysisResult) {
|
||||||
|
if (!container || !analysisResult) return;
|
||||||
|
|
||||||
|
// 删除记忆
|
||||||
|
container.querySelectorAll('.rma-delete-mem').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const idx = parseInt(btn.dataset.index);
|
||||||
|
if (analysisResult.json?.new_memories) {
|
||||||
|
analysisResult.json.new_memories.splice(idx, 1);
|
||||||
|
const item = btn.closest('.rma-pending-item');
|
||||||
|
if (item) item.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拒绝阶段变化
|
||||||
|
container.querySelector('.rma-reject-phase')?.addEventListener('click', () => {
|
||||||
|
if (analysisResult.json?.phase_assessment) {
|
||||||
|
analysisResult.json.phase_assessment.phase_changed = false;
|
||||||
|
analysisResult.json.phase_assessment.new_phase = null;
|
||||||
|
const el = container.querySelector('.rma-phase-change');
|
||||||
|
if (el) el.textContent = '(已拒绝阶段变化)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确认按钮
|
||||||
|
container.querySelector('#rma-confirm-btn')?.addEventListener('click', async () => {
|
||||||
|
// 读取可能被编辑的 narrative
|
||||||
|
const narrativeEl = container.querySelector('.rma-narrative-preview');
|
||||||
|
const finalNarrative = narrativeEl?.textContent || analysisResult.narrative;
|
||||||
|
await applyConfirmedResult(analysisResult, finalNarrative);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用已确认的分析结果
|
||||||
|
* @param {object} analysisResult
|
||||||
|
* @param {string} finalNarrative 最终的 narrative 文本
|
||||||
|
*/
|
||||||
|
async function applyConfirmedResult(analysisResult, finalNarrative) {
|
||||||
|
const config = getCurrentRmaConfig();
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
const { json } = analysisResult;
|
||||||
|
const oldPhase = getPhase();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 写入新记忆
|
||||||
|
let firstMemId = null;
|
||||||
|
if (json?.new_memories) {
|
||||||
|
for (const mem of json.new_memories) {
|
||||||
|
const id = addMemory(mem);
|
||||||
|
if (!firstMemId) firstMemId = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 处理阶段变化
|
||||||
|
if (json?.phase_assessment) {
|
||||||
|
updateTendency(json.phase_assessment.tendency);
|
||||||
|
if (json.phase_assessment.phase_changed && json.phase_assessment.new_phase) {
|
||||||
|
executePhaseChange(json.phase_assessment.new_phase, firstMemId);
|
||||||
|
await switchPhaseEntry(oldPhase, json.phase_assessment.new_phase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 处理秘密更新
|
||||||
|
if (json?.secret_updates) {
|
||||||
|
for (const [id, update] of Object.entries(json.secret_updates)) {
|
||||||
|
if (update.stage_changed && update.new_stage) {
|
||||||
|
updateSecretStage(id, update.new_stage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 处理未解决事项
|
||||||
|
if (json?.unresolved_threads) {
|
||||||
|
(json.unresolved_threads.added || []).forEach(t => addThread(t));
|
||||||
|
(json.unresolved_threads.resolved || []).forEach(t => resolveThread(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 处理故事状态
|
||||||
|
if (json?.story_state_updates) {
|
||||||
|
const updates = {};
|
||||||
|
for (const [key, value] of Object.entries(json.story_state_updates)) {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
updates[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
updateStoryState(updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 更新质感
|
||||||
|
if (finalNarrative) {
|
||||||
|
setCurrentTexture(finalNarrative);
|
||||||
|
await rewriteTextureEntry(finalNarrative);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 检查事件解锁
|
||||||
|
const state = getRmaState();
|
||||||
|
if (state) {
|
||||||
|
await checkAndUnlockEntries(state, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 持久化
|
||||||
|
await saveRmaState();
|
||||||
|
|
||||||
|
// 清除待确认
|
||||||
|
clearPendingAnalysis();
|
||||||
|
|
||||||
|
log.log('确认完成,已写入所有更新');
|
||||||
|
|
||||||
|
// 通知 UI 刷新
|
||||||
|
if (_onConfirmComplete) {
|
||||||
|
_onConfirmComplete();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('应用确认结果失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据确认模式判断是否需要用户确认
|
||||||
|
* @param {string} mode every_turn | important_only | auto
|
||||||
|
* @param {object} analysisJson
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function needsConfirmation(mode, analysisJson) {
|
||||||
|
if (mode === 'every_turn') return true;
|
||||||
|
if (mode === 'auto') return false;
|
||||||
|
|
||||||
|
// important_only: 阶段变化、秘密进展、crack 需要确认
|
||||||
|
if (mode === 'important_only') {
|
||||||
|
if (analysisJson?.phase_assessment?.phase_changed) return true;
|
||||||
|
if (analysisJson?.secret_updates && Object.values(analysisJson.secret_updates).some(s => s.stage_changed)) return true;
|
||||||
|
if (analysisJson?.new_memories?.some(m => m.type === 'crack')) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str || '';
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
373
src/rma/float-panel.js
Normal file
373
src/rma/float-panel.js
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
/**
|
||||||
|
* RMA 悬浮面板
|
||||||
|
* 三态面板:展开 / 半折叠 / 最小化
|
||||||
|
* @module rma/float-panel
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from '@core/logger';
|
||||||
|
import { getExtensionPath } from '@core/constants';
|
||||||
|
import { getCurrentRmaConfig, getPhaseDefinitions, getSecrets } from './config-loader';
|
||||||
|
import { getRmaState, getPhase, getRelevantMemories, getUnresolvedThreads, getStoryState, getCurrentTexture } from './memory-store';
|
||||||
|
import { getPhaseIndex } from './phase-manager';
|
||||||
|
import { setOnAnalysisCompleteCallback, getPendingAnalysis } from './response-hook';
|
||||||
|
import { renderPendingConfirmation, bindConfirmationEvents, setOnConfirmCompleteCallback, needsConfirmation } from './confirmation-ui';
|
||||||
|
import { renderTimeline, bindTimelineEvents } from './timeline-view';
|
||||||
|
import { getRmaConfig } from './index';
|
||||||
|
|
||||||
|
const log = Logger.createModuleLogger('RMA-FloatPanel');
|
||||||
|
|
||||||
|
let _panelEl = null;
|
||||||
|
let _state = 'minimized'; // expanded | half_collapsed | minimized
|
||||||
|
let _timelineVisible = false;
|
||||||
|
let _dragOffset = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 RMA 悬浮面板
|
||||||
|
*/
|
||||||
|
export async function initRmaPanel() {
|
||||||
|
if (_panelEl) return;
|
||||||
|
|
||||||
|
// 加载 HTML 模板
|
||||||
|
try {
|
||||||
|
const basePath = getExtensionPath();
|
||||||
|
const response = await fetch(`${basePath}/ui/rma-panel.html`);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.innerHTML = html;
|
||||||
|
const panel = container.firstElementChild;
|
||||||
|
if (panel) {
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
_panelEl = panel;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('加载 RMA 面板模板失败:', e);
|
||||||
|
createFallbackPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_panelEl) return;
|
||||||
|
|
||||||
|
// 设置初始状态
|
||||||
|
const config = getRmaConfig();
|
||||||
|
_state = config?.floatPanel?.defaultState || 'minimized';
|
||||||
|
applyState();
|
||||||
|
|
||||||
|
// 绑定事件
|
||||||
|
bindPanelEvents();
|
||||||
|
|
||||||
|
// 注册回调
|
||||||
|
setOnAnalysisCompleteCallback(onAnalysisComplete);
|
||||||
|
setOnConfirmCompleteCallback(onConfirmComplete);
|
||||||
|
|
||||||
|
log.log('RMA 悬浮面板初始化完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 fallback 面板(模板加载失败时)
|
||||||
|
*/
|
||||||
|
function createFallbackPanel() {
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.id = 'rma-float-panel';
|
||||||
|
panel.className = 'rma-panel rma-panel-minimized';
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="rma-panel-minimized-icon" id="rma-minimize-icon">🔮</div>
|
||||||
|
<div class="rma-panel-header" id="rma-panel-header">
|
||||||
|
<span class="rma-panel-title">RMA 关系系统</span>
|
||||||
|
<div class="rma-panel-controls">
|
||||||
|
<button class="rma-btn-sm" id="rma-collapse-btn" title="折叠">—</button>
|
||||||
|
<button class="rma-btn-sm" id="rma-minimize-btn" title="最小化">_</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rma-panel-body" id="rma-panel-body">
|
||||||
|
<div id="rma-phase-section"></div>
|
||||||
|
<div id="rma-feeling-section"></div>
|
||||||
|
<div id="rma-secrets-section"></div>
|
||||||
|
<div id="rma-threads-section"></div>
|
||||||
|
<div id="rma-story-section"></div>
|
||||||
|
<div id="rma-memories-section"></div>
|
||||||
|
<div id="rma-pending-section"></div>
|
||||||
|
</div>
|
||||||
|
<div class="rma-panel-half" id="rma-panel-half"></div>
|
||||||
|
<div class="rma-panel-footer">
|
||||||
|
<button class="rma-btn-sm" id="rma-timeline-btn">📜 完整记忆</button>
|
||||||
|
</div>
|
||||||
|
<div id="rma-timeline-container" style="display:none"></div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
_panelEl = panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定面板事件
|
||||||
|
*/
|
||||||
|
function bindPanelEvents() {
|
||||||
|
// 头部点击切换展开/半折叠
|
||||||
|
_panelEl.querySelector('#rma-panel-header')?.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('button')) return;
|
||||||
|
toggleExpandCollapse();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 折叠按钮
|
||||||
|
_panelEl.querySelector('#rma-collapse-btn')?.addEventListener('click', () => {
|
||||||
|
_state = 'half_collapsed';
|
||||||
|
applyState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 最小化按钮
|
||||||
|
_panelEl.querySelector('#rma-minimize-btn')?.addEventListener('click', () => {
|
||||||
|
_state = 'minimized';
|
||||||
|
applyState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 最小化图标点击
|
||||||
|
_panelEl.querySelector('#rma-minimize-icon')?.addEventListener('click', () => {
|
||||||
|
_state = 'half_collapsed';
|
||||||
|
applyState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 时间线按钮
|
||||||
|
_panelEl.querySelector('#rma-timeline-btn')?.addEventListener('click', () => {
|
||||||
|
_timelineVisible = !_timelineVisible;
|
||||||
|
const container = _panelEl.querySelector('#rma-timeline-container');
|
||||||
|
if (container) {
|
||||||
|
if (_timelineVisible) {
|
||||||
|
container.innerHTML = renderTimeline();
|
||||||
|
container.style.display = 'block';
|
||||||
|
bindTimelineEvents(container);
|
||||||
|
} else {
|
||||||
|
container.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 拖拽
|
||||||
|
enableDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换展开/半折叠
|
||||||
|
*/
|
||||||
|
function toggleExpandCollapse() {
|
||||||
|
_state = _state === 'expanded' ? 'half_collapsed' : 'expanded';
|
||||||
|
applyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用面板状态
|
||||||
|
*/
|
||||||
|
function applyState() {
|
||||||
|
if (!_panelEl) return;
|
||||||
|
|
||||||
|
_panelEl.classList.remove('rma-panel-expanded', 'rma-panel-half', 'rma-panel-minimized');
|
||||||
|
|
||||||
|
switch (_state) {
|
||||||
|
case 'expanded':
|
||||||
|
_panelEl.classList.add('rma-panel-expanded');
|
||||||
|
updatePanelContent();
|
||||||
|
break;
|
||||||
|
case 'half_collapsed':
|
||||||
|
_panelEl.classList.add('rma-panel-half');
|
||||||
|
updateHalfContent();
|
||||||
|
break;
|
||||||
|
case 'minimized':
|
||||||
|
_panelEl.classList.add('rma-panel-minimized');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新展开面板内容
|
||||||
|
*/
|
||||||
|
export function updatePanelContent() {
|
||||||
|
if (!_panelEl || _state !== 'expanded') return;
|
||||||
|
|
||||||
|
const config = getCurrentRmaConfig();
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!config || !state) return;
|
||||||
|
|
||||||
|
// 阶段指示器
|
||||||
|
const phaseDefs = getPhaseDefinitions(config);
|
||||||
|
const currentPhase = getPhase();
|
||||||
|
const phaseIdx = getPhaseIndex(currentPhase, config);
|
||||||
|
const phaseHtml = phaseDefs.map((p, i) => {
|
||||||
|
const name = p.name || p.id;
|
||||||
|
const active = name === currentPhase;
|
||||||
|
const past = i < phaseIdx;
|
||||||
|
return `<span class="rma-phase-dot ${active ? 'active' : ''} ${past ? 'past' : ''}">${name}</span>`;
|
||||||
|
}).join(' → ');
|
||||||
|
setInner('#rma-phase-section', `<div class="rma-phase-bar">${phaseHtml}</div>`);
|
||||||
|
|
||||||
|
// 当前感受
|
||||||
|
const texture = getCurrentTexture();
|
||||||
|
setInner('#rma-feeling-section', texture
|
||||||
|
? `<div class="rma-feeling">${escapeHtml(texture.slice(0, 100))}${texture.length > 100 ? '...' : ''}</div>`
|
||||||
|
: '');
|
||||||
|
|
||||||
|
// 秘密
|
||||||
|
const secrets = getSecrets(config);
|
||||||
|
if (Object.keys(secrets).length > 0) {
|
||||||
|
const secretHtml = Object.entries(secrets).map(([id, s]) => {
|
||||||
|
const currentStage = state.secrets?.[id]?.current_stage || s.initial_stage;
|
||||||
|
const stages = s.stages || [];
|
||||||
|
const dots = stages.map(st => st.id === currentStage ? '●' : '○').join('');
|
||||||
|
return `<div class="rma-secret-item">🔮 ${escapeHtml(s.description?.slice(0, 30) || id)} ${dots}</div>`;
|
||||||
|
}).join('');
|
||||||
|
setInner('#rma-secrets-section', `<div class="rma-pending-label">秘密</div>${secretHtml}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未解决事项
|
||||||
|
const threads = getUnresolvedThreads();
|
||||||
|
if (threads.length > 0) {
|
||||||
|
setInner('#rma-threads-section', `<div class="rma-pending-label">悬念</div>${threads.map(t => `<div class="rma-thread">⚡ ${escapeHtml(t)}</div>`).join('')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 故事状态
|
||||||
|
const storyState = getStoryState();
|
||||||
|
if (Object.keys(storyState).length > 0) {
|
||||||
|
const parts = [];
|
||||||
|
if (storyState.location) parts.push(`📍 ${storyState.location}`);
|
||||||
|
if (storyState.time_of_day) parts.push(`🕐 ${storyState.time_of_day}`);
|
||||||
|
if (storyState.day_count) parts.push(`📅 第${storyState.day_count}天`);
|
||||||
|
setInner('#rma-story-section', parts.join(' | '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最近记忆
|
||||||
|
const memories = getRelevantMemories().slice(-3);
|
||||||
|
if (memories.length > 0) {
|
||||||
|
const memHtml = memories.map(m => {
|
||||||
|
const icon = { breakthrough: '⭐', warmth: '💛', crack: '⚡', revelation: '👁' }[m.type] || '📝';
|
||||||
|
return `<div class="rma-mem-brief">${icon} ${escapeHtml(m.event?.slice(0, 50) || '')}</div>`;
|
||||||
|
}).join('');
|
||||||
|
setInner('#rma-memories-section', `<div class="rma-pending-label">最近记忆</div>${memHtml}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 待确认
|
||||||
|
const pending = getPendingAnalysis();
|
||||||
|
const pendingSection = _panelEl.querySelector('#rma-pending-section');
|
||||||
|
if (pending && pendingSection) {
|
||||||
|
pendingSection.innerHTML = renderPendingConfirmation(pending);
|
||||||
|
bindConfirmationEvents(pendingSection, pending);
|
||||||
|
} else if (pendingSection) {
|
||||||
|
pendingSection.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新半折叠摘要
|
||||||
|
*/
|
||||||
|
function updateHalfContent() {
|
||||||
|
if (!_panelEl) return;
|
||||||
|
|
||||||
|
const state = getRmaState();
|
||||||
|
const halfEl = _panelEl.querySelector('#rma-panel-half');
|
||||||
|
if (!halfEl || !state) return;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
parts.push(`💭 ${state.phase?.current || '?'}`);
|
||||||
|
|
||||||
|
const storyState = getStoryState();
|
||||||
|
if (storyState.location) parts.push(`📍 ${storyState.location}`);
|
||||||
|
if (storyState.day_count) parts.push(`📅 第${storyState.day_count}天`);
|
||||||
|
|
||||||
|
const pending = getPendingAnalysis();
|
||||||
|
if (pending) parts.push('🔔 待确认');
|
||||||
|
|
||||||
|
halfEl.textContent = parts.join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析完成回调
|
||||||
|
*/
|
||||||
|
function onAnalysisComplete(result) {
|
||||||
|
if (!result) {
|
||||||
|
// null = 仅刷新
|
||||||
|
if (_state === 'expanded') updatePanelContent();
|
||||||
|
else updateHalfContent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getRmaConfig();
|
||||||
|
const mode = config?.confirmationMode || 'every_turn';
|
||||||
|
|
||||||
|
if (needsConfirmation(mode, result.json)) {
|
||||||
|
// 需要确认:展开面板
|
||||||
|
_state = 'expanded';
|
||||||
|
applyState();
|
||||||
|
} else {
|
||||||
|
// 自动确认
|
||||||
|
import('./confirmation-ui').then(mod => {
|
||||||
|
mod.applyConfirmedResult?.(result, result.narrative);
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认完成回调
|
||||||
|
*/
|
||||||
|
function onConfirmComplete() {
|
||||||
|
updatePanelContent();
|
||||||
|
updateHalfContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示/隐藏面板
|
||||||
|
*/
|
||||||
|
export function showRmaPanel() {
|
||||||
|
if (_panelEl) {
|
||||||
|
_panelEl.style.display = '';
|
||||||
|
if (_state === 'minimized') {
|
||||||
|
_state = 'half_collapsed';
|
||||||
|
}
|
||||||
|
applyState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideRmaPanel() {
|
||||||
|
if (_panelEl) {
|
||||||
|
_panelEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽支持
|
||||||
|
*/
|
||||||
|
function enableDrag() {
|
||||||
|
const header = _panelEl?.querySelector('#rma-panel-header');
|
||||||
|
if (!header) return;
|
||||||
|
|
||||||
|
let isDragging = false;
|
||||||
|
|
||||||
|
header.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.target.closest('button')) return;
|
||||||
|
isDragging = true;
|
||||||
|
_dragOffset.x = e.clientX - _panelEl.offsetLeft;
|
||||||
|
_dragOffset.y = e.clientY - _panelEl.offsetTop;
|
||||||
|
_panelEl.style.transition = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
_panelEl.style.left = (e.clientX - _dragOffset.x) + 'px';
|
||||||
|
_panelEl.style.top = (e.clientY - _dragOffset.y) + 'px';
|
||||||
|
_panelEl.style.right = 'auto';
|
||||||
|
_panelEl.style.bottom = 'auto';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
_panelEl.style.transition = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setInner(selector, html) {
|
||||||
|
const el = _panelEl?.querySelector(selector);
|
||||||
|
if (el) el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str || '';
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
154
src/rma/index.js
Normal file
154
src/rma/index.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* RMA 关系记忆系统 - 模块入口
|
||||||
|
* @module rma
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { loadConfig, saveConfig } from '@config/config-manager';
|
||||||
|
|
||||||
|
// 配置加载
|
||||||
|
export {
|
||||||
|
loadRmaConfigFromCharacter,
|
||||||
|
hasRmaConfig,
|
||||||
|
getCurrentRmaConfig,
|
||||||
|
clearRmaConfigCache,
|
||||||
|
getPhaseDefinitions,
|
||||||
|
getInitialPhase,
|
||||||
|
getSensitivities,
|
||||||
|
getEmotionalProcessing,
|
||||||
|
getSecrets,
|
||||||
|
getWorldbookEntryConfig,
|
||||||
|
} from './config-loader';
|
||||||
|
|
||||||
|
// 记忆存储
|
||||||
|
export {
|
||||||
|
initRmaState,
|
||||||
|
getRmaState,
|
||||||
|
saveRmaState,
|
||||||
|
addMemory,
|
||||||
|
updateMemory,
|
||||||
|
deleteMemory,
|
||||||
|
invalidateMemoriesByMessage,
|
||||||
|
getPhase,
|
||||||
|
setPhase,
|
||||||
|
setPhaseTendency,
|
||||||
|
getSecretState,
|
||||||
|
updateSecretStage,
|
||||||
|
getUnresolvedThreads,
|
||||||
|
addThread,
|
||||||
|
resolveThread,
|
||||||
|
getStoryState,
|
||||||
|
updateStoryState,
|
||||||
|
setCurrentTexture,
|
||||||
|
getCurrentTexture,
|
||||||
|
getRelevantMemories,
|
||||||
|
} from './memory-store';
|
||||||
|
|
||||||
|
// 分析引擎
|
||||||
|
export { analyzeResponse } from './analyzer';
|
||||||
|
|
||||||
|
// 事件监听
|
||||||
|
export {
|
||||||
|
registerRmaEventListeners,
|
||||||
|
setOnAnalysisCompleteCallback,
|
||||||
|
getPendingAnalysis,
|
||||||
|
clearPendingAnalysis,
|
||||||
|
} from './response-hook';
|
||||||
|
|
||||||
|
// 阶段管理
|
||||||
|
export {
|
||||||
|
assessPhaseChange,
|
||||||
|
executePhaseChange,
|
||||||
|
updateTendency,
|
||||||
|
getPhaseIndex,
|
||||||
|
} from './phase-manager';
|
||||||
|
|
||||||
|
// 世界书同步
|
||||||
|
export {
|
||||||
|
findRmaEntries,
|
||||||
|
findRmaWorldBook,
|
||||||
|
switchPhaseEntry,
|
||||||
|
rewriteTextureEntry,
|
||||||
|
checkAndUnlockEntries,
|
||||||
|
} from './worldbook-sync';
|
||||||
|
|
||||||
|
// 确认 UI
|
||||||
|
export {
|
||||||
|
renderPendingConfirmation,
|
||||||
|
bindConfirmationEvents,
|
||||||
|
setOnConfirmCompleteCallback,
|
||||||
|
needsConfirmation,
|
||||||
|
} from './confirmation-ui';
|
||||||
|
|
||||||
|
// 悬浮面板
|
||||||
|
export {
|
||||||
|
initRmaPanel,
|
||||||
|
showRmaPanel,
|
||||||
|
hideRmaPanel,
|
||||||
|
updatePanelContent,
|
||||||
|
} from './float-panel';
|
||||||
|
|
||||||
|
// 时间线
|
||||||
|
export {
|
||||||
|
renderTimeline,
|
||||||
|
bindTimelineEvents,
|
||||||
|
} from './timeline-view';
|
||||||
|
|
||||||
|
// ==================== 插件配置存取 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 RMA 插件配置
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function getRmaConfig() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config?.global?.rmaConfig || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 RMA 插件配置
|
||||||
|
* @param {object} updates
|
||||||
|
*/
|
||||||
|
export function updateRmaConfig(updates) {
|
||||||
|
const config = loadConfig();
|
||||||
|
if (!config.global.rmaConfig) {
|
||||||
|
config.global.rmaConfig = {};
|
||||||
|
}
|
||||||
|
Object.assign(config.global.rmaConfig, updates);
|
||||||
|
saveConfig(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RMA 是否启用
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isRmaEnabled() {
|
||||||
|
return getRmaConfig()?.enabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 RMA 启用状态
|
||||||
|
* @param {boolean} enabled
|
||||||
|
*/
|
||||||
|
export function setRmaEnabled(enabled) {
|
||||||
|
updateRmaConfig({ enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 RMA 分析 API 配置
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function getRmaAnalysisApiConfig() {
|
||||||
|
return getRmaConfig()?.analysisApi || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 RMA 分析 API 配置
|
||||||
|
* @param {object} updates
|
||||||
|
*/
|
||||||
|
export function updateRmaAnalysisApiConfig(updates) {
|
||||||
|
const config = loadConfig();
|
||||||
|
if (!config.global.rmaConfig) config.global.rmaConfig = {};
|
||||||
|
if (!config.global.rmaConfig.analysisApi) config.global.rmaConfig.analysisApi = {};
|
||||||
|
Object.assign(config.global.rmaConfig.analysisApi, updates);
|
||||||
|
saveConfig(config);
|
||||||
|
}
|
||||||
371
src/rma/memory-store.js
Normal file
371
src/rma/memory-store.js
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
/**
|
||||||
|
* RMA 记忆存储管理
|
||||||
|
* 使用 chat_metadata.cola_rma 持久化 RMA 状态
|
||||||
|
* @module rma/memory-store
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from '@core/logger';
|
||||||
|
import { getContext } from '@core/sillytavern-api';
|
||||||
|
import { getInitialPhase, getSecrets } from './config-loader';
|
||||||
|
|
||||||
|
const log = Logger.createModuleLogger('RMA-MemoryStore');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一记忆 ID
|
||||||
|
*/
|
||||||
|
function generateMemoryId() {
|
||||||
|
return 'mem_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 chat_metadata 引用
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
function getChatMetadata() {
|
||||||
|
const context = getContext();
|
||||||
|
return context?.chat_metadata || (typeof window !== 'undefined' ? window.chat_metadata : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 RMA 状态结构
|
||||||
|
* @param {object} rmaConfig 角色卡中的 RMA 配置
|
||||||
|
*/
|
||||||
|
export function initRmaState(rmaConfig) {
|
||||||
|
const meta = getChatMetadata();
|
||||||
|
if (!meta) {
|
||||||
|
log.warn('chat_metadata 不可用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.cola_rma) {
|
||||||
|
log.log('RMA 状态已存在,跳过初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialPhase = getInitialPhase(rmaConfig);
|
||||||
|
const secrets = getSecrets(rmaConfig);
|
||||||
|
|
||||||
|
// 初始化秘密状态
|
||||||
|
const secretStates = {};
|
||||||
|
for (const [id, secret] of Object.entries(secrets)) {
|
||||||
|
secretStates[id] = {
|
||||||
|
current_stage: secret.initial_stage || secret.stages?.[0]?.id || 'unknown',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化故事状态
|
||||||
|
const storyState = {};
|
||||||
|
if (rmaConfig.story_state) {
|
||||||
|
for (const [id, field] of Object.entries(rmaConfig.story_state)) {
|
||||||
|
storyState[id] = field.initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.cola_rma = {
|
||||||
|
config_id: rmaConfig.config_id || '',
|
||||||
|
last_updated: new Date().toISOString(),
|
||||||
|
memories: [],
|
||||||
|
phase: {
|
||||||
|
current: initialPhase,
|
||||||
|
tendency: 'stable',
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
secrets: secretStates,
|
||||||
|
story_state: storyState,
|
||||||
|
unresolved_threads: [],
|
||||||
|
current_texture: rmaConfig.initial_texture || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
log.log(`RMA 状态初始化完成: 阶段=${initialPhase}`);
|
||||||
|
saveRmaState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 RMA 状态
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
export function getRmaState() {
|
||||||
|
const meta = getChatMetadata();
|
||||||
|
return meta?.cola_rma || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 持久化 RMA 状态
|
||||||
|
*/
|
||||||
|
export async function saveRmaState() {
|
||||||
|
try {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (state) {
|
||||||
|
state.last_updated = new Date().toISOString();
|
||||||
|
}
|
||||||
|
const context = getContext();
|
||||||
|
if (context?.saveChat) {
|
||||||
|
await context.saveChat();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('保存 RMA 状态失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 记忆操作 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加记忆
|
||||||
|
* @param {object} memory 记忆数据
|
||||||
|
* @returns {string} 记忆 ID
|
||||||
|
*/
|
||||||
|
export function addMemory(memory) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return null;
|
||||||
|
|
||||||
|
const id = generateMemoryId();
|
||||||
|
state.memories.push({
|
||||||
|
id,
|
||||||
|
type: memory.type || 'warmth',
|
||||||
|
day: memory.day || state.story_state?.day_count || 1,
|
||||||
|
time: memory.time || state.story_state?.time_of_day || '',
|
||||||
|
message_id: memory.message_id || null,
|
||||||
|
event: memory.event || '',
|
||||||
|
emotional_impact: memory.emotional_impact || '',
|
||||||
|
character_reaction: memory.character_reaction || '',
|
||||||
|
lasting_effect: memory.lasting_effect || '',
|
||||||
|
tags: memory.tags || [],
|
||||||
|
significance: memory.significance || 'medium',
|
||||||
|
invalidated: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新记忆
|
||||||
|
* @param {string} id 记忆 ID
|
||||||
|
* @param {object} updates 更新字段
|
||||||
|
*/
|
||||||
|
export function updateMemory(id, updates) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
const mem = state.memories.find(m => m.id === id);
|
||||||
|
if (mem) {
|
||||||
|
Object.assign(mem, updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除记忆
|
||||||
|
* @param {string} id 记忆 ID
|
||||||
|
*/
|
||||||
|
export function deleteMemory(id) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
state.memories = state.memories.filter(m => m.id !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记与指定消息关联的记忆为无效
|
||||||
|
* @param {number} messageId 消息 ID
|
||||||
|
*/
|
||||||
|
export function invalidateMemoriesByMessage(messageId) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
for (const mem of state.memories) {
|
||||||
|
if (mem.message_id === messageId && !mem.invalidated) {
|
||||||
|
mem.invalidated = true;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count > 0) {
|
||||||
|
log.log(`已标记 ${count} 条记忆为无效 (message_id=${messageId})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 阶段操作 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前阶段
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
export function getPhase() {
|
||||||
|
return getRmaState()?.phase?.current || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置阶段
|
||||||
|
* @param {string} newPhase 新阶段名
|
||||||
|
* @param {string} triggerMemoryId 触发该变化的记忆 ID
|
||||||
|
*/
|
||||||
|
export function setPhase(newPhase, triggerMemoryId = null) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
const oldPhase = state.phase.current;
|
||||||
|
if (oldPhase === newPhase) return;
|
||||||
|
|
||||||
|
state.phase.history.push({
|
||||||
|
from: oldPhase,
|
||||||
|
to: newPhase,
|
||||||
|
day: state.story_state?.day_count || 1,
|
||||||
|
trigger_memory: triggerMemoryId,
|
||||||
|
});
|
||||||
|
state.phase.current = newPhase;
|
||||||
|
|
||||||
|
log.log(`阶段切换: ${oldPhase} → ${newPhase}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新阶段趋势
|
||||||
|
* @param {string} tendency warming | stable | cooling
|
||||||
|
*/
|
||||||
|
export function setPhaseTendency(tendency) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (state?.phase) {
|
||||||
|
state.phase.tendency = tendency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 秘密操作 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取秘密状态
|
||||||
|
* @param {string} secretId
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
export function getSecretState(secretId) {
|
||||||
|
return getRmaState()?.secrets?.[secretId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新秘密阶段
|
||||||
|
* @param {string} secretId
|
||||||
|
* @param {string} newStage
|
||||||
|
*/
|
||||||
|
export function updateSecretStage(secretId, newStage) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state?.secrets) return;
|
||||||
|
|
||||||
|
if (!state.secrets[secretId]) {
|
||||||
|
state.secrets[secretId] = {};
|
||||||
|
}
|
||||||
|
state.secrets[secretId].current_stage = newStage;
|
||||||
|
log.log(`秘密更新: ${secretId} → ${newStage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 线索/未解决事项 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取未解决事项列表
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
export function getUnresolvedThreads() {
|
||||||
|
return getRmaState()?.unresolved_threads || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加未解决事项
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
export function addThread(text) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
if (!state.unresolved_threads.includes(text)) {
|
||||||
|
state.unresolved_threads.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解决事项
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
export function resolveThread(text) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
state.unresolved_threads = state.unresolved_threads.filter(t => t !== text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 故事状态 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取故事状态
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function getStoryState() {
|
||||||
|
return getRmaState()?.story_state || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新故事状态
|
||||||
|
* @param {object} updates
|
||||||
|
*/
|
||||||
|
export function updateStoryState(updates) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
Object.assign(state.story_state, updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 质感 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前情绪质感文本
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
export function setCurrentTexture(text) {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (state) {
|
||||||
|
state.current_texture = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前情绪质感文本
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getCurrentTexture() {
|
||||||
|
return getRmaState()?.current_texture || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 记忆检索 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按检索策略获取相关记忆
|
||||||
|
* 规则:最近 3-5 条 + 所有未解决 crack + top 2 breakthrough + 话题相关
|
||||||
|
* @param {string} currentDialogue 当前对话文本(用于关键词匹配)
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export function getRelevantMemories(currentDialogue = '') {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state?.memories?.length) return [];
|
||||||
|
|
||||||
|
const validMemories = state.memories.filter(m => !m.invalidated);
|
||||||
|
const selected = new Set();
|
||||||
|
|
||||||
|
// 1. 最近 5 条
|
||||||
|
const recent = validMemories.slice(-5);
|
||||||
|
recent.forEach(m => selected.add(m.id));
|
||||||
|
|
||||||
|
// 2. 所有未解决 crack
|
||||||
|
validMemories
|
||||||
|
.filter(m => m.type === 'crack' && m.lasting_effect && !m.resolved)
|
||||||
|
.forEach(m => selected.add(m.id));
|
||||||
|
|
||||||
|
// 3. Top 2 breakthrough(按时间降序取最近的)
|
||||||
|
validMemories
|
||||||
|
.filter(m => m.type === 'breakthrough')
|
||||||
|
.slice(-2)
|
||||||
|
.forEach(m => selected.add(m.id));
|
||||||
|
|
||||||
|
// 4. 话题相关(简单关键词匹配)
|
||||||
|
if (currentDialogue) {
|
||||||
|
const dialogueWords = currentDialogue.toLowerCase();
|
||||||
|
validMemories.forEach(m => {
|
||||||
|
if (m.tags?.some(tag => dialogueWords.includes(tag.toLowerCase()))) {
|
||||||
|
selected.add(m.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return validMemories.filter(m => selected.has(m.id));
|
||||||
|
}
|
||||||
83
src/rma/phase-manager.js
Normal file
83
src/rma/phase-manager.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* RMA 阶段管理器
|
||||||
|
* 阶段切换判断和执行
|
||||||
|
* @module rma/phase-manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from '@core/logger';
|
||||||
|
import { getPhaseDefinitions } from './config-loader';
|
||||||
|
import { getPhase, setPhase, setPhaseTendency } from './memory-store';
|
||||||
|
|
||||||
|
const log = Logger.createModuleLogger('RMA-PhaseManager');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评估分析结果中的阶段变化
|
||||||
|
* @param {object} analysisJson AI 分析 JSON 结果
|
||||||
|
* @param {object} config RMA 配置
|
||||||
|
* @returns {{ shouldChange: boolean, newPhase: string|null, tendency: string }}
|
||||||
|
*/
|
||||||
|
export function assessPhaseChange(analysisJson, config) {
|
||||||
|
const assessment = analysisJson?.phase_assessment;
|
||||||
|
if (!assessment) {
|
||||||
|
return { shouldChange: false, newPhase: null, tendency: 'stable' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const phaseDefs = getPhaseDefinitions(config);
|
||||||
|
const phaseNames = phaseDefs.map(p => p.name || p.id);
|
||||||
|
const currentPhase = getPhase();
|
||||||
|
|
||||||
|
// 更新趋势
|
||||||
|
const tendency = assessment.tendency || 'stable';
|
||||||
|
|
||||||
|
// 检查是否建议阶段变化
|
||||||
|
if (!assessment.phase_changed || !assessment.new_phase) {
|
||||||
|
return { shouldChange: false, newPhase: null, tendency };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPhase = assessment.new_phase;
|
||||||
|
|
||||||
|
// 验证阶段名合法
|
||||||
|
if (!phaseNames.includes(newPhase)) {
|
||||||
|
log.warn(`非法阶段名: ${newPhase}`);
|
||||||
|
return { shouldChange: false, newPhase: null, tendency };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不重复切换
|
||||||
|
if (newPhase === currentPhase) {
|
||||||
|
return { shouldChange: false, newPhase: null, tendency };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { shouldChange: true, newPhase, tendency };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行阶段切换
|
||||||
|
* @param {string} newPhase 新阶段名
|
||||||
|
* @param {string} triggerMemoryId 触发记忆 ID
|
||||||
|
*/
|
||||||
|
export function executePhaseChange(newPhase, triggerMemoryId = null) {
|
||||||
|
const oldPhase = getPhase();
|
||||||
|
setPhase(newPhase, triggerMemoryId);
|
||||||
|
log.log(`阶段已切换: ${oldPhase} → ${newPhase}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新阶段趋势
|
||||||
|
* @param {string} tendency
|
||||||
|
*/
|
||||||
|
export function updateTendency(tendency) {
|
||||||
|
if (['warming', 'stable', 'cooling'].includes(tendency)) {
|
||||||
|
setPhaseTendency(tendency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取阶段在定义中的序号(用于 UI 进度条)
|
||||||
|
* @param {string} phaseName
|
||||||
|
* @param {object} config
|
||||||
|
* @returns {number} 0-based index, -1 if not found
|
||||||
|
*/
|
||||||
|
export function getPhaseIndex(phaseName, config) {
|
||||||
|
const defs = getPhaseDefinitions(config);
|
||||||
|
return defs.findIndex(p => (p.name || p.id) === phaseName);
|
||||||
|
}
|
||||||
138
src/rma/response-hook.js
Normal file
138
src/rma/response-hook.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* RMA 事件监听 — 后处理拦截
|
||||||
|
* 监听 MESSAGE_RECEIVED 和 MESSAGE_DELETED 事件
|
||||||
|
* @module rma/response-hook
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from '@core/logger';
|
||||||
|
import { getContext, getEventSource, getEventTypes, getCurrentChat } from '@core/sillytavern-api';
|
||||||
|
import { isRmaEnabled } from './index';
|
||||||
|
import { hasRmaConfig } from './config-loader';
|
||||||
|
import { getRmaState, invalidateMemoriesByMessage, saveRmaState } from './memory-store';
|
||||||
|
import { analyzeResponse } from './analyzer';
|
||||||
|
|
||||||
|
const log = Logger.createModuleLogger('RMA-ResponseHook');
|
||||||
|
|
||||||
|
let _pendingAnalysis = null;
|
||||||
|
let _onAnalysisComplete = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置分析完成回调(由 float-panel 注入)
|
||||||
|
* @param {Function} fn
|
||||||
|
*/
|
||||||
|
export function setOnAnalysisCompleteCallback(fn) {
|
||||||
|
_onAnalysisComplete = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待确认的分析结果
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
export function getPendingAnalysis() {
|
||||||
|
return _pendingAnalysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除待确认的分析结果
|
||||||
|
*/
|
||||||
|
export function clearPendingAnalysis() {
|
||||||
|
_pendingAnalysis = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册 RMA 事件监听器
|
||||||
|
*/
|
||||||
|
export function registerRmaEventListeners() {
|
||||||
|
const eventSource = getEventSource();
|
||||||
|
const eventTypes = getEventTypes();
|
||||||
|
|
||||||
|
if (!eventSource) {
|
||||||
|
log.warn('事件系统不可用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 AI 回复完成事件
|
||||||
|
const messageEvent = eventTypes.CHARACTER_MESSAGE_RENDERED || eventTypes.MESSAGE_RECEIVED;
|
||||||
|
if (messageEvent) {
|
||||||
|
eventSource.on(messageEvent, handleMessageReceived);
|
||||||
|
log.log(`已注册 ${messageEvent === eventTypes.CHARACTER_MESSAGE_RENDERED ? 'CHARACTER_MESSAGE_RENDERED' : 'MESSAGE_RECEIVED'} 监听`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听消息删除事件(回滚同步)
|
||||||
|
if (eventTypes.MESSAGE_DELETED) {
|
||||||
|
eventSource.on(eventTypes.MESSAGE_DELETED, handleMessageDeleted);
|
||||||
|
log.log('已注册 MESSAGE_DELETED 监听');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 回复完成后的处理
|
||||||
|
*/
|
||||||
|
async function handleMessageReceived(messageId) {
|
||||||
|
// 检查条件
|
||||||
|
if (!isRmaEnabled()) return;
|
||||||
|
if (!hasRmaConfig()) return;
|
||||||
|
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chat = getCurrentChat();
|
||||||
|
if (!chat || chat.length === 0) return;
|
||||||
|
|
||||||
|
// 获取最新 AI 回复
|
||||||
|
const latestMsg = chat[chat.length - 1];
|
||||||
|
if (!latestMsg || latestMsg.is_user) return;
|
||||||
|
|
||||||
|
const latestResponse = latestMsg.mes || '';
|
||||||
|
if (!latestResponse.trim()) return;
|
||||||
|
|
||||||
|
// 获取最近消息(最多 10 条)
|
||||||
|
const recentMessages = chat.slice(-10);
|
||||||
|
|
||||||
|
log.log('触发后处理分析...');
|
||||||
|
const result = await analyzeResponse(recentMessages, latestResponse);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// 附加消息 ID
|
||||||
|
if (result.json?.new_memories) {
|
||||||
|
const msgIndex = chat.length - 1;
|
||||||
|
result.json.new_memories.forEach(m => {
|
||||||
|
m.message_id = msgIndex;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingAnalysis = result;
|
||||||
|
log.log('分析完成,等待用户确认');
|
||||||
|
|
||||||
|
// 通知 UI
|
||||||
|
if (_onAnalysisComplete) {
|
||||||
|
_onAnalysisComplete(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('后处理分析出错:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息删除时的回滚同步
|
||||||
|
*/
|
||||||
|
async function handleMessageDeleted(messageId) {
|
||||||
|
if (!isRmaEnabled()) return;
|
||||||
|
if (!hasRmaConfig()) return;
|
||||||
|
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
const numId = typeof messageId === 'number' ? messageId : parseInt(messageId);
|
||||||
|
if (isNaN(numId)) return;
|
||||||
|
|
||||||
|
invalidateMemoriesByMessage(numId);
|
||||||
|
await saveRmaState();
|
||||||
|
|
||||||
|
// 通知 UI 刷新
|
||||||
|
if (_onAnalysisComplete) {
|
||||||
|
_onAnalysisComplete(null); // null 表示仅刷新面板
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/rma/timeline-view.js
Normal file
88
src/rma/timeline-view.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* RMA 记忆时间线视图
|
||||||
|
* 按天分组展示记忆,支持类型图标
|
||||||
|
* @module rma/timeline-view
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getRmaState } from './memory-store';
|
||||||
|
|
||||||
|
const TYPE_ICONS = {
|
||||||
|
breakthrough: '⭐',
|
||||||
|
warmth: '💛',
|
||||||
|
crack: '⚡',
|
||||||
|
revelation: '👁',
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE_LABELS = {
|
||||||
|
breakthrough: '破防',
|
||||||
|
warmth: '暖意',
|
||||||
|
crack: '裂痕',
|
||||||
|
revelation: '揭示',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染完整时间线 HTML
|
||||||
|
* @returns {string} HTML 字符串
|
||||||
|
*/
|
||||||
|
export function renderTimeline() {
|
||||||
|
const state = getRmaState();
|
||||||
|
if (!state?.memories?.length) {
|
||||||
|
return '<div class="rma-timeline-empty">暂无记忆</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按天分组
|
||||||
|
const groups = {};
|
||||||
|
for (const mem of state.memories) {
|
||||||
|
const day = mem.day || '?';
|
||||||
|
if (!groups[day]) groups[day] = [];
|
||||||
|
groups[day].push(mem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 倒序排列(最近的在前)
|
||||||
|
const days = Object.keys(groups).sort((a, b) => Number(b) - Number(a));
|
||||||
|
|
||||||
|
const html = days.map(day => {
|
||||||
|
const memories = groups[day];
|
||||||
|
const memHtml = memories.map(m => {
|
||||||
|
const icon = TYPE_ICONS[m.type] || '📝';
|
||||||
|
const label = TYPE_LABELS[m.type] || m.type;
|
||||||
|
const invalidClass = m.invalidated ? ' rma-mem-invalid' : '';
|
||||||
|
return `<div class="rma-timeline-item${invalidClass}" data-mem-id="${m.id}">
|
||||||
|
<span class="rma-timeline-icon">${icon}</span>
|
||||||
|
<div class="rma-timeline-content">
|
||||||
|
<span class="rma-timeline-type">[${label}]</span>
|
||||||
|
<span class="rma-timeline-event">${escapeHtml(m.event)}</span>
|
||||||
|
${m.emotional_impact ? `<div class="rma-timeline-impact">${escapeHtml(m.emotional_impact)}</div>` : ''}
|
||||||
|
${m.invalidated ? '<div class="rma-timeline-warn">⚠️ 关联消息已删除</div>' : ''}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `<div class="rma-timeline-group">
|
||||||
|
<div class="rma-timeline-day">第 ${day} 天</div>
|
||||||
|
${memHtml}
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `<div class="rma-timeline">${html}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定时间线事件(点击展开详情)
|
||||||
|
* @param {HTMLElement} container
|
||||||
|
*/
|
||||||
|
export function bindTimelineEvents(container) {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.querySelectorAll('.rma-timeline-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
item.classList.toggle('rma-timeline-expanded');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = str || '';
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
270
src/rma/worldbook-sync.js
Normal file
270
src/rma/worldbook-sync.js
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
/**
|
||||||
|
* RMA 世界书同步
|
||||||
|
* 切换阶段条目(互斥)、改写质感条目、检查事件解锁
|
||||||
|
* @module rma/worldbook-sync
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Logger from '@core/logger';
|
||||||
|
import { getContext, loadWorldInfo } from '@core/sillytavern-api';
|
||||||
|
import { getSecretState } from './memory-store';
|
||||||
|
|
||||||
|
const log = Logger.createModuleLogger('RMA-WorldbookSync');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求头(含 CSRF)
|
||||||
|
*/
|
||||||
|
function getRequestHeaders() {
|
||||||
|
const context = getContext();
|
||||||
|
if (context && typeof context.getRequestHeaders === 'function') {
|
||||||
|
return context.getRequestHeaders();
|
||||||
|
}
|
||||||
|
return { 'Content-Type': 'application/json' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存世界书(三级 fallback)
|
||||||
|
*/
|
||||||
|
async function saveWorldBook(bookName, bookData) {
|
||||||
|
try {
|
||||||
|
const context = getContext();
|
||||||
|
|
||||||
|
// 方法 1: context API
|
||||||
|
if (context?.saveWorldInfo) {
|
||||||
|
await context.saveWorldInfo(bookName, bookData, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法 2: 全局函数
|
||||||
|
if (typeof saveWorldInfo === 'function') {
|
||||||
|
await saveWorldInfo(bookName, bookData, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方法 3: REST API
|
||||||
|
const response = await fetch('/api/worldinfo/edit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({ name: bookName, data: bookData }),
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (e) {
|
||||||
|
log.warn(`保存世界书 "${bookName}" 失败:`, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在世界书中查找 RMA 相关条目
|
||||||
|
* @param {object} bookData 世界书数据
|
||||||
|
* @returns {{ phaseEntries: object, textureEntry: object, eventEntries: object }}
|
||||||
|
*/
|
||||||
|
export function findRmaEntries(bookData) {
|
||||||
|
const phaseEntries = {}; // { phaseName: { uid, entry } }
|
||||||
|
const eventEntries = {}; // { comment: { uid, entry } }
|
||||||
|
let textureEntry = null; // { uid, entry }
|
||||||
|
|
||||||
|
if (!bookData?.entries) return { phaseEntries, textureEntry, eventEntries };
|
||||||
|
|
||||||
|
for (const [uid, entry] of Object.entries(bookData.entries)) {
|
||||||
|
const comment = entry.comment || '';
|
||||||
|
|
||||||
|
// [RMA:Phase:xxx]
|
||||||
|
const phaseMatch = comment.match(/\[RMA:Phase:(.+?)\]/);
|
||||||
|
if (phaseMatch) {
|
||||||
|
phaseEntries[phaseMatch[1]] = { uid, entry };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [RMA:Texture]
|
||||||
|
if (comment.includes('[RMA:Texture]')) {
|
||||||
|
textureEntry = { uid, entry };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [RMA:Secret:xxx] 或 [RMA:Event:xxx]
|
||||||
|
if (comment.match(/\[RMA:(Secret|Event):.+?\]/)) {
|
||||||
|
eventEntries[comment] = { uid, entry };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { phaseEntries, textureEntry, eventEntries };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在角色关联的所有世界书中查找 RMA 条目所在的世界书
|
||||||
|
* @returns {Promise<{bookName: string, bookData: object, rmaEntries: object}|null>}
|
||||||
|
*/
|
||||||
|
export async function findRmaWorldBook() {
|
||||||
|
try {
|
||||||
|
const context = getContext();
|
||||||
|
if (!context) return null;
|
||||||
|
|
||||||
|
// 获取角色绑定的世界书
|
||||||
|
const charBook = context.characters?.[context.characterId]?.data?.extensions?.world;
|
||||||
|
const worldNames = [];
|
||||||
|
|
||||||
|
if (charBook) worldNames.push(charBook);
|
||||||
|
|
||||||
|
// 也检查全局启用的世界书
|
||||||
|
const globalBooks = context.world_names || [];
|
||||||
|
for (const name of globalBooks) {
|
||||||
|
if (!worldNames.includes(name)) worldNames.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bookName of worldNames) {
|
||||||
|
const bookData = await loadWorldInfo(bookName);
|
||||||
|
if (!bookData) continue;
|
||||||
|
|
||||||
|
const rmaEntries = findRmaEntries(bookData);
|
||||||
|
const hasRma = Object.keys(rmaEntries.phaseEntries).length > 0 || rmaEntries.textureEntry;
|
||||||
|
|
||||||
|
if (hasRma) {
|
||||||
|
log.log(`找到 RMA 世界书: "${bookName}"`);
|
||||||
|
return { bookName, bookData, rmaEntries };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('查找 RMA 世界书失败:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换阶段条目(互斥:禁用旧阶段,启用新阶段)
|
||||||
|
* @param {string} oldPhase 旧阶段名
|
||||||
|
* @param {string} newPhase 新阶段名
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export async function switchPhaseEntry(oldPhase, newPhase) {
|
||||||
|
const wb = await findRmaWorldBook();
|
||||||
|
if (!wb) {
|
||||||
|
log.warn('未找到 RMA 世界书');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { bookName, bookData, rmaEntries } = wb;
|
||||||
|
const { phaseEntries } = rmaEntries;
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
// 禁用旧阶段
|
||||||
|
if (oldPhase && phaseEntries[oldPhase]) {
|
||||||
|
const { uid } = phaseEntries[oldPhase];
|
||||||
|
if (bookData.entries[uid]) {
|
||||||
|
bookData.entries[uid].enabled = false;
|
||||||
|
if (bookData.entries[uid].extensions) {
|
||||||
|
bookData.entries[uid].extensions.enabled = false;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
log.log(`禁用阶段条目: [RMA:Phase:${oldPhase}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用新阶段
|
||||||
|
if (newPhase && phaseEntries[newPhase]) {
|
||||||
|
const { uid } = phaseEntries[newPhase];
|
||||||
|
if (bookData.entries[uid]) {
|
||||||
|
bookData.entries[uid].enabled = true;
|
||||||
|
if (bookData.entries[uid].extensions) {
|
||||||
|
bookData.entries[uid].extensions.enabled = true;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
log.log(`启用阶段条目: [RMA:Phase:${newPhase}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
return await saveWorldBook(bookName, bookData);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改写质感条目内容
|
||||||
|
* @param {string} newContent 新的情绪质感文本
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export async function rewriteTextureEntry(newContent) {
|
||||||
|
const wb = await findRmaWorldBook();
|
||||||
|
if (!wb) return false;
|
||||||
|
|
||||||
|
const { bookName, bookData, rmaEntries } = wb;
|
||||||
|
const { textureEntry } = rmaEntries;
|
||||||
|
|
||||||
|
if (!textureEntry) {
|
||||||
|
log.warn('未找到 [RMA:Texture] 条目');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uid } = textureEntry;
|
||||||
|
bookData.entries[uid].content = newContent;
|
||||||
|
log.log(`更新质感条目: ${newContent.slice(0, 50)}...`);
|
||||||
|
|
||||||
|
return await saveWorldBook(bookName, bookData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查并解锁事件条目
|
||||||
|
* @param {object} rmaState 当前 RMA 状态
|
||||||
|
* @param {object} config RMA 配置
|
||||||
|
* @returns {Promise<Array<string>>} 本次解锁的条目注释列表
|
||||||
|
*/
|
||||||
|
export async function checkAndUnlockEntries(rmaState, config) {
|
||||||
|
const wb = await findRmaWorldBook();
|
||||||
|
if (!wb) return [];
|
||||||
|
|
||||||
|
const { bookName, bookData, rmaEntries } = wb;
|
||||||
|
const entryConfig = config?.worldbook_entries?.event_entries || [];
|
||||||
|
const unlocked = [];
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
for (const cfg of entryConfig) {
|
||||||
|
if (!cfg.unlock_condition) continue;
|
||||||
|
|
||||||
|
const entryData = rmaEntries.eventEntries[cfg.comment];
|
||||||
|
if (!entryData) continue;
|
||||||
|
|
||||||
|
// 已启用则跳过
|
||||||
|
const { uid, entry } = entryData;
|
||||||
|
if (entry.enabled) continue;
|
||||||
|
|
||||||
|
// 检查解锁条件
|
||||||
|
let shouldUnlock = false;
|
||||||
|
const cond = cfg.unlock_condition;
|
||||||
|
|
||||||
|
if (cond.type === 'secret_stage') {
|
||||||
|
const secretState = getSecretState(cond.secret);
|
||||||
|
if (secretState) {
|
||||||
|
const secretConfig = config.secrets?.[cond.secret];
|
||||||
|
const stages = secretConfig?.stages || [];
|
||||||
|
const currentIdx = stages.findIndex(s => s.id === secretState.current_stage);
|
||||||
|
const requiredIdx = stages.findIndex(s => s.id === cond.min_stage);
|
||||||
|
shouldUnlock = currentIdx >= 0 && requiredIdx >= 0 && currentIdx >= requiredIdx;
|
||||||
|
}
|
||||||
|
} else if (cond.type === 'memory_exists') {
|
||||||
|
shouldUnlock = rmaState.memories.some(m =>
|
||||||
|
m.type === cond.memory_type &&
|
||||||
|
m.tags?.includes(cond.secret_id) &&
|
||||||
|
!m.invalidated
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUnlock) {
|
||||||
|
bookData.entries[uid].enabled = true;
|
||||||
|
if (bookData.entries[uid].extensions) {
|
||||||
|
bookData.entries[uid].extensions.enabled = true;
|
||||||
|
}
|
||||||
|
unlocked.push(cfg.comment);
|
||||||
|
changed = true;
|
||||||
|
log.log(`解锁事件条目: ${cfg.comment}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
await saveWorldBook(bookName, bookData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlocked;
|
||||||
|
}
|
||||||
104
src/ui/events.js
104
src/ui/events.js
@@ -94,6 +94,7 @@ let loadConfigCharDescriptionFn = null;
|
|||||||
let hasImportedSummaryBooksFn = null;
|
let hasImportedSummaryBooksFn = null;
|
||||||
let openIndexMergeConfigModalFn = null;
|
let openIndexMergeConfigModalFn = null;
|
||||||
let openPlotOptimizeConfigModalFn = null;
|
let openPlotOptimizeConfigModalFn = null;
|
||||||
|
let openRmaConfigModalFn = null;
|
||||||
let clearUpdatesListFn = null;
|
let clearUpdatesListFn = null;
|
||||||
let initFlowConfigResizeFn = null;
|
let initFlowConfigResizeFn = null;
|
||||||
let updateMemorySearchBadgeFn = null;
|
let updateMemorySearchBadgeFn = null;
|
||||||
@@ -140,6 +141,7 @@ export function setLoadConfigCharDescriptionFunction(fn) { loadConfigCharDescrip
|
|||||||
export function setHasImportedSummaryBooksFunction(fn) { hasImportedSummaryBooksFn = fn; }
|
export function setHasImportedSummaryBooksFunction(fn) { hasImportedSummaryBooksFn = fn; }
|
||||||
export function setOpenIndexMergeConfigModalFunction(fn) { openIndexMergeConfigModalFn = fn; }
|
export function setOpenIndexMergeConfigModalFunction(fn) { openIndexMergeConfigModalFn = fn; }
|
||||||
export function setOpenPlotOptimizeConfigModalFunction(fn) { openPlotOptimizeConfigModalFn = fn; }
|
export function setOpenPlotOptimizeConfigModalFunction(fn) { openPlotOptimizeConfigModalFn = fn; }
|
||||||
|
export function setOpenRmaConfigModalFunction(fn) { openRmaConfigModalFn = fn; }
|
||||||
export function setClearUpdatesListFunction(fn) { clearUpdatesListFn = fn; }
|
export function setClearUpdatesListFunction(fn) { clearUpdatesListFn = fn; }
|
||||||
export function setInitFlowConfigResizeFunction(fn) { initFlowConfigResizeFn = fn; }
|
export function setInitFlowConfigResizeFunction(fn) { initFlowConfigResizeFn = fn; }
|
||||||
export function setLoadWorldbookControlListFunction(fn) { /* 已有本地实现 */ }
|
export function setLoadWorldbookControlListFunction(fn) { /* 已有本地实现 */ }
|
||||||
@@ -814,6 +816,87 @@ function bindSettingsEvents() {
|
|||||||
showConfigModalFn(category);
|
showConfigModalFn(category);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== RMA 关系记忆系统 ====================
|
||||||
|
|
||||||
|
// RMA 折叠卡片
|
||||||
|
document
|
||||||
|
.getElementById("mm-rma-toggle")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
|
const card = document.getElementById("mm-rma-card");
|
||||||
|
if (card) card.classList.toggle("expanded");
|
||||||
|
});
|
||||||
|
|
||||||
|
// RMA 启用开关
|
||||||
|
document
|
||||||
|
.getElementById("mm-rma-enabled")
|
||||||
|
?.addEventListener("change", (e) => {
|
||||||
|
const checked = e.target.checked;
|
||||||
|
import("@rma").then(({ updateRmaConfig }) => {
|
||||||
|
updateRmaConfig({ enabled: checked });
|
||||||
|
});
|
||||||
|
updateRmaBadge(checked);
|
||||||
|
if (typeof toastr !== 'undefined') {
|
||||||
|
toastr.success(`RMA 关系记忆系统已${checked ? "启用" : "禁用"}`, "记忆管理并发系统");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// RMA 确认模式
|
||||||
|
document
|
||||||
|
.getElementById("mm-rma-confirmation-mode")
|
||||||
|
?.addEventListener("change", (e) => {
|
||||||
|
const mode = e.target.value;
|
||||||
|
import("@rma").then(({ updateRmaConfig }) => {
|
||||||
|
updateRmaConfig({ confirmationMode: mode });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// RMA 面板默认状态
|
||||||
|
document
|
||||||
|
.getElementById("mm-rma-panel-state")
|
||||||
|
?.addEventListener("change", (e) => {
|
||||||
|
const state = e.target.value;
|
||||||
|
import("@rma").then(({ updateRmaConfig }) => {
|
||||||
|
updateRmaConfig({ floatPanel: { defaultState: state } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// RMA API 配置编辑按钮
|
||||||
|
document
|
||||||
|
.getElementById("mm-rma-edit")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
|
if (openRmaConfigModalFn) openRmaConfigModalFn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 RMA 徽章状态
|
||||||
|
* @param {boolean} enabled
|
||||||
|
*/
|
||||||
|
export function updateRmaBadge(enabled) {
|
||||||
|
const badge = document.getElementById("mm-rma-badge");
|
||||||
|
if (badge) {
|
||||||
|
if (enabled) {
|
||||||
|
badge.textContent = "开启";
|
||||||
|
badge.classList.add("active");
|
||||||
|
} else {
|
||||||
|
badge.textContent = "关闭";
|
||||||
|
badge.classList.remove("active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 RMA 模型显示
|
||||||
|
*/
|
||||||
|
export function updateRmaModelDisplay() {
|
||||||
|
import("@rma").then(({ getRmaAnalysisApiConfig }) => {
|
||||||
|
const config = getRmaAnalysisApiConfig();
|
||||||
|
const displayEl = document.getElementById("mm-rma-model-display");
|
||||||
|
if (displayEl) {
|
||||||
|
displayEl.textContent = config?.model || "未配置";
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1622,6 +1705,27 @@ export function loadGlobalSettingsUI() {
|
|||||||
|
|
||||||
// 初始化表格填表 UI
|
// 初始化表格填表 UI
|
||||||
initTableFillerUI();
|
initTableFillerUI();
|
||||||
|
|
||||||
|
// ==================== RMA 关系记忆系统 ====================
|
||||||
|
const rmaConfig = settings.rmaConfig || {};
|
||||||
|
const rmaEnabledCheckbox = document.getElementById("mm-rma-enabled");
|
||||||
|
if (rmaEnabledCheckbox) {
|
||||||
|
rmaEnabledCheckbox.checked = rmaConfig.enabled === true;
|
||||||
|
}
|
||||||
|
updateRmaBadge(rmaConfig.enabled === true);
|
||||||
|
|
||||||
|
const rmaConfirmationMode = document.getElementById("mm-rma-confirmation-mode");
|
||||||
|
if (rmaConfirmationMode) {
|
||||||
|
rmaConfirmationMode.value = rmaConfig.confirmationMode || "every_turn";
|
||||||
|
}
|
||||||
|
|
||||||
|
const rmaPanelState = document.getElementById("mm-rma-panel-state");
|
||||||
|
if (rmaPanelState) {
|
||||||
|
rmaPanelState.value = rmaConfig.floatPanel?.defaultState || "half_collapsed";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 RMA 模型显示
|
||||||
|
updateRmaModelDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export {
|
|||||||
setHasImportedSummaryBooksFunction,
|
setHasImportedSummaryBooksFunction,
|
||||||
setOpenIndexMergeConfigModalFunction,
|
setOpenIndexMergeConfigModalFunction,
|
||||||
setOpenPlotOptimizeConfigModalFunction,
|
setOpenPlotOptimizeConfigModalFunction,
|
||||||
|
setOpenRmaConfigModalFunction,
|
||||||
setClearUpdatesListFunction,
|
setClearUpdatesListFunction,
|
||||||
setInitFlowConfigResizeFunction,
|
setInitFlowConfigResizeFunction,
|
||||||
setLoadWorldbookControlListFunction,
|
setLoadWorldbookControlListFunction,
|
||||||
@@ -107,6 +108,9 @@ export {
|
|||||||
// 模型显示更新
|
// 模型显示更新
|
||||||
updateIndexMergeModelDisplay,
|
updateIndexMergeModelDisplay,
|
||||||
updatePlotOptimizeModelDisplay,
|
updatePlotOptimizeModelDisplay,
|
||||||
|
// RMA 关系记忆
|
||||||
|
updateRmaBadge,
|
||||||
|
updateRmaModelDisplay,
|
||||||
} from './events';
|
} from './events';
|
||||||
|
|
||||||
// 标签过滤组件
|
// 标签过滤组件
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
deleteSummaryConfig,
|
deleteSummaryConfig,
|
||||||
getGlobalSettings,
|
getGlobalSettings,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
|
saveConfig as savePluginConfig,
|
||||||
setMemoryConfig,
|
setMemoryConfig,
|
||||||
setSummaryConfig,
|
setSummaryConfig,
|
||||||
updateGlobalSettings,
|
updateGlobalSettings,
|
||||||
@@ -21,19 +22,22 @@ import { refreshWorldBookList } from "@worldbook/refresh";
|
|||||||
import { getWorldBookList, getWorldBookEntries } from "@worldbook/api";
|
import { getWorldBookList, getWorldBookEntries } from "@worldbook/api";
|
||||||
import { analyzeSummaryContent, formatCharCount } from "@worldbook/summary-splitter";
|
import { analyzeSummaryContent, formatCharCount } from "@worldbook/summary-splitter";
|
||||||
import { getSummaryContent } from "@worldbook/parser";
|
import { getSummaryContent } from "@worldbook/parser";
|
||||||
|
import { buildOpenAIModelsUrl } from "@utils/url-builder";
|
||||||
|
|
||||||
// 更新显示回调函数(将在初始化时注入)
|
// 更新显示回调函数(将在初始化时注入)
|
||||||
let updateIndexMergeModelDisplayFn = null;
|
let updateIndexMergeModelDisplayFn = null;
|
||||||
let updatePlotOptimizeModelDisplayFn = null;
|
let updatePlotOptimizeModelDisplayFn = null;
|
||||||
|
let updateRmaModelDisplayFn = null;
|
||||||
let refreshAIConfigListFn = null;
|
let refreshAIConfigListFn = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置更新显示函数
|
* 设置更新显示函数
|
||||||
*/
|
*/
|
||||||
export function setUpdateDisplayFunctions(indexMergeFn, plotOptimizeFn, refreshConfigListFn) {
|
export function setUpdateDisplayFunctions(indexMergeFn, plotOptimizeFn, refreshConfigListFn, rmaFn) {
|
||||||
updateIndexMergeModelDisplayFn = indexMergeFn;
|
updateIndexMergeModelDisplayFn = indexMergeFn;
|
||||||
updatePlotOptimizeModelDisplayFn = plotOptimizeFn;
|
updatePlotOptimizeModelDisplayFn = plotOptimizeFn;
|
||||||
refreshAIConfigListFn = refreshConfigListFn;
|
refreshAIConfigListFn = refreshConfigListFn;
|
||||||
|
updateRmaModelDisplayFn = rmaFn || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当前编辑状态
|
// 当前编辑状态
|
||||||
@@ -131,6 +135,8 @@ export function showConfigModal(category, type = "memory", partInfo = null) {
|
|||||||
itemConfig = globalSettings.indexMergeConfig || {};
|
itemConfig = globalSettings.indexMergeConfig || {};
|
||||||
} else if (type === "plot") {
|
} else if (type === "plot") {
|
||||||
itemConfig = globalSettings.plotOptimizeConfig || {};
|
itemConfig = globalSettings.plotOptimizeConfig || {};
|
||||||
|
} else if (type === "rma") {
|
||||||
|
itemConfig = globalSettings.rmaConfig?.analysisApi || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置标题
|
// 设置标题
|
||||||
@@ -411,6 +417,25 @@ export async function saveConfig() {
|
|||||||
// 更新显示
|
// 更新显示
|
||||||
if (updatePlotOptimizeModelDisplayFn) updatePlotOptimizeModelDisplayFn();
|
if (updatePlotOptimizeModelDisplayFn) updatePlotOptimizeModelDisplayFn();
|
||||||
Logger.log(`剧情优化配置已保存`);
|
Logger.log(`剧情优化配置已保存`);
|
||||||
|
} else if (currentEditingType === "rma") {
|
||||||
|
const rmaAnalysisApi = {
|
||||||
|
apiFormat: format,
|
||||||
|
apiUrl: urlEl?.value || "",
|
||||||
|
apiKey: keyEl?.value || "",
|
||||||
|
model: modelEl?.value || "",
|
||||||
|
maxTokens: parseInt(maxTokensEl?.value || "1500", 10),
|
||||||
|
temperature: parseFloat(temperatureEl?.value || "0.3"),
|
||||||
|
customTemplate: customTemplateEl?.value || "",
|
||||||
|
responsePath: responsePathEl?.value || "choices.0.message.content",
|
||||||
|
};
|
||||||
|
// 使用 RMA 自带的配置更新函数
|
||||||
|
const config = loadConfig();
|
||||||
|
if (!config.global.rmaConfig) config.global.rmaConfig = {};
|
||||||
|
config.global.rmaConfig.analysisApi = rmaAnalysisApi;
|
||||||
|
savePluginConfig(config);
|
||||||
|
// 更新显示
|
||||||
|
if (updateRmaModelDisplayFn) updateRmaModelDisplayFn();
|
||||||
|
Logger.log(`RMA 分析配置已保存`);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.log(`配置已保存: ${currentEditingCategory}`);
|
Logger.log(`配置已保存: ${currentEditingCategory}`);
|
||||||
@@ -514,18 +539,8 @@ export async function fetchModels() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动补全 /v1/models
|
// 统一的反代兼容模型列表 URL 构造
|
||||||
let modelsUrl = apiUrl;
|
let modelsUrl = buildOpenAIModelsUrl(apiUrl);
|
||||||
if (apiUrl.endsWith("/v1") || apiUrl.endsWith("/v1/")) {
|
|
||||||
modelsUrl = apiUrl.replace(/\/v1\/?$/, "/v1/models");
|
|
||||||
} else if (apiUrl.includes("/v1/chat/completions")) {
|
|
||||||
modelsUrl = apiUrl.replace("/v1/chat/completions", "/v1/models");
|
|
||||||
} else if (apiUrl.includes("/chat/completions")) {
|
|
||||||
modelsUrl = apiUrl.replace("/chat/completions", "/models");
|
|
||||||
} else if (!apiUrl.includes("/models")) {
|
|
||||||
// 尝试添加 /v1/models
|
|
||||||
modelsUrl = apiUrl.replace(/\/?$/, "") + "/v1/models";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
fetchBtn.classList.add("mm-loading-models");
|
fetchBtn.classList.add("mm-loading-models");
|
||||||
|
|||||||
438
style.css
438
style.css
@@ -9074,4 +9074,442 @@ input.mm-input-sm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
RMA 关系记忆系统 - 悬浮面板
|
||||||
|
============================================================================ */
|
||||||
|
|
||||||
|
.rma-panel {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
top: 80px;
|
||||||
|
z-index: 10000;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mm-text, #e4e4e4);
|
||||||
|
transition: var(--mm-transition, all 0.3s ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 最小化状态 */
|
||||||
|
.rma-panel.rma-panel-minimized .rma-panel-header,
|
||||||
|
.rma-panel.rma-panel-minimized .rma-panel-body,
|
||||||
|
.rma-panel.rma-panel-minimized .rma-panel-half,
|
||||||
|
.rma-panel.rma-panel-minimized .rma-panel-footer,
|
||||||
|
.rma-panel.rma-panel-minimized #rma-timeline-container {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel.rma-panel-minimized .rma-panel-minimized-icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel-minimized-icon {
|
||||||
|
display: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--mm-bg-card, #0f3460);
|
||||||
|
border: 1px solid var(--mm-border, #2a2a4a);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: var(--mm-shadow);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel-minimized-icon:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 半折叠状态 */
|
||||||
|
.rma-panel.rma-panel-half .rma-panel-body,
|
||||||
|
.rma-panel.rma-panel-half .rma-panel-footer,
|
||||||
|
.rma-panel.rma-panel-half #rma-timeline-container {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel.rma-panel-half .rma-panel-minimized-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel.rma-panel-half .rma-panel-half {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel.rma-panel-half {
|
||||||
|
width: auto;
|
||||||
|
max-width: 380px;
|
||||||
|
background: var(--mm-bg-card, #0f3460);
|
||||||
|
border: 1px solid var(--mm-border, #2a2a4a);
|
||||||
|
border-radius: var(--mm-radius, 8px);
|
||||||
|
box-shadow: var(--mm-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel-half {
|
||||||
|
display: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 展开状态 */
|
||||||
|
.rma-panel.rma-panel-expanded .rma-panel-minimized-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel.rma-panel-expanded .rma-panel-half {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel.rma-panel-expanded {
|
||||||
|
width: 320px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: var(--mm-bg-card, #0f3460);
|
||||||
|
border: 1px solid var(--mm-border, #2a2a4a);
|
||||||
|
border-radius: var(--mm-radius, 8px);
|
||||||
|
box-shadow: var(--mm-shadow);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 面板头部 */
|
||||||
|
.rma-panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--mm-bg-secondary, #16213e);
|
||||||
|
border-bottom: 1px solid var(--mm-border, #2a2a4a);
|
||||||
|
border-radius: var(--mm-radius, 8px) var(--mm-radius, 8px) 0 0;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-panel-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 面板主体 */
|
||||||
|
.rma-panel-body {
|
||||||
|
padding: 8px 12px;
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-section {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-section:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阶段指示器 */
|
||||||
|
.rma-phase-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-phase-dot {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--mm-bg-secondary, #16213e);
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-phase-dot.active {
|
||||||
|
background: var(--mm-primary, #4a90d9);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-phase-dot.past {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 感受/质感 */
|
||||||
|
.rma-feeling {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
padding: 4px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 秘密 */
|
||||||
|
.rma-secret-item {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 未解决事项 */
|
||||||
|
.rma-thread {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 0;
|
||||||
|
color: var(--mm-warning, #ffc107);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 故事状态 */
|
||||||
|
.rma-story-bar {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 记忆简述 */
|
||||||
|
.rma-mem-brief {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签 */
|
||||||
|
.rma-pending-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid var(--mm-border, #2a2a4a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 面板底部 */
|
||||||
|
.rma-panel-footer {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-top: 1px solid var(--mm-border, #2a2a4a);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 待确认区域 ---- */
|
||||||
|
.rma-pending-area {
|
||||||
|
border: 1px solid var(--mm-warning, #ffc107);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: rgba(255, 193, 7, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-pending-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-pending-event {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-pending-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-type-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-type-breakthrough { background: rgba(255, 215, 0, 0.2); color: #ffd700; }
|
||||||
|
.rma-type-warmth { background: rgba(255, 200, 50, 0.2); color: #ffc832; }
|
||||||
|
.rma-type-crack { background: rgba(220, 53, 69, 0.2); color: #dc3545; }
|
||||||
|
.rma-type-revelation { background: rgba(138, 43, 226, 0.2); color: #9b59b6; }
|
||||||
|
|
||||||
|
.rma-phase-change {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 0;
|
||||||
|
color: var(--mm-primary, #4a90d9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-narrative-preview {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--mm-bg-secondary, #16213e);
|
||||||
|
border-radius: 4px;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-narrative-preview:focus {
|
||||||
|
border-color: var(--mm-primary, #4a90d9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-pending-actions-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 按钮 ---- */
|
||||||
|
.rma-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--mm-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-btn-confirm {
|
||||||
|
background: var(--mm-success, #28a745);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-btn-confirm:hover {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-btn-reanalyze {
|
||||||
|
background: var(--mm-secondary, #6c757d);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-btn-reanalyze:hover {
|
||||||
|
filter: brightness(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-btn-sm {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--mm-border, #2a2a4a);
|
||||||
|
color: var(--mm-text, #e4e4e4);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--mm-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-btn-sm:hover {
|
||||||
|
background: var(--mm-bg-secondary, #16213e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 时间线 ---- */
|
||||||
|
.rma-timeline {
|
||||||
|
padding: 8px 12px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-day {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mm-primary, #4a90d9);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-type {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-event {
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-impact {
|
||||||
|
display: none;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
margin-top: 2px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-item.rma-timeline-expanded .rma-timeline-event {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-item.rma-timeline-expanded .rma-timeline-impact {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-timeline-warn {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--mm-warning, #ffc107);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-mem-invalid {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rma-pending-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--mm-text-muted, #8a8a8a);
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RMA 设置面板状态行 */
|
||||||
|
.mm-rma-status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--mm-text-secondary, #aaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
49
ui/rma-panel.html
Normal file
49
ui/rma-panel.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<!-- RMA 关系记忆系统 悬浮面板 -->
|
||||||
|
<div id="rma-float-panel" class="rma-panel rma-panel-minimized" style="display:none">
|
||||||
|
<!-- 最小化图标 -->
|
||||||
|
<div class="rma-panel-minimized-icon" id="rma-minimize-icon" title="RMA 关系系统">🔮</div>
|
||||||
|
|
||||||
|
<!-- 面板头部 -->
|
||||||
|
<div class="rma-panel-header" id="rma-panel-header">
|
||||||
|
<span class="rma-panel-title">RMA 关系系统</span>
|
||||||
|
<div class="rma-panel-controls">
|
||||||
|
<button class="rma-btn-sm" id="rma-collapse-btn" title="折叠">—</button>
|
||||||
|
<button class="rma-btn-sm" id="rma-minimize-btn" title="最小化">_</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 半折叠摘要行 -->
|
||||||
|
<div class="rma-panel-half" id="rma-panel-half"></div>
|
||||||
|
|
||||||
|
<!-- 展开面板主体 -->
|
||||||
|
<div class="rma-panel-body" id="rma-panel-body">
|
||||||
|
<!-- 阶段指示器 -->
|
||||||
|
<div id="rma-phase-section" class="rma-section"></div>
|
||||||
|
|
||||||
|
<!-- 当前感受 -->
|
||||||
|
<div id="rma-feeling-section" class="rma-section"></div>
|
||||||
|
|
||||||
|
<!-- 秘密进度 -->
|
||||||
|
<div id="rma-secrets-section" class="rma-section"></div>
|
||||||
|
|
||||||
|
<!-- 未解决事项 -->
|
||||||
|
<div id="rma-threads-section" class="rma-section"></div>
|
||||||
|
|
||||||
|
<!-- 故事状态 -->
|
||||||
|
<div id="rma-story-section" class="rma-section rma-story-bar"></div>
|
||||||
|
|
||||||
|
<!-- 最近记忆 -->
|
||||||
|
<div id="rma-memories-section" class="rma-section"></div>
|
||||||
|
|
||||||
|
<!-- 待确认区域 -->
|
||||||
|
<div id="rma-pending-section" class="rma-section rma-pending-area"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作栏 -->
|
||||||
|
<div class="rma-panel-footer">
|
||||||
|
<button class="rma-btn-sm" id="rma-timeline-btn" title="查看完整记忆时间线">📜 完整记忆</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间线容器 -->
|
||||||
|
<div id="rma-timeline-container" style="display:none"></div>
|
||||||
|
</div>
|
||||||
@@ -726,6 +726,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- RMA 关系记忆系统折叠卡片 -->
|
||||||
|
<div class="mm-collapse-card" id="mm-rma-card">
|
||||||
|
<div class="mm-collapse-header" id="mm-rma-toggle">
|
||||||
|
<div class="mm-collapse-title">
|
||||||
|
<i class="fa-solid fa-heart-pulse"></i>
|
||||||
|
<span>RMA 关系记忆</span>
|
||||||
|
<span class="mm-collapse-badge" id="mm-rma-badge">关闭</span>
|
||||||
|
</div>
|
||||||
|
<i class="fa-solid fa-chevron-down mm-collapse-arrow"></i>
|
||||||
|
</div>
|
||||||
|
<div class="mm-collapse-body">
|
||||||
|
<div class="mm-index-mode-content">
|
||||||
|
<!-- 启用开关 -->
|
||||||
|
<div class="mm-setting-item">
|
||||||
|
<label class="mm-label-with-hint">
|
||||||
|
<input type="checkbox" id="mm-rma-enabled" />
|
||||||
|
启用 RMA 关系记忆系统
|
||||||
|
<i class="fa-solid fa-circle-question mm-hint-icon" title="自动分析每轮对话中的关系变化,管理记忆、动态更新世界书条目。需要角色卡包含 RMA 配置。"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 角色卡检测状态 -->
|
||||||
|
<div class="mm-setting-item" id="mm-rma-char-status" style="display: none;">
|
||||||
|
<div class="mm-rma-status-row">
|
||||||
|
<i class="fa-solid fa-user-check" style="color: var(--mm-success);"></i>
|
||||||
|
<span id="mm-rma-char-status-text">当前角色已配置 RMA</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认模式 -->
|
||||||
|
<div class="mm-setting-item">
|
||||||
|
<label class="mm-label-with-hint">
|
||||||
|
确认模式
|
||||||
|
<i class="fa-solid fa-circle-question mm-hint-icon" title="every_turn: 每轮都弹窗确认 important_only: 仅阶段变化/秘密进展/裂痕时确认 auto: 全自动,事后可在面板修改"></i>
|
||||||
|
</label>
|
||||||
|
<select id="mm-rma-confirmation-mode" class="mm-select mm-select-sm">
|
||||||
|
<option value="every_turn">每轮确认</option>
|
||||||
|
<option value="important_only">仅重要变化</option>
|
||||||
|
<option value="auto">全自动</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 面板默认状态 -->
|
||||||
|
<div class="mm-setting-item">
|
||||||
|
<label class="mm-label-with-hint">
|
||||||
|
悬浮面板默认状态
|
||||||
|
<i class="fa-solid fa-circle-question mm-hint-icon" title="面板初始显示状态 展开: 显示所有信息 半折叠: 仅显示一行摘要 最小化: 仅显示角落图标"></i>
|
||||||
|
</label>
|
||||||
|
<select id="mm-rma-panel-state" class="mm-select mm-select-sm">
|
||||||
|
<option value="expanded">展开</option>
|
||||||
|
<option value="half_collapsed">半折叠</option>
|
||||||
|
<option value="minimized">最小化</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RMA 分析 API 配置卡片 -->
|
||||||
|
<div id="mm-rma-config-card" class="mm-ai-config-item" style="margin-top: 10px;">
|
||||||
|
<div class="mm-config-info">
|
||||||
|
<span class="mm-config-name">
|
||||||
|
<i class="fa-solid fa-robot"></i>
|
||||||
|
RMA 分析
|
||||||
|
</span>
|
||||||
|
<span class="mm-config-model" id="mm-rma-model-display">未配置</span>
|
||||||
|
</div>
|
||||||
|
<div class="mm-config-actions">
|
||||||
|
<button type="button" id="mm-rma-edit" class="mm-btn mm-btn-xs mm-btn-primary" title="编辑配置">
|
||||||
|
<i class="fa-solid fa-pen"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ module.exports = (env, argv) => {
|
|||||||
'@ui': path.resolve(__dirname, 'src/ui'),
|
'@ui': path.resolve(__dirname, 'src/ui'),
|
||||||
'@utils': path.resolve(__dirname, 'src/utils'),
|
'@utils': path.resolve(__dirname, 'src/utils'),
|
||||||
'@table-filler': path.resolve(__dirname, 'src/table-filler'),
|
'@table-filler': path.resolve(__dirname, 'src/table-filler'),
|
||||||
|
'@rma': path.resolve(__dirname, 'src/rma'),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user