mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-02 23:45:27 +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
|
||||
|
||||
# ===== 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
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
|
||||
{
|
||||
"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
|
||||
## ⚠️ 免责声明
|
||||
|
||||
[](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 -*-
|
||||
"""账号认证与管理模块"""
|
||||
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"] = ""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
327
routes/admin.py
327
routes/admin.py
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user