mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(discord): preserve non-text payloads in reply scrub
This commit is contained in:
committed by
Peter Steinberger
parent
e259938e96
commit
5572c8137c
@@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/update: make package-update follow-up processes write completion results and exit explicitly, so Windows packaged upgrades do not hang after the new package finishes post-core plugin work. Thanks @vincentkoc.
|
||||
- Release validation: skip Slack live QA unless Slack credentials are explicitly configured, so release gates can keep proving non-Slack surfaces while Slack is still local and credential-gated. Thanks @vincentkoc.
|
||||
- Plugins/update: treat OpenClaw CalVer correction versions like `2026.5.3-1` as satisfying base plugin API ranges, so correction builds can install plugins that require the base runtime API. Fixes #77293. (#77450) Thanks @p3nchan.
|
||||
- Discord/reply-safety: preserve component-only channel payloads (for example `channelData.discord.components` and presentation/interactive-only replies) when final text scrubbing removes all text, so Discord still delivers non-text outbound payloads instead of dropping them. (#77478) Thanks @NikolaFC.
|
||||
- fix(gateway): clamp unbound websocket auth scopes [AI]. (#77413) Thanks @pgondhi987.
|
||||
- Gate zalouser startup name matching [AI]. (#77411) Thanks @pgondhi987.
|
||||
- Active Memory: send a bounded latest-message search query to the recall worker so channel/runtime metadata does not become the memory search string. Fixes #65309. Thanks @joeykrug, @westley3601, @pimenov, and @tasi333.
|
||||
|
||||
@@ -155,6 +155,80 @@ describe("deliverDiscordReply", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves component-only channelData payloads when text scrubs empty", async () => {
|
||||
const channelData = {
|
||||
discord: {
|
||||
components: [
|
||||
{
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
style: 1,
|
||||
label: "Open",
|
||||
custom_id: "open",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
{
|
||||
text: "analysis: internal only",
|
||||
channelData,
|
||||
},
|
||||
],
|
||||
target: "channel:101",
|
||||
token: "token",
|
||||
accountId: "default",
|
||||
runtime,
|
||||
cfg,
|
||||
textLimit: 2000,
|
||||
});
|
||||
|
||||
expect(deliverOutboundPayloadsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payloads: [{ channelData, text: undefined }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves presentation-only payloads when text scrubs empty", async () => {
|
||||
const presentation = {
|
||||
title: "Action required",
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons" as const,
|
||||
buttons: [{ label: "Approve", value: "approve", style: "primary" as const }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await deliverDiscordReply({
|
||||
replies: [
|
||||
{
|
||||
text: "commentary: hidden",
|
||||
presentation,
|
||||
},
|
||||
],
|
||||
target: "channel:101",
|
||||
token: "token",
|
||||
accountId: "default",
|
||||
runtime,
|
||||
cfg,
|
||||
textLimit: 2000,
|
||||
});
|
||||
|
||||
expect(deliverOutboundPayloadsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payloads: [{ presentation, text: undefined }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not strip ordinary code-fenced examples of tool-call labels", async () => {
|
||||
const text = ["Example:", "```", "🛠️ Exec: run ls", "```"].join("\n");
|
||||
|
||||
|
||||
@@ -7,6 +7,32 @@ const DISCORD_INTERNAL_TRACE_LINE_RE =
|
||||
const DISCORD_INTERNAL_CHANNEL_LINE_RE =
|
||||
/^(?:>\s*)?(?:analysis|commentary|tool[-_ ]?call|tool[-_ ]?result|function[-_ ]?call|thinking|reasoning)\s*[:=]/i;
|
||||
|
||||
function hasNonEmptyRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(
|
||||
value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function hasInteractiveOrPresentationBlocks(value: unknown): boolean {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
if (typeof record.title === "string" && record.title.trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
return Array.isArray(record.blocks) && record.blocks.length > 0;
|
||||
}
|
||||
|
||||
function hasNonTextReplyPayloadContent(payload: ReplyPayload): boolean {
|
||||
return (
|
||||
payload.audioAsVoice === true ||
|
||||
hasNonEmptyRecord(payload.channelData) ||
|
||||
hasInteractiveOrPresentationBlocks(payload.interactive) ||
|
||||
hasInteractiveOrPresentationBlocks(payload.presentation)
|
||||
);
|
||||
}
|
||||
|
||||
function stripDiscordInternalTraceLines(text: string): string {
|
||||
let inFence = false;
|
||||
const kept: string[] = [];
|
||||
@@ -45,7 +71,6 @@ export function sanitizeDiscordFrontChannelReplyPayloads(
|
||||
): ReplyPayload[] {
|
||||
const safePayloads: ReplyPayload[] = [];
|
||||
for (const payload of payloads) {
|
||||
const originalParts = resolveSendableOutboundReplyParts(payload);
|
||||
const safeText =
|
||||
typeof payload.text === "string"
|
||||
? sanitizeDiscordFrontChannelText(payload.text)
|
||||
@@ -55,7 +80,7 @@ export function sanitizeDiscordFrontChannelReplyPayloads(
|
||||
? payload
|
||||
: ({ ...payload, text: safeText || undefined } as ReplyPayload);
|
||||
const nextParts = resolveSendableOutboundReplyParts(nextPayload);
|
||||
if (!nextParts.hasText && !originalParts.hasMedia) {
|
||||
if (!nextParts.hasContent && !hasNonTextReplyPayloadContent(nextPayload)) {
|
||||
continue;
|
||||
}
|
||||
safePayloads.push(nextPayload);
|
||||
|
||||
Reference in New Issue
Block a user