515 lines
16 KiB
PHP
515 lines
16 KiB
PHP
<?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']);
|
||
}
|
||
|
||
|
||
}
|
||
} |