various fixes
This commit is contained in:
15
.github/workflows/app-expo-deploy.yml
vendored
15
.github/workflows/app-expo-deploy.yml
vendored
@@ -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
|
||||
|
||||
65
.github/workflows/docker-build-deploy.yml
vendored
65
.github/workflows/docker-build-deploy.yml
vendored
@@ -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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
|
||||
89
README.md
89
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 本项目
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
TENCENT_COS_BASE_URL=https://life-echo-prod-1319381411.cos.ap-shanghai.myqcloud.com
|
||||
# 可选临时凭证
|
||||
# TENCENT_COS_TOKEN=
|
||||
|
||||
156
api/.env.staging
Normal file
156
api/.env.staging
Normal file
@@ -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=
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
EXPO_PUBLIC_API_URL=http://192.168.10.178:8000
|
||||
EXPO_PUBLIC_WS_URL=ws://192.168.10.178:8000
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* 将 app-expo/.env.<name> 复制为 .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';
|
||||
|
||||
@@ -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 ? (
|
||||
<MemoirLoadError onRetry={() => void refetch()} />
|
||||
) : chapters.length === 0 ? (
|
||||
frameworkPlaceholders.map((item) => (
|
||||
<ChapterCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
variant="drafting"
|
||||
t={t as (key: string) => string}
|
||||
onReadPress={() => handleReadChapter(item.id)}
|
||||
onContinuePress={handleStartChapter}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
chapters.map((item) => (
|
||||
displayChapters.map((item) => (
|
||||
<ChapterCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
|
||||
@@ -35,3 +35,17 @@ export function buildFrameworkChapterPlaceholders(
|
||||
wordCount: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页始终展示 8 个框架槽位:已有章节用接口数据,其余槽位仍用框架占位(与「零章节」时一致)。
|
||||
*/
|
||||
export function mergeFrameworkChaptersWithFetched(
|
||||
placeholders: ChapterViewModel[],
|
||||
fetched: ChapterViewModel[],
|
||||
): ChapterViewModel[] {
|
||||
const byOrder = new Map<number, ChapterViewModel>();
|
||||
for (const vm of fetched) {
|
||||
byOrder.set(vm.orderIndex, vm);
|
||||
}
|
||||
return placeholders.map((p) => byOrder.get(p.orderIndex) ?? p);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user