feat(ai): AI 模型列表支持 JSON 格式与按模型思考档位

- 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 <noreply@anthropic.com>
This commit is contained in:
kuaifan 2026-06-10 10:45:41 +00:00
parent ec1ab31b0e
commit e6ef85e176
3 changed files with 112 additions and 21 deletions

View File

@ -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

View File

@ -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

View File

@ -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());