diff --git a/SL/bus/Amily2Bus.js b/SL/bus/Amily2Bus.js index bb2c0ed..1aa3e41 100644 --- a/SL/bus/Amily2Bus.js +++ b/SL/bus/Amily2Bus.js @@ -1,9 +1,12 @@ +import { Logger } from '/core/logger.js'; +import { FilePipe } from '/core/file-pipe.js'; + class Amily2Bus { constructor() { /** @type {Logger|null} */ - this.Logger = null; + this.Logger = new Logger(); /** @type {FilePipe|null} */ - this.FilePipe = null; + this.FilePipe = new FilePipe(); // 已注册插件列表,防止重复注册 this.registry = new Set(); diff --git a/SL/bus/log/Logger.js b/SL/bus/log/Logger.js index 1c8a1f8..51d3703 100644 --- a/SL/bus/log/Logger.js +++ b/SL/bus/log/Logger.js @@ -1,5 +1,6 @@ /** * 日志总类,用于记录日志信息 + * 支持基于位运算的自定义日志级别控制 */ class Logger { @@ -8,52 +9,121 @@ class Logger { static LOG_HEADER_WARN = '[WARN]'; static LOG_HEADER_ERROR = '[ERROR]'; - static LEVEL_WEIGHTS = { - 'debug': 0, - 'info': 1, - 'warn': 2, - 'error': 3 + static LOG_LEVEL_CODE = { + none: 0, + debug: 1 << 0, // 1 + info: 1 << 1, // 2 + warn: 1 << 2, // 4 + error: 1 << 3, // 8 + all: (1 << 4) - 1 // 15 }; constructor() { - // 全局默认级别 - this.globalLevel = 'info'; + // 全局默认级别 (默认开启 info, warn, error) + this.globalLevel = Logger.LOG_LEVEL_CODE.info | Logger.LOG_LEVEL_CODE.warn | Logger.LOG_LEVEL_CODE.error; // 针对特定插件或模块的配置 // 结构示例: // { - // "PluginA": "debug", // PluginA 下所有模块默认为 debug - // "PluginB::ModuleX": "error" // 仅 PluginB 下的 ModuleX 为 error + // "PluginA": 3, // PluginA 下所有模块掩码为 3 (debug | info) + // "PluginB::ModuleX": 8 // 仅 PluginB 下的 ModuleX 掩码为 8 (error) // } this.levelConfig = {}; } /** - * 设置日志级别 - * @param {string} target 目标范围,可以是 'Global'、'PluginName' 或 'PluginName::ModuleName' - * @param {string} level 'debug' | 'info' | 'warn' | 'error' + * 将输入转换为对应的日志级别掩码 + * @param {number|string|string[]} levelInput + * @returns {number} 掩码 */ - setLevel(target, level) { - if (!Logger.LEVEL_WEIGHTS.hasOwnProperty(level)) { - console.warn(`[Logger] Invalid log level: ${level}`); - return; + _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); + } + console.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 = level; - console.log(`[Logger] Global log level set to: ${level}`); + this.globalLevel = mask; + console.log(`[Logger] Global log level mask set to: ${mask.toString(2)}`); } else { - this.levelConfig[target] = level; - console.log(`[Logger] Log level for '${target}' set to: ${level}`); + this.levelConfig[target] = mask; + console.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; + console.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)}`); + } + } + + /** + * 移除日志级别(减量模式) + * @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; + console.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)}`); + } + } + + /** + * 获取指定上下文的生效日志级别掩码(级联查找) * @param {string} plugin * @param {string} origin (Module) */ - _getEffectiveLevel(plugin, origin) { + _getEffectiveLevelMask(plugin, origin) { // 1. 检查精确匹配 "Plugin::Module" const specificKey = `${plugin}::${origin}`; if (this.levelConfig.hasOwnProperty(specificKey)) { @@ -70,14 +140,14 @@ class Logger { } log(plugin, origin, type, message, inFile = false) { - // 获取当前上下文生效的日志级别权重 - const effectiveLevelName = this._getEffectiveLevel(plugin, origin); - const currentWeight = Logger.LEVEL_WEIGHTS[effectiveLevelName]; + // 获取当前上下文生效的日志级别掩码 + const effectiveMask = this._getEffectiveLevelMask(plugin, origin); - const weight = Logger.LEVEL_WEIGHTS[type] !== undefined ? Logger.LEVEL_WEIGHTS[type] : 1; + // 获取当前日志类型的位码 + const typeCode = Logger.LOG_LEVEL_CODE[type]; - // 级别筛选 - if (weight < currentWeight) { + // 级别筛选:位与运算结果为0则表示该级别未开启 + if (typeCode === undefined || (effectiveMask & typeCode) === 0) { return; } @@ -107,7 +177,6 @@ class Logger { // 2. File Output (via FilePipe) if (inFile) { // Logger 自身也需要作为系统组件注册,获取写入权限 - // 注意:为了性能,建议在 constructor 里注册一次保存起来,这里为了逻辑展示暂时简化 if (!this.sysBus) { if (window.Amily2Bus && window.Amily2Bus.register) { this.sysBus = window.Amily2Bus.register('SystemLogger'); @@ -116,7 +185,6 @@ class Logger { if (this.sysBus && this.sysBus.file) { // 使用注册后的安全接口写入,无需再手动传 'SystemLogger' - // TODO: 这里的路径 'runtime.log' 后续可以做成配置项 this.sysBus.file.write('runtime.log', fullMessage + '\n'); } else { // Fallback: 如果总线未就绪,仅在控制台警告一次,避免死循环 diff --git a/core/bus.js b/core/bus.js deleted file mode 100644 index 74c9271..0000000 --- a/core/bus.js +++ /dev/null @@ -1,152 +0,0 @@ -import { eventSource, event_types } from '/script.js'; - -export const LOG_LEVELS = { - DEBUG: 0b0001, - INFO: 0b0010, - WARN: 0b0100, - ERROR: 0b1000, - NONE: 0 -}; - -class FilePipe { - constructor(pluginName, bus) { - this.pluginName = pluginName; - this.bus = bus; - this.basePath = `extensions/${pluginName}`; - } - - /** - * 验证路径是否合法 (仅允许访问插件自己的目录) - * @param {string} path - * @returns {boolean} - */ - validatePath(path) { - // 简单示例:防止 ../ 越权,并确保路径以 basePath 或允许的相对路径开头 - // 实际上 SillyTavern 的 writeFile API 通常已经限制在特定目录, - // 但这里我们做逻辑层的隔离。 - if (path.includes('..')) { - this.bus.log(this.pluginName, 'FilePipe', 'ERROR', `非法路径尝试: ${path}`); - return false; - } - return true; - } - - async read(fileName) { - if (!this.validatePath(fileName)) throw new Error("Access Denied"); - // 这里对接实际的文件读取逻辑,例如 fetch 或 ST 的 API - // return await readFileContent(this.basePath + '/' + fileName); - console.log(`[Mock] Reading ${fileName} for ${this.pluginName}`); - } - - async write(fileName, content) { - if (!this.validatePath(fileName)) throw new Error("Access Denied"); - // 这里对接实际的文件写入逻辑 - // return await writeFileContent(this.basePath + '/' + fileName, content); - console.log(`[Mock] Writing to ${fileName} for ${this.pluginName}`); - } -} - -class Amily2Bus { - constructor() { - this.plugins = new Map(); - this.logConfig = { - globalLevel: LOG_LEVELS.INFO, - filters: {} // e.g., "Amily2.Network": LOG_LEVELS.DEBUG - }; - console.log("[Amily2-Bus] 总线系统初始化..."); - } - - /** - * 注册插件 - * @param {Object} manifest - 必须包含 name 字段 - * @param {Object} instance - 插件实例或主要对象 - * @returns {FilePipe|null} - 返回专属的文件管线对象,注册失败返回 null - */ - register(manifest, instance = {}) { - if (!manifest || !manifest.name) { - this.log('System', 'Bus', 'ERROR', '注册失败: 无效的 manifest'); - return null; - } - - const name = manifest.name; - if (this.plugins.has(name)) { - this.log('System', 'Bus', 'WARN', `插件 ${name} 已注册,正在覆盖...`); - } - - this.plugins.set(name, { - manifest, - instance, - registeredAt: Date.now() - }); - - this.log('System', 'Bus', 'INFO', `插件注册成功: ${name}`); - - // 返回该插件专属的 FilePipe - return new FilePipe(name, this); - } - - /** - * 获取已注册的插件实例 - * @param {string} name - */ - getPlugin(name) { - return this.plugins.get(name)?.instance; - } - - /** - * 设置日志级别 - * @param {string} target - "Global" 或 "PluginName.ModuleName" - * @param {string|number} level - "DEBUG", "INFO", etc. - */ - setLogLevel(target, level) { - const lvl = typeof level === 'string' ? LOG_LEVELS[level.toUpperCase()] : level; - if (target === 'Global') { - this.logConfig.globalLevel = lvl; - } else { - this.logConfig.filters[target] = lvl; - } - } - - /** - * 统一日志方法 - * @param {string} plugin - 插件名 - * @param {string} module - 模块名 - * @param {string} level - 级别 (DEBUG, INFO, WARN, ERROR) - * @param {string|Object} message - 内容 - */ - log(plugin, module, level, message) { - const currentLevel = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO; - const configKey = `${plugin}.${module}`; - - // 检查特定配置,如果没有则使用全局配置 - const requiredLevel = this.logConfig.filters[configKey] !== undefined - ? this.logConfig.filters[configKey] - : this.logConfig.globalLevel; - - if (currentLevel < requiredLevel) { - return; // 级别不足,忽略 - } - - const timestamp = new Date().toLocaleTimeString(); - const formattedMsg = typeof message === 'object' ? JSON.stringify(message) : message; - - // 样式化输出 - let color = '#7f8c8d'; // Default (INFO) - if (level === 'WARN') color = '#f39c12'; - if (level === 'ERROR') color = '#c0392b'; - if (level === 'DEBUG') color = '#3498db'; - - console.log( - `%c[${timestamp}] [${plugin}::${module}]%c ${formattedMsg}`, - `color: ${color}; font-weight: bold;`, - `color: inherit;` - ); - } -} - -// 单例模式,挂载到 window 方便全局调用 -if (!window.Amily2Bus) { - window.Amily2Bus = new Amily2Bus(); -} - -export default window.Amily2Bus;