mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 08:55:50 +00:00
Merge branch 'Wx-2025:main' into main
This commit is contained in:
126
core/archive-manager.js
Normal file
126
core/archive-manager.js
Normal 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);
|
||||
}
|
||||
336
core/auto-char-card/agent-manager.js
Normal file
336
core/auto-char-card/agent-manager.js
Normal 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
122
core/auto-char-card/api.js
Normal 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;
|
||||
}
|
||||
}
|
||||
150
core/auto-char-card/char-api.js
Normal file
150
core/auto-char-card/char-api.js
Normal 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;
|
||||
}
|
||||
}
|
||||
63
core/auto-char-card/context-manager.js
Normal file
63
core/auto-char-card/context-manager.js
Normal 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();
|
||||
}
|
||||
}
|
||||
248
core/auto-char-card/tools.js
Normal file
248
core/auto-char-card/tools.js
Normal 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"]
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
431
core/auto-char-card/ui-bindings.js
Normal file
431
core/auto-char-card/ui-bindings.js
Normal 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
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
229
core/fractal-memory.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
30
core/lore.js
30
core/lore.js
@@ -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
@@ -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: {},
|
||||
};
|
||||
|
||||
70
core/relationship-graph/executor.js
Normal file
70
core/relationship-graph/executor.js
Normal 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;
|
||||
}
|
||||
1
core/relationship-graph/manager.js
Normal file
1
core/relationship-graph/manager.js
Normal file
File diff suppressed because one or more lines are too long
1
core/relationship-graph/visualizer.js
Normal file
1
core/relationship-graph/visualizer.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
40
core/table-system/cleaner.js
Normal file
40
core/table-system/cleaner.js
Normal 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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user