604 lines
18 KiB
PHP
604 lines
18 KiB
PHP
<?php
|
|
|
|
namespace app\api\logic;
|
|
|
|
use app\api\logic\service\TokenLogService;
|
|
use app\common\enum\user\AccountLogEnum;
|
|
use app\common\logic\AccountLogLogic;
|
|
use app\common\model\chat\Assistants;
|
|
use app\common\model\chat\ChatLog;
|
|
use app\common\model\user\User;
|
|
use app\common\model\ChatPrompt;
|
|
use app\common\service\FileService;
|
|
use app\common\model\file\File;
|
|
use app\common\model\mindMap\MindMap;
|
|
|
|
class ChatLogic extends ApiLogic
|
|
{
|
|
|
|
const COMMON_CHAT = 'common_chat'; //通用聊天
|
|
const SCENE_CHAT = 'scene_chat'; //场景聊天
|
|
|
|
|
|
/**
|
|
* @desc 通用聊天
|
|
* @param array $params
|
|
* @return void
|
|
*/
|
|
public static function commonChat(array $params)
|
|
{
|
|
|
|
if (empty($params['message'])) {
|
|
message('参数错误');
|
|
}
|
|
|
|
$request['message'] = $params['message'];
|
|
$request['open_reasoning'] = $params['open_reasoning'] ?? 0;
|
|
$request['stream'] = true;
|
|
|
|
// 存在文件 TODO
|
|
if (isset($params['file_id'])) {
|
|
}
|
|
|
|
$logs = [];
|
|
|
|
if (isset($params['task_id']) && $params['task_id']) {
|
|
$request['task_id'] = $params['task_id'];
|
|
|
|
// 对话记录
|
|
$logs = self::chatLog($request['task_id'], 0, self::$uid);
|
|
|
|
if (!$logs) {
|
|
|
|
message('对话记录ID错误');
|
|
}
|
|
} else {
|
|
|
|
$request['task_id'] = generate_unique_task_id();
|
|
}
|
|
|
|
$messages = array_merge($logs, [
|
|
[
|
|
'role' => 'user',
|
|
'content' => $params['message']
|
|
]
|
|
]);
|
|
|
|
$request['messages'] = $messages;
|
|
|
|
self::requestChatUrl($request, self::COMMON_CHAT, self::$uid);
|
|
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* @desc 获取通用聊天助手信息
|
|
* @return bool
|
|
*/
|
|
public static function commonChatInfo(): bool
|
|
{
|
|
try {
|
|
$assistant = Assistants::where('id', 1)->findOrEmpty();
|
|
|
|
if ($assistant->isEmpty()) {
|
|
throw new \Exception("助手不存在");
|
|
}
|
|
$preliminary_ask = json_decode($assistant->preliminary_ask, true) ?? [];
|
|
$extra = json_decode($assistant->extra ?? '', true) ?? [];
|
|
|
|
foreach ($preliminary_ask as $key => $value) {
|
|
|
|
if (isset($value['logo'])) {
|
|
|
|
$preliminary_ask[$key]['logo'] = FileService::getFileUrl($value['logo']);
|
|
}
|
|
}
|
|
|
|
if (isset($extra['banner'])) {
|
|
$extra['banner'] = FileService::getFileUrl($extra['banner']);
|
|
}
|
|
|
|
$assistant->preliminary_ask = $preliminary_ask;
|
|
$assistant->logo = FileService::getFileUrl($assistant['logo']);
|
|
$assistant->banner = $extra['banner'] ?? '';
|
|
$assistant->new_chat_prompt = $extra['new_chat_prompt'] ?? '';
|
|
$assistant->file_prompt = $extra['file_prompt'] ?? '';
|
|
$assistant->extra = $extra;
|
|
self::$returnData = $assistant->toArray();
|
|
return true;
|
|
} catch (\Throwable $e) {
|
|
self::$error = $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @desc 获取场景聊天 - 助理信息
|
|
* @param array $params
|
|
* @return bool
|
|
*/
|
|
public static function sceneChatInfo(array $params): bool
|
|
{
|
|
try {
|
|
$assistant = Assistants::where('id', $params['assistant_id'])->findOrEmpty();
|
|
|
|
if ($assistant->isEmpty()) {
|
|
throw new \Exception("助手不存在");
|
|
}
|
|
|
|
$assistant->template_info = json_decode($assistant->template_info, true) ?? [];
|
|
$preliminary_ask = json_decode($assistant->preliminary_ask, true) ?? [];
|
|
|
|
foreach ($preliminary_ask as $key => $value) {
|
|
|
|
if (isset($value['logo'])) {
|
|
|
|
$preliminary_ask[$key]['logo'] = FileService::getFileUrl($value['logo']);
|
|
}
|
|
}
|
|
|
|
$assistant->preliminary_ask = $preliminary_ask;
|
|
self::$returnData = $assistant->toArray();
|
|
return true;
|
|
} catch (\Throwable $e) {
|
|
self::$error = $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @desc 场景聊天
|
|
* @param array $params
|
|
* @return true
|
|
*/
|
|
public static function sceneChat(array $params): bool
|
|
{
|
|
|
|
if (empty($params['message']) && empty($params['message_ext'])) {
|
|
message('参数错误');
|
|
}
|
|
|
|
// 获取 场景聊天 - 助理信息
|
|
$assistant = Assistants::where('id', $params['assistant_id'])->findOrEmpty();
|
|
|
|
if ($assistant->isEmpty()) {
|
|
message('助手不存在');
|
|
}
|
|
|
|
$message = $params['message'];
|
|
|
|
// 表单变量替换
|
|
$message_ext = $params['message_ext'] ?? '';
|
|
|
|
if ($message_ext) {
|
|
$message_ext_text = self::parseMsg($message_ext, $assistant['form_info']);
|
|
$message = $message_ext_text . $message;
|
|
}
|
|
|
|
$logs = [];
|
|
|
|
if (isset($params['task_id']) && $params['task_id']) {
|
|
$taskId = $params['task_id'];
|
|
|
|
// 对话记录
|
|
$logs = self::chatLog($taskId, $assistant->id, self::$uid);
|
|
|
|
if (!$logs) {
|
|
|
|
message('对话记录ID错误');
|
|
}
|
|
} else {
|
|
|
|
$taskId = generate_unique_task_id();
|
|
}
|
|
|
|
$request = self::assembleAssistantRequest($assistant->toArray(), $message, $logs);
|
|
|
|
$request['message'] = $message;
|
|
$request['task_id'] = $taskId;
|
|
|
|
// 存在文件 TODO
|
|
if (isset($params['file_id'])) {
|
|
}
|
|
|
|
self::requestChatUrl($request, self::SCENE_CHAT, self::$uid);
|
|
|
|
exit;
|
|
}
|
|
|
|
|
|
/**
|
|
* @desc 提示词聊天
|
|
* @param array $params
|
|
* @return true
|
|
*/
|
|
public static function promptChat(array $params): bool
|
|
{
|
|
|
|
if (empty($params['message'])) {
|
|
|
|
message('参数错误');
|
|
}
|
|
|
|
//获取提示词
|
|
$prompt = ChatPrompt::where('id', $params['prompt_id'])->value('prompt_text') ?? '';
|
|
|
|
if (!$prompt) {
|
|
|
|
message("提示词不存在");
|
|
}
|
|
|
|
//获取场景
|
|
switch ($params['prompt_id']) {
|
|
case 1: //数字人口播
|
|
$scene = 'human_prompt';
|
|
$scene_type = AccountLogEnum::TOKENS_DEC_HUMAN_PROMPT;
|
|
break;
|
|
case 2: //思维导图
|
|
$scene = 'mind_map';
|
|
$scene_type = AccountLogEnum::TOKENS_DEC_MIND_MAP;
|
|
break;
|
|
case 3: //AI画图 - 文生图
|
|
case 4: //AI画图 - 图生图
|
|
case 5: //AI画图 - 商品图
|
|
$scene = 'image_prompt';
|
|
$scene_type = AccountLogEnum::TOKENS_DEC_IMAGE_PROMPT;
|
|
break;
|
|
}
|
|
|
|
$request = [
|
|
"messages" => [
|
|
[
|
|
'role' => "system",
|
|
'content' => $prompt
|
|
],
|
|
[
|
|
'role' => "user",
|
|
'content' => $params['message']
|
|
]
|
|
],
|
|
'stream' => false,
|
|
'message' => $params['message'],
|
|
'task_id' => generate_unique_task_id(),
|
|
'user_id' => self::$uid,
|
|
'assistant_id' => 0,
|
|
'chat_type' => $scene_type,
|
|
'now' => time(),
|
|
];
|
|
|
|
$unit = TokenLogService::checkToken(self::$uid, $scene);
|
|
|
|
if($scene == 'human_prompt'){
|
|
$request['open_reasoning'] = 5;
|
|
}
|
|
|
|
$response = \app\common\service\ToolsService::Chat()->message($request);
|
|
|
|
$reply = $response['data']['choices'][0]['message']['content'] ?? '';
|
|
|
|
//计费
|
|
$tokens = $response['data']['usage']['total_tokens'] ?? 0;
|
|
|
|
if (!$reply || $tokens == 0) {
|
|
|
|
message('获取内容失败');
|
|
}
|
|
|
|
$response = [
|
|
'reply' => $reply,
|
|
'usage_tokens' => $response['data']['usage'] ?? [],
|
|
];
|
|
|
|
// 保存聊天记录
|
|
self::saveChatResponseLog($request, $response);
|
|
|
|
//计算消耗tokens
|
|
$points = $unit > 0 ? ceil($tokens / $unit) : 0;
|
|
|
|
//token扣除
|
|
User::userTokensChange(self::$uid, $points);
|
|
|
|
$extra = ['总消耗tokens数' => $tokens, '算力单价' => $unit, '实际消耗算力' => $points];
|
|
|
|
//扣费记录
|
|
AccountLogLogic::recordUserTokensLog(true, self::$uid, $scene_type, $points, $request['task_id'], $extra);
|
|
|
|
if ($scene_type == AccountLogEnum::TOKENS_DEC_MIND_MAP) {
|
|
|
|
self::$returnData = MindMap::create([
|
|
'user_id' => self::$uid,
|
|
'task_id' => $request['task_id'],
|
|
'ask' => $request['message'],
|
|
'reply' => $reply,
|
|
'task_time' => time() - $request['now'],
|
|
])->toArray();
|
|
} else {
|
|
|
|
self::$returnData = [
|
|
'reply' => $reply,
|
|
];
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @desc 聊天记录
|
|
* @param array $params
|
|
* @return true
|
|
*/
|
|
public static function chatLogs(array $params)
|
|
{
|
|
try {
|
|
|
|
$logList = [];
|
|
|
|
ChatLog::where('user_id', self::$uid)
|
|
->where('assistant_id', $params['assistant_id'])
|
|
->whereIn('chat_type', [AccountLogEnum::TOKENS_DEC_COMMON_CHAT, AccountLogEnum::TOKENS_DEC_SCENE_CHAT, AccountLogEnum::TOKENS_DEC_KNOWLEDGE_CHAT])
|
|
->where('task_id', $params['task_id'])
|
|
->field('id,user_id,task_id,assistant_id,message,reasoning_content,usage_tokens,reply,file_ids,create_time')
|
|
->order('id asc')->select()
|
|
->each(function ($item) use (&$logList) {
|
|
|
|
// 文件处理
|
|
$files = [];
|
|
if (!empty($item['file_ids'])) {
|
|
$ids = json_decode($item['file_ids'], true);
|
|
foreach ($ids as $id) {
|
|
$file = File::where('id', $id)->value('uri') ?? '';
|
|
if ($file) {
|
|
$files[] = FileService::getFileUrl($file);
|
|
}
|
|
}
|
|
}
|
|
|
|
$user_avatar = User::where('id', $item['user_id'])->value('avatar') ?? '';
|
|
$assistants_avatar = Assistants::where('id', $item['assistant_id'] ?: 1)->value('logo') ?? '';
|
|
|
|
$logList[] = [
|
|
'avatar' => FileService::getFileUrl($user_avatar),
|
|
'message' => $item['message'],
|
|
'type' => 1,
|
|
'create_time' => $item['create_time'],
|
|
'file_urls' => $files,
|
|
'tokens_info' => $item['usage_tokens']
|
|
];
|
|
|
|
$logList[] = [
|
|
'avatar' => FileService::getFileUrl($assistants_avatar),
|
|
'reply' => $item['reply'],
|
|
'reasoning_content' => $item['reasoning_content'],
|
|
'type' => 2,
|
|
'create_time' => $item['create_time'],
|
|
'tokens_info' => $item['usage_tokens']
|
|
];
|
|
});
|
|
|
|
self::$returnData = $logList;
|
|
return true;
|
|
} catch (\Throwable $e) {
|
|
self::$error = $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @desc 删除聊天记录
|
|
* @param array $params
|
|
* @return true
|
|
*/
|
|
public static function deleteChat(array $params): bool
|
|
{
|
|
try {
|
|
ChatLog::where('task_id', $params['task_id'])->where('user_id', self::$uid)->select()->delete();
|
|
return true;
|
|
} catch (\Throwable $e) {
|
|
self::$error = $e->getMessage();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @desc 保存聊天记录
|
|
* @return void
|
|
* @date 2024/6/27 9:30
|
|
* @author dagouzi
|
|
*/
|
|
public static function saveChatResponseLog(array $request, array $response = [])
|
|
{
|
|
try {
|
|
$chatLogData = [
|
|
'user_id' => $request['user_id'],
|
|
'task_id' => $request['task_id'],
|
|
'assistant_id' => $request['assistant_id'] ?? 0,
|
|
'message' => $request['message'] ?? ($request['prompt'] ?? ''),
|
|
'reply' => $response['reply'],
|
|
'chat_type' => $request['chat_type'],
|
|
'usage_tokens' => $response['usage_tokens'] ?? [],
|
|
'reasoning_content' => $response['reasoning_content'] ?? null,
|
|
'file_ids' => !empty($request['file_id']) ? json_encode($request['file_id']) : '',
|
|
'task_time' => time() - $request['now'],
|
|
];
|
|
ChatLog::create($chatLogData);
|
|
} catch (\Throwable $e) {
|
|
message($e->getMessage(), 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @desc tokens计费
|
|
* @return void
|
|
* @date 2024/12/17 10:46
|
|
* @author dagouzi
|
|
*/
|
|
public static function chatTokensCharge($request, $tokens): void
|
|
{
|
|
|
|
[$tokenScene, $tokenCode] = match ($request['chat_type']) {
|
|
AccountLogEnum::TOKENS_DEC_COMMON_CHAT => ['common_chat', AccountLogEnum::TOKENS_DEC_COMMON_CHAT],
|
|
AccountLogEnum::TOKENS_DEC_SCENE_CHAT => ['scene_chat', AccountLogEnum::TOKENS_DEC_SCENE_CHAT],
|
|
AccountLogEnum::TOKENS_DEC_KNOWLEDGE_CHAT => ['knowledge_chat', AccountLogEnum::TOKENS_DEC_KNOWLEDGE_CHAT],
|
|
};
|
|
|
|
$unit = TokenLogService::getTypeScore($tokenScene);
|
|
|
|
//计算消耗tokens
|
|
$points = $unit > 0 ? ceil($tokens / $unit) : 0;
|
|
|
|
//token扣除
|
|
User::userTokensChange($request['user_id'], $points);
|
|
|
|
if($request['chat_type'] == AccountLogEnum::TOKENS_DEC_KNOWLEDGE_CHAT){
|
|
$extra = ['总消耗tokens数' => $tokens, '知识库消耗tokens数' => $request['knowledge_tokens'], '算力单价' => $unit, '实际消耗算力' => $points];
|
|
}else{
|
|
$extra = ['总消耗tokens数' => $tokens, '算力单价' => $unit, '实际消耗算力' => $points];
|
|
}
|
|
|
|
|
|
//扣费记录
|
|
AccountLogLogic::recordUserTokensLog(true, $request['user_id'], $tokenCode, $points, $request['task_id'], $extra);
|
|
}
|
|
|
|
/**
|
|
* 获取聊天记录
|
|
* @param string $taskId
|
|
* @param int $assistantId
|
|
* @param int $userId
|
|
* @param int $limit
|
|
* @return array
|
|
*/
|
|
private static function chatLog(string $taskId, int $assistantId, int $userId, int $limit = 10): array
|
|
{
|
|
$logs = [];
|
|
|
|
// 获取指定 taskId 的所有记录,按 id 升序排序
|
|
$ids = ChatLog::where('task_id', $taskId)
|
|
->where('assistant_id', $assistantId)
|
|
->where('user_id', $userId)
|
|
->order('id', 'desc')
|
|
->limit($limit)
|
|
->column('id');
|
|
|
|
ChatLog::whereIn('id', $ids)
|
|
->order('id', 'asc')
|
|
->field('message,reply')
|
|
->select()
|
|
->each(function ($item) use (&$logs) {
|
|
$logs[] = [
|
|
'role' => 'user',
|
|
'content' => $item->message
|
|
];
|
|
|
|
$logs[] = [
|
|
'role' => 'assistant',
|
|
'content' => $item->reply
|
|
];
|
|
});
|
|
|
|
return $logs;
|
|
}
|
|
|
|
/**
|
|
* 助手参数
|
|
* @param array $assistant
|
|
* @return array
|
|
*/
|
|
private static function assembleAssistantRequest(array $assistant, string $message, array $logs = []): array
|
|
{
|
|
|
|
// 系统提示词
|
|
$messages = [
|
|
[
|
|
'role' => 'system',
|
|
'content' => $assistant['instructions']
|
|
],
|
|
];
|
|
|
|
// 对话轮数
|
|
$messages = array_merge($messages, $logs, [
|
|
[
|
|
'role' => 'user',
|
|
'content' => $message
|
|
]
|
|
]);
|
|
|
|
return [
|
|
'temperature' => $assistant['temperature'] ?? 1,
|
|
'top_p' => $assistant['top_p'] ?? 1,
|
|
'stream' => true,
|
|
'assistant_id' => $assistant['id'],
|
|
'messages' => $messages,
|
|
];
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* @desc 解析表单变量
|
|
* @param $message_ext
|
|
* @param $form_info
|
|
* @return array|string|string[]
|
|
* @date 2024/7/2 10:14
|
|
* @author dagouzi
|
|
*/
|
|
private static function parseMsg($message_ext, $form_info)
|
|
{
|
|
$message_ext = json_decode($message_ext, true);
|
|
if (empty($message_ext)) {
|
|
return '';
|
|
}
|
|
preg_match_all('/\${([^\}]+)}/u', $form_info, $matches);
|
|
$keys = $matches[1];
|
|
if (empty($keys)) {
|
|
return '';
|
|
}
|
|
foreach ($message_ext as $key => $value) {
|
|
foreach ($keys as $keyword) {
|
|
if ($keyword == $key) {
|
|
if (!empty($value) && is_array($value)) {
|
|
$value = implode(',', $value);
|
|
}
|
|
$form_info = str_replace('${' . $keyword . '}', $value, $form_info);
|
|
}
|
|
}
|
|
}
|
|
return $form_info;
|
|
}
|
|
|
|
|
|
/**
|
|
* 请求上游接口与计费
|
|
* @param array $request
|
|
* @param string $scene
|
|
* @param int $userId
|
|
* @return void
|
|
* @throws \Exception
|
|
*/
|
|
private static function requestChatUrl(array $request, string $scene, int $userId): void
|
|
{
|
|
|
|
[$tokenScene, $tokenCode] = match ($scene) {
|
|
self::COMMON_CHAT => ['common_chat', AccountLogEnum::TOKENS_DEC_COMMON_CHAT],
|
|
self::SCENE_CHAT => ['scene_chat', AccountLogEnum::TOKENS_DEC_SCENE_CHAT],
|
|
};
|
|
|
|
//检查用户token
|
|
TokenLogService::checkToken($userId, $tokenScene);
|
|
|
|
$requestService = \app\common\service\ToolsService::Chat();
|
|
|
|
|
|
$request['user_id'] = $userId;
|
|
$request['chat_type'] = $tokenCode;
|
|
$request['now'] = time();
|
|
|
|
if ($scene == self::COMMON_CHAT) {
|
|
|
|
$requestService->message($request);
|
|
} else {
|
|
$requestService->sceneMessage($request);
|
|
}
|
|
}
|
|
}
|