mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-10 18:02:55 +00:00
feat: 添加额外数据处理,优化AI助手消息生成与发送逻辑
This commit is contained in:
parent
e801c09c0f
commit
892ad395a7
@ -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
|
* @apiVersion 1.0.0
|
||||||
* @apiGroup dialog
|
* @apiGroup dialog
|
||||||
* @apiName msg__ai_generate
|
* @apiName msg__aiprompt
|
||||||
*
|
*
|
||||||
* @apiParam {Number} dialog_id 对话ID
|
* @apiParam {Number} dialog_id 对话ID
|
||||||
* @apiParam {String} content 消息需求描述
|
* @apiParam {String} content 消息需求描述(用于提示词整理,可为空)
|
||||||
* @apiParam {String} [draft] 当前草稿内容(HTML 格式)
|
* @apiParam {String} [draft] 当前草稿内容(HTML 格式)
|
||||||
* @apiParam {Number} [quote_id] 引用消息ID
|
* @apiParam {Number} [quote_id] 引用消息ID
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
* @apiSuccess {Object} data 返回数据
|
* @apiSuccess {Object} data 返回数据
|
||||||
* @apiSuccess {String} data.text AI 生成的消息文本(Markdown 格式)
|
* @apiSuccess {String} data.system_prompt AI 使用的系统提示词
|
||||||
* @apiSuccess {String} data.html AI 生成的消息内容(HTML 格式)
|
* @apiSuccess {String} data.context_prompt AI 使用的上下文提示词
|
||||||
*/
|
*/
|
||||||
public function msg__ai_generate()
|
public function msg__aiprompt()
|
||||||
{
|
{
|
||||||
$user = User::auth();
|
$user = User::auth();
|
||||||
$user->checkChatInformation();
|
$user->checkChatInformation();
|
||||||
@ -1055,9 +1055,6 @@ class DialogController extends AbstractController
|
|||||||
if ($dialog_id <= 0) {
|
if ($dialog_id <= 0) {
|
||||||
return Base::retError('参数错误');
|
return Base::retError('参数错误');
|
||||||
}
|
}
|
||||||
if ($content === '') {
|
|
||||||
return Base::retError('消息需求描述不能为空');
|
|
||||||
}
|
|
||||||
|
|
||||||
$dialog = WebSocketDialog::checkDialog($dialog_id);
|
$dialog = WebSocketDialog::checkDialog($dialog_id);
|
||||||
|
|
||||||
@ -1144,12 +1141,16 @@ class DialogController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成消息
|
// 生成消息
|
||||||
$result = AI::generateMessage($content, $context);
|
$systemPrompt = AI::messageSystemPrompt();
|
||||||
if (Base::isError($result)) {
|
$contextPrompt = AI::buildMessageContextPrompt($context);
|
||||||
return Base::retError('生成消息失败', $result);
|
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: 正常发送(默认)
|
* - no: 正常发送(默认)
|
||||||
* - yes: 静默发送
|
* - yes: 静默发送
|
||||||
* @apiParam {String} [model_name] 模型名称(仅AI机器人支持)
|
* @apiParam {String} [model_name] 模型名称(仅AI机器人支持)
|
||||||
|
* @apiParam {Object} [extra_data] 附加数据(保存到附加表)
|
||||||
*
|
*
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
@ -1200,6 +1202,7 @@ class DialogController extends AbstractController
|
|||||||
$text_type = strtolower(trim(Request::input('text_type')));
|
$text_type = strtolower(trim(Request::input('text_type')));
|
||||||
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
|
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
|
||||||
$model_name = trim(Request::input('model_name'));
|
$model_name = trim(Request::input('model_name'));
|
||||||
|
$extra_data = Request::input('extra_data');
|
||||||
$markdown = in_array($text_type, ['md', 'markdown']);
|
$markdown = in_array($text_type, ['md', 'markdown']);
|
||||||
//
|
//
|
||||||
$result = [];
|
$result = [];
|
||||||
@ -1233,14 +1236,14 @@ class DialogController extends AbstractController
|
|||||||
$text = WebSocketDialogMsg::formatMsg($text, $dialog_id);
|
$text = WebSocketDialogMsg::formatMsg($text, $dialog_id);
|
||||||
}
|
}
|
||||||
$strlen = mb_strlen($text);
|
$strlen = mb_strlen($text);
|
||||||
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
|
$reallen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
|
||||||
if ($strlen < 1) {
|
if ($strlen < 1) {
|
||||||
return Base::retError('消息内容不能为空');
|
return Base::retError('消息内容不能为空');
|
||||||
}
|
}
|
||||||
if ($noimglen > 200000) {
|
if ($reallen > 200000) {
|
||||||
return Base::retError('消息内容最大不能超过200000字');
|
return Base::retError('消息内容最大不能超过200000字');
|
||||||
}
|
}
|
||||||
if ($noimglen > 5000) {
|
if ($reallen > 5000) {
|
||||||
// 内容过长转成文件发送
|
// 内容过长转成文件发送
|
||||||
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
|
$path = "uploads/chat/" . date("Ym") . "/" . $dialog_id . "/";
|
||||||
Base::makeDir(public_path($path));
|
Base::makeDir(public_path($path));
|
||||||
@ -1282,7 +1285,7 @@ class DialogController extends AbstractController
|
|||||||
if ($model_name) {
|
if ($model_name) {
|
||||||
$msgData['model_name'] = $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 {
|
} else {
|
||||||
$msgData = ['text' => $text];
|
$msgData = ['text' => $text];
|
||||||
if ($markdown) {
|
if ($markdown) {
|
||||||
@ -1291,7 +1294,7 @@ class DialogController extends AbstractController
|
|||||||
if ($model_name) {
|
if ($model_name) {
|
||||||
$msgData['model_name'] = $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;
|
return $result;
|
||||||
@ -3134,11 +3137,11 @@ class DialogController extends AbstractController
|
|||||||
//
|
//
|
||||||
WebSocketDialog::checkDialog($dialog_id);
|
WebSocketDialog::checkDialog($dialog_id);
|
||||||
$strlen = mb_strlen($text);
|
$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)) {
|
if ($strlen < 1 || empty($list)) {
|
||||||
return Base::retError('内容不能为空');
|
return Base::retError('内容不能为空');
|
||||||
}
|
}
|
||||||
if ($noimglen > 200000) {
|
if ($reallen > 200000) {
|
||||||
return Base::retError('内容最大不能超过200000字');
|
return Base::retError('内容最大不能超过200000字');
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
@ -3283,11 +3286,11 @@ class DialogController extends AbstractController
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
$strlen = mb_strlen($text);
|
$strlen = mb_strlen($text);
|
||||||
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
|
$reallen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
|
||||||
if ($strlen < 1) {
|
if ($strlen < 1) {
|
||||||
return Base::retError('内容不能为空');
|
return Base::retError('内容不能为空');
|
||||||
}
|
}
|
||||||
if ($noimglen > 200000) {
|
if ($reallen > 200000) {
|
||||||
return Base::retError('内容最大不能超过200000字');
|
return Base::retError('内容最大不能超过200000字');
|
||||||
}
|
}
|
||||||
$msgData = [
|
$msgData = [
|
||||||
|
|||||||
@ -43,6 +43,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||||
* @property-read int|mixed $percentage
|
* @property-read int|mixed $percentage
|
||||||
* @property-read \App\Models\User|null $user
|
* @property-read \App\Models\User|null $user
|
||||||
|
* @property-read \App\Models\WebSocketDialogMsgExtra|null $extra
|
||||||
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
|
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||||
@ -111,6 +112,14 @@ class WebSocketDialogMsg extends AbstractModel
|
|||||||
return $this->hasOne(User::class, 'userid', 'userid');
|
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
|
* @return int|mixed
|
||||||
@ -1233,9 +1242,10 @@ class WebSocketDialogMsg extends AbstractModel
|
|||||||
* @param bool|null $push_silence 推送-静默
|
* @param bool|null $push_silence 推送-静默
|
||||||
* - type = [text|file|record|meeting] 默认为:false
|
* - type = [text|file|record|meeting] 默认为:false
|
||||||
* @param string|null $search_key 搜索关键词(用于搜索,留空则自动生成)
|
* @param string|null $search_key 搜索关键词(用于搜索,留空则自动生成)
|
||||||
|
* @param array|null $extra_data 额外数据(仅在发送消息时有效)
|
||||||
* @return array
|
* @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;
|
$link = 0;
|
||||||
$mtype = $type;
|
$mtype = $type;
|
||||||
@ -1380,10 +1390,17 @@ class WebSocketDialogMsg extends AbstractModel
|
|||||||
'msg' => $msg,
|
'msg' => $msg,
|
||||||
'read' => 0,
|
'read' => 0,
|
||||||
]);
|
]);
|
||||||
AbstractModel::transaction(function () use ($search_key, $dialogMsg) {
|
AbstractModel::transaction(function () use ($search_key, $dialogMsg, $extra_data) {
|
||||||
$dialogMsg->send = 1;
|
$dialogMsg->send = 1;
|
||||||
$dialogMsg->generateKeyAndSave($search_key);
|
$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);
|
WebSocketDialogSession::updateTitle($dialogMsg->session_id, $dialogMsg);
|
||||||
//
|
//
|
||||||
if ($dialogMsg->type === 'meeting') {
|
if ($dialogMsg->type === 'meeting') {
|
||||||
|
|||||||
56
app/Models/WebSocketDialogMsgExtra.php
Normal file
56
app/Models/WebSocketDialogMsgExtra.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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
|
* @param Report $report
|
||||||
@ -836,7 +770,25 @@ class AI
|
|||||||
return empty($prompts) ? "" : implode("\n", $prompts);
|
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 = [];
|
$prompts = [];
|
||||||
|
|
||||||
|
|||||||
@ -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)) {
|
if (empty($msg)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -523,6 +523,16 @@ class BotReceiveMsgTask extends AbstractTask
|
|||||||
{$sendText}
|
{$sendText}
|
||||||
EOF;
|
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";
|
$webhookUrl = "http://nginx/ai/chat";
|
||||||
} else {
|
} else {
|
||||||
// 用户机器人
|
// 用户机器人
|
||||||
|
|||||||
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -133,6 +133,7 @@ export default {
|
|||||||
inputAutosize: this.defaultInputAutosize,
|
inputAutosize: this.defaultInputAutosize,
|
||||||
inputMaxlength: this.defaultInputMaxlength,
|
inputMaxlength: this.defaultInputMaxlength,
|
||||||
inputOnOk: null,
|
inputOnOk: null,
|
||||||
|
inputOnBeforeSend: null,
|
||||||
|
|
||||||
// 模型选择
|
// 模型选择
|
||||||
inputModel: '',
|
inputModel: '',
|
||||||
@ -186,6 +187,7 @@ export default {
|
|||||||
this.inputAutosize = params.autosize || this.defaultInputAutosize;
|
this.inputAutosize = params.autosize || this.defaultInputAutosize;
|
||||||
this.inputMaxlength = params.maxlength || this.defaultInputMaxlength;
|
this.inputMaxlength = params.maxlength || this.defaultInputMaxlength;
|
||||||
this.inputOnOk = params.onOk || null;
|
this.inputOnOk = params.onOk || null;
|
||||||
|
this.inputOnBeforeSend = params.onBeforeSend || null;
|
||||||
}
|
}
|
||||||
this.responses = [];
|
this.responses = [];
|
||||||
this.pendingResponses = [];
|
this.pendingResponses = [];
|
||||||
@ -368,7 +370,7 @@ export default {
|
|||||||
prompt: rawValue,
|
prompt: rawValue,
|
||||||
});
|
});
|
||||||
this.scrollResponsesToBottom();
|
this.scrollResponsesToBottom();
|
||||||
const message = await this.sendAiMessage(dialogId, this.formatPlainText(rawValue), modelOption.value);
|
const message = await this.sendAiMessage(dialogId, rawValue, modelOption.value);
|
||||||
if (responseEntry) {
|
if (responseEntry) {
|
||||||
responseEntry.userid = userid;
|
responseEntry.userid = userid;
|
||||||
responseEntry.message = message;
|
responseEntry.message = message;
|
||||||
@ -476,11 +478,11 @@ export default {
|
|||||||
const {data} = await this.$store.dispatch("call", {
|
const {data} = await this.$store.dispatch("call", {
|
||||||
url: 'dialog/msg/sendtext',
|
url: 'dialog/msg/sendtext',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: await this.buildPayloadData({
|
||||||
dialog_id: dialogId,
|
dialog_id: dialogId,
|
||||||
text,
|
text,
|
||||||
model_name: model,
|
model_name: model,
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
if (data) {
|
if (data) {
|
||||||
this.$store.dispatch("saveDialogMsg", data);
|
this.$store.dispatch("saveDialogMsg", data);
|
||||||
@ -494,15 +496,26 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将纯文本转换成HTML
|
* 构建最终发送的数据
|
||||||
*/
|
*/
|
||||||
formatPlainText(text) {
|
async buildPayloadData(data) {
|
||||||
const escaped = `${text}`
|
if (typeof this.inputOnBeforeSend !== 'function') {
|
||||||
.replace(/&/g, '&')
|
return data;
|
||||||
.replace(/</g, '<')
|
}
|
||||||
.replace(/>/g, '>')
|
try {
|
||||||
.replace(/\n/g, '<br/>');
|
const result = this.inputOnBeforeSend(data);
|
||||||
return `<p>${escaped}</p>`;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
max-height: calc(100vh - 344px);
|
||||||
|
@media (height <= 900px) {
|
||||||
|
max-height: calc(100vh - 214px);
|
||||||
|
}
|
||||||
|
|
||||||
.ai-assistant-output {
|
.ai-assistant-output {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #f8f9fb;
|
background: #f8f9fb;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
max-height: calc(100vh - 390px);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@media (height <= 900px) {
|
|
||||||
max-height: calc(100vh - 260px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-assistant-output-item + .ai-assistant-output-item {
|
.ai-assistant-output-item + .ai-assistant-output-item {
|
||||||
|
|||||||
@ -342,7 +342,7 @@ import clickoutside from "../../../../directives/clickoutside";
|
|||||||
import longpress from "../../../../directives/longpress";
|
import longpress from "../../../../directives/longpress";
|
||||||
import {inputLoadAdd, inputLoadIsLast, inputLoadRemove} from "./one";
|
import {inputLoadAdd, inputLoadIsLast, inputLoadRemove} from "./one";
|
||||||
import {languageList, languageName} from "../../../../language";
|
import {languageList, languageName} from "../../../../language";
|
||||||
import {isMarkdownFormat} from "../../../../utils/markdown";
|
import {isMarkdownFormat, MarkdownConver} from "../../../../utils/markdown";
|
||||||
import emitter from "../../../../store/events";
|
import emitter from "../../../../store/events";
|
||||||
import historyMixin from "./history";
|
import historyMixin from "./history";
|
||||||
|
|
||||||
@ -1904,63 +1904,45 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.dialogId) {
|
if (!this.dialogId) {
|
||||||
$A.messageWarning(this.$L('当前未选择会话'));
|
$A.messageWarning('当前未选择会话');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let canceled = false;
|
emitter.emit('openAIAssistant', {
|
||||||
$A.modalInput({
|
placeholder: this.$L('请简要描述消息的主题、语气或要点,AI 将生成完整消息'),
|
||||||
title: 'AI 生成',
|
onBeforeSend: async (sendData) => {
|
||||||
placeholder: '请简要描述消息的主题、语气或要点,AI 将生成完整消息',
|
if (!sendData) {
|
||||||
inputProps: {
|
return sendData;
|
||||||
type: 'textarea',
|
|
||||||
rows: 2,
|
|
||||||
autosize: {minRows: 2, maxRows: 6},
|
|
||||||
maxlength: 500,
|
|
||||||
},
|
|
||||||
onCancel: () => {
|
|
||||||
canceled = true;
|
|
||||||
},
|
|
||||||
onOk: (value) => {
|
|
||||||
if (!value) {
|
|
||||||
return '请输入消息需求';
|
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
if (canceled) {
|
const {data: extraData} = await this.$store.dispatch('call', {
|
||||||
reject();
|
url: 'dialog/msg/aiprompt',
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$store.dispatch('call', {
|
|
||||||
url: 'dialog/msg/ai_generate',
|
|
||||||
data: {
|
data: {
|
||||||
dialog_id: this.dialogId,
|
dialog_id: this.dialogId,
|
||||||
content: value,
|
content: sendData.text,
|
||||||
draft: this.value || '',
|
draft: this.value || '',
|
||||||
quote_id: this.quoteData?.id || 0,
|
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() {
|
onFullInput() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user