mirror of
https://github.com/Cola-Echo/memory-manager-concurrent.git
synced 2026-06-06 05:25:53 +00:00
Update from local source
This commit is contained in:
321
src/worldbook/api.js
Normal file
321
src/worldbook/api.js
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 世界书 API 模块
|
||||
* @module worldbook/api
|
||||
*/
|
||||
|
||||
import Logger from '@core/logger';
|
||||
import { getContext, getWorldNames, loadWorldInfo } from '@core/sillytavern-api';
|
||||
import { getImportedBookNames } from '@config/imported-books';
|
||||
import { parseWorldBook } from './parser';
|
||||
|
||||
/**
|
||||
* 获取酒馆中所有可用的世界书列表(包括未启用的)
|
||||
* @returns {Promise<Array<string>>} 世界书名称数组
|
||||
*/
|
||||
export async function getAllAvailableWorldBooks() {
|
||||
try {
|
||||
// 方法1: 使用 SillyTavern Context API(官方推荐)
|
||||
const worldNames = getWorldNames();
|
||||
if (worldNames && worldNames.length > 0) {
|
||||
return [...worldNames];
|
||||
}
|
||||
|
||||
// 方法2: 从 DOM 中提取世界书列表(从世界书选择下拉框)
|
||||
const worldInfoSelect = document.getElementById("world_info");
|
||||
if (worldInfoSelect) {
|
||||
const options = worldInfoSelect.querySelectorAll("option");
|
||||
const names = [];
|
||||
options.forEach((opt) => {
|
||||
const name = opt.textContent?.trim() || opt.text?.trim();
|
||||
if (name && name !== "" && name !== "None" && name !== "— None —") {
|
||||
names.push(name);
|
||||
}
|
||||
});
|
||||
if (names.length > 0) {
|
||||
return names;
|
||||
}
|
||||
}
|
||||
|
||||
// 方法3: 从角色世界书选择框提取
|
||||
const charWorldSelect = document.getElementById("character_world");
|
||||
if (charWorldSelect) {
|
||||
const options = charWorldSelect.querySelectorAll("option");
|
||||
const names = [];
|
||||
options.forEach((opt) => {
|
||||
const name = opt.textContent?.trim() || opt.text?.trim();
|
||||
if (name && name !== "" && name !== "None" && name !== "— None —") {
|
||||
names.push(name);
|
||||
}
|
||||
});
|
||||
if (names.length > 0) {
|
||||
return names;
|
||||
}
|
||||
}
|
||||
|
||||
// 方法4: 尝试通过 jQuery 选择器
|
||||
if (typeof jQuery !== "undefined" || typeof $ !== "undefined") {
|
||||
const jq = typeof jQuery !== "undefined" ? jQuery : $;
|
||||
const $select = jq("#world_info, #character_world");
|
||||
if ($select.length > 0) {
|
||||
const names = [];
|
||||
$select.first().find("option").each(function() {
|
||||
const name = jq(this).text().trim();
|
||||
if (name && name !== "" && name !== "None" && name !== "— None —") {
|
||||
names.push(name);
|
||||
}
|
||||
});
|
||||
if (names.length > 0) {
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 方法5: 尝试通过 SillyTavern REST API 获取
|
||||
try {
|
||||
let headers = { "Content-Type": "application/json" };
|
||||
const context = getContext();
|
||||
if (context && typeof context.getRequestHeaders === "function") {
|
||||
headers = context.getRequestHeaders();
|
||||
}
|
||||
|
||||
const response = await fetch("/api/worldinfo/get", {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && Array.isArray(data)) {
|
||||
const names = data.map((item) => item.name || item).filter((n) => n);
|
||||
if (names.length > 0) {
|
||||
return names;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (apiErr) {
|
||||
// 忽略API错误,继续尝试其他方法
|
||||
}
|
||||
|
||||
// 方法6: 尝试获取全局世界书列表
|
||||
if (typeof window !== 'undefined' && typeof window.selected_world_info !== "undefined") {
|
||||
if (Array.isArray(window.selected_world_info)) {
|
||||
return [...window.selected_world_info];
|
||||
}
|
||||
}
|
||||
|
||||
Logger.warn("无法获取世界书列表,请确保 SillyTavern 已完全加载");
|
||||
return [];
|
||||
} catch (e) {
|
||||
Logger.error("获取世界书列表失败:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界书列表(快速版,不加载条目数量)
|
||||
* @returns {Promise<Array<{name: string, entryCount: number}>>}
|
||||
*/
|
||||
export async function getWorldBookList() {
|
||||
try {
|
||||
const worldBookNames = await getAllAvailableWorldBooks();
|
||||
// 快速返回,不加载每个世界书的条目数量
|
||||
return worldBookNames.map((name) => ({ name, entryCount: -1 }));
|
||||
} catch (e) {
|
||||
Logger.error("获取世界书列表失败:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过名称加载世界书内容
|
||||
* @param {string} name 世界书名称
|
||||
* @returns {Promise<object|null>} 世界书数据
|
||||
*/
|
||||
export async function loadWorldBookByName(name) {
|
||||
try {
|
||||
// 优先使用官方 API
|
||||
const book = await loadWorldInfo(name);
|
||||
if (book) {
|
||||
return { name, ...book };
|
||||
}
|
||||
|
||||
// 备用方案:通过 API 获取
|
||||
let headers = { "Content-Type": "application/json" };
|
||||
const context = getContext();
|
||||
if (context && typeof context.getRequestHeaders === "function") {
|
||||
headers = context.getRequestHeaders();
|
||||
}
|
||||
|
||||
const response = await fetch("/api/worldinfo/get", {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.entries) {
|
||||
return { name, ...data };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
Logger.error(`加载世界书 "${name}" 失败:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界书条目数量(延迟加载)
|
||||
* @param {string} bookName 世界书名称
|
||||
* @returns {Promise<number>} 条目数量
|
||||
*/
|
||||
export async function getWorldBookEntryCount(bookName) {
|
||||
try {
|
||||
const bookData = await loadWorldBookByName(bookName);
|
||||
return bookData?.entries ? Object.keys(bookData.entries).length : 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界书条目列表
|
||||
* @param {string} bookName 世界书名称
|
||||
* @returns {Promise<Array>} 条目数组
|
||||
*/
|
||||
export async function getWorldBookEntries(bookName) {
|
||||
try {
|
||||
const bookData = await loadWorldBookByName(bookName);
|
||||
if (!bookData || !bookData.entries) {
|
||||
return [];
|
||||
}
|
||||
return Object.values(bookData.entries);
|
||||
} catch (e) {
|
||||
Logger.error(`获取世界书 "${bookName}" 条目失败:`, e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已导入的世界书数据
|
||||
* @returns {Promise<Array<object>>} 世界书数据数组
|
||||
*/
|
||||
export async function getImportedWorldBooks() {
|
||||
const bookNames = getImportedBookNames();
|
||||
const books = [];
|
||||
|
||||
for (const name of bookNames) {
|
||||
const book = await loadWorldBookByName(name);
|
||||
if (book) {
|
||||
books.push(book);
|
||||
}
|
||||
}
|
||||
|
||||
return books;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断世界书是否是总结类型
|
||||
* @param {string} bookName 世界书名称
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSummaryBook(bookName) {
|
||||
// 根据命名规则判断
|
||||
return (
|
||||
bookName.includes("敕史局") ||
|
||||
bookName.includes("Summary") ||
|
||||
bookName.includes("summary") ||
|
||||
bookName.includes("Lore-char") ||
|
||||
bookName.includes("lore-char") ||
|
||||
bookName.includes("总结") ||
|
||||
bookName.includes("汇总") ||
|
||||
bookName.includes("归纳")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断世界书是否是记忆类型
|
||||
* @param {object|string} bookOrName 世界书对象或名称
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isMemoryBook(bookOrName) {
|
||||
// 如果传入的是对象(世界书),解析它
|
||||
if (typeof bookOrName === 'object' && bookOrName !== null) {
|
||||
const parsed = parseWorldBook(bookOrName);
|
||||
return Object.keys(parsed.categories).length > 0;
|
||||
}
|
||||
// 如果传入的是字符串(书名),使用简单判断
|
||||
return !isSummaryBook(bookOrName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类世界书
|
||||
* @param {Array<object>} worldBooks 世界书数据数组
|
||||
* @returns {object} { memoryBooks: [], summaryBooks: [], unknownBooks: [] }
|
||||
*/
|
||||
export function classifyWorldBooks(worldBooks) {
|
||||
const memoryBooks = [];
|
||||
const summaryBooks = [];
|
||||
const unknownBooks = [];
|
||||
|
||||
for (const book of worldBooks) {
|
||||
const name = book.name || "";
|
||||
|
||||
// 先检查书名
|
||||
let isSummary = isSummaryBook(name);
|
||||
|
||||
// 如果书名没有匹配,再检查条目的 comment 是否包含 '敕史局'
|
||||
if (!isSummary && book.entries) {
|
||||
for (const [uid, entry] of Object.entries(book.entries)) {
|
||||
const comment = entry.comment || "";
|
||||
if (comment.includes("敕史局")) {
|
||||
isSummary = true;
|
||||
Logger.debug(
|
||||
`世界书 "${name}" 通过条目comment识别为总结类型`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSummary) {
|
||||
summaryBooks.push(book);
|
||||
Logger.debug(`世界书 "${name}" 识别为总结类型`);
|
||||
} else {
|
||||
const parsed = parseWorldBook(book);
|
||||
const categoryCount = Object.keys(parsed.categories).length;
|
||||
// 检查是否有非"未分类"的分类
|
||||
const hasValidCategories = Object.keys(parsed.categories).some(
|
||||
(c) => c !== "未分类",
|
||||
);
|
||||
|
||||
if (categoryCount > 0 && hasValidCategories) {
|
||||
memoryBooks.push({
|
||||
book,
|
||||
categories: parsed.categories,
|
||||
});
|
||||
Logger.debug(
|
||||
`世界书 "${name}" 识别为记忆类型,分类: ${Object.keys(
|
||||
parsed.categories,
|
||||
).join(", ")}`,
|
||||
);
|
||||
} else if (categoryCount > 0) {
|
||||
// 有条目但都是未分类,也作为记忆世界书处理
|
||||
memoryBooks.push({
|
||||
book,
|
||||
categories: parsed.categories,
|
||||
});
|
||||
Logger.debug(`世界书 "${name}" 作为未分类记忆世界书处理`);
|
||||
} else {
|
||||
unknownBooks.push(book);
|
||||
Logger.warn(
|
||||
`世界书 "${name}" 无法识别类型(无启用的条目)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { memoryBooks, summaryBooks, unknownBooks };
|
||||
}
|
||||
42
src/worldbook/index.js
Normal file
42
src/worldbook/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 世界书模块导出
|
||||
* @module worldbook
|
||||
*/
|
||||
|
||||
export {
|
||||
parseWorldBook,
|
||||
parseOldBracketFormat,
|
||||
formatAsWorldBook,
|
||||
getSummaryContent,
|
||||
getCategories,
|
||||
getCategoryEntries,
|
||||
} from './parser';
|
||||
|
||||
export {
|
||||
getAllAvailableWorldBooks,
|
||||
getWorldBookList,
|
||||
loadWorldBookByName,
|
||||
getWorldBookEntryCount,
|
||||
getWorldBookEntries,
|
||||
getImportedWorldBooks,
|
||||
isSummaryBook,
|
||||
isMemoryBook,
|
||||
classifyWorldBooks,
|
||||
} from './api';
|
||||
|
||||
export {
|
||||
refreshWorldBookList,
|
||||
getWorldBooksCache,
|
||||
clearWorldBooksCache,
|
||||
} from './refresh';
|
||||
|
||||
// 更新列表模块
|
||||
export {
|
||||
addUpdates,
|
||||
renderUpdatesList,
|
||||
clearUpdatesList,
|
||||
startWorldBookPolling,
|
||||
stopWorldBookPolling,
|
||||
getUpdatesList,
|
||||
resetWorldBooksSnapshot,
|
||||
} from './updates';
|
||||
176
src/worldbook/parser.js
Normal file
176
src/worldbook/parser.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 世界书解析模块
|
||||
* @module worldbook/parser
|
||||
*/
|
||||
|
||||
import Logger from '@core/logger';
|
||||
import { getGlobalSettings } from '@config/config-manager';
|
||||
|
||||
/**
|
||||
* 解析旧的方括号格式
|
||||
* @param {string} comment 注释内容
|
||||
* @returns {object|null} { category, isIndex } 或 null
|
||||
*/
|
||||
export function parseOldBracketFormat(comment) {
|
||||
if (!comment) return null;
|
||||
|
||||
// 格式: 【XXX】xxx (必须以【开头)
|
||||
const oldMatch = comment.match(/^【([^】]+)】/);
|
||||
if (oldMatch) {
|
||||
return {
|
||||
category: oldMatch[1].trim(),
|
||||
isIndex: comment.toLowerCase().includes("[index]")
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析世界书结构
|
||||
* @param {object} book 世界书对象
|
||||
* @returns {object} { categories: { [categoryName]: { index: [], details: [] } } }
|
||||
*/
|
||||
export function parseWorldBook(book) {
|
||||
if (!book || !book.entries) return { categories: {} };
|
||||
|
||||
const categories = {};
|
||||
|
||||
for (const [uid, entry] of Object.entries(book.entries)) {
|
||||
// SillyTavern 使用 disable 字段(true 表示禁用),而不是 enabled
|
||||
// 如果 disable 为 true,跳过该条目
|
||||
if (entry.disable === true) continue;
|
||||
|
||||
const comment = entry.comment || "";
|
||||
|
||||
// 识别分类名称,支持多种格式:
|
||||
// 1. "[Amily2] Index for 角色表" -> 分类: 角色表, 类型: index
|
||||
// 2. "[Amily2] Detail: 角色表 - 江晦" -> 分类: 角色表, 类型: detail
|
||||
// 3. "【角色表】xxx" -> 分类: 角色表(旧格式兼容)
|
||||
|
||||
let category = "未分类";
|
||||
let isIndex = false;
|
||||
|
||||
// 格式1: Index for XXX
|
||||
const indexMatch = comment.match(/Index\s+for\s+(.+?)(?:\s*$|\s*[.\[])/i);
|
||||
if (indexMatch) {
|
||||
category = indexMatch[1].trim();
|
||||
isIndex = true;
|
||||
} else {
|
||||
// 格式2: Detail: XXX - YYY
|
||||
const detailMatch = comment.match(/Detail:\s*(.+?)\s*-\s*/i);
|
||||
if (detailMatch) {
|
||||
category = detailMatch[1].trim();
|
||||
isIndex = false;
|
||||
} else {
|
||||
// 格式3: 【XXX】(旧格式兼容)
|
||||
const oldFormat = parseOldBracketFormat(comment);
|
||||
if (oldFormat) {
|
||||
category = oldFormat.category;
|
||||
isIndex = oldFormat.isIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!categories[category]) {
|
||||
categories[category] = { index: [], details: [] };
|
||||
}
|
||||
|
||||
if (isIndex) {
|
||||
categories[category].index.push({
|
||||
uid,
|
||||
comment,
|
||||
content: entry.content,
|
||||
keys: entry.key || [],
|
||||
});
|
||||
} else {
|
||||
categories[category].details.push({
|
||||
uid,
|
||||
comment,
|
||||
content: entry.content,
|
||||
keys: entry.key || [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { categories };
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化为世界书内容字符串
|
||||
* @param {Array} indexEntries 索引条目数组
|
||||
* @param {Array} detailEntries 详情条目数组
|
||||
* @returns {string} 格式化后的内容
|
||||
*/
|
||||
export function formatAsWorldBook(indexEntries, detailEntries) {
|
||||
let result = "";
|
||||
const settings = getGlobalSettings();
|
||||
const sendIndexOnly = settings.sendIndexOnly === true;
|
||||
|
||||
if (indexEntries && indexEntries.length > 0) {
|
||||
result += "=== Index ===\n";
|
||||
for (const entry of indexEntries) {
|
||||
result += `[${entry.comment}]\n${entry.content}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sendIndexOnly && detailEntries && detailEntries.length > 0) {
|
||||
result += "=== Details ===\n";
|
||||
for (const entry of detailEntries) {
|
||||
let categoryName = "档案";
|
||||
const categoryMatch = entry.comment?.match(/Detail:\s*([^-]+)\s*-/i);
|
||||
if (categoryMatch) {
|
||||
categoryName = categoryMatch[1].trim();
|
||||
}
|
||||
|
||||
const keyword = entry.keys && entry.keys.length > 0 ? entry.keys[0] : "";
|
||||
|
||||
if (keyword) {
|
||||
result += `【${categoryName}档案: ${keyword}】\n`;
|
||||
}
|
||||
|
||||
result += `[${entry.comment}]\n${entry.content}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取总结世界书的内容
|
||||
* @param {object} book 世界书对象
|
||||
* @returns {string} 世界书内容
|
||||
*/
|
||||
export function getSummaryContent(book) {
|
||||
if (!book || !book.entries) return "";
|
||||
|
||||
let content = "";
|
||||
for (const [uid, entry] of Object.entries(book.entries)) {
|
||||
// SillyTavern 世界书使用 disable 字段(true=禁用,false=启用)
|
||||
// 兼容两种字段名:disable 和 enabled
|
||||
const isDisabled = entry.disable === true || entry.enabled === false;
|
||||
if (isDisabled) continue;
|
||||
content += entry.content + "\n\n";
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界书中的所有分类名称
|
||||
* @param {object} book 世界书对象
|
||||
* @returns {Array<string>} 分类名称数组
|
||||
*/
|
||||
export function getCategories(book) {
|
||||
const parsed = parseWorldBook(book);
|
||||
return Object.keys(parsed.categories);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定分类的条目
|
||||
* @param {object} book 世界书对象
|
||||
* @param {string} category 分类名称
|
||||
* @returns {object} { index: [], details: [] }
|
||||
*/
|
||||
export function getCategoryEntries(book, category) {
|
||||
const parsed = parseWorldBook(book);
|
||||
return parsed.categories[category] || { index: [], details: [] };
|
||||
}
|
||||
282
src/worldbook/refresh.js
Normal file
282
src/worldbook/refresh.js
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 世界书刷新模块
|
||||
* @module worldbook/refresh
|
||||
*/
|
||||
|
||||
import Logger from '@core/logger';
|
||||
import { loadConfig } from '@config/config-manager';
|
||||
import { getImportedWorldBooks, classifyWorldBooks } from './api';
|
||||
|
||||
// 世界书缓存
|
||||
let worldBooksCache = [];
|
||||
let worldBooksSnapshot = null;
|
||||
|
||||
/**
|
||||
* 创建世界书快照(用于变化检测)
|
||||
* @param {Array} worldBooks 世界书数组
|
||||
* @returns {object} 快照对象
|
||||
*/
|
||||
function createWorldBooksSnapshot(worldBooks) {
|
||||
const snapshot = {};
|
||||
for (const book of worldBooks) {
|
||||
const entries = {};
|
||||
if (book.entries) {
|
||||
for (const [uid, entry] of Object.entries(book.entries)) {
|
||||
entries[uid] = {
|
||||
content: entry.content,
|
||||
comment: entry.comment,
|
||||
disable: entry.disable,
|
||||
};
|
||||
}
|
||||
}
|
||||
snapshot[book.name] = {
|
||||
entryCount: Object.keys(entries).length,
|
||||
entries,
|
||||
};
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测世界书变化
|
||||
* @param {object} oldSnapshot 旧快照
|
||||
* @param {object} newSnapshot 新快照
|
||||
* @returns {Array} 变化列表
|
||||
*/
|
||||
function detectWorldBookChanges(oldSnapshot, newSnapshot) {
|
||||
const changes = [];
|
||||
|
||||
// 检查新增的世界书
|
||||
for (const bookName of Object.keys(newSnapshot)) {
|
||||
if (!oldSnapshot[bookName]) {
|
||||
changes.push({ type: 'added', bookName });
|
||||
}
|
||||
}
|
||||
|
||||
// 检查删除的世界书
|
||||
for (const bookName of Object.keys(oldSnapshot)) {
|
||||
if (!newSnapshot[bookName]) {
|
||||
changes.push({ type: 'removed', bookName });
|
||||
}
|
||||
}
|
||||
|
||||
// 检查修改的世界书
|
||||
for (const bookName of Object.keys(newSnapshot)) {
|
||||
if (oldSnapshot[bookName]) {
|
||||
const oldBook = oldSnapshot[bookName];
|
||||
const newBook = newSnapshot[bookName];
|
||||
|
||||
if (oldBook.entryCount !== newBook.entryCount) {
|
||||
changes.push({
|
||||
type: 'modified',
|
||||
bookName,
|
||||
detail: `条目数量变化: ${oldBook.entryCount} -> ${newBook.entryCount}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界书统计信息
|
||||
* @param {Array} worldBooks 世界书数组
|
||||
* @returns {object} 统计信息
|
||||
*/
|
||||
function getWorldBookStats(worldBooks) {
|
||||
return {
|
||||
totalBooks: worldBooks.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义 HTML,防止 XSS 攻击
|
||||
* @param {string} text 原始文本
|
||||
* @returns {string} 转义后的文本
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新世界书列表
|
||||
*/
|
||||
export async function refreshWorldBookList() {
|
||||
const listContainer = document.getElementById("mm-worldbook-list");
|
||||
const countBadge = document.getElementById("mm-book-count");
|
||||
|
||||
if (!listContainer) return;
|
||||
|
||||
listContainer.innerHTML =
|
||||
'<div class="mm-loading"><i class="fa-solid fa-spinner fa-spin"></i> 加载中...</div>';
|
||||
|
||||
try {
|
||||
worldBooksCache = await getImportedWorldBooks();
|
||||
|
||||
// 变化检测
|
||||
const newSnapshot = createWorldBooksSnapshot(worldBooksCache);
|
||||
if (worldBooksSnapshot) {
|
||||
const changes = detectWorldBookChanges(worldBooksSnapshot, newSnapshot);
|
||||
if (changes.length > 0) {
|
||||
Logger.debug("世界书变化:", changes);
|
||||
}
|
||||
}
|
||||
worldBooksSnapshot = newSnapshot;
|
||||
|
||||
const { memoryBooks, summaryBooks, unknownBooks } = classifyWorldBooks(worldBooksCache);
|
||||
const stats = getWorldBookStats(worldBooksCache);
|
||||
|
||||
if (countBadge) countBadge.textContent = stats.totalBooks;
|
||||
|
||||
if (worldBooksCache.length === 0) {
|
||||
listContainer.innerHTML = `
|
||||
<div class="mm-empty-state">
|
||||
<i class="fa-solid fa-book"></i>
|
||||
<p>暂无已导入的世界书</p>
|
||||
<p class="mm-hint">点击"导入世界书"按钮选择要处理的世界书</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
let html = "";
|
||||
|
||||
if (memoryBooks.length > 0) {
|
||||
html += '<div class="mm-book-group">';
|
||||
html += '<div class="mm-book-group-title">记忆世界书</div>';
|
||||
for (const { book, categories } of memoryBooks) {
|
||||
const safeBookName = escapeHtml(book.name);
|
||||
html += `<div class="mm-book-card" data-book="${safeBookName}">`;
|
||||
html += `<div class="mm-book-title">`;
|
||||
html += `<span class="mm-book-name">${safeBookName}</span>`;
|
||||
html += `<button class="mm-btn mm-btn-xs mm-btn-danger" data-action="remove-book" data-book="${safeBookName}" title="移除">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>`;
|
||||
html += `</div>`;
|
||||
html += '<div class="mm-chips-container">';
|
||||
for (const [category, data] of Object.entries(categories)) {
|
||||
const indexCount = data.index?.length || 0;
|
||||
const detailCount = data.details?.length || 0;
|
||||
const totalCount = indexCount + detailCount;
|
||||
const categoryConfig = config?.memoryConfigs?.[category];
|
||||
const hasConfig = !!categoryConfig;
|
||||
const keywordsCount = categoryConfig?.maxKeywords || 10;
|
||||
const relevanceThreshold = categoryConfig?.relevanceThreshold || 0.6;
|
||||
const apiModel = escapeHtml(categoryConfig?.model || "未配置");
|
||||
const statusClass = hasConfig ? "mm-chip-ok" : "mm-chip-warning";
|
||||
|
||||
const safeCategory = escapeHtml(category);
|
||||
html += `
|
||||
<div class="mm-chip ${statusClass}"
|
||||
data-action="edit-config"
|
||||
data-category="${safeCategory}"
|
||||
data-type="memory"
|
||||
title="条目: ${totalCount} | 关键词: ${keywordsCount} | 阈值: ${relevanceThreshold} | 模型: ${apiModel}">
|
||||
<span class="mm-chip-name">${safeCategory}</span>
|
||||
<span class="mm-chip-count">${totalCount}</span>
|
||||
</div>`;
|
||||
}
|
||||
html += "</div></div>";
|
||||
}
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
if (summaryBooks.length > 0) {
|
||||
html += '<div class="mm-book-group">';
|
||||
html += '<div class="mm-book-group-title">总结世界书</div>';
|
||||
for (const book of summaryBooks) {
|
||||
const bookConfig = config?.summaryConfigs?.[book.name];
|
||||
const hasConfig = !!bookConfig;
|
||||
const eventsCount = bookConfig?.maxHistoryEvents || 15;
|
||||
const relevanceThreshold = bookConfig?.relevanceThreshold || 0.6;
|
||||
const apiModel = escapeHtml(bookConfig?.model || "未配置");
|
||||
const entryCount = book.entries ? Object.keys(book.entries).length : 0;
|
||||
const statusClass = hasConfig ? "mm-chip-ok" : "mm-chip-warning";
|
||||
|
||||
const safeBookName = escapeHtml(book.name);
|
||||
html += `
|
||||
<div class="mm-book-card">
|
||||
<div class="mm-book-title">
|
||||
<div class="mm-chip ${statusClass}"
|
||||
data-action="edit-config"
|
||||
data-category="${safeBookName}"
|
||||
data-type="summary"
|
||||
title="条目: ${entryCount} | 事件: ${eventsCount} | 阈值: ${relevanceThreshold} | 模型: ${apiModel}">
|
||||
<span class="mm-chip-name">${safeBookName}</span>
|
||||
<span class="mm-chip-count">${entryCount}</span>
|
||||
</div>
|
||||
<button class="mm-btn mm-btn-xs mm-btn-danger" data-action="remove-book" data-book="${safeBookName}" title="移除">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
// 未识别类型的世界书
|
||||
if (unknownBooks.length > 0) {
|
||||
html += '<div class="mm-book-group">';
|
||||
html += '<div class="mm-book-group-title">未识别的世界书</div>';
|
||||
for (const book of unknownBooks) {
|
||||
const entryCount = book.entries ? Object.keys(book.entries).length : 0;
|
||||
const enabledCount = book.entries
|
||||
? Object.values(book.entries).filter((e) => e.disable !== true).length
|
||||
: 0;
|
||||
|
||||
const safeBookName = escapeHtml(book.name);
|
||||
html += `
|
||||
<div class="mm-book-card">
|
||||
<div class="mm-book-title">
|
||||
<span class="mm-book-name">${safeBookName}</span>
|
||||
<button class="mm-btn mm-btn-xs mm-btn-danger" data-action="remove-book" data-book="${safeBookName}" title="移除">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mm-chips-container">
|
||||
<div class="mm-chip mm-chip-warning">
|
||||
<span class="mm-chip-name">条目</span>
|
||||
<span class="mm-chip-count">${entryCount}</span>
|
||||
</div>
|
||||
<div class="mm-chip">
|
||||
<span class="mm-chip-name">启用</span>
|
||||
<span class="mm-chip-count">${enabledCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mm-hint" style="margin: 10px 0 0; font-size: 12px;">
|
||||
无法识别类型。请确保条目的 comment 字段包含【分类名】格式
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
html += "</div>";
|
||||
}
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
} catch (error) {
|
||||
Logger.error("刷新世界书列表失败:", error);
|
||||
listContainer.innerHTML = `
|
||||
<div class="mm-error-state">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
<p>加载失败: ${error.message}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取世界书缓存
|
||||
* @returns {Array} 世界书缓存数组
|
||||
*/
|
||||
export function getWorldBooksCache() {
|
||||
return worldBooksCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除世界书缓存
|
||||
*/
|
||||
export function clearWorldBooksCache() {
|
||||
worldBooksCache = [];
|
||||
worldBooksSnapshot = null;
|
||||
}
|
||||
208
src/worldbook/updates.js
Normal file
208
src/worldbook/updates.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 世界书更新列表模块
|
||||
* @module worldbook/updates
|
||||
*/
|
||||
|
||||
import Logger from '@core/logger';
|
||||
import { getImportedWorldBooks } from './api';
|
||||
|
||||
// 更新记录列表
|
||||
let updatesList = [];
|
||||
|
||||
// 世界书轮询定时器
|
||||
let worldBookPollingTimer = null;
|
||||
const POLLING_INTERVAL = 5000; // 5秒轮询一次
|
||||
|
||||
// 世界书快照(用于变化检测)
|
||||
let worldBooksSnapshot = null;
|
||||
|
||||
/**
|
||||
* 创建世界书快照
|
||||
* @param {Array} worldBooks 世界书数组
|
||||
* @returns {object} 快照对象
|
||||
*/
|
||||
function createWorldBooksSnapshot(worldBooks) {
|
||||
const snapshot = {};
|
||||
for (const book of worldBooks) {
|
||||
const entries = {};
|
||||
if (book.entries) {
|
||||
for (const [uid, entry] of Object.entries(book.entries)) {
|
||||
entries[uid] = {
|
||||
content: entry.content,
|
||||
comment: entry.comment,
|
||||
disable: entry.disable,
|
||||
};
|
||||
}
|
||||
}
|
||||
snapshot[book.name] = {
|
||||
entryCount: Object.keys(entries).length,
|
||||
entries,
|
||||
};
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测世界书变化
|
||||
* @param {object} oldSnapshot 旧快照
|
||||
* @param {object} newSnapshot 新快照
|
||||
* @returns {Array} 变化列表
|
||||
*/
|
||||
function detectWorldBookChanges(oldSnapshot, newSnapshot) {
|
||||
const changes = [];
|
||||
|
||||
// 检查新增的世界书
|
||||
for (const bookName of Object.keys(newSnapshot)) {
|
||||
if (!oldSnapshot[bookName]) {
|
||||
changes.push({ type: 'added', bookName });
|
||||
}
|
||||
}
|
||||
|
||||
// 检查删除的世界书
|
||||
for (const bookName of Object.keys(oldSnapshot)) {
|
||||
if (!newSnapshot[bookName]) {
|
||||
changes.push({ type: 'removed', bookName });
|
||||
}
|
||||
}
|
||||
|
||||
// 检查修改的世界书
|
||||
for (const bookName of Object.keys(newSnapshot)) {
|
||||
if (oldSnapshot[bookName]) {
|
||||
const oldBook = oldSnapshot[bookName];
|
||||
const newBook = newSnapshot[bookName];
|
||||
|
||||
if (oldBook.entryCount !== newBook.entryCount) {
|
||||
changes.push({
|
||||
type: 'modified',
|
||||
bookName,
|
||||
detail: `条目数量变化: ${oldBook.entryCount} -> ${newBook.entryCount}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加更新记录
|
||||
* @param {Array} changes 变化列表
|
||||
*/
|
||||
export function addUpdates(changes) {
|
||||
if (changes.length === 0) return;
|
||||
|
||||
// 将新变化添加到列表开头
|
||||
updatesList = [...changes, ...updatesList].slice(0, 50); // 最多保留50条
|
||||
renderUpdatesList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染更新列表
|
||||
*/
|
||||
export function renderUpdatesList() {
|
||||
const container = document.getElementById("mm-updates-list");
|
||||
const clearBtn = document.getElementById("mm-clear-updates-btn");
|
||||
if (!container) return;
|
||||
|
||||
if (updatesList.length === 0) {
|
||||
container.innerHTML = '<div class="mm-empty-hint">暂无更新记录</div>';
|
||||
if (clearBtn) clearBtn.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
if (clearBtn) clearBtn.style.display = "inline-flex";
|
||||
|
||||
const html = updatesList
|
||||
.map((change) => {
|
||||
const typeClass = {
|
||||
added: "mm-update-added",
|
||||
removed: "mm-update-removed",
|
||||
modified: "mm-update-modified",
|
||||
}[change.type] || "";
|
||||
|
||||
const typeText = {
|
||||
added: "新增",
|
||||
removed: "移除",
|
||||
modified: "修改",
|
||||
}[change.type] || "变化";
|
||||
|
||||
return `
|
||||
<div class="mm-update-item ${typeClass}">
|
||||
<span class="mm-update-type">${typeText}</span>
|
||||
<span class="mm-update-book">${change.bookName}</span>
|
||||
${change.detail ? `<span class="mm-update-detail">${change.detail}</span>` : ""}
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空更新列表
|
||||
*/
|
||||
export function clearUpdatesList() {
|
||||
updatesList = [];
|
||||
renderUpdatesList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动世界书轮询
|
||||
*/
|
||||
export function startWorldBookPolling() {
|
||||
if (worldBookPollingTimer) return; // 已经在运行
|
||||
|
||||
worldBookPollingTimer = setInterval(async () => {
|
||||
// 只在面板可见时轮询
|
||||
const panel = document.getElementById("memory-manager-panel");
|
||||
if (!panel || !panel.classList.contains("mm-panel-visible")) return;
|
||||
|
||||
try {
|
||||
const currentBooks = await getImportedWorldBooks();
|
||||
if (currentBooks.length === 0) return;
|
||||
|
||||
const newSnapshot = createWorldBooksSnapshot(currentBooks);
|
||||
|
||||
if (worldBooksSnapshot) {
|
||||
const changes = detectWorldBookChanges(worldBooksSnapshot, newSnapshot);
|
||||
if (changes.length > 0) {
|
||||
Logger.log("轮询检测到世界书变化:", changes);
|
||||
addUpdates(changes);
|
||||
}
|
||||
}
|
||||
|
||||
worldBooksSnapshot = newSnapshot;
|
||||
} catch (error) {
|
||||
Logger.error("轮询检测世界书变化失败:", error);
|
||||
}
|
||||
}, POLLING_INTERVAL);
|
||||
|
||||
Logger.log("世界书轮询已启动");
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止世界书轮询
|
||||
*/
|
||||
export function stopWorldBookPolling() {
|
||||
if (worldBookPollingTimer) {
|
||||
clearInterval(worldBookPollingTimer);
|
||||
worldBookPollingTimer = null;
|
||||
Logger.log("世界书轮询已停止");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更新列表
|
||||
* @returns {Array} 更新列表
|
||||
*/
|
||||
export function getUpdatesList() {
|
||||
return updatesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置世界书快照
|
||||
*/
|
||||
export function resetWorldBooksSnapshot() {
|
||||
worldBooksSnapshot = null;
|
||||
}
|
||||
Reference in New Issue
Block a user