Compare commits

..

20 Commits

Author SHA1 Message Date
CI Bot 819d3ae23d fix: prevent docker images grep from failing build with set -e
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1541h46m41s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1541h46m43s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1541h46m43s
CI/CD Pipeline / Build Production Runtime Images (pull_request) Failing after 1541h46m45s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Failing after 1541h46m45s
CI/CD Pipeline / Validate Code Quality And Tests (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 / Staging API Integration Tests (pull_request) Failing after 1542h18m19s
The last line of the build script uses grep to display built images,
but with set -e, grep returning no matches causes the entire script
to fail. This is a classic set -e footgun.

The grep pattern was also broken: docker images shows REPOSITORY and
TAG as separate columns (no ':' between them), so 'xiaoxia-saas.*:'
never matched. Changed to two-stage grep and added || true safety net.

The build and push were actually succeeding - only the final display
line was causing the failure.
2026-07-14 09:39:31 +08:00
xiaoxia 17fbae13a8 fix(ci): 移除构建脚本中docker driver不支持的--cache-to导出 (#302)
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 14m27s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1542h10m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1542h10m46s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1542h25m12s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1542h25m13s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Failing after 1542h56m48s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
fix(ci): 移除构建脚本中docker driver不支持的--cache-to导出
2026-07-14 09:04:18 +08:00
xiaoxia 41e421b44b fix(e2e): Playwright chromium禁用GPU,修复无显示环境下浏览器不稳定问题 (#301)
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 3s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1542h40m39s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1542h40m42s
CI/CD Pipeline / Deploy Production (push) Failing after 1542h40m43s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1542h40m43s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1543h12m16s
fix(e2e): Playwright chromium禁用GPU,修复无显示环境下浏览器不稳定问题
2026-07-14 08:48:42 +08:00
xiaoxia 9f86bd40ca fix(ci): 修复 build_release_images.sh 中 CACHE_TAG 未定义的问题 (#297)
CI/CD Pipeline / Staging E2E Tests (push) Failing after 19m9s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1544h7m25s
CI/CD Pipeline / Deploy Production (push) Failing after 1544h7m26s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1544h7m27s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
2026-07-14 07:22:05 +08:00
xiaoxia 881eea9195 fix(api+ci): DELETE 204响应体全修复 + isort/black整理 + CI环境兼容 + 测试修复 (#286)
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 9s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1544h21m30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1544h21m31s
CI/CD Pipeline / Deploy Production (push) Failing after 1544h21m40s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1544h21m40s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Failing after 1544h53m14s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
fix(api+ci): DELETE 204响应体全修复 + isort/black整理 + CI环境兼容 + 测试修复
2026-07-14 07:07:51 +08:00
CI Test 4faceb8093 fix: DELETE 204路由改用response_model=None并移除return None,彻底解决FastAPI断言错误
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 15s
CI/CD Pipeline / Unit Tests (push) Failing after 11s
CI/CD Pipeline / Integration Tests (push) Failing after 5s
CI/CD Pipeline / Frontend Lint (push) Failing after 2m13s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 1552h27m50s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1552h27m49s
CI/CD Pipeline / Deploy Production (push) Failing after 1552h59m23s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1552h27m47s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1552h27m48s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1552h27m48s
2026-07-13 22:52:53 +08:00
xiaoxia 37293a665d fix: DELETE 204 路由添加 response_class=Response 避免 FastAPI 断言错误
CI/CD Pipeline / Unit Tests (push) Failing after 8s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 15s
CI/CD Pipeline / Integration Tests (push) Failing after 16s
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Build & Push 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 Runtime Images (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
2026-07-13 22:42:08 +08:00
xiaoxia 006fd075d6 Merge pull request 'Phase 4 Backend P2 cleanup: 异常处理/类型注解/模块拆分/大函数拆分/配置项确认' (#279) from cleanup/phase4-backend-p0 into develop
CI/CD Pipeline / Unit Tests (push) Failing after 8s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 10s
CI/CD Pipeline / Integration Tests (push) Failing after 5s
CI/CD Pipeline / Frontend Lint (push) Failing after 3m5s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1553h16m11s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1553h16m12s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1553h16m12s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1553h16m13s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 1553h16m13s
CI/CD Pipeline / Deploy Production (push) Failing after 1553h47m48s
2026-07-13 22:11:05 +08:00
xiaoxia b562e152e0 Merge pull request 'refactor(frontend): Phase 4 前端 P2 — ui.css 死代码清理(Table/Form/Pagination 残留样式)' (#278) from cleanup/phase4-frontend-p2 into develop
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 8s
CI/CD Pipeline / Integration Tests (push) Failing after 6s
CI/CD Pipeline / Unit Tests (push) Failing after 19s
CI/CD Pipeline / Frontend Lint (push) Failing after 1m56s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1554h51m26s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1554h51m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1554h51m26s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1554h51m27s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 1554h51m27s
CI/CD Pipeline / Deploy Production (push) Failing after 1555h23m2s
2026-07-13 20:36:58 +08:00
xiaoxia 20f6e64847 refactor(ui): 清理 Table/Form/Pagination 死代码(~95行)
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 11s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 11s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 4s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m38s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1556h8m6s
CI/CD Pipeline / Build Production Runtime Images (pull_request) Failing after 1556h8m8s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Failing after 1556h8m8s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1556h8m7s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1556h8m7s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1556h39m43s
删除已移除组件的关联样式:
- .xx-table 全节(9个规则)
- .xx-form / .xx-form-compact 全节(4个规则)
- .xx-pagination 全节(8个规则)
- 响应式中 .xx-form 规则

全项目 grep 确认零引用后删除
2026-07-13 19:20:07 +08:00
Deploy Agent 6897a4be96 fix(frontend): 移除 Dashboard.tsx 未使用的 Tag import
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 9s
CI/CD Pipeline / Unit Tests (push) Failing after 7s
CI/CD Pipeline / Integration Tests (push) Failing after 5s
CI/CD Pipeline / Frontend Lint (push) Failing after 1m25s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 1556h38m55s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1556h38m55s
CI/CD Pipeline / Deploy Production (push) Failing after 1557h10m30s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1556h38m53s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1556h38m54s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1556h38m54s
TS6133: 'Tag' is declared but its value is never read.
Phase 3 Mock 替换后遗留的未使用 import。
2026-07-13 18:25:29 +08:00
xiaoxia 6d2d5abff2 refactor(frontend): Phase 4 前端 P0+P1 清理
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 6s
CI/CD Pipeline / Unit Tests (push) Failing after 9s
CI/CD Pipeline / Integration Tests (push) Failing after 7s
CI/CD Pipeline / Frontend Lint (push) Failing after 1m7s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1557h7m57s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 1557h8m1s
CI/CD Pipeline / Deploy Production (push) Failing after 1557h7m59s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1557h8m0s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1557h8m0s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1557h39m36s
Merge PR #275: refactor(frontend): Phase 4 前端 P0+P1 清理 — CSS 死代码 + 未使用组件删除 into develop
2026-07-13 18:21:14 +08:00
xiaoxia e3f5ab5611 fix(accounts): 加回误删的 acc-account-list + 清除响应式死代码
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 23s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 10s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 52s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m11s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1557h12m31s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1557h12m32s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1557h12m33s
CI/CD Pipeline / Build Production Runtime Images (pull_request) Failing after 1557h12m34s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Failing after 1557h12m34s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1557h44m7s
- 加回 acc-account-list(Accounts.tsx 第89行引用)
- 清除 480px 响应式中残留的 acc-account-row / acc-account-avatar
2026-07-13 18:16:36 +08:00
xiaoxia d061bccdd2 Phase 4 Backend P0 cleanup
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 6s
CI/CD Pipeline / Unit Tests (push) Failing after 9s
CI/CD Pipeline / Integration Tests (push) Failing after 5s
CI/CD Pipeline / Frontend Lint (push) Failing after 1m10s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1557h24m20s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 1557h24m21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1557h24m22s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Failing after 1557h24m23s
CI/CD Pipeline / Deploy Production (push) Failing after 1557h55m56s
CI/CD Pipeline / Build Production Runtime Images (push) Failing after 1557h55m58s
Merge PR #276: Phase 4 Backend P0 cleanup into develop
2026-07-13 18:04:49 +08:00
xiaoxia 97a81a39f3 refactor(ui): 删除未使用组件 Form.tsx(全项目无引用)
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 9s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 4s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 45s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m39s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1557h48m42s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1557h48m44s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1557h48m43s
CI/CD Pipeline / Build Production Runtime Images (pull_request) Failing after 1557h48m45s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Failing after 1557h48m45s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1558h20m18s
2026-07-13 17:39:13 +08:00
xiaoxia eab471703e refactor(ui): 删除未使用组件 Pagination.tsx(全项目无引用) 2026-07-13 17:39:13 +08:00
xiaoxia 32a53ab9e6 refactor(ui): 删除未使用组件 Table.tsx(全项目无引用) 2026-07-13 17:39:12 +08:00
xiaoxia e50ba67c11 refactor(ui): 移除未使用组件导出(Table / Form / Pagination) 2026-07-13 17:39:12 +08:00
xiaoxia e8f9e2dabe refactor(accounts): 清理旧版列表视图样式(8个无引用类名,约64行)
移除:acc-account-list / acc-account-row / acc-account-avatar /
acc-account-info / acc-account-name / acc-status-pill /
acc-status--active / acc-status--expired / acc-status--limited
2026-07-13 17:39:12 +08:00
xiaoxia 3f773795ed refactor(dashboard): 清理 CSS 死代码(36个无引用类名→移除)+ 新增 xx-dashboard-empty 空状态样式
移除的未使用类名:
- xx-dashboard-welcome / xx-dashboard-main / xx-dashboard-left-col
- xx-kpi-card / xx-kpi-icon / xx-kpi-value / xx-kpi-label / xx-kpi-trend-*
- xx-task-item / xx-task-info / xx-task-status-* / xx-task-time / xx-task-action
- xx-chart-bar-wrapper / xx-chart-bar / xx-chart-bar-value / xx-chart-labels / xx-chart-label / xx-chart-total
- xx-quick-card / xx-quick-card-icon
- xx-storage-bar-* / xx-storage-section*
- xx-dashboard-section--start

新增:xx-dashboard-empty(P0 修复 — Dashboard 空状态裸奔)
2026-07-13 17:39:11 +08:00
42 changed files with 240 additions and 767 deletions
Executable → Regular
+70 -10
View File
@@ -92,9 +92,9 @@ jobs:
shell: sh
run: |
set -eu
python3 -m pip install --break-system-packages -q -r requirements-base.txt
python3 -m pip install --break-system-packages -q -r requirements.txt
python3 -m pip install --break-system-packages -q -r requirements-dev.txt
python3 -m pip install -q -r requirements-base.txt
python3 -m pip install -q -r requirements.txt
python3 -m pip install -q -r requirements-dev.txt
python3 -m black --version
python3 -m isort --version-number
python3 -m flake8 --version
@@ -169,6 +169,10 @@ jobs:
env:
USE_IN_MEMORY_DB: "true"
OSS_ACCESS_KEY_ID: placeholder
OSS_ACCESS_KEY_SECRET: placeholder
OSS_BUCKET_NAME: xiaoxia-autocut
OSS_ENDPOINT: oss-cn-hangzhou.aliyuncs.com
steps:
- name: Checkout code
@@ -217,13 +221,39 @@ jobs:
tar.extract(member, '.')
PY
- name: Install ffmpeg
shell: sh
run: |
set +e
if command -v ffmpeg > /dev/null 2>&1; then
echo "ffmpeg already installed: $(ffmpeg -version | head -1)"
exit 0
fi
if command -v apt-get > /dev/null 2>&1; then
apt-get update -qq && apt-get install -y -qq ffmpeg
elif command -v yum > /dev/null 2>&1; then
yum install -y -q epel-release 2>/dev/null
yum install -y -q ffmpeg 2>/dev/null
if [ $? -ne 0 ] && command -v dnf > /dev/null 2>&1; then
dnf install -y -q --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm 2>/dev/null
dnf install -y -q ffmpeg 2>/dev/null
fi
elif command -v dnf > /dev/null 2>&1; then
dnf install -y -q ffmpeg 2>/dev/null
fi
if command -v ffmpeg > /dev/null 2>&1; then
echo "ffmpeg installed successfully: $(ffmpeg -version | head -1)"
else
echo "Warning: ffmpeg installation failed or not available, some tests may be skipped"
fi
- name: Install dependencies
shell: sh
run: |
set -eu
python3 -m pip install --break-system-packages -q -r requirements-base.txt
python3 -m pip install --break-system-packages -q -r requirements.txt
python3 -m pip install --break-system-packages -q -r requirements-dev.txt
python3 -m pip install -q -r requirements-base.txt
python3 -m pip install -q -r requirements.txt
python3 -m pip install -q -r requirements-dev.txt
pytest --version
- name: Run unit tests with coverage
@@ -260,6 +290,10 @@ jobs:
env:
DATABASE_URL: postgresql+psycopg://postgres:postgres@127.0.0.1:5432/xiaoxia_saas
USE_IN_MEMORY_DB: "false"
OSS_ACCESS_KEY_ID: placeholder
OSS_ACCESS_KEY_SECRET: placeholder
OSS_BUCKET_NAME: xiaoxia-autocut
OSS_ENDPOINT: oss-cn-hangzhou.aliyuncs.com
steps:
- name: Checkout code
@@ -320,11 +354,37 @@ jobs:
shell: sh
run: |
set -eu
python3 -m pip install --break-system-packages -q -r requirements-base.txt
python3 -m pip install --break-system-packages -q -r requirements.txt
python3 -m pip install --break-system-packages -q -r requirements-dev.txt
python3 -m pip install -q -r requirements-base.txt
python3 -m pip install -q -r requirements.txt
python3 -m pip install -q -r requirements-dev.txt
pytest --version
- name: Install ffmpeg
shell: sh
run: |
set +e
if command -v ffmpeg > /dev/null 2>&1; then
echo "ffmpeg already installed: $(ffmpeg -version | head -1)"
exit 0
fi
if command -v apt-get > /dev/null 2>&1; then
apt-get update -qq && apt-get install -y -qq ffmpeg
elif command -v yum > /dev/null 2>&1; then
yum install -y -q epel-release 2>/dev/null
yum install -y -q ffmpeg 2>/dev/null
if [ $? -ne 0 ] && command -v dnf > /dev/null 2>&1; then
dnf install -y -q --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm 2>/dev/null
dnf install -y -q ffmpeg 2>/dev/null
fi
elif command -v dnf > /dev/null 2>&1; then
dnf install -y -q ffmpeg 2>/dev/null
fi
if command -v ffmpeg > /dev/null 2>&1; then
echo "ffmpeg installed successfully: $(ffmpeg -version | head -1)"
else
echo "Warning: ffmpeg installation failed or not available, some tests may be skipped"
fi
- name: Start Redis
shell: sh
run: |
@@ -394,7 +454,7 @@ jobs:
shell: sh
run: |
set -eu
pip install --break-system-packages -q pytest-rerunfailures
python3 -m pip install -q pytest-rerunfailures
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run --append \
--source=apps/api/app,packages \
--omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \
+2 -2
View File
@@ -12,7 +12,7 @@ from app.schemas.asset_library import (
EnsureDefaultLibraryRequest,
ListAssetLibrariesResponse,
)
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from packages.application import (
CreateAssetLibraryCommand,
@@ -146,7 +146,7 @@ def ensure_default_library(
return _to_asset_library_response(created)
@router.delete("/{library_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/{library_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response)
def delete_asset_library(
library_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
+4 -6
View File
@@ -1,6 +1,7 @@
import logging
from typing import Any, Optional
from app.api.routes._helpers import check_project_access
from app.auth import AuthenticatedUser, get_current_user
from app.core.storage import get_storage_service
from app.dependencies import (
@@ -19,7 +20,7 @@ from app.schemas.asset import (
UpdateAssetReviewRequest,
)
from app.schemas.tag import TagAssetsRequest
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from packages.application import (
CreateAssetCommand,
@@ -27,8 +28,6 @@ from packages.application import (
)
from packages.domain import AssetStatus, ClassificationStatus
from app.api.routes._helpers import check_project_access
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -74,7 +73,6 @@ def _to_asset_response(item, storage_service=None) -> AssetResponse:
)
@router.get("", response_model=ListAssetsResponse)
def list_assets(
library_id: Optional[str] = Query(None),
@@ -330,7 +328,7 @@ def update_asset(
return _to_asset_response(updated)
@router.delete("/{asset_id}", status_code=204)
@router.delete("/{asset_id}", status_code=204, response_class=Response)
def delete_asset(
asset_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -369,7 +367,7 @@ def tag_asset(
return _to_asset_response(updated)
@router.delete("/{asset_id}/tags/{tag_id}", status_code=204)
@router.delete("/{asset_id}/tags/{tag_id}", status_code=204, response_class=Response)
def untag_asset(
asset_id: str,
tag_id: str,
+1 -2
View File
@@ -13,6 +13,7 @@ from pathlib import Path
from typing import Any
from uuid import uuid4
from app.api.routes._helpers import require_project_and_library
from app.auth import AuthenticatedUser, get_current_user
from app.core.celery_app import celery_app
from app.core.storage import OSSStorageService, get_storage_service
@@ -34,8 +35,6 @@ from fastapi.params import File
from packages.application import GetProjectUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase
from app.api.routes._helpers import require_project_and_library
router = APIRouter()
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -239,7 +239,7 @@ def get_duplication_detail(
return _to_detail_response(record)
@router.delete("/records/{record_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
@router.delete("/records/{record_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_duplication_record(
record_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -257,7 +257,7 @@ def delete_duplication_record(
use_case = DeleteDuplicationRecordUseCase(duplication_repository)
use_case.execute(record_id)
return Response(status_code=204)
return
@router.post("/records/{record_id}/retry", response_model=DuplicationUploadResponse)
+4 -3
View File
@@ -25,14 +25,15 @@ from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_db_session, get_project_repository
from app.schemas.generation_task import GenerationTaskResponse
from app.services import EditPlanService
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from ._helpers import check_project_access
from packages.domain.config_schemas import normalize_plan_config
from packages.domain.edit_plan import EditPlan, EditPlanStatus
from ._helpers import check_project_access
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -421,7 +422,7 @@ def update_plan(
return _to_response(result)
@router.delete("/{plan_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
@router.delete("/{plan_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_plan(
plan_id: str,
db: Session = Depends(get_db_session),
@@ -15,8 +15,8 @@ from app.api.routes._helpers import check_project_access
from app.api.routes.edit_plans import (
ClipStatusItem,
EditPlanGenerateResponse,
EditPlanGenerationStatusResponse,
EditPlanGenerationsResponse,
EditPlanGenerationStatusResponse,
)
from app.auth import AuthenticatedUser, get_current_user
from app.core.celery_app import celery_app
+4 -4
View File
@@ -19,7 +19,7 @@ from typing import Optional
from app.api.routes.auth import _verify_internal_api_key
from app.config import settings
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from pydantic import BaseModel, Field
from packages.adapters.redis.feature_flag_store import (
@@ -173,12 +173,12 @@ async def update_feature_flag(
raise HTTPException(status_code=500, detail=f"Failed to update flag: {exc}")
@router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/{name}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
async def delete_feature_flag(
name: str,
_: bool = Depends(_verify_internal_api_key),
store: RedisFeatureFlagStore = Depends(_get_feature_flag_store),
) -> None:
) :
"""删除 Feature Flag。
只允许删除 ALLOWED_FLAGS 列表中的 flag。
@@ -188,7 +188,7 @@ async def delete_feature_flag(
try:
deleted = store.delete(name)
logger.info("Feature flag deleted: name=%s deleted=%s", name, deleted)
return None
pass
except Exception as exc:
logger.error("Failed to delete feature flag %s: %s", name, exc)
raise HTTPException(status_code=500, detail=f"Failed to delete flag: {exc}")
+1 -2
View File
@@ -3,6 +3,7 @@ import random
import uuid
from typing import Any
from app.api.routes._helpers import check_project_access
from app.auth import AuthenticatedUser, get_current_user
from app.core.storage import OSSStorageService, get_storage_service
from app.core.task_enqueue import (
@@ -31,8 +32,6 @@ from app.schemas.generation_task import (
)
from fastapi import APIRouter, Depends, HTTPException
from app.api.routes._helpers import check_project_access
from packages.application import (
CreateGenerationTaskCommand,
CreateGenerationTaskUseCase,
+3 -3
View File
@@ -7,7 +7,7 @@ from app.schemas.project import (
ListProjectsResponse,
ProjectResponse,
)
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, Response, status
from packages.application import (
CreateProjectCommand,
@@ -72,7 +72,7 @@ def create_project(
return _to_project_response(project)
@router.delete("/{project_id}")
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_project(
project_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -88,4 +88,4 @@ def delete_project(
)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
return {"message": "Project deleted successfully"}
return
+2 -2
View File
@@ -10,7 +10,7 @@ from app.schemas.tag import (
ListTagsResponse,
TagResponse,
)
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Response
from packages.domain import Tag
@@ -52,7 +52,7 @@ def create_tag(
return TagResponse(id=created.id, name=created.name, created_at=created.created_at)
@router.delete("/{tag_id}", status_code=204)
@router.delete("/{tag_id}", status_code=204, response_class=Response)
def delete_tag(
tag_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
+4 -4
View File
@@ -206,7 +206,7 @@ def update_template(
return _to_response(template)
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_template(
template_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -217,7 +217,7 @@ def delete_template(
deleted = use_case.execute(template_id, user_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Template not found")
return Response(status_code=204)
return
@router.post("/{template_id}/toggle-favorite", response_model=ToggleFavoriteResponse)
@@ -307,7 +307,7 @@ def create_category(
)
@router.delete("/categories/{category_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
@router.delete("/categories/{category_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_category(
category_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -318,4 +318,4 @@ def delete_category(
deleted = use_case.execute(category_id, user_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found")
return Response(status_code=204)
return
+3 -4
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Optional
from app.api.routes._helpers import get_user_plan
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_db_session, get_user_repository
from app.schemas.title_library import (
@@ -28,8 +29,6 @@ from packages.application.title_library.use_cases import (
)
from packages.ports.user_repository import UserRepository
from app.api.routes._helpers import get_user_plan
router = APIRouter()
@@ -138,7 +137,7 @@ def update_title(
return _to_response(item)
@router.delete("/{title_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
@router.delete("/{title_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_title(
title_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -149,4 +148,4 @@ def delete_title(
deleted = use_case.execute(title_id, user_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Title not found")
return Response(status_code=204)
return
+2 -2
View File
@@ -241,7 +241,7 @@ def get_tts_job_status(
)
@router.delete("/jobs/{job_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
@router.delete("/jobs/{job_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_tts_job(
job_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -253,7 +253,7 @@ def delete_tts_job(
deleted = use_case.execute(job_id, user_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="TTS job not found")
return Response(status_code=204)
return
@router.post(
+1 -2
View File
@@ -2,6 +2,7 @@ import logging
from typing import Any
from uuid import uuid4
from app.api.routes._helpers import require_project_and_library
from app.auth import AuthenticatedUser, get_current_user
from app.config import get_settings
from app.core.celery_app import celery_app
@@ -23,8 +24,6 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, s
from packages.application import SubmitIngestJobCommand, SubmitIngestJobUseCase
from app.api.routes._helpers import require_project_and_library
logger = logging.getLogger(__name__)
router = APIRouter()
+2 -1
View File
@@ -172,6 +172,7 @@ def get_voice_clone_status(
"/{clone_id}",
status_code=status.HTTP_204_NO_CONTENT,
response_model=None,
response_class=Response,
)
def delete_voice_clone(
clone_id: str,
@@ -184,7 +185,7 @@ def delete_voice_clone(
deleted = use_case.execute(clone_id, user_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Voice clone not found")
return Response(status_code=204)
return
@router.post("/{clone_id}/retry", response_model=VoiceCloneProfileResponse)
+3 -4
View File
@@ -7,6 +7,7 @@ from __future__ import annotations
from typing import Literal, Optional
from app.api.routes._helpers import get_user_plan
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_audio_url_signer, get_db_session, get_user_repository
from app.schemas.voice import (
@@ -39,8 +40,6 @@ from packages.application.voice_library.use_cases import (
from packages.domain.preset_voices import PRESET_VOICES
from packages.ports.user_repository import UserRepository
from app.api.routes._helpers import get_user_plan
router = APIRouter()
@@ -323,7 +322,7 @@ def update_voice(
return _to_response(item, sign_url)
@router.delete("/{voice_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
@router.delete("/{voice_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_voice(
voice_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -334,4 +333,4 @@ def delete_voice(
deleted = use_case.execute(voice_id, user_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Voice not found")
return Response(status_code=204)
return
+1 -1
View File
@@ -5,8 +5,8 @@ This module keeps old import paths working so existing code
does not need to change.
"""
from packages.shared.storage import SharedStorageService as OSSStorageService
from packages.shared.storage import (
SharedStorageService as OSSStorageService,
get_shared_storage_service,
get_storage_service,
)
-1
View File
@@ -13,7 +13,6 @@ from __future__ import annotations
import logging
from typing import Any
from packages.application.jobs import (
CancelJobUseCase,
CompleteJobCommand,
+6
View File
@@ -26,6 +26,9 @@ export default defineConfig({
use: {
...devices["Desktop Chrome"],
channel: process.env.E2E_BROWSER_CHANNEL || "msedge",
launchOptions: {
args: ["--disable-gpu", "--disable-software-rasterizer"],
},
},
},
{
@@ -51,6 +54,9 @@ export default defineConfig({
use: {
...devices["Desktop Chrome"],
channel: process.env.E2E_BROWSER_CHANNEL || "msedge",
launchOptions: {
args: ["--disable-gpu", "--disable-software-rasterizer"],
},
},
},
],
-37
View File
@@ -1,37 +0,0 @@
/**
* V21 Form 表单
* 封装 Ant Design Form,应用 V21 设计系统样式
*/
import React from "react";
import { Form as AntForm } from "antd";
import type { FormProps as AntFormProps } from "antd";
import classNames from "classnames";
import "./ui.css";
export interface FormProps extends AntFormProps {
/** 紧凑模式(减小表单项间距) */
compact?: boolean;
}
const Form = ({ className, compact, children, ...rest }: FormProps) => {
const v21Class = classNames(
"xx-form",
compact && "xx-form-compact",
className,
);
return (
<AntForm
className={v21Class}
{...(rest as Omit<FormProps, "className" | "compact" | "children">)}
>
{children as React.ReactNode}
</AntForm>
);
};
/** 导出 Form 的子组件(保持 antd API 一致) */
export const FormItem = AntForm.Item;
export const FormList = AntForm.List;
export const FormProvider = AntForm.Provider;
export default Form;
-25
View File
@@ -1,25 +0,0 @@
/**
* V21 Pagination 分页
* 封装 Ant Design Pagination,应用 V21 设计系统样式
*/
import React from "react";
import { Pagination as AntPagination } from "antd";
import type { PaginationProps as AntPaginationProps } from "antd";
import classNames from "classnames";
import "./ui.css";
export interface PaginationProps extends AntPaginationProps {
/** 使用 V21 样式 */
v21?: boolean;
}
const Pagination: React.FC<PaginationProps> = ({
className,
v21 = true,
...rest
}) => {
const v21Class = classNames(v21 && "xx-pagination", className);
return <AntPagination className={v21Class} {...rest} />;
};
export default Pagination;
-29
View File
@@ -1,29 +0,0 @@
/**
* V21 Table 表格
* 封装 Ant Design Table,应用 V21 设计系统样式
*/
import React from "react";
import { Table as AntTable } from "antd";
import type { TableProps as AntTableProps } from "antd";
import classNames from "classnames";
import "./ui.css";
export interface TableProps<
RecordType = unknown,
> extends AntTableProps<RecordType> {
/** 使用 V21 样式 */
v21?: boolean;
}
function Table<RecordType extends object = Record<string, unknown>>({
className,
v21 = true,
...rest
}: TableProps<RecordType>) {
const v21Class = classNames(v21 && "xx-table", className);
return <AntTable<RecordType> className={v21Class} {...rest} />;
}
export default Table as <RecordType extends object = Record<string, unknown>>(
props: TableProps<RecordType> & React.RefAttributes<HTMLDivElement>,
) => React.ReactElement;
-9
View File
@@ -15,9 +15,6 @@ export type { SelectProps } from "./Select";
export { default as Modal } from "./Modal";
export type { ModalProps } from "./Modal";
export { default as Table } from "./Table";
export type { TableProps } from "./Table";
export { default as Card } from "./Card";
export type { CardProps } from "./Card";
@@ -26,9 +23,3 @@ export type { TagProps, TagVariant } from "./Tag";
export { Tooltip, Popover } from "./Tooltip";
export type { TooltipProps, PopoverProps } from "./Tooltip";
export { default as Form, FormItem, FormList, FormProvider } from "./Form";
export type { FormProps } from "./Form";
export { default as Pagination } from "./Pagination";
export type { PaginationProps } from "./Pagination";
-110
View File
@@ -258,51 +258,6 @@
font-size: 24px !important;
}
/* ============================================================
Table 表格
============================================================ */
.xx-table .ant-table {
background: var(--bg-primary) !important;
color: var(--text-primary) !important;
border-radius: var(--radius-md) !important;
overflow: hidden;
}
.xx-table .ant-table-thead > tr > th {
background: var(--bg-secondary) !important;
color: var(--text-secondary) !important;
font-weight: var(--font-weight-semibold) !important;
border-bottom: 1px solid var(--border-color) !important;
font-size: var(--font-size-sm) !important;
text-transform: uppercase;
letter-spacing: var(--letter-spacing-wide);
}
.xx-table .ant-table-tbody > tr > td {
border-bottom: 1px solid var(--border-light) !important;
color: var(--text-primary) !important;
transition: var(--transition-fast) !important;
}
.xx-table .ant-table-tbody > tr:hover > td {
background: var(--primary-soft) !important;
}
.xx-table .ant-table-tbody > tr:last-child > td {
border-bottom: none !important;
}
/* 排序图标 */
.xx-table .ant-table-column-sorter-up.active,
.xx-table .ant-table-column-sorter-down.active {
color: var(--primary-color) !important;
}
/* 分页 */
.xx-table .ant-pagination {
padding: var(--space-md) 0 !important;
}
/* ============================================================
Card 卡片
============================================================ */
@@ -429,68 +384,6 @@
border-bottom: 1px solid var(--border-light) !important;
}
/* ============================================================
Form 表单
============================================================ */
.xx-form .ant-form-item-label > label {
color: var(--text-primary) !important;
font-weight: var(--font-weight-medium) !important;
font-size: var(--font-size-base) !important;
}
.xx-form .ant-form-item-explain-error {
color: var(--error-color) !important;
font-size: var(--font-size-sm) !important;
}
.xx-form .ant-form-item {
margin-bottom: var(--space-lg) !important;
}
/* 表单项间距紧凑 */
.xx-form-compact .ant-form-item {
margin-bottom: var(--space-md) !important;
}
/* ============================================================
Pagination 分页
============================================================ */
.xx-pagination .ant-pagination-item {
border-radius: var(--radius-xs) !important;
border-color: var(--border-color) !important;
transition: var(--transition-fast) !important;
}
.xx-pagination .ant-pagination-item a {
color: var(--text-primary) !important;
}
.xx-pagination .ant-pagination-item:hover {
border-color: var(--primary-color) !important;
}
.xx-pagination .ant-pagination-item:hover a {
color: var(--primary-color) !important;
}
.xx-pagination .ant-pagination-item-active {
background: var(--gradient-primary) !important;
border-color: transparent !important;
}
.xx-pagination .ant-pagination-item-active a {
color: var(--text-inverse) !important;
}
.xx-pagination .ant-pagination-prev .ant-pagination-item-link,
.xx-pagination .ant-pagination-next .ant-pagination-item-link {
border-radius: var(--radius-xs) !important;
color: var(--text-secondary) !important;
}
.xx-pagination .ant-pagination-disabled .ant-pagination-item-link {
color: var(--text-disabled) !important;
}
/* ============================================================
响应式
@@ -517,9 +410,6 @@
padding: var(--space-md) !important;
}
.xx-form .ant-form-item {
margin-bottom: var(--space-md) !important;
}
}
@media (max-width: 480px) {
+1 -5
View File
@@ -13,11 +13,7 @@ import "./accounts.css";
/* ── 类型定义 ───────────────────────────────────────────── */
export type PlatformId =
| "douyin"
| "kuaishou"
| "xiaohongshu"
| "wechat";
export type PlatformId = "douyin" | "kuaishou" | "xiaohongshu" | "wechat";
export interface Platform {
id: PlatformId;
-82
View File
@@ -82,77 +82,6 @@
gap: 10px;
}
/* ── 账号行 ─────────────────────────────────────────────── */
.acc-account-row {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background: var(--bg-secondary);
border-radius: var(--radius-md);
transition: var(--transition-fast);
}
.acc-account-row:hover {
background: var(--primary-soft);
}
.acc-account-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: grid;
place-items: center;
color: var(--text-inverse);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-sm);
flex-shrink: 0;
}
.acc-account-info {
flex: 1;
min-width: 0;
}
.acc-account-name {
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── 状态标签 ───────────────────────────────────────────── */
.acc-status-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: var(--radius-full);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
line-height: 1.6;
margin-top: 2px;
}
.acc-status--active {
background: var(--success-soft);
color: var(--secondary-color);
}
.acc-status--expired {
background: var(--error-soft);
color: var(--error-color);
}
.acc-status--limited {
background: var(--warning-soft);
color: var(--accent-color);
}
/* ── 空状态 ─────────────────────────────────────────────── */
.acc-empty {
@@ -235,17 +164,6 @@
font-size: var(--font-size-base);
}
.acc-account-row {
padding: 10px;
gap: 8px;
}
.acc-account-avatar {
width: 32px;
height: 32px;
font-size: var(--font-size-xs);
}
.acc-stats-bar {
padding: var(--space-sm) var(--space-md);
font-size: var(--font-size-sm);
+17 -5
View File
@@ -5,7 +5,7 @@
*/
import React from "react";
import { useNavigate } from "react-router-dom";
import { Button, Tag } from "@/components/ui";
import { Button } from "@/components/ui";
import { DatabaseOutlined } from "@ant-design/icons";
import "./dashboard.css";
@@ -39,7 +39,11 @@ const Dashboard: React.FC = () => {
<section className="xx-dashboard-section">
<div className="xx-dashboard-section-header">
<h3>最近任务</h3>
<Button buttonType="ghost" buttonSize="sm" onClick={() => navigate("/app/history")}>
<Button
buttonType="ghost"
buttonSize="sm"
onClick={() => navigate("/app/history")}
>
查看全部
</Button>
</div>
@@ -51,7 +55,10 @@ const Dashboard: React.FC = () => {
</section>
{/* 使用统计 */}
<section className="xx-dashboard-section" style={{ marginTop: "var(--space-md)" }}>
<section
className="xx-dashboard-section"
style={{ marginTop: "var(--space-md)" }}
>
<div className="xx-dashboard-section-header">
<h3>使用统计</h3>
</div>
@@ -66,13 +73,18 @@ const Dashboard: React.FC = () => {
</section>
{/* 公告 */}
<section className="xx-dashboard-section" style={{ marginTop: "var(--space-md)" }}>
<section
className="xx-dashboard-section"
style={{ marginTop: "var(--space-md)" }}
>
<div className="xx-dashboard-section-header">
<h3>公告</h3>
</div>
<div className="xx-announcement-list">
<div className="xx-announcement-item">
<span className="xx-announcement-tag xx-announcement-tag--notice">官方</span>
<span className="xx-announcement-tag xx-announcement-tag--notice">
官方
</span>
<div className="xx-announcement-content">
<h4>欢迎使用小应 SaaS 平台</h4>
<time>当前为演示版本,部分功能正在开发中。</time>
+39 -369
View File
@@ -1,6 +1,6 @@
/**
* 控制台页面 - V21 设计系统样式
* KPI 卡片网格 + 快速入口 + 最近任务卡片列表 + 使用统计图表 + 公告
* KPI 卡片网格 + 快速入口 + 最近任务 + 使用统计图表 + 公告
* 统一使用 CSS 变量,支持深色/浅色主题
*/
@import "../../styles/global.css";
@@ -13,26 +13,6 @@
padding: var(--space-xl);
}
/* ============================================================
欢迎头部
============================================================ */
.xx-dashboard-welcome {
margin-bottom: var(--space-lg);
}
.xx-dashboard-welcome h2 {
margin: 0 0 var(--space-xs);
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--text-primary);
}
.xx-dashboard-welcome p {
margin: 0;
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
/* ============================================================
KPI 卡片网格
============================================================ */
@@ -43,76 +23,6 @@
margin-bottom: var(--space-lg);
}
.xx-kpi-card {
background: linear-gradient(180deg, var(--bg-primary), var(--bg-secondary));
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 20px;
transition: 0.18s ease;
position: relative;
overflow: hidden;
}
.xx-kpi-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.xx-kpi-icon {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-lg);
margin-bottom: var(--space-sm);
}
.xx-kpi-value {
font-size: 30px;
font-weight: 800;
color: var(--text-primary);
line-height: 1.2;
margin-bottom: var(--space-xs);
}
.xx-kpi-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--space-sm);
}
.xx-kpi-trend {
font-size: 12px;
margin-top: 8px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.xx-kpi-trend--up {
color: var(--success-color);
}
.xx-kpi-trend--down {
color: var(--error-color);
}
.xx-kpi-trend--neutral {
color: var(--text-tertiary);
}
/* ============================================================
主内容区两栏布局
============================================================ */
.xx-dashboard-main {
display: grid;
grid-template-columns: 1fr 320px;
gap: var(--space-md);
margin-bottom: var(--space-lg);
}
/* ============================================================
区块卡片
============================================================ */
@@ -161,7 +71,25 @@
}
/* ============================================================
最近任务卡片列表
空状态
============================================================ */
.xx-dashboard-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-xl) var(--space-md);
text-align: center;
color: var(--text-tertiary);
}
.xx-dashboard-empty p {
margin: 0;
font-size: var(--font-size-sm);
}
/* ============================================================
最近任务列表
============================================================ */
.xx-task-list {
display: flex;
@@ -169,86 +97,6 @@
gap: var(--space-sm);
}
.xx-task-item {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: var(--space-sm) var(--space-md);
display: grid;
grid-template-columns: 1fr auto auto auto;
gap: var(--space-md);
align-items: center;
transition: var(--transition-all);
}
.xx-task-item:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-sm);
}
.xx-task-info h4 {
margin: 0 0 var(--space-xs);
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--text-primary);
}
.xx-task-info span {
font-size: var(--font-size-xs);
color: var(--text-secondary);
}
.xx-task-status {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
padding: 3px 10px;
border-radius: var(--radius-sm);
font-size: var(--font-size-xs);
font-weight: 500;
white-space: nowrap;
}
.xx-task-status--completed {
color: var(--success-color);
background: var(--success-soft);
border: 1px solid var(--success-border);
}
.xx-task-status--processing {
color: var(--info-color);
background: var(--primary-soft);
border: 1px solid var(--color-primary-200);
}
.xx-task-status--pending {
color: var(--text-secondary);
background: var(--bg-secondary);
border: 1px solid var(--border-color);
}
.xx-task-status--failed {
color: var(--error-color);
background: var(--error-soft);
border: 1px solid var(--error-border);
}
.xx-task-time {
text-align: right;
font-size: var(--font-size-xs);
color: var(--text-secondary);
min-width: 100px;
}
.xx-task-time span {
display: block;
margin-bottom: var(--space-xxs);
}
.xx-task-action {
font-size: var(--font-size-xs);
}
/* ============================================================
使用统计图表(纯 CSS 柱状图)
============================================================ */
@@ -264,69 +112,27 @@
padding-top: var(--space-sm);
}
.xx-chart-bar-wrapper {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
}
.xx-chart-bar {
width: 100%;
max-width: 36px;
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
background: var(--gradient-primary);
transition: height 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
min-height: 4px;
cursor: pointer;
}
.xx-chart-bar:hover {
opacity: 0.85;
}
.xx-chart-bar:active {
opacity: 0.7;
transform: scaleY(0.97);
transform-origin: bottom;
}
.xx-chart-bar-value {
position: absolute;
top: -20px;
left: 50%;
transform: translateX(-50%);
font-size: var(--font-size-xs);
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
opacity: 0;
transition: var(--transition-opacity);
}
.xx-chart-bar:hover .xx-chart-bar-value {
opacity: 1;
}
.xx-chart-labels {
display: flex;
gap: var(--space-sm);
margin-top: var(--space-sm);
}
.xx-chart-label {
flex: 1;
text-align: center;
font-size: var(--font-size-xs);
color: var(--text-secondary);
}
/* ============================================================
快速入口卡片网格
快速入口
============================================================ */
.xx-quick-entry-section {
margin-bottom: var(--space-md);
}
.xx-quick-entry-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-md);
}
.xx-quick-entry-title {
margin: 0;
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.xx-quick-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -334,53 +140,6 @@
margin-bottom: var(--space-lg);
}
.xx-quick-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: var(--space-lg);
cursor: pointer;
transition: var(--transition-all);
text-align: center;
}
.xx-quick-card:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.xx-quick-card:active {
transform: translateY(0) scale(0.98);
box-shadow: var(--shadow-sm);
transition-duration: 0.1s;
}
.xx-quick-card-icon {
width: 52px;
height: 52px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
margin: 0 auto var(--space-md);
color: var(--text-inverse);
}
.xx-quick-card h3 {
margin: 0 0 var(--space-sm);
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.xx-quick-card p {
margin: 0;
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
/* ============================================================
公告区域
============================================================ */
@@ -451,90 +210,10 @@
color: var(--text-secondary);
}
/* ============================================================
存储用量条
============================================================ */
.xx-storage-bar {
margin-top: var(--space-sm);
}
.xx-storage-bar-track {
height: var(--space-sm);
background: var(--bg-secondary);
border-radius: var(--space-xs);
overflow: hidden;
}
.xx-storage-bar-fill {
height: 100%;
border-radius: var(--space-xs);
background: var(--gradient-primary);
transition: width 0.6s ease;
}
.xx-storage-bar-label {
display: flex;
justify-content: space-between;
font-size: var(--font-size-xs);
color: var(--text-secondary);
margin-top: var(--space-xs);
}
/* ============================================================
迁移自内联样式的工具类
============================================================ */
.xx-dashboard-left-col {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.xx-chart-total {
font-size: var(--font-size-xs);
color: var(--text-secondary);
}
.xx-dashboard-section--start {
align-self: start;
}
.xx-storage-section {
margin-top: var(--space-md);
}
.xx-storage-section-title {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 4px;
}
.xx-quick-entry-section {
margin-bottom: var(--space-md);
}
.xx-quick-entry-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-md);
}
.xx-quick-entry-title {
margin: 0;
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
/* ============================================================
响应式
============================================================ */
@media (max-width: 1200px) {
.xx-dashboard-main {
grid-template-columns: 1fr;
}
.xx-kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
@@ -557,15 +236,6 @@
grid-template-columns: 1fr;
}
.xx-task-item {
grid-template-columns: 1fr;
gap: var(--space-sm);
}
.xx-task-time {
text-align: left;
}
.xx-chart-bars {
height: 120px;
}
@@ -416,7 +416,6 @@ const TitleLibrary: React.FC = () => {
[deleteMutation],
);
/* 新建标题 */
const handleCreateTitle = () => {
if (!newTitleContent.trim()) {
@@ -503,7 +502,6 @@ const TitleLibrary: React.FC = () => {
</h4>
<span>{cat.count} 条</span>
</div>
</div>
</div>
))}
@@ -614,8 +612,6 @@ const TitleLibrary: React.FC = () => {
</div>
</div>
{/* ─── 新建标题弹窗 ─── */}
<AntModal
title="新建标题"
+3 -3
View File
@@ -16,15 +16,15 @@ import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_has_audio, run_ffmpeg
# 延迟导入避免循环依赖:unified_render_service 定义 ResolvedClip / RenderLayer,
# 本模块提供音频函数供 unified_render_service 调用。
# 使用 from __future__ import annotations + TYPE_CHECKING 解决类型引用。
from typing import TYPE_CHECKING
from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_has_audio, run_ffmpeg
if TYPE_CHECKING:
from video_processing.unified_render_service import ResolvedClip, RenderLayer
from video_processing.unified_render_service import RenderLayer, ResolvedClip
logger = logging.getLogger(__name__)
@@ -238,7 +238,10 @@ class UnifiedRenderService:
else:
# 回退到带滤镜的直通渲染
pass_through_has_audio = self._render_pass_through(
layers, output_path, ass_path=ass_path, video_duration=video_duration
layers,
output_path,
ass_path=ass_path,
video_duration=video_duration,
)
else:
filter_complex, input_args = self._build_filter_complex(layers, ass_path=ass_path)
@@ -981,4 +984,3 @@ class UnifiedRenderService:
if clip.duration > 0:
return min(clip.duration, clip.actual_duration) if clip.actual_duration > 0 else clip.duration
return clip.actual_duration if clip.actual_duration > 0 else 0.0
+20 -6
View File
@@ -67,7 +67,11 @@ def _update_task_status(task_id: str, status_action: str, **kwargs) -> bool:
action(**kwargs)
repo.update(task)
logger.info("GenerationTask 状态更新成功: task_id=%s action=%s", task_id, status_action)
logger.info(
"GenerationTask 状态更新成功: task_id=%s action=%s",
task_id,
status_action,
)
return True
finally:
session.close()
@@ -458,7 +462,10 @@ def _download_library_assets(
if not storage_key:
failed_assets.append(f"{asset.name}({asset.id})")
logger.warning(
"[task_id=%s] 素材缺少 file_url, 跳过: asset_id=%s name=%s", task_id, asset.id, asset.name
"[task_id=%s] 素材缺少 file_url, 跳过: asset_id=%s name=%s",
task_id,
asset.id,
asset.name,
)
if gen_task:
gen_task.append_log(
@@ -504,7 +511,12 @@ def _download_library_assets(
)
else:
failed_assets.append(f"{asset.name}({asset.id})")
logger.warning("[task_id=%s] Failed to download asset: %s (id=%s)", task_id, asset.name, asset.id)
logger.warning(
"[task_id=%s] Failed to download asset: %s (id=%s)",
task_id,
asset.name,
asset.id,
)
if gen_task:
gen_task.append_log(
"下载素材",
@@ -910,10 +922,12 @@ def _upload_and_record(
key = normalize_storage_key(file_url)
if not (bucket and bucket.object_exists(key)):
raise RuntimeError(
f"OSS 上传后 URL 不可访问且 object_exists 失败: file_url={file_url}, "
f"storage_key={storage_key}"
f"OSS 上传后 URL 不可访问且 object_exists 失败: file_url={file_url}, " f"storage_key={storage_key}"
)
logger.info("URL 校验失败但 object_exists 确认文件存在,视为上传成功: storage_key=%s", key)
logger.info(
"URL 校验失败但 object_exists 确认文件存在,视为上传成功: storage_key=%s",
key,
)
logger.info(
"[task_id=%s] [OSS上传] 成功: 耗时=%.1fs, file_url=%s",
+4 -7
View File
@@ -63,8 +63,7 @@ BRANCH_NAME="${GITHUB_REF_NAME:-${CI_COMMIT_BRANCH:-unknown}}"
if [ "$USE_CACHE" -eq 1 ]; then
docker buildx build \
--build-arg APP_VERSION="$VERSION" \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_TAG},ignore-error=true" \
--cache-to "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_TAG},mode=max" \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_TAG_PRIMARY},ignore-error=true" \
-f infra/docker/api.Dockerfile \
-t "$API_IMAGE" -t "$API_LATEST" \
--load \
@@ -123,8 +122,7 @@ echo "=== Building Worker image ==="
if [ "$USE_CACHE" -eq 1 ]; then
docker buildx build \
--build-arg APP_VERSION="$VERSION" \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_TAG},ignore-error=true" \
--cache-to "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_TAG},mode=max" \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_TAG_PRIMARY},ignore-error=true" \
-f infra/docker/worker.Dockerfile \
-t "$WORKER_IMAGE" -t "$WORKER_LATEST" \
--load \
@@ -152,8 +150,7 @@ test -f apps/web/dist/index.html
if [ "$USE_CACHE" -eq 1 ]; then
docker buildx build \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG},ignore-error=true" \
--cache-to "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG},mode=max" \
--cache-from "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG_PRIMARY},ignore-error=true" \
-f infra/docker/web-artifact.Dockerfile \
--build-arg "NGINX_CONF=$NGINX_CONF_FILE" \
-t "$WEB_IMAGE" \
@@ -182,4 +179,4 @@ else
fi
echo "=== Build complete ==="
docker images | grep "xiaoxia-saas.*:$VERSION"
docker images | grep "xiaoxia-saas" | grep "$VERSION" || true
+22 -5
View File
@@ -48,10 +48,19 @@ HIGH_RISK_PATTERNS = [
# 中风险模式:可能导致数据丢失或兼容性问题
MEDIUM_RISK_PATTERNS = [
(r"op\.alter_column\([^)]*nullable\s*=\s*False", "新增 NOT NULL 约束 - 旧数据可能为空导致迁移失败"),
(
r"op\.alter_column\([^)]*nullable\s*=\s*False",
"新增 NOT NULL 约束 - 旧数据可能为空导致迁移失败",
),
(r"op\.alter_column\([^)]*type_\s*=", "列类型变更 - 可能导致数据截断或转换失败"),
(r"\bop\.rename_table\(", "op.rename_table() - 重命名表,可能导致依赖该表的代码报错"),
(r"\bop\.rename_column\(", "op.rename_column() - 重命名列,可能导致依赖该列的代码报错"),
(
r"\bop\.rename_table\(",
"op.rename_table() - 重命名表,可能导致依赖该表的代码报错",
),
(
r"\bop\.rename_column\(",
"op.rename_column() - 重命名列,可能导致依赖该列的代码报错",
),
(r"\bop\.drop_index\(", "op.drop_index() - 删除索引,可能影响查询性能"),
(r"\bop\.drop_constraint\(", "op.drop_constraint() - 删除约束,可能影响数据完整性"),
]
@@ -95,7 +104,16 @@ def get_new_migrations_via_diff(diff_target: str) -> List[Path]:
"""
try:
result = subprocess.run(
["git", "diff", "--name-only", "--diff-filter=A", diff_target, "HEAD", "--", "alembic/versions/"],
[
"git",
"diff",
"--name-only",
"--diff-filter=A",
diff_target,
"HEAD",
"--",
"alembic/versions/",
],
cwd=str(REPO_ROOT),
capture_output=True,
text=True,
@@ -247,4 +265,3 @@ def main() -> int:
if __name__ == "__main__":
sys.exit(main())
+2 -2
View File
@@ -308,7 +308,7 @@ class TestPasswordReset:
)
response = client.post(
"/api/v1/auth/password/forgot",
"/api/v1/auth/forgot-password",
json={"email": test_email},
)
@@ -318,7 +318,7 @@ class TestPasswordReset:
def test_request_password_reset_nonexistent_user(self):
"""测试请求不存在的用户密码重置"""
response = client.post(
"/api/v1/auth/password/forgot",
"/api/v1/auth/forgot-password",
json={"email": "nonexistent@example.com"},
)
+1 -1
View File
@@ -219,7 +219,7 @@ class TestDeleteAssetLibrary:
response = client.delete("/api/v1/asset-libraries/lib-1")
assert response.status_code == 403
assert "Access denied" in response.json()["detail"]
assert "无权访问该项目" in response.json()["detail"]
# 库未被删除
assert lib_repo.find_by_id("lib-1") is not None
Regular → Executable
+1 -1
View File
@@ -37,7 +37,7 @@ def _fresh_settings(**env_overrides: dict[str, str]):
"JWT_SECRET_KEY": "unit-test-secret-key-12345",
**env_overrides,
}
with patch.dict(os.environ, env, clear=False):
with patch.dict(os.environ, env, clear=True):
Settings = _load_settings_class()
return Settings()
+7 -7
View File
@@ -318,7 +318,7 @@ class TestGeneratePlan:
clip = _make_clip(plan.id, order=1)
clip_repo.create(clip)
with patch("app.api.routes.edit_plans.celery_app") as mock_celery:
with patch("app.api.routes.edit_plans_generation.celery_app") as mock_celery:
mock_celery.send_task = MagicMock()
resp = client.post(f"/api/v1/edit-plans/{plan.id}/generate")
@@ -406,7 +406,7 @@ class TestGeneratePlan:
clip = _make_clip(plan.id, order=1, status=EditPlanClipStatus.READY)
clip_repo.create(clip)
with patch("app.api.routes.edit_plans.celery_app") as mock_celery:
with patch("app.api.routes.edit_plans_generation.celery_app") as mock_celery:
mock_celery.send_task = MagicMock()
resp = client.post(f"/api/v1/edit-plans/{plan.id}/generate")
@@ -427,7 +427,7 @@ class TestGeneratePlan:
clip = _make_clip(plan.id, order=i + 1)
clip_repo.create(clip)
with patch("app.api.routes.edit_plans.celery_app") as mock_celery:
with patch("app.api.routes.edit_plans_generation.celery_app") as mock_celery:
mock_celery.send_task = MagicMock()
resp = client.post(f"/api/v1/edit-plans/{plan.id}/generate")
@@ -580,7 +580,7 @@ class TestResponseSchema:
clip = _make_clip(plan.id, order=1)
clip_repo.create(clip)
with patch("app.api.routes.edit_plans.celery_app") as mock_celery:
with patch("app.api.routes.edit_plans_generation.celery_app") as mock_celery:
mock_celery.send_task = MagicMock()
resp = client.post(f"/api/v1/edit-plans/{plan.id}/generate")
@@ -623,7 +623,7 @@ class TestGeneratePlanErrorHandling:
clip = _make_clip(plan.id, order=1)
clip_repo.create(clip)
with patch("app.api.routes.edit_plans.celery_app") as mock_celery:
with patch("app.api.routes.edit_plans_generation.celery_app") as mock_celery:
# 模拟 Celery 调度失败
mock_celery.send_task.side_effect = RuntimeError("Redis 连接超时")
resp = client.post(f"/api/v1/edit-plans/{plan.id}/generate")
@@ -647,7 +647,7 @@ class TestGeneratePlanErrorHandling:
clip = _make_clip(plan.id, order=1)
clip_repo.create(clip)
with patch("app.api.routes.edit_plans.celery_app") as mock_celery:
with patch("app.api.routes.edit_plans_generation.celery_app") as mock_celery:
mock_celery.send_task.side_effect = RuntimeError("调度失败")
resp = client.post(f"/api/v1/edit-plans/{plan.id}/generate")
@@ -668,7 +668,7 @@ class TestGeneratePlanErrorHandling:
clip = _make_clip(plan.id, order=1)
clip_repo.create(clip)
with patch("app.api.routes.edit_plans.celery_app") as mock_celery:
with patch("app.api.routes.edit_plans_generation.celery_app") as mock_celery:
mock_celery.send_task.side_effect = ConnectionError("Broker 不可达")
resp = client.post(f"/api/v1/edit-plans/{plan.id}/generate")
Executable → Regular
-1
View File
@@ -7,7 +7,6 @@ from __future__ import annotations
from unittest.mock import MagicMock
from packages.adapters.redis.feature_flag_store import (
FeatureFlagConfig,
InMemoryFeatureFlagStore,
+3
View File
@@ -8,9 +8,12 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
import app.config as app_config
from app.core.storage import OSSStorageService
import packages.shared.config as shared_config
def _reset_settings() -> None:
app_config._settings = None
shared_config._settings = None
def test_create_direct_upload_post_limits_key_and_size(monkeypatch):
-1
View File
@@ -9,7 +9,6 @@ from __future__ import annotations
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
from packages.application.cosyvoice_service import CosyVoiceService
from packages.application.tts_job.workflow import TTSWorkflowService
from packages.domain.tts_job import TTSJob, TTSJobStatus