diff --git a/CONTRIBUTING.en.md b/CONTRIBUTING.en.md
index e8b8c7f..2e14262 100644
--- a/CONTRIBUTING.en.md
+++ b/CONTRIBUTING.en.md
@@ -2,30 +2,23 @@
Language: [中文](CONTRIBUTING.md) | [English](CONTRIBUTING.en.md)
-Thank you for contributing to DS2API!
+Thanks for contributing to DS2API.
## Development Setup
-### Backend
+### Backend (Go)
```bash
-# 1. Clone the repo
+# 1. Clone
git clone https://github.com/CJackHwang/ds2api.git
cd ds2api
-# 2. Create a virtual environment (recommended)
-python -m venv venv
-source venv/bin/activate # Windows: venv\Scripts\activate
-
-# 3. Install dependencies
-pip install -r requirements.txt
-
-# 4. Configure
+# 2. Configure
cp config.example.json config.json
# Edit config.json
-# 5. Run
-python dev.py
+# 3. Run backend
+go run ./cmd/ds2api
```
### Frontend (WebUI)
@@ -36,59 +29,42 @@ npm install
npm run dev
```
-WebUI language packs live in `webui/src/locales/`. Add new locale JSON files there.
+WebUI locales are in `webui/src/locales/`.
## Code Standards
-- **Python**: Follow PEP 8, use 4-space indentation
-- **JavaScript/React**: Use 4-space indentation and function components
-- **Commit messages**: Use semantic prefixes (e.g. `feat:`, `fix:`, `docs:`)
+- **Go**: run `gofmt` and make sure `go test ./...` passes before committing
+- **JavaScript/React**: follow existing project style (functional components)
+- **Commit messages**: use semantic prefixes (`feat:`, `fix:`, `docs:`)
## Submitting a PR
-1. Fork this repo
-2. Create a feature branch (`git checkout -b feature/xxx`)
-3. Commit your changes (`git commit -m 'feat: add xxx'`)
-4. Push your branch (`git push origin feature/xxx`)
+1. Fork the repo
+2. Create a branch (e.g. `feature/xxx`)
+3. Commit changes
+4. Push your branch
5. Open a Pull Request
-## WebUI Build
+## Build WebUI
-> **Important**: After modifying `webui/`, **no manual build is required**.
-
-When a PR is merged into `main`, GitHub Actions will automatically:
-1. Build the WebUI
-2. Commit build artifacts to `static/admin/`
-
-If you need a local build (for testing):
```bash
./scripts/build-webui.sh
```
## Project Structure
-```
+```text
ds2api/
-├── app.py # FastAPI entrypoint
-├── dev.py # Development server
-├── core/ # Core modules
-│ ├── auth.py # Account auth & rotation
-│ ├── config.py # Configuration management
-│ ├── deepseek.py # DeepSeek API calls
-│ ├── models.py # Model definitions
-│ ├── pow.py # PoW calculations
-│ └── sse_parser.py # SSE parsing
-├── routes/ # API routes
-│ ├── openai.py # OpenAI-compatible endpoints
-│ ├── claude.py # Claude-compatible endpoints
-│ ├── home.py # Landing page routes
-│ └── admin/ # Admin endpoints
+├── cmd/ds2api/ # Local/container entrypoint
+├── api/index.go # Vercel serverless entrypoint
+├── internal/ # Go backend implementation
├── webui/ # React WebUI source
-├── static/admin/ # WebUI build output (auto-generated)
-└── scripts/ # Helper scripts
+├── static/admin/ # WebUI build output
+├── Dockerfile
+├── docker-compose.yml
+└── vercel.json
```
## Reporting Issues
-- Use [GitHub Issues](https://github.com/CJackHwang/ds2api/issues)
-- Provide detailed reproduction steps and logs
+Please use [GitHub Issues](https://github.com/CJackHwang/ds2api/issues) with reproducible steps and logs.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f35dcc8..2d87734 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,33 +2,26 @@
语言 / Language: [中文](CONTRIBUTING.md) | [English](CONTRIBUTING.en.md)
-感谢你对 DS2API 的贡献!
+感谢你对 DS2API 的贡献。
## 开发环境设置
-### 后端
+### 后端(Go)
```bash
# 1. 克隆仓库
git clone https://github.com/CJackHwang/ds2api.git
cd ds2api
-# 2. 创建虚拟环境(推荐)
-python -m venv venv
-source venv/bin/activate # Windows: venv\Scripts\activate
-
-# 3. 安装依赖
-pip install -r requirements.txt
-
-# 4. 配置
+# 2. 配置
cp config.example.json config.json
# 编辑 config.json
-# 5. 启动
-python dev.py
+# 3. 启动后端
+go run ./cmd/ds2api
```
-### 前端 (WebUI)
+### 前端(WebUI)
```bash
cd webui
@@ -36,59 +29,42 @@ npm install
npm run dev
```
-WebUI 语言包位于 `webui/src/locales/`,新增语言请在此处添加对应 JSON 文件。
+WebUI 语言包位于 `webui/src/locales/`。
## 代码规范
-- **Python**: 遵循 PEP 8,使用 4 空格缩进
-- **JavaScript/React**: 使用 4 空格缩进,使用函数组件
-- **提交信息**: 使用语义化提交格式(如 `feat:`, `fix:`, `docs:`)
+- **Go**: 提交前运行 `gofmt`,并确保 `go test ./...` 通过
+- **JavaScript/React**: 保持现有代码风格(函数组件)
+- **提交信息**: 使用语义化前缀(`feat:`, `fix:`, `docs:`)
## 提交 PR
-1. Fork 本仓库
-2. 创建功能分支 (`git checkout -b feature/xxx`)
-3. 提交更改 (`git commit -m 'feat: 添加xxx功能'`)
-4. 推送分支 (`git push origin feature/xxx`)
-5. 创建 Pull Request
+1. Fork 仓库
+2. 创建分支(如 `feature/xxx`)
+3. 提交更改
+4. 推送分支
+5. 发起 Pull Request
## WebUI 构建
-> **重要**: 修改 `webui/` 目录后 **无需手动构建**!
-
-当 PR 合并到 `main` 分支后,GitHub Actions 会自动:
-1. 构建 WebUI
-2. 提交构建产物到 `static/admin/`
-
-如果需要本地构建(测试用):
```bash
./scripts/build-webui.sh
```
## 项目结构
-```
+```text
ds2api/
-├── app.py # FastAPI 应用入口
-├── dev.py # 开发服务器
-├── core/ # 核心模块
-│ ├── auth.py # 账号认证与轮询
-│ ├── config.py # 配置管理
-│ ├── deepseek.py # DeepSeek API 调用
-│ ├── models.py # 模型定义
-│ ├── pow.py # PoW 计算
-│ └── sse_parser.py # SSE 解析
-├── routes/ # API 路由
-│ ├── openai.py # OpenAI 兼容接口
-│ ├── claude.py # Claude 兼容接口
-│ ├── home.py # 首页路由
-│ └── admin/ # 管理接口
+├── cmd/ds2api/ # 本地/容器启动入口
+├── api/index.go # Vercel Serverless 入口
+├── internal/ # Go 后端核心实现
├── webui/ # React WebUI 源码
-├── static/admin/ # WebUI 构建产物(自动生成)
-└── scripts/ # 辅助脚本
+├── static/admin/ # WebUI 构建产物
+├── Dockerfile
+├── docker-compose.yml
+└── vercel.json
```
## 问题反馈
-- 使用 [GitHub Issues](https://github.com/CJackHwang/ds2api/issues) 报告问题
-- 提供详细的复现步骤和日志信息
+请使用 [GitHub Issues](https://github.com/CJackHwang/ds2api/issues) 并附复现步骤与日志。
diff --git a/DEPLOY.en.md b/DEPLOY.en.md
index 69f3f54..42ce015 100644
--- a/DEPLOY.en.md
+++ b/DEPLOY.en.md
@@ -1,410 +1,80 @@
-# DS2API Deployment Guide
+# DS2API Deployment Guide (Go)
Language: [中文](DEPLOY.md) | [English](DEPLOY.en.md)
-This document covers all supported DS2API deployment methods.
+## Contents
----
+- Vercel deployment
+- Docker deployment
+- Local run
+- systemd deployment
-## Table of Contents
+## Vercel Deployment
-- [Vercel Deployment (Recommended)](#vercel-deployment-recommended)
-- [Docker Deployment (Recommended)](#docker-deployment-recommended)
-- [Local Development](#local-development)
-- [Production Deployment](#production-deployment)
-- [FAQ](#faq)
+1. Import the repository into Vercel
+2. Set required environment variables:
+- `DS2API_ADMIN_KEY`
+- `DS2API_CONFIG_JSON` (JSON or Base64)
+3. Deploy and open `/admin`
----
+The project uses `api/index.go` as the serverless entrypoint. See `vercel.json`.
-## Vercel Deployment (Recommended)
-
-### One-click deployment
-
-[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api&env=DS2API_ADMIN_KEY&envDescription=Admin%20console%20access%20key%20%28required%29&envLink=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api%23environment-variables&project-name=ds2api&repository-name=ds2api)
-
-### Steps
-
-1. **Click the deploy button**
- - Sign in to GitHub
- - Authorize Vercel access
-
-2. **Set environment variables**
- - `DS2API_ADMIN_KEY`: Admin console password (**required**)
-
-3. **Wait for deployment**
- - Vercel builds and deploys automatically
- - You will receive a deployment URL
-
-4. **Configure accounts**
- - Visit `https://your-project.vercel.app/admin`
- - Log in with the admin key
- - Add DeepSeek accounts
- - Set custom API keys
-
-5. **Sync configuration**
- - Click "Sync to Vercel"
- - The first sync requires a Vercel token and project ID
- - After sync, the configuration is persisted
-
-### Get Vercel credentials
-
-**Vercel token**:
-1. Visit https://vercel.com/account/tokens
-2. Click "Create Token"
-3. Set a name and expiration
-4. Copy the token
-
-**Project ID**:
-1. Open your Vercel project
-2. Go to Settings → General
-3. Copy the "Project ID"
-
----
-
-## Local Development
-
-### Requirements
-
-- Python 3.9+
-- Node.js 18+ (WebUI development)
-- pip
-
-### Quick start
+## Docker Deployment
```bash
-# 1. Clone the repo
-git clone https://github.com/CJackHwang/ds2api.git
-cd ds2api
-
-# 2. Install Python dependencies
-pip install -r requirements.txt
-
-# 3. Configure accounts
-cp config.example.json config.json
-# Edit config.json and fill in DeepSeek account info
-
-# 4. Start the service
-python dev.py
-```
-
-### Config example
-
-```json
-{
- "keys": ["my-api-key-1", "my-api-key-2"],
- "accounts": [
- {
- "email": "your-email@example.com",
- "password": "your-password",
- "token": ""
- },
- {
- "mobile": "12345678901",
- "password": "your-password",
- "token": ""
- }
- ]
-}
-```
-
-**Notes**:
-- `keys`: Custom API keys for calling the service
-- `accounts`: DeepSeek Web accounts
- - Supports `email` or `mobile` login
- - Leave `token` blank; it will be fetched automatically
-
-### WebUI development
-
-```bash
-# Enter the WebUI directory
-cd webui
-
-# Install dependencies
-npm install
-
-# Start the dev server
-npm run dev
-```
-
-The WebUI dev server runs on `http://localhost:5173` and proxies API requests to `http://localhost:5001`.
-
-### WebUI build
-
-Build artifacts are located in `static/admin/`.
-
-**Automatic build (recommended)**:
-- Vercel builds the WebUI during deployment (see `vercel.json` `buildCommand`)
-- The GitHub Actions WebUI build workflow is disabled
-- `static/admin/` build artifacts are no longer committed
-
-**Manual build**:
-```bash
-# Option 1: use script
-./scripts/build-webui.sh
-
-# Option 2: run directly
-cd webui
-npm install
-npm run build
-```
-
-> **Contributor note**: No manual build is required after modifying WebUI; Vercel deploys will build it automatically.
-
----
-
-## Docker Deployment (Recommended)
-
-Docker uses a **non-invasive, decoupled design**:
-- Dockerfile executes standard Python steps and avoids hardcoded project configs
-- WebUI is built during image build (for non-Vercel deployments)
-- Configuration lives in environment variables and `.env`
-- **Rebuild the image to update code without touching Docker config**
-
-### Quick start (Docker Compose)
-
-```bash
-# 1. Copy the environment template
cp .env.example .env
-# Edit .env with DS2API_ADMIN_KEY and DS2API_CONFIG_JSON
+# edit .env
-# 2. Start the service
docker-compose up -d
-# 3. Check logs
docker-compose logs -f
+```
-# 4. Rebuild after code updates
+Rebuild after updates:
+
+```bash
docker-compose up -d --build
```
-### Mount a config file
-
-To use `config.json` instead of environment variables:
-
-```yaml
-# docker-compose.yml
-services:
- ds2api:
- build: .
- ports:
- - "5001:5001"
- environment:
- - DS2API_ADMIN_KEY=your-admin-key
- volumes:
- - ./config.json:/app/config.json:ro
- restart: unless-stopped
-```
-
-### Docker CLI deployment
+## Local Run
```bash
-# Build the image
-docker build -t ds2api:latest .
+cp config.example.json config.json
+# edit config
-# Run with env variables
-docker run -d \
- --name ds2api \
- -p 5001:5001 \
- -e DS2API_ADMIN_KEY=your-admin-key \
- -e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \
- --restart unless-stopped \
- ds2api:latest
-
-# Or mount a config file
-docker run -d \
- --name ds2api \
- -p 5001:5001 \
- -e DS2API_ADMIN_KEY=your-admin-key \
- -v $(pwd)/config.json:/app/config.json:ro \
- --restart unless-stopped \
- ds2api:latest
+go run ./cmd/ds2api
```
-### Development mode (hot reload)
+Default port is `5001` (override with `PORT`).
-```bash
-# Use the dev compose file to enable hot reload
-docker-compose -f docker-compose.dev.yml up
-```
+## systemd Deployment (Linux)
-Development mode:
-- Source code is mounted into the container
-- Log level set to DEBUG
-- Reads local `config.json`
-
-### Maintenance commands
-
-```bash
-# Check container status
-docker-compose ps
-
-# View logs
-docker-compose logs -f ds2api
-
-# Restart
-docker-compose restart
-
-# Stop
-docker-compose down
-
-# Full rebuild (clear cache)
-docker-compose down
-docker-compose build --no-cache
-docker-compose up -d
-```
-
----
-
-## Production Deployment
-
-### Using systemd (Linux)
-
-1. **Create the service file**
-
-```bash
-sudo nano /etc/systemd/system/ds2api.service
-```
+Example unit file:
```ini
[Unit]
-Description=DS2API Service
+Description=DS2API (Go)
After=network.target
[Service]
Type=simple
-User=www-data
WorkingDirectory=/opt/ds2api
-ExecStart=/usr/bin/python3 app.py
-Restart=always
-RestartSec=10
Environment=PORT=5001
-Environment=DS2API_ADMIN_KEY=your-admin-key
+Environment=DS2API_CONFIG_PATH=/opt/ds2api/config.json
+Environment=DS2API_ADMIN_KEY=your-admin-secret-key
+ExecStart=/opt/ds2api/ds2api
+Restart=always
+RestartSec=5
[Install]
WantedBy=multi-user.target
```
-2. **Start the service**
+Useful commands:
```bash
sudo systemctl daemon-reload
sudo systemctl enable ds2api
sudo systemctl start ds2api
-```
-
-3. **Check status**
-
-```bash
sudo systemctl status ds2api
-sudo journalctl -u ds2api -f
```
-
-### Nginx reverse proxy
-
-```nginx
-server {
- listen 80;
- server_name api.yourdomain.com;
-
- # SSL configuration (recommended)
- # listen 443 ssl http2;
- # ssl_certificate /path/to/cert.pem;
- # ssl_certificate_key /path/to/key.pem;
-
- location / {
- proxy_pass http://127.0.0.1:5001;
- proxy_http_version 1.1;
-
- # Disable buffering for SSE
- proxy_buffering off;
- proxy_cache off;
-
- # Connection settings
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- # SSE timeouts
- proxy_read_timeout 300s;
- proxy_send_timeout 300s;
-
- # Chunked transfer
- chunked_transfer_encoding on;
- tcp_nopush on;
- tcp_nodelay on;
- keepalive_timeout 120;
- }
-}
-```
-
----
-
-## FAQ
-
-### Q: What if account validation fails?
-
-**A**: Check the following:
-1. Confirm the DeepSeek account password is correct
-2. Ensure the account is not banned or requires verification
-3. Log in once in a browser
-4. Check logs for detailed errors
-
-### Q: Streaming responses disconnect?
-
-**A**:
-1. Check Nginx / reverse proxy config and ensure `proxy_buffering` is off
-2. Increase `proxy_read_timeout`
-3. Verify network stability
-
-### Q: Configuration lost after Vercel deploy?
-
-**A**:
-1. Ensure you clicked "Sync to Vercel"
-2. Verify the Vercel token is valid and unexpired
-3. Ensure the project ID is correct
-
-### Q: How to update to the latest version?
-
-**Local deployment**:
-```bash
-git pull origin main
-pip install -r requirements.txt
-# Restart the service
-```
-
-**Docker deployment**:
-```bash
-# Pull the latest code
-git pull origin main
-
-# Rebuild and start (Docker config unchanged)
-docker-compose up -d --build
-```
-
-**Vercel deployment**:
-- The project auto-syncs from GitHub
-- Or trigger a redeploy in the Vercel console
-
-### Q: How do I view logs?
-
-**Local dev**:
-```bash
-# Set log level
-export LOG_LEVEL=DEBUG
-python dev.py
-```
-
-**Vercel**:
-- Vercel console → Project → Deployments → Logs
-
-### Q: Token counting is inaccurate?
-
-**A**: DS2API uses a heuristic estimate (characters / 4). The official OpenAI tokenizer may differ, so treat it as a reference only.
-
----
-
-## Get Help
-
-- **GitHub Issues**: https://github.com/CJackHwang/ds2api/issues
-- **Docs**: https://github.com/CJackHwang/ds2api
diff --git a/DEPLOY.md b/DEPLOY.md
index b5c7748..b9547b0 100644
--- a/DEPLOY.md
+++ b/DEPLOY.md
@@ -1,410 +1,80 @@
-# DS2API 部署指南
+# DS2API 部署指南(Go 版本)
语言 / Language: [中文](DEPLOY.md) | [English](DEPLOY.en.md)
-本文档详细介绍 DS2API 的各种部署方式。
-
----
-
## 目录
-- [Vercel 部署(推荐)](#vercel-部署推荐)
-- [Docker 部署(推荐)](#docker-部署推荐)
-- [本地开发](#本地开发)
-- [生产环境部署](#生产环境部署)
-- [常见问题](#常见问题)
+- Vercel 部署
+- Docker 部署
+- 本地运行
+- systemd 部署
----
+## Vercel 部署
-## Vercel 部署(推荐)
+1. 导入仓库到 Vercel
+2. 设置环境变量(至少):
+- `DS2API_ADMIN_KEY`
+- `DS2API_CONFIG_JSON`(JSON 或 Base64)
+3. 部署后访问 `/admin` 管理界面
-### 一键部署
+说明:项目使用 `api/index.go` 作为 Serverless 入口,配置见 `vercel.json`。
-[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api&env=DS2API_ADMIN_KEY&envDescription=管理面板访问密码(必填)&envLink=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api%23环境变量&project-name=ds2api&repository-name=ds2api)
-
-### 部署步骤
-
-1. **点击部署按钮**
- - 登录你的 GitHub 账号
- - 授权 Vercel 访问
-
-2. **设置环境变量**
- - `DS2API_ADMIN_KEY`: 管理面板密码(**必填**)
-
-3. **等待部署完成**
- - Vercel 会自动构建并部署项目
- - 部署完成后获得访问 URL
-
-4. **配置账号**
- - 访问 `https://your-project.vercel.app/admin`
- - 输入管理密码登录
- - 添加 DeepSeek 账号
- - 设置自定义 API Key
-
-5. **同步配置**
- - 点击「同步到 Vercel」按钮
- - 首次需要输入 Vercel Token 和 Project ID
- - 同步成功后配置会持久化
-
-### 获取 Vercel 凭证
-
-**Vercel Token**:
-1. 访问 https://vercel.com/account/tokens
-2. 点击 "Create Token"
-3. 设置名称和有效期
-4. 复制生成的 Token
-
-**Project ID**:
-1. 进入 Vercel 项目页面
-2. 点击 Settings -> General
-3. 复制 "Project ID"
-
----
-
-## 本地开发
-
-### 环境要求
-
-- Python 3.9+
-- Node.js 18+ (WebUI 开发)
-- pip
-
-### 快速开始
+## Docker 部署
```bash
-# 1. 克隆项目
-git clone https://github.com/CJackHwang/ds2api.git
-cd ds2api
-
-# 2. 安装 Python 依赖
-pip install -r requirements.txt
-
-# 3. 配置账号
-cp config.example.json config.json
-# 编辑 config.json,填入 DeepSeek 账号信息
-
-# 4. 启动服务
-python dev.py
-```
-
-### 配置文件示例
-
-```json
-{
- "keys": ["my-api-key-1", "my-api-key-2"],
- "accounts": [
- {
- "email": "your-email@example.com",
- "password": "your-password",
- "token": ""
- },
- {
- "mobile": "12345678901",
- "password": "your-password",
- "token": ""
- }
- ]
-}
-```
-
-**说明**:
-- `keys`: 自定义 API Key,用于调用本服务的接口
-- `accounts`: DeepSeek 网页版账号
- - 支持 `email` 或 `mobile` 登录
- - `token` 留空,系统会自动获取
-
-### WebUI 开发
-
-```bash
-# 进入 WebUI 目录
-cd webui
-
-# 安装依赖
-npm install
-
-# 启动开发服务器
-npm run dev
-```
-
-WebUI 开发服务器会启动在 `http://localhost:5173`,并自动代理 API 请求到后端 `http://localhost:5001`。
-
-### WebUI 构建
-
-WebUI 构建产物位于 `static/admin/` 目录。
-
-**自动构建(推荐)**:
-- 当前由 Vercel 在部署时执行 WebUI 构建(见 `vercel.json` 的 `buildCommand`)
-- GitHub Actions 的 WebUI 自动构建流程已关闭
-- `static/admin/` 构建产物不再提交到仓库
-
-**手动构建**:
-```bash
-# 方式1:使用脚本
-./scripts/build-webui.sh
-
-# 方式2:直接执行
-cd webui
-npm install
-npm run build
-```
-
-> **贡献者注意**:修改 WebUI 后无需手动构建,Vercel 部署会自动构建。
-
----
-
-## Docker 部署(推荐)
-
-Docker 部署采用**零侵入、解耦设计**:
-- Dockerfile 仅执行标准 Python 项目操作,不硬编码任何项目特定配置
-- 构建镜像时会一并构建 WebUI(便于非 Vercel 部署直接访问管理面板)
-- 所有配置通过环境变量和 `.env` 文件管理
-- **主代码更新时只需重新构建镜像,无需修改 Docker 配置**
-
-### 快速开始(Docker Compose)
-
-```bash
-# 1. 复制环境变量模板
cp .env.example .env
-# 编辑 .env,填写 DS2API_ADMIN_KEY 和 DS2API_CONFIG_JSON
+# 编辑 .env
-# 2. 启动服务
docker-compose up -d
-# 3. 查看日志
docker-compose logs -f
+```
-# 4. 主代码更新后重新构建
+更新后重建:
+
+```bash
docker-compose up -d --build
```
-### 配置文件挂载方式
-
-如需使用 `config.json` 而非环境变量:
-
-```yaml
-# docker-compose.yml
-services:
- ds2api:
- build: .
- ports:
- - "5001:5001"
- environment:
- - DS2API_ADMIN_KEY=your-admin-key
- volumes:
- - ./config.json:/app/config.json:ro
- restart: unless-stopped
-```
-
-### Docker 命令行部署
+## 本地运行
```bash
-# 构建镜像
-docker build -t ds2api:latest .
+cp config.example.json config.json
+# 编辑配置
-# 使用环境变量运行
-docker run -d \
- --name ds2api \
- -p 5001:5001 \
- -e DS2API_ADMIN_KEY=your-admin-key \
- -e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \
- --restart unless-stopped \
- ds2api:latest
-
-# 或使用配置文件挂载
-docker run -d \
- --name ds2api \
- -p 5001:5001 \
- -e DS2API_ADMIN_KEY=your-admin-key \
- -v $(pwd)/config.json:/app/config.json:ro \
- --restart unless-stopped \
- ds2api:latest
+go run ./cmd/ds2api
```
-### 开发模式(热重载)
+默认端口 `5001`,可通过 `PORT` 环境变量覆盖。
-```bash
-# 使用开发配置启动,代码修改实时生效
-docker-compose -f docker-compose.dev.yml up
-```
+## systemd 部署(Linux)
-开发模式特性:
-- 源代码挂载到容器,修改即时生效
-- 日志级别设为 DEBUG
-- 自动读取本地 `config.json`
-
-### 维护命令
-
-```bash
-# 查看容器状态
-docker-compose ps
-
-# 查看日志
-docker-compose logs -f ds2api
-
-# 重启服务
-docker-compose restart
-
-# 停止服务
-docker-compose down
-
-# 完全重建(清除缓存)
-docker-compose down
-docker-compose build --no-cache
-docker-compose up -d
-```
-
----
-
-## 生产环境部署
-
-### 使用 systemd (Linux)
-
-1. **创建服务文件**
-
-```bash
-sudo nano /etc/systemd/system/ds2api.service
-```
+示例服务文件:
```ini
[Unit]
-Description=DS2API Service
+Description=DS2API (Go)
After=network.target
[Service]
Type=simple
-User=www-data
WorkingDirectory=/opt/ds2api
-ExecStart=/usr/bin/python3 app.py
-Restart=always
-RestartSec=10
Environment=PORT=5001
-Environment=DS2API_ADMIN_KEY=your-admin-key
+Environment=DS2API_CONFIG_PATH=/opt/ds2api/config.json
+Environment=DS2API_ADMIN_KEY=your-admin-secret-key
+ExecStart=/opt/ds2api/ds2api
+Restart=always
+RestartSec=5
[Install]
WantedBy=multi-user.target
```
-2. **启动服务**
+常用命令:
```bash
sudo systemctl daemon-reload
sudo systemctl enable ds2api
sudo systemctl start ds2api
-```
-
-3. **查看状态**
-
-```bash
sudo systemctl status ds2api
-sudo journalctl -u ds2api -f
```
-
-### Nginx 反向代理
-
-```nginx
-server {
- listen 80;
- server_name api.yourdomain.com;
-
- # SSL 配置(推荐)
- # listen 443 ssl http2;
- # ssl_certificate /path/to/cert.pem;
- # ssl_certificate_key /path/to/key.pem;
-
- location / {
- proxy_pass http://127.0.0.1:5001;
- proxy_http_version 1.1;
-
- # 关闭缓冲,支持 SSE
- proxy_buffering off;
- proxy_cache off;
-
- # 连接设置
- proxy_set_header Host $host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- # SSE 超时设置
- proxy_read_timeout 300s;
- proxy_send_timeout 300s;
-
- # 分块传输
- chunked_transfer_encoding on;
- tcp_nopush on;
- tcp_nodelay on;
- keepalive_timeout 120;
- }
-}
-```
-
----
-
-## 常见问题
-
-### Q: 账号验证失败怎么办?
-
-**A**: 检查以下几点:
-1. 确认 DeepSeek 账号密码正确
-2. 检查账号是否被封禁或需要验证
-3. 尝试在浏览器中手动登录一次
-4. 查看日志获取详细错误信息
-
-### Q: 流式响应断开怎么办?
-
-**A**:
-1. 检查 Nginx/反向代理配置,确保关闭了 `proxy_buffering`
-2. 增加 `proxy_read_timeout` 超时时间
-3. 检查网络连接稳定性
-
-### Q: Vercel 部署后配置丢失?
-
-**A**:
-1. 确保点击了「同步到 Vercel」按钮
-2. 检查 Vercel Token 是否正确且未过期
-3. 确认 Project ID 正确
-
-### Q: 如何更新到新版本?
-
-**本地部署**:
-```bash
-git pull origin main
-pip install -r requirements.txt
-# 重启服务
-```
-
-**Docker 部署**:
-```bash
-# 拉取最新代码
-git pull origin main
-
-# 重新构建并启动(无需修改 Docker 配置)
-docker-compose up -d --build
-```
-
-**Vercel 部署**:
-- 项目会自动从 GitHub 同步更新
-- 或在 Vercel 控制台手动触发重新部署
-
-### Q: 如何查看日志?
-
-**本地开发**:
-```bash
-# 设置日志级别
-export LOG_LEVEL=DEBUG
-python dev.py
-```
-
-**Vercel**:
-- 访问 Vercel 控制台 -> 项目 -> Deployments -> Logs
-
-### Q: Token 计数不准确?
-
-**A**: DS2API 使用估算方式计算 token 数量(字符数 / 4),与 OpenAI 官方的 tokenizer 可能有差异,仅供参考。
-
----
-
-## 获取帮助
-
-- **GitHub Issues**: https://github.com/CJackHwang/ds2api/issues
-- **文档**: https://github.com/CJackHwang/ds2api
diff --git a/app.py b/app.py
deleted file mode 100644
index 4c4d70e..0000000
--- a/app.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-DS2API - DeepSeek to OpenAI API 转换服务
-
-支持:
-- OpenAI 兼容接口: /v1/chat/completions, /v1/models
-- Claude 兼容接口: /anthropic/v1/messages, /anthropic/v1/models
-
-使用方法:
- 本地开发: python dev.py
- 生产环境: uvicorn app:app --host 0.0.0.0 --port 5001
- Vercel: 自动部署
-"""
-import os
-
-from fastapi import FastAPI, Request
-from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse
-
-from core.config import IS_VERCEL, logger
-
-# 创建 FastAPI 应用
-app = FastAPI(
- title="DS2API",
- description="DeepSeek to OpenAI/Claude API",
- version="1.0.0",
-)
-
-
-# 全局异常处理
-@app.exception_handler(Exception)
-async def unhandled_exception_handler(request: Request, exc: Exception):
- logger.exception(f"[unhandled_exception] {request.method} {request.url.path}: {exc}")
- return JSONResponse(
- status_code=500,
- content={"error": {"type": "api_error", "message": "Internal Server Error"}},
- )
-
-
-# CORS 中间件
-app.add_middleware(
- CORSMiddleware,
- allow_origins=["*"],
- allow_credentials=True,
- allow_methods=["GET", "POST", "OPTIONS", "PUT", "DELETE"],
- allow_headers=["Content-Type", "Authorization"],
-)
-
-# 注册路由
-from routes.openai import router as openai_router
-from routes.claude import router as claude_router
-from routes.home import router as home_router
-from routes.admin import router as admin_router
-
-app.include_router(openai_router)
-app.include_router(claude_router)
-# admin_router 必须在 home_router 之前,否则 home.py 的 /admin/{path:path} 会拦截 admin API
-app.include_router(admin_router)
-app.include_router(home_router)
-
-
-# ----------------------------------------------------------------------
-# 本地运行入口
-# ----------------------------------------------------------------------
-if __name__ == "__main__" and not IS_VERCEL:
- import uvicorn
-
- port = int(os.getenv("PORT", "5001"))
- uvicorn.run(app, host="0.0.0.0", port=port)
diff --git a/core/__init__.py b/core/__init__.py
deleted file mode 100644
index 5ad4ac0..0000000
--- a/core/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# DS2API Core Modules
diff --git a/core/auth.py b/core/auth.py
deleted file mode 100644
index f665890..0000000
--- a/core/auth.py
+++ /dev/null
@@ -1,247 +0,0 @@
-# -*- coding: utf-8 -*-
-"""账号认证与管理模块 - 轮询(Round-Robin)策略"""
-import threading
-from fastapi import HTTPException, Request
-
-from .config import CONFIG, logger
-from .deepseek import login_deepseek_via_account, BASE_HEADERS
-from .utils import get_account_identifier
-
-# -------------------------- 全局账号队列 --------------------------
-# 使用列表实现轮询队列,配合线程锁保证并发安全
-account_queue = [] # 可用账号队列
-in_use_accounts = {} # 正在使用的账号 {account_id: account}
-_queue_lock = threading.Lock() # 线程锁
-
-claude_api_key_queue = [] # 维护所有可用的Claude API keys
-
-
-def init_account_queue():
- """初始化时从配置加载账号(不再随机排序,保持配置顺序)"""
- global account_queue, in_use_accounts
- with _queue_lock:
- account_queue = CONFIG.get("accounts", [])[:] # 深拷贝
- in_use_accounts = {}
- # 按 token 有无排序:有 token 的账号优先
- account_queue.sort(key=lambda a: 0 if a.get("token", "").strip() else 1)
- logger.info(f"[init_account_queue] 初始化 {len(account_queue)} 个账号,轮询模式")
-
-
-def init_claude_api_key_queue():
- """Claude API keys由用户自己的token提供,这里初始化为空"""
- global claude_api_key_queue
- claude_api_key_queue = []
-
-
-# 初始化
-init_account_queue()
-init_claude_api_key_queue()
-
-
-# get_account_identifier 已移至 core.utils
-
-
-def get_queue_status() -> dict:
- """获取账号队列状态(用于监控)"""
- with _queue_lock:
- # total 应该是配置中的账号总数,而非队列相加(避免状态不一致导致重复计数)
- total_accounts = len(CONFIG.get("accounts", []))
- return {
- "available": len(account_queue),
- "in_use": len(in_use_accounts),
- "total": total_accounts,
- "available_accounts": [get_account_identifier(a) for a in account_queue],
- "in_use_accounts": list(in_use_accounts.keys()),
- }
-
-
-# ----------------------------------------------------------------------
-# 账号选择与释放 - 轮询(Round-Robin)策略
-# ----------------------------------------------------------------------
-def choose_new_account(exclude_ids=None, target_id=None):
- """轮询选择策略:
- 1. 使用线程锁保证并发安全
- 2. 如果指定了 target_id,优先尝试获取该账号
- 3. 优先选择队首的有 token 账号
- 4. 从队列头部取出账号(FIFO)
- 5. 请求完成后调用 release_account 将账号放回队尾
- """
- if exclude_ids is None:
- exclude_ids = []
-
- with _queue_lock:
- # 0. 如果指定了目标账号,优先尝试获取
- if target_id:
- for i in range(len(account_queue)):
- acc = account_queue[i]
- acc_id = get_account_identifier(acc)
- if acc_id == target_id:
- selected = account_queue.pop(i)
- in_use_accounts[acc_id] = selected
- logger.info(f"[choose_new_account] 指定选择: {acc_id} | 队列剩余: {len(account_queue)}")
- return selected
- # 如果队列中没找到,且不在 in_use 中,说明账号不存在
- if target_id not in in_use_accounts:
- logger.warning(f"[choose_new_account] 指定账号不存在: {target_id}")
- else:
- logger.warning(f"[choose_new_account] 指定账号正忙: {target_id}")
- return None
-
- # 第一轮:优先选择已有 token 的账号
- for i in range(len(account_queue)):
- acc = account_queue[i]
- acc_id = get_account_identifier(acc)
- if acc_id and acc_id not in exclude_ids:
- if acc.get("token", "").strip(): # 已有 token
- selected = account_queue.pop(i)
- in_use_accounts[acc_id] = selected
- logger.info(f"[choose_new_account] 轮询选择(有token): {acc_id} | 队列剩余: {len(account_queue)}")
- return selected
-
- # 第二轮:选择任意账号(需要登录)
- for i in range(len(account_queue)):
- acc = account_queue[i]
- acc_id = get_account_identifier(acc)
- if acc_id and acc_id not in exclude_ids:
- selected = account_queue.pop(i)
- in_use_accounts[acc_id] = selected
- logger.info(f"[choose_new_account] 轮询选择(需登录): {acc_id} | 队列剩余: {len(account_queue)}")
- return selected
-
- logger.warning(f"[choose_new_account] 没有可用账号 | 队列: {len(account_queue)}, 使用中: {len(in_use_accounts)}")
- return None
-
-
-def release_account(account: dict):
- """将账号重新加入队列末尾(轮询核心:用完放队尾)"""
- if not account:
- return
-
- acc_id = get_account_identifier(account)
- with _queue_lock:
- # 从使用中移除
- if acc_id in in_use_accounts:
- del in_use_accounts[acc_id]
- # 放回队尾
- account_queue.append(account)
- logger.debug(f"[release_account] 释放账号: {acc_id} | 队列长度: {len(account_queue)}")
- else:
- logger.warning(f"[release_account] 账号 {acc_id} 不在使用列表中 (可能是因为重置了队列),跳过释放")
-
-
-# ----------------------------------------------------------------------
-# Claude API key 管理函数(简化版本)
-# ----------------------------------------------------------------------
-def choose_claude_api_key():
- """选择一个可用的Claude API key - 现在直接由用户提供"""
- return None
-
-
-def release_claude_api_key(api_key):
- """释放Claude API key - 现在无需操作"""
- pass
-
-
-# ----------------------------------------------------------------------
-# 判断调用模式:配置模式 vs 用户自带 token
-# ----------------------------------------------------------------------
-def determine_mode_and_token(request: Request):
- """
- 根据请求头 Authorization 判断使用哪种模式:
- - 如果 Bearer token 出现在 CONFIG["keys"] 中,则为配置模式,从 CONFIG["accounts"] 中随机选择一个账号(排除已尝试账号),
- 检查该账号是否已有 token,否则调用登录接口获取;
- - 否则,直接使用请求中的 Bearer 值作为 DeepSeek token。
- 结果存入 request.state.deepseek_token;配置模式下同时存入 request.state.account 与 request.state.tried_accounts。
- """
- auth_header = request.headers.get("Authorization", "")
- if not auth_header.startswith("Bearer "):
- raise HTTPException(
- status_code=401, detail="Unauthorized: missing Bearer token."
- )
- caller_key = auth_header.replace("Bearer ", "", 1).strip()
- config_keys = CONFIG.get("keys", [])
- if caller_key in config_keys:
- request.state.use_config_token = True
- request.state.tried_accounts = [] # 初始化已尝试账号
-
- target_account = request.headers.get("X-Ds2-Target-Account")
- selected_account = choose_new_account(target_id=target_account)
-
- if not selected_account:
- detail_msg = "No accounts configured or all accounts are busy."
- if target_account:
- detail_msg = f"Target account {target_account} is busy or not found."
- raise HTTPException(
- status_code=429,
- detail=detail_msg,
- )
- if not selected_account.get("token", "").strip():
- try:
- login_deepseek_via_account(selected_account)
- except Exception as e:
- logger.error(
- f"[determine_mode_and_token] 账号 {get_account_identifier(selected_account)} 登录失败:{e}"
- )
- raise HTTPException(status_code=500, detail="Account login failed.")
-
- request.state.deepseek_token = selected_account.get("token")
- request.state.account = selected_account
-
- else:
- request.state.use_config_token = False
- request.state.deepseek_token = caller_key
-
-
-def get_auth_headers(request: Request) -> dict:
- """返回 DeepSeek 请求所需的公共请求头"""
- return {**BASE_HEADERS, "authorization": f"Bearer {request.state.deepseek_token}"}
-
-
-# determine_claude_mode_and_token 已移除(直接使用 determine_mode_and_token)
-
-
-# ----------------------------------------------------------------------
-# Token 刷新机制
-# ----------------------------------------------------------------------
-def refresh_account_token(request: Request) -> bool:
- """当 token 过期时,刷新账号 token。
-
- 返回 True 表示刷新成功,False 表示刷新失败。
- 调用后 request.state.deepseek_token 会被更新。
- """
- if not getattr(request.state, 'use_config_token', False):
- # 用户自带 token,无法刷新
- return False
-
- account = getattr(request.state, 'account', None)
- if not account:
- return False
-
- acc_id = get_account_identifier(account)
- logger.info(f"[refresh_account_token] 尝试刷新账号 {acc_id} 的 token")
-
- try:
- # 清除旧 token
- account["token"] = ""
- # 重新登录
- login_deepseek_via_account(account)
- # 更新 request 状态
- request.state.deepseek_token = account.get("token")
- logger.info(f"[refresh_account_token] 账号 {acc_id} token 刷新成功")
- return True
- except Exception as e:
- logger.error(f"[refresh_account_token] 账号 {acc_id} token 刷新失败: {e}")
- return False
-
-
-def mark_token_invalid(request: Request):
- """标记当前账号的 token 为无效,清除它以便下次重新登录"""
- if not getattr(request.state, 'use_config_token', False):
- return
-
- account = getattr(request.state, 'account', None)
- if account:
- acc_id = get_account_identifier(account)
- logger.warning(f"[mark_token_invalid] 标记账号 {acc_id} 的 token 为无效")
- account["token"] = ""
-
diff --git a/core/config.py b/core/config.py
deleted file mode 100644
index f215691..0000000
--- a/core/config.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# -*- coding: utf-8 -*-
-"""配置管理模块"""
-import base64
-import json
-import logging
-import os
-import sys
-
-import transformers
-
-# -------------------------- 获取项目根目录 --------------------------
-BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-IS_VERCEL = bool(os.getenv("VERCEL")) or bool(os.getenv("NOW_REGION"))
-
-
-def resolve_path(env_key: str, default_rel: str) -> str:
- """解析路径,支持环境变量覆盖"""
- raw = os.getenv(env_key)
- if raw:
- return raw if os.path.isabs(raw) else os.path.join(BASE_DIR, raw)
- return os.path.join(BASE_DIR, default_rel)
-
-
-# -------------------------- 日志配置 --------------------------
-logging.basicConfig(
- level=os.getenv("LOG_LEVEL", "INFO").upper(),
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
- handlers=[logging.StreamHandler(sys.stdout)],
- force=True,
-)
-logger = logging.getLogger("ds2api")
-
-# -------------------------- 初始化 tokenizer --------------------------
-chat_tokenizer_dir = resolve_path("DS2API_TOKENIZER_DIR", "")
-# 抑制 Mistral tokenizer regex 警告(不影响 DeepSeek tokenization)
-_tf_logger = logging.getLogger("transformers")
-_tf_log_level = _tf_logger.level
-_tf_logger.setLevel(logging.ERROR)
-tokenizer = transformers.AutoTokenizer.from_pretrained(
- chat_tokenizer_dir, trust_remote_code=True
-)
-_tf_logger.setLevel(_tf_log_level)
-
-# ----------------------------------------------------------------------
-# 配置文件的读写函数
-# ----------------------------------------------------------------------
-CONFIG_PATH = resolve_path("DS2API_CONFIG_PATH", "config.json")
-
-
-def load_config() -> dict:
- """加载配置。
-
- 优先从环境变量读取:
- - DS2API_CONFIG_JSON / CONFIG_JSON: 直接 JSON 字符串,或 base64 编码后的 JSON
-
- 若未提供环境变量,再从 CONFIG_PATH 指向的文件读取。
- """
- raw_cfg = os.getenv("DS2API_CONFIG_JSON") or os.getenv("CONFIG_JSON")
- if raw_cfg:
- try:
- return json.loads(raw_cfg)
- except json.JSONDecodeError:
- try:
- decoded = base64.b64decode(raw_cfg).decode("utf-8")
- return json.loads(decoded)
- except Exception as e:
- logger.warning(f"[load_config] 环境变量配置解析失败: {e}")
- return {}
-
- try:
- with open(CONFIG_PATH, "r", encoding="utf-8") as f:
- return json.load(f)
- except Exception as e:
- logger.warning(f"[load_config] 无法读取配置文件({CONFIG_PATH}): {e}")
- return {}
-
-
-def save_config(cfg: dict) -> None:
- """将配置写回 config.json。
-
- Vercel 环境文件系统通常是只读的;且如果配置来自环境变量,也无法回写。
- 所以这里失败不应影响主流程。
- """
- if os.getenv("DS2API_CONFIG_JSON") or os.getenv("CONFIG_JSON"):
- logger.info("[save_config] 配置来自环境变量,跳过写回")
- return
-
- try:
- with open(CONFIG_PATH, "w", encoding="utf-8") as f:
- json.dump(cfg, f, ensure_ascii=False, indent=2)
- except PermissionError as e:
- logger.warning(f"[save_config] 配置文件不可写({CONFIG_PATH}): {e}")
- except Exception as e:
- logger.exception(f"[save_config] 写入 config.json 失败: {e}")
-
-
-# 全局配置
-CONFIG = load_config()
-if not CONFIG:
- logger.warning(
- "[config] 未加载到有效配置,请提供 config.json(路径可用 DS2API_CONFIG_PATH 指定)或设置环境变量 DS2API_CONFIG_JSON"
- )
-
-# WASM 模块文件路径
-WASM_PATH = resolve_path("DS2API_WASM_PATH", "sha3_wasm_bg.7b9ca65ddd.wasm")
-
-# 模板目录
-TEMPLATES_DIR = resolve_path("DS2API_TEMPLATES_DIR", "templates")
-
-# WebUI 静态文件目录
-STATIC_ADMIN_DIR = resolve_path("DS2API_STATIC_ADMIN_DIR", "static/admin")
diff --git a/core/constants.py b/core/constants.py
deleted file mode 100644
index 48c2577..0000000
--- a/core/constants.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# -*- coding: utf-8 -*-
-"""常量定义模块 - 统一管理项目中的所有常量"""
-
-# ----------------------------------------------------------------------
-# 网络和超时配置
-# ----------------------------------------------------------------------
-KEEP_ALIVE_TIMEOUT = 5 # 保活超时(秒)
-STREAM_IDLE_TIMEOUT = 30 # 流无新内容超时(秒)
-MAX_KEEPALIVE_COUNT = 10 # 最大连续 keepalive 次数
-
-# ----------------------------------------------------------------------
-# DeepSeek API 配置
-# ----------------------------------------------------------------------
-DEEPSEEK_HOST = "chat.deepseek.com"
-DEEPSEEK_LOGIN_URL = f"https://{DEEPSEEK_HOST}/api/v0/users/login"
-DEEPSEEK_CREATE_SESSION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat_session/create"
-DEEPSEEK_CREATE_POW_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/create_pow_challenge"
-DEEPSEEK_COMPLETION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/completion"
-
-# ----------------------------------------------------------------------
-# 请求头配置
-# ----------------------------------------------------------------------
-BASE_HEADERS = {
- "Host": "chat.deepseek.com",
- "User-Agent": "DeepSeek/1.6.11 Android/35",
- "Accept": "application/json",
- "Accept-Encoding": "gzip",
- "Content-Type": "application/json",
- "x-client-platform": "android",
- "x-client-version": "1.6.11",
- "x-client-locale": "zh_CN",
- "accept-charset": "UTF-8",
-}
-
-# ----------------------------------------------------------------------
-# SSE 解析配置
-# ----------------------------------------------------------------------
-# 跳过的路径模式(状态相关,不是内容)
-SKIP_PATTERNS = [
- "quasi_status", "elapsed_secs", "token_usage",
- "pending_fragment", "conversation_mode",
- "fragments/-1/status", "fragments/-2/status", "fragments/-3/status"
-]
diff --git a/core/deepseek.py b/core/deepseek.py
deleted file mode 100644
index 76c75b1..0000000
--- a/core/deepseek.py
+++ /dev/null
@@ -1,138 +0,0 @@
-# -*- coding: utf-8 -*-
-"""DeepSeek API 相关逻辑"""
-import time
-from curl_cffi import requests
-from fastapi import HTTPException
-
-from .config import CONFIG, save_config, logger
-from .utils import get_account_identifier
-from .constants import (
- DEEPSEEK_HOST,
- DEEPSEEK_LOGIN_URL,
- DEEPSEEK_CREATE_SESSION_URL,
- DEEPSEEK_CREATE_POW_URL,
- DEEPSEEK_COMPLETION_URL,
- BASE_HEADERS,
-)
-
-
-# get_account_identifier 已移至 core.utils
-
-
-
-
-# ----------------------------------------------------------------------
-# 登录函数:支持使用 email 或 mobile 登录
-# ----------------------------------------------------------------------
-def login_deepseek_via_account(account: dict) -> str:
- """使用 account 中的 email 或 mobile 登录 DeepSeek,
- 成功后将返回的 token 写入 account 并保存至配置文件,返回新 token。
- """
- email = account.get("email", "").strip()
- mobile = account.get("mobile", "").strip()
- password = account.get("password", "").strip()
- if not password or (not email and not mobile):
- raise HTTPException(
- status_code=400,
- detail="账号缺少必要的登录信息(必须提供 email 或 mobile 以及 password)",
- )
- if email:
- payload = {
- "email": email,
- "password": password,
- "device_id": "deepseek_to_api",
- "os": "android",
- }
- else:
- payload = {
- "mobile": mobile,
- "area_code": None,
- "password": password,
- "device_id": "deepseek_to_api",
- "os": "android",
- }
- try:
- resp = requests.post(
- DEEPSEEK_LOGIN_URL, headers=BASE_HEADERS, json=payload, impersonate="safari15_3"
- )
- resp.raise_for_status()
- except Exception as e:
- logger.error(f"[login_deepseek_via_account] 登录请求异常: {e}")
- raise HTTPException(status_code=500, detail="Account login failed: 请求异常")
- try:
- logger.warning(f"[login_deepseek_via_account] {resp.text}")
- data = resp.json()
- except Exception as e:
- logger.error(f"[login_deepseek_via_account] JSON解析失败: {e}")
- raise HTTPException(
- status_code=500, detail="Account login failed: invalid JSON response"
- )
-
- # 检查 API 错误码
- if data.get("code") != 0:
- error_msg = data.get("msg", "Unknown error")
- logger.error(f"[login_deepseek_via_account] API错误: {error_msg}")
- raise HTTPException(
- status_code=500, detail=f"Account login failed: {error_msg}"
- )
-
- # 检查业务错误码
- biz_code = data.get("data", {}).get("biz_code")
- biz_msg = data.get("data", {}).get("biz_msg", "")
- if biz_code != 0:
- logger.error(f"[login_deepseek_via_account] 业务错误: {biz_msg}")
- raise HTTPException(
- status_code=500, detail=f"Account login failed: {biz_msg}"
- )
-
- # 校验响应数据格式是否正确
- if (
- data.get("data") is None
- or data["data"].get("biz_data") is None
- or data["data"]["biz_data"].get("user") is None
- ):
- logger.error(f"[login_deepseek_via_account] 登录响应格式错误: {data}")
- raise HTTPException(
- status_code=500, detail="Account login failed: invalid response format"
- )
- new_token = data["data"]["biz_data"]["user"].get("token")
- if not new_token:
- logger.error(f"[login_deepseek_via_account] 登录响应中缺少 token: {data}")
- raise HTTPException(
- status_code=500, detail="Account login failed: missing token"
- )
- account["token"] = new_token
- save_config(CONFIG)
- return new_token
-
-
-# ----------------------------------------------------------------------
-# 封装对话接口调用的重试机制
-# ----------------------------------------------------------------------
-def call_completion_endpoint(payload: dict, headers: dict, max_attempts: int = 3):
- """调用 DeepSeek 对话接口,支持重试"""
- attempts = 0
- while attempts < max_attempts:
- try:
- deepseek_resp = requests.post(
- DEEPSEEK_COMPLETION_URL,
- headers=headers,
- json=payload,
- stream=True,
- impersonate="safari15_3",
- )
- except Exception as e:
- logger.warning(f"[call_completion_endpoint] 请求异常: {e}")
- time.sleep(1)
- attempts += 1
- continue
- if deepseek_resp.status_code == 200:
- return deepseek_resp
- else:
- logger.warning(
- f"[call_completion_endpoint] 调用对话接口失败, 状态码: {deepseek_resp.status_code}"
- )
- deepseek_resp.close()
- time.sleep(1)
- attempts += 1
- return None
diff --git a/core/messages.py b/core/messages.py
deleted file mode 100644
index e2f359c..0000000
--- a/core/messages.py
+++ /dev/null
@@ -1,118 +0,0 @@
-# -*- coding: utf-8 -*-
-"""消息处理模块"""
-import re
-
-from .config import CONFIG, logger
-
-# Claude 默认模型
-CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-20250514"
-
-# 预编译正则表达式(性能优化)
-_MARKDOWN_IMAGE_PATTERN = re.compile(r"!\[(.*?)\]\((.*?)\)")
-
-
-# ----------------------------------------------------------------------
-# 消息预处理函数,将多轮对话合并成最终 prompt
-# ----------------------------------------------------------------------
-def messages_prepare(messages: list) -> str:
- """处理消息列表,合并连续相同角色的消息,并添加角色标签:
- - 对于 assistant 消息,加上 <|Assistant|> 前缀及 <|end▁of▁sentence|> 结束标签;
- - 对于 user/system 消息(除第一条外)加上 <|User|> 前缀;
- - 如果消息 content 为数组,则提取其中 type 为 "text" 的部分;
- - 最后移除 markdown 图片格式的内容。
- """
- processed = []
- for m in messages:
- role = m.get("role", "")
- content = m.get("content", "")
- if isinstance(content, list):
- texts = [
- item.get("text", "") for item in content if item.get("type") == "text"
- ]
- text = "\n".join(texts)
- else:
- text = str(content)
- processed.append({"role": role, "text": text})
- if not processed:
- return ""
- # 合并连续同一角色的消息
- merged = [processed[0]]
- for msg in processed[1:]:
- if msg["role"] == merged[-1]["role"]:
- merged[-1]["text"] += "\n\n" + msg["text"]
- else:
- merged.append(msg)
- # 添加标签
- parts = []
- for idx, block in enumerate(merged):
- role = block["role"]
- text = block["text"]
- if role == "assistant":
- parts.append(f"<|Assistant|>{text}<|end▁of▁sentence|>")
- elif role in ("user", "system"):
- if idx > 0:
- parts.append(f"<|User|>{text}")
- else:
- parts.append(text)
- else:
- parts.append(text)
- final_prompt = "".join(parts)
- # 仅移除 markdown 图片格式(不全部移除 !)- 使用预编译的正则表达式
- final_prompt = _MARKDOWN_IMAGE_PATTERN.sub(r"[\1](\2)", final_prompt)
- return final_prompt
-
-
-# ----------------------------------------------------------------------
-# OpenAI到Claude格式转换函数
-# ----------------------------------------------------------------------
-def convert_claude_to_deepseek(claude_request: dict) -> dict:
- """将Claude格式的请求转换为DeepSeek格式(基于现有OpenAI接口)"""
- messages = claude_request.get("messages", [])
- model = claude_request.get("model", CLAUDE_DEFAULT_MODEL)
-
- # 从配置文件读取Claude模型映射
- claude_mapping = CONFIG.get(
- "claude_model_mapping", {"fast": "deepseek-chat", "slow": "deepseek-chat"}
- )
-
- # Claude模型映射到DeepSeek模型 - 基于配置和模型特征判断
- if (
- "opus" in model.lower()
- or "reasoner" in model.lower()
- or "slow" in model.lower()
- ):
- deepseek_model = claude_mapping.get("slow", "deepseek-chat")
- else:
- deepseek_model = claude_mapping.get("fast", "deepseek-chat")
-
- deepseek_request = {"model": deepseek_model, "messages": messages.copy()}
-
- # 处理system消息 - 将system参数转换为system role消息
- if "system" in claude_request:
- system_msg = {"role": "system", "content": claude_request["system"]}
- deepseek_request["messages"].insert(0, system_msg)
-
- # 添加可选参数
- if "temperature" in claude_request:
- deepseek_request["temperature"] = claude_request["temperature"]
- if "top_p" in claude_request:
- deepseek_request["top_p"] = claude_request["top_p"]
- if "stop_sequences" in claude_request:
- deepseek_request["stop"] = claude_request["stop_sequences"]
- if "stream" in claude_request:
- deepseek_request["stream"] = claude_request["stream"]
-
- return deepseek_request
-
-
-def convert_deepseek_to_claude_format(
- deepseek_response: dict, original_claude_model: str = CLAUDE_DEFAULT_MODEL
-) -> dict:
- """将DeepSeek响应转换为Claude格式的OpenAI响应"""
- # DeepSeek响应已经是OpenAI格式,只需要修改模型名称
- if isinstance(deepseek_response, dict):
- claude_response = deepseek_response.copy()
- claude_response["model"] = original_claude_model
- return claude_response
-
- return deepseek_response
diff --git a/core/models.py b/core/models.py
deleted file mode 100644
index f1dddaa..0000000
--- a/core/models.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# -*- coding: utf-8 -*-
-"""模型定义模块 - 集中管理所有支持的模型"""
-
-# DeepSeek 模型列表(官方模型名称)
-DEEPSEEK_MODELS = [
- {
- "id": "deepseek-chat",
- "object": "model",
- "created": 1677610602,
- "owned_by": "deepseek",
- "permission": [],
- },
- {
- "id": "deepseek-reasoner",
- "object": "model",
- "created": 1677610602,
- "owned_by": "deepseek",
- "permission": [],
- },
- {
- "id": "deepseek-chat-search",
- "object": "model",
- "created": 1677610602,
- "owned_by": "deepseek",
- "permission": [],
- },
- {
- "id": "deepseek-reasoner-search",
- "object": "model",
- "created": 1677610602,
- "owned_by": "deepseek",
- "permission": [],
- },
-]
-
-# Claude 模型映射列表
-CLAUDE_MODELS = [
- {
- "id": "claude-sonnet-4-20250514",
- "object": "model",
- "created": 1715635200,
- "owned_by": "anthropic",
- },
- {
- "id": "claude-sonnet-4-20250514-fast",
- "object": "model",
- "created": 1715635200,
- "owned_by": "anthropic",
- },
- {
- "id": "claude-sonnet-4-20250514-slow",
- "object": "model",
- "created": 1715635200,
- "owned_by": "anthropic",
- },
-]
-
-
-def get_model_config(model: str) -> tuple[bool, bool]:
- """根据模型名称获取配置
-
- Args:
- model: 模型名称
-
- Returns:
- (thinking_enabled, search_enabled) 元组
- """
- model_lower = model.lower()
-
- if model_lower == "deepseek-chat":
- return False, False
- elif model_lower == "deepseek-reasoner":
- return True, False
- elif model_lower == "deepseek-chat-search":
- return False, True
- elif model_lower == "deepseek-reasoner-search":
- return True, True
- else:
- return None, None # 不支持的模型
-
-
-def get_openai_models_response() -> dict:
- """获取 OpenAI 格式的模型列表响应"""
- return {"object": "list", "data": DEEPSEEK_MODELS}
-
-
-def get_claude_models_response() -> dict:
- """获取 Claude 格式的模型列表响应"""
- return {"object": "list", "data": CLAUDE_MODELS}
-
diff --git a/core/pow.py b/core/pow.py
deleted file mode 100644
index d8d1a72..0000000
--- a/core/pow.py
+++ /dev/null
@@ -1,253 +0,0 @@
-# -*- coding: utf-8 -*-
-"""PoW (Proof of Work) 计算模块"""
-import base64
-import ctypes
-import json
-import struct
-import threading
-import time
-
-from curl_cffi import requests
-from wasmtime import Engine, Linker, Module, Store
-
-from .config import CONFIG, WASM_PATH, logger
-from .utils import get_account_identifier
-
-# ----------------------------------------------------------------------
-# WASM 模块缓存 - 避免每次请求都重新加载
-# ----------------------------------------------------------------------
-_wasm_cache_lock = threading.Lock()
-_wasm_engine = None
-_wasm_module = None
-
-
-def _get_cached_wasm_module(wasm_path: str):
- """获取缓存的 WASM 模块,首次调用时加载"""
- global _wasm_engine, _wasm_module
-
- if _wasm_module is not None:
- return _wasm_engine, _wasm_module
-
- with _wasm_cache_lock:
- # 双重检查锁定
- if _wasm_module is not None:
- return _wasm_engine, _wasm_module
-
- try:
- with open(wasm_path, "rb") as f:
- wasm_bytes = f.read()
- _wasm_engine = Engine()
- _wasm_module = Module(_wasm_engine, wasm_bytes)
- logger.info(f"[WASM] 已缓存 WASM 模块: {wasm_path}")
- except Exception as e:
- logger.error(f"[WASM] 加载 WASM 模块失败: {e}")
- raise RuntimeError(f"加载 wasm 文件失败: {wasm_path}, 错误: {e}")
-
- return _wasm_engine, _wasm_module
-
-
-# 启动时预加载 WASM 模块
-try:
- _get_cached_wasm_module(WASM_PATH)
-except Exception as e:
- logger.warning(f"[WASM] 启动时预加载失败(将在首次使用时重试): {e}")
-
-# get_account_identifier 已移至 core.utils
-
-
-# ----------------------------------------------------------------------
-# 使用 WASM 模块计算 PoW 答案的辅助函数
-# ----------------------------------------------------------------------
-def compute_pow_answer(
- algorithm: str,
- challenge_str: str,
- salt: str,
- difficulty: int,
- expire_at: int,
- signature: str,
- target_path: str,
- wasm_path: str,
-) -> int:
- """
- 使用 WASM 模块计算 DeepSeekHash 答案(answer)。
- 根据 JS 逻辑:
- - 拼接前缀: "{salt}_{expire_at}_"
- - 将 challenge 与前缀写入 wasm 内存后调用 wasm_solve 进行求解,
- - 从 wasm 内存中读取状态与求解结果,
- - 若状态非 0,则返回整数形式的答案,否则返回 None。
-
- 优化:使用缓存的 WASM 模块,避免每次请求都重新加载文件。
- """
- if algorithm != "DeepSeekHashV1":
- raise ValueError(f"不支持的算法:{algorithm}")
-
- prefix = f"{salt}_{expire_at}_"
-
- # 获取缓存的 WASM 模块(避免重复加载文件)
- engine, module = _get_cached_wasm_module(wasm_path)
-
- # 每次调用创建新的 Store 和实例(必须的,因为 Store 不是线程安全的)
- store = Store(engine)
- linker = Linker(engine)
- instance = linker.instantiate(store, module)
- exports = instance.exports(store)
-
- try:
- memory = exports["memory"]
- add_to_stack = exports["__wbindgen_add_to_stack_pointer"]
- alloc = exports["__wbindgen_export_0"]
- wasm_solve = exports["wasm_solve"]
- except KeyError as e:
- raise RuntimeError(f"缺少 wasm 导出函数: {e}")
-
- def write_memory(offset: int, data: bytes):
- size = len(data)
- base_addr = ctypes.cast(memory.data_ptr(store), ctypes.c_void_p).value
- ctypes.memmove(base_addr + offset, data, size)
-
- def read_memory(offset: int, size: int) -> bytes:
- base_addr = ctypes.cast(memory.data_ptr(store), ctypes.c_void_p).value
- return ctypes.string_at(base_addr + offset, size)
-
- def encode_string(text: str):
- data = text.encode("utf-8")
- length = len(data)
- ptr_val = alloc(store, length, 1)
- ptr = int(ptr_val.value) if hasattr(ptr_val, "value") else int(ptr_val)
- write_memory(ptr, data)
- return ptr, length
-
- # 1. 申请 16 字节栈空间
- retptr = add_to_stack(store, -16)
- # 2. 编码 challenge 与 prefix 到 wasm 内存中
- ptr_challenge, len_challenge = encode_string(challenge_str)
- ptr_prefix, len_prefix = encode_string(prefix)
- # 3. 调用 wasm_solve(注意:difficulty 以 float 形式传入)
- wasm_solve(
- store,
- retptr,
- ptr_challenge,
- len_challenge,
- ptr_prefix,
- len_prefix,
- float(difficulty),
- )
- # 4. 从 retptr 处读取 4 字节状态和 8 字节求解结果
- status_bytes = read_memory(retptr, 4)
- if len(status_bytes) != 4:
- add_to_stack(store, 16)
- raise RuntimeError("读取状态字节失败")
- status = struct.unpack(" str | None:
- """创建 DeepSeek 会话
-
- Args:
- request: FastAPI 请求对象
- max_attempts: 最大重试次数
-
- Returns:
- 会话 ID,如果失败返回 None
- """
- attempts = 0
- token_refreshed = False # 标记是否已尝试刷新 token
-
- while attempts < max_attempts:
- headers = get_auth_headers(request)
- try:
- resp = cffi_requests.post(
- DEEPSEEK_CREATE_SESSION_URL,
- headers=headers,
- json={"agent": "chat"},
- impersonate="safari15_3",
- )
- except Exception as e:
- logger.error(f"[create_session] 请求异常: {e}")
- attempts += 1
- continue
-
- try:
- data = resp.json()
- except Exception as e:
- logger.error(f"[create_session] JSON解析异常: {e}")
- data = {}
-
- if resp.status_code == 200 and data.get("code") == 0:
- session_id = data["data"]["biz_data"]["id"]
- resp.close()
- return session_id
- else:
- code = data.get("code")
- msg = data.get("msg", "")
- logger.warning(
- f"[create_session] 创建会话失败, code={code}, msg={msg}"
- )
- resp.close()
-
- # 配置模式下尝试处理 token 问题
- if request.state.use_config_token:
- # token 无效(认证失败)时,先尝试刷新当前账号的 token
- if code in [40001, 40002, 40003] or "token" in msg.lower() or "unauthorized" in msg.lower():
- if not token_refreshed:
- logger.info("[create_session] 检测到 token 可能过期,尝试刷新")
- if refresh_account_token(request):
- token_refreshed = True
- continue # 使用新 token 重试
- else:
- logger.warning("[create_session] token 刷新失败,尝试切换账号")
-
- # token 刷新失败或其他错误,尝试切换账号
- current_id = get_account_identifier(request.state.account)
- if not hasattr(request.state, "tried_accounts"):
- request.state.tried_accounts = []
- if current_id not in request.state.tried_accounts:
- request.state.tried_accounts.append(current_id)
- new_account = choose_new_account(request.state.tried_accounts)
- if new_account is None:
- break
- try:
- login_deepseek_via_account(new_account)
- except Exception as e:
- logger.error(
- f"[create_session] 账号 {get_account_identifier(new_account)} 登录失败:{e}"
- )
- attempts += 1
- continue
- request.state.account = new_account
- request.state.deepseek_token = new_account.get("token")
- token_refreshed = False # 新账号重置刷新标记
- else:
- attempts += 1
- continue
- attempts += 1
- return None
-
-
-def get_pow(request: Request, max_attempts: int = 3) -> str | None:
- """获取 PoW 响应的包装函数
-
- Args:
- request: FastAPI 请求对象
- max_attempts: 最大重试次数
-
- Returns:
- Base64 编码的 PoW 响应,如果失败返回 None
- """
- return get_pow_response(request, max_attempts)
-
-
-def prepare_completion_request(
- request: Request,
- session_id: str,
- prompt: str,
- thinking_enabled: bool = False,
- search_enabled: bool = False,
- max_attempts: int = 3,
-):
- """准备并执行对话补全请求
-
- Args:
- request: FastAPI 请求对象
- session_id: 会话 ID
- prompt: 处理后的提示词
- thinking_enabled: 是否启用思考模式
- search_enabled: 是否启用搜索
- max_attempts: 最大重试次数
-
- Returns:
- DeepSeek 响应对象,如果失败返回 None
- """
- pow_resp = get_pow(request, max_attempts)
- if not pow_resp:
- return None
-
- headers = {**get_auth_headers(request), "x-ds-pow-response": pow_resp}
- payload = {
- "chat_session_id": session_id,
- "parent_message_id": None,
- "prompt": prompt,
- "ref_file_ids": [],
- "thinking_enabled": thinking_enabled,
- "search_enabled": search_enabled,
- }
-
- return call_completion_endpoint(payload, headers, max_attempts)
-
-
-# get_model_config 已移至 core.models
-
-
-def cleanup_account(request: Request):
- """清理账号资源(将账号放回队列)"""
- if getattr(request.state, "use_config_token", False) and hasattr(request.state, "account"):
- release_account(request.state.account)
diff --git a/core/sse_parser.py b/core/sse_parser.py
deleted file mode 100644
index e1c234e..0000000
--- a/core/sse_parser.py
+++ /dev/null
@@ -1,470 +0,0 @@
-# -*- coding: utf-8 -*-
-"""DeepSeek SSE 流解析模块
-
-这个模块包含解析 DeepSeek SSE 响应的公共逻辑,供 openai.py、claude.py 和 accounts.py 共用。
-合并了原 sse_parser.py 和 stream_parser.py 的功能。
-"""
-import json
-import re
-from typing import List, Tuple, Optional, Dict, Any, Generator
-
-from .config import logger
-from .constants import SKIP_PATTERNS
-
-# 预编译正则表达式
-_TOOL_CALL_PATTERN = re.compile(r'\{\s*["\']tool_calls["\']\s*:\s*\[(.*?)\]\s*\}', re.DOTALL)
-_CITATION_PATTERN = re.compile(r"^\[citation:")
-
-
-# ----------------------------------------------------------------------
-# 基础解析函数
-# ----------------------------------------------------------------------
-
-def parse_deepseek_sse_line(raw_line: bytes) -> Optional[Dict[str, Any]]:
- """解析 DeepSeek SSE 行
-
- Args:
- raw_line: 原始字节行
-
- Returns:
- 解析后的 chunk 字典,如果解析失败或应跳过则返回 None
- """
- try:
- line = raw_line.decode("utf-8")
- except Exception as e:
- logger.warning(f"[parse_deepseek_sse_line] 解码失败: {e}")
- return None
-
- if not line or not line.startswith("data:"):
- return None
-
- data_str = line[5:].strip()
-
- if data_str == "[DONE]":
- return {"type": "done"}
-
- try:
- chunk = json.loads(data_str)
- return chunk
- except json.JSONDecodeError as e:
- logger.warning(f"[parse_deepseek_sse_line] JSON解析失败: {e}")
- return None
-
-
-def should_skip_chunk(chunk_path: str) -> bool:
- """判断是否应该跳过这个 chunk(状态相关,不是内容)"""
- if chunk_path == "response/search_status":
- return True
- return any(kw in chunk_path for kw in SKIP_PATTERNS)
-
-
-def is_response_finished(chunk_path: str, v_value: Any) -> bool:
- """判断是否是响应结束信号"""
- return chunk_path == "response/status" and isinstance(v_value, str) and v_value == "FINISHED"
-
-
-def is_finished_signal(chunk_path: str, v_value: str) -> bool:
- """判断字符串 v_value 是否是结束信号"""
- return v_value == "FINISHED" and (not chunk_path or chunk_path == "status")
-
-
-def is_search_result(item: dict) -> bool:
- """判断是否是搜索结果项(url/title/snippet)"""
- return "url" in item and "title" in item
-
-
-# ----------------------------------------------------------------------
-# 内容提取函数
-# ----------------------------------------------------------------------
-
-def extract_content_from_item(item: dict, default_type: str = "text") -> Optional[Tuple[str, str]]:
- """从包含 content 和 type 的项中提取内容
-
- 返回 (content, content_type) 或 None
- """
- if "content" in item and "type" in item:
- inner_type = item.get("type", "").upper()
- content = item.get("content", "")
- if content:
- if inner_type == "THINK" or inner_type == "THINKING":
- return (content, "thinking")
- elif inner_type == "RESPONSE":
- return (content, "text")
- else:
- return (content, default_type)
- return None
-
-
-def extract_content_recursive(items: List[Dict], default_type: str = "text") -> Optional[List[Tuple[str, str]]]:
- """递归提取列表中的内容
-
- 返回 [(content, content_type), ...] 列表,
- 如果遇到 FINISHED 信号返回 None
- """
- extracted: List[Tuple[str, str]] = []
- for item in items:
- if not isinstance(item, dict):
- continue
-
- item_p = item.get("p", "")
- item_v = item.get("v")
-
- # 跳过搜索结果项
- if is_search_result(item):
- continue
-
- # 只有当 p="status" (精确匹配) 且 v="FINISHED" 才认为是真正结束
- if item_p == "status" and item_v == "FINISHED":
- return None # 信号结束
-
- # 跳过状态相关
- if should_skip_chunk(item_p):
- continue
-
- # 直接处理包含 content 和 type 的项
- result = extract_content_from_item(item, default_type)
- if result:
- extracted.append(result)
- continue
-
- # 确定类型(基于 p 字段)
- if "thinking" in item_p:
- content_type = "thinking"
- elif "content" in item_p or item_p == "response" or item_p == "fragments":
- content_type = "text"
- else:
- content_type = default_type
-
- # 处理不同的 v 类型
- if isinstance(item_v, str):
- if item_v and item_v != "FINISHED":
- extracted.append((item_v, content_type))
- elif isinstance(item_v, list):
- # 内层可能是 [{"content": "text", "type": "THINK/RESPONSE", ...}] 格式
- for inner in item_v:
- if isinstance(inner, dict):
- # 检查内层的 type 字段
- inner_type = inner.get("type", "").upper()
- # DeepSeek 使用 THINK 而不是 THINKING
- if inner_type == "THINK" or inner_type == "THINKING":
- final_type = "thinking"
- elif inner_type == "RESPONSE":
- final_type = "text"
- else:
- final_type = content_type # 继承外层类型
-
- content = inner.get("content", "")
- if content:
- extracted.append((content, final_type))
- elif isinstance(inner, str) and inner:
- extracted.append((inner, content_type))
- return extracted
-
-
-# ----------------------------------------------------------------------
-# 高级解析函数
-# ----------------------------------------------------------------------
-
-def parse_sse_chunk_for_content(
- chunk: Dict[str, Any],
- thinking_enabled: bool = False,
- current_fragment_type: str = "thinking"
-) -> Tuple[List[Tuple[str, str]], bool, str]:
- """解析单个 SSE chunk 并提取内容
-
- Args:
- chunk: 解析后的 JSON chunk
- thinking_enabled: 是否启用思考模式
- current_fragment_type: 当前活跃的 fragment 类型 ("thinking" 或 "text")
- 用于处理没有明确路径的空 p 字段内容
-
- Returns:
- (contents, is_finished, new_fragment_type)
- - contents: [(content, content_type), ...] 列表
- - is_finished: 是否是结束信号
- - new_fragment_type: 更新后的 fragment 类型,供下一个 chunk 使用
- """
- if "v" not in chunk:
- return ([], False, current_fragment_type)
-
- v_value = chunk["v"]
- chunk_path = chunk.get("p", "")
- contents: List[Tuple[str, str]] = []
- new_fragment_type = current_fragment_type
-
- # 跳过状态相关 chunk
- if should_skip_chunk(chunk_path):
- return ([], False, current_fragment_type)
-
- # 检查是否是真正的响应结束信号
- if is_response_finished(chunk_path, v_value):
- return ([], True, current_fragment_type)
-
- # 检测 fragment 类型变化(来自 APPEND 操作)
- # 格式: {'p': 'response', 'o': 'BATCH', 'v': [{'p': 'fragments', 'o': 'APPEND', 'v': [{'type': 'THINK/RESPONSE', ...}]}]}
- if chunk_path == "response" and isinstance(v_value, list):
- for batch_item in v_value:
- if isinstance(batch_item, dict) and batch_item.get("p") == "fragments" and batch_item.get("o") == "APPEND":
- fragments = batch_item.get("v", [])
- for frag in fragments:
- if isinstance(frag, dict):
- frag_type = frag.get("type", "").upper()
- if frag_type == "THINK" or frag_type == "THINKING":
- new_fragment_type = "thinking"
- elif frag_type == "RESPONSE":
- new_fragment_type = "text"
-
- # 也检测直接的 fragments 路径
- if "response/fragments" in chunk_path and isinstance(v_value, list):
- for frag in v_value:
- if isinstance(frag, dict):
- frag_type = frag.get("type", "").upper()
- if frag_type == "THINK" or frag_type == "THINKING":
- new_fragment_type = "thinking"
- elif frag_type == "RESPONSE":
- new_fragment_type = "text"
-
- # 确定当前内容类型
- if chunk_path == "response/thinking_content":
- ptype = "thinking"
- elif chunk_path == "response/content":
- ptype = "text"
- elif "response/fragments" in chunk_path and "/content" in chunk_path:
- # 如 response/fragments/-1/content - 使用当前 fragment 类型
- ptype = new_fragment_type
- elif not chunk_path:
- # 空路径内容:使用当前活跃的 fragment 类型
- if thinking_enabled:
- ptype = new_fragment_type
- else:
- ptype = "text"
- else:
- ptype = "text"
-
- # 处理字符串值
- if isinstance(v_value, str):
- if is_finished_signal(chunk_path, v_value):
- return ([], True, new_fragment_type)
- if v_value:
- contents.append((v_value, ptype))
-
- # 处理列表值
- elif isinstance(v_value, list):
- result = extract_content_recursive(v_value, ptype)
- if result is None:
- return ([], True, new_fragment_type)
- contents.extend(result)
-
- # 处理字典值(初始响应 chunk,包含 response.fragments)
- elif isinstance(v_value, dict):
- response_obj = v_value.get("response", v_value)
- fragments = response_obj.get("fragments", [])
- if isinstance(fragments, list):
- for frag in fragments:
- if isinstance(frag, dict):
- frag_type = frag.get("type", "").upper()
- frag_content = frag.get("content", "")
- if frag_type == "THINK" or frag_type == "THINKING":
- new_fragment_type = "thinking"
- if frag_content:
- contents.append((frag_content, "thinking"))
- elif frag_type == "RESPONSE":
- new_fragment_type = "text"
- if frag_content:
- contents.append((frag_content, "text"))
- elif frag_content:
- contents.append((frag_content, ptype))
-
- return (contents, False, new_fragment_type)
-
-
-def extract_content_from_chunk(chunk: Dict[str, Any]) -> Tuple[str, str, bool]:
- """从 DeepSeek chunk 中提取内容(简化版本,兼容旧接口)
-
- Args:
- chunk: 解析后的 chunk 字典
-
- Returns:
- (content, content_type, is_finished) 元组
- content_type 为 "thinking" 或 "text"
- is_finished 为 True 表示响应结束
- """
- if chunk.get("type") == "done":
- return "", "text", True
-
- # 检测内容审核/敏感词阻止
- if "error" in chunk or chunk.get("code") == "content_filter":
- logger.warning(f"[extract_content_from_chunk] 检测到内容过滤: {chunk}")
- return "", "text", True
-
- if "v" not in chunk:
- return "", "text", False
-
- v_value = chunk["v"]
- ptype = "text"
-
- # 检查路径确定类型
- path = chunk.get("p", "")
- if path == "response/search_status":
- return "", "text", False # 跳过搜索状态
- elif path == "response/thinking_content":
- ptype = "thinking"
- elif path == "response/content":
- ptype = "text"
-
- if isinstance(v_value, str):
- if v_value == "FINISHED":
- return "", ptype, True
- return v_value, ptype, False
- elif isinstance(v_value, list):
- for item in v_value:
- if isinstance(item, dict):
- if item.get("p") == "status" and item.get("v") == "FINISHED":
- return "", ptype, True
- return "", ptype, False
-
- return "", ptype, False
-
-
-# ----------------------------------------------------------------------
-# 响应收集函数
-# ----------------------------------------------------------------------
-
-def collect_deepseek_response(response: Any) -> Tuple[str, str]:
- """收集 DeepSeek 流响应的完整内容
-
- Args:
- response: DeepSeek 流响应对象
-
- Returns:
- (reasoning_content, text_content) 元组
- """
- thinking_parts: List[str] = []
- text_parts: List[str] = []
-
- try:
- for raw_line in response.iter_lines():
- chunk = parse_deepseek_sse_line(raw_line)
- if not chunk:
- continue
-
- content, content_type, is_finished = extract_content_from_chunk(chunk)
-
- if is_finished:
- break
-
- if content:
- if content_type == "thinking":
- thinking_parts.append(content)
- else:
- text_parts.append(content)
- except Exception as e:
- logger.error(f"[collect_deepseek_response] 收集响应失败: {e}")
- finally:
- try:
- response.close()
- except Exception:
- pass
-
- return "".join(thinking_parts), "".join(text_parts)
-
-
-# ----------------------------------------------------------------------
-# 工具调用解析
-# ----------------------------------------------------------------------
-
-def parse_tool_calls(text: str, tools_requested: List[Dict]) -> List[Dict[str, Any]]:
- """从响应文本中解析工具调用
-
- Args:
- text: 响应文本
- tools_requested: 请求中定义的工具列表
-
- Returns:
- 检测到的工具调用列表,每项包含 name 和 input
- """
- detected_tools: List[Dict[str, Any]] = []
- cleaned_text = text.strip()
-
- # 尝试直接解析完整 JSON
- if cleaned_text.startswith('{"tool_calls":') and cleaned_text.endswith("]}"):
- try:
- tool_data = json.loads(cleaned_text)
- for tool_call in tool_data.get("tool_calls", []):
- tool_name = tool_call.get("name")
- tool_input = tool_call.get("input", {})
- if any(tool.get("name") == tool_name for tool in tools_requested):
- detected_tools.append({"name": tool_name, "input": tool_input})
- if detected_tools:
- return detected_tools
- except json.JSONDecodeError:
- pass
-
- # 使用正则匹配
- matches = _TOOL_CALL_PATTERN.findall(cleaned_text)
- for match in matches:
- try:
- tool_calls_json = f'{{"tool_calls": [{match}]}}'
- tool_data = json.loads(tool_calls_json)
- for tool_call in tool_data.get("tool_calls", []):
- tool_name = tool_call.get("name")
- tool_input = tool_call.get("input", {})
- if any(tool.get("name") == tool_name for tool in tools_requested):
- detected_tools.append({"name": tool_name, "input": tool_input})
- except json.JSONDecodeError:
- continue
-
- return detected_tools
-
-
-# ----------------------------------------------------------------------
-# 引用过滤
-# ----------------------------------------------------------------------
-
-def should_filter_citation(text: str, search_enabled: bool) -> bool:
- """检查是否应该过滤引用内容
-
- Args:
- text: 内容文本
- search_enabled: 是否启用搜索
-
- Returns:
- 是否应该过滤
- """
- if not search_enabled:
- return False
- return _CITATION_PATTERN.match(text) is not None
-
-
-# ----------------------------------------------------------------------
-# 工具调用格式化
-# ----------------------------------------------------------------------
-
-def format_openai_tool_calls(
- detected_tools: List[Dict[str, Any]],
- base_id: str = ""
-) -> List[Dict[str, Any]]:
- """将检测到的工具调用格式化为 OpenAI API 格式
-
- Args:
- detected_tools: parse_tool_calls 返回的工具调用列表
- base_id: 用于生成唯一 ID 的基础字符串(可选)
-
- Returns:
- OpenAI 格式的 tool_calls 数组,例如:
- [{"id": "call_xxx", "type": "function", "function": {"name": "...", "arguments": "..."}}]
- """
- import random
- import time
-
- tool_calls_data = []
- for idx, tool_info in enumerate(detected_tools):
- tool_calls_data.append({
- "id": f"call_{base_id or int(time.time())}_{random.randint(1000,9999)}_{idx}",
- "type": "function",
- "function": {
- "name": tool_info["name"],
- "arguments": json.dumps(tool_info.get("input", {}), ensure_ascii=False)
- }
- })
- return tool_calls_data
diff --git a/core/utils.py b/core/utils.py
deleted file mode 100644
index 78cb59a..0000000
--- a/core/utils.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-"""公共工具函数模块"""
-
-
-def get_account_identifier(account: dict) -> str:
- """返回账号的唯一标识,优先使用 email,否则使用 mobile"""
- return account.get("email", "").strip() or account.get("mobile", "").strip()
-
-
-def estimate_tokens(text) -> int:
- """估算文本的 token 数量(简单估算:字符数/4)
-
- Args:
- text: 字符串或其他类型
-
- Returns:
- 估算的 token 数量,最小为 1
- """
- if isinstance(text, str):
- return max(1, len(text) // 4)
- elif isinstance(text, list):
- return sum(
- estimate_tokens(item.get("text", ""))
- if isinstance(item, dict)
- else estimate_tokens(str(item))
- for item in text
- )
- else:
- return max(1, len(str(text)) // 4)
diff --git a/dev.py b/dev.py
deleted file mode 100644
index f4d1d0c..0000000
--- a/dev.py
+++ /dev/null
@@ -1,151 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-"""
-DS2API 开发服务器 - 统一启动后端和前端
-
-使用方法:
- python dev.py # 同时启动后端和前端
- python dev.py --backend # 仅启动后端
- python dev.py --frontend # 仅启动前端
- python dev.py --install # 安装所有依赖
-
-环境变量:
- PORT - 后端服务端口,默认 5001
- LOG_LEVEL - 日志级别,默认 INFO
-"""
-import os
-import sys
-import signal
-import subprocess
-import time
-from pathlib import Path
-
-# 配置
-BACKEND_PORT = int(os.getenv("PORT", "5001"))
-FRONTEND_PORT = 5173
-HOST = os.getenv("HOST", "0.0.0.0")
-LOG_LEVEL = os.getenv("LOG_LEVEL", "info").lower()
-PROJECT_DIR = Path(__file__).parent
-WEBUI_DIR = PROJECT_DIR / "webui"
-REQUIREMENTS_FILE = PROJECT_DIR / "requirements.txt"
-
-processes = []
-
-
-def install_dependencies():
- """安装所有 Python 和 Node.js 依赖"""
- print("\n📦 安装 Python 依赖...")
- subprocess.run([
- sys.executable, "-m", "pip", "install", "-r", str(REQUIREMENTS_FILE), "-q"
- ], check=True)
- print("✅ Python 依赖安装完成")
-
- if WEBUI_DIR.exists():
- print("\n📦 安装前端依赖...")
- subprocess.run(["npm", "install"], cwd=WEBUI_DIR, check=True)
- print("✅ 前端依赖安装完成")
-
- print("\n🎉 所有依赖安装完成!运行 `python dev.py` 启动服务\n")
-
-
-def signal_handler(sig, frame):
- """处理退出信号,终止所有子进程"""
- print("\n\n🛑 正在关闭所有服务...")
- for proc in processes:
- if proc.poll() is None:
- proc.terminate()
- try:
- proc.wait(timeout=3)
- except subprocess.TimeoutExpired:
- proc.kill()
- print("👋 已退出\n")
- sys.exit(0)
-
-
-def start_backend():
- """启动后端服务"""
- print(f"🚀 启动后端服务... http://localhost:{BACKEND_PORT}")
- proc = subprocess.Popen(
- [
- sys.executable, "-m", "uvicorn",
- "app:app",
- "--host", HOST,
- "--port", str(BACKEND_PORT),
- "--reload",
- "--reload-dir", str(PROJECT_DIR),
- "--log-level", LOG_LEVEL,
- ],
- cwd=PROJECT_DIR,
- )
- processes.append(proc)
- return proc
-
-
-def start_frontend():
- """启动前端开发服务器"""
- if not WEBUI_DIR.exists():
- print("⚠️ webui 目录不存在,跳过前端启动")
- return None
-
- node_modules = WEBUI_DIR / "node_modules"
- if not node_modules.exists():
- print("📦 安装前端依赖...")
- subprocess.run(["npm", "install"], cwd=WEBUI_DIR, check=True)
-
- print(f"🎨 启动前端服务... http://localhost:{FRONTEND_PORT}")
- proc = subprocess.Popen(
- ["npm", "run", "dev"],
- cwd=WEBUI_DIR,
- )
- processes.append(proc)
- return proc
-
-
-def main():
- # 解析参数
- if "--install" in sys.argv or "-i" in sys.argv:
- install_dependencies()
- return
-
- backend_only = "--backend" in sys.argv or "-b" in sys.argv
- frontend_only = "--frontend" in sys.argv or "-f" in sys.argv
-
- # 注册信号处理
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
-
- print("\n" + "=" * 50)
- print(" DS2API 开发服务器")
- print("=" * 50)
-
- if frontend_only:
- start_frontend()
- elif backend_only:
- start_backend()
- else:
- # 同时启动
- start_backend()
- time.sleep(1) # 等待后端启动
- start_frontend()
-
- print("\n" + "-" * 50)
- if not frontend_only:
- print(f"📡 后端 API: http://localhost:{BACKEND_PORT}")
- if not backend_only:
- print(f"🎨 管理界面: http://localhost:{FRONTEND_PORT}")
- print("-" * 50)
- print("按 Ctrl+C 停止所有服务\n")
-
- # 等待进程结束
- try:
- while processes:
- for proc in processes[:]:
- if proc.poll() is not None:
- processes.remove(proc)
- time.sleep(0.5)
- except KeyboardInterrupt:
- signal_handler(None, None)
-
-
-if __name__ == "__main__":
- main()
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 33be4ce..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-# DS2API 依赖
-# 安装命令: pip install -r requirements.txt
-# Python 版本要求: >=3.9
-
-# ===== Web 框架 =====
-fastapi>=0.110.0,<1.0.0
-uvicorn[standard]>=0.24.0,<1.0.0
-
-# ===== HTTP 客户端 =====
-# curl_cffi: 支持 TLS 指纹模拟,绕过 Cloudflare 等防护
-curl_cffi>=0.7.0
-# httpx: 异步 HTTP 客户端,用于 Vercel API 调用
-httpx>=0.25.0
-
-# ===== 模板引擎 =====
-jinja2>=3.1.0,<4.0.0
-
-# ===== Tokenizer =====
-# 用于 token 计数(可选,不安装则使用估算方式)
-transformers>=4.39.0,<5.0.0
-
-# ===== WASM 运行时 =====
-# 用于 DeepSeek PoW (Proof of Work) 计算
-wasmtime>=14.0.0
diff --git a/routes/__init__.py b/routes/__init__.py
deleted file mode 100644
index bd402eb..0000000
--- a/routes/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# DS2API Routes
diff --git a/routes/admin/__init__.py b/routes/admin/__init__.py
deleted file mode 100644
index a6d7a8e..0000000
--- a/routes/admin/__init__.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Admin 路由模块 - 合并所有子模块路由"""
-from fastapi import APIRouter
-
-from .auth import router as auth_router, verify_admin, ADMIN_KEY
-from .config import router as config_router
-from .accounts import router as accounts_router
-from .vercel import router as vercel_router
-
-# 创建主路由
-router = APIRouter(prefix="/admin", tags=["admin"])
-
-# 包含所有子路由
-router.include_router(auth_router)
-router.include_router(config_router)
-router.include_router(accounts_router)
-router.include_router(vercel_router)
-
-# 导出常用依赖
-__all__ = ["router", "verify_admin", "ADMIN_KEY"]
diff --git a/routes/admin/accounts.py b/routes/admin/accounts.py
deleted file mode 100644
index 6603acd..0000000
--- a/routes/admin/accounts.py
+++ /dev/null
@@ -1,342 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Admin 账号管理模块 - 账号测试与导入"""
-import asyncio
-import json
-import base64
-
-from fastapi import APIRouter, HTTPException, Request, Depends
-from fastapi.responses import JSONResponse
-
-from core.config import CONFIG, save_config, logger, WASM_PATH
-from core.auth import init_account_queue, get_account_identifier
-from core.deepseek import (
- login_deepseek_via_account,
- DEEPSEEK_CREATE_SESSION_URL,
- DEEPSEEK_COMPLETION_URL,
- BASE_HEADERS,
-)
-from core.pow import compute_pow_answer
-from core.models import get_model_config
-from core.sse_parser import parse_sse_chunk_for_content
-
-from .auth import verify_admin
-
-router = APIRouter()
-
-
-# ----------------------------------------------------------------------
-# 账号 API 测试
-# ----------------------------------------------------------------------
-async def test_account_api(account: dict, model: str = "deepseek-chat", message: str = "") -> dict:
- """测试单个账号的 API 调用能力
-
- 如果提供 message,会发送实际请求并返回 AI 回复;
- 否则只快速测试创建会话。
- """
- from curl_cffi import requests as cffi_requests
- import time
-
- acc_id = get_account_identifier(account)
- result = {
- "account": acc_id,
- "success": False,
- "response_time": 0,
- "message": "",
- "model": model,
- }
-
- start_time = time.time()
-
- def _is_token_invalid(status_code: int, data: dict) -> bool:
- msg = (data.get("msg") or data.get("message") or "").lower()
- code = data.get("code")
- return status_code in {401, 403} or code in {40001, 40002, 40003} or "token" in msg or "unauthorized" in msg
-
- def _create_session(token: str) -> dict:
- headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"}
- try:
- session_resp = cffi_requests.post(
- DEEPSEEK_CREATE_SESSION_URL,
- headers=headers,
- json={"agent": "chat"},
- impersonate="safari15_3",
- timeout=15,
- )
- except Exception as e:
- return {"success": False, "message": f"请求异常: {e}", "status_code": 0, "data": {}}
-
- try:
- session_data = session_resp.json()
- except Exception:
- session_data = {}
- finally:
- session_resp.close()
-
- if session_resp.status_code == 200 and session_data.get("code") == 0:
- return {
- "success": True,
- "session_id": session_data.get("data", {}).get("biz_data", {}).get("id"),
- "status_code": session_resp.status_code,
- "data": session_data,
- }
- return {
- "success": False,
- "message": session_data.get("msg") or f"HTTP {session_resp.status_code}",
- "status_code": session_resp.status_code,
- "data": session_data,
- }
-
- try:
- token = account.get("token", "").strip()
- session_result = None
- if token:
- session_result = _create_session(token)
-
- if not token or (session_result and not session_result["success"] and _is_token_invalid(session_result["status_code"], session_result["data"])):
- try:
- account["token"] = ""
- login_deepseek_via_account(account)
- token = account.get("token", "")
- session_result = _create_session(token)
- except Exception as e:
- result["message"] = f"登录失败: {str(e)}"
- return result
-
- if not session_result or not session_result["success"]:
- result["message"] = f"创建会话失败: {session_result['message'] if session_result else 'Unknown error'}"
- return result
-
- session_id = session_result["session_id"]
- headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"}
-
- if not message.strip():
- result["success"] = True
- result["message"] = "API 测试成功(仅会话创建)"
- result["response_time"] = round((time.time() - start_time) * 1000)
- return result
-
- pow_url = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge"
- pow_resp = cffi_requests.post(
- pow_url,
- headers=headers,
- json={"target_path": "/api/v0/chat/completion"},
- timeout=30,
- impersonate="safari15_3",
- )
-
- pow_data = pow_resp.json()
- if pow_data.get("code") != 0:
- result["message"] = f"获取 PoW 失败: {pow_data.get('msg')}"
- return result
-
- challenge = pow_data["data"]["biz_data"]["challenge"]
- try:
- answer = compute_pow_answer(
- challenge["algorithm"],
- challenge["challenge"],
- challenge["salt"],
- challenge.get("difficulty", 144000),
- challenge.get("expire_at", 1680000000),
- challenge["signature"],
- challenge["target_path"],
- WASM_PATH,
- )
- except Exception as e:
- result["message"] = f"PoW 计算失败: {str(e)}"
- return result
-
- pow_dict = {
- "algorithm": challenge["algorithm"],
- "challenge": challenge["challenge"],
- "salt": challenge["salt"],
- "answer": answer,
- "signature": challenge["signature"],
- "target_path": challenge["target_path"],
- }
- pow_str = json.dumps(pow_dict, separators=(",", ":"), ensure_ascii=False)
- pow_header = base64.b64encode(pow_str.encode("utf-8")).decode("utf-8").rstrip()
-
- thinking_enabled, search_enabled = get_model_config(model)
- if thinking_enabled is None:
- thinking_enabled = False
- search_enabled = False
-
- payload = {
- "chat_session_id": session_id,
- "prompt": f"<|User|>{message}",
- "ref_file_ids": [],
- "thinking_enabled": thinking_enabled,
- "search_enabled": search_enabled,
- }
-
- completion_headers = {**headers, "x-ds-pow-response": pow_header}
-
- completion_resp = cffi_requests.post(
- DEEPSEEK_COMPLETION_URL,
- headers=completion_headers,
- json=payload,
- impersonate="safari15_3",
- timeout=60,
- stream=True,
- )
-
- if completion_resp.status_code != 200:
- result["message"] = f"请求失败: HTTP {completion_resp.status_code}"
- return result
-
- thinking_parts = []
- content_parts = []
- current_fragment_type = "thinking" if thinking_enabled else "text"
-
- for line in completion_resp.iter_lines():
- if not line:
- continue
- try:
- line_str = line.decode("utf-8")
- except:
- continue
-
- if not line_str.startswith("data:"):
- continue
-
- data_str = line_str[5:].strip()
- if data_str == "[DONE]":
- break
-
- try:
- chunk = json.loads(data_str)
- # 使用共享的解析函数
- contents, is_finished, current_fragment_type = parse_sse_chunk_for_content(
- chunk, thinking_enabled, current_fragment_type
- )
-
- if is_finished:
- break
-
- for content, ctype in contents:
- if ctype == "thinking":
- thinking_parts.append(content)
- else:
- content_parts.append(content)
- except:
- continue
-
- completion_resp.close()
-
- result["success"] = True
- result["response_time"] = round((time.time() - start_time) * 1000)
- result["message"] = "".join(content_parts) or "(无回复内容)"
- if thinking_parts:
- result["thinking"] = "".join(thinking_parts)
-
- except Exception as e:
- result["message"] = f"测试失败: {str(e)}"
-
- return result
-
-
-@router.post("/accounts/test")
-async def test_single_account(request: Request, _: bool = Depends(verify_admin)):
- """测试单个账号的 API 调用"""
- data = await request.json()
- identifier = data.get("identifier", "")
- model = data.get("model", "deepseek-chat")
- message = data.get("message", "")
-
- if not identifier:
- raise HTTPException(status_code=400, detail="需要账号标识(email 或 mobile)")
-
- account = None
- for acc in CONFIG.get("accounts", []):
- if acc.get("email") == identifier or acc.get("mobile") == identifier:
- account = acc
- break
-
- if not account:
- raise HTTPException(status_code=404, detail="账号不存在")
-
- result = await test_account_api(account, model, message)
- save_config(CONFIG)
-
- return JSONResponse(content=result)
-
-
-@router.post("/accounts/test-all")
-async def test_all_accounts(request: Request, _: bool = Depends(verify_admin)):
- """批量测试所有账号的 API 调用"""
- data = await request.json()
- model = data.get("model", "deepseek-chat")
-
- accounts = CONFIG.get("accounts", [])
- if not accounts:
- return JSONResponse(content={
- "total": 0, "success": 0, "failed": 0, "results": [],
- })
-
- results = []
- success_count = 0
-
- for acc in accounts:
- result = await test_account_api(acc, model)
- results.append(result)
- if result["success"]:
- success_count += 1
- await asyncio.sleep(1)
-
- save_config(CONFIG)
-
- return JSONResponse(content={
- "total": len(accounts),
- "success": success_count,
- "failed": len(accounts) - success_count,
- "results": results,
- })
-
-
-# ----------------------------------------------------------------------
-# 批量导入
-# ----------------------------------------------------------------------
-@router.post("/import")
-async def batch_import(request: Request, _: bool = Depends(verify_admin)):
- """批量导入 keys 和 accounts"""
- try:
- data = await request.json()
- imported_keys = 0
- imported_accounts = 0
-
- if "keys" in data:
- for key in data["keys"]:
- if key not in CONFIG.get("keys", []):
- if "keys" not in CONFIG:
- CONFIG["keys"] = []
- CONFIG["keys"].append(key)
- imported_keys += 1
-
- if "accounts" in data:
- existing_ids = set()
- for acc in CONFIG.get("accounts", []):
- existing_ids.add(acc.get("email", ""))
- existing_ids.add(acc.get("mobile", ""))
-
- for acc in data["accounts"]:
- acc_id = acc.get("email", "") or acc.get("mobile", "")
- if acc_id and acc_id not in existing_ids:
- if "accounts" not in CONFIG:
- CONFIG["accounts"] = []
- CONFIG["accounts"].append(acc)
- existing_ids.add(acc_id)
- imported_accounts += 1
-
- init_account_queue()
- save_config(CONFIG)
-
- return JSONResponse(content={
- "success": True,
- "imported_keys": imported_keys,
- "imported_accounts": imported_accounts,
- })
- except json.JSONDecodeError:
- raise HTTPException(status_code=400, detail="无效的 JSON 格式")
- except Exception as e:
- logger.error(f"[batch_import] 错误: {e}")
- raise HTTPException(status_code=500, detail=str(e))
diff --git a/routes/admin/auth.py b/routes/admin/auth.py
deleted file mode 100644
index 4e855f8..0000000
--- a/routes/admin/auth.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Admin 认证模块 - JWT 和登录相关"""
-import base64
-import os
-import time
-import hashlib
-import hmac
-
-from fastapi import APIRouter, HTTPException, Request, Depends
-from fastapi.responses import JSONResponse
-from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
-
-from core.config import logger
-
-router = APIRouter()
-security = HTTPBearer(auto_error=False)
-
-# Admin Key 验证(默认值适用于开发/演示环境,生产环境请务必修改)
-ADMIN_KEY = os.getenv("DS2API_ADMIN_KEY", "your-admin-secret-key")
-
-# JWT 配置
-JWT_SECRET = os.getenv("DS2API_JWT_SECRET", ADMIN_KEY or "ds2api-default-secret")
-JWT_EXPIRE_HOURS = int(os.getenv("DS2API_JWT_EXPIRE_HOURS", "24"))
-
-
-# ----------------------------------------------------------------------
-# JWT 工具函数(轻量实现,无需额外依赖)
-# ----------------------------------------------------------------------
-def _b64_encode(data: bytes) -> str:
- """Base64 URL 安全编码"""
- return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
-
-def _b64_decode(data: str) -> bytes:
- """Base64 URL 安全解码"""
- padding = 4 - len(data) % 4
- if padding != 4:
- data += "=" * padding
- return base64.urlsafe_b64decode(data)
-
-def create_jwt_token(expire_hours: int = None) -> str:
- """创建 JWT Token"""
- import json
-
- if expire_hours is None:
- expire_hours = JWT_EXPIRE_HOURS
-
- header = {"alg": "HS256", "typ": "JWT"}
- payload = {
- "iat": int(time.time()),
- "exp": int(time.time()) + (expire_hours * 3600),
- "role": "admin"
- }
-
- header_b64 = _b64_encode(json.dumps(header, separators=(",", ":")).encode())
- payload_b64 = _b64_encode(json.dumps(payload, separators=(",", ":")).encode())
-
- message = f"{header_b64}.{payload_b64}"
- signature = hmac.new(JWT_SECRET.encode(), message.encode(), hashlib.sha256).digest()
- signature_b64 = _b64_encode(signature)
-
- return f"{message}.{signature_b64}"
-
-def verify_jwt_token(token: str) -> dict:
- """验证 JWT Token,返回 payload 或抛出异常"""
- import json
-
- try:
- parts = token.split(".")
- if len(parts) != 3:
- raise ValueError("Invalid token format")
-
- header_b64, payload_b64, signature_b64 = parts
-
- # 验证签名
- message = f"{header_b64}.{payload_b64}"
- expected_sig = hmac.new(JWT_SECRET.encode(), message.encode(), hashlib.sha256).digest()
- actual_sig = _b64_decode(signature_b64)
-
- if not hmac.compare_digest(expected_sig, actual_sig):
- raise ValueError("Invalid signature")
-
- # 解析 payload
- payload = json.loads(_b64_decode(payload_b64))
-
- # 验证过期时间
- if payload.get("exp", 0) < time.time():
- raise ValueError("Token expired")
-
- return payload
- except Exception as e:
- raise ValueError(f"Token verification failed: {str(e)}")
-
-
-# ----------------------------------------------------------------------
-# 登录端点
-# ----------------------------------------------------------------------
-@router.post("/login")
-async def admin_login(request: Request):
- """管理员登录,返回 JWT Token"""
- try:
- data = await request.json()
- except:
- data = {}
-
- admin_key = data.get("admin_key", "")
- expire_hours = data.get("expire_hours", JWT_EXPIRE_HOURS)
-
- if admin_key != ADMIN_KEY:
- raise HTTPException(status_code=401, detail="Invalid admin key")
-
- token = create_jwt_token(expire_hours)
- return JSONResponse(content={
- "success": True,
- "token": token,
- "expires_in": expire_hours * 3600
- })
-
-
-@router.get("/verify")
-async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
- """验证当前 Token 是否有效"""
- if not credentials:
- raise HTTPException(status_code=401, detail="No credentials provided")
-
- token = credentials.credentials
- try:
- payload = verify_jwt_token(token)
- return JSONResponse(content={
- "valid": True,
- "expires_at": payload.get("exp"),
- "remaining_seconds": max(0, payload.get("exp", 0) - int(time.time()))
- })
- except ValueError as e:
- raise HTTPException(status_code=401, detail=str(e))
-
-
-def verify_admin(credentials: HTTPAuthorizationCredentials = Depends(security)):
- """验证 Admin 权限(支持 JWT 和直接 admin key)"""
- if not credentials:
- raise HTTPException(status_code=401, detail="Authentication required")
-
- token = credentials.credentials
-
- # 尝试 JWT 验证
- try:
- verify_jwt_token(token)
- return True
- except ValueError:
- pass
-
- # 尝试直接 admin key
- if token == ADMIN_KEY:
- return True
-
- raise HTTPException(status_code=401, detail="Invalid credentials")
diff --git a/routes/admin/config.py b/routes/admin/config.py
deleted file mode 100644
index 5a8471a..0000000
--- a/routes/admin/config.py
+++ /dev/null
@@ -1,226 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Admin 配置管理模块 - 配置、API Keys、账号管理"""
-import os
-
-from fastapi import APIRouter, HTTPException, Request, Depends
-from fastapi.responses import JSONResponse
-
-from core.config import CONFIG, save_config, logger
-from core.auth import init_account_queue, get_queue_status, get_account_identifier
-from core.deepseek import login_deepseek_via_account
-
-from .auth import verify_admin
-
-router = APIRouter()
-
-# Vercel 预配置
-VERCEL_TOKEN = os.getenv("VERCEL_TOKEN", "")
-VERCEL_PROJECT_ID = os.getenv("VERCEL_PROJECT_ID", "")
-VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID", "")
-
-
-# ----------------------------------------------------------------------
-# Vercel 预配置信息
-# ----------------------------------------------------------------------
-@router.get("/vercel/config")
-async def get_vercel_config(_: bool = Depends(verify_admin)):
- """获取预配置的 Vercel 信息(脱敏)"""
- return JSONResponse(content={
- "has_token": bool(VERCEL_TOKEN),
- "project_id": VERCEL_PROJECT_ID,
- "team_id": VERCEL_TEAM_ID or None,
- })
-
-
-# ----------------------------------------------------------------------
-# 配置管理
-# ----------------------------------------------------------------------
-@router.get("/config")
-async def get_config(_: bool = Depends(verify_admin)):
- """获取当前配置(密码脱敏)"""
- safe_config = {
- "keys": CONFIG.get("keys", []),
- "accounts": [],
- "claude_mapping": CONFIG.get("claude_mapping", {}),
- }
-
- for acc in CONFIG.get("accounts", []):
- safe_acc = {
- "email": acc.get("email", ""),
- "mobile": acc.get("mobile", ""),
- "has_password": bool(acc.get("password")),
- "has_token": bool(acc.get("token")),
- "token_preview": acc.get("token", "")[:20] + "..." if acc.get("token") else "",
- }
- safe_config["accounts"].append(safe_acc)
-
- return JSONResponse(content=safe_config)
-
-
-@router.post("/config")
-async def update_config(request: Request, _: bool = Depends(verify_admin)):
- """更新完整配置"""
- data = await request.json()
-
- if "keys" in data:
- CONFIG["keys"] = data["keys"]
-
- if "accounts" in data:
- # 保留原有密码和 token
- existing = {get_account_identifier(a): a for a in CONFIG.get("accounts", [])}
- for acc in data["accounts"]:
- acc_id = get_account_identifier(acc)
- if acc_id in existing:
- if not acc.get("password"):
- acc["password"] = existing[acc_id].get("password", "")
- if not acc.get("token"):
- acc["token"] = existing[acc_id].get("token", "")
- CONFIG["accounts"] = data["accounts"]
- init_account_queue()
-
- if "claude_mapping" in data:
- CONFIG["claude_mapping"] = data["claude_mapping"]
-
- save_config(CONFIG)
- return JSONResponse(content={"success": True, "message": "配置已更新"})
-
-
-# ----------------------------------------------------------------------
-# API Keys 管理
-# ----------------------------------------------------------------------
-@router.post("/keys")
-async def add_key(request: Request, _: bool = Depends(verify_admin)):
- """添加 API Key"""
- data = await request.json()
- key = data.get("key", "").strip()
-
- if not key:
- raise HTTPException(status_code=400, detail="Key 不能为空")
-
- if key in CONFIG.get("keys", []):
- raise HTTPException(status_code=400, detail="Key 已存在")
-
- if "keys" not in CONFIG:
- CONFIG["keys"] = []
- CONFIG["keys"].append(key)
- save_config(CONFIG)
-
- return JSONResponse(content={"success": True, "total_keys": len(CONFIG["keys"])})
-
-
-@router.delete("/keys/{key}")
-async def delete_key(key: str, _: bool = Depends(verify_admin)):
- """删除 API Key"""
- if key not in CONFIG.get("keys", []):
- raise HTTPException(status_code=404, detail="Key 不存在")
-
- CONFIG["keys"].remove(key)
- save_config(CONFIG)
- return JSONResponse(content={"success": True, "total_keys": len(CONFIG["keys"])})
-
-
-# ----------------------------------------------------------------------
-# 账号管理
-# ----------------------------------------------------------------------
-@router.get("/accounts")
-async def list_accounts(
- page: int = 1,
- page_size: int = 10,
- _: bool = Depends(verify_admin)
-):
- """获取账号列表(分页,倒序,密码脱敏)"""
- accounts = CONFIG.get("accounts", [])
- total = len(accounts)
-
- # 倒序排列
- accounts = list(reversed(accounts))
-
- # 计算分页
- page = max(1, page)
- page_size = max(1, min(100, page_size)) # 限制每页最多 100 条
- total_pages = (total + page_size - 1) // page_size if total > 0 else 1
-
- start = (page - 1) * page_size
- end = start + page_size
- page_accounts = accounts[start:end]
-
- # 脱敏处理
- safe_accounts = []
- for acc in page_accounts:
- safe_acc = {
- "email": acc.get("email", ""),
- "mobile": acc.get("mobile", ""),
- "has_password": bool(acc.get("password")),
- "has_token": bool(acc.get("token")),
- "token_preview": acc.get("token", "")[:20] + "..." if acc.get("token") else "",
- }
- safe_accounts.append(safe_acc)
-
- return JSONResponse(content={
- "items": safe_accounts,
- "total": total,
- "page": page,
- "page_size": page_size,
- "total_pages": total_pages,
- })
-
-
-@router.post("/accounts")
-async def add_account(request: Request, _: bool = Depends(verify_admin)):
- """添加账号"""
- data = await request.json()
- email = data.get("email", "").strip()
- mobile = data.get("mobile", "").strip()
- password = data.get("password", "").strip()
- token = data.get("token", "").strip()
-
- if not email and not mobile:
- raise HTTPException(status_code=400, detail="需要 email 或 mobile")
-
- # 检查是否已存在
- for acc in CONFIG.get("accounts", []):
- if email and acc.get("email") == email:
- raise HTTPException(status_code=400, detail="邮箱已存在")
- if mobile and acc.get("mobile") == mobile:
- raise HTTPException(status_code=400, detail="手机号已存在")
-
- new_account = {}
- if email:
- new_account["email"] = email
- if mobile:
- new_account["mobile"] = mobile
- if password:
- new_account["password"] = password
- if token:
- new_account["token"] = token
-
- if "accounts" not in CONFIG:
- CONFIG["accounts"] = []
- CONFIG["accounts"].append(new_account)
- init_account_queue()
- save_config(CONFIG)
-
- return JSONResponse(content={"success": True, "total_accounts": len(CONFIG["accounts"])})
-
-
-@router.delete("/accounts/{identifier}")
-async def delete_account(identifier: str, _: bool = Depends(verify_admin)):
- """删除账号(通过 email 或 mobile)"""
- accounts = CONFIG.get("accounts", [])
- for i, acc in enumerate(accounts):
- if acc.get("email") == identifier or acc.get("mobile") == identifier:
- accounts.pop(i)
- init_account_queue()
- save_config(CONFIG)
- return JSONResponse(content={"success": True, "total_accounts": len(accounts)})
-
- raise HTTPException(status_code=404, detail="账号不存在")
-
-
-# ----------------------------------------------------------------------
-# 账号队列状态
-# ----------------------------------------------------------------------
-@router.get("/queue/status")
-async def get_account_queue_status(_: bool = Depends(verify_admin)):
- """获取账号轮询队列状态"""
- return JSONResponse(content=get_queue_status())
diff --git a/routes/admin/vercel.py b/routes/admin/vercel.py
deleted file mode 100644
index cb365b5..0000000
--- a/routes/admin/vercel.py
+++ /dev/null
@@ -1,316 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Admin Vercel 模块 - Vercel 同步和部署"""
-import asyncio
-import base64
-import hashlib
-import json
-import os
-import time as _time
-
-import httpx
-from fastapi import APIRouter, HTTPException, Request, Depends
-from fastapi.responses import JSONResponse
-
-from core.config import CONFIG, save_config, logger
-from core.auth import get_account_identifier, init_account_queue
-from core.deepseek import login_deepseek_via_account
-
-from .auth import verify_admin
-
-router = APIRouter()
-
-# Vercel 预配置
-VERCEL_TOKEN = os.getenv("VERCEL_TOKEN", "")
-VERCEL_PROJECT_ID = os.getenv("VERCEL_PROJECT_ID", "")
-VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID", "")
-
-
-def _compute_config_hash() -> str:
- """计算可同步配置的指纹哈希(仅包含 keys 和 accounts)"""
- syncable = {
- "keys": CONFIG.get("keys", []),
- "accounts": [
- {k: v for k, v in acc.items() if k != "token"}
- for acc in CONFIG.get("accounts", [])
- ],
- }
- raw = json.dumps(syncable, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
- return hashlib.md5(raw.encode("utf-8")).hexdigest()
-
-
-# ----------------------------------------------------------------------
-# API 测试(通过本地 API)
-# ----------------------------------------------------------------------
-@router.post("/test")
-async def test_api(request: Request, _: bool = Depends(verify_admin)):
- """测试 API 调用"""
- try:
- data = await request.json()
- model = data.get("model", "deepseek-chat")
- message = data.get("message", "你好")
- api_key = data.get("api_key", "")
-
- if not api_key:
- keys = CONFIG.get("keys", [])
- if not keys:
- raise HTTPException(status_code=400, detail="没有可用的 API Key")
- api_key = keys[0]
-
- host = request.headers.get("host", "localhost:5001")
- scheme = "https" if "vercel" in host.lower() else "http"
- base_url = f"{scheme}://{host}"
-
- async with httpx.AsyncClient(timeout=60.0) as client:
- response = await client.post(
- f"{base_url}/v1/chat/completions",
- headers={"Authorization": f"Bearer {api_key}"},
- json={
- "model": model,
- "messages": [{"role": "user", "content": message}],
- "stream": False,
- },
- )
-
- return JSONResponse(content={
- "success": response.status_code == 200,
- "status_code": response.status_code,
- "response": response.json() if response.status_code == 200 else response.text,
- })
- except Exception as e:
- logger.error(f"[test_api] 错误: {e}")
- return JSONResponse(content={"success": False, "error": str(e)})
-
-
-# ----------------------------------------------------------------------
-# Vercel 同步
-# ----------------------------------------------------------------------
-@router.post("/vercel/sync")
-async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)):
- """同步配置到 Vercel 并触发重新部署"""
- try:
- data = await request.json()
- vercel_token = data.get("vercel_token", "")
- project_id = data.get("project_id", "")
- team_id = data.get("team_id", "")
- auto_validate = data.get("auto_validate", True)
- save_vercel_credentials = data.get("save_credentials", True)
-
- use_preconfig = vercel_token == "__USE_PRECONFIG__" or not vercel_token
- if use_preconfig:
- vercel_token = VERCEL_TOKEN
- if not project_id:
- project_id = VERCEL_PROJECT_ID
- if not team_id:
- team_id = VERCEL_TEAM_ID
-
- if not vercel_token or not project_id:
- raise HTTPException(status_code=400, detail="需要 Vercel Token 和 Project ID")
-
- # 自动验证账号
- validated_count = 0
- failed_accounts = []
- if auto_validate:
- accounts = CONFIG.get("accounts", [])
- for acc in accounts:
- acc_id = get_account_identifier(acc)
- if not acc.get("token", "").strip():
- try:
- logger.info(f"[sync_to_vercel] 自动验证账号: {acc_id}")
- login_deepseek_via_account(acc)
- validated_count += 1
- except Exception as e:
- logger.warning(f"[sync_to_vercel] 账号 {acc_id} 验证失败: {e}")
- failed_accounts.append(acc_id)
- await asyncio.sleep(0.5)
-
- config_json = json.dumps(CONFIG, ensure_ascii=False, separators=(",", ":"))
- config_b64 = base64.b64encode(config_json.encode("utf-8")).decode("utf-8")
-
- headers = {"Authorization": f"Bearer {vercel_token}"}
- base_url = "https://api.vercel.com"
-
- async with httpx.AsyncClient(timeout=30.0) as client:
- params = {"teamId": team_id} if team_id else {}
- env_resp = await client.get(
- f"{base_url}/v9/projects/{project_id}/env",
- headers=headers,
- params=params,
- )
-
- if env_resp.status_code != 200:
- raise HTTPException(status_code=env_resp.status_code, detail=f"获取环境变量失败: {env_resp.text}")
-
- env_vars = env_resp.json().get("envs", [])
- existing_env = None
- for env in env_vars:
- if env.get("key") == "DS2API_CONFIG_JSON":
- existing_env = env
- break
-
- if existing_env:
- env_id = existing_env["id"]
- update_resp = await client.patch(
- f"{base_url}/v9/projects/{project_id}/env/{env_id}",
- headers=headers,
- params=params,
- json={"value": config_b64},
- )
- if update_resp.status_code not in [200, 201]:
- raise HTTPException(status_code=update_resp.status_code, detail=f"更新环境变量失败: {update_resp.text}")
- else:
- create_resp = await client.post(
- f"{base_url}/v10/projects/{project_id}/env",
- headers=headers,
- params=params,
- json={
- "key": "DS2API_CONFIG_JSON",
- "value": config_b64,
- "type": "encrypted",
- "target": ["production", "preview"],
- },
- )
- if create_resp.status_code not in [200, 201]:
- raise HTTPException(status_code=create_resp.status_code, detail=f"创建环境变量失败: {create_resp.text}")
-
- # 保存 Vercel 凭证
- saved_credentials = []
- if save_vercel_credentials and not use_preconfig:
- creds_to_save = [
- ("VERCEL_TOKEN", vercel_token),
- ("VERCEL_PROJECT_ID", project_id),
- ]
- if team_id:
- creds_to_save.append(("VERCEL_TEAM_ID", team_id))
-
- for key, value in creds_to_save:
- existing = None
- for env in env_vars:
- if env.get("key") == key:
- existing = env
- break
-
- if existing:
- upd_resp = await client.patch(
- f"{base_url}/v9/projects/{project_id}/env/{existing['id']}",
- headers=headers,
- params=params,
- json={"value": value},
- )
- if upd_resp.status_code in [200, 201]:
- saved_credentials.append(key)
- else:
- crt_resp = await client.post(
- f"{base_url}/v10/projects/{project_id}/env",
- headers=headers,
- params=params,
- json={
- "key": key,
- "value": value,
- "type": "encrypted",
- "target": ["production", "preview"],
- },
- )
- if crt_resp.status_code in [200, 201]:
- saved_credentials.append(key)
-
- # 触发重新部署
- project_resp = await client.get(
- f"{base_url}/v9/projects/{project_id}",
- headers=headers,
- params=params,
- )
-
- if project_resp.status_code == 200:
- project_data = project_resp.json()
- repo = project_data.get("link", {})
-
- if repo.get("type") == "github":
- deploy_resp = await client.post(
- f"{base_url}/v13/deployments",
- headers=headers,
- params=params,
- json={
- "name": project_id,
- "project": project_id,
- "target": "production",
- "gitSource": {
- "type": "github",
- "repoId": repo.get("repoId"),
- "ref": repo.get("productionBranch", "main"),
- },
- },
- )
-
- if deploy_resp.status_code in [200, 201]:
- deploy_data = deploy_resp.json()
- # 记录同步哈希和时间
- CONFIG["_vercel_sync_hash"] = _compute_config_hash()
- CONFIG["_vercel_sync_time"] = int(_time.time())
- save_config(CONFIG)
- result = {
- "success": True,
- "message": "配置已同步,正在重新部署...",
- "deployment_url": deploy_data.get("url"),
- "validated_accounts": validated_count,
- }
- if failed_accounts:
- result["failed_accounts"] = failed_accounts
- if saved_credentials:
- result["saved_credentials"] = saved_credentials
- return JSONResponse(content=result)
-
- # 环境变量已更新,但无法自动触发重新部署
- CONFIG["_vercel_sync_hash"] = _compute_config_hash()
- CONFIG["_vercel_sync_time"] = int(_time.time())
- save_config(CONFIG)
- result = {
- "success": True,
- "message": "配置已同步到 Vercel,请手动触发重新部署",
- "manual_deploy_required": True,
- "validated_accounts": validated_count,
- }
- if failed_accounts:
- result["failed_accounts"] = failed_accounts
- if saved_credentials:
- result["saved_credentials"] = saved_credentials
- return JSONResponse(content=result)
-
- except HTTPException:
- raise
- except Exception as e:
- logger.error(f"[sync_to_vercel] 错误: {e}")
- raise HTTPException(status_code=500, detail=str(e))
-
-
-# ----------------------------------------------------------------------
-# 同步状态查询
-# ----------------------------------------------------------------------
-@router.get("/vercel/status")
-async def get_vercel_sync_status(_: bool = Depends(verify_admin)):
- """检查当前配置与上次同步到 Vercel 的配置是否一致"""
- last_hash = CONFIG.get("_vercel_sync_hash", "")
- last_time = CONFIG.get("_vercel_sync_time", 0)
- current_hash = _compute_config_hash()
-
- synced = bool(last_hash and last_hash == current_hash)
-
- return JSONResponse(content={
- "synced": synced,
- "last_sync_time": last_time if last_time else None,
- "has_synced_before": bool(last_hash),
- })
-
-
-# ----------------------------------------------------------------------
-# 导出配置
-# ----------------------------------------------------------------------
-@router.get("/export")
-async def export_config(_: bool = Depends(verify_admin)):
- """导出完整配置(JSON 和 Base64)"""
- config_json = json.dumps(CONFIG, ensure_ascii=False, separators=(",", ":"))
- config_b64 = base64.b64encode(config_json.encode("utf-8")).decode("utf-8")
-
- return JSONResponse(content={
- "json": config_json,
- "base64": config_b64,
- })
diff --git a/routes/claude.py b/routes/claude.py
deleted file mode 100644
index e9c7b25..0000000
--- a/routes/claude.py
+++ /dev/null
@@ -1,509 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Claude API 路由"""
-import json
-import random
-import time
-
-from curl_cffi import requests as cffi_requests
-from fastapi import APIRouter, HTTPException, Request
-from fastapi.responses import JSONResponse, StreamingResponse
-
-from core.config import CONFIG, logger
-from core.auth import (
- determine_mode_and_token,
- get_auth_headers,
-)
-from core.deepseek import call_completion_endpoint
-from core.session_manager import (
- create_session,
- get_pow,
- cleanup_account,
-)
-from core.models import get_model_config, get_claude_models_response
-from core.sse_parser import (
- parse_deepseek_sse_line,
- parse_sse_chunk_for_content,
- extract_content_from_chunk,
- collect_deepseek_response,
- parse_tool_calls,
-)
-from core.constants import STREAM_IDLE_TIMEOUT
-from core.utils import estimate_tokens
-from core.messages import (
- messages_prepare,
- convert_claude_to_deepseek,
- CLAUDE_DEFAULT_MODEL,
-)
-
-router = APIRouter()
-
-
-
-# ----------------------------------------------------------------------
-# 通过 OpenAI 接口调用 Claude
-# ----------------------------------------------------------------------
-async def call_claude_via_openai(request: Request, claude_payload: dict):
- """通过现有OpenAI接口调用Claude(实际调用DeepSeek)"""
- deepseek_payload = convert_claude_to_deepseek(claude_payload)
-
- try:
- session_id = create_session(request)
- if not session_id:
- raise HTTPException(status_code=401, detail="invalid token.")
-
- pow_resp = get_pow(request)
- if not pow_resp:
- raise HTTPException(
- status_code=401,
- detail="Failed to get PoW (invalid token or unknown error).",
- )
-
- model = deepseek_payload.get("model", "deepseek-chat")
- messages = deepseek_payload.get("messages", [])
-
- # 使用会话管理器获取模型配置
- thinking_enabled, search_enabled = get_model_config(model)
- if thinking_enabled is None:
- # 默认配置
- thinking_enabled = False
- search_enabled = False
-
- final_prompt = messages_prepare(messages)
-
- headers = {**get_auth_headers(request), "x-ds-pow-response": pow_resp}
- payload = {
- "chat_session_id": session_id,
- "parent_message_id": None,
- "prompt": final_prompt,
- "ref_file_ids": [],
- "thinking_enabled": thinking_enabled,
- "search_enabled": search_enabled,
- }
-
- deepseek_resp = call_completion_endpoint(payload, headers, max_attempts=3)
- return deepseek_resp
-
- except Exception as e:
- logger.error(f"[call_claude_via_openai] 调用失败: {e}")
- return None
-
-
-# ----------------------------------------------------------------------
-# Claude 路由:模型列表
-# ----------------------------------------------------------------------
-@router.get("/anthropic/v1/models")
-def list_claude_models():
- data = get_claude_models_response()
- return JSONResponse(content=data, status_code=200)
-
-
-# ----------------------------------------------------------------------
-# Claude 路由:/anthropic/v1/messages
-# ----------------------------------------------------------------------
-@router.post("/anthropic/v1/messages")
-async def claude_messages(request: Request):
- try:
- try:
- determine_mode_and_token(request)
- except HTTPException as exc:
- return JSONResponse(
- status_code=exc.status_code, content={"error": exc.detail}
- )
- except Exception as exc:
- logger.error(f"[claude_messages] determine_mode_and_token 异常: {exc}")
- return JSONResponse(
- status_code=500, content={"error": "Claude authentication failed."}
- )
-
- req_data = await request.json()
- model = req_data.get("model")
- messages = req_data.get("messages", [])
-
- if not model or not messages:
- raise HTTPException(
- status_code=400, detail="Request must include 'model' and 'messages'."
- )
-
- # 标准化消息内容
- normalized_messages = []
- for message in messages:
- normalized_message = message.copy()
- if isinstance(message.get("content"), list):
- content_parts = []
- for content_block in message["content"]:
- if content_block.get("type") == "text" and "text" in content_block:
- content_parts.append(content_block["text"])
- elif content_block.get("type") == "tool_result":
- if "content" in content_block:
- content_parts.append(str(content_block["content"]))
- if content_parts:
- normalized_message["content"] = "\n".join(content_parts)
- elif isinstance(message.get("content"), list) and message["content"]:
- normalized_message["content"] = message["content"]
- else:
- normalized_message["content"] = ""
- normalized_messages.append(normalized_message)
-
- tools_requested = req_data.get("tools") or []
- has_tools = len(tools_requested) > 0
-
- payload = req_data.copy()
- payload["messages"] = normalized_messages.copy()
-
- # 如果有工具定义,添加工具使用指导的系统消息
- if has_tools and not any(m.get("role") == "system" for m in payload["messages"]):
- tool_schemas = []
- for tool in tools_requested:
- tool_name = tool.get("name", "unknown")
- tool_desc = tool.get("description", "No description available")
- schema = tool.get("input_schema", {})
-
- tool_info = f"Tool: {tool_name}\nDescription: {tool_desc}"
- if "properties" in schema:
- props = []
- required = schema.get("required", [])
- for prop_name, prop_info in schema["properties"].items():
- prop_type = prop_info.get("type", "string")
- is_req = " (required)" if prop_name in required else ""
- props.append(f" - {prop_name}: {prop_type}{is_req}")
- if props:
- tool_info += f"\nParameters:\n{chr(10).join(props)}"
- tool_schemas.append(tool_info)
-
- system_message = {
- "role": "system",
- "content": f"""You are Claude, a helpful AI assistant. You have access to these tools:
-
-{chr(10).join(tool_schemas)}
-
-When you need to use tools, you can call multiple tools in a single response. Use this format:
-
-{{"tool_calls": [
- {{"name": "tool1", "input": {{"param": "value"}}}},
- {{"name": "tool2", "input": {{"param": "value"}}}}
-]}}
-
-IMPORTANT: You can call multiple tools in ONE response.
-
-Remember: Output ONLY the JSON, no other text. The response must start with {{ and end with ]}}""",
- }
- payload["messages"].insert(0, system_message)
-
- deepseek_resp = await call_claude_via_openai(request, payload)
- if not deepseek_resp:
- raise HTTPException(status_code=500, detail="Failed to get Claude response.")
-
- if deepseek_resp.status_code != 200:
- deepseek_resp.close()
- return JSONResponse(
- status_code=500,
- content={"error": {"type": "api_error", "message": "Failed to get response"}},
- )
-
- # 流式响应或普通响应
- if bool(req_data.get("stream", False)):
-
- def claude_sse_stream():
- # 使用导入的常量(不再本地定义)
- try:
- message_id = f"msg_{int(time.time())}_{random.randint(1000, 9999)}"
- input_tokens = sum(len(str(m.get("content", ""))) for m in messages) // 4
- output_tokens = 0
- full_response_text = ""
- last_content_time = time.time()
- has_content = False
-
-
- for line in deepseek_resp.iter_lines():
- current_time = time.time()
-
- # 智能超时检测
- if has_content and (current_time - last_content_time) > STREAM_IDLE_TIMEOUT:
- logger.warning(f"[claude_sse_stream] 智能超时: 已有内容但 {STREAM_IDLE_TIMEOUT}s 无新数据,强制结束")
- break
-
- if not line:
- continue
- try:
- line_str = line.decode("utf-8")
- except Exception:
- continue
-
- if line_str.startswith("data:"):
- data_str = line_str[5:].strip()
- if data_str == "[DONE]":
- break
-
- try:
- chunk = json.loads(data_str)
-
- # 检测内容审核/敏感词阻止
- if "error" in chunk or chunk.get("code") == "content_filter":
- logger.warning(f"[claude_sse_stream] 检测到内容过滤: {chunk}")
- break
-
- if "v" in chunk and isinstance(chunk["v"], str):
- content = chunk["v"]
- # 检查是否是 FINISHED 状态
- if content == "FINISHED":
- break
- full_response_text += content
- if content:
- has_content = True
- last_content_time = current_time
- elif "v" in chunk and isinstance(chunk["v"], list):
- for item in chunk["v"]:
- if item.get("p") == "status" and item.get("v") == "FINISHED":
- break
- except (json.JSONDecodeError, KeyError):
- continue
-
- # 发送Claude格式的事件
- message_start = {
- "type": "message_start",
- "message": {
- "id": message_id,
- "type": "message",
- "role": "assistant",
- "model": model,
- "content": [],
- "stop_reason": None,
- "stop_sequence": None,
- "usage": {"input_tokens": input_tokens, "output_tokens": 0},
- },
- }
- yield f"data: {json.dumps(message_start)}\n\n"
-
- # 检查工具调用
- # 使用公共函数检测工具调用
- detected_tools = parse_tool_calls(full_response_text, tools_requested)
-
- content_index = 0
- if detected_tools:
- stop_reason = "tool_use"
- for tool_info in detected_tools:
- tool_use_id = f"toolu_{int(time.time())}_{random.randint(1000, 9999)}_{content_index}"
- tool_name = tool_info["name"]
- tool_input = tool_info["input"]
-
- yield f"data: {json.dumps({'type': 'content_block_start', 'index': content_index, 'content_block': {'type': 'tool_use', 'id': tool_use_id, 'name': tool_name, 'input': tool_input}})}\n\n"
- yield f"data: {json.dumps({'type': 'content_block_stop', 'index': content_index})}\n\n"
-
- content_index += 1
- output_tokens += len(str(tool_input)) // 4
- else:
- stop_reason = "end_turn"
- if full_response_text:
- yield f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
- yield f"data: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': full_response_text}})}\n\n"
- yield f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
- output_tokens += len(full_response_text) // 4
-
- yield f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': stop_reason, 'stop_sequence': None}, 'usage': {'output_tokens': output_tokens}})}\n\n"
- yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
-
- except Exception as e:
- logger.error(f"[claude_sse_stream] 异常: {e}")
- error_event = {
- "type": "error",
- "error": {"type": "api_error", "message": f"Stream processing error: {str(e)}"},
- }
- yield f"data: {json.dumps(error_event)}\n\n"
- finally:
- try:
- deepseek_resp.close()
- except Exception:
- pass
- # 注意:不在此处调用 cleanup_account,由外层 finally 统一处理
-
- return StreamingResponse(
- claude_sse_stream(),
- media_type="text/event-stream",
- headers={"Content-Type": "text/event-stream"},
- )
- else:
- # 非流式响应处理
- try:
- final_content = ""
- final_reasoning = ""
-
- for line in deepseek_resp.iter_lines():
- if not line:
- continue
- try:
- line_str = line.decode("utf-8")
- except Exception as e:
- logger.warning(f"[claude_messages] 行解码失败: {e}")
- continue
-
- if line_str.startswith("data:"):
- data_str = line_str[5:].strip()
- if data_str == "[DONE]":
- break
-
- try:
- chunk = json.loads(data_str)
- if "v" in chunk:
- v_value = chunk["v"]
- if "p" in chunk and chunk.get("p") == "response/search_status":
- continue
- ptype = "text"
- if "p" in chunk and chunk.get("p") == "response/thinking_content":
- ptype = "thinking"
- elif "p" in chunk and chunk.get("p") == "response/content":
- ptype = "text"
- if isinstance(v_value, str):
- if ptype == "thinking":
- final_reasoning += v_value
- else:
- final_content += v_value
- elif isinstance(v_value, list):
- for item in v_value:
- if item.get("p") == "status" and item.get("v") == "FINISHED":
- break
- except json.JSONDecodeError as e:
- logger.warning(f"[claude_messages] JSON解析失败: {e}")
- continue
- except Exception as e:
- logger.warning(f"[claude_messages] chunk处理失败: {e}")
- continue
-
- try:
- deepseek_resp.close()
- except Exception as e:
- logger.warning(f"[claude_messages] 关闭响应异常: {e}")
-
- # 检查工具调用
- detected_tools = parse_tool_calls(final_content, tools_requested)
-
- # 构造响应
- claude_response = {
- "id": f"msg_{int(time.time())}_{random.randint(1000, 9999)}",
- "type": "message",
- "role": "assistant",
- "model": model,
- "content": [],
- "stop_reason": "tool_use" if detected_tools else "end_turn",
- "stop_sequence": None,
- "usage": {
- "input_tokens": len(str(normalized_messages)) // 4,
- "output_tokens": (len(final_content) + len(final_reasoning)) // 4,
- },
- }
-
- if final_reasoning:
- claude_response["content"].append({"type": "thinking", "thinking": final_reasoning})
-
- if detected_tools:
- for i, tool_info in enumerate(detected_tools):
- tool_use_id = f"toolu_{int(time.time())}_{random.randint(1000, 9999)}_{i}"
- claude_response["content"].append({
- "type": "tool_use",
- "id": tool_use_id,
- "name": tool_info["name"],
- "input": tool_info["input"],
- })
- else:
- if final_content or not final_reasoning:
- claude_response["content"].append({
- "type": "text",
- "text": final_content or "抱歉,没有生成有效的响应内容。",
- })
-
- return JSONResponse(content=claude_response, status_code=200)
-
- except Exception as e:
- logger.error(f"[claude_messages] 非流式响应处理异常: {e}")
- try:
- deepseek_resp.close()
- except Exception as close_e:
- logger.warning(f"[claude_messages] 关闭响应异常2: {close_e}")
- return JSONResponse(
- status_code=500,
- content={"error": {"type": "api_error", "message": "Response processing error"}},
- )
-
- except HTTPException as exc:
- return JSONResponse(
- status_code=exc.status_code,
- content={"error": {"type": "invalid_request_error", "message": exc.detail}},
- )
- except Exception as exc:
- logger.error(f"[claude_messages] 未知异常: {exc}")
- return JSONResponse(
- status_code=500,
- content={"error": {"type": "api_error", "message": "Internal Server Error"}},
- )
- finally:
- cleanup_account(request)
-
-
-# ----------------------------------------------------------------------
-# Claude 路由:/anthropic/v1/messages/count_tokens
-# ----------------------------------------------------------------------
-@router.post("/anthropic/v1/messages/count_tokens")
-async def claude_count_tokens(request: Request):
- try:
- try:
- determine_mode_and_token(request)
- except HTTPException as exc:
- return JSONResponse(status_code=exc.status_code, content={"error": exc.detail})
- except Exception as exc:
- logger.error(f"[claude_count_tokens] determine_mode_and_token 异常: {exc}")
- return JSONResponse(status_code=500, content={"error": "Claude authentication failed."})
-
- req_data = await request.json()
- model = req_data.get("model")
- messages = req_data.get("messages", [])
- system = req_data.get("system", "")
-
- if not model or not messages:
- raise HTTPException(
- status_code=400, detail="Request must include 'model' and 'messages'."
- )
-
- input_tokens = 0
-
- if system:
- input_tokens += estimate_tokens(system)
-
- for message in messages:
- content = message.get("content", "")
- input_tokens += 2 # 角色标记
-
- if isinstance(content, list):
- for content_block in content:
- if isinstance(content_block, dict):
- if content_block.get("type") == "text":
- input_tokens += estimate_tokens(content_block.get("text", ""))
- elif content_block.get("type") == "tool_result":
- input_tokens += estimate_tokens(content_block.get("content", ""))
- else:
- input_tokens += estimate_tokens(str(content_block))
- else:
- input_tokens += estimate_tokens(str(content_block))
- else:
- input_tokens += estimate_tokens(content)
-
- tools = req_data.get("tools", [])
- if tools:
- for tool in tools:
- input_tokens += estimate_tokens(tool.get("name", ""))
- input_tokens += estimate_tokens(tool.get("description", ""))
- input_schema = tool.get("input_schema", {})
- input_tokens += estimate_tokens(json.dumps(input_schema, ensure_ascii=False))
-
- response = {"input_tokens": max(1, input_tokens)}
- return JSONResponse(content=response, status_code=200)
-
- except HTTPException as exc:
- return JSONResponse(
- status_code=exc.status_code,
- content={"error": {"type": "invalid_request_error", "message": exc.detail}},
- )
- except Exception as exc:
- logger.error(f"[claude_count_tokens] 未知异常: {exc}")
- return JSONResponse(
- status_code=500,
- content={"error": {"type": "api_error", "message": "Internal Server Error"}},
- )
diff --git a/routes/home.py b/routes/home.py
deleted file mode 100644
index 7199f33..0000000
--- a/routes/home.py
+++ /dev/null
@@ -1,308 +0,0 @@
-# -*- coding: utf-8 -*-
-"""首页和 WebUI 路由"""
-import os
-from fastapi import APIRouter, Request
-from fastapi.responses import HTMLResponse, FileResponse
-
-from core.config import STATIC_ADMIN_DIR
-
-router = APIRouter()
-
-# 首页 HTML(内嵌避免依赖模板目录)
-WELCOME_HTML = """
-
-
-
-
- DeepSeek to OpenAI & Claude Compatible API Interface 完美适配 OpenAI 与 Claude API 格式,无缝集成现有工具。 内置智能轮询机制,支持多账号并发,稳定高效。 完整支持 推理过程输出,让思考可见。 集成 DeepSeek 原生搜索能力,获取最新实时资讯。全面兼容
- 负载均衡
- 深度思考
- 联网搜索
- WebUI not built
Run cd webui && npm run build first.