diff --git a/src/addon/aikefu/api/controller/Kefu.php b/src/addon/aikefu/api/controller/Kefu.php new file mode 100644 index 000000000..9a8b7cae0 --- /dev/null +++ b/src/addon/aikefu/api/controller/Kefu.php @@ -0,0 +1,236 @@ +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())); + } + } +} diff --git a/src/addon/aikefu/config/info.php b/src/addon/aikefu/config/info.php new file mode 100644 index 000000000..109b6ef92 --- /dev/null +++ b/src/addon/aikefu/config/info.php @@ -0,0 +1,15 @@ + 'aikefu', + 'title' => '智能客服', + 'description' => '基于Dify的智能客服系统', + 'author' => 'admin', + 'version' => '1.0.0', + 'scene' => 'web', + 'state' => 0, + 'category' => 'business', + 'need_install' => 1, + 'need_cache' => 1, + 'hooks' => [], +]; diff --git a/src/addon/aikefu/data/install.sql b/src/addon/aikefu/data/install.sql new file mode 100644 index 000000000..d57b46cbc --- /dev/null +++ b/src/addon/aikefu/data/install.sql @@ -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='智能客服消息表'; diff --git a/src/addon/aikefu/data/uninstall.sql b/src/addon/aikefu/data/uninstall.sql new file mode 100644 index 000000000..637a520e1 --- /dev/null +++ b/src/addon/aikefu/data/uninstall.sql @@ -0,0 +1,4 @@ +-- 智能客服插件卸载脚本 +-- 删除智能客服相关表(配置信息存储在系统配置表中,无需单独删除) +DROP TABLE IF EXISTS `lucky_aikefu_message`; +DROP TABLE IF EXISTS `lucky_aikefu_conversation`; diff --git a/src/addon/aikefu/event/Install.php b/src/addon/aikefu/event/Install.php new file mode 100644 index 000000000..5152d79cb --- /dev/null +++ b/src/addon/aikefu/event/Install.php @@ -0,0 +1,50 @@ +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); + } +} diff --git a/src/addon/aikefu/event/Kefu.php b/src/addon/aikefu/event/Kefu.php new file mode 100644 index 000000000..169293ec7 --- /dev/null +++ b/src/addon/aikefu/event/Kefu.php @@ -0,0 +1,129 @@ +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', + ]; + } +} diff --git a/src/addon/aikefu/event/UnInstall.php b/src/addon/aikefu/event/UnInstall.php new file mode 100644 index 000000000..d4334c02e --- /dev/null +++ b/src/addon/aikefu/event/UnInstall.php @@ -0,0 +1,20 @@ +deleteAddon(['name' => 'aikefu']); + + return success(1); + } +} diff --git a/src/addon/aikefu/icon.png b/src/addon/aikefu/icon.png new file mode 100644 index 000000000..ae26bf15c --- /dev/null +++ b/src/addon/aikefu/icon.png @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/src/addon/aikefu/model/Config.php b/src/addon/aikefu/model/Config.php new file mode 100644 index 000000000..b0de5f4b3 --- /dev/null +++ b/src/addon/aikefu/model/Config.php @@ -0,0 +1,53 @@ +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; + } +} diff --git a/src/addon/aikefu/model/Conversation.php b/src/addon/aikefu/model/Conversation.php new file mode 100644 index 000000000..86132915e --- /dev/null +++ b/src/addon/aikefu/model/Conversation.php @@ -0,0 +1,102 @@ +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); + } +} diff --git a/src/addon/aikefu/model/Message.php b/src/addon/aikefu/model/Message.php new file mode 100644 index 000000000..60368be02 --- /dev/null +++ b/src/addon/aikefu/model/Message.php @@ -0,0 +1,118 @@ +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]); + } +} diff --git a/src/addon/aikefu/shop/controller/Kefu.php b/src/addon/aikefu/shop/controller/Kefu.php new file mode 100644 index 000000000..9c4fd4c3b --- /dev/null +++ b/src/addon/aikefu/shop/controller/Kefu.php @@ -0,0 +1,222 @@ +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); + } +} diff --git a/src/addon/aikefu/shop/view/kefu/config.html b/src/addon/aikefu/shop/view/kefu/config.html new file mode 100644 index 000000000..79f65e3d6 --- /dev/null +++ b/src/addon/aikefu/shop/view/kefu/config.html @@ -0,0 +1,98 @@ + + + + + + 智能客服配置 + + + + +
+
+
+

智能客服配置

+
+
+
+
+ + +
+ 从Dify平台获取的API密钥,用于调用Dify聊天机器人API。 + 前往Dify平台 +
+
+ +
+ + +
+ Dify API的基础地址,默认为https://api.dify.ai/v1 +
+
+ +
+ + +
+ 聊天接口的端点,默认为/chat-messages +
+
+ +
+ +
+ + +
+
+ 启用或禁用智能客服功能 +
+
+ +
+ + +
+
+
+
+
+ + + + + + diff --git a/src/addon/aikefu/shop/view/kefu/conversation.html b/src/addon/aikefu/shop/view/kefu/conversation.html new file mode 100644 index 000000000..134567ea4 --- /dev/null +++ b/src/addon/aikefu/shop/view/kefu/conversation.html @@ -0,0 +1,245 @@ + + + + + + 会话管理 + + + + +
+
+
+

会话管理

+
+
+ +
+ + + + + + + + + + + + + + + + +
ID会话ID用户ID会话名称状态创建时间更新时间操作
+
+ +
+
+
+ + + + + + diff --git a/src/addon/aikefu/shop/view/kefu/message.html b/src/addon/aikefu/shop/view/kefu/message.html new file mode 100644 index 000000000..acfb493e8 --- /dev/null +++ b/src/addon/aikefu/shop/view/kefu/message.html @@ -0,0 +1,276 @@ + + + + + + 消息管理 + + + + + +
+
+
+

消息管理

+
+
+
+ +
+ + + +
+ +
+ + +
+
+
+ + + + + + diff --git a/src/app/api/controller/Kefu.php b/src/app/api/controller/Kefu.php new file mode 100644 index 000000000..d180c91af --- /dev/null +++ b/src/app/api/controller/Kefu.php @@ -0,0 +1,172 @@ +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())); + } + } +} diff --git a/src/docs/kefu_api.md b/src/docs/kefu_api.md new file mode 100644 index 000000000..e22f537f9 --- /dev/null +++ b/src/docs/kefu_api.md @@ -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