mirror of
https://github.com/kuaifan/dootask.git
synced 2026-06-11 09:52:26 +00:00
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:
parent
ec1ab31b0e
commit
e6ef85e176
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
30
resources/assets/js/utils/ai.js
vendored
30
resources/assets/js/utils/ai.js
vendored
@ -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());
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user