article-writer-cn
Version:
AI 驱动的智能写作系统 - 专注公众号/自媒体文章创作
466 lines (401 loc) • 13.6 kB
text/typescript
import fs from 'fs-extra';
import path from 'path';
import yaml from 'js-yaml';
import { logger } from '../utils/logger.js';
interface PluginConfig {
name: string
version: string
description: string
type: 'feature' | 'expert' | 'workflow'
commands?: Array<{
id: string
file: string
description: string
}>
experts?: Array<{
id: string
file: string
title: string
description: string
}>
dependencies?: {
core: string
}
installation?: {
files?: Array<{
source: string
target: string
prefix?: string
}>
message?: string
}
}
export class PluginManager {
private pluginsDir: string
private commandsDirs: {
claude: string
cursor: string
gemini: string
windsurf: string
roocode: string
}
private expertsDir: string
constructor(projectRoot: string) {
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(): Promise<void> {
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)
}
}
/**
* 扫描插件目录,返回所有插件名称
*/
private async scanPlugins(): Promise<string[]> {
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 []
}
}
/**
* 加载单个插件
*/
private async loadPlugin(pluginName: string): Promise<void> {
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)
}
}
/**
* 读取并解析插件配置
*/
private async loadConfig(configPath: string): Promise<PluginConfig | null> {
try {
const content = await fs.readFile(configPath, 'utf-8')
const config = yaml.load(content) as PluginConfig
// 验证必要字段
if (!config.name || !config.version) {
return null
}
return config
} catch (error) {
logger.error(`读取配置文件失败: ${configPath}`, error)
return null
}
}
/**
* 检查插件依赖
*/
private checkDependencies(config: PluginConfig): boolean {
if (!config.dependencies) {
return true
}
// 检查核心版本依赖
if (config.dependencies.core) {
// 这里简化处理,实际应该比较版本号
// 可以使用 semver 库进行版本比较
const requiredVersion = config.dependencies.core
logger.debug(`需要核心版本: ${requiredVersion}`)
// TODO: 实现版本比较逻辑
}
return true
}
/**
* 检测项目支持的 AI 类型
*/
private async detectSupportedAIs(): Promise<{
claude: boolean
cursor: boolean
gemini: boolean
windsurf: boolean
roocode: boolean
}> {
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 目录
*/
private async injectCommands(
pluginName: string,
commands: PluginConfig['commands']
): Promise<void> {
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)
}
}
}
/**
* 注册插件专家
*/
private async registerExperts(
pluginName: string,
experts: PluginConfig['experts']
): Promise<void> {
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(): Promise<PluginConfig[]> {
const plugins = await this.scanPlugins()
const configs: PluginConfig[] = []
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: string, source?: string): Promise<void> {
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: string): Promise<void> {
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 格式
*/
private convertMarkdownToToml(mdContent: string, cmd: any): string | null {
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: string, source?: string): Promise<void> {
logger.info(`更新插件: ${pluginName}`)
// 先移除旧版本
await this.removePlugin(pluginName)
// 安装新版本
await this.installPlugin(pluginName, source)
}
}