mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 20:55:51 +00:00
Add files via upload
This commit is contained in:
303
core/api.js
Normal file
303
core/api.js
Normal file
@@ -0,0 +1,303 @@
|
||||
|
||||
import { extension_settings, getContext } from "/scripts/extensions.js";
|
||||
import { characters } from "/script.js";
|
||||
import { world_names } from "/scripts/world-info.js";
|
||||
import { extensionName } from "../utils/settings.js";
|
||||
import { getCombinedWorldbookContent, findLatestSummaryLore, DEDICATED_LOREBOOK_NAME, getChatIdentifier } from "./lore.js";
|
||||
|
||||
|
||||
let isFetchingModels = false;
|
||||
|
||||
export async function fetchSupportedModels() {
|
||||
const apiUrl = $("#amily2_api_url").val().trim();
|
||||
const apiKey = $("#amily2_api_key").val().trim();
|
||||
|
||||
if (!apiUrl) {
|
||||
toastr.error("请先配置API URL", "获取模型失败");
|
||||
return [];
|
||||
}
|
||||
if (isFetchingModels) {
|
||||
toastr.info("正在获取模型列表,请稍候...", "获取模型");
|
||||
return [];
|
||||
}
|
||||
|
||||
isFetchingModels = true;
|
||||
$("#amily2_refresh_models")
|
||||
.prop("disabled", true)
|
||||
.html('<i class="fas fa-spinner fa-spin"></i> 加载中');
|
||||
|
||||
try {
|
||||
let modelListUrl;
|
||||
if (apiUrl.includes("/v1/chat/completions")) {
|
||||
modelListUrl = apiUrl.replace("/v1/chat/completions", "/v1/models");
|
||||
} else if (apiUrl.endsWith("/v1")) {
|
||||
modelListUrl = `${apiUrl}/models`;
|
||||
} else if (apiUrl.endsWith("/")) {
|
||||
modelListUrl = `${apiUrl}v1/models`;
|
||||
} else {
|
||||
modelListUrl = `${apiUrl}/v1/models`;
|
||||
}
|
||||
|
||||
console.log("[更新] 模型列表请求地址:", modelListUrl);
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
|
||||
if (modelListUrl.includes("love.qinyan.xyz")) {
|
||||
headers["X-Custom-Proxy"] = "Amily2-ChatPlugin";
|
||||
headers["Origin"] = window.location.origin;
|
||||
}
|
||||
|
||||
const response = await fetch(modelListUrl, {
|
||||
method: "GET",
|
||||
headers: headers,
|
||||
mode: "cors",
|
||||
credentials: "omit",
|
||||
});
|
||||
if (!response.ok) {
|
||||
let errorBody = "";
|
||||
try {
|
||||
const errorResponse = await response.json();
|
||||
errorBody = errorResponse.error?.message
|
||||
? ` - ${errorResponse.error.message}`
|
||||
: await response.text();
|
||||
} catch (e) {
|
||||
errorBody = "无法解析错误响应";
|
||||
}
|
||||
throw new Error(
|
||||
`API返回错误: ${response.status} ${response.statusText}${errorBody}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
let models = [];
|
||||
if (Array.isArray(data)) {
|
||||
models = data.map((m) => m.id || m);
|
||||
} else if (data.data && Array.isArray(data.data)) {
|
||||
models = data.data.map((m) => m.id);
|
||||
} else if (data.models && Array.isArray(data.models)) {
|
||||
models = data.models;
|
||||
} else {
|
||||
throw new Error("未知的模型列表格式");
|
||||
}
|
||||
|
||||
const availableModels = models.filter(
|
||||
(m) =>
|
||||
!m.includes("embed") &&
|
||||
!m.includes("search") &&
|
||||
!m.includes("similarity") &&
|
||||
!m.includes("audio"),
|
||||
);
|
||||
availableModels.sort();
|
||||
|
||||
console.log(
|
||||
`获取模型列表成功 (${availableModels.length}个):`,
|
||||
availableModels,
|
||||
);
|
||||
|
||||
toastr.success(
|
||||
`成功获取 ${availableModels.length} 个可用模型`,
|
||||
"模型加载完成",
|
||||
);
|
||||
return availableModels;
|
||||
} catch (error) {
|
||||
console.error("[错误详情] 获取模型列表失败:", {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
if (error.message.includes("Failed to fetch"))
|
||||
toastr.error("网络连接失败,请检查API地址和网络状态", "网络错误");
|
||||
else if (error.message.includes("401") || error.message.includes("403"))
|
||||
toastr.error("API密钥无效或权限不足", "认证错误");
|
||||
else if (error.message.includes("404"))
|
||||
toastr.error(
|
||||
"API端点不存在,请确保URL指向OpenAI兼容的/v1/models端点",
|
||||
"端点错误",
|
||||
);
|
||||
else toastr.error(`获取模型失败: ${error.message}`, "错误");
|
||||
return [];
|
||||
} finally {
|
||||
isFetchingModels = false;
|
||||
$("#amily2_refresh_models")
|
||||
.prop("disabled", false)
|
||||
.html('<i class="fas fa-sync-alt"></i> 刷新模型');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function checkAndFixWithAPI(latestMessage, previousMessages) {
|
||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||
console.error("[Amily2-制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||
return null;
|
||||
}
|
||||
console.groupCollapsed(
|
||||
`[Amily2号-优化任务] ${new Date().toLocaleTimeString()}`,
|
||||
);
|
||||
console.time("优化任务总耗时");
|
||||
|
||||
const settings = extension_settings[extensionName];
|
||||
if (!settings.apiUrl || !settings.apiUrl.trim()) {
|
||||
toastr.error("API URL 未配置。", "API错误");
|
||||
console.timeEnd("优化任务总耗时");
|
||||
console.groupEnd();
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const userLatestMessage =
|
||||
previousMessages.length > 0
|
||||
? previousMessages[previousMessages.length - 1]
|
||||
: null;
|
||||
|
||||
let textToOptimize = latestMessage.mes;
|
||||
if (userLatestMessage && userLatestMessage.is_user) {
|
||||
console.log("【陛下最新圣旨】:", userLatestMessage.mes);
|
||||
}
|
||||
console.log("【待优化原文 (Amily回复)】:", textToOptimize);
|
||||
const initialContentMatch = latestMessage.mes.match(
|
||||
/<content>([\s\S]*?)<\/content>/,
|
||||
);
|
||||
if (initialContentMatch) {
|
||||
textToOptimize = initialContentMatch[1].trim();
|
||||
}
|
||||
|
||||
let worldbookContent = "";
|
||||
if (settings.worldbookEnabled) {
|
||||
console.time("世界书调阅耗时");
|
||||
let combinedContents = [];
|
||||
const context = getContext();
|
||||
const character = context.characters[context.characterId];
|
||||
const characterLorebookName = character?.data?.extensions?.world;
|
||||
if (
|
||||
characterLorebookName &&
|
||||
world_names.includes(characterLorebookName)
|
||||
) {
|
||||
const characterLore = await getCombinedWorldbookContent(
|
||||
characterLorebookName,
|
||||
);
|
||||
if (characterLore) {
|
||||
worldbookContent = characterLore; // 将角色世界书内容赋给主变量
|
||||
combinedContents.push(`角色主档案(${characterLorebookName})`);
|
||||
}
|
||||
}
|
||||
const chatIdentifier = await getChatIdentifier();
|
||||
const summaryLoreEntry = await findLatestSummaryLore(
|
||||
DEDICATED_LOREBOOK_NAME,
|
||||
chatIdentifier,
|
||||
);
|
||||
if (summaryLoreEntry && summaryLoreEntry.content) {
|
||||
combinedContents.push(`Amily2号自动总结档案`);
|
||||
}
|
||||
if (combinedContents.length > 0) {
|
||||
console.log(
|
||||
`[情报部] 已装载世界书内容: ${combinedContents.join("、 ")}`,
|
||||
);
|
||||
}
|
||||
console.timeEnd("世界书调阅耗时");
|
||||
}
|
||||
|
||||
console.groupCollapsed("Amily2号-统一情报卷宗");
|
||||
let userCommand = "请根据以下信息,执行你的多任务指令:\n\n";
|
||||
const history = previousMessages
|
||||
.map((m) => `${m.is_user ? "陛下" : "姐姐Amily"}: ${m.mes}`)
|
||||
.join("\n");
|
||||
if (history) {
|
||||
console.log("【历史对话】已装载");
|
||||
userCommand += `[近期对话历史]:\n${history}\n\n---\n`;
|
||||
}
|
||||
if (worldbookContent) {
|
||||
console.log("【世界书】已装载");
|
||||
userCommand += `[参考档案总集]:\n${worldbookContent}\n\n---\n`;
|
||||
}
|
||||
if (settings.mainPrompt && settings.mainPrompt.trim()) {
|
||||
console.log("【指令】已附加破限提示词");
|
||||
userCommand += `[最高优先级指令]:\n${settings.mainPrompt}\n\n---\n`;
|
||||
}
|
||||
userCommand += `[待处理的原文]:\n${textToOptimize}`;
|
||||
|
||||
let finalSystemPrompt = settings.systemPrompt;
|
||||
console.log("【规则】已附加系统提示词 (预设提示词)");
|
||||
if (settings.outputFormatPrompt && settings.outputFormatPrompt.trim()) {
|
||||
console.log("【格式】已附加优化内容格式提示词");
|
||||
finalSystemPrompt += `\n\n[输出格式指令]:\n你必须严格遵循以下格式来构建<content>标签内的所有内容:\n${settings.outputFormatPrompt}`;
|
||||
}
|
||||
if (
|
||||
settings.summarizationEnabled &&
|
||||
settings.summarizationPrompt &&
|
||||
settings.summarizationPrompt.trim()
|
||||
) {
|
||||
console.log("【总结】已附加总结提示词");
|
||||
finalSystemPrompt += `\n\n[总结附加指令]:\n${settings.summarizationPrompt}`;
|
||||
}
|
||||
console.groupEnd();
|
||||
|
||||
const messages = [
|
||||
{ role: "system", content: finalSystemPrompt },
|
||||
{ role: "user", content: userCommand },
|
||||
];
|
||||
|
||||
console.time("API请求耗时");
|
||||
let apiUrl = settings.apiUrl.trim();
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (apiUrl.endsWith("/v1")) apiUrl += "/chat/completions";
|
||||
else if (apiUrl.endsWith("/")) apiUrl += "v1/chat/completions";
|
||||
else apiUrl += "/v1/chat/completions";
|
||||
}
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (settings.apiKey) headers["Authorization"] = `Bearer ${settings.apiKey}`;
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
model: settings.model,
|
||||
messages: messages,
|
||||
max_tokens: settings.maxTokens,
|
||||
temperature: settings.temperature,
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
console.timeEnd("API请求耗时");
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`API请求失败: ${response.status} ${response.statusText} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
const data = await response.json();
|
||||
const rawContent = data.choices?.[0]?.message?.content;
|
||||
if (!rawContent) {
|
||||
console.timeEnd("优化任务总耗时");
|
||||
console.groupEnd();
|
||||
return null;
|
||||
}
|
||||
|
||||
const separator = "###AMILY2-SUMMARY###";
|
||||
let optimizedContent = rawContent;
|
||||
let summary = null;
|
||||
if (rawContent.includes(separator)) {
|
||||
const parts = rawContent.split(separator);
|
||||
optimizedContent = parts[0].trim();
|
||||
summary = parts[1] ? parts[1].trim() : null;
|
||||
}
|
||||
|
||||
if (summary) {
|
||||
console.groupCollapsed("Amily2号-生成总结");
|
||||
console.log(summary);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
console.timeEnd("优化任务总耗时");
|
||||
console.groupEnd();
|
||||
return { optimizedContent, summary };
|
||||
} catch (error) {
|
||||
console.error(`[Amily2-情报解析官] 发生严重错误: ${error.message}`);
|
||||
toastr.error(`API调用失败: ${error.message}`, "Amily2号");
|
||||
console.timeEnd("优化任务总耗时");
|
||||
console.groupEnd();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
198
core/commands.js
Normal file
198
core/commands.js
Normal file
@@ -0,0 +1,198 @@
|
||||
import { getContext, extension_settings } from "/scripts/extensions.js";
|
||||
import { saveChatConditional, reloadCurrentChat } from "/script.js";
|
||||
import { extensionName } from "../utils/settings.js";
|
||||
import { SlashCommand } from "/scripts/slash-commands/SlashCommand.js";
|
||||
import { SlashCommandParser } from "/scripts/slash-commands/SlashCommandParser.js";
|
||||
import { checkAndFixWithAPI } from "./api.js";
|
||||
|
||||
async function checkLatestMessage() {
|
||||
const context = getContext();
|
||||
const chat = context.chat || [];
|
||||
|
||||
if (!chat || chat.length === 0) {
|
||||
console.log("[Amily2-命令检查器] 没有聊天记录。");
|
||||
return { message: null, previousMessages: [] };
|
||||
}
|
||||
|
||||
const latestMessage = chat[chat.length - 1];
|
||||
|
||||
console.log("[Amily2-命令检查器] 正在侦测消息:", {
|
||||
isUser: latestMessage.is_user,
|
||||
messagePreview: latestMessage.mes?.substring(0, 50) + "...",
|
||||
});
|
||||
|
||||
if (latestMessage.is_user) {
|
||||
console.log("[Amily2-命令检查器] 目标为用户消息,跳过。");
|
||||
return { message: latestMessage, previousMessages: [] };
|
||||
}
|
||||
|
||||
const settings = extension_settings[extensionName];
|
||||
const contextCount = settings.contextMessages || 2;
|
||||
const startIndex = Math.max(0, chat.length - contextCount - 1);
|
||||
const previousMessages = chat.slice(startIndex, chat.length - 1);
|
||||
|
||||
console.log("[Amily2-命令检查器] 已获取上下文消息:", {
|
||||
count: previousMessages.length,
|
||||
});
|
||||
|
||||
return { message: latestMessage, previousMessages };
|
||||
}
|
||||
|
||||
async function checkCommand() {
|
||||
const settings = extension_settings[extensionName];
|
||||
if (!settings.apiUrl) {
|
||||
toastr.error("请先配置API URL", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
const checkResult = await checkLatestMessage();
|
||||
if (!checkResult.message || checkResult.message.is_user) {
|
||||
toastr.info("最新消息是用户消息,无需检查", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
toastr.info("正在使用API检查回复...", "命令检查器");
|
||||
const result = await checkAndFixWithAPI(
|
||||
checkResult.message,
|
||||
checkResult.previousMessages,
|
||||
);
|
||||
if (
|
||||
result &&
|
||||
result.optimizedContent &&
|
||||
result.optimizedContent !== checkResult.message.mes
|
||||
) {
|
||||
toastr.warning("检测到问题,建议使用修复功能", "命令检查器");
|
||||
} else {
|
||||
toastr.success("未检测到问题", "命令检查器");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
export async function fixCommand() {
|
||||
const settings = extension_settings[extensionName];
|
||||
if (!settings.apiUrl) {
|
||||
toastr.error("请先配置API URL", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
if (!chat || chat.length === 0) {
|
||||
toastr.info("没有可修复的消息", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
const latestMessage = chat[chat.length - 1];
|
||||
if (latestMessage.is_user) {
|
||||
toastr.info("最新消息是用户消息,无需修复", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
const contextCount = settings.contextMessages || 2;
|
||||
const startIndex = Math.max(0, chat.length - 1 - contextCount);
|
||||
const previousMessages = chat.slice(startIndex, chat.length - 1);
|
||||
toastr.info("正在检查并修复回复...", "命令检查器");
|
||||
const result = await checkAndFixWithAPI(latestMessage, previousMessages);
|
||||
if (
|
||||
result &&
|
||||
result.optimizedContent &&
|
||||
result.optimizedContent !== latestMessage.mes
|
||||
) {
|
||||
latestMessage.mes = result.optimizedContent;
|
||||
await saveChatConditional();
|
||||
await reloadCurrentChat();
|
||||
toastr.success("回复已修复", "命令检查器");
|
||||
} else {
|
||||
toastr.info("未检测到需要修复的问题", "命令检查器");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function testReplyChecker() {
|
||||
const settings = extension_settings[extensionName];
|
||||
if (!settings.apiUrl) {
|
||||
toastr.error("请先配置API URL", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
if (!chat || chat.length < 2) {
|
||||
toastr.warning("需要至少2条消息才能测试", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
let testMessage = null;
|
||||
for (let i = chat.length - 2; i >= 0; i--) {
|
||||
if (!chat[i].is_user) {
|
||||
testMessage = chat[i].mes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!testMessage) {
|
||||
toastr.warning("没有找到可用于测试的AI消息", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
const lastMessage = chat[chat.length - 1];
|
||||
if (lastMessage.is_user) {
|
||||
toastr.warning("最后一条消息是用户消息,无法测试", "命令检查器");
|
||||
return "";
|
||||
}
|
||||
const originalMessage = lastMessage.mes;
|
||||
lastMessage.mes = testMessage + "\n\n" + testMessage;
|
||||
toastr.info("正在使用API测试检测功能...", "命令检查器");
|
||||
const contextCount = settings.contextMessages || 2;
|
||||
const startIndex = Math.max(0, chat.length - contextCount - 1);
|
||||
const previousMessages = chat.slice(startIndex, chat.length - 1);
|
||||
const result = await checkAndFixWithAPI(lastMessage, previousMessages);
|
||||
lastMessage.mes = originalMessage;
|
||||
if (
|
||||
result &&
|
||||
result.optimizedContent &&
|
||||
result.optimizedContent !== testMessage + "\n\n" + testMessage
|
||||
) {
|
||||
toastr.success("测试成功!API检测到重复内容并提供了修复建议", "命令检查器");
|
||||
} else {
|
||||
toastr.warning(
|
||||
"测试结果:API未检测到问题,请检查API配置或提示词",
|
||||
"命令检查器",
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function registerSlashCommands() {
|
||||
try {
|
||||
if (
|
||||
typeof SlashCommand === "undefined" ||
|
||||
typeof SlashCommandParser === "undefined"
|
||||
) {
|
||||
console.error(
|
||||
"[Amily2] 致命错误:SlashCommand 或 SlashCommandParser 模块未能加载。",
|
||||
);
|
||||
return;
|
||||
}
|
||||
SlashCommandParser.addCommandObject(
|
||||
SlashCommand.fromProps({
|
||||
name: "check-reply",
|
||||
callback: checkCommand,
|
||||
helpString: "检查最新的AI回复是否有问题",
|
||||
}),
|
||||
);
|
||||
console.log("[Amily2-新诏] /check-reply 命令已成功颁布。");
|
||||
|
||||
SlashCommandParser.addCommandObject(
|
||||
SlashCommand.fromProps({
|
||||
name: "fix-reply",
|
||||
callback: fixCommand,
|
||||
helpString: "修复最新的AI回复中的问题",
|
||||
}),
|
||||
);
|
||||
console.log("[Amily2-新诏] /fix-reply 命令已成功颁布。");
|
||||
|
||||
SlashCommandParser.addCommandObject(
|
||||
SlashCommand.fromProps({
|
||||
name: "test-reply-checker",
|
||||
callback: testReplyChecker,
|
||||
helpString: "测试聊天回复检查器功能",
|
||||
}),
|
||||
);
|
||||
console.log("[Amily2-新诏] /test-reply-checker 命令已成功颁布。");
|
||||
} catch (e) {
|
||||
console.error("[Amily2] 命令注册过程中发生意外错误:", e);
|
||||
}
|
||||
}
|
||||
98
core/events.js
Normal file
98
core/events.js
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
|
||||
import { getContext, extension_settings } from "/scripts/extensions.js";
|
||||
import { characters, saveChatConditional, reloadCurrentChat } from "/script.js";
|
||||
import { extensionName } from "../utils/settings.js";
|
||||
import { checkAndFixWithAPI } from "./api.js";
|
||||
import { writeSummaryToLorebook, getChatIdentifier } from "./lore.js";
|
||||
|
||||
|
||||
const pendingWriteData = {
|
||||
summary: null,
|
||||
targetLorebook: null,
|
||||
chatIdentifier: null,
|
||||
sourceAiMessageTimestamp: null,
|
||||
};
|
||||
|
||||
export async function onMessageReceived(data) {
|
||||
const context = getContext();
|
||||
if ((data && data.is_user) || context.isWaitingForUserInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = extension_settings[extensionName];
|
||||
const chat = context.chat;
|
||||
if (!chat || chat.length === 0) return;
|
||||
|
||||
const latestMessage = chat[chat.length - 1];
|
||||
|
||||
if (latestMessage.is_user || !settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingWriteData.summary) {
|
||||
await writeSummaryToLorebook(pendingWriteData);
|
||||
}
|
||||
if (!settings.optimizationEnabled && !settings.summarizationEnabled) {
|
||||
console.log("[Amily2号] 优化与总结功能均未启用,任务中止。");
|
||||
return;
|
||||
}
|
||||
if (!settings.apiUrl) return;
|
||||
|
||||
const contextCount = settings.contextMessages || 2;
|
||||
const startIndex = Math.max(0, chat.length - 1 - contextCount);
|
||||
const previousMessages = chat.slice(startIndex, chat.length - 1);
|
||||
const result = await checkAndFixWithAPI(latestMessage, previousMessages);
|
||||
|
||||
if (result) {
|
||||
if (
|
||||
result.optimizedContent &&
|
||||
result.optimizedContent !== latestMessage.mes &&
|
||||
settings.optimizationEnabled
|
||||
) {
|
||||
latestMessage.mes = result.optimizedContent;
|
||||
await saveChatConditional();
|
||||
if (settings.optimizationMode === "refresh") {
|
||||
await reloadCurrentChat();
|
||||
}
|
||||
}
|
||||
|
||||
if (result.summary && settings.summarizationEnabled) {
|
||||
|
||||
pendingWriteData.summary = result.summary;
|
||||
pendingWriteData.sourceAiMessageTimestamp = latestMessage.send_date;
|
||||
pendingWriteData.targetLorebook = settings.lorebookTarget;
|
||||
pendingWriteData.chatIdentifier = await getChatIdentifier();
|
||||
|
||||
if (settings.showOptimizationToast) {
|
||||
let targetName = "独立中央档案";
|
||||
if (settings.lorebookTarget === "character_main") {
|
||||
const character = characters[context.characterId];
|
||||
targetName = character?.data?.extensions?.world || "未绑定的主世界书";
|
||||
}
|
||||
toastr.info(
|
||||
`已优化并将总结:“${result.summary}” 写入 “${targetName}”`,
|
||||
"Amily2号",
|
||||
{ timeOut: 7000 },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function onChatChanged() {
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
if (!chat || chat.length === 0) {
|
||||
pendingWriteData.summary = null;
|
||||
return;
|
||||
}
|
||||
const latestMessage = chat[chat.length - 1];
|
||||
if (latestMessage.is_user && pendingWriteData.summary) {
|
||||
console.log(
|
||||
"[Amily2号-遗忘哨兵] 检测到AI回复被操作,已清除待写入的过时总结。",
|
||||
);
|
||||
pendingWriteData.summary = null;
|
||||
}
|
||||
}
|
||||
241
core/lore.js
Normal file
241
core/lore.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import { getContext } from "/scripts/extensions.js";
|
||||
import { characters, eventSource, event_types } from "/script.js";
|
||||
import { loadWorldInfo, createNewWorldInfo, createWorldInfoEntry, saveWorldInfo, world_names } from "/scripts/world-info.js";
|
||||
|
||||
|
||||
export const LOREBOOK_PREFIX = "Amily2档案-";
|
||||
export const DEDICATED_LOREBOOK_NAME = "Amily2号-国史馆";
|
||||
export const INTRODUCTORY_TEXT =
|
||||
"【Amily2号自动档案】\n此卷宗由Amily2号优化助手自动生成并维护,记录核心事件脉络。\n---\n";
|
||||
|
||||
export async function getChatIdentifier() {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
const interval = 100;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
const context = getContext();
|
||||
if (context && context.characterId) {
|
||||
const character = characters[context.characterId];
|
||||
if (character && character.avatar) {
|
||||
return `char-${character.avatar.replace(/\.(png|webp|jpg|jpeg|gif)$/, "")}`;
|
||||
}
|
||||
return `char-${context.characterId}`;
|
||||
}
|
||||
if (context && context.chat_filename) {
|
||||
const fileName = context.chat_filename.split(/[\\/]/).pop();
|
||||
return fileName.replace(/\.jsonl?$/, "");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[Amily2-户籍管理处] 等待上下文时发生轻微错误 (尝试次数 ${attempts + 1}):`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
console.error("[Amily2-国史馆] 户籍管理处在长时间等待后,仍无法确定户籍。");
|
||||
toastr.warning(
|
||||
"Amily2号无法确定当前聊天身份,世界书功能将受影响。",
|
||||
"上下文错误",
|
||||
);
|
||||
return "unknown_chat_timeout";
|
||||
}
|
||||
|
||||
export async function findLatestSummaryLore(lorebookName, chatIdentifier) {
|
||||
try {
|
||||
const bookData = await loadWorldInfo(lorebookName);
|
||||
if (!bookData || !bookData.entries) {
|
||||
return null;
|
||||
}
|
||||
const entriesArray = Object.values(bookData.entries);
|
||||
const uniqueLoreName = `${LOREBOOK_PREFIX}${chatIdentifier}`;
|
||||
return (
|
||||
entriesArray.find(
|
||||
(entry) => entry.comment === uniqueLoreName && !entry.disable,
|
||||
) || null
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[Amily2-国史馆] 钦差大臣在 '${lorebookName}' 检索时发生错误:`,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCombinedWorldbookContent(lorebookName) {
|
||||
if (!lorebookName) return "";
|
||||
try {
|
||||
const bookData = await loadWorldInfo(lorebookName);
|
||||
if (!bookData || !bookData.entries) {
|
||||
return "";
|
||||
}
|
||||
const activeContents = Object.values(bookData.entries)
|
||||
.filter((entry) => !entry.disable)
|
||||
.map((entry) => `[条目: ${entry.comment || "无标题"}]\n${entry.content}`);
|
||||
return activeContents.join("\n\n---\n\n");
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[Amily2-国史馆] 钦差大臣在整合 '${lorebookName}' 时发生错误:`,
|
||||
error,
|
||||
);
|
||||
toastr.error(`读取世界书 '${lorebookName}' 失败!`, "档案整合错误");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWorldbookListOnly(newBookName = null) {
|
||||
console.log("[Amily2号-工部-v1.3] 执行“圣谕广播”式UI更新...");
|
||||
try {
|
||||
if (newBookName) {
|
||||
if (Array.isArray(world_names) && !world_names.includes(newBookName)) {
|
||||
world_names.push(newBookName);
|
||||
world_names.sort();
|
||||
console.log(`[Amily2号-工部] 已将《${newBookName}》注入前端数据模型。`);
|
||||
} else {
|
||||
console.log(`[Amily2号-工部] 《${newBookName}》已存在于数据模型中,跳过注入。`);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
eventSource &&
|
||||
typeof eventSource.emit === "function" &&
|
||||
event_types.CHARACTER_PAGE_LOADED
|
||||
) {
|
||||
console.log(`[Amily2号-工部] 正在广播事件: ${event_types.CHARACTER_PAGE_LOADED}`);
|
||||
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
|
||||
console.log("[Amily2号-工部] “character_page_loaded”事件已广播,UI应已响应刷新。");
|
||||
} else {
|
||||
console.error("[Amily2号] 致命错误: eventSource 或 event_types.CHARACTER_PAGE_LOADED 未找到。无法广播刷新事件。");
|
||||
toastr.error("Amily2号无法触发UI刷新。", "核心事件系统缺失");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Amily2号-工部] “圣谕广播”式刷新失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeSummaryToLorebook(pendingData) {
|
||||
if (!pendingData || !pendingData.summary || !pendingData.sourceAiMessageTimestamp) return;
|
||||
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
let isSourceMessageValid = false;
|
||||
let sourceMessageCandidate = null;
|
||||
|
||||
for (let i = chat.length - 2; i >= 0; i--) {
|
||||
if (!chat[i].is_user) {
|
||||
sourceMessageCandidate = chat[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
sourceMessageCandidate &&
|
||||
sourceMessageCandidate.send_date === pendingData.sourceAiMessageTimestamp
|
||||
) {
|
||||
isSourceMessageValid = true;
|
||||
}
|
||||
|
||||
|
||||
const summaryToCommit = pendingData.summary;
|
||||
const targetLorebookFromPending = pendingData.targetLorebook;
|
||||
|
||||
|
||||
if (!isSourceMessageValid) {
|
||||
console.log(
|
||||
"[Amily2号-逆时寻踪] 裁决: 源消息已被修改或删除,遵旨废黜过时总结。",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.groupCollapsed(
|
||||
`[Amily2号-存档任务-v19.0 最终版] ${new Date().toLocaleTimeString()}`,
|
||||
);
|
||||
console.time("总结写入总耗时");
|
||||
|
||||
try {
|
||||
const chatIdentifier = await getChatIdentifier();
|
||||
const character = characters[context.characterId];
|
||||
let targetLorebookName = null;
|
||||
let isNewBook = false;
|
||||
|
||||
switch (targetLorebookFromPending) {
|
||||
case "character_main":
|
||||
targetLorebookName = character?.data?.extensions?.world;
|
||||
if (!targetLorebookName) {
|
||||
toastr.warning(
|
||||
"角色未绑定主世界书,总结写入任务已中止。",
|
||||
"Amily2号",
|
||||
);
|
||||
console.groupEnd();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "dedicated":
|
||||
targetLorebookName = `${DEDICATED_LOREBOOK_NAME}-${chatIdentifier}`;
|
||||
break;
|
||||
default:
|
||||
toastr.error(
|
||||
`收到未知的写入指令: "${targetLorebookFromPending}"`,
|
||||
"Amily2号",
|
||||
);
|
||||
console.groupEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!world_names.includes(targetLorebookName)) {
|
||||
await createNewWorldInfo(targetLorebookName);
|
||||
isNewBook = true;
|
||||
}
|
||||
|
||||
const uniqueLoreName = `${LOREBOOK_PREFIX}${chatIdentifier}`;
|
||||
const bookData = await loadWorldInfo(targetLorebookName);
|
||||
if (!bookData) {
|
||||
toastr.error(`无法加载世界书《${targetLorebookName}》`, "Amily2号");
|
||||
console.groupEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
const existingEntry = Object.values(bookData.entries).find(
|
||||
(e) => e.comment === uniqueLoreName && !e.disable,
|
||||
);
|
||||
|
||||
if (existingEntry) {
|
||||
const existingContent = existingEntry.content
|
||||
.replace(INTRODUCTORY_TEXT, "")
|
||||
.trim();
|
||||
const lines = existingContent ? existingContent.split("\n") : [];
|
||||
const nextNumber = lines.length + 1;
|
||||
existingEntry.content += `\n${nextNumber}. ${summaryToCommit}`;
|
||||
} else {
|
||||
const newEntry = createWorldInfoEntry(targetLorebookName, bookData);
|
||||
Object.assign(newEntry, {
|
||||
comment: uniqueLoreName,
|
||||
content: `${INTRODUCTORY_TEXT}1. ${summaryToCommit}`,
|
||||
key: [chatIdentifier, "Amily2", "总结"],
|
||||
disable: false,
|
||||
});
|
||||
}
|
||||
|
||||
await saveWorldInfo(targetLorebookName, bookData, true);
|
||||
console.log(`[史官司] 总结已遵旨写入《${targetLorebookName}》文件。`);
|
||||
|
||||
if (isNewBook) {
|
||||
await refreshWorldbookListOnly(targetLorebookName);
|
||||
toastr.success(
|
||||
`已创建并写入新档案《${targetLorebookName}》!`,
|
||||
"Amily2号",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Amily2号-写入失败] 写入流程发生意外错误:", error);
|
||||
toastr.error("后台写入总结时发生错误。", "Amily2号");
|
||||
} finally {
|
||||
console.timeEnd("总结写入总耗时");
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user