dc816991d6
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3m38s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 4m3s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4m24s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 4m58s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m9s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m34s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m40s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m17s
CI/CD Pipeline / CI Gate (pull_request) Successful in 47s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 45s
AI Code Review / AI Code Review (pull_request) Successful in 7m56s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Frontend Lint (push) Failing after 10m18s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m17s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m45s
CI/CD Pipeline / Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Unit Tests (push) Successful in 14m44s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
P0 线上事故:前端 smartMatchAssets 按 {items: AssetItem[]} 解析,
items.map(a => a.id) 直接取 id;但后端 SmartMatchItem 为
{asset: AssetResponse, score, breakdown} 包装结构,导致 id 全为
undefined → GET /assets/undefined 404 连发、POST /clips/from-assets
携带 null 422,自动模式预览无法生成、标题不显示。
修复:
- SmartMatchItem 改为继承 AssetResponse 的扁平结构:id/usable/
used_duration/available_duration/used_ratio 等素材字段全部在 item
顶层,前端拿到即可用;score/breakdown 评分字段保留
- 路由用 model_dump() 解包构造,packages.domain.smart_select_assets
域层 SmartMatchResult.asset 不动,generation_tasks 内部生成链路
([r.asset.id for r in results])零影响
- ClipsFromAssetsRequest 加 pre validator 过滤 null/空串/空白 id;
from-assets 路由层二次归一化兜底,全非法时 400 而非 422/500
测试:扁平结构 3 用例(顶层 id/无 asset 包装/余量字段可读/评分保留)、
端点 TestClient 断言更新、from-assets 容错 4 用例;相关 95 用例全过
148 lines
5.7 KiB
Python
Executable File
148 lines
5.7 KiB
Python
Executable File
from pydantic import BaseModel, Field
|
||
|
||
|
||
class CreateAssetRequest(BaseModel):
|
||
project_id: str | None = Field(default=None, description="可选,不传时从 library.project_id 自动推导")
|
||
library_id: str = Field(..., min_length=1)
|
||
name: str = Field(..., min_length=1, max_length=100)
|
||
storage_key: str = Field(..., min_length=1, max_length=255)
|
||
mime_type: str = Field(..., min_length=1, max_length=100)
|
||
metadata: dict[str, object] = Field(default_factory=dict)
|
||
file_size: int = Field(default=0, ge=0)
|
||
thumbnail_url: str | None = None
|
||
duration: float | None = Field(default=None, ge=0)
|
||
width: int | None = Field(default=None, ge=0)
|
||
height: int | None = Field(default=None, ge=0)
|
||
fps: float | None = Field(default=None, ge=0)
|
||
codec: str | None = None
|
||
status: str = Field(default="uploading")
|
||
classification_status: str = Field(default="pending")
|
||
quality_score: float | None = Field(default=None, ge=0, le=100)
|
||
uploaded_by_user_id: str = Field(default="", max_length=100)
|
||
|
||
|
||
class UpdateAssetReviewRequest(BaseModel):
|
||
review_status: str = Field(..., pattern="^(pending_review|approved|rejected)$")
|
||
|
||
|
||
class UpdateAssetRequest(BaseModel):
|
||
name: str | None = Field(default=None, min_length=1, max_length=100)
|
||
metadata: dict[str, object] | None = None
|
||
tags: list[str] | None = None
|
||
|
||
|
||
class AssetResponse(BaseModel):
|
||
id: str
|
||
project_id: str
|
||
library_id: str
|
||
name: str
|
||
storage_key: str
|
||
mime_type: str
|
||
metadata: dict[str, object]
|
||
file_size: int
|
||
file_url: str | None = None
|
||
thumbnail_url: str | None = None
|
||
duration: float | None = None
|
||
width: int | None = None
|
||
height: int | None = None
|
||
fps: float | None = None
|
||
codec: str | None = None
|
||
status: str
|
||
classification_status: str
|
||
quality_score: float | None = None
|
||
created_at: str
|
||
uploaded_by_user_id: str
|
||
tag_ids: list[str] = Field(default_factory=list)
|
||
# 片段级余量信息(仅视频素材返回,非视频/无时长记录为 None,前端按可用处理)
|
||
used_duration: float | None = Field(default=None, description="已使用片段时长(秒,历史区间合并去重后)")
|
||
available_duration: float | None = Field(default=None, description="剩余可用时长(秒)= 素材总时长 - 已用时长")
|
||
used_ratio: float | None = Field(default=None, description="已用时长占比(0~1)")
|
||
usable: bool = Field(
|
||
default=True,
|
||
description="是否仍可用于新片段:零重复可切区间耗尽且所有历史区间复用次数" "(use_count)均达上限时为 false",
|
||
)
|
||
|
||
|
||
MAX_BATCH_SIZE = 200
|
||
|
||
|
||
class BatchGetRequest(BaseModel):
|
||
"""批量获取素材详情请求。"""
|
||
|
||
ids: list[str] = Field(..., min_length=1, max_length=MAX_BATCH_SIZE, description="素材 ID 列表")
|
||
|
||
|
||
class BatchDeleteRequest(BaseModel):
|
||
"""批量删除请求(软删除)。"""
|
||
|
||
asset_ids: list[str] = Field(..., min_length=1, max_length=MAX_BATCH_SIZE, description="要删除的素材 ID 列表")
|
||
|
||
|
||
class BatchOperationResponse(BaseModel):
|
||
"""批量操作通用响应。"""
|
||
|
||
success_count: int = Field(..., ge=0, description="成功数量")
|
||
failed_ids: list[str] = Field(default_factory=list, description="失败的 ID 列表")
|
||
failed_details: dict[str, str] = Field(default_factory=dict, description="失败详情 {asset_id: reason}")
|
||
|
||
|
||
class BatchTagRequest(BaseModel):
|
||
"""批量打标签请求。"""
|
||
|
||
asset_ids: list[str] = Field(..., min_length=1, max_length=MAX_BATCH_SIZE, description="素材 ID 列表")
|
||
tag_ids: list[str] = Field(..., min_length=1, max_length=50, description="标签 ID 列表")
|
||
mode: str = Field(default="add", pattern="^(add|replace)$", description="add=添加合并,replace=全量替换")
|
||
|
||
|
||
class BatchClassifyRequest(BaseModel):
|
||
"""批量修改分类请求。"""
|
||
|
||
asset_ids: list[str] = Field(..., min_length=1, max_length=MAX_BATCH_SIZE, description="素材 ID 列表")
|
||
category: str = Field(..., min_length=1, max_length=50, description="内容分类,如 person/scenic/product")
|
||
|
||
|
||
class BatchMarkRequest(BaseModel):
|
||
"""批量设置智能视图标记请求。"""
|
||
|
||
asset_ids: list[str] = Field(..., min_length=1, max_length=MAX_BATCH_SIZE, description="素材 ID 列表")
|
||
smart_view: str = Field(
|
||
..., pattern="^(recommended|caution|high_risk)$", description="智能视图标记:recommended/caution/high_risk"
|
||
)
|
||
|
||
|
||
class ListAssetsResponse(BaseModel):
|
||
items: list[AssetResponse]
|
||
total: int = Field(default=0, ge=0)
|
||
skip: int = Field(default=0, ge=0)
|
||
limit: int = Field(default=100, ge=1)
|
||
|
||
|
||
class SmartMatchRequest(BaseModel):
|
||
"""智能选素材请求。"""
|
||
|
||
library_id: str = Field(..., min_length=1, description="素材库 ID")
|
||
limit: int | None = Field(default=None, ge=1, le=200, description="最大返回数量,不传则返回全部匹配素材")
|
||
kind: str | None = Field(
|
||
default=None,
|
||
pattern="^(video|image|audio)$",
|
||
description="按文件类型过滤,不传则返回所有类型",
|
||
)
|
||
|
||
|
||
class SmartMatchItem(AssetResponse):
|
||
"""智能选素材结果条目(扁平结构)。
|
||
|
||
素材字段(id/usable/余量等)直接挂在条目顶层,前端拿到 item 即可读 item.id,
|
||
与 AssetResponse 字段完全一致;score/breakdown 为智能匹配附加的评分字段。
|
||
"""
|
||
|
||
score: float = Field(..., ge=0, le=100, description="综合得分 0-100")
|
||
breakdown: dict[str, float] = Field(default_factory=dict, description="各维度得分明细")
|
||
|
||
|
||
class SmartMatchResponse(BaseModel):
|
||
"""智能选素材响应。"""
|
||
|
||
items: list[SmartMatchItem]
|
||
total_candidates: int = Field(default=0, ge=0, description="参与评分的候选素材总数")
|