perf(outbound): use read-only channel registry seam

This commit is contained in:
Vincent Koc
2026-04-13 17:05:45 +01:00
parent 0369bd75c1
commit ae3d731810
6 changed files with 45 additions and 29 deletions

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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",

View File

@@ -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;

View File

@@ -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", () => {

View File

@@ -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: {