diff --git a/CLAUDE.md b/CLAUDE.md index e666214..9340e4d 100644 --- a/CLAUDE.md +++ b/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 ".[dev]" # 安装开发依赖 uvicorn app.main:app --reload --port 8030 # 启动开发服务器 (localhost:8030) -python scripts/init_db.py [用户名] [密码] # 初始化数据库管理员账户 +python scripts/init_db.py [用户名] [密码] # 初始化数据库管理员账户(默认 admin/admin123) # 测试与代码检查 pytest # 运行测试 +pytest tests/test_xxx.py::test_name # 运行单个测试 ruff check . # 代码检查 ruff format . # 代码格式化 ``` @@ -31,54 +32,68 @@ npm install # 安装依赖 npm run dev # 启动开发服务器 (localhost:3030) npm run build # 构建生产版本 npm run lint # ESLint 检查 +npm run clean:next # 清理 .next 缓存(遇到 404 或缓存问题时使用) ``` ### Docker 部署 (infra) ```bash cd infra -cp .env.example .env # 配置环境变量 -docker-compose up -d # 启动所有服务 +cp .env.example .env # 配置环境变量(必须设置 LLM_API_KEY) +docker-compose up -d # 启动所有服务(web:3030, api:8030, redis:6379, mysql:3306) ``` ## 架构概览 ``` apps/ - web/ # Next.js 14 前端 (TypeScript + Tailwind CSS) - api/ # FastAPI 后端 (Python 3.11+, async) + web/ # Next.js 14 前端 (TypeScript + Tailwind CSS + shadcn/ui) + api/ # FastAPI 后端 (Python 3.11+, 全异步) infra/ # Docker Compose 配置 ``` ### 后端结构 (apps/api/app) -- `main.py` - FastAPI 应用入口,配置中间件和路由 +- `main.py` - FastAPI 应用入口,lifespan 管理各服务连接 - `core/` - 配置(config.py)、日志(logging.py)、数据库(database.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 +- `api/` - 路由层:translate.py、admin.py、ai_provider.py、stats.py +- `services/` - 业务逻辑(单例模式):llm.py、cache.py、rate_limit.py、auth.py、stats.py - `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 端点 - `POST /api/v1/translate` - 非流式翻译 -- `POST /api/v1/translate/stream` - SSE 流式翻译 +- `POST /api/v1/translate/stream` - SSE 流式翻译(event: meta → chunk → done) - `GET /health` - 健康检查 ### 数据流 -1. 请求 → 限流检查(Redis) → 缓存查询(Redis) -2. 缓存命中 → 直接返回 -3. 缓存未命中 → LLM 调用 → 写入缓存 → 返回 +1. 请求 → IP 限流检查(Redis) → 缓存查询(Redis) +2. 缓存命中 → 直接返回(流式模式下一次性返回完整翻译) +3. 缓存未命中 → LLM 调用 → 异步写入缓存 → 返回 + +### AI Provider 配置优先级 + +1. 数据库中 `is_active=True && is_default=True` 的 AIProvider +2. 回退到环境变量 `LLM_API_KEY`、`LLM_BASE_URL`、`LLM_MODEL` ## 环境变量 后端 `.env` 关键配置: - `LLM_API_KEY` - LLM 服务 API Key(必需) - `LLM_MODEL` - 模型名称(默认 gpt-4o-mini) -- `LLM_BASE_URL` - 自定义 API 地址(可选) -- `DATABASE_URL` - MySQL 连接串 +- `LLM_BASE_URL` - 自定义 API 地址(可选,默认 OpenAI) +- `DATABASE_URL` - MySQL 连接串(格式:mysql+aiomysql://user:pass@host:3306/db) - `REDIS_URL` - Redis 连接串 前端 `.env.local`: -- `NEXT_PUBLIC_API_BASE_URL` - 后端 API 地址 +- `NEXT_PUBLIC_API_BASE_URL` - 后端 API 地址(如 http://localhost:8030) diff --git a/apps/api/app/services/auth.py b/apps/api/app/services/auth.py index 6574e26..08bd8a6 100644 --- a/apps/api/app/services/auth.py +++ b/apps/api/app/services/auth.py @@ -1,18 +1,20 @@ from datetime import datetime, timedelta +import bcrypt from jose import jwt, JWTError -from passlib.context import CryptContext from ..core import get_settings settings = get_settings() -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 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: - 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: diff --git a/apps/api/app/services/llm.py b/apps/api/app/services/llm.py index 568939d..86ad824 100644 --- a/apps/api/app/services/llm.py +++ b/apps/api/app/services/llm.py @@ -112,6 +112,7 @@ class LLMService: "stream": True, }, ) as response: + response.raise_for_status() async for line in response.aiter_lines(): if line.startswith("data: "): data = line[6:] diff --git a/apps/web/app/admin/providers/page.tsx b/apps/web/app/admin/providers/page.tsx index 4b1bf03..09e47a3 100644 --- a/apps/web/app/admin/providers/page.tsx +++ b/apps/web/app/admin/providers/page.tsx @@ -55,6 +55,7 @@ export default function ProvidersPage() { latency_ms?: number message?: string } | null>(null) + const [testingId, setTestingId] = useState(null) const [form, setForm] = useState({ name: '', @@ -234,6 +235,7 @@ export default function ProvidersPage() { const testProvider = async (provider: Provider) => { setTestResult(null) + setTestingId(provider.id) setError('') try { @@ -249,6 +251,8 @@ export default function ProvidersPage() { return } setTestResult({ ok: false, message: err?.message || '测试失败' }) + } finally { + setTestingId(null) } } @@ -278,6 +282,24 @@ export default function ProvidersPage() { )} + {testResult && ( +
+ {testResult.ok ? ( +
+ 测试成功{typeof testResult.latency_ms === 'number' ? `(延迟 ${testResult.latency_ms}ms)` : ''} +
+ ) : ( +
{testResult.message || '测试失败'}
+ )} +
+ )} +
编辑 -