diff --git a/extensions/discord/directory-contract-api.ts b/extensions/discord/directory-contract-api.ts new file mode 100644 index 00000000000..027b29f0459 --- /dev/null +++ b/extensions/discord/directory-contract-api.ts @@ -0,0 +1,4 @@ +export { + listDiscordDirectoryGroupsFromConfig, + listDiscordDirectoryPeersFromConfig, +} from "./src/directory-config.js"; diff --git a/extensions/slack/directory-contract-api.ts b/extensions/slack/directory-contract-api.ts new file mode 100644 index 00000000000..183f055df40 --- /dev/null +++ b/extensions/slack/directory-contract-api.ts @@ -0,0 +1,4 @@ +export { + listSlackDirectoryGroupsFromConfig, + listSlackDirectoryPeersFromConfig, +} from "./src/directory-config.js"; diff --git a/extensions/telegram/directory-contract-api.ts b/extensions/telegram/directory-contract-api.ts new file mode 100644 index 00000000000..d37033f2502 --- /dev/null +++ b/extensions/telegram/directory-contract-api.ts @@ -0,0 +1,4 @@ +export { + listTelegramDirectoryGroupsFromConfig, + listTelegramDirectoryPeersFromConfig, +} from "./src/directory-config.js"; diff --git a/extensions/telegram/src/account-config.ts b/extensions/telegram/src/account-config.ts new file mode 100644 index 00000000000..a6d3b4269a1 --- /dev/null +++ b/extensions/telegram/src/account-config.ts @@ -0,0 +1,37 @@ +import { + normalizeAccountId, + resolveAccountEntry, + type OpenClawConfig, +} from "openclaw/plugin-sdk/account-core"; +import type { TelegramAccountConfig } from "openclaw/plugin-sdk/config-runtime"; + +export function resolveTelegramAccountConfig( + cfg: OpenClawConfig, + accountId: string, +): TelegramAccountConfig | undefined { + const normalized = normalizeAccountId(accountId); + return resolveAccountEntry(cfg.channels?.telegram?.accounts, normalized); +} + +export function mergeTelegramAccountConfig( + cfg: OpenClawConfig, + accountId: string, +): TelegramAccountConfig { + const { + accounts: _ignored, + defaultAccount: _ignoredDefaultAccount, + groups: channelGroups, + ...base + } = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & { + accounts?: unknown; + defaultAccount?: unknown; + }; + const account = resolveTelegramAccountConfig(cfg, accountId) ?? {}; + + // Multi-account bots must not inherit channel-level groups unless explicitly set. + const configuredAccountIds = Object.keys(cfg.channels?.telegram?.accounts ?? {}); + const isMultiAccount = configuredAccountIds.length > 1; + const groups = account.groups ?? (isMultiAccount ? undefined : channelGroups); + + return { ...base, ...account, groups }; +} diff --git a/extensions/telegram/src/accounts.ts b/extensions/telegram/src/accounts.ts index 4bcfb21896c..bfb2081f26f 100644 --- a/extensions/telegram/src/accounts.ts +++ b/extensions/telegram/src/accounts.ts @@ -3,7 +3,6 @@ import { createAccountActionGate, normalizeAccountId, normalizeOptionalAccountId, - resolveAccountEntry, resolveAccountWithDefaultFallback, type OpenClawConfig, } from "openclaw/plugin-sdk/account-core"; @@ -14,6 +13,7 @@ import type { import { formatSetExplicitDefaultInstruction } from "openclaw/plugin-sdk/routing"; import { createSubsystemLogger, isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; +import { mergeTelegramAccountConfig, resolveTelegramAccountConfig } from "./account-config.js"; import { listTelegramAccountIds as listSelectedTelegramAccountIds, resolveDefaultTelegramAccountSelection, @@ -21,6 +21,8 @@ import { import type { TelegramTransport } from "./fetch.js"; import { resolveTelegramToken } from "./token.js"; +export { mergeTelegramAccountConfig, resolveTelegramAccountConfig } from "./account-config.js"; + let log: ReturnType | null = null; function getLog() { @@ -89,43 +91,6 @@ export function resolveDefaultTelegramAccountId(cfg: OpenClawConfig): string { return selection.accountId; } -export function resolveTelegramAccountConfig( - cfg: OpenClawConfig, - accountId: string, -): TelegramAccountConfig | undefined { - const normalized = normalizeAccountId(accountId); - return resolveAccountEntry(cfg.channels?.telegram?.accounts, normalized); -} - -export function mergeTelegramAccountConfig( - cfg: OpenClawConfig, - accountId: string, -): TelegramAccountConfig { - const { - accounts: _ignored, - defaultAccount: _ignoredDefaultAccount, - groups: channelGroups, - ...base - } = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & { - accounts?: unknown; - defaultAccount?: unknown; - }; - const account = resolveTelegramAccountConfig(cfg, accountId) ?? {}; - - // In multi-account setups, channel-level `groups` must NOT be inherited by - // accounts that don't have their own `groups` config. A bot that is not a - // member of a configured group will fail when handling group messages, and - // this failure disrupts message delivery for *all* accounts. - // Single-account setups keep backward compat: channel-level groups still - // applies when the account has no override. - // See: https://github.com/openclaw/openclaw/issues/30673 - const configuredAccountIds = Object.keys(cfg.channels?.telegram?.accounts ?? {}); - const isMultiAccount = configuredAccountIds.length > 1; - const groups = account.groups ?? (isMultiAccount ? undefined : channelGroups); - - return { ...base, ...account, groups }; -} - export function createTelegramActionGate(params: { cfg: OpenClawConfig; accountId?: string | null; diff --git a/extensions/telegram/src/directory-config.ts b/extensions/telegram/src/directory-config.ts index aba2fca3de1..eee8a3fb264 100644 --- a/extensions/telegram/src/directory-config.ts +++ b/extensions/telegram/src/directory-config.ts @@ -1,12 +1,30 @@ +import { normalizeAccountId } from "openclaw/plugin-sdk/account-core"; import { mapAllowFromEntries } from "openclaw/plugin-sdk/channel-config-helpers"; -import { createInspectedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime"; -import { inspectTelegramAccount, type InspectedTelegramAccount } from "./account-inspect.js"; +import type { OpenClawConfig, TelegramAccountConfig } from "openclaw/plugin-sdk/config-runtime"; +import { createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime"; +import { mergeTelegramAccountConfig } from "./account-config.js"; +import { resolveDefaultTelegramAccountSelection } from "./account-selection.js"; + +type TelegramDirectoryAccount = { + config: TelegramAccountConfig; +}; + +function resolveTelegramDirectoryAccount( + cfg: OpenClawConfig, + accountId?: string | null, +): TelegramDirectoryAccount { + const resolvedAccountId = accountId?.trim() + ? normalizeAccountId(accountId) + : resolveDefaultTelegramAccountSelection(cfg).accountId; + return { + config: mergeTelegramAccountConfig(cfg, resolvedAccountId), + }; +} export const listTelegramDirectoryPeersFromConfig = - createInspectedDirectoryEntriesLister({ + createResolvedDirectoryEntriesLister({ kind: "user", - inspectAccount: (cfg, accountId) => - inspectTelegramAccount({ cfg, accountId }) as InspectedTelegramAccount | null, + resolveAccount: (cfg, accountId) => resolveTelegramDirectoryAccount(cfg, accountId), resolveSources: (account) => [ mapAllowFromEntries(account.config.allowFrom), Object.keys(account.config.dms ?? {}), @@ -24,10 +42,9 @@ export const listTelegramDirectoryPeersFromConfig = }); export const listTelegramDirectoryGroupsFromConfig = - createInspectedDirectoryEntriesLister({ + createResolvedDirectoryEntriesLister({ kind: "group", - inspectAccount: (cfg, accountId) => - inspectTelegramAccount({ cfg, accountId }) as InspectedTelegramAccount | null, + resolveAccount: (cfg, accountId) => resolveTelegramDirectoryAccount(cfg, accountId), resolveSources: (account) => [Object.keys(account.config.groups ?? {})], normalizeId: (entry) => entry.trim() || null, }); diff --git a/extensions/whatsapp/directory-contract-api.ts b/extensions/whatsapp/directory-contract-api.ts new file mode 100644 index 00000000000..389f33d5e64 --- /dev/null +++ b/extensions/whatsapp/directory-contract-api.ts @@ -0,0 +1,4 @@ +export { + listWhatsAppDirectoryGroupsFromConfig, + listWhatsAppDirectoryPeersFromConfig, +} from "./src/directory-config.js"; diff --git a/test/helpers/channels/plugins-core-extension-contract.ts b/test/helpers/channels/plugins-core-extension-contract.ts index f1cb1678300..1be328de840 100644 --- a/test/helpers/channels/plugins-core-extension-contract.ts +++ b/test/helpers/channels/plugins-core-extension-contract.ts @@ -6,56 +6,66 @@ import type { } from "../../../src/channels/plugins/types.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; import type { LineProbeResult } from "../../../src/plugin-sdk/line.js"; -import { loadBundledPluginContractApiSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; +import { loadBundledPluginPublicSurfaceSync } from "../../../src/test-utils/bundled-plugin-public-surface.js"; import { withEnvAsync } from "../../../src/test-utils/env.js"; -type DiscordContractApiSurface = Pick< - typeof import("@openclaw/discord/contract-api.js"), +type DiscordDirectoryContractApiSurface = Pick< + typeof import("@openclaw/discord/directory-contract-api.js"), "listDiscordDirectoryPeersFromConfig" | "listDiscordDirectoryGroupsFromConfig" >; type DiscordProbe = import("@openclaw/discord/api.js").DiscordProbe; type DiscordTokenResolution = import("@openclaw/discord/api.js").DiscordTokenResolution; type IMessageProbe = import("@openclaw/imessage/runtime-api.js").IMessageProbe; type SignalProbe = import("@openclaw/signal/api.js").SignalProbe; -type SlackContractApiSurface = Pick< - typeof import("@openclaw/slack/contract-api.js"), +type SlackDirectoryContractApiSurface = Pick< + typeof import("@openclaw/slack/directory-contract-api.js"), "listSlackDirectoryPeersFromConfig" | "listSlackDirectoryGroupsFromConfig" >; type SlackProbe = import("@openclaw/slack/api.js").SlackProbe; -type TelegramContractApiSurface = Pick< - typeof import("@openclaw/telegram/contract-api.js"), +type TelegramDirectoryContractApiSurface = Pick< + typeof import("@openclaw/telegram/directory-contract-api.js"), "listTelegramDirectoryPeersFromConfig" | "listTelegramDirectoryGroupsFromConfig" >; type TelegramProbe = import("@openclaw/telegram/api.js").TelegramProbe; type TelegramTokenResolution = import("@openclaw/telegram/api.js").TelegramTokenResolution; -type WhatsAppContractApiSurface = Pick< - typeof import("@openclaw/whatsapp/contract-api.js"), +type WhatsAppDirectoryContractApiSurface = Pick< + typeof import("@openclaw/whatsapp/directory-contract-api.js"), "listWhatsAppDirectoryPeersFromConfig" | "listWhatsAppDirectoryGroupsFromConfig" >; -let discordContractApi: DiscordContractApiSurface | undefined; -let slackContractApi: SlackContractApiSurface | undefined; -let telegramContractApi: TelegramContractApiSurface | undefined; -let whatsappContractApi: WhatsAppContractApiSurface | undefined; +let discordDirectoryContractApi: DiscordDirectoryContractApiSurface | undefined; +let slackDirectoryContractApi: SlackDirectoryContractApiSurface | undefined; +let telegramDirectoryContractApi: TelegramDirectoryContractApiSurface | undefined; +let whatsappDirectoryContractApi: WhatsAppDirectoryContractApiSurface | undefined; -function getDiscordContractApi(): DiscordContractApiSurface { - discordContractApi ??= loadBundledPluginContractApiSync("discord"); - return discordContractApi; +function loadDirectoryContractApi(pluginId: string): T { + return loadBundledPluginPublicSurfaceSync({ + pluginId, + artifactBasename: "directory-contract-api.js", + }); } -function getSlackContractApi(): SlackContractApiSurface { - slackContractApi ??= loadBundledPluginContractApiSync("slack"); - return slackContractApi; +function getDiscordDirectoryContractApi(): DiscordDirectoryContractApiSurface { + discordDirectoryContractApi ??= + loadDirectoryContractApi("discord"); + return discordDirectoryContractApi; } -function getTelegramContractApi(): TelegramContractApiSurface { - telegramContractApi ??= loadBundledPluginContractApiSync("telegram"); - return telegramContractApi; +function getSlackDirectoryContractApi(): SlackDirectoryContractApiSurface { + slackDirectoryContractApi ??= loadDirectoryContractApi("slack"); + return slackDirectoryContractApi; } -function getWhatsAppContractApi(): WhatsAppContractApiSurface { - whatsappContractApi ??= loadBundledPluginContractApiSync("whatsapp"); - return whatsappContractApi; +function getTelegramDirectoryContractApi(): TelegramDirectoryContractApiSurface { + telegramDirectoryContractApi ??= + loadDirectoryContractApi("telegram"); + return telegramDirectoryContractApi; +} + +function getWhatsAppDirectoryContractApi(): WhatsAppDirectoryContractApiSurface { + whatsappDirectoryContractApi ??= + loadDirectoryContractApi("whatsapp"); + return whatsappDirectoryContractApi; } type DirectoryListFn = (params: { @@ -87,8 +97,8 @@ async function expectDirectoryIds( export function describeDiscordPluginsCoreExtensionContract() { describe("discord plugins-core extension contract", () => { - const listPeers = () => getDiscordContractApi().listDiscordDirectoryPeersFromConfig; - const listGroups = () => getDiscordContractApi().listDiscordDirectoryGroupsFromConfig; + const listPeers = () => getDiscordDirectoryContractApi().listDiscordDirectoryPeersFromConfig; + const listGroups = () => getDiscordDirectoryContractApi().listDiscordDirectoryGroupsFromConfig; it("DiscordProbe satisfies BaseProbeResult", () => { expectTypeOf().toMatchTypeOf(); @@ -188,8 +198,8 @@ export function describeDiscordPluginsCoreExtensionContract() { export function describeSlackPluginsCoreExtensionContract() { describe("slack plugins-core extension contract", () => { - const listPeers = () => getSlackContractApi().listSlackDirectoryPeersFromConfig; - const listGroups = () => getSlackContractApi().listSlackDirectoryGroupsFromConfig; + const listPeers = () => getSlackDirectoryContractApi().listSlackDirectoryPeersFromConfig; + const listGroups = () => getSlackDirectoryContractApi().listSlackDirectoryGroupsFromConfig; it("SlackProbe satisfies BaseProbeResult", () => { expectTypeOf().toMatchTypeOf(); @@ -264,8 +274,9 @@ export function describeSlackPluginsCoreExtensionContract() { export function describeTelegramPluginsCoreExtensionContract() { describe("telegram plugins-core extension contract", () => { - const listPeers = () => getTelegramContractApi().listTelegramDirectoryPeersFromConfig; - const listGroups = () => getTelegramContractApi().listTelegramDirectoryGroupsFromConfig; + const listPeers = () => getTelegramDirectoryContractApi().listTelegramDirectoryPeersFromConfig; + const listGroups = () => + getTelegramDirectoryContractApi().listTelegramDirectoryGroupsFromConfig; it("TelegramProbe satisfies BaseProbeResult", () => { expectTypeOf().toMatchTypeOf(); @@ -359,8 +370,9 @@ export function describeTelegramPluginsCoreExtensionContract() { export function describeWhatsAppPluginsCoreExtensionContract() { describe("whatsapp plugins-core extension contract", () => { - const listPeers = () => getWhatsAppContractApi().listWhatsAppDirectoryPeersFromConfig; - const listGroups = () => getWhatsAppContractApi().listWhatsAppDirectoryGroupsFromConfig; + const listPeers = () => getWhatsAppDirectoryContractApi().listWhatsAppDirectoryPeersFromConfig; + const listGroups = () => + getWhatsAppDirectoryContractApi().listWhatsAppDirectoryGroupsFromConfig; it("lists peers/groups from config", async () => { const cfg = {