mcpField
S
slack

slack

github.com/vercel-labs测试与质量2.0k196 次安装1 天前更新
代码仓库

关于 slack

面向开发者的有状态 Slack Web API 模拟器,用于本地测试 Slack 集成。它涵盖消息、会话、文件、OAuth 和应用交互流程,并支持种子状态及 Webhook 回调。

主要功能

  1. 01有状态的 Slack Web API 模拟
  2. 02消息、线程与表情回应
  3. 03会话和用户状态变更
  4. 04文件上传、固定项与书签
  5. 05OAuth 和交互式视图流程

适用场景

  1. 01无需真实工作区测试 Slack 集成
  2. 02验证消息及线程处理
  3. 03检查 OAuth 与令牌流程
  4. 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),以及执行操作的 user ID 或名称。它支持操作区块和分区附件中的按钮,包括该用户可见的临时消息。模拟器要求使用经过身份验证的模拟器令牌,且执行操作的用户必须属于该会话。
  • 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_revoked
  • chat.update 上带有 subtype: message_changed 的 message
  • chat.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 流程
  1. 将用户重定向到 $SLACK_EMULATOR_URL/oauth/v2/authorize?client_id=...&redirect_uri=...&scope=chat:write,channels:read&state=...
  2. 用户在模拟器的界面上选择一个已设置的用户
  3. 模拟器将用户重定向回来,并附带 ?code=...&state=...
  4. 通过 POST /api/oauth.v2.access 交换授权码以获取令牌
  5. 使用 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。

评论

支持 Markdown