api-sse.html 18.3 KB
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SSE 事件总线 · 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-sse.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>SSE 事件总线</span></div>
      <h1>SSE 事件总线</h1>
      <p class="deck">15 类事件的完整规格。每条带 event 名、payload schema、发出时机、消费者。前端 Dashboard 是主消费者;CLI 工具也可订阅做实时监听。9 个核心 + 6 个 Agent 持久化事件。</p>
    </header>
    <article class="body">

      <section>
        <h2>订阅协议</h2>
        <pre class="code"><span class="c"># 普通订阅</span>
GET /api/devloop/projects/:pid/stream
Accept: text/event-stream
Authorization: Bearer &lt;jwt&gt;

<span class="c"># 断线重连</span>
GET /api/devloop/projects/:pid/stream
Last-Event-ID: 1234        <span class="c"># 上次收到的最大 id</span>

<span class="c"># 服务端响应(标准 SSE 格式)</span>
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

id: 1235
event: unit.state
data: {"unit_id":"auth-be","from":"coding","to":"testing"}

:keepalive    <span class="c"># 每 25s 一次</span></pre>
      </section>

      <section>
        <h2>核心事件(9 类)</h2>

        <details class="acc" open>
          <summary>① <code>prd.updated</code> · PM 写完 PRD<span class="role">L3</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"rev"</span>: <span class="v">3</span>,
  <span class="n">"by"</span>: <span class="s">"pm-agent"</span>,
  <span class="n">"origin"</span>: <span class="s">"yibo"</span>,
  <span class="n">"changeSummary"</span>: <span class="s">"+ Section: OAuth login"</span>,
  <span class="n">"hasConflict"</span>: <span class="v">false</span>
<span class="p">}</span></pre>
            <p><b>发出时机:</b>PM Agent 写完 prd.html,DocStore 校验通过后</p>
            <p><b>消费者:</b>Dashboard PRD diff 面板 · Architect 队列触发</p>
          </div>
        </details>

        <details class="acc">
          <summary>② <code>sdd.updated</code> · Architect 写完 SDD<span class="role">L3 + L2</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"rev"</span>: <span class="v">5</span>,
  <span class="n">"mode"</span>: <span class="s">"generate"</span> | <span class="s">"patch"</span>,
  <span class="n">"derived_from"</span>: <span class="s">"prd@rev3"</span>,
  <span class="n">"units_diff"</span>: <span class="p">{</span>
    <span class="n">"added"</span>:   <span class="p">[</span><span class="s">"oauth-be"</span>, <span class="s">"oauth-fe"</span><span class="p">]</span>,
    <span class="n">"changed"</span>: <span class="p">[]</span>,
    <span class="n">"removed"</span>: <span class="p">[]</span>
  <span class="p">}</span>
<span class="p">}</span></pre>
          </div>
        </details>

        <details class="acc">
          <summary>③ <code>unit.state</code> · Unit 状态机转换<span class="role">L1</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"unit_id"</span>: <span class="s">"auth-be"</span>,
  <span class="n">"from"</span>: <span class="s">"coding"</span>,
  <span class="n">"to"</span>: <span class="s">"testing"</span>,
  <span class="n">"retries"</span>: <span class="v">1</span>,
  <span class="n">"reason"</span>: <span class="s">"coder_done"</span>
<span class="p">}</span></pre>
            <p><b>消费者:</b>Dashboard DAG 节点重染色</p>
          </div>
        </details>

        <details class="acc">
          <summary>④ <code>agent.stream</code> · Agent stdout/stderr 流<span class="role">L1 + L2</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"agent"</span>: <span class="s">"tester"</span>,
  <span class="n">"unit_id"</span>: <span class="s">"auth-fe"</span>,
  <span class="n">"stream"</span>: <span class="s">"stdout"</span> | <span class="s">"stderr"</span>,
  <span class="n">"chunk"</span>: <span class="s">"FAIL  tests/login.spec.ts > invalid pwd\n"</span>
<span class="p">}</span></pre>
            <p><b>注:</b>高频事件。前端 LiveAgentStream 用 RAF 批渲染,避免每条都触发 reactive 更新。</p>
          </div>
        </details>

        <details class="acc">
          <summary>⑤ <code>integration.result</code> · L2 结果<span class="role">L2</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"run_id"</span>: <span class="s">"run-int-20260513-1031"</span>,
  <span class="n">"ok"</span>: <span class="v">false</span>,
  <span class="n">"failures_count"</span>: <span class="v">1</span>,
  <span class="n">"duration_ms"</span>: <span class="v">8421</span>
<span class="p">}</span></pre>
          </div>
        </details>

        <details class="acc">
          <summary>⑥ <code>triage.result</code> · BugTriager 归因完成<span class="role">L2</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"category"</span>: <span class="s">"contract_mismatch"</span>,
  <span class="n">"confidence"</span>: <span class="v">0.86</span>,
  <span class="n">"affected_units"</span>: <span class="p">[</span><span class="s">"auth-be"</span>, <span class="s">"auth-fe"</span><span class="p">]</span>,
  <span class="n">"will_auto_apply"</span>: <span class="v">true</span>
<span class="p">}</span></pre>
          </div>
        </details>

        <details class="acc">
          <summary>⑦ <code>human_intervention_needed</code> · 需要人类介入<span class="role">L2 / L1 blocked</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"reason"</span>: <span class="s">"low_confidence_triage"</span> | <span class="s">"unit_blocked"</span> | <span class="s">"l2_exhausted"</span> | <span class="s">"prd_conflict"</span>,
  <span class="n">"suggestion"</span>: <span class="s">"SDD token 续期策略未规定,需要决策"</span>,
  <span class="n">"context"</span>: <span class="p">{</span> /* triage.json / unit info / etc */ <span class="p">}</span>
<span class="p">}</span></pre>
            <p><b>消费者:</b>Dashboard 弹紫色 toast。</p>
          </div>
        </details>

        <details class="acc">
          <summary>⑧ <code>project.shipped</code> · 项目交付<span class="role">L2</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"final_rev"</span>: <span class="p">{</span> <span class="n">"prd"</span>: <span class="v">3</span>, <span class="n">"sdd"</span>: <span class="v">5</span> <span class="p">}</span>,
  <span class="n">"total_duration_ms"</span>: <span class="v">1_842_000</span>,
  <span class="n">"total_tokens"</span>: <span class="v">487_321</span>,
  <span class="n">"units"</span>: <span class="v">12</span>
<span class="p">}</span></pre>
            <p><b>UI 反应:</b>整页 confetti 5s + L2 状态卡变深绿。</p>
          </div>
        </details>

        <details class="acc">
          <summary>⑨ <code>project.exhausted</code> · L2 预算耗尽<span class="role">L2</span></summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"rounds"</span>: <span class="v">3</span>,
  <span class="n">"last_triage"</span>: <span class="p">{</span> /* triage.json */ <span class="p">}</span>,
  <span class="n">"next_action"</span>: <span class="s">"awaiting_human_decision"</span>
<span class="p">}</span></pre>
          </div>
        </details>
      </section>

      <section>
        <h2>Agent 持久化事件(6 类)· P-8 触发</h2>
        <p>由 P-8 <code>callAgentWithPersistence</code> 流程发出,用于 Dashboard 渲染「Agent at step 3/5」+ 崩溃恢复对话框。</p>

        <details class="acc" open>
          <summary>⑩ <code>agent.started</code> · Agent 调用开始</summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"agent"</span>: <span class="s">"architect"</span>,
  <span class="n">"call_id"</span>: <span class="s">"call-20260513-1031-7f3a"</span>,
  <span class="n">"unit_id"</span>: <span class="v">null</span>,
  <span class="n">"input_summary"</span>: <span class="s">"mode=generate, prd@rev3"</span>
<span class="p">}</span></pre>
            <p><b>发出时机:</b>statePath 写入 status=running 后立即</p>
          </div>
        </details>

        <details class="acc">
          <summary>⑪ <code>agent.todolist</code> · Agent 写完 todolist</summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"agent"</span>: <span class="s">"architect"</span>,
  <span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
  <span class="n">"items"</span>: <span class="p">[</span>
    <span class="p">{</span> <span class="n">"id"</span>:<span class="s">"step-001"</span>, <span class="n">"description"</span>:<span class="s">"读 prd.html"</span>, <span class="n">"status"</span>:<span class="s">"todo"</span> <span class="p">}</span>,
    <span class="p">{</span> <span class="n">"id"</span>:<span class="s">"step-002"</span>, <span class="n">"description"</span>:<span class="s">"识别功能列表"</span>,    <span class="n">"status"</span>:<span class="s">"todo"</span> <span class="p">}</span>,
    ...
  <span class="p">]</span>
<span class="p">}</span></pre>
            <p><b>消费者:</b>Dashboard 展开 UnitDrawer / Agent panel 显示子任务列表</p>
          </div>
        </details>

        <details class="acc">
          <summary>⑫ <code>agent.step</code> · 子任务进度更新</summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"agent"</span>: <span class="s">"architect"</span>,
  <span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
  <span class="n">"item_id"</span>: <span class="s">"step-003"</span>,
  <span class="n">"from"</span>: <span class="s">"doing"</span>,
  <span class="n">"to"</span>: <span class="s">"done"</span>,
  <span class="n">"progress_pct"</span>: <span class="v">60</span>,
  <span class="n">"current_desc"</span>: <span class="s">"为 OAuth 拆 3 个 Unit"</span>
<span class="p">}</span></pre>
            <p><b>消费者:</b>Dashboard 顶部 Agent 卡片显示「Architect: 3/5」+ desc</p>
          </div>
        </details>

        <details class="acc">
          <summary>⑬ <code>agent.done</code> · 成功完成</summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"agent"</span>: <span class="s">"architect"</span>,
  <span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
  <span class="n">"duration_ms"</span>: <span class="v">42_318</span>,
  <span class="n">"tokens_used"</span>: <span class="v">8_421</span>,
  <span class="n">"output_ref"</span>: <span class="s">"agent-state/snapshots/call-..-out.json"</span>
<span class="p">}</span></pre>
          </div>
        </details>

        <details class="acc">
          <summary>⑭ <code>agent.error</code> · 异常退出</summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"agent"</span>: <span class="s">"coder"</span>,
  <span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
  <span class="n">"code"</span>: <span class="s">"SANDBOX_VIOLATION"</span> | <span class="s">"TIMEOUT"</span> | <span class="s">"AGENT_CRASH"</span>,
  <span class="n">"message"</span>: <span class="s">"试图写 ../profile-be/code/server.js"</span>,
  <span class="n">"recoverable"</span>: <span class="v">true</span>
<span class="p">}</span></pre>
          </div>
        </details>

        <details class="acc">
          <summary>⑮ <code>resume_decision_needed</code> · 重启后等决策</summary>
          <div class="inner">
<pre class="code"><span class="p">{</span>
  <span class="n">"agent"</span>: <span class="s">"architect"</span>,
  <span class="n">"call_id"</span>: <span class="s">"call-..."</span>,
  <span class="n">"last_step"</span>: <span class="v">3</span>, <span class="n">"total"</span>: <span class="v">5</span>,
  <span class="n">"paused_at"</span>: <span class="s">"2026-05-13T10:42:13Z"</span>,
  <span class="n">"resume_token"</span>: <span class="s">"rt-..."</span>,
  <span class="n">"options"</span>: <span class="p">[</span><span class="s">"resume_from_step"</span>, <span class="s">"restart_fresh"</span>, <span class="s">"abort"</span><span class="p">]</span>
<span class="p">}</span></pre>
            <p><b>发出时机:</b>P-8 <code>recoverOnBoot</code> 扫到 status=running(即上次进程被杀)</p>
            <p><b>UI 反应:</b>Dashboard 顶部 banner 加紫色「⚠ 上次启动有 N 个 Agent 中断,需要决策」</p>
          </div>
        </details>
      </section>

      <section>
        <h2>事件全表</h2>
        <table class="dense">
          <thead><tr><th>id</th><th>event</th><th>层</th><th>频率</th><th>关键 payload</th></tr></thead>
          <tbody>
            <tr><td>1</td><td><code>prd.updated</code></td><td>L3</td><td>低</td><td>rev, changeSummary, hasConflict</td></tr>
            <tr><td>2</td><td><code>sdd.updated</code></td><td>L2/L3</td><td>低</td><td>rev, mode, units_diff</td></tr>
            <tr><td>3</td><td><code>unit.state</code></td><td>L1</td><td>中</td><td>unit_id, from, to, retries</td></tr>
            <tr><td>4</td><td><code>agent.stream</code></td><td>L1/L2</td><td>高(千 / 项目)</td><td>chunk</td></tr>
            <tr><td>5</td><td><code>integration.result</code></td><td>L2</td><td>低(1-3 次 / 项目)</td><td>ok, failures_count</td></tr>
            <tr><td>6</td><td><code>triage.result</code></td><td>L2</td><td>低</td><td>category, confidence, affected_units</td></tr>
            <tr><td>7</td><td><code>human_intervention_needed</code></td><td>—</td><td>极低</td><td>reason, suggestion</td></tr>
            <tr><td>8</td><td><code>project.shipped</code></td><td>—</td><td>1 次</td><td>final_rev, total_tokens</td></tr>
            <tr><td>9</td><td><code>project.exhausted</code></td><td>—</td><td>极低</td><td>rounds, last_triage</td></tr>
            <tr><td>10</td><td><code>agent.started</code></td><td>L1/L2/L3</td><td>中</td><td>agent, call_id, input_summary</td></tr>
            <tr><td>11</td><td><code>agent.todolist</code></td><td>同上</td><td>中</td><td>items[]</td></tr>
            <tr><td>12</td><td><code>agent.step</code></td><td>同上</td><td>中</td><td>item_id, from, to, progress_pct</td></tr>
            <tr><td>13</td><td><code>agent.done</code></td><td>同上</td><td>中</td><td>duration_ms, tokens_used</td></tr>
            <tr><td>14</td><td><code>agent.error</code></td><td>同上</td><td>低</td><td>code, message, recoverable</td></tr>
            <tr><td>15</td><td><code>resume_decision_needed</code></td><td>boot</td><td>极低</td><td>options[]</td></tr>
          </tbody>
        </table>
      </section>

      <section>
        <h2>客户端订阅样板</h2>
<pre class="code"><span class="c">// Vue 3 composable</span>
<span class="k">function</span> <span class="n">useDevLoopStream</span>(pid) <span class="p">{</span>
  <span class="k">const</span> es = <span class="k">new</span> <span class="n">EventSource</span>(<span class="s">`/api/devloop/projects/</span><span class="v">${pid}</span><span class="s">/stream`</span>);

  es.<span class="n">addEventListener</span>(<span class="s">'unit.state'</span>, e =&gt; <span class="n">handleUnitState</span>(<span class="n">JSON.parse</span>(e.<span class="n">data</span>)));
  es.<span class="n">addEventListener</span>(<span class="s">'agent.stream'</span>, e =&gt; <span class="n">handleStream</span>(<span class="n">JSON.parse</span>(e.<span class="n">data</span>)));
  es.<span class="n">addEventListener</span>(<span class="s">'prd.updated'</span>, e =&gt; <span class="n">refreshPrd</span>());
  es.<span class="n">addEventListener</span>(<span class="s">'human_intervention_needed'</span>, e =&gt; <span class="n">showToast</span>(<span class="n">JSON.parse</span>(e.<span class="n">data</span>)));
  es.<span class="n">addEventListener</span>(<span class="s">'project.shipped'</span>, () =&gt; <span class="n">celebrate</span>());

  <span class="c">// 浏览器原生支持 Last-Event-ID 自动重连,不需要手动写</span>
  onUnmounted(() =&gt; es.<span class="n">close</span>());
<span class="p">}</span></pre>
      </section>

      <section>
        <h2>事件序号与 Replay</h2>
        <p>每条事件在 <code>stream.log</code> 中按顺序持久化。Last-Event-ID 重连时,broker 从 log 文件读 id &gt; 该值的所有事件批量补发,再切实时流。</p>
        <pre class="code"><span class="c"># stream.log 格式(NDJSON)</span>
<span class="p">{</span><span class="n">"id"</span>:<span class="v">1234</span>,<span class="n">"event"</span>:<span class="s">"unit.state"</span>,<span class="n">"data"</span>:<span class="p">{</span>...<span class="p">}</span>,<span class="n">"ts"</span>:<span class="v">1747120731</span><span class="p">}</span>
<span class="p">{</span><span class="n">"id"</span>:<span class="v">1235</span>,<span class="n">"event"</span>:<span class="s">"agent.stream"</span>,<span class="n">"data"</span>:<span class="p">{</span>...<span class="p">}</span>,<span class="n">"ts"</span>:<span class="v">1747120732</span><span class="p">}</span></pre>
        <p class="sig">log 保留 7 天,超出自动 gzip 归档。</p>
      </section>

    </article>
    <nav class="prev-next"></nav>
    <footer class="site"><div><b>Section</b>06 · API</div><div><b>Page</b>api-sse.html</div></footer>
  </main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>