migrate-trained-skills.js 7.26 KB
#!/usr/bin/env node
/**
 * 迁移脚本:将已有训练的 skill 从 data/skills/ 移到 data/trained_skills/user_{id}/
 *
 * 用法:
 *   node scripts/migrate-trained-skills.js --dry-run          # 预览
 *   node scripts/migrate-trained-skills.js --execute          # 执行迁移
 *   node scripts/migrate-trained-skills.js --execute --user 2 # 指定归属用户
 */

const fs = require('fs');
const path = require('path');

// ── 配置 ──
const DATA_DIR = path.resolve(__dirname, '../../data');
const SKILLS_DATA_DIR = path.join(DATA_DIR, 'skills');
const TRAINED_SKILLS_DIR = path.join(DATA_DIR, 'trained_skills');
const DB_PATH = path.join(DATA_DIR, 'skills.sqlite');

// 系统 Agent 名单(不迁移)
const SYSTEM_SKILLS = new Set([
  'master', 'coder', 'innovator', 'innovator-evolver', 'evolving',
  'editor', 'verifier', 'gateway', 'benchmark',
  'frontend-tester', 'frontend-orch', 'backend-orch',
  'multi-agent-system', 'novel_writer', 'dual_column_layout',
  'skill-creator', 'task-monitor', 'feishu-bot', 'whatsapp-bot',
  'novel_outline', 'comedy_writing', 'novel_humor_insertion',
  'aliyun_oss_operator', 'brd_writer', 'anime_screenwriter',
  'lemon_skill_store', 'arenask',
  // system L1/L2/L3 agents
  'l1-api-request', 'l1-data-extract', 'l1-diff-checker',
  'l1-file-read', 'l1-kv-store', 'l1-search-engine',
  'l1-shell-exec', 'l1-text-transform', 'l1-web-fetch',
  'l1-webhook-push', 'l2-digest', 'l2-price-tracker',
  'l2-web-research', 'l3-deep-research', 'l3-hunt',
]);

// ── 命令行参数 ──
const args = process.argv.slice(2);
const dryRun = !args.includes('--execute');
const userIdArg = args.includes('--user') ? args[args.indexOf('--user') + 1] : null;
const DEFAULT_USER_ID = userIdArg || '2'; // 默认归属用户 id=2 (yibo)

console.log(`\n${'='.repeat(60)}`);
console.log(`  训练 Skill 迁移工具`);
console.log(`  模式: ${dryRun ? '🔍 DRY RUN (预览)' : '🚀 EXECUTE (执行)'}`);
console.log(`  默认用户: user_${DEFAULT_USER_ID}`);
console.log(`${'='.repeat(60)}\n`);

// ── Step 1: 找出需要迁移的 skill ──
// 判定标准:data/skills/ 中有 hunt/digest 实际训练数据的 skill,
// 或者名称以 l1-/l2-/l3- 开头的系统训练 skill
function findTrainedSkills() {
  if (!fs.existsSync(SKILLS_DATA_DIR)) {
    console.log('❌ data/skills/ 不存在,无需迁移');
    return [];
  }

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

  const trained = [];
  for (const skillName of dirs) {
    if (SYSTEM_SKILLS.has(skillName)) continue;

    const skillDir = path.join(SKILLS_DATA_DIR, skillName);

    // 检查是否有实际的训练产物数据
    const huntDir = path.join(skillDir, 'hunt');
    const hunterDir = path.join(skillDir, 'hunter');
    const digestDir = path.join(skillDir, 'digest');
    const sourceDir = path.join(skillDir, 'source');

    const hasHuntData = _hasFiles(huntDir) || _hasFiles(hunterDir);
    const hasDigestData = _hasFiles(digestDir);
    const hasSourceData = _hasFiles(sourceDir);
    const hasSkillMd = fs.existsSync(path.join(skillDir, 'SKILL.md'));
    const isLevelPrefixed = /^l[123]-/.test(skillName);

    // 只迁移有训练数据的 skill(有 hunt/digest 内容 或 l1-/l2-/l3- 前缀的自训练 skill)
    if (hasHuntData || hasDigestData || isLevelPrefixed) {
      trained.push({
        skillName,
        sourceDir: skillDir,
        hasSkillMd,
        hasHuntData,
        hasDigestData,
        hasSourceData,
        isLevelPrefixed,
      });
    }
  }
  return trained;
}

/** 检查目录是否存在且有文件 */
function _hasFiles(dir) {
  if (!fs.existsSync(dir)) return false;
  try {
    const entries = fs.readdirSync(dir);
    return entries.length > 0;
  } catch { return false; }
}

// ── Step 2: 迁移 ──
function migrateSkill(skill, userId) {
  const targetDir = path.join(TRAINED_SKILLS_DIR, `user_${userId}`, skill.skillName);
  console.log(`  📦 ${skill.skillName}`);
  console.log(`     From: ${skill.sourceDir}`);
  console.log(`     To:   ${targetDir}`);
  console.log(`     Has: ${[
    skill.hasSkillMd && 'SKILL.md',
    skill.hasHuntData && 'hunt/',
    skill.hasDigestData && 'digest/',
    skill.hasSourceData && 'source/',
    skill.isLevelPrefixed && '(l-prefix)',
  ].filter(Boolean).join(', ')}`);

  if (dryRun) {
    console.log(`     ⏭️  跳过 (dry-run)\n`);
    return { migrated: false, targetDir };
  }

  // 创建目标目录
  fs.mkdirSync(targetDir, { recursive: true });

  // 递归复制
  copyDirRecursive(skill.sourceDir, targetDir);
  console.log(`     ✅  复制完成\n`);

  return { migrated: true, targetDir };
}

function copyDirRecursive(src, dest) {
  if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });

  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
    const srcPath = path.join(src, entry.name);
    const destPath = path.join(dest, entry.name);

    if (entry.isDirectory()) {
      copyDirRecursive(srcPath, destPath);
    } else {
      fs.copyFileSync(srcPath, destPath);
    }
  }
}

// ── Step 3: 更新 SQLite ──
function updateDatabase(results) {
  if (dryRun) {
    console.log('\n📋 数据库更新 (dry-run 跳过):');
    results.forEach(r => {
      console.log(`   UPDATE skills SET directory='${r.targetDir}', user_id='${DEFAULT_USER_ID}', storage_type='trained' WHERE skill_name='${r.skillName}'`);
    });
    return;
  }

  try {
    const Database = require('better-sqlite3');
    const db = new Database(DB_PATH);

    const stmt = db.prepare(`
      UPDATE skills
      SET directory = ?, user_id = ?, storage_type = 'trained', last_updated = ?
      WHERE skill_name = ?
    `);

    const now = new Date().toISOString();
    let updated = 0;

    for (const r of results) {
      const info = stmt.run(r.targetDir, DEFAULT_USER_ID, now, r.skillName);
      if (info.changes > 0) {
        console.log(`   ✅ ${r.skillName} → user_${DEFAULT_USER_ID}`);
        updated++;
      } else {
        console.log(`   ⚠️  ${r.skillName} 在数据库中未找到`);
      }
    }

    db.close();
    console.log(`\n📊 数据库更新: ${updated}/${results.length} 条记录`);
  } catch (err) {
    console.error(`\n❌ 数据库更新失败: ${err.message}`);
  }
}

// ── Main ──
function main() {
  const trained = findTrainedSkills();

  if (trained.length === 0) {
    console.log('✅ 没有找到需要迁移的训练 Skill');
    return;
  }

  console.log(`🔍 发现 ${trained.length} 个训练 Skill:\n`);

  const results = [];
  for (const skill of trained) {
    const { migrated, targetDir } = migrateSkill(skill, DEFAULT_USER_ID);
    results.push({ skillName: skill.skillName, targetDir, migrated });
  }

  // 更新数据库
  updateDatabase(results.filter(r => r.migrated || dryRun));

  console.log(`\n${'─'.repeat(60)}`);
  if (dryRun) {
    console.log(`\n💡 这是 DRY RUN。运行 --execute 真正执行迁移:`);
    console.log(`   node scripts/migrate-trained-skills.js --execute\n`);
  } else {
    console.log(`\n✅ 迁移完成!共迁移 ${results.filter(r => r.migrated).length} 个 Skill`);
    console.log(`\n⚠️  原始目录 data/skills/ 中的文件已保留(未删除)。`);
    console.log(`   如确认无误后可手动删除原始目录。\n`);
  }
}

main();