From ed8198223986bc2428832da6b750cbd283b001bc Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Wed, 10 Dec 2025 10:19:36 +0800 Subject: [PATCH] =?UTF-8?q?chore(addon/aikefu):=20=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=8E=A5=E5=8F=A3=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/addon/aikefu/api/controller/Kefu.php | 588 ++++++++++---------- src/addon/aikefu/docs/stream_chat_demo.html | 437 +++++++++++++++ src/addon/aikefu/event/KefuChat.php | 7 +- src/app/api/controller/Kefu.php | 110 ++-- 4 files changed, 828 insertions(+), 314 deletions(-) create mode 100644 src/addon/aikefu/docs/stream_chat_demo.html diff --git a/src/addon/aikefu/api/controller/Kefu.php b/src/addon/aikefu/api/controller/Kefu.php index 3cc2bf081..f8c2f1593 100644 --- a/src/addon/aikefu/api/controller/Kefu.php +++ b/src/addon/aikefu/api/controller/Kefu.php @@ -22,72 +22,87 @@ class Kefu extends BaseApi */ private function curlRequestStreaming($url, $method = 'GET', $data = [], $headers = [], $on_data = null, $on_error = null) { - $ch = curl_init(); + try { + $ch = curl_init(); - // 设置URL - curl_setopt($ch, CURLOPT_URL, $url); + // 设置URL + curl_setopt($ch, CURLOPT_URL, $url); - // 设置请求方法 - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); + // 设置请求方法 + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); - // 设置POST数据 - if ($method === 'POST' && !empty($data)) { - curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? json_encode($data) : $data); - } - - // 设置请求头 - if (!empty($headers)) { - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - } else { - // 默认请求头 - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - ]); - } - - // 设置cURL选项以支持流式输出 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // 不返回结果,直接输出 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_TIMEOUT, 0); // 无超时限制,适用于长时间流式响应 - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); - curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($curl, $data) use ($on_data) { - // 调用自定义数据处理回调 - if (is_callable($on_data)) { - $on_data($data); - } else { - // 默认直接输出 - echo $data; - ob_flush(); - flush(); + // 设置POST数据 + if ($method === 'POST' && !empty($data)) { + curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? json_encode($data) : $data); } - return strlen($data); - }); - - // 设置SSE响应头 - header('Content-Type: text/event-stream'); - header('Cache-Control: no-cache'); - header('Connection: keep-alive'); - ob_end_clean(); // 清除输出缓冲区 - ob_implicit_flush(true); // 自动刷新输出 - - // 执行请求并流式输出响应 - curl_exec($ch); - - if (curl_errno($ch)) { - $error = curl_error($ch); - if (is_callable($on_error)) { - $on_error($error); + // 设置请求头 + if (!empty($headers)) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } else { - echo "data: " . json_encode(["event" => "error", "data" => $error]) . "\n\n"; + // 默认请求头 + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + ]); } + + // 设置cURL选项以支持流式输出 + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // 不返回结果,直接输出 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 0); // 无超时限制,适用于长时间流式响应 + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($curl, $data) use ($on_data) { + // 调用自定义数据处理回调 + if (is_callable($on_data)) { + $on_data($data); + } else { + // 默认直接输出 + echo $data; + // 只在有输出缓冲时才刷新 + if (ob_get_level() > 0) { + ob_flush(); + } + flush(); + } + + return strlen($data); + }); + + // 设置SSE响应头 + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('Connection: keep-alive'); + header('X-Accel-Buffering: no'); // 禁用Nginx缓冲 + + ob_end_clean(); // 清除输出缓冲区 + ob_implicit_flush(true); // 自动刷新输出 + + // 执行请求并流式输出响应 + curl_exec($ch); + + if (curl_errno($ch)) { + $error = curl_error($ch); + if (is_callable($on_error)) { + $on_error($error); + } else { + echo "data: " . json_encode(["event" => "error", "data" => $error]) . "\n\n"; + } + curl_close($ch); + return false; + } + curl_close($ch); + return true; + } catch (\Exception $e) { + $this->log(json_encode(["event" => "error", "data" => $e->getMessage(), "line" => $e->getLine(), "file" => $e->getFile()]), 'error'); + if (is_callable($on_error)) { + $on_error($e->getMessage()); + } else { + echo "data: " . json_encode(["event" => "error", "data" => $e->getMessage()]) . "\n\n"; + } return false; } - - curl_close($ch); - return true; } /** @@ -343,14 +358,14 @@ class Kefu extends BaseApi */ private function log($message, $level = 'info') { - // Log::write($message, $level); + log_write($message, $level, '', 2); } private function handleStreamingResponse($url, $requestData, $headers, $message, $user_id) { // 记录开始处理流式请求 $this->log('AI客服流式请求开始处理,用户ID:' . $user_id . ',请求消息:' . $message, 'info'); - + // 初始化模型 $kefu_conversation_model = new KefuConversationModel(); $kefu_message_model = new KefuMessageModel(); @@ -365,234 +380,243 @@ class Kefu extends BaseApi $user_message_saved = false; $user_message_content = $message; - // 数据处理回调函数 - $on_data = function ($data) use (&$real_conversation_id, &$real_assistant_message_id, &$real_user_message_id, &$assistant_content, &$user_message_saved, $user_message_content, $kefu_message_model, $kefu_conversation_model, $site_id, $current_user_id) { - // 记录接收到的数据块 - $this->log('接收到AI客服数据块:' . $data, 'debug'); - - // 输出原始数据 - echo $data; - ob_flush(); - flush(); - - // 解析Dify的流式响应 - $lines = explode("\n", $data); - foreach ($lines as $line) { - $line = trim($line); - if (empty($line)) - continue; - - // 查找以"data: "开头的行 - if (strpos($line, 'data: ') === 0) { - $json_data = substr($line, 6); - $event_data = json_decode($json_data, true); - - if (json_last_error() === JSON_ERROR_NONE && isset($event_data['event'])) { - $event = $event_data['event']; - $this->log('处理AI客服事件:' . $event, 'info'); - - switch ($event_data['event']) { - case 'message': - // LLM返回文本块事件 - if (isset($event_data['data']['conversation_id'])) { - $real_conversation_id = $event_data['data']['conversation_id']; - $this->log('获取到会话ID:' . $real_conversation_id, 'info'); - } - if (isset($event_data['data']['message_id'])) { - $real_assistant_message_id = $event_data['data']['message_id']; - $this->log('获取到助手消息ID:' . $real_assistant_message_id, 'info'); - } - // 积累助手回复内容 - if (isset($event_data['data']['answer'])) { - $assistant_content .= $event_data['data']['answer']; - $this->log('积累助手回复内容:' . $event_data['data']['answer'], 'debug'); - } - // 保存用户消息(仅在第一次获取到会话ID时保存) - if (!$user_message_saved && !empty($real_conversation_id)) { - // 使用Dify返回的真实会话ID保存用户消息 - $this->saveUserMessage($kefu_message_model, $site_id, $current_user_id, $real_conversation_id, '', $user_message_content); - $this->updateOrCreateConversation($kefu_conversation_model, $site_id, $current_user_id, $real_conversation_id); - $user_message_saved = true; - $this->log('用户消息已保存,会话ID:' . $real_conversation_id, 'info'); - } - break; - - case 'agent_message': - // Agent模式下返回文本块事件 - if (isset($event_data['data']['conversation_id'])) { - $real_conversation_id = $event_data['data']['conversation_id']; - $this->log('获取到Agent模式会话ID:' . $real_conversation_id, 'info'); - } - if (isset($event_data['data']['message_id'])) { - $real_assistant_message_id = $event_data['data']['message_id']; - $this->log('获取到Agent模式助手消息ID:' . $real_assistant_message_id, 'info'); - } - // 积累助手回复内容 - if (isset($event_data['data']['answer'])) { - $assistant_content .= $event_data['data']['answer']; - $this->log('积累Agent回复内容:' . $event_data['data']['answer'], 'debug'); - } - // 保存用户消息(仅在第一次获取到会话ID时保存) - if (!$user_message_saved && !empty($real_conversation_id)) { - $this->saveUserMessage($kefu_message_model, $site_id, $current_user_id, $real_conversation_id, '', $user_message_content); - $this->updateOrCreateConversation($kefu_conversation_model, $site_id, $current_user_id, $real_conversation_id); - $user_message_saved = true; - $this->log('Agent模式下用户消息已保存,会话ID:' . $real_conversation_id, 'info'); - } - break; - - case 'agent_thought': - if (isset($event_data['data']['thought'])) { - // 格式化思考过程 - $thought_content = "\n[思考过程]: " . $event_data['data']['thought']; - if (isset($event_data['data']['tool'])) { - $thought_content .= "\n[使用工具]: " . $event_data['data']['tool']; - } - if (isset($event_data['data']['observation'])) { - $thought_content .= "\n[观察结果]: " . $event_data['data']['observation']; - } - $assistant_content .= $thought_content; - $this->log('Agent思考过程:' . $thought_content, 'debug'); - } - break; - - case 'file': - if (isset($event_data['data']['id']) && isset($event_data['data']['type']) && isset($event_data['data']['url'])) { - $file_id = $event_data['data']['id']; - $file_type = $event_data['data']['type']; - $file_url = $event_data['data']['url']; - // 可以将文件信息作为特殊内容添加到回复中 - $file_content = "\n[文件]: " . $file_type . " - " . $file_url; - $assistant_content .= $file_content; - $this->log('收到文件事件:' . $file_type . ' - ' . $file_url, 'info'); - } - break; - - case 'message_start': - if (isset($event_data['data']['conversation_id'])) { - $real_conversation_id = $event_data['data']['conversation_id']; - $this->log('消息开始事件,会话ID:' . $real_conversation_id, 'info'); - } - break; - - case 'message_delta': - if (isset($event_data['data']['delta']['content'])) { - $assistant_content .= $event_data['data']['delta']['content']; - $this->log('积累增量内容:' . $event_data['data']['delta']['content'], 'debug'); - } - break; - - case 'message_end': - // 最终内容已通过message或message_delta事件积累 - $this->log('消息结束事件,会话ID:' . $real_conversation_id . ',消息ID:' . $real_assistant_message_id, 'info'); - break; - - case 'tts_message': - // TTS音频流事件 - // 这里可以记录音频流信息,但由于是文本消息处理,暂时不做特殊处理 - $this->log('收到TTS消息事件', 'debug'); - break; - - case 'tts_message_end': - // TTS音频流结束事件 - // 同样不做特殊处理 - $this->log('收到TTS消息结束事件', 'debug'); - break; - - case 'message_replace': - if (isset($event_data['data']['answer'])) { - // 直接替换所有回复文本 - $assistant_content = $event_data['data']['answer']; - $this->log('消息内容替换为:' . $event_data['data']['answer'], 'debug'); - } - break; - - case 'error': - $error_message = isset($event_data['data']['message']) ? $event_data['data']['message'] : '流式输出异常'; - $assistant_content .= "\n[错误]: " . $error_message; - $this->log('AI客服错误事件:' . $error_message, 'error'); - break; - - case 'ping': - // 保持连接存活的ping事件 - // 无需特殊处理,继续保持连接 - $this->log('收到ping事件', 'debug'); - break; - - case 'tool_call_start': - // 工具调用开始事件 - $this->log('工具调用开始事件', 'debug'); - break; - - case 'tool_call_delta': - // 工具调用增量事件 - $this->log('工具调用增量事件', 'debug'); - break; - - case 'tool_call_end': - // 工具调用结束事件 - $this->log('工具调用结束事件', 'debug'); - break; - - case 'done': - // 完成事件 - $this->log('完成事件', 'debug'); - break; - - case 'workflow_start': - // 工作流开始事件 - $this->log('工作流开始事件', 'debug'); - break; - - case 'workflow_node_start': - if (isset($event_data['data']['node_id']) && isset($event_data['data']['node_name'])) { - $this->log('工作流节点开始:' . $event_data['data']['node_name'], 'debug'); - } - break; - - case 'workflow_node_end': - if (isset($event_data['data']['node_id']) && isset($event_data['data']['outputs'])) { - $this->log('工作流节点结束:' . $event_data['data']['node_id'], 'debug'); - } - break; - - case 'workflow_end': - // 工作流结束事件 - $this->log('工作流结束事件', 'debug'); - break; - - default: - // 处理未知事件类型 - $this->log('未知事件类型:' . $event_data['event'], 'warning'); - break; - } - } else { - $this->log('AI客服事件解析失败:' . $json_data, 'error'); - } - } - } - }; - - // 错误处理回调函数 - $on_error = function ($error) use ($user_id) { - $this->log('AI客服请求错误,用户ID:' . $user_id . ',错误信息:' . json_encode($error), 'error'); - }; - try { + // 数据处理回调函数 + $on_data = function ($data) use (&$real_conversation_id, &$real_assistant_message_id, &$real_user_message_id, &$assistant_content, &$user_message_saved, $user_message_content, $kefu_message_model, $kefu_conversation_model, $site_id, $current_user_id) { + + try { + // 记录接收到的数据块 + $this->log('接收到AI客服数据块:' . $data, 'debug'); + + // 输出原始数据(为了兼容旧版本或调试) + echo $data; + // 只在有输出缓冲时才刷新 + if (ob_get_level() > 0) { + ob_flush(); + } + flush(); + + // 解析Dify的流式响应 + $lines = explode("\n", $data); + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) + continue; + + // 查找以"data: "开头的行 + if (strpos($line, 'data: ') === 0) { + $json_data = substr($line, 6); + $event_data = json_decode($json_data, true); + + if (json_last_error() === JSON_ERROR_NONE && isset($event_data['event'])) { + $event = $event_data['event']; + $this->log('处理AI客服事件:' . $event, 'info'); + + switch ($event_data['event']) { + case 'message': + // LLM返回文本块事件 + if (isset($event_data['data']['conversation_id'])) { + $real_conversation_id = $event_data['data']['conversation_id']; + $this->log('获取到会话ID:' . $real_conversation_id, 'info'); + } + if (isset($event_data['data']['message_id'])) { + $real_assistant_message_id = $event_data['data']['message_id']; + $this->log('获取到助手消息ID:' . $real_assistant_message_id, 'info'); + } + // 积累助手回复内容 + if (isset($event_data['data']['answer'])) { + $assistant_content .= $event_data['data']['answer']; + $this->log('积累助手回复内容:' . $event_data['data']['answer'], 'debug'); + } + // 保存用户消息(仅在第一次获取到会话ID时保存) + if (!$user_message_saved && !empty($real_conversation_id)) { + // 使用Dify返回的真实会话ID保存用户消息 + $this->saveUserMessage($kefu_message_model, $site_id, $current_user_id, $real_conversation_id, '', $user_message_content); + $this->updateOrCreateConversation($kefu_conversation_model, $site_id, $current_user_id, $real_conversation_id); + $user_message_saved = true; + $this->log('用户消息已保存,会话ID:' . $real_conversation_id, 'info'); + } + break; + + case 'agent_message': + // Agent模式下返回文本块事件 + if (isset($event_data['data']['conversation_id'])) { + $real_conversation_id = $event_data['data']['conversation_id']; + $this->log('获取到Agent模式会话ID:' . $real_conversation_id, 'info'); + } + if (isset($event_data['data']['message_id'])) { + $real_assistant_message_id = $event_data['data']['message_id']; + $this->log('获取到Agent模式助手消息ID:' . $real_assistant_message_id, 'info'); + } + // 积累助手回复内容 + if (isset($event_data['data']['answer'])) { + $assistant_content .= $event_data['data']['answer']; + $this->log('积累Agent回复内容:' . $event_data['data']['answer'], 'debug'); + } + // 保存用户消息(仅在第一次获取到会话ID时保存) + if (!$user_message_saved && !empty($real_conversation_id)) { + $this->saveUserMessage($kefu_message_model, $site_id, $current_user_id, $real_conversation_id, '', $user_message_content); + $this->updateOrCreateConversation($kefu_conversation_model, $site_id, $current_user_id, $real_conversation_id); + $user_message_saved = true; + $this->log('Agent模式下用户消息已保存,会话ID:' . $real_conversation_id, 'info'); + } + break; + + case 'agent_thought': + if (isset($event_data['data']['thought'])) { + // 格式化思考过程 + $thought_content = "\n[思考过程]: " . $event_data['data']['thought']; + if (isset($event_data['data']['tool'])) { + $thought_content .= "\n[使用工具]: " . $event_data['data']['tool']; + } + if (isset($event_data['data']['observation'])) { + $thought_content .= "\n[观察结果]: " . $event_data['data']['observation']; + } + $assistant_content .= $thought_content; + $this->log('Agent思考过程:' . $thought_content, 'debug'); + } + break; + + case 'file': + if (isset($event_data['data']['id']) && isset($event_data['data']['type']) && isset($event_data['data']['url'])) { + $file_id = $event_data['data']['id']; + $file_type = $event_data['data']['type']; + $file_url = $event_data['data']['url']; + // 可以将文件信息作为特殊内容添加到回复中 + $file_content = "\n[文件]: " . $file_type . " - " . $file_url; + $assistant_content .= $file_content; + $this->log('收到文件事件:' . $file_type . ' - ' . $file_url, 'info'); + } + break; + + case 'message_start': + if (isset($event_data['data']['conversation_id'])) { + $real_conversation_id = $event_data['data']['conversation_id']; + $this->log('消息开始事件,会话ID:' . $real_conversation_id, 'info'); + } + break; + + case 'message_delta': + if (isset($event_data['data']['delta']['content'])) { + $assistant_content .= $event_data['data']['delta']['content']; + $this->log('积累增量内容:' . $event_data['data']['delta']['content'], 'debug'); + } + break; + + case 'message_end': + // 最终内容已通过message或message_delta事件积累 + $this->log('消息结束事件,会话ID:' . $real_conversation_id . ',消息ID:' . $real_assistant_message_id, 'info'); + break; + + case 'tts_message': + // TTS音频流事件 + // 这里可以记录音频流信息,但由于是文本消息处理,暂时不做特殊处理 + $this->log('收到TTS消息事件', 'debug'); + break; + + case 'tts_message_end': + // TTS音频流结束事件 + // 同样不做特殊处理 + $this->log('收到TTS消息结束事件', 'debug'); + break; + + case 'message_replace': + if (isset($event_data['data']['answer'])) { + // 直接替换所有回复文本 + $assistant_content = $event_data['data']['answer']; + $this->log('消息内容替换为:' . $event_data['data']['answer'], 'debug'); + } + break; + + case 'error': + $error_message = isset($event_data['data']['message']) ? $event_data['data']['message'] : '流式输出异常'; + $assistant_content .= "\n[错误]: " . $error_message; + $this->log('AI客服错误事件:' . $error_message, 'error'); + break; + + case 'ping': + // 保持连接存活的ping事件 + // 无需特殊处理,继续保持连接 + $this->log('收到ping事件', 'debug'); + break; + + case 'tool_call_start': + // 工具调用开始事件 + $this->log('工具调用开始事件', 'debug'); + break; + + case 'tool_call_delta': + // 工具调用增量事件 + $this->log('工具调用增量事件', 'debug'); + break; + + case 'tool_call_end': + // 工具调用结束事件 + $this->log('工具调用结束事件', 'debug'); + break; + + case 'done': + // 完成事件 + $this->log('完成事件', 'debug'); + break; + + case 'workflow_start': + // 工作流开始事件 + $this->log('工作流开始事件', 'debug'); + break; + + case 'workflow_node_start': + if (isset($event_data['data']['node_id']) && isset($event_data['data']['node_name'])) { + $this->log('工作流节点开始:' . $event_data['data']['node_name'], 'debug'); + } + break; + + case 'workflow_node_end': + if (isset($event_data['data']['node_id']) && isset($event_data['data']['outputs'])) { + $this->log('工作流节点结束:' . $event_data['data']['node_id'], 'debug'); + } + break; + + case 'workflow_end': + // 工作流结束事件 + $this->log('工作流结束事件', 'debug'); + break; + + default: + // 处理未知事件类型 + $this->log('未知事件类型:' . $event_data['event'], 'warning'); + break; + } + } else { + $this->log('AI客服事件解析失败:' . $json_data, 'error'); + } + } + } + } catch (Exception $e) { + $this->log('AI客服事件处理异常:' . $e->getMessage(), 'error'); + } + }; + + // 错误处理回调函数 + $on_error = function ($error) use ($user_id) { + $this->log('AI客服请求错误,用户ID:' . $user_id . ',错误信息:' . json_encode($error), 'error'); + }; + // 调用curl流式请求 - $this->curlRequestStreaming($url, $requestData, $headers, $on_data, $on_error); + $this->curlRequestStreaming($url, 'POST', $requestData, $headers, $on_data, $on_error); + $this->log('AI客服请求成功,用户ID:' . $user_id . ',会话ID:' . $real_conversation_id, 'info'); // 数据流结束时发送明确的"done"事件 - $done_event = [ - 'event' => 'done', - 'data' => [ - 'conversation_id' => $real_conversation_id, - 'message_id' => $real_assistant_message_id, - 'content' => $assistant_content - ] + $done_data = [ + 'conversation_id' => $real_conversation_id, + 'message_id' => $real_assistant_message_id, + 'content' => $assistant_content ]; - echo "data: " . json_encode($done_event) . "\n\n"; - ob_flush(); + echo "event: done\ndata: " . json_encode($done_data) . "\n\n"; + // 只在有输出缓冲时才刷新 + if (ob_get_level() > 0) { + ob_flush(); + } flush(); $this->log('发送done事件,会话ID:' . $real_conversation_id, 'info'); @@ -625,7 +649,7 @@ class Kefu extends BaseApi { // 记录开始处理阻塞请求 $this->log('AI客服阻塞请求开始处理,用户ID:' . $user_id . ',会话ID:' . $conversation_id, 'info'); - + // 初始化模型 $kefu_message_model = new KefuMessageModel(); $kefu_conversation_model = new KefuConversationModel(); diff --git a/src/addon/aikefu/docs/stream_chat_demo.html b/src/addon/aikefu/docs/stream_chat_demo.html new file mode 100644 index 000000000..da62c8973 --- /dev/null +++ b/src/addon/aikefu/docs/stream_chat_demo.html @@ -0,0 +1,437 @@ + + + + + + 流式聊天测试 Demo + + + +
+

流式聊天测试 Demo

+ +
+

选择请求方式:

+ + +
+ +
+ +
+ + +
+ +
+ 就绪 +
+
+ + + + \ No newline at end of file diff --git a/src/addon/aikefu/event/KefuChat.php b/src/addon/aikefu/event/KefuChat.php index bdc898d44..25aed0b5d 100644 --- a/src/addon/aikefu/event/KefuChat.php +++ b/src/addon/aikefu/event/KefuChat.php @@ -12,7 +12,7 @@ class KefuChat /** * 处理智能客服聊天事件 * @param array $data 事件数据 - * @return array + * @return array|null */ public function handle($data) { @@ -26,6 +26,11 @@ class KefuChat // 调用addon的chat方法 $response = $kefu_api->chat(); + // 对于流式请求,直接输出不返回数据 + if (isset($data['stream']) && $data['stream']) { + return null; + } + // 返回响应数据 return json_decode($response->getContent(), true); } catch (\Exception $e) { diff --git a/src/app/api/controller/Kefu.php b/src/app/api/controller/Kefu.php index c4ad57685..49f0d80f3 100644 --- a/src/app/api/controller/Kefu.php +++ b/src/app/api/controller/Kefu.php @@ -16,6 +16,11 @@ class Kefu extends BaseApi */ public function health() { + // 检查请求方式是否为POST + if (!request()->isPost()) { + return $this->response($this->error('请求方式错误,仅支持POST请求')); + } + $start_time = microtime(true); // (必选) 获取站点ID和会员ID,可以通过事件数据传递 @@ -49,7 +54,7 @@ class Kefu extends BaseApi try { // 触发健康检查事件 - $result = Event::trigger('KefuHealthCheck', $event_data); + $result = event('KefuHealthCheck', $event_data, true); // 汇总检查结果 $health_summary = [ @@ -143,6 +148,11 @@ class Kefu extends BaseApi */ public function info() { + // 检查请求方式是否为POST + if (!request()->isPost()) { + return $this->response($this->error('请求方式错误,仅支持POST请求')); + } + // (可选)获取站点ID和会员ID,可以通过事件数据传递 $site_id = $this->params['uniacid'] ?? $this->site_id; $member_id = $this->params['member_id'] ?? $this->member_id; @@ -167,7 +177,7 @@ class Kefu extends BaseApi ]; // 触发获取配置信息事件 - $result = event('KefuGetInfo', $event_data); + $result = event('KefuGetInfo', $event_data, true); // 处理事件结果 $response = [ @@ -223,6 +233,11 @@ class Kefu extends BaseApi */ public function clearConversation() { + // 检查请求方式是否为POST + if (!request()->isPost()) { + return $this->response($this->error('请求方式错误,仅支持POST请求')); + } + // 获取请求参数 $conversation_id = $this->params['conversation_id'] ?? ''; $user_id = $this->params['user_id'] ?? $this->member_id; @@ -253,7 +268,7 @@ class Kefu extends BaseApi ]; // 触发清除会话事件 - $result = event('KefuClearConversation', $event_data); + $result = event('KefuClearConversation', $event_data, true); // 处理事件结果 $response = [ @@ -286,11 +301,33 @@ class Kefu extends BaseApi */ public function chat() { + // 检查请求方式是否为POST或者是EventSource请求 + $isPost = request()->isPost(); + $isGet = request()->isGet(); + $isEventSource = $isGet && request()->header('Accept') === 'text/event-stream'; + + if (!$isPost && !$isEventSource) { + return $this->response($this->error('请求方式错误,仅支持POST或EventSource请求')); + } + // 获取请求参数 - $message = $this->params['message'] ?? ''; - $user_id = $this->params['user_id'] ?? $this->member_id; - $conversation_id = $this->params['conversation_id'] ?? ''; - $stream = $this->params['stream'] ?? false; + // 对于GET请求,需要单独获取参数,因为BaseApi的构造函数可能没有正确处理GET参数 + if ($isGet) { + $message = request()->get('message', ''); + $user_id = request()->get('user_id', $this->member_id); + $conversation_id = request()->get('conversation_id', ''); + $stream = request()->get('stream', false); + } else { + $message = $this->params['message'] ?? ''; + $user_id = $this->params['user_id'] ?? $this->member_id; + $conversation_id = $this->params['conversation_id'] ?? ''; + $stream = $this->params['stream'] ?? false; + } + + // 确保stream参数正确处理字符串'false'和'0' + if (is_string($stream)) { + $stream = filter_var($stream, FILTER_VALIDATE_BOOLEAN); + } // (可选)获取站点ID和会员ID,可以通过事件数据传递 $site_id = $this->params['uniacid'] ?? $this->site_id; // 使用 uniacid, 方便以后迁移,而且uniacid 是唯一的, site_id 不是,同时被params给过滤了 @@ -320,33 +357,39 @@ class Kefu extends BaseApi ]; if ($stream) { + // 设置SSE响应头 + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('Connection: keep-alive'); + header('X-Accel-Buffering: no'); // 禁用Nginx缓冲 + + // 触发事件,让监听器处理流式响应 event('KefuChat', $event_data); - exit; // 流式请求直接输出并结束 - } - - // 触发智能客服聊天事件(非流式) - $result = event('KefuChat', $event_data); - - // 处理事件结果 - $response = [ - 'code' => 0, - 'message' => 'success', - 'data' => [] - ]; - - if (is_array($result) && !empty($result)) { - foreach ($result as $res) { - if (isset($res['code']) && $res['code'] < 0) { - $response = $res; - break; - } - if (isset($res['data'])) { - $response['data'] = array_merge($response['data'], $res['data']); + } else { + // 触发智能客服聊天事件(非流式) + $result = event('KefuChat', $event_data); + + // 处理事件结果 + $response = [ + 'code' => 0, + 'message' => 'success', + 'data' => [] + ]; + + if (is_array($result) && !empty($result)) { + foreach ($result as $res) { + if (isset($res['code']) && $res['code'] < 0) { + $response = $res; + break; + } + if (isset($res['data'])) { + $response['data'] = array_merge($response['data'], $res['data']); + } } } - } - return $this->response($response); + return $this->response($response); + } } catch (\Exception $e) { return $this->response($this->error('请求失败:' . $e->getMessage())); } @@ -358,6 +401,11 @@ class Kefu extends BaseApi */ public function getHistory() { + // 检查请求方式是否为POST + if (!request()->isPost()) { + return $this->response($this->error('请求方式错误,仅支持POST请求')); + } + // 获取请求参数 $conversation_id = $this->params['conversation_id'] ?? ''; $user_id = $this->params['user_id'] ?? $this->member_id; @@ -387,7 +435,7 @@ class Kefu extends BaseApi ]; // 触发获取历史消息事件 - $result = event('KefuGetHistory', $event_data); + $result = event('KefuGetHistory', $event_data, true); // 处理事件结果 $response = [