mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(outbound): keep Discord runtime adapters resolvable (#91119)
Summary: - The branch changes outbound channel bootstrap and resolution so delivery paths prefer send-capable runtime a ... avoid setup-only shells for runtime delivery, retry non-send-capable bootstraps, and add regression tests. - PR surface: Source +121, Tests +294. Total +415 across 4 files. - Reproducibility: yes. The linked stable-release report supplies the user-visible Discord failure, and curren ... p-shell/direct-registry path that can satisfy runtime resolution before a send-capable adapter is verified. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(outbound): keep Discord runtime adapters resolvable - PR branch already contained follow-up commit before automerge: fix(clawsweeper): reconcile automerge-openclaw-openclaw-90198 with ma… Validation: - ClawSweeper review passed for head8711ada0c4. - Required merge gates passed before the squash merge. Prepared head SHA:8711ada0c4Review: https://github.com/openclaw/openclaw/pull/91119#issuecomment-4641934231 Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: thewilloftheshadow Co-authored-by: thewilloftheshadow <35580099+thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
co-authored by
Andy Ye
clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
thewilloftheshadow
parent
dcba17d019
commit
58b68e92f2
@@ -76,4 +76,94 @@ describe("bootstrapOutboundChannelPlugin", () => {
|
||||
|
||||
expect(loaderMocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips bootstrap when the active replacement registry can send for a pinned setup shell", () => {
|
||||
const setup = createEmptyPluginRegistry();
|
||||
setup.channels = [
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: { id: "discord", meta: {} },
|
||||
source: "setup",
|
||||
},
|
||||
] as never;
|
||||
const runtime = createEmptyPluginRegistry();
|
||||
runtime.channels = [
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: {
|
||||
id: "discord",
|
||||
meta: {},
|
||||
outbound: { sendText: async () => ({ messageId: "1" }) },
|
||||
},
|
||||
source: "runtime",
|
||||
},
|
||||
] as never;
|
||||
setActivePluginRegistry(setup);
|
||||
pinActivePluginChannelRegistry(setup);
|
||||
setActivePluginRegistry(runtime);
|
||||
|
||||
bootstrapOutboundChannelPlugin({
|
||||
channel: "discord",
|
||||
cfg: discordConfig,
|
||||
});
|
||||
|
||||
expect(loaderMocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips bootstrap when the active replacement registry has a message send surface", () => {
|
||||
const setup = createEmptyPluginRegistry();
|
||||
setup.channels = [
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: { id: "discord", meta: {} },
|
||||
source: "setup",
|
||||
},
|
||||
] as never;
|
||||
const runtime = createEmptyPluginRegistry();
|
||||
runtime.channels = [
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: {
|
||||
id: "discord",
|
||||
meta: {},
|
||||
message: { send: { text: async () => ({ messageId: "1" }) } },
|
||||
},
|
||||
source: "runtime",
|
||||
},
|
||||
] as never;
|
||||
setActivePluginRegistry(setup);
|
||||
pinActivePluginChannelRegistry(setup);
|
||||
setActivePluginRegistry(runtime);
|
||||
|
||||
bootstrapOutboundChannelPlugin({
|
||||
channel: "discord",
|
||||
cfg: discordConfig,
|
||||
});
|
||||
|
||||
expect(loaderMocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retries when bootstrap returns without making the channel send-capable", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.channels = [
|
||||
{
|
||||
pluginId: "discord",
|
||||
plugin: { id: "discord", meta: {} },
|
||||
source: "setup",
|
||||
},
|
||||
] as never;
|
||||
setActivePluginRegistry(registry);
|
||||
pinActivePluginChannelRegistry(registry);
|
||||
|
||||
bootstrapOutboundChannelPlugin({
|
||||
channel: "discord",
|
||||
cfg: discordConfig,
|
||||
});
|
||||
bootstrapOutboundChannelPlugin({
|
||||
channel: "discord",
|
||||
cfg: discordConfig,
|
||||
});
|
||||
|
||||
expect(loaderMocks.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { PluginChannelRegistration } from "../../plugins/registry-types.js"
|
||||
import {
|
||||
getActivePluginChannelRegistry,
|
||||
getActivePluginChannelRegistryVersion,
|
||||
getActivePluginRegistry,
|
||||
getActivePluginRegistryVersion,
|
||||
} from "../../plugins/runtime.js";
|
||||
import type { DeliverableMessageChannel } from "../../utils/message-channel.js";
|
||||
|
||||
@@ -22,6 +24,27 @@ function channelEntryCanSend(entry: PluginChannelRegistration | undefined): bool
|
||||
return Boolean(entry?.plugin?.outbound?.sendText ?? entry?.plugin?.message?.send?.text);
|
||||
}
|
||||
|
||||
function findChannelEntry(
|
||||
registry: ReturnType<typeof getActivePluginRegistry>,
|
||||
channel: DeliverableMessageChannel,
|
||||
): PluginChannelRegistration | undefined {
|
||||
return registry?.channels?.find((entry) => entry?.plugin?.id === channel);
|
||||
}
|
||||
|
||||
function canResolveSendCapableChannel(channel: DeliverableMessageChannel): boolean {
|
||||
const activeChannelRegistry = getActivePluginChannelRegistry();
|
||||
const channelEntry = findChannelEntry(activeChannelRegistry, channel);
|
||||
if (channelEntryCanSend(channelEntry)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
if (activeRegistry && activeRegistry !== activeChannelRegistry) {
|
||||
return channelEntryCanSend(findChannelEntry(activeRegistry, channel));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Loads runtime plugins on demand when a selected outbound channel has only a setup shell. */
|
||||
export function bootstrapOutboundChannelPlugin(params: {
|
||||
channel: DeliverableMessageChannel;
|
||||
@@ -32,15 +55,11 @@ export function bootstrapOutboundChannelPlugin(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeChannelRegistry = getActivePluginChannelRegistry();
|
||||
const activeChannelEntry = activeChannelRegistry?.channels?.find(
|
||||
(entry) => entry?.plugin?.id === params.channel,
|
||||
);
|
||||
if (channelEntryCanSend(activeChannelEntry)) {
|
||||
if (canResolveSendCapableChannel(params.channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attemptKey = `${getActivePluginChannelRegistryVersion()}:${params.channel}`;
|
||||
const attemptKey = `${getActivePluginChannelRegistryVersion()}:${getActivePluginRegistryVersion()}:${params.channel}`;
|
||||
if (bootstrapAttempts.has(attemptKey)) {
|
||||
return;
|
||||
}
|
||||
@@ -61,6 +80,9 @@ export function bootstrapOutboundChannelPlugin(params: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
});
|
||||
if (!canResolveSendCapableChannel(params.channel)) {
|
||||
bootstrapAttempts.delete(attemptKey);
|
||||
}
|
||||
} catch {
|
||||
bootstrapAttempts.delete(attemptKey);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const getChannelPluginMock = vi.hoisted(() => vi.fn());
|
||||
const applyPluginAutoEnableMock = vi.hoisted(() => vi.fn());
|
||||
const resolveRuntimePluginRegistryMock = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginRegistryMock = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginRegistryVersionMock = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginChannelRegistryMock = vi.hoisted(() => vi.fn());
|
||||
const getActivePluginChannelRegistryVersionMock = vi.hoisted(() => vi.fn());
|
||||
const normalizeMessageChannelMock = vi.hoisted(() => vi.fn());
|
||||
@@ -34,6 +35,8 @@ vi.mock("../../plugins/loader.js", () => ({
|
||||
|
||||
vi.mock("../../plugins/runtime.js", () => ({
|
||||
getActivePluginRegistry: (...args: unknown[]) => getActivePluginRegistryMock(...args),
|
||||
getActivePluginRegistryVersion: (...args: unknown[]) =>
|
||||
getActivePluginRegistryVersionMock(...args),
|
||||
getActivePluginChannelRegistry: (...args: unknown[]) =>
|
||||
getActivePluginChannelRegistryMock(...args),
|
||||
getActivePluginChannelRegistryVersion: (...args: unknown[]) =>
|
||||
@@ -75,6 +78,7 @@ describe("outbound channel resolution", () => {
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
resolveRuntimePluginRegistryMock.mockReset();
|
||||
getActivePluginRegistryMock.mockReset();
|
||||
getActivePluginRegistryVersionMock.mockReset();
|
||||
getActivePluginChannelRegistryMock.mockReset();
|
||||
getActivePluginChannelRegistryVersionMock.mockReset();
|
||||
normalizeMessageChannelMock.mockReset();
|
||||
@@ -87,6 +91,7 @@ describe("outbound channel resolution", () => {
|
||||
["alpha", "beta", "gamma"].includes(String(value)),
|
||||
);
|
||||
getActivePluginRegistryMock.mockReturnValue({ channels: [] });
|
||||
getActivePluginRegistryVersionMock.mockReturnValue(1);
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({ channels: [] });
|
||||
getActivePluginChannelRegistryVersionMock.mockReturnValue(1);
|
||||
applyPluginAutoEnableMock.mockReturnValue({
|
||||
@@ -110,7 +115,7 @@ describe("outbound channel resolution", () => {
|
||||
});
|
||||
|
||||
it("returns the already-registered plugin without bootstrapping", async () => {
|
||||
const plugin = { id: "alpha" };
|
||||
const plugin = { id: "alpha", outbound: { sendText: vi.fn() } };
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(plugin);
|
||||
const channelResolution = await importChannelResolution("existing-plugin");
|
||||
|
||||
@@ -124,7 +129,7 @@ describe("outbound channel resolution", () => {
|
||||
});
|
||||
|
||||
it("returns a bundled plugin without bootstrapping", async () => {
|
||||
const plugin = { id: "alpha" };
|
||||
const plugin = { id: "alpha", outbound: { sendText: vi.fn() } };
|
||||
getLoadedChannelPluginMock.mockReturnValue(undefined);
|
||||
getChannelPluginMock.mockReturnValue(plugin);
|
||||
const channelResolution = await importChannelResolution("bundled-plugin");
|
||||
@@ -158,8 +163,52 @@ describe("outbound channel resolution", () => {
|
||||
).toBe(plugin);
|
||||
});
|
||||
|
||||
it("resolves message adapters from the pinned channel registry after active registry replacement", async () => {
|
||||
const message = { send: { text: vi.fn() } };
|
||||
const plugin = { id: "alpha", message };
|
||||
getLoadedChannelPluginMock.mockReturnValue(undefined);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin }],
|
||||
});
|
||||
getActivePluginRegistryMock.mockReturnValue({ channels: [] });
|
||||
const channelResolution = await importChannelResolution("pinned-message-registry");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelMessageAdapter({
|
||||
channel: "alpha",
|
||||
cfg: {} as never,
|
||||
}),
|
||||
).toBe(message);
|
||||
});
|
||||
|
||||
it("skips metadata-only loaded message shells for active send-capable message adapters", async () => {
|
||||
const setupMessage = { receive: { defaultAckPolicy: "manual" } };
|
||||
const runtimeMessage = { send: { text: vi.fn() } };
|
||||
const setupPlugin = { id: "alpha", message: setupMessage };
|
||||
const runtimePlugin = { id: "alpha", message: runtimeMessage };
|
||||
getLoadedChannelPluginMock.mockReturnValue(setupPlugin);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: setupPlugin }],
|
||||
});
|
||||
getActivePluginRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: runtimePlugin }],
|
||||
});
|
||||
const channelResolution = await importChannelResolution("message-metadata-shell");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelMessageAdapter({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
allowBootstrap: true,
|
||||
}),
|
||||
).toBe(runtimeMessage);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("bootstraps configured channel plugins when the active registry is missing the target", async () => {
|
||||
const plugin = { id: "alpha" };
|
||||
const plugin = { id: "alpha", outbound: { sendText: vi.fn() } };
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin);
|
||||
const channelResolution = await importChannelResolution("bootstrap-missing-target");
|
||||
|
||||
@@ -182,6 +231,161 @@ describe("outbound channel resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("bootstraps instead of returning a pinned setup shell as the outbound plugin", async () => {
|
||||
const setupPlugin = { id: "alpha" };
|
||||
const runtimePlugin = { id: "alpha", outbound: { sendText: vi.fn() } };
|
||||
getLoadedChannelPluginMock.mockReturnValueOnce(setupPlugin).mockReturnValueOnce(runtimePlugin);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
getActivePluginRegistryMock.mockReturnValue({ channels: [] });
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: setupPlugin }],
|
||||
});
|
||||
const channelResolution = await importChannelResolution("bootstrap-setup-shell");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
allowBootstrap: true,
|
||||
}),
|
||||
).toBe(runtimePlugin);
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not return a setup shell when bootstrap does not produce a runtime plugin", async () => {
|
||||
const setupPlugin = { id: "alpha" };
|
||||
getLoadedChannelPluginMock.mockReturnValue(setupPlugin);
|
||||
getChannelPluginMock.mockReturnValue(setupPlugin);
|
||||
getActivePluginRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: setupPlugin }],
|
||||
});
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: setupPlugin }],
|
||||
});
|
||||
const channelResolution = await importChannelResolution("bootstrap-still-setup-shell");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
allowBootstrap: true,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not treat an actions-only plugin as send-capable after bootstrap", async () => {
|
||||
const actionsOnlyPlugin = { id: "alpha", actions: { handleAction: vi.fn() } };
|
||||
getLoadedChannelPluginMock.mockReturnValue(actionsOnlyPlugin);
|
||||
getChannelPluginMock.mockReturnValue(actionsOnlyPlugin);
|
||||
getActivePluginRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: actionsOnlyPlugin }],
|
||||
});
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: actionsOnlyPlugin }],
|
||||
});
|
||||
const channelResolution = await importChannelResolution("actions-only-plugin");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
allowBootstrap: true,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("prefers an active runtime plugin over a loaded setup shell", async () => {
|
||||
const setupPlugin = { id: "alpha" };
|
||||
const runtimePlugin = { id: "alpha", outbound: { sendText: vi.fn() } };
|
||||
getLoadedChannelPluginMock.mockReturnValue(setupPlugin);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
getActivePluginRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: runtimePlugin }],
|
||||
});
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: setupPlugin }],
|
||||
});
|
||||
const channelResolution = await importChannelResolution("active-runtime-over-setup");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
allowBootstrap: true,
|
||||
}),
|
||||
).toBe(runtimePlugin);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves outbound plugins from the selected runtime channel registry", async () => {
|
||||
const setupPlugin = { id: "alpha" };
|
||||
const runtimePlugin = { id: "alpha", outbound: { sendText: vi.fn() } };
|
||||
getLoadedChannelPluginMock.mockReturnValue(undefined);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
getActivePluginRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: setupPlugin }],
|
||||
});
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: runtimePlugin }],
|
||||
});
|
||||
const channelResolution = await importChannelResolution("selected-runtime-registry");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
allowBootstrap: true,
|
||||
}),
|
||||
).toBe(runtimePlugin);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves runtime outbound adapters that do not send text directly", async () => {
|
||||
const setupPlugin = { id: "alpha" };
|
||||
const runtimePlugin = { id: "alpha", outbound: { deliveryMode: "gateway" } };
|
||||
getLoadedChannelPluginMock.mockReturnValue(setupPlugin);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
getActivePluginRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: runtimePlugin }],
|
||||
});
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: setupPlugin }],
|
||||
});
|
||||
const channelResolution = await importChannelResolution("runtime-outbound-adapter");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelPlugin({
|
||||
channel: "alpha",
|
||||
cfg: { channels: {} } as never,
|
||||
allowBootstrap: true,
|
||||
}),
|
||||
).toBe(runtimePlugin);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps setup shells visible for read-only channel lookup", async () => {
|
||||
const setupPlugin = { id: "alpha" };
|
||||
const runtimePlugin = { id: "alpha", outbound: { sendText: vi.fn() } };
|
||||
getLoadedChannelPluginMock.mockReturnValue(setupPlugin);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
getActivePluginRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: runtimePlugin }],
|
||||
});
|
||||
getActivePluginChannelRegistryMock.mockReturnValue({
|
||||
channels: [{ plugin: setupPlugin }],
|
||||
});
|
||||
const channelResolution = await importChannelResolution("read-setup-shell");
|
||||
|
||||
expect(
|
||||
channelResolution.resolveOutboundChannelPluginForRead({
|
||||
channel: "alpha",
|
||||
}),
|
||||
).toBe(setupPlugin);
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attempts activation when the active registry has other channels but not the requested one", async () => {
|
||||
getLoadedChannelPluginMock.mockReturnValue(undefined);
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
@@ -203,7 +407,7 @@ describe("outbound channel resolution", () => {
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not retry registry loads after a missing outbound plugin", async () => {
|
||||
it("retries registry loads after bootstrap does not make the channel send-capable", async () => {
|
||||
getChannelPluginMock.mockReturnValue(undefined);
|
||||
const channelResolution = await importChannelResolution("bootstrap-retry");
|
||||
|
||||
@@ -220,7 +424,7 @@ describe("outbound channel resolution", () => {
|
||||
cfg: { channels: {} } as never,
|
||||
allowBootstrap: true,
|
||||
});
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1);
|
||||
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("allows another activation attempt when the pinned channel registry version changes", async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
ChannelThreadingAdapter,
|
||||
} from "../../channels/plugins/types.public.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { getActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { getActivePluginChannelRegistry, getActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import {
|
||||
isDeliverableMessageChannel,
|
||||
normalizeMessageChannel,
|
||||
@@ -111,12 +111,14 @@ function maybeBootstrapChannelPlugin(params: {
|
||||
bootstrapOutboundChannelPlugin(params);
|
||||
}
|
||||
|
||||
function resolveDirectFromActiveRegistry(channel: string): ChannelPlugin | undefined {
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
if (!activeRegistry) {
|
||||
function resolveDirectFromRegistry(
|
||||
registry: ReturnType<typeof getActivePluginRegistry>,
|
||||
channel: string,
|
||||
): ChannelPlugin | undefined {
|
||||
if (!registry) {
|
||||
return undefined;
|
||||
}
|
||||
for (const entry of activeRegistry.channels) {
|
||||
for (const entry of registry.channels) {
|
||||
const plugin = entry?.plugin;
|
||||
if (plugin?.id === channel) {
|
||||
return plugin;
|
||||
@@ -125,6 +127,81 @@ function resolveDirectFromActiveRegistry(channel: string): ChannelPlugin | undef
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function messageAdapterCanSendText(
|
||||
message: ChannelMessageAdapterShape | undefined,
|
||||
): message is ChannelMessageAdapterShape {
|
||||
return typeof message?.send?.text === "function";
|
||||
}
|
||||
|
||||
function resolveSendCapableMessageAdapter(
|
||||
plugin: ChannelPlugin | undefined,
|
||||
): ChannelMessageAdapterShape | undefined {
|
||||
const message = plugin?.message;
|
||||
return messageAdapterCanSendText(message) ? message : undefined;
|
||||
}
|
||||
|
||||
function channelPluginHasRuntimeOutboundSurface(plugin: ChannelPlugin | undefined): boolean {
|
||||
return Boolean(plugin?.outbound ?? resolveSendCapableMessageAdapter(plugin));
|
||||
}
|
||||
|
||||
function resolveRuntimeOutboundPlugin(plugin: ChannelPlugin): ChannelPlugin | undefined {
|
||||
return channelPluginHasRuntimeOutboundSurface(plugin) ? plugin : undefined;
|
||||
}
|
||||
|
||||
function resolveRuntimeOutboundPluginCandidate(params: {
|
||||
loaded?: ChannelPlugin;
|
||||
runtime?: ChannelPlugin;
|
||||
setupFallback?: ChannelPlugin;
|
||||
bundled?: ChannelPlugin;
|
||||
allowSetupShell?: boolean;
|
||||
}): ChannelPlugin | undefined {
|
||||
if (channelPluginHasRuntimeOutboundSurface(params.loaded)) {
|
||||
return params.loaded;
|
||||
}
|
||||
if (params.runtime) {
|
||||
return params.runtime;
|
||||
}
|
||||
if (channelPluginHasRuntimeOutboundSurface(params.bundled)) {
|
||||
return params.bundled;
|
||||
}
|
||||
if (params.allowSetupShell) {
|
||||
return params.loaded ?? params.setupFallback ?? params.bundled;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveValueFromRuntimeRegistries<TValue>(
|
||||
channel: string,
|
||||
resolveValue: (plugin: ChannelPlugin) => TValue | undefined,
|
||||
): TValue | undefined {
|
||||
const channelRegistry = getActivePluginChannelRegistry();
|
||||
const channelPlugin = resolveDirectFromRegistry(channelRegistry, channel);
|
||||
if (channelPlugin) {
|
||||
const value = resolveValue(channelPlugin);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
const activeRegistry = getActivePluginRegistry();
|
||||
if (activeRegistry && activeRegistry !== channelRegistry) {
|
||||
const activePlugin = resolveDirectFromRegistry(activeRegistry, channel);
|
||||
if (activePlugin) {
|
||||
return resolveValue(activePlugin);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveDirectFromRuntimeRegistries(channel: string): ChannelPlugin | undefined {
|
||||
return resolveValueFromRuntimeRegistries(channel, (plugin) => plugin);
|
||||
}
|
||||
|
||||
function resolveRuntimeOutboundPluginFromRuntimeRegistries(
|
||||
channel: string,
|
||||
): ChannelPlugin | undefined {
|
||||
return resolveValueFromRuntimeRegistries(channel, resolveRuntimeOutboundPlugin);
|
||||
}
|
||||
|
||||
function toOutboundChannelRuntime(plugin: ChannelPlugin): OutboundChannelRuntime {
|
||||
return {
|
||||
id: plugin.id,
|
||||
@@ -191,17 +268,18 @@ export function resolveOutboundChannelPlugin(params: {
|
||||
const resolveLoaded = () => getLoadedChannelPlugin(normalized);
|
||||
const resolve = () => getChannelPlugin(normalized);
|
||||
const current = resolveLoaded();
|
||||
if (current) {
|
||||
return current;
|
||||
}
|
||||
const directCurrent = resolveDirectFromActiveRegistry(normalized);
|
||||
if (directCurrent) {
|
||||
return directCurrent;
|
||||
}
|
||||
|
||||
const runtimeCurrent = resolveRuntimeOutboundPluginFromRuntimeRegistries(normalized);
|
||||
const setupFallback = resolveDirectFromRuntimeRegistries(normalized);
|
||||
const bundledCurrent = resolve();
|
||||
if (bundledCurrent) {
|
||||
return bundledCurrent;
|
||||
const candidate = resolveRuntimeOutboundPluginCandidate({
|
||||
loaded: current,
|
||||
runtime: runtimeCurrent,
|
||||
setupFallback,
|
||||
bundled: bundledCurrent,
|
||||
allowSetupShell: params.allowBootstrap !== true,
|
||||
});
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (params.allowBootstrap !== true) {
|
||||
@@ -209,7 +287,12 @@ export function resolveOutboundChannelPlugin(params: {
|
||||
}
|
||||
|
||||
maybeBootstrapChannelPlugin({ channel: normalized, cfg: params.cfg });
|
||||
return resolveLoaded() ?? resolveDirectFromActiveRegistry(normalized) ?? resolve();
|
||||
return resolveRuntimeOutboundPluginCandidate({
|
||||
loaded: resolveLoaded(),
|
||||
runtime: resolveRuntimeOutboundPluginFromRuntimeRegistries(normalized),
|
||||
setupFallback: resolveDirectFromRuntimeRegistries(normalized),
|
||||
bundled: resolve(),
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolves the message adapter for a deliverable outbound channel. */
|
||||
@@ -218,7 +301,23 @@ export function resolveOutboundChannelMessageAdapter(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
allowBootstrap?: boolean;
|
||||
}): ChannelMessageAdapterShape | undefined {
|
||||
return resolveOutboundChannelPlugin(params)?.message;
|
||||
const normalized = normalizeDeliverableOutboundChannel(params.channel);
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
const current =
|
||||
resolveSendCapableMessageAdapter(getLoadedChannelPlugin(normalized)) ??
|
||||
resolveValueFromRuntimeRegistries(normalized, resolveSendCapableMessageAdapter) ??
|
||||
resolveSendCapableMessageAdapter(getChannelPlugin(normalized));
|
||||
if (current || params.allowBootstrap !== true) {
|
||||
return current;
|
||||
}
|
||||
maybeBootstrapChannelPlugin({ channel: normalized, cfg: params.cfg });
|
||||
return (
|
||||
resolveSendCapableMessageAdapter(getLoadedChannelPlugin(normalized)) ??
|
||||
resolveValueFromRuntimeRegistries(normalized, resolveSendCapableMessageAdapter) ??
|
||||
resolveSendCapableMessageAdapter(getChannelPlugin(normalized))
|
||||
);
|
||||
}
|
||||
|
||||
/** Resolves a channel plugin for read-only metadata paths. */
|
||||
@@ -235,7 +334,7 @@ export function resolveOutboundChannelPluginForRead(params: {
|
||||
if (current) {
|
||||
return current;
|
||||
}
|
||||
const directCurrent = resolveDirectFromActiveRegistry(normalized);
|
||||
const directCurrent = resolveDirectFromRuntimeRegistries(normalized);
|
||||
if (directCurrent) {
|
||||
return directCurrent;
|
||||
}
|
||||
@@ -244,7 +343,7 @@ export function resolveOutboundChannelPluginForRead(params: {
|
||||
maybeBootstrapChannelPlugin({ channel: deliverable, cfg: params.cfg });
|
||||
return (
|
||||
getLoadedChannelPlugin(deliverable) ??
|
||||
resolveDirectFromActiveRegistry(deliverable) ??
|
||||
resolveDirectFromRuntimeRegistries(deliverable) ??
|
||||
getChannelPlugin(deliverable)
|
||||
);
|
||||
}
|
||||
@@ -270,6 +369,6 @@ export function resolveLoadedOutboundChannelPluginForRead(params: {
|
||||
}
|
||||
return (
|
||||
getLoadedChannelPlugin(normalized as Parameters<typeof getLoadedChannelPlugin>[0]) ??
|
||||
resolveDirectFromActiveRegistry(normalized)
|
||||
resolveDirectFromRuntimeRegistries(normalized)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user