quantumbyteQB
§ · Nested detail page

实时状态流 深度解析

这条流不是 token 专线,而是项目一致性通道:消息、项目元数据、资产与 Harness 共享一个 JSON 快照。

§ 00 · IMPORT CHAIN

导入链与契约

服务端用 400ms 轮询换取无 Broker 的简单一致性,并通过 15 秒保活和 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:19、src/app/api/projects/[id]/chat/stream/route.ts:21。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/app/api/projects/[id]/chat/stream/route.ts · L30-50TypeScript · Python
  30    const stream = new ReadableStream({
  31      async start(controller) {
  32        let closed = false;
  33        const startedAt = Date.now();
  34        let lastSnapshot = '';
  35        let lastKeepalive = Date.now();
  36  
  37        const send = (data: string) => {
  38          controller.enqueue(encoder.encode(`data: ${data}\n\n`));
  39        };
  40  
  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.
§ 01 · CORE IMPL

核心实现,逐段拆

▸快照去重与运行态判定

每次 tick 并行查四组数据,随后拼成一个字符串快照;只有字符串变化才发 data 帧。运行态同时检查 PENDING、PROCESSING 与 harnessRunning,覆盖 Agent 到验收的交接空隙 对应源码落点是 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、src/app/api/projects/[id]/chat/stream/route.ts:85、src/app/api/projects/[id]/chat/stream/route.ts:86。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/app/api/projects/[id]/chat/stream/route.ts · L51-100TypeScript · Python
  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        };

▸客户端自己掌控读流

前端使用 fetch reader 而不是 EventSource,以便在空闲时不自动重连;它自己拆分双换行事件、解析 data 行,并在流关闭后重新拉取最终快照 对应源码落点是 src/components/blueprint/use-project-stream.ts:83、src/components/blueprint/use-project-stream.ts:88、src/components/blueprint/use-project-stream.ts:89、src/components/blueprint/use-project-stream.ts:93、src/components/blueprint/use-project-stream.ts:98、src/components/blueprint/use-project-stream.ts:101、src/components/blueprint/use-project-stream.ts:105。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/components/blueprint/use-project-stream.ts · L83-128TypeScript · Python
  83    /**
  84     * Tail the SSE stream. Uses fetch + a reader (not EventSource) so we control the
  85     * lifecycle — the server self-closes once neither a turn nor the harness is in
  86     * flight, and we don't auto-reconnect while idle.
  87     */
  88    const startStream = useCallback(async () => {
  89      if (abortRef.current) return; // already tailing
  90      const ac = new AbortController();
  91      abortRef.current = ac;
  92      try {
  93        const res = await fetch(`/api/projects/${projectId}/chat/stream`, {
  94          signal: ac.signal,
  95          headers: { Accept: 'text/event-stream' },
  96        });
  97        if (!res.ok || !res.body) return;
  98        const reader = res.body.getReader();
  99        const decoder = new TextDecoder();
 100        let buffer = '';
 101        for (;;) {
 102          const { value, done } = await reader.read();
 103          if (done) break;
 104          buffer += decoder.decode(value, { stream: true });
 105          const events = buffer.split('\n\n');
 106          buffer = events.pop() ?? '';
 107          for (const evt of events) {
 108            const line = evt.split('\n').find((l) => l.startsWith('data:'));
 109            if (!line) continue;
 110            try {
 111              const payload = JSON.parse(line.slice(5).trim());
 112              if (payload.messages) setRows(payload.messages);
 113              if (payload.project) setProject(payload.project);
 114              if (payload.harness) setHarness(payload.harness);
 115            } catch {
 116              /* ignore partial/non-json */
 117            }
 118          }
 119        }
 120      } catch {
 121        /* aborted or network — ignore */
 122      } finally {
 123        if (abortRef.current === ac) abortRef.current = null;
 124        // Settle after the stream closes; if the harness kicked off (or is still
 125        // running) the effect below reopens the stream.
 126        refetch();
 127      }
 128    }, [projectId, refetch]);
§ 02 · KEY FLOW

关键流程

mount → GET seeds → pending/running → fetch SSE
snapshot changed → update slices → idle close → refetch

动作完成后 poke 会主动开流,两个 effect 分别负责首次快照和运行态重启,卸载时 abort。最终 hook 把 rows、映射后的 messages、project、harness 与恢复函数一次返回 对应源码落点是 src/components/blueprint/use-project-stream.ts:130、src/components/blueprint/use-project-stream.ts:132、src/components/blueprint/use-project-stream.ts:136、src/components/blueprint/use-project-stream.ts:140、src/components/blueprint/use-project-stream.ts:143、src/components/blueprint/use-project-stream.ts:144、src/components/blueprint/use-project-stream.ts:147。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

§ 03 · EDGE CASES

边界 / 失败模式

  • 解析失败被忽略,下一帧完整快照会自然修复。
  • 连接超时后依靠 refetch 和运行态 effect 重新进入。
  • 400ms 多表查询在高并发下可能成为数据库压力源。
Sub-page · 2026-07-15 Source · QuantumByteOSS/quantumbyte ↑ quantumbyte 主页面