Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f098eabc12 | |||
| a633b64d4f | |||
| 7eecabaf3d | |||
| 5fb9975913 |
@@ -1412,9 +1412,9 @@ jobs:
|
||||
- validate-security
|
||||
- validate-python
|
||||
- unit-tests
|
||||
- frontend-lint
|
||||
- frontend-unit-test
|
||||
if: |
|
||||
(startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && github.ref_name == 'main')) && !failure() && !cancelled()
|
||||
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && github.ref_name == 'main')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -2068,4 +2068,3 @@ jobs:
|
||||
START_TIME=""
|
||||
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
|
||||
curl -sfH "Authorization: token ${GITHUB_TOKEN:-$GITEA_TOKEN}" -o /tmp/_ci_trace.py "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/ci_trace_report.py?ref=${GITHUB_SHA}" 2>/dev/null && python3 /tmp/_ci_trace.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
|
||||
# CI retry trigger
|
||||
|
||||
@@ -24,6 +24,9 @@ ruff_cache/
|
||||
.env.production
|
||||
.env.staging
|
||||
!.env.example
|
||||
# 配置模板不受忽略规则限制
|
||||
!deploy/configs/.env.staging
|
||||
!deploy/configs/.env.production
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
retrigger3
|
||||
@@ -263,4 +263,3 @@ pytest --cov=packages --cov-report=html
|
||||
---
|
||||
|
||||
**License**: MIT
|
||||
<!-- CI trigger: 1788229339 -->
|
||||
@@ -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")
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# ============================================================
|
||||
# 小虾 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=true
|
||||
|
||||
# SMTP 服务器地址
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
|
||||
# SMTP 端口
|
||||
SMTP_PORT=587
|
||||
|
||||
# SMTP 用户名
|
||||
# ${SMTP_USER} — 替换为实际的 SMTP 用户名
|
||||
SMTP_USER=${SMTP_USER}
|
||||
|
||||
# SMTP 密码 / 应用专用密码
|
||||
# ${SMTP_PASSWORD} — 替换为实际的 SMTP 密码
|
||||
SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
|
||||
# 发件人邮箱
|
||||
SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com
|
||||
|
||||
# 发件人显示名称
|
||||
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://www.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=longxiaochun_v3
|
||||
|
||||
# 采样率
|
||||
COSYVOICE_SAMPLE_RATE=22050
|
||||
|
||||
# 输出格式
|
||||
COSYVOICE_FORMAT=mp3
|
||||
|
||||
# 音色克隆模型名(固定值)
|
||||
COSYVOICE_CLONE_MODEL=voice-enrollment
|
||||
|
||||
|
||||
# ==================== 豆包大模型(火山引擎方舟)====================
|
||||
|
||||
# 豆包 API Key
|
||||
# ${DOUBAO_API_KEY} — 替换为实际的豆包 API Key
|
||||
DOUBAO_API_KEY=${DOUBAO_API_KEY}
|
||||
|
||||
# 模型名称
|
||||
DOUBAO_MODEL=doubao-seed-1-6-250615
|
||||
|
||||
# API 基础 URL
|
||||
DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||
|
||||
# 请求超时时间(秒)
|
||||
DOUBAO_TIMEOUT=30
|
||||
|
||||
# 最大重试次数
|
||||
DOUBAO_MAX_RETRIES=2
|
||||
|
||||
|
||||
# ==================== 监控(可选)====================
|
||||
# Sentry DSN(取消注释并填入实际值以启用错误追踪)
|
||||
# SENTRY_DSN=${SENTRY_DSN}
|
||||
@@ -0,0 +1,244 @@
|
||||
# ============================================================
|
||||
# 小虾 SaaS — Staging 环境配置模板
|
||||
# ============================================================
|
||||
# 使用方式:复制为 /var/lib/xiaoxia-saas-staging/.env 并填入实际密钥
|
||||
# 敏感值标记为 ${PLACEHOLDER},部署前必须替换为真实值
|
||||
# ============================================================
|
||||
|
||||
|
||||
# ==================== 应用基本配置 ====================
|
||||
|
||||
# 应用名称
|
||||
APP_NAME=xiaoxia-saas
|
||||
|
||||
# 环境标识
|
||||
APP_ENV=staging
|
||||
|
||||
# 关闭 Debug 模式
|
||||
DEBUG=false
|
||||
|
||||
# 应用基础 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=30
|
||||
|
||||
# Refresh Token 过期时间(天)
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||
|
||||
|
||||
# ==================== 邮件配置 ====================
|
||||
|
||||
# 启用邮件投递
|
||||
ENABLE_EMAIL_DELIVERY=true
|
||||
|
||||
# SMTP 服务器地址
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
|
||||
# SMTP 端口
|
||||
SMTP_PORT=587
|
||||
|
||||
# SMTP 用户名
|
||||
# ${SMTP_USER} — 替换为实际的 SMTP 用户名
|
||||
SMTP_USER=${SMTP_USER}
|
||||
|
||||
# SMTP 密码 / 应用专用密码
|
||||
# ${SMTP_PASSWORD} — 替换为实际的 SMTP 密码
|
||||
SMTP_PASSWORD=${SMTP_PASSWORD}
|
||||
|
||||
# 发件人邮箱
|
||||
SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com
|
||||
|
||||
# 发件人显示名称
|
||||
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=${MINIO_BUCKET}
|
||||
|
||||
# 是否使用 SSL 连接 MinIO
|
||||
MINIO_USE_SSL=false
|
||||
|
||||
|
||||
# ==================== CORS 配置 ====================
|
||||
|
||||
# 允许跨域的前端域名列表,逗号分隔
|
||||
CORS_ORIGINS_RAW=https://staging.xiaoxiajianji.com,https://staging-web.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=longxiaochun_v3
|
||||
|
||||
# 采样率
|
||||
COSYVOICE_SAMPLE_RATE=22050
|
||||
|
||||
# 输出格式
|
||||
COSYVOICE_FORMAT=mp3
|
||||
|
||||
# 音色克隆模型名(固定值)
|
||||
COSYVOICE_CLONE_MODEL=voice-enrollment
|
||||
|
||||
|
||||
# ==================== 豆包大模型(火山引擎方舟)====================
|
||||
|
||||
# 豆包 API Key
|
||||
# ${DOUBAO_API_KEY} — 替换为实际的豆包 API Key
|
||||
DOUBAO_API_KEY=${DOUBAO_API_KEY}
|
||||
|
||||
# 模型名称
|
||||
DOUBAO_MODEL=doubao-seed-1-6-250615
|
||||
|
||||
# API 基础 URL
|
||||
DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||
|
||||
# 请求超时时间(秒)
|
||||
DOUBAO_TIMEOUT=30
|
||||
|
||||
# 最大重试次数
|
||||
DOUBAO_MAX_RETRIES=2
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 基础镜像同步脚本 - 从公共镜像源同步到私有ACR
|
||||
# 用法:
|
||||
# ACR_USERNAME=xxx ACR_PASSWORD=yyy bash scripts/ci/sync_base_images.sh
|
||||
# ============================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ACR_REGISTRY="${ACR_REGISTRY:-xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji}"
|
||||
ACR_USERNAME="${ACR_USERNAME:-}"
|
||||
ACR_PASSWORD="${ACR_PASSWORD:-}"
|
||||
SOURCE_PREFIX="${SOURCE_PREFIX:-docker.m.daocloud.io/library}"
|
||||
|
||||
# 需要同步的镜像列表 (源镜像名:tag => ACR目标名:tag)
|
||||
IMAGES=(
|
||||
"python:3.12-slim-bookworm"
|
||||
"python:3.12-slim"
|
||||
"node:20"
|
||||
"nginx:alpine"
|
||||
)
|
||||
|
||||
echo "============================================"
|
||||
echo " 基础镜像同步到 ACR"
|
||||
echo " ACR: $ACR_REGISTRY"
|
||||
echo " 源: $SOURCE_PREFIX"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# 登录 ACR
|
||||
if [ -n "$ACR_PASSWORD" ] && [ -n "$ACR_USERNAME" ]; then
|
||||
echo "登录 ACR..."
|
||||
ACR_HOST=$(echo "$ACR_REGISTRY" | cut -d/ -f1)
|
||||
printf '%s' "$ACR_PASSWORD" | docker login "$ACR_HOST" -u "$ACR_USERNAME" --password-stdin
|
||||
echo "ACR 登录成功"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
success=0
|
||||
failed=0
|
||||
|
||||
for image in "${IMAGES[@]}"; do
|
||||
source_image="${SOURCE_PREFIX}/${image}"
|
||||
target_image="${ACR_REGISTRY}/base/${image}"
|
||||
|
||||
echo "--- 同步: $image ---"
|
||||
echo " 源: $source_image"
|
||||
echo " 目标: $target_image"
|
||||
|
||||
# Pull 源镜像(带重试)
|
||||
pulled=0
|
||||
for attempt in 1 2 3; do
|
||||
echo " Pull 尝试 $attempt/3..."
|
||||
if docker pull "$source_image"; then
|
||||
pulled=1
|
||||
break
|
||||
fi
|
||||
echo " Pull 失败,5s 后重试..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ "$pulled" -eq 0 ]; then
|
||||
echo " ❌ Pull 失败: $image"
|
||||
failed=$((failed + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Tag
|
||||
docker tag "$source_image" "$target_image"
|
||||
echo " Tag 完成"
|
||||
|
||||
# Push 到 ACR
|
||||
pushed=0
|
||||
for attempt in 1 2 3; do
|
||||
echo " Push 尝试 $attempt/3..."
|
||||
if docker push "$target_image"; then
|
||||
pushed=1
|
||||
break
|
||||
fi
|
||||
echo " Push 失败,5s 后重试..."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ "$pushed" -eq 1 ]; then
|
||||
echo " ✅ 同步成功: $image"
|
||||
success=$((success + 1))
|
||||
else
|
||||
echo " ❌ Push 失败: $image"
|
||||
failed=$((failed + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "============================================"
|
||||
echo " 同步完成"
|
||||
echo " 成功: $success"
|
||||
echo " 失败: $failed"
|
||||
echo "============================================"
|
||||
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/bin/bash
|
||||
# CI Validate: 代码质量与安全扫描(并行Job 1/3)
|
||||
# 包含:密钥扫描、格式检查、安全扫描、依赖漏洞、死代码检测、脚本语法校验
|
||||
set -eu
|
||||
|
||||
echo "=== CI Validate: 代码质量与安全扫描 ==="
|
||||
|
||||
# --- 密钥检测 ---
|
||||
echo ""
|
||||
echo "=== [1/6] Secret detection (detect-secrets) ==="
|
||||
python3 -m pip install -q detect-secrets
|
||||
detect-secrets --version
|
||||
|
||||
detect-secrets scan \
|
||||
--all-files \
|
||||
--exclude-files '(^|/)(tests|test|e2e|__tests__|spec|docs|node_modules|site-packages|migrations|alembic|.gitea|.git|.pytest_cache|.next|dist|build)/' \
|
||||
--exclude-files '\.(md|rst|txt|lock|example|sample|min\.js|min\.css|spec\.ts|test\.ts|test\.py)$' \
|
||||
--exclude-files '(package-lock|yarn\.lock|poetry\.lock|Pipfile\.lock)$' \
|
||||
--disable-plugin Base64HighEntropyString \
|
||||
--disable-plugin HexHighEntropyString \
|
||||
--disable-plugin BasicAuthDetector \
|
||||
--disable-plugin KeywordDetector \
|
||||
--disable-plugin IPPublicDetector \
|
||||
> /tmp/secrets-scan.json 2>&1
|
||||
|
||||
FOUND=$(python3 -c "
|
||||
import json
|
||||
try:
|
||||
with open('/tmp/secrets-scan.json') as f:
|
||||
data = json.load(f)
|
||||
results = data.get('results', {})
|
||||
total = sum(len(v) for v in results.values())
|
||||
print(total)
|
||||
except Exception:
|
||||
print('error')
|
||||
")
|
||||
|
||||
echo "Secrets detected: $FOUND"
|
||||
if [ "$FOUND" != "0" ] && [ "$FOUND" != "error" ]; then
|
||||
echo ""
|
||||
echo "=== Secret details ==="
|
||||
python3 -c "
|
||||
import json
|
||||
with open('/tmp/secrets-scan.json') as f:
|
||||
data = json.load(f)
|
||||
for fpath, items in data.get('results', {}).items():
|
||||
for item in items:
|
||||
line = item.get('line_number', '?')
|
||||
stype = item.get('type', '?')
|
||||
hashed = item.get('hashed_secret', '')[:16]
|
||||
print(f' {fpath}:{line} [{stype}] {hashed}...')
|
||||
"
|
||||
echo ""
|
||||
echo "ERROR: Potential secrets detected in code!"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Secret scan passed"
|
||||
|
||||
# --- 代码质量检查(全量,PR 和 push 统一标准)---
|
||||
# 历史:PR 侧用增量检查以加速,但会导致 push 侧全量检查失败时 PR 侧感知不到
|
||||
# 现在统一全量检查,确保 CI 真正保护主分支(black/isort/ruff 全量仅多几十秒)
|
||||
echo ""
|
||||
echo "=== [2/6] Code quality checks (full scan) ==="
|
||||
SCAN_MODE="full"
|
||||
echo "Full scan mode"
|
||||
python3 -m compileall -q alembic apps packages tests scripts
|
||||
python3 -m black --check --fast alembic apps packages tests scripts
|
||||
python3 -m isort --check-only alembic apps packages tests scripts
|
||||
python3 -m ruff check apps packages tests --statistics
|
||||
|
||||
echo "✅ Code quality checks passed"
|
||||
|
||||
# --- Bandit 安全扫描(仅告警) ---
|
||||
echo ""
|
||||
echo "=== [3/6] Security scan (bandit, advisory only) ==="
|
||||
set +e
|
||||
bandit -r apps packages -q -ll
|
||||
BANDIT_EXIT=$?
|
||||
set -e
|
||||
if [ "$BANDIT_EXIT" -ne 0 ]; then
|
||||
echo "⚠️ Bandit found security issues (advisory mode - not blocking CI)"
|
||||
else
|
||||
echo "✅ Bandit security scan passed"
|
||||
fi
|
||||
|
||||
# --- Pip-audit 依赖漏洞扫描(仅告警) ---
|
||||
echo ""
|
||||
echo "=== [4/6] Python dependency vulnerability scan (pip-audit, advisory only) ==="
|
||||
python3 -m pip install -q pip-audit
|
||||
pip-audit --version
|
||||
EXIT_CODE=0
|
||||
for req_file in requirements.txt requirements-base.txt requirements-dev.txt; do
|
||||
if [ -f "$req_file" ]; then
|
||||
echo "--- Scanning $req_file ---"
|
||||
pip-audit -r "$req_file" --desc on 2>&1 | head -40 || EXIT_CODE=$?
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
echo "pip-audit scan completed (advisory mode - warnings only, not blocking CI)"
|
||||
|
||||
# --- Vulture 死代码检测(仅告警) ---
|
||||
echo ""
|
||||
echo "=== [5/6] Dead code detection (vulture, advisory only) ==="
|
||||
set +e
|
||||
python3 -m pip install -q vulture
|
||||
vulture --version
|
||||
echo "告警模式,不阻断CI。置信度>=90%建议尽快确认。"
|
||||
echo ""
|
||||
vulture apps packages scripts \
|
||||
--exclude "tests,test,migrations,.gitea,docs,node_modules,site-packages,*/test_*.py,*/conftest.py" \
|
||||
--min-confidence 70 \
|
||||
2>&1 | sort -t'(' -k2 -rn | head -80
|
||||
echo ""
|
||||
echo "=== vulture scan summary ==="
|
||||
echo "发现潜在死代码(可能包含框架装饰器注册的函数,为误报)"
|
||||
echo "建议:定期人工审查高置信度(>=90%)条目"
|
||||
set -e
|
||||
|
||||
# --- CI脚本语法校验 ---
|
||||
echo ""
|
||||
echo "=== [6/6] CI & shell scripts syntax validation ==="
|
||||
SYNTAX_ERROR=0
|
||||
# 检查所有 CI shell 脚本
|
||||
for script in scripts/ci/*.sh; do
|
||||
if [ -f "$script" ]; then
|
||||
if ! bash -n "$script" 2>&1; then
|
||||
echo "❌ 语法错误: $script"
|
||||
SYNTAX_ERROR=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
# 检查所有 CI Python 脚本语法
|
||||
for script in scripts/ci/*.py; do
|
||||
if [ -f "$script" ]; then
|
||||
if ! python3 -m py_compile "$script" 2>&1; then
|
||||
echo "❌ Python语法错误: $script"
|
||||
SYNTAX_ERROR=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
# 检查 .gitea/workflows 下的脚本(如果有)
|
||||
for script in .gitea/workflows/*.sh; do
|
||||
if [ -f "$script" ]; then
|
||||
if ! bash -n "$script" 2>&1; then
|
||||
echo "❌ 语法错误: $script"
|
||||
SYNTAX_ERROR=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$SYNTAX_ERROR" -ne 0 ]; then
|
||||
echo "❌ CI脚本语法校验失败,见上方错误"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ All CI scripts syntax OK"
|
||||
|
||||
echo ""
|
||||
echo "=== CI Validate: 代码质量与安全扫描 全部通过 ✅ ==="
|
||||
Reference in New Issue
Block a user