mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 03:11:10 +00:00
fix(regression): preserve discord thread bindings for plugin commands
This commit is contained in:
@@ -39,6 +39,7 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
|
||||
function createInteraction(params?: {
|
||||
channelType?: ChannelType;
|
||||
channelId?: string;
|
||||
threadParentId?: string | null;
|
||||
guildId?: string;
|
||||
guildName?: string;
|
||||
}): MockCommandInteraction {
|
||||
@@ -48,6 +49,7 @@ function createInteraction(params?: {
|
||||
globalName: "Tester",
|
||||
channelType: params?.channelType ?? ChannelType.DM,
|
||||
channelId: params?.channelId ?? "dm-1",
|
||||
threadParentId: params?.threadParentId,
|
||||
guildId: params?.guildId ?? null,
|
||||
guildName: params?.guildName,
|
||||
interactionId: "interaction-1",
|
||||
@@ -501,6 +503,73 @@ describe("Discord native plugin command dispatch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards Discord thread metadata into direct plugin command execution", async () => {
|
||||
const cfg = {
|
||||
commands: {
|
||||
useAccessGroups: false,
|
||||
},
|
||||
channels: {
|
||||
discord: {
|
||||
groupPolicy: "allowlist",
|
||||
guilds: {
|
||||
"345678901234567890": {
|
||||
channels: {
|
||||
"thread-123": {
|
||||
allow: true,
|
||||
requireMention: false,
|
||||
},
|
||||
"parent-456": {
|
||||
allow: true,
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const commandSpec: NativeCommandSpec = {
|
||||
name: "cron_jobs",
|
||||
description: "List cron jobs",
|
||||
acceptsArgs: false,
|
||||
};
|
||||
const interaction = createInteraction({
|
||||
channelType: ChannelType.PublicThread,
|
||||
channelId: "thread-123",
|
||||
threadParentId: "parent-456",
|
||||
guildId: "345678901234567890",
|
||||
guildName: "Test Guild",
|
||||
});
|
||||
const pluginMatch = {
|
||||
command: {
|
||||
name: "cron_jobs",
|
||||
description: "List cron jobs",
|
||||
pluginId: "cron-jobs",
|
||||
acceptsArgs: false,
|
||||
handler: vi.fn().mockResolvedValue({ text: "jobs" }),
|
||||
},
|
||||
args: undefined,
|
||||
};
|
||||
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never);
|
||||
const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({
|
||||
text: "direct plugin output",
|
||||
});
|
||||
const command = await createNativeCommand(cfg, commandSpec);
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
expect(executeSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "discord",
|
||||
from: "discord:channel:thread-123",
|
||||
to: "slash:owner",
|
||||
messageThreadId: "thread-123",
|
||||
threadParentId: "parent-456",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes native slash commands through configured ACP Discord channel bindings", async () => {
|
||||
const { cfg, interaction } = createConfiguredAcpCase({
|
||||
channelType: ChannelType.GuildText,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { vi } from "vitest";
|
||||
|
||||
export type MockCommandInteraction = {
|
||||
user: { id: string; username: string; globalName: string };
|
||||
channel: { type: ChannelType; id: string };
|
||||
channel: { type: ChannelType; id: string; parentId?: string | null };
|
||||
guild: { id: string; name?: string } | null;
|
||||
rawData: { id: string; member: { roles: string[] } };
|
||||
options: {
|
||||
@@ -22,6 +22,7 @@ type CreateMockCommandInteractionParams = {
|
||||
globalName?: string;
|
||||
channelType?: ChannelType;
|
||||
channelId?: string;
|
||||
threadParentId?: string | null;
|
||||
guildId?: string | null;
|
||||
guildName?: string;
|
||||
interactionId?: string;
|
||||
@@ -42,6 +43,7 @@ export function createMockCommandInteraction(
|
||||
channel: {
|
||||
type: params.channelType ?? ChannelType.DM,
|
||||
id: params.channelId ?? "dm-1",
|
||||
parentId: params.threadParentId,
|
||||
},
|
||||
guild,
|
||||
rawData: {
|
||||
|
||||
@@ -917,6 +917,13 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
return;
|
||||
}
|
||||
const channelId = rawChannelId || "unknown";
|
||||
const isThreadChannel =
|
||||
interaction.channel?.type === ChannelType.PublicThread ||
|
||||
interaction.channel?.type === ChannelType.PrivateThread ||
|
||||
interaction.channel?.type === ChannelType.AnnouncementThread;
|
||||
const messageThreadId = !isDirectMessage && isThreadChannel ? channelId : undefined;
|
||||
const threadParentId =
|
||||
!isDirectMessage && isThreadChannel ? (interaction.channel.parentId ?? undefined) : undefined;
|
||||
const pluginReply = await executePluginCommandImpl({
|
||||
command: pluginMatch.command,
|
||||
args: pluginMatch.args,
|
||||
@@ -933,6 +940,8 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
: `discord:channel:${channelId}`,
|
||||
to: `slash:${user.id}`,
|
||||
accountId,
|
||||
messageThreadId,
|
||||
threadParentId,
|
||||
});
|
||||
if (!hasRenderableReplyPayload(pluginReply)) {
|
||||
await respond("Done.");
|
||||
|
||||
@@ -48,6 +48,7 @@ export const handlePluginCommand: CommandHandler = async (
|
||||
typeof params.ctx.MessageThreadId === "number"
|
||||
? params.ctx.MessageThreadId
|
||||
: undefined,
|
||||
threadParentId: params.ctx.ThreadParentId?.trim() || undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -243,6 +243,24 @@ describe("registerPluginCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves Discord thread command bindings with parent channel context intact", () => {
|
||||
expect(
|
||||
__testing.resolveBindingConversationFromCommand({
|
||||
channel: "discord",
|
||||
from: "discord:channel:1480554272859881494",
|
||||
accountId: "default",
|
||||
messageThreadId: "thread-42",
|
||||
threadParentId: "channel-parent-7",
|
||||
}),
|
||||
).toEqual({
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
conversationId: "channel:1480554272859881494",
|
||||
parentConversationId: "channel-parent-7",
|
||||
threadId: "thread-42",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves Telegram topic command bindings without a Telegram registry entry", () => {
|
||||
expect(
|
||||
__testing.resolveBindingConversationFromCommand({
|
||||
|
||||
@@ -148,6 +148,7 @@ function resolveBindingConversationFromCommand(params: {
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
messageThreadId?: string | number;
|
||||
threadParentId?: string;
|
||||
}): {
|
||||
channel: string;
|
||||
accountId: string;
|
||||
@@ -199,6 +200,8 @@ function resolveBindingConversationFromCommand(params: {
|
||||
"conversationId" in target
|
||||
? target.conversationId
|
||||
: `${target.chatType === "direct" ? "user" : "channel"}:${target.to}`,
|
||||
parentConversationId: params.threadParentId?.trim() || undefined,
|
||||
threadId: params.messageThreadId,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -224,6 +227,7 @@ export async function executePluginCommand(params: {
|
||||
to?: PluginCommandContext["to"];
|
||||
accountId?: PluginCommandContext["accountId"];
|
||||
messageThreadId?: PluginCommandContext["messageThreadId"];
|
||||
threadParentId?: PluginCommandContext["threadParentId"];
|
||||
}): Promise<PluginCommandResult> {
|
||||
const { command, args, senderId, channel, isAuthorizedSender, commandBody, config } = params;
|
||||
|
||||
@@ -244,6 +248,7 @@ export async function executePluginCommand(params: {
|
||||
to: params.to,
|
||||
accountId: params.accountId,
|
||||
messageThreadId: params.messageThreadId,
|
||||
threadParentId: params.threadParentId,
|
||||
});
|
||||
|
||||
const ctx: PluginCommandContext = {
|
||||
@@ -259,6 +264,7 @@ export async function executePluginCommand(params: {
|
||||
to: params.to,
|
||||
accountId: params.accountId,
|
||||
messageThreadId: params.messageThreadId,
|
||||
threadParentId: params.threadParentId,
|
||||
requestConversationBinding: async (bindingParams) => {
|
||||
if (!command.pluginRoot || !bindingConversation) {
|
||||
return {
|
||||
|
||||
@@ -1178,6 +1178,8 @@ export type PluginCommandContext = {
|
||||
accountId?: string;
|
||||
/** Thread/topic id if available */
|
||||
messageThreadId?: string | number;
|
||||
/** Parent conversation id for thread-capable channels */
|
||||
threadParentId?: string;
|
||||
requestConversationBinding: (
|
||||
params?: PluginConversationBindingRequestParams,
|
||||
) => Promise<PluginConversationBindingRequestResult>;
|
||||
|
||||
Reference in New Issue
Block a user