{"record":{"id":"a1d0baa380659d12","repo":"yikart/AiToEarn","slug":"aicallfailed-a1d0ba","errorCode":"AiCallFailed","errorMessage":"AiCallFailed","messagePattern":"AiCallFailed","errorType":"error_code","errorClass":"AppException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/libs/volcengine/services/drama-recap.service.ts","lineNumber":79,"sourceCode":"    request: QueryDramaRecapTaskRequest,\n  ): Promise<QueryDramaRecapTaskResponse> {\n    const api = this.vodService.createAPI<\n      QueryDramaRecapTaskRequest,\n      QueryDramaRecapTaskResponse\n    >('QueryDramaRecapTask', {\n      Version: '2025-03-03',\n      method: 'GET',\n      contentType: 'json',\n    })\n    const response = await api(request)\n    // 检查 API 响应中的错误\n    if (response.ResponseMetadata?.Error) {\n      const error = response.ResponseMetadata.Error\n      this.logger.error(\n        { error, requestData: request },\n        `QueryDramaRecapTask failed: ${error.Code || 'Unknown'} - ${error.Message}`,\n      )\n      throw new AppException(ResponseCode.AiCallFailed, {\n        code: error.Code || 'Unknown',\n        message: error.Message,\n      })\n    }\n\n    // 对于短剧解说任务，Result 可能为空（任务刚提交还在准备中）\n    // 返回一个默认的处理中状态，让上层继续轮询\n    if (!response.Result) {\n      this.logger.debug({ taskId: request.TaskId }, '[getDramaRecapTask] Result 为空，任务可能还在准备中')\n      return {\n        TaskId: request.TaskId,\n        Status: DramaRecapTaskStatus.Processing,\n      }\n    }\n\n    return response.Result\n  }\n}","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-ai/src/core/ai/libs/volcengine/services/drama-recap.service.ts#L61-L97","documentation":"drama-recap.service.ts 的 getDramaRecapTask 查询短剧解说任务时，若响应 ResponseMetadata.Error 存在（火山引擎业务级错误，如无效任务 ID、权限、限流），则统一包装为 AiCallFailed 抛出，附原始 Code 和 Message。","triggerScenarios":"调用 getDramaRecapTask（或已废弃的 queryDramaRecapTask）时火山返回带 ResponseMetadata.Error 的响应，例如 QueryDramaRecapTask 的 TaskId 不存在、AK/SK 权限不足、签名错误、配额/限流 InvalidRequest 或 InternalError。","commonSituations":"AK/SK 配置错误或未开通短剧解说权限；查询尚未提交成功的 TaskId；账号被限流；火山侧服务故障；任务属于其他 Region/账号。","solutions":["读取异常 data 中的 code/message（如 InvalidTaskId、SignatureDoesNotMatch），按具体错误处理。","核对 VolcengineConfig 中 accessKeyId/secretAccessKey 及对应服务开通状态。","确认 TaskId 来自 createDramaRecapTask 的返回且仍在有效期内。","对限流类错误（RequestLimitExceeded 等）加指数退避重试。"],"exampleFix":"// before\nconst res = await volcengineService.getDramaRecapTask({ TaskId })\n// after\ntry {\n  const res = await volcengineService.getDramaRecapTask({ TaskId })\n} catch (e) {\n  const code = (e as any)?.data?.code\n  if (code === 'RequestLimitExceeded') return retryWithBackoff()\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"if (!taskId) throw new Error('TaskId is required')\n// env check\nfor (const k of ['VOLC_ACCESS_KEY_ID', 'VOLC_SECRET_ACCESS_KEY']) {\n  if (!process.env[k]) throw new Error(`Missing env ${k}`)\n}","typeGuard":"function hasVolcengineError(r: unknown): r is { ResponseMetadata: { Error: { Code: string, Message: string } } } {\n  return r != null && typeof r === 'object'\n    && !!(r as any).ResponseMetadata?.Error\n}","tryCatchPattern":"try {\n  const res = await getDramaRecapTask({ TaskId })\n} catch (e) {\n  const code = (e as any)?.data?.code\n  if (code === 'RequestLimitExceeded') return retryWithBackoff()\n  if (code === 'InvalidTaskId' || code === 'ResourceNotFound') return markTaskFailed(taskId)\n  throw e\n}","preventionTips":["按异常 data.code 分支处理，而不是把所有 AiCallFailed 一律重试","确保 AK/SK 来自已开通短剧解说能力的账号并定期轮换","只查询 createDramaRecapTask 返回的 TaskId","对限流错误使用指数退避，避免风暴式重试"],"tags":["volcengine","api-error","drama-recap","ai-call"],"backgroundTag":"upstream-api-error","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}