mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(testing): speed channel contract loading
This commit is contained in:
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
6
extensions/googlechat/directory-contract-api.ts
Normal file
6
extensions/googlechat/directory-contract-api.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { googlechatDirectoryAdapter } from "./src/channel.adapters.js";
|
||||
|
||||
export const googlechatDirectoryContractPlugin = {
|
||||
id: "googlechat",
|
||||
directory: googlechatDirectoryAdapter,
|
||||
};
|
||||
45
extensions/msteams/directory-contract-api.ts
Normal file
45
extensions/msteams/directory-contract-api.ts
Normal 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,
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
Reference in New Issue
Block a user