当前位置: 首页 > news >正文

Hyperf方案 飞书机器人智能客服= 基于 Hyperf + 飞书机器人事件订阅,实现接收用户消息 → 调用 Claude/GPT → 回复的完整链路

整体流程 用户发消息 → 飞书事件推送 → 验签解密 → 读取上下文 → 调用 Claude → 流式回复 → 存上下文---1.配置 config/autoload/feishu.php 追加:'bot'=>['encrypt_key'=>env('FEISHU_ENCRYPT_KEY',''),'verification_token'=>env('FEISHU_VERIFY_TOKEN',''),],config/autoload/ai.phpreturn['anthropic'=>['api_key'=>env('ANTHROPIC_API_KEY',''),'model'=>'claude-opus-4-6','max_tokens'=>2048,],];---2.上下文管理 app/Service/Bot/ContextManager.php<?phpnamespaceApp\Service\Bot;use Psr\SimpleCache\CacheInterface;classContextManager{privateconstTTL=1800;// 30 分钟无消息清空上下文privateconstMAX_MSG=20;// 最多保留 20 条历史publicfunction__construct(privateCacheInterface $cache){}publicfunctionget(string $sessionId):array{return$this->cache->get($this->key($sessionId),[]);}publicfunctionappend(string $sessionId,string $role,string $content):void{$messages=$this->get($sessionId);$messages[]=['role'=>$role,'content'=>$content];// 超出上限时丢弃最早的一问一答(保留 system prompt 不动)if(count($messages)>self::MAX_MSG){array_splice($messages,0,2);}$this->cache->set($this->key($sessionId),$messages,self::TTL);}publicfunctionclear(string $sessionId):void{$this->cache->delete($this->key($sessionId));}privatefunctionkey(string $sessionId):string{return'feishu:bot:ctx:'.$sessionId;}}---3.Claude 调用服务(流式) app/Service/Bot/ClaudeService.php<?phpnamespaceApp\Service\Bot;use Hyperf\Guzzle\ClientFactory;use Hyperf\Contract\ConfigInterface;classClaudeService{privateconstSYSTEM_PROMPT=<<<PROMPT 你是一个专业的智能客服助手,回答简洁、准确、友好。 如果不确定答案,请如实告知,不要编造信息。 PROMPT;publicfunction__construct(privateClientFactory $clientFactory,privateConfigInterface $config){}/** * 流式调用 Claude,通过 callback 逐块返回文本 * @param callable $onChunk function(string $text): void */publicfunctionstreamChat(array $messages,callable $onChunk):string{$client=$this->clientFactory->create(['timeout'=>120,'connect_timeout'=>10,]);$response=$client->post('https://api.anthropic.com/v1/messages', ['headers'=>['x-api-key'=>$this->config->get('ai.anthropic.api_key'),'anthropic-version'=>'2023-06-01','content-type'=>'application/json',],'json'=>['model'=>$this->config->get('ai.anthropic.model'),'max_tokens'=>$this->config->get('ai.anthropic.max_tokens'),'system'=>self::SYSTEM_PROMPT,'messages'=>$messages,'stream'=>true,],'stream'=>true,]);$fullText='';$body=$response->getBody();while(!$body->eof()){$line=$this->readLine($body);if(!str_starts_with($line,'data: '))continue;$data=json_decode(substr($line,6),true);if(($data['type']??'')!=='content_block_delta')continue;$chunk=$data['delta']['text']??'';if($chunk==='')continue;$fullText.=$chunk;$onChunk($chunk);}return$fullText;}/** * 非流式,直接返回完整回复(备用) */publicfunctionchat(array $messages):string{$client=$this->clientFactory->create(['timeout'=>60]);$response=$client->post('https://api.anthropic.com/v1/messages', ['headers'=>['x-api-key'=>$this->config->get('ai.anthropic.api_key'),'anthropic-version'=>'2023-06-01','content-type'=>'application/json',],'json'=>['model'=>$this->config->get('ai.anthropic.model'),'max_tokens'=>$this->config->get('ai.anthropic.max_tokens'),'system'=>self::SYSTEM_PROMPT,'messages'=>$messages,],]);$data=json_decode($response->getBody()->getContents(),true);return$data['content'][0]['text']??'';}privatefunctionreadLine($body):string{$line='';while(!$body->eof()){$char=$body->read(1);if($char==="\n")break;$line.=$char;}returnrtrim($line,"\r");}}---4.消息处理服务 app/Service/Bot/MessageHandler.php<?phpnamespaceApp\Service\Bot;use App\Service\Feishu\MessageService;use Psr\SimpleCache\CacheInterface;classMessageHandler{publicfunction__construct(privateClaudeService $claude,privateContextManager $context,privateMessageService $messageService,privateCacheInterface $cache){}publicfunctionhandle(array $event):void{$msgId=$event['message']['message_id'];$chatId=$event['message']['chat_id'];$openId=$event['sender']['sender_id']['open_id'];$msgType=$event['message']['message_type'];// 幂等:同一条消息只处理一次(飞书可能重推)$dedupKey='feishu:bot:dedup:'.$msgId;if($this->cache->get($dedupKey))return;$this->cache->set($dedupKey,1,60);// 只处理文本消息if($msgType!=='text'){$this->messageService->sendText($chatId,'暂时只支持文字消息','chat_id');return;}$content=json_decode($event['message']['content'],true);$userMsg=trim($content['text']??'');// 清空上下文指令if(in_array($userMsg,['/clear','/重置','/新对话'],true)){$this->context->clear($openId);$this->messageService->sendText($chatId,'上下文已清空,开始新对话','chat_id');return;}// 追加用户消息到上下文$this->context->append($openId,'user',$userMsg);$messages=$this->context->get($openId);// 先发一条「正在思考」占位(可选)$this->messageService->sendText($chatId,'正在思考中...','chat_id');// 调用 Claude(非流式,飞书不支持直接流式更新消息)$reply=$this->claude->chat($messages);// 追加 AI 回复到上下文$this->context->append($openId,'assistant',$reply);// 回复用户(富文本支持 markdown)$this->replyMarkdown($chatId,$reply);}/** * 飞书不支持直接流式,用「分段发送」模拟流式体验 * 每积累一定字数发一次更新 */publicfunctionhandleStream(array $event):void{$chatId=$event['message']['chat_id'];$openId=$event['sender']['sender_id']['open_id'];$content=json_decode($event['message']['content'],true);$userMsg=trim($content['text']??'');$this->context->append($openId,'user',$userMsg);$messages=$this->context->get($openId);$buffer='';$messageId=null;$fullReply='';$this->claude->streamChat($messages,function(string $chunk)use(&$buffer,&$messageId,&$fullReply,$chatId){$buffer.=$chunk;$fullReply.=$chunk;// 每 50 字或遇到句号/换行就推送一次if(mb_strlen($buffer)>=50||preg_match('/[。!?\n]/',$buffer)){if($messageId===null){// 第一次:发新消息$messageId=$this->messageService->sendText($chatId,$buffer,'chat_id');}else{// 后续:更新同一条消息(追加内容)// 注意:飞书更新消息是全量替换,需拼接完整内容// 此处简化为继续发新消息分段展示$this->messageService->sendText($chatId,$buffer,'chat_id');}$buffer='';}});// 发送剩余内容if($buffer!==''){$this->messageService->sendText($chatId,$buffer,'chat_id');}$this->context->append($openId,'assistant',$fullReply);}privatefunctionreplyMarkdown(string $chatId,string $text):void{// 长文本自动分段(飞书单条消息上限 30000 字)$chunks=mb_str_split($text,2000);foreach($chunks as $chunk){$this->messageService->sendText($chatId,$chunk,'chat_id');}}}---5.事件接收 Controller app/Controller/Feishu/BotController.php<?phpnamespaceApp\Controller\Feishu;use App\Service\Bot\MessageHandler;use App\Service\Feishu\EventVerifier;use App\Job\BotMessageJob;use Hyperf\AsyncQueue\Driver\DriverFactory;use Hyperf\HttpServer\Annotation\Controller;use Hyperf\HttpServer\Annotation\PostMapping;use Hyperf\HttpServer\Contract\RequestInterface;#[Controller(prefix:'/feishu/bot')]classBotController{publicfunction__construct(privateEventVerifier $verifier,privateDriverFactory $driverFactory){}#[PostMapping(path:'/event')]publicfunctionevent(RequestInterface $request):array{$body=$request->getBody()->getContents();$data=$this->verifier->verify($body);// URL 验证握手$challenge=$this->verifier->handleChallenge($data);if($challenge!==null){return['challenge'=>$challenge];}$eventType=$data['header']['event_type']??'';if($eventType==='im.message.receive_v1'){// 投入异步队列,3 秒内先返回 200$this->driverFactory->get('default')->push(newBotMessageJob($data['event']));}return['code'=>0];}}---6.异步队列 Job app/Job/BotMessageJob.php<?phpnamespaceApp\Job;use App\Service\Bot\MessageHandler;use Hyperf\AsyncQueue\Job;classBotMessageJobextends Job{publicint$maxAttempts=1;// AI 调用不重试,避免重复回复publicfunction__construct(privatearray $event){}publicfunctionhandle():void{$handler=make(MessageHandler::class);$handler->handle($this->event);}}---7.飞书后台配置 开放平台 → 事件订阅: ✓ 接收消息 im.message.receive_v1 权限申请: ✓ im:message 读取消息 ✓ im:message:send_as_bot 发送消息 回调地址:https://your-domain.com/feishu/bot/event机器人设置 → 勾选「允许机器人接收消息」---关键点汇总 ┌───────────────┬──────────────────────────────────────────────────────┐ │ 要点 │ 说明 │ ├───────────────┼──────────────────────────────────────────────────────┤ │ 幂等去重 │ 飞书同一事件可能重推,用 message_id+Redis60s 去重 │ ├───────────────┼──────────────────────────────────────────────────────┤ │3秒响应 │ Controller 立即返回,AI 调用全部走异步队列 │ ├───────────────┼──────────────────────────────────────────────────────┤ │ 上下文 key │ 用 open_id 区分用户,群聊可改用 open_id+chat_id │ ├───────────────┼──────────────────────────────────────────────────────┤ │ 流式限制 │ 飞书消息不支持 SSE,流式只能分段发多条或轮询更新 │ ├───────────────┼──────────────────────────────────────────────────────┤ │ maxAttempts=1│ AI Job 禁止重试,否则失败后重推会重复回复用户 │ ├───────────────┼──────────────────────────────────────────────────────┤ │ 上下文裁剪 │ 超出20条丢弃最早一问一答,避免 token 超限 │ ├───────────────┼──────────────────────────────────────────────────────┤ │ 群聊 @ 过滤 │ 群里需判断 mentions 是否包含机器人自身,私聊不需要 │ └───────────────┴──────────────────────────────────────────────────────┘
http://www.jsqmd.com/news/622891/

相关文章:

  • SAP BAPI批量处理MD61/MD62计划独立需求的实战解析
  • 终极Windows与Office激活指南:5分钟完成智能激活的完整解决方案
  • 共话2026年AI搜索优化,津胜网络助力企业提升排名哪家强 - 工业设备
  • 2026.4.09总结
  • Spring AI实战:基于DeepSeek与Milvus构建企业级智能问答系统的RAG架构解析
  • 如何快速掌握虚拟机检测工具VMDE:面向初学者的完整指南
  • LaTeX中二重闭合积分的完美呈现:esint宏包实战指南
  • 再也不担心论文!Nano-Banana Pro 论文绘图最全教程出书了!
  • 如何用LRC Maker零基础制作专业歌词文件:从音频到精准时间轴的完整指南
  • 避坑指南:为什么你的Pyside6 QMediaPlayer播不了视频?我总结了3种替代方案
  • 如何用GraphvizOnline在5分钟内创建专业流程图:终极免费可视化工具指南
  • 终极指南:如何用EldenRingFpsUnlockAndMore解锁《艾尔登法环》帧率限制和优化游戏体验
  • 一个公司 OPC 必备的 10 个 Skill
  • 2026年AI搜索优化品牌企业推荐,津胜网络实力出众 - mypinpai
  • 通义千问3-VL-Reranker-8B实战:快速搭建多模态检索排序服务
  • Qwen3.5-9B-AWQ-4bit辅助MATLAB科学计算:算法解释与代码转换
  • 讲讲西北靠谱的阳光板供应厂家,选购要点有哪些 - 工业品网
  • 浅谈:给导航栏添加悬浮背景变化/固定顶部效果
  • 5分钟掌握SD-PPP:让Photoshop变身AI图像生成工作站的终极指南
  • Spring Boot 4.0 Agent就绪架构到底多快?实测对比Spring Boot 3.3:冷启动缩短68%,GC暂停下降92%
  • 组合专机-基于PROE平台的立式双轴缸孔半精镗机床总体及刀具设计
  • Agent Skills:AI 正在学会自己进化,这意味着什么?
  • ClawdBot惊艳效果:模糊车牌图片→OCR识别→中英双语翻译+校验
  • 别再只会用imfilter了!用MATLAB玩转频域滤波:从理想、高斯到巴特沃斯,一次搞定图像平滑与锐化
  • 盒马鲜生购物卡变现秘诀 - 团团收购物卡回收
  • Dify-AI应用嵌入第三方项目实战:从零到一的无缝集成指南
  • 3分钟搞定!Windows 11任务栏拖放功能一键修复指南 [特殊字符]
  • 【EF Core 10向量搜索扩展权威白皮书】:基于176处源码注释+8类数据库适配器对比的工业级实践框架
  • LangGraph 从入门到精通:3个核心概念构建智能体工作流
  • AT_abc329_c [ABC329C] Count xxx