devloop.js 3.75 KB
/**
 * DevLoop · REST Routes (P1)
 *
 *   POST /api/devloop/projects
 *     body: { company_id, name?, requirement, mode?, target_path?, mock? }
 *     → 创建项目 + 跑 PM / Architect 两个 Agent (mock 模式产出 PRD/SDD)
 *     → 返 { pid, root_path, pm: {...}, architect: {...} }
 *
 *   GET /api/devloop/projects/:pid/state
 *     → 返 { pid, project, units, prd_rev, sdd_rev, prd_site, sdd_site, timeline_tail }
 *
 *   GET /api/devloop/projects   (额外:列项目)
 *     → 返 { projects: [...] }
 *
 * 规格: docs/prd/newfunction/_spec/plan-migration.md · P1 验收
 */

const express = require('express');
const router = express.Router();

const loopController = require('../src/core/devloop/loop-controller');
const pathResolver = require('../services/devloop/path-resolver');

const h = (fn) => async (req, res) => {
  try { await fn(req, res); }
  catch (e) {
    console.error(`[DevLoop] ${req.method} ${req.path}:`, e.message);
    res.status(e.statusCode || 500).json({ error: e.message });
  }
};

// POST /api/devloop/projects
router.post('/projects', h(async (req, res) => {
  const { company_id, requirement, mode, target_path, mock, name } = req.body || {};
  if (!company_id) {
    return res.status(400).json({ error: 'missing field: company_id' });
  }
  if (!requirement || typeof requirement !== 'string') {
    return res.status(400).json({ error: 'missing field: requirement' });
  }

  // 1. 创建项目 (sandbox 默认)
  const proj = await loopController.createProject({
    company_id,
    mode: mode || 'sandbox',
    target_path,
    idea: name ? `${name}: ${requirement}` : requirement,
  });

  // 2. PM → Architect (mock 模式)
  const phase = await loopController.runDocsPhase(proj.pid, requirement, {
    mock: mock !== false,
  });

  res.json({
    ok: true,
    pid: proj.pid,
    root_path: proj.root_path,
    mode: proj.mode,
    pm: {
      files_written: phase.pm.files_written.length,
      commit: phase.pm.commit?.sha || null,
      mock: phase.pm.mock,
    },
    architect: {
      files_written: phase.architect.files_written.length,
      commit: phase.architect.commit?.sha || null,
      mock: phase.architect.mock,
    },
  });
}));

// GET /api/devloop/projects
router.get('/projects', h(async (_req, res) => {
  const projects = pathResolver.listProjects();
  res.json({ projects });
}));

// POST /api/devloop/projects/:pid/run?only=<unitId>
//   P2: only=<unitId> 跑单 unit;不带 only → 501 (P3 才补全 runProject)
router.post('/projects/:pid/run', h(async (req, res) => {
  const { pid } = req.params;
  const { only } = req.query;
  if (!only) {
    return res.status(501).json({ error: 'P2 only supports ?only=<unitId>; full project run lands in P3' });
  }
  try {
    const result = await loopController.runUnit(pid, only, { mock: true });
    res.json({
      ok: result.ok,
      pid, unit_id: only,
      attempts: result.attempts,
      final_state: result.final_state,
      history: result.history.map(h => ({
        agent: h.agent, attempt: h.attempt,
        ok: h.ok, files: h.files?.length || 0,
        commit: h.commit || null,
      })),
    });
  } catch (e) {
    if (/Unknown project/.test(e.message)) {
      return res.status(404).json({ error: 'unknown project: ' + pid });
    }
    if (/not found in project/.test(e.message)) {
      return res.status(404).json({ error: 'unknown unit: ' + only });
    }
    throw e;
  }
}));

// GET /api/devloop/projects/:pid/state
router.get('/projects/:pid/state', h(async (req, res) => {
  try {
    const state = loopController.getProjectState(req.params.pid);
    res.json(state);
  } catch (e) {
    if (/Unknown project/.test(e.message)) {
      return res.status(404).json({ error: 'unknown project: ' + req.params.pid });
    }
    throw e;
  }
}));

module.exports = router;