From 69c66053b76097f6e4baddd5e05c603686d89ae6 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Fri, 7 Nov 2025 22:03:11 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84AI=E5=8A=A9=E6=89=8B?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D=E6=95=B4=E7=90=86=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E6=B5=81=E5=BC=8F=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=86=97=E4=BD=99=E6=95=B0=E6=8D=AE=E8=A1=A8=E5=92=8C=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Api/AssistantController.php | 251 +++++++++++ app/Http/Controllers/Api/DialogController.php | 133 +----- app/Models/WebSocketDialogMsg.php | 21 +- app/Models/WebSocketDialogMsgExtra.php | 56 --- app/Tasks/BotReceiveMsgTask.php | 12 +- ...ate_web_socket_dialog_msg_extras_table.php | 35 -- .../assets/js/components/AIAssistant.vue | 395 +++++++++--------- .../manage/components/ChatInput/index.vue | 10 +- resources/assets/js/store/actions.js | 6 +- routes/web.php | 4 + 10 files changed, 457 insertions(+), 466 deletions(-) create mode 100644 app/Http/Controllers/Api/AssistantController.php delete mode 100644 app/Models/WebSocketDialogMsgExtra.php delete mode 100644 database/migrations/2025_11_07_183944_create_web_socket_dialog_msg_extras_table.php diff --git a/app/Http/Controllers/Api/AssistantController.php b/app/Http/Controllers/Api/AssistantController.php new file mode 100644 index 000000000..268b8da5c --- /dev/null +++ b/app/Http/Controllers/Api/AssistantController.php @@ -0,0 +1,251 @@ +checkChatInformation(); + // + $dialog_id = intval(Request::input('dialog_id')); + $content = trim(Request::input('content', '')); + if ($dialog_id <= 0) { + return Base::retError('参数错误'); + } + + $dialog = WebSocketDialog::checkDialog($dialog_id); + + // 基本信息 + $context = [ + 'dialog_name' => $dialog->name ?: '', + 'dialog_type' => $dialog->type ?: '', + 'group_type' => $dialog->group_type ?: '', + ]; + + // 当前草稿 + $draft = Request::input('draft', ''); + if (is_string($draft) && trim($draft) !== '') { + $context['current_draft'] = Base::html2markdown($draft); + } + + // 引用消息 + $quote_id = intval(Request::input('quote_id')); + if ($quote_id > 0) { + $quote = WebSocketDialogMsg::whereDialogId($dialog_id) + ->whereId($quote_id) + ->with('user') + ->first(); + if ($quote) { + $context['quote_summary'] = WebSocketDialogMsg::previewMsg($quote); + $context['quote_user'] = $quote->user->nickname ?? ''; + } + } + + // 成员列表 + $members = WebSocketDialogUser::whereDialogId($dialog_id) + ->join('users', 'users.userid', '=', 'web_socket_dialog_users.userid') + ->orderBy('web_socket_dialog_users.id') + ->limit(10) + ->pluck('users.nickname') + ->filter() + ->values() + ->all(); + if (!empty($members)) { + $context['members'] = $members; + } + + // 最近消息 + $recentMessagesQuery = WebSocketDialogMsg::whereDialogId($dialog_id) + ->orderByDesc('id') + ->with('user'); + + $recentMessages = (clone $recentMessagesQuery)->take(15)->get(); + if ($recentMessages->isNotEmpty()) { + $formatRecentMessages = function ($messages) { + return $messages->reverse()->map(function ($msg) { + return [ + 'sender' => $msg->user->nickname ?? ('用户' . $msg->userid), + 'summary' => $msg->extractMessageContent(500), + ]; + })->filter(function ($item) { + return !empty($item['summary']); + })->values(); + }; + + $formattedRecentMessages = $formatRecentMessages($recentMessages); + $summaryLength = $formattedRecentMessages->sum(function ($item) { + return mb_strlen($item['summary']); + }); + + if ($summaryLength < 500 && $recentMessages->count() === 15) { + $lastMessageId = optional($recentMessages->last())->id; + $additionalMessages = collect(); + if ($lastMessageId) { + $additionalMessages = (clone $recentMessagesQuery) + ->where('id', '<', $lastMessageId) + ->take(10) + ->get(); + } + if ($additionalMessages->isNotEmpty()) { + $recentMessages = $recentMessages->concat($additionalMessages); + $formattedRecentMessages = $formatRecentMessages($recentMessages); + } + } + + if ($formattedRecentMessages->isNotEmpty()) { + $context['recent_messages'] = $formattedRecentMessages->all(); + } + } + + // 生成消息 + $systemPrompt = AI::messageSystemPrompt(); + $contextPrompt = AI::buildMessageContextPrompt($context); + if ($content) { + $contextPrompt .= "\n\n请根据以上信息,结合提示词生成一条待发送的消息:"; + } + + return Base::retSuccess('success', [ + 'system_prompt' => $systemPrompt, + 'context_prompt' => $contextPrompt, + ]); + } + + /** + * @api {post} api/assistant/auth 生成授权码 + * + * @apiDescription 需要token身份,生成 AI 流式会话的 stream_key + * @apiVersion 1.0.0 + * @apiGroup assistant + * @apiName auth + * + * @apiParam {String} model_type 模型类型 + * @apiParam {String} model_name 模型名称 + * @apiParam {JSON} context 上下文数组 + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + * @apiSuccess {String} data.stream_key 流式会话凭证 + */ + public function auth() + { + $user = User::auth(); + $user->checkChatInformation(); + + $modelType = trim(Request::input('model_type', '')); + $modelName = trim(Request::input('model_name', '')); + $contextInput = Request::input('context', []); + + if ($modelType === '' || $modelName === '') { + return Base::retError('参数错误'); + } + + if (is_string($contextInput)) { + $decoded = json_decode($contextInput, true); + if (json_last_error() === JSON_ERROR_NONE) { + $contextInput = $decoded; + } + } + if (!is_array($contextInput)) { + return Base::retError('context 参数格式错误'); + } + + $context = []; + foreach ($contextInput as $item) { + if (!is_array($item) || count($item) < 2) { + continue; + } + $role = trim((string)($item[0] ?? '')); + $message = trim((string)($item[1] ?? '')); + if ($role === '' || $message === '') { + continue; + } + $context[] = [$role, $message]; + } + + $contextJson = json_encode($context, JSON_UNESCAPED_UNICODE); + if ($contextJson === false) { + return Base::retError('context 参数格式错误'); + } + + $setting = Base::setting('aibotSetting'); + $apiKey = Base::val($setting, $modelType . '_key'); + if ($modelType === 'wenxin') { + $wenxinSecret = Base::val($setting, 'wenxin_secret'); + if ($wenxinSecret) { + $apiKey = trim(($apiKey ?: '') . ':' . $wenxinSecret); + } + } + if ($modelType === 'ollama' && empty($apiKey)) { + $apiKey = Base::strRandom(6); + } + if (empty($apiKey)) { + return Base::retError('模型未启用'); + } + + $remoteModelType = match ($modelType) { + 'qianwen' => 'qwen', + default => $modelType, + }; + + $authResult = Ihttp::ihttp_post('http://nginx/ai/invoke/auth', [ + 'api_key' => $apiKey, + 'model_type' => $remoteModelType, + 'model_name' => $modelName, + 'context' => $contextJson, + ], 30); + + if (Base::isError($authResult)) { + return Base::retError($authResult['msg']); + } + + $body = Base::json2array($authResult['data']); + if ($body['code'] !== 200) { + return Base::retError($body['error'] ?: 'AI 接口返回异常', $body); + } + + $streamKey = Base::val($body, 'data.stream_key'); + if (empty($streamKey)) { + return Base::retError('AI 接口返回数据异常'); + } + + return Base::retSuccess('success', [ + 'stream_key' => $streamKey, + ]); + } +} diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 65ab05e68..4f7373a9e 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -1026,133 +1026,6 @@ class DialogController extends AbstractController return Base::retSuccess('success'); } - /** - * @api {post} api/dialog/msg/aiprompt AI 提示词助手 - * - * @apiDescription 需要token身份,整理当前会话的系统提示词与上下文信息 - * @apiVersion 1.0.0 - * @apiGroup dialog - * @apiName msg__aiprompt - * - * @apiParam {Number} dialog_id 对话ID - * @apiParam {String} content 消息需求描述(用于提示词整理,可为空) - * @apiParam {String} [draft] 当前草稿内容(HTML 格式) - * @apiParam {Number} [quote_id] 引用消息ID - * - * @apiSuccess {Number} ret 返回状态码(1正确、0错误) - * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据 - * @apiSuccess {String} data.system_prompt AI 使用的系统提示词 - * @apiSuccess {String} data.context_prompt AI 使用的上下文提示词 - */ - public function msg__aiprompt() - { - $user = User::auth(); - $user->checkChatInformation(); - // - $dialog_id = intval(Request::input('dialog_id')); - $content = trim(Request::input('content', '')); - if ($dialog_id <= 0) { - return Base::retError('参数错误'); - } - - $dialog = WebSocketDialog::checkDialog($dialog_id); - - // 基本信息 - $context = [ - 'dialog_name' => $dialog->name ?: '', - 'dialog_type' => $dialog->type ?: '', - 'group_type' => $dialog->group_type ?: '', - ]; - - // 当前草稿 - $draft = Request::input('draft', ''); - if (is_string($draft) && trim($draft) !== '') { - $context['current_draft'] = Base::html2markdown($draft); - } - - // 引用消息 - $quote_id = intval(Request::input('quote_id')); - if ($quote_id > 0) { - $quote = WebSocketDialogMsg::whereDialogId($dialog_id) - ->whereId($quote_id) - ->with('user') - ->first(); - if ($quote) { - $context['quote_summary'] = WebSocketDialogMsg::previewMsg($quote); - $context['quote_user'] = $quote->user->nickname ?? ''; - } - } - - // 成员列表 - $members = WebSocketDialogUser::whereDialogId($dialog_id) - ->join('users', 'users.userid', '=', 'web_socket_dialog_users.userid') - ->orderBy('web_socket_dialog_users.id') - ->limit(10) - ->pluck('users.nickname') - ->filter() - ->values() - ->all(); - if (!empty($members)) { - $context['members'] = $members; - } - - // 最近消息 - $recentMessagesQuery = WebSocketDialogMsg::whereDialogId($dialog_id) - ->orderByDesc('id') - ->with('user'); - - $recentMessages = (clone $recentMessagesQuery)->take(15)->get(); - if ($recentMessages->isNotEmpty()) { - $formatRecentMessages = function ($messages) { - return $messages->reverse()->map(function ($msg) { - return [ - 'sender' => $msg->user->nickname ?? ('用户' . $msg->userid), - 'summary' => $msg->extractMessageContent(500), - ]; - })->filter(function ($item) { - return !empty($item['summary']); - })->values(); - }; - - $formattedRecentMessages = $formatRecentMessages($recentMessages); - $summaryLength = $formattedRecentMessages->sum(function ($item) { - return mb_strlen($item['summary']); - }); - - if ($summaryLength < 500 && $recentMessages->count() === 15) { - $lastMessageId = optional($recentMessages->last())->id; - $additionalMessages = collect(); - if ($lastMessageId) { - $additionalMessages = (clone $recentMessagesQuery) - ->where('id', '<', $lastMessageId) - ->take(10) - ->get(); - } - if ($additionalMessages->isNotEmpty()) { - $recentMessages = $recentMessages->concat($additionalMessages); - $formattedRecentMessages = $formatRecentMessages($recentMessages); - } - } - - if ($formattedRecentMessages->isNotEmpty()) { - $context['recent_messages'] = $formattedRecentMessages->all(); - } - } - - // 生成消息 - $systemPrompt = AI::messageSystemPrompt(); - $contextPrompt = AI::buildMessageContextPrompt($context); - if ($content) { - $contextPrompt .= "\n\n请根据以上信息,结合提示词生成一条待发送的消息:\n\n"; - } - - return Base::retSuccess('success', [ - 'system_prompt' => $systemPrompt, - 'context_prompt' => $contextPrompt, - ]); - } - /** * @api {post} api/dialog/msg/sendtext 发送消息 * @@ -1180,7 +1053,6 @@ class DialogController extends AbstractController * - no: 正常发送(默认) * - yes: 静默发送 * @apiParam {String} [model_name] 模型名称(仅AI机器人支持) - * @apiParam {Object} [extra_data] 附加数据(保存到附加表) * * @apiSuccess {Number} ret 返回状态码(1正确、0错误) * @apiSuccess {String} msg 返回信息(错误描述) @@ -1202,7 +1074,6 @@ class DialogController extends AbstractController $text_type = strtolower(trim(Request::input('text_type'))); $silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']); $model_name = trim(Request::input('model_name')); - $extra_data = Request::input('extra_data'); $markdown = in_array($text_type, ['md', 'markdown']); // $result = []; @@ -1285,7 +1156,7 @@ class DialogController extends AbstractController if ($model_name) { $msgData['model_name'] = $model_name; } - $result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'longtext', $msgData, $user->userid, false, false, $silence, $key, $extra_data); + $result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'longtext', $msgData, $user->userid, false, false, $silence, $key); } else { $msgData = ['text' => $text]; if ($markdown) { @@ -1294,7 +1165,7 @@ class DialogController extends AbstractController if ($model_name) { $msgData['model_name'] = $model_name; } - $result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key, $extra_data); + $result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key); } } return $result; diff --git a/app/Models/WebSocketDialogMsg.php b/app/Models/WebSocketDialogMsg.php index cf33a03f4..716429299 100644 --- a/app/Models/WebSocketDialogMsg.php +++ b/app/Models/WebSocketDialogMsg.php @@ -43,7 +43,6 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property \Illuminate\Support\Carbon|null $deleted_at * @property-read int|mixed $percentage * @property-read \App\Models\User|null $user - * @property-read \App\Models\WebSocketDialogMsgExtra|null $extra * @property-read \App\Models\WebSocketDialog|null $webSocketDialog * @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend() * @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden() @@ -112,14 +111,6 @@ class WebSocketDialogMsg extends AbstractModel return $this->hasOne(User::class, 'userid', 'userid'); } - /** - * @return \Illuminate\Database\Eloquent\Relations\HasOne - */ - public function extra(): \Illuminate\Database\Eloquent\Relations\HasOne - { - return $this->hasOne(WebSocketDialogMsgExtra::class, 'msg_id', 'id'); - } - /** * 阅读占比 * @return int|mixed @@ -1242,10 +1233,9 @@ class WebSocketDialogMsg extends AbstractModel * @param bool|null $push_silence 推送-静默 * - type = [text|file|record|meeting] 默认为:false * @param string|null $search_key 搜索关键词(用于搜索,留空则自动生成) - * @param array|null $extra_data 额外数据(仅在发送消息时有效) * @return array */ - public static function sendMsg($action, $dialog_id, $type, $msg, $sender = null, $push_self = false, $push_retry = false, $push_silence = null, $search_key = null, $extra_data = null) + public static function sendMsg($action, $dialog_id, $type, $msg, $sender = null, $push_self = false, $push_retry = false, $push_silence = null, $search_key = null) { $link = 0; $mtype = $type; @@ -1390,17 +1380,10 @@ class WebSocketDialogMsg extends AbstractModel 'msg' => $msg, 'read' => 0, ]); - AbstractModel::transaction(function () use ($search_key, $dialogMsg, $extra_data) { + AbstractModel::transaction(function () use ($search_key, $dialogMsg) { $dialogMsg->send = 1; $dialogMsg->generateKeyAndSave($search_key); // - if ($extra_data) { - WebSocketDialogMsgExtra::createInstance([ - 'msg_id' => $dialogMsg->id, - 'data' => Base::array2json($extra_data), - ])->save(); - } - // WebSocketDialogSession::updateTitle($dialogMsg->session_id, $dialogMsg); // if ($dialogMsg->type === 'meeting') { diff --git a/app/Models/WebSocketDialogMsgExtra.php b/app/Models/WebSocketDialogMsgExtra.php deleted file mode 100644 index 1d9ef32fc..000000000 --- a/app/Models/WebSocketDialogMsgExtra.php +++ /dev/null @@ -1,56 +0,0 @@ -belongsTo(WebSocketDialogMsg::class, 'msg_id', 'id'); - } -} - diff --git a/app/Tasks/BotReceiveMsgTask.php b/app/Tasks/BotReceiveMsgTask.php index a501bd9c6..c3e172a20 100644 --- a/app/Tasks/BotReceiveMsgTask.php +++ b/app/Tasks/BotReceiveMsgTask.php @@ -66,7 +66,7 @@ class BotReceiveMsgTask extends AbstractTask } // 判断消息是否存在 - $msg = WebSocketDialogMsg::with(['user', 'extra'])->find($this->msgId); + $msg = WebSocketDialogMsg::with(['user'])->find($this->msgId); if (empty($msg)) { return; } @@ -523,16 +523,6 @@ class BotReceiveMsgTask extends AbstractTask {$sendText} EOF; } - // 处理额外数据 - if ($msg->extra && $msg->extra->data) { - $extraData = $msg->extra->data; - if ($extraData['system_prompt']) { - $extras['system_message'] = $extraData['system_prompt']; - } - if ($extraData['context_prompt']) { - $sendText = $extraData['context_prompt'] . $sendText; - } - } $webhookUrl = "http://nginx/ai/chat"; } else { // 用户机器人 diff --git a/database/migrations/2025_11_07_183944_create_web_socket_dialog_msg_extras_table.php b/database/migrations/2025_11_07_183944_create_web_socket_dialog_msg_extras_table.php deleted file mode 100644 index 6b812ae88..000000000 --- a/database/migrations/2025_11_07_183944_create_web_socket_dialog_msg_extras_table.php +++ /dev/null @@ -1,35 +0,0 @@ -id(); - $table->unsignedBigInteger('msg_id')->nullable()->default(0)->comment('消息ID'); - $table->longText('data')->nullable()->comment('额外数据'); - $table->timestamps(); - - $table->foreign('msg_id')->references('id')->on('web_socket_dialog_msgs')->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::dropIfExists('web_socket_dialog_msg_extras'); - } -} diff --git a/resources/assets/js/components/AIAssistant.vue b/resources/assets/js/components/AIAssistant.vue index 76313a8ee..5b0c569f2 100644 --- a/resources/assets/js/components/AIAssistant.vue +++ b/resources/assets/js/components/AIAssistant.vue @@ -37,7 +37,7 @@ @@ -93,8 +93,8 @@