Files
shop-platform/src/app/api/controller/AI.php

532 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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()));
}
}
}