feat(addon/aikefu): 新增AI智能客服插件
This commit is contained in:
236
src/addon/aikefu/api/controller/Kefu.php
Normal file
236
src/addon/aikefu/api/controller/Kefu.php
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addon\aikefu\api\controller;
|
||||||
|
|
||||||
|
use addon\aikefu\model\Config as KefuConfigModel;
|
||||||
|
use addon\aikefu\model\Conversation as KefuConversationModel;
|
||||||
|
use addon\aikefu\model\Message as KefuMessageModel;
|
||||||
|
use app\api\controller\BaseApi;
|
||||||
|
use extend\api\HttpClient;
|
||||||
|
|
||||||
|
class Kefu extends BaseApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 智能客服聊天接口
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
|
if (empty($message)) {
|
||||||
|
return $this->response($this->error('请输入消息内容'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取智能客服配置
|
||||||
|
$kefu_config_model = new KefuConfigModel();
|
||||||
|
$config_info = $kefu_config_model->getConfig($this->site_id);
|
||||||
|
|
||||||
|
if (empty($config_info['data']['value']) || $config_info['data']['value']['status'] != 1) {
|
||||||
|
return $this->response($this->error('智能客服暂未启用'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $config_info['data']['value'];
|
||||||
|
$apiKey = $config['api_key'];
|
||||||
|
$baseUrl = $config['base_url'];
|
||||||
|
$chatEndpoint = $config['chat_endpoint'];
|
||||||
|
|
||||||
|
// 构建请求数据
|
||||||
|
$requestData = [
|
||||||
|
'inputs' => [],
|
||||||
|
'query' => $message,
|
||||||
|
'response_mode' => $stream ? 'streaming' : 'blocking',
|
||||||
|
'user' => $user_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 如果有会话ID,添加到请求中
|
||||||
|
if (!empty($conversation_id)) {
|
||||||
|
$requestData['conversation_id'] = $conversation_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
$headers = [
|
||||||
|
'Authorization: Bearer ' . $apiKey,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 发送请求到Dify API
|
||||||
|
$url = $baseUrl . $chatEndpoint;
|
||||||
|
$response = HttpClient::http($url, 'POST', json_encode($requestData), $headers);
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
return $this->response($this->error('解析响应失败'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存消息记录
|
||||||
|
$kefu_message_model = new KefuMessageModel();
|
||||||
|
$kefu_conversation_model = new KefuConversationModel();
|
||||||
|
|
||||||
|
// 保存用户消息
|
||||||
|
$kefu_message_model->addMessage([
|
||||||
|
'site_id' => $this->site_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'conversation_id' => $result['conversation_id'] ?? $conversation_id,
|
||||||
|
'message_id' => $result['message_id'] ?? '',
|
||||||
|
'role' => 'user',
|
||||||
|
'content' => $message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 保存机器人回复
|
||||||
|
$kefu_message_model->addMessage([
|
||||||
|
'site_id' => $this->site_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'conversation_id' => $result['conversation_id'] ?? $conversation_id,
|
||||||
|
'message_id' => $result['id'] ?? '',
|
||||||
|
'role' => 'assistant',
|
||||||
|
'content' => $result['answer'] ?? '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 更新会话状态或创建新会话
|
||||||
|
$conversation_info = $kefu_conversation_model->getConversationInfo([
|
||||||
|
['site_id', '=', $this->site_id],
|
||||||
|
['conversation_id', '=', $result['conversation_id'] ?? $conversation_id],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($conversation_info['data'])) {
|
||||||
|
// 创建新会话
|
||||||
|
$kefu_conversation_model->addConversation([
|
||||||
|
'site_id' => $this->site_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'conversation_id' => $result['conversation_id'] ?? '',
|
||||||
|
'name' => '智能客服会话',
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// 更新会话状态
|
||||||
|
$kefu_conversation_model->updateConversation([
|
||||||
|
'status' => 1,
|
||||||
|
], [
|
||||||
|
['id', '=', $conversation_info['data']['id']],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
return $this->response($this->success([
|
||||||
|
'conversation_id' => $result['conversation_id'] ?? '',
|
||||||
|
'reply' => $result['answer'] ?? '',
|
||||||
|
'message_id' => $result['message_id'] ?? '',
|
||||||
|
'finish_reason' => $result['finish_reason'] ?? '',
|
||||||
|
'usage' => $result['usage'] ?? [],
|
||||||
|
]));
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
|
if (empty($conversation_id)) {
|
||||||
|
return $this->response($this->error('会话ID不能为空'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取会话历史记录
|
||||||
|
$kefu_message_model = new KefuMessageModel();
|
||||||
|
$message_list = $kefu_message_model->getMessageList([
|
||||||
|
['site_id', '=', $this->site_id],
|
||||||
|
['user_id', '=', $user_id],
|
||||||
|
['conversation_id', '=', $conversation_id],
|
||||||
|
], 'id, role, content, create_time', 'create_time asc', $limit, $offset);
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
return $this->response($this->success([
|
||||||
|
'messages' => $message_list['data'] ?? [],
|
||||||
|
'total' => $message_list['total'] ?? 0,
|
||||||
|
'limit' => $limit,
|
||||||
|
'offset' => $offset,
|
||||||
|
]));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->response($this->error('请求失败:' . $e->getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新会话
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function createConversation()
|
||||||
|
{
|
||||||
|
// 获取请求参数
|
||||||
|
$user_id = $this->params['user_id'] ?? $this->member_id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取智能客服配置
|
||||||
|
$kefu_config_model = new KefuConfigModel();
|
||||||
|
$config_info = $kefu_config_model->getConfig($this->site_id);
|
||||||
|
|
||||||
|
if (empty($config_info['data']['value']) || $config_info['data']['value']['status'] != 1) {
|
||||||
|
return $this->response($this->error('智能客服暂未启用'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $config_info['data']['value'];
|
||||||
|
$apiKey = $config['api_key'];
|
||||||
|
$baseUrl = $config['base_url'];
|
||||||
|
|
||||||
|
// 构建请求数据
|
||||||
|
$requestData = [
|
||||||
|
'name' => '智能客服会话',
|
||||||
|
'user' => $user_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 构建请求头
|
||||||
|
$headers = [
|
||||||
|
'Authorization: Bearer ' . $apiKey,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 发送请求到Dify API
|
||||||
|
$url = $baseUrl . '/conversations';
|
||||||
|
$response = HttpClient::http($url, 'POST', json_encode($requestData), $headers);
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
return $this->response($this->error('解析响应失败'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存会话记录
|
||||||
|
$kefu_conversation_model = new KefuConversationModel();
|
||||||
|
$kefu_conversation_model->addConversation([
|
||||||
|
'site_id' => $this->site_id,
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'conversation_id' => $result['id'] ?? '',
|
||||||
|
'name' => $result['name'] ?? '智能客服会话',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
return $this->response($this->success([
|
||||||
|
'conversation_id' => $result['id'] ?? '',
|
||||||
|
'name' => $result['name'] ?? '',
|
||||||
|
'created_at' => $result['created_at'] ?? '',
|
||||||
|
]));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->response($this->error('请求失败:' . $e->getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/addon/aikefu/config/info.php
Normal file
15
src/addon/aikefu/config/info.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => 'aikefu',
|
||||||
|
'title' => '智能客服',
|
||||||
|
'description' => '基于Dify的智能客服系统',
|
||||||
|
'author' => 'admin',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'scene' => 'web',
|
||||||
|
'state' => 0,
|
||||||
|
'category' => 'business',
|
||||||
|
'need_install' => 1,
|
||||||
|
'need_cache' => 1,
|
||||||
|
'hooks' => [],
|
||||||
|
];
|
||||||
36
src/addon/aikefu/data/install.sql
Normal file
36
src/addon/aikefu/data/install.sql
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
-- 智能客服插件安装脚本
|
||||||
|
-- 1. 智能客服插件使用系统配置表存储配置信息,无需创建独立数据表
|
||||||
|
-- 2. 会话和消息数据存储在独立数据表中
|
||||||
|
|
||||||
|
-- 创建智能客服会话表
|
||||||
|
CREATE TABLE IF NOT EXISTS `lucky_aikefu_conversation` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`site_id` int(11) NOT NULL COMMENT '站点ID',
|
||||||
|
`user_id` varchar(50) NOT NULL COMMENT '用户ID',
|
||||||
|
`conversation_id` varchar(100) NOT NULL COMMENT 'Dify会话ID',
|
||||||
|
`name` varchar(255) NOT NULL COMMENT '会话名称',
|
||||||
|
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1活跃,0结束',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `site_id` (`site_id`),
|
||||||
|
KEY `user_id` (`user_id`),
|
||||||
|
KEY `conversation_id` (`conversation_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能客服会话表';
|
||||||
|
|
||||||
|
-- 创建智能客服消息表
|
||||||
|
CREATE TABLE IF NOT EXISTS `lucky_aikefu_message` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`site_id` int(11) NOT NULL COMMENT '站点ID',
|
||||||
|
`user_id` varchar(50) NOT NULL COMMENT '用户ID',
|
||||||
|
`conversation_id` varchar(100) NOT NULL COMMENT '会话ID',
|
||||||
|
`message_id` varchar(100) NOT NULL COMMENT '消息ID',
|
||||||
|
`role` varchar(20) NOT NULL COMMENT '角色:user用户,assistant助手',
|
||||||
|
`content` text NOT NULL COMMENT '消息内容',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `site_id` (`site_id`),
|
||||||
|
KEY `user_id` (`user_id`),
|
||||||
|
KEY `conversation_id` (`conversation_id`),
|
||||||
|
KEY `message_id` (`message_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能客服消息表';
|
||||||
4
src/addon/aikefu/data/uninstall.sql
Normal file
4
src/addon/aikefu/data/uninstall.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- 智能客服插件卸载脚本
|
||||||
|
-- 删除智能客服相关表(配置信息存储在系统配置表中,无需单独删除)
|
||||||
|
DROP TABLE IF EXISTS `lucky_aikefu_message`;
|
||||||
|
DROP TABLE IF EXISTS `lucky_aikefu_conversation`;
|
||||||
50
src/addon/aikefu/event/Install.php
Normal file
50
src/addon/aikefu/event/Install.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addon\aikefu\event;
|
||||||
|
|
||||||
|
use app\model\system\Addon as AddonModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能客服插件安装
|
||||||
|
*/
|
||||||
|
class Install
|
||||||
|
{
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$addon_model = new AddonModel();
|
||||||
|
$info = $addon_model->getAddonInfo(['name' => 'aikefu']);
|
||||||
|
|
||||||
|
if (empty($info['data'])) {
|
||||||
|
// 插件未安装,执行安装逻辑
|
||||||
|
$addon_model->addAddon([
|
||||||
|
'name' => 'aikefu',
|
||||||
|
'title' => '智能客服',
|
||||||
|
'description' => '基于Dify的智能客服系统',
|
||||||
|
'author' => 'admin',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'scene' => 'web',
|
||||||
|
'state' => 0,
|
||||||
|
'category' => 'business',
|
||||||
|
'need_install' => 1,
|
||||||
|
'need_cache' => 1,
|
||||||
|
'create_time' => time(),
|
||||||
|
'update_time' => time()
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// 插件已存在,更新插件信息
|
||||||
|
$addon_model->updateAddon([
|
||||||
|
'title' => '智能客服',
|
||||||
|
'description' => '基于Dify的智能客服系统',
|
||||||
|
'author' => 'admin',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'scene' => 'web',
|
||||||
|
'category' => 'business',
|
||||||
|
'need_install' => 1,
|
||||||
|
'need_cache' => 1,
|
||||||
|
'update_time' => time()
|
||||||
|
], ['name' => 'aikefu']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/addon/aikefu/event/Kefu.php
Normal file
129
src/addon/aikefu/event/Kefu.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addon\aikefu\event;
|
||||||
|
|
||||||
|
use addon\aikefu\api\controller\Kefu as KefuApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能客服事件处理类
|
||||||
|
*/
|
||||||
|
class Kefu
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 处理智能客服聊天事件
|
||||||
|
* @param array $data 事件数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function handleKefuChat($data)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 创建addon的KefuApi实例
|
||||||
|
$kefu_api = new KefuApi();
|
||||||
|
|
||||||
|
// 设置必要的属性
|
||||||
|
$kefu_api->site_id = $data['site_id'] ?? 0;
|
||||||
|
$kefu_api->member_id = $data['member_id'] ?? 0;
|
||||||
|
$kefu_api->token = $data['token'] ?? '';
|
||||||
|
$kefu_api->params = [
|
||||||
|
'message' => $data['message'] ?? '',
|
||||||
|
'user_id' => $data['user_id'] ?? '',
|
||||||
|
'conversation_id' => $data['conversation_id'] ?? '',
|
||||||
|
'stream' => $data['stream'] ?? false,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 调用addon的chat方法
|
||||||
|
$response = $kefu_api->chat();
|
||||||
|
|
||||||
|
// 返回响应数据
|
||||||
|
return json_decode($response->getContent(), true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'code' => -1,
|
||||||
|
'message' => '聊天失败:' . $e->getMessage(),
|
||||||
|
'data' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理智能客服创建会话事件
|
||||||
|
* @param array $data 事件数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function handleKefuCreateConversation($data)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 创建addon的KefuApi实例
|
||||||
|
$kefu_api = new KefuApi();
|
||||||
|
|
||||||
|
// 设置必要的属性
|
||||||
|
$kefu_api->site_id = $data['site_id'] ?? 0;
|
||||||
|
$kefu_api->member_id = $data['member_id'] ?? 0;
|
||||||
|
$kefu_api->token = $data['token'] ?? '';
|
||||||
|
$kefu_api->params = [
|
||||||
|
'user_id' => $data['user_id'] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 调用addon的createConversation方法
|
||||||
|
$response = $kefu_api->createConversation();
|
||||||
|
|
||||||
|
// 返回响应数据
|
||||||
|
return json_decode($response->getContent(), true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'code' => -1,
|
||||||
|
'message' => '创建会话失败:' . $e->getMessage(),
|
||||||
|
'data' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理智能客服获取历史消息事件
|
||||||
|
* @param array $data 事件数据
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function handleKefuGetHistory($data)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 创建addon的KefuApi实例
|
||||||
|
$kefu_api = new KefuApi();
|
||||||
|
|
||||||
|
// 设置必要的属性
|
||||||
|
$kefu_api->site_id = $data['site_id'] ?? 0;
|
||||||
|
$kefu_api->member_id = $data['member_id'] ?? 0;
|
||||||
|
$kefu_api->token = $data['token'] ?? '';
|
||||||
|
$kefu_api->params = [
|
||||||
|
'conversation_id' => $data['conversation_id'] ?? '',
|
||||||
|
'user_id' => $data['user_id'] ?? '',
|
||||||
|
'limit' => $data['limit'] ?? 20,
|
||||||
|
'offset' => $data['offset'] ?? 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 调用addon的getHistory方法
|
||||||
|
$response = $kefu_api->getHistory();
|
||||||
|
|
||||||
|
// 返回响应数据
|
||||||
|
return json_decode($response->getContent(), true);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'code' => -1,
|
||||||
|
'message' => '获取历史消息失败:' . $e->getMessage(),
|
||||||
|
'data' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件监听映射
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function subscribe()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'KefuChat' => 'handleKefuChat',
|
||||||
|
'KefuCreateConversation' => 'handleKefuCreateConversation',
|
||||||
|
'KefuGetHistory' => 'handleKefuGetHistory',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/addon/aikefu/event/UnInstall.php
Normal file
20
src/addon/aikefu/event/UnInstall.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addon\aikefu\event;
|
||||||
|
|
||||||
|
use app\model\system\Addon as AddonModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能客服插件卸载
|
||||||
|
*/
|
||||||
|
class UnInstall
|
||||||
|
{
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$addon_model = new AddonModel();
|
||||||
|
// 删除插件信息
|
||||||
|
$addon_model->deleteAddon(['name' => 'aikefu']);
|
||||||
|
|
||||||
|
return success(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/addon/aikefu/icon.png
Normal file
1
src/addon/aikefu/icon.png
Normal file
File diff suppressed because one or more lines are too long
53
src/addon/aikefu/model/Config.php
Normal file
53
src/addon/aikefu/model/Config.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addon\aikefu\model;
|
||||||
|
|
||||||
|
use app\model\system\Config as ConfigModel;
|
||||||
|
use app\model\BaseModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能客服配置
|
||||||
|
*/
|
||||||
|
class Config extends BaseModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 设置智能客服配置
|
||||||
|
* @param array $data
|
||||||
|
* @param int $site_id
|
||||||
|
* @param string $app_module
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function setConfig($data, $site_id = 0, $app_module = 'shop')
|
||||||
|
{
|
||||||
|
$config = new ConfigModel();
|
||||||
|
$res = $config->setConfig($data, '智能客服配置', 1, [['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'AIKEFU_CONFIG']]);
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能客服配置
|
||||||
|
* @param int $site_id
|
||||||
|
* @param string $app_module
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getConfig($site_id = 0, $app_module = 'shop')
|
||||||
|
{
|
||||||
|
$config = new ConfigModel();
|
||||||
|
$res = $config->getConfig([['site_id', '=', $site_id], ['app_module', '=', $app_module], ['config_key', '=', 'AIKEFU_CONFIG']]);
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能客服配置信息
|
||||||
|
* @param array $condition
|
||||||
|
* @param string $field
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getConfigInfo($condition = [], $field = '*')
|
||||||
|
{
|
||||||
|
// 兼容旧的调用方式
|
||||||
|
$site_id = $condition[0][1] ?? 0;
|
||||||
|
$res = $this->getConfig($site_id);
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/addon/aikefu/model/Conversation.php
Normal file
102
src/addon/aikefu/model/Conversation.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addon\aikefu\model;
|
||||||
|
|
||||||
|
use app\model\BaseModel;
|
||||||
|
|
||||||
|
class Conversation extends BaseModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 表名
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $name = 'aikefu_conversation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话信息
|
||||||
|
* @param array $condition
|
||||||
|
* @param string $field
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getConversationInfo($condition = [], $field = '*')
|
||||||
|
{
|
||||||
|
$info = $this->where($condition)->field($field)->find();
|
||||||
|
return empty($info) ? [] : $info->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话列表
|
||||||
|
* @param array $condition
|
||||||
|
* @param string $field
|
||||||
|
* @param string $order
|
||||||
|
* @param int $page
|
||||||
|
* @param int $limit
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getConversationList($condition = [], $field = '*', $order = 'id desc', $page = 1, $limit = 10)
|
||||||
|
{
|
||||||
|
$list = $this->where($condition)->field($field)->order($order)->paginate([
|
||||||
|
'page' => $page,
|
||||||
|
'list_rows' => $limit
|
||||||
|
]);
|
||||||
|
return $this->pageFormat($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加会话
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function addConversation($data)
|
||||||
|
{
|
||||||
|
$result = $this->insert($data);
|
||||||
|
return $this->success(['result' => $result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新会话
|
||||||
|
* @param array $data
|
||||||
|
* @param array $condition
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function updateConversation($data, $condition)
|
||||||
|
{
|
||||||
|
$result = $this->where($condition)->update($data);
|
||||||
|
return $this->success(['result' => $result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话
|
||||||
|
* @param array $condition
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function deleteConversation($condition)
|
||||||
|
{
|
||||||
|
$result = $this->where($condition)->delete();
|
||||||
|
return $this->success(['result' => $result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户会话列表
|
||||||
|
* @param int $site_id
|
||||||
|
* @param string $user_id
|
||||||
|
* @param int $page
|
||||||
|
* @param int $limit
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getUserConversationList($site_id, $user_id, $page = 1, $limit = 10)
|
||||||
|
{
|
||||||
|
$condition = [
|
||||||
|
['site_id', '=', $site_id],
|
||||||
|
['user_id', '=', $user_id]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->getConversationList($condition, '*', 'update_time desc', $page, $limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/addon/aikefu/model/Message.php
Normal file
118
src/addon/aikefu/model/Message.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addon\aikefu\model;
|
||||||
|
|
||||||
|
use app\model\BaseModel;
|
||||||
|
|
||||||
|
class Message extends BaseModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 表名
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $name = 'aikefu_message';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息信息
|
||||||
|
* @param array $condition
|
||||||
|
* @param string $field
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getMessageInfo($condition = [], $field = '*')
|
||||||
|
{
|
||||||
|
$info = $this->where($condition)->field($field)->find();
|
||||||
|
return empty($info) ? [] : $info->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息列表
|
||||||
|
* @param array $condition
|
||||||
|
* @param string $field
|
||||||
|
* @param string $order
|
||||||
|
* @param int $page
|
||||||
|
* @param int $limit
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getMessageList($condition = [], $field = '*', $order = 'id asc', $page = 1, $limit = 20)
|
||||||
|
{
|
||||||
|
$list = $this->where($condition)->field($field)->order($order)->paginate([
|
||||||
|
'page' => $page,
|
||||||
|
'list_rows' => $limit
|
||||||
|
]);
|
||||||
|
return $this->pageFormat($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加消息
|
||||||
|
* @param array $data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function addMessage($data)
|
||||||
|
{
|
||||||
|
$result = $this->insert($data);
|
||||||
|
return $this->success(['result' => $result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新消息
|
||||||
|
* @param array $data
|
||||||
|
* @param array $condition
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function updateMessage($data, $condition)
|
||||||
|
{
|
||||||
|
$result = $this->where($condition)->update($data);
|
||||||
|
return $this->success(['result' => $result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息
|
||||||
|
* @param array $condition
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function deleteMessage($condition)
|
||||||
|
{
|
||||||
|
$result = $this->where($condition)->delete();
|
||||||
|
return $this->success(['result' => $result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话消息记录
|
||||||
|
* @param int $site_id
|
||||||
|
* @param string $conversation_id
|
||||||
|
* @param int $limit
|
||||||
|
* @param int $offset
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getConversationMessages($site_id, $conversation_id, $limit = 20, $offset = 0)
|
||||||
|
{
|
||||||
|
$condition = [
|
||||||
|
['site_id', '=', $site_id],
|
||||||
|
['conversation_id', '=', $conversation_id]
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->getMessageList($condition, '*', 'create_time asc', ($offset / $limit) + 1, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户消息总数
|
||||||
|
* @param int $site_id
|
||||||
|
* @param string $user_id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getUserMessageCount($site_id, $user_id)
|
||||||
|
{
|
||||||
|
$count = $this->where([
|
||||||
|
['site_id', '=', $site_id],
|
||||||
|
['user_id', '=', $user_id]
|
||||||
|
])->count();
|
||||||
|
|
||||||
|
return $this->success(['count' => $count]);
|
||||||
|
}
|
||||||
|
}
|
||||||
222
src/addon/aikefu/shop/controller/Kefu.php
Normal file
222
src/addon/aikefu/shop/controller/Kefu.php
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace addon\aikefu\shop\controller;
|
||||||
|
|
||||||
|
use addon\aikefu\model\Config as KefuConfigModel;
|
||||||
|
use addon\aikefu\model\Conversation as KefuConversationModel;
|
||||||
|
use addon\aikefu\model\Message as KefuMessageModel;
|
||||||
|
use app\shop\controller\BaseShop;
|
||||||
|
use think\facade\View;
|
||||||
|
|
||||||
|
class Kefu extends BaseShop
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 智能客服配置页
|
||||||
|
* @return \think\response\View
|
||||||
|
*/
|
||||||
|
public function config()
|
||||||
|
{
|
||||||
|
$kefu_config_model = new KefuConfigModel();
|
||||||
|
$config_info = $kefu_config_model->getConfigInfo([['site_id', '=', $this->site_id]]);
|
||||||
|
|
||||||
|
View::assign('config_info', $config_info);
|
||||||
|
return View::fetch('kefu/config');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存智能客服配置
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function saveConfig()
|
||||||
|
{
|
||||||
|
$params = $this->request->post();
|
||||||
|
|
||||||
|
$kefu_config_model = new KefuConfigModel();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'api_key' => $params['api_key'] ?? '',
|
||||||
|
'base_url' => $params['base_url'] ?? 'https://api.dify.ai/v1',
|
||||||
|
'chat_endpoint' => $params['chat_endpoint'] ?? '/chat-messages',
|
||||||
|
'status' => $params['status'] ?? 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = $kefu_config_model->setConfig($data, $this->site_id);
|
||||||
|
|
||||||
|
return $this->success($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话管理列表
|
||||||
|
* @return \think\response\View
|
||||||
|
*/
|
||||||
|
public function conversation()
|
||||||
|
{
|
||||||
|
return View::fetch('kefu/conversation');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话列表
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getConversationList()
|
||||||
|
{
|
||||||
|
$params = $this->request->post();
|
||||||
|
$page = $params['page'] ?? 1;
|
||||||
|
$limit = $params['limit'] ?? 10;
|
||||||
|
$user_id = $params['user_id'] ?? '';
|
||||||
|
$status = $params['status'] ?? '';
|
||||||
|
|
||||||
|
$kefu_conversation_model = new KefuConversationModel();
|
||||||
|
$condition = [['site_id', '=', $this->site_id]];
|
||||||
|
|
||||||
|
if (!empty($user_id)) {
|
||||||
|
$condition[] = ['user_id', '=', $user_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status !== '') {
|
||||||
|
$condition[] = ['status', '=', $status];
|
||||||
|
}
|
||||||
|
|
||||||
|
$conversation_list = $kefu_conversation_model->getConversationList($condition, '*', 'update_time desc', $page, $limit);
|
||||||
|
|
||||||
|
return $this->success($conversation_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话信息
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getConversationInfo()
|
||||||
|
{
|
||||||
|
$params = $this->request->post();
|
||||||
|
$conversation_id = $params['conversation_id'] ?? '';
|
||||||
|
|
||||||
|
if (empty($conversation_id)) {
|
||||||
|
return $this->error('会话ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kefu_conversation_model = new KefuConversationModel();
|
||||||
|
$conversation_info = $kefu_conversation_model->getConversationInfo([
|
||||||
|
['site_id', '=', $this->site_id],
|
||||||
|
['conversation_id', '=', $conversation_id]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($conversation_info)) {
|
||||||
|
return $this->error('会话不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($conversation_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束会话
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function endConversation()
|
||||||
|
{
|
||||||
|
$params = $this->request->post();
|
||||||
|
$id = $params['id'] ?? '';
|
||||||
|
|
||||||
|
if (empty($id)) {
|
||||||
|
return $this->error('会话ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kefu_conversation_model = new KefuConversationModel();
|
||||||
|
$result = $kefu_conversation_model->updateConversation(
|
||||||
|
['status' => 0],
|
||||||
|
[
|
||||||
|
['id', '=', $id],
|
||||||
|
['site_id', '=', $this->site_id]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->success($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function deleteConversation()
|
||||||
|
{
|
||||||
|
$params = $this->request->post();
|
||||||
|
$id = $params['id'] ?? '';
|
||||||
|
|
||||||
|
if (empty($id)) {
|
||||||
|
return $this->error('会话ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kefu_conversation_model = new KefuConversationModel();
|
||||||
|
$kefu_message_model = new KefuMessageModel();
|
||||||
|
|
||||||
|
// 开启事务
|
||||||
|
$this->model->startTrans();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 删除会话关联的消息
|
||||||
|
$conversation_info = $kefu_conversation_model->getConversationInfo([
|
||||||
|
['id', '=', $id],
|
||||||
|
['site_id', '=', $this->site_id]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($conversation_info)) {
|
||||||
|
$kefu_message_model->deleteMessage([
|
||||||
|
['site_id', '=', $this->site_id],
|
||||||
|
['conversation_id', '=', $conversation_info['conversation_id']]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除会话
|
||||||
|
$result = $kefu_conversation_model->deleteConversation([
|
||||||
|
['id', '=', $id],
|
||||||
|
['site_id', '=', $this->site_id]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
$this->model->commit();
|
||||||
|
|
||||||
|
return $this->success($result);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 回滚事务
|
||||||
|
$this->model->rollback();
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息管理列表
|
||||||
|
* @return \think\response\View
|
||||||
|
*/
|
||||||
|
public function message()
|
||||||
|
{
|
||||||
|
$conversation_id = $this->request->param('conversation_id') ?? '';
|
||||||
|
View::assign('conversation_id', $conversation_id);
|
||||||
|
return View::fetch('kefu/message');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息列表
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function getMessageList()
|
||||||
|
{
|
||||||
|
$params = $this->request->post();
|
||||||
|
$page = $params['page'] ?? 1;
|
||||||
|
$limit = $params['limit'] ?? 50;
|
||||||
|
$conversation_id = $params['conversation_id'] ?? '';
|
||||||
|
|
||||||
|
if (empty($conversation_id)) {
|
||||||
|
return $this->error('会话ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$kefu_message_model = new KefuMessageModel();
|
||||||
|
$condition = [
|
||||||
|
['site_id', '=', $this->site_id],
|
||||||
|
['conversation_id', '=', $conversation_id]
|
||||||
|
];
|
||||||
|
|
||||||
|
$message_list = $kefu_message_model->getMessageList($condition, '*', 'create_time asc', $page, $limit);
|
||||||
|
|
||||||
|
return $this->success($message_list);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/addon/aikefu/shop/view/kefu/config.html
Normal file
98
src/addon/aikefu/shop/view/kefu/config.html
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>智能客服配置</title>
|
||||||
|
<link rel="stylesheet" href="/__STATIC__/admin/css/global.css">
|
||||||
|
<link rel="stylesheet" href="/__STATIC__/admin/css/form.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-title">
|
||||||
|
<h2>智能客服配置</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<form id="kefuConfigForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="api_key">Dify API密钥</label>
|
||||||
|
<input type="text" name="api_key" id="api_key" value="{$config_info.api_key ?? ''}" placeholder="请输入Dify API密钥">
|
||||||
|
<div class="form-tips">
|
||||||
|
从Dify平台获取的API密钥,用于调用Dify聊天机器人API。
|
||||||
|
<a href="https://dify.ai/" target="_blank">前往Dify平台</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="base_url">API基础地址</label>
|
||||||
|
<input type="text" name="base_url" id="base_url" value="{$config_info.base_url ?? 'https://api.dify.ai/v1'}" placeholder="请输入Dify API基础地址">
|
||||||
|
<div class="form-tips">
|
||||||
|
Dify API的基础地址,默认为https://api.dify.ai/v1
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="chat_endpoint">聊天接口端点</label>
|
||||||
|
<input type="text" name="chat_endpoint" id="chat_endpoint" value="{$config_info.chat_endpoint ?? '/chat-messages'}" placeholder="请输入聊天接口端点">
|
||||||
|
<div class="form-tips">
|
||||||
|
聊天接口的端点,默认为/chat-messages
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="status">状态</label>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label class="radio-item">
|
||||||
|
<input type="radio" name="status" value="1" {if isset($config_info.status) && $config_info.status == 1}checked{/if}>
|
||||||
|
<span>启用</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-item">
|
||||||
|
<input type="radio" name="status" value="0" {if !isset($config_info.status) || $config_info.status == 0}checked{/if}>
|
||||||
|
<span>禁用</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-tips">
|
||||||
|
启用或禁用智能客服功能
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">保存配置</button>
|
||||||
|
<button type="reset" class="btn btn-default">重置</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/__STATIC__/admin/js/jquery.min.js"></script>
|
||||||
|
<script src="/__STATIC__/admin/js/layer/layer.js"></script>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
$('#kefuConfigForm').on('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var formData = $(this).serialize();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/shop/aikefu/kefu/saveConfig',
|
||||||
|
type: 'POST',
|
||||||
|
data: formData,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
if (res.code === 0) {
|
||||||
|
layer.msg('保存成功', {icon: 1});
|
||||||
|
} else {
|
||||||
|
layer.msg(res.message, {icon: 2});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
245
src/addon/aikefu/shop/view/kefu/conversation.html
Normal file
245
src/addon/aikefu/shop/view/kefu/conversation.html
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>会话管理</title>
|
||||||
|
<link rel="stylesheet" href="/__STATIC__/admin/css/global.css">
|
||||||
|
<link rel="stylesheet" href="/__STATIC__/admin/css/table.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-title">
|
||||||
|
<h2>会话管理</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="search-box">
|
||||||
|
<div class="search-item">
|
||||||
|
<label for="user_id">用户ID</label>
|
||||||
|
<input type="text" name="user_id" id="user_id" placeholder="请输入用户ID">
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<label for="status">状态</label>
|
||||||
|
<select name="status" id="status">
|
||||||
|
<option value="">全部</option>
|
||||||
|
<option value="1">活跃</option>
|
||||||
|
<option value="0">已结束</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<button type="button" class="btn btn-primary" id="searchBtn">搜索</button>
|
||||||
|
<button type="button" class="btn btn-default" id="resetBtn">重置</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>会话ID</th>
|
||||||
|
<th>用户ID</th>
|
||||||
|
<th>会话名称</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>更新时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="conversationList">
|
||||||
|
<!-- 会话列表将通过JavaScript动态加载 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pagination" id="pagination">
|
||||||
|
<!-- 分页将通过JavaScript动态加载 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/__STATIC__/admin/js/jquery.min.js"></script>
|
||||||
|
<script src="/__STATIC__/admin/js/layer/layer.js"></script>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
// 分页参数
|
||||||
|
var page = 1;
|
||||||
|
var limit = 10;
|
||||||
|
var total = 0;
|
||||||
|
|
||||||
|
// 加载会话列表
|
||||||
|
function loadConversationList() {
|
||||||
|
var user_id = $('#user_id').val();
|
||||||
|
var status = $('#status').val();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/shop/aikefu/kefu/getConversationList',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
user_id: user_id,
|
||||||
|
status: status
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
if (res.code === 0) {
|
||||||
|
var list = res.data.list;
|
||||||
|
total = res.data.total;
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
if (list.length > 0) {
|
||||||
|
list.forEach(function(item) {
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td>' + item.id + '</td>';
|
||||||
|
html += '<td>' + item.conversation_id + '</td>';
|
||||||
|
html += '<td>' + item.user_id + '</td>';
|
||||||
|
html += '<td>' + item.name + '</td>';
|
||||||
|
html += '<td>' + (item.status === 1 ? '<span class="status-active">活跃</span>' : '<span class="status-inactive">已结束</span>') + '</td>';
|
||||||
|
html += '<td>' + item.create_time + '</td>';
|
||||||
|
html += '<td>' + item.update_time + '</td>';
|
||||||
|
html += '<td>';
|
||||||
|
html += '<a href="javascript:;" class="btn btn-small btn-primary" onclick="viewMessages(' + item.id + ', \'' + item.conversation_id + '\')">查看消息</a>';
|
||||||
|
html += '<a href="javascript:;" class="btn btn-small btn-warning" onclick="endConversation(' + item.id + ')">结束会话</a>';
|
||||||
|
html += '<a href="javascript:;" class="btn btn-small btn-danger" onclick="deleteConversation(' + item.id + ')">删除</a>';
|
||||||
|
html += '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html += '<tr><td colspan="8" class="text-center">暂无会话数据</td></tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#conversationList').html(html);
|
||||||
|
// 渲染分页
|
||||||
|
renderPagination();
|
||||||
|
} else {
|
||||||
|
layer.msg('加载失败:' + res.message, {icon: 2});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染分页
|
||||||
|
function renderPagination() {
|
||||||
|
var pages = Math.ceil(total / limit);
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (pages > 1) {
|
||||||
|
html += '<ul class="pagination-list">';
|
||||||
|
|
||||||
|
// 上一页
|
||||||
|
if (page > 1) {
|
||||||
|
html += '<li><a href="javascript:;" onclick="changePage(' + (page - 1) + ')">上一页</a></li>';
|
||||||
|
} else {
|
||||||
|
html += '<li class="disabled"><a href="javascript:;">上一页</a></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码
|
||||||
|
for (var i = 1; i <= pages; i++) {
|
||||||
|
if (i === page) {
|
||||||
|
html += '<li class="active"><a href="javascript:;">' + i + '</a></li>';
|
||||||
|
} else {
|
||||||
|
html += '<li><a href="javascript:;" onclick="changePage(' + i + ')">' + i + '</a></li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一页
|
||||||
|
if (page < pages) {
|
||||||
|
html += '<li><a href="javascript:;" onclick="changePage(' + (page + 1) + ')">下一页</a></li>';
|
||||||
|
} else {
|
||||||
|
html += '<li class="disabled"><a href="javascript:;">下一页</a></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#pagination').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换页码
|
||||||
|
window.changePage = function(p) {
|
||||||
|
page = p;
|
||||||
|
loadConversationList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看消息
|
||||||
|
window.viewMessages = function(id, conversation_id) {
|
||||||
|
layer.open({
|
||||||
|
type: 2,
|
||||||
|
title: '消息记录',
|
||||||
|
content: '/shop/aikefu/kefu/message?conversation_id=' + conversation_id,
|
||||||
|
area: ['90%', '90%']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束会话
|
||||||
|
window.endConversation = function(id) {
|
||||||
|
layer.confirm('确定要结束该会话吗?', function(index) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/shop/aikefu/kefu/endConversation',
|
||||||
|
type: 'POST',
|
||||||
|
data: {id: id},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
if (res.code === 0) {
|
||||||
|
layer.msg('会话已结束', {icon: 1});
|
||||||
|
loadConversationList();
|
||||||
|
} else {
|
||||||
|
layer.msg('操作失败:' + res.message, {icon: 2});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layer.close(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除会话
|
||||||
|
window.deleteConversation = function(id) {
|
||||||
|
layer.confirm('确定要删除该会话吗?删除后将无法恢复', function(index) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/shop/aikefu/kefu/deleteConversation',
|
||||||
|
type: 'POST',
|
||||||
|
data: {id: id},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
if (res.code === 0) {
|
||||||
|
layer.msg('会话已删除', {icon: 1});
|
||||||
|
loadConversationList();
|
||||||
|
} else {
|
||||||
|
layer.msg('操作失败:' + res.message, {icon: 2});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
layer.close(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
$('#searchBtn').click(function() {
|
||||||
|
page = 1;
|
||||||
|
loadConversationList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
$('#resetBtn').click(function() {
|
||||||
|
$('#user_id').val('');
|
||||||
|
$('#status').val('');
|
||||||
|
page = 1;
|
||||||
|
loadConversationList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
loadConversationList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
276
src/addon/aikefu/shop/view/kefu/message.html
Normal file
276
src/addon/aikefu/shop/view/kefu/message.html
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>消息管理</title>
|
||||||
|
<link rel="stylesheet" href="/__STATIC__/admin/css/global.css">
|
||||||
|
<link rel="stylesheet" href="/__STATIC__/admin/css/table.css">
|
||||||
|
<style>
|
||||||
|
.message-list {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.message-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.message-item.user {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.message-item.assistant {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.message-avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
.message-content {
|
||||||
|
max-width: 70%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.message-item.user .message-content {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: white;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.message-item.assistant .message-content {
|
||||||
|
background-color: white;
|
||||||
|
color: #333;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.message-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 5px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.message-role {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.conversation-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.conversation-info h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.conversation-info p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="main">
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-title">
|
||||||
|
<h2>消息管理</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div id="conversationInfo" class="conversation-info">
|
||||||
|
<!-- 会话信息将通过JavaScript动态加载 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-box">
|
||||||
|
<div class="search-item">
|
||||||
|
<label for="conversation_id">会话ID</label>
|
||||||
|
<input type="text" name="conversation_id" id="conversation_id" placeholder="请输入会话ID" value="{$conversation_id ?? ''}">
|
||||||
|
</div>
|
||||||
|
<div class="search-item">
|
||||||
|
<button type="button" class="btn btn-primary" id="searchBtn">搜索</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-list" id="messageList">
|
||||||
|
<!-- 消息列表将通过JavaScript动态加载 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pagination" id="pagination">
|
||||||
|
<!-- 分页将通过JavaScript动态加载 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/__STATIC__/admin/js/jquery.min.js"></script>
|
||||||
|
<script src="/__STATIC__/admin/js/layer/layer.js"></script>
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
// 分页参数
|
||||||
|
var page = 1;
|
||||||
|
var limit = 50;
|
||||||
|
var total = 0;
|
||||||
|
var conversation_id = $('#conversation_id').val();
|
||||||
|
|
||||||
|
// 加载会话信息
|
||||||
|
function loadConversationInfo() {
|
||||||
|
if (!conversation_id) return;
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/shop/aikefu/kefu/getConversationInfo',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
conversation_id: conversation_id
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
if (res.code === 0) {
|
||||||
|
var info = res.data;
|
||||||
|
var html = '<h3>会话详情</h3>';
|
||||||
|
html += '<p><strong>会话ID:</strong>' + info.conversation_id + '</p>';
|
||||||
|
html += '<p><strong>用户ID:</strong>' + info.user_id + '</p>';
|
||||||
|
html += '<p><strong>会话名称:</strong>' + info.name + '</p>';
|
||||||
|
html += '<p><strong>状态:</strong>' + (info.status === 1 ? '<span class="status-active">活跃</span>' : '<span class="status-inactive">已结束</span>') + '</p>';
|
||||||
|
html += '<p><strong>创建时间:</strong>' + info.create_time + '</p>';
|
||||||
|
html += '<p><strong>更新时间:</strong>' + info.update_time + '</p>';
|
||||||
|
$('#conversationInfo').html(html);
|
||||||
|
} else {
|
||||||
|
$('#conversationInfo').html('<h3>会话详情</h3><p>未找到会话信息</p>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('#conversationInfo').html('<h3>会话详情</h3><p>加载会话信息失败</p>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载消息列表
|
||||||
|
function loadMessageList() {
|
||||||
|
if (!conversation_id) {
|
||||||
|
$('#messageList').html('<div style="text-align: center; padding: 50px;">请输入会话ID进行搜索</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/shop/aikefu/kefu/getMessageList',
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
page: page,
|
||||||
|
limit: limit,
|
||||||
|
conversation_id: conversation_id
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
if (res.code === 0) {
|
||||||
|
var list = res.data.list;
|
||||||
|
total = res.data.total;
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
if (list.length > 0) {
|
||||||
|
list.forEach(function(item) {
|
||||||
|
var role = item.role === 'user' ? '用户' : '机器人';
|
||||||
|
var roleClass = item.role === 'user' ? 'user' : 'assistant';
|
||||||
|
var avatar = item.role === 'user' ? '/__STATIC__/admin/img/user.png' : '/__STATIC__/admin/img/robot.png';
|
||||||
|
|
||||||
|
html += '<div class="message-item ' + roleClass + '">';
|
||||||
|
if (item.role === 'assistant') {
|
||||||
|
html += '<img src="' + avatar + '" class="message-avatar">';
|
||||||
|
}
|
||||||
|
html += '<div>';
|
||||||
|
html += '<div class="message-role">' + role + '</div>';
|
||||||
|
html += '<div class="message-content">' + item.content + '</div>';
|
||||||
|
html += '<div class="message-time">' + item.create_time + '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
if (item.role === 'user') {
|
||||||
|
html += '<img src="' + avatar + '" class="message-avatar">';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html += '<div style="text-align: center; padding: 50px;">暂无消息记录</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#messageList').html(html);
|
||||||
|
// 滚动到底部
|
||||||
|
$('#messageList').scrollTop($('#messageList')[0].scrollHeight);
|
||||||
|
// 渲染分页
|
||||||
|
renderPagination();
|
||||||
|
} else {
|
||||||
|
layer.msg('加载失败:' + res.message, {icon: 2});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
layer.msg('请求失败,请稍后重试', {icon: 2});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染分页
|
||||||
|
function renderPagination() {
|
||||||
|
var pages = Math.ceil(total / limit);
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (pages > 1) {
|
||||||
|
html += '<ul class="pagination-list">';
|
||||||
|
|
||||||
|
// 上一页
|
||||||
|
if (page > 1) {
|
||||||
|
html += '<li><a href="javascript:;" onclick="changePage(' + (page - 1) + ')">上一页</a></li>';
|
||||||
|
} else {
|
||||||
|
html += '<li class="disabled"><a href="javascript:;">上一页</a></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码
|
||||||
|
for (var i = 1; i <= pages; i++) {
|
||||||
|
if (i === page) {
|
||||||
|
html += '<li class="active"><a href="javascript:;">' + i + '</a></li>';
|
||||||
|
} else {
|
||||||
|
html += '<li><a href="javascript:;" onclick="changePage(' + i + ')">' + i + '</a></li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一页
|
||||||
|
if (page < pages) {
|
||||||
|
html += '<li><a href="javascript:;" onclick="changePage(' + (page + 1) + ')">下一页</a></li>';
|
||||||
|
} else {
|
||||||
|
html += '<li class="disabled"><a href="javascript:;">下一页</a></li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#pagination').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换页码
|
||||||
|
window.changePage = function(p) {
|
||||||
|
page = p;
|
||||||
|
loadMessageList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
$('#searchBtn').click(function() {
|
||||||
|
conversation_id = $('#conversation_id').val();
|
||||||
|
page = 1;
|
||||||
|
loadConversationInfo();
|
||||||
|
loadMessageList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
if (conversation_id) {
|
||||||
|
loadConversationInfo();
|
||||||
|
loadMessageList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
172
src/app/api/controller/Kefu.php
Normal file
172
src/app/api/controller/Kefu.php
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use app\api\controller\BaseApi;
|
||||||
|
use think\facade\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能客服API控制器
|
||||||
|
*/
|
||||||
|
class Kefu extends BaseApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 智能客服聊天接口
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
|
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' => $this->site_id,
|
||||||
|
'member_id' => $this->member_id,
|
||||||
|
'token' => $this->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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新会话
|
||||||
|
* @return \think\response\Json
|
||||||
|
*/
|
||||||
|
public function createConversation()
|
||||||
|
{
|
||||||
|
// 获取请求参数
|
||||||
|
$user_id = $this->params['user_id'] ?? $this->member_id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 准备事件数据
|
||||||
|
$event_data = [
|
||||||
|
'user_id' => $user_id,
|
||||||
|
'site_id' => $this->site_id,
|
||||||
|
'member_id' => $this->member_id,
|
||||||
|
'token' => $this->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;
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
|
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' => $this->site_id,
|
||||||
|
'member_id' => $this->member_id,
|
||||||
|
'token' => $this->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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
266
src/docs/kefu_api.md
Normal file
266
src/docs/kefu_api.md
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
# 智能客服API接口文档
|
||||||
|
|
||||||
|
## 一、接口说明
|
||||||
|
|
||||||
|
本接口用于连接微信小程序与Dify聊天机器人,实现智能客服功能。
|
||||||
|
|
||||||
|
## 二、配置说明
|
||||||
|
|
||||||
|
### 1. 安装插件
|
||||||
|
|
||||||
|
在ThinkPHP后台的插件管理页面中,找到智能客服插件(aikefu)并点击安装按钮。
|
||||||
|
|
||||||
|
### 2. 配置插件
|
||||||
|
|
||||||
|
1. 进入智能客服配置页面
|
||||||
|
2. 输入从Dify平台获取的API密钥
|
||||||
|
3. 配置API基础地址(默认:https://api.dify.ai/v1)
|
||||||
|
4. 配置聊天接口端点(默认:/chat-messages)
|
||||||
|
5. 启用智能客服功能
|
||||||
|
|
||||||
|
### 3. 获取Dify API密钥
|
||||||
|
|
||||||
|
1. 登录Dify平台
|
||||||
|
2. 进入工作台
|
||||||
|
3. 选择您的聊天机器人项目
|
||||||
|
4. 点击"发布"按钮
|
||||||
|
5. 在API访问页面获取API密钥
|
||||||
|
|
||||||
|
## 三、接口列表
|
||||||
|
|
||||||
|
### 1. 智能客服聊天接口
|
||||||
|
|
||||||
|
**接口地址**:`/api/kefu/chat`
|
||||||
|
|
||||||
|
**请求方式**:POST
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| message | string | 是 | 用户输入的消息内容 |
|
||||||
|
| user_id | string | 否 | 用户ID,默认使用当前登录会员ID |
|
||||||
|
| conversation_id | string | 否 | 会话ID,第一次聊天可不传,系统会自动创建 |
|
||||||
|
| stream | bool | 否 | 是否使用流式响应,默认false |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"conversation_id": "conv_123456789",
|
||||||
|
"reply": "您好,我是智能客服,有什么可以帮助您的?",
|
||||||
|
"message_id": "msg_123456789",
|
||||||
|
"finish_reason": "stop",
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 10,
|
||||||
|
"completion_tokens": 20,
|
||||||
|
"total_tokens": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"timestamp": 1640995200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 获取会话历史
|
||||||
|
|
||||||
|
**接口地址**:`/api/kefu/getHistory`
|
||||||
|
|
||||||
|
**请求方式**:POST
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| conversation_id | string | 是 | 会话ID |
|
||||||
|
| user_id | string | 否 | 用户ID,默认使用当前登录会员ID |
|
||||||
|
| limit | int | 否 | 每页条数,默认20 |
|
||||||
|
| offset | int | 否 | 偏移量,默认0 |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"id": "msg_123456789",
|
||||||
|
"role": "user",
|
||||||
|
"content": "你好",
|
||||||
|
"created_at": "2023-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "msg_987654321",
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "您好,我是智能客服,有什么可以帮助您的?",
|
||||||
|
"created_at": "2023-01-01T00:00:01Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 2,
|
||||||
|
"limit": 20,
|
||||||
|
"offset": 0
|
||||||
|
},
|
||||||
|
"timestamp": 1640995200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 创建新会话
|
||||||
|
|
||||||
|
**接口地址**:`/api/kefu/createConversation`
|
||||||
|
|
||||||
|
**请求方式**:POST
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
|
||||||
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| user_id | string | 否 | 用户ID,默认使用当前登录会员ID |
|
||||||
|
|
||||||
|
**响应示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"conversation_id": "conv_123456789",
|
||||||
|
"name": "智能客服会话",
|
||||||
|
"created_at": "2023-01-01T00:00:00Z"
|
||||||
|
},
|
||||||
|
"timestamp": 1640995200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、前端调用示例
|
||||||
|
|
||||||
|
### Uniapp调用示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 引入请求封装(根据项目实际情况调整)
|
||||||
|
import { request } from '@/utils/request';
|
||||||
|
|
||||||
|
// 智能客服聊天
|
||||||
|
async function chatWithAI(message, conversationId = '') {
|
||||||
|
try {
|
||||||
|
const res = await request({
|
||||||
|
url: '/api/kefu/chat',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
message: message,
|
||||||
|
conversation_id: conversationId,
|
||||||
|
// user_id: 'your-user-id', // 可选
|
||||||
|
// stream: false // 可选
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
console.error('聊天失败:', res.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('聊天请求失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话历史
|
||||||
|
async function getChatHistory(conversationId, limit = 20, offset = 0) {
|
||||||
|
try {
|
||||||
|
const res = await request({
|
||||||
|
url: '/api/kefu/getHistory',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
conversation_id: conversationId,
|
||||||
|
limit: limit,
|
||||||
|
offset: offset
|
||||||
|
// user_id: 'your-user-id', // 可选
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
console.error('获取历史记录失败:', res.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取历史记录请求失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新会话
|
||||||
|
async function createNewConversation() {
|
||||||
|
try {
|
||||||
|
const res = await request({
|
||||||
|
url: '/api/kefu/createConversation',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
// user_id: 'your-user-id', // 可选
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 0) {
|
||||||
|
return res.data.conversation_id;
|
||||||
|
} else {
|
||||||
|
console.error('创建会话失败:', res.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建会话请求失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、使用流程
|
||||||
|
|
||||||
|
1. **初始化会话**:小程序端进入客服页面时,调用`createConversation`接口创建新会话,或使用本地存储的会话ID
|
||||||
|
2. **发送消息**:用户输入消息后,调用`chat`接口发送消息,获取机器人回复
|
||||||
|
3. **显示消息**:将用户消息和机器人回复显示在聊天界面
|
||||||
|
4. **加载历史记录**:需要时调用`getHistory`接口加载历史消息
|
||||||
|
5. **维护会话**:保持会话ID,用于后续消息交流
|
||||||
|
|
||||||
|
## 六、注意事项
|
||||||
|
|
||||||
|
1. 请确保Dify API密钥的安全性,不要泄露给前端
|
||||||
|
2. 建议对用户ID进行加密处理,避免直接使用敏感信息
|
||||||
|
3. 对于大量用户的场景,建议实现会话管理机制,定期清理过期会话
|
||||||
|
4. 建议添加请求频率限制,防止恶意请求
|
||||||
|
5. 在生产环境中,建议关闭DEBUG模式
|
||||||
|
|
||||||
|
## 七、测试建议
|
||||||
|
|
||||||
|
1. 首先在Dify平台测试聊天机器人功能是否正常
|
||||||
|
2. 在后端配置正确的API密钥
|
||||||
|
3. 使用Postman或类似工具测试后端API接口
|
||||||
|
4. 在小程序端集成并测试完整流程
|
||||||
|
5. 模拟不同场景下的用户输入,测试机器人回复效果
|
||||||
|
|
||||||
|
## 八、常见问题
|
||||||
|
|
||||||
|
### 1. 接口返回401错误
|
||||||
|
|
||||||
|
**原因**:Dify API密钥无效或过期
|
||||||
|
**解决方法**:重新获取有效的API密钥并更新插件配置
|
||||||
|
|
||||||
|
### 2. 接口返回500错误
|
||||||
|
|
||||||
|
**原因**:后端服务器错误或Dify API服务异常
|
||||||
|
**解决方法**:查看服务器日志,检查Dify API服务状态
|
||||||
|
|
||||||
|
### 3. 机器人回复为空
|
||||||
|
|
||||||
|
**原因**:Dify聊天机器人配置问题或请求参数错误
|
||||||
|
**解决方法**:检查Dify机器人配置,验证请求参数是否正确
|
||||||
|
|
||||||
|
### 4. 会话ID无效
|
||||||
|
|
||||||
|
**原因**:会话已过期或不存在
|
||||||
|
**解决方法**:创建新会话,获取新的会话ID
|
||||||
Reference in New Issue
Block a user