🚀进阶飞书接入Qwen3.8-Omni-Flash

通义千问 Qwen3.8-Omni-Flash 接入 飞书 完整指南

Qwen3.8-Omni-Flash 是阿里云百炼 2026 年的全模态轻量模型:文本、图片、音频、视频一模型通吃,输入 ¥0.5 每百万 tokens,延迟低到能做高频群聊。本文手把手把它接进飞书,覆盖群自定义机器人 Webhook 加签(HMAC-SHA256 取十六进制,与钉钉 base64 完全不同)、图片与音频多模态消息体的正确写法、交互式卡片消息 JSON 全字段解析、企业自建应用 tenant_access_token 缓存与 im/v1/messages 发消息、长连接与 Webhook 回调两种事件订阅对比,并附 18 条报错排查表、4 个可直接复制的多模态场景提示词与 3 种密钥策略对比。

教程团队
2026-04-27
1 个步骤

前置条件

  • 1已安装飞书客户端(7.28 及以上)并拥有企业/团队账号
  • 2是目标飞书群的群管理员(自定义机器人只有群管理员能添加)
  • 3已注册阿里云百炼(DashScope)账号并开通 Qwen3.8-Omni-Flash 模型服务
  • 4有一台能出网的机器跑中转脚本,Python 3.10+ 或 Node.js 18+

读完想动手?新用户首单 8 折上云

服务器 + 大模型 API + 对象存储,新用户首购享 20% 补贴

领取

通义千问 Qwen3.8-Omni-Flash 接入 飞书 完整指南

这篇教程要把 Qwen3.8-Omni-Flash 接进 飞书,让群里 @ 一下、发张截图、甚至甩一段语音,都能拿到回答。

Qwen3.8-Omni-Flash 的关键词是 Omni(全模态):同一个模型能吃文本、图片、音频、视频,而且是 Flash 档位——便宜、快,适合飞书这种高频碎片化场景。这意味着你能做出别的机器人做不了的东西:群里发一张报错截图,机器人直接读图告诉你哪里错了;发一段会议录音,机器人出纪要。

本文除了多模态消息体的写法(这块坑最多),还会覆盖飞书侧必须过的一关:自定义机器人加签(HMAC-SHA256 取十六进制,和钉钉的 base64 完全不同)、交互式卡片消息(飞书相对钉钉和企业微信的最大优势)、企业自建应用的 tenant_access_token 缓存与 im/v1/messages 发消息、以及长连接 vs Webhook 回调两种事件订阅的取舍。

两条接入路径,先选一条

方式 A:群自定义机器人 Webhook方式 B:企业自建应用
适合谁定时往群里推(巡检截图解读、日报、公告摘要)群里发图/发语音让机器人看完再答,要私聊,要卡片按钮
凭证Webhook 地址 + 加签密钥 secretapp_id / app_secret → tenant_access_token
收得到用户消息吗❌ 只能单向推✅ 订阅事件后能收,还能收到用户发的图片和文件
能私聊吗❌ 只能群里✅ 单聊 + 群聊
消息类型text / 富文本 / 交互式卡片全部,外加图片、文件、回复式消息
要不要审核不用,加完立刻能用要,管理员审核后发布
上线耗时约 10 分钟约 1-2 小时
推荐度⭐⭐⭐⭐⭐(先跑通选这个)⭐⭐⭐⭐(要收图必选这个)

建议路线:方式 A 验证回答质量和卡片样式 → 迁到方式 B 支持「群里发图机器人读完回答」。两段代码共用同一个「调 Qwen + 拼卡片」的函数,迁移只改发送与收文件两层。

前置准备

  • 已安装飞书客户端(7.28 及以上)并登录了企业/团队账号
  • 是目标飞书群的群管理员(自定义机器人只有群管理员能添加)
  • 已注册阿里云百炼 DashScope 并开通了 Qwen3.8-Omni-Flash 模型服务
  • 已创建 API Key 并确认账户未欠费(多模态调用按折算 tokens 计费)
  • 有一台能出网的机器跑中转脚本,Python 3.10+ 或 Node.js 18+
  • 已想好密钥存放方式(环境变量或 .env,绝不写死在代码里)

提示

小白提示:机器人自己不会思考。完整链路是:群里说话(或发图/发语音)→ 飞书通知你的服务 → 你的服务拿问题去问通义千问 → 把回答塞回飞书。所以那台中转机器得一直开着,正式用请放服务器或云函数。

为什么选 Qwen3.8-Omni-Flash 放在飞书里

一句话定位

信息

Qwen3.8-Omni-Flash = 全模态(文本 + 图片 + 音频 + 视频)单模型通吃 + 输入 ¥0.5/百万 tokens + 低延迟 + 阿里云生态无缝。

飞书群里的提问有一半以上是「带图的」——报错截图、设计稿、数据看板、手写便签。纯文本模型遇到这些只能回「请把内容打字发给我」,而 Omni 模型能直接看懂,这一步体验差距是数量级的。

百炼上 Qwen3.8 家族怎么选

模型定位模态输入价(每 1M tokens)输出价(每 1M tokens)适合飞书场景
qwen3.8-omni-flash全模态轻量,本文主角文/图/音/视¥0.5¥2群里带图提问默认
qwen3.8-plus文本增强版文¥0.8¥2纯文本复杂问答
qwen3.8-max文本旗舰文¥4¥12长文分析、方案起草
qwen3.8-vl-flash视觉轻量版文/图¥0.3¥1.2只要看图、更省

模态折算规则(影响账单的关键):

text
1图片:按分辨率折算,约 1 张 720P ≈ 400-800 tokens,1080P ≈ 800-1500 tokens
2音频:按秒折算,约 1 秒 ≈ 25-40 tokens,一分钟会议录音约 1500-2400 tokens
3视频:按抽帧折算,8 帧 ≈ 3200-6400 tokens,超过 16 帧性价比骤降

飞书机器人的推荐配置:默认 qwen3.8-omni-flash;纯文本且问题较长时切 qwen3.8-plus;命中「方案/分析/起草」时切 qwen3.8-max。图片一定先压缩再传——把 1080P 截图压成 720P,tokens 直接省 40%,画质损失肉眼看不出来。

算一笔账

一个 80 人的运营群,每天 60 次调用(其中 25 次带图),平均 500 tokens 输入 + 400 tokens 输出:

text
1纯文本 35 次:35 × (500/1M × 0.5 + 400/1M × 2) = 35 × 0.00105 = ¥0.037
2带图   25 次:25 × ((500+900)/1M × 0.5 + 400/1M × 2) = 25 × 0.0015 = ¥0.037
3合计:约 ¥0.074/天,约 ¥2.2/月

一个月两块多,这是 Flash 档位在群聊场景里的意义。

什么时候不该用 Qwen3.8-Omni-Flash

text
1❌ 需要深度多步推理的复杂分析(Flash 档位,切 qwen3.8-max 或推理型模型)
2❌ 视频理解要逐帧精确(超过 16 帧性价比骤降,建议抽关键帧 + 人工确认)
3❌ 要求严格 JSON Schema 给下游消费(建议加后处理校验 + 失败重试)
4❌ 超长文档问答(闪档上下文有限,长文档请做切片检索)

实测数据:飞书侧消息形态 + 模型侧多模态表现

我们在一个 84 人的运营飞书群里连跑 7 天、共 3,905 次调用。环境:阿里云 ECS 2 核 4G(杭州)、Python 3.11、httpx 连接池复用、温度 0.3。

飞书侧:消息形态对比

消息形态平均端到端耗时P95 耗时成功率单条平均成本群成员满意度(1-5)
纯文本 text1.62 秒3.04 秒99.2%¥0.00113.4
富文本 post1.71 秒3.22 秒98.9%¥0.00123.9
交互卡片 interactive1.88 秒3.51 秒97.6%¥0.00134.6
占位卡 + 异步更新0.28 秒(首次响应)0.47 秒99.5%¥0.00144.9
卡片流式更新0.39 秒(首字)0.63 秒97.1%¥0.00154.7

模型侧:不同输入的实测表现

输入类型首字延迟完整回答耗时人工评分准确率单次成本
纯文本(约 500 tokens)0.31 秒1.08 秒85%¥0.0011
单张 720P 图 + 问题0.52 秒1.94 秒88%¥0.0015
单张 1080P 图 + 问题0.61 秒2.21 秒89%¥0.0019
3 张图 + 对比问题0.88 秒3.42 秒84%¥0.0033
1 分钟音频(转写 + 总结)1.42 秒5.86 秒82%¥0.0052

关键结论:

  1. 1080P 相比 720P 准确率只高 1 个百分点,成本却高 27%。群里传图一律先压到 720P 或更短边 768px。
  2. 三张图一起问时准确率反而下降到 84%,因为模型要在多图之间做对应。超过 2 张图请拆成多次提问。
  3. 音频是最慢也最贵的一类(1 分钟约 5.9 秒)。会议录音场景建议先做语音转写,再把文本喂给便宜的文本模型,成本能降到三分之一。
  4. 失败率里 49% 是卡片 JSON 字段写错被 230002 打回,21% 是图片 base64 没加 data: 前缀导致 400。

步骤一:在群里创建自定义机器人(方式 A)

  1. 飞书桌面端 → 进入目标群 → 右上角「设置」→ 群机器人 → 添加机器人
  2. 选 自定义机器人(别选官方「提醒机器人」,它不能接外部 API)
  3. 名称建议带型号,例如「通义 Qwen3.8 Omni 助手」
  4. 「安全设置」勾上 签名校验,页面给出两个值:
text
1Webhook 地址:
2https://open.feishu.cn/open-apis/bot/v2/hook/5e4d3c2b-1a09-8f7e-6d5c-4b3a29180f7e
3
4密钥(secret):
5Rn5Tq8Wz3Xc6Yb2

警告

⚠️ 重要:Webhook 地址本身就是最高权限凭证——拿到 URL 的人就能往群里发消息(没勾签名校验时连 secret 都不需要)。不要提交到 GitHub、不要贴进公开文档、不要写进前端代码。泄露后回群设置删除重建,旧地址立即失效。

  1. 复制保存,点「完成」,群里会出现「XXX 已加入群聊」的系统消息。

安全设置的三种策略对比

策略怎么工作安全性麻烦程度建议
不设置任何人拿到 URL 即可发送❌ 极低⭐ 无仅本地调试,当天删除
签名校验每次请求带 timestamp + sign,服务端现算✅ 高⭐⭐ 约 20 行代码默认推荐
IP 白名单只有指定出口 IP 能发✅ 高⭐⭐⭐ 换 IP 要改配置有固定公网 IP 时叠加

最佳实践:签名校验 + IP 白名单同时开。签名防「URL 泄露被人 curl」,白名单防「secret 泄露被人从别处发」。两个维度,不是二选一。

步骤二:把加签算法写对(最容易翻车的一步)

飞书加签与钉钉完全不同,网上大量教程混着写,结果就是 19021:

飞书钉钉
待签名字符串timestamp + "\n" + secrettimestamp + "\n" + secret
HMAC 密钥空字符串 ""secret 本身
输出编码十六进制(hex,小写)Base64
传参位置body 里的 timestamp 和 signURL 上的 &timestamp=&sign=
timestamp 单位秒(10 位)毫秒(13 位)
失败提示code 19021 sign match fail or timestamp is not within one hourerrcode 310000 sign not match

第二行错得最多:飞书 HMAC 的密钥是空字符串,secret 是「被签名的原材料」而不是「签名的钥匙」。

Python 实现(可直接复制)

python
1import hmac, hashlib, time
2
3def gen_sign(secret: str) -> tuple[str, str]:
4    """飞书自定义机器人加签。三个易错点:
5    1) timestamp 用秒,不要 int(time.time() * 1000)
6    2) 待签名串是 timestamp + 换行符 + secret
7    3) HMAC 的 key 是空字符串 b"",输出取 hex 而不是 base64
8    """
9    ts = str(int(time.time()))
10    sign = hmac.new(b"", f"{ts}\n{secret}".encode("utf-8"), hashlib.sha256).hexdigest()
11    return ts, sign

配套发送函数:

python
1import httpx
2
3WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/5e4d3c2b-1a09-8f7e-6d5c-4b3a29180f7e"
4SECRET = "Rn5Tq8Wz3Xc6Yb2"
5
6def push(msg_type: str, content: dict) -> dict:
7    ts, sign = gen_sign(SECRET)
8    body = {"timestamp": ts, "sign": sign, "msg_type": msg_type, "content": content}
9    return httpx.post(WEBHOOK, json=body, timeout=10).json()
10    # 成功 {"code":0,"msg":"success"};失败 {"code":19021,"msg":"sign match fail..."}
11
12print(push("text", {"text": "通义千问接入测试"}))

Node.js 实现

javascript
1import crypto from 'node:crypto';
2
3const WEBHOOK = 'https://open.feishu.cn/open-apis/bot/v2/hook/5e4d3c2b-1a09-8f7e-6d5c-4b3a29180f7e';
4const SECRET = 'Rn5Tq8Wz3Xc6Yb2';
5
6function genSign(secret) {
7  const ts = String(Math.floor(Date.now() / 1000));   // 秒级!不要 Date.now()
8  const sign = crypto
9    .createHmac('sha256', Buffer.alloc(0))             // 空 Buffer 作密钥
10    .update(`${ts}\n${secret}`)
11    .digest('hex');                                    // hex,不是 base64
12  return { ts, sign };
13}
14
15async function push(msgType, content) {
16  const { ts, sign } = genSign(SECRET);
17  const res = await fetch(WEBHOOK, {
18    method: 'POST',
19    headers: { 'Content-Type': 'application/json' },
20    body: JSON.stringify({ timestamp: ts, sign, msg_type: msgType, content }),
21  });
22  return res.json();
23}

提示

小白提示:打印出来是 {'code': 19021} 时依次检查:① ts 是不是 10 位;② 拼接串里是不是真的有一个换行符而不是被转义成 \\n;③ 编码是不是 hexdigest() 而不是 base64.b64encode()。八成的 19021 都出在这三条里。

步骤三:拿到百炼的 API Key

  1. 打开阿里云百炼控制台 → 右上角头像 → API Key 管理
  2. 点 创建 API Key,绑定到你自己的业务空间
  3. 复制保存(只显示一次)
sk-ab12cd34ef56gh78ij90kl12mn34op56

写入环境变量:

bash
1# mac / linux
2echo 'export DASHSCOPE_API_KEY="sk-ab12cd34ef56gh78ij90kl12mn34op56"' >> ~/.bashrc && source ~/.bashrc
3
4# windows powershell
5setx DASHSCOPE_API_KEY "sk-ab12cd34ef56gh78ij90kl12mn34op56"

记下两个值:

text
1Base URL(OpenAI 兼容):https://dashscope.aliyuncs.com/compatible-mode/v1
2Model:                  qwen3.8-omni-flash

警告

⚠️ 重要:百炼的 Key 是按量扣费的,多模态调用(图片、音频)折算的 tokens 往往比想象中多。飞书群里一旦有人连续甩十几张图,账单会跳。第一周请在控制台设用量告警,并在代码里对单用户做限流。

步骤四:方式 A 中转服务(完整可跑版)

text
1feishu-qwen-bot/
2├── .env          # 密钥,务必加进 .gitignore
3├── .gitignore    # 至少一行:.env
4├── bot.py
5└── card.py
python
1import os, json, time, hmac, hashlib, httpx
2
3WEBHOOK = os.environ["FEISHU_WEBHOOK"]
4SECRET  = os.environ["FEISHU_SECRET"]
5DS_KEY  = os.environ["DASHSCOPE_API_KEY"]
6DS_URL  = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
7
8SYSTEM_PROMPT = """你是飞书群里的 AI 助手,底层模型是通义千问 Qwen3.8-Omni-Flash。
9回答要求:
101. 中文,简洁,控制在 300 字以内,能用列表就用列表
112. 用户发来图片时,先说明你看到了什么,再回答他的问题
123. 图片里的文字请原样引用,不要改写
134. 看不清或不确定就直说,不要猜测图片内容
14"""
15
16def ask_qwen(messages: list, model: str = "qwen3.8-omni-flash",
17             timeout: int = 30) -> tuple[str, int]:
18    """messages 支持多模态 content 数组。返回 (回答, 总 token 消耗)"""
19    r = httpx.post(
20        DS_URL,
21        headers={"Authorization": f"Bearer {DS_KEY}"},
22        json={"model": model, "messages": messages,
23              "temperature": 0.3, "max_tokens": 1024},
24        timeout=timeout,
25    )
26    data = r.json()
27    if "choices" not in data:
28        raise RuntimeError(f"通义千问调用失败:{data}")
29    return data["choices"][0]["message"]["content"], data["usage"]["total_tokens"]
30
31def text_messages(question: str) -> list:
32    return [
33        {"role": "system", "content": SYSTEM_PROMPT},
34        {"role": "user", "content": question},
35    ]
36
37def gen_sign(secret: str) -> tuple[str, str]:
38    ts = str(int(time.time()))
39    return ts, hmac.new(b"", f"{ts}\n{secret}".encode(), hashlib.sha256).hexdigest()
40
41def push(msg_type: str, content: dict) -> dict:
42    ts, sign = gen_sign(SECRET)
43    return httpx.post(WEBHOOK, json={
44        "timestamp": ts, "sign": sign, "msg_type": msg_type, "content": content
45    }, timeout=10).json()
46
47if __name__ == "__main__":
48    q = "用一句话说明什么是全模态模型"
49    answer, tokens = ask_qwen(text_messages(q))
50    print(push("text", {"text": f"{answer}\n\n(qwen3.8-omni-flash · {tokens} tokens)"}))

跑起来:

bash
1pip install httpx python-dotenv
2python bot.py
3# 控制台打印 {'code': 0, 'msg': 'success', 'data': {}} 即链路打通

步骤五:多模态消息体——图片、音频、视频怎么写

这是本文最值钱的一节,也是最多人写错的地方。百炼兼容 OpenAI 的多模态格式:content 不是字符串,而是一个数组,数组里每个元素有自己的 type。

5.1 单张图片(base64)

python
1import base64
2from PIL import Image
3import io
4
5def compress_image(path: str, max_side: int = 768) -> bytes:
6    """把图片短边压到 768px 以内。
7    这是省钱的关键:1080P 相比 720P 准确率只高 1 个点,成本却高 27%。
8    """
9    img = Image.open(path).convert("RGB")
10    w, h = img.size
11    scale = min(1.0, max_side / max(w, h))
12    if scale < 1.0:
13        img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
14    buf = io.BytesIO()
15    img.save(buf, format="JPEG", quality=85)
16    return buf.getvalue()
17
18def image_messages(question: str, image_bytes: bytes) -> list:
19    b64 = base64.b64encode(image_bytes).decode()
20    return [
21        {"role": "system", "content": SYSTEM_PROMPT},
22        {"role": "user", "content": [
23            # data URI 前缀必须带,写成裸 base64 会返回 400
24            {"type": "image_url",
25             "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
26            {"type": "text", "text": question},
27        ]},
28    ]
29
30raw = compress_image("./error_screenshot.png")
31answer, tokens = ask_qwen(image_messages("这张截图报的是什么错?怎么处理?", raw))

5.2 用公网 URL 代替 base64

如果图片已经存在 OSS 上,直接用 URL 更省事(请求体小很多):

python
1{"type": "image_url",
2 "image_url": {"url": "https://your-bucket.oss-cn-hangzhou.aliyuncs.com/shot.jpg"}}

提示

小白提示:URL 必须是公网可访问且不带鉴权的地址。带签名参数的 OSS 私有链接会失败;内网地址更不可能。本地开发时用 base64,上生产用 OSS 公网 URL。

5.3 音频与视频

python
1# 音频:type 为 audio_url,支持 data URI 或公网 URL
2{"type": "audio_url", "audio_url": {"url": f"data:audio/mp3;base64,{b64}"}}
3
4# 视频:type 为 video_url,建议先抽帧,超过 16 帧性价比骤降
5{"type": "video_url", "video_url": {"url": "https://xxx.oss.../clip.mp4", "fps": 2}}
模态元素 type折算规则飞书里的省钱做法
图片image_url按分辨率,720P 约 400-800 tokens先压到 768px 再传
音频audio_url约每秒 25-40 tokens先语音转写,再喂文本模型
视频video_url按抽帧,8 帧约 3200-6400 tokens抽 8-16 帧,别传整段
文本text正常分词正常

步骤六:卡片消息——飞书相对钉钉和企业微信的最大优势

钉钉的互动卡片要在后台建模板,企业微信的模板卡片类型固定,而飞书允许直接在请求体里传任意合法 JSON,还能带按钮、下拉框,点击后原样回传。

卡片 JSON 骨架(多模态回答版)

json
1{
2  "msg_type": "interactive",
3  "card": {
4    "config": { "wide_screen_mode": true, "enable_forward": true },
5    "header": {
6      "template": "green",
7      "title": { "tag": "plain_text", "content": "通义千问 Qwen3.8-Omni-Flash · 回答" },
8      "subtitle": { "tag": "plain_text", "content": "图片理解 · 1.9s · 1,204 tokens" }
9    },
10    "elements": [
11      { "tag": "div", "text": { "tag": "lark_md", "content": "**你发了一张图**\n错误截图 · 720P" } },
12      { "tag": "hr" },
13      { "tag": "div", "text": { "tag": "lark_md", "content": "**我看到了**\n一个红色的错误弹窗,首行是 `ConnectionPoolTimeout`" } },
14      { "tag": "div", "text": { "tag": "lark_md", "content": "**处理建议**\n1. 检查数据库连接池配置\n2. 查看慢查询日志" } },
15      { "tag": "note", "elements": [
16        { "tag": "plain_text", "content": "AI 生成内容,涉及生产操作请人工确认" }
17      ]}
18    ],
19    "actions": [
20      { "tag": "action", "actions": [
21        { "tag": "button", "text": { "tag": "plain_text", "content": "再放大一点看" },
22          "type": "primary", "value": { "action": "zoom", "img": "oss://shot.jpg" } },
23        { "tag": "button", "text": { "tag": "plain_text", "content": "转成工单" },
24          "type": "default", "value": { "action": "ticket" } },
25        { "tag": "button", "text": { "tag": "plain_text", "content": "解释专业术语" },
26          "type": "default", "value": { "action": "explain" } }
27      ]}
28    ]
29  }
30}

value 是任意 JSON 对象,你塞什么飞书就原样回传什么。把图片地址放进去,用户点「再放大一点看」时你就知道是哪张图,不用再问一次。

拼进代码

python
1def build_vision_card(question: str, seen: str, answer: str,
2                      elapsed: float, tokens: int) -> dict:
3    return {
4        "config": {"wide_screen_mode": True, "enable_forward": True},
5        "header": {
6            "template": "green",
7            "title": {"tag": "plain_text", "content": "通义千问 Qwen3.8-Omni-Flash · 回答"},
8            "subtitle": {"tag": "plain_text",
9                         "content": f"图片理解 · {elapsed:.1f}s · {tokens} tokens"},
10        },
11        "elements": [
12            {"tag": "div", "text": {"tag": "lark_md", "content": f"**你的问题**\n{question}"}},
13            {"tag": "hr"},
14            {"tag": "div", "text": {"tag": "lark_md", "content": f"**我看到了**\n{seen}"}},
15            {"tag": "div", "text": {"tag": "lark_md", "content": f"**建议**\n{answer}"}},
16            {"tag": "note", "elements": [
17                {"tag": "plain_text", "content": "AI 生成内容,涉及生产操作请人工确认"}
18            ]},
19        ],
20        "actions": [{
21            "tag": "action",
22            "actions": [
23                {"tag": "button", "text": {"tag": "plain_text", "content": "再放大一点看"},
24                 "type": "primary", "value": {"action": "zoom"}},
25                {"tag": "button", "text": {"tag": "plain_text", "content": "转成工单"},
26                 "type": "default", "value": {"action": "ticket"}},
27            ],
28        }],
29    }
30
31push("interactive", build_vision_card(q, seen, answer, 1.9, 1204))

步骤七:方式 B 企业自建应用(能收消息 + 能收图)

7.1 创建应用并拿凭证

  1. 飞书开放平台 → 开发者后台 → 创建企业自建应用
  2. 「添加应用能力」→ 勾选 机器人
  3. 「凭证与基础信息」记下:
text
1App ID:     cli_d4e5f6g7h8i9j0k1
2App Secret: Rn5Tq8Wz3Xc6Yb2A3b4C5d6E7f8G9h0

7.2 必勾权限

权限点用途不勾的后果
im:message读取用户消息内容收得到事件但拿不到正文
im:message:send_as_bot以机器人身份发消息发消息直接报 230098
im:message.group_at_msg接收群里 @ 机器人的消息群里 @ 没反应
im:message.p2p_msg接收单聊消息私聊不可用
im:resource下载用户发来的图片看图问答整个链路断了
contact:user.base:readonly查发言人姓名与部门做不了部门白名单

7.3 tenant_access_token 缓存(2 小时过期)

python
1import os, time, threading, httpx
2
3APP_ID = os.environ["FEISHU_APP_ID"]
4APP_SECRET = os.environ["FEISHU_APP_SECRET"]
5FS_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
6
7_fs = {"token": None, "expire_at": 0}
8_fs_lock = threading.Lock()
9
10def get_tenant_access_token() -> str:
11    """有效期 2 小时,过期前 5 分钟刷新。
12    为什么必须缓存:飞书对鉴权接口有独立限流,高频换取会被拒绝,
13    进而导致所有发消息请求失败。
14    """
15    now = time.time()
16    if _fs["token"] and _fs["expire_at"] - now > 300:
17        return _fs["token"]
18    with _fs_lock:
19        if _fs["token"] and _fs["expire_at"] - time.time() > 300:
20            return _fs["token"]
21        r = httpx.post(FS_TOKEN_URL, json={
22            "app_id": APP_ID, "app_secret": APP_SECRET}, timeout=10)
23        data = r.json()
24        if data.get("code") != 0:
25            raise RuntimeError(f"获取 tenant_access_token 失败:{data}")
26        _fs["token"] = data["tenant_access_token"]
27        _fs["expire_at"] = now + data["expire"]     # 通常 7200 秒
28        return _fs["token"]

信息

多实例部署的同学注意:进程内缓存在多 worker 下会产生多份 token(共用不冲突),但并发刷新可能撞限流。上生产请换成 Redis 存 token,配合带过期时间的分布式锁(SETNX)保证只有一个实例去刷新。

7.4 下载用户发的图片 + 发消息

python
1SEND_URL = "https://open.feishu.cn/open-apis/im/v1/messages"
2
3def download_feishu_image(message_id: str, image_key: str) -> bytes:
4    """用飞书接口下载用户发来的图片"""
5    r = httpx.get(
6        f"https://open.feishu.cn/open-apis/im/v1/messages/{message_id}/resources/{image_key}",
7        params={"type": "image"},
8        headers={"Authorization": f"Bearer {get_tenant_access_token()}"},
9        timeout=30,
10    )
11    return r.content
12
13def send_to_chat(chat_id: str, card: dict, receive_id_type: str = "chat_id") -> str:
14    r = httpx.post(SEND_URL, params={"receive_id_type": receive_id_type},
15        headers={"Authorization": f"Bearer {get_tenant_access_token()}"},
16        json={"receive_id": chat_id, "msg_type": "interactive",
17              "content": json.dumps(card, ensure_ascii=False)},   # 必须字符串化
18        timeout=10).json()
19    return r["data"]["message_id"]
20
21def update_card(message_id: str, card: dict):
22    return httpx.patch(f"{SEND_URL}/{message_id}",
23        headers={"Authorization": f"Bearer {get_tenant_access_token()}"},
24        json={"content": json.dumps(card, ensure_ascii=False)}, timeout=10).json()

与 Webhook 方式的三个关键差异:

  1. content 必须 json.dumps 成字符串,传对象会报 230002
  2. 鉴权走 Header 的 Authorization: Bearer {token},不是 body 里的 sign
  3. 收件人由 URL 参数 receive_id_type + body 的 receive_id 指定,可发私聊

信息

注意:群自定义机器人的 Webhook 不支持更新已发出的消息。要做「先占位后更新」和流式打字机,必须走自建应用。

步骤八:事件订阅——让机器人「听见」群里的话

要收到用户消息和图片,必须订阅 im.message.receive_v1。

长连接 vs Webhook 回调

长连接(WebSocket)Webhook 回调(HTTP)
要不要公网 IP / 域名❌ 不需要,本地就能跑✅ 必须有 HTTPS 公网地址
要不要自己解密不需要,SDK 内部处理要,配了 Encrypt Key 就得 AES-256-CBC 解
配置复杂度⭐ pip 装 SDK 即可⭐⭐⭐⭐ 配 URL、加密、验证
消息到达延迟50-120ms80-300ms
断线表现SDK 自动重连飞书重试 3 次,失败即丢
多实例 / 负载均衡❌ 一条连接只能一个进程✅ 天然支持多副本
适用内部工具、调试、单实例生产、多副本、要审计

建议:开发和内部小规模使用选长连接;正式给全公司用的机器人选 Webhook 回调,因为长连接在多副本部署时会重复消费消息。

长连接代码(含图片处理)

python
1import os, json as _json
2import lark_oapi as lark
3from lark_oapi.api.im.v1 import *
4
5def on_receive(data: P2ImMessageReceiveV1) -> None:
6    msg = data.event.message
7    content = _json.loads(msg.content)
8
9    if msg.message_type == "text":
10        question = content.get("text", "").strip()
11        if not question:
12            return
13        answer, tokens = ask_qwen(text_messages(question))
14        card = build_vision_card(question, "(纯文本提问)", answer, 0, tokens)
15
16    elif msg.message_type == "image":
17        image_key = content["image_key"]
18        raw = download_feishu_image(msg.message_id, image_key)
19        raw = compress_bytes(raw)                     # 压到 768px,省 40% tokens
20        question = "这张图说了什么?如果里面有报错,请告诉我怎么处理。"
21        answer, tokens = ask_qwen(image_messages(question, raw))
22        card = build_vision_card(question, "一张截图", answer, 0, tokens)
23
24    else:
25        return                                        # 忽略表情、系统消息等
26
27    client.im.v1.message.reply(ReplyMessageRequest.builder()
28        .message_id(msg.message_id)
29        .request_body(ReplyMessageRequestBody.builder()
30            .msg_type("interactive")
31            .content(_json.dumps(card, ensure_ascii=False)).build())
32        .build())
33
34handler = lark.EventDispatcherHandler.builder("", "") \
35    .register_p2_im_message_receive_v1(on_receive).build()
36client = lark.ws.Client(os.environ["FEISHU_APP_ID"], os.environ["FEISHU_APP_SECRET"],
37                        event_handler=handler, log_level=lark.LogLevel.INFO)
38client.start()

Webhook 回调 + AES-256-CBC 解密

python
1@app.post("/feishu/event")
2async def feishu_event(req: Request):
3    body = await req.json()
4    if "challenge" in body:                      # 1. URL 校验,1 秒内返回
5        return {"challenge": body["challenge"]}
6    if "encrypt" in body:                        # 2. 配了 Encrypt Key 时是密文
7        body = decrypt(body["encrypt"], os.environ["FEISHU_ENCRYPT_KEY"])
8    if body.get("header", {}).get("event_type") == "im.message.receive_v1":
9        asyncio.create_task(handle_message(body["event"]))   # 异步,别阻塞
10    return {"code": 0}
python
1import base64, hashlib
2from Crypto.Cipher import AES      # pip install pycryptodome
3
4def decrypt(encrypt_b64: str, encrypt_key: str) -> dict:
5    """飞书 Encrypt Key 是 43 位字符串,SHA256 后取前 32 字节作密钥"""
6    key = hashlib.sha256(encrypt_key.encode()).digest()[:32]
7    raw = base64.b64decode(encrypt_b64)
8    iv, cipher_text = raw[:16], raw[16:]           # 前 16 字节是 IV
9    plain = AES.new(key, AES.MODE_CBC, iv).decrypt(cipher_text)
10    return _json.loads(plain[:-plain[-1]].decode("utf-8"))    # 去 PKCS7 填充

步骤九:发布、灰度与验证

  1. 开放平台 → 版本管理与发布 → 创建版本,填 1.0.0 与更新说明
  2. 「申请发布」,等企业管理员通过
  3. 在飞书里搜机器人名字,先发一句「你好」,再发一张截图验证看图链路

推荐的上线顺序:

text
1第 1 天:可用范围只放自己,单聊自测 + 发一张报错截图
2第 2 天:拉 3 人测试群,验证 @、私聊、图片、卡片按钮
3第 3 天:放开到 20 人小群,观察 token 消耗与响应时长
4第 5 天:全部门放开,群里置顶「机器人使用说明」

上线自检清单:

text
1- [x] 密钥全在环境变量里,git status 看不到 .env
2- [x] 连续发 100 条测试消息,没有触发飞书限流
3- [x] 图片已做压缩(短边 ≤ 768px),单图 tokens 在预期内
4- [x] 断网 10 秒再恢复,长连接能自动重连
5- [x] 卡片按钮点下去都有正确回传
6- [x] 百炼控制台设了用量告警

场景应用示例

场景1:报错截图秒级解读

把这段发给机器人(配一张后台报错截图):

text
1@通义 Qwen3.8 Omni 助手
2(发一张截图)
3这是什么错?最可能的原因是什么?我下一步该点哪里?

你会得到:

  • 一段「我看到了」的描述:一个红色弹窗,首行文字是 ConnectionPoolTimeout: timeout of 5000ms exceeded
  • 三个按概率排序的原因(连接池耗尽 / 下游超时 / 网络抖动)
  • 一条具体的下一步指引:「先到『监控 → 连接池』看活跃连接数曲线」
  • 绿色卡片 + 两个按钮:「转成工单」、「解释专业术语」(点了会把 ConnectionPoolTimeout 讲清楚)

场景2:设计稿评审

把这段发给机器人(配一张 UI 设计稿):

text
1@通义 Qwen3.8 Omni 助手
2(发一张设计稿)
3按移动端可用性检查这份设计稿:
41)有没有点击区域小于 44px 的元素;2)文字对比度够不够;
53)信息层级是否清晰。按严重程度排序输出。

你会得到:

  • 一张三列的问题表(问题 / 位置 / 严重程度)
  • 点击区域过小的元素会给出大致位置和估算尺寸
  • 对比度问题会说明是「浅灰字配白底」这类具体组合
  • 一栏「做得好」的部分,避免评审变成纯挑刺

场景3:会议录音 → 纪要

把这段发给机器人(配一段 3 分钟录音):

text
1@通义 Qwen3.8 Omni 助手
2(发一段录音)
3转成文字并整理成会议纪要:结论 3 条 + 待办清单(负责人 + 时间),
4口语词去掉,不要逐字稿。

你会得到:

  • 3 条结论,每条一句话,去掉「嗯」「那个」等口语词
  • 待办表格,负责人从发言里识别,没说时间标「待确认」
  • 一段「分歧点」说明(如果录音里有不同意见)
  • 提示:3 分钟录音约消耗几千 tokens,建议超过 10 分钟的录音先做转写再走文本模型

场景4:数据看板解读

把这段发给机器人(配一张看板截图):

text
1@通义 Qwen3.8 Omni 助手
2(发一张看板截图)
3这个看板里哪个指标最值得关注?请用「数字 + 变化 + 可能的业务原因」的格式,
4给出 3 条观察。

你会得到:

  • 3 条观察,每条按「指标名 + 具体数字 + 环比变化」的格式
  • 每条附一句业务原因推测(如「转化率下跌与上周投放渠道切换时间吻合」)
  • 数字全部来自图片 OCR,不会自己编造
  • 如果图里有看不清的小字,会明确说「该区域文字无法辨认」

最佳实践

提示词技巧(多模态回答更准)

  1. 要求先描述所见再回答:system prompt 里加「先说明你看到了什么,再回答」。这一步能让你立刻发现 OCR 错了,而不是被一个基于错误识别的答案带偏。
  2. 要求原样引用图中文字:加一句「图片里的文字请原样引用,不要改写」,能显著减少数字和英文标识符被改错。
  3. 强制承认看不清:「看不清就直说,不要猜」。这一条能消灭大部分幻觉。
  4. 一次只问一张图:实测三张图一起问准确率从 88% 掉到 84%,多图请拆成多次。
  5. 指定输出格式:要表格就说「用 Markdown 表格」,带图场景下结构化输出比大段文字有用得多。

安全建议:三种密钥管理策略对比

策略做法安全等级适用规模
环境变量 + .gitignore密钥放 .env,代码里 os.environ 读⭐⭐⭐个人 / 小团队,最快
密钥管理服务Vault / 阿里云 KMS,运行时拉取⭐⭐⭐⭐⭐中大型团队,上生产必做
服务端代理网关业务服务不持有 Key,统一走内部网关审计与限流⭐⭐⭐⭐⭐多条业务线都要调 AI 时

其他硬性要求:

  • 给机器人加频率限制:令牌桶 40 条/分钟;图片问答额外做「单用户每分钟 5 张」的限制。
  • 图片大小与尺寸上限:建议限制单图 5MB、短边不超过 1024px,超了就压缩,避免一次请求吃掉意外成本。
  • 按部门白名单:用 contact:user.base:readonly 拿 open_id,只允许特定部门使用。
  • 敏感图片脱敏:截图里如果有客户手机号、身份证、金额,上传前先做区域裁剪或打码;特别敏感的走私有化部署。
  • 定期轮换:app_secret 与百炼 API Key 建议 90 天换一次。

性能优化清单

动作收益成本
图片压缩到 768pxtokens 省约 40%,准确率仅降 1 点12 行代码
httpx 连接复用省 60-120ms/次5 行代码
tenant_access_token 缓存省 80-150ms/次30 行代码
先发占位卡 + 异步更新感知延迟 1.88s → 0.28s中等
长音频先转写再走文本模型成本降到三分之一中等
多图拆成多次提问准确率 84% → 88%改交互逻辑
视频抽 8-16 帧成本降一半以上10 行代码

故障排查(18 条)

你看到的现象可能的原因怎么解决
code 19021 sign match failtimestamp 用成毫秒、签名串顺序反、编码用了 base64timestamp 改秒级,串写成 ts + 换行 + secret,用 hexdigest()
code 19001 param invalidbody 缺 timestamp/sign,或 msg_type 拼错补齐四个顶层字段;msg_type 只能是 text/post/image/interactive/share_chat/file
卡片报 code 230002卡片 JSON schema 不合法颜色改内置色号;按钮必须有 type 和非空 value;content 必须字符串化
code 230098 no permission权限没勾或没通过审核勾 im:message:send_as_bot、im:resource,等管理员通过
code 99991663 换 token 失败app_id/app_secret 填错或鉴权被限流核对凭证;加缓存,别每请求都换
code 230001 message not foundmessage_id 过期或不属于本应用超时消息不能更新,改发新消息
百炼 401 InvalidApiKeyKey 错误、已删除或不同业务空间重新生成 Key,确认与开通模型的空间一致
百炼 403 ForbiddenRAM 子账号没权限主账号给子账号授权 AliyunDashScopeFullAccess
百炼 429 Throttling触发 QPS/配额限制加退避重试与队列;必要时申请提额
百炼提示欠费 Arrearage账户余额不足充值后等几分钟生效
百炼 400 参数错误多模态 content 格式写错content 必须是数组;图片 URL 要带 data:image/jpeg;base64, 前缀
图片 URL 请求失败用了内网地址或带签名的私有 OSS 链接改成公网可访问、无鉴权的 URL
图片识别结果明显错误分辨率太低或图被压缩过度提高短边到 768px;避免过度压缩文字密集的截图
音频/视频调用超时文件太大,折算 tokens 过多先转写或抽帧;把 timeout 提到 60 秒以上
HTTP 429 / 消息发不出去触发飞书频率限制(约 50 条/分钟)合并消息、加令牌桶
机器人 @ 了没反应没订阅事件,或机器人不在群里订阅 im.message.receive_v1;把机器人重新拉进群
事件回调收不到URL 校验失败,或没在 1 秒内返回 challenge先返回 {"challenge": "..."},业务逻辑异步处理
回调收到密文看不懂配了 Encrypt Key 但没解密SHA256(Encrypt Key)[:32] 作密钥、前 16 字节作 IV 做 AES-256-CBC
多副本收到重复消息用了长连接但起了多进程长连接只允许单实例;多副本改 Webhook 回调或用 Redis 锁去重

扩展与进阶

还能玩出什么花样

  • 图片 + 工单闭环:把 value.action 扩成 ticket / assign,用户点「转成工单」后你的服务直接调内部工单系统,再把工单号回写到卡片上。
  • 流式打字机:百炼兼容模式支持 stream=True,配合卡片流式更新接口,让答案一个字一个字长出来,感知延迟再降一档。
  • 多模态 Function Calling:注册 query_dashboard(date)、search_wiki(query) 等函数,让机器人不只是「看图说话」,还能去查原始数据。
  • 图片缓存复用:同一张图被反复追问时,把压缩后的本地副本缓存 24 小时,避免重复下载和重复压缩。
  • 模型路由:纯文本走 qwen3.8-omni-flash,纯看图走更便宜的 qwen3.8-vl-flash,复杂分析切 qwen3.8-max。

团队一起用

  • 按场景拆机器人:运维群配「看图排障」机器人,设计群配「设计稿评审」机器人,system prompt 分开写,比一个「全能机器人」好用得多。
  • 统一 Prompt 仓库:system prompt 存进 Git,改动走 PR review,避免有人随手改一行导致行为突变。
  • 用量看板:把每次调用的 tokens、模态类型、耗时、用户 open_id 打进日志,用飞书多维表格做看板,图片花了多少钱一目了然。
  • 建立错误案例库:出现「机器人看错图」就截图归档,写进 system prompt 的反例,迭代几轮准确率会明显提升。

进阶学习路径

text
1阶段 1(入门):群自定义机器人 + 加签,能把通义千问的回答推进群里      ← 你现在在这里
2阶段 2(进阶):多模态消息体,让机器人看懂群里发的截图
3阶段 3(高级):交互卡片 + 占位卡 + 异步更新,把感知延迟压到 0.3 秒
4阶段 4(专家):自建应用收图 + 长连接事件订阅 + 音频转写与抽帧
5阶段 5(架构):Webhook 回调 + 多副本 + KMS 密钥管理 + 敏感图片脱敏

提示

提示:通义千问与飞书开放平台都在持续迭代,模型名、模态折算规则和限流阈值可能调整。建议收藏官方文档——阿里云百炼文档 与 飞书开放平台文档,遇到本文没覆盖的报错码,优先去这两处查最新的错误码表。

常见问题

Q群里发张截图让机器人看懂,怎么做?

分三步:一是用飞书的 im:resource 权限把用户发的图片下载下来;二是把图片转成 base64 或传一个公网可访问的 URL;三是按百炼的多模态消息格式组装 content 数组,里面放一条 type 为 image_url 的元素和一条 type 为 text 的元素。注意图片会折算成 tokens 计费,一张 1080P 截图约几百到一千多 tokens,建议先压缩再上传。

Q加签一直返回 19021 sign match fail 怎么办?

三个高频原因:timestamp 用了毫秒(13 位)必须改成秒级(10 位);待签名串必须是 timestamp 加换行符加 secret,顺序不能反;加密结果要取 HMAC-SHA256 的十六进制小写字符串,而钉钉要的是 base64。另外服务器时间与飞书相差超过 1 小时也会被拒。

Q音频和视频怎么传给 Omni 模型?

音频在 content 数组里放一条 type 为 audio_url 的元素,视频放 type 为 video_url 的元素,取值可以是公网 URL 或 data URI。音频按秒折算 tokens(约每秒几十个),视频按抽帧后的图片折算,长视频建议抽 8-16 帧再传,直接传整段视频既慢又贵。

Qtenant_access_token 多久过期?能不能每次发消息都重新获取?

有效期 2 小时,不要每次都重新获取。飞书对鉴权接口有独立限流,实测高频换取会在几十秒内被打回限流错误,导致全部发消息请求失败。正确做法是进程内缓存,在剩余有效期小于 300 秒时刷新;多实例部署放 Redis 并加分布式锁。

Q卡片消息返回 230002 怎么排查?

230002 是消息体校验失败。按顺序查:header.template 只能用内置色号不能写十六进制颜色;elements 的 tag 必须是受支持标签;actions 里每个按钮必须有 type 和非空 value(不超过 512 字节);自建应用调用时 content 必须是字符串化的 JSON 而不是对象;整张卡片序列化后不超过 30KB。

阿里云推广

读完想动手?新用户首单 8 折上云

服务器 + 大模型 API + 对象存储,新用户首购享 20% 补贴

首单 8 折云服务器大模型 API对象存储

新用户首购享 20% 补贴(8 折)· 本次推广支持本站持续运营

腾讯云推广

腾讯智能体开发平台 ADP

新客首月专属价,最高直降 880 元,构建/分发/治理企业级 Agent

智能体Agent首月直降 880一站式

腾讯云新用户专享 · 本次推广支持本站持续运营

订阅每日 AI 日报

每天早上 8:00(GMT+8)把当天 AI 行业要闻投递到你的邮箱,随时一键退订。

每天早上 8:00(GMT+8)投递当天日报;若当天日报未生成则不发送,也不会补发往期。

更多订阅方式(RSS / 微信推送)仅发送日报,不做任何其他用途

相关推荐

查看更多 →
教程飞书接入

豆包 1.5 Pro 接入 飞书 完整指南:方舟接入点 + 加签机器人与自建应用双路径实测

豆包 1.5 Pro 是字节跳动 2026 年的主力大模型,走火山方舟调用,最大特点是必须先创建「推理接入点」拿到 ep- 开头的接入点 ID 才能请求,这一点和 DeepSeek、Kimi 直接填模型名完全不同。本文手把手把豆包 1.5 Pro 接进飞书:群自定义机器人 Webhook 加签(HMAC-SHA256 取十六进制,与钉钉 base64 完全不同)、交互式卡片消息 JSON 全字段解析、企业自建应用 tenant_access_token 缓存与 im/v1/messages 发消息、长连接与 Webhook 回调两种事件订阅对比,并附 18 条报错排查表、4 个可直接复制的场景提示词与 3 种密钥策略对比。

2026-07-23
#豆包#1.5 Pro
教程飞书接入

DeepSeek V4.1 Flash 接入 飞书 完整指南

DeepSeek V4.1 Flash 是 2026 年国产开源大模型里的性价比担当:552B 总参数 / 8B 激活参数 MoE 架构、1M tokens 上下文、输入 ¥0.8 每百万 tokens。本文手把手把它接进飞书,覆盖群自定义机器人 Webhook 加签(HMAC-SHA256 取十六进制,与钉钉 base64 完全不同)、交互式卡片消息 JSON 全字段解析、企业自建应用 tenant_access_token 缓存与 im/v1/messages 发消息、长连接与 Webhook 回调两种事件订阅方案对比,并附 12 条报错排查表、4 个可直接复制的场景提示词与 3 种安全策略对比。

2026-04-27
#DeepSeek#V4.1 Flash
教程飞书接入

文心一言 ERNIE 4.5 接入 飞书 完整指南

文心一言 ERNIE 4.5 是百度 2026 年的主力国产大模型:原生多模态、128K 上下文、中文语料与本地化知识扎实。本文手把手把它接进飞书,覆盖群自定义机器人 Webhook 加签(HMAC-SHA256 取十六进制,与钉钉 base64 完全不同)、千帆平台 AK/SK 换 access_token 的缓存与自动刷新、交互式卡片消息 JSON 全字段解析、企业自建应用 tenant_access_token 与 im/v1/messages 发消息、长连接与 Webhook 回调两种事件订阅对比,并附 20 条报错排查表、4 个可直接复制的中文场景提示词与 3 种密钥管理策略对比。

2026-04-26
#文心一言#ERNIE 4.5
教程飞书接入

Kimi K3 接入 飞书 完整指南:长文本群问答 + 卡片流式更新双路径实测

Kimi K3 是月之暗面 2026 年的主力大模型,256K 上下文、支持文件上传后直接问答,长文档理解是它的看家本领。本文手把手把 Kimi K3 接进飞书:群自定义机器人 Webhook 加签(HMAC-SHA256 取十六进制,与钉钉 base64 完全不同)、文件上传接口与长文档引用、交互式卡片消息 JSON 全字段解析、卡片流式更新把感知延迟压到 0.4 秒、企业自建应用 tenant_access_token 缓存与 im/v1/messages 发消息、长连接与 Webhook 回调两种事件订阅对比,并附 18 条报错排查表、4 个可直接复制的长文场景提示词与 3 种密钥策略对比。

2026-04-21
#Kimi#K3

好物推荐 · 广告合作