mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-06 02:21:46 +00:00
* feat(channels): add channel-owned setup contracts * test(channels): align legacy setup fixtures * chore(channels): regenerate config and SDK baselines after rebase * fix(update): run fresh doctor after current-process core changes * fix(channels): align add pre-scan with execution precedence * style(cli): format channels-cli test additions * fix(channels): restore option-before-positional channel resolution via metadata arity scan * fix(channels): keep help flags out of metadata arity escalation * test(update): mock fresh post-update doctor in current-process suites * style: format review fixes and correct entrypoint mock type * fix(channels): register only modern contract options for dual-publishing plugins * test(update): align downgrade suites with fresh-doctor child invocation * docs(channels): record empty-contract and input-forwarding invariants * fix(line): keep the shipped --token switch as a channel access token alias * fix(signal): stop treating exact cross-family loopback endpoints as bind-aligned * chore(config): regenerate docs config baselines after second rebase * style: format rebased channels add tests * fix(channels): enforce field-key and flag-name agreement in setup contracts * fix(signal): detect container endpoints for bare --http-url setup * fix(signal): ignore unconfigured accounts in transport collision checks * fix(channels): validate negated setup flags in contract and normalizer * fix(signal): preserve existing transport kind when setup detection is unreachable * style(signal): use direct boolean check in collision guard * style(signal): type test config literals * docs(update): record two-read design of fresh-doctor validation gate * fix(channels): satisfy post-rebase architecture gates * docs: refresh channel setup map --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
165 lines
6.5 KiB
TypeScript
165 lines
6.5 KiB
TypeScript
// 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 { isSlackPluginAccountConfigured } from "./account-configured.js";
|
|
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 { SLACK_CHANNEL } from "./setup-shared.js";
|
|
|
|
export { isSlackPluginAccountConfigured };
|
|
|
|
export const slackConfigAdapter = createScopedChannelConfigAdapter<
|
|
ResolvedSlackAccount,
|
|
SlackConfigAccessorAccount
|
|
>({
|
|
sectionKey: SLACK_CHANNEL,
|
|
listAccountIds: listSlackAccountIds,
|
|
resolveAccount: adaptScopedAccountAccessor(resolveSlackAccount),
|
|
resolveAccessorAccount: resolveSlackConfigAccessorAccount,
|
|
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"]>;
|
|
setup: NonNullable<ChannelPlugin<ResolvedSlackAccount>["setup"]>;
|
|
setupContract?: NonNullable<ChannelPlugin<ResolvedSlackAccount>["setupContract"]>;
|
|
}): Pick<
|
|
ChannelPlugin<ResolvedSlackAccount>,
|
|
| "id"
|
|
| "meta"
|
|
| "setupWizard"
|
|
| "capabilities"
|
|
| "commands"
|
|
| "doctor"
|
|
| "agentPrompt"
|
|
| "streaming"
|
|
| "reload"
|
|
| "configSchema"
|
|
| "config"
|
|
| "setup"
|
|
| "setupContract"
|
|
| "security"
|
|
| "secrets"
|
|
> {
|
|
return {
|
|
id: SLACK_CHANNEL,
|
|
meta: {
|
|
...getChatChannelMeta(SLACK_CHANNEL),
|
|
preferSessionLookupForAnnounceTarget: true,
|
|
},
|
|
setupWizard: params.setupWizard,
|
|
...(params.setupContract ? { setupContract: params.setupContract } : {}),
|
|
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 <url|label>.",
|
|
"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.<account>.capabilities`).",
|
|
]
|
|
).concat([
|
|
"- For line, bar, area, or pie data, use a `presentation` chart block; Slack renders it as a native chart and retains a text data summary for accessibility.",
|
|
"- For row-and-column data, use an explicit `presentation` table block; Slack renders it as a native table and retains a linear text summary for accessibility. Markdown pipe tables are not auto-promoted.",
|
|
"- Slack plain text sends: write standard Markdown; OpenClaw converts it to Slack mrkdwn, including `**bold**`, headings, lists, and `[label](url)` links.",
|
|
"- When mentioning Slack users, use the stable `<@USER_ID>` token from Slack context instead of plain `@name` text so Slack notifies and links the user.",
|
|
"- Slack Block Kit or presentation text fields are sent as Slack mrkdwn directly; use `*bold*`, `_italic_`, `~strike~`, `<url|label>` links, and avoid Markdown headings or pipe tables there.",
|
|
]),
|
|
},
|
|
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<ResolvedSlackAccount>,
|
|
| "id"
|
|
| "meta"
|
|
| "setupWizard"
|
|
| "capabilities"
|
|
| "commands"
|
|
| "doctor"
|
|
| "agentPrompt"
|
|
| "streaming"
|
|
| "reload"
|
|
| "configSchema"
|
|
| "config"
|
|
| "setup"
|
|
| "security"
|
|
| "secrets"
|
|
>;
|
|
}
|