From f59bdaf5e0c230a3e8a4648759d7deede8f96651 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Tue, 30 Sep 2025 04:25:50 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=20webhook=20=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=EF=BC=8C=E4=BC=98=E5=8C=96=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Api/DialogController.php | 24 ++++ app/Http/Controllers/Api/UsersController.php | 11 +- app/Models/UserBot.php | 128 ++++++++++++++++++ app/Models/WebSocketDialog.php | 101 +++++++++++++- app/Tasks/BotReceiveMsgTask.php | 101 +++++++------- ...01_000001_add_user_bots_webhook_events.php | 39 ++++++ .../assets/js/pages/manage/application.vue | 56 +++++++- .../pages/manage/components/DialogWrapper.vue | 38 ++++++ 8 files changed, 443 insertions(+), 55 deletions(-) create mode 100644 database/migrations/2025_10_01_000001_add_user_bots_webhook_events.php diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index b39c4f8bf..a653f34b6 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -12,6 +12,7 @@ use App\Module\AI; use App\Module\Doo; use App\Models\File; use App\Models\User; +use App\Models\UserBot; use App\Module\Base; use App\Module\Timer; use App\Models\Setting; @@ -443,6 +444,29 @@ class DialogController extends AbstractController return Base::retError('打开会话失败'); } $data = WebSocketDialog::synthesizeData($dialog->id, $user->userid); + + if ($userid > 0) { + $botTarget = User::whereUserid($userid)->whereBot(1)->first(); + if ($botTarget) { + $userBot = UserBot::whereBotId($botTarget->userid)->first(); + if ($userBot) { + $userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_DIALOG_OPEN, [ + 'dialog_id' => $dialog->id, + 'dialog_type' => $dialog->type, + 'session_id' => $dialog->session_id, + 'dialog_name' => $dialog->getGroupName(), + 'user' => [ + 'userid' => $user->userid, + 'email' => $user->email, + 'nickname' => $user->nickname, + ], + ], 10, [ + 'dialog' => $dialog->id, + 'operator' => $user->userid, + ]); + } + } + } return Base::retSuccess('success', $data); } diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index 1f29ebaa6..05e2f0629 100755 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -2150,7 +2150,8 @@ class UsersController extends AbstractController 'users.nickname', 'users.userimg', 'user_bots.clear_day', - 'user_bots.webhook_url' + 'user_bots.webhook_url', + 'user_bots.webhook_events' ]) ->orderByDesc('id') ->get() @@ -2160,6 +2161,7 @@ class UsersController extends AbstractController $bot['name'] = $bot['nickname']; $bot['avatar'] = $bot['userimg']; $bot['system_name'] = UserBot::systemBotName($bot['name']); + $bot['webhook_events'] = UserBot::normalizeWebhookEvents($bot['webhook_events'] ?? null, empty($bot['webhook_events'])); unset($bot['userid'], $bot['nickname'], $bot['userimg']); } @@ -2211,11 +2213,13 @@ class UsersController extends AbstractController 'avatar' => $botUser->userimg, 'clear_day' => 0, 'webhook_url' => '', + 'webhook_events' => [UserBot::WEBHOOK_EVENT_MESSAGE], 'system_name' => UserBot::systemBotName($botUser->email), ]; if ($userBot) { $data['clear_day'] = $userBot->clear_day; $data['webhook_url'] = $userBot->webhook_url; + $data['webhook_events'] = $userBot->webhook_events; } return Base::retSuccess('success', $data); } @@ -2296,6 +2300,9 @@ class UsersController extends AbstractController if (Arr::exists($data, 'webhook_url')) { $upBot['webhook_url'] = trim($data['webhook_url']); } + if (Arr::exists($data, 'webhook_events')) { + $upBot['webhook_events'] = UserBot::normalizeWebhookEvents($data['webhook_events'], false); + } // if ($upUser) { $botUser->updateInstance($upUser); @@ -2312,11 +2319,13 @@ class UsersController extends AbstractController 'avatar' => $botUser->userimg, 'clear_day' => 0, 'webhook_url' => '', + 'webhook_events' => [UserBot::WEBHOOK_EVENT_MESSAGE], 'system_name' => UserBot::systemBotName($botUser->email), ]; if ($userBot) { $data['clear_day'] = $userBot->clear_day; $data['webhook_url'] = $userBot->webhook_url; + $data['webhook_events'] = $userBot->webhook_events; } return Base::retSuccess($botId ? '修改成功' : '添加成功', $data); } diff --git a/app/Models/UserBot.php b/app/Models/UserBot.php index b1c4c38e1..d3cd78eb8 100644 --- a/app/Models/UserBot.php +++ b/app/Models/UserBot.php @@ -5,10 +5,12 @@ namespace App\Models; use App\Module\Base; use App\Module\Doo; use App\Module\Extranet; +use App\Module\Ihttp; use App\Module\Timer; use App\Tasks\JokeSoupTask; use Cache; use Carbon\Carbon; +use Throwable; /** * App\Models\UserBot @@ -20,6 +22,7 @@ use Carbon\Carbon; * @property \Illuminate\Support\Carbon|null $clear_at 下一次清理时间 * @property string|null $webhook_url 消息webhook地址 * @property int|null $webhook_num 消息webhook请求次数 + * @property array|null $webhook_events Webhook事件配置 * @property \Illuminate\Support\Carbon|null $created_at * @property \Illuminate\Support\Carbon|null $updated_at * @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend() @@ -44,6 +47,131 @@ use Carbon\Carbon; */ class UserBot extends AbstractModel { + public const WEBHOOK_EVENT_MESSAGE = 'message'; + public const WEBHOOK_EVENT_DIALOG_OPEN = 'dialog_open'; + public const WEBHOOK_EVENT_MEMBER_JOIN = 'member_join'; + public const WEBHOOK_EVENT_MEMBER_LEAVE = 'member_leave'; + + protected $casts = [ + 'webhook_events' => 'array', + ]; + + /** + * 获取可选的 webhook 事件 + * + * @return string[] + */ + public static function webhookEventOptions(): array + { + return [ + self::WEBHOOK_EVENT_MESSAGE, + self::WEBHOOK_EVENT_DIALOG_OPEN, + self::WEBHOOK_EVENT_MEMBER_JOIN, + self::WEBHOOK_EVENT_MEMBER_LEAVE, + ]; + } + + /** + * 标准化 webhook 事件配置 + * + * @param mixed $events + * @return array + */ + public static function normalizeWebhookEvents(mixed $events, bool $useFallback = true): array + { + if (is_string($events)) { + $events = Base::json2array($events); + } + if ($events === null) { + $events = []; + } + if (!is_array($events)) { + $events = [$events]; + } + $events = array_filter(array_map('strval', $events)); + $events = array_values(array_intersect($events, self::webhookEventOptions())); + return $events ?: ($useFallback ? [self::WEBHOOK_EVENT_MESSAGE] : []); + } + + /** + * 获取 webhook 事件配置 + * + * @param mixed $value + * @return array + */ + public function getWebhookEventsAttribute(mixed $value): array + { + if ($value === null || $value === '') { + return self::normalizeWebhookEvents(null, true); + } + return self::normalizeWebhookEvents($value, false); + } + + /** + * 设置 webhook 事件配置 + * + * @param mixed $value + * @return void + */ + public function setWebhookEventsAttribute(mixed $value): void + { + $useFallback = $value === null; + $this->attributes['webhook_events'] = Base::array2json(self::normalizeWebhookEvents($value, $useFallback)); + } + + /** + * 判断是否需要触发指定 webhook 事件 + * + * @param string $event + * @return bool + */ + public function shouldDispatchWebhook(string $event): bool + { + if (!$this->webhook_url) { + return false; + } + if (!preg_match('/^https?:\/\//', $this->webhook_url)) { + return false; + } + return in_array($event, $this->webhook_events ?? [], true); + } + + /** + * 发送 webhook + * + * @param string $event + * @param array $payload + * @param int $timeout + * @param array $context + * @return array|null + */ + public function dispatchWebhook(string $event, array $payload, int $timeout = 30, array $context = []): ?array + { + if (!$this->shouldDispatchWebhook($event)) { + return null; + } + + $payload = array_merge([ + 'event' => $event, + 'timestamp' => time(), + 'bot_uid' => $this->bot_id, + 'owner_uid' => $this->userid, + ], $payload); + + try { + $result = Ihttp::ihttp_post($this->webhook_url, $payload, $timeout); + $this->increment('webhook_num'); + return $result; + } catch (Throwable $th) { + info(Base::array2json(array_merge($context, [ + 'bot_userid' => $this->bot_id, + 'event' => $event, + 'webhook_url' => $this->webhook_url, + 'error' => $th->getMessage(), + ]))); + return null; + } + } /** * 判断是否系统机器人 diff --git a/app/Models/WebSocketDialog.php b/app/Models/WebSocketDialog.php index 02aaba5b5..0b6f3267d 100644 --- a/app/Models/WebSocketDialog.php +++ b/app/Models/WebSocketDialog.php @@ -461,7 +461,8 @@ class WebSocketDialog extends AbstractModel */ public function joinGroup($userid, $inviter, $important = null) { - AbstractModel::transaction(function () use ($important, $inviter, $userid) { + $addedUserIds = []; + AbstractModel::transaction(function () use ($important, $inviter, $userid, &$addedUserIds) { foreach (is_array($userid) ? $userid : [$userid] as $value) { if ($value > 0) { $updateData = [ @@ -480,6 +481,7 @@ class WebSocketDialog extends AbstractModel ]); }, $isInsert); if ($isInsert) { + $addedUserIds[] = $value; WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [ 'notice' => User::userid2nickname($value) . " 已加入群组" ], $inviter, true, true); @@ -490,6 +492,16 @@ class WebSocketDialog extends AbstractModel $data = WebSocketDialog::generatePeople($this->id); $data['id'] = $this->id; $this->pushMsg("groupUpdate", $data); + if ($addedUserIds) { + $meta = ['action' => 'join']; + if ($inviter > 0) { + $actor = $this->getUserSnapshots([$inviter]); + if (!empty($actor)) { + $meta['actor'] = $actor[0]; + } + } + $this->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_JOIN, $addedUserIds, $meta); + } return true; } @@ -503,14 +515,15 @@ class WebSocketDialog extends AbstractModel public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true) { $typeDesc = $type === 'remove' ? '移出' : '退出'; - AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid) { + $removedUserIds = []; + AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid, &$removedUserIds) { $builder = WebSocketDialogUser::whereDialogId($this->id); if (is_array($userid)) { $builder->whereIn('userid', $userid); } else { $builder->whereUserid($userid); } - $builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type) { + $builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type, &$removedUserIds) { /** @var WebSocketDialogUser $item */ foreach ($list as $item) { if ($checkDelete) { @@ -531,6 +544,7 @@ class WebSocketDialog extends AbstractModel } // $item->delete(); + $removedUserIds[] = $item->userid; // if ($pushMsg) { if ($type === 'remove') { @@ -549,6 +563,87 @@ class WebSocketDialog extends AbstractModel $data = WebSocketDialog::generatePeople($this->id); $data['id'] = $this->id; $this->pushMsg("groupUpdate", $data); + if ($removedUserIds) { + $meta = ['action' => $type]; + $operatorId = User::userid(); + if ($operatorId > 0) { + $actor = $this->getUserSnapshots([$operatorId]); + if (!empty($actor)) { + $meta['actor'] = $actor[0]; + } + } + $this->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_LEAVE, $removedUserIds, $meta); + } + } + + /** + * 获取用户快照 + * @param array $userIds + * @return array + */ + protected function getUserSnapshots(array $userIds): array + { + $userIds = array_values(array_unique(array_filter($userIds))); + if (empty($userIds)) { + return []; + } + return User::whereIn('userid', $userIds) + ->get(['userid', 'nickname', 'email', 'bot']) + ->map(function (User $user) { + return [ + 'userid' => $user->userid, + 'nickname' => $user->nickname, + 'email' => $user->email, + 'is_bot' => (bool)$user->bot, + ]; + }) + ->values() + ->all(); + } + + /** + * 推送成员事件到机器人 webhook + * @param string $event + * @param array $memberIds + * @param array $meta + * @return void + */ + protected function dispatchMemberWebhook(string $event, array $memberIds, array $meta = []): void + { + $memberIds = array_values(array_unique(array_filter($memberIds))); + if (empty($memberIds)) { + return; + } + + $botIds = $this->dialogUser()->where('bot', 1)->pluck('userid')->toArray(); + if (empty($botIds)) { + return; + } + + $userBots = UserBot::whereIn('bot_id', $botIds)->get(); + if ($userBots->isEmpty()) { + return; + } + + $members = $this->getUserSnapshots($memberIds); + if (empty($members)) { + return; + } + + $payload = array_merge([ + 'dialog_id' => $this->id, + 'dialog_type' => $this->type, + 'group_type' => $this->group_type, + 'dialog_name' => $this->getGroupName(), + 'members' => $members, + ], array_filter($meta, fn ($value) => $value !== null)); + + foreach ($userBots as $userBot) { + $userBot->dispatchWebhook($event, $payload, 10, [ + 'dialog' => $this->id, + 'event_members' => $memberIds, + ]); + } } /** diff --git a/app/Tasks/BotReceiveMsgTask.php b/app/Tasks/BotReceiveMsgTask.php index 4e0012856..68dc7917a 100644 --- a/app/Tasks/BotReceiveMsgTask.php +++ b/app/Tasks/BotReceiveMsgTask.php @@ -427,6 +427,7 @@ class BotReceiveMsgTask extends AbstractTask private function handleWebhookRequest($sendText, $replyText, WebSocketDialogMsg $msg, WebSocketDialog $dialog, User $botUser) { $webhookUrl = null; + $userBot = null; $extras = ['timestamp' => time()]; try { @@ -530,13 +531,11 @@ class BotReceiveMsgTask extends AbstractTask return; } $userBot = UserBot::whereBotId($botUser->userid)->first(); - if ($userBot) { - $userBot->webhook_num++; - $userBot->save(); - $webhookUrl = $userBot->webhook_url; + if (!$userBot || !$userBot->shouldDispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE)) { + return; } } - if (!preg_match("/^https?:\/\//", $webhookUrl)) { + if (!$userBot && !preg_match("/^https?:\/\//", $webhookUrl)) { return; } } catch (\Exception $e) { @@ -547,50 +546,60 @@ class BotReceiveMsgTask extends AbstractTask return; } // - try { - $data = [ - 'text' => $sendText, - 'reply_text' => $replyText, - 'token' => User::generateToken($botUser), - 'session_id' => $dialog->session_id, - 'dialog_id' => $dialog->id, - 'dialog_type' => $dialog->type, - 'msg_id' => $msg->id, - 'msg_uid' => $msg->userid, - 'mention' => $this->mention ? 1 : 0, - 'bot_uid' => $botUser->userid, - 'version' => Base::getVersion(), - 'extras' => Base::array2json($extras) + $data = [ + 'text' => $sendText, + 'reply_text' => $replyText, + 'token' => User::generateToken($botUser), + 'session_id' => $dialog->session_id, + 'dialog_id' => $dialog->id, + 'dialog_type' => $dialog->type, + 'msg_id' => $msg->id, + 'msg_uid' => $msg->userid, + 'mention' => $this->mention ? 1 : 0, + 'bot_uid' => $botUser->userid, + 'version' => Base::getVersion(), + 'extras' => Base::array2json($extras) + ]; + // 添加用户信息 + $userInfo = User::find($msg->userid); + if ($userInfo) { + $data['msg_user'] = [ + 'userid' => $userInfo->userid, + 'email' => $userInfo->email, + 'nickname' => $userInfo->nickname, + 'profession' => $userInfo->profession, + 'lang' => $userInfo->lang, + 'token' => User::generateTokenNoDevice($userInfo, now()->addHour()), ]; - // 添加用户信息 - $userInfo = User::find($msg->userid); - if ($userInfo) { - $data['msg_user'] = [ - 'userid' => $userInfo->userid, - 'email' => $userInfo->email, - 'nickname' => $userInfo->nickname, - 'profession' => $userInfo->profession, - 'lang' => $userInfo->lang, - 'token' => User::generateTokenNoDevice($userInfo, now()->addHour()), - ]; - } - // 请求Webhook - $result = Ihttp::ihttp_post($webhookUrl, $data, 30); - if ($result['data'] && $data = Base::json2array($result['data'])) { - if ($data['code'] != 200 && $data['message']) { - WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [ - 'text' => $result['data']['message'] - ], $botUser->userid, false, false, true); - } - } - } catch (\Throwable $th) { - info(Base::array2json([ - 'bot_userid' => $botUser->userid, + } + + $result = null; + if ($userBot) { + $result = $userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE, $data, 30, [ 'dialog' => $dialog->id, 'msg' => $msg->id, - 'webhook_url' => $webhookUrl, - 'error' => $th->getMessage(), - ])); + ]); + } else { + try { + $result = Ihttp::ihttp_post($webhookUrl, $data, 30); + } catch (\Throwable $th) { + info(Base::array2json([ + 'bot_userid' => $botUser->userid, + 'dialog' => $dialog->id, + 'msg' => $msg->id, + 'webhook_url' => $webhookUrl, + 'error' => $th->getMessage(), + ])); + } + } + + if ($result && isset($result['data'])) { + $responseData = Base::json2array($result['data']); + if (($responseData['code'] ?? 0) != 200 && !empty($responseData['message'])) { + WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [ + 'text' => $responseData['message'] + ], $botUser->userid, false, false, true); + } } } diff --git a/database/migrations/2025_10_01_000001_add_user_bots_webhook_events.php b/database/migrations/2025_10_01_000001_add_user_bots_webhook_events.php new file mode 100644 index 000000000..31e4efb15 --- /dev/null +++ b/database/migrations/2025_10_01_000001_add_user_bots_webhook_events.php @@ -0,0 +1,39 @@ +text('webhook_events')->nullable()->after('webhook_num')->comment('Webhook事件配置'); + } + }); + + DB::table('user_bots') + ->where(function ($query) { + $query->whereNull('webhook_events')->orWhere('webhook_events', ''); + }) + ->update(['webhook_events' => json_encode(['message'])]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('user_bots', function (Blueprint $table) { + if (Schema::hasColumn('user_bots', 'webhook_events')) { + $table->dropColumn('webhook_events'); + } + }); + } +}; diff --git a/resources/assets/js/pages/manage/application.vue b/resources/assets/js/pages/manage/application.vue index f6f2c861a..388f6d9fc 100644 --- a/resources/assets/js/pages/manage/application.vue +++ b/resources/assets/js/pages/manage/application.vue @@ -106,6 +106,7 @@

ID:{{ item.id }}

{{ $L('清理时间') }}:{{ item.clear_day }}

Webhook:{{ item.webhook_url || '-' }}

+

{{ $L('Webhook事件') }}:{{ formatWebhookEvents(item.webhook_events) }}