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 { inspectSlackAccount } from "./account-inspect.js"; import { listSlackAccountIds, resolveSlackConfigAccessorAccount, resolveDefaultSlackAccountId, resolveSlackAccount, type SlackConfigAccessorAccount, type ResolvedSlackAccount, } from "./accounts.js"; import { getChatChannelMeta, type ChannelPlugin } from "./channel-api.js"; import { SlackChannelConfigSchema } from "./config-schema.js"; import { slackDoctor } from "./doctor.js"; import { isSlackInteractiveRepliesEnabled } from "./interactive-replies.js"; import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js"; import { slackSecurityAdapter } from "./security.js"; import { SLACK_CHANNEL } from "./setup-shared.js"; export { setSlackChannelAllowlist, SLACK_CHANNEL } from "./setup-shared.js"; export function isSlackPluginAccountConfigured(account: ResolvedSlackAccount): boolean { const mode = account.config.mode ?? "socket"; const hasBotToken = Boolean(account.botToken?.trim()); if (!hasBotToken) { return false; } if (mode === "http") { return Boolean(account.config.signingSecret?.trim()); } return Boolean(account.appToken?.trim()); } export const slackConfigAdapter = createScopedChannelConfigAdapter< ResolvedSlackAccount, SlackConfigAccessorAccount >({ sectionKey: SLACK_CHANNEL, listAccountIds: listSlackAccountIds, resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount), resolveAccessorAccount: resolveSlackConfigAccessorAccount, inspectAccount: adaptScopedAccountAccessor(inspectSlackAccount), defaultAccountId: resolveDefaultSlackAccountId, clearBaseFields: ["botToken", "appToken", "name"], resolveAllowFrom: (account) => account.allowFrom, formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }), resolveDefaultTo: (account) => account.defaultTo, }); export function createSlackPluginBase(params: { setupWizard: NonNullable["setupWizard"]>; setup: NonNullable["setup"]>; }): Pick< ChannelPlugin, | "id" | "meta" | "setupWizard" | "capabilities" | "commands" | "doctor" | "agentPrompt" | "streaming" | "reload" | "configSchema" | "config" | "setup" | "security" | "secrets" > { return { id: SLACK_CHANNEL, meta: { ...getChatChannelMeta(SLACK_CHANNEL), preferSessionLookupForAnnounceTarget: true, }, setupWizard: params.setupWizard, capabilities: { chatTypes: ["direct", "channel", "thread"], reactions: true, threads: true, media: true, nativeCommands: true, }, commands: { nativeCommandsAutoEnabled: false, nativeSkillsAutoEnabled: false, resolveNativeCommandName: ({ commandKey, defaultName }) => commandKey === "status" ? "agentstatus" : defaultName, }, doctor: slackDoctor, agentPrompt: { inboundFormattingHints: () => ({ text_markup: "slack_mrkdwn", rules: [ "Use Slack mrkdwn, not standard Markdown.", "Bold uses *single asterisks*.", "Links use .", "Code blocks use triple backticks without a language identifier.", "Do not use markdown headings or pipe tables.", ], }), messageToolHints: ({ cfg, accountId }) => isSlackInteractiveRepliesEnabled({ cfg, accountId }) ? [ "- Prefer Slack buttons/selects for 2-5 discrete choices or parameter picks instead of asking the user to type one.", "- Slack interactive replies: use `[[slack_buttons: Label:value, Other:other]]` to add action buttons that route clicks back as Slack interaction system events.", "- Slack selects: use `[[slack_select: Placeholder | Label:value, Other:other]]` to add a static select menu that routes the chosen value back as a Slack interaction system event.", ] : [ "- Slack interactive replies are disabled. If needed, ask to set `channels.slack.capabilities.interactiveReplies=true` (or the same under `channels.slack.accounts..capabilities`).", ], }, streaming: { blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 }, }, reload: { configPrefixes: ["channels.slack"] }, security: slackSecurityAdapter, configSchema: SlackChannelConfigSchema, config: { ...slackConfigAdapter, hasConfiguredState: ({ env }) => ["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some( (key) => typeof env?.[key] === "string" && env[key]?.trim().length > 0, ), isConfigured: (account) => isSlackPluginAccountConfigured(account), describeAccount: (account) => describeAccountSnapshot({ account, configured: isSlackPluginAccountConfigured(account), extra: { botTokenSource: account.botTokenSource, appTokenSource: account.appTokenSource, }, }), }, secrets: { secretTargetRegistryEntries, collectRuntimeConfigAssignments, }, setup: params.setup, } as Pick< ChannelPlugin, | "id" | "meta" | "setupWizard" | "capabilities" | "commands" | "doctor" | "agentPrompt" | "streaming" | "reload" | "configSchema" | "config" | "setup" | "security" | "secrets" >; }