飞书机器人接入博客
从零开始:将你的 Web 项目接入飞书机器人
本文以「智造中枢:销售生产一体化管理系统(SPMS)」为例,手把手演示如何用 Python + Flask + lark-oapi SDK,通过 WebSocket 长连接 将一个已有的 Web 系统接入飞书机器人,实现群聊指令驱动的业务操作。
一、为什么选择飞书机器人?
传统 ERP / MES 系统的痛点是:操作人员必须坐在电脑前打开浏览器才能干活。而在制造业场景中,车间工人、销售外勤更习惯用手机沟通。飞书机器人的价值在于——把系统功能搬进群聊,用一句话完成操作。
| 传统方式 | 飞书机器人方式 |
|---|---|
| 打开浏览器 → 登录 → 找菜单 → 填表单 → 提交 | 群里 @机器人 发一句「报工MO202506001 50件」 |
| 必须在电脑前 | 手机随时随地 |
| 多系统切换 | 一个群搞定 |
二、技术选型:WebSocket 长连接 vs Webhook 回调
飞书开放平台提供了两种事件订阅方式:
| 对比项 | Webhook 回调 | WebSocket 长连接 |
|---|---|---|
| 公网要求 | 需要公网可访问的 URL | ❌ 不需要 |
| 开发复杂度 | 需处理验签、加解密 | SDK 全部内置 |
| 本地调试 | 需内网穿透(frp/ngrok) | 直接本地运行 |
| 适用场景 | 生产环境部署 | 开发调试、内网环境 |
| 卡片交互 | ✅ 支持 card.action.trigger |
❌ 不支持 |
本文选择 WebSocket 长连接,原因很简单:开发阶段不需要公网 IP,本地直接跑,5 分钟就能验证。
三、飞书后台配置
3.1 创建企业自建应用
- 打开 飞书开放平台,登录后进入「开发者后台」
- 点击「创建企业自建应用」,填写应用名称和描述
- 创建完成后,在「凭证与基础信息」页面获取三个关键参数:
App ID: cli_xxxxxxxxxxxxxxxx
App Secret: xxxxxxxxxxxxxxxxxxxxxxxxxx
Verification Token: xxxxxxxxxxxxxxxxxxxxxxxxxx
这三个值后续会写进 Python 代码中。
3.2 配置权限
进入「权限管理」页面,搜索并开通以下权限:
| 权限名称 | 权限标识 | 用途 |
|---|---|---|
| 获取与发送单聊、群组消息 | im:message |
收发消息 |
| 读取用户发给机器人的单聊消息 | im:message.receive_v1 |
接收用户消息 |
| 以应用的身份发消息 | im:message:send_as_bot |
机器人发消息 |
| 获取群组信息 | im:chat:readonly |
获取群聊 ID |
3.3 配置事件订阅
- 进入「事件订阅」页面
- 订阅方式选择 「使用长连接接收事件」(重要!不要选 Webhook)
- 添加事件:搜索
im.message.receive_v1(接收消息),勾选并保存
⚠️ 避坑提示:
card.action.trigger(卡片按钮回调)不支持 WebSocket 长连接,只能走 Webhook。如果你需要卡片交互,必须额外配置公网回调地址。本文用纯文本指令方案,无需卡片。
3.4 发布应用
配置完成后,在「版本管理与发布」页面创建版本并提交审核。企业自建应用通常审核很快。
3.5 将机器人加入群聊
在飞书群聊设置中 →「群机器人」→「添加机器人」→ 选择你创建的应用。
四、安装 SDK
pip install lark-oapi
建议使用清华镜像源加速:
pip install lark-oapi -i https://pypi.tuna.tsinghua.edu.cn/simple
验证安装:
import lark_oapi as lark
from lark_oapi.ws import Client as WsClient
print('SDK 加载成功')
五、编写飞书长连接客户端
5.1 核心架构
整个飞书机器人客户端的架构非常简洁:
飞书服务器 (wss://msg-frontier.feishu.cn)
↕ WebSocket 长连接
本地 Python 进程 (lark_oapi SDK)
↕ 事件回调
handle_message()
↕ 操作数据库
Flask app / SQLAlchemy
5.2 完整代码
以下是项目中 feishu_ws.py 的核心结构,去掉业务逻辑后的骨架:
"""
飞书长连接事件订阅客户端
使用 lark_oapi SDK 的 WebSocket 长连接接收事件,无需公网URL
"""
import json
import os
import sys
import logging
import re
# ========== 1. 应用凭证 ==========
APP_ID = 'cli_xxxxxxxxxxxxxxxx'
APP_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxx'
VERIFICATION_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxxxx'
# ========== 2. 导入 SDK ==========
import lark_oapi as lark
from lark_oapi.api.im.v1 import CreateMessageRequest, CreateMessageRequestBody
from lark_oapi.ws import Client as WsClient
from lark_oapi.event.dispatcher_handler import EventDispatcherHandlerBuilder
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger('feishu_bot')
# ========== 3. 发送消息函数 ==========
def send_message(client, chat_id, text):
"""发送文本消息到飞书群聊"""
try:
req = CreateMessageRequest.builder() \
.receive_id_type('chat_id') \
.request_body(
CreateMessageRequestBody.builder()
.receive_id(chat_id)
.msg_type('text')
.content(json.dumps({'text': text}, ensure_ascii=False))
.build()
) \
.build()
resp = client.im.v1.message.create(req)
if resp.success():
logger.info(f'消息已发送到 {chat_id}')
else:
logger.error(f'发送失败: code={resp.code} msg={resp.msg}')
except Exception as e:
logger.error(f'发送异常: {e}')
# ========== 4. 业务处理函数 ==========
def query_orders():
"""示例:查询最近订单"""
from app import create_app
app = create_app()
with app.app_context():
from models.sales_order import SalesOrder
orders = SalesOrder.query.order_by(
SalesOrder.created_time.desc()
).limit(5).all()
if orders:
lines = ['📋 最近5个订单\n']
for o in orders:
lines.append(f'• {o.order_id} | {o.customer_name} | ¥{float(o.total_amount):,.0f}')
return '\n'.join(lines)
return '暂无订单数据'
# ========== 5. 消息处理路由 ==========
def handle_message(client, event):
"""处理收到的消息事件"""
try:
msg = event.event.message
chat_id = msg.chat_id
message_type = msg.message_type
# 只处理文本消息
if message_type != 'text':
return
# 解析消息内容
content = json.loads(msg.content)
text = content.get('text', '').strip()
# 清理 @机器人 的飞书内部ID
text = re.sub(r'@_user_\d+', '', text).strip()
text_clean = re.sub(r'\s+', '', text.lower())
# ===== 命令路由 =====
# 帮助
if any(kw in text_clean for kw in ['帮助', 'help', '?', '?']) or not text_clean:
reply = (
'📋 机器人命令列表\n\n'
' • 订单 - 查询最近订单\n'
' • 库存 - 查询库存预警\n'
' • 帮助 - 显示本菜单'
)
send_message(client, chat_id, reply)
# 查询订单
elif '订单' in text_clean:
reply = query_orders()
send_message(client, chat_id, reply)
# 未识别
else:
send_message(client, chat_id,
f'未识别命令: "{text[:50]}"\n发送"帮助"查看可用命令')
except Exception as e:
logger.error(f'handle_message error: {e}', exc_info=True)
# ========== 6. 创建事件处理器 ==========
def create_event_handler(client):
"""注册事件处理器"""
builder = EventDispatcherHandlerBuilder(
encrypt_key='', # WebSocket 模式无需加密密钥
verification_token=VERIFICATION_TOKEN,
)
# 注册「接收消息」事件
builder.register_p2_im_message_receive_v1(
lambda event: handle_message(client, event)
)
return builder.build()
# ========== 7. 启动长连接 ==========
def start_ws_client():
"""启动飞书长连接客户端(阻塞)"""
logger.info('飞书长连接客户端启动中...')
# 创建 API Client(用于主动发消息)
client = lark.Client.builder() \
.app_id(APP_ID) \
.app_secret(APP_SECRET) \
.log_level(lark.LogLevel.INFO) \
.build()
# 创建事件处理器
event_handler = create_event_handler(client)
# 创建 WebSocket 长连接客户端
ws_client = WsClient(
app_id=APP_ID,
app_secret=APP_SECRET,
event_handler=event_handler,
log_level=lark.LogLevel.INFO,
auto_reconnect=True, # 断线自动重连
)
logger.info('正在连接飞书服务器...')
logger.info('请在飞书群聊中 @机器人 发送消息测试')
# 启动(阻塞主线程)
ws_client.start()
if __name__ == '__main__':
start_ws_client()
5.3 代码结构解析
整个客户端由 7 个部分组成,下面逐一说明:
| 序号 | 模块 | 作用 |
|---|---|---|
| 1 | 应用凭证 | App ID / App Secret / Token,从飞书后台获取 |
| 2 | SDK 导入 | 导入 lark_oapi 的 Client、WsClient、EventDispatcher |
| 3 | send_message | 封装发消息 API,传入 chat_id 和文本内容 |
| 4 | 业务函数 | 查询/创建/修改/删除等业务逻辑,操作数据库 |
| 5 | handle_message | 消息路由中心,解析用户输入并分发到对应业务函数 |
| 6 | create_event_handler | 用 Builder 注册 im.message.receive_v1 事件 |
| 7 | start_ws_client | 创建 Client + WsClient,调用 ws_client.start() 阻塞运行 |
六、与 Flask 应用集成
飞书机器人需要读写数据库,但你的 Web 应用用的是 Flask + SQLAlchemy 的应用上下文模式。直接在机器人代码中查询数据库会报 RuntimeError: Working outside of application context。
解决方案很简单——在机器人代码中手动创建 Flask 应用上下文:
def query_orders():
from app import create_app
app = create_app()
with app.app_context():
from models.sales_order import SalesOrder
orders = SalesOrder.query.order_by(
SalesOrder.created_time.desc()
).limit(5).all()
# ... 处理查询结果
每个业务函数都独立创建一次
app和app_context,这样既能复用 Flask 的模型定义,又不会与 Web 服务的 Flask 实例冲突。
七、多行文本指令解析
飞书机器人不仅能处理单行命令(如「工单」),还能解析多行结构化输入。在 SPMS 项目中,创建工单的指令是这样的:
新建工单
编码: P001
名称: 铝合金外壳
部门: 第一车间
数量: 1000
开工: 2025-06-25
完工: 2025-07-10
优先级: 3
订单: SO202406001
解析逻辑使用正则按行提取键值对:
def create_mo(text):
import re
# 键名映射表
key_map = {
'编码': 'product_code', '产品编码': 'product_code',
'名称': 'product_name', '产品名称': 'product_name',
'部门': 'production_dept', '生产部门': 'production_dept',
'数量': 'plan_qty', '计划数量': 'plan_qty',
'开工': 'plan_start', '计划开工': 'plan_start',
'完工': 'plan_end', '计划完工': 'plan_end',
'优先级': 'priority',
'订单': 'order_id', '关联订单': 'order_id',
}
# 逐行解析
fields = {}
for line in text.split('\n'):
line = line.strip()
if not line:
continue
m = re.match(r'(.+?)[::]\s*(.+)', line) # 支持中英文冒号
if not m:
continue
key_raw = m.group(1).strip()
val_raw = m.group(2).strip()
if key_raw in key_map:
fields[key_map[key_raw]] = val_raw
# 校验必填字段
required = ['product_code', 'product_name', 'production_dept',
'plan_qty', 'plan_start', 'plan_end']
missing = [f for f in required if f not in fields]
if missing:
return f'❌ 缺少必填字段: {", ".join(missing)}'
# ... 创建工单逻辑
关键设计点:
- 键名映射表支持多种叫法(如「编码」和「产品编码」等价)
- 冒号同时支持中文
:和英文: - 必填字段校验 + 友好的错误提示
- 日期支持多种格式(
2025-06-25、6月25日、2025/06/25)
八、运行多个机器人
在 SPMS 项目中,我们创建了两个独立的飞书应用,分别负责不同业务域:
| 机器人 | App ID | 职责 | 脚本文件 |
|---|---|---|---|
| 销售通用机器人 | cli_aaba20... |
订单 CRUD、库存查询 | feishu_ws.py |
| 生产专用机器人 | cli_aab3fb... |
工单 CRUD、排产、报工 | feishu_ws_production.py |
两个机器人的代码结构完全相同,区别仅在于:
- App ID / App Secret 不同(各自对应一个飞书应用)
- 业务函数不同(销售机器人操作订单,生产机器人操作工单)
- 命令路由不同(各自只响应自己业务域的命令)
这样做的好处是职责隔离——销售群只加销售机器人,生产群只加生产机器人,互不干扰。
九、一键启动脚本
Web 服务和飞书机器人是独立进程,需要同时启动。我们用 start.py 统一管理:
"""
SPMS 项目统一启动脚本
一键启动: Flask服务 + 飞书销售机器人 + 飞书生产机器人
"""
import os
import sys
import time
import signal
import subprocess
import threading
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PYTHON = sys.executable
processes = [] # [(name, proc), ...]
lock = threading.Lock()
def log(msg):
print(f'[{time.strftime("%H:%M:%S")}] {msg}', flush=True)
def start_flask():
"""启动 Flask Web 服务"""
log('启动 Flask 服务...')
proc = subprocess.Popen(
[PYTHON, '-u', os.path.join(BASE_DIR, 'run_flask.py')],
cwd=BASE_DIR,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
universal_newlines=True,
)
processes.append(('Flask', proc))
log(f'Flask 已启动 (PID={proc.pid}),访问: http://127.0.0.1:5000')
def start_feishu_ws(script_name, description):
"""启动飞书长连接脚本"""
log(f'启动 {description}...')
proc = subprocess.Popen(
[PYTHON, '-u', os.path.join(BASE_DIR, script_name)],
cwd=BASE_DIR,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
universal_newlines=True,
)
processes.append((description, proc))
log(f'{description} 已启动 (PID={proc.pid})')
def shutdown(sig=None, frame=None):
"""优雅关闭所有子进程"""
log('正在停止所有服务...')
with lock:
for name, proc in processes[:]:
if proc.poll() is None:
log(f' 终止 {name} (PID={proc.pid})...')
proc.terminate()
for name, proc in processes[:]:
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
log('所有服务已停止')
sys.exit(0)
def main():
signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)
log('=' * 50)
log('SPMS 智造中枢 - 启动中...')
log('=' * 50)
start_flask()
time.sleep(2)
start_feishu_ws('feishu_ws.py', '飞书-销售机器人')
time.sleep(2)
start_feishu_ws('feishu_ws_production.py', '飞书-生产机器人')
log('=' * 50)
log('所有服务已启动!')
log(' Flask: http://127.0.0.1:5000')
log(' 飞书销售: 运行中')
log(' 飞书生产: 运行中')
log('提示: 按 Ctrl+C 停止所有服务')
log('=' * 50)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
shutdown()
if __name__ == '__main__':
main()
启动:
python start.py
停止:
按 Ctrl+C,所有子进程优雅退出。
十、实际运行效果
启动后,控制台输出:
[22:30:01] ==================================================
[22:30:01] SPMS 智造中枢 - 启动中...
[22:30:01] ==================================================
[22:30:01] 启动 Flask 服务...
[22:30:01] Flask 已启动 (PID=12345),访问: http://127.0.0.1:5000
[22:30:03] 启动 飞书-销售机器人...
[22:30:03] 飞书-销售机器人 已启动 (PID=12346)
[22:30:05] 启动 飞书-生产机器人...
[22:30:05] 飞书-生产机器人 已启动 (PID=12347)
[22:30:07] ==================================================
[22:30:07] 所有服务已启动!
[22:30:07] Flask: http://127.0.0.1:5000
[22:30:07] 飞书销售: 运行中
[22:30:07] 飞书生产: 运行中
[22:30:07] 提示: 按 Ctrl+C 停止所有服务
在飞书群聊中 @机器人,发送命令:
用户: @生产机器人 帮助
机器人: 🔧 SPMS 生产计划机器人
查询类:
• 工单 - 查询最近工单
• MOxxx - 查看工单详情
...
用户: @生产机器人 报工MO202506001 50件
机器人: 📊 报工成功
工单号: MO202506001
产品: 智能控制器A型
本次完成: 50件
累计完成: 50/500 (10.0%)
十一、踩坑记录
坑1:飞书长连接启动时阻塞
ws_client.start() 是阻塞调用。如果在 Flask 的 app.py 中直接启动,会卡住 Web 服务。
解决方案: 将飞书长连接放在独立进程中运行,用 start.py 统一管理。
坑2:card.action.trigger 不支持 WebSocket
想在飞书群里发交互卡片(带按钮),用户点击按钮触发回调——这个事件只能走 Webhook,WebSocket 长连接模式下收不到。
解决方案: 改用纯文本指令方案,所有操作通过消息文本完成。
坑3:@_user_N 污染消息内容
用户在群里 @机器人 时,飞书会在消息文本中插入 @_user_1 这样的内部标记,直接用 text 做命令匹配会失败。
解决方案: 用正则清理:
text = re.sub(r'@_user_\d+', '', text).strip()
坑4:数据库操作需要 Flask 上下文
飞书机器人是独立进程,不在 Flask 请求上下文中,直接查数据库会报错。
解决方案: 每个业务函数手动创建应用上下文:
from app import create_app
app = create_app()
with app.app_context():
# 数据库操作
坑5:控制台 emoji 显示乱码
Windows 控制台默认 GBK 编码,print 含 emoji 的字符串会报 UnicodeEncodeError。
解决方案: 重定向标准输出编码:
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
十二、总结
将 Web 项目接入飞书机器人,核心只需要四步:
1. 飞书后台创建应用,获取凭证
2. pip install lark-oapi
3. 写一个 WebSocket 长连接客户端(~100 行代码)
4. 在 handle_message 中做命令路由 → 调用业务函数
不需要公网 IP,不需要内网穿透,不需要 Nginx——这是 WebSocket 长连接模式最大的优势。对于内部使用的企业管理系统,这套方案开发快、部署简单、维护成本低。
完整项目代码结构:
spms/
├── app.py # Flask 应用工厂
├── config.py # 配置(含飞书凭证)
├── start.py # 一键启动脚本
├── run_flask.py # Flask 启动入口
├── feishu_ws.py # 飞书-销售机器人
├── feishu_ws_production.py # 飞书-生产机器人
├── models/ # 数据模型
│ ├── sales_order.py
│ ├── production.py
│ └── material.py
├── blueprints/ # Flask 路由
└── templates/ # Web 页面
本文基于真实项目「智造中枢:销售生产一体化管理系统(SPMS)」,项目使用 Flask + SQLAlchemy + SQLite + lark-oapi,包含销售订货、生产计划、物料管控、销售发货等完整业务模块,两个飞书机器人分别覆盖销售和生产场景。
技术栈:Python 3.13 / Flask / SQLAlchemy / lark-oapi / WebSocket
浙公网安备 33010602011771号