api.html
11.1 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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>REST API + 骨架 · LemonBoss PRD</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="assets/shell.css" />
</head>
<body data-page="api.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">06 · API</span><span class="sep">/</span><span>REST + 骨架</span></div>
<h1>REST API + 代码骨架</h1>
<p class="deck">新增 5 个 REST 端点,全部挂在 <code>/api/devloop/*</code>。骨架代码合计约 700 行,分布在 3 个新文件 + 1 处修改。</p>
</header>
<article class="body">
<section>
<h2>端点清单</h2>
<table>
<thead><tr><th>方法</th><th>路径</th><th>说明</th><th>触发的 Loop</th></tr></thead>
<tbody>
<tr><td><code>POST</code></td><td><code>/api/devloop/projects</code></td><td>创建项目,传入初始需求</td><td>L3 启动</td></tr>
<tr><td><code>POST</code></td><td><code>/api/devloop/projects/:pid/requirement</code></td><td>追加人类反馈,触发 PRD 增量更新</td><td>L3</td></tr>
<tr><td><code>POST</code></td><td><code>/api/devloop/projects/:pid/run</code></td><td>启动 / 续跑 LoopController</td><td>L1 + L2</td></tr>
<tr><td><code>GET</code></td><td><code>/api/devloop/projects/:pid/state</code></td><td>返回 units.json + 当前 loop 阶段</td><td>UI 轮询</td></tr>
<tr><td><code>GET</code></td><td><code>/api/devloop/projects/:pid/stream</code></td><td>SSE — 实时推送 Agent 输出 / 状态变更</td><td>UI 流式</td></tr>
<tr><td><code>POST</code></td><td><code>/api/devloop/projects/:pid/sign-off</code></td><td>人类显式 sign-off,状态置 <code>shipped-final</code></td><td>L3 终止</td></tr>
<tr><td><code>POST</code></td><td><code>/api/devloop/projects/:pid/abandon</code></td><td>放弃项目(保留数据,从列表移除)</td><td>L3 终止</td></tr>
</tbody>
</table>
</section>
<section>
<h2>Request / Response 示例</h2>
<div class="tabs" data-tabs>
<div class="tabbar" role="tablist">
<button role="tab" aria-selected="true" aria-controls="t-c">create</button>
<button role="tab" aria-selected="false" aria-controls="t-r">requirement (L3)</button>
<button role="tab" aria-selected="false" aria-controls="t-s">state</button>
<button role="tab" aria-selected="false" aria-controls="t-sse">SSE stream</button>
</div>
<div class="tabpanel" id="t-c">
<pre class="code"><span class="c"># 创建项目</span>
<span class="c"># 严格按 CLAUDE.md 警告:先写 /tmp/req.json,再 -d @file</span>
curl -X POST http://localhost:3010/api/devloop/projects \
-H <span class="s">"Content-Type: application/json"</span> \
-d @/tmp/create.json
<span class="c"># /tmp/create.json — 单行紧凑</span>
<span class="p">{</span><span class="n">"company_id"</span>:<span class="s">"c-lemonv-001"</span>,<span class="n">"name"</span>:<span class="s">"Auto Auth System"</span>,<span class="n">"requirement"</span>:<span class="s">"需要一个邮箱密码登录的系统"</span><span class="p">}</span>
<span class="c"># 响应</span>
<span class="p">{</span>
<span class="n">"project_id"</span>: <span class="s">"p-731042"</span>,
<span class="n">"status"</span>: <span class="s">"prd_drafting"</span>,
<span class="n">"stream_url"</span>: <span class="s">"/api/devloop/projects/p-731042/stream"</span>
<span class="p">}</span></pre>
</div>
<div class="tabpanel" id="t-r" hidden>
<pre class="code"><span class="c"># 追加新需求</span>
curl -X POST http://localhost:3010/api/devloop/projects/p-731042/requirement \
-H <span class="s">"Content-Type: application/json"</span> \
-d @/tmp/feedback.json
<span class="c"># /tmp/feedback.json</span>
<span class="p">{</span><span class="n">"text"</span>:<span class="s">"再加 GitHub OAuth"</span>,<span class="n">"origin"</span>:<span class="s">"yibo"</span><span class="p">}</span>
<span class="c"># 响应(即刻返回,PM 是异步跑)</span>
<span class="p">{</span>
<span class="n">"accepted"</span>: <span class="v">true</span>,
<span class="n">"queued_rev"</span>: <span class="v">3</span>,
<span class="n">"eta_seconds"</span>: <span class="v">30</span>
<span class="p">}</span></pre>
</div>
<div class="tabpanel" id="t-s" hidden>
<pre class="code"><span class="c"># 查询状态</span>
curl http://localhost:3010/api/devloop/projects/p-731042/state
<span class="p">{</span>
<span class="n">"project_id"</span>: <span class="s">"p-731042"</span>,
<span class="n">"prd_rev"</span>: <span class="s">"3"</span>,
<span class="n">"sdd_rev"</span>: <span class="s">"5"</span>,
<span class="n">"current_loop"</span>: <span class="s">"L1"</span>,
<span class="n">"units"</span>: <span class="p">[</span>
<span class="p">{</span> <span class="n">"id"</span>: <span class="s">"auth-be"</span>, <span class="n">"status"</span>: <span class="s">"passed"</span> <span class="p">}</span>,
<span class="p">{</span> <span class="n">"id"</span>: <span class="s">"auth-fe"</span>, <span class="n">"status"</span>: <span class="s">"testing"</span> <span class="p">}</span>,
<span class="p">{</span> <span class="n">"id"</span>: <span class="s">"oauth-be"</span>, <span class="n">"status"</span>: <span class="s">"pending"</span> <span class="p">}</span>
<span class="p">]</span>,
<span class="n">"l2_rounds"</span>: <span class="v">0</span>,
<span class="n">"l2_budget"</span>: <span class="v">3</span>,
<span class="n">"agents_active"</span>: <span class="p">[</span><span class="s">"tester:auth-fe"</span><span class="p">]</span>
<span class="p">}</span></pre>
</div>
<div class="tabpanel" id="t-sse" hidden>
<pre class="code"><span class="c"># SSE 流(浏览器或 curl --no-buffer)</span>
curl -N http://localhost:3010/api/devloop/projects/p-731042/stream
event: unit.state
data: {"unit_id":"auth-fe","from":"coding","to":"testing"}
event: agent.stream
data: {"agent":"tester","unit_id":"auth-fe","chunk":"PASS tests/login.spec.ts > valid creds\n"}
event: agent.stream
data: {"agent":"tester","unit_id":"auth-fe","chunk":"FAIL tests/login.spec.ts > invalid pwd\n"}
event: unit.state
data: {"unit_id":"auth-fe","from":"testing","to":"failed","retries":1}
event: prd.updated
data: {"rev":4,"changeSummary":"+ NFR: session timeout 1h"}</pre>
</div>
</div>
</section>
<section>
<h2>routes/devloop.js 骨架</h2>
<pre class="code"><span class="c">// server/routes/devloop.js · NEW · ~200 行</span>
<span class="k">const</span> express = <span class="n">require</span>(<span class="s">'express'</span>);
<span class="k">const</span> <span class="p">{</span> loopController <span class="p">}</span> = <span class="n">require</span>(<span class="s">'../src/core/loop-controller'</span>);
<span class="k">const</span> <span class="p">{</span> docStore <span class="p">}</span> = <span class="n">require</span>(<span class="s">'../services/doc-store'</span>);
<span class="k">const</span> <span class="p">{</span> unitRegistry <span class="p">}</span> = <span class="n">require</span>(<span class="s">'../services/unit-registry'</span>);
<span class="k">const</span> <span class="p">{</span> sseBroker <span class="p">}</span> = <span class="n">require</span>(<span class="s">'../services/sse-broker'</span>);
<span class="k">const</span> router = express.<span class="n">Router</span>();
router.<span class="n">post</span>(<span class="s">'/projects'</span>, <span class="k">async</span> (req, res) => <span class="p">{</span>
<span class="k">const</span> <span class="p">{</span> company_id, name, requirement <span class="p">}</span> = req.body;
<span class="k">const</span> pid = <span class="k">await</span> loopController.<span class="n">createProject</span>(<span class="p">{</span> company_id, name <span class="p">}</span>);
<span class="n">loopController</span>.<span class="n">onHumanFeedback</span>(pid, <span class="p">{</span> text: requirement, origin: req.user?.id <span class="p">}</span>);
res.<span class="n">json</span>(<span class="p">{</span> project_id: pid, status: <span class="s">'prd_drafting'</span>,
stream_url: <span class="s">`/api/devloop/projects/</span><span class="v">${pid}</span><span class="s">/stream`</span> <span class="p">}</span>);
<span class="p">}</span>);
router.<span class="n">post</span>(<span class="s">'/projects/:pid/requirement'</span>, <span class="k">async</span> (req, res) => <span class="p">{</span>
loopController.<span class="n">onHumanFeedback</span>(req.params.pid, req.body);
res.<span class="n">json</span>(<span class="p">{</span> accepted: <span class="v">true</span> <span class="p">}</span>);
<span class="p">}</span>);
router.<span class="n">get</span>(<span class="s">'/projects/:pid/state'</span>, (req, res) => <span class="p">{</span>
res.<span class="n">json</span>(loopController.<span class="n">getState</span>(req.params.pid));
<span class="p">}</span>);
router.<span class="n">get</span>(<span class="s">'/projects/:pid/stream'</span>, (req, res) => <span class="p">{</span>
sseBroker.<span class="n">subscribe</span>(req.params.pid, req, res);
<span class="p">}</span>);
<span class="k">module.exports</span> = router;</pre>
</section>
<section>
<h2>挂载到 server/index.js</h2>
<p>仅 1 行改动:</p>
<pre class="code"><span class="c">// server/index.js 第 90 行后追加</span>
app.<span class="n">use</span>(<span class="s">'/api/devloop'</span>, <span class="n">require</span>(<span class="s">'./routes/devloop'</span>));</pre>
</section>
<section>
<h2>认证</h2>
<p>沿用 <code>routes/auth.js</code> 的 JWT 中间件:</p>
<ul>
<li>POST 类端点:必须带 <code>Authorization: Bearer <jwt></code>,校验 <code>req.user.company_id</code> 与 path 中的 cid 匹配。</li>
<li>GET state / stream:同上。</li>
<li>Open API 调用(无人类 cookie,纯 server-to-server):使用现有的 <code>OPEN_API_KEYS</code> 环境变量 + <code>X-API-Key</code> header。</li>
</ul>
<div class="callout warn">
初版不做 RBAC(任何登录用户都能创建项目)。多用户隔离靠 cid。这是已知简化,P5 再加。
</div>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site"><div><b>Section</b>06 · API</div><div><b>Page</b>api.html</div></footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>