skill-dir-manager.js 4.6 KB
/**
 * 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,
};