整体流程
                                                                                                                          用户发消息 → 飞书事件推送 → 验签解密 → 读取上下文 → 调用 Claude → 流式回复 → 存上下文
                                                                                                                          ---
  1. 配置

  config/autoload/feishu.php 追加:

  'bot' => [
      'encrypt_key'        => env('FEISHU_ENCRYPT_KEY', ''),
      'verification_token' => env('FEISHU_VERIFY_TOKEN', ''),
  ],

  config/autoload/ai.php

  return [
      'anthropic' => [
          'api_key' => env('ANTHROPIC_API_KEY', ''),
          'model'   => 'claude-opus-4-6',
          'max_tokens' => 2048,
      ],
  ];

  ---
  2. 上下文管理

  app/Service/Bot/ContextManager.php

  <?php

  namespace App\Service\Bot;

  use Psr\SimpleCache\CacheInterface;

  class ContextManager
  {
      private const TTL     = 1800;   // 30 分钟无消息清空上下文
      private const MAX_MSG = 20;     // 最多保留 20 条历史

      public function __construct(private CacheInterface $cache) {}

      public function get(string $sessionId): array
      {
          return $this->cache->get($this->key($sessionId), []);
      }

      public function append(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);
      }

      public function clear(string $sessionId): void
      {
          $this->cache->delete($this->key($sessionId));
      }

      private function key(string $sessionId): string
      {
          return 'feishu:bot:ctx:' . $sessionId;
      }
  }

  ---
  3. Claude 调用服务(流式)

  app/Service/Bot/ClaudeService.php

  <?php

  namespace App\Service\Bot;

  use Hyperf\Guzzle\ClientFactory;
  use Hyperf\Contract\ConfigInterface;

  class ClaudeService
  {
      private const SYSTEM_PROMPT = <<<PROMPT
  你是一个专业的智能客服助手,回答简洁、准确、友好。
  如果不确定答案,请如实告知,不要编造信息。
  PROMPT;

      public function __construct(
          private ClientFactory $clientFactory,
          private ConfigInterface $config
      ) {}

      /**
       * 流式调用 Claude,通过 callback 逐块返回文本
       * @param callable $onChunk function(string $text): void
       */
      public function streamChat(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;
      }

      /**
       * 非流式,直接返回完整回复(备用)
       */
      public function chat(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'] ?? '';
      }

      private function readLine($body): string
      {
          $line = '';
          while (!$body->eof()) {
              $char = $body->read(1);
              if ($char === "\n") break;
              $line .= $char;
          }
          return rtrim($line, "\r");
      }
  }

  ---
  4. 消息处理服务

  app/Service/Bot/MessageHandler.php

  <?php

  namespace App\Service\Bot;

  use App\Service\Feishu\MessageService;
  use Psr\SimpleCache\CacheInterface;

  class MessageHandler
  {
      public function __construct(
          private ClaudeService   $claude,
          private ContextManager  $context,
          private MessageService  $messageService,
          private CacheInterface  $cache
      ) {}

      public function handle(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);
      }

      /**
       * 飞书不支持直接流式,用「分段发送」模拟流式体验
       * 每积累一定字数发一次更新
       */
      public function handleStream(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);
      }

      private function replyMarkdown(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

  <?php

  namespace App\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')]
  class BotController
  {
      public function __construct(
          private EventVerifier  $verifier,
          private DriverFactory  $driverFactory
      ) {}

      #[PostMapping(path: '/event')]
      public function event(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(
                  new BotMessageJob($data['event'])
              );
          }

          return ['code' => 0];
      }
  }

  ---
  6. 异步队列 Job

  app/Job/BotMessageJob.php

  <?php

  namespace App\Job;

  use App\Service\Bot\MessageHandler;
  use Hyperf\AsyncQueue\Job;

  class BotMessageJob extends Job
  {
      public int $maxAttempts = 1;   // AI 调用不重试,避免重复回复

      public function __construct(private array $event) {}

      public function handle(): 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 + Redis 60s 去重 │
  ├───────────────┼──────────────────────────────────────────────────────┤
  │ 3 秒响应      │ Controller 立即返回,AI 调用全部走异步队列           │
  ├───────────────┼──────────────────────────────────────────────────────┤
  │ 上下文 key    │ 用 open_id 区分用户,群聊可改用 open_id + chat_id    │
  ├───────────────┼──────────────────────────────────────────────────────┤
  │ 流式限制      │ 飞书消息不支持 SSE,流式只能分段发多条或轮询更新     │
  ├───────────────┼──────────────────────────────────────────────────────┤
  │ maxAttempts=1 │ AI Job 禁止重试,否则失败后重推会重复回复用户        │
  ├───────────────┼──────────────────────────────────────────────────────┤
  │ 上下文裁剪    │ 超出 20 条丢弃最早一问一答,避免 token 超限          │
  ├───────────────┼──────────────────────────────────────────────────────┤
  │ 群聊 @ 过滤   │ 群里需判断 mentions 是否包含机器人自身,私聊不需要   │
  └───────────────┴──────────────────────────────────────────────────────┘
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐