# Step 66: HTTP + SSE 后端
一句话导读:把
QueryEngine.run()的 async generator 事件流直接映射成 SSE,证明同一个无头内核可以换一张 Web 前端。
# 一、这一步做了什么(What)
step66 新增 server/session.ts 和 server/httpServer.ts。createSession() 组装一个可运行的 agent 会话;HTTP 服务提供 POST /chat,收到用户消息后跑 engine.run(),把每个 EngineEvent 序列化成 SSE data: 推回客户端。
它是 Phase 7 Part B 的起点:不继续深挖手写 Ink,而是走真实 Claude Code 的多前端路线。
# 二、面试官视角:为什么要做?(Why)
面试题:已经有终端 REPL 了,为什么还要把 agent 包成 HTTP 服务?
因为真实 Claude Code 本质是“无头内核 + 多个 surface”。终端只是一个 surface,Web、SDK、IDE、Bridge 都可以消费同一条事件流。只要 QueryEngine 不绑定终端,换前端就是工程装配问题。
| 终端 REPL | HTTP/SSE |
|---|---|
| readline 输入 | POST JSON |
| console 渲染事件 | SSE 推事件 |
| 本地交互 | 浏览器/远端可消费 |
# 三、原理:它是怎么工作的(How)
SSE 处理在 server/httpServer.ts:
res.writeHead(200, {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
for await (const ev of session.engine.run(message)) {
res.write(`data: ${JSON.stringify(ev)}\n\n`);
}
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
2
3
4
5
6
7
8
9
10
请求体中文坑的修复也很关键:
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk as Buffer);
const body = Buffer.concat(chunks).toString("utf8");
2
3
数据流:
浏览器 POST /chat
→ createSession().engine.run(message)
→ thinking/text/tool/round 事件
→ SSE data: {...}
→ 浏览器逐条渲染
2
3
4
5
# 四、深入追问(面试常见 follow-up)
Q:为什么用 SSE,不直接返回完整 JSON?
A:agent 输出是流式的,工具调用和 thinking 也会分阶段出现。SSE 能边生成边推,和 async generator 天然匹配。
Q:SSE 的限制是什么?
A:单向。服务器能推给浏览器,但浏览器不能在同一通道里回答权限请求,所以 step67 要换 WebSocket。
Q:为什么请求体要先 Buffer.concat 再 utf8 解码?
A:中文可能跨 chunk。边拼字符串会造成多字节字符拆坏,先收 Buffer 再统一解码才安全。
Q:这一步权限怎么处理?
A:step66 暂时非交互:危险命令仍由 Bash 分类器兜底,其它自动放行。真正交互式权限留给双向通道。
# 五、踩坑 / 设计权衡
这一步故意用 Node 原生 http,不是为了生产,而是为了看清协议本身:HTTP 请求进来,SSE 长连接出去,中间就是 for await 消费引擎事件。
# 六、与真实源码的对照
| 我们的实现 | Claude Code 源码 |
|---|---|
engine.run() → SSE | SDK/Bridge 消费 async 事件流 |
| 单会话 HTTP 服务 | 多会话、鉴权、远程 session |
| 非交互权限兜底 | 完整 canUseTool 控制通道 |
| 无取消/背压 | abort、断线、重连、状态同步 |
# 七、一句话总结
step66 的关键不是 HTTP 本身,而是证明 QueryEngine 已经足够无头:同一条事件流可以被终端消费,也可以被 Web 通过 SSE 消费。
# 下一节预告
下一篇是 step67:用 WebSocket 补上双向通道,让浏览器也能处理交互式权限。