diff --git a/README.MD b/README.MD index 920201f08..2f8ebe556 100644 --- a/README.MD +++ b/README.MD @@ -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`)。 @@ -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 diff --git a/README.en.md b/README.en.md index afb4c7dd1..6d5fee497 100644 --- a/README.en.md +++ b/README.en.md @@ -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). @@ -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 diff --git a/README.vi.md b/README.vi.md new file mode 100644 index 000000000..d9d5d3a91 --- /dev/null +++ b/README.vi.md @@ -0,0 +1,66 @@ +

+ DS2API icon +

+ +# 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. diff --git a/internal/account/pool_core.go b/internal/account/pool_core.go index 90e2594d0..bcae69cb1 100644 --- a/internal/account/pool_core.go +++ b/internal/account/pool_core.go @@ -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, diff --git a/internal/config/store.go b/internal/config/store.go index 603ff9a4f..351d30f8e 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -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 { @@ -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() @@ -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 } @@ -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() diff --git a/internal/httpapi/admin/accounts/handler_accounts_crud.go b/internal/httpapi/admin/accounts/handler_accounts_crud.go index 7375b403c..1aca7194c 100644 --- a/internal/httpapi/admin/accounts/handler_accounts_crud.go +++ b/internal/httpapi/admin/accounts/handler_accounts_crud.go @@ -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, @@ -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}) } diff --git a/internal/httpapi/admin/accounts/handler_accounts_testing.go b/internal/httpapi/admin/accounts/handler_accounts_testing.go index d92c1dcf4..64959971c 100644 --- a/internal/httpapi/admin/accounts/handler_accounts_testing.go +++ b/internal/httpapi/admin/accounts/handler_accounts_testing.go @@ -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 { diff --git a/internal/httpapi/admin/shared/deps.go b/internal/httpapi/admin/shared/deps.go index e063ae1c7..2aaff3c5a 100644 --- a/internal/httpapi/admin/shared/deps.go +++ b/internal/httpapi/admin/shared/deps.go @@ -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 diff --git a/start.bat b/start.bat new file mode 100644 index 000000000..87232adb6 --- /dev/null +++ b/start.bat @@ -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 diff --git a/webui/src/components/LanguageToggle.jsx b/webui/src/components/LanguageToggle.jsx index ce90fa9f1..51d8cb964 100644 --- a/webui/src/components/LanguageToggle.jsx +++ b/webui/src/components/LanguageToggle.jsx @@ -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 ( ) } diff --git a/webui/src/features/account/AccountsTable.jsx b/webui/src/features/account/AccountsTable.jsx index 14915edcb..160b91242 100644 --- a/webui/src/features/account/AccountsTable.jsx +++ b/webui/src/features/account/AccountsTable.jsx @@ -135,6 +135,11 @@ export default function AccountsTable({ )}
{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : isActive ? t('accountManager.sessionActive') : runtimeUnknown ? t('accountManager.runtimeStatusUnknown') : t('accountManager.reauthRequired')} + {acc.test_status === 'failed' && acc.error_message && ( + + {acc.error_message} + + )} {acc.token_preview && ( {acc.token_preview} diff --git a/webui/src/features/account/QueueCards.jsx b/webui/src/features/account/QueueCards.jsx index 53d483b82..9ac452d54 100644 --- a/webui/src/features/account/QueueCards.jsx +++ b/webui/src/features/account/QueueCards.jsx @@ -1,4 +1,4 @@ -import { CheckCircle2, Server, ShieldCheck } from 'lucide-react' +import { AlertCircle, CheckCircle2, Server, ShieldCheck } from 'lucide-react' export default function QueueCards({ queueStatus, t }) { if (!queueStatus) { @@ -6,7 +6,7 @@ export default function QueueCards({ queueStatus, t }) { } return ( -
+
@@ -37,6 +37,29 @@ export default function QueueCards({ queueStatus, t }) { {t('accountManager.accountsUnit')}
+
0 + ? 'bg-destructive/5 border-destructive/30' + : 'bg-card border-border' + }`}> +
+ +
+

0 ? 'text-destructive' : 'text-muted-foreground' + }`}>{t('accountManager.failedPool')}

+
+ 0 ? 'text-destructive' : 'text-foreground' + }`}>{queueStatus.failed ?? 0} + {t('accountManager.accountsUnit')} +
+ {queueStatus.failed > 0 && ( +

+ {t('accountManager.failedPoolHint')} +

+ )} +
) } diff --git a/webui/src/i18n.jsx b/webui/src/i18n.jsx index 5515b7f59..2f51e24ad 100644 --- a/webui/src/i18n.jsx +++ b/webui/src/i18n.jsx @@ -1,9 +1,10 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react' import en from './locales/en.json' import zh from './locales/zh.json' +import vi from './locales/vi.json' const STORAGE_KEY = 'ds2api_lang' -const translations = { en, zh } +const translations = { en, zh, vi } const I18nContext = createContext({ lang: 'zh', @@ -13,7 +14,11 @@ const I18nContext = createContext({ const getBrowserLang = () => { if (typeof navigator === 'undefined') return 'zh' - return navigator.language?.toLowerCase().startsWith('zh') ? 'zh' : 'en' + const browserLang = navigator.language?.toLowerCase() + if (!browserLang) return 'zh' + if (browserLang.startsWith('zh')) return 'zh' + if (browserLang.startsWith('vi')) return 'vi' + return 'en' } const getValue = (obj, key) => { diff --git a/webui/src/locales/en.json b/webui/src/locales/en.json index c0db122b1..480c82b2b 100644 --- a/webui/src/locales/en.json +++ b/webui/src/locales/en.json @@ -2,7 +2,8 @@ "language": { "label": "Language", "english": "English", - "chinese": "中文" + "chinese": "中文", + "vietnamese": "Vietnamese" }, "nav": { "accounts": { @@ -109,6 +110,8 @@ "available": "Available", "inUse": "In use", "totalPool": "Total pool", + "failedPool": "Unavailable", + "failedPoolHint": "Refresh token to re-verify", "accountsUnit": "accounts", "threadsUnit": "threads", "apiKeysTitle": "API Keys", diff --git a/webui/src/locales/vi.json b/webui/src/locales/vi.json new file mode 100644 index 000000000..8cf0c11d9 --- /dev/null +++ b/webui/src/locales/vi.json @@ -0,0 +1,491 @@ +{ + "language": { + "label": "Ngôn ngữ", + "english": "Tiếng Anh", + "chinese": "Tiếng Trung", + "vietnamese": "Tiếng Việt" + }, + "nav": { + "accounts": { + "label": "Quản lý tài khoản", + "desc": "Quản lý kho tài khoản DeepSeek" + }, + "proxies": { + "label": "Proxy IPs", + "desc": "Quản lý các nút proxy cho tài khoản" + }, + "test": { + "label": "Kiểm tra API", + "desc": "Kiểm tra kết nối và phản hồi API" + }, + "history": { + "label": "Phản hồi", + "desc": "Xem lịch sử phản hồi từ server" + }, + "import": { + "label": "Nhập hàng loạt", + "desc": "Nhập cấu hình tài khoản số lượng lớn" + }, + "vercel": { + "label": "Đồng bộ Vercel", + "desc": "Đồng bộ cấu hình sang Vercel" + }, + "settings": { + "label": "Cài đặt", + "desc": "Chỉnh sửa cài đặt hệ thống và bảo mật" + } + }, + "sidebar": { + "onlineAdminConsole": "Bảng điều khiển trực tuyến", + "systemStatus": "Trạng thái hệ thống", + "statusOnline": "Trực tuyến", + "accounts": "Tài khoản", + "keys": "Khóa API", + "signOut": "Đăng xuất", + "version": "Phiên bản", + "updateAvailable": "Có bản cập nhật mới: {latest}" + }, + "auth": { + "expired": "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.", + "checking": "Đang kiểm tra trạng thái xác thực..." + }, + "errors": { + "fetchConfig": "Lỗi khi lấy cấu hình: {error}" + }, + "actions": { + "cancel": "Hủy", + "add": "Thêm", + "delete": "Xóa", + "copy": "Sao chép", + "generate": "Tạo mới", + "test": "Làm mới token", + "testing": "Đang làm mới...", + "loading": "Đang tải..." + }, + "messages": { + "deleted": "Xóa thành công", + "deleteFailed": "Xóa thất bại", + "failedToAdd": "Thêm thất bại", + "networkError": "Lỗi mạng.", + "requestFailed": "Yêu cầu thất bại.", + "generationStopped": "Đã dừng tạo.", + "invalidJson": "Định dạng JSON không hợp lệ.", + "importFailed": "Nhập thất bại.", + "copyFailed": "Sao chép thất bại." + }, + "landing": { + "adminConsole": "Bảng điều khiển Admin", + "apiStatus": "Trạng thái API", + "features": { + "compatibility": { + "title": "Tương thích hoàn toàn", + "desc": "Hỗ trợ định dạng OpenAI & Claude" + }, + "loadBalancing": { + "title": "Cân bằng tải", + "desc": "Tự động xoay vòng tài khoản ổn định" + }, + "reasoning": { + "title": "Suy luận sâu", + "desc": "Hiển thị quá trình suy luận khi được bật" + }, + "search": { + "title": "Tìm kiếm Web", + "desc": "Tích hợp tìm kiếm web bản địa" + } + } + }, + "accountManager": { + "addKeySuccess": "Đã thêm khóa API thành công.", + "updateKeySuccess": "Đã cập nhật khóa API thành công.", + "addAccountSuccess": "Đã thêm tài khoản thành công.", + "updateAccountSuccess": "Đã cập nhật thông tin tài khoản thành công.", + "requiredFields": "Yêu cầu mật khẩu và email/số điện thoại.", + "deleteKeyConfirm": "Bạn có chắc chắn muốn xóa khóa API này?", + "deleteAccountConfirm": "Bạn có chắc chắn muốn xóa tài khoản này?", + "invalidIdentifier": "Mã định danh tài khoản không hợp lệ. Đã hủy thao tác.", + "testAllConfirm": "Làm mới tất cả token tài khoản và xác minh đăng nhập?", + "testAllCompleted": "Hoàn tất: {success}/{total} đã được làm mới", + "testFailed": "Kiểm tra thất bại: {error}", + "available": "Sẵn sàng", + "inUse": "Đang sử dụng", + "totalPool": "Tổng cộng", + "failedPool": "Không dùng được", + "failedPoolHint": "Làm mới token để kiểm tra lại", + "accountsUnit": "tài khoản", + "threadsUnit": "luồng", + "apiKeysTitle": "Khóa API", + "apiKeysDesc": "Quản lý kho khóa truy cập API. Nhấp vào biểu tượng bút chì để chỉnh sửa tên và ghi chú.", + "addKey": "Thêm khóa", + "editKeyTitle": "Sửa khóa", + "editAccountTitle": "Sửa tài khoản", + "copied": "Đã sao chép", + "copyFailed": "Sao chép thất bại", + "copyKeyTitle": "Sao chép khóa", + "deleteKeyTitle": "Xóa khóa", + "noApiKeys": "Không tìm thấy khóa API nào.", + "accountsTitle": "Tài khoản DeepSeek", + "accountsDesc": "Quản lý kho tài khoản DeepSeek và chỉnh sửa tên/ghi chú.", + "testAll": "Làm mới tất cả token", + "addAccount": "Thêm tài khoản", + "testingAllAccounts": "Đang làm mới token cho tất cả tài khoản...", + "sessionActive": "Phiên hoạt động", + "reauthRequired": "Yêu cầu kiểm tra lại", + "runtimeStatusUnknown": "Sẽ được xác định sau khi đồng bộ", + "testStatusFailed": "Lần kiểm tra cuối thất bại", + "noAccounts": "Không tìm thấy tài khoản nào.", + "modalAddKeyTitle": "Thêm khóa API", + "modalEditKeyTitle": "Sửa khóa API", + "modalEditAccountTitle": "Sửa chi tiết tài khoản", + "newKeyLabel": "Giá trị khóa mới", + "newKeyPlaceholder": "Nhập khóa API tùy chỉnh", + "keyLabel": "Giá trị khóa", + "keyReadonlyPlaceholder": "Không thể thay đổi giá trị khóa", + "keyReadonlyHint": "Giá trị khóa là chỉ đọc. Vui lòng cập nhật tên và ghi chú.", + "generate": "Tạo mới", + "generateHint": "Nhấp Tạo mới để tạo khóa ngẫu nhiên.", + "addKeyLoading": "Đang thêm...", + "addKeyAction": "Thêm khóa", + "editKeyLoading": "Đang lưu...", + "editKeyAction": "Lưu thay đổi", + "editAccountHint": "Chỉ có thể thay đổi tên và ghi chú. Mã định danh tài khoản sẽ giữ nguyên.", + "accountIdentifierLabel": "Mã định danh tài khoản", + "editAccountLoading": "Đang lưu...", + "editAccountAction": "Lưu thay đổi", + "modalAddAccountTitle": "Thêm tài khoản DeepSeek", + "nameOptional": "Tên (tùy chọn)", + "namePlaceholder": "v.d. Tài khoản chính A", + "remarkOptional": "Ghi chú (tùy chọn)", + "remarkPlaceholder": "v.d. Dùng chung nhóm / chỉ thử nghiệm", + "emailOptional": "Email (tùy chọn)", + "mobileOptional": "Số điện thoại (tùy chọn)", + "passwordLabel": "Mật khẩu", + "passwordPlaceholder": "Mật khẩu tài khoản", + "addAccountLoading": "Đang thêm...", + "addAccountAction": "Thêm tài khoản", + "pageInfo": "Trang {current}/{total}, tổng cộng {count} tài khoản", + "searchPlaceholder": "Tìm kiếm tài khoản...", + "searchNoResults": "Không có tài khoản nào khớp với tìm kiếm", + "sessionCount": "Phiên: {count}", + "deleteAllSessions": "Xóa tất cả phiên", + "deleteAllSessionsConfirm": "Bạn có chắc chắn muốn xóa tất cả phiên của tài khoản này? Hành động này không thể hoàn tác.", + "deleteAllSessionsSuccess": "Đã xóa tất cả phiên thành công", + "accountProxyLabel": "Proxy tài khoản", + "proxyNone": "Kết nối trực tiếp", + "proxyBadge": "Proxy: {name}", + "proxyUpdateSuccess": "Đã cập nhật proxy cho tài khoản.", + "envModeRiskTitle": "Phát hiện chế độ biến môi trường (rủi ro lưu trữ)", + "envModeRiskDesc": "Phát hiện DS2API_CONFIG_JSON. Nếu không bật DS2API_ENV_WRITEBACK, các thay đổi trên UI chỉ có hiệu lực trong bộ nhớ và sẽ mất sau khi khởi động lại.", + "envModeWritebackPendingTitle": "Chế độ Env + tự động lưu trữ đã bật (đang chờ bàn giao file)", + "envModeWritebackActiveTitle": "Chế độ Env + tự động lưu trữ đang hoạt động", + "envModeWritebackDesc": "Ứng dụng sẽ tự động tạo/ghi file cấu hình và chuyển sang chế độ lưu trữ file. Đường dẫn lưu trữ hiện tại: {path}" + }, + "proxyManager": { + "title": "Proxy IPs", + "desc": "Quản lý các nút SOCKS cho tài khoản và kiểm tra kết nối đến DeepSeek.", + "addProxy": "Thêm proxy", + "editProxy": "Sửa proxy", + "deleteProxy": "Xóa proxy", + "modalAddTitle": "Thêm nút proxy", + "modalEditTitle": "Sửa nút proxy", + "modalDesc": "Hỗ trợ socks5 và socks5h. Tài khoản sẽ sử dụng nút này làm đường truyền ra.", + "nameLabel": "Tên proxy", + "namePlaceholder": "Ví dụ: Hong Kong Exit A", + "typeLabel": "Loại proxy", + "hostLabel": "Máy chủ proxy", + "hostPlaceholder": "127.0.0.1 hoặc tên miền", + "portLabel": "Cổng", + "usernameLabel": "Tên người dùng (tùy chọn)", + "usernamePlaceholder": "Tên người dùng proxy", + "passwordLabel": "Mật khẩu (tùy chọn)", + "passwordPlaceholder": "Mật khẩu proxy", + "passwordKeepHint": "Để trống để giữ mật khẩu hiện tại.", + "typeHelp": "socks5 phân giải tên miền tại địa phương; socks5h chuyển tiếp tên miền cho proxy để phân giải DNS từ xa.", + "requiredFields": "Yêu cầu máy chủ và cổng.", + "saving": "Đang lưu...", + "testing": "Đang kiểm tra", + "testAction": "Kiểm tra proxy", + "untested": "Chưa kiểm tra", + "saveAdd": "Thêm proxy", + "saveEdit": "Lưu thay đổi", + "addSuccess": "Đã thêm proxy thành công.", + "updateSuccess": "Đã cập nhật proxy thành công.", + "deleteConfirm": "Xóa proxy {name}? Các tài khoản dùng nó sẽ quay về kết nối trực tiếp.", + "noProxies": "Chưa có nút proxy nào.", + "authEnabled": "Đã bật xác thực", + "testSuccessShort": "Kết nối được {time}ms", + "testFailedShort": "Kiểm tra thất bại", + "totalProxies": "Tổng số proxy", + "socks5hCount": "Số nút socks5h", + "authProxyCount": "Số nút có xác thực" + }, + "apiTester": { + "defaultMessage": "Xin chào, hãy giới thiệu bản thân trong một câu.", + "models": { + "flash": "v4 Flash (mặc định bật suy luận)", + "pro": "v4 Pro (mặc định bật suy luận)", + "flashSearch": "v4 Flash (có tìm kiếm)", + "proSearch": "v4 Pro (có tìm kiếm)", + "vision": "v4 Vision (mặc định bật suy luận)", + "generic": "Mô hình tương thích", + "noThinking": "bắt buộc tắt suy luận" + }, + "missingApiKey": "Vui lòng cung cấp khóa API.", + "requestFailed": "Yêu cầu thất bại.", + "networkError": "Lỗi mạng: {error}", + "requestSuccess": "{account}: Yêu cầu thành công ({time}ms)", + "testSuccess": "{account}: Làm mới token thành công ({time}ms)", + "config": "Cấu hình", + "modelLabel": "Mô hình", + "modelPickerHint": "Chọn mô hình từ danh sách.", + "loadingModels": "Đang tải danh sách mô hình...", + "loadingModelsHint": "Đang lấy danh sách mô hình từ /v1/models.", + "noModels": "Không có mô hình nào khả dụng", + "noModelsHint": "Endpoint /v1/models không trả về mô hình nào. Kiểm tra cấu hình backend hoặc trạng thái API.", + "noModelsMessagePlaceholder": "Hiện không có mô hình nào, không thể gửi yêu cầu.", + "streamMode": "Streaming", + "accountSelector": "Tài khoản", + "autoRandom": "🤖 Tự động / Ngẫu nhiên", + "apiKeyOptional": "Khóa API (tùy chọn)", + "apiKeyDefault": "Mặc định: {preview}", + "apiKeyPlaceholder": "Nhập khóa tùy chỉnh", + "modeManaged": "Chế độ khóa quản lý (dùng kho tài khoản).", + "modeDirect": "Chế độ token trực tiếp (yêu cầu token DeepSeek hợp lệ).", + "attachmentAccountHint": "Tệp đính kèm được gắn với tài khoản {account}. Gửi sẽ dùng cùng tài khoản này.", + "fileAccountConflict": "Các tệp đính kèm đến từ các tài khoản khác nhau. Hãy xóa và tải lên lại dưới một tài khoản.", + "fileAccountMismatch": "Tài khoản đã chọn không khớp với tài khoản của tệp đính kèm. Hãy chuyển sang tài khoản tương ứng hoặc xóa tệp đính kèm.", + "statusError": "Lỗi", + "reasoningTrace": "Quá trình suy luận", + "generating": "Đang tạo phản hồi...", + "enterMessage": "Nhập tin nhắn...", + "adminConsoleLabel": "Bảng điều khiển DeepSeek" + }, + "chatHistory": { + "loading": "Đang tải lịch sử trò chuyện...", + "loadFailed": "Lỗi khi tải lịch sử trò chuyện.", + "retentionTitle": "Lưu trữ", + "retentionDesc": "Máy chủ chỉ giữ lại N bản ghi phản hồi DeepSeek mới nhất trên các giao diện OpenAI Chat, OpenAI Responses, Claude và Gemini.", + "off": "TẮT", + "refresh": "Làm mới", + "clearAll": "Xóa tất cả", + "clearSuccess": "Đã xóa lịch sử trò chuyện.", + "clearFailed": "Lỗi khi xóa lịch sử trò chuyện.", + "deleteSuccess": "Đã xóa cuộc trò chuyện.", + "deleteFailed": "Lỗi khi xóa cuộc trò chuyện.", + "updateLimitFailed": "Lỗi khi cập nhật giới hạn lưu trữ.", + "disabledSuccess": "Đã tắt lưu lịch sử trò chuyện.", + "limitUpdated": "Đã cập nhật giới hạn lưu trữ thành {limit}", + "listTitle": "Lịch sử", + "detailTitle": "Chi tiết", + "viewModeList": "Chế độ danh sách", + "viewModeMerged": "Chế độ hợp nhất", + "emptyTitle": "Chưa có lịch sử trò chuyện", + "emptyDesc": "Khi một giao diện được hỗ trợ gửi yêu cầu đến DeepSeek và nhận phản hồi, kết quả sẽ tự động được lưu tại đây.", + "untitled": "Cuộc trò chuyện không tên", + "noPreview": "Không có bản xem trước.", + "selectPrompt": "Chọn một bản ghi bên trái để xem chi tiết.", + "mergedInput": "Tin nhắn cuối cùng gửi đến DeepSeek", + "emptyMergedPrompt": "Không có lời nhắc hợp nhất.", + "copyHistory": "Sao chép LỊCH SỬ", + "downloadHistory": "Tải về LỊCH SỬ", + "copyMerged": "Sao chép lời nhắc hợp nhất", + "downloadMerged": "Tải về lời nhắc hợp nhất", + "copySuccess": "Sao chép thành công.", + "copyFailed": "Sao chép thất bại.", + "downloadSuccess": "Tải về thành công.", + "downloadFailed": "Tải về thất bại.", + "expand": "Mở rộng", + "collapse": "Thu gọn", + "reasoningTrace": "Quá trình suy luận", + "failedOutput": "Yêu cầu thất bại và không có phản hồi từ trợ lý.", + "emptyAssistantOutput": "Không có phản hồi từ trợ lý.", + "emptyUserInput": "Không có đầu vào từ người dùng.", + "confirmClearTitle": "Xóa tất cả bản ghi?", + "confirmClearDesc": "Hành động này sẽ xóa mọi bản ghi cuộc trò chuyện trên server và không thể hoàn tác.", + "confirmClearAction": "Xóa tất cả", + "metaTitle": "Siêu dữ liệu", + "metaAccount": "Tài khoản", + "metaElapsed": "Thời gian", + "metaSurface": "Giao diện", + "metaModel": "Mô hình", + "metaStatusCode": "Mã trạng thái", + "metaStream": "Chế độ đầu ra", + "metaCaller": "Mã định danh người gọi", + "metaTime": "Hoàn thành lúc", + "metaUnknown": "Không xác định", + "backToTop": "Về đầu trang", + "backToBottom": "Xuống cuối trang", + "streamMode": "Streaming", + "nonStreamMode": "Không streaming", + "status": { + "streaming": "Đang stream", + "success": "Thành công", + "error": "Lỗi", + "stopped": "Đã dừng" + }, + "role": { + "user": "Người dùng", + "assistant": "Trợ lý", + "tool": "Công cụ", + "system": "Hệ thống" + } + }, + "batchImport": { + "templates": { + "full": { + "name": "Mẫu cấu hình đầy đủ", + "desc": "Tải từ config.example.json với khóa, tài khoản và mặc định" + }, + "emailOnly": { + "name": "Tài khoản dùng Email", + "desc": "Nhập hàng loạt tài khoản đăng nhập bằng Email" + }, + "mobileOnly": { + "name": "Tài khoản dùng Số điện thoại", + "desc": "Nhập hàng loạt tài khoản đăng nhập bằng Số điện thoại" + }, + "keysOnly": { + "name": "Chỉ khóa API", + "desc": "Chỉ thêm các khóa truy cập API" + } + }, + "enterJson": "Vui lòng cung cấp nội dung cấu hình JSON.", + "importSuccess": "Nhập thành công: {keys} khóa, {accounts} tài khoản", + "templateLoaded": "Đã tải mẫu: {name}", + "currentConfigLoaded": "Đã tải cấu hình hiện tại.", + "fetchConfigFailed": "Lỗi khi lấy cấu hình.", + "copySuccess": "Đã sao chép cấu hình Base64 vào bộ nhớ tạm.", + "quickTemplates": "Mẫu nhanh", + "dataExport": "Xuất dữ liệu", + "dataExportDesc": "Sao chép cấu hình mã hóa Base64 cho biến môi trường Vercel.", + "copyBase64": "Sao chép cấu hình Base64", + "copied": "Đã sao chép", + "variableName": "Tên biến", + "jsonEditor": "Trình sửa JSON", + "loadCurrentConfig": "Tải cấu hình hiện tại", + "applyConfig": "Áp dụng cấu hình", + "importing": "Đang nhập...", + "importComplete": "Hoàn tất nhập", + "importSummary": "Đã nhập {keys} khóa API và cập nhật {accounts} tài khoản." + }, + "settings": { + "loadFailed": "Lỗi khi tải cài đặt.", + "nonJsonResponse": "Phản hồi không phải JSON từ server (trạng thái: {status}).", + "save": "Lưu cài đặt", + "saving": "Đang lưu...", + "saveSuccess": "Đã lưu cài đặt và tải lại nóng.", + "saveFailed": "Lỗi khi lưu cài đặt.", + "securityTitle": "Bảo mật", + "jwtExpireHours": "Thời hạn JWT (giờ)", + "newPassword": "Mật khẩu admin mới", + "newPasswordPlaceholder": "Nhập mật khẩu mới (tối thiểu 4 ký tự)", + "updatePassword": "Cập nhật mật khẩu", + "updating": "Đang cập nhật...", + "passwordTooShort": "Mật khẩu phải có ít nhất 4 ký tự.", + "passwordUpdated": "Đã cập nhật mật khẩu. Vui lòng đăng nhập lại.", + "passwordUpdateFailed": "Lỗi khi cập nhật mật khẩu.", + "runtimeTitle": "Runtime", + "accountMaxInflight": "Số yêu cầu tối đa mỗi tài khoản", + "accountMaxQueue": "Kích thước hàng đợi mỗi tài khoản", + "globalMaxInflight": "Số yêu cầu tối đa toàn cục", + "tokenRefreshIntervalHours": "Khoảng thời gian làm mới token (giờ)", + "behaviorTitle": "Hành vi", + "responsesTTL": "Thời gian lưu trữ phản hồi (giây)", + "embeddingsProvider": "Nhà cung cấp Embeddings", + "thinkingInjectionEnabled": "Chèn định dạng suy luận", + "thinkingInjectionDesc": "Thêm danh sách kiểm tra vào tin nhắn cuối của người dùng trước khi lắp ráp lời nhắc.", + "thinkingInjectionPrompt": "Lời nhắc chèn suy luận", + "thinkingInjectionPromptHelp": "Để trống để sử dụng lời nhắc mặc định tích hợp.", + "currentInputFileTitle": "Chia tách độc lập", + "currentInputFileEnabled": "Chia tách độc lập (theo kích thước)", + "currentInputFileDesc": "Mặc định bật. Khi đạt đến ngưỡng ký tự, sẽ tải lên toàn bộ ngữ cảnh dưới dạng tệp DS2API_HISTORY.txt.", + "currentInputFileMinChars": "Ngưỡng đầu vào hiện tại (ký tự)", + "currentInputFileHelp": "Mặc định là 0, sử dụng chia tách độc lập cho bất kỳ đầu vào nào không trống.", + "modelTitle": "Ánh xạ mô hình", + "modelAliases": "Biệt danh mô hình toàn cục (JSON)", + "autoDeleteTitle": "Chính sách dọn dép phiên", + "autoDeleteDesc": "Chọn cách dọn dép các bản ghi trò chuyện từ xa trên DeepSeek sau mỗi yêu cầu.", + "autoDeleteMode": "Chế độ xóa", + "autoDeleteNone": "Không xóa", + "autoDeleteSingle": "Xóa phiên hiện tại", + "autoDeleteAll": "Xóa tất cả các phiên", + "autoDeleteNoneDesc": "Giữ lại phiên từ xa sau khi yêu cầu hoàn thành.", + "autoDeleteSingleDesc": "Chỉ xóa phiên từ xa được tạo bởi yêu cầu này.", + "autoDeleteAllDesc": "Xóa mọi phiên từ xa của tài khoản sau khi yêu cầu hoàn thành.", + "autoDeleteWarning": "Chế độ này sẽ xóa các bản ghi trò chuyện từ xa. Hãy thận trọng.", + "backupTitle": "Sao lưu & Phục hồi", + "loadExport": "Tải bản xuất hiện tại", + "downloadExport": "Tải về tệp sao lưu", + "importModeMerge": "Nhập hợp nhất (mặc định)", + "importModeReplace": "Nhập thay thế tất cả", + "chooseImportFile": "Chọn tệp nhập", + "importNow": "Nhập ngay", + "importing": "Đang nhập...", + "importPlaceholder": "Dán JSON cấu hình để nhập", + "importEmpty": "Vui lòng nhập JSON.", + "importInvalidJson": "JSON không hợp lệ.", + "importFailed": "Nhập thất bại.", + "importSuccess": "Đã nhập cấu hình (chế độ: {mode}).", + "importFileLoaded": "Đã tải nội dung tệp nhập.", + "importFileReadFailed": "Lỗi khi đọc tệp nhập.", + "exportFailed": "Xuất thất bại.", + "exportLoaded": "Đã tải bản xuất hiện tại.", + "exportDownloaded": "Đã bắt đầu tải về tệp sao lưu.", + "exportJson": "Xuất JSON", + "invalidJsonField": "{field} không phải là đối tượng JSON hợp lệ.", + "defaultPasswordWarning": "Bạn đang sử dụng mật khẩu mặc định \"admin\". Vui lòng thay đổi nó.", + "vercelSyncHint": "Cấu hình đã thay đổi. Đối với triển khai Vercel, hãy đồng bộ thủ công trong Vercel Sync và triển khai lại.", + "autoFetchPaused": "Tự động tải tạm dừng sau {count} lần lỗi: {error}", + "retryLoad": "Thử lại ngay" + }, + "login": { + "welcome": "Chào mừng trở lại", + "subtitle": "Nhập khóa quản trị để tiếp tục", + "adminKeyLabel": "Khóa quản trị", + "adminKeyPlaceholder": "Nhập khóa quản trị của bạn...", + "rememberSession": "Ghi nhớ phiên này", + "signIn": "Đăng nhập", + "secureConnection": "Kết nối an toàn", + "adminPortal": "Cổng quản trị DS2API", + "signInFailed": "Đăng nhập thất bại.", + "networkError": "Lỗi mạng: {error}" + }, + "vercel": { + "tokenRequired": "Yêu cầu Vercel access token.", + "projectRequired": "Yêu cầu Project ID.", + "syncFailed": "Đồng bộ thất bại.", + "networkError": "Lỗi mạng.", + "title": "Triển khai Vercel", + "description": "Đồng bộ các khóa và tài khoản hiện tại trực tiếp với các biến môi trường Vercel.", + "tokenLabel": "Vercel Access Token", + "getToken": "Lấy token", + "tokenPlaceholderPreconfig": "Sử dụng token đã cấu hình trước", + "tokenPlaceholder": "Nhập Vercel access token", + "projectIdLabel": "Project ID", + "projectIdHint": "Tìm trong Project Settings → General.", + "teamIdLabel": "Team ID", + "optional": "tùy chọn", + "saveCredentials": "Ghi nhớ thông tin Vercel", + "saveCredentialsHint": "Lưu token, project ID và team ID cho lần đồng bộ tới.", + "syncing": "Đang đồng bộ...", + "syncRedeploy": "Đồng bộ & Triển khai lại", + "redeployHint": "Điều này sẽ kích hoạt triển khai lại Vercel và thường mất 30–60 giây.", + "syncSucceeded": "Đồng bộ thành công", + "syncFailedLabel": "Đồng bộ thất bại", + "openDeployment": "Mở bản triển khai", + "statusSynced": "Đã đồng bộ", + "statusNotSynced": "Chưa đồng bộ", + "statusNeverSynced": "Chưa từng đồng bộ", + "lastSyncTime": "Lần đồng bộ cuối: {time}", + "draftDiffers": "Bản nháp frontend khác với cấu hình env. Nhấp Đồng bộ & Triển khai lại.", + "pollPaused": "Tạm dừng lấy trạng thái sau {count} lần lỗi.", + "manualRefresh": "Làm mới thủ công", + "howItWorks": "Cách thức hoạt động", + "steps": { + "one": "Cấu hình hiện tại (khóa và tài khoản) được xuất dưới dạng JSON.", + "two": "JSON được mã hóa Base64 để định dạng an toàn.", + "three": "Cập nhật biến môi trường trên Vercel:", + "four": "Kích hoạt triển khai lại để áp dụng các biến môi trường đã cập nhật." + } + } +} diff --git a/webui/src/locales/zh.json b/webui/src/locales/zh.json index 7508a392d..1e9f48a63 100644 --- a/webui/src/locales/zh.json +++ b/webui/src/locales/zh.json @@ -2,7 +2,8 @@ "language": { "label": "语言", "english": "English", - "chinese": "中文" + "chinese": "中文", + "vietnamese": "越南语" }, "nav": { "accounts": { @@ -109,6 +110,8 @@ "available": "可用", "inUse": "正在使用", "totalPool": "账号池总数", + "failedPool": "不可用", + "failedPoolHint": "刷新 Token 重新验证", "accountsUnit": "个账号", "threadsUnit": "线程", "apiKeysTitle": "API 密钥",