connector-slack
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSlack Connector (experimental)
Slack 连接器(实验性)
Post messages to a Slack workspace from a Caffeine canister.
⚠️ Experimental (). The request path is fixed and verified — a realslack-client@0.0.3posts successfully against the live Slack API (the earlier query-vs-form bug is resolved; POST params now go in anchat.postMessagebody). NOT yet runtime-verified: the success-response decode (application/x-www-form-urlencoded→ the success schema) and the{"ok":true,…}error envelope (see Known limitations). Treat as pre-release; don't advertise Slack as fully supported until a >= 0.1.0 release.{"ok":false}
从Caffeine canister向Slack工作区发送消息。
⚠️ 实验性版本()。请求路径已固定并验证——真实的slack-client@0.0.3调用可成功对接Slack实时API(此前的查询与表单Bug已修复;POST参数现在会放入chat.postMessage请求体中)。尚未经过运行时验证的内容包括:成功响应的解码逻辑(application/x-www-form-urlencoded→ 成功响应 schema)以及{"ok":true,…}错误包(参见已知限制)。请将其视为预发布版本;在发布>=0.1.0版本之前,不要宣传Slack为完全支持的功能。{"ok":false}
Orchestrator routing notes
编排器路由说明
Load this skill when the user, spec, or a prior task mentions Slack, posting to a
channel, or notifying a Slack workspace. The generated package is
the preferred path; raw to is an
anti-pattern that re-implements auth, percent-encoding, and JSON parsing by hand.
slack-clientic.http_requesthttps://slack.com/api/*Intent → capability mapping:
| User intent | Capability |
|---|---|
| Post a message to a Slack channel as the app | |
| Post to Slack as a named person | |
Before generating code, report the token choice back to the prompting user
(bot vs user — see Auth model for the one-line rule and the trade-offs) and
tell them where to obtain it. They cannot proceed without pasting a token, so
surfacing this early avoids an app that traps on first use.
Scope of this generated drop: the messaging-core families ,
, , , , and (10 API modules).
Other Slack methods are out of scope until the spec is regenerated.
chatconversationsusersfilesreactionspins当用户、需求规格或前置任务提及Slack、向频道发布消息或通知Slack工作区时,加载此技能。生成的包是首选方案;直接使用调用是反模式,因为这需要手动重新实现认证、百分编码和JSON解析逻辑。
slack-clientic.http_requesthttps://slack.com/api/*意图→能力映射:
| 用户意图 | 对应能力 |
|---|---|
| 以应用身份向Slack频道发送消息 | 使用机器人令牌( |
| 以指定用户身份发布到Slack | 使用用户令牌( |
在生成代码之前,请向发起请求的用户确认令牌类型选择(机器人令牌还是用户令牌——参见认证模型中的简单规则和权衡点),并告知他们如何获取令牌。用户必须粘贴令牌才能继续,因此提前告知这一点可以避免应用在首次使用时陷入停滞。
此生成版本的覆盖范围:消息核心系列的、、、、和模块(共10个API模块)。其他Slack方法目前不在范围内,直到规格重新生成后才会支持。
chatconversationsusersfilesreactionspinsAuth model — bot token (xoxb-
) or user token (xoxp-
)
xoxb-xoxp-认证模型——机器人令牌(xoxb-
)或用户令牌(xoxp-
)
xoxb-xoxp-Both are bearer credentials and the client treats them identically, but they
differ in who the workspace sees acting. Ask the prompting user which one they
want before writing code, and state the trade-off — the answer changes what
their app looks like in Slack, and it cannot be swapped later without a
re-install.
| The user wants messages to appear as… | Token | Consequences to report back |
|---|---|---|
the app itself (posts show the app's name with an | | One workspace-wide credential, independent of any employee. The bot must be invited to every channel it posts in ( |
| a specific person (posts show that human's name and avatar) | | Every action is attributed to, and audited as, that person. Reaches whatever they can reach, no channel invite needed. Dies when they leave the workspace or revoke the app. Required for a few user-only APIs (e.g. |
If the request is "post notifications/alerts from my app", that is . Only
choose when the user explicitly wants messages to look like they came
from a human, or needs a user-only API. Say which you picked and why.
xoxb-xoxp-两者均为Bearer凭证,客户端对它们的处理方式相同,但工作区中显示的操作主体不同。在编写代码之前,请询问发起请求的用户想要使用哪种令牌,并说明权衡点——选择会影响应用在Slack中的显示方式,且后续无法在不重新安装的情况下更换令牌。
| 用户希望消息显示为… | 令牌类型 | 需要告知用户的后果 |
|---|---|---|
应用本身(消息显示应用名称并带有 | | 一个工作区范围内的凭证,独立于任何员工。机器人必须被邀请到每个要发布消息的频道( |
| 特定用户(消息显示该用户的姓名和头像) | | 所有操作都归属于该用户并被审计。可以访问该用户能访问的所有内容,无需频道邀请。当该用户离开工作区或撤销应用权限时,令牌失效。部分仅用户可用的API(如 |
如果需求是“从我的应用发送通知/警报”,则应使用令牌。仅当用户明确希望消息看起来来自人类,或需要使用仅用户可用的API时,才选择令牌。请说明你选择的类型及原因。
xoxb-xoxp-Obtaining a bot token (xoxb-
)
xoxb-获取机器人令牌(xoxb-
)
xoxb-- https://api.slack.com/apps → Create New App → From scratch, pick the workspace.
- OAuth & Permissions → Scopes → Bot Token Scopes: add (plus
chat:write,channels:read, … only as needed).reactions:write - Install to Workspace → authorize → copy the Bot User OAuth Token,
which starts with .
xoxb- - In Slack, invite the app to each target channel: . Skipping this is the most common first failure (
/invite @YourApp).not_in_channel
- 访问https://api.slack.com/apps → Create New App(创建新应用)→ From scratch(从头开始),选择目标工作区。
- 进入OAuth & Permissions(OAuth与权限)→ Scopes(权限范围)→ Bot Token Scopes(机器人令牌权限):添加(根据需要可额外添加
chat:write、channels:read等)。reactions:write - 点击Install to Workspace(安装到工作区)→ 授权 → 复制Bot User OAuth Token(机器人用户OAuth令牌),该令牌以开头。
xoxb- - 在Slack中,将应用邀请到每个目标频道:。跳过此步骤是最常见的首次使用失败原因(会返回
/invite @YourApp错误)。not_in_channel
Obtaining a user token (xoxp-
)
xoxp-获取用户令牌(xoxp-
)
xoxp-- Same app → OAuth & Permissions → Scopes → User Token Scopes (a
separate list from bot scopes): add e.g. ,
chat:write.search:read - Install to Workspace (or Reinstall, if the app already exists) and authorize — the token represents whoever clicks Allow.
- Copy the User OAuth Token, which starts with .
xoxp-
A single admin-supplied is supported by this recipe: it goes through the
same setter and the same . What is out of scope here is per-user
OAuth, i.e. each end-user authorising their own account — that needs a full
redirect + code-exchange + refresh flow, for which no Slack helper exists yet.
Do not attempt to hand-roll it.
xoxp-config.auth- 在同一个应用中 → OAuth & Permissions(OAuth与权限)→ Scopes(权限范围)→ User Token Scopes(用户令牌权限)(与机器人权限范围是独立列表):添加例如、
chat:write等权限。search:read - 点击Install to Workspace(安装到工作区)(如果应用已存在则点击Reinstall重新安装)并授权——令牌代表点击“允许”的用户。
- 复制User OAuth Token(用户OAuth令牌),该令牌以开头。
xoxp-
此方案支持由管理员提供的单个令牌:它通过相同的设置器和传递。不在范围内的是每个终端用户授权自己账户的OAuth流程,即每个终端用户都需要完成完整的重定向+代码交换+刷新流程,目前尚无Slack辅助工具支持此流程,请不要尝试手动实现。
xoxp-config.authHanding the token to the canister
向canister传递令牌
Whichever flavour, the workspace admin pastes it into the canister through an
admin-gated setter — gated on
. The token is held by the
canister only and is never returned to the frontend.
AccessControl.hasPermission(state, caller, #admin)⚠️ Never gate the setter on a first-caller-claims-ownership scheme. On the IC every unauthenticated caller is the same anonymous principal, so if an anonymous call claims ownership first, every anonymous caller passes thecheck and can overwrite the workspace token. Use the authorization component'scaller == ownerpermission, as the example below does.#admin
The canister then hands that token to the client only through
, which every method turns into an
header. No method takes a token argument and no method
puts the credential in the URL, so it cannot leak through a logged query string.
config.auth = ?#bearer(token)Authorization: Bearer …无论使用哪种令牌,工作区管理员都需要通过管理员权限 gated的设置器将其粘贴到canister中——权限验证基于。令牌仅由canister持有,绝不会返回给前端。
AccessControl.hasPermission(state, caller, #admin)⚠️ 绝不要使用“先调用者拥有所有权”的方案来限制设置器权限。在IC(互联网计算机)上,所有未认证的调用者都是同一个匿名主体,因此如果匿名调用者首先声明所有权,那么所有匿名调用者都能通过检查,并可以覆盖工作区令牌。请使用授权组件的caller == owner权限,如下例所示。#admin
canister随后仅通过将令牌传递给客户端,每个方法都会将其转换为请求头。没有任何方法会将令牌作为参数接收,也不会将凭证放入URL中,因此令牌不会通过日志中的查询字符串泄露。
config.auth = ?#bearer(token)Authorization: Bearer …is_replicated = ?false
is REQUIRED
is_replicated = ?false必须设置is_replicated = ?false
is_replicated = ?falseEvery Slack outcall must set on its :
is_replicated = ?falseConfig- Security. A replicated outcall repeats the request from every node in the
subnet, each carrying the /
Authorization: Bearer xoxb-…header — a leak from any node compromises the workspace token.xoxp-… - Billing & side effects. Replicated outcalls fan out to N identical API calls: ~13× the cycles and N duplicate messages posted to the channel.
- Determinism. Slack responses carry per-request fields (message ), so replicated consensus would fail; non-replicated bypasses consensus.
ts
每个Slack出站调用都必须在其中设置:
Configis_replicated = ?false- 安全性。复制式出站调用会从子网中的每个节点重复发送请求,每个请求都携带/
Authorization: Bearer xoxb-…请求头——任何节点泄露令牌都会危及工作区安全。xoxp-… - 计费与副作用。复制式出站调用会扩散为N个相同的API调用:消耗约13倍的cycles,并向频道发送N条重复消息。
- 确定性。Slack响应包含每个请求独有的字段(消息),因此复制式共识会失败;非复制式调用会绕过共识。
ts
Backend
后端
Add dependencies
添加依赖
The admin gate in the recipe below needs the authorization component alongside
the client:
bash
mops add slack-client@0.0.3
mops add caffeineai-authorization@1.0.1The generated function is
. Pass empty strings / for the
options you don't use. The token is not an argument — it travels only in
(see Auth model above); this holds for every method in the
client.
ChatApi.chatPostMessage(config, channel, asUser, attachments, blocks, iconEmoji, iconUrl, linkNames, mrkdwn, parse, replyBroadcast, text, threadTs, unfurlLinks, unfurlMedia, username)falseconfig.authmotoko
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.initState();
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);
};motoko
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;
};
};motoko
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 }) {
// Non-replicated outcall carrying the Slack token as a bearer credential.
func slackClientConfig(token : Text) : Config {
{
defaultConfig with
auth = ?#bearer(token);
is_replicated = ?false;
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;
};
};以下方案中的管理员权限验证需要授权组件和客户端一起使用:
bash
mops add slack-client@0.0.3
mops add caffeineai-authorization@1.0.1生成的函数为。对于不需要使用的选项,传递空字符串 / 。令牌不是参数——它仅通过传递(参见上方的认证模型);客户端中的所有方法均遵循此规则。
ChatApi.chatPostMessage(config, channel, asUser, attachments, blocks, iconEmoji, iconUrl, linkNames, mrkdwn, parse, replyBroadcast, text, threadTs, unfurlLinks, unfurlMedia, username)falseconfig.authmotoko
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.initState();
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);
};motoko
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;
};
};motoko
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 }) {
// Non-replicated outcall carrying the Slack token as a bearer credential.
func slackClientConfig(token : Text) : Config {
{
defaultConfig with
auth = ?#bearer(token);
is_replicated = ?false;
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;
};
};Known limitations (experimental)
已知限制(实验性)
- envelope. Slack signals logical failures (bad token, missing scope, channel not found) as
{"ok": false}over HTTP 200. The status-code-based error path does not see those, and the body fails to convert into the success schema, so the call rejects ({"ok": false, "error": "…"}, i.e. a failedError.reject) instead of returning a value. Handle it as a rejected call —await; withtry { … } catch (e) { Error.message(e) }on that message carries the raw Slack body, including Slack'sdiagnosticsstring. Do not writeerror: a returned value has already decoded, soif (res.ok) …is alwaysokthere and the check is dead code. Atrue/responseEnvelopegenerator feature that turns this into a clean error value is the next fix.okEnvelope - Reachability (IPv4) — no proxy needed. is IPv4-only, which used to put it out of reach of IC HTTPS outcalls. Since 2025-08-04 the IC tries a direct (IPv6) connection and automatically retries through an IC-managed SOCKS proxy when that fails, so IPv4-only hosts work: leave
slack.comat the defaultconfig.baseUrl. The TLS session is end-to-end between node and Slack, so the proxy sees only ciphertext. Expect some added latency on the fallback path (non-replicated outcalls are also the slower path — see above).https://slack.com/api - Auth is header-based, for every method. The token rides the
header only — never in the URL, so it cannot land in a logged query string, and it is never a method argument.
Authorization: Bearer - Partial runtime verification. The request shape is proven against live Slack
(a real post lands); the success-response decode is not yet runtime-confirmed —
is on, so any decode failure surfaces the raw Slack body. The schema is also generated from an archived (~2020) spec revision.
diagnostics
- 错误包。Slack通过HTTP 200状态码返回
{"ok": false}来表示逻辑错误(无效令牌、缺少权限、频道未找到等)。基于状态码的错误处理路径无法识别这些错误,且响应体无法转换为成功响应schema,因此调用会拒绝({"ok": false, "error": "…"},即Error.reject失败)而不是返回值。请将其视为被拒绝的调用——使用await处理;开启try { … } catch (e) { Error.message(e) }后,错误消息会包含原始Slack响应体,包括Slack的diagnostics字符串。不要编写error代码:返回的值已经完成了解码,因此if (res.ok) …始终为ok,该检查是无效代码。下一步的修复是添加true/responseEnvelope生成器功能,将其转换为清晰的错误值。okEnvelope - 可达性(IPv4)——无需代理。仅支持IPv4,这在过去导致IC HTTPS出站调用无法访问。自2025-08-04起,IC会尝试直接(IPv6)连接,当连接失败时会自动通过IC管理的SOCKS代理重试,因此仅支持IPv4的主机现在可以正常访问:请将
slack.com保留为默认值config.baseUrl。TLS会话在节点和Slack之间是端到端的,因此代理只能看到密文。在回退路径上会有一些额外的延迟(非复制式出站调用也是较慢的路径——参见上文)。https://slack.com/api - 所有方法均基于请求头认证。令牌仅通过请求头传递——绝不会出现在URL中,因此不会出现在日志的查询字符串中,也绝不会作为方法参数。
Authorization: Bearer - 部分运行时验证。请求格式已通过真实Slack验证(可成功发布真实消息);成功响应的解码逻辑尚未经过运行时确认——开启后,任何解码失败都会显示原始Slack响应体。schema也是基于存档的(约2020年)规格版本生成的。
diagnostics