"""内部接口路由。 **本层只做编排,不含任何厂商知识**:解析请求 → 按 `media_kind` 找适配器 → 调用 → 把领域结果序列化。厂商差异全在 `app/providers/<厂商>/` 内。 一处刻意的选择:路径参数是**服务商的任务 ID**,不是我方 `job_id`。本服务不持有 状态,唯一能用来问服务商的就是这个 ID;若用 `job_id` 就得存一份映射,「无状态」 不再成立,且会多出一个「Java 拿到 ID 但查不到任何东西」的中间态。 """ from __future__ import annotations import logging from fastapi import APIRouter, Depends, Request from app.domain.generation import MediaKind from app.providers.base import GenerationProvider from app.providers.registry import ProviderRegistry from app.providers.volcengine.client import ArkError from .auth import verify_service_token from .errors import ProviderError, ServiceError, ServiceErrorCode from .schemas import SubmitRequest, SubmitResponse, TaskResponse log = logging.getLogger(__name__) router = APIRouter( prefix="/internal/generation", dependencies=[Depends(verify_service_token)], ) def _registry(request: Request) -> ProviderRegistry: return request.app.state.registry def _provider_for( registry: ProviderRegistry, media_kind: MediaKind ) -> GenerationProvider: """按 `media_kind` 取适配器。 查不到即 `provider_unavailable`(503)而不是 500:这通常是凭证未配置导致适配器 没装配,属环境问题而非代码缺陷,且对调用方而言是**可重试**的——配好就能用。 """ for provider in registry.all(): if provider.media_kind is media_kind: return provider raise ServiceError( ServiceErrorCode.PROVIDER_UNAVAILABLE, f"没有可用的 {media_kind.value} 生成适配器(检查服务商凭证是否已配置)", ) @router.post("/tasks", response_model=SubmitResponse) def submit_task(payload: SubmitRequest, request: Request) -> SubmitResponse: """提交生成任务,返回服务商任务 ID。 调用方**必须在上游产生任何计费动作之前**把该 ID 落库——火山不支持幂等键, 这是提交超时后唯一能判断「是否已提交」的依据。 """ provider = _provider_for(_registry(request), payload.media_kind) try: provider_request_id = provider.submit(payload.to_domain()) except ArkError as exc: # 服务商调用失败:原样带上分类,调用方据 retry 决定重试。 raise ProviderError(exc.failure) from exc except ValueError as exc: # 本地预校验拦下的参数问题(超出该模型的时长/分辨率/ratio 限制,或内容为空)。 # 属 invalid_request 而非 provider 错误——请求根本没发出去。 raise ServiceError(ServiceErrorCode.INVALID_REQUEST, str(exc)) from exc log.info( "已提交生成任务 job_id=%s provider_request_id=%s", payload.job_id, provider_request_id, ) return SubmitResponse(provider_request_id=provider_request_id) @router.get("/tasks/{provider_request_id}", response_model=TaskResponse) def get_task( provider_request_id: str, request: Request, media_kind: MediaKind = MediaKind.VIDEO, ) -> TaskResponse: """查询任务。 **任务失败也返回 200**:HTTP 调用成功了,失败的是任务,`state=failed` + `failure` 表达它。做成非 2xx 会让「查不到任务」与「任务失败了」难以区分。 :param media_kind: 决定问哪个适配器。默认 video——当前只有视频适配器; 接入图片后调用方应显式传入。刻意不做「遍历所有适配器试一遍」:那会把 一个任务 ID 拿去问不相干的服务商。 """ provider = _provider_for(_registry(request), media_kind) try: result = provider.fetch(provider_request_id) except ArkError as exc: # 服务商说任务不存在(含超过 7 天保留期)→ 这是 service 类的 task_not_found, # 而不是 provider 错误:调用方要做的是停止轮询,不是退避重试。 if exc.http_status == 404: raise ServiceError( ServiceErrorCode.TASK_NOT_FOUND, f"服务商无此任务(或已超出其 7 天保留期):{provider_request_id}", ) from exc raise ProviderError(exc.failure) from exc return TaskResponse.of(result)