Update from local source

This commit is contained in:
Cola-Echo
2026-02-04 10:33:49 +08:00
parent 84dc04ef61
commit 1fd223930d
78 changed files with 28619 additions and 83 deletions

321
src/worldbook/api.js Normal file
View 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
View 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
View 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
View 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
View 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;
}