feat:修复BUG
This commit is contained in:
49
CLAUDE.md
49
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
AI 翻译网站 - Monorepo 架构,前端 Next.js + 后端 FastAPI,支持流式输出(SSE)、Redis 缓存/限流、PostgreSQL 数据存储。
|
AI 翻译网站 - Monorepo 架构,前端 Next.js 14 + 后端 FastAPI,支持流式输出(SSE)、Redis 缓存/限流、MySQL 数据存储。
|
||||||
|
|
||||||
## 常用命令
|
## 常用命令
|
||||||
|
|
||||||
@@ -15,10 +15,11 @@ cd apps/api
|
|||||||
pip install -e . # 安装依赖
|
pip install -e . # 安装依赖
|
||||||
pip install -e ".[dev]" # 安装开发依赖
|
pip install -e ".[dev]" # 安装开发依赖
|
||||||
uvicorn app.main:app --reload --port 8030 # 启动开发服务器 (localhost:8030)
|
uvicorn app.main:app --reload --port 8030 # 启动开发服务器 (localhost:8030)
|
||||||
python scripts/init_db.py [用户名] [密码] # 初始化数据库管理员账户
|
python scripts/init_db.py [用户名] [密码] # 初始化数据库管理员账户(默认 admin/admin123)
|
||||||
|
|
||||||
# 测试与代码检查
|
# 测试与代码检查
|
||||||
pytest # 运行测试
|
pytest # 运行测试
|
||||||
|
pytest tests/test_xxx.py::test_name # 运行单个测试
|
||||||
ruff check . # 代码检查
|
ruff check . # 代码检查
|
||||||
ruff format . # 代码格式化
|
ruff format . # 代码格式化
|
||||||
```
|
```
|
||||||
@@ -31,54 +32,68 @@ npm install # 安装依赖
|
|||||||
npm run dev # 启动开发服务器 (localhost:3030)
|
npm run dev # 启动开发服务器 (localhost:3030)
|
||||||
npm run build # 构建生产版本
|
npm run build # 构建生产版本
|
||||||
npm run lint # ESLint 检查
|
npm run lint # ESLint 检查
|
||||||
|
npm run clean:next # 清理 .next 缓存(遇到 404 或缓存问题时使用)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker 部署 (infra)
|
### Docker 部署 (infra)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd infra
|
cd infra
|
||||||
cp .env.example .env # 配置环境变量
|
cp .env.example .env # 配置环境变量(必须设置 LLM_API_KEY)
|
||||||
docker-compose up -d # 启动所有服务
|
docker-compose up -d # 启动所有服务(web:3030, api:8030, redis:6379, mysql:3306)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 架构概览
|
## 架构概览
|
||||||
|
|
||||||
```
|
```
|
||||||
apps/
|
apps/
|
||||||
web/ # Next.js 14 前端 (TypeScript + Tailwind CSS)
|
web/ # Next.js 14 前端 (TypeScript + Tailwind CSS + shadcn/ui)
|
||||||
api/ # FastAPI 后端 (Python 3.11+, async)
|
api/ # FastAPI 后端 (Python 3.11+, 全异步)
|
||||||
infra/ # Docker Compose 配置
|
infra/ # Docker Compose 配置
|
||||||
```
|
```
|
||||||
|
|
||||||
### 后端结构 (apps/api/app)
|
### 后端结构 (apps/api/app)
|
||||||
|
|
||||||
- `main.py` - FastAPI 应用入口,配置中间件和路由
|
- `main.py` - FastAPI 应用入口,lifespan 管理各服务连接
|
||||||
- `core/` - 配置(config.py)、日志(logging.py)、数据库(database.py)
|
- `core/` - 配置(config.py)、日志(logging.py)、数据库(database.py)
|
||||||
- `api/` - 路由层:translate.py(翻译)、admin.py(管理)、ai_provider.py、stats.py
|
- `api/` - 路由层:translate.py、admin.py、ai_provider.py、stats.py
|
||||||
- `services/` - 业务逻辑:llm.py(LLM调用)、cache.py(Redis缓存)、rate_limit.py(限流)、auth.py、stats.py
|
- `services/` - 业务逻辑(单例模式):llm.py、cache.py、rate_limit.py、auth.py、stats.py
|
||||||
- `schemas/` - Pydantic 请求/响应模型
|
- `schemas/` - Pydantic 请求/响应模型
|
||||||
- `models/` - SQLAlchemy ORM 模型
|
- `models/` - SQLAlchemy ORM 模型(AIProvider、Admin、UsageStats)
|
||||||
|
|
||||||
|
### 前端结构 (apps/web)
|
||||||
|
|
||||||
|
- `app/` - Next.js App Router 页面
|
||||||
|
- `page.tsx` - 翻译主页
|
||||||
|
- `login/` - 管理员登录
|
||||||
|
- `admin/` - 后台管理(providers、stats、settings)
|
||||||
|
- `components/` - React 组件(TranslatorForm 等)
|
||||||
|
|
||||||
### 核心 API 端点
|
### 核心 API 端点
|
||||||
|
|
||||||
- `POST /api/v1/translate` - 非流式翻译
|
- `POST /api/v1/translate` - 非流式翻译
|
||||||
- `POST /api/v1/translate/stream` - SSE 流式翻译
|
- `POST /api/v1/translate/stream` - SSE 流式翻译(event: meta → chunk → done)
|
||||||
- `GET /health` - 健康检查
|
- `GET /health` - 健康检查
|
||||||
|
|
||||||
### 数据流
|
### 数据流
|
||||||
|
|
||||||
1. 请求 → 限流检查(Redis) → 缓存查询(Redis)
|
1. 请求 → IP 限流检查(Redis) → 缓存查询(Redis)
|
||||||
2. 缓存命中 → 直接返回
|
2. 缓存命中 → 直接返回(流式模式下一次性返回完整翻译)
|
||||||
3. 缓存未命中 → LLM 调用 → 写入缓存 → 返回
|
3. 缓存未命中 → LLM 调用 → 异步写入缓存 → 返回
|
||||||
|
|
||||||
|
### AI Provider 配置优先级
|
||||||
|
|
||||||
|
1. 数据库中 `is_active=True && is_default=True` 的 AIProvider
|
||||||
|
2. 回退到环境变量 `LLM_API_KEY`、`LLM_BASE_URL`、`LLM_MODEL`
|
||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
后端 `.env` 关键配置:
|
后端 `.env` 关键配置:
|
||||||
- `LLM_API_KEY` - LLM 服务 API Key(必需)
|
- `LLM_API_KEY` - LLM 服务 API Key(必需)
|
||||||
- `LLM_MODEL` - 模型名称(默认 gpt-4o-mini)
|
- `LLM_MODEL` - 模型名称(默认 gpt-4o-mini)
|
||||||
- `LLM_BASE_URL` - 自定义 API 地址(可选)
|
- `LLM_BASE_URL` - 自定义 API 地址(可选,默认 OpenAI)
|
||||||
- `DATABASE_URL` - MySQL 连接串
|
- `DATABASE_URL` - MySQL 连接串(格式:mysql+aiomysql://user:pass@host:3306/db)
|
||||||
- `REDIS_URL` - Redis 连接串
|
- `REDIS_URL` - Redis 连接串
|
||||||
|
|
||||||
前端 `.env.local`:
|
前端 `.env.local`:
|
||||||
- `NEXT_PUBLIC_API_BASE_URL` - 后端 API 地址
|
- `NEXT_PUBLIC_API_BASE_URL` - 后端 API 地址(如 http://localhost:8030)
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import bcrypt
|
||||||
from jose import jwt, JWTError
|
from jose import jwt, JWTError
|
||||||
from passlib.context import CryptContext
|
|
||||||
from ..core import get_settings
|
from ..core import get_settings
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
return pwd_context.hash(password)
|
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def verify_password(plain: str, hashed: str) -> bool:
|
def verify_password(plain: str, hashed: str) -> bool:
|
||||||
return pwd_context.verify(plain, hashed)
|
try:
|
||||||
|
return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def create_token(data: dict) -> str:
|
def create_token(data: dict) -> str:
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ class LLMService:
|
|||||||
"stream": True,
|
"stream": True,
|
||||||
},
|
},
|
||||||
) as response:
|
) as response:
|
||||||
|
response.raise_for_status()
|
||||||
async for line in response.aiter_lines():
|
async for line in response.aiter_lines():
|
||||||
if line.startswith("data: "):
|
if line.startswith("data: "):
|
||||||
data = line[6:]
|
data = line[6:]
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default function ProvidersPage() {
|
|||||||
latency_ms?: number
|
latency_ms?: number
|
||||||
message?: string
|
message?: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
const [testingId, setTestingId] = useState<number | null>(null)
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -234,6 +235,7 @@ export default function ProvidersPage() {
|
|||||||
|
|
||||||
const testProvider = async (provider: Provider) => {
|
const testProvider = async (provider: Provider) => {
|
||||||
setTestResult(null)
|
setTestResult(null)
|
||||||
|
setTestingId(provider.id)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -249,6 +251,8 @@ export default function ProvidersPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setTestResult({ ok: false, message: err?.message || '测试失败' })
|
setTestResult({ ok: false, message: err?.message || '测试失败' })
|
||||||
|
} finally {
|
||||||
|
setTestingId(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +282,24 @@ export default function ProvidersPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{testResult && (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
testResult.ok
|
||||||
|
? 'rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300'
|
||||||
|
: 'rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{testResult.ok ? (
|
||||||
|
<div>
|
||||||
|
测试成功{typeof testResult.latency_ms === 'number' ? `(延迟 ${testResult.latency_ms}ms)` : ''}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>{testResult.message || '测试失败'}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
@@ -379,8 +401,8 @@ export default function ProvidersPage() {
|
|||||||
<Pencil className="mr-2 h-4 w-4" />
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="outline" onClick={() => testProvider(p)}>
|
<Button size="sm" variant="outline" onClick={() => testProvider(p)} disabled={testingId === p.id}>
|
||||||
测试
|
{testingId === p.id ? '测试中...' : '测试'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="destructive" onClick={() => handleDelete(p)}>
|
<Button size="sm" variant="destructive" onClick={() => handleDelete(p)}>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
Reference in New Issue
Block a user