mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 12:31:32 +00:00
refactor(channels): unify account and setup adapters (#114290)
This commit is contained in:
committed by
GitHub
parent
40c17e76da
commit
7cce91cda9
43
extensions/msteams/src/channel-config.ts
Normal file
43
extensions/msteams/src/channel-config.ts
Normal 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,
|
||||
});
|
||||
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
29
extensions/slack/src/config-adapter.ts
Normal file
29
extensions/slack/src/config-adapter.ts
Normal 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,
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"]>;
|
||||
|
||||
Reference in New Issue
Block a user