refactor(channels): unify account and setup adapters (#114290)

This commit is contained in:
Peter Steinberger
2026-07-27 00:55:46 -04:00
committed by GitHub
parent 40c17e76da
commit 7cce91cda9
8 changed files with 170 additions and 146 deletions

View File

@@ -0,0 +1,43 @@
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import { resolveMSTeamsCredentials } from "./token.js";
export type ResolvedMSTeamsAccount = {
accountId: string;
enabled: boolean;
configured: boolean;
};
export const msteamsMeta = {
id: "msteams",
label: "Microsoft Teams",
selectionLabel: "Microsoft Teams (Bot Framework)",
docsPath: "/channels/msteams",
docsLabel: "msteams",
blurb: "Teams SDK; enterprise support.",
aliases: ["teams"],
order: 60,
} as const;
export const msteamsConfigAdapter = createTopLevelChannelConfigAdapter<
ResolvedMSTeamsAccount,
{
allowFrom?: Array<string | number>;
defaultTo?: string;
}
>({
sectionKey: "msteams",
resolveAccount: (cfg) => ({
accountId: DEFAULT_ACCOUNT_ID,
enabled: cfg.channels?.msteams?.enabled !== false,
configured: Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
}),
resolveAccessorAccount: ({ cfg }) => ({
allowFrom: cfg.channels?.msteams?.allowFrom,
defaultTo: cfg.channels?.msteams?.defaultTo,
}),
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account) => account.defaultTo,
});

View File

@@ -1,60 +1,21 @@
// Msteams plugin module implements channel.setup behavior.
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
msteamsConfigAdapter,
msteamsMeta,
type ResolvedMSTeamsAccount,
} from "./channel-config.js";
import { MSTeamsChannelConfigSchema } from "./config-schema.js";
import { msteamsSetupAdapter, msteamsSetupContract } from "./setup-core.js";
import { msteamsSetupWizard } from "./setup-surface.js";
import { resolveMSTeamsCredentials } from "./token.js";
type ResolvedMSTeamsAccount = {
accountId: string;
enabled: boolean;
configured: boolean;
};
const meta = {
id: "msteams",
label: "Microsoft Teams",
selectionLabel: "Microsoft Teams (Bot Framework)",
docsPath: "/channels/msteams",
docsLabel: "msteams",
blurb: "Teams SDK; enterprise support.",
aliases: ["teams"],
order: 60,
} as const;
const resolveMSTeamsChannelConfig = (cfg: OpenClawConfig) => ({
allowFrom: cfg.channels?.msteams?.allowFrom,
defaultTo: cfg.channels?.msteams?.defaultTo,
});
const msteamsConfigAdapter = createTopLevelChannelConfigAdapter<
ResolvedMSTeamsAccount,
{
allowFrom?: Array<string | number>;
defaultTo?: string;
}
>({
sectionKey: "msteams",
resolveAccount: (cfg) => ({
accountId: "default",
enabled: cfg.channels?.msteams?.enabled !== false,
configured: Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
}),
resolveAccessorAccount: ({ cfg }) => resolveMSTeamsChannelConfig(cfg),
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account) => account.defaultTo,
});
export const msteamsSetupPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
id: "msteams",
meta: {
...meta,
aliases: [...meta.aliases],
...msteamsMeta,
aliases: [...msteamsMeta.aliases],
},
capabilities: {
chatTypes: ["direct", "channel", "thread"],

View File

@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import { MSTeamsConfigSchema } from "../config-api.js";
import { msTeamsApprovalAuth } from "./approval-auth.js";
import { msteamsPlugin } from "./channel.js";
import { msteamsSetupPlugin } from "./channel.setup.js";
function createConfiguredMSTeamsCfg(): OpenClawConfig {
return {
@@ -18,6 +19,58 @@ function createConfiguredMSTeamsCfg(): OpenClawConfig {
}
describe("msteamsPlugin", () => {
it("shares account and metadata contracts with the lightweight setup plugin", () => {
expect(msteamsSetupPlugin.meta).toEqual(msteamsPlugin.meta);
for (const key of [
"listAccountIds",
"resolveAccount",
"defaultAccountId",
"setAccountEnabled",
"deleteAccount",
"resolveAllowFrom",
"formatAllowFrom",
"resolveDefaultTo",
] as const) {
expect(msteamsSetupPlugin.config[key]).toBe(msteamsPlugin.config[key]);
}
});
it("preserves the default account and allowlist across runtime and setup", () => {
const cfg: OpenClawConfig = {
channels: {
msteams: {
...createConfiguredMSTeamsCfg().channels?.msteams,
allowFrom: ["OWNER", " Team.Member "],
defaultTo: "19:team@thread.tacv2",
},
},
};
for (const plugin of [msteamsPlugin, msteamsSetupPlugin]) {
expect(plugin.config.defaultAccountId?.(cfg)).toBe("default");
expect(plugin.config.resolveAccount(cfg, "ignored")).toEqual({
accountId: "default",
enabled: true,
configured: true,
});
expect(plugin.config.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual([
"OWNER",
" Team.Member ",
]);
expect(
plugin.config.formatAllowFrom?.({
cfg,
accountId: "default",
allowFrom: ["OWNER", " Team.Member "],
}),
).toEqual(["owner", "team.member"]);
expect(plugin.config.resolveDefaultTo?.({ cfg, accountId: "default" })).toBe(
"19:team@thread.tacv2",
);
}
});
it("exposes approval auth through approvalCapability", () => {
expect(msteamsPlugin.approvalCapability).toBe(msTeamsApprovalAuth);
});

View File

@@ -1,7 +1,5 @@
// Msteams plugin module implements channel behavior.
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import { createTopLevelChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
import type {
ChannelMessageActionAdapter,
ChannelMessageToolDiscovery,
@@ -43,6 +41,11 @@ import {
PAIRING_APPROVED_MESSAGE,
} from "../runtime-api.js";
import { msTeamsApprovalAuth } from "./approval-auth.js";
import {
msteamsConfigAdapter,
msteamsMeta,
type ResolvedMSTeamsAccount,
} from "./channel-config.js";
import { MSTeamsChannelConfigSchema } from "./config-schema.js";
import { collectMSTeamsMutableAllowlistWarnings } from "./doctor.js";
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
@@ -67,23 +70,6 @@ import { msteamsSetupAdapter, msteamsSetupContract } from "./setup-core.js";
import { msteamsSetupWizard } from "./setup-surface.js";
import { resolveMSTeamsCredentials } from "./token.js";
type ResolvedMSTeamsAccount = {
accountId: string;
enabled: boolean;
configured: boolean;
};
const meta = {
id: "msteams",
label: "Microsoft Teams",
selectionLabel: "Microsoft Teams (Bot Framework)",
docsPath: "/channels/msteams",
docsLabel: "msteams",
blurb: "Teams SDK; enterprise support.",
aliases: ["teams"],
order: 60,
} as const;
const TEAMS_GRAPH_PERMISSION_HINTS: Record<string, string> = {
"ChannelMessage.Read.All": "channel history",
"Chat.Read.All": "chat history",
@@ -118,30 +104,6 @@ const loadMSTeamsChannelRuntime = createLazyRuntimeNamedExport(
"msTeamsChannelRuntime",
);
const resolveMSTeamsChannelConfig = (cfg: OpenClawConfig) => ({
allowFrom: cfg.channels?.msteams?.allowFrom,
defaultTo: cfg.channels?.msteams?.defaultTo,
});
const msteamsConfigAdapter = createTopLevelChannelConfigAdapter<
ResolvedMSTeamsAccount,
{
allowFrom?: Array<string | number>;
defaultTo?: string;
}
>({
sectionKey: "msteams",
resolveAccount: (cfg) => ({
accountId: DEFAULT_ACCOUNT_ID,
enabled: cfg.channels?.msteams?.enabled !== false,
configured: Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
}),
resolveAccessorAccount: ({ cfg }) => resolveMSTeamsChannelConfig(cfg),
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account) => account.defaultTo,
});
function jsonActionResult(data: Record<string, unknown>) {
const text = JSON.stringify(data);
return {
@@ -529,8 +491,8 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
base: {
id: "msteams",
meta: {
...meta,
aliases: [...meta.aliases],
...msteamsMeta,
aliases: [...msteamsMeta.aliases],
},
setupWizard: msteamsSetupWizard,
capabilities: {

View File

@@ -1,18 +1,7 @@
// Slack plugin module implements channel.setup behavior.
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import type { ResolvedSlackAccount } from "./accounts.js";
import {
listSlackAccountIds,
resolveSlackConfigAccessorAccount,
resolveDefaultSlackAccountId,
resolveSlackAccount,
type SlackConfigAccessorAccount,
} from "./accounts.js";
import type { ChannelPlugin } from "./channel-api.js";
import { slackBaseConfigAdapter } from "./config-adapter.js";
import { SlackChannelConfigSchema } from "./config-schema.js";
import {
slackSetupAdapter,
@@ -29,21 +18,6 @@ const slackSetupWizard = createSlackSetupWizardProxy(async () => ({
slackSetupWizard: (await import("./setup-surface.js")).slackSetupWizard,
}));
const slackSetupConfigAdapter = createScopedChannelConfigAdapter<
ResolvedSlackAccount,
SlackConfigAccessorAccount
>({
sectionKey: SLACK_CHANNEL,
listAccountIds: listSlackAccountIds,
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
resolveAccessorAccount: resolveSlackConfigAccessorAccount,
defaultAccountId: resolveDefaultSlackAccountId,
clearBaseFields: ["botToken", "appToken", "userToken", "signingSecret", "name"],
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account) => account.defaultTo,
});
export const slackSetupPlugin: ChannelPlugin<ResolvedSlackAccount> = {
id: SLACK_CHANNEL,
meta: {
@@ -78,7 +52,7 @@ export const slackSetupPlugin: ChannelPlugin<ResolvedSlackAccount> = {
reload: { configPrefixes: ["channels.slack"] },
configSchema: SlackChannelConfigSchema,
config: {
...slackSetupConfigAdapter,
...slackBaseConfigAdapter,
hasConfiguredState: ({ env }) =>
["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some(
(key) => typeof env?.[key] === "string" && env[key]?.trim().length > 0,

View File

@@ -0,0 +1,29 @@
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import {
listSlackAccountIds,
resolveDefaultSlackAccountId,
resolveSlackAccount,
resolveSlackConfigAccessorAccount,
type ResolvedSlackAccount,
type SlackConfigAccessorAccount,
} from "./accounts.js";
import { SLACK_CHANNEL } from "./setup-shared.js";
export const slackBaseConfigAdapter = createScopedChannelConfigAdapter<
ResolvedSlackAccount,
SlackConfigAccessorAccount
>({
sectionKey: SLACK_CHANNEL,
listAccountIds: listSlackAccountIds,
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
resolveAccessorAccount: resolveSlackConfigAccessorAccount,
defaultAccountId: resolveDefaultSlackAccountId,
clearBaseFields: ["botToken", "appToken", "userToken", "signingSecret", "name"],
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account) => account.defaultTo,
});

View File

@@ -1,6 +1,7 @@
// Slack tests cover shared plugin behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it } from "vitest";
import { slackSetupPlugin } from "./channel.setup.js";
import { setSlackChannelAllowlist } from "./setup-shared.js";
import { createSlackPluginBase, slackConfigAdapter } from "./shared.js";
@@ -67,6 +68,24 @@ describe("setSlackChannelAllowlist", () => {
});
describe("slackConfigAdapter", () => {
it("shares lightweight account accessors with setup but keeps inspection runtime-only", () => {
for (const key of [
"listAccountIds",
"resolveAccount",
"defaultAccountId",
"setAccountEnabled",
"deleteAccount",
"resolveAllowFrom",
"formatAllowFrom",
"resolveDefaultTo",
] as const) {
expect(slackSetupPlugin.config[key]).toBe(slackConfigAdapter[key]);
}
expect(slackConfigAdapter.inspectAccount).toBeTypeOf("function");
expect(slackSetupPlugin.config.inspectAccount).toBeUndefined();
});
it("clears user-identity credentials when deleting the root account", () => {
const cfg = {
channels: {
@@ -124,5 +143,9 @@ describe("slackConfigAdapter", () => {
expect(slackConfigAdapter.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual(["U123"]);
expect(slackConfigAdapter.resolveDefaultTo?.({ cfg, accountId: "default" })).toBe("C123");
expect(slackSetupPlugin.config.resolveAllowFrom?.({ cfg, accountId: "default" })).toEqual([
"U123",
]);
expect(slackSetupPlugin.config.resolveDefaultTo?.({ cfg, accountId: "default" })).toBe("C123");
});
});

View File

@@ -1,21 +1,11 @@
// Slack plugin module implements shared behavior.
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
import { formatAllowFromLowercase } from "openclaw/plugin-sdk/allow-from";
import {
adaptScopedAccountAccessor,
createScopedChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers";
import { isSlackPluginAccountConfigured } from "./account-configured.js";
import { inspectSlackAccount } from "./account-inspect.js";
import {
listSlackAccountIds,
resolveSlackConfigAccessorAccount,
resolveDefaultSlackAccountId,
resolveSlackAccount,
type SlackConfigAccessorAccount,
type ResolvedSlackAccount,
} from "./accounts.js";
import type { ResolvedSlackAccount } from "./accounts.js";
import { getChatChannelMeta, type ChannelPlugin } from "./channel-api.js";
import { slackBaseConfigAdapter } from "./config-adapter.js";
import { SlackChannelConfigSchema } from "./config-schema.js";
import { slackDoctor } from "./doctor.js";
import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js";
@@ -27,21 +17,10 @@ export { SLACK_CHANNEL } from "./setup-shared.js";
export { isSlackPluginAccountConfigured };
export const slackConfigAdapter = createScopedChannelConfigAdapter<
ResolvedSlackAccount,
SlackConfigAccessorAccount
>({
sectionKey: SLACK_CHANNEL,
listAccountIds: listSlackAccountIds,
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
resolveAccessorAccount: resolveSlackConfigAccessorAccount,
export const slackConfigAdapter = {
...slackBaseConfigAdapter,
inspectAccount: adaptScopedAccountAccessor(inspectSlackAccount),
defaultAccountId: resolveDefaultSlackAccountId,
clearBaseFields: ["botToken", "appToken", "userToken", "signingSecret", "name"],
resolveAllowFrom: (account) => account.allowFrom,
formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
resolveDefaultTo: (account) => account.defaultTo,
});
};
export function createSlackPluginBase(params: {
setupWizard: NonNullable<ChannelPlugin<ResolvedSlackAccount>["setupWizard"]>;