AIGatewayService.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. <?php
  2. namespace app\service;
  3. use think\Db;
  4. use think\Queue;
  5. class AIGatewayService{
  6. /**
  7. * 接口访问配置
  8. * - api_key:API 调用密钥(Token)
  9. * - api_url:对应功能的服务端地址
  10. */
  11. // public $config = [
  12. // //图生文 gemini-3-pro-preview
  13. // 'gemini_imgtotxt' => [
  14. // 'api_key' => 'sk-R4O93k4FrJTXMLYZ2eB32WDPHWiDNbeUdlUcsLjgjeDKuzFI',
  15. // 'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-preview:generateContent'
  16. // ],
  17. // //文生文 gpt-4
  18. // 'txttotxtgtp' => [
  19. // 'api_key' => 'sk-fxlawqVtbbQbNW0wInR3E4wsLo5JHozDC2XOHzMa711su6ss',
  20. // 'api_url' => 'https://chatapi.onechats.top/v1/chat/completions'
  21. // ],
  22. // //文生文 gemini-2.0-flash
  23. // 'txttotxtgemini' => [
  24. // 'api_key' => 'sk-cqfCZFiiSIdpDjIHLMBbH6uWfeg7iVsASvlubjrNEmfUXbpX',
  25. // 'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-2.0-flash:generateContent'
  26. // ],
  27. // //文生图 black-forest-labs/FLUX.1-kontext-pro、dall-e-3、gpt-image-1
  28. // 'txttoimg' => [
  29. // 'api_key' => 'sk-MB6SR8qNaTjO80U7HJl4ztivX3zQKPgKVka9oyfVSXIkHSYZ',
  30. // 'api_url' => 'https://chatapi.onechats.ai/v1/images/generations'
  31. // ],
  32. // //文生图 gemini-3-pro-image-preview
  33. // 'gemini_txttoimg' => [
  34. // 'api_key' => 'sk-8nTt32NDI6q7klryBehwjEfnGaGrX8m1zI0C4ddfudLtanqP',
  35. // 'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:streamGenerateContent'
  36. // ],
  37. // //图生图【gemini-3-pro-image-preview】
  38. // 'gemini_imgtoimg' => [
  39. // 'api_key' => 'sk-8nTt32NDI6q7klryBehwjEfnGaGrX8m1zI0C4ddfudLtanqP',
  40. // 'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:streamGenerateContent'
  41. // ],
  42. // //即梦AI 创建视频任务接口【首帧图 + 尾帧图 = 新效果视频】
  43. // 'Create_ImgToVideo' => [
  44. // 'api_key' => '',
  45. // 'api_url' => 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks'
  46. // ]
  47. // ];
  48. //刘海瑞API接口文档秘钥
  49. public $config = [
  50. //图生文【gemini-3-pro-preview】
  51. 'gemini_imgtotxt' => [
  52. 'api_key' => 'sk-QiakVPhSisJiOh90LQFpjx9MX27mqGGOpOQ8XjKRhekoNCyr',
  53. 'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-preview:generateContent'
  54. ],
  55. //文生文【gpt-4 (OpenAI格式)】
  56. 'txttotxtgtp' => [
  57. 'api_key' => 'sk-pAlJU9xScpzDGvj1rhYKpokYcECETaceCSqDMUtq5N7FnbnA',
  58. 'api_url' => 'https://chatapi.onechats.top/v1/chat/completions'
  59. ],
  60. //文生文【gemini-2.0-flash (Gemini格式)】
  61. 'txttotxtgemini' => [
  62. 'api_key' => 'sk-pAlJU9xScpzDGvj1rhYKpokYcECETaceCSqDMUtq5N7FnbnA',
  63. 'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-2.0-flash:generateContent'
  64. ],
  65. //文生图【gemini-3-pro-image-preview】
  66. 'gemini_txttoimg' => [
  67. 'api_key' => 'sk-pAlJU9xScpzDGvj1rhYKpokYcECETaceCSqDMUtq5N7FnbnA',
  68. 'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:streamGenerateContent'
  69. ],
  70. //图生图【gemini-3-pro-image-preview】
  71. 'gemini_imgtoimg' => [
  72. 'api_key' => 'sk-IrIWvqkTs8DwvB9MFBRSWKQHdZRawNeKTnVPHjAJ0KryBWeF',
  73. 'api_url' => 'https://chatapi.onechats.ai/v1beta/models/gemini-3-pro-image-preview:generateContent'
  74. ],
  75. //即梦AI 创建视频任务接口【首帧图 + 尾帧图 = 新效果视频】
  76. 'Create_ImgToVideo' => [
  77. 'api_key' => '3da64aa0-afe2-4e3b-be4e-3eea5169aa6a',
  78. 'api_url' => 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks'
  79. ]
  80. ];
  81. /**
  82. * 图生文
  83. * @param string $prompt 对图像的提问内容或提示文本
  84. */
  85. public function callGptApi($model,$prompt,$mimeType,$base64Data)
  86. {
  87. // 针对gemini-3-pro-preview模型使用专用API端点和参数格式
  88. if ($model === 'gemini-3-pro-preview') {
  89. $data = [
  90. "contents" => [
  91. [
  92. "role" => "user",
  93. "parts" => [
  94. ["inlineData" => [
  95. "mimeType" => $mimeType,
  96. "data" => $base64Data
  97. ]],
  98. ["text" => $prompt]
  99. ]
  100. ]
  101. ],
  102. "generationConfig" => [
  103. "responseModalities" => ["TEXT"],
  104. "maxOutputTokens" => 1000,
  105. "temperature" => 0.7,
  106. "language" => "zh-CN"
  107. ]
  108. ];
  109. return self::callApi($this->config['gemini_imgtotxt']['api_url'], $this->config['gemini_imgtotxt']['api_key'], $data);
  110. } else {
  111. // 处理其他模型或抛出异常
  112. throw new \Exception("不支持的模型类型: {$model}");
  113. }
  114. }
  115. /**
  116. * 文生文
  117. * @param string $prompt 用户输入的文本提示内容
  118. */
  119. public function txtGptApi($prompt,$model)
  120. {
  121. //判断使用模型
  122. if ($model === 'gemini-2.0-flash') {
  123. // 使用Gemini API的正确参数格式
  124. $data = [
  125. "contents" => [
  126. [
  127. "role" => "user",
  128. "parts" => [
  129. ["text" => $prompt]
  130. ]
  131. ]
  132. ],
  133. "generationConfig" => [
  134. "responseModalities" => ["TEXT"],
  135. "maxOutputTokens" => 1024,
  136. "temperature" => 0.7,
  137. "language" => "zh-CN"
  138. ]
  139. ];
  140. return self::callApi(
  141. $this->config['txttotxtgemini']['api_url'],
  142. $this->config['txttotxtgemini']['api_key'],
  143. $data
  144. );
  145. }else if ($model === 'gpt-4') {
  146. // 使用OpenAI API格式
  147. $data = [
  148. 'model' => $model,
  149. 'messages' => [
  150. ['role' => 'user', 'content' => $prompt]
  151. ],
  152. 'temperature' => 0.7,
  153. 'max_tokens' => 1024
  154. ];
  155. return $this->callApi(
  156. $this->config['txttotxtgtp']['api_url'],
  157. $this->config['txttotxtgtp']['api_key'],
  158. $data
  159. );
  160. } else {
  161. // 处理其他模型或抛出异常
  162. throw new \Exception("不支持的模型类型: {$model}");
  163. }
  164. }
  165. /**
  166. * 文生图
  167. *
  168. * @param string $prompt 提示文本,用于指导图像生成(最长建议 1000 字符)
  169. * @param string $selectedOption 模型名称,例如 'dall-e-3' 或其他兼容模型
  170. *
  171. * 默认参数说明(适用于所有模型):
  172. * - n: 1(生成 1 张图)
  173. * - size: '1024x1024'(标准正方形图像)
  174. * - quality: 'hd'(高清质量)
  175. * - style: 'vivid'(鲜明风格)
  176. *
  177. * response_format 参数差异:
  178. * - 若模型为 'dall-e-3':返回 base64 图像,字段为 `b64_json`
  179. * - 其他模型默认返回图像 URL,字段为 `url`
  180. *
  181. * ⚠️ 注意:使用此方法后,需在 Job/TextToImageJob.php 中按 response_format 判断提取方式:
  182. * 提取 url 图像
  183. * $base64Image = $dalle1024['data'][0]['url'] ?? null;
  184. * 提取 base64 图像
  185. * $base64Image = $dalle1024['data'][0]['b64_json'] ?? null;
  186. *
  187. * @return array 返回接口响应,成功时包含 'data' 字段,失败时包含 'error' 信息
  188. */
  189. public function callDalleApi($prompt, $model,$size)
  190. {
  191. // 尺寸优先级:传入有效值 > 默认值
  192. $imageSize = !empty($size) ? $size : '1024x1024';
  193. if ($model === 'dall-e-3') {
  194. $data = [
  195. 'prompt' => $prompt,
  196. 'model' => $model,
  197. 'n' => 1,
  198. 'size' => $imageSize,
  199. 'quality' => 'hd',
  200. 'style' => 'vivid',
  201. 'response_format' => 'url',
  202. ];
  203. return $this->callApi($this->config['txttoimg']['api_url'],$this->config['txttoimg']['api_key'],$data);
  204. } else if ($model === 'black-forest-labs/FLUX.1-kontext-pro') {
  205. $data = [
  206. 'prompt' => $prompt,
  207. 'model' => $model,
  208. 'n' => 1,
  209. 'size' => $imageSize,
  210. 'quality' => 'hd',
  211. 'style' => 'vivid',
  212. 'response_format' => 'url',
  213. ];
  214. return $this->callApi($this->config['txttoimg']['api_url'],$this->config['txttoimg']['api_key'],$data);
  215. } else if ($model === 'MID_JOURNEY') {
  216. $data = [
  217. 'botType' => $model,
  218. 'prompt' => $prompt,
  219. 'base64Array' => [],
  220. 'accountFilter' => [
  221. 'channelId' => "",
  222. 'instanceId' => "",
  223. 'modes' => [],
  224. 'remark' => "",
  225. 'remix' => true,
  226. 'remixAutoConsidered' => true
  227. ],
  228. 'notifyHook' => "",
  229. 'state' => ""
  230. ];
  231. return $this->callApi($this->config['submitimage']['api_url'],$this->config['submitimage']['api_key'],$data);
  232. }else if ($model === 'gpt-image-1') {
  233. $data = [
  234. 'prompt' => $prompt,
  235. 'model' => $model,
  236. 'n' => 1,
  237. 'size' => $imageSize,
  238. 'quality' => 'hd',
  239. 'style' => 'vivid',
  240. 'response_format' => 'url',
  241. ];
  242. return $this->callApi($this->config['txttoimg']['api_url'],$this->config['txttoimg']['api_key'],$data);
  243. } else if ($model === 'gemini-3-pro-image-preview') {
  244. // 使用Google Gemini模型的参数格式
  245. // 支持的aspectRatio值:1:1, 4:3, 3:4, 16:9, 9:16
  246. $supportedAspectRatios = ['1:1', '4:3', '3:4', '16:9', '9:16'];
  247. // 解析宽高比
  248. if (!empty($size) && strpos($size, ':') !== false) {
  249. // 直接使用前端传递的宽高比格式
  250. $aspectRatio = $size;
  251. // 验证宽高比是否在支持的列表中
  252. if (!in_array($aspectRatio, $supportedAspectRatios)) {
  253. // 如果不在支持列表中,使用默认值
  254. $aspectRatio = "1:1";
  255. }
  256. } else {
  257. // 默认1:1宽高比
  258. $aspectRatio = "1:1";
  259. }
  260. $data = [
  261. "contents" => [
  262. [
  263. "role" => "user",
  264. "parts" => [
  265. ["text" => $prompt]
  266. ]
  267. ]
  268. ],
  269. "generationConfig" => [
  270. "responseModalities" => ["TEXT", "IMAGE"],
  271. "imageConfig" => [
  272. "aspectRatio" => $aspectRatio
  273. ]
  274. ]
  275. ];
  276. return $this->callApi($this->config['gemini_txttoimg']['api_url'],$this->config['gemini_txttoimg']['api_key'],$data);
  277. }else{
  278. return [
  279. 'code' => 400,
  280. 'msg' => '未配置的文生图模型: ' . $model,
  281. 'data' => null
  282. ];
  283. }
  284. }
  285. /**
  286. * 图生图(产品图 + 模板图 → 新图)
  287. * @param string $prompt 提示词
  288. * @param string $model 模型名,如 gemini-3-pro-image-preview
  289. * @param string $productImgRaw 产品图路径,如 /uploads/merchant/xxx/xxx.png
  290. * @param string $templateImgRaw 模板图路径,如 /uploads/template/xxx.png
  291. * @return array API 响应,成功时含 candidates[0].content.parts[0].inlineData
  292. */
  293. public function GeminiImToImgCallApi($promptContent, $model,$size, $product_base64Data,$product_mimeType,$template_base64Data,$template_mimeType)
  294. {
  295. $data = [
  296. 'contents' => [
  297. [
  298. 'role' => 'user',
  299. 'parts' => [
  300. ['text' => $promptContent],
  301. ['inlineData' => ['mimeType' => $product_mimeType, 'data' => $product_base64Data]],
  302. ['inlineData' => ['mimeType' => $template_mimeType, 'data' => $template_base64Data]]
  303. ]
  304. ]
  305. ],
  306. 'generationConfig' => [
  307. 'responseModalities' => ['IMAGE'],
  308. 'imageConfig' => [
  309. 'aspectRatio' => $size,
  310. 'quality' => 'HIGH',
  311. 'width' => 1000,
  312. 'height' => 800
  313. ],
  314. 'temperature' => 0.3,
  315. 'topP' => 0.8,
  316. 'maxOutputTokens' => 2048
  317. ]
  318. ];
  319. return $this->callApi($this->config['gemini_imgtoimg']['api_url'], $this->config['gemini_imgtoimg']['api_key'], $data, 300);
  320. }
  321. /**
  322. * 计算最大公约数
  323. */
  324. public function gcd($a, $b) {
  325. while ($b != 0) {
  326. $temp = $a % $b;
  327. $a = $b;
  328. $b = $temp;
  329. }
  330. return $a;
  331. }
  332. /**
  333. * 图生图
  334. * @param string $prompt 用户输入的文本提示内容
  335. * @param string $new_image_url 原图路径
  336. * @param array $options 可选参数,可覆盖默认配置
  337. * @return array
  338. */
  339. public function txt2imgWithControlNet($prompt, $controlImgUrl, $options = []) {
  340. $apiUrl = "http://20.0.17.188:45001/sdapi/v1/txt2img";
  341. $headers = ['Content-Type: application/json'];
  342. $imgPath = ROOT_PATH . 'public/' . ltrim($controlImgUrl, '/');
  343. if (!file_exists($imgPath)) {
  344. return ['code' => 1, 'msg' => '图片不存在:' . $controlImgUrl];
  345. }
  346. $imgData = file_get_contents($imgPath);
  347. $base64Img = 'data:image/png;base64,' . base64_encode($imgData);
  348. $params = [
  349. 'prompt' => $prompt,
  350. 'negative_prompt' => '(deformed, distorted, disfigured:1.3), poorly drawn, bad anatomy',
  351. 'steps' => 20,
  352. 'sampler_name' => 'DPM++ 2M SDE',
  353. 'cfg_scale' => 7,
  354. 'seed' => -1,
  355. 'width' => 1024,
  356. 'height' => 1303,
  357. 'override_settings' => [
  358. 'sd_model_checkpoint' => 'realisticVisionV51_v51VAE-inpainting',
  359. 'sd_vae' => 'vae-ft-mse-840000-ema-pruned',
  360. 'CLIP_stop_at_last_layers' => 2
  361. ],
  362. 'clip_skip' => 2,
  363. 'alwayson_scripts' => [
  364. 'controlnet' => [
  365. 'args' => [[
  366. 'enabled' => true,
  367. 'input_image' => $base64Img,
  368. 'module' => 'inpaint_only+lama',
  369. 'model' => 'control_v11p_sd15_inpaint_fp16 [be8bc0ed]',
  370. 'weight' => 1,
  371. 'resize_mode' => 'Resize and Fill',
  372. 'pixel_perfect' => false,
  373. 'control_mode' => 'ControlNet is more important',
  374. 'starting_control_step' => 0,
  375. 'ending_control_step' => 1
  376. ]]
  377. ]
  378. ]
  379. ];
  380. if (!empty($options)) {
  381. $params = array_merge($params, $options);
  382. }
  383. $ch = curl_init();
  384. curl_setopt($ch, CURLOPT_URL, $apiUrl);
  385. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  386. curl_setopt($ch, CURLOPT_POST, true);
  387. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  388. curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params, JSON_UNESCAPED_UNICODE));
  389. curl_setopt($ch, CURLOPT_TIMEOUT, 180);
  390. $response = curl_exec($ch);
  391. $error = curl_error($ch);
  392. curl_close($ch);
  393. if ($error) {
  394. return ['code' => 1, 'msg' => '请求失败:' . $error];
  395. }
  396. $data = json_decode($response, true);
  397. if (!isset($data['images'][0])) {
  398. return ['code' => 1, 'msg' => '接口未返回图像数据'];
  399. }
  400. return [
  401. 'code' => 0,
  402. 'msg' => '成功',
  403. 'data' => [
  404. 'base64' => $data['images'][0],
  405. 'info' => $data['info'] ?? ''
  406. ]
  407. ];
  408. }
  409. // 第一阶段:图生图
  410. public function upscaleWithImg2Img($prompt, $imgPath)
  411. {
  412. if (!file_exists($imgPath)) {
  413. return ['code' => 1, 'msg' => '原图不存在:' . $imgPath];
  414. }
  415. // 获取原始图像尺寸
  416. [$origWidth, $origHeight] = getimagesize($imgPath);
  417. if (!$origWidth || !$origHeight) {
  418. return ['code' => 1, 'msg' => '无法识别图片尺寸'];
  419. }
  420. // 按2倍尺寸计算目标大小
  421. $targetWidth = $origWidth * 2;
  422. $targetHeight = $origHeight * 2;
  423. // 编码图像为 base64
  424. $imgData = file_get_contents($imgPath);
  425. $base64Img = 'data:image/png;base64,' . base64_encode($imgData);
  426. // 构造参数
  427. $params = [
  428. 'init_images' => [$base64Img],
  429. 'prompt' => $prompt,
  430. 'steps' => 20,
  431. 'sampler_name' => 'DPM++ 2M SDE Heun',
  432. 'cfg_scale' => 7,
  433. 'seed' => 1669863506,
  434. 'width' => $targetWidth,
  435. 'height' => $targetHeight,
  436. 'denoising_strength' => 0.2,
  437. 'clip_skip' => 2,
  438. 'override_settings' => [
  439. 'sd_model_checkpoint' => 'realisticVisionV51_v51VAE-inpainting.safetensors [f0d4872d24]',
  440. 'sd_vae' => 'vae-ft-mse-840000-ema-pruned.safetensors',
  441. 'CLIP_stop_at_last_layers' => 2
  442. ],
  443. 'override_settings_restore_afterwards' => true
  444. ];
  445. $apiUrl = "http://20.0.17.188:45001/sdapi/v1/img2img";
  446. $headers = ['Content-Type: application/json'];
  447. // 发起请求
  448. $ch = curl_init();
  449. curl_setopt_array($ch, [
  450. CURLOPT_URL => $apiUrl,
  451. CURLOPT_RETURNTRANSFER => true,
  452. CURLOPT_POST => true,
  453. CURLOPT_HTTPHEADER => $headers,
  454. CURLOPT_POSTFIELDS => json_encode($params),
  455. CURLOPT_TIMEOUT => 180
  456. ]);
  457. $response = curl_exec($ch);
  458. $error = curl_error($ch);
  459. curl_close($ch);
  460. if ($error) return ['code' => 1, 'msg' => '图生图请求失败:' . $error];
  461. $data = json_decode($response, true);
  462. if (!isset($data['images'][0])) return ['code' => 1, 'msg' => '图生图接口未返回图像'];
  463. return ['code' => 0, 'data' => ['base64' => $data['images'][0]]];
  464. }
  465. // 第二阶段:高清超分
  466. public function imgtogqGptApi($imageRelPath, $options = [])
  467. {
  468. $imgPath = ROOT_PATH . 'public/' . $imageRelPath;
  469. if (!file_exists($imgPath)) {
  470. return ['code' => 1, 'msg' => '原图不存在:' . $imageRelPath];
  471. }
  472. $defaultParams = [
  473. 'resize_mode' => 0,
  474. 'show_extras_results' => true,
  475. 'gfpgan_visibility' => 0,
  476. 'codeformer_visibility' => 0,
  477. 'codeformer_weight' => 0,
  478. 'upscaling_resize' => 1.62,
  479. 'upscaling_crop' => true,
  480. 'upscaler_1' => 'R-ESRGAN 4x+ Anime6B',
  481. 'upscaler_2' => 'None',
  482. 'extras_upscaler_2_visibility' => 0,
  483. 'upscale_first' => false
  484. ];
  485. $params = array_merge($defaultParams, $options);
  486. $imgData = file_get_contents($imgPath);
  487. $params['image'] = base64_encode($imgData);
  488. $apiUrl = "http://20.0.17.188:45001/sdapi/v1/extra-single-image";
  489. $headers = ['Content-Type: application/json'];
  490. $ch = curl_init();
  491. curl_setopt_array($ch, [
  492. CURLOPT_URL => $apiUrl,
  493. CURLOPT_RETURNTRANSFER => true,
  494. CURLOPT_POST => true,
  495. CURLOPT_HTTPHEADER => $headers,
  496. CURLOPT_POSTFIELDS => json_encode($params),
  497. CURLOPT_TIMEOUT => 120
  498. ]);
  499. $response = curl_exec($ch);
  500. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  501. $curlErr = curl_error($ch);
  502. curl_close($ch);
  503. if ($curlErr) {
  504. return ['code' => 1, 'msg' => '请求失败:' . $curlErr];
  505. }
  506. if ($httpCode !== 200) {
  507. return ['code' => 1, 'msg' => 'API请求失败,HTTP状态码:' . $httpCode];
  508. }
  509. $data = json_decode($response, true);
  510. if (json_last_error() !== JSON_ERROR_NONE) {
  511. return ['code' => 1, 'msg' => 'API返回数据解析失败:' . json_last_error_msg()];
  512. }
  513. if (empty($data['image'])) {
  514. return ['code' => 1, 'msg' => '接口未返回有效的图像数据'];
  515. }
  516. return [
  517. 'code' => 0,
  518. 'msg' => '高清图生成成功',
  519. 'data' => [
  520. 'base64_image' => $data['image'],
  521. 'original_size' => strlen($imgData),
  522. 'processed_size' => strlen(base64_decode($data['image']))
  523. ]
  524. ];
  525. }
  526. /**
  527. * 通用 API 调用方法(支持重试机制)
  528. *
  529. * @param string $url 接口地址
  530. * @param string $apiKey 授权密钥(Bearer Token)
  531. * @param array $data 请求数据(JSON 格式)
  532. *
  533. * 功能说明:
  534. * - 使用 cURL 发送 POST 请求到指定 API 接口
  535. * - 设置请求头和超时时间等参数
  536. * - 支持最多重试 2 次,当接口调用失败时自动重试
  537. * - 返回成功时解析 JSON 响应为数组
  538. *
  539. * 异常处理:
  540. * - 若全部重试失败,将抛出异常并包含最后一次错误信息
  541. *
  542. * @return array 接口响应数据(成功时返回解析后的数组)
  543. * @throws \Exception 接口请求失败时抛出异常
  544. */
  545. public static function callApi($url, $apiKey, $data, $timeout = 60)
  546. {
  547. $maxRetries = 0; // 减少重试次数为0,避免不必要的等待
  548. $attempt = 0;
  549. $lastError = '';
  550. $httpCode = 0;
  551. $apiErrorDetail = '';
  552. while ($attempt <= $maxRetries) {
  553. try {
  554. $ch = curl_init();
  555. curl_setopt_array($ch, [
  556. CURLOPT_URL => $url,
  557. CURLOPT_RETURNTRANSFER => true,
  558. CURLOPT_POST => true,
  559. CURLOPT_POSTFIELDS => json_encode($data, JSON_UNESCAPED_UNICODE),
  560. CURLOPT_HTTPHEADER => [
  561. 'Content-Type: application/json',
  562. 'Authorization: Bearer ' . $apiKey
  563. ],
  564. CURLOPT_TIMEOUT => (int) $timeout,
  565. CURLOPT_SSL_VERIFYPEER => false,
  566. CURLOPT_SSL_VERIFYHOST => false,
  567. CURLOPT_CONNECTTIMEOUT => 15, // 减少连接超时时间为15秒
  568. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  569. CURLOPT_FAILONERROR => false
  570. ]);
  571. $response = curl_exec($ch);
  572. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  573. $curlError = curl_error($ch);
  574. if ($response === false) {
  575. // 尝试从curl错误信息中提取HTTP状态码
  576. if (preg_match('/HTTP\/[0-9.]+\s+([0-9]+)/', $curlError, $matches)) {
  577. $httpCode = (int)$matches[1];
  578. }
  579. throw new \Exception("请求发送失败: " . $curlError);
  580. }
  581. $result = json_decode($response, true);
  582. // 检查API返回的错误
  583. if (isset($result['error'])) {
  584. $apiErrorDetail = $result['error']['message'] ?? '';
  585. $errorType = $result['error']['type'] ?? '';
  586. $errorCode = $result['error']['code'] ?? '';
  587. // 常见错误类型映射
  588. $errorMessages = [
  589. 'invalid_request_error' => '请求参数错误',
  590. 'authentication_error' => '认证失败',
  591. 'rate_limit_error' => '请求频率过高',
  592. 'insufficient_quota' => '额度不足',
  593. 'billing_not_active' => '账户未开通付费',
  594. 'content_policy_violation' => '内容违反政策',
  595. 'model_not_found' => '模型不存在或无可用渠道'
  596. ];
  597. // 优先使用errorCode进行映射,如果没有则使用errorType
  598. $friendlyMessage = $errorMessages[$errorCode] ?? ($errorMessages[$errorType] ?? 'API服务错误');
  599. // 构建详细的错误信息,包含错误代码、类型和详细描述
  600. $detailedError = "{$friendlyMessage}";
  601. if ($errorCode) {
  602. $detailedError .= " (错误代码: {$errorCode})";
  603. }
  604. if ($apiErrorDetail) {
  605. $detailedError .= ": {$apiErrorDetail}";
  606. }
  607. throw new \Exception($detailedError);
  608. }
  609. if ($httpCode !== 200) {
  610. // HTTP状态码映射
  611. $statusMessages = [
  612. 400 => '请求参数不合法',
  613. 401 => 'API密钥无效或权限不足',
  614. 403 => '访问被拒绝',
  615. 404 => 'API端点不存在',
  616. 429 => '请求过于频繁,请稍后再试',
  617. 500 => '服务器内部错误',
  618. 503 => '服务暂时不可用'
  619. ];
  620. $statusMessage = $statusMessages[$httpCode] ?? "HTTP错误({$httpCode})";
  621. throw new \Exception($statusMessage);
  622. }
  623. curl_close($ch);
  624. return $result;
  625. } catch (\Exception $e) {
  626. $lastError = $e->getMessage();
  627. $attempt++;
  628. if ($attempt <= $maxRetries) {
  629. sleep(pow(2, $attempt));
  630. } else {
  631. // 最终失败时的详细错误信息
  632. $errorDetails = [
  633. '错误原因' => self::getErrorCause($httpCode, $apiErrorDetail),
  634. '解决方案' => self::getErrorSolution($httpCode),
  635. '请求参数' => json_encode($data, JSON_UNESCAPED_UNICODE),
  636. 'HTTP状态码' => $httpCode,
  637. '重试次数' => $attempt
  638. ];
  639. // 构建最终的错误信息,优先显示原始的详细错误消息
  640. $finalError = "API请求失败\n";
  641. $finalError .= "失败说明: " . $lastError . "\n"; // 使用原始的详细错误消息
  642. $finalError .= "建议解决方案: " . $errorDetails['解决方案'] . "\n";
  643. $finalError .= "技术详情: HTTP {$httpCode} - " . $errorDetails['错误原因'];
  644. throw new \Exception($finalError);
  645. }
  646. }
  647. }
  648. }
  649. /**
  650. * 根据错误类型获取友好的错误原因
  651. */
  652. private static function getErrorCause($httpCode, $apiError)
  653. {
  654. $causes = [
  655. 401 => 'API密钥无效、过期或没有访问权限',
  656. 400 => $apiError ?: '请求参数不符合API要求',
  657. 429 => '已达到API调用频率限制',
  658. 403 => '您的账户可能没有开通相关服务权限',
  659. 500 => 'OpenAI服务器处理请求时出错',
  660. 503 => 'API服务暂时不可用,可能是服务器维护或负载过高'
  661. ];
  662. return $causes[$httpCode] ?? '未知错误,请检查网络连接和API配置';
  663. }
  664. /**
  665. * 根据错误类型获取解决方案建议
  666. */
  667. private static function getErrorSolution($httpCode)
  668. {
  669. $solutions = [
  670. 401 => '1. 检查API密钥是否正确 2. 确认密钥是否有访问权限 3. 尝试创建新密钥',
  671. 400 => '1. 检查请求参数 2. 验证提示词内容 3. 参考API文档修正参数',
  672. 429 => '1. 等待1分钟后重试 2. 升级账户提高限额 3. 优化调用频率',
  673. 403 => '1. 检查账户状态 2. 确认是否已开通付费 3. 联系OpenAI支持',
  674. 500 => '1. 等待几分钟后重试 2. 检查OpenAI服务状态页',
  675. 503 => '1. 等待几分钟后重试 2. 检查API服务提供商状态 3. 联系服务提供商确认服务可用性'
  676. ];
  677. return $solutions[$httpCode] ?? '1. 检查网络连接 2. 查看服务日志 3. 联系技术支持';
  678. }
  679. /**
  680. * 获取图片的base64数据和MIME类型
  681. * @return array 包含base64数据和MIME类型的数组
  682. */
  683. public static function file_get_contents($ImageUrl){
  684. // 构建完整的文件系统路径
  685. $rootPath = str_replace('\\', '/', ROOT_PATH);
  686. $filePath = rtrim($rootPath, '/') . '/public/' . $ImageUrl;
  687. // 检查文件是否存在
  688. if (!file_exists($filePath)) {
  689. throw new \Exception('图片文件不存在');
  690. }
  691. // 读取图片内容并进行base64编码
  692. $imageContent = file_get_contents($filePath);
  693. if (!$imageContent) {
  694. throw new \Exception('图片内容读取失败');
  695. }
  696. // 获取图片的MIME类型
  697. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  698. $mimeType = finfo_file($finfo, $filePath);
  699. finfo_close($finfo);
  700. // 返回包含base64数据和MIME类型的数组
  701. return [
  702. 'base64Data' => base64_encode($imageContent),
  703. 'mimeType' => $mimeType
  704. ];
  705. }
  706. }