mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
refactor(plugins): single-source interactive dispatch and its auth downgrade via plugin SDK (#120062)
This commit is contained in:
committed by
GitHub
parent
d64f5b6a09
commit
0b849a316d
@@ -1,8 +1,7 @@
|
||||
// Discord plugin module implements interactive dispatch behavior.
|
||||
import type { ChannelStructuredComponents } from "openclaw/plugin-sdk/channel-contract";
|
||||
import {
|
||||
createInteractiveConversationBindingHelpers,
|
||||
dispatchPluginInteractiveHandler,
|
||||
createChannelInteractiveDispatcher,
|
||||
type PluginConversationBinding,
|
||||
type PluginConversationBindingRequestParams,
|
||||
type PluginConversationBindingRequestResult,
|
||||
@@ -52,61 +51,24 @@ export type DiscordInteractiveHandlerRegistration = PluginInteractiveRegistratio
|
||||
"discord"
|
||||
>;
|
||||
|
||||
type DiscordInteractiveDispatchContext = Omit<
|
||||
DiscordInteractiveHandlerContext,
|
||||
| "interaction"
|
||||
| "respond"
|
||||
| "channel"
|
||||
| "requestConversationBinding"
|
||||
| "detachConversationBinding"
|
||||
| "getCurrentConversationBinding"
|
||||
> & {
|
||||
interaction: Omit<
|
||||
DiscordInteractiveHandlerContext["interaction"],
|
||||
"data" | "namespace" | "payload"
|
||||
>;
|
||||
};
|
||||
const dispatchDiscordInteractive = createChannelInteractiveDispatcher<
|
||||
"discord",
|
||||
"interaction",
|
||||
DiscordInteractiveHandlerContext
|
||||
>({
|
||||
channel: "discord",
|
||||
interactiveKey: "interaction",
|
||||
});
|
||||
|
||||
export async function dispatchDiscordPluginInteractiveHandler(params: {
|
||||
data: string;
|
||||
interactionId: string;
|
||||
ctx: DiscordInteractiveDispatchContext;
|
||||
ctx: Parameters<typeof dispatchDiscordInteractive>[0]["ctx"];
|
||||
respond: DiscordInteractiveHandlerContext["respond"];
|
||||
onMatched?: () => Promise<void> | void;
|
||||
}) {
|
||||
return await dispatchPluginInteractiveHandler<DiscordInteractiveHandlerRegistration>({
|
||||
channel: "discord",
|
||||
data: params.data,
|
||||
return await dispatchDiscordInteractive({
|
||||
...params,
|
||||
dedupeId: params.interactionId,
|
||||
onMatched: params.onMatched,
|
||||
invoke: ({ registration, namespace, payload }) =>
|
||||
registration.handler({
|
||||
...params.ctx,
|
||||
channel: "discord",
|
||||
interaction: {
|
||||
...params.ctx.interaction,
|
||||
data: params.data,
|
||||
namespace,
|
||||
payload,
|
||||
},
|
||||
respond: params.respond,
|
||||
...createInteractiveConversationBindingHelpers({
|
||||
// Untrusted callbacks may reach their plugin, but never inherit conversation-binding authority.
|
||||
registration:
|
||||
params.ctx.auth?.isAuthorizedSender &&
|
||||
params.ctx.senderId?.trim() &&
|
||||
params.ctx.accountId?.trim() &&
|
||||
params.ctx.conversationId?.trim()
|
||||
? registration
|
||||
: { ...registration, pluginRoot: undefined },
|
||||
senderId: params.ctx.senderId,
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: params.ctx.accountId,
|
||||
conversationId: params.ctx.conversationId,
|
||||
parentConversationId: params.ctx.parentConversationId,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Slack plugin module implements interactive dispatch behavior.
|
||||
import {
|
||||
createInteractiveConversationBindingHelpers,
|
||||
dispatchPluginInteractiveHandler,
|
||||
createChannelInteractiveDispatcher,
|
||||
type PluginConversationBinding,
|
||||
type PluginConversationBindingRequestParams,
|
||||
type PluginConversationBindingRequestResult,
|
||||
@@ -86,25 +85,21 @@ export type SlackInteractiveHandlerRegistration = PluginInteractiveRegistration<
|
||||
SlackInteractiveHandlerResult
|
||||
>;
|
||||
|
||||
type SlackInteractiveDispatchContext = Omit<
|
||||
const dispatchSlackInteractive = createChannelInteractiveDispatcher<
|
||||
"slack",
|
||||
"interaction",
|
||||
SlackInteractiveHandlerContext,
|
||||
| "interaction"
|
||||
| "respond"
|
||||
| "channel"
|
||||
| "requestConversationBinding"
|
||||
| "detachConversationBinding"
|
||||
| "getCurrentConversationBinding"
|
||||
> & {
|
||||
interaction:
|
||||
| Omit<SlackBlockInteractivePayload, "data" | "namespace" | "payload">
|
||||
| Omit<SlackModalInteractivePayload, "data" | "namespace" | "payload">;
|
||||
};
|
||||
SlackInteractiveHandlerResult
|
||||
>({
|
||||
channel: "slack",
|
||||
interactiveKey: "interaction",
|
||||
});
|
||||
|
||||
export async function dispatchSlackPluginInteractiveHandler(params: {
|
||||
data: string;
|
||||
interactionId: string;
|
||||
channelType?: "im" | "mpim" | "channel" | "group";
|
||||
ctx: SlackInteractiveDispatchContext;
|
||||
ctx: Parameters<typeof dispatchSlackInteractive>[0]["ctx"];
|
||||
respond: SlackInteractiveHandlerContext["respond"];
|
||||
onMatched?: () => Promise<void> | void;
|
||||
}) {
|
||||
@@ -117,39 +112,17 @@ export async function dispatchSlackPluginInteractiveHandler(params: {
|
||||
: params.ctx.conversationId.trim();
|
||||
const threadId = params.ctx.threadId?.trim() || undefined;
|
||||
|
||||
return await dispatchPluginInteractiveHandler<SlackInteractiveHandlerRegistration>({
|
||||
channel: "slack",
|
||||
data: params.data,
|
||||
return await dispatchSlackInteractive({
|
||||
...params,
|
||||
dedupeId: params.interactionId,
|
||||
onMatched: params.onMatched,
|
||||
invoke: ({ registration, namespace, payload }) =>
|
||||
registration.handler({
|
||||
...params.ctx,
|
||||
channel: "slack",
|
||||
interaction: {
|
||||
...params.ctx.interaction,
|
||||
data: params.data,
|
||||
namespace,
|
||||
payload,
|
||||
},
|
||||
respond: params.respond,
|
||||
...createInteractiveConversationBindingHelpers({
|
||||
// The shared helpers fail closed without owner authority; never expose it to unauthenticated actions.
|
||||
registration:
|
||||
params.ctx.auth.isAuthorizedSender && baseConversationId
|
||||
? registration
|
||||
: { ...registration, pluginRoot: undefined },
|
||||
senderId: params.ctx.senderId,
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: params.ctx.accountId,
|
||||
conversationId: threadId ?? baseConversationId,
|
||||
parentConversationId: threadId
|
||||
? (params.ctx.parentConversationId ?? baseConversationId)
|
||||
: params.ctx.parentConversationId,
|
||||
threadId,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
conversation: {
|
||||
channel: "slack",
|
||||
accountId: params.ctx.accountId,
|
||||
conversationId: threadId ?? baseConversationId,
|
||||
parentConversationId: threadId
|
||||
? (params.ctx.parentConversationId ?? baseConversationId)
|
||||
: params.ctx.parentConversationId,
|
||||
threadId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,13 +98,75 @@ vi.mock("openclaw/plugin-sdk/question-gateway-runtime", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/plugin-runtime", () => ({
|
||||
dispatchPluginInteractiveHandler: (arg: unknown) => dispatchPluginInteractiveHandlerMock(arg),
|
||||
createInteractiveConversationBindingHelpers: (params: {
|
||||
registration: { pluginRoot?: string };
|
||||
conversation: Record<string, unknown>;
|
||||
}) => createInteractiveConversationBindingHelpersMock(params),
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/plugin-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
createChannelInteractiveDispatcher: (config: {
|
||||
channel: string;
|
||||
interactiveKey: PropertyKey;
|
||||
dispatchInteractiveKey?: PropertyKey;
|
||||
}) => {
|
||||
return (params: {
|
||||
data: string;
|
||||
dedupeId: string;
|
||||
ctx: Record<PropertyKey, unknown> & {
|
||||
accountId: string;
|
||||
conversationId: string;
|
||||
senderId?: string;
|
||||
auth: { isAuthorizedSender: boolean };
|
||||
};
|
||||
respond: unknown;
|
||||
conversation?: Record<string, unknown>;
|
||||
onMatched?: () => Promise<void> | void;
|
||||
afterInvoke?: (result: unknown) => Promise<void> | void;
|
||||
}) =>
|
||||
dispatchPluginInteractiveHandlerMock({
|
||||
channel: config.channel,
|
||||
data: params.data,
|
||||
dedupeId: params.dedupeId,
|
||||
onMatched: params.onMatched,
|
||||
afterInvoke: params.afterInvoke,
|
||||
invoke: ({
|
||||
registration,
|
||||
namespace,
|
||||
payload,
|
||||
}: {
|
||||
registration: { pluginRoot?: string; handler: (ctx: unknown) => unknown };
|
||||
namespace: string;
|
||||
payload: string;
|
||||
}) => {
|
||||
const dispatchKey = config.dispatchInteractiveKey ?? config.interactiveKey;
|
||||
const handlerContext = { ...params.ctx };
|
||||
const interactiveContext = handlerContext[dispatchKey];
|
||||
delete handlerContext[dispatchKey];
|
||||
const hasBindingAuthority =
|
||||
params.ctx.auth.isAuthorizedSender &&
|
||||
params.ctx.senderId?.trim() &&
|
||||
params.ctx.accountId.trim() &&
|
||||
params.ctx.conversationId.trim();
|
||||
return registration.handler({
|
||||
...handlerContext,
|
||||
channel: config.channel,
|
||||
[config.interactiveKey]: {
|
||||
...(interactiveContext as object),
|
||||
data: params.data,
|
||||
namespace,
|
||||
payload,
|
||||
},
|
||||
respond: params.respond,
|
||||
...createInteractiveConversationBindingHelpersMock({
|
||||
registration: hasBindingAuthority
|
||||
? registration
|
||||
: { ...registration, pluginRoot: undefined },
|
||||
conversation: params.conversation ?? {},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../conversation.runtime.js", () => {
|
||||
const parsePluginBindingApprovalCustomId = (value: string) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Telegram plugin module implements interactive dispatch behavior.
|
||||
import {
|
||||
createInteractiveConversationBindingHelpers,
|
||||
dispatchPluginInteractiveHandler,
|
||||
createChannelInteractiveDispatcher,
|
||||
type PluginConversationBinding,
|
||||
type PluginConversationBindingRequestParams,
|
||||
type PluginConversationBindingRequestResult,
|
||||
@@ -63,78 +62,28 @@ export type TelegramInteractiveHandlerRegistration = PluginInteractiveRegistrati
|
||||
TelegramInteractiveHandlerResult
|
||||
>;
|
||||
|
||||
type TelegramInteractiveDispatchContext = Omit<
|
||||
const dispatchTelegramInteractive = createChannelInteractiveDispatcher<
|
||||
"telegram",
|
||||
"callback",
|
||||
TelegramInteractiveHandlerContext,
|
||||
| "callback"
|
||||
| "respond"
|
||||
| "channel"
|
||||
| "requestConversationBinding"
|
||||
| "detachConversationBinding"
|
||||
| "getCurrentConversationBinding"
|
||||
> & {
|
||||
callbackMessage: {
|
||||
messageId: number;
|
||||
chatId: string;
|
||||
messageText?: string;
|
||||
};
|
||||
};
|
||||
TelegramInteractiveHandlerResult,
|
||||
"callbackMessage"
|
||||
>({
|
||||
channel: "telegram",
|
||||
interactiveKey: "callback",
|
||||
dispatchInteractiveKey: "callbackMessage",
|
||||
});
|
||||
|
||||
export async function dispatchTelegramPluginInteractiveHandler(params: {
|
||||
data: string;
|
||||
callbackId: string;
|
||||
ctx: TelegramInteractiveDispatchContext;
|
||||
respond: {
|
||||
reply: (params: { text: string; buttons?: TelegramInteractiveButtons }) => Promise<void>;
|
||||
editMessage: (params: { text: string; buttons?: TelegramInteractiveButtons }) => Promise<void>;
|
||||
editButtons: (params: { buttons: TelegramInteractiveButtons }) => Promise<void>;
|
||||
clearButtons: () => Promise<void>;
|
||||
deleteMessage: () => Promise<void>;
|
||||
};
|
||||
ctx: Parameters<typeof dispatchTelegramInteractive>[0]["ctx"];
|
||||
respond: TelegramInteractiveHandlerContext["respond"];
|
||||
onMatched?: () => Promise<void> | void;
|
||||
afterInvoke?: (result: TelegramInteractiveHandlerResult) => Promise<void> | void;
|
||||
}) {
|
||||
return await dispatchPluginInteractiveHandler<
|
||||
TelegramInteractiveHandlerRegistration,
|
||||
TelegramInteractiveHandlerResult
|
||||
>({
|
||||
channel: "telegram",
|
||||
data: params.data,
|
||||
return await dispatchTelegramInteractive({
|
||||
...params,
|
||||
dedupeId: params.callbackId,
|
||||
onMatched: params.onMatched,
|
||||
afterInvoke: params.afterInvoke,
|
||||
invoke: ({ registration, namespace, payload }) => {
|
||||
const { callbackMessage, ...handlerContext } = params.ctx;
|
||||
return registration.handler({
|
||||
...handlerContext,
|
||||
channel: "telegram",
|
||||
callback: {
|
||||
data: params.data,
|
||||
namespace,
|
||||
payload,
|
||||
messageId: callbackMessage.messageId,
|
||||
chatId: callbackMessage.chatId,
|
||||
messageText: callbackMessage.messageText,
|
||||
},
|
||||
respond: params.respond,
|
||||
...createInteractiveConversationBindingHelpers({
|
||||
// Untrusted callbacks may reach their plugin, but never inherit conversation-binding authority.
|
||||
registration:
|
||||
handlerContext.auth?.isAuthorizedSender &&
|
||||
handlerContext.senderId?.trim() &&
|
||||
handlerContext.accountId?.trim() &&
|
||||
handlerContext.conversationId?.trim()
|
||||
? registration
|
||||
: { ...registration, pluginRoot: undefined },
|
||||
senderId: handlerContext.senderId,
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: handlerContext.accountId,
|
||||
conversationId: handlerContext.conversationId,
|
||||
parentConversationId: handlerContext.parentConversationId,
|
||||
threadId: handlerContext.threadId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user