diff --git a/extensions/feishu/channel-plugin-api.ts b/extensions/feishu/channel-plugin-api.ts new file mode 100644 index 00000000000..66cb83be024 --- /dev/null +++ b/extensions/feishu/channel-plugin-api.ts @@ -0,0 +1 @@ +export { feishuPlugin } from "./src/channel.js"; diff --git a/extensions/feishu/index.ts b/extensions/feishu/index.ts index 9d0f6b2540d..9a54b361366 100644 --- a/extensions/feishu/index.ts +++ b/extensions/feishu/index.ts @@ -67,7 +67,7 @@ export default defineBundledChannelEntry({ description: "Feishu/Lark channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "feishuPlugin", }, secrets: { diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index d07d94cc0cb..a080b406588 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -49,7 +49,6 @@ import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, } from "./channel-runtime-api.js"; -import { createFeishuClient } from "./client.js"; import { isRecord } from "./comment-shared.js"; import { FeishuConfigSchema } from "./config-schema.js"; import { @@ -119,6 +118,11 @@ const loadFeishuChannelRuntime = createLazyRuntimeNamedExport( "feishuChannelRuntime", ); +async function createFeishuActionClient(account: ResolvedFeishuAccount) { + const { createFeishuClient } = await import("./client.js"); + return createFeishuClient(account); +} + const collectFeishuSecurityWarnings = createAllowlistProviderGroupPolicyWarningCollector<{ cfg: ClawdbotConfig; accountId?: string | null; @@ -841,7 +845,7 @@ export const feishuPlugin: ChannelPlugin { + const { beginAppRegistration, initAppRegistration, pollAppRegistration, printQrCode } = + await import("./app-registration.js"); try { await initAppRegistration("feishu"); } catch { @@ -371,6 +365,7 @@ async function runNewAppFlow(params: { // Fetch openId via API for manual flow. if (appId && appSecretProbeValue) { + const { getAppOwnerOpenId } = await import("./app-registration.js"); scanOpenId = await getAppOwnerOpenId({ appId, appSecret: appSecretProbeValue, @@ -528,6 +523,7 @@ export const feishuSetupWizard: ChannelSetupWizard = { let probeResult = null; if (configured && resolvedCredentials) { try { + const { probeFeishu } = await import("./probe.js"); probeResult = await probeFeishu(resolvedCredentials); } catch {} } diff --git a/extensions/googlechat/channel-plugin-api.ts b/extensions/googlechat/channel-plugin-api.ts new file mode 100644 index 00000000000..605b2539fcb --- /dev/null +++ b/extensions/googlechat/channel-plugin-api.ts @@ -0,0 +1 @@ +export { googlechatPlugin } from "./src/channel.js"; diff --git a/extensions/googlechat/index.ts b/extensions/googlechat/index.ts index 11216e06f1a..17e9836b182 100644 --- a/extensions/googlechat/index.ts +++ b/extensions/googlechat/index.ts @@ -6,7 +6,7 @@ export default defineBundledChannelEntry({ description: "OpenClaw Google Chat channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "googlechatPlugin", }, secrets: { diff --git a/extensions/imessage/channel-plugin-api.ts b/extensions/imessage/channel-plugin-api.ts new file mode 100644 index 00000000000..2b6d13a4369 --- /dev/null +++ b/extensions/imessage/channel-plugin-api.ts @@ -0,0 +1 @@ +export { imessagePlugin } from "./src/channel.js"; diff --git a/extensions/imessage/index.ts b/extensions/imessage/index.ts index aeb56673f45..55694355deb 100644 --- a/extensions/imessage/index.ts +++ b/extensions/imessage/index.ts @@ -6,7 +6,7 @@ export default defineBundledChannelEntry({ description: "iMessage channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "imessagePlugin", }, runtime: { diff --git a/extensions/line/channel-plugin-api.ts b/extensions/line/channel-plugin-api.ts new file mode 100644 index 00000000000..bfb3db010aa --- /dev/null +++ b/extensions/line/channel-plugin-api.ts @@ -0,0 +1 @@ +export { linePlugin } from "./src/channel.js"; diff --git a/extensions/line/index.ts b/extensions/line/index.ts index 8a6428da2f4..338e4dfe1f3 100644 --- a/extensions/line/index.ts +++ b/extensions/line/index.ts @@ -31,7 +31,7 @@ export default defineBundledChannelEntry({ description: "LINE Messaging API channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "linePlugin", }, runtime: { diff --git a/extensions/msteams/channel-plugin-api.ts b/extensions/msteams/channel-plugin-api.ts new file mode 100644 index 00000000000..36c93f4ea87 --- /dev/null +++ b/extensions/msteams/channel-plugin-api.ts @@ -0,0 +1 @@ +export { msteamsPlugin } from "./src/channel.js"; diff --git a/extensions/msteams/index.ts b/extensions/msteams/index.ts index 56ce8932b19..b259d7d9c04 100644 --- a/extensions/msteams/index.ts +++ b/extensions/msteams/index.ts @@ -6,7 +6,7 @@ export default defineBundledChannelEntry({ description: "Microsoft Teams channel plugin (Bot Framework)", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "msteamsPlugin", }, secrets: { diff --git a/extensions/nextcloud-talk/channel-plugin-api.ts b/extensions/nextcloud-talk/channel-plugin-api.ts new file mode 100644 index 00000000000..05701614b9e --- /dev/null +++ b/extensions/nextcloud-talk/channel-plugin-api.ts @@ -0,0 +1 @@ +export { nextcloudTalkPlugin } from "./src/channel.js"; diff --git a/extensions/nextcloud-talk/index.ts b/extensions/nextcloud-talk/index.ts index 4cf42af8913..6a77e515592 100644 --- a/extensions/nextcloud-talk/index.ts +++ b/extensions/nextcloud-talk/index.ts @@ -6,7 +6,7 @@ export default defineBundledChannelEntry({ description: "Nextcloud Talk channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "nextcloudTalkPlugin", }, secrets: { diff --git a/extensions/nostr/channel-plugin-api.ts b/extensions/nostr/channel-plugin-api.ts new file mode 100644 index 00000000000..3cdf86a5120 --- /dev/null +++ b/extensions/nostr/channel-plugin-api.ts @@ -0,0 +1 @@ +export { nostrPlugin } from "./src/channel.js"; diff --git a/extensions/nostr/index.ts b/extensions/nostr/index.ts index cb13331e698..f451e86db0d 100644 --- a/extensions/nostr/index.ts +++ b/extensions/nostr/index.ts @@ -35,7 +35,7 @@ export default defineBundledChannelEntry({ description: "Nostr DM channel plugin via NIP-04", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "nostrPlugin", }, runtime: { diff --git a/extensions/qa-channel/channel-plugin-api.ts b/extensions/qa-channel/channel-plugin-api.ts new file mode 100644 index 00000000000..08854379e82 --- /dev/null +++ b/extensions/qa-channel/channel-plugin-api.ts @@ -0,0 +1 @@ +export { qaChannelPlugin } from "./src/channel.js"; diff --git a/extensions/qa-channel/index.ts b/extensions/qa-channel/index.ts index cbeac3e6e85..6c708aa866e 100644 --- a/extensions/qa-channel/index.ts +++ b/extensions/qa-channel/index.ts @@ -6,7 +6,7 @@ export default defineBundledChannelEntry({ description: "Synthetic QA channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "qaChannelPlugin", }, runtime: { diff --git a/extensions/qqbot/channel-plugin-api.ts b/extensions/qqbot/channel-plugin-api.ts new file mode 100644 index 00000000000..b8381f8e4c3 --- /dev/null +++ b/extensions/qqbot/channel-plugin-api.ts @@ -0,0 +1 @@ +export { qqbotPlugin } from "./src/channel.js"; diff --git a/extensions/qqbot/index.ts b/extensions/qqbot/index.ts index 044507e1136..0521489b789 100644 --- a/extensions/qqbot/index.ts +++ b/extensions/qqbot/index.ts @@ -95,7 +95,7 @@ export default defineBundledChannelEntry({ description: "QQ Bot channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "qqbotPlugin", }, runtime: { diff --git a/extensions/synology-chat/channel-plugin-api.ts b/extensions/synology-chat/channel-plugin-api.ts new file mode 100644 index 00000000000..3bfa6e74ac0 --- /dev/null +++ b/extensions/synology-chat/channel-plugin-api.ts @@ -0,0 +1 @@ +export { synologyChatPlugin } from "./src/channel.js"; diff --git a/extensions/synology-chat/index.ts b/extensions/synology-chat/index.ts index 909d4fe22e9..964ac50d3da 100644 --- a/extensions/synology-chat/index.ts +++ b/extensions/synology-chat/index.ts @@ -6,7 +6,7 @@ export default defineBundledChannelEntry({ description: "Native Synology Chat channel plugin for OpenClaw", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "synologyChatPlugin", }, runtime: { diff --git a/extensions/tlon/channel-plugin-api.ts b/extensions/tlon/channel-plugin-api.ts new file mode 100644 index 00000000000..bb19cd45b55 --- /dev/null +++ b/extensions/tlon/channel-plugin-api.ts @@ -0,0 +1 @@ +export { tlonPlugin } from "./src/channel.js"; diff --git a/extensions/tlon/index.ts b/extensions/tlon/index.ts index fd2ec8112e2..8fb1ad5863a 100644 --- a/extensions/tlon/index.ts +++ b/extensions/tlon/index.ts @@ -119,7 +119,7 @@ export default defineBundledChannelEntry({ description: "Tlon/Urbit channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "tlonPlugin", }, runtime: { diff --git a/extensions/twitch/channel-plugin-api.ts b/extensions/twitch/channel-plugin-api.ts new file mode 100644 index 00000000000..556e36458b3 --- /dev/null +++ b/extensions/twitch/channel-plugin-api.ts @@ -0,0 +1 @@ +export { twitchPlugin } from "./src/plugin.js"; diff --git a/extensions/twitch/index.ts b/extensions/twitch/index.ts index c9d078a5424..86d45d23d3f 100644 --- a/extensions/twitch/index.ts +++ b/extensions/twitch/index.ts @@ -6,7 +6,7 @@ export default defineBundledChannelEntry({ description: "Twitch IRC chat channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "twitchPlugin", }, runtime: { diff --git a/extensions/zalo/channel-plugin-api.ts b/extensions/zalo/channel-plugin-api.ts new file mode 100644 index 00000000000..871c01eb4d0 --- /dev/null +++ b/extensions/zalo/channel-plugin-api.ts @@ -0,0 +1 @@ +export { zaloPlugin } from "./src/channel.js"; diff --git a/extensions/zalo/index.ts b/extensions/zalo/index.ts index 342666bc0bf..ef946dc3bfc 100644 --- a/extensions/zalo/index.ts +++ b/extensions/zalo/index.ts @@ -6,7 +6,7 @@ export default defineBundledChannelEntry({ description: "Zalo channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "zaloPlugin", }, secrets: { diff --git a/src/secrets/runtime-openai-file-fixture.test-helper.ts b/src/secrets/runtime-openai-file-fixture.test-helper.ts index b38ae9a0d6e..a696dd89b71 100644 --- a/src/secrets/runtime-openai-file-fixture.test-helper.ts +++ b/src/secrets/runtime-openai-file-fixture.test-helper.ts @@ -1,11 +1,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import { expect } from "vitest"; -import { ensureAuthProfileStore, type AuthProfileStore } from "../agents/auth-profiles.js"; +import type { AuthProfileStore } from "../agents/auth-profiles.js"; import { loadConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginOrigin } from "../plugins/plugin-origin.types.js"; import type { captureEnv } from "../test-utils/env.js"; +import { getActiveSecretsRuntimeSnapshot } from "./runtime.js"; export const OPENAI_ENV_KEY_REF = { source: "env", @@ -102,7 +103,10 @@ export function createOpenAIFileRuntimeConfig(secretFile: string): OpenClawConfi export function expectResolvedOpenAIRuntime(agentDir: string) { expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime"); - expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({ + const activeAuthStore = getActiveSecretsRuntimeSnapshot()?.authStores.find( + (entry) => entry.agentDir === agentDir, + )?.store; + expect(activeAuthStore?.profiles["openai:default"]).toMatchObject({ type: "api_key", key: "sk-file-runtime", }); diff --git a/test/helpers/channels/bundled-channel-plugin-loader.ts b/test/helpers/channels/bundled-channel-plugin-loader.ts new file mode 100644 index 00000000000..c42574eca26 --- /dev/null +++ b/test/helpers/channels/bundled-channel-plugin-loader.ts @@ -0,0 +1,55 @@ +import { listBundledChannelPluginIds as listCatalogBundledChannelPluginIds } from "../../../src/channels/plugins/bundled-ids.js"; +import type { ChannelId } from "../../../src/channels/plugins/channel-id.types.js"; +import type { ChannelPlugin } from "../../../src/channels/plugins/types.js"; +import { + listChannelCatalogEntries, + type PluginChannelCatalogEntry, +} from "../../../src/plugins/channel-catalog-registry.js"; +import { loadBundledPluginPublicSurfaceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; + +type ChannelPluginApiModule = Record; + +const channelPluginCache = new Map(); +let channelCatalogEntries: PluginChannelCatalogEntry[] | undefined; + +function isChannelPlugin(value: unknown): value is ChannelPlugin { + return ( + Boolean(value) && + typeof value === "object" && + typeof (value as Partial).id === "string" && + Boolean((value as Partial).meta) && + Boolean((value as Partial).config) + ); +} + +export function listBundledChannelPluginIds(): readonly ChannelId[] { + return listCatalogBundledChannelPluginIds() as ChannelId[]; +} + +export function getBundledChannelCatalogEntry( + id: ChannelId, +): PluginChannelCatalogEntry | undefined { + channelCatalogEntries ??= listChannelCatalogEntries({ origin: "bundled" }); + return channelCatalogEntries.find((entry) => entry.pluginId === id || entry.channel.id === id); +} + +export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined { + if (channelPluginCache.has(id)) { + return channelPluginCache.get(id) ?? undefined; + } + + const loaded = loadBundledPluginPublicSurfaceSync({ + pluginId: id, + artifactBasename: "channel-plugin-api.js", + }); + const plugin = Object.values(loaded).find(isChannelPlugin) ?? null; + channelPluginCache.set(id, plugin); + return plugin ?? undefined; +} + +export function listBundledChannelPlugins(): readonly ChannelPlugin[] { + return listBundledChannelPluginIds().flatMap((id) => { + const plugin = getBundledChannelPlugin(id); + return plugin ? [plugin] : []; + }); +} diff --git a/test/helpers/channels/registry-plugin.ts b/test/helpers/channels/registry-plugin.ts index 6c6edc7b4b5..6a68c04ad20 100644 --- a/test/helpers/channels/registry-plugin.ts +++ b/test/helpers/channels/registry-plugin.ts @@ -1,11 +1,12 @@ -import { - getBundledChannelPlugin, - listBundledChannelPluginIds, - listBundledChannelPlugins, -} from "../../../src/channels/plugins/bundled.js"; import type { ChannelId } from "../../../src/channels/plugins/channel-id.types.js"; import { normalizeChannelMeta } from "../../../src/channels/plugins/meta-normalization.js"; import type { ChannelPlugin } from "../../../src/channels/plugins/types.js"; +import { + getBundledChannelCatalogEntry, + getBundledChannelPlugin, + listBundledChannelPluginIds, + listBundledChannelPlugins, +} from "./bundled-channel-plugin-loader.js"; type PluginContractEntry = { id: string; @@ -13,11 +14,12 @@ type PluginContractEntry = { }; function toPluginContractEntry(plugin: ChannelPlugin): PluginContractEntry { + const existingMeta = getBundledChannelCatalogEntry(plugin.id)?.channel; return { id: plugin.id, plugin: { ...plugin, - meta: normalizeChannelMeta({ id: plugin.id, meta: plugin.meta }), + meta: normalizeChannelMeta({ id: plugin.id, meta: plugin.meta, existing: existingMeta }), }, }; } diff --git a/test/helpers/channels/surface-contract-registry.ts b/test/helpers/channels/surface-contract-registry.ts index a6f8c02e1d4..cebd12acd2c 100644 --- a/test/helpers/channels/surface-contract-registry.ts +++ b/test/helpers/channels/surface-contract-registry.ts @@ -1,17 +1,11 @@ -import { - getBundledChannelPlugin, - listBundledChannelPluginIds, - listBundledChannelPlugins, - setBundledChannelRuntime, -} from "../../../src/channels/plugins/bundled.js"; import type { ChannelId } from "../../../src/channels/plugins/channel-id.types.js"; import type { ChannelPlugin } from "../../../src/channels/plugins/types.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; import { - listLineAccountIds, - resolveDefaultLineAccountId, - resolveLineAccount, -} from "../../../src/plugin-sdk/line.js"; + getBundledChannelPlugin, + listBundledChannelPluginIds, + listBundledChannelPlugins, +} from "./bundled-channel-plugin-loader.js"; import { channelPluginSurfaceKeys, type ChannelPluginSurface } from "./manifest.js"; type SurfaceContractEntry = { @@ -44,17 +38,6 @@ type DirectoryContractEntry = { accountId?: string; }; -setBundledChannelRuntime("line", { - channel: { - line: { - listLineAccountIds, - resolveDefaultLineAccountId, - resolveLineAccount: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string }) => - resolveLineAccount({ cfg, accountId }), - }, - }, -} as never); - let surfaceContractRegistryCache: SurfaceContractEntry[] | undefined; const surfaceContractEntryCache = new Map(); let threadingContractRegistryCache: ThreadingContractEntry[] | undefined; diff --git a/test/helpers/channels/threading-directory-contract-suites.ts b/test/helpers/channels/threading-directory-contract-suites.ts index 16749aba4b9..56404e6d820 100644 --- a/test/helpers/channels/threading-directory-contract-suites.ts +++ b/test/helpers/channels/threading-directory-contract-suites.ts @@ -7,9 +7,18 @@ import type { } from "../../../src/channels/plugins/types.core.js"; import type { ChannelPlugin } from "../../../src/channels/plugins/types.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; -import { createNonExitingRuntime } from "../../../src/runtime.js"; +import type { RuntimeEnv } from "../../../src/runtime.js"; -const contractRuntime = createNonExitingRuntime(); +let contractRuntime: RuntimeEnv | undefined; + +async function getDirectoryContractRuntime(): Promise { + if (contractRuntime) { + return contractRuntime; + } + const { createNonExitingRuntime } = await import("../../../src/runtime.js"); + contractRuntime = createNonExitingRuntime(); + return contractRuntime; +} function expectDirectoryEntryShape(entry: ChannelDirectoryEntry) { expect(["user", "group", "channel"]).toContain(entry.kind); @@ -177,10 +186,11 @@ export function installChannelDirectoryContractSuite(params: { if (params.coverage === "presence") { return; } + const runtime = await getDirectoryContractRuntime(); const self = await directory?.self?.({ cfg: params.cfg ?? ({} as OpenClawConfig), accountId: params.accountId ?? "default", - runtime: contractRuntime, + runtime, }); if (self) { expectDirectoryEntryShape(self); @@ -192,7 +202,7 @@ export function installChannelDirectoryContractSuite(params: { accountId: params.accountId ?? "default", query: "", limit: 5, - runtime: contractRuntime, + runtime, })) ?? []; expect(Array.isArray(peers)).toBe(true); for (const peer of peers) { @@ -205,7 +215,7 @@ export function installChannelDirectoryContractSuite(params: { accountId: params.accountId ?? "default", query: "", limit: 5, - runtime: contractRuntime, + runtime, })) ?? []; expect(Array.isArray(groups)).toBe(true); for (const group of groups) { @@ -218,7 +228,7 @@ export function installChannelDirectoryContractSuite(params: { accountId: params.accountId ?? "default", groupId: groups[0].id, limit: 5, - runtime: contractRuntime, + runtime, }); expect(Array.isArray(members)).toBe(true); for (const member of members) {