mirror of
https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 09:15:50 +00:00
Compare commits
32 Commits
f93fd2df1d
...
b77122d025
| Author | SHA1 | Date | |
|---|---|---|---|
| b77122d025 | |||
| 60befb69ea | |||
| ab161e475d | |||
| 859588461d | |||
| 3043c3d80a | |||
| 40c3d6c735 | |||
| 91e85aec94 | |||
| 70224a6b61 | |||
| 53ff9bd307 | |||
| 4c61837b93 | |||
| 1136707f1f | |||
| 3ba4e39193 | |||
| 8f2b91c36c | |||
| 56017782bb | |||
| 398649c754 | |||
| dedc4418c7 | |||
| a5f3c92fa3 | |||
| 3d16bb20bb | |||
| 33f09a615e | |||
| d405f1d624 | |||
| 4e9f2defb7 | |||
| 4895a259e6 | |||
| 6ab18f545e | |||
| 9625974d33 | |||
| 256b295739 | |||
| b9df92435a | |||
| c56780570a | |||
| f9b2f35828 | |||
| cb54fc3eb4 | |||
| 3d051d69a1 | |||
| e370ce7d3f | |||
| 46fea93115 |
180
SL/bus/Amily2Bus.js
Normal file
180
SL/bus/Amily2Bus.js
Normal 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
111
SL/bus/README.md
Normal 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
323
SL/bus/api/ModelCaller.js
Normal 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
98
SL/bus/api/Options.js
Normal 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
74
SL/bus/api/RequestBody.js
Normal 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
56
SL/bus/chain/Chain.js
Normal 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
61
SL/bus/file/FilePipe.js
Normal 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
220
SL/bus/log/Logger.js
Normal 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
14
TODO.md
@@ -13,6 +13,10 @@
|
|||||||
|
|
||||||
以下为待开发内容
|
以下为待开发内容
|
||||||
|
|
||||||
|
- **项目框架重构 (Project Refactoring)**:
|
||||||
|
- 现状:大量功能模块(如 `NccsApi.js`)存在手动组装参数、逻辑耦合度高、代码风格不统一("能跑就行"遗留债)等问题。
|
||||||
|
- 目标:系统性重构项目架构,统一使用 Builder 模式(如 `Options.builder`),解耦业务逻辑与配置管理,提升代码可维护性和优雅度。
|
||||||
|
|
||||||
## 未修复
|
## 未修复
|
||||||
|
|
||||||
以下为示例(预计三个版本后移除)
|
以下为示例(预计三个版本后移除)
|
||||||
@@ -31,3 +35,13 @@
|
|||||||
|
|
||||||
- 添加了**TODO.md**,现在可以记录任务清单并更清楚的记录开发完成状态了。
|
- 添加了**TODO.md**,现在可以记录任务清单并更清楚的记录开发完成状态了。
|
||||||
- 无实际功能更新
|
- 无实际功能更新
|
||||||
|
|
||||||
|
### 中间版本未维护该文件
|
||||||
|
|
||||||
|
### 1.8.3
|
||||||
|
|
||||||
|
以下为修复内容
|
||||||
|
|
||||||
|
以下为更新内容:
|
||||||
|
|
||||||
|
- 添加记忆管理并发调用
|
||||||
|
|||||||
@@ -1,438 +1,443 @@
|
|||||||
|
|
||||||
<div class="amily2-header">
|
<div class="amily2-header">
|
||||||
<button id="amily2_back_to_main_settings" class="menu_button secondary small_button interactable">
|
<button id="amily2_back_to_main_settings" class="menu_button secondary small_button interactable">
|
||||||
<i class="fas fa-arrow-left"></i> 返回主殿
|
<i class="fas fa-arrow-left"></i> 返回主殿
|
||||||
</button>
|
</button>
|
||||||
<div id="amily2_open_neige_tutorial" class="additional-features-title interactable" title="查看内阁使用教程" style="cursor: pointer;">
|
<div id="amily2_open_neige_tutorial" class="additional-features-title interactable" title="查看内阁使用教程" style="cursor: pointer;">
|
||||||
<i class="fas fa-landmark-dome"></i> 内阁密室
|
<i class="fas fa-landmark-dome"></i> 内阁密室
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="header-divider">
|
<hr class="header-divider">
|
||||||
|
|
||||||
|
|
||||||
<fieldset class="settings-group">
|
<fieldset class="settings-group">
|
||||||
<legend><i class="fas fa-scroll"></i> 皇家史册管理员</legend>
|
<legend><i class="fas fa-scroll"></i> 皇家史册管理员</legend>
|
||||||
|
|
||||||
<div class="control-pair-container">
|
<div class="control-pair-container">
|
||||||
<div class="amily2_settings_block">
|
<div class="amily2_settings_block">
|
||||||
<label for="amily2_auto_hide_enabled">按阈值自动隐藏</label>
|
<label for="amily2_auto_hide_enabled">按阈值自动隐藏</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input id="amily2_auto_hide_enabled" type="checkbox" />
|
<input id="amily2_auto_hide_enabled" type="checkbox" />
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_settings_block">
|
<div class="amily2_settings_block">
|
||||||
<label for="amily2_auto_hide_summarized_enabled">隐藏已总结楼层</label>
|
<label for="amily2_auto_hide_summarized_enabled">隐藏已总结楼层</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input id="amily2_auto_hide_summarized_enabled" type="checkbox" />
|
<input id="amily2_auto_hide_summarized_enabled" type="checkbox" />
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="amily2_settings_block">
|
<div class="amily2_settings_block">
|
||||||
<button id="amily2_unhide_all_button" class="menu_button secondary small_button interactable" title="一键取消所有已隐藏的楼层">
|
<button id="amily2_unhide_all_button" class="menu_button secondary small_button interactable" title="一键取消所有已隐藏的楼层">
|
||||||
<i class="fas fa-folder-open"></i>
|
<i class="fas fa-folder-open"></i>
|
||||||
<span>全部可见</span>
|
<span>全部可见</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr style="border-style: dashed; margin: 10px 0;">
|
<hr style="border-style: dashed; margin: 10px 0;">
|
||||||
|
|
||||||
|
|
||||||
<div class="amily2_settings_block">
|
<div class="amily2_settings_block">
|
||||||
<label for="amily2_auto_hide_threshold">保留最新消息层数: <span id="amily2_auto_hide_threshold_value">30</span></label>
|
<label for="amily2_auto_hide_threshold">保留最新消息层数: <span id="amily2_auto_hide_threshold_value">30</span></label>
|
||||||
<input id="amily2_auto_hide_threshold" type="range" min="5" max="100" step="1" value="30" />
|
<input id="amily2_auto_hide_threshold" type="range" min="5" max="100" step="1" value="30" />
|
||||||
<small class="notes">设定始终在你的上下文中保留的最新消息数量。</small>
|
<small class="notes">设定始终在你的上下文中保留的最新消息数量。</small>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="settings-group">
|
<fieldset class="settings-group">
|
||||||
<legend><i class="fas fa-pen-ruler"></i> 手动敕令司</legend>
|
<legend><i class="fas fa-pen-ruler"></i> 手动敕令司</legend>
|
||||||
|
|
||||||
|
|
||||||
<div class="manual-command-block">
|
<div class="manual-command-block">
|
||||||
<label>隐藏范围:</label>
|
<label>隐藏范围:</label>
|
||||||
<input type="number" id="amily2_manual_hide_from" class="manual-input" placeholder="起始层">
|
<input type="number" id="amily2_manual_hide_from" class="manual-input" placeholder="起始层">
|
||||||
<span class="manual-command-divider">-</span>
|
<span class="manual-command-divider">-</span>
|
||||||
<input type="number" id="amily2_manual_hide_to" class="manual-input" placeholder="结束层">
|
<input type="number" id="amily2_manual_hide_to" class="manual-input" placeholder="结束层">
|
||||||
<button id="amily2_manual_hide_confirm" class="menu_button primary small_button interactable">
|
<button id="amily2_manual_hide_confirm" class="menu_button primary small_button interactable">
|
||||||
<i class="fas fa-eye-slash"></i> 确认隐藏
|
<i class="fas fa-eye-slash"></i> 确认隐藏
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="manual-command-block">
|
<div class="manual-command-block">
|
||||||
<label>取消隐藏:</label>
|
<label>取消隐藏:</label>
|
||||||
<input type="number" id="amily2_manual_unhide_from" class="manual-input" placeholder="起始层">
|
<input type="number" id="amily2_manual_unhide_from" class="manual-input" placeholder="起始层">
|
||||||
<span class="manual-command-divider">-</span>
|
<span class="manual-command-divider">-</span>
|
||||||
<input type="number" id="amily2_manual_unhide_to" class="manual-input" placeholder="结束层">
|
<input type="number" id="amily2_manual_unhide_to" class="manual-input" placeholder="结束层">
|
||||||
<button id="amily2_manual_unhide_confirm" class="menu_button accent small_button interactable">
|
<button id="amily2_manual_unhide_confirm" class="menu_button accent small_button interactable">
|
||||||
<i class="fas fa-eye"></i> 确认取消
|
<i class="fas fa-eye"></i> 确认取消
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="notes" style="text-align: center; display: block; margin-top: 10px;">
|
<small class="notes" style="text-align: center; display: block; margin-top: 10px;">
|
||||||
提示:若“起始层”留空,则仅操作“结束层”所指定的单层。
|
提示:若“起始层”留空,则仅操作“结束层”所指定的单层。
|
||||||
</small>
|
</small>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
||||||
<fieldset class="settings-group">
|
<fieldset class="settings-group">
|
||||||
<legend><i class="fas fa-gavel"></i> 总结与律法</legend>
|
<legend><i class="fas fa-gavel"></i> 总结与律法</legend>
|
||||||
<div class="lore-config-grid">
|
<div class="lore-config-grid">
|
||||||
<!-- Left Column -->
|
<!-- Left Column -->
|
||||||
<div class="amily2_settings_block grid-left-col">
|
<div class="amily2_settings_block grid-left-col">
|
||||||
<label>总结写入目标:</label>
|
<label>总结写入目标:</label>
|
||||||
<div class="radio-group vertical">
|
<div class="radio-group vertical">
|
||||||
<input type="radio" id="amily2_target_main" name="amily2_lorebook_target" value="character_main" checked>
|
<input type="radio" id="amily2_target_main" name="amily2_lorebook_target" value="character_main" checked>
|
||||||
<label for="amily2_target_main">写入【主世界书】</label>
|
<label for="amily2_target_main">写入【主世界书】</label>
|
||||||
<input type="radio" id="amily2_target_dedicated" name="amily2_lorebook_target" value="dedicated">
|
<input type="radio" id="amily2_target_dedicated" name="amily2_lorebook_target" value="dedicated">
|
||||||
<label for="amily2_target_dedicated">写入【独立档案】</label>
|
<label for="amily2_target_dedicated">写入【独立档案】</label>
|
||||||
</div>
|
</div>
|
||||||
<small class="notes">更推荐使用独立档案,会生成一个新的世界书执行总结逻辑。</small>
|
<small class="notes">更推荐使用独立档案,会生成一个新的世界书执行总结逻辑。</small>
|
||||||
</div>
|
</div>
|
||||||
<!-- Right Column -->
|
<!-- Right Column -->
|
||||||
<div class="grid-right-col">
|
<div class="grid-right-col">
|
||||||
<div class="amily2_settings_block">
|
<div class="amily2_settings_block">
|
||||||
<label for="amily2_lore_activation_mode">默认激活模式:</label>
|
<label for="amily2_lore_activation_mode">默认激活模式:</label>
|
||||||
<select id="amily2_lore_activation_mode" class="text_pole">
|
<select id="amily2_lore_activation_mode" class="text_pole">
|
||||||
<option value="always">🔵 蓝灯 (始终激活)</option>
|
<option value="always">🔵 蓝灯 (始终激活)</option>
|
||||||
<option value="keyed">🟢 绿灯 (关键词触发)</option>
|
<option value="keyed">🟢 绿灯 (关键词触发)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="amily2_settings_block">
|
<div class="amily2_settings_block">
|
||||||
<label for="amily2_lore_insertion_position">默认插入位置:</label>
|
<label for="amily2_lore_insertion_position">默认插入位置:</label>
|
||||||
<select id="amily2_lore_insertion_position" class="text_pole">
|
<select id="amily2_lore_insertion_position" class="text_pole">
|
||||||
<option value="before_char">角色定义之前</option>
|
<option value="before_char">角色定义之前</option>
|
||||||
<option value="after_char">角色定义之后</option>
|
<option value="after_char">角色定义之后</option>
|
||||||
<option value="before_an">作者注释之前</option>
|
<option value="before_an">作者注释之前</option>
|
||||||
<option value="after_an">作者注释之后</option>
|
<option value="after_an">作者注释之后</option>
|
||||||
<option value="at_depth">@D 注入指定深度</option>
|
<option value="at_depth">@D 注入指定深度</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="amily2_lore_depth_container" class="amily2_settings_block" style="display: none;">
|
<div id="amily2_lore_depth_container" class="amily2_settings_block" style="display: none;">
|
||||||
<label for="amily2_lore_depth_input">注入深度:</label>
|
<label for="amily2_lore_depth_input">注入深度:</label>
|
||||||
<input id="amily2_lore_depth_input" title="深度" class="text_pole" type="number" name="depth" placeholder="2" min="0" max="9999">
|
<input id="amily2_lore_depth_input" title="深度" class="text_pole" type="number" name="depth" placeholder="2" min="0" max="9999">
|
||||||
</div>
|
</div>
|
||||||
<div class="amily2_settings_block">
|
<div class="amily2_settings_block">
|
||||||
<button id="amily2_save_lore_settings" class="menu_button"><i class="fas fa-save"></i> 确认敕令</button>
|
<button id="amily2_save_lore_settings" class="menu_button"><i class="fas fa-save"></i> 确认敕令</button>
|
||||||
<small id="amily2_lore_save_status" class="notes" style="text-align: center; width: 100%;"></small>
|
<small id="amily2_lore_save_status" class="notes" style="text-align: center; width: 100%;"></small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|
||||||
<fieldset class="settings-group">
|
<fieldset class="settings-group">
|
||||||
<legend><i class="fas fa-satellite-dish"></i> Ngms API 调用系统</legend>
|
<legend><i class="fas fa-satellite-dish"></i> Ngms API 调用系统</legend>
|
||||||
<small class="notes" style="text-align: center; display: block; margin-bottom: 15px;">
|
<small class="notes" style="text-align: center; display: block; margin-bottom: 15px;">
|
||||||
独立的API调用系统,可与主系统并行使用,支持全兼容和SillyTavern预设两种模式。
|
独立的API调用系统,可与主系统并行使用,支持全兼容和SillyTavern预设两种模式。
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<div class="amily2_settings_block">
|
<div class="amily2_settings_block">
|
||||||
<label for="amily2_ngms_enabled">启用Ngms API系统</label>
|
<label for="amily2_ngms_enabled">启用Ngms API系统</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input id="amily2_ngms_enabled" type="checkbox" />
|
<input id="amily2_ngms_enabled" type="checkbox" />
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_settings_block" id="amily2_ngms_content" style="display: none;">
|
<div class="amily2_settings_block" id="amily2_ngms_content" style="display: none;">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="amily2_ngms_api_mode">API调用模式:</label>
|
<label for="amily2_ngms_api_mode">API调用模式:</label>
|
||||||
<select id="amily2_ngms_api_mode" class="text_pole">
|
<select id="amily2_ngms_api_mode" class="text_pole">
|
||||||
<option value="openai_test">全兼容模式</option>
|
<option value="openai_test">全兼容模式</option>
|
||||||
<option value="sillytavern_preset">SillyTavern预设模式</option>
|
<option value="sillytavern_preset">SillyTavern预设模式</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="amily2_ngms_config_panel" style="margin-top: 15px;">
|
<div id="amily2_ngms_config_panel" style="margin-top: 15px;">
|
||||||
<!-- 全兼容模式配置 -->
|
<!-- 全兼容模式配置 -->
|
||||||
<div id="amily2_ngms_compatible_config">
|
<div id="amily2_ngms_compatible_config">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="amily2_ngms_api_url">API地址:</label>
|
<label for="amily2_ngms_api_url">API地址:</label>
|
||||||
<input type="text" id="amily2_ngms_api_url" class="text_pole" placeholder="https://api.openai.com/v1" />
|
<input type="text" id="amily2_ngms_api_url" class="text_pole" placeholder="https://api.openai.com/v1" />
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="amily2_ngms_api_key">API密钥:</label>
|
<label for="amily2_ngms_api_key">API密钥:</label>
|
||||||
<input type="password" id="amily2_ngms_api_key" class="text_pole" placeholder="sk-..." />
|
<input type="password" id="amily2_ngms_api_key" class="text_pole" placeholder="sk-..." />
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="amily2_ngms_model">模型:</label>
|
<label for="amily2_ngms_model">模型:</label>
|
||||||
<div class="select-with-refresh">
|
<div class="select-with-refresh">
|
||||||
<select id="amily2_ngms_model_select" class="text_pole" style="flex-grow: 1; display: none;">
|
<select id="amily2_ngms_model_select" class="text_pole" style="flex-grow: 1; display: none;">
|
||||||
<option value="">-- 请选择模型 --</option>
|
<option value="">-- 请选择模型 --</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="text" id="amily2_ngms_model" class="text_pole" placeholder="gpt-4" style="flex-grow: 1;" />
|
<input type="text" id="amily2_ngms_model" class="text_pole" placeholder="gpt-4" style="flex-grow: 1;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SillyTavern预设模式配置 -->
|
<!-- SillyTavern预设模式配置 -->
|
||||||
<div id="amily2_ngms_preset_config" style="display: none;">
|
<div id="amily2_ngms_preset_config" style="display: none;">
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="amily2_ngms_tavern_profile">选择SillyTavern预设:</label>
|
<label for="amily2_ngms_tavern_profile">选择SillyTavern预设:</label>
|
||||||
<select id="amily2_ngms_tavern_profile" class="text_pole">
|
<select id="amily2_ngms_tavern_profile" class="text_pole">
|
||||||
<option value="">-- 请选择预设 --</option>
|
<option value="">-- 请选择预设 --</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 通用参数配置 -->
|
<!-- 通用参数配置 -->
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="amily2_ngms_max_tokens">最大令牌数:<span id="amily2_ngms_max_tokens_value">4000</span></label>
|
<label for="amily2_ngms_max_tokens">最大令牌数:<span id="amily2_ngms_max_tokens_value">4000</span></label>
|
||||||
<input type="range" id="amily2_ngms_max_tokens" min="100" max="100000" step="100" value="4000" />
|
<input type="range" id="amily2_ngms_max_tokens" min="100" max="100000" step="100" value="4000" />
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label for="amily2_ngms_temperature">温度:<span id="amily2_ngms_temperature_value">0.7</span></label>
|
<label for="amily2_ngms_temperature">温度:<span id="amily2_ngms_temperature_value">0.7</span></label>
|
||||||
<input type="range" id="amily2_ngms_temperature" min="0" max="2" step="0.1" value="0.7" />
|
<input type="range" id="amily2_ngms_temperature" min="0" max="2" step="0.1" value="0.7" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 测试按钮组 - 水平排列 -->
|
<div class="control-group" style="display: flex; align-items: center; gap: 10px;">
|
||||||
<div class="ngms-button-row" style="display: flex; gap: 10px; justify-content: center; margin-top: 15px;">
|
<label for="amily2_ngms_fakestream_enabled" style="margin-bottom: 0;">启用流式支持 (防超时)</label>
|
||||||
<button id="amily2_ngms_test_connection" class="menu_button primary small_button interactable">
|
<input type="checkbox" id="amily2_ngms_fakestream_enabled" style="width: auto;" />
|
||||||
<i class="fas fa-plug"></i> 测试连接
|
</div>
|
||||||
</button>
|
|
||||||
<button id="amily2_ngms_fetch_models" class="menu_button secondary small_button interactable" title="获取可用模型列表">
|
<!-- 测试按钮组 - 水平排列 -->
|
||||||
<i class="fas fa-download"></i> 获取模型
|
<div class="ngms-button-row" style="display: flex; gap: 10px; justify-content: center; margin-top: 15px;">
|
||||||
</button>
|
<button id="amily2_ngms_test_connection" class="menu_button primary small_button interactable">
|
||||||
</div>
|
<i class="fas fa-plug"></i> 测试连接
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<button id="amily2_ngms_fetch_models" class="menu_button secondary small_button interactable" title="获取可用模型列表">
|
||||||
</fieldset>
|
<i class="fas fa-download"></i> 获取模型
|
||||||
|
</button>
|
||||||
<fieldset class="settings-group" id="amily2_manual_historiography_bureau">
|
</div>
|
||||||
<legend><i class="fas fa-gavel"></i> 手动敕史局</legend>
|
</div>
|
||||||
<small class="notes" style="text-align: center; display: block; margin-bottom: 15px;">
|
</div>
|
||||||
赋予你选取任意对话、熔铸为永恒史册的至高权力。
|
</fieldset>
|
||||||
</small>
|
|
||||||
|
<fieldset class="settings-group" id="amily2_manual_historiography_bureau">
|
||||||
|
<legend><i class="fas fa-gavel"></i> 手动敕史局</legend>
|
||||||
<fieldset class="settings-group" style="border-style: dashed;">
|
<small class="notes" style="text-align: center; display: block; margin-bottom: 15px;">
|
||||||
<legend><EFBFBD> 微言录 (Small Summary)</legend>
|
赋予你选取任意对话、熔铸为永恒史册的至高权力。
|
||||||
|
</small>
|
||||||
|
|
||||||
<div class="amily2_settings_block">
|
|
||||||
<div class="label-with-button">
|
<fieldset class="settings-group" style="border-style: dashed;">
|
||||||
<label for="amily2_mhb_small_prompt_selector">选择编辑的谕旨:</label>
|
<legend><EFBFBD> 微言录 (Small Summary)</legend>
|
||||||
<i id="amily2_mhb_small_expand_editor" class="editor_maximize fa-solid fa-maximize right_menu_button interactable" title="展开编辑器" tabindex="0"></i>
|
|
||||||
</div>
|
|
||||||
<select id="amily2_mhb_small_prompt_selector" class="text_pole">
|
<div class="amily2_settings_block">
|
||||||
<option value="jailbreak">小总结主要提示词</option>
|
<div class="label-with-button">
|
||||||
<option value="summary">小总结任务提示词</option>
|
<label for="amily2_mhb_small_prompt_selector">选择编辑的谕旨:</label>
|
||||||
</select>
|
<i id="amily2_mhb_small_expand_editor" class="editor_maximize fa-solid fa-maximize right_menu_button interactable" title="展开编辑器" tabindex="0"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="amily2_settings_block prompt-editor-area">
|
<select id="amily2_mhb_small_prompt_selector" class="text_pole">
|
||||||
<textarea id="amily2_mhb_small_editor" class="text_pole" rows="3"></textarea>
|
<option value="jailbreak">小总结主要提示词</option>
|
||||||
<div class="editor-buttons-panel">
|
<option value="summary">小总结任务提示词</option>
|
||||||
<button id="amily2_mhb_small_save_button" class="menu_button accent small_button interactable"><i class="fas fa-save"></i> 保存当前</button>
|
</select>
|
||||||
<button id="amily2_mhb_small_restore_button" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 恢复默认</button>
|
</div>
|
||||||
</div>
|
<div class="amily2_settings_block prompt-editor-area">
|
||||||
</div>
|
<textarea id="amily2_mhb_small_editor" class="text_pole" rows="3"></textarea>
|
||||||
|
<div class="editor-buttons-panel">
|
||||||
|
<button id="amily2_mhb_small_save_button" class="menu_button accent small_button interactable"><i class="fas fa-save"></i> 保存当前</button>
|
||||||
<div class="mhb-controls-wrapper">
|
<button id="amily2_mhb_small_restore_button" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 恢复默认</button>
|
||||||
|
</div>
|
||||||
<div class="manual-command-block">
|
</div>
|
||||||
<label>手动总结范围:</label>
|
|
||||||
<input type="number" id="amily2_mhb_small_start_floor" class="manual-input" placeholder="起始层">
|
|
||||||
<span class="manual-command-divider">-</span>
|
<div class="mhb-controls-wrapper">
|
||||||
<input type="number" id="amily2_mhb_small_end_floor" class="manual-input" placeholder="结束层">
|
|
||||||
<button id="amily2_mhb_small_manual_execute" class="menu_button primary small_button interactable">
|
<div class="manual-command-block">
|
||||||
<i class="fas fa-fire"></i> 开始
|
<label>手动总结范围:</label>
|
||||||
</button>
|
<input type="number" id="amily2_mhb_small_start_floor" class="manual-input" placeholder="起始层">
|
||||||
</div>
|
<span class="manual-command-divider">-</span>
|
||||||
|
<input type="number" id="amily2_mhb_small_end_floor" class="manual-input" placeholder="结束层">
|
||||||
<div class="hly-control-block" style="flex-direction: row; justify-content: space-between; align-items: center; margin-top: 10px;">
|
<button id="amily2_mhb_small_manual_execute" class="menu_button primary small_button interactable">
|
||||||
<label for="historiography-tag-extraction-toggle">标签提取</label>
|
<i class="fas fa-fire"></i> 开始
|
||||||
<label class="hly-toggle-switch">
|
</button>
|
||||||
<input type="checkbox" id="historiography-tag-extraction-toggle" data-setting-key="condensation.tagExtractionEnabled" data-type="boolean">
|
</div>
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
<div class="hly-control-block" style="flex-direction: row; justify-content: space-between; align-items: center; margin-top: 10px;">
|
||||||
</div>
|
<label for="historiography-tag-extraction-toggle">标签提取</label>
|
||||||
<div id="historiography-tag-input-container" class="hly-control-block" style="display: none;">
|
<label class="hly-toggle-switch">
|
||||||
<label for="historiography-tag-input">输入标签 (以逗号分隔):</label>
|
<input type="checkbox" id="historiography-tag-extraction-toggle" data-setting-key="condensation.tagExtractionEnabled" data-type="boolean">
|
||||||
<textarea id="historiography-tag-input" class="hly-imperial-brush" rows="2" placeholder="例如: content,details" data-setting-key="condensation.tags" data-type="string"></textarea>
|
<span class="slider"></span>
|
||||||
</div>
|
</label>
|
||||||
<div class="hly-button-group" style="justify-content: flex-start; margin-top: 10px; display: flex; align-items: center; gap: 20px;">
|
</div>
|
||||||
<button id="historiography-exclusion-rules-btn" class="hly-action-button">内容排除</button>
|
<div id="historiography-tag-input-container" class="hly-control-block" style="display: none;">
|
||||||
|
<label for="historiography-tag-input">输入标签 (以逗号分隔):</label>
|
||||||
<div class="auto-control-pair" style="margin-bottom: 0;">
|
<textarea id="historiography-tag-input" class="hly-imperial-brush" rows="2" placeholder="例如: content,details" data-setting-key="condensation.tags" data-type="string"></textarea>
|
||||||
<label for="historiography_auto_summary_interactive" title="开启后,“自动巡录”将弹出交互窗口确认,而不是在后台静默运行。">交互式巡录:</label>
|
</div>
|
||||||
<label class="toggle-switch">
|
<div class="hly-button-group" style="justify-content: flex-start; margin-top: 10px; display: flex; align-items: center; gap: 20px;">
|
||||||
<input id="historiography_auto_summary_interactive" type="checkbox" />
|
<button id="historiography-exclusion-rules-btn" class="hly-action-button">内容排除</button>
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
<div class="auto-control-pair" style="margin-bottom: 0;">
|
||||||
</div>
|
<label for="historiography_auto_summary_interactive" title="开启后,“自动巡录”将弹出交互窗口确认,而不是在后台静默运行。">交互式巡录:</label>
|
||||||
</div>
|
<label class="toggle-switch">
|
||||||
|
<input id="historiography_auto_summary_interactive" type="checkbox" />
|
||||||
|
<span class="slider"></span>
|
||||||
<div class="auto-command-block" id="amily2_mhb_auto_command_block">
|
</label>
|
||||||
|
</div>
|
||||||
<button id="amily2_mhb_small_expedition_execute" class="menu_button primary small_button interactable" title="立即发动一次彻底的总结远征,将所有未归档的历史一次性清算。">
|
</div>
|
||||||
<i class="fas fa-flag-checkered"></i> 自动批量
|
|
||||||
</button>
|
|
||||||
|
<div class="auto-command-block" id="amily2_mhb_auto_command_block">
|
||||||
|
|
||||||
<div class="auto-control-pair">
|
<button id="amily2_mhb_small_expedition_execute" class="menu_button primary small_button interactable" title="立即发动一次彻底的总结远征,将所有未归档的历史一次性清算。">
|
||||||
<label for="amily2_mhb_small_auto_enabled" title="在您聊天时,于后台默默守护史册的完整。">静默总结:</label>
|
<i class="fas fa-flag-checkered"></i> 自动批量
|
||||||
<label class="toggle-switch">
|
</button>
|
||||||
<input id="amily2_mhb_small_auto_enabled" type="checkbox" />
|
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
<div class="auto-control-pair">
|
||||||
</div>
|
<label for="amily2_mhb_small_auto_enabled" title="在您聊天时,于后台默默守护史册的完整。">静默总结:</label>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input id="amily2_mhb_small_auto_enabled" type="checkbox" />
|
||||||
<div class="auto-control-pair">
|
<span class="slider"></span>
|
||||||
<label for="historiography_write_to_lorebook" title="将生成的总结写入世界书。">存世界书:</label>
|
</label>
|
||||||
<label class="toggle-switch">
|
</div>
|
||||||
<input id="historiography_write_to_lorebook" type="checkbox" checked />
|
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
<div class="auto-control-pair">
|
||||||
</div>
|
<label for="historiography_write_to_lorebook" title="将生成的总结写入世界书。">存世界书:</label>
|
||||||
|
<label class="toggle-switch">
|
||||||
<div class="auto-control-pair">
|
<input id="historiography_write_to_lorebook" type="checkbox" checked />
|
||||||
<label for="historiography_ingest_to_rag" title="将生成的总结存入翰林院进行向量化。">上传向量:</label>
|
<span class="slider"></span>
|
||||||
<label class="toggle-switch">
|
</label>
|
||||||
<input id="historiography_ingest_to_rag" type="checkbox" />
|
</div>
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
<div class="auto-control-pair">
|
||||||
</div>
|
<label for="historiography_ingest_to_rag" title="将生成的总结存入翰林院进行向量化。">上传向量:</label>
|
||||||
|
<label class="toggle-switch">
|
||||||
<div class="auto-control-pair">
|
<input id="historiography_ingest_to_rag" type="checkbox" />
|
||||||
<label for="amily2_mhb_small_trigger_count" title="“自动巡录”和“开始远征”的单次作战楼层数。">总结阈值:</label>
|
<span class="slider"></span>
|
||||||
<input id="amily2_mhb_small_trigger_count" type="number" min="1" class="text_pole" style="width: 70px;" placeholder="30">
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auto-control-pair">
|
<div class="auto-control-pair">
|
||||||
<label for="historiography_retention_count" title="保留最近的对话层数不参与自动总结。">保留层数:</label>
|
<label for="amily2_mhb_small_trigger_count" title="“自动巡录”和“开始远征”的单次作战楼层数。">总结阈值:</label>
|
||||||
<input id="historiography_retention_count" type="number" min="0" class="text_pole" style="width: 70px;" placeholder="5">
|
<input id="amily2_mhb_small_trigger_count" type="number" min="1" class="text_pole" style="width: 70px;" placeholder="30">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="auto-control-pair">
|
||||||
|
<label for="historiography_retention_count" title="保留最近的对话层数不参与自动总结。">保留层数:</label>
|
||||||
<small class="notes" style="text-align: center; display: block; margin-top: 5px;">
|
<input id="historiography_retention_count" type="number" min="0" class="text_pole" style="width: 70px;" placeholder="5">
|
||||||
【自动批量】将立即清算所有未记录的历史。 【静默总结】则在您聊天时,于后台默默守护史册的完整。
|
</div>
|
||||||
</small>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
<small class="notes" style="text-align: center; display: block; margin-top: 5px;">
|
||||||
</fieldset>
|
【自动批量】将立即清算所有未记录的历史。 【静默总结】则在您聊天时,于后台默默守护史册的完整。
|
||||||
|
</small>
|
||||||
<!-- 上下分割线 -->
|
|
||||||
<hr class="header-divider" style="margin: 20px 0;">
|
|
||||||
|
</div>
|
||||||
<fieldset class="settings-group" style="border-style: dashed;">
|
</fieldset>
|
||||||
<legend>📚 史册归档与回溯</legend>
|
|
||||||
<small class="notes" style="text-align: center; display: block; margin-bottom: 15px;">
|
<!-- 上下分割线 -->
|
||||||
管理多条时间线的史册,随时封存或重启旧的历史。
|
<hr class="header-divider" style="margin: 20px 0;">
|
||||||
</small>
|
|
||||||
|
<fieldset class="settings-group" style="border-style: dashed;">
|
||||||
<div class="amily2_settings_block">
|
<legend>📚 史册归档与回溯</legend>
|
||||||
<button id="amily2_mhb_archive_current" class="menu_button secondary small_button interactable" style="width: 100%; margin-bottom: 10px;">
|
<small class="notes" style="text-align: center; display: block; margin-bottom: 15px;">
|
||||||
<i class="fas fa-archive"></i> 归档当前【对话流水总帐】并停用
|
管理多条时间线的史册,随时封存或重启旧的历史。
|
||||||
</button>
|
</small>
|
||||||
</div>
|
|
||||||
|
<div class="amily2_settings_block">
|
||||||
<div class="mhb-selector-container">
|
<button id="amily2_mhb_archive_current" class="menu_button secondary small_button interactable" style="width: 100%; margin-bottom: 10px;">
|
||||||
<div class="mhb-selector-group">
|
<i class="fas fa-archive"></i> 归档当前【对话流水总帐】并停用
|
||||||
<label for="amily2_mhb_archive_selector">选择要回溯的旧史册</label>
|
</button>
|
||||||
<div class="select-with-refresh">
|
</div>
|
||||||
<select id="amily2_mhb_archive_selector" class="text_pole" style="flex-grow: 1;">
|
|
||||||
<option value="">请刷新列表...</option>
|
<div class="mhb-selector-container">
|
||||||
</select>
|
<div class="mhb-selector-group">
|
||||||
<button id="amily2_mhb_refresh_archives" class="menu_button secondary small_button interactable" title="刷新归档列表">
|
<label for="amily2_mhb_archive_selector">选择要回溯的旧史册</label>
|
||||||
<i class="fas fa-sync-alt"></i>
|
<div class="select-with-refresh">
|
||||||
</button>
|
<select id="amily2_mhb_archive_selector" class="text_pole" style="flex-grow: 1;">
|
||||||
</div>
|
<option value="">请刷新列表...</option>
|
||||||
</div>
|
</select>
|
||||||
</div>
|
<button id="amily2_mhb_refresh_archives" class="menu_button secondary small_button interactable" title="刷新归档列表">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
<button id="amily2_mhb_restore_archive" class="menu_button primary small_button interactable" style="margin-top: 10px; width: 100%;">
|
</button>
|
||||||
<i class="fas fa-trash-restore"></i> 回溯选中史册 (自动归档当前)
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
|
||||||
|
<button id="amily2_mhb_restore_archive" class="menu_button primary small_button interactable" style="margin-top: 10px; width: 100%;">
|
||||||
<!-- 上下分割线 -->
|
<i class="fas fa-trash-restore"></i> 回溯选中史册 (自动归档当前)
|
||||||
<hr class="header-divider" style="margin: 20px 0;">
|
</button>
|
||||||
|
|
||||||
<fieldset class="settings-group" style="border-style: dashed;">
|
</fieldset>
|
||||||
<legend>💎 宏史卷 (史册精炼)</legend>
|
|
||||||
|
<!-- 上下分割线 -->
|
||||||
|
<hr class="header-divider" style="margin: 20px 0;">
|
||||||
<div class="amily2_settings_block">
|
|
||||||
<div class="label-with-button">
|
<fieldset class="settings-group" style="border-style: dashed;">
|
||||||
<label for="amily2_mhb_large_prompt_selector">选择编辑的谕旨:</label>
|
<legend>💎 宏史卷 (史册精炼)</legend>
|
||||||
<i id="amily2_mhb_large_expand_editor" class="editor_maximize fa-solid fa-maximize right_menu_button interactable" title="展开编辑器" tabindex="0"></i>
|
|
||||||
</div>
|
|
||||||
<select id="amily2_mhb_large_prompt_selector" class="text_pole">
|
<div class="amily2_settings_block">
|
||||||
<option value="jailbreak">大总结主要提示词</option>
|
<div class="label-with-button">
|
||||||
<option value="summary">大总结任务提示词)</option>
|
<label for="amily2_mhb_large_prompt_selector">选择编辑的谕旨:</label>
|
||||||
</select>
|
<i id="amily2_mhb_large_expand_editor" class="editor_maximize fa-solid fa-maximize right_menu_button interactable" title="展开编辑器" tabindex="0"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="amily2_settings_block prompt-editor-area">
|
<select id="amily2_mhb_large_prompt_selector" class="text_pole">
|
||||||
<textarea id="amily2_mhb_large_editor" class="text_pole" rows="4"></textarea>
|
<option value="jailbreak">大总结主要提示词</option>
|
||||||
<div class="editor-buttons-panel">
|
<option value="summary">大总结任务提示词)</option>
|
||||||
<button id="amily2_mhb_large_save_button" class="menu_button accent small_button interactable"><i class="fas fa-save"></i> 保存当前</button>
|
</select>
|
||||||
<button id="amily2_mhb_large_restore_button" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 恢复默认</button>
|
</div>
|
||||||
</div>
|
<div class="amily2_settings_block prompt-editor-area">
|
||||||
</div>
|
<textarea id="amily2_mhb_large_editor" class="text_pole" rows="4"></textarea>
|
||||||
|
<div class="editor-buttons-panel">
|
||||||
|
<button id="amily2_mhb_large_save_button" class="menu_button accent small_button interactable"><i class="fas fa-save"></i> 保存当前</button>
|
||||||
<div class="mhb-controls-wrapper">
|
<button id="amily2_mhb_large_restore_button" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 恢复默认</button>
|
||||||
|
</div>
|
||||||
<div class="mhb-selector-container">
|
</div>
|
||||||
|
|
||||||
<div class="mhb-selector-group">
|
|
||||||
<label for="amily2_mhb_large_worldbook_selector">目标国史馆 (世界书)</label>
|
<div class="mhb-controls-wrapper">
|
||||||
<div class="select-with-refresh">
|
|
||||||
<select id="amily2_mhb_large_worldbook_selector" class="text_pole" style="flex-grow: 1;">
|
<div class="mhb-selector-container">
|
||||||
<option value="">刷新列表...</option>
|
|
||||||
</select>
|
<div class="mhb-selector-group">
|
||||||
<button id="amily2_mhb_large_refresh_worldbooks" class="menu_button secondary small_button interactable" title="刷新世界书列表">
|
<label for="amily2_mhb_large_worldbook_selector">目标国史馆 (世界书)</label>
|
||||||
<i class="fas fa-atlas"></i>
|
<div class="select-with-refresh">
|
||||||
</button>
|
<select id="amily2_mhb_large_worldbook_selector" class="text_pole" style="flex-grow: 1;">
|
||||||
</div>
|
<option value="">刷新列表...</option>
|
||||||
</div>
|
</select>
|
||||||
|
<button id="amily2_mhb_large_refresh_worldbooks" class="menu_button secondary small_button interactable" title="刷新世界书列表">
|
||||||
<div class="mhb-selector-group">
|
<i class="fas fa-atlas"></i>
|
||||||
<label for="amily2_mhb_large_lore_selector">待精炼的史册条目</label>
|
</button>
|
||||||
<div class="select-with-refresh">
|
</div>
|
||||||
<select id="amily2_mhb_large_lore_selector" class="text_pole" style="flex-grow: 1;">
|
</div>
|
||||||
<option value="">请先选择世界书...</option>
|
|
||||||
</select>
|
<div class="mhb-selector-group">
|
||||||
<button id="amily2_mhb_large_refresh_lores" class="menu_button secondary small_button interactable" title="刷新所选世界书的史册列表">
|
<label for="amily2_mhb_large_lore_selector">待精炼的史册条目</label>
|
||||||
<i class="fas fa-sync-alt"></i>
|
<div class="select-with-refresh">
|
||||||
</button>
|
<select id="amily2_mhb_large_lore_selector" class="text_pole" style="flex-grow: 1;">
|
||||||
</div>
|
<option value="">请先选择世界书...</option>
|
||||||
</div>
|
</select>
|
||||||
</div>
|
<button id="amily2_mhb_large_refresh_lores" class="menu_button secondary small_button interactable" title="刷新所选世界书的史册列表">
|
||||||
|
<i class="fas fa-sync-alt"></i>
|
||||||
|
</button>
|
||||||
<button id="amily2_mhb_large_refine_execute" class="menu_button primary small_button interactable" style="margin-top: 15px; width: 100%;">
|
</div>
|
||||||
<i class="fas fa-gem"></i> 开始精炼
|
</div>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
<div class="auto-control-pair" style="margin-top: 10px; justify-content: center;">
|
|
||||||
<label for="amily2_vectorize_summary_content">将前段的大总结内容自动送去向量化。</label>
|
<button id="amily2_mhb_large_refine_execute" class="menu_button primary small_button interactable" style="margin-top: 15px; width: 100%;">
|
||||||
<label class="toggle-switch">
|
<i class="fas fa-gem"></i> 开始精炼
|
||||||
<input id="amily2_vectorize_summary_content" type="checkbox" />
|
</button>
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
<div class="auto-control-pair" style="margin-top: 10px; justify-content: center;">
|
||||||
</div>
|
<label for="amily2_vectorize_summary_content">将前段的大总结内容自动送去向量化。</label>
|
||||||
</div>
|
<label class="toggle-switch">
|
||||||
</div>
|
<input id="amily2_vectorize_summary_content" type="checkbox" />
|
||||||
</fieldset>
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div id="amily2_mhb_hidden_prompts" style="display:none;">
|
</div>
|
||||||
<textarea id="amily2_mhb_small_jailbreak_prompt"></textarea>
|
</div>
|
||||||
<textarea id="amily2_mhb_small_summary_prompt"></textarea>
|
</fieldset>
|
||||||
<textarea id="amily2_mhb_large_jailbreak_prompt"></textarea>
|
|
||||||
<textarea id="amily2_mhb_large_summary_prompt"></textarea>
|
|
||||||
</div>
|
<div id="amily2_mhb_hidden_prompts" style="display:none;">
|
||||||
|
<textarea id="amily2_mhb_small_jailbreak_prompt"></textarea>
|
||||||
</fieldset>
|
<textarea id="amily2_mhb_small_summary_prompt"></textarea>
|
||||||
|
<textarea id="amily2_mhb_large_jailbreak_prompt"></textarea>
|
||||||
|
<textarea id="amily2_mhb_large_summary_prompt"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</fieldset>
|
||||||
@@ -1,428 +1,433 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Amily2 表格编辑器</title>
|
<title>Amily2 表格编辑器</title>
|
||||||
<link rel="stylesheet" href="table.css">
|
<link rel="stylesheet" href="table.css">
|
||||||
<style>
|
<style>
|
||||||
.worldbook-selection-container {
|
.worldbook-selection-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.worldbook-column {
|
.worldbook-column {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: 0; /* Prevents flex items from overflowing */
|
min-width: 0; /* Prevents flex items from overflowing */
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable-container {
|
.scrollable-container {
|
||||||
border: 1px solid var(--input-border-color, #444);
|
border: 1px solid var(--input-border-color, #444);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: var(--input-bg-color, #222);
|
background-color: var(--input-bg-color, #222);
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable-container .checkbox-item {
|
.scrollable-container .checkbox-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable-container .checkbox-item input[type="checkbox"] {
|
.scrollable-container .checkbox-item input[type="checkbox"] {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable-container .checkbox-item label {
|
.scrollable-container .checkbox-item label {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
select.text_pole {
|
select.text_pole {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
|
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: right 8px center;
|
background-position: right 8px center;
|
||||||
background-size: 1.2em;
|
background-size: 1.2em;
|
||||||
padding-right: 2em !important;
|
padding-right: 2em !important;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="amily2-header">
|
<div class="amily2-header">
|
||||||
<div class="additional-features-title">
|
<div class="additional-features-title">
|
||||||
<i class="fas fa-table"></i> 内存储司 · 表格核心
|
<i class="fas fa-table"></i> 内存储司 · 表格核心
|
||||||
</div>
|
</div>
|
||||||
<button id="amily2_back_to_main_from_forms" class="menu_button secondary small_button interactable">
|
<button id="amily2_back_to_main_from_forms" class="menu_button secondary small_button interactable">
|
||||||
返回主殿 <i class="fas fa-arrow-right"></i>
|
返回主殿 <i class="fas fa-arrow-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<hr class="header-divider" style="margin-top: 5px; margin-bottom: 10px;">
|
<hr class="header-divider" style="margin-top: 5px; margin-bottom: 10px;">
|
||||||
|
|
||||||
<div id="upper-controls-wrapper">
|
<div id="upper-controls-wrapper">
|
||||||
<fieldset class="settings-group" style="padding: 10px; margin-bottom: 10px;">
|
<fieldset class="settings-group" style="padding: 10px; margin-bottom: 10px;">
|
||||||
<legend><i class="fas fa-brain"></i> 中枢决策室</legend>
|
<legend><i class="fas fa-brain"></i> 中枢决策室</legend>
|
||||||
|
|
||||||
<div class="sinan-navigation-deck" style="gap: 5px; margin-bottom: 10px;">
|
<div class="sinan-navigation-deck" style="gap: 5px; margin-bottom: 10px;">
|
||||||
<button class="sinan-nav-item active" data-tab="injection-settings"><i class="fas fa-cogs"></i> 注入设置</button>
|
<button class="sinan-nav-item active" data-tab="injection-settings"><i class="fas fa-cogs"></i> 注入设置</button>
|
||||||
<button class="sinan-nav-item" data-tab="action-center"><i class="fas fa-toolbox"></i> 操作中心</button>
|
<button class="sinan-nav-item" data-tab="action-center"><i class="fas fa-toolbox"></i> 操作中心</button>
|
||||||
<button class="sinan-nav-item" data-tab="ai-template"><i class="fas fa-robot"></i> 指令模板</button>
|
<button class="sinan-nav-item" data-tab="ai-template"><i class="fas fa-robot"></i> 指令模板</button>
|
||||||
<button class="sinan-nav-item" data-tab="world-settings"><i class="fas fa-globe"></i> 世界读取</button>
|
<button class="sinan-nav-item" data-tab="world-settings"><i class="fas fa-globe"></i> 世界读取</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sinan-content-wrapper">
|
<div class="sinan-content-wrapper">
|
||||||
<div id="sinan-injection-settings-tab" class="sinan-tab-pane active">
|
<div id="sinan-injection-settings-tab" class="sinan-tab-pane active">
|
||||||
<div class="inline-settings-grid">
|
<div class="inline-settings-grid">
|
||||||
<label for="table-injection-position">注入位置</label>
|
<label for="table-injection-position">注入位置</label>
|
||||||
<select id="table-injection-position" class="text_pole">
|
<select id="table-injection-position" class="text_pole">
|
||||||
<option value="2">主提示前</option>
|
<option value="2">主提示前</option>
|
||||||
<option value="0">主提示后</option>
|
<option value="0">主提示后</option>
|
||||||
<option value="1">聊天内</option>
|
<option value="1">聊天内</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="table-injection-depth">注入深度</label>
|
<label for="table-injection-depth">注入深度</label>
|
||||||
<input type="number" id="table-injection-depth" class="text_pole" value="3">
|
<input type="number" id="table-injection-depth" class="text_pole" value="3">
|
||||||
<label>注入角色</label>
|
<label>注入角色</label>
|
||||||
<div class="radio-group">
|
<div class="radio-group">
|
||||||
<input type="radio" id="table-role-system" name="table-injection-role" value="0" data-setting-key="injection.role" data-type="integer">
|
<input type="radio" id="table-role-system" name="table-injection-role" value="0" data-setting-key="injection.role" data-type="integer">
|
||||||
<label for="table-role-system">系统</label>
|
<label for="table-role-system">系统</label>
|
||||||
<input type="radio" id="table-role-user" name="table-injection-role" value="1" data-setting-key="injection.role" data-type="integer">
|
<input type="radio" id="table-role-user" name="table-injection-role" value="1" data-setting-key="injection.role" data-type="integer">
|
||||||
<label for="table-role-user">用户</label>
|
<label for="table-role-user">用户</label>
|
||||||
<input type="radio" id="table-role-ai" name="table-injection-role" value="2" data-setting-key="injection.role" data-type="integer">
|
<input type="radio" id="table-role-ai" name="table-injection-role" value="2" data-setting-key="injection.role" data-type="integer">
|
||||||
<label for="table-role-ai">AI</label>
|
<label for="table-role-ai">AI</label>
|
||||||
</div>
|
</div>
|
||||||
<label for="batch-filling-threshold">批处理阈值</label>
|
<label for="batch-filling-threshold">批处理阈值</label>
|
||||||
<input type="number" id="batch-filling-threshold" class="text_pole" value="30">
|
<input type="number" id="batch-filling-threshold" class="text_pole" value="30">
|
||||||
</div>
|
</div>
|
||||||
<div class="control-block-with-switch" style="margin-top: 10px;">
|
<div class="control-block-with-switch" style="margin-top: 10px;">
|
||||||
<label for="show-table-in-chat-toggle">聊天内显示表格</label>
|
<label for="show-table-in-chat-toggle">聊天内显示表格</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="show-table-in-chat-toggle" data-setting-key="show_table_in_chat" data-type="boolean">
|
<input type="checkbox" id="show-table-in-chat-toggle" data-setting-key="show_table_in_chat" data-type="boolean">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-block-with-switch" style="margin-top: 10px;">
|
<div class="control-block-with-switch" style="margin-top: 10px;">
|
||||||
<label for="render-on-every-message-toggle">持续渲染最新消息</label>
|
<label for="render-on-every-message-toggle">持续渲染最新消息</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="render-on-every-message-toggle" data-setting-key="render_on_every_message" data-type="boolean">
|
<input type="checkbox" id="render-on-every-message-toggle" data-setting-key="render_on_every_message" data-type="boolean">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sinan-world-settings-tab" class="sinan-tab-pane">
|
<div id="sinan-world-settings-tab" class="sinan-tab-pane">
|
||||||
<div class="amily2_opt_settings_block">
|
<div class="amily2_opt_settings_block">
|
||||||
<label for="table_worldbook_enabled">启用世界书</label>
|
<label for="table_worldbook_enabled">启用世界书</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input id="table_worldbook_enabled" type="checkbox" />
|
<input id="table_worldbook_enabled" type="checkbox" />
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_opt_settings_block">
|
<div class="amily2_opt_settings_block">
|
||||||
<label for="table_worldbook_char_limit">世界书最大字符数: <span id="table_worldbook_char_limit_value">60000</span></label>
|
<label for="table_worldbook_char_limit">世界书最大字符数: <span id="table_worldbook_char_limit_value">60000</span></label>
|
||||||
<input type="range" id="table_worldbook_char_limit" min="1000" max="200000" step="1000" value="60000">
|
<input type="range" id="table_worldbook_char_limit" min="1000" max="200000" step="1000" value="60000">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="amily2_opt_settings_block_radio">
|
<div class="amily2_opt_settings_block_radio">
|
||||||
<label>世界书来源</label>
|
<label>世界书来源</label>
|
||||||
<div class="amily2_opt_radio_group">
|
<div class="amily2_opt_radio_group">
|
||||||
<input type="radio" id="table_worldbook_source_character" name="table_worldbook_source" value="character" checked>
|
<input type="radio" id="table_worldbook_source_character" name="table_worldbook_source" value="character" checked>
|
||||||
<label for="table_worldbook_source_character">角色卡主世界书</label>
|
<label for="table_worldbook_source_character">角色卡主世界书</label>
|
||||||
<input type="radio" id="table_worldbook_source_manual" name="table_worldbook_source" value="manual">
|
<input type="radio" id="table_worldbook_source_manual" name="table_worldbook_source" value="manual">
|
||||||
<label for="table_worldbook_source_manual">手动选择世界书</label>
|
<label for="table_worldbook_source_manual">手动选择世界书</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="table_worldbook_select_wrapper" style="display: none;">
|
<div id="table_worldbook_select_wrapper" style="display: none;">
|
||||||
<div class="worldbook-selection-container">
|
<div class="worldbook-selection-container">
|
||||||
<!-- World Book List Column -->
|
<!-- World Book List Column -->
|
||||||
<div class="worldbook-column">
|
<div class="worldbook-column">
|
||||||
<div class="amily2_opt_label_with_button_wrapper">
|
<div class="amily2_opt_label_with_button_wrapper">
|
||||||
<label>选择世界书 (可多选)</label>
|
<label>选择世界书 (可多选)</label>
|
||||||
<button id="table_refresh_worldbooks" class="menu_button" title="刷新世界书列表"><i class="fa-solid fa-sync"></i></button>
|
<button id="table_refresh_worldbooks" class="menu_button" title="刷新世界书列表"><i class="fa-solid fa-sync"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-search-wrapper">
|
<div class="table-search-wrapper">
|
||||||
<input type="text" id="table_worldbook_search" class="table-search-input" placeholder="搜索世界书名称...">
|
<input type="text" id="table_worldbook_search" class="table-search-input" placeholder="搜索世界书名称...">
|
||||||
<i class="fas fa-search table-search-icon"></i>
|
<i class="fas fa-search table-search-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div id="table_worldbook_checkbox_list" class="scrollable-container">
|
<div id="table_worldbook_checkbox_list" class="scrollable-container">
|
||||||
<!-- 世界书勾选框将在这里动态生成 -->
|
<!-- 世界书勾选框将在这里动态生成 -->
|
||||||
</div>
|
</div>
|
||||||
<small class="notes">勾选需要启用的世界书。</small>
|
<small class="notes">勾选需要启用的世界书。</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- World Book Entry List Column -->
|
<!-- World Book Entry List Column -->
|
||||||
<div class="worldbook-column" style="margin-top: 10px;">
|
<div class="worldbook-column" style="margin-top: 10px;">
|
||||||
<label>选择条目 (可多选)</label>
|
<label>选择条目 (可多选)</label>
|
||||||
<div class="table-search-wrapper">
|
<div class="table-search-wrapper">
|
||||||
<input type="text" id="table_entry_search" class="table-search-input" placeholder="搜索条目名称、关键词...">
|
<input type="text" id="table_entry_search" class="table-search-input" placeholder="搜索条目名称、关键词...">
|
||||||
<i class="fas fa-search table-search-icon"></i>
|
<i class="fas fa-search table-search-icon"></i>
|
||||||
</div>
|
</div>
|
||||||
<div id="table_worldbook_entry_list" class="scrollable-container">
|
<div id="table_worldbook_entry_list" class="scrollable-container">
|
||||||
<!-- 世界书条目勾选框将在这里动态生成 -->
|
<!-- 世界书条目勾选框将在这里动态生成 -->
|
||||||
</div>
|
</div>
|
||||||
<small class="notes">勾选需要注入的条目。</small>
|
<small class="notes">勾选需要注入的条目。</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="sinan-action-center-tab" class="sinan-tab-pane">
|
<div id="sinan-action-center-tab" class="sinan-tab-pane">
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 15px; padding: 8px; border: 2px solid #4a9eff; border-radius: 5px; background: rgba(74, 158, 255, 0.1);">
|
<div class="control-block-with-switch" style="margin-bottom: 15px; padding: 8px; border: 2px solid #4a9eff; border-radius: 5px; background: rgba(74, 158, 255, 0.1);">
|
||||||
<label for="table-system-master-switch" style="font-weight: bold; color: #4a9eff;">
|
<label for="table-system-master-switch" style="font-weight: bold; color: #4a9eff;">
|
||||||
<i class="fas fa-power-off"></i> 表格系统总开关
|
<i class="fas fa-power-off"></i> 表格系统总开关
|
||||||
</label>
|
</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="table-system-master-switch" data-setting-key="table_system_enabled" data-type="boolean">
|
<input type="checkbox" id="table-system-master-switch" data-setting-key="table_system_enabled" data-type="boolean">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px;">
|
||||||
<label for="table-injection-enabled-toggle">启用表格注入</label>
|
<label for="table-injection-enabled-toggle">启用表格注入</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="table-injection-enabled" data-setting-key="table_injection_enabled" data-type="boolean">
|
<input type="checkbox" id="table-injection-enabled" data-setting-key="table_injection_enabled" data-type="boolean">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px;">
|
||||||
<label for="context-optimization-enabled-toggle">启用上下文优化 (合并世界书)</label>
|
<label for="context-optimization-enabled-toggle">启用上下文优化 (合并世界书)</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="context-optimization-enabled" data-setting-key="context_optimization_enabled" data-type="boolean">
|
<input type="checkbox" id="context-optimization-enabled" data-setting-key="context_optimization_enabled" data-type="boolean">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px;">
|
||||||
<label>填表模式</label>
|
<label>填表模式</label>
|
||||||
<div class="radio-group">
|
<div class="radio-group">
|
||||||
<input type="radio" id="main-api-mode" name="filling-mode" value="main-api" checked>
|
<input type="radio" id="main-api-mode" name="filling-mode" value="main-api" checked>
|
||||||
<label for="main-api-mode">原始</label>
|
<label for="main-api-mode">原始</label>
|
||||||
<input type="radio" id="secondary-api-mode" name="filling-mode" value="secondary-api">
|
<input type="radio" id="secondary-api-mode" name="filling-mode" value="secondary-api">
|
||||||
<label for="secondary-api-mode">分步</label>
|
<label for="secondary-api-mode">分步</label>
|
||||||
<input type="radio" id="optimized-mode" name="filling-mode" value="optimized">
|
<input type="radio" id="optimized-mode" name="filling-mode" value="optimized">
|
||||||
<label for="optimized-mode">兼容优化</label>
|
<label for="optimized-mode">兼容优化</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分步填表高级控制 - 仅在分步模式下显示 -->
|
<!-- 分步填表高级控制 - 仅在分步模式下显示 -->
|
||||||
<div id="secondary-filler-controls" style="display: none; border-left: 2px solid #4a9eff; padding-left: 10px; margin-bottom: 10px;">
|
<div id="secondary-filler-controls" style="display: none; border-left: 2px solid #4a9eff; padding-left: 10px; margin-bottom: 10px;">
|
||||||
|
|
||||||
<!-- 上下文深度 -->
|
<!-- 上下文深度 -->
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
||||||
<label for="secondary-filler-context">上下文深度</label>
|
<label for="secondary-filler-context">上下文深度</label>
|
||||||
<input type="number" id="secondary-filler-context" min="0" max="20" step="1" value="2" class="text_pole" style="width: 80px; margin-top: 5px;">
|
<input type="number" id="secondary-filler-context" min="0" max="20" step="1" value="2" class="text_pole" style="width: 80px; margin-top: 5px;">
|
||||||
<small class="notes" style="margin-top: 5px; display: block;">填表时参考的历史上下文消息数量。</small>
|
<small class="notes" style="margin-top: 5px; display: block;">填表时参考的历史上下文消息数量。</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 填表批次 -->
|
<!-- 填表批次 -->
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
||||||
<label for="secondary-filler-batch">填表批次 (Batch)</label>
|
<label for="secondary-filler-batch">填表批次 (Batch)</label>
|
||||||
<input type="number" id="secondary-filler-batch" min="0" max="20" step="1" value="0" class="text_pole" style="width: 80px; margin-top: 5px;">
|
<input type="number" id="secondary-filler-batch" min="0" max="20" step="1" value="0" class="text_pole" style="width: 80px; margin-top: 5px;">
|
||||||
<small class="notes" style="margin-top: 5px; display: block;">单次填表处理的消息数量 (0 = 实时单条模式)。</small>
|
<small class="notes" style="margin-top: 5px; display: block;">单次填表处理的消息数量 (0 = 实时单条模式)。</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 保留楼层 -->
|
<!-- 保留楼层 -->
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
||||||
<label for="secondary-filler-buffer">保留楼层 (Buffer)</label>
|
<label for="secondary-filler-buffer">保留楼层 (Buffer)</label>
|
||||||
<input type="number" id="secondary-filler-buffer" min="0" max="10" step="1" value="0" class="text_pole" style="width: 80px; margin-top: 5px;">
|
<input type="number" id="secondary-filler-buffer" min="0" max="10" step="1" value="0" class="text_pole" style="width: 80px; margin-top: 5px;">
|
||||||
<small class="notes" style="margin-top: 5px; display: block;">始终保留不填表的最新消息数量 (缓冲防抖)。</small>
|
<small class="notes" style="margin-top: 5px; display: block;">始终保留不填表的最新消息数量 (缓冲防抖)。</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="table-independent-rules-container" class="control-block-with-switch" style="margin-bottom: 10px; display: none; flex-direction: column; align-items: flex-start; gap: 8px;">
|
<div id="table-independent-rules-container" class="control-block-with-switch" style="margin-bottom: 10px; display: none; flex-direction: column; align-items: flex-start; gap: 8px;">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
|
||||||
<label for="table-independent-rules-enabled">启用独立提取规则</label>
|
<label for="table-independent-rules-enabled">启用独立提取规则</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="table-independent-rules-enabled">
|
<input type="checkbox" id="table-independent-rules-enabled">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button id="table-configure-rules-btn" class="menu_button small_button" style="display: none;"><i class="fas fa-cog"></i> 配置规则</button>
|
<button id="table-configure-rules-btn" class="menu_button small_button" style="display: none;"><i class="fas fa-cog"></i> 配置规则</button>
|
||||||
<small class="notes">启用后,分步填表和批量填表将使用下方配置的专属规则,而非微言录的规则。</small>
|
<small class="notes">启用后,分步填表和批量填表将使用下方配置的专属规则,而非微言录的规则。</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-center-buttons" style="gap: 8px;">
|
<div class="action-center-buttons" style="gap: 8px;">
|
||||||
<button id="amily2-open-relationship-graph-btn" class="menu_button accent small_button interactable"><i class="fas fa-project-diagram"></i> 关系图谱</button>
|
<button id="amily2-open-relationship-graph-btn" class="menu_button accent small_button interactable"><i class="fas fa-project-diagram"></i> 关系图谱</button>
|
||||||
<button id="amily2-export-preset-btn" class="menu_button primary small_button interactable"><i class="fas fa-file-export"></i> 导出预设</button>
|
<button id="amily2-export-preset-btn" class="menu_button primary small_button interactable"><i class="fas fa-file-export"></i> 导出预设</button>
|
||||||
<button id="amily2-export-preset-full-btn" class="menu_button primary small_button interactable"><i class="fas fa-file-archive"></i> 导出备份</button>
|
<button id="amily2-export-preset-full-btn" class="menu_button primary small_button interactable"><i class="fas fa-file-archive"></i> 导出备份</button>
|
||||||
<button id="amily2-import-preset-btn" class="menu_button secondary small_button interactable"><i class="fas fa-file-upload"></i> 导入预设</button>
|
<button id="amily2-import-preset-btn" class="menu_button secondary small_button interactable"><i class="fas fa-file-upload"></i> 导入预设</button>
|
||||||
<button id="amily2-import-global-preset-btn" class="menu_button secondary small_button interactable"><i class="fas fa-globe"></i> 导入全局</button>
|
<button id="amily2-import-global-preset-btn" class="menu_button secondary small_button interactable"><i class="fas fa-globe"></i> 导入全局</button>
|
||||||
<button id="amily2-clear-global-preset-btn" class="menu_button danger small_button interactable"><i class="fas fa-undo"></i> 清除全局</button>
|
<button id="amily2-clear-global-preset-btn" class="menu_button danger small_button interactable"><i class="fas fa-undo"></i> 清除全局</button>
|
||||||
<button id="amily2-clear-all-tables-btn" class="menu_button danger small_button interactable"><i class="fas fa-trash-alt"></i> 清空内容</button>
|
<button id="amily2-clear-all-tables-btn" class="menu_button danger small_button interactable"><i class="fas fa-trash-alt"></i> 清空内容</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="notes" style="margin-top: 8px; margin-bottom: 10px;">说明:可以导出不含剧情的"纯净预设"用于分享,或导出包含剧情的"完整备份"用于存档。</p>
|
<p class="notes" style="margin-top: 8px; margin-bottom: 10px;">说明:可以导出不含剧情的"纯净预设"用于分享,或导出包含剧情的"完整备份"用于存档。</p>
|
||||||
|
|
||||||
<hr class="section-divider" style="margin: 10px 0;">
|
<hr class="section-divider" style="margin: 10px 0;">
|
||||||
|
|
||||||
<!-- 历史记录清理区域 -->
|
<!-- 历史记录清理区域 -->
|
||||||
<fieldset class="settings-group" style="border-style: dashed; padding: 8px; margin-bottom: 10px;">
|
<fieldset class="settings-group" style="border-style: dashed; padding: 8px; margin-bottom: 10px;">
|
||||||
<legend><i class="fas fa-trash-alt"></i> 历史记录清理</legend>
|
<legend><i class="fas fa-trash-alt"></i> 历史记录清理</legend>
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px; flex-direction: column; align-items: flex-start;">
|
||||||
<label for="clear-records-before-floor">清除此楼层前的表格记录</label>
|
<label for="clear-records-before-floor">清除此楼层前的表格记录</label>
|
||||||
<div style="display: flex; gap: 10px; align-items: center; width: 100%; margin-top: 5px;">
|
<div style="display: flex; gap: 10px; align-items: center; width: 100%; margin-top: 5px;">
|
||||||
<input type="number" id="clear-records-before-floor" class="text_pole" style="flex: 1;" placeholder="输入楼层号 (例如: 100)">
|
<input type="number" id="clear-records-before-floor" class="text_pole" style="flex: 1;" placeholder="输入楼层号 (例如: 100)">
|
||||||
<button id="clear-records-btn" class="menu_button danger small_button interactable">
|
<button id="clear-records-btn" class="menu_button danger small_button interactable">
|
||||||
<i class="fas fa-eraser"></i> 一键清除
|
<i class="fas fa-eraser"></i> 一键清除
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="notes" style="margin-top: 5px; display: block;">
|
<small class="notes" style="margin-top: 5px; display: block;">
|
||||||
警告:此操作将永久删除指定楼层之前所有消息中存储的表格快照。这可以显著减小聊天记录文件的大小,但会导致无法回退到这些楼层的表格状态。当前最新的表格状态不会受影响。
|
警告:此操作将永久删除指定楼层之前所有消息中存储的表格快照。这可以显著减小聊天记录文件的大小,但会导致无法回退到这些楼层的表格状态。当前最新的表格状态不会受影响。
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<hr class="section-divider" style="margin: 10px 0;">
|
<hr class="section-divider" style="margin: 10px 0;">
|
||||||
|
|
||||||
<!-- Nccs API 控制区域 -->
|
<!-- Nccs API 控制区域 -->
|
||||||
<fieldset class="settings-group" style="border-style: dashed; padding: 8px; margin-bottom: 10px;">
|
<fieldset class="settings-group" style="border-style: dashed; padding: 8px; margin-bottom: 10px;">
|
||||||
<legend><i class="fas fa-brain"></i> Nccs API 系统</legend>
|
<legend><i class="fas fa-brain"></i> Nccs API 系统</legend>
|
||||||
|
|
||||||
<div class="control-block-with-switch" style="margin-bottom: 10px;">
|
<div class="control-block-with-switch" style="margin-bottom: 10px;">
|
||||||
<label for="nccs-api-enabled">启用 Nccs API</label>
|
<label for="nccs-api-enabled">启用 Nccs API</label>
|
||||||
<label class="toggle-switch">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" id="nccs-api-enabled" data-setting-key="nccsEnabled" data-type="boolean">
|
<input type="checkbox" id="nccs-api-enabled" data-setting-key="nccsEnabled" data-type="boolean">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="nccs-api-config" style="display: none;">
|
<div id="nccs-api-config" style="display: none;">
|
||||||
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
||||||
<label for="nccs-api-mode">API 模式</label>
|
<label for="nccs-api-mode">API 模式</label>
|
||||||
<select id="nccs-api-mode" class="text_pole">
|
<select id="nccs-api-mode" class="text_pole">
|
||||||
<option value="openai_test">全兼容模式</option>
|
<option value="openai_test">全兼容模式</option>
|
||||||
<option value="sillytavern_preset">SillyTavern预设模式</option>
|
<option value="sillytavern_preset">SillyTavern预设模式</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
||||||
<label for="nccs-api-url">API URL</label>
|
<label for="nccs-api-url">API URL</label>
|
||||||
<input type="text" id="nccs-api-url" class="text_pole" placeholder="https://api.openai.com/v1">
|
<input type="text" id="nccs-api-url" class="text_pole" placeholder="https://api.openai.com/v1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
||||||
<label for="nccs-api-key">API Key</label>
|
<label for="nccs-api-key">API Key</label>
|
||||||
<input type="password" id="nccs-api-key" class="text_pole" placeholder="输入您的API密钥">
|
<input type="password" id="nccs-api-key" class="text_pole" placeholder="输入您的API密钥">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
||||||
<label for="nccs-api-model">模型</label>
|
<label for="nccs-api-model">模型</label>
|
||||||
<div style="display: flex; gap: 5px; align-items: center;">
|
<div style="display: flex; gap: 5px; align-items: center;">
|
||||||
<input type="text" id="nccs-api-model" class="text_pole" placeholder="选择或输入模型">
|
<input type="text" id="nccs-api-model" class="text_pole" placeholder="选择或输入模型">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
||||||
<label for="nccs-max-tokens">最大Token数: <span id="nccs-max-tokens-value">2000</span></label>
|
<label for="nccs-max-tokens">最大Token数: <span id="nccs-max-tokens-value">2000</span></label>
|
||||||
<input type="range" id="nccs-max-tokens" min="100" max="100000" step="100" value="2000">
|
<input type="range" id="nccs-max-tokens" min="100" max="100000" step="100" value="2000">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
||||||
<label for="nccs-temperature">Temperature: <span id="nccs-temperature-value">0.7</span></label>
|
<label for="nccs-temperature">Temperature: <span id="nccs-temperature-value">0.7</span></label>
|
||||||
<input type="range" id="nccs-temperature" min="0" max="2" step="0.1" value="0.7">
|
<input type="range" id="nccs-temperature" min="0" max="2" step="0.1" value="0.7">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
||||||
<label for="nccs-sillytavern-preset">SillyTavern 预设</label>
|
<label for="nccs-api-fakestream-enabled">启用流式支持: </label>
|
||||||
<select id="nccs-sillytavern-preset" class="text_pole">
|
<input type="checkbox" id="nccs-api-fakestream-enabled" data-setting-key="nccsFakeStreamEnabled" data-type="boolean">
|
||||||
<option value="">选择预设</option>
|
</div>
|
||||||
</select>
|
|
||||||
</div>
|
<div class="amily2_opt_settings_block" style="margin-bottom: 10px;">
|
||||||
|
<label for="nccs-sillytavern-preset">SillyTavern 预设</label>
|
||||||
<div class="nccs-button-row" style="display: flex; gap: 10px; justify-content: center; margin-top: 15px;">
|
<select id="nccs-sillytavern-preset" class="text_pole">
|
||||||
<button id="nccs-test-connection" class="menu_button primary small_button interactable">
|
<option value="">选择预设</option>
|
||||||
<i class="fas fa-plug"></i> 测试连接
|
</select>
|
||||||
</button>
|
</div>
|
||||||
<button id="nccs-fetch-models" class="menu_button secondary small_button interactable">
|
|
||||||
<i class="fas fa-download"></i> 获取模型
|
<div class="nccs-button-row" style="display: flex; gap: 10px; justify-content: center; margin-top: 15px;">
|
||||||
</button>
|
<button id="nccs-test-connection" class="menu_button primary small_button interactable">
|
||||||
</div>
|
<i class="fas fa-plug"></i> 测试连接
|
||||||
</div>
|
</button>
|
||||||
</fieldset>
|
<button id="nccs-fetch-models" class="menu_button secondary small_button interactable">
|
||||||
|
<i class="fas fa-download"></i> 获取模型
|
||||||
<hr class="section-divider" style="margin: 10px 0;">
|
</button>
|
||||||
|
</div>
|
||||||
<div class="action-center-buttons" id="theme-action-buttons" style="gap: 8px;">
|
</div>
|
||||||
<button id="amily2-import-theme-btn" class="menu_button small_button interactable"><i class="fas fa-palette"></i> 导入主题</button>
|
</fieldset>
|
||||||
<button id="amily2-export-theme-btn" class="menu_button small_button interactable"><i class="fas fa-paint-brush"></i> 导出主题</button>
|
|
||||||
<button id="amily2-reset-theme-btn" class="menu_button small_button interactable"><i class="fas fa-undo"></i> 恢复默认</button>
|
<hr class="section-divider" style="margin: 10px 0;">
|
||||||
</div>
|
|
||||||
<p class="notes" style="margin-top: 8px;">说明:导入下载的主题JSON文件,或将当前的外观导出分享。</p>
|
<div class="action-center-buttons" id="theme-action-buttons" style="gap: 8px;">
|
||||||
</div>
|
<button id="amily2-import-theme-btn" class="menu_button small_button interactable"><i class="fas fa-palette"></i> 导入主题</button>
|
||||||
|
<button id="amily2-export-theme-btn" class="menu_button small_button interactable"><i class="fas fa-paint-brush"></i> 导出主题</button>
|
||||||
|
<button id="amily2-reset-theme-btn" class="menu_button small_button interactable"><i class="fas fa-undo"></i> 恢复默认</button>
|
||||||
<div id="sinan-ai-template-tab" class="sinan-tab-pane">
|
</div>
|
||||||
<fieldset class="settings-group" style="border-style: dashed; padding: 8px; margin-bottom: 10px;">
|
<p class="notes" style="margin-top: 8px;">说明:导入下载的主题JSON文件,或将当前的外观导出分享。</p>
|
||||||
<legend><i class="fas fa-scroll"></i> 规则提示词</legend>
|
</div>
|
||||||
<div class="amily2_settings_block prompt-editor-area">
|
|
||||||
<textarea id="ai-rule-template-editor" class="text_pole" rows="2"></textarea>
|
|
||||||
<div class="editor-buttons-panel">
|
<div id="sinan-ai-template-tab" class="sinan-tab-pane">
|
||||||
<button id="ai-rule-template-save-btn" class="menu_button accent small_button interactable"><i class="fas fa-save"></i> 保存</button>
|
<fieldset class="settings-group" style="border-style: dashed; padding: 8px; margin-bottom: 10px;">
|
||||||
<button id="ai-rule-template-restore-btn" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 默认</button>
|
<legend><i class="fas fa-scroll"></i> 规则提示词</legend>
|
||||||
</div>
|
<div class="amily2_settings_block prompt-editor-area">
|
||||||
</div>
|
<textarea id="ai-rule-template-editor" class="text_pole" rows="2"></textarea>
|
||||||
</fieldset>
|
<div class="editor-buttons-panel">
|
||||||
|
<button id="ai-rule-template-save-btn" class="menu_button accent small_button interactable"><i class="fas fa-save"></i> 保存</button>
|
||||||
<fieldset class="settings-group" style="border-style: dashed; padding: 8px;">
|
<button id="ai-rule-template-restore-btn" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 默认</button>
|
||||||
<legend><i class="fas fa-tasks"></i> 流程提示词</legend>
|
</div>
|
||||||
<div class="amily2_settings_block prompt-editor-area">
|
</div>
|
||||||
<textarea id="ai-flow-template-editor" class="text_pole" rows="2"></textarea>
|
</fieldset>
|
||||||
<div class="editor-buttons-panel">
|
|
||||||
<button id="ai-flow-template-save-btn" class="menu_button accent small_button interactable"><i class="fas fa-save"></i> 保存</button>
|
<fieldset class="settings-group" style="border-style: dashed; padding: 8px;">
|
||||||
<button id="ai-flow-template-restore-btn" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 默认</button>
|
<legend><i class="fas fa-tasks"></i> 流程提示词</legend>
|
||||||
</div>
|
<div class="amily2_settings_block prompt-editor-area">
|
||||||
</div>
|
<textarea id="ai-flow-template-editor" class="text_pole" rows="2"></textarea>
|
||||||
</fieldset>
|
<div class="editor-buttons-panel">
|
||||||
|
<button id="ai-flow-template-save-btn" class="menu_button accent small_button interactable"><i class="fas fa-save"></i> 保存</button>
|
||||||
<hr class="section-divider" style="margin: 10px 0;">
|
<button id="ai-flow-template-restore-btn" class="menu_button secondary small_button interactable"><i class="fas fa-undo"></i> 默认</button>
|
||||||
|
</div>
|
||||||
<div id="table-filling-controls" style="display: flex; flex-direction: column; gap: 10px; align-items: center;">
|
</div>
|
||||||
<!-- 楼层选择区域 -->
|
</fieldset>
|
||||||
<div style="display: flex; gap: 10px; align-items: center; justify-content: center;">
|
|
||||||
<label for="floor-start-input" style="font-size: 12px; white-space: nowrap;">起始楼层:</label>
|
<hr class="section-divider" style="margin: 10px 0;">
|
||||||
<input type="number" id="floor-start-input" class="text_pole" style="width: 80px;" placeholder="1" min="1">
|
|
||||||
<label for="floor-end-input" style="font-size: 12px; white-space: nowrap;">结束楼层:</label>
|
<div id="table-filling-controls" style="display: flex; flex-direction: column; gap: 10px; align-items: center;">
|
||||||
<input type="number" id="floor-end-input" class="text_pole" style="width: 80px;" placeholder="10" min="1">
|
<!-- 楼层选择区域 -->
|
||||||
</div>
|
<div style="display: flex; gap: 10px; align-items: center; justify-content: center;">
|
||||||
|
<label for="floor-start-input" style="font-size: 12px; white-space: nowrap;">起始楼层:</label>
|
||||||
<!-- 填表按钮区域 -->
|
<input type="number" id="floor-start-input" class="text_pole" style="width: 80px;" placeholder="1" min="1">
|
||||||
<div style="display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap;">
|
<label for="floor-end-input" style="font-size: 12px; white-space: nowrap;">结束楼层:</label>
|
||||||
<button id="fill-table-now-btn" class="menu_button small_button">立即填表</button>
|
<input type="number" id="floor-end-input" class="text_pole" style="width: 80px;" placeholder="10" min="1">
|
||||||
<button id="fill-selected-floors-btn" class="menu_button accent small_button">选定楼层填表</button>
|
</div>
|
||||||
<button id="fill-current-floor-btn" class="menu_button secondary small_button">填当前楼层</button>
|
|
||||||
<button id="rollback-and-refill-btn" class="menu_button secondary small_button">回退重填</button>
|
<!-- 填表按钮区域 -->
|
||||||
<button id="reorganize-table-btn" class="menu_button warning small_button">重新整理</button>
|
<div style="display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap;">
|
||||||
</div>
|
<button id="fill-table-now-btn" class="menu_button small_button">立即填表</button>
|
||||||
</div>
|
<button id="fill-selected-floors-btn" class="menu_button accent small_button">选定楼层填表</button>
|
||||||
</div>
|
<button id="fill-current-floor-btn" class="menu_button secondary small_button">填当前楼层</button>
|
||||||
</div>
|
<button id="rollback-and-refill-btn" class="menu_button secondary small_button">回退重填</button>
|
||||||
</fieldset>
|
<button id="reorganize-table-btn" class="menu_button warning small_button">重新整理</button>
|
||||||
|
</div>
|
||||||
<div id="table-log-display" class="hly-log-display" style="margin-top: 10px; margin-bottom: 10px;">
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
|
|
||||||
<div id="all-tables-container" class="hly-scroll">
|
<div id="table-log-display" class="hly-log-display" style="margin-top: 10px; margin-bottom: 10px;">
|
||||||
|
|
||||||
<div id="add-table-placeholder" title="敕令新表">
|
</div>
|
||||||
<i class="fas fa-plus"></i>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
<div id="all-tables-container" class="hly-scroll">
|
||||||
|
|
||||||
</body>
|
<div id="add-table-placeholder" title="敕令新表">
|
||||||
</html>
|
<i class="fas fa-plus"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,89 +12,80 @@ try {
|
|||||||
console.warn("[Amily2号-Nccs外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
console.warn("[Amily2号-Nccs外交部] 未能召唤“皇家信使”,部分高级功能(如Claw代理)将受限。请考虑更新SillyTavern版本。", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeApiResponse(responseData) {
|
let nccsCtx = null;
|
||||||
let data = responseData;
|
// 尝试连接总线
|
||||||
if (typeof data === 'string') {
|
if (window.Amily2Bus) {
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(data);
|
// 注册 'NccsApi' 身份,获取专属上下文
|
||||||
} catch (e) {
|
nccsCtx = window.Amily2Bus.register('NccsApi');
|
||||||
console.error(`[${extensionName}] Nccs API响应JSON解析失败:`, e);
|
|
||||||
return { error: { message: 'Invalid JSON response' } };
|
// 【联动】暴露 Nccs 的核心调用能力,允许其他插件通过 query('NccsApi') 借用此通道
|
||||||
}
|
nccsCtx.expose({
|
||||||
|
call: callNccsAI,
|
||||||
|
getSettings: getNccsApiSettings
|
||||||
|
});
|
||||||
|
|
||||||
|
nccsCtx.log('Init', 'info', 'NccsApi 已连接至 Amily2Bus,网络通道准备就绪。');
|
||||||
|
} catch (e) {
|
||||||
|
// 如果是热重载导致重复注册,尝试降级获取(注意:严格锁模式下无法获取旧Context,这里仅做日志提示)
|
||||||
|
// 在生产环境中,页面刷新会重置 Bus,不会有问题。
|
||||||
|
console.warn('[Amily2-Nccs] Bus 注册警告 (可能是热重载):', e);
|
||||||
}
|
}
|
||||||
if (data && typeof data.data === 'object' && data.data !== null && !Array.isArray(data.data)) {
|
} else {
|
||||||
if (Object.hasOwn(data.data, 'data')) {
|
console.error('[Amily2-Nccs] 严重警告: Amily2Bus 未找到,NccsApi 网络层将无法工作!');
|
||||||
data = data.data;
|
toastr.error("核心组件 Amily2Bus 丢失,请检查安装。", "Nccs-System");
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNccsApiSettings() {
|
export function getNccsApiSettings() {
|
||||||
return {
|
return {
|
||||||
|
nccsEnabled: extension_settings[extensionName]?.nccsEnabled || false,
|
||||||
apiMode: extension_settings[extensionName]?.nccsApiMode || 'openai_test',
|
apiMode: extension_settings[extensionName]?.nccsApiMode || 'openai_test',
|
||||||
apiUrl: extension_settings[extensionName]?.nccsApiUrl?.trim() || '',
|
apiUrl: extension_settings[extensionName]?.nccsApiUrl?.trim() || '',
|
||||||
apiKey: extension_settings[extensionName]?.nccsApiKey?.trim() || '',
|
apiKey: extension_settings[extensionName]?.nccsApiKey?.trim() || '',
|
||||||
model: extension_settings[extensionName]?.nccsModel || '',
|
model: extension_settings[extensionName]?.nccsModel || '',
|
||||||
maxTokens: extension_settings[extensionName]?.nccsMaxTokens || 4000,
|
maxTokens: extension_settings[extensionName]?.nccsMaxTokens || 4000,
|
||||||
temperature: extension_settings[extensionName]?.nccsTemperature || 0.7,
|
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 = {}) {
|
export async function callNccsAI(messages, options = {}) {
|
||||||
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
if (window.AMILY2_SYSTEM_PARALYZED === true) {
|
||||||
console.error("[Amily2-Nccs制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
console.error("[Amily2-Nccs制裁] 系统完整性已受损,所有外交活动被无限期中止。");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiSettings = getNccsApiSettings();
|
const settings = getNccsApiSettings();
|
||||||
|
|
||||||
const finalOptions = {
|
const finalOptions = {
|
||||||
maxTokens: apiSettings.maxTokens,
|
...settings,
|
||||||
temperature: apiSettings.temperature,
|
|
||||||
model: apiSettings.model,
|
|
||||||
apiUrl: apiSettings.apiUrl,
|
|
||||||
apiKey: apiSettings.apiKey,
|
|
||||||
apiMode: apiSettings.apiMode,
|
|
||||||
tavernProfile: apiSettings.tavernProfile,
|
|
||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保 stream 标志位存在
|
||||||
|
finalOptions.stream = finalOptions.useFakeStream ?? false;
|
||||||
|
|
||||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||||
console.warn("[Amily2-Nccs外交部] API配置不完整,无法调用AI");
|
console.warn("[Amily2-Nccs外交部] API配置不完整,无法调用AI");
|
||||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Nccs-外交部");
|
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Nccs-外交部");
|
||||||
return null;
|
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 {
|
try {
|
||||||
let responseContent;
|
let responseContent;
|
||||||
|
|
||||||
switch (finalOptions.apiMode) {
|
switch (finalOptions.apiMode) {
|
||||||
case 'openai_test':
|
case 'openai_test':
|
||||||
responseContent = await callNccsOpenAITest(messages, finalOptions);
|
responseContent = await callNccsOpenAITest(messages, finalOptions);
|
||||||
@@ -103,53 +94,86 @@ export async function callNccsAI(messages, options = {}) {
|
|||||||
responseContent = await callNccsSillyTavernPreset(messages, finalOptions);
|
responseContent = await callNccsSillyTavernPreset(messages, finalOptions);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.error(`[Amily2-Nccs外交部] 未支持的API模式: ${finalOptions.apiMode}`);
|
console.error(`未支持的 API 模式: ${finalOptions.apiMode}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!responseContent) {
|
|
||||||
console.warn('[Amily2-Nccs外交部] 未能获取AI响应内容');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.groupCollapsed("[Amily2号-Nccs AI回复]");
|
|
||||||
console.log(responseContent);
|
|
||||||
console.groupEnd();
|
|
||||||
|
|
||||||
return responseContent;
|
return responseContent;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Amily2-Nccs外交部] API调用发生错误:`, error);
|
console.error(`[Amily2-Nccs] API 调用失败:`, error);
|
||||||
|
toastr.error(`调用失败: ${error.message}`, "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调用失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
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) {
|
async function callNccsOpenAITest(messages, options) {
|
||||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
chat_completion_source: 'openai',
|
chat_completion_source: 'openai',
|
||||||
messages: messages,
|
messages: messages,
|
||||||
model: options.model,
|
model: options.model,
|
||||||
reverse_proxy: options.apiUrl,
|
reverse_proxy: options.apiUrl,
|
||||||
proxy_password: options.apiKey,
|
proxy_password: options.apiKey,
|
||||||
stream: false,
|
stream: !!options.stream,
|
||||||
max_tokens: options.maxTokens || 30000,
|
max_tokens: options.maxTokens || 4000,
|
||||||
temperature: options.temperature || 1,
|
temperature: options.temperature || 1,
|
||||||
top_p: options.top_p || 1,
|
top_p: options.top_p || 1,
|
||||||
};
|
};
|
||||||
@@ -157,109 +181,65 @@ async function callNccsOpenAITest(messages, options) {
|
|||||||
if (!isGoogleApi) {
|
if (!isGoogleApi) {
|
||||||
Object.assign(body, {
|
Object.assign(body, {
|
||||||
custom_prompt_post_processing: 'strict',
|
custom_prompt_post_processing: 'strict',
|
||||||
enable_web_search: false,
|
|
||||||
frequency_penalty: 0,
|
|
||||||
group_names: [],
|
|
||||||
include_reasoning: false,
|
|
||||||
presence_penalty: 0.12,
|
presence_penalty: 0.12,
|
||||||
reasoning_effort: 'medium',
|
|
||||||
request_images: false,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/backends/chat-completions/generate', {
|
const fetchOpts = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
};
|
||||||
|
|
||||||
if (!response.ok) {
|
if (options.stream) {
|
||||||
const errorText = await response.text();
|
return await fetchFakeStream('/api/backends/chat-completions/generate', fetchOpts);
|
||||||
throw new Error(`Nccs全兼容API请求失败: ${response.status} - ${errorText}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseData = await response.json();
|
const response = await fetch('/api/backends/chat-completions/generate', fetchOpts);
|
||||||
return responseData?.choices?.[0]?.message?.content;
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||||
|
return normalizeApiResponse(await response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function callNccsSillyTavernPreset(messages, options) {
|
async function callNccsSillyTavernPreset(messages, options) {
|
||||||
console.log('[Amily2号-NccsST预设] 使用SillyTavern预设调用');
|
|
||||||
|
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
if (!context) {
|
if (!context) throw new Error('SillyTavern context unavailable');
|
||||||
throw new Error('无法获取SillyTavern上下文');
|
|
||||||
}
|
|
||||||
|
|
||||||
const profileId = options.tavernProfile;
|
const profileId = options.tavernProfile;
|
||||||
if (!profileId) {
|
if (!profileId) throw new Error('No profile ID configured');
|
||||||
throw new Error('未配置SillyTavern预设ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
let originalProfile = '';
|
const originalProfile = await amilyHelper.triggerSlash('/profile');
|
||||||
let responsePromise;
|
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
||||||
|
|
||||||
|
if (!targetProfile) throw new Error(`Profile ${profileId} not found`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
originalProfile = await amilyHelper.triggerSlash('/profile');
|
if (originalProfile !== targetProfile.name) {
|
||||||
console.log(`[Amily2号-NccsST预设] 当前配置文件: ${originalProfile}`);
|
await amilyHelper.triggerSlash(`/profile await=true "${targetProfile.name.replace(/"/g, '\\"')}"`);
|
||||||
|
|
||||||
const targetProfile = context.extensionSettings?.connectionManager?.profiles?.find(p => p.id === profileId);
|
|
||||||
if (!targetProfile) {
|
|
||||||
throw new Error(`未找到配置文件ID: ${profileId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetProfileName = targetProfile.name;
|
if (!context.ConnectionManagerRequestService) throw new Error('ConnectionManagerRequestService unavailable');
|
||||||
console.log(`[Amily2号-NccsST预设] 目标配置文件: ${targetProfileName}`);
|
|
||||||
|
|
||||||
const currentProfile = await amilyHelper.triggerSlash('/profile');
|
const result = await context.ConnectionManagerRequestService.sendRequest(
|
||||||
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(
|
|
||||||
targetProfile.id,
|
targetProfile.id,
|
||||||
messages,
|
messages,
|
||||||
options.maxTokens || 4000
|
options.maxTokens || 4000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return normalizeApiResponse(result);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
// Restore profile
|
||||||
const currentProfileAfterCall = await amilyHelper.triggerSlash('/profile');
|
const current = await amilyHelper.triggerSlash('/profile');
|
||||||
if (originalProfile && originalProfile !== currentProfileAfterCall) {
|
if (originalProfile && originalProfile !== current) {
|
||||||
console.log(`[Amily2号-NccsST预设] 恢复原始配置文件: ${currentProfileAfterCall} -> ${originalProfile}`);
|
await amilyHelper.triggerSlash(`/profile await=true "${originalProfile.replace(/"/g, '\\"')}"`);
|
||||||
const escapedOriginalProfile = originalProfile.replace(/"/g, '\\"');
|
|
||||||
await amilyHelper.triggerSlash(`/profile await=true "${escapedOriginalProfile}"`);
|
|
||||||
}
|
|
||||||
} catch (restoreError) {
|
|
||||||
console.error('[Amily2号-NccsST预设] 恢复配置文件失败:', restoreError);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
export async function fetchNccsModels() {
|
||||||
console.log('[Amily2号-Nccs外交部] 开始获取模型列表');
|
console.log('[Amily2号-Nccs外交部] 开始获取模型列表');
|
||||||
|
|
||||||
const apiSettings = getNccsApiSettings();
|
const apiSettings = getNccsApiSettings();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||||
// SillyTavern预设模式:获取当前预设的模型
|
// SillyTavern预设模式:获取当前预设的模型
|
||||||
@@ -267,29 +247,28 @@ export async function fetchNccsModels() {
|
|||||||
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
if (!context?.extensionSettings?.connectionManager?.profiles) {
|
||||||
throw new Error('无法获取SillyTavern配置文件列表');
|
throw new Error('无法获取SillyTavern配置文件列表');
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
const targetProfile = context.extensionSettings.connectionManager.profiles.find(p => p.id === apiSettings.tavernProfile);
|
||||||
if (!targetProfile) {
|
if (!targetProfile) {
|
||||||
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
throw new Error(`未找到配置文件ID: ${apiSettings.tavernProfile}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const models = [];
|
const models = [];
|
||||||
if (targetProfile.openai_model) {
|
if (targetProfile.openai_model) {
|
||||||
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
models.push({ id: targetProfile.openai_model, name: targetProfile.openai_model });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (models.length === 0) {
|
if (models.length === 0) {
|
||||||
throw new Error('当前预设未配置模型');
|
throw new Error('当前预设未配置模型');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Amily2号-Nccs外交部] SillyTavern预设模式获取到模型:', models);
|
console.log('[Amily2号-Nccs外交部] SillyTavern预设模式获取到模型:', models);
|
||||||
return models;
|
return models;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
if (!apiSettings.apiUrl || !apiSettings.apiKey) {
|
||||||
throw new Error('API URL或Key未配置');
|
throw new Error('API URL或Key未配置');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/backends/chat-completions/status', {
|
const response = await fetch('/api/backends/chat-completions/status', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -340,7 +319,7 @@ export async function fetchNccsModels() {
|
|||||||
|
|
||||||
export async function testNccsApiConnection() {
|
export async function testNccsApiConnection() {
|
||||||
console.log('[Amily2号-Nccs外交部] 开始API连接测试');
|
console.log('[Amily2号-Nccs外交部] 开始API连接测试');
|
||||||
|
|
||||||
const apiSettings = getNccsApiSettings();
|
const apiSettings = getNccsApiSettings();
|
||||||
|
|
||||||
if (apiSettings.apiMode === 'sillytavern_preset') {
|
if (apiSettings.apiMode === 'sillytavern_preset') {
|
||||||
@@ -357,7 +336,7 @@ export async function testNccsApiConnection() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
toastr.info('正在发送测试消息"你好!"...', 'Nccs API连接测试');
|
toastr.info('正在发送测试消息"你好!"...', 'Nccs API连接测试');
|
||||||
|
|
||||||
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
const userName = window.SillyTavern.getContext?.()?.name1 || '用户';
|
||||||
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
const systemPrompt = `接下来用户会使用测试按钮测试api连接是否正常,请回复ta连接是正常的,称呼用户为:“${userName}大人”,并复述群号1060183271,告知ta,本扩展开发者:“诗与酒.”正静待ta的入驻。`;
|
||||||
|
|
||||||
@@ -365,9 +344,9 @@ export async function testNccsApiConnection() {
|
|||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: '你好!' }
|
{ role: 'user', content: '你好!' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const response = await callNccsAI(testMessages);
|
const response = await callNccsAI(testMessages);
|
||||||
|
|
||||||
if (response && response.trim()) {
|
if (response && response.trim()) {
|
||||||
console.log('[Amily2号-Nccs外交部] 测试消息响应:', response);
|
console.log('[Amily2号-Nccs外交部] 测试消息响应:', response);
|
||||||
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
const formattedResponse = response.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
|
||||||
@@ -376,10 +355,11 @@ export async function testNccsApiConnection() {
|
|||||||
} else {
|
} else {
|
||||||
throw new Error('API未返回有效响应');
|
throw new Error('API未返回有效响应');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Amily2号-Nccs外交部] 连接测试失败:', error);
|
console.error('[Amily2号-Nccs外交部] 连接测试失败:', error);
|
||||||
toastr.error(`连接测试失败: ${error.message}`, 'Nccs API连接测试失败');
|
toastr.error(`连接测试失败: ${error.message}`, 'Nccs API连接测试失败');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export function getNgmsApiSettings() {
|
|||||||
model: extension_settings[extensionName]?.ngmsModel || '',
|
model: extension_settings[extensionName]?.ngmsModel || '',
|
||||||
maxTokens: extension_settings[extensionName]?.ngmsMaxTokens || 4000,
|
maxTokens: extension_settings[extensionName]?.ngmsMaxTokens || 4000,
|
||||||
temperature: extension_settings[extensionName]?.ngmsTemperature || 0.7,
|
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
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保 stream 标志位存在
|
||||||
|
finalOptions.stream = finalOptions.useFakeStream ?? apiSettings.useFakeStream ?? false;
|
||||||
|
|
||||||
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
if (finalOptions.apiMode !== 'sillytavern_preset') {
|
||||||
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
if (!finalOptions.apiUrl || !finalOptions.model || !finalOptions.apiKey) {
|
||||||
console.warn("[Amily2-Ngms外交部] API配置不完整,无法调用AI");
|
console.warn("[Amily2-Ngms外交部] API配置不完整,无法调用AI");
|
||||||
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Ngms-外交部");
|
toastr.error("API配置不完整,请检查URL、Key和模型配置。", "Ngms-外交部");
|
||||||
return null;
|
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()}`);
|
console.groupCollapsed(`[Amily2号-Ngms统一API调用] ${new Date().toLocaleTimeString()}`);
|
||||||
@@ -87,6 +98,7 @@ export async function callNgmsAI(messages, options = {}) {
|
|||||||
model: finalOptions.model,
|
model: finalOptions.model,
|
||||||
maxTokens: finalOptions.maxTokens,
|
maxTokens: finalOptions.maxTokens,
|
||||||
temperature: finalOptions.temperature,
|
temperature: finalOptions.temperature,
|
||||||
|
stream: finalOptions.stream,
|
||||||
messagesCount: messages.length
|
messagesCount: messages.length
|
||||||
});
|
});
|
||||||
console.log("【消息内容】:", messages);
|
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) {
|
async function callNgmsOpenAITest(messages, options) {
|
||||||
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
const isGoogleApi = options.apiUrl.includes('googleapis.com');
|
||||||
|
|
||||||
@@ -148,7 +208,7 @@ async function callNgmsOpenAITest(messages, options) {
|
|||||||
model: options.model,
|
model: options.model,
|
||||||
reverse_proxy: options.apiUrl,
|
reverse_proxy: options.apiUrl,
|
||||||
proxy_password: options.apiKey,
|
proxy_password: options.apiKey,
|
||||||
stream: false,
|
stream: !!options.stream,
|
||||||
max_tokens: options.maxTokens || 30000,
|
max_tokens: options.maxTokens || 30000,
|
||||||
temperature: options.temperature || 1,
|
temperature: options.temperature || 1,
|
||||||
top_p: options.top_p || 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',
|
method: 'POST',
|
||||||
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
headers: { ...getRequestHeaders(), 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body)
|
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) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
|
|||||||
@@ -155,4 +155,8 @@ export const tableSystemDefaultSettings = {
|
|||||||
table_independent_rules_enabled: false,
|
table_independent_rules_enabled: false,
|
||||||
table_tags_to_extract: '',
|
table_tags_to_extract: '',
|
||||||
table_exclusion_rules: [],
|
table_exclusion_rules: [],
|
||||||
|
|
||||||
|
// Nccs API 设置
|
||||||
|
nccsEnabled: false,
|
||||||
|
nccsFakeStreamEnabled: false,
|
||||||
};
|
};
|
||||||
|
|||||||
41
imports.js
Normal file
41
imports.js
Normal 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';
|
||||||
@@ -1,58 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "Amily2号聊天优化助手",
|
"name": "Amily2号聊天优化助手",
|
||||||
"display_name": "Amily2号助手",
|
"display_name": "Amily2号助手",
|
||||||
"version": "1.8.3",
|
"version": "1.8.4-a",
|
||||||
"author": "Wx-2025",
|
"author": "Wx-2025",
|
||||||
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
"description": "一个拥有独立UI的智能引擎,正文优化、自动总结、记忆表格、rag向量、隐藏楼层、剧情推进等多功能整合。",
|
||||||
"minSillyTavernVersion": "1.10.0",
|
"minSillyTavernVersion": "1.10.0",
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"homePage": "https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git",
|
"homePage": "https://github.com/Wx-2025/ST-Amily2-Chat-Optimisation.git",
|
||||||
"loading_order": 9999,
|
"loading_order": 9999,
|
||||||
"js": "index.js",
|
"js": "index.js",
|
||||||
"styles": ["style.css"]
|
"styles": ["style.css"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ async function initializePanel(contentPanel, errorContainer) {
|
|||||||
const mainContainer = contentPanel.find('#amily2_chat_optimiser');
|
const mainContainer = contentPanel.find('#amily2_chat_optimiser');
|
||||||
|
|
||||||
if (mainContainer.length) {
|
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>`;
|
const additionalPanelHtml = `<div id="amily2_additional_features_panel" style="display: none;">${additionalFeaturesContent}</div>`;
|
||||||
mainContainer.append(additionalPanelHtml);
|
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>`;
|
const textOptimizationPanelHtml = `<div id="amily2_text_optimization_panel" style="display: none;">${textOptimizationContent}</div>`;
|
||||||
mainContainer.append(textOptimizationPanelHtml);
|
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>`;
|
const hanlinyuanPanelHtml = `<div id="amily2_hanlinyuan_panel" style="display: none;">${hanlinyuanContent}</div>`;
|
||||||
mainContainer.append(hanlinyuanPanelHtml);
|
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>`;
|
const memorisationFormsPanelHtml = `<div id="amily2_memorisation_forms_panel" style="display: none;">${memorisationFormsContent}</div>`;
|
||||||
mainContainer.append(memorisationFormsPanelHtml);
|
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>`;
|
const worldEditorPanelHtml = `<div id="amily2_world_editor_panel" style="display: none;">${worldEditorContent}</div>`;
|
||||||
mainContainer.append(worldEditorPanelHtml);
|
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>`;
|
const glossaryPanelHtml = `<div id="amily2_glossary_panel" style="display: none;">${glossaryContent}</div>`;
|
||||||
mainContainer.append(glossaryPanelHtml);
|
mainContainer.append(glossaryPanelHtml);
|
||||||
|
|
||||||
|
|||||||
@@ -408,6 +408,7 @@ function bindNgmsApiEvents() {
|
|||||||
|
|
||||||
// Ngms API 开关控制
|
// Ngms API 开关控制
|
||||||
const ngmsToggle = document.getElementById('amily2_ngms_enabled');
|
const ngmsToggle = document.getElementById('amily2_ngms_enabled');
|
||||||
|
const ngmsFakeStreamToggle = document.getElementById('amily2_ngms_fakestream_enabled');
|
||||||
const ngmsContent = document.getElementById('amily2_ngms_content');
|
const ngmsContent = document.getElementById('amily2_ngms_content');
|
||||||
|
|
||||||
if (ngmsToggle && ngmsContent) {
|
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模式切换
|
// API模式切换
|
||||||
const apiModeSelect = document.getElementById('amily2_ngms_api_mode');
|
const apiModeSelect = document.getElementById('amily2_ngms_api_mode');
|
||||||
const compatibleConfig = document.getElementById('amily2_ngms_compatible_config');
|
const compatibleConfig = document.getElementById('amily2_ngms_compatible_config');
|
||||||
|
|||||||
@@ -1980,6 +1980,7 @@ function bindNccsApiEvents() {
|
|||||||
const settings = extension_settings[extensionName];
|
const settings = extension_settings[extensionName];
|
||||||
|
|
||||||
if (settings.nccsEnabled === undefined) settings.nccsEnabled = false;
|
if (settings.nccsEnabled === undefined) settings.nccsEnabled = false;
|
||||||
|
if (settings.nccsFakeStreamEnabled === undefined) settings.nccsFakeStreamEnabled = false;
|
||||||
if (settings.nccsApiMode === undefined) settings.nccsApiMode = 'openai_test';
|
if (settings.nccsApiMode === undefined) settings.nccsApiMode = 'openai_test';
|
||||||
if (settings.nccsApiUrl === undefined) settings.nccsApiUrl = 'https://api.openai.com/v1';
|
if (settings.nccsApiUrl === undefined) settings.nccsApiUrl = 'https://api.openai.com/v1';
|
||||||
if (settings.nccsApiKey === undefined) settings.nccsApiKey = '';
|
if (settings.nccsApiKey === undefined) settings.nccsApiKey = '';
|
||||||
@@ -1989,6 +1990,7 @@ function bindNccsApiEvents() {
|
|||||||
if (settings.nccsTavernProfile === undefined) settings.nccsTavernProfile = '';
|
if (settings.nccsTavernProfile === undefined) settings.nccsTavernProfile = '';
|
||||||
|
|
||||||
const enabledToggle = document.getElementById('nccs-api-enabled');
|
const enabledToggle = document.getElementById('nccs-api-enabled');
|
||||||
|
const enabledFakeStreamToggle = document.getElementById('nccs-api-fakestream-enabled');
|
||||||
const configDiv = document.getElementById('nccs-api-config');
|
const configDiv = document.getElementById('nccs-api-config');
|
||||||
const modeSelect = document.getElementById('nccs-api-mode');
|
const modeSelect = document.getElementById('nccs-api-mode');
|
||||||
const urlInput = document.getElementById('nccs-api-url');
|
const urlInput = document.getElementById('nccs-api-url');
|
||||||
@@ -2005,6 +2007,7 @@ function bindNccsApiEvents() {
|
|||||||
if (!enabledToggle || !configDiv) return;
|
if (!enabledToggle || !configDiv) return;
|
||||||
|
|
||||||
enabledToggle.checked = settings.nccsEnabled;
|
enabledToggle.checked = settings.nccsEnabled;
|
||||||
|
enabledFakeStreamToggle.checked = settings.nccsFakeStreamEnabled;
|
||||||
if (modeSelect) modeSelect.value = settings.nccsApiMode;
|
if (modeSelect) modeSelect.value = settings.nccsApiMode;
|
||||||
if (urlInput) urlInput.value = settings.nccsApiUrl;
|
if (urlInput) urlInput.value = settings.nccsApiUrl;
|
||||||
if (keyInput) keyInput.value = settings.nccsApiKey;
|
if (keyInput) keyInput.value = settings.nccsApiKey;
|
||||||
@@ -2065,6 +2068,12 @@ function bindNccsApiEvents() {
|
|||||||
log(`Nccs API ${enabledToggle.checked ? '已启用' : '已禁用'}`, 'info');
|
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) {
|
if (modeSelect) {
|
||||||
modeSelect.addEventListener('change', () => {
|
modeSelect.addEventListener('change', () => {
|
||||||
settings.nccsApiMode = modeSelect.value;
|
settings.nccsApiMode = modeSelect.value;
|
||||||
|
|||||||
Reference in New Issue
Block a user