|
|
@@ -50,13 +50,13 @@ class AIImageGenerationService
|
|
|
/**
|
|
|
* 单次上游请求超时(秒)
|
|
|
*
|
|
|
- * 必须小于队列 job 的 timeout(360 秒),保证请求先超时、由业务层记录错误并重新排队,
|
|
|
+ * 必须小于队列 job 的 timeout(540 秒),保证请求先超时、由业务层记录错误并重新排队,
|
|
|
* 而不是被 worker 强杀导致任务长期停留在 processing。
|
|
|
- * 取值依据(近 3 天成功任务实测):gpt-image-2 生成耗时 p99 ≈ 261~286s、max ≈ 297s,
|
|
|
- * 取 300s 基本不掐断正常请求(若取 240s 会掐掉 2.4%~5.4%,那部分请求上游往往已经生成
|
|
|
- * 完成,重试等于重复生成、白花一次费用),并给 job 留 60s 收尾时间,故取 300s。
|
|
|
+ * 取值依据:4K 编辑(3840x2160 / 2160x3840)平均 148~152s、长尾超过 300s;
|
|
|
+ * 批量提交时上游排队会把单请求推到 300s 以上,故取 480s 给长尾留出余量,
|
|
|
+ * 并给 job 留 60s 收尾时间。
|
|
|
*/
|
|
|
- const REQUEST_TIMEOUT_SECONDS = 300;
|
|
|
+ const REQUEST_TIMEOUT_SECONDS = 480;
|
|
|
/** 连接超时(秒) */
|
|
|
const REQUEST_CONNECT_TIMEOUT_SECONDS = 30;
|
|
|
/** extra_params 中记录重试信息的键名 */
|
|
|
@@ -435,12 +435,15 @@ class AIImageGenerationService
|
|
|
*/
|
|
|
public function submitTaskToGptImage2Api(MpGeneratePicTask $task): void
|
|
|
{
|
|
|
- try {
|
|
|
- // 上游任务/请求 ID:百度渠道在响应头中返回,成功或失败都尽量记录到 result_json
|
|
|
- $upstreamIds = [];
|
|
|
- // 本次上游返回的响应体(失败/重试路径用于保留原始结果并补记 token)
|
|
|
- $lastResponseData = null;
|
|
|
+ // 上游任务/请求 ID:百度渠道在响应头中返回,成功或失败都尽量记录到 result_json
|
|
|
+ $upstreamIds = [];
|
|
|
+ // 本次上游返回的响应体(失败/重试路径用于保留原始结果并补记 token)
|
|
|
+ $lastResponseData = null;
|
|
|
+ // 本次上游返回的响应头(编辑模式 cURL / 文生图 Guzzle 均采集),
|
|
|
+ // 超时或异常时写入 result_json,用于判断该次是否已拿到上游任务/请求 ID
|
|
|
+ $rawResponseHeaders = [];
|
|
|
|
|
|
+ try {
|
|
|
// 渠道判定:baidu- 前缀走「百度生成模型服务V2」(gpt-image-2 / 2.5 系),其余走现有快快渠道
|
|
|
$isBaiduChannel = strpos((string)$task->model, 'baidu-') === 0;
|
|
|
// 上游模型名不带渠道前缀:表里存 baidu-gpt-image-2,调用上游用 gpt-image-2
|
|
|
@@ -490,13 +493,17 @@ class AIImageGenerationService
|
|
|
|
|
|
// 创建专用客户端,优化配置
|
|
|
$gptClient = new Client([
|
|
|
- // 超时必须小于队列 job 的 timeout(300s):请求先超时并抛出异常,
|
|
|
+ // 超时必须小于队列 job 的 timeout(540s):请求先超时并抛出异常,
|
|
|
// 由业务层记录错误并重新排队,避免 worker 强杀导致任务长期悬挂
|
|
|
'timeout' => self::REQUEST_TIMEOUT_SECONDS,
|
|
|
'connect_timeout' => self::REQUEST_CONNECT_TIMEOUT_SECONDS,
|
|
|
'http_errors' => false, // 不自动抛出HTTP错误
|
|
|
'verify' => false, // 禁用SSL验证(如果有证书问题)
|
|
|
'allow_redirects' => true,
|
|
|
+ // 响应头到达即采集,body 卡住超时时也能记录到上游任务/请求 ID
|
|
|
+ 'on_headers' => function (\Psr\Http\Message\ResponseInterface $response) use (&$rawResponseHeaders) {
|
|
|
+ $rawResponseHeaders = $response->getHeaders();
|
|
|
+ },
|
|
|
]);
|
|
|
|
|
|
// 根据是否有参考图选择不同的请求方式
|
|
|
@@ -617,9 +624,7 @@ class AIImageGenerationService
|
|
|
|
|
|
$startTime = microtime(true);
|
|
|
|
|
|
- // 使用原生 cURL
|
|
|
- // 采集响应头(百度上游任务 ID 在其中),成功与失败响应都需要
|
|
|
- $rawResponseHeaders = [];
|
|
|
+ // 使用原生 cURL;响应头由方法上方统一声明的 $rawResponseHeaders 采集
|
|
|
$ch = curl_init();
|
|
|
curl_setopt_array($ch, [
|
|
|
CURLOPT_URL => $api_url,
|
|
|
@@ -633,7 +638,7 @@ class AIImageGenerationService
|
|
|
'Content-Length: ' . strlen($postFields),
|
|
|
'Expect:', // 移除 Expect: 100-continue,避免等待
|
|
|
],
|
|
|
- CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT_SECONDS, // 与 job timeout(300s) 对齐,先超时再落状态
|
|
|
+ CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT_SECONDS, // 与 job timeout(540s) 对齐,先超时再落状态
|
|
|
CURLOPT_CONNECTTIMEOUT => self::REQUEST_CONNECT_TIMEOUT_SECONDS,
|
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
|
CURLOPT_SSL_VERIFYHOST => false,
|
|
|
@@ -783,13 +788,13 @@ class AIImageGenerationService
|
|
|
|
|
|
if ($this->isRetryableApiError($e)) {
|
|
|
// 超时/连接失败等可重试错误:先记录上游返回结果/token,再放回队列尾部重试
|
|
|
- $failResultJson = $this->captureFailedAttempt($task, $lastResponseData, $upstreamIds, '图片任务失败重试,仅记录token不扣积分');
|
|
|
+ $failResultJson = $this->captureFailedAttempt($task, $lastResponseData, $upstreamIds, '图片任务失败重试,仅记录token不扣积分', $rawResponseHeaders);
|
|
|
$this->requeueTaskForRetry($task, $errorMessage, $failResultJson);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 最终失败:同样记录上游返回结果与 token(失败不扣积分)
|
|
|
- $failResultJson = $this->captureFailedAttempt($task, $lastResponseData, $upstreamIds, '图片任务最终失败,仅记录token不扣积分');
|
|
|
+ $failResultJson = $this->captureFailedAttempt($task, $lastResponseData, $upstreamIds, '图片任务最终失败,仅记录token不扣积分', $rawResponseHeaders);
|
|
|
$failData = ['error_message' => $errorMessage];
|
|
|
if (!empty($failResultJson)) {
|
|
|
$failData['result_json'] = $failResultJson;
|
|
|
@@ -990,6 +995,11 @@ class AIImageGenerationService
|
|
|
// 上游任务/请求 ID 随 result_json 一并落库,供月度消耗比对
|
|
|
$cleanedResponseData = $this->mergeUpstreamIds($cleanedResponseData, $upstreamIds);
|
|
|
|
|
|
+ // 保留此前重试失败时留下的超时响应头,任务最终成功也能回溯该次是否拿到上游 ID
|
|
|
+ if (is_array($task->result_json) && !empty($task->result_json['raw_response_headers'])) {
|
|
|
+ $cleanedResponseData['raw_response_headers'] = $task->result_json['raw_response_headers'];
|
|
|
+ }
|
|
|
+
|
|
|
// 图片生成成功统一处理(事务:状态更新 + 计费 + 分镜同步)
|
|
|
$this->handleImageTaskSuccess($task, $result_urls, $cleanedResponseData);
|
|
|
} catch (\Exception $e) {
|
|
|
@@ -2638,19 +2648,32 @@ class AIImageGenerationService
|
|
|
* @param mixed $responseData 上游响应体(数组或 null)
|
|
|
* @param array $upstreamIds 上游响应头中解析出的任务/请求 ID
|
|
|
* @param string $remark 备注
|
|
|
+ * @param array $rawResponseHeaders 本次收到的原始响应头(超时/请求中断时留痕)
|
|
|
* @return array 写入 result_json 的内容
|
|
|
*/
|
|
|
- private function captureFailedAttempt(MpGeneratePicTask $task, $responseData, array $upstreamIds, string $remark): array
|
|
|
+ private function captureFailedAttempt(MpGeneratePicTask $task, $responseData, array $upstreamIds, string $remark, array $rawResponseHeaders = []): array
|
|
|
{
|
|
|
$resultJson = [];
|
|
|
try {
|
|
|
$resultJson = is_array($responseData) ? $this->removeB64JsonFromResponse($responseData) : [];
|
|
|
- $resultJson = $this->mergeUpstreamIds($resultJson, $upstreamIds);
|
|
|
|
|
|
// 本次没有响应体时,保留此前已记录的 result_json,避免用空值覆盖
|
|
|
if (empty($resultJson)) {
|
|
|
$resultJson = is_array($task->result_json) ? $task->result_json : [];
|
|
|
}
|
|
|
+
|
|
|
+ $resultJson = $this->mergeUpstreamIds($resultJson, $upstreamIds);
|
|
|
+
|
|
|
+ // 超时/请求中断时也留下本次收到的响应头;本次没拿到时沿用此前记录的,
|
|
|
+ // 便于事后判断超时那次到底有没有拿到上游 ID
|
|
|
+ $headersToKeep = !empty($rawResponseHeaders)
|
|
|
+ ? $rawResponseHeaders
|
|
|
+ : (is_array($task->result_json) && !empty($task->result_json['raw_response_headers'])
|
|
|
+ ? $task->result_json['raw_response_headers']
|
|
|
+ : []);
|
|
|
+ if (!empty($headersToKeep)) {
|
|
|
+ $resultJson['raw_response_headers'] = $headersToKeep;
|
|
|
+ }
|
|
|
} catch (\Throwable $e) {
|
|
|
dLog('generate')->warning('记录失败尝试上游结果异常(不影响token补记): ' . $e->getMessage(), [
|
|
|
'task_id' => $task->task_id ?? null,
|