首页 / 资讯中心 / 文章详情

MCP实战手记(十一):MCP接飞书,从机器人到事件订阅的三层深度接入

MCP实战手记(十一):MCP接飞书,从机器人到事件订阅的三层深度接入 ★ FEATURED ARTICLE
本文聚焦飞书三层接入机器人Webhook/自建应用卡片/长连接事件订阅钉钉横向对比。0. 开篇为什么从飞书开始CSDN上飞书对接的实战文不少但飞书长连接事件订阅MCP双向闭环的完整跑通几乎空白。飞书开放平台对MCP最友好个人手机号就能建团队本文所有实测都不需要企业资质、不需要公网服务器一条WebSocket长连接把群里机器人→调Tool→回卡片消息打通按open_id做多租户数据隔离全程走飞书事件订阅。先看一段真实得不能再真实的对话[08:30] 天河城店店长MCP助手查一下我们店今天的销售 [08:30] MCP助手 门店助手 · 查询成功 **指令**销售 天河城店 今日销售12,480元客流431 [08:31] 天河城店店长库存呢SKU-1001还剩多少 [08:31] MCP助手 门店助手 · 查询成功 **指令**库存 SKU-1001库存23安全线50 [08:31] 天河城店店长补货建议 [08:31] MCP助手SKU-1001库存23 → 建议补货至安全线的1.2倍这不是演示而是零售/供应链企业里企业系统对接最常见的一类刚需把企业已有的业务系统销售、库存、设备、补货通过MCP Server开放出来再用企业IM飞书做入口让非技术角色店长、区域经理用一句话就能查询和决策。先消掉三个最常见门槛这是读者放弃前的最大拦路虎需要企业资质吗不需要。个人手机号注册即可建团队创建企业自建应用。要实名/要钱吗飞书个人免费长连接方案免内网穿透连ngrok都省了。多久能跑通第1层群机器人Webhook五分钟第3层长连接半天内。本文主线是个人手机号免公网即可完整跑通飞书深度接入。场景对齐零售/供应链店长飞书群机器人 查本门店销售/库存/设备/补货。1. 三层接入模型总览一张决策树三秒对号入座要告警推送────────────── 第1层Webhook5分钟 │否 要富卡片用户身份/隔离─── 第2层自建应用 │否 要群里机器人双向交互─── 第3层事件订阅 已有钉钉体系──────────── 钉钉webhook对比对应到零售场景的三种真实用法生产场景用哪层为什么门店设备离线/低库存告警推给店长群第1层Webhook单向推送足够最省事总部下发行情卡、区域经理按身份看自己区域第2层自建应用富卡片用户身份数据隔离店长在群里机器人查自己门店第3层事件订阅双向交互MCP工具当后台先澄清一个最容易混的点第1/2/3层共用同一个自建应用按发→收→双向交互逐层开启并非三套独立凭证。但第1层的群机器人和第2/3层的自建应用是两套不同凭证体系——群机器人无App ID/Secret、纯出站自建应用有完整凭证、可入站。本文第2/3层统一用自建应用别把两套凭证搞混。每层标题后的徽章[本机跑通]/[设计伪代码]一眼分清可照搬与仅思路2. 第1层飞书机器人Webhook5分钟跑通[本机跑通]生产场景门店空调、照明、风扇等设备离线或某SKU库存跌破安全线需要第一时间推给店长群。这是单向通知不需要店长回复所以第1层Webhook最合适。群机器人适合门店告警/设备离线推店长群。创建群机器人拿到Webhook地址加签后POST文本即可。飞书群机器人加签与钉钉同构timestamp\nsecret做HMAC-SHA256// 飞书群机器人加签复用篇8思路飞书版StringstringToSigntimestamp\nsecret;MacmacMac.getInstance(HmacSHA256);mac.init(newSecretKeySpec(secret.getBytes(UTF_8),HmacSHA256));StringsignBase64.getEncoder().encodeToString(mac.doFinal(stringToSign.getBytes(UTF_8)));StringurlwebhooktimestamptimestampsignURLEncoder.encode(sign,UTF-8);边界很硬原则第8条明边界纯出站、无用户身份、无交互、无鉴权上下文。它只是推不是接。能做的只有系统→群单向广播店长不能反问。如果业务需要群机器人查本门店必须上第3层。3. 第2层飞书自建应用卡片/权限/用户身份[本机跑通]生产场景区域经理下发行情卡店长按身份看自己门店的数据。这一层的关键不是发卡片而是自建应用才有入站能力用户身份。自建应用才有入站能力。先拿tenant_access_token用App ID/Secret换申请im:message权限把Tool查询结果渲染成飞书交互卡片推回去生产注意多实例tenant_access_token要跨实例共享同一份别各实例独立刷新——用 Redis 存 token TTL 续期并用 singleflight/分布式锁防并发刷新击穿多实例同时到期会齐刷飞书 token 接口被限流。{config:{wide_screen_mode:true},header:{title:{tag:plain_text,content:门店助手·查询成功},template:green},elements:[{tag:div,text:{tag:lark_md,content:**指令**查库存ST001\n\nSKU-1001库存23安全线50}},{tag:action,actions:[{tag:button,text:{tag:plain_text,content:补货},value:{sku:SKU-1001}}]}]}版本说明本文用飞书卡片1.0协议顶层configheaderelementstext.taglark_md兼容最广、实测跑通。新项目可升级2.0schema:2.0body.elementstext.tagmarkdown字段结构不同迁移时注意。用户身份与多租户数据隔离本文相对篇9的实质增量篇9解决怎么拿身份sender的open_id本篇解决身份映射到门店。闭环是事件sender.sender_id.open_id→通讯录contactAPI解析user_id/部门→绑定门店→工具层按门店过滤get_store_sales(storeCode本门店)。身份闭环流程事件.sender.sender_id.open_id │ (飞书平台鉴权身份可信) ▼ 通讯录contact APIopen_id → user_id → 部门 → 门店编码 │ (生产真实绑定本机demoopen_id→门店 映射表) ▼ 工具层按门店过滤get_store_sales(storeCode本门店)在真实工程里我把它做进了FeishuLongConnConfig的resolveStoreCodeprivateStringresolveStoreCode(StringopenId,Stringtext){MatchermSTORE.matcher(text);if(m.find())returnm.group().toUpperCase();// 用户直接说了门店编码StringboundOPENID_STORE.get(openId);// 否则走 open_id→门店绑定returnbound;// null → 工具层返回需要补充门店编码}这里必须讲清生产和demo的差别真实代码里OPENID_STORE目前是Map.of()空映射因为生产来源应是飞书通讯录contactAPIopen_id→user_id→部门/门店而个人免费版能否开通contact权限仍需在散点文03单独验证。所以本文本机实测用open_id→门店demo映射表跑通隔离机制生产要换成通讯录API的真实绑定——这是机制演示和生产级真实绑定的区别别混。数据只按门店维度切片但隔离策略本身已经落地即使店长乱敲storeCodeST002工具层也只认他绑定的门店。出站生产注意技术4.4飞书消息有频控失败重试语义生产要加退避重试别裸发// 退避重试飞书频控 瞬时失败指数退避 随机抖动避免多实例同时重试把飞书打得更限流intattempt0;while(attempt3){try{apiClient.im().message().send(req);break;}catch(RateLimitExceptione){longbase(1Lattempt)*500;// 0.5s / 1s / 2slongjitterThreadLocalRandom.current().nextLong(0,500);// 随机抖动错开重试Thread.sleep(basejitter);attempt;}}4. 第3层事件订阅双向机器人触发MCP调用★重点已实测[本机跑通]生产场景店长在飞书群里MCP助手查销售/库存/设备/补货机器人收到消息、调用MCP工具、把结果渲染成卡片回复。这是人→系统→人的双向闭环也是企业系统对接真正值钱的部分——把MCP Server变成群里的智能助理。选型上长连接(WebSocket)优于公网回调免公网入站端口、免内网穿透本机只需出站连wss://msg-frontier.feishu.cn官方强调服务器需能访问公网。长连接是飞书官方推荐路线把接入成本降到最低。兜底公网回调仅在长连接被质疑/受限时作退路清单P2原方案已验证可达ngrok不写成并列第二方案。收事件im.message.receive_v1单聊群鉴权App ID/Secret→access_token→建连。SDK坐标定案com.larksuite.oapi:oapi-sdk:2.3.4注意不是官方文档写的com.lark.oapi:lark-oapi-sdk长连接客户端类路径不同。闭环核心代码节选自com.ethanliang.mcp.feishu.FeishuLongConnConfig完整见仓库11-feishu// 主API客户端全限定名避免与ws.Client同名冲突踩坑1com.lark.oapi.ClientapiClientcom.lark.oapi.Client.newBuilder(appId,appSecret).build();EventDispatcherdispatcherEventDispatcher.newBuilder(appId,appSecret).onP2MessageReceiveV1(newImService.P2MessageReceiveV1Handler(){publicvoidhandle(P2MessageReceiveV1event){StringopenIdevent.getEvent().getSender().getSenderId().getOpenId();StringtextparseText(event.getEvent().getMessage().getContent());// 剥提及StringstoreCoderesolveStoreCode(openId,text);// 多租户open_id→门店StoreResultrdispatch(text,storeCode);// 路由到门店ToolStringcardbuildCard(text,r);// 渲染交互卡片apiClient.im().message().reply(ReplyMessageReq.newBuilder().messageId(event.getEvent().getMessage().getMessageId()).replyMessageReqBody(ReplyMessageReqBody.newBuilder().msgType(interactive).content(card).build()).build());}}).build();ClientwsClientnewClient.Builder(appId,appSecret).eventHandler(dispatcher).autoReconnect(true).build();newThread(wsClient::start).start();// 守护线程起长连接这段代码还原了生产里的完整链路收事件→剥→解析意图→多租户定门店→调MCP工具→渲染卡片回复。dispatch按关键词路由到门店工具privateStoreResultdispatch(Stringtext,StringstoreCode){Stringlowertext.toLowerCase();if(lower.contains(库存))returnstoreService.getInventory(storeCode);if(lower.contains(设备))returnstoreService.getDeviceStatus(storeCode);if(lower.contains(补货))returnstoreService.suggestRestock(storeCode);returnstoreService.getStoreSales(storeCode);// 默认销售}真实启动日志2026-10-07本机Started FeishuMcpApplication in 3.035 seconds (Spring Boot 4.0.0 / Java 21 / Spring AI 2.0.1) Registered Tool: get_store_sales / save_context / get_inventory / suggest_restock / load_context / get_device_status → Registered tools: 6 connected to wss://msg-frontier.feishu.cn/ws/v2?...conn_id7693857954043481288群内MCP篇11测试 查库存ST001→本机客户端实时打印事件、调工具、回卡片应用内机器人显示名即MCP篇11测试开篇对话为阅读顺畅称MCP助手。六工具零重写挂载篇8的StoreService业务域Tool引用P5通过。踩坑必看5条1补com.lark.oapi.ClientAPI与com.lark.oapi.ws.ClientWS同名Client两个single-type import会冲突API客户端须全限定名。发布版本须携im:message.receive_v10.0.1未携收不到事件发0.0.2即可达。补发布版本后事件生效约1分钟延迟飞书FAQ常误以为立即生效。飞书只有下拉框真正选中机器人才生成mention实体直接输入名称文本不触发。群机器人被误移出后需API重建验证群已用oc_a1c5...群完成。生产化小节本机/待生产两栏事件幂等event_id去重。飞书投递不保证不重复生产第一天就要做——区别于本自主题下的篇04的外部攻击重放那是另一回事攻击重放需签名校验时间窗平台重投是正常重复。多实例分发一实例一条WS、事件只进一个实例多实例需跨实例共享去重Redis分发/发布订阅。事件落到哪个实例各实例各开一条 WS 接住全量事件按chat_id一致性哈希把同一会话固定到同一实例会话状态亲和其余实例靠 Redis 全局去重防重复触发或简单轮询分发代价是InMemoryContextStore改为 Redis 共享。注意这与InMemoryContextStore上下文共享是两个不同问题别混——前者是事件去重后者是会话状态共享。// 跨实例幂等Redis SETNX 带TTLif(!redis.setnx(evt:eventId,1,Duration.ofMinutes(10)))return;// 已处理丢弃5. 横向对比钉钉钉钉拆两层说别笼统说钉钉双向受限群自定义机器人Outgoing截至2026-10实测显示功能维护中本次仅单向推送可达本篇独家差异。加签POST返回{errcode:0,errmsg:ok}验证链路通但收消息回调不可用。钉钉Stream模式官方推荐双向方案能力对等飞书长连接TCP持久连接、免公网但需企业内部应用组织开发者权限个人版门槛更高本篇未实测已标注。结论受限仅指群自定义机器人Outgoing线不是泛指钉钉平台。若已有钉钉体系且是企业应用Stream是等价路线。6. 选型决策表场景方案门店告警/设备离线第1层 群机器人Webhook富卡片用户身份隔离第2层 自建应用群交互查门店第3层 事件订阅已有钉钉体系钉钉webhookOutgoing维护中单向/ Stream企业应用关于统一接口封装三平台事件模型差异大强统一事件层是过度设计原第6节已砍思路并入本表——飞书/钉钉各用原生客户端对外只暴露WebhookSender最小抽象两实现各≤20行即可。7. 踩坑速查表第4节已逐条展开此处速查钉钉要点单列。#飞书踩坑现象/解法1Client同名冲突API客户端用com.lark.oapi.Client全限定名2事件权限未携发布版本须含im:message.receive_v1发0.0.23发布后延迟约1分钟才生效别当失败4不触发必须下拉框真选机器人文本无效5群机器人被移出API重建验证群钉钉Outgoing截至2026-10维护中仅单向可达8. 边界声明本机已验证长连接连飞书、收p2p/群事件、调6工具回卡、钉钉webhook推送环境Spring Boot 4.0.0/Java 21/Spring AI 2.0.1。待生产验证真实流量限流、access_token轮换与多实例共享、消息加解密与重放防护、长连接多实例分发与event_id幂等、门店数据隔离多租户压测。密钥声明调试用飞书App Secret/钉钉secret已重置本地含密钥文件已删除。能力→岗位对照求职资产化飞书三层接入Webhook/卡片/事件→ 飞书开放平台API系统集成开发 长连接事件订阅WS → 企业IM双向消息/机器人回调 用户身份门店数据隔离 → 权限管理、组织架构对接 门店场景销售/库存/设备 → 零售/供应链业务系统集成完整代码仓库11-feishu模块v11tagGitee/GitHub一键启动只需飞书App ID/Secret复制两个yml。
阅读完成 · 觉得有帮助?
咨询建站