Files
xiaoxia 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
fix(assets): smart-match 响应扁平化——item 顶层直出素材字段,修复自动选素材全链路 404/422
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 用例全过
2026-08-31 13:35:05 +08:00

148 lines
5.7 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="参与评分的候选素材总数")