Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/L4CFHP)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/CJackHwang/ds2api)

语言 / Language: [中文](README.MD) | [English](README.en.md)
语言 / Language: [中文](README.MD) | [English](README.en.md) | [Tiếng Việt](README.vi.md)

将 DeepSeek Web 对话能力转换为 OpenAI、Claude 与 Gemini 兼容 API。核心后端以 **Go** 实现,Vercel 流式桥接额外使用少量 Node Runtime,前端为 React WebUI 管理台(源码在 `webui/`,部署时自动构建到 `static/admin`)。

Expand Down Expand Up @@ -304,6 +304,8 @@ base64 < config.json | tr -d '\n'

**前置要求**:Go 1.26+,Node.js `20.19+` 或 `22.12+`(仅在需要构建 WebUI 时;CI / Docker 构建使用 Node 24);同时确保 `npm` 可用,建议 `npm 10+`

**Windows 一键启动**:双击仓库根目录的 `start.bat`。它会在缺少 Go 时自动下载安装 Go 1.26.3,首次运行时自动将 `config.example.json` 复制为 `config.json` 并用记事本打开,从 `.env` 读取 `PORT`,然后执行 `go run ./cmd/ds2api`,无需任何手动配置。

```bash
# 1. 克隆仓库
git clone https://github.com/CJackHwang/ds2api.git
Expand Down
4 changes: 3 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/L4CFHP)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/CJackHwang/ds2api)

Language: [中文](README.MD) | [English](README.en.md)
Language: [中文](README.MD) | [English](README.en.md) | [Tiếng Việt](README.vi.md)

DS2API converts DeepSeek Web chat capability into OpenAI-compatible, Claude-compatible, and Gemini-compatible APIs. The core backend is Go-based, with a small Node Runtime bridge used for Vercel streaming, and the React WebUI admin panel lives in `webui/` (build output auto-generated to `static/admin` during deployment).

Expand Down Expand Up @@ -292,6 +292,8 @@ For detailed deployment instructions, see the [Deployment Guide](docs/DEPLOY.en.

**Prerequisites**: Go 1.26+, Node.js `20.19+` or `22.12+` (only if building WebUI locally; CI / Docker builds use Node 24), and npm available; npm 10+ is recommended

**Windows one-click launcher**: double-click `start.bat` in the repo root. It auto-installs Go 1.26.3 if missing, copies `config.example.json` to `config.json` on first run (and opens it in Notepad), reads `PORT` from `.env`, then runs `go run ./cmd/ds2api`. No manual setup needed.

```bash
# 1. Clone
git clone https://github.com/CJackHwang/ds2api.git
Expand Down
66 changes: 66 additions & 0 deletions README.vi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<p align="center">
<img src="webui/public/ds2api-favicon.svg" width="128" height="128" alt="DS2API icon" />
</p>

# DS2API

Chuyển đổi khả năng trò chuyện Web của DeepSeek thành các API tương thích với OpenAI, Claude và Gemini. Backend được xây dựng bằng Go, kết hợp với một cầu nối Node Runtime nhỏ cho việc streaming trên Vercel, và bảng điều khiển quản trị React WebUI nằm trong thư mục `webui/`.

Ngôn ngữ: [中文](README.MD) | [English](README.en.md) | [Tiếng Việt](README.vi.md)

## Các tính năng chính

| Tính năng | Chi tiết |
| --- | --- |
| Tương thích OpenAI | Hỗ trợ đầy đủ các endpoint `/v1/chat/completions`, `/v1/responses`, `/v1/embeddings`, `/v1/files`, v.v. |
| Tương thích Claude | Hỗ trợ các endpoint `/anthropic/v1/messages` và các đường dẫn tắt tương ứng. |
| Tương thích Gemini | Hỗ trợ `generateContent` và `streamGenerateContent`. |
| Xoay vòng nhiều tài khoản | Tự động làm mới token, hỗ trợ đăng nhập bằng Email và Số điện thoại. |
| Kiểm soát truy cập | Giới hạn số yêu cầu đồng thời trên mỗi tài khoản và hàng đợi thông minh. |
| Giải mã DeepSeek PoW | Bộ giải mã hiệu suất cao viết bằng Go thuần, phản hồi trong mili giây. |
| Hỗ trợ Tool Calling | Xử lý chống rò rỉ, hỗ trợ gọi công cụ có cấu trúc. |
| Bảng điều khiển Quản trị | Giao diện web hiện đại tại `/admin` (Hỗ trợ đa ngôn ngữ Trung/Anh/Việt, chế độ tối). |

## Khởi động nhanh

### Cách triển khai khuyến nghị:

1. **Tải về bản build sẵn**: Cách dễ nhất cho hầu hết người dùng.
2. **Triển khai Docker**: Phù hợp cho môi trường container.
3. **Triển khai Vercel**: Phù hợp nếu bạn muốn dùng serverless.
4. **Chạy từ mã nguồn**: Dành cho nhà phát triển muốn tùy chỉnh.

### Bước chuẩn bị chung:

Sử dụng `config.json` làm nguồn cấu hình chính:

```bash
cp config.example.json config.json
# Chỉnh sửa config.json với thông tin tài khoản DeepSeek của bạn
```

### Chạy cục bộ:

**Yêu cầu**: Go 1.26+, Node.js 20.19+ (nếu build WebUI cục bộ).

**Windows — khởi động 1 click**: double-click `start.bat` ở thư mục gốc repo. Script tự động cài Go 1.26.3 nếu chưa có, tự sao chép `config.example.json` thành `config.json` lần đầu chạy (và mở bằng Notepad để điền thông tin), đọc `PORT` từ `.env`, rồi chạy `go run ./cmd/ds2api` — không cần cấu hình thủ công.

```bash
git clone https://github.com/CJackHwang/ds2api.git
cd ds2api
cp config.example.json config.json
go run ./cmd/ds2api
```

URL mặc định: `http://127.0.0.1:5001`

## Tài liệu

| Tài liệu | Mô tả |
| --- | --- |
| [API.en.md](API.en.md) | Tài liệu tham khảo API với các ví dụ |
| [DEPLOY.en.md](docs/DEPLOY.en.md) | Hướng dẫn triển khai chi tiết |

## Miễn trừ trách nhiệm

Dự án này được xây dựng thông qua kỹ thuật đảo ngược (reverse engineering) và chỉ được cung cấp cho mục đích học tập, nghiên cứu và thử nghiệm cá nhân. Không có ủy quyền thương mại nào được cấp, và không có đảm bảo về tính ổn định hay kết quả sử dụng. Tác giả không chịu trách nhiệm cho bất kỳ tổn thất hoặc rủi ro pháp lý nào phát sinh từ việc sử dụng dự án này.
20 changes: 20 additions & 0 deletions internal/account/pool_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,32 @@ func (p *Pool) Status() map[string]any {
}
}
sort.Strings(inUseAccounts)

// Count failed accounts via test status if store is available
failedCount := 0
failedAccounts := make([]string, 0)
if p.store != nil {
for _, acc := range p.store.Accounts() {
id := acc.Identifier()
if id == "" {
continue
}
if status, ok := p.store.AccountTestStatus(id); ok && status == "failed" {
failedCount++
failedAccounts = append(failedAccounts, id)
}
}
sort.Strings(failedAccounts)
}

return map[string]any{
"available": len(available),
"in_use": inUseSlots,
"total": len(p.store.Accounts()),
"failed": failedCount,
"available_accounts": available,
"in_use_accounts": inUseAccounts,
"failed_accounts": failedAccounts,
"max_inflight_per_account": p.maxInflightPerAccount,
"global_max_inflight": p.globalMaxInflight,
"recommended_concurrency": p.recommendedConcurrency,
Expand Down
38 changes: 31 additions & 7 deletions internal/config/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (
)

type Store struct {
mu sync.RWMutex
cfg Config
path string
fromEnv bool
keyMap map[string]struct{} // O(1) API key lookup index
accMap map[string]int // O(1) account lookup: identifier -> slice index
accTest map[string]string // runtime-only account test status cache
mu sync.RWMutex
cfg Config
path string
fromEnv bool
keyMap map[string]struct{} // O(1) API key lookup index
accMap map[string]int // O(1) account lookup: identifier -> slice index
accTest map[string]string // runtime-only account test status cache
accTestMsg map[string]string // runtime-only account test error message cache
}

func LoadStore() *Store {
Expand Down Expand Up @@ -173,6 +174,10 @@ func (s *Store) FindAccount(identifier string) (Account, bool) {
}

func (s *Store) UpdateAccountTestStatus(identifier, status string) error {
return s.UpdateAccountTestStatusWithMessage(identifier, status, "")
}

func (s *Store) UpdateAccountTestStatusWithMessage(identifier, status, message string) error {
identifier = strings.TrimSpace(identifier)
s.mu.Lock()
defer s.mu.Unlock()
Expand All @@ -181,6 +186,15 @@ func (s *Store) UpdateAccountTestStatus(identifier, status string) error {
return errors.New("account not found")
}
s.setAccountTestStatusLocked(s.cfg.Accounts[idx], status, identifier)
// store error message for failed accounts
if s.accTestMsg == nil {
s.accTestMsg = make(map[string]string)
}
if strings.TrimSpace(message) != "" {
s.accTestMsg[identifier] = strings.TrimSpace(message)
} else {
delete(s.accTestMsg, identifier)
}
return nil
}

Expand All @@ -195,6 +209,16 @@ func (s *Store) AccountTestStatus(identifier string) (string, bool) {
return status, ok
}

func (s *Store) AccountTestMessage(identifier string) string {
identifier = strings.TrimSpace(identifier)
if identifier == "" {
return ""
}
s.mu.RLock()
defer s.mu.RUnlock()
return s.accTestMsg[identifier]
}

func (s *Store) UpdateAccountToken(identifier, token string) error {
identifier = strings.TrimSpace(identifier)
s.mu.Lock()
Expand Down
8 changes: 6 additions & 2 deletions internal/httpapi/admin/accounts/handler_accounts_crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
for _, acc := range accounts[start:end] {
testStatus, _ := h.Store.AccountTestStatus(acc.Identifier())
token := strings.TrimSpace(acc.Token)
items = append(items, map[string]any{
item := map[string]any{
"identifier": acc.Identifier(),
"name": acc.Name,
"remark": acc.Remark,
Expand All @@ -69,7 +69,11 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
"has_token": token != "",
"token_preview": maskSecretPreview(token),
"test_status": testStatus,
})
}
if testStatus == "failed" {
item["error_message"] = h.Store.AccountTestMessage(acc.Identifier())
}
items = append(items, item)
}
writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})
}
Expand Down
5 changes: 4 additions & 1 deletion internal/httpapi/admin/accounts/handler_accounts_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,13 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
}
defer func() {
status := "failed"
msg := ""
if ok, _ := result["success"].(bool); ok {
status = "ok"
} else {
msg, _ = result["message"].(string)
}
_ = h.Store.UpdateAccountTestStatus(identifier, status)
_ = h.Store.UpdateAccountTestStatusWithMessage(identifier, status, msg)
}()
token, err := h.DS.Login(ctx, acc)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions internal/httpapi/admin/shared/deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ type ConfigStore interface {
FindAccount(identifier string) (config.Account, bool)
UpdateAccountToken(identifier, token string) error
UpdateAccountTestStatus(identifier, status string) error
UpdateAccountTestStatusWithMessage(identifier, status, message string) error
AccountTestStatus(identifier string) (string, bool)
AccountTestMessage(identifier string) string
Update(mutator func(*config.Config) error) error
ExportJSONAndBase64() (string, string, error)
IsEnvBacked() bool
Expand Down
80 changes: 80 additions & 0 deletions start.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@echo off
setlocal enabledelayedexpansion
chcp 65001 >nul
title DS2API - Dev Server

echo ============================================
echo DS2API - Dev Server (go run)
echo ============================================
echo.

set "ROOT=%~dp0"

:: ── Step 1: Check / auto-install Go ──────────────────────────────────────
where go >nul 2>&1
if errorlevel 1 (
echo [SETUP] Go not found. Downloading and installing Go 1.26.3...
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$msi = \"$env:TEMP\go-installer.msi\";" ^
"$url = 'https://go.dev/dl/go1.26.3.windows-amd64.msi';" ^
"Write-Host '[SETUP] Downloading...';" ^
"try { Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing }" ^
"catch { Write-Host '[ERROR]' $_.Exception.Message; exit 1 };" ^
"Write-Host '[SETUP] Installing (Windows will prompt for Admin)...';" ^
"Start-Process msiexec.exe -ArgumentList \"/i `\"$msi`\" /quiet /norestart\" -Verb RunAs -Wait;" ^
"Remove-Item $msi -Force -ErrorAction SilentlyContinue;" ^
"Write-Host '[OK] Go installed.'"
if errorlevel 1 (
echo [ERROR] Installation failed. Install manually from: https://go.dev/dl/
pause & exit /b 1
)
:: Reload PATH in current session
for /f "tokens=*" %%p in ('powershell -NoProfile -Command ^
"[System.Environment]::GetEnvironmentVariable('PATH','Machine')"') do set "PATH=%%p;%PATH%"
where go >nul 2>&1
if errorlevel 1 (
echo [INFO] Go installed. Please close and reopen this window to reload PATH.
pause & exit /b 0
)
)
for /f "tokens=3" %%v in ('go version') do set GO_VER=%%v
echo [OK] Go %GO_VER%

:: ── Step 2: Check config.json ─────────────────────────────────────────────
if not exist "%ROOT%config.json" (
echo [SETUP] config.json not found. Copying from config.example.json...
copy "%ROOT%config.example.json" "%ROOT%config.json" >nul
echo [SETUP] config.json created. Fill in your DeepSeek account and API key.
start "" notepad "%ROOT%config.json"
pause & exit /b 0
)
echo [OK] config.json

:: ── Step 3: Read PORT from .env ───────────────────────────────────────────
set "PORT=5001"
if exist "%ROOT%.env" (
for /f "usebackq tokens=1,2 delims==" %%a in ("%ROOT%.env") do (
if "%%a"=="PORT" set "PORT=%%b"
)
)

:: ── Step 4: Start server ──────────────────────────────────────────────────
echo.
echo Admin : http://127.0.0.1:%PORT%/admin
echo API : http://127.0.0.1:%PORT%/v1
echo Health: http://127.0.0.1:%PORT%/healthz
echo.
echo [INFO] Starting server... (Ctrl+C to stop)
echo ============================================
echo.

cd /d "%ROOT%"
set "DS2API_CONFIG_PATH=%ROOT%config.json"
set "LOG_LEVEL=INFO"
set "PORT=%PORT%"

go run ./cmd/ds2api

echo.
echo [INFO] Server stopped.
pause
13 changes: 10 additions & 3 deletions webui/src/components/LanguageToggle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ 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')
const languages = ['zh', 'en', 'vi']
const currentIndex = languages.indexOf(lang)
const nextLang = languages[(currentIndex + 1) % languages.length]

const labelMap = {
zh: t('language.chinese'),
en: t('language.english'),
vi: t('language.vietnamese'),
}

return (
<button
Expand All @@ -12,7 +19,7 @@ export default function LanguageToggle({ className = '' }) {
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}
{labelMap[nextLang]}
</button>
)
}
5 changes: 5 additions & 0 deletions webui/src/features/account/AccountsTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ export default function AccountsTable({
)}
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : isActive ? t('accountManager.sessionActive') : runtimeUnknown ? t('accountManager.runtimeStatusUnknown') : t('accountManager.reauthRequired')}</span>
{acc.test_status === 'failed' && acc.error_message && (
<span className="font-mono bg-destructive/10 text-destructive px-1.5 py-0.5 rounded text-[10px] max-w-[260px] truncate" title={acc.error_message}>
{acc.error_message}
</span>
)}
{acc.token_preview && (
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
{acc.token_preview}
Expand Down
Loading
Loading