AIGatewayService.php 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. <?php
  2. namespace app\service;
  3. use think\Db;
  4. use think\Queue;
  5. class AIGatewayService{
  6. /**
  7. * 根据模型与任务类型构建 API 请求体(主分发方法)
  8. * @param string $status_val 任务类型:图生文、文生文、文生图、图生图
  9. * @param string $model 模型名,如 gpt-4、gemini_flash、dall-e-3 等
  10. * @param string $prompt 提示词
  11. * @param string $size 尺寸或比例,如 850x1133 或 9:16(文生图/图生图用)
  12. * @param string $product_base64Data 产品图 base64(图生图用)
  13. * @param string $product_mimeType 产品图 MIME 类型(图生图用)
  14. * @param string $template_base64Data 模板图 base64(图生图用)
  15. * @param string $template_mimeType 模板图 MIME 类型(图生图用)
  16. * @return array API 响应
  17. * @throws \Exception
  18. */
  19. public function buildRequestData(
  20. string $status_val,
  21. string $model,
  22. string $prompt,
  23. string $size = '',
  24. string $product_base64Data = '',
  25. string $product_mimeType = '',
  26. string $template_base64Data = '',
  27. string $template_mimeType = ''
  28. ) {
  29. // 1. 通用参数校验(提前拦截无效参数)
  30. $validStatus = ['图生文', '文生文', '文生图', '图生图'];
  31. if (!in_array($status_val, $validStatus)) {
  32. throw new \Exception("无效的任务类型: {$status_val}");
  33. }
  34. // 2. 按「模型+任务类型」分发到对应细分方法
  35. switch (true) {
  36. // 文生文
  37. case $status_val === '文生文' && $model === 'gemini_flash':
  38. $data = $this->buildText2TextGeminiFlash($prompt);
  39. break;
  40. case $status_val === '文生文' && $model === 'gpt-4':
  41. $data = $this->buildText2TextGpt4($prompt);
  42. break;
  43. // 文生图
  44. case $status_val === '文生图' && $model === 'dall-e-3':
  45. $data = $this->buildText2ImageDallE3($prompt, $size);
  46. break;
  47. case $status_val === '文生图' && $model === 'gemini-3-pro-preview':
  48. $data = $this->buildText2ImageGemini3Pro($prompt, $size);
  49. break;
  50. // 图生文
  51. case $status_val === '图生文' && $model === 'gemini-3-pro-preview':
  52. $data = $this->buildImage2TextGemini3Pro($prompt, $product_base64Data, $product_mimeType);
  53. break;
  54. // 图生图
  55. case $status_val === '图生图' && $model === 'gemini-3-pro-image-preview':
  56. $data = $this->buildImage2ImageGemini3ProImage($prompt, $size, $product_base64Data, $product_mimeType, $template_base64Data, $template_mimeType);
  57. break;
  58. case $status_val === '图生图' && $model === 'gemini-3.1-flash-image-preview':
  59. $data = $this->buildImage2ImageGemini31Flash($prompt, $size, $product_base64Data, $product_mimeType, $template_base64Data, $template_mimeType);
  60. break;
  61. // 未匹配的组合
  62. default:
  63. throw new \Exception("未配置模型+任务类型组合: {$model}({$status_val})");
  64. }
  65. // 3. 统一调用 API(主方法只做分发,不耦合具体逻辑)
  66. return $this->callApi($data, $model);
  67. }
  68. // -------------------------- 细分方法:按「任务类型+模型」拆分 --------------------------
  69. /**
  70. * 文生文 - gemini_flash 模型
  71. */
  72. private function buildText2TextGeminiFlash(string $prompt): array
  73. {
  74. return [
  75. "contents" => [
  76. [
  77. "role" => "user",
  78. "parts" => [["text" => $prompt]]
  79. ]
  80. ],
  81. "generationConfig" => [
  82. "responseModalities" => ["TEXT"],
  83. "maxOutputTokens" => 1024,
  84. "temperature" => 0.7,
  85. "language" => "zh-CN"
  86. ]
  87. ];
  88. }
  89. /**
  90. * 文生文 - gpt-4 模型
  91. */
  92. private function buildText2TextGpt4(string $prompt): array
  93. {
  94. return [
  95. 'model' => 'gpt-4',
  96. 'messages' => [['role' => 'user', 'content' => $prompt]],
  97. 'temperature' => 0.7,
  98. 'max_tokens' => 1024
  99. ];
  100. }
  101. /**
  102. * 文生图 - dall-e-3 模型
  103. */
  104. private function buildText2ImageDallE3(string $prompt, string $size): array
  105. {
  106. return [
  107. 'group' => 'OpenAI',
  108. 'prompt' => $prompt,
  109. 'model' => 'dall-e-3',
  110. 'n' => 1,
  111. 'size' => $size,
  112. 'quality' => 'hd',
  113. 'style' => 'vivid',
  114. 'response_format' => 'url',
  115. ];
  116. }
  117. /**
  118. * 文生图 - gemini-3-pro-preview 模型
  119. */
  120. private function buildText2ImageGemini3Pro(string $prompt, string $size): array
  121. {
  122. $supportedAspectRatios = ['1:1', '4:3', '3:4', '16:9', '9:16'];
  123. $aspectRatio = (in_array($size, $supportedAspectRatios)) ? $size : '1:1';
  124. return [
  125. "model" => "gemini-3-pro-preview",
  126. "prompt" => $prompt,
  127. "size" => $aspectRatio,
  128. "n" => 1,
  129. "quality" => "standard",
  130. "response_format" => "url"
  131. ];
  132. }
  133. /**
  134. * 图生文 - gemini-3-pro-preview 模型
  135. */
  136. private function buildImage2TextGemini3Pro(string $prompt, string $productBase64, string $productMimeType): array
  137. {
  138. return [
  139. "contents" => [
  140. [
  141. "role" => "user",
  142. "parts" => [
  143. ["inlineData" => ["mimeType" => $productMimeType, "data" => $productBase64]],
  144. ["text" => $prompt]
  145. ]
  146. ]
  147. ],
  148. "generationConfig" => [
  149. "responseModalities" => ["TEXT"],
  150. "maxOutputTokens" => 1000,
  151. "temperature" => 0.7,
  152. "language" => "zh-CN"
  153. ]
  154. ];
  155. }
  156. /**
  157. * 图生图 - gemini-3-pro-image-preview 模型
  158. */
  159. private function buildImage2ImageGemini3ProImage(
  160. string $prompt,
  161. string $size,
  162. string $productBase64,
  163. string $productMimeType,
  164. string $templateBase64,
  165. string $templateMimeType
  166. ): array {
  167. // 尺寸转标准比例
  168. if (!empty($size) && strpos($size, 'x') !== false) {
  169. $parts = explode('x', trim($size), 2);
  170. if (count($parts) == 2) {
  171. $w = (int)$parts[0];
  172. $h = (int)$parts[1];
  173. if ($w > 0 && $h > 0) {
  174. $ratio = $w / $h;
  175. $standard = [['1:1', 1], ['4:3', 4/3], ['3:4', 3/4], ['16:9', 16/9], ['9:16', 9/16]];
  176. $minDiff = PHP_FLOAT_MAX;
  177. foreach ($standard as $r) {
  178. $diff = abs($ratio - $r[1]);
  179. if ($diff < $minDiff) {
  180. $minDiff = $diff;
  181. $size = $r[0];
  182. }
  183. }
  184. }
  185. }
  186. }
  187. return [
  188. 'contents' => [
  189. [
  190. 'role' => 'user',
  191. 'parts' => [
  192. ['text' => $prompt],
  193. ['inline_data' => ['mime_type' => $productMimeType, 'data' => $productBase64]],
  194. ['inline_data' => ['mime_type' => $templateMimeType, 'data' => $templateBase64]]
  195. ]
  196. ]
  197. ],
  198. 'generationConfig' => [
  199. 'responseModalities' => ['IMAGE'],
  200. 'imageConfig' => ['aspectRatio' => $size, 'imageSize' => '1k']
  201. ]
  202. ];
  203. }
  204. /**
  205. * 图生图 - gemini-3.1-flash-image-preview 模型
  206. */
  207. private function buildImage2ImageGemini31Flash(
  208. string $prompt,
  209. string $size,
  210. string $productBase64,
  211. string $productMimeType,
  212. string $templateBase64,
  213. string $templateMimeType
  214. ): array {
  215. return [
  216. 'model' => 'gemini-3.1-flash-image-preview',
  217. 'messages' => [
  218. [
  219. 'role' => 'user',
  220. 'content' => [
  221. ['type' => 'text', 'text' => $prompt],
  222. [
  223. 'type' => 'image_url',
  224. 'image_url' => ['url' => 'data:' . $productMimeType . ';base64,' . $productBase64]
  225. ],
  226. [
  227. 'type' => 'image_url',
  228. 'image_url' => ['url' => 'data:' . $templateMimeType . ';base64,' . $templateBase64]
  229. ]
  230. ]
  231. ]
  232. ],
  233. 'response_modalities' => ['image'],
  234. 'image_config' => [
  235. 'aspect_ratio' => $size,
  236. 'quality' => 'high',
  237. 'width' => '850',
  238. 'height' => '1133'
  239. ],
  240. 'temperature' => 0.3,
  241. 'top_p' => 0.8,
  242. 'max_tokens' => 2048
  243. ];
  244. }
  245. // /**
  246. // * 根据模型与任务类型构建 API 请求体
  247. // * @param string $status_val 任务类型:图生文、文生文、文生图、图生图
  248. // * @param string $model 模型名,如 gpt-4、gemini_flash、dall-e-3 等
  249. // * @param string $prompt 提示词
  250. // * @param string $size 尺寸或比例,如 850x1133 或 9:16(文生图/图生图用)
  251. // * @param string $product_base64Data 产品图 base64(图生图用)
  252. // * @param string $product_mimeType 产品图 MIME 类型(图生图用)
  253. // * @param string $template_base64Data 模板图 base64(图生图用)
  254. // * @param string $template_mimeType 模板图 MIME 类型(图生图用)
  255. // * @return array API 响应
  256. // */
  257. //
  258. // public function buildRequestData($status_val,$model,$prompt,$size='',$product_base64Data='',$product_mimeType='',$template_base64Data='',$template_mimeType='')
  259. // {
  260. // //判断使用哪个模型、在判断此模型使用类型
  261. // if ($model === 'gemini_flash') {
  262. // if($status_val == '文生文'){
  263. // // 使用Gemini API的正确参数格式
  264. // $data = [
  265. // "contents" => [
  266. // [
  267. // "role" => "user",
  268. // "parts" => [
  269. // ["text" => $prompt]
  270. // ]
  271. // ]
  272. // ],
  273. // "generationConfig" => [
  274. // "responseModalities" => ["TEXT"],
  275. // "maxOutputTokens" => 1024,
  276. // "temperature" => 0.7,
  277. // "language" => "zh-CN"
  278. // ]
  279. // ];
  280. // return $this->callApi($data,$model);
  281. // }
  282. // }else if ($model === 'gpt-4') {
  283. // if($status_val == '文生文'){
  284. // // 使用OpenAI API格式
  285. // $data = [
  286. // 'model' => $model,
  287. // 'messages' => [
  288. // ['role' => 'user', 'content' => $prompt]
  289. // ],
  290. // 'temperature' => 0.7,
  291. // 'max_tokens' => 1024
  292. // ];
  293. // return $this->callApi($data,$model);
  294. // }
  295. // }else if($model == 'dall-e-3'){
  296. // if($status_val == '文生图'){
  297. // $data = [
  298. // 'group' => 'OpenAI',
  299. // 'prompt' => $prompt,
  300. // 'model' => $model,
  301. // 'n' => 1,
  302. // 'size' => $size,
  303. // 'quality' => 'hd',
  304. // 'style' => 'vivid',
  305. // 'response_format' => 'url',
  306. // ];
  307. // return $this->callApi($data,$model);
  308. // }
  309. // }else if($model == 'gemini-3-pro-image-preview'){
  310. // if($status_val == '图生图'){
  311. // // 宽高(850x1133)转标准比例(如3:4),模型需 aspectRatio 非尺寸
  312. // if (!empty($size) && strpos($size, 'x') !== false) {
  313. // $parts = explode('x', trim($size), 2);
  314. // if (count($parts) == 2) {
  315. // $w = (int)$parts[0];
  316. // $h = (int)$parts[1];
  317. // if ($w > 0 && $h > 0) {
  318. // $ratio = $w / $h;
  319. // $standard = [['1:1', 1], ['4:3', 4/3], ['3:4', 3/4], ['16:9', 16/9], ['9:16', 9/16]];
  320. // $minDiff = PHP_FLOAT_MAX;
  321. // foreach ($standard as $r) {
  322. // $diff = abs($ratio - $r[1]);
  323. // if ($diff < $minDiff) {
  324. // $minDiff = $diff;
  325. // $size = $r[0];
  326. // }
  327. // }
  328. // }
  329. // }
  330. // }
  331. // $data = [
  332. // 'contents' => [
  333. // [
  334. // 'role' => 'user',
  335. // 'parts' => [
  336. // ['text' => $prompt],
  337. // [
  338. // 'inline_data' => [
  339. // 'mime_type' => $product_mimeType,
  340. // 'data' => $product_base64Data
  341. // ]
  342. // ],
  343. // [
  344. // 'inline_data' => [
  345. // 'mime_type' => $template_mimeType,
  346. // 'data' => $template_base64Data
  347. // ]
  348. // ]
  349. // ]
  350. // ]
  351. // ],
  352. // 'generationConfig' => [
  353. // 'responseModalities' => ['IMAGE'],
  354. // 'imageConfig' => [
  355. // 'aspectRatio' => $size,
  356. // 'imageSize' => '1k'
  357. // ]
  358. // ]
  359. // ];
  360. // return $this->callApi($data,$model);
  361. // }
  362. // }else if($model == 'gemini-3-pro-preview'){
  363. // if($status_val == '图生文'){
  364. // $data = [
  365. // "contents" => [
  366. // [
  367. // "role" => "user",
  368. // "parts" => [
  369. // ["inlineData" => [
  370. // "mimeType" => $product_mimeType,
  371. // "data" => $product_base64Data
  372. // ]],
  373. // ["text" => $prompt]
  374. // ]
  375. // ]
  376. // ],
  377. // "generationConfig" => [
  378. // "responseModalities" => ["TEXT"],
  379. // "maxOutputTokens" => 1000,
  380. // "temperature" => 0.7,
  381. // "language" => "zh-CN"
  382. // ]
  383. // ];
  384. // return $this->callApi($data,$model);
  385. // }else if($status_val == '文生图'){
  386. // // 支持的宽高比(与官方保持一致)
  387. // $supportedAspectRatios = ['1:1', '4:3', '3:4', '16:9', '9:16'];
  388. //
  389. // // 解析并验证宽高比
  390. // $size = $_POST['size'] ?? '';
  391. // if (!empty($size) && strpos($size, ':') !== false && in_array($size, $supportedAspectRatios)) {
  392. // $aspectRatio = $size;
  393. // } else {
  394. // $aspectRatio = "1:1"; // 默认宽高比
  395. // }
  396. //
  397. // // 构建符合 /v1/images/generations 接口规范的请求参数
  398. // $data = [
  399. // "model" => $model, // 直接使用传入的模型名,如 "gemini-3-pro-image-preview"
  400. // "prompt" => $prompt, // 提示词
  401. // "size" => $aspectRatio, // 宽高比,如 "9:16"
  402. // "n" => 1, // 生成数量
  403. // "quality" => "standard", // 生成质量
  404. // "response_format" => "url" // 输出格式:url 或 b64_json
  405. // ];
  406. // return $this->callApi($data,$model);
  407. // }
  408. // }else if($model == 'gemini-3.1-flash-image-preview'){
  409. // if($status_val == '图生图'){
  410. // $data = [
  411. // 'model' => $model,
  412. // 'messages' => [
  413. // [
  414. // 'role' => 'user',
  415. // 'content' => [
  416. // ['type' => 'text', 'text' => $prompt],
  417. // [
  418. // 'type' => 'image_url',
  419. // 'image_url' => [
  420. // 'url' => 'data:' . $product_mimeType . ';base64,' . $product_base64Data
  421. // ]
  422. // ],
  423. // [
  424. // 'type' => 'image_url',
  425. // 'image_url' => [
  426. // 'url' => 'data:' . $template_mimeType . ';base64,' . $template_base64Data
  427. // ]
  428. // ]
  429. // ]
  430. // ]
  431. // ],
  432. // 'response_modalities' => ['image'],
  433. // 'image_config' => [
  434. // 'aspect_ratio' => $size,
  435. // 'quality' => 'high',
  436. // 'width' => '850',
  437. // 'height' => '1133'
  438. // ],
  439. // 'temperature' => 0.3,
  440. // 'top_p' => 0.8,
  441. // 'max_tokens' => 2048
  442. // ];
  443. // return $this->callApi($data,$model);
  444. // }
  445. // }else{
  446. // // 处理其他模型或抛出异常
  447. // throw new \Exception("未配置模型类型: {$model}");
  448. // }
  449. // }
  450. /**
  451. * 构建视频请求体
  452. * $status_val == 文生视频、图生视频、首图尾图生视频
  453. * $model 模型
  454. * $prompt 提示词
  455. * $size 尺寸大小
  456. * $seconds 时间
  457. */
  458. public function Txt_to_video($status_val,$prompt, $model, $size, $seconds)
  459. {
  460. //判断使用哪个模型、在判断此模型使用类型
  461. if ($model === 'sora-2') {
  462. if ($status_val == '文生视频') {
  463. $data = [
  464. 'prompt' => trim($prompt),
  465. 'model' => $model ?: 'sora-2',
  466. 'seconds' => (string)((int)$seconds ?: 5),
  467. 'size' => $size ?: '1920x1080'
  468. ];
  469. return self::callApi($this->config['sora-2']['api_url'], $this->config['sora-2']['api_key'], $data, 300);
  470. }
  471. }else{
  472. throw new \Exception("未配置模型类型: {$model}");
  473. }
  474. }
  475. /**
  476. * 计算最大公约数
  477. */
  478. public function gcd($a, $b) {
  479. while ($b != 0) {
  480. $temp = $a % $b;
  481. $a = $b;
  482. $b = $temp;
  483. }
  484. return $a;
  485. }
  486. /**
  487. * 通用 API 调用方法(支持多接口故障自动切换)
  488. *
  489. * @param array $data 请求数据(JSON 格式)
  490. * @param string $model 模型名
  491. * @param int $timeout 请求超时时间(秒)
  492. * @return array 接口响应数据(成功时返回解析后的数组)
  493. * @throws \Exception 所有接口都失败时抛出异常
  494. */
  495. public function callApi(array $data, string $model, int $timeout = 60): array
  496. {
  497. $allErrors = [];
  498. $httpCodes = [];
  499. $curlErrnos = [];
  500. // 1. 预加载该模型的所有API配置(按优先级排序,仅启用状态)
  501. $aiModelConfigs = Db::name("ai_model")
  502. ->where('model_name', $model)
  503. ->where('status', '1')
  504. ->order('sort ASC')
  505. ->select();
  506. if (empty($aiModelConfigs)) {
  507. throw new \Exception("模型配置不存在: {$model}");
  508. }
  509. // 2. 提前序列化JSON(只做一次,避免重复编码)
  510. $postData = json_encode($data, JSON_UNESCAPED_UNICODE);
  511. if (json_last_error() !== JSON_ERROR_NONE) {
  512. throw new \Exception("请求数据JSON编码失败: " . json_last_error_msg());
  513. }
  514. // 3. 遍历所有接口,逐个尝试调用
  515. foreach ($aiModelConfigs as $index => $config) {
  516. $ch = null;
  517. try {
  518. // 跳过空配置(同时记录到错误数组,避免最终汇总时 $httpCodes/$curlErrnos 为空导致未定义下标)
  519. if (empty($config['api_url']) || empty($config['api_key'])) {
  520. $allErrors[] = "第" . ($index + 1) . "个接口配置无效(URL/Key为空)";
  521. $httpCodes[] = 0;
  522. $curlErrnos[] = 0;
  523. continue;
  524. }
  525. // 初始化curl(每个接口新建连接,避免复用导致的问题)
  526. $ch = curl_init();
  527. $curlOptions = [
  528. CURLOPT_URL => $config['api_url'],
  529. CURLOPT_RETURNTRANSFER => true,
  530. CURLOPT_POST => true,
  531. CURLOPT_POSTFIELDS => $postData,
  532. CURLOPT_HTTPHEADER => [
  533. 'Content-Type: application/json',
  534. 'Authorization: Bearer ' . $config['api_key'],
  535. 'Connection: keep-alive',
  536. 'Keep-Alive: 300'
  537. ],
  538. CURLOPT_TIMEOUT => $timeout,
  539. CURLOPT_CONNECTTIMEOUT => 15,
  540. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  541. CURLOPT_FAILONERROR => false,
  542. // 生产环境建议开启SSL验证(需配置CA证书)
  543. CURLOPT_SSL_VERIFYPEER => true,
  544. CURLOPT_SSL_VERIFYHOST => 2,
  545. CURLOPT_TCP_NODELAY => true,
  546. CURLOPT_FORBID_REUSE => false,
  547. CURLOPT_FRESH_CONNECT => false
  548. ];
  549. curl_setopt_array($ch, $curlOptions);
  550. // 执行请求
  551. $response = curl_exec($ch);
  552. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  553. $curlErrno = curl_errno($ch);
  554. $curlError = curl_error($ch);
  555. // 检查CURL底层错误
  556. if ($response === false) {
  557. $errorMsg = "第" . ($index + 1) . "个接口CURL失败 [{$curlErrno}]: {$curlError}";
  558. $allErrors[] = $errorMsg;
  559. $httpCodes[] = $httpCode;
  560. $curlErrnos[] = $curlErrno;
  561. continue; // 跳过当前接口,尝试下一个
  562. }
  563. // 解析响应
  564. $result = empty($response) ? [] : json_decode($response, true);
  565. if (json_last_error() !== JSON_ERROR_NONE && !empty($response)) {
  566. $errorMsg = "第" . ($index + 1) . "个接口响应解析失败: " . json_last_error_msg();
  567. $allErrors[] = $errorMsg;
  568. $httpCodes[] = $httpCode;
  569. $curlErrnos[] = $curlErrno;
  570. continue;
  571. }
  572. // 检查API业务错误
  573. if (isset($result['error'])) {
  574. $apiErrorDetail = $result['error']['message'] ?? '';
  575. $errorType = $result['error']['type'] ?? '';
  576. $errorCode = $result['error']['code'] ?? '';
  577. $errorMessages = [
  578. 'invalid_request_error' => '请求参数错误',
  579. 'authentication_error' => '认证失败',
  580. 'rate_limit_error' => '请求频率过高',
  581. 'insufficient_quota' => '额度不足',
  582. 'billing_not_active' => '账户未开通付费',
  583. 'content_policy_violation' => '内容违反政策',
  584. 'model_not_found' => '模型不存在或无可用渠道'
  585. ];
  586. $friendlyMessage = $errorMessages[$errorCode] ?? ($errorMessages[$errorType] ?? 'API服务错误');
  587. $detailedError = "第" . ($index + 1) . "个接口{$friendlyMessage}";
  588. if ($errorCode) $detailedError .= " (错误代码: {$errorCode})";
  589. if ($apiErrorDetail) $detailedError .= ": {$apiErrorDetail}";
  590. $allErrors[] = $detailedError;
  591. $httpCodes[] = $httpCode;
  592. $curlErrnos[] = $curlErrno;
  593. continue;
  594. }
  595. // 检查HTTP状态码
  596. if ($httpCode !== 200) {
  597. $statusMessages = [
  598. 400 => '请求参数不合法',
  599. 401 => 'API密钥无效或权限不足',
  600. 403 => '访问被拒绝',
  601. 404 => 'API端点不存在',
  602. 429 => '请求过于频繁,请稍后再试',
  603. 500 => '服务器内部错误',
  604. 503 => '服务暂时不可用'
  605. ];
  606. $statusMessage = $statusMessages[$httpCode] ?? "HTTP错误({$httpCode})";
  607. $allErrors[] = "第" . ($index + 1) . "个接口{$statusMessage}";
  608. $httpCodes[] = $httpCode;
  609. $curlErrnos[] = $curlErrno;
  610. continue;
  611. }
  612. // 任意一个接口成功,直接返回结果
  613. curl_close($ch);
  614. return $result;
  615. } catch (\Exception $e) {
  616. // 捕获当前接口的异常,记录后尝试下一个
  617. $allErrors[] = "第" . ($index + 1) . "个接口异常: " . $e->getMessage();
  618. $httpCodes[] = 0;
  619. $curlErrnos[] = 0;
  620. if (is_resource($ch)) {
  621. curl_close($ch);
  622. }
  623. continue;
  624. }
  625. }
  626. // 所有接口都失败,抛出汇总异常
  627. $finalError = "所有API接口调用失败(共尝试" . count($aiModelConfigs) . "个接口)\n";
  628. $finalError .= "失败详情:\n- " . implode("\n- ", $allErrors) . "\n";
  629. $lastHttpCode = $httpCodes[count($httpCodes) - 1] ?? 0;
  630. $lastCurlErrno = $curlErrnos[count($curlErrnos) - 1] ?? 0;
  631. $finalError .= "建议解决方案: " . $this->getErrorSolution($lastHttpCode, $lastCurlErrno) . "\n";
  632. throw new \Exception($finalError);
  633. }
  634. /**
  635. * 获取错误原因
  636. */
  637. private function getErrorCause(int $httpCode, string $apiErrorDetail, int $curlErrno): string
  638. {
  639. $causeMap = [
  640. // HTTP状态码
  641. 400 => '参数格式错误/必填参数缺失',
  642. 401 => 'API Key无效/过期/无权限',
  643. 429 => '超出接口调用频率限制',
  644. 500 => '服务端内部故障',
  645. 503 => '服务维护/算力不足',
  646. // CURL错误码
  647. CURLE_OPERATION_TIMEDOUT => '请求超时(模型处理耗时超过设置值)',
  648. CURLE_COULDNT_CONNECT => '无法连接到API服务器',
  649. CURLE_SSL_CACERT => 'SSL证书验证失败',
  650. // 业务错误关键词
  651. 'model_not_found' => '模型渠道未开通/无可用资源',
  652. 'invalid_size' => '模型尺寸参数不符合要求',
  653. ];
  654. // 优先匹配CURL错误码
  655. if (isset($causeMap[$curlErrno])) {
  656. return $causeMap[$curlErrno];
  657. }
  658. // 匹配HTTP状态码
  659. if (isset($causeMap[$httpCode])) {
  660. return $causeMap[$httpCode];
  661. }
  662. // 匹配业务错误关键词
  663. foreach ($causeMap as $key => $cause) {
  664. if (is_string($key) && strpos($apiErrorDetail, $key) !== false) {
  665. return $cause;
  666. }
  667. }
  668. // 特殊关键词匹配
  669. if (strpos($apiErrorDetail, 'No available capacity') !== false) {
  670. return '模型算力不足,无可用资源';
  671. } elseif (strpos($apiErrorDetail, 'size is invalid') !== false) {
  672. return '模型尺寸参数无效';
  673. }
  674. // 兜底
  675. return $apiErrorDetail ?: '未知原因';
  676. }
  677. /**
  678. * 获取错误解决方案
  679. */
  680. private function getErrorSolution(int $httpCode, int $curlErrno): string
  681. {
  682. $solutionMap = [
  683. // HTTP状态码
  684. 400 => '1. 检查参数是否完整 2. 确认参数类型 3. 验证尺寸/模型名是否合法',
  685. 401 => '1. 检查API Key是否正确 2. 确认Key未过期/有对应模型权限',
  686. 429 => '1. 降低请求频率 2. 等待1-5分钟后重试 3. 联系服务商提升配额',
  687. 500 => '1. 等待几分钟后重试 2. 联系API服务商排查',
  688. 503 => '1. 等待算力释放 2. 联系服务商扩容',
  689. // CURL错误码
  690. CURLE_OPERATION_TIMEDOUT => '1. 延长超时时间 2. 检查模型生成耗时 3. 重试请求',
  691. CURLE_COULDNT_CONNECT => '1. 检查API地址是否正确 2. 验证网络连通性 3. 检查防火墙配置',
  692. CURLE_SSL_CACERT => '1. 开启SSL证书验证 2. 配置正确的CA证书路径 3. 确认API域名证书有效',
  693. ];
  694. if (isset($solutionMap[$curlErrno])) {
  695. return $solutionMap[$curlErrno];
  696. }
  697. if (isset($solutionMap[$httpCode])) {
  698. return $solutionMap[$httpCode];
  699. }
  700. return '1. 等待几分钟后重试 2. 检查API服务提供商状态 3. 联系服务提供商确认服务可用性';
  701. }
  702. // /**
  703. // * 通用 API 调用方法(支持重试机制)
  704. // *
  705. // * @param string $url 接口地址
  706. // * @param string $apiKey 授权密钥(Bearer Token)
  707. // * @param array $data 请求数据(JSON 格式)
  708. // *
  709. // * 功能说明:
  710. // * - 使用 cURL 发送 POST 请求到指定 API 接口
  711. // * - 设置请求头和超时时间等参数
  712. // * - 支持最多重试 2 次,当接口调用失败时自动重试
  713. // * - 返回成功时解析 JSON 响应为数组
  714. // *
  715. // * 异常处理:
  716. // * - 若全部重试失败,将抛出异常并包含最后一次错误信息
  717. // *
  718. // * @return array 接口响应数据(成功时返回解析后的数组)
  719. // * @throws \Exception 接口请求失败时抛出异常
  720. // */
  721. // public static function callApi($data,$model,$timeout = 60)
  722. // {
  723. // $maxRetries = 0; // 减少重试次数为0,避免不必要的等待
  724. // $attempt = 0;
  725. // $lastError = '';
  726. // $httpCode = 0;
  727. // $apiErrorDetail = '';
  728. //
  729. // $ai_model = Db::name("ai_model")
  730. // ->where('model_name',$model)
  731. // ->select();
  732. // $num = 0;
  733. // while ($attempt <= $maxRetries) {
  734. // try {
  735. // if(!$ai_model[$num]){
  736. // throw new \Exception("请求发送失败: " . $curlError);
  737. // }
  738. // $ch = curl_init();
  739. // curl_setopt_array($ch, [
  740. // CURLOPT_URL => $ai_model[$num]['api_url'],
  741. // CURLOPT_RETURNTRANSFER => true,
  742. // CURLOPT_POST => true,
  743. // CURLOPT_POSTFIELDS => json_encode($data, JSON_UNESCAPED_UNICODE),
  744. // CURLOPT_HTTPHEADER => [
  745. // 'Content-Type: application/json',
  746. // 'Authorization: Bearer ' . $ai_model[$num]['api_key']
  747. // ],
  748. // CURLOPT_TIMEOUT => (int) $timeout,
  749. // CURLOPT_SSL_VERIFYPEER => false,
  750. // CURLOPT_SSL_VERIFYHOST => false,
  751. // CURLOPT_CONNECTTIMEOUT => 15, // 减少连接超时时间为15秒
  752. // CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  753. // CURLOPT_FAILONERROR => false
  754. // ]);
  755. //
  756. // $response = curl_exec($ch);
  757. // $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  758. // $curlError = curl_error($ch);
  759. //
  760. // if ($response === false) {
  761. // // 尝试从curl错误信息中提取HTTP状态码
  762. // if (preg_match('/HTTP\/[0-9.]+\s+([0-9]+)/', $curlError, $matches)) {
  763. // $httpCode = (int)$matches[1];
  764. // }
  765. // throw new \Exception("请求发送失败: " . $curlError);
  766. // }
  767. //
  768. // $result = json_decode($response, true);
  769. //
  770. // // 检查API返回的错误
  771. // if (isset($result['error'])) {
  772. // $apiErrorDetail = $result['error']['message'] ?? '';
  773. // $errorType = $result['error']['type'] ?? '';
  774. // $errorCode = $result['error']['code'] ?? '';
  775. //
  776. // // 常见错误类型映射
  777. // $errorMessages = [
  778. // 'invalid_request_error' => '请求参数错误',
  779. // 'authentication_error' => '认证失败',
  780. // 'rate_limit_error' => '请求频率过高',
  781. // 'insufficient_quota' => '额度不足',
  782. // 'billing_not_active' => '账户未开通付费',
  783. // 'content_policy_violation' => '内容违反政策',
  784. // 'model_not_found' => '模型不存在或无可用渠道'
  785. // ];
  786. //
  787. // // 优先使用errorCode进行映射,如果没有则使用errorType
  788. // $friendlyMessage = $errorMessages[$errorCode] ?? ($errorMessages[$errorType] ?? 'API服务错误');
  789. //
  790. // // 构建详细的错误信息,包含错误代码、类型和详细描述
  791. // $detailedError = "{$friendlyMessage}";
  792. // if ($errorCode) {
  793. // $detailedError .= " (错误代码: {$errorCode})";
  794. // }
  795. // if ($apiErrorDetail) {
  796. // $detailedError .= ": {$apiErrorDetail}";
  797. // }
  798. //
  799. // throw new \Exception($detailedError);
  800. // }
  801. //
  802. // if ($httpCode !== 200) {
  803. // // HTTP状态码映射
  804. // $statusMessages = [
  805. // 400 => '请求参数不合法',
  806. // 401 => 'API密钥无效或权限不足',
  807. // 403 => '访问被拒绝',
  808. // 404 => 'API端点不存在',
  809. // 429 => '请求过于频繁,请稍后再试',
  810. // 500 => '服务器内部错误',
  811. // 503 => '服务暂时不可用'
  812. // ];
  813. //
  814. // $statusMessage = $statusMessages[$httpCode] ?? "HTTP错误({$httpCode})";
  815. // throw new \Exception($statusMessage);
  816. // }
  817. //
  818. // curl_close($ch);
  819. // return $result;
  820. //
  821. // } catch (\Exception $e) {
  822. // $lastError = $e->getMessage();
  823. // $attempt++;
  824. // $num++;
  825. // if ($attempt <= $maxRetries) {
  826. // sleep(pow(2, $attempt));
  827. // } else {
  828. // // 最终失败时的详细错误信息
  829. // $errorDetails = [
  830. // '错误原因' => self::getErrorCause($httpCode, $apiErrorDetail),
  831. // '解决方案' => self::getErrorSolution($httpCode),
  832. // '请求参数' => json_encode($data, JSON_UNESCAPED_UNICODE),
  833. // 'HTTP状态码' => $httpCode,
  834. // '重试次数' => $attempt
  835. // ];
  836. //
  837. // // 构建最终的错误信息,优先显示原始的详细错误消息
  838. // $finalError = "API请求失败\n";
  839. // $finalError .= "失败说明: " . $lastError . "\n"; // 使用原始的详细错误消息
  840. // $finalError .= "建议解决方案: " . $errorDetails['解决方案'] . "\n";
  841. // $finalError .= "技术详情: HTTP {$httpCode} - " . $errorDetails['错误原因'];
  842. //
  843. // throw new \Exception($finalError);
  844. // }
  845. // }
  846. // }
  847. // }
  848. //
  849. // private static function getErrorCause($httpCode, $apiErrorDetail)
  850. // {
  851. // $causeMap = [
  852. // 400 => '参数格式错误/必填参数缺失',
  853. // 401 => 'API Key无效/过期/无权限',
  854. // 429 => '超出接口调用频率限制',
  855. // 500 => '服务端内部故障',
  856. // 503 => '服务维护/算力不足',
  857. // 'model_not_found' => '模型渠道未开通/无可用资源',
  858. // 'invalid_size' => '模型尺寸参数不符合要求',
  859. // CURLE_OPERATION_TIMEDOUT => '请求超时(模型处理耗时超过设置值)'
  860. // ];
  861. //
  862. // if (strpos($apiErrorDetail, 'No available capacity') !== false) {
  863. // return '模型算力不足,无可用资源';
  864. // } elseif (strpos($apiErrorDetail, 'size is invalid') !== false) {
  865. // return '模型尺寸参数无效';
  866. // } elseif (isset($causeMap[$httpCode])) {
  867. // return $causeMap[$httpCode];
  868. // }
  869. // return $apiErrorDetail ?: '未知原因';
  870. // }
  871. //
  872. // private static function getErrorSolution($httpCode)
  873. // {
  874. // $solutionMap = [
  875. // 400 => '1. 检查参数是否完整 2. 确认参数类型(如seconds为字符串) 3. 验证尺寸/模型名是否合法',
  876. // 401 => '1. 检查API Key是否正确 2. 确认Key未过期/有对应模型权限',
  877. // 429 => '1. 降低请求频率 2. 等待1-5分钟后重试',
  878. // 500 => '1. 等待几分钟后重试 2. 联系API服务商排查',
  879. // 503 => '1. 等待算力释放 2. 联系服务商扩容',
  880. // CURLE_OPERATION_TIMEDOUT => '1. 延长超时时间 2. 检查模型生成耗时 3. 重试请求'
  881. // ];
  882. //
  883. // if (isset($solutionMap[$httpCode])) {
  884. // return $solutionMap[$httpCode];
  885. // }
  886. // return '1. 等待几分钟后重试 2. 检查API服务提供商状态 3. 联系服务提供商确认服务可用性';
  887. // }
  888. /**
  889. * 获取图片的base64数据和MIME类型
  890. * @return array 包含base64数据和MIME类型的数组
  891. */
  892. public static function file_get_contents($ImageUrl){
  893. // 构建完整的文件系统路径
  894. $rootPath = str_replace('\\', '/', ROOT_PATH);
  895. $filePath = rtrim($rootPath, '/') . '/public/' . $ImageUrl;
  896. // 检查文件是否存在
  897. if (!file_exists($filePath)) {
  898. throw new \Exception('图片文件不存在');
  899. }
  900. // 读取图片内容并进行base64编码
  901. $imageContent = file_get_contents($filePath);
  902. if (!$imageContent) {
  903. throw new \Exception('图片内容读取失败');
  904. }
  905. // 获取图片的MIME类型
  906. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  907. $mimeType = finfo_file($finfo, $filePath);
  908. finfo_close($finfo);
  909. // 返回包含base64数据和MIME类型的数组
  910. return [
  911. 'base64Data' => base64_encode($imageContent),
  912. 'mimeType' => $mimeType
  913. ];
  914. }
  915. }