#!/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']) { // 28 批次 1:system/templates/ 已并入 system/agents/ 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 };