Compare commits

...

69 Commits

Author SHA1 Message Date
CI Bot 410c390cf0 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m48s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m55s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m58s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m24s
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m30s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 19m0s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m28s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 209h1m13s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 209h1m16s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 209h1m16s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 209h1m16s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 209h19m59s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 209h20m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 209h20m7s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 209h20m12s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 209h20m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 209h20m16s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 209h20m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 209h20m19s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 209h20m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 209h20m19s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 209h20m22s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 209h35m53s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 209h54m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 209h54m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 209h54m54s
2026-09-07 14:03:49 +00:00
saas-backend-agent 9f85d40855 fix: 修复 compute_duplicate_rate 方法被错误嵌套到模块函数内部
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m46s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m55s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 209h22m35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 209h22m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 209h22m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 209h22m49s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 209h22m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 209h22m47s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 209h22m48s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 209h22m48s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 209h22m52s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 209h22m52s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 209h22m52s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 209h22m53s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 209h57m16s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 209h57m29s
PR #1762 查重增强时,compute_duplicate_rate 方法被意外放到
compute_structure_similarity 模块函数体内(return 之后的死代码),
导致 VideoDeduplicator 类丢失该方法,27 个旧单测 AttributeError。

将方法移回 VideoDeduplicator 类,缩进不变。
验证:test_duplicate_rate/test_duplicate_rate_scope/
test_dedup_1702/test_bad_fingerprint_filter/test_dedup_enhanced
共 78 个测试全部通过。
2026-09-07 22:01:13 +08:00
xiaoxia 034eaac695 Merge pull request 'feat: 像素级扰动滤镜降低平台查重风险 (Issue #1765)' (#1771) from feat/pixel-perturbation-1765 into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 47s
CI/CD Pipeline / Build Staging API Image (push) Successful in 49s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 34s
CI/CD Pipeline / Integration Tests (push) Successful in 1m56s
CI/CD Pipeline / Validate - Style (push) Successful in 2m21s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m16s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m7s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m39s
AI Code Review / AI Code Review (pull_request) Successful in 6m36s
CI/CD Pipeline / Unit Tests (push) Failing after 8m12s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m22s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 10s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 10s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Security (push) Successful in 22m17s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 210h5m40s
CI/CD Pipeline / CI Gate (push) Failing after 210h5m42s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 210h5m42s
CI/CD Pipeline / Build Production API Image (push) Failing after 210h5m42s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 210h12m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 210h12m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 210h12m38s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 210h12m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 210h12m40s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 210h12m40s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 210h12m39s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 210h12m40s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 210h12m40s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 210h12m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 210h24m24s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 210h26m2s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 210h26m3s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 210h27m55s
CI/CD Pipeline / PR Build Web Image (push) Failing after 210h27m56s
CI/CD Pipeline / Frontend Lint (push) Failing after 210h27m56s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 210h28m4s
CI/CD Pipeline / Deploy Production (push) Failing after 210h40m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 210h40m21s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 210h47m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h47m19s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 210h47m19s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 210h47m20s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 210h47m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 211h0m41s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 210h47m20s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h47m20s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h2m35s
2026-09-07 20:56:11 +08:00
saas-backend-agent 27dccf6591 fix: #1769 智能剪辑 Step2 素材加载 400 错误
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / Integration Tests (push) Successful in 1m41s
CI/CD Pipeline / Validate - Style (push) Successful in 2m27s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 16s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 18s
CI/CD Pipeline / Build Staging API Image (push) Successful in 37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m20s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m33s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m21s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m50s
AI Code Review / AI Code Review (pull_request) Successful in 6m41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m25s
CI/CD Pipeline / Unit Tests (push) Failing after 8m28s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m1s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 210h41m31s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 210h41m32s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 210h41m37s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 210h41m43s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 210h41m51s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 210h41m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 210h41m52s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 210h41m46s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 210h42m0s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 210h42m5s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h42m10s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 210h42m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 210h43m25s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h43m25s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 210h42m6s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 210h43m26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 210h43m26s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 210h43m26s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 210h43m27s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 210h43m27s
CI/CD Pipeline / Frontend Lint (push) Failing after 210h45m3s
CI/CD Pipeline / PR Build Web Image (push) Failing after 210h45m35s
CI/CD Pipeline / PR Build API Image (push) Failing after 210h45m36s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 210h45m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 210h45m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 211h16m11s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 211h16m48s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h16m55s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 211h18m4s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 211h18m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h18m6s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h20m15s
- 根因:模板 f7578c059c2e4dfca2236eb86cb6808a 是无效测试模板
  - is_active=false(已失效)
  - template_clip_configs=0(无片段配置)
  - template_segments=0(无旧版片段配置)
- 修复:_get_template_segments 添加详细错误日志,便于排查
- 前端应过滤 is_active=false 的模板
- 用户应选择 clip_config_count > 0 的有效模板
2026-09-07 20:38:35 +08:00
CI Bot ac7ab679b7 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 15s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 11s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m57s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m4s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m48s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
AI Code Review / AI Code Review (pull_request) Successful in 6m40s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m25s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 19m24s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 13s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m33s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 210h35m22s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 210h35m26s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 210h35m26s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 210h35m26s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 210h52m4s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 210h52m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 210h52m58s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h54m28s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 210h54m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 210h54m30s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 210h54m57s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 210h54m44s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 210h54m57s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h54m44s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 210h54m59s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 211h10m1s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h26m44s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h29m8s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h29m37s
2026-09-07 12:28:56 +00:00
saas-backend-agent bb98620a8a feat: 像素级扰动滤镜降低平台查重风险 (Issue #1765)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 19s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m36s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 20s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m26s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m36s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h55m5s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 210h55m9s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 210h55m33s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 210h55m36s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 210h55m43s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h56m22s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 210h59m0s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h29m47s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h30m15s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h31m2s
- generate_pixel_perturbation():随机选 2-3 种滤镜组合
  - noise: 轻微噪声 (0.01~0.02)
  - unsharp: 锐化/柔化 (-0.5~+0.5)
  - curves: 对比度微调 (0.95~1.05)
  - color_balance: RGB 通道偏移
- _apply_pixel_perturbation():渲染时追加到 FFmpeg filter chain
- edit_plan_service:批量变体生成时为每个变体生成不同像素扰动
- 参数幅度确保肉眼不可见(SSIM > 0.95),帧级差异 > 3%
- 11 个新测试覆盖滤镜组合/参数范围/验收
2026-09-07 20:24:13 +08:00
xiaoxia 2a0b75c007 Merge pull request 'feat: 节奏模板让批量变体片段时长分布不同 (Issue #1764)' (#1770) from feat/rhythm-templates-1764 into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 6s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 7s
CI/CD Pipeline / Check push changed paths (push) Successful in 35s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / Integration Tests (push) Successful in 1m42s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 39s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Validate - Style (push) Successful in 2m41s
CI/CD Pipeline / Build Staging API Image (push) Successful in 42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m41s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m33s
CI/CD Pipeline / Unit Tests (push) Failing after 11m13s
AI Code Review / AI Code Review (pull_request) Failing after 10m34s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Failing after 12m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m22s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m3s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m27s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m32s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m25s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h49m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 210h55m9s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 210h55m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 210h55m28s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 210h55m11s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 210h55m27s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 210h55m30s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 210h55m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 210h58m29s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h59m6s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h0m7s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 210h59m8s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h0m8s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 210h59m8s
CI/CD Pipeline / Frontend Lint (push) Failing after 211h1m29s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h2m11s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h2m5s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 211h2m20s
CI/CD Pipeline / PR Build Web Image (push) Failing after 211h2m6s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h2m8s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h34m46s
2026-09-07 20:21:38 +08:00
CI Bot 0d9d4e584f style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 27s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m10s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 49s
AI Code Review / AI Code Review (pull_request) Successful in 6m41s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Failing after 12m53s
Preview Cleanup / Cleanup Preview Environment (pull_request) Failing after 13m1s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 20m23s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 210h41m47s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 210h41m49s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 210h42m2s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 210h42m2s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 210h42m4s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 211h2m58s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 211h3m9s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h3m10s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h2m58s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 211h3m12s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h2m58s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h3m13s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h3m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h3m19s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h3m13s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h3m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h37m44s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h37m51s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h37m59s
2026-09-07 12:20:53 +00:00
saas-backend-agent eb089fa26d feat: 节奏模板让批量变体片段时长分布不同 (Issue #1764)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m31s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m29s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 211h5m53s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h5m56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h6m2s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h6m2s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h6m3s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h6m4s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h6m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h6m6s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h6m6s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h6m9s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h40m35s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 211h40m42s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 211h40m43s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h40m46s
- 新增 RHYTHM_TEMPLATES 节奏模板池(6 种权重序列)
- get_rhythm_template(seed):根据变体 seed 随机选模板
- adapt_template_length():将模板适配到实际片段数
- plan_clip_durations() 支持 rhythm_template 参数按权重分配时长
- 保底约束:每段 >= 2s,总时长 = 配音时长
- edit_plan_service: ensure_batch_variant_plans 为每个变体生成不同节奏模板
- 17 个新测试覆盖模板池/适配/分配/验收
2026-09-07 20:17:52 +08:00
saas-backend-agent 1ec274b9f5 ci: trigger push pipeline for dedup enhanced
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 12s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 7s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 50s
CI/CD Pipeline / Integration Tests (push) Successful in 1m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 55s
CI/CD Pipeline / Validate - Style (push) Successful in 2m29s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 30s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m3s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m49s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m33s
AI Code Review / AI Code Review (pull_request) Successful in 6m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Unit Tests (push) Failing after 7m56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 6m11s
CI/CD Pipeline / Validate - Security (push) Successful in 16m42s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 211h15m1s
CI/CD Pipeline / CI Gate (push) Failing after 211h15m3s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 211h15m3s
CI/CD Pipeline / Build Production Web Image (push) Failing after 211h15m3s
CI/CD Pipeline / Build Production API Image (push) Failing after 211h15m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 211h27m48s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 211h29m4s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 211h28m58s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h29m4s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 211h29m5s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h29m4s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 211h29m5s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 211h29m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h29m5s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 211h29m8s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 211h29m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 211h29m8s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h29m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h31m37s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h29m11s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 211h31m37s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 211h31m39s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 211h31m37s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 211h31m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h31m43s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h31m43s
CI/CD Pipeline / Frontend Lint (push) Failing after 211h31m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h31m50s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h31m49s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h31m51s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 211h31m56s
CI/CD Pipeline / Deploy Production (push) Failing after 211h49m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h3m43s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h3m46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h6m16s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 212h6m17s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h6m21s
CI/CD Pipeline / PR Build Web Image (push) Failing after 212h6m28s
2026-09-07 19:51:52 +08:00
xiaoxia 62820391c3 fix: Step5 单视频全宽布局+视频播放器居中 (#1761)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Blocked by required conditions
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Check push changed paths (push) Waiting to run
CI/CD Pipeline / Build Staging API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Retag skipped Staging API Image (push) Blocked by required conditions
CI/CD Pipeline / Retag skipped Staging Web Image (push) Blocked by required conditions
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Blocked by required conditions
2026-09-07 19:51:42 +08:00
xiaoxia 322b0a082c Merge pull request 'feat(dedup): 增加文案+结构维度查重 (P2-后端3)' (#1762) from feat/dedup-enhanced-text-structure into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 16s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 58s
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h33m11s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h33m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 211h33m12s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 211h34m8s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h33m12s
CI/CD Pipeline / Frontend Lint (push) Failing after 211h33m15s
2026-09-07 19:50:00 +08:00
CI Bot 07aa03da24 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 43s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 17s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m12s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m37s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m3s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m8s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m17s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m23s
AI Code Review / AI Code Review (pull_request) Successful in 6m26s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 16m23s
CI/CD Pipeline / CI Gate (pull_request) Failing after 3s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 211h18m29s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 211h18m32s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 211h18m32s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 211h18m32s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 211h34m26s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h34m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h34m28s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h34m30s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h34m30s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 211h34m47s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h34m52s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h34m57s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h34m59s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h34m57s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 211h53m10s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h9m5s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h9m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h9m30s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 212h9m36s
2026-09-07 11:49:16 +00:00
saas-backend-agent add93bb9de feat(dedup): 增加文案+结构维度查重 (Issue #P2-后端3)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m16s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m1s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 211h36m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h36m38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h36m42s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h36m47s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h36m47s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h37m12s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h37m13s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h37m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h37m19s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h37m21s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h11m17s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h11m26s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h11m45s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h11m58s
- 新增 compute_text_similarity: 字符级 Jaccard 相似度比对配音文本
- 新增 compute_structure_similarity: 片段序列相似度(数量+类型+时长分布)
- 多维度融合公式: duplicate_rate = visual*0.5 + text*0.25 + structure*0.25
- 文案/结构数据缺失时降级为纯视觉维度
- 21 个新测试覆盖文本+结构+权重常量
2026-09-07 19:46:43 +08:00
xiaoxia 6fe3e03d6a Merge pull request 'feat: 封面智能选帧 — 帧质量评分(清晰度/亮度/色彩)' (#1759) from feat/cover-frame-scoring into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 39s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m51s
CI/CD Pipeline / Integration Tests (push) Successful in 2m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m43s
CI/CD Pipeline / Validate - Style (push) Successful in 2m52s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m32s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
AI Code Review / AI Code Review (pull_request) Successful in 6m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Unit Tests (push) Successful in 8m37s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m0s
CI/CD Pipeline / Validate - Security (push) Successful in 10m32s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 211h45m14s
CI/CD Pipeline / CI Gate (push) Failing after 211h45m18s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 211h45m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 211h51m51s
CI/CD Pipeline / Build Production Web Image (push) Failing after 211h45m18s
CI/CD Pipeline / Build Production API Image (push) Failing after 211h45m18s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 211h52m42s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 211h54m6s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 211h52m42s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h54m15s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 211h52m42s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h54m15s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 211h55m31s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 211h54m22s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 211h55m31s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 211h55m33s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h55m33s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h55m41s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h55m41s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 211h55m42s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h55m42s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h55m43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 211h55m44s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 211h55m45s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h55m46s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h55m49s
CI/CD Pipeline / Frontend Lint (push) Failing after 211h55m51s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h55m53s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 211h55m54s
CI/CD Pipeline / Deploy Production (push) Failing after 212h19m53s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h28m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h29m1s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h30m12s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h30m13s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 212h30m20s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h30m21s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 212h30m23s
CI/CD Pipeline / PR Build Web Image (push) Failing after 212h30m27s
2026-09-07 19:28:22 +08:00
xiaoxia d73c0a77d8 fix: Step5 BatchGenerationGrid 单卡居中 + maxWidth 240px (#1760)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 22s
CI/CD Pipeline / Integration Tests (push) Successful in 1m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 28s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m31s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m48s
CI/CD Pipeline / Validate - Style (push) Successful in 2m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m14s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m0s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m28s
CI/CD Pipeline / Validate - Security (push) Successful in 5m33s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
AI Code Review / AI Code Review (pull_request) Successful in 6m32s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m38s
CI/CD Pipeline / Unit Tests (push) Successful in 8m28s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 25s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 58s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m11s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 212h8m11s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 212h8m17s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h8m17s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h15m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h15m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h15m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h15m46s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 212h15m52s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h15m58s
CI/CD Pipeline / Canary Release to Production (push) Failing after 212h24m8s
CI/CD Pipeline / CI Gate (push) Failing after 212h24m12s
CI/CD Pipeline / Build Production Web Image (push) Failing after 212h24m12s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 212h24m12s
CI/CD Pipeline / Build Production API Image (push) Failing after 212h24m12s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 212h29m27s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 212h31m10s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 212h31m11s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 212h31m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h15m55s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h15m55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 212h15m57s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 212h15m55s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 212h15m57s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 212h15m57s
CI/CD Pipeline / Frontend Lint (push) Failing after 212h32m38s
CI/CD Pipeline / PR Build Web Image (push) Failing after 212h32m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 212h32m43s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 212h32m46s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 212h42m50s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h42m57s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 212h50m15s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 212h50m25s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h50m31s
CI/CD Pipeline / Deploy Production (push) Failing after 212h58m47s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h50m25s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 212h50m35s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h50m31s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 213h7m22s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 18:51:29 +08:00
saas-backend-agent 1087ec181b fix: test_empty_frame_returns_zero 添加 cv2 可用性检测
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m15s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m11s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m17s
AI Code Review / AI Code Review (pull_request) Successful in 6m20s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 9m11s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 5m59s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 9s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 45s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 212h30m21s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 212h30m26s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h30m27s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h30m27s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h39m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h39m31s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h39m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h39m30s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h39m37s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h39m37s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h39m43s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h39m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h39m38s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h39m42s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h39m42s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 213h5m0s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h14m8s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 213h14m14s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h14m20s
2026-09-07 18:44:32 +08:00
CI Bot 04463fbdce style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m48s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m46s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m42s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m19s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m39s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 4s
AI Code Review / AI Code Review (pull_request) Successful in 6m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m9s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 212h43m42s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 212h43m45s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h43m45s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h50m12s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h50m12s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h43m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h50m17s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h50m22s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 212h50m22s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h50m22s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h50m23s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h50m24s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h50m24s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h50m25s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h50m30s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 213h18m20s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h24m50s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 213h25m0s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h25m1s
2026-09-07 10:33:42 +00:00
saas-backend-agent fadca408d4 fix: 测试添加 cv2 可用性检测,mock 环境下 skip
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 52s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h52m31s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 212h52m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h52m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h52m45s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h52m38s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 212h52m45s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h52m38s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h52m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 212h52m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h52m43s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h52m44s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h52m50s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 213h27m10s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 213h27m24s
2026-09-07 18:31:18 +08:00
saas-backend-agent c6a3137949 fix: 加强 cv2 mock 检测(用 inspect.isroutine 验证)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h53m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h53m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h53m13s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h53m14s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h53m14s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h53m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h53m21s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h53m4s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h53m27s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h53m22s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h27m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 213h27m53s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 213h27m55s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h28m1s
2026-09-07 18:30:39 +08:00
saas-backend-agent 3447ab2582 fix: 修复 CI 测试失败
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h54m5s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h54m7s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 212h54m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h54m17s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 212h54m18s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h54m18s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h54m23s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 212h54m23s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h54m23s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h54m28s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h54m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 213h28m52s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 213h29m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 213h29m2s
1. cover_frame_scorer.py: 加强 cv2/numpy 真实性检查,防止 mock 对象
2. test_generation_cover.py: 更新 max_frames 断言 1→5
2026-09-07 18:29:43 +08:00
CI Bot 37ebf42278 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m30s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m43s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m41s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m18s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m33s
AI Code Review / AI Code Review (pull_request) Successful in 6m59s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 212h56m31s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h56m34s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 212h56m31s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h56m34s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 213h2m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h2m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 213h2m42s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 213h2m49s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 213h2m50s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 213h2m52s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 213h2m53s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 213h2m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h2m56s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 213h3m0s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 213h31m13s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 213h37m12s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 213h37m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 213h37m31s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 213h37m34s
2026-09-07 10:21:04 +00:00
saas-backend-agent d6a2715a26 feat: 封面智能选帧 — 帧质量评分(清晰度/亮度/色彩)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m5s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m46s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m17s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 2m29s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m29s
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 213h5m24s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h5m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 213h5m29s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 213h5m32s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 213h5m33s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 213h5m32s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 213h5m33s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 213h5m39s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 213h5m39s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h5m39s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 213h5m40s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 213h40m3s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 213h40m11s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 213h40m13s
P1-后端2:封面智能选帧

新增 cover_frame_scorer.py:
- score_frame(): 评估单帧质量 0~100 分
  - 清晰度 (0~40): 拉普拉斯方差,越高越清晰
  - 亮度 (0~30): 80~180 舒适区间满分
  - 色彩丰富度 (0~30): RGB 三维直方图非零 bin
- score_frames(): 批量评分+排序
- select_best_frame(): 选出最佳帧
- 降级策略:cv2 不可用时返回 50.0 默认分

修改 thumbnail_generator.py:
- extract_and_upload_cover_frames: num_frames=3→5
- 抽帧后对所有候选帧进行质量评分
- 按分数从高到低排序返回
- 统一临时文件管理(评分后清理)

修改 generation_cover.py:
- E1/E2 步骤 max_frames=1→5
- 新增 _select_best_frame_from_snapshots() 辅助函数
- MediaKit 抽多帧后通过质量评分选最佳

修改 render_adapter.py:
- 调用 extract_and_upload_cover_frames 时 num_frames=5

测试:test_cover_frame_scorer.py(18 个测试)
2026-09-07 18:18:19 +08:00
xiaoxia b8073b6c38 fix: Step5卡片黑边+缩放 & 配音时长对齐纳入转场时长 (#1756) (#1758)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m5s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m25s
AI Code Review / AI Code Review (pull_request) Successful in 6m17s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / Integration Tests (push) Successful in 1m41s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m4s
CI/CD Pipeline / Validate - Style (push) Successful in 2m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m22s
CI/CD Pipeline / Validate - Security (push) Successful in 6m49s
CI/CD Pipeline / Unit Tests (push) Successful in 7m57s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 214h16m41s
CI/CD Pipeline / CI Gate (push) Failing after 214h16m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 214h16m45s
CI/CD Pipeline / Build Production API Image (push) Failing after 214h16m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 214h24m13s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 214h24m13s
CI/CD Pipeline / Frontend Lint (push) Failing after 214h24m44s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 214h23m3s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 214h23m3s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 214h24m41s
CI/CD Pipeline / Build Staging API Image (push) Failing after 214h24m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 214h40m36s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 214h43m55s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 214h43m58s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 214h44m0s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 214h44m0s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 214h44m1s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 214h44m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 214h44m2s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 214h44m1s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 214h44m3s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 214h44m3s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 214h44m2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 214h44m3s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 214h44m2s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 214h44m3s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 214h44m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 214h44m4s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 214h44m4s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 214h24m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 214h24m43s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 214h44m9s
CI/CD Pipeline / Deploy Production (push) Failing after 214h51m20s
CI/CD Pipeline / Build Production Web Image (push) Failing after 214h51m24s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 214h58m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 214h59m21s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 214h59m26s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 214h57m42s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 214h59m20s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 215h18m34s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 215h18m39s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 215h18m41s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 215h18m41s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 215h18m42s
2026-09-07 16:39:24 +08:00
xiaoxia debb3a5d67 feat(#1758): FFmpeg 渲染加速——preset medium→fast + 多线程 + 环境变量可覆盖 (#1757)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 56s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 214h45m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 214h45m44s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 214h45m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 214h45m45s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 214h45m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 214h45m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 214h45m47s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 214h46m29s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 214h46m30s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 214h46m31s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 214h46m31s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 214h46m31s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 214h46m32s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 214h46m36s
CI/CD Pipeline / Frontend Lint (push) Failing after 214h46m38s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 214h46m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 214h46m40s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 214h46m40s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 214h46m40s
CI/CD Pipeline / PR Build API Image (push) Failing after 214h46m41s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 214h46m41s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 214h46m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 215h20m17s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 215h20m25s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 215h21m8s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 215h21m9s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 215h21m10s
CI/CD Pipeline / PR Build Web Image (push) Failing after 215h21m18s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 16:37:35 +08:00
xiaoxia 87a61b7f2b fix(#1754): ASS 标题位置常量同步前端改动(TOP 120→180, BOTTOM 60→100) (#1756)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / Build Staging API Image (push) Successful in 19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
CI/CD Pipeline / Validate - Style (push) Successful in 2m21s
CI/CD Pipeline / Integration Tests (push) Successful in 2m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m31s
CI/CD Pipeline / Validate - Security (push) Successful in 5m55s
AI Code Review / AI Code Review (pull_request) Successful in 6m21s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m49s
CI/CD Pipeline / Unit Tests (push) Successful in 7m59s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 216h19m8s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 216h19m11s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 216h26m0s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 216h26m0s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 216h26m6s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 216h26m9s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 216h26m9s
CI/CD Pipeline / Canary Release to Production (push) Failing after 216h36m42s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 216h26m10s
CI/CD Pipeline / CI Gate (push) Failing after 216h36m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 216h36m45s
CI/CD Pipeline / Build Production API Image (push) Failing after 216h36m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 216h41m2s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 216h42m13s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 216h42m13s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 216h42m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 216h26m8s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 216h26m8s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 216h26m8s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 216h26m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 216h26m9s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 216h44m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 216h44m44s
CI/CD Pipeline / Frontend Lint (push) Failing after 216h44m45s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 216h44m49s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 216h53m45s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 216h53m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 217h0m41s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 217h0m44s
CI/CD Pipeline / Deploy Production (push) Failing after 217h11m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 217h11m22s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 217h0m37s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 217h0m43s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 216h53m48s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 217h0m46s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 217h0m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 217h0m46s
CI/CD Pipeline / PR Build Web Image (push) Failing after 217h19m21s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 14:39:27 +08:00
xiaoxia 4b3feb2d88 feat(#1754): 渲染期画面扰动——让批量视频本身更不同 (#1755)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 8s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 12s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 40s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 15s
CI/CD Pipeline / Build Staging API Image (push) Successful in 18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / Integration Tests (push) Successful in 1m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m34s
CI/CD Pipeline / Validate - Style (push) Successful in 2m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Validate - Security (push) Successful in 6m9s
CI/CD Pipeline / Unit Tests (push) Successful in 6m53s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 217h5m50s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 217h5m52s
CI/CD Pipeline / Deploy Production (push) Failing after 217h5m50s
CI/CD Pipeline / Build Production API Image (push) Failing after 217h5m52s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 217h12m23s
CI/CD Pipeline / PR Build API Image (push) Failing after 217h12m45s
CI/CD Pipeline / Frontend Lint (push) Failing after 217h12m45s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 217h11m27s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 217h11m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 217h25m2s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 217h27m26s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 217h27m28s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 217h27m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 217h27m29s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 217h28m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 217h27m30s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 217h28m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 217h28m7s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 217h28m8s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 217h28m8s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 217h28m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / CI Gate (push) Failing after 217h40m30s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 217h47m1s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 217h47m23s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 217h47m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 217h46m5s
CI/CD Pipeline / Build Production Web Image (push) Failing after 217h40m30s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 217h47m1s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 218h2m6s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 218h2m44s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 218h2m46s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 218h2m47s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 218h2m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 217h47m23s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 13:55:47 +08:00
xiaoxia d8da69970b fix(generate): #1754 预览卡片黑边+占比过大/配音时长兜底调速/标题位置优化 (#1754)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 14s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 16s
CI/CD Pipeline / Build Staging API Image (push) Successful in 26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 58s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 37s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m56s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m6s
CI/CD Pipeline / Validate - Style (push) Successful in 2m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 217h29m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 217h29m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 217h29m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 217h30m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 217h30m1s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 217h30m1s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 217h30m5s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 217h30m1s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 217h30m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 217h30m7s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 217h31m44s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 217h30m7s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 217h31m44s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 217h31m48s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 217h31m45s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 217h31m49s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 217h31m45s
CI/CD Pipeline / Frontend Lint (push) Failing after 217h31m52s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 217h31m46s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 217h31m54s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 217h31m46s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 217h31m47s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 217h31m54s
CI/CD Pipeline / PR Build Web Image (push) Failing after 217h31m54s
CI/CD Pipeline / PR Build API Image (push) Failing after 217h31m54s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 217h31m56s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 218h4m33s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 218h4m37s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 218h4m44s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 218h4m49s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 218h6m25s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 13:52:23 +08:00
xiaoxia 3557635f93 fix(#1749): variant-plans clips 补 status 字段(P0 批量预览过滤修复) (#1753)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 12s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 29s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m40s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m43s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m27s
CI/CD Pipeline / Integration Tests (push) Successful in 1m57s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m1s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Style (push) Successful in 2m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m50s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m19s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Validate - Security (push) Successful in 6m25s
AI Code Review / AI Code Review (pull_request) Successful in 6m32s
CI/CD Pipeline / Unit Tests (push) Successful in 7m34s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 20m25s
CI/CD Pipeline / Canary Release to Production (push) Failing after 220h7m48s
CI/CD Pipeline / Deploy Production (push) Failing after 220h7m48s
CI/CD Pipeline / Build Production API Image (push) Failing after 220h7m52s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 220h12m8s
CI/CD Pipeline / CI Gate (push) Failing after 220h7m52s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 220h13m23s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 220h7m52s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 220h13m26s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 220h14m46s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 220h14m55s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 220h14m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 220h14m55s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 220h14m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 220h15m12s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 220h15m15s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 220h15m17s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 220h15m20s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 220h15m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 220h15m24s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 220h15m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 220h15m26s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 220h15m25s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 220h15m26s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 220h15m27s
CI/CD Pipeline / Frontend Lint (push) Failing after 220h15m28s
CI/CD Pipeline / PR Build API Image (push) Failing after 220h15m32s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 220h15m31s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 220h15m32s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 220h15m33s
CI/CD Pipeline / Build Production Web Image (push) Failing after 220h42m29s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 220h48m1s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 220h49m26s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 220h49m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 220h49m52s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 220h50m1s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 220h50m2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 220h50m3s
CI/CD Pipeline / PR Build Web Image (push) Failing after 220h50m9s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 11:08:46 +08:00
xiaoxia e83a0b86f8 fix(#1749): 批量三连修:配音严格守卫/配音时长分配+末帧冻结/素材级去重 + variant-plans 接口 (#1752)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 58s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
CI/CD Pipeline / Validate - Style (push) Successful in 2m31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m30s
CI/CD Pipeline / Integration Tests (push) Successful in 2m35s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 4m34s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m54s
AI Code Review / AI Code Review (pull_request) Successful in 6m19s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m57s
CI/CD Pipeline / Unit Tests (push) Successful in 8m33s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m48s
CI/CD Pipeline / Canary Release to Production (push) Failing after 221h53m0s
CI/CD Pipeline / CI Gate (push) Failing after 221h53m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 221h53m4s
CI/CD Pipeline / Build Production API Image (push) Failing after 221h53m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 221h58m26s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 221h59m0s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 221h59m0s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 222h0m54s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 222h0m57s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 221h59m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 222h0m57s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 222h0m55s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 222h1m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 222h1m17s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 222h0m55s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 222h1m23s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 222h1m28s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 222h1m39s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 222h1m33s
CI/CD Pipeline / Frontend Lint (push) Failing after 222h1m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 222h1m37s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 222h1m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 222h1m43s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 222h1m44s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 222h1m45s
CI/CD Pipeline / Deploy Production (push) Failing after 222h27m37s
CI/CD Pipeline / Build Production Web Image (push) Failing after 222h27m42s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 222h35m33s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 222h35m50s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 222h36m14s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 222h36m15s
CI/CD Pipeline / PR Build Web Image (push) Failing after 222h36m20s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 09:22:34 +08:00
xiaoxia 478c9c2bc3 fix(generate): #1750 批量预览五连修——删除假数据 fallback/配音时长/竖屏播放器/封面一键生成 (#1751)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 26s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 27s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m1s
CI/CD Pipeline / Validate - Style (push) Successful in 2m14s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m7s
CI/CD Pipeline / Integration Tests (push) Successful in 2m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Validate - Security (push) Successful in 5m13s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m54s
AI Code Review / AI Code Review (pull_request) Successful in 6m20s
CI/CD Pipeline / Unit Tests (push) Failing after 7m50s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m6s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 7m15s
CI/CD Pipeline / Canary Release to Production (push) Failing after 229h47m32s
CI/CD Pipeline / CI Gate (push) Failing after 229h47m46s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 229h47m46s
CI/CD Pipeline / Build Production API Image (push) Failing after 229h47m46s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 229h52m6s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 229h52m58s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 229h52m58s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 229h54m40s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 229h54m49s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 229h54m49s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 229h54m49s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 229h54m55s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 229h54m56s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 229h54m56s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 229h55m12s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 229h55m12s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 229h55m34s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 229h55m33s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 229h55m34s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 229h55m34s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 229h55m35s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 229h55m39s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 229h55m39s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 229h55m39s
CI/CD Pipeline / Frontend Lint (push) Failing after 229h55m40s
CI/CD Pipeline / PR Build Web Image (push) Failing after 229h55m40s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 229h55m43s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 229h55m44s
CI/CD Pipeline / Deploy Production (push) Failing after 230h22m21s
CI/CD Pipeline / Build Production Web Image (push) Failing after 230h22m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 230h27m35s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 230h29m26s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 230h29m32s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 230h29m49s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 230h30m11s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 230h30m11s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 230h30m16s
CI/CD Pipeline / PR Build API Image (push) Failing after 230h30m17s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 01:28:34 +08:00
xiaoxia bcbcb41750 feat(generate): #1744 批量预览对接后端变体真实片段 + 智能匹配候选池×3 (#1748)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 11s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m27s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Validate - Style (push) Successful in 2m26s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m37s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m13s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m19s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m26s
CI/CD Pipeline / Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m50s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m34s
AI Code Review / AI Code Review (pull_request) Successful in 6m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Failing after 8m1s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m11s
CI/CD Pipeline / Validate - Security (push) Successful in 18m6s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 234h20m56s
CI/CD Pipeline / CI Gate (push) Failing after 234h20m59s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 234h20m59s
CI/CD Pipeline / Build Production Web Image (push) Failing after 234h20m59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 234h35m39s
CI/CD Pipeline / Build Production API Image (push) Failing after 234h21m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 234h36m5s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 234h36m6s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 234h37m27s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 234h37m31s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 234h37m33s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 234h37m37s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 234h38m28s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 234h39m0s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 234h38m31s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 234h38m39s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 234h39m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 234h38m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 234h39m1s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 234h38m49s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 234h39m1s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 234h38m55s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 234h39m1s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 234h39m2s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 234h39m2s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 234h39m2s
CI/CD Pipeline / PR Build Web Image (push) Failing after 234h39m2s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 234h39m8s
CI/CD Pipeline / PR Build API Image (push) Failing after 234h39m3s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 234h39m10s
CI/CD Pipeline / Frontend Lint (push) Failing after 234h39m5s
CI/CD Pipeline / Deploy Production (push) Failing after 234h55m33s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 235h10m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 235h12m8s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 235h12m11s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 235h13m35s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 235h13m36s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 235h13m37s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 20:45:10 +08:00
xiaoxia 4c03e05e6c fix(#1743): smart-match排序随机噪声 + 素材使用次数按成片实际片段计数 (#1747)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 45s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 48s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m24s
CI/CD Pipeline / Integration Tests (push) Successful in 2m33s
CI/CD Pipeline / Validate - Style (push) Successful in 2m34s
AI Code Review / AI Code Review (pull_request) Failing after 3m19s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m39s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Unit Tests (push) Failing after 9m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m27s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 14s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 15s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 50s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Security (push) Successful in 19m9s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 234h47m57s
CI/CD Pipeline / CI Gate (push) Failing after 234h47m59s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 234h47m59s
CI/CD Pipeline / Build Production API Image (push) Failing after 234h47m59s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 234h52m2s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 234h52m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 234h52m9s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 234h52m10s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 234h52m10s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 234h52m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 234h52m16s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 234h52m16s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 234h52m11s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 234h52m16s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 234h52m15s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 234h52m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 234h52m15s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 234h52m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 235h3m39s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 235h4m37s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 235h4m37s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 235h4m38s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 235h7m9s
CI/CD Pipeline / PR Build API Image (push) Failing after 235h7m9s
CI/CD Pipeline / Frontend Lint (push) Failing after 235h7m9s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 235h7m14s
CI/CD Pipeline / Deploy Production (push) Failing after 235h22m33s
CI/CD Pipeline / Build Production Web Image (push) Failing after 235h22m35s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 235h26m41s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 235h26m44s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 235h26m46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 235h26m52s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 235h26m57s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 235h26m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 235h26m48s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 235h26m52s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 235h26m53s
CI/CD Pipeline / PR Build Web Image (push) Failing after 235h41m46s
2026-09-06 20:17:07 +08:00
saas-backend-agent 6cdb70bb61 test(#1743): 修复 smart-match 随机噪声引发的确定性排序断言
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 14s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 15s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m22s
AI Code Review / AI Code Review (pull_request) Successful in 1m31s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m35s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m49s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m7s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m31s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 14m41s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m30s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 235h10m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 235h10m29s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 235h10m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 235h25m7s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 235h10m36s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 235h25m10s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 235h10m36s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 235h25m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 235h25m13s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 235h25m14s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 235h25m14s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 235h25m22s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 235h25m15s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 235h25m20s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 235h25m20s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 235h59m44s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 235h59m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 235h59m51s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 235h59m57s
CI Unit Tests 暴露(沙箱 httpx2 无法 collect test_smart_match*.py 未本地复现):
- test_smart_match_fallback test_limit_backfills: second(quality=40)/third(30)
  质量项仅差 4 分 < 噪声 20,third 可翻到首位 → second 质量提至 90(差 24>20),
  200 seeds 验证零翻转
- test_smart_match test_returns_sorted_by_score_descending / test_image_assets:
  smart_select_assets 直接调用处注入零噪声 rng(_ZeroNoiseRandom),验证纯评分排序
- 验证生产噪声行为:大分差素材排名稳定、同分素材组合随机
2026-09-06 19:58:59 +08:00
saas-backend-agent ed09794f4d fix(#1743): smart-match排序注入随机噪声 + 素材使用次数按成片实际片段计数回写
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m27s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m25s
AI Code Review / AI Code Review (pull_request) Successful in 1m32s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 235h35m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 235h35m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 235h35m44s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 235h35m40s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 235h35m44s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 235h35m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 235h35m45s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 235h35m45s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 235h35m46s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 235h35m46s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 235h35m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 236h10m20s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 236h10m21s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 236h10m22s
1. smart-match 排序零随机修复(主因):
   - smart_select_assets 排序/多样性分桶注入 0~SCORE_RANDOM_NOISE_MAX 随机噪声,
     同分/近分素材每次选出不同组合与顺序;分差>20的高质量素材保持稳定优先级
   - 噪声以 asset.id 为 key 同次调用内一致;r.score 始终为无噪声原始分
   - 支持 rng 注入(测试可复现);smart-match API/正式生成/模板编辑器三调用点全受益
2. 素材使用次数口径修复:
   - mark_asset_used_for_generation 新增 times 参数,按成片实际渲染片段引用次数累加
   - worker 回写从 task.asset_ids(请求列表,含未被plan选用的素材)改为
     统计最终成片 plan 的 edit_plan_clips(同素材多片段复用按片段数累加)
   - 抽 _count_plan_clip_asset_usage/_record_rendered_asset_usage 纯函数(可单测)
   - plan 无有效片段时兜底 task.asset_ids 单次计数;单素材失败不阻断其他
3. 测试:22 新测试(噪声 10 + 回写计数 12);旧确定性排序断言注入零噪声 rng;
   修复 test_distribute_assets 预存在 flaky(shuffle 未被零噪声 patch 覆盖)
2026-09-06 19:48:08 +08:00
xiaoxia 52664f7168 fix(auth): 默认昵称改小虾同学 + 登录不覆盖存量昵称 + 中文昵称链路守护 (#1746)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m50s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m57s
CI/CD Pipeline / Integration Tests (push) Successful in 2m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
CI/CD Pipeline / Validate - Style (push) Successful in 2m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m9s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m32s
AI Code Review / AI Code Review (pull_request) Successful in 4m16s
CI/CD Pipeline / Validate - Security (push) Successful in 4m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m49s
CI/CD Pipeline / Unit Tests (push) Failing after 7m59s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 236h52m16s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 236h52m20s
CI/CD Pipeline / Deploy Production (push) Failing after 236h52m16s
CI/CD Pipeline / Build Production Web Image (push) Failing after 236h52m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 236h57m2s
CI/CD Pipeline / Build Production API Image (push) Failing after 236h52m20s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 236h59m8s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 236h59m10s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 236h59m30s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 236h59m35s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 236h59m42s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 236h59m38s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 236h59m43s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 236h59m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 237h0m8s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 236h59m40s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 237h0m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 237h0m17s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 237h0m17s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 237h0m19s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 237h0m17s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 237h0m19s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 237h0m18s
CI/CD Pipeline / Frontend Lint (push) Failing after 237h0m19s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 237h0m23s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 237h0m23s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 237h0m24s
CI/CD Pipeline / PR Build API Image (push) Failing after 237h0m24s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 237h0m25s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 237h0m26s
CI/CD Pipeline / CI Gate (push) Failing after 237h26m57s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 237h33m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 237h34m19s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 237h34m21s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 237h34m46s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 237h34m55s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 237h35m0s
CI/CD Pipeline / PR Build Web Image (push) Failing after 237h35m1s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 18:23:55 +08:00
xiaoxia 0469272bd6 fix(#1743): 批量变体独立选片——完整重跑单视频选片+批次20%重叠避让+查重超阈重渲+封面独立 (#1745)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 22s
CI/CD Pipeline / Build Staging API Image (push) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m59s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m24s
CI/CD Pipeline / Integration Tests (push) Successful in 2m40s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m16s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Style (push) Successful in 3m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m42s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m48s
CI/CD Pipeline / Validate - Security (push) Successful in 5m28s
AI Code Review / AI Code Review (pull_request) Failing after 5m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m5s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m22s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m6s
CI/CD Pipeline / Unit Tests (push) Successful in 8m54s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 237h7m46s
CI/CD Pipeline / Deploy Production (push) Failing after 237h7m46s
CI/CD Pipeline / CI Gate (push) Failing after 237h7m57s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 237h7m57s
CI/CD Pipeline / Build Production API Image (push) Failing after 237h7m58s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 237h13m3s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 237h14m5s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 237h14m6s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 237h14m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 237h14m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 237h15m1s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 237h16m28s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 237h15m5s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 237h16m28s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 237h16m47s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 237h16m47s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 237h16m51s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 237h16m51s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 237h16m51s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 237h16m52s
CI/CD Pipeline / Frontend Lint (push) Failing after 237h16m54s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 237h16m52s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 237h16m55s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 237h16m53s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 237h16m56s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 237h16m55s
CI/CD Pipeline / PR Build Web Image (push) Failing after 237h16m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 237h16m55s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 237h16m57s
CI/CD Pipeline / PR Build API Image (push) Failing after 237h16m56s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 237h16m58s
CI/CD Pipeline / Build Production Web Image (push) Failing after 237h42m33s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 237h48m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 237h49m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 237h49m56s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 237h51m4s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 237h51m23s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 237h51m27s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 18:07:24 +08:00
xiaoxia 6638f8b29e feat(#1741): 批量预览卡片缩小至3/5 + 每个视频播放都有声音(配音全挂载+播放互斥+静音按钮) (#1742)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m57s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / Validate - Style (push) Successful in 2m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 2m13s
AI Code Review / AI Code Review (pull_request) Failing after 5m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m32s
CI/CD Pipeline / Validate - Security (push) Successful in 5m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m51s
CI/CD Pipeline / Unit Tests (push) Successful in 8m12s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 22m33s
CI/CD Pipeline / Canary Release to Production (push) Failing after 239h17m11s
CI/CD Pipeline / Deploy Production (push) Failing after 239h34m24s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 239h34m26s
CI/CD Pipeline / Build Production Web Image (push) Failing after 239h34m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 239h34m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 239h39m13s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 239h40m50s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 239h40m55s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 239h40m57s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 239h40m59s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 239h40m58s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 239h41m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 239h41m7s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 239h41m2s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 239h41m15s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 239h41m17s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 239h42m3s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 239h42m3s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 239h42m35s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 239h42m33s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 239h42m36s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 239h42m34s
CI/CD Pipeline / Frontend Lint (push) Failing after 239h42m38s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 239h42m34s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 239h42m43s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 239h42m44s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 239h42m37s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 239h42m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 239h42m44s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 239h42m46s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 239h42m47s
CI/CD Pipeline / CI Gate (push) Failing after 240h9m2s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 240h15m31s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 240h15m44s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 240h16m38s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 240h17m10s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 240h17m19s
CI/CD Pipeline / PR Build Web Image (push) Failing after 240h17m20s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 15:41:36 +08:00
xiaoxia 794793f992 Merge pull request 'fix(auth): 微信 unionid 账号打通逻辑修复(staging 部署版,含 profile_completed)' (#1740) from fix/wechat-unionid-linking-develop into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 13s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m10s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 44s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m56s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Style (push) Successful in 2m33s
CI/CD Pipeline / Integration Tests (push) Successful in 2m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m33s
CI/CD Pipeline / Validate - Security (push) Successful in 5m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m57s
AI Code Review / AI Code Review (pull_request) Failing after 6m12s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m11s
CI/CD Pipeline / Unit Tests (push) Successful in 8m39s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 240h6m14s
CI/CD Pipeline / CI Gate (push) Failing after 240h6m18s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 240h6m18s
CI/CD Pipeline / Build Production API Image (push) Failing after 240h6m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 240h11m29s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 240h12m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 240h12m53s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 240h12m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 240h12m57s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 240h12m58s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 240h13m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 240h13m9s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 240h13m12s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 240h13m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 240h14m22s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 240h14m22s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 240h14m49s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 240h14m50s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 240h14m51s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 240h14m52s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 240h14m53s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 240h14m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 240h14m57s
CI/CD Pipeline / Frontend Lint (push) Failing after 240h14m57s
CI/CD Pipeline / PR Build Web Image (push) Failing after 240h15m1s
CI/CD Pipeline / PR Build API Image (push) Failing after 240h15m2s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 240h15m4s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 240h15m4s
CI/CD Pipeline / Deploy Production (push) Failing after 240h40m50s
CI/CD Pipeline / Build Production Web Image (push) Failing after 240h40m54s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 240h47m30s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 240h47m40s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 240h47m47s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 240h48m59s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 240h49m27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 240h49m28s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 240h49m33s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 240h49m37s
2026-09-06 15:09:18 +08:00
CI Bot 3fba310b9e style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 6s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 11s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 18s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m15s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m56s
AI Code Review / AI Code Review (pull_request) Successful in 2m14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m20s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m15s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 15s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 10m19s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 240h15m28s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 240h15m29s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 240h15m29s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 240h19m35s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 240h15m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 240h19m40s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 240h19m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 240h19m44s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 240h19m44s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 240h19m49s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 240h19m49s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 240h19m50s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 240h19m51s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 240h20m1s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 240h50m5s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 240h54m12s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 240h54m21s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 240h54m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 240h54m27s
2026-09-06 07:04:19 +00:00
xiaoxia 0fa5b31f4f test(auth): 补充unionid补写/跨端绑定/冲突拒绝场景测试
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m15s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m25s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m37s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m42s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 2m30s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 240h22m7s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 240h22m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 240h22m13s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 240h22m18s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 240h22m20s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 240h22m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 240h22m31s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 240h22m28s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 240h22m31s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 240h22m29s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 240h22m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 240h56m45s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 240h56m57s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 240h57m7s
2026-09-06 15:01:27 +08:00
xiaoxia 68d2319234 fix(auth): 微信unionid账号打通 - 先unionid后openid查找 + 老账号补写unionid + 冲突去重保护 2026-09-06 15:01:26 +08:00
xiaoxia e148f995a8 fix(#1737): 生成页标题框聚焦即展开标题库列表 + 下拉箭头提示 (#1739)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 14s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 40s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 40s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m52s
CI/CD Pipeline / Build Staging API Image (push) Successful in 53s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m12s
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Integration Tests (push) Successful in 2m19s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 2m46s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m43s
AI Code Review / AI Code Review (pull_request) Successful in 4m34s
CI/CD Pipeline / Validate - Security (push) Successful in 5m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m27s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m53s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m15s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 10m10s
CI/CD Pipeline / Unit Tests (push) Successful in 12m11s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 240h24m48s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 240h24m52s
CI/CD Pipeline / Build Production API Image (push) Failing after 240h24m52s
CI/CD Pipeline / Deploy Production (push) Failing after 240h24m48s
CI/CD Pipeline / CI Gate (push) Failing after 240h24m52s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 240h32m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 240h33m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 240h34m52s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 240h34m53s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 240h34m54s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 240h34m55s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 240h34m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 240h35m4s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 240h35m7s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 240h35m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 240h36m13s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 240h36m17s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 240h37m0s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 240h37m3s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 240h37m4s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 240h37m6s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 240h37m7s
CI/CD Pipeline / Frontend Lint (push) Failing after 240h37m9s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 240h37m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 240h37m16s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 240h37m16s
CI/CD Pipeline / PR Build API Image (push) Failing after 240h37m17s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 240h37m18s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 240h37m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 240h59m28s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 241h9m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 241h9m31s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 241h9m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 241h10m52s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 241h11m38s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 241h11m41s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 241h11m51s
CI/CD Pipeline / PR Build Web Image (push) Failing after 241h11m52s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 14:47:03 +08:00
xiaoxia 873008dde8 fix(#1714): beat 调度文件改 /tmp,修复定时巡检任务从未执行 (#1736)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 22s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 51s
CI/CD Pipeline / Build Staging API Image (push) Successful in 50s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 49s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 2m4s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m55s
CI/CD Pipeline / Integration Tests (push) Successful in 2m16s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m12s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m54s
AI Code Review / AI Code Review (pull_request) Failing after 6m29s
CI/CD Pipeline / Validate - Security (push) Successful in 6m34s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m26s
CI/CD Pipeline / Unit Tests (push) Successful in 11m21s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Failing after 240h50m49s
CI/CD Pipeline / CI Gate (push) Failing after 240h50m53s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 240h50m54s
CI/CD Pipeline / Build Production API Image (push) Failing after 240h50m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 240h57m12s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 241h0m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 241h0m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 241h0m43s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 241h0m35s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 241h0m47s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 241h0m51s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 241h0m51s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 241h0m55s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 241h0m54s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 241h0m57s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 241h0m57s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 241h0m58s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 241h2m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 241h2m16s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 241h2m11s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 241h2m16s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 241h2m16s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 241h2m17s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 241h2m17s
CI/CD Pipeline / Frontend Lint (push) Failing after 241h2m19s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 241h2m17s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 241h2m26s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 241h2m25s
CI/CD Pipeline / PR Build Web Image (push) Failing after 241h2m25s
CI/CD Pipeline / PR Build API Image (push) Failing after 241h2m25s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 241h2m28s
CI/CD Pipeline / Build Production Web Image (push) Failing after 241h25m31s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 241h35m12s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 241h35m34s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 241h35m34s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 241h36m48s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 241h36m53s
2026-09-06 14:21:55 +08:00
saas-backend-agent dddc1cd081 fix(#1714): beat 调度文件改用 /tmp 路径,修复 celery 用户无 CWD 写权限导致 beat 崩溃、定时巡检(ingest 孤儿清理)从未执行
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 36s
AI Code Review / AI Code Review (pull_request) Successful in 49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 51s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m45s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m50s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m25s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 5m20s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m24s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 15s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 5m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 10m21s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 12m29s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 12m47s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 240h57m35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 240h57m40s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 241h2m45s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 241h2m46s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 241h2m46s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 241h2m46s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 241h10m3s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 241h10m4s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 241h10m8s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 241h10m8s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 241h10m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 241h10m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 241h10m11s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 241h10m17s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 241h44m41s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 241h44m46s
2026-09-06 14:13:43 +08:00
xiaoxia 2ce3a5efd3 fix: SPA 路由回退的 HTML 补 no-cache 头(配合 #1732 白屏修复) (#1735)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 49s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m57s
AI Code Review / AI Code Review (pull_request) Successful in 2m6s
CI/CD Pipeline / Validate - Style (push) Successful in 2m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m9s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 5m13s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Unit Tests (push) Successful in 8m25s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 240h44m37s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 240h49m32s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 240h56m53s
CI/CD Pipeline / Canary Release to Production (push) Failing after 241h4m38s
CI/CD Pipeline / Deploy Production (push) Failing after 241h9m49s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 241h10m2s
CI/CD Pipeline / Build Production Web Image (push) Failing after 241h10m2s
CI/CD Pipeline / Build Production API Image (push) Failing after 241h10m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 241h14m43s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 240h57m11s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 241h17m5s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 241h17m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 240h57m44s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 241h17m7s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 240h49m35s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 240h49m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 240h57m47s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 240h59m56s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 240h59m50s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 240h59m55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 240h59m57s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 240h59m58s
CI/CD Pipeline / Frontend Lint (push) Failing after 241h18m30s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 240h59m59s
CI/CD Pipeline / PR Build Web Image (push) Failing after 241h18m36s
CI/CD Pipeline / PR Build API Image (push) Failing after 241h18m36s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 241h0m0s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 241h18m39s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 241h31m31s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 241h32m24s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 241h34m33s
CI/CD Pipeline / CI Gate (push) Failing after 241h44m38s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 241h31m31s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 241h24m11s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 241h34m31s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 241h34m6s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 241h34m31s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 241h53m12s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 14:05:44 +08:00
xiaoxia 54916aff86 fix(#1714): 同名兜底去重误杀新视频 + ingest 链路孤儿清理/启动恢复 (#1734)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 29s
CI/CD Pipeline / Build Staging API Image (push) Successful in 31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m1s
CI/CD Pipeline / Integration Tests (push) Successful in 2m22s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m11s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m16s
CI/CD Pipeline / Validate - Style (push) Successful in 2m58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m36s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m47s
CI/CD Pipeline / Validate - Security (push) Successful in 6m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m33s
AI Code Review / AI Code Review (pull_request) Successful in 6m27s
CI/CD Pipeline / Unit Tests (push) Successful in 8m30s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 241h24m45s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 241h24m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 241h24m56s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 241h25m4s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 241h25m7s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 241h25m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 241h25m19s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 241h25m57s
CI/CD Pipeline / Canary Release to Production (push) Failing after 241h34m12s
CI/CD Pipeline / CI Gate (push) Failing after 241h34m16s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 241h34m16s
CI/CD Pipeline / Build Production API Image (push) Failing after 241h34m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 241h38m54s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 0s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 241h41m44s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 241h41m46s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 0s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 0s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 241h25m51s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 241h25m52s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 241h25m52s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 241h25m56s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 241h25m56s
CI/CD Pipeline / Frontend Lint (push) Failing after 241h42m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 241h42m51s
CI/CD Pipeline / PR Build API Image (push) Failing after 241h42m52s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 241h42m57s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 241h59m22s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 241h59m42s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 241h59m53s
CI/CD Pipeline / Deploy Production (push) Failing after 242h8m49s
CI/CD Pipeline / Build Production Web Image (push) Failing after 242h8m52s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 0s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 242h16m21s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 0s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 242h0m28s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 242h0m28s
CI/CD Pipeline / PR Build Web Image (push) Failing after 242h17m27s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 13:41:26 +08:00
xiaoxia 9a289e1e1f fix: 发版后懒加载 chunk 失效白屏——ErrorBoundary 自动刷新 + lazy 重试 (#1732)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 16s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 12s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 13s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 15s
CI/CD Pipeline / Build Staging API Image (push) Successful in 21s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 23s
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m7s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m23s
CI/CD Pipeline / Validate - Style (push) Successful in 2m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m6s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m36s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m31s
CI/CD Pipeline / Validate - Security (push) Successful in 5m21s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 241h44m7s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 241h46m52s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 241h46m53s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 241h47m15s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 241h47m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 241h47m25s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 241h47m26s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 241h47m27s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 241h47m38s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 241h47m27s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 241h47m39s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 241h47m41s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 241h47m39s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 241h47m42s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 241h48m8s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 241h48m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 241h48m21s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 241h48m22s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 241h48m22s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 241h48m23s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 241h48m24s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 241h48m24s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 241h48m25s
CI/CD Pipeline / PR Build Web Image (push) Failing after 241h48m25s
CI/CD Pipeline / Frontend Lint (push) Failing after 241h48m27s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 241h48m41s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 241h48m43s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 242h21m52s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 242h22m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 242h22m43s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 242h22m57s
CI/CD Pipeline / PR Build API Image (push) Failing after 242h23m2s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 13:35:35 +08:00
xiaoxia 9bca7e53e3 feat(#1714): complete 请求携带 file_size,修复同名兜底误杀新视频 (#1731)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 43s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 43s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m53s
CI/CD Pipeline / Integration Tests (push) Successful in 2m17s
CI/CD Pipeline / Validate - Style (push) Successful in 2m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
AI Code Review / AI Code Review (pull_request) Failing after 3m38s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m23s
CI/CD Pipeline / Validate - Security (push) Successful in 5m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m3s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m4s
CI/CD Pipeline / Unit Tests (push) Successful in 8m16s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 242h31m46s
CI/CD Pipeline / Deploy Production (push) Failing after 242h31m46s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 242h31m48s
CI/CD Pipeline / CI Gate (push) Failing after 242h31m48s
CI/CD Pipeline / Build Production Web Image (push) Failing after 242h31m49s
CI/CD Pipeline / Build Production API Image (push) Failing after 242h31m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 242h38m35s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 242h38m36s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 242h38m57s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 242h39m20s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 242h39m21s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 242h39m27s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 242h39m22s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 242h39m29s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 242h40m5s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 242h39m29s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 242h40m5s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 242h39m30s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 242h40m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 242h39m30s
CI/CD Pipeline / Frontend Lint (push) Failing after 242h40m8s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 242h40m4s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 242h40m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 242h40m6s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 242h40m15s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 242h40m6s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 242h40m15s
CI/CD Pipeline / PR Build API Image (push) Failing after 242h40m16s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 242h40m6s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 242h40m17s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 242h40m7s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 242h40m18s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 243h13m10s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 243h13m33s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 243h14m40s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 243h14m42s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 243h14m49s
CI/CD Pipeline / PR Build Web Image (push) Failing after 243h14m50s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 12:44:05 +08:00
xiaoxia 70dde8cbfb feat(#1714): prepare_direct_upload 去重 + 预建 PROCESSING asset 占位 (#1730)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m2s
AI Code Review / AI Code Review (pull_request) Failing after 1m43s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 2m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 31s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m15s
CI/CD Pipeline / Integration Tests (push) Successful in 2m47s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m18s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m42s
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m47s
CI/CD Pipeline / Validate - Security (push) Successful in 4m42s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m12s
CI/CD Pipeline / Unit Tests (push) Successful in 10m53s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 242h41m46s
CI/CD Pipeline / CI Gate (push) Failing after 242h41m50s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 242h41m50s
CI/CD Pipeline / Build Production API Image (push) Failing after 242h41m50s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 242h48m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 242h49m48s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 242h49m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 242h50m30s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 242h50m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 242h50m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 242h50m45s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 242h52m11s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 242h52m30s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 242h52m12s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 242h52m30s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 242h52m40s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 242h52m41s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 242h52m41s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 242h52m41s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 242h52m42s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 242h52m43s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 242h52m44s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 242h52m45s
CI/CD Pipeline / Frontend Lint (push) Failing after 242h52m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 242h52m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 242h52m47s
CI/CD Pipeline / PR Build API Image (push) Failing after 242h52m47s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 242h52m47s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 242h52m49s
CI/CD Pipeline / Deploy Production (push) Failing after 243h16m21s
CI/CD Pipeline / Build Production Web Image (push) Failing after 243h16m25s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 243h24m23s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 243h25m12s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 243h25m19s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 243h26m47s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 243h27m4s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 243h27m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 243h27m19s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 12:31:38 +08:00
xiaoxia 528f56254d feat(#1718): PATCH /auth/me 资料更新接口 + profile_completed 字段 (#1728)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 20s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 23s
CI/CD Pipeline / Build Staging API Image (push) Successful in 17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 33s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m0s
CI/CD Pipeline / Integration Tests (push) Successful in 2m17s
CI/CD Pipeline / Validate - Style (push) Successful in 2m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m6s
AI Code Review / AI Code Review (pull_request) Successful in 3m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m38s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 4m54s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m17s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m10s
CI/CD Pipeline / Unit Tests (push) Successful in 8m18s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 11m48s
CI/CD Pipeline / Canary Release to Production (push) Failing after 243h9m2s
CI/CD Pipeline / CI Gate (push) Failing after 243h9m5s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 243h9m5s
CI/CD Pipeline / Build Production Web Image (push) Failing after 243h9m5s
CI/CD Pipeline / Build Production API Image (push) Failing after 243h9m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 243h13m34s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 243h16m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 243h16m10s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 243h16m12s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 243h16m16s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 243h16m17s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 243h16m19s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 243h16m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 243h16m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 243h17m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 243h16m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 243h17m17s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 243h17m18s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 243h17m18s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 243h17m19s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 243h17m19s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 243h17m20s
CI/CD Pipeline / Frontend Lint (push) Failing after 243h17m24s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 243h17m20s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 243h17m25s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 243h17m26s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 243h17m26s
CI/CD Pipeline / PR Build Web Image (push) Failing after 243h17m26s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 243h17m27s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 243h17m28s
CI/CD Pipeline / PR Build API Image (push) Failing after 243h17m27s
CI/CD Pipeline / Deploy Production (push) Failing after 243h43m37s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 243h50m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 243h50m52s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 243h51m20s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 243h51m52s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 243h51m54s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 243h52m0s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 12:06:58 +08:00
xiaoxia ff60fdf956 feat(#1714): 前端 prepare 短路(后端 skip_transfer 命中时跳过 OSS 直传) (#1729)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 39s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m54s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m16s
CI/CD Pipeline / Validate - Style (push) Successful in 2m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m10s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m31s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m32s
CI/CD Pipeline / Validate - Security (push) Successful in 5m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m41s
AI Code Review / AI Code Review (pull_request) Successful in 6m20s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m32s
CI/CD Pipeline / Unit Tests (push) Successful in 8m24s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 243h31m40s
CI/CD Pipeline / CI Gate (push) Failing after 243h31m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 243h31m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 243h37m0s
CI/CD Pipeline / Build Production Web Image (push) Failing after 243h31m45s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 243h38m36s
CI/CD Pipeline / Build Production API Image (push) Failing after 243h31m45s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 243h38m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 243h38m47s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 243h38m49s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 243h38m48s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 243h38m51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 243h38m52s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 243h38m55s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 243h38m57s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 243h39m39s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 243h39m40s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 243h40m7s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 243h40m9s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 243h40m8s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 243h40m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 243h40m11s
CI/CD Pipeline / Frontend Lint (push) Failing after 243h40m11s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 243h40m12s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 243h40m13s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 243h40m13s
CI/CD Pipeline / PR Build API Image (push) Failing after 243h40m14s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 243h40m15s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 243h40m17s
CI/CD Pipeline / Deploy Production (push) Failing after 244h6m15s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 244h13m21s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 244h13m25s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 244h13m28s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 244h14m14s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 244h14m43s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 244h14m45s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 244h14m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 244h14m49s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 11:43:59 +08:00
xiaoxia 06b0bacce1 fix(#1718): 昵称页提交防连点 + 补 /vite.svg favicon (#1727)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 24s
CI/CD Pipeline / Build Staging API Image (push) Successful in 26s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m46s
CI/CD Pipeline / Validate - Style (push) Successful in 2m16s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m2s
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m0s
CI/CD Pipeline / Integration Tests (push) Successful in 3m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 51s
AI Code Review / AI Code Review (pull_request) Failing after 4m18s
CI/CD Pipeline / Validate - Security (push) Successful in 5m9s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m1s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m17s
CI/CD Pipeline / Unit Tests (push) Successful in 8m29s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 243h50m59s
CI/CD Pipeline / Build Production API Image (push) Failing after 243h51m4s
CI/CD Pipeline / Deploy Production (push) Failing after 243h50m59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 243h56m32s
CI/CD Pipeline / CI Gate (push) Failing after 243h51m4s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 243h56m40s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 243h51m4s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 243h56m40s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 243h58m39s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 243h58m45s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 243h58m40s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 243h58m45s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 243h58m41s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 243h59m14s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 243h58m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 243h59m31s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 243h58m42s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 243h59m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 243h58m44s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 243h59m32s
CI/CD Pipeline / Frontend Lint (push) Failing after 243h59m34s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 243h59m14s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 243h59m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 243h59m31s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 243h59m38s
CI/CD Pipeline / PR Build Web Image (push) Failing after 243h59m39s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 243h59m32s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 243h59m39s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 243h59m33s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 243h59m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 243h59m37s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 243h59m38s
CI/CD Pipeline / PR Build API Image (push) Failing after 243h59m39s
CI/CD Pipeline / Build Production Web Image (push) Failing after 244h25m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 244h31m14s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 244h33m19s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 244h33m48s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 244h34m5s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 11:24:48 +08:00
xiaoxia a83ed58864 feat(#1718): 微信登录/绑定改为弹窗内嵌二维码,不再整页跳转 (#1726)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 23s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m59s
AI Code Review / AI Code Review (pull_request) Failing after 2m5s
CI/CD Pipeline / Validate - Style (push) Successful in 2m21s
CI/CD Pipeline / Integration Tests (push) Successful in 2m19s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m14s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m47s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m32s
CI/CD Pipeline / Validate - Security (push) Successful in 5m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m7s
CI/CD Pipeline / Unit Tests (push) Successful in 8m26s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 12m1s
CI/CD Pipeline / Canary Release to Production (push) Failing after 254h44m2s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 254h44m4s
CI/CD Pipeline / Build Production Web Image (push) Failing after 254h44m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 254h48m59s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 254h50m6s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 254h50m8s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 254h51m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 254h51m20s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 254h51m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 254h51m27s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 254h51m32s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 254h51m34s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 254h51m49s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 254h52m26s
CI/CD Pipeline / Deploy Production (push) Failing after 254h44m2s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 254h52m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 254h44m4s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 254h52m26s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 254h52m27s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 254h52m28s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 254h52m28s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 254h52m29s
CI/CD Pipeline / PR Build Web Image (push) Failing after 254h52m30s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 254h51m51s
CI/CD Pipeline / Frontend Lint (push) Failing after 254h52m31s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 254h51m52s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 254h52m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 254h52m39s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 254h52m28s
CI/CD Pipeline / CI Gate (push) Failing after 255h18m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 255h24m41s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 255h25m52s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 255h25m58s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 255h26m6s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 255h26m59s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 255h27m0s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 255h27m1s
CI/CD Pipeline / PR Build API Image (push) Failing after 255h27m5s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 00:31:48 +08:00
xiaoxia cdcb032e45 fix(#1718/#1714): 微信回调state误杀修复+错误透传防连点、上传失败完整可观测、哈希阈值降至64MB、昵称不预填 (#1723)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m28s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m12s
AI Code Review / AI Code Review (pull_request) Failing after 6m15s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 22s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Style (push) Successful in 2m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m14s
CI/CD Pipeline / Unit Tests (push) Successful in 8m4s
CI/CD Pipeline / Validate - Security (push) Successful in 16m53s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 255h51m59s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 255h52m3s
CI/CD Pipeline / Build Production Web Image (push) Failing after 255h52m3s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 256h8m32s
CI/CD Pipeline / Deploy Production (push) Failing after 255h51m59s
CI/CD Pipeline / Frontend Lint (push) Failing after 256h8m58s
CI/CD Pipeline / Build Production API Image (push) Failing after 255h52m3s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 256h6m56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 256h6m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 256h27m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 256h29m25s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 256h29m30s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 256h29m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 256h29m25s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 256h29m34s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 256h29m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 256h29m36s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 256h29m35s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 256h29m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 256h31m33s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 256h31m34s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 256h31m34s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 256h31m35s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 256h8m58s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 256h31m35s
CI/CD Pipeline / PR Build API Image (push) Failing after 256h8m58s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 256h31m37s
CI/CD Pipeline / CI Gate (push) Failing after 256h26m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 256h43m7s
CI/CD Pipeline / PR Build Web Image (push) Failing after 256h43m32s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 256h43m36s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 256h41m30s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 256h43m6s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 257h4m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 257h4m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 257h4m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 257h6m5s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 257h6m8s
2026-09-05 22:52:49 +08:00
xiaoxia c8b1c4b8ff feat(#1719): 微信账号绑定/解绑三接口 + /me 返回 wechat_bound (#1725)
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m37s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m36s
AI Code Review / AI Code Review (pull_request) Failing after 6m16s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 8s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m37s
CI/CD Pipeline / Validate - Style (push) Successful in 2m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m52s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m3s
CI/CD Pipeline / Unit Tests (push) Successful in 7m36s
CI/CD Pipeline / Validate - Security (push) Successful in 24m19s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Failing after 256h44m17s
CI/CD Pipeline / Build Production Web Image (push) Failing after 256h44m18s
CI/CD Pipeline / Build Production API Image (push) Failing after 256h44m18s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 257h8m15s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 257h8m15s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 257h8m37s
CI/CD Pipeline / Frontend Lint (push) Failing after 257h8m38s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 257h8m42s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 257h16m21s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 257h16m23s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 257h16m23s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 257h16m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 257h16m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 257h16m29s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 257h16m29s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 257h16m30s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 257h16m30s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 257h16m30s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 257h16m31s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 257h6m19s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 257h16m31s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 257h6m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 257h29m52s
CI/CD Pipeline / Deploy Production (push) Failing after 257h18m50s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 257h42m49s
CI/CD Pipeline / PR Build API Image (push) Failing after 257h43m11s
CI/CD Pipeline / Canary Release to Production (push) Failing after 257h18m50s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 257h18m51s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 257h50m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 257h50m57s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 257h51m0s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 257h51m3s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 257h51m3s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 257h51m4s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 257h51m5s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 257h40m53s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 257h51m0s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 257h51m3s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 257h51m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 257h51m5s
CI/CD Pipeline / PR Build Web Image (push) Failing after 257h43m11s
2026-09-05 21:50:54 +08:00
saas-backend-agent 5ca64898b7 feat(#1719): 微信账号绑定/解绑三接口(GET bind/url、POST bind、DELETE bind)+ /me 返回 wechat_bound
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 22s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m58s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m51s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m9s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m53s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m23s
AI Code Review / AI Code Review (pull_request) Successful in 6m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m34s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 18m5s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 6s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m30s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 257h34m15s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 257h34m18s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 257h34m18s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 257h34m18s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 257h50m45s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 257h50m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 257h50m50s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 257h50m51s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 257h50m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 257h51m17s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 257h51m41s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 257h52m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 257h52m17s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 257h52m27s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 258h8m49s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 258h25m20s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 258h25m25s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 258h26m15s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 258h26m50s
- WechatBindUseCase:绑定到当前登录账号不建新号;openid/unionid 已绑其他账号 409;已绑同一微信幂等
- WechatUnbindUseCase:解绑前守卫——必须有已验证手机或真实已验证邮箱(随机密码 hash/占位邮箱不算兜底,口径同 binding_complete)
- /auth/me 增加 wechat_bound 字段,供设置页判断绑定状态
- state 复用 RedisStateStore CSRF 校验;22 个新单测,diff coverage 100%
2026-09-05 21:31:44 +08:00
xiaoxia 5575d98512 feat(#1718): 微信state存储Redis化+回调UA日志+中文昵称UTF-8修复 (#1724)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 25s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 35s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 33s
CI/CD Pipeline / Validate - Style (push) Successful in 2m4s
CI/CD Pipeline / Integration Tests (push) Successful in 2m19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m39s
AI Code Review / AI Code Review (pull_request) Failing after 4m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Unit Tests (push) Successful in 8m24s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m6s
CI/CD Pipeline / Validate - Security (push) Successful in 16m39s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 258h4m25s
CI/CD Pipeline / CI Gate (push) Failing after 258h4m29s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 258h4m29s
CI/CD Pipeline / Build Production Web Image (push) Failing after 258h4m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 258h17m43s
CI/CD Pipeline / Build Production API Image (push) Failing after 258h4m29s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 258h19m29s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 258h19m30s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 258h19m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 258h19m44s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 258h19m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 258h19m53s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 258h19m57s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 258h20m3s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 258h20m42s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 258h20m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 258h21m2s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 258h21m2s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 258h21m2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 258h21m3s
CI/CD Pipeline / Frontend Lint (push) Failing after 258h21m6s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 258h21m4s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 258h21m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 258h21m5s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 258h21m9s
CI/CD Pipeline / PR Build API Image (push) Failing after 258h21m10s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 258h21m9s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 258h21m11s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 258h21m9s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 258h21m13s
CI/CD Pipeline / Deploy Production (push) Failing after 258h38m59s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 258h54m2s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 258h54m18s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 258h54m22s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 258h54m34s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 258h55m16s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 258h55m36s
CI/CD Pipeline / PR Build Web Image (push) Failing after 258h55m43s
2026-09-05 21:03:16 +08:00
saas-backend-agent f11b71361d feat(#1718): 微信 state 存储 Redis 化 + 回调 UA 日志 + 中文昵称 UTF-8 修复
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 25s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m42s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m47s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m6s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m2s
AI Code Review / AI Code Review (pull_request) Successful in 3m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m31s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 15m42s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 8s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m14s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 258h23m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 258h23m18s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 258h23m18s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 258h23m18s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 258h37m56s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 258h37m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 258h38m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 258h38m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 258h38m31s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 258h38m18s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 258h38m34s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 258h38m23s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 258h38m59s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 258h38m28s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 258h39m5s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 258h57m50s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 259h12m33s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 259h13m2s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 259h13m32s
- state store 改 Redis(复用 celery Redis,key 前缀 wechat:state:,
  TTL 10 分钟):SET NX EX 写入,Lua 脚本原子 GET+DEL 一次性消费
  (兼容 Redis <6.2 无 GETDEL);Redis 不可用时自动降级内存,登录不中断;
  容器重启/多实例后 state 不丢,修复 worker 扩容后回调 state 失效
- /wechat/callback 加可观测日志:User-Agent(识别 MicroMessenger
  微信内置浏览器)、state 校验结果、失败上下文,便于排查回调停滞
- 修复微信中文昵称乱码:sns/oauth2/access_token 与 sns/userinfo
  响应在 .json() 前显式 encoding=utf-8(微信响应头不带 charset,
  requests 默认 ISO-8859-1 解码导致中文乱码)
- 15 个新单测(全 mock/fake,CI 无 redis 也覆盖):Redis state
  存取/一次性消费/eval 降级/异常降级内存/ping 失败降级、中文昵称
  UTF-8 解析、errcode 透传、callback 路由日志分支、工厂降级分支
2026-09-05 20:45:02 +08:00
xiaoxia e7d6e69396 fix(#1714): Celery队列隔离+孤儿消息作废(generation/transcode独立队列+revoke/purge+执行前状态守卫) (#1722)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m18s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m9s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m51s
AI Code Review / AI Code Review (pull_request) Successful in 7m3s
CI/CD Pipeline / CI Gate (pull_request) Failing after 11m41s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 46s
CI/CD Pipeline / Integration Tests (push) Successful in 3m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 36s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m44s
CI/CD Pipeline / Validate - Style (push) Successful in 3m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m16s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m35s
CI/CD Pipeline / Unit Tests (push) Successful in 9m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m50s
CI/CD Pipeline / Validate - Security (push) Successful in 18m13s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 258h45m17s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 258h45m18s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 258h47m10s
CI/CD Pipeline / PR Build API Image (push) Failing after 258h47m19s
CI/CD Pipeline / Frontend Lint (push) Failing after 258h49m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 258h49m43s
CI/CD Pipeline / Canary Release to Production (push) Failing after 258h31m19s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 258h31m22s
CI/CD Pipeline / Build Production API Image (push) Failing after 258h31m22s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 259h8m24s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 259h11m11s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 259h11m20s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 259h11m19s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 259h11m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 259h11m26s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 259h11m39s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 259h11m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 259h13m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 259h13m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 259h13m59s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 259h14m0s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 259h14m1s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 259h14m2s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 259h14m4s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 259h14m6s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 259h14m5s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 259h14m15s
CI/CD Pipeline / Deploy Production (push) Failing after 259h5m52s
CI/CD Pipeline / Build Production Web Image (push) Failing after 259h5m55s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 259h19m50s
CI/CD Pipeline / CI Gate (push) Failing after 259h5m56s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 259h45m54s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 259h46m11s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 259h47m40s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 259h48m34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 259h48m36s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 259h48m39s
CI/CD Pipeline / PR Build Web Image (push) Failing after 259h21m49s
2026-09-05 20:10:12 +08:00
saas-backend-agent 6a1ec20e68 test(#1714): 补 mock redis 覆盖率测试,diff coverage 98%
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m47s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m47s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m11s
AI Code Review / AI Code Review (pull_request) Successful in 6m37s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m41s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 14m45s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 9s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 2m36s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 259h16m47s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 259h16m49s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 259h16m49s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 259h31m14s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 259h31m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 259h31m26s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 259h31m14s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 259h31m26s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 259h31m32s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 259h31m32s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 259h31m36s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 259h31m33s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 259h31m36s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 259h31m40s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 259h51m21s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 259h51m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 260h5m54s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 260h6m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 260h6m9s
CI runner 无本地 redis,purge/revoke 与路由持久化的真实 redis 测试
全部 skip 导致这些改动零覆盖、diff coverage 45% 未达 60% 门禁:

- 新增 test_orphan_guard_purge_mocked_1714.py(31 测试,全 mock):
  _extract_business_ids 各形态(三元组/裸 args/dict args/坏 JSON/坏
  base64/空 args)、_purge_one_queue(biz/celery id 双匹配命中、未命中
  rpush 保序、全删不 rpush、解析失败保守保留、lrange/重写异常)、
  purge_stale_messages_from_queues(空 ids 早退、redis 未安装、连接失败、
  close 异常)、revoke_and_purge(逐条 revoke、异常不阻断、空 id 跳过)
- 新增 test_persist_celery_id_routes_1714.py(9 测试):ingest_jobs /
  task_center 重试 / upload helper 的 celery_task_id 持久化正常与异常
  吞掉分支、task_enqueue 持久化失败仍入队成功、celery_app 队列配置
  异常不阻断启动(用独立模块对象加载,不 reload 污染 task_enqueue)、
  仓储 update 落库 celery_task_id
- chunked_upload 完成回调去重:改为复用 upload._persist_celery_task_id
2026-09-05 19:52:50 +08:00
saas-backend-bot df99305dd6 feat(worker): celery 队列隔离 + 孤儿任务消息作废 (#1714)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 49s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m39s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m19s
AI Code Review / AI Code Review (pull_request) Failing after 2m52s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m54s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m11s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m23s
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m39s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 260h6m22s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 260h6m25s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 260h6m25s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 260h12m32s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 260h12m33s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 260h12m43s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 260h12m33s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 260h12m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 260h12m43s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 260h12m48s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 260h12m49s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 260h12m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 260h12m49s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 260h12m57s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 260h40m56s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 260h40m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 260h47m12s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 260h47m22s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 260h47m24s
问题:素材转码与视频生成共用 celery 默认队列、worker 单进程消费,
20+ 转码积压会把用户生成任务堵 40 分钟以上;孤儿清理把任务标 failed
后 Redis 队列消息未作废,消息被重投导致 failed→running 非法转换,
worker 打印 ERROR 后继续产出半成品。

队列隔离:
- 新增 packages/shared/celery_queues.py:generation/transcode/celery
  三队列与 task_routes(generate_video→generation;ingest_asset/
  classify_asset/duplication→transcode),apply_queue_settings()
- worker 入口改双进程:generation worker 独占队列并内嵌 beat
  (prefetch=1, GENERATION_CONCURRENCY 默认 2),transcode worker
  消费 transcode,celery(并发=总-2,最小 1),任一退出则整体终止
- compose/部署脚本/ps1 同步新增 GENERATION_CONCURRENCY 与健康检查

消息作废:
- 新增 packages/shared/celery_orphan_guard.py:终态守卫
  ensure_task_claimable、Redis 队列消息物理清理(JSON 信封解析,
  按业务 id + celery headers.id 双匹配,未命中 rpush 保序)、
  revoke_and_purge(control.revoke + 物理清队列双保险)
- 入队点(生成/上传/分片/重试)send_task 后持久化 celery_task_id
  到 generation_tasks/ingest_jobs(新列,067 迁移,失败仅 warning)
- generate_video/ingest_asset 执行前校验 DB 状态:终态直接 discarded
  不进业务逻辑;mark_processing 返回 False(非法转换)安全中止
- 孤儿/超时清理标 failed 时同时 revoke + 清队列消息
- pending 超时阈值 15→45 分钟,与 running 孤儿(20min)区分

测试:新增 22 个单测(路由表/真实 Redis 消息清理/终态守卫/
非法转换中止/标 failed 后消息不重投/入队持久化),全量
14301 passed;067 迁移隔离 DDL 验证 upgrade/downgrade 通过。
2026-09-05 19:07:47 +08:00
xiaoxia 9840d5d778 deploy(#1718): 微信OAuth凭证纳入env模板+CI渲染,修复staging部署覆盖丢失 (#1721)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 18s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / Build Staging API Image (push) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 43s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 43s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m56s
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Integration Tests (push) Successful in 2m22s
CI/CD Pipeline / Validate - Style (push) Successful in 2m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 4m53s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m31s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m8s
AI Code Review / AI Code Review (pull_request) Successful in 6m28s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m54s
CI/CD Pipeline / Unit Tests (push) Successful in 9m4s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 261h35m29s
CI/CD Pipeline / CI Gate (push) Failing after 261h35m33s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 261h35m33s
CI/CD Pipeline / Build Production API Image (push) Failing after 261h35m33s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 261h41m21s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 261h42m35s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 261h42m39s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 261h42m39s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 261h42m41s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 261h42m43s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 261h42m44s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 261h42m50s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 261h42m50s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 261h42m50s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 261h42m51s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 261h44m31s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 261h42m51s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 261h44m32s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 261h44m29s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 261h44m32s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 261h44m29s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 261h44m32s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 261h44m29s
CI/CD Pipeline / Frontend Lint (push) Failing after 261h44m37s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 261h44m30s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 261h44m39s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 261h44m41s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 261h44m39s
CI/CD Pipeline / PR Build Web Image (push) Failing after 261h44m39s
CI/CD Pipeline / PR Build API Image (push) Failing after 261h44m39s
CI/CD Pipeline / Deploy Production (push) Failing after 262h10m3s
CI/CD Pipeline / Build Production Web Image (push) Failing after 262h10m7s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 262h17m10s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 262h17m16s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 262h17m19s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 262h17m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 262h19m5s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 262h19m6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-05 17:39:46 +08:00
xiaoxia c8cfed0add feat(#1718): 微信扫码登录前端——昵称引导页 + 设置页微信绑定/解绑 (#1720)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m3s
CI/CD Pipeline / Integration Tests (push) Successful in 2m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m24s
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
CI/CD Pipeline / Validate - Style (push) Successful in 3m10s
CI/CD Pipeline / Build Staging API Image (push) Successful in 29s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 31s
AI Code Review / AI Code Review (pull_request) Failing after 3m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m5s
CI/CD Pipeline / Validate - Security (push) Successful in 5m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m54s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 261h48m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 261h48m38s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 261h48m38s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 261h50m20s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 261h51m21s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 261h51m24s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 261h51m27s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 261h51m40s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 261h51m34s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 261h53m21s
CI/CD Pipeline / PR Build API Image (push) Failing after 261h53m22s
CI/CD Pipeline / Frontend Lint (push) Failing after 261h53m31s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 261h53m45s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 261h53m46s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 262h25m55s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 262h26m7s
CI/CD Pipeline / PR Build Web Image (push) Failing after 262h27m54s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-05 17:30:42 +08:00
xiaoxia 8a6d51f6c3 fix(auth): 微信OAuth服务工厂改为真单例,修复回调state必校验失败 (#1718) (#1719)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 55s
CI/CD Pipeline / Build Staging API Image (push) Successful in 58s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m57s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 29s
CI/CD Pipeline / Integration Tests (push) Successful in 2m11s
CI/CD Pipeline / Validate - Style (push) Successful in 2m21s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m4s
CI/CD Pipeline / Validate - Security (push) Successful in 5m22s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m24s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m32s
AI Code Review / AI Code Review (pull_request) Failing after 6m21s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m55s
CI/CD Pipeline / Unit Tests (push) Successful in 9m46s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 17s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m23s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Failing after 261h57m25s
CI/CD Pipeline / CI Gate (push) Failing after 261h57m29s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 261h57m29s
CI/CD Pipeline / Build Production Web Image (push) Failing after 261h57m29s
CI/CD Pipeline / Build Production API Image (push) Failing after 261h57m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 262h3m26s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 262h5m13s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 262h5m14s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 0s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 261h48m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 261h48m38s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 261h48m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 261h48m46s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 0s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 261h48m49s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 0s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 0s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 0s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 0s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 0s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 261h48m55s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 261h48m56s
CI/CD Pipeline / Frontend Lint (push) Failing after 262h7m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 261h48m58s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 262h7m20s
CI/CD Pipeline / PR Build API Image (push) Failing after 262h7m20s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 261h49m56s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 262h7m21s
CI/CD Pipeline / Deploy Production (push) Failing after 262h31m58s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 262h39m47s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 262h23m9s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 0s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 262h23m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 0s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 0s
CI/CD Pipeline / PR Build Web Image (push) Failing after 262h41m53s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-05 17:17:07 +08:00
xiaoxia 6521be5426 fix(#1714): 素材上传去重防重 + complete幂等传递 + 轮询收敛(前端P0) (#1717)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 20s
CI/CD Pipeline / Build Staging API Image (push) Successful in 45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m51s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m58s
CI/CD Pipeline / Integration Tests (push) Successful in 2m13s
CI/CD Pipeline / Validate - Style (push) Successful in 2m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m17s
AI Code Review / AI Code Review (pull_request) Failing after 4m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m35s
CI/CD Pipeline / Validate - Security (push) Successful in 5m22s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m20s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m5s
CI/CD Pipeline / Unit Tests (push) Successful in 8m14s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 12m54s
CI/CD Pipeline / Canary Release to Production (push) Failing after 262h35m59s
CI/CD Pipeline / Build Production Web Image (push) Failing after 262h36m3s
CI/CD Pipeline / Deploy Production (push) Failing after 262h35m59s
CI/CD Pipeline / Build Production API Image (push) Failing after 262h36m3s
CI/CD Pipeline / CI Gate (push) Failing after 262h36m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 262h41m3s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 262h42m18s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 262h42m21s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 262h42m24s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 262h42m34s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 262h42m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 262h42m35s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 262h42m35s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 262h42m36s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 262h42m47s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 262h42m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 262h42m49s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 262h42m51s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 262h44m11s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 262h44m10s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 262h44m11s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 262h44m12s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 262h44m13s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 262h44m14s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 262h44m16s
CI/CD Pipeline / Frontend Lint (push) Failing after 262h44m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 262h44m22s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 262h44m23s
CI/CD Pipeline / PR Build API Image (push) Failing after 262h44m23s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 262h44m24s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 262h44m24s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 263h10m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 263h16m56s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 263h17m23s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 263h17m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 263h18m46s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 263h18m49s
CI/CD Pipeline / PR Build Web Image (push) Failing after 263h18m57s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-05 16:40:05 +08:00
xiaoxia 0a7ae8db4f fix(upload): complete 接口幂等去重 + HEVC 转码回写占位 asset 禁止兜底新建 (#1714) (#1715)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m8s
AI Code Review / AI Code Review (pull_request) Failing after 2m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m51s
CI/CD Pipeline / Validate - Style (push) Successful in 2m36s
CI/CD Pipeline / Integration Tests (push) Successful in 2m33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m21s
CI/CD Pipeline / Validate - Security (push) Successful in 5m43s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m1s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m20s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m32s
CI/CD Pipeline / Unit Tests (push) Successful in 8m29s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / CI Gate (pull_request) Successful in 17s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 262h50m20s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 262h50m38s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 262h50m40s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 262h57m36s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 262h57m48s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 262h57m49s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 262h57m54s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 262h57m56s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 262h57m37s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 262h58m2s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 262h57m37s
CI/CD Pipeline / Deploy Production (push) Failing after 263h8m9s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 263h8m10s
CI/CD Pipeline / Build Production Web Image (push) Failing after 263h8m10s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 263h12m49s
CI/CD Pipeline / Build Production API Image (push) Failing after 263h8m10s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 263h14m0s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 263h14m0s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 263h14m1s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 262h57m58s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 262h58m1s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 262h58m1s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 262h57m58s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 262h58m1s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 262h57m58s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 263h16m37s
CI/CD Pipeline / PR Build API Image (push) Failing after 263h16m38s
CI/CD Pipeline / PR Build Web Image (push) Failing after 263h16m38s
CI/CD Pipeline / Frontend Lint (push) Failing after 263h16m39s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 263h16m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 263h32m15s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 263h32m22s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 263h32m29s
CI/CD Pipeline / CI Gate (push) Failing after 263h42m43s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 263h24m54s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 263h25m13s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 263h32m34s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-05 16:07:45 +08:00
xiaoxia 04b4fab131 fix(#1677): 移除预览网格截断+空指针防御+排序显式Number(AI Review 第二轮) (#1713)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 16s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 17s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 22s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 38s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m4s
CI/CD Pipeline / Integration Tests (push) Successful in 2m7s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m6s
CI/CD Pipeline / Validate - Style (push) Successful in 2m50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m37s
CI/CD Pipeline / Validate - Security (push) Successful in 5m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m24s
AI Code Review / AI Code Review (pull_request) Failing after 5m48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m28s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m21s
CI/CD Pipeline / Unit Tests (push) Successful in 8m42s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 265h47m54s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 265h47m55s
CI/CD Pipeline / Build Production Web Image (push) Failing after 265h47m56s
CI/CD Pipeline / Deploy Production (push) Failing after 265h47m54s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 265h55m2s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 265h55m8s
CI/CD Pipeline / Build Production API Image (push) Failing after 265h47m56s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 265h55m10s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 265h52m54s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 265h55m10s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 265h55m11s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 265h55m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 265h55m15s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 265h56m8s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 265h56m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 265h56m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 265h56m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 265h56m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 265h56m35s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 265h56m35s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 265h56m37s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 265h56m37s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 265h56m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 265h56m38s
CI/CD Pipeline / Frontend Lint (push) Failing after 265h56m40s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 265h56m43s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 265h56m45s
CI/CD Pipeline / PR Build API Image (push) Failing after 265h56m45s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 265h56m46s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 265h56m46s
CI/CD Pipeline / CI Gate (push) Failing after 266h22m28s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 266h29m40s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 266h29m44s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 266h30m40s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 266h30m43s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 266h31m8s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 266h31m9s
CI/CD Pipeline / PR Build Web Image (push) Failing after 266h31m17s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-05 13:27:46 +08:00
xiaoxia fbfd19fbb9 fix(#1677): AI Review 跟进修复(标题校验/TTS依赖/排序兜底/预览数硬上限) (#1712)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 20s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 22s
CI/CD Pipeline / Build Staging API Image (push) Successful in 15s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 26s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 13s
CI/CD Pipeline / Integration Tests (push) Successful in 1m40s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m1s
CI/CD Pipeline / Validate - Style (push) Successful in 2m15s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m18s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m23s
AI Code Review / AI Code Review (pull_request) Successful in 3m45s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 4m34s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m37s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m9s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m9s
CI/CD Pipeline / Unit Tests (push) Successful in 8m22s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 266h19m51s
CI/CD Pipeline / CI Gate (push) Failing after 266h19m54s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 266h19m54s
CI/CD Pipeline / Build Production API Image (push) Failing after 266h19m55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 266h24m36s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 266h26m50s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 266h26m53s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 266h27m40s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 266h27m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 266h27m41s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 266h27m42s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 266h28m7s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 266h28m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 266h27m42s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 266h28m12s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 266h27m42s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 266h28m12s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 266h27m43s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 266h28m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 266h27m43s
CI/CD Pipeline / Frontend Lint (push) Failing after 266h28m16s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 266h28m3s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 266h28m18s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 266h28m19s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 266h28m19s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 266h28m11s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 266h28m20s
CI/CD Pipeline / PR Build Web Image (push) Failing after 266h28m19s
CI/CD Pipeline / PR Build API Image (push) Failing after 266h28m19s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 266h28m20s
CI/CD Pipeline / Deploy Production (push) Failing after 266h54m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 266h54m26s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 267h1m23s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 267h2m13s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 267h2m37s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 267h2m43s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 267h2m45s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 267h2m50s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-05 12:56:10 +08:00
230 changed files with 18952 additions and 2971 deletions
+2
View File
@@ -1187,6 +1187,8 @@ jobs:
COSYVOICE_API_KEY: ${{ secrets.COSYVOICE_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
MEDIAKIT_API_KEY: ${{ secrets.MEDIAKIT_API_KEY }}
WECHAT_APP_ID: ${{ secrets.WECHAT_APP_ID }}
WECHAT_APP_SECRET: ${{ secrets.WECHAT_APP_SECRET }}
run: |
set -eu
echo "Rendering .env from template + secrets..."
@@ -0,0 +1,34 @@
"""add client_upload_id to assets and asset_id to ingest_jobs
Issue #1714:上传 complete 幂等 + worker 转码回写关联。
- assets.client_upload_id:客户端幂等 token(complete 去重)
- ingest_jobs.asset_id:complete 阶段创建的占位 asset id(worker 回写关联,
防止 HEVC 转码改写 storage_key 后找不到占位而兜底新建 READY 记录)
Revision ID: 066_upload_idempotency
Revises: 065_dup_record_sim_match
Create Date: 2026-09-05
"""
import sqlalchemy as sa
from alembic import op
revision = "066_upload_idempotency"
down_revision = "065_dup_record_sim_match"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("assets", sa.Column("client_upload_id", sa.String(64), nullable=True))
op.create_index("ix_assets_client_upload_id", "assets", ["client_upload_id"])
op.add_column("ingest_jobs", sa.Column("asset_id", sa.String(36), nullable=False, server_default=""))
op.create_index("ix_ingest_jobs_asset_id", "ingest_jobs", ["asset_id"])
def downgrade() -> None:
op.drop_index("ix_ingest_jobs_asset_id", table_name="ingest_jobs")
op.drop_column("ingest_jobs", "asset_id")
op.drop_index("ix_assets_client_upload_id", table_name="assets")
op.drop_column("assets", "client_upload_id")
@@ -0,0 +1,35 @@
"""add celery_task_id to generation_tasks and ingest_jobs
Issue #1714:孤儿恢复/超时清理撤销队列消息。
- generation_tasks.celery_task_id:入队时记录的 Celery 消息 ID,清理时 revoke
- ingest_jobs.celery_task_id:同上(素材转码任务)
Revision ID: 067_celery_task_id
Revises: 066_upload_idempotency
Create Date: 2026-09-05
"""
import sqlalchemy as sa
from alembic import op
revision = "067_celery_task_id"
down_revision = "066_upload_idempotency"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"generation_tasks",
sa.Column("celery_task_id", sa.String(64), nullable=False, server_default=""),
)
op.add_column(
"ingest_jobs",
sa.Column("celery_task_id", sa.String(64), nullable=False, server_default=""),
)
def downgrade() -> None:
op.drop_column("ingest_jobs", "celery_task_id")
op.drop_column("generation_tasks", "celery_task_id")
@@ -0,0 +1,26 @@
"""add profile_completed to users
Issue #1718:微信新用户首次登录需设置昵称(PATCH /auth/me)。
- users.profile_completed:资料是否已完善;存量行默认 True(不触发引导),
微信新建用户在应用层置 False。
"""
import sqlalchemy as sa
from alembic import op
revision = "068_user_profile_completed"
down_revision = "067_celery_task_id"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"users",
sa.Column("profile_completed", sa.Boolean(), nullable=False, server_default=sa.text("true")),
)
def downgrade() -> None:
op.drop_column("users", "profile_completed")
+6
View File
@@ -11,6 +11,7 @@ from app.api.routes.feature_flags import router as feature_flags_router
from app.api.routes.generation_cover import router as generation_cover_router
from app.api.routes.generation_preview import router as generation_preview_router
from app.api.routes.generation_tasks import router as generation_tasks_router
from app.api.routes.generation_variant_plans import router as generation_variant_plans_router
from app.api.routes.health import router as health_check_router
from app.api.routes.ingest_jobs import router as ingest_jobs_router
from app.api.routes.internal_render import router as internal_render_router
@@ -99,6 +100,11 @@ api_router.include_router(
prefix="/generation",
tags=["Generation"],
)
api_router.include_router(
generation_variant_plans_router,
prefix="/generation",
tags=["Generation"],
)
api_router.include_router(
generation_cover_router,
prefix="/generation",
+186 -2
View File
@@ -1,4 +1,5 @@
"""
from __future__ import annotations
Canonical authentication API routes.
The route layer is intentionally thin: repository construction lives in
@@ -13,9 +14,9 @@ import jwt
from app.auth import AuthenticatedUser, blacklist_token, get_current_user
from app.config import settings
from app.dependencies import get_auth_email_service, get_auth_session_store, get_user_repository
from fastapi import APIRouter, Depends, Header, HTTPException, status
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, EmailStr
from pydantic import BaseModel, EmailStr, field_validator
from packages.adapters.redis import NoopSessionStore
from packages.adapters.smtp import NoopEmailService
@@ -84,6 +85,23 @@ class CurrentUserResponse(BaseModel):
phone: str = ""
phone_verified: bool = False
binding_complete: bool = False
wechat_bound: bool = False
profile_completed: bool = True
class UserProfileResponse(BaseModel):
"""用户资料负载(PATCH /me、绑定/解绑接口复用;字段与 GET /auth/me 一致,前端 normalizeUser 直接消费)"""
user_id: str
email: str
username: str
display_name: str
email_verified: bool
phone: str = ""
phone_verified: bool = False
binding_complete: bool = False
wechat_bound: bool = False
profile_completed: bool = True
class PasswordResetRequestModel(BaseModel):
@@ -272,9 +290,52 @@ async def get_current_user_info(
phone=user.phone or "",
phone_verified=user.phone_verified,
binding_complete=binding_complete,
wechat_bound=bool(user.wechat_openid),
profile_completed=user.profile_completed,
)
class UpdateProfileRequest(BaseModel):
"""更新个人资料请求(当前仅支持昵称)"""
display_name: str
@field_validator("display_name")
@classmethod
def _validate_display_name(cls, v: str) -> str:
name = (v or "").strip()
if not name:
raise ValueError("昵称不能为空白")
if len(name) > 20:
raise ValueError("昵称长度需在 1-20 个字符之间")
return name
class UpdateProfileResponse(BaseModel):
"""更新资料响应:前端 normalizeUser(response.user) 直接消费"""
user: UserProfileResponse
@router.patch("/me", response_model=UpdateProfileResponse)
async def update_current_user_profile(
request: UpdateProfileRequest,
current_user: AuthenticatedUser = Depends(get_current_user),
user_repository: UserRepository = Depends(get_user_repository),
) -> UpdateProfileResponse:
"""更新当前登录用户昵称(微信新用户首次设置昵称后置 profile_completed=True)。"""
user = current_user.user
user.display_name = request.display_name # 已 strip(validator)
if not user.profile_completed:
user.profile_completed = True
user_repository.save(user)
logger.info("[资料更新] 用户 %s 更新昵称,profile_completed=%s", user.id, user.profile_completed)
# 重新读取,确保返回的是持久化后的最新状态
fresh = user_repository.find_by_id(user.id) or user
return UpdateProfileResponse(user=_user_profile(fresh))
class _NoopSessionStore(NoopSessionStore):
pass
@@ -426,6 +487,7 @@ async def get_wechat_auth_url() -> WechatAuthUrlResponse:
@router.post("/wechat/callback", response_model=WechatLoginResponse)
async def wechat_callback(
request: WechatCallbackRequest,
http_request: Request,
user_repository: UserRepository = Depends(get_user_repository),
) -> WechatLoginResponse:
"""微信登录回调处理"""
@@ -433,11 +495,30 @@ async def wechat_callback(
from packages.application.auth.wechat_sync_use_case import WechatSyncRequest as SyncRequest
from packages.application.auth.wechat_sync_use_case import WechatSyncUseCase
# 回调可观测性:记录 UA(区分微信内置浏览器 MicroMessenger)与 state,
# 便于排查"停留 open.weixin.qq.com / 回调失败"类问题(#1718)
user_agent = http_request.headers.get("User-Agent", "")
is_wechat_browser = "MicroMessenger" in user_agent
logger.info(
"[微信回调] 收到回调: state=%s code_len=%d UA=%r 微信内置浏览器=%s",
(request.state or "")[:8],
len(request.code or ""),
user_agent[:200],
is_wechat_browser,
)
# 1. 用 code 换微信用户信息
oauth_service = get_wechat_oauth_service()
wechat_user, err = oauth_service.handle_callback(request.code, request.state)
if err:
# state 校验失败 / 微信 errcode 等错误原文已在 service 内 log,这里带上 UA 上下文
logger.warning("[微信回调] 处理失败: err=%s 微信内置浏览器=%s", err, is_wechat_browser)
raise HTTPException(status_code=400, detail=err)
logger.info(
"[微信回调] state 校验通过,微信用户信息获取成功: openid=%s unionid=%s",
wechat_user.openid[:8] if wechat_user.openid else "",
bool(wechat_user.unionid),
)
# 2. 同步登录/注册(复用 wechat-sync 逻辑)
use_case = WechatSyncUseCase(user_repository=user_repository)
@@ -472,6 +553,109 @@ async def wechat_callback(
)
# ==================== 微信账号绑定/解绑(已登录用户) ====================
class WechatBindUrlResponse(BaseModel):
auth_url: str
state: str
class WechatBindCompleteRequest(BaseModel):
code: str
state: str = ""
class WechatBindCompleteResponse(BaseModel):
success: bool
user: UserProfileResponse
class WechatUnbindResponse(BaseModel):
success: bool
def _user_profile(user) -> UserProfileResponse:
binding_complete = bool(
user.phone_verified and user.email_verified and user.email and "@wechat.local" not in user.email
)
return UserProfileResponse(
user_id=user.id,
email=user.email,
username=user.username,
display_name=user.display_name,
email_verified=user.email_verified,
phone=user.phone or "",
phone_verified=user.phone_verified,
binding_complete=binding_complete,
wechat_bound=bool(user.wechat_openid),
profile_completed=user.profile_completed,
)
@router.get("/wechat/bind/url", response_model=WechatBindUrlResponse)
async def get_wechat_bind_url(
current_user: AuthenticatedUser = Depends(get_current_user),
) -> WechatBindUrlResponse:
"""获取微信绑定授权链接(已登录用户场景)。state 经 Redis 存储做 CSRF 校验。"""
from packages.application.auth.wechat_oauth_service import get_wechat_oauth_service
oauth_service = get_wechat_oauth_service()
auth_url, state = oauth_service.generate_auth_url()
logger.info("[微信绑定] 用户 %s 请求绑定授权链接", current_user.user.id)
return WechatBindUrlResponse(auth_url=auth_url, state=state)
@router.post("/wechat/bind", response_model=WechatBindCompleteResponse)
async def wechat_bind(
request: WechatBindCompleteRequest,
current_user: AuthenticatedUser = Depends(get_current_user),
user_repository: UserRepository = Depends(get_user_repository),
) -> WechatBindCompleteResponse:
"""微信绑定完成:扫码回调后用 code 换 openid,绑定到当前登录账号(不创建新用户)。"""
from packages.application.auth.wechat_bind_use_case import WechatBindRequest, WechatBindUseCase
from packages.application.auth.wechat_oauth_service import get_wechat_oauth_service
oauth_service = get_wechat_oauth_service()
wechat_user, err = oauth_service.handle_callback(request.code, request.state)
if err:
logger.warning("[微信绑定] 用户 %s 换取微信信息失败: %s", current_user.user.id, err)
raise HTTPException(status_code=400, detail=err)
use_case = WechatBindUseCase(user_repository=user_repository)
result, error, http_status = use_case.bind(
WechatBindRequest(
user_id=current_user.user.id,
openid=wechat_user.openid,
unionid=wechat_user.unionid or "",
)
)
if error:
logger.warning("[微信绑定] 用户 %s 绑定失败: %s", current_user.user.id, error)
raise HTTPException(status_code=http_status, detail=error)
logger.info("[微信绑定] 用户 %s 绑定成功 openid=%s", current_user.user.id, wechat_user.openid[:8])
return WechatBindCompleteResponse(success=True, user=_user_profile(result.user))
@router.delete("/wechat/bind", response_model=WechatUnbindResponse)
async def wechat_unbind(
current_user: AuthenticatedUser = Depends(get_current_user),
user_repository: UserRepository = Depends(get_user_repository),
) -> WechatUnbindResponse:
"""解绑微信:需账号仍有其他登录方式(密码/手机/真实邮箱),否则拒绝。"""
from packages.application.auth.wechat_bind_use_case import WechatUnbindUseCase
use_case = WechatUnbindUseCase(user_repository=user_repository)
result, error, http_status = use_case.unbind(current_user.user.id)
if error:
logger.warning("[微信解绑] 用户 %s 解绑失败: %s", current_user.user.id, error)
raise HTTPException(status_code=http_status, detail=error)
logger.info("[微信解绑] 用户 %s 解绑成功", current_user.user.id)
return WechatUnbindResponse(success=True)
# ==================== 验证码 & 绑定 ====================
+3 -1
View File
@@ -14,6 +14,7 @@ from typing import Any
from uuid import uuid4
from app.api.routes._helpers import require_project_and_library
from app.api.routes.upload import _persist_celery_task_id
from app.auth import AuthenticatedUser, get_current_user
from app.core.celery_app import celery_app
from app.core.storage import OSSStorageService, get_storage_service
@@ -381,7 +382,8 @@ async def complete_chunked_upload(
file_hash=request.file_hash,
)
)
celery_app.send_task("worker.ingest_asset", args=[job.id])
celery_result = celery_app.send_task("worker.ingest_asset", args=[job.id])
_persist_celery_task_id(ingest_job_repository, job, getattr(celery_result, "id", ""))
# Update metadata status
meta["status"] = "completed"
+84 -4
View File
@@ -75,6 +75,86 @@ class GenerateCoverResponse(BaseModel):
# ── Route ────────────────────────────────────────────────────────────────
def _select_best_frame_from_snapshots(
snapshots: list[dict], plan_id: str
) -> str:
"""从 MediaKit 抽帧结果中,通过质量评分选出最佳帧。
降级策略:cv2 不可用或评分失败时,返回第一帧。
Args:
snapshots: MediaKit 返回的帧列表 [{"image_url": str, ...}, ...]
plan_id: 计划 ID(日志用)
Returns:
最佳帧的 image_url,或空字符串
"""
if not snapshots:
return ""
if len(snapshots) == 1:
return snapshots[0].get("image_url") or snapshots[0].get("url") or ""
try:
import tempfile
import httpx
from packages.shared.cover_frame_scorer import score_frames
scored_candidates = []
for snap in snapshots:
url = snap.get("image_url") or snap.get("url") or ""
if not url:
continue
# 下载帧到临时文件进行评分
try:
resp = httpx.get(url, timeout=15, follow_redirects=True)
resp.raise_for_status()
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
tmp.write(resp.content)
tmp_path = tmp.name
scored_candidates.append({"image_path": tmp_path, "url": url})
except Exception:
# 下载失败的帧跳过,给默认低分
scored_candidates.append({"image_path": None, "url": url, "score": 0.0})
if not scored_candidates:
return snapshots[0].get("image_url") or snapshots[0].get("url") or ""
scored = score_frames(scored_candidates)
best = scored[0] if scored else None
best_url = best.get("url", "") if best else ""
best_score = best.get("score", 0.0) if best else 0.0
logger.info(
"[封面生成] 帧质量评分完成: plan_id=%s candidates=%d best_score=%.1f",
plan_id,
len(scored_candidates),
best_score,
)
# 清理临时文件
for c in scored_candidates:
path = c.get("image_path")
if path:
try:
from pathlib import Path
Path(path).unlink(missing_ok=True)
except Exception:
pass
return best_url
except Exception:
logger.warning(
"[封面生成] 帧质量评分失败,使用第一帧: plan_id=%s",
plan_id,
exc_info=True,
)
return snapshots[0].get("image_url") or snapshots[0].get("url") or ""
def _persist_cover_frame(
frame_url: str,
plan_id: str,
@@ -652,13 +732,13 @@ def generate_cover(
snapshots = mk_client.extract_frames(
video_url=primary_video_url,
strategy="SpecifiedFrames",
max_frames=1,
max_frames=5, # 抽 5 帧,通过质量评分选最佳
poll_interval=2.0,
max_poll_attempts=5,
max_retries=0,
)
if snapshots:
raw = snapshots[0].get("image_url") or snapshots[0].get("url") or ""
raw = _select_best_frame_from_snapshots(snapshots, plan_id)
if raw:
cover_url_from_task = _persist_cover_frame(raw, plan_id)
logger.info(
@@ -715,13 +795,13 @@ def generate_cover(
snapshots = mk_client.extract_frames(
video_url=src_url,
strategy="SpecifiedFrames",
max_frames=1,
max_frames=5, # 抽 5 帧,通过质量评分选最佳
poll_interval=2.0,
max_poll_attempts=5,
max_retries=0,
)
if snapshots:
raw = snapshots[0].get("image_url") or snapshots[0].get("url") or ""
raw = _select_best_frame_from_snapshots(snapshots, plan_id)
if raw:
cover_url_from_task = _persist_cover_frame(
raw,
+89 -14
View File
@@ -390,7 +390,6 @@ def create_preview_generation_task(
template_id=request.template_id,
asset_ids=list(request.asset_ids),
title_ids=list(request.title_ids),
voice_ids=list(request.voice_ids),
created_by_user_id=user_id,
source_edit_plan_id=request.source_edit_plan_id,
asset_select_mode="",
@@ -420,42 +419,118 @@ def create_preview_generation_task(
logger.error("[预览生成] 创建失败: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="创建预览生成任务失败,请稍后再试") from e
# ── 克隆独立变体 plan:N 个预览全部克隆(预览不污染源 plan)──
# 源 plan 不存在(无编辑历史)时各任务走自身随机选片流程,不克隆。
# ── 独立变体 plan(#1743)──
# count=1:克隆源 plan(预览不污染源 plan,仅起点重算),行为与旧版一致;
# count>1:变体 0 保留源 plan,变体 1..N-1 用 reselect_plan_for_variant 完整
# 重跑单视频选片(素材洗牌+镜头洗牌+起点随机+跨变体避让+批次 20% 重叠重选),
# 所见即所得——预览变体差异即正式成片差异。
source_plan_id = created_tasks[0].source_edit_plan_id if created_tasks else ""
if source_plan_id:
# #1749:各变体配音解析(严格守卫已在 schema;此处取每变体 voice 查时长)+ 时长分配
def _preview_voice_durations() -> list[float]:
try:
from packages.domain.variant_voice_resolver import resolve_variant_voice_ids
voices = resolve_variant_voice_ids(
count=count,
voice_library_id=request.voice_library_id,
voice_library_ids=request.voice_library_ids or None,
)
except Exception:
logger.warning("[预览生成] 配音解析失败(按无配音处理)", exc_info=True)
return [0.0] * count
try:
from app.api.routes.generation_tasks import _query_voice_durations
return _query_voice_durations(db, voices)
except Exception:
return [0.0] * count
voice_durations = _preview_voice_durations()
if source_plan_id and count == 1:
# 单预览:克隆一份(原逻辑)+ 配音时长分配
try:
from app.services.edit_plan_service import EditPlanService
_plan_svc = EditPlanService(db)
for variant_index in range(count):
variant_plan = _plan_svc.clone_plan_for_variant(
source_plan_id,
created_by_user_id=user_id,
name_suffix="预览变体",
)
if voice_durations and voice_durations[0] > 0:
try:
_plan_svc.apply_voice_duration_to_plan(variant_plan.id, voice_durations[0])
except Exception:
logger.exception("[预览生成] 变体0 配音分配失败(不阻断): plan=%s", variant_plan.id)
variant_plan_ids.append(variant_plan.id)
except Exception as e:
logger.error("[预览生成] 克隆预览 plan 异常: %s", e, exc_info=True)
for t in created_tasks:
_mark_task_failed(generation_task_repository, t, "预览计划创建失败")
raise HTTPException(
status_code=500,
detail="创建预览任务失败:无法生成独立剪辑计划,请重试",
) from e
elif source_plan_id and count > 1:
try:
from app.services.edit_plan_service import EditPlanService
_plan_svc = EditPlanService(db)
# #1749:变体 0 也 clone(不污染源 plan)+ 配音分配;变体 1..N-1 独立选片
_plan0 = _plan_svc.clone_plan_for_variant(
source_plan_id,
created_by_user_id=user_id,
name_suffix="预览变体1",
)
if voice_durations and voice_durations[0] > 0:
try:
_plan_svc.apply_voice_duration_to_plan(_plan0.id, voice_durations[0])
except Exception:
logger.exception("[预览生成] 变体0 配音分配失败(不阻断): plan=%s", _plan0.id)
variant_plan_ids.append(_plan0.id)
batch_asset_pool = list(dict.fromkeys(request.asset_ids or []))
for variant_index in range(1, count):
last_err: Exception | None = None
variant_plan = None
for _attempt in range(2): # 1 次重试,抗 DB 瞬时抖动
try:
variant_plan = _plan_svc.clone_plan_for_variant(
variant_plan = _plan_svc.reselect_plan_for_variant(
source_plan_id,
batch_asset_pool,
created_by_user_id=user_id,
name_suffix=f"预览变体{variant_index + 1}" if count > 1 else "预览变体",
name_suffix=f"预览变体{variant_index + 1}",
voice_duration=(
voice_durations[variant_index] if variant_index < len(voice_durations) else 0.0
),
)
break
except Exception as clone_err: # noqa: PERF203
last_err = clone_err
except ValueError as ve:
logger.warning("[预览生成] 变体独立选片失败(素材不足): %s", ve)
for t in created_tasks:
_mark_task_failed(generation_task_repository, t, "预览变体选片失败")
raise HTTPException(
status_code=400,
detail=f"批量预览第 {variant_index + 1} 个视频无法独立选片:{ve}。"
"请增加素材库中的视频素材后重试。",
) from ve
except Exception as reselection_err: # noqa: PERF203
last_err = reselection_err
logger.warning(
"[预览生成] 克隆变体 plan 失败(尝试%d/2): variant=%d error=%s",
"[预览生成] 变体独立选片失败(尝试%d/2): variant=%d error=%s",
_attempt + 1,
variant_index,
clone_err,
reselection_err,
exc_info=True,
)
if variant_plan is None:
logger.error(
"[预览生成] 克隆预览变体 plan 重试仍失败: variant=%d source=%s",
"[预览生成] 变体独立选片重试仍失败: variant=%d source=%s",
variant_index,
source_plan_id,
exc_info=last_err,
)
# 标记已创建任务失败
for t in created_tasks:
_mark_task_failed(generation_task_repository, t, "预览变体计划创建失败")
raise HTTPException(
@@ -466,7 +541,7 @@ def create_preview_generation_task(
except HTTPException:
raise
except Exception as e:
logger.error("[预览生成] 克隆变体 plan 异常: %s", e, exc_info=True)
logger.error("[预览生成] 变体 plan 生成异常: %s", e, exc_info=True)
for t in created_tasks:
_mark_task_failed(generation_task_repository, t, "预览变体计划创建失败")
raise HTTPException(
+225 -44
View File
@@ -57,6 +57,30 @@ def _variant_value(values: list[str], index: int, fallback: str = "") -> str:
return values[index] if index < len(values) else fallback
def _query_voice_durations(db: Session, voice_ids: list[str]) -> list[float]:
"""批量查询配音素材时长(秒),#1749 配音时长分配用。
逐项 try/float 硬化:MagicMock/异常/缺失 → 0.0(无配音不分配,不阻断)。
"""
ids = [v for v in dict.fromkeys(voice_ids or []) if v]
if not ids:
return []
try:
from packages.adapters.sqlalchemy_impl.models import AssetModel
rows = db.query(AssetModel.id, AssetModel.duration).filter(AssetModel.id.in_(ids)).all()
dur_map: dict[str, float] = {}
for row in rows:
try:
dur_map[row[0]] = float(row[1] or 0.0)
except (TypeError, ValueError):
dur_map[row[0]] = 0.0
return [dur_map.get(v, 0.0) for v in ids]
except Exception:
logger.warning("[生成任务] 配音时长查询失败(按无配音处理,不阻断)", exc_info=True)
return [0.0 for _ in ids]
def _to_generation_task_response(task) -> GenerationTaskResponse:
return GenerationTaskResponse(
id=task.id,
@@ -123,6 +147,7 @@ def _select_assets_from_library(
assets: list,
mode: str,
count: int,
rng=None,
) -> list[str]:
"""根据选取模式从素材库中选取 ready 状态的视频素材 ID。
@@ -130,6 +155,8 @@ def _select_assets_from_library(
assets: 素材库中所有素材(Asset 实体列表)
mode: 选取模式 — all=全部, smart=智能匹配(多维度评分+多样性)
count: 选取数量,0 表示全部(仅 smart 模式有效)
rng: 可选随机源(smart 模式排序噪声用),生产环境不传则内部随机;
测试可注入固定种子或零噪声随机源获得确定性结果。
Returns:
选中的素材 ID 列表
@@ -142,8 +169,9 @@ def _select_assets_from_library(
if mode == "smart":
# 智能匹配:统一使用 packages/domain/smart_match.py 的多维评分+多样性选取
# 评分维度:质量分(40%) + 时长适配(30%) + 新鲜度(20%) + 未使用加分(10%)
# 排序注入随机噪声(#1743):同分素材每次选出不同组合,从素材组合层面降重
limit = count if count > 0 else None
results = smart_select_assets(ready_video_assets, limit=limit, kind="video")
results = smart_select_assets(ready_video_assets, limit=limit, kind="video", rng=rng)
return [r.asset.id for r in results]
# 默认 all 模式:返回全部 ready 视频素材
@@ -432,55 +460,199 @@ def create_generation_task(
logger.info("画中画已下线,strategy_id %s → one_take", effective_strategy_id)
effective_strategy_id = "one_take"
# 批量生成时每个任务关联独立克隆 plan(片段起点重算),
# 禁止 N 条任务共用同一 source_edit_plan_id 导致片段一模一样。
# 在创建任何任务【之前】预克隆全部变体:克隆失败直接中断(此时无脏数据),
# 绝不静默退回共用源 plan(否则批量视频内容重复,违反去重诉求)。
# 批量生成(count>1):每个变体必须走与单视频完全相同的独立选片流程(#1743/#1749)。
# - 变体 0:clone 源 plan(不污染源 plan),变体 1..N-1 用 reselect_plan_for_variant
# 完整重跑选片(素材级去重:fresh 优先 → 受控复用 overlap≤20% → 短素材禁复用);
# - #1749:前端可回传 variant-plans 接口预生成的 plan_id(variant_plan_ids),直接复用;
# 回传 plan 仍按各变体配音幂等重分配段长(防 variant-plans 阶段未带配音/占位时长);
# - 配音时长:独立配音各自时长、统一配音同值,逐变体 apply_voice_duration_to_plan,
# 成片总时长=配音时长(素材短→末帧冻结,禁慢放/禁截配音);
# - count>1 但没有源 plan 时,不允许 N 个任务兜底共用同一 plan,直接 4xx 中断。
# 在创建任何任务【之前】预生成/校验全部变体 plan:失败直接中断(此时无脏数据)。
variant_plan_ids: list[str] = []
if count > 1 and request.source_edit_plan_id:
if count > 1:
from app.services.edit_plan_service import EditPlanService
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
_plan_svc = EditPlanService(db)
for task_index in range(1, count):
variant = None
last_err: Exception | None = None
for _attempt in range(2): # 1 次重试,抗 DB 瞬时抖动
try:
variant = _plan_svc.clone_plan_for_variant(
request.source_edit_plan_id,
created_by_user_id=user_id,
name_suffix=f"批量{task_index + 1}",
# 解析每变体配音(严格守卫:独立配音长度/缺值 → 400,禁静默 fallback)
try:
variant_voices = resolve_variant_voice_ids(
count=count,
voice_library_id=request.voice_library_id,
voice_library_ids=request.voice_library_ids or None,
)
except VariantVoiceError as ve:
raise HTTPException(status_code=400, detail=str(ve)) from ve
# 各变体配音时长(查询硬化:异常 → 0.0 不阻断)
voice_durations = _query_voice_durations(db, variant_voices)
# 解析批量源 plan:优先前端传入;否则按 template_id + user 查最新(与单任务兜底同源)
batch_source_plan_id = request.source_edit_plan_id
if not batch_source_plan_id and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_latest = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == request.template_id,
EditPlanModel.created_by_user_id == user_id,
)
break
except Exception as clone_err: # noqa: PERF203
last_err = clone_err
logger.warning(
"[生成任务] 克隆变体 plan 失败(尝试%d/2): source=%s error=%s",
_attempt + 1,
request.source_edit_plan_id,
clone_err,
exc_info=True,
)
if variant is None:
logger.error(
"[生成任务] 克隆变体 plan 重试仍失败,中断批量创建: source=%s",
request.source_edit_plan_id,
exc_info=last_err,
.order_by(EditPlanModel.created_at.desc())
.first()
)
if _latest:
batch_source_plan_id = _latest.id
except Exception:
logger.warning("[生成任务] 批量源 plan 解析失败", exc_info=True)
if not batch_source_plan_id and not request.variant_plan_ids:
# 无任何可用源 plan:批量变体无从选片,明确报错,严禁静默共用/同源
logger.error("[生成任务] 批量 count=%d 但无可编辑计划(无 source_edit_plan_id/template plan)", count)
raise HTTPException(
status_code=400,
detail="批量生成需要先完成预览生成(缺少剪辑计划)。请先生成预览后再批量创建。",
)
# 批次素材池:请求显式素材 + 库自动匹配素材(resolved_asset_ids)
batch_asset_pool = list(dict.fromkeys(resolved_asset_ids or []))
if request.variant_plan_ids:
# ① 前端回传 variant-plans 预生成结果:直接复用(轻量选片接口已建好 plan)
if len(request.variant_plan_ids) != count:
raise HTTPException(
status_code=500,
detail="创建批量任务失败:无法生成独立剪辑计划,请重试",
) from last_err
variant_plan_ids.append(variant.id)
status_code=400,
detail=f"variant_plan_ids 数量({len(request.variant_plan_ids)})与视频数量({count})不一致",
)
# 校验归属权
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
for _pid in request.variant_plan_ids:
_pm = db.query(EditPlanModel).filter(EditPlanModel.id == _pid).first()
if _pm is None:
raise HTTPException(status_code=400, detail=f"剪辑计划不存在: {_pid}")
if _pm.created_by_user_id and _pm.created_by_user_id != user_id:
raise HTTPException(status_code=403, detail=f"无权使用剪辑计划: {_pid}")
variant_plan_ids = list(request.variant_plan_ids)
else:
# ② 服务端选片:变体 0 clone 源 plan(不污染源 plan)
try:
_plan0 = _plan_svc.clone_plan_for_variant(
batch_source_plan_id,
created_by_user_id=user_id,
name_suffix="批量1",
)
except Exception as clone_err:
logger.error("[生成任务] 变体0 clone 失败: %s", clone_err, exc_info=True)
raise HTTPException(
status_code=500, detail="创建批量任务失败:无法生成独立剪辑计划,请重试"
) from clone_err
variant_plan_ids.append(_plan0.id)
# 变体 1..N-1 独立选片
for task_index in range(1, count):
variant = None
last_err: Exception | None = None
for _attempt in range(2): # 1 次重试,抗 DB 瞬时抖动
try:
variant = _plan_svc.reselect_plan_for_variant(
batch_source_plan_id,
batch_asset_pool,
created_by_user_id=user_id,
name_suffix=f"批量{task_index + 1}",
voice_duration=voice_durations[task_index] if task_index < len(voice_durations) else 0.0,
)
break
except ValueError as ve:
# 素材不足等可预期错误:不重试,直接中断并给出明确提示
logger.warning("[生成任务] 变体独立选片失败(素材不足): %s", ve)
raise HTTPException(
status_code=400,
detail=f"批量生成第 {task_index + 1} 个视频无法独立选片:{ve}。"
"请增加素材库中的视频素材后重试。",
) from ve
except Exception as reselection_err: # noqa: PERF203
last_err = reselection_err
logger.warning(
"[生成任务] 变体独立选片失败(尝试%d/2): source=%s error=%s",
_attempt + 1,
batch_source_plan_id,
reselection_err,
exc_info=True,
)
if variant is None:
logger.error(
"[生成任务] 变体独立选片重试仍失败,中断批量创建: source=%s",
batch_source_plan_id,
exc_info=last_err,
)
raise HTTPException(
status_code=500,
detail="创建批量任务失败:无法生成独立剪辑计划,请重试",
) from last_err
variant_plan_ids.append(variant.id)
# ③ 配音时长分配(回传 plan / clone 变体0 均需幂等分配;reselect 已在选片时分配)
for _vi, _pid in enumerate(variant_plan_ids):
_vd = voice_durations[_vi] if _vi < len(voice_durations) else 0.0
if _vd > 0:
try:
_plan_svc.apply_voice_duration_to_plan(_pid, _vd)
except Exception:
logger.exception("[生成任务] 变体%d 配音时长分配失败(不阻断): plan=%s", _vi, _pid)
# N=1 正式生成:渲染侧全局慢放兜底已删除(#1749),enqueue 前也必须按配音分配段长
if count == 1 and not request.is_preview:
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
try:
_voices = resolve_variant_voice_ids(
count=1,
voice_library_id=request.voice_library_id,
voice_library_ids=request.voice_library_ids or None,
)
_single_vd: list[float] = _query_voice_durations(db, _voices)
_single_dur = _single_vd[0] if _single_vd else 0.0
_single_plan = request.source_edit_plan_id
if not _single_plan and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_latest = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == request.template_id,
EditPlanModel.created_by_user_id == user_id,
)
.order_by(EditPlanModel.created_at.desc())
.first()
)
if _latest:
_single_plan = _latest.id
except Exception:
logger.warning("[生成任务] 单任务源 plan 解析失败", exc_info=True)
if _single_dur > 0 and _single_plan:
from app.services.edit_plan_service import EditPlanService
try:
EditPlanService(db).apply_voice_duration_to_plan(_single_plan, _single_dur)
except Exception:
logger.exception("[生成任务] N=1 配音时长分配失败(不阻断): plan=%s", _single_plan)
except VariantVoiceError as ve:
raise HTTPException(status_code=400, detail=str(ve)) from ve
except Exception:
logger.exception("[生成任务] N=1 配音分配兜底异常(不阻断)")
try:
for task_index in range(count):
# 第 1 条复用源 plan(保留用户编辑结果);其余使用预克隆的独立变体 plan。
# 无源 plan(source_edit_plan_id 为空)时无可克隆对象,variant_plan_ids
# 为空列表:各任务走自身随机选片流程,不做索引访问(防 IndexError)
effective_plan_id = request.source_edit_plan_id
if task_index > 0 and variant_plan_ids:
effective_plan_id = variant_plan_ids[task_index - 1]
# #1749:count>1 时每个变体(含变体0)都关联各自独立 plan(clone/reselect/variant-plans)。
if count > 1 and variant_plan_ids:
effective_plan_id = variant_plan_ids[task_index]
else:
effective_plan_id = request.source_edit_plan_id
# 变体级独立配置:titles[]/voice_library_ids[]/cover_urls[]
# 长度1=所有变体共用,长度=count=每个变体独立,空数组=回退单值字段
@@ -500,7 +672,6 @@ def create_generation_task(
template_id=request.template_id,
asset_ids=resolved_asset_ids,
title_ids=request.title_ids,
voice_ids=request.voice_ids,
created_by_user_id=user_id,
source_edit_plan_id=effective_plan_id,
asset_select_mode=request.asset_select_mode,
@@ -523,7 +694,19 @@ def create_generation_task(
try:
# 兜底关联编辑计划:前端未传 source_edit_plan_id 时,
# 通过 template_id + user_id 在 DB 层直接查找最新的 plan。
# 必须在 enqueue 之前执行,避免 worker 读取时 source_edit_plan_id 为空(竞态条件)
# 必须在 enqueue 之前执行,避免 worker 读取时 source_edit_plan_id 为空(竞态条件)。
# #1743:批量(count>1)场景严禁兜底共用——变体 plan 已在上方预生成,
# 走到这里还缺 plan 说明预生成漏配,直接报错中断,不允许 N 任务关联同一 plan。
if not task.source_edit_plan_id and count > 1:
logger.error(
"[生成任务] 批量任务缺少独立 plan(禁止共用兜底): task_index=%d task_id=%s",
task_index,
task.id,
)
raise HTTPException(
status_code=500,
detail="创建批量任务失败:变体剪辑计划缺失,请重新预览后再批量生成。",
)
if not task.source_edit_plan_id and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
@@ -690,7 +873,6 @@ def confirm_generation(
template_id=source_task.template_id,
asset_ids=source_task.asset_ids,
title_ids=source_task.title_ids,
voice_ids=source_task.voice_ids,
created_by_user_id=authenticated_user.user.id,
source_edit_plan_id=source_task.source_edit_plan_id or "",
asset_select_mode=source_task.asset_select_mode,
@@ -834,7 +1016,6 @@ def retry_generation_task(
template_id=task.template_id,
asset_ids=task.asset_ids,
title_ids=task.title_ids,
voice_ids=task.voice_ids,
created_by_user_id=user_id,
source_edit_plan_id=task.source_edit_plan_id or "",
asset_select_mode=getattr(task, "asset_select_mode", ""),
@@ -0,0 +1,178 @@
"""轻量选片接口 POST /generation/variant-plans(#1749)。
与正式生成共用同一套选片函数(EditPlanService.ensure_variant_plans →
clone_plan_for_variant / reselect_plan_for_variant → variant_plan_selector),
但**不建任务、不入队、不渲染**:
- 仅为 N 个变体创建/选好 EditPlan + clips,返回 plan_id 与片段列表;
- 前端确认后调正式生成接口回传 variant_plan_ids,直接复用这些 plan,
不再重复选片(回传后仍按各变体配音幂等重分配段长);
- 配音守卫:voice_library_ids 长度/缺值 → 400(variant_voice_resolver),
禁静默 fallback;
- 素材不足等选片失败 → 400(与正式生成同口径);除此之外不报错打断。
"""
from __future__ import annotations
import logging
from typing import Any
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_db_session
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, model_validator
from sqlalchemy.orm import Session
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
logger = logging.getLogger(__name__)
router = APIRouter()
class VariantPlanRequest(BaseModel):
"""轻量选片请求体(与前端 variantPlans.ts 契约一致)。"""
template_id: str = Field(default="", description="模板 ID(无 source_edit_plan_id 时用于查找骨架 plan)")
asset_ids: list[str] = Field(default_factory=list, description="批次素材池")
count: int = Field(default=1, ge=1, le=50, description="变体数量")
source_edit_plan_id: str = Field(default="", description="源剪辑计划 ID(优先)")
# 配音(可选;传独立配音时严格守卫)
voice_library_id: str = Field(default="", description="统一配音 ID")
voice_library_ids: list[str] = Field(default_factory=list, description="独立配音 ID 列表(长度须=count)")
@model_validator(mode="after")
def _validate(self) -> "VariantPlanRequest":
if not self.template_id.strip() and not self.source_edit_plan_id.strip():
raise ValueError("template_id 与 source_edit_plan_id 至少需要提供一个")
try:
resolve_variant_voice_ids(
count=self.count,
voice_library_id=self.voice_library_id,
voice_library_ids=self.voice_library_ids or None,
)
except VariantVoiceError as exc:
raise ValueError(str(exc)) from exc
return self
class VariantPlanItem(BaseModel):
variant_index: int
plan_id: str
clips: list[dict[str, Any]] = Field(default_factory=list)
class VariantPlanResponse(BaseModel):
items: list[VariantPlanItem]
total: int
@router.post("/variant-plans", response_model=VariantPlanResponse)
def create_variant_plans(
request: VariantPlanRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
db: Session = Depends(get_db_session),
) -> VariantPlanResponse:
"""轻量选片:为 N 个变体创建独立 EditPlan + clips,不建任务/不渲染。
Returns:
200 + {items: [{variant_index, plan_id, clips}], total}
"""
user_id = authenticated_user.user.id
# 配音严格守卫(schema 已校验,此处复用解析取每变体配音)
try:
voices = resolve_variant_voice_ids(
count=request.count,
voice_library_id=request.voice_library_id,
voice_library_ids=request.voice_library_ids or None,
)
except VariantVoiceError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
# 解析源 plan:显式传入优先;否则按 template_id + user 查最新
source_plan_id = request.source_edit_plan_id.strip()
if not source_plan_id and request.template_id.strip():
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_latest = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == request.template_id.strip(),
EditPlanModel.created_by_user_id == user_id,
)
.order_by(EditPlanModel.created_at.desc())
.first()
)
if _latest:
source_plan_id = _latest.id
except Exception:
logger.warning("[variant-plans] 源 plan 解析失败", exc_info=True)
if not source_plan_id:
raise HTTPException(
status_code=400,
detail="缺少剪辑计划:请先完成一次预览生成(或传入 source_edit_plan_id)后再试。",
)
# 配音时长(硬化:异常 → 0.0 不阻断选片)
try:
from app.api.routes.generation_tasks import _query_voice_durations
voice_durations = _query_voice_durations(db, voices)
except Exception:
logger.warning("[variant-plans] 配音时长查询失败(按占位段长选片)", exc_info=True)
voice_durations = [0.0] * request.count
from app.services.edit_plan_service import EditPlanService
svc = EditPlanService(db)
try:
plan_ids = svc.ensure_variant_plans(
source_plan_id,
request.count,
list(dict.fromkeys(request.asset_ids or [])),
created_by_user_id=user_id,
voice_durations=voice_durations,
)
except ValueError as ve:
# 素材池为空/时长全未知等可预期错误 → 400(与正式生成同口径)
logger.warning("[variant-plans] 选片失败: %s", ve)
raise HTTPException(status_code=400, detail=f"变体选片失败:{ve}。请增加素材后重试。") from ve
except HTTPException:
raise
except Exception as e:
logger.error("[variant-plans] 选片异常: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="选片失败,请稍后重试") from e
# 组装 clips 响应
items: list[VariantPlanItem] = []
for idx, pid in enumerate(plan_ids):
clips = svc.list_clips(pid)
clip_dicts = [
{
"id": c.id,
"order": c.order,
"asset_id": c.asset_id,
"start_time": float(c.start_time or 0.0),
"duration": float(c.duration or 0.0),
"clip_type": c.clip_type,
"transition_effect": c.transition_effect,
"transition_duration": float(c.transition_duration or 0.0),
"playback_speed": float(c.playback_speed or 1.0),
"text_content": c.text_content or "",
"status": c.status or "ready",
}
for c in clips
]
items.append(VariantPlanItem(variant_index=idx, plan_id=pid, clips=clip_dicts))
logger.info(
"[variant-plans] 轻量选片完成: user=%s source=%s count=%d plans=%d",
user_id,
source_plan_id,
request.count,
len(plan_ids),
)
return VariantPlanResponse(items=items, total=len(items))
+7 -1
View File
@@ -43,7 +43,13 @@ def submit_ingest_job(
)
)
celery_app.send_task("worker.ingest_asset", args=[job.id])
celery_result = celery_app.send_task("worker.ingest_asset", args=[job.id])
if getattr(celery_result, "id", ""):
try:
job.celery_task_id = celery_result.id
ingest_job_repository.update(job)
except Exception: # noqa: BLE001
pass
return IngestJobResponse(
id=job.id,
+7 -1
View File
@@ -375,7 +375,13 @@ def retry_project_task(
storage_key=job.storage_key,
)
)
celery_app.send_task("worker.ingest_asset", args=[retried.id])
celery_result = celery_app.send_task("worker.ingest_asset", args=[retried.id])
if getattr(celery_result, "id", ""):
try:
retried.celery_task_id = celery_result.id
ingest_job_repository.update(retried)
except Exception: # noqa: BLE001
pass
return ProjectTaskResponse(
id=f"ingest:{retried.id}",
task_type="ingest",
@@ -460,6 +460,11 @@ def _get_template_segments(
except Exception:
logger.warning("旧模板系统查询segments失败", exc_info=True)
# 所有途径都失败:模板没有片段配置(可能是无效测试模板)
logger.error(
"模板无片段配置:template_id=%s(可能是 is_active=false 的无效模板)",
template_id,
)
return []
+278 -58
View File
@@ -85,12 +85,22 @@ def _infer_mime_type_from_storage_key(storage_key: str) -> str:
"""从 storage_key 推断 MIME 类型(与 worker 端保持一致)。"""
lower_filename = storage_key.rsplit("/", 1)[-1].lower()
_MIME_MAP = {
".mov": "video/quicktime", ".mp4": "video/mp4", ".avi": "video/x-msvideo",
".mkv": "video/x-matroska", ".webm": "video/webm",
".png": "image/png", ".gif": "image/gif", ".bmp": "image/bmp",
".svg": "image/svg+xml", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg",
".flac": "audio/flac", ".m4a": "audio/x-m4a",
".mov": "video/quicktime",
".mp4": "video/mp4",
".avi": "video/x-msvideo",
".mkv": "video/x-matroska",
".webm": "video/webm",
".png": "image/png",
".gif": "image/gif",
".bmp": "image/bmp",
".svg": "image/svg+xml",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".mp3": "audio/mpeg",
".wav": "audio/wav",
".ogg": "audio/ogg",
".flac": "audio/flac",
".m4a": "audio/x-m4a",
}
for ext, mime in _MIME_MAP.items():
if lower_filename.endswith(ext):
@@ -98,10 +108,137 @@ def _infer_mime_type_from_storage_key(storage_key: str) -> str:
return "video/mp4" # default
# 兜底去重:无 file_hash / client_upload_id 且大小已知时,同库同名同大小近期活动记录视为重复
FALLBACK_DEDUP_WINDOW_MINUTES = 30
def _find_duplicate_asset(
asset_repository: Any,
*,
library_id: str,
file_hash: str,
client_upload_id: str,
filename: str,
file_size: int = 0,
) -> Any:
"""complete/上传幂等去重,按优先级查找已存在的素材。
1. client_upload_id(客户端幂等 token,同一次上传的重试保持一致)
2. file_hash(内容哈希,不同上传只要内容相同即去重)
3. 兜底(严格模式,宁可漏判不可误杀):file_hash 与 client_upload_id
均缺失、且 file_size > 0 时,同库 + 同文件名 + **同大小** 且 30 分钟内
仍处 uploading/processing 的记录才判重。
- file_hash 非空时跳过兜底(hash 已代表内容;同名但内容全新的视频
如 iPhone 的 IMG_xxxx.MOV 绝不能被同名占位误杀)
- file_size=0(未知)时不允许仅凭同名 + processing 判重,直接放行
全部为鸭子类型调用:旧仓储无对应方法时静默跳过,不破坏既有实现。
"""
if client_upload_id:
find = getattr(asset_repository, "find_by_library_and_client_upload_id", None)
if callable(find):
existing = find(library_id=library_id, client_upload_id=client_upload_id)
if existing is not None:
logger.info(
"素材幂等命中(client_upload_id): library=%s token=%s asset=%s",
library_id,
client_upload_id,
getattr(existing, "id", "?"),
)
return existing
if file_hash:
existing = asset_repository.find_by_library_and_file_hash(
library_id=library_id,
file_hash=file_hash,
)
if existing is not None:
logger.info(
"素材去重命中(file_hash): library=%s hash=%s asset=%s",
library_id,
file_hash,
existing.id,
)
return existing
# 同名兜底去重(最后防线,严格模式):
# - 仅当 file_hash / client_upload_id 均缺失时启用(hash 能代表内容时不靠同名猜)
# - file_size 必须 > 0 且与记录大小严格一致;大小未知(0)直接放行
# - 只命中近期 UPLOADING/PROCESSING 活动记录(READY 历史素材不拦)
if filename and not file_hash and not client_upload_id and file_size and file_size > 0:
find_recent = getattr(asset_repository, "find_recent_active_by_library_and_name", None)
if callable(find_recent):
existing = find_recent(
library_id=library_id,
name=filename,
within_minutes=FALLBACK_DEDUP_WINDOW_MINUTES,
file_size=file_size,
)
if existing is not None:
logger.info(
"素材幂等兜底命中(近期同名同大小活动记录): library=%s name=%s asset=%s status=%s size=%s",
library_id,
filename,
getattr(existing, "id", "?"),
getattr(existing, "status", None),
file_size,
)
return existing
elif filename and not file_hash and not client_upload_id and not file_size:
logger.debug(
"同名兜底去重跳过(file_size 未知,宁可放行不可误杀): library=%s name=%s",
library_id,
filename,
)
return None
def _create_pending_asset(
asset_repository, project_id, library_id, storage_key, filename, mime_type, user_id, file_hash=""
asset_repository,
project_id,
library_id,
storage_key,
filename,
mime_type,
user_id,
file_hash="",
client_upload_id="",
file_size: int = 0,
):
"""立即创建一条 PROCESSING 状态的 Asset 记录,使前端能马上看到新素材。"""
"""立即创建或复用一条 PROCESSING 状态的 Asset 记录。
find-or-create:prepare 阶段已按 file_hash/client_upload_id 预建的占位记录
会被 find_by_library_and_file_hash/find_by_library_and_client_upload_id 命中,
直接复用并补齐字段(避免 pre-create + complete 重复建两条)。
"""
# 1. 按 client_upload_id / file_hash 查找现有记录
existing = None
if client_upload_id:
find_by_cuid = getattr(asset_repository, "find_by_library_and_client_upload_id", None)
if callable(find_by_cuid):
existing = find_by_cuid(library_id=library_id, client_upload_id=client_upload_id)
if existing is None and file_hash:
existing = asset_repository.find_by_library_and_file_hash(library_id=library_id, file_hash=file_hash)
if existing is not None:
# 补齐字段(幂等:避免重复建记录,前端已拿到 asset_id)
changed = False
if file_hash and not existing.file_hash:
existing.file_hash = file_hash
changed = True
if client_upload_id and not existing.client_upload_id:
existing.client_upload_id = client_upload_id
changed = True
if file_size and not existing.file_size:
existing.file_size = file_size
changed = True
if existing.status not in (AssetStatus.PROCESSING, AssetStatus.UPLOADING):
existing.status = AssetStatus.PROCESSING
changed = True
if changed:
try:
asset_repository.update(existing)
except Exception: # noqa: BLE001 — 字段补齐失败不阻塞主流程
pass
return existing
asset = Asset.create(
project_id=project_id,
library_id=library_id,
@@ -111,16 +248,30 @@ def _create_pending_asset(
status=AssetStatus.PROCESSING,
uploaded_by_user_id=user_id,
file_hash=file_hash,
client_upload_id=client_upload_id,
file_size=file_size,
)
return asset_repository.create(asset)
def _persist_celery_task_id(repo: Any, job: Any, celery_task_id: str) -> None:
"""记录 celery 消息 ID 到任务行,供孤儿清理时 revoke/清除队列消息(#1714)。"""
if not celery_task_id:
return
try:
job.celery_task_id = celery_task_id
repo.update(job)
except Exception: # noqa: BLE001 — 记录失败不影响主流程(执行前状态守卫兜底)
pass
def _submit_ingest_job(
project_id: str,
library_id: str,
storage_key: str,
ingest_job_repository: Any,
file_hash: str = "",
asset_id: str = "",
) -> Any:
use_case = SubmitIngestJobUseCase(ingest_job_repository)
job = use_case.execute(
@@ -129,9 +280,11 @@ def _submit_ingest_job(
library_id=library_id,
storage_key=storage_key,
file_hash=file_hash,
asset_id=asset_id,
)
)
celery_app.send_task("worker.ingest_asset", args=[job.id])
celery_result = celery_app.send_task("worker.ingest_asset", args=[job.id])
_persist_celery_task_id(ingest_job_repository, job, getattr(celery_result, "id", ""))
return job
@@ -141,9 +294,15 @@ async def prepare_direct_upload(
authenticated_user: AuthenticatedUser = Depends(get_current_user),
project_repository: Any = Depends(get_project_repository),
asset_library_repository: Any = Depends(get_asset_library_repository),
asset_repository: Any = Depends(get_asset_repository),
storage_service: OSSStorageService = Depends(get_storage_service),
) -> DirectUploadPrepareResponse:
"""创建浏览器直传 OSS 的短期表单签名。"""
"""创建浏览器直传 OSS 的短期表单签名,并在签名前按 file_hash/client_upload_id 去重。
命中去重:直接返回 duplicated=True + skip_transfer=True(前端跳过 OSS 直传),
未命中:正常签名 OSS 并立即预建一条 PROCESSING 状态的 asset 记录占住
file_hash 闸门,响应带 asset_id 供前端/后续 complete 关联。
"""
settings = get_settings()
max_size_bytes = settings.OSS_DIRECT_UPLOAD_MAX_MB * 1024 * 1024
if request.file_size > max_size_bytes:
@@ -162,8 +321,39 @@ async def prepare_direct_upload(
asset_library_repository,
)
file_id = uuid4().hex[:8]
safe_filename = request.filename.replace("/", "_").replace("\\", "_")
# ── prepare 阶段去重:OSS 签名之前先查已存在素材 ──
if request.file_hash or request.client_upload_id:
existing = _find_duplicate_asset(
asset_repository,
library_id=request.library_id,
file_hash=request.file_hash,
client_upload_id=request.client_upload_id,
filename=request.filename,
file_size=request.file_size,
)
if existing is not None:
logger.info(
"prepare 命中去重: library=%s hash=%s cuid=%s existing_asset=%s",
request.library_id,
request.file_hash,
request.client_upload_id,
existing.id,
)
return DirectUploadPrepareResponse(
upload_url="",
method="",
storage_key=existing.storage_key,
expires_at="",
fields={},
max_size_bytes=0,
duplicated=True,
skip_transfer=True,
asset_id=existing.id,
)
file_id = uuid4().hex[:8]
storage_key = f"uploads/{file_id}/{safe_filename}"
try:
payload = storage_service.create_direct_upload_post(
@@ -182,6 +372,27 @@ async def prepare_direct_upload(
detail=f"Failed to prepare upload: {type(error).__name__}",
) from error
# ── 预建 asset 占位:占住 file_hash/client_upload_id 闸门,避免并发重复上传 ──
pending_asset_id = ""
if request.file_hash or request.client_upload_id:
try:
pending = _create_pending_asset(
asset_repository=asset_repository,
project_id=request.project_id,
library_id=request.library_id,
storage_key=storage_key,
filename=safe_filename,
mime_type=validated_content_type,
user_id=authenticated_user.user.id,
file_hash=request.file_hash,
client_upload_id=request.client_upload_id,
file_size=request.file_size,
)
pending_asset_id = pending.id
except Exception as error:
# 预建失败不阻塞签名:complete 仍可按 OSS 文件 + hash 兜底去重
logger.warning("预建 asset 占位失败,降级走 old flow: %s", error)
return DirectUploadPrepareResponse(
upload_url=str(payload["url"]),
method=str(payload["method"]),
@@ -189,6 +400,9 @@ async def prepare_direct_upload(
expires_at=str(payload["expires_at"]),
fields={str(key): str(value) for key, value in dict(payload["fields"]).items()},
max_size_bytes=max_size_bytes,
duplicated=False,
skip_transfer=False,
asset_id=pending_asset_id,
)
@@ -202,7 +416,7 @@ async def complete_direct_upload(
asset_repository: Any = Depends(get_asset_repository),
storage_service: OSSStorageService = Depends(get_storage_service),
) -> DirectUploadCompleteResponse:
"""确认浏览器直传完成并创建导入任务。"""
"""确认浏览器直传完成并创建导入任务(幂等:重复 complete 返回同一素材)。"""
require_project_and_library(
request.project_id,
request.library_id,
@@ -212,6 +426,29 @@ async def complete_direct_upload(
normalized_key = storage_service._normalize_storage_key(request.storage_key)
if not normalized_key.startswith("uploads/"):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid upload key")
filename = normalized_key.rsplit("/", 1)[-1]
# ── 幂等去重(放在 OSS 检查之前):complete 超时后前端重试时,
# 第一次 complete 可能已建好占位记录,此时即使 OSS 检查失败也必须返回
# 已存在记录,绝不能再建第二条。─
existing = _find_duplicate_asset(
asset_repository,
library_id=request.library_id,
file_hash=request.file_hash,
client_upload_id=request.client_upload_id,
filename=filename,
file_size=request.file_size,
)
if existing is not None:
return DirectUploadCompleteResponse(
storage_key=existing.storage_key,
ingest_job_id="",
duplicated=True,
asset_id=existing.id,
url=storage_service.get_url(existing.storage_key),
)
try:
file_exists = storage_service.file_exists(normalized_key)
except Exception as error:
@@ -223,29 +460,7 @@ async def complete_direct_upload(
if not file_exists:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Uploaded file not found")
# ── 素材去重检测:同素材库 + 同 file_hash 视为重复 ──
if request.file_hash:
existing = asset_repository.find_by_library_and_file_hash(
library_id=request.library_id,
file_hash=request.file_hash,
)
if existing is not None:
logger.info(
"素材去重命中: library=%s hash=%s existing_asset=%s",
request.library_id,
request.file_hash,
existing.id,
)
return DirectUploadCompleteResponse(
storage_key=normalized_key,
ingest_job_id="",
duplicated=True,
asset_id=existing.id,
url=storage_service.get_url(normalized_key),
)
# 立即创建 Asset 记录(PROCESSING 状态),使前端刷新后即可看到新素材
filename = normalized_key.rsplit("/", 1)[-1]
mime_type = _infer_mime_type_from_storage_key(normalized_key)
pending_asset = _create_pending_asset(
asset_repository=asset_repository,
@@ -256,6 +471,8 @@ async def complete_direct_upload(
mime_type=mime_type,
user_id=authenticated_user.user.id,
file_hash=request.file_hash,
client_upload_id=request.client_upload_id,
file_size=request.file_size,
)
job = _submit_ingest_job(
@@ -264,6 +481,7 @@ async def complete_direct_upload(
storage_key=normalized_key,
ingest_job_repository=ingest_job_repository,
file_hash=request.file_hash,
asset_id=pending_asset.id,
)
return DirectUploadCompleteResponse(
storage_key=normalized_key,
@@ -283,7 +501,8 @@ async def upload_asset(
project_id: str = Form(..., min_length=1, description="项目 ID"),
library_id: str = Form(..., min_length=1, description="素材库 ID"),
file: UploadFile = File(..., description="要上传的文件(视频、音频、图片等)"),
file_hash: str = Form(default="", description="文件 MD5 哈希,用于去重检测"),
file_hash: str = Form(default="", description="文件哈希,用于去重检测"),
client_upload_id: str = Form(default="", description="客户端幂等 token(同一次上传的重试保持一致)"),
authenticated_user: AuthenticatedUser = Depends(get_current_user),
ingest_job_repository: Any = Depends(get_ingest_job_repository),
project_repository: Any = Depends(get_project_repository),
@@ -294,32 +513,31 @@ async def upload_asset(
"""上传素材文件并触发导入流水线。"""
require_project_and_library(project_id, library_id, project_repository, asset_library_repository)
# ── 素材去重检测:上传前检查同素材库 + 同 file_hash ──
if file_hash:
existing = asset_repository.find_by_library_and_file_hash(
library_id=library_id,
file_hash=file_hash,
)
if existing is not None:
logger.info(
"素材去重命中(multipart): library=%s hash=%s existing_asset=%s",
library_id,
file_hash,
existing.id,
)
return UploadAssetResponse(
storage_key=existing.storage_key,
ingest_job_id="",
url="",
duplicated=True,
asset_id=existing.id,
)
# P2-5: 服务端验证 MIME 类型
# P2-5: 服务端验证 MIME 类型(先验证,再幂等去重,避免非法类型绕过)
validated_content_type = _validate_mime_type(file.content_type)
file_id = uuid4().hex[:8]
safe_filename = file.filename.replace("/", "_").replace("\\", "_") if file.filename else "unknown"
# ── 幂等去重:client_upload_id → file_hash → 近期活动同名记录兜底 ──
# 放在 OSS 上传之前:重复提交直接返回,不占 OSS 流量、不建新记录。
existing = _find_duplicate_asset(
asset_repository,
library_id=library_id,
file_hash=file_hash,
client_upload_id=client_upload_id,
filename=safe_filename,
file_size=0,
)
if existing is not None:
return UploadAssetResponse(
storage_key=existing.storage_key,
ingest_job_id="",
url="",
duplicated=True,
asset_id=existing.id,
)
file_id = uuid4().hex[:8]
storage_key = f"uploads/{file_id}/{safe_filename}"
try:
@@ -348,6 +566,7 @@ async def upload_asset(
mime_type=validated_content_type,
user_id=authenticated_user.user.id,
file_hash=file_hash,
client_upload_id=client_upload_id,
)
job = _submit_ingest_job(
@@ -356,6 +575,7 @@ async def upload_asset(
storage_key=storage_key,
ingest_job_repository=ingest_job_repository,
file_hash=file_hash,
asset_id=pending_asset.id,
)
return UploadAssetResponse(
+8
View File
@@ -5,3 +5,11 @@ settings = get_settings()
celery_app = Celery("xiaoxia-saas-api")
celery_app.conf.broker_url = settings.CELERY_BROKER_URL
celery_app.conf.result_backend = settings.CELERY_RESULT_BACKEND
# #1714 队列隔离:视频生成走 generation 队列,素材转码走 transcode 队列
try:
from packages.shared.celery_queues import apply_queue_settings
apply_queue_settings(celery_app)
except Exception: # noqa: BLE001 — 队列配置失败不阻断 API 启动
pass
+11 -1
View File
@@ -279,7 +279,17 @@ def safe_enqueue_generation_task(
# ── 发送 Celery 任务 ──
try:
celery_app.send_task("worker.generate_video", args=[task.id])
celery_result = celery_app.send_task("worker.generate_video", args=[task.id])
# 记录 celery 消息 ID:孤儿清理/超时作废时据此 revoke + 清除队列消息(#1714)
celery_task_id = getattr(celery_result, "id", "")
if celery_task_id:
try:
task.celery_task_id = celery_task_id
generation_task_repository.update(task)
except Exception as persist_err: # noqa: BLE001
logger.warning(
"%s 持久化 celery_task_id 失败(不影响主流程): task_id=%s err=%s", log_prefix, task.id, persist_err
)
except Exception as e:
logger.error(
"%s 入队失败,标记为失败: task_id=%s error=%s",
+44 -10
View File
@@ -36,9 +36,13 @@ class CreateGenerationTaskRequest(BaseModel):
template_id: str = ""
asset_ids: list[str] = Field(default_factory=list)
title_ids: list[str] = Field(default_factory=list)
voice_ids: list[str] = Field(default_factory=list)
# ── 来源剪辑计划 ──
source_edit_plan_id: str = ""
# ── variant-plans 轻量选片回传(#1749):正式生成直接复用,不再重选 ──
variant_plan_ids: list[str] = Field(
default_factory=list,
description="POST /generation/variant-plans 返回的各变体 plan_id(长度须=count);为空则走服务端选片",
)
# ── 标题配置(结构化)──
title_config: dict | None = Field(
default=None,
@@ -96,11 +100,31 @@ class CreateGenerationTaskRequest(BaseModel):
@model_validator(mode="after")
def _check_variant_arrays(self) -> "CreateGenerationTaskRequest":
"""变体数组字段长度校验:空数组(回退单值)、长度 1(共用)、或长度 = count(独立)。"""
for name in ("voice_library_ids", "cover_urls", "titles"):
"""变体数组字段长度校验 + #1749 配音严格守卫。
- cover_urls/titles:空(回退单值)、长度 1(共用)或长度 = count(独立);
- voice_library_ids:独立配音长度必须恰好 = count 且逐项非空,禁止静默 fallback
(长度 1 的"共用"场景请用 voice_library_id 单值字段);
- variant_plan_ids:非空时长度必须 = count。
"""
for name in ("cover_urls", "titles"):
arr = getattr(self, name)
if arr and len(arr) != 1 and len(arr) != self.count:
raise ValueError(f"{name} 长度必须为 1(共用)或 {self.count}(与 count 一致),当前为 {len(arr)}")
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
try:
resolve_variant_voice_ids(
count=self.count,
voice_library_id=self.voice_library_id,
voice_library_ids=self.voice_library_ids or None,
)
except VariantVoiceError as exc:
raise ValueError(str(exc)) from exc
if self.variant_plan_ids and len(self.variant_plan_ids) != self.count:
raise ValueError(f"variant_plan_ids 长度({len(self.variant_plan_ids)})必须与 count({self.count})一致")
return self
@model_validator(mode="after")
@@ -110,9 +134,9 @@ class CreateGenerationTaskRequest(BaseModel):
if not has_project and not has_template:
raise ValueError("project_id 或 template_id 至少需要提供一个")
has_library = bool(self.asset_library_id.strip())
has_assets = bool(self.asset_ids or self.title_ids or self.voice_ids)
has_assets = bool(self.asset_ids or self.title_ids)
if not has_library and not has_assets:
raise ValueError("asset_library_id 或 asset_ids/title_ids/voice_ids 至少需要提供一个")
raise ValueError("asset_library_id 或 asset_ids/title_ids 至少需要提供一个")
return self
@@ -189,7 +213,6 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
template_id: str
asset_ids: list[str] = Field(default_factory=list)
title_ids: list[str] = Field(default_factory=list)
voice_ids: list[str] = Field(default_factory=list)
voice_library_id: str = Field(
default="", description="配音素材库ID(用户上传的音频或AI配音),对应配音选择页面选择的配音素材"
)
@@ -231,13 +254,24 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
@model_validator(mode="after")
def _check_variant_arrays(self) -> "CreatePreviewGenerationTaskRequest":
"""变体数组字段长度校验:空数组(回退单值)、长度 1(共用)、或长度 = preview_count(独立)。"""
for name in ("titles", "voice_library_ids", "cover_urls"):
"""变体数组字段长度校验 + #1749 配音严格守卫。"""
for name in ("titles", "cover_urls"):
arr = getattr(self, name)
if arr and len(arr) != 1 and len(arr) != self.preview_count:
raise ValueError(
f"{name} 长度必须为 1(共用)或 {self.preview_count}(与 preview_count 一致),当前为 {len(arr)}"
)
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
try:
resolve_variant_voice_ids(
count=self.preview_count,
voice_library_id=self.voice_library_id,
voice_library_ids=self.voice_library_ids or None,
)
except VariantVoiceError as exc:
raise ValueError(str(exc)) from exc
return self
@model_validator(mode="after")
@@ -248,8 +282,8 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
@model_validator(mode="after")
def _check_asset_ids(self) -> "CreatePreviewGenerationTaskRequest":
if not self.asset_ids and not self.title_ids and not self.voice_ids:
raise ValueError("asset_ids/title_ids/voice_ids 至少需要提供一个")
if not self.asset_ids and not self.title_ids:
raise ValueError("asset_ids/title_ids 至少需要提供一个")
return self
+11 -5
View File
@@ -16,6 +16,7 @@ class DirectUploadPrepareRequest(BaseModel):
content_type: str = Field(default="application/octet-stream", min_length=1, max_length=100)
file_size: int = Field(..., gt=0)
file_hash: str = Field(default="", max_length=64, description="文件 MD5 哈希,用于去重检测")
client_upload_id: str = Field(default="", max_length=64, description="客户端幂等 token(同一次上传的重试保持一致)")
class DirectUploadPrepareResponse(BaseModel):
@@ -25,20 +26,25 @@ class DirectUploadPrepareResponse(BaseModel):
expires_at: str
fields: dict[str, str]
max_size_bytes: int
duplicated: bool = False
skip_transfer: bool = False
asset_id: str = ""
class DirectUploadCompleteRequest(BaseModel):
project_id: str = Field(..., min_length=1)
library_id: str = Field(..., min_length=1)
storage_key: str = Field(..., min_length=1, max_length=255)
file_hash: str = Field(default="", max_length=64, description="文件 MD5 哈希,用于去重检测")
file_hash: str = Field(default="", max_length=64, description="文件哈希,用于去重检测")
client_upload_id: str = Field(default="", max_length=64, description="客户端幂等 token(同一次上传的重试保持一致)")
file_size: int = Field(default=0, ge=0, description="文件大小(字节),用于无 hash 时的兜底去重")
class DirectUploadCompleteResponse(BaseModel):
storage_key: str
ingest_job_id: str
duplicated: bool = Field(default=False, description="是否为重复素材(命中去重)")
asset_id: str = Field(default="", description="重复素材的 asset_id(duplicated=true 时返回)")
duplicated: bool = Field(default=False, description="是否为重复素材/重复 complete(命中幂等去重)")
asset_id: str = Field(default="", description="素材 asset_id(重复 complete 时返回已存在记录)")
url: str = Field(default="", description="Public URL of uploaded file")
@@ -46,5 +52,5 @@ class UploadAssetResponse(BaseModel):
storage_key: str
ingest_job_id: str
url: str = Field(..., description="Public URL of uploaded file")
duplicated: bool = Field(default=False, description="是否为重复素材(命中去重)")
asset_id: str = Field(default="", description="重复素材的 asset_id(duplicated=true 时返回)")
duplicated: bool = Field(default=False, description="是否为重复素材/重复提交(命中幂等去重)")
asset_id: str = Field(default="", description="素材 asset_id(重复提交时返回已存在记录)")
+395
View File
@@ -409,8 +409,13 @@ class EditPlanService:
clip_type=clip_item.get("clip_type", "main"),
order=order,
asset_id=clip_item.get("asset_id", ""),
text_content=clip_item.get("text_content", ""),
start_time=clip_item.get("start_time", 0.0),
duration=clip_item.get("duration", 0.0),
transition_effect=clip_item.get("transition_effect", "cut"),
transition_duration=clip_item.get("transition_duration", 0.0),
playback_speed=clip_item.get("playback_speed", 1.0),
config=clip_item.get("config") or None,
)
model = EditPlanClipModel(
id=clip.id,
@@ -459,6 +464,159 @@ class EditPlanService:
logger.exception("事务性替换片段失败: plan_id=%s", plan_id)
raise
def reselect_plan_for_variant(
self,
source_plan_id: str,
candidate_asset_ids: list[str],
*,
created_by_user_id: str = "",
name_suffix: str = "变体",
voice_duration: float = 0.0,
rng=None,
) -> EditPlan:
"""为批量变体生成独立 plan:完整重跑单视频选片流程(#1743)。
与 clone_plan_for_variant(只重算起点、素材/顺序不变)不同,本方法:
- 源 plan 片段骨架(clip_type/order/duration/文案/转场)保留;
- 素材池 shuffle 随机分配 + main 片段顺序洗牌;
- 起点走场景镜头洗牌/随机起点/历史区间避让(与单视频同一入口);
- 批次内同素材区间重叠 >20% 自动重选起点;
- 新片段区间 record_used_segments 写回素材 metadata(跨变体/跨任务避让)。
Args:
source_plan_id: 源 plan(任务 0 / 预览源)。
candidate_asset_ids: 素材池(源 plan 素材 ∪ 批次素材)。
created_by_user_id: 新 plan 归属用户。
name_suffix: plan 名后缀。
rng: 可选随机数(测试注入种子)。
Raises:
ValueError: 源 plan 不存在/无片段、素材池为空或时长全未知。
"""
from packages.adapters.sqlalchemy_impl.models import AssetModel
from packages.domain.plan_generator_utils import extract_scene_points_from_metadata
from packages.domain.variant_plan_selector import reselect_clips_for_variant
source = self.get_plan_or_raise(source_plan_id)
# 分页读取源 plan 全部片段
clips: List[EditPlanClip] = []
skip, page = 0, 500
while True:
batch = self._clip_repo.list_by_plan(source_plan_id, skip=skip, limit=page)
if not batch:
break
clips.extend(batch)
if len(batch) < page:
break
skip += page
if not clips:
raise ValueError(f"源 plan 无片段,无法生成变体: {source_plan_id}")
source_clips_data: list[dict[str, Any]] = [
{
"order": c.order if c.order is not None else i,
"asset_id": c.asset_id,
"start_time": float(c.start_time or 0.0),
"duration": float(c.duration or 0.0),
"clip_type": c.clip_type,
"playback_speed": float(c.playback_speed or 1.0),
"transition_effect": c.transition_effect,
"transition_duration": float(c.transition_duration or 0.0),
"text_content": c.text_content or "",
"config": c.config or {},
}
for i, c in enumerate(clips)
]
db = self._clip_repo.session
# #1749:配音时长 → 每段目标段长(片段数=模板片段数定死;素材不足由渲染末帧冻结铺满)
target_durations: list[float] | None = None
try:
voice = float(voice_duration or 0.0)
except (TypeError, ValueError):
voice = 0.0
if voice > 0 and source_clips_data:
from packages.domain.voice_duration_planner import plan_clip_durations
_effects: list[str | None] = [c.get("transition_effect") for c in source_clips_data]
_tdurs: list[float] = [float(c.get("transition_duration") or 0.0) for c in source_clips_data]
target_durations = plan_clip_durations(
len(source_clips_data),
voice,
transition_effects=_effects,
transition_durations=_tdurs,
)
if target_durations:
for _c, _d in zip(source_clips_data, target_durations, strict=False):
_c["duration"] = _d
# 素材池 = 源 plan 素材 ∪ 调用方传入素材(去重保序)
pool_ids: list[str] = []
seen = set()
for aid in [c.asset_id for c in clips if c.asset_id] + list(candidate_asset_ids or []):
if aid and aid not in seen:
seen.add(aid)
pool_ids.append(aid)
# 时长 + 场景点
durations: dict[str, float] = {}
scene_points: dict[str, list[float]] = {}
if pool_ids:
for m in db.query(AssetModel).filter(AssetModel.id.in_(pool_ids)).all():
durations[m.id] = float(getattr(m, "duration", 0.0) or 0.0)
pts = extract_scene_points_from_metadata(getattr(m, "metadata", None))
if pts:
scene_points[m.id] = pts
historical = get_used_segments(db, pool_ids)
# 创建新 plan(复制模板归属与 config)
new_plan = self.create_plan(
template_id=source.template_id,
name=f"{source.name or '剪辑计划'} · {name_suffix}",
config=dict(source.config or {}),
total_duration=source.total_duration,
project_id=source.project_id or "",
created_by_user_id=created_by_user_id or (source.created_by_user_id or ""),
)
# 批次内区间:以源 plan(变体 0)片段为初始避让对象
batch_segments: dict[str, list[tuple[float, float]]] = {}
for c in clips:
if c.asset_id and float(c.duration or 0) > 0:
st = float(c.start_time or 0.0)
batch_segments.setdefault(c.asset_id, []).append((st, st + float(c.duration)))
clips_data = reselect_clips_for_variant(
source_clips_data,
pool_ids,
asset_durations=durations,
asset_scene_points=scene_points,
historical_used_segments=historical,
batch_segments=batch_segments,
target_durations=target_durations,
rng=rng,
)
# 片段区间写回素材 metadata(与落库同事务;replace_all_clips_transactional 内 commit)
for item in clips_data:
aid = item.get("asset_id", "")
if aid:
st = float(item.get("start_time", 0.0))
record_used_segments(db, aid, st, st + float(item.get("duration", 0.0)), new_plan.id)
self.replace_all_clips_transactional(new_plan.id, clips_data)
logger.info(
"变体独立选片完成: source=%s new=%s clips=%d assets=%d",
source_plan_id,
new_plan.id,
len(clips_data),
len(pool_ids),
)
return new_plan
def clone_plan_for_variant(
self,
source_plan_id: str,
@@ -569,6 +727,243 @@ class EditPlanService:
)
return new_plan
# ── #1749 配音时长分配 / 素材时长查询 / 批量变体 plan 确保 ──────────────
def get_asset_durations(self, asset_ids: list[str]) -> dict[str, float]:
"""批量查询素材时长(秒),O(N) 单查;缺失/异常返回 0.0。"""
from packages.adapters.sqlalchemy_impl.models import AssetModel
ids = [a for a in dict.fromkeys(asset_ids or []) if a]
if not ids:
return {}
db = self._clip_repo.session
out: dict[str, float] = {}
for m in db.query(AssetModel).filter(AssetModel.id.in_(ids)).all():
try:
out[m.id] = float(getattr(m, "duration", 0.0) or 0.0)
except (TypeError, ValueError):
out[m.id] = 0.0
return out
def apply_voice_duration_to_plan(self, plan_id: str, voice_duration: float) -> Optional[EditPlan]:
"""把配音时长分配到 plan 的每段(#1749)。
- 片段数保持不变(= 模板片段数,定死);
- 每段 duration 按 voice_duration_planner 分配(含转场重叠扣减);
- 素材短于段长 → start_time 钳制为 0(末帧冻结由渲染侧 tpad/apad 铺满);
- plan.total_duration 回写为成片净时长(≈ 配音时长);
- 幂等:配音时长相同则分配结果不变,可重复调用。
无配音(<=0)或无片段时直接返回 None,不报错。
"""
try:
voice = float(voice_duration or 0.0)
except (TypeError, ValueError):
return None
if voice <= 0:
return None
plan = self.get_plan(plan_id)
if plan is None:
return None
clips: List[EditPlanClip] = []
skip, page = 0, 500
while True:
batch = self._clip_repo.list_by_plan(plan_id, skip=skip, limit=page)
if not batch:
break
clips.extend(batch)
if len(batch) < page:
break
skip += page
if not clips:
return None
clips.sort(key=lambda c: (c.order if c.order is not None else 0))
from packages.domain.voice_duration_planner import plan_clip_durations, total_output_duration
# #1764:从 plan config 读取节奏模板
rhythm_template = None
if plan and hasattr(plan, "config") and plan.config:
rhythm_template = plan.config.get("rhythm_template")
target = plan_clip_durations(
len(clips),
voice,
transition_effects=[c.transition_effect for c in clips],
transition_durations=[float(c.transition_duration or 0.0) for c in clips],
rhythm_template=rhythm_template,
)
if not target:
return None
# 素材时长(短素材起点钳 0)
asset_ids = [c.asset_id for c in clips if c.asset_id]
durations = self.get_asset_durations(asset_ids)
clips_data: list[dict] = []
for i, c in enumerate(clips):
dur = float(target[i])
total = durations.get(c.asset_id, 0.0)
start = float(c.start_time or 0.0)
if c.asset_id and total > 0:
# 素材短于段长:起点钳 0,段长超出部分渲染侧末帧冻结
max_start = max(0.0, total - min(dur, total))
start = min(start, max_start)
clips_data.append(
{
"order": c.order if c.order is not None else i,
"asset_id": c.asset_id or "",
"start_time": round(start, 3),
"duration": dur,
"clip_type": c.clip_type,
"playback_speed": float(c.playback_speed or 1.0),
"transition_effect": c.transition_effect,
"transition_duration": float(c.transition_duration or 0.0),
"text_content": c.text_content or "",
"config": c.config or {},
}
)
self.replace_all_clips_transactional(plan_id, clips_data)
net = total_output_duration(
target,
transition_effects=[c.transition_effect for c in clips],
transition_durations=[float(c.transition_duration or 0.0) for c in clips],
)
try:
plan.total_duration = net
db = self._clip_repo.session
db.commit()
except Exception:
db.rollback()
logger.exception("回写 plan.total_duration 失败(不阻断): plan_id=%s", plan_id)
logger.info(
"配音时长分配完成: plan=%s clips=%d voice=%.2fs 成片净时长=%.2fs",
plan_id,
len(clips),
voice,
net,
)
return plan
def ensure_variant_plans(
self,
source_plan_id: str,
count: int,
candidate_asset_ids: list[str],
*,
created_by_user_id: str = "",
voice_durations: Optional[list[float]] = None,
rng=None,
) -> list[str]:
"""确保批量 N 个变体各自拥有独立 plan(#1749 批量正式生成/预览共用)。
- 变体 0:clone 源 plan(不污染源 plan,片段独立可改),并按配音分配段长;
- 变体 1..N-1:reselect_plan_for_variant 完整重跑选片(素材级去重);
- voice_durations:每个变体的配音时长(独立配音各自时长;统一配音同值);
缺省/为 0 时不分配(段长保持骨架/模板值)。
Returns:
plan_id 列表,长度 == count,index 即 variant_index。
"""
import random as _random
rng = rng or _random.Random()
plan_ids: list[str] = []
# 变体 0:clone(片段结构同源 plan,起点重算),不污染源 plan
plan0 = self.clone_plan_for_variant(
source_plan_id,
created_by_user_id=created_by_user_id,
name_suffix="变体1",
)
v0_voice = 0.0
if voice_durations and len(voice_durations) > 0:
try:
v0_voice = float(voice_durations[0] or 0.0)
except (TypeError, ValueError):
v0_voice = 0.0
if v0_voice > 0:
try:
self.apply_voice_duration_to_plan(plan0.id, v0_voice)
except Exception:
logger.exception("变体0 配音分配失败(不阻断): plan=%s", plan0.id)
plan_ids.append(plan0.id)
# 变体 1..N-1:独立选片
for i in range(1, count):
voice = 0.0
if voice_durations and i < len(voice_durations):
try:
voice = float(voice_durations[i] or 0.0)
except (TypeError, ValueError):
voice = 0.0
variant = self.reselect_plan_for_variant(
source_plan_id,
candidate_asset_ids,
created_by_user_id=created_by_user_id,
name_suffix=f"变体{i + 1}",
voice_duration=voice,
rng=rng,
)
plan_ids.append(variant.id)
# #1764:为每个变体生成独立节奏模板(让批量视频片段时长分布不同)
from packages.domain.voice_duration_planner import RHYTHM_TEMPLATES, adapt_template_length
clip_count = 0
if voice_durations and len(voice_durations) > 0:
# 从源 plan 获取片段数
source_plan = self.get_plan(source_plan_id)
if source_plan and hasattr(source_plan, "clips"):
clip_count = len(list(source_plan.clips)) if source_plan.clips else 0
rhythm_templates_for_variants = []
if clip_count > 0:
for idx in range(len(plan_ids)):
# 每个变体用不同的 seed 选择节奏模板
variant_seed = rng.randint(0, 999999)
template = adapt_template_length(RHYTHM_TEMPLATES[variant_seed % len(RHYTHM_TEMPLATES)], clip_count)
rhythm_templates_for_variants.append(template)
logger.info("变体 %d 节奏模板: plan=%s template=%s", idx, plan_ids[idx], template)
# 为每个变体生成独立视觉扰动参数(让批量视频画面本身更不同)
from packages.domain.variant_plan_selector import generate_visual_perturbation
for idx, pid in enumerate(plan_ids):
try:
perturbation = generate_visual_perturbation(rng)
# 变体 0 不做 hflip(保持预览 plan 原始画面方向)
if idx == 0:
perturbation["hflip"] = False
config_update = {"visual_perturbation": perturbation}
# #1764:写入节奏模板
if idx < len(rhythm_templates_for_variants):
config_update["rhythm_template"] = rhythm_templates_for_variants[idx]
# #1765:写入像素级扰动滤镜
from packages.domain.variant_plan_selector import generate_pixel_perturbation
pixel_pert = generate_pixel_perturbation(rng)
config_update["pixel_perturbation"] = pixel_pert
self.update_plan_config(pid, config_update)
logger.info("变体 %d 视觉扰动+像素扰动: plan=%s vis=%s pix=%s", idx, pid, perturbation, pixel_pert)
except Exception:
logger.exception("变体 %d 视觉扰动生成失败(不阻断): plan=%s", idx, pid)
# 标记所有变体 plan 的 clips 为 ready(已分配素材+起点,语义上就是 ready)
for pid in plan_ids:
try:
self.mark_clips_ready(pid)
except Exception:
logger.exception("标记 clips ready 失败(不阻断): plan=%s", pid)
return plan_ids
# ── 片段分割与合并 ──────────────────────────────────────────────────────
def split_clip(self, clip_id: str, split_time: float) -> Dict[str, Any]:
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#3b82f6"/>
<text x="32" y="44" font-size="34" text-anchor="middle">🦐</text>
</svg>

After

Width:  |  Height:  |  Size: 194 B

+11
View File
@@ -139,6 +139,17 @@ export interface DirectUploadPrepareResult {
* 旧后端不返回该字段,前端降级为无预建卡片的原有行为。
*/
asset_id?: string
/**
* 后端 file_hash 命中素材库已有相同文件时为 true,前端应跳过 transfer + complete 阶段
* 直接按「去重命中」处理(不调 transfer、不调 complete、立即刷新素材列表)。
* 旧后端不返回该字段,前端降级为走老流程。
*/
duplicated?: boolean
/**
* 与 duplicated 语义一致:true 表示跳过传输,前端据此短路。
* 两个字段是同一语义的别名(后端可能只返回其一),前端任意为 true 即视为命中去重。
*/
skip_transfer?: boolean
}
/** 直传完成确认返回 */
+58 -4
View File
@@ -4,6 +4,7 @@
import apiClient from "../client"
import { getOrCreateDefaultProject } from "../projects"
import type { DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types"
import { computeFileHash, makeClientUploadId } from "./uploadDedup"
/** 预签名直传准备 */
export const prepareDirectUpload = async (data: {
@@ -12,8 +13,13 @@ export const prepareDirectUpload = async (data: {
filename: string
content_type: string
file_size: number
/** 前端算好的文件内容哈希(SHA-256 hex),打开后端 file_hash 去重闸门 */
file_hash?: string
/** 前端生成的上传幂等 token,同一次逻辑上传(含重试)保持不变 */
client_upload_id?: string
}): Promise<DirectUploadPrepareResult> => {
const response = await apiClient.post("/upload/direct/prepare", data)
// prepare 单独放宽到 30s(全局 axios 实例只有 10s,staging 抖动时易超时)
const response = await apiClient.post("/upload/direct/prepare", data, { timeout: 30_000 })
return response.data
}
@@ -22,8 +28,16 @@ export const completeDirectUpload = async (data: {
project_id: string
library_id: string
storage_key: string
/** 前端算好的文件内容哈希(与 prepare 一致),后端按 hash 幂等去重 */
file_hash?: string
/** 前端上传幂等 token(与 prepare 一致),同一次上传重发 complete 不重复建记录 */
client_upload_id?: string
/** 文件字节数;后端同名兜底去重需用它做大小校验,缺失(=0)时同名记录一律不判重 */
file_size?: number
}): Promise<DirectUploadCompleteResult> => {
const response = await apiClient.post("/upload/direct/complete", data)
// complete 内含 OSS 存在性检查 + 建库 + 派单,放宽到 60s;
// 超时不代表失败(记录可能已建成),调用方禁止超时后盲目重传整个文件
const response = await apiClient.post("/upload/direct/complete", data, { timeout: 60_000 })
return response.data
}
@@ -109,8 +123,20 @@ export interface DirectUploadHandle {
export const prepareDirectUploadHandle = async (data: {
file: File
library_id: string
/** 前端算好的文件内容哈希(SHA-256 hex),prepare/complete 均携带 */
fileHash?: string
/** 本次逻辑上传的幂等 token,prepare/complete 一致、重试复用 */
clientUploadId?: string
}): Promise<DirectUploadHandle> => {
const project = await getOrCreateDefaultProject()
// 默认项目初始化失败(项目列表接口异常/自动创建失败)给出独立、明确的提示,
// 不与 prepare 的签名接口错误混在一起
let project: Awaited<ReturnType<typeof getOrCreateDefaultProject>>
try {
project = await getOrCreateDefaultProject()
} catch (err) {
const reason = err instanceof Error ? err.message : "网络异常"
throw new Error(`初始化默认项目失败,无法开始上传:${reason}`)
}
const prepared = await prepareDirectUpload({
project_id: project.id,
@@ -118,6 +144,8 @@ export const prepareDirectUploadHandle = async (data: {
filename: data.file.name,
content_type: data.file.type || "application/octet-stream",
file_size: data.file.size,
file_hash: data.fileHash,
client_upload_id: data.clientUploadId,
})
return {
@@ -128,6 +156,10 @@ export const prepareDirectUploadHandle = async (data: {
project_id: project.id,
library_id: data.library_id,
storage_key: prepared.storage_key,
file_hash: data.fileHash,
client_upload_id: data.clientUploadId,
// 透传文件字节数:后端同名兜底去重依赖大小校验,缺省会导致同名新视频被误判重复
file_size: data.file.size,
}),
}
}
@@ -137,8 +169,30 @@ export const uploadAssetDirect = async (data: {
file: File
library_id: string
onProgress?: (percent: number) => void
/** 文件内容哈希;未传时自动补算(配音/封面/克隆等非队列链路统一受益) */
fileHash?: string
/** 幂等 token;未传时自动生成 */
clientUploadId?: string
}): Promise<DirectUploadCompleteResult> => {
const handle = await prepareDirectUploadHandle({ file: data.file, library_id: data.library_id })
// 自动补算哈希与幂等 token:确保 file_hash 去重闸门对所有上传链路生效
const fileHash = data.fileHash ?? (await computeFileHash(data.file))
const clientUploadId = data.clientUploadId ?? makeClientUploadId()
const handle = await prepareDirectUploadHandle({
file: data.file,
library_id: data.library_id,
fileHash,
clientUploadId,
})
// prepare 阶段后端 file_hash 命中素材库已有相同文件:跳过 transfer + complete
if (handle.prepared.skip_transfer || handle.prepared.duplicated) {
return {
storage_key: handle.prepared.storage_key,
ingest_job_id: "",
url: "",
duplicated: true,
asset_id: handle.prepared.asset_id,
}
}
await handle.transfer(data.onProgress)
return handle.complete()
}
+143
View File
@@ -0,0 +1,143 @@
/**
* 上传去重 / 幂等工具(Issue #1714)
*
* 背景:同一文件被反复入队、complete 超时后盲目重传,导致后端创建大量重复
* PROCESSING 素材记录。本模块提供两类纯函数:
*
* 1. 文件指纹:
* - makeFileFingerprint():文件名+大小+lastModified,入队去重用(同步、零开销)
* - computeFileHash():SHA-256 内容哈希(小文件全量、大文件抽样头尾),
* prepare/complete 时发给后端打开 file_hash 去重闸门
* 2. 队列去重:findDuplicateInQueue() 判断文件是否已在队列中
* 3. 幂等 token:makeClientUploadId() 生成上传幂等 ID(每次"一次逻辑上传"一个,
* 重试复用同一 ID,重新入队才生成新 ID)
*/
/** 全量哈希阈值:≤64MB 全量读入计算;超过即走头尾抽样,避免 100~256MB 视频被整文件读进内存卡死页面 */
export const HASH_FULL_READ_LIMIT = 64 * 1024 * 1024 // 64MB
/** 抽样读取的头尾片段大小(各 16MB) */
export const HASH_SAMPLE_CHUNK = 16 * 1024 * 1024
/** 计算指纹时,文件在队列中已存在的状态(已失败的可以重试,不算重复) */
export type DedupExcludeStatus = "error" | "done"
/**
* 文件入队指纹:同库 + 文件名 + 大小 + 修改时间。
* 同一文件(File 对象由 <input> 重选或拖拽重复触发时三个字段均一致)稳定复现;
* 不同文件极小概率碰撞时可由后端 file_hash 内容去重兜底。
*/
export function makeFileFingerprint(file: Pick<File, "name" | "size" | "lastModified">): string {
return `${file.name}::${file.size}::${file.lastModified}`
}
/**
* 在现有队列项中查找同一文件的在途记录。
* 已失败(error)的项允许重试路径复用、已完成(done)的可跳过;
* 处于 preparing/uploading/ingesting 的在途项一律视为重复,禁止重复入队。
*
* 返回命中的队列项 id(tempId),未命中返回 null。
*/
export function findDuplicateInQueue<T extends { fileKey: string; status: string }>(
queue: T[],
fileKey: string,
excludeStatuses: DedupExcludeStatus[] = [],
): T | null {
const exclude = new Set<string>(excludeStatuses)
return queue.find((it) => it.fileKey === fileKey && !exclude.has(it.status)) ?? null
}
/** 生成上传幂等 token:一次"逻辑上传"一个,重试复用、重新入队换新 */
export function makeClientUploadId(): string {
const rand =
typeof crypto !== "undefined" && "randomUUID" in crypto
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${Math.random()
.toString(36)
.slice(2, 10)}`
return `up_${Date.now().toString(36)}_${rand.replace(/-/g, "").slice(0, 16)}`
}
/** 读取 Blob/File 片段为 ArrayBuffer:优先 Blob.arrayBuffer(),老环境回退 FileReader */
function readAsArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
if (typeof blob.arrayBuffer === "function") {
return blob.arrayBuffer()
}
return new Promise<ArrayBuffer>((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as ArrayBuffer)
reader.onerror = () => reject(reader.error ?? new Error("FileReader read failed"))
reader.readAsArrayBuffer(blob)
})
}
/**
* 把 buffer 复制到当前 JS realm 的 Uint8Array 再哈希。
* jsdom/测试环境中 Blob.arrayBuffer() 可能返回另一 realm 的 ArrayBuffer,
* Node WebCrypto 的 WebIDL instanceof 校验会拒绝跨 realm 参数。
*/
async function digestSha256(buffer: ArrayBuffer): Promise<ArrayBuffer> {
const subtle =
typeof globalThis !== "undefined" && globalThis.crypto ? globalThis.crypto.subtle : null
if (!subtle) throw new Error("crypto.subtle unavailable")
const local = new Uint8Array(buffer.byteLength)
local.set(new Uint8Array(buffer))
return subtle.digest("SHA-256", local)
}
function toHex(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let hex = ""
for (let i = 0; i < bytes.length; i += 1) {
hex += bytes[i].toString(16).padStart(2, "0")
}
return hex
}
/**
* 计算文件内容 SHA-256(hex,64 字符,与后端 file_hash 字段长度一致)。
* - ≤64MB:全量哈希,内容一致必然一致
* - >64MB:哈希「头部 16MB + 尾部 16MB + 文件大小」,视频素材体积大、
* 头部含 moov 元数据、尾部含 mdat 结尾,抽样碰撞概率可忽略,
* 且避免 100~256MB 视频被整文件读进内存导致页面卡死/崩溃
*
* 运行环境不支持 crypto.subtle(非安全上下文/老浏览器)时返回空字符串,
* 调用方据此降级为不传 hash(后端仍有幂等 token + 同文件名兜底去重)。
*/
export async function computeFileHash(file: File): Promise<string> {
try {
const subtle =
typeof globalThis !== "undefined" &&
globalThis.crypto &&
typeof globalThis.crypto.subtle?.digest === "function"
? globalThis.crypto.subtle
: null
if (!subtle) return ""
if (file.size <= HASH_FULL_READ_LIMIT) {
const data = await readAsArrayBuffer(file.slice(0, file.size))
return toHex(await digestSha256(data))
}
// 大文件:头 8MB + 尾 8MB + 大小,拼成一段后哈希
const head = await readAsArrayBuffer(file.slice(0, HASH_SAMPLE_CHUNK))
const tail =
file.size > HASH_SAMPLE_CHUNK
? await readAsArrayBuffer(file.slice(Math.max(0, file.size - HASH_SAMPLE_CHUNK), file.size))
: new ArrayBuffer(0)
const merged = new Uint8Array(head.byteLength + tail.byteLength + 8)
merged.set(new Uint8Array(head), 0)
merged.set(new Uint8Array(tail), head.byteLength)
const sizeView = new DataView(merged.buffer, head.byteLength + tail.byteLength, 8)
// 文件大小以 64 位大端写入(BigInt 最稳;不支持 BigInt64 时手算高低位)
if (typeof sizeView.setBigUint64 === "function") {
sizeView.setBigUint64(0, BigInt(file.size), false)
} else {
sizeView.setUint32(0, Math.floor(file.size / 0x100000000), false)
sizeView.setUint32(4, file.size >>> 0, false)
}
return toHex(await digestSha256(merged.buffer))
} catch (err) {
console.warn("[uploadDedup] 计算文件哈希失败,降级为不传 file_hash:", err)
return ""
}
}
+14 -3
View File
@@ -12,13 +12,18 @@ export type {
UserResponse,
WechatAuthUrlResponse,
WechatCallbackResponse,
WechatBindUrlResponse,
WechatBindCompleteResponse,
WechatUnbindResponse,
UpdateProfileRequest,
UpdateProfileResponse,
SendVerificationCodeRequest,
BindContactRequest,
BindContactResponse,
} from "./types"
// 用户工具函数
export { normalizeUser } from "./user"
export { normalizeUser, updateProfile } from "./user"
// 登录/注册/登出/刷新
export { login, refreshAccessToken, register, logout } from "./login"
@@ -32,8 +37,14 @@ export { requestPasswordReset, resetPassword } from "./password"
// 邮箱验证
export { verifyEmail } from "./email"
// 微信登录
export { getWechatAuthUrl, wechatCallback } from "./wechat"
// 微信登录 / 绑定
export {
getWechatAuthUrl,
wechatCallback,
getWechatBindUrl,
bindWechat,
unbindWechat,
} from "./wechat"
// 联系方式
export { sendVerificationCode, bindContact } from "./contact"
+44
View File
@@ -34,6 +34,17 @@ export interface User {
is_email_verified: boolean
email_verified: boolean
created_at?: string
/** 微信是否已绑定 */
wechat_bound?: boolean
/** 微信昵称(绑定后展示) */
wechat_nickname?: string
/** 头像 URL(微信头像等) */
avatar_url?: string
/** 手机号 */
phone?: string
phone_verified?: boolean
/** 资料是否完善(微信新用户首次登录为 false,需填昵称引导) */
profile_completed?: boolean
}
export interface UserResponse {
@@ -45,6 +56,12 @@ export interface UserResponse {
is_email_verified?: boolean
email_verified?: boolean
created_at?: string
wechat_bound?: boolean
wechat_nickname?: string
avatar_url?: string
phone?: string
phone_verified?: boolean
profile_completed?: boolean
}
export interface WechatAuthUrlResponse {
@@ -80,3 +97,30 @@ export interface BindContactResponse {
success: boolean
user: User
}
/** 更新个人资料请求 */
export interface UpdateProfileRequest {
display_name?: string
}
/** 更新个人资料响应(返回最新用户信息) */
export interface UpdateProfileResponse {
user: UserResponse
}
/** 微信绑定授权链接响应 */
export interface WechatBindUrlResponse {
auth_url: string
state: string
}
/** 微信绑定完成响应 */
export interface WechatBindCompleteResponse {
success: boolean
user: UserResponse
}
/** 微信解绑响应 */
export interface WechatUnbindResponse {
success: boolean
}
+16 -1
View File
@@ -1,4 +1,5 @@
import type { User, UserResponse } from "./types"
import apiClient from "../client"
import type { User, UserResponse, UpdateProfileRequest, UpdateProfileResponse } from "./types"
/**
* 规范化用户数据,兼容不同后端返回格式
@@ -16,5 +17,19 @@ export const normalizeUser = (data: UserResponse): User => {
is_email_verified: emailVerified,
email_verified: emailVerified,
created_at: data.created_at,
wechat_bound: data.wechat_bound,
wechat_nickname: data.wechat_nickname,
avatar_url: data.avatar_url,
phone: data.phone,
phone_verified: data.phone_verified,
profile_completed: data.profile_completed,
}
}
/**
* 更新个人资料(昵称等)
*/
export const updateProfile = async (data: UpdateProfileRequest): Promise<User> => {
const response = await apiClient.patch<UpdateProfileResponse>("/auth/me", data)
return normalizeUser(response.data.user)
}
+35 -2
View File
@@ -1,8 +1,14 @@
import apiClient from "../client"
import type { WechatAuthUrlResponse, WechatCallbackResponse } from "./types"
import type {
WechatAuthUrlResponse,
WechatCallbackResponse,
WechatBindUrlResponse,
WechatBindCompleteResponse,
WechatUnbindResponse,
} from "./types"
/**
* 获取微信授权链接
* 获取微信授权链接(登录场景)
*/
export const getWechatAuthUrl = async (): Promise<WechatAuthUrlResponse> => {
const response = await apiClient.get("/auth/wechat/url")
@@ -19,3 +25,30 @@ export const wechatCallback = async (
const response = await apiClient.post("/auth/wechat/callback", { code, state })
return response.data
}
/**
* 获取微信绑定授权链接(已登录用户绑定场景)
*/
export const getWechatBindUrl = async (): Promise<WechatBindUrlResponse> => {
const response = await apiClient.get("/auth/wechat/bind/url")
return response.data
}
/**
* 微信绑定完成(扫码回调后用 code 绑定到当前登录账号)
*/
export const bindWechat = async (
code: string,
state: string,
): Promise<WechatBindCompleteResponse> => {
const response = await apiClient.post("/auth/wechat/bind", { code, state })
return response.data
}
/**
* 解绑微信
*/
export const unbindWechat = async (): Promise<WechatUnbindResponse> => {
const response = await apiClient.delete("/auth/wechat/bind")
return response.data
}
+112
View File
@@ -0,0 +1,112 @@
/**
* 微信扫码登录 WxLogin JS-SDK 动态加载与授权参数解析
*
* 微信官网嵌入式二维码方案:页面引入 https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js
* 后挂载全局 window.WxLogin,new WxLogin({...}) 会在指定容器内渲染二维码 iframe。
* 本模块负责:动态加载该脚本(带超时/失败检测)、从后端返回的 auth_url 中解析
* WxLogin 所需的 appid / redirect_uri / state。
*/
const WX_LOGIN_SRC = "https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
/** 脚本加载超时(毫秒):超时视为加载失败,调用方回退整页跳转 */
const WX_LOGIN_LOAD_TIMEOUT = 8000
/** WxLogin 构造参数(微信官方字段,保持原名) */
export interface WxLoginOptions {
/** 是否内嵌二维码(回调在 iframe 内完成) */
self_redirect: boolean
/** 二维码容器元素 id */
id: string
/** 微信开放平台 AppID */
appid: string
/** 应用授权作用域,网站应用固定 snsapi_login */
scope: "snsapi_login"
/** 回调地址(需与微信开放平台配置一致,WxLogin 内部会 encodeURIComponent) */
redirect_uri: string
/** 防 CSRF 随机串,由后端 state store 生成并在回调时一次性消费 */
state: string
/** 二维码样式:black / white */
style?: "black" | "white"
/** 自定义样式链接(可选) */
href?: string
}
/** 微信脚本挂载到 window 上的全局构造函数类型 */
export interface WxLoginConstructor {
new (options: WxLoginOptions): unknown
}
declare global {
interface Window {
WxLogin?: WxLoginConstructor
}
}
let loadPromise: Promise<WxLoginConstructor> | null = null
/**
* 动态加载微信 WxLogin JS(单例:并发调用复用同一个 promise)。
* 加载失败或超时会 reject,调用方应回退到整页跳转授权方式。
*/
export function loadWxLoginScript(): Promise<WxLoginConstructor> {
if (window.WxLogin) return Promise.resolve(window.WxLogin)
if (loadPromise) return loadPromise
loadPromise = new Promise<WxLoginConstructor>((resolve, reject) => {
const script = document.createElement("script")
script.src = WX_LOGIN_SRC
script.async = true
script.onload = () => {
if (window.WxLogin) {
resolve(window.WxLogin)
} else {
loadPromise = null
reject(new Error("微信登录脚本加载完成但 WxLogin 未挂载"))
}
}
script.onerror = () => {
loadPromise = null
script.remove()
reject(new Error("微信登录脚本加载失败"))
}
document.head.appendChild(script)
// 超时兜底:部分网络环境下脚本既不 onload 也不 onerror
window.setTimeout(() => {
if (window.WxLogin) {
resolve(window.WxLogin)
return
}
loadPromise = null
script.remove()
reject(new Error("微信登录脚本加载超时"))
}, WX_LOGIN_LOAD_TIMEOUT)
})
return loadPromise
}
/** 从微信授权链接 query 中解析出的 WxLogin 所需参数 */
export interface ParsedWxAuthParams {
appid: string
/** 已 URL 解码的回调地址(传给 WxLogin 时由其内部再次编码) */
redirect_uri: string
state: string
}
/**
* 从后端返回的微信授权链接(https://open.weixin.qq.com/connect/qrconnect?appid=...&redirect_uri=...&state=...)
* 中解析 appid / redirect_uri / state。解析失败时返回 null,由调用方回退整页跳转。
*/
export function parseWxAuthUrl(authUrl: string, stateFallback?: string): ParsedWxAuthParams | null {
try {
const url = new URL(authUrl)
const appid = url.searchParams.get("appid")
const redirectUri = url.searchParams.get("redirect_uri")
const state = url.searchParams.get("state") || stateFallback || ""
if (!appid || !redirectUri || !state) return null
return { appid, redirect_uri: redirectUri, state }
} catch {
return null
}
}
+8 -4
View File
@@ -44,8 +44,10 @@ export interface BgmConfig {
export interface TemplateSegment {
id?: string
segment_order: number
duration_min: number
duration_max: number
/** @deprecated 模板无时长概念(#1750 基线):字段保留仅为兼容旧数据读取,新模板可不传 */
duration_min?: number
/** @deprecated 同上 */
duration_max?: number
material_type: string | null
}
@@ -59,7 +61,8 @@ export interface EditingTemplate {
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
/** @deprecated 模板无时长概念(#1750 基线):成片时长由配音时长决定;字段保留兼容旧数据 */
estimated_duration?: number
segments: TemplateSegment[]
watermark_config?: WatermarkConfig
intro_outro_config?: IntroOutroConfig
@@ -89,7 +92,8 @@ export interface SaveTemplatePayload {
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
/** @deprecated 模板无时长概念(#1750 基线):保留兼容旧数据 */
estimated_duration?: number
segments: Omit<TemplateSegment, "id">[]
watermark_config?: WatermarkConfig
intro_outro_config?: IntroOutroConfig
+132
View File
@@ -0,0 +1,132 @@
/**
* 统一错误信息提取
* 把 axios 错误(后端 detail / FastAPI 校验错误 / HTTP 状态码)、XHR/OSS 错误、
* 网络/超时错误、普通 Error 统一转成「可直接展示给用户」的中文信息。
*
* 与 api/client.ts 响应拦截器的提示口径保持一致;拦截器负责全局 toast,
* 页面/队列卡片用本工具把真实原因展示在持久位置(回调页、失败卡片等)。
*/
import type { AxiosError } from "axios"
/** 后端错误响应体可能出现的字段(FastAPI:detail;历史接口:message/msg) */
interface ErrorBody {
detail?: unknown
message?: unknown
msg?: unknown
}
/** FastAPI 422 校验错误单项 */
interface ValidationItem {
loc?: (string | number)[]
msg?: string
}
/** 从后端响应体提取人类可读信息(detail 可能是字符串、对象、422 数组) */
function extractBodyMessage(data: unknown): string {
if (!data || typeof data !== "object") return ""
const body = data as ErrorBody
const walk = (val: unknown): string => {
if (typeof val === "string") return val
if (Array.isArray(val)) {
// FastAPI 422: [{loc, msg, type}, ...] → 取每条 msg 拼接
const parts = val
.map((item) => {
if (typeof item === "string") return item
if (item && typeof item === "object") {
const v = item as ValidationItem
if (typeof v.msg === "string") {
const field = Array.isArray(v.loc) ? v.loc.filter((x) => x !== "body").join(".") : ""
return field ? `${field}: ${v.msg}` : v.msg
}
return walk(item)
}
return ""
})
.filter(Boolean)
return parts.join(";")
}
if (val && typeof val === "object") {
const obj = val as Record<string, unknown>
if (typeof obj.message === "string") return obj.message
if (typeof obj.msg === "string") return obj.msg
if (typeof obj.detail === "string") return obj.detail
if (obj.message && typeof obj.message === "object") return walk(obj.message)
if (obj.msg && typeof obj.msg === "object") return walk(obj.msg)
try {
return JSON.stringify(val)
} catch {
return ""
}
}
return ""
}
return walk(body.detail) || walk(body.message) || walk(body.msg)
}
/** 无响应体时按 HTTP 状态码给出兜底提示(与 client.ts 拦截器口径一致) */
function statusFallback(status: number): string {
switch (status) {
case 400:
return "请求参数有误(HTTP 400)"
case 401:
return "登录状态已失效,请重新登录(HTTP 401)"
case 403:
return "没有权限执行该操作(HTTP 403)"
case 404:
return "请求的资源不存在(HTTP 404)"
case 409:
return "操作冲突,资源状态已变化(HTTP 409)"
case 413:
return "文件过大,请缩小后重试(HTTP 413)"
case 415:
return "不支持的文件格式(HTTP 415)"
case 429:
return "操作过于频繁,请稍后再试(HTTP 429)"
case 503:
return "服务暂不可用,请稍后再试(HTTP 503)"
default:
if (status >= 500) return `服务器繁忙,请稍后再试(HTTP ${status})`
return `请求失败(HTTP ${status})`
}
}
/**
* 从任意抛出值提取可展示的错误信息。
* @param fallback 全部提取失败时的兜底文案
*/
export function getErrorMessage(err: unknown, fallback = "操作失败,请稍后重试"): string {
if (!err) return fallback
// axios 错误(后端 JSON 响应 / HTTP 错误状态)
const ax = err as AxiosError<ErrorBody>
if (ax.isAxiosError || (typeof ax === "object" && "response" in (ax as object))) {
// 超时
if (ax.code === "ECONNABORTED" || /timeout/i.test(ax.message || "")) {
return "请求超时,请检查网络后重试"
}
const resp = ax.response
if (resp) {
const bodyMsg = extractBodyMessage(resp.data)
if (bodyMsg) return bodyMsg
return statusFallback(resp.status)
}
// 请求已发出但无响应(断网/CORS/DNS)
if (ax.request) return "网络连接异常,请检查网络设置"
return ax.message || fallback
}
if (err instanceof Error) {
// XHR 直传 OSS 失败等场景自带详细 message(含 HTTP 状态 + OSS Code/Message)
if (err.message) return err.message
}
if (typeof err === "string") return err
return fallback
}
/** client.ts 拦截器是否已对该错误弹过全局 toast(__msgShown 标记) */
export function isErrorMsgShown(err: unknown): boolean {
return Boolean((err as { __msgShown?: boolean } | null)?.__msgShown)
}
@@ -0,0 +1,61 @@
/**
* 批量变体剪辑计划 API(#1744)
*
* 批量预览时向后端申请 N 个变体的「独立剪辑计划片段」:
* - 变体 0 保留源 plan(用户在编辑器/智能选片产出的片段,含标题样式编辑结果);
* - 变体 1..N-1 由后端 reselect_plan_for_variant 完整重跑单视频选片流程
* (素材洗牌 + main 片段顺序洗牌 + 镜头/起点随机 + 跨变体 20% 区间避让 +
* 素材使用区间写回 metadata),与正式批量生成 POST /generation/tasks?count=N
* 使用同一套选片逻辑;
* - 正式生成时把 variant_plan_ids 原样回传,后端直接关联这些 plan 渲染,
* 不再重新选片 —— 预览所见即成片。
*
* 该接口只做选片/建 plan(秒级),不触发视频渲染,无渲染成本。
* 后端端点未上线(404)或选片失败(素材不足等)时前端降级为本地 variantSeed
* 模拟预览,不阻塞用户流程。
*/
import apiClient from "../client"
import type { EditPlanClip } from "../template-editor"
/** 批量变体计划请求体 */
export interface BatchVariantPlansRequest {
template_id: string
/** 本批次素材池(手动选择或智能匹配结果) */
asset_ids: string[]
/** 变体数量(≥1);=1 时只返回源 plan 片段 */
count: number
/** 源剪辑计划 ID:优先取预览/草稿关联的 plan;不传由后端按 template_id+user 兜底最新 plan */
source_edit_plan_id?: string
}
/** 单个变体的计划片段 */
export interface VariantPlan {
/** 变体序号,从 0 开始 */
variant_index: number
/** 该变体关联的剪辑计划 ID(正式生成时回传,实现预览即成片) */
plan_id: string
/** 该变体的真实片段(顺序/素材/起点与正式成片一致) */
clips: EditPlanClip[]
}
/** 批量变体计划响应 */
export interface BatchVariantPlansResponse {
items: VariantPlan[]
total: number
}
/**
* 创建批量变体剪辑计划并返回各变体片段。
*
* 注意:端点 404(后端未上线)/ 400(素材不足)等失败由调用方 catch 后降级,
* 不要抛 unhandled rejection。
*/
export async function createBatchVariantPlans(
params: BatchVariantPlansRequest,
): Promise<BatchVariantPlansResponse> {
const response = await apiClient.post<BatchVariantPlansResponse>(
"/generation/variant-plans",
params,
)
return response.data
}
+6
View File
@@ -100,6 +100,12 @@ export interface CreateGenerationTaskRequest {
voice_library_ids?: string[]
/** 各变体独立封面URL:长度1=共用,长度=count=独立,空数组=回退 cover_url */
cover_urls?: string[]
/**
* 批量变体剪辑计划 ID(#1744):预览阶段后端独立选片产出的 plan id 列表
* (按变体全量索引,长度=previewCount)。正式生成回传后后端直接关联这些
* plan 渲染、不再重新选片,保证预览所见即成片。后端未支持时忽略该字段。
*/
variant_plan_ids?: string[]
}
/** 单个生成任务详情(对齐后端 GenerationTaskResponse) */
+4 -4
View File
@@ -42,8 +42,10 @@ export interface TemplateItem {
export interface TemplateSegment {
id?: string
segment_order: number
duration_min: number
duration_max: number
/** @deprecated 模板无时长概念(#1750 基线):字段保留仅为兼容旧数据读取 */
duration_min?: number
/** @deprecated 同上 */
duration_max?: number
material_type: string | null
description?: string
}
@@ -55,8 +57,6 @@ export interface TemplateListParams {
category?: string
tags?: string
keyword?: string
/** 时长筛选(秒):short < 30, medium 30-120, long > 120 */
duration_range?: "short" | "medium" | "long"
}
/** 模板列表分页响应 */
@@ -0,0 +1,74 @@
.xx-wechat-qr-modal {
position: relative;
padding: 8px 0 4px;
min-height: 320px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 常驻二维码容器(WxLogin 渲染目标) */
.xx-wechat-qr-container {
display: flex;
justify-content: center;
min-height: 260px;
}
/* loading / error 遮罩层,覆盖在二维码容器之上 */
.xx-wechat-qr-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fff;
text-align: center;
color: #666;
}
.xx-wechat-qr-overlay p {
margin-top: 16px;
margin-bottom: 0;
}
.xx-wechat-qr-container iframe {
border: none;
}
.xx-wechat-qr-tip {
margin: 12px 0 0;
color: #666;
font-size: 14px;
}
.xx-wechat-qr-error {
text-align: center;
width: 100%;
}
.xx-wechat-qr-error-msg {
color: #ef4444;
font-size: 14px;
line-height: 1.6;
margin: 0 0 16px;
word-break: break-word;
}
.xx-wechat-qr-error-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.xx-wechat-qr-fallback {
background: none;
border: none;
color: var(--primary-color, #3b82f6);
cursor: pointer;
font-size: 13px;
padding: 0;
text-decoration: underline;
}
@@ -0,0 +1,265 @@
/**
* 微信扫码二维码弹窗(登录 / 绑定复用)
*
* 微信官方嵌入式二维码方案:弹窗内用 new WxLogin({ self_redirect: true }) 渲染二维码,
* 扫码后微信重定向到本站回调页(在二维码 iframe 内加载),回调页通过 postMessage
* 把成功/失败结果通知本弹窗(消息协议见 ./messages)。
*
* 兜底:获取授权链接成功但 WxLogin JS 加载失败/超时时,自动回退整页跳转授权
* (与旧流程一致);获取授权链接本身失败时在弹窗内展示错误并提供重试。
*/
import React, { useEffect, useRef, useState } from "react"
import { Spin } from "antd"
import Modal from "@/components/ui/Modal"
import Button from "@/components/ui/Button"
import {
getWechatAuthUrl,
getWechatBindUrl,
getCurrentUser,
normalizeUser,
type User,
} from "@/api/auth"
import { useAuthStore } from "@/store/authStore"
import { scheduleProactiveRefresh } from "@/api/auth/tokenRefresh"
import { getErrorMessage } from "@/api/errors"
import { loadWxLoginScript, parseWxAuthUrl } from "@/api/auth/wxLogin"
import { isWechatQrMessage, type WechatQrScene } from "./messages"
import "./WechatQrModal.css"
export interface WechatQrModalProps {
open: boolean
scene: WechatQrScene
onClose: () => void
/** 登录场景成功回调(needOnboarding=true 时调用方应跳昵称引导页) */
onLoginSuccess?: (needOnboarding: boolean) => void
/** 绑定场景成功回调(调用方刷新用户信息/提示) */
onBindSuccess?: () => void
}
type QrStatus = "loading" | "qrcode" | "error"
const CONTAINER_ID: Record<WechatQrScene, string> = {
login: "wechat-qr-login-container",
bind: "wechat-qr-bind-container",
}
const STATE_STORAGE_KEY: Record<WechatQrScene, string> = {
login: "wechat_state",
bind: "wechat_bind_state",
}
/**
* 等待二维码容器挂载到 DOM。antd Modal 内容通过 portal 渲染且带进场动画,
* 父组件 effect 首次执行时容器可能尚未出现在 document 中。
*/
function waitForContainer(id: string, timeoutMs = 3000): Promise<HTMLElement | null> {
return new Promise((resolve) => {
const start = Date.now()
const check = () => {
const el = document.getElementById(id)
if (el) {
resolve(el)
return
}
if (Date.now() - start > timeoutMs) {
resolve(null)
return
}
setTimeout(check, 50)
}
check()
})
}
const WechatQrModal: React.FC<WechatQrModalProps> = ({
open,
scene,
onClose,
onLoginSuccess,
onBindSuccess,
}) => {
const setAuth = useAuthStore((state) => state.setAuth)
const setUser = useAuthStore((state) => state.setUser)
const [status, setStatus] = useState<QrStatus>("loading")
const [errorMsg, setErrorMsg] = useState("")
/** 刷新二维码计数:变化时重新请求授权链接并重渲染 */
const [renderSeq, setRenderSeq] = useState(0)
/** 最新授权链接,用于"整页打开"兜底 */
const authUrlRef = useRef<string | null>(null)
const isLogin = scene === "login"
// 初始化:获取授权链接 → 加载 WxLogin JS → 内嵌渲染二维码
useEffect(() => {
if (!open) return
let cancelled = false
authUrlRef.current = null
setStatus("loading")
setErrorMsg("")
const init = async () => {
try {
const fetchUrl = isLogin ? getWechatAuthUrl : getWechatBindUrl
const result = await fetchUrl()
if (cancelled) return
// 写 state(整页跳转兜底路径的回调页也会清理它)
localStorage.setItem(STATE_STORAGE_KEY[scene], result.state)
authUrlRef.current = result.auth_url
const params = parseWxAuthUrl(result.auth_url, result.state)
if (!params) {
// 授权链接格式异常:直接整页跳转,由微信侧/回调页兜底
window.location.href = result.auth_url
return
}
const WxLogin = await loadWxLoginScript()
if (cancelled) return
// 等 Modal portal 中的容器挂载完成
const container = await waitForContainer(CONTAINER_ID[scene])
if (cancelled) return
if (!container) {
window.location.href = result.auth_url
return
}
container.innerHTML = ""
new WxLogin({
self_redirect: true,
id: CONTAINER_ID[scene],
appid: params.appid,
scope: "snsapi_login",
redirect_uri: params.redirect_uri,
state: params.state,
style: "black",
})
if (!cancelled) setStatus("qrcode")
} catch (err) {
if (cancelled) return
if (authUrlRef.current) {
// 授权链接已拿到但二维码脚本加载失败/超时:回退整页跳转
window.location.href = authUrlRef.current
return
}
// 授权链接接口本身失败:弹窗内展示真实原因,允许重试
setErrorMsg(getErrorMessage(err, "微信服务暂不可用,请稍后重试"))
setStatus("error")
}
}
init()
return () => {
cancelled = true
}
}, [open, scene, isLogin, renderSeq])
// 监听 iframe 内回调页 postMessage 回来的扫码结果
useEffect(() => {
if (!open) return
const handleMessage = async (event: MessageEvent) => {
// 只接受同源消息
if (event.origin !== window.location.origin) return
if (!isWechatQrMessage(event.data, scene)) return
const msg = event.data
if (msg.success) {
if (isLogin) {
// iframe 内回调页已把 token 写入 localStorage(同源共享),
// 父窗口同步内存登录态后交给调用方跳转
try {
const userData = await getCurrentUser()
const user = normalizeUser(userData) as User
setAuth(
user,
localStorage.getItem("access_token") || "",
localStorage.getItem("refresh_token"),
)
scheduleProactiveRefresh()
} catch {
// token 已持久化,即使这里失败路由守卫/刷新也能恢复登录态
}
onLoginSuccess?.(msg.payload?.needOnboarding ?? false)
} else {
try {
const userData = await getCurrentUser()
setUser(normalizeUser(userData) as User)
} catch {
// 绑定结果以后端为准,调用方 invalidateQueries 会兜底刷新
}
onBindSuccess?.()
}
return
}
// 失败:弹窗内展示回调页透传的真实原因,提供刷新/整页跳转
setErrorMsg(msg.detail || "微信授权失败,请重试")
setStatus("error")
}
window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [open, scene, isLogin, onLoginSuccess, onBindSuccess, setAuth, setUser])
const handleRefresh = () => setRenderSeq((seq) => seq + 1)
const handleFullPageRedirect = () => {
if (authUrlRef.current) {
window.location.href = authUrlRef.current
}
}
return (
<Modal
title={isLogin ? "微信扫码登录" : "绑定微信"}
open={open}
onCancel={onClose}
footer={null}
width={380}
maskClosable={false}
destroyOnHidden
>
<div className="xx-wechat-qr-modal">
{/* 二维码容器常驻:WxLogin 在 loading 阶段就会把 iframe 渲染进来,
不能按 status 条件渲染,否则 effect 里永远找不到容器 */}
<div
id={CONTAINER_ID[scene]}
className="xx-wechat-qr-container"
style={{ visibility: status === "qrcode" ? "visible" : "hidden" }}
/>
{status === "loading" && (
<div className="xx-wechat-qr-overlay">
<Spin size="large" />
<p>正在生成微信二维码...</p>
</div>
)}
{status === "qrcode" && (
<p className="xx-wechat-qr-tip">请使用微信扫描二维码{isLogin ? "登录" : "绑定账号"}</p>
)}
{status === "error" && (
<div className="xx-wechat-qr-overlay xx-wechat-qr-error">
<p className="xx-wechat-qr-error-msg">{errorMsg}</p>
<div className="xx-wechat-qr-error-actions">
<Button buttonType="primary" buttonSize="md" onClick={handleRefresh}>
刷新二维码
</Button>
{authUrlRef.current && (
<button
type="button"
className="xx-wechat-qr-fallback"
onClick={handleFullPageRedirect}
>
使用整页方式打开
</button>
)}
</div>
</div>
)}
</div>
</Modal>
)
}
export default WechatQrModal
@@ -0,0 +1,71 @@
/**
* 微信扫码弹窗与 iframe 内回调页之间的 postMessage 消息协议
*
* 流程:弹窗内 WxLogin(self_redirect:true) 渲染的二维码 iframe 扫码后,
* 微信重定向到本站回调页(同源,在 iframe 内加载);回调页完成换 token/绑定后,
* 通过 window.parent.postMessage 把结果通知弹窗,弹窗负责关闭/展示错误/同步登录态。
*/
/** 扫码场景:登录 / 绑定 */
export type WechatQrScene = "login" | "bind"
export interface WechatQrSuccessPayload {
/** 登录场景:是否需要昵称引导(新用户或资料未完善) */
needOnboarding?: boolean
}
export interface WechatQrMessageData {
/** 固定协议标识,父窗口只认该 source */
source: "xiaoxia-wechat-qr"
/** 场景,需与弹窗发起时一致(login/bind),父窗口据此过滤 */
scene: WechatQrScene
/** 成功 / 失败 */
success: boolean
/** 失败时的真实原因(已在回调页拼好,含后端 detail) */
detail?: string
payload?: WechatQrSuccessPayload
}
export const WECHAT_QR_MESSAGE_SOURCE = "xiaoxia-wechat-qr"
/** 判断收到的 message 是否为本协议消息(且场景匹配) */
export function isWechatQrMessage(
data: unknown,
scene: WechatQrScene,
): data is WechatQrMessageData {
if (!data || typeof data !== "object") return false
const msg = data as Partial<WechatQrMessageData>
return msg.source === WECHAT_QR_MESSAGE_SOURCE && msg.scene === scene
}
/** 当前页面是否运行在 iframe(弹窗内嵌二维码)中 */
export function isInIframe(): boolean {
try {
return window.parent !== window
} catch {
// 跨域访问 window.parent 可能抛异常,按非 iframe 处理
return false
}
}
/**
* iframe 内回调页向父窗口上报扫码结果。同源回调页加载,targetOrigin 限定本站 origin。
*/
export function postWechatQrResult(
scene: WechatQrScene,
success: boolean,
options?: { detail?: string; needOnboarding?: boolean },
): void {
if (!isInIframe()) return
const data: WechatQrMessageData = {
source: WECHAT_QR_MESSAGE_SOURCE,
scene,
success,
detail: options?.detail,
payload:
success && options?.needOnboarding !== undefined
? { needOnboarding: options.needOnboarding }
: undefined,
}
window.parent.postMessage(data, window.location.origin)
}
@@ -0,0 +1,85 @@
/**
* 全局错误边界:专门兜底"发版后旧标签页懒加载 chunk 失效"导致的白屏,
* 同时兜住页面级渲染崩溃,避免任何未捕获错误导致整页白屏无反馈。
*
* 捕获到 ChunkLoadError / Failed to fetch dynamically imported module:
* 1. 首次:自动整页刷新一次(sessionStorage 标记,刷新后 index.html 重新拉取,
* 拿到新 chunk 引用,白屏自愈)
* 2. 刷新后仍失败(标记未过期):不再自动刷新,显示"系统已更新,请点击刷新"
* 兜底界面,由用户手动点击
*
* 其他非 chunk 错误:显示通用错误页 + "返回首页"按钮(跳首页而非刷新当前 URL,
* 避免刷新后再次命中同一路由崩溃形成死循环)。
*/
import React from "react"
import { Button, Result } from "antd"
import {
getChunkReloadedAt,
goHomeRecover,
isChunkLoadError,
reloadForChunkError,
} from "@/utils/chunkLoadError"
interface Props {
children: React.ReactNode
}
interface State {
error: Error | null
isChunkError: boolean
/** 捕获错误时是否已经自动刷新过(决定显示自动刷新中还是手动兜底) */
alreadyReloaded: boolean
}
class ChunkErrorBoundary extends React.Component<Props, State> {
state: State = { error: null, isChunkError: false, alreadyReloaded: false }
static getDerivedStateFromError(error: Error): State {
const chunk = isChunkLoadError(error)
return {
error,
isChunkError: chunk,
alreadyReloaded: chunk ? getChunkReloadedAt() !== null : false,
}
}
componentDidCatch(error: Error): void {
// 仅 chunk 错误且本次会话没自动刷新过 → 打标记并整页刷新(自愈)
if (isChunkLoadError(error) && getChunkReloadedAt() === null) {
reloadForChunkError()
}
}
render(): React.ReactNode {
const { error, isChunkError, alreadyReloaded } = this.state
if (!error) return this.props.children
if (isChunkError && !alreadyReloaded) {
// 已打标记、componentDidCatch 里已触发 reload;极短瞬间展示加载中
return (
<Result status="info" title="系统正在更新" subTitle="检测到新版本,正在自动刷新页面…" />
)
}
// 手动兜底统一跳首页(整页导航):chunk 失效时脱离旧 chunk 引用;
// 业务崩溃时绕开当前报错路由,避免刷新-再崩死循环
return (
<Result
status="warning"
title={isChunkError ? "系统已更新" : "页面出现异常"}
subTitle={
isChunkError
? "检测到新版本,请点击下方按钮回到首页加载最新内容。"
: "页面加载遇到问题,点击返回首页通常可以恢复,未保存的内容可能丢失。"
}
extra={
<Button type="primary" onClick={goHomeRecover}>
{isChunkError ? "刷新并返回首页" : "返回首页"}
</Button>
}
/>
)
}
}
export default ChunkErrorBoundary
+4 -1
View File
@@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ConfigProvider, App as AntApp } from "antd"
import zhCN from "antd/locale/zh_CN"
import router from "./router"
import ChunkErrorBoundary from "./components/common/ChunkErrorBoundary"
import { scheduleProactiveRefresh } from "./api/auth/tokenRefresh"
// 应用启动时,如果用户已登录,立即调度主动 token 刷新
@@ -99,7 +100,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<ConfigProvider locale={zhCN} theme={theme}>
<AntApp>
<RouterProvider router={router} />
<ChunkErrorBoundary>
<RouterProvider router={router} />
</ChunkErrorBoundary>
</AntApp>
</ConfigProvider>
</QueryClientProvider>
@@ -42,6 +42,7 @@ const AssetLibrary: React.FC = () => {
assetsError,
assetsErrorObj,
refetchAssets,
stalledAssetIds,
searchText,
setSearchText,
filterType,
@@ -77,6 +78,7 @@ const AssetLibrary: React.FC = () => {
removeUpload,
clearFinished,
uploading,
transferActive,
activeCount,
pendingCount,
} = useAssetUpload({ effectiveLibId })
@@ -168,6 +170,7 @@ const AssetLibrary: React.FC = () => {
{/* 上传区域 */}
<AssetUploadZone
uploading={uploading}
transferActive={transferActive}
activeCount={activeCount}
pendingCount={pendingCount}
onUpload={enqueueUploads}
@@ -214,6 +217,7 @@ const AssetLibrary: React.FC = () => {
selectedIds={selectedIds}
diagnosingId={diagnosingId}
uploadProgressMap={uploadProgressMap}
stalledAssetIds={stalledAssetIds}
onRetry={refetchAssets}
onToggleSelect={toggleSelect}
onDiagnose={handleDiagnose}
+36
View File
@@ -831,6 +831,20 @@
color: #ef4444;
}
.xx-upload-queue-error-detail {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: #ef4444;
word-break: break-word;
white-space: normal;
}
.xx-upload-queue-error-hint {
margin-top: 2px;
color: #b45309;
}
.xx-upload-queue-actions {
display: flex;
gap: 6px;
@@ -1041,3 +1055,25 @@
background: #fef2f2;
color: #dc2626;
}
/* 上传入口禁用态(直传进行中,防重复提交,Issue #1714) */
.xx-asset-upload-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.xx-asset-upload-btn:disabled:hover {
opacity: 0.6;
}
.xx-asset-upload-btn:disabled:active {
transform: none;
}
/* 处理超时遮罩:创建超过 10 分钟仍在处理中(疑似后端卡住),停止转圈并警示 */
.xx-asset-thumb-stalled {
background: rgba(217, 119, 6, 0.28);
color: #fde68a;
backdrop-filter: blur(2px);
}
.xx-asset-thumb-stalled :first-child {
font-size: var(--font-size-2xl);
}
@@ -22,6 +22,8 @@ export interface AssetCardProps {
diagnosing?: boolean
/** 上传中实时进度(仅 uploading 态有值;ingesting 后由后端状态接管) */
uploadProgress?: { progress: number; uploading: boolean }
/** 处理超过 10 分钟仍未就绪(疑似后端卡住):停止转圈并提示处理超时 */
stalled?: boolean
onToggle: () => void
onDiagnose: () => void
onPlay: () => void
@@ -33,6 +35,7 @@ const AssetCard: React.FC<AssetCardProps> = ({
selected,
diagnosing,
uploadProgress,
stalled,
onToggle,
onDiagnose,
onPlay,
@@ -67,11 +70,15 @@ const AssetCard: React.FC<AssetCardProps> = ({
</div>
)}
{/* 转码/处理中遮罩 */}
{/* 转码/处理中遮罩(卡死超过 10 分钟时停止转圈,提示超时) */}
{asset.loading && !isUploading && (
<div className="xx-asset-thumb-overlay xx-asset-thumb-processing">
<LoadingOutlined />
<span>转码处理中</span>
<div
className={`xx-asset-thumb-overlay ${
stalled ? "xx-asset-thumb-stalled" : "xx-asset-thumb-processing"
}`}
>
{stalled ? <CloseCircleOutlined /> : <LoadingOutlined />}
<span>{stalled ? "处理超时,可重试上传" : "转码处理中"}</span>
</div>
)}
@@ -129,7 +136,7 @@ const AssetCard: React.FC<AssetCardProps> = ({
</p>
<div className="xx-asset-meta">
<span className="xx-asset-meta-status">
<StatusPill status={asset.status} label={asset.statusLabel} />
<StatusPill status={asset.status} label={stalled ? "处理超时" : asset.statusLabel} />
</span>
{asset.duration && <span className="xx-asset-meta-duration">{asset.duration}</span>}
</div>
@@ -19,6 +19,8 @@ export interface AssetGridSectionProps {
selectedIds: Set<string>
diagnosingId: string | null
uploadProgressMap?: UploadProgressMap
/** 创建超过 10 分钟仍在处理中的素材 id(疑似后端卡住),卡片提示处理超时 */
stalledAssetIds?: Set<string>
onRetry?: () => void
onToggleSelect: (id: string) => void
onDiagnose: (asset: AssetItem) => void
@@ -34,6 +36,7 @@ export const AssetGridSection: React.FC<AssetGridSectionProps> = ({
selectedIds,
diagnosingId,
uploadProgressMap,
stalledAssetIds,
onRetry,
onToggleSelect,
onDiagnose,
@@ -76,6 +79,7 @@ export const AssetGridSection: React.FC<AssetGridSectionProps> = ({
selected={selectedIds.has(asset.id)}
diagnosing={diagnosingId === asset.id}
uploadProgress={uploadProgressMap?.get(asset.id)}
stalled={stalledAssetIds?.has(asset.id)}
onToggle={() => onToggleSelect(asset.id)}
onDiagnose={() => onDiagnose(asset)}
onPlay={() => onPlay(asset)}
@@ -4,10 +4,13 @@
* - 拖拽文件到内容区任意位置同样触发上传(不再占用大面积虚线框)
*/
import React, { useRef, useState } from "react"
import { message } from "antd"
import { PlusOutlined, CloudUploadOutlined } from "@ant-design/icons"
export interface AssetUploadZoneProps {
uploading: boolean
/** 有文件正在本地指纹/prepare/直传(非服务端转码),此时禁用入口防重复提交 */
transferActive: boolean
activeCount: number
pendingCount: number
onUpload: (files: File[]) => void
@@ -15,6 +18,7 @@ export interface AssetUploadZoneProps {
export const AssetUploadZone: React.FC<AssetUploadZoneProps> = ({
uploading,
transferActive,
activeCount,
pendingCount,
onUpload,
@@ -27,6 +31,11 @@ export const AssetUploadZone: React.FC<AssetUploadZoneProps> = ({
const pickFiles = (list: FileList | null) => {
if (!list || list.length === 0) return
// 直传进行中拦截重复触发:相同文件仍由入队去重兜底,这里先给明确反馈
if (transferActive) {
message.warning("文件正在上传中,请等待当前上传完成后再添加")
return
}
onUpload(Array.from(list))
}
@@ -58,10 +67,18 @@ export const AssetUploadZone: React.FC<AssetUploadZoneProps> = ({
<button
type="button"
className="xx-asset-upload-btn"
onClick={() => inputRef.current?.click()}
disabled={transferActive}
title={transferActive ? "文件上传中,暂不能添加新文件" : undefined}
onClick={() => {
if (transferActive) {
message.warning("文件正在上传中,请等待当前上传完成后再添加")
return
}
inputRef.current?.click()
}}
>
<PlusOutlined />
上传素材
{transferActive ? "上传中…" : "上传素材"}
</button>
<span className="xx-asset-upload-status">
{uploading ? (
@@ -12,7 +12,8 @@ import {
ReloadOutlined,
CloseOutlined,
} from "@ant-design/icons"
import type { UploadItem } from "../hooks/useAssetUpload"
import type { UploadItem, UploadFailStage } from "../hooks/useAssetUpload"
import { COMPLETE_RETRY_HINT } from "../hooks/useAssetUpload"
export interface UploadQueuePanelProps {
items: UploadItem[]
@@ -29,6 +30,13 @@ const STATUS_TEXT: Record<UploadItem["status"], string> = {
error: "上传失败",
}
/** 失败阶段中文名:让用户一眼看到失败发生在哪一步 */
const FAIL_STAGE_TEXT: Record<UploadFailStage, string> = {
prepare: "准备上传阶段",
transfer: "文件传输阶段",
complete: "确认入库阶段",
}
const UploadQueuePanel: React.FC<UploadQueuePanelProps> = ({
items,
onRetry,
@@ -80,16 +88,34 @@ const UploadQueuePanel: React.FC<UploadQueuePanelProps> = ({
) : null}
<div className="xx-upload-queue-status">
{it.duplicated ? "素材已存在,已跳过" : STATUS_TEXT[it.status]}
{it.status === "preparing" && it.hint ? `(${it.hint})` : ""}
{it.status === "uploading" ? ` ${it.progress}%` : ""}
{it.status === "error" && it.error ? `:${it.error}` : ""}
{it.status === "error" && it.failedStage
? `(${FAIL_STAGE_TEXT[it.failedStage]})`
: ""}
</div>
{it.status === "error" && it.error ? (
<div className="xx-upload-queue-error-detail" title={it.error}>
{it.error.split("\n").map((line, idx) =>
line === COMPLETE_RETRY_HINT ? (
<div key={idx} className="xx-upload-queue-error-hint">
{line}
</div>
) : (
<div key={idx}>{line}</div>
),
)}
</div>
) : null}
</div>
<span className="xx-upload-queue-actions">
{it.status === "error" && (
<button
type="button"
className="xx-upload-queue-btn"
title="重试"
title={
it.failedStage === "complete" ? "安全重试(只确认,不重新上传)" : "重试上传"
}
onClick={() => onRetry(it.tempId)}
>
<ReloadOutlined />
+218 -33
View File
@@ -2,34 +2,65 @@ import { useState, useCallback, useRef, useEffect } from "react"
import { useQueryClient } from "@tanstack/react-query"
import { message } from "antd"
import { prepareDirectUploadHandle, type DirectUploadHandle } from "@/api/assets"
import { getErrorMessage, isErrorMsgShown } from "@/api/errors"
import { MAX_FILE_SIZE } from "../constants"
import {
computeFileHash,
findDuplicateInQueue,
makeClientUploadId,
makeFileFingerprint,
} from "@/api/assets/uploadDedup"
/** 单文件上传状态机 */
export type UploadItemStatus = "preparing" | "uploading" | "ingesting" | "done" | "error"
/** 失败发生的阶段:complete 阶段失败时记录可能已在后端建成,禁止盲目重传整个文件 */
export type UploadFailStage = "prepare" | "transfer" | "complete"
export interface UploadItem {
/** 前端临时 id(prepare 前无 asset_id 时用) */
/** 前端临时 id(prepare 前无 asset_id 时用),同时作为队列项 key */
tempId: string
file: File
fileName: string
/** 进度 0~100(仅直传阶段有真实进度) */
progress: number
status: UploadItemStatus
/** 文件指纹(name+size+lastModified),入队去重用 */
fileKey: string
/** 本次逻辑上传的幂等 token:重试复用、重新入队才换新 */
clientUploadId: string
/** 上传前算好的文件内容哈希(SHA-256),prepare/complete 都带上 */
fileHash?: string
/** 后端 prepare 预建的 asset id(旧后端可能为空) */
assetId?: string
/** 去重命中:complete 返回 duplicated,标记完成但不产生新素材 */
duplicated?: boolean
/** 失败发生的阶段;complete 阶段失败点重试只重发 complete,不重新上传文件 */
failedStage?: UploadFailStage
/** 状态行补充提示(如"正在计算文件指纹…") */
hint?: string
error?: string
}
/** 批量直传最大并发数,避免多文件瓜分上行带宽 */
const MAX_CONCURRENT = 3
/** complete 阶段失败后的安全提示:素材可能已在后端建成,重试只重发 complete 幂等安全 */
export const COMPLETE_RETRY_HINT = "素材可能已在服务器处理中,点重试将安全确认,不会重新上传文件"
/** 失败阶段中文名(toast 提示用,明确失败发生在哪一步) */
const STAGE_LABEL: Record<UploadFailStage, string> = {
prepare: "准备上传",
transfer: "文件传输",
complete: "确认入库",
}
/**
* 素材批量上传 Hook
* - prepare 阶段后端预建 status=uploading 的 asset,前端拿到 asset_id 立即刷新列表
* - 入队按文件指纹(name+size+lastModified)去重:同一文件已在队列/上传中/处理中时不重复入队
* - 上传前计算文件 SHA-256,prepare/complete 携带 file_hash + 幂等 token(clientUploadId)
* - complete 超时/失败不盲目重传:复用 handle 只重发 complete(幂等),prepare/transfer 失败才全量重跑
* - OSS 直传并发限制为 3,其余排队;每个文件独立进度/状态
* - complete 后素材进入转码(ingesting/processing),由列表轮询反映
* - 失败卡片支持重试/移除
*/
export function useAssetUpload({ effectiveLibId }: { effectiveLibId: string }) {
@@ -39,6 +70,13 @@ export function useAssetUpload({ effectiveLibId }: { effectiveLibId: string }) {
const itemsRef = useRef<UploadItem[]>([])
itemsRef.current = items
/**
* prepare 成功后的 handle 按 tempId 留存:
* complete 阶段失败(超时/网络)时 OSS 文件已存在、后端记录也可能已建成,
* 重试必须复用同一 handle 只重发 complete,绝不能重新 prepare+直传。
*/
const handlesRef = useRef<Map<string, DirectUploadHandle>>(new Map())
const updateItem = useCallback((tempId: string, patch: Partial<UploadItem>) => {
setItems((prev) => prev.map((it) => (it.tempId === tempId ? { ...it, ...patch } : it)))
}, [])
@@ -52,14 +90,71 @@ export function useAssetUpload({ effectiveLibId }: { effectiveLibId: string }) {
queryClient.invalidateQueries({ queryKey: ["asset-libraries"] })
}, [queryClient, effectiveLibId])
/** 执行单个文件的完整上传流程(prepare→transfer→complete) */
/**
* 执行单个文件的完整上传流程。
* @param completeOnly complete 阶段失败后的重试:跳过 hash/prepare/transfer,只重发 complete
* (OSS 文件已传完,重发由 file_hash + clientUploadId 保证幂等)
*/
const runUpload = useCallback(
async (item: UploadItem, handle?: DirectUploadHandle) => {
async (item: UploadItem, handle?: DirectUploadHandle, completeOnly = false) => {
let stage: UploadFailStage = "prepare"
try {
// 1. prepare(重试时复用已准备的 handle 也行,但签名可能过期,重新 prepare 最稳)
const h =
handle ??
(await prepareDirectUploadHandle({ file: item.file, library_id: effectiveLibId }))
let h = handle
if (completeOnly && h) {
// ── complete 重试:文件已在 OSS,直接幂等重发确认 ──
stage = "complete"
updateItem(item.tempId, {
status: "ingesting",
progress: 100,
error: undefined,
failedStage: undefined,
hint: undefined,
})
const result = await h.complete()
refreshList()
handlesRef.current.delete(item.tempId)
if (result.duplicated) {
updateItem(item.tempId, { status: "done", duplicated: true, assetId: result.asset_id })
message.info(`"${item.fileName}" 与素材库已有内容相同,已跳过`)
} else {
updateItem(item.tempId, { status: "done", assetId: result.asset_id || item.assetId })
message.success(`"${item.fileName}" 上传完成,正在转码处理`)
}
return
}
// 1. 计算文件内容哈希(失败不阻塞,降级为不传 hash;后端仍有幂等 token 兜底)
updateItem(item.tempId, { hint: "正在计算文件指纹…" })
const fileHash = item.fileHash || (await computeFileHash(item.file))
updateItem(item.tempId, { fileHash, hint: undefined })
// 2. prepare(携带 file_hash + 幂等 token;重试时复用同一 clientUploadId)
stage = "prepare"
h =
h ??
(await prepareDirectUploadHandle({
file: item.file,
library_id: effectiveLibId,
fileHash,
clientUploadId: item.clientUploadId,
}))
handlesRef.current.set(item.tempId, h)
// prepare 阶段后端 file_hash 命中素材库已有相同文件(skip_transfer / duplicated):
// 立即标记 done、调一次 refreshList 让已存在素材立即显示,跳过 transfer + complete
if (h.prepared.skip_transfer || h.prepared.duplicated) {
updateItem(item.tempId, {
status: "done",
duplicated: true,
assetId: h.prepared.asset_id,
})
handlesRef.current.delete(item.tempId)
refreshList()
message.info(`"${item.fileName}" 与素材库已有内容相同,已跳过`)
return
}
if (h.prepared.asset_id) {
updateItem(item.tempId, {
status: "uploading",
@@ -72,26 +167,59 @@ export function useAssetUpload({ effectiveLibId }: { effectiveLibId: string }) {
updateItem(item.tempId, { status: "uploading", progress: 0 })
}
// 2. OSS 直传(真实进度)
// 3. OSS 直传(真实进度)
stage = "transfer"
await h.transfer((pct) => updateItem(item.tempId, { progress: pct }))
// 3. complete:后端创建 ingest job,素材进入转码
// 4. complete:后端确认入库并创建 ingest job(file_hash + 幂等 token 已在 handle 闭包中)
stage = "complete"
updateItem(item.tempId, { status: "ingesting", progress: 100 })
const result = await h.complete()
refreshList()
handlesRef.current.delete(item.tempId)
if (result.duplicated) {
updateItem(item.tempId, { status: "done", duplicated: true, assetId: result.asset_id })
message.info(`"${item.fileName}" 与素材库已有内容相同,已跳过`)
} else {
updateItem(item.tempId, { status: "done" })
updateItem(item.tempId, { status: "done", assetId: result.asset_id || item.assetId })
message.success(`"${item.fileName}" 上传完成,正在转码处理`)
}
} catch (err: unknown) {
const detail = err instanceof Error ? err.message : "上传失败"
console.error("[useAssetUpload] 上传失败:", item.fileName, err)
updateItem(item.tempId, { status: "error", error: detail })
message.error(`"${item.fileName}" 上传失败:${detail}`)
// 完整失败原因:HTTP 状态码 / OSS XML 的 Code+Message / 后端 detail,
// 由 getErrorMessage 统一提取(OSS XHR 错误自带「OSS 直传失败: HTTP xxx ...」明细)
const detail = getErrorMessage(err, "未知错误")
console.error("[useAssetUpload] 上传失败:", item.fileName, stage, err)
if (stage === "complete") {
// complete 失败(超时/5xx/网络):后端记录可能已建成,handle 保留供幂等重试;
// 刷新列表让用户看到可能已创建的「处理中」素材,避免误以为没传上去而重复操作。
// 卡片同时展示真实错误原因 + 安全重试提示(重试只重发 complete,不重新上传)
refreshList()
updateItem(item.tempId, {
status: "error",
failedStage: "complete",
error: `${detail}\n${COMPLETE_RETRY_HINT}`,
hint: undefined,
})
if (!isErrorMsgShown(err)) {
message.error(`"${item.fileName}" 确认入库失败:${detail}`)
}
} else {
// prepare / transfer 失败:后端尚无素材记录,可安全全量重跑
handlesRef.current.delete(item.tempId)
updateItem(item.tempId, {
status: "error",
failedStage: stage === "transfer" ? "transfer" : "prepare",
error: detail,
hint: undefined,
})
// 拦截器已对后端错误弹过 toast(含真实 detail)时不重复弹;
// OSS XHR 直传错误不走 axios,必须在这里弹
if (!isErrorMsgShown(err)) {
message.error(`"${item.fileName}" ${STAGE_LABEL[stage]}失败:${detail}`)
}
}
}
},
[effectiveLibId, refreshList, updateItem],
@@ -113,7 +241,9 @@ export function useAssetUpload({ effectiveLibId }: { effectiveLibId: string }) {
if (!next) return
claimedRef.current.add(next.tempId)
inFlightRef.current += 1
void runUpload(next).finally(() => {
// complete 阶段失败的重试:复用留存的 handle,只重发 complete
const existingHandle = handlesRef.current.get(next.tempId)
void runUpload(next, existingHandle, existingHandle !== undefined).finally(() => {
inFlightRef.current -= 1
claimedRef.current.delete(next.tempId)
// 一个任务结束(成功/失败)后继续拉起排队任务
@@ -126,7 +256,11 @@ export function useAssetUpload({ effectiveLibId }: { effectiveLibId: string }) {
pumpRef.current()
}, [items])
/** 入队一个或多个文件 */
/**
* 入队一个或多个文件(按文件指纹去重):
* - 同一文件已在队列且 preparing/uploading/ingesting/done → 跳过,不重复入队
* - 同一文件此前失败(error)→ 重新激活原队列项(复用 clientUploadId,保持幂等语义)
*/
const enqueueUploads = useCallback(
(files: File[]) => {
if (!effectiveLibId) {
@@ -143,37 +277,86 @@ export function useAssetUpload({ effectiveLibId }: { effectiveLibId: string }) {
}
if (valid.length === 0) return
const newItems: UploadItem[] = valid.map((file, idx) => ({
tempId: `${Date.now()}-${idx}-${Math.random().toString(36).slice(2, 8)}`,
file,
fileName: file.name,
progress: 0,
status: "preparing",
}))
setItems((prev) => [...prev, ...newItems])
let skipped = 0
let rearmed = 0
const newItems: UploadItem[] = []
for (const file of valid) {
const fileKey = makeFileFingerprint(file)
// error 项允许重新激活;其余状态(preparing/uploading/ingesting/done)都算重复
const dup = findDuplicateInQueue([...itemsRef.current, ...newItems], fileKey, ["error"])
if (dup) {
skipped += 1
continue
}
// 失败项重新激活:复用 tempId/clientUploadId,由 pump 按留存 handle 决定重试方式
const failed = itemsRef.current.find(
(it) => it.fileKey === fileKey && it.status === "error",
)
if (failed) {
rearmed += 1
updateItem(failed.tempId, {
status: "preparing",
progress: 0,
error: undefined,
failedStage: undefined,
hint: undefined,
})
continue
}
newItems.push({
tempId: `${Date.now()}-${newItems.length}-${Math.random().toString(36).slice(2, 8)}`,
file,
fileName: file.name,
progress: 0,
status: "preparing",
fileKey,
clientUploadId: makeClientUploadId(),
})
}
if (newItems.length > 0) {
setItems((prev) => [...prev, ...newItems])
}
if (skipped > 0) {
message.warning(`已跳过 ${skipped} 个重复文件(已在上传队列、处理中或本页已上传)`)
}
if (rearmed > 0) {
message.info(`已重新加入 ${rearmed} 个此前失败的文件`)
}
},
[effectiveLibId],
[effectiveLibId, updateItem],
)
/** 重试失败任务 */
/**
* 重试失败任务(仅限 status=error):
* - complete 阶段失败:复用留存 handle 只重发 complete(幂等,不重新上传)
* - prepare/transfer 阶段失败:全量重跑(后端尚无记录,安全)
*/
const retryUpload = useCallback(
(tempId: string) => {
const target = itemsRef.current.find((it) => it.tempId === tempId)
if (!target) return
updateItem(tempId, { status: "preparing", progress: 0, error: undefined })
// 状态更新后由 useEffect 触发 pump
if (!target || target.status !== "error") return
updateItem(tempId, { status: "preparing", progress: 0, error: undefined, hint: undefined })
// 状态更新后由 useEffect 触发 pump;pump 会按 handlesRef 自动选择 completeOnly / 全量
},
[updateItem],
)
/** 从上传列表移除(已进入转码的由素材网格管理;这里只移除上传面板记录) */
const removeUpload = useCallback((tempId: string) => {
handlesRef.current.delete(tempId)
setItems((prev) => prev.filter((it) => it.tempId !== tempId))
}, [])
/** 清空已完成/去重记录 */
const clearFinished = useCallback(() => {
setItems((prev) => prev.filter((it) => it.status !== "done"))
setItems((prev) => {
for (const it of prev) {
if (it.status === "done") handlesRef.current.delete(it.tempId)
}
return prev.filter((it) => it.status !== "done")
})
}, [])
const activeCount = items.filter(
@@ -188,8 +371,10 @@ export function useAssetUpload({ effectiveLibId }: { effectiveLibId: string }) {
retryUpload,
removeUpload,
clearFinished,
/** 是否有进行中的上传(用于上传区文案) */
/** 是否有进行中的上传(用于上传区文案/禁用入口) */
uploading: hasActive,
/** 是否有文件正在本地处理或直传(用于禁用上传入口,防重复提交) */
transferActive: activeCount > 0,
activeCount,
pendingCount,
}
@@ -10,6 +10,26 @@ import {
import { getOrCreateDefaultProject } from "@/api/projects"
import { mapLibrary, mapAsset, type AssetItem, type LibraryItem } from "../types"
/** 处理中素材快速轮询(3s)的最大持续时间:超过后停止快轮询,避免孤儿任务永久转圈 */
const PROCESSING_POLL_MAX_MS = 10 * 60 * 1000 // 10 分钟
const isProcessingStatus = (st?: string | null): boolean =>
st === "uploading" || st === "ingesting" || st === "processing" || st === "pending"
/** 判断列表是否存在「创建超过 maxMs 仍在处理中」的卡死素材 */
const hasStalledProcessing = (
list: ApiAssetItem[],
maxMs: number = PROCESSING_POLL_MAX_MS,
): boolean => {
const now = Date.now()
return list.some((a) => {
if (!isProcessingStatus(a.status ?? "")) return false
if (!a.created_at) return false
const created = new Date(a.created_at).getTime()
return Number.isFinite(created) && now - created > maxMs
})
}
/**
* 素材库数据 Hook
* 封装视频库列表、素材列表的数据查询,以及筛选、搜索状态管理
@@ -63,15 +83,15 @@ export function useAssetsData() {
}),
enabled: !!effectiveLibId,
staleTime: 30_000,
// 列表中存在上传中/转码中素材时每 3s 轮询;全部就绪后自动停止
// 列表中存在上传中/转码中素材时每 3s 轮询;全部就绪后自动停止。
// 但若处理中素材创建已超过 10 分钟仍未就绪(疑似后端卡住/孤儿任务),
// 停止快轮询避免无限转圈——卡死素材在网格中显示「处理超时」提示。
refetchInterval: (query) => {
const data = query.state.data as { items: ApiAssetItem[] } | undefined
const items = data?.items ?? []
const processing = items.some((a) => {
const st = a.status ?? ""
return st === "uploading" || st === "ingesting" || st === "processing" || st === "pending"
})
return processing ? 3000 : false
const processing = items.some((a) => isProcessingStatus(a.status ?? ""))
if (!processing) return false
return hasStalledProcessing(items) ? false : 3000
},
})
@@ -80,6 +100,21 @@ export function useAssetsData() {
[apiAssets],
)
/** 创建超过 10 分钟仍在处理中的素材(后端可能卡住),网格提示「处理超时」 */
const stalledAssetIds = useMemo(() => {
const list = Array.isArray(apiAssets?.items) ? apiAssets.items : []
const ids = new Set<string>()
const now = Date.now()
for (const a of list) {
if (!isProcessingStatus(a.status ?? "") || !a.created_at) continue
const created = new Date(a.created_at).getTime()
if (Number.isFinite(created) && now - created > PROCESSING_POLL_MAX_MS) {
ids.add(a.id)
}
}
return ids
}, [apiAssets])
/* ── 筛选状态 ── */
const [searchText, setSearchText] = useState("")
const [filterType, setFilterType] = useState<string>("all")
@@ -129,6 +164,8 @@ export function useAssetsData() {
assetsError,
assetsErrorObj,
refetchAssets,
stalledAssetIds,
hasStalledAssets: stalledAssetIds.size > 0,
// 筛选
searchText,
setSearchText,
+32 -24
View File
@@ -5,8 +5,8 @@ import React, { useState } from "react"
import { Form, Input, Checkbox, message } from "antd"
import { Link, useNavigate } from "react-router-dom"
import { useLogin } from "@/hooks/useAuth"
import { getWechatAuthUrl } from "@/api/auth"
import Button from "@/components/ui/Button"
import WechatQrModal from "@/components/auth/WechatQrModal"
import "./Login.css"
interface LoginFormValues {
@@ -19,7 +19,7 @@ const Login: React.FC = () => {
const navigate = useNavigate()
const loginMutation = useLogin()
const [form] = Form.useForm()
const [wechatLoading, setWechatLoading] = useState(false)
const [wechatQrOpen, setWechatQrOpen] = useState(false)
const onFinish = async (values: LoginFormValues) => {
try {
@@ -35,27 +35,28 @@ const Login: React.FC = () => {
}
}
const handleWechatLogin = async () => {
try {
setWechatLoading(true)
const result = await getWechatAuthUrl()
// 保存 state 到 localStorage 用于回调时验证
localStorage.setItem("wechat_state", result.state)
// 记录登录前的来源页,登录成功后跳回
const from = window.location.pathname + window.location.search
if (from !== "/login" && from !== "/register") {
localStorage.setItem("login_redirect", from)
} else {
localStorage.removeItem("login_redirect")
}
// 跳转到微信授权页
window.location.href = result.auth_url
} catch (error) {
if (!(error as { __msgShown?: boolean })?.__msgShown)
message.error("微信登录暂不可用,请稍后重试")
} finally {
setWechatLoading(false)
const handleWechatLogin = () => {
// 记录登录前的来源页,登录成功后(弹窗回调)跳回
const from = window.location.pathname + window.location.search
if (from !== "/login" && from !== "/register") {
localStorage.setItem("login_redirect", from)
} else {
localStorage.removeItem("login_redirect")
}
setWechatQrOpen(true)
// 弹窗打开期间按钮 disabled;WxLogin 脚本加载失败/超时时弹窗内会自动回退整页跳转
}
// 弹窗扫码登录成功:登录态已由弹窗同步,按用户类型跳转
const handleWechatQrSuccess = (needOnboarding: boolean) => {
setWechatQrOpen(false)
if (needOnboarding) {
navigate("/welcome/wechat", { replace: true })
return
}
const redirect = localStorage.getItem("login_redirect") || "/"
localStorage.removeItem("login_redirect")
navigate(redirect, { replace: true })
}
return (
@@ -126,10 +127,10 @@ const Login: React.FC = () => {
type="button"
className="xx-btn-wechat"
onClick={handleWechatLogin}
disabled={wechatLoading}
disabled={wechatQrOpen}
>
<span className="xx-wechat-icon">💬</span>
{wechatLoading ? "加载中..." : "微信登录"}
微信登录
</button>
</div>
@@ -137,6 +138,13 @@ const Login: React.FC = () => {
还没有账号? <Link to="/register">立即注册</Link>
</div>
</div>
<WechatQrModal
open={wechatQrOpen}
scene="login"
onClose={() => setWechatQrOpen(false)}
onLoginSuccess={handleWechatQrSuccess}
/>
</div>
)
}
@@ -0,0 +1,118 @@
/**
* 微信绑定回调页(已登录用户在设置页发起"绑定微信"扫码后回到这里)
* 用 code 调绑定接口把微信关联到当前账号,成功后回设置页
*
* 两种运行环境:
* - 整页跳转授权(旧流程/兜底):本页整页加载,成功/失败后 navigate 回设置页
* - 弹窗内嵌二维码(WxLogin self_redirect):本页在同源 iframe 内加载,
* 结果通过 postMessage 通知父窗口弹窗,不做页面导航
*/
import React, { useEffect, useState } from "react"
import { useSearchParams, useNavigate } from "react-router-dom"
import { Spin } from "antd"
import { bindWechat, normalizeUser } from "@/api/auth"
import { getErrorMessage } from "@/api/errors"
import { useAuthStore } from "@/store/authStore"
import { isInIframe, postWechatQrResult } from "@/components/auth/WechatQrModal/messages"
const WechatBindCallback: React.FC = () => {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const setUser = useAuthStore((state) => state.setUser)
const [error, setError] = useState<string | null>(null)
const inIframe = isInIframe()
useEffect(() => {
const code = searchParams.get("code")
const state = searchParams.get("state")
const fail = (message: string) => {
if (inIframe) {
// 弹窗模式:把真实原因上报父窗口在 Modal 内展示
postWechatQrResult("bind", false, { detail: message })
return
}
setError(message)
}
if (!code || !state) {
fail("无效的回调参数,请回到设置页重新扫码绑定")
return
}
const handleBind = async () => {
// state 校验由后端 state store 一次性消费兜底(前端不再比对 localStorage,
// 微信内打开/跨浏览器场景本地无 state 会误杀);清理绑定前写入的 state
localStorage.removeItem("wechat_bind_state")
try {
const result = await bindWechat(code, state)
setUser(normalizeUser(result.user))
if (inIframe) {
// 弹窗模式:通知父窗口关闭弹窗并刷新绑定状态
postWechatQrResult("bind", true)
return
}
// 用 replace 回设置页,query 携带成功标记由设置页提示
navigate("/app/profile?wechat_bind=success", { replace: true })
} catch (err) {
// 绑定失败直接在本页展示/上报真实原因(如微信已被其他账号绑定),不静默跳走
fail(`微信绑定失败:${getErrorMessage(err, "请回到设置页重试")}`)
}
}
handleBind()
}, [searchParams, navigate, setUser, inIframe])
if (error) {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
background: "#f5f5f5",
}}
>
<div style={{ textAlign: "center" }}>
<p style={{ color: "#ef4444", fontSize: 16, marginBottom: 16 }}>{error}</p>
<button
onClick={() => navigate("/app/profile")}
style={{
padding: "8px 24px",
background: "var(--primary-color, #3b82f6)",
color: "white",
border: "none",
borderRadius: 6,
cursor: "pointer",
}}
>
返回设置
</button>
</div>
</div>
)
}
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
background: "#f5f5f5",
}}
>
<div style={{ textAlign: "center" }}>
<Spin size="large" />
<p style={{ marginTop: 16, color: "#666" }}>正在绑定微信...</p>
</div>
</div>
)
}
export default WechatBindCallback
+83 -77
View File
@@ -1,40 +1,63 @@
/**
* 微信登录回调页
* 扫码授权后由微信重定向回来:用 code 换登录态,
* 新用户/资料未完善 → 跳昵称引导页;老用户 → 回来源页/首页
*
* 两种运行环境:
* - 整页跳转授权(旧流程/兜底):本页整页加载,按上述逻辑导航
* - 弹窗内嵌二维码(WxLogin self_redirect):本页在同源 iframe 内加载,
* 成功/失败均通过 postMessage 通知父窗口弹窗,不做页面导航
*/
import React, { useEffect, useState } from "react"
import { useSearchParams, useNavigate } from "react-router-dom"
import { Spin, message } from "antd"
import { Spin } from "antd"
import { wechatCallback, getCurrentUser, normalizeUser, type User } from "@/api/auth"
import { getErrorMessage } from "@/api/errors"
import { useAuthStore } from "@/store/authStore"
import BindContactModal from "@/components/auth/BindContactModal"
import { scheduleProactiveRefresh } from "@/api/auth/tokenRefresh"
import { isInIframe, postWechatQrResult } from "@/components/auth/WechatQrModal/messages"
const WechatCallback: React.FC = () => {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const setAuth = useAuthStore((state) => state.setAuth)
const [loading, setLoading] = useState(true)
const [showBindModal, setShowBindModal] = useState(false)
const [error, setError] = useState<string | null>(null)
const inIframe = isInIframe()
useEffect(() => {
const code = searchParams.get("code")
const state = searchParams.get("state")
if (!code || !state) {
setError("无效的回调参数")
const fail = (message: string) => {
if (inIframe) {
// 弹窗模式:把真实原因上报父窗口在 Modal 内展示,本页保持"处理中"即可
postWechatQrResult("login", false, { detail: message })
return
}
setError(message)
setLoading(false)
}
// 微信重定向出错时(如用户拒绝授权 error=access_denied)直接展示/上报原因
const wxErrorCode = searchParams.get("error")
const wxErrDesc = searchParams.get("error_description")
if (wxErrorCode || wxErrDesc) {
const reason = [wxErrorCode, wxErrDesc].filter(Boolean).join(":")
fail(`微信授权失败:${reason}`)
return
}
if (!code || !state) {
fail("无效的回调参数,请重新扫码登录")
return
}
const handleCallback = async () => {
try {
// 校验 state,防止 CSRF
const savedState = localStorage.getItem("wechat_state")
if (!savedState || savedState !== state) {
setError("安全校验失败,请重新登录")
setLoading(false)
return
}
// state 的 CSRF 校验由后端 state store 一次性消费兜底(前端不再比对
// localStorage——微信内打开、跨浏览器等场景本地没有 state,会误杀正常回调);
// 清理登录前写入的 state,避免残留
localStorage.removeItem("wechat_state")
const result = await wechatCallback(code, state)
@@ -49,41 +72,34 @@ const WechatCallback: React.FC = () => {
const userData = await getCurrentUser()
const user: User = normalizeUser(userData)
setAuth(user, result.access_token, result.refresh_token)
scheduleProactiveRefresh()
if (result.binding_complete) {
// 已绑定,跳转到登录前页面或首页
message.success("登录成功")
const redirect = localStorage.getItem("login_redirect") || "/"
localStorage.removeItem("login_redirect")
navigate(redirect, { replace: true })
} else {
// 未绑定,显示绑定弹窗
setLoading(false)
setShowBindModal(true)
// 新用户 或 资料未完善(如上次中断没填昵称)→ 强制昵称引导
const needOnboarding = result.is_new_user || user.profile_completed === false
if (inIframe) {
// 弹窗模式:token 已写入同源 localStorage,通知父窗口同步登录态并跳转
postWechatQrResult("login", true, { needOnboarding })
return
}
if (needOnboarding) {
navigate("/welcome/wechat", { replace: true })
return
}
// 老用户:回登录前页面或首页
const redirect = localStorage.getItem("login_redirect") || "/"
localStorage.removeItem("login_redirect")
navigate(redirect, { replace: true })
} catch (err) {
setError("登录失败,请重试")
setLoading(false)
// 透传后端真实错误(如 state 过期、code 已消费、接口异常),禁止吞成通用提示
fail(`微信登录失败:${getErrorMessage(err, "请重试或更换登录方式")}`)
}
}
handleCallback()
}, [searchParams, navigate, setAuth])
const handleBindSuccess = (user: User) => {
const setUser = useAuthStore.getState().setUser
setUser(user)
setShowBindModal(false)
message.success("绑定成功")
const redirect = localStorage.getItem("login_redirect") || "/"
localStorage.removeItem("login_redirect")
navigate(redirect, { replace: true })
}
const handleBindCancel = () => {
setShowBindModal(false)
navigate("/login")
}
}, [searchParams, navigate, setAuth, inIframe])
if (loading) {
return (
@@ -98,49 +114,39 @@ const WechatCallback: React.FC = () => {
>
<div style={{ textAlign: "center" }}>
<Spin size="large" />
<p style={{ marginTop: 16, color: "#666" }}>正在登录...</p>
</div>
</div>
)
}
if (error) {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
background: "#f5f5f5",
}}
>
<div style={{ textAlign: "center" }}>
<p style={{ color: "#ef4444", fontSize: 16, marginBottom: 16 }}>{error}</p>
<button
onClick={() => navigate("/login")}
style={{
padding: "8px 24px",
background: "var(--primary-color, #3b82f6)",
color: "white",
border: "none",
borderRadius: 6,
cursor: "pointer",
}}
>
返回登录
</button>
<p style={{ marginTop: 16, color: "#666" }}>微信登录中...</p>
</div>
</div>
)
}
return (
<BindContactModal
open={showBindModal}
onSuccess={handleBindSuccess}
onCancel={handleBindCancel}
/>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "100vh",
background: "#f5f5f5",
}}
>
<div style={{ textAlign: "center" }}>
<p style={{ color: "#ef4444", fontSize: 16, marginBottom: 16 }}>{error}</p>
<button
onClick={() => navigate("/login")}
style={{
padding: "8px 24px",
background: "var(--primary-color, #3b82f6)",
color: "white",
border: "none",
borderRadius: 6,
cursor: "pointer",
}}
>
返回登录
</button>
</div>
</div>
)
}
@@ -0,0 +1,116 @@
/**
* 微信新用户昵称引导页
* 新微信用户首次登录后强制填写昵称,完成后才进入主界面
*/
import React, { useRef } from "react"
import { Form, Input, message } from "antd"
import { Navigate, useNavigate } from "react-router-dom"
import { useMutation } from "@tanstack/react-query"
import { updateProfile } from "@/api/auth"
import { getErrorMessage, isErrorMsgShown } from "@/api/errors"
import { useAuthStore } from "@/store/authStore"
import Button from "@/components/ui/Button"
import "./Login.css"
interface OnboardingFormValues {
display_name: string
}
const WechatOnboarding: React.FC = () => {
const navigate = useNavigate()
const setUser = useAuthStore((state) => state.setUser)
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
const user = useAuthStore((state) => state.user)
const hasAccessToken = Boolean(localStorage.getItem("access_token"))
const [form] = Form.useForm<OnboardingFormValues>()
// 同步防连点守卫:antd loading 要等 React 重渲染后才禁用按钮,
// 连点两次时第一次的 mutation 刚触发、重渲染未发生,第二次 click 仍会进来
// (截图里 PATCH /me 405 出现两次就是连点导致的重复提交)
const submittingRef = useRef(false)
const saveMutation = useMutation({
mutationFn: (displayName: string) => updateProfile({ display_name: displayName }),
})
// 已登录且资料已完善的用户不该停留在引导页
if (isAuthenticated && hasAccessToken && user?.profile_completed === true) {
return <Navigate to="/app/dashboard" replace />
}
// 未登录(如手动输入 URL)回登录页
if (!isAuthenticated || !hasAccessToken) {
return <Navigate to="/login" replace />
}
const onFinish = async (values: OnboardingFormValues) => {
if (submittingRef.current) return
submittingRef.current = true
try {
const updated = await saveMutation.mutateAsync(values.display_name.trim())
// 后端返回的 profile_completed 以最新资料为准,前端同步标记完善
setUser({ ...updated, profile_completed: true })
message.success("欢迎加入小虾智剪!")
const redirect = localStorage.getItem("login_redirect") || "/app/dashboard"
localStorage.removeItem("login_redirect")
navigate(redirect, { replace: true })
} catch (err) {
// 透传后端真实原因(如接口异常/校验失败);拦截器已弹过的不重复弹
if (!isErrorMsgShown(err)) {
message.error(`昵称保存失败:${getErrorMessage(err, "请稍后重试")}`)
}
submittingRef.current = false
}
// 成功时页面跳走,不复位
}
return (
<div className="xx-auth-page">
<div className="xx-auth-card">
<div className="xx-auth-header">
<div className="xx-auth-brand">
<span className="xx-auth-logo">🦐</span>
<span className="xx-auth-brand-name">小虾智剪</span>
</div>
<p>欢迎使用微信登录,请先设置您的昵称</p>
</div>
<Form
form={form}
name="wechat-onboarding"
onFinish={onFinish}
autoComplete="off"
layout="vertical"
// 不预填:新微信用户必须自己输入昵称(user.display_name 可能是微信昵称/系统占位)
initialValues={{ display_name: "" }}
>
<Form.Item
name="display_name"
label="昵称"
rules={[
{ required: true, message: "请输入昵称" },
{ whitespace: true, message: "昵称不能为空白" },
{ min: 1, max: 20, message: "昵称长度需在 1-20 个字符之间" },
]}
extra="昵称将展示在您的作品和账户中,之后可在个人设置中修改"
>
<Input placeholder="请输入您的昵称" size="large" maxLength={20} showCount />
</Form.Item>
<Form.Item>
<Button
buttonType="primary"
buttonSize="lg"
htmlType="submit"
loading={saveMutation.isPending}
disabled={saveMutation.isPending}
style={{ width: "100%" }}
>
{saveMutation.isPending ? "保存中..." : "进入小虾智剪"}
</Button>
</Form.Item>
</Form>
</div>
</div>
)
}
export default WechatOnboarding
@@ -108,7 +108,10 @@ export function usePlanLoading({
id: `seg-${idx}`,
template_segment_id: `seg-${idx}`,
type: (seg.material_type === "voiceover" ? "voice" : "pip") as ClipType,
duration: (seg.duration_min + seg.duration_max) / 2,
duration:
seg.duration_min != null && seg.duration_max != null
? (seg.duration_min + seg.duration_max) / 2
: 5,
startOffset: 0,
script_text: "",
order: seg.segment_order,
@@ -43,7 +43,10 @@ export function useTemplateDetail({
id: seg.id || `seg-${idx}`,
template_segment_id: seg.id || `seg-${idx}`,
type: (seg.material_type === "voiceover" ? "voice" : "pip") as ClipType,
duration: (seg.duration_min + seg.duration_max) / 2,
duration:
seg.duration_min != null && seg.duration_max != null
? (seg.duration_min + seg.duration_max) / 2
: 5,
startOffset: 0,
script_text: "",
order: seg.segment_order,
+148 -18
View File
@@ -27,6 +27,8 @@ import { useStepNavigation } from "./hooks/useStepNavigation"
import { useGenerateVideo } from "./hooks/useGenerateVideo"
import { usePreviewAssets } from "./hooks/usePreviewAssets"
import { useBatchVariantPlans } from "./hooks/useBatchVariantPlans"
import { useVariantVoicePreview } from "./hooks/useVariantVoicePreview"
import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters"
import { getAssetsByKind } from "@/api/assets"
import { previewTts } from "@/api/tts"
@@ -132,16 +134,21 @@ const GeneratePage: React.FC = () => {
const [previewVoiceAudioUrl, setPreviewVoiceAudioUrl] = useState<string | null>(null)
const ttsAbortRef = useRef<AbortController | null>(null)
// TTS 试听文案:批量跟随变体0标题(仅取首项,避免编辑其他变体标题触发多余 TTS 请求)
const variant0Title = isBatch ? previewTitles?.[0] || "" : ""
useEffect(() => {
// #1750:批量模式每变体独立/共用配音由 useVariantVoicePreview 接管(含独立模式每卡片挂各自 URL),
// 本 effect 只服务单视频,批量模式直接跳过(避免共用 TTS 结果错误下发给所有变体)
if (isBatch) return
const voiceAsset = voiceMaterials.find((m) => m.id === selectedVoice)
if (voiceAsset?.file_url) {
setPreviewVoiceAudioUrl(voiceAsset.file_url)
return
}
// 批量模式下 TTS 文案跟随变体0标题;单视频跟随主标题
const ttsTitle = isBatch ? previewTitles[0] || "" : titleSettings.title
// 单视频 TTS 文案跟随主标题
const ttsTitle = titleSettings.title
const voiceId = selectedClonedVoice || selectedVoice
if (!voiceId || !ttsTitle) {
setPreviewVoiceAudioUrl(null)
@@ -174,7 +181,7 @@ const GeneratePage: React.FC = () => {
selectedVoice,
selectedClonedVoice,
titleSettings.title,
previewTitles,
variant0Title,
isBatch,
voiceMaterials,
])
@@ -211,14 +218,56 @@ const GeneratePage: React.FC = () => {
/* ── 加载素材详情(供前端预览播放器使用) ── */
const previewAssetsEnabled = previewAssetIds.length > 0
const { assets: previewAssets, ready: previewAssetsReady } = usePreviewAssets(
previewAssetIds,
previewAssetsEnabled,
)
const {
assets: previewAssets,
ready: previewAssetsReady,
ensureAssets,
} = usePreviewAssets(previewAssetIds, previewAssetsEnabled)
/* ── 预览就绪:纯前端 Canvas 预览,素材详情加载完即可秒开(单视频/批量一致) ── */
const previewReady = previewAssetsReady && !!currentTemplate
/* ── 批量变体真实片段(#1744):后端独立选片,预览即成片;失败静默降级本地模拟 ──
仅批量(N>1)且在第 4 步预览时申请,避免选素材阶段频繁请求;
变体 0 沿用草稿 plan(与单视频一致),变体 1..N-1 后端 reselect 独立选片 */
const {
clipsByVariant: variantClips,
planIdsByVariant: variantPlanIds,
loading: variantClipsLoading,
error: variantClipsError,
retry: retryVariantClips,
} = useBatchVariantPlans({
enabled: isBatch && currentStep === 4 && previewAssetsReady,
count: previewCount,
templateId: selectedTemplate || "",
assetIds: previewAssetIds,
sourcePlanId: storedSourceEditPlanId || sourceEditPlanId || "",
})
/* ── 批量变体配音预览 URL(#1750):独立模式每变体挂各自配音,共用模式全挂同一条;
hook 内部自取配音素材库并做 TTS 缓存/串行/防竞态 */
const variantVoiceAudioUrls = useVariantVoicePreview({
enabled: isBatch,
count: previewCount,
perVideo: voiceModePerVideo,
sharedVoiceId: selectedVoice || "",
clonedVoiceId: selectedClonedVoice || "",
variantVoiceIds: voiceLibraryIds || [],
titles: previewTitles || [],
})
/* ── 变体计划 clips 引用的素材可能不在已选列表中(#1750):差集补拉素材详情,
保证播放器拿得到 file_url,而不是静默丢片段/用假数据冒充 */
const clipAssetIds = useMemo(() => {
if (!isBatch || !variantClips?.length) return []
const ids = new Set<string>()
variantClips.forEach((list) => list.forEach((c) => c.asset_id && ids.add(c.asset_id)))
return Array.from(ids)
}, [isBatch, variantClips])
useEffect(() => {
if (clipAssetIds.length > 0) void ensureAssets(clipAssetIds)
}, [clipAssetIds, ensureAssets])
/* ── 勾选变体 ── */
const toggleVariantSelect = useCallback(
(index: number) => {
@@ -260,6 +309,8 @@ const GeneratePage: React.FC = () => {
autoSubtitles,
bgm,
sourceEditPlanId: storedSourceEditPlanId || sourceEditPlanId,
// #1744:批量预览阶段后端为每个变体生成的独立 plan id,正式生成回传 → 预览即成片
variantPlanIds,
previewTaskId,
bgmConfig,
previewCount,
@@ -321,9 +372,13 @@ const GeneratePage: React.FC = () => {
message.warning("请为每个勾选的视频输入标题")
return
}
} else if (!titleSettings.aiAutoSelect && !titleSettings.title?.trim()) {
message.warning("请先选择或输入标题")
return
} else if (!titleSettings.title?.trim()) {
// 与 buildPayload.validateGenerateInputs 一致:AI 自动选标题模式(aiAutoSelect)
// 允许空标题由后端生成;手动模式必须填写,避免提交空标题
if (!titleSettings.aiAutoSelect) {
message.warning("请先选择或输入标题")
return
}
}
if (!previewReady) {
message.warning("预览素材正在加载,请稍候")
@@ -365,7 +420,9 @@ const GeneratePage: React.FC = () => {
const layoutClassName = useMemo(() => {
if (currentStep < 4) return "xx-generate-layout full-width"
if (currentStep === 4) return "xx-generate-layout step4-layout"
// 步骤5/6:批量网格需要整行宽度;单视频保持 表单+右侧成片 两栏
// 步骤5:全宽+内容居中(单视频视频播放器居中,批量网格居中)
if (currentStep === 5) return "xx-generate-layout full-width"
// 步骤6:封面选择保持两栏布局
return isBatch ? "xx-generate-layout full-width" : "xx-generate-layout"
}, [currentStep, isBatch])
@@ -387,7 +444,6 @@ const GeneratePage: React.FC = () => {
/* 单视频:前端 Canvas 实时预览(与旧版一致,零回归) */
<FrontendPreviewPlayer
assets={previewAssets}
template={currentTemplate}
videoRatio={videoRatio}
ready={previewAssets.length > 0}
serverClips={serverClips}
@@ -419,11 +475,14 @@ const GeneratePage: React.FC = () => {
<CanvasPreviewGrid
count={previewCount}
assets={previewAssets}
template={currentTemplate}
videoRatio={videoRatio}
titles={previewTitles}
titleSettings={titleSettings}
voiceAudioUrl={previewVoiceAudioUrl || undefined}
voiceAudioUrls={variantVoiceAudioUrls}
variantClips={variantClips}
clipsLoading={variantClipsLoading}
clipsError={variantClipsError}
onRetryClips={retryVariantClips}
selectedIds={selectedVariantIds}
onToggleSelect={toggleVariantSelect}
selectable={!generating}
@@ -496,17 +555,88 @@ const GeneratePage: React.FC = () => {
generateError={generateError}
selectedCount={isBatch ? selectedVariantIds.length : 1}
/>
{/* ════ 步骤5(单视频):成片播放器内联居中(#1761) ════ */}
{currentStep === 5 && !isBatch && generated && finalVideo && (
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: 16,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
background: "#000",
borderRadius: 12,
padding: 8,
maxWidth: 320,
width: "100%",
}}
>
<video
src={finalVideo.download_url || finalVideo.file_url}
controls
style={{
width: "auto",
maxWidth: "100%",
maxHeight: "70vh",
aspectRatio: "9 / 16",
objectFit: "contain",
borderRadius: 8,
}}
poster={finalVideo.thumbnail_url || undefined}
/>
<div style={{ display: "flex", gap: 8, marginTop: 12, justifyContent: "center" }}>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleDownload}>
⬇️ 下载
</button>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleShare}>
🔗 分享
</button>
<button
className="xx-btn xx-btn-ghost xx-btn-sm"
onClick={() => navigate("/app/products")}
>
📁 前往成片库
</button>
</div>
</div>
</div>
)}
</div>
{/* ════ 步骤5/6(单视频):右侧成片播放器 ════ */}
{currentStep >= 5 && !isBatch && generated && finalVideo && (
{/* ════ 步骤6(单视频):右侧成片播放器 ════ */}
{currentStep >= 6 && !isBatch && generated && finalVideo && (
<div className="xx-generate-right-col">
<div className="xx-inline-video-player">
<div
className="xx-inline-video-player"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
background: "#000",
borderRadius: 12,
padding: 8,
}}
>
<video
src={finalVideo.download_url || finalVideo.file_url}
controls
autoPlay={currentStep === 5}
style={{ width: "100%", maxHeight: "70vh", objectFit: "contain", borderRadius: 12 }}
// 竖屏自适应(#1750):成片固定 1080×1920(9:16),元数据到达前按 9:16 占位,
// 到达后浏览器按真实宽高比 contain;黑底居中杜绝左右大黑边
style={{
width: "auto",
maxWidth: "100%",
maxHeight: "70vh",
aspectRatio: "9 / 16",
objectFit: "contain",
borderRadius: 8,
}}
poster={finalVideo.thumbnail_url || undefined}
/>
<div style={{ display: "flex", gap: 8, marginTop: 12, justifyContent: "center" }}>
@@ -22,7 +22,9 @@ const BatchGenerationGrid: React.FC<BatchGenerationGridProps> = ({
titles,
onRetryTask,
}) => {
const sorted = [...tasks].sort((a, b) => a.variantIndex - b.variantIndex)
const sorted = [...tasks].sort(
(a, b) => Number(a.variantIndex || 0) - Number(b.variantIndex || 0),
)
return (
<div className="xx-form-section">
@@ -32,12 +34,26 @@ const BatchGenerationGrid: React.FC<BatchGenerationGridProps> = ({
完成 {tasks.filter((t) => t.status === "completed").length} / {tasks.length}
</span>
</div>
<div className="xx-batch-gen-grid">
<div
className="xx-batch-gen-grid"
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(160px, 180px))",
justifyContent: "center",
justifyItems: "center",
gap: 14,
width: "100%",
}}
>
{sorted.map((task) => {
const title = titles[task.variantIndex] || `视频 ${task.variantIndex + 1}`
const video = (task.videos?.[0] || null) as GeneratedVideo | null
return (
<div key={task.taskId} className={`xx-batch-gen-card status-${task.status}`}>
<div
key={task.taskId}
className={`xx-batch-gen-card status-${task.status}`}
style={{ maxWidth: 240 }}
>
<div className="xx-batch-gen-card-head">
<span className="xx-batch-gen-card-title" title={title}>
{task.status === "completed" ? (
@@ -64,12 +80,32 @@ const BatchGenerationGrid: React.FC<BatchGenerationGridProps> = ({
</>
)}
{task.status === "completed" && video && (
<video
src={video.download_url || video.file_url}
controls
style={{ width: "100%", borderRadius: 8, background: "#000", maxHeight: 280 }}
poster={video.thumbnail_url}
/>
// 竖屏自适应容器(#1750):成片固定 1080×1920(9:16),
// 视频按真实宽高比 contain 显示,黑底居中,杜绝横屏播放器左右大黑边
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
background: "transparent",
borderRadius: 8,
padding: 0,
}}
>
<video
src={video.download_url || video.file_url}
controls
style={{
width: "100%",
maxHeight: "70vh",
aspectRatio: "9 / 16",
objectFit: "cover",
borderRadius: 4,
background: "transparent",
}}
poster={video.thumbnail_url}
/>
</div>
)}
{task.status === "completed" && !video && (
<div className="xx-batch-gen-card-done">✅ 已完成(成片可在下一步选择封面)</div>
@@ -1,27 +1,47 @@
/**
* 批量前端 Canvas 实时预览网格(Issue #1677 修正方案)
* 批量前端 Canvas 实时预览网格(Issue #1677 起,#1750 对齐基线:只播放后端真实计划)
*
* N 个 FrontendPreviewPlayer 网格排列:
* - 纯前端 Canvas + video 元素实时播放素材片段,不调任何后端渲染接口
* - variantSeed 让每个变体素材排布/起始点不同,画面有可见差异
* - 各自叠加独立标题浮层(variantTitle),标题样式全局共用
* - 勾选框决定提交时生成哪些变体
* - 纯前端 Canvas + video 元素实时播放素材片段,不调任何后端渲染接口;
* - #1744/#1750:片段只来自后端变体计划接口(POST /generation/variant-plans)的真实
* clips,与正式批量生成同源自 reselect_plan_for_variant,预览即成片;
* 接口失败/数据不完整 → 整网格显示错误态+重试(严禁本地假数据冒充预览);
* 加载中 → 9:16 占位防塌陷;
* - 各自叠加独立标题浮层(variantTitle),标题样式全局共用;
* - 勾选框决定提交时生成哪些变体;
* - 每个变体挂载各自配音 URL(独立模式 #1750)或共用同一条;播放互斥:
* 点击某卡片播放时其他卡片自动暂停,同一时刻只有一路声音(#1741)。
*/
import React from "react"
import React, { useState } from "react"
import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner"
import type { EditPlanClip } from "@/api/template-editor"
import { LoadingOutlined, ReloadOutlined } from "@ant-design/icons"
import { Button } from "antd"
import type { TitleSettings } from "../types"
import FrontendPreviewPlayer from "./FrontendPreviewPlayer"
interface CanvasPreviewGridProps {
count: number
assets: AssetItem[]
template: EditingTemplate | null
videoRatio: string
titles: string[]
titleSettings: TitleSettings
/** 共用配音预览音频(仅第 1 个变体播放,避免多路音频重叠) */
voiceAudioUrl?: string
/**
* 各变体配音预览音频 URL(#1750:独立配音模式每变体一条;共用模式全为同一条;
* 元素为 null 表示该变体暂无音频(AI 音色 TTS 合成中))
*/
voiceAudioUrls?: (string | null)[]
/**
* 各变体的后端真实片段(#1744/#1750):长度=count。
* 仅 clipsLoading=false 且 clipsError=false 时才会传给播放器。
*/
variantClips?: EditPlanClip[][]
/** 是否正在向后端申请变体计划 */
clipsLoading?: boolean
/** 申请变体计划失败(端点未上线/网络错误/数据不完整):显示错误态,严禁假数据 */
clipsError?: boolean
/** 用户点击错误态「重试」 */
onRetryClips?: () => void
/** 勾选的变体序号 */
selectedIds: number[]
onToggleSelect: (index: number) => void
@@ -32,15 +52,23 @@ interface CanvasPreviewGridProps {
const CanvasPreviewGrid: React.FC<CanvasPreviewGridProps> = ({
count,
assets,
template,
videoRatio,
titles,
titleSettings,
voiceAudioUrl,
voiceAudioUrls,
variantClips,
clipsLoading = false,
clipsError = false,
onRetryClips,
selectedIds,
onToggleSelect,
selectable = true,
}) => {
// ── 播放互斥(#1741):同一时刻只有一个卡片持有播放权(token = 变体序号,0 起,#1750) ──
const [activePlayToken, setActivePlayToken] = useState<number | null>(null)
// count 上限已在源头 PreviewCountModal 的数量选择(1~MAX_PREVIEW_COUNT=10)clamp,
// 这里完整渲染所有变体,保证每个变体都有勾选/预览入口,UI 与数据不脱节
return (
<div className="xx-canvas-grid">
{Array.from({ length: count }, (_, i) => {
@@ -62,29 +90,54 @@ const CanvasPreviewGrid: React.FC<CanvasPreviewGridProps> = ({
<span>视频 {i + 1}</span>
</label>
</div>
<FrontendPreviewPlayer
assets={assets}
template={template}
videoRatio={videoRatio}
ready={assets.length > 0}
variantSeed={i + 1}
variantTitle={titles[i] || ""}
voiceAudioUrl={i === 0 ? voiceAudioUrl : undefined}
compact
titleSettings={{
title: titles[i] || "",
size: titleSettings.size,
font: titleSettings.font,
color: titleSettings.color,
position: titleSettings.position as "top" | "center" | "bottom" | "custom",
bold: titleSettings.bold,
italic: titleSettings.italic,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
posX: titleSettings.posX,
posY: titleSettings.posY,
}}
/>
{clipsError ? (
// ── 错误态(#1750):9:16 占位防塌陷,不渲染任何播放器(严禁假数据) ──
<div className="xx-variant-clips-status" role="alert">
<span className="xx-variant-clips-error-text">预览加载失败,请重试</span>
{i === 0 && onRetryClips ? (
<Button
size="small"
icon={<ReloadOutlined />}
onClick={onRetryClips}
style={{ marginTop: 12 }}
>
重试
</Button>
) : null}
</div>
) : clipsLoading || !variantClips?.[i]?.length ? (
// ── 加载态:9:16 占位防塌陷 ──
<div className="xx-variant-clips-status" aria-label={`变体${i + 1}片段加载中`}>
<LoadingOutlined />
<span style={{ marginTop: 8 }}>独立选片中…</span>
</div>
) : (
<FrontendPreviewPlayer
assets={assets}
videoRatio={videoRatio}
ready={assets.length > 0}
playToken={i}
serverClips={variantClips[i]}
variantTitle={titles[i] || ""}
voiceAudioUrl={voiceAudioUrls?.[i] || undefined}
activePlayToken={activePlayToken}
onPlayTokenChange={setActivePlayToken}
compact
titleSettings={{
title: titles[i] || "",
size: titleSettings.size,
font: titleSettings.font,
color: titleSettings.color,
position: titleSettings.position as "top" | "center" | "bottom" | "custom",
bold: titleSettings.bold,
italic: titleSettings.italic,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
posX: titleSettings.posX,
posY: titleSettings.posY,
}}
/>
)}
</div>
)
})}
@@ -13,18 +13,19 @@ import {
PauseCircleOutlined,
SoundOutlined,
LoadingOutlined,
AudioOutlined,
AudioMutedOutlined,
} from "@ant-design/icons"
import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner"
import type { EditPlanClip } from "@/api/template-editor"
import { useSegmentScheduler, type PlaybackSegment } from "../hooks/useSegmentScheduler"
import { useCanvasPlayer } from "../hooks/useCanvasPlayer"
interface FrontendPreviewPlayerProps {
assets: AssetItem[]
template: EditingTemplate | null
videoRatio: string
ready: boolean
/** 服务端变体计划真实片段(#1750:必填,无 fallback;批量网格传入各变体自己的 clips) */
serverClips?: EditPlanClip[]
voiceAudioUrl?: string
titleSettings?: {
@@ -42,15 +43,21 @@ interface FrontendPreviewPlayerProps {
}
onTitlePositionChange?: (posX: number, posY: number) => void
/**
* 变体种子(批量生成 #1677):同一批素材在不同变体中采用不同的素材顺序与
* 片段起始点,让 N 个 Canvas 预览画面有差异(纯前端随机剪辑模拟,不调后端)。
* 0 / 不传 = 单视频,排布与旧版完全一致(零回归)。
* 播放互斥 token(#1750):批量网格中为变体序号(0 起),单视频不传。
* 持有播放权的实例播放,其余自动暂停。
*/
variantSeed?: number
playToken?: number
/** 变体标题文字(批量时每个预览独立标题,叠加在画面上);不传用 titleSettings.title */
variantTitle?: string
/** 紧凑模式(批量网格中使用,缩小内边距/标题尺寸) */
compact?: boolean
/**
* 批量网格播放互斥(#1741/#1750):当前持有播放权的实例 token(变体序号)。
* 持有权变化且不等于自身时,本实例自动暂停(视频+配音)。单视频模式不传。
*/
activePlayToken?: number | null
/** 播放权变化回调:本实例请求播放时传自身 playToken,暂停时传 null */
onPlayTokenChange?: (token: number | null) => void
}
function formatTime(seconds: number): string {
@@ -60,114 +67,107 @@ function formatTime(seconds: number): string {
}
/**
* 将素材映射为播放片段(复用原逻辑)
* 将后端变体计划 clips 映射为播放片段(#1750:唯一数据来源,无本地模拟 fallback)
*
* 预览不渲染:浏览器按后端真实计划即时播放素材原片,预览即成片。
* 模板片段数固定、成片时长=配音时长(后端等比分配),前端不再有任何时长假设。
*/
/** 简单可复现随机数(mulberry32),同一种子产出稳定排布,避免每次渲染抖动 */
function seededRandom(seed: number): () => number {
let a = seed >>> 0
return () => {
a |= 0
a = (a + 0x6d2b79f5) | 0
let t = Math.imul(a ^ (a >>> 15), 1 | a)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
function buildPlaybackSegments(
assets: AssetItem[],
template: EditingTemplate | null,
serverClips?: EditPlanClip[],
variantSeed = 0,
/** #1754 前端兜底:配音时长≠clips 总时长时,按比例缩放每段播放时长并调速 */
speedFactor = 1,
): PlaybackSegment[] {
if (!assets.length) return []
if (!assets.length || !serverClips || serverClips.length === 0) return []
// Build asset lookup map
const assetMap = new Map(assets.map((a) => [a.id, a]))
// 优先使用服务端 clips(含随机 start_time 和正确数量),与最终生成结果一致
if (serverClips && serverClips.length > 0) {
const segments: PlaybackSegment[] = []
for (const clip of serverClips) {
const asset = assetMap.get(clip.asset_id)
if (!asset) continue
const assetDuration = asset.duration || asset.metadata?.duration || 30
const startTime = clip.start_time || 0
const endTime = Math.min(startTime + (clip.duration || assetDuration), assetDuration)
const videoUrl = asset.file_url || asset.storage_key
segments.push({ assetId: asset.id, videoUrl, startTime, endTime, order: clip.order })
}
if (segments.length > 0) {
return segments.sort((a, b) => a.order - b.order)
}
}
// Fallback: 本地构建片段
// variantSeed=0(单视频):与旧行为完全一致(素材原序、起始点 0),零回归
// variantSeed>0(批量变体):素材顺序按种子轮换 + 片段起始点在素材内偏移,
// 模拟后端"AI 随机剪辑出不同版本",让 N 个预览画面有可见差异
const templateSegments = template?.segments || []
const segments: PlaybackSegment[] = []
const orderedAssets = variantSeed > 0 ? [...assets] : assets
if (variantSeed > 0 && orderedAssets.length > 1) {
const rand = seededRandom(variantSeed * 7919 + 13)
// 素材轮换:把数组旋转 (seed % n) 位,再对后半段做一次稳定交换
const n = orderedAssets.length
const rotate = variantSeed % n
orderedAssets.push(...orderedAssets.splice(0, rotate))
const swapA = Math.floor(rand() * n)
const swapB = Math.floor(rand() * n)
if (swapA !== swapB) {
;[orderedAssets[swapA], orderedAssets[swapB]] = [orderedAssets[swapB], orderedAssets[swapA]]
}
}
orderedAssets.forEach((asset, i) => {
const assetDuration = asset.duration || asset.metadata?.duration || 30
const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1]
const segDuration = tplSeg
? Math.min(tplSeg.duration_max, Math.max(tplSeg.duration_min, assetDuration))
: Math.min(assetDuration, 10)
let startTime = 0
if (variantSeed > 0 && assetDuration - segDuration > 1) {
const rand = seededRandom(variantSeed * 104729 + i * 31 + 7)
// 起始点在素材可用区间内随机偏移(至少留 0.5s 余量)
const maxStart = Math.max(0, assetDuration - segDuration - 0.5)
startTime = Math.round(rand() * maxStart * 10) / 10
}
const endTime = Math.min(startTime + segDuration, assetDuration)
// speedFactor > 1 表示 clips 偏短需加速;< 1 表示 clips 偏长需减速
const invSpeed = speedFactor > 0 && Math.abs(speedFactor - 1) > 0.01 ? 1 / speedFactor : 1
for (const clip of serverClips) {
const asset = assetMap.get(clip.asset_id)
if (!asset) continue
const assetDuration = asset.duration || asset.metadata?.duration || 0
const startTime = clip.start_time || 0
// 片段时长以后端计划为准(配音时长等比分配);素材时长仅用于兜底钳制边界
const rawClipDuration = clip.duration || 0
// #1754:按 speedFactor 缩放片段时长,使总时长匹配配音
const clipDuration = invSpeed !== 1 ? rawClipDuration * invSpeed : rawClipDuration
const endTime =
assetDuration > 0
? Math.min(startTime + clipDuration, assetDuration)
: startTime + clipDuration
const videoUrl = asset.file_url || asset.storage_key
segments.push({ assetId: asset.id, videoUrl, startTime, endTime, order: i })
})
return segments
segments.push({
assetId: asset.id,
videoUrl,
startTime,
endTime,
order: clip.order,
playbackRate: invSpeed !== 1 ? speedFactor : undefined,
})
}
return segments.sort((a, b) => a.order - b.order)
}
const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
assets,
template,
videoRatio,
ready,
serverClips,
voiceAudioUrl,
titleSettings,
onTitlePositionChange,
variantSeed = 0,
playToken,
variantTitle,
compact = false,
activePlayToken = null,
onPlayTokenChange,
}) => {
// #1754:测量配音时长,计算缩放因子
const [voiceDuration, setVoiceDuration] = useState(0)
useEffect(() => {
if (!voiceAudioUrl) {
setVoiceDuration(0)
return
}
const audio = new Audio()
audio.preload = "metadata"
const onLoaded = () => {
if (audio.duration && isFinite(audio.duration)) {
setVoiceDuration(audio.duration)
}
}
audio.addEventListener("loadedmetadata", onLoaded)
audio.src = voiceAudioUrl
return () => {
audio.removeEventListener("loadedmetadata", onLoaded)
}
}, [voiceAudioUrl])
// #1756:clips 原始总时长 + 转场时长(后端等比分配配音时包含转场占位)
const rawClipsDuration = useMemo(() => {
if (!serverClips?.length) return 0
return serverClips.reduce((sum, c) => sum + (c.duration || 0) + (c.transition_duration || 0), 0)
}, [serverClips])
// #1754→#1756:配音时长可用且与 clips+转场 总时长偏差 > 5% 时,按比例调速
const speedFactor = useMemo(() => {
if (!voiceDuration || voiceDuration <= 0 || rawClipsDuration <= 0) return 1
const ratio = rawClipsDuration / voiceDuration
return Math.abs(ratio - 1) > 0.05 ? ratio : 1
}, [voiceDuration, rawClipsDuration])
const segments = useMemo(
() => buildPlaybackSegments(assets, template, serverClips, variantSeed),
[assets, template, serverClips, variantSeed],
() => buildPlaybackSegments(assets, serverClips, speedFactor),
[assets, serverClips, speedFactor],
)
// 批量变体:标题文字取 variantTitle,样式仍由全局 titleSettings 控制
const effectiveTitle = variantTitle ?? titleSettings?.title
// ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ──
const TITLE_MARGIN_TOP = 120
const TITLE_MARGIN_BOTTOM = 60
const TITLE_MARGIN_TOP = 180
const TITLE_MARGIN_BOTTOM = 100
const TITLE_MARGIN_SIDE = 40
const playRes = (() => {
switch (videoRatio) {
@@ -339,6 +339,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
canPlay: videoCanPlay,
togglePlayPause: videoTogglePlayPause,
seekTo: videoSeekTo,
pause: videoPause,
videoRefs,
} = useSegmentScheduler(segments)
@@ -353,6 +354,10 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
// ── 配音音频同步 ──
const audioRef = useRef<HTMLAudioElement | null>(null)
const prevIsPlayingRef = useRef(false)
// 本卡片静音开关(#1741):默认有声,用户可点喇叭单独静音某张卡片
const [muted, setMuted] = useState(false)
// 有配音时 video 素材保持静音(避免原声与配音混音);无配音时取消静音,素材原声兜底
const hasVoice = !!voiceAudioUrl
useEffect(() => {
if (!voiceAudioUrl) {
@@ -370,7 +375,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
if (audioRef.current.src !== voiceAudioUrl) {
audioRef.current.src = voiceAudioUrl
}
}, [voiceAudioUrl])
audioRef.current.muted = muted
}, [voiceAudioUrl, muted])
useEffect(() => {
const audio = audioRef.current
@@ -409,17 +415,42 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
[effectiveUseWebCodecs, canvasControls, videoSeekTo],
)
// ── 批量网格播放互斥(#1741):播放权属于其他实例时,本实例自动暂停(视频+配音) ──
useEffect(() => {
if (activePlayToken == null || playToken == null || activePlayToken === playToken) return
if (effectiveUseWebCodecs) {
if (canvasState.isPlaying) canvasControls.pause()
} else if (isPlaying) {
videoPause()
}
// isPlaying/canvasState.isPlaying 不放依赖:只在 token 变化时执行一次暂停,
// token 等于自身时本实例的播放在 handleTogglePlay 里处理
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activePlayToken, playToken, effectiveUseWebCodecs])
const handleTogglePlay = useCallback(() => {
if (effectiveUseWebCodecs) {
if (canvasState.isPlaying) {
canvasControls.pause()
onPlayTokenChange?.(null)
} else {
if (playToken != null) onPlayTokenChange?.(playToken)
canvasControls.play()
}
} else {
// video fallback:先上报播放权(暂停其他卡片),再切换本卡片播放/暂停
if (playToken != null) onPlayTokenChange?.(isPlaying ? null : playToken)
videoTogglePlayPause()
}
}, [effectiveUseWebCodecs, canvasState.isPlaying, canvasControls, videoTogglePlayPause])
}, [
effectiveUseWebCodecs,
canvasState.isPlaying,
canvasControls,
videoTogglePlayPause,
isPlaying,
playToken,
onPlayTokenChange,
])
// ── 进度条拖拽 ──
const [isDragging, setIsDragging] = useState(false)
@@ -573,7 +604,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
maxWidth: compact ? "100%" : 280,
margin: compact ? 0 : "0 auto",
aspectRatio: "9 / 16",
background: "#0a0a0a",
background: compact ? "transparent" : "#0a0a0a",
borderRadius: compact ? 10 : 24,
overflow: "hidden",
boxShadow: compact
@@ -597,7 +628,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
style={{
width: "100%",
height: "100%",
objectFit: "contain",
objectFit: "cover",
}}
/>
</div>
@@ -608,7 +639,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
segments.map((seg, i) => (
<video
key={seg.assetId}
muted
muted={hasVoice || muted}
ref={(el) => {
videoRefs.current[i] = el
}}
@@ -619,7 +650,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
inset: 0,
width: "100%",
height: "100%",
objectFit: "contain",
objectFit: "cover",
background: "#000",
zIndex: 1,
opacity: i === videoCurrentSegIdx ? 1 : 0,
@@ -662,8 +693,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
? { top: "50%", transform: "translate(-50%, -50%)" }
: { bottom: `${titleBottomPct}%` }),
}),
pointerEvents: onTitlePositionChange && variantSeed === 0 ? "auto" : "none",
cursor: onTitlePositionChange && variantSeed === 0 ? "grab" : "default",
pointerEvents: onTitlePositionChange && playToken == null ? "auto" : "none",
cursor: onTitlePositionChange && playToken == null ? "grab" : "default",
touchAction: "none",
userSelect: "none",
WebkitUserSelect: "none",
@@ -743,6 +774,45 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
</button>
)}
{/* 静音/有声切换(#1741):左上角,默认有声;批量与单视频均可单独静音 */}
{segments.length > 0 && (
<button
type="button"
aria-label={muted ? "取消静音" : "静音"}
title={muted ? "取消静音" : "静音"}
onClick={() => setMuted((m) => !m)}
style={{
position: "absolute",
top: 8,
left: 8,
width: compact ? 26 : 30,
height: compact ? 26 : 30,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,0.45)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: "50%",
color: muted ? "rgba(255,255,255,0.45)" : "rgba(255,255,255,0.92)",
fontSize: compact ? 13 : 15,
cursor: "pointer",
zIndex: 10,
padding: 0,
transition: "background 0.15s, color 0.15s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(0,0,0,0.65)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(0,0,0,0.45)"
}}
>
{muted ? <AudioMutedOutlined /> : <AudioOutlined />}
</button>
)}
{/* 片段指示器 — 右上角胶囊 */}
<div
style={{
@@ -753,9 +823,9 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
color: "rgba(255,255,255,0.9)",
fontSize: 10,
fontSize: compact ? 9 : 10,
fontWeight: 500,
padding: "2px 8px",
padding: compact ? "1px 6px" : "2px 8px",
borderRadius: 999,
zIndex: 10,
border: "1px solid rgba(255,255,255,0.1)",
@@ -774,8 +844,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
right: 0,
display: "flex",
alignItems: "center",
gap: 10,
padding: "12px 16px 16px",
gap: compact ? 6 : 10,
padding: compact ? "8px 10px 10px" : "12px 16px 16px",
background: "linear-gradient(transparent, rgba(0,0,0,0.7))",
backdropFilter: "blur(4px)",
WebkitBackdropFilter: "blur(4px)",
@@ -788,10 +858,10 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
background: "rgba(255,255,255,0.15)",
border: "none",
color: "#fff",
fontSize: 16,
fontSize: compact ? 14 : 16,
cursor: "pointer",
width: 32,
height: 32,
width: compact ? 26 : 32,
height: compact ? 26 : 32,
borderRadius: "50%",
display: "flex",
alignItems: "center",
@@ -811,9 +881,9 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
<span
style={{
fontSize: 11,
fontSize: compact ? 10 : 11,
color: "rgba(255,255,255,0.85)",
minWidth: 72,
minWidth: compact ? 58 : 72,
fontVariantNumeric: "tabular-nums",
letterSpacing: 0.2,
}}
@@ -1,96 +0,0 @@
import React from "react"
import { Typography } from "antd"
import { PlayCircleOutlined, CloseCircleOutlined } from "@ant-design/icons"
import type { GeneratedVideo } from "@/api/template-editor"
import { ProgressIndicator } from "./result-panel/ProgressIndicator"
import { ResultVideoCard } from "./result-panel/ResultVideoCard"
const { Text } = Typography
interface GenerateResultPanelProps {
generated: boolean
generating: boolean
progress: number
generateError: string | null
generatedVideos: GeneratedVideo[]
onVideoPreview: (video: GeneratedVideo) => void
onDownload: () => void
onShare: () => void
onGoToLibrary: () => void
}
const GenerateResultPanel: React.FC<GenerateResultPanelProps> = ({
generated,
generating,
progress,
generateError,
generatedVideos,
onVideoPreview,
onDownload,
onShare,
onGoToLibrary,
}) => {
return (
<div className="xx-generate-result">
<div className="xx-result-header">
<h3>生成结果</h3>
{generated && generatedVideos.length > 0 && (
<span className="xx-result-count">{generatedVideos.length} 个视频</span>
)}
</div>
{generating && <ProgressIndicator progress={progress} />}
{generateError && !generating && (
<div className="xx-result-empty">
<CloseCircleOutlined style={{ fontSize: 40, color: "#ff4d4f", marginBottom: 12 }} />
<Text strong style={{ display: "block", marginBottom: 4 }}>
生成失败
</Text>
<Text style={{ fontSize: 12, color: "var(--text-secondary)" }}>
{typeof generateError === "string" ? generateError : "请重试"}
</Text>
</div>
)}
{!generated && !generating && !generateError && (
<div className="xx-result-empty">
<PlayCircleOutlined
style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }}
/>
<Text style={{ color: "var(--text-secondary)", fontSize: 13 }}>
完成配置后点击「确认生成」
</Text>
<Text style={{ color: "var(--text-tertiary)", fontSize: 12, marginTop: 4 }}>
生成的视频将在这里展示
</Text>
</div>
)}
{generated && generatedVideos.length > 0 && (
<div className="xx-video-grid">
{generatedVideos.map((video, idx) => (
<ResultVideoCard
key={video.id || idx}
video={video}
index={idx}
onPreview={onVideoPreview}
onDownload={onDownload}
onShare={onShare}
/>
))}
</div>
)}
{generated && (
<div className="xx-result-footer">
<button className="xx-btn xx-btn-ghost xx-btn-block" onClick={onGoToLibrary}>
前往成片库 →
</button>
</div>
)}
</div>
)
}
export default GenerateResultPanel
@@ -1,380 +0,0 @@
/**
* 右侧预览视频面板 — 服务器渲染预览架构
*
* Step4+: 显示预览面板
* Step5: 播放服务器渲染的真实视频(POST /generation/preview)
*
* 架构:
* - 进入 Step4/5 时自动创建服务器预览渲染任务
* - 轮询完成后用 <video> 标签播放返回的 video_url
* - 标题样式编辑时 CSS TitleOverlay 实时叠加预览
* - 素材/配音/BGM 变更自动重新渲染
* - 标题文字/样式变更标记 stale,保留旧视频 + 显示"重新预览"按钮
*
* 点"确认生成"时走 confirm 路径,成品就是预览视频本身,100% 一致。
*/
import React, { useMemo, useRef, useState, useEffect } from "react"
import { LoadingOutlined, ReloadOutlined, ExclamationCircleOutlined } from "@ant-design/icons"
import { Button } from "antd"
import type { TitleSettings } from "../types"
import { getFontFamily } from "../constants"
import type { ServerPreviewStatus } from "../hooks/useServerPreview"
interface PreviewVideoPanelProps {
/** 服务器预览状态 */
previewStatus: ServerPreviewStatus
/** 服务器渲染视频 URL */
videoUrl: string | null
/** 渲染进度 0-100 */
progress: number
/** 错误信息 */
error: string | null
/** 重新预览回调 */
onRetry: () => void
/** 视频比例 */
videoRatio: string
/** 标题设置 — CSS 实时预览层 */
titleSettings?: TitleSettings
/** 素材数量 */
assetCount?: number
}
/* ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ── */
const TITLE_MARGIN_TOP = 120
const TITLE_MARGIN_BOTTOM = 60
const TITLE_MARGIN_SIDE = 40
/** 根据视频比例返回后端实际渲染分辨率(PlayResX × PlayResY) */
function getResolution(ratio: string): { width: number; height: number } {
switch (ratio) {
case "16:9":
return { width: 1920, height: 1080 }
case "1:1":
return { width: 1080, height: 1080 }
case "9:16":
default:
return { width: 1080, height: 1920 }
}
}
function getPositionStyle(
position: string,
playResX: number,
playResY: number,
): React.CSSProperties {
const sidePercent = (TITLE_MARGIN_SIDE / playResX) * 100
switch (position) {
case "bottom":
return {
bottom: `${(TITLE_MARGIN_BOTTOM / playResY) * 100}%`,
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
case "center":
return {
top: "50%",
transform: "translateY(-50%)",
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
case "top":
default:
return {
top: `${(TITLE_MARGIN_TOP / playResY) * 100}%`,
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
}
}
function buildTitleStyle(
settings: TitleSettings,
containerHeight: number,
playResY: number,
): React.CSSProperties {
// 字号按容器高度与 PlayResY 的比例缩放,不设上限(与后端一致)
const fontSizePx =
containerHeight > 0
? (settings.size / playResY) * containerHeight
: (settings.size / playResY) * 400
const base: React.CSSProperties = {
fontFamily: getFontFamily(settings.font),
fontSize: `${fontSizePx}px`,
color: settings.color || "#ffffff",
fontWeight: settings.bold ? 700 : 400,
fontStyle: settings.italic ? "italic" : "normal",
lineHeight: 1.05,
wordBreak: "break-word",
pointerEvents: "none",
userSelect: "none",
}
if (settings.stroke) base.WebkitTextStroke = "1px #000000"
if (settings.shadow) base.textShadow = "2px 2px 4px rgba(0,0,0,0.8)"
return base
}
/** CSS 标题实时预览覆盖层 */
const TitleOverlay: React.FC<{ titleSettings: TitleSettings; videoRatio: string }> = ({
titleSettings,
videoRatio,
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const [containerHeight, setContainerHeight] = useState(400)
useEffect(() => {
const el = containerRef.current
if (!el) return
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const h = entry.contentRect.height
if (h > 0) setContainerHeight(h)
}
})
ro.observe(el)
const rect = el.getBoundingClientRect()
if (rect.height > 0) setContainerHeight(rect.height)
return () => ro.disconnect()
}, [])
const { width: playResX, height: playResY } = getResolution(videoRatio)
const positionStyle = useMemo(
() => getPositionStyle(titleSettings.position, playResX, playResY),
// eslint-disable-next-line react-hooks/exhaustive-deps
[titleSettings.position, playResX, playResY],
)
const titleStyle = useMemo(
() => buildTitleStyle(titleSettings, containerHeight, playResY),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
containerHeight,
titleSettings.font,
titleSettings.size,
titleSettings.color,
titleSettings.bold,
titleSettings.italic,
titleSettings.stroke,
titleSettings.shadow,
playResY,
],
)
const displayTitle = titleSettings.title?.trim() || "标题预览"
return (
<div
ref={containerRef}
style={{
position: "absolute",
inset: 0,
zIndex: 20,
pointerEvents: "none",
overflow: "hidden",
}}
>
<div style={{ ...positionStyle, ...titleStyle, position: "absolute" }}>
{displayTitle.split(/[//]/).map((part, i) => (
<span key={i}>
{i > 0 && <br />}
{part}
</span>
))}
</div>
</div>
)
}
/* ── 主组件 ── */
export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
previewStatus,
videoUrl,
progress,
error,
onRetry,
videoRatio,
titleSettings,
assetCount,
}) => {
const videoAspectStyle = { aspectRatio: (videoRatio || "9:16").replace(":", "/") }
const isLoading = previewStatus === "loading"
const isReady = previewStatus === "ready" || previewStatus === "stale"
const isFailed = previewStatus === "failed"
const isIdle = previewStatus === "idle"
const isStale = previewStatus === "stale"
return (
<div className="xx-generate-preview">
<div className="xx-preview-header">
<h3>预览视频</h3>
{isReady && !isStale && <span className="xx-preview-badge">服务器渲染</span>}
{isStale && (
<span
style={{
fontSize: 12,
color: "#faad14",
display: "flex",
alignItems: "center",
gap: 4,
}}
>
<ExclamationCircleOutlined /> 配置已变更
</span>
)}
{isLoading && <span className="xx-preview-badge">渲染中</span>}
</div>
<div className="xx-preview-video" style={{ ...videoAspectStyle, position: "relative" }}>
{/* 加载中 */}
{isLoading && (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
zIndex: 5,
background: "rgba(0,0,0,0.6)",
}}
>
<LoadingOutlined style={{ fontSize: 36, color: "#fff" }} spin />
<p style={{ marginTop: 12, color: "rgba(255,255,255,0.9)", fontSize: 14 }}>
正在渲染预览视频{progress > 0 ? `...${progress}%` : "..."}
</p>
<p style={{ marginTop: 4, color: "rgba(255,255,255,0.5)", fontSize: 12 }}>
首次渲染约需 30-60 秒
</p>
</div>
)}
{/* 空闲状态(尚未触发预览) */}
{isIdle && (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,0.3)",
zIndex: 5,
}}
>
<p style={{ color: "rgba(255,255,255,0.7)", fontSize: 14 }}>等待素材选择...</p>
</div>
)}
{/* 服务器渲染的真实视频 */}
{isReady && videoUrl && (
<video
key={videoUrl}
src={videoUrl}
controls
autoPlay
loop
playsInline
style={{
width: "100%",
height: "100%",
objectFit: "contain",
position: "absolute",
inset: 0,
zIndex: 1,
}}
/>
)}
{/* 标题样式实时预览层(仅在有视频时叠加) */}
{isReady && titleSettings && (
<TitleOverlay titleSettings={titleSettings} videoRatio={videoRatio} />
)}
{/* stale 遮罩:配置变更提示 */}
{isStale && (
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
padding: "10px 16px",
background: "linear-gradient(transparent, rgba(0,0,0,0.85))",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
zIndex: 30,
}}
>
<span style={{ color: "rgba(255,255,255,0.9)", fontSize: 12 }}>
配置已变更,预览内容可能不是最新
</span>
<Button size="small" type="primary" icon={<ReloadOutlined />} onClick={onRetry}>
重新预览
</Button>
</div>
)}
{/* 错误状态 */}
{isFailed && (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,0.7)",
zIndex: 10,
padding: 24,
}}
>
<ExclamationCircleOutlined style={{ fontSize: 40, color: "#ff4d4f" }} />
<p
style={{
marginTop: 12,
color: "rgba(255,255,255,0.9)",
fontSize: 14,
textAlign: "center",
}}
>
{error || "预览渲染失败"}
</p>
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={onRetry}
style={{ marginTop: 12 }}
>
重新预览
</Button>
</div>
)}
</div>
{/* 素材信息 */}
{assetCount !== undefined && assetCount > 0 && (
<div className="xx-preview-info">
<div className="xx-preview-info-row">
<span>素材数</span>
<span>{assetCount} 个</span>
</div>
<div className="xx-preview-info-row">
<span>比例</span>
<span>{videoRatio}</span>
</div>
</div>
)}
</div>
)
}
export default PreviewVideoPanel
@@ -9,12 +9,13 @@
* - 标题样式(字体/颜色/位置/大小/粗斜描边/预设):全局统一
*/
import React, { useMemo, useState } from "react"
import { AutoComplete, Input, message } from "antd"
import { Input, message } from "antd"
import { LoadingOutlined } from "@ant-design/icons"
import type { TitleSettings } from "../types"
import { POSITION_OPTIONS, FONT_OPTIONS } from "../constants"
import { useStep4Title } from "../hooks/useStep4Title"
import AiTitleGenerator from "./title/AiTitleGenerator"
import TitleLibraryAutoComplete from "./title/TitleLibraryAutoComplete"
import TitleStylePanel from "./title/TitleStylePanel"
import { AI_TITLE_TEMPLATES } from "../constants"
@@ -201,21 +202,14 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
</div>
<div className="xx-form-field">
<label>标题</label>
<AutoComplete
placeholder="输入标题文字…"
allowClear
maxLength={50}
style={{ width: "100%" }}
value={(previewTitles?.[0] ?? t.titleSettings.title) || undefined}
<TitleLibraryAutoComplete
placeholder="输入或从标题库选择"
value={previewTitles?.[0] ?? t.titleSettings.title}
onChange={(val) => {
t.updateTitle(val || "")
onPreviewTitlesChange?.([val || ""])
}}
options={titleOptions}
filterOption={(inputValue, option) => {
const title = (option?.label || option?.value || "") as string
return title.toLowerCase().includes((inputValue || "").toLowerCase())
}}
/>
</div>
</>
@@ -269,17 +263,11 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
{Array.from({ length: previewCount }, (_, i) => (
<div className="xx-form-field" key={i}>
<label>视频 {i + 1} 标题</label>
<AutoComplete
placeholder={`视频 ${i + 1} 的标题…`}
maxLength={50}
style={{ width: "100%" }}
value={previewTitles?.[i] || undefined}
onChange={(val) => updateVariantTitle(i, val || "")}
<TitleLibraryAutoComplete
placeholder={`输入或选择视频 ${i + 1} 的标题`}
value={previewTitles?.[i] || ""}
onChange={(val) => updateVariantTitle(i, val)}
options={titleOptions}
filterOption={(inputValue, option) => {
const title = (option?.label || option?.value || "") as string
return title.toLowerCase().includes((inputValue || "").toLowerCase())
}}
/>
</div>
))}
@@ -37,6 +37,12 @@ const isAiVoice = (item: AssetItem): boolean => {
return (!duration || duration <= 0) && (!size || size <= 0)
}
/** 格式化时长(秒 → "14.7秒";0/无效 → "时长未知",不做任何时长过滤) */
const formatDurationSeconds = (seconds?: number): string => {
if (!seconds || seconds <= 0) return "时长未知"
return `${seconds.toFixed(1)}秒`
}
/** 格式化文件大小 */
const formatFileSize = (bytes?: number): string => {
if (!bytes || bytes <= 0) return "未知"
@@ -261,7 +267,9 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
>
{isAiVoice(item) ? (
<span style={{ color: "#1677ff", fontWeight: 500 }}>AI 音色</span>
) : null}
) : (
<span>{formatDurationSeconds(getDuration(item))}</span>
)}
<span>{isAiVoice(item) ? "按文本合成" : formatFileSize(getFileSize(item))}</span>
</div>
</div>
@@ -91,11 +91,13 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
shadow: props.titleSettings?.shadow ?? false,
},
covers: batchCoversList,
onCoversChange: (urls) => {
// 按卡片顺序写回对应变体索引
onCoversChange: (updater) => {
// 按卡片顺序写回对应变体索引;支持函数式 updater(#1750:串行回写避免闭包覆盖)
const prevCardView = cardIndexes.map((vi) => (props.previewCovers || [])[vi] || "")
const nextCardView = typeof updater === "function" ? updater(prevCardView) : updater
const next = [...(props.previewCovers || [])]
cardIndexes.forEach((vi, cardPos) => {
next[vi] = urls[cardPos] || ""
next[vi] = nextCardView[cardPos] || ""
})
props.onPreviewCoversChange?.(next)
},
@@ -142,7 +144,8 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
<Button
buttonType="primary"
onClick={() => void batchCovers.generateAll()}
disabled={completedVideos.length === 0 || batchCovers.loadingIndex !== null}
disabled={completedVideos.length === 0 || batchCovers.busyIndexes.length > 0}
style={{ whiteSpace: "nowrap", flexShrink: 0 }}
>
✨ 一键全部自动生成
</Button>
@@ -151,8 +154,8 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
<div className="xx-cover-grid">
{cardIndexes.map((variantIndex, cardPos) => {
const url = batchCoversList[cardPos]
const isLoading = batchCovers.loadingIndex === cardPos
const isUploading = batchCovers.uploadingIndex === cardPos
const isLoading = batchCovers.busyIndexes.includes(cardPos)
const isUploading = batchCovers.uploadingIndexes.includes(cardPos)
const title = batchTitles[cardPos]
return (
<div className="xx-cover-card" key={variantIndex}>
@@ -1,40 +0,0 @@
import React from "react"
import { Typography } from "antd"
const { Text } = Typography
interface ProgressIndicatorProps {
progress: number
}
export const ProgressIndicator: React.FC<ProgressIndicatorProps> = ({ progress }) => {
return (
<div className="xx-result-progress">
<div className="xx-progress-circle">
<svg viewBox="0 0 80 80">
<circle cx="40" cy="40" r="36" fill="none" stroke="var(--border-color)" strokeWidth="6" />
<circle
cx="40"
cy="40"
r="36"
fill="none"
stroke="var(--primary-color)"
strokeWidth="6"
strokeDasharray={`${Math.round(progress) * 2.26} 226`}
strokeLinecap="round"
transform="rotate(-90 40 40)"
/>
</svg>
<span className="xx-progress-percent">{Math.round(progress)}%</span>
</div>
<div className="xx-progress-text">
<Text strong style={{ fontSize: 14, display: "block", marginBottom: 4 }}>
正在生成视频
</Text>
<Text style={{ fontSize: 12, color: "var(--text-secondary)" }}>
AI 正在处理素材,请稍候…
</Text>
</div>
</div>
)
}
@@ -1,63 +0,0 @@
import React from "react"
import { PlayCircleOutlined, DownloadOutlined, ShareAltOutlined } from "@ant-design/icons"
import type { GeneratedVideo } from "@/api/template-editor"
import { formatDuration } from "@/api/voice-clone"
interface ResultVideoCardProps {
video: GeneratedVideo
index: number
onPreview: (video: GeneratedVideo) => void
onDownload: () => void
onShare: () => void
}
export const ResultVideoCard: React.FC<ResultVideoCardProps> = ({
video,
index,
onPreview,
onDownload,
onShare,
}) => {
return (
<div className="xx-video-card" onClick={() => onPreview(video)}>
<div className="xx-video-thumb">
{video.thumbnail_url ? (
<img src={video.thumbnail_url} alt="" />
) : (
<div className="xx-video-thumb-placeholder">
<PlayCircleOutlined style={{ fontSize: 32, opacity: 0.5 }} />
</div>
)}
<div className="xx-video-play-overlay">
<PlayCircleOutlined style={{ fontSize: 36, color: "#fff" }} />
</div>
{video.duration && (
<span className="xx-video-duration">{formatDuration(video.duration)}</span>
)}
</div>
<div className="xx-video-info">
<div className="xx-video-title">视频 {index + 1}</div>
<div className="xx-video-actions">
<button
className="xx-video-action-btn"
onClick={(e) => {
e.stopPropagation()
onDownload()
}}
>
<DownloadOutlined />
</button>
<button
className="xx-video-action-btn"
onClick={(e) => {
e.stopPropagation()
onShare()
}}
>
<ShareAltOutlined />
</button>
</div>
</div>
</div>
)
}
@@ -0,0 +1,75 @@
/**
* 标题库 AutoComplete(Issue #1737)
*
* 原生 antd AutoComplete(combobox 模式)的两个行为不符合产品预期:
* 1. combobox 默认 showAction=[],输入框聚焦时下拉不展开——用户必须先打字才能看到标题库,
* 且组件无下拉箭头,视觉上是"纯输入框",不知道标题库里已有标题可选。
* 2. 空态聚焦不展示任何标题库内容。
*
* 本组件封装修复:
* - 受控 open:聚焦(且标题库非空)即展开,展示全部标题;失焦/选中/Esc 关闭
* (rc-select 失焦会主动 onToggleOpen(false),onOpenChange 同步状态即可,不会死循环)
* - suffixIcon 加下拉三角,视觉提示"可选择";有值时 allowClear 的清除按钮照常出现
* - 输入文字时由 filterOption 过滤(空串展示全部)
* - 保留 combobox 自由输入能力:用户可输入标题库之外的自定义标题
*/
import React, { useState } from "react"
import { AutoComplete } from "antd"
import { DownOutlined } from "@ant-design/icons"
import type { AutoCompleteProps } from "antd"
export interface TitleOption {
label: string
value: string
}
interface TitleLibraryAutoCompleteProps {
value: string
onChange: (val: string) => void
options: TitleOption[]
placeholder?: string
allowClear?: boolean
maxLength?: number
style?: React.CSSProperties
}
const TitleLibraryAutoComplete: React.FC<TitleLibraryAutoCompleteProps> = ({
value,
onChange,
options,
placeholder = "输入或从标题库选择",
allowClear = true,
maxLength = 50,
style,
}) => {
const [open, setOpen] = useState(false)
const hasTitles = options.length > 0
const filterOption: AutoCompleteProps["filterOption"] = (inputValue, option) => {
const title = (option?.label || option?.value || "") as string
return title.toLowerCase().includes((inputValue || "").toLowerCase())
}
return (
<AutoComplete
value={value || undefined}
onChange={(val) => onChange(val || "")}
options={options}
filterOption={filterOption}
open={open}
onOpenChange={setOpen}
onFocus={() => {
// 标题库为空时不展开(避免弹出"暂无数据"空壳)
if (hasTitles) setOpen(true)
}}
onSelect={() => setOpen(false)}
suffixIcon={<DownOutlined style={{ color: "var(--text-secondary, #bbb)", fontSize: 12 }} />}
placeholder={placeholder}
allowClear={allowClear}
maxLength={maxLength}
style={{ width: "100%", ...style }}
/>
)
}
export default TitleLibraryAutoComplete
+53 -9
View File
@@ -3282,17 +3282,28 @@
/* ============================================================
批量前端 Canvas 预览网格(Issue #1677 修正:纯前端实时预览)
============================================================ */
/* #1741:卡片整体缩小至约 3/5——宽屏排 3 列(卡片限宽 220px 居中),
中屏自动回退 2 列,窄屏 1 列(见下方媒体查询);卡片保持 9:16 比例不变形 */
.xx-canvas-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(160px, 180px));
justify-content: center;
gap: 14px;
}
/* 窄屏单列时卡片限宽居中,避免 1fr 拉伸导致卡片过高 */
@media (max-width: 960px) {
.xx-canvas-grid {
grid-template-columns: minmax(0, 320px);
}
}
.xx-canvas-grid-card {
position: relative;
border: 2px solid var(--border-primary, #e2e8f0);
border-radius: 12px;
overflow: hidden;
background: #000;
background: transparent;
transition: border-color 0.2s ease;
min-width: 0;
}
@@ -3330,6 +3341,37 @@
accent-color: var(--primary-color, #1677ff);
}
/* #1750:批量变体片段加载/错误占位(9:16 竖屏比例,与播放器卡片同尺寸防塌陷) */
.xx-variant-clips-status {
position: relative;
width: 100%;
aspect-ratio: 9 / 16;
max-height: 70vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
font-size: 13px;
color: rgba(255, 255, 255, 0.75);
background: #0a0a0a;
border-radius: 16px;
overflow: hidden;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.3),
0 20px 50px -12px rgba(0, 0, 0, 0.5),
inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.xx-variant-clips-status .anticon {
font-size: 22px;
}
.xx-variant-clips-error-text {
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
}
/* ============================================================
批量标题:AI 一键生成行(Issue #1677)
============================================================ */
@@ -3361,16 +3403,18 @@
第5步确认生成:批量渲染进度网格(Issue #1677)
============================================================ */
.xx-batch-gen-grid {
justify-items: center;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(160px, 180px));
justify-content: center;
gap: 14px;
}
.xx-batch-gen-card {
border: 1px solid var(--border-primary, #e2e8f0);
border-radius: 12px;
padding: 14px;
background: var(--bg-surface, #fff);
background: transparent;
display: flex;
flex-direction: column;
gap: 10px;
@@ -3435,10 +3479,10 @@
word-break: break-word;
}
/* ── 响应式:窄屏批量网格回退单列 ── */
/* ── 响应式:窄屏批量网格回退单列(.xx-canvas-grid 的窄屏限宽见网格定义处 #1741) ── */
@media (max-width: 960px) {
.xx-canvas-grid,
.xx-batch-gen-grid {
grid-template-columns: 1fr;
justify-items: center;
grid-template-columns: minmax(0, 320px);
}
}
@@ -42,6 +42,12 @@ export interface UseGenerateVideoProps {
variantCoverUrls?: string[]
/** 勾选要生成的变体索引(批量模式) */
selectedVariantIndexes?: number[]
/**
* 批量变体剪辑计划 ID(#1744,按变体全量索引长度=previewCount):
* 预览阶段后端独立选片产出,正式生成按勾选顺序回传,实现预览即成片;
* 为空(降级本地模拟/后端未上线)时不传,后端自行独立选片。
*/
variantPlanIds?: string[]
}
/** 生成阶段 */
@@ -13,21 +13,22 @@ interface UseSmartMatchOptions {
}
/** 默认 limit(拿不到目标时长时的兜底上限) */
const DEFAULT_LIMIT = 10
/** 每个素材切片按 15 秒估算所需素材数 */
const SECONDS_PER_ASSET = 15
const DEFAULT_LIMIT = 30
/**
* 候选池放大倍数(#1744):批量 N 个变体独立选片、跨变体 20% 区间避让需要
* 足够大的素材池才能保证 N 条成片素材排布互不相同(历史已用区间写回素材 metadata)。
* 旧规则「总时长/15 秒」只够 1 条成片的选片量,池太小会导致变体间大量复用同区间。
*/
const ASSET_POOL_MULTIPLIER = 3
/**
* 根据模板 segments 计算所需素材数量上限。
* 取每个 segment 的 duration_min 之和作为目标视频总时长,
* 再按 15 秒/素材估算需要多少个素材,且保证不少于片段数(每个片段至少 1 个素材);
* 结果钳制到 [1, 200] 区间(后端 limit 上限 200)。
* 根据模板 segments 计算智能匹配候选素材数量上限(#1750:模板无时长概念)。
* 候选池 = 片段数 × 3:每个片段至少 3 个候选素材供变体间洗牌/避让
* (独立选片 + 区间不重叠);结果钳制到 [1, 200] 区间(后端 limit 上限 200)。
*/
function computeLimitFromSegments(segments?: TemplateSegment[]): number {
export function computeLimitFromSegments(segments?: TemplateSegment[]): number {
if (!segments || segments.length === 0) return DEFAULT_LIMIT
const totalSeconds = segments.reduce((sum, seg) => sum + (seg.duration_min || 0), 0)
if (totalSeconds <= 0) return DEFAULT_LIMIT
const limit = Math.max(segments.length, Math.ceil(totalSeconds / SECONDS_PER_ASSET))
const limit = segments.length * ASSET_POOL_MULTIPLIER
return Math.max(1, Math.min(limit, 200))
}
@@ -1,6 +1,13 @@
/**
* 批量封面 Hook(Issue #1677)
* 批量封面 Hook(Issue #1677 / #1750 修复)
* N 个视频时:逐个自动生成封面(从对应成片抽帧 + 叠加对应标题)或上传自定义封面
*
* #1750 修复:
* - patchCover 改为函数式 updater 回写(onCoversChange 支持 (prev)=>next),
* 修复串行 generateAll 时闭包捕获旧 covers 数组导致回写互相覆盖的 bug;
* - loading/uploading 状态改数组(busyIndexes/uploadingIndexes),支持多个卡片在途;
* - generateOne 返回 boolean,generateAll 据此统计成功/失败数量分级提示,单个失败不阻塞;
* - generateAll 一次点击即处理所有未生成封面(无需点 3 次)。
*/
import { useCallback, useState } from "react"
import { message } from "antd"
@@ -8,6 +15,9 @@ import { generateCover } from "@/api/generation"
import { uploadAssetDirect, getAssetLibraries } from "@/api/assets"
import type { GeneratedVideo } from "@/api/template-editor"
/** onCoversChange 支持直接传值或函数式 updater(函数式用于串行回写避免闭包覆盖) */
type CoversChangeFn = (updater: string[] | ((prev: string[]) => string[])) => void
interface UseBatchCoversOptions {
selectedTemplate: string
generatedVideos: GeneratedVideo[]
@@ -24,7 +34,7 @@ interface UseBatchCoversOptions {
shadow: boolean
}
covers: string[]
onCoversChange: (urls: string[]) => void
onCoversChange: CoversChangeFn
}
export function useBatchCovers({
@@ -35,28 +45,46 @@ export function useBatchCovers({
covers,
onCoversChange,
}: UseBatchCoversOptions) {
const [loadingIndex, setLoadingIndex] = useState<number | null>(null)
const [uploadingIndex, setUploadingIndex] = useState<number | null>(null)
/** 正在自动生成封面的变体序号集合(数组支持多个卡片同时在途) */
const [busyIndexes, setBusyIndexes] = useState<number[]>([])
/** 正在上传自定义封面的变体序号集合 */
const [uploadingIndexes, setUploadingIndexes] = useState<number[]>([])
const addBusy = useCallback((i: number) => {
setBusyIndexes((prev) => (prev.includes(i) ? prev : [...prev, i]))
}, [])
const removeBusy = useCallback((i: number) => {
setBusyIndexes((prev) => prev.filter((x) => x !== i))
}, [])
const addUploading = useCallback((i: number) => {
setUploadingIndexes((prev) => (prev.includes(i) ? prev : [...prev, i]))
}, [])
const removeUploading = useCallback((i: number) => {
setUploadingIndexes((prev) => prev.filter((x) => x !== i))
}, [])
/** 函数式回写单个封面位置(基于最新 prev,不受闭包旧值影响) */
const patchCover = useCallback(
(index: number, url: string) => {
const next = [...covers]
next[index] = url
onCoversChange(next)
onCoversChange((prev) => {
const next = [...prev]
next[index] = url
return next
})
},
[covers, onCoversChange],
[onCoversChange],
)
/** 为第 index 个视频自动生成封面 */
/** 为第 index 个视频自动生成封面;返回是否成功(供 generateAll 统计) */
const generateOne = useCallback(
async (index: number) => {
async (index: number): Promise<boolean> => {
const finalVideos = generatedVideos.filter((v) => v.status === "completed")
const target = finalVideos[index] || generatedVideos[index]
if (!target) {
message.warning("该视频尚未生成完成")
return
return false
}
setLoadingIndex(index)
addBusy(index)
try {
const titleText = titles[index] || ""
const response = await generateCover(selectedTemplate, {
@@ -82,23 +110,25 @@ export function useBatchCovers({
if (url) {
patchCover(index, url)
message.success(`视频 ${index + 1} 封面生成成功`)
} else {
message.warning(`视频 ${index + 1} 封面生成未返回图片,请重试`)
return true
}
message.warning(`视频 ${index + 1} 封面生成未返回图片,请重试`)
return false
} catch (err) {
console.error(`[封面] 视频 ${index + 1} 生成失败:`, err)
message.error(`视频 ${index + 1} 封面生成失败,请重试`)
return false
} finally {
setLoadingIndex(null)
removeBusy(index)
}
},
[generatedVideos, titles, titleStyle, selectedTemplate, patchCover],
[generatedVideos, titles, titleStyle, selectedTemplate, patchCover, addBusy, removeBusy],
)
/** 为第 index 个视频上传自定义封面 */
const uploadOne = useCallback(
async (index: number, file: File) => {
setUploadingIndex(index)
addUploading(index)
try {
const libs = await getAssetLibraries()
const imageLib = libs.find((l) => l.kind === "image") || libs[0]
@@ -121,26 +151,43 @@ export function useBatchCovers({
console.error(`[封面] 视频 ${index + 1} 上传失败:`, err)
message.error("封面上传失败,请重试")
} finally {
setUploadingIndex(null)
removeUploading(index)
}
},
[patchCover],
[patchCover, addUploading, removeUploading],
)
/** 一键全部自动生成(串行,避免队列限流) */
/** 一键全部自动生成(串行,避免队列限流;单个失败不阻塞,结束后分级提示) */
const generateAll = useCallback(async () => {
const finalVideos = generatedVideos.filter((v) => v.status === "completed")
for (let i = 0; i < finalVideos.length; i++) {
if (covers[i]) continue // 已有封面跳过
const total = finalVideos.length
// 待处理:基于调用时刻的 covers 快照判断(已有封面跳过);
// 回写走函数式 updater,循环内不再依赖可能过期的 covers 闭包
const pending = finalVideos.map((_, i) => i).filter((i) => !covers[i])
if (pending.length === 0) {
message.info("所有视频封面已生成")
return
}
let okCount = 0
let failCount = 0
for (const i of pending) {
// eslint-disable-next-line no-await-in-loop
await generateOne(i)
const ok = await generateOne(i)
if (ok) okCount += 1
else failCount += 1
}
if (failCount === 0) {
message.success(`全部 ${total} 个封面已生成`)
} else if (okCount === 0) {
message.error("封面生成全部失败,请重试")
} else {
message.warning(`已生成 ${okCount} 个封面,${failCount} 个失败,请对失败项重试`)
}
message.success("全部封面已生成")
}, [generatedVideos, covers, generateOne])
return {
loadingIndex,
uploadingIndex,
busyIndexes,
uploadingIndexes,
generateOne,
uploadOne,
generateAll,
@@ -0,0 +1,153 @@
/**
* 批量变体真实片段 Hook(#1744 接入 / #1750 修正)
*
* 批量预览(N>1)进入第 4 步时,向后端轻量接口 POST /generation/variant-plans
* 申请 N 个变体的独立剪辑计划片段:
* - 接口只做选片/建 plan(秒级),不渲染视频,无渲染成本;
* - 选片逻辑与正式批量生成(POST /generation/tasks?count=N)完全同源
* (reselect_plan_for_variant:素材洗牌 + main 片段洗牌 + 起点随机 +
* 跨变体 20% 区间避让 + 使用区间写回素材 metadata),
* 因此前端按这些 clips 播放的预览与最终成片一致;
* - 正式生成时把 plan_ids 回传,后端直接关联预览 plan 渲染,不再重新选片。
*
* 错误处理(#1750 基线:严禁用前端假数据冒充预览):
* - 端点 404(后端未上线)/ 400 / 网络错误 / 超时:一律置 error 状态,
* UI 显示「预览加载失败,请重试」,不再静默降级本地模拟;
* - 单个变体 clips 为空同样视为加载失败(计划数据不完整)。
*
* N=1 不调用本 hook(单视频零回归)。
*/
import { useCallback, useEffect, useRef, useState } from "react"
import type { EditPlanClip } from "@/api/template-editor"
import { createBatchVariantPlans, type VariantPlan } from "@/api/generation/variantPlans"
export interface BatchVariantClipsState {
/** 各变体的服务端真实片段(按 variant_index 排序);未就绪为空数组 */
clipsByVariant: EditPlanClip[][]
/** 各变体的 plan_id(正式生成回传,保证预览即成片);未就绪为空串 */
planIdsByVariant: string[]
/** 是否正在向后端申请变体计划 */
loading: boolean
/** 后端真实片段是否全部可用(每个变体都有 ≥1 条片段) */
ready: boolean
/** 加载失败(端点未上线/网络错误/数据不完整);true 时 UI 显示错误态+重试 */
error: boolean
/** 用户点击「重试」:强制重新申请变体计划 */
retry: () => void
}
interface UseBatchVariantPlansOptions {
/** 是否启用:仅批量(count>1)且素材已选时为 true */
enabled: boolean
count: number
templateId: string
assetIds: string[]
/** 源剪辑计划 ID(草稿/预览关联),无则空串由后端兜底最新 plan */
sourcePlanId?: string
}
export function useBatchVariantPlans({
enabled,
count,
templateId,
assetIds,
sourcePlanId = "",
}: UseBatchVariantPlansOptions): BatchVariantClipsState {
const [clipsByVariant, setClipsByVariant] = useState<EditPlanClip[][]>([])
const [planIdsByVariant, setPlanIdsByVariant] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const requestSeqRef = useRef(0)
// 记录上次成功申请的入参指纹,素材/数量未变时不重复请求
const lastKeyRef = useRef("")
// 重试计数(state):用户点重试时 +1,驱动 effect 重新发起请求(即使指纹相同);
// 用 state 而非 ref + 直接调 load,避免 retry 与 effect 双发导致 seq 竞争
const [retryNonce, setRetryNonce] = useState(0)
const load = useCallback(async () => {
const seq = ++requestSeqRef.current
setLoading(true)
setError(false)
try {
const resp = await createBatchVariantPlans({
template_id: templateId,
asset_ids: assetIds,
count,
...(sourcePlanId ? { source_edit_plan_id: sourcePlanId } : {}),
})
if (seq !== requestSeqRef.current) return
const items: VariantPlan[] = Array.isArray(resp.items) ? resp.items : []
const clips: EditPlanClip[][] = Array.from({ length: count }, () => [])
const planIds: string[] = Array.from({ length: count }, () => "")
for (const item of items) {
const idx = item.variant_index
if (idx < 0 || idx >= count) continue
planIds[idx] = item.plan_id || ""
clips[idx] = (item.clips || [])
.filter((c) => c && c.asset_id && c.status === "ready")
.sort((a, b) => a.order - b.order)
}
// 数据完整性校验:每个变体都必须有真实片段,否则视为失败(不允许假数据冒充)
const incomplete = clips.some((list) => list.length === 0)
if (incomplete) {
console.warn("[useBatchVariantPlans] 变体计划数据不完整(存在空片段变体),标记加载失败")
setClipsByVariant([])
setPlanIdsByVariant([])
setError(true)
} else {
setClipsByVariant(clips)
setPlanIdsByVariant(planIds)
setError(false)
}
} catch (err) {
if (seq !== requestSeqRef.current) return
// 404(后端端点未上线)/ 400(参数错误)/ 网络错误 / 超时:统一错误态
console.warn("[useBatchVariantPlans] 申请变体计划失败,预览加载失败:", err)
setClipsByVariant([])
setPlanIdsByVariant([])
setError(true)
} finally {
if (seq === requestSeqRef.current) setLoading(false)
}
}, [templateId, count, sourcePlanId, assetIds])
/** 用户点击「重试」:nonce +1 驱动 effect 重新发起请求(effect 内 lastKey 校验保证只发一次) */
const retry = useCallback(() => {
setRetryNonce((n) => n + 1)
}, [])
useEffect(() => {
if (!enabled || !templateId || assetIds.length === 0 || count <= 1) {
requestSeqRef.current += 1
// 函数式更新:已是目标值时返回 prev(Object.is 相等 React 跳过渲染),
// 避免父组件传入内联字面量数组导致 effect 每次 render 触发 → 无限 setState 循环
setClipsByVariant((prev) => (prev.length === 0 ? prev : []))
setPlanIdsByVariant((prev) => (prev.length === 0 ? prev : []))
setLoading((prev) => (prev === false ? prev : false))
setError((prev) => (prev === false ? prev : false))
lastKeyRef.current = ""
return
}
const key = `${retryNonce}|${templateId}|${count}|${sourcePlanId}|${[...assetIds]
.sort()
.join(",")}`
if (key === lastKeyRef.current) return
lastKeyRef.current = key
load()
}, [enabled, templateId, count, sourcePlanId, assetIds, load, retryNonce])
const ready = !error && !loading && clipsByVariant.every((list) => list.length > 0)
return {
clipsByVariant,
planIdsByVariant,
loading,
ready,
error,
retry,
}
}
export default useBatchVariantPlans
@@ -178,6 +178,14 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
isBatch && props.variantCoverUrls?.length
? indexes.map((i) => props.variantCoverUrls![i] || "")
: []
// #1744 变体 plan 数组:预览阶段后端独立选片产出的 plan id,按勾选顺序回传,
// 后端直接关联这些 plan 渲染(不再重新选片)→ 预览所见即成片。
// 全部为空(降级本地模拟/后端端点未上线)时不传,后端走自身独立选片。
const variantPlansArr =
isBatch && props.variantPlanIds?.length
? indexes.map((i) => props.variantPlanIds![i] || "")
: []
const hasVariantPlans = variantPlansArr.some((id) => !!id)
try {
const taskResp = await createGenerationTask({
@@ -200,6 +208,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
...(titlesArr.length ? { titles: titlesArr } : {}),
...(voiceArr.length ? { voice_library_ids: voiceArr } : {}),
...(coversArr.length ? { cover_urls: coversArr } : {}),
...(hasVariantPlans ? { variant_plan_ids: variantPlansArr } : {}),
...(props.titleSettings?.title
? {
title_config: {
@@ -44,6 +44,12 @@ interface UsePreviewAssetsReturn {
ready: boolean
/** 手动触发重新加载 */
reload: () => void
/**
* 差集补拉(#1750):后端变体计划 clips 可能引用不在用户已选列表中的素材
* (跨素材库选片/素材池扩展),发现 assets 中缺失的 asset_id 时补拉详情并合并,
* 保证预览播放器拿得到素材文件 URL,而不是静默丢片段。
*/
ensureAssets: (ids: string[]) => Promise<void>
}
/**
@@ -91,11 +97,41 @@ export function usePreviewAssets(assetIds: string[], enabled: boolean): UsePrevi
}
}, [stableAssetIds, enabled])
// 补拉用独立序号:不干扰主 load 的竞态守卫
const ensureSeqRef = useRef(0)
const assetsRef = useRef<AssetItem[]>([])
useEffect(() => {
assetsRef.current = assets
}, [assets])
/**
* 差集补拉(#1750):变体计划 clips 引用的 asset_id 不在当前素材列表时,
* 补拉素材详情并去重合并(不静默丢片段、不用假数据冒充)。
*/
const ensureAssets = useCallback(async (ids: string[]) => {
const validIds = ids.filter((id): id is string => typeof id === "string" && id.length > 0)
if (!validIds.length) return
const seq = ++ensureSeqRef.current
const missing = Array.from(new Set(validIds)).filter(
(id) => !assetsRef.current.some((a) => a.id === id),
)
if (!missing.length) return
const fetched = await fetchAssetsByIds(missing)
if (seq !== ensureSeqRef.current || !fetched.length) return
const existing = new Set(assetsRef.current.map((a) => a.id))
const additions = fetched.filter((a) => !existing.has(a.id))
if (!additions.length) return
const merged = [...assetsRef.current, ...additions]
assetsRef.current = merged
setAssets(merged)
setReady(merged.length > 0)
}, [])
useEffect(() => {
load()
}, [load])
return { assets, loading, ready, reload: load }
return { assets, loading, ready, reload: load, ensureAssets }
}
/**
@@ -15,6 +15,8 @@ export interface PlaybackSegment {
startTime: number
endTime: number
order: number
/** #1754 兜底:配音时长≠clips 总时长时按比例调速,1.0 = 原速 */
playbackRate?: number
}
export interface SegmentSchedulerState {
@@ -131,6 +133,11 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
if (Math.abs(video.currentTime - localTime) > 0.05) {
video.currentTime = localTime
}
// #1754:按比例调速(配音时长≠clips 总时长时的前端兜底)
const rate = seg.playbackRate || 1
if (Math.abs(video.playbackRate - rate) > 0.01) {
video.playbackRate = rate
}
segIdxRef.current = index
setCurrentSegmentIndex(index)
@@ -242,6 +249,12 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
video.currentTime = seg.startTime
}
// #1754:调速
const rate = seg?.playbackRate || 1
if (Math.abs(video.playbackRate - rate) > 0.01) {
video.playbackRate = rate
}
try {
await video.play()
setIsPlaying(true)
@@ -1,297 +0,0 @@
/**
* 服务器渲染预览 Hook
*
* 核心职责:
* 1. 调用 POST /generation/preview 创建服务器预览渲染任务
* 2. 轮询 GET /generation/preview/{task_id} 直到完成
* 3. 返回服务器渲染的真实视频 URL(供 <video> 标签播放)
* 4. 检测配置变更,标记预览失效(stale)或自动重新渲染
* 5. 网络错误自动重试 2 次
*
* 状态机:
* idle → loading → ready → stale (config changed)
* ↘ failed → idle (retry)
*/
import { useState, useCallback, useRef, useEffect } from "react"
import { createPreview, getPreviewStatus } from "@/api/generation/preview"
import type { CreatePreviewRequest } from "@/api/generation/types"
export type ServerPreviewStatus = "idle" | "loading" | "ready" | "stale" | "failed"
interface UseServerPreviewOptions {
/** 是否启用预览(Step4+ 且有素材和模板时) */
enabled: boolean
/** 构建预览请求参数(每次 render 调用,获取最新配置) */
buildRequest: () => CreatePreviewRequest
/** 预览任务创建成功回调 */
onPreviewTaskCreated?: (taskId: string, sourceEditPlanId?: string) => void
}
interface UseServerPreviewReturn {
status: ServerPreviewStatus
videoUrl: string | null
error: string | null
/** 进度 0-100 */
progress: number
/** 手动触发预览创建("重新预览"按钮或标题变更后手动刷新) */
triggerPreview: () => void
/** 当前预览任务 ID */
taskId: string | null
}
const POLL_INTERVAL = 2000
const POLL_TIMEOUT = 120_000
const MAX_NETWORK_RETRIES = 2
/**
* 对配置参数做指纹,用于检测配置是否变化
*/
function buildFingerprint(req: CreatePreviewRequest): string {
return JSON.stringify({
t: req.template_id,
a: [...req.asset_ids].sort(),
d: req.duration,
r: req.video_ratio,
v: req.voice_library_id,
b: req.bgm_config,
title: req.title_config,
})
}
export function useServerPreview({
enabled,
buildRequest,
onPreviewTaskCreated,
}: UseServerPreviewOptions): UseServerPreviewReturn {
const [status, setStatus] = useState<ServerPreviewStatus>("idle")
const [videoUrl, setVideoUrl] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [progress, setProgress] = useState(0)
const [taskId, setTaskId] = useState<string | null>(null)
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const timeoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const requestSeqRef = useRef(0)
const renderedFingerprintRef = useRef<string>("")
const mountedRef = useRef(true)
const networkRetriesRef = useRef(0)
// 始终持有最新的 buildRequest 和回调
const buildRequestRef = useRef(buildRequest)
buildRequestRef.current = buildRequest
const onCreatedRef = useRef(onPreviewTaskCreated)
onCreatedRef.current = onPreviewTaskCreated
/* ── 清理 ── */
const clearTimers = useCallback(() => {
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current)
pollTimerRef.current = null
}
if (timeoutTimerRef.current) {
clearTimeout(timeoutTimerRef.current)
timeoutTimerRef.current = null
}
}, [])
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
clearTimers()
}
}, [clearTimers])
/* ── 创建预览 + 轮询 ── */
const createAndPoll = useCallback(
async (request: CreatePreviewRequest, seq: number) => {
setStatus("loading")
setProgress(0)
setError(null)
networkRetriesRef.current = 0
try {
const resp = await createPreview(request)
if (seq !== requestSeqRef.current || !mountedRef.current) return
// 兼容批量响应 {items, total}:取第一个变体
const firstTask = resp.items?.[0]
const taskId = firstTask?.task_id || ""
setTaskId(taskId)
onCreatedRef.current?.(taskId, resp.source_edit_plan_id)
let completed = false
// 超时保护
timeoutTimerRef.current = setTimeout(() => {
if (completed || seq !== requestSeqRef.current || !mountedRef.current) return
completed = true
clearTimers()
setStatus("failed")
setError("预览渲染超时(120秒),请重试")
}, POLL_TIMEOUT)
const poll = async () => {
if (completed || seq !== requestSeqRef.current || !mountedRef.current) return
try {
const st = await getPreviewStatus(taskId)
if (completed || seq !== requestSeqRef.current || !mountedRef.current) return
if (st.status === "completed" && st.video_url) {
completed = true
clearTimers()
renderedFingerprintRef.current = buildFingerprint(request)
setVideoUrl(st.video_url)
setProgress(100)
setStatus("ready")
setError(null)
return
}
if (st.status === "failed" || st.status === "cancelled") {
completed = true
clearTimers()
setStatus("failed")
setError(
st.status === "cancelled"
? "预览任务已取消"
: st.error_message || "预览渲染失败,请重试",
)
return
}
// pending / generating
if (typeof st.progress === "number") setProgress(st.progress)
pollTimerRef.current = setTimeout(poll, POLL_INTERVAL)
} catch (pollErr) {
if (completed || seq !== requestSeqRef.current || !mountedRef.current) return
if (networkRetriesRef.current < MAX_NETWORK_RETRIES) {
networkRetriesRef.current += 1
console.warn(
`[ServerPreview] 轮询网络错误,第 ${networkRetriesRef.current} 次重试`,
pollErr,
)
pollTimerRef.current = setTimeout(poll, POLL_INTERVAL * 2)
} else {
completed = true
clearTimers()
setStatus("failed")
setError("网络错误,无法获取预览状态,请重试")
}
}
}
poll()
} catch (createErr) {
if (seq !== requestSeqRef.current || !mountedRef.current) return
console.error("[ServerPreview] 创建预览任务失败:", createErr)
const isNetworkError =
!!(createErr as { request?: unknown })?.request ||
(createErr as { code?: string })?.code === "ERR_NETWORK"
if (isNetworkError && networkRetriesRef.current < MAX_NETWORK_RETRIES) {
networkRetriesRef.current += 1
console.warn(`[ServerPreview] 创建任务网络错误,第 ${networkRetriesRef.current} 次重试`)
setTimeout(() => {
if (seq === requestSeqRef.current && mountedRef.current) {
createAndPoll(request, seq)
}
}, POLL_INTERVAL * 2)
return
}
const errData = (
createErr as { response?: { data?: { detail?: string; message?: string } } }
)?.response?.data
setStatus("failed")
setError(errData?.detail || errData?.message || "预览任务创建失败,请重试")
}
},
[clearTimers],
)
/* ── 手动触发预览 ── */
const triggerPreview = useCallback(() => {
if (!enabled) return
const request = buildRequestRef.current()
if (!request.template_id || request.asset_ids.length === 0) return
clearTimers()
const seq = ++requestSeqRef.current
setVideoUrl(null)
setTaskId(null)
createAndPoll(request, seq)
}, [enabled, clearTimers, createAndPoll])
/* ── 自动触发 + 配置变更检测 ── */
// 每次 render 都检查最新配置 fingerprint,与已渲染的 fingerprint 比较
const request = enabled ? buildRequest() : null
const currentFingerprint = request
? request.template_id && request.asset_ids.length > 0
? buildFingerprint(request)
: ""
: ""
// 首次进入自动触发
const didInitRef = useRef(false)
useEffect(() => {
if (!enabled || !currentFingerprint) {
didInitRef.current = false
// 禁用时取消进行中的轮询,避免回到前序步骤后仍在后台轮询
requestSeqRef.current += 1
clearTimers()
return
}
if (!didInitRef.current) {
didInitRef.current = true
renderedFingerprintRef.current = currentFingerprint
triggerPreview()
}
}, [enabled, currentFingerprint, triggerPreview, clearTimers])
// 配置变更检测:素材/配音/BGM 等变化 → 自动重渲染;标题样式变化 → 标记 stale
const prevFingerprintRef = useRef(currentFingerprint)
useEffect(() => {
if (!enabled || !currentFingerprint) return
const prev = prevFingerprintRef.current
prevFingerprintRef.current = currentFingerprint
if (!prev || prev === currentFingerprint) return
if (currentFingerprint === renderedFingerprintRef.current) return
// 配置已变更
// 判断是标题样式变更还是素材/配音/BGM 变更
const prevParsed = JSON.parse(prev) as Record<string, unknown>
const currParsed = JSON.parse(currentFingerprint) as Record<string, unknown>
const nonTitleChanged =
prevParsed.t !== currParsed.t ||
prevParsed.a !== currParsed.a ||
prevParsed.d !== currParsed.d ||
prevParsed.r !== currParsed.r ||
prevParsed.v !== currParsed.v ||
JSON.stringify(prevParsed.b) !== JSON.stringify(currParsed.b)
if (nonTitleChanged) {
// 素材/配音/BGM/模板等变化 → 自动重新渲染
renderedFingerprintRef.current = currentFingerprint
triggerPreview()
} else {
// 仅标题文字/样式变化 → 标记 stale,不自动重渲染(避免频繁请求)
// 实时预览由 CSS TitleOverlay 提供
setStatus((s) => (s === "ready" ? "stale" : s))
}
}, [enabled, currentFingerprint, triggerPreview])
return {
status,
videoUrl,
error,
progress,
triggerPreview,
taskId,
}
}
export default useServerPreview
@@ -0,0 +1,166 @@
/**
* 批量变体配音预览音频解析(#1750)
*
* 独立配音模式下每个变体挂载各自的配音 URL:
* - 配音素材(voice 资产,有 file_url)→ 直接用素材文件 URL;
* - AI 音色(预置/克隆,无实体文件)→ 按该变体自己的标题调 /tts/preview 合成;
* - 共用模式下所有变体解析为同一条(等价于旧 previewVoiceAudioUrl)。
*
* N=1 不使用本 hook(单视频配音预览逻辑在 GeneratePage 内保持不变,零回归)。
*/
import { useEffect, useRef, useState } from "react"
import { useQuery } from "@tanstack/react-query"
import { getAssetsByKind } from "@/api/assets"
import type { AssetItem } from "@/api/assets"
import { previewTts } from "@/api/tts"
interface UseVariantVoicePreviewOptions {
/** 是否批量模式(count>1) */
enabled: boolean
/** 变体数量 */
count: number
/** 是否每个视频独立配音 */
perVideo: boolean
/** 共用配音 ID(独立模式下为变体 0 的配音) */
sharedVoiceId: string
/** 克隆音色 ID 覆盖(共用模式,与旧逻辑一致:selectedClonedVoice || selectedVoice) */
clonedVoiceId?: string
/** 各变体独立配音 ID(独立模式);长度不足时回退共用 */
variantVoiceIds: string[]
/** 各变体标题(TTS 合成文案源) */
titles: string[]
}
/** 稳定的空数组常量:避免 useQuery 数据未就绪时每次渲染产生新引用导致 effect 无限触发 */
const EMPTY_VOICE_MATERIALS: AssetItem[] = []
/** 判断配音 ID 是否对应实体素材(有 file_url);否则视为 AI 音色需 TTS */
function findMaterialUrl(id: string, materials: AssetItem[]): string | null {
if (!id) return null
const m = materials.find((x) => x.id === id)
return m?.file_url || null
}
export function useVariantVoicePreview({
enabled,
count,
perVideo,
sharedVoiceId,
clonedVoiceId = "",
variantVoiceIds,
titles,
}: UseVariantVoicePreviewOptions): (string | null)[] {
const [urls, setUrls] = useState<(string | null)[]>([])
// 配音素材库:组件内部自取,避免调用方传入不稳定数组引用导致 effect 反复触发
const { data: voiceMaterialsData } = useQuery({
queryKey: ["assets", "voice"],
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
})
const voiceMaterials: AssetItem[] = voiceMaterialsData ?? EMPTY_VOICE_MATERIALS
// 已缓存的 TTS 结果:key = `${voiceId}|${title}`,避免重复合成
const ttsCacheRef = useRef<Map<string, string>>(new Map())
// 在途请求 AbortController
const abortRef = useRef<AbortController | null>(null)
const seqRef = useRef(0)
useEffect(() => {
if (!enabled || count <= 1) {
setUrls((prev) => (prev.length === 0 ? prev : []))
return
}
const seq = ++seqRef.current
abortRef.current?.abort()
const controller = new AbortController()
abortRef.current = controller
// 每个变体实际使用的配音 ID
const voiceIds = Array.from({ length: count }, (_, i) =>
perVideo ? variantVoiceIds[i] || sharedVoiceId : sharedVoiceId,
)
// 先用素材 URL 同步填充;AI 音色位置先置 null,待 TTS 完成
const result: (string | null)[] = voiceIds.map((id, i) => {
const materialUrl = findMaterialUrl(id, voiceMaterials)
if (materialUrl) return materialUrl
// 共用模式下克隆音色 ID 可能与 selectedVoice 不同(与旧 useEffect 逻辑一致)
if (!perVideo && i === 0 && clonedVoiceId) {
return findMaterialUrl(clonedVoiceId, voiceMaterials)
}
return null
})
// 收集需要 TTS 的变体:无素材 URL 且有标题且有音色 ID
const ttsJobs: { index: number; voiceId: string; title: string }[] = []
voiceIds.forEach((id, i) => {
if (result[i]) return
// 共用模式沿用旧逻辑:voice_id = selectedClonedVoice || selectedVoice
const ttsVoiceId = !perVideo && i === 0 ? clonedVoiceId || id : id
const title = titles[i] || ""
if (!ttsVoiceId || !title) return
ttsJobs.push({ index: i, voiceId: ttsVoiceId, title })
})
setUrls((prev) =>
prev.length === result.length && prev.every((v, i) => v === result[i]) ? prev : result,
)
if (ttsJobs.length === 0) return
let cancelled = false
void (async () => {
// 串行 TTS,避免瞬间 N 个合成请求打爆后端
for (const job of ttsJobs) {
const cacheKey = `${job.voiceId}|${job.title}`
const cached = ttsCacheRef.current.get(cacheKey)
if (cached) {
if (seq === seqRef.current) {
setUrls((prev) => {
if (prev[job.index] === cached) return prev
const next = [...prev]
next[job.index] = cached
return next
})
}
continue
}
try {
// eslint-disable-next-line no-await-in-loop
const res = await previewTts({ text: job.title, voice_id: job.voiceId })
if (cancelled || controller.signal.aborted || seq !== seqRef.current) return
const audioUrl = res.audio_url || ""
if (audioUrl) {
ttsCacheRef.current.set(cacheKey, audioUrl)
setUrls((prev) => {
if (prev[job.index] === audioUrl) return prev
const next = [...prev]
next[job.index] = audioUrl
return next
})
}
} catch (err) {
if (cancelled || controller.signal.aborted || seq !== seqRef.current) return
console.warn(`[变体${job.index + 1}预览配音生成失败]`, err)
}
}
})()
return () => {
cancelled = true
controller.abort()
}
}, [
enabled,
count,
perVideo,
sharedVoiceId,
clonedVoiceId,
variantVoiceIds,
titles,
voiceMaterials,
])
return urls
}
export default useVariantVoicePreview
@@ -1,54 +0,0 @@
/**
* 共享:根据素材列表和模板片段计算总视频时长
* GeneratePage(配音校验)和 FrontendPreviewPlayer(播放控制)共用
*/
export interface DurationAsset {
id?: string
duration?: number
metadata?: { duration?: number }
}
export interface DurationTemplateSegment {
duration_min?: number
duration_max?: number
}
/**
* 计算总视频时长
* @param assets 素材列表
* @param template 模板(含 segments)
* @returns 总时长(秒),无有效数据时返回 0
*/
export function calculateTotalVideoDuration(
assets: DurationAsset[] | undefined,
template: { segments?: DurationTemplateSegment[] } | undefined,
): number {
if (!assets || assets.length === 0 || !template) return 0
const templateSegments = template.segments || []
return assets.reduce((sum, asset, i) => {
const assetDuration = asset.duration || asset.metadata?.duration || 30
const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1]
const segDuration = tplSeg
? Math.min(
tplSeg.duration_max ?? assetDuration,
Math.max(tplSeg.duration_min ?? 0, assetDuration),
)
: Math.min(assetDuration, 10)
return sum + segDuration
}, 0)
}
/**
* 估算总视频时长(仅依赖模板 segments)
* 当素材未加载或加载失败时,用各片段 duration_max 之和作为估算值
* 确保配音时长校验不会因素材未就绪而跳过
*/
export function estimateTotalVideoDuration(
template: { segments?: DurationTemplateSegment[] } | undefined,
): number {
if (!template?.segments || template.segments.length === 0) return 0
return template.segments.reduce((sum, seg) => sum + (seg.duration_max || 0), 0)
}
@@ -66,7 +66,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
<div className="mt-card-meta">
<Text type="secondary" style={{ fontSize: 12 }}>
{tpl.segments.length} 片段 · 预估 ~{tpl.estimated_duration}s
{tpl.segments.length} 个片段
</Text>
{tpl.category && <Tag style={{ fontSize: 11, marginTop: 4 }}>{tpl.category}</Tag>}
</div>
@@ -57,11 +57,6 @@ export function useMyTemplates() {
title_config: tpl.title_config,
subtitle_config: tpl.subtitle_config,
bgm_config: tpl.bgm_config,
estimated_duration:
tpl.estimated_duration ??
Math.round(
tpl.segments.reduce((s, seg) => s + (seg.duration_min + seg.duration_max) / 2, 0),
),
segments: tpl.segments.map(({ id: _id, ...rest }) => rest),
}),
onSuccess: () => {
@@ -178,3 +178,35 @@
border-color: var(--border-color);
margin: var(--space-lg) 0;
}
/* 微信账号绑定卡片 */
.xx-settings-wechat {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-lg);
flex-wrap: wrap;
}
.xx-settings-wechat-info {
display: flex;
align-items: center;
gap: var(--space-md);
}
.xx-settings-wechat-info .xx-wechat-icon {
font-size: 28px;
line-height: 1;
}
.xx-settings-wechat-info strong {
display: block;
color: var(--text-primary);
font-size: var(--font-size-md);
}
.xx-settings-wechat-info p {
margin: 2px 0 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
+154 -25
View File
@@ -1,39 +1,110 @@
/**
* 个人设置页面
* P1-2: 添加 PageHead
* P1-3: antd Form/Input/Button/Alert → 自定义 UI 组件
* - 个人资料(昵称)保存
* - 微信账号绑定状态 / 绑定 / 解绑
*/
import React, { useState } from "react"
import React, { useEffect, useRef, useState } from "react"
import { useSearchParams } from "react-router-dom"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { message } from "antd"
import { Button, Input, Modal } from "@/components/ui"
import { getCurrentUser, updateProfile, unbindWechat } from "@/api/auth"
import { useAuthStore } from "@/store/authStore"
import PageHead from "@/components/layout/PageHead"
import WechatQrModal from "@/components/auth/WechatQrModal"
import "./ProfileSettings.css"
const Settings: React.FC = () => {
const user = useAuthStore((state) => state.user)
const setUser = useAuthStore((state) => state.setUser)
const queryClient = useQueryClient()
const [searchParams, setSearchParams] = useSearchParams()
const [displayName, setDisplayName] = useState(user?.display_name || "")
const [wechatBindOpen, setWechatBindOpen] = useState(false)
const bindTipShownRef = useRef(false)
const handleSave = () => {
Modal.info({
title: "提示",
content: "个人资料修改接口暂未开放,保存功能即将上线。",
// 拉取最新用户信息(微信绑定状态以后端为准)
const { data: freshUser } = useQuery({
queryKey: ["currentUser"],
queryFn: getCurrentUser,
})
useEffect(() => {
if (freshUser) {
setUser(freshUser)
setDisplayName((prev) => prev || freshUser.display_name || "")
}
}, [freshUser, setUser])
// 绑定回调结果提示(?wechat_bind=success|failed)
useEffect(() => {
if (bindTipShownRef.current) return
const result = searchParams.get("wechat_bind")
if (!result) return
bindTipShownRef.current = true
if (result === "success") {
message.success("微信绑定成功")
} else if (result === "failed") {
message.error("微信绑定失败,请重试")
}
searchParams.delete("wechat_bind")
setSearchParams(searchParams, { replace: true })
}, [searchParams, setSearchParams])
const wechatBound = user?.wechat_bound === true
const saveProfileMutation = useMutation({
mutationFn: () => updateProfile({ display_name: displayName.trim() }),
onSuccess: (updated) => {
setUser(updated)
message.success("资料已保存")
},
onError: () => {
message.error("保存失败,请重试")
},
})
// 弹窗扫码绑定成功:关闭弹窗,刷新用户信息并提示
const handleBindSuccess = () => {
setWechatBindOpen(false)
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
message.success("微信绑定成功")
}
const unbindMutation = useMutation({
mutationFn: unbindWechat,
onSuccess: () => {
message.success("已解绑微信")
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
// 本地立即更新,避免等待刷新
if (user) {
setUser({ ...user, wechat_bound: false, wechat_nickname: "" })
}
},
onError: () => {
message.error("解绑失败,请重试")
},
})
const handleUnbind = () => {
Modal.confirm({
title: "解绑微信",
content: "解绑后将无法使用微信登录该账号,确定要解绑吗?",
okText: "确定解绑",
cancelText: "取消",
okButtonProps: { danger: true },
onOk: () => unbindMutation.mutateAsync(),
})
}
const displayNameDirty = displayName.trim() !== (user?.display_name || "")
return (
<div className="xx-settings-page">
<PageHead title="个人设置" description="管理您的账户信息" />
<div className="xx-settings-card">
<h3>个人信息</h3>
<div className="xx-settings-notice">
<span className="xx-settings-notice-icon">ℹ️</span>
<div>
<strong>个人资料编辑暂未开放</strong>
<p>当前仅展示登录用户信息,资料修改接口接入后再开放保存。</p>
</div>
</div>
<div className="xx-settings-form">
<div className="xx-settings-field">
<label className="xx-settings-label">用户名</label>
@@ -42,25 +113,83 @@ const Settings: React.FC = () => {
<div className="xx-settings-field">
<label className="xx-settings-label">邮箱</label>
<Input value={user?.email || ""} disabled placeholder="邮箱" />
</div>
<div className="xx-settings-field">
<label className="xx-settings-label">显示名称</label>
<Input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="请输入显示名称"
value={user?.email && !user.email.endsWith("@wechat.local") ? user.email : ""}
disabled
placeholder={user?.email?.endsWith("@wechat.local") ? "微信账号暂未绑定邮箱" : "邮箱"}
/>
</div>
<div className="xx-settings-field">
<Button buttonType="primary" buttonSize="md" onClick={handleSave} disabled>
保存暂未开放
<label className="xx-settings-label">昵称</label>
<Input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="请输入昵称"
maxLength={20}
/>
</div>
<div className="xx-settings-field">
<Button
buttonType="primary"
buttonSize="md"
onClick={() => saveProfileMutation.mutate()}
loading={saveProfileMutation.isPending}
disabled={!displayName.trim() || !displayNameDirty}
>
保存
</Button>
</div>
</div>
</div>
<div className="xx-settings-card">
<h3>微信账号</h3>
<div className="xx-settings-wechat">
<div className="xx-settings-wechat-info">
<span className="xx-wechat-icon">💬</span>
<div>
{wechatBound ? (
<>
<strong>
已绑定微信{user?.wechat_nickname ? `(${user.wechat_nickname})` : ""}
</strong>
<p>可使用微信扫码登录本账号</p>
</>
) : (
<>
<strong>未绑定微信</strong>
<p>绑定后可使用微信扫码快速登录</p>
</>
)}
</div>
</div>
<div className="xx-settings-wechat-actions">
{wechatBound ? (
<Button
buttonType="ghost"
buttonSize="md"
onClick={handleUnbind}
loading={unbindMutation.isPending}
>
解绑
</Button>
) : (
<Button buttonType="primary" buttonSize="md" onClick={() => setWechatBindOpen(true)}>
绑定微信
</Button>
)}
</div>
</div>
</div>
<WechatQrModal
open={wechatBindOpen}
scene="bind"
onClose={() => setWechatBindOpen(false)}
onBindSuccess={handleBindSuccess}
/>
</div>
)
}
@@ -1,7 +1,7 @@
/**
* 模板库页面(升级版)— V21 设计系统
* 对接后端模板管理 API:
* - 分页查询(page/page_size/category/keyword/duration_range)
* - 分页查询(page/page_size/category/keyword)
* - 模板详情(素材规则、字幕样式、BGM、比例等参数配置)
* - 复制模板 / 从模板生成
* - 卡片网格布局 + 类型筛选 + 搜索 + 收藏
@@ -24,7 +24,6 @@ const TemplateLibrary: React.FC = () => {
error,
searchText,
activeType,
durationRange,
page,
pageSize,
setPage,
@@ -34,7 +33,6 @@ const TemplateLibrary: React.FC = () => {
handleCreate,
handleSearchChange,
handleCategoryChange,
handleDurationChange,
} = useTemplateLibrary()
const {
@@ -62,8 +60,6 @@ const TemplateLibrary: React.FC = () => {
onSearchChange={handleSearchChange}
activeType={activeType}
onTypeChange={handleCategoryChange}
durationRange={durationRange}
onDurationChange={handleDurationChange}
/>
{/* 模板展示区 */}
@@ -77,7 +73,6 @@ const TemplateLibrary: React.FC = () => {
errorMessage={error?.message}
searchText={searchText}
activeType={activeType}
durationRange={durationRange}
onPageChange={setPage}
onPreview={handlePreview}
onToggleFavorite={toggleFavorite}
@@ -1,7 +1,7 @@
import React from "react"
import { Tag } from "antd"
import type { TemplateItem } from "@/api/templates"
import { gradientForCategory, getTypeColor, formatDuration } from "../../utils/templateLibrary"
import { gradientForCategory, getTypeColor } from "../../utils/templateLibrary"
interface TemplateCardProps {
template: TemplateItem
@@ -37,7 +37,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
<div className="xx-template-thumb-name">{template.name}</div>
<div className="xx-template-thumb-meta">
<span className="xx-template-thumb-duration">
{formatDuration(template.estimated_duration ?? template.target_duration)}
{template.clip_count || template.segments?.length || 0} 个片段
</span>
</div>
<div className="xx-template-preview-hint">点击查看详情</div>
@@ -5,9 +5,8 @@ import PreviewArea from "./template-detail-modal/PreviewArea"
import SegmentList from "./template-detail-modal/SegmentList"
import StyleConfig from "./template-detail-modal/StyleConfig"
import DetailFooter from "./template-detail-modal/DetailFooter"
import { getTypeColor, formatDuration } from "../../utils/templateLibrary"
import { getTypeColor } from "../../utils/templateLibrary"
import { TEMPLATE_TYPES } from "../../constants/templateLibrary"
import { calcTotalSegmentDuration } from "../../utils/templateLibrary"
export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
template,
@@ -18,7 +17,6 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
onCopy,
}) => {
const segments = template.segments ?? []
const totalSegmentDuration = calcTotalSegmentDuration(segments)
const typeInfo = TEMPLATE_TYPES.find((t) => t.type === template.category)
return (
@@ -64,11 +62,6 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
size="small"
className="xx-template-modal-desc-table"
items={[
{
key: "duration",
label: "目标时长",
children: formatDuration(template.estimated_duration ?? template.target_duration),
},
{
key: "clips",
label: "片段数量",
@@ -87,7 +80,7 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
]}
/>
<SegmentList segments={segments} totalDuration={totalSegmentDuration} />
<SegmentList segments={segments} />
<StyleConfig template={template} />
<DetailFooter
template={template}
@@ -14,7 +14,6 @@ interface TemplateGridProps {
errorMessage?: string
searchText: string
activeType: string
durationRange: string
onPageChange: (page: number) => void
onPreview: (template: TemplateItem) => void
onToggleFavorite: (id: string, e: React.MouseEvent) => void
@@ -31,7 +30,6 @@ export const TemplateGrid: React.FC<TemplateGridProps> = ({
errorMessage,
searchText,
activeType,
durationRange,
onPageChange,
onPreview,
onToggleFavorite,
@@ -64,7 +62,7 @@ export const TemplateGrid: React.FC<TemplateGridProps> = ({
// 空状态
if (templates.length === 0) {
const hasFilter = !!searchText || activeType !== "全部" || !!durationRange
const hasFilter = !!searchText || activeType !== "全部"
return (
<div className="xx-templates-empty">
<div className="xx-templates-empty-icon">
@@ -1,15 +1,13 @@
import React from "react"
import { SearchOutlined } from "@ant-design/icons"
import type { EditTemplateType, DurationRange } from "../../types/templateLibrary"
import { TEMPLATE_TYPES, DURATION_OPTIONS } from "../../constants/templateLibrary"
import type { EditTemplateType } from "../../types/templateLibrary"
import { TEMPLATE_TYPES } from "../../constants/templateLibrary"
interface TemplateToolbarProps {
searchText: string
onSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void
activeType: EditTemplateType | "全部"
onTypeChange: (type: EditTemplateType | "全部") => void
durationRange: DurationRange
onDurationChange: (value: DurationRange) => void
}
export const TemplateToolbar: React.FC<TemplateToolbarProps> = ({
@@ -17,8 +15,6 @@ export const TemplateToolbar: React.FC<TemplateToolbarProps> = ({
onSearchChange,
activeType,
onTypeChange,
durationRange,
onDurationChange,
}) => {
return (
<div className="xx-templates-toolbar">
@@ -46,18 +42,8 @@ export const TemplateToolbar: React.FC<TemplateToolbarProps> = ({
</button>
))}
</div>
{/* 时长筛选 */}
<div className="xx-templates-duration-filter">
{DURATION_OPTIONS.map((opt) => (
<button
key={opt.value}
className={`xx-templates-duration-btn${durationRange === opt.value ? " active" : ""}`}
onClick={() => onDurationChange(opt.value)}
>
{opt.label}
</button>
))}
</div>
</div>
)
}
export default TemplateToolbar
@@ -1,15 +1,13 @@
import React from "react"
import { Tooltip } from "antd"
import type { TemplateSegment } from "@/api/templates"
import { getMaterialTypeLabel, getTypeColor, formatDuration } from "../../../utils/templateLibrary"
import { getMaterialTypeLabel, getTypeColor } from "../../../utils/templateLibrary"
interface SegmentListProps {
segments: TemplateSegment[]
totalDuration: number
}
/** 素材规则 / 片段列表 */
const SegmentList: React.FC<SegmentListProps> = ({ segments, totalDuration }) => {
/** 素材规则 / 片段列表(#1750:模板只定片段数/结构,无时长概念) */
const SegmentList: React.FC<SegmentListProps> = ({ segments }) => {
if (segments.length === 0) return null
return (
@@ -35,17 +33,9 @@ const SegmentList: React.FC<SegmentListProps> = ({ segments, totalDuration }) =>
<span className="xx-template-modal-clip-desc">
{seg.description || `片段 ${seg.segment_order}`}
</span>
<Tooltip title={`时长范围: ${seg.duration_min}秒 - ${seg.duration_max}秒`}>
<span className="xx-template-modal-clip-duration">
{seg.duration_min}-{seg.duration_max}秒
</span>
</Tooltip>
</div>
))}
</div>
<div className="xx-template-modal-total-duration">
预估总时长:{formatDuration(Math.round(totalDuration))}
</div>
</div>
)
}
@@ -1,4 +1,4 @@
import type { EditTemplateType, DurationRange } from "../types/templateLibrary"
import type { EditTemplateType } from "../types/templateLibrary"
export const TEMPLATE_TYPES: Array<{
type: EditTemplateType | "全部"
@@ -15,16 +15,6 @@ export const TEMPLATE_TYPES: Array<{
{ type: "Vlog", label: "Vlog", icon: "📹", color: "#ec4899" },
]
export const DURATION_OPTIONS: Array<{
value: DurationRange
label: string
}> = [
{ value: "", label: "全部时长" },
{ value: "short", label: "30秒以内" },
{ value: "medium", label: "30秒-2分钟" },
{ value: "long", label: "2分钟以上" },
]
export const MATERIAL_TYPE_LABELS: Record<string, string> = {
video: "视频",
image: "图片",
@@ -9,7 +9,7 @@ import {
type TemplateItem,
type TemplateListParams,
} from "@/api/templates"
import type { EditTemplateType, DurationRange } from "../types/templateLibrary"
import type { EditTemplateType } from "../types/templateLibrary"
import { DEFAULT_PAGE_SIZE } from "../constants/templateLibrary"
export const useTemplateLibrary = () => {
@@ -19,7 +19,6 @@ export const useTemplateLibrary = () => {
/* 筛选状态 */
const [searchText, setSearchText] = useState("")
const [activeType, setActiveType] = useState<EditTemplateType | "全部">("全部")
const [durationRange, setDurationRange] = useState<DurationRange>("")
const [page, setPage] = useState(1)
const [pageSize] = useState(DEFAULT_PAGE_SIZE)
@@ -31,9 +30,8 @@ export const useTemplateLibrary = () => {
}
if (activeType !== "全部") params.category = activeType
if (searchText.trim()) params.keyword = searchText.trim()
if (durationRange) params.duration_range = durationRange
return params
}, [page, pageSize, activeType, searchText, durationRange])
}, [page, pageSize, activeType, searchText])
/* 获取模板列表 */
const {
@@ -112,12 +110,6 @@ export const useTemplateLibrary = () => {
setPage(1)
}, [])
/* 切换时长筛选 */
const handleDurationChange = useCallback((value: DurationRange) => {
setDurationRange(value)
setPage(1)
}, [])
return {
/* 状态 */
templates,
@@ -127,7 +119,6 @@ export const useTemplateLibrary = () => {
error,
searchText,
activeType,
durationRange,
page,
pageSize,
/* mutations */
@@ -142,6 +133,5 @@ export const useTemplateLibrary = () => {
handleCreate,
handleSearchChange,
handleCategoryChange,
handleDurationChange,
}
}
@@ -2,7 +2,6 @@
export type EditTemplateType = "口播" | "种草" | "产品" | "品牌" | "混剪" | "Vlog"
/** 时长筛选值 */
export type DurationRange = "" | "short" | "medium" | "long"
/** 配置展示字段 */
export interface ConfigDisplayFields {
@@ -5,7 +5,6 @@ import {
DEFAULT_GRADIENT,
MATERIAL_TYPE_LABELS,
} from "../constants/templateLibrary"
import type { TemplateSegment } from "@/api/templates"
/** 获取类型对应颜色 */
export const getTypeColor = (type: string): string => {
@@ -18,16 +17,6 @@ export const gradientForCategory = (category: string): string => {
return CATEGORY_GRADIENT_MAP[category] ?? DEFAULT_GRADIENT
}
/** 格式化时长 */
export const formatDuration = (seconds: number | undefined | null): string => {
if (!seconds || seconds <= 0) return "0秒"
const totalSec = Math.round(seconds)
const m = Math.floor(totalSec / 60)
const s = totalSec % 60
if (m === 0) return `${s}秒`
return `${m}分${s > 0 ? `${s}秒` : ""}`
}
/** 格式化配置对象为可读文本 */
export const formatConfig = (config?: object): string => {
if (!config || Object.keys(config).length === 0) return "默认"
@@ -47,8 +36,3 @@ export const getMaterialTypeLabel = (materialType: string | null | undefined): s
if (!materialType) return "不限"
return MATERIAL_TYPE_LABELS[materialType] ?? materialType
}
/** 计算片段总时长(取每个片段 min/max 的平均值) */
export const calcTotalSegmentDuration = (segments: TemplateSegment[]): number => {
return segments.reduce((sum, s) => sum + (s.duration_min + s.duration_max) / 2, 0)
}
+6
View File
@@ -6,10 +6,16 @@ import { useAuthStore } from "@/store/authStore"
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
const hasAccessToken = Boolean(localStorage.getItem("access_token"))
const profileCompleted = useAuthStore((state) => state.user?.profile_completed !== false)
if (!isAuthenticated || !hasAccessToken) {
return <Navigate to="/login" replace />
}
// 微信新用户未完成昵称引导时,禁止进入主界面
if (!profileCompleted) {
return <Navigate to="/welcome/wechat" replace />
}
return <>{children}</>
}
+29 -112
View File
@@ -1,6 +1,7 @@
import { Navigate, type RouteObject } from "react-router-dom"
import MainLayout from "@/components/layout/MainLayout"
import { ProtectedRoute } from "./ProtectedRoute"
import { lazyRoute } from "./lazyRoute"
/**
* 受保护的 /app 子路由
@@ -13,202 +14,118 @@ const appChildren: RouteObject[] = [
},
{
path: "dashboard",
lazy: () =>
import("@/pages/dashboard/Dashboard").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/dashboard/Dashboard")),
},
{
path: "assets",
lazy: () =>
import("@/pages/assets/AssetLibrary").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/assets/AssetLibrary")),
},
{
path: "titles",
lazy: () =>
import("@/pages/titles/TitleLibrary").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/titles/TitleLibrary")),
},
{
path: "voices",
lazy: () =>
import("@/pages/voices/VoiceLibrary").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/voices/VoiceLibrary")),
},
{
path: "templates",
lazy: () =>
import("@/pages/templates/TemplateLibrary").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/templates/TemplateLibrary")),
},
{
path: "generate",
lazy: () =>
import("@/pages/generate/GeneratePage").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/generate/GeneratePage")),
},
{
path: "history",
lazy: () =>
import("@/pages/history/TaskHistory").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/history/TaskHistory")),
},
{
path: "products",
lazy: () =>
import("@/pages/products/ProductLibrary").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/products/ProductLibrary")),
},
{
path: "products/:id",
lazy: () =>
import("@/pages/products/ProductDetail").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/products/ProductDetail")),
},
{
path: "tasks",
lazy: () =>
import("@/pages/tasks/TaskCenter").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/tasks/TaskCenter")),
},
{
path: "editing-planner",
lazy: () =>
import("@/pages/editing-planner/EditingPlanner").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/editing-planner/EditingPlanner")),
},
{
path: "my-templates",
lazy: () =>
import("@/pages/my-templates/MyTemplates").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/my-templates/MyTemplates")),
},
{
path: "voice-clone",
lazy: () =>
import("@/pages/voice-clone/VoiceClone").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/voice-clone/VoiceClone")),
},
{
path: "voice-materials",
lazy: () =>
import("@/pages/voice-materials/VoiceMaterialLibrary").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/voice-materials/VoiceMaterialLibrary")),
},
{
path: "my-voices",
lazy: () =>
import("@/pages/my-voices/MyVoices").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/my-voices/MyVoices")),
},
{
path: "accounts",
lazy: () =>
import("@/pages/accounts/Accounts").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/accounts/Accounts")),
},
{
path: "duplication",
lazy: () =>
import("@/pages/duplication/DuplicationUpload").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/duplication/DuplicationUpload")),
},
{
path: "duplication/results",
lazy: () =>
import("@/pages/duplication/DuplicationResults").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/duplication/DuplicationResults")),
},
{
path: "duplication/:id",
lazy: () =>
import("@/pages/duplication/DuplicationDetail").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/duplication/DuplicationDetail")),
},
{
path: "subscription",
lazy: () =>
import("@/pages/subscription/Plans").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/subscription/Plans")),
},
{
path: "subscription/upgrade",
lazy: () =>
import("@/pages/subscription/UpgradeSubscription").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/subscription/UpgradeSubscription")),
},
{
path: "subscription/billing",
lazy: () =>
import("@/pages/subscription/Billing").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/subscription/Billing")),
},
{
path: "profile",
lazy: () =>
import("@/pages/profile/Settings").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/profile/Settings")),
},
{
path: "admin",
children: [
{
index: true,
lazy: () =>
import("@/pages/admin/AdminComingSoon").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/admin/AdminComingSoon")),
},
{
path: "users",
lazy: () =>
import("@/pages/admin/AdminComingSoon").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/admin/AdminComingSoon")),
},
{
path: "analytics",
lazy: () =>
import("@/pages/admin/AdminComingSoon").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/admin/AdminComingSoon")),
},
{
path: "monitor",
lazy: () =>
import("@/pages/admin/AdminComingSoon").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/admin/AdminComingSoon")),
},
{
path: "logs",
lazy: () =>
import("@/pages/admin/AdminComingSoon").then((m) => ({
Component: m.default,
})),
lazy: lazyRoute(() => import("@/pages/admin/AdminComingSoon")),
},
],
},
+40
View File
@@ -0,0 +1,40 @@
import type { LazyRouteFunction, RouteObject } from "react-router-dom"
import { isChunkLoadError } from "@/utils/chunkLoadError"
/**
* 给 React Router data router 的路由懒加载包一层自动重试:
*
* - 网络抖动 / 瞬态失败:自动重试最多 2 次(间隔 300ms / 800ms),用户无感恢复
* - 发版后旧 chunk 404(chunk 文件名已不存在):重试也拿不到旧文件名,
* 重试耗尽后抛出,由全局 ChunkErrorBoundary 捕获并引导整页刷新
* (刷新后 index.html 是 no-cache 的,会拿到新 chunk 引用)
*/
const RETRY_DELAYS_MS = [300, 800]
const RETRY_COUNT = RETRY_DELAYS_MS.length
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
export const lazyRoute = (
factory: () => Promise<{ default: React.ComponentType }>,
): LazyRouteFunction<RouteObject> => {
return async () => {
let lastError: unknown
for (let attempt = 0; attempt <= RETRY_COUNT; attempt++) {
try {
const mod = await factory()
if (!mod.default) {
throw new Error("lazyRoute: 目标模块缺少 default 导出")
}
return { Component: mod.default }
} catch (err) {
lastError = err
// 非 chunk 加载错误(代码 bug 等)立即抛出,不浪费重试
if (!isChunkLoadError(err)) throw err
if (attempt < RETRY_COUNT) {
await sleep(RETRY_DELAYS_MS[attempt])
}
}
}
throw lastError
}
}
+10
View File
@@ -5,6 +5,8 @@ import Register from "@/pages/auth/Register"
import ForgotPassword from "@/pages/auth/ForgotPassword"
import ResetPassword from "@/pages/auth/ResetPassword"
import WechatCallback from "@/pages/auth/WechatCallback"
import WechatOnboarding from "@/pages/auth/WechatOnboarding"
import WechatBindCallback from "@/pages/auth/WechatBindCallback"
import { useAuthStore } from "@/store/authStore"
/** 首页路由组件:已登录跳 dashboard,未登录显示落地页 */
@@ -45,4 +47,12 @@ export const publicRoutes: RouteObject[] = [
path: "/auth/wechat/callback",
element: <WechatCallback />,
},
{
path: "/auth/wechat/bind/callback",
element: <WechatBindCallback />,
},
{
path: "/welcome/wechat",
element: <WechatOnboarding />,
},
]
+6
View File
@@ -13,6 +13,12 @@ interface User {
display_name: string
is_email_verified: boolean
email_verified: boolean
wechat_bound?: boolean
wechat_nickname?: string
avatar_url?: string
phone?: string
phone_verified?: boolean
profile_completed?: boolean
}
interface AuthState {
+122
View File
@@ -242,6 +242,20 @@ describe("assets API", () => {
await expect(completeDirectUpload({ name: "test-item" })).resolves.not.toThrow()
})
it("请求体携带 file_size(后端同名兜底去重的大小校验依赖它)", async () => {
await completeDirectUpload({
project_id: "p-1",
library_id: "l-1",
storage_key: "uploads/k.mp4",
file_size: 12345,
} as never)
const completeCalls = mockPost.mock.calls.filter(
([u]: [string]) => u === "/upload/direct/complete",
)
expect(completeCalls).toHaveLength(1)
expect(completeCalls[0][1]).toMatchObject({ file_size: 12345 })
})
it("should reject on API error", async () => {
mockGet.mockRejectedValue(new Error("Network error"))
mockPost.mockRejectedValue(new Error("Network error"))
@@ -259,6 +273,114 @@ describe("assets API", () => {
})
})
describe("uploadAssetDirect skip_transfer 短路", () => {
it("prepare 返回 skip_transfer=true → 直接返回 duplicated,不调 transfer/complete", async () => {
mockPost.mockImplementation((url: string) => {
if (url === "/upload/direct/prepare") {
return Promise.resolve({
data: {
upload_url: "https://oss/x",
method: "POST",
storage_key: "uploads/skip/y.mp4",
expires_at: "2099",
fields: {},
max_size_bytes: 1e9,
asset_id: "existing-asset",
skip_transfer: true,
duplicated: true,
},
})
}
if (url === "/upload/direct/complete") {
throw new Error("complete 不应被调用")
}
throw new Error("unexpected url " + url)
})
const putSpy = vi.spyOn(globalThis, "XMLHttpRequest")
const file = new File(["x"], "x.mp4", { type: "video/mp4" })
const result = await uploadAssetDirect({ file, library_id: "lib-1" })
expect(result.duplicated).toBe(true)
expect(result.asset_id).toBe("existing-asset")
// complete 未被调用(mockPost 只记录 prepare,complete 若调用会抛 "不应被调用")
const completeCalls = mockPost.mock.calls.filter(
([u]: [string]) => u === "/upload/direct/complete",
)
expect(completeCalls).toHaveLength(0)
putSpy.mockRestore()
})
it("prepare 返回 skip_transfer=false → 走老流程(complete 被调用)", async () => {
mockPost.mockImplementation((url: string) => {
if (url === "/upload/direct/prepare") {
return Promise.resolve({
data: {
upload_url: "https://oss/x",
method: "POST",
storage_key: "uploads/normal/y.mp4",
expires_at: "2099",
fields: {},
max_size_bytes: 1e9,
asset_id: "new-asset",
},
})
}
if (url === "/upload/direct/complete") {
return Promise.resolve({
data: {
storage_key: "uploads/normal/y.mp4",
ingest_job_id: "job-1",
url: "https://oss/y.mp4",
duplicated: false,
asset_id: "new-asset",
},
})
}
throw new Error("unexpected url " + url)
})
// mock XMLHttpRequest:send 之后下一 tick 触发 onload 让 transfer 立即成功
const origOpen = XMLHttpRequest.prototype.open
const origSend = XMLHttpRequest.prototype.send
const origSetReadyState = Object.getOwnPropertyDescriptor(
XMLHttpRequest.prototype,
"readyState",
) as PropertyDescriptor | undefined
const origStatus = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "status")
Object.defineProperty(XMLHttpRequest.prototype, "readyState", {
configurable: true,
writable: true,
value: 4,
})
Object.defineProperty(XMLHttpRequest.prototype, "status", {
configurable: true,
writable: true,
value: 200,
})
XMLHttpRequest.prototype.open = vi.fn() as unknown as typeof origOpen
XMLHttpRequest.prototype.send = vi.fn(function (this: XMLHttpRequest) {
// 下一 tick 触发 onload(模拟 XHR 异步完成)
setTimeout(() => this.onload?.(new ProgressEvent("load")), 0)
}) as unknown as typeof origSend
const file = new File(["x"], "x.mp4", { type: "video/mp4" })
const result = await uploadAssetDirect({ file, library_id: "lib-1" })
expect(result.duplicated).toBeFalsy()
expect(result.asset_id).toBe("new-asset")
const completeCalls = mockPost.mock.calls.filter(
([u]: [string]) => u === "/upload/direct/complete",
)
expect(completeCalls).toHaveLength(1)
// complete 请求必须带上 file_size,否则后端同名兜底会误杀同名新视频
expect(completeCalls[0][1]).toMatchObject({ file_size: file.size })
XMLHttpRequest.prototype.open = origOpen
XMLHttpRequest.prototype.send = origSend
if (origSetReadyState) {
Object.defineProperty(XMLHttpRequest.prototype, "readyState", origSetReadyState)
}
if (origStatus) {
Object.defineProperty(XMLHttpRequest.prototype, "status", origStatus)
}
})
})
describe("getIngestJob", () => {
it("should resolve successfully", async () => {
await expect(getIngestJob("test-jobId")).resolves.not.toThrow()
+130
View File
@@ -0,0 +1,130 @@
/**
* 上传去重/幂等工具单测(Issue #1714)
*/
import { describe, it, expect, vi } from "vitest"
import {
computeFileHash,
findDuplicateInQueue,
HASH_FULL_READ_LIMIT,
HASH_SAMPLE_CHUNK,
makeClientUploadId,
makeFileFingerprint,
} from "@/api/assets/uploadDedup"
const makeFile = (name: string, size = 100, lastModified = 1_700_000_000_000) =>
new File([new Uint8Array(size)], name, { type: "video/mp4", lastModified })
describe("makeFileFingerprint", () => {
it("同一文件(name+size+lastModified 相同)指纹一致", () => {
const a = makeFile("a.mp4", 1000, 12345)
const b = makeFile("a.mp4", 1000, 12345)
expect(makeFileFingerprint(a)).toBe(makeFileFingerprint(b))
})
it("文件名/大小/修改时间任一不同指纹即不同", () => {
const base = makeFile("a.mp4", 1000, 100)
expect(makeFileFingerprint(base)).not.toBe(makeFileFingerprint(makeFile("b.mp4", 1000, 100)))
expect(makeFileFingerprint(base)).not.toBe(makeFileFingerprint(makeFile("a.mp4", 1001, 100)))
expect(makeFileFingerprint(base)).not.toBe(makeFileFingerprint(makeFile("a.mp4", 1000, 101)))
})
})
describe("findDuplicateInQueue", () => {
const queue = [
{ fileKey: "k1", status: "preparing" },
{ fileKey: "k2", status: "uploading" },
{ fileKey: "k3", status: "ingesting" },
{ fileKey: "k4", status: "done" },
{ fileKey: "k5", status: "error" },
]
it("在途状态(preparing/uploading/ingesting/done)命中重复", () => {
expect(findDuplicateInQueue(queue, "k1")?.status).toBe("preparing")
expect(findDuplicateInQueue(queue, "k2")?.status).toBe("uploading")
expect(findDuplicateInQueue(queue, "k3")?.status).toBe("ingesting")
expect(findDuplicateInQueue(queue, "k4")?.status).toBe("done")
})
it("未命中返回 null", () => {
expect(findDuplicateInQueue(queue, "missing")).toBeNull()
})
it("排除 error 状态后,失败项不算重复(允许重新激活)", () => {
expect(findDuplicateInQueue(queue, "k5", ["error"])).toBeNull()
})
it("同时排除 done 后,已完成项也不算重复", () => {
expect(findDuplicateInQueue(queue, "k4", ["error", "done"])).toBeNull()
// 但在途的仍然命中
expect(findDuplicateInQueue(queue, "k1", ["error", "done"])).not.toBeNull()
})
})
describe("makeClientUploadId", () => {
it("生成带前缀且互不相同的幂等 token", () => {
const ids = new Set(Array.from({ length: 20 }, () => makeClientUploadId()))
expect(ids.size).toBe(20)
for (const id of ids) expect(id.startsWith("up_")).toBe(true)
})
})
describe("computeFileHash", () => {
it("相同内容 hash 一致、不同内容 hash 不同", async () => {
const f1 = makeFile("a.mp4", 4096)
const f2 = makeFile("b.mp4", 4096)
// 两个文件都是 0 填充,内容相同 → hash 一致
expect(await computeFileHash(f1)).toBe(await computeFileHash(f2))
const f3 = new File([new Uint8Array(4096).fill(7)], "c.mp4", { type: "video/mp4" })
expect(await computeFileHash(f1)).not.toBe(await computeFileHash(f3))
})
it("返回 64 位十六进制(SHA-256,与后端 file_hash 长度一致)", async () => {
const hash = await computeFileHash(makeFile("a.mp4", 1024))
expect(hash).toMatch(/^[0-9a-f]{64}$/)
})
})
describe("computeFileHash 大文件抽样(>64MB)", () => {
it("抽样路径正常返回 64 位 hex,且大小不同则 hash 不同", async () => {
// mock 一个「声称」300MB 的 File:slice 返回小 buffer 即可,不真分配 300MB
const makeBig = (declaredSize: number, head: number) => {
const f = new File([new Uint8Array([head, 2, 3])], "big.mov", { type: "video/quicktime" })
Object.defineProperty(f, "size", { value: declaredSize, configurable: true })
// slice 仍按真实内容返回小片段(头尾片段内容由底层小 buffer 决定)
return f
}
const h1 = await computeFileHash(makeBig(300 * 1024 * 1024, 1))
const h2 = await computeFileHash(makeBig(301 * 1024 * 1024, 1))
expect(h1).toMatch(/^[0-9a-f]{64}$/)
// 声明大小不同 → 写入的 64 位 size 字段不同 → hash 必须不同(锁定 setBigUint64 路径)
expect(h1).not.toBe(h2)
})
it("≤64MB 走全量读取(slice 一次覆盖整个文件)", async () => {
const f = new File([new Uint8Array(1024).fill(9)], "full.mp4", { type: "video/mp4" })
Object.defineProperty(f, "size", { value: HASH_FULL_READ_LIMIT, configurable: true })
const sliceSpy = vi.spyOn(f, "slice")
await computeFileHash(f)
// 全量路径:唯一一次 slice 为 (0, size)
expect(sliceSpy).toHaveBeenCalledTimes(1)
expect(sliceSpy).toHaveBeenCalledWith(0, HASH_FULL_READ_LIMIT)
sliceSpy.mockRestore()
})
it(">64MB 只读取头尾各 16MB 抽样,绝不整文件读入内存", async () => {
const f = new File([new Uint8Array(1024).fill(9)], "big.mp4", { type: "video/mp4" })
Object.defineProperty(f, "size", { value: HASH_FULL_READ_LIMIT + 1, configurable: true })
const sliceSpy = vi.spyOn(f, "slice")
await computeFileHash(f)
// 抽样路径:两次 slice —— 头部 (0, 16MB) 与尾部 (size-16MB, size)
expect(sliceSpy).toHaveBeenCalledTimes(2)
expect(sliceSpy).toHaveBeenNthCalledWith(1, 0, HASH_SAMPLE_CHUNK)
expect(sliceSpy).toHaveBeenNthCalledWith(
2,
HASH_FULL_READ_LIMIT + 1 - HASH_SAMPLE_CHUNK,
HASH_FULL_READ_LIMIT + 1,
)
sliceSpy.mockRestore()
})
})
+64
View File
@@ -0,0 +1,64 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
describe("wxLogin 工具", () => {
describe("parseWxAuthUrl", () => {
it("从微信授权链接解析出 appid/redirect_uri/state(redirect_uri 解码)", async () => {
const { parseWxAuthUrl } = await import("@/api/auth/wxLogin")
const authUrl =
"https://open.weixin.qq.com/connect/qrconnect?appid=wxb7ae80b48e53980d" +
"&redirect_uri=https%3A%2F%2Fstaging.xiaoxiajianji.com%2Fauth%2Fwechat%2Fcallback" +
"&response_type=code&scope=snsapi_login&state=abc123#wechat_redirect"
const params = parseWxAuthUrl(authUrl)
expect(params).not.toBeNull()
expect(params?.appid).toBe("wxb7ae80b48e53980d")
expect(params?.redirect_uri).toBe("https://staging.xiaoxiajianji.com/auth/wechat/callback")
expect(params?.state).toBe("abc123")
})
it("链接里缺 state 时回退使用 stateFallback", async () => {
const { parseWxAuthUrl } = await import("@/api/auth/wxLogin")
const authUrl =
"https://open.weixin.qq.com/connect/qrconnect?appid=wx123" +
"&redirect_uri=https%3A%2F%2Fexample.com%2Fcb"
const params = parseWxAuthUrl(authUrl, "fallback-state")
expect(params?.state).toBe("fallback-state")
})
it("缺 appid 或 redirect_uri 时返回 null(调用方应回退整页跳转)", async () => {
const { parseWxAuthUrl } = await import("@/api/auth/wxLogin")
expect(parseWxAuthUrl("https://open.weixin.qq.com/connect/qrconnect?appid=wx123")).toBeNull()
expect(parseWxAuthUrl("not a url")).toBeNull()
})
})
describe("loadWxLoginScript", () => {
beforeEach(() => {
vi.resetModules()
document.head.querySelectorAll("script[src*='wxLogin']").forEach((el) => el.remove())
delete (window as unknown as { WxLogin?: unknown }).WxLogin
})
afterEach(() => {
vi.restoreAllMocks()
})
it("window.WxLogin 已存在时直接复用,不重复插入 script", async () => {
const fakeCtor = vi.fn()
;(window as unknown as { WxLogin: unknown }).WxLogin = fakeCtor
const { loadWxLoginScript } = await import("@/api/auth/wxLogin")
const ctor = await loadWxLoginScript()
expect(ctor).toBe(fakeCtor)
expect(document.head.querySelector("script[src*='wxLogin']")).toBeNull()
})
it("脚本 onerror 时 reject(调用方据此回退整页跳转)", async () => {
const { loadWxLoginScript } = await import("@/api/auth/wxLogin")
const promise = loadWxLoginScript()
const script = document.head.querySelector(
"script[src*='wxLogin']",
) as HTMLScriptElement | null
expect(script).not.toBeNull()
script?.dispatchEvent(new Event("error"))
await expect(promise).rejects.toThrow(/加载失败/)
})
})
})
@@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
import { render, screen, fireEvent } from "@testing-library/react"
import { Button } from "antd"
import { useState } from "react"
import ChunkErrorBoundary from "@/components/common/ChunkErrorBoundary"
import * as chunkUtils from "@/utils/chunkLoadError"
// reload 函数 mock 掉(jsdom 不支持真实 window.location.reload)
vi.mock("@/utils/chunkLoadError", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/utils/chunkLoadError")>()
return {
...actual,
reloadForChunkError: vi.fn(),
goHomeRecover: vi.fn(),
}
})
const { reloadForChunkError, goHomeRecover } = vi.mocked(chunkUtils)
/** 渲染时直接抛错的子组件 */
const Boom: React.FC<{ error: Error }> = ({ error }) => {
throw error
}
/** 点击按钮后才抛 chunk 错误的子组件 */
const ChunkBoomButton: React.FC = () => {
const [boom, setBoom] = useState(false)
if (boom) {
throw new TypeError("Failed to fetch dynamically imported module: /assets/x.js")
}
return <Button onClick={() => setBoom(true)}>boom</Button>
}
const renderBoundary = (ui: React.ReactNode) =>
render(<ChunkErrorBoundary>{ui}</ChunkErrorBoundary>)
beforeEach(() => {
sessionStorage.clear()
vi.clearAllMocks()
// error boundary 捕获后 React 会打 error log,静默掉
vi.spyOn(console, "error").mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
sessionStorage.clear()
})
describe("ChunkErrorBoundary", () => {
it("正常渲染 children", () => {
renderBoundary(<div>hello-child</div>)
expect(screen.getByText("hello-child")).toBeInTheDocument()
})
it("首次捕获 chunk 错误 → 自动刷新(reloadForChunkError)并显示自动刷新提示", () => {
renderBoundary(<ChunkBoomButton />)
fireEvent.click(screen.getByText("boom"))
expect(reloadForChunkError).toHaveBeenCalledTimes(1)
expect(screen.getByText(/正在自动刷新/)).toBeInTheDocument()
})
it("已刷新过仍失败 → 不再自动刷新,显示手动兜底按钮", () => {
// 模拟"本会话已经自动刷新过一次"
sessionStorage.setItem("chunk_error_reloaded_at", String(Date.now()))
renderBoundary(
<Boom error={new TypeError("Failed to fetch dynamically imported module: /assets/y.js")} />,
)
expect(reloadForChunkError).not.toHaveBeenCalled()
expect(screen.getByText("系统已更新")).toBeInTheDocument()
// 点击兜底按钮 → goHomeRecover(跳首页,不刷新当前 URL)
fireEvent.click(screen.getByText("刷新并返回首页"))
expect(goHomeRecover).toHaveBeenCalledTimes(1)
})
it("非 chunk 错误 → 显示通用错误页,不触发 chunk 自动刷新", () => {
renderBoundary(<Boom error={new Error("普通业务报错")} />)
expect(reloadForChunkError).not.toHaveBeenCalled()
expect(screen.getByText("页面出现异常")).toBeInTheDocument()
})
})
@@ -0,0 +1,165 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"
import { render, screen, waitFor, cleanup, fireEvent } from "@testing-library/react"
import WechatQrModal from "@/components/auth/WechatQrModal"
const { mockWxLoginCtor, mockGetAuthUrl, mockGetBindUrl, mockGetCurrentUser } = vi.hoisted(() => ({
mockWxLoginCtor: vi.fn(),
mockGetAuthUrl: vi.fn(),
mockGetBindUrl: vi.fn(),
mockGetCurrentUser: vi.fn(),
}))
vi.mock("@/api/auth", () => ({
getWechatAuthUrl: (...args: unknown[]) => mockGetAuthUrl(...args),
getWechatBindUrl: (...args: unknown[]) => mockGetBindUrl(...args),
getCurrentUser: (...args: unknown[]) => mockGetCurrentUser(...args),
normalizeUser: (u: unknown) => u,
}))
vi.mock("@/api/auth/wxLogin", () => ({
loadWxLoginScript: vi.fn(async () => mockWxLoginCtor),
parseWxAuthUrl: vi.fn(() => ({
appid: "wxb7ae80b48e53980d",
redirect_uri: "https://staging.xiaoxiajianji.com/auth/wechat/callback",
state: "state-from-url",
})),
}))
vi.mock("@/api/auth/tokenRefresh", () => ({
scheduleProactiveRefresh: vi.fn(),
cancelProactiveRefresh: vi.fn(),
}))
const { mockSetAuth, mockSetUser } = vi.hoisted(() => ({
mockSetAuth: vi.fn(),
mockSetUser: vi.fn(),
}))
vi.mock("@/store/authStore", () => ({
useAuthStore: (selector: (s: unknown) => unknown) =>
selector({ setAuth: mockSetAuth, setUser: mockSetUser }),
}))
const AUTH_URL =
"https://open.weixin.qq.com/connect/qrconnect?appid=wxb7ae80b48e53980d" +
"&redirect_uri=https%3A%2F%2Fstaging.xiaoxiajianji.com%2Fauth%2Fwechat%2Fcallback&state=st123"
const postMessage = (data: Record<string, unknown>) =>
window.dispatchEvent(new MessageEvent("message", { data, origin: window.location.origin }))
beforeEach(() => {
vi.clearAllMocks()
mockGetAuthUrl.mockResolvedValue({ auth_url: AUTH_URL, state: "st123" })
mockGetBindUrl.mockResolvedValue({ auth_url: AUTH_URL, state: "st123" })
mockGetCurrentUser.mockResolvedValue({ id: 1, display_name: "测试用户" })
localStorage.clear()
})
afterEach(() => cleanup())
describe("WechatQrModal", () => {
it("open=false 时不渲染弹窗内容", () => {
render(<WechatQrModal open={false} scene="login" onClose={vi.fn()} />)
expect(screen.queryByText("微信扫码登录")).toBeNull()
})
it("登录场景:open 后请求授权链接、写入 state、用 WxLogin 渲染二维码", async () => {
render(<WechatQrModal open scene="login" onClose={vi.fn()} />)
await waitFor(() => expect(mockGetAuthUrl).toHaveBeenCalledTimes(1))
expect(localStorage.getItem("wechat_state")).toBe("st123")
await waitFor(() => expect(mockWxLoginCtor).toHaveBeenCalledTimes(1))
expect(mockWxLoginCtor).toHaveBeenCalledWith(
expect.objectContaining({
self_redirect: true,
appid: "wxb7ae80b48e53980d",
scope: "snsapi_login",
state: "state-from-url",
redirect_uri: "https://staging.xiaoxiajianji.com/auth/wechat/callback",
}),
)
expect(screen.getByText(/请使用微信扫描二维码登录/)).toBeTruthy()
})
it("绑定场景:请求 bind/url 且写入 wechat_bind_state", async () => {
render(<WechatQrModal open scene="bind" onClose={vi.fn()} />)
await waitFor(() => expect(mockGetBindUrl).toHaveBeenCalledTimes(1))
expect(mockGetAuthUrl).not.toHaveBeenCalled()
expect(localStorage.getItem("wechat_bind_state")).toBe("st123")
await waitFor(() => expect(mockWxLoginCtor).toHaveBeenCalledTimes(1))
})
it("获取授权链接失败时弹窗内展示错误并提供刷新", async () => {
mockGetAuthUrl.mockRejectedValueOnce({
response: { status: 500, data: { detail: "微信服务内部错误" } },
})
render(<WechatQrModal open scene="login" onClose={vi.fn()} />)
expect(await screen.findByText(/微信服务内部错误/)).toBeTruthy()
expect(screen.getByText("刷新二维码")).toBeTruthy()
// 点刷新后重新请求
fireEvent.click(screen.getByText("刷新二维码"))
await waitFor(() => expect(mockGetAuthUrl).toHaveBeenCalledTimes(2))
})
it("登录成功消息:同步登录态并回调 onLoginSuccess(needOnboarding)", async () => {
const onSuccess = vi.fn()
localStorage.setItem("access_token", "tok-123")
render(<WechatQrModal open scene="login" onClose={vi.fn()} onLoginSuccess={onSuccess} />)
await waitFor(() => expect(mockWxLoginCtor).toHaveBeenCalledTimes(1))
postMessage({
source: "xiaoxia-wechat-qr",
scene: "login",
success: true,
payload: { needOnboarding: true },
})
await waitFor(() => expect(onSuccess).toHaveBeenCalledWith(true))
expect(mockGetCurrentUser).toHaveBeenCalled()
expect(mockSetAuth).toHaveBeenCalledWith(expect.objectContaining({ id: 1 }), "tok-123", null)
})
it("登录失败消息:弹窗内展示回调页透传的真实原因", async () => {
render(<WechatQrModal open scene="login" onClose={vi.fn()} />)
await waitFor(() => expect(mockWxLoginCtor).toHaveBeenCalledTimes(1))
postMessage({
source: "xiaoxia-wechat-qr",
scene: "login",
success: false,
detail: "微信登录失败:state 已过期或已被使用",
})
expect(await screen.findByText(/state 已过期或已被使用/)).toBeTruthy()
})
it("绑定成功消息:刷新用户并回调 onBindSuccess", async () => {
const onBindSuccess = vi.fn()
render(<WechatQrModal open scene="bind" onClose={vi.fn()} onBindSuccess={onBindSuccess} />)
await waitFor(() => expect(mockWxLoginCtor).toHaveBeenCalledTimes(1))
postMessage({ source: "xiaoxia-wechat-qr", scene: "bind", success: true })
await waitFor(() => expect(onBindSuccess).toHaveBeenCalledTimes(1))
expect(mockSetUser).toHaveBeenCalled()
})
it("忽略跨源消息和其他场景的消息", async () => {
const onSuccess = vi.fn()
render(<WechatQrModal open scene="login" onClose={vi.fn()} onLoginSuccess={onSuccess} />)
await waitFor(() => expect(mockWxLoginCtor).toHaveBeenCalledTimes(1))
// 跨源
window.dispatchEvent(
new MessageEvent("message", {
data: { source: "xiaoxia-wechat-qr", scene: "login", success: true },
origin: "https://evil.example.com",
}),
)
// 场景不符(bind 消息发给 login 弹窗)
postMessage({ source: "xiaoxia-wechat-qr", scene: "bind", success: true })
// 无协议标识
postMessage({ foo: "bar" })
await new Promise((r) => setTimeout(r, 50))
expect(onSuccess).not.toHaveBeenCalled()
})
})

Some files were not shown because too many files have changed in this diff Show More