上一篇讲了怎么拿 tenant_access_token([L1]),这一课用上它:让机器人把消息发出去。

群里 @AI 问一句、日报定时推到团队群、客服自动回复——这些场景的底层都是同一个接口:POST /open-apis/im/v1/messages。

它只有三个必填字段,但实际对接的人大多会卡在三个地方:receive_id_type 选错、content 的双重 JSON 序列化、批量推送撞限流。这篇按官方现行文档写全,代码可以直接跑。

一、接口速览

项值
URLhttps://open.feishu.cn/open-apis/im/v1/messages?receive_id_type={type}
MethodPOST
Content-Typeapplication/json; charset=utf-8
权限(任一)im:message 或 im:message:send_as_bot
接口频率1000 次/分钟、50 次/秒
细粒度限流同一用户 5 QPS;同一群组内机器人共享 5 QPS
体积上限文本 ≤150 KB;卡片/富文本 ≤30 KB

请求体四个字段:

字段必填说明
receive_id是接收者 ID,类型与 receive_id_type 一致
msg_type是消息类型(见第三节)
content是JSON 序列化后的字符串(不是对象,第四节详讲)
uuid否幂等键:相同 uuid 在 1 小时内至多成功发送一条,≤50 字符

二、receive_id_type:五种取值,选错一个全失败

"发给谁"由 URL 参数 receive_id_type + 请求体 receive_id 成对决定:

receive_id_type发给谁典型场景
open_id单个用户应用维度用户 ID(ou_ 开头),最常用
union_id单个用户跨应用统一 ID
user_id单个用户租户内员工 ID
email单个用户直接用邮箱发
chat_id群聊往群里发必须用这个

两个高频翻车点:

  1. 发群消息却传了用户的 open_id——报错或消息"发成功"但群里看不到。发群只认 chat_id
  2. chat_id 去哪拿:把机器人拉进群 → 调「获取群列表」GET /open-apis/im/v1/chats → 返回里就是 chat_id

结论:receive_id 和 receive_id_type 必须成对匹配,群消息只认 chat_id。

三、msg_type:十一种消息类型,先用 text

官方支持:text(文本)、post(富文本)、image(图片)、file(文件)、audio(语音)、media(视频)、sticker(表情包)、interactive(卡片)、share_chat(分享群名片)、share_user(分享个人名片)、system(系统消息,仅单聊)。

图片/音频/视频/文件要先调上传接口拿 key,不能直接传文件。interactive 卡片是 L3 的主题(把大模型回复做成带按钮的交互卡片)。

这一课先把 text 跑通——它是后面一切的基础。

四、content 的坑:双重 JSON 序列化

最反直觉的一个坑。请求体长这样:

{
  "receive_id": "oc_xxx",
  "msg_type": "text",
  "content": "{\"text\":\"hello\"}"
}

content 的值不是 JSON 对象,是一个字符串——内层 JSON 先序列化一次,再作为字符串放进外层 body 序列化第二次。直接传对象报 230001。

三个细节:

  1. 换行符:文本里的 \n 在 content 字符串里是 \\n(用 json.dumps 自动处理,别手拼字符串)
  2. 超长:AI 生成长回复直接塞会撞 230025(content reaches its limit),进飞书前先截断或分段
  3. 中文:序列化时 ensure_ascii=False,否则中文变 \uXXXX(不影响发送但影响调试)

结论:content 双重序列化 + 换行转义 + 超长截断,三件事少做一件就报错。

五、完整代码(复用 L1 的 token 管理器)

L1 的 FeishuToken 类(缓存 + 提前刷新 + 错误码映射)直接拿来用,这课只加发送部分:

import json
import urllib.error
import urllib.request

SEND_URL = "https://open.feishu.cn/open-apis/im/v1/messages"

# 官方错误码 → 中文排查(im/v1/messages 高频 12 个)
SEND_ERROR_HINTS = {
    230001: "请求参数不合法:检查 receive_id_type 与 receive_id 是否匹配、content 是否为合法 JSON 字符串",
    230002: "机器人不在该群里:先把机器人拉进目标群",
    230006: "机器人能力未启用:去开发者后台开启机器人能力",
    230013: "机器人对该用户不可见:检查应用可用范围是否包含该用户",
    230020: "触发频率限制:同一用户/群仅 5QPS,客户端排队重试",
    230022: "消息含敏感信息被拦截:改写内容后重试",
    230025: "消息超长:文本≤150KB、卡片/富文本≤30KB,AI 回复先截断",
    230027: "缺少权限:确认已开通 im:message 或 im:message:send_as_bot",
    230034: "receive_id 无效:核对 ID 来源与 receive_id_type 是否一致",
    230035: "无发送权限:检查应用身份权限配置",
    230053: "用户已停用该机器人:对方关闭了机器人会话",
    232009: "群已解散:chat_id 失效,重新获取群列表",
}


def send_text(token_header, receive_id, text, receive_id_type="open_id", uuid=None):
    """发送文本消息。content 双重序列化在这里处理。"""
    inner = json.dumps({"text": text}, ensure_ascii=False)  # 第一层:内层 JSON
    body = {
        "receive_id": receive_id,
        "msg_type": "text",
        "content": inner,  # 第二层:作为字符串放入外层 body
    }
    if uuid:
        body["uuid"] = uuid[:50]  # 幂等键,1 小时内同 uuid 只发一条

    req = urllib.request.Request(
        f"{SEND_URL}?receive_id_type={receive_id_type}",
        data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
        headers={**token_header, "Content-Type": "application/json; charset=utf-8"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            payload = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        # 发送失败是 HTTP 400,错误码在响应体里,必须读 body
        payload = json.loads(e.read().decode("utf-8"))

    if payload.get("code") != 0:
        code = payload.get("code")
        hint = SEND_ERROR_HINTS.get(code, "查官方文档 im/v1/messages 错误码章节")
        raise RuntimeError(f"[{code}] {payload.get('msg')} | 排查:{hint}")
    return payload["data"]  # 含 message_id


if __name__ == "__main__":
    import os
    from l1_token import FeishuToken  # L1 代码存为 l1_token.py
    ft = FeishuToken(os.environ["FEISHU_APP_ID"], os.environ["FEISHU_APP_SECRET"])
    msg = send_text(
        ft.auth_header(),
        receive_id=os.environ["FEISHU_TEST_CHAT_ID"],  # 群 chat_id
        text="Hello from 提示岛教程 L2:机器人发消息跑通了",
        receive_id_type="chat_id",
        uuid="l2-demo-0001",  # 重试不重复
    )
    print("message_id:", msg.get("message_id"))

这段代码把三个坑都处理了:receive_id_type 显式传参(群消息传 chat_id)、content 用 json.dumps 做内层序列化(换行自动转义)、uuid 幂等防重复。

批量推送的限流处理:同一群 5 QPS,给 200 个群发日报时自己加排队间隔(如 time.sleep(0.25)),重试时带同一个 uuid——网络超时重试不会造成重复消息。

六、限流与幂等:uuid 是官方给的防重复开关

限制值应对
接口整体1000 次/分钟、50 次/秒一般够用
同一用户5 QPS批量私信自己排队
同一群组机器人共享 5 QPS多机器人同群要合并计算
uuid 幂等相同 uuid 1 小时内至多成功 1 条超时重试必带,防重复消息

结论:批量推送必须自己限流;重试带 uuid 防重复,这是官方给的幂等开关。

七、两段可以直接复制的机器人提示词

消息通道打通了,接下来是"发什么"。这两段来自提示岛站内条目,对应机器人两个最典型场景。

① 群内自动回复(客服/答疑机器人)——id=5058「AI 客服专家」

Act as an AI Customer Support Specialist. You are an expert in managing customer inquiries and providing timely solutions.

Your task is to:
- Understand and categorize customer issues
- Provide accurate and helpful responses
- Escalate complex issues to human agents as needed

Rules:
- Maintain a professional and friendly tone
- Ensure customer satisfaction with every interaction
- Follow company policies and procedures for handling customer data

Variables:
- ${customerIssue} - Description of the customer's issue
- ${responseTime:immediate} - Desired response time

用法:事件订阅收到群消息后,把用户问题填进 ${customerIssue},大模型回复经 send_text 发回群里。注意 ${responseTime:immediate} 这种带默认值的变量写法——冒号后是缺省值,调用方不传就用它,这个设计值得抄。

② 定时推送(日报/简报机器人)——id=11805「简报写手机器人」核心段

I'd like you to simulate a BOT called '[NAME OF YOUR NEWSLETTER] BOT'. In this role your task is to act as a collaborative writer and editor for [NAME OF YOUR NEWSLETTER]. You should strive to maintain the style, tone, and register used in all the examples I provide with anything you write.

[NEWSLETTER CREATION PROCESS] As a co-writer and editor of [NAME OF YOUR NEWSLETTER] one of your tasks is to assist me in drafting newsletters for my input and final approval. There are 6 phases to this process. They are: RAW INPUT COLLECTION + ORGANIZATION, OUTLINING, EXPANDING THE OUTLINE, EDITING, CALL TO ACTION, LOGO.

(全文 3566 字,站内可一键复制。)用法:定时任务触发 → 这段提示词产出简报 → send_text 推到群。它把生成过程拆成 6 个阶段(收集→大纲→扩写→编辑→行动号召→署名),比"帮我写个日报"稳定得多。

结论:机器人提示词要带流程阶段和变量缺省值,输出才稳定到能直接推送。

这一课做完,你手上有什么

  • 一个能往群/单聊发文本的完整函数(含错误码中文映射)
  • receive_id_type 五值对照 + chat_id 获取路径
  • uuid 幂等 + 5QPS 限流的批量推送策略
  • 客服、简报两段可直接改用的机器人提示词

下一课(L3)讲消息卡片:msg_type=interactive,把大模型的回复做成带按钮、可交互的卡片,以及 30KB 限制下的卡片 JSON 瘦身技巧。


附:提示岛是什么

本文提示词来自提示岛(prompts.ssjintaitong.com),AI 提示词与 Skill 库,实测收录 10907 条,其中文字提示词 3482 条(完整全文、一键复制),Skill/插件 5298 条,23 个分类。免登录、免注册。

飞书相关入口:

  • 飞书 AI 自动化专题(145 条):prompts.ssjintaitong.com/topic/feishu
  • 本文两段提示词原文:prompts.ssjintaitong.com/prompts/5058、/prompts/11805
  • 文字提示词总库:prompts.ssjintaitong.com/prompts?type=text
  • 本系列 L1(token 获取):同专题可查

站内两个实用功能:Skill 质量评分(5298 个插件条目按四维度打 A-D 档,报告在 /quality-report);每条附原始出处(GitHub 链接 + 许可证)。搜索技巧:用场景词搜,搜「飞书」有 130 条。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐