feat: Localize UI strings to Chinese and add new API documentation.

This commit is contained in:
CJACK
2026-02-01 07:09:07 +08:00
parent ef11d9cf13
commit 0dc83912bc
9 changed files with 540 additions and 130 deletions

369
API.md Normal file
View 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));
}
```

View File

@@ -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":"你好"}]}'
```
### 管理接口

View File

@@ -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)})

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>