From 7f6abc331bba11e08301efbd6291fd0ec21953fd Mon Sep 17 00:00:00 2001 From: kuaifan Date: Tue, 23 Sep 2025 14:05:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20AI=20=E5=8A=A9?= =?UTF-8?q?=E6=89=8B=E7=94=9F=E6=88=90=E6=B6=88=E6=81=AF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 DialogController 中新增 msg__ai_generate 接口,支持根据用户需求自动生成聊天消息 - 在 AI 模块中实现 generateMessage 方法,处理消息生成逻辑 - 更新前端 ChatInput 组件,添加 AI 生成按钮,集成消息生成请求 - 增强用户交互体验,支持输入消息主题和要点 --- app/Http/Controllers/Api/DialogController.php | 94 ++++++++++++++ app/Module/AI.php | 121 ++++++++++++++++++ .../manage/components/ChatInput/index.vue | 61 +++++++++ 3 files changed, 276 insertions(+) diff --git a/app/Http/Controllers/Api/DialogController.php b/app/Http/Controllers/Api/DialogController.php index 230e28467..e6f559d2c 100755 --- a/app/Http/Controllers/Api/DialogController.php +++ b/app/Http/Controllers/Api/DialogController.php @@ -989,6 +989,100 @@ class DialogController extends AbstractController return Base::retSuccess('success'); } + /** + * @api {post} api/dialog/msg/ai_generate 21.1 使用 AI 助手生成消息 + * + * @apiDescription 需要token身份,根据上下文自动生成拟发送的聊天消息 + * @apiVersion 1.0.0 + * @apiGroup dialog + * @apiName msg__ai_generate + * + * @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.text AI 生成的消息文本(Markdown 格式) + * @apiSuccess {String} data.html AI 生成的消息内容(HTML 格式) + */ + public function msg__ai_generate() + { + $user = User::auth(); + $user->checkChatInformation(); + // + $dialog_id = intval(Request::input('dialog_id')); + $content = trim(Request::input('content', '')); + if ($dialog_id <= 0) { + return Base::retError('参数错误'); + } + if ($content === '') { + 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; + } + + $recentMessages = WebSocketDialogMsg::whereDialogId($dialog_id) + ->orderByDesc('id') + ->take(6) + ->with('user') + ->get(); + if ($recentMessages->isNotEmpty()) { + $context['recent_messages'] = $recentMessages->reverse()->map(function ($msg) { + return [ + 'sender' => $msg->user->nickname ?? ('用户' . $msg->userid), + 'summary' => WebSocketDialogMsg::previewMsg($msg), + ]; + })->filter(function ($item) { + return !empty($item['summary']); + })->values()->all(); + } + + $result = AI::generateMessage($content, $context); + if (Base::isError($result)) { + return Base::retError('生成消息失败', $result); + } + + return Base::retSuccess('生成消息成功', $result['data']); + } + /** * @api {post} api/dialog/msg/sendtext 21. 发送消息 * diff --git a/app/Module/AI.php b/app/Module/AI.php index acba3c5a0..b1d43289c 100644 --- a/app/Module/AI.php +++ b/app/Module/AI.php @@ -551,6 +551,71 @@ class AI ]); } + /** + * 通过 openAI 生成聊天消息 + * @param string $text 消息需求描述 + * @param array $context 上下文信息 + * @return array + */ + public static function generateMessage($text, $context = []) + { + $text = trim((string)$text); + if ($text === '') { + return Base::retError("消息需求不能为空"); + } + + $contextPrompt = self::buildMessageContextPrompt($context); + + $post = json_encode([ + "model" => "gpt-5-nano", + "messages" => [ + [ + "role" => "system", + "content" => << "user", + "content" => ($contextPrompt ? $contextPrompt . "\n\n" : "") . "请根据以上信息,为以下需求生成一条待发送的消息:\n\n" . $text + ], + ], + ]); + + $ai = new self($post); + $ai->setTimeout(45); + + $res = $ai->request(); + if (Base::isError($res)) { + return Base::retError("消息生成失败", $res); + } + + $content = trim($res['data']); + $content = preg_replace('/^\s*```(?:markdown|md|text)?\s*/i', '', $content); + $content = preg_replace('/\s*```\s*$/', '', $content); + $content = trim($content); + + if ($content === '') { + return Base::retError("消息生成结果为空"); + } + + return Base::retSuccess("success", [ + 'text' => $content, + 'html' => Base::markdown2html($content), + ]); + } + /** * 构建任务生成的上下文提示信息 * @param array $context 上下文信息 @@ -636,6 +701,62 @@ class AI return empty($prompts) ? "" : implode("\n", $prompts); } + private static function buildMessageContextPrompt($context) + { + $prompts = []; + + if (!empty($context['dialog_name']) || !empty($context['dialog_type']) || !empty($context['group_type'])) { + $prompts[] = "## 会话信息"; + if (!empty($context['dialog_name'])) { + $prompts[] = "名称:" . Base::cutStr($context['dialog_name'], 60); + } + if (!empty($context['dialog_type'])) { + $typeMap = ['group' => '群聊', 'user' => '单聊']; + $prompts[] = "类型:" . ($typeMap[$context['dialog_type']] ?? $context['dialog_type']); + } + if (!empty($context['group_type'])) { + $prompts[] = "分类:" . Base::cutStr($context['group_type'], 60); + } + } + + if (!empty($context['members']) && is_array($context['members'])) { + $members = array_slice(array_filter($context['members']), 0, 10); + if (!empty($members)) { + $prompts[] = "## 会话成员"; + $prompts[] = implode(",", array_map(fn($name) => Base::cutStr($name, 30), $members)); + } + } + + if (!empty($context['recent_messages']) && is_array($context['recent_messages'])) { + $prompts[] = "## 最近消息"; + foreach ($context['recent_messages'] as $item) { + $sender = Base::cutStr(trim($item['sender'] ?? ''), 40) ?: '成员'; + $summary = Base::cutStr(trim($item['summary'] ?? ''), 120); + if ($summary !== '') { + $prompts[] = "- {$sender}:{$summary}"; + } + } + } + + if (!empty($context['quote_summary'])) { + $prompts[] = "## 引用消息"; + $quoteUser = Base::cutStr(trim($context['quote_user'] ?? ''), 40); + $quoteText = Base::cutStr(trim($context['quote_summary']), 200); + if ($quoteUser !== '') { + $prompts[] = "{$quoteUser}:{$quoteText}"; + } else { + $prompts[] = $quoteText; + } + } + + if (!empty($context['current_draft'])) { + $prompts[] = "## 当前草稿"; + $prompts[] = Base::cutStr(trim($context['current_draft']), 200); + } + + return empty($prompts) ? "" : implode("\n", $prompts); + } + private static function normalizeProjectColumns($columns) { if (is_string($columns)) { diff --git a/resources/assets/js/pages/manage/components/ChatInput/index.vue b/resources/assets/js/pages/manage/components/ChatInput/index.vue index 547585bb2..ea60b5a72 100755 --- a/resources/assets/js/pages/manage/components/ChatInput/index.vue +++ b/resources/assets/js/pages/manage/components/ChatInput/index.vue @@ -152,6 +152,10 @@ {{$L('上传文件')}} +
+ + {{$L('AI 生成')}} +
{{$L('全屏输入')}} @@ -429,6 +433,8 @@ export default { showMore: false, showEmoji: false, + chatAiLoading: false, + emojiQuickShow: false, emojiQuickKey: '', emojiQuickItems: [], @@ -1762,6 +1768,10 @@ export default { this.openMenu("#"); break; + case 'ai': + this.onMessageAI(); + break; + case 'maybe-photo': this.$emit('on-file', { type: 'photo', @@ -1812,6 +1822,57 @@ export default { } }, + onMessageAI() { + if (this.disabled || this.chatAiLoading) { + return; + } + if (!this.dialogId) { + $A.messageWarning(this.$L('当前未选择会话')); + return; + } + $A.modalInput({ + title: 'AI 生成', + placeholder: '请简要描述消息的主题、语气或要点,AI 将生成完整消息', + inputProps: { + type: 'textarea', + rows: 2, + autosize: {minRows: 2, maxRows: 6}, + maxlength: 500, + }, + onOk: (value) => { + if (!value) { + return '请输入消息需求'; + } + return new Promise((resolve, reject) => { + this.chatAiLoading = true; + this.$store.dispatch('call', { + url: 'dialog/msg/ai_generate', + data: { + dialog_id: this.dialogId, + content: value, + draft: this.value || '', + quote_id: this.quoteData?.id || 0, + }, + timeout: 45 * 1000, + }).then(({data}) => { + const html = data && (data.html || data.text) ? (data.html || data.text) : ''; + if (!html) { + reject(this.$L('AI 未生成内容')); + return; + } + this.$emit('input', html); + this.$nextTick(() => this.focus()); + resolve(); + }).catch(({msg}) => { + reject(msg); + }).finally(() => { + this.chatAiLoading = false; + }); + }); + } + }) + }, + onFullInput() { if (this.disabled) { return