Files
cunkebao_v3/Server/application/chukebao/controller/AiChatController.php

515 lines
16 KiB
PHP
Raw Normal View History

2025-09-19 16:49:25 +08:00
<?php
namespace app\chukebao\controller;
2025-10-25 17:41:20 +08:00
use app\ai\controller\CozeAI;
2025-09-19 16:49:25 +08:00
use app\ai\controller\DouBaoAI;
2025-10-25 17:41:20 +08:00
use app\api\model\WechatFriendModel;
2025-09-19 16:49:25 +08:00
use app\chukebao\controller\TokensRecordController as tokensRecord;
2025-10-25 17:41:20 +08:00
use app\chukebao\model\AiSettings;
use app\chukebao\model\FriendSettings;
2025-09-30 16:10:43 +08:00
use app\chukebao\model\TokensCompany;
2025-09-19 16:49:25 +08:00
use library\ResponseHelper;
use think\Db;
2025-10-25 17:41:20 +08:00
/**
* AI聊天控制器
* 负责处理与好友的AI对话功能
*/
2025-09-19 16:49:25 +08:00
class AiChatController extends BaseController
{
2025-10-25 17:41:20 +08:00
// 对话状态常量
const STATUS_CREATED = 'created'; // 对话已创建
const STATUS_IN_PROGRESS = 'in_progress'; // 智能体正在处理中
const STATUS_COMPLETED = 'completed'; // 智能体已完成处理
const STATUS_FAILED = 'failed'; // 对话失败
const STATUS_REQUIRES_ACTION = 'requires_action'; // 对话中断,需要进一步处理
const STATUS_CANCELED = 'canceled'; // 对话已取消
// 轮询配置
const MAX_RETRY_TIMES = 30; // 最大重试次数
const RETRY_INTERVAL = 2; // 重试间隔(秒)
/**
* AI聊天主入口
*
* @return \think\response\Json
*/
public function index()
{
try {
// 1. 参数验证和初始化
$params = $this->validateAndInitParams();
if ($params === false) {
return ResponseHelper::error('参数验证失败');
}
// 2. 验证Tokens余额
if (!$this->checkTokensBalance($params['companyId'])) {
return ResponseHelper::error('Tokens余额不足请充值后再试');
}
// 3. 获取AI配置
$setting = $this->getAiSettings($params['companyId']);
if (!$setting) {
return ResponseHelper::error('未找到AI配置信息请先配置AI策略');
}
// 4. 获取好友AI设置
$friendSettings = $this->getFriendSettings($params['companyId'], $params['friendId']);
if (!$friendSettings) {
return ResponseHelper::error('该好友未配置或未开启AI功能');
}
// 5. 确保会话存在
$conversationId = $this->ensureConversation($friendSettings, $setting, $params);
if (!$conversationId) {
return ResponseHelper::error('创建会话失败');
}
// 6. 获取历史消息
$msgData = $this->getHistoryMessages($params['friendId'], $friendSettings);
// 7. 创建AI对话
$chatId = $this->createAiChat($setting, $friendSettings, $msgData);
if (!$chatId) {
return ResponseHelper::error('创建对话失败');
}
// 8. 等待AI处理完成轮询
$chatResult = $this->waitForChatCompletion($conversationId, $chatId);
if (!$chatResult) {
return ResponseHelper::error('AI处理超时或失败');
}
// 9. 扣除Tokens
$this->consumeTokens($chatResult, $params, $friendSettings);
// 10. 获取对话消息
$messages = $this->getChatMessages($conversationId, $chatId);
if (!$messages) {
return ResponseHelper::error('获取对话消息失败');
}
return ResponseHelper::success($messages[1]['content'], '对话成功');
} catch (\Exception $e) {
\think\facade\Log::error('AI聊天异常' . $e->getMessage());
return ResponseHelper::error('系统异常:' . $e->getMessage());
}
}
/**
* 验证和初始化参数
*
* @return array|false
*/
private function validateAndInitParams()
{
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
$friendId = $this->request->param('friendId', '');
$wechatAccountId = $this->request->param('wechatAccountId', '');
if (empty($wechatAccountId) || empty($friendId)) {
return false;
}
return [
'userId' => $userId,
'companyId' => $companyId,
'friendId' => $friendId,
'wechatAccountId' => $wechatAccountId
];
}
/**
* 检查Tokens余额
*
* @param int $companyId 公司ID
* @return bool
*/
private function checkTokensBalance($companyId)
{
$tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
return !empty($tokens) && $tokens > 1000;
}
/**
* 获取AI配置
*
* @param int $companyId 公司ID
* @return AiSettings|null
*/
private function getAiSettings($companyId)
{
return AiSettings::where(['companyId' => $companyId])->find();
}
/**
* 获取好友AI设置
*
* @param int $companyId 公司ID
* @param string $friendId 好友ID
* @return FriendSettings|null
*/
private function getFriendSettings($companyId, $friendId)
{
$friendSettings = FriendSettings::where([
'companyId' => $companyId,
'friendId' => $friendId
])->find();
if (empty($friendSettings) || $friendSettings->type == 0) {
return null;
}
return $friendSettings;
}
/**
* 确保会话存在
*
* @param FriendSettings $friendSettings 好友设置
* @param AiSettings $setting AI设置
* @param array $params 参数
* @return string|null 会话ID
*/
private function ensureConversation($friendSettings, $setting, $params)
{
if (!empty($friendSettings->conversationId)) {
return $friendSettings->conversationId;
}
// 创建新会话
$cozeAI = new CozeAI();
$data = [
'bot_id' => $setting->botId,
'name' => '与好友' . $params['friendId'] . '的对话',
'meta_data' => [
'friendId' => (string)$friendSettings->friendId,
'wechatAccountId' => (string)$params['wechatAccountId'],
],
];
$res = $cozeAI->createConversation($data);
$res = json_decode($res, true);
if ($res['code'] != 200) {
\think\facade\Log::error('创建会话失败:' . ($res['msg'] ?? '未知错误'));
return null;
}
// 保存会话ID
$conversationId = $res['data']['id'];
$friendSettings->conversationId = $conversationId;
$friendSettings->conversationTime = time();
$friendSettings->save();
return $conversationId;
}
/**
* 获取历史消息
*
* @param string $friendId 好友ID
* @param FriendSettings $friendSettings 好友设置
* @return array
*/
private function getHistoryMessages($friendId, $friendSettings)
{
$msgData = [];
// 会话创建时间小于1分钟加载最近10条消息
if ($friendSettings->conversationTime >= time() - 60) {
$messages = Db::table('s2_wechat_message')
->where('wechatFriendId', $friendId)
->where('msgType', '<', 50)
->order('wechatTime desc')
->field('id,content,msgType,isSend,wechatTime')
->limit(10)
->select();
// 按时间正序排列
usort($messages, function ($a, $b) {
return $a['wechatTime'] <=> $b['wechatTime'];
});
// 处理聊天数据
foreach ($messages as $val) {
if (empty($val['content'])) {
continue;
}
$msg = [
'role' => empty($val['isSend']) ? 'user' : 'assistant',
'content' => $val['content'],
'type' => empty($val['isSend']) ? 'question' : 'answer',
'content_type' => 'text'
];
$msgData[] = $msg;
}
} else {
// 只加载最新一条用户消息
$message = Db::table('s2_wechat_message')
->where('wechatFriendId', $friendId)
->where('msgType', '<', 50)
->where('isSend', 0)
->order('wechatTime desc')
->field('id,content,msgType,isSend,wechatTime')
->find();
if (!empty($message) && !empty($message['content'])) {
$msgData[] = [
'role' => 'user',
'content' => $message['content'],
'type' => 'question',
'content_type' => 'text'
];
}
}
return $msgData;
}
/**
* 创建AI对话
*
* @param AiSettings $setting AI设置
* @param FriendSettings $friendSettings 好友设置
* @param array $msgData 消息数据
* @return string|null 对话ID
*/
private function createAiChat($setting, $friendSettings, $msgData)
{
$cozeAI = new CozeAI();
$data = [
'bot_id' => $setting->botId,
'uid' => $friendSettings->friendId,
'conversation_id' => $friendSettings->conversationId,
'question' => $msgData,
];
$res = $cozeAI->createChat($data);
$res = json_decode($res, true);
if ($res['code'] != 200) {
\think\facade\Log::error('创建对话失败:' . ($res['msg'] ?? '未知错误'));
return null;
}
return $res['data']['id'];
}
/**
* 等待AI处理完成轮询机制
*
* @param string $conversationId 会话ID
* @param string $chatId 对话ID
* @return array|null
*/
private function waitForChatCompletion($conversationId, $chatId)
{
$cozeAI = new CozeAI();
$retryCount = 0;
while ($retryCount < self::MAX_RETRY_TIMES) {
// 获取对话状态
$res = $cozeAI->getConversationChat([
'conversation_id' => $conversationId,
'chat_id' => $chatId,
]);
$res = json_decode($res, true);
if ($res['code'] != 200) {
\think\facade\Log::error('获取对话状态失败:' . ($res['msg'] ?? '未知错误'));
return null;
}
$status = $res['data']['status'] ?? '';
// 处理不同的状态
switch ($status) {
case self::STATUS_COMPLETED:
// 对话完成,返回结果
return $res['data'];
case self::STATUS_IN_PROGRESS:
case self::STATUS_CREATED:
// 继续等待
$retryCount++;
sleep(self::RETRY_INTERVAL);
break;
case self::STATUS_FAILED:
\think\facade\Log::error('对话失败chat_id: ' . $chatId);
return null;
case self::STATUS_CANCELED:
\think\facade\Log::error('对话已取消chat_id: ' . $chatId);
return null;
case self::STATUS_REQUIRES_ACTION:
\think\facade\Log::warning('对话需要进一步处理chat_id: ' . $chatId);
return null;
default:
\think\facade\Log::error('未知状态:' . $status);
return null;
}
}
// 超时
\think\facade\Log::error('对话处理超时chat_id: ' . $chatId);
return null;
}
/**
* 扣除Tokens
*
* @param array $chatResult 对话结果
* @param array $params 参数
* @param FriendSettings $friendSettings 好友设置
*/
private function consumeTokens($chatResult, $params, $friendSettings)
{
$tokenCount = $chatResult['usage']['token_count'] ?? 0;
if (empty($tokenCount)) {
return;
}
// 获取好友昵称
$nickname = WechatFriendModel::where('id', $friendSettings->friendId)->value('nickname');
$remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天';
// 扣除Tokens
$tokensRecord = new tokensRecord();
$data = [
'tokens' => $tokenCount * 20,
'type' => 0,
'form' => 1,
'wechatAccountId' => $params['wechatAccountId'],
'friendIdOrGroupId' => $params['friendId'],
'remarks' => $remarks,
];
$tokensRecord->consumeTokens($data);
}
/**
* 获取对话消息
*
* @param string $conversationId 会话ID
* @param string $chatId 对话ID
* @return array|null
*/
private function getChatMessages($conversationId, $chatId)
{
$cozeAI = new CozeAI();
$res = $cozeAI->listConversationMessage([
'conversation_id' => $conversationId,
'chat_id' => $chatId,
]);
$res = json_decode($res, true);
if ($res['code'] != 200) {
\think\facade\Log::error('获取对话消息失败:' . ($res['msg'] ?? '未知错误'));
return null;
}
return $res['data'] ?? [];
}
public function index2222()
{
2025-09-19 16:49:25 +08:00
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
2025-10-25 17:41:20 +08:00
$friendId = $this->request->param('friendId', '');
$wechatAccountId = $this->request->param('wechatAccountId', '');
$content = $this->request->param('content', '');
2025-09-19 16:49:25 +08:00
2025-10-25 17:41:20 +08:00
if (empty($wechatAccountId) || empty($friendId)) {
2025-09-19 16:49:25 +08:00
return ResponseHelper::error('参数缺失');
}
2025-09-30 16:10:43 +08:00
$tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
2025-10-25 17:41:20 +08:00
if (empty($tokens) || $tokens <= 0) {
2025-09-19 16:49:25 +08:00
return ResponseHelper::error('用户Tokens余额不足');
}
//读取AI配置
2025-10-25 17:41:20 +08:00
$setting = Db::name('ai_settings')->where(['companyId' => $companyId, 'userId' => $userId])->find();
if (empty($setting)) {
2025-09-19 16:49:25 +08:00
return ResponseHelper::error('未找到配置信息请先配置AI策略');
}
2025-10-25 17:41:20 +08:00
$config = json_decode($setting['config'], true);
2025-09-19 16:49:25 +08:00
$modelSetting = $config['modelSetting'];
$round = isset($config['round']) ? $config['round'] : 10;
// 导出聊天
$messages = Db::table('s2_wechat_message')
->where('wechatFriendId', $friendId)
->order('wechatTime desc')
->field('id,content,msgType,isSend,wechatTime')
->limit($round)
->select();
2025-10-25 17:41:20 +08:00
usort($messages, function ($a, $b) {
2025-09-19 16:49:25 +08:00
return $a['wechatTime'] <=> $b['wechatTime'];
});
//处理聊天数据
$msg = [];
2025-10-25 17:41:20 +08:00
foreach ($messages as $val) {
if (empty($val['content'])) {
2025-09-19 16:49:25 +08:00
continue;
}
2025-10-25 17:41:20 +08:00
if (!empty($val['isSend'])) {
$msg[] = '客服:' . $val['content'];
} else {
$msg[] = '用户:' . $val['content'];
2025-09-19 16:49:25 +08:00
}
}
$content = implode("\n", $msg);
$params = [
'model' => 'doubao-1-5-pro-32k-250115',
'messages' => [
// ['role' => 'system', 'content' => '请完成跟客户的对话'],
['role' => 'system', 'content' => '角色设定:' . $modelSetting['role']],
['role' => 'system', 'content' => '公司背景:' . $modelSetting['businessBackground']],
['role' => 'system', 'content' => '对话风格:' . $modelSetting['dialogueStyle']],
['role' => 'user', 'content' => $content],
],
];
//AI处理
$ai = new DouBaoAI();
$res = $ai->text($params);
2025-10-25 17:41:20 +08:00
$res = json_decode($res, true);
2025-09-19 16:49:25 +08:00
if ($res['code'] == 200) {
//扣除Tokens
$tokensRecord = new tokensRecord();
$nickname = Db::table('s2_wechat_friend')->where(['id' => $friendId])->value('nickname');
2025-10-25 17:41:20 +08:00
$remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天';
2025-09-19 16:49:25 +08:00
$data = [
'tokens' => $res['data']['token'],
'type' => 0,
'form' => 1,
'wechatAccountId' => $wechatAccountId,
'friendIdOrGroupId' => $friendId,
'remarks' => $remarks,
];
$tokensRecord->consumeTokens($data);
return ResponseHelper::success($res['data']['content']);
2025-10-25 17:41:20 +08:00
} else {
2025-09-19 16:49:25 +08:00
return ResponseHelper::error($res['msg']);
}
}
}