post = $post ?? []; $this->headers = $headers ?? []; } /** * 设置请求参数 * @param array $post */ public function setPost($post) { $this->post = array_merge($this->post, $post); } /** * 设置请求头 * @param array $headers */ public function setHeaders($headers) { $this->headers = array_merge($this->headers, $headers); } /** * 设置请求路径 * @param string $urlPath */ public function setUrlPath($urlPath) { $this->urlPath = $urlPath; } /** * 设置请求超时时间 * @param int $timeout */ public function setTimeout($timeout) { $this->timeout = $timeout; } /** * 指定请求所使用的模型配置 * @param array $provider */ public function setProvider(array $provider) { $this->providerConfig = $provider; } /** * 请求 AI 接口 * @param bool $resRaw 是否返回原始数据 * @return array */ public function request($resRaw = false) { $provider = $this->providerConfig ?: self::resolveTextProvider(); if (!$provider) { return Base::retError("请先配置 AI 助手"); } $headers = [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $provider['api_key'], ]; if (!empty($provider['agency'])) { $headers['CURLOPT_PROXY'] = $provider['agency']; $headers['CURLOPT_PROXYTYPE'] = str_contains($provider['agency'], 'socks') ? CURLPROXY_SOCKS5 : CURLPROXY_HTTP; } $headers = array_merge($headers, $this->headers); $baseUrl = $provider['base_url'] ?: 'https://api.openai.com/v1'; $url = $baseUrl . ($this->urlPath ?: '/chat/completions'); $result = Ihttp::ihttp_request($url, $this->post, $headers, $this->timeout); if (Base::isError($result)) { return Base::retError("AI 接口请求失败", $result); } $result = $result['data']; if (!$resRaw) { $resData = Base::json2array($result); if (empty($resData['choices'])) { return Base::retError("AI 接口返回数据格式错误", $resData); } $result = $resData['choices'][0]['message']['content']; $result = trim($result); if (empty($result)) { return Base::retError("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_request('http://nginx/ai/invoke/auth', $authParams, [ 'Content-Type' => 'application/x-www-form-urlencoded', 'Authorization' => 'Bearer ' . Base::token(), ], 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, ]); } /** ******************************************************************************************** */ /** ******************************************************************************************** */ /** ******************************************************************************************** */ /** * 通过 openAI 语音转文字 * @param string $filePath 语音文件路径 * @param array $extParams 扩展参数 * @param array $extHeaders 扩展请求头 * @param bool $noCache 是否禁用缓存 * @return array */ public static function transcriptions($filePath, $extParams = [], $extHeaders = [], $noCache = false) { Apps::isInstalledThrow('ai'); if (!file_exists($filePath)) { return Base::retError("语音文件不存在"); } $cacheKey = "openAItranscriptions::" . md5($filePath . '_' . Base::array2json($extParams)); if ($noCache) { Cache::forget($cacheKey); } $audioProvider = self::resolveOpenAIAudioProvider(); if (!$audioProvider) { return Base::retError("请先在「AI 助手」设置中配置 OpenAI"); } $result = Cache::remember($cacheKey, Carbon::now()->addDays(), function () use ($extParams, $extHeaders, $filePath, $audioProvider) { $post = array_merge($extParams, [ 'file' => new \CURLFile($filePath), 'model' => 'whisper-1', ]); $header = array_merge($extHeaders, [ 'Content-Type' => 'multipart/form-data', ]); $ai = new self($post, $header); $ai->setProvider($audioProvider); $ai->setUrlPath('/audio/transcriptions'); $ai->setTimeout(15); $res = $ai->request(true); if (Base::isError($res)) { return Base::retError("语音转文字失败", $res); } $resData = Base::json2array($res['data']); if (empty($resData['text'])) { return Base::retError("语音转文字失败", $resData); } return Base::retSuccess("success", [ 'file' => $filePath, 'text' => $resData['text'], ]); }); if (Base::isError($result)) { Cache::forget($cacheKey); } return $result; } /** * 通过 openAI 翻译 * @param string $text 需要翻译的文本内容 * @param string $targetLanguage 目标语言(如:English, 简体中文, 日本語等) * @param bool $noCache 是否禁用缓存 * @return array */ public static function translations($text, $targetLanguage, $noCache = false) { Apps::isInstalledThrow('ai'); $cacheKey = "openAItranslations::" . md5($text . '_' . $targetLanguage); if ($noCache) { Cache::forget($cacheKey); } $provider = self::resolveTextProvider(); if (!$provider) { return Base::retError("请先配置 AI 助手"); } $result = Cache::remember($cacheKey, Carbon::now()->addDays(7), function () use ($text, $targetLanguage, $provider) { $payload = [ "model" => $provider['model'], "messages" => [ [ "role" => "system", "content" => << "user", "content" => "请将以下内容翻译为 {$targetLanguage}:\n\n{$text}" ] ], ]; if (self::shouldSendReasoningEffort($provider)) { $payload['reasoning_effort'] = 'minimal'; } $post = json_encode($payload); $ai = new self($post); $ai->setProvider($provider); $ai->setTimeout(60); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("翻译请求失败", $res); } $result = $res['data']; if (empty($result)) { return Base::retError("翻译结果为空"); } return Base::retSuccess("success", [ 'translated_text' => $result, 'target_language' => $targetLanguage, 'translated_at' => date('Y-m-d H:i:s') ]); }); if (Base::isError($result)) { Cache::forget($cacheKey); } return $result; } /** * 通过 openAI 生成标题 * @param string $text 需要生成标题的文本内容 * @param bool $noCache 是否禁用缓存 * @return array */ 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); } $provider = self::resolveTextProvider(); if (!$provider) { return Base::retError("请先配置 AI 助手"); } $result = Cache::remember($cacheKey, Carbon::now()->addHours(24), function () use ($text, $provider) { $payload = [ "model" => $provider['model'], "messages" => [ [ "role" => "system", "content" => << "user", "content" => "请为以下内容生成一个合适的标题:\n\n" . $text ] ], ]; if (self::shouldSendReasoningEffort($provider)) { $payload['reasoning_effort'] = 'minimal'; } $post = json_encode($payload); $ai = new self($post); $ai->setProvider($provider); $ai->setTimeout(10); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("标题生成失败", $res); } $result = $res['data']; if (empty($result)) { return Base::retError("生成的标题为空"); } return Base::retSuccess("success", [ 'title' => $result, 'length' => mb_strlen($result), 'generated_at' => date('Y-m-d H:i:s') ]); }); if (Base::isError($result)) { Cache::forget($cacheKey); } return $result; } /** * 通过 openAI 生成职场笑话、心灵鸡汤 * @param bool $noCache 是否禁用缓存 * @return array 返回20个笑话和20个心灵鸡汤 */ 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); } $provider = self::resolveTextProvider(); if (!$provider) { return Base::retError("请先配置 AI 助手"); } $result = Cache::remember($cacheKey, Carbon::now()->addHours(6), function () use ($provider) { $payload = [ "model" => $provider['model'], "messages" => [ [ "role" => "system", "content" => << "user", "content" => "请生成20个职场笑话和20个心灵鸡汤" ] ], ]; if (self::shouldSendReasoningEffort($provider)) { $payload['reasoning_effort'] = 'minimal'; } $post = json_encode($payload); $ai = new self($post); $ai->setProvider($provider); $ai->setTimeout(120); $res = $ai->request(); if (Base::isError($res)) { return Base::retError("生成失败", $res); } // 清理可能的markdown代码块标记 $content = $res['data']; $content = preg_replace('/^\s*```json\s*/', '', $content); $content = preg_replace('/\s*```\s*$/', '', $content); if (empty($content)) { return Base::retError("翻译结果为空"); } // 解析JSON $parsedData = Base::json2array($content); if (!$parsedData || !isset($parsedData['jokes']) || !isset($parsedData['soups'])) { return Base::retError("生成内容格式错误", $content); } // 验证数据完整性 if (!is_array($parsedData['jokes']) || !is_array($parsedData['soups'])) { return Base::retError("生成内容格式错误", $parsedData); } // 过滤空内容并确保有内容 $jokes = array_filter(array_map('trim', $parsedData['jokes'])); $soups = array_filter(array_map('trim', $parsedData['soups'])); if (empty($jokes) || empty($soups)) { return Base::retError("生成内容为空", $parsedData); } return Base::retSuccess("success", [ 'jokes' => array_values($jokes), 'soups' => array_values($soups), 'total_jokes' => count($jokes), 'total_soups' => count($soups), 'generated_at' => date('Y-m-d H:i:s') ]); }); if (Base::isError($result)) { Cache::forget($cacheKey); } return $result; } /** * 选择可用的文本模型配置 * @return array|null */ protected static function resolveTextProvider() { $setting = Base::setting('aibotSetting'); if (!is_array($setting)) { $setting = []; } foreach (self::TEXT_MODEL_PRIORITY as $vendor) { $config = self::buildProviderConfig($setting, $vendor); if ($config) { return $config; } } return null; } /** * 构建指定厂商的请求参数 * @param array $setting * @param string $vendor * @return array|null */ protected static function buildProviderConfig(array $setting, string $vendor) { $key = trim((string)($setting[$vendor . '_key'] ?? '')); $baseUrl = trim((string)($setting[$vendor . '_base_url'] ?? '')); $agency = trim((string)($setting[$vendor . '_agency'] ?? '')); switch ($vendor) { case 'openai': if ($key === '') { return null; } $baseUrl = $baseUrl ?: 'https://api.openai.com/v1'; $model = self::resolveOpenAITextModel($setting); break; case 'ollama': if ($baseUrl === '') { return null; } if ($key === '') { $key = Base::strRandom(6); } $model = trim((string)($setting[$vendor . '_model'] ?? '')); break; case 'wenxin': $secret = trim((string)($setting['wenxin_secret'] ?? '')); if ($key === '' || $secret === '' || $baseUrl === '') { return null; } $key = $key . ':' . $secret; $model = trim((string)($setting[$vendor . '_model'] ?? '')); break; default: if ($key === '' || $baseUrl === '') { return null; } $model = trim((string)($setting[$vendor . '_model'] ?? '')); break; } if ($model === '') { return null; } return [ 'vendor' => $vendor, 'model' => $model, 'api_key' => $key, 'base_url' => rtrim($baseUrl, '/'), 'agency' => $agency, ]; } /** * 解析 OpenAI 文本模型 * @param array $setting * @return string */ protected static function resolveOpenAITextModel(array $setting) { $models = Setting::AIBotModels2Array($setting['openai_models'] ?? '', true); if (in_array(self::OPENAI_DEFAULT_MODEL, $models, true)) { return self::OPENAI_DEFAULT_MODEL; } if (!empty($setting['openai_model'])) { return $setting['openai_model']; } return $models[0] ?? self::OPENAI_DEFAULT_MODEL; } /** * OpenAI 语音模型配置 * @return array|null */ protected static function resolveOpenAIAudioProvider() { $setting = Base::setting('aibotSetting'); if (!is_array($setting)) { $setting = []; } $key = trim((string)($setting['openai_key'] ?? '')); if ($key === '') { return null; } $baseUrl = trim((string)($setting['openai_base_url'] ?? '')); $baseUrl = $baseUrl ?: 'https://api.openai.com/v1'; $agency = trim((string)($setting['openai_agency'] ?? '')); return [ 'vendor' => 'openai', 'model' => 'whisper-1', 'api_key' => $key, 'base_url' => rtrim($baseUrl, '/'), 'agency' => $agency, ]; } /** * 是否需要附加 reasoning_effort 参数 * @param array $provider * @return bool */ protected static function shouldSendReasoningEffort(array $provider): bool { if (($provider['vendor'] ?? '') !== 'openai') { return false; } $model = $provider['model'] ?? ''; // 匹配 gpt- 开头后跟数字的模型名称 if (preg_match('/^gpt-(\d+)/', $model, $matches)) { return intval($matches[1]) >= 5; } return false; } }