UNPKG

novel-writer-cn

Version:

AI 驱动的中文小说创作工具 - 基于结构化工作流的智能写作助手

373 lines (371 loc) 14.8 kB
import fs from 'fs-extra'; import path from 'path'; import yaml from 'js-yaml'; import { logger } from '../utils/logger.js'; export class PluginManager { pluginsDir; commandsDirs; expertsDir; constructor(projectRoot) { this.pluginsDir = path.join(projectRoot, 'plugins'); this.commandsDirs = { claude: path.join(projectRoot, '.claude', 'commands'), cursor: path.join(projectRoot, '.cursor', 'commands'), gemini: path.join(projectRoot, '.gemini', 'commands'), windsurf: path.join(projectRoot, '.windsurf', 'workflows'), roocode: path.join(projectRoot, '.roo', 'commands') }; this.expertsDir = path.join(projectRoot, 'experts'); } /** * 扫描并加载所有插件 */ async loadPlugins() { try { // 确保插件目录存在 await fs.ensureDir(this.pluginsDir); // 扫描插件目录 const plugins = await this.scanPlugins(); if (plugins.length === 0) { logger.info('没有发现插件'); return; } logger.info(`发现 ${plugins.length} 个插件`); // 加载每个插件 for (const pluginName of plugins) { await this.loadPlugin(pluginName); } logger.success('所有插件加载完成'); } catch (error) { logger.error('加载插件失败:', error); } } /** * 扫描插件目录,返回所有插件名称 */ async scanPlugins() { try { // 检查插件目录是否存在 if (!await fs.pathExists(this.pluginsDir)) { return []; } const entries = await fs.promises.readdir(this.pluginsDir, { withFileTypes: true }); // 过滤出目录,并且包含config.yaml的 const plugins = []; for (const entry of entries) { if (entry.isDirectory()) { const configPath = path.join(this.pluginsDir, entry.name, 'config.yaml'); if (await fs.pathExists(configPath)) { plugins.push(entry.name); } } } return plugins; } catch (error) { logger.error('扫描插件目录失败:', error); return []; } } /** * 加载单个插件 */ async loadPlugin(pluginName) { try { logger.info(`加载插件: ${pluginName}`); // 读取插件配置 const configPath = path.join(this.pluginsDir, pluginName, 'config.yaml'); const config = await this.loadConfig(configPath); if (!config) { logger.warn(`插件 ${pluginName} 配置无效`); return; } // 检查依赖 if (!this.checkDependencies(config)) { logger.warn(`插件 ${pluginName} 依赖不满足`); return; } // 注入命令 if (config.commands && config.commands.length > 0) { await this.injectCommands(pluginName, config.commands); } // 注册专家 if (config.experts && config.experts.length > 0) { await this.registerExperts(pluginName, config.experts); } logger.success(`插件 ${pluginName} 加载成功`); // 显示安装信息 if (config.installation?.message) { console.log(config.installation.message); } } catch (error) { logger.error(`加载插件 ${pluginName} 失败:`, error); } } /** * 读取并解析插件配置 */ async loadConfig(configPath) { try { const content = await fs.readFile(configPath, 'utf-8'); const config = yaml.load(content); // 验证必要字段 if (!config.name || !config.version) { return null; } return config; } catch (error) { logger.error(`读取配置文件失败: ${configPath}`, error); return null; } } /** * 检查插件依赖 */ checkDependencies(config) { if (!config.dependencies) { return true; } // 检查核心版本依赖 if (config.dependencies.core) { // 这里简化处理,实际应该比较版本号 // 可以使用 semver 库进行版本比较 const requiredVersion = config.dependencies.core; logger.debug(`需要核心版本: ${requiredVersion}`); // TODO: 实现版本比较逻辑 } return true; } /** * 检测项目支持的 AI 类型 */ async detectSupportedAIs() { return { claude: await fs.pathExists(this.commandsDirs.claude), cursor: await fs.pathExists(this.commandsDirs.cursor), gemini: await fs.pathExists(this.commandsDirs.gemini), windsurf: await fs.pathExists(this.commandsDirs.windsurf), roocode: await fs.pathExists(this.commandsDirs.roocode) }; } /** * 注入插件命令到对应的 AI 目录 */ async injectCommands(pluginName, commands) { if (!commands) return; // 检测项目支持哪些 AI const supportedAIs = await this.detectSupportedAIs(); for (const cmd of commands) { try { // 处理 Markdown 格式(Claude、Cursor、Windsurf) const sourcePath = path.join(this.pluginsDir, pluginName, cmd.file); if (supportedAIs.claude) { const destPath = path.join(this.commandsDirs.claude, `${cmd.id}.md`); await fs.ensureDir(this.commandsDirs.claude); await fs.copy(sourcePath, destPath); logger.debug(`注入命令到 Claude: /${cmd.id}`); } if (supportedAIs.cursor) { const destPath = path.join(this.commandsDirs.cursor, `${cmd.id}.md`); await fs.ensureDir(this.commandsDirs.cursor); await fs.copy(sourcePath, destPath); logger.debug(`注入命令到 Cursor: /${cmd.id}`); } if (supportedAIs.windsurf) { const destPath = path.join(this.commandsDirs.windsurf, `${cmd.id}.md`); await fs.ensureDir(this.commandsDirs.windsurf); await fs.copy(sourcePath, destPath); logger.debug(`注入命令到 Windsurf: /${cmd.id}`); } if (supportedAIs.roocode) { const destPath = path.join(this.commandsDirs.roocode, `${cmd.id}.md`); await fs.ensureDir(this.commandsDirs.roocode); await fs.copy(sourcePath, destPath); logger.debug(`注入命令到 Roo Code: /${cmd.id}`); } // 处理 TOML 格式(Gemini) if (supportedAIs.gemini) { // 检查是否有预定义的 TOML 版本 const cmdId = path.basename(cmd.id, path.extname(cmd.id)); const tomlSourcePath = path.join(this.pluginsDir, pluginName, 'commands-gemini', `${cmdId}.toml`); if (await fs.pathExists(tomlSourcePath)) { const destPath = path.join(this.commandsDirs.gemini, `${cmdId}.toml`); await fs.ensureDir(this.commandsDirs.gemini); await fs.copy(tomlSourcePath, destPath); logger.debug(`注入命令到 Gemini: /${cmdId} (TOML)`); } else { // 如果没有预定义的 TOML,尝试从 Markdown 转换 try { const mdContent = await fs.readFile(sourcePath, 'utf-8'); const tomlContent = this.convertMarkdownToToml(mdContent, cmd); if (tomlContent) { const destPath = path.join(this.commandsDirs.gemini, `${cmdId}.toml`); await fs.ensureDir(this.commandsDirs.gemini); await fs.writeFile(destPath, tomlContent); logger.debug(`自动转换并注入命令到 Gemini: /${cmdId}`); } else { logger.debug(`插件 ${pluginName} 命令 ${cmdId} 无法转换为 TOML`); } } catch (err) { logger.debug(`插件 ${pluginName} 命令 ${cmdId} 转换失败: ${err}`); } } } } catch (error) { logger.error(`注入命令 ${cmd.id} 失败:`, error); } } } /** * 注册插件专家 */ async registerExperts(pluginName, experts) { if (!experts) return; const pluginExpertsDir = path.join(this.expertsDir, 'plugins', pluginName); await fs.ensureDir(pluginExpertsDir); for (const expert of experts) { try { const sourcePath = path.join(this.pluginsDir, pluginName, expert.file); const destPath = path.join(pluginExpertsDir, `${expert.id}.md`); // 复制专家文件 await fs.copy(sourcePath, destPath); logger.debug(`注册专家: ${expert.title} (${expert.id})`); } catch (error) { logger.error(`注册专家 ${expert.id} 失败:`, error); } } } /** * 列出所有已安装的插件 */ async listPlugins() { const plugins = await this.scanPlugins(); const configs = []; for (const pluginName of plugins) { const configPath = path.join(this.pluginsDir, pluginName, 'config.yaml'); const config = await this.loadConfig(configPath); if (config) { configs.push(config); } } return configs; } /** * 安装插件(从模板或远程) */ async installPlugin(pluginName, source) { try { logger.info(`安装插件: ${pluginName}`); // 如果提供了源路径,从源复制 if (source) { const destPath = path.join(this.pluginsDir, pluginName); await fs.copy(source, destPath); } else { // TODO: 实现从远程仓库或注册中心安装 logger.warn('远程安装功能尚未实现'); return; } // 加载新安装的插件 await this.loadPlugin(pluginName); logger.success(`插件 ${pluginName} 安装成功`); } catch (error) { logger.error(`安装插件 ${pluginName} 失败:`, error); } } /** * 移除插件 */ async removePlugin(pluginName) { try { logger.info(`移除插件: ${pluginName}`); // 删除插件目录 const pluginPath = path.join(this.pluginsDir, pluginName); await fs.remove(pluginPath); // 删除注入的命令(从所有 AI 目录) const supportedAIs = await this.detectSupportedAIs(); if (supportedAIs.claude && await fs.pathExists(this.commandsDirs.claude)) { const commandFiles = await fs.promises.readdir(this.commandsDirs.claude); for (const file of commandFiles) { if (file.startsWith(`plugin-${pluginName}-`)) { await fs.remove(path.join(this.commandsDirs.claude, file)); logger.debug(`移除命令文件: ${file}`); } } } // 对其他 AI 目录做同样的清理 for (const [aiType, dir] of Object.entries(this.commandsDirs)) { if (aiType !== 'claude' && await fs.pathExists(dir)) { const commandFiles = await fs.promises.readdir(dir); for (const file of commandFiles) { if (file.startsWith(`plugin-${pluginName}-`)) { await fs.remove(path.join(dir, file)); logger.debug(`移除 ${aiType} 命令文件: ${file}`); } } } } // 删除注册的专家 const pluginExpertsDir = path.join(this.expertsDir, 'plugins', pluginName); if (await fs.pathExists(pluginExpertsDir)) { await fs.remove(pluginExpertsDir); logger.debug(`移除专家目录: ${pluginExpertsDir}`); } logger.success(`插件 ${pluginName} 移除成功`); } catch (error) { logger.error(`移除插件 ${pluginName} 失败:`, error); } } /** * 将 Markdown 命令转换为 TOML 格式 */ convertMarkdownToToml(mdContent, cmd) { try { // 提取 frontmatter const frontmatterMatch = mdContent.match(/^---\n([\s\S]*?)\n---/); let description = cmd.description || ''; if (frontmatterMatch) { const yamlContent = frontmatterMatch[1]; const descMatch = yamlContent.match(/description:\s*(.+)/); if (descMatch) { description = descMatch[1].trim().replace(/^['"]|['"]$/g, ''); } } // 提取内容(去除 frontmatter) const content = mdContent.replace(/^---\n[\s\S]*?\n---\n/, ''); // 构建 TOML 内容 const tomlContent = `description = "${description}" prompt = """ ${content} 用户输入:{{args}} """`; return tomlContent; } catch (error) { return null; } } /** * 更新插件 */ async updatePlugin(pluginName, source) { logger.info(`更新插件: ${pluginName}`); // 先移除旧版本 await this.removePlugin(pluginName); // 安装新版本 await this.installPlugin(pluginName, source); } } //# sourceMappingURL=manager.js.map