chore(addon/aikefu): 变更API暴漏的端点
This commit is contained in:
@@ -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' => [
|
||||
|
||||
235
src/addon/aikefu/event/KefuChatStream.php
Normal file
235
src/addon/aikefu/event/KefuChatStream.php
Normal file
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\model\Config as KefuConfigModel;
|
||||
use addon\aikefu\model\Conversation as KefuConversationModel;
|
||||
use addon\aikefu\model\Message as KefuMessageModel;
|
||||
|
||||
/**
|
||||
* 智能客服流式聊天
|
||||
*/
|
||||
class KefuChatStream
|
||||
{
|
||||
/**
|
||||
* 处理智能客服流式聊天事件
|
||||
* @param array $data 事件数据
|
||||
* @return array
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$message = $data['message'] ?? '';
|
||||
$user_id = $data['user_id'] ?? '';
|
||||
$conversation_id = $data['conversation_id'] ?? '';
|
||||
$site_id = $data['site_id'] ?? 0;
|
||||
$request_id = $data['request_id'] ?? '';
|
||||
|
||||
try {
|
||||
// 验证参数
|
||||
if (empty($message)) {
|
||||
return [
|
||||
[
|
||||
'type' => '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;
|
||||
}
|
||||
}
|
||||
118
src/addon/aikefu/event/KefuClearConversation.php
Normal file
118
src/addon/aikefu/event/KefuClearConversation.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\model\Conversation as KefuConversationModel;
|
||||
use addon\aikefu\model\Message as KefuMessageModel;
|
||||
|
||||
/**
|
||||
* 清除客服会话历史
|
||||
*/
|
||||
class KefuClearConversation
|
||||
{
|
||||
/**
|
||||
* 处理清除会话历史事件
|
||||
* @param array $data 事件数据
|
||||
* @return array
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
try {
|
||||
$conversation_id = $data['conversation_id'] ?? '';
|
||||
$user_id = $data['user_id'] ?? '';
|
||||
$site_id = $data['site_id'] ?? 0;
|
||||
|
||||
// 验证参数
|
||||
if (empty($conversation_id) && empty($user_id)) {
|
||||
return [
|
||||
'code' => -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' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
349
src/addon/aikefu/event/KefuHealthCheck.php
Normal file
349
src/addon/aikefu/event/KefuHealthCheck.php
Normal file
@@ -0,0 +1,349 @@
|
||||
<?php
|
||||
|
||||
namespace addon\aikefu\event;
|
||||
|
||||
use addon\aikefu\model\Config as KefuConfigModel;
|
||||
use think\facade\Db;
|
||||
|
||||
/**
|
||||
* 智能客服健康检查
|
||||
*/
|
||||
class KefuHealthCheck
|
||||
{
|
||||
/**
|
||||
* 处理健康检查事件
|
||||
* @param array $data 事件数据
|
||||
* @return array
|
||||
*/
|
||||
public function handle($data)
|
||||
{
|
||||
$check_results = [];
|
||||
$site_id = $data['site_id'] ?? 0;
|
||||
$check_type = $data['check_type'] ?? 'full';
|
||||
|
||||
try {
|
||||
// 1. 数据库连接检查
|
||||
if (in_array($check_type, ['full', 'basic'])) {
|
||||
$check_results[] = $this->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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
531
src/app/api/controller/AI.php
Normal file
531
src/app/api/controller/AI.php
Normal file
@@ -0,0 +1,531 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\api\controller\BaseApi;
|
||||
use think\facade\Event;
|
||||
|
||||
/**
|
||||
* 智能客服API控制器
|
||||
*/
|
||||
class AI extends BaseApi
|
||||
{
|
||||
|
||||
/**
|
||||
* 系统健康检查, 用来检测当前的AI服务是否正常
|
||||
* @return \think\response\Json | bool
|
||||
*/
|
||||
public function health()
|
||||
{
|
||||
$start_time = microtime(true);
|
||||
|
||||
// (必选) 获取站点ID和会员ID,可以通过事件数据传递
|
||||
$site_id = $this->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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user