feat: 优化打开会话事件接口,优化机器人webhook逻辑

- 新增 `open__event` 方法用于处理打开会话事件
- 移除旧的 `open__webhook` 方法
- 更新前端调用逻辑,使用新的事件接口
- 优化 webhook 事件推送逻辑,简化参数传递
This commit is contained in:
kuaifan 2025-11-06 13:59:10 +00:00
parent 130c8bf3b1
commit 4bfe33a37f
8 changed files with 492 additions and 359 deletions

View File

@ -448,6 +448,39 @@ class DialogController extends AbstractController
return Base::retSuccess('success', $data); return Base::retSuccess('success', $data);
} }
/**
* @api {get} api/dialog/open/event 打开会话事件
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName open__event
*
* @apiParam {Number} dialog_id 对话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function open__event()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
if (empty($dialog)) {
return Base::retError('打开会话失败');
}
//
Cache::remember("webhook_dialog_open_{$dialog->id}_{$user->userid}", Carbon::now()->addMinute(), function () use ($dialog, $user) {
$dialog->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_DIALOG_OPEN, $user->userid, $user->userid);
return true;
});
//
return Base::retSuccess('success');
}
/** /**
* @api {get} api/dialog/msg/list 获取消息列表 * @api {get} api/dialog/msg/list 获取消息列表
* *
@ -1258,9 +1291,6 @@ class DialogController extends AbstractController
if ($model_name) { if ($model_name) {
$msgData['model_name'] = $model_name; $msgData['model_name'] = $model_name;
} }
if (User::isBot($user->userid)) {
$msgData['force_webhook'] = true; // 强制使用webhook发送
}
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key); $result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key);
} }
} }
@ -3688,61 +3718,4 @@ class DialogController extends AbstractController
// //
return Base::retSuccess('重命名成功', $session); return Base::retSuccess('重命名成功', $session);
} }
/**
* @api {get} api/dialog/open/webhook 打开机器人会话推送 webhook
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName open__webhook
*
* @apiParam {Number} dialog_id 对话ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function open__webhook()
{
$user = User::auth();
//
$dialog_id = intval(Request::input('dialog_id'));
if (empty($dialog_id)) {
return Base::retError('错误的会话');
}
//
$dialog = WebSocketDialog::checkDialog($dialog_id);
if (empty($dialog)) {
return Base::retError('打开会话失败');
}
$data = WebSocketDialog::synthesizeData($dialog->id, $user->userid);
if ($data['bot'] == 1) {
$botTarget = User::whereUserid($data['dialog_user']['userid'])->whereBot(1)->first();
if ($botTarget) {
$userBot = UserBot::whereBotId($botTarget->userid)->first();
if ($userBot) {
// 每个机器人1分钟只触发一次 webhook
Cache::remember('webhook_dialog_open_' . $botTarget->userid, 60, function () use ($userBot, $dialog, $user) {
$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 true;
});
}
}
}
return Base::retSuccess('success');
}
} }

View File

@ -4,7 +4,6 @@ namespace App\Models;
use App\Module\Base; use App\Module\Base;
use App\Module\Doo; use App\Module\Doo;
use App\Module\Extranet;
use App\Module\Ihttp; use App\Module\Ihttp;
use App\Module\Timer; use App\Module\Timer;
use App\Tasks\JokeSoupTask; use App\Tasks\JokeSoupTask;
@ -56,43 +55,6 @@ class UserBot extends AbstractModel
'webhook_events' => 'array', '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 事件配置 * 获取 webhook 事件配置
* *
@ -140,35 +102,27 @@ class UserBot extends AbstractModel
* 发送 webhook * 发送 webhook
* *
* @param string $event * @param string $event
* @param array $payload * @param array $data
* @param int $timeout * @param int $timeout
* @param array $context
* @return array|null * @return array|null
*/ */
public function dispatchWebhook(string $event, array $payload, int $timeout = 30, array $context = []): ?array public function dispatchWebhook(string $event, array $data, int $timeout = 30): ?array
{ {
if (!$this->shouldDispatchWebhook($event)) { if (!$this->shouldDispatchWebhook($event)) {
return null; return null;
} }
$payload = array_merge([
'event' => $event,
'timestamp' => time(),
'bot_uid' => $this->bot_id,
'owner_uid' => $this->userid,
], $payload);
try { try {
$result = Ihttp::ihttp_post($this->webhook_url, $payload, $timeout); $data['event'] = $event;
$result = Ihttp::ihttp_post($this->webhook_url, $data, $timeout);
$this->increment('webhook_num'); $this->increment('webhook_num');
return $result; return $result;
} catch (Throwable $th) { } catch (Throwable $th) {
info(Base::array2json(array_merge($context, [ info(Base::array2json([
'bot_userid' => $this->bot_id,
'event' => $event,
'webhook_url' => $this->webhook_url, 'webhook_url' => $this->webhook_url,
'data' => $data,
'error' => $th->getMessage(), 'error' => $th->getMessage(),
]))); ]));
return null; return null;
} }
} }
@ -607,4 +561,42 @@ class UserBot extends AbstractModel
} }
return Base::retSuccess("创建成功。", $data); return Base::retSuccess("创建成功。", $data);
} }
/**
* 获取可选的 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
* @param bool $useFallback
* @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] : []);
}
} }

View File

@ -461,8 +461,7 @@ class WebSocketDialog extends AbstractModel
*/ */
public function joinGroup($userid, $inviter, $important = null) public function joinGroup($userid, $inviter, $important = null)
{ {
$addedUserIds = []; AbstractModel::transaction(function () use ($important, $inviter, $userid) {
AbstractModel::transaction(function () use ($important, $inviter, $userid, &$addedUserIds) {
foreach (is_array($userid) ? $userid : [$userid] as $value) { foreach (is_array($userid) ? $userid : [$userid] as $value) {
if ($value > 0) { if ($value > 0) {
$updateData = [ $updateData = [
@ -481,7 +480,6 @@ class WebSocketDialog extends AbstractModel
]); ]);
}, $isInsert); }, $isInsert);
if ($isInsert) { if ($isInsert) {
$addedUserIds[] = $value;
WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [ WebSocketDialogMsg::sendMsg(null, $this->id, 'notice', [
'notice' => User::userid2nickname($value) . " 已加入群组" 'notice' => User::userid2nickname($value) . " 已加入群组"
], $inviter, true, true); ], $inviter, true, true);
@ -492,16 +490,6 @@ class WebSocketDialog extends AbstractModel
$data = WebSocketDialog::generatePeople($this->id); $data = WebSocketDialog::generatePeople($this->id);
$data['id'] = $this->id; $data['id'] = $this->id;
$this->pushMsg("groupUpdate", $data); $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; return true;
} }
@ -515,15 +503,14 @@ class WebSocketDialog extends AbstractModel
public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true) public function exitGroup($userid, $type = 'exit', $checkDelete = true, $pushMsg = true)
{ {
$typeDesc = $type === 'remove' ? '移出' : '退出'; $typeDesc = $type === 'remove' ? '移出' : '退出';
$removedUserIds = []; AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid) {
AbstractModel::transaction(function () use ($pushMsg, $checkDelete, $typeDesc, $type, $userid, &$removedUserIds) {
$builder = WebSocketDialogUser::whereDialogId($this->id); $builder = WebSocketDialogUser::whereDialogId($this->id);
if (is_array($userid)) { if (is_array($userid)) {
$builder->whereIn('userid', $userid); $builder->whereIn('userid', $userid);
} else { } else {
$builder->whereUserid($userid); $builder->whereUserid($userid);
} }
$builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type, &$removedUserIds) { $builder->chunkById(100, function($list) use ($pushMsg, $checkDelete, $typeDesc, $type) {
/** @var WebSocketDialogUser $item */ /** @var WebSocketDialogUser $item */
foreach ($list as $item) { foreach ($list as $item) {
if ($checkDelete) { if ($checkDelete) {
@ -543,8 +530,8 @@ class WebSocketDialog extends AbstractModel
} }
} }
// //
$item->operator_id = User::userid();
$item->delete(); $item->delete();
$removedUserIds[] = $item->userid;
// //
if ($pushMsg) { if ($pushMsg) {
if ($type === 'remove') { if ($type === 'remove') {
@ -563,58 +550,17 @@ class WebSocketDialog extends AbstractModel
$data = WebSocketDialog::generatePeople($this->id); $data = WebSocketDialog::generatePeople($this->id);
$data['id'] = $this->id; $data['id'] = $this->id;
$this->pushMsg("groupUpdate", $data); $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 * 推送成员事件到机器人 webhook
* @param string $event * @param string $event
* @param array $memberIds * @param int $memberId
* @param array $meta * @param int $operatorId
* @return void * @return void
*/ */
protected function dispatchMemberWebhook(string $event, array $memberIds, array $meta = []): void public function dispatchMemberWebhook(string $event, int $memberId, int $operatorId): void
{ {
$memberIds = array_values(array_unique(array_filter($memberIds)));
if (empty($memberIds)) {
return;
}
$botIds = $this->dialogUser()->where('bot', 1)->pluck('userid')->toArray(); $botIds = $this->dialogUser()->where('bot', 1)->pluck('userid')->toArray();
if (empty($botIds)) { if (empty($botIds)) {
return; return;
@ -625,24 +571,20 @@ class WebSocketDialog extends AbstractModel
return; return;
} }
$members = $this->getUserSnapshots($memberIds); $member = User::find($memberId, ['userid', 'nickname', 'email', 'bot'])?->toArray();
if (empty($members)) { $operator = $operatorId === $memberId ? $member : User::find($operatorId, ['userid', 'nickname', 'email', 'bot'])?->toArray();
return;
}
$payload = array_merge([ $payload = [
'dialog_id' => $this->id, 'dialog_id' => $this->id,
'dialog_type' => $this->type, 'dialog_type' => $this->type,
'group_type' => $this->group_type, 'group_type' => $this->group_type,
'dialog_name' => $this->getGroupName(), 'dialog_name' => $this->getGroupName(),
'members' => $members, 'member' => $member,
], array_filter($meta, fn ($value) => $value !== null)); 'operator' => $operator,
];
foreach ($userBots as $userBot) { foreach ($userBots as $userBot) {
$userBot->dispatchWebhook($event, $payload, 10, [ $userBot->dispatchWebhook($event, $payload, 10);
'dialog' => $this->id,
'event_members' => $memberIds,
]);
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Observers; namespace App\Observers;
use App\Models\Deleted; use App\Models\Deleted;
use App\Models\UserBot;
use App\Models\WebSocketDialogUser; use App\Models\WebSocketDialogUser;
use App\Tasks\ZincSearchSyncTask; use App\Tasks\ZincSearchSyncTask;
use Carbon\Carbon; use Carbon\Carbon;
@ -31,6 +32,11 @@ class WebSocketDialogUserObserver extends AbstractObserver
} }
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid); Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray())); self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
//
$dialog = $webSocketDialogUser->webSocketDialog;
if ($dialog) {
$dialog->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_JOIN, $webSocketDialogUser->userid, intval($webSocketDialogUser->inviter));
}
} }
/** /**
@ -54,6 +60,12 @@ class WebSocketDialogUserObserver extends AbstractObserver
{ {
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid); Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
self::taskDeliver(new ZincSearchSyncTask('deleteUser', $webSocketDialogUser->toArray())); self::taskDeliver(new ZincSearchSyncTask('deleteUser', $webSocketDialogUser->toArray()));
//
$dialog = $webSocketDialogUser->webSocketDialog;
if ($dialog) {
$operatorId = $webSocketDialogUser->operator_id ?? 0;
$dialog->dispatchMemberWebhook(UserBot::WEBHOOK_EVENT_MEMBER_LEAVE, $webSocketDialogUser->userid, intval($operatorId));
}
} }
/** /**

View File

@ -75,11 +75,7 @@ class BotReceiveMsgTask extends AbstractTask
$msg->readSuccess($botUser->userid); $msg->readSuccess($botUser->userid);
// 判断消息是否是机器人发送的则不处理,避免循环 // 判断消息是否是机器人发送的则不处理,避免循环
if ((!$msg->user || $msg->user->bot)) { if (!$msg->user || $msg->user->bot) {
$msgData = Base::json2array($msg->msg);
if (Base::val($msgData, 'force_webhook') && $msg->webSocketDialog) {
$this->handleWebhookRequest($msgData['text'], null, $msg, $msg->webSocketDialog, $botUser);
}
return; return;
} }
@ -539,9 +535,6 @@ class BotReceiveMsgTask extends AbstractTask
return; return;
} }
} }
if (!$userBot && !preg_match("/^https?:\/\//", $webhookUrl)) {
return;
}
} catch (\Exception $e) { } catch (\Exception $e) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [ WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'template', [
'type' => 'content', 'type' => 'content',
@ -549,8 +542,10 @@ class BotReceiveMsgTask extends AbstractTask
], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务 ], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
return; return;
} }
//
// 基本请求数据
$data = [ $data = [
'event' => UserBot::WEBHOOK_EVENT_MESSAGE,
'text' => $sendText, 'text' => $sendText,
'reply_text' => $replyText, 'reply_text' => $replyText,
'token' => User::generateToken($botUser), 'token' => User::generateToken($botUser),
@ -561,8 +556,9 @@ class BotReceiveMsgTask extends AbstractTask
'msg_uid' => $msg->userid, 'msg_uid' => $msg->userid,
'mention' => $this->mention ? 1 : 0, 'mention' => $this->mention ? 1 : 0,
'bot_uid' => $botUser->userid, 'bot_uid' => $botUser->userid,
'extras' => Base::array2json($extras),
'version' => Base::getVersion(), 'version' => Base::getVersion(),
'extras' => Base::array2json($extras) 'timestamp' => time(),
]; ];
// 添加用户信息 // 添加用户信息
$userInfo = User::find($msg->userid); $userInfo = User::find($msg->userid);
@ -579,19 +575,14 @@ class BotReceiveMsgTask extends AbstractTask
$result = null; $result = null;
if ($userBot) { if ($userBot) {
$result = $userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE, $data, 30, [ $result = $userBot->dispatchWebhook(UserBot::WEBHOOK_EVENT_MESSAGE, $data);
'dialog' => $dialog->id,
'msg' => $msg->id,
]);
} else { } else {
try { try {
$result = Ihttp::ihttp_post($webhookUrl, $data, 30); $result = Ihttp::ihttp_post($webhookUrl, $data, 30);
} catch (\Throwable $th) { } catch (\Throwable $th) {
info(Base::array2json([ info(Base::array2json([
'bot_userid' => $botUser->userid,
'dialog' => $dialog->id,
'msg' => $msg->id,
'webhook_url' => $webhookUrl, 'webhook_url' => $webhookUrl,
'data' => $data,
'error' => $th->getMessage(), 'error' => $th->getMessage(),
])); ]));
} }
@ -599,7 +590,7 @@ class BotReceiveMsgTask extends AbstractTask
if ($result && isset($result['data'])) { if ($result && isset($result['data'])) {
$responseData = Base::json2array($result['data']); $responseData = Base::json2array($result['data']);
if (($responseData['code'] ?? 0) != 200 && !empty($responseData['message'])) { if (($responseData['code'] ?? 0) === 200 && !empty($responseData['message'])) {
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [ WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', [
'text' => $responseData['message'] 'text' => $responseData['message']
], $botUser->userid, false, false, true); ], $botUser->userid, false, false, true);
@ -722,7 +713,7 @@ class BotReceiveMsgTask extends AbstractTask
</role_setting> </role_setting>
EOF; EOF;
} }
// 上下文信息(项目、任务、部门等)+ 操作指令 // 上下文信息(项目、任务、部门等)+ 操作指令
switch ($dialog->type) { switch ($dialog->type) {
// 用户对话 // 用户对话
@ -757,7 +748,7 @@ class BotReceiveMsgTask extends AbstractTask
项目状态:{$projectStatus} 项目状态:{$projectStatus}
</context_info> </context_info>
EOF; EOF;
$sections[] = <<<EOF $sections[] = <<<EOF
<instructions> <instructions>
如果你判断我想要或需要添加任务,请按照以下格式回复: 如果你判断我想要或需要添加任务,请按照以下格式回复:
@ -787,7 +778,7 @@ class BotReceiveMsgTask extends AbstractTask
{$taskContext} {$taskContext}
</context_info> </context_info>
EOF; EOF;
$sections[] = <<<EOF $sections[] = <<<EOF
<instructions> <instructions>
如果你判断我想要或需要添加子任务,请按照以下格式回复: 如果你判断我想要或需要添加子任务,请按照以下格式回复:
@ -822,7 +813,7 @@ class BotReceiveMsgTask extends AbstractTask
EOF; EOF;
break; break;
} }
// 聊天历史 // 聊天历史
if ($dialog->type === 'group') { if ($dialog->type === 'group') {
$chatHistory = $this->getRecentChatHistory($dialog, 15); $chatHistory = $this->getRecentChatHistory($dialog, 15);
@ -836,7 +827,7 @@ class BotReceiveMsgTask extends AbstractTask
} }
break; break;
} }
// 更新系统提示词 // 更新系统提示词
if (!empty($sections)) { if (!empty($sections)) {
$extras['system_message'] = implode("\n\n", $sections); $extras['system_message'] = implode("\n\n", $sections);

View File

@ -4,6 +4,7 @@
<script> <script>
import DialogMarkdown from "../../DialogMarkdown.vue"; import DialogMarkdown from "../../DialogMarkdown.vue";
import {languageName} from "../../../../../language";
export default { export default {
components: {DialogMarkdown}, components: {DialogMarkdown},
@ -11,130 +12,357 @@ export default {
msg: Object, msg: Object,
}, },
data() { data() {
return {}; return {
isChinese: /^zh/.test(languageName),
chineseTemplate: `
## API 使用说明
---
## 机器人主动发送消息
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">发送文本消息接口</span>
</summary>
<br>
**功能说明**开发者可以通过调用此API接口让机器人主动向指定的对话群组或私聊发送文本消息这是一个主动推送接口适用于机器人需要定时通知告警提醒等主动发送消息的场景
#### 接口信息
| 属性 | 结果 |
|------|------|
| **请求方式** | POST |
| **接口地址** | \`{{sendApiUrl}}\` |
| **认证方式** | 通过请求头中的 version token 进行认证 |
| **超时时间** | 30 |
#### 请求头
| 参数名 | | 必填 | 说明 |
|--------|-----|------|------|
| \`version\` | \`{{version}}\` | √ | 系统版本号 |
| \`token\` | 机器人Token | √ | 机器人的访问令牌,可在机器人设置中获取 |
#### 请求参数
| 参数名 | 说明 | 类型 | 必填 | 示例值 |
|--------|------|------|------|--------|
| \`dialog_id\` | 对话ID | string | √ | |
| \`text\` | 消息内容 | string | √ | |
| \`text_type\` | 文本类型 | string | | html 或 md |
| \`key\` | 消息唯一标识 | string | | 留空则自动生成 |
| \`silence\` | 静默模式 | string | | yes 或 no |
| \`reply_id\` | 回复消息ID | string | | |
</details>
---
## Webhook 事件推送
**功能说明**当特定事件发生时系统会自动向机器人配置的 Webhook 地址发送 POST 请求推送事件数据这是一个被动接收机制适用于机器人需要响应用户消息监听群组事件等场景
**重要提示**
- 请确保 Webhook 地址可正常访问且能在超时时间内响应
- 建议对推送的数据进行签名验证确保数据来源可信
- Webhook 接口应尽快返回响应200 OK复杂业务逻辑建议异步处理
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">接收消息事件<code>message</code></span>
</summary>
<br>
**触发时机**当机器人在对话中收到新消息时包括被@提及或私聊消息系统会自动推送到配置的 Webhook 地址
**超时时间**30
**使用场景**智能问答关键词回复消息记录自动客服等
#### 推送参数
| 参数名 | 说明 | 类型 |
|--------|------|------|
| \`event\` | 事件类型,固定值 \`message\` | string |
| \`text\` | 消息的文本内容 | string |
| \`reply_text\` | 如果是回复消息,则包含被回复消息的文本内容 | string |
| \`token\` | 机器人Token | string |
| \`session_id\` | 会话ID {{sessionDesc}} | string |
| \`dialog_id\` | 对话ID | string |
| \`dialog_type\` | 对话类型 | string |
| \`msg_id\` | 消息ID | string |
| \`msg_uid\` | 消息发送人的用户ID | string |
| \`msg_user\` | 消息发送人的详细信息(昵称、头像等) | object |
| \`mention\` | 机器人是否被@提及 | boolean |
| \`bot_uid\` | 机器人ID | string |
| \`version\` | 系统版本 | string |
</details>
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">打开会话事件<code>dialog_open</code></span>
</summary>
<br>
**触发时机**当用户打开与机器人的会话窗口时首次打开或重新进入系统会推送此事件
**超时时间**10
**使用场景**欢迎语菜单展示会话初始化用户行为统计等
#### 推送参数
| 参数名 | 说明 | 类型 |
|--------|------|------|
| \`event\` | 事件类型,固定值 \`dialog_open\` | string |
| \`dialog_id\` | 对话ID | number |
| \`dialog_type\` | 对话类型 | string |
| \`group_type\` | 群组类型(仅群组对话时有值) | string |
| \`dialog_name\` | 对话名称(群组名称或用户昵称) | string |
| \`member\` | 打开会话的成员信息 | object |
| \`operator\` | 操作人信息(通常与 member 相同) | object |
</details>
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">成员加入事件<code>member_join</code></span>
</summary>
<br>
**触发时机**当新成员加入机器人所在的群组时系统会推送此事件
**超时时间**10
**使用场景**新成员欢迎语群规则提醒自动分配权限成员统计等
#### 推送参数
| 参数名 | 说明 | 类型 |
|--------|------|------|
| \`event\` | 事件类型,固定值 \`member_join\` | string |
| \`dialog_id\` | 群组对话ID | number |
| \`dialog_type\` | 对话类型,此处固定为 \`group\` | string |
| \`group_type\` | 群组类型 | string |
| \`dialog_name\` | 群组名称 | string |
| \`member\` | 加入的成员信息昵称、ID等 | object |
| \`operator\` | 操作人信息(邀请人,如果是自己加入则与 member 相同) | object |
</details>
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">成员退出事件<code>member_leave</code></span>
</summary>
<br>
**触发时机**当成员退出或被移出机器人所在的群组时系统会推送此事件
**超时时间**10
**使用场景**成员变动记录权限清理离职提醒成员统计等
#### 推送参数
| 参数名 | 说明 | 类型 |
|--------|------|------|
| \`event\` | 事件类型,固定值 \`member_leave\` | string |
| \`dialog_id\` | 群组对话ID | number |
| \`dialog_type\` | 对话类型,此处固定为 \`group\` | string |
| \`group_type\` | 群组类型 | string |
| \`dialog_name\` | 群组名称 | string |
| \`member\` | 退出的成员信息昵称、ID等 | object |
| \`operator\` | 操作人信息(踢出者,如果是自己退出则与 member 相同) | object |
</details>
---
**提示**请妥善保管机器人 Token确保 Webhook 接口稳定可用并及时响应更多帮助请发送 <span class="mark-color mark-set">/help</span> 命令查看
`,
englishTemplate: `
## API Documentation
---
## 1. Bot Proactive Message Sending
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">Send Text Message API</span>
</summary>
<br>
**Description**: Developers can call this API to allow the bot to proactively send text messages to specified conversations (groups or private chats). This is a proactive push interface, suitable for scenarios where the bot needs to send scheduled notifications, alerts, and other proactive messages.
#### API Information
| Property | Value |
|------|------|
| **Request Method** | POST |
| **API Endpoint** | \`{{sendApiUrl}}\` |
| **Authentication** | Authenticate via version and token in request headers |
| **Timeout** | 30 seconds |
#### Request Headers
| Parameter | Value | Required | Description |
|--------|-----|------|------|
| \`version\` | \`{{version}}\` | √ | System version number |
| \`token\` | Bot Token | √ | Bot access token, available in bot settings |
#### Request Parameters
| Parameter | Description | Type | Required | Example |
|--------|------|------|------|--------|
| \`dialog_id\` | Dialog ID | string | √ | |
| \`text\` | Message content | string | √ | |
| \`text_type\` | Text type | string | | html or md |
| \`key\` | Unique message identifier | string | | Auto-generated if left empty |
| \`silence\` | Silent mode | string | | yes or no |
| \`reply_id\` | Reply message ID | string | | |
</details>
---
## 2. Webhook Event Push
**Description**: When specific events occur, the system will automatically send POST requests to the Webhook URL configured for the bot, pushing event data. This is a passive receiving mechanism, suitable for scenarios where the bot needs to respond to user messages, monitor group events, etc.
**Important Notes**:
- Ensure the Webhook URL is accessible and can respond within the timeout period
- Recommend verifying the signature of pushed data to ensure the source is trustworthy
- Webhook interface should return a response (200 OK) as quickly as possible; complex business logic should be handled asynchronously
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">Message Received Event (<code>message</code>)</span>
</summary>
<br>
**Trigger Timing**: When the bot receives a new message in a conversation (including being @mentioned or private messages), the system will automatically push to the configured Webhook URL.
**Timeout**: 30 seconds
**Use Cases**: Intelligent Q&A, keyword replies, message logging, automatic customer service, etc.
#### Push Parameters
| Parameter | Description | Type |
|--------|------|------|
| \`event\` | Event type, fixed value \`message\` | string |
| \`text\` | Text content of the message | string |
| \`reply_text\` | If it's a reply message, contains the text content of the replied message | string |
| \`token\` | Bot Token | string |
| \`session_id\` | Session ID {{sessionDesc}} | string |
| \`dialog_id\` | Dialog ID | string |
| \`dialog_type\` | Dialog type | string |
| \`msg_id\` | Message ID | string |
| \`msg_uid\` | User ID of message sender | string |
| \`msg_user\` | Detailed information of message sender (nickname, avatar, etc.) | object |
| \`mention\` | Whether the bot was @mentioned | boolean |
| \`bot_uid\` | Bot ID | string |
| \`version\` | System version | string |
</details>
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">Dialog Open Event (<code>dialog_open</code>)</span>
</summary>
<br>
**Trigger Timing**: When a user opens a conversation window with the bot (first time or re-entry), the system will push this event.
**Timeout**: 10 seconds
**Use Cases**: Welcome messages, menu display, session initialization, user behavior statistics, etc.
#### Push Parameters
| Parameter | Description | Type |
|--------|------|------|
| \`event\` | Event type, fixed value \`dialog_open\` | string |
| \`dialog_id\` | Dialog ID | number |
| \`dialog_type\` | Dialog type | string |
| \`group_type\` | Group type (only for group conversations) | string |
| \`dialog_name\` | Dialog name (group name or user nickname) | string |
| \`member\` | Information of member opening the session | object |
| \`operator\` | Operator information (usually same as member) | object |
</details>
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">Member Join Event (<code>member_join</code>)</span>
</summary>
<br>
**Trigger Timing**: When a new member joins a group where the bot is present, the system will push this event.
**Timeout**: 10 seconds
**Use Cases**: New member welcome messages, group rules reminders, automatic permission assignment, member statistics, etc.
#### Push Parameters
| Parameter | Description | Type |
|--------|------|------|
| \`event\` | Event type, fixed value \`member_join\` | string |
| \`dialog_id\` | Group dialog ID | number |
| \`dialog_type\` | Dialog type, fixed as \`group\` | string |
| \`group_type\` | Group type | string |
| \`dialog_name\` | Group name | string |
| \`member\` | Information of joining member (nickname, ID, etc.) | object |
| \`operator\` | Operator information (inviter, same as member if self-joined) | object |
</details>
<details>
<summary>
<span style="font-size:1.25em;font-weight:bold;padding-left:8px">Member Leave Event (<code>member_leave</code>)</span>
</summary>
<br>
**Trigger Timing**: When a member leaves or is removed from a group where the bot is present, the system will push this event.
**Timeout**: 10 seconds
**Use Cases**: Member change records, permission cleanup, departure reminders, member statistics, etc.
#### Push Parameters
| Parameter | Description | Type |
|--------|------|------|
| \`event\` | Event type, fixed value \`member_leave\` | string |
| \`dialog_id\` | Group dialog ID | number |
| \`dialog_type\` | Dialog type, fixed as \`group\` | string |
| \`group_type\` | Group type | string |
| \`dialog_name\` | Group name | string |
| \`member\` | Information of leaving member (nickname, ID, etc.) | object |
| \`operator\` | Operator information (remover, same as member if self-left) | object |
</details>
---
**Tip**: Please keep the bot Token secure, ensure the Webhook interface is stable and responds promptly. For more help, send the <span class="mark-color mark-set">/help</span> command.
`,
};
}, },
computed: { computed: {
content() { content() {
const sessionDesc = !/^(ai-|user-session-)/.test(this.msg.email) ? " <span style='color:#999;padding-left:4px;'>({{该机器人不支持}})</span>" : ""; const variables = {
return [ sendApiUrl: $A.apiUrl('dialog/msg/sendtext'),
"## {{API 使用说明}}", version: this.msg.version,
"", sessionDesc: !/^(ai-|user-session-)/.test(this.msg.email) ? ` <span style='color:#999;padding-left:4px;'>(${this.$L('该机器人不支持')})</span>` : ``,
"### 1. {{发送文本消息}}", };
"", const template = this.isChinese ? this.chineseTemplate : this.englishTemplate;
"{{开发者可以通过此接口调用机器人向指定对话发送文本消息。}}", return template.replace(/\{\{([^}]+)\}\}/g, (_, v1) => variables[v1] || v1).trim();
"",
"#### {{接口信息}}",
"",
"| {{属性}} | {{结果}} |",
"|------|------|",
"| **{{请求方式}}** | POST |",
"| **{{接口地址}}** | `" + $A.apiUrl('dialog/msg/sendtext') + "` |",
"| **{{说明}}** | {{通过机器人向指定对话发送文本消息}} |",
"",
"#### {{请求头}}",
"",
"| {{参数名}} | {{值}} | {{必填}} |",
"|--------|-----|------|",
"| `version` | `" + this.msg.version + "` | √ |",
"| `token` | {{机器人Token}} | √ |",
"",
"#### {{请求参数}}",
"",
"| {{参数名}} | {{说明}} | {{类型}} | {{必填}} | {{示例值}} |",
"|--------|------|------|------|--------|",
"| `dialog_id` | {{对话ID}} | string | √ | |",
"| `text` | {{消息内容}} | string | √ | |",
"| `text_type` | {{文本类型}} | string | | {{html 或 md}} |",
"| `key` | {{搜索词}} | string | | {{留空自动生成}} |",
"| `silence` | {{静默模式}} | string | | {{yes 或 no}} |",
"| `reply_id` | {{回复指定消息ID}} | string | | |",
"",
"### 2. {{Webhook 消息推送}}",
"",
"{{机器人收到消息后会自动POST推送到配置的Webhook地址请求超时为10秒。}}",
"",
"#### {{推送参数}}",
"",
"| {{参数名}} | {{说明}} | {{类型}} |",
"|--------|------|------|",
"| `text` | {{消息文本内容}} | string |",
"| `reply_text` | {{回复/引用的消息文本}} | string |",
"| `token` | {{机器人Token}} | string |",
"| `session_id` | {{会话ID}}" + sessionDesc + " | string |",
"| `dialog_id` | {{对话ID}} | string |",
"| `dialog_type` | {{对话类型}} | string |",
"| `msg_id` | {{消息ID}} | string |",
"| `msg_uid` | {{消息发送人ID}} | string |",
"| `msg_user` | {{消息发送人信息}} | object |",
"| `mention` | {{是否被@到}} | boolean |",
"| `bot_uid` | {{机器人ID}} | string |",
"| `version` | {{系统版本}} | string |",
"### 3. {{打开会话 消息推送}}",
"",
"{{打开机器人会话后会自动POST推送到配置的Webhook地址请求超时为30秒。}}",
"",
"#### {{推送参数}}",
"",
"| {{参数名}} | {{说明}} | {{类型}} |",
"|--------|------|------|",
"| `event` | {{推送事件}} | string |",
"| `timestamp` | {{推送时间戳}} | string |",
"| `dialog_id` | {{对话ID}} | string |",
"| `dialog_type` | {{对话类型}} | string |",
"| `bot_uid` | {{机器人ID}} | string |",
"| `owner_uid` | {{机器人所属用户ID}} | string |",
"| `user` | {{机器人所属用户信息}} | object |",
"| `user.userid` | {{用户ID}} | string |",
"| `user.email` | {{用户邮箱}} | string |",
"| `user.nickname` | {{用户昵称}} | string |",
"### 4. {{成员加入 消息推送}}",
"",
"{{成员加入群组后会自动POST推送到配置的Webhook地址请求超时为30秒。}}",
"",
"#### {{推送参数}}",
"",
"| {{参数名}} | {{说明}} | {{类型}} |",
"|--------|------|------|",
"| `event` | {{推送事件}} | string |",
"| `timestamp` | {{推送时间戳}} | string |",
"| `dialog_id` | {{对话ID}} | string |",
"| `dialog_type` | {{对话类型}} | string |",
"| `bot_uid` | {{机器人ID}} | string |",
"| `owner_uid` | {{机器人所属用户ID}} | string |",
"| `user` | {{机器人所属用户信息}} | object |",
"| `user.userid` | {{用户ID}} | string |",
"| `user.email` | {{用户邮箱}} | string |",
"| `user.nickname` | {{用户昵称}} | string |",
"### 5. {{成员退出 消息推送}}",
"",
"{{成员退出群组后会自动POST推送到配置的Webhook地址请求超时为30秒。}}",
"",
"#### {{推送参数}}",
"",
"| {{参数名}} | {{说明}} | {{类型}} |",
"|--------|------|------|",
"| `event` | {{推送事件}} | string |",
"| `timestamp` | {{推送时间戳}} | string |",
"| `dialog_id` | {{对话ID}} | string |",
"| `dialog_type` | {{对话类型}} | string |",
"| `dialog_name` | {{对话名称}} | string |",
"| `group_type` | {{群组类型}} | string |",
"| `bot_uid` | {{机器人ID}} | string |",
"| `owner_uid` | {{机器人所属用户ID}} | string |",
"| `action` | {{动作}} | string |",
"| `actor` | {{操作人信息}} | object |",
"| `actor.userid` | {{用户ID}} | string |",
"| `actor.email` | {{用户邮箱}} | string |",
"| `actor.nickname` | {{用户昵称}} | string |",
"| `actor.is_bot` | {{是否机器人}} | boolean |",
"| `members` | {{成员信息}} | array |",
"| `members.userid` | {{用户ID}} | string |",
"| `members.email` | {{用户邮箱}} | string |",
"| `members.nickname` | {{用户昵称}} | string |",
"| `members.is_bot` | {{是否机器人}} | boolean |",
].map(item => item.replace(/\{\{([^}]+)\}\}/g, (_, v1) => this.$L(v1))).join("\n");
}, },
}, },
methods: {}, methods: {},
} }
</script> </script>

View File

@ -1241,8 +1241,7 @@ export default {
this.getDialogBase(dialog_id) this.getDialogBase(dialog_id)
this.generateUnreadData(old_id) this.generateUnreadData(old_id)
// //
this.$store.dispatch('openDialogWebhook', dialog_id) this.$store.dispatch('openDialogEvent', dialog_id)
//
this.$store.dispatch('closeDialog', {id: old_id}) this.$store.dispatch('closeDialog', {id: old_id})
// //
window.localStorage.removeItem('__cache:vote__') window.localStorage.removeItem('__cache:vote__')

View File

@ -3514,35 +3514,6 @@ export default {
}) })
}, },
/**
* 打开会话打开机器人会话推送 webhook
* @param state
* @param dispatch
* @param dialogId
* @returns {Promise<unknown>}
*/
openDialogWebhook({state, dispatch}, dialogId) {
return new Promise((resolve, reject) => {
const dialog = state.cacheDialogs.find(item => {
if (item.type !== 'user') {
return false
}
return item.id === dialogId
});
if (dialog && dialog.bot === 1) {
dispatch("call", {
url: 'dialog/open/webhook',
data: {
dialog_id: dialogId,
},
}).catch(e => {
console.warn(e);
reject(e);
})
}
});
},
/** /**
* 打开会话通过会员ID打开个人会话 * 打开会话通过会员ID打开个人会话
* @param state * @param state
@ -3576,6 +3547,31 @@ export default {
}); });
}, },
/**
* 打开会话事件
* @param state
* @param dispatch
* @param dialogId
* @returns {Promise<unknown>}
*/
openDialogEvent({state, dispatch}, dialogId) {
return new Promise((resolve, reject) => {
if (!dialogId) {
reject({msg: 'Parameter error'});
return;
}
dispatch("call", {
url: 'dialog/open/event',
data: {
dialog_id: dialogId,
},
}).catch(e => {
console.warn(e);
reject(e);
})
});
},
/** /**
* 打开会话客户端新窗口 * 打开会话客户端新窗口
* @param state * @param state