Skrip ini tidak untuk dipasang secara langsung. Ini adalah pustaka skrip lain untuk disertakan dengan direktif meta // @require https://update.greasyfork.org/scripts/545792/1641440/Caliberjs%20Framework%20Library.js
// ==UserScript==
// @name Caliber.js Framework Library
// @namespace You Boy
// @version 1.0.0
// @description 一个旨在帮助开发者快速构建功能强大、可维护的现代油猴脚本的框架。它提供模块化架构、自动化的UI设置面板、响应式生命周期管理、高性能DOM调度器与网络请求拦截器等核心功能,让您专注于实现创意,而非繁琐的底层细节。
// @author You Boy
// @license MIT
// @grant none
// ==/UserScript==
((window) => {
'use strict';
if (window.Caliber) {
console.warn('Caliber.js has already been loaded.');
return;
}
const Caliber = (() => {
'use strict';
// #region --- 类型定义 (Type Definitions) ---
/**
* @typedef {object} DomBatchProcessorInstance 高性能DOM批量处理调度器实例
* @property {(selector: string, callback: (node: HTMLElement) => void, options?: object) => Symbol} register - 注册一个DOM处理任务,返回一个唯一的任务ID。
* @property {(taskId: Symbol) => void} unregister - 根据任务ID注销一个DOM处理任务。
*/
/**
* @typedef {object} ConfigManagerInstance 配置管理器实例
* @property {() => object} getConfig - 获取当前已加载的配置对象。
* @property {(path: string, value: any) => Promise<void>} updateAndSave - 按路径更新配置项并自动保存。
* @property {() => Promise<void>} save - 手动将当前配置保存到存储中。
*/
/**
* @typedef {object} ModuleManagerInstance 模块管理器实例
* @property {(ModuleClass: typeof Module) => void} register - 注册一个模块类。
* @property {() => Module[]} getAllRegisteredModules - 获取所有已注册模块的元数据,主要用于UI生成。
*/
/**
* @typedef {object} LoggerInstance 日志记录器实例
* @property {(message: any, ...args: any[]) => void} log - 输出一条标准日志。
* @property {(message: any, ...args: any[]) => void} warn - 输出一条警告日志。
* @property {(message: any, ...args: any[]) => void} error - 输出一条错误日志。
* @property {(tag: string, styleOptions?: object) => LoggerInstance} createTaggedLogger - 创建一个带有自定义标签和样式的子日志记录器。
*/
/**
* @typedef {object} EventBusInstance 全局事件总线实例
* @property {(eventName: string, callback: (data: any) => void) => void} on - 订阅一个事件。
* @property {(eventName: string, callback: (data: any) => void) => void} off - 取消订阅一个事件。
* @property {(eventName: string, data?: any) => void} emit - 发布一个事件。
*/
/**
* @typedef {object} StorageAdapter 存储服务适配器
* @property {() => Promise<object>} get - 异步获取存储的配置对象。
* @property {(value: object) => Promise<void>} set - 异步将配置对象写入存储。
*/
/**
* @typedef {object} StyleAdapter 样式服务适配器
* @property {(cssString: string, id: string) => Promise<void>} add - 注入一段CSS样式,并关联一个唯一ID。
* @property {(id: string) => void} remove - 根据ID移除之前注入的样式。
*/
/**
* @typedef {string} FetchInterceptorRequestString
*
* 定义当一个网络请求被成功匹配时,在**发起实际请求之前**要执行的修改逻辑。
* 这段代码在宿主页面的上下文中运行,拥有访问`window`对象的全部能力,但必须是一个无闭包依赖的纯字符串。
*
* 示例:为一个API请求添加或修改一个查询参数
* .onRequest(`
* [可用上下文]
* - url: {string} 原始的、完整的请求URL字符串。
* - config: {object} 原始的fetch配置对象 (如 method, headers, body 等)。
* - urlObject: {URL} 由框架预先创建的URL实例,强烈推荐使用它来操作URL。
*
* 1. (推荐) 使用内置的 urlObject 来修改URL
* urlObject.searchParams.set('new_param', 'caliber_rocks');
*
* 2. (可选) 修改 fetch 配置对象
* config.headers = { ...config.headers, 'X-Caliber-Injected': 'true' };
*
* [返回契约]
* 必须返回一个包含 'url' 和 'config' 键的对象。
* - url: {string} 最终将要被请求的URL字符串。
* - config: {object} 最终将要被使用的fetch配置对象。
* return { url: urlObject.toString(), config };
* `)
*
* 示例:添加或修改URL查询参数
* > .onRequest(`
* [可用上下文]
* - url: {string} 原始的、完整的请求URL字符串。
* - config: {object} 原始的fetch配置对象 (如 method, headers, body 等)。
* - urlObject: {URL} 由框架预先创建的URL实例,强烈推荐使用它来操作URL。
*
* (推荐) 使用内置的 urlObject 来修改URL的查询参数
* > urlObject.searchParams.set('page', '1');
* > urlObject.searchParams.set('limit', '50');
*
* 必须返回一个包含 'url' 和 'config' 键的对象。
* > return { url: urlObject.toString(), config };
*
* > `)
*
*/
/**
* @typedef {object} FetchInterceptorBuilder 网络请求拦截器构建器
* @property {(handlerString: FetchInterceptorRequestString) => FetchInterceptorBuilder} onRequest - 定义请求被拦截时要执行的逻辑。
* @property {(callback: (responseData: any) => void) => FetchInterceptorBuilder} onResponse - 定义在沙箱中处理响应数据的回调函数。
* @property {(id: string|string[]) => void} register - 最终确定并注册这个拦截器。
*/
/**
* @typedef {object} PageScopeExecutorInstance 页面作用域代码执行器服务实例
* @property {(codeString: string) => Promise<any>} execute - 在宿主页面环境中异步执行一段JS代码,并返回其可序列化的结果。
*/
/**
* @callback ResponseCallback
* @param {any} responseData - 从注入脚本传回的、已解析的响应数据。
*/
/**
* @typedef {object} FetchInterceptorInstance 网络请求拦截器服务实例
* @property {(urlOrOptions: string|{url: string, method?: string, match?: string|RegExp|Array<string|RegExp>}) => FetchInterceptorBuilder} target - [推荐] 启动一个链式调用来构建拦截器。
* @property {(options: {targetUrl: string, method?: string, handler: string}) => string} createHook - (底层) 创建一个钩子函数字符串。
* @property {(path: string[]|string, hookFunctionString: string, awaitsResponse?: boolean) => void} addHook - (底层) 添加一个仅修改请求的钩子。
* @property {(path: string[]|string, hookFunctionString: string, responseCallback: ResponseCallback) => void} addHookWithResponse - (底层) 添加一个带响应回调的钩子。
* @property {(path: string[]|string) => void} removeHook - (底层/通用) 按ID移除一个已注册的拦截器。
*/
/**
* @typedef {object} DOMSanitizerInstance DOM净化与安全注入服务实例
* @property {(htmlString: string) => (TrustedHTML|string)} createTrustedHTML - 创建一个受信任的HTML对象(如果Trusted Types策略可用)。
* @property {(element: Element, htmlString: string) => void} setInnerHTML - 安全地设置一个元素的innerHTML。
* @property {(doc: Document, codeString: string) => void} injectScript - 安全地向文档注入一段JavaScript代码。
* @property {(doc: Document, cssString: string, id: string) => (HTMLStyleElement|null)} injectStyle - 安全地向文档注入一段CSS样式。
*/
/**
* @typedef {object} FrameworkServices 框架核心服务集合
* @property {DomBatchProcessorInstance} scheduler - 高性能DOM批量处理调度器。
* @property {FetchInterceptorInstance} interceptor - 网络请求拦截器。
* @property {DOMSanitizerInstance} sanitizer - DOM净化与安全注入服务。
* @property {PageScopeExecutorInstance} executor - 页面作用域代码执行器。
*/
/**
* @typedef {object} CaliberServices 内核服务包
* @property {LoggerInstance} logger - 日志服务实例。
* @property {EventBusInstance} eventBus - 全局事件总线实例。
* @property {Window} hostWindow - 宿主环境的 window 对象 (通常是 unsafeWindow)。
* @property {Document} hostDocument - 宿主环境的 document 对象。
* @property {StorageAdapter} storage - 存储服务适配器 (e.g., GM.getValue/setValue)。
* @property {StyleAdapter} style - 样式服务适配器 (e.g., GM.addStyle)。
* @property {string} APP_NAME - 当前应用的名称。
* @property {ConfigManagerInstance} configManager - 配置管理器实例。
* @property {ModuleManagerInstance} moduleManager - 模块管理器实例。
* @property {FrameworkServices} framework - 框架核心服务集合。
*/
/**
* @typedef {object} ModuleInterface 模块类的公共API与内部属性接口
* @property {string} id - 模块的唯一标识符,用于配置和管理。
* @property {string} name - 模块的显示名称,用于UI。
* @property {string} description - 模块的功能描述,用于UI。
* @property {object} defaultConfig - 模块的默认配置。
* @property {string|RegExp|Array<string|RegExp>|null} [match=null] - (可选) 限制模块仅在匹配该规则的页面上运行。
*
* @property {CaliberServices} _services - (底层) 内核注入的完整核心服务集合。
* @property {object} _config - 模块在总配置对象中的专属部分。
* @property {LoggerInstance} _logger - (快捷方式) 日志记录器。
* @property {EventBusInstance} _eventBus - (快捷方式) 事件总线。
* @property {Window} _hostWindow - (快捷方式) 宿主 window 对象。
* @property {Document} _hostDocument - (快捷方式) 宿主 document 对象。
* @property {DomBatchProcessorInstance} _scheduler - (快捷方式) 高性能DOM批量处理调度器。
* @property {FetchInterceptorInstance} _interceptor - (快捷方式) 网络请求拦截器。
* @property {DOMSanitizerInstance} _sanitizer - (快捷方式) DOM净化与安全注入服务。
* @property {PageScopeExecutorInstance} _executor - (快捷方式) 页面作用域代码执行器
*
* @property {(context: {params: object, query: object}) => void} onEnable - 当模块被启用时调用。会收到包含解析后URL参数的上下文对象。
* @property {() => void} onDisable - 当模块被禁用时调用。必须在此处清理所有副作用。
* @property {(key: string, newValue: any, oldValue: any) => void} onConfigChange - 当模块的特定配置项发生变化时调用。
*/
// #endregion
/**
* @class Module - 功能模块的标准化基类
*
* 所有功能模块都应继承此类,以确保接口统一和生命周期管理。
* @implements {ModuleInterface}
*/
class Module {
id = 'base-module';
name = 'Base Module';
description = '这是一个模块模板。';
defaultConfig = {};
match = null;
_services;
_config;
_logger;
_eventBus;
_hostWindow;
_hostDocument;
_scheduler;
_interceptor;
_sanitizer;
_executor;
/**
* @param {CaliberServices} services - 内核注入的核心服务。
* @param {object} moduleConfig - 该模块在总配置中的专属配置部分。
*/
constructor(services, moduleConfig) {
if (!services) {
return;
}
this._services = services;
this._config = moduleConfig;
this._logger = services.logger;
this._eventBus = services.eventBus;
this._hostWindow = services.hostWindow;
this._hostDocument = services.hostDocument;
// framework内部工具快捷方式
this._scheduler = services.framework.scheduler;
this._interceptor = services.framework.interceptor;
this._sanitizer = services.framework.sanitizer;
this._executor = services.framework.executor;
}
/**
* 当模块被启用时调用。所有事件监听和DOM操作应在此处初始化。
* @param {{params: object, query: object}} context - 包含从URL解析出的命名参数 (`params`) 和查询参数 (`query`) 的对象。
*/
onEnable(context) {
this._logger.warn(`Module '${this.id}' is missing the 'onEnable' implementation.`);
}
onDisable() { }
/**
* 当模块的特定配置项发生变化时调用。
* @param {string} key - 发生变化的配置键。
* @param {*} newValue - 新的配置值。
* @param {*} oldValue - 旧的配置值。
*/
onConfigChange(key, newValue, oldValue) { }
}
/**
* @class AppKernel - 应用程序内核
*
* 负责管理模块生命周期、配置、UI和所有核心服务。
*/
class AppKernel {
#services = {};
#moduleManager;
#configManager;
#uiManager;
#logger;
#auditor = null;
/**
* @param {object} injectedServices - 由 `createApp` 组装好的所有核心服务和配置。
* @param {Window} injectedServices.hostWindow - 宿主 window 对象。
* @param {Document} injectedServices.hostDocument - 宿主 document 对象。
* @param {EventBusInstance} injectedServices.eventBus - 全局事件总线。
* @param {LoggerInstance} injectedServices.logger - 主日志记录器。
* @param {StorageAdapter} injectedServices.storage - 存储服务适配器。
* @param {StyleAdapter} injectedServices.style - 样式服务适配器。
* @param {FrameworkServices} injectedServices.framework - 框架内部服务集合。
* @param {string} injectedServices.APP_NAME - 应用名称。
* @param {string} injectedServices.SAFE_APP_NAME - 安全的应用名称。
* @param {boolean} injectedServices.IS_DEBUG - 是否为调试模式。
* @param {object} injectedServices.initialConfig - 框架的初始配置。
*/
constructor(injectedServices) {
const {
// --- 运行时服务 ---
hostWindow,
hostDocument,
eventBus,
logger,
storage,
style,
framework,
APP_NAME,
SAFE_APP_NAME,
// --- 元数据/配置 ---
IS_DEBUG,
initialConfig,
} = injectedServices;
this.#logger = logger;
// 将“运行时服务”组装到内核的 #services 对象中
this.#services = { hostWindow, hostDocument, eventBus, logger, storage, style, framework, APP_NAME };
// 使用元数据进行初始化
if (IS_DEBUG) {
this.#auditor = new ModuleAuditor(this.#logger, hostWindow, SAFE_APP_NAME);
this.#auditor?.patchScheduler(framework.scheduler);
}
// 创建并添加 Kernel 自身管理的核心服务实例
this.#configManager = new ConfigManager(storage, logger, initialConfig);
this.#moduleManager = new ModuleManager(this.#services, this.#auditor);
this.#uiManager = new UIManager(this.#services);
// 将新创建的管理器添加回 #services 包,以便所有模块都能访问它们
this.#services.configManager = this.#configManager;
this.#services.moduleManager = this.#moduleManager;
this.#logger.log('Kernel constructed.');
this.#services.eventBus.on('command:toggle-settings-panel', this.#handleToggleSettingsPanel);
this.#services.eventBus.on('config-updated', this.#onConfigUpdated);
}
#handleToggleSettingsPanel = async () => {
const currentConfig = this.#configManager.getConfig();
if (!currentConfig) {
this.#logger.error("Cannot toggle settings panel: config not loaded yet.");
return;
}
const newState = !currentConfig.settingsPanel.enabled;
await this.#configManager.updateAndSave('settingsPanel.enabled', newState);
this.#services.eventBus.emit('config-updated', {
path: 'settingsPanel.enabled',
value: newState,
newConfig: this.#configManager.getConfig()
});
}
#onConfigUpdated = (detail) => {
if (detail.path === 'settingsPanel.enabled') {
this.#logger.log(`Settings Panel visibility changed to: ${detail.value}`);
if (detail.value) {
this.#uiManager.showPanelTrigger();
} else {
this.#uiManager.hidePanelTrigger();
}
}
}
/**
* 启动应用。这是整个应用逻辑的入口点。
*/
async run() {
this.#logger.log('Kernel is running...');
const moduleDefaultConfigs = this.#moduleManager.getAllDefaultConfigs();
const finalConfig = await this.#configManager.loadAndGetConfig(moduleDefaultConfigs);
this.#moduleManager.initializeActiveModules(finalConfig);
this.#uiManager.init(finalConfig);
this.#logger.log('Kernel run sequence complete.');
}
/**
* 注册一个功能模块类。
* @param {typeof Module} ModuleClass - 要注册的模块类 (注意是类本身,不是实例)。
*/
registerModule(ModuleClass) {
this.#moduleManager.register(ModuleClass);
}
}
/**
* @class ConfigManager - 配置管理器
*
* 负责加载、合并、保存配置。
*/
class ConfigManager {
#storage;
#logger;
#config;
#initialConfig;
constructor(storage, logger, initialConfig) {
this.#storage = storage;
this.#logger = logger;
this.#initialConfig = initialConfig;
}
/**
* 加载、合并配置,并返回最终结果。
* @param {object} moduleDefaultConfigs - 所有模块的默认配置集合。
* @returns {Promise<object>} 最终的运行时配置。
*/
async loadAndGetConfig(moduleDefaultConfigs) {
const userConfig = await this.#storage.get();
const baseConfig = { ...this.#initialConfig, ...moduleDefaultConfigs };
let mergedConfig = this.#deepMerge(baseConfig, userConfig);
this.#config = this.#prune(mergedConfig, baseConfig);
this.#logger.log('Configuration loaded, merged, and pruned:', this.#config);
return this.#config;
}
getConfig() {
return this.#config;
}
/**
* 通过路径更新配置树中的一个值,并触发保存。
* @param {string} path - 要更新的配置路径,例如 'modules.themeSwitcher.theme'
* @param {*} value - 新的配置值
*/
async updateAndSave(path, value) {
this.#set(this.#config, path, value);
await this.save();
}
/**
* 将当前配置保存到存储中。
*/
async save() {
await this.#storage.set(this.#config);
this.#logger.log('Configuration saved.');
}
#deepMerge(target, source) {
const output = { ...target };
if (this.#isObject(target) && this.#isObject(source)) {
for (const key in source) {
const targetValue = target[key];
const sourceValue = source[key];
if (this.#isObject(targetValue) && this.#isObject(sourceValue)) {
output[key] = this.#deepMerge(targetValue, sourceValue);
}
else if (this.#isObject(targetValue) && !this.#isObject(sourceValue)) {
continue;
}
else {
output[key] = sourceValue;
}
}
}
return output;
}
#isObject = (item) => (item && typeof item === 'object' && !Array.isArray(item));
#set = (obj, path, value) => {
const keys = path.split('.');
const lastKey = keys.pop();
const finalObj = keys.reduce((o, k) => o[k] = o[k] || {}, obj);
finalObj[lastKey] = value;
};
/**
* 以 template 为模板,递归地净化 source 对象。
* 1. 移除所有不存在于 template 中的键。
* 2. 检查值的类型,如果 source 中的值的类型与 template 不匹配,则强制回退到 template 的默认值。
* 这是框架数据自愈能力的核心。
* @param {object} source - 要被净化的对象 (例如,合并后的配置)。
* @param {object} template - 权威的结构模板 (例如,默认基础配置)。
* @returns {object} 净化后的新对象。
* @private
*/
#prune(source, template) {
const prunedSource = {};
for (const key in template) {
if (source.hasOwnProperty(key)) {
const sourceValue = source[key];
const templateValue = template[key];
const sourceType = typeof sourceValue;
const templateType = typeof templateValue;
// 核心净化逻辑:
// 1. 如果类型匹配且都是对象,则递归净化。
// 2. 如果类型匹配但不是对象,则接受用户的源值。
// 3. 如果类型不匹配,则无条件地丢弃用户的源值,回退到模板的默认值。
if (sourceType === templateType) {
if (this.#isObject(sourceValue) && this.#isObject(templateValue)) {
prunedSource[key] = this.#prune(sourceValue, templateValue);
} else {
prunedSource[key] = sourceValue; // 类型匹配,接受用户的值
}
} else {
this.#logger.warn(`Configuration type mismatch for key '${key}'. User value (${sourceType}) discarded. Falling back to default (${templateType}).`);
prunedSource[key] = templateValue; // 类型不匹配,强制回退
}
} else {
// 如果用户的配置中缺少这个键,直接使用模板的默认值
prunedSource[key] = template[key];
}
}
// 遍历 source 中存在、但 template 中不存在的键,并将它们保留下来
for (const key in source) {
if (!template.hasOwnProperty(key)) {
prunedSource[key] = source[key];
if (this.#logger) this.#logger.log(`Dynamically added key '${key}' from user config has been preserved.`);
}
}
return prunedSource;
}
}
/**
* @class ModuleManager - 模块管理器
*
* 负责注册、实例化和管理所有模块的生命周期。
*/
class ModuleManager {
#services;
#registeredModuleClasses = new Map();
#activeModuleInstances = new Map();
#auditor = null;
constructor(services, auditor) {
this.#services = services;
this.#auditor = auditor;
this.#services.eventBus.on('config-updated', this.#onConfigUpdated);
this.#services.eventBus.on('navigate', this.#onNavigate);
}
/**
* 注册一个模块类。
* @param {typeof Module} ModuleClass
*/
register(ModuleClass) {
if (typeof ModuleClass !== 'function') {
this.#services.logger.warn(`Attempted to register an invalid value. Expected a class.`, ModuleClass);
return;
}
const tempInstance = new ModuleClass();
// 校验模块实例是否符合最基本的规范 (必须有 id 和 name)
const isValidId = tempInstance.id && typeof tempInstance.id === 'string' && tempInstance.id !== 'base-module';
const hasValidName = tempInstance.name && typeof tempInstance.name === 'string';
if (!isValidId || !hasValidName) {
this.#services.logger.warn(`Attempted to register an invalid module. It must have a valid 'id' and 'name' property.`, tempInstance);
return;
}
// 检查 ID 是否重复
if (this.#registeredModuleClasses.has(tempInstance.id)) {
this.#services.logger.warn(`Attempt to register module with duplicate ID: '${tempInstance.id}'. Skipping.`);
return;
}
this.#registeredModuleClasses.set(tempInstance.id, ModuleClass);
this.#services.logger.log(`Module class '${tempInstance.name} (${tempInstance.id})' registered.`);
}
/**
* 根据最终配置,初始化所有应该被激活的模块。
* @param {object} config - 最终的运行时配置。
*/
initializeActiveModules(config) {
this.#services.logger.log('Initializing active modules...');
this.#onNavigate();
}
/**
* 遍历所有已注册的模块类,收集它们的默认配置。
* @returns {object} 一个包含所有模块默认配置的聚合对象。
*/
getAllDefaultConfigs() {
const modulesConfig = {};
for (const ModuleClass of this.#registeredModuleClasses.values()) {
const tempInstance = new ModuleClass();
const processedDefaultConfig = {};
for (const key in tempInstance.defaultConfig) {
const item = tempInstance.defaultConfig[key];
if (typeof item === 'object' && item !== null && 'value' in item) {
processedDefaultConfig[key] = item.value;
} else {
processedDefaultConfig[key] = item;
}
}
const initialEnabledState = processedDefaultConfig.enabled === true;
delete processedDefaultConfig.enabled;
modulesConfig[tempInstance.id] = {
enabled: initialEnabledState,
...processedDefaultConfig,
};
}
return { modules: modulesConfig };
}
/**
* 返回所有已注册模块类的实例数组,用于UI生成。
* @returns {Module[]}
*/
getAllRegisteredModules() {
const modules = [];
for (const ModuleClass of this.#registeredModuleClasses.values()) {
const moduleInstance = new ModuleClass();
const clonedDefaultConfig = structuredClone(moduleInstance.defaultConfig);
modules.push({
id: moduleInstance.id,
name: moduleInstance.name,
description: moduleInstance.description,
defaultConfig: clonedDefaultConfig,
});
}
return modules;
}
/**
* 当配置更新时被调用的核心响应函数
* @param {object} detail - 包含 { path, value, newConfig } 的事件数据
*/
#onConfigUpdated = (detail) => {
const { path, value, newConfig } = detail;
const enabledMatch = path.match(/^modules\.([^.]+)\.enabled$/);
if (enabledMatch) {
const moduleId = enabledMatch[1];
this.#revalidateModuleState(moduleId, newConfig);
return;
}
const optionMatch = path.match(/^modules\.([^.]+)\.(.+)$/);
if (optionMatch) {
const moduleId = optionMatch[1];
const key = optionMatch[2];
const moduleInstance = this.#activeModuleInstances.get(moduleId);
if (moduleInstance) {
const oldConfig = { ...moduleInstance._config };
moduleInstance._config[key] = value;
moduleInstance.onConfigChange(key, value, oldConfig[key]);
}
}
}
/**
* 封装的启用模块的逻辑
*/
#enableModule(id, ModuleClass, config, matchContext) {
if (this.#activeModuleInstances.has(id)) return;
try {
// 创建模块专用的服务门面
const moduleConfig = config.modules[id];
const configManagerFacade = {
// 只暴露与当前模块相关的配置API
getConfig: () => moduleConfig,
updateAndSave: async (key, value) => {
// 自动为路径添加模块ID前缀,防止跨模块修改
const path = `modules.${id}.${key}`;
await this.#services.configManager.updateAndSave(path, value);
// 触发全局事件,以保持UI同步等
this.#services.eventBus.emit('config-updated', {
path,
value,
newConfig: this.#services.configManager.getConfig()
});
}
};
const moduleServicesFacade = {
...this.#services, // 继承所有安全的服务
configManager: configManagerFacade, // 覆盖为安全的门面
moduleManager: null, // 彻底隐藏ModuleManager,模块不应该直接操作它
};
// 使用安全的门面对象来实例化模块
const moduleInstance = new ModuleClass(moduleServicesFacade, moduleConfig);
this.#auditor?.auditStart(id);
moduleInstance.onEnable(matchContext);
this.#auditor?.auditEnd();
this.#activeModuleInstances.set(id, moduleInstance);
this.#services.logger.log(`Module '${id}' dynamically ENABLED.`);
} catch (e) {
this.#services.logger.error(`Failed to dynamically enable module '${id}'.`, e);
}
}
/**
* 封装的禁用模块的逻辑
*/
#disableModule(id) {
const moduleInstance = this.#activeModuleInstances.get(id);
if (!moduleInstance) return;
try {
// 植入审计钩子
this.#auditor?.auditStart(id);
moduleInstance.onDisable();
this.#auditor?.auditEnd();
// 从活动实例映射中移除,并允许垃圾回收
this.#activeModuleInstances.delete(id);
// 在模块禁用后,运行泄漏检查
this.#auditor?.runChecks(id);
this.#services.logger.log(`Module '${id}' dynamically DISABLED.`);
} catch (e) {
this.#services.logger.error(`Failed to dynamically disable module '${id}'.`, e);
}
}
/**
* 在SPA导航时触发,重新评估所有模块的match状态。
* @private
*/
#onNavigate = () => {
this.#services.logger.log('Navigation detected, re-evaluating all module states...');
const currentConfig = this.#services.configManager.getConfig();
if (!currentConfig) return;
for (const id of this.#registeredModuleClasses.keys()) {
this.#revalidateModuleState(id, currentConfig);
}
}
/**
* 重新评估一个模块的最终状态(启用/禁用),并执行相应操作。
* 这是模块生命周期管理的唯一决策点。
* @param {string} id - 模块ID
* @param {object} config - 当前的全局配置
* @private
*/
#revalidateModuleState(id, config) {
const ModuleClass = this.#registeredModuleClasses.get(id);
if (!ModuleClass) return;
const isEnabledInConfig = config.modules[id]?.enabled; // 用户配置
const tempInstance = new ModuleClass();
const matchResult = _CaliberInternals._checkMatch(tempInstance.match, this.#services.hostWindow);
const shouldBeActive = isEnabledInConfig && !!matchResult;
const isActiveNow = this.#activeModuleInstances.has(id);
if (shouldBeActive && !isActiveNow) {
this.#enableModule(id, ModuleClass, config, matchResult);
} else if (!shouldBeActive && isActiveNow) {
this.#disableModule(id);
}
}
}
/**
* @class UIManager - UI管理器
*
* 负责创建、管理和销毁UI组件,如下方的设置面板。
*/
class UIManager {
#appName = 'CaliberApp';
#services;
#logger;
#hostDocument;
#hostWindow;
#panelElement = null;
#lastKnownConfig = null;
#guardianIntervalId = null;
#sanitizer;
/**
* @param {CaliberServices} services
*/
constructor(services) {
this.#services = services;
this.#logger = services.logger;
this.#hostDocument = services.hostDocument;
this.#hostWindow = services.hostWindow;
this.#appName = services.APP_NAME || this.#appName;
this.#sanitizer = services.framework.sanitizer;
this.#defineSettingsPanelComponent(this.#sanitizer);
}
/**
* 根据最终配置决定是否显示设置面板的触发按钮
* @param {object} finalConfig - 从Kernel传入的最终运行时配置
*/
init(finalConfig) {
this.#lastKnownConfig = finalConfig;
if (finalConfig.settingsPanel.enabled) {
this.showPanelTrigger();
}
}
/**
* 创建并显示设置面板的触发按钮
*/
showPanelTrigger() {
if (this.#panelElement) return;
this.#logger.log('Showing settings panel trigger.');
this.#panelElement = this.#hostDocument.createElement('settings-panel');
const modules = this.#services.moduleManager.getAllRegisteredModules();
this.#panelElement.setData(
modules,
this.#lastKnownConfig,
this.#services.configManager,
this.#services.eventBus,
this.#hostWindow,
);
// 首次挂载
this.#hostDocument.documentElement.appendChild(this.#panelElement);
// 启动守护者
this.#startGuardian();
}
hidePanelTrigger() {
// 隐藏时,必须停止守护者
this.#stopGuardian();
if (this.#panelElement) {
this.#panelElement.remove();
this.#panelElement = null;
}
this.#logger.log('Settings panel trigger hidden.');
}
/**
* 启动一个定时器,在一段时间内持续检查面板是否存在,如果被移除则重新挂载。
* @private
*/
#startGuardian() {
// 先确保之前的守护者已停止
this.#stopGuardian();
const checkInterval = 500; // 每500毫秒检查一次
const duration = 10000; // 守护10秒,足以覆盖绝大多数SPA的加载时间
let elapsedTime = 0;
this.#guardianIntervalId = setInterval(() => {
if (!this.#panelElement) {
// 如果面板被hidePanelTrigger正常移除了,就停止守护
this.#stopGuardian();
return;
}
// 检查我们的组件是否还在 html 元素中
if (!this.#hostDocument.documentElement.contains(this.#panelElement)) {
this.#logger.warn('Settings panel was removed from DOM. Re-appending...');
this.#hostDocument.documentElement.appendChild(this.#panelElement);
}
elapsedTime += checkInterval;
if (elapsedTime >= duration) {
this.#logger.log('Guardian period ended. Panel is stable.');
this.#stopGuardian(); // 停止检查,节省资源
}
}, checkInterval);
}
/**
* 停止守护定时器。
* @private
*/
#stopGuardian() {
if (this.#guardianIntervalId) {
clearInterval(this.#guardianIntervalId);
this.#guardianIntervalId = null;
}
}
/**
* 定义 <settings-panel> Web Component
*/
#defineSettingsPanelComponent(sanitizer) {
if (customElements.get('settings-panel')) return;
const APP_NAME = this.#appName;
class SettingsPanel extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._isOpen = false;
this._modules = [];
this._config = {};
this._configManager = null;
this._eventBus = null;
this._boundOpenPanel = this.openPanel.bind(this);
this._boundClosePanel = this.closePanel.bind(this);
this._eventsBound = false;
}
// 外部数据注入接口
setData(modules, config, configManager, eventBus, hostWindow) {
this._modules = modules;
this._config = config;
this._configManager = configManager;
this._eventBus = eventBus;
this._hostWindow = hostWindow;
this.render(); // 数据注入后重新渲染
}
connectedCallback() {
if (!this.shadowRoot.firstChild) {
this.render();
}
if (!this._eventsBound) {
this.#addEventListeners();
this._eventsBound = true;
}
this.#applyTheme();
}
/**
* 当组件从DOM中移除时被调用,这是清理内存的关键。
*/
disconnectedCallback() {
this.#removeEventListeners();
this._eventsBound = false;
}
/**
* 集中添加所有事件监听器。
* @private
*/
#addEventListeners() {
this.shadowRoot.querySelector('.trigger-btn').addEventListener('click', this._boundOpenPanel);
this.shadowRoot.querySelector('.overlay').addEventListener('click', this._boundClosePanel);
this.shadowRoot.querySelector('.drawer-content').addEventListener('change', this.#handleInputChange);
}
/**
* 集中移除所有事件监听器,防止内存泄漏。
* @private
*/
#removeEventListeners() {
const triggerBtn = this.shadowRoot.querySelector('.trigger-btn');
if (triggerBtn) triggerBtn.removeEventListener('click', this._boundOpenPanel);
const overlay = this.shadowRoot.querySelector('.overlay');
if (overlay) overlay.removeEventListener('click', this._boundClosePanel);
const content = this.shadowRoot.querySelector('.drawer-content');
if (content) content.removeEventListener('change', this.#handleInputChange);
}
/**
* input change 事件的统一处理函数。
* @private
*/
#handleInputChange = (e) => {
const target = e.target;
if (!target.dataset.configPath) return;
const path = target.dataset.configPath;
let value;
switch (target.type) {
case 'checkbox': value = target.checked; break;
case 'number': value = Number(target.value); break;
case 'text':
case 'select-one':
case 'color':
default: value = target.value; break;
}
this.handleConfigChange(path, value);
}
openPanel = () => {
if (this._isOpen) return;
this.#applyTheme();
this._isOpen = true;
this.shadowRoot.querySelector('.drawer').classList.add('open');
this.shadowRoot.querySelector('.overlay').classList.add('open');
this.shadowRoot.querySelector('.trigger-btn').classList.add('hidden');
}
closePanel = () => {
if (!this._isOpen) return;
this._isOpen = false;
this.shadowRoot.querySelector('.drawer').classList.remove('open');
this.shadowRoot.querySelector('.overlay').classList.remove('open');
this.shadowRoot.querySelector('.trigger-btn').classList.remove('hidden');
}
handleConfigChange = async (path, value) => {
await this._configManager.updateAndSave(path, value);
// 通知所有模块配置已更新
this._eventBus.emit('config-updated', {
path,
value,
newConfig: this._configManager.getConfig()
});
}
/**
* 检查系统颜色模式并为面板应用相应的主题。
* @private
*/
#applyTheme() {
const drawer = this.shadowRoot.querySelector('.drawer');
if (!drawer) return;
const isSystemDark = this._hostWindow.matchMedia('(prefers-color-scheme: dark)').matches;
if (isSystemDark) {
drawer.dataset.theme = 'dark';
} else {
drawer.dataset.theme = 'light';
}
}
render() {
const styles = `
:host {
position: fixed;
bottom: 25px;
right: 25px;
z-index: 9999;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
/* --- 1. 触发按钮 --- */
.trigger-btn {
width: 42px;
height: 42px;
border-radius: 12px 0 0 12px;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), background-color 0.2s;
opacity: 1;
visibility: visible;
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
position: fixed;
right: 0;
bottom: 50px;
}
.trigger-btn:hover {
transform: scale(1.1);
background-color: rgba(255, 255, 255, 0.3);
}
.trigger-btn .icon {
font-size: 24px;
color: #1d1d1f;
}
.trigger-btn.hidden {
opacity: 0;
visibility: hidden;
transform: scale(0.8);
pointer-events: none;
}
/* --- 2. 遮罩层 --- */
.overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.45);
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s;
cursor: pointer;
}
.overlay.open {
opacity: 1;
visibility: visible;
}
/* --- 3. 抽屉面板 --- */
.drawer {
position: fixed;
top: 0;
right: -100%;
width: 360px;
height: 100%;
display: flex;
flex-direction: column;
transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s, box-shadow 0.3s;
box-shadow: -4px 0 20px rgba(0,0,0,0.1);
--bg-primary: #f3f4f5;
--bg-secondary: #ffffff;
--bg-tertiary: #f7f8f9;
--bg-hover: #f7f8f9;
--bg-input: #f7f8f9;
--bg-input-focus: #ffffff;
--bg-switch: #e5e7eb;
--bg-switch-checked: #006ef4;
--text-primary: #14191e;
--text-secondary: #64696e;
--text-tertiary: #8c9196;
--text-placeholder: #b0b5b9;
--border-primary: #e5e7eb;
--border-secondary: #dadde0;
--border-focus: #006ef4;
--border-focus-shadow: rgba(0, 110, 244, 0.2);
--shadow-primary: -4px 0 20px rgba(0,0,0,0.1);
background-color: var(--bg-primary);
--border-focus-shadow: rgba(0, 110, 244, 0.2);
--select-arrow-svg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%2364696e' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}
.drawer[data-theme="dark"] {
--bg-primary: #1c1c1e;
--bg-secondary: #2c2c2e;
--bg-tertiary: #3a3a3c;
--bg-hover: #3a3a3c;
--bg-input: #3a3a3c;
--bg-input-focus: #1c1c1e;
--bg-switch: #3a3a3c;
--bg-switch-checked: #0a84ff;
--text-primary: #f5f5f7;
--text-secondary: #aeaeb2;
--text-tertiary: #8e8e93;
--text-placeholder: #636366;
--border-primary: #3a3a3c;
--border-secondary: #545458;
--border-focus: #0a84ff;
--border-focus-shadow: rgba(10, 132, 255, 0.2);
--shadow-primary: -4px 0 20px rgba(0,0,0,0.3);
--border-focus-shadow: rgba(10, 132, 255, 0.2);
--select-arrow-svg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23aeaeb2' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}
.drawer {
box-shadow: var(--shadow-primary);
}
.drawer.open {
right: 0;
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
flex-shrink: 0;
transition: background-color 0.3s, border-color 0.3s;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-secondary);
}
.drawer-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
transition: color 0.3s;
}
.drawer-content {
flex-grow: 1;
overflow-y: auto;
padding: 16px;
}
/* --- 4. 表单与布局 --- */
.details-wrapper {
border-radius: 8px;
margin-bottom: 12px;
overflow: hidden;
transition: background-color 0.3s, border-color 0.3s;
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
}
summary {
list-style: none;
display: block;
cursor: pointer;
padding: 16px 20px;
transition: background-color 0.2s;
}
summary::-webkit-details-marker {
display: none;
}
summary:hover {
background-color: var(--bg-hover);
}
.details-wrapper[noconfig] > summary {
cursor: default;
}
.details-wrapper[noconfig] > summary {
cursor: default;
}
.details-wrapper[open][noconfig] > .sub-items-container,
.sub-items-container:empty {
display: none;
}
.details-wrapper[open]:not([noconfig]) > summary {
border-bottom: 1px solid var(--border-primary);
}
.summary-content {
display: flex;
align-items: center;
margin-bottom: 0;
}
.form-info {
margin-left: 16px;
padding-top: 0;
flex: 1;
}
.form-info h4 {
margin: 0 0 4px;
font-size: 14px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
transition: color 0.3s;
color: var(--text-primary);
}
.form-info h4 span {
font-size: 12px;
margin-left: 8px;
transition: color 0.3s;
color: var(--text-tertiary);
}
.form-info p {
margin: 0;
font-size: 12px;
transition: color 0.3s;
color: var(--text-tertiary);
}
.sub-items-container {
padding: 16px 20px;
}
.sub-item {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: 16px;
}
.sub-item:last-child {
margin-bottom: 0;
}
.sub-item-label {
font-size: 14px;
flex: 1;
transition: color 0.3s;
color: var(--text-secondary);
}
.sub-item-info {
flex-grow: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.sub-item-desc {
font-size: 12px;
margin-top: 6px;
padding: 0;
transition: color 0.3s;
color: var(--text-tertiary);
p {
margin: 0;
line-height: 1.2;
}
}
/* --- 5. Switch 开关样式 --- */
.switch {
position: relative;
display: inline-block;
width: 40px;
height: 22px;
flex-shrink: 0;
}
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute;
cursor: pointer;
inset: 0;
border-radius: 22px;
transition: .3s;
background-color: var(--bg-switch);
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: white;
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
transition: .3s;
}
input:checked + .slider {
background-color: var(--bg-switch-checked);
}
input:checked + .slider:before {
transform: translateX(18px);
}
/* --- 6. Text/Number 输入框样式 --- */
.text-input {
appearance: none;
-webkit-appearance: none;
min-height: 30px;
width: 120px;
padding: 6px 10px;
border-radius: 6px;
font-size: 14px;
outline: none;
text-align: right;
box-sizing: border-box;
transition: all 0.2s ease-in-out;
border: 1px solid var(--border-secondary);
background-color: var(--bg-input);
color: var(--text-primary);
}
.text-input:hover {
border-color: var(--border-primary);
background-color: var(--bg-tertiary);
}
.text-input:focus {
border-color: var(--border-focus);
background-color: var(--bg-input-focus);
box-shadow: 0 0 0 3px var(--border-focus-shadow);
}
select.text-input {
text-align: left;
text-align-last: left;
padding-right: 30px; /* 为箭头留出空间 */
background-image: var(--select-arrow-svg);
background-repeat: no-repeat;
background-position: right 0.7rem center;
background-size: 0.9em 0.9em;
}
input[type="color"].text-input {
width: 50px;
min-height: 34px;
padding: 4px;
background-color: transparent;
}
input[type="color"].text-input::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"].text-input::-webkit-color-swatch {
border: none;
border-radius: 4px;
}
input[type="color"].text-input::-moz-color-swatch {
border: none;
border-radius: 4px;
}
/* --- 7. 其他辅助样式 --- */
.info-only { padding: 0 8px 8px; margin-bottom: 0; }
.info-text { font-size: 12px; text-align: center; width: 100%; margin: 0; transition: color 0.3s; color: var(--text-tertiary); }
.no-sub-config-text { font-size: 12px; text-align: center; padding: 8px 0; color: var(--text-placeholder); }
.main-divider { display: none; }
`;
const triggerButtonHTML = `<button class="trigger-btn">⚙️</button>`;
const drawerHTML = `
<div class="overlay"></div>
<div class="drawer">
<div class="drawer-header">
<h2>${APP_NAME} 设置</h2>
</div>
<div class="drawer-content">
${this.generateFormHTML()}
</div>
</div>
`;
const fullHTML = `
<style>${styles}</style>
${triggerButtonHTML}
${drawerHTML}
`;
sanitizer.setInnerHTML(this.shadowRoot, fullHTML);
this.#syncUIWithConfig();
}
generateFormHTML() {
let html = '';
html += `<div class="form-group info-only"><p class="info-text">所有设置修改后将自动保存,部分设置需刷新页面生效。</p></div>`;
for (const module of this._modules) {
const moduleConfig = this._config.modules[module.id];
if (!moduleConfig) continue;
const configKeys = Object.keys(module.defaultConfig);
const nonEnabledConfigKeys = configKeys.filter(key => key !== 'enabled');
const optionCount = nonEnabledConfigKeys.length;
html += `<details class="details-wrapper" ${optionCount === 0 ? 'noconfig' : ''}>
<summary>`;
html += `<div class="form-group summary-content">
<label class="switch">
<input type="checkbox" data-config-path="modules.${module.id}.enabled" data-needs-reload="true">
<span class="slider"></span>
</label>
<div class="form-info">
<h4>${module.name}<span>${optionCount === 0 ? "" : " ⚙️"}</span></h4>
<p>${module.description}</p>
</div>
</div></summary>`;
html += `<div class="sub-items-container">`;
if (nonEnabledConfigKeys.length === 0) {
html += ``;
} else {
for (const key of nonEnabledConfigKeys) {
const configItem = module.defaultConfig[key];
const value = moduleConfig[key];
const path = `modules.${module.id}.${key}`;
let inputHTML = '';
let labelText, controlType, itemDescription;
if (typeof configItem === 'object' && configItem !== null && 'value' in configItem) {
labelText = configItem.label || key;
controlType = configItem.type || 'string';
itemDescription = configItem.description || '';
} else {
labelText = key;
controlType = typeof configItem;
itemDescription = '';
}
switch (controlType) {
case 'boolean':
inputHTML = `<label class="switch"><input type="checkbox" data-config-path="${path}" ${value ? 'checked' : ''}><span class="slider"></span></label>`;
break;
case 'number':
inputHTML = `<input type="number" class="text-input" data-config-path="${path}" value="${value || 0}">`; // value: 如果不存在,默认为0
break;
case 'string':
inputHTML = `<input type="text" class="text-input" data-config-path="${path}" value="${value || ''}">`; // value: 如果不存在,默认为空字符串
break;
case 'select':
inputHTML = `<select class="text-input" data-config-path="${path}">`;
if (configItem.options && Array.isArray(configItem.options)) {
configItem.options.forEach(option => {
const selected = (option.value === value) ? 'selected' : '';
inputHTML += `<option value="${option.value}" ${selected}>${option.label || option.value}</option>`;
});
}
inputHTML += `</select>`;
break;
case 'color':
inputHTML = `<input type="color" class="text-input color-input" data-config-path="${path}" value="${value}">`;
break;
default:
html += `<div class="form-group sub-item"><span class="sub-item-label">${labelText}</span><p style="color:red;">(不支持的配置类型: ${controlType})</p></div>`;
continue; // 跳过不支持的类型
}
html += `
<div class="form-group sub-item">
<div class="sub-item-info">
<label class="sub-item-label">${labelText}</label>
${inputHTML}
</div>
${itemDescription ? `<div class="sub-item-desc"><p>${itemDescription}</p></div>` : ''}
</div>`;
}
}
html += `</div></details><hr class="main-divider"/>`;
}
if (html.endsWith('<hr class="module-divider"/>')) {
html = html.slice(0, -28);
}
return html;
}
/**
* 遍历所有带 data-config-path 的输入框,并根据 this._config 设置其状态。
* @private
*/
#syncUIWithConfig() {
this.shadowRoot.querySelectorAll('[data-config-path]').forEach(input => {
const path = input.dataset.configPath;
const keys = path.split('.');
let value = this._config;
for (const key of keys) {
if (value === undefined || value === null) break;
value = value[key];
}
if (value === undefined || value === null) return;
switch (input.type) {
case 'checkbox':
input.checked = Boolean(value);
break;
case 'number':
case 'text':
case 'color':
case 'select-one':
input.value = value;
break;
}
});
}
}
customElements.define('settings-panel', SettingsPanel);
}
}
/**
* @class ModuleAuditor - (仅在Debug模式下激活) 模块审计员
*
* 负责监控模块的副作用(事件监听、定时器),并在模块禁用后报告任何未被清理的资源泄漏。
*/
class ModuleAuditor {
#logger;
#hostWindow;
#originalAddEventListener;
#originalRemoveEventListener;
#originalSetInterval;
#originalClearInterval;
#originalSchedulerRegister;
#originalSchedulerUnregister;
#activeModuleId = null;
#trackedResources = new Map();
constructor(logger, hostWindow, SAFE_APP_NAME) {
this.#logger = logger.createTaggedLogger('Auditor', { backgroundColor: '#9C27B0' });
this.#hostWindow = hostWindow;
this.#patchGlobalApis(SAFE_APP_NAME);
this.#logger.warn('Module Auditor is active. Resource leakage will be reported.');
}
/**
* 在 scheduler 实例创建后,由 Kernel 调用,用于代理其方法。
* @param {DomBatchProcessorInstance} schedulerInstance
*/
patchScheduler(schedulerInstance) {
if (!schedulerInstance) return;
this.#originalSchedulerRegister = schedulerInstance.register;
this.#originalSchedulerUnregister = schedulerInstance.unregister;
const self = this;
schedulerInstance.register = function (selector, callback, options) {
const taskId = self.#originalSchedulerRegister.call(this, selector, callback, options);
if (self.#activeModuleId) {
const resources = self.#initializeTracking(self.#activeModuleId);
resources.schedulers.add(taskId);
}
return taskId;
};
schedulerInstance.unregister = function (taskId) {
// 全局查找并删除,因为 unregister 可能在模块上下文之外被调用
for (const resources of self.#trackedResources.values()) {
if (resources.schedulers.has(taskId)) {
resources.schedulers.delete(taskId);
break;
}
}
return self.#originalSchedulerUnregister.call(this, taskId);
};
this.#logger.log('Scheduler has been patched for auditing.');
}
/**
* 在调用模块的 onEnable/onDisable 之前调用,设置审计上下文。
* @param {string} moduleId - 正在被审计的模块ID。
*/
auditStart(moduleId) {
this.#activeModuleId = moduleId;
}
/**
* 在调用模块的 onEnable/onDisable 之后调用,清除审计上下文。
*/
auditEnd() {
this.#activeModuleId = null;
}
/**
* 在模块被禁用后,运行泄漏检查。
* @param {string} moduleId - 已被禁用的模块ID。
*/
runChecks(moduleId) {
const resources = this.#trackedResources.get(moduleId);
if (!resources) return;
let leaksFound = 0;
// 修正: 检查事件监听器泄漏
if (resources.events.size > 0) {
leaksFound += resources.events.size;
resources.events.forEach((types, target) => {
this.#logger.error(`LEAK DETECTED in module '${moduleId}': Event listener(s) for type(s) [${[...types].join(', ')}] were NOT removed from element:`, target);
});
}
// 检查定时器泄漏
if (resources.intervals.size > 0) {
leaksFound += resources.intervals.size;
resources.intervals.forEach((id) => {
this.#logger.error(`LEAK DETECTED in module '${moduleId}': A setInterval (ID: ${id}) was NOT cleared.`);
});
}
// 检查调度器任务泄漏
if (resources.schedulers.size > 0) {
leaksFound += resources.schedulers.size;
resources.schedulers.forEach((id) => {
this.#logger.error(`LEAK DETECTED in module '${moduleId}': A scheduler task (ID: ${id.toString()}) was NOT unregistered.`);
});
}
if (leaksFound === 0) {
this.#logger.log(`Module '${moduleId}' passed audit. All tracked resources were cleaned up.`);
}
this.#trackedResources.delete(moduleId);
}
#initializeTracking(moduleId) {
if (!this.#trackedResources.has(moduleId)) {
this.#trackedResources.set(moduleId, {
events: new Map(),
intervals: new Set(),
schedulers: new Set()
});
}
return this.#trackedResources.get(moduleId);
}
#patchGlobalApis(SAFE_APP_NAME) {
this.#originalAddEventListener = EventTarget.prototype.addEventListener;
this.#originalRemoveEventListener = EventTarget.prototype.removeEventListener;
this.#originalSetInterval = this.#hostWindow.setInterval;
this.#originalClearInterval = this.#hostWindow.clearInterval;
const self = this;
const namespace = `__CALIBER_${SAFE_APP_NAME}`;
const responseEventName = `${namespace}_RESPONSE`;
// --- Patch addEventListener ---
EventTarget.prototype.addEventListener = function (type, listener, options) {
if (this === self.#hostWindow.document && type === responseEventName) {
// 直接调用原始方法,不进行任何追踪
return self.#originalAddEventListener.call(this, type, listener, options);
}
if (self.#activeModuleId) {
const resources = self.#initializeTracking(self.#activeModuleId);
if (!resources.events.has(this)) {
resources.events.set(this, new Set());
}
resources.events.get(this).add(type);
}
return self.#originalAddEventListener.call(this, type, listener, options);
};
// --- Patch removeEventListener ---
EventTarget.prototype.removeEventListener = function (type, listener, options) {
for (const resources of self.#trackedResources.values()) {
if (resources.events.has(this)) {
const types = resources.events.get(this);
types.delete(type);
if (types.size === 0) {
resources.events.delete(this);
}
break;
}
}
return self.#originalRemoveEventListener.call(this, type, listener, options);
};
// --- Patch setInterval ---
this.#hostWindow.setInterval = function (handler, timeout) {
const intervalId = self.#originalSetInterval.call(this, handler, timeout);
if (self.#activeModuleId) {
const resources = self.#initializeTracking(self.#activeModuleId);
resources.intervals.add(intervalId);
}
return intervalId;
};
// --- Patch clearInterval ---
this.#hostWindow.clearInterval = function (id) {
// 全局查找并删除,因为 clearInterval 可能在模块上下文之外被调用
for (const resources of self.#trackedResources.values()) {
if (resources.intervals.has(id)) {
resources.intervals.delete(id);
break;
}
}
return self.#originalClearInterval.call(this, id);
};
}
}
/**
* @class DomBatchProcessor - 高性能DOM批量处理调度器
*/
class DomBatchProcessor {
#observer = null;
#taskQueue = [];
#registeredTasks = new Map();
#isLoopRunning = false;
#isObserving = false;
#batchSize;
#logger;
#hostDocument;
/**
* @param {number} batchSize - 在每个渲染帧中处理的最大任务数。
* @param {LoggerInstance} logger - 用于日志输出的 logger 对象。
* @param {Document} hostDocument - 宿主 document 对象。
*/
constructor(batchSize, logger, hostDocument) {
this.#batchSize = batchSize || 20;
this.#logger = logger;
this.#hostDocument = hostDocument;
// 只创建 MutationObserver 实例,不立即启动监听(懒启动)
this.#observer = new MutationObserver(this.#handleMutations);
}
/**
* 注册一个DOM处理任务。
* @param {string} selector - 用于匹配节点的CSS选择器。
* @param {(node: HTMLElement) => void} callback - 匹配到节点时要执行的回调函数。
* @param {object} [options] - (可选) 监听选项。
* @param {boolean} [options.add=true] - 是否监听节点的添加。
* @param {boolean} [options.attributes=false] - 是否监听节点属性的变化。
* @param {string[]} [options.attributeFilter] - (可选) 只监听特定属性的变化。
* @param {HTMLElement | string} [options.root] - (可选) 任务的根节点或根选择器。只有当DOM变动发生在此根节点内部时,任务才会被匹配。
* @param {boolean} [options.processExisting=false] - (可选) 是否在注册时立即处理DOM中已存在的匹配节点。
* @returns {Symbol} 一个唯一的任务ID,用于后续注销。
*/
register(selector, callback, options = {}) {
const taskId = Symbol(selector);
this.#registeredTasks.set(taskId, {
selector,
callback,
options: { add: true, ...options }
});
this.#logger.log(`Task registered for selector "${selector}"`, options.root ? `within root "${options.root}"` : '');
if (options.processExisting) {
const rootNode = (typeof options.root === 'string'
? this.#hostDocument.querySelector(options.root)
: options.root) || this.#hostDocument;
const existingNodes = rootNode.querySelectorAll(selector);
if (existingNodes.length > 0) {
this.#logger.log(`Explicitly processing ${existingNodes.length} existing node(s) for selector "${selector}".`);
existingNodes.forEach(node => {
// 直接推入队列,由 rAF 循环处理
this.#taskQueue.push({ node, callback: callback });
});
// 确保处理循环启动
this.#startLoop();
}
}
this.#updateObserver();
return taskId;
}
/**
* 注销一个DOM处理任务。
* @param {Symbol} taskId - 注册时返回的任务ID。
*/
unregister(taskId) {
if (this.#registeredTasks.has(taskId)) {
const selector = this.#registeredTasks.get(taskId).selector;
this.#registeredTasks.delete(taskId);
this.#logger.log(`Task for selector "${selector}" unregistered.`);
this.#updateObserver();
}
}
/**
* 根据当前所有注册任务的需求,动态计算并应用最优的 MutationObserver 配置。
* 这是实现懒启动、自动休眠和动态功能切换的核心。
* @private
*/
#updateObserver() {
if (this.#isObserving) {
this.#observer.disconnect();
this.#isObserving = false;
}
// 如果没有任何任务,则保持休眠状态
if (this.#registeredTasks.size === 0) {
this.#logger.log('Observer stopped as no tasks are registered.');
return;
}
let needsChildList = false;
let needsAttributes = false;
const attributeFilterSet = new Set();
let useAttributeFilter = true;
// 遍历所有任务,聚合出最小的配置需求
for (const task of this.#registeredTasks.values()) {
if (task.options.add) needsChildList = true;
if (task.options.attributes) {
needsAttributes = true;
if (task.options.attributeFilter && Array.isArray(task.options.attributeFilter)) {
task.options.attributeFilter.forEach(attr => attributeFilterSet.add(attr));
} else {
// 只要有一个任务需要监听属性但未提供 filter,就不能启用 filter 优化
useAttributeFilter = false;
}
}
}
const observerConfig = { subtree: true };
if (needsChildList) observerConfig.childList = true;
if (needsAttributes) {
observerConfig.attributes = true;
// 仅当所有需要监听属性的任务都指定了 filter 时,才应用 filter
if (useAttributeFilter && attributeFilterSet.size > 0) {
observerConfig.attributeFilter = Array.from(attributeFilterSet);
}
}
// 只有在需要监听任何东西的情况下才启动观察
if (observerConfig.childList || observerConfig.attributes) {
this.#observer.observe(this.#hostDocument.documentElement, observerConfig);
this.#isObserving = true;
this.#logger.log('Observer (re)started with new config:', observerConfig);
}
}
/**
* MutationObserver的回调,负责过滤和排队任务。
* @private
*/
#handleMutations = (mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const addedNode of mutation.addedNodes) {
// 只处理元素节点,忽略文本节点等
if (addedNode.nodeType === Node.ELEMENT_NODE) {
this.#queueMatchingTasks(addedNode, 'add');
// 同时检查新增节点下的所有子元素是否也匹配
const descendants = addedNode.querySelectorAll('*');
for (const descendant of descendants) {
this.#queueMatchingTasks(descendant, 'add');
}
}
}
} else if (mutation.type === 'attributes') {
if (mutation.target.nodeType === Node.ELEMENT_NODE) {
this.#queueMatchingTasks(mutation.target, 'attributes', mutation.attributeName);
}
}
}
// 只要队列中有任务,就确保 rAF 循环在运行
if (this.#taskQueue.length > 0) {
this.#startLoop();
}
}
/**
* 辅助函数:检查一个节点是否匹配任何已注册的任务,并将其排队。
* @private
*/
#queueMatchingTasks(node, mutationType, attributeName = null) {
for (const task of this.#registeredTasks.values()) {
// --- 根节点靶向检查 ---
if (task.options.root) {
const rootNode = typeof task.options.root === 'string'
? this.#hostDocument.querySelector(task.options.root)
: task.options.root;
// 如果根节点不存在,或者变化节点不在根节点内部,则直接跳过此任务
if (!rootNode || !rootNode.contains(node)) {
continue;
}
}
// 检查任务是否订阅了此类变更,并且节点是否匹配选择器
if (task.options[mutationType] && node.matches(task.selector)) {
// 对于属性变更,额外检查 attributeFilter
if (mutationType === 'attributes' && task.options.attributeFilter) {
if (!task.options.attributeFilter.includes(attributeName)) {
continue; // 属性不匹配,跳过当前任务
}
}
this.#taskQueue.push({ node, callback: task.callback });
}
}
}
/**
* 启动 rAF 循环。
* @private
*/
#startLoop() {
if (this.#isLoopRunning) return;
this.#isLoopRunning = true;
requestAnimationFrame(this.#processQueue);
}
/**
* rAF 循环的核心,负责分批处理任务。
* @private
*/
#processQueue = () => {
const batch = this.#taskQueue.splice(0, this.#batchSize);
for (const task of batch) {
try {
// 在执行回调前,最好检查一下节点是否还在DOM中
if (this.#hostDocument.documentElement.contains(task.node)) {
task.callback(task.node);
}
} catch (e) {
this.#logger.error('Error in DomBatchProcessor task callback:', e);
}
}
if (this.#taskQueue.length > 0) {
requestAnimationFrame(this.#processQueue);
} else {
this.#isLoopRunning = false;
}
}
}
/**
* @class LoggerService - 专用的、可派生的日志服务
*/
class LoggerService {
#isDebug;
#baseTagStyle = `color: white; padding: 2px 6px; border-radius: 4px; font-weight: bold;`;
constructor(isDebug) {
this.#isDebug = Boolean(isDebug);
}
/**
* (私有辅助函数) 创建一组核心的日志方法。
* @private
*/
#createLogMethodsFor(tag, styles) {
return {
log: (message, ...args) => this.#isDebug && console.log(`%c${tag}`, styles.log, message, ...args),
warn: (message, ...args) => this.#isDebug && console.warn(`%c${tag}`, styles.warn, message, ...args),
error: (message, ...args) => console.error(`%c${tag}`, styles.error, message, ...args),
};
}
/**
* 创建一个主 logger 实例。
*/
createMainLogger(appName) {
const styles = {
log: `background-color: #0057b8; ${this.#baseTagStyle}`,
warn: `background-color: #ff9800; color: black; ${this.#baseTagStyle}`,
error: `background-color: #f44336; ${this.#baseTagStyle}`,
};
const mainLogger = this.#createLogMethodsFor(appName, styles);
mainLogger.createTaggedLogger = (tag, styleOptions = {}) => {
const taggedStyles = {
log: `background-color: ${styleOptions.backgroundColor || '#757575'}; color: ${styleOptions.color || 'white'}; ${this.#baseTagStyle}`,
warn: `background-color: ${styleOptions.backgroundColor || '#757575'}; color: ${styleOptions.color || 'white'}; ${this.#baseTagStyle}`,
error: `background-color: #f44336; color: white; ${this.#baseTagStyle}`, // 错误总是红色
};
return this.#createLogMethodsFor(tag, taggedStyles);
};
return mainLogger;
}
}
/**
* 框架内部工具
*/
const _CaliberInternals = {
/**
* @private
* [上下文创建器] 从应用名称派生出所有需要的上下文状态。
* @param {string} appName - 原始的应用名称。
* @returns {{safeAppName: string, instanceKey: string}} 包含派生状态的上下文对象。
*/
_createAppContext: (appName) => {
const utf8Bytes = new TextEncoder().encode(appName);
let binaryString = '';
utf8Bytes.forEach(byte => {
binaryString += String.fromCharCode(byte);
});
let safeAppName = btoa(binaryString);
safeAppName = safeAppName.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const instanceKey = `CALIBER_INSTANCE_${safeAppName}`;
return { safeAppName, instanceKey };
},
/**
* @private
* [预检器] 对应用配置和运行环境执行所有预检。
* @param {object} options - createApp 接收的原始选项。
* @param {string} instanceKey - 由 _createAppContext 生成的实例键。
* @param {object} logger - 用于报告错误的主 logger。
* @param {Window} hostWindow - 宿主页面的 window 对象。
* @returns {boolean} - 所有检查通过返回 true,否则返回 false。
*/
runPreflightChecks: (options, instanceKey, logger, hostWindow) => {
const { appName, modules, services } = options || {};
if (!options || typeof options !== 'object' || !appName || typeof appName !== 'string' || !Array.isArray(modules) || !services || !services.storage || !services.command) {
logger.error('Preflight check failed: Invalid configuration object.');
return false;
}
if (modules.length === 0) {
if (options.isDebug) logger.log(`Preflight check skipped: No modules provided.`);
return false;
}
if (hostWindow[instanceKey]) {
logger.warn('Preflight check failed: Script instance already running.');
return false;
}
return true;
},
/**
* 初始化所有框架核心服务。
* @param {object} options - createApp 接收的原始选项。
* @param {{safeAppName: string}} context - 部分上下文,包含 safeAppName。
* @param {LoggerInstance} logger - 主 logger。
* @param {Window} hostWindow - 宿主页面的 window 对象。
* @param {Document} hostDocument - 宿主页面的 document 对象。
* @param {DOMSanitizerInstance} sanitizer - DOM净化服务实例。
* @returns {{eventBus: EventBusInstance, framework: FrameworkServices}} - 包含事件总线和框架服务集合的对象。
*/
initializeCoreServices: (options, context, logger, hostWindow, hostDocument, sanitizer) => {
const eventBus = _CaliberInternals.createEventBus(logger);
const schedulerLogger = logger.createTaggedLogger('Scheduler', { backgroundColor: '#4CAF50' });
const frameworkServices = {
scheduler: new DomBatchProcessor(options.framework?.domProcessorBatchSize, schedulerLogger, hostDocument),
interceptor: _CaliberInternals.createFetchInterceptor(logger, hostWindow, hostDocument, context.safeAppName, options.isDebug, sanitizer),
sanitizer: sanitizer,
executor: _CaliberInternals.createPageScopeExecutor(logger, context.safeAppName, sanitizer, hostDocument),
};
_CaliberInternals.patchHistoryForNavigation(eventBus, hostWindow);
return { eventBus, framework: frameworkServices };
},
/**
* @private
* 核心匹配引擎 - 检查给定的匹配规则是否与当前页面URL匹配。
* @param {string|RegExp|Array<string|RegExp>|null|undefined} matchRule - 匹配规则。
* @param {Window} hostWindow - 宿主 window 对象。
* @returns {object|false} - 不匹配返回false,匹配返回 { params, query }。
*/
_checkMatch: (matchRule, hostWindow) => {
const currentUrl = new URL(hostWindow.location.href);
// 处理查询参数
const query = {};
const searchParams = currentUrl.searchParams;
for (const [key, value] of searchParams.entries()) {
const existing = query[key];
if (existing !== undefined) {
query[key] = Array.isArray(existing)
? [...existing, value]
: [existing, value];
} else {
query[key] = value;
}
}
// 路径规范化
const rawPathname = currentUrl.pathname;
const pathname = rawPathname.endsWith('/') && rawPathname.length > 1
? rawPathname.slice(0, -1)
: rawPathname;
const href = currentUrl.href;
// 空规则快速返回
if (matchRule == null) {
return { params: {}, query };
}
// 规则数组处理
const rules = Array.isArray(matchRule) ? matchRule : [matchRule];
// 核心匹配逻辑
for (const rule of rules) {
const result = checkRule(rule);
if (result) return result;
}
return false;
// 辅助函数保持内部作用域
function checkRule(rule) {
if (rule == null) return { params: {}, query };
if (!rule) return false;
// 正则表达式规则
if (rule instanceof RegExp) {
const match = rule.exec(pathname) || rule.exec(href);
return match
? { params: match.groups || {}, query }
: false;
}
// 字符串规则
if (typeof rule === 'string') {
let isAbsolute = false;
let rulePath = rule;
let ruleProtocol = '';
let ruleHost = '';
try {
const urlObj = new URL(rule);
isAbsolute = true;
ruleProtocol = urlObj.protocol;
ruleHost = urlObj.host;
rulePath = urlObj.pathname;
} catch { }
if (isAbsolute) {
if (ruleProtocol !== currentUrl.protocol || ruleHost !== currentUrl.host) {
return false;
}
}
// 路径规范化
const normalizedRule = rulePath.endsWith('/') && rulePath.length > 1
? rulePath.slice(0, -1)
: rulePath;
// 快速前缀匹配(无参数路径)
if (pathname === normalizedRule || pathname.startsWith(normalizedRule + '/')) {
return { params: {}, query };
}
// 参数化路径匹配
return matchParamPath(normalizedRule);
}
return false;
}
// 参数化路径匹配
function matchParamPath(pattern) {
// 检查是否需要参数匹配
const hasParams = pattern.includes(':') || pattern.includes('*');
if (!hasParams) return false;
// 构建正则表达式
const parts = pattern.split('/').slice(1);
let regexStr = '^';
const paramNames = [];
let hasWildcard = false;
for (const part of parts) {
if (hasWildcard) return false; // 通配符后不能有其他部分
if (part.startsWith(':')) {
const isOptional = part.endsWith('?');
const name = isOptional ? part.slice(1, -1) : part.slice(1);
paramNames.push(name);
regexStr += isOptional ? '(?:/([^/]+))?' : '/([^/]+)';
}
else if (part === '*') {
paramNames.push('_');
regexStr += '(?:/(.*))?';
hasWildcard = true;
}
else {
regexStr += '/' + part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
regexStr += '$';
const regex = new RegExp(regexStr);
const match = regex.exec(pathname);
if (match) {
const params = {};
for (let i = 0; i < paramNames.length; i++) {
params[paramNames[i]] = match[i + 1] ?? undefined;
}
return { params, query };
}
return false;
}
},
/**
* 创建一个事件总线实例。 (工厂职责)
* @param {object} logger - 用于错误报告的logger实例。
*/
createEventBus: (logger) => {
const listeners = new Map();
const log = logger || console;
return {
on: (eventName, callback) => {
if (!listeners.has(eventName)) {
listeners.set(eventName, []);
}
listeners.get(eventName).push(callback);
},
off: (eventName, callback) => {
if (listeners.has(eventName)) {
const eventListeners = listeners.get(eventName);
const index = eventListeners.indexOf(callback);
if (index > -1) {
eventListeners.splice(index, 1);
}
}
},
emit: (eventName, data) => {
if (listeners.has(eventName)) {
[...listeners.get(eventName)].forEach(callback => {
try {
callback(data);
} catch (e) {
log.error(`[EventBus] Error in callback for event "${eventName}":`, e);
}
});
}
}
};
},
/**
* 代理 history API 以感知SPA导航。 (配置器职责)
* @param {object} bus - 事件总线实例。
*/
patchHistoryForNavigation: (bus, hostWindow) => {
const history = hostWindow.history;
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
bus.emit('navigate');
};
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
bus.emit('navigate');
};
hostWindow.addEventListener('popstate', () => bus.emit('navigate'));
},
/**
* @private
* [服务工厂] 创建一个通用的DOM净化与安全注入服务。
* 这是框架中所有CSP/Trusted-Types对抗策略的唯一来源。
* @param {object} logger - 用于报告错误的 logger 实例。
* @returns {object} DOMSanitizer 服务实例。
*/
createDOMSanitizer: (logger) => {
let _policy = undefined; // 使用闭包缓存 Trusted Types 策略
const _getPolicy = () => {
if (_policy === undefined) {
_policy = null;
if (window.trustedTypes && window.trustedTypes.createPolicy) {
try {
_policy = window.trustedTypes.createPolicy('CaliberUniversalPolicy#html', {
createHTML: s => s,
createScript: s => s,
});
} catch (e) {
if (window.trustedTypes.defaultPolicy) _policy = window.trustedTypes.defaultPolicy;
}
}
}
return _policy;
};
const service = {
/**
* [核心] 创建一个TrustedHTML对象(如果策略可用)。
* @param {string} htmlString - 要处理的HTML字符串。
* @returns {TrustedHTML|string} 返回TrustedHTML对象或原始字符串。
*/
createTrustedHTML(htmlString) {
const policy = _getPolicy();
return policy ? policy.createHTML(htmlString) : htmlString;
},
/**
* [便捷方法] 安全地设置一个元素的 innerHTML。
* @param {Element} element - 目标元素。
* @param {string} htmlString - 要设置的HTML字符串。
*/
setInnerHTML(element, htmlString) {
try {
element.innerHTML = this.createTrustedHTML(htmlString);
} catch (e) {
logger.error('setInnerHTML failed due to CSP.', `Error: ${e.message}`);
}
},
/**
* [便捷方法] 安全地注入脚本。
* @param {Document} doc - 目标文档。
* @param {string} codeString - 脚本字符串。
*/
injectScript(doc, codeString) {
try {
const script = doc.createElement('script');
const policy = _getPolicy();
if (policy) script.textContent = policy.createScript(codeString);
else script.textContent = codeString;
(doc.head || doc.documentElement).prepend(script);
script.remove();
} catch (e) {
logger.error('Script injection failed due to CSP.', `Error: ${e.message}`);
}
},
/**
* [便捷方法] 安全地注入样式。
* @param {Document} doc - 目标文档。
* @param {string} cssString - 样式字符串。
* @param {string} id - 样式元素的ID。
* @returns {HTMLStyleElement|null} 创建的元素或null。
*/
injectStyle(doc, cssString, id) {
try {
const style = doc.createElement('style');
style.dataset.caliberId = id;
style.innerHTML = this.createTrustedHTML(cssString);
doc.head.appendChild(style);
return style;
} catch (e) {
logger.error('Style injection failed due to CSP.', `Error: ${e.message}`);
return null;
}
}
};
return service;
},
/**
* 网络请求拦截器服务。
* 创建一个通用的拦截器服务,它通过安全的脚本注入来代理原生fetch,并使用CustomEvent将响应数据传回沙箱。
*
* @param {object} logger - 框架的主 logger 实例。
* @param {Window} hostWindow - 宿主页面的 window 对象。
* @param {Document} hostDocument - 宿主页面的 Document 对象。
* @param {string} safeAppName - 当前应用名称,用于生成唯一的命名空间。
* @param {boolean} isDebug - 是否为调试模式,用于控制注入脚本的日志输出。
* @param {DOMSanitizerInstance} sanitizer - DOM净化与安全注入服务实例。
* @returns {FetchInterceptorInstance} 服务对象。
*/
createFetchInterceptor: (logger, hostWindow, hostDocument, safeAppName, isDebug, sanitizer) => {
const namespace = `__CALIBER_${safeAppName}`;
const patchFlag = `${namespace}_PATCHED`;
const hooksRegistry = `${namespace}_HOOKS`;
const responseEventName = `${namespace}_RESPONSE`;
const responseCallbacks = new Map();
let listenerRefCount = 0;
// 事件处理函数
const _handleInjectedEvent = (event) => {
const { path, responseData } = event.detail;
const pathKey = JSON.stringify(path);
if (responseCallbacks.has(pathKey)) {
try {
responseCallbacks.get(pathKey)(responseData);
} catch (e) {
logger.error(`[FetchInterceptor] Error in response callback for path [${path.join('/')}]`, e);
}
}
};
// 统一处理路径验证和转换
const _validateAndTransformPath = (path, methodName) => {
if (typeof path === 'string' && path) return [path];
if (Array.isArray(path) && path.length > 0) return path;
logger.error(`[FetchInterceptor] ${methodName} failed: path must be a non-empty array or a non-empty string.`);
return null;
};
const service = {
/**
* 根据配置对象构建一个 fetch 钩子函数的字符串。
* 这是一个便捷的“填空题”工具,用于简化 addHook 的使用。
* @param {object} options - 钩子配置。
* @param {string} options.targetUrl - 必须完全匹配的目标URL (origin + pathname)。
* @param {string} [options.method='GET'] - (可选) 匹配的HTTP方法 (大小写不敏感)。
* @param {string} options.handler - 在匹配成功后,要执行的核心逻辑的函数体字符串。
* 在此字符串中,你可以使用 `urlObject` 和 `config` 这两个变量。
* 它必须返回一个 `{ url: string, config: object }` 或 `undefined`。
* @returns {string} - 一个完整的、自包含的、可注入的钩子函数字符串。
*/
createHook({ targetUrl, method = 'GET', handler }) {
if (!targetUrl || !handler) {
logger.error(`[FetchInterceptor.createHook] failed: 'targetUrl' and 'handler' are required.`);
return `() => {}`;
}
const template = `
(url, config) => {
const TARGET_URL = '${targetUrl}';
const TARGET_METHOD = '${method.toUpperCase()}';
try {
if (config.method && config.method.toUpperCase() !== TARGET_METHOD) return;
const urlObject = new URL(url);
if (urlObject.origin + urlObject.pathname !== TARGET_URL) return;
const result = (() => { ${handler} })();
return result;
} catch (e) { /* ignore errors */ }
}`;
return template;
},
/**
* 添加一个网络请求钩子,并注册一个用于处理响应的回调。
* @param {string[]|string} path - 钩子的唯一路径。
* @param {string} hookFunctionString - 修改请求的钩子函数字符串。
* @param {(responseData: any) => void} responseCallback - 接收响应数据的回调函数。
*/
addHookWithResponse(path, hookFunctionString, responseCallback) {
const finalPath = _validateAndTransformPath(path, 'addHookWithResponse');
if (!finalPath) return;
if (typeof responseCallback !== 'function') {
logger.error('[FetchInterceptor] addHookWithResponse failed: responseCallback must be a function.');
return;
}
const pathKey = JSON.stringify(finalPath);
// 如果是新注册的回调,增加引用计数
if (!responseCallbacks.has(pathKey)) {
if (listenerRefCount === 0) {
hostDocument.addEventListener(responseEventName, _handleInjectedEvent);
if (isDebug) logger.log(`[FetchInterceptor] Global response listener attached for event: ${responseEventName}`);
}
listenerRefCount++;
}
responseCallbacks.set(pathKey, responseCallback);
this.addHook(finalPath, hookFunctionString, true);
},
/**
* 添加或更新一个网络请求钩子。
* @param {string[]|string} path - 钩子路径。
* @param {string} hookFunctionString - 钩子函数字符串。
* @param {boolean} [awaitsResponse=false] - (内部) 标记此钩子是否需要返回响应。
*/
addHook(path, hookFunctionString, awaitsResponse = false) {
const finalPath = _validateAndTransformPath(path, 'addHook');
if (!finalPath || !hookFunctionString) {
if (!hookFunctionString) logger.error('[FetchInterceptor] addHook failed: hookFunctionString is required.');
return;
}
const pathJson = JSON.stringify(finalPath);
const injectionCode = `
(() => {
const doLog = ${isDebug};
if (!window['${patchFlag}']) {
window['${patchFlag}'] = true;
window['${hooksRegistry}'] = {};
const originalFetch = window.fetch;
const RESPONSE_EVENT_NAME = '${responseEventName}';
const executeHooks = (node, url, config, currentPath = []) => {
let matchedPath = null;
if (typeof node === 'object' && node !== null) {
for (const key in node) {
if (Object.prototype.hasOwnProperty.call(node, key)) {
const newPath = [...currentPath, key];
const hookResult = executeHooks(node[key], url, config, newPath);
url = hookResult.url;
config = hookResult.config;
if (hookResult.matchedPath) {
matchedPath = hookResult.matchedPath;
break;
}
}
}
} else if (typeof node === 'function') {
try {
const result = node(url, config);
if (result && result.url && result.config) {
url = result.url;
config = result.config;
matchedPath = currentPath;
}
} catch (e) { if (doLog) console.error('[Caliber Hook Error]', e); }
}
return { url, config, matchedPath };
};
window.fetch = async function(...args) {
let resource = args[0], config = args[1] || {}, url = new URL(String(resource), window.location.origin).toString();
const { url: finalUrl, config: finalConfig, matchedPath } = executeHooks(window['${hooksRegistry}'], url, config);
args[0] = finalUrl;
args[1] = finalConfig;
const response = await originalFetch.apply(this, args);
if (matchedPath) {
const responseClone = response.clone();
responseClone.text().then(text => {
let responseData;
try {
responseData = JSON.parse(text);
} catch (e) {
responseData = text;
}
const event = new CustomEvent(RESPONSE_EVENT_NAME, {
detail: { path: matchedPath, responseData }
});
document.dispatchEvent(event);
}).catch(e => {
if (doLog) console.warn('[Caliber Interceptor] Could not read response body for hooked request.', e);
});
}
return response;
};
if (doLog) console.warn('[Caliber] Injected fetch interceptor is active.');
}
const setByPath = (obj, p, val) => {
const last = p.pop();
let node = obj;
p.forEach(k => { node = (node[k] = (typeof node[k] === 'object' && node[k] !== null) ? node[k] : {}); });
node[last] = val;
};
setByPath(window['${hooksRegistry}'], ${pathJson}, (${hookFunctionString}));
if (${isDebug}) console.log(\`[Caliber] Hook at path [${finalPath.join('/')}] added/updated. Awaits response: ${awaitsResponse}\`);
})();
`;
sanitizer.injectScript(hostDocument, injectionCode);
},
/**
* 从注入的钩子注册表中移除一个钩子或分支。
* @param {string[]|string} path - 要移除的钩子或分支的路径。
*/
removeHook(path) {
const finalPath = _validateAndTransformPath(path, 'removeHook');
if (!finalPath) return;
const pathKey = JSON.stringify(finalPath);
// 如果确实存在一个回调被移除,减少引用计数
if (responseCallbacks.has(pathKey)) {
responseCallbacks.delete(pathKey);
listenerRefCount--;
if (listenerRefCount === 0) {
hostDocument.removeEventListener(responseEventName, _handleInjectedEvent);
if (isDebug) logger.log(`[FetchInterceptor] Global response listener removed as no hooks are active.`);
}
if (isDebug) logger.log(`[FetchInterceptor] Response callback for path [${finalPath.join('/')}] removed.`);
}
const pathJson = JSON.stringify(finalPath);
const removalCode = `
(() => {
const registry = window['${hooksRegistry}'];
if (!registry) return;
const path = ${pathJson};
let parent = registry;
for (let i = 0; i < path.length - 1; i++) {
if (typeof parent[path[i]] === 'undefined') return;
parent = parent[path[i]];
}
delete parent[path[path.length - 1]];
if (${isDebug}) console.log(\`[Caliber] Hook or branch at path [${finalPath.join('/')}] removed.\`);
})();
`;
sanitizer.injectScript(hostDocument, removalCode);
},
/**
* 启动一个链式调用来创建和注册一个拦截器。
* @param {string|{url: string, method?: string, match?: string|RegExp|Array<string|RegExp>}} urlOrOptions - 目标URL或一个包含URL、方法和页面匹配规则的对象。
* @returns {object} 一个包含 .onRequest(), .onResponse(), .register() 的构建器对象。
*/
target(urlOrOptions) {
const builder = {
_targetConfig: {},
_requestHandlerStr: `(url, config) => ({ url, config })`,
_responseCallback: null,
_init(targetConfig) {
this._targetConfig = targetConfig;
return this;
},
/**
* 定义请求被拦截时要执行的逻辑。
* @param {string} handlerString - 一个将要被注入的函数体字符串。
* @returns {builder}
*/
onRequest(handlerString) {
this._requestHandlerStr = handlerString;
return this;
},
/**
* 定义在沙箱中处理响应数据的回调函数。
* @param {(responseData: any) => void} callback - 回调函数。
* @returns {builder}
*/
onResponse(callback) {
this._responseCallback = callback;
return this;
},
/**
* 最终确定并注册这个拦截器。
* @param {string|string[]} id - 拦截器的唯一ID,通常是模块的this.id。
*/
register(id) {
const { match } = this._targetConfig;
const isMatched = _CaliberInternals._checkMatch(match, hostWindow);
if (!isMatched) {
if (isDebug) {
logger.log(`[Interceptor.register] Registration for ID '${id}' skipped. Current URL "${hostWindow.location.href}" does not match the rule:`, match);
}
return;
}
if (!id) {
logger.error('[Interceptor.register] An ID is required to register a hook.');
return;
}
const isRequestModified = this._requestHandlerStr !== `(url, config) => ({ url, config })`;
const isResponseHandled = this._responseCallback !== null;
if (!isRequestModified && !isResponseHandled) {
if (isDebug) {
logger.warn(`[Interceptor.register] Registration for ID '${id}' was silently cancelled because both onRequest and onResponse were omitted.`);
}
return;
}
const finalHandler = isRequestModified ? this._requestHandlerStr : `return { url, config };`;
const fullHookString = service.createHook({
targetUrl: this._targetConfig.url,
method: this._targetConfig.method,
handler: finalHandler
});
if (isResponseHandled) {
service.addHookWithResponse(id, fullHookString, this._responseCallback);
} else {
service.addHook(id, fullHookString);
}
}
};
const targetConfig = typeof urlOrOptions === 'string'
? { url: urlOrOptions, method: 'GET' }
: { method: 'GET', ...urlOrOptions };
return builder._init(targetConfig);
}
};
return service;
},
/**
* @private
* [原生适配器] 基于Web标准API的默认服务实现。
*/
_nativeBrowserAdapters: {
storage: (storageKey) => ({
get: () => Promise.resolve(JSON.parse(localStorage.getItem(storageKey) || '{}')),
set: (value) => Promise.resolve(localStorage.setItem(storageKey, JSON.stringify(value))),
}),
command: {
register: (name, callback) => { },
},
style: (sanitizer, hostDocument) => ({
_addedStyles: new Map(),
add(cssString, id) {
const styleElement = sanitizer.injectStyle(hostDocument, cssString, id);
if (styleElement) {
this._addedStyles.set(id, styleElement);
}
},
remove(id) {
if (this._addedStyles.has(id)) {
this._addedStyles.get(id).remove();
this._addedStyles.delete(id);
}
}
})
},
/**
* @private
* [服务工厂] 创建一个页面作用域代码执行器服务。
* 可将任意JS代码字符串注入到宿主页面执行,并异步返回其可序列化的结果。
* @param {object} logger - 框架的主 logger 实例。
* @param {string} safeAppName - 应用的安全名称,用于生成唯一事件名。
* @param {DOMSanitizerInstance} sanitizer - DOM净化与安全注入服务实例。
* @param {Document} hostDocument - 宿主 document 对象。
* @returns {{execute: (codeString: string) => Promise<any>}} PageScopeExecutor 服务实例。
*/
createPageScopeExecutor: (logger, safeAppName, sanitizer, hostDocument) => {
const namespace = `__CALIBER_PAGE_EXECUTOR_${safeAppName}`;
return {
async execute(codeString) {
return new Promise((resolve, reject) => {
const requestId = `req_${Math.random().toString(36).substr(2, 9)}`;
const responseEventName = `${namespace}_RESPONSE_${requestId}`;
const handleResponse = (event) => {
const { success, data, errorMsg } = event.detail;
hostDocument.removeEventListener(responseEventName, handleResponse);
if (success) {
resolve(data);
} else {
reject(new Error(errorMsg || 'Page-scope code execution failed.'));
}
};
hostDocument.addEventListener(responseEventName, handleResponse, { once: true });
const injectionCode = `
(async () => {
const RESPONSE_EVENT_NAME = '${responseEventName}';
try {
const result = await (${codeString});
document.dispatchEvent(new CustomEvent(RESPONSE_EVENT_NAME, {
detail: { success: true, data: result }
}));
} catch (e) {
document.dispatchEvent(new CustomEvent(RESPONSE_EVENT_NAME, {
detail: { success: false, errorMsg: e.message }
}));
}
})();
`;
sanitizer.injectScript(hostDocument, injectionCode);
});
}
};
},
};
/**
* 创建并启动一个基于 Caliber 框架的增强脚本应用。
* 这是 Caliber 框架的唯一入口点。
*
* @param {object} options - 应用的配置对象。
* @param {string} options.appName - 应用的名称。将用于日志前缀、UI标题和菜单项。
* @param {class[]} options.modules - 一个由模块类(必须继承自 Caliber.Module)组成的数组。
* @param {object} options.services - 一个包含所有平台相关服务实现的对象。
* @param {object} options.services.storage - 存储服务适配器。必须实现 get() 和 set(value) 方法。
* @param {() => Promise<object>} options.services.storage.get - 一个异步函数,返回存储的用户配置对象。
* @param {(value: object) => Promise<void>} options.services.storage.set - 一个异步函数,将配置对象写入存储。
* @param {object} options.services.command - 命令服务适配器。必须实现 register(name, callback) 方法。
* @param {(name: string, callback: () => void) => void} options.services.command.register - 一个函数,用于注册一个菜单命令。
* @param {object} [options.services.hostWindow=window] - (可选) 要操作的窗口对象。默认为油猴环境的`unsafeWindow`或标准`window`。
* @param {object} [options.services.hostDocument=document] - (可选) 要操作的文档对象。默认为`document`。
* @param {boolean} [options.isDebug=true] - (可选) 是否开启调试模式,会影响日志的输出。默认为`false`。
* @param {boolean} [options.settingsPanelEnabled=true] - (可选) 设置面板在首次启动时是否默认开启。默认为`true`。
* @param {object} [options.framework] - (可选) 用于微调 Caliber 框架内部行为的配置。
* @param {number} [options.framework.domProcessorBatchSize=20] - (可选) 设置 DomBatchProcessor 在每个渲染帧中处理的最大任务数。
* @returns {Promise<void>}
*/
async function createApp(options) {
const { appName, modules, services, isDebug = false, settingsPanelEnabled = true } = options || {};
// 初始化基础环境
const loggerFactory = new LoggerService(isDebug);
const mainLogger = loggerFactory.createMainLogger(appName || 'CaliberApp');
const hostWindow = (services.hostWindow || (('unsafeWindow' in window) ? unsafeWindow : window));
const hostDocument = (services.hostDocument || hostWindow.document || document);
// 创建上下文
const context = _CaliberInternals._createAppContext(appName);
// 执行预检
if (!_CaliberInternals.runPreflightChecks(options, context.instanceKey, mainLogger, hostWindow)) {
return;
}
const sanitizer = _CaliberInternals.createDOMSanitizer(mainLogger);
// 优先使用用户提供的,否则使用框架内置的原生适配器
const finalServices = {
storage: services.storage || _CaliberInternals._nativeBrowserAdapters.storage(`CALIBER_STORAGE_${appName}`),
command: _CaliberInternals._nativeBrowserAdapters.command,
style: services.style || _CaliberInternals._nativeBrowserAdapters.style(sanitizer, hostDocument),
...services
};
// 初始化核心服务
const coreServices = _CaliberInternals.initializeCoreServices(options, context, mainLogger, hostWindow, hostDocument, sanitizer);
// 组装依赖并运行内核
const injectedServices = {
hostWindow,
hostDocument,
eventBus: coreServices.eventBus,
storage: finalServices.storage,
logger: mainLogger,
style: finalServices.style,
framework: {
...coreServices.framework,
...options.framework,
},
IS_DEBUG: isDebug,
APP_NAME: appName,
SAFE_APP_NAME: context.safeAppName,
initialConfig: { settingsPanel: { enabled: settingsPanelEnabled } },
};
const kernel = new AppKernel(injectedServices);
hostWindow[context.instanceKey] = kernel;
modules.forEach(ModuleClass => kernel.registerModule(ModuleClass));
await kernel.run();
// 注册外部接口
finalServices.command.register(`⚙️ ${appName} 设置`, () => {
coreServices.eventBus.emit('command:toggle-settings-panel');
});
mainLogger.log('Bootstrap sequence complete. Application is alive.');
}
return {
createApp,
Module // 暴露 Module 基类,以便应用脚本可以继承它
};
})();
// 将Caliber对象挂载到window上
window.Caliber = Caliber;
})((typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window);