mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-21 00:17:44 +08:00
feat: Implement DeepSeek account validation, testing, and queue status monitoring with corresponding admin API endpoints.
This commit is contained in:
@@ -33,8 +33,9 @@ LOG_LEVEL=INFO
|
|||||||
# DS2API_WASM_PATH=sha3_wasm_bg.7b9ca65ddd.wasm
|
# DS2API_WASM_PATH=sha3_wasm_bg.7b9ca65ddd.wasm
|
||||||
|
|
||||||
# ===== Admin 管理界面 =====
|
# ===== Admin 管理界面 =====
|
||||||
# Admin API 密钥(留空则开发模式,无需认证)
|
# Admin API 密钥(Vercel 部署必填!保护 WebUI 管理界面)
|
||||||
# DS2API_ADMIN_KEY=your-admin-secret-key
|
# 首次同步时会自动保存到 Vercel 环境变量
|
||||||
|
DS2API_ADMIN_KEY=your-admin-secret-key
|
||||||
|
|
||||||
# ===== Vercel 集成(可选,用于一键同步部署)=====
|
# ===== Vercel 集成(可选,用于一键同步部署)=====
|
||||||
# Vercel API Token(从 https://vercel.com/account/tokens 获取)
|
# Vercel API Token(从 https://vercel.com/account/tokens 获取)
|
||||||
|
|||||||
310
README.MD
310
README.MD
@@ -1,273 +1,149 @@
|
|||||||
# DeepSeek2API
|
# DS2API
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
支持高速流式输出、支持多轮对话、支持R1深度思考,多路账号支持。
|
将 DeepSeek 网页版转换为 OpenAI 兼容 API,支持多账号轮询、自动 Token 刷新、可视化管理界面。
|
||||||
|
|
||||||
与ChatGPT接口完全兼容。
|
## ✨ 特性
|
||||||
|
|
||||||
## 目录
|
- 🚀 **OpenAI 兼容** - 完全兼容 OpenAI API 格式,无缝接入现有应用
|
||||||
|
- 🔄 **多账号轮询** - Round-Robin 负载均衡,支持高并发
|
||||||
|
- 🔐 **Token 自动刷新** - 过期自动重新登录,无需手动维护
|
||||||
|
- 🌐 **WebUI 管理** - 可视化添加账号、测试 API、同步 Vercel
|
||||||
|
- 🔍 **联网搜索** - 支持 DeepSeek 搜索增强模式
|
||||||
|
- 🧠 **深度思考** - 支持 R1 推理模式
|
||||||
|
- ☁️ **Vercel 部署** - 一键部署,无需服务器
|
||||||
|
|
||||||
* [免责声明](#免责声明)
|
## 📋 模型支持
|
||||||
* [接入准备](#接入准备)
|
|
||||||
* [多账号接入](#多账号接入)
|
|
||||||
* [Vercel部署](#Vercel部署)
|
|
||||||
* [接口列表](#接口列表)
|
|
||||||
* [模型列表](#模型列表)
|
|
||||||
* [对话补全](#对话补全)
|
|
||||||
* [注意事项](#注意事项)
|
|
||||||
* [Nginx反代优化](#Nginx反代优化)
|
|
||||||
* [Token统计](#Token统计)
|
|
||||||
* [Star History](#star-history)
|
|
||||||
* [鸣谢](#鸣谢)
|
|
||||||
|
|
||||||
## 免责声明
|
| 模型 | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `deepseek-chat` | DeepSeek V3.2 非推理模式 |
|
||||||
|
| `deepseek-reasoner` | DeepSeek V3.2 推理模式(深度思考) |
|
||||||
|
| `deepseek-chat-search` | 非推理 + 联网搜索 |
|
||||||
|
| `deepseek-reasoner-search` | 推理 + 联网搜索 |
|
||||||
|
|
||||||
**逆向API是不稳定的,建议前往DeepSeek官方 https://platform.deepseek.com/ 付费使用API,避免封禁的风险。**
|
## 🚀 快速开始
|
||||||
|
|
||||||
**本组织和个人不接受任何资金捐助和交易,此项目是纯粹研究交流学习性质!**
|
### Vercel 部署(推荐)
|
||||||
|
|
||||||
**仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!**
|
[](https://vercel.com/import/project?template=https://github.com/CJackHwang/ds2api)
|
||||||
|
|
||||||
**仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!**
|
1. 点击上方按钮部署
|
||||||
|
2. 设置环境变量 `DS2API_ADMIN_KEY`(管理密码,**必填**)
|
||||||
|
3. 部署完成后访问 `/webui` 管理界面
|
||||||
|
4. 添加 DeepSeek 账号和 API Key
|
||||||
|
5. 点击「同步到 Vercel」完成配置
|
||||||
|
|
||||||
**仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!**
|
> **首次同步会自动验证账号、保存 Token 和 Vercel 凭证,后续操作无需再输入。**
|
||||||
|
|
||||||
## 接入准备
|
### 本地运行
|
||||||
|
|
||||||
一个或多个 DeepSeek 账号
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone https://github.com/CJackHwang/ds2api.git
|
||||||
|
cd ds2api
|
||||||
|
|
||||||
### 多账号接入
|
# 安装依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
目前同个账号同时只能有*一路*输出,你可以通过提供多个账号并使用。
|
# 配置
|
||||||
|
cp config.example.json config.json
|
||||||
|
# 编辑 config.json 添加账号
|
||||||
|
|
||||||
每次请求服务会从中挑选一个。
|
# 运行
|
||||||
|
python dev.py
|
||||||
|
```
|
||||||
|
|
||||||
## Vercel部署
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
> [!NOTE]
|
### 环境变量
|
||||||
> Vercel免费账户的请求响应超时时间为10秒,但接口响应通常较久,可能会遇到Vercel返回的504超时错误!
|
|
||||||
|
|
||||||
[](https://vercel.com/import/project?template=https://github.com/iidamie/deepseek2api)
|
| 变量 | 说明 | 是否必填 |
|
||||||
|
|-----|------|---------|
|
||||||
|
| `DS2API_ADMIN_KEY` | WebUI 管理密码 | Vercel 必填 |
|
||||||
|
| `DS2API_CONFIG_JSON` | 配置 JSON(支持 Base64) | 可选 |
|
||||||
|
| `VERCEL_TOKEN` | Vercel API Token(用于快捷同步) | 可选 |
|
||||||
|
| `VERCEL_PROJECT_ID` | Vercel 项目 ID | 可选 |
|
||||||
|
|
||||||
### 部署步骤
|
### 配置文件格式
|
||||||
|
|
||||||
1. **点击上方按钮一键部署到 Vercel**
|
|
||||||
|
|
||||||
2. **配置方式选择(二选一)**
|
|
||||||
|
|
||||||
#### 方式 A:使用配置文件(推荐用于开发测试)
|
|
||||||
|
|
||||||
部署完成后,返回你的 Github 仓库,创建/编辑 `config.json` 文件(可参考 `config.example.json`):
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"keys": [
|
"keys": ["your-api-key"],
|
||||||
"your-api-key-1",
|
|
||||||
"your-api-key-2"
|
|
||||||
],
|
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"email": "example1@example.com",
|
"email": "your@email.com",
|
||||||
"password": "password1",
|
"password": "your-password",
|
||||||
"token": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"email": "example2@example.com",
|
|
||||||
"password": "password2",
|
|
||||||
"token": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"mobile": "12345678901",
|
|
||||||
"password": "password3",
|
|
||||||
"token": ""
|
"token": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
配置完成后,返回 Vercel 控制台,点击 "Redeploy" 重新部署即可。
|
## 📡 API 接口
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> 一定要是私库,防止信息泄露!
|
|
||||||
|
|
||||||
#### 方式 B:使用环境变量(推荐用于生产环境)
|
|
||||||
|
|
||||||
在 Vercel 控制台的 Settings → Environment Variables 中添加以下环境变量:
|
|
||||||
|
|
||||||
- **环境变量名**:`DS2API_CONFIG_JSON` 或 `CONFIG_JSON`
|
|
||||||
- **环境变量值**:配置内容的 JSON 字符串(可选:使用 base64 编码)
|
|
||||||
|
|
||||||
**JSON 字符串示例**:
|
|
||||||
```json
|
|
||||||
{"keys":["your-api-key-1"],"accounts":[{"email":"example@example.com","password":"your-password","token":""}]}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Base64 编码示例**(Linux/Mac):
|
|
||||||
```bash
|
|
||||||
echo '{"keys":["your-api-key-1"],"accounts":[{"email":"example@example.com","password":"your-password","token":""}]}' | base64
|
|
||||||
```
|
|
||||||
|
|
||||||
添加环境变量后,在 Vercel 控制台重新部署即可。
|
|
||||||
|
|
||||||
### 配置说明
|
|
||||||
|
|
||||||
* `keys` - 你的 API 鉴权密钥(可设置多个)
|
|
||||||
* `accounts` - DeepSeek 账号列表,支持多个账号轮换,避免单账号受限
|
|
||||||
* 支持使用 `email` 或 `mobile` 登录
|
|
||||||
* `token` 字段可以留空,系统会在首次使用时自动登录并填充(注意:Vercel 环境下 token 无法回写)
|
|
||||||
|
|
||||||
### 注意事项
|
|
||||||
|
|
||||||
- 推荐使用**环境变量**方式配置,更加安全
|
|
||||||
- 如使用配置文件方式,确保你的 GitHub 仓库是**私有仓库**
|
|
||||||
- Vercel 免费版有请求时长限制(10秒),如遇 504 错误属正常现象
|
|
||||||
- Vercel 环境下文件系统是只读的,token 自动更新功能可能受限
|
|
||||||
|
|
||||||
## 接口列表
|
|
||||||
|
|
||||||
目前支持与openai兼容的 `/v1/chat/completions` 接口,可自行使用与openai或其他兼容的客户端接入接口。
|
|
||||||
|
|
||||||
### 模型列表
|
### 模型列表
|
||||||
|
|
||||||
获取模型列表接口
|
```
|
||||||
|
GET /v1/models
|
||||||
**GET /v1/models**
|
|
||||||
|
|
||||||
响应数据:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"created": 1677610602,
|
|
||||||
"id": "deepseek-chat",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "deepseek",
|
|
||||||
"permission": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created": 1677610602,
|
|
||||||
"id": "deepseek-reasoner",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "deepseek",
|
|
||||||
"permission": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created": 1677610602,
|
|
||||||
"id": "deepseek-chat-search",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "deepseek",
|
|
||||||
"permission": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"created": 1677610602,
|
|
||||||
"id": "deepseek-reasoner-search",
|
|
||||||
"object": "model",
|
|
||||||
"owned_by": "deepseek",
|
|
||||||
"permission": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"object": "list"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 对话补全
|
### 对话补全
|
||||||
|
|
||||||
对话补全接口,与openai的 [chat-completions-api](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) 兼容。
|
|
||||||
|
|
||||||
**POST /v1/chat/completions**
|
|
||||||
|
|
||||||
header 需要设置 Authorization 头部:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Authorization: Bearer [userToken value or keys]
|
POST /v1/chat/completions
|
||||||
|
Authorization: Bearer your-api-key
|
||||||
```
|
```
|
||||||
|
|
||||||
请求数据:
|
请求示例:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
// model名称
|
"model": "deepseek-chat",
|
||||||
// 默认:deepseek-chat 或 deepseek-v3
|
"messages": [{"role": "user", "content": "你好"}],
|
||||||
// 深度思考:deepseek-reasoner 或 deepseek-r1
|
"stream": true
|
||||||
// 联网搜索:deepseek-chat-search 或 deepseek-v3-search
|
|
||||||
// 联网搜索 + 深度思考:deepseek-reasoner-search 或 deepseek-r1-search
|
|
||||||
"model": "deepseek-chat",
|
|
||||||
// 多轮对话基于消息合并实现,某些场景可能导致能力下降且受单轮最大token数限制
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"role": "user",
|
|
||||||
"content": "你是谁?"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// 如果使用流式响应请设置为true,默认false
|
|
||||||
"stream": false
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
响应数据:
|
### 管理接口
|
||||||
```json
|
|
||||||
{
|
|
||||||
"choices": [
|
|
||||||
{
|
|
||||||
"finish_reason": "stop",
|
|
||||||
"index": 0,
|
|
||||||
"message": {
|
|
||||||
"content": "您好!我是由中国的深度求索(DeepSeek)公司开发的智能助手DeepSeek-R1。如您有任何任何问题,我会尽我所能为您提供帮助。",
|
|
||||||
"reasoning_content": "您好!我是由中国的深度求索(DeepSeek)公司开发的智能助手DeepSeek-R1。如您有任何任何问题,我会尽我所能为您提供帮助。\n",
|
|
||||||
"role": "assistant"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"created": 1738598554,
|
|
||||||
"id": "bc223da6-f091-4687-9f59-b9f2a917bd49",
|
|
||||||
"model": "deepseek-r1",
|
|
||||||
"object": "chat.completion",
|
|
||||||
"usage": {
|
|
||||||
"completion_tokens": 37,
|
|
||||||
"prompt_tokens": 1,
|
|
||||||
"total_tokens": 38
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
| 接口 | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `GET /webui` | 管理界面 |
|
||||||
|
| `GET /admin/config` | 获取配置 |
|
||||||
|
| `POST /admin/accounts/test` | 测试单个账号 |
|
||||||
|
| `POST /admin/accounts/test-all` | 批量测试账号 |
|
||||||
|
| `POST /admin/vercel/sync` | 同步到 Vercel |
|
||||||
|
|
||||||
### Nginx反代优化
|
## 🔧 Nginx 反代配置
|
||||||
|
|
||||||
如果您正在使用Nginx反向代理deepseek2api,请添加以下配置项优化流的输出效果,优化体验感。
|
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
# 关闭代理缓冲。当设置为off时,Nginx会立即将客户端请求发送到后端服务器,并立即将从后端服务器接收到的响应发送回客户端。
|
location / {
|
||||||
proxy_buffering off;
|
proxy_pass http://localhost:5001;
|
||||||
# 启用分块传输编码。分块传输编码允许服务器为动态生成的内容分块发送数据,而不需要预先知道内容的大小。
|
proxy_buffering off;
|
||||||
chunked_transfer_encoding on;
|
chunked_transfer_encoding on;
|
||||||
# 开启TCP_NOPUSH,这告诉Nginx在数据包发送到客户端之前,尽可能地发送数据。这通常在sendfile使用时配合使用,可以提高网络效率。
|
tcp_nopush on;
|
||||||
tcp_nopush on;
|
tcp_nodelay on;
|
||||||
# 开启TCP_NODELAY,这告诉Nginx不延迟发送数据,立即发送小数据包。在某些情况下,这可以减少网络的延迟。
|
keepalive_timeout 120;
|
||||||
tcp_nodelay on;
|
|
||||||
# 设置保持连接的超时时间,这里设置为120秒。如果在这段时间内,客户端和服务器之间没有进一步的通信,连接将被关闭。
|
|
||||||
keepalive_timeout 120;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Token统计
|
|
||||||
|
|
||||||
符合 OPENAI 接口规范
|
|
||||||
|
|
||||||
示例:
|
|
||||||
```json
|
|
||||||
"usage": {
|
|
||||||
"completion_tokens": 37,
|
|
||||||
"prompt_tokens": 1,
|
|
||||||
"total_tokens": 38
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Star History
|
## ⚠️ 免责声明
|
||||||
|
|
||||||
[](https://star-history.com/#iidamie/deepseek2api&Date)
|
**逆向 API 不稳定,建议前往 [DeepSeek 官方](https://platform.deepseek.com/) 付费使用 API。**
|
||||||
|
|
||||||
## 鸣谢
|
**本项目仅供学习交流,禁止商用或对外提供服务,风险自担!**
|
||||||
|
|
||||||
本项目部分代码参考了 [LLM-Red-Team/deepseek-free-api](https://github.com/LLM-Red-Team/deepseek-free-api),在此表示感谢
|
## 📜 鸣谢
|
||||||
|
|
||||||
|
本项目基于以下开源项目并进行了大量修改:
|
||||||
|
|
||||||
|
- [iidamie/deepseek2api](https://github.com/iidamie/deepseek2api) - 二开版本
|
||||||
|
- [LLM-Red-Team/deepseek-free-api](https://github.com/LLM-Red-Team/deepseek-free-api) - 早期原始项目
|
||||||
|
|
||||||
|
## 📊 Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#CJackHwang/ds2api&Date)
|
||||||
|
|||||||
147
core/auth.py
147
core/auth.py
@@ -1,21 +1,29 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""账号认证与管理模块"""
|
"""账号认证与管理模块 - 轮询(Round-Robin)策略"""
|
||||||
import random
|
import threading
|
||||||
from fastapi import HTTPException, Request
|
from fastapi import HTTPException, Request
|
||||||
|
|
||||||
from .config import CONFIG, logger
|
from .config import CONFIG, logger
|
||||||
from .deepseek import login_deepseek_via_account, BASE_HEADERS
|
from .deepseek import login_deepseek_via_account, BASE_HEADERS
|
||||||
|
|
||||||
# -------------------------- 全局账号队列 --------------------------
|
# -------------------------- 全局账号队列 --------------------------
|
||||||
account_queue = [] # 维护所有可用账号
|
# 使用列表实现轮询队列,配合线程锁保证并发安全
|
||||||
|
account_queue = [] # 可用账号队列
|
||||||
|
in_use_accounts = {} # 正在使用的账号 {account_id: account}
|
||||||
|
_queue_lock = threading.Lock() # 线程锁
|
||||||
|
|
||||||
claude_api_key_queue = [] # 维护所有可用的Claude API keys
|
claude_api_key_queue = [] # 维护所有可用的Claude API keys
|
||||||
|
|
||||||
|
|
||||||
def init_account_queue():
|
def init_account_queue():
|
||||||
"""初始化时从配置加载账号"""
|
"""初始化时从配置加载账号(不再随机排序,保持配置顺序)"""
|
||||||
global account_queue
|
global account_queue, in_use_accounts
|
||||||
account_queue = CONFIG.get("accounts", [])[:] # 深拷贝
|
with _queue_lock:
|
||||||
random.shuffle(account_queue) # 初始随机排序
|
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():
|
def init_claude_api_key_queue():
|
||||||
@@ -37,43 +45,71 @@ def get_account_identifier(account: dict) -> str:
|
|||||||
return account.get("email", "").strip() or account.get("mobile", "").strip()
|
return account.get("email", "").strip() or account.get("mobile", "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_queue_status() -> dict:
|
||||||
|
"""获取账号队列状态(用于监控)"""
|
||||||
|
with _queue_lock:
|
||||||
|
return {
|
||||||
|
"available": len(account_queue),
|
||||||
|
"in_use": len(in_use_accounts),
|
||||||
|
"total": len(account_queue) + len(in_use_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):
|
def choose_new_account(exclude_ids=None):
|
||||||
"""选择策略:
|
"""轮询选择策略:
|
||||||
1. 优先选择已有 token 的账号(避免登录)
|
1. 使用线程锁保证并发安全
|
||||||
2. 遍历队列,找到第一个未被 exclude_ids 包含的账号
|
2. 优先选择队首的有 token 账号
|
||||||
3. 从队列中移除该账号
|
3. 从队列头部取出账号(FIFO)
|
||||||
4. 返回该账号(由后续逻辑保证最终会重新入队)
|
4. 请求完成后调用 release_account 将账号放回队尾
|
||||||
"""
|
"""
|
||||||
if exclude_ids is None:
|
if exclude_ids is None:
|
||||||
exclude_ids = []
|
exclude_ids = []
|
||||||
|
|
||||||
# 第一轮:优先选择已有 token 的账号
|
with _queue_lock:
|
||||||
for i in range(len(account_queue)):
|
# 第一轮:优先选择已有 token 的账号
|
||||||
acc = account_queue[i]
|
for i in range(len(account_queue)):
|
||||||
acc_id = get_account_identifier(acc)
|
acc = account_queue[i]
|
||||||
if acc_id and acc_id not in exclude_ids:
|
acc_id = get_account_identifier(acc)
|
||||||
if acc.get("token", "").strip(): # 已有 token
|
if acc_id and acc_id not in exclude_ids:
|
||||||
logger.info(f"[choose_new_account] 选择已有token的账号: {acc_id}")
|
if acc.get("token", "").strip(): # 已有 token
|
||||||
return account_queue.pop(i)
|
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)):
|
for i in range(len(account_queue)):
|
||||||
acc = account_queue[i]
|
acc = account_queue[i]
|
||||||
acc_id = get_account_identifier(acc)
|
acc_id = get_account_identifier(acc)
|
||||||
if acc_id and acc_id not in exclude_ids:
|
if acc_id and acc_id not in exclude_ids:
|
||||||
logger.info(f"[choose_new_account] 选择需登录的账号: {acc_id}")
|
selected = account_queue.pop(i)
|
||||||
return 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("[choose_new_account] 没有可用的账号或所有账号都在使用中")
|
logger.warning(f"[choose_new_account] 没有可用账号 | 队列: {len(account_queue)}, 使用中: {len(in_use_accounts)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def release_account(account: dict):
|
def release_account(account: dict):
|
||||||
"""将账号重新加入队列末尾"""
|
"""将账号重新加入队列末尾(轮询核心:用完放队尾)"""
|
||||||
account_queue.append(account)
|
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)}")
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -144,3 +180,50 @@ def get_auth_headers(request: Request) -> dict:
|
|||||||
def determine_claude_mode_and_token(request: Request):
|
def determine_claude_mode_and_token(request: Request):
|
||||||
"""Claude认证:沿用现有的OpenAI接口认证逻辑"""
|
"""Claude认证:沿用现有的OpenAI接口认证逻辑"""
|
||||||
determine_mode_and_token(request)
|
determine_mode_and_token(request)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# 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"] = ""
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ DEEPSEEK_COMPLETION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/completion"
|
|||||||
|
|
||||||
BASE_HEADERS = {
|
BASE_HEADERS = {
|
||||||
"Host": "chat.deepseek.com",
|
"Host": "chat.deepseek.com",
|
||||||
"User-Agent": "DeepSeek/1.0.13 Android/35",
|
"User-Agent": "DeepSeek/1.6.11 Android/35",
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"Accept-Encoding": "gzip",
|
"Accept-Encoding": "gzip",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-client-platform": "android",
|
"x-client-platform": "android",
|
||||||
"x-client-version": "1.3.0-auto-resume",
|
"x-client-version": "1.6.11",
|
||||||
"x-client-locale": "zh_CN",
|
"x-client-locale": "zh_CN",
|
||||||
"accept-charset": "UTF-8",
|
"accept-charset": "UTF-8",
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,24 @@ def login_deepseek_via_account(account: dict) -> str:
|
|||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500, detail="Account login failed: invalid JSON response"
|
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 (
|
if (
|
||||||
data.get("data") is None
|
data.get("data") is None
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from .auth import (
|
|||||||
choose_new_account,
|
choose_new_account,
|
||||||
get_account_identifier,
|
get_account_identifier,
|
||||||
release_account,
|
release_account,
|
||||||
|
refresh_account_token,
|
||||||
)
|
)
|
||||||
from .deepseek import (
|
from .deepseek import (
|
||||||
DEEPSEEK_CREATE_SESSION_URL,
|
DEEPSEEK_CREATE_SESSION_URL,
|
||||||
@@ -30,6 +31,8 @@ def create_session(request: Request, max_attempts: int = 3) -> str | None:
|
|||||||
会话 ID,如果失败返回 None
|
会话 ID,如果失败返回 None
|
||||||
"""
|
"""
|
||||||
attempts = 0
|
attempts = 0
|
||||||
|
token_refreshed = False # 标记是否已尝试刷新 token
|
||||||
|
|
||||||
while attempts < max_attempts:
|
while attempts < max_attempts:
|
||||||
headers = get_auth_headers(request)
|
headers = get_auth_headers(request)
|
||||||
try:
|
try:
|
||||||
@@ -56,13 +59,25 @@ def create_session(request: Request, max_attempts: int = 3) -> str | None:
|
|||||||
return session_id
|
return session_id
|
||||||
else:
|
else:
|
||||||
code = data.get("code")
|
code = data.get("code")
|
||||||
|
msg = data.get("msg", "")
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[create_session] 创建会话失败, code={code}, msg={data.get('msg')}"
|
f"[create_session] 创建会话失败, code={code}, msg={msg}"
|
||||||
)
|
)
|
||||||
resp.close()
|
resp.close()
|
||||||
|
|
||||||
# 配置模式下尝试切换账号
|
# 配置模式下尝试处理 token 问题
|
||||||
if request.state.use_config_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)
|
current_id = get_account_identifier(request.state.account)
|
||||||
if not hasattr(request.state, "tried_accounts"):
|
if not hasattr(request.state, "tried_accounts"):
|
||||||
request.state.tried_accounts = []
|
request.state.tried_accounts = []
|
||||||
@@ -81,6 +96,7 @@ def create_session(request: Request, max_attempts: int = 3) -> str | None:
|
|||||||
continue
|
continue
|
||||||
request.state.account = new_account
|
request.state.account = new_account
|
||||||
request.state.deepseek_token = new_account.get("token")
|
request.state.deepseek_token = new_account.get("token")
|
||||||
|
token_refreshed = False # 新账号重置刷新标记
|
||||||
else:
|
else:
|
||||||
attempts += 1
|
attempts += 1
|
||||||
continue
|
continue
|
||||||
|
|||||||
327
routes/admin.py
327
routes/admin.py
@@ -4,13 +4,15 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import httpx
|
import httpx
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Request, Depends
|
from fastapi import APIRouter, HTTPException, Request, Depends
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
|
||||||
from core.config import CONFIG, save_config, logger
|
from core.config import CONFIG, save_config, logger
|
||||||
from core.auth import account_queue, init_account_queue
|
from core.auth import account_queue, init_account_queue, get_queue_status, get_account_identifier
|
||||||
|
from core.deepseek import login_deepseek_via_account
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||||
security = HTTPBearer(auto_error=False)
|
security = HTTPBearer(auto_error=False)
|
||||||
@@ -176,6 +178,242 @@ async def delete_account(identifier: str, _: bool = Depends(verify_admin)):
|
|||||||
raise HTTPException(status_code=404, detail="账号不存在")
|
raise HTTPException(status_code=404, detail="账号不存在")
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# 账号队列状态(监控)
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
@router.get("/queue/status")
|
||||||
|
async def get_account_queue_status(_: bool = Depends(verify_admin)):
|
||||||
|
"""获取账号轮询队列状态"""
|
||||||
|
status = get_queue_status()
|
||||||
|
return JSONResponse(content=status)
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# 账号验证
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
async def validate_single_account(account: dict) -> dict:
|
||||||
|
"""验证单个账号的有效性"""
|
||||||
|
acc_id = get_account_identifier(account)
|
||||||
|
result = {
|
||||||
|
"account": acc_id,
|
||||||
|
"valid": False,
|
||||||
|
"has_token": bool(account.get("token", "").strip()),
|
||||||
|
"message": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 如果已有 token,尝试简单验证(这里简化处理)
|
||||||
|
if result["has_token"]:
|
||||||
|
result["valid"] = True
|
||||||
|
result["message"] = "已有有效 token"
|
||||||
|
else:
|
||||||
|
# 尝试登录
|
||||||
|
try:
|
||||||
|
login_deepseek_via_account(account)
|
||||||
|
result["valid"] = True
|
||||||
|
result["has_token"] = True
|
||||||
|
result["message"] = "登录成功"
|
||||||
|
except Exception as e:
|
||||||
|
result["valid"] = False
|
||||||
|
result["message"] = f"登录失败: {str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
result["message"] = f"验证出错: {str(e)}"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/accounts/validate")
|
||||||
|
async def validate_account(request: Request, _: bool = Depends(verify_admin)):
|
||||||
|
"""验证单个账号"""
|
||||||
|
data = await request.json()
|
||||||
|
identifier = data.get("identifier", "").strip()
|
||||||
|
|
||||||
|
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 validate_single_account(account)
|
||||||
|
|
||||||
|
# 如果验证成功且获取了新 token,保存配置
|
||||||
|
if result["valid"] and result["has_token"]:
|
||||||
|
save_config(CONFIG)
|
||||||
|
|
||||||
|
return JSONResponse(content=result)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/accounts/validate-all")
|
||||||
|
async def validate_all_accounts(_: bool = Depends(verify_admin)):
|
||||||
|
"""批量验证所有账号"""
|
||||||
|
accounts = CONFIG.get("accounts", [])
|
||||||
|
if not accounts:
|
||||||
|
return JSONResponse(content={
|
||||||
|
"total": 0,
|
||||||
|
"valid": 0,
|
||||||
|
"invalid": 0,
|
||||||
|
"results": [],
|
||||||
|
})
|
||||||
|
|
||||||
|
results = []
|
||||||
|
valid_count = 0
|
||||||
|
|
||||||
|
for acc in accounts:
|
||||||
|
result = await validate_single_account(acc)
|
||||||
|
results.append(result)
|
||||||
|
if result["valid"]:
|
||||||
|
valid_count += 1
|
||||||
|
# 添加小延迟避免请求过快
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# 保存可能更新的 token
|
||||||
|
save_config(CONFIG)
|
||||||
|
|
||||||
|
return JSONResponse(content={
|
||||||
|
"total": len(accounts),
|
||||||
|
"valid": valid_count,
|
||||||
|
"invalid": len(accounts) - valid_count,
|
||||||
|
"results": results,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
# 账号 API 测试(实际发送请求)
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
|
async def test_account_api(account: dict, model: str = "deepseek-chat") -> dict:
|
||||||
|
"""测试单个账号的 API 调用能力"""
|
||||||
|
from curl_cffi import requests as cffi_requests
|
||||||
|
from core.deepseek import DEEPSEEK_CREATE_SESSION_URL, DEEPSEEK_CHAT_COMPLETION_URL, BASE_HEADERS
|
||||||
|
from core.pow import get_pow_response_direct
|
||||||
|
|
||||||
|
acc_id = get_account_identifier(account)
|
||||||
|
result = {
|
||||||
|
"account": acc_id,
|
||||||
|
"success": False,
|
||||||
|
"response_time": 0,
|
||||||
|
"message": "",
|
||||||
|
"model": model,
|
||||||
|
}
|
||||||
|
|
||||||
|
import time
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 确保有 token
|
||||||
|
token = account.get("token", "").strip()
|
||||||
|
if not token:
|
||||||
|
try:
|
||||||
|
login_deepseek_via_account(account)
|
||||||
|
token = account.get("token", "")
|
||||||
|
except Exception as e:
|
||||||
|
result["message"] = f"登录失败: {str(e)}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
# 1. 创建会话
|
||||||
|
session_resp = cffi_requests.post(
|
||||||
|
DEEPSEEK_CREATE_SESSION_URL,
|
||||||
|
headers=headers,
|
||||||
|
json={"agent": "chat"},
|
||||||
|
impersonate="safari15_3",
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
if session_resp.status_code != 200:
|
||||||
|
result["message"] = f"创建会话失败: HTTP {session_resp.status_code}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
session_data = session_resp.json()
|
||||||
|
if session_data.get("code") != 0:
|
||||||
|
result["message"] = f"创建会话失败: {session_data.get('msg', 'Unknown error')}"
|
||||||
|
# token 可能过期,清除它
|
||||||
|
account["token"] = ""
|
||||||
|
return result
|
||||||
|
|
||||||
|
session_id = session_data["data"]["biz_data"]["id"]
|
||||||
|
result["success"] = True
|
||||||
|
result["message"] = "API 测试成功"
|
||||||
|
result["response_time"] = round((time.time() - start_time) * 1000) # ms
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 保存可能更新的 token
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 保存可能更新的 token
|
||||||
|
save_config(CONFIG)
|
||||||
|
|
||||||
|
return JSONResponse(content={
|
||||||
|
"total": len(accounts),
|
||||||
|
"success": success_count,
|
||||||
|
"failed": len(accounts) - success_count,
|
||||||
|
"results": results,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# 批量导入
|
# 批量导入
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -286,9 +524,12 @@ async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)):
|
|||||||
vercel_token = data.get("vercel_token", "")
|
vercel_token = data.get("vercel_token", "")
|
||||||
project_id = data.get("project_id", "")
|
project_id = data.get("project_id", "")
|
||||||
team_id = data.get("team_id", "") # 可选
|
team_id = data.get("team_id", "") # 可选
|
||||||
|
auto_validate = data.get("auto_validate", True) # 默认自动验证
|
||||||
|
save_vercel_credentials = data.get("save_credentials", True) # 是否保存 Vercel 凭证
|
||||||
|
|
||||||
# 支持使用预配置的 token
|
# 支持使用预配置的 token
|
||||||
if vercel_token == "__USE_PRECONFIG__" or not vercel_token:
|
use_preconfig = vercel_token == "__USE_PRECONFIG__" or not vercel_token
|
||||||
|
if use_preconfig:
|
||||||
vercel_token = VERCEL_TOKEN
|
vercel_token = VERCEL_TOKEN
|
||||||
if not project_id:
|
if not project_id:
|
||||||
project_id = VERCEL_PROJECT_ID
|
project_id = VERCEL_PROJECT_ID
|
||||||
@@ -298,6 +539,23 @@ async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)):
|
|||||||
if not vercel_token or not project_id:
|
if not vercel_token or not project_id:
|
||||||
raise HTTPException(status_code=400, detail="需要 Vercel Token 和 Project ID(可通过环境变量 VERCEL_TOKEN 和 VERCEL_PROJECT_ID 预配置)")
|
raise HTTPException(status_code=400, detail="需要 Vercel Token 和 Project ID(可通过环境变量 VERCEL_TOKEN 和 VERCEL_PROJECT_ID 预配置)")
|
||||||
|
|
||||||
|
# 自动验证所有无 token 的账号
|
||||||
|
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) # 避免请求过快
|
||||||
|
|
||||||
# 准备配置 JSON
|
# 准备配置 JSON
|
||||||
config_json = json.dumps(CONFIG, ensure_ascii=False, separators=(",", ":"))
|
config_json = json.dumps(CONFIG, ensure_ascii=False, separators=(",", ":"))
|
||||||
config_b64 = base64.b64encode(config_json.encode("utf-8")).decode("utf-8")
|
config_b64 = base64.b64encode(config_json.encode("utf-8")).decode("utf-8")
|
||||||
@@ -352,6 +610,51 @@ async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)):
|
|||||||
if create_resp.status_code not in [200, 201]:
|
if create_resp.status_code not in [200, 201]:
|
||||||
raise HTTPException(status_code=create_resp.status_code, detail=f"创建环境变量失败: {create_resp.text}")
|
raise HTTPException(status_code=create_resp.status_code, detail=f"创建环境变量失败: {create_resp.text}")
|
||||||
|
|
||||||
|
# 2.5 保存 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)
|
||||||
|
|
||||||
# 3. 触发重新部署 (获取最新的 git 信息并创建新部署)
|
# 3. 触发重新部署 (获取最新的 git 信息并创建新部署)
|
||||||
# 获取项目信息
|
# 获取项目信息
|
||||||
project_resp = await client.get(
|
project_resp = await client.get(
|
||||||
@@ -384,18 +687,30 @@ async def sync_to_vercel(request: Request, _: bool = Depends(verify_admin)):
|
|||||||
|
|
||||||
if deploy_resp.status_code in [200, 201]:
|
if deploy_resp.status_code in [200, 201]:
|
||||||
deploy_data = deploy_resp.json()
|
deploy_data = deploy_resp.json()
|
||||||
return JSONResponse(content={
|
result = {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "配置已同步,正在重新部署...",
|
"message": "配置已同步,正在重新部署...",
|
||||||
"deployment_url": deploy_data.get("url"),
|
"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)
|
||||||
|
|
||||||
# 如果无法自动部署,返回成功但提示手动部署
|
# 如果无法自动部署,返回成功但提示手动部署
|
||||||
return JSONResponse(content={
|
result = {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "配置已同步到 Vercel,请手动触发重新部署",
|
"message": "配置已同步到 Vercel,请手动触发重新部署",
|
||||||
"manual_deploy_required": True,
|
"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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
export default function AccountManager({ config, onRefresh, onMessage }) {
|
export default function AccountManager({ config, onRefresh, onMessage }) {
|
||||||
const [showAddKey, setShowAddKey] = useState(false)
|
const [showAddKey, setShowAddKey] = useState(false)
|
||||||
@@ -6,6 +6,30 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
|
|||||||
const [newKey, setNewKey] = useState('')
|
const [newKey, setNewKey] = useState('')
|
||||||
const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' })
|
const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' })
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [validating, setValidating] = useState({}) // 单个账号验证状态
|
||||||
|
const [validatingAll, setValidatingAll] = useState(false)
|
||||||
|
const [testing, setTesting] = useState({}) // 单个账号测试状态
|
||||||
|
const [testingAll, setTestingAll] = useState(false)
|
||||||
|
const [queueStatus, setQueueStatus] = useState(null)
|
||||||
|
|
||||||
|
// 获取队列状态
|
||||||
|
const fetchQueueStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/admin/queue/status')
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
setQueueStatus(data)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取队列状态失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchQueueStatus()
|
||||||
|
const interval = setInterval(fetchQueueStatus, 5000) // 每5秒刷新
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const addKey = async () => {
|
const addKey = async () => {
|
||||||
if (!newKey.trim()) return
|
if (!newKey.trim()) return
|
||||||
@@ -90,8 +114,115 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证单个账号
|
||||||
|
const validateAccount = async (identifier) => {
|
||||||
|
setValidating(prev => ({ ...prev, [identifier]: true }))
|
||||||
|
try {
|
||||||
|
const res = await fetch('/admin/accounts/validate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ identifier }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.valid) {
|
||||||
|
onMessage('success', `${identifier}: ${data.message}`)
|
||||||
|
} else {
|
||||||
|
onMessage('error', `${identifier}: ${data.message}`)
|
||||||
|
}
|
||||||
|
onRefresh()
|
||||||
|
} catch (e) {
|
||||||
|
onMessage('error', '验证失败: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setValidating(prev => ({ ...prev, [identifier]: false }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量验证所有账号
|
||||||
|
const validateAllAccounts = async () => {
|
||||||
|
if (!confirm('确定要验证所有账号?这可能需要一些时间。')) return
|
||||||
|
setValidatingAll(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/admin/accounts/validate-all', { method: 'POST' })
|
||||||
|
const data = await res.json()
|
||||||
|
onMessage('success', `验证完成: ${data.valid}/${data.total} 个账号有效`)
|
||||||
|
onRefresh()
|
||||||
|
} catch (e) {
|
||||||
|
onMessage('error', '批量验证失败: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setValidatingAll(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试单个账号 API
|
||||||
|
const testAccount = async (identifier) => {
|
||||||
|
setTesting(prev => ({ ...prev, [identifier]: true }))
|
||||||
|
try {
|
||||||
|
const res = await fetch('/admin/accounts/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ identifier }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.success) {
|
||||||
|
onMessage('success', `${identifier}: API 测试成功 (${data.response_time}ms)`)
|
||||||
|
} else {
|
||||||
|
onMessage('error', `${identifier}: ${data.message}`)
|
||||||
|
}
|
||||||
|
onRefresh()
|
||||||
|
} catch (e) {
|
||||||
|
onMessage('error', 'API 测试失败: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setTesting(prev => ({ ...prev, [identifier]: false }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量测试所有账号 API
|
||||||
|
const testAllAccounts = async () => {
|
||||||
|
if (!confirm('确定要测试所有账号的 API?这可能需要较长时间。')) return
|
||||||
|
setTestingAll(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/admin/accounts/test-all', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({}),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
onMessage('success', `API 测试完成: ${data.success}/${data.total} 个账号可用`)
|
||||||
|
onRefresh()
|
||||||
|
} catch (e) {
|
||||||
|
onMessage('error', '批量 API 测试失败: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
setTestingAll(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
|
{/* 队列状态监控 */}
|
||||||
|
{queueStatus && (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<span className="card-title">📊 轮询队列状态</span>
|
||||||
|
<button className="btn btn-secondary" onClick={fetchQueueStatus}>刷新</button>
|
||||||
|
</div>
|
||||||
|
<div className="queue-status">
|
||||||
|
<div className="stat-row">
|
||||||
|
<span className="stat-label">可用账号:</span>
|
||||||
|
<span className="stat-value stat-success">{queueStatus.available}</span>
|
||||||
|
<span className="stat-label" style={{ marginLeft: '20px' }}>使用中:</span>
|
||||||
|
<span className="stat-value stat-warning">{queueStatus.in_use}</span>
|
||||||
|
<span className="stat-label" style={{ marginLeft: '20px' }}>总计:</span>
|
||||||
|
<span className="stat-value">{queueStatus.total}</span>
|
||||||
|
</div>
|
||||||
|
{queueStatus.in_use > 0 && (
|
||||||
|
<div className="stat-detail">
|
||||||
|
正在使用: {queueStatus.in_use_accounts.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* API Keys */}
|
{/* API Keys */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
@@ -117,22 +248,57 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
|
|||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<span className="card-title">👤 DeepSeek 账号</span>
|
<span className="card-title">👤 DeepSeek 账号</span>
|
||||||
<button className="btn btn-primary" onClick={() => setShowAddAccount(true)}>+ 添加</button>
|
<div className="btn-group-inline">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={testAllAccounts}
|
||||||
|
disabled={testingAll || validatingAll || !config.accounts?.length}
|
||||||
|
>
|
||||||
|
{testingAll ? <span className="loading"></span> : '🧪 批量测试'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary btn-sm"
|
||||||
|
onClick={validateAllAccounts}
|
||||||
|
disabled={validatingAll || testingAll || !config.accounts?.length}
|
||||||
|
>
|
||||||
|
{validatingAll ? <span className="loading"></span> : '✅ 批量验证'}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={() => setShowAddAccount(true)}>+ 添加</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.accounts?.length > 0 ? (
|
{config.accounts?.length > 0 ? (
|
||||||
<div className="list">
|
<div className="list">
|
||||||
{config.accounts.map((acc, i) => (
|
{config.accounts.map((acc, i) => {
|
||||||
<div key={i} className="list-item">
|
const id = acc.email || acc.mobile
|
||||||
<div className="list-item-info">
|
return (
|
||||||
<span className="list-item-text">{acc.email || acc.mobile}</span>
|
<div key={i} className="list-item">
|
||||||
<span className={`badge ${acc.has_token ? 'badge-success' : 'badge-warning'}`}>
|
<div className="list-item-info">
|
||||||
{acc.has_token ? '已登录' : '未登录'}
|
<span className="list-item-text">{id}</span>
|
||||||
</span>
|
<span className={`badge ${acc.has_token ? 'badge-success' : 'badge-warning'}`}>
|
||||||
|
{acc.has_token ? '已登录' : '未登录'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="btn-group-inline">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={() => testAccount(id)}
|
||||||
|
disabled={testing[id]}
|
||||||
|
>
|
||||||
|
{testing[id] ? <span className="loading"></span> : '测试'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary btn-sm"
|
||||||
|
onClick={() => validateAccount(id)}
|
||||||
|
disabled={validating[id]}
|
||||||
|
>
|
||||||
|
{validating[id] ? <span className="loading"></span> : '验证'}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-danger btn-sm" onClick={() => deleteAccount(id)}>删除</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-danger" onClick={() => deleteAccount(acc.email || acc.mobile)}>删除</button>
|
)
|
||||||
</div>
|
})}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="empty-state">暂无账号</div>
|
<div className="empty-state">暂无账号</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
const MODELS = [
|
const MODELS = [
|
||||||
{ id: 'deepseek-chat', name: 'DeepSeek V3 (Chat)' },
|
{ id: 'deepseek-chat', name: 'deepseek-chat' },
|
||||||
{ id: 'deepseek-reasoner', name: 'DeepSeek R1 (Reasoner)' },
|
{ id: 'deepseek-reasoner', name: 'deepseek-reasoner' },
|
||||||
{ id: 'deepseek-chat-search', name: 'DeepSeek V3 + 搜索' },
|
{ id: 'deepseek-chat-search', name: 'deepseek-chat-search' },
|
||||||
{ id: 'deepseek-reasoner-search', name: 'DeepSeek R1 + 搜索' },
|
{ id: 'deepseek-reasoner-search', name: 'deepseek-reasoner-search' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function ApiTester({ config, onMessage }) {
|
export default function ApiTester({ config, onMessage }) {
|
||||||
|
|||||||
@@ -301,7 +301,9 @@ textarea.form-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
@@ -397,25 +399,72 @@ textarea.form-input {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Queue Status */
|
||||||
|
.queue-status {
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-success {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-warning {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-detail {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Group Inline */
|
||||||
|
.btn-group-inline {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.app {
|
.app {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group {
|
.btn-group {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user