mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-07 07:58:36 +00:00
fix(discord): parse provider-prefixed channel targets (#78625)
* fix(discord): parse provider-prefixed channel targets * fix(discord): resolve allowlisted numeric dm targets
This commit is contained in:
@@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai
|
||||
- QA/Mantis: add `pnpm openclaw qa mantis slack-desktop-smoke` to run Slack live QA inside a Crabbox VNC desktop, open Slack Web, and capture desktop screenshots beside the Slack QA artifacts.
|
||||
- QA/Mantis: add an opt-in Discord thread attachment before/after scenario that creates a real thread, calls `message.thread-reply` with `filePath`, and captures baseline/candidate screenshot evidence.
|
||||
- Discord: preserve `filePath` and `path` attachments when replying to a thread with the message tool.
|
||||
- Discord/message: parse provider-prefixed targets like `discord:channel:<id>` as channel sends instead of legacy Discord DM targets, so cross-channel agent `message(action="send")` calls no longer misroute channel IDs into misleading `Unknown Channel` failures. Fixes #78572.
|
||||
- QA/Mantis: add visual desktop tasks with Crabbox MP4 recording, screenshot capture, and optional image-understanding assertions, and preserve video artifacts in Mantis before/after reports.
|
||||
- QA/WhatsApp: add `pnpm openclaw qa whatsapp` for live DM canary and pairing-gate coverage using two pre-linked WhatsApp Web sessions from the QA credential pool.
|
||||
- QA/Mantis: pass the runtime env through desktop-browser Crabbox and artifact-copy child commands, so embedded Mantis callers can provide Crabbox credentials without mutating the parent process. Thanks @vincentkoc.
|
||||
|
||||
@@ -172,6 +172,33 @@ describe("discordPlugin outbound", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves bare allowlisted Discord user IDs as message-tool DM targets", async () => {
|
||||
const resolveTarget = discordPlugin.messaging?.targetResolver?.resolveTarget;
|
||||
if (!resolveTarget) {
|
||||
throw new Error(
|
||||
"Expected discordPlugin.messaging.targetResolver.resolveTarget to be defined",
|
||||
);
|
||||
}
|
||||
|
||||
await expect(
|
||||
resolveTarget({
|
||||
cfg: {
|
||||
channels: {
|
||||
discord: {
|
||||
allowFrom: ["1439091261670948987"],
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
input: "1439091261670948987",
|
||||
normalized: "channel:1439091261670948987",
|
||||
preferredKind: "channel",
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
to: "user:1439091261670948987",
|
||||
kind: "user",
|
||||
});
|
||||
});
|
||||
|
||||
it("honors per-account replyToMode overrides", () => {
|
||||
const resolveReplyToMode = discordPlugin.threading?.resolveReplyToMode;
|
||||
if (!resolveReplyToMode) {
|
||||
|
||||
@@ -79,6 +79,7 @@ import { discordSetupAdapter } from "./setup-adapter.js";
|
||||
import { createDiscordPluginBase, discordConfigAdapter } from "./shared.js";
|
||||
import { collectDiscordStatusIssues } from "./status-issues.js";
|
||||
import { parseDiscordTarget } from "./target-parsing.js";
|
||||
import { resolveDiscordTarget } from "./target-resolver.js";
|
||||
|
||||
const REQUIRED_DISCORD_PERMISSIONS = ["ViewChannel", "SendMessages"] as const;
|
||||
const DISCORD_ACCOUNT_STARTUP_STAGGER_MS = 10_000;
|
||||
@@ -326,6 +327,21 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeDiscordTargetId,
|
||||
hint: "<channelId|user:ID|channel:ID>",
|
||||
resolveTarget: async ({ cfg, accountId, input, preferredKind }) => {
|
||||
const target = await resolveDiscordTarget(
|
||||
input,
|
||||
{ cfg, accountId: accountId ?? undefined },
|
||||
{ defaultKind: preferredKind === "user" ? "user" : "channel" },
|
||||
);
|
||||
return target
|
||||
? {
|
||||
to: target.normalized,
|
||||
kind: target.kind,
|
||||
display: target.raw,
|
||||
source: "normalized",
|
||||
}
|
||||
: null;
|
||||
},
|
||||
},
|
||||
},
|
||||
approvalCapability: getDiscordApprovalCapability(),
|
||||
|
||||
@@ -31,6 +31,9 @@ export function normalizeDiscordOutboundTarget(
|
||||
}
|
||||
return { ok: true, to: `channel:${trimmed}` };
|
||||
}
|
||||
if (/^discord:(?:channel|user):/i.test(trimmed)) {
|
||||
return { ok: true, to: normalizeDiscordMessagingTarget(trimmed) ?? trimmed };
|
||||
}
|
||||
return { ok: true, to: trimmed };
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,13 @@ describe("normalizeDiscordOutboundTarget", () => {
|
||||
expect(normalizeDiscordOutboundTarget("channel:123")).toEqual({ ok: true, to: "channel:123" });
|
||||
});
|
||||
|
||||
it("normalizes provider-prefixed channel targets", () => {
|
||||
expect(normalizeDiscordOutboundTarget("discord:channel:123")).toEqual({
|
||||
ok: true,
|
||||
to: "channel:123",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes through user: prefixed targets", () => {
|
||||
expect(normalizeDiscordOutboundTarget("user:123")).toEqual({ ok: true, to: "user:123" });
|
||||
});
|
||||
|
||||
@@ -31,4 +31,36 @@ describe("resolveDiscordOutboundSessionRoute", () => {
|
||||
});
|
||||
expect(route?.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("routes provider-prefixed channel targets as channels", () => {
|
||||
const route = resolveDiscordOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
target: "discord:channel:123",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
sessionKey: "agent:main:discord:channel:123",
|
||||
baseSessionKey: "agent:main:discord:channel:123",
|
||||
chatType: "channel",
|
||||
from: "discord:channel:123",
|
||||
to: "channel:123",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps legacy provider-prefixed numeric targets as direct messages", () => {
|
||||
const route = resolveDiscordOutboundSessionRoute({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
target: "discord:123",
|
||||
});
|
||||
|
||||
expect(route).toMatchObject({
|
||||
sessionKey: "agent:main:main",
|
||||
baseSessionKey: "agent:main:main",
|
||||
chatType: "direct",
|
||||
from: "discord:123",
|
||||
to: "user:123",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,10 @@ export function parseDiscordTarget(
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const providerPrefixedTarget = parseDiscordProviderPrefixedTarget(trimmed);
|
||||
if (providerPrefixedTarget) {
|
||||
return providerPrefixedTarget;
|
||||
}
|
||||
const userTarget = parseMentionPrefixOrAtUserTarget({
|
||||
raw: trimmed,
|
||||
mentionPattern: /^<@!?(\d+)>$/,
|
||||
@@ -47,6 +51,19 @@ export function parseDiscordTarget(
|
||||
return buildMessagingTarget("channel", trimmed, trimmed);
|
||||
}
|
||||
|
||||
function parseDiscordProviderPrefixedTarget(raw: string): DiscordTarget | undefined {
|
||||
const match = /^discord:(channel|user):(.+)$/i.exec(raw);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const kind = match[1]?.toLowerCase() as "channel" | "user" | undefined;
|
||||
const id = match[2]?.trim();
|
||||
if (!kind || !id) {
|
||||
return undefined;
|
||||
}
|
||||
return buildMessagingTarget(kind, id, `${kind}:${id}`);
|
||||
}
|
||||
|
||||
export function resolveDiscordChannelId(raw: string): string {
|
||||
const target = parseDiscordTarget(raw, { defaultKind: "channel" });
|
||||
return requireTargetKind({ platform: "Discord", target, kind: "channel" });
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("parseDiscordTarget", () => {
|
||||
{ input: "<@123>", id: "123", normalized: "user:123" },
|
||||
{ input: "<@!456>", id: "456", normalized: "user:456" },
|
||||
{ input: "user:789", id: "789", normalized: "user:789" },
|
||||
{ input: "discord:user:789", id: "789", normalized: "user:789" },
|
||||
{ input: "discord:987", id: "987", normalized: "user:987" },
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
@@ -32,6 +33,7 @@ describe("parseDiscordTarget", () => {
|
||||
it("parses channel targets", () => {
|
||||
const cases = [
|
||||
{ input: "channel:555", id: "555", normalized: "channel:555" },
|
||||
{ input: "discord:channel:555", id: "555", normalized: "channel:555" },
|
||||
{ input: "general", id: "general", normalized: "channel:general" },
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
@@ -225,6 +227,10 @@ describe("normalizeDiscordMessagingTarget", () => {
|
||||
it("defaults raw numeric ids to channels", () => {
|
||||
expect(normalizeDiscordMessagingTarget("123")).toBe("channel:123");
|
||||
});
|
||||
|
||||
it("normalizes provider-prefixed channel targets as channels", () => {
|
||||
expect(normalizeDiscordMessagingTarget("discord:channel:123")).toBe("channel:123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("discord group policy", () => {
|
||||
|
||||
Reference in New Issue
Block a user