37 Commits

Author SHA1 Message Date
2cc1379260 Update version number in manifest.json
Updated version number from 1.8.4-a to 1.8.4 in manifest.json.
2026-01-24 17:50:40 +08:00
6cc8b3f358 Update print statement from 'Hello' to 'Goodbye' 2026-01-22 00:04:08 +08:00
a7e97eb127 Update print statement from 'Hello' to 'Goodbye' 2026-01-22 00:02:12 +08:00
47575e763f Refactor HTML structure and styles for amily2 modal 2026-01-22 00:00:16 +08:00
c278972b75 Update print statement from 'Hello' to 'Goodbye' 2026-01-21 23:56:08 +08:00
b77122d025 更新版本信息为1.8.4-a 2026-01-20 12:52:34 +08:00
60befb69ea 页面调整 2026-01-20 12:00:28 +08:00
ab161e475d 添加假流功能的设置选项和事件绑定 2026-01-20 11:56:58 +08:00
859588461d fix 2026-01-20 11:36:33 +08:00
3043c3d80a rollback 2026-01-20 11:32:54 +08:00
40c3d6c735 fix 2026-01-20 11:21:14 +08:00
91e85aec94 fix 2026-01-20 10:58:41 +08:00
70224a6b61 fix 2026-01-20 10:45:41 +08:00
53ff9bd307 fix 2026-01-20 10:36:44 +08:00
4c61837b93 优化 _buildProfilePayload 方法,全面继承 Profile 属性并规范化关键字段,确保参数完整性和兼容性 2026-01-20 10:30:31 +08:00
1136707f1f 重构参数合并逻辑,封装 Profile 参数提取至 _buildProfilePayload 方法,确保后端正确应用预设配置 2026-01-20 10:20:28 +08:00
3ba4e39193 修复预设模式下的参数合并逻辑,确保后端正确应用配置,优化请求有效性 2026-01-20 10:17:15 +08:00
8f2b91c36c 优化 callNccsAI 函数,增强配置管理和兼容性,自动收集额外参数,改进日志记录 2026-01-20 10:03:34 +08:00
56017782bb 重构项目架构,添加 Nccs API 设置,优化流式支持逻辑 2026-01-20 09:51:21 +08:00
398649c754 增强 NccsApi 和 Amily2Bus 的注册逻辑,添加 FakeStream 设置选项,优化初始化流程 2026-01-18 23:01:34 +08:00
dedc4418c7 fix 2026-01-18 12:42:38 +08:00
a5f3c92fa3 优化 ModelCaller 的日志记录和流式处理逻辑,增强 SSE 解析能力 2026-01-18 12:38:37 +08:00
3d16bb20bb 移除 Logger 类中 process 方法的调试输出,使用注释替代 2026-01-18 11:59:48 +08:00
33f09a615e 修复 ModelCaller 中的导入路径,确保正确引用 amilyHelper 2026-01-18 11:42:13 +08:00
d405f1d624 添加 ModelCaller 和 Options 类,重构 Logger,增强 Amily2Bus 架构,更新 README 和 TODO 文件 2026-01-18 11:40:35 +08:00
4e9f2defb7 Merge branch 'LICENCE' into SL-Dev-2601 2026-01-18 10:14:57 +08:00
4895a259e6 重调整项目目录结构 2026-01-18 04:41:19 +08:00
6ab18f545e 重调整项目目录结构 2026-01-18 04:34:21 +08:00
9625974d33 重调整项目目录结构 2026-01-18 04:31:28 +08:00
256b295739 增强日志记录功能,添加安全控制台以避免劫持,并在日志处理时输出调试信息 2026-01-16 15:56:20 +08:00
b9df92435a Debug 2026-01-16 11:03:39 +08:00
c56780570a little fix... 2026-01-16 10:39:06 +08:00
f9b2f35828 初步实现Logger功能 2026-01-16 10:24:48 +08:00
cb54fc3eb4 little fix... 2026-01-16 10:06:47 +08:00
3d051d69a1 AmilyBus注入初始化 2026-01-16 00:25:41 +08:00
e370ce7d3f 进行解耦合功能拆分处理 2026-01-15 23:22:55 +08:00
46fea93115 总线开发工程-init 2026-01-15 17:41:51 +08:00
29 changed files with 5216 additions and 3906 deletions

180
SL/bus/Amily2Bus.js Normal file
View File

@@ -0,0 +1,180 @@
import Logger from './log/Logger.js';
import FilePipe from './file/FilePipe.js';
import ModelCaller from './api/ModelCaller.js';
import Options from './api/Options.js';
// 【逃生通道】创建一个纯净的 Console 对象,绕过任何潜在的劫持
const getSafeConsole = () => {
try {
if (window._amilySafeConsole) return window._amilySafeConsole;
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
const safe = iframe.contentWindow.console;
// document.body.removeChild(iframe); // 保持 iframe 以维持 console 引用有效
window._amilySafeConsole = safe;
return safe;
} catch (e) {
return window.console; // Fallback
}
};
class Amily2Bus {
constructor() {
this.safeConsole = getSafeConsole();
// 1. 初始化 Logger
/** @type {Logger} */
this.Logger = new Logger(this.safeConsole);
/** @type {FilePipe} */
this.FilePipe = new FilePipe();
// 2. 依赖注入 (Dependency Injection)
// 创建一个 Logger 代理适配器传给 ModelCaller
const loggerDelegate = {
log: (type, message, origin, plugin) => {
// 回调 Bus 的 Logger 实例
this.Logger.process(plugin || 'Global', origin || 'Model', type, message);
}
};
// ModelCaller 不再包含 Bus只包含 logger 代理
/** @type {ModelCaller} */
this.ModelCaller = new ModelCaller(loggerDelegate);
// 存储上下文引用(严格锁:每个插件名仅限一次成功注册)
this.registry = new Map();
// 存储公开的联动接口(联动模块)
this.publicRegistry = new Map();
this.safeConsole.log('[Amily2Bus] Core Initialized (Decoupled Architecture).');
// 3. 自动注册并锁定 PUBLIC 命名空间
this._initPublicNamespace();
this.register('Amily2');
}
/**
* 初始化系统保留的 PUBLIC 模块
* 用于提供系统级信息的联动查询,防止 PUBLIC 命名被滥用
*/
_initPublicNamespace() {
try {
// 这里利用 register 的机制,直接抢占 'PUBLIC' 并加上严格锁
const sysCtx = this.register('PUBLIC');
// 暴露系统级能力给 query('PUBLIC')
sysCtx.expose({
description: 'Amily2 System Public Interface',
version: '2.0.0-Core',
// 允许查询当前有哪些插件暴露了公共接口
getAvailableModules: () => {
return Array.from(this.publicRegistry.keys());
},
// 允许查询当前所有已注册(被锁定的)插件名
getRegisteredPlugins: () => {
return Array.from(this.registry.keys());
},
// 简单的系统状态检查
ping: () => 'pong'
});
// 内部记录一条初始化日志
sysCtx.log('System', 'info', 'PUBLIC namespace reserved and strictly locked.');
} catch (e) {
this.safeConsole.error('[Amily2Bus] CRITICAL: Failed to init PUBLIC namespace.', e);
}
}
/**
* 直接记录系统级日志 (Global Scope)
* 支持手动指定来源,方便终端调试或非插件环境调用
* @param {string} type 日志级别 (debug, info, warn, error)
* @param {string} message 消息内容
* @param {string} [origin='Bus' 来源模块,默认为 'Bus'
* @param {string} [plugin='Global'] 来源插件/命名空间,调试时可指定如 'Console'
*/
log(type, message, origin = 'Bus', plugin = 'Global') {
if (this.Logger) {
this.Logger.process(plugin, origin, type, message);
}
}
/**
* 注册插件并获取专属上下文 (严格锁机制)
* @param {string} pluginName 插件名称
* @returns {Object} 包含该插件专属 API 的上下文对象
*/
register(pluginName) {
if (!pluginName || typeof pluginName !== 'string') {
throw new Error('[Amily2Bus] Invalid plugin name.');
}
if (this.registry.has(pluginName)) {
const errorMsg = `[Amily2Bus] Security Error: Plugin '${pluginName}' is already registered and locked.`;
this.safeConsole.error(errorMsg);
throw new Error(errorMsg);
}
// 返回该插件专属的 API 上下文 (Capability Token)
const context = {
// 1. 日志能力 (绑定了身份的日志接口)
log: (origin, type, message) => this.Logger.log(pluginName, origin, type, message),
// 2. 文件能力 (绑定了身份的文件接口)
file: {
read: (path) => {
return this.FilePipe ? this.FilePipe.read(pluginName, path) : null;
},
write: (path, data) => {
return this.FilePipe ? this.FilePipe.write(pluginName, path, data) : false;
}
},
// 3. 网络能力 (ModelCaller)
model: {
// 暴露 Options 类,方便插件直接 new context.model.Options() 或使用 builder
Options: Options,
// 插件调用时Bus 负责将 pluginName 传给无状态的 ModelCaller
call: (messages, options) => this.ModelCaller.call(pluginName, messages, options)
},
/**
* 将本插件的能力暴露给公共查询池,实现插件间联动
* @param {Object} apiMethods
*/
expose: (apiMethods) => {
if (typeof apiMethods !== 'object') throw new Error('Exposed API must be an object');
this.publicRegistry.set(pluginName, Object.freeze(apiMethods));
this.log('info', `Module exposed to public registry.`, 'Bus', pluginName);
}
};
this.registry.set(pluginName, context);
this.safeConsole.log(`[Amily2Bus] Plugin registered: ${pluginName}`);
return context;
}
/**
* 联动查询:获取其他插件通过 expose 暴露的能力
* @param {string} pluginName 目标插件名称
* @returns {Object|null}
*/
query(pluginName) {
return this.publicRegistry.get(pluginName) || null;
}
}
// 挂载全局单例
if (!window.Amily2Bus) {
window.Amily2Bus = new Amily2Bus();
}
export function initializeAmilyBus() {
if (!(window.Amily2Bus instanceof Amily2Bus)) {
window.Amily2Bus = new Amily2Bus();
console.log('[Amily2] Amily2Bus 已成功初始化并附加到 window 对象');
}
}

111
SL/bus/README.md Normal file
View File

@@ -0,0 +1,111 @@
# Amily2Bus (Amily2 总线系统)
Amily2Bus 是 Amily2-Chat-Optimisation 插件系统的核心基础设施。它为所有子模块和外部插件提供了一个规范化、安全且高兼容性的运行环境。
## 核心特性
- **安全控制台 (SafeConsole)**: 通过 Iframe 逃生通道获取纯净 Console 引用,绕过 SillyTavern 等环境的日志劫持。
- **能力令牌 (Capability Token)**: 插件注册后获取专属上下文,自动绑定身份,实现日志追踪与文件隔离。
- **防超时网络层 (FakeStream)**: `ModelCaller` 支持伪流式聚合,通过持续保持 TCP 连接活跃,彻底解决 CloudFlare 524 超时问题。
- **位运算日志系统**: 基于位掩码的日志级别控制,支持针对特定插件或模块动态调整输出粒度。
- **异步责任链**: 预置 `Chain` 模块,支持插件化的异步中间件处理流程。
---
## 快速开始
### 1. 初始化
总线通常在系统启动时自动挂载到 `window.Amily2Bus`
```javascript
import { initializeAmilyBus } from './SL/bus/Amily2Bus.js';
initializeAmilyBus();
```
### 2. 插件注册
所有插件必须注册以获取专属上下文:
```javascript
const amily = window.Amily2Bus.register('MyAwesomePlugin');
```
---
## 模块说明
### 1. 标准日志 (Logger)
支持 `debug`, `info`, `warn`, `error` 四个级别。
```javascript
// 自动绑定插件名,输出格式: [时间] [MyAwesomePlugin::Main] [INFO]: 消息内容
amily.log('Main', 'info', '插件已就绪');
```
### 2. 网络请求 (ModelCaller & Options)
统一处理 API 调用,支持自动切换 ST 配置文件 (Profile) 及防超时处理。
```javascript
const { Options } = amily.model;
const opt = Options.builder()
.setMode('direct') // 'direct' (直连) 或 'preset' (ST预设)
.setFakeStream(true) // 开启伪流式聚合,防止 524 超时
.setApiUrl('...')
.setApiKey('...')
.setModel('gpt-4o')
.build();
const result = await amily.model.call(messages, opt);
```
### 3. 文件操作 (FilePipe)
提供基于插件命名的虚拟文件系统隔离,防止插件间非法访问。
```javascript
// 写入文件 (自动定位到 /virtual_fs/MyAwesomePlugin/config.json)
await amily.file.write('config.json', { theme: 'dark' });
// 读取文件
const data = await amily.file.read('config.json');
```
### 4. 责任链 (Chain)
用于处理复杂的、可扩展的异步逻辑流。
```javascript
import { Chain } from './SL/bus/chain/Chain.js';
const pipeline = new Chain();
pipeline.use(async (ctx, next) => {
ctx.data += " -> 步骤1处理";
await next();
});
const context = { data: "开始" };
await pipeline.execute(context);
```
---
## 目录结构
- `Amily2Bus.js`: 总线入口,协调各模块。
- `log/Logger.js`: 位运算日志管理器。
- `file/FilePipe.js`: 安全文件操作管道。
- `api/ModelCaller.js`: 核心 API 调用器。
- `api/Options.js`: API 请求配置构建器。
- `chain/Chain.js`: 异步责任链工具。
---
## 开发规范
1. **强制类型**: `model.call` 必须接收 `Options` 类的实例,建议始终使用 `Options.builder()` 构建参数。
2. **路径安全**: 使用 `file` 接口时,禁止在路径中使用 `..` 等跳转符,系统会自动进行安全校验。
3. **日志分级**: 生产环境默认屏蔽 `debug` 级别,调试时可通过 `window.Amily2Bus.Logger.setLevel('PluginName', 'all')` 动态开启。

323
SL/bus/api/ModelCaller.js Normal file
View File

@@ -0,0 +1,323 @@
import { getRequestHeaders } from "/script.js";
import { getContext, extension_settings } from "/scripts/extensions.js";
import { amilyHelper } from '../../../core/tavern-helper/main.js';
import Options from './Options.js';
import RequestBody from './RequestBody.js';
/**
* ModelCaller Service
* 负责执行 API 调用逻辑,旨在替换 NccsApi 及其他散乱的请求逻辑。
* 支持标准直连、ST预设调用、伪流式聚合(防超时)、数据归一化。
*/
export default class ModelCaller {
/**
* 构造函数注入 Logger 依赖
* @param {Object} loggerDelegate - { log: (level, msg, origin, plugin) => void }
*/
constructor(loggerDelegate) {
/** @type {Object} */
this.logger = loggerDelegate;
this.defaultHeaders = {
'Content-Type': 'application/json'
};
}
/**
* 统一调用入口
* @param {string} callerName - 调用者名称(日志用)
* @param {Array} messages - 聊天消息历史
* @param {Options} options - 配置对象实例
* @returns {Promise<string>} - 返回归一化后的文本内容
*/
async call(callerName, messages, options) {
// 1. 强制类型检查
if (!(options instanceof Options)) {
const errorMsg = `[ModelCaller] Options must be instance of Options class.`;
throw new TypeError(errorMsg);
}
// 2. 逻辑中直接使用 options 属性
// 记录一下当前的流模式,方便调试
this._log('info', `API Request [${options.mode}] StreamMode: ${options.fakeStream}`, callerName);
try {
// 统一构建请求体 DTO
const requestBody = new RequestBody(messages, options);
let result;
if (options.mode === 'preset') {
result = await this._callPreset(callerName, requestBody, options);
} else {
result = await this._callDirect(callerName, requestBody, options);
}
// 如果是流式返回result 已经是拼接好的字符串,不需要 normalize 的部分逻辑
// 但为了统一,我们还是传进去检查一下
return this._normalize(result, options.fakeStream);
} catch (error) {
this._log('error', `Request Failed: ${error.message}`, callerName);
throw error;
}
}
// 内部日志封装
_log(level, msg, plugin) {
if (this.logger?.log) {
this.logger.log(level, msg, 'ModelCaller', plugin);
}
}
// ========================================================================
// 模式一Direct (标准直连)
// 对应 NccsApi 中的 callNccsOpenAITest
// ========================================================================
async _callDirect(callerName, requestBody, options) {
// 构建标准 OpenAI 兼容 Body
// 目标通常是 ST 的后端代理接口
const url = '/api/backends/chat-completions/generate';
const payload = requestBody.toPayload(); // 使用 DTO 生成数据
const fetchOpts = {
method: 'POST',
headers: { ...getRequestHeaders(), ...this.defaultHeaders },
body: JSON.stringify(payload)
};
return options.fakeStream
? this._fetchFakeStream(url, fetchOpts)
: this._fetchStandard(url, fetchOpts);
}
// ========================================================================
// 模式二Preset (ST预设调用)
// 对应 NccsApi 中的 callNccsSillyTavernPreset
// ========================================================================
async _callPreset(callerName, requestBody, options) {
const context = getContext();
// 1. 记录并切换 Profile
const originalProfile = await amilyHelper.triggerSlash('/profile');
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === options.presetId);
if (!targetProfile) throw new Error(`Preset ID ${options.presetId} not found`);
if (originalProfile !== targetProfile.name) {
this._log('info', `Switching profile: ${originalProfile} -> ${targetProfile.name}`, callerName);
const escapedName = targetProfile.name.replace(/"/g, '\\"');
await amilyHelper.triggerSlash(`/profile await=true "${escapedName}"`);
}
try {
// 2. 根据流式需求分流处理
if (options.fakeStream) {
// 【流式预设请求】
// 难点ST 的 ConnectionManagerRequestService 不暴露流。
// 策略:切换 Profile 后,手动向生成接口发送请求。
const url = '/api/backends/chat-completions/generate';
// [修复]: 手动合并 Profile 中的关键参数,否则后端不会自动应用预设配置
// 提取逻辑已封装至 _buildProfilePayload
const profilePayload = this._buildProfilePayload(targetProfile);
// 合并顺序基础Payload(msg) < Profile预设 < 显式Params覆盖
// toMinimalPayload 包含: messages, stream, max_tokens, ...params
const minimal = requestBody.toMinimalPayload();
const finalPayload = {
...profilePayload,
...minimal,
...options.params
};
const fetchOpts = {
method: 'POST',
headers: { ...getRequestHeaders(), ...this.defaultHeaders },
body: JSON.stringify(finalPayload)
};
return await this._fetchFakeStream(url, fetchOpts);
} else {
// 【非流式预设请求】
// 直接使用 ST 原生服务,最稳妥
if (!context.ConnectionManagerRequestService) throw new Error('ST Request Service unavailable');
return await context.ConnectionManagerRequestService.sendRequest(
targetProfile.id,
requestBody.messages,
options.maxTokens
);
}
} finally {
// 3. 恢复 Profile
if (originalProfile) {
try {
const current = await amilyHelper.triggerSlash('/profile');
if (originalProfile !== current) {
const escapedOriginal = originalProfile.replace(/"/g, '\\"');
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginal}"`);
}
} catch (e) {
this._log('warn', `Failed to restore profile: ${e.message}`, callerName);
}
}
}
}
// ========================================================================
// 网络层核心
// ========================================================================
async _fetchStandard(url, opts) {
const res = await fetch(url, opts);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
}
// 【核心升级】:支持 SSE 解析的伪流式聚合,防 CloudFlare 超时
async _fetchFakeStream(url, opts) {
const res = await fetch(url, opts);
if (!res.ok) throw new Error(`Stream HTTP ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullContent = ""; // 用于存储最终拼接的纯文本
let buffer = ""; // 用于存储未处理完的数据片段
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 1. 解码当前数据包
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// 2. 处理 SSE 格式 (data: {...})
// 以双换行符分割每一条 SSE 消息
const lines = buffer.split('\n');
// 保留最后一个可能不完整的片段在 buffer 中
buffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed === 'data: [DONE]') continue;
if (trimmed.startsWith('data: ')) {
try {
const jsonStr = trimmed.substring(6); // 去掉 'data: '
const json = JSON.parse(jsonStr);
// 提取 delta content
const delta = json.choices?.[0]?.delta?.content;
if (delta) {
fullContent += delta;
}
} catch (e) {
// 忽略解析错误的行,防止因为个别丢包导致整个请求失败
console.warn('[ModelCaller] SSE Parse Error:', e);
}
}
}
}
} finally {
reader.releaseLock();
}
// 如果 fullContent 是空的,说明可能服务端根本没返回 SSE 格式,而是直接返回了纯文本或 JSON
// 这种情况下尝试降级处理
if (!fullContent && buffer) {
try {
const json = JSON.parse(buffer);
return json; // 是标准 JSON
} catch {
return buffer; // 是纯文本
}
}
return fullContent;
}
// ========================================================================
// 数据归一化
// ========================================================================
_normalize(data, isFromStream = false) {
// 如果是从流式聚合来的,它已经是一个纯字符串了,直接返回
if (isFromStream && typeof data === 'string') {
return data;
}
// 如果是 JSON 字符串则解析
if (typeof data === 'string') {
try { data = JSON.parse(data); } catch (e) { return data; }
}
// 处理 OpenAI 格式
if (data?.choices?.[0]?.message?.content) {
return data.choices[0].message.content.trim();
}
// 处理常规 content 格式
if (data?.content) {
return data.content.trim();
}
// Fallback
return typeof data === 'object' ? JSON.stringify(data) : data;
}
/**
* 辅助方法:从 Profile 对象中提取标准生成参数
* 严格复刻 SillyTavern 原始 Payload 逻辑
*/
_buildProfilePayload(targetProfile) {
const context = getContext();
// 1. 基础克隆
const payload = { ...targetProfile };
// 2. 注入运行时元数据 (这是旧版能通的关键,包含用户/角色名等)
payload.user_name = context.name1 || 'User';
payload.char_name = context.name2 || 'AI';
payload.group_names = []; // 暂不处理群组
payload.use_sysprompt = true;
payload.type = 'quiet';
payload.custom_prompt_post_processing = payload.custom_prompt_post_processing || 'strict';
// 3. 规范化模型字段
if (!payload.model) {
payload.model = payload.openai_model || payload.claude_model || payload.mistral_model || '';
}
// 4. 精准对齐 URL 映射 (解决 403/400 错误的核心)
const rawUrl = payload['api-url'] || payload['api_url'] || payload.custom_url || payload.url;
if (rawUrl) {
// 如果 Source 是 custom严格遵循旧版custom_url 有值reverse_proxy 为空
if (payload.chat_completion_source === 'custom') {
payload.custom_url = rawUrl;
payload.reverse_proxy = payload.reverse_proxy || '';
} else {
// 如果是 openai则填充 reverse_proxy
payload.reverse_proxy = rawUrl;
payload.custom_url = rawUrl;
}
// 兼容性修补
payload.zai_endpoint = rawUrl;
payload.vertexai_region = rawUrl;
}
// 5. 补全采样参数 (严格对齐 UI 当前状态)
const globalGenSettings = extension_settings.text_generation || {};
const fields = ['temperature', 'max_tokens', 'top_p', 'top_k', 'min_p', 'frequency_penalty', 'presence_penalty', 'repetition_penalty'];
fields.forEach(field => {
if (payload[field] === undefined) {
payload[field] = globalGenSettings[field] ?? (field === 'temperature' ? 1 : 0);
}
});
// 6. 确保 Source 存在且不被错误覆盖
if (!payload.chat_completion_source) {
payload.chat_completion_source = 'openai';
}
return payload;
}
}

98
SL/bus/api/Options.js Normal file
View File

@@ -0,0 +1,98 @@
/**
* ModelCaller 请求配置类
* 支持构造函数直接传入对象,或使用 Builder 链式调用
*/
export class Options {
constructor(config = {}) {
/** @type {'direct'|'preset'} */
this.mode = config.mode || 'direct';
/** @type {boolean} */
this.fakeStream = config.fakeStream ?? false;
/** @type {string} */
this.apiUrl = config.apiUrl || '';
/** @type {string} */
this.apiKey = config.apiKey || '';
/** @type {string} */
this.model = config.model || '';
/** @type {string} */
this.presetId = config.presetId || '';
/** @type {number} */
this.maxTokens = config.maxTokens || 4000;
/** @type {number} */
this.temperature = config.temperature || 0.7;
/** @type {Object} 额外透传参数 */
this.params = config.params || {};
}
/**
* 获取 Builder 实例
* @returns {OptionsBuilder}
*/
static builder() {
return new OptionsBuilder();
}
}
/**
* Options 构建器类
*/
class OptionsBuilder {
constructor() {
this.config = {};
}
setMode(mode) {
this.config.mode = mode;
return this;
}
setFakeStream(enabled) {
this.config.fakeStream = enabled;
return this;
}
setApiUrl(url) {
this.config.apiUrl = url;
return this;
}
setApiKey(key) {
this.config.apiKey = key;
return this;
}
setModel(model) {
this.config.model = model;
return this;
}
setPresetId(id) {
this.config.presetId = id;
return this;
}
setMaxTokens(tokens) {
this.config.maxTokens = tokens;
return this;
}
setTemperature(temp) {
this.config.temperature = temp;
return this;
}
setParams(params) {
this.config.params = { ...(this.config.params || {}), ...params };
return this;
}
/**
* 构建最终的 Options 对象
* @returns {Options}
*/
build() {
return new Options(this.config);
}
}
export default Options;

74
SL/bus/api/RequestBody.js Normal file
View File

@@ -0,0 +1,74 @@
import Options from './Options.js';
/**
* RequestBody (DTO)
* 严格约束发送给 LLM/ST 后端的请求体结构
* 类似于 Java 中的 RequestBean
*/
export class RequestBody {
/**
* @param {Array} messages
* @param {Options} options
*/
constructor(messages, options) {
if (!Array.isArray(messages)) throw new TypeError('messages must be an array');
if (!(options instanceof Options)) throw new TypeError('options must be an instance of Options');
this.messages = messages;
this.options = options;
}
/**
* 构建标准 OpenAI 兼容的 Payload
* @returns {Object} 纯净的 JSON 对象
*/
toPayload() {
const { apiUrl, apiKey, model, maxTokens, temperature, params, fakeStream } = this.options;
const isGoogle = apiUrl && apiUrl.includes('googleapis.com');
// 基础字段 (Base Fields)
const payload = {
chat_completion_source: 'openai',
messages: this.messages,
model: model,
reverse_proxy: apiUrl,
proxy_password: apiKey,
// 【核心修正】: 如果客户端开启防超时聚合(fakeStream)
// 必须告诉服务端开启流式传输,否则服务端不会分块发送数据。
stream: fakeStream,
max_tokens: maxTokens,
temperature: temperature,
// 允许 Options 中的 params 覆盖上述字段
...params
};
// 平台特定字段处理 (Platform Specific Logic)
if (!isGoogle) {
Object.assign(payload, {
custom_prompt_post_processing: 'strict',
presence_penalty: 0.12,
include_reasoning: false,
reasoning_effort: 'medium'
});
}
return payload;
}
/**
* 仅获取消息体的 Payload (用于 Preset 模式)
*/
toMinimalPayload() {
return {
messages: this.messages,
// 同样需要联动
stream: this.options.fakeStream,
max_tokens: this.options.maxTokens,
...this.options.params
};
}
}
export default RequestBody;

56
SL/bus/chain/Chain.js Normal file
View File

@@ -0,0 +1,56 @@
/**
* 通用责任链/中间件管理器
* 用于规范操作顺序,支持异步流程控制
*/
export class Chain {
constructor() {
this.middlewares = [];
}
/**
* 注册中间件
* @param {Function} fn (context, next) => Promise<void> | void
*/
use(fn) {
if (typeof fn !== 'function') {
throw new Error('[Chain] Middleware must be a function');
}
this.middlewares.push(fn);
return this;
}
/**
* 执行责任链
* @param {Object} context 传递给中间件的上下文对象
*/
async execute(context = {}) {
let index = -1;
const dispatch = async (i) => {
if (i <= index) {
throw new Error('[Chain] next() called multiple times in one middleware');
}
index = i;
const fn = this.middlewares[i];
if (!fn) return; // 链结束
try {
// 执行中间件,传入 context 和 next 函数
await fn(context, () => dispatch(i + 1));
} catch (err) {
console.error('[Chain] Middleware execution error:', err);
throw err;
}
};
await dispatch(0);
}
/**
* 清空链
*/
clear() {
this.middlewares = [];
}
}

61
SL/bus/file/FilePipe.js Normal file
View File

@@ -0,0 +1,61 @@
class FilePipe {
constructor() {
this.name = "FilePipe";
// 模拟的根存储路径,实际环境可能对应 IndexedDB 的 Store Name 或 Node 的 baseDir
this.basePath = "/virtual_fs/";
}
/**
* 安全路径解析与校验
* @param {string} plugin 插件名称(命名空间)
* @param {string} relativePath 相对路径
* @returns {string|null} 合法的绝对路径,如果违规则返回 null
*/
_resolvePath(plugin, relativePath) {
if (!plugin || typeof plugin !== 'string') {
console.error(`[FilePipe] Security Error: Invalid plugin identity.`);
return null;
}
// 简单防越权:禁止包含 ".."
if (relativePath.includes('..')) {
console.error(`[FilePipe] Security Error: Directory traversal attempt blocked for plugin '${plugin}'. Path: ${relativePath}`);
return null;
}
// 强制限定在插件目录下
// 格式: /virtual_fs/PluginName/filename
return `${this.basePath}${plugin}/${relativePath}`;
}
/**
* 读取文件
* @param {string} plugin 调用方插件名
* @param {string} path 文件相对路径
*/
async read(plugin, path) {
const safePath = this._resolvePath(plugin, path);
if (!safePath) return null;
console.log(`[FilePipe] Reading from: ${safePath}`);
// TODO: Implement actual file reading logic
return null;
}
/**
* 写入文件
* @param {string} plugin 调用方插件名
* @param {string} path 文件相对路径
* @param {any} data 数据
*/
async write(plugin, path, data) {
const safePath = this._resolvePath(plugin, path);
if (!safePath) return false;
console.log(`[FilePipe] Writing to: ${safePath}`);
// TODO: Implement actual file writing logic
return true;
}
}
export default FilePipe;

220
SL/bus/log/Logger.js Normal file
View File

@@ -0,0 +1,220 @@
/**
* 日志总类,用于记录日志信息
* 支持基于位运算的自定义日志级别控制
*/
class Logger {
static LOG_HEADER_DEBUG = '[DEBUG]';
static LOG_HEADER_INFO = '[INFO]';
static LOG_HEADER_WARN = '[WARN]';
static LOG_HEADER_ERROR = '[ERROR]';
static LOG_LEVEL_CODE = {
none: 0x0, // 0
debug: 0x1, // 1
info: 0x2, // 2
warn: 0x4, // 4
error: 0x8, // 8
all: 0xF // 15
};
constructor(safeConsole = null) {
// 使用传入的 safeConsole如果没有则回退到全局 console
this.safeConsole = safeConsole || (typeof window !== 'undefined' ? window.console : console);
// 全局默认级别 (默认开启 info, warn, error)
this.globalLevel = Logger.LOG_LEVEL_CODE.info | Logger.LOG_LEVEL_CODE.warn | Logger.LOG_LEVEL_CODE.error;
// 针对特定插件或模块的配置
// 结构示例:
// {
// "PluginA": 3, // PluginA 下所有模块掩码为 3 (debug | info)
// "PluginB::ModuleX": 8 // 仅 PluginB 下的 ModuleX 掩码为 8 (error)
// }
this.levelConfig = {};
}
/**
* 将输入转换为对应的日志级别掩码
* @param {number|string|string[]} levelInput
* @returns {number} 掩码
*/
_parseLevelInput(levelInput) {
if (typeof levelInput === 'number') {
return levelInput;
}
if (typeof levelInput === 'string') {
if (Logger.LOG_LEVEL_CODE.hasOwnProperty(levelInput)) {
return Logger.LOG_LEVEL_CODE[levelInput];
}
// 支持 "debug|info" 这种写法
if (levelInput.includes('|')) {
return levelInput.split('|').reduce((mask, l) => mask | (Logger.LOG_LEVEL_CODE[l.trim()] || 0), 0);
}
this.safeConsole.warn(`[Logger] Unknown log level string: ${levelInput}`);
return 0;
}
if (Array.isArray(levelInput)) {
return levelInput.reduce((mask, l) => mask | (Logger.LOG_LEVEL_CODE[l] || 0), 0);
}
return 0;
}
/**
* 设置日志级别(覆盖模式)
* @param {string} target 目标范围,可以是 'Global'、'PluginName' 或 'PluginName::ModuleName'
* @param {number|string|string[]} level 输入的级别配置
*/
setLevel(target, level) {
const mask = this._parseLevelInput(level);
if (target === 'Global') {
this.globalLevel = mask;
this.safeConsole.log(`[Logger] Global log level mask set to: ${mask.toString(2)}`);
} else {
this.levelConfig[target] = mask;
this.safeConsole.log(`[Logger] Log level mask for '${target}' set to: ${mask.toString(2)}`);
}
}
/**
* 添加日志级别(增量模式)
* @param {string} target
* @param {number|string|string[]} level
*/
addLevel(target, level) {
const maskToAdd = this._parseLevelInput(level);
let currentMask;
if (target === 'Global') {
currentMask = this.globalLevel;
this.globalLevel = currentMask | maskToAdd;
this.safeConsole.log(`[Logger] Added level to Global. New mask: ${this.globalLevel.toString(2)}`);
} else {
currentMask = this.levelConfig[target] !== undefined ? this.levelConfig[target] : this.globalLevel;
this.levelConfig[target] = currentMask | maskToAdd;
this.safeConsole.log(`[Logger] Added level to '${target}'. New mask: ${this.levelConfig[target].toString(2)}`);
}
}
/**
* 移除日志级别(减量模式)
* @param {string} target
* @param {number|string|string[]} level
*/
removeLevel(target, level) {
const maskToRemove = this._parseLevelInput(level);
let currentMask;
if (target === 'Global') {
currentMask = this.globalLevel;
// 使用 & ~mask 实现移除
this.globalLevel = currentMask & ~maskToRemove;
this.safeConsole.log(`[Logger] Removed level from Global. New mask: ${this.globalLevel.toString(2)}`);
} else {
currentMask = this.levelConfig[target] !== undefined ? this.levelConfig[target] : this.globalLevel;
this.levelConfig[target] = currentMask & ~maskToRemove;
this.safeConsole.log(`[Logger] Removed level from '${target}'. New mask: ${this.levelConfig[target].toString(2)}`);
}
}
/**
* 获取指定上下文的生效日志级别掩码(级联查找)
* @param {string} plugin
* @param {string} origin (Module)
*/
_getEffectiveLevelMask(plugin, origin) {
// 1. 检查精确匹配 "Plugin::Module"
const specificKey = `${plugin}::${origin}`;
if (this.levelConfig.hasOwnProperty(specificKey)) {
return this.levelConfig[specificKey];
}
// 2. 检查插件级匹配 "Plugin"
if (this.levelConfig.hasOwnProperty(plugin)) {
return this.levelConfig[plugin];
}
// 3. 返回全局默认
return this.globalLevel;
}
/**
* 标准日志处理方法 (Core Processing)
* 统一处理过滤、格式化和输出,支持默认归属 Global
*/
process(plugin, origin, type, message, inFile = false) {
// [DEBUG] 强制输出以确认方法被调用 (使用 error 级别防止被过滤)
// 【核心修改】:使用 safeConsole 替代全局 console
// this.safeConsole.error('[Logger DEBUG] Process called:', { plugin, origin, type, message });
// 1. 默认归属处理
const safePlugin = plugin || 'Global';
const safeOrigin = origin || 'System';
// 2. 获取当前上下文生效的日志级别掩码
const effectiveMask = this._getEffectiveLevelMask(safePlugin, safeOrigin);
// 3. 获取当前日志类型的位码
const typeCode = Logger.LOG_LEVEL_CODE[type];
// 4. 级别筛选位与运算结果为0则表示该级别未开启
if (typeCode === undefined || (effectiveMask & typeCode) === 0) {
return;
}
const timestamp = new Date().toLocaleTimeString();
// 格式: [12:00:00] [PluginName::ClassName] [INFO: message
const fullMessage = `[${timestamp}] [${safePlugin}::${safeOrigin}] [${type.toUpperCase()}]: ${message}`;
// 5. Console Output
// 【核心修改】:使用 safeConsole 替代全局 console
switch (type) {
case 'debug':
this.safeConsole.debug(fullMessage);
break;
case 'info':
this.safeConsole.info(fullMessage);
break;
case 'warn':
this.safeConsole.warn(fullMessage);
break;
case 'error':
this.safeConsole.error(fullMessage);
break;
default:
this.safeConsole.log(fullMessage);
break;
}
// 6. File Output (via FilePipe)
if (inFile) {
// Logger 自身也需要作为系统组件注册,获取写入权限
if (!this.sysBus) {
if (window.Amily2Bus && window.Amily2Bus.register) {
this.sysBus = window.Amily2Bus.register('SystemLogger');
}
}
if (this.sysBus && this.sysBus.file) {
// 使用注册后的安全接口写入,无需再手动传 'SystemLogger'
this.sysBus.file.write('runtime.log', fullMessage + '\n');
} else {
// Fallback: 如果总线未就绪,仅在控制台警告一次,避免死循环
if (!this._warned) {
this.safeConsole.warn('[Logger] FilePipe system not linked. Log not saved to file.');
this._warned = true;
}
}
}
}
log(plugin, origin, type, message, inFile = false) {
this.process(plugin, origin, type, message, inFile);
}
}
export default Logger;

14
TODO.md
View File

@@ -13,6 +13,10 @@
以下为待开发内容
- **项目框架重构 (Project Refactoring)**:
- 现状:大量功能模块(如 `NccsApi.js`)存在手动组装参数、逻辑耦合度高、代码风格不统一("能跑就行"遗留债)等问题。
- 目标:系统性重构项目架构,统一使用 Builder 模式(如 `Options.builder`),解耦业务逻辑与配置管理,提升代码可维护性和优雅度。
## 未修复
以下为示例(预计三个版本后移除)
@@ -31,3 +35,13 @@
- 添加了**TODO.md**,现在可以记录任务清单并更清楚的记录开发完成状态了。
- 无实际功能更新
### 中间版本未维护该文件
### 1.8.3
以下为修复内容
以下为更新内容:
- 添加记忆管理并发调用

View File

@@ -190,6 +190,11 @@
<input type="range" id="amily2_ngms_temperature" min="0" max="2" step="0.1" value="0.7" />
</div>
<div class="control-group" style="display: flex; align-items: center; gap: 10px;">
<label for="amily2_ngms_fakestream_enabled" style="margin-bottom: 0;">启用流式支持 (防超时)</label>
<input type="checkbox" id="amily2_ngms_fakestream_enabled" style="width: auto;" />
</div>
<!-- 测试按钮组 - 水平排列 -->
<div class="ngms-button-row" style="display: flex; gap: 10px; justify-content: center; margin-top: 15px;">
<button id="amily2_ngms_test_connection" class="menu_button primary small_button interactable">

View File

@@ -336,6 +336,11 @@
<input type="range" id="nccs-temperature" min="0" max="2" step="0.1" value="0.7">
</div>
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
<label for="nccs-api-fakestream-enabled">启用流式支持: </label>
<input type="checkbox" id="nccs-api-fakestream-enabled" data-setting-key="nccsFakeStreamEnabled" data-type="boolean">
</div>
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
<label for="nccs-sillytavern-preset">SillyTavern 预设</label>
<select id="nccs-sillytavern-preset" class="text_pole">

View File

@@ -291,6 +291,15 @@
<button class="menu_button primary interactable" id="amily2_test"><i class="fas fa-search"></i> 测试检查</button>
<button class="menu_button accent interactable" id="amily2_fix_now"><i class="fas fa-magic"></i> 立即修复</button>
</div>
<div class="amily2_settings_block" style="display: flex; flex-direction: row; gap: 10px; align-items: center; margin-top: 10px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 15px;">
<div style="position: relative; flex-shrink: 0;">
<input type="number" id="amily2_jump_to_message_id" class="text_pole" placeholder="楼层" style="width: 100px !important; padding-left: 30px;">
<i class="fas fa-hashtag" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: rgba(255,255,255,0.5);"></i>
</div>
<button id="amily2_jump_to_message_btn" class="menu_button interactable" style="flex-grow: 1; white-space: nowrap; display: flex; align-items: center; justify-content: center; gap: 8px;">
<i class="fas fa-share"></i> <span>跳转到楼层</span>
</button>
</div>
</fieldset>
</div>

View File

@@ -729,3 +729,25 @@ hr.header-divider {
#amily2_test_api_connection {
margin-left: 10px;
}
/* === 消息高亮样式 === */
.highlight_message {
animation: highlight-pulse 2s ease-out;
border: 2px solid #ff9800 !important;
box-shadow: 0 0 15px rgba(255, 152, 0, 0.5);
}
@keyframes highlight-pulse {
0% {
background-color: rgba(255, 152, 0, 0.3);
transform: scale(1.02);
}
50% {
background-color: rgba(255, 152, 0, 0.1);
transform: scale(1);
}
100% {
background-color: transparent;
transform: scale(1);
}
}

View File

@@ -12,89 +12,80 @@ try {
console.warn("[Amily2号-Nccs外交部] 未能召唤“皇家信使”部分高级功能如Claw代理将受限。请考虑更新SillyTavern版本。", e);
}
function normalizeApiResponse(responseData) {
let data = responseData;
if (typeof data === 'string') {
let nccsCtx = null;
// 尝试连接总线
if (window.Amily2Bus) {
try {
data = JSON.parse(data);
// 注册 'NccsApi' 身份,获取专属上下文
nccsCtx = window.Amily2Bus.register('NccsApi');
// 【联动】暴露 Nccs 的核心调用能力,允许其他插件通过 query('NccsApi') 借用此通道
nccsCtx.expose({
call: callNccsAI,
getSettings: getNccsApiSettings
});
nccsCtx.log('Init', 'info', 'NccsApi 已连接至 Amily2Bus网络通道准备就绪。');
} catch (e) {
console.error(`[${extensionName}] Nccs API响应JSON解析失败:`, e);
return { error: { message: 'Invalid JSON response' } };
// 如果是热重载导致重复注册尝试降级获取注意严格锁模式下无法获取旧Context这里仅做日志提示
// 在生产环境中,页面刷新会重置 Bus不会有问题。
console.warn('[Amily2-Nccs] Bus 注册警告 (可能是热重载):', e);
}
}
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
if (Object.hasOwn(data.data, 'data')) {
data = data.data;
}
}
if (data && data.choices && data.choices[0]) {
return { content: data.choices[0].message?.content?.trim() };
}
if (data && data.content) {
return { content: data.content.trim() };
}
if (data && data.data) {
return { data: data.data };
}
if (data && data.error) {
return { error: data.error };
}
return data;
} else {
console.error('[Amily2-Nccs] 严重警告: Amily2Bus 未找到NccsApi 网络层将无法工作!');
toastr.error("核心组件 Amily2Bus 丢失,请检查安装。", "Nccs-System");
}
export function getNccsApiSettings() {
return {
nccsEnabled: extension_settings[extensionName]?.nccsEnabled || false,
apiMode: extension_settings[extensionName]?.nccsApiMode || 'openai_test',
apiUrl: extension_settings[extensionName]?.nccsApiUrl?.trim() || '',
apiKey: extension_settings[extensionName]?.nccsApiKey?.trim() || '',
model: extension_settings[extensionName]?.nccsModel || '',
maxTokens: extension_settings[extensionName]?.nccsMaxTokens || 4000,
temperature: extension_settings[extensionName]?.nccsTemperature || 0.7,
tavernProfile: extension_settings[extensionName]?.nccsTavernProfile || ''
tavernProfile: extension_settings[extensionName]?.nccsTavernProfile || '',
useFakeStream: extension_settings[extensionName]?.nccsFakeStreamEnabled || false
};
}
// =================================================================================================
// 核心调用入口 (Legacy First Mode)
// =================================================================================================
export async function callNccsAI(messages, options = {}) {
if (window.AMILY2_SYSTEM_PARALYZED === true) {
console.error("[Amily2-Nccs制裁] 系统完整性已受损,所有外交活动被无限期中止。");
return null;
}
const apiSettings = getNccsApiSettings();
const settings = getNccsApiSettings();
const finalOptions = {
maxTokens: apiSettings.maxTokens,
temperature: apiSettings.temperature,
model: apiSettings.model,
apiUrl: apiSettings.apiUrl,
apiKey: apiSettings.apiKey,
apiMode: apiSettings.apiMode,
tavernProfile: apiSettings.tavernProfile,
...settings,
...options
};
// 确保 stream 标志位存在
finalOptions.stream = finalOptions.useFakeStream ?? false;
if (finalOptions.apiMode !== 'sillytavern_preset') {
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
console.warn("[Amily2-Nccs外交部] API配置不完整无法调用AI");
toastr.error("API配置不完整请检查URL、Key和模型配置。", "Nccs-外交部");
return null;
}
} else {
// [限制] 预设模式暂不支持流式
if (finalOptions.stream) {
console.warn("[Amily2-Nccs] 预设模式目前尚不支持流式处理方案,已自动切换为标准模式。");
toastr.warning("SillyTavern预设模式目前暂不支持流式处理假流式已为您切换为标准请求模式。该功能将在后续版本中支持。", "Nccs-外交部");
finalOptions.stream = false;
}
}
console.groupCollapsed(`[Amily2号-Nccs统一API调用] ${new Date().toLocaleTimeString()}`);
console.log("【请求参数】:", {
mode: finalOptions.apiMode,
model: finalOptions.model,
maxTokens: finalOptions.maxTokens,
temperature: finalOptions.temperature,
messagesCount: messages.length
});
console.log("【消息内容】:", messages);
console.groupEnd();
try {
let responseContent;
switch (finalOptions.apiMode) {
case 'openai_test':
responseContent = await callNccsOpenAITest(messages, finalOptions);
@@ -103,53 +94,86 @@ export async function callNccsAI(messages, options = {}) {
responseContent = await callNccsSillyTavernPreset(messages, finalOptions);
break;
default:
console.error(`[Amily2-Nccs外交部] 未支持的API模式: ${finalOptions.apiMode}`);
console.error(`未支持的 API 模式: ${finalOptions.apiMode}`);
return null;
}
if (!responseContent) {
console.warn('[Amily2-Nccs外交部] 未能获取AI响应内容');
return null;
}
console.groupCollapsed("[Amily2号-Nccs AI回复]");
console.log(responseContent);
console.groupEnd();
return responseContent;
} catch (error) {
console.error(`[Amily2-Nccs外交部] API调用发生错误:`, error);
if (error.message.includes('400')) {
toastr.error(`API请求格式错误 (400): 请检查消息格式和模型配置`, "Nccs API调用失败");
} else if (error.message.includes('401')) {
toastr.error(`API认证失败 (401): 请检查API Key配置`, "Nccs API调用失败");
} else if (error.message.includes('403')) {
toastr.error(`API访问被拒绝 (403): 请检查权限设置`, "Nccs API调用失败");
} else if (error.message.includes('429')) {
toastr.error(`API调用频率超限 (429): 请稍后重试`, "Nccs API调用失败");
} else if (error.message.includes('500')) {
toastr.error(`API服务器错误 (500): 请稍后重试`, "Nccs API调用失败");
} else {
toastr.error(`API调用失败: ${error.message}`, "Nccs API调用失败");
}
console.error(`[Amily2-Nccs] API 调用失败:`, error);
toastr.error(`调用失败: ${error.message}`, "Nccs API Error");
return null;
}
}
async function fetchFakeStream(url, opts) {
const res = await fetch(url, opts);
if (!res.ok) throw new Error(`Stream HTTP ${res.status}: ${await res.text()}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullContent = "";
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed === 'data: [DONE]') continue;
if (trimmed.startsWith('data: ')) {
try {
const json = JSON.parse(trimmed.substring(6));
const delta = json.choices?.[0]?.delta?.content;
if (delta) fullContent += delta;
} catch (e) {
console.warn('[NccsApi] SSE Parse Error:', e);
}
}
}
}
} finally {
reader.releaseLock();
}
if (!fullContent && buffer) {
try {
const data = JSON.parse(buffer);
return data.choices?.[0]?.message?.content || data.content || buffer;
} catch { return buffer; }
}
return fullContent;
}
// =================================================================================================
// Legacy Implementations
// =================================================================================================
function normalizeApiResponse(responseData) {
let data = responseData;
if (typeof data === 'string') {
try { data = JSON.parse(data); } catch (e) { return data; }
}
if (data?.choices?.[0]?.message?.content) return data.choices[0].message.content.trim();
if (data?.content) return data.content.trim();
return typeof data === 'object' ? JSON.stringify(data) : data;
}
async function callNccsOpenAITest(messages, options) {
const isGoogleApi = options.apiUrl.includes('googleapis.com');
const body = {
chat_completion_source: 'openai',
messages: messages,
model: options.model,
reverse_proxy: options.apiUrl,
proxy_password: options.apiKey,
stream: false,
max_tokens: options.maxTokens || 30000,
stream: !!options.stream,
max_tokens: options.maxTokens || 4000,
temperature: options.temperature || 1,
top_p: options.top_p || 1,
};
@@ -157,104 +181,60 @@ async function callNccsOpenAITest(messages, options) {
if (!isGoogleApi) {
Object.assign(body, {
custom_prompt_post_processing: 'strict',
enable_web_search: false,
frequency_penalty: 0,
group_names: [],
include_reasoning: false,
presence_penalty: 0.12,
reasoning_effort: 'medium',
request_images: false,
});
}
const response = await fetch('/api/backends/chat-completions/generate', {
const fetchOpts = {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
};
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Nccs全兼容API请求失败: ${response.status} - ${errorText}`);
if (options.stream) {
return await fetchFakeStream('/api/backends/chat-completions/generate', fetchOpts);
}
const responseData = await response.json();
return responseData?.choices?.[0]?.message?.content;
const response = await fetch('/api/backends/chat-completions/generate', fetchOpts);
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
return normalizeApiResponse(await response.json());
}
async function callNccsSillyTavernPreset(messages, options) {
console.log('[Amily2号-NccsST预设] 使用SillyTavern预设调用');
const context = getContext();
if (!context) {
throw new Error('无法获取SillyTavern上下文');
}
if (!context) throw new Error('SillyTavern context unavailable');
const profileId = options.tavernProfile;
if (!profileId) {
throw new Error('未配置SillyTavern预设ID');
}
if (!profileId) throw new Error('No profile ID configured');
let originalProfile = '';
let responsePromise;
const originalProfile = await amilyHelper.triggerSlash('/profile');
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
if (!targetProfile) throw new Error(`Profile ${profileId} not found`);
try {
originalProfile = await amilyHelper.triggerSlash('/profile');
console.log(`[Amily2号-NccsST预设] 当前配置文件: ${originalProfile}`);
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
if (!targetProfile) {
throw new Error(`未找到配置文件ID: ${profileId}`);
if (originalProfile !== targetProfile.name) {
await amilyHelper.triggerSlash(`/profile await=true "${targetProfile.name.replace(/"/g, '\\"')}"`);
}
const targetProfileName = targetProfile.name;
console.log(`[Amily2号-NccsST预设] 目标配置文件: ${targetProfileName}`);
if (!context.ConnectionManagerRequestService) throw new Error('ConnectionManagerRequestService unavailable');
const currentProfile = await amilyHelper.triggerSlash('/profile');
if (currentProfile !== targetProfileName) {
console.log(`[Amily2号-NccsST预设] 切换配置文件: ${currentProfile} -> ${targetProfileName}`);
const escapedProfileName = targetProfileName.replace(/"/g, '\\"');
await amilyHelper.triggerSlash(`/profile await=true "${escapedProfileName}"`);
}
if (!context.ConnectionManagerRequestService) {
throw new Error('ConnectionManagerRequestService不可用');
}
console.log(`[Amily2号-NccsST预设] 通过配置文件 ${targetProfileName} 发送请求`);
responsePromise = context.ConnectionManagerRequestService.sendRequest(
const result = await context.ConnectionManagerRequestService.sendRequest(
targetProfile.id,
messages,
options.maxTokens || 4000
);
return normalizeApiResponse(result);
} finally {
try {
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
if (originalProfile && originalProfile !== currentProfileAfterCall) {
console.log(`[Amily2号-NccsST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
}
} catch (restoreError) {
console.error('[Amily2号-NccsST预设] 恢复配置文件失败:', restoreError);
// Restore profile
const current = await amilyHelper.triggerSlash('/profile');
if (originalProfile && originalProfile !== current) {
await amilyHelper.triggerSlash(`/profile await=true "${originalProfile.replace(/"/g, '\\"')}"`);
}
}
const result = await responsePromise;
if (!result) {
throw new Error('未收到API响应');
}
const normalizedResult = normalizeApiResponse(result);
if (normalizedResult.error) {
throw new Error(normalizedResult.error.message || 'SillyTavern预设API调用失败');
}
return normalizedResult.content;
}
export async function fetchNccsModels() {
console.log('[Amily2号-Nccs外交部] 开始获取模型列表');
@@ -284,7 +264,6 @@ export async function fetchNccsModels() {
console.log('[Amily2号-Nccs外交部] SillyTavern预设模式获取到模型:', models);
return models;
} else {
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
throw new Error('API URL或Key未配置');
@@ -383,3 +362,4 @@ export async function testNccsApiConnection() {
return false;
}
}

View File

@@ -50,7 +50,8 @@ export function getNgmsApiSettings() {
model: extension_settings[extensionName]?.ngmsModel || '',
maxTokens: extension_settings[extensionName]?.ngmsMaxTokens || 4000,
temperature: extension_settings[extensionName]?.ngmsTemperature || 0.7,
tavernProfile: extension_settings[extensionName]?.ngmsTavernProfile || ''
tavernProfile: extension_settings[extensionName]?.ngmsTavernProfile || '',
useFakeStream: extension_settings[extensionName]?.ngmsFakeStreamEnabled || false
};
}
@@ -73,12 +74,22 @@ export async function callNgmsAI(messages, options = {}) {
...options
};
// 确保 stream 标志位存在
finalOptions.stream = finalOptions.useFakeStream ?? apiSettings.useFakeStream ?? false;
if (finalOptions.apiMode !== 'sillytavern_preset') {
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
console.warn("[Amily2-Ngms外交部] API配置不完整无法调用AI");
toastr.error("API配置不完整请检查URL、Key和模型配置。", "Ngms-外交部");
return null;
}
} else {
// [限制] 预设模式暂不支持流式
if (finalOptions.stream) {
console.warn("[Amily2-Ngms] 预设模式目前尚不支持流式处理方案,已自动切换为标准模式。");
toastr.warning("SillyTavern预设模式目前暂不支持流式处理假流式已为您切换为标准请求模式。该功能将在后续版本中支持。", "Ngms-外交部");
finalOptions.stream = false;
}
}
console.groupCollapsed(`[Amily2号-Ngms统一API调用] ${new Date().toLocaleTimeString()}`);
@@ -87,6 +98,7 @@ export async function callNgmsAI(messages, options = {}) {
model: finalOptions.model,
maxTokens: finalOptions.maxTokens,
temperature: finalOptions.temperature,
stream: finalOptions.stream,
messagesCount: messages.length
});
console.log("【消息内容】:", messages);
@@ -139,6 +151,54 @@ export async function callNgmsAI(messages, options = {}) {
}
}
async function fetchFakeStream(url, opts) {
const res = await fetch(url, opts);
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Stream HTTP ${res.status}: ${errorText}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullContent = "";
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed === 'data: [DONE]') continue;
if (trimmed.startsWith('data: ')) {
try {
const json = JSON.parse(trimmed.substring(6));
const delta = json.choices?.[0]?.delta?.content;
if (delta) fullContent += delta;
} catch (e) {
console.warn('[NgmsApi] SSE Parse Error:', e);
}
}
}
}
} finally {
reader.releaseLock();
}
if (!fullContent && buffer) {
try {
const data = JSON.parse(buffer);
return data.choices?.[0]?.message?.content || data.content || buffer;
} catch { return buffer; }
}
return fullContent;
}
async function callNgmsOpenAITest(messages, options) {
const isGoogleApi = options.apiUrl.includes('googleapis.com');
@@ -148,7 +208,7 @@ async function callNgmsOpenAITest(messages, options) {
model: options.model,
reverse_proxy: options.apiUrl,
proxy_password: options.apiKey,
stream: false,
stream: !!options.stream,
max_tokens: options.maxTokens || 30000,
temperature: options.temperature || 1,
top_p: options.top_p || 1,
@@ -167,11 +227,17 @@ async function callNgmsOpenAITest(messages, options) {
});
}
const response = await fetch('/api/backends/chat-completions/generate', {
const fetchOpts = {
method: 'POST',
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
};
if (options.stream) {
return await fetchFakeStream('/api/backends/chat-completions/generate', fetchOpts);
}
const response = await fetch('/api/backends/chat-completions/generate', fetchOpts);
if (!response.ok) {
const errorText = await response.text();

View File

@@ -155,4 +155,8 @@ export const tableSystemDefaultSettings = {
table_independent_rules_enabled: false,
table_tags_to_extract: '',
table_exclusion_rules: [],
// Nccs API 设置
nccsEnabled: false,
nccsFakeStreamEnabled: false,
};

41
imports.js Normal file
View File

@@ -0,0 +1,41 @@
// Side-effect imports (独立模块/自初始化模块)
import "./PresetSettings/index.js";
import "./PreOptimizationViewer/index.js";
import "./WorldEditor/WorldEditor.js";
import './core/amily2-updater.js';
import './SL/bus/Amily2Bus.js'
// Re-exports (重新导出供 index.js 使用)
export { createDrawer } from "./ui/drawer.js";
export { showPlotOptimizationProgress, updatePlotOptimizationProgress, hidePlotOptimizationProgress } from './ui/optimization-progress.js';
export { registerSlashCommands } from "./core/commands.js";
export { onMessageReceived, handleTableUpdate } from "./core/events.js";
export { processPlotOptimization } from "./core/summarizer.js";
// External SillyTavern scripts (外部脚本)
export { getContext, extension_settings } from "/scripts/extensions.js";
export { characters, this_chid, eventSource, event_types, saveSettingsDebounced } from '/script.js';
// Core Systems
export { injectTableData, generateTableContent } from "./core/table-system/injector.js";
export { initialize as initializeRagProcessor } from "./core/rag-processor.js";
export { loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables } from './core/table-system/manager.js';
export { fillWithSecondaryApi } from './core/table-system/secondary-filler.js';
export { renderTables } from './ui/table-bindings.js';
export { log } from './core/table-system/logger.js';
export { checkForUpdates, fetchMessageBoardContent } from './core/api.js';
export { setUpdateInfo, applyUpdateIndicator } from './ui/state.js';
export { pluginVersion, extensionName, defaultSettings } from './utils/settings.js';
export { checkAuthorization, refreshUserInfo } from './utils/auth.js';
export { tableSystemDefaultSettings } from './core/table-system/settings.js';
export { manageLorebookEntriesForChat } from './core/lore.js';
// Feature Modules
export { initializeCharacterWorldBook } from './CharacterWorldBook/cwb_index.js';
export { cwbDefaultSettings } from './CharacterWorldBook/src/cwb_config.js';
export { bindGlossaryEvents } from './glossary/GT_bindings.js';
export { updateOrInsertTableInChat, startContinuousRendering, stopContinuousRendering } from './ui/message-table-renderer.js';
export { initializeRenderer } from './core/tavern-helper/renderer.js';
export { initializeApiListener, registerApiHandler, amilyHelper, initializeAmilyHelper } from './core/tavern-helper/main.js';
export { registerContextOptimizerMacros, resetContextBuffer } from './core/context-optimizer.js';
export { initializeSuperMemory } from './core/super-memory/manager.js';

402
index.js
View File

@@ -1,36 +1,33 @@
import { createDrawer } from "./ui/drawer.js";
import "./PresetSettings/index.js"; // 【预设设置】独立模块
import "./PreOptimizationViewer/index.js"; // 【优化前文查看器】独立模块
import "./WorldEditor/WorldEditor.js"; // 【世界编辑器】独立模块
import { showPlotOptimizationProgress, updatePlotOptimizationProgress, hidePlotOptimizationProgress } from './ui/optimization-progress.js';
import { registerSlashCommands } from "./core/commands.js";
import { onMessageReceived, handleTableUpdate } from "./core/events.js";
import { processPlotOptimization } from "./core/summarizer.js";
import { getContext } from "/scripts/extensions.js";
import { characters, this_chid } from '/script.js';
import { injectTableData, generateTableContent } from "./core/table-system/injector.js";
import { initialize as initializeRagProcessor } from "./core/rag-processor.js";
import { loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables } from './core/table-system/manager.js';
import { fillWithSecondaryApi } from './core/table-system/secondary-filler.js';
import { renderTables } from './ui/table-bindings.js';
import { log } from './core/table-system/logger.js';
import { eventSource, event_types, saveSettingsDebounced } from '/script.js';
import { checkForUpdates, fetchMessageBoardContent } from './core/api.js';
import { setUpdateInfo, applyUpdateIndicator } from './ui/state.js';
import { pluginVersion, extensionName, defaultSettings } from './utils/settings.js';
import { checkAuthorization, refreshUserInfo } from './utils/auth.js';
import { tableSystemDefaultSettings } from './core/table-system/settings.js';
import { extension_settings } from '/scripts/extensions.js';
import { manageLorebookEntriesForChat } from './core/lore.js';
import { initializeCharacterWorldBook } from './CharacterWorldBook/cwb_index.js';
import { cwbDefaultSettings } from './CharacterWorldBook/src/cwb_config.js';
import { bindGlossaryEvents } from './glossary/GT_bindings.js';
import './core/amily2-updater.js';
import { updateOrInsertTableInChat, startContinuousRendering, stopContinuousRendering } from './ui/message-table-renderer.js';
import { initializeRenderer } from './core/tavern-helper/renderer.js';
import { initializeApiListener, registerApiHandler, amilyHelper, initializeAmilyHelper } from './core/tavern-helper/main.js';
import { registerContextOptimizerMacros, resetContextBuffer } from './core/context-optimizer.js';
import { initializeSuperMemory } from './core/super-memory/manager.js';
import {
createDrawer,
showPlotOptimizationProgress, updatePlotOptimizationProgress, hidePlotOptimizationProgress,
registerSlashCommands,
onMessageReceived, handleTableUpdate,
processPlotOptimization,
getContext, extension_settings,
characters, this_chid, eventSource, event_types, saveSettingsDebounced,
injectTableData, generateTableContent,
initializeRagProcessor,
loadTables, clearHighlights, rollbackAndRefill, rollbackState, commitPendingDeletions, saveStateToMessage, getMemoryState, clearUpdatedTables,
fillWithSecondaryApi,
renderTables,
log,
checkForUpdates, fetchMessageBoardContent,
setUpdateInfo, applyUpdateIndicator,
pluginVersion, extensionName, defaultSettings,
checkAuthorization, refreshUserInfo,
tableSystemDefaultSettings,
manageLorebookEntriesForChat,
initializeCharacterWorldBook,
cwbDefaultSettings,
bindGlossaryEvents,
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";
@@ -312,12 +309,13 @@ function loadPluginStyles() {
// 颁布三道制衣圣谕
loadStyleFile("style.css"); // 【第一道圣谕】为帝国主体宫殿披上通用华服
loadStyleFile("historiography.css"); // 【第二道圣谕】为敕史局披上其专属华服
loadStyleFile("hanlinyuan.css"); // 【第三道圣谕】为翰林院披上其专属华服
loadStyleFile("amily2-glossary.css"); // 【新圣谕】为术语表披上其专属华服
loadStyleFile("table.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("iframe-renderer.css"); // 【新圣谕】为iframe渲染内容披上其专属华服
loadStyleFile("renderer.css"); // 【新圣谕】为iframe渲染内容披上其专属华服
loadStyleFile("super-memory.css"); // 【新圣谕】为超级记忆披上其专属华服
// 【第六道圣谕】为角色世界书披上其专属华服
@@ -409,99 +407,77 @@ window.addEventListener("error", (event) => {
});
jQuery(async () => {
console.log("[Amily2号-帝国枢密院] 开始执行开国大典...");
let isProcessingPlotOptimization = false;
// 启动外部库加载 (DOMPurify)
/**
* 加载必要的外部库(如 DOMPurify
* 如果加载失败,会回退到内置的简单净化器。
*/
function loadExternalLibraries() {
loadExternalScript(DOMPURIFY_CDN, 'DOMPurify').catch(e => console.warn("[Amily2] DOMPurify 加载失败,将使用内置净化器:", e));
}
// 【V146.2 紧急优化】优先注册上下文优化器,确保它在密折司之前拦截并处理 Prompt
/**
* 初始化上下文优化器模块。
* 优先注册宏,确保其在其他处理之前生效。
*/
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) => {
return amilyHelper.getChatMessages(data.range, data.options);
});
registerApiHandler('setChatMessages', async (data) => {
return await amilyHelper.setChatMessages(data.messages, data.options);
});
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) => {
return await amilyHelper.createChatMessages(data.messages, data.options);
});
registerApiHandler('deleteChatMessages', async (data) => {
return await amilyHelper.deleteChatMessages(data.ids, data.options);
});
registerApiHandler('getLorebooks', async (data) => {
return await amilyHelper.getLorebooks();
});
registerApiHandler('getCharLorebooks', async (data) => {
return await amilyHelper.getCharLorebooks(data.options);
});
registerApiHandler('getLorebookEntries', async (data) => {
return await amilyHelper.getLorebookEntries(data.bookName);
});
registerApiHandler('setLorebookEntries', async (data) => {
return await amilyHelper.setLorebookEntries(data.bookName, data.entries);
});
registerApiHandler('createLorebookEntries', async (data) => {
return await amilyHelper.createLorebookEntries(data.bookName, data.entries);
});
registerApiHandler('createLorebook', async (data) => {
return await amilyHelper.createLorebook(data.bookName);
});
registerApiHandler('triggerSlash', async (data) => {
return await amilyHelper.triggerSlash(data.command);
});
registerApiHandler('getLastMessageId', async (data) => {
return amilyHelper.getLastMessageId();
});
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) {
@@ -509,56 +485,39 @@ jQuery(async () => {
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}`);
});
}
initializeAmilyHelper();
console.log("[Amily2号-帝国枢密院] 开始执行开国大典...");
/**
* 合并插件的默认设置与用户设置。
* 确保即使在升级后,新增加的设置项也有默认值。
*/
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号-帝国枢密院] 帝国基本法已确认,档案室已与国库对接完毕。");
}
let attempts = 0;
const maxAttempts = 100;
const checkInterval = 100;
const targetSelector = "#sys-settings-button";
const deploymentInterval = setInterval(async () => {
if ($(targetSelector).length > 0) {
clearInterval(deploymentInterval);
console.log("[Amily2号-帝国枢密院] SillyTavern宫殿主体已确认开国大典正式开始");
try {
console.log("[Amily2号-开国大典] 步骤一:为宫殿披上华服...");
loadPluginStyles();
console.log("[Amily2号-开国大典] 步骤二:皇家仪仗队就位...");
await registerSlashCommands();
console.log("[Amily2号-开国大典] 步骤三:开始召唤府邸...");
createDrawer();
function waitForGlossaryPanelAndBindEvents() {
/**
* 等待术语表面板加载完毕并绑定事件。
* 包含重试机制,防止面板尚未渲染导致绑定失败。
*/
function waitForGlossaryPanelAndBindEvents() {
let attempts = 0;
const maxAttempts = 50;
const interval = 100;
@@ -583,10 +542,13 @@ jQuery(async () => {
}
}
}, interval);
}
waitForGlossaryPanelAndBindEvents();
}
function waitForCwbPanelAndInitialize() {
/**
* 等待角色世界书面板加载完毕并进行初始化。
* 包含重试机制。
*/
function waitForCwbPanelAndInitialize() {
let attempts = 0;
const maxAttempts = 50;
const interval = 100;
@@ -611,23 +573,19 @@ jQuery(async () => {
}
}
}, interval);
}
waitForCwbPanelAndInitialize();
}
/**
* 注册用于表格内容的 SillyTavern 宏。
* 允许在 Prompt 中使用 {{Amily2EditContent}} 来插入动态生成的表格数据。
*/
function registerTableMacros() {
console.log("[Amily2号-开国大典] 步骤3.8:注册表格占位符宏...");
try {
// 【V144.0】注册上下文优化器宏 (已移至开国大典步骤0优先执行此处仅保留重置逻辑)
// registerContextOptimizerMacros();
// 注册生成开始事件以重置缓冲区
eventSource.on(event_types.GENERATION_STARTED, () => {
resetContextBuffer();
// 故障恢复:如果生成开始了,说明之前的优化肯定结束了(或者被绕过了),强制重置标志位
if (isProcessingPlotOptimization) {
console.warn("[Amily2-剧情优化] 检测到生成开始,但优化标志位仍为 true。这可能是并发生成或状态未及时重置。");
// 我们不在这里强制重置,因为优化可能正在进行中,我们希望它完成并修改输入框。
}
});
@@ -647,32 +605,30 @@ jQuery(async () => {
} catch (error) {
console.error('[Amily2-核心引擎] 注册表格宏时发生错误:', error);
}
}
console.log("[Amily2号-开国大典] 步骤四:部署帝国哨兵网络...");
let isProcessingPlotOptimization = false;
async function onPlotGenerationAfterCommands(type, params, dryRun) {
/**
* 处理用户发送消息前的逻辑(剧情优化)。
* 拦截消息发送,进行剧情梳理和总结,然后注入到 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 });
// Skip for regenerations or dry runs
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;
}
if (globalSettings?.plotOpt_enabled === false) return false;
const isJqyhEnabled = globalSettings?.jqyhEnabled === true;
const isMainApiConfigured = !!globalSettings?.apiUrl || !!globalSettings?.tavernProfile;
@@ -682,7 +638,6 @@ jQuery(async () => {
return false;
}
// Determine the message to be processed
let userMessage = $('#send_textarea').val();
let isFromTextarea = true;
const context = getContext();
@@ -696,12 +651,8 @@ jQuery(async () => {
}
}
}
if (!userMessage) return false;
if (!userMessage) {
return false; // Nothing to process
}
// Set the flag to prevent loops and show progress
isProcessingPlotOptimization = true;
const cancellationState = { isCancelled: false };
showPlotOptimizationProgress(cancellationState);
@@ -727,13 +678,10 @@ jQuery(async () => {
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 (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;
@@ -747,19 +695,15 @@ jQuery(async () => {
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.");
@@ -780,7 +724,14 @@ jQuery(async () => {
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);
@@ -792,20 +743,16 @@ jQuery(async () => {
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);
@@ -815,7 +762,6 @@ jQuery(async () => {
} else {
log('【监察系统】未配置填表模式,跳过填表。', 'info');
}
renderTables();
log('【监察系统】滑动后填表完成UI 已刷新。', 'success');
});
@@ -823,11 +769,9 @@ jQuery(async () => {
handleTableUpdate(mes_id);
updateOrInsertTableInChat();
});
eventSource.on(event_types.CHAT_CHANGED, () => {
window.lastPreOptimizationResult = null;
document.dispatchEvent(new CustomEvent('preOptimizationTextUpdated'));
manageLorebookEntriesForChat();
setTimeout(() => {
log("【监察系统】检测到“朝代更迭”(CHAT_CHANGED),开始重修史书并刷新宫殿...", 'info');
@@ -835,7 +779,6 @@ jQuery(async () => {
clearUpdatedTables();
loadTables();
renderTables();
if (extension_settings[extensionName].render_on_every_message) {
startContinuousRendering();
} else {
@@ -843,40 +786,31 @@ jQuery(async () => {
}
}, 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;
}
}
console.log("[Amily2号-开国大典] 步骤五初始化RAG处理器...");
try {
initializeRagProcessor();
console.log('[Amily2-翰林院] RAG处理器已成功初始化');
} catch (error) {
console.error('[Amily2-翰林院] RAG处理器初始化失败:', error);
}
console.log("[Amily2号-开国大典] 步骤六:智能冲突检测与注入策略...");
async function executeAmily2Injection(...args) {
/**
* 执行 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);
}
if (window.hanlinyuanRagProcessor && typeof window.hanlinyuanRagProcessor.rearrangeChat === 'function') {
try {
console.log('[Amily2-核心引擎] 执行内置RAG注入。');
@@ -885,31 +819,45 @@ jQuery(async () => {
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);
}
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);
});
}).catch(e => console.warn("[Amily2] 后台刷新用户信息失败:", e));
}
console.log("[Amily2号-开国大典] 步骤七:初始化版本显示系统...");
@@ -924,13 +872,10 @@ jQuery(async () => {
handleUpdateCheck();
handleMessageBoard();
initializeOnlineTracker(); // 【Amily2号-在线统计】启动在线人数统计
initializeLocalLinkage(); // 【Amily2号-本地联动】启动本地联动服务
initializeOnlineTracker();
initializeLocalLinkage();
// 【V146.4】自动初始化超级记忆系统
setTimeout(() => {
initializeSuperMemory();
}, 3000); // 延迟3秒以确保 ST 环境完全就绪
setTimeout(() => initializeSuperMemory(), 3000);
initializeRenderer();
@@ -941,25 +886,70 @@ jQuery(async () => {
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号-开国大典] 步骤三:开始召唤府邸...");
createDrawer();
waitForGlossaryPanelAndBindEvents();
waitForCwbPanelAndInitialize();
registerTableMacros();
registerEventListeners();
initializeRagAndInjection();
performPostDeploymentTasks();
} catch (error) {
console.error("!!!【开国大典失败】在执行系列法令时发生严重错误:", error);
}
}
jQuery(async () => {
console.log("[Amily2号-帝国枢密院] 开始执行开国大典...");
initializeAmilyBus();
loadExternalLibraries();
initializeContextOptimizer();
await initializeMiZheSi();
registerAllApiHandlers();
initializeAmilyHelper();
mergePluginSettings();
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) {

View File

@@ -1,7 +1,7 @@
{
"name": "Amily2号聊天优化助手",
"display_name": "Amily2号助手",
"version": "1.8.3",
"version": "1.8.4",
"author": "Wx-2025",
"description": "一个拥有独立UI的智能引擎正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
"minSillyTavernVersion": "1.10.0",
@@ -12,47 +12,3 @@
"styles": ["style.css"]
}

View File

@@ -579,6 +579,84 @@ export function bindModalEvents() {
},
);
container
.off("click.amily2.jump")
.on("click.amily2.jump", "#amily2_jump_to_message_btn", function() {
const targetId = parseInt($("#amily2_jump_to_message_id").val());
if (isNaN(targetId)) {
toastr.warning("请输入有效的楼层号");
return;
}
// 1. 尝试查找 DOM 元素
const targetElement = document.querySelector(`.mes[mesid="${targetId}"]`);
if (targetElement) {
// 【V60.1】增强跳转:自动展开被隐藏的楼层及其上下文
const allMessages = Array.from(document.querySelectorAll('.mes'));
const targetIndex = allMessages.indexOf(targetElement);
if (targetIndex !== -1) {
// 展开前后各10条确保上下文连贯
const contextRange = 10;
const start = Math.max(0, targetIndex - contextRange);
const end = Math.min(allMessages.length - 1, targetIndex + contextRange);
let unhiddenCount = 0;
for (let i = start; i <= end; i++) {
const msg = allMessages[i];
if (msg.style.display === 'none') {
msg.style.removeProperty('display');
unhiddenCount++;
}
}
if (unhiddenCount > 0) {
toastr.info(`已临时展开 ${unhiddenCount} 条被隐藏的消息以显示上下文。`);
}
}
targetElement.scrollIntoView({ behavior: "smooth", block: "center" });
targetElement.classList.add('highlight_message');
setTimeout(() => targetElement.classList.remove('highlight_message'), 2000);
toastr.success(`已跳转到楼层 ${targetId}`);
} else {
// 2. DOM 中未找到,尝试从内存中获取并弹窗显示
const context = getContext();
if (context && context.chat && context.chat[targetId]) {
const msg = context.chat[targetId];
const sender = msg.name;
let formattedContent = msg.mes;
// 尝试使用 SillyTavern 的格式化函数
if (typeof messageFormatting === 'function') {
formattedContent = messageFormatting(msg.mes, sender, false, false);
} else {
formattedContent = msg.mes.replace(/\n/g, '<br>');
}
const html = `
<div style="padding: 10px;">
<div style="margin-bottom: 10px; font-size: 1.1em; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 5px;">
<strong style="color: var(--smart-theme-color, #ffcc00);">${sender}</strong>
<span style="opacity: 0.6; font-size: 0.8em;">(楼层 #${targetId})</span>
</div>
<div class="mes_text" style="max-height: 60vh; overflow-y: auto;">
${formattedContent}
</div>
<div style="margin-top: 15px; font-size: 0.9em; opacity: 0.7; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 5px;">
<i class="fas fa-info-circle"></i> 该楼层未在当前页面渲染(可能已被清理以节省内存),无法直接跳转,已为您在弹窗中显示。
</div>
</div>
`;
showHtmlModal(`查看历史记录`, html);
toastr.info(`楼层 ${targetId} 未渲染,已在弹窗中显示内容。`);
} else {
toastr.error(`未找到楼层 ${targetId},聊天记录中不存在该索引。`);
}
}
});
container
.off("click.amily2.expand_editor")
.on("click.amily2.expand_editor", "#amily2_expand_editor", function (event) {

View File

@@ -75,7 +75,7 @@ async function initializePanel(contentPanel, errorContainer) {
const mainContainer = contentPanel.find('#amily2_chat_optimiser');
if (mainContainer.length) {
const additionalFeaturesContent = await $.get(`${extensionFolderPath}/assets/Amily2-AdditionalFeatures.html`);
const additionalFeaturesContent = await $.get(`${extensionFolderPath}/assets/amily-additional-features/Amily2-AdditionalFeatures.html`);
const additionalPanelHtml = `<div id="amily2_additional_features_panel" style="display: none;">${additionalFeaturesContent}</div>`;
mainContainer.append(additionalPanelHtml);
@@ -83,11 +83,11 @@ async function initializePanel(contentPanel, errorContainer) {
const textOptimizationPanelHtml = `<div id="amily2_text_optimization_panel" style="display: none;">${textOptimizationContent}</div>`;
mainContainer.append(textOptimizationPanelHtml);
const hanlinyuanContent = await $.get(`${extensionFolderPath}/assets/hanlinyuan.html`);
const hanlinyuanContent = await $.get(`${extensionFolderPath}/assets/amily-hanlinyuan-system/hanlinyuan.html`);
const hanlinyuanPanelHtml = `<div id="amily2_hanlinyuan_panel" style="display: none;">${hanlinyuanContent}</div>`;
mainContainer.append(hanlinyuanPanelHtml);
const memorisationFormsContent = await $.get(`${extensionFolderPath}/assets/Memorisation-forms.html`);
const memorisationFormsContent = await $.get(`${extensionFolderPath}/assets/amily-data-table/Memorisation-forms.html`);
const memorisationFormsPanelHtml = `<div id="amily2_memorisation_forms_panel" style="display: none;">${memorisationFormsContent}</div>`;
mainContainer.append(memorisationFormsPanelHtml);
@@ -103,7 +103,7 @@ async function initializePanel(contentPanel, errorContainer) {
const worldEditorPanelHtml = `<div id="amily2_world_editor_panel" style="display: none;">${worldEditorContent}</div>`;
mainContainer.append(worldEditorPanelHtml);
const glossaryContent = await $.get(`${extensionFolderPath}/assets/amily2-glossary.html`);
const glossaryContent = await $.get(`${extensionFolderPath}/assets/amily-glossary-system/amily2-glossary.html`);
const glossaryPanelHtml = `<div id="amily2_glossary_panel" style="display: none;">${glossaryContent}</div>`;
mainContainer.append(glossaryPanelHtml);

View File

@@ -408,6 +408,7 @@ function bindNgmsApiEvents() {
// Ngms API 开关控制
const ngmsToggle = document.getElementById('amily2_ngms_enabled');
const ngmsFakeStreamToggle = document.getElementById('amily2_ngms_fakestream_enabled');
const ngmsContent = document.getElementById('amily2_ngms_content');
if (ngmsToggle && ngmsContent) {
@@ -421,6 +422,13 @@ function bindNgmsApiEvents() {
});
}
if (ngmsFakeStreamToggle) {
ngmsFakeStreamToggle.checked = extension_settings[extensionName].ngmsFakeStreamEnabled ?? false;
ngmsFakeStreamToggle.addEventListener('change', function() {
updateAndSaveSetting('ngmsFakeStreamEnabled', this.checked);
});
}
// API模式切换
const apiModeSelect = document.getElementById('amily2_ngms_api_mode');
const compatibleConfig = document.getElementById('amily2_ngms_compatible_config');

View File

@@ -1980,6 +1980,7 @@ function bindNccsApiEvents() {
const settings = extension_settings[extensionName];
if (settings.nccsEnabled === undefined) settings.nccsEnabled = false;
if (settings.nccsFakeStreamEnabled === undefined) settings.nccsFakeStreamEnabled = false;
if (settings.nccsApiMode === undefined) settings.nccsApiMode = 'openai_test';
if (settings.nccsApiUrl === undefined) settings.nccsApiUrl = 'https://api.openai.com/v1';
if (settings.nccsApiKey === undefined) settings.nccsApiKey = '';
@@ -1989,6 +1990,7 @@ function bindNccsApiEvents() {
if (settings.nccsTavernProfile === undefined) settings.nccsTavernProfile = '';
const enabledToggle = document.getElementById('nccs-api-enabled');
const enabledFakeStreamToggle = document.getElementById('nccs-api-fakestream-enabled');
const configDiv = document.getElementById('nccs-api-config');
const modeSelect = document.getElementById('nccs-api-mode');
const urlInput = document.getElementById('nccs-api-url');
@@ -2005,6 +2007,7 @@ function bindNccsApiEvents() {
if (!enabledToggle || !configDiv) return;
enabledToggle.checked = settings.nccsEnabled;
enabledFakeStreamToggle.checked = settings.nccsFakeStreamEnabled;
if (modeSelect) modeSelect.value = settings.nccsApiMode;
if (urlInput) urlInput.value = settings.nccsApiUrl;
if (keyInput) keyInput.value = settings.nccsApiKey;
@@ -2065,6 +2068,12 @@ function bindNccsApiEvents() {
log(`Nccs API ${enabledToggle.checked ? '已启用' : '已禁用'}`, 'info');
});
enabledFakeStreamToggle.addEventListener('change', () => {
settings.nccsFakeStreamEnabled = enabledFakeStreamToggle.checked;
saveSettingsDebounced();
log(`Nccs API FakeStream ${enabledFakeStreamToggle.checked ? 'Enabled' : 'Disabled'}`, 'info');
});
if (modeSelect) {
modeSelect.addEventListener('change', () => {
settings.nccsApiMode = modeSelect.value;

File diff suppressed because one or more lines are too long