mirror of
https://github.com/SilenceLurker/ST-Amily2-Chat-Optimisation.git
synced 2026-06-06 01:55:49 +00:00
添加 ModelCaller 和 Options 类,重构 Logger,增强 Amily2Bus 架构,更新 README 和 TODO 文件
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
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 = () => {
|
||||
@@ -21,34 +23,87 @@ const getSafeConsole = () => {
|
||||
class Amily2Bus {
|
||||
constructor() {
|
||||
this.safeConsole = getSafeConsole();
|
||||
/** @type {Logger|null} */
|
||||
this.Logger = new Logger();
|
||||
/** @type {FilePipe|null} */
|
||||
this.FilePipe = new FilePipe();
|
||||
|
||||
// 已注册插件列表,防止重复注册
|
||||
this.registry = new Set();
|
||||
|
||||
this.safeConsole.log('[Amily2Bus] Core container initialized with secure registry.');
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统保留的 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} [origin='Bus' 来源模块,默认为 'Bus'
|
||||
* @param {string} [plugin='Global'] 来源插件/命名空间,调试时可指定如 'Console'
|
||||
*/
|
||||
log(type, message, origin = 'Bus', plugin = 'Global') {
|
||||
this.safeConsole.error('[Amily2Bus DEBUG] log called (via SafeConsole):', { type, loggerExists: !!this.Logger });
|
||||
if (this.Logger) {
|
||||
this.Logger.process(plugin, origin, type, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册插件并获取专属上下文
|
||||
* 注册插件并获取专属上下文 (严格锁机制)
|
||||
* @param {string} pluginName 插件名称
|
||||
* @returns {Object} 包含该插件专属 API 的上下文对象
|
||||
*/
|
||||
@@ -58,23 +113,17 @@ class Amily2Bus {
|
||||
}
|
||||
|
||||
if (this.registry.has(pluginName)) {
|
||||
console.warn(`[Amily2Bus] Plugin '${pluginName}' is already registered.`);
|
||||
} else {
|
||||
this.registry.add(pluginName);
|
||||
console.log(`[Amily2Bus] Plugin registered: ${pluginName}`);
|
||||
const errorMsg = `[Amily2Bus] Security Error: Plugin '${pluginName}' is already registered and locked.`;
|
||||
this.safeConsole.error(errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
// 返回该插件专属的 API 上下文 (Capability Token)
|
||||
return {
|
||||
// 绑定了身份的日志接口
|
||||
log: (origin, type, message) => {
|
||||
if (this.Logger) {
|
||||
// 自动填充 plugin 参数
|
||||
this.Logger.log(pluginName, origin, type, message);
|
||||
}
|
||||
},
|
||||
|
||||
// 绑定了身份的文件接口
|
||||
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;
|
||||
@@ -82,18 +131,49 @@ class Amily2Bus {
|
||||
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 instanceof Amily2Bus)) {
|
||||
// 挂载全局单例
|
||||
if (!window.Amily2Bus) {
|
||||
window.Amily2Bus = new Amily2Bus();
|
||||
}
|
||||
|
||||
export function initializeAmilyBus() {
|
||||
if (!window.Amily2Bus || !(window.Amily2Bus instanceof Amily2Bus)) {
|
||||
if (!(window.Amily2Bus instanceof Amily2Bus)) {
|
||||
window.Amily2Bus = new Amily2Bus();
|
||||
console.log('[Amily2] Amily2Bus 已成功初始化并附加到 window 对象');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
112
SL/bus/README.md
112
SL/bus/README.md
@@ -1,3 +1,111 @@
|
||||
# Amily2Bus
|
||||
# Amily2Bus (Amily2 总线系统)
|
||||
|
||||
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')` 动态开启。
|
||||
|
||||
223
SL/bus/api/ModelCaller.js
Normal file
223
SL/bus/api/ModelCaller.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import { getRequestHeaders } from "/script.js";
|
||||
import { getContext } 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}] Stream: ${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);
|
||||
}
|
||||
|
||||
return this._normalize(result);
|
||||
} catch (error) {
|
||||
this._log('error', `Request Failed: ${error.message}`, callerName);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 内部日志封装
|
||||
_log(level, msg, plugin) {
|
||||
if (this.logger && typeof this.logger.log === 'function') {
|
||||
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';
|
||||
// Preset 模式下只需要最小载荷
|
||||
const payload = requestBody.toMinimalPayload();
|
||||
|
||||
const fetchOpts = {
|
||||
method: 'POST',
|
||||
headers: { ...getRequestHeaders(), ...this.defaultHeaders },
|
||||
body: JSON.stringify(payload)
|
||||
};
|
||||
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, options) {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
// const text = await response.text();
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// 伪流式聚合:防 CloudFlare 超时
|
||||
async _fetchFakeStream(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Stream HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
let aggregated = "";
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
// 持续读取保持连接活跃
|
||||
aggregated += decoder.decode(value, { stream: true });
|
||||
}
|
||||
aggregated += decoder.decode();
|
||||
|
||||
try {
|
||||
return JSON.parse(aggregated);
|
||||
} catch (e) {
|
||||
// 如果是 SSE 格式或其他非 JSON 格式,暂且返回文本
|
||||
return aggregated;
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 数据归一化
|
||||
// ========================================================================
|
||||
|
||||
_normalize(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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
68
SL/bus/api/RequestBody.js
Normal file
68
SL/bus/api/RequestBody.js
Normal file
@@ -0,0 +1,68 @@
|
||||
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 } = 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,
|
||||
stream: false, // 这里的 stream 是指 ST 后端行为,始终为 false
|
||||
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,
|
||||
max_tokens: this.options.maxTokens,
|
||||
...this.options.params
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default RequestBody;
|
||||
@@ -18,7 +18,9 @@ class Logger {
|
||||
all: 0xF // 15
|
||||
};
|
||||
|
||||
constructor() {
|
||||
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;
|
||||
|
||||
@@ -49,7 +51,7 @@ class Logger {
|
||||
if (levelInput.includes('|')) {
|
||||
return levelInput.split('|').reduce((mask, l) => mask | (Logger.LOG_LEVEL_CODE[l.trim()] || 0), 0);
|
||||
}
|
||||
console.warn(`[Logger] Unknown log level string: ${levelInput}`);
|
||||
this.safeConsole.warn(`[Logger] Unknown log level string: ${levelInput}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -70,10 +72,10 @@ class Logger {
|
||||
|
||||
if (target === 'Global') {
|
||||
this.globalLevel = mask;
|
||||
console.log(`[Logger] Global log level mask set to: ${mask.toString(2)}`);
|
||||
this.safeConsole.log(`[Logger] Global log level mask set to: ${mask.toString(2)}`);
|
||||
} else {
|
||||
this.levelConfig[target] = mask;
|
||||
console.log(`[Logger] Log level mask for '${target}' set to: ${mask.toString(2)}`);
|
||||
this.safeConsole.log(`[Logger] Log level mask for '${target}' set to: ${mask.toString(2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,11 +91,11 @@ class Logger {
|
||||
if (target === 'Global') {
|
||||
currentMask = this.globalLevel;
|
||||
this.globalLevel = currentMask | maskToAdd;
|
||||
console.log(`[Logger] Added level to Global. New mask: ${this.globalLevel.toString(2)}`);
|
||||
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;
|
||||
console.log(`[Logger] Added level to '${target}'. New mask: ${this.levelConfig[target].toString(2)}`);
|
||||
this.safeConsole.log(`[Logger] Added level to '${target}'. New mask: ${this.levelConfig[target].toString(2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,11 +112,11 @@ class Logger {
|
||||
currentMask = this.globalLevel;
|
||||
// 使用 & ~mask 实现移除
|
||||
this.globalLevel = currentMask & ~maskToRemove;
|
||||
console.log(`[Logger] Removed level from Global. New mask: ${this.globalLevel.toString(2)}`);
|
||||
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;
|
||||
console.log(`[Logger] Removed level from '${target}'. New mask: ${this.levelConfig[target].toString(2)}`);
|
||||
this.safeConsole.log(`[Logger] Removed level from '${target}'. New mask: ${this.levelConfig[target].toString(2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +147,8 @@ class Logger {
|
||||
*/
|
||||
process(plugin, origin, type, message, inFile = false) {
|
||||
// [DEBUG] 强制输出以确认方法被调用 (使用 error 级别防止被过滤)
|
||||
console.error('[Logger DEBUG] Process called:', { plugin, origin, type, message });
|
||||
// 【核心修改】:使用 safeConsole 替代全局 console
|
||||
this.safeConsole.error('[Logger DEBUG] Process called:', { plugin, origin, type, message });
|
||||
|
||||
// 1. 默认归属处理
|
||||
const safePlugin = plugin || 'Global';
|
||||
@@ -163,25 +166,26 @@ class Logger {
|
||||
}
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
// 格式: [12:00:00] [PluginName::ClassName] [INFO]: message
|
||||
// 格式: [12:00:00] [PluginName::ClassName] [INFO: message
|
||||
const fullMessage = `[${timestamp}] [${safePlugin}::${safeOrigin}] [${type.toUpperCase()}]: ${message}`;
|
||||
|
||||
// 5. Console Output
|
||||
// 【核心修改】:使用 safeConsole 替代全局 console
|
||||
switch (type) {
|
||||
case 'debug':
|
||||
console.debug(fullMessage);
|
||||
this.safeConsole.debug(fullMessage);
|
||||
break;
|
||||
case 'info':
|
||||
console.info(fullMessage);
|
||||
this.safeConsole.info(fullMessage);
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(fullMessage);
|
||||
this.safeConsole.warn(fullMessage);
|
||||
break;
|
||||
case 'error':
|
||||
console.error(fullMessage);
|
||||
this.safeConsole.error(fullMessage);
|
||||
break;
|
||||
default:
|
||||
console.log(fullMessage);
|
||||
this.safeConsole.log(fullMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -200,7 +204,7 @@ class Logger {
|
||||
} else {
|
||||
// Fallback: 如果总线未就绪,仅在控制台警告一次,避免死循环
|
||||
if (!this._warned) {
|
||||
console.warn('[Logger] FilePipe system not linked. Log not saved to file.');
|
||||
this.safeConsole.warn('[Logger] FilePipe system not linked. Log not saved to file.');
|
||||
this._warned = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user