devloop.js
3.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
* 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;