evolution.js 5.53 KB
/**
 * Evolution API Routes
 * 进化逻辑从 @mas/evolution 共享包导入。
 */
const express = require('express');
const router = express.Router();
const {
  evolutionQueries, forageSkills, RunController,
  crossoverGeneration, selectGeneration, evaluateRunFitness,
} = require('@mas/evolution');

// ── 通用 async handler ──
const h = (fn) => async (req, res) => {
  try { await fn(req, res); }
  catch (e) { console.error(`[Evolution] ${req.method} ${req.path}:`, e.message); res.status(500).json({ error: e.message }); }
};

// ── AgentManager 工厂 ──
function _createAgentManager() {
  const AgentManager = require('../src/core/agent-manager');
  const path = require('path');
  const fs = require('fs');
  let backendsConfig = {};
  try { backendsConfig = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/backends.json'), 'utf-8')); } catch {}

  const clients = {};
  for (const [name, launcher] of Object.entries(backendsConfig.launchers || {})) {
    clients[name] = {
      name, command: launcher.command, acpArgs: launcher.args || [],
      acpProtocol: name.includes('claude') ? 'claude-stream' : 'acp',
    };
  }
  return new AgentManager({
    agents: { evaluator: { name: 'evaluator', type: 'executor', client: 'kimi', deploy: 'local', mode: 'cli', prompt: null, description: '进化引擎 LLM 评估器' } },
    clients, ...backendsConfig,
  }, process.cwd());
}

function _makeController(opts = {}) {
  return new RunController({ agentManager: _createAgentManager(), ...opts });
}

// ── Queries ──
router.get('/overview', h(async (req, res) => res.json(await evolutionQueries.getOverview())));

router.get('/runs', h(async (req, res) => {
  const { page, pageSize, status, skillName } = req.query;
  res.json(await evolutionQueries.getRuns({ page: parseInt(page) || 1, pageSize: parseInt(pageSize) || 20, status, skillName }));
}));

router.get('/runs/:id', h(async (req, res) => {
  const data = await evolutionQueries.getRunStats(req.params.id);
  data ? res.json(data) : res.status(404).json({ error: 'Run not found' });
}));

router.get('/runs/:runId/genomes', h(async (req, res) => {
  const { generation, aliveOnly } = req.query;
  res.json(await evolutionQueries.getGenomesByRun(req.params.runId, {
    generation: generation !== undefined ? parseInt(generation) : undefined,
    aliveOnly: aliveOnly === 'true',
  }));
}));

router.get('/genomes/:id', h(async (req, res) => {
  const genome = await evolutionQueries.getGenome(req.params.id);
  genome ? res.json(genome) : res.status(404).json({ error: 'Genome not found' });
}));

router.get('/runs/:runId/evaluations', h(async (req, res) => {
  const { generation } = req.query;
  res.json(await evolutionQueries.getEvaluationsByRun(req.params.runId, {
    generation: generation !== undefined ? parseInt(generation) : undefined,
  }));
}));

router.get('/benchmarks', h(async (req, res) => res.json(await evolutionQueries.getBenchmarks(req.query.skillName))));
router.post('/benchmarks', h(async (req, res) => res.json(await evolutionQueries.upsertBenchmark(req.body))));

router.get('/runs/:runId/learnings', h(async (req, res) => {
  res.json(await evolutionQueries.getLearningEvents(req.params.runId, { dimension: req.query.dimension, eventType: req.query.eventType }));
}));

// ── Actions ──
router.post('/actions/forage', h(async (req, res) => {
  const results = await forageSkills(req.body.skillsDir || undefined);
  res.json({ success: true, count: results.length, genomes: results });
}));

router.post('/actions/run-phase1', h(async (req, res) => {
  const { skillName, skillsDir } = req.body;
  if (!skillName) return res.status(400).json({ error: 'skillName is required' });
  res.json(await _makeController({ skillsDir }).runPhase1(skillName));
}));

router.post('/actions/run-phase2', h(async (req, res) => {
  const { runId, generations } = req.body;
  if (!runId) return res.status(400).json({ error: 'runId is required' });
  res.json(await _makeController().runPhase2(runId, generations || 5));
}));

router.post('/actions/crossover', h(async (req, res) => {
  const { runId, generation } = req.body;
  if (!runId || generation === undefined) return res.status(400).json({ error: 'runId and generation are required' });
  const children = await crossoverGeneration(runId, generation);
  res.json({ success: true, count: children.length, children: children.map(c => ({ id: c.id, skill_name: c.skill_name })) });
}));

router.post('/actions/select', h(async (req, res) => {
  const { runId, eliteCount, survivalRate } = req.body;
  if (!runId) return res.status(400).json({ error: 'runId is required' });
  res.json({ success: true, ...await selectGeneration(runId, { eliteCount, survivalRate }) });
}));

router.post('/actions/deploy', h(async (req, res) => {
  const { runId, skillsDir } = req.body;
  if (!runId) return res.status(400).json({ error: 'runId is required' });
  const controller = _makeController({
    skillsDir: skillsDir || require('path').join(require('os').homedir(), '.gemini/antigravity/skills'),
  });
  res.json(await controller.deploy(runId));
}));

router.post('/actions/evaluate-fitness', h(async (req, res) => {
  const { runId } = req.body;
  if (!runId) return res.status(400).json({ error: 'runId is required' });
  const results = await evaluateRunFitness(runId);
  const best = results.reduce((max, r) => r.overall > max.overall ? r : max, results[0]);
  res.json({
    success: true, evaluated: results.length, bestFitness: best?.overall,
    results: results.map(r => ({ genomeId: r.genomeId, overall: r.overall, ...r.dimensionScores })),
  });
}));

module.exports = router;