fix(openclaw): support /hooks/wake compatibility payload
This commit is contained in:
@@ -6,6 +6,82 @@ function nonEmpty(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function shouldUseWakeTextPayload(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const path = parsed.pathname.toLowerCase();
|
||||
return path === "/hooks/wake" || path.endsWith("/hooks/wake");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildWakeText(payload: {
|
||||
runId: string;
|
||||
agentId: string;
|
||||
companyId: string;
|
||||
taskId: string | null;
|
||||
issueId: string | null;
|
||||
wakeReason: string | null;
|
||||
wakeCommentId: string | null;
|
||||
approvalId: string | null;
|
||||
approvalStatus: string | null;
|
||||
issueIds: string[];
|
||||
}): string {
|
||||
const lines = [
|
||||
"Paperclip wake event.",
|
||||
"",
|
||||
`runId: ${payload.runId}`,
|
||||
`agentId: ${payload.agentId}`,
|
||||
`companyId: ${payload.companyId}`,
|
||||
];
|
||||
|
||||
if (payload.taskId) lines.push(`taskId: ${payload.taskId}`);
|
||||
if (payload.issueId) lines.push(`issueId: ${payload.issueId}`);
|
||||
if (payload.wakeReason) lines.push(`wakeReason: ${payload.wakeReason}`);
|
||||
if (payload.wakeCommentId) lines.push(`wakeCommentId: ${payload.wakeCommentId}`);
|
||||
if (payload.approvalId) lines.push(`approvalId: ${payload.approvalId}`);
|
||||
if (payload.approvalStatus) lines.push(`approvalStatus: ${payload.approvalStatus}`);
|
||||
if (payload.issueIds.length > 0) lines.push(`issueIds: ${payload.issueIds.join(",")}`);
|
||||
|
||||
lines.push("", "Run your Paperclip heartbeat procedure now.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function isTextRequiredResponse(responseText: string): boolean {
|
||||
const parsed = parseOpenClawResponse(responseText);
|
||||
const parsedError = parsed && typeof parsed.error === "string" ? parsed.error : null;
|
||||
if (parsedError && parsedError.toLowerCase().includes("text required")) {
|
||||
return true;
|
||||
}
|
||||
return responseText.toLowerCase().includes("text required");
|
||||
}
|
||||
|
||||
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 fetch(params.url, {
|
||||
method: params.method,
|
||||
headers: params.headers,
|
||||
body: JSON.stringify(params.payload),
|
||||
signal: params.signal,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
if (responseText.trim().length > 0) {
|
||||
await params.onLog("stdout", `[openclaw] response (${response.status}) ${responseText.slice(0, 2000)}\n`);
|
||||
} else {
|
||||
await params.onLog("stdout", `[openclaw] response (${response.status}) <empty>\n`);
|
||||
}
|
||||
|
||||
return { response, responseText };
|
||||
}
|
||||
|
||||
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
|
||||
const { config, runId, agent, context, onLog, onMeta } = ctx;
|
||||
const url = asString(config.url, "").trim();
|
||||
@@ -52,13 +128,17 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
: [],
|
||||
};
|
||||
|
||||
const body = {
|
||||
const paperclipBody = {
|
||||
...payloadTemplate,
|
||||
paperclip: {
|
||||
...wakePayload,
|
||||
context,
|
||||
},
|
||||
};
|
||||
const wakeTextBody = {
|
||||
text: buildWakeText(wakePayload),
|
||||
mode: "now",
|
||||
};
|
||||
|
||||
if (onMeta) {
|
||||
await onMeta({
|
||||
@@ -75,21 +155,69 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutSec * 1000);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
const preferWakeTextPayload = shouldUseWakeTextPayload(url);
|
||||
if (preferWakeTextPayload) {
|
||||
await onLog("stdout", "[openclaw] using wake text payload for /hooks/wake compatibility\n");
|
||||
}
|
||||
|
||||
const initialPayload = preferWakeTextPayload ? wakeTextBody : paperclipBody;
|
||||
|
||||
const { response, responseText } = await sendWebhookRequest({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
payload: initialPayload,
|
||||
onLog,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
if (responseText.trim().length > 0) {
|
||||
await onLog("stdout", `[openclaw] response (${response.status}) ${responseText.slice(0, 2000)}\n`);
|
||||
} else {
|
||||
await onLog("stdout", `[openclaw] response (${response.status}) <empty>\n`);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const canRetryWithWakeText = !preferWakeTextPayload && isTextRequiredResponse(responseText);
|
||||
|
||||
if (canRetryWithWakeText) {
|
||||
await onLog("stdout", "[openclaw] endpoint requires text payload; retrying with wake compatibility format\n");
|
||||
|
||||
const retry = await sendWebhookRequest({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
payload: wakeTextBody,
|
||||
onLog,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (retry.response.ok) {
|
||||
return {
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
provider: "openclaw",
|
||||
model: null,
|
||||
summary: `OpenClaw webhook ${method} ${url} (wake compatibility)`,
|
||||
resultJson: {
|
||||
status: retry.response.status,
|
||||
statusText: retry.response.statusText,
|
||||
compatibilityMode: "wake_text",
|
||||
response: parseOpenClawResponse(retry.responseText) ?? retry.responseText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
errorMessage: `OpenClaw webhook failed with status ${retry.response.status}`,
|
||||
errorCode: "openclaw_http_error",
|
||||
resultJson: {
|
||||
status: retry.response.status,
|
||||
statusText: retry.response.statusText,
|
||||
compatibilityMode: "wake_text",
|
||||
response: parseOpenClawResponse(retry.responseText) ?? retry.responseText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
|
||||
@@ -29,6 +29,11 @@ function normalizeHostname(value: string | null | undefined): string | null {
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
function isWakePath(pathname: string): boolean {
|
||||
const value = pathname.trim().toLowerCase();
|
||||
return value === "/hooks/wake" || value.endsWith("/hooks/wake");
|
||||
}
|
||||
|
||||
function pushDeploymentDiagnostics(
|
||||
checks: AdapterEnvironmentCheck[],
|
||||
ctx: AdapterEnvironmentTestContext,
|
||||
@@ -148,6 +153,15 @@ export async function testEnvironment(
|
||||
hint: "Use a reachable hostname/IP (for example Tailscale/private hostname or public domain).",
|
||||
});
|
||||
}
|
||||
|
||||
if (isWakePath(url.pathname)) {
|
||||
checks.push({
|
||||
code: "openclaw_wake_endpoint_compat_mode",
|
||||
level: "info",
|
||||
message: "Endpoint targets /hooks/wake; adapter will use OpenClaw wake compatibility payload (text/mode).",
|
||||
hint: "For structured Paperclip JSON payloads, use a mapped webhook endpoint such as /hooks/paperclip.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pushDeploymentDiagnostics(checks, ctx, url);
|
||||
|
||||
Reference in New Issue
Block a user