innovator-service.js 8.64 KB
/**
 * InnovatorService — Skill 管理服务
 * 职责: Skill 注册/发现/同步 (SkillRegistry CRUD)
 */
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const { SkillRegistry } = require('./skill-registry');

class InnovatorService {
    constructor(options = {}) {
        this.skillsDir = options.skillsDir || path.join(__dirname, '../innovator');
        this.skillsDataDir = options.skillsDataDir || null;
        this.trainedSkillsDir = options.trainedSkillsDir || null;
        this.registry = new SkillRegistry(this.skillsDir);
        this.conversationsDir = options.conversationsDir
            || path.resolve(this.skillsDir, '..', 'conversations');
    }

    /**
     * 自动发现并注册新 Skill (扫描 data/skills/ 下有 SKILL.md 的目录)
     * @returns {string[]} 新注册的 skill 名称列表
     */
    autoRegisterNewSkills() {
        this.registry.invalidateCache();
        const registered = [];
        const existingSkills = new Set(this.registry.listSkills().map(s => s.skillName));

        try {
            const scanDir = this.skillsDataDir || this.skillsDir;
            if (!fs.existsSync(scanDir)) return registered;

            const dirs = fs.readdirSync(scanDir, { withFileTypes: true })
                .filter(d => d.isDirectory())
                .map(d => d.name);

            for (const dirName of dirs) {
                const skillMdPath = path.join(scanDir, dirName, 'SKILL.md');

                if (existingSkills.has(dirName)) {
                    // 已注册 — 检查是否需要启用
                    const skill = this.registry.getSkill(dirName);
                    if (skill && skill.status !== 'enable' && fs.existsSync(skillMdPath)) {
                        this.registry.updateSkill(dirName, {
                            status: 'enable', directory: path.join(scanDir, dirName),
                        });
                        this._syncSkillsToCLI(dirName);
                        registered.push(dirName);
                    }
                    continue;
                }

                if (!fs.existsSync(skillMdPath)) continue;

                // 解析 YAML frontmatter 并注册
                const meta = this._parseSkillFrontmatter(skillMdPath);
                if (!meta.name) meta.name = dirName;

                try {
                    const layer = meta.layer ? parseInt(meta.layer) : null;
                    let deps = [];
                    if (typeof meta.dependencies === 'string') {
                        deps = meta.dependencies.replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean);
                    } else if (Array.isArray(meta.dependencies)) {
                        deps = meta.dependencies;
                    }

                    this.registry.registerSkill({
                        skillName: dirName, description: meta.description || '',
                        domain: meta.domain || '', scope: meta.scope || '',
                        keywords: meta.keywords || [], layer, dependencies: deps,
                        directory: path.join(scanDir, dirName),
                    });
                    this.registry.updateSkill(dirName, {
                        status: 'enable', version: meta.version || '1.0.0',
                    });
                    this._syncSkillsToCLI(dirName);
                    registered.push(dirName);
                } catch (err) {
                    console.warn(`[Innovator] 注册 "${dirName}" 失败:`, err.message);
                }
            }
        } catch (err) {
            console.warn('[Innovator] autoRegisterNewSkills error:', err.message);
        }

        // 扫描 trained_skills/ 目录(per-user)
        try {
            const trainedDir = this.trainedSkillsDir;
            if (trainedDir && fs.existsSync(trainedDir)) {
                const userDirs = fs.readdirSync(trainedDir, { withFileTypes: true })
                    .filter(d => d.isDirectory() && d.name.startsWith('user_'));

                for (const userDir of userDirs) {
                    const userSkillsPath = path.join(trainedDir, userDir.name);
                    const skillDirs = fs.readdirSync(userSkillsPath, { withFileTypes: true })
                        .filter(d => d.isDirectory());

                    for (const skillEntry of skillDirs) {
                        const dirName = skillEntry.name;
                        const skillMdPath = path.join(userSkillsPath, dirName, 'SKILL.md');
                        if (!fs.existsSync(skillMdPath)) continue;

                        const userId = userDir.name.replace('user_', '');
                        const fullDir = path.join(userSkillsPath, dirName);

                        if (existingSkills.has(dirName)) {
                            // 更新 directory 指向
                            try {
                                this.registry.updateSkill(dirName, {
                                    status: 'enable', directory: fullDir,
                                    userId, storageType: 'trained',
                                });
                            } catch {}
                            continue;
                        }

                        const meta = this._parseSkillFrontmatter(skillMdPath);
                        if (!meta.name) meta.name = dirName;

                        try {
                            const layer = meta.layer ? parseInt(meta.layer) : null;
                            let deps = [];
                            if (typeof meta.dependencies === 'string') {
                                deps = meta.dependencies.replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean);
                            } else if (Array.isArray(meta.dependencies)) {
                                deps = meta.dependencies;
                            }

                            this.registry.registerSkill({
                                skillName: dirName, description: meta.description || '',
                                domain: meta.domain || '', scope: meta.scope || '',
                                keywords: meta.keywords || [], layer, dependencies: deps,
                                directory: fullDir,
                                userId, storageType: 'trained',
                            });
                            this.registry.updateSkill(dirName, {
                                status: 'enable', version: meta.version || '1.0.0',
                            });
                            registered.push(dirName);
                        } catch (err) {
                            console.warn(`[Innovator] 注册 trained "${dirName}" (${userDir.name}) 失败:`, err.message);
                        }
                        existingSkills.add(dirName);
                    }
                }
            }
        } catch (err) {
            console.warn('[Innovator] autoRegisterNewSkills (trained) error:', err.message);
        }

        return registered;
    }

    /** 解析 SKILL.md 的 YAML frontmatter */
    _parseSkillFrontmatter(filePath) {
        try {
            const content = fs.readFileSync(filePath, 'utf-8');
            const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
            if (!fmMatch) return {};

            const meta = {};
            let currentKey = null, multilineValue = '';

            for (const line of fmMatch[1].split('\n')) {
                const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
                if (kvMatch) {
                    if (currentKey && multilineValue) meta[currentKey] = multilineValue.trim();
                    currentKey = kvMatch[1];
                    const val = kvMatch[2].trim();
                    if (val === '>' || val === '|') { multilineValue = ''; }
                    else { meta[currentKey] = val; currentKey = null; multilineValue = ''; }
                } else if (currentKey) {
                    multilineValue += ' ' + line.trim();
                }
            }
            if (currentKey && multilineValue) meta[currentKey] = multilineValue.trim();
            return meta;
        } catch { return {}; }
    }

    /** 同步 skill 到 CLI 配置目录 (.claude/) */
    _syncSkillsToCLI(skillName) {
        const syncScript = path.resolve(__dirname, '../../scripts/sync-skills.js');
        if (!fs.existsSync(syncScript)) return;
        try {
            execFileSync(process.execPath, [syncScript, ...(skillName ? [skillName] : [])], {
                cwd: path.resolve(__dirname, '../..'), stdio: 'pipe', timeout: 10000,
            });
        } catch (err) {
            console.warn(`[Innovator] Skill 同步失败: ${err.message}`);
        }
    }
}

module.exports = { InnovatorService };