api.html 11.1 KB
<!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) =&gt; <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) =&gt; <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) =&gt; <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) =&gt; <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 &lt;jwt&gt;</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>