AIGatewayService.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <?php
  2. namespace app\service;
  3. use think\Db;
  4. use think\Queue;
  5. class AIGatewayService{
  6. /**
  7. * 接口访问配置
  8. *
  9. * 每个模块包含:
  10. * - api_key:API 调用密钥(Token)
  11. * - api_url:对应功能的服务端地址
  12. */
  13. protected $config = [
  14. //图生文
  15. 'imgtotxt' => [
  16. 'api_key' => 'sk-LVcDfTx5SYK6pWiGpfcAN2KA0LunymnMiYSVfzUKQXrjlkZv',
  17. 'api_url' => 'https://chatapi.onechats.top/v1/chat/completions'
  18. ],
  19. //文生文
  20. // 'txttotxt' => [
  21. // 'api_key' => 'sk-fxlawqVtbbQbNW0wInR3E4wsLo5JHozDC2XOHzMa711su6ss',
  22. // 'api_url' => 'https://one.opengptgod.com/v1/chat/completions'
  23. // ],
  24. //文生文
  25. 'txttotxt' => [
  26. 'api_key' => 'sk-fxlawqVtbbQbNW0wInR3E4wsLo5JHozDC2XOHzMa711su6ss',
  27. 'api_url' => 'https://chatapi.onechats.top/v1/chat/completions'
  28. ],
  29. // //文生图
  30. // 'txttoimg' => [
  31. // 'api_key' => 'sk-Bhos1lXTRpZiAAmN06624a219a874eCd91Dc068b902a3e73',
  32. // 'api_url' => 'https://one.opengptgod.com/v1/chat/completions'
  33. // ]
  34. // //文生图
  35. 'txttoimg' => [
  36. 'api_key' => 'sk-MB6SR8qNaTjO80U7HJl4ztivX3zQKPgKVka9oyfVSXIkHSYZ',
  37. 'api_url' => 'https://chatapi.onechats.top/v1/images/generations'
  38. ]
  39. ];
  40. /**
  41. * 图生文
  42. * @param string $imageUrl 图像 URL,支持公网可访问地址
  43. * @param string $prompt 对图像的提问内容或提示文本
  44. */
  45. public function callGptApi($imageUrl, $prompt)
  46. {
  47. //方式一
  48. $data = [
  49. "model" => "gemini-2.5-flash-preview",
  50. "messages" => [[
  51. "role" => "user",
  52. "content" => [
  53. ["type" => "text", "text" => $prompt],
  54. ["type" => "image_url", "image_url" => [
  55. "url" => $imageUrl,
  56. "detail" => "auto"
  57. ]]
  58. ]
  59. ]],
  60. "max_tokens" => 1000
  61. ];
  62. //方式二
  63. // $data = [
  64. // "model" => "gpt-4-vision-preview",
  65. // "messages" => [[
  66. // "role" => "user",
  67. // "content" => [
  68. // ["type" => "text", "text" => $prompt],
  69. // ["type" => "image_url", "image_url" => [
  70. // "url" => $imageUrl,
  71. // "detail" => "auto"
  72. // ]]
  73. // ]
  74. // ]],
  75. // "max_tokens" => 1000
  76. // ];
  77. return $this->callApi($this->config['imgtotxt']['api_url'], $this->config['imgtotxt']['api_key'], $data);
  78. }
  79. /**
  80. * 文生文
  81. * @param string $prompt 用户输入的文本提示内容
  82. */
  83. public function txtGptApi($prompt)
  84. {
  85. if (empty($prompt)) {
  86. throw new \Exception("Prompt 不允许为空");
  87. }
  88. //方式一
  89. // $data = [
  90. // 'prompt' => $prompt,
  91. // 'model' => 'gpt-4',
  92. // 'session_id' => null,
  93. // 'context_reset' => true
  94. // ];
  95. //方式二
  96. $data = [
  97. 'model' => 'gpt-4',
  98. 'messages' => [
  99. ['role' => 'user', 'content' => $prompt]
  100. ],
  101. 'temperature' => 0.7,
  102. 'max_tokens' => 1024
  103. ];
  104. return $this->callApi(
  105. $this->config['txttotxt']['api_url'],
  106. $this->config['txttotxt']['api_key'],
  107. $data
  108. );
  109. }
  110. /**
  111. * 文生图
  112. *
  113. * @param string $prompt 提示文本,用于指导图像生成(最长建议 1000 字符)
  114. * @param string $selectedOption 模型名称,例如 'dall-e-3' 或其他兼容模型
  115. *
  116. * 默认参数说明(适用于所有模型):
  117. * - n: 1(生成 1 张图)
  118. * - size: '1024x1024'(标准正方形图像)
  119. * - quality: 'hd'(高清质量)
  120. * - style: 'vivid'(鲜明风格)
  121. *
  122. * response_format 参数差异:
  123. * - 若模型为 'dall-e-3':返回 base64 图像,字段为 `b64_json`
  124. * - 其他模型默认返回图像 URL,字段为 `url`
  125. *
  126. * ⚠️ 注意:使用此方法后,需在 Job/TextToImageJob.php 中按 response_format 判断提取方式:
  127. * 提取 url 图像
  128. * $base64Image = $dalle1024['data'][0]['url'] ?? null;
  129. * 提取 base64 图像
  130. * $base64Image = $dalle1024['data'][0]['b64_json'] ?? null;
  131. *
  132. * @return array 返回接口响应,成功时包含 'data' 字段,失败时包含 'error' 信息
  133. */
  134. public function callDalleApi($prompt, $selectedOption)
  135. {
  136. if ($selectedOption === 'dall-e-3') {
  137. $data = [
  138. 'prompt' => $prompt,
  139. 'model' => $selectedOption,
  140. 'n' => 1,
  141. 'size' => '1024x1024',
  142. 'quality' => 'hd',
  143. 'style' => 'vivid',
  144. 'response_format' => 'b64_json',
  145. ];
  146. } else {
  147. $data = [
  148. 'prompt' => $prompt,
  149. 'model' => $selectedOption,
  150. 'n' => 1,
  151. 'size' => '1024x1024',
  152. 'quality' => 'hd',
  153. 'style' => 'vivid',
  154. 'response_format' => 'b64_json',
  155. ];
  156. }
  157. return $this->callApi($this->config['txttoimg']['api_url'],$this->config['txttoimg']['api_key'],$data);
  158. }
  159. /**
  160. * 图生图
  161. * @param string $prompt 用户输入的文本提示内容
  162. * @param string $new_image_url 原图
  163. */
  164. public function imgtoimgGptApi($prompt, $new_image_url)
  165. {
  166. $imgPath = ROOT_PATH . 'public/' . $new_image_url;
  167. if (!file_exists($imgPath)) {
  168. return ['code' => 1, 'msg' => '原图不存在:' . $new_image_url];
  169. }
  170. // 原图 base64 编码
  171. $imgData = file_get_contents($imgPath);
  172. $base64Img = base64_encode($imgData);
  173. $initImage = 'data:image/png;base64,' . $base64Img;
  174. // 构建 POST 请求参数
  175. $postData = json_encode([
  176. 'prompt' => $prompt,
  177. 'sampler_name' => 'DPM++ 2M SDE Heun',
  178. 'seed' => -1,
  179. 'steps' => 20,
  180. 'cfg_scale' => 7,
  181. 'denoising_strength' => 0.6,
  182. 'width' => 1024,
  183. 'height' => 2048,
  184. 'resize_mode' => 0,
  185. 'inpaint_full_res' => true,
  186. 'inpainting_fill' => 1,
  187. 'init_images' => [$initImage],
  188. 'override_settings' => [
  189. 'sd_model_checkpoint' => 'realisticVisionV51_v51VAE-inpainting.safetensors [f0d4872d24]',
  190. 'sd_vae' => 'anything-v4.5.vae.pt'
  191. ]
  192. ]);
  193. $apiUrl = "http://20.0.17.233:45001/sdapi/v1/img2img";
  194. $headers = ['Content-Type: application/json'];
  195. $ch = curl_init();
  196. curl_setopt($ch, CURLOPT_URL, $apiUrl);
  197. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  198. curl_setopt($ch, CURLOPT_POST, true);
  199. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  200. curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
  201. curl_setopt($ch, CURLOPT_TIMEOUT, 90);
  202. $response = curl_exec($ch);
  203. $error = curl_error($ch);
  204. curl_close($ch);
  205. if ($error) {
  206. return ['code' => 1, 'msg' => '请求失败:' . $error];
  207. }
  208. $data = json_decode($response, true);
  209. if (!isset($data['images'][0])) {
  210. return ['code' => 1, 'msg' => 'API未返回图像数据'];
  211. }
  212. return [
  213. 'code' => 0,
  214. 'msg' => '图像生成成功',
  215. 'data' => [
  216. 'url' => $data['images'][0]
  217. ]
  218. ];
  219. }
  220. /**
  221. * 通用 API 调用方法(支持重试机制)
  222. *
  223. * @param string $url 接口地址
  224. * @param string $apiKey 授权密钥(Bearer Token)
  225. * @param array $data 请求数据(JSON 格式)
  226. *
  227. * 功能说明:
  228. * - 使用 cURL 发送 POST 请求到指定 API 接口
  229. * - 设置请求头和超时时间等参数
  230. * - 支持最多重试 2 次,当接口调用失败时自动重试
  231. * - 返回成功时解析 JSON 响应为数组
  232. *
  233. * 异常处理:
  234. * - 若全部重试失败,将抛出异常并包含最后一次错误信息
  235. *
  236. * @return array 接口响应数据(成功时返回解析后的数组)
  237. * @throws \Exception 接口请求失败时抛出异常
  238. */
  239. public function callApi($url, $apiKey, $data)
  240. {
  241. $maxRetries = 2;
  242. $attempt = 0;
  243. $lastError = '';
  244. $httpCode = 0;
  245. $apiErrorDetail = '';
  246. while ($attempt <= $maxRetries) {
  247. try {
  248. $ch = curl_init();
  249. curl_setopt_array($ch, [
  250. CURLOPT_URL => $url,
  251. CURLOPT_RETURNTRANSFER => true,
  252. CURLOPT_POST => true,
  253. CURLOPT_POSTFIELDS => json_encode($data),
  254. CURLOPT_HTTPHEADER => [
  255. 'Content-Type: application/json',
  256. 'Authorization: Bearer ' . $apiKey
  257. ],
  258. CURLOPT_TIMEOUT => 120,
  259. CURLOPT_SSL_VERIFYPEER => true,
  260. CURLOPT_SSL_VERIFYHOST => 2,
  261. CURLOPT_CONNECTTIMEOUT => 30,
  262. CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  263. CURLOPT_FAILONERROR => true
  264. ]);
  265. $response = curl_exec($ch);
  266. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  267. $curlError = curl_error($ch);
  268. if ($response === false) {
  269. throw new \Exception("请求发送失败: " . $curlError);
  270. }
  271. $result = json_decode($response, true);
  272. // 检查API返回的错误
  273. if (isset($result['error'])) {
  274. $apiErrorDetail = $result['error']['message'] ?? '';
  275. $errorType = $result['error']['type'] ?? '';
  276. // 常见错误类型映射
  277. $errorMessages = [
  278. 'invalid_request_error' => '请求参数错误',
  279. 'authentication_error' => '认证失败',
  280. 'rate_limit_error' => '请求频率过高',
  281. 'insufficient_quota' => '额度不足',
  282. 'billing_not_active' => '账户未开通付费',
  283. 'content_policy_violation' => '内容违反政策'
  284. ];
  285. $friendlyMessage = $errorMessages[$errorType] ?? 'API服务错误';
  286. throw new \Exception("{$friendlyMessage}: {$apiErrorDetail}");
  287. }
  288. if ($httpCode !== 200) {
  289. // HTTP状态码映射
  290. $statusMessages = [
  291. 400 => '请求参数不合法',
  292. 401 => 'API密钥无效或权限不足',
  293. 403 => '访问被拒绝',
  294. 404 => 'API端点不存在',
  295. 429 => '请求过于频繁,请稍后再试',
  296. 500 => '服务器内部错误',
  297. 503 => '服务暂时不可用'
  298. ];
  299. $statusMessage = $statusMessages[$httpCode] ?? "HTTP错误({$httpCode})";
  300. throw new \Exception($statusMessage);
  301. }
  302. curl_close($ch);
  303. return $result;
  304. } catch (\Exception $e) {
  305. $lastError = $e->getMessage();
  306. $attempt++;
  307. if ($attempt <= $maxRetries) {
  308. sleep(pow(2, $attempt));
  309. } else {
  310. // 最终失败时的详细错误信息
  311. $errorDetails = [
  312. '错误原因' => $this->getErrorCause($httpCode, $apiErrorDetail),
  313. '解决方案' => $this->getErrorSolution($httpCode),
  314. '请求参数' => json_encode($data, JSON_UNESCAPED_UNICODE),
  315. 'HTTP状态码' => $httpCode,
  316. '重试次数' => $attempt
  317. ];
  318. throw new \Exception("API请求失败\n" .
  319. "失败说明: " . $errorDetails['错误原因'] . "\n" .
  320. "建议解决方案: " . $errorDetails['解决方案'] . "\n" .
  321. "技术详情: HTTP {$httpCode} - " . $lastError);
  322. }
  323. }
  324. }
  325. }
  326. /**
  327. * 根据错误类型获取友好的错误原因
  328. */
  329. private function getErrorCause($httpCode, $apiError)
  330. {
  331. $causes = [
  332. 401 => 'API密钥无效、过期或没有访问权限',
  333. 400 => $apiError ?: '请求参数不符合API要求',
  334. 429 => '已达到API调用频率限制',
  335. 403 => '您的账户可能没有开通相关服务权限',
  336. 500 => 'OpenAI服务器处理请求时出错'
  337. ];
  338. return $causes[$httpCode] ?? '未知错误,请检查网络连接和API配置';
  339. }
  340. /**
  341. * 根据错误类型获取解决方案建议
  342. */
  343. private function getErrorSolution($httpCode)
  344. {
  345. $solutions = [
  346. 401 => '1. 检查API密钥是否正确 2. 确认密钥是否有访问权限 3. 尝试创建新密钥',
  347. 400 => '1. 检查请求参数 2. 验证提示词内容 3. 参考API文档修正参数',
  348. 429 => '1. 等待1分钟后重试 2. 升级账户提高限额 3. 优化调用频率',
  349. 403 => '1. 检查账户状态 2. 确认是否已开通付费 3. 联系OpenAI支持',
  350. 500 => '1. 等待几分钟后重试 2. 检查OpenAI服务状态页'
  351. ];
  352. return $solutions[$httpCode] ?? '1. 检查网络连接 2. 查看服务日志 3. 联系技术支持';
  353. }
  354. }