fix: stream Claw background task progress
This commit is contained in:
parent
000e5c75c9
commit
55be81d3d8
@ -232,6 +232,126 @@ function parseSseBlock(block: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenClawBackgroundTask = {
|
||||||
|
jobId: string;
|
||||||
|
state: string;
|
||||||
|
stage: string;
|
||||||
|
phase: string;
|
||||||
|
displayText: string;
|
||||||
|
updatedAt: string;
|
||||||
|
completed: number;
|
||||||
|
total: number;
|
||||||
|
progressPercent: number;
|
||||||
|
pollEndpoint: string;
|
||||||
|
pollAfterMs: number;
|
||||||
|
maxWaitSeconds: number;
|
||||||
|
autoNotify: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseOpenClawBackgroundTask(
|
||||||
|
payload: unknown
|
||||||
|
): OpenClawBackgroundTask | null {
|
||||||
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rawPayload = payload as Record<string, unknown>;
|
||||||
|
const rawTask =
|
||||||
|
rawPayload.background_task &&
|
||||||
|
typeof rawPayload.background_task === "object" &&
|
||||||
|
!Array.isArray(rawPayload.background_task)
|
||||||
|
? (rawPayload.background_task as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
if (!rawTask) return null;
|
||||||
|
|
||||||
|
const jobId = String(rawTask.job_id || "").trim();
|
||||||
|
const state = String(rawTask.state || "").trim();
|
||||||
|
const pollEndpoint = String(rawTask.poll_endpoint || "").trim();
|
||||||
|
if (!jobId || !state) return null;
|
||||||
|
|
||||||
|
const completed = Math.max(0, Number(rawTask.completed) || 0);
|
||||||
|
const total = Math.max(1, Number(rawTask.total) || 1);
|
||||||
|
const progressPercent = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
100,
|
||||||
|
Number.isFinite(Number(rawTask.progress_percent))
|
||||||
|
? Number(rawTask.progress_percent)
|
||||||
|
: Math.round((completed * 100) / total)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
jobId,
|
||||||
|
state,
|
||||||
|
stage: String(rawTask.stage || "").trim(),
|
||||||
|
phase: String(rawTask.phase || "").trim(),
|
||||||
|
displayText: String(
|
||||||
|
rawTask.display_text || rawTask.phase || rawPayload.answer || ""
|
||||||
|
).trim(),
|
||||||
|
updatedAt: String(rawTask.updated_at || "").trim(),
|
||||||
|
completed,
|
||||||
|
total,
|
||||||
|
progressPercent,
|
||||||
|
pollEndpoint,
|
||||||
|
pollAfterMs: Math.max(
|
||||||
|
800,
|
||||||
|
Math.min(10_000, Number(rawTask.poll_after_ms) || 1500)
|
||||||
|
),
|
||||||
|
maxWaitSeconds: Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(25, Number(rawTask.max_wait_seconds) || 20)
|
||||||
|
),
|
||||||
|
autoNotify: rawTask.auto_notify === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOpenClawTaskProgress(task: OpenClawBackgroundTask) {
|
||||||
|
return {
|
||||||
|
step_code: task.stage || "background_task",
|
||||||
|
step_state:
|
||||||
|
task.state === "failed"
|
||||||
|
? "failed"
|
||||||
|
: task.state === "processing"
|
||||||
|
? "started"
|
||||||
|
: "completed",
|
||||||
|
message:
|
||||||
|
task.displayText ||
|
||||||
|
task.phase ||
|
||||||
|
(task.state === "processing" ? "任务正在处理" : "任务状态已更新"),
|
||||||
|
display:
|
||||||
|
task.displayText ||
|
||||||
|
task.phase ||
|
||||||
|
(task.state === "processing" ? "任务正在处理" : "任务状态已更新"),
|
||||||
|
completed: task.completed,
|
||||||
|
total: task.total,
|
||||||
|
progress_percent: task.progressPercent,
|
||||||
|
stage: task.stage,
|
||||||
|
state: task.state,
|
||||||
|
updated_at: task.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOpenClawTaskStatusUrl(params: {
|
||||||
|
baseUrl: string;
|
||||||
|
task: OpenClawBackgroundTask;
|
||||||
|
userId: string;
|
||||||
|
conversationId: string;
|
||||||
|
}) {
|
||||||
|
const base = new URL(params.baseUrl);
|
||||||
|
const endpoint = params.task.pollEndpoint.startsWith("/")
|
||||||
|
? params.task.pollEndpoint
|
||||||
|
: "/api/chat/task-status";
|
||||||
|
const url = new URL(endpoint, base.origin);
|
||||||
|
url.searchParams.set("job_id", params.task.jobId);
|
||||||
|
url.searchParams.set("user_id", params.userId);
|
||||||
|
url.searchParams.set("conversation_id", params.conversationId);
|
||||||
|
url.searchParams.set("wait_seconds", String(params.task.maxWaitSeconds));
|
||||||
|
if (params.task.updatedAt) {
|
||||||
|
url.searchParams.set("known_updated_at", params.task.updatedAt);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
async function persistClawdResponse({
|
async function persistClawdResponse({
|
||||||
assistantMessageId,
|
assistantMessageId,
|
||||||
attachments,
|
attachments,
|
||||||
@ -361,6 +481,181 @@ async function persistClawdResponse({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function streamOpenClawBackgroundTask(params: {
|
||||||
|
request: Request;
|
||||||
|
cancelKey: string;
|
||||||
|
config: { baseUrl: string; token: string; timeoutMs: number };
|
||||||
|
initialPayload: Record<string, unknown>;
|
||||||
|
initialTask: OpenClawBackgroundTask;
|
||||||
|
assistantMessageId: string;
|
||||||
|
attachments: OpenClawChatMediaAttachment[];
|
||||||
|
chatId: string;
|
||||||
|
conversationId: string;
|
||||||
|
displayMessage: string;
|
||||||
|
skillTagId: string;
|
||||||
|
skillTagName: string;
|
||||||
|
spreadsheetAttachments: ClawdSpreadsheetAttachment[];
|
||||||
|
userId: string;
|
||||||
|
userMessageId: string;
|
||||||
|
}) {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const stream = new ReadableStream<Uint8Array>({
|
||||||
|
async start(streamController) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const handleRequestAbort = () => controller.abort();
|
||||||
|
params.request.signal.addEventListener("abort", handleRequestAbort, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
registerActiveClawdRequest(
|
||||||
|
params.cancelKey,
|
||||||
|
controller,
|
||||||
|
Math.max(params.config.timeoutMs, 60 * 60 * 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
let task = params.initialTask;
|
||||||
|
let finalPayload = params.initialPayload;
|
||||||
|
let lastProgressKey = "";
|
||||||
|
let consecutiveFailures = 0;
|
||||||
|
|
||||||
|
const emitProgress = (nextTask: OpenClawBackgroundTask) => {
|
||||||
|
const progress = buildOpenClawTaskProgress(nextTask);
|
||||||
|
const key = [
|
||||||
|
nextTask.state,
|
||||||
|
nextTask.stage,
|
||||||
|
nextTask.updatedAt,
|
||||||
|
nextTask.completed,
|
||||||
|
nextTask.total,
|
||||||
|
progress.message,
|
||||||
|
].join("|");
|
||||||
|
if (key === lastProgressKey) return;
|
||||||
|
lastProgressKey = key;
|
||||||
|
streamController.enqueue(
|
||||||
|
encoder.encode(encodeSseEvent("progress", progress))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
emitProgress(task);
|
||||||
|
while (task.state === "processing" && task.autoNotify) {
|
||||||
|
if (
|
||||||
|
controller.signal.aborted ||
|
||||||
|
isClawdRequestCanceled(params.cancelKey)
|
||||||
|
) {
|
||||||
|
throw new DOMException("Claw request canceled", "AbortError");
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusUrl = buildOpenClawTaskStatusUrl({
|
||||||
|
baseUrl: params.config.baseUrl,
|
||||||
|
task,
|
||||||
|
userId: params.userId,
|
||||||
|
conversationId: params.conversationId,
|
||||||
|
});
|
||||||
|
let statusResponse: Response;
|
||||||
|
try {
|
||||||
|
statusResponse = await fetch(statusUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${params.config.token}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
cache: "no-store",
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (controller.signal.aborted) throw error;
|
||||||
|
consecutiveFailures += 1;
|
||||||
|
if (consecutiveFailures >= 5) throw error;
|
||||||
|
streamController.enqueue(
|
||||||
|
encoder.encode(
|
||||||
|
encodeSseEvent("progress", {
|
||||||
|
step_code: task.stage || "background_task",
|
||||||
|
step_state: "waiting",
|
||||||
|
message: "任务仍在运行,正在重新连接进度服务",
|
||||||
|
display: "任务仍在运行,正在重新连接进度服务",
|
||||||
|
completed: task.completed,
|
||||||
|
total: task.total,
|
||||||
|
progress_percent: task.progressPercent,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, task.pollAfterMs)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusResponse.ok) {
|
||||||
|
throw new Error(`OpenClaw task status failed: ${statusResponse.status}`);
|
||||||
|
}
|
||||||
|
const statusPayload = (await statusResponse.json().catch(() => null)) as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| null;
|
||||||
|
const nextTask = parseOpenClawBackgroundTask(statusPayload);
|
||||||
|
if (!statusPayload || !nextTask) {
|
||||||
|
throw new Error("OpenClaw task status payload is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
consecutiveFailures = 0;
|
||||||
|
finalPayload = {
|
||||||
|
...statusPayload,
|
||||||
|
request_id:
|
||||||
|
statusPayload.request_id || params.initialPayload.request_id || null,
|
||||||
|
};
|
||||||
|
task = nextTask;
|
||||||
|
emitProgress(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
const persisted = await persistClawdResponse({
|
||||||
|
assistantMessageId: params.assistantMessageId,
|
||||||
|
attachments: params.attachments,
|
||||||
|
chatId: params.chatId,
|
||||||
|
conversationId: params.conversationId,
|
||||||
|
displayMessage: params.displayMessage,
|
||||||
|
skillTagId: params.skillTagId,
|
||||||
|
skillTagName: params.skillTagName,
|
||||||
|
spreadsheetAttachments: params.spreadsheetAttachments,
|
||||||
|
upstreamPayload: finalPayload,
|
||||||
|
userMessageId: params.userMessageId,
|
||||||
|
});
|
||||||
|
clearClawdRequestCancellation(params.cancelKey);
|
||||||
|
streamController.enqueue(
|
||||||
|
encoder.encode(encodeSseEvent("final", persisted))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const wasCanceled =
|
||||||
|
controller.signal.aborted ||
|
||||||
|
isClawdRequestCanceled(params.cancelKey) ||
|
||||||
|
(error instanceof Error && error.name === "AbortError");
|
||||||
|
streamController.enqueue(
|
||||||
|
encoder.encode(
|
||||||
|
encodeSseEvent(
|
||||||
|
wasCanceled ? "final" : "error",
|
||||||
|
wasCanceled
|
||||||
|
? { canceled: true }
|
||||||
|
: { error: CLAW_UPGRADE_RETRY_MESSAGE }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
params.request.signal.removeEventListener("abort", handleRequestAbort);
|
||||||
|
unregisterActiveClawdRequest(params.cancelKey);
|
||||||
|
try {
|
||||||
|
streamController.close();
|
||||||
|
} catch {
|
||||||
|
// The browser may already have closed the stream.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-cache, no-store",
|
||||||
|
"Content-Type": "text/event-stream; charset=utf-8",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const session = await auth().catch(() => null);
|
const session = await auth().catch(() => null);
|
||||||
|
|
||||||
@ -500,6 +795,7 @@ export async function POST(request: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${config.token}`,
|
Authorization: `Bearer ${config.token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
Accept: "text/event-stream",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
conversation_id: conversationId,
|
conversation_id: conversationId,
|
||||||
@ -657,6 +953,31 @@ export async function POST(request: Request) {
|
|||||||
{ status: response.status }
|
{ status: response.status }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backgroundTask = parseOpenClawBackgroundTask(upstreamPayload);
|
||||||
|
if (
|
||||||
|
backgroundTask?.state === "processing" &&
|
||||||
|
backgroundTask.autoNotify &&
|
||||||
|
backgroundTask.pollEndpoint
|
||||||
|
) {
|
||||||
|
return streamOpenClawBackgroundTask({
|
||||||
|
request,
|
||||||
|
cancelKey,
|
||||||
|
config,
|
||||||
|
initialPayload: upstreamPayload as Record<string, unknown>,
|
||||||
|
initialTask: backgroundTask,
|
||||||
|
assistantMessageId,
|
||||||
|
attachments: parsed.attachments,
|
||||||
|
chatId: parsed.id,
|
||||||
|
conversationId,
|
||||||
|
displayMessage,
|
||||||
|
skillTagId,
|
||||||
|
skillTagName,
|
||||||
|
spreadsheetAttachments,
|
||||||
|
userId: session.user.id,
|
||||||
|
userMessageId,
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
unregisterActiveClawdRequest(cancelKey);
|
unregisterActiveClawdRequest(cancelKey);
|
||||||
const errorName =
|
const errorName =
|
||||||
|
|||||||
@ -494,6 +494,65 @@ type PendingClawdRequestRestoreSnapshot = {
|
|||||||
shouldRestoreSkillTag: boolean;
|
shouldRestoreSkillTag: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ClawdProgressEvent = {
|
||||||
|
key: string;
|
||||||
|
eventId?: number;
|
||||||
|
sequence?: number;
|
||||||
|
stepCode: string;
|
||||||
|
stepState: string;
|
||||||
|
message: string;
|
||||||
|
currentOrderId: string;
|
||||||
|
completed?: number;
|
||||||
|
total?: number;
|
||||||
|
progressPercent?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeClawdProgressEvent(
|
||||||
|
progress: Record<string, unknown>
|
||||||
|
): ClawdProgressEvent | null {
|
||||||
|
if (progress.heartbeat === true) return null;
|
||||||
|
const message = String(
|
||||||
|
progress.message || progress.display || ""
|
||||||
|
).trim();
|
||||||
|
if (!message) return null;
|
||||||
|
const eventId = Number(progress.event_id);
|
||||||
|
const sequence = Number(progress.sequence);
|
||||||
|
const stepCode = String(progress.step_code || "").trim();
|
||||||
|
const stepState = String(progress.step_state || "").trim();
|
||||||
|
const currentOrderId = String(progress.current_order_id || "").trim();
|
||||||
|
const completed = Number(progress.completed);
|
||||||
|
const total = Number(progress.total);
|
||||||
|
const progressPercent = Number(progress.progress_percent);
|
||||||
|
const normalizedEventId = Number.isFinite(eventId) && eventId > 0
|
||||||
|
? eventId
|
||||||
|
: undefined;
|
||||||
|
const normalizedSequence = Number.isFinite(sequence) && sequence > 0
|
||||||
|
? sequence
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key:
|
||||||
|
normalizedEventId != null
|
||||||
|
? `event-${normalizedEventId}`
|
||||||
|
: `sequence-${normalizedSequence || 0}-${stepCode}-${message}`,
|
||||||
|
eventId: normalizedEventId,
|
||||||
|
sequence: normalizedSequence,
|
||||||
|
stepCode,
|
||||||
|
stepState,
|
||||||
|
message,
|
||||||
|
currentOrderId,
|
||||||
|
completed:
|
||||||
|
Number.isFinite(completed) && completed >= 0 ? completed : undefined,
|
||||||
|
total: Number.isFinite(total) && total > 0 ? total : undefined,
|
||||||
|
progressPercent:
|
||||||
|
Number.isFinite(progressPercent) &&
|
||||||
|
progressPercent >= 0 &&
|
||||||
|
progressPercent <= 100
|
||||||
|
? Math.round(progressPercent)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function isClawdSubmittedComposerText(
|
function isClawdSubmittedComposerText(
|
||||||
snapshot: ClawdComposerSubmissionSnapshot,
|
snapshot: ClawdComposerSubmissionSnapshot,
|
||||||
value: string
|
value: string
|
||||||
@ -1924,16 +1983,85 @@ function CanvasAssistantTypingPlaceholder({
|
|||||||
label = "分析中",
|
label = "分析中",
|
||||||
onCancel,
|
onCancel,
|
||||||
cancelLabel = "停止",
|
cancelLabel = "停止",
|
||||||
|
progressEvents = [],
|
||||||
}: {
|
}: {
|
||||||
hint?: string | null;
|
hint?: string | null;
|
||||||
label?: string;
|
label?: string;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
cancelLabel?: string;
|
cancelLabel?: string;
|
||||||
|
progressEvents?: ClawdProgressEvent[];
|
||||||
}) {
|
}) {
|
||||||
|
const visibleProgressEvents = progressEvents.slice(-12);
|
||||||
|
const hiddenProgressCount = Math.max(
|
||||||
|
0,
|
||||||
|
progressEvents.length - visibleProgressEvents.length
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full justify-start">
|
<div className="flex w-full justify-start">
|
||||||
<div className="flex max-w-[min(78%,680px)] flex-col items-start gap-2">
|
<div className="flex w-full max-w-[min(92%,680px)] flex-col items-start gap-2 sm:max-w-[min(78%,680px)]">
|
||||||
{hint ? (
|
{visibleProgressEvents.length > 0 ? (
|
||||||
|
<div
|
||||||
|
className="w-full min-w-0 rounded-lg bg-[#f4f5f7] px-4 py-3 text-[#2f343d] shadow-sm dark:bg-white/8 dark:text-zinc-200"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<div className="mb-2 flex items-center justify-between gap-3 text-[12px] font-medium text-[#737d8c] dark:text-zinc-400">
|
||||||
|
<span>执行进度</span>
|
||||||
|
<span>{progressEvents.length} 步</span>
|
||||||
|
</div>
|
||||||
|
{hiddenProgressCount > 0 ? (
|
||||||
|
<div className="mb-1.5 pl-6 text-[11px] text-[#9aa3b2] dark:text-zinc-500">
|
||||||
|
已记录此前 {hiddenProgressCount} 步
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="max-h-64 space-y-1.5 overflow-y-auto pr-1">
|
||||||
|
{visibleProgressEvents.map((progressEvent, index) => {
|
||||||
|
const isLatest = index === visibleProgressEvents.length - 1;
|
||||||
|
const isFailed = progressEvent.stepState === "failed";
|
||||||
|
const isActive =
|
||||||
|
isLatest &&
|
||||||
|
["accepted", "queued", "started", "waiting"].includes(
|
||||||
|
progressEvent.stepState
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={progressEvent.key}
|
||||||
|
className="flex min-w-0 items-start gap-2 text-[13px] leading-5"
|
||||||
|
>
|
||||||
|
{isFailed ? (
|
||||||
|
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-rose-500" />
|
||||||
|
) : isActive ? (
|
||||||
|
<Loader2 className="mt-0.5 h-4 w-4 shrink-0 animate-spin text-[#617086] dark:text-zinc-300" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="break-words">{progressEvent.message}</div>
|
||||||
|
{progressEvent.currentOrderId ||
|
||||||
|
progressEvent.total ? (
|
||||||
|
<div className="text-[11px] text-[#929baa] dark:text-zinc-500">
|
||||||
|
{progressEvent.currentOrderId
|
||||||
|
? `订单 ${progressEvent.currentOrderId}`
|
||||||
|
: ""}
|
||||||
|
{progressEvent.currentOrderId &&
|
||||||
|
progressEvent.total
|
||||||
|
? " · "
|
||||||
|
: ""}
|
||||||
|
{progressEvent.total
|
||||||
|
? `${progressEvent.completed || 0}/${progressEvent.total}${
|
||||||
|
progressEvent.progressPercent != null
|
||||||
|
? ` · ${progressEvent.progressPercent}%`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : hint ? (
|
||||||
<div className="rounded-[18px] bg-[#f4f5f7] px-4 py-3 text-[15px] leading-6 text-[#2f343d] shadow-sm dark:bg-white/8 dark:text-zinc-200">
|
<div className="rounded-[18px] bg-[#f4f5f7] px-4 py-3 text-[15px] leading-6 text-[#2f343d] shadow-sm dark:bg-white/8 dark:text-zinc-200">
|
||||||
{hint}
|
{hint}
|
||||||
</div>
|
</div>
|
||||||
@ -2493,6 +2621,9 @@ export function CanvasChatPanel({
|
|||||||
const [isDetailGenerating, setIsDetailGenerating] = useState(false);
|
const [isDetailGenerating, setIsDetailGenerating] = useState(false);
|
||||||
const [isClawdSubmitting, setIsClawdSubmitting] = useState(false);
|
const [isClawdSubmitting, setIsClawdSubmitting] = useState(false);
|
||||||
const [clawdProcessingHint, setClawdProcessingHint] = useState<string | null>(null);
|
const [clawdProcessingHint, setClawdProcessingHint] = useState<string | null>(null);
|
||||||
|
const [clawdProgressEvents, setClawdProgressEvents] = useState<
|
||||||
|
ClawdProgressEvent[]
|
||||||
|
>([]);
|
||||||
const [detailSections, setDetailSections] = useState<Record<DetailSectionKey, boolean>>({
|
const [detailSections, setDetailSections] = useState<Record<DetailSectionKey, boolean>>({
|
||||||
required: true,
|
required: true,
|
||||||
strategy: false,
|
strategy: false,
|
||||||
@ -7804,6 +7935,7 @@ export function CanvasChatPanel({
|
|||||||
clawdAbortControllerRef.current = null;
|
clawdAbortControllerRef.current = null;
|
||||||
setIsClawdSubmitting(false);
|
setIsClawdSubmitting(false);
|
||||||
setClawdProcessingHint(null);
|
setClawdProcessingHint(null);
|
||||||
|
setClawdProgressEvents([]);
|
||||||
releaseSendGuard(snapshot.chatId);
|
releaseSendGuard(snapshot.chatId);
|
||||||
restorePendingClawdRequestToComposer(snapshot);
|
restorePendingClawdRequestToComposer(snapshot);
|
||||||
toast.message("已停止处理,可以重新编辑后再发送");
|
toast.message("已停止处理,可以重新编辑后再发送");
|
||||||
@ -11527,6 +11659,7 @@ export function CanvasChatPanel({
|
|||||||
clawdAbortControllerRef.current = clawdAbortController;
|
clawdAbortControllerRef.current = clawdAbortController;
|
||||||
pendingClawdRequestRestoreRef.current = clawdRequestRestoreSnapshot;
|
pendingClawdRequestRestoreRef.current = clawdRequestRestoreSnapshot;
|
||||||
setClawdProcessingHint(nextProcessingHint);
|
setClawdProcessingHint(nextProcessingHint);
|
||||||
|
setClawdProgressEvents([]);
|
||||||
setIsClawdSubmitting(true);
|
setIsClawdSubmitting(true);
|
||||||
setMessages((previousMessages) =>
|
setMessages((previousMessages) =>
|
||||||
sortCanvasMessagesByCreatedAt([
|
sortCanvasMessagesByCreatedAt([
|
||||||
@ -11584,6 +11717,19 @@ export function CanvasChatPanel({
|
|||||||
if (display) {
|
if (display) {
|
||||||
setClawdProcessingHint(display);
|
setClawdProcessingHint(display);
|
||||||
}
|
}
|
||||||
|
const normalizedProgress = normalizeClawdProgressEvent(progress);
|
||||||
|
if (normalizedProgress) {
|
||||||
|
setClawdProgressEvents((previousEvents) => {
|
||||||
|
if (
|
||||||
|
previousEvents.some(
|
||||||
|
(item) => item.key === normalizedProgress.key
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return previousEvents;
|
||||||
|
}
|
||||||
|
return [...previousEvents, normalizedProgress].slice(-2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (response.status === 499 || responseData?.canceled === true) {
|
if (response.status === 499 || responseData?.canceled === true) {
|
||||||
canceledClawdMessageIdsRef.current.add(userMessageId);
|
canceledClawdMessageIdsRef.current.add(userMessageId);
|
||||||
@ -11713,6 +11859,7 @@ export function CanvasChatPanel({
|
|||||||
}
|
}
|
||||||
setIsClawdSubmitting(false);
|
setIsClawdSubmitting(false);
|
||||||
setClawdProcessingHint(null);
|
setClawdProcessingHint(null);
|
||||||
|
setClawdProgressEvents([]);
|
||||||
releaseSendGuard(activeChatId);
|
releaseSendGuard(activeChatId);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -15183,6 +15330,7 @@ export function CanvasChatPanel({
|
|||||||
hint={clawdProcessingHint}
|
hint={clawdProcessingHint}
|
||||||
label="处理中"
|
label="处理中"
|
||||||
onCancel={handleCancelClawdSubmission}
|
onCancel={handleCancelClawdSubmission}
|
||||||
|
progressEvents={clawdProgressEvents}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user