关于 connector-slack
connector-slack 是一项实验性技能,可通过生成的 slack-client 包为 Caffeine canister 添加 Slack 消息功能。它支持配置 bearer token 后以应用或指定用户身份发消息;响应解码已实现,但尚未经过实时验证。
主要功能
- 01使用 chatPostMessage 发布消息
- 02选择机器人或用户令牌身份
- 03通过管理员权限设置器保存令牌
- 04经由 slack-client 发送认证请求
- 05使用消息相关模块的生成 API
适用场景
- 01向 Slack 频道发送应用通知
- 02以指定用户身份发送消息
- 03为 Caffeine canister 添加 Slack 消息功能
- 04为工作区管理员配置令牌访问
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill connector-slack安装后在 Claude Code 中输入/connector-slack确认已加载。
详细介绍
Slack 连接器(实验性)
从 Caffeine canister 向 Slack 工作区发送消息。
⚠️ 实验性(
[email protected])。 **已通过实时 Slack 验证:**请求路径——真实的chat.postMessage可成功发送消息(此前的查询参数与表单参数错误已修复;POST 参数现放在application/x-www-form-urlencoded请求体中)。 已在 0.1.0 中实现,但尚未进行实时验证:成功响应解码({"ok":true,…}→ 成功架构)以及{"ok":false}错误封装的路由(各自的作用见 已知限制)。因此,响应端代码已完成,但尚未得到验证——已启用diagnostics,所以解码失败时会显示 Slack 的原始响应体。在一次实时运行确认成功回复已解码之前,不要将 Slack 描述为完全受支持的平台功能;这项条件,而非版本号,才是准入门槛。
编排器路由说明
当用户、规范或先前任务提到 Slack、向频道发送消息或通知 Slack 工作区时,加载此技能。生成的 slack-client 包是首选方式;直接使用 ic.http_request 向 https://slack.com/api/* 发送请求属于反模式,因为这会手动重新实现身份验证、百分号编码和 JSON 解析。
意图 → 能力映射:
| 用户意图 | 能力 |
|---|---|
| 以应用身份向 Slack 频道发送消息 | 使用 bot token(xoxb-)调用 slack-client 的 chatPostMessage |
| 以指定人员身份向 Slack 发送消息 | 使用 user token(xoxp-)调用 slack-client 的 chatPostMessage |
生成代码前,告知提出请求的用户应选择哪种 token(bot 或 user——单行规则和权衡见 身份验证模型),并告诉他们如何获取。没有粘贴 token 就无法继续,因此尽早说明可避免应用首次使用时卡住。
token 需要由真人创建,所以后端编译通过并不代表构建完成。只有当应用本身告知管理员如何获取 token,并提供粘贴 token 的位置时,才算完成——具体步骤见 获取 bot token,所需勾选项见 请求的权限范围,频道 ID 见 指定频道,必须交付的页面见 前端。在完成消息中也要重复这些步骤;聊天记录不会比应用或管理员的记忆更长久。
此生成版本的范围:messaging-core 系列中的 chat、conversations、users、files、reactions 和 pins(10 个 API 模块)——各模块的具体划分见 可用 API 范围。
其他 Slack 方法不在范围内,除非重新生成规范。
身份验证模型 — bot token(xoxb-)或 user token(xoxp-)
两者都是 bearer 凭据,客户端对它们的处理方式相同,但工作区看到的发言者身份不同。编写代码前,先询问提出请求的用户想用哪一种,并说明权衡——答案会改变他们的应用在 Slack 中呈现的样子,而且之后无法在不重新安装的情况下切换。
| 用户希望消息显示为…… | Token | 需向用户说明的影响 |
|---|---|---|
应用本身(消息显示应用名称,并带有 APP 徽标) | xoxb- (默认——优先选择此项) | 整个工作区共用一个凭据,不依赖任何员工。bot 必须加入它要发送消息的每个频道(/invite @YourApp),否则 Slack 会返回 not_in_channel。它无法查看自己未加入的私密频道或 DM。 |
| 指定人员(消息显示该人员的姓名和头像) | xoxp- | 每项操作都会归属于该人员,并作为该人员的操作接受审计。可访问该人员能够访问的所有内容,无需邀请加入频道。该人员离开工作区或撤销应用时,token 会失效。少数仅限 user 的 API 需要此 token(例如 search.messages)。 |
如果请求是“从我的应用发送通知/提醒”,应使用 xoxb-。只有当用户明确希望消息看起来像由真人发送,或需要仅限 user 的 API 时,才选择 xoxp-。说明你选择了哪种 token 以及原因。
获取机器人令牌(xoxb-)
- https://api.slack.com/apps → Create New App → From scratch,选择工作区。
- OAuth & Permissions → Scopes → Bot Token Scopes:添加构建所需的权限范围——至少需要
chat:write;其余权限范围见下文的 要请求的权限范围,并务必现在就确定完整列表(之后再添加权限范围会导致必须重新安装)。 - Install to Workspace → 授权 → 复制以
xoxb-开头的 Bot User OAuth Token。 - 在 Slack 中,将应用邀请到每个目标频道:
/invite @YourApp。跳过这一步是最常见的首次失败原因(not_in_channel)。
要请求的权限范围
上面的第 2 步决定了应用能做什么——管理员无法猜出权限范围列表,因此应根据构建实际使用的功能来确定,并在设置界面中准确列出这些权限范围(参见 前端)。Bot 令牌权限范围:
| 应用需要…… | Bot 权限范围 |
|---|---|
| 向已邀请机器人的频道发消息 | chat:write |
| 无需邀请即可向任意公开频道发消息 | chat:write.public |
| 将频道名称解析为 ID、列出公开频道 | channels:read |
| 列出机器人加入的私有频道 | groups:read |
读取消息(conversationsHistory / conversationsReplies) | channels:history,私有频道还需 groups:history |
| 列出用户 / 查找用户 | users:read,使用 usersLookupByEmail 时还需 users:read.email |
给某人发送私信(conversationsOpen → chatPostMessage) | im:write |
| 添加表情回应 | reactions:write |
| 置顶消息 | pins:write |
| 上传 / 读取文件 | files:write、files:read |
⚠️ 之后添加权限范围会导致令牌失效。 Slack 要求任何权限范围变更后都必须重新安装,重新安装会签发一个新的
xoxb-——旧令牌仍可用于已有权限范围,但永远不会获得新权限范围,因此在管理员粘贴新令牌之前,应用会因missing_scope而失败。首次就确定正确的权限范围列表,并让设置页面支持重新粘贴令牌,而不是只能设置一次。
获取用户令牌(xoxp-)
- 使用同一个应用 → OAuth & Permissions → Scopes → User Token Scopes(与 Bot 权限范围列表分开):添加例如
chat:write、search:read等权限范围。 - Install to Workspace(如果应用已存在,则选择 Reinstall)并授权——令牌代表点击 Allow 的用户。
- 复制以
xoxp-开头的 User OAuth Token。
此方案支持由管理员提供单个 xoxp-:它通过同一个 setter 和同一个 config.auth 传入。这里不在范围内的是按用户进行 OAuth,即每位终端用户授权自己的账户——这需要完整的重定向 + 代码交换 + 刷新流程,目前还没有可用的 Slack 辅助工具。请勿尝试自行实现。
将令牌交给 canister
无论使用哪种令牌,工作区管理员都通过一个仅管理员可用的 setter 将其粘贴到 canister 中——访问权限由 AccessControl.hasPermission(state, caller, #admin) 控制。令牌仅由 canister 持有,绝不会返回给前端。
⚠️ 切勿使用“首位调用者认领所有权”的方案来限制 setter。 在 IC 上,所有未通过身份验证的调用者都是同一个匿名 principal,因此,如果匿名调用先认领所有权,那么所有匿名调用者都会通过
caller == owner检查,并能覆盖工作区令牌。请使用授权组件的#admin权限,如下方示例所示。
随后,canister 仅通过 config.auth = ?#bearer(token) 将该令牌交给客户端,每个方法都会将其转换为 Authorization: Bearer … 标头。没有任何方法接受令牌参数,也没有任何方法会将凭据放入 URL,因此凭据不会通过被记录的查询字符串泄露。
外部调用已经是非复制的
defaultConfig 自带 is_replicated = ?false,因此通过记录更新从它派生的任何配置都已经正确——无需记住任何内容,也无需添加任何内容。
不要将其覆盖为 ?true 或 null。复制的外部调用会从子网中的每个节点重复发送请求;对于 Slack,这意味着每个副本都会发布一次消息(约 13 条重复消息),Authorization: Bearer xoxb-…/xoxp-… 标头会从每个节点发出,而且共识无论如何都会失败,因为 Slack 的回复会携带每个请求各自的 ts。
后端
添加依赖项
下面配方中的管理权限门控需要同时引入授权组件和客户端:
mops add [email protected]
mops add [email protected]
生成的函数为
ChatApi.chatPostMessage(config, channel, asUser, attachments, blocks, iconEmoji, iconUrl, linkNames, mrkdwn, parse, replyBroadcast, text, threadTs, unfurlLinks, unfurlMedia, username)。不使用的选项请传入空字符串 / false。令牌不是参数——它仅通过 config.auth 传递(见上文的 Auth model);客户端中的每个方法都是如此。
import AccessControl "mo:caffeineai-authorization/access-control";
import MixinAuthorization "mo:caffeineai-authorization/MixinAuthorization";
import MixinSlackConfig "mixins/slack-config";
import MixinSlackMessaging "mixins/slack-messaging";
actor {
let accessControlState : AccessControl.AccessControlState;
include MixinAuthorization(accessControlState, null);
// Admin-held Slack token, `xoxb-…` or `xoxp-…` — never returned to the frontend.
let slackConfig : { var token : Text };
include MixinSlackConfig(accessControlState, slackConfig);
include MixinSlackMessaging(slackConfig);
};
迁移链的头部:
import AccessControl "mo:caffeineai-authorization/access-control";
module {
type NewActor = {
accessControlState : AccessControl.AccessControlState;
slackConfig : { var token : Text };
};
public func migration(_old : {}) : NewActor {
{
accessControlState = AccessControl.initState();
slackConfig = { var token = "" };
};
};
};
import AccessControl "mo:caffeineai-authorization/access-control";
import Runtime "mo:core/Runtime";
mixin (
accessControlState : AccessControl.AccessControlState,
slackConfig : { var token : Text },
) {
public query func isSlackConfigured() : async Bool {
slackConfig.token.size() > 0;
};
// Admin-only; accepts either token flavour. NOTE: `#admin` — never a
// first-caller-claims-ownership check,
// which the shared anonymous principal would defeat.
public shared ({ caller }) func setSlackToken(token : Text) : async () {
if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {
Runtime.trap("Unauthorized: Only admins can set the Slack token");
};
slackConfig.token := token;
};
};
import Principal "mo:core/Principal";
import Runtime "mo:core/Runtime";
import { chatPostMessage } "mo:slack-client/Apis/ChatApi";
import { defaultConfig; type Config } "mo:slack-client/Config";
mixin (slackConfig : { var token : Text }) {
// Token rides `config.auth`; `defaultConfig` is already non-replicated.
func slackClientConfig(token : Text) : Config {
{
defaultConfig with
auth = ?#bearer(token);
max_response_bytes = ?(1_000_000 : Nat64);
};
};
// Post `text` to `channel` (channel ID like "C012AB3CD" or "#general").
// Returns the posted message timestamp (`ts`).
public shared ({ caller }) func postSlackMessage(channel : Text, text : Text) : async Text {
if (caller.isAnonymous()) Runtime.trap("Sign in to post to Slack");
if (slackConfig.token.size() == 0) {
Runtime.trap("Slack is not configured (an admin must set the token)");
};
let res = await* chatPostMessage(
slackClientConfig(slackConfig.token), // token rides config.auth — never a URL param
channel,
"", // asUser
"", // attachments
"", // blocks
"", // iconEmoji
"", // iconUrl
false, // linkNames
true, // mrkdwn
"", // parse
false, // replyBroadcast
text, // text
"", // threadTs
false, // unfurlLinks
false, // unfurlMedia
"", // username
);
res.ts;
};
};
定位频道——需要使用 ID,且机器人必须加入频道
chatPostMessage 的第一个参数是频道 ID:公共或私有频道使用 C…,私信使用 D…,旧版群组使用 G…。#general 这类名称仍可用于定位公共频道,但已弃用,而且对私有频道始终无效——请优先使用 ID 并将其保存,不要硬编码名称。
用户在哪里可以找到 ID(请将这些说明放在 UI 中,而不只是完成消息里):
- 打开频道 → 点击标题栏中的频道名称 → About 标签页 → ID 位于面板底部(
C012AB3CD),旁边有复制按钮。 - 或者右键点击侧边栏中的频道 → Copy link → ID 是
…/archives/C012AB3CD的最后一段路径。
**Canister 如何获取 ID:**调用 ConversationsApi.conversationsList 并按 name 匹配(需要 channels:read)。解析一次并将 ID 缓存到稳定变量中——不要每次发布消息时都解析;这会使外部调用和 cycles 消耗翻倍。
机器人必须是频道成员,否则 chatPostMessage 会返回 not_in_channel——这是最常见的首次失败原因。按优先顺序,有三种解决办法:
- 由管理员在目标频道中运行
/invite @YourApp; - 授予
chat:write.public,这样机器人无需邀请即可在任何公共频道发布消息; - 调用
conversationsJoin(仅限公共频道,需要channels:join)。
**向某人发送私信:**调用 usersLookupByEmail(需要 users:read.email)或
usersList → 获取用户 ID → 调用 conversationsOpen → 向其返回的 D… 频道发布消息。需要 im:write。
可用的 API 接口
该交付包包含 10 个 API 模块 / 约 140 个操作。只有 chatPostMessage 经过运行时验证(参见 已知限制);其余操作都基于相同的规范生成,并通过类型检查,但其响应解码尚未得到验证。
| 模块 | 用途 | 代表性函数 |
|---|---|---|
ChatApi | 发布、编辑、删除消息及获取永久链接 | chatPostMessage、chatUpdate、chatDelete、chatPostEphemeral、chatScheduleMessage、chatGetPermalink |
ChatScheduledMessagesApi | 定时消息队列 | chatScheduledMessagesList |
ConversationsApi | 频道:列表、读取、成员管理、生命周期 | conversationsList、conversationsHistory、conversationsReplies、conversationsInfo、conversationsOpen、conversationsJoin、conversationsInvite、conversationsCreate |
UsersApi | 用户目录查询、在线状态 | usersList、usersInfo、usersLookupByEmail、usersConversations、usersGetPresence |
UsersProfileApi | 个人资料字段 | usersProfileGet、usersProfileSet |
ReactionsApi | 表情回应 | reactionsAdd、reactionsRemove、reactionsList |
PinsApi | 置顶消息 | pinsAdd、pinsRemove、pinsList |
FilesApi | 文件列表 / 元数据 / 分享 | filesList、filesInfo、filesDelete |
FilesCommentsApi | 文件评论 | filesCommentsDelete |
FilesRemoteApi | 外部文件注册表 | filesRemoteAdd、filesRemoteInfo、filesRemoteList |
此客户端无法进行二进制文件上传 / 下载(multipart 请求体不属于生成的 JSON / 表单接口范围)。受支持的替代方案是使用 filesRemoteAdd 注册存放在外部 URL 的文件。
前端
Slack 不需要 OAuth 回调:凭据是管理员粘贴的长期令牌,因此无需重定向 URI、无需 /connect/slack 路由,也无需逐用户握手。不要构建这些功能。Slack 版本必须提供一个页面,让管理员获取并输入令牌——这些是验收标准,不是建议,缺少这些内容的版本属于损坏,而非仅仅不完整:
- 令牌页面必须存在且可访问。 如果“发布到 Slack”功能没有输入令牌的页面,就无法使用。已登录的管理员必须能从导航栏或“尚未配置”提示访问
/settings/slack。 - 令牌生成步骤必须显示在 UI 中,不能只出现在 Agent 的聊天回复中。管理员可能在聊天结束数周后才返回此页面。
- 必须显示确切的作用域列表。 管理员无法推断此应用需要哪些作用域;请显示此版本实际使用的作用域(要请求的作用域)。
-
必须提供登录流程。
setSlackToken会检查#admin,因此应用需要非匿名调用方。请参考extension-authorization中的登录、useInternetIdentity/useActor配置及管理员角色门控。 -
管理员设置页 —
/settings/slack(仅管理员可访问)。要求:- 在输入框上方放置「如何获取 Slack 机器人令牌」面板,将其描述为一次性、约需 5 分钟的设置,并包含以下编号步骤(Agent 的完成消息必须逐字重复这些步骤):
- 打开 https://api.slack.com/apps → Create New App → From scratch → 为应用命名并选择工作区;
- OAuth & Permissions → Scopes → Bot Token Scopes → 添加本页列出的权限范围;
- Install to Workspace → Allow → 复制 Bot User OAuth Token(令牌以
xoxb-开头); - 在下方粘贴并保存;
- 在 Slack 中,针对应用要发消息的每个频道运行
/invite @YourApp。 添加一个便捷链接,打开 https://api.slack.com/apps。
- 将权限范围列表呈现为可复制文本 — 仅列出此版本实际使用的权限范围,不要列出完整表格。
- 一个绑定到
setSlackToken(token)的密码输入框。按回车提交,成功后清空,并允许重新粘贴:权限范围变更后需要重新安装并获取新令牌,因此这不是一次性表单。 - 状态由
isSlackConfigured()(Bool)驱动 — 显示「已配置」/「未配置」。绝不回显令牌,即使是掩码加后缀的形式也不行。 - 如果应用会向固定频道发消息,该频道 ID 也应设置在此页:将配置 mixin 扩展为
{ var token : Text; var channel : Text },并添加仅管理员可调用的setSlackChannel;在输入框旁内嵌「在哪里查找频道 ID」提示(参见 指定频道)。不要让用户在没有说明 ID 来源的情况下输入原始 ID。 - 确保此页面可访问。 当
isCallerAdmin为 true 时,共享 Layout 导航栏必须链接到此页,否则隐藏该链接。在定义导航栏的位置添加链接,不要将其放在此页面内部。
- 在输入框上方放置「如何获取 Slack 机器人令牌」面板,将其描述为一次性、约需 5 分钟的设置,并包含以下编号步骤(Agent 的完成消息必须逐字重复这些步骤):
-
空状态提示。 当
isSlackConfigured()为false时,绝不显示无效的「发送到 Slack」按钮:管理员显示指向/settings/slack的「设置 Slack」链接;非管理员显示说明,例如「尚未设置 Slack — 需要管理员在设置中添加工作区令牌。」 -
翻译 Slack 错误。 逻辑失败会以 被拒绝的调用形式返回,消息中包含 Slack 自身的
error字符串(参见 已知限制)。至少将以下三种错误映射为操作提示,而不是显示原始拒绝消息:not_in_channel→「将应用邀请到频道:/invite @YourApp」invalid_auth/not_authed→「Slack 令牌无效 — 请在设置中粘贴新令牌」(管理员显示链接)channel_not_found→「检查频道 ID」(附带查找方法提示)missing_scope→「应用需要添加另一个 Slack 权限范围 — 添加后重新安装,并粘贴新令牌」
建议的路由布局:
/ → Main UI (any signed-in user; empty-state when unconfigured)
/settings/slack → Admin token + default channel (admin-only)
# No /connect/slack: Slack uses a pasted long-lived token, not a redirect flow.
编写者必须告知 Caffeine 用户的内容
在人工创建 Slack 应用并粘贴令牌之前,应用无法运行,因此,编写者中的完成消息是交付内容的一部分,而不是对交付内容的总结。它必须按以下顺序包含:
- 说明需要令牌,以及由谁输入——管理员在
/settings/slack页面输入;登录后可从导航栏访问该页面。 - 逐字写出 Frontend 第 2 项中的五个编号步骤:在 https://api.slack.com/apps 创建应用,添加机器人权限范围,选择 Install to Workspace,复制
xoxb-…令牌,粘贴令牌,然后在每个目标频道中执行/invite @YourApp。 - 列出此版本所需的确切权限范围,取自 Scopes to request——以用户可直接复制到 Slack 权限范围选择器中的文本形式提供,而不是对其进行描述。
- 说明如何查找频道 ID——提供 Addressing a channel 中的 About 选项卡/复制链接步骤——每当用户需要指定频道时都要说明。
- 故障排查对应表,每项一行:
not_in_channel→ 邀请应用;invalid_auth→ 重新粘贴令牌;channel_not_found→ 检查 ID;missing_scope→ 添加权限范围、重新安装、粘贴新令牌。
不要把这些内容压缩成“在设置中配置 Slack”,也不要用 Slack 文档链接替代。用户正处于构建过程中,很可能从未见过 Slack 应用控制面板,而编写者就是他们正在查看的地方。此处的措辞应与设置页面面板中的措辞一致,避免两处内容不一致。
已知限制(实验性)
{"ok": false}会作为被拒绝的调用报告。 Slack 会通过 HTTP 200 返回{"ok": false, "error": "…"}来表示逻辑失败(令牌错误、缺少权限范围、找不到频道)。从 0.1.0 开始,客户端会检测到这种情况,并将其送入错误处理路径,因此拒绝消息会显示 Slack 自己的error字符串(例如not_authed、invalid_auth、channel_not_found),而不是解码失败。应将其作为被拒绝的调用处理:try { … } catch (e) { Error.message(e) }。不要编写if (res.ok) …:返回值已经解码,因此其中的ok始终为true,这项检查是无效代码。将这些情况转换为#ok/#err返回值会改变每个方法的签名,因此要等到重大版本更新时再处理。- 可达性(IPv4)——无需代理。
slack.com仅支持 IPv4,过去因此无法通过 IC HTTPS 出站调用访问。自 2025-08-04 起,IC 会尝试直接连接(IPv6),失败时自动通过 IC 管理的 SOCKS 代理重试,因此仅支持 IPv4 的主机也可以正常工作:将config.baseUrl保持为默认值https://slack.com/api。TLS 会话在节点与 Slack 之间端到端加密,因此代理只能看到密文。备用路径可能会增加一些延迟(非复制出站调用本身也较慢——见上文)。 - 所有方法均通过标头进行身份验证。 令牌仅通过
Authorization: Bearer标头传递——绝不会放在 URL 中,因此不会出现在记录的查询字符串里,也绝不会作为方法参数传递。 - 运行时验证不完整。 请求结构已通过真实 Slack 验证(实际消息已成功发送);成功响应的解码尚未经过运行时确认——已启用
diagnostics,因此任何解码失败都会显示 Slack 的原始响应正文。该架构也是根据一份归档的(约 2020 年)规范修订版生成的。
相关内容
mops add [email protected]— 生成的 Slack Web API 绑定。- Slack Web API 参考 — 每个方法、其权限范围及错误字符串。
chat.postMessage— 唯一经过运行时验证的路径;其channel/text/blocks语义。- Slack 令牌类型 — 机器人令牌(
xoxb-)与用户令牌(xoxp-)的区别,以及重新安装对它们的影响。 - Slack 应用管理 — 管理员在此创建应用、设置权限范围并将其安装到工作区。
- extension-authorization — 必需的前置条件。Internet Identity 登录、
useInternetIdentity/useActor接线,以及令牌设置器所需的#admin角色门控。