perf: 优化机器人Webhook消息

This commit is contained in:
kuaifan 2025-03-24 15:25:02 +08:00
parent d366cf9885
commit a49c0aea47
6 changed files with 154 additions and 113 deletions

View File

@ -629,12 +629,11 @@ class UsersController extends AbstractController
User::auth(); User::auth();
// //
$type = trim(Request::input('type')); $type = trim(Request::input('type'));
$botName = "ai-{$type}"; if (!UserBot::systemBotName($type)) {
if (!UserBot::isAiBot("{$botName}@bot.system")) {
return Base::retError('AI机器人不存在'); return Base::retError('AI机器人不存在');
} }
// //
$botUser = User::botGetOrCreate($botName); $botUser = User::botGetOrCreate("ai-{$type}");
if (empty($botUser)) { if (empty($botUser)) {
return Base::retError('AI机器人不存在'); return Base::retError('AI机器人不存在');
} }

View File

@ -255,6 +255,18 @@ class User extends AbstractModel
return false; return false;
} }
/**
* 返回是否用户机器人
* @return bool
*/
public function isUserBot()
{
if (preg_match('/^user-(.*?)@bot\.system$/', $this->email)) {
return true;
}
return false;
}
/** /**
* 判断是否管理员 * 判断是否管理员
*/ */

View File

@ -55,16 +55,6 @@ class UserBot extends AbstractModel
return str_ends_with($email, '@bot.system') && self::systemBotName($email); return str_ends_with($email, '@bot.system') && self::systemBotName($email);
} }
/**
* 判断是否系统AI机器人
* @param $email
* @return bool
*/
public static function isAiBot($email)
{
return str_starts_with($email, 'ai-') && self::isSystemBot($email);
}
/** /**
* 系统机器人名称 * 系统机器人名称
* @param $name string 邮箱 邮箱前缀 * @param $name string 邮箱 邮箱前缀

View File

@ -9,7 +9,7 @@ use App\Services\RequestContext;
use Cache; use Cache;
use Carbon\Carbon; use Carbon\Carbon;
use League\CommonMark\CommonMarkConverter; use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Exception\CommonMarkException; use League\HTMLToMarkdown\HtmlConverter;
use Overtrue\Pinyin\Pinyin; use Overtrue\Pinyin\Pinyin;
use Redirect; use Redirect;
use Request; use Request;
@ -2977,11 +2977,26 @@ class Base
*/ */
public static function markdown2html($markdown) public static function markdown2html($markdown)
{ {
$converter = new CommonMarkConverter();
try { try {
$converter = new CommonMarkConverter();
return $converter->convert($markdown); return $converter->convert($markdown);
} catch (CommonMarkException $e) { } catch (\League\CommonMark\Exception\CommonMarkException $e) {
return $markdown; return $markdown;
} }
} }
/**
* html MD(markdown)
* @param $html
* @return mixed|string
*/
public static function html2markdown($html)
{
try {
$converter = new HtmlConverter();
return $converter->convert($html);
} catch (\Exception) {
return $html;
}
}
} }

View File

@ -92,7 +92,7 @@ class BotReceiveMsgTask extends AbstractTask
// 提取指令 // 提取指令
try { try {
$command = $this->extractCommand($msg, $botUser->isAiBot(), $this->mention); $command = $this->extractCommand($msg, $botUser, $this->mention);
if (empty($command)) { if (empty($command)) {
return; return;
} }
@ -406,6 +406,7 @@ class BotReceiveMsgTask extends AbstractTask
$serverUrl = 'http://nginx'; $serverUrl = 'http://nginx';
$userBot = null; $userBot = null;
$extras = []; $extras = [];
$replyText = null;
$errorContent = null; $errorContent = null;
if ($botUser->isAiBot($type)) { if ($botUser->isAiBot($type)) {
// AI机器人 // AI机器人
@ -456,32 +457,13 @@ class BotReceiveMsgTask extends AbstractTask
} }
if ($msg->reply_id > 0) { if ($msg->reply_id > 0) {
$replyMsg = WebSocketDialogMsg::find($msg->reply_id); $replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
$replyCommand = null; if (Base::isError($replyCommand)) {
if ($replyMsg) { $errorContent = $replyCommand['msg'];
switch ($replyMsg->type) { } else {
case 'text':
try {
$replyCommand = $this->extractCommand($replyMsg, true);
} catch (Exception) {
$errorContent = "引用消息解析失败。";
}
break;
case 'file':
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
$fileResult = TextExtractor::extractFile(public_path($msgData['path']));
if (Base::isError($fileResult)) {
$errorContent = $fileResult['msg'];
} else {
$replyCommand = $fileResult['data'];
}
break;
}
}
if ($replyCommand) {
$command = <<<EOF $command = <<<EOF
<quoted_content> <quoted_content>
{$replyCommand} {$replyCommand['data']}
</quoted_content> </quoted_content>
The content within the above quoted_content tags is a citation. The content within the above quoted_content tags is a citation.
@ -494,9 +476,17 @@ class BotReceiveMsgTask extends AbstractTask
$webhookUrl = "{$serverUrl}/ai/chat"; $webhookUrl = "{$serverUrl}/ai/chat";
} else { } else {
// 用户机器人 // 用户机器人
if (str_starts_with($command, '/')) { if ($botUser->isUserBot() && str_starts_with($command, '/')) {
// 用户机器人不处理指令类型命令
return; return;
} }
if ($msg->reply_id > 0) {
$replyCommand = $this->extractReplyCommand($msg->reply_id, $botUser);
if (Base::isSuccess($replyCommand)) {
$replyText = $replyCommand['data'] ?: '';
}
}
$userBot = UserBot::whereBotId($botUser->userid)->first(); $userBot = UserBot::whereBotId($botUser->userid)->first();
$webhookUrl = $userBot?->webhook_url; $webhookUrl = $userBot?->webhook_url;
} }
@ -514,6 +504,7 @@ class BotReceiveMsgTask extends AbstractTask
try { try {
$data = [ $data = [
'text' => $command, 'text' => $command,
'reply_text' => $replyText,
'token' => User::generateToken($botUser), 'token' => User::generateToken($botUser),
'dialog_id' => $dialog->id, 'dialog_id' => $dialog->id,
'dialog_type' => $dialog->type, 'dialog_type' => $dialog->type,
@ -575,12 +566,12 @@ class BotReceiveMsgTask extends AbstractTask
/** /**
* 提取消息指令(提取消息内容) * 提取消息指令(提取消息内容)
* @param WebSocketDialogMsg $msg * @param WebSocketDialogMsg $msg
* @param bool $isAiBot * @param User $botUser
* @param bool $mention * @param bool $mention
* @return string * @return string
* @throws Exception * @throws Exception
*/ */
private function extractCommand(WebSocketDialogMsg $msg, bool $isAiBot = false, bool $mention = false) private function extractCommand(WebSocketDialogMsg $msg, User $botUser, bool $mention = false)
{ {
if ($msg->type !== 'text') { if ($msg->type !== 'text') {
return ''; return '';
@ -597,80 +588,113 @@ class BotReceiveMsgTask extends AbstractTask
} }
return $command; return $command;
} }
if (!$isAiBot) {
if ($botUser->isAiBot()) {
// AI 机器人
$contents = [];
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) {
// 任务
$taskIds = Base::newIntval($match[1]);
foreach ($taskIds as $index => $taskId) {
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first();
if (!$taskInfo) {
throw new Exception("任务不存在或已被删除");
}
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})";
$taskContext = implode("\n", $taskInfo->AIContext());
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>";
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original);
}
}
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
// 文件、报告
$urlPaths = $match[2];
foreach ($urlPaths as $index => $urlPath) {
$pathTag = null;
$pathName = null;
$pathContent = null;
// 文件
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
if (!$fileInfo || !isset($fileInfo->content['url'])) {
throw new Exception("文件不存在或已被删除");
}
$urlPath = public_path($fileInfo->content['url']);
if (!file_exists($urlPath)) {
throw new Exception("文件不存在或已被删除");
}
$fileResult = TextExtractor::extractFile($urlPath);
if (Base::isError($fileResult)) {
throw new Exception("文件读取失败:" . $fileResult['msg']);
}
$pathTag = "file_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
$pathContent = $fileResult['data'];
}
// 报告
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
if (!$reportInfo) {
throw new Exception("报告不存在或已被删除");
}
$pathTag = "report_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
$pathContent = $reportInfo->content;
}
if ($pathTag) {
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
}
}
}
$original = Base::html2markdown($original);
if ($contents) {
// 添加tag内容
$original .= "\n\n" . implode("\n\n", $contents);
}
return $original;
} elseif ($botUser->isUserBot()) {
// 用户机器人
return Base::html2markdown($original);
} else {
// 其他机器人(系统)
return trim(strip_tags($original)); return trim(strip_tags($original));
} }
}
$contents = []; /**
// 任务 * 提取回复消息指令
if (preg_match_all("/<span class=\"mention task\" data-id=\"(\d+)\">(.*?)<\/span>/", $original, $match)) { * @param $id
$taskIds = Base::newIntval($match[1]); * @param User $botUser
foreach ($taskIds as $index => $taskId) { * @return array
$taskInfo = ProjectTask::with(['content'])->whereId($taskId)->first(); */
if (!$taskInfo) { private function extractReplyCommand($id, User $botUser)
throw new Exception("任务不存在或已被删除"); {
} $replyMsg = WebSocketDialogMsg::find($id);
$taskName = addslashes($taskInfo->name) . " (ID:{$taskId})"; $replyCommand = null;
$taskContext = implode("\n", $taskInfo->AIContext()); if ($replyMsg) {
$contents[] = "<task_content path=\"{$taskName}\">\n{$taskContext}\n</task_content>"; switch ($replyMsg->type) {
$original = str_replace($match[0][$index], "'{$taskName}' (see below for task_content tag)", $original); case 'text':
try {
$replyCommand = $this->extractCommand($replyMsg, $botUser);
} catch (Exception) {
return Base::retError('error', "引用消息解析失败。");
}
break;
case 'file':
if ($botUser->isAiBot()) {
$msgData = Base::json2array($replyMsg->getRawOriginal('msg'));
$fileResult = TextExtractor::extractFile(public_path($msgData['path']));
if (Base::isError($fileResult)) {
return Base::retError('error', $fileResult['msg']);
} else {
$replyCommand = $fileResult['data'];
}
}
break;
} }
} }
// 文件、报告 return Base::retSuccess('success', $replyCommand);
if (preg_match_all("/<a class=\"mention ([^'\"]*)\" href=\"([^\"']+?)\"[^>]*?>[~%]([^>]*)<\/a>/", $original, $match)) {
$urlPaths = $match[2];
foreach ($urlPaths as $index => $urlPath) {
$pathTag = null;
$pathName = null;
$pathContent = null;
// 文件
if (preg_match("/single\/file\/(.*?)$/", $urlPath, $fileMatch)) {
$fileInfo = FileContent::idOrCodeToContent($fileMatch[1]);
if (!$fileInfo || !isset($fileInfo->content['url'])) {
throw new Exception("文件不存在或已被删除");
}
$urlPath = public_path($fileInfo->content['url']);
if (!file_exists($urlPath)) {
throw new Exception("文件不存在或已被删除");
}
$fileResult = TextExtractor::extractFile($urlPath);
if (Base::isError($fileResult)) {
throw new Exception("文件读取失败:" . $fileResult['msg']);
}
$pathTag = "file_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$fileInfo->id})";
$pathContent = $fileResult['data'];
}
// 报告
elseif (preg_match("/single\/report\/detail\/(.*?)$/", $urlPath, $reportMatch)) {
$reportInfo = Report::idOrCodeToContent($reportMatch[1]);
if (!$reportInfo) {
throw new Exception("报告不存在或已被删除");
}
$pathTag = "report_content";
$pathName = addslashes($match[3][$index]) . " (ID:{$reportInfo->id})";
$pathContent = $reportInfo->content;
}
if ($pathTag) {
$contents[] = "<{$pathTag} path=\"{$pathName}\">\n{$pathContent}\n</{$pathTag}>";
$original = str_replace($match[0][$index], "'{$pathName}' (see below for {$pathTag} tag)", $original);
}
}
}
if ($msg->msg['type'] !== 'md') {
// 转换为Markdown
try {
$converter = new HtmlConverter();
$original = $converter->convert($original);
} catch (\Exception) {
throw new Exception("Failed to convert HTML to Markdown");
}
}
if ($contents) {
// 添加tag内容
$original .= "\n\n" . implode("\n\n", $contents);
}
return $original ?: '';
} }
/** /**

View File

@ -18,6 +18,7 @@
<p><b>{{$L("Webhook说明")}}</b></p> <p><b>{{$L("Webhook说明")}}</b></p>
<p>{{$L("机器人收到消息后会将消息POST推送到Webhook地址请求超时为10秒请求参数如下")}}</p> <p>{{$L("机器人收到消息后会将消息POST推送到Webhook地址请求超时为10秒请求参数如下")}}</p>
<p><span class="mark-color">text</span>: {{$L("消息文本")}}</p> <p><span class="mark-color">text</span>: {{$L("消息文本")}}</p>
<p><span class="mark-color">reply_text</span>: {{$L("回复/引用消息文本")}}</p>
<p><span class="mark-color">token</span>: {{$L("机器人Token")}}</p> <p><span class="mark-color">token</span>: {{$L("机器人Token")}}</p>
<p><span class="mark-color">dialog_id</span>: {{$L("对话ID")}}</p> <p><span class="mark-color">dialog_id</span>: {{$L("对话ID")}}</p>
<p><span class="mark-color">dialog_type</span>: {{$L("对话类型")}}</p> <p><span class="mark-color">dialog_type</span>: {{$L("对话类型")}}</p>