Add session scoped runs and cancellation
This commit is contained in:
@@ -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" });
|
||||
|
||||
Reference in New Issue
Block a user