532 lines
19 KiB
PHP
532 lines
19 KiB
PHP
<?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()));
|
||
}
|
||
}
|
||
}
|