mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-07 18:05:30 +08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a536c5c27 | ||
|
|
ce73814583 | ||
|
|
792d458ce0 | ||
|
|
392473722c | ||
|
|
baca42280f | ||
|
|
ba96554cc1 | ||
|
|
015ec6eb3c | ||
|
|
9626d6ccbd | ||
|
|
aa032540dc | ||
|
|
b6062fdb0b | ||
|
|
fda5f9339e | ||
|
|
86ade10888 | ||
|
|
063c5d2540 | ||
|
|
4e62292500 | ||
|
|
58d54adee2 | ||
|
|
8ca5581171 | ||
|
|
2007e0cde3 | ||
|
|
51f8d3c09f | ||
|
|
94238070d8 | ||
|
|
a917e19e9d | ||
|
|
f65cf0c510 | ||
|
|
840042d301 | ||
|
|
16ea6b7a5a | ||
|
|
d67b64633b | ||
|
|
a1b3f122a7 | ||
|
|
43cb68cc1d | ||
|
|
06ae417dad | ||
|
|
28b70ca26c | ||
|
|
ee2dac28bd | ||
|
|
db28b4e93e | ||
|
|
092532c625 | ||
|
|
1a842c28d1 | ||
|
|
ce0538fcd9 | ||
|
|
eef4ec4a65 | ||
|
|
951c47f8c5 | ||
|
|
db80aebadc |
69
.dockerignore
Normal file
69
.dockerignore
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# 虚拟环境
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# 环境配置(通过 docker-compose 挂载或环境变量传递)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
config.json
|
||||||
|
|
||||||
|
# 开发工具
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# 测试
|
||||||
|
tests/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Node.js / WebUI 开发依赖
|
||||||
|
node_modules/
|
||||||
|
webui/node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# 文档
|
||||||
|
*.md
|
||||||
|
!README*.md
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github/
|
||||||
|
.releaserc.json
|
||||||
|
|
||||||
|
# 其他
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -17,4 +17,8 @@
|
|||||||
|
|
||||||
#### 📝 补充信息 | Additional Information
|
#### 📝 补充信息 | Additional Information
|
||||||
|
|
||||||
<!-- Add any other context about the Pull Request here. -->
|
<!-- Add any other context about the Pull Request here. -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 💡 **提示**:如果修改了 `webui/` 目录下的文件,PR 合并后 CI 会自动构建并提交产物,无需手动构建。
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -55,7 +55,8 @@ webui/node_modules/
|
|||||||
webui/dist/
|
webui/dist/
|
||||||
.npm
|
.npm
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
package-lock.json
|
# 保留 webui/package-lock.json 用于 CI 缓存
|
||||||
|
# package-lock.json # 如果有根目录的可以忽略
|
||||||
yarn.lock
|
yarn.lock
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|
||||||
@@ -63,6 +64,8 @@ pnpm-lock.yaml
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
.cache/
|
.cache/
|
||||||
.parcel-cache/
|
.parcel-cache/
|
||||||
|
static/admin/*
|
||||||
|
!static/admin/.gitkeep
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
701
API.en.md
Normal file
701
API.en.md
Normal file
@@ -0,0 +1,701 @@
|
|||||||
|
# DS2API API Reference
|
||||||
|
|
||||||
|
Language: [中文](API.md) | [English](API.en.md)
|
||||||
|
|
||||||
|
This document describes all DS2API API endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Basics](#basics)
|
||||||
|
- [OpenAI-Compatible API](#openai-compatible-api)
|
||||||
|
- [List Models](#list-models)
|
||||||
|
- [Chat Completions](#chat-completions)
|
||||||
|
- [Claude-Compatible API](#claude-compatible-api)
|
||||||
|
- [Claude Model List](#claude-model-list)
|
||||||
|
- [Claude Messages](#claude-messages)
|
||||||
|
- [Token Counting](#token-counting)
|
||||||
|
- [Admin API](#admin-api)
|
||||||
|
- [Login](#login)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Account Management](#account-management)
|
||||||
|
- [Vercel Sync](#vercel-sync)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Examples](#examples)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basics
|
||||||
|
|
||||||
|
| Item | Description |
|
||||||
|
|-----|------|
|
||||||
|
| **Base URL** | `https://your-domain.com` or `http://localhost:5001` |
|
||||||
|
| **OpenAI auth** | `Authorization: Bearer <api-key>` |
|
||||||
|
| **Claude auth** | `x-api-key: <api-key>` |
|
||||||
|
| **Response format** | JSON |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OpenAI-Compatible API
|
||||||
|
|
||||||
|
### List Models
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response example**:
|
||||||
|
|
||||||
|
```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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Chat Completions
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1/chat/completions
|
||||||
|
Authorization: Bearer your-api-key
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----|------|:----:|------|
|
||||||
|
| `model` | string | ✅ | Model name (see below) |
|
||||||
|
| `messages` | array | ✅ | Chat messages |
|
||||||
|
| `stream` | boolean | ❌ | Stream responses (default `false`) |
|
||||||
|
| `temperature` | number | ❌ | Temperature (0-2) |
|
||||||
|
| `max_tokens` | number | ❌ | Max output tokens |
|
||||||
|
| `tools` | array | ❌ | Tool definitions (Function Calling) |
|
||||||
|
| `tool_choice` | string | ❌ | Tool selection strategy |
|
||||||
|
|
||||||
|
**Supported models**:
|
||||||
|
|
||||||
|
| Model | Reasoning | Search | Notes |
|
||||||
|
|-----|:--------:|:------:|------|
|
||||||
|
| `deepseek-chat` | ❌ | ❌ | Standard chat |
|
||||||
|
| `deepseek-reasoner` | ✅ | ❌ | Reasoning mode with trace |
|
||||||
|
| `deepseek-chat-search` | ❌ | ✅ | Search enhanced |
|
||||||
|
| `deepseek-reasoner-search` | ✅ | ✅ | Reasoning + search |
|
||||||
|
|
||||||
|
**Basic request example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": "Hello"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Streaming request example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "deepseek-reasoner-search",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "What's in the news today?"}
|
||||||
|
],
|
||||||
|
"stream": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Streaming response format** (`stream: true`):
|
||||||
|
|
||||||
|
```
|
||||||
|
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"},"index":0}]}
|
||||||
|
|
||||||
|
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"reasoning_content":"Let me think..."},"index":0}]}
|
||||||
|
|
||||||
|
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Based on search results..."},"index":0}]}
|
||||||
|
|
||||||
|
data: {"id":"...","object":"chat.completion.chunk","choices":[{"index":0,"finish_reason":"stop"}]}
|
||||||
|
|
||||||
|
data: [DONE]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: Reasoning models emit `reasoning_content` with the trace.
|
||||||
|
|
||||||
|
**Non-streaming response format** (`stream: false`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "chatcmpl-xxx",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": 1738400000,
|
||||||
|
"model": "deepseek-reasoner",
|
||||||
|
"choices": [{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Response text",
|
||||||
|
"reasoning_content": "Reasoning trace (reasoner only)"
|
||||||
|
},
|
||||||
|
"finish_reason": "stop"
|
||||||
|
}],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 10,
|
||||||
|
"completion_tokens": 50,
|
||||||
|
"total_tokens": 60,
|
||||||
|
"completion_tokens_details": {
|
||||||
|
"reasoning_tokens": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tool Calling (Function Calling)
|
||||||
|
|
||||||
|
**Request example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"messages": [{"role": "user", "content": "What's the weather in Beijing?"}],
|
||||||
|
"tools": [{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"description": "Get the weather for a city",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {"type": "string", "description": "City name"}
|
||||||
|
},
|
||||||
|
"required": ["location"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "chatcmpl-xxx",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"choices": [{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": null,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": "call_xxx",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"arguments": "{\"location\": \"Beijing\"}"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"finish_reason": "tool_calls"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Claude-Compatible API
|
||||||
|
|
||||||
|
### Claude Model List
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /anthropic/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"object": "list",
|
||||||
|
"data": [
|
||||||
|
{"id": "claude-sonnet-4-20250514", "object": "model", "owned_by": "anthropic"},
|
||||||
|
{"id": "claude-sonnet-4-20250514-fast", "object": "model", "owned_by": "anthropic"},
|
||||||
|
{"id": "claude-sonnet-4-20250514-slow", "object": "model", "owned_by": "anthropic"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Model mapping**:
|
||||||
|
|
||||||
|
| Claude Model | Actual | Notes |
|
||||||
|
|------------|--------|------|
|
||||||
|
| `claude-sonnet-4-20250514` | deepseek-chat | Standard mode |
|
||||||
|
| `claude-sonnet-4-20250514-fast` | deepseek-chat | Fast mode |
|
||||||
|
| `claude-sonnet-4-20250514-slow` | deepseek-reasoner | Reasoning mode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Claude Messages
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /anthropic/v1/messages
|
||||||
|
x-api-key: your-api-key
|
||||||
|
Content-Type: application/json
|
||||||
|
anthropic-version: 2023-06-01
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----|------|:----:|------|
|
||||||
|
| `model` | string | ✅ | Model name |
|
||||||
|
| `max_tokens` | integer | ✅ | Max output tokens |
|
||||||
|
| `messages` | array | ✅ | Chat messages |
|
||||||
|
| `stream` | boolean | ❌ | Stream responses (default `false`) |
|
||||||
|
| `system` | string | ❌ | System prompt |
|
||||||
|
| `temperature` | number | ❌ | Temperature |
|
||||||
|
|
||||||
|
**Request example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "claude-sonnet-4-20250514",
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "Hello, please introduce yourself."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Non-streaming response**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "msg_xxx",
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"content": [{
|
||||||
|
"type": "text",
|
||||||
|
"text": "Hello! I'm an AI assistant..."
|
||||||
|
}],
|
||||||
|
"model": "claude-sonnet-4-20250514",
|
||||||
|
"stop_reason": "end_turn",
|
||||||
|
"usage": {
|
||||||
|
"input_tokens": 10,
|
||||||
|
"output_tokens": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Streaming response** (SSE):
|
||||||
|
|
||||||
|
```
|
||||||
|
event: message_start
|
||||||
|
data: {"type":"message_start","message":{"id":"msg_xxx","type":"message","role":"assistant","model":"claude-sonnet-4-20250514"}}
|
||||||
|
|
||||||
|
event: content_block_start
|
||||||
|
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
|
||||||
|
|
||||||
|
event: content_block_delta
|
||||||
|
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
|
||||||
|
|
||||||
|
event: content_block_stop
|
||||||
|
data: {"type":"content_block_stop","index":0}
|
||||||
|
|
||||||
|
event: message_delta
|
||||||
|
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":50}}
|
||||||
|
|
||||||
|
event: message_stop
|
||||||
|
data: {"type":"message_stop"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Token Counting
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /anthropic/v1/messages/count_tokens
|
||||||
|
x-api-key: your-api-key
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model": "claude-sonnet-4-20250514",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "Hello"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response example**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"input_tokens": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin API
|
||||||
|
|
||||||
|
All admin endpoints (except login) require `Authorization: Bearer <jwt-token>`.
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/login
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "your-admin-key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"token": "jwt-token-string",
|
||||||
|
"expires_in": 86400
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Tokens are valid for 24 hours by default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Get configuration
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /admin/config
|
||||||
|
Authorization: Bearer <jwt-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"keys": ["api-key-1", "api-key-2"],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "***",
|
||||||
|
"token": "session-token"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update configuration
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/config
|
||||||
|
Authorization: Bearer <jwt-token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"keys": ["new-api-key"],
|
||||||
|
"accounts": [...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Account Management
|
||||||
|
|
||||||
|
#### Add account
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/accounts
|
||||||
|
Authorization: Bearer <jwt-token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "password123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Batch import accounts
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/accounts/batch
|
||||||
|
Authorization: Bearer <jwt-token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accounts": [
|
||||||
|
{"email": "user1@example.com", "password": "pass1"},
|
||||||
|
{"email": "user2@example.com", "password": "pass2"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Test one account
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/accounts/test
|
||||||
|
Authorization: Bearer <jwt-token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "user@example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Test all accounts
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/accounts/test-all
|
||||||
|
Authorization: Bearer <jwt-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Queue status
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /admin/queue/status
|
||||||
|
Authorization: Bearer <jwt-token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total_accounts": 5,
|
||||||
|
"healthy_accounts": 4,
|
||||||
|
"queue_size": 10,
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"status": "healthy",
|
||||||
|
"last_used": "2026-02-01T12:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Vercel Sync
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /admin/vercel/sync
|
||||||
|
Authorization: Bearer <jwt-token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request body** (first sync only):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vercel_token": "your-vercel-token",
|
||||||
|
"project_id": "your-project-id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> After a successful first sync, credentials are stored for future syncs.
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Configuration synced to Vercel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All error responses follow this structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"message": "Error description",
|
||||||
|
"type": "error_type",
|
||||||
|
"code": "error_code"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common error codes**:
|
||||||
|
|
||||||
|
| HTTP Status | Error Type | Description |
|
||||||
|
|:----------:|---------|------|
|
||||||
|
| 400 | `invalid_request_error` | Invalid request parameters |
|
||||||
|
| 401 | `authentication_error` | Missing or invalid API key |
|
||||||
|
| 403 | `permission_denied` | Insufficient permissions |
|
||||||
|
| 429 | `rate_limit_error` | Too many requests |
|
||||||
|
| 500 | `internal_error` | Internal server error |
|
||||||
|
| 503 | `service_unavailable` | No available accounts |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Python (OpenAI SDK)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="your-api-key",
|
||||||
|
base_url="https://your-domain.com/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Basic chat
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-chat",
|
||||||
|
messages=[{"role": "user", "content": "Hello"}]
|
||||||
|
)
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
|
||||||
|
# Streaming + reasoning
|
||||||
|
for chunk in client.chat.completions.create(
|
||||||
|
model="deepseek-reasoner",
|
||||||
|
messages=[{"role": "user", "content": "Explain relativity"}],
|
||||||
|
stream=True
|
||||||
|
):
|
||||||
|
delta = chunk.choices[0].delta
|
||||||
|
if hasattr(delta, 'reasoning_content') and delta.reasoning_content:
|
||||||
|
print(f"[Reasoning] {delta.reasoning_content}", end="")
|
||||||
|
if delta.content:
|
||||||
|
print(delta.content, end="")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python (Anthropic SDK)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import anthropic
|
||||||
|
|
||||||
|
client = anthropic.Anthropic(
|
||||||
|
api_key="your-api-key",
|
||||||
|
base_url="https://your-domain.com/anthropic"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.messages.create(
|
||||||
|
model="claude-sonnet-4-20250514",
|
||||||
|
max_tokens=1024,
|
||||||
|
messages=[{"role": "user", "content": "Hello"}]
|
||||||
|
)
|
||||||
|
print(response.content[0].text)
|
||||||
|
```
|
||||||
|
|
||||||
|
### cURL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# OpenAI format
|
||||||
|
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": "Hello"}]
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Claude format
|
||||||
|
curl https://your-domain.com/anthropic/v1/messages \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: your-api-key" \
|
||||||
|
-H "anthropic-version: 2023-06-01" \
|
||||||
|
-d '{
|
||||||
|
"model": "claude-sonnet-4-20250514",
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript / TypeScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// OpenAI format - streaming request
|
||||||
|
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: 'What is in the news today?' }],
|
||||||
|
stream: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
const chunk = decoder.decode(value);
|
||||||
|
const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const data = line.slice(6);
|
||||||
|
if (data === '[DONE]') continue;
|
||||||
|
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
const content = json.choices?.[0]?.delta?.content;
|
||||||
|
if (content) process.stdout.write(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node.js (OpenAI SDK)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
const client = new OpenAI({
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
baseURL: 'https://your-domain.com/v1'
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream = await client.chat.completions.create({
|
||||||
|
model: 'deepseek-reasoner',
|
||||||
|
messages: [{ role: 'user', content: 'Explain black holes' }],
|
||||||
|
stream: true
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
const content = chunk.choices[0]?.delta?.content;
|
||||||
|
if (content) process.stdout.write(content);
|
||||||
|
}
|
||||||
|
```
|
||||||
2
API.md
2
API.md
@@ -1,5 +1,7 @@
|
|||||||
# DS2API 接口文档
|
# DS2API 接口文档
|
||||||
|
|
||||||
|
语言 / Language: [中文](API.md) | [English](API.en.md)
|
||||||
|
|
||||||
本文档详细介绍 DS2API 提供的所有 API 端点。
|
本文档详细介绍 DS2API 提供的所有 API 端点。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
94
CONTRIBUTING.en.md
Normal file
94
CONTRIBUTING.en.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Contributing Guide
|
||||||
|
|
||||||
|
Language: [中文](CONTRIBUTING.md) | [English](CONTRIBUTING.en.md)
|
||||||
|
|
||||||
|
Thank you for contributing to DS2API!
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repo
|
||||||
|
git clone https://github.com/CJackHwang/ds2api.git
|
||||||
|
cd ds2api
|
||||||
|
|
||||||
|
# 2. Create a virtual environment (recommended)
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||||
|
|
||||||
|
# 3. Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 4. Configure
|
||||||
|
cp config.example.json config.json
|
||||||
|
# Edit config.json
|
||||||
|
|
||||||
|
# 5. Run
|
||||||
|
python dev.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (WebUI)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd webui
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
WebUI language packs live in `webui/src/locales/`. Add new locale JSON files there.
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
- **Python**: Follow PEP 8, use 4-space indentation
|
||||||
|
- **JavaScript/React**: Use 4-space indentation and function components
|
||||||
|
- **Commit messages**: Use semantic prefixes (e.g. `feat:`, `fix:`, `docs:`)
|
||||||
|
|
||||||
|
## Submitting a PR
|
||||||
|
|
||||||
|
1. Fork this repo
|
||||||
|
2. Create a feature branch (`git checkout -b feature/xxx`)
|
||||||
|
3. Commit your changes (`git commit -m 'feat: add xxx'`)
|
||||||
|
4. Push your branch (`git push origin feature/xxx`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## WebUI Build
|
||||||
|
|
||||||
|
> **Important**: After modifying `webui/`, **no manual build is required**.
|
||||||
|
|
||||||
|
When a PR is merged into `main`, GitHub Actions will automatically:
|
||||||
|
1. Build the WebUI
|
||||||
|
2. Commit build artifacts to `static/admin/`
|
||||||
|
|
||||||
|
If you need a local build (for testing):
|
||||||
|
```bash
|
||||||
|
./scripts/build-webui.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ds2api/
|
||||||
|
├── app.py # FastAPI entrypoint
|
||||||
|
├── dev.py # Development server
|
||||||
|
├── core/ # Core modules
|
||||||
|
│ ├── auth.py # Account auth & rotation
|
||||||
|
│ ├── config.py # Configuration management
|
||||||
|
│ ├── deepseek.py # DeepSeek API calls
|
||||||
|
│ ├── models.py # Model definitions
|
||||||
|
│ ├── pow.py # PoW calculations
|
||||||
|
│ └── sse_parser.py # SSE parsing
|
||||||
|
├── routes/ # API routes
|
||||||
|
│ ├── openai.py # OpenAI-compatible endpoints
|
||||||
|
│ ├── claude.py # Claude-compatible endpoints
|
||||||
|
│ ├── home.py # Landing page routes
|
||||||
|
│ └── admin/ # Admin endpoints
|
||||||
|
├── webui/ # React WebUI source
|
||||||
|
├── static/admin/ # WebUI build output (auto-generated)
|
||||||
|
└── scripts/ # Helper scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
- Use [GitHub Issues](https://github.com/CJackHwang/ds2api/issues)
|
||||||
|
- Provide detailed reproduction steps and logs
|
||||||
94
CONTRIBUTING.md
Normal file
94
CONTRIBUTING.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# 贡献指南
|
||||||
|
|
||||||
|
语言 / Language: [中文](CONTRIBUTING.md) | [English](CONTRIBUTING.en.md)
|
||||||
|
|
||||||
|
感谢你对 DS2API 的贡献!
|
||||||
|
|
||||||
|
## 开发环境设置
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 克隆仓库
|
||||||
|
git clone https://github.com/CJackHwang/ds2api.git
|
||||||
|
cd ds2api
|
||||||
|
|
||||||
|
# 2. 创建虚拟环境(推荐)
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||||
|
|
||||||
|
# 3. 安装依赖
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 4. 配置
|
||||||
|
cp config.example.json config.json
|
||||||
|
# 编辑 config.json
|
||||||
|
|
||||||
|
# 5. 启动
|
||||||
|
python dev.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端 (WebUI)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd webui
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
WebUI 语言包位于 `webui/src/locales/`,新增语言请在此处添加对应 JSON 文件。
|
||||||
|
|
||||||
|
## 代码规范
|
||||||
|
|
||||||
|
- **Python**: 遵循 PEP 8,使用 4 空格缩进
|
||||||
|
- **JavaScript/React**: 使用 4 空格缩进,使用函数组件
|
||||||
|
- **提交信息**: 使用语义化提交格式(如 `feat:`, `fix:`, `docs:`)
|
||||||
|
|
||||||
|
## 提交 PR
|
||||||
|
|
||||||
|
1. Fork 本仓库
|
||||||
|
2. 创建功能分支 (`git checkout -b feature/xxx`)
|
||||||
|
3. 提交更改 (`git commit -m 'feat: 添加xxx功能'`)
|
||||||
|
4. 推送分支 (`git push origin feature/xxx`)
|
||||||
|
5. 创建 Pull Request
|
||||||
|
|
||||||
|
## WebUI 构建
|
||||||
|
|
||||||
|
> **重要**: 修改 `webui/` 目录后 **无需手动构建**!
|
||||||
|
|
||||||
|
当 PR 合并到 `main` 分支后,GitHub Actions 会自动:
|
||||||
|
1. 构建 WebUI
|
||||||
|
2. 提交构建产物到 `static/admin/`
|
||||||
|
|
||||||
|
如果需要本地构建(测试用):
|
||||||
|
```bash
|
||||||
|
./scripts/build-webui.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ds2api/
|
||||||
|
├── app.py # FastAPI 应用入口
|
||||||
|
├── dev.py # 开发服务器
|
||||||
|
├── core/ # 核心模块
|
||||||
|
│ ├── auth.py # 账号认证与轮询
|
||||||
|
│ ├── config.py # 配置管理
|
||||||
|
│ ├── deepseek.py # DeepSeek API 调用
|
||||||
|
│ ├── models.py # 模型定义
|
||||||
|
│ ├── pow.py # PoW 计算
|
||||||
|
│ └── sse_parser.py # SSE 解析
|
||||||
|
├── routes/ # API 路由
|
||||||
|
│ ├── openai.py # OpenAI 兼容接口
|
||||||
|
│ ├── claude.py # Claude 兼容接口
|
||||||
|
│ ├── home.py # 首页路由
|
||||||
|
│ └── admin/ # 管理接口
|
||||||
|
├── webui/ # React WebUI 源码
|
||||||
|
├── static/admin/ # WebUI 构建产物(自动生成)
|
||||||
|
└── scripts/ # 辅助脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 问题反馈
|
||||||
|
|
||||||
|
- 使用 [GitHub Issues](https://github.com/CJackHwang/ds2api/issues) 报告问题
|
||||||
|
- 提供详细的复现步骤和日志信息
|
||||||
410
DEPLOY.en.md
Normal file
410
DEPLOY.en.md
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
# DS2API Deployment Guide
|
||||||
|
|
||||||
|
Language: [中文](DEPLOY.md) | [English](DEPLOY.en.md)
|
||||||
|
|
||||||
|
This document covers all supported DS2API deployment methods.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Vercel Deployment (Recommended)](#vercel-deployment-recommended)
|
||||||
|
- [Docker Deployment (Recommended)](#docker-deployment-recommended)
|
||||||
|
- [Local Development](#local-development)
|
||||||
|
- [Production Deployment](#production-deployment)
|
||||||
|
- [FAQ](#faq)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vercel Deployment (Recommended)
|
||||||
|
|
||||||
|
### One-click deployment
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api&env=DS2API_ADMIN_KEY&envDescription=Admin%20console%20access%20key%20%28required%29&envLink=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api%23environment-variables&project-name=ds2api&repository-name=ds2api)
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **Click the deploy button**
|
||||||
|
- Sign in to GitHub
|
||||||
|
- Authorize Vercel access
|
||||||
|
|
||||||
|
2. **Set environment variables**
|
||||||
|
- `DS2API_ADMIN_KEY`: Admin console password (**required**)
|
||||||
|
|
||||||
|
3. **Wait for deployment**
|
||||||
|
- Vercel builds and deploys automatically
|
||||||
|
- You will receive a deployment URL
|
||||||
|
|
||||||
|
4. **Configure accounts**
|
||||||
|
- Visit `https://your-project.vercel.app/admin`
|
||||||
|
- Log in with the admin key
|
||||||
|
- Add DeepSeek accounts
|
||||||
|
- Set custom API keys
|
||||||
|
|
||||||
|
5. **Sync configuration**
|
||||||
|
- Click "Sync to Vercel"
|
||||||
|
- The first sync requires a Vercel token and project ID
|
||||||
|
- After sync, the configuration is persisted
|
||||||
|
|
||||||
|
### Get Vercel credentials
|
||||||
|
|
||||||
|
**Vercel token**:
|
||||||
|
1. Visit https://vercel.com/account/tokens
|
||||||
|
2. Click "Create Token"
|
||||||
|
3. Set a name and expiration
|
||||||
|
4. Copy the token
|
||||||
|
|
||||||
|
**Project ID**:
|
||||||
|
1. Open your Vercel project
|
||||||
|
2. Go to Settings → General
|
||||||
|
3. Copy the "Project ID"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local Development
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- Node.js 18+ (WebUI development)
|
||||||
|
- pip
|
||||||
|
|
||||||
|
### Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repo
|
||||||
|
git clone https://github.com/CJackHwang/ds2api.git
|
||||||
|
cd ds2api
|
||||||
|
|
||||||
|
# 2. Install Python dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 3. Configure accounts
|
||||||
|
cp config.example.json config.json
|
||||||
|
# Edit config.json and fill in DeepSeek account info
|
||||||
|
|
||||||
|
# 4. Start the service
|
||||||
|
python dev.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"keys": ["my-api-key-1", "my-api-key-2"],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"email": "your-email@example.com",
|
||||||
|
"password": "your-password",
|
||||||
|
"token": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mobile": "12345678901",
|
||||||
|
"password": "your-password",
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- `keys`: Custom API keys for calling the service
|
||||||
|
- `accounts`: DeepSeek Web accounts
|
||||||
|
- Supports `email` or `mobile` login
|
||||||
|
- Leave `token` blank; it will be fetched automatically
|
||||||
|
|
||||||
|
### WebUI development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enter the WebUI directory
|
||||||
|
cd webui
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start the dev server
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The WebUI dev server runs on `http://localhost:5173` and proxies API requests to `http://localhost:5001`.
|
||||||
|
|
||||||
|
### WebUI build
|
||||||
|
|
||||||
|
Build artifacts are located in `static/admin/`.
|
||||||
|
|
||||||
|
**Automatic build (recommended)**:
|
||||||
|
- Vercel builds the WebUI during deployment (see `vercel.json` `buildCommand`)
|
||||||
|
- The GitHub Actions WebUI build workflow is disabled
|
||||||
|
- `static/admin/` build artifacts are no longer committed
|
||||||
|
|
||||||
|
**Manual build**:
|
||||||
|
```bash
|
||||||
|
# Option 1: use script
|
||||||
|
./scripts/build-webui.sh
|
||||||
|
|
||||||
|
# Option 2: run directly
|
||||||
|
cd webui
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Contributor note**: No manual build is required after modifying WebUI; Vercel deploys will build it automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Deployment (Recommended)
|
||||||
|
|
||||||
|
Docker uses a **non-invasive, decoupled design**:
|
||||||
|
- Dockerfile executes standard Python steps and avoids hardcoded project configs
|
||||||
|
- WebUI is built during image build (for non-Vercel deployments)
|
||||||
|
- Configuration lives in environment variables and `.env`
|
||||||
|
- **Rebuild the image to update code without touching Docker config**
|
||||||
|
|
||||||
|
### Quick start (Docker Compose)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copy the environment template
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with DS2API_ADMIN_KEY and DS2API_CONFIG_JSON
|
||||||
|
|
||||||
|
# 2. Start the service
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 3. Check logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# 4. Rebuild after code updates
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mount a config file
|
||||||
|
|
||||||
|
To use `config.json` instead of environment variables:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
ds2api:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5001:5001"
|
||||||
|
environment:
|
||||||
|
- DS2API_ADMIN_KEY=your-admin-key
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/app/config.json:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker CLI deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the image
|
||||||
|
docker build -t ds2api:latest .
|
||||||
|
|
||||||
|
# Run with env variables
|
||||||
|
docker run -d \
|
||||||
|
--name ds2api \
|
||||||
|
-p 5001:5001 \
|
||||||
|
-e DS2API_ADMIN_KEY=your-admin-key \
|
||||||
|
-e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \
|
||||||
|
--restart unless-stopped \
|
||||||
|
ds2api:latest
|
||||||
|
|
||||||
|
# Or mount a config file
|
||||||
|
docker run -d \
|
||||||
|
--name ds2api \
|
||||||
|
-p 5001:5001 \
|
||||||
|
-e DS2API_ADMIN_KEY=your-admin-key \
|
||||||
|
-v $(pwd)/config.json:/app/config.json:ro \
|
||||||
|
--restart unless-stopped \
|
||||||
|
ds2api:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development mode (hot reload)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use the dev compose file to enable hot reload
|
||||||
|
docker-compose -f docker-compose.dev.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
Development mode:
|
||||||
|
- Source code is mounted into the container
|
||||||
|
- Log level set to DEBUG
|
||||||
|
- Reads local `config.json`
|
||||||
|
|
||||||
|
### Maintenance commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f ds2api
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Full rebuild (clear cache)
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build --no-cache
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Using systemd (Linux)
|
||||||
|
|
||||||
|
1. **Create the service file**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/systemd/system/ds2api.service
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=DS2API Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=www-data
|
||||||
|
WorkingDirectory=/opt/ds2api
|
||||||
|
ExecStart=/usr/bin/python3 app.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
Environment=PORT=5001
|
||||||
|
Environment=DS2API_ADMIN_KEY=your-admin-key
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start the service**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable ds2api
|
||||||
|
sudo systemctl start ds2api
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check status**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl status ds2api
|
||||||
|
sudo journalctl -u ds2api -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nginx reverse proxy
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name api.yourdomain.com;
|
||||||
|
|
||||||
|
# SSL configuration (recommended)
|
||||||
|
# listen 443 ssl http2;
|
||||||
|
# ssl_certificate /path/to/cert.pem;
|
||||||
|
# ssl_certificate_key /path/to/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Disable buffering for SSE
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
|
||||||
|
# Connection settings
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# SSE timeouts
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
|
||||||
|
# Chunked transfer
|
||||||
|
chunked_transfer_encoding on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 120;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Q: What if account validation fails?
|
||||||
|
|
||||||
|
**A**: Check the following:
|
||||||
|
1. Confirm the DeepSeek account password is correct
|
||||||
|
2. Ensure the account is not banned or requires verification
|
||||||
|
3. Log in once in a browser
|
||||||
|
4. Check logs for detailed errors
|
||||||
|
|
||||||
|
### Q: Streaming responses disconnect?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
1. Check Nginx / reverse proxy config and ensure `proxy_buffering` is off
|
||||||
|
2. Increase `proxy_read_timeout`
|
||||||
|
3. Verify network stability
|
||||||
|
|
||||||
|
### Q: Configuration lost after Vercel deploy?
|
||||||
|
|
||||||
|
**A**:
|
||||||
|
1. Ensure you clicked "Sync to Vercel"
|
||||||
|
2. Verify the Vercel token is valid and unexpired
|
||||||
|
3. Ensure the project ID is correct
|
||||||
|
|
||||||
|
### Q: How to update to the latest version?
|
||||||
|
|
||||||
|
**Local deployment**:
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
pip install -r requirements.txt
|
||||||
|
# Restart the service
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker deployment**:
|
||||||
|
```bash
|
||||||
|
# Pull the latest code
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Rebuild and start (Docker config unchanged)
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vercel deployment**:
|
||||||
|
- The project auto-syncs from GitHub
|
||||||
|
- Or trigger a redeploy in the Vercel console
|
||||||
|
|
||||||
|
### Q: How do I view logs?
|
||||||
|
|
||||||
|
**Local dev**:
|
||||||
|
```bash
|
||||||
|
# Set log level
|
||||||
|
export LOG_LEVEL=DEBUG
|
||||||
|
python dev.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vercel**:
|
||||||
|
- Vercel console → Project → Deployments → Logs
|
||||||
|
|
||||||
|
### Q: Token counting is inaccurate?
|
||||||
|
|
||||||
|
**A**: DS2API uses a heuristic estimate (characters / 4). The official OpenAI tokenizer may differ, so treat it as a reference only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Get Help
|
||||||
|
|
||||||
|
- **GitHub Issues**: https://github.com/CJackHwang/ds2api/issues
|
||||||
|
- **Docs**: https://github.com/CJackHwang/ds2api
|
||||||
183
DEPLOY.md
183
DEPLOY.md
@@ -1,5 +1,7 @@
|
|||||||
# DS2API 部署指南
|
# DS2API 部署指南
|
||||||
|
|
||||||
|
语言 / Language: [中文](DEPLOY.md) | [English](DEPLOY.en.md)
|
||||||
|
|
||||||
本文档详细介绍 DS2API 的各种部署方式。
|
本文档详细介绍 DS2API 的各种部署方式。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -7,6 +9,7 @@
|
|||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
- [Vercel 部署(推荐)](#vercel-部署推荐)
|
- [Vercel 部署(推荐)](#vercel-部署推荐)
|
||||||
|
- [Docker 部署(推荐)](#docker-部署推荐)
|
||||||
- [本地开发](#本地开发)
|
- [本地开发](#本地开发)
|
||||||
- [生产环境部署](#生产环境部署)
|
- [生产环境部署](#生产环境部署)
|
||||||
- [常见问题](#常见问题)
|
- [常见问题](#常见问题)
|
||||||
@@ -125,6 +128,131 @@ npm run dev
|
|||||||
|
|
||||||
WebUI 开发服务器会启动在 `http://localhost:5173`,并自动代理 API 请求到后端 `http://localhost:5001`。
|
WebUI 开发服务器会启动在 `http://localhost:5173`,并自动代理 API 请求到后端 `http://localhost:5001`。
|
||||||
|
|
||||||
|
### WebUI 构建
|
||||||
|
|
||||||
|
WebUI 构建产物位于 `static/admin/` 目录。
|
||||||
|
|
||||||
|
**自动构建(推荐)**:
|
||||||
|
- 当前由 Vercel 在部署时执行 WebUI 构建(见 `vercel.json` 的 `buildCommand`)
|
||||||
|
- GitHub Actions 的 WebUI 自动构建流程已关闭
|
||||||
|
- `static/admin/` 构建产物不再提交到仓库
|
||||||
|
|
||||||
|
**手动构建**:
|
||||||
|
```bash
|
||||||
|
# 方式1:使用脚本
|
||||||
|
./scripts/build-webui.sh
|
||||||
|
|
||||||
|
# 方式2:直接执行
|
||||||
|
cd webui
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
> **贡献者注意**:修改 WebUI 后无需手动构建,Vercel 部署会自动构建。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker 部署(推荐)
|
||||||
|
|
||||||
|
Docker 部署采用**零侵入、解耦设计**:
|
||||||
|
- Dockerfile 仅执行标准 Python 项目操作,不硬编码任何项目特定配置
|
||||||
|
- 构建镜像时会一并构建 WebUI(便于非 Vercel 部署直接访问管理面板)
|
||||||
|
- 所有配置通过环境变量和 `.env` 文件管理
|
||||||
|
- **主代码更新时只需重新构建镜像,无需修改 Docker 配置**
|
||||||
|
|
||||||
|
### 快速开始(Docker Compose)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 复制环境变量模板
|
||||||
|
cp .env.example .env
|
||||||
|
# 编辑 .env,填写 DS2API_ADMIN_KEY 和 DS2API_CONFIG_JSON
|
||||||
|
|
||||||
|
# 2. 启动服务
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 3. 查看日志
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# 4. 主代码更新后重新构建
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置文件挂载方式
|
||||||
|
|
||||||
|
如需使用 `config.json` 而非环境变量:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
ds2api:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5001:5001"
|
||||||
|
environment:
|
||||||
|
- DS2API_ADMIN_KEY=your-admin-key
|
||||||
|
volumes:
|
||||||
|
- ./config.json:/app/config.json:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 命令行部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建镜像
|
||||||
|
docker build -t ds2api:latest .
|
||||||
|
|
||||||
|
# 使用环境变量运行
|
||||||
|
docker run -d \
|
||||||
|
--name ds2api \
|
||||||
|
-p 5001:5001 \
|
||||||
|
-e DS2API_ADMIN_KEY=your-admin-key \
|
||||||
|
-e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \
|
||||||
|
--restart unless-stopped \
|
||||||
|
ds2api:latest
|
||||||
|
|
||||||
|
# 或使用配置文件挂载
|
||||||
|
docker run -d \
|
||||||
|
--name ds2api \
|
||||||
|
-p 5001:5001 \
|
||||||
|
-e DS2API_ADMIN_KEY=your-admin-key \
|
||||||
|
-v $(pwd)/config.json:/app/config.json:ro \
|
||||||
|
--restart unless-stopped \
|
||||||
|
ds2api:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发模式(热重载)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用开发配置启动,代码修改实时生效
|
||||||
|
docker-compose -f docker-compose.dev.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
开发模式特性:
|
||||||
|
- 源代码挂载到容器,修改即时生效
|
||||||
|
- 日志级别设为 DEBUG
|
||||||
|
- 自动读取本地 `config.json`
|
||||||
|
|
||||||
|
### 维护命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看容器状态
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose logs -f ds2api
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 完全重建(清除缓存)
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build --no-cache
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 生产环境部署
|
## 生产环境部署
|
||||||
@@ -210,52 +338,6 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker 部署(可选)
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# Dockerfile
|
|
||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
EXPOSE 5001
|
|
||||||
CMD ["python", "app.py"]
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 构建镜像
|
|
||||||
docker build -t ds2api .
|
|
||||||
|
|
||||||
# 运行容器
|
|
||||||
docker run -d \
|
|
||||||
--name ds2api \
|
|
||||||
-p 5001:5001 \
|
|
||||||
-e DS2API_ADMIN_KEY=your-admin-key \
|
|
||||||
-e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \
|
|
||||||
ds2api
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
ds2api:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "5001:5001"
|
|
||||||
environment:
|
|
||||||
- DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY}
|
|
||||||
- DS2API_CONFIG_JSON=${DS2API_CONFIG_JSON}
|
|
||||||
restart: unless-stopped
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
@@ -291,6 +373,15 @@ pip install -r requirements.txt
|
|||||||
# 重启服务
|
# 重启服务
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Docker 部署**:
|
||||||
|
```bash
|
||||||
|
# 拉取最新代码
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 重新构建并启动(无需修改 Docker 配置)
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
**Vercel 部署**:
|
**Vercel 部署**:
|
||||||
- 项目会自动从 GitHub 同步更新
|
- 项目会自动从 GitHub 同步更新
|
||||||
- 或在 Vercel 控制台手动触发重新部署
|
- 或在 Vercel 控制台手动触发重新部署
|
||||||
|
|||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# DS2API Docker 镜像
|
||||||
|
# 采用极简、零侵入设计,所有配置通过环境变量传递
|
||||||
|
# 主代码更新时只需重新构建镜像,无需修改 Dockerfile
|
||||||
|
|
||||||
|
FROM node:20 AS webui-builder
|
||||||
|
|
||||||
|
WORKDIR /app/webui
|
||||||
|
|
||||||
|
COPY webui/package.json webui/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY webui ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 安装依赖(利用 Docker 缓存层)
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# 复制整个项目(保留原始目录结构)
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 拷贝 WebUI 构建产物(非 Vercel / Docker 部署可直接使用)
|
||||||
|
COPY --from=webui-builder /app/static/admin /app/static/admin
|
||||||
|
|
||||||
|
# 暴露服务端口
|
||||||
|
EXPOSE 5001
|
||||||
|
|
||||||
|
# 启动命令(依赖项目自身的启动逻辑)
|
||||||
|
CMD ["python", "app.py"]
|
||||||
34
README.MD
34
README.MD
@@ -4,15 +4,26 @@
|
|||||||

|

|
||||||

|

|
||||||
[](version.txt)
|
[](version.txt)
|
||||||
|
[](DEPLOY.md#docker-部署推荐)
|
||||||
|
|
||||||
|
语言 / Language: [中文](README.MD) | [English](README.en.md)
|
||||||
|
|
||||||
将 DeepSeek 免费对话版转换为 **OpenAI & Claude 兼容 API**,支持多账号轮询、自动 Token 刷新、可视化管理界面。
|
将 DeepSeek 免费对话版转换为 **OpenAI & Claude 兼容 API**,支持多账号轮询、自动 Token 刷新、可视化管理界面。
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## ✨ 特性
|
## ✨ 特性
|
||||||
|
|
||||||
- 🔄 **双协议兼容** - 同时支持 OpenAI 和 Claude (Anthropic) API 格式
|
- 🔄 **双协议兼容** - 同时支持 OpenAI 和 Claude (Anthropic) API 格式
|
||||||
- 🚀 **多账号轮询** - Round-Robin 负载均衡,支持高并发场景
|
- 🚀 **多账号轮询** - Round-Robin 负载均衡,支持高并发场景
|
||||||
- 🔐 **Token 自动刷新** - 过期自动重新登录,无需手动维护
|
- 🔐 **Token 自动刷新** - 过期自动重新登录,无需手动维护
|
||||||
- 🌐 **WebUI 管理** - 可视化添加账号、测试 API、同步 Vercel 配置
|
- 🌐 **WebUI 管理** - 可视化添加账号、测试 API、同步 Vercel 配置
|
||||||
|
- 🌍 **多语言切换** - WebUI 内置中英双语,可随时切换
|
||||||
- 🔍 **联网搜索** - 支持 DeepSeek 原生搜索增强模式
|
- 🔍 **联网搜索** - 支持 DeepSeek 原生搜索增强模式
|
||||||
- 🧠 **深度思考** - 支持推理模式,输出思考过程
|
- 🧠 **深度思考** - 支持推理模式,输出思考过程
|
||||||
- 🛠️ **工具调用** - 兼容 OpenAI Function Calling 格式
|
- 🛠️ **工具调用** - 兼容 OpenAI Function Calling 格式
|
||||||
@@ -184,17 +195,26 @@ location / {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker 部署(可选)
|
### 方式三:Docker 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 使用环境变量配置
|
# 1. 克隆仓库并进入目录
|
||||||
docker run -d \
|
git clone https://github.com/CJackHwang/ds2api.git
|
||||||
-p 5001:5001 \
|
cd ds2api
|
||||||
-e DS2API_ADMIN_KEY=your-admin-key \
|
|
||||||
-e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \
|
# 2. 配置环境变量
|
||||||
ds2api
|
cp .env.example .env
|
||||||
|
# 编辑 .env,填写 DS2API_ADMIN_KEY 和 DS2API_CONFIG_JSON
|
||||||
|
|
||||||
|
# 3. 启动服务
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 4. 查看日志
|
||||||
|
docker-compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Docker 优势**:零侵入设计,主代码更新只需 `docker-compose up -d --build`,无需修改 Docker 配置。详见 [DEPLOY.md](DEPLOY.md#docker-部署推荐)。
|
||||||
|
|
||||||
## ⚠️ 免责声明
|
## ⚠️ 免责声明
|
||||||
|
|
||||||
**本项目基于逆向工程实现,服务稳定性无法保证。**
|
**本项目基于逆向工程实现,服务稳定性无法保证。**
|
||||||
|
|||||||
233
README.en.md
Normal file
233
README.en.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# DS2API
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|

|
||||||
|

|
||||||
|
[](version.txt)
|
||||||
|
[](DEPLOY.md#docker-deployment-recommended)
|
||||||
|
|
||||||
|
Language: [中文](README.MD) | [English](README.en.md)
|
||||||
|
|
||||||
|
Convert DeepSeek Web into an **OpenAI & Claude compatible API**, with multi-account rotation, automatic token refresh, and a visual admin console.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- 🔄 **Dual-protocol support** - OpenAI and Claude (Anthropic) compatible APIs
|
||||||
|
- 🚀 **Multi-account rotation** - Round-robin load balancing for high concurrency
|
||||||
|
- 🔐 **Automatic token refresh** - Re-auth on expiry without manual maintenance
|
||||||
|
- 🌐 **WebUI management** - Add accounts, test APIs, and sync Vercel settings visually
|
||||||
|
- 🌍 **Language toggle** - Built-in Chinese and English UI switcher
|
||||||
|
- 🔍 **Web search** - DeepSeek native search enhancement mode
|
||||||
|
- 🧠 **Deep reasoning** - Reasoning mode with trace output
|
||||||
|
- 🛠️ **Tool calling** - OpenAI Function Calling compatible
|
||||||
|
- ☁️ **One-click Vercel deploy** - No server required
|
||||||
|
|
||||||
|
## 📋 Model Support
|
||||||
|
|
||||||
|
### OpenAI compatible endpoint (`/v1/chat/completions`)
|
||||||
|
|
||||||
|
| Model | Reasoning | Search | Notes |
|
||||||
|
|-----|:--------:|:------:|------|
|
||||||
|
| `deepseek-chat` | ❌ | ❌ | Standard chat |
|
||||||
|
| `deepseek-reasoner` | ✅ | ❌ | Reasoning (shows trace) |
|
||||||
|
| `deepseek-chat-search` | ❌ | ✅ | Web search mode |
|
||||||
|
| `deepseek-reasoner-search` | ✅ | ✅ | Reasoning + search |
|
||||||
|
|
||||||
|
### Claude compatible endpoint (`/anthropic/v1/messages`)
|
||||||
|
|
||||||
|
| Model | Notes |
|
||||||
|
|-----|------|
|
||||||
|
| `claude-sonnet-4-20250514` | Maps to deepseek-chat (standard) |
|
||||||
|
| `claude-sonnet-4-20250514-fast` | Maps to deepseek-chat (fast) |
|
||||||
|
| `claude-sonnet-4-20250514-slow` | Maps to deepseek-reasoner (reasoning) |
|
||||||
|
|
||||||
|
> **Tip**: The Claude endpoint actually calls DeepSeek and returns Anthropic-format responses.
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Option 1: Vercel deployment (recommended)
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api&env=DS2API_ADMIN_KEY&envDescription=Admin%20console%20access%20key%20%28required%29&envLink=https%3A%2F%2Fgithub.com%2FCJackHwang%2Fds2api%23environment-variables&project-name=ds2api&repository-name=ds2api)
|
||||||
|
|
||||||
|
1. Click the button above and set `DS2API_ADMIN_KEY`
|
||||||
|
2. After deployment, visit `/admin`
|
||||||
|
3. Add DeepSeek accounts and custom API keys
|
||||||
|
4. Click "Sync to Vercel" to persist configuration
|
||||||
|
|
||||||
|
> **First sync validates accounts and stores tokens automatically.**
|
||||||
|
|
||||||
|
### Option 2: Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repo
|
||||||
|
git clone https://github.com/CJackHwang/ds2api.git
|
||||||
|
cd ds2api
|
||||||
|
|
||||||
|
# 2. Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 3. Configure accounts
|
||||||
|
cp config.example.json config.json
|
||||||
|
# Edit config.json to add DeepSeek account info
|
||||||
|
|
||||||
|
# 4. Start the service
|
||||||
|
python dev.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit `http://localhost:5001` after startup.
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
| Variable | Description | Required |
|
||||||
|
|-----|------|:----:|
|
||||||
|
| `DS2API_ADMIN_KEY` | Admin console password | Required on Vercel |
|
||||||
|
| `DS2API_CONFIG_JSON` | Config JSON or Base64 | Optional |
|
||||||
|
| `VERCEL_TOKEN` | Vercel API token (for sync) | Optional |
|
||||||
|
| `VERCEL_PROJECT_ID` | Vercel project ID | Optional |
|
||||||
|
| `PORT` | Service port (default 5001) | Optional |
|
||||||
|
|
||||||
|
### Config file format (`config.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"keys": ["your-api-key-1", "your-api-key-2"],
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"password": "your-password",
|
||||||
|
"token": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mobile": "12345678901",
|
||||||
|
"password": "your-password",
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Notes**:
|
||||||
|
> - `keys`: Custom API keys for calling this service
|
||||||
|
> - `accounts`: DeepSeek Web accounts (email or mobile)
|
||||||
|
> - `token`: Leave blank; DS2API will fetch and refresh automatically
|
||||||
|
|
||||||
|
## 📡 API Usage
|
||||||
|
|
||||||
|
See **[API.md](API.md)** for full API documentation.
|
||||||
|
|
||||||
|
### Quick examples
|
||||||
|
|
||||||
|
**List models**:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5001/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
**OpenAI-compatible call**:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5001/v1/chat/completions \
|
||||||
|
-H "Authorization: Bearer your-api-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "deepseek-chat",
|
||||||
|
"messages": [{"role": "user", "content": "Hello"}],
|
||||||
|
"stream": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claude-compatible call**:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:5001/anthropic/v1/messages \
|
||||||
|
-H "x-api-key: your-api-key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "anthropic-version: 2023-06-01" \
|
||||||
|
-d '{
|
||||||
|
"model": "claude-sonnet-4-20250514",
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"messages": [{"role": "user", "content": "Hello"}]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python SDK usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="your-api-key",
|
||||||
|
base_url="http://localhost:5001/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="deepseek-reasoner",
|
||||||
|
messages=[{"role": "user", "content": "Explain quantum entanglement"}],
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for chunk in response:
|
||||||
|
if chunk.choices[0].delta.content:
|
||||||
|
print(chunk.choices[0].delta.content, end="")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Deployment Notes
|
||||||
|
|
||||||
|
### Nginx reverse proxy
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:5001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
chunked_transfer_encoding on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 120;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Docker deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repo and enter the directory
|
||||||
|
git clone https://github.com/CJackHwang/ds2api.git
|
||||||
|
cd ds2api
|
||||||
|
|
||||||
|
# 2. Configure environment variables
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env and fill in DS2API_ADMIN_KEY and DS2API_CONFIG_JSON
|
||||||
|
|
||||||
|
# 3. Start the service
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 4. Check logs
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Docker advantage**: Zero-intrusion design; update the main code with `docker-compose up -d --build` without changing Docker configuration. See [DEPLOY.md](DEPLOY.md#docker-deployment-recommended).
|
||||||
|
|
||||||
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
**This project is based on reverse engineering and stability is not guaranteed.**
|
||||||
|
|
||||||
|
- For learning and research only. **No commercial use or public service is allowed.**
|
||||||
|
- For production, use the official [DeepSeek API](https://platform.deepseek.com/)
|
||||||
|
- You assume all risks from using this project
|
||||||
|
|
||||||
|
## 📜 Acknowledgements
|
||||||
|
|
||||||
|
This project is based on the following open-source projects:
|
||||||
|
|
||||||
|
- [iidamie/deepseek2api](https://github.com/iidamie/deepseek2api)
|
||||||
|
- [LLM-Red-Team/deepseek-free-api](https://github.com/LLM-Red-Team/deepseek-free-api)
|
||||||
|
|
||||||
|
## 📊 Star History
|
||||||
|
|
||||||
|
[](https://star-history.com/#CJackHwang/ds2api&Date)
|
||||||
45
docker-compose.dev.yml
Normal file
45
docker-compose.dev.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# DS2API 开发环境配置
|
||||||
|
# 特性:
|
||||||
|
# - 源代码挂载(热重载)
|
||||||
|
# - 调试日志级别
|
||||||
|
# - 自动重启
|
||||||
|
#
|
||||||
|
# 使用说明:
|
||||||
|
# docker-compose -f docker-compose.dev.yml up
|
||||||
|
|
||||||
|
services:
|
||||||
|
ds2api:
|
||||||
|
build: .
|
||||||
|
image: ds2api:dev
|
||||||
|
container_name: ds2api-dev
|
||||||
|
command: [
|
||||||
|
"uvicorn",
|
||||||
|
"app:app",
|
||||||
|
"--host",
|
||||||
|
"0.0.0.0",
|
||||||
|
"--port",
|
||||||
|
"5001",
|
||||||
|
"--reload",
|
||||||
|
"--reload-dir",
|
||||||
|
"/app",
|
||||||
|
"--log-level",
|
||||||
|
"debug"
|
||||||
|
]
|
||||||
|
ports:
|
||||||
|
- "${PORT:-5001}:5001"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
- LOG_LEVEL=DEBUG
|
||||||
|
volumes:
|
||||||
|
# 源代码挂载(开发时实时生效)
|
||||||
|
- ./app.py:/app/app.py:ro
|
||||||
|
- ./core:/app/core:ro
|
||||||
|
- ./routes:/app/routes:ro
|
||||||
|
- ./static:/app/static:ro
|
||||||
|
# 配置文件挂载(便于本地修改)
|
||||||
|
- ./config.json:/app/config.json
|
||||||
|
restart: "no"
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# DS2API 生产环境配置
|
||||||
|
# 使用说明:
|
||||||
|
# 1. 复制 .env.example 为 .env 并填写配置
|
||||||
|
# 2. docker-compose up -d
|
||||||
|
# 3. 主代码更新后:docker-compose up -d --build
|
||||||
|
#
|
||||||
|
# 设计原则:
|
||||||
|
# - 零侵入:所有项目配置通过 .env 文件传递
|
||||||
|
# - 易维护:主代码更新只需重新构建镜像
|
||||||
|
|
||||||
|
services:
|
||||||
|
ds2api:
|
||||||
|
build: .
|
||||||
|
image: ds2api:latest
|
||||||
|
container_name: ds2api
|
||||||
|
ports:
|
||||||
|
- "${PORT:-5001}:5001"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
# 确保容器内使用正确的主机绑定
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5001/v1/models')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
# DS2API 依赖
|
# DS2API 依赖
|
||||||
# 安装命令: pip install -r requirements.txt
|
# 安装命令: pip install -r requirements.txt
|
||||||
|
# Python 版本要求: >=3.9
|
||||||
|
|
||||||
# Web 框架
|
# ===== Web 框架 =====
|
||||||
fastapi>=0.110.0,<1.0.0
|
fastapi>=0.110.0,<1.0.0
|
||||||
uvicorn[standard]>=0.24.0,<1.0.0
|
uvicorn[standard]>=0.24.0,<1.0.0
|
||||||
|
|
||||||
# HTTP 客户端
|
# ===== HTTP 客户端 =====
|
||||||
|
# curl_cffi: 支持 TLS 指纹模拟,绕过 Cloudflare 等防护
|
||||||
curl_cffi>=0.7.0
|
curl_cffi>=0.7.0
|
||||||
|
# httpx: 异步 HTTP 客户端,用于 Vercel API 调用
|
||||||
httpx>=0.25.0
|
httpx>=0.25.0
|
||||||
|
|
||||||
# 模板引擎
|
# ===== 模板引擎 =====
|
||||||
jinja2>=3.1.0,<4.0.0
|
jinja2>=3.1.0,<4.0.0
|
||||||
|
|
||||||
# Tokenizer(用于 token 计数)
|
# ===== Tokenizer =====
|
||||||
|
# 用于 token 计数(可选,不安装则使用估算方式)
|
||||||
transformers>=4.39.0,<5.0.0
|
transformers>=4.39.0,<5.0.0
|
||||||
|
|
||||||
# WASM 运行时(用于 PoW 计算)
|
# ===== WASM 运行时 =====
|
||||||
|
# 用于 DeepSeek PoW (Proof of Work) 计算
|
||||||
wasmtime>=14.0.0
|
wasmtime>=14.0.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Admin 账号管理模块 - 账号验证和测试"""
|
"""Admin 账号管理模块 - 账号测试与导入"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
@@ -24,93 +24,6 @@ from .auth import verify_admin
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# 账号验证
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
async def validate_single_account(account: dict) -> dict:
|
|
||||||
"""验证单个账号的有效性"""
|
|
||||||
acc_id = get_account_identifier(account)
|
|
||||||
result = {
|
|
||||||
"account": acc_id,
|
|
||||||
"valid": False,
|
|
||||||
"has_token": bool(account.get("token", "").strip()),
|
|
||||||
"message": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
if result["has_token"]:
|
|
||||||
result["valid"] = True
|
|
||||||
result["message"] = "已有有效 token"
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
login_deepseek_via_account(account)
|
|
||||||
result["valid"] = True
|
|
||||||
result["has_token"] = True
|
|
||||||
result["message"] = "登录成功"
|
|
||||||
except Exception as e:
|
|
||||||
result["valid"] = False
|
|
||||||
result["message"] = f"登录失败: {str(e)}"
|
|
||||||
except Exception as e:
|
|
||||||
result["message"] = f"验证出错: {str(e)}"
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/accounts/validate")
|
|
||||||
async def validate_account(request: Request, _: bool = Depends(verify_admin)):
|
|
||||||
"""验证单个账号"""
|
|
||||||
data = await request.json()
|
|
||||||
identifier = data.get("identifier", "").strip()
|
|
||||||
|
|
||||||
if not identifier:
|
|
||||||
raise HTTPException(status_code=400, detail="需要账号标识(email 或 mobile)")
|
|
||||||
|
|
||||||
account = None
|
|
||||||
for acc in CONFIG.get("accounts", []):
|
|
||||||
if acc.get("email") == identifier or acc.get("mobile") == identifier:
|
|
||||||
account = acc
|
|
||||||
break
|
|
||||||
|
|
||||||
if not account:
|
|
||||||
raise HTTPException(status_code=404, detail="账号不存在")
|
|
||||||
|
|
||||||
result = await validate_single_account(account)
|
|
||||||
|
|
||||||
if result["valid"] and result["has_token"]:
|
|
||||||
save_config(CONFIG)
|
|
||||||
|
|
||||||
return JSONResponse(content=result)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/accounts/validate-all")
|
|
||||||
async def validate_all_accounts(_: bool = Depends(verify_admin)):
|
|
||||||
"""批量验证所有账号"""
|
|
||||||
accounts = CONFIG.get("accounts", [])
|
|
||||||
if not accounts:
|
|
||||||
return JSONResponse(content={
|
|
||||||
"total": 0, "valid": 0, "invalid": 0, "results": [],
|
|
||||||
})
|
|
||||||
|
|
||||||
results = []
|
|
||||||
valid_count = 0
|
|
||||||
|
|
||||||
for acc in accounts:
|
|
||||||
result = await validate_single_account(acc)
|
|
||||||
results.append(result)
|
|
||||||
if result["valid"]:
|
|
||||||
valid_count += 1
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
save_config(CONFIG)
|
|
||||||
|
|
||||||
return JSONResponse(content={
|
|
||||||
"total": len(accounts),
|
|
||||||
"valid": valid_count,
|
|
||||||
"invalid": len(accounts) - valid_count,
|
|
||||||
"results": results,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# 账号 API 测试
|
# 账号 API 测试
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -134,38 +47,68 @@ async def test_account_api(account: dict, model: str = "deepseek-chat", message:
|
|||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
|
def _is_token_invalid(status_code: int, data: dict) -> bool:
|
||||||
|
msg = (data.get("msg") or data.get("message") or "").lower()
|
||||||
|
code = data.get("code")
|
||||||
|
return status_code in {401, 403} or code in {40001, 40002, 40003} or "token" in msg or "unauthorized" in msg
|
||||||
|
|
||||||
|
def _create_session(token: str) -> dict:
|
||||||
|
headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"}
|
||||||
|
try:
|
||||||
|
session_resp = cffi_requests.post(
|
||||||
|
DEEPSEEK_CREATE_SESSION_URL,
|
||||||
|
headers=headers,
|
||||||
|
json={"agent": "chat"},
|
||||||
|
impersonate="safari15_3",
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "message": f"请求异常: {e}", "status_code": 0, "data": {}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
session_data = session_resp.json()
|
||||||
|
except Exception:
|
||||||
|
session_data = {}
|
||||||
|
finally:
|
||||||
|
session_resp.close()
|
||||||
|
|
||||||
|
if session_resp.status_code == 200 and session_data.get("code") == 0:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"session_id": session_data.get("data", {}).get("biz_data", {}).get("id"),
|
||||||
|
"status_code": session_resp.status_code,
|
||||||
|
"data": session_data,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": session_data.get("msg") or f"HTTP {session_resp.status_code}",
|
||||||
|
"status_code": session_resp.status_code,
|
||||||
|
"data": session_data,
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = account.get("token", "").strip()
|
token = account.get("token", "").strip()
|
||||||
if not token:
|
session_result = None
|
||||||
|
if token:
|
||||||
|
session_result = _create_session(token)
|
||||||
|
|
||||||
|
if not token or (session_result and not session_result["success"] and _is_token_invalid(session_result["status_code"], session_result["data"])):
|
||||||
try:
|
try:
|
||||||
|
account["token"] = ""
|
||||||
login_deepseek_via_account(account)
|
login_deepseek_via_account(account)
|
||||||
token = account.get("token", "")
|
token = account.get("token", "")
|
||||||
|
session_result = _create_session(token)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result["message"] = f"登录失败: {str(e)}"
|
result["message"] = f"登录失败: {str(e)}"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
if not session_result or not session_result["success"]:
|
||||||
|
result["message"] = f"创建会话失败: {session_result['message'] if session_result else 'Unknown error'}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
session_id = session_result["session_id"]
|
||||||
headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"}
|
headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
session_resp = cffi_requests.post(
|
|
||||||
DEEPSEEK_CREATE_SESSION_URL,
|
|
||||||
headers=headers,
|
|
||||||
json={"agent": "chat"},
|
|
||||||
impersonate="safari15_3",
|
|
||||||
timeout=15,
|
|
||||||
)
|
|
||||||
|
|
||||||
if session_resp.status_code != 200:
|
|
||||||
result["message"] = f"创建会话失败: HTTP {session_resp.status_code}"
|
|
||||||
return result
|
|
||||||
|
|
||||||
session_data = session_resp.json()
|
|
||||||
if session_data.get("code") != 0:
|
|
||||||
result["message"] = f"创建会话失败: {session_data.get('msg', 'Unknown error')}"
|
|
||||||
account["token"] = ""
|
|
||||||
return result
|
|
||||||
|
|
||||||
session_id = session_data.get("data", {}).get("biz_data", {}).get("id")
|
|
||||||
|
|
||||||
if not message.strip():
|
if not message.strip():
|
||||||
result["success"] = True
|
result["success"] = True
|
||||||
result["message"] = "API 测试成功(仅会话创建)"
|
result["message"] = "API 测试成功(仅会话创建)"
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ WELCOME_HTML = """<!DOCTYPE html>
|
|||||||
<div class="feature-card">
|
<div class="feature-card">
|
||||||
<span class="feature-icon">🧠</span>
|
<span class="feature-icon">🧠</span>
|
||||||
<h3>深度思考</h3>
|
<h3>深度思考</h3>
|
||||||
<p>完整支持 DeepSeek-R1 推理过程输出,让思考可见。</p>
|
<p>完整支持 推理过程输出,让思考可见。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-card">
|
<div class="feature-card">
|
||||||
<span class="feature-icon">🔍</span>
|
<span class="feature-icon">🔍</span>
|
||||||
@@ -263,7 +263,7 @@ WELCOME_HTML = """<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2024 DS2API Project. Designed for flexibility & performance.</p>
|
<p>© 2026 DS2API Project. Designed for flexibility & performance.</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@@ -290,12 +290,19 @@ async def webui(request: Request, path: str = ""):
|
|||||||
if path and "." in path:
|
if path and "." in path:
|
||||||
file_path = os.path.join(STATIC_ADMIN_DIR, path)
|
file_path = os.path.join(STATIC_ADMIN_DIR, path)
|
||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
return FileResponse(file_path)
|
cache_control = "public, max-age=31536000, immutable"
|
||||||
|
if path.startswith("assets/"):
|
||||||
|
headers = {"Cache-Control": cache_control}
|
||||||
|
else:
|
||||||
|
headers = {"Cache-Control": "no-store, must-revalidate"}
|
||||||
|
return FileResponse(file_path, headers=headers)
|
||||||
return HTMLResponse(content="Not Found", status_code=404)
|
return HTMLResponse(content="Not Found", status_code=404)
|
||||||
|
|
||||||
# 否则返回 index.html(SPA 路由)
|
# 否则返回 index.html(SPA 路由)
|
||||||
index_path = os.path.join(STATIC_ADMIN_DIR, "index.html")
|
index_path = os.path.join(STATIC_ADMIN_DIR, "index.html")
|
||||||
if os.path.isfile(index_path):
|
if os.path.isfile(index_path):
|
||||||
return FileResponse(index_path)
|
headers = {"Cache-Control": "no-store, must-revalidate"}
|
||||||
|
return FileResponse(index_path, headers=headers)
|
||||||
|
|
||||||
return HTMLResponse(content="index.html not found", status_code=404)
|
return HTMLResponse(content="index.html not found", status_code=404)
|
||||||
|
|
||||||
|
|||||||
165
routes/openai.py
165
routes/openai.py
@@ -453,107 +453,84 @@ IMPORTANT: If calling tools, output ONLY the JSON. The response must start with
|
|||||||
|
|
||||||
def collect_data():
|
def collect_data():
|
||||||
nonlocal result
|
nonlocal result
|
||||||
ptype = "text"
|
current_fragment_type = "thinking" if thinking_enabled else "text"
|
||||||
try:
|
try:
|
||||||
for raw_line in deepseek_resp.iter_lines():
|
for raw_line in deepseek_resp.iter_lines():
|
||||||
try:
|
chunk = parse_deepseek_sse_line(raw_line)
|
||||||
line = raw_line.decode("utf-8")
|
if not chunk:
|
||||||
except Exception as e:
|
continue
|
||||||
logger.warning(f"[chat_completions] 解码失败: {e}")
|
if chunk.get("type") == "done":
|
||||||
if ptype == "thinking":
|
|
||||||
think_list.append("解码失败,请稍候再试")
|
|
||||||
else:
|
|
||||||
text_list.append("解码失败,请稍候再试")
|
|
||||||
data_queue.put(None)
|
data_queue.put(None)
|
||||||
break
|
break
|
||||||
if not line:
|
try:
|
||||||
continue
|
contents, is_finished, new_fragment_type = parse_sse_chunk_for_content(
|
||||||
if line.startswith("data:"):
|
chunk, thinking_enabled, current_fragment_type
|
||||||
data_str = line[5:].strip()
|
)
|
||||||
if data_str == "[DONE]":
|
current_fragment_type = new_fragment_type
|
||||||
data_queue.put(None)
|
if is_finished:
|
||||||
break
|
final_reasoning = "".join(think_list)
|
||||||
try:
|
final_content = "".join(text_list)
|
||||||
chunk = json.loads(data_str)
|
prompt_tokens = len(final_prompt) // 4
|
||||||
if "v" in chunk:
|
reasoning_tokens = len(final_reasoning) // 4
|
||||||
v_value = chunk["v"]
|
completion_tokens = len(final_content) // 4
|
||||||
if "p" in chunk and chunk.get("p") == "response/search_status":
|
|
||||||
continue
|
# 检测工具调用
|
||||||
if "p" in chunk and chunk.get("p") == "response/thinking_content":
|
detected_tools = []
|
||||||
ptype = "thinking"
|
finish_reason = "stop"
|
||||||
elif "p" in chunk and chunk.get("p") == "response/content":
|
if has_tools:
|
||||||
ptype = "text"
|
detected_tools = parse_tool_calls(final_content, [{"name": t.get("function", t).get("name")} for t in tools_requested])
|
||||||
if isinstance(v_value, str):
|
if detected_tools:
|
||||||
if search_enabled and v_value.startswith("[citation:"):
|
finish_reason = "tool_calls"
|
||||||
continue
|
|
||||||
if ptype == "thinking":
|
# 构建 message 对象
|
||||||
think_list.append(v_value)
|
message_obj = {
|
||||||
else:
|
"role": "assistant",
|
||||||
text_list.append(v_value)
|
"content": final_content if not detected_tools else None,
|
||||||
elif isinstance(v_value, list):
|
}
|
||||||
for item in v_value:
|
# 只有启用思考模式时才包含 reasoning_content
|
||||||
if item.get("p") == "status" and item.get("v") == "FINISHED":
|
if thinking_enabled and final_reasoning:
|
||||||
final_reasoning = "".join(think_list)
|
message_obj["reasoning_content"] = final_reasoning
|
||||||
final_content = "".join(text_list)
|
# 添加工具调用
|
||||||
prompt_tokens = len(final_prompt) // 4
|
if detected_tools:
|
||||||
reasoning_tokens = len(final_reasoning) // 4
|
tool_calls_data = format_openai_tool_calls(detected_tools)
|
||||||
completion_tokens = len(final_content) // 4
|
message_obj["tool_calls"] = tool_calls_data
|
||||||
|
message_obj["content"] = None
|
||||||
# 检测工具调用
|
|
||||||
detected_tools = []
|
result = {
|
||||||
finish_reason = "stop"
|
"id": completion_id,
|
||||||
if has_tools:
|
"object": "chat.completion",
|
||||||
detected_tools = parse_tool_calls(final_content, [{"name": t.get("function", t).get("name")} for t in tools_requested])
|
"created": created_time,
|
||||||
if detected_tools:
|
"model": model,
|
||||||
finish_reason = "tool_calls"
|
"choices": [{
|
||||||
|
"index": 0,
|
||||||
# 构建 message 对象
|
"message": message_obj,
|
||||||
message_obj = {
|
"finish_reason": finish_reason,
|
||||||
"role": "assistant",
|
}],
|
||||||
"content": final_content if not detected_tools else None,
|
"usage": {
|
||||||
}
|
"prompt_tokens": prompt_tokens,
|
||||||
# 只有启用思考模式时才包含 reasoning_content
|
"completion_tokens": reasoning_tokens + completion_tokens,
|
||||||
if thinking_enabled and final_reasoning:
|
"total_tokens": prompt_tokens + reasoning_tokens + completion_tokens,
|
||||||
message_obj["reasoning_content"] = final_reasoning
|
"completion_tokens_details": {"reasoning_tokens": reasoning_tokens},
|
||||||
# 添加工具调用
|
},
|
||||||
if detected_tools:
|
}
|
||||||
tool_calls_data = format_openai_tool_calls(detected_tools)
|
data_queue.put("DONE")
|
||||||
message_obj["tool_calls"] = tool_calls_data
|
return
|
||||||
message_obj["content"] = None
|
|
||||||
|
for content_text, content_type in contents:
|
||||||
result = {
|
if should_filter_citation(content_text, search_enabled):
|
||||||
"id": completion_id,
|
continue
|
||||||
"object": "chat.completion",
|
if content_type == "thinking":
|
||||||
"created": created_time,
|
think_list.append(content_text)
|
||||||
"model": model,
|
|
||||||
"choices": [{
|
|
||||||
"index": 0,
|
|
||||||
"message": message_obj,
|
|
||||||
"finish_reason": finish_reason,
|
|
||||||
}],
|
|
||||||
"usage": {
|
|
||||||
"prompt_tokens": prompt_tokens,
|
|
||||||
"completion_tokens": reasoning_tokens + completion_tokens,
|
|
||||||
"total_tokens": prompt_tokens + reasoning_tokens + completion_tokens,
|
|
||||||
"completion_tokens_details": {"reasoning_tokens": reasoning_tokens},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data_queue.put("DONE")
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[collect_data] 无法解析: {data_str}, 错误: {e}")
|
|
||||||
if ptype == "thinking":
|
|
||||||
think_list.append("解析失败,请稍候再试")
|
|
||||||
else:
|
else:
|
||||||
text_list.append("解析失败,请稍候再试")
|
text_list.append(content_text)
|
||||||
data_queue.put(None)
|
except Exception as e:
|
||||||
break
|
logger.warning(f"[collect_data] 无法解析: {chunk}, 错误: {e}")
|
||||||
|
text_list.append("解析失败,请稍候再试")
|
||||||
|
data_queue.put(None)
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[collect_data] 错误: {e}")
|
logger.warning(f"[collect_data] 错误: {e}")
|
||||||
if ptype == "thinking":
|
text_list.append("处理失败,请稍候再试")
|
||||||
think_list.append("处理失败,请稍候再试")
|
|
||||||
else:
|
|
||||||
text_list.append("处理失败,请稍候再试")
|
|
||||||
data_queue.put(None)
|
data_queue.put(None)
|
||||||
finally:
|
finally:
|
||||||
deepseek_resp.close()
|
deepseek_resp.close()
|
||||||
|
|||||||
27
scripts/build-webui.sh
Executable file
27
scripts/build-webui.sh
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# WebUI 构建脚本
|
||||||
|
# 用法: ./scripts/build-webui.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔨 Building WebUI..."
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../webui"
|
||||||
|
|
||||||
|
# 检查 node_modules
|
||||||
|
if [ ! -d "node_modules" ]; then
|
||||||
|
echo "📦 Installing dependencies..."
|
||||||
|
npm install
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
echo "🏗️ Running build..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
if [ ! -f "../static/admin/index.html" ]; then
|
||||||
|
echo "❌ WebUI build failed: static/admin/index.html not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ WebUI built successfully!"
|
||||||
|
echo "📁 Output: static/admin/"
|
||||||
1
static/admin/.gitkeep
Normal file
1
static/admin/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,43 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
|
|
||||||
<!-- SEO Meta Tags -->
|
|
||||||
<title>DS2API - DeepSeek API 管理面板</title>
|
|
||||||
<meta name="description" content="DS2API 管理面板 - 轻松管理 DeepSeek 账号、测试 API、同步 Vercel 配置" />
|
|
||||||
<meta name="keywords" content="DeepSeek, API, OpenAI, 管理面板, DS2API" />
|
|
||||||
<meta name="author" content="CJackHwang" />
|
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
|
||||||
|
|
||||||
<!-- Open Graph / Social -->
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:title" content="DS2API - DeepSeek API 管理面板" />
|
|
||||||
<meta property="og:description" content="轻松管理 DeepSeek 账号、测试 API、同步 Vercel 配置" />
|
|
||||||
<meta property="og:site_name" content="DS2API" />
|
|
||||||
|
|
||||||
<!-- PWA / Mobile -->
|
|
||||||
<meta name="theme-color" content="#f59e0b" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
||||||
<meta name="apple-mobile-web-app-title" content="DS2API" />
|
|
||||||
|
|
||||||
<!-- Favicon - using data URI for orange-yellow gradient icon -->
|
|
||||||
<link rel="icon" type="image/svg+xml"
|
|
||||||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%23f59e0b'/%3E%3Cstop offset='100%25' stop-color='%23ef4444'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect rx='20' width='100' height='100' fill='url(%23g)'/%3E%3Ctext x='50' y='68' font-family='Arial,sans-serif' font-size='48' font-weight='bold' fill='white' text-anchor='middle'%3EDS%3C/text%3E%3C/svg%3E" />
|
|
||||||
|
|
||||||
<!-- Fonts -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
|
||||||
<script type="module" crossorigin src="/admin/assets/index-Da8PBvaf.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-BzdNSWuo.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
29
vercel.json
29
vercel.json
@@ -1,15 +1,30 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"builds": [
|
"buildCommand": "bash scripts/build-webui.sh",
|
||||||
|
"rewrites": [
|
||||||
{
|
{
|
||||||
"src": "app.py",
|
"source": "/(.*)",
|
||||||
"use": "@vercel/python"
|
"destination": "/app.py"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"routes": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"src": "/(.*)",
|
"source": "/admin/assets/(.*)",
|
||||||
"dest": "app.py"
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "public, max-age=31536000, immutable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/admin/(.*)",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "no-store, must-revalidate"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
<!-- SEO Meta Tags -->
|
<!-- SEO Meta Tags -->
|
||||||
<title>DS2API - DeepSeek API 管理面板</title>
|
<title>DS2API - 管理面板 / Admin Console</title>
|
||||||
<meta name="description" content="DS2API 管理面板 - 轻松管理 DeepSeek 账号、测试 API、同步 Vercel 配置" />
|
<meta name="description" content="DS2API 管理面板:管理 DeepSeek 账号、测试 API、同步 Vercel 配置 / Admin console for accounts, API tests, and Vercel sync." />
|
||||||
<meta name="keywords" content="DeepSeek, API, OpenAI, 管理面板, DS2API" />
|
<meta name="keywords" content="DeepSeek, API, OpenAI, 管理面板, admin console, DS2API" />
|
||||||
<meta name="author" content="CJackHwang" />
|
<meta name="author" content="CJackHwang" />
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
|
||||||
<!-- Open Graph / Social -->
|
<!-- Open Graph / Social -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:title" content="DS2API - DeepSeek API 管理面板" />
|
<meta property="og:title" content="DS2API - 管理面板 / Admin Console" />
|
||||||
<meta property="og:description" content="轻松管理 DeepSeek 账号、测试 API、同步 Vercel 配置" />
|
<meta property="og:description" content="Manage DeepSeek accounts, test the API, and sync Vercel configuration." />
|
||||||
<meta property="og:site_name" content="DS2API" />
|
<meta property="og:site_name" content="DS2API" />
|
||||||
|
|
||||||
<!-- PWA / Mobile -->
|
<!-- PWA / Mobile -->
|
||||||
@@ -39,4 +39,4 @@
|
|||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
2986
webui/package-lock.json
generated
Normal file
2986
webui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,19 +25,22 @@ import BatchImport from './components/BatchImport'
|
|||||||
import VercelSync from './components/VercelSync'
|
import VercelSync from './components/VercelSync'
|
||||||
import Login from './components/Login'
|
import Login from './components/Login'
|
||||||
import LandingPage from './components/LandingPage'
|
import LandingPage from './components/LandingPage'
|
||||||
|
import LanguageToggle from './components/LanguageToggle'
|
||||||
const NAV_ITEMS = [
|
import { useI18n } from './i18n'
|
||||||
{ id: 'accounts', label: '账号管理', icon: Users, description: '管理 DeepSeek 账号池' },
|
|
||||||
{ id: 'test', label: 'API 测试', icon: Server, description: '测试 API 连接与响应' },
|
|
||||||
{ id: 'import', label: '批量导入', icon: Upload, description: '批量导入账号配置' },
|
|
||||||
{ id: 'vercel', label: 'Vercel 同步', icon: Cloud, description: '同步配置到 Vercel' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message }) {
|
function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message }) {
|
||||||
|
const { t } = useI18n()
|
||||||
const [activeTab, setActiveTab] = useState('accounts')
|
const [activeTab, setActiveTab] = useState('accounts')
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ id: 'accounts', label: t('nav.accounts.label'), icon: Users, description: t('nav.accounts.desc') },
|
||||||
|
{ id: 'test', label: t('nav.test.label'), icon: Server, description: t('nav.test.desc') },
|
||||||
|
{ id: 'import', label: t('nav.import.label'), icon: Upload, description: t('nav.import.desc') },
|
||||||
|
{ id: 'vercel', label: t('nav.vercel.label'), icon: Cloud, description: t('nav.vercel.desc') },
|
||||||
|
]
|
||||||
|
|
||||||
const authFetch = async (url, options = {}) => {
|
const authFetch = async (url, options = {}) => {
|
||||||
const headers = {
|
const headers = {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
@@ -47,7 +50,7 @@ function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message
|
|||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
onLogout()
|
onLogout()
|
||||||
throw new Error('认证已过期,请重新登录')
|
throw new Error(t('auth.expired'))
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -87,11 +90,14 @@ function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message
|
|||||||
</div>
|
</div>
|
||||||
<span>DS2API</span>
|
<span>DS2API</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-muted-foreground mt-2 font-semibold tracking-[0.1em] uppercase opacity-60 px-1">在线管理面板</p>
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<p className="text-[10px] text-muted-foreground font-semibold tracking-[0.1em] uppercase opacity-60 px-1">{t('sidebar.onlineAdminConsole')}</p>
|
||||||
|
<LanguageToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 px-3 space-y-1 overflow-y-auto pt-2">
|
<nav className="flex-1 px-3 space-y-1 overflow-y-auto pt-2">
|
||||||
{NAV_ITEMS.map((item) => {
|
{navItems.map((item) => {
|
||||||
const Icon = item.icon
|
const Icon = item.icon
|
||||||
const isActive = activeTab === item.id
|
const isActive = activeTab === item.id
|
||||||
return (
|
return (
|
||||||
@@ -119,19 +125,19 @@ function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message
|
|||||||
<div className="p-4 border-t border-border bg-card">
|
<div className="p-4 border-t border-border bg-card">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between text-sm px-1">
|
<div className="flex items-center justify-between text-sm px-1">
|
||||||
<span className="text-muted-foreground font-semibold text-[10px] uppercase tracking-wider">系统状态</span>
|
<span className="text-muted-foreground font-semibold text-[10px] uppercase tracking-wider">{t('sidebar.systemStatus')}</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="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>
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||||
在线
|
{t('sidebar.statusOnline')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="bg-background rounded-lg p-3 border border-border shadow-sm">
|
<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">账号</div>
|
<div className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70">{t('sidebar.accounts')}</div>
|
||||||
<div className="text-lg font-bold text-foreground leading-tight">{config.accounts?.length || 0}</div>
|
<div className="text-lg font-bold text-foreground leading-tight">{config.accounts?.length || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-background rounded-lg p-3 border border-border shadow-sm">
|
<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">密钥</div>
|
<div className="text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70">{t('sidebar.keys')}</div>
|
||||||
<div className="text-lg font-bold text-foreground">{config.keys?.length || 0}</div>
|
<div className="text-lg font-bold text-foreground">{config.keys?.length || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +146,7 @@ function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message
|
|||||||
className="w-full h-10 flex items-center justify-center gap-2 rounded-lg border border-border text-xs font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/20 transition-all"
|
className="w-full h-10 flex items-center justify-center gap-2 rounded-lg border border-border text-xs font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/20 transition-all"
|
||||||
>
|
>
|
||||||
<LogOut className="w-3.5 h-3.5" />
|
<LogOut className="w-3.5 h-3.5" />
|
||||||
退出登录
|
{t('sidebar.signOut')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,22 +160,25 @@ function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message
|
|||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-sm">DS2API</span>
|
<span className="font-semibold text-sm">DS2API</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => setSidebarOpen(true)}
|
<LanguageToggle />
|
||||||
className="p-2 -mr-2 text-muted-foreground hover:text-foreground"
|
<button
|
||||||
>
|
onClick={() => setSidebarOpen(true)}
|
||||||
<Menu className="w-5 h-5" />
|
className="p-2 -mr-2 text-muted-foreground hover:text-foreground"
|
||||||
</button>
|
>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto bg-background p-4 lg:p-10">
|
<div className="flex-1 overflow-auto bg-background p-4 lg:p-10">
|
||||||
<div className="max-w-6xl mx-auto space-y-4 lg:space-y-6">
|
<div className="max-w-6xl mx-auto space-y-4 lg:space-y-6">
|
||||||
<div className="hidden lg:block mb-8">
|
<div className="hidden lg:block mb-8">
|
||||||
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
<h1 className="text-3xl font-bold tracking-tight mb-2">
|
||||||
{NAV_ITEMS.find(n => n.id === activeTab)?.label}
|
{navItems.find(n => n.id === activeTab)?.label}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{NAV_ITEMS.find(n => n.id === activeTab)?.description}
|
{navItems.find(n => n.id === activeTab)?.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -195,6 +204,7 @@ function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const { t } = useI18n()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [config, setConfig] = useState({ keys: [], accounts: [] })
|
const [config, setConfig] = useState({ keys: [], accounts: [] })
|
||||||
@@ -207,7 +217,7 @@ export default function App() {
|
|||||||
const isAdminRoute = location.pathname.startsWith('/admin') || isProduction
|
const isAdminRoute = location.pathname.startsWith('/admin') || isProduction
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 只在 admin 路由时检查登录状态
|
// Only check auth status on admin routes.
|
||||||
if (!isAdminRoute) {
|
if (!isAdminRoute) {
|
||||||
setAuthChecking(false)
|
setAuthChecking(false)
|
||||||
return
|
return
|
||||||
@@ -248,8 +258,8 @@ export default function App() {
|
|||||||
setConfig(data)
|
setConfig(data)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取配置失败:', e)
|
console.error('Failed to fetch config:', e)
|
||||||
showMessage('error', e.message)
|
showMessage('error', t('errors.fetchConfig', { error: e.message }))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -278,13 +288,13 @@ export default function App() {
|
|||||||
sessionStorage.removeItem('ds2api_token_expires')
|
sessionStorage.removeItem('ds2api_token_expires')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在 admin 路由时,等待认证检查完成
|
// Wait for auth checks on admin routes.
|
||||||
if (isAdminRoute && authChecking) {
|
if (isAdminRoute && authChecking) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<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>
|
<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">{t('auth.checking')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,26 +2,25 @@ import { useState, useEffect } from 'react'
|
|||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
RefreshCw,
|
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
AlertCircle,
|
|
||||||
Search,
|
|
||||||
Play,
|
Play,
|
||||||
MoreHorizontal,
|
|
||||||
X,
|
X,
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck
|
ShieldCheck,
|
||||||
|
Copy,
|
||||||
|
Check
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { useI18n } from '../i18n'
|
||||||
|
|
||||||
export default function AccountManager({ config, onRefresh, onMessage, authFetch }) {
|
export default function AccountManager({ config, onRefresh, onMessage, authFetch }) {
|
||||||
|
const { t } = useI18n()
|
||||||
const [showAddKey, setShowAddKey] = useState(false)
|
const [showAddKey, setShowAddKey] = useState(false)
|
||||||
const [showAddAccount, setShowAddAccount] = useState(false)
|
const [showAddAccount, setShowAddAccount] = useState(false)
|
||||||
const [newKey, setNewKey] = useState('')
|
const [newKey, setNewKey] = useState('')
|
||||||
|
const [copiedKey, setCopiedKey] = useState(null)
|
||||||
const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' })
|
const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' })
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [validating, setValidating] = useState({})
|
|
||||||
const [validatingAll, setValidatingAll] = useState(false)
|
|
||||||
const [testing, setTesting] = useState({})
|
const [testing, setTesting] = useState({})
|
||||||
const [testingAll, setTestingAll] = useState(false)
|
const [testingAll, setTestingAll] = useState(false)
|
||||||
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] })
|
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] })
|
||||||
@@ -57,39 +56,39 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
body: JSON.stringify({ key: newKey.trim() }),
|
body: JSON.stringify({ key: newKey.trim() }),
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
onMessage('success', 'API 密钥添加成功')
|
onMessage('success', t('accountManager.addKeySuccess'))
|
||||||
setNewKey('')
|
setNewKey('')
|
||||||
setShowAddKey(false)
|
setShowAddKey(false)
|
||||||
onRefresh()
|
onRefresh()
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
onMessage('error', data.detail || 'Failed to add')
|
onMessage('error', data.detail || t('messages.failedToAdd'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '网络错误')
|
onMessage('error', t('messages.networkError'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteKey = async (key) => {
|
const deleteKey = async (key) => {
|
||||||
if (!confirm('确定要删除此 API 密钥吗?')) return
|
if (!confirm(t('accountManager.deleteKeyConfirm'))) return
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch(`/admin/keys/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
const res = await apiFetch(`/admin/keys/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
onMessage('success', 'Deleted successfully')
|
onMessage('success', t('messages.deleted'))
|
||||||
onRefresh()
|
onRefresh()
|
||||||
} else {
|
} else {
|
||||||
onMessage('error', 'Delete failed')
|
onMessage('error', t('messages.deleteFailed'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', 'Network error')
|
onMessage('error', t('messages.networkError'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addAccount = async () => {
|
const addAccount = async () => {
|
||||||
if (!newAccount.password || (!newAccount.email && !newAccount.mobile)) {
|
if (!newAccount.password || (!newAccount.email && !newAccount.mobile)) {
|
||||||
onMessage('error', 'Password and Email/Mobile are required')
|
onMessage('error', t('accountManager.requiredFields'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -100,90 +99,36 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
body: JSON.stringify(newAccount),
|
body: JSON.stringify(newAccount),
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
onMessage('success', '账号添加成功')
|
onMessage('success', t('accountManager.addAccountSuccess'))
|
||||||
setNewAccount({ email: '', mobile: '', password: '' })
|
setNewAccount({ email: '', mobile: '', password: '' })
|
||||||
setShowAddAccount(false)
|
setShowAddAccount(false)
|
||||||
onRefresh()
|
onRefresh()
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
onMessage('error', data.detail || 'Failed to add')
|
onMessage('error', data.detail || t('messages.failedToAdd'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '网络错误')
|
onMessage('error', t('messages.networkError'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAccount = async (id) => {
|
const deleteAccount = async (id) => {
|
||||||
if (!confirm('确定要删除此账号吗?')) return
|
if (!confirm(t('accountManager.deleteAccountConfirm'))) return
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch(`/admin/accounts/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
const res = await apiFetch(`/admin/accounts/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
onMessage('success', 'Deleted successfully')
|
onMessage('success', t('messages.deleted'))
|
||||||
onRefresh()
|
onRefresh()
|
||||||
} else {
|
} else {
|
||||||
onMessage('error', 'Delete failed')
|
onMessage('error', t('messages.deleteFailed'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', 'Network error')
|
onMessage('error', t('messages.networkError'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateAccount = async (identifier) => {
|
|
||||||
setValidating(prev => ({ ...prev, [identifier]: true }))
|
|
||||||
try {
|
|
||||||
const res = await apiFetch('/admin/accounts/validate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ identifier }),
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
onMessage(data.valid ? 'success' : 'error', `${identifier}: ${data.message}`)
|
|
||||||
onRefresh()
|
|
||||||
} catch (e) {
|
|
||||||
onMessage('error', 'Validation failed: ' + e.message)
|
|
||||||
} finally {
|
|
||||||
setValidating(prev => ({ ...prev, [identifier]: false }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateAllAccounts = async () => {
|
|
||||||
if (!confirm('校验所有账号?这可能需要一些时间。')) return
|
|
||||||
const accounts = config.accounts || []
|
|
||||||
if (accounts.length === 0) return
|
|
||||||
|
|
||||||
setValidatingAll(true)
|
|
||||||
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 apiFetch('/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', `Completed: ${validCount}/${accounts.length} valid`)
|
|
||||||
onRefresh()
|
|
||||||
setValidatingAll(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const testAccount = async (identifier) => {
|
const testAccount = async (identifier) => {
|
||||||
setTesting(prev => ({ ...prev, [identifier]: true }))
|
setTesting(prev => ({ ...prev, [identifier]: true }))
|
||||||
try {
|
try {
|
||||||
@@ -193,17 +138,20 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
body: JSON.stringify({ identifier }),
|
body: JSON.stringify({ identifier }),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
onMessage(data.success ? 'success' : 'error', `${identifier}: ${data.success ? `Success (${data.response_time}ms)` : data.message}`)
|
const statusMessage = data.success
|
||||||
|
? t('apiTester.testSuccess', { account: identifier, time: data.response_time })
|
||||||
|
: `${identifier}: ${data.message}`
|
||||||
|
onMessage(data.success ? 'success' : 'error', statusMessage)
|
||||||
onRefresh()
|
onRefresh()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', 'Test failed: ' + e.message)
|
onMessage('error', t('accountManager.testFailed', { error: e.message }))
|
||||||
} finally {
|
} finally {
|
||||||
setTesting(prev => ({ ...prev, [identifier]: false }))
|
setTesting(prev => ({ ...prev, [identifier]: false }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const testAllAccounts = async () => {
|
const testAllAccounts = async () => {
|
||||||
if (!confirm('测试所有账号的 API 连通性?')) return
|
if (!confirm(t('accountManager.testAllConfirm'))) return
|
||||||
const accounts = config.accounts || []
|
const accounts = config.accounts || []
|
||||||
if (accounts.length === 0) return
|
if (accounts.length === 0) return
|
||||||
|
|
||||||
@@ -233,7 +181,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] })
|
setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] })
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage('success', `Completed: ${successCount}/${accounts.length} available`)
|
onMessage('success', t('accountManager.testAllCompleted', { success: successCount, total: accounts.length }))
|
||||||
onRefresh()
|
onRefresh()
|
||||||
setTestingAll(false)
|
setTestingAll(false)
|
||||||
}
|
}
|
||||||
@@ -248,30 +196,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">
|
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||||
<CheckCircle2 className="w-16 h-16" />
|
<CheckCircle2 className="w-16 h-16" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">可用</p>
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">{t('accountManager.available')}</p>
|
||||||
<div className="mt-2 flex items-baseline gap-2">
|
<div className="mt-2 flex items-baseline gap-2">
|
||||||
<span className="text-3xl font-bold text-foreground">{queueStatus.available}</span>
|
<span className="text-3xl font-bold text-foreground">{queueStatus.available}</span>
|
||||||
<span className="text-xs text-muted-foreground">个账号</span>
|
<span className="text-xs text-muted-foreground">{t('accountManager.accountsUnit')}</span>
|
||||||
</div>
|
</div>
|
||||||
</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="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">
|
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||||
<Server className="w-16 h-16" />
|
<Server className="w-16 h-16" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">正在使用</p>
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">{t('accountManager.inUse')}</p>
|
||||||
<div className="mt-2 flex items-baseline gap-2">
|
<div className="mt-2 flex items-baseline gap-2">
|
||||||
<span className="text-3xl font-bold text-foreground">{queueStatus.in_use}</span>
|
<span className="text-3xl font-bold text-foreground">{queueStatus.in_use}</span>
|
||||||
<span className="text-xs text-muted-foreground">线程</span>
|
<span className="text-xs text-muted-foreground">{t('accountManager.threadsUnit')}</span>
|
||||||
</div>
|
</div>
|
||||||
</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="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">
|
<div className="absolute right-0 top-0 p-4 opacity-5 group-hover:opacity-10 transition-opacity">
|
||||||
<ShieldCheck className="w-16 h-16" />
|
<ShieldCheck className="w-16 h-16" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">账号池总数</p>
|
<p className="text-xs font-medium text-muted-foreground uppercase tracking-widest">{t('accountManager.totalPool')}</p>
|
||||||
<div className="mt-2 flex items-baseline gap-2">
|
<div className="mt-2 flex items-baseline gap-2">
|
||||||
<span className="text-3xl font-bold text-foreground">{queueStatus.total}</span>
|
<span className="text-3xl font-bold text-foreground">{queueStatus.total}</span>
|
||||||
<span className="text-xs text-muted-foreground">个账号</span>
|
<span className="text-xs text-muted-foreground">{t('accountManager.accountsUnit')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,15 +230,15 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
||||||
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">API 密钥</h2>
|
<h2 className="text-lg font-semibold">{t('accountManager.apiKeysTitle')}</h2>
|
||||||
<p className="text-sm text-muted-foreground">管理 API 访问密钥池</p>
|
<p className="text-sm text-muted-foreground">{t('accountManager.apiKeysDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAddKey(true)}
|
onClick={() => setShowAddKey(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"
|
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" />
|
<Plus className="w-4 h-4" />
|
||||||
添加密钥
|
{t('accountManager.addKey')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -298,19 +246,38 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
{config.keys?.length > 0 ? (
|
{config.keys?.length > 0 ? (
|
||||||
config.keys.map((key, i) => (
|
config.keys.map((key, i) => (
|
||||||
<div key={i} className="p-4 flex items-center justify-between hover:bg-muted/50 transition-colors group">
|
<div key={i} className="p-4 flex items-center justify-between hover:bg-muted/50 transition-colors group">
|
||||||
<div className="font-mono text-sm bg-muted/50 px-3 py-1 rounded inline-block">
|
<div className="flex items-center gap-2">
|
||||||
{key.slice(0, 16)}****
|
<div className="font-mono text-sm bg-muted/50 px-3 py-1 rounded inline-block">
|
||||||
|
{key.slice(0, 16)}****
|
||||||
|
</div>
|
||||||
|
{copiedKey === key && (
|
||||||
|
<span className="text-xs text-green-500 animate-pulse">{t('accountManager.copied')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(key)
|
||||||
|
setCopiedKey(key)
|
||||||
|
setTimeout(() => setCopiedKey(null), 2000)
|
||||||
|
}}
|
||||||
|
className="p-2 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-md transition-colors opacity-0 group-hover:opacity-100"
|
||||||
|
title={t('accountManager.copyKeyTitle')}
|
||||||
|
>
|
||||||
|
{copiedKey === key ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => deleteKey(key)}
|
||||||
|
className="p-2 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors opacity-0 group-hover:opacity-100"
|
||||||
|
title={t('accountManager.deleteKeyTitle')}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => deleteKey(key)}
|
|
||||||
className="p-2 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors opacity-0 group-hover:opacity-100"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="p-8 text-center text-muted-foreground">未找到 API 密钥</div>
|
<div className="p-8 text-center text-muted-foreground">{t('accountManager.noApiKeys')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -319,41 +286,33 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
||||||
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold">DeepSeek 账号</h2>
|
<h2 className="text-lg font-semibold">{t('accountManager.accountsTitle')}</h2>
|
||||||
<p className="text-sm text-muted-foreground">管理 DeepSeek 账号池</p>
|
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={testAllAccounts}
|
onClick={testAllAccounts}
|
||||||
disabled={testingAll || validatingAll || !config.accounts?.length}
|
disabled={testingAll || !config.accounts?.length}
|
||||||
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"
|
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" />}
|
{testingAll ? <span className="animate-spin mr-2">⟳</span> : <Play className="w-3 h-3 mr-2" />}
|
||||||
测试全部
|
{t('accountManager.testAll')}
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={validateAllAccounts}
|
|
||||||
disabled={validatingAll || testingAll || !config.accounts?.length}
|
|
||||||
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" />}
|
|
||||||
校验全部
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAddAccount(true)}
|
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"
|
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" />
|
<Plus className="w-4 h-4" />
|
||||||
添加账号
|
{t('accountManager.addAccount')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Batch Progress */}
|
{/* Batch Progress */}
|
||||||
{(testingAll || validatingAll) && batchProgress.total > 0 && (
|
{testingAll && batchProgress.total > 0 && (
|
||||||
<div className="p-4 border-b border-border bg-muted/30">
|
<div className="p-4 border-b border-border bg-muted/30">
|
||||||
<div className="flex items-center justify-between text-sm mb-2">
|
<div className="flex items-center justify-between text-sm mb-2">
|
||||||
<span className="font-medium">{testingAll ? '正在测试所有账号...' : '正在校验所有账号...'}</span>
|
<span className="font-medium">{t('accountManager.testingAllAccounts')}</span>
|
||||||
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
|
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
|
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
|
||||||
@@ -391,7 +350,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="font-medium truncate">{id}</div>
|
<div className="font-medium truncate">{id}</div>
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
||||||
<span>{acc.has_token ? '已建立会话' : '需重新登录'}</span>
|
<span>{acc.has_token ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
|
||||||
{acc.token_preview && (
|
{acc.token_preview && (
|
||||||
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
|
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
|
||||||
{acc.token_preview}
|
{acc.token_preview}
|
||||||
@@ -406,14 +365,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
disabled={testing[id]}
|
disabled={testing[id]}
|
||||||
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
|
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{testing[id] ? '正在测试...' : '测试'}
|
{testing[id] ? t('actions.testing') : t('actions.test')}
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => validateAccount(id)}
|
|
||||||
disabled={validating[id]}
|
|
||||||
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{validating[id] ? '正在校验...' : '校验'}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => deleteAccount(id)}
|
onClick={() => deleteAccount(id)}
|
||||||
@@ -426,7 +378,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="p-8 text-center text-muted-foreground">未找到任何账号</div>
|
<div className="p-8 text-center text-muted-foreground">{t('accountManager.noAccounts')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -437,27 +389,37 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||||
<div className="p-4 border-b border-border flex justify-between items-center">
|
<div className="p-4 border-b border-border flex justify-between items-center">
|
||||||
<h3 className="font-semibold">添加 API 密钥</h3>
|
<h3 className="font-semibold">{t('accountManager.modalAddKeyTitle')}</h3>
|
||||||
<button onClick={() => setShowAddKey(false)} className="text-muted-foreground hover:text-foreground">
|
<button onClick={() => setShowAddKey(false)} className="text-muted-foreground hover:text-foreground">
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1.5">新密钥值</label>
|
<label className="block text-sm font-medium mb-1.5">{t('accountManager.newKeyLabel')}</label>
|
||||||
<input
|
<div className="flex gap-2">
|
||||||
type="text"
|
<input
|
||||||
className="input-field bg-[#09090b]"
|
type="text"
|
||||||
placeholder="输入自定义 API 密钥"
|
className="input-field bg-[#09090b] flex-1"
|
||||||
value={newKey}
|
placeholder={t('accountManager.newKeyPlaceholder')}
|
||||||
onChange={e => setNewKey(e.target.value)}
|
value={newKey}
|
||||||
autoFocus
|
onChange={e => setNewKey(e.target.value)}
|
||||||
/>
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setNewKey('sk-' + crypto.randomUUID().replace(/-/g, ''))}
|
||||||
|
className="px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-sm font-medium border border-border whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t('accountManager.generate')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1.5">{t('accountManager.generateHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<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">取消</button>
|
<button onClick={() => setShowAddKey(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">{t('actions.cancel')}</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">
|
<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 ? '添加中...' : '添加密钥'}
|
{loading ? t('accountManager.addKeyLoading') : t('accountManager.addKeyAction')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -471,14 +433,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||||
<div className="p-4 border-b border-border flex justify-between items-center">
|
<div className="p-4 border-b border-border flex justify-between items-center">
|
||||||
<h3 className="font-semibold">添加 DeepSeek 账号</h3>
|
<h3 className="font-semibold">{t('accountManager.modalAddAccountTitle')}</h3>
|
||||||
<button onClick={() => setShowAddAccount(false)} className="text-muted-foreground hover:text-foreground">
|
<button onClick={() => setShowAddAccount(false)} className="text-muted-foreground hover:text-foreground">
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1.5">邮箱 (可选)</label>
|
<label className="block text-sm font-medium mb-1.5">{t('accountManager.emailOptional')}</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
className="input-field"
|
className="input-field"
|
||||||
@@ -488,7 +450,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1.5">手机号 (可选)</label>
|
<label className="block text-sm font-medium mb-1.5">{t('accountManager.mobileOptional')}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="input-field"
|
className="input-field"
|
||||||
@@ -498,19 +460,19 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1.5">密码 <span className="text-destructive">*</span></label>
|
<label className="block text-sm font-medium mb-1.5">{t('accountManager.passwordLabel')} <span className="text-destructive">*</span></label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
className="input-field bg-[#09090b]"
|
className="input-field bg-[#09090b]"
|
||||||
placeholder="账号密码"
|
placeholder={t('accountManager.passwordPlaceholder')}
|
||||||
value={newAccount.password}
|
value={newAccount.password}
|
||||||
onChange={e => setNewAccount({ ...newAccount, password: e.target.value })}
|
onChange={e => setNewAccount({ ...newAccount, password: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<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">取消</button>
|
<button onClick={() => setShowAddAccount(false)} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">{t('actions.cancel')}</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">
|
<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 ? '添加中...' : '添加账号'}
|
{loading ? t('accountManager.addAccountLoading') : t('accountManager.addAccountAction')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Send,
|
Send,
|
||||||
Square,
|
Square,
|
||||||
@@ -17,17 +17,13 @@ import {
|
|||||||
Zap
|
Zap
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { useI18n } from '../i18n'
|
||||||
const MODELS = [
|
|
||||||
{ 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 }) {
|
export default function ApiTester({ config, onMessage, authFetch }) {
|
||||||
|
const { t } = useI18n()
|
||||||
const [model, setModel] = useState('deepseek-chat')
|
const [model, setModel] = useState('deepseek-chat')
|
||||||
const [message, setMessage] = useState('Hello, please introduce yourself in one sentence.')
|
const defaultMessage = t('apiTester.defaultMessage')
|
||||||
|
const [message, setMessage] = useState(defaultMessage)
|
||||||
const [apiKey, setApiKey] = useState('')
|
const [apiKey, setApiKey] = useState('')
|
||||||
const [selectedAccount, setSelectedAccount] = useState('')
|
const [selectedAccount, setSelectedAccount] = useState('')
|
||||||
const [response, setResponse] = useState(null)
|
const [response, setResponse] = useState(null)
|
||||||
@@ -36,12 +32,19 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
const [streamingThinking, setStreamingThinking] = useState('')
|
const [streamingThinking, setStreamingThinking] = useState('')
|
||||||
const [isStreaming, setIsStreaming] = useState(false)
|
const [isStreaming, setIsStreaming] = useState(false)
|
||||||
const abortControllerRef = useRef(null)
|
const abortControllerRef = useRef(null)
|
||||||
|
const defaultMessageRef = useRef(defaultMessage)
|
||||||
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||||
const [configExpanded, setConfigExpanded] = useState(false)
|
const [configExpanded, setConfigExpanded] = useState(false)
|
||||||
|
|
||||||
const apiFetch = authFetch || fetch
|
const apiFetch = authFetch || fetch
|
||||||
const accounts = config.accounts || []
|
const accounts = config.accounts || []
|
||||||
|
const models = [
|
||||||
|
{ id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: t('apiTester.models.chat'), color: "text-amber-500" },
|
||||||
|
{ id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: t('apiTester.models.reasoner'), color: "text-amber-600" },
|
||||||
|
{ id: "deepseek-chat-search", name: "deepseek-chat-search", icon: SearchIcon, desc: t('apiTester.models.chatSearch'), color: "text-cyan-500" },
|
||||||
|
{ id: "deepseek-reasoner-search", name: "deepseek-reasoner-search", icon: SearchIcon, desc: t('apiTester.models.reasonerSearch'), color: "text-cyan-600" },
|
||||||
|
]
|
||||||
|
|
||||||
const stopGeneration = () => {
|
const stopGeneration = () => {
|
||||||
if (abortControllerRef.current) {
|
if (abortControllerRef.current) {
|
||||||
@@ -66,7 +69,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
try {
|
try {
|
||||||
const key = apiKey || (config.keys?.[0] || '')
|
const key = apiKey || (config.keys?.[0] || '')
|
||||||
if (!key) {
|
if (!key) {
|
||||||
onMessage('error', '请提供 API 密钥')
|
onMessage('error', t('apiTester.missingApiKey'))
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setIsStreaming(false)
|
setIsStreaming(false)
|
||||||
return
|
return
|
||||||
@@ -88,8 +91,8 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setResponse({ success: false, error: data.error?.message || '请求失败' })
|
setResponse({ success: false, error: data.error?.message || t('apiTester.requestFailed') })
|
||||||
onMessage('error', data.error?.message || '请求失败')
|
onMessage('error', data.error?.message || t('apiTester.requestFailed'))
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
setIsStreaming(false)
|
setIsStreaming(false)
|
||||||
return
|
return
|
||||||
@@ -138,9 +141,9 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.name === 'AbortError') {
|
if (e.name === 'AbortError') {
|
||||||
onMessage('info', '已停止生成')
|
onMessage('info', t('messages.generationStopped'))
|
||||||
} else {
|
} else {
|
||||||
onMessage('error', '网络错误: ' + e.message)
|
onMessage('error', t('apiTester.networkError', { error: e.message }))
|
||||||
setResponse({ error: e.message, success: false })
|
setResponse({ error: e.message, success: false })
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -172,12 +175,12 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
account: selectedAccount,
|
account: selectedAccount,
|
||||||
})
|
})
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
onMessage('success', `${selectedAccount}: 测试成功 (${data.response_time}ms)`)
|
onMessage('success', t('apiTester.testSuccess', { account: selectedAccount, time: data.response_time }))
|
||||||
} else {
|
} else {
|
||||||
onMessage('error', `${selectedAccount}: ${data.message}`)
|
onMessage('error', `${selectedAccount}: ${data.message}`)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '网络错误: ' + e.message)
|
onMessage('error', t('apiTester.networkError', { error: e.message }))
|
||||||
setResponse({ error: e.message })
|
setResponse({ error: e.message })
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -188,6 +191,11 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
directTest()
|
directTest()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMessage((prev) => (prev === defaultMessageRef.current ? defaultMessage : prev))
|
||||||
|
defaultMessageRef.current = defaultMessage
|
||||||
|
}, [defaultMessage])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col lg:grid lg:grid-cols-12 gap-6 h-[calc(100vh-140px)]">
|
<div className="flex flex-col lg:grid lg:grid-cols-12 gap-6 h-[calc(100vh-140px)]">
|
||||||
{/* Configuration Panel */}
|
{/* Configuration Panel */}
|
||||||
@@ -201,12 +209,12 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
onClick={() => setConfigExpanded(!configExpanded)}
|
onClick={() => setConfigExpanded(!configExpanded)}
|
||||||
className="lg:hidden flex items-center justify-between p-4 w-full bg-muted/20 hover:bg-muted/30 transition-colors"
|
className="lg:hidden flex items-center justify-between p-4 w-full bg-muted/20 hover:bg-muted/30 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5 font-medium text-sm text-foreground">
|
<div className="flex items-center gap-2.5 font-medium text-sm text-foreground">
|
||||||
<div className="p-1.5 rounded-md bg-transparent text-foreground">
|
<div className="p-1.5 rounded-md bg-transparent text-foreground">
|
||||||
<Terminal className="w-4 h-4" />
|
<Terminal className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<span>{t('apiTester.config')}</span>
|
||||||
</div>
|
</div>
|
||||||
<span>配置</span>
|
|
||||||
</div>
|
|
||||||
<div className={clsx("transition-transform duration-300 text-muted-foreground", configExpanded ? "rotate-180" : "")}>
|
<div className={clsx("transition-transform duration-300 text-muted-foreground", configExpanded ? "rotate-180" : "")}>
|
||||||
<ChevronDown className="w-4 h-4" />
|
<ChevronDown className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
@@ -217,9 +225,9 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
!configExpanded && "hidden lg:block"
|
!configExpanded && "hidden lg:block"
|
||||||
)}>
|
)}>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">模型</label>
|
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.modelLabel')}</label>
|
||||||
<div className="grid grid-cols-1 gap-2">
|
<div className="grid grid-cols-1 gap-2">
|
||||||
{MODELS.map(m => {
|
{models.map(m => {
|
||||||
const Icon = m.icon
|
const Icon = m.icon
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -256,14 +264,14 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">账号策略</label>
|
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.accountStrategy')}</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
className="w-full h-10 pl-3 pr-8 bg-secondary border border-border rounded-lg text-sm appearance-none focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all cursor-pointer hover:bg-muted"
|
className="w-full h-10 pl-3 pr-8 bg-secondary border border-border rounded-lg text-sm appearance-none focus:outline-none focus:ring-1 focus:ring-ring focus:border-ring transition-all cursor-pointer hover:bg-muted"
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={e => setSelectedAccount(e.target.value)}
|
onChange={e => setSelectedAccount(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="" className="bg-popover text-popover-foreground">🎲 随机切换 (支持流式预览)</option>
|
<option value="" className="bg-popover text-popover-foreground">{t('apiTester.randomRotation')}</option>
|
||||||
{accounts.map((acc, i) => (
|
{accounts.map((acc, i) => (
|
||||||
<option key={i} value={acc.email || acc.mobile} className="bg-popover text-popover-foreground">
|
<option key={i} value={acc.email || acc.mobile} className="bg-popover text-popover-foreground">
|
||||||
👤 {acc.email || acc.mobile}
|
👤 {acc.email || acc.mobile}
|
||||||
@@ -275,11 +283,11 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">API 密钥 (可选)</label>
|
<label className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider ml-0.5">{t('apiTester.apiKeyOptional')}</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
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"
|
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)}` : '输入自定义密钥'}
|
placeholder={config.keys?.[0] ? t('apiTester.apiKeyDefault', { suffix: config.keys[0].slice(-6) }) : t('apiTester.apiKeyPlaceholder')}
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
onChange={e => setApiKey(e.target.value)}
|
onChange={e => setApiKey(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -324,7 +332,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
"text-[10px] px-1.5 py-0.5 rounded-sm border uppercase font-medium tracking-wider",
|
"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.success ? "border-emerald-500/20 text-emerald-500 bg-emerald-500/10" : "border-destructive/20 text-destructive bg-destructive/10"
|
||||||
)}>
|
)}>
|
||||||
{response.status_code || '错误'}
|
{response.status_code || t('apiTester.statusError')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -333,7 +341,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="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">
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
<Zap className="w-3.5 h-3.5" />
|
<Zap className="w-3.5 h-3.5" />
|
||||||
<span className="font-medium">思维链过程</span>
|
<span className="font-medium">{t('apiTester.reasoningTrace')}</span>
|
||||||
</div>
|
</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">
|
<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}
|
{streamingThinking || response?.response?.thinking}
|
||||||
@@ -345,7 +353,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
{!selectedAccount ? (
|
{!selectedAccount ? (
|
||||||
streamingContent || (response?.error && <span className="text-destructive font-medium">{response.error}</span>)
|
streamingContent || (response?.error && <span className="text-destructive font-medium">{response.error}</span>)
|
||||||
) : (
|
) : (
|
||||||
response?.response?.message || <span className="text-muted-foreground italic">正在生成响应...</span>
|
response?.response?.message || <span className="text-muted-foreground italic">{t('apiTester.generating')}</span>
|
||||||
)}
|
)}
|
||||||
{isStreaming && <span className="inline-block w-1.5 h-4 bg-primary ml-1 align-middle animate-pulse" />}
|
{isStreaming && <span className="inline-block w-1.5 h-4 bg-primary ml-1 align-middle animate-pulse" />}
|
||||||
</div>
|
</div>
|
||||||
@@ -357,9 +365,9 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
{/* Input Area */}
|
{/* Input Area */}
|
||||||
<div className="p-4 lg:p-6 border-t border-border bg-card">
|
<div className="p-4 lg:p-6 border-t border-border bg-card">
|
||||||
<div className="max-w-4xl mx-auto relative group">
|
<div className="max-w-4xl mx-auto relative group">
|
||||||
<textarea
|
<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"
|
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="输入消息..."
|
placeholder={t('apiTester.enterMessage')}
|
||||||
rows={1}
|
rows={1}
|
||||||
style={{ minHeight: '52px' }}
|
style={{ minHeight: '52px' }}
|
||||||
value={message}
|
value={message}
|
||||||
@@ -391,7 +399,7 @@ export default function ApiTester({ config, onMessage, authFetch }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-4xl mx-auto mt-3 flex justify-center">
|
<div className="max-w-4xl mx-auto mt-3 flex justify-center">
|
||||||
<span className="text-[10px] text-muted-foreground/40 font-medium">DeepSeek 管理员界面</span>
|
<span className="text-[10px] text-muted-foreground/40 font-medium">{t('apiTester.adminConsoleLabel')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,68 +1,69 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { FileCode, Download, Upload, Copy, Check, AlertTriangle } from 'lucide-react'
|
import { FileCode, Download, Upload, Copy, Check, AlertTriangle } from 'lucide-react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { useI18n } from '../i18n'
|
||||||
const TEMPLATES = {
|
|
||||||
full: {
|
|
||||||
name: '全量配置模板',
|
|
||||||
desc: '包含密钥、账号及模型映射',
|
|
||||||
config: {
|
|
||||||
keys: ["your-api-key-1", "your-api-key-2"],
|
|
||||||
accounts: [
|
|
||||||
{ email: "user1@example.com", password: "password1", token: "" },
|
|
||||||
{ email: "user2@example.com", password: "password2", token: "" },
|
|
||||||
{ mobile: "+8613800138001", password: "password3", token: "" }
|
|
||||||
],
|
|
||||||
claude_model_mapping: {
|
|
||||||
fast: "deepseek-chat",
|
|
||||||
slow: "deepseek-reasoner"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
email_only: {
|
|
||||||
name: '仅邮箱账号',
|
|
||||||
desc: '批量导入邮箱格式账号',
|
|
||||||
config: {
|
|
||||||
keys: ["your-api-key"],
|
|
||||||
accounts: [
|
|
||||||
{ email: "account1@example.com", password: "pass1", token: "" },
|
|
||||||
{ email: "account2@example.com", password: "pass2", token: "" },
|
|
||||||
{ email: "account3@example.com", password: "pass3", token: "" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mobile_only: {
|
|
||||||
name: '仅手机号账号',
|
|
||||||
desc: '批量导入手机号格式账号',
|
|
||||||
config: {
|
|
||||||
keys: ["your-api-key"],
|
|
||||||
accounts: [
|
|
||||||
{ mobile: "+8613800000001", password: "pass1", token: "" },
|
|
||||||
{ mobile: "+8613800000002", password: "pass2", token: "" },
|
|
||||||
{ mobile: "+8613800000003", password: "pass3", token: "" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keys_only: {
|
|
||||||
name: '仅 API 密钥',
|
|
||||||
desc: '仅添加 API 访问密钥',
|
|
||||||
config: {
|
|
||||||
keys: ["key-1", "key-2", "key-3"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
||||||
|
const { t } = useI18n()
|
||||||
const [jsonInput, setJsonInput] = useState('')
|
const [jsonInput, setJsonInput] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [result, setResult] = useState(null)
|
const [result, setResult] = useState(null)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
const apiFetch = authFetch || fetch
|
const apiFetch = authFetch || fetch
|
||||||
|
const templates = {
|
||||||
|
full: {
|
||||||
|
name: t('batchImport.templates.full.name'),
|
||||||
|
desc: t('batchImport.templates.full.desc'),
|
||||||
|
config: {
|
||||||
|
keys: ["your-api-key-1", "your-api-key-2"],
|
||||||
|
accounts: [
|
||||||
|
{ email: "user1@example.com", password: "password1", token: "" },
|
||||||
|
{ email: "user2@example.com", password: "password2", token: "" },
|
||||||
|
{ mobile: "+8613800138001", password: "password3", token: "" }
|
||||||
|
],
|
||||||
|
claude_model_mapping: {
|
||||||
|
fast: "deepseek-chat",
|
||||||
|
slow: "deepseek-reasoner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
email_only: {
|
||||||
|
name: t('batchImport.templates.emailOnly.name'),
|
||||||
|
desc: t('batchImport.templates.emailOnly.desc'),
|
||||||
|
config: {
|
||||||
|
keys: ["your-api-key"],
|
||||||
|
accounts: [
|
||||||
|
{ email: "account1@example.com", password: "pass1", token: "" },
|
||||||
|
{ email: "account2@example.com", password: "pass2", token: "" },
|
||||||
|
{ email: "account3@example.com", password: "pass3", token: "" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mobile_only: {
|
||||||
|
name: t('batchImport.templates.mobileOnly.name'),
|
||||||
|
desc: t('batchImport.templates.mobileOnly.desc'),
|
||||||
|
config: {
|
||||||
|
keys: ["your-api-key"],
|
||||||
|
accounts: [
|
||||||
|
{ mobile: "+8613800000001", password: "pass1", token: "" },
|
||||||
|
{ mobile: "+8613800000002", password: "pass2", token: "" },
|
||||||
|
{ mobile: "+8613800000003", password: "pass3", token: "" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keys_only: {
|
||||||
|
name: t('batchImport.templates.keysOnly.name'),
|
||||||
|
desc: t('batchImport.templates.keysOnly.desc'),
|
||||||
|
config: {
|
||||||
|
keys: ["key-1", "key-2", "key-3"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleImport = async () => {
|
const handleImport = async () => {
|
||||||
if (!jsonInput.trim()) {
|
if (!jsonInput.trim()) {
|
||||||
onMessage('error', '请输入 JSON 配置内容')
|
onMessage('error', t('batchImport.enterJson'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
|||||||
try {
|
try {
|
||||||
config = JSON.parse(jsonInput)
|
config = JSON.parse(jsonInput)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '无效的 JSON 格式')
|
onMessage('error', t('messages.invalidJson'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,23 +86,23 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setResult(data)
|
setResult(data)
|
||||||
onMessage('success', `导入成功: ${data.imported_keys} 个密钥, ${data.imported_accounts} 个账号`)
|
onMessage('success', t('batchImport.importSuccess', { keys: data.imported_keys, accounts: data.imported_accounts }))
|
||||||
onRefresh()
|
onRefresh()
|
||||||
} else {
|
} else {
|
||||||
onMessage('error', data.detail || '导入失败')
|
onMessage('error', data.detail || t('messages.importFailed'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '网络错误')
|
onMessage('error', t('messages.networkError'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadTemplate = (key) => {
|
const loadTemplate = (key) => {
|
||||||
const tpl = TEMPLATES[key]
|
const tpl = templates[key]
|
||||||
if (tpl) {
|
if (tpl) {
|
||||||
setJsonInput(JSON.stringify(tpl.config, null, 2))
|
setJsonInput(JSON.stringify(tpl.config, null, 2))
|
||||||
onMessage('info', `已加载模板: ${tpl.name}`)
|
onMessage('info', t('batchImport.templateLoaded', { name: tpl.name }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,10 +112,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
setJsonInput(JSON.stringify(JSON.parse(data.json), null, 2))
|
setJsonInput(JSON.stringify(JSON.parse(data.json), null, 2))
|
||||||
onMessage('success', '当前配置已加载')
|
onMessage('success', t('batchImport.currentConfigLoaded'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '获取配置失败')
|
onMessage('error', t('batchImport.fetchConfigFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,10 +127,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
|||||||
await navigator.clipboard.writeText(data.base64)
|
await navigator.clipboard.writeText(data.base64)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
onMessage('success', 'Base64 配置已复制到剪贴板')
|
onMessage('success', t('batchImport.copySuccess'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '复制失败')
|
onMessage('error', t('messages.copyFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +141,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
|||||||
<div className="bg-card border border-border rounded-xl p-5 shadow-sm">
|
<div className="bg-card border border-border rounded-xl p-5 shadow-sm">
|
||||||
<h3 className="font-semibold flex items-center gap-2 mb-4">
|
<h3 className="font-semibold flex items-center gap-2 mb-4">
|
||||||
<FileCode className="w-4 h-4 text-primary" />
|
<FileCode className="w-4 h-4 text-primary" />
|
||||||
快速模板
|
{t('batchImport.quickTemplates')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Object.entries(TEMPLATES).map(([key, tpl]) => (
|
{Object.entries(templates).map(([key, tpl]) => (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
onClick={() => loadTemplate(key)}
|
onClick={() => loadTemplate(key)}
|
||||||
@@ -159,20 +160,20 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
|||||||
<div className="bg-linear-to-br from-primary/10 to-transparent border border-primary/20 rounded-xl p-5 shadow-sm">
|
<div className="bg-linear-to-br from-primary/10 to-transparent border border-primary/20 rounded-xl p-5 shadow-sm">
|
||||||
<h3 className="font-semibold flex items-center gap-2 mb-2 text-primary">
|
<h3 className="font-semibold flex items-center gap-2 mb-2 text-primary">
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
数据导出
|
{t('batchImport.dataExport')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
获取配置的 Base64 字符串,用于 Vercel 环境变量。
|
{t('batchImport.dataExportDesc')}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={copyBase64}
|
onClick={copyBase64}
|
||||||
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"
|
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 ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||||
{copied ? '已复制' : '复制 Base64 配置'}
|
{copied ? t('batchImport.copied') : t('batchImport.copyBase64')}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-[10px] text-muted-foreground mt-2 text-center">
|
<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>
|
{t('batchImport.variableName')}: <code className="bg-background px-1 py-0.5 rounded border border-border">DS2API_CONFIG_JSON</code>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,14 +183,14 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
|||||||
<div className="p-4 border-b border-border flex items-center justify-between bg-muted/20">
|
<div className="p-4 border-b border-border flex items-center justify-between bg-muted/20">
|
||||||
<h3 className="font-semibold flex items-center gap-2">
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
<Upload className="w-4 h-4 text-primary" />
|
<Upload className="w-4 h-4 text-primary" />
|
||||||
JSON 编辑器
|
{t('batchImport.jsonEditor')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-2">
|
<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">
|
<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">
|
||||||
加载当前配置
|
{t('batchImport.loadCurrentConfig')}
|
||||||
</button>
|
</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">
|
<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 ? '正在导入...' : '应用配置'}
|
{loading ? t('batchImport.importing') : t('batchImport.applyConfig')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,10 +218,10 @@ export default function BatchImport({ onRefresh, onMessage, authFetch }) {
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h4 className={clsx("font-medium", result.imported_keys || result.imported_accounts ? "text-emerald-500" : "text-destructive")}>
|
<h4 className={clsx("font-medium", result.imported_keys || result.imported_accounts ? "text-emerald-500" : "text-destructive")}>
|
||||||
导入操作已完成
|
{t('batchImport.importComplete')}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm opacity-80 mt-1">
|
<p className="text-sm opacity-80 mt-1">
|
||||||
成功导入了 {result.imported_keys} 个 API 密钥,并更新了 {result.imported_accounts} 个账号。
|
{t('batchImport.importSummary', { keys: result.imported_keys, accounts: result.imported_accounts })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useI18n } from '../i18n'
|
||||||
|
import LanguageToggle from './LanguageToggle'
|
||||||
|
|
||||||
const LandingPage = ({ onEnter }) => {
|
const LandingPage = ({ onEnter }) => {
|
||||||
|
const { t } = useI18n()
|
||||||
return (
|
return (
|
||||||
<div className="landing-container min-h-screen relative overflow-hidden flex flex-col items-center justify-center p-6 text-center">
|
<div className="landing-container min-h-screen relative overflow-hidden flex flex-col items-center justify-center p-6 text-center">
|
||||||
{/* Animated Background Elements - using Tailwind with some custom CSS in styles.css if needed,
|
{/* Animated Background Elements - using Tailwind with some custom CSS in styles.css if needed,
|
||||||
@@ -84,6 +87,10 @@ const LandingPage = ({ onEnter }) => {
|
|||||||
<div className="blob" style={{ top: '10%', left: '15%' }} />
|
<div className="blob" style={{ top: '10%', left: '15%' }} />
|
||||||
<div className="blob" style={{ bottom: '10%', right: '15%', animationDelay: '-5s' }} />
|
<div className="blob" style={{ bottom: '10%', right: '15%', animationDelay: '-5s' }} />
|
||||||
|
|
||||||
|
<div className="absolute top-6 right-6 z-20">
|
||||||
|
<LanguageToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="landing-content">
|
<div className="landing-content">
|
||||||
<header className="mb-12">
|
<header className="mb-12">
|
||||||
<h1 className="logo-text">DS2API</h1>
|
<h1 className="logo-text">DS2API</h1>
|
||||||
@@ -97,14 +104,14 @@ const LandingPage = ({ onEnter }) => {
|
|||||||
onClick={onEnter}
|
onClick={onEnter}
|
||||||
className="btn-premium text-white px-8 py-3 rounded-xl font-bold transition-all flex items-center gap-2"
|
className="btn-premium text-white px-8 py-3 rounded-xl font-bold transition-all flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span>🎛️</span> 管理面板
|
<span>🎛️</span> {t('landing.adminConsole')}
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href="/v1/models"
|
href="/v1/models"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="glass-card text-white px-8 py-3 rounded-xl font-semibold transition-all flex items-center gap-2"
|
className="glass-card text-white px-8 py-3 rounded-xl font-semibold transition-all flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span>📡</span> API 状态
|
<span>📡</span> {t('landing.apiStatus')}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/CJackHwang/ds2api"
|
href="https://github.com/CJackHwang/ds2api"
|
||||||
@@ -117,10 +124,10 @@ const LandingPage = ({ onEnter }) => {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6 text-left">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6 text-left">
|
||||||
{[
|
{[
|
||||||
{ icon: '🚀', title: '全面兼容', desc: '适配 OpenAI 与 Claude 格式' },
|
{ icon: '🚀', title: t('landing.features.compatibility.title'), desc: t('landing.features.compatibility.desc') },
|
||||||
{ icon: '⚖️', title: '负载均衡', desc: '智能轮询,稳定高效' },
|
{ icon: '⚖️', title: t('landing.features.loadBalancing.title'), desc: t('landing.features.loadBalancing.desc') },
|
||||||
{ icon: '🧠', title: '深度思考', desc: '支持推理过程输出' },
|
{ icon: '🧠', title: t('landing.features.reasoning.title'), desc: t('landing.features.reasoning.desc') },
|
||||||
{ icon: '🔍', title: '联网搜索', desc: '集成原生网页搜索能力' },
|
{ icon: '🔍', title: t('landing.features.search.title'), desc: t('landing.features.search.desc') },
|
||||||
].map((feature, idx) => (
|
].map((feature, idx) => (
|
||||||
<div key={idx} className="glass-card p-6 rounded-2xl">
|
<div key={idx} className="glass-card p-6 rounded-2xl">
|
||||||
<span className="text-2xl mb-4 block">{feature.icon}</span>
|
<span className="text-2xl mb-4 block">{feature.icon}</span>
|
||||||
|
|||||||
18
webui/src/components/LanguageToggle.jsx
Normal file
18
webui/src/components/LanguageToggle.jsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useI18n } from '../i18n'
|
||||||
|
|
||||||
|
export default function LanguageToggle({ className = '' }) {
|
||||||
|
const { lang, setLang, t } = useI18n()
|
||||||
|
const nextLang = lang === 'zh' ? 'en' : 'zh'
|
||||||
|
const label = nextLang === 'zh' ? t('language.chinese') : t('language.english')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLang(nextLang)}
|
||||||
|
className={`text-xs font-semibold px-2 py-1 rounded-md border border-border bg-secondary/50 text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors ${className}`}
|
||||||
|
title={t('language.label')}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Key, ArrowRight, ShieldCheck, Lock, Check } from 'lucide-react'
|
import { Key, ArrowRight, ShieldCheck, Lock, Check } from 'lucide-react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { useI18n } from '../i18n'
|
||||||
|
import LanguageToggle from './LanguageToggle'
|
||||||
|
|
||||||
export default function Login({ onLogin, onMessage }) {
|
export default function Login({ onLogin, onMessage }) {
|
||||||
|
const { t } = useI18n()
|
||||||
const [adminKey, setAdminKey] = useState('')
|
const [adminKey, setAdminKey] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [remember, setRemember] = useState(true)
|
const [remember, setRemember] = useState(true)
|
||||||
@@ -32,10 +35,10 @@ export default function Login({ onLogin, onMessage }) {
|
|||||||
onMessage('warning', data.message)
|
onMessage('warning', data.message)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onMessage('error', data.detail || '登录失败')
|
onMessage('error', data.detail || t('login.signInFailed'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '网络错误: ' + e.message)
|
onMessage('error', t('login.networkError', { error: e.message }))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -43,6 +46,9 @@ export default function Login({ onLogin, onMessage }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background text-foreground">
|
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background text-foreground">
|
||||||
|
<div className="absolute top-6 right-6">
|
||||||
|
<LanguageToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="w-full max-w-[400px] relative z-10 animate-in fade-in zoom-in-95 duration-200">
|
<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="w-full bg-card border border-border rounded-xl p-8 shadow-sm">
|
||||||
@@ -50,13 +56,13 @@ export default function Login({ onLogin, onMessage }) {
|
|||||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-2">
|
<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" />
|
<Lock className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">欢迎回来</h1>
|
<h1 className="text-3xl font-bold tracking-tight text-foreground">{t('login.welcome')}</h1>
|
||||||
<p className="text-sm text-muted-foreground/80">请输入管理员密钥以继续</p>
|
<p className="text-sm text-muted-foreground/80">{t('login.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleLogin} className="space-y-5 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-150">
|
<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">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-widest ml-1">管理员密钥</label>
|
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-widest ml-1">{t('login.adminKeyLabel')}</label>
|
||||||
<div className="relative group">
|
<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">
|
<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" />
|
<Key className="w-4 h-4" />
|
||||||
@@ -64,7 +70,7 @@ export default function Login({ onLogin, onMessage }) {
|
|||||||
<input
|
<input
|
||||||
type="password"
|
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"
|
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="输入您的管理员密钥..."
|
placeholder={t('login.adminKeyPlaceholder')}
|
||||||
value={adminKey}
|
value={adminKey}
|
||||||
onChange={e => setAdminKey(e.target.value)}
|
onChange={e => setAdminKey(e.target.value)}
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -84,7 +90,7 @@ export default function Login({ onLogin, onMessage }) {
|
|||||||
<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>
|
<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" />
|
<Check className="absolute w-3 h-3 text-primary-foreground opacity-0 peer-checked:opacity-100 left-0.5 transition-opacity" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors">记住登录状态</span>
|
<span className="text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors">{t('login.rememberSession')}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,7 +103,7 @@ export default function Login({ onLogin, onMessage }) {
|
|||||||
<div className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground 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">
|
<div className="flex items-center gap-2">
|
||||||
<span>登录</span>
|
<span>{t('login.signIn')}</span>
|
||||||
<ArrowRight className="w-4 h-4" />
|
<ArrowRight className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -107,13 +113,13 @@ export default function Login({ onLogin, onMessage }) {
|
|||||||
<div className="mt-6 pt-6 border-t border-border flex justify-center">
|
<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">
|
<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" />
|
<ShieldCheck className="w-3 h-3" />
|
||||||
<span>安全连接</span>
|
<span>{t('login.secureConnection')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
<p className="text-[10px] text-muted-foreground/30 font-mono text-center">DS2API 管理员门户</p>
|
<p className="text-[10px] text-muted-foreground/30 font-mono text-center">{t('login.adminPortal')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Cloud, ArrowRight, ExternalLink, Info, CheckCircle2, XCircle } from 'lucide-react'
|
import { Cloud, ArrowRight, ExternalLink, Info, CheckCircle2, XCircle } from 'lucide-react'
|
||||||
|
import { useI18n } from '../i18n'
|
||||||
|
|
||||||
export default function VercelSync({ onMessage, authFetch }) {
|
export default function VercelSync({ onMessage, authFetch }) {
|
||||||
|
const { t } = useI18n()
|
||||||
const [vercelToken, setVercelToken] = useState('')
|
const [vercelToken, setVercelToken] = useState('')
|
||||||
const [projectId, setProjectId] = useState('')
|
const [projectId, setProjectId] = useState('')
|
||||||
const [teamId, setTeamId] = useState('')
|
const [teamId, setTeamId] = useState('')
|
||||||
@@ -32,11 +34,11 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
const tokenToUse = preconfig?.has_token && !vercelToken ? '__USE_PRECONFIG__' : vercelToken
|
const tokenToUse = preconfig?.has_token && !vercelToken ? '__USE_PRECONFIG__' : vercelToken
|
||||||
|
|
||||||
if (!tokenToUse && !preconfig?.has_token) {
|
if (!tokenToUse && !preconfig?.has_token) {
|
||||||
onMessage('error', '需要 Vercel 访问令牌')
|
onMessage('error', t('vercel.tokenRequired'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
onMessage('error', '需要项目 ID')
|
onMessage('error', t('vercel.projectRequired'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
vercel_token: vercelToken,
|
vercel_token: tokenToUse,
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
team_id: teamId || undefined,
|
team_id: teamId || undefined,
|
||||||
}),
|
}),
|
||||||
@@ -58,10 +60,10 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
onMessage('success', data.message)
|
onMessage('success', data.message)
|
||||||
} else {
|
} else {
|
||||||
setResult({ ...data, success: false })
|
setResult({ ...data, success: false })
|
||||||
onMessage('error', data.detail || '同步失败')
|
onMessage('error', data.detail || t('vercel.syncFailed'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onMessage('error', '网络错误')
|
onMessage('error', t('vercel.networkError'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -74,26 +76,26 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
<div className="border-b border-border pb-6">
|
<div className="border-b border-border pb-6">
|
||||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||||
<Cloud className="w-6 h-6 text-primary" />
|
<Cloud className="w-6 h-6 text-primary" />
|
||||||
Vercel 部署
|
{t('vercel.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground text-sm mt-1">
|
<p className="text-muted-foreground text-sm mt-1">
|
||||||
将当前密钥和账号配置直接同步到 Vercel 环境变量中。
|
{t('vercel.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium flex items-center justify-between">
|
<label className="text-sm font-medium flex items-center justify-between">
|
||||||
Vercel 访问令牌
|
{t('vercel.tokenLabel')}
|
||||||
<a href="https://vercel.com/account/tokens" target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline flex items-center gap-1">
|
<a href="https://vercel.com/account/tokens" target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline flex items-center gap-1">
|
||||||
获取令牌 <ExternalLink className="w-3 h-3" />
|
{t('vercel.getToken')} <ExternalLink className="w-3 h-3" />
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="password"
|
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"
|
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 ? "正在使用预配置的令牌" : "输入 Vercel 访问令牌"}
|
placeholder={preconfig?.has_token ? t('vercel.tokenPlaceholderPreconfig') : t('vercel.tokenPlaceholder')}
|
||||||
value={vercelToken}
|
value={vercelToken}
|
||||||
onChange={e => setVercelToken(e.target.value)}
|
onChange={e => setVercelToken(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -106,7 +108,7 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">项目 ID</label>
|
<label className="text-sm font-medium">{t('vercel.projectIdLabel')}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
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"
|
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"
|
||||||
@@ -114,12 +116,12 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
value={projectId}
|
value={projectId}
|
||||||
onChange={e => setProjectId(e.target.value)}
|
onChange={e => setProjectId(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">可在项目设置 (Project Settings) → 常规 (General) 中找到</p>
|
<p className="text-xs text-muted-foreground">{t('vercel.projectIdHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium flex items-center gap-2">
|
<label className="text-sm font-medium flex items-center gap-2">
|
||||||
团队 ID <span className="text-xs text-muted-foreground font-normal">(可选)</span>
|
{t('vercel.teamIdLabel')} <span className="text-xs text-muted-foreground font-normal">({t('vercel.optional')})</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -140,16 +142,16 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
<span className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
||||||
正在同步...
|
{t('vercel.syncing')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
同步并重新部署 <ArrowRight className="w-4 h-4" />
|
{t('vercel.syncRedeploy')} <ArrowRight className="w-4 h-4" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-center text-muted-foreground mt-4">
|
<p className="text-xs text-center text-muted-foreground mt-4">
|
||||||
这将触发 Vercel 的重新部署,大约需要 30-60 秒。
|
{t('vercel.redeployHint')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,14 +172,14 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
)}
|
)}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h3 className={`font-semibold text-lg ${result.success ? 'text-emerald-500' : 'text-destructive'}`}>
|
<h3 className={`font-semibold text-lg ${result.success ? 'text-emerald-500' : 'text-destructive'}`}>
|
||||||
{result.success ? '同步成功' : '同步失败'}
|
{result.success ? t('vercel.syncSucceeded') : t('vercel.syncFailedLabel')}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm opacity-90">{result.message}</p>
|
<p className="text-sm opacity-90">{result.message}</p>
|
||||||
|
|
||||||
{result.deployment_url && (
|
{result.deployment_url && (
|
||||||
<div className="pt-3 mt-3 border-t border-emerald-500/20">
|
<div className="pt-3 mt-3 border-t border-emerald-500/20">
|
||||||
<a href={`https://${result.deployment_url}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-sm font-medium hover:underline">
|
<a href={`https://${result.deployment_url}`} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-sm font-medium hover:underline">
|
||||||
访问部署地址 <ExternalLink className="w-3 h-3" />
|
{t('vercel.openDeployment')} <ExternalLink className="w-3 h-3" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -189,24 +191,26 @@ export default function VercelSync({ onMessage, authFetch }) {
|
|||||||
<div className="bg-secondary/20 border border-border rounded-xl p-6">
|
<div className="bg-secondary/20 border border-border rounded-xl p-6">
|
||||||
<h3 className="font-semibold flex items-center gap-2 mb-4">
|
<h3 className="font-semibold flex items-center gap-2 mb-4">
|
||||||
<Info className="w-5 h-5 text-primary" />
|
<Info className="w-5 h-5 text-primary" />
|
||||||
工作原理
|
{t('vercel.howItWorks')}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-4">
|
<ul className="space-y-4">
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">1</span>
|
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">1</span>
|
||||||
<p className="text-sm text-muted-foreground">当前配置 (密钥和账号) 被导出为 JSON 字符串。</p>
|
<p className="text-sm text-muted-foreground">{t('vercel.steps.one')}</p>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">2</span>
|
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">2</span>
|
||||||
<p className="text-sm text-muted-foreground">JSON 被编码为 Base64 以确保格式兼容性。</p>
|
<p className="text-sm text-muted-foreground">{t('vercel.steps.two')}</p>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">3</span>
|
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">3</span>
|
||||||
<p className="text-sm text-muted-foreground">更新 Vercel 项目中的 <code className="bg-background px-1 py-0.5 rounded border border-border text-xs">DS2API_CONFIG_JSON</code> 环境变量。</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t('vercel.steps.three')} <code className="bg-background px-1 py-0.5 rounded border border-border text-xs">DS2API_CONFIG_JSON</code>
|
||||||
|
</p>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">4</span>
|
<span className="shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground">4</span>
|
||||||
<p className="text-sm text-muted-foreground">触发重新部署以应用新的环境变量。</p>
|
<p className="text-sm text-muted-foreground">{t('vercel.steps.four')}</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
62
webui/src/i18n.jsx
Normal file
62
webui/src/i18n.jsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
|
import en from './locales/en.json'
|
||||||
|
import zh from './locales/zh.json'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'ds2api_lang'
|
||||||
|
const translations = { en, zh }
|
||||||
|
|
||||||
|
const I18nContext = createContext({
|
||||||
|
lang: 'zh',
|
||||||
|
setLang: () => {},
|
||||||
|
t: (key) => key,
|
||||||
|
})
|
||||||
|
|
||||||
|
const getBrowserLang = () => {
|
||||||
|
if (typeof navigator === 'undefined') return 'zh'
|
||||||
|
return navigator.language?.toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValue = (obj, key) => {
|
||||||
|
if (!obj) return undefined
|
||||||
|
return key.split('.').reduce((acc, part) => (acc ? acc[part] : undefined), obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMessage = (message, vars) => {
|
||||||
|
if (!vars) return message
|
||||||
|
return message.replace(/\{(\w+)\}/g, (match, key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
||||||
|
return vars[key]
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const I18nProvider = ({ children }) => {
|
||||||
|
const [lang, setLang] = useState(() => {
|
||||||
|
if (typeof localStorage === 'undefined') return getBrowserLang()
|
||||||
|
return localStorage.getItem(STORAGE_KEY) || getBrowserLang()
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem(STORAGE_KEY, lang)
|
||||||
|
}
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.documentElement.lang = lang === 'zh' ? 'zh-CN' : 'en'
|
||||||
|
}
|
||||||
|
}, [lang])
|
||||||
|
|
||||||
|
const t = useMemo(() => {
|
||||||
|
return (key, vars) => {
|
||||||
|
const value = getValue(translations[lang], key) ?? getValue(translations.en, key) ?? key
|
||||||
|
if (typeof value !== 'string') return value
|
||||||
|
return formatMessage(value, vars)
|
||||||
|
}
|
||||||
|
}, [lang])
|
||||||
|
|
||||||
|
const contextValue = useMemo(() => ({ lang, setLang, t }), [lang, t])
|
||||||
|
|
||||||
|
return <I18nContext.Provider value={contextValue}>{children}</I18nContext.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useI18n = () => useContext(I18nContext)
|
||||||
231
webui/src/locales/en.json
Normal file
231
webui/src/locales/en.json
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
{
|
||||||
|
"language": {
|
||||||
|
"label": "Language",
|
||||||
|
"english": "English",
|
||||||
|
"chinese": "中文"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"accounts": {
|
||||||
|
"label": "Account Management",
|
||||||
|
"desc": "Manage the DeepSeek account pool"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"label": "API Test",
|
||||||
|
"desc": "Test API connectivity and responses"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"label": "Batch Import",
|
||||||
|
"desc": "Bulk import account configuration"
|
||||||
|
},
|
||||||
|
"vercel": {
|
||||||
|
"label": "Vercel Sync",
|
||||||
|
"desc": "Sync configuration to Vercel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"onlineAdminConsole": "Online Admin Console",
|
||||||
|
"systemStatus": "System Status",
|
||||||
|
"statusOnline": "Online",
|
||||||
|
"accounts": "Accounts",
|
||||||
|
"keys": "Keys",
|
||||||
|
"signOut": "Sign out"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"expired": "Authentication expired. Please sign in again.",
|
||||||
|
"checking": "Checking authentication status..."
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"fetchConfig": "Failed to fetch configuration: {error}"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"add": "Add",
|
||||||
|
"delete": "Delete",
|
||||||
|
"copy": "Copy",
|
||||||
|
"generate": "Generate",
|
||||||
|
"test": "Test",
|
||||||
|
"testing": "Testing...",
|
||||||
|
"loading": "Loading..."
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"deleted": "Deleted successfully",
|
||||||
|
"deleteFailed": "Delete failed",
|
||||||
|
"failedToAdd": "Failed to add",
|
||||||
|
"networkError": "Network error.",
|
||||||
|
"requestFailed": "Request failed.",
|
||||||
|
"generationStopped": "Generation stopped.",
|
||||||
|
"invalidJson": "Invalid JSON format.",
|
||||||
|
"importFailed": "Import failed.",
|
||||||
|
"copyFailed": "Copy failed."
|
||||||
|
},
|
||||||
|
"landing": {
|
||||||
|
"adminConsole": "Admin Console",
|
||||||
|
"apiStatus": "API Status",
|
||||||
|
"features": {
|
||||||
|
"compatibility": {
|
||||||
|
"title": "Full Compatibility",
|
||||||
|
"desc": "OpenAI & Claude format support"
|
||||||
|
},
|
||||||
|
"loadBalancing": {
|
||||||
|
"title": "Load Balancing",
|
||||||
|
"desc": "Smart rotation with stable throughput"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"title": "Deep Reasoning",
|
||||||
|
"desc": "Expose reasoning traces when enabled"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"title": "Web Search",
|
||||||
|
"desc": "Integrated native web search"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accountManager": {
|
||||||
|
"addKeySuccess": "API key added successfully.",
|
||||||
|
"addAccountSuccess": "Account added successfully.",
|
||||||
|
"requiredFields": "Password and email/mobile are required.",
|
||||||
|
"deleteKeyConfirm": "Are you sure you want to delete this API key?",
|
||||||
|
"deleteAccountConfirm": "Are you sure you want to delete this account?",
|
||||||
|
"testAllConfirm": "Test API connectivity for all accounts?",
|
||||||
|
"testAllCompleted": "Completed: {success}/{total} available",
|
||||||
|
"testFailed": "Test failed: {error}",
|
||||||
|
"available": "Available",
|
||||||
|
"inUse": "In use",
|
||||||
|
"totalPool": "Total pool",
|
||||||
|
"accountsUnit": "accounts",
|
||||||
|
"threadsUnit": "threads",
|
||||||
|
"apiKeysTitle": "API Keys",
|
||||||
|
"apiKeysDesc": "Manage the API access key pool",
|
||||||
|
"addKey": "Add key",
|
||||||
|
"copied": "Copied",
|
||||||
|
"copyKeyTitle": "Copy key",
|
||||||
|
"deleteKeyTitle": "Delete key",
|
||||||
|
"noApiKeys": "No API keys found.",
|
||||||
|
"accountsTitle": "DeepSeek Accounts",
|
||||||
|
"accountsDesc": "Manage the DeepSeek account pool",
|
||||||
|
"testAll": "Test all",
|
||||||
|
"addAccount": "Add account",
|
||||||
|
"testingAllAccounts": "Testing all accounts...",
|
||||||
|
"sessionActive": "Session active",
|
||||||
|
"reauthRequired": "Re-auth required",
|
||||||
|
"noAccounts": "No accounts found.",
|
||||||
|
"modalAddKeyTitle": "Add API key",
|
||||||
|
"newKeyLabel": "New key value",
|
||||||
|
"newKeyPlaceholder": "Enter a custom API key",
|
||||||
|
"generate": "Generate",
|
||||||
|
"generateHint": "Click Generate to create a random key.",
|
||||||
|
"addKeyLoading": "Adding...",
|
||||||
|
"addKeyAction": "Add key",
|
||||||
|
"modalAddAccountTitle": "Add DeepSeek account",
|
||||||
|
"emailOptional": "Email (optional)",
|
||||||
|
"mobileOptional": "Mobile (optional)",
|
||||||
|
"passwordLabel": "Password",
|
||||||
|
"passwordPlaceholder": "Account password",
|
||||||
|
"addAccountLoading": "Adding...",
|
||||||
|
"addAccountAction": "Add account"
|
||||||
|
},
|
||||||
|
"apiTester": {
|
||||||
|
"defaultMessage": "Hello, please introduce yourself in one sentence.",
|
||||||
|
"models": {
|
||||||
|
"chat": "Non-reasoning model",
|
||||||
|
"reasoner": "Reasoning model",
|
||||||
|
"chatSearch": "Non-reasoning model (with search)",
|
||||||
|
"reasonerSearch": "Reasoning model (with search)"
|
||||||
|
},
|
||||||
|
"missingApiKey": "Please provide an API key.",
|
||||||
|
"requestFailed": "Request failed.",
|
||||||
|
"networkError": "Network error: {error}",
|
||||||
|
"testSuccess": "{account}: Test successful ({time}ms)",
|
||||||
|
"config": "Configuration",
|
||||||
|
"modelLabel": "Model",
|
||||||
|
"accountStrategy": "Account Strategy",
|
||||||
|
"randomRotation": "🎲 Random rotation (streaming preview supported)",
|
||||||
|
"apiKeyOptional": "API Key (optional)",
|
||||||
|
"apiKeyDefault": "Default: ...{suffix}",
|
||||||
|
"apiKeyPlaceholder": "Enter a custom key",
|
||||||
|
"statusError": "Error",
|
||||||
|
"reasoningTrace": "Reasoning Trace",
|
||||||
|
"generating": "Generating response...",
|
||||||
|
"enterMessage": "Enter a message...",
|
||||||
|
"adminConsoleLabel": "DeepSeek admin console"
|
||||||
|
},
|
||||||
|
"batchImport": {
|
||||||
|
"templates": {
|
||||||
|
"full": {
|
||||||
|
"name": "Full configuration template",
|
||||||
|
"desc": "Includes keys, accounts, and model mapping"
|
||||||
|
},
|
||||||
|
"emailOnly": {
|
||||||
|
"name": "Email-only accounts",
|
||||||
|
"desc": "Batch import accounts using email login"
|
||||||
|
},
|
||||||
|
"mobileOnly": {
|
||||||
|
"name": "Mobile-only accounts",
|
||||||
|
"desc": "Batch import accounts using mobile login"
|
||||||
|
},
|
||||||
|
"keysOnly": {
|
||||||
|
"name": "API keys only",
|
||||||
|
"desc": "Add API access keys only"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enterJson": "Please provide JSON configuration content.",
|
||||||
|
"importSuccess": "Import successful: {keys} keys, {accounts} accounts",
|
||||||
|
"templateLoaded": "Template loaded: {name}",
|
||||||
|
"currentConfigLoaded": "Current configuration loaded.",
|
||||||
|
"fetchConfigFailed": "Failed to fetch configuration.",
|
||||||
|
"copySuccess": "Base64 configuration copied to clipboard.",
|
||||||
|
"quickTemplates": "Quick Templates",
|
||||||
|
"dataExport": "Data Export",
|
||||||
|
"dataExportDesc": "Copy the Base64-encoded configuration for Vercel environment variables.",
|
||||||
|
"copyBase64": "Copy Base64 config",
|
||||||
|
"copied": "Copied",
|
||||||
|
"variableName": "Variable name",
|
||||||
|
"jsonEditor": "JSON Editor",
|
||||||
|
"loadCurrentConfig": "Load current config",
|
||||||
|
"applyConfig": "Apply config",
|
||||||
|
"importing": "Importing...",
|
||||||
|
"importComplete": "Import complete",
|
||||||
|
"importSummary": "Imported {keys} API keys and updated {accounts} accounts."
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"welcome": "Welcome back",
|
||||||
|
"subtitle": "Enter your admin key to continue",
|
||||||
|
"adminKeyLabel": "Admin key",
|
||||||
|
"adminKeyPlaceholder": "Enter your admin key...",
|
||||||
|
"rememberSession": "Remember this session",
|
||||||
|
"signIn": "Sign in",
|
||||||
|
"secureConnection": "Secure connection",
|
||||||
|
"adminPortal": "DS2API admin portal",
|
||||||
|
"signInFailed": "Sign-in failed.",
|
||||||
|
"networkError": "Network error: {error}"
|
||||||
|
},
|
||||||
|
"vercel": {
|
||||||
|
"tokenRequired": "Vercel access token is required.",
|
||||||
|
"projectRequired": "Project ID is required.",
|
||||||
|
"syncFailed": "Sync failed.",
|
||||||
|
"networkError": "Network error.",
|
||||||
|
"title": "Vercel Deployment",
|
||||||
|
"description": "Sync the current keys and accounts directly to Vercel environment variables.",
|
||||||
|
"tokenLabel": "Vercel Access Token",
|
||||||
|
"getToken": "Get token",
|
||||||
|
"tokenPlaceholderPreconfig": "Using preconfigured token",
|
||||||
|
"tokenPlaceholder": "Enter Vercel access token",
|
||||||
|
"projectIdLabel": "Project ID",
|
||||||
|
"projectIdHint": "Find it in Project Settings → General.",
|
||||||
|
"teamIdLabel": "Team ID",
|
||||||
|
"optional": "optional",
|
||||||
|
"syncing": "Syncing...",
|
||||||
|
"syncRedeploy": "Sync & redeploy",
|
||||||
|
"redeployHint": "This triggers a Vercel redeploy and usually takes 30–60 seconds.",
|
||||||
|
"syncSucceeded": "Sync succeeded",
|
||||||
|
"syncFailedLabel": "Sync failed",
|
||||||
|
"openDeployment": "Open deployment",
|
||||||
|
"howItWorks": "How it works",
|
||||||
|
"steps": {
|
||||||
|
"one": "The current configuration (keys and accounts) is exported as JSON.",
|
||||||
|
"two": "The JSON is Base64-encoded for safe formatting.",
|
||||||
|
"three": "Update the env var in Vercel:",
|
||||||
|
"four": "Trigger a redeploy to apply the updated environment variables."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
231
webui/src/locales/zh.json
Normal file
231
webui/src/locales/zh.json
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
{
|
||||||
|
"language": {
|
||||||
|
"label": "语言",
|
||||||
|
"english": "English",
|
||||||
|
"chinese": "中文"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"accounts": {
|
||||||
|
"label": "账号管理",
|
||||||
|
"desc": "管理 DeepSeek 账号池"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"label": "API 测试",
|
||||||
|
"desc": "测试 API 连接与响应"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"label": "批量导入",
|
||||||
|
"desc": "批量导入账号配置"
|
||||||
|
},
|
||||||
|
"vercel": {
|
||||||
|
"label": "Vercel 同步",
|
||||||
|
"desc": "同步配置到 Vercel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"onlineAdminConsole": "在线管理面板",
|
||||||
|
"systemStatus": "系统状态",
|
||||||
|
"statusOnline": "在线",
|
||||||
|
"accounts": "账号",
|
||||||
|
"keys": "密钥",
|
||||||
|
"signOut": "退出登录"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"expired": "认证已过期,请重新登录",
|
||||||
|
"checking": "正在检查登录状态..."
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"fetchConfig": "获取配置失败: {error}"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"cancel": "取消",
|
||||||
|
"add": "添加",
|
||||||
|
"delete": "删除",
|
||||||
|
"copy": "复制",
|
||||||
|
"generate": "生成",
|
||||||
|
"test": "测试",
|
||||||
|
"testing": "正在测试...",
|
||||||
|
"loading": "加载中..."
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"deleted": "删除成功",
|
||||||
|
"deleteFailed": "删除失败",
|
||||||
|
"failedToAdd": "添加失败",
|
||||||
|
"networkError": "网络错误",
|
||||||
|
"requestFailed": "请求失败",
|
||||||
|
"generationStopped": "已停止生成",
|
||||||
|
"invalidJson": "无效的 JSON 格式",
|
||||||
|
"importFailed": "导入失败",
|
||||||
|
"copyFailed": "复制失败"
|
||||||
|
},
|
||||||
|
"landing": {
|
||||||
|
"adminConsole": "管理面板",
|
||||||
|
"apiStatus": "API 状态",
|
||||||
|
"features": {
|
||||||
|
"compatibility": {
|
||||||
|
"title": "全面兼容",
|
||||||
|
"desc": "适配 OpenAI 与 Claude 格式"
|
||||||
|
},
|
||||||
|
"loadBalancing": {
|
||||||
|
"title": "负载均衡",
|
||||||
|
"desc": "智能轮询,稳定高效"
|
||||||
|
},
|
||||||
|
"reasoning": {
|
||||||
|
"title": "深度思考",
|
||||||
|
"desc": "支持推理过程输出"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"title": "联网搜索",
|
||||||
|
"desc": "集成原生网页搜索能力"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"accountManager": {
|
||||||
|
"addKeySuccess": "API 密钥添加成功",
|
||||||
|
"addAccountSuccess": "账号添加成功",
|
||||||
|
"requiredFields": "需要填写密码以及邮箱或手机号",
|
||||||
|
"deleteKeyConfirm": "确定要删除此 API 密钥吗?",
|
||||||
|
"deleteAccountConfirm": "确定要删除此账号吗?",
|
||||||
|
"testAllConfirm": "测试所有账号的 API 连通性?",
|
||||||
|
"testAllCompleted": "完成:{success}/{total} 可用",
|
||||||
|
"testFailed": "测试失败: {error}",
|
||||||
|
"available": "可用",
|
||||||
|
"inUse": "正在使用",
|
||||||
|
"totalPool": "账号池总数",
|
||||||
|
"accountsUnit": "个账号",
|
||||||
|
"threadsUnit": "线程",
|
||||||
|
"apiKeysTitle": "API 密钥",
|
||||||
|
"apiKeysDesc": "管理 API 访问密钥池",
|
||||||
|
"addKey": "添加密钥",
|
||||||
|
"copied": "已复制",
|
||||||
|
"copyKeyTitle": "复制密钥",
|
||||||
|
"deleteKeyTitle": "删除密钥",
|
||||||
|
"noApiKeys": "未找到 API 密钥",
|
||||||
|
"accountsTitle": "DeepSeek 账号",
|
||||||
|
"accountsDesc": "管理 DeepSeek 账号池",
|
||||||
|
"testAll": "测试全部",
|
||||||
|
"addAccount": "添加账号",
|
||||||
|
"testingAllAccounts": "正在测试所有账号...",
|
||||||
|
"sessionActive": "已建立会话",
|
||||||
|
"reauthRequired": "需重新登录",
|
||||||
|
"noAccounts": "未找到任何账号",
|
||||||
|
"modalAddKeyTitle": "添加 API 密钥",
|
||||||
|
"newKeyLabel": "新密钥值",
|
||||||
|
"newKeyPlaceholder": "输入自定义 API 密钥",
|
||||||
|
"generate": "生成",
|
||||||
|
"generateHint": "点击「生成」自动创建随机密钥",
|
||||||
|
"addKeyLoading": "添加中...",
|
||||||
|
"addKeyAction": "添加密钥",
|
||||||
|
"modalAddAccountTitle": "添加 DeepSeek 账号",
|
||||||
|
"emailOptional": "邮箱 (可选)",
|
||||||
|
"mobileOptional": "手机号 (可选)",
|
||||||
|
"passwordLabel": "密码",
|
||||||
|
"passwordPlaceholder": "账号密码",
|
||||||
|
"addAccountLoading": "添加中...",
|
||||||
|
"addAccountAction": "添加账号"
|
||||||
|
},
|
||||||
|
"apiTester": {
|
||||||
|
"defaultMessage": "你好,请用一句话介绍你自己。",
|
||||||
|
"models": {
|
||||||
|
"chat": "非思考模型",
|
||||||
|
"reasoner": "思考模型",
|
||||||
|
"chatSearch": "非思考模型 (带搜索)",
|
||||||
|
"reasonerSearch": "思考模型 (带搜索)"
|
||||||
|
},
|
||||||
|
"missingApiKey": "请提供 API 密钥",
|
||||||
|
"requestFailed": "请求失败",
|
||||||
|
"networkError": "网络错误: {error}",
|
||||||
|
"testSuccess": "{account}: 测试成功 ({time}ms)",
|
||||||
|
"config": "配置",
|
||||||
|
"modelLabel": "模型",
|
||||||
|
"accountStrategy": "账号策略",
|
||||||
|
"randomRotation": "🎲 随机切换 (支持流式预览)",
|
||||||
|
"apiKeyOptional": "API 密钥 (可选)",
|
||||||
|
"apiKeyDefault": "默认: ...{suffix}",
|
||||||
|
"apiKeyPlaceholder": "输入自定义密钥",
|
||||||
|
"statusError": "错误",
|
||||||
|
"reasoningTrace": "思维链过程",
|
||||||
|
"generating": "正在生成响应...",
|
||||||
|
"enterMessage": "输入消息...",
|
||||||
|
"adminConsoleLabel": "DeepSeek 管理员界面"
|
||||||
|
},
|
||||||
|
"batchImport": {
|
||||||
|
"templates": {
|
||||||
|
"full": {
|
||||||
|
"name": "全量配置模板",
|
||||||
|
"desc": "包含密钥、账号及模型映射"
|
||||||
|
},
|
||||||
|
"emailOnly": {
|
||||||
|
"name": "仅邮箱账号",
|
||||||
|
"desc": "批量导入邮箱格式账号"
|
||||||
|
},
|
||||||
|
"mobileOnly": {
|
||||||
|
"name": "仅手机号账号",
|
||||||
|
"desc": "批量导入手机号格式账号"
|
||||||
|
},
|
||||||
|
"keysOnly": {
|
||||||
|
"name": "仅 API 密钥",
|
||||||
|
"desc": "仅添加 API 访问密钥"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enterJson": "请输入 JSON 配置内容",
|
||||||
|
"importSuccess": "导入成功: {keys} 个密钥, {accounts} 个账号",
|
||||||
|
"templateLoaded": "已加载模板: {name}",
|
||||||
|
"currentConfigLoaded": "当前配置已加载",
|
||||||
|
"fetchConfigFailed": "获取配置失败",
|
||||||
|
"copySuccess": "Base64 配置已复制到剪贴板",
|
||||||
|
"quickTemplates": "快速模板",
|
||||||
|
"dataExport": "数据导出",
|
||||||
|
"dataExportDesc": "获取配置的 Base64 字符串,用于 Vercel 环境变量。",
|
||||||
|
"copyBase64": "复制 Base64 配置",
|
||||||
|
"copied": "已复制",
|
||||||
|
"variableName": "变量名",
|
||||||
|
"jsonEditor": "JSON 编辑器",
|
||||||
|
"loadCurrentConfig": "加载当前配置",
|
||||||
|
"applyConfig": "应用配置",
|
||||||
|
"importing": "正在导入...",
|
||||||
|
"importComplete": "导入操作已完成",
|
||||||
|
"importSummary": "成功导入了 {keys} 个 API 密钥,并更新了 {accounts} 个账号。"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"welcome": "欢迎回来",
|
||||||
|
"subtitle": "请输入管理员密钥以继续",
|
||||||
|
"adminKeyLabel": "管理员密钥",
|
||||||
|
"adminKeyPlaceholder": "输入您的管理员密钥...",
|
||||||
|
"rememberSession": "记住登录状态",
|
||||||
|
"signIn": "登录",
|
||||||
|
"secureConnection": "安全连接",
|
||||||
|
"adminPortal": "DS2API 管理员门户",
|
||||||
|
"signInFailed": "登录失败",
|
||||||
|
"networkError": "网络错误: {error}"
|
||||||
|
},
|
||||||
|
"vercel": {
|
||||||
|
"tokenRequired": "需要 Vercel 访问令牌",
|
||||||
|
"projectRequired": "需要项目 ID",
|
||||||
|
"syncFailed": "同步失败",
|
||||||
|
"networkError": "网络错误",
|
||||||
|
"title": "Vercel 部署",
|
||||||
|
"description": "将当前密钥和账号配置直接同步到 Vercel 环境变量中。",
|
||||||
|
"tokenLabel": "Vercel 访问令牌",
|
||||||
|
"getToken": "获取令牌",
|
||||||
|
"tokenPlaceholderPreconfig": "正在使用预配置的令牌",
|
||||||
|
"tokenPlaceholder": "输入 Vercel 访问令牌",
|
||||||
|
"projectIdLabel": "项目 ID",
|
||||||
|
"projectIdHint": "可在项目设置 (Project Settings) → 常规 (General) 中找到",
|
||||||
|
"teamIdLabel": "团队 ID",
|
||||||
|
"optional": "可选",
|
||||||
|
"syncing": "正在同步...",
|
||||||
|
"syncRedeploy": "同步并重新部署",
|
||||||
|
"redeployHint": "这将触发 Vercel 的重新部署,大约需要 30-60 秒。",
|
||||||
|
"syncSucceeded": "同步成功",
|
||||||
|
"syncFailedLabel": "同步失败",
|
||||||
|
"openDeployment": "访问部署地址",
|
||||||
|
"howItWorks": "工作原理",
|
||||||
|
"steps": {
|
||||||
|
"one": "当前配置 (密钥和账号) 被导出为 JSON 字符串。",
|
||||||
|
"two": "JSON 被编码为 Base64 以确保格式兼容性。",
|
||||||
|
"three": "更新 Vercel 项目中的环境变量:",
|
||||||
|
"four": "触发重新部署以应用新的环境变量。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,17 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
import { I18nProvider } from './i18n'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
|
||||||
const basename = import.meta.env.MODE === 'production' ? '/admin' : '/'
|
const basename = import.meta.env.MODE === 'production' ? '/admin' : '/'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter basename={basename}>
|
<I18nProvider>
|
||||||
<App />
|
<BrowserRouter basename={basename}>
|
||||||
</BrowserRouter>
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</I18nProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user