Files
cunkebao_v3/Server/application/chukebao/controller/AiChatController.php
2025-10-25 17:41:20 +08:00

515 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\chukebao\controller;
use app\ai\controller\CozeAI;
use app\ai\controller\DouBaoAI;
use app\api\model\WechatFriendModel;
use app\chukebao\controller\TokensRecordController as tokensRecord;
use app\chukebao\model\AiSettings;
use app\chukebao\model\FriendSettings;
use app\chukebao\model\TokensCompany;
use library\ResponseHelper;
use think\Db;
/**
* AI聊天控制器
* 负责处理与好友的AI对话功能
*/
class AiChatController extends BaseController
{
// 对话状态常量
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()
{
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
$friendId = $this->request->param('friendId', '');
$wechatAccountId = $this->request->param('wechatAccountId', '');
$content = $this->request->param('content', '');
if (empty($wechatAccountId) || empty($friendId)) {
return ResponseHelper::error('参数缺失');
}
$tokens = TokensCompany::where(['companyId' => $companyId])->value('tokens');
if (empty($tokens) || $tokens <= 0) {
return ResponseHelper::error('用户Tokens余额不足');
}
//读取AI配置
$setting = Db::name('ai_settings')->where(['companyId' => $companyId, 'userId' => $userId])->find();
if (empty($setting)) {
return ResponseHelper::error('未找到配置信息请先配置AI策略');
}
$config = json_decode($setting['config'], true);
$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();
usort($messages, function ($a, $b) {
return $a['wechatTime'] <=> $b['wechatTime'];
});
//处理聊天数据
$msg = [];
foreach ($messages as $val) {
if (empty($val['content'])) {
continue;
}
if (!empty($val['isSend'])) {
$msg[] = '客服:' . $val['content'];
} else {
$msg[] = '用户:' . $val['content'];
}
}
$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);
$res = json_decode($res, true);
if ($res['code'] == 200) {
//扣除Tokens
$tokensRecord = new tokensRecord();
$nickname = Db::table('s2_wechat_friend')->where(['id' => $friendId])->value('nickname');
$remarks = !empty($nickname) ? '与好友【' . $nickname . '】聊天' : '与好友聊天';
$data = [
'tokens' => $res['data']['token'],
'type' => 0,
'form' => 1,
'wechatAccountId' => $wechatAccountId,
'friendIdOrGroupId' => $friendId,
'remarks' => $remarks,
];
$tokensRecord->consumeTokens($data);
return ResponseHelper::success($res['data']['content']);
} else {
return ResponseHelper::error($res['msg']);
}
}
}