关于 slack
面向开发者的有状态 Slack Web API 模拟器,用于本地测试 Slack 集成。它涵盖消息、会话、文件、OAuth 和应用交互流程,并支持种子状态及 Webhook 回调。
主要功能
- 01有状态的 Slack Web API 模拟
- 02消息、线程与表情回应
- 03会话和用户状态变更
- 04文件上传、固定项与书签
- 05OAuth 和交互式视图流程
适用场景
- 01无需真实工作区测试 Slack 集成
- 02验证消息及线程处理
- 03检查 OAuth 与令牌流程
- 04测试文件、视图和交互回调
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill slack安装后在 Claude Code 中输入/slack确认已加载。
详细介绍
Slack API 模拟器
全面有状态的 Slack Web API 模拟,支持频道、消息、线程、回应、用户个人资料、在线状态、现代文件上传、置顶、书签、视图、OAuth v2 和传入 Webhook。聊天写入会保留常见的富消息字段,例如 blocks、attachments、metadata、格式标志、展开标志和客户端消息 ID。会话写入会更新归档状态、名称、主题、用途、成员关系、DM、MPIM 和已读游标。用户写入会更新个人资料字段、状态、自定义字段,以及确定性的活跃或离开状态。文件写入支持当前的外部上传流程,包括本地上传 URL、文件分享消息、读取、列表、下载和删除。置顶和书签写入支持频道消息置顶和链接书签。视图写入支持 App Home 发布和模态窗口堆栈。预置的 OAuth 应用和 OAuth 安装会创建机器人用户和安装记录。OAuth 交换和显式令牌种子会创建带作用域的令牌记录。状态更改会向已配置的 Webhook URL 分发 event_callback 负载。
Slack 消息文本在聊天写入、传入 Webhook 和文件上传初始评论中的长度上限为 40,000 个 Unicode 字符。超出长度的文本会在存储或分发前按 Unicode 码点边界截断。成功的 Web API 响应会包含 warning: "message_truncated",以及带有相应警告和说明消息的 response_metadata。blocks 和 attachments 等富字段会原样保留。
启动
本地监听器默认绑定到 127.0.0.1。若要从容器或其他机器访问,请向 CLI 传入 --host 0.0.0.0,或向 createEmulator 传入 hostname: '0.0.0.0'。请配置 --base-url 或 baseUrl,以设置这些客户端可访问的对外 URL。
# Slack only
npx emulate --service slack
# Default port (when run alone)
# http://localhost:4000
或以编程方式启动:
import { createEmulator } from 'emulate'
const slack = await createEmulator({ service: 'slack', port: 4003 })
// slack.url === 'http://localhost:4003'
身份验证
将令牌作为 Authorization: Bearer <token> 传入。所有 Web API 端点都需要身份验证。
curl -X POST http://localhost:4003/api/auth.test \
-H "Authorization: Bearer test_token_admin"
不带令牌的请求会返回 not_authed。在宽松作用域模式下,任何非空的未知 Bearer 令牌都会映射到第一个预置用户。
默认情况下,本地开发会启用宽松作用域检查。如果需要受支持的 Web API 方法以 Slack 风格返回包含 needed 和 provided 字段的 missing_scope 错误,请在种子配置中设置 slack.strict_scopes: true。严格模式会检查 chat:write、channels:read、channels:history、channels:join、channels:manage、channels:write、groups:read、groups:history、groups:write、im:read、im:history、im:write、mpim:read、mpim:history、mpim:write、users:read、users:read.email、users.profile:read、users.profile:write、users:write、files:read、files:write、pins:read、pins:write、bookmarks:read、bookmarks:write、reactions:read、reactions:write、team:read 和 admin.conversations:write。Slack 未为 views.publish、views.open、views.update 或 views.push 列出特定方法所需的作用域,因此模拟器会要求身份验证,但不会对这些方法添加严格作用域检查。
将应用指向模拟器
环境变量
SLACK_EMULATOR_URL=http://localhost:4003
Slack SDK / Bolt
import { WebClient } from '@slack/web-api'
const client = new WebClient(token, {
slackApiUrl: `${process.env.SLACK_EMULATOR_URL}/api/`,
})
OAuth URL 映射
| Slack 正式版 URL | 模拟器 URL |
|---|---|
https://slack.com/oauth/v2/authorize | $SLACK_EMULATOR_URL/oauth/v2/authorize |
https://slack.com/api/oauth.v2.access | $SLACK_EMULATOR_URL/api/oauth.v2.access |
Auth.js / NextAuth.js
{
id: 'slack',
name: 'Slack',
type: 'oauth',
authorization: {
url: `${process.env.SLACK_EMULATOR_URL}/oauth/v2/authorize`,
params: { scope: 'chat:write,channels:read,users:read,users.profile:read' },
},
token: {
url: `${process.env.SLACK_EMULATOR_URL}/api/oauth.v2.access`,
},
clientId: process.env.SLACK_CLIENT_ID,
clientSecret: process.env.SLACK_CLIENT_SECRET,
}
种子配置
slack:
team:
name: My Workspace
domain: my-workspace
users:
- name: developer
real_name: Developer
email: [email protected]
is_admin: true
profile:
title: Local Developer
status_text: Testing locally
status_emoji: ":computer:"
presence: active
- name: designer
real_name: Designer
email: [email protected]
profile:
title: Designer
presence: away
channels:
- name: general
topic: General discussion
- name: engineering
topic: Engineering discussions
is_private: true
bots:
- name: my-bot
oauth_apps:
- client_id: "12345.67890"
client_secret: example_client_secret
app_id: A000000001
name: My Slack App
redirect_uris:
- http://localhost:3000/api/auth/callback/slack
scopes:
- chat:write
- channels:read
- channels:history
- channels:join
- channels:manage
- channels:write
- groups:read
- groups:history
- groups:write
- im:read
- im:history
- im:write
- mpim:read
- mpim:history
- mpim:write
- users:read
- users:read.email
- users.profile:read
- users.profile:write
- users:write
- files:read
- files:write
- pins:read
- pins:write
- bookmarks:read
- bookmarks:write
- reactions:read
- reactions:write
- team:read
user_scopes:
- users:read
- users.profile:read
bot_name: my-bot
tokens:
- token: xoxb-local-test
user: developer
scopes:
- chat:write
- channels:read
- channels:history
- channels:join
- channels:manage
- channels:write
- groups:read
- groups:history
- groups:write
- im:read
- im:history
- im:write
- mpim:read
- mpim:history
- mpim:write
- users:read
- users:read.email
- users.profile:read
- users.profile:write
- users:write
- files:read
- files:write
- pins:read
- pins:write
- bookmarks:read
- bookmarks:write
- reactions:read
- reactions:write
- team:read
incoming_webhooks:
- channel: general
label: CI Notifications
strict_scopes: false
signing_secret: my_signing_secret
未配置 OAuth 应用时,模拟器接受任意 client_id。配置应用后,会严格验证 client_id、client_secret 和 redirect_uri。
signing_secret 用于为每个发出的事件订阅回调签名。请使用相同的密钥配置接收应用;带签名的回调包含 X-Slack-Request-Timestamp 和 X-Slack-Signature,其中 X-Slack-Signature 的值为 v0=<HMAC-SHA256(secret, "v0:<timestamp>:<raw-body>")>。请使用未经解析的请求正文进行验证。密钥缺失或为空时,回调不带签名。
API 端点
身份验证
# Test authentication
curl -X POST http://localhost:4003/api/auth.test \
-H "Authorization: Bearer $TOKEN"
聊天
chat.postMessage、chat.update、conversations.history 和 conversations.replies 支持文本以及常见富消息字段的往返传递:blocks、attachments、metadata、mrkdwn、parse、link_names、unfurl_links、unfurl_media、username、icon_url、icon_emoji、bot_id、app_id、client_msg_id 和 reply_broadcast。chat.postMessage 还可以向已打开的 DM 会话或受支持的 Slack 用户 ID 发送消息。
chat.postEphemeral 会将临时消息存储在频道历史记录之外。chat.scheduleMessage、chat.deleteScheduledMessage 和 chat.scheduledMessages.list 会让计划消息保持待处理状态,直到被删除或查看。
# Post message
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Hello from the emulator!"}'
# Post threaded reply
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Thread reply", "thread_ts": "1234567890.123456"}'
# Post rich message
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Deploy complete", "blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": "*Deploy* complete"}}], "metadata": {"event_type": "deploy_complete", "event_payload": {"deploy_id": "dep_123"}}, "unfurl_links": false}'
# Update message
curl -X POST http://localhost:4003/api/chat.update \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456", "text": "Updated message"}'
# Delete message
curl -X POST http://localhost:4003/api/chat.delete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
# Get message permalink
curl -X GET 'http://localhost:4003/api/chat.getPermalink?channel=C000000001&message_ts=1234567890.123456' \
-H "Authorization: Bearer $TOKEN"
# Post ephemeral message
curl -X POST http://localhost:4003/api/chat.postEphemeral \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "user": "U000000001", "text": "Only you can see this"}'
# Schedule message
curl -X POST http://localhost:4003/api/chat.scheduleMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"C000000001\", \"text\": \"Reminder\", \"post_at\": $(($(date +%s) + 3600))}"
# List scheduled messages
curl -X POST http://localhost:4003/api/chat.scheduledMessages.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Delete scheduled message
curl -X POST http://localhost:4003/api/chat.deleteScheduledMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "scheduled_message_id": "Q123456789"}'
# /me message
curl -X POST http://localhost:4003/api/chat.meMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "is thinking..."}'
会话
# List channels (cursor pagination)
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN"
# List DMs and MPIMs
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"types": "im,mpim"}'
# Get channel info
curl -X POST http://localhost:4003/api/conversations.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Create channel
curl -X POST http://localhost:4003/api/conversations.create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "new-channel", "is_private": false}'
# Archive and unarchive a non-general channel
curl -X POST http://localhost:4003/api/conversations.archive \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000002"}'
curl -X POST http://localhost:4003/api/conversations.unarchive \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000002"}'
# Rename channel
curl -X POST http://localhost:4003/api/conversations.rename \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "name": "new-channel-name"}'
# Set topic / purpose
curl -X POST http://localhost:4003/api/conversations.setTopic \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "topic": "Release coordination"}'
curl -X POST http://localhost:4003/api/conversations.setPurpose \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "purpose": "Coordinate release work"}'
# Channel history (top-level messages only)
curl -X POST http://localhost:4003/api/conversations.history \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Thread replies
curl -X POST http://localhost:4003/api/conversations.replies \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
# Join / leave channel
curl -X POST http://localhost:4003/api/conversations.join \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Discover IDs before membership or DM examples
curl -X POST http://localhost:4003/api/users.list \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"types": "public_channel,private_channel"}'
CHANNEL_ID="<channel-id-from-conversations.list>"
USER_ID="<user-id-from-users.list>"
# Invite / kick user
curl -X POST http://localhost:4003/api/conversations.invite \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$CHANNEL_ID\", \"users\": \"$USER_ID\"}"
curl -X POST http://localhost:4003/api/conversations.kick \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$CHANNEL_ID\", \"user\": \"$USER_ID\"}"
# Open DM, then set DM_CHANNEL_ID to the returned channel.id
curl -X POST http://localhost:4003/api/conversations.open \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"users\": \"$USER_ID\", \"return_im\": true}"
DM_CHANNEL_ID="<channel.id-from-conversations.open>"
curl -X POST http://localhost:4003/api/conversations.close \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$DM_CHANNEL_ID\"}"
# Mark conversation read
curl -X POST http://localhost:4003/api/conversations.mark \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
# List members
curl -X POST http://localhost:4003/api/conversations.members \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
用户
# List users (cursor pagination)
curl -X POST http://localhost:4003/api/users.list \
-H "Authorization: Bearer $TOKEN"
# Get user info
curl -X POST http://localhost:4003/api/users.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user": "U000000001"}'
# Lookup by email
curl -X POST http://localhost:4003/api/users.lookupByEmail \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
# Get a user profile
curl -X GET 'http://localhost:4003/api/users.profile.get?user=U000000001' \
-H "Authorization: Bearer $TOKEN"
# Update profile fields and status
curl -X POST http://localhost:4003/api/users.profile.set \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user": "U000000001", "profile": {"display_name": "Developer", "status_text": "Testing locally", "status_emoji": ":computer:"}}'
# Get presence
curl -X GET 'http://localhost:4003/api/users.getPresence?user=U000000001' \
-H "Authorization: Bearer $TOKEN"
# Set the authed user away
curl -X POST http://localhost:4003/api/users.setPresence \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"presence": "away"}'
# Return the authed user to automatic presence
curl -X POST http://localhost:4003/api/users.setPresence \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"presence": "auto"}'
文件
# Request a local upload URL
curl -X POST http://localhost:4003/api/files.getUploadURLExternal \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"filename": "deploy.txt", "length": 18}'
# Upload bytes to the returned upload_url
curl -X POST http://localhost:4003/upload/v1/F000000001 \
-H "Content-Type: application/octet-stream" \
--data-binary @deploy.txt
# Complete and share the file in a channel
curl -X POST http://localhost:4003/api/files.completeUploadExternal \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"files": [{"id": "F000000001", "title": "Deploy Log"}], "channel_id": "C000000001", "initial_comment": "Deploy log attached"}'
# Get file info
curl -X GET 'http://localhost:4003/api/files.info?file=F000000001' \
-H "Authorization: Bearer $TOKEN"
# List files
curl -X POST http://localhost:4003/api/files.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Download file bytes from url_private
curl -X GET http://localhost:4003/files-pri/F000000001/deploy.txt \
-H "Authorization: Bearer $TOKEN"
# Delete a file
curl -X POST http://localhost:4003/api/files.delete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"file": "F000000001"}'
置顶内容和书签
# Pin a message
curl -X POST http://localhost:4003/api/pins.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456"}'
# List pinned messages
curl -X GET 'http://localhost:4003/api/pins.list?channel=C000000001' \
-H "Authorization: Bearer $TOKEN"
# Remove a message pin
curl -X POST http://localhost:4003/api/pins.remove \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456"}'
# Add a link bookmark
curl -X POST http://localhost:4003/api/bookmarks.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "C000000001", "title": "Runbook", "type": "link", "link": "https://example.com/runbook"}'
# Edit a bookmark
curl -X POST http://localhost:4003/api/bookmarks.edit \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "C000000001", "bookmark_id": "Bk000000001", "title": "Updated Runbook"}'
# List bookmarks
curl -X POST http://localhost:4003/api/bookmarks.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "C000000001"}'
# Remove a bookmark
curl -X POST http://localhost:4003/api/bookmarks.remove \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel_id": "C000000001", "bookmark_id": "Bk000000001"}'
视图
# Publish an App Home view
curl -X POST http://localhost:4003/api/views.publish \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id": "U000000001", "view": {"type": "home", "blocks": [{"type": "section", "text": {"type": "plain_text", "text": "Local App Home"}}]}}'
# Generate a local trigger id for modal tests
curl -X POST http://localhost:4003/api/views.generateTriggerId \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user_id": "U000000001"}'
# Open a modal view with the returned trigger_id or interactivity_pointer before it expires
curl -X POST http://localhost:4003/api/views.open \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"trigger_id": "<trigger_id from views.generateTriggerId>", "view": {"type": "modal", "title": {"type": "plain_text", "text": "Local Modal"}, "blocks": [{"type": "section", "text": {"type": "plain_text", "text": "Modal body"}}]}}'
# Update a view
curl -X POST http://localhost:4003/api/views.update \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"view_id": "V000000001", "view": {"type": "modal", "title": {"type": "plain_text", "text": "Updated Modal"}, "blocks": [{"type": "section", "text": {"type": "plain_text", "text": "Updated body"}}]}}'
# Generate a push trigger tied to an existing modal view
curl -X POST http://localhost:4003/api/views.generateTriggerId \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"view_id": "V000000001"}'
# Push a modal view onto the current stack with the returned trigger_id or interactivity_pointer
curl -X POST http://localhost:4003/api/views.push \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"trigger_id": "<trigger_id from views.generateTriggerId>", "view": {"type": "modal", "title": {"type": "plain_text", "text": "Next Modal"}, "blocks": []}}'
表情回应
# Add reaction
curl -X POST http://localhost:4003/api/reactions.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456", "name": "thumbsup"}'
# Remove reaction
curl -X POST http://localhost:4003/api/reactions.remove \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456", "name": "thumbsup"}'
# Get reactions
curl -X POST http://localhost:4003/api/reactions.get \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456"}'
团队
# Get workspace info
curl -X POST http://localhost:4003/api/team.info \
-H "Authorization: Bearer $TOKEN"
机器人
# Get bot info
curl -X POST http://localhost:4003/api/bots.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"bot": "B000000001"}'
传入 Webhook
# Post via incoming webhook
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Deployment complete!"}'
# Post to a specific channel
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Alert!", "channel": "C000000002"}'
# Post threaded webhook message
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Thread update", "thread_ts": "1234567890.123456"}'
交互
按钮和模态框提交可以调用你应用的 Interactivity Request URL。在种子配置的 slack.oauth_apps 条目中设置 interactivity_url,并将 slack.signing_secret 设为接收方使用的同一密钥:
slack:
signing_secret: local-signing-secret
oauth_apps:
- app_id: A_MY_APP
client_id: my-client
client_secret: my-secret
name: My App
redirect_uris: []
interactivity_url: http://localhost:3000/slack/interactions
POST /_slack/simulate/block-actions接受channel、message_ts和按钮选择器(action_id、完全匹配的text或url),以及执行操作的userID 或名称。它支持操作区块和分区附件中的按钮,包括该用户可见的临时消息。模拟器要求使用经过身份验证的模拟器令牌,且执行操作的用户必须属于该会话。POST /_slack/simulate/view-submission接受view_id、可选的user,以及以输入区块 ID 为键的values。文本输入接受字符串;静态选择器和单选按钮接受选项值;多选器和复选框接受选项值数组。省略的值会保留先前输入或初始值。其他输入元素会返回unsupported_element。
回调采用 application/x-www-form-urlencoded 格式,包含一个 payload JSON 字段,并根据确切编码后的请求正文签名。按钮负载包含绑定到应用且只能使用一次的 trigger_id,可为执行操作的用户打开模态框。触发器默认在三秒后过期;将 slack.view_trigger_ttl_seconds 设为正数,可在本地测试中延长有效时间。使用应用的机器人令牌或用户令牌发布的消息会保留应用所有权。模拟器会返回负载和 delivery_id;尝试记录和传递结果会保存在 slack.interaction_deliveries 中。
按钮负载的 response_url 接受不带 bearer 令牌的 JSON,可在 30 分钟内使用最多五次。默认情况下,它会发布临时回复;设置 response_type: "in_channel" 可发布公开回复。使用 replace_original: true 或 delete_original: true 修改源消息。替换操作会保留源消息的可见性。支持富区块和线程回复。
模态框接收方必须在三秒内确认。空的 HTTP 200 会关闭已提交的视图,而 response_action: "clear" 会关闭整个视图堆栈。errors 会保留视图并返回区块错误;update 会替换其内容;push 会添加新视图,但最多只能有三个视图。传递失败和无效确认不会关闭模态框。快捷方式、斜杠命令、非按钮操作、view_closed 和 block_suggestion 均不在模拟范围内。
OAuth
# Authorize (browser flow, shows user picker)
# GET /oauth/v2/authorize?client_id=...&redirect_uri=...&scope=...&state=...
# The picker submits POST /oauth/v2/authorize/callback to create the auth code.
# Token exchange
curl -X POST http://localhost:4003/api/oauth.v2.access \
-H "Content-Type: application/json" \
-d '{"client_id": "12345.67890", "client_secret": "example_client_secret", "code": "<code>"}'
返回 Slack 风格的响应:
{
"ok": true,
"access_token": "xoxb-...",
"token_type": "bot",
"scope": "chat:write,channels:read",
"app_id": "A000000001",
"bot_user_id": "U000000099",
"team": { "id": "T000000001", "name": "Emulate" },
"authed_user": { "id": "U000000001" }
}
POST /api/apps.uninstall 接受 client_id、client_secret,以及通过 Authorization 标头或 POST token 字段传入的某个安装的用户令牌。它会移除该令牌所属工作区中此应用的安装,并撤销所有相关的 OAuth 机器人令牌和用户令牌,包括较早授予的令牌。即使启用了宽松身份验证,被撤销的凭据在调用 Web API 时也会返回 token_revoked。其他应用和工作区的凭据不受影响。细粒度机器人令牌无法卸载应用,并会返回 no_permission;未绑定应用的已配置开发令牌可以卸载本地安装。无需任何方法作用域。
卸载时会向匹配的事件订阅发送 app_uninstalled 和 tokens_revoked,并附带被移除应用的 api_app_id。tokens_revoked.tokens.oauth 和 .bot 包含去重后的用户 ID,不包含令牌字符串。凭据会在回调发送前失效。处理程序应能接受任一事件顺序。应用定义仍然可用,可用于新的 OAuth 安装;重新安装会签发新令牌,但不会重新激活已撤销的令牌。
Inspector
在 Slack 模拟器中打开 GET /,即可检查对话、消息、文件、视图、身份验证记录、传入 Webhook、事件订阅和事件投递。
事件分发
对于会触发事件的受支持 Slack 写入操作,模拟器会将 event_callback 负载分发到已配置的 Webhook URL。这些负载符合 Slack Events API 格式:
每个回调除内部 event 外,还包含外层 token、api_app_id、authorizations、team_id、event_id,以及整数 Unix 秒级时间戳 event_time。对于经过身份验证的 Web API 写入操作,team_id 来自所提供的 Slack 令牌对应的安装。对于没有已存储 Slack 记录的开发令牌,系统会依次回退到受影响的频道、用户或文件所属的团队,然后是预置的工作区团队(或 T000000001)。传入 Webhook 帖子使用其 Webhook 记录中的团队;如果没有匹配记录,则使用目标频道的团队。每个逻辑事件都会获得一个独立的 event_id,并由投递给多个订阅者的消息共用。
回调会使用接收方的 api_app_id 和机器人授权来标识每个工作区安装。如果安装了多个应用,每个应用都会收到单独的信封;已配置的 Webhook 订阅由它们共享,并可按 api_app_id 路由。没有安装时,回调会使用 A000000001 和本地用户授权。旧版 token 字段默认值为 emulate-verification-token;如果接收端会检查此字段,请设置 slack.verification_token。使用 slack.signing_secret 验证请求签名。
每个内部事件都包含 event_ts。消息事件包含 channel_type(channel、group、im 或 mpim)。使用已签发或预置的机器人令牌发布的消息,如果存在相应的机器人记录,则会包含机器人的 bot_id、app_id 和 bot_profile。这些字段会保留在历史记录、回复和更新中,方便处理程序识别自己的消息。使用 conversations.kick 或 conversations.leave 移除已安装的机器人时,也会针对受影响的应用发送包含 actor_id 和 event_ts 的 channel_left 或 group_left 事件。
要测试机器人处理程序,请使用 conversations.invite 邀请已安装应用的 bot_user_id,订阅 app_mention,然后使用 chat.postMessage 发布包含 <@BOT_USER_ID> 的文本。频道提及会为每个被提及的应用各发送一个回调,其中包含外层 api_app_id、内部 event_ts,以及原始消息和线程标识。私信和多人即时消息使用 message 事件。
chat.postMessage上的message事件- 当频道消息文本提及已安装的成员机器人时,
chat.postMessage上的app_mention apps.uninstall成功时的app_uninstalled/tokens_revokedchat.update上带有subtype: message_changed的messagechat.delete上带有subtype: message_deleted的message- 如果存在,已发布的
message事件会包含丰富消息字段 reactions.add/reactions.remove上的reaction_added/reaction_removed事件pins.add/pins.remove上的pin_added/pin_removed事件- 收到传入 Webhook 帖子时,带有
subtype: bot_message的message - 公开频道生命周期归档写入对应的
channel_archive/channel_unarchive - 私有频道生命周期归档写入对应的
group_archive/group_unarchive conversations.rename上的channel_rename/group_rename及匹配的名称消息子类型- 写入主题和用途时,带有公开
channel_topic/channel_purpose或私有group_topic/group_purpose子类型的message - 邀请、加入、离开和踢出写入对应的
member_joined_channel/member_left_channel - 已安装的机器人离开或被移除时的
channel_left/group_left - DM 和 MPIM 写入对应的
im_created、im_open、im_close、im_marked以及群组打开、关闭和标记事件 - 配置文件写入时的
user_change - 在线状态写入时的
presence_change - 文件写入时的
file_created、file_shared和file_deleted - 共享文件上传时,带有
subtype: file_share的message
配置 slack.signing_secret 后,每个现有的出站事件订阅回调都会包含 X-Slack-Request-Timestamp 和 X-Slack-Signature。签名格式为 v0=<HMAC-SHA256(secret, "v0:<timestamp>:<raw-body>")>,使用精确序列化后的回调正文。使用相同的密钥配置接收方,并验证未经解析的请求正文。如果密钥缺失或为空,回调将不带签名。
频道隐私转换
POST /api/admin.conversations.convertToPrivate 和 POST /api/admin.conversations.convertToPublic 接受 channel_id。使用属于已植入管理员的用户令牌;机器人令牌和测试令牌会返回 not_allowed_token_type,非管理员用户会返回 missing_scope。当 slack.strict_scopes: true 时,令牌还需要 admin.conversations:write。例如:
slack:
tokens:
- token: local-admin-user
type: user
user: admin
scopes: [admin.conversations:write, channels:read, groups:read]
转换会保留频道 ID、成员和历史记录,更新会话和文件读取中的可见性,并附加带有子类型 channel_convert_to_private 或 channel_convert_to_public 的 message。事件回调会包含执行操作的用户和新的 channel_type(group 或 channel)。管理员无需加入该频道。DM、MPIM 和重复转换均不受支持;#general 不能转换为私有频道。
这些方法作用于模拟工作区,不受 Slack Enterprise 计划或组织范围安装要求的限制。Enterprise 角色、频道管理策略和 MPIM 转换均未模拟。
Slack Connect 测试数据
为用户设置不同的 team_id,以测试来自其他工作区的消息。users.info 和 users.list 会返回该用户所属的工作区和 is_stranger 标记。未指定用户所属团队时,默认使用模拟的工作区。is_stranger 默认为 false,可将其设为 true,以模拟不在应用可见的共享频道中的用户。
slack:
users:
- name: partner
team_id: T_PARTNER
is_stranger: false
tokens:
- token: xoxp-partner
type: user
user: partner
scopes: [chat:write]
使用已设置用户的令牌,以该外部用户的身份执行操作。使用 conversations.invite 邀请该用户和应用的机器人加入频道,然后发布一条提及机器人的消息。应用提及事件包含作者的 user_team 和 source_team;外层的 team_id 标识接收事件的安装工作区,该工作区由令牌选择,默认是模拟的工作区。普通消息回调会保留作者的用户 ID,可通过 users.info 查询。
包含外部成员的频道会返回 is_ext_shared: true 和 is_shared: true;其事件回调会包含 is_ext_shared_channel: true。设置 channels[].is_ext_shared: true,可模拟已共享的频道。即使最后一名外部成员离开或被移除,共享状态仍会保留,包括在已保存的状态中。不会模拟 Slack Connect 邀请与审批流程、工作区断开连接以及各工作区的频道设置。
事件重试
使用经过身份验证的模拟器令牌,通过 POST /_slack/simulate/event-retry 发送 JSON { "event_id": "Ev...", "reason": "http_timeout" },以测试事件去重。从收到的回调或检查器的 Events 标签页获取事件 ID。模拟器会向原始订阅重新发送该事件最近一次记录的投递,保留完整载荷和事件 ID。如果有多个订阅者收到该事件,只会重试最近一次接收的订阅者。
每次尝试的 X-Slack-Retry-Num 都会递增,从 1 开始;X-Slack-Retry-Reason 默认为 http_timeout。原因也可以是 http_error、connection_failed、ssl_error、too_many_redirects 和 unknown_error。配置了 slack.signing_secret 时,重试会使用新的请求时间戳和签名。响应包含 delivery_id、retry_num、success 和 status_code;失败的回调会被记录,且仍会消耗一个重试序号。
重试需显式触发,不会自动安排,也没有三次尝试的限制。内存中只保留最近的 1,000 条事件投递记录,因此记录被清除或模拟器重启后,将无法重试。未知事件会返回 event_not_found,非活跃订阅会返回 subscription_inactive。
当前限制
尚未实现 Slack Connect 邀请与审批流程、各工作区的频道设置、其他 Enterprise Grid 管理 API、Audit Logs API、SCIM、Legal Holds、Socket Mode、斜杠命令、快捷方式、按钮操作和模态提交以外的交互类型、用户组、提醒、星标、通话、画布、列表、函数、工作流、聊天流式传输、旧版 files.upload、精确速率限制以及付费套餐行为。
常见模式
发布消息并添加表情回应
TOKEN="test_token_admin"
BASE="http://localhost:4003"
# Post a message
curl -X POST $BASE/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Hello!"}'
# React to it (use the ts from the response)
curl -X POST $BASE/api/reactions.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "<ts>", "name": "wave"}'
OAuth 流程
- 将用户重定向到
$SLACK_EMULATOR_URL/oauth/v2/authorize?client_id=...&redirect_uri=...&scope=chat:write,channels:read&state=... - 用户在模拟器的界面上选择一个已设置的用户
- 模拟器将用户重定向回来,并附带
?code=...&state=... - 通过
POST /api/oauth.v2.access交换授权码以获取令牌 - 使用
xoxb-令牌调用 Web API 端点
如果授权 URL 和回调中包含 user_scope,交换响应会包含 authed_user.access_token、authed_user.scope 和 authed_user.token_type。
通过 Webhook 发送 CI 通知
# Use the default incoming webhook
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Build passed on main"}'
与内置模拟器一起使用自定义模拟器
使用 npx emulate init --custom inventory 搭建第三方 API 模拟器和测试。使用 emulate 中的 defineConfig 在 emulate.config.ts 中注册该模拟器,并与 { emulator: "slack" } 等内置条目并列。运行 npx emulate start --watch 以重新加载导入,并在打印出的 /_emulate URL 中检查自定义状态。重新加载成功后,会将本次运行重置为种子状态。现有的扁平种子配置仍然可用;--config 用于指定文件。有关编写和测试第三方 API 模拟器的信息,请参阅 https://emulate.dev/docs/custom-emulators。