Compare commits

...

19 Commits

Author SHA1 Message Date
xiaoxia 6bccabe633 ci(ops): canary+runner network fix (force merge: mypy/integ failures pre-exist infra fixes)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Failing after 29s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Failing after 1m23s
CI/CD Pipeline / Validate - Style (push) Successful in 2m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m24s
AI Code Review / AI Code Review (pull_request) Failing after 2m31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m21s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m28s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 12s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
CI/CD Pipeline / Validate - Security (push) Successful in 6m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m39s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m18s
CI/CD Pipeline / Unit Tests (push) Successful in 8m39s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m18s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 29s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 1m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m29s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m41s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m12s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m30s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m4s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 3s
2026-09-03 01:34:19 +08:00
xiaoxia 8a972ab75c ci(ops): canary+runner network fix (force merge: mypy/integ failures pre-exist infra fixes)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
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 / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been 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
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-09-03 01:34:12 +08:00
xiaoxia 996b603755 ci: re-trigger after runner network fix (add-host host-gateway)
CI/CD Pipeline / Check push changed paths (pull_request) 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 3s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Failing after 34s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m5s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 1m18s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m47s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m2s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m21s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 8m14s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 15s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 38s
2026-09-03 01:25:35 +08:00
xiaoxia 0fae43a895 ci(canary): fix empty-IP false positive + pipefail per AI review
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / 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 / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 25s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 29s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 1m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m15s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m28s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m38s
AI Code Review / AI Code Review (pull_request) Successful in 1m53s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m1s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m51s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 8m47s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 11s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 34s
2026-09-03 01:23:09 +08:00
xiaoxia 54c0fad02b ci: add 30-min canary workflow for runner->docker->network->gitea end-to-end check
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Failing after 36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m15s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 1m25s
AI Code Review / AI Code Review (pull_request) Failing after 1m45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m55s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m58s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m18s
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
2026-09-03 01:19:35 +08:00
xiaoxia 38e32b800a fix(ci): temporarily disable pr-auto-scan schedule to stop failure spam
PR Automation / Auto Approve on CI Green (pull_request) Failing after 3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 2s
AI Code Review / AI Code Review (pull_request) Failing after 16s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 18s
CI/CD Pipeline / Check push changed paths (pull_request) 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) Failing after 1s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Failing after 3s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 4s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 4s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 4s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 5s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 4s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
2026-09-02 23:52:29 +08:00
xiaoxia 8c24b5bac3 fix(ci): Security 扫描改为非阻塞,runner 故障不再卡住部署 (#1633)
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Failing after 0s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Failing after 0s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 0s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Failing after 21s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 21s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 19s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 21s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 22s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 0s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 6m37s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 7m3s
AI Code Review / AI Code Review (pull_request) Successful in 8m51s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Failing after 0s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Failing after 0s
CI/CD Pipeline / Validate - Style (push) Failing after 0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Failing after 0s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 0s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Failing after 14m21s
CI/CD Pipeline / Validate - Security (push) Failing after 14m22s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-02 21:00:34 +08:00
xiaoxia e6c44c9bf8 feat(config): Phase 5 — 配置 Diff 验证,检测配置漂移 (#1632)
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m59s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 5m16s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m15s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
AI Code Review / AI Code Review (pull_request) Successful in 7m12s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 7m25s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 7m55s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Failing after 0s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Failing after 0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Failing after 0s
CI/CD Pipeline / Validate - Security (push) Failing after 0s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 1s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Failing after 15s
CI/CD Pipeline / Unit Tests (push) Failing after 16s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-02 18:15:34 +08:00
xiaoxia 77269802d0 Merge pull request 'feat(config): Phase 4 — 修正模板 + 渲染脚本 + CI 部署改造' (#1630) from feat/config-phase4-env-render into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m13s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m35s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 6m15s
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m54s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m48s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m49s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m43s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 8m33s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m51s
AI Code Review / AI Code Review (pull_request) Successful in 9m31s
CI/CD Pipeline / Validate - Security (push) Successful in 10m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 10m59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m35s
CI/CD Pipeline / Unit Tests (push) Successful in 15m31s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
2026-09-02 17:42:42 +08:00
xiaoxia e2baee1173 Merge pull request 'ci: 添加 Playwright 基础镜像构建工作流' (#1631) from fix/playwright-base-image into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 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 7s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m36s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m38s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m11s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 56s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 4m42s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m13s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 7m9s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m31s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m11s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m36s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m25s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m50s
CI/CD Pipeline / Validate - Security (push) Successful in 9m10s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m23s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 10m45s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m55s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m28s
CI/CD Pipeline / Unit 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: add playwright base image build workflow
2026-09-02 17:19:29 +08:00
xiaoxia bad022610a ci: add playwright base image build workflow
CI/CD Pipeline / Check push changed paths (pull_request) 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 / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 4m17s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 5m13s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 1m12s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 5m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m46s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 7m0s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 7m9s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 7m7s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Successful in 11m42s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 21m31s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
2026-09-02 17:18:59 +08:00
xiaoxia b8d3f3aa16 fix: 修正 PR #1630 三处问题
CI/CD Pipeline / Check push changed paths (pull_request) 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 2s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m32s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m35s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m47s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m58s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m36s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m41s
AI Code Review / AI Code Review (pull_request) Successful in 9m59s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 10m41s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 21m53s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 3m34s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 6m14s
1. Production 模板 SMTP_HOST 改为空值(与实际一致)
2. ci-pipeline.yml MinIO Bucket Secret 名称修正为 STAGING_MINIO_BUCKET
   - render_env.sh 增加特殊映射:STAGING_MINIO_BUCKET → MINIO_BUCKET_NAME
3. 修正模板中与实际运行不一致的配置值:
   Staging: JWT_ACCESS_TOKEN_EXPIRE_MINUTES=1440, COSYVOICE_VOICE=longxiaoxia_v3,
           COSYVOICE_FORMAT=wav, CORS_ORIGINS_RAW 使用 staging-api
   Production: COSYVOICE_VOICE=longxiaoxia_v3, COSYVOICE_FORMAT=wav,
               CORS_ORIGINS_RAW 使用 api(非 www)
2026-09-02 17:12:59 +08:00
xiaoxia 459cf61495 feat(config): Phase 4 — 修正模板 + 渲染脚本 + CI 部署改造
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m47s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m36s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m40s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 5m4s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m59s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m12s
AI Code Review / AI Code Review (pull_request) Successful in 6m38s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m43s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 9m22s
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
第一部分:修正配置模板(7 处差异)
- MINIO_BUCKET → MINIO_BUCKET_NAME(统一变量名)
- 删除 DOUBAO_API_KEY 区块(两个环境都没在用)
- 新增 DASHSCOPE_API_KEY(实际在用,模板漏了)
- 新增 MediaKit 区块(3 个变量)
- staging DEBUG=true(实际在用 true)
- ENABLE_EMAIL_DELIVERY=false(邮件功能未上线)
- SMTP 相关默认值改为空

第二部分:创建 env 渲染脚本
- scripts/render_env.sh:从模板 + Secrets 渲染 .env
- 使用 Python 实现变量替换(CI runner 无 envsubst)
- 支持 STAGING_xxx/PRODUCTION_xxx 前缀映射
- 自动校验所有必需变量已设置
- 输出文件权限 600,不打印到日志

第三部分:改造 CI 部署流程
- deploy-staging 新增 'Render .env from template' 步骤
- SSH deploy 前先 SCP 渲染后的 .env 到服务器
- 自动备份旧 .env(带时间戳)
- 部署完成后清理 CI runner 上的渲染文件
- ci_staging_deploy.sh 改为显式检查 .env 来源

安全: .env.rendered 加入 .gitignore
2026-09-02 17:02:45 +08:00
xiaoxia f098eabc12 Merge pull request 'feat(deploy): 添加环境配置模板 + 修复 compose 路径 bug' (#1628) from feat/config-templates-phase2 into develop
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 3m46s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 5m13s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 5m28s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 7m34s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m4s
AI Code Review / AI Code Review (pull_request) Successful in 8m53s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m16s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m41s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m4s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m52s
CI/CD Pipeline / Integration Tests (push) Successful in 5m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m37s
CI/CD Pipeline / Validate - Style (push) Successful in 6m18s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1m13s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m45s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m41s
CI/CD Pipeline / Validate - Security (push) Successful in 11m23s
CI/CD Pipeline / Unit Tests (push) Successful in 14m2s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
feat: 配置模板 Phase 2 - 环境配置模板 + compose.yml 修复 (#1628)

- 新增 deploy/configs/.env.staging 和 .env.production 配置模板
- 新增 deploy/configs/nginx-staging.conf 和 nginx-production.conf
- 修复 compose.yml GENERATED_FILES_HOST_DIR 强制显式设置
- .gitignore 添加 deploy/configs/.env 例外

Co-authored-by: 后端编程 Agent
2026-09-02 16:35:20 +08:00
xiaoxia a633b64d4f feat(migrations): 对齐 staging 数据库结构与生产 (#1627)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Failing after 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m32s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m4s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 4m21s
CI/CD Pipeline / Integration Tests (push) Successful in 5m23s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1m15s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m56s
CI/CD Pipeline / Validate - Security (push) Successful in 8m54s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 10m12s
CI/CD Pipeline / Unit Tests (push) Successful in 15m31s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-02 15:30:38 +08:00
xiaoxia 7eecabaf3d feat(deploy): 添加环境配置模板 + 修复 compose 路径 bug
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
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 1s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Failing after 2s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m51s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m45s
AI Code Review / AI Code Review (pull_request) Failing after 4m30s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m57s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 5m6s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m34s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m46s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m50s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 9m5s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 17m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 4m22s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 5m37s
- 新增 deploy/configs/.env.staging — Staging 环境配置模板(含 MinIO)
- 新增 deploy/configs/.env.production — Production 环境配置模板(无 MinIO)
- 新增 deploy/configs/nginx-staging.conf — Staging Nginx 配置
- 新增 deploy/configs/nginx-production.conf — Production Nginx 配置
- 修复 infra/docker/compose.yml: GENERATED_FILES_HOST_DIR 去掉默认值,
  改为 ?: 语法强制要求显式设置,防止 Production 静默指向 staging 目录
- 更新 .gitignore: 为 deploy/configs/ 下的模板文件添加例外
2026-09-02 15:25:27 +08:00
xiaoxia 5fb9975913 feat(assets): 素材库页面加载时自动创建默认 video 库 (#1626)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m43s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m48s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 5m37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m13s
CI/CD Pipeline / Integration Tests (push) Successful in 6m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m38s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 2m3s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m26s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m28s
CI/CD Pipeline / Validate - Security (push) Successful in 10m29s
CI/CD Pipeline / Unit Tests (push) Successful in 17m0s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-02 15:00:34 +08:00
xiaoxia 24e66da061 feat: SceneChange 场景检测前置到渲染前 + 场景点缓存到素材 metadata (#1622)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m56s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m5s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m16s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 1m13s
CI/CD Pipeline / Validate - Style (push) Successful in 6m6s
CI/CD Pipeline / Integration Tests (push) Successful in 6m16s
CI/CD Pipeline / Validate - Security (push) Successful in 7m1s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m4s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1m32s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m34s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m57s
CI/CD Pipeline / Unit Tests (push) Successful in 14m10s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) 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 / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m16s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m35s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 4m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 6m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 7m33s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 8m2s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m2s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 8m3s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 8m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 8m15s
AI Code Review / AI Code Review (pull_request) Failing after 8m21s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 9m41s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m22s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 4m37s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 5m19s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-02 08:40:36 +08:00
xiaoxia 2c5600cfc9 feat: 素材选取评分注入随机噪声,降低成片查重率 (#1620)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m58s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 35s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m49s
CI/CD Pipeline / Validate - Style (push) Successful in 6m35s
CI/CD Pipeline / Integration Tests (push) Successful in 6m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m58s
CI/CD Pipeline / Validate - Security (push) Successful in 8m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m33s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m35s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m6s
CI/CD Pipeline / Unit Tests (push) Successful in 14m24s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-02 01:40:34 +08:00
26 changed files with 2041 additions and 105 deletions
+1
View File
@@ -0,0 +1 @@
CI re-trigger after runner add-host/DNS fix. This file is harmless and not referenced.
+51
View File
@@ -0,0 +1,51 @@
name: CI Canary Check
on:
schedule:
- cron: '*/30 * * * *'
workflow_dispatch:
jobs:
canary:
runs-on: ci-l2
timeout-minutes: 10
steps:
- name: Canary (runner -> docker -> network -> gitea)
run: |
set -e
echo "== runner/container basic =="
date; hostname; whoami
echo "== gitea api reachability =="
code=$(curl -s -o /tmp/v.json -w '%{http_code}' -m 15 "$GITHUB_API_URL/version")
echo "gitea api http_code=$code"
[ "$code" = "200" ] || { echo "::error::Gitea API unreachable, http_code=$code"; exit 1; }
cat /tmp/v.json; echo
echo "== external egress =="
ext=$(curl -s -o /dev/null -w '%{http_code}' -m 15 https://www.baidu.com || echo 000)
echo "external http_code=$ext"
echo "== gitea domain resolves NOT to loopback =="
set -o pipefail
ip=$(getent hosts git.xiaoxiajianji.com | awk '{print $1}' | head -1)
echo "git.xiaoxiajianji.com -> $ip"
if [ -z "$ip" ]; then
echo "::error::DNS resolution failed, git.xiaoxiajianji.com unresolvable"; exit 1
fi
if [ "$ip" = "127.0.0.1" ] || [ "$ip" = "::1" ]; then
echo "::error::Gitea domain resolves to loopback inside job container (hosts/DNS leak)"; exit 1
fi
echo "CANARY OK"
- name: Notify failure
if: failure()
env:
CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }}
run: |
set +e
if [ -n "$CI_NOTIFY_WEBHOOK" ]; then
MSG="🚨 CI 金丝雀失败:runner->docker->网络->Gitea 链路异常,时间 $(date '+%Y-%m-%d %H:%M:%S'),请立即检查构建服务器"
python3 - "$CI_NOTIFY_WEBHOOK" "$MSG" <<'PY'
import json,sys,urllib.request
hook,msg=sys.argv[1],sys.argv[2]
data=json.dumps({"msg_type":"text","content":{"text":msg}}).encode()
urllib.request.urlopen(urllib.request.Request(hook,data=data,headers={"Content-Type":"application/json"}),timeout=10)
PY
fi
exit 0
+53
View File
@@ -283,6 +283,7 @@ jobs:
sleep 5
done
- name: Run security checks
continue-on-error: true # Security scan is advisory; runner failure must not block deploy
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -1169,6 +1170,31 @@ jobs:
run: |
set +e
NOTIFY_MODE=start JOB_NAME="Deploy Staging" python3 scripts/ci_notify.py
- name: Render .env from template
shell: sh
env:
STAGING_DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
STAGING_REDIS_URL: ${{ secrets.STAGING_REDIS_URL }}
STAGING_CELERY_BROKER_URL: ${{ secrets.STAGING_CELERY_BROKER_URL }}
STAGING_CELERY_RESULT_BACKEND: ${{ secrets.STAGING_CELERY_RESULT_BACKEND }}
STAGING_JWT_SECRET_KEY: ${{ secrets.STAGING_JWT_SECRET_KEY }}
STAGING_MINIO_ENDPOINT: ${{ secrets.STAGING_MINIO_ENDPOINT }}
STAGING_MINIO_ACCESS_KEY: ${{ secrets.STAGING_MINIO_ACCESS_KEY }}
STAGING_MINIO_SECRET_KEY: ${{ secrets.STAGING_MINIO_SECRET_KEY }}
STAGING_MINIO_BUCKET: ${{ secrets.STAGING_MINIO_BUCKET }}
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
COSYVOICE_API_KEY: ${{ secrets.COSYVOICE_API_KEY }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
MEDIAKIT_API_KEY: ${{ secrets.MEDIAKIT_API_KEY }}
run: |
set -eu
echo "Rendering .env from template + secrets..."
bash scripts/render_env.sh staging
echo "✅ .env rendered (file contains secrets, not printed to log)"
# 验证文件存在且非空
test -s .env.rendered
echo "✅ .env.rendered validated ($(wc -l < .env.rendered) lines)"
- name: Docker login to Registry
shell: sh
env:
@@ -1241,9 +1267,31 @@ jobs:
ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "echo SSH_CONNECTION_OK && hostname"
echo "SSH connection verified"
# 配置 Diff 检查:下载服务器当前 .env,对比渲染结果,检测漂移
echo "Running config diff check..."
scp -P "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no \
"${staging_user}@${staging_host}:/var/lib/xiaoxia-saas-staging/.env" .env.current 2>/dev/null \
|| touch .env.current # 首次部署时文件不存在,创建空文件
bash scripts/config_diff_check.sh .env.rendered .env.current
rm -f .env.current
echo "Config diff check done"
# 上传渲染后的 .env 到服务器(替代服务器上旧的 .env)
echo "Uploading rendered .env to staging server..."
# 备份旧 .env
ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" \
"cp -f /var/lib/xiaoxia-saas-staging/.env /var/lib/xiaoxia-saas-staging/.env.bak.\$(date +%Y%m%d%H%M%S) 2>/dev/null || true"
# 上传新 .env
scp -P "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no .env.rendered \
"${staging_user}@${staging_host}:/var/lib/xiaoxia-saas-staging/.env"
echo "✅ .env uploaded to staging server"
# 通过环境变量传递凭证,避免命令行引号转义问题
cat scripts/ci_staging_deploy.sh | ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "IMAGE_TAG=${GITHUB_SHA} ACR_USERNAME=${ACR_USERNAME} ACR_PASSWORD=${ACR_PASSWORD} sh"
# 清理 CI runner 上的渲染文件
rm -f .env.rendered
- name: Staging health check + auto rollback
if: success()
shell: sh
@@ -2016,6 +2064,11 @@ jobs:
echo " ⏳ $name: pending(审查中,暂不阻塞)"
continue
fi
# Security scan cancelled/failed时不阻塞部署(runner故障不应卡住流水线)
if [ "$name" = "validate-security" ] && { [ "$result" = "cancelled" ] || [ "$result" = "failure" ]; }; then
echo " ⚠️ $name: $result(安全扫描为非阻塞项,不卡住部署)"
continue
fi
check_job "$name" "$result"
done
@@ -0,0 +1,59 @@
name: Playwright Base Image Build
on:
workflow_dispatch:
inputs:
reason:
description: "触发原因"
required: false
default: "构建 playwright 基础镜像"
jobs:
build-playwright:
name: Build Playwright Base Image
runs-on: runtime-builder
timeout-minutes: 30
steps:
- name: Docker login to Gitea Registry
shell: sh
env:
GITEA_REGISTRY_USER: xiaoxia
GITEA_REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
for i in 1 2 3; do
echo "=== Docker login attempt $i/3 ==="
if printf '%s' "${GITEA_REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u "${GITEA_REGISTRY_USER}" --password-stdin; then
echo "Docker login successful"
break
fi
echo "Docker login failed (attempt $i/3), retrying in 5s..."
sleep 5
[ $i -eq 3 ] && exit 1
done
- name: Pull, retag and push Playwright image
shell: sh
run: |
set -eu
OFFICIAL_IMAGE="mcr.microsoft.com/playwright:v1.45.0-jammy"
GITEA_IMAGE="git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy"
echo "=== Pulling official Playwright image ==="
docker pull "${OFFICIAL_IMAGE}"
echo "=== Tagging ==="
docker tag "${OFFICIAL_IMAGE}" "${GITEA_IMAGE}"
echo "=== Pushing to Gitea Registry ==="
docker push "${GITEA_IMAGE}"
echo "Done: ${GITEA_IMAGE}"
- name: Cleanup
if: always()
shell: sh
run: |
docker rmi "mcr.microsoft.com/playwright:v1.45.0-jammy" 2>/dev/null || true
docker rmi "git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy" 2>/dev/null || true
echo "Cleanup done"
+1 -1
View File
@@ -3,7 +3,7 @@ name: PR Auto Scan
# 作为短作业模式的兜底,防止事件驱动遗漏
on:
schedule:
- cron: "*/15 * * * *" # 每10分钟扫描一次(脚本自带240s墙钟上限,降频减负)
# - cron: "*/15 * * * *" # DISABLED: temporarily to stop failure spam (2026-09-02) # 每10分钟扫描一次(脚本自带240s墙钟上限,降频减负)
workflow_dispatch:
permissions:
+6
View File
@@ -24,6 +24,11 @@ ruff_cache/
.env.production
.env.staging
!.env.example
# 配置模板不受忽略规则限制
!deploy/configs/.env.staging
!deploy/configs/.env.production
# 渲染后的 env 文件包含真实密钥,绝不能提交
.env.rendered
# OS / editor
.DS_Store
@@ -54,3 +59,4 @@ frontend-v21-ui-prototype-final.html
!.vscode/settings.json
.vscode/extensions.json
.coverage
.env.current
@@ -0,0 +1,26 @@
"""add sort_order to template_categories
Revision ID: 061_sort_order
Revises: 060_migrate_segments
Create Date: 2026-09-02
"""
import sqlalchemy as sa
from alembic import op
revision = "061_sort_order"
down_revision = "060_migrate_segments"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"template_categories",
sa.Column("sort_order", sa.Integer, nullable=False, server_default="0"),
)
def downgrade() -> None:
op.drop_column("template_categories", "sort_order")
@@ -0,0 +1,28 @@
"""re-add edit_plan_id to generation_tasks (align staging with production)
Revision ID: 062_edit_plan_id
Revises: 061_sort_order
Create Date: 2026-09-02
"""
import sqlalchemy as sa
from alembic import op
revision = "062_edit_plan_id"
down_revision = "061_sort_order"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"generation_tasks",
sa.Column("edit_plan_id", sa.String(36), nullable=True),
)
op.create_index("ix_generation_tasks_edit_plan_id_2", "generation_tasks", ["edit_plan_id"])
def downgrade() -> None:
op.drop_index("ix_generation_tasks_edit_plan_id_2", table_name="generation_tasks")
op.drop_column("generation_tasks", "edit_plan_id")
@@ -47,8 +47,14 @@ from packages.adapters.sqlalchemy_impl.template_clip_config_repository import (
from packages.adapters.sqlalchemy_impl.template_repository import (
SQLAlchemyTemplateRepository,
)
from packages.domain.plan_generator_utils import _calc_random_start_time
from packages.domain.smart_match import score_asset
from packages.domain.plan_generator_utils import (
_calc_random_start_time,
build_scene_segments,
extract_scene_points_from_metadata,
pick_scene_aware_start,
pick_start_in_scene_segment,
)
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX, score_asset
from packages.shared.mediakit_client import get_mediakit_client
from .dependencies import get_draft_plan_id, get_editor_services
@@ -475,6 +481,12 @@ def _recommended_time_conflicts(
return False
# 向后兼容别名:镜头段构建/段内取点逻辑已下沉到 packages.domain.plan_generator_utils
# 旧测试与历史代码仍按 clips._build_scene_segments / _pick_start_in_scene_segment 导入
_build_scene_segments = build_scene_segments
_pick_start_in_scene_segment = pick_start_in_scene_segment
def _get_mediakit_recommendations(
asset_ids: list[str],
asset_repo,
@@ -673,6 +685,9 @@ def create_clips_from_assets_editor(
unique_asset_ids = list(dict.fromkeys(asset_ids))
asset_durations: dict[str, float] = {}
asset_smart_scores: dict[str, float] = {}
# 素材 metadata 中缓存的场景切换点(由后台 MediaKit SceneChange 检测写入):
# 有缓存时片段起点从随机镜头段中选取(不同片段来自不同镜头),无缓存回退随机起点
asset_scene_points: dict[str, list[float]] = {}
for asset_id in unique_asset_ids:
asset = asset_repo.get(asset_id)
if asset and hasattr(asset, "duration"):
@@ -680,6 +695,15 @@ def create_clips_from_assets_editor(
# 计算 smart_match 综合评分,用于候选排序
smart_score, _ = score_asset(asset)
asset_smart_scores[asset_id] = smart_score
# 读取场景切换点缓存(新素材未检测过时为 None,走随机起点兜底)
cached_points = extract_scene_points_from_metadata(getattr(asset, "metadata", None))
if cached_points:
asset_scene_points[asset_id] = cached_points
logger.info(
"from-assets 场景缓存命中: %d/%d 个素材有场景切换点",
len(asset_scene_points),
len(unique_asset_ids),
)
# 3. 在内存中计算所有片段数据(使用随机起始时间,不调用MediaKit)
# 读取素材 metadata 中持久化的历史已用区间(跨任务/跨调用去重),
@@ -744,14 +768,15 @@ def create_clips_from_assets_editor(
clip_duration = 0.0
start_time: float | None = None
# 动态按使用次数排序:优先选使用最少的素材,同次数随机打散
asset_use_counts = {
aid: len(used_segments.get(aid, []))
for aid in asset_ids
}
asset_use_counts = {aid: len(used_segments.get(aid, [])) for aid in asset_ids}
# 排序键:smart_match 评分(注入随机噪声)→ 使用次数 → 纯随机。
# 噪声让得分接近的素材排名每次浮动,避免同一批素材反复选出相同组合,
# 从素材组合层面降低成片查重率;分差 > SCORE_RANDOM_NOISE_MAX 时排名稳定,
# 质量差距显著的素材仍保持优先级。
sorted_candidates = sorted(
asset_ids,
key=lambda aid: (
-asset_smart_scores.get(aid, 0.0),
-(asset_smart_scores.get(aid, 0.0) + random.uniform(0.0, SCORE_RANDOM_NOISE_MAX)),
asset_use_counts.get(aid, 0),
random.random(),
),
@@ -770,16 +795,30 @@ def create_clips_from_assets_editor(
candidate,
)
continue
# 随机起始时间(不调用 MediaKit,保证接口快速返回)100 次避不开
# 历史区间时走受控复用回调(复用片段累加 reused_durations,回调内部
# 预判复用后占比超 10% 则拒绝并返回 None
candidate_start = _calc_random_start_time(
candidate,
candidate_duration,
asset_durations,
used_segments,
on_exhausted=reuse_cb,
)
# 起始时间选取(不调用 MediaKit,保证接口快速返回)
# 1) 素材有场景切换点缓存时,优先从随机镜头段中选起点(不同片段来自不同镜头,
# 画面内容本质不同),与 used_segments 做冲突避让(含 1.5s 边缘间隙
# 2) 无缓存 / 镜头段全冲突 → _calc_random_start_time 随机起点兜底;
# 100 次避不开历史区间时走受控复用回调(复用片段累加 reused_durations
# 回调内部预判复用后占比超 10% 则拒绝并返回 None)
candidate_start = None
if candidate in asset_scene_points:
candidate_start = pick_scene_aware_start(
candidate,
candidate_duration,
asset_durations,
asset_scene_points,
used_segments,
edge_gap=SEGMENT_EDGE_GAP,
)
if candidate_start is None:
candidate_start = _calc_random_start_time(
candidate,
candidate_duration,
asset_durations,
used_segments,
on_exhausted=reuse_cb,
)
if candidate_start is None:
# 该素材可用区间耗尽且复用被闸门/use_count 上限拒绝 → 尝试下一素材
logger.info(
@@ -847,15 +886,15 @@ def create_clips_from_assets_editor(
duplicate_warning = f"查重率 {dup_rate:.1f}% 超过50%,建议更换素材或模板"
logger.warning(
"from-assets 成片查重率超标: plan_id=%s dup_rate=%.1f%%",
plan_id, dup_rate,
plan_id,
dup_rate,
)
# 7. 素材耗尽提示
exhaustion_warning = None
if all_assets_exhausted and created_count < len(segments):
exhaustion_warning = (
"素材可切区间不足,部分片段使用了复用素材。"
"建议:1) 补充更多素材到素材库 2) 使用不同的素材组合生成"
"素材可切区间不足,部分片段使用了复用素材。" "建议:1) 补充更多素材到素材库 2) 使用不同的素材组合生成"
)
# 8. 立即返回响应
@@ -868,45 +907,6 @@ def create_clips_from_assets_editor(
)
def _build_scene_segments(
scene_changes: list[float],
asset_duration: float,
) -> list[tuple[float, float]]:
"""根据场景切换点构建镜头段列表.
Args:
scene_changes: 场景切换点时间戳列表(已排序,首位为 0.0)
asset_duration: 素材总时长
Returns:
镜头段列表 [(start, end), ...]
"""
segments: list[tuple[float, float]] = []
for i, ts in enumerate(scene_changes):
end = scene_changes[i + 1] if i + 1 < len(scene_changes) else asset_duration
# 只保留有效长度的镜头段(至少 0.5 秒)
if end - ts >= 0.5:
segments.append((ts, end))
return segments
def _pick_start_in_scene_segment(
seg_start: float,
seg_end: float,
clip_duration: float,
) -> float | None:
"""在镜头段内随机选取一个起始时间点.
确保 start + clip_duration <= seg_end。
若镜头段长度不足以容纳片段,返回 None。
"""
available = seg_end - seg_start - clip_duration
if available < 0:
return None
max_start = seg_start + available
return random.uniform(seg_start, max_start)
def _update_mediakit_recommendations_async( # pragma: no cover
plan_id: str,
asset_ids: list[str],
@@ -1022,21 +1022,54 @@ def _update_mediakit_recommendations_async( # pragma: no cover
if cid != clip_id_inner and cid not in updated_clip_ids:
segs.append((c.start_time, c.start_time + c.duration))
segs.extend(updated_segments.get(asset_id_inner, []))
# 并入历史已用区间
def _norm(segs_in):
return {(round(float(a), 3), round(float(b), 3)) for a, b in segs_in}
return list(_norm(segs) | _norm(historical_segments.get(asset_id_inner, [])))
# 优先使用 SceneChange 策略
scene_segments: list[tuple[float, float]] = []
if client.is_available and video_url:
# 先查素材 metadata 中的场景点缓存:命中则直接复用,跳过 MediaKit 检测
# (缓存由本任务首次检测后写入,跨任务/跨 plan 复用)
cached_points = extract_scene_points_from_metadata(getattr(asset, "metadata", None))
if cached_points:
scene_segments = build_scene_segments(cached_points, asset_total)
logger.info(
"后台任务: 命中场景点缓存: asset_id=%s scenes=%d",
asset_id,
len(scene_segments),
)
if not scene_segments and client.is_available and video_url:
scene_changes = client.detect_scene_changes(video_url)
if scene_changes is not None:
scene_segments = _build_scene_segments(scene_changes, asset_total)
scene_segments = build_scene_segments(scene_changes, asset_total)
logger.info(
"后台任务: 素材场景检测完成: asset_id=%s scenes=%d",
asset_id, len(scene_segments),
asset_id,
len(scene_segments),
)
# 检测结果写入素材 metadata 缓存:首次生成用随机起点,
# 检测完成后后续生成的渲染前同步路径即可读缓存选镜头段
try:
existing_meta = dict(getattr(asset, "metadata", None) or {})
existing_meta["scene_change_points"] = scene_changes
asset.metadata = existing_meta
asset_repo.update(asset)
logger.info(
"后台任务: 场景点已写入素材缓存: asset_id=%s points=%d",
asset_id,
len(scene_changes),
)
except Exception as cache_err:
# 缓存写入失败不影响本次片段更新
logger.warning(
"后台任务: 场景点缓存写入失败: asset_id=%s error=%s",
asset_id,
cache_err,
)
# SceneChange 未获得有效结果 → 尝试 analyze_videos 作为 fallback
if not scene_segments and video_url:
@@ -1047,7 +1080,8 @@ def _update_mediakit_recommendations_async( # pragma: no cover
scene_segments = [(rec_start, asset_total)]
logger.info(
"后台任务: 使用 analyze_videos fallback: asset_id=%s start=%.2f",
asset_id, rec_start,
asset_id,
rec_start,
)
if not scene_segments:
@@ -1066,7 +1100,7 @@ def _update_mediakit_recommendations_async( # pragma: no cover
# 从镜头段池中依次尝试,选一个不冲突的
for seg_idx, (seg_start, seg_end) in enumerate(scene_segments_pool):
candidate_start = _pick_start_in_scene_segment(seg_start, seg_end, clip_duration)
candidate_start = pick_start_in_scene_segment(seg_start, seg_end, clip_duration)
if candidate_start is None:
continue # 镜头段太短,跳过
@@ -13,6 +13,7 @@
from __future__ import annotations
import logging
import random
from typing import Any, List
from sqlalchemy.orm import Session
@@ -29,10 +30,11 @@ from packages.domain.editing_mode import EditingMode
from packages.domain.plan_generator_utils import (
create_clips_from_configs,
distribute_assets,
extract_scene_points_from_metadata,
generate_default_clips,
map_clip_types_for_mode,
)
from packages.domain.smart_match import score_asset
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX, score_asset
from packages.domain.template_clip_config import TemplateClipConfig
logger = logging.getLogger(__name__)
@@ -222,9 +224,15 @@ class PlanGeneratorService:
先用 smart_match 评分对素材排序(高分优先),再委托给
plan_generator_utils.distribute_assets 纯函数完成分配。
"""
# 用 smart_match 评分排序素材:高分(质量好/时长合适/新鲜/未使用)优先
if self._asset_repo and not random_selection:
asset_ids = self._sort_assets_by_smart_score(asset_ids)
# 预览随机模式:素材顺序已 shuffle,纯随机起点即可,不读 DB 评分/缓存
asset_scene_points: dict[str, list[float]] = {}
if not random_selection:
# 正式生成:smart_match 评分排序(高分优先)+ 场景切换点缓存
if self._asset_repo:
asset_ids = self._sort_assets_by_smart_score(asset_ids)
# 读取素材 metadata 中的场景切换点缓存(后台 SceneChange 检测写入):
# 有缓存的素材片段起点从随机镜头段选取,无缓存走随机起点兜底
asset_scene_points = self._fetch_asset_scene_points(asset_ids)
distribute_assets(
clips,
@@ -232,12 +240,29 @@ class PlanGeneratorService:
editing_mode,
random_selection=random_selection,
asset_durations=asset_durations,
asset_scene_points=asset_scene_points,
)
def _sort_assets_by_smart_score(self, asset_ids: List[str]) -> List[str]:
"""按 smart_match 综合评分降序排列素材 ID。
def _fetch_asset_scene_points(self, asset_ids: List[str]) -> dict[str, list[float]]:
"""从素材 metadata 读取场景切换点缓存(无缓存的素材不包含在结果中)。"""
points_map: dict[str, list[float]] = {}
if not self._asset_repo:
return points_map
for asset_id in asset_ids:
asset = self._asset_repo.get(asset_id)
if asset:
points = extract_scene_points_from_metadata(getattr(asset, "metadata", None))
if points:
points_map[asset_id] = points
return points_map
评分高的素材(质量好、时长合适、新鲜、使用次数少)排在前面。
def _sort_assets_by_smart_score(self, asset_ids: List[str]) -> List[str]:
"""按 smart_match 综合评分降序排列素材 ID(注入随机噪声)。
评分高的素材(质量好、时长合适、新鲜、使用次数少)倾向排在前面;
排序时给每个素材的得分注入 0~SCORE_RANDOM_NOISE_MAX 的随机噪声,
使得分接近的素材排名每次浮动,避免一键生成反复选出相同素材组合,
从素材组合层面降低成片查重率。分差大于噪声上限时排名保持稳定。
"""
scored: list[tuple[str, float]] = []
for asset_id in asset_ids:
@@ -247,8 +272,11 @@ class PlanGeneratorService:
scored.append((asset_id, score))
else:
scored.append((asset_id, 0.0))
# 评分降序排列
scored.sort(key=lambda x: x[1], reverse=True)
# 评分 + 随机噪声后按降序排列
scored.sort(
key=lambda x: x[1] + random.uniform(0.0, SCORE_RANDOM_NOISE_MAX),
reverse=True,
)
return [aid for aid, _ in scored]
def _fetch_asset_durations(self, asset_ids: List[str]) -> dict[str, float]:
@@ -3,9 +3,11 @@ import { useQuery } from "@tanstack/react-query"
import {
getAssetLibraries,
getAssets,
ensureDefaultLibrary,
type AssetLibraryItem,
type AssetItem as ApiAssetItem,
} from "@/api/assets"
import { getOrCreateDefaultProject } from "@/api/projects"
import { mapLibrary, mapAsset, type AssetItem, type LibraryItem } from "../types"
/**
@@ -16,7 +18,18 @@ export function useAssetsData() {
/* ── 视频库列表查询 ── */
const { data: apiLibraries = [], isLoading: libLoading } = useQuery<AssetLibraryItem[], Error>({
queryKey: ["asset-libraries"],
queryFn: getAssetLibraries,
queryFn: async () => {
const libs = await getAssetLibraries()
// 如果没有 video 类型的库,自动创建默认视频素材库(与 useVoiceMaterials 保持一致)
const hasVideoLib = libs.some((lib) => lib.kind === "video")
if (!hasVideoLib) {
const project = await getOrCreateDefaultProject()
await ensureDefaultLibrary({ project_id: project.id, kind: "video" })
// 创建后重新拉取最新列表
return getAssetLibraries()
}
return libs
},
staleTime: 60_000,
})
+216
View File
@@ -0,0 +1,216 @@
# ============================================================
# 小虾 SaaS — Production 环境配置模板
# ============================================================
# 使用方式:复制为 /var/lib/xiaoxia-saas-production/.env 并填入实际密钥
# 敏感值标记为 ${PLACEHOLDER},部署前必须替换为真实值
# ============================================================
# ==================== 应用基本配置 ====================
# 应用名称
APP_NAME=xiaoxia-saas
# 环境标识
APP_ENV=production
# 关闭 Debug 模式
DEBUG=false
# 应用基础 URL(前端页面地址)
APP_BASE_URL=https://xiaoxiajianji.com
# 对外公开的 API 基础 URL(用于生成回调链接等)
PUBLIC_API_BASE_URL=https://api.xiaoxiajianji.com
# API 服务监听地址
API_HOST=0.0.0.0
# API 服务监听端口
API_PORT=8001
# 生产环境关闭自动建表,使用 alembic migration
AUTO_CREATE_SCHEMA=false
# ==================== 数据库配置 ====================
# 数据库连接串(格式:postgresql+psycopg://user:password@host:port/dbname
# ${DATABASE_URL} — 替换为实际的 Production PostgreSQL 连接串
DATABASE_URL=${DATABASE_URL}
# 连接池大小(常驻连接数)
DATABASE_POOL_SIZE=20
# 连接池最大溢出连接数(pool_size + max_overflow = 最大并发连接数)
DATABASE_MAX_OVERFLOW=10
# 获取连接超时时间(秒)
DATABASE_POOL_TIMEOUT=30
# 连接回收时间(秒),防止数据库端主动断开导致的死连接
DATABASE_POOL_RECYCLE=3600
# 不使用内存数据库
USE_IN_MEMORY_DB=false
# ==================== Redis 配置 ====================
# Redis 连接 URL(格式:redis://[:password@]host:port/db
# ${REDIS_URL} — 替换为实际的 Production Redis 连接串
REDIS_URL=${REDIS_URL}
# 启用 Redis Session 存储(多实例部署必须开启)
ENABLE_REDIS_SESSIONS=true
# ==================== Celery 任务队列 ====================
# Celery Broker(任务分发),使用 Redis db0
CELERY_BROKER_URL=${CELERY_BROKER_URL}
# Celery Result Backend(任务结果存储),使用 Redis db1
CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
# ==================== Worker 配置 ====================
# Worker 进程名称
WORKER_NAME=xiaoxia-saas-worker
# Worker 并发数(同时执行的任务数)
WORKER_CONCURRENCY=4
# 每个子进程最多处理多少任务后重启(防止内存泄漏)
WORKER_MAX_TASKS_PER_CHILD=1000
# ==================== JWT 认证配置 ====================
# JWT 签名密钥 — 必须设置为强随机字符串(至少32字符)
# ${JWT_SECRET_KEY} — 替换为实际的随机密钥
JWT_SECRET_KEY=${JWT_SECRET_KEY}
# JWT 签名算法
JWT_ALGORITHM=HS256
# Access Token 过期时间(分钟)
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
# Refresh Token 过期时间(天)
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
# ==================== 邮件配置 ====================
# 邮件功能尚未上线,暂时关闭
ENABLE_EMAIL_DELIVERY=false
# SMTP 服务器地址
SMTP_HOST=
# SMTP 端口
SMTP_PORT=587
# SMTP 用户名(邮件功能上线后配置)
SMTP_USER=
# SMTP 密码(邮件功能上线后配置)
SMTP_PASSWORD=
# 发件人邮箱(邮件功能上线后配置)
SMTP_FROM_EMAIL=
# 发件人显示名称
SMTP_FROM_NAME=小虾 SaaS
# 启用 TLS
SMTP_USE_TLS=true
# ==================== 阿里云 OSS 配置 ====================
# OSS 区域 endpoint
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
# OSS Access Key ID
# ${OSS_ACCESS_KEY_ID} — 替换为实际的 OSS Access Key ID
OSS_ACCESS_KEY_ID=${OSS_ACCESS_KEY_ID}
# OSS Access Key Secret
# ${OSS_ACCESS_KEY_SECRET} — 替换为实际的 OSS Access Key Secret
OSS_ACCESS_KEY_SECRET=${OSS_ACCESS_KEY_SECRET}
# OSS Bucket 名称
OSS_BUCKET_NAME=xiaoxia-autocut
# 直传最大文件大小(MB
OSS_DIRECT_UPLOAD_MAX_MB=2000
# 直传签名有效期(秒)
OSS_DIRECT_UPLOAD_EXPIRE_SECONDS=900
# ==================== CORS 配置 ====================
# 允许跨域的前端域名列表,逗号分隔
CORS_ORIGINS_RAW=https://xiaoxiajianji.com,https://api.xiaoxiajianji.com
# ==================== 生成文件路径 ====================
# 容器内生成文件目录(固定值,勿改)
GENERATED_FILES_DIR=/app/generated
# 生成文件 URL 前缀
GENERATED_FILES_URL_PREFIX=/generated-files
# 主机上生成文件目录(供 Docker volume bind mount 使用)
GENERATED_FILES_HOST_DIR=/var/lib/xiaoxia-saas-production/generated
# ==================== 渲染引擎配置 ====================
# 渲染引擎选择:legacy(旧引擎,稳定)/ unified(新架构)
RENDER_ENGINE=legacy
# ==================== CosyVoice 语音合成 ====================
# 阿里云百灵语音合成服务 API Key
# ${COSYVOICE_API_KEY} — 替换为实际的 CosyVoice API Key
COSYVOICE_API_KEY=${COSYVOICE_API_KEY}
# API 基础 URL
COSYVOICE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
# 模型选择:cosyvoice-v3-flash(推荐)/ cosyvoice-v3-plus
COSYVOICE_MODEL=cosyvoice-v3-flash
# 音色:v3 系列系统音色带 _v3 后缀
COSYVOICE_VOICE=longxiaoxia_v3
# 采样率
COSYVOICE_SAMPLE_RATE=22050
# 输出格式
COSYVOICE_FORMAT=wav
# 音色克隆模型名(固定值)
COSYVOICE_CLONE_MODEL=voice-enrollment
# DashScope 通用 API Key(与 CosyVoice 共用)
DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
# ==================== MediaKit 视频理解(火山引擎)====================
MEDIAKIT_API_KEY=${MEDIAKIT_API_KEY}
MEDIAKIT_BASE_URL=https://mediakit.cn-beijing.volces.com/api/v1
MEDIAKIT_TIMEOUT=60
# ==================== 监控(可选)====================
# Sentry DSN(取消注释并填入实际值以启用错误追踪)
# SENTRY_DSN=${SENTRY_DSN}
+233
View File
@@ -0,0 +1,233 @@
# ============================================================
# 小虾 SaaS — Staging 环境配置模板
# ============================================================
# 使用方式:复制为 /var/lib/xiaoxia-saas-staging/.env 并填入实际密钥
# 敏感值标记为 ${PLACEHOLDER},部署前必须替换为真实值
# ============================================================
# ==================== 应用基本配置 ====================
# 应用名称
APP_NAME=xiaoxia-saas
# 环境标识
APP_ENV=staging
# Staging 开启 Debug 模式便于排查问题
DEBUG=true
# 应用基础 URL(前端页面地址)
APP_BASE_URL=https://staging.xiaoxiajianji.com
# 对外公开的 API 基础 URL(用于生成回调链接等)
PUBLIC_API_BASE_URL=https://staging-api.xiaoxiajianji.com
# API 服务监听地址
API_HOST=0.0.0.0
# API 服务监听端口
API_PORT=8000
# 生产/预发布环境关闭自动建表,使用 alembic migration
AUTO_CREATE_SCHEMA=false
# ==================== 数据库配置 ====================
# 数据库连接串(格式:postgresql+psycopg://user:password@host:port/dbname
# ${DATABASE_URL} — 替换为实际的 Staging PostgreSQL 连接串
DATABASE_URL=${DATABASE_URL}
# 连接池大小(常驻连接数)
DATABASE_POOL_SIZE=20
# 连接池最大溢出连接数(pool_size + max_overflow = 最大并发连接数)
DATABASE_MAX_OVERFLOW=10
# 获取连接超时时间(秒)
DATABASE_POOL_TIMEOUT=30
# 连接回收时间(秒),防止数据库端主动断开导致的死连接
DATABASE_POOL_RECYCLE=3600
# 不使用内存数据库
USE_IN_MEMORY_DB=false
# ==================== Redis 配置 ====================
# Redis 连接 URL(格式:redis://[:password@]host:port/db
# ${REDIS_URL} — 替换为实际的 Staging Redis 连接串
REDIS_URL=${REDIS_URL}
# 启用 Redis Session 存储(多实例部署必须开启)
ENABLE_REDIS_SESSIONS=true
# ==================== Celery 任务队列 ====================
# Celery Broker(任务分发),使用 Redis db0
CELERY_BROKER_URL=${CELERY_BROKER_URL}
# Celery Result Backend(任务结果存储),使用 Redis db1
CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
# ==================== Worker 配置 ====================
# Worker 进程名称
WORKER_NAME=xiaoxia-saas-worker
# Worker 并发数(同时执行的任务数)
WORKER_CONCURRENCY=1
# 每个子进程最多处理多少任务后重启(防止内存泄漏)
WORKER_MAX_TASKS_PER_CHILD=1000
# ==================== JWT 认证配置 ====================
# JWT 签名密钥 — 必须设置为强随机字符串(至少32字符)
# ${JWT_SECRET_KEY} — 替换为实际的随机密钥
JWT_SECRET_KEY=${JWT_SECRET_KEY}
# JWT 签名算法
JWT_ALGORITHM=HS256
# Access Token 过期时间(分钟)
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=1440
# Refresh Token 过期时间(天)
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
# ==================== 邮件配置 ====================
# 邮件功能尚未上线,暂时关闭
ENABLE_EMAIL_DELIVERY=false
# SMTP 服务器地址
SMTP_HOST=smtp.gmail.com
# SMTP 端口
SMTP_PORT=587
# SMTP 用户名(邮件功能上线后配置)
SMTP_USER=
# SMTP 密码(邮件功能上线后配置)
SMTP_PASSWORD=
# 发件人邮箱(邮件功能上线后配置)
SMTP_FROM_EMAIL=
# 发件人显示名称
SMTP_FROM_NAME=小虾 SaaS
# 启用 TLS
SMTP_USE_TLS=true
# ==================== 阿里云 OSS 配置 ====================
# OSS 区域 endpoint
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
# OSS Access Key ID
# ${OSS_ACCESS_KEY_ID} — 替换为实际的 OSS Access Key ID
OSS_ACCESS_KEY_ID=${OSS_ACCESS_KEY_ID}
# OSS Access Key Secret
# ${OSS_ACCESS_KEY_SECRET} — 替换为实际的 OSS Access Key Secret
OSS_ACCESS_KEY_SECRET=${OSS_ACCESS_KEY_SECRET}
# OSS Bucket 名称
OSS_BUCKET_NAME=xiaoxia-autocut
# 直传最大文件大小(MB
OSS_DIRECT_UPLOAD_MAX_MB=2000
# 直传签名有效期(秒)
OSS_DIRECT_UPLOAD_EXPIRE_SECONDS=900
# ==================== MinIO 配置(Staging 独有)====================
# Staging 环境使用 MinIO 替代 OSS 进行文件存储测试
# MinIO 服务 Endpoint
# ${MINIO_ENDPOINT} — 替换为实际的 MinIO 地址
MINIO_ENDPOINT=${MINIO_ENDPOINT}
# MinIO Access Key
# ${MINIO_ACCESS_KEY} — 替换为实际的 MinIO Access Key
MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
# MinIO Secret Key
# ${MINIO_SECRET_KEY} — 替换为实际的 MinIO Secret Key
MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
# MinIO Bucket 名称
MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME}
# 是否使用 SSL 连接 MinIO
MINIO_USE_SSL=false
# ==================== CORS 配置 ====================
# 允许跨域的前端域名列表,逗号分隔
CORS_ORIGINS_RAW=https://staging.xiaoxiajianji.com,https://staging-api.xiaoxiajianji.com
# ==================== 生成文件路径 ====================
# 容器内生成文件目录(固定值,勿改)
GENERATED_FILES_DIR=/app/generated
# 生成文件 URL 前缀
GENERATED_FILES_URL_PREFIX=/generated-files
# 主机上生成文件目录(供 Docker volume bind mount 使用)
GENERATED_FILES_HOST_DIR=/var/lib/xiaoxia-saas-staging/generated
# ==================== 渲染引擎配置 ====================
# 渲染引擎选择:legacy(旧引擎,稳定)/ unified(新架构)
RENDER_ENGINE=legacy
# ==================== CosyVoice 语音合成 ====================
# 阿里云百灵语音合成服务 API Key
# ${COSYVOICE_API_KEY} — 替换为实际的 CosyVoice API Key
COSYVOICE_API_KEY=${COSYVOICE_API_KEY}
# API 基础 URL
COSYVOICE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
# 模型选择:cosyvoice-v3-flash(推荐)/ cosyvoice-v3-plus
COSYVOICE_MODEL=cosyvoice-v3-flash
# 音色:v3 系列系统音色带 _v3 后缀
COSYVOICE_VOICE=longxiaoxia_v3
# 采样率
COSYVOICE_SAMPLE_RATE=22050
# 输出格式
COSYVOICE_FORMAT=wav
# 音色克隆模型名(固定值)
COSYVOICE_CLONE_MODEL=voice-enrollment
# DashScope 通用 API Key(与 CosyVoice 共用)
DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
# ==================== MediaKit 视频理解(火山引擎)====================
MEDIAKIT_API_KEY=${MEDIAKIT_API_KEY}
MEDIAKIT_BASE_URL=https://mediakit.cn-beijing.volces.com/api/v1
MEDIAKIT_TIMEOUT=60
+51
View File
@@ -0,0 +1,51 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss;
client_max_body_size 800m;
# SPA routing - index.html 禁止缓存,确保每次获取最新版本
location / {
try_files $uri /index.html;
}
# API proxy — Production 环境代理到 production API 容器
resolver 127.0.0.11 valid=10s;
resolver_timeout 5s;
location /api/ {
proxy_pass http://xiaoxia-api-production:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_request_buffering off;
}
# Generated files — 通过 alias 映射容器内 /app/generated/ 目录
location /generated-files/ {
alias /app/generated/;
}
# Assets with legacy fallback — 部署期间兼容旧版缓存的 hash 文件名
# 先在当前镜像中找,找不到去 legacy-assets 目录找(从旧版本容器中备份的)
location ^~ /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri /assets-legacy$uri =404;
}
# 静态资源长缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
+50
View File
@@ -0,0 +1,50 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss;
client_max_body_size 800m;
# SPA routing - index.html 禁止缓存,确保每次获取最新版本
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
expires 0;
}
# SPA fallback
location / {
try_files $uri /index.html;
}
# API proxy — Staging 环境代理到 staging API 容器
resolver 127.0.0.11 valid=10s;
resolver_timeout 5s;
location /api/ {
proxy_pass http://xiaoxia-api-staging:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_request_buffering off;
}
# Generated files — 通过 alias 映射容器内 /app/generated/ 目录
location /generated-files/ {
alias /app/generated/;
}
# 静态资源长缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
+1 -1
View File
@@ -208,7 +208,7 @@ volumes:
# 重要: 确保主机目录存在且有正确权限
# Staging: /var/lib/xiaoxia-saas-staging/generated
# Production: /var/lib/xiaoxia-saas-production/generated
device: ${GENERATED_FILES_HOST_DIR:-/var/lib/xiaoxia-saas-staging/generated}
device: ${GENERATED_FILES_HOST_DIR:?GENERATED_FILES_HOST_DIR must be set in .env}
# ===========================================
# 网络配置
@@ -289,6 +289,7 @@ class GenerationTaskModel(Base):
completed_at = Column(DateTime, nullable=True)
created_by_user_id = Column(String(36), nullable=False, default="", index=True)
source_edit_plan_id = Column(String(36), nullable=True, index=True)
edit_plan_id = Column(String(36), nullable=True, index=True)
asset_select_mode = Column(String(20), nullable=False, default="")
batch_id = Column(String(36), nullable=False, default="", index=True)
video_title = Column(String(255), nullable=False, default="")
@@ -497,6 +498,7 @@ class TemplateCategoryModel(Base):
id = Column(String(36), primary_key=True)
user_id = Column(String(36), nullable=False, index=True)
name = Column(String(100), nullable=False)
sort_order = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
+200 -13
View File
@@ -27,6 +27,137 @@ DEFAULT_INTRO_DURATION = 3.0
DEFAULT_OUTRO_DURATION = 3.0
# ── SceneChange 镜头段工具 ────────────────────────────────────────────────────
def build_scene_segments(
scene_changes: list[float],
asset_duration: float,
) -> list[tuple[float, float]]:
"""根据场景切换点构建镜头段列表.
Args:
scene_changes: 场景切换点时间戳列表(已排序,首位为 0.0)
asset_duration: 素材总时长
Returns:
镜头段列表 [(start, end), ...],仅保留长度 >= 0.5s 的段
"""
segments: list[tuple[float, float]] = []
for i, ts in enumerate(scene_changes):
end = scene_changes[i + 1] if i + 1 < len(scene_changes) else asset_duration
# 只保留有效长度的镜头段(至少 0.5 秒)
if end - ts >= 0.5:
segments.append((ts, end))
return segments
def pick_start_in_scene_segment(
seg_start: float,
seg_end: float,
clip_duration: float,
) -> float | None:
"""在镜头段内随机选取一个起始时间点.
确保 start + clip_duration <= seg_end。
若镜头段长度不足以容纳片段,返回 None。
"""
available = seg_end - seg_start - clip_duration
if available < 0:
return None
max_start = seg_start + available
return random.uniform(seg_start, max_start)
def _segments_overlap(
start: float,
duration: float,
used: list[tuple[float, float]],
edge_gap: float = 0.0,
) -> bool:
"""候选区间 [start, start+duration] 是否与已用区间冲突(含边缘间隙扩边)。"""
end = start + duration
for used_start, used_end in used:
if start < used_end + edge_gap and end > used_start - edge_gap:
return True
return False
def pick_scene_aware_start(
asset_id: str,
clip_duration: float,
asset_durations: dict[str, float],
asset_scene_points: dict[str, list[float]] | None,
used_segments: dict[str, list[tuple[float, float]]],
*,
edge_gap: float = 0.0,
) -> float | None:
"""基于缓存的场景切换点,从随机镜头段中选取不冲突的起始时间.
流程:
1. 读取 asset_scene_points 中该素材的场景切换点缓存 → 构建镜头段
2. random.shuffle 镜头段(保证同一素材多次生成选不同镜头,而非固定第N段)
3. 依次尝试:段内随机取点 → 越界检查 → 与 used_segments 冲突检查
4. 全部冲突/无缓存 → 返回 None,由调用方回退 _calc_random_start_time
Args:
asset_id: 素材 ID
clip_duration: 片段时长(秒)
asset_durations: 素材 ID -> 总时长
asset_scene_points: 素材 ID -> 场景切换点列表(metadata 缓存)
used_segments: 素材 ID -> 已用区间列表(冲突避让)
edge_gap: 冲突判定的边缘间隙(秒),已用区间按 [s-gap, e+gap] 扩边
"""
asset_total = (asset_durations or {}).get(asset_id)
if not asset_total or asset_total <= 0:
return None
scene_points = (asset_scene_points or {}).get(asset_id)
if not scene_points:
return None
used = used_segments.get(asset_id, []) if used_segments else []
scene_segments = build_scene_segments(scene_points, asset_total)
if not scene_segments:
return None
random.shuffle(scene_segments)
for seg_start, seg_end in scene_segments:
candidate = pick_start_in_scene_segment(seg_start, seg_end, clip_duration)
if candidate is None:
continue
# 越界检查(防御:场景点末尾段理论上不越界,metadata 脏数据兜底)
if candidate + clip_duration > asset_total:
continue
# 与已用区间冲突检查
if _segments_overlap(candidate, clip_duration, used, edge_gap):
continue
return candidate
return None
def extract_scene_points_from_metadata(metadata: object) -> list[float] | None:
"""从素材 metadata 中提取并校验场景切换点缓存.
合法缓存:list 类型、至少 2 个数值点、单调非负;否则返回 None(按未缓存处理)。
"""
if not isinstance(metadata, dict):
return None
points = metadata.get("scene_change_points")
if not isinstance(points, list) or len(points) < 2:
return None
try:
cleaned = [float(p) for p in points]
except (TypeError, ValueError):
return None
if any(p < 0 for p in cleaned):
return None
cleaned = sorted(cleaned)
if cleaned[0] != 0.0:
cleaned.insert(0, 0.0)
return cleaned
# ── 素材分配 ────────────────────────────────────────────────────────────────
@@ -37,6 +168,7 @@ def distribute_assets(
*,
random_selection: bool = False,
asset_durations: dict[str, float] | None = None,
asset_scene_points: dict[str, list[float]] | None = None,
) -> None:
"""按 editing_mode 将素材分配到 clips(就地修改).
@@ -46,12 +178,16 @@ def distribute_assets(
- VOICE_OVER: 素材→main clips (B-roll)
- VOICE_PIP: 第1个→background, 第2个→corner_voice, 其余→b_roll
start_time 选取:素材 metadata 中有场景切换点缓存时,优先从随机镜头段
取起点(不同片段来自不同镜头);无缓存或镜头段都冲突时回退随机起点。
Args:
clips: 剪辑片段列表(就地修改 asset_id)
asset_ids: 素材 ID 列表
editing_mode: 剪辑模式字符串
random_selection: 是否随机选择素材(用于预览生成)
asset_durations: 素材 ID -> 时长(秒)映射,用于设置随机 start_time
asset_durations: 素材 ID -> 时长(秒)映射,用于设置 start_time
asset_scene_points: 素材 ID -> 场景切换点列表(metadata 缓存)
"""
if not asset_ids or not clips:
return
@@ -62,22 +198,56 @@ def distribute_assets(
random.shuffle(asset_ids)
if editing_mode == EditingMode.ONE_TAKE.value:
_distribute_one_take(clips, asset_ids, asset_durations)
_distribute_one_take(clips, asset_ids, asset_durations, asset_scene_points)
elif editing_mode == EditingMode.PIP.value:
_distribute_pip(clips, asset_ids, asset_durations)
_distribute_pip(clips, asset_ids, asset_durations, asset_scene_points)
elif editing_mode == EditingMode.VOICE_OVER.value:
_distribute_voice_over(clips, asset_ids, asset_durations)
_distribute_voice_over(clips, asset_ids, asset_durations, asset_scene_points)
elif editing_mode == EditingMode.VOICE_PIP.value:
_distribute_voice_pip(clips, asset_ids, asset_durations)
_distribute_voice_pip(clips, asset_ids, asset_durations, asset_scene_points)
else:
# 未知模式,退化为 one_take
_distribute_one_take(clips, asset_ids, asset_durations)
_distribute_one_take(clips, asset_ids, asset_durations, asset_scene_points)
def _resolve_start_time(
asset_id: str,
clip_duration: float,
asset_durations: dict[str, float] | None,
used_segments: dict[str, list[tuple[float, float]]],
asset_scene_points: dict[str, list[float]] | None = None,
on_exhausted: Callable[[str, float], tuple[float, float] | None] | None = None,
) -> float | None:
"""选取片段起点:场景缓存优先(随机镜头段),无缓存/全冲突回退随机起点.
场景路径与随机路径共享 used_segments 冲突避让;场景路径返回 None 时
(无缓存、镜头段全冲突)回退 _calc_random_start_time,其受控复用逻辑
on_exhausted)不受影响。
"""
if asset_scene_points and asset_scene_points.get(asset_id):
scene_start = pick_scene_aware_start(
asset_id,
clip_duration,
asset_durations or {},
asset_scene_points,
used_segments,
)
if scene_start is not None:
return scene_start
return _calc_random_start_time(
asset_id,
clip_duration,
asset_durations,
used_segments,
on_exhausted=on_exhausted,
)
def _distribute_one_take(
clips: List[EditPlanClip],
asset_ids: List[str],
asset_durations: dict[str, float] | None = None,
asset_scene_points: dict[str, list[float]] | None = None,
) -> None:
"""ONE_TAKE: 素材按顺序依次分配给 main 类型 clips."""
used_segments: dict[str, list[tuple[float, float]]] = {}
@@ -85,7 +255,9 @@ def _distribute_one_take(
for i, clip in enumerate(main_clips):
if i < len(asset_ids):
asset_id = asset_ids[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations, used_segments)
start_time = _resolve_start_time(
asset_id, clip.duration, asset_durations, used_segments, asset_scene_points
)
clip.assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
@@ -98,6 +270,7 @@ def _distribute_pip(
clips: List[EditPlanClip],
asset_ids: List[str],
asset_durations: dict[str, float] | None = None,
asset_scene_points: dict[str, list[float]] | None = None,
) -> None:
"""PIP: 第1个素材→main(全屏背景),其余→overlay clips."""
used_segments: dict[str, list[tuple[float, float]]] = {}
@@ -105,7 +278,9 @@ def _distribute_pip(
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
if main_clips and asset_ids:
asset_id = asset_ids[0]
start_time = _calc_random_start_time(asset_id, main_clips[0].duration, asset_durations, used_segments)
start_time = _resolve_start_time(
asset_id, main_clips[0].duration, asset_durations, used_segments, asset_scene_points
)
main_clips[0].assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
@@ -119,7 +294,9 @@ def _distribute_pip(
for i, clip in enumerate(overlay_clips):
if i < len(remaining):
asset_id = remaining[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations, used_segments)
start_time = _resolve_start_time(
asset_id, clip.duration, asset_durations, used_segments, asset_scene_points
)
clip.assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
@@ -132,6 +309,7 @@ def _distribute_voice_over(
clips: List[EditPlanClip],
asset_ids: List[str],
asset_durations: dict[str, float] | None = None,
asset_scene_points: dict[str, list[float]] | None = None,
) -> None:
"""VOICE_OVER: 素材→main clips (B-roll)."""
used_segments: dict[str, list[tuple[float, float]]] = {}
@@ -139,7 +317,9 @@ def _distribute_voice_over(
for i, clip in enumerate(main_clips):
if i < len(asset_ids):
asset_id = asset_ids[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations, used_segments)
start_time = _resolve_start_time(
asset_id, clip.duration, asset_durations, used_segments, asset_scene_points
)
clip.assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
@@ -152,6 +332,7 @@ def _distribute_voice_pip(
clips: List[EditPlanClip],
asset_ids: List[str],
asset_durations: dict[str, float] | None = None,
asset_scene_points: dict[str, list[float]] | None = None,
) -> None:
"""VOICE_PIP: 第1个→background, 第2个→corner_voice, 其余→b_roll."""
used_segments: dict[str, list[tuple[float, float]]] = {}
@@ -164,7 +345,9 @@ def _distribute_voice_pip(
# 第1个 → background
if idx < len(asset_ids) and bg_clips:
asset_id = asset_ids[idx]
start_time = _calc_random_start_time(asset_id, bg_clips[0].duration, asset_durations, used_segments)
start_time = _resolve_start_time(
asset_id, bg_clips[0].duration, asset_durations, used_segments, asset_scene_points
)
bg_clips[0].assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
@@ -176,7 +359,9 @@ def _distribute_voice_pip(
# 第2个 → corner_voice
if idx < len(asset_ids) and voice_clips:
asset_id = asset_ids[idx]
start_time = _calc_random_start_time(asset_id, voice_clips[0].duration, asset_durations, used_segments)
start_time = _resolve_start_time(
asset_id, voice_clips[0].duration, asset_durations, used_segments, asset_scene_points
)
voice_clips[0].assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
@@ -190,7 +375,9 @@ def _distribute_voice_pip(
for i, clip in enumerate(broll_clips):
if i < len(remaining):
asset_id = remaining[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations, used_segments)
start_time = _resolve_start_time(
asset_id, clip.duration, asset_durations, used_segments, asset_scene_points
)
clip.assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
+7
View File
@@ -14,6 +14,13 @@ from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
# 素材选取排序时注入的随机噪声上限(分)。
# score_asset 综合得分范围为 0-100,噪声 0~20 意味着:
# - 素材间得分差距 > 20 分时,排名不受影响(质量差异显著的素材保持稳定优先级)
# - 得分接近(差距 <= 20 分)的素材排名会随机浮动,使每次生成选出的素材组合不同,
# 从素材组合层面降低成片重复率;排名靠后的低分素材也有机会入选。
SCORE_RANDOM_NOISE_MAX = 20.0
@dataclass
class SmartMatchResult:
+7 -1
View File
@@ -55,7 +55,13 @@ if [ -z "$IMAGE_TAG" ]; then
exit 1
fi
test -f "$ENV_FILE"
# .env 文件由 CI 从模板 + Secrets 渲染后通过 SCP 上传到服务器
# 如果文件不存在,说明 CI 渲染步骤失败或未执行
if [ ! -f "$ENV_FILE" ]; then
echo "ERROR: $ENV_FILE 不存在。CI 应先在 render_env 步骤渲染并上传此文件"
exit 1
fi
echo "✅ .env file found: $ENV_FILE ($(wc -l < "$ENV_FILE") lines)"
mkdir -p "$GENERATED_DIR"
mkdir -p "$LEGACY_ASSETS_DIR"
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# ===========================================================
# config_diff_check.sh — 对比渲染 .env 与服务器当前 .env
# ===========================================================
# 用法: scripts/config_diff_check.sh <rendered_file> <current_file>
#
# 输出:
# + ADDED 渲染文件有、当前文件没有(新增配置)
# - REMOVED 当前文件有、渲染文件没有(将被删除)
# ~ CHANGED 两边都有但值不同(将被覆盖)
#
# 敏感值脱敏:KEY/SECRET/PASSWORD/TOKEN/URL 类变量只显示前4字符+***
# 退出码: 始终返回 0(仅告警,不阻塞部署)
# ===========================================================
set -u
RENDERED_FILE="${1:-}"
CURRENT_FILE="${2:-}"
if [ -z "$RENDERED_FILE" ] || [ -z "$CURRENT_FILE" ]; then
echo "ERROR: 用法: $0 <rendered_file> <current_file>" >&2
exit 0
fi
if [ ! -f "$RENDERED_FILE" ]; then
echo "ERROR: 渲染文件不存在: $RENDERED_FILE" >&2
exit 0
fi
# 判断是否为敏感变量(键名包含以下关键词)
is_sensitive() {
local key="$1"
case "$key" in
*KEY*|*SECRET*|*PASSWORD*|*TOKEN*|*URL*|*BROKER*|*BACKEND*) return 0 ;;
*) return 1 ;;
esac
}
# 脱敏:敏感值只显示前4字符+***
mask_value() {
local key="$1"
local value="$2"
if is_sensitive "$key"; then
if [ ${#value} -le 4 ]; then
echo "****"
else
echo "${value:0:4}***"
fi
else
echo "$value"
fi
}
# 解析文件为 KEY=VALUE(忽略注释和空行)
parse_env() {
local file="$1"
grep -vE '^\s*#|^\s*$' "$file" 2>/dev/null | while IFS= read -r line; do
# 只取第一个 = 之前的部分作为 key
key="${line%%=*}"
value="${line#*=}"
# 跳过无效行
if [ -n "$key" ] && [ "$key" != "$line" ]; then
echo "${key}=${value}"
fi
done
}
echo "=========================================="
echo " 配置 Diff 检查(检测配置漂移)"
echo "=========================================="
echo "渲染文件: $RENDERED_FILE"
echo "当前文件: $CURRENT_FILE"
echo ""
# 解析两个文件
if [ ! -f "$CURRENT_FILE" ] || [ ! -s "$CURRENT_FILE" ]; then
# 服务器 .env 不存在或为空(首次部署)
echo "⚠️ 服务器 .env 不存在或为空(可能是首次部署)"
echo " 所有配置项将标记为 ADDED"
echo ""
added=0
while IFS='=' read -r key value; do
[ -z "$key" ] && continue
masked=$(mask_value "$key" "$value")
echo " + ADDED ${key}=${masked}"
added=$((added + 1))
done < <(parse_env "$RENDERED_FILE")
echo ""
echo "=========================================="
echo " 汇总: 新增 ${added} 项 | 删除 0 项 | 变更 0 项 | 无变化 0 项"
echo "=========================================="
exit 0
fi
# 用临时文件存储解析结果
tmp_rendered=$(mktemp)
tmp_current=$(mktemp)
trap "rm -f $tmp_rendered $tmp_current" EXIT
parse_env "$RENDERED_FILE" | sort > "$tmp_rendered"
parse_env "$CURRENT_FILE" | sort > "$tmp_current"
added=0
removed=0
changed=0
unchanged=0
echo "--- 新增配置(渲染文件有、当前文件无)---"
# 找 ADDED:渲染文件有但当前文件没有的 key
while IFS='=' read -r key value; do
[ -z "$key" ] && continue
current_line=$(grep -m1 "^${key}=" "$tmp_current" 2>/dev/null || true)
if [ -z "$current_line" ]; then
masked=$(mask_value "$key" "$value")
echo " + ADDED ${key}=${masked}"
added=$((added + 1))
fi
done < "$tmp_rendered"
if [ "$added" -eq 0 ]; then
echo " (无)"
fi
echo ""
echo "--- 删除配置(当前文件有、渲染文件无)---"
# 找 REMOVED:当前文件有但渲染文件没有的 key
while IFS='=' read -r key value; do
[ -z "$key" ] && continue
rendered_line=$(grep -m1 "^${key}=" "$tmp_rendered" 2>/dev/null || true)
if [ -z "$rendered_line" ]; then
masked=$(mask_value "$key" "$value")
echo " - REMOVED ${key}=${masked}"
removed=$((removed + 1))
fi
done < "$tmp_current"
if [ "$removed" -eq 0 ]; then
echo " (无)"
fi
echo ""
echo "--- 变更配置(两边都有但值不同)---"
# 找 CHANGED:两边都有但值不同
while IFS='=' read -r key value; do
[ -z "$key" ] && continue
current_line=$(grep -m1 "^${key}=" "$tmp_current" 2>/dev/null || true)
if [ -n "$current_line" ]; then
current_value="${current_line#*=}"
if [ "$value" != "$current_value" ]; then
masked_new=$(mask_value "$key" "$value")
masked_old=$(mask_value "$key" "$current_value")
echo " ~ CHANGED ${key}: ${masked_old}${masked_new}"
changed=$((changed + 1))
else
unchanged=$((unchanged + 1))
fi
fi
done < "$tmp_rendered"
if [ "$changed" -eq 0 ]; then
echo " (无)"
fi
echo ""
echo "=========================================="
echo " 汇总: 新增 ${added} 项 | 删除 ${removed} 项 | 变更 ${changed} 项 | 无变化 ${unchanged}"
echo "=========================================="
if [ "$added" -gt 0 ] || [ "$removed" -gt 0 ] || [ "$changed" -gt 0 ]; then
echo "⚠️ 检测到配置漂移,请确认以上变更是否符合预期"
else
echo "✅ 配置无漂移,与服务器当前配置一致"
fi
exit 0
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# ===========================================================
# render_env.sh — 从模板 + Secrets 渲染 .env 文件
# ===========================================================
# 用法: scripts/render_env.sh <staging|production>
#
# 输入: deploy/configs/.env.staging 或 .env.production 模板
# 输出: .env.rendered(包含真实密钥,切勿提交或打印)
#
# 环境变量映射规则:
# STAGING_xxx / PRODUCTION_xxx → xxx(去掉环境前缀)
# 共用 secrets 直接使用(如 OSS_ACCESS_KEY_ID
# ===========================================================
set -eu
TARGET_ENV="${1:-}"
if [ -z "$TARGET_ENV" ] || { [ "$TARGET_ENV" != "staging" ] && [ "$TARGET_ENV" != "production" ]; }; then
echo "ERROR: 用法: $0 <staging|production>" >&2
exit 1
fi
TEMPLATE_FILE="deploy/configs/.env.${TARGET_ENV}"
OUTPUT_FILE=".env.rendered"
if [ ! -f "$TEMPLATE_FILE" ]; then
echo "ERROR: 模板文件不存在: $TEMPLATE_FILE" >&2
exit 1
fi
# 构建环境变量映射(带环境前缀的 secrets → 模板变量名)
ENV_PREFIX=$(echo "$TARGET_ENV" | tr '[:lower:]' '[:upper:]')
# 需要映射的带环境前缀变量
MAPPED_VARS="DATABASE_URL REDIS_URL CELERY_BROKER_URL CELERY_RESULT_BACKEND JWT_SECRET_KEY"
# Staging 独有的 MinIO 变量
if [ "$TARGET_ENV" = "staging" ]; then
MAPPED_VARS="$MAPPED_VARS MINIO_ENDPOINT MINIO_ACCESS_KEY MINIO_SECRET_KEY"
fi
# 将带前缀的 secrets 导出为无前缀的环境变量
for var in $MAPPED_VARS; do
prefixed_var="${ENV_PREFIX}_${var}"
value="${!prefixed_var:-}"
if [ -n "$value" ]; then
export "$var=$value"
fi
done
# 特殊映射:CI secret 名称与模板占位符不一致的变量
# STAGING_MINIO_BUCKET → MINIO_BUCKET_NAME
if [ "$TARGET_ENV" = "staging" ]; then
if [ -n "${STAGING_MINIO_BUCKET:-}" ]; then
export "MINIO_BUCKET_NAME=$STAGING_MINIO_BUCKET"
fi
fi
# 共用 secrets 直接导出(如果存在)
SHARED_SECRETS="OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET COSYVOICE_API_KEY DASHSCOPE_API_KEY MEDIAKIT_API_KEY"
for var in $SHARED_SECRETS; do
value="${!var:-}"
# 已经在环境中了,无需额外操作
done
# 使用 Python 进行变量替换(Python 在 CI runner 中一定存在)
python3 - "$TEMPLATE_FILE" "$OUTPUT_FILE" "$ENV_PREFIX" "$MAPPED_VARS" "$SHARED_SECRETS" <<'PYTHON_SCRIPT'
import sys
import os
import re
template_file = sys.argv[1]
output_file = sys.argv[2]
env_prefix = sys.argv[3]
mapped_vars_str = sys.argv[4]
shared_secrets_str = sys.argv[5]
# 收集所有可用的替换变量
all_vars = set()
for v in mapped_vars_str.split():
all_vars.add(v)
for v in shared_secrets_str.split():
all_vars.add(v)
# 读取模板
with open(template_file, 'r') as f:
template = f.read()
# 找出模板中所有的 ${VAR} 占位符(仅检查非注释行)
pattern = re.compile(r'\$\{(\w+)\}')
placeholders = set()
for line in template.splitlines():
stripped = line.strip()
if stripped.startswith('#'):
continue
placeholders.update(pattern.findall(line))
# 检查必需变量是否已设置
missing = []
for var in placeholders:
value = os.environ.get(var, '')
if not value:
missing.append(var)
if missing:
print(f"ERROR: 以下变量未设置或为空: {', '.join(sorted(missing))}", file=sys.stderr)
print(f"请确认对应的 {env_prefix}_xxx 或共用 secrets 已在 Gitea Secrets 中配置", file=sys.stderr)
sys.exit(1)
# 执行替换
def replace_var(match):
var_name = match.group(1)
return os.environ.get(var_name, match.group(0))
rendered = pattern.sub(replace_var, template)
# 写入输出文件
with open(output_file, 'w') as f:
f.write(rendered)
# 设置文件权限为仅 owner 可读写
os.chmod(output_file, 0o600)
print(f"✅ .env 渲染完成: {template_file} → {output_file}")
print(f" 替换了 {len(placeholders)} 个变量")
PYTHON_SCRIPT
# 验证输出文件
if [ ! -f "$OUTPUT_FILE" ]; then
echo "ERROR: 渲染失败,输出文件不存在" >&2
exit 1
fi
# 检查输出文件中是否还有未替换的占位符(仅检查非注释行)
if grep -vE '^\s*#' "$OUTPUT_FILE" | grep -qE '\$\{[A-Z_]+\}'; then
echo "ERROR: 输出文件中仍有未替换的占位符:" >&2
grep -nE '\$\{[A-Z_]+\}' "$OUTPUT_FILE" | grep -v '^\s*#' >&2
exit 1
fi
echo "✅ 渲染文件校验通过,无残留占位符"
echo "⚠️ $OUTPUT_FILE 包含敏感信息,请勿提交或打印到日志"
+26 -2
View File
@@ -38,6 +38,28 @@ def _segments(count: int, dur_min: float = 3.0, dur_max: float = 5.0):
return [(i, dur_min, dur_max) for i in range(count)]
def _patch_zero_noise():
"""消除 clips.py 排序随机噪声,用于确定性断言(如均衡分配)。
排序噪声(random.uniform(0, SCORE_RANDOM_NOISE_MAX))返回 0
其他 uniform 调用(片段时长随机)委托给独立 Random 实例,行为不变。
"""
import random as _stdlib_random
from app.api.routes.templates_editor import clips as clips_module
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX
_fallback = _stdlib_random.Random()
def _fake_uniform(a, b):
if b == SCORE_RANDOM_NOISE_MAX:
return 0.0
return _fallback.uniform(a, b)
return patch.object(clips_module.random, "uniform", _fake_uniform)
def _patch_segments(segments=None):
return patch(
"app.api.routes.templates_editor.clips._get_template_segments",
@@ -130,7 +152,8 @@ class TestEditorClipsBySegments:
body = ClipsFromAssetsRequest(asset_ids=["a1", "a2"], required_clips_count=2)
with _patch_segments(DEFAULT_SEGMENTS):
# 均衡分配由 use_count 贪心保证,消除排序噪声后确定性断言
with _patch_zero_noise(), _patch_segments(DEFAULT_SEGMENTS):
result = create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
@@ -795,7 +818,8 @@ class TestClipsFromAssetsInvalidIds:
body = ClipsFromAssetsRequest(asset_ids=["a1", None, "", "a2"]) # type: ignore[list-item]
with _patch_segments(_segments(2, dur_min=3.0, dur_max=5.0)):
# 消除排序噪声,确定性断言两条合法素材各被使用
with _patch_zero_noise(), _patch_segments(_segments(2, dur_min=3.0, dur_max=5.0)):
result = create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
+33 -10
View File
@@ -306,6 +306,27 @@ class TestGetTemplateSegments:
# ── from-assets 端点集成测试 ────────────────────────────────────────────────
def _patch_zero_noise():
"""消除 clips.py 排序随机噪声(SCORE_RANDOM_NOISE_MAX 噪声返回 0)。
用于均衡分配等确定性断言;其他 uniform 调用(片段时长随机)行为不变。
"""
import random as _stdlib_random
from app.api.routes.templates_editor import clips as clips_module
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX
_fallback = _stdlib_random.Random()
def _fake_uniform(a, b):
if b == SCORE_RANDOM_NOISE_MAX:
return 0.0
return _fallback.uniform(a, b)
return patch.object(clips_module.random, "uniform", _fake_uniform)
def _make_auth_user():
auth = MagicMock()
auth.user.id = "user-001"
@@ -470,16 +491,18 @@ class TestFromAssetsByTemplateSegments:
mock_asset_repo.get.side_effect = get_asset
body = ClipsFromAssetsRequest(asset_ids=["a1", "a2"])
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id="plan-1",
services=(mock_tpl_svc, mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
# 消除排序噪声,确定性断言贪心均衡分配
with _patch_zero_noise():
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id="plan-1",
services=(mock_tpl_svc, mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
clips_data = _get_clips_data(mock_plan_svc)
asset_ids = [c["asset_id"] for c in clips_data]
+368
View File
@@ -0,0 +1,368 @@
"""测试 SceneChange 场景检测前置到渲染前 + 场景点缓存读写。
验证:
- 场景点缓存读取(extract_scene_points_from_metadata):合法/非法/脏数据
- pick_scene_aware_start:随机镜头段选取、冲突避让、shuffle 随机化、无缓存回退 None
- from-assets 路径:metadata 有 scene_change_points 时,start_time 落在镜头段内
- 一键生成路径:distribute_assets 传入 asset_scene_points 时使用镜头段
- 后台任务:检测结果写入素材 metadata(缓存)
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
import pytest
from packages.domain.plan_generator_utils import (
_calc_random_start_time,
build_scene_segments,
distribute_assets,
extract_scene_points_from_metadata,
pick_scene_aware_start,
)
# ── metadata 缓存解析 ─────────────────────────────────────────────────────────
class TestExtractScenePoints:
def test_valid_points(self):
md = {"scene_change_points": [0.0, 3.2, 7.8, 12.5]}
assert extract_scene_points_from_metadata(md) == [0.0, 3.2, 7.8, 12.5]
def test_missing_returns_none(self):
assert extract_scene_points_from_metadata({}) is None
assert extract_scene_points_from_metadata(None) is None
assert extract_scene_points_from_metadata("not-a-dict") is None
def test_empty_list_returns_none(self):
assert extract_scene_points_from_metadata({"scene_change_points": []}) is None
assert extract_scene_points_from_metadata({"scene_change_points": [0.0]}) is None
def test_dirty_data_returns_none(self):
assert extract_scene_points_from_metadata({"scene_change_points": ["a", 1.0]}) is None
def test_auto_prepends_zero(self):
result = extract_scene_points_from_metadata({"scene_change_points": [3.2, 7.8]})
assert result == [0.0, 3.2, 7.8]
def test_sorts_unsorted(self):
result = extract_scene_points_from_metadata({"scene_change_points": [0.0, 12.5, 3.2, 7.8]})
assert result == [0.0, 3.2, 7.8, 12.5]
def test_negative_rejected(self):
assert extract_scene_points_from_metadata({"scene_change_points": [-1.0, 3.2]}) is None
# ── 镜头段选取 ────────────────────────────────────────────────────────────────
class TestPickSceneAwareStart:
def test_start_within_some_scene_segment(self):
"""有缓存时,起点落在某个镜头段内部。"""
points = [0.0, 5.0, 10.0, 15.0]
durations = {"a1": 18.0}
scene_points = {"a1": points}
used: dict = {}
start = pick_scene_aware_start("a1", 3.0, durations, scene_points, used)
assert start is not None
segments = build_scene_segments(points, 18.0)
assert any(seg_start <= start and start + 3.0 <= seg_end for seg_start, seg_end in segments)
def test_no_cache_returns_none(self):
"""无缓存返回 None(调用方回退随机起点)。"""
result = pick_scene_aware_start("a1", 3.0, {"a1": 18.0}, {}, {})
assert result is None
def test_bounds_respected(self):
"""起点 + 片段时长不超过素材总时长。"""
points = [0.0, 5.0, 10.0, 15.0]
for _ in range(30):
start = pick_scene_aware_start("a1", 4.0, {"a1": 18.0}, {"a1": points}, {})
assert start is not None
assert start + 4.0 <= 18.0 + 1e-6
def test_conflict_avoidance(self):
"""所有镜头段都被占满时返回 None(回退随机路径)。"""
# 3 段各 6s,片段 5s;把所有段占满([0,5.5] [5.5,11] 覆盖段1/2,段3太短放不下5s)
points = [0.0, 6.0, 12.0]
used = {"a1": [(0.0, 5.6), (6.0, 11.6)]}
# 段3 [12, 18] 可用 → 应返回其中起点
start = pick_scene_aware_start("a1", 5.0, {"a1": 18.0}, {"a1": points}, used)
assert start is not None
assert start >= 12.0
def test_all_segments_conflict_returns_none(self):
"""全部镜头段都冲突时返回 None。"""
points = [0.0, 6.0, 12.0]
# 占满整个素材
used = {"a1": [(0.0, 18.0)]}
start = pick_scene_aware_start("a1", 5.0, {"a1": 18.0}, {"a1": points}, used)
assert start is None
def test_shuffle_produces_varied_segments(self):
"""镜头段顺序被 shuffle:30 次选取,起点分布应覆盖多个镜头段。"""
points = [0.0, 5.0, 10.0, 15.0]
observed: set[int] = set()
for _ in range(40):
start = pick_scene_aware_start("a1", 2.0, {"a1": 18.0}, {"a1": points}, {})
assert start is not None
# 记录起点落在哪个段(段宽 5s
observed.add(int(start // 5.0))
assert len(observed) >= 3, f"镜头段 shuffle 后应覆盖多个段,实际 {observed}"
# ── 一键生成路径:distribute_assets 接入场景缓存 ──────────────────────────────
class TestDistributeWithScenePoints:
def _make_clips(self, n):
from packages.domain.edit_plan_clip import EditPlanClip
return [EditPlanClip(id=f"c{i}", plan_id="p1", clip_type="main", duration=4.0, order=i) for i in range(n)]
def test_one_take_uses_scene_segments(self):
"""ONE_TAKE 模式下,有场景缓存的素材起点落在镜头段内。"""
from packages.domain.editing_mode import EditingMode
clips = self._make_clips(2)
points = [0.0, 6.0, 12.0, 18.0]
distribute_assets(
clips,
["a1"],
EditingMode.ONE_TAKE.value,
asset_durations={"a1": 24.0},
asset_scene_points={"a1": points},
)
segments = build_scene_segments(points, 24.0)
for clip in clips:
assert clip.start_time is not None
assert any(
s <= clip.start_time and clip.start_time + 4.0 <= e for s, e in segments
), f"起点 {clip.start_time} 不在任何镜头段内"
def test_no_scene_points_falls_back_random(self):
"""无场景缓存时正常分配(回退随机起点),不报错。"""
from packages.domain.editing_mode import EditingMode
clips = self._make_clips(1)
distribute_assets(
clips,
["a1"],
EditingMode.ONE_TAKE.value,
asset_durations={"a1": 24.0},
asset_scene_points={},
)
for clip in clips:
assert clip.asset_id == "a1"
assert clip.start_time is not None
assert 0.0 <= clip.start_time <= 20.0
def test_random_preview_ignores_scene_points(self):
"""random_selection 预览模式行为不变(不崩溃、正常分配)。"""
from packages.domain.editing_mode import EditingMode
clips = self._make_clips(2)
distribute_assets(
clips,
["a1", "a2"],
EditingMode.ONE_TAKE.value,
random_selection=True,
asset_durations={"a1": 24.0, "a2": 24.0},
asset_scene_points={"a1": [0.0, 6.0]},
)
assert all(c.asset_id for c in clips)
# ── from-assets 路径:渲染前读缓存选镜头段 ────────────────────────────────────
def _make_auth_user():
auth = MagicMock()
auth.user.id = "user-001"
auth.user.email = "test@example.com"
auth.user.display_name = "测试用户"
auth.user_id = "user-001"
return auth
def _make_asset_with_scenes(aid, duration, scene_points=None):
asset = MagicMock()
asset.id = aid
asset.duration = duration
asset.quality_score = None
asset.created_at = None
asset.metadata = {"scene_change_points": scene_points} if scene_points is not None else {}
return asset
class TestFromAssetsSceneCache:
def test_cached_scene_points_used_for_start_time(self):
"""素材 metadata 有场景点缓存时,片段起点落在镜头段内。"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
# 镜头段宽度 10s,片段最长 ~5.3s(含转场补偿),每段都能容纳
scene_points = [0.0, 10.0, 20.0, 30.0]
asset_duration = 40.0
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(
side_effect=lambda aid: _make_asset_with_scenes(aid, asset_duration, scene_points)
)
mock_plan_svc = MagicMock()
mock_plan_svc.replace_all_clips_transactional = MagicMock(return_value=3)
segments = [(0, 3.0, 5.0), (1, 3.0, 5.0), (2, 3.0, 5.0)]
with (
patch(
"app.api.routes.templates_editor.clips._get_template_segments",
return_value=segments,
),
patch("app.api.routes.templates_editor.clips.get_used_segments", return_value={}),
patch("app.api.routes.templates_editor.clips.record_used_segments", return_value=None),
):
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=3)
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id="plan-scene-1",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
clips_data = mock_plan_svc.replace_all_clips_transactional.call_args.args[1]
scene_segments = build_scene_segments(scene_points, asset_duration)
for clip in clips_data:
start = clip["start_time"]
dur = clip["duration"]
in_segment = any(s <= start and start + dur <= e + 0.1 for s, e in scene_segments)
assert in_segment, f"起点 {start:.2f} 时长 {dur:.2f} 不在任何镜头段内"
def test_no_cache_falls_back_random_no_error(self):
"""素材无场景缓存时正常走随机起点,流程不报错。"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=lambda aid: _make_asset_with_scenes(aid, 30.0, None))
mock_plan_svc = MagicMock()
mock_plan_svc.replace_all_clips_transactional = MagicMock(return_value=2)
segments = [(0, 3.0, 5.0), (1, 3.0, 5.0)]
with (
patch(
"app.api.routes.templates_editor.clips._get_template_segments",
return_value=segments,
),
patch("app.api.routes.templates_editor.clips.get_used_segments", return_value={}),
patch("app.api.routes.templates_editor.clips.record_used_segments", return_value=None),
):
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=2)
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id="plan-scene-2",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
clips_data = mock_plan_svc.replace_all_clips_transactional.call_args.args[1]
assert len(clips_data) == 2
assert all(c["start_time"] is not None for c in clips_data)
# ── 后台任务:检测结果写缓存 ──────────────────────────────────────────────────
class TestAsyncCacheWrite:
def test_scene_points_persisted_to_metadata(self):
"""detect_scene_changes 返回结果后写入素材 metadata 并调用 repo.update。"""
from app.api.routes.templates_editor import clips as clips_module
detected_points = [0.0, 4.5, 9.0, 14.2]
mock_asset = MagicMock()
mock_asset.id = "a1"
mock_asset.duration = 20.0
mock_asset.storage_key = "v.mp4"
mock_asset.mime_type = "video/mp4"
mock_asset.metadata = {}
mock_asset_repo = MagicMock()
mock_asset_repo.find_by_ids = MagicMock(return_value=[mock_asset])
mock_asset_repo.update = MagicMock(side_effect=lambda a: a)
mock_clip = MagicMock()
mock_clip.id = "clip-1"
mock_clip.asset_id = "a1"
mock_clip.order = 0
mock_clip.start_time = 2.0
mock_clip.duration = 4.0
mock_plan_svc = MagicMock()
mock_plan_svc.list_clips = MagicMock(return_value=[mock_clip])
mock_plan_svc.update_clip = MagicMock()
plan_svc_factory = MagicMock(return_value=mock_plan_svc)
mock_client = MagicMock()
mock_client.is_available = True
mock_client.detect_scene_changes = MagicMock(return_value=detected_points)
mock_storage = MagicMock()
mock_storage.get_download_url = MagicMock(return_value="https://example.com/v.mp4")
mock_session = MagicMock()
with (
patch(
"packages.adapters.sqlalchemy_impl.asset_repository.SQLAlchemyAssetRepository",
return_value=mock_asset_repo,
),
patch(
"app.api.routes.templates_editor.clips.EditPlanService",
plan_svc_factory,
),
patch(
"packages.adapters.sqlalchemy_impl.session.SessionLocal",
MagicMock(return_value=mock_session),
),
patch(
"app.api.routes.templates_editor.clips.get_storage_service",
return_value=mock_storage,
),
patch(
"app.api.routes.templates_editor.clips.get_mediakit_client",
return_value=mock_client,
),
patch(
"app.api.routes.templates_editor.clips.get_used_segments",
return_value={},
),
patch(
"app.api.routes.templates_editor.clips.record_used_segments",
return_value=None,
),
patch(
"app.api.routes.templates_editor.clips.remove_used_segment",
return_value=False,
),
):
clips_module._update_mediakit_recommendations_async("plan-1", ["a1"])
# 验证素材 metadata 被写入场景点并持久化
assert mock_asset.metadata.get("scene_change_points") == detected_points
mock_asset_repo.update.assert_called()
+154 -3
View File
@@ -23,7 +23,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
import pytest
from packages.domain.smart_match import score_asset, smart_select_assets
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX, score_asset, smart_select_assets
# ── 辅助工厂 ──────────────────────────────────────────────────────────────────
@@ -158,6 +158,38 @@ def _make_auth_user():
return auth
def _make_zero_noise_patcher(module):
"""构造 patch(module.random.uniform):噪声调用(上界=SCORE_RANDOM_NOISE_MAX)返回 0。
其他 uniform 调用(如片段时长随机)委托给一个独立的 Random 实例,
避免递归回已 patch 的全局函数。
"""
import random as _stdlib_random
_fallback = _stdlib_random.Random()
def _fake_uniform(a, b):
if b == SCORE_RANDOM_NOISE_MAX:
return 0.0
return _fallback.uniform(a, b)
return patch.object(module.random, "uniform", _fake_uniform)
def _patch_zero_noise_clips():
"""消除 clips.py 排序噪声,其他 uniform 调用不受影响。"""
from app.api.routes.templates_editor import clips as clips_module
return _make_zero_noise_patcher(clips_module)
def _patch_zero_noise_plan_service():
"""消除 plan_generator_service.py 排序噪声,其他 uniform 调用不受影响。"""
from app.services import plan_generator_service as svc_module
return _make_zero_noise_patcher(svc_module)
class TestFromAssetsSmartMatchIntegration:
"""验证 clips.py 中 sorted_candidates 使用 smart_match 评分。"""
@@ -180,6 +212,7 @@ class TestFromAssetsSmartMatchIntegration:
segments = [(0, 3.0, 5.0), (1, 3.0, 5.0)]
with (
_patch_zero_noise_clips(),
patch(
"app.api.routes.templates_editor.clips._get_template_segments",
return_value=segments,
@@ -219,6 +252,63 @@ class TestFromAssetsSmartMatchIntegration:
first_clip_asset == "a_fresh"
), f"第一个片段应分配给 smart_match 分更高的 a_fresh,实际是 {first_clip_asset}"
def test_score_noise_causes_varied_selection(self):
"""得分接近(差距 < SCORE_RANDOM_NOISE_MAX)的素材,多次生成的素材组合应有变化。
两条同等质量/时长/新鲜度的素材(use_count 相同),smart_match 得分一致,
噪声让两者的相对排名随机浮动,多次调用首个片段的素材分布应两者都出现。
"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
def _get_asset(aid):
return _make_mock_asset_for_clips(aid, 30.0, 0)
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
segments = [(0, 3.0, 5.0), (1, 3.0, 5.0)]
first_assets: set[str] = set()
for _ in range(30):
mock_plan_svc = MagicMock()
mock_plan_svc.replace_all_clips_transactional = MagicMock(return_value=2)
with (
patch(
"app.api.routes.templates_editor.clips._get_template_segments",
return_value=segments,
),
patch(
"app.api.routes.templates_editor.clips.get_used_segments",
return_value={},
),
patch(
"app.api.routes.templates_editor.clips.record_used_segments",
return_value=None,
),
):
body = ClipsFromAssetsRequest(
asset_ids=["a_x", "a_y"],
required_clips_count=2,
)
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id=f"plan-noise-{len(first_assets)}-{_}",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
clips_data = mock_plan_svc.replace_all_clips_transactional.call_args.args[1]
first_assets.add(clips_data[0]["asset_id"])
assert first_assets == {
"a_x",
"a_y",
}, f"噪声应使两条等分素材的排名浮动,30 次调用首个片段应覆盖两者,实际 {first_assets}"
# ── 一键生成路径集成测试 ─────────────────────────────────────────────────────
@@ -247,7 +337,8 @@ class TestPlanGeneratorSmartMatchIntegration:
db = MagicMock()
svc = PlanGeneratorService(db, asset_repo=mock_asset_repo)
sorted_ids = svc._sort_assets_by_smart_score(["high_use", "low_use", "mid_use"])
with _patch_zero_noise_plan_service():
sorted_ids = svc._sort_assets_by_smart_score(["high_use", "low_use", "mid_use"])
# low_use (0次) 应排第一,high_use (10次) 应排最后
assert sorted_ids[0] == "low_use"
@@ -284,7 +375,10 @@ class TestPlanGeneratorSmartMatchIntegration:
EditPlanClip(id="c2", plan_id="p1", clip_type="main", duration=5.0, order=1),
]
with patch("app.services.plan_generator_service.distribute_assets") as mock_dist:
with (
_patch_zero_noise_plan_service(),
patch("app.services.plan_generator_service.distribute_assets") as mock_dist,
):
svc._distribute_assets(
clips,
["old_asset", "new_asset"],
@@ -322,3 +416,60 @@ class TestPlanGeneratorSmartMatchIntegration:
)
# random_selection=True 时不应调用 asset_repo.get(不执行排序)
mock_asset_repo.get.assert_not_called()
class TestPlanGeneratorScoreNoise:
"""验证一键生成路径的评分排序注入了随机噪声。"""
def test_equal_scores_produce_varied_order(self):
"""两条 smart_match 得分相同的素材,多次排序的首位应覆盖两者。"""
from app.services.plan_generator_service import PlanGeneratorService
def _get_asset(aid):
asset = MagicMock()
asset.id = aid
asset.duration = 15.0
asset.quality_score = None
asset.created_at = None
asset.metadata = {"generation_use_count": 0}
return asset
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
svc = PlanGeneratorService(MagicMock(), asset_repo=mock_asset_repo)
first_ids: set[str] = set()
for _ in range(30):
order = svc._sort_assets_by_smart_score(["equal_a", "equal_b"])
first_ids.add(order[0])
assert first_ids == {
"equal_a",
"equal_b",
}, f"噪声应使等分素材排名浮动,30 次排序首位应覆盖两者,实际 {first_ids}"
def test_large_score_gap_not_flipped(self):
"""得分差距远大于噪声上限时,低分素材不会因噪声超过高分素材。
quality 100 vs 0 → quality 维度差距 40 分 > 噪声上限 20,
其余维度完全一致,50 次排序高质量素材必须始终排第一。
"""
from app.services.plan_generator_service import PlanGeneratorService
def _get_asset(aid):
quality = {"top": 100.0, "bad": 0.0}[aid]
asset = MagicMock()
asset.id = aid
asset.duration = 15.0
asset.quality_score = quality
asset.created_at = None
asset.metadata = {"generation_use_count": 0}
return asset
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
svc = PlanGeneratorService(MagicMock(), asset_repo=mock_asset_repo)
for _ in range(50):
order = svc._sort_assets_by_smart_score(["top", "bad"])
assert order[0] == "top", f"质量差距 40 分 > 噪声上限,top 应始终排第一,实际 {order}"