Merge branch 'Wx-2025:main' into main

This commit is contained in:
SilenceLurker
2025-12-24 22:11:44 +08:00
committed by GitHub
44 changed files with 3992 additions and 1297 deletions

126
core/archive-manager.js Normal file
View File

@@ -0,0 +1,126 @@
import { ingestTextToHanlinyuan, getSettings } from './rag-processor.js';
import { deleteRow, insertRow, updateRow } from './table-system/manager.js';
import { extension_settings } from '/scripts/extensions.js';
import { extensionName } from '../utils/settings.js';
let isArchiving = false;
export function initializeArchiveManager() {
document.addEventListener('AMILY2_TABLE_UPDATED', handleTableUpdate);
console.log('[归档管理器] 已启动,正在监控表格状态...');
}
async function handleTableUpdate(event) {
const { tableName, data, role } = event.detail;
const settings = getSettings();
if (!settings.archive || !settings.archive.enabled) return;
const targetTable = settings.archive.targetTable || '总结表';
const threshold = settings.archive.threshold || 20;
if (tableName !== targetTable) return;
if (isArchiving) return;
let hasNotice = false;
if (data.length > 0 && data[0][2] && data[0][2].includes('已自动归档')) {
hasNotice = true;
realRows = data.slice(1);
}
if (realRows.length > threshold) {
console.log(`[归档管理器] 检测到 ${targetTable} 行数 (${realRows.length}) 超过阈值 (${threshold}),开始归档...`);
await performArchive(data, hasNotice, targetTable);
}
}
async function performArchive(allRows, hasNotice, targetTable) {
isArchiving = true;
const settings = getSettings();
const batchSize = settings.archive.batchSize || 10;
try {
const startIndex = hasNotice ? 1 : 0;
const rowsToArchive = allRows.slice(startIndex, startIndex + batchSize);
if (rowsToArchive.length === 0) return;
const tables = getMemoryState();
const outlineTable = tables ? tables.find(t => t.name === '总体大纲') : null;
const outlineMap = new Map();
if (outlineTable && outlineTable.rows) {
outlineTable.rows.forEach(row => {
if (row[0]) outlineMap.set(row[0], row[1] || '无大纲内容');
});
}
const archiveText = rowsToArchive.map(row => {
const index = row[0] || '未知索引';
const timeSpan = row[1] || '未知时间';
const summary = row[2] || '无内容';
const outline = outlineMap.get(index) || '无大纲关联';
return `[历史总结归档] [索引: ${index}] [时间: ${timeSpan}] [大纲: ${outline}]\n${summary}`;
}).join('\n\n');
const fullText = archiveText;
console.log('[归档管理器] 正在将旧总结录入翰林院...');
const result = await ingestTextToHanlinyuan(
fullText,
'manual',
{ sourceName: '历史总结归档' },
(progress) => console.log(`[归档进度] ${progress.message}`)
);
if (result.success) {
console.log('[归档管理器] 录入成功,正在清理表格...');
const indicesToDelete = [];
for (let i = 0; i < rowsToArchive.length; i++) {
indicesToDelete.push(startIndex + i);
}
for (let i = indicesToDelete.length - 1; i >= 0; i--) {
await deleteRow(findTableIndex(targetTable), indicesToDelete[i]);
}
const noticeText = `(已自动归档 ${rowsToArchive.length} 条历史记录至翰林院,可随时询问找回)`;
const noticeRowData = {
0: 'SYSTEM',
1: '---',
2: noticeText
};
if (hasNotice) {
await updateRow(findTableIndex(targetTable), 0, noticeRowData);
} else {
await insertRow(findTableIndex(targetTable), 0, 'above');
await updateRow(findTableIndex(targetTable), 0, noticeRowData);
}
console.log('[归档管理器] 归档流程完成。');
} else {
console.error('[归档管理器] RAG 录入失败,取消清理。', result.error);
}
} catch (error) {
console.error('[归档管理器] 执行出错:', error);
} finally {
isArchiving = false;
}
}
import { getMemoryState } from './table-system/manager.js';
function findTableIndex(name) {
const tables = getMemoryState();
if (!tables) return -1;
return tables.findIndex(t => t.name === name);
}

View File

@@ -0,0 +1,336 @@
import { callAi, getApiConfig } from "./api.js";
import { tools, getToolDefinitions } from "./tools.js";
import { ContextManager } from "./context-manager.js";
export class AgentManager {
constructor() {
this.history = [];
this.executorHistory = [];
this.reviewerHistory = [];
this.executorSystemPrompt = this.buildExecutorSystemPrompt();
this.reviewerSystemPrompt = this.buildReviewerSystemPrompt();
this.contextManager = new ContextManager();
this.currentChid = undefined;
this.currentBookName = undefined;
}
setContext(chid, bookName) {
this.currentChid = chid;
this.currentBookName = bookName;
this.executorSystemPrompt = this.buildExecutorSystemPrompt();
}
buildReviewerSystemPrompt() {
const toolDefs = getToolDefinitions();
let prompt = `你是一个经验丰富的角色卡设计师和辅导员Reviewer。你的搭档是一个执行力强且富有创造力的 AI 助手Executor
你的目标是根据用户的需求,设计出高质量的角色卡和世界书方案,并指导 Executor 一步步实现。
Executor 拥有以下工具(你不能直接使用,但需要知道它能做什么):
`;
toolDefs.forEach(tool => {
prompt += `- ${tool.name}: ${tool.description}\n`;
});
prompt += `
### 世界书高级设置指南 (World Info Settings)
- **constant (蓝灯)**: 如果为 true该条目将始终被激活并包含在上下文中忽略关键词触发。
- **position (插入位置)**: 决定条目内容在 Prompt 中的位置。
- \`before/after_character_definition\`: 角色定义前后。
- \`before/after_author_note\`: 作者注释前后。
- \`at_depth_as_system\`: 在指定深度作为系统消息插入(推荐)。
- **depth (插入深度)**: 仅当 position 为 \`at_depth_as_system\` 时有效。表示条目距离最新消息的距离(例如 0 为最新4 为倒数第 4 条消息后)。
- **scanDepth (扫描深度)**: 系统扫描关键词的消息范围。例如 2 表示只扫描最近 2 条消息。
- **exclude_recursion**: 如果为 true此条目的内容不会触发其他条目。
- **prevent_recursion**: 如果为 true其他条目的内容不会触发此条目。
你的工作流程:
1. 分析用户需求。
2. 制定详细的实施计划(大纲)。
3. 将计划拆解为 Executor 可以执行的**指导性指令**。
4. 审查 Executor 的执行结果,提出修改意见。
**关键原则:**
- **只给方案,不给成品**:你负责提供创意方向、关键设定点和风格指导,让 Executor 去进行具体的文本创作和扩写。不要直接把完整的角色描述或世界书内容写出来让 Executor 照抄。
- **示例**
- ❌ 错误:“请写入以下描述:她有一头金发,性格傲娇...”
- ✅ 正确:“请为角色撰写一段详细的外貌和性格描述。外貌上要突出她的金发和贵族气质,性格上要体现出‘傲娇’的特点,即外表冷漠但内心渴望被关怀。请发挥你的文采。”
交互规则:
- 当你需要 Executor 执行操作时,请在回复的最后一行使用标签:<instruction>你的指令</instruction>
- **重要**<instruction> 标签内必须是**自然语言指令**。**严禁**直接输出 JSON 代码块作为指令。
- **单步原则**:每次指令**只能包含一个**具体的任务(例如:只创建一个世界书条目,或只更新角色描述)。严禁一次性下达多个任务。
- **字数强制**:在指令中必须明确要求 Executor 进行深度扩写。
- 世界书条目:要求**不低于 300 字**。
- 角色开场白:要求**不低于 1500 字**。
- 当你认为任务已完成或需要用户反馈时,直接回复用户即可,不要包含 <instruction> 标签。
`;
return prompt;
}
buildExecutorSystemPrompt() {
const toolDefs = getToolDefinitions();
let contextInfo = "";
if (this.currentChid === 'new') {
contextInfo += `**注意:用户希望创建一个新角色。**\n请首先使用 \`create_character\` 工具创建角色。创建成功后,你将获得新的角色 ID请使用该 ID 进行后续操作(如 \`update_character_card\`)。\n`;
} else if (this.currentChid !== undefined) {
contextInfo += `当前操作的角色ID: ${this.currentChid}\n`;
}
if (this.currentBookName) {
contextInfo += `当前操作的世界书: ${this.currentBookName}\n`;
}
let prompt = `你是一个专业的角色卡构建助手Executor。你的目标是根据 Reviewer 的指导和用户的需求,在当前选定的“工作区”(角色卡和世界书)中进行**创作**和修改。
${contextInfo}
**你的职责:**
1. **理解指令**:仔细阅读 Reviewer 的指导性指令。
2. **深度扩写**这是你的核心任务。Reviewer 给出的只是大纲,你需要将其扩写成丰富、细腻的文学作品。
- **世界书条目**:必须丰富细节,字数**不低于 300 字**。
- **角色开场白**:必须包含环境描写、心理活动、动作细节,字数**不低于 1500 字**。
3. **执行操作**:使用工具将你创作的内容写入系统。
TOOL USE
你拥有以下工具可以使用。你可以使用这些工具来完成任务。每次回复只能使用一个工具。
# Tools
`;
toolDefs.forEach(tool => {
prompt += `## ${tool.name}\n`;
prompt += `Description: ${tool.description}\n`;
prompt += `Parameters:\n${JSON.stringify(tool.parameters, null, 2)}\n\n`;
});
prompt += `
### 世界书高级设置指南 (World Info Settings)
- **constant (蓝灯)**: 如果为 true该条目将始终被激活并包含在上下文中忽略关键词触发。
- **position (插入位置)**: 决定条目内容在 Prompt 中的位置。
- \`before/after_character_definition\`: 角色定义前后。
- \`before/after_author_note\`: 作者注释前后。
- \`at_depth_as_system\`: 在指定深度作为系统消息插入(推荐)。
- **depth (插入深度)**: 仅当 position 为 \`at_depth_as_system\` 时有效。表示条目距离最新消息的距离(例如 0 为最新4 为倒数第 4 条消息后)。
- **scanDepth (扫描深度)**: 系统扫描关键词的消息范围。例如 2 表示只扫描最近 2 条消息。
- **exclude_recursion**: 如果为 true此条目的内容不会触发其他条目。
- **prevent_recursion**: 如果为 true其他条目的内容不会触发此条目。
# Tool Use Formatting
工具调用必须使用以下 XML 格式。工具名称包含在开始和结束标签中,每个参数也包含在自己的标签中:
<工具名称>
<参数1>值1</参数1>
<参数2>值2</参数2>
...
</工具名称>
**注意**:对于复杂参数(如数组或对象),请直接在标签内写入 **JSON 字符串**。
例如:
<write_world_info_entry>
<book_name>MyWorld</book_name>
<entries>[{"key": "Entry1", "content": "..."}]</entries>
</write_world_info_entry>
# Tool Use Guidelines
1. **必须思考 (Mandatory Thinking)**: 在调用任何工具之前,你**必须**先输出一段思考过程,解释你为什么要这样做,以及你打算如何创作内容。请使用 \`<thinking>\` 标签包裹你的思考。**严禁**直接输出工具调用而不进行思考。
2. **单步执行**: 每次回复只能使用**一个**工具。必须等待工具执行结果(成功或失败)后,才能决定并执行下一步操作。
3. **等待确认**: 永远不要假设工具执行成功。必须根据实际返回的结果来判断。
4. **参数完整性**: 确保提供所有必需的参数。
# Capabilities
- 你可以读取和修改当前绑定的世界书World Info
- 你可以读取和修改当前角色的详细信息Name, Description, Personality, Scenario, First Message, etc.)。
- 你可以管理角色的开场白(添加、修改、删除)。
# Rules
1. **工作区**: 你始终在当前选定的角色卡和世界书上下文中操作。
2. **路径**: 如果涉及文件路径(虽然主要通过 API 操作),请认为是相对于工作区的虚拟路径。
3. **完成任务**: 当你认为任务已经完成时,请向用户汇报结果。不要在汇报结果后继续提问。
现在,请开始你的工作。
`;
return prompt;
}
async handleUserMessage(message, onStreamUpdate, onPreviewUpdate) {
this.history.push({ role: 'user', content: message });
this.reviewerHistory.push({ role: 'user', content: message });
await this.runDualAgentLoop(onStreamUpdate, onPreviewUpdate);
}
async runDualAgentLoop(onStreamUpdate, onPreviewUpdate) {
let maxLoops = 3;
let currentLoop = 0;
while (currentLoop < maxLoops) {
currentLoop++;
onStreamUpdate("Reviewer (模型B) 正在思考...", 'system');
const reviewerConfig = getApiConfig('reviewer');
const reviewerMessages = this.contextManager.buildMessages(
this.reviewerSystemPrompt,
this.reviewerHistory,
reviewerConfig.maxTokens
);
let reviewerResponse;
try {
reviewerResponse = await callAi('reviewer', reviewerMessages);
} catch (error) {
onStreamUpdate(`[Reviewer 错误] ${error.message}`, 'system');
return;
}
const instructionMatch = reviewerResponse.match(/<instruction>([\s\S]*?)<\/instruction>/);
const instruction = instructionMatch ? instructionMatch[1].trim() : null;
const displayContent = reviewerResponse.replace(/<instruction>[\s\S]*?<\/instruction>/, '').trim();
if (displayContent) {
onStreamUpdate(displayContent, 'assistant');
this.history.push({ role: 'assistant', content: displayContent });
this.reviewerHistory.push({ role: 'assistant', content: displayContent });
}
if (!instruction) {
break;
}
onStreamUpdate(`Reviewer 指令: ${instruction}`, 'system');
this.executorHistory.push({ role: 'user', content: instruction });
await this.runExecutorLoop(onStreamUpdate, onPreviewUpdate);
const lastExecutorResponse = this.executorHistory[this.executorHistory.length - 1];
if (lastExecutorResponse && lastExecutorResponse.role === 'assistant') {
this.reviewerHistory.push({
role: 'user',
content: `[Executor 执行结果]\n${lastExecutorResponse.content}`
});
}
}
}
async runExecutorLoop(onStreamUpdate, onPreviewUpdate) {
let maxTurns = 5;
let currentTurn = 0;
while (currentTurn < maxTurns) {
currentTurn++;
const executorConfig = getApiConfig('executor');
const messages = this.contextManager.buildMessages(
this.executorSystemPrompt,
this.executorHistory,
executorConfig.maxTokens
);
let responseContent;
try {
responseContent = await callAi('executor', messages);
} catch (error) {
onStreamUpdate(`[Executor 错误] ${error.message}`, 'system');
return;
}
onStreamUpdate(responseContent, 'executor');
this.executorHistory.push({ role: 'assistant', content: responseContent });
const toolCall = this.parseToolCall(responseContent);
if (toolCall) {
if (toolCall.name === 'update_character_card' || toolCall.name === 'read_character_card' || toolCall.name === 'edit_character_text' || toolCall.name === 'manage_first_message') {
if (toolCall.arguments.chid === undefined && this.currentChid !== undefined) {
toolCall.arguments.chid = parseInt(this.currentChid);
}
}
if (toolCall.name === 'write_world_info_entry' || toolCall.name === 'read_world_info') {
if (!toolCall.arguments.book_name && this.currentBookName) {
toolCall.arguments.book_name = this.currentBookName;
}
}
onStreamUpdate(`[Executor] 执行工具: ${toolCall.name}`, 'system');
let result;
try {
if (tools[toolCall.name]) {
result = await tools[toolCall.name](toolCall.arguments);
if (toolCall.name === 'create_character' && result.includes('ID:')) {
const match = result.match(/ID:\s*(\d+)/);
if (match) {
this.currentChid = parseInt(match[1]);
this.executorSystemPrompt = this.buildExecutorSystemPrompt();
}
}
} else {
result = `Error: Tool '${toolCall.name}' not found.`;
}
} catch (error) {
result = `Error executing tool '${toolCall.name}': ${error.message}`;
}
const toolResultMsg = `[Tool Result for ${toolCall.name}]\n${result}`;
this.executorHistory.push({ role: 'user', content: toolResultMsg });
onStreamUpdate(`[Executor] 工具结果: ${result.substring(0, 100)}...`, 'system');
if (onPreviewUpdate && !result.startsWith('Error')) {
onPreviewUpdate(toolCall.name, toolCall.arguments);
}
} else {
break;
}
}
}
parseToolCall(content) {
const toolNames = Object.keys(tools);
for (const name of toolNames) {
const regex = new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`);
const match = content.match(regex);
if (match) {
const argsContent = match[1];
const args = {};
const paramRegex = /<(\w+)>([\s\S]*?)<\/\1>/g;
let paramMatch;
while ((paramMatch = paramRegex.exec(argsContent)) !== null) {
const paramName = paramMatch[1];
let paramValue = paramMatch[2];
if (paramValue.trim().startsWith('{') || paramValue.trim().startsWith('[')) {
try {
paramValue = JSON.parse(paramValue);
} catch (e) {
}
}
args[paramName] = paramValue;
}
return { name, arguments: args };
}
}
return null;
}
clearHistory() {
this.history = [];
this.executorHistory = [];
this.reviewerHistory = [];
}
}

122
core/auto-char-card/api.js Normal file
View File

@@ -0,0 +1,122 @@
import { extension_settings } from "/scripts/extensions.js";
import { getRequestHeaders } from "/script.js";
import { extensionName } from "../../utils/settings.js";
const DEFAULT_CONFIG = {
apiUrl: "",
apiKey: "",
model: "",
maxTokens: 4000,
temperature: 0.7
};
export function getApiConfig(role) {
const settings = extension_settings[extensionName] || {};
const configKey = `acc_${role}_config`;
return { ...DEFAULT_CONFIG, ...(settings[configKey] || {}) };
}
export function setApiConfig(role, config) {
if (!extension_settings[extensionName]) {
extension_settings[extensionName] = {};
}
const configKey = `acc_${role}_config`;
extension_settings[extensionName][configKey] = { ...getApiConfig(role), ...config };
}
export async function callAi(role, messages, options = {}) {
const config = { ...getApiConfig(role), ...options };
const roleName = role === 'executor' ? '执行者(模型A)' : '规划者(模型B)';
if (!config.apiUrl || !config.apiKey || !config.model) {
throw new Error(`[自动构建器] ${roleName} API 配置不完整,请检查 URL、Key 和模型设置。`);
}
console.log(`[自动构建器] 正在调用 AI (${roleName})...`, { model: config.model, messagesCount: messages.length });
const body = {
chat_completion_source: 'openai',
messages: messages,
model: config.model,
reverse_proxy: config.apiUrl,
proxy_password: config.apiKey,
stream: false,
max_tokens: config.maxTokens,
temperature: config.temperature,
top_p: 1,
custom_prompt_post_processing: 'strict',
enable_web_search: false,
frequency_penalty: 0,
presence_penalty: 0,
};
try {
const response = await fetch('/api/backends/chat-completions/generate', {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API 请求失败: ${response.status} - ${errorText}`);
}
const responseData = await response.json();
if (!responseData || !responseData.choices || responseData.choices.length === 0) {
if (responseData.error) {
throw new Error(`API 返回错误: ${responseData.error.message || JSON.stringify(responseData.error)}`);
}
throw new Error('API 返回了空响应。');
}
const content = responseData.choices[0].message?.content;
console.log(`[自动构建器] AI (${roleName}) 响应接收成功。长度: ${content?.length}`);
return content;
} catch (error) {
console.error(`[自动构建器] AI (${roleName}) 调用失败:`, error);
throw error;
}
}
export async function testConnection(role) {
try {
const response = await callAi(role, [
{ role: 'user', content: 'Hi' }
], { maxTokens: 10 });
return !!response;
} catch (error) {
console.error(`[自动构建器] ${role} 连接测试失败:`, error);
return false;
}
}
export async function fetchModels(apiUrl, apiKey) {
try {
const response = await fetch('/api/backends/chat-completions/status', {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({
reverse_proxy: apiUrl,
proxy_password: apiKey,
chat_completion_source: 'openai'
})
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const models = Array.isArray(data) ? data : (data.data || data.models || []);
return models.map(m => {
const id = m.id || m.model || m.name || m;
return typeof id === 'string' ? id : JSON.stringify(id);
}).sort();
} catch (error) {
console.error('[自动构建器] 获取模型列表失败:', error);
throw error;
}
}

View File

@@ -0,0 +1,150 @@
import { characters, saveCharacterDebounced, this_chid, getCharacters, getRequestHeaders } from "/script.js";
export function getCharacter(chid = this_chid) {
if (chid === undefined || chid < 0 || !characters[chid]) {
console.warn(`[Amily2 CharAPI] Invalid character ID: ${chid}`);
return null;
}
return characters[chid];
}
export function updateCharacter(chid, updates) {
const char = getCharacter(chid);
if (!char) return false;
let changed = false;
const fields = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example'];
fields.forEach(field => {
if (updates[field] !== undefined && char[field] !== updates[field]) {
char[field] = updates[field];
changed = true;
}
});
if (changed) {
saveCharacterDebounced();
console.log(`[Amily2 CharAPI] Updated character ${chid}:`, Object.keys(updates));
return true;
}
return false;
}
export function getFirstMessages(chid) {
const char = getCharacter(chid);
if (!char) return [];
const messages = [char.first_mes];
if (char.data && Array.isArray(char.data.alternate_greetings)) {
messages.push(...char.data.alternate_greetings);
}
return messages;
}
export function addFirstMessage(chid, message) {
const char = getCharacter(chid);
if (!char) return false;
if (!char.data) char.data = {};
if (!Array.isArray(char.data.alternate_greetings)) {
char.data.alternate_greetings = [];
}
char.data.alternate_greetings.push(message);
saveCharacterDebounced();
console.log(`[Amily2 CharAPI] Added alternate greeting to character ${chid}`);
return true;
}
export function updateFirstMessage(chid, index, message) {
const char = getCharacter(chid);
if (!char) return false;
if (index === 0) {
char.first_mes = message;
} else {
const altIndex = index - 1;
if (char.data && Array.isArray(char.data.alternate_greetings) && char.data.alternate_greetings[altIndex] !== undefined) {
char.data.alternate_greetings[altIndex] = message;
} else {
console.warn(`[Amily2 CharAPI] Alternate greeting index out of bounds: ${altIndex}`);
return false;
}
}
saveCharacterDebounced();
console.log(`[Amily2 CharAPI] Updated greeting ${index} for character ${chid}`);
return true;
}
export function removeFirstMessage(chid, index) {
const char = getCharacter(chid);
if (!char) return false;
if (index === 0) {
console.warn(`[Amily2 CharAPI] Cannot remove main greeting, clearing instead.`);
char.first_mes = "";
} else {
const altIndex = index - 1;
if (char.data && Array.isArray(char.data.alternate_greetings) && char.data.alternate_greetings[altIndex] !== undefined) {
char.data.alternate_greetings.splice(altIndex, 1);
} else {
console.warn(`[Amily2 CharAPI] Alternate greeting index out of bounds: ${altIndex}`);
return false;
}
}
saveCharacterDebounced();
console.log(`[Amily2 CharAPI] Removed greeting ${index} for character ${chid}`);
return true;
}
export async function createNewCharacter(name) {
try {
const formData = new FormData();
formData.append('ch_name', name);
formData.append('description', '');
formData.append('personality', '');
formData.append('scenario', '');
formData.append('first_mes', 'Hello!');
formData.append('mes_example', '');
formData.append('creator', 'Amily2-AutoChar');
formData.append('creator_notes', 'Character created automatically by Amily2 AutoChar Card.');
formData.append('tags', '');
formData.append('character_version', '1.0');
formData.append('post_history_instructions', '');
formData.append('system_prompt', '');
formData.append('talkativeness', '0.5');
formData.append('extensions', '{}');
formData.append('fav', 'false');
formData.append('world', '');
formData.append('depth_prompt_prompt', '');
formData.append('depth_prompt_depth', '4');
formData.append('depth_prompt_role', 'system');
const response = await fetch('/api/characters/create', {
method: 'POST',
headers: getRequestHeaders({ omitContentType: true }),
body: formData,
});
if (response.ok) {
const avatarId = await response.text();
console.log(`[Amily2 CharAPI] Created character: ${name}, Avatar ID: ${avatarId}`);
await getCharacters();
const newChid = characters.findIndex(c => c.avatar === avatarId);
if (newChid !== -1) {
return newChid;
}
return -2;
} else {
console.error(`[Amily2 CharAPI] Failed to create character: ${response.statusText}`);
return -1;
}
} catch (error) {
console.error(`[Amily2 CharAPI] Error creating character:`, error);
return -1;
}
}

View File

@@ -0,0 +1,63 @@
export class ContextManager {
constructor() {
this.keepToolOutputTurns = 3;
this.tokenLimit = 12000;
}
estimateTokens(text) {
return Math.ceil((text || '').length / 3.5);
}
buildMessages(systemPrompt, history, maxTokens) {
const limit = maxTokens || this.tokenLimit;
const systemTokens = this.estimateTokens(systemPrompt);
let availableTokens = limit - systemTokens - 1000;
if (availableTokens < 0) availableTokens = 1000;
const optimizedHistory = this.optimizeToolOutputs(history);
const finalMessages = [];
let currentTokens = 0;
for (let i = optimizedHistory.length - 1; i >= 0; i--) {
const msg = optimizedHistory[i];
const msgTokens = this.estimateTokens(msg.content);
if (currentTokens + msgTokens > availableTokens) {
finalMessages.unshift({ role: 'system', content: "[Earlier history truncated to save tokens]" });
break;
}
finalMessages.unshift(msg);
currentTokens += msgTokens;
}
return [
{ role: 'system', content: systemPrompt },
...finalMessages
];
}
optimizeToolOutputs(history) {
let toolOutputCount = 0;
const reversedHistory = [...history].reverse();
const processedReversed = reversedHistory.map((msg) => {
if (msg.role === 'user' && msg.content.startsWith('[Tool Result')) {
toolOutputCount++;
if (toolOutputCount > this.keepToolOutputTurns) {
const firstLine = msg.content.split('\n')[0];
return {
role: msg.role,
content: `${firstLine}\n[Content hidden to save tokens. The tool was executed successfully.]`
};
}
}
return msg;
});
return processedReversed.reverse();
}
}

View File

@@ -0,0 +1,248 @@
import { amilyHelper } from "../tavern-helper/main.js";
import * as charApi from "./char-api.js";
export const tools = {
read_world_info: async ({ book_name }) => {
const entries = await amilyHelper.getLorebookEntries(book_name);
return JSON.stringify(entries, null, 2);
},
write_world_info_entry: async ({ book_name, entries }) => {
if (typeof entries === 'string') {
try {
const cleanEntries = entries.replace(/```json/g, '').replace(/```/g, '').trim();
entries = JSON.parse(cleanEntries);
} catch (e) {
return `错误: 'entries' 参数必须是有效的 JSON 数组。解析错误: ${e.message}`;
}
}
if (!Array.isArray(entries)) {
if (typeof entries === 'object' && entries !== null) {
entries = [entries];
} else {
return "错误: 'entries' 参数必须是数组或对象。";
}
}
const updates = [];
const creates = [];
for (const entry of entries) {
if (entry.uid !== undefined) {
updates.push(entry);
} else {
creates.push(entry);
}
}
let resultMsg = "";
if (updates.length > 0) {
const success = await amilyHelper.setLorebookEntries(book_name, updates);
resultMsg += success ? `成功更新了 ${updates.length} 个条目。 ` : `更新条目失败。 `;
}
if (creates.length > 0) {
const success = await amilyHelper.createLorebookEntries(book_name, creates);
resultMsg += success ? `成功创建了 ${creates.length} 个条目。 ` : `创建条目失败。 `;
}
return resultMsg || "未执行任何操作。";
},
create_world_book: async ({ book_name }) => {
const success = await amilyHelper.createLorebook(book_name);
return success ? `世界书 "${book_name}" 创建成功。` : `创建世界书 "${book_name}" 失败。`;
},
read_character_card: async ({ chid }) => {
const char = charApi.getCharacter(chid);
if (!char) return "未找到角色。";
const safeChar = {
name: char.name,
description: char.description,
personality: char.personality,
scenario: char.scenario,
first_mes: char.first_mes,
mes_example: char.mes_example,
alternate_greetings: char.data?.alternate_greetings || []
};
return JSON.stringify(safeChar, null, 2);
},
update_character_card: async (args) => {
const { chid, ...updates } = args;
const finalUpdates = args.updates || updates;
const success = charApi.updateCharacter(chid, finalUpdates);
return success ? "角色卡更新成功。" : "更新角色卡失败。";
},
edit_character_text: async ({ chid, field, search, replace }) => {
const char = charApi.getCharacter(chid);
if (!char) return "未找到角色。";
const allowedFields = ['description', 'personality', 'scenario', 'first_mes', 'mes_example'];
if (!allowedFields.includes(field)) {
return `无效的字段。允许的字段: ${allowedFields.join(', ')}`;
}
const originalText = char[field] || '';
if (!originalText.includes(search)) {
return `在字段 '${field}' 中未找到搜索文本。`;
}
const newText = originalText.replace(search, replace);
const success = charApi.updateCharacter(chid, { [field]: newText });
return success ? `字段 '${field}' 更新成功。` : `更新字段 '${field}' 失败。`;
},
manage_first_message: async ({ action, chid, index, message }) => {
let success = false;
switch (action) {
case 'add':
success = charApi.addFirstMessage(chid, message);
break;
case 'update':
success = charApi.updateFirstMessage(chid, index, message);
break;
case 'remove':
success = charApi.removeFirstMessage(chid, index);
break;
default:
return "无效的操作。";
}
return success ? `开场白 ${action} 成功。` : `开场白 ${action} 失败。`;
},
create_character: async ({ name }) => {
const result = await charApi.createNewCharacter(name);
if (result === -1) return "创建角色失败。";
if (result === -2) return "角色创建请求已发送。请手动刷新角色列表以查看新角色。";
return `角色创建成功ID: ${result}`;
}
};
export function getToolDefinitions() {
return [
{
name: "read_world_info",
description: "Read all entries from a specific world book.",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "The name of the world book." }
},
required: ["book_name"]
}
},
{
name: "write_world_info_entry",
description: "Create or update entries in a world book.",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "The name of the world book." },
entries: {
type: "array",
items: {
type: "object",
properties: {
uid: { type: "number", description: "Entry ID (optional, for update)." },
comment: { type: "string", description: "Entry title/comment." },
content: { type: "string", description: "Entry content." },
key: { type: "array", items: { type: "string" }, description: "Keywords." },
enabled: { type: "boolean", description: "Is enabled." },
constant: { type: "boolean", description: "Constant (Blue light)." },
position: { type: "string", enum: ["before_character_definition", "after_character_definition", "before_author_note", "after_author_note", "at_depth_as_system"], description: "Insertion position." },
depth: { type: "number", description: "Insertion depth." },
scanDepth: { type: "number", description: "Scan depth." },
exclude_recursion: { type: "boolean", description: "Exclude from recursion." },
prevent_recursion: { type: "boolean", description: "Prevent recursion." }
}
}
}
},
required: ["book_name", "entries"]
}
},
{
name: "create_world_book",
description: "Create a new empty world book.",
parameters: {
type: "object",
properties: {
book_name: { type: "string", description: "The name of the new world book." }
},
required: ["book_name"]
}
},
{
name: "read_character_card",
description: "Read character card data.",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "Character ID." }
},
required: ["chid"]
}
},
{
name: "update_character_card",
description: "Update character card fields (overwrite).",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "Character ID." },
name: { type: "string" },
description: { type: "string" },
personality: { type: "string" },
scenario: { type: "string" },
first_mes: { type: "string" },
mes_example: { type: "string" }
},
required: ["chid"]
}
},
{
name: "edit_character_text",
description: "Edit a specific text field of a character using search and replace.",
parameters: {
type: "object",
properties: {
chid: { type: "number", description: "Character ID." },
field: { type: "string", enum: ["description", "personality", "scenario", "first_mes", "mes_example"], description: "The field to edit." },
search: { type: "string", description: "The exact text to find." },
replace: { type: "string", description: "The text to replace with." }
},
required: ["chid", "field", "search", "replace"]
}
},
{
name: "manage_first_message",
description: "Add, update, or remove alternate greetings.",
parameters: {
type: "object",
properties: {
action: { type: "string", enum: ["add", "update", "remove"] },
chid: { type: "number", description: "Character ID." },
index: { type: "number", description: "Index of the greeting (required for update/remove)." },
message: { type: "string", description: "Content of the greeting (required for add/update)." }
},
required: ["action", "chid"]
}
},
{
name: "create_character",
description: "Create a new character card.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Name of the new character." }
},
required: ["name"]
}
}
];
}

View File

@@ -0,0 +1,431 @@
import { extensionName } from "../../utils/settings.js";
import { AgentManager } from "./agent-manager.js";
import { characters, this_chid, saveSettingsDebounced } from "/script.js";
import { world_names } from "/scripts/world-info.js";
import { getApiConfig, setApiConfig, testConnection, fetchModels } from "./api.js";
import { tools } from "./tools.js";
const extensionFolderPath = `scripts/extensions/third-party/${extensionName}`;
let isInitialized = false;
let agentManager = null;
let previousCharData = {};
let previousWorldData = {};
export async function openAutoCharCardWindow() {
toastr.info("该功能正在开发,尚未完成,请耐心等待。");
return;
if ($('#acc-window').length > 0) {
$('#acc-window').show();
return;
}
if (!$('#acc-style').length) {
$('<link>')
.attr('id', 'acc-style')
.attr('rel', 'stylesheet')
.attr('type', 'text/css')
.attr('href', `${extensionFolderPath}/assets/auto-char-card/style.css`)
.appendTo('head');
}
try {
const htmlContent = await $.get(`${extensionFolderPath}/assets/auto-char-card/index.html`);
$('body').append(htmlContent);
bindEvents();
agentManager = new AgentManager();
try {
populateDropdowns();
loadApiSettings();
} catch (dataError) {
console.error('[Amily2 AutoCharCard] Failed to load data:', dataError);
toastr.warning('数据加载部分失败,请检查控制台。');
}
isInitialized = true;
console.log('[Amily2 AutoCharCard] Window initialized.');
} catch (error) {
console.error('[Amily2 AutoCharCard] Failed to initialize window:', error);
toastr.error(`无法加载自动构建器界面: ${error.message}`);
$('#acc-window').remove();
}
}
function populateDropdowns() {
const charSelect = $('#acc-target-char');
charSelect.empty().append('<option value="">-- 请选择 --</option>');
charSelect.append('<option value="new">新建角色卡</option>');
characters.forEach((char, index) => {
if (char) {
const option = $('<option>').val(index).text(char.name);
if (index === this_chid) option.prop('selected', true);
charSelect.append(option);
}
});
const worldSelect = $('#acc-target-world');
worldSelect.empty().append('<option value="">-- 请选择 --</option>');
worldSelect.append('<option value="new">新建世界书</option>');
world_names.forEach(name => {
worldSelect.append($('<option>').val(name).text(name));
});
}
function loadApiSettings() {
const executorConfig = getApiConfig('executor');
$('#acc-executor-url').val(executorConfig.apiUrl);
$('#acc-executor-key').val(executorConfig.apiKey);
const executorModelSelect = $('#acc-executor-model');
if (executorConfig.model) {
if (executorModelSelect.find(`option[value="${executorConfig.model}"]`).length === 0) {
executorModelSelect.append(new Option(executorConfig.model, executorConfig.model));
}
executorModelSelect.val(executorConfig.model);
}
const reviewerConfig = getApiConfig('reviewer');
$('#acc-reviewer-url').val(reviewerConfig.apiUrl);
$('#acc-reviewer-key').val(reviewerConfig.apiKey);
const reviewerModelSelect = $('#acc-reviewer-model');
if (reviewerConfig.model) {
if (reviewerModelSelect.find(`option[value="${reviewerConfig.model}"]`).length === 0) {
reviewerModelSelect.append(new Option(reviewerConfig.model, reviewerConfig.model));
}
reviewerModelSelect.val(reviewerConfig.model);
}
}
function bindEvents() {
const windowEl = $('#acc-window');
const minIcon = $('#acc-minimized-icon');
$('#acc-close-btn').on('click', () => {
if (confirm('确定要关闭自动构建器吗?当前任务可能会丢失。')) {
windowEl.remove();
minIcon.hide();
isInitialized = false;
agentManager = null;
}
});
$('#acc-minimize-btn').on('click', () => {
windowEl.hide();
minIcon.show();
});
minIcon.on('click', () => {
minIcon.hide();
windowEl.show();
minIcon.find('.acc-notification-dot').hide();
});
$('#acc-send-btn').on('click', handleSendMessage);
$('#acc-user-input').on('keypress', (e) => {
if (e.which === 13 && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
});
$('.acc-preview-tabs .acc-tab-btn').on('click', function() {
$('.acc-preview-tabs .acc-tab-btn').removeClass('active');
$(this).addClass('active');
const tab = $(this).data('tab');
console.log('Switch preview tab to:', tab);
});
$('#acc-api-settings-toggle').on('click', function() {
const content = $('#acc-api-settings-content');
const icon = $(this).find('.fa-chevron-down, .fa-chevron-up');
if (content.is(':visible')) {
content.slideUp();
icon.removeClass('fa-chevron-up').addClass('fa-chevron-down');
} else {
content.slideDown();
icon.removeClass('fa-chevron-down').addClass('fa-chevron-up');
}
});
$('#acc-api-settings-content .acc-tab-btn').on('click', function() {
const target = $(this).data('target');
$('#acc-api-settings-content .acc-tab-btn').removeClass('active');
$(this).addClass('active');
$('.acc-api-group').hide();
$(`#acc-api-${target}`).show();
});
$('#acc-save-api').on('click', () => {
const executorConfig = {
apiUrl: $('#acc-executor-url').val().trim(),
apiKey: $('#acc-executor-key').val().trim(),
model: $('#acc-executor-model').val() || ''
};
const reviewerConfig = {
apiUrl: $('#acc-reviewer-url').val().trim(),
apiKey: $('#acc-reviewer-key').val().trim(),
model: $('#acc-reviewer-model').val() || ''
};
setApiConfig('executor', executorConfig);
setApiConfig('reviewer', reviewerConfig);
saveSettingsDebounced();
toastr.success('API 配置已保存');
});
const handleRefreshModels = async (role) => {
const urlInput = $(`#acc-${role}-url`);
const keyInput = $(`#acc-${role}-key`);
const select = $(`#acc-${role}-model`);
const btn = $(`#acc-${role}-refresh-models`);
const apiUrl = urlInput.val().trim();
const apiKey = keyInput.val().trim();
if (!apiUrl) {
toastr.warning('请先输入 API URL');
return;
}
const originalIcon = btn.html();
btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>');
select.empty().append('<option value="">加载中...</option>');
try {
const models = await fetchModels(apiUrl, apiKey);
select.empty().append('<option value="">-- 请选择模型 --</option>');
if (models.length === 0) {
select.append('<option value="" disabled>未找到模型</option>');
} else {
models.forEach(model => {
select.append(new Option(model, model));
});
toastr.success(`成功获取 ${models.length} 个模型`);
}
} catch (error) {
console.error(`[AutoCharCard] Failed to fetch models for ${role}:`, error);
toastr.error(`获取模型失败: ${error.message}`);
select.empty().append('<option value="">获取失败</option>');
} finally {
btn.prop('disabled', false).html(originalIcon);
}
};
$('#acc-executor-refresh-models').on('click', () => handleRefreshModels('executor'));
$('#acc-reviewer-refresh-models').on('click', () => handleRefreshModels('reviewer'));
$('#acc-executor-test').on('click', async function() {
const btn = $(this);
btn.prop('disabled', true).text('测试中...');
const success = await testConnection('executor');
btn.prop('disabled', false).text('测试连接');
if (success) toastr.success('模型 A 连接成功');
else toastr.error('模型 A 连接失败');
});
$('#acc-reviewer-test').on('click', async function() {
const btn = $(this);
btn.prop('disabled', true).text('测试中...');
const success = await testConnection('reviewer');
btn.prop('disabled', false).text('测试连接');
if (success) toastr.success('模型 B 连接成功');
else toastr.error('模型 B 连接失败');
});
}
async function handleSendMessage() {
const input = $('#acc-user-input');
const message = input.val().trim();
if (!message) return;
if (!agentManager) {
toastr.error('Agent 未初始化');
return;
}
const selectedCharId = $('#acc-target-char').val();
const selectedWorld = $('#acc-target-world').val();
if (!selectedCharId && selectedCharId !== '0') {
toastr.warning('请先选择一个目标角色(或选择新建)');
return;
}
addMessage('user', message);
input.val('');
$('#acc-send-btn').prop('disabled', true);
$('#acc-status-indicator').removeClass('status-idle').addClass('status-working').text('工作中...');
try {
agentManager.setContext(selectedCharId, selectedWorld);
await agentManager.handleUserMessage(
message,
(content, role) => {
addMessage(role, content);
},
(toolName, args) => {
updatePreview(toolName, args);
}
);
} catch (error) {
console.error('Agent Error:', error);
addMessage('system', `发生错误: ${error.message}`);
} finally {
$('#acc-send-btn').prop('disabled', false);
$('#acc-status-indicator').removeClass('status-working').addClass('status-idle').text('空闲');
}
}
function addMessage(role, content) {
const stream = $('#acc-chat-stream');
let displayContent = content;
if (role === 'executor') {
const tools = [
'read_world_info', 'write_world_info_entry', 'create_world_book',
'read_character_card', 'update_character_card', 'edit_character_text',
'manage_first_message', 'use_tool'
];
const regex = new RegExp(`<(${tools.join('|')})>[\\s\\S]*?<\\/\\1>`, 'g');
displayContent = content.replace(regex, '').trim();
if (!displayContent) {
displayContent = "<i>(正在执行操作...)</i>";
}
}
const escapedContent = displayContent
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
const formattedContent = escapedContent.replace(/\n/g, '<br>');
const msgDiv = $('<div>').addClass(`acc-message ${role}`);
const avatarDiv = $('<div>').addClass('acc-avatar');
if (role === 'user') {
avatarDiv.html('<i class="fas fa-user"></i>');
} else if (role === 'assistant') {
avatarDiv.html('<i class="fas fa-brain" style="color: #ff9800;"></i>');
} else if (role === 'executor') {
avatarDiv.html('<i class="fas fa-robot" style="color: #4caf50;"></i>');
} else if (role === 'system') {
avatarDiv.html('<i class="fas fa-info-circle"></i>');
}
const contentDiv = $('<div>').addClass('acc-message-content');
msgDiv.append(avatarDiv);
msgDiv.append(contentDiv);
stream.append(msgDiv);
if (role === 'assistant') {
let i = 0;
const speed = 2;
const chunkSize = 5;
function typeWriter() {
if (i < formattedContent.length) {
let chunk = "";
let count = 0;
while (count < chunkSize && i < formattedContent.length) {
if (formattedContent.charAt(i) === '<') {
const tagEnd = formattedContent.indexOf('>', i);
if (tagEnd !== -1) {
chunk += formattedContent.substring(i, tagEnd + 1);
i = tagEnd + 1;
} else {
chunk += formattedContent.charAt(i);
i++;
}
} else {
chunk += formattedContent.charAt(i);
i++;
}
count++;
}
contentDiv.html(contentDiv.html() + chunk);
stream.scrollTop(stream[0].scrollHeight);
setTimeout(typeWriter, speed);
}
}
typeWriter();
} else {
contentDiv.html(formattedContent);
stream.scrollTop(stream[0].scrollHeight);
}
}
async function updatePreview(toolName, args) {
const container = $('#acc-preview-container');
if (toolName === 'update_character_card' || toolName === 'edit_character_text') {
const chid = args.chid !== undefined ? args.chid : $('#acc-target-char').val();
if (chid !== undefined) {
const charData = await tools.read_character_card({ chid });
const char = JSON.parse(charData);
let html = `<h3>角色预览: ${char.name}</h3>`;
const fields = ['description', 'personality', 'first_mes', 'scenario'];
fields.forEach(field => {
const oldVal = previousCharData[field] || '';
const newVal = char[field] || '';
let contentHtml = newVal;
if (oldVal !== newVal) {
contentHtml = `<div class="diff-added">${newVal}</div>`;
if (oldVal) {
contentHtml += `<div class="diff-removed" style="display:none;">${oldVal}</div>`;
}
}
html += `<div class="acc-preview-item"><strong>${field}:</strong><pre>${contentHtml}</pre></div>`;
});
container.html(html);
previousCharData = char;
}
} else if (toolName === 'write_world_info_entry') {
const bookName = args.book_name || $('#acc-target-world').val();
if (bookName) {
const entriesData = await tools.read_world_info({ book_name: bookName });
const entries = JSON.parse(entriesData);
let html = `<h3>世界书预览: ${bookName}</h3>`;
entries.forEach(entry => {
let isModified = false;
if (args.entries) {
const modifiedEntries = Array.isArray(args.entries) ? args.entries : [args.entries];
isModified = modifiedEntries.some(e => e.key === entry.key || (Array.isArray(entry.keys) && entry.keys.includes(e.key)));
}
const contentClass = isModified ? 'diff-added' : '';
html += `<div class="acc-preview-item ${contentClass}">
<strong>Key:</strong> ${Array.isArray(entry.keys) ? entry.keys.join(', ') : entry.key}<br>
<strong>Content:</strong><pre>${entry.content}</pre>
</div>`;
});
container.html(html);
}
}
}

229
core/fractal-memory.js Normal file
View File

@@ -0,0 +1,229 @@
import { getContext, extension_settings } from "/scripts/extensions.js";
import { setExtensionPrompt, eventSource, event_types } from "/script.js";
import { callAI } from "./api.js";
import { callNgmsAI } from "./api/Ngms_api.js";
import { extensionName } from "../utils/settings.js";
import { getMemoryState, updateRow, insertRow, deleteRow, clearAllTables } from "./table-system/manager.js";
const FRACTAL_INJECTION_KEY = 'HANLINYUAN_FRACTAL_MEMORY';
const BUFFER_SIZE = 5;
const UPDATE_INTERVAL = 5;
export async function initializeFractalMemory() {
eventSource.on(event_types.MESSAGE_RECEIVED, handleMessageReceived);
console.log('[分形记忆] 系统已启动,正在构建多维记忆...');
}
let messageCounter = 0;
async function handleMessageReceived() {
messageCounter++;
if (messageCounter >= UPDATE_INTERVAL) {
messageCounter = 0;
await updateSceneLayer();
}
}
async function updateSceneLayer() {
const context = getContext();
const settings = extension_settings[extensionName];
if (!settings.fractalMemory) {
settings.fractalMemory = {
saga: "故事刚刚开始...",
arc: [],
scene: []
};
}
const memory = settings.fractalMemory;
console.log('[分形记忆] 正在提取近期事态...');
const recentChat = context.chat.slice(-UPDATE_INTERVAL).map(m => `${m.name}: ${m.mes}`).join('\n');
const prompt = `
请将以下对话总结为一句话的“场景事件”,描述发生了什么。
要求:简洁、客观、包含关键动作。
【对话内容】
${recentChat}
【输出】
(仅输出一句话总结)
`;
const newEvent = await _callLLM(prompt);
if (!newEvent) return;
console.log(`[分形记忆] 新增场景事件: ${newEvent}`);
memory.scene.push(newEvent);
if (memory.scene.length >= BUFFER_SIZE) {
await compressSceneToArc();
}
context.saveSettingsDebounced();
injectFractalMemory();
syncToTables();
}
async function compressSceneToArc() {
const context = getContext();
const settings = extension_settings[extensionName];
const memory = settings.fractalMemory;
console.log('[分形记忆] 场景层已满,正在压缩至篇章层...');
const sceneEvents = memory.scene.join('\n');
const prompt = `
请将以下 5 个连续的“场景事件”合并总结为一条“篇章节点”。
这条节点应该概括这一系列事件对剧情的推动作用。
【场景事件列表】
${sceneEvents}
【输出】
(仅输出一句话总结)
`;
const newArcEvent = await _callLLM(prompt);
if (!newArcEvent) return;
console.log(`[分形记忆] 新增篇章节点: ${newArcEvent}`);
memory.arc.push(newArcEvent);
memory.scene = [];
if (memory.arc.length >= BUFFER_SIZE) {
await compressArcToSaga();
}
}
async function compressArcToSaga() {
const context = getContext();
const settings = extension_settings[extensionName];
const memory = settings.fractalMemory;
console.log('[分形记忆] 篇章层已满,正在重写宏观史诗...');
const arcEvents = memory.arc.join('\n');
const oldSaga = memory.saga;
const prompt = `
请根据“旧的宏观史诗”和新发生的“篇章事件”,重写并更新整个故事的“宏观史诗”。
宏观史诗应该是一个高度概括的段落,描述故事的起因、经过和当前状态。
【旧史诗】
${oldSaga}
【新篇章事件】
${arcEvents}
【输出】
(输出一段更新后的宏观史诗,约 100-200 字)
`;
const newSaga = await _callLLM(prompt);
if (!newSaga) return;
console.log(`[分形记忆] 宏观史诗已更新。`);
memory.saga = newSaga;
memory.arc = [];
}
function syncToTables() {
const settings = extension_settings[extensionName];
if (!settings || !settings.fractalMemory) return;
const memory = settings.fractalMemory;
const tables = getMemoryState();
if (!tables) return;
const targetTableName = '【系统】分形记忆';
const tableIndex = tables.findIndex(t => t.name === targetTableName);
if (tableIndex !== -1) {
const table = tables[tableIndex];
const targetRows = [];
targetRows.push({
0: '宏观史诗',
1: memory.saga
});
memory.arc.forEach((event, i) => {
targetRows.push({
0: `篇章-${i+1}`,
1: event
});
});
memory.scene.forEach((event, i) => {
targetRows.push({
0: `场景-${i+1}`,
1: event
});
});
while (table.rows.length > targetRows.length) {
deleteRow(tableIndex, table.rows.length - 1);
}
targetRows.forEach((rowData, i) => {
if (i < table.rows.length) {
updateRow(tableIndex, i, rowData);
} else {
insertRow(tableIndex, rowData);
}
});
}
}
export function injectFractalMemory() {
const settings = extension_settings[extensionName];
if (!settings || !settings.fractalMemory) return;
const memory = settings.fractalMemory;
let content = `【分形记忆系统】\n`;
content += `[宏观史诗]\n${memory.saga}\n\n`;
if (memory.arc.length > 0) {
content += `[当前篇章]\n${memory.arc.map(e => `- ${e}`).join('\n')}\n\n`;
}
if (memory.scene.length > 0) {
content += `[近期事态]\n${memory.scene.map(e => `- ${e}`).join('\n')}`;
}
setExtensionPrompt(
FRACTAL_INJECTION_KEY,
content,
0,
4,
false,
0
);
}
async function _callLLM(prompt) {
const settings = extension_settings[extensionName];
const messages = [{ role: 'user', content: prompt }];
try {
let responseText = '';
if (settings.ngmsEnabled) {
responseText = await callNgmsAI(messages);
} else {
responseText = await callAI(messages);
}
return responseText.trim();
} catch (error) {
console.error('[分形记忆] AI 调用失败:', error);
return null;
}
}

View File

@@ -577,7 +577,7 @@ export async function executeRefinement(worldbook, loreKey) {
}
break;
case 'coreContent':
messages.push({ role: "user", content: `请将以下多个零散的"详细总结记录"提炼并融合成一段连贯的章节历史。原文如下:\n\n${contentToRefine}` });
messages.push({ role: "user", content: `<核心处理内容>\n\n${contentToRefine}\n\n</核心处理内容>` });
break;
}
}

View File

@@ -281,11 +281,16 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) {
liveSettings.selectedWorldbooks = [];
if (liveSettings.worldbookSource === 'manual') {
panel.find('#amily2_opt_worldbook_checkbox_list input[type="checkbox"]:checked').each(function() {
panel.find('#amily2_opt_worldbook_checkbox_list input[type="checkbox"]:not(.amily2_opt_wb_auto_check):checked').each(function() {
liveSettings.selectedWorldbooks.push($(this).val());
});
}
liveSettings.autoSelectWorldbooks = [];
panel.find('#amily2_opt_worldbook_checkbox_list input.amily2_opt_wb_auto_check:checked').each(function() {
liveSettings.autoSelectWorldbooks.push($(this).data('book'));
});
liveSettings.worldbookCharLimit = parseInt(panel.find('#amily2_opt_worldbook_char_limit').val(), 10) || 60000;
let enabledEntries = {};
@@ -311,6 +316,7 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) {
worldbookEnabled: apiSettings.plotOpt_worldbook_enabled,
worldbookSource: apiSettings.plotOpt_worldbook_source || 'character', // Default to 'character'
selectedWorldbooks: apiSettings.plotOpt_worldbook_selected_worldbooks,
autoSelectWorldbooks: apiSettings.plotOpt_autoSelectWorldbooks || [],
worldbookCharLimit: apiSettings.plotOpt_worldbook_char_limit,
enabledWorldbookEntries: apiSettings.plotOpt_worldbook_selected_entries,
};
@@ -351,11 +357,23 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) {
if (allEntries.length === 0) return '';
const enabledEntriesMap = liveSettings.enabledWorldbookEntries || {};
const autoSelectedBooks = liveSettings.autoSelectWorldbooks || [];
const userEnabledEntries = allEntries.filter(entry => {
if (!entry.enabled) return false;
// 检查是否在UI中被勾选或被自动全选
const isAuto = autoSelectedBooks.includes(entry.bookName);
const bookConfig = enabledEntriesMap[entry.bookName];
// 同时检查数字和字符串类型的UID以兼容从实时UI数字和已保存设置可能为字符串中读取的配置
return bookConfig ? (bookConfig.includes(entry.uid) || bookConfig.includes(String(entry.uid))) : false;
const isChecked = isAuto || (bookConfig ? (bookConfig.includes(entry.uid) || bookConfig.includes(String(entry.uid))) : false);
if (isChecked) {
// 勾选状态下必读 (强制设为 Constant)
entry.constant = true;
}
// 不勾选则依靠蓝绿灯 (保持原样,不返回 false)
return true;
});
if (userEnabledEntries.length === 0) return '';
@@ -399,7 +417,11 @@ export async function getPlotOptimizedWorldbookContent(context, apiSettings) {
pendingGreenLights = nextPendingGreenLights;
}
const finalContent = Array.from(triggeredEntries).map(entry => entry.content).filter(Boolean);
const finalContent = Array.from(triggeredEntries).map(entry => {
const keys = [...new Set([...(entry.key || []), ...(entry.keys || [])])].filter(Boolean).join('、');
const displayName = entry.comment || `Entry ${entry.uid}`;
return `【世界书条目:${displayName}。绿灯触发关键词:${keys}\n内容:${entry.content}`;
}).filter(Boolean);
if (finalContent.length === 0) return '';
const combinedContent = finalContent.join('\n\n---\n\n');

File diff suppressed because one or more lines are too long

View File

@@ -1,87 +1,98 @@
'use strict';
export const defaultSettings = {
retrieval: {
enabled: false,
apiEndpoint: 'openai',
customApiUrl: 'https://api.siliconflow.cn/v1',
apiKey: '',
embeddingModel: 'text-embedding-3-small',
notify: true,
batchSize: 50,
independentChatMemoryEnabled: false,
},
advanced: {
chunkSize: 768,
overlap: 50,
matchThreshold: 0.5,
queryMessageCount: 2,
maxResults: 10,
},
injection_novel: {
template: '以下内容是翰林院向量化后注入的原著小说剧情,但可能顺序会有些错乱,已经对前后做出了标识,请自行判断顺序:\n\n{{novel_text}}\n\n【以上内容是小说的原著剧情切莫以此作为剧情进展只是作为剧情的关联】',
position: 1,
depth: 2,
depth_role: 0,
},
injection_chat: {
template: '以下内容是翰林院向量化后注入的聊天对话记录,但可能顺序会有些错乱,已经对前后做出了标识,请自行判断顺序:\n\n{{chat_text}}\n\n【以上内容是对话的楼层记录切莫以此作为剧情进展只是作为相关提示】',
position: 1,
depth: 2,
depth_role: 0,
},
injection_lorebook: {
template: '以下内容是翰林院向量化后注入的世界书的条目内容(可能内含对话记录的总结),顺序可能会有些错乱,但已经对前后做出了标识,请自行判断顺序:\n\n{{lorebook_text}}\n\n【以上内容是从世界书中向量化后的内容切莫以此作为剧情进展只是作为已发生过的事情提醒】',
position: 1,
depth: 2,
depth_role: 0,
},
injection_manual: {
template: '以下内容是翰林院向量化后用户手动注入的内容,可能顺序会有些错乱,但已经对前后做出了标识,请自行判断顺序:\n\n{{manual_text}}\n\n【以上内容为用户手动向量化注入的内容切莫以此作为剧情进展只是作为相关提示】',
position: 1,
depth: 2,
depth_role: 0,
},
condensation: {
enabled: true,
layerStart: 1,
layerEnd: 10,
messageTypes: { user: true, ai: true, hidden: false },
tagExtractionEnabled: false,
tags: '摘要',
exclusionRules: [],
},
rerank: {
enabled: false,
url: 'https://api.siliconflow.cn/v1',
apiKey: '',
model: 'Pro/BAAI/bge-reranker-v2-m3',
top_n: 5,
hybrid_alpha: 0.7,
notify: true,
superSortEnabled: false,
priorityRetrieval: {
enabled: false,
sources: {
novel: {
enabled: false,
count: 5
},
chat_history: {
enabled: false,
count: 5
},
lorebook: {
enabled: false,
count: 5
},
manual: {
enabled: false,
count: 5
}
}
},
},
knowledgeBases: {},
};
'use strict';
export const defaultSettings = {
retrieval: {
enabled: false,
apiEndpoint: 'openai',
customApiUrl: 'https://api.siliconflow.cn/v1',
apiKey: '',
embeddingModel: 'text-embedding-3-small',
notify: true,
batchSize: 50,
independentChatMemoryEnabled: false,
},
advanced: {
chunkSize: 768,
overlap: 50,
matchThreshold: 0.5,
queryMessageCount: 2,
maxResults: 10,
},
injection_novel: {
template: '以下内容是翰林院向量化后注入的原著小说剧情,但可能顺序会有些错乱,已经对前后做出了标识,请自行判断顺序:\n\n{{novel_text}}\n\n【以上内容是小说的原著剧情切莫以此作为剧情进展只是作为剧情的关联】',
position: 1,
depth: 2,
depth_role: 0,
},
injection_chat: {
template: '以下内容是翰林院向量化后注入的聊天对话记录,但可能顺序会有些错乱,已经对前后做出了标识,请自行判断顺序:\n\n{{chat_text}}\n\n【以上内容是对话的楼层记录切莫以此作为剧情进展只是作为相关提示】',
position: 1,
depth: 2,
depth_role: 0,
},
injection_lorebook: {
template: '以下内容是翰林院向量化后注入的世界书的条目内容(可能内含对话记录的总结),顺序可能会有些错乱,但已经对前后做出了标识,请自行判断顺序:\n\n{{lorebook_text}}\n\n【以上内容是从世界书中向量化后的内容切莫以此作为剧情进展只是作为已发生过的事情提醒】',
position: 1,
depth: 2,
depth_role: 0,
},
injection_manual: {
template: '以下内容是翰林院向量化后用户手动注入的内容,可能顺序会有些错乱,但已经对前后做出了标识,请自行判断顺序:\n\n{{manual_text}}\n\n【以上内容为用户手动向量化注入的内容切莫以此作为剧情进展只是作为相关提示】',
position: 1,
depth: 2,
depth_role: 0,
},
condensation: {
enabled: true,
autoCondense: false,
preserveFloors: 10,
layerStart: 1,
layerEnd: 10,
messageTypes: { user: true, ai: true, hidden: false },
tagExtractionEnabled: false,
tags: '摘要',
exclusionRules: [],
},
archive: {
enabled: false,
threshold: 20,
batchSize: 10,
targetTable: '总结表'
},
relationshipGraph: {
enabled: false,
},
rerank: {
enabled: false,
url: 'https://api.siliconflow.cn/v1',
apiKey: '',
model: 'Pro/BAAI/bge-reranker-v2-m3',
top_n: 5,
hybrid_alpha: 0.7,
notify: true,
superSortEnabled: false,
priorityRetrieval: {
enabled: false,
sources: {
novel: {
enabled: false,
count: 5
},
chat_history: {
enabled: false,
count: 5
},
lorebook: {
enabled: false,
count: 5
},
manual: {
enabled: false,
count: 5
}
}
},
},
knowledgeBases: {},
};

View File

@@ -0,0 +1,70 @@
import { getGraph, getRelatedNodes } from "./manager.js";
export async function executeGraphRetrieval(queryText) {
if (!queryText) return '';
const graph = getGraph();
if (!graph.nodes || graph.nodes.length === 0) return '';
const foundNodes = graph.nodes.filter(node => {
return queryText.toLowerCase().includes(node.label.toLowerCase());
});
if (foundNodes.length === 0) return '';
console.log(`[关系图谱] 在查询中发现 ${foundNodes.length} 个实体: ${foundNodes.map(n => n.label).join(', ')}`);
const contextNodes = new Map();
for (const node of foundNodes) {
contextNodes.set(node.id, { node, reason: '直接匹配' });
const related = getRelatedNodes(node.id, 1);
for (const rel of related) {
if (!contextNodes.has(rel.node.id)) {
contextNodes.set(rel.node.id, {
node: rel.node,
reason: `关联至 ${node.label} (${rel.relation})`
});
}
}
}
let output = '';
const nodesArray = Array.from(contextNodes.values());
if (nodesArray.length > 0) {
output += '<GraphContext>\n';
output += '<!-- 以下信息源自关系图谱,基于上下文中的实体自动联想生成。 -->\n';
for (const item of nodesArray) {
const { node, reason } = item;
output += `[实体: ${node.label}]\n`;
output += ` - 来源: ${reason}\n`;
if (node.metadata && node.metadata.info) {
output += ` - 信息: ${node.metadata.info}\n`;
}
const edges = graph.edges.filter(e =>
(e.source === node.id && contextNodes.has(e.target)) ||
(e.target === node.id && contextNodes.has(e.source))
);
if (edges.length > 0) {
output += ` - 连接:\n`;
for (const edge of edges) {
const otherId = edge.source === node.id ? edge.target : edge.source;
const otherNode = contextNodes.get(otherId).node;
const direction = edge.source === node.id ? '->' : '<-';
output += ` * ${direction} ${otherNode.label} (${edge.relation})\n`;
}
}
output += '\n';
}
output += '</GraphContext>';
}
console.log(`[关系图谱] 生成了包含 ${nodesArray.length} 个节点的上下文。`);
return output;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,18 @@ import { extensionName } from "../../utils/settings.js";
import { extension_settings } from "/scripts/extensions.js";
import { saveSettingsDebounced } from "/script.js";
import { initializeSuperMemory, purgeSuperMemory } from "./manager.js";
import { defaultSettings as ragDefaultSettings } from "../rag-settings.js";
import { getMemoryState } from "../table-system/manager.js";
const RAG_MODULE_NAME = 'hanlinyuan-rag-core';
function getRagSettings() {
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
if (!extension_settings[extensionName][RAG_MODULE_NAME]) {
extension_settings[extensionName][RAG_MODULE_NAME] = structuredClone(ragDefaultSettings);
}
return extension_settings[extensionName][RAG_MODULE_NAME];
}
export function bindSuperMemoryEvents() {
const panel = $('#amily2_super_memory_panel');
@@ -17,20 +29,85 @@ export function bindSuperMemoryEvents() {
panel.find(`#sm-${tab}-tab`).addClass('active');
});
// 处理 Checkbox 变更
panel.on('change', 'input[type="checkbox"]', function() {
if ($(this).hasClass('sm-table-setting-check')) return; // Skip table settings checks here
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
const id = this.id;
let key = null;
if (id === 'sm-system-enabled') key = 'super_memory_enabled';
if (id === 'sm-bridge-enabled') key = 'superMemory_bridgeEnabled';
if (key) {
extension_settings[extensionName][key] = this.checked;
// Super Memory 自身设置
if (id === 'sm-system-enabled') {
extension_settings[extensionName]['super_memory_enabled'] = this.checked;
saveSettingsDebounced();
console.log(`[Amily2-SuperMemory] Setting updated: ${key} = ${this.checked}`);
return;
}
if (id === 'sm-bridge-enabled') {
extension_settings[extensionName]['superMemory_bridgeEnabled'] = this.checked;
saveSettingsDebounced();
return;
}
// RAG 设置 (归档 & 关联图谱)
const ragSettings = getRagSettings();
if (id === 'sm-archive-enabled') {
if (!ragSettings.archive) ragSettings.archive = {};
ragSettings.archive.enabled = this.checked;
}
else if (id === 'sm-relationship-graph-enabled') {
if (!ragSettings.relationshipGraph) ragSettings.relationshipGraph = {};
ragSettings.relationshipGraph.enabled = this.checked;
}
saveSettingsDebounced();
console.log(`[Amily2-SuperMemory] Checkbox updated: ${id} = ${this.checked}`);
});
// 处理 Input 变更 (归档阈值等)
panel.on('change', 'input[type="number"], input[type="text"]', function() {
const id = this.id;
const ragSettings = getRagSettings();
if (!ragSettings.archive) ragSettings.archive = {};
if (id === 'sm-archive-threshold') {
ragSettings.archive.threshold = parseInt(this.value, 10);
}
else if (id === 'sm-archive-batch-size') {
ragSettings.archive.batchSize = parseInt(this.value, 10);
}
else if (id === 'sm-archive-target-table') {
ragSettings.archive.targetTable = this.value;
}
saveSettingsDebounced();
console.log(`[Amily2-SuperMemory] Input updated: ${id} = ${this.value}`);
});
// 绑定刷新表格列表按钮
panel.on('click', '#sm-refresh-table-list', function() {
renderTableSettingsList();
});
// 绑定表格专属配置的 Checkbox
panel.on('change', '.sm-table-setting-check', function() {
if (!extension_settings[extensionName]) extension_settings[extensionName] = {};
if (!extension_settings[extensionName].superMemory_tableSettings) {
extension_settings[extensionName].superMemory_tableSettings = {};
}
const tableName = $(this).data('table');
const type = $(this).data('type'); // 'sync' or 'constant'
const checked = this.checked;
if (!extension_settings[extensionName].superMemory_tableSettings[tableName]) {
extension_settings[extensionName].superMemory_tableSettings[tableName] = {};
}
extension_settings[extensionName].superMemory_tableSettings[tableName][type] = checked;
saveSettingsDebounced();
console.log(`[Amily2-SuperMemory] Table setting updated: ${tableName}.${type} = ${checked}`);
});
loadSuperMemorySettings();
@@ -38,11 +115,76 @@ export function bindSuperMemoryEvents() {
console.log('[Amily2-SuperMemory] Events bound successfully.');
}
function renderTableSettingsList() {
const container = $('#sm-table-settings-list');
container.html('<div style="text-align: center; color: #888; padding: 20px;">正在加载...</div>');
const tables = getMemoryState();
if (!tables || tables.length === 0) {
container.html('<div style="text-align: center; color: #888; padding: 20px;">暂无表格数据。请先在聊天中使用表格功能。</div>');
return;
}
const settings = extension_settings[extensionName]?.superMemory_tableSettings || {};
let html = '';
tables.forEach(table => {
const tableName = table.name;
const tableConfig = settings[tableName] || {};
// Default values: Sync=True, Constant=True
const isSyncEnabled = tableConfig.sync !== false;
const isConstant = tableConfig.constant !== false;
html += `
<div class="sm-control-block" style="border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px; margin-bottom: 10px;">
<div style="font-weight: bold; margin-bottom: 5px; color: #e0e0e0;">${tableName}</div>
<div style="display: flex; justify-content: space-between;">
<div style="display: flex; align-items: center;">
<label class="sm-toggle-switch" style="transform: scale(0.8); margin-right: 5px;">
<input type="checkbox" class="sm-table-setting-check" data-table="${tableName}" data-type="sync" ${isSyncEnabled ? 'checked' : ''}>
<span class="sm-slider"></span>
</label>
<span style="font-size: 0.9em; color: #ccc;">写入世界书</span>
</div>
<div style="display: flex; align-items: center;">
<label class="sm-toggle-switch" style="transform: scale(0.8); margin-right: 5px;">
<input type="checkbox" class="sm-table-setting-check" data-table="${tableName}" data-type="constant" ${isConstant ? 'checked' : ''}>
<span class="sm-slider"></span>
</label>
<span style="font-size: 0.9em; color: #ccc;">索引绿灯(常驻)</span>
</div>
</div>
</div>
`;
});
container.html(html);
}
function loadSuperMemorySettings() {
const settings = extension_settings[extensionName] || {};
const ragSettings = getRagSettings();
// Super Memory 设置
$('#sm-system-enabled').prop('checked', settings.super_memory_enabled ?? false);
$('#sm-bridge-enabled').prop('checked', settings.superMemory_bridgeEnabled ?? false);
// 归档设置
if (ragSettings.archive) {
$('#sm-archive-enabled').prop('checked', ragSettings.archive.enabled ?? false);
$('#sm-archive-threshold').val(ragSettings.archive.threshold ?? 20);
$('#sm-archive-batch-size').val(ragSettings.archive.batchSize ?? 10);
$('#sm-archive-target-table').val(ragSettings.archive.targetTable ?? '总结表');
}
// 关联图谱设置
if (ragSettings.relationshipGraph) {
$('#sm-relationship-graph-enabled').prop('checked', ragSettings.relationshipGraph.enabled ?? false);
}
// 渲染表格列表
renderTableSettingsList();
}
window.sm_initializeSystem = async function() {

View File

@@ -64,12 +64,57 @@
</label>
</div>
</fieldset>
<fieldset class="sm-settings-group">
<legend><i class="fas fa-list-alt"></i> 表格专属配置</legend>
<div class="sm-control-block" style="display: block;">
<p style="font-size: 0.9em; color: #aaa; margin-bottom: 10px;">在此处配置特定表格的同步策略。</p>
<div id="sm-table-settings-list" style="max-height: 300px; overflow-y: auto; padding-right: 5px;">
<!-- Table items will be injected here -->
<div style="text-align: center; color: #888; padding: 20px;">正在加载表格列表...</div>
</div>
<button id="sm-refresh-table-list" class="sm-action-button secondary" style="width: 100%; margin-top: 10px;">刷新表格列表</button>
</div>
</fieldset>
<fieldset class="sm-settings-group">
<legend><i class="fas fa-archive"></i> 历史归档配置</legend>
<div class="sm-control-block">
<label>启用自动归档:</label>
<label class="sm-toggle-switch">
<input type="checkbox" id="sm-archive-enabled">
<span class="sm-slider"></span>
</label>
</div>
<div class="sm-control-block">
<label>触发阈值 (行数):</label>
<input type="number" id="sm-archive-threshold" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="20">
</div>
<div class="sm-control-block">
<label title="每次触发归档时,一次性迁移的行数。">归档批次 (行数):</label>
<input type="number" id="sm-archive-batch-size" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="10">
</div>
<small style="color: #888; font-size: 0.8em; display: block; margin-top: -5px; margin-bottom: 10px; padding-left: 5px;">
阈值是 20批次是 10。当表格达到 21 行时,会把最早的 10 行向量化,表格与世界书剩下 11 条。
</small>
<div class="sm-control-block">
<label>目标表格名称:</label>
<input type="text" id="sm-archive-target-table" style="background: rgba(0, 0, 0, 0.3); border: 1px solid #444; color: #e0e0e0; padding: 5px; border-radius: 4px;" value="总结表">
</div>
</fieldset>
</div>
<!-- Relation Tab -->
<div id="sm-relation-tab" class="sm-tab-pane">
<fieldset class="sm-settings-group">
<legend><i class="fas fa-project-diagram"></i> 关联网络 (The Mesh)</legend>
<div class="sm-control-block">
<label>启用角色关联图谱:</label>
<label class="sm-toggle-switch">
<input type="checkbox" id="sm-relationship-graph-enabled">
<span class="sm-slider"></span>
</label>
</div>
<p>关联触发逻辑正在开发中...</p>
</fieldset>
</div>

View File

@@ -17,8 +17,8 @@ export function getMemoryBookName() {
return `Amily2_Memory_${safeCharName}`;
}
export async function syncToLorebook(tableName, data, indexText, role, headers, rowStatuses, depth = 100) {
console.log(`[Amily2-Bridge] 开始同步表格: ${tableName} (Depth: ${depth})`);
export async function syncToLorebook(tableName, data, indexText, role, headers, rowStatuses, depth = 100, isIndexConstant = true) {
console.log(`[Amily2-Bridge] 开始同步表格: ${tableName} (Depth: ${depth}, IndexConstant: ${isIndexConstant})`);
await ensureMemoryBook();
@@ -30,23 +30,34 @@ export async function syncToLorebook(tableName, data, indexText, role, headers,
const entriesToUpdate = [];
const entriesToCreate = [];
const processEntry = (comment, keys, content, type = 'selective', enabled = true) => {
const processEntry = (comment, keys, content, type = 'selective', enabled = true, excludeRecursion = false, specificOrder = null, specificDepth = null) => {
const existingEntry = entries.find(e => e.comment === comment);
if (existingEntry) {
existingEntry.content = content;
existingEntry.key = keys;
// existingEntry.order = depth; // 【V153.0】不再覆盖用户的深度/排序设置
existingEntry.exclude_recursion = excludeRecursion;
existingEntry.prevent_recursion = excludeRecursion;
existingEntry.excludeRecursion = excludeRecursion;
existingEntry.preventRecursion = excludeRecursion;
if (specificOrder !== null) {
existingEntry.order = specificOrder;
existingEntry.position = 4;
}
if (specificDepth !== null) {
existingEntry.depth = specificDepth;
}
if (type === 'constant') {
existingEntry.constant = true;
} else {
existingEntry.constant = false;
}
if (existingEntry.enabled !== undefined) {
existingEntry.enabled = enabled;
} else {
existingEntry.disable = !enabled;
}
existingEntry.enabled = enabled;
delete existingEntry.disable;
delete existingEntry.disabled;
entriesToUpdate.push(existingEntry);
} else {
entriesToCreate.push({
@@ -54,15 +65,20 @@ export async function syncToLorebook(tableName, data, indexText, role, headers,
keys: keys,
content: content,
type: type,
position: 1,
order: depth,
enabled: enabled
position: 4,
order: specificOrder !== null ? specificOrder : depth,
depth: specificDepth !== null ? specificDepth : depth,
enabled: enabled,
exclude_recursion: excludeRecursion,
prevent_recursion: excludeRecursion,
excludeRecursion: excludeRecursion,
preventRecursion: excludeRecursion
});
}
};
if (role === 'database') {
const indexKey = [`Amily_Index_${tableName}`];
const indexKey = [`Amily_Index_${tableName}`,'索引'];
const indexComment = `[Amily2] Index for ${tableName}`;
let indexContent = `${tableName} 索引】\n${indexText.trim()}`;
@@ -70,14 +86,14 @@ export async function syncToLorebook(tableName, data, indexText, role, headers,
indexContent += `\n<system_note>\n【Amily2-前瞻性预测指令】\n为了实现“提前一轮触发绿灯世界书”的高级记忆回流,请在生成本轮回复时,根据当前剧情走向,**大胆预测并自然提及**下一轮可能出现的索引编码。可自行增加一个标签并使用<!--到-->html注释掉该编码。这将激活系统的预加载机制确保剧情发展的连贯性。\n</system_note>`;
}
processEntry(indexComment, indexKey, indexContent, 'constant');
const indexType = isIndexConstant ? 'constant' : 'selective';
processEntry(indexComment, indexKey, indexContent, indexType, true, true, 0, 0);
}
data.forEach((row, index) => {
if (!row || row.length === 0) return;
const rawVal = row[0];
// 【V152.0】修复Falsy检查漏洞 (支持数字0作为主键)
if (rawVal === undefined || rawVal === null) return;
const primaryVal = String(rawVal).trim();
@@ -128,7 +144,6 @@ export async function syncToLorebook(tableName, data, indexText, role, headers,
const activeKeys = new Set();
for(const row of data) {
// 【V152.0】修复Falsy检查漏洞 (支持数字0作为主键)
if(row && row.length > 0) {
const rVal = row[0];
if (rVal !== undefined && rVal !== null) {

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +1,35 @@
export function generateIndex(data, role, tableName = "") {
if (!Array.isArray(data) || data.length === 0) {
export function generateIndex(data, headers, role, tableName = "") {
if (!Array.isArray(data) || data.length === 0 || !Array.isArray(headers) || headers.length === 0) {
return "";
}
const headers = Object.keys(data[0]);
if (headers.length === 0) return "";
const indexColumns = identifyIndexColumns(data, headers);
const indexColumnIndices = identifyIndexColumns(data, headers);
const indexColumnHeaders = indexColumnIndices.map(i => headers[i]);
let indexLines = [];
indexLines.push(`| ${indexColumns.join(' | ')} |`);
indexLines.push(`| ${indexColumns.map(() => '---').join(' | ')} |`);
indexLines.push(`| ${indexColumnHeaders.join(' | ')} |`);
indexLines.push(`| ${indexColumnHeaders.map(() => '---').join(' | ')} |`);
let processedData = [...data];
const firstColKey = headers[0];
const firstColVal = data[0] ? data[0][firstColKey] : '';
const isIndexCol = (firstColKey && (firstColKey.includes('索引') || firstColKey.includes('Index'))) ||
const firstColIndex = 0;
const firstColHeader = headers[firstColIndex];
const firstColVal = data[0] ? data[0][firstColIndex] : '';
const isIndexCol = (firstColHeader && (firstColHeader.includes('索引') || firstColHeader.includes('Index'))) ||
(typeof firstColVal === 'string' && /^\s*M\d+/.test(firstColVal)) ||
(tableName && (tableName.includes('总结') || tableName.includes('大纲')));
if (isIndexCol) {
processedData.sort((a, b) => {
const valA = String(a[firstColKey] || '');
const valB = String(b[firstColKey] || '');
const valA = String(a[firstColIndex] || '');
const valB = String(b[firstColIndex] || '');
return valA.localeCompare(valB, undefined, { numeric: true });
});
}
for (const row of processedData) {
const lineParts = indexColumns.map(col => {
let val = row[col];
const lineParts = indexColumnIndices.map(colIndex => {
let val = row[colIndex];
if (val === undefined || val === null) return "";
val = String(val).trim();
if (val.length > 15) val = val.substring(0, 12) + "...";
@@ -43,19 +42,20 @@ export function generateIndex(data, role, tableName = "") {
}
function identifyIndexColumns(data, headers) {
if (headers.length <= 2) return headers;
if (headers.length <= 2) return headers.map((_, i) => i);
const candidates = [];
const maxColumns = 3;
for (const header of headers) {
for (let i = 0; i < headers.length; i++) {
if (candidates.length >= maxColumns) break;
const header = headers[i];
let totalLen = 0;
let count = 0;
for (const row of data) {
if (row[header]) {
totalLen += String(row[header]).length;
if (row[i]) {
totalLen += String(row[i]).length;
count++;
}
}
@@ -65,12 +65,12 @@ function identifyIndexColumns(data, headers) {
const isBlacklisted = /desc|bio|detail|history|经历|描述|详情/i.test(header);
if (!isLongText && !isBlacklisted) {
candidates.push(header);
candidates.push(i);
}
}
if (candidates.length === 0) {
return headers.slice(0, Math.min(headers.length, maxColumns));
return headers.map((_, i) => i).slice(0, Math.min(headers.length, maxColumns));
}
return candidates;

View File

@@ -0,0 +1,40 @@
import { getContext, extension_settings } from '/scripts/extensions.js';
import { saveChatDebounced } from '/script.js';
import { log } from './logger.js';
import { extensionName } from '../../utils/settings.js';
const TABLE_DATA_KEY = 'amily2_tables_data';
export async function clearTableRecordsBefore(floorIndex) {
const context = getContext();
if (!context || !context.chat || context.chat.length === 0) {
log('无法清除:聊天记录为空。', 'warn');
return 0;
}
let clearedCount = 0;
const chat = context.chat;
const targetIndex = Math.min(floorIndex, chat.length);
log(`开始清除第 ${targetIndex} 楼之前的表格记录...`, 'info');
for (let i = 0; i < targetIndex; i++) {
const message = chat[i];
if (message.extra && message.extra[TABLE_DATA_KEY]) {
delete message.extra[TABLE_DATA_KEY];
if (Object.keys(message.extra).length === 0) {
delete message.extra;
}
clearedCount++;
}
}
if (clearedCount > 0) {
await saveChatDebounced();
log(`成功清除了 ${clearedCount} 条消息中的表格记录。`, 'success');
} else {
log('没有发现需要清除的表格记录。', 'info');
}
return clearedCount;
}

File diff suppressed because one or more lines are too long

View File

@@ -2,12 +2,12 @@ import { getContext, extension_settings } from "/scripts/extensions.js";
import { saveChat } from "/script.js";
import { renderTables } from '../../ui/table-bindings.js';
import { extensionName } from "../../utils/settings.js";
import { convertTablesToCsvString, saveStateToMessage, getMemoryState, updateTableFromText, getBatchFillerRuleTemplate, getBatchFillerFlowTemplate } from './manager.js';
import { convertTablesToCsvString, convertSelectedTablesToCsvString, saveStateToMessage, getMemoryState, updateTableFromText, getBatchFillerRuleTemplate, getBatchFillerFlowTemplate } from './manager.js';
import { getPresetPrompts, getMixedOrder } from '../../PresetSettings/index.js';
import { callAI, generateRandomSeed } from '../api.js';
import { callNccsAI } from '../api/NccsApi.js';
export async function reorganizeTableContent() {
export async function reorganizeTableContent(selectedTableIndices) {
const settings = extension_settings[extensionName];
if (window.AMILY2_SYSTEM_PARALYZED === true) {
@@ -24,7 +24,13 @@ export async function reorganizeTableContent() {
try {
toastr.info('正在重新整理表格内容...', 'Amily2-重新整理');
const currentTableDataString = convertTablesToCsvString();
let currentTableDataString;
if (selectedTableIndices && Array.isArray(selectedTableIndices) && selectedTableIndices.length > 0) {
currentTableDataString = convertSelectedTablesToCsvString(selectedTableIndices);
} else {
currentTableDataString = convertTablesToCsvString();
}
if (!currentTableDataString.trim()) {
toastr.warning('当前没有表格内容需要整理。', 'Amily2-重新整理');
return;

View File

@@ -96,50 +96,74 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
}
try {
// --- 延迟填表逻辑 (V151.0) ---
const delay = parseInt(settings.secondary_filler_delay || 0, 10);
const bufferSize = parseInt(settings.secondary_filler_buffer || 0, 10);
const batchSize = parseInt(settings.secondary_filler_batch || 0, 10);
const contextLimit = parseInt(settings.secondary_filler_context || 2, 10);
const chat = context.chat;
let targetMessage;
let targetIndex;
const totalMessages = chat.length;
const validEndIndex = totalMessages - 1 - bufferSize;
if (delay > 0) {
// 如果有延迟,我们需要找到“延迟前”的那条消息
// chat.length - 1 是当前最新消息的索引
// 目标索引 = (chat.length - 1) - delay
targetIndex = (chat.length - 1) - delay;
if (targetIndex < 0) {
console.log(`[Amily2-副API] 延迟模式(${delay}): 历史楼层不足,跳过填表。`);
return;
}
targetMessage = chat[targetIndex];
// 检查目标消息是否是AI消息通常填表针对AI回复
// 如果目标消息是用户的消息而我们只想填AI的表这可能是一个问题。
// 但如果用户设置了延迟他们可能期望每隔几层填一次或者只填AI层。
// 现有的 `fillWithSecondaryApi` 是在 `CHAT_COMPLETION` 后调用的此时最新消息通常是AI消息。
// 如果延迟是奇数例如1目标消息可能是用户消息。
// 假设延迟是偶数例如2目标消息是上一条AI消息。
// 为了安全起见,如果目标消息是用户消息,我们可能应该跳过?或者依然填表(记录用户消息的表)?
// 目前表系统通常绑定在AI回复上。
// 如果 targetMessage.is_user我们尝试往回找最近的一条AI消息
// 不,这会乱套。严格按照楼层索引来。
console.log(`[Amily2-副API] 延迟模式生效: 当前总楼层 ${chat.length}, 延迟 ${delay}, 目标楼层索引 ${targetIndex}`);
} else {
// 无延迟,使用传入的最新消息
targetMessage = latestMessage;
targetIndex = chat.length - 1;
}
let textToProcess = targetMessage.mes;
if (!textToProcess || !textToProcess.trim()) {
console.log("[Amily2-副API] 目标消息内容为空,跳过填表任务。");
if (validEndIndex < 0) {
console.log(`[Amily2-副API] 消息数量不足以超出保留区(${bufferSize}),跳过。`);
return;
}
let targetMessages = [];
let needsProcessing = false;
const getContentHash = (content) => {
let hash = 0, i, chr;
if (content.length === 0) return hash;
for (i = 0; i < content.length; i++) {
chr = content.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
};
for (let i = validEndIndex; i >= 0; i--) {
const msg = chat[i];
if (msg.is_user) continue;
const currentHash = getContentHash(msg.mes);
const savedHash = msg.metadata?.Amily2_Process_Hash;
const isUnprocessed = !savedHash;
const isChanged = savedHash && savedHash !== currentHash;
if (isUnprocessed || isChanged) {
targetMessages.unshift({ index: i, msg: msg, hash: currentHash });
if (batchSize > 0 && targetMessages.length >= batchSize) {
needsProcessing = true;
break;
}
} else {
continue;
}
}
if (targetMessages.length === 0) {
console.log("[Amily2-副API] 没有发现需要处理的消息。");
return;
}
if (batchSize > 0) {
if (targetMessages.length < batchSize) {
console.log(`[Amily2-副API] 批量模式: 当前累积 ${targetMessages.length}/${batchSize} 条未处理消息,暂不触发。`);
return;
}
} else {
targetMessages = [targetMessages[targetMessages.length - 1]];
}
console.log(`[Amily2-副API] 触发填表: 处理 ${targetMessages.length} 条消息。索引范围: ${targetMessages[0].index} - ${targetMessages[targetMessages.length-1].index}`);
toastr.info(`分步填表正在执行,正在填写 ${targetMessages[0].index + 1} 楼至 ${targetMessages[targetMessages.length-1].index + 1} 楼的内容`, "Amily2-分步填表");
let tagsToExtract = [];
let exclusionRules = [];
if (settings.table_independent_rules_enabled) {
@@ -147,35 +171,38 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
exclusionRules = settings.table_exclusion_rules || [];
}
if (tagsToExtract.length > 0) {
const blocks = extractBlocksByTags(textToProcess, tagsToExtract);
textToProcess = blocks.join('\n\n');
}
textToProcess = applyExclusionRules(textToProcess, exclusionRules);
if (!textToProcess.trim()) {
console.log("[Amily2-副API] 规则处理后消息内容为空,跳过填表任务。");
return;
}
let coreContentText = "";
const userName = context.name1 || '用户';
const characterName = context.name2 || '角色';
// 寻找目标消息之前的最后一条用户消息
let lastUserMessage = null;
let lastUserMessageIndex = -1;
// 从 targetIndex - 1 开始往前找
for (let i = targetIndex - 1; i >= 0; i--) {
if (chat[i].is_user) {
lastUserMessage = chat[i];
lastUserMessageIndex = i;
break;
for (const target of targetMessages) {
let textToProcess = target.msg.mes;
if (tagsToExtract.length > 0) {
const blocks = extractBlocksByTags(textToProcess, tagsToExtract);
textToProcess = blocks.join('\n\n');
}
textToProcess = applyExclusionRules(textToProcess, exclusionRules);
if (!textToProcess.trim()) continue;
coreContentText += `\n【第 ${target.index + 1} 楼】${characterName}AI消息\n${textToProcess}\n`;
}
const currentInteractionContent = (lastUserMessage ? `${userName}(用户)消息:${lastUserMessage.mes}\n` : '') +
`${characterName}AI消息[核心处理内容]${textToProcess}`;
if (!coreContentText.trim()) {
console.log("[Amily2-副API] 目标内容处理后为空,跳过。");
return;
}
const historyEndIndex = targetMessages[0].index - 1;
let historyContextStr = "";
if (contextLimit > 0 && historyEndIndex >= 0) {
historyContextStr = await getHistoryContext(contextLimit, historyEndIndex, tagsToExtract, exclusionRules) || "";
}
const currentInteractionContent = (historyContextStr ? `${historyContextStr}\n\n` : '') +
`<核心填表内容>\n${coreContentText}\n</核心填表内容>`;
let mixedOrder;
try {
@@ -187,10 +214,7 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
console.error("[副API填表] 加载混合顺序失败:", e);
}
const order = getMixedOrder('secondary_filler') || [];
const presetPrompts = await getPresetPrompts('secondary_filler');
const messages = [
@@ -219,18 +243,8 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
}
break;
case 'contextHistory':
const contextReadingLevel = settings.context_reading_level || 4;
const historyMessagesToGet = contextReadingLevel > 2 ? contextReadingLevel - 2 : 0;
if (historyMessagesToGet > 0) {
// 这里的 historyEndIndex 应该是我们上面计算出的 lastUserMessageIndex
// 如果没找到用户消息,则使用 targetIndex - 1
const historyEndIndex = lastUserMessageIndex !== -1 ? lastUserMessageIndex : Math.max(0, targetIndex - 1);
const historyContext = await getHistoryContext(historyMessagesToGet, historyEndIndex, tagsToExtract, exclusionRules);
if (historyContext) {
messages.push({ role: "system", content: historyContext });
}
if (historyContextStr) {
messages.push({ role: "system", content: historyContextStr });
}
break;
case 'ruleTemplate':
@@ -240,7 +254,7 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
messages.push({ role: "system", content: finalFlowPrompt });
break;
case 'coreContent':
messages.push({ role: 'user', content: `请严格根据以下"最新消息"中的内容进行填写表格,并按照指定的格式输出,不要添加任何额外信息。\n\n<最新消息>\n${currentInteractionContent}\n</最新消息>` });
messages.push({ role: 'user', content: `请严格根据以下"核心填表内容"进行填写表格,并按照指定的格式输出,不要添加任何额外信息。\n\n<核心填表内容>\n${coreContentText}\n</核心填表内容>` });
break;
}
}
@@ -269,21 +283,22 @@ export async function fillWithSecondaryApi(latestMessage, forceRun = false) {
updateTableFromText(rawContent);
// 保存到目标消息
if (saveStateToMessage(getMemoryState(), targetMessage)) {
// 如果目标消息不是最新消息,我们可能需要重新渲染整个聊天记录或者特定消息的表格?
// renderTables() 通常重新渲染所有可见表格
const memoryState = getMemoryState();
const lastProcessedMsg = targetMessages[targetMessages.length - 1].msg;
for (const target of targetMessages) {
if (!target.msg.metadata) target.msg.metadata = {};
target.msg.metadata.Amily2_Process_Hash = target.hash;
}
if (saveStateToMessage(memoryState, lastProcessedMsg)) {
renderTables();
// updateOrInsertTableInChat 通常插入到DOM中
// 我们可能需要传递 targetIndex 给 updateOrInsertTableInChat 吗?
// 目前 updateOrInsertTableInChat 似乎是查找 .mes_text 并插入。
// 如果我们更新了历史消息的数据,我们需要确保 DOM 也更新。
// 由于 SillyTavern 的消息渲染机制,如果消息已经在屏幕上,仅仅修改数据可能不会自动更新 DOM。
// 但是 renderTables() 应该会处理这个。
updateOrInsertTableInChat();
}
saveChat();
toastr.success("分步填表执行完毕。", "Amily2-分步填表");
} catch (error) {
console.error(`[Amily2-副API] 发生严重错误:`, error);

View File

@@ -479,6 +479,8 @@ class AmilyHelper {
depth: newEntryData.depth ?? 998,
scanDepth: newEntryData.scanDepth ?? null,
disable: !(newEntryData.enabled ?? true),
excludeRecursion: newEntryData.excludeRecursion ?? newEntryData.exclude_recursion ?? false,
preventRecursion: newEntryData.preventRecursion ?? newEntryData.prevent_recursion ?? false,
});
if (newEntryData.type === 'selective') newEntry.constant = false;
}