From 0c44bfad701f2f8b7a6fbce3d5b09115b677c8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Trung=20Hi=E1=BA=BFu?= Date: Tue, 12 May 2026 07:10:09 +0700 Subject: [PATCH 1/4] feat: failed accounts card, error message display, Vietnamese i18n - pool_core: Status() now includes failed/failed_accounts counts by querying AccountTestStatus for each account in the pool - store: add accTestMsg map, UpdateAccountTestStatusWithMessage(), AccountTestMessage() to persist error message alongside test status - shared/deps: extend ConfigStore interface with the two new methods - handler_accounts_crud: listAccounts returns error_message field when test_status == failed - handler_accounts_testing: testAccount saves error message on failure via UpdateAccountTestStatusWithMessage - QueueCards: add 4th card 'Unavailable' (red when > 0, neutral when 0) using queueStatus.failed from the updated pool Status() - AccountsTable: show error_message badge under failed account rows - LanguageToggle: fix crash - render labelMap[nextLang] not undefined label - i18n/vi: add Vietnamese locale (vi.json), wire into I18nProvider and getBrowserLang auto-detection - locales en/zh/vi: add failedPool and failedPoolHint keys - start.bat: one-click dev server launcher with auto Go install --- internal/account/pool_core.go | 20 + internal/config/store.go | 38 +- .../admin/accounts/handler_accounts_crud.go | 8 +- .../accounts/handler_accounts_testing.go | 5 +- internal/httpapi/admin/shared/deps.go | 2 + start.bat | 80 +++ webui/src/components/LanguageToggle.jsx | 13 +- webui/src/features/account/AccountsTable.jsx | 5 + webui/src/features/account/QueueCards.jsx | 27 +- webui/src/i18n.jsx | 8 +- webui/src/locales/en.json | 5 +- webui/src/locales/vi.json | 491 ++++++++++++++++++ webui/src/locales/zh.json | 5 +- 13 files changed, 688 insertions(+), 19 deletions(-) create mode 100644 start.bat create mode 100644 webui/src/locales/vi.json diff --git a/internal/account/pool_core.go b/internal/account/pool_core.go index 90e2594d..bcae69cb 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 603ff9a4..351d30f8 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 7375b403..1aca7194 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 d92c1dcf..64959971 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 e063ae1c..2aaff3c5 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 00000000..921229e3 --- /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" + +:: ── Bước 1: Kiểm tra / tự cài Go ───────────────────────────────────────── +where go >nul 2>&1 +if errorlevel 1 ( + echo [SETUP] Khong tim thay Go. Dang tu dong tai va cai dat 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] Dang tai...';" ^ + "try { Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing }" ^ + "catch { Write-Host '[LOI]' $_.Exception.Message; exit 1 };" ^ + "Write-Host '[SETUP] Dang cai dat (Windows se hoi quyen Admin)...';" ^ + "Start-Process msiexec.exe -ArgumentList \"/i `\"$msi`\" /quiet /norestart\" -Verb RunAs -Wait;" ^ + "Remove-Item $msi -Force -ErrorAction SilentlyContinue;" ^ + "Write-Host '[OK] Cai dat xong.'" + if errorlevel 1 ( + echo [LOI] Cai dat that bai. Cai thu cong tai: https://go.dev/dl/ + pause & exit /b 1 + ) + :: Reload PATH trong session hien tai + 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 da cai xong. Dong va mo lai cua so nay de PATH cap nhat. + pause & exit /b 0 + ) +) +for /f "tokens=3" %%v in ('go version') do set GO_VER=%%v +echo [OK] Go %GO_VER% + +:: ── Bước 2: Kiểm tra config.json ───────────────────────────────────────── +if not exist "%ROOT%config.json" ( + echo [SETUP] Chua co config.json, sao chep tu config.example.json... + copy "%ROOT%config.example.json" "%ROOT%config.json" >nul + echo [SETUP] Da tao config.json. Hay dien tai khoan DeepSeek va API key. + start "" notepad "%ROOT%config.json" + pause & exit /b 0 +) +echo [OK] config.json + +:: ── Bước 3: Đọc PORT từ .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" + ) +) + +:: ── Bước 4: Chạy 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] Khoi dong server... (Ctrl+C de dung) +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 da dung. +pause diff --git a/webui/src/components/LanguageToggle.jsx b/webui/src/components/LanguageToggle.jsx index ce90fa9f..51d8cb96 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 14915edc..160b9124 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 53d483b8..9ac452d5 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 5515b7f5..71df59f6 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,10 @@ 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.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 c0db122b..480c82b2 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 00000000..8cf0c11d --- /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 7508a392..1e9f48a6 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 密钥", From 0238f29ec9e8d1ce13546bd235b8d26e86717505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Trung=20Hi=E1=BA=BFu?= Date: Tue, 12 May 2026 07:12:09 +0700 Subject: [PATCH 2/4] docs: translate start.bat to English, add start.bat docs to README --- README.MD | 4 +++- README.en.md | 4 +++- start.bat | 32 ++++++++++++++++---------------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/README.MD b/README.MD index 920201f0..2f8ebe55 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 afb4c7dd..6d5fee49 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/start.bat b/start.bat index 921229e3..87232adb 100644 --- a/start.bat +++ b/start.bat @@ -10,47 +10,47 @@ echo. set "ROOT=%~dp0" -:: ── Bước 1: Kiểm tra / tự cài Go ───────────────────────────────────────── +:: ── Step 1: Check / auto-install Go ────────────────────────────────────── where go >nul 2>&1 if errorlevel 1 ( - echo [SETUP] Khong tim thay Go. Dang tu dong tai va cai dat Go 1.26.3... + 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] Dang tai...';" ^ + "Write-Host '[SETUP] Downloading...';" ^ "try { Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing }" ^ - "catch { Write-Host '[LOI]' $_.Exception.Message; exit 1 };" ^ - "Write-Host '[SETUP] Dang cai dat (Windows se hoi quyen Admin)...';" ^ + "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] Cai dat xong.'" + "Write-Host '[OK] Go installed.'" if errorlevel 1 ( - echo [LOI] Cai dat that bai. Cai thu cong tai: https://go.dev/dl/ + echo [ERROR] Installation failed. Install manually from: https://go.dev/dl/ pause & exit /b 1 ) - :: Reload PATH trong session hien tai + :: 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 da cai xong. Dong va mo lai cua so nay de PATH cap nhat. + 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% -:: ── Bước 2: Kiểm tra config.json ───────────────────────────────────────── +:: ── Step 2: Check config.json ───────────────────────────────────────────── if not exist "%ROOT%config.json" ( - echo [SETUP] Chua co config.json, sao chep tu config.example.json... + echo [SETUP] config.json not found. Copying from config.example.json... copy "%ROOT%config.example.json" "%ROOT%config.json" >nul - echo [SETUP] Da tao config.json. Hay dien tai khoan DeepSeek va API key. + 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 -:: ── Bước 3: Đọc PORT từ .env ───────────────────────────────────────────── +:: ── 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 ( @@ -58,13 +58,13 @@ if exist "%ROOT%.env" ( ) ) -:: ── Bước 4: Chạy server ────────────────────────────────────────────────── +:: ── 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] Khoi dong server... (Ctrl+C de dung) +echo [INFO] Starting server... (Ctrl+C to stop) echo ============================================ echo. @@ -76,5 +76,5 @@ set "PORT=%PORT%" go run ./cmd/ds2api echo. -echo [INFO] Server da dung. +echo [INFO] Server stopped. pause From 58d0ab622f4772a1a8aa3d858e5a10afa82a528a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Trung=20Hi=E1=BA=BFu?= Date: Tue, 12 May 2026 07:13:09 +0700 Subject: [PATCH 3/4] docs: add start.bat section to README.vi.md --- README.vi.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 README.vi.md diff --git a/README.vi.md b/README.vi.md new file mode 100644 index 00000000..d9d5d3a9 --- /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. From 7c976288f5f9820ddac303a0019b281062eb4eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Trung=20Hi=E1=BA=BFu?= Date: Tue, 12 May 2026 07:23:28 +0700 Subject: [PATCH 4/4] fix: guard navigator.language against undefined in getBrowserLang Addresses Codex review on PR #497: navigator.language?.toLowerCase() can return undefined in embedded browsers or test harnesses with incomplete navigator stubs. Add an explicit falsy check before calling startsWith() to restore null-safe behavior. --- webui/src/i18n.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webui/src/i18n.jsx b/webui/src/i18n.jsx index 71df59f6..2f51e24a 100644 --- a/webui/src/i18n.jsx +++ b/webui/src/i18n.jsx @@ -15,6 +15,7 @@ const I18nContext = createContext({ const getBrowserLang = () => { if (typeof navigator === 'undefined') return 'zh' const browserLang = navigator.language?.toLowerCase() + if (!browserLang) return 'zh' if (browserLang.startsWith('zh')) return 'zh' if (browserLang.startsWith('vi')) return 'vi' return 'en'