#!/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();