2026 01 16 image load failed retry mechanism
本地来源:Knowledge/World/项目/Practice/BUG-SOLUTIONS/2026-01-16-image-load-failed-retry-mechanism.md
2026-01-16 Image Load Failed 自动重试机制问题报告
TL;DR 速查表
问题速览
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 1 | Image load failed 任务直接失败 | FAL API 网络波动 | 自动重试 3 次,间隔 2 秒 |
| 2 | 用户输入网址而非图片 URL | 用户误解 | isLikelyImageUrl() 检测,跳过重试 |
| 3 | 轮询端点不发飞书通知 | 代码遗漏 | /api/ai/query 加入通知逻辑 |
| 4 | retryCount 信息丢失 | FAL 响应覆盖 | 合并保留 retryCount 字段 |
| 5 | 多语言错误提示缺失 | 新增场景 | 10 个语言文件各加 3 条翻译 |
关键函数速查
// 判断错误是否可重试
isRetryableError(errorMessage) // 检测 "image load failed" 等关键词
// 判断 URL 是否像图片
isLikelyImageUrl(url) // 检测扩展名、CDN 域名、路径特征
// 获取重试次数
getRetryCount(taskInfo) // 从 JSON 解析 retryCount
// 发送飞书错误通知
sendErrorNotification({...}) // 在重试耗尽或不可重试时调用
重试决策流程图
任务失败
↓
是可重试错误? ─否→ 直接失败 + 发通知
↓是
URL 像图片? ─否→ 跳过重试 + 发通知(标注用户输入错误)
↓是
重试次数 < 3? ─否→ 最终失败 + 发通知(标注已重试 N 次)
↓是
等待 2 秒 → 重新提交任务 → 继续轮询
修改文件清单
| 文件 | 改动 |
|---|---|
src/app/api/ai/query/route.ts |
+重试逻辑 +URL检测 +飞书通知 |
src/config/locale/messages/*/ai/image.json |
+3 条错误提示 ×10 语言 |
项目信息
- 项目名称: longcat-video.org
- 涉及模块: AI 图像/视频生成服务
- AI 服务商: FAL API
- 报告日期: 2026-01-16
问题一:Image load failed 错误无法自动恢复
具体问题
FAL API 在处理图生图/图生视频任务时,偶尔返回 "Image load failed" 错误,导致任务直接失败。
现象
- 用户提交图生图或图生视频任务
- FAL API 返回错误状态,错误信息为 "Image load failed"
- 任务被标记为 FAILED,用户积分被扣除但无产出
- 用户需要手动重新提交任务
本质
FAL API 服务端在下载用户提供的图片 URL 时,由于网络波动、CDN 延迟、或临时不可达等原因,导致图片加载失败。这是一个可恢复的临时性错误,而非永久性错误。
罪魁祸首
- FAL API 服务端网络不稳定 - 从用户图片 URL 下载图片时偶发失败
- 缺乏重试机制 - 原代码在收到失败状态后直接标记任务失败,没有尝试重试
- 错误处理过于简单 - 没有区分可恢复错误和不可恢复错误
解决办法
在 /api/ai/query/route.ts 中增加自动重试逻辑:
const MAX_RETRY_COUNT = 3;
const RETRY_DELAY_MS = 2000;
// 检测是否是可重试的错误
function isRetryableError(errorMessage?: string): boolean {
if (!errorMessage) return false;
const lower = errorMessage.toLowerCase();
return (
lower.includes('image load failed') ||
lower.includes('failed to fetch') ||
lower.includes('failed to download') ||
lower.includes('url is not accessible') ||
lower.includes('connection timeout') ||
lower.includes('network error')
);
}
// 重试逻辑
if (isFailed && canRetry) {
await sleep(RETRY_DELAY_MS);
const retryResult = await aiProvider.generate?.({ params: { ... } });
if (retryResult?.taskId) {
// 更新任务为新的 taskId,继续轮询
}
}
关键代码修改
文件: src/app/api/ai/query/route.ts
- 新增
isRetryableError()函数判断错误是否可重试 - 新增
getRetryCount()函数从 taskInfo 获取已重试次数 - 在检测到可重试错误时,等待 2 秒后重新调用
aiProvider.generate() - 重试成功后更新任务的
taskId,保留retryCount信息 - 最多重试 3 次,超过后才标记为最终失败
问题二:用户输入网站地址而非图片 URL
具体问题
部分用户在图生图/图生视频功能中,输入的是网站首页地址(如 https://oleificioferreri.eu),而非实际的图片直链。
现象
- 用户输入
https://example.com这样的网站地址 - FAL API 尝试下载该 URL,返回 "Image load failed"
- 系统误判为临时网络问题,触发自动重试
- 重试 3 次后仍然失败,浪费服务器资源
本质
用户不理解"图片 URL"的含义,误以为输入任意包含图片的网站地址即可。实际上需要输入图片的直接链接(如 https://example.com/image.jpg)。
罪魁祸首
- 前端校验不足 - 没有在提交前检测 URL 是否像图片
- 用户引导不清晰 - 提示文案没有明确说明需要直链
- 重试逻辑无差别对待 - 对明显错误的 URL 也进行重试
解决办法
增加 URL 有效性检测,跳过对明显非图片 URL 的重试:
function isLikelyImageUrl(url: string | undefined): boolean {
if (!url) return false;
const lower = url.toLowerCase();
// 1. 检查图片扩展名
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.avif', '.bmp', '.tiff'];
if (imageExtensions.some((ext) => lower.includes(ext))) return true;
// 2. 检查 CDN/存储域名
const cdnDomains = ['r2.dev', 'cloudflare', 'amazonaws.com', 's3.', 'fal.media', ...];
if (cdnDomains.some((domain) => lower.includes(domain))) return true;
// 3. 检查 URL 路径特征
if (lower.includes('/image') || lower.includes('/img') || lower.includes('/photo')) return true;
// 4. 如果是网站首页,认为不是图片
try {
const parsed = new URL(url);
if (parsed.pathname === '/' || parsed.pathname === '') return false;
} catch {}
return false;
}
判断逻辑
| URL 示例 | 判断结果 | 原因 |
|---|---|---|
https://example.com/photo.jpg |
✅ 是图片 | 包含 .jpg 扩展名 |
https://cdn.example.com/abc123 |
✅ 是图片 | CDN 域名 |
https://fal.media/files/xxx |
✅ 是图片 | fal.media 域名 |
https://example.com/ |
❌ 不是图片 | 网站首页 |
https://oleificioferreri.eu |
❌ 不是图片 | 网站首页,无图片特征 |
飞书通知增强
当检测到用户输入的 URL 不是图片时,在飞书错误通知中附加说明:
if (isRetryable && !imageUrlLooksValid && imageUrl) {
errorSuffix = ` [用户输入的 URL 不是图片: ${imageUrl.slice(0, 60)}]`;
}
问题三:飞书错误通知只在 Webhook 端点发送
具体问题
原来的飞书错误通知只在 /api/ai/notify/[provider](Webhook 回调)中实现,但轮询端点 /api/ai/query 检测到失败时不发送通知。
现象
- 部分任务通过 Webhook 回调更新状态,错误会发送飞书通知
- 部分任务通过前端轮询
/api/ai/query更新状态,错误不发送通知 - 运维无法及时发现通过轮询检测到的错误
本质
状态更新有两个入口(Webhook 和轮询),但错误通知只在 Webhook 入口实现,导致通知覆盖不全。
罪魁祸首
代码设计时只考虑了 Webhook 场景,忽略了轮询场景也需要发送通知。
解决办法
在 /api/ai/query/route.ts 中也加入飞书错误通知逻辑:
import { sendErrorNotification } from '@/extensions/notification';
// 检查是否需要发送错误通知
const shouldNotifyError =
!isTerminalStatus(task.status) &&
(result.taskStatus === AITaskStatus.FAILED || result.taskStatus === AITaskStatus.CANCELED) &&
(!isRetryable || !imageUrlLooksValid || actualRetryCount >= MAX_RETRY_COUNT || (retryAttempted && !retrySucceeded));
if (shouldNotifyError) {
await sendErrorNotification({
email: user.email,
name: user.name,
apiEndpoint: errorDetails.apiEndpoint,
apiProvider: task.provider,
errorCode: errorDetails.errorCode,
errorMessage: errorDetails.errorMessage + errorSuffix,
prompt: task.prompt,
type: buildTaskType(task),
taskId: task.taskId || task.id,
});
}
通知发送条件
只有满足以下条件才发送通知,避免重复告警:
- 任务从非终态变为 FAILED 或 CANCELED
- 且满足以下任一条件: - 不是可重试的错误(如 NSFW 被拦截) - 图片 URL 无效(用户输入错误) - 已达到最大重试次数(3 次) - 本次重试失败
问题四:重试次数信息丢失
具体问题
在重试失败后,retryCount 信息没有正确保存到 taskInfo 中。
现象
- 第一次重试失败后,
retryCount应该是 1 - 但查询 taskInfo 时发现
retryCount丢失或为 0 - 导致通知中显示的重试次数不准确
本质
重试失败后,代码使用 result.taskInfo(来自 FAL API 的原始响应)构建 updateAITask,而 FAL API 的响应中不包含我们自定义的 retryCount 字段。
罪魁祸首
// 错误代码:直接使用 result.taskInfo,丢失了 retryCount
const updateAITask: UpdateAITask = {
taskInfo: result.taskInfo ? JSON.stringify(result.taskInfo) : null,
// ...
};
解决办法
在构建 updateAITask 前,合并保留 retryCount:
// 正确代码:保留重试次数信息
const updatedTaskInfo = (() => {
const base = isRecord(result.taskInfo) ? result.taskInfo : {};
if (currentRetryCount > 0 || retryAttempted) {
return {
...base,
retryCount: retryAttempted ? currentRetryCount + 1 : currentRetryCount,
};
}
return base;
})();
const updateAITask: UpdateAITask = {
taskInfo: Object.keys(updatedTaskInfo).length > 0 ? JSON.stringify(updatedTaskInfo) : null,
// ...
};
问题五:多语言错误提示缺失
具体问题
新增的错误场景(URL 无效、重试中、重试耗尽)没有对应的多语言翻译。
现象
前端显示英文错误提示,非英语用户体验差。
解决办法
在 10 个语言文件中添加 3 条新的错误消息:
新增字段:
- error_invalid_image_url - 用户输入的不是图片链接
- error_retrying - 正在自动重试 ({current}/{max})
- error_retry_exhausted - 重试 {count} 次后仍失败
涉及文件:
- src/config/locale/messages/en/ai/image.json
- src/config/locale/messages/zh/ai/image.json
- src/config/locale/messages/ar/ai/image.json
- src/config/locale/messages/de/ai/image.json
- src/config/locale/messages/es/ai/image.json
- src/config/locale/messages/fr/ai/image.json
- src/config/locale/messages/it/ai/image.json
- src/config/locale/messages/ja/ai/image.json
- src/config/locale/messages/ko/ai/image.json
- src/config/locale/messages/pt/ai/image.json
查漏补缺
已确认不受影响的场景
| 场景 | 是否受影响 | 原因 |
|---|---|---|
| 视频 URL 被误判为图片 | ❌ 不受影响 | isLikelyImageUrl() 只检查 options.image_url,不检查 taskResult 中的视频输出 |
| 文生图任务 | ❌ 不受影响 | 文生图没有 image_url 参数,isLikelyImageUrl() 返回 false,不触发重试 |
| 正常的图片 URL | ✅ 正常工作 | CDN 域名或带扩展名的 URL 会被正确识别 |
潜在改进点
- 前端校验 - 可以在前端提交前就检测 URL 是否像图片,提前提示用户
- 图片预加载验证 - 在提交任务前,先尝试 HEAD 请求验证图片可访问性
- 错误分类细化 - 可以进一步区分"网络超时"和"404 不存在"等不同错误类型
修改文件清单
| 文件路径 | 修改内容 |
|---|---|
src/app/api/ai/query/route.ts |
新增自动重试逻辑、URL 有效性检测、飞书错误通知 |
src/app/api/ai/notify/[provider]/route.ts |
已有飞书通知逻辑(本次未修改) |
src/config/locale/messages/*/ai/image.json |
新增 3 条多语言错误提示(10 个语言文件) |
测试建议
- 正常图片 URL 测试 - 提交有效的图片 URL,确认正常生成
- 网站地址测试 - 提交
https://example.com,确认不重试且收到飞书通知 - 临时网络错误模拟 - 如果可能,模拟临时网络错误,确认自动重试机制工作
- 多语言测试 - 切换不同语言,确认错误提示正确显示
总结
本次修改主要解决了 FAL API "Image load failed" 错误导致任务直接失败的问题,通过增加自动重试机制提高了任务成功率。同时针对用户输入错误(网站地址而非图片 URL)的场景进行了识别和处理,避免无意义的重试,并通过飞书通知及时告知运维人员。
SOP 检查清单
部署前检查
- [ ] 代码编译通过 -
npm run build无错误 - [ ] TypeScript 类型检查 - 无 type error
- [ ] 环境变量配置 - 确认
FEISHU_WEBHOOK_ERRORS已配置
功能验证清单
1. 正常流程测试
- [ ] 图生图:有效图片 URL → 生成成功
- [ ] 图生视频:有效图片 URL → 生成成功
- [ ] 文生图:无 image_url → 生成成功(不触发重试逻辑)
2. 重试机制测试
- [ ] 模拟网络错误 → 自动重试(检查日志
[AI Query] Retryable error detected) - [ ] 重试成功 → 任务继续执行,taskInfo 包含
retryCount - [ ] 重试 3 次失败 → 最终标记 FAILED,发送飞书通知
3. 无效 URL 测试
- [ ] 输入网站首页
https://example.com→ 不重试,直接失败 - [ ] 飞书通知包含
[用户输入的 URL 不是图片: ...] - [ ] 日志输出
[AI Query] Image URL does not look like an image, skipping retry
4. 飞书通知测试
- [ ] Webhook 端点失败 → 收到飞书通知
- [ ] 轮询端点失败 → 收到飞书通知
- [ ] 重试中不发通知 → 只在最终失败时发送
- [ ] 通知内容包含:用户邮箱、错误信息、taskId、重试次数
5. 多语言测试
- [ ] 切换到中文 → 显示中文错误提示
- [ ] 切换到英文 → 显示英文错误提示
- [ ] 新增的 3 条 key 都有对应翻译:
error_invalid_image_urlerror_retryingerror_retry_exhausted
监控与告警
- [ ] 日志关键词监控(可选)
[AI Query] Retryable error detected- 触发重试[AI Query] Retry successful- 重试成功[AI Query] Retry failed- 重试失败-
[AI Query] Image URL does not look like an image- 无效 URL -
[ ] 飞书通知频率检查
- 正常情况:偶发通知(网络问题导致的最终失败)
- 异常情况:大量通知(可能是 FAL API 故障或配置问题)
回滚方案
如需回滚,直接 revert 以下 commit:
git revert e083b72 # feat: add auto-retry for image load failures
回滚后影响: - Image load failed 错误将不再自动重试 - 轮询端点将不再发送飞书错误通知 - 多语言错误提示将缺失(前端需要降级处理)
变更记录
| 日期 | 版本 | 变更内容 | 作者 |
|---|---|---|---|
| 2026-01-16 | v1.0 | 初始版本:自动重试机制、URL 检测、多语言支持 | Claude |
本文档为站内渲染。原始文件本地路径:saas/source/knowledge-world/Knowledge-World-项目-Practice-BUG-SOLUTIONS-2026-01-16-image-l-10a286.md(仅本地保留,不入库不部署)