mcpField
C
connector-slack

connector-slack

github.com/caffeinelabs生产力214.8k 次安装6 天前更新
代码仓库

关于 connector-slack

connector-slack 是一项实验性技能,可通过生成的 slack-client 包为 Caffeine canister 添加 Slack 消息功能。它支持配置 bearer token 后以应用或指定用户身份发消息;响应解码已实现,但尚未经过实时验证。

主要功能

  1. 01使用 chatPostMessage 发布消息
  2. 02选择机器人或用户令牌身份
  3. 03通过管理员权限设置器保存令牌
  4. 04经由 slack-client 发送认证请求
  5. 05使用消息相关模块的生成 API

适用场景

  1. 01向 Slack 频道发送应用通知
  2. 02以指定用户身份发送消息
  3. 03为 Caffeine canister 添加 Slack 消息功能
  4. 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-)
  1. https://api.slack.com/apps → Create New App → From scratch,选择工作区。
  2. OAuth & Permissions → Scopes → Bot Token Scopes:添加构建所需的权限范围——至少需要 chat:write;其余权限范围见下文的 要请求的权限范围,并务必现在就确定完整列表(之后再添加权限范围会导致必须重新安装)。
  3. Install to Workspace → 授权 → 复制以 xoxb- 开头的 Bot User OAuth Token。
  4. 在 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-)
  1. 使用同一个应用 → OAuth & Permissions → Scopes → User Token Scopes(与 Bot 权限范围列表分开):添加例如 chat:write、search:read 等权限范围。
  2. Install to Workspace(如果应用已存在,则选择 Reinstall)并授权——令牌代表点击 Allow 的用户。
  3. 复制以 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——这是最常见的首次失败原因。按优先顺序,有三种解决办法:

  1. 由管理员在目标频道中运行 /invite @YourApp;
  2. 授予 chat:write.public,这样机器人无需邀请即可在任何公共频道发布消息;
  3. 调用 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 的聊天回复中。管理员可能在聊天结束数周后才返回此页面。
  • 必须显示确切的作用域列表。 管理员无法推断此应用需要哪些作用域;请显示此版本实际使用的作用域(要请求的作用域)。
  1. 必须提供登录流程。 setSlackToken 会检查 #admin,因此应用需要非匿名调用方。请参考 extension-authorization 中的登录、useInternetIdentity / useActor 配置及管理员角色门控。

  2. 管理员设置页 — /settings/slack(仅管理员可访问)。要求:

    • 在输入框上方放置「如何获取 Slack 机器人令牌」面板,将其描述为一次性、约需 5 分钟的设置,并包含以下编号步骤(Agent 的完成消息必须逐字重复这些步骤):
      1. 打开 https://api.slack.com/apps → Create New App → From scratch → 为应用命名并选择工作区;
      2. OAuth & Permissions → Scopes → Bot Token Scopes → 添加本页列出的权限范围;
      3. Install to Workspace → Allow → 复制 Bot User OAuth Token(令牌以 xoxb- 开头);
      4. 在下方粘贴并保存;
      5. 在 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 导航栏必须链接到此页,否则隐藏该链接。在定义导航栏的位置添加链接,不要将其放在此页面内部。
  3. 空状态提示。 当 isSlackConfigured() 为 false 时,绝不显示无效的「发送到 Slack」按钮:管理员显示指向 /settings/slack 的「设置 Slack」链接;非管理员显示说明,例如「尚未设置 Slack — 需要管理员在设置中添加工作区令牌。」

  4. 翻译 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 应用并粘贴令牌之前,应用无法运行,因此,编写者中的完成消息是交付内容的一部分,而不是对交付内容的总结。它必须按以下顺序包含:

  1. 说明需要令牌,以及由谁输入——管理员在 /settings/slack 页面输入;登录后可从导航栏访问该页面。
  2. 逐字写出 Frontend 第 2 项中的五个编号步骤:在 https://api.slack.com/apps 创建应用,添加机器人权限范围,选择 Install to Workspace,复制 xoxb-… 令牌,粘贴令牌,然后在每个目标频道中执行 /invite @YourApp。
  3. 列出此版本所需的确切权限范围,取自 Scopes to request——以用户可直接复制到 Slack 权限范围选择器中的文本形式提供,而不是对其进行描述。
  4. 说明如何查找频道 ID——提供 Addressing a channel 中的 About 选项卡/复制链接步骤——每当用户需要指定频道时都要说明。
  5. 故障排查对应表,每项一行: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 角色门控。

评论

支持 Markdown