quantumbyte 把意图变成可验证应用
QuantumByte 不是只有一个“输入提示词、输出代码”的界面。它把生成任务写进 Postgres,由独立 worker 接手,再用同一条 SSE 同步聊天、项目状态、资产和业务需求验证结果;更关键的是,验证者只读源码,避免生成者给自己打分。
↓ SSE snapshot ↓ git workspace
requirements → read-only harness → verdict
它解决什么,凭什么值得拆
入口把首条产品意图和 Project 记录放进同一事务语义:创建时直接生成 PENDING 用户消息,因此页面层不需要再发第二个启动请求,调度器也能只观察数据库状态 对应源码落点是 src/app/api/projects/route.ts:32、src/app/api/projects/route.ts:34、src/app/api/projects/route.ts:39、src/app/api/projects/route.ts:40、src/app/api/projects/route.ts:42、src/app/api/projects/route.ts:45、src/app/api/projects/route.ts:48。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
后续对话仍沿用相同契约:接口只做输入清洗、附件链接拼接与 PENDING 入库,不在 Web 进程里运行模型。这个分离让 Agent 执行时间不会绑住 HTTP 请求 对应源码落点是 src/app/api/projects/[id]/chat/route.ts:17、src/app/api/projects/[id]/chat/route.ts:22、src/app/api/projects/[id]/chat/route.ts:37、src/app/api/projects/[id]/chat/route.ts:41、src/app/api/projects/[id]/chat/route.ts:46、src/app/api/projects/[id]/chat/route.ts:48、src/app/api/projects/[id]/chat/route.ts:53。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
▸核心数据
深度阅读 → 入口展开。
三应用,一张状态表
系统拓扑的关键不在消息中间件,而在“数据库即协议”:Web 写状态,Worker 更新状态,SSE 路由读取快照。每条连接有明确轮询、保活和最长时限,避免永久占用浏览器连接 对应源码落点是 src/app/api/projects/[id]/chat/stream/route.ts:5、src/app/api/projects/[id]/chat/stream/route.ts:8、src/app/api/projects/[id]/chat/stream/route.ts:9、src/app/api/projects/[id]/chat/stream/route.ts:10、src/app/api/projects/[id]/chat/stream/route.ts:15、src/app/api/projects/[id]/chat/stream/route.ts:18、src/app/api/projects/[id]/chat/stream/route.ts:21。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
▸系统拓扑
↑ SSE snapshot ↓
Preview iframe ← generated workspace → GitHub
▸模块清单
前端把聊天与 Harness 汇入单一 hook:先读取普通 GET 快照,再在任务或验证运行时打开 SSE。这不是简单 UI 封装,而是对浏览器同源连接上限的架构响应 对应源码落点是 src/components/blueprint/use-project-stream.ts:37、src/components/blueprint/use-project-stream.ts:40、src/components/blueprint/use-project-stream.ts:42、src/components/blueprint/use-project-stream.ts:44、src/components/blueprint/use-project-stream.ts:48、src/components/blueprint/use-project-stream.ts:57、src/components/blueprint/use-project-stream.ts:60。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
▸关键数据结构
32 export async function POST(req: NextRequest) {
33 const body = await req.json().catch(() => null);
34 const prompt = typeof body?.prompt === 'string' ? body.prompt.trim() : '';
35 if (!prompt) {
36 return NextResponse.json({ error: 'prompt is required' }, { status: 400 });
37 }
38
39 const title = prompt.length > 48 ? `${prompt.slice(0, 48)}…` : prompt;
40 const project = await prisma.project.create({
41 data: {
42 id: ulid(),
43 prompt,
44 title,
45 generationStatus: 'DRAFT',
46 // Seed the prompt as the first chat turn (PENDING) so the blueprint
47 // chat renders it and the orchestrator triggers the agent immediately.
48 chatMessages: {
49 create: {
50 id: ulid(),
51 role: 'USER',
52 content: prompt,
53 status: 'PENDING',
54 },
55 },
56 },
57 });
58
59 return NextResponse.json(project, { status: 201 });
60 }
消息与实时状态
▸PENDING 是跨进程交接点
消息接口落库后立即返回 201,真正的 Agent 由外部 Worker 运行。这里把“用户请求被接受”和“模型已经完成”拆成两个状态,使失败重试、租约恢复和多 Worker 分配都有可观测锚点 对应源码落点是 src/app/api/projects/[id]/chat/route.ts:53、src/app/api/projects/[id]/chat/route.ts:54、src/app/api/projects/[id]/chat/route.ts:55、src/app/api/projects/[id]/chat/route.ts:56、src/app/api/projects/[id]/chat/route.ts:57、src/app/api/projects/[id]/chat/route.ts:59、src/app/api/projects/[id]/chat/route.ts:63。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
48 const project = await prisma.project.findUnique({ where: { id } });
49 if (!project) {
50 return NextResponse.json({ error: 'project not found' }, { status: 404 });
51 }
52
53 // Persist the user turn as PENDING and stop. The orchestrator assigns it to a
54 // worker; the worker (Claude Agent SDK, warm session) runs the turn and writes
55 // the ASSISTANT reply + flips this row to COMPLETED/FAILED (RFC 0003). The web
56 // app never runs the agent — the client polls the GET endpoint for the result.
57 const message = await prisma.chatMessage.create({
58 data: {
59 id: ulid(),
60 projectId: id,
61 role: 'USER',
62 content,
63 status: 'PENDING',
64 },
65 });
66
67 return NextResponse.json({ message }, { status: 201 });
68 }
▸一条 SSE 承载四类状态
流接口并行读取消息、项目、资产和需求,序列化为统一快照。它只在 JSON 变化时发送数据,空闲时发送注释型 keepalive,因此前端得到的是一致视图,不必自行拼接四条异步订阅 对应源码落点是 src/app/api/projects/[id]/chat/stream/route.ts:41、src/app/api/projects/[id]/chat/stream/route.ts:45、src/app/api/projects/[id]/chat/stream/route.ts:51、src/app/api/projects/[id]/chat/stream/route.ts:56、src/app/api/projects/[id]/chat/stream/route.ts:67、src/app/api/projects/[id]/chat/stream/route.ts:71、src/app/api/projects/[id]/chat/stream/route.ts:77。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
41 const tick = async (): Promise<boolean> => {
42 // Also tail the Project (title/desc/logo) + its assets so product edits
43 // made by the worker's qb-designer tools surface live — the worker bumps
44 // Project.updatedAt / inserts Asset rows, and this snapshot picks it up.
45 // Also tail the harness tables + harnessRunning flag so pass-3 verdicts and
46 // the running indicator stream over this same connection (browsers cap ~6
47 // per origin, so the harness UI shares the chat stream instead of opening
48 // its own). The worker runs the harness right after the turn settles, so
49 // the in-flight condition below also keys off harnessRunning to keep this
50 // connection open across that hand-off.
51 const [messages, project, assets, requirements] = await Promise.all([
52 prisma.chatMessage.findMany({
53 where: { projectId: id },
54 orderBy: { createdAt: 'asc' },
55 }),
56 prisma.project.findUnique({
57 where: { id },
58 select: {
59 title: true,
60 desc: true,
61 logo: true,
62 status: true,
63 updatedAt: true,
64 harnessRunning: true,
65 },
66 }),
67 prisma.asset.findMany({
68 where: { projectId: id },
69 orderBy: { createdAt: 'asc' },
70 }),
71 prisma.harnessRequirement.findMany({
72 where: { projectId: id },
73 include: { result: true },
74 orderBy: { createdAt: 'asc' },
75 }),
76 ]);
77 const harness = {
78 requirements: requirements.map(serializeRequirement),
79 results: requirements
80 .map((r) => r.result)
81 .filter((r) => r !== null)
82 .map(serializeResult),
83 is_running: project?.harnessRunning ?? false,
84 };
85 const payload = { messages, project, assets, harness };
86 const snapshot = JSON.stringify(payload);
87 if (snapshot !== lastSnapshot) {
88 lastSnapshot = snapshot;
89 send(snapshot);
90 lastKeepalive = Date.now();
91 } else if (Date.now() - lastKeepalive > KEEPALIVE_MS) {
92 controller.enqueue(encoder.encode(': keepalive\n\n'));
93 lastKeepalive = Date.now();
94 }
95
96 const inFlight =
97 messages.some((m) => m.status === 'PENDING' || m.status === 'PROCESSING') ||
98 (project?.harnessRunning ?? false);
99 return inFlight;
100 };
从意图到可验收应用
客户端启动时先并发读取聊天与验证快照,只有状态进入运行中才打开流。这种“快照 + 临时增量”模式让刷新页面可恢复,又避免空闲项目长期保持 EventSource 对应源码落点是 src/components/blueprint/use-project-stream.ts:64、src/components/blueprint/use-project-stream.ts:65、src/components/blueprint/use-project-stream.ts:66、src/components/blueprint/use-project-stream.ts:67、src/components/blueprint/use-project-stream.ts:69、src/components/blueprint/use-project-stream.ts:73、src/components/blueprint/use-project-stream.ts:75。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
▸交互链:快照、流、收敛
COMPLETED + harness idle → server closes → refetch
▸验收链:人工需求与自动结果分离
Worker read-only audit → result upsert → shared SSE
验证契约与边界
Web 层专门用序列化器把 Prisma 枚举转换成稳定线格式:类型和严重度小写,结果字段采用 snake_case。这个小模块明确了数据库模型与前端契约的边界,测试也集中覆盖它 对应源码落点是 src/lib/harness.ts:8、src/lib/harness.ts:10、src/lib/harness.ts:13、src/lib/harness.ts:15、src/lib/harness.ts:19、src/lib/harness.ts:23、src/lib/harness.ts:25。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
▸测试覆盖
当前自动化测试最明确地覆盖 Harness 序列化:枚举大小写、locked 标记和结果字段映射都有断言。不过从仓库现状看,消息流、并发恢复和预览代理仍缺少同等级端到端测试 对应源码落点是 src/lib/harness.test.ts:38、src/lib/harness.test.ts:39、src/lib/harness.test.ts:40、src/lib/harness.test.ts:44、src/lib/harness.test.ts:50、src/lib/harness.test.ts:51、src/lib/harness.test.ts:57。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
38 describe('serializeRequirement', () => {
39 it('lowercases type and severity and keeps the rest verbatim', () => {
40 expect(serializeRequirement(requirement())).toEqual({
41 id: 'req_1',
42 excerpt: 'must send a receipt email',
43 description: 'A receipt email is sent after checkout',
44 type: 'functional',
45 severity: 'blocker',
46 locked: false,
47 });
48 });
49
50 it('preserves the locked flag', () => {
51 expect(serializeRequirement(requirement({ locked: true })).locked).toBe(
52 true,
53 );
54 });
55 });
56
57 describe('serializeResult', () => {
58 it('maps camelCase fields to the snake_case wire format', () => {
59 expect(serializeResult(result())).toEqual({
60 requirement_id: 'req_1',
61 result: 'PASS',
62 evidence: 'checkout_test.ts asserts the email',
63 run_status: 'DONE',
64 last_verified_sha: 'abc123',
65 watched_paths: ['src/checkout.ts'],
66 run_at: '2026-07-14T12:00:00.000Z',
67 is_stale: false,
68 });
69 });
70
71 it('emits null run_at when runAt is null', () => {
72 expect(serializeResult(result({ runAt: null })).run_at).toBeNull();
73 });
▸已知边界 / 失败模式
- 连接最长 5 分钟;长任务依靠关闭后的重新读取与再次开流延续。
- 预览把任何 HTTP 响应都视为 ready,编译错误页也会被展示。
- 附件只限制数量和体积,当前源码未显示内容安全扫描。
值得借鉴的工程决策
手动点击不会直接启动模型,而是写入 harnessRunRequested;重复点击在运行中保持幂等,Worker 之后领取该标志 对应源码落点是 src/app/api/projects/[id]/harness/run/route.ts:9、src/app/api/projects/[id]/harness/run/route.ts:13、src/app/api/projects/[id]/harness/run/route.ts:14、src/app/api/projects/[id]/harness/run/route.ts:18、src/app/api/projects/[id]/harness/run/route.ts:22、src/app/api/projects/[id]/harness/run/route.ts:23、src/app/api/projects/[id]/harness/run/route.ts:24。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
9 export async function POST(
10 _req: NextRequest,
11 { params }: { params: Promise<{ id: string }> }
12 ) {
13 const { id } = await params;
14 const project = await prisma.project.findUnique({
15 where: { id },
16 select: { id: true, harnessRunning: true },
17 });
18 if (!project) {
19 return NextResponse.json({ error: 'project not found' }, { status: 404 });
20 }
21
22 // Already running → nothing to request; the in-flight run will reflect current state.
23 if (!project.harnessRunning) {
24 await prisma.project.update({
25 where: { id },
26 data: { harnessRunRequested: true },
27 });
28 }
29
30 return NextResponse.json({ status: 'queued' }, { status: 202 });文件先进入对象存储和 Asset 表,聊天只携带 URL,因此 Agent 主链无需维护二进制协议 对应源码落点是 src/app/api/projects/[id]/attachments/route.ts:11、src/app/api/projects/[id]/attachments/route.ts:12、src/app/api/projects/[id]/attachments/route.ts:14、src/app/api/projects/[id]/attachments/route.ts:18、src/app/api/projects/[id]/attachments/route.ts:28、src/app/api/projects/[id]/attachments/route.ts:32、src/app/api/projects/[id]/attachments/route.ts:36。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
39 for (const file of files) {
40 if (file.size > MAX_FILE_BYTES) {
41 return NextResponse.json(
42 { error: `${file.name} exceeds the 10 MB limit` },
43 { status: 413 }
44 );
45 }
46 }
47
48 const uploaded = [];
49 for (const file of files) {
50 const assetId = ulid();
51 const filename = `${assetId}-${safeName(file.name)}`;
52 const key = `attachments/${id}/${filename}`;
53 const mimeType = file.type || 'application/octet-stream';
54 const url = await uploadToS3(key, Buffer.from(await file.arrayBuffer()), mimeType);
55 const asset = await prisma.asset.create({
56 data: {
57 id: assetId,
58 projectId: id,
59 filename,
60 originalFilename: file.name,
61 url,
62 size: file.size,
63 mimeType,
64 },
65 });
66 uploaded.push({
67 id: asset.id,
68 url: asset.url,
69 filename: file.name,
70 mimeType,
71 size: file.size,
72 });
73 }
74
75 return NextResponse.json({ attachments: uploaded }, { status: 201 });技术亮点之外,别忽略这些
预览代理目前假设 Web 与 Worker 共享 localhost;源码注释已把迁移到真实主机列作后续方向。这意味着现阶段的部署拓扑不能直接水平扩展到隔离节点 对应源码落点是 src/app/api/projects/[id]/preview/route.ts:4、src/app/api/projects/[id]/preview/route.ts:7、src/app/api/projects/[id]/preview/route.ts:9、src/app/api/projects/[id]/preview/route.ts:10、src/app/api/projects/[id]/preview/route.ts:11、src/app/api/projects/[id]/preview/route.ts:15、src/app/api/projects/[id]/preview/route.ts:30。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。
这组实现把输入校验、持久状态、异步执行和界面恢复连接成可以逐段核对的契约。阅读时不仅要看正常返回,还要看空值、重复动作、连接关闭和下一轮恢复的分支;这些分支共同决定用户看到的是可靠进度还是永远旋转的等待状态。 第一处证据位于 src/components/blueprint/use-project-stream.ts:10、src/components/blueprint/use-project-stream.ts:12、src/components/blueprint/use-project-stream.ts:14,它界定了入口和基础状态。
这组实现把输入校验、持久状态、异步执行和界面恢复连接成可以逐段核对的契约。阅读时不仅要看正常返回,还要看空值、重复动作、连接关闭和下一轮恢复的分支;这些分支共同决定用户看到的是可靠进度还是永远旋转的等待状态。 第二处证据位于 src/components/blueprint/use-project-stream.ts:16、src/components/blueprint/use-project-stream.ts:18、src/components/blueprint/use-project-stream.ts:20,它展示了处理分支与数据变换。
这组实现把输入校验、持久状态、异步执行和界面恢复连接成可以逐段核对的契约。阅读时不仅要看正常返回,还要看空值、重复动作、连接关闭和下一轮恢复的分支;这些分支共同决定用户看到的是可靠进度还是永远旋转的等待状态。 第三处证据位于 src/components/blueprint/use-project-stream.ts:22、src/components/blueprint/use-project-stream.ts:24、src/components/blueprint/use-project-stream.ts:26,它说明了输出、失败和恢复边界。