mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix: message sends fail when optional location is blank (#112013)
* fix(outbound): ignore blank shared-schema send location Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b529c5fd-6822-4c0d-ab8d-9906bd7dc8d9 * fix(outbound): normalize blank send locations Co-authored-by: ronan-dandelion-cult <ronan.dandelion.cult@hotmail.com> --------- Co-authored-by: ronan-dandelion-cult <ronan.dandelion.cult@hotmail.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
committed by
GitHub
parent
267d9f89ef
commit
4e6a6bdbcc
@@ -2937,6 +2937,51 @@ describe("runMessageAction plugin dispatch", () => {
|
||||
expectRecordFields(gatewayActionParams, { presentation }, "gateway action params");
|
||||
});
|
||||
|
||||
it("omits a blank shared-schema location from gateway-routed sends", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
cardchat: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
messages: { responsePrefix: "[Nexus]" },
|
||||
} as OpenClawConfig;
|
||||
mocks.callGatewayLeastPrivilege.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
messageId: "card-location",
|
||||
});
|
||||
|
||||
const result = await runMessageAction({
|
||||
cfg,
|
||||
action: "send",
|
||||
params: {
|
||||
channel: "cardchat",
|
||||
target: "channel:test-card",
|
||||
message: "hello",
|
||||
location: "",
|
||||
},
|
||||
gateway: {
|
||||
clientName: "cli",
|
||||
mode: "cli",
|
||||
},
|
||||
dryRun: false,
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
expect(result.handledBy).toBe("plugin");
|
||||
expect(handleAction).not.toHaveBeenCalled();
|
||||
const gatewayCall = readMockCallArg(
|
||||
mocks.callGatewayLeastPrivilege,
|
||||
"gateway least privilege call",
|
||||
);
|
||||
const gatewayActionParams = readRecordField(
|
||||
readRecordField(gatewayCall, "params", "gateway call params"),
|
||||
"params",
|
||||
"gateway action params",
|
||||
);
|
||||
expect(gatewayActionParams).not.toHaveProperty("location");
|
||||
});
|
||||
|
||||
it("keeps gateway-routed chart presentations on the gateway", async () => {
|
||||
const presentation = {
|
||||
blocks: [
|
||||
|
||||
@@ -472,6 +472,39 @@ describe("runMessageAction send validation", () => {
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
});
|
||||
|
||||
it.each(["", " \t\n"])(
|
||||
"treats blank shared-schema event location %j as omitted on send",
|
||||
async (location) => {
|
||||
const result = await runDrySend({
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hello",
|
||||
location,
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("send");
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps rejecting a non-empty event location string on send", async () => {
|
||||
await expect(
|
||||
runDrySend({
|
||||
cfg: workspaceConfig,
|
||||
actionParams: {
|
||||
channel: "workspace",
|
||||
target: "#C12345678",
|
||||
message: "hello",
|
||||
location: "Main stage",
|
||||
},
|
||||
toolContext: { currentChannelId: "C12345678" },
|
||||
}),
|
||||
).rejects.toThrow("location must be an object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("message body alias normalization", () => {
|
||||
|
||||
@@ -686,6 +686,17 @@ function updateSendPayloadPartsFromReplyPayload(
|
||||
};
|
||||
}
|
||||
|
||||
function applySendLocationToActionParams(
|
||||
actionParams: Record<string, unknown>,
|
||||
location: ReplyPayload["location"],
|
||||
) {
|
||||
if (location) {
|
||||
actionParams.location = location;
|
||||
} else {
|
||||
delete actionParams.location;
|
||||
}
|
||||
}
|
||||
|
||||
function applySendPayloadPartsToActionParams(
|
||||
actionParams: Record<string, unknown>,
|
||||
parts: SendPayloadParts,
|
||||
@@ -703,7 +714,7 @@ function applySendPayloadPartsToActionParams(
|
||||
actionParams.asVoice = parts.asVoice || undefined;
|
||||
actionParams.audioAsVoice = parts.asVoice || undefined;
|
||||
actionParams.asVideoNote = parts.payload.videoAsNote || undefined;
|
||||
actionParams.location = parts.payload.location;
|
||||
applySendLocationToActionParams(actionParams, parts.payload.location);
|
||||
}
|
||||
|
||||
function collectMessageAttachmentMediaHints(value: unknown): string[] {
|
||||
@@ -1188,7 +1199,14 @@ async function buildSendPayloadParts(params: {
|
||||
Boolean(mediaHint) || mediaUrlHints.length > 0 || attachmentMediaHints.length > 0;
|
||||
const hasPresentation = hasMessagePresentationBlocks(actionParams.presentation);
|
||||
const hasInteractive = hasLegacyInteractiveReplyBlocks(actionParams.interactive);
|
||||
const location = normalizeOutboundLocation(actionParams.location);
|
||||
const rawLocation = actionParams.location;
|
||||
// The flat tool schema also carries scheduled-event `location` as a string,
|
||||
// and some models pad unused optional slots with blanks. Keep real send locations strict.
|
||||
const location =
|
||||
typeof rawLocation === "string" && normalizeOptionalString(rawLocation) === undefined
|
||||
? undefined
|
||||
: normalizeOutboundLocation(rawLocation);
|
||||
applySendLocationToActionParams(actionParams, location);
|
||||
const caption = readStringParam(actionParams, "caption", { allowEmpty: true }) ?? "";
|
||||
let message =
|
||||
readStringParam(actionParams, "message", {
|
||||
|
||||
Reference in New Issue
Block a user