/** * Skill Directory Manager — Skill 目录管理 */ const fs = require('fs'); const path = require('path'); const config = require('../config'); // 核心 Skills 白名单(保留在 repo/skills/,随 Git 部署) const CORE_SKILLS = new Set([ 'master', 'innovator', 'gateway', 'coder', 'verifier', 'editor', 'meta_benchmark', 'meta_evolving', 'meta_creator', 'meta_hunt', 'meta_digest', 'meta_research', 'task-monitor', 'feishu-bot', 'whatsapp-bot', 'arenask', 'frontend-tester', 'dual_column_layout', 'multi-agent-system', ]); function isCodeSkill(skillName) { return CORE_SKILLS.has(skillName); } // 项目根目录 skills/(核心 skills,随 Git 部署) const REPO_SKILLS_DIR = path.resolve(__dirname, '../../skills'); /** * 查找 skill 目录 (优先 repo/skills/ → skillsCodeDir → skillsDataDir) */ function resolveSkillDir(skillName) { // 1. 项目根目录 skills/(core skills: master, coder, etc.) const repoDir = path.join(REPO_SKILLS_DIR, skillName); if (fs.existsSync(repoDir)) return repoDir; // 2. skillsCodeDir(data/skills/) const codeDir = path.join(config.skillsCodeDir, skillName); if (fs.existsSync(codeDir)) return codeDir; // 3. skillsDataDir(通常与 codeDir 相同,但可通过 env 分离) const dataDir = path.join(config.skillsDataDir, skillName); if (fs.existsSync(dataDir)) return dataDir; return null; } /** * 获取新建 skill 的目标目录 */ function getTargetDir(skillName) { return path.join( isCodeSkill(skillName) ? config.skillsCodeDir : config.skillsDataDir, skillName ); } /** * 初始化 skill 目录(创建生命周期子目录) */ function initSkillDir(skillName, options = {}) { const baseDir = (options.isCode ?? isCodeSkill(skillName)) ? config.skillsCodeDir : config.skillsDataDir; const skillDir = path.join(baseDir, skillName); const subdirs = ['source', 'hunt', 'hunt/crawl', 'digest', 'evolve', 'production', 'production/logs', 'production/stats']; for (const sub of subdirs) { fs.mkdirSync(path.join(skillDir, sub), { recursive: true }); } const metaPath = path.join(skillDir, 'source', 'metadata.json'); if (!fs.existsSync(metaPath)) { fs.writeFileSync(metaPath, JSON.stringify({ skillName, createdAt: new Date().toISOString(), sourceUrl: null, author: null, }, null, 2)); } return skillDir; } /** * 获取用户训练 skill 的目标目录 * @param {string} skillName * @param {string|number} userId * @returns {string} 绝对路径 */ function getTrainedSkillDir(skillName, userId) { return path.join(config.trainedSkillsDir, `user_${userId}`, skillName); } /** * 初始化用户训练 skill 目录(含生命周期子目录) * @param {string} skillName * @param {string|number} userId * @returns {string} skillDir 绝对路径 */ function initTrainedSkillDir(skillName, userId) { const skillDir = getTrainedSkillDir(skillName, userId); const subdirs = ['source', 'hunt', 'hunt/crawl', 'digest', 'evolve', 'production', 'production/logs', 'production/stats']; for (const sub of subdirs) { fs.mkdirSync(path.join(skillDir, sub), { recursive: true }); } const metaPath = path.join(skillDir, 'source', 'metadata.json'); if (!fs.existsSync(metaPath)) { fs.writeFileSync(metaPath, JSON.stringify({ skillName, userId: String(userId), createdAt: new Date().toISOString(), sourceUrl: null, author: null, }, null, 2)); } return skillDir; } /** * 在训练目录中查找 skill(扫描所有 user_* 子目录) * @param {string} skillName * @param {string|number} [userId] - 指定用户,不指定则搜索所有用户 * @returns {string|null} */ function resolveTrainedSkillDir(skillName, userId) { // 优先指定用户的目录 if (userId) { const dir = getTrainedSkillDir(skillName, userId); if (fs.existsSync(dir)) return dir; } // 搜索所有用户目录 const trainedDir = config.trainedSkillsDir; if (!fs.existsSync(trainedDir)) return null; try { const userDirs = fs.readdirSync(trainedDir).filter(d => d.startsWith('user_')); for (const userDir of userDirs) { const dir = path.join(trainedDir, userDir, skillName); if (fs.existsSync(dir)) return dir; } } catch {} return null; } /** 确保全局数据目录存在 */ function ensureDataDirs() { for (const dir of [config.skillsDataDir, config.skillsLogDir, config.trainedSkillsDir]) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } } module.exports = { CORE_SKILLS, isCodeSkill, resolveSkillDir, getTargetDir, initSkillDir, getTrainedSkillDir, initTrainedSkillDir, resolveTrainedSkillDir, ensureDataDirs, };