From 0b6e6914fd2462fc57c4fc09a4f26ab3b5e201d8 Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Sat, 6 Dec 2025 16:12:12 +0800 Subject: [PATCH] =?UTF-8?q?chore(addon/aikefu):=20=E5=8F=98=E6=9B=B4API?= =?UTF-8?q?=E6=9A=B4=E6=BC=8F=E7=9A=84=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/addon/aikefu/config/event.php | 9 + src/addon/aikefu/event/KefuChatStream.php | 235 ++++++++ .../aikefu/event/KefuClearConversation.php | 118 ++++ src/addon/aikefu/event/KefuHealthCheck.php | 349 ++++++++++++ src/addon/huaweipay/model/Pay.php | 6 +- src/app/api/controller/AI.php | 531 ++++++++++++++++++ 6 files changed, 1245 insertions(+), 3 deletions(-) create mode 100644 src/addon/aikefu/event/KefuChatStream.php create mode 100644 src/addon/aikefu/event/KefuClearConversation.php create mode 100644 src/addon/aikefu/event/KefuHealthCheck.php create mode 100644 src/app/api/controller/AI.php diff --git a/src/addon/aikefu/config/event.php b/src/addon/aikefu/config/event.php index eeb7eaca9..9893130f5 100644 --- a/src/addon/aikefu/config/event.php +++ b/src/addon/aikefu/config/event.php @@ -17,6 +17,15 @@ return [ 'KefuGetHistory' => [ 'addon\aikefu\event\KefuGetHistory' ], + 'KefuClearConversation' => [ + 'addon\aikefu\event\KefuClearConversation' + ], + 'KefuHealthCheck' => [ + 'addon\aikefu\event\KefuHealthCheck' + ], + 'KefuChatStream' => [ + 'addon\aikefu\event\KefuChatStream' + ], ], 'subscribe' => [ diff --git a/src/addon/aikefu/event/KefuChatStream.php b/src/addon/aikefu/event/KefuChatStream.php new file mode 100644 index 000000000..5b50d0ae0 --- /dev/null +++ b/src/addon/aikefu/event/KefuChatStream.php @@ -0,0 +1,235 @@ + 'error', + 'message' => '消息内容不能为空' + ] + ]; + } + + // 获取智能客服配置 + $kefu_config_model = new KefuConfigModel(); + $config_info = $kefu_config_model->getConfig($site_id)['data']['value'] ?? []; + + if (empty($config_info) || $config_info['status'] != 1) { + return [ + [ + 'type' => 'error', + 'message' => '智能客服暂未启用' + ] + ]; + } + + $config = $config_info; + $apiKey = $config['api_key']; + $baseUrl = $config['base_url']; + $chatEndpoint = $config['chat_endpoint']; + + // 构建请求数据 + $requestData = [ + 'inputs' => [], + 'query' => $message, + 'response_mode' => 'streaming', + 'user' => $user_id, + ]; + + if (!empty($conversation_id)) { + $requestData['conversation_id'] = $conversation_id; + } + + // 构建请求头 + $headers = [ + 'Authorization: Bearer ' . $apiKey, + 'Content-Type: application/json', + 'Accept: text/event-stream', + ]; + + // 发送流式请求到Dify API + $url = $baseUrl . $chatEndpoint; + $result = $this->executeStreamRequest($url, $requestData, $headers); + + return $result; + + } catch (\Exception $e) { + return [ + [ + 'type' => 'error', + 'message' => '请求失败:' . $e->getMessage() + ] + ]; + } + } + + /** + * 执行流式请求 + */ + private function executeStreamRequest($url, $requestData, $headers) + { + $ch = curl_init(); + + // 设置curl选项 + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData)); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, [$this, 'streamCallback']); + curl_setopt($ch, CURLOPT_TIMEOUT, 120); // 设置较长的超时时间 + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + + // 执行请求 + $result = []; + $this->stream_buffer = ''; + $this->conversation_id = ''; + $this->current_message_id = ''; + + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($response === false || !empty($error)) { + return [ + [ + 'type' => 'error', + 'message' => '请求失败:' . $error + ] + ]; + } + + if ($http_code >= 400) { + return [ + [ + 'type' => 'error', + 'message' => '服务端错误,HTTP状态码:' . $http_code + ] + ]; + } + + return $this->parseStreamResponse($this->stream_buffer); + } + + /** + * 流式回调函数 + */ + private function streamCallback($ch, $data) + { + $this->stream_buffer .= $data; + return strlen($data); + } + + /** + * 解析流式响应 + */ + private function parseStreamResponse($response) + { + $result = []; + $lines = explode("\n", $response); + $conversation_id = ''; + $message_id = ''; + $buffer = ''; + + foreach ($lines as $line) { + $line = trim($line); + + if (empty($line)) { + continue; + } + + if (strpos($line, 'data: ') === 0) { + $data_str = substr($line, 6); + + if ($data_str === '[DONE]') { + // 流结束,发送完成事件 + $result[] = [ + 'type' => 'complete', + 'conversation_id' => $conversation_id, + 'message_id' => $message_id, + 'content' => $buffer, + 'finished' => true + ]; + break; + } + + $data = json_decode($data_str, true); + if (json_last_error() === JSON_ERROR_NONE && isset($data)) { + // 提取会话ID和消息ID + if (isset($data['conversation_id'])) { + $conversation_id = $data['conversation_id']; + } + if (isset($data['message_id'])) { + $message_id = $data['message_id']; + } + + // 处理内容块 + if (isset($data['answer'])) { + $buffer .= $data['answer']; + $result[] = [ + 'type' => 'chunk', + 'content' => $data['answer'], + 'conversation_id' => $conversation_id, + 'message_id' => $message_id, + 'finished' => false + ]; + } + + // 检查是否结束 + if (isset($data['finish_reason']) && $data['finish_reason'] !== 'null') { + $result[] = [ + 'type' => 'complete', + 'conversation_id' => $conversation_id, + 'message_id' => $message_id, + 'content' => $buffer, + 'finish_reason' => $data['finish_reason'], + 'usage' => $data['usage'] ?? [], + 'finished' => true + ]; + break; + } + } + } + } + + // 如果没有收到[DONE]信号,确保发送完成事件 + if (empty($result) || end($result)['type'] !== 'complete') { + $result[] = [ + 'type' => 'complete', + 'conversation_id' => $conversation_id, + 'message_id' => $message_id, + 'content' => $buffer, + 'finished' => true + ]; + } + + return $result; + } +} \ No newline at end of file diff --git a/src/addon/aikefu/event/KefuClearConversation.php b/src/addon/aikefu/event/KefuClearConversation.php new file mode 100644 index 000000000..dfefa9af1 --- /dev/null +++ b/src/addon/aikefu/event/KefuClearConversation.php @@ -0,0 +1,118 @@ + -1, + 'message' => '会话ID或用户ID不能为空', + 'data' => [] + ]; + } + + $conversation_model = new KefuConversationModel(); + $message_model = new KefuMessageModel(); + + $deleted_messages = 0; + $deleted_conversations = 0; + + if (!empty($conversation_id)) { + // 删除指定会话的消息和会话记录 + + // 先删除该会话的所有消息 + $message_condition = [ + ['site_id', '=', $site_id], + ['conversation_id', '=', $conversation_id] + ]; + + $message_result = $message_model->deleteMessage($message_condition); + if ($message_result['code'] >= 0) { + $deleted_messages = $message_result['data']['result'] ?? 0; + } + + // 再删除会话记录 + $conversation_condition = [ + ['site_id', '=', $site_id], + ['conversation_id', '=', $conversation_id] + ]; + + $conversation_result = $conversation_model->deleteConversation($conversation_condition); + if ($conversation_result['code'] >= 0) { + $deleted_conversations = $conversation_result['data']['result'] ?? 0; + } + + } else if (!empty($user_id)) { + // 删除指定用户的所有会话和消息 + + // 先获取该用户的所有会话ID + $conversation_list = $conversation_model->getConversationList([ + ['site_id', '=', $site_id], + ['user_id', '=', $user_id] + ], 'conversation_id'); + + $conversation_ids = array_column($conversation_list['data'], 'conversation_id'); + + if (!empty($conversation_ids)) { + // 删除所有会话的消息 + $message_condition = [ + ['site_id', '=', $site_id], + ['conversation_id', 'in', $conversation_ids] + ]; + + $message_result = $message_model->deleteMessage($message_condition); + if ($message_result['code'] >= 0) { + $deleted_messages = $message_result['data']['result'] ?? 0; + } + + // 删除所有会话记录 + $conversation_condition = [ + ['site_id', '=', $site_id], + ['user_id', '=', $user_id] + ]; + + $conversation_result = $conversation_model->deleteConversation($conversation_condition); + if ($conversation_result['code'] >= 0) { + $deleted_conversations = $conversation_result['data']['result'] ?? 0; + } + } + } + + return [ + 'code' => 0, + 'message' => '清除成功', + 'data' => [ + 'deleted_messages' => $deleted_messages, + 'deleted_conversations' => $deleted_conversations + ] + ]; + + } catch (\Exception $e) { + return [ + 'code' => -1, + 'message' => '清除失败:' . $e->getMessage(), + 'data' => [] + ]; + } + } +} \ No newline at end of file diff --git a/src/addon/aikefu/event/KefuHealthCheck.php b/src/addon/aikefu/event/KefuHealthCheck.php new file mode 100644 index 000000000..0173fd305 --- /dev/null +++ b/src/addon/aikefu/event/KefuHealthCheck.php @@ -0,0 +1,349 @@ +checkDatabase(); + } + + // 2. AI服务配置检查 + if (in_array($check_type, ['full', 'ai_service'])) { + $check_results[] = $this->checkAIServiceConfig($site_id); + } + + // 3. AI服务连接检查 + if (in_array($check_type, ['full', 'ai_service'])) { + $check_results[] = $this->checkAIServiceConnection($site_id); + } + + // 4. 系统资源检查 + if (in_array($check_type, ['full'])) { + $check_results[] = $this->checkSystemResources(); + } + + } catch (\Exception $e) { + $check_results[] = [ + 'component' => 'health_check_error', + 'status' => 'error', + 'message' => '健康检查过程异常:' . $e->getMessage(), + 'response_time_ms' => 0 + ]; + } + + return $check_results; + } + + /** + * 检查数据库连接 + */ + private function checkDatabase() + { + $start_time = microtime(true); + + try { + // 测试数据库连接 + $result = Db::query('SELECT 1 as test'); + + if (!empty($result) && $result[0]['test'] == 1) { + return [ + 'component' => 'database', + 'status' => 'healthy', + 'message' => '数据库连接正常', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2), + 'details' => [ + 'connection' => 'success', + 'query_test' => 'passed' + ] + ]; + } else { + return [ + 'component' => 'database', + 'status' => 'error', + 'message' => '数据库查询测试失败', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ]; + } + + } catch (\Exception $e) { + return [ + 'component' => 'database', + 'status' => 'error', + 'message' => '数据库连接失败:' . $e->getMessage(), + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ]; + } + } + + /** + * 检查AI服务配置 + */ + private function checkAIServiceConfig($site_id) + { + $start_time = microtime(true); + + try { + $config_model = new KefuConfigModel(); + $config_info = $config_model->getConfig($site_id); + + if (empty($config_info['data']['value'])) { + return [ + 'component' => 'ai_service_config', + 'status' => 'warning', + 'message' => '智能客服配置未设置', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2), + 'details' => [ + 'configured' => false, + 'required_fields' => ['api_key', 'base_url', 'chat_endpoint'] + ] + ]; + } + + $config = $config_info['data']['value']; + $required_fields = ['api_key', 'base_url', 'chat_endpoint']; + $missing_fields = []; + + foreach ($required_fields as $field) { + if (empty($config[$field])) { + $missing_fields[] = $field; + } + } + + if (!empty($missing_fields)) { + return [ + 'component' => 'ai_service_config', + 'status' => 'warning', + 'message' => 'AI服务配置不完整,缺少字段:' . implode(', ', $missing_fields), + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2), + 'details' => [ + 'configured' => true, + 'complete' => false, + 'missing_fields' => $missing_fields + ] + ]; + } + + if ($config['status'] != 1) { + return [ + 'component' => 'ai_service_config', + 'status' => 'warning', + 'message' => '智能服务已禁用', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2), + 'details' => [ + 'configured' => true, + 'complete' => true, + 'enabled' => false + ] + ]; + } + + return [ + 'component' => 'ai_service_config', + 'status' => 'healthy', + 'message' => 'AI服务配置正常', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2), + 'details' => [ + 'configured' => true, + 'complete' => true, + 'enabled' => true, + 'base_url' => $config['base_url'] + ] + ]; + + } catch (\Exception $e) { + return [ + 'component' => 'ai_service_config', + 'status' => 'error', + 'message' => 'AI服务配置检查失败:' . $e->getMessage(), + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ]; + } + } + + /** + * 检查AI服务连接 + */ + private function checkAIServiceConnection($site_id) + { + $start_time = microtime(true); + + try { + $config_model = new KefuConfigModel(); + $config_info = $config_model->getConfig($site_id); + + if (empty($config_info['data']['value'])) { + return [ + 'component' => 'ai_service_connection', + 'status' => 'warning', + 'message' => 'AI服务未配置,跳过连接检查', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ]; + } + + $config = $config_info['data']['value']; + + if ($config['status'] != 1 || empty($config['api_key']) || empty($config['base_url'])) { + return [ + 'component' => 'ai_service_connection', + 'status' => 'warning', + 'message' => 'AI服务未启用或配置不完整,跳过连接检查', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ]; + } + + // 测试连接(发送一个简单的健康检查请求) + $url = $config['base_url']; + $headers = [ + 'Authorization: Bearer ' . $config['api_key'], + 'Content-Type: application/json', + ]; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_NOBODY, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($response === false) { + return [ + 'component' => 'ai_service_connection', + 'status' => 'error', + 'message' => '无法连接到AI服务', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ]; + } + + if ($http_code >= 200 && $http_code < 300) { + return [ + 'component' => 'ai_service_connection', + 'status' => 'healthy', + 'message' => 'AI服务连接正常', + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2), + 'details' => [ + 'http_status' => $http_code, + 'url' => $url + ] + ]; + } else { + return [ + 'component' => 'ai_service_connection', + 'status' => 'warning', + 'message' => 'AI服务响应异常,HTTP状态码:' . $http_code, + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2), + 'details' => [ + 'http_status' => $http_code, + 'url' => $url + ] + ]; + } + + } catch (\Exception $e) { + return [ + 'component' => 'ai_service_connection', + 'status' => 'error', + 'message' => 'AI服务连接检查失败:' . $e->getMessage(), + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ]; + } + } + + /** + * 检查系统资源 + */ + private function checkSystemResources() + { + $start_time = microtime(true); + + try { + $memory_usage = memory_get_usage(true); + $memory_limit = $this->parseMemoryLimit(ini_get('memory_limit')); + $memory_usage_percent = ($memory_usage / $memory_limit) * 100; + + $details = [ + 'php_version' => PHP_VERSION, + 'memory_usage' => round($memory_usage / 1024 / 1024, 2) . ' MB', + 'memory_limit' => round($memory_limit / 1024 / 1024, 2) . ' MB', + 'memory_usage_percent' => round($memory_usage_percent, 2) . '%', + 'max_execution_time' => ini_get('max_execution_time') . 's', + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'post_max_size' => ini_get('post_max_size') + ]; + + $status = 'healthy'; + $message = '系统资源正常'; + + // 检查内存使用率 + if ($memory_usage_percent > 90) { + $status = 'error'; + $message = '内存使用率过高'; + } elseif ($memory_usage_percent > 80) { + $status = 'warning'; + $message = '内存使用率较高'; + } + + return [ + 'component' => 'system_resources', + 'status' => $status, + 'message' => $message, + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2), + 'details' => $details + ]; + + } catch (\Exception $e) { + return [ + 'component' => 'system_resources', + 'status' => 'error', + 'message' => '系统资源检查失败:' . $e->getMessage(), + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ]; + } + } + + /** + * 解析内存限制值 + */ + private function parseMemoryLimit($val) + { + $val = trim($val); + $last = strtolower($val[strlen($val)-1]); + $val = (int)$val; + + switch($last) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + + return $val; + } +} \ No newline at end of file diff --git a/src/addon/huaweipay/model/Pay.php b/src/addon/huaweipay/model/Pay.php index 89c0e4070..54ce7a921 100644 --- a/src/addon/huaweipay/model/Pay.php +++ b/src/addon/huaweipay/model/Pay.php @@ -176,19 +176,19 @@ class Pay extends BaseModel Log::info('华为APP支付响应: ' . json_encode($result)); if (isset($result['code']) && $result['code'] == '0') { - return $this->success([ + return success(0, '', [ 'type' => 'params', 'data' => $result ]); } else { $errorMsg = $result['msg'] ?? ($result['message'] ?? '华为支付请求失败'); Log::error('华为APP支付失败: ' . $errorMsg); - return $this->error('', $errorMsg); + return error(500000, $errorMsg); } break; default: // 默认返回错误 - return $this->error('', '不支持的支付类型: ' . $param["app_type"]); + return error(500000, '不支持的支付类型: ' . $param["app_type"]); } } catch (\Exception $e) { Log::error('华为支付生成支付失败: ' . $e->getMessage() . ',错误堆栈: ' . $e->getTraceAsString()); diff --git a/src/app/api/controller/AI.php b/src/app/api/controller/AI.php new file mode 100644 index 000000000..47cb97db2 --- /dev/null +++ b/src/app/api/controller/AI.php @@ -0,0 +1,531 @@ +params['uniacid'] ?? $this->site_id; // 使用 uniacid, 方便以后迁移,而且uniacid 是唯一的, site_id 不是,同时被params给过滤了 + + // 检查site_id 如果 < 1, 则返回错误 + if ($site_id < 1) { + return $this->response($this->error('缺少关键参数站点ID')); + } + + // 准备事件数据 + $event_data = [ + 'check_id' => uniqid('health_', true), + 'timestamp' => date('Y-m-d H:i:s'), + 'site_id' => $site_id, + 'check_type' => $this->params['check_type'] ?? 'full' // full, basic, ai_service + ]; + + try { + // 触发健康检查事件 + $result = Event::trigger('KefuHealthCheck', $event_data); + + // 汇总检查结果 + $health_summary = [ + 'status' => 'healthy', + 'check_id' => $event_data['check_id'], + 'timestamp' => $event_data['timestamp'], + 'total_checks' => 0, + 'passed_checks' => 0, + 'failed_checks' => 0, + 'response_time_ms' => 0, + 'components' => [], + 'warnings' => [], + 'errors' => [] + ]; + + if (is_array($result) && !empty($result)) { + foreach ($result as $check_result) { + if (isset($check_result['component'])) { + $health_summary['components'][$check_result['component']] = $check_result; + $health_summary['total_checks']++; + + if ($check_result['status'] === 'healthy') { + $health_summary['passed_checks']++; + } else { + $health_summary['failed_checks']++; + if ($check_result['status'] === 'error') { + $health_summary['errors'][] = $check_result['message'] ?? 'Unknown error'; + $health_summary['status'] = 'unhealthy'; + } elseif ($check_result['status'] === 'warning') { + $health_summary['warnings'][] = $check_result['message'] ?? 'Unknown warning'; + if ($health_summary['status'] === 'healthy') { + $health_summary['status'] = 'warning'; + } + } + } + } + } + } + + // 计算总体响应时间 + $health_summary['response_time_ms'] = round((microtime(true) - $start_time) * 1000, 2); + + // 如果没有任何检查结果,进行基础检查 + if ($health_summary['total_checks'] === 0) { + $health_summary['components']['basic'] = [ + 'status' => 'healthy', + 'message' => '基础服务正常', + 'response_time_ms' => 0, + 'details' => [ + 'php_version' => PHP_VERSION, + 'memory_usage' => round(memory_get_usage() / 1024 / 1024, 2) . ' MB', + 'time_limit' => ini_get('max_execution_time') . 's' + ] + ]; + $health_summary['total_checks'] = 1; + $health_summary['passed_checks'] = 1; + } + + // 根据检查结果确定HTTP状态码 + $http_code = 200; + if ($health_summary['status'] === 'unhealthy') { + $http_code = 503; // Service Unavailable + } elseif ($health_summary['status'] === 'warning') { + $http_code = 200; // Still OK but with warnings + } + + return $this->response([ + 'code' => 0, + 'message' => $health_summary['status'], + 'data' => $health_summary + ], $http_code); + + } catch (\Exception $e) { + return $this->response([ + 'code' => -1, + 'message' => '健康检查失败', + 'data' => [ + 'status' => 'error', + 'check_id' => $event_data['check_id'], + 'timestamp' => $event_data['timestamp'], + 'error' => $e->getMessage(), + 'response_time_ms' => round((microtime(true) - $start_time) * 1000, 2) + ] + ], 500); + } + } + + /** + * 清除会话历史 + */ + public function clearConversation() + { + // 获取请求参数 + $conversation_id = $this->params['conversation_id'] ?? ''; + $user_id = $this->params['user_id'] ?? $this->member_id; + + // (可选)获取站点ID和会员ID,可以通过事件数据传递 + $site_id = $this->params['uniacid'] ?? $this->site_id; // 使用 uniacid, 方便以后迁移,而且uniacid 是唯一的, site_id 不是,同时被params给过滤了 + $member_id = $this->params['member_id'] ?? $this->member_id; + $token = $this->params['token'] ?? $this->token; + + // 验证参数 + if (empty($conversation_id)) { + return $this->response($this->error('会话ID不能为空')); + } + + try { + // 准备事件数据 + $event_data = [ + 'conversation_id' => $conversation_id, + 'user_id' => $user_id, + 'site_id' =>$site_id, + 'member_id' => $member_id, + 'token' => $token, + ]; + + // 触发清除会话事件 + $result = Event::trigger('KefuClearConversation', $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); + } catch (\Exception $e) { + return $this->response($e->getMessage()); + } + } + + /** + * 智能客服聊天接口 + * @return \think\response\Json + */ + public function chat() + { + // 获取请求参数 + $message = $this->params['message'] ?? ''; + $user_id = $this->params['user_id'] ?? $this->member_id; + $conversation_id = $this->params['conversation_id'] ?? ''; + $stream = $this->params['stream'] ?? false; + + // (可选)获取站点ID和会员ID,可以通过事件数据传递 + $site_id = $this->params['uniacid'] ?? $this->site_id; // 使用 uniacid, 方便以后迁移,而且uniacid 是唯一的, site_id 不是,同时被params给过滤了 + $member_id = $this->params['member_id'] ?? $this->member_id; + $token = $this->params['token'] ?? $this->token; + + // 验证参数 + if (empty($message)) { + return $this->response($this->error('请输入消息内容')); + } + + try { + // 准备事件数据 + $event_data = [ + 'message' => $message, + 'user_id' => $user_id, + 'conversation_id' => $conversation_id, + 'stream' => $stream, + 'site_id' =>$site_id, + 'member_id' => $member_id, + 'token' => $token, + ]; + + // 触发智能客服聊天事件 + $result = Event::trigger('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); + } catch (\Exception $e) { + return $this->response($this->error('请求失败:' . $e->getMessage())); + } + } + + /** + * 智能客服聊天流式接口 + */ + public function chatStream() + { + // 获取请求参数 + $message = $this->params['message'] ?? ''; + $user_id = $this->params['user_id'] ?? $this->member_id; + $conversation_id = $this->params['conversation_id'] ?? ''; + $stream = true; + + // (可选)获取站点ID和会员ID,可以通过事件数据传递 + $site_id = $this->params['uniacid'] ?? $this->site_id; // 使用 uniacid, 方便以后迁移,而且uniacid 是唯一的, site_id 不是,同时被params给过滤了 + $member_id = $this->params['member_id'] ?? $this->member_id; + $token = $this->params['token'] ?? $this->token; + + // 验证参数 + if (empty($message)) { + $this->sendStreamError('请输入消息内容'); + return; + } + + try { + // 设置流式响应头 + $this->setStreamHeaders(); + + // 准备事件数据 + $event_data = [ + 'message' => $message, + 'user_id' => $user_id, + 'conversation_id' => $conversation_id, + 'stream' => $stream, + 'site_id' =>$site_id, + 'member_id' => $member_id, + 'token' => $token, + 'request_id' => uniqid('stream_', true), + 'timestamp' => time(), + ]; + + // 发送开始事件 + $this->sendStreamEvent('start', [ + 'request_id' => $event_data['request_id'], + 'timestamp' => $event_data['timestamp'], + 'message' => '开始处理请求' + ]); + + // 触发流式聊天事件 + $result = Event::trigger('KefuChatStream', $event_data); + + // 处理事件结果并流式输出 + $this->processStreamResults($result, $event_data); + + } catch (\Exception $e) { + $this->sendStreamError('请求失败:' . $e->getMessage()); + } + } + + /** + * 设置流式响应头 + */ + private function setStreamHeaders() + { + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('Connection: keep-alive'); + header('Access-Control-Allow-Origin: *'); + header('Access-Control-Allow-Headers: Cache-Control, Content-Type'); + header('X-Accel-Buffering: no'); // 禁用nginx缓冲 + if (function_exists('apache_setenv')) { + apache_setenv('no-gzip', '1'); + } + } + + /** + * 发送流式事件 + */ + private function sendStreamEvent($event_type, $data) + { + $payload = [ + 'id' => uniqid(), + 'event' => $event_type, + 'timestamp' => time(), + 'data' => $data + ]; + + echo "event: {$event_type}\n"; + echo "data: " . json_encode($payload, JSON_UNESCAPED_UNICODE) . "\n\n"; + + if (ob_get_level()) { + ob_flush(); + } + flush(); + } + + /** + * 发送流式错误 + */ + private function sendStreamError($message) + { + if (!headers_sent()) { + $this->setStreamHeaders(); + } + + $this->sendStreamEvent('error', [ + 'error' => $message, + 'finished' => true + ]); + + exit; + } + + /** + * 处理流式结果 + */ + private function processStreamResults($results, $event_data) + { + try { + if (is_array($results) && !empty($results)) { + foreach ($results as $result) { + if (isset($result['type'])) { + switch ($result['type']) { + case 'chunk': + // 发送内容块 + $this->sendStreamEvent('message', [ + 'content' => $result['content'] ?? '', + 'conversation_id' => $result['conversation_id'] ?? $event_data['conversation_id'], + 'message_id' => $result['message_id'] ?? '', + 'finished' => $result['finished'] ?? false + ]); + break; + + case 'error': + // 发送错误 + $this->sendStreamEvent('error', [ + 'error' => $result['message'] ?? '未知错误', + 'conversation_id' => $result['conversation_id'] ?? $event_data['conversation_id'] + ]); + break; + + case 'complete': + // 发送完成事件 + $this->sendStreamEvent('complete', [ + 'conversation_id' => $result['conversation_id'] ?? $event_data['conversation_id'], + 'message_id' => $result['message_id'] ?? '', + 'usage' => $result['usage'] ?? [], + 'finish_reason' => $result['finish_reason'] ?? '' + ]); + break; + } + } + } + } else { + // 如果没有事件处理器或结果为空,发送完成事件 + $this->sendStreamEvent('complete', [ + 'conversation_id' => $event_data['conversation_id'], + 'message' => '暂无可用响应' + ]); + } + + // 发送结束事件 + $this->sendStreamEvent('end', [ + 'request_id' => $event_data['request_id'], + 'status' => 'completed' + ]); + + } catch (\Exception $e) { + $this->sendStreamError('处理流式结果失败:' . $e->getMessage()); + } + } + + + + /** + * 创建新会话 + * @return \think\response\Json + */ + public function createConversation() + { + // 获取请求参数 + $user_id = $this->params['user_id'] ?? $this->member_id; + + // (可选)获取站点ID和会员ID,可以通过事件数据传递 + $site_id = $this->params['uniacid'] ?? $this->site_id; // 使用 uniacid, 方便以后迁移,而且uniacid 是唯一的, site_id 不是,同时被params给过滤了 + $member_id = $this->params['member_id'] ?? $this->member_id; + $token = $this->params['token'] ?? $this->token; + + try { + // 准备事件数据 + $event_data = [ + 'user_id' => $user_id, + 'site_id' =>$site_id, + 'member_id' => $member_id, + 'token' => $token, + ]; + + // 触发创建会话事件 + $result = Event::trigger('KefuCreateConversation', $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); + } catch (\Exception $e) { + return $this->response($this->error('请求失败:' . $e->getMessage())); + } + } + + /** + * 获取会话历史 + * @return \think\response\Json + */ + public function getHistory() + { + // 获取请求参数 + $conversation_id = $this->params['conversation_id'] ?? ''; + $user_id = $this->params['user_id'] ?? $this->member_id; + $limit = $this->params['limit'] ?? 20; + $offset = $this->params['offset'] ?? 0; + + // (可选)获取站点ID和会员ID,可以通过事件数据传递 + $site_id = $this->params['uniacid'] ?? $this->site_id; // 使用 uniacid, 方便以后迁移,而且uniacid 是唯一的, site_id 不是,同时被params给过滤了 + $member_id = $this->params['member_id'] ?? $this->member_id; + $token = $this->params['token'] ?? $this->token; + + // 验证参数 + if (empty($conversation_id)) { + return $this->response($this->error('会话ID不能为空')); + } + + try { + // 准备事件数据 + $event_data = [ + 'conversation_id' => $conversation_id, + 'user_id' => $user_id, + 'limit' => $limit, + 'offset' => $offset, + 'site_id' =>$site_id, + 'member_id' => $member_id, + 'token' => $token, + ]; + + // 触发获取历史消息事件 + $result = Event::trigger('KefuGetHistory', $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); + } catch (\Exception $e) { + return $this->response($this->error('请求失败:' . $e->getMessage())); + } + } +}