From e6ef85e17629e980992fc53a3974d783f83c3973 Mon Sep 17 00:00:00 2001 From: kuaifan Date: Wed, 10 Jun 2026 10:45:41 +0000 Subject: [PATCH] =?UTF-8?q?feat(ai):=20AI=20=E6=A8=A1=E5=9E=8B=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=94=AF=E6=8C=81=20JSON=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E4=B8=8E=E6=8C=89=E6=A8=A1=E5=9E=8B=E6=80=9D=E8=80=83=E6=A1=A3?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Setting::AIBotModels2Array 解析 JSON 数组(含 thinking)并兼容旧 id|name 格式, 新增 AIBotModelThinking() 取模型思考档位;aibotSetting 规范化对 JSON 串透传 - BotReceiveMsgTask 据所选模型档位向 AI 插件透传 thinking_effort, 旧 (thinking)/-reasoning 后缀降级为 medium 兜底 - 前端 AIModelNames 解析器兼容 JSON 与旧 id|name 格式 Co-Authored-By: Claude Opus 4.8 --- app/Models/Setting.php | 70 +++++++++++++++++++++++++++++---- app/Tasks/BotReceiveMsgTask.php | 33 ++++++++++------ resources/assets/js/utils/ai.js | 30 +++++++++++++- 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 1bbfddebe..f96ed9299 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -89,11 +89,13 @@ class Setting extends AbstractModel $content = !empty($value[$key]) ? trim($value[$key]) : ''; switch ($fieldName) { case 'models': - if ($content) { + // 新 JSON 数组格式原样保留;仅旧的换行格式按行清洗 + if ($content && !str_starts_with($content, '[')) { $content = explode("\n", $content); $content = array_filter($content); + $content = implode("\n", $content); } - $content = is_array($content) ? implode("\n", $content) : ''; + $content = is_string($content) ? $content : ''; break; case 'model': $models = Setting::AIBotModels2Array($array[$key . 's'], true); @@ -219,15 +221,49 @@ class Setting extends AbstractModel */ public static function AIBotModels2Array($models, $retValue = false) { - $list = is_array($models) ? $models : explode("\n", $models); + $list = null; + if (is_array($models)) { + $list = $models; + } else { + $text = trim((string)$models); + if ($text !== '' && str_starts_with($text, '[')) { + $decoded = json_decode($text, true); + if (is_array($decoded)) { + $list = $decoded; + } + } + if ($list === null) { + $list = explode("\n", (string)$models); + } + } $array = []; foreach ($list as $item) { - $arr = Base::newTrim(explode('|', $item . '|')); - if ($arr[0]) { + if (is_array($item)) { + // 新 JSON 记录格式:{id,name,thinking}(兼容 {value,label}) + $value = trim((string)($item['id'] ?? $item['value'] ?? '')); + if ($value === '') { + continue; + } + $label = trim((string)($item['name'] ?? $item['label'] ?? '')); + $thinking = strtolower(trim((string)($item['thinking'] ?? 'off'))); + if (!in_array($thinking, ['off', 'low', 'medium', 'high'], true)) { + $thinking = 'off'; + } $array[] = [ - 'value' => $arr[0], - 'label' => $arr[1] ?: $arr[0] + 'value' => $value, + 'label' => $label !== '' ? $label : $value, + 'thinking' => $thinking, ]; + } else { + // 兼容旧字符串格式 "id|name" + $arr = Base::newTrim(explode('|', $item . '|')); + if ($arr[0]) { + $array[] = [ + 'value' => $arr[0], + 'label' => $arr[1] ?: $arr[0], + 'thinking' => 'off', + ]; + } } } if ($retValue) { @@ -236,6 +272,26 @@ class Setting extends AbstractModel return $array; } + /** + * 获取指定模型的思考档位(off|low|medium|high),未配置返回 off + * @param string|array $models 模型列表设置(JSON 字符串或旧格式) + * @param string $modelName 模型 ID + * @return string + */ + public static function AIBotModelThinking($models, $modelName) + { + $modelName = trim((string)$modelName); + if ($modelName === '') { + return 'off'; + } + foreach (self::AIBotModels2Array($models) as $item) { + if ($item['value'] === $modelName) { + return $item['thinking'] ?? 'off'; + } + } + return 'off'; + } + /** * 规范自定义微应用配置 * @param array $list diff --git a/app/Tasks/BotReceiveMsgTask.php b/app/Tasks/BotReceiveMsgTask.php index 58150b149..c9058a573 100644 --- a/app/Tasks/BotReceiveMsgTask.php +++ b/app/Tasks/BotReceiveMsgTask.php @@ -6,6 +6,7 @@ use App\Models\FileContent; use App\Models\Project; use App\Models\ProjectTask; use App\Models\Report; +use App\Models\Setting; use App\Models\User; use App\Models\UserBot; use App\Models\UserDepartment; @@ -469,21 +470,29 @@ class BotReceiveMsgTask extends AbstractTask if ($msg->msg['model_name']) { $extras['model_name'] = $msg->msg['model_name']; } - // 提取模型“思考”参数 - $thinkPatterns = [ - "/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", - "/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/" - ]; - $thinkMatch = []; - foreach ($thinkPatterns as $pattern) { - if (preg_match($pattern, $extras['model_name'], $thinkMatch)) { - break; + // 优先读取模型列表中按模型配置的思考档位(off|low|medium|high) + $thinkingEffort = Setting::AIBotModelThinking($setting[$type . '_models'] ?? '', $extras['model_name']); + // 兼容旧约定:模型名带 (thinking)/-reasoning 等后缀时,剥离后缀并视为 medium 档 + if ($thinkingEffort === 'off') { + $thinkPatterns = [ + "/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", + "/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/" + ]; + $thinkMatch = []; + foreach ($thinkPatterns as $pattern) { + if (preg_match($pattern, $extras['model_name'], $thinkMatch)) { + break; + } + } + if ($thinkMatch && !empty($thinkMatch[1])) { + $extras['model_name'] = $thinkMatch[1]; + $thinkingEffort = 'medium'; } } - if ($thinkMatch && !empty($thinkMatch[1])) { - $extras['model_name'] = $thinkMatch[1]; + if ($thinkingEffort !== 'off') { + $extras['thinking_effort'] = $thinkingEffort; $extras['max_tokens'] = 20000; - $extras['thinking'] = 4096; + $extras['thinking'] = 4096; // 兼容旧版插件 $extras['temperature'] = 1.0; } // 设定会话ID diff --git a/resources/assets/js/utils/ai.js b/resources/assets/js/utils/ai.js index 5a21bec45..84d1ddec8 100644 --- a/resources/assets/js/utils/ai.js +++ b/resources/assets/js/utils/ai.js @@ -180,11 +180,37 @@ const withLanguagePreferencePrompt = (prompt) => { /** * 解析模型列表文本为选项数组 - * 支持以 "|" 分隔显示名 + * 新格式:JSON 数组 [{id,name,thinking}];旧格式:每行 "id|name" */ const AIModelNames = (str) => { - const lines = str.split('\n').filter(line => line.trim()); + if (typeof str !== 'string') { + return []; + } + const trimmed = str.trim(); + // 新的 JSON 数组格式 + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + return parsed + .map(item => ({ + value: String(item?.id ?? item?.value ?? '').trim(), + label: String(item?.name ?? item?.label ?? '').trim() + })) + .filter(item => item.value) + .map(item => ({ + value: item.value, + label: item.label || item.value + })); + } + } catch (e) { + // 解析失败回退到旧格式 + } + } + + // 兼容旧的 "id|name" 换行格式 + const lines = str.split('\n').filter(line => line.trim()); return lines.map(line => { const [value, label] = line.split('|').map(s => s.trim());