mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
feat: Add token preview and batch progress to admin account management, and expand API tests for admin endpoints.
This commit is contained in:
@@ -62,11 +62,13 @@ async def get_config(_: bool = Depends(verify_admin)):
|
||||
"claude_model_mapping": CONFIG.get("claude_model_mapping", {}),
|
||||
}
|
||||
for acc in CONFIG.get("accounts", []):
|
||||
token = acc.get("token", "")
|
||||
safe_acc = {
|
||||
"email": acc.get("email", ""),
|
||||
"mobile": acc.get("mobile", ""),
|
||||
"has_password": bool(acc.get("password")),
|
||||
"has_token": bool(acc.get("token")),
|
||||
"has_token": bool(token),
|
||||
"token_preview": token[:12] + "..." if len(token) > 12 else "" if token else "",
|
||||
}
|
||||
safe_config["accounts"].append(safe_acc)
|
||||
return JSONResponse(content=safe_config)
|
||||
@@ -288,14 +290,9 @@ async def validate_all_accounts(_: bool = Depends(verify_admin)):
|
||||
# 账号 API 测试(实际发送请求)
|
||||
# ----------------------------------------------------------------------
|
||||
async def test_account_api(account: dict, model: str = "deepseek-chat", message: str = "") -> dict:
|
||||
"""测试单个账号的 API 调用能力
|
||||
|
||||
如果提供了 message,会发送实际请求并返回 AI 回复;
|
||||
否则只测试创建会话(快速测试)。
|
||||
"""
|
||||
"""测试单个账号的 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
|
||||
from core.deepseek import DEEPSEEK_CREATE_SESSION_URL, BASE_HEADERS
|
||||
|
||||
acc_id = get_account_identifier(account)
|
||||
result = {
|
||||
@@ -304,7 +301,6 @@ async def test_account_api(account: dict, model: str = "deepseek-chat", message:
|
||||
"response_time": 0,
|
||||
"message": "",
|
||||
"model": model,
|
||||
"reply": "", # AI 回复内容
|
||||
}
|
||||
|
||||
import time
|
||||
@@ -323,7 +319,7 @@ async def test_account_api(account: dict, model: str = "deepseek-chat", message:
|
||||
|
||||
headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"}
|
||||
|
||||
# 1. 创建会话
|
||||
# 创建会话来测试 API 可用性
|
||||
session_resp = cffi_requests.post(
|
||||
DEEPSEEK_CREATE_SESSION_URL,
|
||||
headers=headers,
|
||||
@@ -343,70 +339,8 @@ async def test_account_api(account: dict, model: str = "deepseek-chat", message:
|
||||
account["token"] = ""
|
||||
return result
|
||||
|
||||
session_id = session_data["data"]["biz_data"]["id"]
|
||||
|
||||
# 如果没有消息,只测试会话创建
|
||||
if not message:
|
||||
result["success"] = True
|
||||
result["message"] = "API 测试成功"
|
||||
result["response_time"] = round((time.time() - start_time) * 1000)
|
||||
return result
|
||||
|
||||
# 2. 发送实际请求
|
||||
from core.pow import get_pow_response_direct
|
||||
pow_resp = get_pow_response_direct(headers)
|
||||
if not pow_resp:
|
||||
result["message"] = "获取 PoW 失败"
|
||||
return result
|
||||
|
||||
chat_headers = {**headers, "x-ds-pow-response": pow_resp}
|
||||
thinking_enabled = "reasoner" in model.lower()
|
||||
search_enabled = "search" in model.lower()
|
||||
|
||||
payload = {
|
||||
"chat_session_id": session_id,
|
||||
"parent_message_id": None,
|
||||
"prompt": message,
|
||||
"ref_file_ids": [],
|
||||
"thinking_enabled": thinking_enabled,
|
||||
"search_enabled": search_enabled,
|
||||
}
|
||||
|
||||
chat_resp = cffi_requests.post(
|
||||
DEEPSEEK_CHAT_COMPLETION_URL,
|
||||
headers=chat_headers,
|
||||
json=payload,
|
||||
impersonate="safari15_3",
|
||||
timeout=60,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
if chat_resp.status_code != 200:
|
||||
result["message"] = f"对话请求失败: HTTP {chat_resp.status_code}"
|
||||
return result
|
||||
|
||||
# 解析流式响应获取回复
|
||||
reply_text = ""
|
||||
for line in chat_resp.iter_lines():
|
||||
try:
|
||||
line_str = line.decode("utf-8")
|
||||
if line_str.startswith("data:"):
|
||||
data_str = line_str[5:].strip()
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
import json
|
||||
chunk = json.loads(data_str)
|
||||
choices = chunk.get("choices", [])
|
||||
if choices:
|
||||
delta = choices[0].get("delta", {})
|
||||
if delta.get("type") == "text":
|
||||
reply_text += delta.get("content", "")
|
||||
except:
|
||||
pass
|
||||
|
||||
result["success"] = True
|
||||
result["message"] = "API 测试成功"
|
||||
result["reply"] = reply_text[:500] # 限制长度
|
||||
result["response_time"] = round((time.time() - start_time) * 1000)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -558,6 +558,80 @@ class TestRunner:
|
||||
"details": {"input_length": len(long_text)}
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# 管理 API 测试
|
||||
# =====================================================================
|
||||
|
||||
def test_admin_config(self) -> dict:
|
||||
"""测试管理配置 API"""
|
||||
resp = requests.get(
|
||||
f"{self.endpoint}/admin/config",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
return {"success": False, "message": f"状态码: {resp.status_code}"}
|
||||
|
||||
data = resp.json()
|
||||
|
||||
# 验证返回结构
|
||||
if "accounts" not in data:
|
||||
return {"success": False, "message": "响应缺少 accounts 字段"}
|
||||
|
||||
# 验证 token_preview 字段存在
|
||||
accounts = data.get("accounts", [])
|
||||
if accounts:
|
||||
first_acc = accounts[0]
|
||||
if "token_preview" not in first_acc:
|
||||
return {"success": False, "message": "响应缺少 token_preview 字段"}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"获取配置成功,{len(accounts)} 个账号",
|
||||
"details": {"account_count": len(accounts)}
|
||||
}
|
||||
|
||||
def test_admin_account_test(self) -> dict:
|
||||
"""测试单账号 API 测试端点"""
|
||||
# 先获取配置以获取账号
|
||||
config_resp = requests.get(f"{self.endpoint}/admin/config", timeout=10)
|
||||
if config_resp.status_code != 200:
|
||||
return {"success": False, "message": "获取配置失败"}
|
||||
|
||||
accounts = config_resp.json().get("accounts", [])
|
||||
if not accounts:
|
||||
return {"success": False, "message": "没有可测试的账号"}
|
||||
|
||||
# 测试第一个账号
|
||||
first_acc = accounts[0]
|
||||
identifier = first_acc.get("email") or first_acc.get("mobile")
|
||||
|
||||
resp = requests.post(
|
||||
f"{self.endpoint}/admin/accounts/test",
|
||||
json={"identifier": identifier},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
return {"success": False, "message": f"状态码: {resp.status_code}"}
|
||||
|
||||
data = resp.json()
|
||||
|
||||
# 验证返回结构
|
||||
required_fields = ["account", "success", "response_time", "message"]
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return {"success": False, "message": f"响应缺少 {field} 字段"}
|
||||
|
||||
if not data["success"]:
|
||||
return {"success": False, "message": f"账号测试失败: {data['message']}"}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"账号 {identifier} 测试成功 ({data['response_time']}ms)",
|
||||
"details": {"response_time": data["response_time"]}
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# 运行测试
|
||||
# =====================================================================
|
||||
@@ -599,6 +673,10 @@ class TestRunner:
|
||||
self.run_test("多轮对话", self.test_multi_turn_conversation)
|
||||
self.run_test("长输入处理", self.test_long_input)
|
||||
|
||||
# 管理 API 测试
|
||||
self.run_test("管理配置 API", self.test_admin_config)
|
||||
self.run_test("账号测试 API", self.test_admin_account_test)
|
||||
|
||||
# 输出测试报告
|
||||
self.print_report()
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.3.4
|
||||
1.6.11
|
||||
@@ -10,6 +10,7 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
|
||||
const [validatingAll, setValidatingAll] = useState(false)
|
||||
const [testing, setTesting] = useState({}) // 单个账号测试状态
|
||||
const [testingAll, setTestingAll] = useState(false)
|
||||
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] })
|
||||
const [queueStatus, setQueueStatus] = useState(null)
|
||||
|
||||
// 获取队列状态
|
||||
@@ -137,20 +138,41 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
|
||||
}
|
||||
}
|
||||
|
||||
// 批量验证所有账号
|
||||
// 批量验证所有账号(带进度)
|
||||
const validateAllAccounts = async () => {
|
||||
if (!confirm('确定要验证所有账号?这可能需要一些时间。')) return
|
||||
if (!confirm('确定要验证所有账号?')) return
|
||||
const accounts = config.accounts || []
|
||||
if (accounts.length === 0) 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)
|
||||
setBatchProgress({ current: 0, total: accounts.length, results: [] })
|
||||
|
||||
let validCount = 0
|
||||
const results = []
|
||||
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
const acc = accounts[i]
|
||||
const id = acc.email || acc.mobile
|
||||
|
||||
try {
|
||||
const res = await fetch('/admin/accounts/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identifier: id }),
|
||||
})
|
||||
const data = await res.json()
|
||||
results.push({ id, success: data.valid, message: data.message })
|
||||
if (data.valid) validCount++
|
||||
} catch (e) {
|
||||
results.push({ id, success: false, message: e.message })
|
||||
}
|
||||
|
||||
setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] })
|
||||
}
|
||||
|
||||
onMessage('success', `验证完成: ${validCount}/${accounts.length} 个账号有效`)
|
||||
onRefresh()
|
||||
setValidatingAll(false)
|
||||
}
|
||||
|
||||
// 测试单个账号 API
|
||||
@@ -176,24 +198,41 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
|
||||
}
|
||||
}
|
||||
|
||||
// 批量测试所有账号 API
|
||||
// 批量测试所有账号 API(带进度)
|
||||
const testAllAccounts = async () => {
|
||||
if (!confirm('确定要测试所有账号的 API?这可能需要较长时间。')) return
|
||||
if (!confirm('确定要测试所有账号的 API?')) return
|
||||
const accounts = config.accounts || []
|
||||
if (accounts.length === 0) 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)
|
||||
setBatchProgress({ current: 0, total: accounts.length, results: [] })
|
||||
|
||||
let successCount = 0
|
||||
const results = []
|
||||
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
const acc = accounts[i]
|
||||
const id = acc.email || acc.mobile
|
||||
|
||||
try {
|
||||
const res = await fetch('/admin/accounts/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identifier: id }),
|
||||
})
|
||||
const data = await res.json()
|
||||
results.push({ id, success: data.success, message: data.message, time: data.response_time })
|
||||
if (data.success) successCount++
|
||||
} catch (e) {
|
||||
results.push({ id, success: false, message: e.message })
|
||||
}
|
||||
|
||||
setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] })
|
||||
}
|
||||
|
||||
onMessage('success', `API 测试完成: ${successCount}/${accounts.length} 个账号可用`)
|
||||
onRefresh()
|
||||
setTestingAll(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -267,6 +306,31 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 批量操作进度条 */}
|
||||
{(testingAll || validatingAll) && batchProgress.total > 0 && (
|
||||
<div className="batch-progress">
|
||||
<div className="progress-header">
|
||||
<span>{testingAll ? '🧪 批量测试中...' : '✅ 批量验证中...'}</span>
|
||||
<span>{batchProgress.current}/{batchProgress.total}</span>
|
||||
</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
{batchProgress.results.length > 0 && (
|
||||
<div className="progress-results">
|
||||
{batchProgress.results.map((r, i) => (
|
||||
<div key={i} className={`progress-result ${r.success ? 'success' : 'failed'}`}>
|
||||
{r.success ? '✓' : '✗'} {r.id} {r.time ? `(${r.time}ms)` : ''}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config.accounts?.length > 0 ? (
|
||||
<div className="list">
|
||||
{config.accounts.map((acc, i) => {
|
||||
@@ -278,6 +342,11 @@ export default function AccountManager({ config, onRefresh, onMessage }) {
|
||||
<span className={`badge ${acc.has_token ? 'badge-success' : 'badge-warning'}`}>
|
||||
{acc.has_token ? '已登录' : '未登录'}
|
||||
</span>
|
||||
{acc.token_preview && (
|
||||
<span className="token-preview" title="Token 预览">
|
||||
🔑 {acc.token_preview}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="btn-group-inline">
|
||||
<button
|
||||
|
||||
@@ -446,6 +446,65 @@ textarea.form-input {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Token Preview */
|
||||
.token-preview {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Batch Progress */
|
||||
.batch-progress {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary), var(--primary-hover));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-results {
|
||||
margin-top: 0.75rem;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.progress-result {
|
||||
padding: 0.25rem 0;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
}
|
||||
|
||||
.progress-result.success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.progress-result.failed {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.app {
|
||||
padding: 1rem;
|
||||
|
||||
Reference in New Issue
Block a user