From b9ecfd02a4ccff20d1ecbabc8ca51e52d5155260 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 23 Mar 2026 13:21:07 +0800 Subject: [PATCH] various fixes --- .github/workflows/app-expo-deploy.yml | 15 +- .github/workflows/docker-build-deploy.yml | 65 +++++--- .gitignore | 4 + README.md | 89 ---------- api/.env.example | 71 ++++---- api/.env.production | 114 +++++++------ api/.env.staging | 156 ++++++++++++++++++ api/Dockerfile | 3 - api/app/core/config.py | 11 +- .../features/memoir/memoir_images/settings.py | 21 --- api/app/tasks/memoir_tasks.py | 28 ---- api/development.sh | 19 ++- api/docker-compose.yml | 14 +- app-expo/.env.development | 2 - app-expo/.env.example | 11 +- app-expo/scripts/use-env.js | 10 ++ app-expo/src/app/(tabs)/memoir.tsx | 23 ++- .../features/memoir/framework-chapter-keys.ts | 14 ++ 18 files changed, 393 insertions(+), 277 deletions(-) create mode 100644 api/.env.staging delete mode 100644 app-expo/.env.development diff --git a/.github/workflows/app-expo-deploy.yml b/.github/workflows/app-expo-deploy.yml index b4bfddc..9f3542f 100644 --- a/.github/workflows/app-expo-deploy.yml +++ b/.github/workflows/app-expo-deploy.yml @@ -7,11 +7,11 @@ # - 若需兼容旧 32 位 ARM 设备,可将该属性改为 armeabi-v7a,arm64-v8a(体积会增大)。 # - 若需用 x86 模拟器验证此 APK,需改回含 x86 的 ABI 或另建 job。 # -# 环境映射(按触发源自动推断): -# main → dev (开发 + 内部测试) -# v*.*.* → prod (正式发布 + GitHub Release 附带 APK) +# 环境映射(与后端 api 一致:main → 预发 staging,tag → 生产 production): +# push main → stage → node scripts/use-env.js staging → .env.staging +# push v*.*.* → prod → node scripts/use-env.js production → .env.production # -# 手动触发:workflow_dispatch 可选择 dev / stage / prod +# 手动触发 workflow_dispatch:可选 dev / stage / prod(dev 用 .env.development,便于打内部测试包) # # Repository secrets(与 android-release.yml 共用同一套即可): # ANDROID_KEYSTORE_BASE64 / ANDROID_STORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD @@ -72,7 +72,8 @@ jobs: elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then echo "env=${{ github.event.inputs.environment }}" else - echo "env=dev" + # push 到 main(本 workflow 仅监听 main 与 tag) + echo "env=stage" fi >> $GITHUB_OUTPUT - name: Set up Node.js @@ -86,8 +87,8 @@ jobs: working-directory: app-expo run: npm ci - - name: Quality checks (dev only) - if: steps.env.outputs.env == 'dev' + - name: Quality checks (non-prod) + if: steps.env.outputs.env != 'prod' working-directory: app-expo run: | npm run format:check diff --git a/.github/workflows/docker-build-deploy.yml b/.github/workflows/docker-build-deploy.yml index 4e7c63a..0da841a 100644 --- a/.github/workflows/docker-build-deploy.yml +++ b/.github/workflows/docker-build-deploy.yml @@ -1,15 +1,15 @@ -# API Docker:main → Dev 机(Repository secrets: DEV_*),Tag v*.*.* → Prod 机(PROD_*) +# API Docker:main → Staging 机(Repository secrets: STAGING_*),Tag v*.*.* → Prod 机(PROD_*) # 在 Repo → Settings → Secrets and variables → Actions 中配置,无需 GitHub Environments。 -# 命名:DEV_SSH_HOST / DEV_SSH_USER / DEV_SSH_PRIVATE_KEY / DEV_SSH_PORT / DEV_DEPLOY_PATH +# 命名:STAGING_SSH_HOST / STAGING_SSH_USER / STAGING_SSH_PRIVATE_KEY / STAGING_SSH_PORT / STAGING_DEPLOY_PATH # PROD_SSH_HOST / PROD_SSH_USER / PROD_SSH_PRIVATE_KEY / PROD_SSH_PORT / PROD_DEPLOY_PATH # 阿里云镜像仍为仓库级:ALIYUN_CR_USERNAME / ALIYUN_CR_PASSWORD # # 从旧版迁移:若仓库里仍是 SSH_HOST、SSH_PRIVATE_KEY、DEPLOY_PATH 等无前缀名称, -# 请把「原机 / 内部测试」对应值复制为 DEV_*,「新生产机」填 PROD_*,并删除旧的无前缀 Secret。 +# 请把「预发机」对应值迁移为 STAGING_*,「新生产机」填 PROD_*,并删除旧的无前缀 Secret。 # # 发布策略: -# - merge / push 到 main:构建并部署到 Dev / 内部测试 -# - 手动创建并推送 tag vMAJOR.MINOR.PATCH:构建并部署到 Production +# - merge / push 到 main:构建并部署到 Staging 机;使用仓库中的 api/.env.staging,上传后切换为运行时 .env +# - 手动创建并推送 tag vMAJOR.MINOR.PATCH:构建并部署到 Production;使用仓库中的 api/.env.production,上传后切换为运行时 .env # # 注意:paths 过滤在 tag push 时按「被指向的 commit」判断;若该 commit 未改 api/ 与本 workflow,不会触发。 # 此时可用 workflow_dispatch 选择对应 tag/ref 手动部署。 @@ -122,7 +122,7 @@ jobs: if [[ "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "target=prod" >> "$GITHUB_OUTPUT" else - echo "target=dev" >> "$GITHUB_OUTPUT" + echo "target=staging" >> "$GITHUB_OUTPUT" fi - name: Ensure production SSH secret is set @@ -135,28 +135,28 @@ jobs: exit 1 fi - - name: Ensure development SSH secret is set + - name: Ensure staging SSH secret is set if: steps.deploy_target.outputs.target != 'prod' env: - DEV_SSH_PRIVATE_KEY: ${{ secrets.DEV_SSH_PRIVATE_KEY }} + STAGING_SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} run: | - if [ -z "$DEV_SSH_PRIVATE_KEY" ]; then - echo "::error::DEV_SSH_PRIVATE_KEY 未配置或为空,无法部署开发机。请在 Repository secrets 中设置 DEV_SSH_*。" + if [ -z "$STAGING_SSH_PRIVATE_KEY" ]; then + echo "::error::STAGING_SSH_PRIVATE_KEY 未配置或为空,无法部署 staging。请在 Repository secrets 中设置 STAGING_SSH_*。" exit 1 fi - # 勿用 `prod && PROD_KEY || DEV_KEY`:PROD 为空时会错误回退到 DEV 密钥,导致连生产机报 Permission denied。 + # 勿用 `prod && PROD_KEY || STAGING_KEY`:PROD 为空时会错误回退到 staging 密钥,导致连生产机报 Permission denied。 - name: Set up SSH (production) if: steps.deploy_target.outputs.target == 'prod' uses: webfactory/ssh-agent@v0.9.1 with: ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }} - - name: Set up SSH (development) + - name: Set up SSH (staging) if: steps.deploy_target.outputs.target != 'prod' uses: webfactory/ssh-agent@v0.9.1 with: - ssh-private-key: ${{ secrets.DEV_SSH_PRIVATE_KEY }} + ssh-private-key: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} - name: Export deploy connection env run: | @@ -169,10 +169,10 @@ jobs: } >> "$GITHUB_ENV" else { - echo "SSH_HOST=${{ secrets.DEV_SSH_HOST }}" - echo "SSH_USER=${{ secrets.DEV_SSH_USER }}" - echo "SSH_PORT=${{ secrets.DEV_SSH_PORT || '22' }}" - echo "COMPOSE_DIR=${{ secrets.DEV_DEPLOY_PATH || '/opt/life-echo' }}" + echo "SSH_HOST=${{ secrets.STAGING_SSH_HOST }}" + echo "SSH_USER=${{ secrets.STAGING_SSH_USER }}" + echo "SSH_PORT=${{ secrets.STAGING_SSH_PORT || '22' }}" + echo "COMPOSE_DIR=${{ secrets.STAGING_DEPLOY_PATH || '/opt/life-echo' }}" } >> "$GITHUB_ENV" fi @@ -222,10 +222,32 @@ jobs: docker network inspect api_life-echo-network >/dev/null 2>&1 || docker network create api_life-echo-network " - echo "上传候选 compose、Caddyfile 与环境变量..." + if [ "${{ steps.deploy_target.outputs.target }}" = "prod" ]; then + ENV_SRC="api/.env.production" + else + ENV_SRC="api/.env.staging" + fi + if [ ! -f "$ENV_SRC" ]; then + echo "::error::缺少 $ENV_SRC,无法部署。" + exit 1 + fi + if grep -Eq '=(your_|replace_with_|\\.{3}$)' "$ENV_SRC"; then + echo "::error::$ENV_SRC 仍包含占位符值,请先完善环境文件。" + exit 1 + fi + if grep -Eq '^DATABASE_URL=.*@localhost:' "$ENV_SRC" || grep -Eq '^REDIS_URL=redis://localhost' "$ENV_SRC"; then + echo "::error::$ENV_SRC 包含 localhost 数据库或 Redis 地址,容器内将无法连接。" + exit 1 + fi + if grep -Eq '^DATABASE_URL=.*@postgresql:' "$ENV_SRC"; then + echo "::error::$ENV_SRC 仍引用过期主机名 postgresql;当前 compose 服务名应为 postgres。" + exit 1 + fi + + echo "上传候选 compose、Caddyfile 与环境文件..." scp -P "$SSH_PORT" ./api/docker-compose.yml "$SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/docker-compose.candidate.yml" scp -P "$SSH_PORT" ./api/Caddyfile "$SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/Caddyfile.candidate" - scp -P "$SSH_PORT" ./api/.env.production "$SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/.env.production.candidate" + scp -P "$SSH_PORT" "$ENV_SRC" "$SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/.env.candidate" ssh -p "$SSH_PORT" "$SSH_USER@$SSH_HOST" " set -euo pipefail @@ -255,9 +277,12 @@ jobs: if [ -f '.env.production' ]; then cp '.env.production' '.env.production.predeploy' fi + if [ -f '.env' ]; then + cp '.env' '.env.predeploy' + fi mv 'docker-compose.candidate.yml' '$COMPOSE_FILE' mv 'Caddyfile.candidate' 'Caddyfile' - mv '.env.production.candidate' '.env.production' + mv '.env.candidate' '.env' docker-compose -f '$COMPOSE_FILE' up -d --remove-orphans echo '等待服务启动...' sleep 20 diff --git a/.gitignore b/.gitignore index e41917d..541f3c9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,10 @@ ENV/ .env .env.* !.env.example +!api/.env.staging +!api/.env.production +!app-expo/.env.staging +!app-expo/.env.production # 证书与私钥(勿提交,README 可提交) api/certs/*.pem diff --git a/README.md b/README.md index 916980b..4504ec2 100644 --- a/README.md +++ b/README.md @@ -31,21 +31,7 @@ life-echo/ │ ├── main.py # FastAPI 应用入口 │ └── README.md # API 服务文档 │ -├── app-android/ # Android 应用 -│ └── app/ -│ └── src/main/java/com/huaga/life_echo/ -│ ├── data/ # 数据层(Room、Repository) -│ ├── network/ # 网络层(WebSocket、REST API) -│ ├── ui/ # UI 层(Jetpack Compose) -│ ├── feature/ # 功能模块(语音录制等) -│ └── navigation/ # 导航管理 -│ └── README.md # Android 应用文档 -│ └── docs/ # 项目文档 - ├── 开发计划.md # 开发计划与任务分解 - ├── 数据库设计.md # 数据库设计文档 - ├── AGENT.md # Agent 设计文档 - └── v1_features_*.md # 功能需求文档 ``` ## 🚀 快速开始 @@ -57,11 +43,6 @@ life-echo/ - Docker & Docker Compose(用于 PostgreSQL、Redis) - LLM API Key(DeepSeek 或兼容 OpenAI 的服务) -- **Android 开发**: - - Android Studio Hedgehog | 2023.1.1+ - - JDK 11+ - - Android SDK(API 24+) - ### 后端服务启动 详细步骤请参考 [api/README.md](api/README.md) @@ -103,25 +84,6 @@ uv run celery -A tasks.celery_app worker --loglevel=info --pool=solo 常用命令详见 `api/README.md`。 -### Android 应用启动 - -详细步骤请参考 [app-android/README.md](app-android/README.md) - -```bash -# 1. 使用 Android Studio 打开项目 -# File -> Open -> 选择 app-android 目录 - -# 2. 同步 Gradle 依赖 -# Android Studio 会自动同步,或点击 Sync Now - -# 3. 配置 API 地址(开发环境) -# 编辑 app/src/main/java/com/huaga/life_echo/config/AppConfig.kt -# 修改 BASE_URL 和 WS_BASE_URL - -# 4. 运行应用 -# 点击 Run 按钮或使用快捷键 Shift+F10 -``` - ## 🛠️ 技术栈 ### 后端技术栈 @@ -138,37 +100,8 @@ uv run celery -A tasks.celery_app worker --loglevel=info --pool=solo | DeepSeek API | - | LLM(大语言模型) | | ReportLab + WeasyPrint | - | PDF 生成 | -### Android 技术栈 - -| 技术 | 版本 | 用途 | -|------|------|------| -| Kotlin | 1.9+ | 编程语言 | -| Jetpack Compose | - | 声明式 UI 框架 | -| Ktor | - | HTTP 客户端和 WebSocket | -| Room | - | 本地数据库(SQLite) | -| Coroutines + Flow | - | 异步编程和响应式数据流 | -| DataStore | - | 键值对存储(Token 管理) | -| Coil | - | 图片加载库 | - ## 📚 文档导航 -### 项目文档 - -- [开发计划](docs/开发计划.md) - 项目开发计划与任务分解 -- [数据库设计](docs/数据库设计.md) - 数据库表结构设计 -- [Agent 设计](docs/AGENT.md) - AI Agent 工作流程设计 - -### API 文档 - -- [API README](api/README.md) - API 服务完整文档 -- [本地开发环境配置](api/docs/本地开发环境配置.md) - 开发环境搭建指南 -- [WebSocket 快速测试指南](api/docs/WebSocket快速测试指南.md) - WebSocket 测试文档 -- [WebSocket 测试文档](api/docs/WebSocket测试文档.md) - WebSocket 详细测试说明 - -### Android 文档 - -- [Android README](app-android/README.md) - Android 应用完整文档 - ## 🔐 认证系统 系统使用 JWT(JSON Web Token)进行认证,采用访问令牌(Access Token)+ 刷新令牌(Refresh Token)机制: @@ -202,26 +135,6 @@ uv run celery -A tasks.celery_app worker --loglevel=info --pool=solo - 🔄 章节编辑功能 - 🔄 多语言支持 -## 📝 开发指南 - -### 添加新的 API 路由 - -1. 在 `api/routers/` 目录创建新的路由文件 -2. 定义路由函数 -3. 在 `api/main.py` 中注册路由 - -### 添加新的数据库模型 - -1. 在 `api/database/models.py` 中定义模型类 -2. 继承 `Base` -3. 运行数据库迁移 - -### 添加新的 Android 功能 - -1. 在 `app-android/app/src/main/java/com/huaga/life_echo/` 下创建功能模块 -2. 遵循 MVVM 架构模式 -3. 使用 Compose 构建 UI - ## 🔒 安全注意事项 1. **环境变量安全**:确保 `.env` 文件不被提交到版本控制 @@ -230,8 +143,6 @@ uv run celery -A tasks.celery_app worker --loglevel=info --pool=solo 4. **API Key 安全**:妥善保管 LLM API Key 5. **密码安全**:密码使用 bcrypt 哈希存储 -详细安全建议请参考 [api/README.md#安全注意事项](api/README.md#安全注意事项) - ## 🤝 贡献指南 1. Fork 本项目 diff --git a/api/.env.example b/api/.env.example index 8dacf38..e77991a 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,7 +1,10 @@ # ============================================================================= -# Life Echo API environment example -# 复制为 .env 或 .env.production 后按实际环境修改 -# 不要把真实密钥提交到仓库 +# Life Echo API — 模板(example) +# +# 本地:复制为 .env.development(勿提交密钥),再运行 api/development.sh 会在首次自动生成 .env(从 +# .env.development 复制);Settings 只读 .env(见 app/core/config.py)。 +# 服务端:仓库维护 .env.staging / .env.production;workflow 按目标环境上传并复制为运行时 .env,compose 的 env_file 统一指向 .env。 +# 不要把真实密钥提交到仓库。 # ============================================================================= # ============================================================================= @@ -19,18 +22,18 @@ DEEPSEEK_MODEL=deepseek-chat # ============================================================================= # Database # ============================================================================= -# 本地开发示例: +# 本地开发: # DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo -# Docker / production 示例: +# Docker / 服务端(主机名一般为 compose 服务名 postgres): # DATABASE_URL=postgresql://postgres:postgres@postgres:5432/life_echo DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo # ============================================================================= # Redis # ============================================================================= -# 本地开发示例: +# 本地开发: # REDIS_URL=redis://localhost:6379/0 -# Docker / production 示例: +# Docker / 服务端: # REDIS_URL=redis://redis:6379/0 REDIS_URL=redis://localhost:6379/0 REDIS_SESSION_TTL=86400 @@ -44,42 +47,52 @@ ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=120 # ============================================================================= -# Tencent SMS +# Tencent Cloud — 短信 # ============================================================================= +# 短信、一句话 ASR/TTS、COS 为不同产品;同一主账号可共用同一对 SecretId/SecretKey(分别填三处)。 TENCENT_SMS_SECRET_ID=your_tencent_sms_secret_id TENCENT_SMS_SECRET_KEY=your_tencent_sms_secret_key +# 短信应用 SDK AppID TENCENT_SMS_SDK_APP_ID=your_sms_sdk_app_id +# 短信签名内容(不包含【】符号) TENCENT_SMS_SIGN_NAME=your_sms_sign_name +# 短信模板 ID TENCENT_SMS_TEMPLATE_ID=your_sms_template_id +# 短信模板参数数量(1=仅验证码,2=验证码+过期时间) +# 若遇 TemplateParamSetNotMatchApprovedTemplate,请对照控制台模板配置 TENCENT_SMS_TEMPLATE_PARAM_COUNT=1 # ============================================================================= -# ASR Provider -# whisper | tencent +# ASR Provider(whisper | tencent) # ============================================================================= ASR_PROVIDER=whisper -# Whisper ASR +# ============================================================================= +# Whisper ASR(ASR_PROVIDER=whisper 时使用) +# ============================================================================= ASR_MODEL_SIZE=small ASR_DEVICE=cpu ASR_COMPUTE_TYPE=int8 -# Tencent ASR -# 仅在 ASR_PROVIDER=tencent 时需要 -TENCENT_SECRET_ID=your_tencent_asr_secret_id -TENCENT_SECRET_KEY=your_tencent_asr_secret_key -# TENCENT_ASR_APP_ID= +# GPU 环境(示例,按需启用) +# ASR_MODEL_SIZE=medium +# ASR_DEVICE=cuda +# ASR_COMPUTE_TYPE=float16 # ============================================================================= -# TTS(文字转语音,Agent 回复播音)— 与上方 ASR 完全独立 +# Tencent Cloud — 一句话 ASR + TTS(ASR_PROVIDER=tencent 或 TTS_PROVIDER=tencent) +# ============================================================================= +TENCENT_SECRET_ID=your_tencent_asr_secret_id +TENCENT_SECRET_KEY=your_tencent_asr_secret_key + +# ============================================================================= +# TTS(文字转语音,Agent 回复播音)— 与 ASR 独立 # ============================================================================= # ENABLE_TTS:仅控制是否合成并下发 TTS_AUDIO;不影响用户语音转写(ASR) -# false / 0 / no 关闭语音合成,不调 TTS 厂商 API ENABLE_TTS=true TTS_PROVIDER=tencent # 仅 TTS_PROVIDER=openai 时需要 # OPENAI_API_KEY=your_openai_api_key -# 仅 TTS_PROVIDER=tencent 时生效;密钥变量名可与 ASR 相同,但开关与流程互不关联 # 音色 ID 见 https://cloud.tencent.com/document/product/1073/92668 TTS_VOICE_TYPE=502001 TTS_CODEC=mp3 @@ -90,10 +103,13 @@ TTS_CODEC=mp3 WECHAT_PAY_APP_ID=your_wechat_pay_app_id WECHAT_PAY_MCH_ID=your_wechat_mch_id WECHAT_PAY_API_V3_KEY=your_wechat_api_v3_key +# 商户私钥:推荐使用文件路径,避免 .env 中长 PEM 转义问题 WECHAT_PAY_PRIVATE_KEY_PATH=certs/apiclient_key.pem +# 若不用文件,可配置 WECHAT_PAY_PRIVATE_KEY(PEM,换行用 \n) +# WECHAT_PAY_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" WECHAT_PAY_CERT_SERIAL_NO=your_wechat_cert_serial_no WECHAT_PAY_NOTIFY_URL=https://your-domain.com/api/payment/notify/wechat -# 可选平台公钥模式 +# 平台公钥模式(仅当无法走平台证书自动拉取时使用);勿填商户私钥路径 # WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH=certs/wechat_platform_public_key.pem # WECHAT_PAY_PLATFORM_PUBLIC_KEY_ID=your_wechat_platform_public_key_id @@ -111,19 +127,15 @@ ALIPAY_NOTIFY_URL=https://your-domain.com/api/payment/notify/alipay ENABLE_TEST_SUBSCRIPTION=0 # ============================================================================= -# Memoir image generation +# Memoir image generation(Story 主图等;轮询 Liblib 任务) # ============================================================================= MEMOIR_IMAGE_ENABLED=false -MEMOIR_IMAGE_MAX_PER_CHAPTER=2 MEMOIR_IMAGE_POLL_INTERVAL=3 MEMOIR_IMAGE_MAX_ATTEMPTS=20 -# 正文越长,可允许更多图片 -MEMOIR_IMAGE_CHARS_PER_EXTRA=1500 -MEMOIR_IMAGE_MAX_CAP=8 MEMOIR_IMAGE_PROVIDER=liblib MEMOIR_IMAGE_STYLE_DEFAULT=watercolor MEMOIR_IMAGE_SIZE_DEFAULT=1280x720 -# 可选,限制 Liblib 下载域名 +# 可选,Liblib 返回图片域名不在默认白名单时(逗号分隔) # MEMOIR_IMAGE_DOWNLOAD_HOSTS=liblib.cloud,liblibai.cloud # ============================================================================= @@ -135,13 +147,12 @@ LIBLIB_BASE_URL=https://openapi.liblibai.cloud LIBLIB_TEMPLATE_UUID=your_liblib_template_uuid # ============================================================================= -# Tencent COS for memoir images -# 生产环境请确认这里使用的是生产 bucket,而不是开发 bucket +# Tencent Cloud — COS(回忆录图片存储) # ============================================================================= TENCENT_COS_SECRET_ID=your_tencent_cos_secret_id TENCENT_COS_SECRET_KEY=your_tencent_cos_secret_key TENCENT_COS_REGION=ap-shanghai -TENCENT_COS_BUCKET=your_production_bucket -TENCENT_COS_BASE_URL=https://your_production_bucket.cos.ap-shanghai.myqcloud.com +TENCENT_COS_BUCKET=your_bucket_name +TENCENT_COS_BASE_URL=https://your_bucket_name.cos.ap-shanghai.myqcloud.com # 可选临时凭证 # TENCENT_COS_TOKEN= diff --git a/api/.env.production b/api/.env.production index d8ff6e8..8e53f14 100644 --- a/api/.env.production +++ b/api/.env.production @@ -1,7 +1,9 @@ -# DeepSeek API 配置(推荐,优先使用) -DEEPSEEK_API_KEY=sk-09f17fb61c5a4299a3afc2a01de7af75 -DEEPSEEK_BASE_URL=https://api.deepseek.com -DEEPSEEK_MODEL=deepseek-chat +# ============================================================================= +# Life Echo API — production(生产) +# +# 仓库维护本文件;production 发布时 workflow 会上传并复制为运行时 .env。 +# 若仓库可被非授权人员访问,请不要在此文件中保留真实密钥。 +# ============================================================================= # ============================================================================= # Logging(loguru sink 最低级别:TRACE / DEBUG / INFO / WARNING / ERROR / CRITICAL) @@ -9,37 +11,43 @@ DEEPSEEK_MODEL=deepseek-chat LOG_LEVEL=INFO # ============================================================================= -# 数据库配置(必需) +# LLM / DeepSeek # ============================================================================= -# PostgreSQL 数据库连接 URL -# 格式: postgresql://用户名:密码@主机:端口/数据库名 -DATABASE_URL=postgresql://postgres:postgres@postgresql:5432/life_echo +DEEPSEEK_API_KEY=sk-09f17fb61c5a4299a3afc2a01de7af75 +DEEPSEEK_BASE_URL=https://api.deepseek.com +DEEPSEEK_MODEL=deepseek-chat # ============================================================================= -# Redis 配置(必需) +# Database # ============================================================================= -# Redis 连接 URL -# 格式: redis://[:密码@]主机:端口[/数据库编号] +# 本地开发: +# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo +# Docker / 服务端(主机名一般为 compose 服务名 postgres): +# DATABASE_URL=postgresql://postgres:postgres@postgres:5432/life_echo +DATABASE_URL=postgresql://postgres:postgres@postgres:5432/life_echo + +# ============================================================================= +# Redis +# ============================================================================= +# 本地开发: +# REDIS_URL=redis://localhost:6379/0 +# Docker / 服务端: +# REDIS_URL=redis://redis:6379/0 REDIS_URL=redis://redis:6379/0 -# Redis 会话过期时间(可选,默认: 86400 秒,即 24 小时) REDIS_SESSION_TTL=86400 # ============================================================================= -# 认证配置(必需) +# Auth # ============================================================================= -# JWT 签名密钥(必需) -# 建议使用随机生成的强密钥,例如: openssl rand -hex 32 -# 生产环境必须更换为强随机字符串 +# 建议使用: openssl rand -hex 32 SECRET_KEY=cf47555c7ecbe5ddb7fd2113c59e08a8bcb110810c42f7c644e06a5acc898608 -# JWT 算法(可选,默认: HS256) ALGORITHM=HS256 -# 访问令牌过期时间(可选,默认: 120 分钟,即 2 小时) ACCESS_TOKEN_EXPIRE_MINUTES=120 # ============================================================================= -# 腾讯云短信服务配置(必需) +# Tencent Cloud — 短信 # ============================================================================= -# 腾讯云 SecretId 和 SecretKey +# 短信、一句话 ASR/TTS、COS 为不同产品;同一主账号可共用同一对 SecretId/SecretKey(分别填三处)。 TENCENT_SMS_SECRET_ID=AKIDa2ILCwUr56uVt31oU0JOHxPfGhvvkLiq TENCENT_SMS_SECRET_KEY=xiFbjlZ9XheS2NWYLvHRPAh2A5nGYcR2 # 短信应用 SDK AppID @@ -49,86 +57,100 @@ TENCENT_SMS_SIGN_NAME=上海华嘎科技有限公司 # 短信模板 ID TENCENT_SMS_TEMPLATE_ID=2592163 # 短信模板参数数量(1=仅验证码,2=验证码+过期时间) -# 如果遇到 TemplateParamSetNotMatchApprovedTemplate 错误,请检查腾讯云控制台中的模板配置 -# 并根据实际模板参数数量设置此值 +# 若遇 TemplateParamSetNotMatchApprovedTemplate,请对照控制台模板配置 TENCENT_SMS_TEMPLATE_PARAM_COUNT=1 # ============================================================================= -# ASR Provider 选择 +# ASR Provider(whisper | tencent) # ============================================================================= -# ASR Provider: whisper(默认,本地 faster-whisper)| tencent(腾讯云一句话识别) ASR_PROVIDER=tencent # ============================================================================= -# Whisper ASR 配置(ASR_PROVIDER=whisper 时使用) +# Whisper ASR(ASR_PROVIDER=whisper 时使用) # ============================================================================= -# CPU 环境(推荐 small + int8) ASR_MODEL_SIZE=small ASR_DEVICE=cpu ASR_COMPUTE_TYPE=int8 -# GPU 环境(推荐 medium + float16) +# GPU 环境(示例,按需启用) # ASR_MODEL_SIZE=medium # ASR_DEVICE=cuda # ASR_COMPUTE_TYPE=float16 # ============================================================================= -# 腾讯云 ASR 配置(ASR_PROVIDER=tencent 时使用) +# Tencent Cloud — 一句话 ASR + TTS(ASR_PROVIDER=tencent 或 TTS_PROVIDER=tencent) # ============================================================================= -# 腾讯云 API 密钥(与短信服务共用,或单独配置语音服务专用密钥) TENCENT_SECRET_ID=AKIDa2ILCwUr56uVt31oU0JOHxPfGhvvkLiq TENCENT_SECRET_KEY=xiFbjlZ9XheS2NWYLvHRPAh2A5nGYcR2 -# 腾讯云 ASR 应用 ID(可选) -# TENCENT_ASR_APP_ID= +# ============================================================================= +# TTS(文字转语音,Agent 回复播音)— 与 ASR 独立 +# ============================================================================= +# ENABLE_TTS:仅控制是否合成并下发 TTS_AUDIO;不影响用户语音转写(ASR) +ENABLE_TTS=true +TTS_PROVIDER=tencent +# 仅 TTS_PROVIDER=openai 时需要 +# OPENAI_API_KEY=your_openai_api_key +# 音色 ID 见 https://cloud.tencent.com/document/product/1073/92668 +TTS_VOICE_TYPE=502001 +TTS_CODEC=mp3 + +# ============================================================================= +# WeChat Pay +# ============================================================================= WECHAT_PAY_APP_ID=wx1df508452e06cfb8 WECHAT_PAY_MCH_ID=1662979099 WECHAT_PAY_API_V3_KEY=xjvGSJLGJAJfjgskfjslafjsajsdjals -# 商户私钥:推荐使用文件路径,避免 .env 中长 PEM 的转义/换行导致 Invalid private key +# 商户私钥:推荐使用文件路径,避免 .env 中长 PEM 转义问题 WECHAT_PAY_PRIVATE_KEY_PATH=certs/apiclient_key.pem -# 若不用文件,可改为配置 WECHAT_PAY_PRIVATE_KEY(完整 PEM,换行用 \n) +# 若不用文件,可配置 WECHAT_PAY_PRIVATE_KEY(PEM,换行用 \n) # WECHAT_PAY_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" WECHAT_PAY_CERT_SERIAL_NO=1AA82328AC1456C6F115B014606F22CD621D2032 WECHAT_PAY_NOTIFY_URL=https://lifecho.worldsplats.com/api/payment/notify/wechat -# 平台公钥模式(仅当无法访问 api.mch.weixin.qq.com 时使用) -# 注意:必须填「微信支付平台公钥」的路径/内容,不能填商户私钥 apiclient_key.pem(否则会报 MismatchedTags PUBLIC KEY / PRIVATE KEY) -# 从商户平台「API安全」→「平台证书」获取微信平台公钥,保存为 PEM 后配置下面两项;否则留空走「证书模式」自动拉取。 +# 平台公钥模式(仅当无法走平台证书自动拉取时使用);勿填商户私钥路径 # WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH=certs/wechat_platform_public_key.pem # WECHAT_PAY_PLATFORM_PUBLIC_KEY_ID=PUB_KEY_ID_0116629790992026020700181671002400 +# ============================================================================= +# Alipay +# ============================================================================= ALIPAY_APP_ID=... ALIPAY_PRIVATE_KEY=... ALIPAY_PUBLIC_KEY=... ALIPAY_NOTIFY_URL=https://lifecho.worldsplats.com/api/payment/notify/alipay +# ============================================================================= +# Misc +# ============================================================================= ENABLE_TEST_SUBSCRIPTION=1 # ============================================================================= -# 回忆录图片生成配置 +# Memoir image generation(Story 主图等;轮询 Liblib 任务) # ============================================================================= -# 总开关(true 启用,不设置则关闭) MEMOIR_IMAGE_ENABLED=true -# 每章最多生成图片数 -MEMOIR_IMAGE_MAX_PER_CHAPTER=2 -# 轮询间隔(秒)和最大尝试次数 MEMOIR_IMAGE_POLL_INTERVAL=3 MEMOIR_IMAGE_MAX_ATTEMPTS=20 +MEMOIR_IMAGE_PROVIDER=liblib +MEMOIR_IMAGE_STYLE_DEFAULT=watercolor +MEMOIR_IMAGE_SIZE_DEFAULT=1280x720 +# 可选,Liblib 返回图片域名不在默认白名单时(逗号分隔) +# MEMOIR_IMAGE_DOWNLOAD_HOSTS= # ============================================================================= -# Liblib 图片生成 API(HMAC-SHA1 签名认证) +# Liblib image provider # ============================================================================= LIBLIB_ACCESS_KEY=zrDp6quCOKlLwcewOEfrog LIBLIB_SECRET_KEY=iTVHo5Nf3KA-xpC1Mja80bC93u6chJem LIBLIB_BASE_URL=https://openapi.liblibai.cloud -# 星流 Star-3 Alpha 文生图模板(默认值,可按需切换为其他模型) LIBLIB_TEMPLATE_UUID=5d7e67009b344550bc1aa6ccbfa1d7f4 # ============================================================================= -# 腾讯云 COS(回忆录图片存储) +# Tencent Cloud — COS(回忆录图片存储) # ============================================================================= TENCENT_COS_SECRET_ID=AKIDa2ILCwUr56uVt31oU0JOHxPfGhvvkLiq TENCENT_COS_SECRET_KEY=xiFbjlZ9XheS2NWYLvHRPAh2A5nGYcR2 TENCENT_COS_REGION=ap-shanghai -# 要把bucket改成生产环境的bucket TENCENT_COS_BUCKET=life-echo-prod-1319381411 -TENCENT_COS_BASE_URL=https://life-echo-prod-1319381411.cos.ap-shanghai.myqcloud.com \ No newline at end of file +TENCENT_COS_BASE_URL=https://life-echo-prod-1319381411.cos.ap-shanghai.myqcloud.com +# 可选临时凭证 +# TENCENT_COS_TOKEN= diff --git a/api/.env.staging b/api/.env.staging new file mode 100644 index 0000000..8491fac --- /dev/null +++ b/api/.env.staging @@ -0,0 +1,156 @@ +# ============================================================================= +# Life Echo API — staging(预发) +# +# 仓库维护本文件;staging 发布时 workflow 会上传并复制为运行时 .env。 +# 不要把生产密钥误填进本文件。 +# ============================================================================= + +# ============================================================================= +# Logging(loguru sink 最低级别:TRACE / DEBUG / INFO / WARNING / ERROR / CRITICAL) +# ============================================================================= +LOG_LEVEL=INFO + +# ============================================================================= +# LLM / DeepSeek +# ============================================================================= +DEEPSEEK_API_KEY=your_deepseek_api_key +DEEPSEEK_BASE_URL=https://api.deepseek.com +DEEPSEEK_MODEL=deepseek-chat + +# ============================================================================= +# Database +# ============================================================================= +# 本地开发: +# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/life_echo +# Docker / 服务端(主机名一般为 compose 服务名 postgres): +# DATABASE_URL=postgresql://postgres:postgres@postgres:5432/life_echo +DATABASE_URL=postgresql://postgres:postgres@postgres:5432/life_echo + +# ============================================================================= +# Redis +# ============================================================================= +# 本地开发: +# REDIS_URL=redis://localhost:6379/0 +# Docker / 服务端: +# REDIS_URL=redis://redis:6379/0 +REDIS_URL=redis://redis:6379/0 +REDIS_SESSION_TTL=86400 + +# ============================================================================= +# Auth +# ============================================================================= +# 建议使用: openssl rand -hex 32 +SECRET_KEY=replace_with_a_strong_random_secret +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=120 + +# ============================================================================= +# Tencent Cloud — 短信 +# ============================================================================= +# 短信、一句话 ASR/TTS、COS 为不同产品;同一主账号可共用同一对 SecretId/SecretKey(分别填三处)。 +TENCENT_SMS_SECRET_ID=your_tencent_sms_secret_id +TENCENT_SMS_SECRET_KEY=your_tencent_sms_secret_key +# 短信应用 SDK AppID +TENCENT_SMS_SDK_APP_ID=your_sms_sdk_app_id +# 短信签名内容(不包含【】符号) +TENCENT_SMS_SIGN_NAME=your_sms_sign_name +# 短信模板 ID +TENCENT_SMS_TEMPLATE_ID=your_sms_template_id +# 短信模板参数数量(1=仅验证码,2=验证码+过期时间) +# 若遇 TemplateParamSetNotMatchApprovedTemplate,请对照控制台模板配置 +TENCENT_SMS_TEMPLATE_PARAM_COUNT=1 + +# ============================================================================= +# ASR Provider(whisper | tencent) +# ============================================================================= +ASR_PROVIDER=whisper + +# ============================================================================= +# Whisper ASR(ASR_PROVIDER=whisper 时使用) +# ============================================================================= +ASR_MODEL_SIZE=small +ASR_DEVICE=cpu +ASR_COMPUTE_TYPE=int8 + +# GPU 环境(示例,按需启用) +# ASR_MODEL_SIZE=medium +# ASR_DEVICE=cuda +# ASR_COMPUTE_TYPE=float16 + +# ============================================================================= +# Tencent Cloud — 一句话 ASR + TTS(ASR_PROVIDER=tencent 或 TTS_PROVIDER=tencent) +# ============================================================================= +TENCENT_SECRET_ID=your_tencent_asr_secret_id +TENCENT_SECRET_KEY=your_tencent_asr_secret_key + +# ============================================================================= +# TTS(文字转语音,Agent 回复播音)— 与 ASR 独立 +# ============================================================================= +# ENABLE_TTS:仅控制是否合成并下发 TTS_AUDIO;不影响用户语音转写(ASR) +ENABLE_TTS=true +TTS_PROVIDER=tencent +# 仅 TTS_PROVIDER=openai 时需要 +# OPENAI_API_KEY=your_openai_api_key +# 音色 ID 见 https://cloud.tencent.com/document/product/1073/92668 +TTS_VOICE_TYPE=502001 +TTS_CODEC=mp3 + +# ============================================================================= +# WeChat Pay +# ============================================================================= +WECHAT_PAY_APP_ID=your_wechat_pay_app_id +WECHAT_PAY_MCH_ID=your_wechat_mch_id +WECHAT_PAY_API_V3_KEY=your_wechat_api_v3_key +# 商户私钥:推荐使用文件路径,避免 .env 中长 PEM 转义问题 +WECHAT_PAY_PRIVATE_KEY_PATH=certs/apiclient_key.pem +# 若不用文件,可配置 WECHAT_PAY_PRIVATE_KEY(PEM,换行用 \n) +# WECHAT_PAY_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" +WECHAT_PAY_CERT_SERIAL_NO=your_wechat_cert_serial_no +WECHAT_PAY_NOTIFY_URL=https://your-domain.com/api/payment/notify/wechat +# 平台公钥模式(仅当无法走平台证书自动拉取时使用);勿填商户私钥路径 +# WECHAT_PAY_PLATFORM_PUBLIC_KEY_PATH=certs/wechat_platform_public_key.pem +# WECHAT_PAY_PLATFORM_PUBLIC_KEY_ID=your_wechat_platform_public_key_id + +# ============================================================================= +# Alipay +# ============================================================================= +ALIPAY_APP_ID=your_alipay_app_id +ALIPAY_PRIVATE_KEY=your_alipay_private_key +ALIPAY_PUBLIC_KEY=your_alipay_public_key +ALIPAY_NOTIFY_URL=https://your-domain.com/api/payment/notify/alipay + +# ============================================================================= +# Misc +# ============================================================================= +ENABLE_TEST_SUBSCRIPTION=0 + +# ============================================================================= +# Memoir image generation(Story 主图等;轮询 Liblib 任务) +# ============================================================================= +MEMOIR_IMAGE_ENABLED=false +MEMOIR_IMAGE_POLL_INTERVAL=3 +MEMOIR_IMAGE_MAX_ATTEMPTS=20 +MEMOIR_IMAGE_PROVIDER=liblib +MEMOIR_IMAGE_STYLE_DEFAULT=watercolor +MEMOIR_IMAGE_SIZE_DEFAULT=1280x720 +# 可选,Liblib 返回图片域名不在默认白名单时(逗号分隔) +# MEMOIR_IMAGE_DOWNLOAD_HOSTS=liblib.cloud,liblibai.cloud + +# ============================================================================= +# Liblib image provider +# ============================================================================= +LIBLIB_ACCESS_KEY=your_liblib_access_key +LIBLIB_SECRET_KEY=your_liblib_secret_key +LIBLIB_BASE_URL=https://openapi.liblibai.cloud +LIBLIB_TEMPLATE_UUID=your_liblib_template_uuid + +# ============================================================================= +# Tencent Cloud — COS(回忆录图片存储) +# ============================================================================= +TENCENT_COS_SECRET_ID=your_tencent_cos_secret_id +TENCENT_COS_SECRET_KEY=your_tencent_cos_secret_key +TENCENT_COS_REGION=ap-shanghai +TENCENT_COS_BUCKET=your_bucket_name +TENCENT_COS_BASE_URL=https://your_bucket_name.cos.ap-shanghai.myqcloud.com +# 可选临时凭证 +# TENCENT_COS_TOKEN= diff --git a/api/Dockerfile b/api/Dockerfile index 70f0735..ee10208 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -30,9 +30,6 @@ COPY . . ARG ASR_MODEL_SIZE=small RUN uv run python -c "from faster_whisper import WhisperModel; WhisperModel('${ASR_MODEL_SIZE}', device='cpu', compute_type='int8', download_root='/app/models/whisper')" -# 复制生产环境配置 -COPY .env.production ./.env - # 非 root 用户 RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app USER appuser diff --git a/api/app/core/config.py b/api/app/core/config.py index d85aa6c..9c1a502 100644 --- a/api/app/core/config.py +++ b/api/app/core/config.py @@ -1,6 +1,10 @@ """ 统一配置:所有环境变量通过此模块的 Settings 单点读取。 业务代码只允许 import settings,禁止散落 os.getenv() / load_dotenv()。 + +本地开发时由 api/development.sh 在启动前将 .env.development 复制为 .env(若尚无 .env)。 +Docker / 服务端由镜像与 compose 注入进程环境;此处仅固定读取工作目录下的 .env 作为默认值来源。 +进程环境变量(容器 environment、export)覆盖 .env 同名项。 """ import secrets @@ -54,10 +58,9 @@ class Settings(BaseSettings): tencent_sms_template_id: str = "" tencent_sms_template_param_count: int = 2 - # ── Tencent ASR ────────────────────────────────────────── + # ── Tencent ASR / TTS(共用 Secret;与短信、COS 密钥独立)──────────────── tencent_secret_id: str = "" tencent_secret_key: str = "" - tencent_asr_app_id: str = "" # ── TTS (openai | tencent),与 ASR 独立:仅控制回复侧语音合成 ── enable_tts: bool = True @@ -94,15 +97,11 @@ class Settings(BaseSettings): enable_test_subscription: int = 0 enable_test_plan: str = "" # "1" / "true" / "yes" 为 True enable_docs: bool = True - migration_database_url: str = "" # 脚本迁移用,空则用 database_url # ── Memoir Image ───────────────────────────────────────── memoir_image_enabled: bool = False - memoir_image_max_per_chapter: int = 2 memoir_image_poll_interval: int = 3 memoir_image_max_attempts: int = 20 - memoir_image_chars_per_extra: int = 1500 - memoir_image_max_cap: int = 8 memoir_image_provider: str = "liblib" memoir_image_style_default: str = "watercolor" memoir_image_size_default: str = "1280x720" diff --git a/api/app/features/memoir/memoir_images/settings.py b/api/app/features/memoir/memoir_images/settings.py index 751ceaa..757d277 100644 --- a/api/app/features/memoir/memoir_images/settings.py +++ b/api/app/features/memoir/memoir_images/settings.py @@ -5,9 +5,6 @@ if TYPE_CHECKING: from app.core.config import Settings DEFAULT_LIBLIB_TEMPLATE_UUID = "5d7e67009b344550bc1aa6ccbfa1d7f4" -DEFAULT_MAX_IMAGES_PER_CHAPTER = 2 -DEFAULT_CHARS_PER_EXTRA_IMAGE = 1500 -DEFAULT_MAX_IMAGES_CAP = 8 DEFAULT_IMAGE_PROVIDER = "liblib" DEFAULT_IMAGE_STYLE = "watercolor" DEFAULT_IMAGE_SIZE = "1280x720" @@ -18,9 +15,6 @@ DEFAULT_MAX_ATTEMPTS = 60 @dataclass(frozen=True) class MemoirImageSettings: enabled: bool = False - max_per_chapter: int = DEFAULT_MAX_IMAGES_PER_CHAPTER - chars_per_extra_image: int = DEFAULT_CHARS_PER_EXTRA_IMAGE - max_images_cap: int = DEFAULT_MAX_IMAGES_CAP provider: str = DEFAULT_IMAGE_PROVIDER default_style: str = DEFAULT_IMAGE_STYLE default_size: str = DEFAULT_IMAGE_SIZE @@ -33,9 +27,6 @@ class MemoirImageSettings: s = settings return cls( enabled=bool(s.memoir_image_enabled), - max_per_chapter=s.memoir_image_max_per_chapter, - chars_per_extra_image=s.memoir_image_chars_per_extra, - max_images_cap=s.memoir_image_max_cap, provider=s.memoir_image_provider or DEFAULT_IMAGE_PROVIDER, default_style=s.memoir_image_style_default or DEFAULT_IMAGE_STYLE, default_size=s.memoir_image_size_default or DEFAULT_IMAGE_SIZE, @@ -49,15 +40,3 @@ class MemoirImageSettings: from app.core.config import settings as _s return cls.from_settings(_s) - - def effective_max_images(self, content_length: int) -> int: - """根据正文字数动态计算单章允许的最大图片数。""" - base_max = max(self.max_per_chapter, 0) - effective_cap = max(self.max_images_cap, base_max) - safe_length = max(content_length, 0) - extra = ( - safe_length // self.chars_per_extra_image - if self.chars_per_extra_image > 0 - else 0 - ) - return min(base_max + extra, effective_cap) diff --git a/api/app/tasks/memoir_tasks.py b/api/app/tasks/memoir_tasks.py index d5f0958..b18d8c5 100644 --- a/api/app/tasks/memoir_tasks.py +++ b/api/app/tasks/memoir_tasks.py @@ -181,34 +181,6 @@ def _memoir_image_from_asset( ) -def _select_placeholders_for_effective_max( - placeholders: list[dict], - existing_images: list[dict] | None, - effective_max: int, -) -> list[dict]: - existing_placeholders = { - item.get("placeholder") - for item in normalize_image_assets(existing_images) - if item.get("placeholder") - } - existing_count_in_content = sum( - 1 for item in placeholders if item.get("placeholder") in existing_placeholders - ) - remaining_new_slots = max(0, effective_max - existing_count_in_content) - - selected: list[dict] = [] - for item in placeholders: - if item.get("placeholder") in existing_placeholders: - selected.append(item) - continue - if remaining_new_slots <= 0: - continue - selected.append(item) - remaining_new_slots -= 1 - - return [{**item, "index": index} for index, item in enumerate(selected)] - - def _coerce_state(model: MemoirState) -> MemoirStateSchema: """将数据库模型转换为 Schema""" return MemoirStateSchema.model_validate( diff --git a/api/development.sh b/api/development.sh index a963e57..ba0d6ec 100755 --- a/api/development.sh +++ b/api/development.sh @@ -180,11 +180,27 @@ ensure_venv() { fi } +# 本地约定:仓库维护 .env.development;一键启动时复制为 .env,供 pydantic Settings(env_file=".env") 读取。 +# 若已存在 .env 则不覆盖(便于你本地覆盖);需要与模板同步时可删除 .env 后重新运行本脚本。 +ensure_dotenv_from_development() { + print_header "准备本地 .env" + if [[ -f "${ROOT_DIR}/.env" ]]; then + print_ok "已存在 .env(未覆盖)" + return 0 + fi + if [[ -f "${ROOT_DIR}/.env.development" ]]; then + cp "${ROOT_DIR}/.env.development" "${ROOT_DIR}/.env" + print_ok "已从 .env.development 复制为 .env" + return 0 + fi + print_warn "未找到 .env.development,无法自动生成 .env" + print_warn "请执行: cp api/.env.example api/.env.development 后按说明填写,再运行 ./development.sh" +} + check_env_file() { print_header "检查环境变量文件" if [[ ! -f "${ROOT_DIR}/.env" ]]; then print_warn "未找到 .env,应用可能因缺少配置启动失败" - print_warn "请参考 api/README.md 创建 .env" else print_ok "检测到 .env" fi @@ -242,6 +258,7 @@ main() { start_infra wait_postgres_ready || true ensure_venv + ensure_dotenv_from_development check_env_file run_migrations start_services diff --git a/api/docker-compose.yml b/api/docker-compose.yml index 2ec26f2..f199ba6 100644 --- a/api/docker-compose.yml +++ b/api/docker-compose.yml @@ -51,6 +51,7 @@ services: max-file: "3" # FastAPI 应用 + # 运行时统一读取 .env;部署时在远端将 .env.staging 或 .env.production 复制为 .env。 api: build: context: . @@ -60,10 +61,8 @@ services: expose: - "8000" env_file: - - .env.production + - .env environment: - - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/life_echo - - REDIS_URL=redis://redis:6379/0 - ASR_MODEL_CACHE_DIR=/app/models/whisper volumes: - /root/apiclient_key.pem:/app/certs/apiclient_key.pem:ro @@ -96,10 +95,7 @@ services: container_name: life-echo-celery-worker command: uv run celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4 env_file: - - .env.production - environment: - - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/life_echo - - REDIS_URL=redis://redis:6379/0 + - .env restart: always depends_on: postgres: @@ -153,7 +149,7 @@ services: # container_name: life-echo-celery-beat # command: celery -A app.tasks.celery_app beat --loglevel=info # env_file: - # - .env.production + # - .env # environment: # - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/life_echo # - REDIS_URL=redis://redis:6379/0 @@ -177,7 +173,7 @@ services: # ports: # - "5555:5555" # env_file: - # - .env.production + # - .env # environment: # - REDIS_URL=redis://redis:6379/0 # restart: always diff --git a/app-expo/.env.development b/app-expo/.env.development deleted file mode 100644 index dd9d8cf..0000000 --- a/app-expo/.env.development +++ /dev/null @@ -1,2 +0,0 @@ -EXPO_PUBLIC_API_URL=http://192.168.10.178:8000 -EXPO_PUBLIC_WS_URL=ws://192.168.10.178:8000 diff --git a/app-expo/.env.example b/app-expo/.env.example index dd9d8cf..b1fa4b8 100644 --- a/app-expo/.env.example +++ b/app-expo/.env.example @@ -1,2 +1,9 @@ -EXPO_PUBLIC_API_URL=http://192.168.10.178:8000 -EXPO_PUBLIC_WS_URL=ws://192.168.10.178:8000 +# 复制为 .env.development / .env.staging / .env.production 后填写(勿提交含密钥的副本)。 +# 本地:npm start 会通过 prestart 执行 `use-env development` 生成 .env; +# 或手动 `npm run use-env -- staging` / `npm run use-env -- production`。 +# CI:GitHub Actions 在构建 APK 前会按分支调用 use-env(main → staging,tag → production)。 +# +# 变量在构建时注入;修改后需重新 prebuild/打包客户端。 + +EXPO_PUBLIC_API_URL=https://your-api.example.com +EXPO_PUBLIC_WS_URL=wss://your-api.example.com diff --git a/app-expo/scripts/use-env.js b/app-expo/scripts/use-env.js index eb68fd0..4af3221 100644 --- a/app-expo/scripts/use-env.js +++ b/app-expo/scripts/use-env.js @@ -1,3 +1,13 @@ +/** + * 将 app-expo/.env. 复制为 .env,供 Metro/Expo 读取 EXPO_PUBLIC_*。 + * + * 参数 name → 源文件: + * development → .env.development(本地默认:npm start / prestart) + * staging → .env.staging + * production → .env.production + * + * CI:.github/workflows/app-expo-deploy.yml 按分支/tag 调用本脚本,与后端 env 策略对齐。 + */ const fs = require('fs'); const path = require('path'); const env = process.argv[2] || 'development'; diff --git a/app-expo/src/app/(tabs)/memoir.tsx b/app-expo/src/app/(tabs)/memoir.tsx index a80dbb8..81244ea 100644 --- a/app-expo/src/app/(tabs)/memoir.tsx +++ b/app-expo/src/app/(tabs)/memoir.tsx @@ -24,7 +24,10 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Text } from '@/components/ui/text'; import { ScreenGutter } from '@/constants/layout'; import { useCreateConversation } from '@/features/conversation/hooks'; -import { buildFrameworkChapterPlaceholders } from '@/features/memoir/framework-chapter-keys'; +import { + buildFrameworkChapterPlaceholders, + mergeFrameworkChaptersWithFetched, +} from '@/features/memoir/framework-chapter-keys'; import { useChapters, useCheckCoverGeneration } from '@/features/memoir/hooks'; import type { ChapterViewModel } from '@/features/memoir/types'; @@ -263,6 +266,11 @@ export default function MemoirScreen() { [t], ); + const displayChapters = useMemo( + () => mergeFrameworkChaptersWithFetched(frameworkPlaceholders, chapters), + [frameworkPlaceholders, chapters], + ); + useEffect(() => { if (didRunInitialCoverCheckRef.current) return; didRunInitialCoverCheckRef.current = true; @@ -318,19 +326,8 @@ export default function MemoirScreen() { ) : isError ? ( void refetch()} /> - ) : chapters.length === 0 ? ( - frameworkPlaceholders.map((item) => ( - string} - onReadPress={() => handleReadChapter(item.id)} - onContinuePress={handleStartChapter} - /> - )) ) : ( - chapters.map((item) => ( + displayChapters.map((item) => ( (); + for (const vm of fetched) { + byOrder.set(vm.orderIndex, vm); + } + return placeholders.map((p) => byOrder.get(p.orderIndex) ?? p); +}