mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
fix(ci): repair discord and telegram follow-ups
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
ensureConfiguredBindingRouteReady,
|
||||
resolveConfiguredBindingRoute,
|
||||
} from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import * as conversationRuntime from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
resolveDiscordBoundConversationRoute,
|
||||
@@ -10,7 +7,9 @@ import {
|
||||
} from "./route-resolution.js";
|
||||
import type { ThreadBindingRecord } from "./thread-bindings.js";
|
||||
|
||||
type ResolvedConfiguredBindingRoute = ReturnType<typeof resolveConfiguredBindingRoute>;
|
||||
type ResolvedConfiguredBindingRoute = ReturnType<
|
||||
typeof conversationRuntime.resolveConfiguredBindingRoute
|
||||
>;
|
||||
type ConfiguredBindingResolution = NonNullable<
|
||||
NonNullable<ResolvedConfiguredBindingRoute>["bindingResolution"]
|
||||
>;
|
||||
@@ -21,7 +20,9 @@ export type DiscordNativeInteractionRouteState = {
|
||||
boundSessionKey?: string;
|
||||
configuredRoute: ResolvedConfiguredBindingRoute | null;
|
||||
configuredBinding: ConfiguredBindingResolution | null;
|
||||
bindingReadiness: Awaited<ReturnType<typeof ensureConfiguredBindingRouteReady>> | null;
|
||||
bindingReadiness: Awaited<
|
||||
ReturnType<typeof conversationRuntime.ensureConfiguredBindingRouteReady>
|
||||
> | null;
|
||||
};
|
||||
|
||||
export async function resolveDiscordNativeInteractionRouteState(params: {
|
||||
@@ -50,7 +51,7 @@ export async function resolveDiscordNativeInteractionRouteState(params: {
|
||||
});
|
||||
const configuredRoute =
|
||||
params.threadBinding == null
|
||||
? resolveConfiguredBindingRoute({
|
||||
? conversationRuntime.resolveConfiguredBindingRoute({
|
||||
cfg: params.cfg,
|
||||
route,
|
||||
conversation: {
|
||||
@@ -73,7 +74,7 @@ export async function resolveDiscordNativeInteractionRouteState(params: {
|
||||
});
|
||||
const bindingReadiness =
|
||||
params.enforceConfiguredBindingReadiness && configuredBinding
|
||||
? await ensureConfiguredBindingRouteReady({
|
||||
? await conversationRuntime.ensureConfiguredBindingRouteReady({
|
||||
cfg: params.cfg,
|
||||
bindingResolution: configuredBinding,
|
||||
})
|
||||
|
||||
@@ -10,33 +10,14 @@ import {
|
||||
} from "./native-command.test-helpers.js";
|
||||
import { createNoopThreadBindingManager } from "./thread-bindings.js";
|
||||
|
||||
type EnsureConfiguredBindingRouteReadyFn =
|
||||
typeof import("openclaw/plugin-sdk/conversation-runtime").ensureConfiguredBindingRouteReady;
|
||||
|
||||
let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand;
|
||||
|
||||
const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() =>
|
||||
vi.fn<EnsureConfiguredBindingRouteReadyFn>(async () => ({
|
||||
ok: true,
|
||||
})),
|
||||
);
|
||||
let discordNativeCommandTesting: typeof import("./native-command.js").__testing;
|
||||
const runtimeModuleMocks = vi.hoisted(() => ({
|
||||
matchPluginCommand: vi.fn(),
|
||||
executePluginCommand: vi.fn(),
|
||||
dispatchReplyWithDispatcher: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
ensureConfiguredBindingRouteReady: (...args: unknown[]) =>
|
||||
ensureConfiguredBindingRouteReadyMock(
|
||||
...(args as Parameters<EnsureConfiguredBindingRouteReadyFn>),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/plugin-runtime")>();
|
||||
return {
|
||||
@@ -168,6 +149,72 @@ async function createNativeCommand(cfg: OpenClawConfig, commandSpec: NativeComma
|
||||
});
|
||||
}
|
||||
|
||||
function createConfiguredRouteState(params: {
|
||||
sessionKey: string;
|
||||
agentId?: string;
|
||||
accountId?: string;
|
||||
}) {
|
||||
return {
|
||||
route: {
|
||||
agentId: params.agentId ?? "main",
|
||||
channel: "discord",
|
||||
accountId: params.accountId ?? "default",
|
||||
sessionKey: params.sessionKey,
|
||||
mainSessionKey: `agent:${params.agentId ?? "main"}:main`,
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "binding.channel",
|
||||
},
|
||||
effectiveRoute: {
|
||||
agentId: params.agentId ?? "main",
|
||||
channel: "discord",
|
||||
accountId: params.accountId ?? "default",
|
||||
sessionKey: params.sessionKey,
|
||||
mainSessionKey: `agent:${params.agentId ?? "main"}:main`,
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "binding.channel",
|
||||
},
|
||||
boundSessionKey: params.sessionKey,
|
||||
configuredRoute: null,
|
||||
configuredBinding: null,
|
||||
bindingReadiness: { ok: true } as const,
|
||||
} satisfies Awaited<
|
||||
ReturnType<typeof import("./native-command-route.js").resolveDiscordNativeInteractionRouteState>
|
||||
>;
|
||||
}
|
||||
|
||||
function createUnboundRouteState(params: {
|
||||
sessionKey: string;
|
||||
agentId?: string;
|
||||
accountId?: string;
|
||||
}) {
|
||||
return {
|
||||
route: {
|
||||
agentId: params.agentId ?? "main",
|
||||
channel: "discord",
|
||||
accountId: params.accountId ?? "default",
|
||||
sessionKey: params.sessionKey,
|
||||
mainSessionKey: `agent:${params.agentId ?? "main"}:main`,
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "default",
|
||||
},
|
||||
effectiveRoute: {
|
||||
agentId: params.agentId ?? "main",
|
||||
channel: "discord",
|
||||
accountId: params.accountId ?? "default",
|
||||
sessionKey: params.sessionKey,
|
||||
mainSessionKey: `agent:${params.agentId ?? "main"}:main`,
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "default",
|
||||
},
|
||||
boundSessionKey: undefined,
|
||||
configuredRoute: null,
|
||||
configuredBinding: null,
|
||||
bindingReadiness: null,
|
||||
} satisfies Awaited<
|
||||
ReturnType<typeof import("./native-command-route.js").resolveDiscordNativeInteractionRouteState>
|
||||
>;
|
||||
}
|
||||
|
||||
async function createPluginCommand(params: { cfg: OpenClawConfig; name: string }) {
|
||||
return createDiscordNativeCommand({
|
||||
command: {
|
||||
@@ -262,7 +309,6 @@ function expectBoundSessionDispatch(
|
||||
}
|
||||
expect(dispatchCall.ctx.SessionKey).toMatch(expectedPattern);
|
||||
expect(dispatchCall.ctx.CommandTargetSessionKey).toMatch(expectedPattern);
|
||||
expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
async function expectBoundStatusCommandDispatch(params: {
|
||||
@@ -283,17 +329,14 @@ async function expectBoundStatusCommandDispatch(params: {
|
||||
|
||||
describe("Discord native plugin command dispatch", () => {
|
||||
beforeAll(async () => {
|
||||
({ createDiscordNativeCommand } = await import("./native-command.js"));
|
||||
({ createDiscordNativeCommand, __testing: discordNativeCommandTesting } =
|
||||
await import("./native-command.js"));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
clearPluginCommands();
|
||||
setDefaultChannelPluginRegistryForTests();
|
||||
ensureConfiguredBindingRouteReadyMock.mockReset();
|
||||
ensureConfiguredBindingRouteReadyMock.mockResolvedValue({
|
||||
ok: true,
|
||||
});
|
||||
const actualPluginRuntime = await vi.importActual<
|
||||
typeof import("openclaw/plugin-sdk/plugin-runtime")
|
||||
>("openclaw/plugin-sdk/plugin-runtime");
|
||||
@@ -313,6 +356,23 @@ describe("Discord native plugin command dispatch", () => {
|
||||
tool: 0,
|
||||
},
|
||||
} as never);
|
||||
discordNativeCommandTesting.setMatchPluginCommand(
|
||||
runtimeModuleMocks.matchPluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").matchPluginCommand,
|
||||
);
|
||||
discordNativeCommandTesting.setExecutePluginCommand(
|
||||
runtimeModuleMocks.executePluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").executePluginCommand,
|
||||
);
|
||||
discordNativeCommandTesting.setDispatchReplyWithDispatcher(
|
||||
runtimeModuleMocks.dispatchReplyWithDispatcher as typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithDispatcher,
|
||||
);
|
||||
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async (params) =>
|
||||
createUnboundRouteState({
|
||||
sessionKey: params.isDirectMessage
|
||||
? `agent:main:discord:dm:${params.directUserId ?? "owner"}`
|
||||
: `agent:main:discord:channel:${params.conversationId}`,
|
||||
accountId: params.accountId,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("executes plugin commands from the real registry through the native Discord command path", async () => {
|
||||
@@ -449,6 +509,12 @@ describe("Discord native plugin command dispatch", () => {
|
||||
guildId: "1459246755253325866",
|
||||
guildName: "Ops",
|
||||
});
|
||||
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async () =>
|
||||
createConfiguredRouteState({
|
||||
sessionKey: "agent:codex:acp:binding:discord:default:guild-channel",
|
||||
agentId: "codex",
|
||||
}),
|
||||
);
|
||||
|
||||
await expectBoundStatusCommandDispatch({
|
||||
cfg,
|
||||
@@ -494,9 +560,39 @@ describe("Discord native plugin command dispatch", () => {
|
||||
guildName: "Ops",
|
||||
});
|
||||
|
||||
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async () =>
|
||||
createUnboundRouteState({
|
||||
sessionKey: `agent:qwen:discord:channel:${channelId}`,
|
||||
agentId: "qwen",
|
||||
}),
|
||||
);
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(null);
|
||||
const dispatchSpy = createDispatchSpy();
|
||||
const command = await createStatusCommand(cfg);
|
||||
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async () => ({
|
||||
route: {
|
||||
agentId: "qwen",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:qwen:discord:channel:1478836151241412759",
|
||||
mainSessionKey: "agent:qwen:main",
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "binding.channel",
|
||||
},
|
||||
effectiveRoute: {
|
||||
agentId: "qwen",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:qwen:discord:channel:1478836151241412759",
|
||||
mainSessionKey: "agent:qwen:main",
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "binding.channel",
|
||||
},
|
||||
boundSessionKey: undefined,
|
||||
configuredRoute: null,
|
||||
configuredBinding: null,
|
||||
bindingReadiness: null,
|
||||
}));
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
@@ -508,7 +604,6 @@ describe("Discord native plugin command dispatch", () => {
|
||||
expect(dispatchCall.ctx?.CommandTargetSessionKey).toBe(
|
||||
"agent:qwen:discord:channel:1478836151241412759",
|
||||
);
|
||||
expect(ensureConfiguredBindingRouteReadyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes Discord DM native slash commands through configured ACP bindings", async () => {
|
||||
@@ -517,6 +612,12 @@ describe("Discord native plugin command dispatch", () => {
|
||||
channelId: "dm-1",
|
||||
peerKind: "direct",
|
||||
});
|
||||
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async () =>
|
||||
createConfiguredRouteState({
|
||||
sessionKey: "agent:codex:acp:binding:discord:default:dm",
|
||||
agentId: "codex",
|
||||
}),
|
||||
);
|
||||
|
||||
await expectBoundStatusCommandDispatch({
|
||||
cfg,
|
||||
@@ -534,10 +635,12 @@ describe("Discord native plugin command dispatch", () => {
|
||||
guildName: "Ops",
|
||||
includeChannelAccess: false,
|
||||
});
|
||||
ensureConfiguredBindingRouteReadyMock.mockResolvedValue({
|
||||
ok: false,
|
||||
error: "acpx exited with code 1",
|
||||
});
|
||||
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async () =>
|
||||
createConfiguredRouteState({
|
||||
sessionKey: "agent:codex:acp:binding:discord:default:recovery",
|
||||
agentId: "codex",
|
||||
}),
|
||||
);
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(null);
|
||||
const dispatchSpy = createDispatchSpy();
|
||||
const command = await createNativeCommand(cfg, {
|
||||
@@ -556,7 +659,6 @@ describe("Discord native plugin command dispatch", () => {
|
||||
expect(dispatchCall.ctx?.CommandTargetSessionKey).toMatch(
|
||||
/^agent:codex:acp:binding:discord:default:/,
|
||||
);
|
||||
expect(ensureConfiguredBindingRouteReadyMock).not.toHaveBeenCalled();
|
||||
expect(interaction.reply).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: "Configured ACP binding is unavailable right now. Please try again.",
|
||||
|
||||
@@ -36,13 +36,13 @@ import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/config-runti
|
||||
import { resolveOpenProviderRuntimeGroupPolicy } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { buildPairingReply } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { executePluginCommand, matchPluginCommand } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import * as pluginRuntime from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import {
|
||||
resolveSendableOutboundReplyParts,
|
||||
resolveTextChunksWithFallback,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import * as replyRuntime from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveChunkMode, resolveTextChunkLimit } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { dispatchReplyWithDispatcher } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -84,6 +84,41 @@ const log = createSubsystemLogger("discord/native-command");
|
||||
// Discord application command and option descriptions are limited to 1-100 chars.
|
||||
// https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure
|
||||
const DISCORD_COMMAND_DESCRIPTION_MAX = 100;
|
||||
let matchPluginCommandImpl = pluginRuntime.matchPluginCommand;
|
||||
let executePluginCommandImpl = pluginRuntime.executePluginCommand;
|
||||
let dispatchReplyWithDispatcherImpl = replyRuntime.dispatchReplyWithDispatcher;
|
||||
let resolveDiscordNativeInteractionRouteStateImpl = resolveDiscordNativeInteractionRouteState;
|
||||
|
||||
export const __testing = {
|
||||
setMatchPluginCommand(
|
||||
next: typeof pluginRuntime.matchPluginCommand,
|
||||
): typeof pluginRuntime.matchPluginCommand {
|
||||
const previous = matchPluginCommandImpl;
|
||||
matchPluginCommandImpl = next;
|
||||
return previous;
|
||||
},
|
||||
setExecutePluginCommand(
|
||||
next: typeof pluginRuntime.executePluginCommand,
|
||||
): typeof pluginRuntime.executePluginCommand {
|
||||
const previous = executePluginCommandImpl;
|
||||
executePluginCommandImpl = next;
|
||||
return previous;
|
||||
},
|
||||
setDispatchReplyWithDispatcher(
|
||||
next: typeof replyRuntime.dispatchReplyWithDispatcher,
|
||||
): typeof replyRuntime.dispatchReplyWithDispatcher {
|
||||
const previous = dispatchReplyWithDispatcherImpl;
|
||||
dispatchReplyWithDispatcherImpl = next;
|
||||
return previous;
|
||||
},
|
||||
setResolveDiscordNativeInteractionRouteState(
|
||||
next: typeof resolveDiscordNativeInteractionRouteState,
|
||||
): typeof resolveDiscordNativeInteractionRouteState {
|
||||
const previous = resolveDiscordNativeInteractionRouteStateImpl;
|
||||
resolveDiscordNativeInteractionRouteStateImpl = next;
|
||||
return previous;
|
||||
},
|
||||
};
|
||||
|
||||
function truncateDiscordCommandDescription(params: { value: string; label: string }): string {
|
||||
const { value, label } = params;
|
||||
@@ -863,13 +898,13 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginMatch = matchPluginCommand(prompt);
|
||||
const pluginMatch = matchPluginCommandImpl(prompt);
|
||||
if (pluginMatch) {
|
||||
if (suppressReplies) {
|
||||
return;
|
||||
}
|
||||
const channelId = rawChannelId || "unknown";
|
||||
const pluginReply = await executePluginCommand({
|
||||
const pluginReply = await executePluginCommandImpl({
|
||||
command: pluginMatch.command,
|
||||
args: pluginMatch.args,
|
||||
senderId: sender.id,
|
||||
@@ -926,7 +961,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
const interactionId = interaction.rawData.id;
|
||||
const threadBinding = isThreadChannel ? threadBindings.getByThreadId(rawChannelId) : undefined;
|
||||
const commandName = command.nativeName ?? command.key;
|
||||
const routeState = await resolveDiscordNativeInteractionRouteState({
|
||||
const routeState = await resolveDiscordNativeInteractionRouteStateImpl({
|
||||
cfg,
|
||||
accountId,
|
||||
guildId: interaction.guild?.id ?? undefined,
|
||||
@@ -994,7 +1029,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, effectiveRoute.agentId);
|
||||
|
||||
let didReply = false;
|
||||
const dispatchResult = await dispatchReplyWithDispatcher({
|
||||
const dispatchResult = await dispatchReplyWithDispatcherImpl({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcherOptions: {
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from "./src/allow-from.js";
|
||||
export * from "./src/api-fetch.js";
|
||||
export * from "./src/bot/helpers.js";
|
||||
export * from "./src/directory-config.js";
|
||||
export * from "./src/exec-approval-forwarding.js";
|
||||
export * from "./src/exec-approvals.js";
|
||||
export * from "./src/group-policy.js";
|
||||
export * from "./src/inline-buttons.js";
|
||||
|
||||
Reference in New Issue
Block a user