AI 生图显示 PROVIDER_SUBMISSION_UNKNOWN:真正失败在 R2 资产入库

  1. 1. 问题现象
  2. 2. 排查过程
    1. 2.1. 从任务表追到 Attempt
    2. 2.2. 还原真实调用链
    3. 2.3. 检查 R2 凭据格式
  3. 3. 根因
    1. 3.1. 配置错误:Access Key ID 与 Secret Access Key 填反或填错
    2. 3.2. 代码错误:把所有同步 Generate 错误都当成提交未知
  4. 4. 解决方案
    1. 4.1. 为结果入库错误建立明确类型
    2. 4.2. 校验 R2 配置,尽早阻止凭据填反
  5. 5. 验证与回归
  6. 6. 常见误区与边界
  7. 7. 检查清单

任务状态显示 PROVIDER_SUBMISSION_UNKNOWN 时,不一定意味着模型服务没有返回结果。一次实际故障证明,供应商已经完成生图,失败发生在结果写入 R2 的最后一步。本文记录如何通过 Attempt 持久化数据还原真实数据流,并修正错误分类和凭据校验。

问题现象

前端收到的任务快照大致如下,敏感标识已替换:

1
2
3
4
5
6
7
8
9
{
"media_type": "image",
"model_id": "doubao-seedream-5-0-pro-260628",
"status": "failed",
"progress": 35,
"actual_credits": 0,
"error_code": "PROVIDER_SUBMISSION_UNKNOWN",
"error_message": "供应商提交状态未知,已转人工对账且不会扣除积分"
}

Worker 日志同时出现:

1
2
provider submission quarantined
provider_charged=false

第一反应通常是重试模型请求。但对没有幂等键或提交查询能力的供应商,盲目重试可能产生重复计费,所以必须先读 Attempt 的内部错误。

排查过程

从任务表追到 Attempt

任务表里的错误是面向用户的摘要,技术原因保存在 video_generation_task_attempt

1
2
3
4
5
6
SELECT status, provider_job_id, checkpoint, error_code,
error_message, provider_charged, reconciliation_required
FROM video_generation_task_attempt
WHERE task_id = '<task-id>'
ORDER BY attempt_number DESC
LIMIT 1;

这次 Attempt 的内部错误是:

1
2
persisting Volcengine image result: putting private provider artifact:
Credential access key has length 64, should be 32

这条信息改变了排查方向:供应商提交并不是未知,平台是在保存供应商结果时失败。

还原真实调用链

对于同步返回图片的模型,Worker 的调用链是:

1
2
3
4
5
6
7
WorkerService.Process
-> Provider.Submit
-> Provider.Generate
-> Volcengine /api/v3/images/generations
-> 得到图片 URL
-> StoreRemote 下载图片
-> PrivateArtifactStore 写入 R2

火山方舟已经返回图片 URL,StoreRemote 成功拿到图片后,PrivateArtifactStore 调用 S3 客户端上传对象。R2 在签名阶段拒绝了凭据,因此图片没有成为平台资产。

检查 R2 凭据格式

Cloudflare R2 的 S3 凭据应区分两个字段:

字段 典型长度 用途
Access Key ID 32 位 签名中的访问标识
Secret Access Key 64 位 签名密钥,不应填入 Access Key ID

数据库里只检查长度和脱敏后的非敏感字段,不要把密钥原文打印到日志:

1
2
3
4
SELECT provider, endpoint, account_id, bucket,
CHAR_LENGTH(access_key_id) AS access_key_length
FROM video_storage_setting
WHERE id = 1;

本次查询确认 Access Key ID 是 64 位,正好对应 R2 返回的拒绝原因。此前还发现 Endpoint 中的账户 ID 与 Account ID 字段不一致,也需要一并修正。

根因

这里有两个层次的错误。

配置错误:Access Key ID 与 Secret Access Key 填反或填错

R2 根据密钥格式直接拒绝了对象上传。因为上传发生在模型返回图片之后,所以“模型生成失败”不是事实。

代码错误:把所有同步 Generate 错误都当成提交未知

原来的 Submit 对非视频模型统一包装:

1
2
3
4
5
6
7
result, err := p.Generate(ctx, task)
if err != nil {
return nil, &domainvideo.SubmissionError{
Err: err,
Unknown: true,
}
}

这会把三类完全不同的错误混在一起:请求没发出去、供应商明确拒绝、供应商已返回结果但平台入库失败。最后一类被错误地送进人工对账隔离分支。

解决方案

为结果入库错误建立明确类型

在供应商适配器中,图片、文本和同步视频结果写入失败时使用专门的 artifactPersistenceError 包装。它表达一个关键事实:上游结果已经拿到,平台没有完成资产提交。

Submit 遇到该类型时返回:

1
2
3
4
5
&domainvideo.SubmissionError{
Err: err,
Unknown: false,
Charged: true,
}

Worker 随后使用 PROVIDER_CHARGED_NO_ARTIFACT 完成失败结算,释放用户预留积分,同时创建需要关注的供应商成本对账记录。它不会把同一请求盲目再提交一次。

校验 R2 配置,尽早阻止凭据填反

管理服务现在在保存和测试 Cloudflare R2 配置时校验:

1
2
3
4
5
6
7
8
if len(strings.TrimSpace(input.AccessKeyID)) != 32 {
return invalidAdminInput(
"Cloudflare R2 Access Key ID 必须为 32 个字符,请勿填写 64 位 Secret Access Key",
)
}
if secret := strings.TrimSpace(input.SecretKey); secret != "" && len(secret) != 64 {
return invalidAdminInput("Cloudflare R2 Secret Access Key 必须为 64 个字符")
}

同时校验 Endpoint 与 Account ID 是否属于同一 R2 账户。后台的“测试配置”按钮仍然会执行真实的验桶、写入、读取和删除测试;测试通过后再保存并重启 API、Worker。

验证与回归

新增了两组回归测试:

  • Provider 收到图片结果后,模拟对象存储失败,确认返回的是 Charged=trueUnknown=false
  • Worker 收到已计费的资产入库错误,确认 Attempt 进入 failed,错误码为 PROVIDER_CHARGED_NO_ARTIFACT,并创建对账状态。

实际运行的验证命令:

1
2
3
4
make test
make lint
make build
curl -fsS http://localhost:8080/health

Go 测试、前端测试、go vet、ESLint、Prettier、TypeScript 检查和构建均通过。修复后的 API 与 Worker 热重载后保持运行。

常见误区与边界

  • PROVIDER_SUBMISSION_UNKNOWN 是一个需要谨慎处理的安全状态,不应该看到它就直接重试。先查 Attempt 的 error_messagecheckpoint
  • 供应商已经返回结果、但本地或对象存储入库失败时,重试生成请求可能造成重复成本。正确做法是修复存储后重新处理,或创建新的用户重试任务。
  • actual_credits: 0 只说明平台没有向用户结算成功结果,不等于供应商一定没有产生费用;后者需要通过对账记录和供应商账单确认。
  • R2 Endpoint、Account ID、Access Key ID、Secret Access Key 必须来自同一账户。测试通过后仍需重启 API 与 Worker,因为存储客户端在进程启动时构建。

检查清单

  • 先查 video_generation_task_attempt 的内部错误,不只看任务摘要。
  • 确认供应商是否已经返回图片 URL 或其他结果。
  • 检查 Access Key ID 与 Secret Access Key 是否填反,长度是否分别为 32 和 64。
  • 校验 R2 Endpoint 与 Account ID 是否匹配。
  • 先点击对象存储“测试配置”,再保存并重启 API、Worker。
  • 只有在确认提交未成功且供应商支持安全重试时,才重新提交同一类请求。
投喂小莫
给快要饿死的小莫投喂点零食吧~
投喂小莫
分享
分享提示信息