mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
feat: Localize UI strings to Chinese and add new API documentation.
This commit is contained in:
369
API.md
Normal file
369
API.md
Normal file
@@ -0,0 +1,369 @@
|
||||
# API 文档
|
||||
|
||||
本文档详细介绍 DS2API 提供的所有 API 端点。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **Base URL**: `https://your-domain.com` 或 `http://localhost:5001`
|
||||
- **认证方式**: Bearer Token
|
||||
- **响应格式**: JSON
|
||||
|
||||
## OpenAI 兼容接口
|
||||
|
||||
### 获取模型列表
|
||||
|
||||
```http
|
||||
GET /v1/models
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{"id": "deepseek-chat", "object": "model", "owned_by": "deepseek"},
|
||||
{"id": "deepseek-reasoner", "object": "model", "owned_by": "deepseek"},
|
||||
{"id": "deepseek-chat-search", "object": "model", "owned_by": "deepseek"},
|
||||
{"id": "deepseek-reasoner-search", "object": "model", "owned_by": "deepseek"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 对话补全
|
||||
|
||||
```http
|
||||
POST /v1/chat/completions
|
||||
Authorization: Bearer your-api-key
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|-----|------|------|------|
|
||||
| `model` | string | ✅ | 模型名称 |
|
||||
| `messages` | array | ✅ | 对话消息列表 |
|
||||
| `stream` | boolean | ❌ | 是否流式输出,默认 `false` |
|
||||
| `temperature` | number | ❌ | 温度参数,0-2 |
|
||||
| `max_tokens` | number | ❌ | 最大输出 token 数 |
|
||||
|
||||
**支持的模型**:
|
||||
|
||||
| 模型 | thinking_enabled | search_enabled | 说明 |
|
||||
|-----|-----------------|----------------|------|
|
||||
| `deepseek-chat` | ❌ | ❌ | 标准对话 |
|
||||
| `deepseek-reasoner` | ✅ | ❌ | 深度思考(R1 推理) |
|
||||
| `deepseek-chat-search` | ❌ | ✅ | 联网搜索 |
|
||||
| `deepseek-reasoner-search` | ✅ | ✅ | 深度思考 + 联网搜索 |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-reasoner-search",
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个有帮助的助手。"},
|
||||
{"role": "user", "content": "今天有什么重要新闻?"}
|
||||
],
|
||||
"stream": true
|
||||
}
|
||||
```
|
||||
|
||||
**流式响应格式** (`stream: true`):
|
||||
|
||||
```
|
||||
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}
|
||||
|
||||
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"reasoning_content":"让我思考一下..."},"index":0}]}
|
||||
|
||||
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"根据搜索结果..."},"index":0}]}
|
||||
|
||||
data: {"id":"...","object":"chat.completion.chunk","choices":[{"index":0,"finish_reason":"stop"}]}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
**非流式响应格式** (`stream: false`):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-xxx",
|
||||
"object": "chat.completion",
|
||||
"created": 1699000000,
|
||||
"model": "deepseek-reasoner",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "回复内容",
|
||||
"reasoning_content": "思考过程(仅 reasoner 模型)"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 50,
|
||||
"total_tokens": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 管理接口
|
||||
|
||||
所有管理接口需要在请求头中携带 `Authorization: Bearer <DS2API_ADMIN_KEY>`。
|
||||
|
||||
### 登录
|
||||
|
||||
```http
|
||||
POST /admin/login
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "your-admin-key"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"token": "jwt-token",
|
||||
"expires_in": 86400
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取配置
|
||||
|
||||
```http
|
||||
GET /admin/config
|
||||
Authorization: Bearer <jwt-token>
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"keys": ["api-key-1", "api-key-2"],
|
||||
"accounts": [
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "***",
|
||||
"token": "session-token"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 添加账号
|
||||
|
||||
```http
|
||||
POST /admin/accounts
|
||||
Authorization: Bearer <jwt-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 批量导入账号
|
||||
|
||||
```http
|
||||
POST /admin/accounts/batch
|
||||
Authorization: Bearer <jwt-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"accounts": [
|
||||
{"email": "user1@example.com", "password": "pass1"},
|
||||
{"email": "user2@example.com", "password": "pass2"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 测试账号
|
||||
|
||||
```http
|
||||
POST /admin/accounts/test
|
||||
Authorization: Bearer <jwt-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 批量测试所有账号
|
||||
|
||||
```http
|
||||
POST /admin/accounts/test-all
|
||||
Authorization: Bearer <jwt-token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取队列状态
|
||||
|
||||
```http
|
||||
GET /admin/queue/status
|
||||
Authorization: Bearer <jwt-token>
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"total_accounts": 5,
|
||||
"healthy_accounts": 4,
|
||||
"queue_size": 10,
|
||||
"accounts": [
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"status": "healthy",
|
||||
"last_used": "2026-02-01T06:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 同步到 Vercel
|
||||
|
||||
```http
|
||||
POST /admin/vercel/sync
|
||||
Authorization: Bearer <jwt-token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"vercel_token": "your-vercel-token",
|
||||
"project_id": "your-project-id"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
所有错误响应遵循以下格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "错误描述",
|
||||
"type": "error_type",
|
||||
"code": "error_code"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**常见错误码**:
|
||||
|
||||
| HTTP 状态码 | 错误类型 | 说明 |
|
||||
|------------|---------|------|
|
||||
| 400 | `invalid_request_error` | 请求参数错误 |
|
||||
| 401 | `authentication_error` | API Key 无效或未提供 |
|
||||
| 403 | `permission_denied` | 权限不足 |
|
||||
| 429 | `rate_limit_error` | 请求过于频繁 |
|
||||
| 500 | `internal_error` | 服务器内部错误 |
|
||||
| 503 | `service_unavailable` | 无可用账号 |
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import openai
|
||||
|
||||
client = openai.OpenAI(
|
||||
api_key="your-api-key",
|
||||
base_url="https://your-domain.com/v1"
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="deepseek-reasoner",
|
||||
messages=[{"role": "user", "content": "你好"}],
|
||||
stream=True
|
||||
)
|
||||
|
||||
for chunk in response:
|
||||
if chunk.choices[0].delta.content:
|
||||
print(chunk.choices[0].delta.content, end="")
|
||||
```
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
curl https://your-domain.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-api-key" \
|
||||
-d '{
|
||||
"model": "deepseek-chat",
|
||||
"messages": [{"role": "user", "content": "你好"}]
|
||||
}'
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
```javascript
|
||||
const response = await fetch('https://your-domain.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer your-api-key'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat-search',
|
||||
messages: [{ role: 'user', content: '今天有什么新闻?' }],
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
console.log(decoder.decode(value));
|
||||
}
|
||||
```
|
||||
26
README.MD
26
README.MD
@@ -85,27 +85,21 @@ python dev.py
|
||||
|
||||
## 📡 API 接口
|
||||
|
||||
### 模型列表
|
||||
完整的 API 文档请参阅 **[API.md](API.md)**。
|
||||
|
||||
### 快速示例
|
||||
|
||||
**模型列表**:
|
||||
```
|
||||
GET /v1/models
|
||||
```
|
||||
|
||||
### 对话补全
|
||||
|
||||
```
|
||||
POST /v1/chat/completions
|
||||
Authorization: Bearer your-api-key
|
||||
```
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "deepseek-chat",
|
||||
"messages": [{"role": "user", "content": "你好"}],
|
||||
"stream": true
|
||||
}
|
||||
**对话补全**:
|
||||
```bash
|
||||
curl https://your-domain.com/v1/chat/completions \
|
||||
-H "Authorization: Bearer your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"deepseek-chat","messages":[{"role":"user","content":"你好"}]}'
|
||||
```
|
||||
|
||||
### 管理接口
|
||||
|
||||
@@ -171,7 +171,12 @@ async def chat_completions(request: Request):
|
||||
result_queue.put(None)
|
||||
return
|
||||
|
||||
# logger.info(f"[sse_stream] RAW 原始chunk: {data_str[:300]}")
|
||||
logger.info(f"[sse_stream] RAW 原始chunk: {data_str[:300]}")
|
||||
print(f"[DEBUG] RAW: {data_str[:300]}", flush=True)
|
||||
|
||||
# 写入原始 chunk 到日志文件
|
||||
with open("/tmp/ds2api_debug.log", "a") as f:
|
||||
f.write(f"[MAIN] chunk_path={chunk.get('p', '')}, v_type={type(chunk.get('v')).__name__}, chunk={str(chunk)[:300]}\n")
|
||||
|
||||
if "v" in chunk:
|
||||
v_value = chunk["v"]
|
||||
@@ -181,6 +186,23 @@ async def chat_completions(request: Request):
|
||||
if chunk_path == "response/search_status":
|
||||
continue
|
||||
|
||||
# 跳过所有状态相关的 chunk(不是内容)
|
||||
# 注意:response/status 是真正的结束信号,需要特殊处理(后面的代码会处理)
|
||||
# 但 response/fragments/-1/status 等需要跳过
|
||||
skip_patterns = [
|
||||
"quasi_status", "elapsed_secs", "token_usage",
|
||||
"pending_fragment", "conversation_mode",
|
||||
"fragments/-1/status", "fragments/-2/status" # 搜索片段状态
|
||||
]
|
||||
if any(kw in chunk_path for kw in skip_patterns):
|
||||
continue
|
||||
|
||||
# 检查是否是真正的响应结束信号
|
||||
if chunk_path == "response/status" and isinstance(v_value, str) and v_value == "FINISHED":
|
||||
result_queue.put({"choices": [{"index": 0, "finish_reason": "stop"}]})
|
||||
result_queue.put(None)
|
||||
return
|
||||
|
||||
# 检测是否开始正式回复
|
||||
# 只有当 fragments 包含 RESPONSE 类型时才认为开始正式回复
|
||||
if "response/fragments" in chunk_path and isinstance(v_value, list):
|
||||
@@ -207,10 +229,12 @@ async def chat_completions(request: Request):
|
||||
else:
|
||||
ptype = "text"
|
||||
|
||||
# logger.info(f"[sse_stream] ptype={ptype}, response_started={response_started}, chunk_path='{chunk_path}', v_type={type(v_value).__name__}, v={str(v_value)[:50]}")
|
||||
logger.info(f"[sse_stream] ptype={ptype}, response_started={response_started}, chunk_path='{chunk_path}', v_type={type(v_value).__name__}, v={str(v_value)[:100]}")
|
||||
if isinstance(v_value, str):
|
||||
# 检查是否是 FINISHED 状态
|
||||
if v_value == "FINISHED":
|
||||
# 只有当 chunk_path 为空或为 "status" 时才认为是真正的结束
|
||||
# 搜索模型会发送 "response/fragments/-1/status": "FINISHED" 表示搜索片段完成,不是响应结束
|
||||
if v_value == "FINISHED" and (not chunk_path or chunk_path == "status"):
|
||||
result_queue.put({"choices": [{"index": 0, "finish_reason": "stop"}]})
|
||||
result_queue.put(None)
|
||||
return
|
||||
@@ -227,21 +251,51 @@ async def chat_completions(request: Request):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
# 检查是否是 FINISHED 状态
|
||||
if item.get("p") == "status" and item.get("v") == "FINISHED":
|
||||
return None # 信号结束
|
||||
|
||||
item_p = item.get("p", "")
|
||||
item_v = item.get("v")
|
||||
|
||||
# 写入调试日志 - 显示完整的 item
|
||||
with open("/tmp/ds2api_debug.log", "a") as f:
|
||||
f.write(f"[extract] full_item={str(item)[:200]}\n")
|
||||
|
||||
# 跳过搜索结果项(包含 url/title/snippet 的项目)
|
||||
if "url" in item and "title" in item:
|
||||
continue
|
||||
|
||||
# 跳过 quasi_status(搜索完成信号,不是响应完成)
|
||||
if item_p == "quasi_status":
|
||||
continue
|
||||
|
||||
# 跳过 accumulated_token_usage 和 has_pending_fragment
|
||||
if item_p in ("accumulated_token_usage", "has_pending_fragment"):
|
||||
continue
|
||||
|
||||
# 只有当 p="status" (精确匹配) 且 v="FINISHED" 才认为是真正结束
|
||||
if item_p == "status" and item_v == "FINISHED":
|
||||
return None # 信号结束
|
||||
|
||||
# 跳过搜索状态
|
||||
if item_p == "response/search_status":
|
||||
continue
|
||||
|
||||
# 确定类型
|
||||
# 直接处理包含 content 和 type 的项 (例如 {'id': 2, 'type': 'RESPONSE', 'content': '...'})
|
||||
if "content" in item and "type" in item:
|
||||
inner_type = item.get("type", "").upper()
|
||||
if inner_type == "THINK" or inner_type == "THINKING":
|
||||
final_type = "thinking"
|
||||
elif inner_type == "RESPONSE":
|
||||
final_type = "text"
|
||||
else:
|
||||
final_type = default_type
|
||||
content = item.get("content", "")
|
||||
if content:
|
||||
extracted.append((content, final_type))
|
||||
continue
|
||||
|
||||
# 确定类型(基于 p 字段)
|
||||
if "thinking" in item_p:
|
||||
content_type = "thinking"
|
||||
elif "content" in item_p or item_p == "response":
|
||||
elif "content" in item_p or item_p == "response" or item_p == "fragments":
|
||||
content_type = "text"
|
||||
else:
|
||||
content_type = default_type
|
||||
@@ -256,7 +310,6 @@ async def chat_completions(request: Request):
|
||||
if isinstance(inner, dict):
|
||||
# 检查内层的 type 字段
|
||||
inner_type = inner.get("type", "").upper()
|
||||
# logger.info(f"[sse_stream] 内层 type={inner_type}, content={str(inner.get('content', ''))[:50]}")
|
||||
# DeepSeek 使用 THINK 而不是 THINKING
|
||||
if inner_type == "THINK" or inner_type == "THINKING":
|
||||
final_type = "thinking"
|
||||
@@ -396,7 +449,8 @@ async def chat_completions(request: Request):
|
||||
if ctype == "thinking":
|
||||
if thinking_enabled:
|
||||
final_thinking += ctext
|
||||
elif ctype == "text":
|
||||
else:
|
||||
# 非 thinking 内容都作为普通文本处理(包括 ctype=None 或 "text")
|
||||
final_text += ctext
|
||||
delta_obj = {}
|
||||
if not first_chunk_sent:
|
||||
@@ -405,8 +459,10 @@ async def chat_completions(request: Request):
|
||||
if ctype == "thinking":
|
||||
if thinking_enabled:
|
||||
delta_obj["reasoning_content"] = ctext
|
||||
elif ctype == "text":
|
||||
delta_obj["content"] = ctext
|
||||
else:
|
||||
# 非 thinking 内容都作为 content 输出
|
||||
if ctext:
|
||||
delta_obj["content"] = ctext
|
||||
if delta_obj:
|
||||
new_choices.append({"delta": delta_obj, "index": choice.get("index", 0)})
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function App() {
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||
<p className="text-muted-foreground animate-pulse">检查登录状态...</p>
|
||||
<p className="text-muted-foreground animate-pulse">正在检查登录状态...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -184,7 +184,7 @@ export default function App() {
|
||||
</div>
|
||||
<span>DS2API</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground mt-2 font-semibold tracking-[0.1em] uppercase opacity-60 px-1">V1.0.0 Admin Panel</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-2 font-semibold tracking-[0.1em] uppercase opacity-60 px-1">V1.0.0 管理面板</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-3 space-y-1 overflow-y-auto pt-2">
|
||||
@@ -216,20 +216,20 @@ export default function App() {
|
||||
<div className="p-4 border-t border-border bg-card">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between text-sm px-1">
|
||||
<span className="text-muted-foreground font-semibold text-[10px] uppercase tracking-wider">System Status</span>
|
||||
<span className="text-muted-foreground font-semibold text-[10px] uppercase tracking-wider">系统状态</span>
|
||||
<span className="flex items-center gap-1.5 text-[10px] font-bold text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
ONLINE
|
||||
在线
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="bg-background rounded-lg p-3 border border-border shadow-sm">
|
||||
<div className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70">Accounts</div>
|
||||
<div className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70">账号</div>
|
||||
<div className="text-lg font-bold text-foreground leading-tight">{config.accounts?.length || 0}</div>
|
||||
</div>
|
||||
<div className="bg-background rounded-lg p-3 border border-border shadow-sm">
|
||||
<div className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70">API Keys</div>
|
||||
<div className="text-lg font-bold text-foreground leading-tight">{config.keys?.length || 0}</div>
|
||||
<div className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70">密钥</div>
|
||||
<div className="text-lg font-bold text-foreground">{config.keys?.length || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
body: JSON.stringify({ key: newKey.trim() }),
|
||||
})
|
||||
if (res.ok) {
|
||||
onMessage('success', 'API Key added successfully')
|
||||
onMessage('success', 'API 密钥添加成功')
|
||||
setNewKey('')
|
||||
setShowAddKey(false)
|
||||
onRefresh()
|
||||
@@ -66,14 +66,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
onMessage('error', data.detail || 'Failed to add')
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', 'Network error')
|
||||
onMessage('error', '网络错误')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteKey = async (key) => {
|
||||
if (!confirm('Are you sure you want to delete this API Key?')) return
|
||||
if (!confirm('确定要删除此 API 密钥吗?')) return
|
||||
try {
|
||||
const res = await apiFetch(`/admin/keys/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
@@ -100,7 +100,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
body: JSON.stringify(newAccount),
|
||||
})
|
||||
if (res.ok) {
|
||||
onMessage('success', 'Account added successfully')
|
||||
onMessage('success', '账号添加成功')
|
||||
setNewAccount({ email: '', mobile: '', password: '' })
|
||||
setShowAddAccount(false)
|
||||
onRefresh()
|
||||
@@ -109,14 +109,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
onMessage('error', data.detail || 'Failed to add')
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', 'Network error')
|
||||
onMessage('error', '网络错误')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAccount = async (id) => {
|
||||
if (!confirm('Are you sure you want to delete this account?')) return
|
||||
if (!confirm('确定要删除此账号吗?')) return
|
||||
try {
|
||||
const res = await apiFetch(`/admin/accounts/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||
if (res.ok) {
|
||||
@@ -149,7 +149,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
}
|
||||
|
||||
const validateAllAccounts = async () => {
|
||||
if (!confirm('Validate ALL accounts? This might take a while.')) return
|
||||
if (!confirm('校验所有账号?这可能需要一些时间。')) return
|
||||
const accounts = config.accounts || []
|
||||
if (accounts.length === 0) return
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
}
|
||||
|
||||
const testAllAccounts = async () => {
|
||||
if (!confirm('Test API connectivity for ALL accounts?')) return
|
||||
if (!confirm('测试所有账号的 API 连通性?')) return
|
||||
const accounts = config.accounts || []
|
||||
if (accounts.length === 0) return
|
||||
|
||||
@@ -248,30 +248,30 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<CheckCircle2 className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">Available</p>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">可用</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.available}</span>
|
||||
<span className="text-xs text-muted-foreground">accounts</span>
|
||||
<span className="text-xs text-muted-foreground">个账号</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-xl p-4 flex flex-col justify-between shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<Server className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">In Use</p>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">正在使用</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.in_use}</span>
|
||||
<span className="text-xs text-muted-foreground">threads</span>
|
||||
<span className="text-xs text-muted-foreground">线程</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-card border border-border rounded-xl p-4 flex flex-col justify-between shadow-sm relative overflow-hidden group">
|
||||
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||
<ShieldCheck className="w-16 h-16" />
|
||||
</div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">Total Pool</p>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">账号池总数</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-foreground">{queueStatus.total}</span>
|
||||
<span className="text-xs text-muted-foreground">accounts</span>
|
||||
<span className="text-xs text-muted-foreground">个账号</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,7 +290,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Key
|
||||
添加密钥
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -329,7 +329,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
|
||||
>
|
||||
{testingAll ? <span className="animate-spin mr-2">⟳</span> : <Play className="w-3 h-3 mr-2" />}
|
||||
Test All
|
||||
测试全部
|
||||
</button>
|
||||
<button
|
||||
onClick={validateAllAccounts}
|
||||
@@ -337,14 +337,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
|
||||
>
|
||||
{validatingAll ? <span className="animate-spin mr-2">⟳</span> : <CheckCircle2 className="w-3 h-3 mr-2" />}
|
||||
Validate All
|
||||
校验全部
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddAccount(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Account
|
||||
添加账号
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -455,9 +455,9 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setShowAddKey(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">Cancel</button>
|
||||
<button onClick={() => setShowAddKey(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">取消</button>
|
||||
<button onClick={addKey} disabled={loading} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50">
|
||||
{loading ? 'Adding...' : 'Add Key'}
|
||||
{loading ? '添加中...' : '添加密钥'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -508,9 +508,9 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={() => setShowAddAccount(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">Cancel</button>
|
||||
<button onClick={() => setShowAddAccount(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">取消</button>
|
||||
<button onClick={addAccount} disabled={loading} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50">
|
||||
{loading ? 'Adding...' : 'Add Account'}
|
||||
{loading ? '添加中...' : '添加账号'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,10 @@ import {
|
||||
import clsx from 'clsx'
|
||||
|
||||
const MODELS = [
|
||||
{ id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: "通用高智商模型 (V3)", color: "text-amber-500" },
|
||||
{ id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: "深度推理/思维链 (R1)", color: "text-amber-600" },
|
||||
{ id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: "非思考模型", color: "text-amber-500" },
|
||||
{ id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: "思考模型", color: "text-amber-600" },
|
||||
{ id: "deepseek-chat-search", name: "deepseek-chat-search", icon: SearchIcon, desc: "非思考模型 (带搜索)", color: "text-cyan-500" },
|
||||
{ id: "deepseek-reasoner-search", name: "deepseek-reasoner-search", icon: SearchIcon, desc: "思考模型 (带搜索)", color: "text-cyan-600" },
|
||||
];
|
||||
|
||||
export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
@@ -116,13 +118,16 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
|
||||
try {
|
||||
const json = JSON.parse(dataStr)
|
||||
console.log('[ApiTester] Parsed JSON:', json)
|
||||
const choice = json.choices?.[0]
|
||||
if (choice?.delta) {
|
||||
const delta = choice.delta
|
||||
console.log('[ApiTester] Delta:', delta)
|
||||
if (delta.reasoning_content) {
|
||||
setStreamingThinking(prev => prev + delta.reasoning_content)
|
||||
}
|
||||
if (delta.content) {
|
||||
console.log('[ApiTester] Content:', delta.content)
|
||||
setStreamingContent(prev => prev + delta.content)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +140,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
if (e.name === 'AbortError') {
|
||||
onMessage('info', '已停止生成')
|
||||
} else {
|
||||
onMessage('error', 'Network error: ' + e.message)
|
||||
onMessage('error', '网络错误: ' + e.message)
|
||||
setResponse({ error: e.message, success: false })
|
||||
}
|
||||
} finally {
|
||||
@@ -167,12 +172,12 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
account: selectedAccount,
|
||||
})
|
||||
if (data.success) {
|
||||
onMessage('success', `${selectedAccount}: Test Success (${data.response_time}ms)`)
|
||||
onMessage('success', `${selectedAccount}: 测试成功 (${data.response_time}ms)`)
|
||||
} else {
|
||||
onMessage('error', `${selectedAccount}: ${data.message}`)
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', 'Network error: ' + e.message)
|
||||
onMessage('error', '网络错误: ' + e.message)
|
||||
setResponse({ error: e.message })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -229,7 +234,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
>
|
||||
<div className={clsx(
|
||||
"p-1.5 rounded-md shrink-0 transition-colors",
|
||||
model === m.id ? "text-primary" : "text-muted-foreground group-hover:text-foreground"
|
||||
model === m.id ? m.color : "text-muted-foreground group-hover:text-foreground"
|
||||
)}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
@@ -240,7 +245,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">{m.desc}</div>
|
||||
</div>
|
||||
{model === m.id && (
|
||||
<div className="absolute top-3 right-3 text-primary">
|
||||
<div className={clsx("absolute top-3 right-3", m.color)}>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-current" />
|
||||
</div>
|
||||
)}
|
||||
@@ -274,7 +279,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
<input
|
||||
type="password"
|
||||
className="w-full h-10 px-3 bg-muted/30 border border-border rounded-lg text-sm font-mono placeholder:text-muted-foreground/40 focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all"
|
||||
placeholder={config.keys?.[0] ? `默认: ...${config.keys[0].slice(-6)}` : '输入自定义 Key'}
|
||||
placeholder={config.keys?.[0] ? `默认: ...${config.keys[0].slice(-6)}` : '输入自定义密钥'}
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
/>
|
||||
@@ -319,7 +324,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
"text-[10px] px-1.5 py-0.5 rounded-sm border uppercase font-medium tracking-wider",
|
||||
response.success ? "border-emerald-500/20 text-emerald-500 bg-emerald-500/10" : "border-destructive/20 text-destructive bg-destructive/10"
|
||||
)}>
|
||||
{response.status_code || 'ERR'}
|
||||
{response.status_code || '错误'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -328,7 +333,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
<div className="text-xs bg-secondary/50 border border-border rounded-lg p-3 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Zap className="w-3.5 h-3.5" />
|
||||
<span className="font-medium">Thinking Process</span>
|
||||
<span className="font-medium">思维链过程</span>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap leading-relaxed text-muted-foreground font-mono text-[11px] max-h-60 overflow-y-auto custom-scrollbar pl-5 border-l-2 border-border/50">
|
||||
{streamingThinking || response?.response?.thinking}
|
||||
@@ -340,7 +345,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
{!selectedAccount ? (
|
||||
streamingContent || (response?.error && <span className="text-destructive font-medium">{response.error}</span>)
|
||||
) : (
|
||||
response?.response?.message || <span className="text-muted-foreground italic">Generating response...</span>
|
||||
response?.response?.message || <span className="text-muted-foreground italic">正在生成响应...</span>
|
||||
)}
|
||||
{isStreaming && <span className="inline-block w-1.5 h-4 bg-primary ml-1 align-middle animate-pulse" />}
|
||||
</div>
|
||||
@@ -354,7 +359,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
<div className="max-w-4xl mx-auto relative group">
|
||||
<textarea
|
||||
className="w-full bg-[#09090b] border border-border rounded-xl pl-4 pr-12 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all resize-none custom-scrollbar placeholder:text-muted-foreground/50 text-foreground shadow-inner"
|
||||
placeholder="Type a message..."
|
||||
placeholder="输入消息..."
|
||||
rows={1}
|
||||
style={{ minHeight: '52px' }}
|
||||
value={message}
|
||||
@@ -386,7 +391,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto mt-3 flex justify-center">
|
||||
<span className="text-[10px] text-muted-foreground/40 font-medium">DeepSeek Admin Interface</span>
|
||||
<span className="text-[10px] text-muted-foreground/40 font-medium">DeepSeek 管理员界面</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
onMessage('error', data.detail || '导入失败')
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', 'Network error')
|
||||
onMessage('error', '网络错误')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
className="w-full flex items-center justify-center gap-2 py-2.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium text-sm shadow-sm"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? 'Copied' : 'Copy Base64 Config'}
|
||||
{copied ? '已复制' : '复制 Base64 配置'}
|
||||
</button>
|
||||
<p className="text-[10px] text-muted-foreground mt-2 text-center">
|
||||
变量名: <code className="bg-background px-1 py-0.5 rounded border border-border">DS2API_CONFIG_JSON</code>
|
||||
@@ -186,10 +186,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleExport} className="px-3 py-1.5 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border">
|
||||
Load Current
|
||||
加载当前配置
|
||||
</button>
|
||||
<button onClick={handleImport} disabled={loading} className="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-xs font-medium disabled:opacity-50">
|
||||
{loading ? 'Importing...' : 'Apply Config'}
|
||||
{loading ? '正在导入...' : '应用配置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Key, ArrowRight, ShieldCheck, Lock } from 'lucide-react'
|
||||
import { Key, ArrowRight, ShieldCheck, Lock, Check } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export default function Login({ onLogin, onMessage }) {
|
||||
@@ -46,69 +46,55 @@ export default function Login({ onLogin, onMessage }) {
|
||||
|
||||
<div className="w-full max-w-[400px] relative z-10 animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="w-full bg-card border border-border rounded-xl p-8 shadow-sm">
|
||||
<div className="text-center space-y-2 mb-8">
|
||||
<div className="text-center space-y-2 mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-2">
|
||||
<Lock className="w-6 h-6" />
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold tracking-tight text-foreground">
|
||||
欢迎回来
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
请输入管理员密钥继续
|
||||
</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">欢迎回来</h1>
|
||||
<p className="text-sm text-muted-foreground/80">请输入管理员密钥以继续</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-muted-foreground ml-0.5">
|
||||
管理员密钥
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-muted-foreground/50 transition-colors">
|
||||
<Key className="w-4 h-4" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="admin_key"
|
||||
className="w-full h-12 px-4 bg-[#09090b] border border-border rounded-lg text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all text-foreground font-mono"
|
||||
placeholder="••••••••••••••••"
|
||||
value={adminKey}
|
||||
onChange={(e) => setAdminKey(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||
/>
|
||||
<form onSubmit={handleLogin} className="space-y-5 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-150">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-widest ml-1">管理员密钥</label>
|
||||
<div className="relative group">
|
||||
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors">
|
||||
<Key className="w-4 h-4" />
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
className="w-full bg-[#09090b] border border-border rounded-xl pl-10 pr-4 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all placeholder:text-muted-foreground/30 text-foreground"
|
||||
placeholder="输入您的管理员密钥..."
|
||||
value={adminKey}
|
||||
onChange={e => setAdminKey(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2.5">
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={remember}
|
||||
onClick={() => setRemember(!remember)}
|
||||
className={clsx(
|
||||
"w-4 h-4 rounded border flex items-center justify-center transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-ring/40",
|
||||
remember ? "bg-primary border-primary text-primary-foreground" : "border-muted-foreground/40 bg-transparent hover:border-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{remember && <div className="w-2 h-2 rounded-[1px] bg-current" />}
|
||||
</button>
|
||||
<span
|
||||
onClick={() => setRemember(!remember)}
|
||||
className="text-xs text-muted-foreground cursor-pointer select-none hover:text-foreground transition-colors"
|
||||
>
|
||||
记住登录状态
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<label className="flex items-center gap-2.5 cursor-pointer group">
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer sr-only"
|
||||
checked={remember}
|
||||
onChange={e => setRemember(e.target.checked)}
|
||||
/>
|
||||
<div className="w-4.5 h-4.5 bg-secondary border border-border rounded-md peer-checked:bg-primary peer-checked:border-primary transition-all shadow-sm"></div>
|
||||
<Check className="absolute w-3 h-3 text-primary-foreground opacity-0 peer-checked:opacity-100 left-0.5 transition-opacity" />
|
||||
</div>
|
||||
<span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors">记住登录状态</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full flex items-center justify-center py-2.5 px-4 rounded-lg bg-primary hover:bg-primary/90 text-primary-foreground font-medium text-sm transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ring disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="w-full h-12 flex items-center justify-center gap-2 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 transition-all font-semibold text-sm shadow-lg shadow-primary/20 hover:shadow-primary/30 disabled:opacity-50 disabled:shadow-none"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin" />
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>登录</span>
|
||||
@@ -121,13 +107,13 @@ export default function Login({ onLogin, onMessage }) {
|
||||
<div className="mt-6 pt-6 border-t border-border flex justify-center">
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground/60 font-medium tracking-wide uppercase">
|
||||
<ShieldCheck className="w-3 h-3" />
|
||||
<span>Secured Connection</span>
|
||||
<span>安全连接</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-[10px] text-muted-foreground/30 font-mono">DS2API Admin Portal</p>
|
||||
<p className="text-[10px] text-muted-foreground/30 font-mono text-center">DS2API 管理员门户</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
onMessage('error', data.detail || '同步失败')
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', 'Network error')
|
||||
onMessage('error', '网络错误')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
<input
|
||||
type="password"
|
||||
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all pr-10"
|
||||
placeholder={preconfig?.has_token ? "Using pre-configured token" : "Enter Vercel Access Token"}
|
||||
placeholder={preconfig?.has_token ? "正在使用预配置的令牌" : "输入 Vercel 访问令牌"}
|
||||
value={vercelToken}
|
||||
onChange={e => setVercelToken(e.target.value)}
|
||||
/>
|
||||
@@ -140,11 +140,11 @@ export default function VercelSync({ onMessage, authFetch }) {
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||
Syncing...
|
||||
正在同步...
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-2">
|
||||
Sync & Redeploy <ArrowRight className="w-4 h-4" />
|
||||
同步并重新部署 <ArrowRight className="w-4 h-4" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user