fix(claw): preserve per-task credentials on reconnect

This commit is contained in:
134 2026-08-11 18:15:44 +08:00
parent a913f78f8b
commit 92e0f49fbc
3 changed files with 83 additions and 6 deletions

View File

@ -576,7 +576,7 @@ export async function POST(request: Request) {
: "image/jpeg"),
}));
await persistPendingClawdTask({
const persistedTask = await persistPendingClawdTask({
chatId: parsed.id,
userMessageId,
assistantMessageId,
@ -587,6 +587,22 @@ export async function POST(request: Request) {
skillTagName,
processingHint,
});
if (
persistedTask &&
persistedTask.assistantMessageId !== assistantMessageId
) {
return NextResponse.json(
buildTaskResponse({
chatId: parsed.id,
userMessageId,
assistantMessageId: persistedTask.assistantMessageId,
status: persistedTask.status,
processingHint:
persistedTask.processingHint || DEFAULT_PROCESSING_HINT,
}),
{ status: 202, headers: { "Retry-After": "2" } }
);
}
const userProfile = await getUserById({ id: session.user.id }).catch(
() => null

View File

@ -8,7 +8,10 @@ import {
updateMessageById,
} from "@/lib/db/queries";
import type { DBMessage } from "@/lib/db/schema";
import { encryptOpenClawTaskSecret } from "@/lib/openclaw-chat/task-secret";
import {
decryptOpenClawTaskSecret,
encryptOpenClawTaskSecret,
} from "@/lib/openclaw-chat/task-secret";
import {
appendClawdTaskProgress,
CLAWD_TASK_KIND,
@ -37,6 +40,32 @@ export type ClawdUpstreamTaskState = {
lastEventId?: number | null;
};
export async function findPersistedClawdUpstreamTaskState(params: {
chatId: string;
taskId: string;
}): Promise<ClawdUpstreamTaskState | null> {
const messages = await getMessagesByChatId({ id: params.chatId }).catch(
() => []
);
for (const message of messages.toReversed()) {
const metadata = normalizeClawdTaskMetadata(message.videoMetadata);
if (
!metadata ||
metadata.upstreamTaskId !== params.taskId ||
!metadata.upstreamTaskKey
) {
continue;
}
return {
taskId: metadata.upstreamTaskId,
taskKey: decryptOpenClawTaskSecret(metadata.upstreamTaskKey),
eventsEndpoint: metadata.upstreamEventsEndpoint,
lastEventId: metadata.upstreamLastEventId,
};
}
return null;
}
function buildTaskMetadata(params: {
userMessageId: string;
assistantMessageId: string;

View File

@ -2,6 +2,7 @@ import "server-only";
import {
appendPersistedClawdTaskProgress,
findPersistedClawdUpstreamTaskState,
type ClawdUpstreamTaskState,
persistClawdUpstreamTaskState,
} from "@/lib/openclaw-chat/task-store";
@ -301,19 +302,37 @@ export async function submitOpenClawDurableTask(params: {
const task = readRecord(root.task);
const taskId = readText(root.task_id) || readText(task.task_id);
if (!taskId) throw new Error("OpenClaw 未返回 task_id");
const responseTaskKey = readText(root.task_key);
const persistedTask = responseTaskKey
? null
: await findPersistedClawdUpstreamTaskState({
chatId: params.target.chatId,
taskId,
});
const taskKey = responseTaskKey || persistedTask?.taskKey || null;
if (!taskKey) {
throw new OpenClawTaskCredentialUnavailableError(taskId);
}
const submittedTask = mergeUpstreamTaskState(
{
taskId,
taskKey: readText(root.task_key) || null,
eventsEndpoint: getTaskEventsEndpoint(payload, taskId),
lastEventId: 0,
taskKey,
eventsEndpoint:
getTaskEventsEndpoint(payload, taskId) ||
persistedTask?.eventsEndpoint ||
null,
lastEventId: persistedTask?.lastEventId || 0,
},
payload
);
// A task-creation response can include the latest upstream cursor even though
// none of its events have been persisted locally yet. Always consume the
// event stream from the beginning after accepting a new durable task.
const upstreamTask = { ...submittedTask, lastEventId: 0 };
const upstreamTask = {
...submittedTask,
taskKey,
lastEventId: persistedTask?.lastEventId || 0,
};
await persistClawdUpstreamTaskState({
...params.target,
upstreamTask,
@ -342,6 +361,11 @@ export async function refreshOpenClawDurableTask(params: {
target: ClawdTaskPersistenceTarget;
signal?: AbortSignal;
}): Promise<OpenClawTaskOutcome> {
if (!params.upstreamTask.taskKey) {
throw new OpenClawTaskCredentialUnavailableError(
params.upstreamTask.taskId
);
}
const eventsUrl = buildUpstreamUrl(
params.config.baseUrl,
params.upstreamTask.eventsEndpoint ||
@ -459,6 +483,14 @@ export class OpenClawTaskDetachedError extends Error {
}
}
export class OpenClawTaskCredentialUnavailableError extends OpenClawTaskDetachedError {
constructor(readonly taskId: string) {
super();
this.name = "OpenClawTaskCredentialUnavailableError";
this.message = "OpenClaw task key is unavailable; keep the task detached";
}
}
export async function waitForOpenClawDurableTask(params: {
config: OpenClawUpstreamConfig;
upstreamTask: ClawdUpstreamTaskState;