Keep chat stream alive during long runs

This commit is contained in:
武阳
2026-05-07 15:33:03 +08:00
parent cd4e73c159
commit ce6445bf77
+58 -44
View File
@@ -13,6 +13,7 @@ import {
} from "@/lib/claw-auth"; } from "@/lib/claw-auth";
export const runtime = "nodejs"; export const runtime = "nodejs";
export const maxDuration = 300;
type ClawChatResponse = { type ClawChatResponse = {
final_output?: string; final_output?: string;
@@ -92,55 +93,68 @@ export async function POST(req: Request) {
const stream = createUIMessageStream({ const stream = createUIMessageStream({
execute: async ({ writer }) => { execute: async ({ writer }) => {
const streamState = { textStarted: false, textStreamed: false }; const streamState = { textStarted: false, textStreamed: false };
let heartbeatCount = 0;
const heartbeat = setInterval(() => {
heartbeatCount += 1;
writer.write({
type: "reasoning-delta",
id: "reasoning-1",
delta: `\n仍在处理请求,已等待 ${formatDuration(heartbeatCount * 15000)}`,
});
}, 15000);
writer.write({ type: "start" }); writer.write({ type: "start" });
writer.write({ type: "start-step" }); writer.write({ type: "start-step" });
const startedAt = Date.now(); try {
writer.write({ type: "reasoning-start", id: "reasoning-1" }); const startedAt = Date.now();
writer.write({ writer.write({ type: "reasoning-start", id: "reasoning-1" });
type: "reasoning-delta", writer.write({
id: "reasoning-1", type: "reasoning-delta",
delta: "思考中,正在判断是否需要调用工具。", id: "reasoning-1",
}); delta: "思考中,正在判断是否需要调用工具。",
const payload = await callClawBackendStream( });
userPrompt, const payload = await callClawBackendStream(
runtimeContext, userPrompt,
account.id, runtimeContext,
sessionId, account.id,
resumeId, sessionId,
writer, resumeId,
streamState, writer,
); streamState,
const elapsedMs = payload.elapsed_ms ?? Date.now() - startedAt; );
const text = formatClawResponse(payload); const elapsedMs = payload.elapsed_ms ?? Date.now() - startedAt;
const text = formatClawResponse(payload);
writer.write({ writer.write({
type: "reasoning-delta", type: "reasoning-delta",
id: "reasoning-1", id: "reasoning-1",
delta: `\n思考并执行完成,用时 ${formatDuration(elapsedMs)}`, delta: `\n思考并执行完成,用时 ${formatDuration(elapsedMs)}`,
}); });
writer.write({ type: "reasoning-end", id: "reasoning-1" }); writer.write({ type: "reasoning-end", id: "reasoning-1" });
writeToolTrace(writer, payload.transcript); writeToolTrace(writer, payload.transcript);
if (!streamState.textStreamed) { if (!streamState.textStreamed) {
writer.write({ type: "text-start", id: "text-1" }); writer.write({ type: "text-start", id: "text-1" });
writer.write({ type: "text-delta", id: "text-1", delta: text }); writer.write({ type: "text-delta", id: "text-1", delta: text });
writer.write({ type: "text-end", id: "text-1" }); writer.write({ type: "text-end", id: "text-1" });
} else if (streamState.textStarted) { } else if (streamState.textStarted) {
writer.write({ type: "text-end", id: "text-1" }); writer.write({ type: "text-end", id: "text-1" });
streamState.textStarted = false; streamState.textStarted = false;
}
writer.write({ type: "finish-step" });
writer.write({
type: "finish",
finishReason: payload.error ? "error" : "stop",
messageMetadata: {
sessionId: payload.session_id,
toolCalls: payload.tool_calls,
stopReason: payload.stop_reason,
elapsedMs,
usage: payload.usage,
},
});
} finally {
clearInterval(heartbeat);
} }
writer.write({ type: "finish-step" });
writer.write({
type: "finish",
finishReason: payload.error ? "error" : "stop",
messageMetadata: {
sessionId: payload.session_id,
toolCalls: payload.tool_calls,
stopReason: payload.stop_reason,
elapsedMs,
usage: payload.usage,
},
});
}, },
}); });