diff --git a/.gitea/workflows/ci-pipeline.yml b/.gitea/workflows/ci-pipeline.yml index 2eec845f7..e34885632 100755 --- a/.gitea/workflows/ci-pipeline.yml +++ b/.gitea/workflows/ci-pipeline.yml @@ -1169,6 +1169,31 @@ jobs: run: | set +e NOTIFY_MODE=start JOB_NAME="Deploy Staging" python3 scripts/ci_notify.py + - name: Render .env from template + shell: sh + env: + STAGING_DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }} + STAGING_REDIS_URL: ${{ secrets.STAGING_REDIS_URL }} + STAGING_CELERY_BROKER_URL: ${{ secrets.STAGING_CELERY_BROKER_URL }} + STAGING_CELERY_RESULT_BACKEND: ${{ secrets.STAGING_CELERY_RESULT_BACKEND }} + STAGING_JWT_SECRET_KEY: ${{ secrets.STAGING_JWT_SECRET_KEY }} + STAGING_MINIO_ENDPOINT: ${{ secrets.STAGING_MINIO_ENDPOINT }} + STAGING_MINIO_ACCESS_KEY: ${{ secrets.STAGING_MINIO_ACCESS_KEY }} + STAGING_MINIO_SECRET_KEY: ${{ secrets.STAGING_MINIO_SECRET_KEY }} + STAGING_MINIO_BUCKET: ${{ secrets.STAGING_MINIO_BUCKET }} + OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }} + OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} + COSYVOICE_API_KEY: ${{ secrets.COSYVOICE_API_KEY }} + DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} + MEDIAKIT_API_KEY: ${{ secrets.MEDIAKIT_API_KEY }} + run: | + set -eu + echo "Rendering .env from template + secrets..." + bash scripts/render_env.sh staging + echo "✅ .env rendered (file contains secrets, not printed to log)" + # 验证文件存在且非空 + test -s .env.rendered + echo "✅ .env.rendered validated ($(wc -l < .env.rendered) lines)" - name: Docker login to Registry shell: sh env: @@ -1241,9 +1266,22 @@ jobs: ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "echo SSH_CONNECTION_OK && hostname" echo "SSH connection verified" + # 上传渲染后的 .env 到服务器(替代服务器上旧的 .env) + echo "Uploading rendered .env to staging server..." + # 备份旧 .env + ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" \ + "cp -f /var/lib/xiaoxia-saas-staging/.env /var/lib/xiaoxia-saas-staging/.env.bak.\$(date +%Y%m%d%H%M%S) 2>/dev/null || true" + # 上传新 .env + scp -P "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no .env.rendered \ + "${staging_user}@${staging_host}:/var/lib/xiaoxia-saas-staging/.env" + echo "✅ .env uploaded to staging server" + # 通过环境变量传递凭证,避免命令行引号转义问题 cat scripts/ci_staging_deploy.sh | ssh -p "$staging_port" -i "$key_path" -o StrictHostKeyChecking=no "${staging_user}@${staging_host}" "IMAGE_TAG=${GITHUB_SHA} ACR_USERNAME=${ACR_USERNAME} ACR_PASSWORD=${ACR_PASSWORD} sh" + # 清理 CI runner 上的渲染文件 + rm -f .env.rendered + - name: Staging health check + auto rollback if: success() shell: sh diff --git a/.gitignore b/.gitignore index 96705506e..72a62eb23 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ ruff_cache/ # 配置模板不受忽略规则限制 !deploy/configs/.env.staging !deploy/configs/.env.production +# 渲染后的 env 文件包含真实密钥,绝不能提交 +.env.rendered # OS / editor .DS_Store diff --git a/deploy/configs/.env.production b/deploy/configs/.env.production index 14d480e64..55ab74ff3 100644 --- a/deploy/configs/.env.production +++ b/deploy/configs/.env.production @@ -104,25 +104,23 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS=30 # ==================== 邮件配置 ==================== -# 启用邮件投递 -ENABLE_EMAIL_DELIVERY=true +# 邮件功能尚未上线,暂时关闭 +ENABLE_EMAIL_DELIVERY=false # SMTP 服务器地址 -SMTP_HOST=smtp.gmail.com +SMTP_HOST= # SMTP 端口 SMTP_PORT=587 -# SMTP 用户名 -# ${SMTP_USER} — 替换为实际的 SMTP 用户名 -SMTP_USER=${SMTP_USER} +# SMTP 用户名(邮件功能上线后配置) +SMTP_USER= -# SMTP 密码 / 应用专用密码 -# ${SMTP_PASSWORD} — 替换为实际的 SMTP 密码 -SMTP_PASSWORD=${SMTP_PASSWORD} +# SMTP 密码(邮件功能上线后配置) +SMTP_PASSWORD= -# 发件人邮箱 -SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com +# 发件人邮箱(邮件功能上线后配置) +SMTP_FROM_EMAIL= # 发件人显示名称 SMTP_FROM_NAME=小虾 SaaS @@ -157,7 +155,7 @@ OSS_DIRECT_UPLOAD_EXPIRE_SECONDS=900 # ==================== CORS 配置 ==================== # 允许跨域的前端域名列表,逗号分隔 -CORS_ORIGINS_RAW=https://xiaoxiajianji.com,https://www.xiaoxiajianji.com +CORS_ORIGINS_RAW=https://xiaoxiajianji.com,https://api.xiaoxiajianji.com # ==================== 生成文件路径 ==================== @@ -191,35 +189,26 @@ COSYVOICE_BASE_URL=https://dashscope.aliyuncs.com/api/v1 COSYVOICE_MODEL=cosyvoice-v3-flash # 音色:v3 系列系统音色带 _v3 后缀 -COSYVOICE_VOICE=longxiaochun_v3 +COSYVOICE_VOICE=longxiaoxia_v3 # 采样率 COSYVOICE_SAMPLE_RATE=22050 # 输出格式 -COSYVOICE_FORMAT=mp3 +COSYVOICE_FORMAT=wav # 音色克隆模型名(固定值) COSYVOICE_CLONE_MODEL=voice-enrollment +# DashScope 通用 API Key(与 CosyVoice 共用) +DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY} -# ==================== 豆包大模型(火山引擎方舟)==================== -# 豆包 API Key -# ${DOUBAO_API_KEY} — 替换为实际的豆包 API Key -DOUBAO_API_KEY=${DOUBAO_API_KEY} +# ==================== MediaKit 视频理解(火山引擎)==================== -# 模型名称 -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 +MEDIAKIT_API_KEY=${MEDIAKIT_API_KEY} +MEDIAKIT_BASE_URL=https://mediakit.cn-beijing.volces.com/api/v1 +MEDIAKIT_TIMEOUT=60 # ==================== 监控(可选)==================== diff --git a/deploy/configs/.env.staging b/deploy/configs/.env.staging index 423c7acb8..538417191 100644 --- a/deploy/configs/.env.staging +++ b/deploy/configs/.env.staging @@ -14,8 +14,8 @@ APP_NAME=xiaoxia-saas # 环境标识 APP_ENV=staging -# 关闭 Debug 模式 -DEBUG=false +# Staging 开启 Debug 模式便于排查问题 +DEBUG=true # 应用基础 URL(前端页面地址) APP_BASE_URL=https://staging.xiaoxiajianji.com @@ -96,7 +96,7 @@ JWT_SECRET_KEY=${JWT_SECRET_KEY} JWT_ALGORITHM=HS256 # Access Token 过期时间(分钟) -JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 +JWT_ACCESS_TOKEN_EXPIRE_MINUTES=1440 # Refresh Token 过期时间(天) JWT_REFRESH_TOKEN_EXPIRE_DAYS=30 @@ -104,8 +104,8 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS=30 # ==================== 邮件配置 ==================== -# 启用邮件投递 -ENABLE_EMAIL_DELIVERY=true +# 邮件功能尚未上线,暂时关闭 +ENABLE_EMAIL_DELIVERY=false # SMTP 服务器地址 SMTP_HOST=smtp.gmail.com @@ -113,16 +113,14 @@ SMTP_HOST=smtp.gmail.com # SMTP 端口 SMTP_PORT=587 -# SMTP 用户名 -# ${SMTP_USER} — 替换为实际的 SMTP 用户名 -SMTP_USER=${SMTP_USER} +# SMTP 用户名(邮件功能上线后配置) +SMTP_USER= -# SMTP 密码 / 应用专用密码 -# ${SMTP_PASSWORD} — 替换为实际的 SMTP 密码 -SMTP_PASSWORD=${SMTP_PASSWORD} +# SMTP 密码(邮件功能上线后配置) +SMTP_PASSWORD= -# 发件人邮箱 -SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com +# 发件人邮箱(邮件功能上线后配置) +SMTP_FROM_EMAIL= # 发件人显示名称 SMTP_FROM_NAME=小虾 SaaS @@ -170,7 +168,7 @@ MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} MINIO_SECRET_KEY=${MINIO_SECRET_KEY} # MinIO Bucket 名称 -MINIO_BUCKET=${MINIO_BUCKET} +MINIO_BUCKET_NAME=${MINIO_BUCKET_NAME} # 是否使用 SSL 连接 MinIO MINIO_USE_SSL=false @@ -179,7 +177,7 @@ MINIO_USE_SSL=false # ==================== CORS 配置 ==================== # 允许跨域的前端域名列表,逗号分隔 -CORS_ORIGINS_RAW=https://staging.xiaoxiajianji.com,https://staging-web.xiaoxiajianji.com +CORS_ORIGINS_RAW=https://staging.xiaoxiajianji.com,https://staging-api.xiaoxiajianji.com # ==================== 生成文件路径 ==================== @@ -213,32 +211,23 @@ COSYVOICE_BASE_URL=https://dashscope.aliyuncs.com/api/v1 COSYVOICE_MODEL=cosyvoice-v3-flash # 音色:v3 系列系统音色带 _v3 后缀 -COSYVOICE_VOICE=longxiaochun_v3 +COSYVOICE_VOICE=longxiaoxia_v3 # 采样率 COSYVOICE_SAMPLE_RATE=22050 # 输出格式 -COSYVOICE_FORMAT=mp3 +COSYVOICE_FORMAT=wav # 音色克隆模型名(固定值) COSYVOICE_CLONE_MODEL=voice-enrollment +# DashScope 通用 API Key(与 CosyVoice 共用) +DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY} -# ==================== 豆包大模型(火山引擎方舟)==================== -# 豆包 API Key -# ${DOUBAO_API_KEY} — 替换为实际的豆包 API Key -DOUBAO_API_KEY=${DOUBAO_API_KEY} +# ==================== MediaKit 视频理解(火山引擎)==================== -# 模型名称 -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 +MEDIAKIT_API_KEY=${MEDIAKIT_API_KEY} +MEDIAKIT_BASE_URL=https://mediakit.cn-beijing.volces.com/api/v1 +MEDIAKIT_TIMEOUT=60 diff --git a/scripts/ci_staging_deploy.sh b/scripts/ci_staging_deploy.sh index 70aa0955c..9d8cefc39 100755 --- a/scripts/ci_staging_deploy.sh +++ b/scripts/ci_staging_deploy.sh @@ -55,7 +55,13 @@ if [ -z "$IMAGE_TAG" ]; then exit 1 fi -test -f "$ENV_FILE" +# .env 文件由 CI 从模板 + Secrets 渲染后通过 SCP 上传到服务器 +# 如果文件不存在,说明 CI 渲染步骤失败或未执行 +if [ ! -f "$ENV_FILE" ]; then + echo "ERROR: $ENV_FILE 不存在。CI 应先在 render_env 步骤渲染并上传此文件" + exit 1 +fi +echo "✅ .env file found: $ENV_FILE ($(wc -l < "$ENV_FILE") lines)" mkdir -p "$GENERATED_DIR" mkdir -p "$LEGACY_ASSETS_DIR" diff --git a/scripts/render_env.sh b/scripts/render_env.sh new file mode 100644 index 000000000..42930f6e1 --- /dev/null +++ b/scripts/render_env.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# =========================================================== +# render_env.sh — 从模板 + Secrets 渲染 .env 文件 +# =========================================================== +# 用法: scripts/render_env.sh +# +# 输入: deploy/configs/.env.staging 或 .env.production 模板 +# 输出: .env.rendered(包含真实密钥,切勿提交或打印) +# +# 环境变量映射规则: +# STAGING_xxx / PRODUCTION_xxx → xxx(去掉环境前缀) +# 共用 secrets 直接使用(如 OSS_ACCESS_KEY_ID) +# =========================================================== +set -eu + +TARGET_ENV="${1:-}" + +if [ -z "$TARGET_ENV" ] || { [ "$TARGET_ENV" != "staging" ] && [ "$TARGET_ENV" != "production" ]; }; then + echo "ERROR: 用法: $0 " >&2 + exit 1 +fi + +TEMPLATE_FILE="deploy/configs/.env.${TARGET_ENV}" +OUTPUT_FILE=".env.rendered" + +if [ ! -f "$TEMPLATE_FILE" ]; then + echo "ERROR: 模板文件不存在: $TEMPLATE_FILE" >&2 + exit 1 +fi + +# 构建环境变量映射(带环境前缀的 secrets → 模板变量名) +ENV_PREFIX=$(echo "$TARGET_ENV" | tr '[:lower:]' '[:upper:]') + +# 需要映射的带环境前缀变量 +MAPPED_VARS="DATABASE_URL REDIS_URL CELERY_BROKER_URL CELERY_RESULT_BACKEND JWT_SECRET_KEY" + +# Staging 独有的 MinIO 变量 +if [ "$TARGET_ENV" = "staging" ]; then + MAPPED_VARS="$MAPPED_VARS MINIO_ENDPOINT MINIO_ACCESS_KEY MINIO_SECRET_KEY" +fi + +# 将带前缀的 secrets 导出为无前缀的环境变量 +for var in $MAPPED_VARS; do + prefixed_var="${ENV_PREFIX}_${var}" + value="${!prefixed_var:-}" + if [ -n "$value" ]; then + export "$var=$value" + fi +done + +# 特殊映射:CI secret 名称与模板占位符不一致的变量 +# STAGING_MINIO_BUCKET → MINIO_BUCKET_NAME +if [ "$TARGET_ENV" = "staging" ]; then + if [ -n "${STAGING_MINIO_BUCKET:-}" ]; then + export "MINIO_BUCKET_NAME=$STAGING_MINIO_BUCKET" + fi +fi + +# 共用 secrets 直接导出(如果存在) +SHARED_SECRETS="OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET COSYVOICE_API_KEY DASHSCOPE_API_KEY MEDIAKIT_API_KEY" +for var in $SHARED_SECRETS; do + value="${!var:-}" + # 已经在环境中了,无需额外操作 +done + +# 使用 Python 进行变量替换(Python 在 CI runner 中一定存在) +python3 - "$TEMPLATE_FILE" "$OUTPUT_FILE" "$ENV_PREFIX" "$MAPPED_VARS" "$SHARED_SECRETS" <<'PYTHON_SCRIPT' +import sys +import os +import re + +template_file = sys.argv[1] +output_file = sys.argv[2] +env_prefix = sys.argv[3] +mapped_vars_str = sys.argv[4] +shared_secrets_str = sys.argv[5] + +# 收集所有可用的替换变量 +all_vars = set() +for v in mapped_vars_str.split(): + all_vars.add(v) +for v in shared_secrets_str.split(): + all_vars.add(v) + +# 读取模板 +with open(template_file, 'r') as f: + template = f.read() + +# 找出模板中所有的 ${VAR} 占位符(仅检查非注释行) +pattern = re.compile(r'\$\{(\w+)\}') +placeholders = set() +for line in template.splitlines(): + stripped = line.strip() + if stripped.startswith('#'): + continue + placeholders.update(pattern.findall(line)) + +# 检查必需变量是否已设置 +missing = [] +for var in placeholders: + value = os.environ.get(var, '') + if not value: + missing.append(var) + +if missing: + print(f"ERROR: 以下变量未设置或为空: {', '.join(sorted(missing))}", file=sys.stderr) + print(f"请确认对应的 {env_prefix}_xxx 或共用 secrets 已在 Gitea Secrets 中配置", file=sys.stderr) + sys.exit(1) + +# 执行替换 +def replace_var(match): + var_name = match.group(1) + return os.environ.get(var_name, match.group(0)) + +rendered = pattern.sub(replace_var, template) + +# 写入输出文件 +with open(output_file, 'w') as f: + f.write(rendered) + +# 设置文件权限为仅 owner 可读写 +os.chmod(output_file, 0o600) + +print(f"✅ .env 渲染完成: {template_file} → {output_file}") +print(f" 替换了 {len(placeholders)} 个变量") +PYTHON_SCRIPT + +# 验证输出文件 +if [ ! -f "$OUTPUT_FILE" ]; then + echo "ERROR: 渲染失败,输出文件不存在" >&2 + exit 1 +fi + +# 检查输出文件中是否还有未替换的占位符(仅检查非注释行) +if grep -vE '^\s*#' "$OUTPUT_FILE" | grep -qE '\$\{[A-Z_]+\}'; then + echo "ERROR: 输出文件中仍有未替换的占位符:" >&2 + grep -nE '\$\{[A-Z_]+\}' "$OUTPUT_FILE" | grep -v '^\s*#' >&2 + exit 1 +fi + +echo "✅ 渲染文件校验通过,无残留占位符" +echo "⚠️ $OUTPUT_FILE 包含敏感信息,请勿提交或打印到日志"