feat: Implement DeepSeek account validation, testing, and queue status monitoring with corresponding admin API endpoints.

This commit is contained in:
CJACK
2026-02-01 03:10:26 +08:00
parent bc260899c1
commit 5ac472626e
9 changed files with 807 additions and 283 deletions

View File

@@ -33,8 +33,9 @@ LOG_LEVEL=INFO
# DS2API_WASM_PATH=sha3_wasm_bg.7b9ca65ddd.wasm
# ===== Admin 管理界面 =====
# Admin API 密钥(留空则开发模式,无需认证
# DS2API_ADMIN_KEY=your-admin-secret-key
# Admin API 密钥(Vercel 部署必填!保护 WebUI 管理界面
# 首次同步时会自动保存到 Vercel 环境变量
DS2API_ADMIN_KEY=your-admin-secret-key
# ===== Vercel 集成(可选,用于一键同步部署)=====
# Vercel API Token从 https://vercel.com/account/tokens 获取)

310
README.MD
View File

@@ -1,273 +1,149 @@
# DeepSeek2API
# DS2API
[![](https://img.shields.io/github/license/iidamie/deepseek2api.svg)](LICENSE)
![](https://img.shields.io/github/stars/iidamie/deepseek2api.svg)
![](https://img.shields.io/github/forks/iidamie/deepseek2api.svg)
[![License](https://img.shields.io/github/license/CJackHwang/ds2api.svg)](LICENSE)
![Stars](https://img.shields.io/github/stars/CJackHwang/ds2api.svg)
![Forks](https://img.shields.io/github/forks/CJackHwang/ds2api.svg)
支持高速流式输出、支持多轮对话、支持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 部署(推荐)
**仅限自用,禁止对外提供服务或商用,避免对官方造成服务压力,否则风险自担!**
[![Deploy with Vercel](https://vercel.com/button)](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超时错误
### 环境变量
[![Deploy with Vercel](https://vercel.com/button)](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
{
"keys": [
"your-api-key-1",
"your-api-key-2"
],
"keys": ["your-api-key"],
"accounts": [
{
"email": "example1@example.com",
"password": "password1",
"token": ""
},
{
"email": "example2@example.com",
"password": "password2",
"token": ""
},
{
"mobile": "12345678901",
"password": "password3",
"email": "your@email.com",
"password": "your-password",
"token": ""
}
]
}
```
配置完成后,返回 Vercel 控制台,点击 "Redeploy" 重新部署即可。
> [!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或其他兼容的客户端接入接口。
## 📡 API 接口
### 模型列表
获取模型列表接口
**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"
}
```
GET /v1/models
```
### 对话补全
对话补全接口与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
{
// model名称
// 默认deepseek-chat 或 deepseek-v3
// 深度思考deepseek-reasoner 或 deepseek-r1
// 联网搜索deepseek-chat-search 或 deepseek-v3-search
// 联网搜索 + 深度思考deepseek-reasoner-search 或 deepseek-r1-search
"model": "deepseek-chat",
// 多轮对话基于消息合并实现某些场景可能导致能力下降且受单轮最大token数限制
"messages": [
{
"role": "user",
"content": "你是谁?"
}
],
// 如果使用流式响应请设置为true默认false
"stream": false
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "你好"}],
"stream": true
}
```
响应数据:
```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反向代理deepseek2api请添加以下配置项优化流的输出效果优化体验感。
## 🔧 Nginx 反代配置
```nginx
# 关闭代理缓冲。当设置为off时Nginx会立即将客户端请求发送到后端服务器并立即将从后端服务器接收到的响应发送回客户端。
proxy_buffering off;
# 启用分块传输编码。分块传输编码允许服务器为动态生成的内容分块发送数据,而不需要预先知道内容的大小。
chunked_transfer_encoding on;
# 开启TCP_NOPUSH这告诉Nginx在数据包发送到客户端之前尽可能地发送数据。这通常在sendfile使用时配合使用可以提高网络效率。
tcp_nopush on;
# 开启TCP_NODELAY这告诉Nginx不延迟发送数据立即发送小数据包。在某些情况下这可以减少网络的延迟。
tcp_nodelay on;
# 设置保持连接的超时时间这里设置为120秒。如果在这段时间内客户端和服务器之间没有进一步的通信连接将被关闭。
keepalive_timeout 120;
```
### Token统计
符合 OPENAI 接口规范
示例:
```json
"usage": {
"completion_tokens": 37,
"prompt_tokens": 1,
"total_tokens": 38
location / {
proxy_pass http://localhost:5001;
proxy_buffering off;
chunked_transfer_encoding on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 120;
}
```
## Star History
## ⚠️ 免责声明
[![Star History Chart](https://api.star-history.com/svg?repos=iidamie/deepseek2api&type=Date)](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
[![Star History Chart](https://api.star-history.com/svg?repos=CJackHwang/ds2api&type=Date)](https://star-history.com/#CJackHwang/ds2api&Date)

View File

@@ -1,21 +1,29 @@
# -*- coding: utf-8 -*-
"""账号认证与管理模块"""
import random
"""账号认证与管理模块 - 轮询(Round-Robin)策略"""
import threading
from fastapi import HTTPException, Request
from .config import CONFIG, logger
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
def init_account_queue():
"""初始化时从配置加载账号"""
global account_queue
account_queue = CONFIG.get("accounts", [])[:] # 深拷贝
random.shuffle(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():
@@ -37,43 +45,71 @@ def get_account_identifier(account: dict) -> str:
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):
"""选择策略:
1. 优先选择已有 token 的账号(避免登录)
2. 遍历队列,找到第一个未被 exclude_ids 包含的账号
3. 从队列中移除该账号
4. 返回该账号(由后续逻辑保证最终会重新入队)
"""轮询选择策略:
1. 使用线程锁保证并发安全
2. 优先选择队首的有 token 账号
3. 从队列头部取出账号FIFO
4. 请求完成后调用 release_account 将账号放回队尾
"""
if exclude_ids is None:
exclude_ids = []
# 第一轮:优先选择已有 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
logger.info(f"[choose_new_account] 选择已有token的账号: {acc_id}")
return account_queue.pop(i)
with _queue_lock:
# 第一轮:优先选择已有 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:
logger.info(f"[choose_new_account] 选择需登录的账号: {acc_id}")
return account_queue.pop(i)
# 第二轮:选择任意账号(需要登录)
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("[choose_new_account] 没有可用账号或所有账号都在使用中")
return None
logger.warning(f"[choose_new_account] 没有可用账号 | 队列: {len(account_queue)}, 使用中: {len(in_use_accounts)}")
return None
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):
"""Claude认证沿用现有的OpenAI接口认证逻辑"""
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"] = ""

View File

@@ -17,12 +17,12 @@ DEEPSEEK_COMPLETION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/completion"
BASE_HEADERS = {
"Host": "chat.deepseek.com",
"User-Agent": "DeepSeek/1.0.13 Android/35",
"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.3.0-auto-resume",
"x-client-version": "1.6.11",
"x-client-locale": "zh_CN",
"accept-charset": "UTF-8",
}
@@ -79,6 +79,24 @@ def login_deepseek_via_account(account: dict) -> str:
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

View File

@@ -9,6 +9,7 @@ from .auth import (
choose_new_account,
get_account_identifier,
release_account,
refresh_account_token,
)
from .deepseek import (
DEEPSEEK_CREATE_SESSION_URL,
@@ -30,6 +31,8 @@ def create_session(request: Request, max_attempts: int = 3) -> str | None:
会话 ID如果失败返回 None
"""
attempts = 0
token_refreshed = False # 标记是否已尝试刷新 token
while attempts < max_attempts:
headers = get_auth_headers(request)
try:
@@ -56,13 +59,25 @@ def create_session(request: Request, max_attempts: int = 3) -> str | None:
return session_id
else:
code = data.get("code")
msg = data.get("msg", "")
logger.warning(
f"[create_session] 创建会话失败, code={code}, msg={data.get('msg')}"
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 = []
@@ -81,6 +96,7 @@ def create_session(request: Request, max_attempts: int = 3) -> str | None:
continue
request.state.account = new_account
request.state.deepseek_token = new_account.get("token")
token_refreshed = False # 新账号重置刷新标记
else:
attempts += 1
continue

View File

@@ -4,13 +4,15 @@ import base64
import json
import os
import httpx
import asyncio
from fastapi import APIRouter, HTTPException, Request, Depends
from fastapi.responses import JSONResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
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"])
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="账号不存在")
# ----------------------------------------------------------------------
# 账号队列状态(监控)
# ----------------------------------------------------------------------
@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", "")
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) # 是否保存 Vercel 凭证
# 支持使用预配置的 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
if not 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:
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
config_json = json.dumps(CONFIG, ensure_ascii=False, separators=(",", ":"))
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]:
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 信息并创建新部署)
# 获取项目信息
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]:
deploy_data = deploy_resp.json()
return JSONResponse(content={
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)
# 如果无法自动部署,返回成功但提示手动部署
return JSONResponse(content={
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

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
export default function AccountManager({ config, onRefresh, onMessage }) {
const [showAddKey, setShowAddKey] = useState(false)
@@ -6,6 +6,30 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
const [newKey, setNewKey] = useState('')
const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' })
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 () => {
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 (
<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 */}
<div className="card">
<div className="card-header">
@@ -117,22 +248,57 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
<div className="card">
<div className="card-header">
<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>
{config.accounts?.length > 0 ? (
<div className="list">
{config.accounts.map((acc, i) => (
<div key={i} className="list-item">
<div className="list-item-info">
<span className="list-item-text">{acc.email || acc.mobile}</span>
<span className={`badge ${acc.has_token ? 'badge-success' : 'badge-warning'}`}>
{acc.has_token ? '已登录' : '未登录'}
</span>
{config.accounts.map((acc, i) => {
const id = acc.email || acc.mobile
return (
<div key={i} className="list-item">
<div className="list-item-info">
<span className="list-item-text">{id}</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>
<button className="btn btn-danger" onClick={() => deleteAccount(acc.email || acc.mobile)}>删除</button>
</div>
))}
)
})}
</div>
) : (
<div className="empty-state">暂无账号</div>

View File

@@ -1,10 +1,10 @@
import { useState } from 'react'
const MODELS = [
{ id: 'deepseek-chat', name: 'DeepSeek V3 (Chat)' },
{ id: 'deepseek-reasoner', name: 'DeepSeek R1 (Reasoner)' },
{ id: 'deepseek-chat-search', name: 'DeepSeek V3 + 搜索' },
{ id: 'deepseek-reasoner-search', name: 'DeepSeek R1 + 搜索' },
{ id: 'deepseek-chat', name: 'deepseek-chat' },
{ id: 'deepseek-reasoner', name: 'deepseek-reasoner' },
{ id: 'deepseek-chat-search', name: 'deepseek-chat-search' },
{ id: 'deepseek-reasoner-search', name: 'deepseek-reasoner-search' },
]
export default function ApiTester({ config, onMessage }) {

View File

@@ -301,7 +301,9 @@ textarea.form-input {
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
.modal-overlay {
@@ -397,25 +399,72 @@ textarea.form-input {
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) {
.app {
padding: 1rem;
}
.tabs {
flex-direction: column;
}
.tab {
text-align: center;
}
.btn-group {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
}
}