fix: harden discord agent cid parsing (#29013) (thanks @Jacky1n7)

This commit is contained in:
Peter Steinberger
2026-03-02 03:07:30 +00:00
parent c14c17403e
commit c869ca4bbf
3 changed files with 51 additions and 16 deletions

View File

@@ -111,6 +111,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Discord/Agent component interactions: accept Components v2 `cid` payloads alongside legacy `componentId`, and safely decode percent-encoded IDs without throwing on malformed `%` sequences. Landed from contributor PR #29013 by @Jacky1n7. Thanks @Jacky1n7.
- Discord/Inbound media fallback: preserve attachment and sticker metadata when Discord CDN fetch/save fails by keeping URL-based media entries in context, with regression coverage for save failures and mixed success/failure ordering. Landed from contributor PR #28906 by @Sid-Qin. Thanks @Sid-Qin.
- Docs/Docker images: clarify the official GHCR image source and tag guidance (`main`, `latest`, `<version>`), and document that `OPENCLAW_IMAGE` skips local image builds but still uses the repo-local compose/setup flow. (#27214, #31180) Fixes #15655. Thanks @ipl31.
- Agents/Model fallback: classify additional network transport errors (`ECONNREFUSED`, `ENETUNREACH`, `EHOSTUNREACH`, `ENETRESET`, `EAI_AGAIN`) as failover-worthy so fallback chains advance when primary providers are unreachable. Landed from contributor PR #19077 by @ayanesakura. Thanks @ayanesakura.

View File

@@ -406,22 +406,21 @@ export function buildAgentSelectCustomId(componentId: string): string {
/**
* Parse agent component data from Carbon's parsed ComponentData
* Carbon parses "key:componentId=xxx" into { componentId: "xxx" }
* Supports both legacy { componentId } and Components v2 { cid } payloads.
*/
function readParsedComponentId(data: ComponentData): unknown {
if (!data || typeof data !== "object") {
return undefined;
}
return "cid" in data
? (data as Record<string, unknown>).cid
: (data as Record<string, unknown>).componentId;
}
function parseAgentComponentData(data: ComponentData): {
componentId: string;
} | null {
if (!data || typeof data !== "object") {
return null;
}
// Carbon parses "key:componentId=xxx" into { componentId: "xxx" }
// Components v2 / other builders may use { cid: "xxx" } (e.g. occomp:cid=xxx).
const raw =
("cid" in data
? (data as Record<string, unknown>).cid
: (data as Record<string, unknown>).componentId) ??
(data as Record<string, unknown>).componentId;
const raw = readParsedComponentId(data);
const decodeSafe = (value: string): string => {
// `cid` values may be raw (not URI-encoded). Guard against malformed % sequences.
@@ -601,10 +600,7 @@ function parseDiscordComponentData(
if (!data || typeof data !== "object") {
return null;
}
const rawComponentId =
"cid" in data
? (data as { cid?: unknown }).cid
: (data as { componentId?: unknown }).componentId;
const rawComponentId = readParsedComponentId(data);
const rawModalId =
"mid" in data ? (data as { mid?: unknown }).mid : (data as { modalId?: unknown }).modalId;
let componentId = normalizeComponentId(rawComponentId);

View File

@@ -182,6 +182,44 @@ describe("agent components", () => {
expect(reply).toHaveBeenCalledWith({ content: "✓" });
expect(enqueueSystemEventMock).toHaveBeenCalled();
});
it("accepts cid payloads for agent button interactions", async () => {
const button = createAgentComponentButton({
cfg: createCfg(),
accountId: "default",
dmPolicy: "allowlist",
allowFrom: ["123456789"],
});
const { interaction, defer, reply } = createDmButtonInteraction();
await button.run(interaction, { cid: "hello_cid" } as ComponentData);
expect(defer).toHaveBeenCalledWith({ ephemeral: true });
expect(reply).toHaveBeenCalledWith({ content: "✓" });
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
expect.stringContaining("hello_cid"),
expect.any(Object),
);
});
it("keeps malformed percent cid values without throwing", async () => {
const button = createAgentComponentButton({
cfg: createCfg(),
accountId: "default",
dmPolicy: "allowlist",
allowFrom: ["123456789"],
});
const { interaction, defer, reply } = createDmButtonInteraction();
await button.run(interaction, { cid: "hello%2G" } as ComponentData);
expect(defer).toHaveBeenCalledWith({ ephemeral: true });
expect(reply).toHaveBeenCalledWith({ content: "✓" });
expect(enqueueSystemEventMock).toHaveBeenCalledWith(
expect.stringContaining("hello%2G"),
expect.any(Object),
);
});
});
describe("discord component interactions", () => {