feat: 添加额外数据处理,优化AI助手消息生成与发送逻辑

This commit is contained in:
kuaifan 2025-11-07 20:38:06 +00:00
parent e801c09c0f
commit 892ad395a7
8 changed files with 228 additions and 157 deletions

View File

@ -1027,25 +1027,25 @@ class DialogController extends AbstractController
}
/**
* @api {post} api/dialog/msg/ai_generate 使用 AI 助手生成消息
* @api {post} api/dialog/msg/aiprompt AI 提示词助手
*
* @apiDescription 需要token身份根据上下文自动生成拟发送的聊天消
* @apiDescription 需要token身份整理当前会话的系统提示词与上下文信
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__ai_generate
* @apiName msg__aiprompt
*
* @apiParam {Number} dialog_id 对话ID
* @apiParam {String} content 消息需求描述
* @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 格式)
* @apiSuccess {String} data.system_prompt AI 使用的系统提示词
* @apiSuccess {String} data.context_prompt AI 使用的上下文提示词
*/
public function msg__ai_generate()
public function msg__aiprompt()
{
$user = User::auth();
$user->checkChatInformation();
@ -1055,9 +1055,6 @@ class DialogController extends AbstractController
if ($dialog_id <= 0) {
return Base::retError('参数错误');
}
if ($content === '') {
return Base::retError('消息需求描述不能为空');
}
$dialog = WebSocketDialog::checkDialog($dialog_id);
@ -1144,12 +1141,16 @@ class DialogController extends AbstractController
}
// 生成消息
$result = AI::generateMessage($content, $context);
if (Base::isError($result)) {
return Base::retError('生成消息失败', $result);
$systemPrompt = AI::messageSystemPrompt();
$contextPrompt = AI::buildMessageContextPrompt($context);
if ($content) {
$contextPrompt .= "\n\n请根据以上信息,结合提示词生成一条待发送的消息:\n\n";
}
return Base::retSuccess('生成消息成功', $result['data']);
return Base::retSuccess('success', [
'system_prompt' => $systemPrompt,
'context_prompt' => $contextPrompt,
]);
}
/**
@ -1179,6 +1180,7 @@ class DialogController extends AbstractController
* - no: 正常发送(默认)
* - yes: 静默发送
* @apiParam {String} [model_name] 模型名称仅AI机器人支持
* @apiParam {Object} [extra_data] 附加数据(保存到附加表)
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
@ -1200,6 +1202,7 @@ 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 = [];
@ -1233,14 +1236,14 @@ class DialogController extends AbstractController
$text = WebSocketDialogMsg::formatMsg($text, $dialog_id);
}
$strlen = mb_strlen($text);
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
$reallen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
if ($strlen < 1) {
return Base::retError('消息内容不能为空');
}
if ($noimglen > 200000) {
if ($reallen > 200000) {
return Base::retError('消息内容最大不能超过200000字');
}
if ($noimglen > 5000) {
if ($reallen > 5000) {
// 内容过长转成文件发送
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
Base::makeDir(public_path($path));
@ -1282,7 +1285,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);
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'longtext', $msgData, $user->userid, false, false, $silence, $key, $extra_data);
} else {
$msgData = ['text' => $text];
if ($markdown) {
@ -1291,7 +1294,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);
$result = WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence, $key, $extra_data);
}
}
return $result;
@ -3134,11 +3137,11 @@ class DialogController extends AbstractController
//
WebSocketDialog::checkDialog($dialog_id);
$strlen = mb_strlen($text);
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
$reallen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
if ($strlen < 1 || empty($list)) {
return Base::retError('内容不能为空');
}
if ($noimglen > 200000) {
if ($reallen > 200000) {
return Base::retError('内容最大不能超过200000字');
}
//
@ -3283,11 +3286,11 @@ class DialogController extends AbstractController
});
} else {
$strlen = mb_strlen($text);
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
$reallen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
if ($strlen < 1) {
return Base::retError('内容不能为空');
}
if ($noimglen > 200000) {
if ($reallen > 200000) {
return Base::retError('内容最大不能超过200000字');
}
$msgData = [

View File

@ -43,6 +43,7 @@ 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()
@ -111,6 +112,14 @@ 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
@ -1233,9 +1242,10 @@ 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)
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)
{
$link = 0;
$mtype = $type;
@ -1380,10 +1390,17 @@ class WebSocketDialogMsg extends AbstractModel
'msg' => $msg,
'read' => 0,
]);
AbstractModel::transaction(function () use ($search_key, $dialogMsg) {
AbstractModel::transaction(function () use ($search_key, $dialogMsg, $extra_data) {
$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') {

View File

@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Module\Base;
/**
* App\Models\WebSocketDialogMsgExtra
*
* @property int $id
* @property int|null $msg_id 消息ID
* @property string|null $data 长内容
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Models\WebSocketDialogMsg|null $webSocketDialogMsg
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgExtra newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgExtra newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgExtra query()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgExtra whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgExtra whereData($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgExtra whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgExtra whereMsgId($value)
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgExtra whereUpdatedAt($value)
* @mixin \Eloquent
*/
class WebSocketDialogMsgExtra extends AbstractModel
{
/**
* @param $value
* @return array
*/
public function getDataAttribute($value)
{
if (is_array($value)) {
return $value;
}
return Base::json2array($value);
}
/**
* 关联到消息
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function webSocketDialogMsg(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(WebSocketDialogMsg::class, 'msg_id', 'id');
}
}

View File

@ -556,72 +556,6 @@ 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-mini",
"reasoning_effort" => "minimal",
"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 Report $report
@ -836,7 +770,25 @@ class AI
return empty($prompts) ? "" : implode("\n", $prompts);
}
private static function buildMessageContextPrompt($context)
public static function messageSystemPrompt()
{
return <<<EOF
你是一名专业的沟通助手,协助用户编写得体、清晰且具行动指向的即时消息。
写作要求:
1. 根据用户提供的需求与上下文生成完整消息,语气需符合业务沟通场景,保持真诚、礼貌且高效
2. 默认使用简洁的短段落,可使用 Markdown 基础格式(加粗、列表、引用)增强结构,但不要输出代码块或 JSON
3. 如果上下文包含引用信息或草稿,请在消息中自然呼应相关要点
4. 如无特别说明,将消息长度控制在 60-180 字;若需更短或更长,遵循用户描述
5. 如需提出行动或问题,请明确表达,避免含糊
输出规范:
- 仅返回可直接发送的消息内容
- 禁止在内容前后添加额外说明、标签或引导语
EOF;
}
public static function buildMessageContextPrompt($context)
{
$prompts = [];

View File

@ -66,7 +66,7 @@ class BotReceiveMsgTask extends AbstractTask
}
// 判断消息是否存在
$msg = WebSocketDialogMsg::with(['user'])->find($this->msgId);
$msg = WebSocketDialogMsg::with(['user', 'extra'])->find($this->msgId);
if (empty($msg)) {
return;
}
@ -523,6 +523,16 @@ 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 {
// 用户机器人

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateWebSocketDialogMsgExtrasTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('web_socket_dialog_msg_extras', function (Blueprint $table) {
$table->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');
}
}

View File

@ -133,6 +133,7 @@ export default {
inputAutosize: this.defaultInputAutosize,
inputMaxlength: this.defaultInputMaxlength,
inputOnOk: null,
inputOnBeforeSend: null,
//
inputModel: '',
@ -186,6 +187,7 @@ export default {
this.inputAutosize = params.autosize || this.defaultInputAutosize;
this.inputMaxlength = params.maxlength || this.defaultInputMaxlength;
this.inputOnOk = params.onOk || null;
this.inputOnBeforeSend = params.onBeforeSend || null;
}
this.responses = [];
this.pendingResponses = [];
@ -368,7 +370,7 @@ export default {
prompt: rawValue,
});
this.scrollResponsesToBottom();
const message = await this.sendAiMessage(dialogId, this.formatPlainText(rawValue), modelOption.value);
const message = await this.sendAiMessage(dialogId, rawValue, modelOption.value);
if (responseEntry) {
responseEntry.userid = userid;
responseEntry.message = message;
@ -476,11 +478,11 @@ export default {
const {data} = await this.$store.dispatch("call", {
url: 'dialog/msg/sendtext',
method: 'post',
data: {
data: await this.buildPayloadData({
dialog_id: dialogId,
text,
model_name: model,
},
}),
});
if (data) {
this.$store.dispatch("saveDialogMsg", data);
@ -494,15 +496,26 @@ export default {
},
/**
* 将纯文本转换成HTML
* 构建最终发送的数据
*/
formatPlainText(text) {
const escaped = `${text}`
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br/>');
return `<p>${escaped}</p>`;
async buildPayloadData(data) {
if (typeof this.inputOnBeforeSend !== 'function') {
return data;
}
try {
const result = this.inputOnBeforeSend(data);
if (result && typeof result.then === 'function') {
const resolved = await result;
if ($A.isJson(resolved)) {
return resolved;
}
} else if ($A.isJson(result)) {
return result;
}
} catch (e) {
console.warn('[AIAssistant] onBeforeSend error:', e);
}
return data;
},
/**
@ -681,16 +694,19 @@ export default {
display: flex;
flex-direction: column;
gap: 16px;
max-height: calc(100vh - 344px);
@media (height <= 900px) {
max-height: calc(100vh - 214px);
}
.ai-assistant-output {
flex: 1;
min-height: 0;
padding: 12px;
border-radius: 8px;
background: #f8f9fb;
border: 1px solid rgba(0, 0, 0, 0.04);
max-height: calc(100vh - 390px);
overflow-y: auto;
@media (height <= 900px) {
max-height: calc(100vh - 260px);
}
}
.ai-assistant-output-item + .ai-assistant-output-item {

View File

@ -342,7 +342,7 @@ import clickoutside from "../../../../directives/clickoutside";
import longpress from "../../../../directives/longpress";
import {inputLoadAdd, inputLoadIsLast, inputLoadRemove} from "./one";
import {languageList, languageName} from "../../../../language";
import {isMarkdownFormat} from "../../../../utils/markdown";
import {isMarkdownFormat, MarkdownConver} from "../../../../utils/markdown";
import emitter from "../../../../store/events";
import historyMixin from "./history";
@ -1904,63 +1904,45 @@ export default {
return;
}
if (!this.dialogId) {
$A.messageWarning(this.$L('当前未选择会话'));
$A.messageWarning('当前未选择会话');
return;
}
let canceled = false;
$A.modalInput({
title: 'AI 生成',
placeholder: '请简要描述消息的主题、语气或要点AI 将生成完整消息',
inputProps: {
type: 'textarea',
rows: 2,
autosize: {minRows: 2, maxRows: 6},
maxlength: 500,
},
onCancel: () => {
canceled = true;
},
onOk: (value) => {
if (!value) {
return '请输入消息需求';
emitter.emit('openAIAssistant', {
placeholder: this.$L('请简要描述消息的主题、语气或要点AI 将生成完整消息'),
onBeforeSend: async (sendData) => {
if (!sendData) {
return sendData;
}
return new Promise((resolve, reject) => {
if (canceled) {
reject();
return;
}
this.$store.dispatch('call', {
url: 'dialog/msg/ai_generate',
try {
const {data: extraData} = await this.$store.dispatch('call', {
url: 'dialog/msg/aiprompt',
data: {
dialog_id: this.dialogId,
content: value,
content: sendData.text,
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 (canceled) {
resolve();
return;
}
if (!html) {
reject(this.$L('AI 未生成内容'));
return;
}
this.$emit('input', html);
this.$nextTick(() => this.focus());
resolve();
}).catch(({msg}) => {
if (canceled) {
resolve();
return;
}
reject(msg);
});
});
}
})
if ($A.isJson(extraData)) {
sendData.extra_data = extraData;
}
return sendData;
} catch (error) {
const msg = error?.msg || 'AI 提示生成失败';
$A.modalError(msg);
throw error;
}
},
onOk: ({aiContent}) => {
if (!aiContent) {
$A.messageWarning('AI 未生成内容');
return;
}
const html = MarkdownConver(aiContent);
this.$emit('input', html);
this.$nextTick(() => this.focus());
},
});
},
onFullInput() {