From 891e2a3da8c674f284cdc2cd71acd86d34782d7b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 18 Mar 2026 09:54:22 -0700 Subject: [PATCH] Build: isolate optional bundled plugin-sdk clusters --- scripts/lib/optional-bundled-clusters.mjs | 14 ++++++ src/plugin-sdk/googlechat.ts | 38 +++++++++++++-- src/plugin-sdk/matrix.ts | 21 ++++++++- src/plugin-sdk/msteams.ts | 21 ++++++++- src/plugin-sdk/nostr.ts | 20 +++++++- src/plugin-sdk/optional-channel-setup.ts | 56 +++++++++++++++++++++++ src/plugin-sdk/tlon.ts | 20 +++++++- src/plugin-sdk/twitch.ts | 21 +++++++-- src/plugin-sdk/zalouser.ts | 21 ++++++++- tsdown.config.ts | 4 ++ 10 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 src/plugin-sdk/optional-channel-setup.ts diff --git a/scripts/lib/optional-bundled-clusters.mjs b/scripts/lib/optional-bundled-clusters.mjs index c3c442d4ae7..153dfee4ad6 100644 --- a/scripts/lib/optional-bundled-clusters.mjs +++ b/scripts/lib/optional-bundled-clusters.mjs @@ -14,3 +14,17 @@ export const optionalBundledClusters = [ ]; export const optionalBundledClusterSet = new Set(optionalBundledClusters); + +export const OPTIONAL_BUNDLED_BUILD_ENV = "OPENCLAW_INCLUDE_OPTIONAL_BUNDLED"; + +export function isOptionalBundledCluster(cluster) { + return optionalBundledClusterSet.has(cluster); +} + +export function shouldIncludeOptionalBundledClusters(env = process.env) { + return env[OPTIONAL_BUNDLED_BUILD_ENV] === "1"; +} + +export function shouldBuildBundledCluster(cluster, env = process.env) { + return shouldIncludeOptionalBundledClusters(env) || !isOptionalBundledCluster(cluster); +} diff --git a/src/plugin-sdk/googlechat.ts b/src/plugin-sdk/googlechat.ts index ade38097fad..bbb818b78b8 100644 --- a/src/plugin-sdk/googlechat.ts +++ b/src/plugin-sdk/googlechat.ts @@ -1,6 +1,12 @@ // Narrow plugin-sdk surface for the bundled googlechat plugin. // Keep this list additive and scoped to symbols used under extensions/googlechat. +import { resolveChannelGroupRequireMention } from "./channel-policy.js"; +import { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + export { createActionGate, jsonResult, @@ -20,7 +26,6 @@ export { export { buildComputedAccountStatusSnapshot } from "./status-helpers.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; export { createAccountStatusSink, runPassiveAccountLifecycle } from "./channel-lifecycle.js"; -export { resolveGoogleChatGroupRequireMention } from "../../extensions/googlechat/src/group-policy.js"; export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; export { @@ -65,8 +70,6 @@ export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.j export { resolveDmGroupAccessWithLists } from "../security/dm-policy-shared.js"; export { formatDocsLink } from "../terminal/links.js"; export type { WizardPrompter } from "../wizard/prompts.js"; -export { googlechatSetupAdapter } from "../../extensions/googlechat/api.js"; -export { googlechatSetupWizard } from "../../extensions/googlechat/api.js"; export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "./inbound-envelope.js"; export { createScopedPairingAccess } from "./pairing-access.js"; export { issuePairingChallenge } from "../pairing/pairing-challenge.js"; @@ -88,3 +91,32 @@ export { resolveWebhookTargetWithAuthOrReject, withResolvedWebhookRequestPipeline, } from "./webhook-targets.js"; + +type GoogleChatGroupContext = { + cfg: import("../config/config.js").OpenClawConfig; + accountId?: string | null; + groupId?: string | null; +}; + +export function resolveGoogleChatGroupRequireMention(params: GoogleChatGroupContext): boolean { + return resolveChannelGroupRequireMention({ + cfg: params.cfg, + channel: "googlechat", + groupId: params.groupId, + accountId: params.accountId, + }); +} + +export const googlechatSetupAdapter = createOptionalChannelSetupAdapter({ + channel: "googlechat", + label: "Google Chat", + npmSpec: "@openclaw/googlechat", + docsPath: "/channels/googlechat", +}); + +export const googlechatSetupWizard = createOptionalChannelSetupWizard({ + channel: "googlechat", + label: "Google Chat", + npmSpec: "@openclaw/googlechat", + docsPath: "/channels/googlechat", +}); diff --git a/src/plugin-sdk/matrix.ts b/src/plugin-sdk/matrix.ts index 099b53792da..5bbaac2ce48 100644 --- a/src/plugin-sdk/matrix.ts +++ b/src/plugin-sdk/matrix.ts @@ -1,6 +1,11 @@ // Narrow plugin-sdk surface for the bundled matrix plugin. // Keep this list additive and scoped to symbols used under extensions/matrix. +import { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + export { createActionGate, jsonResult, @@ -108,5 +113,17 @@ export { buildProbeChannelStatusSummary, collectStatusIssuesFromLastError, } from "./status-helpers.js"; -export { matrixSetupWizard } from "../../extensions/matrix/api.js"; -export { matrixSetupAdapter } from "../../extensions/matrix/api.js"; + +export const matrixSetupWizard = createOptionalChannelSetupWizard({ + channel: "matrix", + label: "Matrix", + npmSpec: "@openclaw/matrix", + docsPath: "/channels/matrix", +}); + +export const matrixSetupAdapter = createOptionalChannelSetupAdapter({ + channel: "matrix", + label: "Matrix", + npmSpec: "@openclaw/matrix", + docsPath: "/channels/matrix", +}); diff --git a/src/plugin-sdk/msteams.ts b/src/plugin-sdk/msteams.ts index 1185558de79..803dd999a62 100644 --- a/src/plugin-sdk/msteams.ts +++ b/src/plugin-sdk/msteams.ts @@ -1,6 +1,11 @@ // Narrow plugin-sdk surface for the bundled msteams plugin. // Keep this list additive and scoped to symbols used under extensions/msteams. +import { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + export type { ChunkMode } from "../auto-reply/chunk.js"; export type { HistoryEntry } from "../auto-reply/reply/history.js"; export { @@ -117,5 +122,17 @@ export { createDefaultChannelRuntimeState, } from "./status-helpers.js"; export { normalizeStringEntries } from "../shared/string-normalization.js"; -export { msteamsSetupWizard } from "../../extensions/msteams/api.js"; -export { msteamsSetupAdapter } from "../../extensions/msteams/api.js"; + +export const msteamsSetupWizard = createOptionalChannelSetupWizard({ + channel: "msteams", + label: "Microsoft Teams", + npmSpec: "@openclaw/msteams", + docsPath: "/channels/msteams", +}); + +export const msteamsSetupAdapter = createOptionalChannelSetupAdapter({ + channel: "msteams", + label: "Microsoft Teams", + npmSpec: "@openclaw/msteams", + docsPath: "/channels/msteams", +}); diff --git a/src/plugin-sdk/nostr.ts b/src/plugin-sdk/nostr.ts index 4c8abc0f15a..a3bd64e34fc 100644 --- a/src/plugin-sdk/nostr.ts +++ b/src/plugin-sdk/nostr.ts @@ -1,6 +1,11 @@ // Narrow plugin-sdk surface for the bundled nostr plugin. // Keep this list additive and scoped to symbols used under extensions/nostr. +import { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js"; export { formatPairingApproveHint } from "../channels/plugins/helpers.js"; @@ -19,4 +24,17 @@ export { } from "./status-helpers.js"; export { createFixedWindowRateLimiter } from "./webhook-memory-guards.js"; export { mapAllowFromEntries } from "./channel-config-helpers.js"; -export { nostrSetupAdapter, nostrSetupWizard } from "../../extensions/nostr/setup-api.js"; + +export const nostrSetupAdapter = createOptionalChannelSetupAdapter({ + channel: "nostr", + label: "Nostr", + npmSpec: "@openclaw/nostr", + docsPath: "/channels/nostr", +}); + +export const nostrSetupWizard = createOptionalChannelSetupWizard({ + channel: "nostr", + label: "Nostr", + npmSpec: "@openclaw/nostr", + docsPath: "/channels/nostr", +}); diff --git a/src/plugin-sdk/optional-channel-setup.ts b/src/plugin-sdk/optional-channel-setup.ts new file mode 100644 index 00000000000..42f62e2efcd --- /dev/null +++ b/src/plugin-sdk/optional-channel-setup.ts @@ -0,0 +1,56 @@ +import type { ChannelSetupWizard } from "../channels/plugins/setup-wizard.js"; +import type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js"; +import { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; +import { formatDocsLink } from "../terminal/links.js"; + +type OptionalChannelSetupParams = { + channel: string; + label: string; + npmSpec?: string; + docsPath?: string; +}; + +function buildOptionalChannelSetupMessage(params: OptionalChannelSetupParams): string { + const installTarget = params.npmSpec ?? `the ${params.label} plugin`; + const message = [`${params.label} setup requires ${installTarget} to be installed.`]; + if (params.docsPath) { + message.push(`Docs: ${formatDocsLink(params.docsPath, params.docsPath.replace(/^\/+/u, ""))}`); + } + return message.join(" "); +} + +export function createOptionalChannelSetupAdapter( + params: OptionalChannelSetupParams, +): ChannelSetupAdapter { + const message = buildOptionalChannelSetupMessage(params); + return { + resolveAccountId: ({ accountId }) => accountId ?? DEFAULT_ACCOUNT_ID, + applyAccountConfig: () => { + throw new Error(message); + }, + validateInput: () => message, + }; +} + +export function createOptionalChannelSetupWizard( + params: OptionalChannelSetupParams, +): ChannelSetupWizard { + const message = buildOptionalChannelSetupMessage(params); + return { + channel: params.channel, + status: { + configuredLabel: `${params.label} plugin installed`, + unconfiguredLabel: `install ${params.label} plugin`, + configuredHint: message, + unconfiguredHint: message, + unconfiguredScore: 0, + resolveConfigured: () => false, + resolveStatusLines: () => [message], + resolveSelectionHint: () => message, + }, + credentials: [], + finalize: async () => { + throw new Error(message); + }, + }; +} diff --git a/src/plugin-sdk/tlon.ts b/src/plugin-sdk/tlon.ts index 1bcd9078292..cd11ca66545 100644 --- a/src/plugin-sdk/tlon.ts +++ b/src/plugin-sdk/tlon.ts @@ -1,6 +1,11 @@ // Narrow plugin-sdk surface for the bundled tlon plugin. // Keep this list additive and scoped to symbols used under extensions/tlon. +import { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + export type { ReplyPayload } from "../auto-reply/types.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; export { @@ -27,4 +32,17 @@ export type { RuntimeEnv } from "../runtime.js"; export { formatDocsLink } from "../terminal/links.js"; export type { WizardPrompter } from "../wizard/prompts.js"; export { createLoggerBackedRuntime } from "./runtime.js"; -export { tlonSetupAdapter, tlonSetupWizard } from "../../extensions/tlon/setup-api.js"; + +export const tlonSetupAdapter = createOptionalChannelSetupAdapter({ + channel: "tlon", + label: "Tlon", + npmSpec: "@openclaw/tlon", + docsPath: "/channels/tlon", +}); + +export const tlonSetupWizard = createOptionalChannelSetupWizard({ + channel: "tlon", + label: "Tlon", + npmSpec: "@openclaw/tlon", + docsPath: "/channels/tlon", +}); diff --git a/src/plugin-sdk/twitch.ts b/src/plugin-sdk/twitch.ts index 907cdd171fa..77bba58209e 100644 --- a/src/plugin-sdk/twitch.ts +++ b/src/plugin-sdk/twitch.ts @@ -1,6 +1,11 @@ // Narrow plugin-sdk surface for the bundled twitch plugin. // Keep this list additive and scoped to symbols used under extensions/twitch. +import { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + export type { ReplyPayload } from "../auto-reply/types.js"; export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; export type { @@ -33,7 +38,15 @@ export type { OpenClawPluginApi } from "../plugins/types.js"; export type { RuntimeEnv } from "../runtime.js"; export { formatDocsLink } from "../terminal/links.js"; export type { WizardPrompter } from "../wizard/prompts.js"; -export { - twitchSetupAdapter, - twitchSetupWizard, -} from "../../extensions/twitch/src/setup-surface.js"; + +export const twitchSetupAdapter = createOptionalChannelSetupAdapter({ + channel: "twitch", + label: "Twitch", + npmSpec: "@openclaw/twitch", +}); + +export const twitchSetupWizard = createOptionalChannelSetupWizard({ + channel: "twitch", + label: "Twitch", + npmSpec: "@openclaw/twitch", +}); diff --git a/src/plugin-sdk/zalouser.ts b/src/plugin-sdk/zalouser.ts index ed66e31754e..e2ab63e0e7a 100644 --- a/src/plugin-sdk/zalouser.ts +++ b/src/plugin-sdk/zalouser.ts @@ -1,6 +1,11 @@ // Narrow plugin-sdk surface for the bundled zalouser plugin. // Keep this list additive and scoped to symbols used under extensions/zalouser. +import { + createOptionalChannelSetupAdapter, + createOptionalChannelSetupWizard, +} from "./optional-channel-setup.js"; + export type { ReplyPayload } from "../auto-reply/types.js"; export { mergeAllowlist, summarizeMapping } from "../channels/allowlists/resolve-utils.js"; export { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; @@ -53,8 +58,6 @@ export type { WizardPrompter } from "../wizard/prompts.js"; export { formatAllowFromLowercase } from "./allow-from.js"; export { resolveSenderCommandAuthorization } from "./command-auth.js"; export { resolveChannelAccountConfigBasePath } from "./config-paths.js"; -export { zalouserSetupAdapter } from "../../extensions/zalouser/api.js"; -export { zalouserSetupWizard } from "../../extensions/zalouser/api.js"; export { evaluateGroupRouteAccessForPolicy, resolveSenderScopedGroupPolicy, @@ -73,3 +76,17 @@ export { export { formatResolvedUnresolvedNote } from "./resolution-notes.js"; export { buildBaseAccountStatusSnapshot } from "./status-helpers.js"; export { chunkTextForOutbound } from "./text-chunking.js"; + +export const zalouserSetupAdapter = createOptionalChannelSetupAdapter({ + channel: "zalouser", + label: "Zalo Personal", + npmSpec: "@openclaw/zalouser", + docsPath: "/channels/zalouser", +}); + +export const zalouserSetupWizard = createOptionalChannelSetupWizard({ + channel: "zalouser", + label: "Zalo Personal", + npmSpec: "@openclaw/zalouser", + docsPath: "/channels/zalouser", +}); diff --git a/tsdown.config.ts b/tsdown.config.ts index 0d643b046ac..aafa874a041 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { defineConfig, type UserConfig } from "tsdown"; +import { shouldBuildBundledCluster } from "./scripts/lib/optional-bundled-clusters.mjs"; import { buildPluginSdkEntrySources } from "./scripts/lib/plugin-sdk-entries.mjs"; type InputOptionsFactory = Extract, Function>; @@ -81,6 +82,9 @@ function listBundledPluginBuildEntries(): Record { if (!dirent.isDirectory()) { continue; } + if (!shouldBuildBundledCluster(dirent.name, process.env)) { + continue; + } const pluginDir = path.join(extensionsRoot, dirent.name); const manifestPath = path.join(pluginDir, "openclaw.plugin.json");