diff --git a/src/channels/plugins/registry-read.ts b/src/channels/plugins/registry-read.ts new file mode 100644 index 00000000000..20587895f7c --- /dev/null +++ b/src/channels/plugins/registry-read.ts @@ -0,0 +1,25 @@ +import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { getBundledChannelPlugin } from "./bundled.js"; +import { getLoadedChannelPluginById, listLoadedChannelPlugins } from "./registry-loaded.js"; +import type { ChannelPlugin } from "./types.plugin.js"; +import type { ChannelId } from "./types.public.js"; + +export function listChannelPluginsForRead(): ChannelPlugin[] { + return listLoadedChannelPlugins() as ChannelPlugin[]; +} + +export function getLoadedChannelPluginForRead(id: ChannelId): ChannelPlugin | undefined { + const resolvedId = normalizeOptionalString(id) ?? ""; + if (!resolvedId) { + return undefined; + } + return getLoadedChannelPluginById(resolvedId) as ChannelPlugin | undefined; +} + +export function getChannelPluginForRead(id: ChannelId): ChannelPlugin | undefined { + const resolvedId = normalizeOptionalString(id) ?? ""; + if (!resolvedId) { + return undefined; + } + return getLoadedChannelPluginForRead(resolvedId) ?? getBundledChannelPlugin(resolvedId); +} diff --git a/src/cron/isolated-agent/delivery-target.ts b/src/cron/isolated-agent/delivery-target.ts index 52880efff6d..d2dd454d76d 100644 --- a/src/cron/isolated-agent/delivery-target.ts +++ b/src/cron/isolated-agent/delivery-target.ts @@ -1,4 +1,4 @@ -import { getLoadedChannelPlugin } from "../../channels/plugins/index.js"; +import { getLoadedChannelPluginForRead } from "../../channels/plugins/registry-read.js"; import type { ChannelId } from "../../channels/plugins/types.public.js"; import { resolveAgentMainSessionKey } from "../../config/sessions/main-session.js"; import { resolveStorePath } from "../../config/sessions/paths.js"; @@ -177,7 +177,7 @@ export async function resolveDeliveryTarget( }; } - const channelPlugin = getLoadedChannelPlugin(channel); + const channelPlugin = getLoadedChannelPluginForRead(channel); const resolvedAccountId = normalizeAccountId(accountId); const configuredAllowFromRaw = channelPlugin?.config.resolveAllowFrom?.({ cfg, diff --git a/src/infra/outbound/target-normalization.test.ts b/src/infra/outbound/target-normalization.test.ts index d4155fc3f9d..a8d7c80dfc2 100644 --- a/src/infra/outbound/target-normalization.test.ts +++ b/src/infra/outbound/target-normalization.test.ts @@ -1,7 +1,6 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; -const normalizeChannelIdMock = vi.hoisted(() => vi.fn()); const getChannelPluginMock = vi.hoisted(() => vi.fn()); const getActivePluginChannelRegistryVersionMock = vi.hoisted(() => vi.fn()); @@ -15,12 +14,8 @@ let resolveNormalizedTargetInput: TargetNormalizationModule["resolveNormalizedTa let normalizeTargetForProvider: TargetNormalizationModule["normalizeTargetForProvider"]; let resetTargetNormalizerCacheForTests: TargetNormalizationModule["__testing"]["resetTargetNormalizerCacheForTests"]; -vi.mock("../../channels/registry.js", () => ({ - normalizeAnyChannelId: (...args: unknown[]) => normalizeChannelIdMock(...args), -})); - -vi.mock("../../channels/plugins/index.js", () => ({ - getChannelPlugin: (...args: unknown[]) => getChannelPluginMock(...args), +vi.mock("../../channels/plugins/registry-read.js", () => ({ + getChannelPluginForRead: (...args: unknown[]) => getChannelPluginMock(...args), })); vi.mock("../../plugins/runtime.js", () => ({ @@ -43,7 +38,6 @@ beforeAll(async () => { }); beforeEach(() => { - normalizeChannelIdMock.mockReset(); getChannelPluginMock.mockReset(); getActivePluginChannelRegistryVersionMock.mockReset(); resetTargetNormalizerCacheForTests(); @@ -64,14 +58,13 @@ describe("normalizeTargetForProvider", () => { { provider: "unknown", setup: () => { - normalizeChannelIdMock.mockReturnValueOnce(null); + getChannelPluginMock.mockReturnValueOnce(undefined); }, expected: "raw-id", }, { provider: "telegram", setup: () => { - normalizeChannelIdMock.mockReturnValueOnce("telegram"); getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(1); getChannelPluginMock.mockReturnValueOnce(undefined); }, @@ -88,7 +81,6 @@ describe("normalizeTargetForProvider", () => { it("uses the cached target normalizer until the plugin registry version changes", () => { const firstNormalizer = vi.fn((raw: string) => raw.trim().toUpperCase()); const secondNormalizer = vi.fn((raw: string) => `next:${raw.trim()}`); - normalizeChannelIdMock.mockReturnValue("telegram"); getActivePluginChannelRegistryVersionMock .mockReturnValueOnce(10) .mockReturnValueOnce(10) @@ -111,7 +103,6 @@ describe("normalizeTargetForProvider", () => { }); it("returns undefined when the provider normalizer resolves to an empty value", () => { - normalizeChannelIdMock.mockReturnValueOnce("telegram"); getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(20); getChannelPluginMock.mockReturnValueOnce({ messaging: { @@ -129,7 +120,6 @@ describe("resolveNormalizedTargetInput", () => { }); it("returns raw and normalized values", () => { - normalizeChannelIdMock.mockReturnValueOnce("telegram"); getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(1); getChannelPluginMock.mockReturnValueOnce({ messaging: { @@ -198,7 +188,6 @@ describe("maybeResolvePluginMessagingTarget", () => { }); it("invokes the plugin resolver with normalized input and defaults source", async () => { - normalizeChannelIdMock.mockReturnValueOnce("slack"); getActivePluginChannelRegistryVersionMock.mockReturnValueOnce(1); const resolveTarget = vi.fn().mockResolvedValue({ to: "channel:C123ABC", diff --git a/src/infra/outbound/target-normalization.ts b/src/infra/outbound/target-normalization.ts index 969775cce7e..39d99e92ce0 100644 --- a/src/infra/outbound/target-normalization.ts +++ b/src/infra/outbound/target-normalization.ts @@ -1,9 +1,11 @@ -import { getChannelPlugin } from "../../channels/plugins/index.js"; +import { getChannelPluginForRead } from "../../channels/plugins/registry-read.js"; import type { ChannelDirectoryEntryKind, ChannelId } from "../../channels/plugins/types.public.js"; -import { normalizeAnyChannelId } from "../../channels/registry.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { getActivePluginChannelRegistryVersion } from "../../plugins/runtime.js"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { + normalizeOptionalLowercaseString, + normalizeOptionalString, +} from "../../shared/string-coerce.js"; export function normalizeChannelTargetInput(raw: string): string { return raw.trim(); @@ -28,10 +30,10 @@ export const __testing = { function resolveTargetNormalizer(channelId: ChannelId): TargetNormalizer { const version = getActivePluginChannelRegistryVersion(); const cached = targetNormalizerCacheByChannelId.get(channelId); - if (cached?.version === version) { + if (cached && cached.version === version) { return cached.normalizer; } - const plugin = getChannelPlugin(channelId); + const plugin = getChannelPluginForRead(channelId); const normalizer = plugin?.messaging?.normalizeTarget; targetNormalizerCacheByChannelId.set(channelId, { version, @@ -48,7 +50,7 @@ export function normalizeTargetForProvider(provider: string, raw?: string): stri if (!fallback) { return undefined; } - const providerId = normalizeAnyChannelId(provider); + const providerId = normalizeOptionalLowercaseString(provider); const normalizer = providerId ? resolveTargetNormalizer(providerId) : undefined; return normalizeOptionalString(normalizer?.(raw) ?? fallback); } @@ -83,7 +85,7 @@ export function looksLikeTargetId(params: { }): boolean { const normalizedInput = params.normalized ?? normalizeTargetForProvider(params.channel, params.raw); - const lookup = getChannelPlugin(params.channel)?.messaging?.targetResolver?.looksLikeId; + const lookup = getChannelPluginForRead(params.channel)?.messaging?.targetResolver?.looksLikeId; if (lookup) { return lookup(params.raw, normalizedInput ?? params.raw); } @@ -114,7 +116,7 @@ export async function maybeResolvePluginMessagingTarget(params: { if (!normalizedInput) { return undefined; } - const resolver = getChannelPlugin(params.channel)?.messaging?.targetResolver; + const resolver = getChannelPluginForRead(params.channel)?.messaging?.targetResolver; if (!resolver?.resolveTarget) { return undefined; } @@ -147,7 +149,7 @@ export async function maybeResolvePluginMessagingTarget(params: { } export function buildTargetResolverSignature(channel: ChannelId): string { - const plugin = getChannelPlugin(channel); + const plugin = getChannelPluginForRead(channel); const resolver = plugin?.messaging?.targetResolver; const hint = resolver?.hint ?? ""; const looksLike = resolver?.looksLikeId; diff --git a/src/infra/outbound/targets-loaded.test.ts b/src/infra/outbound/targets-loaded.test.ts index c0691fcae84..5f639a64cc6 100644 --- a/src/infra/outbound/targets-loaded.test.ts +++ b/src/infra/outbound/targets-loaded.test.ts @@ -6,8 +6,8 @@ const mocks = vi.hoisted(() => ({ getLoadedChannelPlugin: vi.fn(), })); -vi.mock("../../channels/plugins/index.js", () => ({ - getLoadedChannelPlugin: mocks.getLoadedChannelPlugin, +vi.mock("../../channels/plugins/registry-read.js", () => ({ + getLoadedChannelPluginForRead: mocks.getLoadedChannelPlugin, })); describe("tryResolveLoadedOutboundTarget", () => { diff --git a/src/infra/outbound/targets-loaded.ts b/src/infra/outbound/targets-loaded.ts index b5cabd71efe..8769215b726 100644 --- a/src/infra/outbound/targets-loaded.ts +++ b/src/infra/outbound/targets-loaded.ts @@ -1,4 +1,4 @@ -import { getLoadedChannelPlugin } from "../../channels/plugins/index.js"; +import { getLoadedChannelPluginForRead } from "../../channels/plugins/registry-read.js"; import type { ChannelPlugin } from "../../channels/plugins/types.plugin.js"; import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.public.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -18,7 +18,7 @@ function resolveLoadedOutboundChannelPlugin(channel: string): ChannelPlugin | un return undefined; } - return getLoadedChannelPlugin(normalized); + return getLoadedChannelPluginForRead(normalized); } export function tryResolveLoadedOutboundTarget(params: {