mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 20:40:45 +00:00
fix(plugins): tighten interactive callback handling
This commit is contained in:
@@ -877,8 +877,9 @@ async function dispatchPluginDiscordInteractiveEvent(params: {
|
||||
} catch {
|
||||
// Interaction may have expired after the handler finished.
|
||||
}
|
||||
return "handled";
|
||||
}
|
||||
return "handled";
|
||||
return "unmatched";
|
||||
}
|
||||
|
||||
function resolveComponentCommandAuthorized(params: {
|
||||
|
||||
@@ -598,6 +598,27 @@ describe("discord component interactions", () => {
|
||||
);
|
||||
expect(dispatchReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls through to built-in Discord component routing when a plugin declines handling", async () => {
|
||||
registerDiscordComponentEntries({
|
||||
entries: [createButtonEntry({ callbackData: "codex:approve" })],
|
||||
modals: [],
|
||||
});
|
||||
dispatchPluginInteractiveHandlerMock.mockResolvedValue({
|
||||
matched: true,
|
||||
handled: false,
|
||||
duplicate: false,
|
||||
});
|
||||
|
||||
const button = createDiscordComponentButton(createComponentContext());
|
||||
const { interaction, reply } = createComponentButtonInteraction();
|
||||
|
||||
await button.run(interaction, { cid: "btn_1" } as ComponentData);
|
||||
|
||||
expect(dispatchPluginInteractiveHandlerMock).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledWith({ content: "✓" });
|
||||
expect(dispatchReplyMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDiscordOwnerAllowFrom", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
buildCanonicalSentMessageHookContext,
|
||||
deriveInboundMessageHookContext,
|
||||
toPluginInboundClaimContext,
|
||||
toInternalMessagePreprocessedContext,
|
||||
toInternalMessageReceivedContext,
|
||||
toInternalMessageSentContext,
|
||||
@@ -99,6 +100,29 @@ describe("message hook mappers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes Discord channel targets for inbound claim contexts", () => {
|
||||
const canonical = deriveInboundMessageHookContext(
|
||||
makeInboundCtx({
|
||||
Provider: "discord",
|
||||
Surface: "discord",
|
||||
OriginatingChannel: "discord",
|
||||
To: "channel:123456789012345678",
|
||||
OriginatingTo: "channel:123456789012345678",
|
||||
GroupChannel: "general",
|
||||
GroupSubject: "guild",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(toPluginInboundClaimContext(canonical)).toEqual({
|
||||
channelId: "discord",
|
||||
accountId: "acc-1",
|
||||
conversationId: "123456789012345678",
|
||||
parentConversationId: undefined,
|
||||
senderId: "sender-1",
|
||||
messageId: "msg-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps transcribed and preprocessed internal payloads", () => {
|
||||
const cfg = {} as OpenClawConfig;
|
||||
const canonical = deriveInboundMessageHookContext(makeInboundCtx({ Transcript: undefined }));
|
||||
|
||||
@@ -153,6 +153,12 @@ function stripChannelPrefix(value: string | undefined, channelId: string): strin
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const genericPrefixes = ["channel:", "chat:", "user:"];
|
||||
for (const prefix of genericPrefixes) {
|
||||
if (value.startsWith(prefix)) {
|
||||
return value.slice(prefix.length);
|
||||
}
|
||||
}
|
||||
const prefix = `${channelId}:`;
|
||||
return value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
||||
}
|
||||
|
||||
@@ -146,4 +146,56 @@ describe("plugin interactive handlers", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not consume dedupe keys when a handler throws", async () => {
|
||||
const handler = vi
|
||||
.fn(async () => ({ handled: true }))
|
||||
.mockRejectedValueOnce(new Error("boom"))
|
||||
.mockResolvedValueOnce({ handled: true });
|
||||
expect(
|
||||
registerPluginInteractiveHandler("codex-plugin", {
|
||||
channel: "telegram",
|
||||
namespace: "codex",
|
||||
handler,
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
|
||||
const baseParams = {
|
||||
channel: "telegram" as const,
|
||||
data: "codex:resume:thread-1",
|
||||
callbackId: "cb-throw",
|
||||
ctx: {
|
||||
accountId: "default",
|
||||
callbackId: "cb-throw",
|
||||
conversationId: "-10099:topic:77",
|
||||
parentConversationId: "-10099",
|
||||
senderId: "user-1",
|
||||
senderUsername: "ada",
|
||||
threadId: 77,
|
||||
isGroup: true,
|
||||
isForum: true,
|
||||
auth: { isAuthorizedSender: true },
|
||||
callbackMessage: {
|
||||
messageId: 55,
|
||||
chatId: "-10099",
|
||||
messageText: "Pick a thread",
|
||||
},
|
||||
},
|
||||
respond: {
|
||||
reply: vi.fn(async () => {}),
|
||||
editMessage: vi.fn(async () => {}),
|
||||
editButtons: vi.fn(async () => {}),
|
||||
clearButtons: vi.fn(async () => {}),
|
||||
deleteMessage: vi.fn(async () => {}),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(dispatchPluginInteractiveHandler(baseParams)).rejects.toThrow("boom");
|
||||
await expect(dispatchPluginInteractiveHandler(baseParams)).resolves.toEqual({
|
||||
matched: true,
|
||||
handled: true,
|
||||
duplicate: false,
|
||||
});
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,7 +187,7 @@ export async function dispatchPluginInteractiveHandler(params: {
|
||||
|
||||
const dedupeKey =
|
||||
params.channel === "telegram" ? params.callbackId?.trim() : params.interactionId?.trim();
|
||||
if (dedupeKey && callbackDedupe.check(dedupeKey)) {
|
||||
if (dedupeKey && callbackDedupe.peek(dedupeKey)) {
|
||||
return { matched: true, handled: true, duplicate: true };
|
||||
}
|
||||
|
||||
@@ -253,6 +253,9 @@ export async function dispatchPluginInteractiveHandler(params: {
|
||||
});
|
||||
}
|
||||
const resolved = await result;
|
||||
if (dedupeKey) {
|
||||
callbackDedupe.check(dedupeKey);
|
||||
}
|
||||
|
||||
return {
|
||||
matched: true,
|
||||
|
||||
@@ -61,4 +61,23 @@ describe("createTelegramTypingLease", () => {
|
||||
|
||||
lease.stop();
|
||||
});
|
||||
|
||||
it("falls back to the default interval for non-finite values", async () => {
|
||||
vi.useFakeTimers();
|
||||
const pulse = vi.fn(async () => undefined);
|
||||
|
||||
const lease = await createTelegramTypingLease({
|
||||
to: "telegram:123",
|
||||
intervalMs: Number.NaN,
|
||||
pulse,
|
||||
});
|
||||
|
||||
expect(pulse).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(3_999);
|
||||
expect(pulse).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(pulse).toHaveBeenCalledTimes(2);
|
||||
|
||||
lease.stop();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,10 @@ export async function createTelegramTypingLease(params: CreateTelegramTypingLeas
|
||||
refresh: () => Promise<void>;
|
||||
stop: () => void;
|
||||
}> {
|
||||
const intervalMs = Math.max(1000, Math.floor(params.intervalMs ?? 4_000));
|
||||
const intervalMs =
|
||||
typeof params.intervalMs === "number" && Number.isFinite(params.intervalMs)
|
||||
? Math.max(1_000, Math.floor(params.intervalMs))
|
||||
: 4_000;
|
||||
let stopped = false;
|
||||
|
||||
const refresh = async () => {
|
||||
|
||||
Reference in New Issue
Block a user