mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-13 19:45:50 +00:00
### 新功能
- **翰林院向量化质量升级**:
- **边界感知切块**:替换四个来源(聊天记录/小说/世界书/手动)的纯字符硬切——优先在段落边界断开,其次句末标点(含中文引号闭合),极端长串才硬切;句子/对话不再被拦腰截断,embedding 质量同步受益。仅影响新录入,已有向量无需重建
- **注入时序重排**:检索结果注入提示词前按时序重排(聊天记录按楼层、小说按卷/章/节——中文数字章节号可解析),rerank 只决定"选哪些块",不再决定呈现顺序;修复"不打不相识的剧情之后紧跟关系亲密"这类因按相关度排序导致的认知时间错乱
- **断层提示**:聊天记录相邻块楼层跳跃时自动插入"与上文相隔约 N 楼,并非连续发生"提示行,消除中间剧情缺失造成的割裂感
- **时间标识**:新录入的聊天记录块在来源标识中带上消息发送时间(ST 向量存储不持久化元数据,时间必须写入块文本才能在检索后取回;旧格式块兼容解析)
- **记忆块工作流(memory-blocks)**:剧情优化新增"自定义记忆块"体系——占位符驱动的并发工作流框架
- 在剧情优化面板「匹配替换 (sulv)」下方可增删自定义块:每个块定义一个占位符,执行剧情优化时主/拦截提示词中的占位符会被块的产出替换
- **静态块**:直接输出固定内容;**AI 调用块**:用所选 API 功能槽独立请求一次,把回复(或其中指定 `<标签>` 的内容)作为替换值
- 原有 sulv1-4 速率占位符迁入同一框架,行为与旧版逐字节一致
- 块定义为纯 JSON、随设置持久化,为后续导入导出与战斗系统接入预留扩展点
- 框架层新增**顺序拼接式 Chain**(`composeChain`):与占位符替换并列的第二种组合范式——同链的块并发执行后按 `order` 排序、以 `separator` 拼接并可选 `header/footer` 包裹,产出一个完整注入块;为记忆注入合成块与战斗系统"底部战报块"预留的承载结构,本版本暂无 UI 入口
- **渐进记忆(开发中功能,暂未对外开放)**:主菜单新增独立入口(点击提示"开发中,未来版本开放"),后续完善后放出。当前已落地的设计:
- 按"近期完整、远期摘要"的时间梯度,从指定表格(默认总结表,行序旧→新)采样历史并注入上下文:最新 X 行全量保留 + 其余历史对半拆分,较近一半等距取 Y 行、较远一半等距取 Z 行(中心对齐等距采样,不随机、不首尾加权,避免内容扎堆或事件结局被规律性忽略)
- 经 `setExtensionPrompt` 直接注入当回合上下文——内容独立、不写世界书、不随聊天/角色卡导出,生命周期天然跟随会话(区别于超级记忆的世界书条目路线)
- 注入位置 / 深度 / 角色 / 模板(含 `{{progressive_memory}}` 占位符)均可在面板配置;采样参数 X/Y/Z 默认 5/5/3,全部纯 JSON 持久化
- 采样器 `sampler.js` 为纯函数,参数结构与 memory-blocks 工作链对齐,后续可平移为 `progressive_sample` 节点
- **超级记忆 · 首行常驻**(表格专属配置新增开关,默认关闭):表格第一行通常是总调/全局定义行(基调、主线目标等),原先与普通行一样走绿灯——没人提到主键就永远不注入;开启后该行详情条目升为蓝灯常驻,切换即时生效
- **API 连接配置**:
- 角色世界书(cwb)与一键生卡(autoCharCard)纳入旧配置自动迁移:老用户首次加载会把旧 URL / Key / 模型自动迁移为连接配置并分配槽位(一键生卡仅在规划者与执行者配置一致或规划者为空时迁移,避免悄悄改变行为)
- **profile 已分配时参数控件 informational 化**:主面板 / 并发剧情优化 / 角色世界书 / 术语表的温度、maxTokens 控件在槽位分配 profile 后自动禁用并显示"由连接配置控制"提示,消除"改了没效果"的用户陷阱
- **profile 状态卡新增"本设备无 Key"警示**:API Key 仅保存在最初填写它的设备/浏览器上(安全设计,不随云端设置同步),换设备后状态卡会直接亮出警示徽标,不必等到调用报错才发现
### 修复
- **独立聊天记忆从摆设变真功能**:此前向量数据"随卡不随聊天"——开启"独立聊天记忆"后录入仍存进角色库、查询却去查一个从未被写入过的聊天集合、计数恒为 0,整体静默失效。现已重构为聊天级分桶:
- 独立模式下,聊天记录类向量按当前聊天隔离存储与检索,同一张卡开多个聊天(不同剧情线)的记忆互不污染
- 小说 / 世界书 / 手动录入属于"知识",仍随角色卡跨聊天共享;全局库不受影响
- 知识管理列表为聊天专属库显示"聊天级"徽标;聊天级库禁止移动到全局
- 统一模式(默认关闭独立记忆)的存量数据与行为完全不变
- 已知限制:聊天专属记忆跟随聊天文件,重命名聊天文件会使其失联(与 ST 官方向量扩展同等限制)
- **超级排序截断顺序修正**:开启"超级排序"时,时序重排发生在 top_n 截断之前,导致保留的是"时序最早"而非"最相关"的块,检索结果长期偏向最旧的聊天记录。现改为先按相关度截取 top_n、再做时序排序
- **翰林院向量化失败("向量化块数量不识别"反馈)**:
- 一次性清洗 profile-sync 历史污染:`retrieval/rerank.apiKey` 中的掩码占位符在持久层根治(此前仅读取侧防御);`apiEndpoint` / `rerank.apiMode` 的非法值(如被旧版写入的空字符串)归一化为 `custom`
- 修复 `apiEndpoint` 为空/非法时请求被硬定向到 `api.openai.com`、无视用户自定义 URL 的问题(CSP 拦截 / 401 的元凶)
- 修复**本地代理(LM Studio/Ollama)模式**自始就缺少 URL 分支、同样被错误定向到 openai.com 的问题
- API 模式下拉补全 `OpenAI 官方` / `Azure` 选项;默认 API 模式改为 `custom`(与默认 URL 配套),新用户不再因选项缺失导致首次保存写入空值
- profile-sync 给下拉框赋不存在选项值的污染源头修复(影响所有模块面板,不止翰林院)
- **Rerank "测试成功但实际请求报 API Key 未提供"(路径分叉根因)**:实际重排调用 `executeRerank(query, docs, settings.rerank)` 直接把 legacy 嵌套设置当连接传入,绕过了 `getRerankSettings()` 的 profile 解析;而「测试连接」传 `null` 会正常解析 profile——于是用 API Profile 配 rerank 的用户测试通过、实际生成时却拿到空 apiKey/stale url 报错。现实际调用点统一走 `getRerankSettings()`(profile 优先、legacy 兜底),与测试路径一致;`enabled / notify / hybrid_alpha` 等行为开关仍读 legacy 设置
- **Rerank "API Key 未提供"报错升级**:当原因是"连接配置在本设备没有可用 Key"时,报错会直接说明 Key 的设备本地性并指引到 API 连接配置重新填写(向量化 Google 直连、获取模型列表同步处理)
- **旧配置迁移**:一键生卡迁移时排除掩码占位符,避免把历史污染的假 Key 迁入新连接配置
- **超级记忆稳定性专项**(针对"工作不大稳定"反馈,4 处根因一次修复):
- **切聊天竞态污染**:CHAT_CHANGED 时超级记忆立即全量同步,而表格系统延迟 100ms 才加载新聊天的表格,导致【旧聊天】的表格内容被写进【新角色】的记忆世界书;两边表名不同时旧表条目无 GC 兜底会**永久残留**("记忆串台"元凶)。现 CHAT_CHANGED 只确保世界书存在,新状态同步交由 `loadTables()` 完成后的自动推送,单次且时序正确
- **死代码双轨存储拆除**:`saveStateToMetadata` / `tryRestoreStateFromMetadata` 把表格状态写到 `msg.metadata`——该字段非 ST 持久化位(同 v2.2.5 二次填表修过的坑),写入即蒸发、恢复永远为空,且每次同步还白调一次 `saveChat()`。整条链路删除,表格状态唯一信源为表格系统的 `msg.extra.amily2_tables_data`
- **`awaitSync()` 穿透**:同步队列正忙时 `pushUpdate` 会用一个立即 resolve 的空 Promise 覆盖 `_syncPromise`,Pipeline Stage 4 等待形同虚设、后续阶段在同步未完成时被放行。现忙时不覆盖,正在运行的 drain 循环自然吃掉新入队项
- **开关打开不生效**:启动时若总开关为关,初始化早退且不注册监听器;此后在 UI 勾选开关只写设置,超级记忆直到刷新页面前都是死的。现勾选即触发初始化(幂等)
- 附带:`forceSyncAll` 的表格角色推断改为复用 `events-schema.inferTableRole`,消除两处重复逻辑漂移风险;每次切聊天的双倍全量同步(restore 路径一次 + 显式一次)随死代码移除归一
### 重构
- 表格核心 `manager.js` 瘦身(约 1050 → 600 行):19 个 UI 突变操作拆分至 `actions/ui-mutations.js`,SuperMemory 事件分发拆分至 `events-dispatch.js`;全部经 re-export 保持兼容,外部调用路径零改动
- 角色世界书最后 2 处散乱的厂商 URL 判断迁移至 `detectVendor` 统一入口,业务路径上不再有硬编码的 URL substring 判断
1130 lines
48 KiB
JavaScript
1130 lines
48 KiB
JavaScript
import {
|
||
createDrawer,
|
||
showPlotOptimizationProgress, updatePlotOptimizationProgress, hidePlotOptimizationProgress,
|
||
registerSlashCommands,
|
||
onMessageReceived, handleTableUpdate,
|
||
processPlotOptimization,
|
||
getContext, extension_settings,
|
||
characters, this_chid, eventSource, event_types, saveSettingsDebounced,
|
||
injectTableData, generateTableContent,
|
||
injectProgressiveMemory,
|
||
initializeRagProcessor,
|
||
loadHanlinyuanSettingsToUI,
|
||
loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables,
|
||
fillWithSecondaryApi,
|
||
renderTables,
|
||
log,
|
||
checkForUpdates, fetchMessageBoardContent,
|
||
setUpdateInfo, applyUpdateIndicator,
|
||
pluginVersion, extensionName, defaultSettings,
|
||
configManager, apiProfileManager,
|
||
checkAuthorization, refreshUserInfo,
|
||
tableSystemDefaultSettings,
|
||
manageLorebookEntriesForChat,
|
||
cwbDefaultSettings,
|
||
updateOrInsertTableInChat, startContinuousRendering, stopContinuousRendering,
|
||
initializeRenderer,
|
||
initializeApiListener, registerApiHandler, amilyHelper, initializeAmilyHelper,
|
||
registerContextOptimizerMacros, resetContextBuffer,
|
||
initializeSuperMemory
|
||
} from './imports.js';
|
||
import { initializeAmilyBus } from './SL/bus/Amily2Bus.js';
|
||
|
||
const DOMPURIFY_CDN = "https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.7/purify.min.js";
|
||
|
||
function loadExternalScript(url, globalName) {
|
||
return new Promise((resolve, reject) => {
|
||
if (window[globalName]) {
|
||
resolve(window[globalName]);
|
||
return;
|
||
}
|
||
const existingScript = document.querySelector(`script[src="${url}"]`);
|
||
if (existingScript) {
|
||
existingScript.addEventListener('load', () => resolve(window[globalName]));
|
||
existingScript.addEventListener('error', reject);
|
||
return;
|
||
}
|
||
const script = document.createElement('script');
|
||
script.src = url;
|
||
script.async = true;
|
||
script.onload = () => {
|
||
console.log(`[Amily2-核心] 外部库加载成功: ${globalName}`);
|
||
resolve(window[globalName]);
|
||
};
|
||
script.onerror = (err) => {
|
||
console.error(`[Amily2-核心] 外部库加载失败: ${globalName}`, err);
|
||
reject(err);
|
||
};
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
|
||
const STYLE_SETTINGS_KEY = 'amily2_custom_styles';
|
||
const STYLE_ROOT_SELECTOR = '#amily2_memorisation_forms_panel';
|
||
let styleRoot = null;
|
||
|
||
function getStyleRoot() {
|
||
if (!styleRoot) {
|
||
styleRoot = document.querySelector(STYLE_ROOT_SELECTOR);
|
||
}
|
||
return styleRoot;
|
||
}
|
||
|
||
function applyStyles(styleObject) {
|
||
const root = getStyleRoot();
|
||
if (!root || !styleObject) return;
|
||
delete styleObject._comment;
|
||
|
||
for (const [key, value] of Object.entries(styleObject)) {
|
||
if (key.startsWith('--am2-')) {
|
||
root.style.setProperty(key, value);
|
||
}
|
||
}
|
||
}
|
||
|
||
function loadAndApplyStyles() {
|
||
const savedStyles = extension_settings[extensionName]?.[STYLE_SETTINGS_KEY];
|
||
if (savedStyles && typeof savedStyles === 'object' && Object.keys(savedStyles).length > 0) {
|
||
applyStyles(savedStyles);
|
||
}
|
||
}
|
||
|
||
function saveStyles(styleObject) {
|
||
if (!extension_settings[extensionName]) {
|
||
extension_settings[extensionName] = {};
|
||
}
|
||
extension_settings[extensionName][STYLE_SETTINGS_KEY] = styleObject;
|
||
saveSettingsDebounced();
|
||
}
|
||
|
||
function resetToDefaultStyles() {
|
||
const root = getStyleRoot();
|
||
if (!root) return;
|
||
const savedStyles = extension_settings[extensionName]?.[STYLE_SETTINGS_KEY];
|
||
if (savedStyles && typeof savedStyles === 'object') {
|
||
for (const key of Object.keys(savedStyles)) {
|
||
if (key.startsWith('--am2-')) {
|
||
root.style.removeProperty(key);
|
||
}
|
||
}
|
||
}
|
||
saveStyles(null);
|
||
toastr.success('已恢复默认界面样式。');
|
||
}
|
||
|
||
function getDefaultCssVars() {
|
||
return {
|
||
"--am2-font-size-base": "14px", "--am2-gap-main": "10px", "--am2-padding-main": "8px 5px",
|
||
"--am2-container-bg": "rgba(0,0,0,0.1)", "--am2-container-border": "1px solid rgba(255, 255, 255, 0.2)",
|
||
"--am2-container-border-radius": "12px", "--am2-container-padding": "10px", "--am2-container-shadow": "inset 0 0 15px rgba(0,0,0,0.2)",
|
||
"--am2-title-font-size": "1.1em", "--am2-title-font-weight": "bold", "--am2-title-text-shadow": "0 0 5px rgba(200, 200, 255, 0.3)",
|
||
"--am2-title-gradient-start": "#c0bde4", "--am2-title-gradient-end": "#dfdff0", "--am2-title-icon-color": "#9e8aff",
|
||
"--am2-title-icon-margin": "10px", "--am2-table-bg": "rgba(0,0,0,0.2)", "--am2-table-border": "1px solid rgba(255, 255, 255, 0.25)",
|
||
"--am2-table-cell-padding": "6px 8px", "--am2-table-cell-font-size": "0.95em", "--am2-header-bg": "rgba(255, 255, 255, 0.1)",
|
||
"--am2-header-color": "#e0e0e0", "--am2-header-editable-bg": "rgba(172, 216, 255, 0.1)", "--am2-header-editable-focus-bg": "rgba(172, 216, 255, 0.25)",
|
||
"--am2-header-editable-focus-outline": "1px solid #79b8ff", "--am2-cell-editable-bg": "rgba(255, 255, 172, 0.1)",
|
||
"--am2-cell-editable-focus-bg": "rgba(255, 255, 172, 0.25)", "--am2-cell-editable-focus-outline": "1px solid #ffc107",
|
||
"--am2-index-col-bg": "rgba(0, 0, 0, 0.3) !important", "--am2-index-col-color": "#aaa !important", "--am2-index-col-width": "40px",
|
||
"--am2-index-col-padding": "10px 5px !important", "--am2-controls-gap": "5px", "--am2-controls-margin-bottom": "10px",
|
||
"--am2-cell-highlight-bg": "rgba(144, 238, 144, 0.3)"
|
||
};
|
||
}
|
||
|
||
function exportStyles() {
|
||
const root = getStyleRoot();
|
||
if (!root) { toastr.error('无法导出样式:找不到根元素。'); return; }
|
||
const computedStyle = getComputedStyle(root);
|
||
const stylesToExport = {};
|
||
const defaultVars = getDefaultCssVars();
|
||
for (const key of Object.keys(defaultVars)) {
|
||
stylesToExport[key] = computedStyle.getPropertyValue(key).trim();
|
||
}
|
||
const blob = new Blob([JSON.stringify(stylesToExport, null, 2)], { type: 'application/json' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `Amily2-Theme-${new Date().toISOString().slice(0, 10)}.json`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
toastr.success('主题文件已开始下载。', '导出成功');
|
||
}
|
||
|
||
function importStyles() {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.json';
|
||
input.style.display = 'none';
|
||
|
||
const cleanup = () => {
|
||
if (document.body.contains(input)) {
|
||
document.body.removeChild(input);
|
||
}
|
||
};
|
||
|
||
input.onchange = e => {
|
||
const file = e.target.files[0];
|
||
if (!file) {
|
||
cleanup();
|
||
return;
|
||
}
|
||
const reader = new FileReader();
|
||
reader.onload = event => {
|
||
try {
|
||
const importedStyles = JSON.parse(event.target.result);
|
||
if (typeof importedStyles !== 'object' || Array.isArray(importedStyles)) {
|
||
throw new Error('无效的JSON格式。');
|
||
}
|
||
applyStyles(importedStyles);
|
||
saveStyles(importedStyles);
|
||
toastr.success('主题已成功导入并应用!');
|
||
} catch (error) {
|
||
toastr.error(`导入失败:${error.message}`, '错误');
|
||
} finally {
|
||
cleanup();
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
};
|
||
|
||
document.body.appendChild(input);
|
||
input.click();
|
||
}
|
||
|
||
function compareVersions(v1, v2) {
|
||
const parts1 = v1.split('.').map(Number);
|
||
const parts2 = v2.split('.').map(Number);
|
||
const len = Math.max(parts1.length, parts2.length);
|
||
|
||
for (let i = 0; i < len; i++) {
|
||
const p1 = parts1[i] || 0;
|
||
const p2 = parts2[i] || 0;
|
||
if (p1 > p2) return true;
|
||
if (p1 < p2) return false;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
async function handleUpdateCheck() {
|
||
console.log("【Amily2号】帝国已就绪,现派遣外交官,为陛下探查外界新情报...");
|
||
const updateInfo = await checkForUpdates();
|
||
|
||
if (updateInfo && updateInfo.version) {
|
||
const isNew = compareVersions(updateInfo.version, pluginVersion);
|
||
if(isNew) {
|
||
console.log(`【Amily2号-情报部】捷报!发现新版本: ${updateInfo.version}。情报已转交内务府。`);
|
||
} else {
|
||
console.log(`【Amily2号-情报部】一切安好,帝国已是最新版本。情报已转交内务府备案。`);
|
||
}
|
||
setUpdateInfo(isNew, updateInfo);
|
||
applyUpdateIndicator();
|
||
}
|
||
}
|
||
|
||
function sanitizeHTML(html) {
|
||
if (window.DOMPurify) {
|
||
return window.DOMPurify.sanitize(html, {
|
||
ALLOWED_TAGS: ['b', 'i', 'u', 'em', 'strong', 'a', 'p', 'br', 'span', 'div', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'font', 'blockquote', 'code', 'pre', 'hr', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td'],
|
||
ALLOWED_ATTR: ['href', 'target', 'style', 'class', 'color', 'size', 'src', 'alt', 'title', 'width', 'height', 'align'],
|
||
FORBID_TAGS: ['script', 'style', 'iframe', 'frame', 'object', 'embed', 'form', 'input', 'textarea', 'button', 'select', 'option'],
|
||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup', 'ondblclick', 'onkeydown', 'onkeypress', 'onkeyup', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect', 'oncontextmenu'],
|
||
ADD_ATTR: ['target'],
|
||
});
|
||
}
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.innerHTML = html;
|
||
|
||
const allowedTags = ['b', 'i', 'u', 'em', 'strong', 'a', 'p', 'br', 'span', 'div', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'font'];
|
||
const allowedAttrs = ['href', 'target', 'style', 'class', 'color', 'size'];
|
||
|
||
const elements = tempDiv.querySelectorAll('*');
|
||
const allElements = tempDiv.getElementsByTagName('*');
|
||
for (let i = allElements.length - 1; i >= 0; i--) {
|
||
const el = allElements[i];
|
||
const tagName = el.tagName.toLowerCase();
|
||
|
||
if (!allowedTags.includes(tagName)) {
|
||
el.parentNode.removeChild(el);
|
||
continue;
|
||
}
|
||
|
||
// 移除所有属性,只保留允许的
|
||
const attrs = Array.from(el.attributes);
|
||
for (const attr of attrs) {
|
||
const attrName = attr.name.toLowerCase();
|
||
if (!allowedAttrs.includes(attrName)) {
|
||
el.removeAttribute(attr.name);
|
||
} else if (attrName === 'href') {
|
||
// 检查 href 是否包含 javascript:
|
||
if (attr.value.toLowerCase().trim().startsWith('javascript:')) {
|
||
el.removeAttribute('href');
|
||
}
|
||
} else if (attrName.startsWith('on')) { // 双重保险,移除所有事件处理器
|
||
el.removeAttribute(attr.name);
|
||
}
|
||
}
|
||
}
|
||
return tempDiv.innerHTML;
|
||
}
|
||
|
||
async function handleMessageBoard() {
|
||
const updateMessage = async () => {
|
||
try {
|
||
const messageData = await fetchMessageBoardContent();
|
||
if (messageData && messageData.message) {
|
||
const messageBoard = $('#amily2_message_board');
|
||
const messageContent = $('#amily2_message_content');
|
||
// 使用净化后的 HTML,防止 XSS 攻击
|
||
const safeContent = sanitizeHTML(messageData.message);
|
||
messageContent.html(safeContent);
|
||
messageBoard.show();
|
||
console.log("【Amily2号-内务府】已成功获取并展示来自陛下的最新圣谕。");
|
||
}
|
||
} catch (error) {
|
||
console.error("【Amily2号-内务府】获取留言板失败:", error);
|
||
}
|
||
};
|
||
await updateMessage();
|
||
setInterval(updateMessage, 300000); // 5分钟刷新一次(从60秒改为300秒)
|
||
}
|
||
|
||
|
||
|
||
function loadPluginStyles() {
|
||
const loadStyleFile = (fileName) => {
|
||
const styleId = `amily2-style-${fileName.split('.')[0]}`;
|
||
if (document.getElementById(styleId)) return;
|
||
|
||
const extensionPath = `scripts/extensions/third-party/${extensionName}/assets/${fileName}?v=${Date.now()}`;
|
||
|
||
const link = document.createElement("link");
|
||
link.id = styleId;
|
||
link.rel = "stylesheet";
|
||
link.type = "text/css";
|
||
link.href = extensionPath;
|
||
document.head.appendChild(link);
|
||
console.log(`[Amily2号-皇家制衣局] 已为帝国披上华服: ${fileName}`);
|
||
};
|
||
|
||
// 颁布三道制衣圣谕
|
||
loadStyleFile("style.css"); // 【第一道圣谕】为帝国主体宫殿披上通用华服
|
||
loadStyleFile("historiography.css"); // 【第二道圣谕】为敕史局披上其专属华服
|
||
loadStyleFile("amily-hanlinyuan-system/hanlinyuan.css"); // 【第三道圣谕】为翰林院披上其专属华服
|
||
loadStyleFile("amily-glossary-system/amily2-glossary.css"); // 【新圣谕】为术语表披上其专属华服
|
||
loadStyleFile("amily-data-table/table.css"); // 【第四道圣谕】为内存储司披上其专属华服
|
||
loadStyleFile("optimization.css"); // 【第五道圣谕】为剧情优化披上其专属华服
|
||
loadStyleFile("renderer.css"); // 【新圣谕】为渲染器披上其专属华服
|
||
// loadStyleFile("iframe-renderer.css"); // 【新圣谕】为iframe渲染内容披上其专属华服
|
||
loadStyleFile("renderer.css"); // 【新圣谕】为iframe渲染内容披上其专属华服
|
||
loadStyleFile("super-memory.css"); // 【新圣谕】为超级记忆披上其专属华服
|
||
|
||
// 【第六道圣谕】为角色世界书披上其专属华服
|
||
const cwbStyleId = 'cwb-feature-style';
|
||
if (!document.getElementById(cwbStyleId)) {
|
||
const cwbLink = document.createElement("link");
|
||
cwbLink.id = cwbStyleId;
|
||
cwbLink.rel = "stylesheet";
|
||
cwbLink.type = "text/css";
|
||
cwbLink.href = `scripts/extensions/third-party/${extensionName}/CharacterWorldBook/cwb_style.css?v=${Date.now()}`;
|
||
document.head.appendChild(cwbLink);
|
||
console.log(`[Amily2号-皇家制衣局] 已为角色世界书披上华服: cwb_style.css`);
|
||
}
|
||
|
||
// 【第七道圣谕】为世界编辑器披上其专属华服
|
||
const worldEditorStyleId = 'world-editor-style';
|
||
if (!document.getElementById(worldEditorStyleId)) {
|
||
const worldEditorLink = document.createElement("link");
|
||
worldEditorLink.id = worldEditorStyleId;
|
||
worldEditorLink.rel = "stylesheet";
|
||
worldEditorLink.type = "text/css";
|
||
worldEditorLink.href = `scripts/extensions/third-party/${extensionName}/WorldEditor/WorldEditor.css?v=${Date.now()}`;
|
||
document.head.appendChild(worldEditorLink);
|
||
console.log(`[Amily2号-皇家制衣局] 已为世界编辑器披上华服: WorldEditor.css`);
|
||
}
|
||
|
||
}
|
||
|
||
|
||
window.addEventListener('message', function (event) {
|
||
// 处理头像获取请求
|
||
if (event.data && event.data.type === 'getAvatars') {
|
||
// 【兼容性修复】如果 LittleWhiteBox 激活,则不处理此消息,避免冲突
|
||
if (window.isXiaobaixEnabled) {
|
||
return;
|
||
}
|
||
const userAvatar = `/characters/${getContext().userCharacter?.avatar ?? ''}`;
|
||
const charAvatar = `/characters/${getContext().characters[this_chid]?.avatar ?? ''}`;
|
||
event.source.postMessage({
|
||
source: 'amily2-host',
|
||
type: 'avatars',
|
||
urls: { user: userAvatar, char: charAvatar }
|
||
}, '*');
|
||
return;
|
||
}
|
||
|
||
// 处理来自 iframe 的交互事件
|
||
if (event.data && event.data.source === 'amily2-iframe') {
|
||
const { action, detail } = event.data;
|
||
console.log(`[Amily2-主窗口] 收到来自iframe的动作: ${action}`, detail);
|
||
|
||
switch (action) {
|
||
case 'sendMessage':
|
||
if (detail && detail.message) {
|
||
$('#send_textarea').val(detail.message).trigger('input');
|
||
$('#send_but').trigger('click');
|
||
console.log(`[Amily2-主窗口] 已发送消息: ${detail.message}`);
|
||
}
|
||
break;
|
||
|
||
case 'showToast':
|
||
if (detail && detail.message && window.toastr) {
|
||
const toastType = detail.type || 'info';
|
||
if (typeof window.toastr[toastType] === 'function') {
|
||
window.toastr[toastType](detail.message, detail.title || '通知');
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'buttonClick':
|
||
console.log(`[Amily2-主窗口] 按钮被点击:`, detail);
|
||
if (window.toastr) {
|
||
window.toastr.info(`按钮 "${detail.buttonId || '未知'}" 被点击`, 'iframe交互');
|
||
}
|
||
break;
|
||
|
||
default:
|
||
console.warn(`[Amily2-主窗口] 未知的动作类型: ${action}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
window.addEventListener("error", (event) => {
|
||
const stackTrace = event.error?.stack || "";
|
||
if (stackTrace.includes("ST-Amily2-Chat-Optimisation")) {
|
||
console.error("[Amily2-全局卫队] 捕获到严重错误:", event.error);
|
||
toastr.error(`Amily2插件错误: ${event.error?.message || "未知错误"}`, "严重错误", { timeOut: 10000 });
|
||
}
|
||
});
|
||
|
||
|
||
let isProcessingPlotOptimization = false;
|
||
|
||
/**
|
||
* 加载必要的外部库(如 DOMPurify)。
|
||
* 如果加载失败,会回退到内置的简单净化器。
|
||
*/
|
||
function loadExternalLibraries() {
|
||
loadExternalScript(DOMPURIFY_CDN, 'DOMPurify').catch(e => console.warn("[Amily2] DOMPurify 加载失败,将使用内置净化器:", e));
|
||
}
|
||
|
||
/**
|
||
* 初始化上下文优化器模块。
|
||
* 优先注册宏,确保其在其他处理之前生效。
|
||
*/
|
||
function initializeContextOptimizer() {
|
||
try {
|
||
console.log("[Amily2号-开国大典] 步骤0:优先注册上下文优化器...");
|
||
registerContextOptimizerMacros();
|
||
} catch (e) {
|
||
console.error("[Amily2号-开国大典] 上下文优化器注册失败:", e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 异步初始化“密折司”模块。
|
||
* 该模块通常用于处理机密或特殊的后台逻辑。
|
||
*/
|
||
async function initializeMiZheSi() {
|
||
try {
|
||
await import("./MiZheSi/index.js");
|
||
console.log("[Amily2号-开国大典] 密折司模块已就位。");
|
||
} catch (e) {
|
||
console.error("[Amily2号-开国大典] 密折司加载失败:", e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注册所有与 SillyTavern 交互的 API 处理器。
|
||
* 包括消息获取、设置、删除,以及 Lorebook 管理等功能。
|
||
*/
|
||
function registerAllApiHandlers() {
|
||
initializeApiListener();
|
||
|
||
registerApiHandler('getChatMessages', async (data) => amilyHelper.getChatMessages(data.range, data.options));
|
||
registerApiHandler('setChatMessages', async (data) => amilyHelper.setChatMessages(data.messages, data.options));
|
||
registerApiHandler('setChatMessage', async (data) => {
|
||
const field_values = data.field_values || data.content;
|
||
const message_id = data.message_id !== undefined ? data.message_id : data.index;
|
||
const options = data.options || {};
|
||
console.log('[Amily2-API] setChatMessage 收到参数:', { field_values, message_id, options, raw_data: data });
|
||
return await amilyHelper.setChatMessage(field_values, message_id, options);
|
||
});
|
||
registerApiHandler('createChatMessages', async (data) => amilyHelper.createChatMessages(data.messages, data.options));
|
||
registerApiHandler('deleteChatMessages', async (data) => amilyHelper.deleteChatMessages(data.ids, data.options));
|
||
registerApiHandler('getLorebooks', async (data) => amilyHelper.getLorebooks());
|
||
registerApiHandler('getCharLorebooks', async (data) => amilyHelper.getCharLorebooks(data.options));
|
||
registerApiHandler('getLorebookEntries', async (data) => amilyHelper.getLorebookEntries(data.bookName));
|
||
registerApiHandler('setLorebookEntries', async (data) => amilyHelper.setLorebookEntries(data.bookName, data.entries));
|
||
registerApiHandler('createLorebookEntries', async (data) => amilyHelper.createLorebookEntries(data.bookName, data.entries));
|
||
registerApiHandler('createLorebook', async (data) => amilyHelper.createLorebook(data.bookName));
|
||
registerApiHandler('triggerSlash', async (data) => amilyHelper.triggerSlash(data.command));
|
||
registerApiHandler('getLastMessageId', async (data) => amilyHelper.getLastMessageId());
|
||
registerApiHandler('toastr', async (data) => {
|
||
if (window.toastr && typeof window.toastr[data.type] === 'function') {
|
||
window.toastr[data.type](data.message, data.title);
|
||
}
|
||
return true;
|
||
});
|
||
registerApiHandler('switchSwipe', async (data) => {
|
||
const { messageIndex, swipeIndex } = data;
|
||
const messages = await amilyHelper.getChatMessages(messageIndex, { include_swipes: true });
|
||
if (messages && messages.length > 0 && messages[0].swipes) {
|
||
const content = messages[0].swipes[swipeIndex];
|
||
if (content !== undefined) {
|
||
await amilyHelper.setChatMessages([{
|
||
message_id: messageIndex,
|
||
message: content
|
||
}], { refresh: 'affected' });
|
||
const context = getContext();
|
||
if (context.chat[messageIndex]) {
|
||
context.chat[messageIndex].swipe_id = swipeIndex;
|
||
}
|
||
return { success: true, message: `已切换至开场白 ${swipeIndex}` };
|
||
}
|
||
}
|
||
throw new Error(`无法切换到开场白 ${swipeIndex}`);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 合并插件的默认设置与用户设置。
|
||
* 确保即使在升级后,新增加的设置项也有默认值。
|
||
*/
|
||
function mergePluginSettings() {
|
||
if (!extension_settings[extensionName]) {
|
||
extension_settings[extensionName] = {};
|
||
}
|
||
const combinedDefaultSettings = { ...defaultSettings, ...tableSystemDefaultSettings, ...cwbDefaultSettings, render_on_every_message: false, amily_render_enabled: false };
|
||
for (const key in combinedDefaultSettings) {
|
||
if (extension_settings[extensionName][key] === undefined) {
|
||
extension_settings[extensionName][key] = combinedDefaultSettings[key];
|
||
}
|
||
}
|
||
console.log("[Amily2号-帝国枢密院] 帝国基本法已确认,档案室已与国库对接完毕。");
|
||
}
|
||
|
||
/**
|
||
* 注册用于表格内容的 SillyTavern 宏。
|
||
* 允许在 Prompt 中使用 {{Amily2EditContent}} 来插入动态生成的表格数据。
|
||
*/
|
||
function registerTableMacros() {
|
||
console.log("[Amily2号-开国大典] 步骤3.8:注册表格占位符宏...");
|
||
try {
|
||
eventSource.on(event_types.GENERATION_STARTED, () => {
|
||
resetContextBuffer();
|
||
if (isProcessingPlotOptimization) {
|
||
console.warn("[Amily2-剧情优化] 检测到生成开始,但优化标志位仍为 true。这可能是并发生成或状态未及时重置。");
|
||
}
|
||
});
|
||
|
||
const context = getContext();
|
||
if (context && typeof context.registerMacro === 'function') {
|
||
context.registerMacro('Amily2EditContent', () => {
|
||
const content = generateTableContent();
|
||
if (content) {
|
||
window.AMILY2_MACRO_REPLACED = true;
|
||
}
|
||
return content;
|
||
});
|
||
console.log('[Amily2-核心引擎] 已成功注册表格占位符宏: {{Amily2EditContent}}');
|
||
} else {
|
||
console.warn('[Amily2-核心引擎] 无法注册表格宏,可能是 SillyTavern 版本不兼容。');
|
||
}
|
||
} catch (error) {
|
||
console.error('[Amily2-核心引擎] 注册表格宏时发生错误:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理用户发送消息前的逻辑(剧情优化)。
|
||
* 拦截消息发送,进行剧情梳理和总结,然后注入到 Prompt 中。
|
||
*
|
||
* @param {string} type - 触发类型 (例如 'send')
|
||
* @param {object} params - 参数对象
|
||
* @param {boolean} dryRun - 是否为试运行
|
||
* @returns {Promise<boolean>} - 返回 false 以阻止默认行为(如果已异步处理),或不做阻拦。
|
||
*/
|
||
async function onPlotGenerationAfterCommands(type, params, dryRun) {
|
||
clearUpdatedTables();
|
||
if (isProcessingPlotOptimization) {
|
||
console.log("[Amily2-剧情优化] 优化正在进行中,拦截重复触发。");
|
||
return;
|
||
}
|
||
console.log("[Amily2-剧情优化] Generation after commands triggered", { type, params, dryRun });
|
||
if (type === 'regenerate' || dryRun) {
|
||
console.log("[Amily2-剧情优化] Skipping due to regenerate or dryRun.");
|
||
return false;
|
||
}
|
||
const globalSettings = extension_settings[extensionName];
|
||
if (globalSettings?.plotOpt_enabled === false) return false;
|
||
|
||
const isJqyhEnabled = globalSettings?.jqyhEnabled === true;
|
||
const hasProfile = !!apiProfileManager.getAssignment('main') || !!apiProfileManager.getAssignment('plotOpt');
|
||
const hasLegacyConfig = !!globalSettings?.apiUrl || !!globalSettings?.tavernProfile
|
||
|| !!globalSettings?.plotOpt_apiUrl || !!globalSettings?.plotOpt_tavernProfile;
|
||
|
||
if (!isJqyhEnabled && !hasProfile && !hasLegacyConfig) {
|
||
console.log("[Amily2-剧情优化] 优化已启用,但未配置任何可用的 API(无 Profile 分配亦无独立配置)。");
|
||
return false;
|
||
}
|
||
|
||
let userMessage = $('#send_textarea').val();
|
||
let isFromTextarea = true;
|
||
const context = getContext();
|
||
if (!userMessage) {
|
||
if (context.chat && context.chat.length > 0) {
|
||
const lastMsg = context.chat[context.chat.length - 1];
|
||
if (lastMsg.is_user) {
|
||
userMessage = lastMsg.mes;
|
||
isFromTextarea = false;
|
||
console.log("[Amily2-剧情优化] Detected empty textarea, processing last user message.");
|
||
}
|
||
}
|
||
}
|
||
if (!userMessage) return false;
|
||
|
||
isProcessingPlotOptimization = true;
|
||
const cancellationState = { isCancelled: false };
|
||
showPlotOptimizationProgress(cancellationState);
|
||
|
||
const onProgress = (message, isDone = false, isSkipped = false) => {
|
||
updatePlotOptimizationProgress(message, isDone, isSkipped);
|
||
};
|
||
|
||
try {
|
||
const cancellationPromise = new Promise((_, reject) => {
|
||
const checkCancel = setInterval(() => {
|
||
if (cancellationState.isCancelled) {
|
||
clearInterval(checkCancel);
|
||
reject(new Error("Optimization cancelled by user"));
|
||
}
|
||
}, 100);
|
||
});
|
||
|
||
const contextTurnCount = globalSettings.plotOpt_contextLimit ?? globalSettings.plotOpt_contextTurnCount ?? 10;
|
||
const contextSource = isFromTextarea ? context.chat : context.chat.slice(0, -1);
|
||
const slicedContext = contextTurnCount > 0 ? contextSource.slice(-contextTurnCount) : contextSource;
|
||
|
||
const optimizationPromise = processPlotOptimization({ mes: userMessage }, slicedContext, cancellationState, onProgress);
|
||
const result = await Promise.race([optimizationPromise, cancellationPromise]);
|
||
|
||
if (cancellationState.isCancelled) throw new Error("Optimization cancelled by user");
|
||
|
||
if (result && result.contentToAppend) {
|
||
const finalMessage = userMessage + '\n' + result.contentToAppend;
|
||
if (params && typeof params === 'object') {
|
||
try {
|
||
if (params.prompt) params.prompt = finalMessage;
|
||
if (Array.isArray(params.messages)) {
|
||
const lastMsg = params.messages[params.messages.length - 1];
|
||
if (lastMsg && lastMsg.role === 'user') {
|
||
lastMsg.content = finalMessage;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn("[Amily2-剧情优化] 尝试修改 params 失败:", e);
|
||
}
|
||
}
|
||
if (isFromTextarea) {
|
||
$('#send_textarea').val(finalMessage).trigger('input');
|
||
} else {
|
||
const targetMessageId = context.chat.length - 1;
|
||
await amilyHelper.setChatMessage(finalMessage, targetMessageId, { refresh: 'none' });
|
||
}
|
||
toastr.success('剧情优化已完成并注入,继续生成...', '操作成功');
|
||
isProcessingPlotOptimization = false;
|
||
hidePlotOptimizationProgress();
|
||
return false;
|
||
} else {
|
||
console.log("[Amily2-剧情优化] Plot optimization returned no result. Sending original message.");
|
||
isProcessingPlotOptimization = false;
|
||
hidePlotOptimizationProgress();
|
||
return false;
|
||
}
|
||
|
||
} catch (error) {
|
||
if (cancellationState.isCancelled || error.message === "Optimization cancelled by user") {
|
||
console.log("[Amily2-剧情优化] 优化流程已被用户中止。发送原始消息。");
|
||
toastr.warning('记忆管理任务已中止。', '操作取消', { timeOut: 2000 });
|
||
} else {
|
||
console.error(`[Amily2-剧情优化] 处理发送前事件时出错:`, error);
|
||
toastr.error('记忆管理处理失败,将发送原始消息。', '错误');
|
||
}
|
||
isProcessingPlotOptimization = false;
|
||
hidePlotOptimizationProgress();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注册核心事件监听器。
|
||
* 包含对消息接收、编辑、删除、滑动等事件的处理,以及剧情优化的触发。
|
||
*/
|
||
function registerEventListeners() {
|
||
console.log("[Amily2号-开国大典] 步骤四:部署帝国哨兵网络...");
|
||
if (!window.amily2EventsRegistered) {
|
||
eventSource.on(event_types.GENERATION_AFTER_COMMANDS, onPlotGenerationAfterCommands);
|
||
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
|
||
eventSource.on(event_types.IMPERSONATE_READY, onMessageReceived);
|
||
// handleTableUpdate for MESSAGE_RECEIVED removed — now handled by pipeline Stage 3 inside onMessageReceived
|
||
eventSource.on(event_types.MESSAGE_SWIPED, async (chat_id) => {
|
||
const context = getContext();
|
||
if (context.chat.length < 2) {
|
||
log('【监察系统】检测到消息滑动,但聊天记录不足,已跳过状态回退。', 'info');
|
||
return;
|
||
}
|
||
log('【监察系统】检测到消息滑动 (SWIPED),开始执行状态回退...', 'warn');
|
||
rollbackState();
|
||
const latestMessage = context.chat[chat_id] || context.chat[context.chat.length - 1];
|
||
if (latestMessage.is_user) {
|
||
log('【监察系统】滑动后最新消息是用户,跳过填表。', 'info');
|
||
renderTables();
|
||
return;
|
||
}
|
||
const settings = extension_settings[extensionName];
|
||
const fillingMode = settings.filling_mode || 'main-api';
|
||
if (fillingMode === 'main-api') {
|
||
log(`【监察系统】主填表模式,回退后强制刷新消息ID: ${chat_id}。`, 'info');
|
||
await handleTableUpdate(chat_id, true);
|
||
} else if (fillingMode === 'secondary-api' || fillingMode === 'optimized') {
|
||
log('【监察系统】分步/优化模式,回退后触发二次填表扫描(受保留缓冲区限制)。', 'info');
|
||
await fillWithSecondaryApi(latestMessage, true);
|
||
} else {
|
||
log('【监察系统】未配置填表模式,跳过填表。', 'info');
|
||
}
|
||
renderTables();
|
||
log('【监察系统】滑动后填表完成,UI 已刷新。', 'success');
|
||
});
|
||
eventSource.on(event_types.MESSAGE_EDITED, (mes_id) => {
|
||
handleTableUpdate(mes_id);
|
||
updateOrInsertTableInChat();
|
||
});
|
||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||
window.lastPreOptimizationResult = null;
|
||
document.dispatchEvent(new CustomEvent('preOptimizationTextUpdated'));
|
||
manageLorebookEntriesForChat();
|
||
setTimeout(() => {
|
||
log("【监察系统】检测到“朝代更迭”(CHAT_CHANGED),开始重修史书并刷新宫殿...", 'info');
|
||
clearHighlights();
|
||
clearUpdatedTables();
|
||
loadTables();
|
||
renderTables();
|
||
if (extension_settings[extensionName].render_on_every_message) {
|
||
startContinuousRendering();
|
||
} else {
|
||
stopContinuousRendering();
|
||
}
|
||
}, 100);
|
||
});
|
||
eventSource.on(event_types.MESSAGE_DELETED, (message, index) => {
|
||
log(`【监察系统】检测到消息 ${index} 被删除,开始精确回滚UI状态。`, 'warn');
|
||
clearHighlights();
|
||
loadTables(index);
|
||
renderTables();
|
||
});
|
||
eventSource.on(event_types.MESSAGE_RECEIVED, updateOrInsertTableInChat);
|
||
eventSource.on(event_types.chat_updated, updateOrInsertTableInChat);
|
||
|
||
window.amily2EventsRegistered = true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行 Amily2 的统一注入逻辑。
|
||
* 同时兼容表格数据注入和 RAG 上下文重排。
|
||
* @param {...any} args - 传递给 injectTableData 和 rearrangeChat 的参数
|
||
*/
|
||
async function executeAmily2Injection(...args) {
|
||
console.log('[Amily2-核心引擎] 开始执行统一注入 (聊天长度:', args[0]?.length || 0, ')');
|
||
try {
|
||
await injectTableData(...args);
|
||
} catch (error) {
|
||
console.error('[Amily2-内存储司] 表格注入失败:', error);
|
||
}
|
||
try {
|
||
// 渐进记忆(内测,type2 门槛由引擎内部判定);args[3] = type('quiet' 时跳过)
|
||
injectProgressiveMemory(args[3]);
|
||
} catch (error) {
|
||
console.error('[Amily2-渐进记忆] 注入失败:', error);
|
||
}
|
||
if (window.hanlinyuanRagProcessor && typeof window.hanlinyuanRagProcessor.rearrangeChat === 'function') {
|
||
try {
|
||
console.log('[Amily2-核心引擎] 执行内置RAG注入。');
|
||
await window.hanlinyuanRagProcessor.rearrangeChat(...args);
|
||
} catch (error) {
|
||
console.error('[Amily2-翰林院] RAG注入失败:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化 RAG 处理器并设置注入策略。
|
||
* 覆盖 `vectors_rearrangeChat` 以确保 Amily2 的注入逻辑优先执行。
|
||
*/
|
||
function initializeRagAndInjection() {
|
||
console.log("[Amily2号-开国大典] 步骤五:初始化RAG处理器...");
|
||
try {
|
||
initializeRagProcessor();
|
||
console.log('[Amily2-翰林院] RAG处理器已成功初始化');
|
||
} catch (error) {
|
||
console.error('[Amily2-翰林院] RAG处理器初始化失败:', error);
|
||
}
|
||
|
||
// 此时 ST settings hydration 已完成,且 RAG 第二次 init 拿到的是真实 saved settings 引用。
|
||
// mount 阶段那次 loadSettingsToUI 跑得过早(hydration 之前),UI 拿到的是默认值;
|
||
// 在此重跑一次以让翰林院面板显示真实持久化值。
|
||
try {
|
||
loadHanlinyuanSettingsToUI();
|
||
} catch (error) {
|
||
console.error('[Amily2-翰林院] 步骤五重载面板设置失败:', error);
|
||
}
|
||
|
||
console.log("[Amily2号-开国大典] 步骤六:智能冲突检测与注入策略...");
|
||
console.log('[Amily2-策略] 采用“完全主导”策略,覆盖 `vectors_rearrangeChat`。');
|
||
window['vectors_rearrangeChat'] = executeAmily2Injection;
|
||
if (window['amily2HanlinyuanInjector']) {
|
||
window['amily2HanlinyuanInjector'] = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行部署完成后的后续任务。
|
||
* 包括:版本检查、在线人数统计、本地联动、超级记忆初始化、渲染器启动和主题应用。
|
||
*/
|
||
function performPostDeploymentTasks() {
|
||
console.log("【Amily2号】帝国秩序已完美建立。Amily2号的府邸已恭候陛下的莅临。");
|
||
if (checkAuthorization()) {
|
||
const userType = localStorage.getItem("plugin_user_type") || "未知";
|
||
const userNote = localStorage.getItem("plugin_user_note");
|
||
const displayNote = userNote || userType;
|
||
toastr.success(`欢迎回来!授权状态有效 (用户: ${displayNote})`, "Amily2 插件已就绪");
|
||
refreshUserInfo().then(data => {
|
||
if (data && data.note && data.note !== userNote) {
|
||
console.log("[Amily2] 用户信息已更新:", data.note);
|
||
}
|
||
}).catch(e => console.warn("[Amily2] 后台刷新用户信息失败:", e));
|
||
}
|
||
|
||
console.log("[Amily2号-开国大典] 步骤七:初始化版本显示系统...");
|
||
if (typeof window.amily2Updater !== 'undefined') {
|
||
setTimeout(() => {
|
||
console.log("[Amily2号-版本系统] 正在启动版本检测器...");
|
||
window.amily2Updater.initialize();
|
||
}, 2000);
|
||
} else {
|
||
console.warn("[Amily2号-版本系统] 版本检测器未找到,可能加载失败");
|
||
}
|
||
|
||
handleUpdateCheck();
|
||
handleMessageBoard();
|
||
initializeOnlineTracker();
|
||
initializeLocalLinkage();
|
||
|
||
setTimeout(() => initializeSuperMemory(), 3000);
|
||
|
||
initializeRenderer();
|
||
|
||
if (extension_settings[extensionName].render_on_every_message) {
|
||
startContinuousRendering();
|
||
}
|
||
|
||
setTimeout(() => {
|
||
try {
|
||
loadAndApplyStyles();
|
||
const importThemeBtn = document.getElementById('amily2-import-theme-btn');
|
||
const exportThemeBtn = document.getElementById('amily2-export-theme-btn');
|
||
const resetThemeBtn = document.getElementById('amily2-reset-theme-btn');
|
||
if (importThemeBtn) importThemeBtn.addEventListener('click', importStyles);
|
||
if (exportThemeBtn) exportThemeBtn.addEventListener('click', exportStyles);
|
||
if (resetThemeBtn) resetThemeBtn.addEventListener('click', resetToDefaultStyles);
|
||
log('【凤凰阁】内联主题系统已通过延迟加载成功初始化并绑定事件。', 'success');
|
||
} catch (error) {
|
||
log(`【凤凰阁】内联主题系统初始化失败: ${error}`, 'error');
|
||
}
|
||
}, 500);
|
||
}
|
||
|
||
/**
|
||
* Amily2 核心部署流程(开国大典)。
|
||
* 只有当 SillyTavern 基础 UI 加载完成后才会执行此函数。
|
||
* 负责按顺序初始化插件的各个子系统。
|
||
*/
|
||
async function runAmily2Deployment() {
|
||
console.log("[Amily2号-帝国枢密院] SillyTavern宫殿主体已确认,开国大典正式开始!");
|
||
try {
|
||
console.log("[Amily2号-开国大典] 步骤一:为宫殿披上华服...");
|
||
loadPluginStyles();
|
||
|
||
console.log("[Amily2号-开国大典] 步骤二:皇家仪仗队就位...");
|
||
await registerSlashCommands();
|
||
|
||
console.log("[Amily2号-开国大典] 步骤三:开始召唤府邸(模块注册式架构)...");
|
||
await createDrawer();
|
||
|
||
// Glossary 和 CWB 的初始化已由 ModuleRegistry 在 mount 阶段完成,
|
||
// 不再需要 waitForGlossaryPanelAndBindEvents / waitForCwbPanelAndInitialize 轮询。
|
||
registerTableMacros();
|
||
|
||
registerEventListeners();
|
||
initializeRagAndInjection();
|
||
performPostDeploymentTasks();
|
||
|
||
} catch (error) {
|
||
console.error("!!!【开国大典失败】在执行系列法令时发生严重错误:", error);
|
||
}
|
||
}
|
||
|
||
jQuery(async () => {
|
||
console.log("[Amily2号-帝国枢密院] 开始执行开国大典...");
|
||
|
||
initializeAmilyBus();
|
||
loadExternalLibraries();
|
||
initializeContextOptimizer();
|
||
await initializeMiZheSi();
|
||
|
||
registerAllApiHandlers();
|
||
initializeAmilyHelper();
|
||
mergePluginSettings();
|
||
configManager.migrate(); // 将 extension_settings 中残留的敏感字段迁移到 localStorage
|
||
await configManager.init();
|
||
|
||
let attempts = 0;
|
||
const maxAttempts = 100;
|
||
const checkInterval = 100;
|
||
const targetSelector = "#sys-settings-button";
|
||
|
||
const deploymentInterval = setInterval(async () => {
|
||
if ($(targetSelector).length > 0) {
|
||
clearInterval(deploymentInterval);
|
||
await runAmily2Deployment();
|
||
} else {
|
||
attempts++;
|
||
if (attempts >= maxAttempts) {
|
||
clearInterval(deploymentInterval);
|
||
console.error(`[Amily2号] 部署失败:等待 ${targetSelector} 超时。`);
|
||
}
|
||
}
|
||
}, checkInterval);
|
||
});
|
||
|
||
function applyMessageLimit() {
|
||
const limit = window.amily2MaxMessages;
|
||
if (!limit) return;
|
||
|
||
const chatContainer = document.getElementById('chat');
|
||
if (!chatContainer) return;
|
||
|
||
const messages = Array.from(chatContainer.getElementsByClassName('mes'));
|
||
const total = messages.length;
|
||
|
||
if (total <= limit) {
|
||
// 如果消息数未超标,确保所有消息可见
|
||
messages.forEach(el => el.style.display = '');
|
||
return;
|
||
}
|
||
|
||
// 隐藏旧消息,保留最后 limit 条
|
||
const hideCount = total - limit;
|
||
for (let i = 0; i < total; i++) {
|
||
if (i < hideCount) {
|
||
messages[i].style.setProperty('display', 'none', 'important');
|
||
} else {
|
||
messages[i].style.removeProperty('display');
|
||
}
|
||
}
|
||
console.log(`[Amily2-性能优化] 已隐藏 ${hideCount} 条旧消息,仅显示最近 ${limit} 条。`);
|
||
}
|
||
|
||
// 监听聊天更新事件以应用限制
|
||
eventSource.on(event_types.MESSAGE_RECEIVED, () => setTimeout(applyMessageLimit, 100));
|
||
eventSource.on(event_types.chat_updated, () => setTimeout(applyMessageLimit, 100));
|
||
|
||
function initializeOnlineTracker() {
|
||
const wsUrl = 'wss://amilyservice.amily49.cc';
|
||
|
||
let ws = null;
|
||
let reconnectTimer = null;
|
||
let isConnecting = false;
|
||
|
||
function mountTracker() {
|
||
const $drawerContent = $('#amily2_drawer_content');
|
||
if ($drawerContent.length === 0 || !$drawerContent.data('initialized')) {
|
||
setTimeout(mountTracker, 1000);
|
||
return;
|
||
}
|
||
if ($('#amily2-online-tracker').length > 0) return;
|
||
const $container = $('<div id="amily2-online-tracker" style="text-align: center; padding: 8px; font-size: 13px; color: rgba(255,255,255,0.7); border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 10px; background: rgba(0,0,0,0.1); border-radius: 5px;"></div>');
|
||
$container.html('<i class="fas fa-users" style="color: #4caf50; font-size: 12px; vertical-align: middle; margin-right: 6px;"></i><span id="amily2-online-count" style="vertical-align: middle; font-weight: bold;">Connecting...</span>');
|
||
|
||
$drawerContent.prepend($container);
|
||
|
||
connect();
|
||
}
|
||
|
||
function connect() {
|
||
// 单例模式检查:如果已有连接且处于连接中或打开状态,则不重复创建
|
||
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
|
||
console.log('[Amily2-在线统计] 连接已存在,跳过创建');
|
||
return;
|
||
}
|
||
|
||
// 防止短时间内重复调用
|
||
if (isConnecting) return;
|
||
isConnecting = true;
|
||
|
||
// 清理旧连接
|
||
if (ws) {
|
||
try {
|
||
ws.close();
|
||
} catch (e) {}
|
||
ws = null;
|
||
}
|
||
|
||
try {
|
||
console.log('[Amily2-在线统计] 开始建立连接...');
|
||
ws = new WebSocket(wsUrl);
|
||
|
||
ws.onopen = () => {
|
||
console.log('[Amily2-在线统计] 已连接到服务器');
|
||
isConnecting = false;
|
||
if (reconnectTimer) {
|
||
clearTimeout(reconnectTimer);
|
||
reconnectTimer = null;
|
||
}
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
if (data.type === 'online_count') {
|
||
$('#amily2-online-count').text(`${data.count} 人在线`);
|
||
}
|
||
} catch (e) {
|
||
console.error('[Amily2-在线统计] 解析消息失败:', e);
|
||
}
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
console.log('[Amily2-在线统计] 连接断开');
|
||
$('#amily2-online-count').text('离线');
|
||
isConnecting = false;
|
||
ws = null;
|
||
|
||
// 延迟重连,而不是立即循环
|
||
if (!reconnectTimer) {
|
||
reconnectTimer = setTimeout(() => {
|
||
reconnectTimer = null;
|
||
connect();
|
||
}, 5000);
|
||
}
|
||
};
|
||
|
||
ws.onerror = (err) => {
|
||
console.warn('[Amily2-在线统计] 连接错误:', err);
|
||
// onerror 通常会触发 onclose,所以这里不需要额外的重连逻辑,交给 onclose 处理
|
||
};
|
||
} catch (e) {
|
||
console.error('[Amily2-在线统计] 初始化失败:', e);
|
||
isConnecting = false;
|
||
if (!reconnectTimer) {
|
||
reconnectTimer = setTimeout(() => {
|
||
reconnectTimer = null;
|
||
connect();
|
||
}, 5000);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 启动挂载流程
|
||
mountTracker();
|
||
}
|
||
|
||
function initializeLocalLinkage() {
|
||
const wsUrl = 'ws://127.0.0.1:2086';
|
||
let ws = null;
|
||
let retryCount = 0;
|
||
const maxRetries = 5;
|
||
|
||
function connect() {
|
||
if (retryCount >= maxRetries) {
|
||
console.log('[Amily2-本地联动] 达到最大重试次数,停止连接本地服务。');
|
||
return;
|
||
}
|
||
|
||
console.log('[Amily2-本地联动] 尝试连接本地联动服务...');
|
||
ws = new WebSocket(wsUrl);
|
||
|
||
ws.onopen = () => {
|
||
console.log('[Amily2-本地联动] 已连接到启动器服务');
|
||
if (window.toastr) toastr.success('已连接到 Amily 启动器', '本地联动');
|
||
retryCount = 0; // 连接成功,重置计数
|
||
};
|
||
|
||
ws.onmessage = async (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
if (data.type === 'command') {
|
||
console.log('[Amily2-本地联动] 收到指令:', data.command, data.args);
|
||
if (data.command === 'triggerSlash') {
|
||
if (window.AmilyHelper) {
|
||
await window.AmilyHelper.triggerSlash(data.args.content);
|
||
}
|
||
} else if (data.command === 'cleanOldMessages') {
|
||
const keep = parseInt(data.args.keep) || 50;
|
||
if (window.AmilyHelper) {
|
||
const total = window.AmilyHelper.getLastMessageId() + 1;
|
||
if (total > keep) {
|
||
const deleteCount = total - keep;
|
||
// 生成要删除的 ID 列表 (0 到 deleteCount - 1)
|
||
const idsToDelete = Array.from({length: deleteCount}, (_, i) => i);
|
||
await window.AmilyHelper.deleteChatMessages(idsToDelete, { refresh: 'all' });
|
||
if (window.toastr) window.toastr.success(`已清理 ${deleteCount} 条旧消息,保留最近 ${keep} 条`, '清理完成');
|
||
} else {
|
||
if (window.toastr) window.toastr.info('消息数量未超过保留限制,无需清理', '无需清理');
|
||
}
|
||
}
|
||
} else if (data.command === 'setMaxMessages') {
|
||
const limit = parseInt(data.args.limit);
|
||
if (!isNaN(limit) && limit > 0) {
|
||
window.amily2MaxMessages = limit;
|
||
applyMessageLimit();
|
||
if (window.toastr) window.toastr.success(`已限制显示最近 ${limit} 条消息`, '性能优化');
|
||
}
|
||
}
|
||
// 这里可以扩展更多指令
|
||
}
|
||
} catch (e) {
|
||
console.error('[Amily2-本地联动] 处理消息失败:', e);
|
||
}
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
console.log('[Amily2-本地联动] 连接断开');
|
||
retryCount++;
|
||
if (retryCount < maxRetries) {
|
||
console.log(`[Amily2-本地联动] ${5}秒后尝试重连 (${retryCount}/${maxRetries})`);
|
||
setTimeout(connect, 5000);
|
||
} else {
|
||
console.log('[Amily2-本地联动] 已停止重连尝试。');
|
||
}
|
||
};
|
||
|
||
ws.onerror = (err) => {
|
||
// console.warn('[Amily2-本地联动] 连接错误:', err);
|
||
};
|
||
}
|
||
|
||
connect();
|
||
}
|