必须改用异步轮询而非同步等待:/v2/text-to-image仅返回task_id,需调用/v2/tasks/{task_id}轮询状态,设CURLOPT_TIMEOUT于轮询接口而非提交接口,配合usleep、connection_aborted()、session_write_close()及状态机完备判断(含TIMEOUT)确保健壮。

PHP 调用万相-文生图 V2 API 时卡在 curl_exec() 不返回,不是网络问题,是同步阻塞 + 接口本身耗时长(常达 10–30 秒)导致的典型超时假象。必须弃用单次长等待,改用异步轮询。
为什么 curl_setopt($ch, CURLOPT_TIMEOUT, 30) 还会超时?
万相 V2 的图像生成是异步任务,/v2/text-to-image 接口返回的是 task_id,不是图片。直接对生成接口设 30 秒超时,反而容易失败——因为任务刚提交,后端还没开始画图,你却在等一个永远不返回的响应体。
常见错误现象:curl_exec() 阻塞超过 30 秒,PHP 报 Operation timed out after 30000 milliseconds,但万相控制台里任务其实已成功运行。
- 真正该设超时的是轮询状态接口(
/v2/tasks/{task_id}),不是提交接口 -
CURLOPT_TIMEOUT_MS在毫秒级下会绕过 DNS 超时,导致首次请求卡在解析阶段,建议统一用CURLOPT_TIMEOUT(秒级)+CURLOPT_CONNECTTIMEOUT分开控制 - 别在 Web 请求中用
sleep(1)轮询:它会锁死 PHP-FPM worker,用户看到白屏,Nginx 日志出现upstream timed out
怎么用 usleep() 安全轮询任务状态?
轮询必须“小步快跑”,避免阻塞、可控退出、防无限循环。关键不是睡多久,而是每次检查后是否让出执行权并判断环境。
立即学习“PHP免费学习笔记(深入)”;
- 用
usleep(500000)(0.5 秒)代替sleep(1),粒度更细,总耗时感知更低 - 循环开头加
if (connection_aborted()) { exit; },用户关页就停,不浪费资源 - 用
microtime(true)计算总耗时,而非固定次数;万相任务最长约 60 秒,设上限65秒足够 - 每次轮询前检查
http_response_code() === 200且返回 JSON 中status字段为"SUCCEEDED"或"FAILED",别只看 HTTP 状态码
如何避免轮询请求被 PHP-FPM 队列阻塞?
当用户发起生成请求(A)后立刻发起轮询请求(B),如果两个请求共用同一会话(session_start()),B 会被 A 持有的会话锁阻塞,直到 A 结束——这就是为什么 checkprogress.php 一直 pending。
- 提交任务的脚本(如
submit.php)里调用session_write_close(),释放会话锁后再进入轮询逻辑或重定向 - 轮询接口(如
poll.php?task_id=xxx)完全不要启动会话,或显式禁用:ini_set('session.use_cookies', 0); session_destroy(); - 更彻底的解法:把轮询逻辑移出 Web 层,用 CLI 脚本(
php poll_task.php --task-id=xxx)+ Redis 缓存结果,Web 层只做轻量状态查询 - 万相返回的
result_url是临时直链,有效期 5 分钟,轮询拿到后应立即file_get_contents()下载并保存到本地,别等前端再请求一次
轮询失败时要不要重试?重试几次?
万相接口偶发 502/504 或 "status": "PROCESSING" 卡住,不是代码 bug,是服务端瞬时压力。重试能显著提升成功率,但策略要克制。
- 对
/v2/tasks/{task_id}返回非 200(如 502、504、cURL 错误)时才重试,status === "PROCESSING"是正常态,不重试 - 最多重试 2 次,每次间隔从 0.5 秒逐步加到 1.5 秒(
usleep(500000 * ($retry + 1))) - 重试前加
if (connection_aborted())判断,避免用户已离开还继续发请求 - 万相文档明确写明:任务状态变更后,
result_url才有效;不要在"PROCESSING"时提前请求该 URL,会 404
最易被忽略的一点:轮询不是技术难点,而是状态机设计。万相的 status 有 "QUEUED"、"PROCESSING"、"SUCCEEDED"、"FAILED"、"TIMEOUT" 五种,漏判 "TIMEOUT" 会导致前端永远等待。别假设只有成功或失败两种终态。



















