From 46fea931155215cb1fd29b4cec87e861f9ffe656 Mon Sep 17 00:00:00 2001 From: Silence_Lurker Date: Thu, 15 Jan 2026 17:41:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=80=BB=E7=BA=BF=E5=BC=80=E5=8F=91=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B-init?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SL/bus/Amily2Bus.js | 59 ++++++++++++++++ SL/bus/README.md | 3 + SL/bus/file/FilePipe.js | 61 ++++++++++++++++ SL/bus/log/Logger.js | 137 ++++++++++++++++++++++++++++++++++++ TODO.md | 4 ++ core/bus.js | 152 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 416 insertions(+) create mode 100644 SL/bus/Amily2Bus.js create mode 100644 SL/bus/README.md create mode 100644 SL/bus/file/FilePipe.js create mode 100644 SL/bus/log/Logger.js create mode 100644 core/bus.js diff --git a/SL/bus/Amily2Bus.js b/SL/bus/Amily2Bus.js new file mode 100644 index 0000000..bf193bb --- /dev/null +++ b/SL/bus/Amily2Bus.js @@ -0,0 +1,59 @@ +class Amily2Bus { + constructor() { + /** @type {Logger|null} */ + this.Logger = null; + /** @type {FilePipe|null} */ + this.FilePipe = null; + + // 已注册插件列表,防止重复注册 + this.registry = new Set(); + + console.log('[Amily2Bus] Core container initialized with secure registry.'); + } + + /** + * 注册插件并获取专属上下文 + * @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)) { + console.warn(`[Amily2Bus] Plugin '${pluginName}' is already registered.`); + } else { + this.registry.add(pluginName); + console.log(`[Amily2Bus] Plugin registered: ${pluginName}`); + } + + // 返回该插件专属的 API 上下文 (Capability Token) + return { + // 绑定了身份的日志接口 + log: (origin, type, message) => { + if (this.Logger) { + // 自动填充 plugin 参数 + this.Logger.log(pluginName, origin, type, message); + } + }, + + // 绑定了身份的文件接口 + 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; + } + } + }; + } +} + +// 挂载全局单例 +if (!window.Amily2Bus) { + window.Amily2Bus = new Amily2Bus(); +} + +export default Amily2Bus; \ No newline at end of file diff --git a/SL/bus/README.md b/SL/bus/README.md new file mode 100644 index 0000000..ee18cdb --- /dev/null +++ b/SL/bus/README.md @@ -0,0 +1,3 @@ +# Amily2Bus + +Amily2总线类,提供基础的文件操作方法和标准日志操作方法,便于开发者使用和规范化日志处理。同时提供了插件注册和事件监听机制。 diff --git a/SL/bus/file/FilePipe.js b/SL/bus/file/FilePipe.js new file mode 100644 index 0000000..aa0eddc --- /dev/null +++ b/SL/bus/file/FilePipe.js @@ -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; \ No newline at end of file diff --git a/SL/bus/log/Logger.js b/SL/bus/log/Logger.js new file mode 100644 index 0000000..1c8a1f8 --- /dev/null +++ b/SL/bus/log/Logger.js @@ -0,0 +1,137 @@ +/** + * 日志总类,用于记录日志信息 + */ +class Logger { + + static LOG_HEADER_DEBUG = '[DEBUG]'; + static LOG_HEADER_INFO = '[INFO]'; + static LOG_HEADER_WARN = '[WARN]'; + static LOG_HEADER_ERROR = '[ERROR]'; + + static LEVEL_WEIGHTS = { + 'debug': 0, + 'info': 1, + 'warn': 2, + 'error': 3 + }; + + constructor() { + // 全局默认级别 + this.globalLevel = 'info'; + + // 针对特定插件或模块的配置 + // 结构示例: + // { + // "PluginA": "debug", // PluginA 下所有模块默认为 debug + // "PluginB::ModuleX": "error" // 仅 PluginB 下的 ModuleX 为 error + // } + this.levelConfig = {}; + } + + /** + * 设置日志级别 + * @param {string} target 目标范围,可以是 'Global'、'PluginName' 或 'PluginName::ModuleName' + * @param {string} level 'debug' | 'info' | 'warn' | 'error' + */ + setLevel(target, level) { + if (!Logger.LEVEL_WEIGHTS.hasOwnProperty(level)) { + console.warn(`[Logger] Invalid log level: ${level}`); + return; + } + + if (target === 'Global') { + this.globalLevel = level; + console.log(`[Logger] Global log level set to: ${level}`); + } else { + this.levelConfig[target] = level; + console.log(`[Logger] Log level for '${target}' set to: ${level}`); + } + } + + /** + * 获取指定上下文的生效日志级别(级联查找) + * @param {string} plugin + * @param {string} origin (Module) + */ + _getEffectiveLevel(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; + } + + log(plugin, origin, type, message, inFile = false) { + // 获取当前上下文生效的日志级别权重 + const effectiveLevelName = this._getEffectiveLevel(plugin, origin); + const currentWeight = Logger.LEVEL_WEIGHTS[effectiveLevelName]; + + const weight = Logger.LEVEL_WEIGHTS[type] !== undefined ? Logger.LEVEL_WEIGHTS[type] : 1; + + // 级别筛选 + if (weight < currentWeight) { + return; + } + + const timestamp = new Date().toLocaleTimeString(); + // 格式: [12:00:00] [PluginName::ClassName] [INFO]: message + const fullMessage = `[${timestamp}] [${plugin}::${origin}] [${type.toUpperCase()}]: ${message}`; + + // 1. Console Output + switch (type) { + case 'debug': + console.debug(fullMessage); + break; + case 'info': + console.info(fullMessage); + break; + case 'warn': + console.warn(fullMessage); + break; + case 'error': + console.error(fullMessage); + break; + default: + console.log(fullMessage); + break; + } + + // 2. File Output (via FilePipe) + if (inFile) { + // Logger 自身也需要作为系统组件注册,获取写入权限 + // 注意:为了性能,建议在 constructor 里注册一次保存起来,这里为了逻辑展示暂时简化 + if (!this.sysBus) { + if (window.Amily2Bus && window.Amily2Bus.register) { + this.sysBus = window.Amily2Bus.register('SystemLogger'); + } + } + + if (this.sysBus && this.sysBus.file) { + // 使用注册后的安全接口写入,无需再手动传 'SystemLogger' + // TODO: 这里的路径 'runtime.log' 后续可以做成配置项 + this.sysBus.file.write('runtime.log', fullMessage + '\n'); + } else { + // Fallback: 如果总线未就绪,仅在控制台警告一次,避免死循环 + if (!this._warned) { + console.warn('[Logger] FilePipe system not linked. Log not saved to file.'); + this._warned = true; + } + } + } + } + +} + +// Ensure Amily2Bus namespace exists to prevent crash if loaded out of order +window.Amily2Bus = window.Amily2Bus || {}; +window.Amily2Bus.Logger = new Logger(); + +export default Logger; \ No newline at end of file diff --git a/TODO.md b/TODO.md index a70435e..8711baf 100644 --- a/TODO.md +++ b/TODO.md @@ -31,3 +31,7 @@ - 添加了**TODO.md**,现在可以记录任务清单并更清楚的记录开发完成状态了。 - 无实际功能更新 + +### 中间版本未维护 + +### 1.8.3 diff --git a/core/bus.js b/core/bus.js new file mode 100644 index 0000000..cd5077b --- /dev/null +++ b/core/bus.js @@ -0,0 +1,152 @@ +import { eventSource, event_types } from '/script.js'; + +export const LOG_LEVELS = { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + NONE: 99 +}; + +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;