/** * 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 };