diff --git a/app/Http/Controllers/Api/AssistantController.php b/app/Http/Controllers/Api/AssistantController.php index 9f744f177..81a199241 100644 --- a/app/Http/Controllers/Api/AssistantController.php +++ b/app/Http/Controllers/Api/AssistantController.php @@ -3,8 +3,9 @@ namespace App\Http\Controllers\Api; use App\Models\User; +use App\Module\AI; +use App\Module\Apps; use App\Module\Base; -use App\Module\Ihttp; use Request; /** @@ -14,6 +15,11 @@ use Request; */ class AssistantController extends AbstractController { + public function __construct() + { + Apps::isInstalledThrow('ai'); + } + /** * @api {post} api/assistant/auth 生成授权码 * @@ -40,104 +46,28 @@ class AssistantController extends AbstractController $modelName = trim(Request::input('model_name', '')); $contextInput = Request::input('context', []); - if ($modelType === '' || $modelName === '') { - return Base::retError('参数错误'); - } - - if (is_string($contextInput)) { - $decoded = json_decode($contextInput, true); - if (json_last_error() === JSON_ERROR_NONE) { - $contextInput = $decoded; - } - } - if (!is_array($contextInput)) { - return Base::retError('context 参数格式错误'); - } - - $context = []; - foreach ($contextInput as $item) { - if (!is_array($item) || count($item) < 2) { - continue; - } - $role = trim((string)($item[0] ?? '')); - $message = trim((string)($item[1] ?? '')); - if ($role === '' || $message === '') { - continue; - } - $context[] = [$role, $message]; - } - - $contextJson = json_encode($context, JSON_UNESCAPED_UNICODE); - if ($contextJson === false) { - return Base::retError('context 参数格式错误'); - } + return AI::createStreamKey($modelType, $modelName, $contextInput); + } + /** + * @api {get} api/assistant/models 获取AI模型 + * + * @apiDescription 获取所有AI机器人模型设置 + * @apiVersion 1.0.0 + * @apiGroup assistant + * @apiName models + * + * @apiSuccess {Number} ret 返回状态码(1正确、0错误) + * @apiSuccess {String} msg 返回信息(错误描述) + * @apiSuccess {Object} data 返回数据 + */ + public function models() + { $setting = Base::setting('aibotSetting'); - $apiKey = Base::val($setting, $modelType . '_key'); - if ($modelType === 'wenxin') { - $wenxinSecret = Base::val($setting, 'wenxin_secret'); - if ($wenxinSecret) { - $apiKey = trim(($apiKey ?: '') . ':' . $wenxinSecret); - } - } - if ($modelType === 'ollama' && empty($apiKey)) { - $apiKey = Base::strRandom(6); - } - if (empty($apiKey)) { - return Base::retError('模型未启用'); - } + $setting = array_filter($setting, function ($value, $key) { + return str_ends_with($key, '_models') || str_ends_with($key, '_model'); + }, ARRAY_FILTER_USE_BOTH); - $remoteModelType = match ($modelType) { - 'qianwen' => 'qwen', - default => $modelType, - }; - - $authParams = [ - 'api_key' => $apiKey, - 'model_type' => $remoteModelType, - 'model_name' => $modelName, - 'context' => $contextJson, - ]; - - if ($setting[$modelType . '_base_url']) { - $authParams['base_url'] = $setting[$modelType . '_base_url']; - } - if ($setting[$modelType . '_agency']) { - $authParams['agency'] = $setting[$modelType . '_agency']; - } - - $thinkPatterns = [ - "/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", - "/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/" - ]; - $thinkMatch = []; - foreach ($thinkPatterns as $pattern) { - if (preg_match($pattern, $authParams['model_name'], $thinkMatch)) { - break; - } - } - if ($thinkMatch && !empty($thinkMatch[1])) { - $authParams['model_name'] = $thinkMatch[1]; - } - - $authResult = Ihttp::ihttp_post('http://nginx/ai/invoke/auth', $authParams, 30); - - if (Base::isError($authResult)) { - return Base::retError($authResult['msg']); - } - - $body = Base::json2array($authResult['data']); - if ($body['code'] !== 200) { - return Base::retError($body['error'] ?: 'AI 接口返回异常', $body); - } - - $streamKey = Base::val($body, 'data.stream_key'); - if (empty($streamKey)) { - return Base::retError('AI 接口返回数据异常'); - } - - return Base::retSuccess('success', [ - 'stream_key' => $streamKey, - ]); + return Base::retSuccess('success', $setting ?: json_decode('{}')); } } diff --git a/app/Http/Controllers/Api/SystemController.php b/app/Http/Controllers/Api/SystemController.php index ea16400db..aa5e9f0b5 100755 --- a/app/Http/Controllers/Api/SystemController.php +++ b/app/Http/Controllers/Api/SystemController.php @@ -333,27 +333,6 @@ class SystemController extends AbstractController return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}')); } - /** - * @api {get} api/system/setting/aibot_models 获取AI模型 - * - * @apiDescription 获取所有AI机器人模型设置 - * @apiVersion 1.0.0 - * @apiGroup system - * @apiName aibot_models - * - * @apiSuccess {Number} ret 返回状态码(1正确、0错误) - * @apiSuccess {String} msg 返回信息(错误描述) - * @apiSuccess {Object} data 返回数据 - */ - public function setting__aibot_models() - { - $setting = Base::setting('aibotSetting'); - $setting = array_filter($setting, function($value, $key) { - return str_ends_with($key, '_models') || str_ends_with($key, '_model'); - }, ARRAY_FILTER_USE_BOTH); - return Base::retSuccess('success', $setting ?: json_decode('{}')); - } - /** * @api {get} api/system/setting/checkin 获取签到设置、保存签到设置(限管理员) * diff --git a/app/Module/AI.php b/app/Module/AI.php index bfbe7c8fa..17d37e073 100644 --- a/app/Module/AI.php +++ b/app/Module/AI.php @@ -132,6 +132,126 @@ class AI return Base::retSuccess("success", $result); } + /** + * 生成 AI 流式会话凭证 + * @param string $modelType + * @param string $modelName + * @param mixed $contextInput + * @return array + */ + public static function createStreamKey($modelType, $modelName, $contextInput = []) + { + $modelType = trim((string)$modelType); + $modelName = trim((string)$modelName); + + if ($modelType === '' || $modelName === '') { + return Base::retError('参数错误'); + } + + if (is_string($contextInput)) { + $decoded = json_decode($contextInput, true); + if (json_last_error() === JSON_ERROR_NONE) { + $contextInput = $decoded; + } + } + + if (!is_array($contextInput)) { + return Base::retError('context 参数格式错误'); + } + + $context = []; + foreach ($contextInput as $item) { + if (!is_array($item) || count($item) < 2) { + continue; + } + $role = trim((string)($item[0] ?? '')); + $message = trim((string)($item[1] ?? '')); + if ($role === '' || $message === '') { + continue; + } + $context[] = [$role, $message]; + } + + $contextJson = json_encode($context, JSON_UNESCAPED_UNICODE); + if ($contextJson === false) { + return Base::retError('context 参数格式错误'); + } + + $setting = Base::setting('aibotSetting'); + if (!is_array($setting)) { + $setting = []; + } + + $apiKey = Base::val($setting, $modelType . '_key'); + if ($modelType === 'wenxin') { + $wenxinSecret = Base::val($setting, 'wenxin_secret'); + if ($wenxinSecret) { + $apiKey = trim(($apiKey ?: '') . ':' . $wenxinSecret); + } + } + if ($modelType === 'ollama' && empty($apiKey)) { + $apiKey = Base::strRandom(6); + } + if (empty($apiKey)) { + return Base::retError('模型未启用'); + } + + $remoteModelType = match ($modelType) { + 'qianwen' => 'qwen', + default => $modelType, + }; + + $authParams = [ + 'api_key' => $apiKey, + 'model_type' => $remoteModelType, + 'model_name' => $modelName, + 'context' => $contextJson, + ]; + + $baseUrl = trim((string)($setting[$modelType . '_base_url'] ?? '')); + if ($baseUrl !== '') { + $authParams['base_url'] = $baseUrl; + } + + $agency = trim((string)($setting[$modelType . '_agency'] ?? '')); + if ($agency !== '') { + $authParams['agency'] = $agency; + } + + $thinkPatterns = [ + "/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/", + "/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/" + ]; + $thinkMatch = []; + foreach ($thinkPatterns as $pattern) { + if (preg_match($pattern, $authParams['model_name'], $thinkMatch)) { + break; + } + } + if ($thinkMatch && !empty($thinkMatch[1])) { + $authParams['model_name'] = $thinkMatch[1]; + } + + $authResult = Ihttp::ihttp_post('http://nginx/ai/invoke/auth', $authParams, 30); + if (Base::isError($authResult)) { + return Base::retError($authResult['msg']); + } + + $body = Base::json2array($authResult['data']); + if (($body['code'] ?? null) !== 200) { + return Base::retError(($body['error'] ?? '') ?: 'AI 接口返回异常', $body); + } + + $streamKey = Base::val($body, 'data.stream_key'); + if (empty($streamKey)) { + return Base::retError('AI 接口返回数据异常'); + } + + return Base::retSuccess('success', [ + 'stream_key' => $streamKey, + ]); + } + /** ******************************************************************************************** */ /** ******************************************************************************************** */ /** ******************************************************************************************** */ @@ -146,6 +266,8 @@ class AI */ public static function transcriptions($filePath, $extParams = [], $extHeaders = [], $noCache = false) { + Apps::isInstalledThrow('ai'); + if (!file_exists($filePath)) { return Base::retError("语音文件不存在"); } @@ -202,6 +324,8 @@ class AI */ public static function translations($text, $targetLanguage, $noCache = false) { + Apps::isInstalledThrow('ai'); + $cacheKey = "openAItranslations::" . md5($text . '_' . $targetLanguage); if ($noCache) { Cache::forget($cacheKey); @@ -285,6 +409,10 @@ class AI */ public static function generateTitle($text, $noCache = false) { + if (!Apps::isInstalled('ai')) { + return Base::retError('应用「AI Assistant」未安装'); + } + $cacheKey = "openAIGenerateTitle::" . md5($text); if ($noCache) { Cache::forget($cacheKey); @@ -362,6 +490,10 @@ class AI */ public static function generateJokeAndSoup($noCache = false) { + if (!Apps::isInstalled('ai')) { + return Base::retError('应用「AI Assistant」未安装'); + } + $cacheKey = "openAIJokeAndSoup::" . md5(date('Y-m-d')); if ($noCache) { Cache::forget($cacheKey); diff --git a/app/Module/Apps.php b/app/Module/Apps.php index 1fcc0d75e..9ca9dff11 100644 --- a/app/Module/Apps.php +++ b/app/Module/Apps.php @@ -44,7 +44,7 @@ class Apps { if (!self::isInstalled($appId)) { $name = match ($appId) { - 'ai' => 'AI Robot', + 'ai' => 'AI Assistant', 'face' => 'Face check-in', 'appstore' => 'AppStore', 'approve' => 'Approval', diff --git a/app/Tasks/BotReceiveMsgTask.php b/app/Tasks/BotReceiveMsgTask.php index c3e172a20..127fc760f 100644 --- a/app/Tasks/BotReceiveMsgTask.php +++ b/app/Tasks/BotReceiveMsgTask.php @@ -446,7 +446,7 @@ class BotReceiveMsgTask extends AbstractTask } // 判断AI应用是否安装 if (!Apps::isInstalled('ai')) { - throw new Exception('应用「AI Robot」未安装'); + throw new Exception('应用「AI Assistant」未安装'); } // 整理机器人参数 $setting = Base::setting('aibotSetting'); diff --git a/resources/assets/js/components/AIAssistant.vue b/resources/assets/js/components/AIAssistant.vue index 68fdc0190..e42ff43e8 100644 --- a/resources/assets/js/components/AIAssistant.vue +++ b/resources/assets/js/components/AIAssistant.vue @@ -4,10 +4,7 @@ :title="$L('AI 助手')" :mask-closable="false" :closable="false" - :styles="{ - width: '90%', - maxWidth: shouldCreateNewSession ? '420px' : '600px', - }" + :width="shouldCreateNewSession ? '420px' : '600px'" class-name="ai-assistant-modal">