Reintroduce OpenClaw webhook transport alongside SSE
This commit is contained in:
227
packages/adapters/openclaw/src/server/execute-webhook.ts
Normal file
227
packages/adapters/openclaw/src/server/execute-webhook.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclipai/adapter-utils";
|
||||
import {
|
||||
appendWakeText,
|
||||
buildExecutionState,
|
||||
buildWakeCompatibilityPayload,
|
||||
isTextRequiredResponse,
|
||||
isWakeCompatibilityEndpoint,
|
||||
readAndLogResponseText,
|
||||
redactForLog,
|
||||
sendJsonRequest,
|
||||
stringifyForLog,
|
||||
type OpenClawExecutionState,
|
||||
} from "./execute-common.js";
|
||||
import { parseOpenClawResponse } from "./parse.js";
|
||||
|
||||
function nonEmpty(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function buildWebhookBody(input: {
|
||||
state: OpenClawExecutionState;
|
||||
context: AdapterExecutionContext["context"];
|
||||
}): Record<string, unknown> {
|
||||
const { state, context } = input;
|
||||
const templateText = nonEmpty(state.payloadTemplate.text);
|
||||
const payloadText = templateText ? appendWakeText(templateText, state.wakeText) : state.wakeText;
|
||||
|
||||
return {
|
||||
...state.payloadTemplate,
|
||||
stream: false,
|
||||
sessionKey: state.sessionKey,
|
||||
text: payloadText,
|
||||
paperclip: {
|
||||
...state.wakePayload,
|
||||
sessionKey: state.sessionKey,
|
||||
streamTransport: "webhook",
|
||||
env: state.paperclipEnv,
|
||||
context,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function sendWebhookRequest(params: {
|
||||
url: string;
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
payload: Record<string, unknown>;
|
||||
onLog: AdapterExecutionContext["onLog"];
|
||||
signal: AbortSignal;
|
||||
}): Promise<{ response: Response; responseText: string }> {
|
||||
const response = await sendJsonRequest({
|
||||
url: params.url,
|
||||
method: params.method,
|
||||
headers: params.headers,
|
||||
payload: params.payload,
|
||||
signal: params.signal,
|
||||
});
|
||||
|
||||
const responseText = await readAndLogResponseText({ response, onLog: params.onLog });
|
||||
return { response, responseText };
|
||||
}
|
||||
|
||||
export async function executeWebhook(ctx: AdapterExecutionContext, url: string): Promise<AdapterExecutionResult> {
|
||||
const { onLog, onMeta, context } = ctx;
|
||||
const state = buildExecutionState(ctx);
|
||||
|
||||
if (onMeta) {
|
||||
await onMeta({
|
||||
adapterType: "openclaw",
|
||||
command: "webhook",
|
||||
commandArgs: [state.method, url],
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
const headers = { ...state.headers };
|
||||
const webhookBody = buildWebhookBody({ state, context });
|
||||
const wakeCompatibilityBody = buildWakeCompatibilityPayload(state.wakeText);
|
||||
const preferWakeCompatibilityBody = isWakeCompatibilityEndpoint(url);
|
||||
const initialBody = preferWakeCompatibilityBody ? wakeCompatibilityBody : webhookBody;
|
||||
|
||||
const outboundHeaderKeys = Object.keys(headers).sort();
|
||||
await onLog(
|
||||
"stdout",
|
||||
`[openclaw] outbound headers (redacted): ${stringifyForLog(redactForLog(headers), 4_000)}\n`,
|
||||
);
|
||||
await onLog(
|
||||
"stdout",
|
||||
`[openclaw] outbound payload (redacted): ${stringifyForLog(redactForLog(initialBody), 12_000)}\n`,
|
||||
);
|
||||
await onLog("stdout", `[openclaw] outbound header keys: ${outboundHeaderKeys.join(", ")}\n`);
|
||||
await onLog("stdout", `[openclaw] invoking ${state.method} ${url} (transport=webhook)\n`);
|
||||
|
||||
if (preferWakeCompatibilityBody) {
|
||||
await onLog("stdout", "[openclaw] using wake text payload for /hooks/wake compatibility\n");
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = state.timeoutSec > 0 ? setTimeout(() => controller.abort(), state.timeoutSec * 1000) : null;
|
||||
|
||||
try {
|
||||
const initialResponse = await sendWebhookRequest({
|
||||
url,
|
||||
method: state.method,
|
||||
headers,
|
||||
payload: initialBody,
|
||||
onLog,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!initialResponse.response.ok) {
|
||||
const canRetryWithWakeCompatibility =
|
||||
!preferWakeCompatibilityBody && isTextRequiredResponse(initialResponse.responseText);
|
||||
|
||||
if (canRetryWithWakeCompatibility) {
|
||||
await onLog(
|
||||
"stdout",
|
||||
"[openclaw] endpoint requires text payload; retrying with wake compatibility format\n",
|
||||
);
|
||||
|
||||
const retryResponse = await sendWebhookRequest({
|
||||
url,
|
||||
method: state.method,
|
||||
headers,
|
||||
payload: wakeCompatibilityBody,
|
||||
onLog,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (retryResponse.response.ok) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
provider: "openclaw",
|
||||
model: null,
|
||||
summary: `OpenClaw webhook ${state.method} ${url} (wake compatibility)`,
|
||||
resultJson: {
|
||||
status: retryResponse.response.status,
|
||||
statusText: retryResponse.response.statusText,
|
||||
compatibilityMode: "wake_text",
|
||||
response: parseOpenClawResponse(retryResponse.responseText) ?? retryResponse.responseText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
errorMessage:
|
||||
isTextRequiredResponse(retryResponse.responseText)
|
||||
? "OpenClaw endpoint rejected the wake compatibility payload as text-required."
|
||||
: `OpenClaw webhook failed with status ${retryResponse.response.status}`,
|
||||
errorCode: isTextRequiredResponse(retryResponse.responseText)
|
||||
? "openclaw_text_required"
|
||||
: "openclaw_http_error",
|
||||
resultJson: {
|
||||
status: retryResponse.response.status,
|
||||
statusText: retryResponse.response.statusText,
|
||||
compatibilityMode: "wake_text",
|
||||
response: parseOpenClawResponse(retryResponse.responseText) ?? retryResponse.responseText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
errorMessage:
|
||||
isTextRequiredResponse(initialResponse.responseText)
|
||||
? "OpenClaw endpoint rejected the payload as text-required."
|
||||
: `OpenClaw webhook failed with status ${initialResponse.response.status}`,
|
||||
errorCode: isTextRequiredResponse(initialResponse.responseText)
|
||||
? "openclaw_text_required"
|
||||
: "openclaw_http_error",
|
||||
resultJson: {
|
||||
status: initialResponse.response.status,
|
||||
statusText: initialResponse.response.statusText,
|
||||
response: parseOpenClawResponse(initialResponse.responseText) ?? initialResponse.responseText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
provider: "openclaw",
|
||||
model: null,
|
||||
summary: `OpenClaw webhook ${state.method} ${url}`,
|
||||
resultJson: {
|
||||
status: initialResponse.response.status,
|
||||
statusText: initialResponse.response.statusText,
|
||||
response: parseOpenClawResponse(initialResponse.responseText) ?? initialResponse.responseText,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
const timeoutMessage =
|
||||
state.timeoutSec > 0
|
||||
? `[openclaw] webhook request timed out after ${state.timeoutSec}s\n`
|
||||
: "[openclaw] webhook request aborted\n";
|
||||
await onLog("stderr", timeoutMessage);
|
||||
return {
|
||||
exitCode: null,
|
||||
signal: null,
|
||||
timedOut: true,
|
||||
errorMessage: state.timeoutSec > 0 ? `Timed out after ${state.timeoutSec}s` : "Request aborted",
|
||||
errorCode: "openclaw_webhook_timeout",
|
||||
};
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
await onLog("stderr", `[openclaw] request failed: ${message}\n`);
|
||||
return {
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
errorMessage: message,
|
||||
errorCode: "openclaw_request_failed",
|
||||
};
|
||||
} finally {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user