migrate-agent-naming.js 6.06 KB
#!/usr/bin/env node
/**
 * migrate-agent-naming.js — 存量 system agent 按 27-NAMING §3.4 归位
 *
 * 依据: docs/todo/system-design/27-naming-conventions.md §3.4(R-15.3 定案)
 * 命名轴是「节点在哪一层图里」,把散落的老 agent 归到它实际所处的层级:
 *
 *   innovator → org-innovator       type='cxo',是 org graph 层的节点
 *   hunter    → innovator-hunt      retrain-pipeline.js:161,innovator work graph 第 1 阶段
 *   digester  → innovator-digest    同上第 2 阶段(行 179,输入是 hunter 产出)
 *   evolver   → innovator-evolver   同上第 3 阶段(行 196)
 *
 * 迁移内容:
 *   目录  system/agents/{旧}/     → system/agents/{新}/
 *         system/templates/{旧}/  → system/templates/{新}/
 *   DB    system_agents 的 id 与 legacy_path
 *   实例  data/companies/*\/agents/{旧}/ 与 company.json.agents[](若有公司雇过)
 *
 * ⚠️ 明确不动的东西(改了会坏):
 *   - skill 内部子目录名 `{skillDir}/hunter/knowhow.md` —— 那是 hunt/digest/evolve
 *     三阶段的产物目录约定,不是 agent 名(open-api.js:402、retrain-pipeline.js:148/374、
 *     evolution-loop.js:178、sync-skills.js:62 的 INTERNAL_DIRS 等)
 *   - skill 命名空间的白名单 —— skill-dir-manager.js 的 CORE_SKILLS、
 *     migrate-trained-skills.js 的 SYSTEM_SKILLS,那里的 'innovator' 指的是同名 skill
 *   - `evolver-youtube-crawler` —— 27-NAMING §3.4 列为待定,按精确目录名匹配,不会误伤
 *   - conversations/ 与 system/tasklog/ 下的历史记录 —— 运行态数据,不改写历史
 *
 * 用法:
 *   node scripts/migrate-agent-naming.js --dry-run
 *   node scripts/migrate-agent-naming.js
 *
 * 幂等: 已迁移过(新名存在且旧名不存在)会被跳过,可重复执行。
 */

const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');

const ROOT = path.resolve(__dirname, '..');
const DRY = process.argv.includes('--dry-run');

/** 旧名 → 新名。顺序无关,全部按精确名匹配,不做前缀/正则匹配。 */
const RENAMES = {
  innovator: 'org-innovator',
  hunter: 'innovator-hunt',
  digester: 'innovator-digest',
  evolver: 'innovator-evolver',
};

const log = (...a) => console.log(DRY ? '[dry-run]' : '[migrate]', ...a);
const stats = { dirs: 0, dbRows: 0, companyInstances: 0, skipped: 0 };

/** 用 git mv 保留历史;不在 git 索引里的目录退回 fs.renameSync */
function moveDir(from, to) {
  if (DRY) return;
  try {
    execFileSync('git', ['mv', from, to], { cwd: ROOT, stdio: 'pipe' });
  } catch {
    fs.renameSync(path.join(ROOT, from), path.join(ROOT, to));
  }
}

function migrateDirs() {
  for (const [oldName, newName] of Object.entries(RENAMES)) {
    for (const base of ['system/agents', 'system/templates']) {
      const from = `${base}/${oldName}`;
      const to = `${base}/${newName}`;
      const fromAbs = path.join(ROOT, from);
      const toAbs = path.join(ROOT, to);

      if (!fs.existsSync(fromAbs)) {
        if (fs.existsSync(toAbs)) { stats.skipped++; }   // 已迁移过
        continue;
      }
      if (fs.existsSync(toAbs)) {
        log(`⚠️  跳过(新旧同时存在,需人工处理): ${from} / ${to}`);
        stats.skipped++;
        continue;
      }
      log(`目录: ${from} → ${to}`);
      moveDir(from, to);
      stats.dirs++;
    }
  }
}

function migrateDb() {
  const { getDb } = require(path.join(ROOT, 'server/services/graph-db'));
  const db = getDb();
  const sel = db.prepare('SELECT id, legacy_path FROM system_agents WHERE id = ?');
  const upd = db.prepare('UPDATE system_agents SET id = ?, legacy_path = ?, updated_at = ? WHERE id = ?');
  const now = new Date().toISOString();

  const tx = db.transaction(() => {
    for (const [oldName, newName] of Object.entries(RENAMES)) {
      const row = sel.get(oldName);
      if (!row) continue;
      const newPath = (row.legacy_path || '').replace(
        new RegExp(`(^|/)${oldName}$`), `$1${newName}`
      );
      log(`DB: system_agents ${oldName} → ${newName}(legacy_path: ${row.legacy_path} → ${newPath})`);
      if (!DRY) upd.run(newName, newPath, now, oldName);
      stats.dbRows++;
    }
  });
  tx();
}

/** 有公司雇过这四个的话,同步改私有目录名与 company.json.agents[] */
function migrateCompanyInstances() {
  const companiesDir = path.join(ROOT, 'data/companies');
  if (!fs.existsSync(companiesDir)) return;

  for (const d of fs.readdirSync(companiesDir, { withFileTypes: true })) {
    if (!d.isDirectory()) continue;
    const cid = d.name;

    for (const [oldName, newName] of Object.entries(RENAMES)) {
      const from = path.join(companiesDir, cid, 'agents', oldName);
      const to = path.join(companiesDir, cid, 'agents', newName);
      if (!fs.existsSync(from) || fs.existsSync(to)) continue;
      log(`实例: ${cid}/agents/${oldName} → ${newName}`);
      if (!DRY) fs.renameSync(from, to);
      stats.companyInstances++;
    }

    const cjPath = path.join(companiesDir, cid, 'company.json');
    if (!fs.existsSync(cjPath)) continue;
    let cj;
    try { cj = JSON.parse(fs.readFileSync(cjPath, 'utf-8')); } catch { continue; }
    if (!Array.isArray(cj.agents)) continue;

    const next = cj.agents.map(a => RENAMES[a] || a);
    if (JSON.stringify(next) !== JSON.stringify(cj.agents)) {
      log(`实例: ${cid}/company.json agents[] 已更新`);
      if (!DRY) {
        cj.agents = next;
        fs.writeFileSync(cjPath, JSON.stringify(cj, null, 2) + '\n', 'utf-8');
      }
    }
  }
}

function main() {
  log(DRY ? '=== DRY RUN(不写盘)===' : '=== 开始迁移 ===');
  migrateDirs();
  migrateDb();
  migrateCompanyInstances();
  console.log('\n' + (DRY ? '[dry-run] ' : '') + '汇总:', JSON.stringify(stats));
  if (DRY) console.log('这是 dry-run,未做任何改动。去掉 --dry-run 实际执行。');
  else console.log('完成。代码引用点(retrain-pipeline.js / agent-chat.js / company.js)需另行修改,见 27-NAMING §3.4。');
}

if (require.main === module) main();

module.exports = { RENAMES };