gateway: return actionable error for send channel webchat (openclaw#15703) thanks @rodrigouroz

Verified:
- pnpm build
- pnpm check (fails on current main with unrelated type errors in src/memory/embedding-manager.test-harness.ts)
- pnpm test:macmini (not run after pnpm check failure)
- pnpm test -- src/gateway/server-methods/send.test.ts

Co-authored-by: rodrigouroz <165576107+rodrigouroz@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Rodrigo Uroz
2026-02-15 12:31:11 -03:00
committed by GitHub
parent 3d38e56401
commit 6565ec2e53
3 changed files with 47 additions and 1 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
- TUI: suppress false `(no output)` placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.
- Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie.
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz.
- Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n.
- Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.
- Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.

View File

@@ -19,7 +19,7 @@ vi.mock("../../config/config.js", async () => {
vi.mock("../../channels/plugins/index.js", () => ({
getChannelPlugin: () => ({ outbound: {} }),
normalizeChannelId: (value: string) => value,
normalizeChannelId: (value: string) => (value === "webchat" ? null : value),
}));
vi.mock("../../infra/outbound/targets.js", () => ({
@@ -108,6 +108,39 @@ describe("gateway send mirroring", () => {
);
});
it("returns actionable guidance when channel is internal webchat", async () => {
const respond = vi.fn();
await sendHandlers.send({
params: {
to: "x",
message: "hi",
channel: "webchat",
idempotencyKey: "idem-webchat",
},
respond,
context: makeContext(),
req: { type: "req", id: "1", method: "send" },
client: null,
isWebchatConnect: () => false,
});
expect(mocks.deliverOutboundPayloads).not.toHaveBeenCalled();
expect(respond).toHaveBeenCalledWith(
false,
undefined,
expect.objectContaining({
message: expect.stringContaining("unsupported channel: webchat"),
}),
);
expect(respond).toHaveBeenCalledWith(
false,
undefined,
expect.objectContaining({
message: expect.stringContaining("Use `chat.send`"),
}),
);
});
it("does not mirror when delivery returns no results", async () => {
mocks.deliverOutboundPayloads.mockResolvedValue([]);

View File

@@ -106,6 +106,18 @@ export const sendHandlers: GatewayRequestHandlers = {
const channelInput = typeof request.channel === "string" ? request.channel : undefined;
const normalizedChannel = channelInput ? normalizeChannelId(channelInput) : null;
if (channelInput && !normalizedChannel) {
const normalizedInput = channelInput.trim().toLowerCase();
if (normalizedInput === "webchat") {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
"unsupported channel: webchat (internal-only). Use `chat.send` for WebChat UI messages or choose a deliverable channel.",
),
);
return;
}
respond(
false,
undefined,