fix(testing): speed channel contract loading

This commit is contained in:
Vincent Koc
2026-06-03 21:13:59 +02:00
parent f0237caf27
commit a0717ef61c
9 changed files with 261 additions and 10 deletions

View File

@@ -1,4 +1,17 @@
export {
import {
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
} from "./src/directory-config.js";
export {
listDiscordDirectoryGroupsFromConfig,
listDiscordDirectoryPeersFromConfig,
};
export const discordDirectoryContractPlugin = {
id: "discord",
directory: {
listPeers: listDiscordDirectoryPeersFromConfig,
listGroups: listDiscordDirectoryGroupsFromConfig,
},
};

View File

@@ -0,0 +1,6 @@
import { googlechatDirectoryAdapter } from "./src/channel.adapters.js";
export const googlechatDirectoryContractPlugin = {
id: "googlechat",
directory: googlechatDirectoryAdapter,
};

View File

@@ -0,0 +1,45 @@
import type { ChannelDirectoryAdapter } from "openclaw/plugin-sdk/channel-contract";
import { listDirectoryEntriesFromSources } from "openclaw/plugin-sdk/directory-runtime";
import { normalizeMSTeamsMessagingTarget } from "./src/resolve-allowlist.js";
import { resolveMSTeamsCredentials } from "./src/token.js";
const msteamsDirectoryContractAdapter: ChannelDirectoryAdapter = {
self: async ({ cfg }) => {
const creds = resolveMSTeamsCredentials(cfg.channels?.msteams);
return creds ? { kind: "user" as const, id: creds.appId, name: creds.appId } : null;
},
listPeers: async ({ cfg, query, limit }) =>
listDirectoryEntriesFromSources({
kind: "user",
sources: [
cfg.channels?.msteams?.allowFrom ?? [],
Object.keys(cfg.channels?.msteams?.dms ?? {}),
],
query,
limit,
normalizeId: (raw) => {
const normalized = normalizeMSTeamsMessagingTarget(raw) ?? raw;
const lowered = normalized.toLowerCase();
return lowered.startsWith("user:") || lowered.startsWith("conversation:")
? normalized
: `user:${normalized}`;
},
}),
listGroups: async ({ cfg, query, limit }) =>
listDirectoryEntriesFromSources({
kind: "group",
sources: [
Object.values(cfg.channels?.msteams?.teams ?? {}).flatMap((team) =>
Object.keys(team.channels ?? {}),
),
],
query,
limit,
normalizeId: (raw) => `conversation:${raw.replace(/^conversation:/i, "").trim()}`,
}),
};
export const msteamsDirectoryContractPlugin = {
id: "msteams",
directory: msteamsDirectoryContractAdapter,
};

View File

@@ -1,4 +1,17 @@
export {
import {
listWhatsAppDirectoryGroupsFromConfig,
listWhatsAppDirectoryPeersFromConfig,
} from "./src/directory-config.js";
export {
listWhatsAppDirectoryGroupsFromConfig,
listWhatsAppDirectoryPeersFromConfig,
};
export const whatsappDirectoryContractPlugin = {
id: "whatsapp",
directory: {
listPeers: listWhatsAppDirectoryPeersFromConfig,
listGroups: listWhatsAppDirectoryGroupsFromConfig,
},
};

View File

@@ -10,7 +10,7 @@ import {
createAllowlistProviderGroupPolicyWarningCollector,
} from "openclaw/plugin-sdk/channel-policy";
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
import { createChannelPluginBase, getChatChannelMeta } from "openclaw/plugin-sdk/core";
import { createChannelPluginBase } from "openclaw/plugin-sdk/core";
import {
createDelegatedSetupWizardProxy,
type ChannelSetupWizard,
@@ -150,7 +150,13 @@ export function createWhatsAppPluginBase(params: {
const base = createChannelPluginBase({
id: WHATSAPP_CHANNEL,
meta: {
...getChatChannelMeta(WHATSAPP_CHANNEL),
label: "WhatsApp",
selectionLabel: "WhatsApp (QR link)",
detailLabel: "WhatsApp Web",
docsPath: "/channels/whatsapp",
docsLabel: "whatsapp",
blurb: "works with your own number; recommend a separate phone + eSIM.",
systemImage: "message",
showConfigured: false,
quickstartAllowFrom: true,
forceAccountBinding: true,

View File

@@ -10,9 +10,22 @@ import type { ChannelId } from "../../channel-id.types.js";
import type { ChannelPlugin } from "../../types.js";
type ChannelPluginApiModule = Record<string, unknown>;
type ChannelDirectoryContractModule = Record<string, unknown>;
const channelPluginCache = new Map<ChannelId, ChannelPlugin | null>();
const channelPluginPromiseCache = new Map<ChannelId, Promise<ChannelPlugin | null>>();
const channelDirectoryPluginCache = new Map<ChannelId, Pick<ChannelPlugin, "id" | "directory"> | null>();
const channelDirectoryPluginPromiseCache = new Map<
ChannelId,
Promise<Pick<ChannelPlugin, "id" | "directory"> | null>
>();
class MissingBundledDirectoryContractArtifactError extends Error {
constructor(id: ChannelId) {
super(`Missing bundled directory contract artifact for ${id}`);
this.name = "MissingBundledDirectoryContractArtifactError";
}
}
function isChannelPlugin(value: unknown): value is ChannelPlugin {
return (
@@ -24,6 +37,38 @@ function isChannelPlugin(value: unknown): value is ChannelPlugin {
);
}
function isChannelDirectoryContractPlugin(
value: unknown,
): value is Pick<ChannelPlugin, "id" | "directory"> {
return (
Boolean(value) &&
typeof value === "object" &&
typeof (value as Partial<ChannelPlugin>).id === "string" &&
Boolean((value as Partial<ChannelPlugin>).directory)
);
}
function findChannelPlugin(module: ChannelPluginApiModule): ChannelPlugin | null {
return Object.values(module).find(isChannelPlugin) ?? null;
}
function findChannelDirectoryContractPlugin(
module: ChannelDirectoryContractModule,
): Pick<ChannelPlugin, "id" | "directory"> | null {
return Object.values(module).find(isChannelDirectoryContractPlugin) ?? null;
}
function hasBasePluginMetadata(plugin: ChannelPlugin | null, id: ChannelId): boolean {
return (
plugin?.id === id &&
plugin.meta?.id === id &&
typeof plugin.meta.label === "string" &&
typeof plugin.meta.selectionLabel === "string" &&
typeof plugin.meta.docsPath === "string" &&
typeof plugin.meta.blurb === "string"
);
}
function isBuiltArtifactMissingDependency(error: unknown): boolean {
const record = error as
| {
@@ -101,6 +146,68 @@ async function importBundledChannelPluginSourceSurface(id: ChannelId) {
return (await import(pathToFileURL(sourcePath).href)) as ChannelPluginApiModule;
}
function resolveSourceArtifactPath(artifactPath: string): string {
if (artifactPath.endsWith(".js") && fs.existsSync(`${artifactPath.slice(0, -3)}.ts`)) {
return `${artifactPath.slice(0, -3)}.ts`;
}
return artifactPath;
}
async function importBundledChannelDirectoryContractSourceSurface(
id: ChannelId,
): Promise<ChannelDirectoryContractModule> {
const artifactPath = resolveBundledPluginPublicModulePath({
pluginId: id,
artifactBasename: "directory-contract-api.js",
});
const sourcePath = resolveSourceArtifactPath(artifactPath);
if (!fs.existsSync(sourcePath)) {
throw new MissingBundledDirectoryContractArtifactError(id);
}
return (await import(pathToFileURL(sourcePath).href)) as ChannelDirectoryContractModule;
}
function isMissingBundledDirectoryContractArtifact(error: unknown, id: ChannelId): boolean {
return (
error instanceof Error &&
error.message === `Unable to resolve bundled plugin public surface ${id}/directory-contract-api.js`
);
}
async function loadBundledChannelDirectoryContractSurface(
id: ChannelId,
): Promise<ChannelDirectoryContractModule> {
return await loadBundledPluginPublicSurface<ChannelDirectoryContractModule>({
pluginId: id,
artifactBasename: "directory-contract-api.js",
}).catch((error: unknown) => {
if (isMissingBundledDirectoryContractArtifact(error, id)) {
throw new MissingBundledDirectoryContractArtifactError(id);
}
if (!isBuiltArtifactMissingDependency(error) || !canFallbackToPackageSource()) {
throw error;
}
return importBundledChannelDirectoryContractSourceSurface(id);
});
}
async function resolveBundledChannelPluginFromSurface(
id: ChannelId,
loaded: ChannelPluginApiModule,
): Promise<ChannelPlugin | null> {
const plugin = findChannelPlugin(loaded);
if (!plugin) {
return plugin;
}
if (hasBasePluginMetadata(plugin, id)) {
return plugin;
}
const sourceLoaded = await importBundledChannelPluginSourceSurface(id);
const sourcePlugin = findChannelPlugin(sourceLoaded);
return sourcePlugin ?? plugin;
}
export function listBundledChannelPluginIds(): readonly ChannelId[] {
return listCatalogBundledChannelPluginIds() as ChannelId[];
}
@@ -127,8 +234,8 @@ export async function getBundledChannelPluginAsync(
}
return importBundledChannelPluginSourceSurface(id);
})
.then((loaded) => {
const plugin = Object.values(loaded).find(isChannelPlugin) ?? null;
.then(async (loaded) => {
const plugin = await resolveBundledChannelPluginFromSurface(id, loaded);
channelPluginCache.set(id, plugin);
return plugin;
})
@@ -138,3 +245,40 @@ export async function getBundledChannelPluginAsync(
channelPluginPromiseCache.set(id, loading);
return (await loading) ?? undefined;
}
export async function getBundledChannelDirectoryPluginAsync(
id: ChannelId,
): Promise<Pick<ChannelPlugin, "id" | "directory"> | undefined> {
if (channelDirectoryPluginCache.has(id)) {
return channelDirectoryPluginCache.get(id) ?? undefined;
}
const cachedPromise = channelDirectoryPluginPromiseCache.get(id);
if (cachedPromise) {
return (await cachedPromise) ?? undefined;
}
const loading = loadBundledChannelDirectoryContractSurface(id)
.catch(async (error: unknown) => {
if (error instanceof MissingBundledDirectoryContractArtifactError) {
return null;
}
throw error;
})
.then(async (loaded) => {
if (!loaded) {
return (await getBundledChannelPluginAsync(id)) ?? null;
}
const plugin = findChannelDirectoryContractPlugin(loaded);
return plugin ?? (await getBundledChannelPluginAsync(id)) ?? null;
})
.then((plugin) => {
channelDirectoryPluginCache.set(id, plugin);
return plugin;
})
.finally(() => {
channelDirectoryPluginPromiseCache.delete(id);
});
channelDirectoryPluginPromiseCache.set(id, loading);
return (await loading) ?? undefined;
}

View File

@@ -1,6 +1,9 @@
import { expectChannelPluginContract } from "openclaw/plugin-sdk/channel-test-helpers";
import { beforeAll, describe, it } from "vitest";
import { getBundledChannelPluginAsync } from "./bundled-channel-plugin-loader.js";
import {
getBundledChannelDirectoryPluginAsync,
getBundledChannelPluginAsync,
} from "./bundled-channel-plugin-loader.js";
import { channelPluginSurfaceKeys } from "./manifest.js";
import { getPluginContractRegistryShardRefs } from "./registry-plugin.js";
import {
@@ -68,11 +71,14 @@ export function installDirectoryContractRegistryShard(params: ContractShardParam
installEmptyShardSuite("directory contract registry shard");
return;
}
const pluginCache = new Map<string, Awaited<ReturnType<typeof getBundledChannelPluginAsync>>>();
const pluginCache = new Map<
string,
Awaited<ReturnType<typeof getBundledChannelDirectoryPluginAsync>>
>();
beforeAll(async () => {
await Promise.all(
entries.map(async (entry) => {
pluginCache.set(entry.id, await getBundledChannelPluginAsync(entry.id));
pluginCache.set(entry.id, await getBundledChannelDirectoryPluginAsync(entry.id));
}),
);
});

View File

@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import type { ChannelPlugin } from "../channels/plugins/types.plugin.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import type { OpenClawPluginApi, PluginRegistrationMode } from "../plugins/types.js";
import { defineChannelPluginEntry } from "./core.js";
import { createChannelPluginBase, defineChannelPluginEntry } from "./core.js";
function createChannelPlugin(id: string): ChannelPlugin {
return {
@@ -119,3 +119,20 @@ describe("defineChannelPluginEntry", () => {
expect(registerFull).toHaveBeenCalledWith(fullApi);
});
});
describe("createChannelPluginBase", () => {
it("keeps meta id aligned with the channel id", () => {
const plugin = createChannelPluginBase({
id: "metadata-id-channel",
meta: {
label: "Metadata ID Channel",
selectionLabel: "Metadata ID Channel",
docsPath: "/channels/metadata-id-channel",
blurb: "metadata id channel",
},
setup: {} as NonNullable<ChannelPlugin["setup"]>,
});
expect(plugin.meta.id).toBe("metadata-id-channel");
});
});

View File

@@ -826,6 +826,7 @@ export function createChannelPluginBase<TResolvedAccount>(
meta: {
...resolveSdkChatChannelMeta(params.id),
...params.meta,
id: params.id,
},
...(params.setupWizard ? { setupWizard: params.setupWizard } : {}),
...(params.capabilities ? { capabilities: params.capabilities } : {}),