mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-13 20:12:48 +00:00
feat: 添加 AI 助手生成消息功能
- 在 DialogController 中新增 msg__ai_generate 接口,支持根据用户需求自动生成聊天消息 - 在 AI 模块中实现 generateMessage 方法,处理消息生成逻辑 - 更新前端 ChatInput 组件,添加 AI 生成按钮,集成消息生成请求 - 增强用户交互体验,支持输入消息主题和要点
This commit is contained in:
parent
c190aab8b9
commit
7f6abc331b
@ -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. 发送消息
|
||||
*
|
||||
|
||||
@ -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" => <<<EOF
|
||||
你是一名专业的沟通助手,协助用户编写得体、清晰且具行动指向的即时消息。
|
||||
|
||||
写作要求:
|
||||
1. 根据用户提供的需求与上下文生成完整消息,语气需符合业务沟通场景,保持真诚、礼貌且高效
|
||||
2. 默认使用简洁的短段落,可使用 Markdown 基础格式(加粗、列表、引用)增强结构,但不要输出代码块或 JSON
|
||||
3. 如果上下文包含引用信息或草稿,请在消息中自然呼应相关要点
|
||||
4. 如无特别说明,将消息长度控制在 60-180 字;若需更短或更长,遵循用户描述
|
||||
5. 如需提出行动或问题,请明确表达,避免含糊
|
||||
|
||||
输出规范:
|
||||
- 仅返回可直接发送的消息内容
|
||||
- 禁止在内容前后添加额外说明、标签或引导语
|
||||
EOF
|
||||
],
|
||||
[
|
||||
"role" => "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)) {
|
||||
|
||||
@ -152,6 +152,10 @@
|
||||
<em>{{$L('上传文件')}}</em>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chat-input-popover-item" @click="onToolbar('ai')">
|
||||
<i class="taskfont"></i>
|
||||
<em>{{$L('AI 生成')}}</em>
|
||||
</div>
|
||||
<div ref="moreFull" class="chat-input-popover-item" @click="onToolbar('full')">
|
||||
<i class="taskfont"></i>
|
||||
<em>{{$L('全屏输入')}}</em>
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user