实现后台及前台通过API访问UV埋点,所有代码全部保存
This commit is contained in:
505
src/app/api/controller/AI.php
Normal file
505
src/app/api/controller/AI.php
Normal file
@@ -0,0 +1,505 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AI API服务, 主要用来与终端进行AI相关的交互
|
||||
* 1. 与微信小程序/H5的智能客服进行交互
|
||||
* 2. 连接Dify平台或RAGFlow平台的智能体
|
||||
*/
|
||||
namespace app\api\controller;
|
||||
use Exception;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
use think\response\Json;
|
||||
use app\exception\ApiException;
|
||||
|
||||
use app\model\ai\AiChatSession;
|
||||
use app\model\ai\AiChatHistory;
|
||||
use app\model\web\Config;
|
||||
|
||||
class AI extends BaseApi
|
||||
{
|
||||
/**
|
||||
* 日志文件名称
|
||||
* @var string
|
||||
*/
|
||||
private $log_file = 'ai.log';
|
||||
|
||||
/**
|
||||
* 配置模型实例
|
||||
* @var Config
|
||||
*/
|
||||
protected $configModel;
|
||||
|
||||
/**
|
||||
* AiChatSession模型实例
|
||||
* @var AiChatSession
|
||||
*/
|
||||
protected $aiChatSessionModel;
|
||||
|
||||
/**
|
||||
* AiChatHistory模型实例
|
||||
* @var AiChatHistory
|
||||
*/
|
||||
protected $aiChatHistoryModel;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->aiChatSessionModel = new AiChatSession();
|
||||
$this->aiChatHistoryModel = new AiChatHistory();
|
||||
$this->configModel = new Config();
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台类型常量
|
||||
*/
|
||||
const PLATFORM_DIFY = 'dify';
|
||||
const PLATFORM_RAGFLOW = 'ragflow';
|
||||
|
||||
/**
|
||||
* AI对话接口
|
||||
* 发送对话或者创建会话
|
||||
* 1. 支持与Dify平台或RAGFlow平台的智能体进行交互
|
||||
* 2. 支持会话管理,每个用户在每个平台上的会话是独立的
|
||||
* 3. 支持上下文管理,每个会话都维护一个上下文信息,用于持续对话
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
public function chat()
|
||||
{
|
||||
log_write('AI chat request: ' . json_encode($this->params), 'info', $this->log_file);
|
||||
try {
|
||||
// 获取请求参数
|
||||
$message = $this->params['message'] ?? ''; // 用户消息
|
||||
$session_id = $this->params['session_id'] ?? ''; // 会话ID
|
||||
$user_id = $this->params['user_id'] ?? $this->member_id; // 用户ID
|
||||
$context = $this->params['context'] ?? []; // 上下文信息
|
||||
|
||||
$site_id = $this->params['uniacid'] ?? $this->site_id; // 站点ID
|
||||
$app_module = $this->params['app_module'] ?? $this->app_module; // 应用模块
|
||||
|
||||
// 参数验证
|
||||
if (empty($message)) {
|
||||
return $this->response($this->error('', 'MESSAGE_EMPTY'));
|
||||
}
|
||||
|
||||
// 获取平台配置
|
||||
$config = $this->getPlatformConfig($site_id, $app_module);
|
||||
if (!$config['status']) {
|
||||
return $this->response($this->error('', $config['message']));
|
||||
}
|
||||
|
||||
$platform = $config['data']['default']['type'] ?? self::PLATFORM_DIFY; // 平台类型
|
||||
|
||||
// 生成或使用现有会话ID
|
||||
$session_id = $session_id ?: $this->generateSessionId($user_id, $platform);
|
||||
|
||||
// 根据平台类型调用不同的方法
|
||||
$result = [];
|
||||
if ($platform === self::PLATFORM_DIFY) {
|
||||
$result = $this->callDifyApi($config['data'], $message, $session_id, $user_id, $context);
|
||||
} else if ($platform === self::PLATFORM_RAGFLOW) {
|
||||
$result = $this->callRagflowApi($config['data'], $message, $session_id, $user_id, $context);
|
||||
}
|
||||
|
||||
if (!$result['status']) {
|
||||
return $this->response($this->error('', $result['message']));
|
||||
}
|
||||
|
||||
// 保存会话记录
|
||||
$this->saveChatHistory($user_id, $session_id, $platform, $message, $result['data']['content']);
|
||||
|
||||
return $this->response($this->success([
|
||||
'session_id' => $session_id,
|
||||
'content' => $result['data']['content'],
|
||||
'tokens' => $result['data']['tokens'] ?? null,
|
||||
'response_time' => $result['data']['response_time'] ?? null
|
||||
]));
|
||||
|
||||
} catch (Exception $e) {
|
||||
// 记录错误日志
|
||||
log_write('AI chat error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'AI_SERVICE_ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
* @param int $site_id 站点ID 默认为1, 业务站点uniacid值与site_id保持一致
|
||||
* @param string $app_module 应用模块 默认为shop
|
||||
* @return array
|
||||
*/
|
||||
private function getPlatformConfig($site_id, $app_module)
|
||||
{
|
||||
$config = [];
|
||||
try {
|
||||
// 从配置模型获取平台配置
|
||||
$config = $this->configModel->getAIPlatformConfig($site_id, $app_module);
|
||||
|
||||
throw new \Exception('Get AI platform config error: ' . json_encode($config));
|
||||
|
||||
if (!$config || !$config['status']) {
|
||||
return ['status' => false, 'message' => 'PLATFORM_CONFIG_NOT_FOUND'];
|
||||
}
|
||||
|
||||
$config_data = json_decode($config['config'], true);
|
||||
if (!$config_data || empty($config_data['api_key']) || empty($config_data['base_url']) || empty($config_data['app_id'])) {
|
||||
return ['status' => false, 'message' => 'PLATFORM_CONFIG_INVALID'];
|
||||
}
|
||||
|
||||
return ['status' => true, 'data' => $config_data];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return ['status' => false, 'message' => 'GET_CONFIG_ERROR' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话ID
|
||||
* @param string $user_id
|
||||
* @param string $platform
|
||||
* @return string
|
||||
*/
|
||||
private function generateSessionId($user_id, $platform)
|
||||
{
|
||||
return md5($user_id . '_' . $platform . '_' . time() . '_' . rand(1000, 9999));
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Dify API
|
||||
* @param array $config
|
||||
* @param string $message
|
||||
* @param string $session_id
|
||||
* @param string $user_id
|
||||
* @param array $context
|
||||
* @return array
|
||||
*/
|
||||
private function callDifyApi($config, $message, $session_id, $user_id, $context = [])
|
||||
{
|
||||
try {
|
||||
$start_time = microtime(true);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $config['api_key'],
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
];
|
||||
|
||||
$data = [
|
||||
'inputs' => $context,
|
||||
'query' => $message,
|
||||
'response_mode' => $config['response_mode'] ?? 'streaming',
|
||||
'user' => $user_id,
|
||||
'conversation_id' => $session_id
|
||||
];
|
||||
|
||||
$url = rtrim($config['base_url'], '/') . '/v1/chat-messages';
|
||||
|
||||
// 发送请求
|
||||
$result = $this->httpRequest($url, $data, $headers);
|
||||
|
||||
if (!$result['status']) {
|
||||
return ['status' => false, 'message' => 'DIFY_API_ERROR'];
|
||||
}
|
||||
|
||||
$response_time = round((microtime(true) - $start_time) * 1000, 2);
|
||||
|
||||
return [
|
||||
'status' => true,
|
||||
'data' => [
|
||||
'content' => $result['data']['answer'] ?? $result['data']['choices'][0]['message']['content'] ?? '',
|
||||
'tokens' => [
|
||||
'prompt' => $result['data']['usage']['prompt_tokens'] ?? 0,
|
||||
'completion' => $result['data']['usage']['completion_tokens'] ?? 0,
|
||||
'total' => $result['data']['usage']['total_tokens'] ?? 0
|
||||
],
|
||||
'response_time' => $response_time
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('Dify API error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'DIFY_CALL_ERROR'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用RAGFlow API
|
||||
* @param array $config
|
||||
* @param string $message
|
||||
* @param string $session_id
|
||||
* @param string $user_id
|
||||
* @param array $context
|
||||
* @return array
|
||||
*/
|
||||
private function callRagflowApi($config, $message, $session_id, $user_id, $context = [])
|
||||
{
|
||||
try {
|
||||
$start_time = microtime(true);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $config['api_key'],
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
];
|
||||
|
||||
$data = [
|
||||
'query' => $message,
|
||||
'conversation_id' => $session_id,
|
||||
'user_id' => $user_id,
|
||||
'agent_id' => $config['app_id'],
|
||||
'stream' => $config['stream'] ?? false
|
||||
];
|
||||
|
||||
$url = rtrim($config['base_url'], '/') . '/api/v1/chat/completions';
|
||||
|
||||
// 发送请求
|
||||
$result = $this->httpRequest($url, $data, $headers);
|
||||
|
||||
if (!$result['status']) {
|
||||
return ['status' => false, 'message' => 'RAGFLOW_API_ERROR'];
|
||||
}
|
||||
|
||||
$response_time = round((microtime(true) - $start_time) * 1000, 2);
|
||||
|
||||
return [
|
||||
'status' => true,
|
||||
'data' => [
|
||||
'content' => $result['data']['choices'][0]['message']['content'] ?? $result['data']['answer'] ?? '',
|
||||
'tokens' => [
|
||||
'prompt' => $result['data']['usage']['prompt_tokens'] ?? 0,
|
||||
'completion' => $result['data']['usage']['completion_tokens'] ?? 0,
|
||||
'total' => $result['data']['usage']['total_tokens'] ?? 0
|
||||
],
|
||||
'response_time' => $response_time
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('RAGFlow API error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'RAGFLOW_CALL_ERROR'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存聊天记录
|
||||
* @param string $user_id
|
||||
* @param string $session_id
|
||||
* @param string $platform
|
||||
* @param string $user_message
|
||||
* @param string $ai_message
|
||||
*/
|
||||
private function saveChatHistory($user_id, $session_id, $platform, $user_message, $ai_message)
|
||||
{
|
||||
try {
|
||||
$data = [
|
||||
'site_id' => $this->site_id,
|
||||
'user_id' => $user_id,
|
||||
'session_id' => $session_id,
|
||||
'platform' => $platform,
|
||||
'user_message' => $user_message,
|
||||
'ai_message' => $ai_message,
|
||||
'create_time' => time(),
|
||||
'ip' => Request::ip()
|
||||
];
|
||||
|
||||
// 使用事务保存聊天记录
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 使用模型保存聊天记录
|
||||
$save_result = $this->aiChatHistoryModel->saveHistory($data);
|
||||
if (!$save_result['success']) {
|
||||
log_write('Save chat history failed: ' . $save_result['msg'], 'error', $this->log_file);
|
||||
}
|
||||
|
||||
// 更新会话最后活动时间
|
||||
$update_result = $this->aiChatSessionModel->updateLastActiveTime($session_id);
|
||||
if (!$update_result['success']) {
|
||||
log_write('Update session active time failed: ' . $update_result['msg'], 'error', $this->log_file);
|
||||
}
|
||||
|
||||
// 如果会话不存在,创建新会话
|
||||
$session_info = $this->aiChatSessionModel->getSessionInfo(['session_id' => $session_id]);
|
||||
if (!$session_info['success']) {
|
||||
$session_data = [
|
||||
'site_id' => $this->site_id,
|
||||
'user_id' => $user_id,
|
||||
'session_id' => $session_id,
|
||||
'platform' => $platform,
|
||||
'create_time' => time(),
|
||||
'last_active_time' => time()
|
||||
];
|
||||
$create_result = $this->aiChatSessionModel->createSession($session_data);
|
||||
if (!$create_result['success']) {
|
||||
log_write('Create session failed: ' . $create_result['msg'], 'error', $this->log_file);
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
log_write('Save chat history error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
log_write('Save chat history exception: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP请求方法
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
* @param array $headers
|
||||
* @return array
|
||||
*/
|
||||
private function httpRequest($url, $data = [], $headers = [])
|
||||
{
|
||||
try {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
if (!empty($data)) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
}
|
||||
|
||||
if (!empty($headers)) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($http_code != 200) {
|
||||
log_write('HTTP request failed: URL=' . $url . ', Code=' . $http_code . ', Response=' . $response, 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'HTTP_REQUEST_FAILED', 'code' => $http_code];
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
log_write('JSON decode error: ' . json_last_error_msg(), 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'JSON_DECODE_ERROR'];
|
||||
}
|
||||
|
||||
return ['status' => true, 'data' => $result];
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('HTTP request exception: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'HTTP_REQUEST_EXCEPTION'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话历史
|
||||
* @return Json
|
||||
*/
|
||||
public function getHistory()
|
||||
{
|
||||
try {
|
||||
$session_id = $this->params['session_id'] ?? '';
|
||||
$user_id = $this->params['user_id'] ?? $this->member_id;
|
||||
$page = $this->params['page'] ?? 1;
|
||||
$page_size = $this->params['page_size'] ?? 20;
|
||||
|
||||
if (empty($session_id)) {
|
||||
return $this->response($this->error('', 'SESSION_ID_EMPTY'));
|
||||
}
|
||||
|
||||
// 使用模型获取会话历史
|
||||
$where = [
|
||||
'site_id' => $this->site_id,
|
||||
'user_id' => $user_id
|
||||
];
|
||||
|
||||
$result = $this->aiChatHistoryModel->getHistoryBySessionId($session_id, $where, $page, $page_size);
|
||||
|
||||
if (!$result['success']) {
|
||||
log_write('Get history failed: ' . $result['msg'], 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'GET_HISTORY_ERROR'));
|
||||
}
|
||||
|
||||
return $this->response($this->success($result['data']));
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('Get history error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'GET_HISTORY_ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的会话列表
|
||||
* @return Json
|
||||
*/
|
||||
public function getSessions()
|
||||
{
|
||||
try {
|
||||
$user_id = $this->params['user_id'] ?? $this->member_id;
|
||||
$page = $this->params['page'] ?? 1;
|
||||
$page_size = $this->params['page_size'] ?? 20;
|
||||
|
||||
// 使用模型获取会话列表
|
||||
$where = [
|
||||
'site_id' => $this->site_id,
|
||||
'user_id' => $user_id
|
||||
];
|
||||
|
||||
$result = $this->aiChatSessionModel->getSessionList($where, ['*'], 'last_active_time DESC', $page, $page_size);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $this->response($this->error('', 'GET_SESSIONS_ERROR'));
|
||||
}
|
||||
|
||||
return $this->response($this->success($result['data']));
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('Get sessions error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'GET_SESSIONS_ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteSession()
|
||||
{
|
||||
try {
|
||||
$session_id = $this->params['session_id'] ?? '';
|
||||
$user_id = $this->params['user_id'] ?? $this->member_id;
|
||||
|
||||
if (empty($session_id)) {
|
||||
return $this->response($this->error('', 'SESSION_ID_EMPTY'));
|
||||
}
|
||||
|
||||
// 使用模型删除会话
|
||||
$where = [
|
||||
'site_id' => $this->site_id,
|
||||
'session_id' => $session_id,
|
||||
'user_id' => $user_id
|
||||
];
|
||||
|
||||
$result = $this->aiChatSessionModel->deleteSession($where);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $this->response($this->error('', 'DELETE_SESSION_ERROR'));
|
||||
}
|
||||
|
||||
return $this->response($this->success());
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('Delete session exception: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'DELETE_SESSION_EXCEPTION'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use think\facade\Cache;
|
||||
use addon\store\model\Config as StoreConfig;
|
||||
use app\model\store\Store;
|
||||
use think\Response;
|
||||
use app\tracking\VisitorTracker;
|
||||
|
||||
class BaseApi
|
||||
{
|
||||
@@ -97,6 +98,15 @@ class BaseApi
|
||||
}
|
||||
|
||||
$this->store_id = $this->params[ 'store_id' ] ?? 0;
|
||||
|
||||
|
||||
// ------------ 埋点:新增用户访问来源统计数据 ------------
|
||||
$visitorTracker = new VisitorTracker();
|
||||
$visitorTracker->shop_visit(['site_id' => $this->site_id,]);
|
||||
if (!empty($this->params[ 'store_id' ])) {
|
||||
$visitorTracker->store_visit(['site_id' => $this->site_id, 'store_id' => $this->store_id]);
|
||||
}
|
||||
// ------------------------------------------- ------------
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ use app\model\web\DiyView as DiyViewModel;
|
||||
use app\model\shop\Shop as ShopModel;
|
||||
|
||||
use app\model\member\Config as ConfigMemberModel;
|
||||
|
||||
class Config extends BaseApi
|
||||
{
|
||||
|
||||
@@ -107,7 +108,6 @@ class Config extends BaseApi
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
|
||||
$diy_view = new DiyViewModel();
|
||||
$diy_style = $diy_view->getStyleConfig($this->site_id)[ 'data' ][ 'value' ];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user