Add session scoped runs and cancellation

This commit is contained in:
武阳
2026-05-07 16:26:20 +08:00
parent a4a0677ce4
commit 9d7d496443
6 changed files with 542 additions and 76 deletions
+92 -16
View File
@@ -29,6 +29,7 @@ type ClawChatResponse = {
type ClawRuntimeEvent = {
type?: string;
run_id?: string;
tool_name?: string;
tool_call_id?: string;
arguments?: unknown;
@@ -36,6 +37,7 @@ type ClawRuntimeEvent = {
metadata?: Record<string, unknown>;
ok?: boolean;
delta?: string;
elapsed_ms?: number;
};
type ClawStreamItem =
@@ -43,6 +45,13 @@ type ClawStreamItem =
| { kind: "result"; payload?: ClawChatResponse }
| { kind: "error"; error?: string; detail?: string; status_code?: number };
type StreamState = {
textStarted: boolean;
textStreamed: boolean;
phase: "waiting" | "queued" | "running" | "done";
runId?: string;
};
type ChatRequestBody = {
id?: string;
messages: UIMessage[];
@@ -92,26 +101,25 @@ export async function POST(req: Request) {
const stream = createUIMessageStream({
execute: async ({ writer }) => {
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);
const streamState: StreamState = {
textStarted: false,
textStreamed: false,
phase: "waiting",
};
writer.write({ type: "start" });
writer.write({ type: "start-step" });
try {
const cancelOnAbort = () => {
void cancelClawRun(account.id, sessionId, streamState.runId);
};
req.signal.addEventListener("abort", cancelOnAbort, { once: true });
const startedAt = Date.now();
writer.write({ type: "reasoning-start", id: "reasoning-1" });
writer.write({
type: "reasoning-delta",
id: "reasoning-1",
delta: "思考中,正在判断是否需要调用工具。",
delta: "请求已发送,等待后端开始处理。",
});
const payload = await callClawBackendStream(
userPrompt,
@@ -121,7 +129,9 @@ export async function POST(req: Request) {
resumeId,
writer,
streamState,
req.signal,
);
req.signal.removeEventListener("abort", cancelOnAbort);
const elapsedMs = payload.elapsed_ms ?? Date.now() - startedAt;
const text = formatClawResponse(payload);
@@ -152,8 +162,22 @@ export async function POST(req: Request) {
usage: payload.usage,
},
});
} finally {
clearInterval(heartbeat);
} catch (err) {
const text =
err instanceof Error && err.name === "AbortError"
? "请求已取消。"
: err instanceof Error
? err.message
: "请求失败。";
writer.write({ type: "reasoning-end", id: "reasoning-1" });
writer.write({ type: "text-start", id: "text-1" });
writer.write({ type: "text-delta", id: "text-1", delta: text });
writer.write({ type: "text-end", id: "text-1" });
writer.write({ type: "finish-step" });
writer.write({
type: "finish",
finishReason: "error",
});
}
},
});
@@ -326,12 +350,14 @@ async function callClawBackendStream(
sessionId: string,
resumeSessionId?: string,
writer?: UIMessageStreamWriter<UIMessage>,
streamState?: { textStarted: boolean; textStreamed: boolean },
streamState?: StreamState,
signal?: AbortSignal,
): Promise<ClawChatResponse> {
try {
const response = await fetch(`${CLAW_API_URL}/api/chat/stream`, {
method: "POST",
headers: { "content-type": "application/json" },
signal,
body: JSON.stringify({
prompt,
runtime_context: runtimeContext,
@@ -364,10 +390,30 @@ async function callClawBackendStream(
}
}
async function cancelClawRun(
accountId: string,
sessionId: string,
runId?: string,
) {
try {
await fetch(`${CLAW_API_URL}/api/runs/cancel`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
account_id: accountId,
session_id: sessionId,
...(runId ? { run_id: runId } : {}),
}),
});
} catch {
// 浏览器关闭或网络中断时,取消请求本身也可能失败;后端流断开仍会结束本次响应。
}
}
async function consumeClawStream(
body: ReadableStream<Uint8Array>,
writer?: UIMessageStreamWriter<UIMessage>,
streamState?: { textStarted: boolean; textStreamed: boolean },
streamState?: StreamState,
) {
const reader = body.getReader();
const decoder = new TextDecoder();
@@ -430,8 +476,38 @@ function parseStreamLine(line: string): ClawStreamItem | undefined {
function writeRuntimeEvent(
writer: UIMessageStreamWriter<UIMessage>,
event: ClawRuntimeEvent,
streamState?: { textStarted: boolean; textStreamed: boolean },
streamState?: StreamState,
) {
if (event.type === "run_queued") {
if (streamState) streamState.phase = "queued";
if (streamState && event.run_id) streamState.runId = event.run_id;
writer.write({
type: "reasoning-delta",
id: "reasoning-1",
delta: "\n当前会话已有任务在执行,本轮正在排队。",
});
}
if (event.type === "run_started") {
if (streamState) streamState.phase = "running";
if (streamState && event.run_id) streamState.runId = event.run_id;
writer.write({
type: "reasoning-delta",
id: "reasoning-1",
delta: "\n后端已开始执行本轮任务。",
});
}
if (event.type === "server_heartbeat") {
const elapsed =
typeof event.elapsed_ms === "number"
? `,已等待 ${formatDuration(event.elapsed_ms)}`
: "";
const phase = streamState?.phase === "queued" ? "仍在排队" : "后端仍在处理";
writer.write({
type: "reasoning-delta",
id: "reasoning-1",
delta: `\n${phase}${elapsed}`,
});
}
if (event.type === "final_text_start") {
if (!streamState?.textStarted) {
writer.write({ type: "text-start", id: "text-1" });