mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
feat(webui): 账号列表添加搜索过滤功能
- 后端 GET /admin/accounts 支持 ?q= 参数,大小写不敏感匹配 identifier/email/mobile - 前端搜索框内嵌于标题栏按钮行(测试全部按钮前) - 搜索时重置到第 1 页,分页 total 反映过滤后数量 - 无匹配结果时显示专属提示文案(中英文)
This commit is contained in:
@@ -1,115 +1,128 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"ds2api/internal/config"
|
||||
)
|
||||
|
||||
func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
page := intFromQuery(r, "page", 1)
|
||||
pageSize := intFromQuery(r, "page_size", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 1
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
accounts := h.Store.Snapshot().Accounts
|
||||
total := len(accounts)
|
||||
reverseAccounts(accounts)
|
||||
totalPages := 1
|
||||
if total > 0 {
|
||||
totalPages = (total + pageSize - 1) / pageSize
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
items := make([]map[string]any, 0, end-start)
|
||||
for _, acc := range accounts[start:end] {
|
||||
token := strings.TrimSpace(acc.Token)
|
||||
preview := ""
|
||||
if token != "" {
|
||||
if len(token) > 20 {
|
||||
preview = token[:20] + "..."
|
||||
} else {
|
||||
preview = token
|
||||
}
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"identifier": acc.Identifier(),
|
||||
"email": acc.Email,
|
||||
"mobile": acc.Mobile,
|
||||
"has_password": acc.Password != "",
|
||||
"has_token": token != "",
|
||||
"token_preview": preview,
|
||||
"test_status": acc.TestStatus,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})
|
||||
}
|
||||
|
||||
func (h *Handler) addAccount(w http.ResponseWriter, r *http.Request) {
|
||||
var req map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
acc := toAccount(req)
|
||||
if acc.Identifier() == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "需要 email 或 mobile"})
|
||||
return
|
||||
}
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
for _, a := range c.Accounts {
|
||||
if acc.Email != "" && a.Email == acc.Email {
|
||||
return fmt.Errorf("邮箱已存在")
|
||||
}
|
||||
if acc.Mobile != "" && a.Mobile == acc.Mobile {
|
||||
return fmt.Errorf("手机号已存在")
|
||||
}
|
||||
}
|
||||
c.Accounts = append(c.Accounts, acc)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
h.Pool.Reset()
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_accounts": len(h.Store.Snapshot().Accounts)})
|
||||
}
|
||||
|
||||
func (h *Handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
|
||||
identifier := chi.URLParam(r, "identifier")
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
idx := -1
|
||||
for i, a := range c.Accounts {
|
||||
if accountMatchesIdentifier(a, identifier) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("账号不存在")
|
||||
}
|
||||
c.Accounts = append(c.Accounts[:idx], c.Accounts[idx+1:]...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
h.Pool.Reset()
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_accounts": len(h.Store.Snapshot().Accounts)})
|
||||
}
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"ds2api/internal/config"
|
||||
)
|
||||
|
||||
func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
page := intFromQuery(r, "page", 1)
|
||||
pageSize := intFromQuery(r, "page_size", 10)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 1
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
accounts := h.Store.Snapshot().Accounts
|
||||
reverseAccounts(accounts)
|
||||
q := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("q")))
|
||||
if q != "" {
|
||||
filtered := make([]config.Account, 0, len(accounts))
|
||||
for _, acc := range accounts {
|
||||
id := strings.ToLower(acc.Identifier())
|
||||
if strings.Contains(id, q) ||
|
||||
strings.Contains(strings.ToLower(acc.Email), q) ||
|
||||
strings.Contains(strings.ToLower(acc.Mobile), q) {
|
||||
filtered = append(filtered, acc)
|
||||
}
|
||||
}
|
||||
accounts = filtered
|
||||
}
|
||||
total := len(accounts)
|
||||
totalPages := 1
|
||||
if total > 0 {
|
||||
totalPages = (total + pageSize - 1) / pageSize
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
items := make([]map[string]any, 0, end-start)
|
||||
for _, acc := range accounts[start:end] {
|
||||
token := strings.TrimSpace(acc.Token)
|
||||
preview := ""
|
||||
if token != "" {
|
||||
if len(token) > 20 {
|
||||
preview = token[:20] + "..."
|
||||
} else {
|
||||
preview = token
|
||||
}
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"identifier": acc.Identifier(),
|
||||
"email": acc.Email,
|
||||
"mobile": acc.Mobile,
|
||||
"has_password": acc.Password != "",
|
||||
"has_token": token != "",
|
||||
"token_preview": preview,
|
||||
"test_status": acc.TestStatus,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})
|
||||
}
|
||||
|
||||
func (h *Handler) addAccount(w http.ResponseWriter, r *http.Request) {
|
||||
var req map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
acc := toAccount(req)
|
||||
if acc.Identifier() == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "需要 email 或 mobile"})
|
||||
return
|
||||
}
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
for _, a := range c.Accounts {
|
||||
if acc.Email != "" && a.Email == acc.Email {
|
||||
return fmt.Errorf("邮箱已存在")
|
||||
}
|
||||
if acc.Mobile != "" && a.Mobile == acc.Mobile {
|
||||
return fmt.Errorf("手机号已存在")
|
||||
}
|
||||
}
|
||||
c.Accounts = append(c.Accounts, acc)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
h.Pool.Reset()
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_accounts": len(h.Store.Snapshot().Accounts)})
|
||||
}
|
||||
|
||||
func (h *Handler) deleteAccount(w http.ResponseWriter, r *http.Request) {
|
||||
identifier := chi.URLParam(r, "identifier")
|
||||
err := h.Store.Update(func(c *config.Config) error {
|
||||
idx := -1
|
||||
for i, a := range c.Accounts {
|
||||
if accountMatchesIdentifier(a, identifier) {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return fmt.Errorf("账号不存在")
|
||||
}
|
||||
c.Accounts = append(c.Accounts[:idx], c.Accounts[idx+1:]...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]any{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
h.Pool.Reset()
|
||||
writeJSON(w, http.StatusOK, map[string]any{"success": true, "total_accounts": len(h.Store.Snapshot().Accounts)})
|
||||
}
|
||||
|
||||
@@ -1,117 +1,121 @@
|
||||
import { useI18n } from '../../i18n'
|
||||
import { useAccountsData } from './useAccountsData'
|
||||
import { useAccountActions } from './useAccountActions'
|
||||
import QueueCards from './QueueCards'
|
||||
import ApiKeysPanel from './ApiKeysPanel'
|
||||
import AccountsTable from './AccountsTable'
|
||||
import AddKeyModal from './AddKeyModal'
|
||||
import AddAccountModal from './AddAccountModal'
|
||||
|
||||
export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
|
||||
const { t } = useI18n()
|
||||
const apiFetch = authFetch || fetch
|
||||
|
||||
const {
|
||||
queueStatus,
|
||||
keysExpanded,
|
||||
setKeysExpanded,
|
||||
accounts,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalAccounts,
|
||||
loadingAccounts,
|
||||
fetchAccounts,
|
||||
changePageSize,
|
||||
resolveAccountIdentifier,
|
||||
} = useAccountsData({ apiFetch })
|
||||
|
||||
const {
|
||||
showAddKey,
|
||||
setShowAddKey,
|
||||
showAddAccount,
|
||||
setShowAddAccount,
|
||||
newKey,
|
||||
setNewKey,
|
||||
copiedKey,
|
||||
setCopiedKey,
|
||||
newAccount,
|
||||
setNewAccount,
|
||||
loading,
|
||||
testing,
|
||||
testingAll,
|
||||
batchProgress,
|
||||
addKey,
|
||||
deleteKey,
|
||||
addAccount,
|
||||
deleteAccount,
|
||||
testAccount,
|
||||
testAllAccounts,
|
||||
} = useAccountActions({
|
||||
apiFetch,
|
||||
t,
|
||||
onMessage,
|
||||
onRefresh,
|
||||
config,
|
||||
fetchAccounts,
|
||||
resolveAccountIdentifier,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<QueueCards queueStatus={queueStatus} t={t} />
|
||||
|
||||
<ApiKeysPanel
|
||||
t={t}
|
||||
config={config}
|
||||
keysExpanded={keysExpanded}
|
||||
setKeysExpanded={setKeysExpanded}
|
||||
setShowAddKey={setShowAddKey}
|
||||
copiedKey={copiedKey}
|
||||
setCopiedKey={setCopiedKey}
|
||||
onDeleteKey={deleteKey}
|
||||
/>
|
||||
|
||||
<AccountsTable
|
||||
t={t}
|
||||
accounts={accounts}
|
||||
loadingAccounts={loadingAccounts}
|
||||
testing={testing}
|
||||
testingAll={testingAll}
|
||||
batchProgress={batchProgress}
|
||||
totalAccounts={totalAccounts}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
totalPages={totalPages}
|
||||
resolveAccountIdentifier={resolveAccountIdentifier}
|
||||
onTestAll={testAllAccounts}
|
||||
onShowAddAccount={() => setShowAddAccount(true)}
|
||||
onTestAccount={testAccount}
|
||||
onDeleteAccount={deleteAccount}
|
||||
onPrevPage={() => fetchAccounts(page - 1)}
|
||||
onNextPage={() => fetchAccounts(page + 1)}
|
||||
onPageSizeChange={changePageSize}
|
||||
/>
|
||||
|
||||
<AddKeyModal
|
||||
show={showAddKey}
|
||||
t={t}
|
||||
newKey={newKey}
|
||||
setNewKey={setNewKey}
|
||||
loading={loading}
|
||||
onClose={() => setShowAddKey(false)}
|
||||
onAdd={addKey}
|
||||
/>
|
||||
|
||||
<AddAccountModal
|
||||
show={showAddAccount}
|
||||
t={t}
|
||||
newAccount={newAccount}
|
||||
setNewAccount={setNewAccount}
|
||||
loading={loading}
|
||||
onClose={() => setShowAddAccount(false)}
|
||||
onAdd={addAccount}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useI18n } from '../../i18n'
|
||||
import { useAccountsData } from './useAccountsData'
|
||||
import { useAccountActions } from './useAccountActions'
|
||||
import QueueCards from './QueueCards'
|
||||
import ApiKeysPanel from './ApiKeysPanel'
|
||||
import AccountsTable from './AccountsTable'
|
||||
import AddKeyModal from './AddKeyModal'
|
||||
import AddAccountModal from './AddAccountModal'
|
||||
|
||||
export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
|
||||
const { t } = useI18n()
|
||||
const apiFetch = authFetch || fetch
|
||||
|
||||
const {
|
||||
queueStatus,
|
||||
keysExpanded,
|
||||
setKeysExpanded,
|
||||
accounts,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalAccounts,
|
||||
loadingAccounts,
|
||||
fetchAccounts,
|
||||
changePageSize,
|
||||
resolveAccountIdentifier,
|
||||
searchQuery,
|
||||
handleSearchChange,
|
||||
} = useAccountsData({ apiFetch })
|
||||
|
||||
const {
|
||||
showAddKey,
|
||||
setShowAddKey,
|
||||
showAddAccount,
|
||||
setShowAddAccount,
|
||||
newKey,
|
||||
setNewKey,
|
||||
copiedKey,
|
||||
setCopiedKey,
|
||||
newAccount,
|
||||
setNewAccount,
|
||||
loading,
|
||||
testing,
|
||||
testingAll,
|
||||
batchProgress,
|
||||
addKey,
|
||||
deleteKey,
|
||||
addAccount,
|
||||
deleteAccount,
|
||||
testAccount,
|
||||
testAllAccounts,
|
||||
} = useAccountActions({
|
||||
apiFetch,
|
||||
t,
|
||||
onMessage,
|
||||
onRefresh,
|
||||
config,
|
||||
fetchAccounts,
|
||||
resolveAccountIdentifier,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<QueueCards queueStatus={queueStatus} t={t} />
|
||||
|
||||
<ApiKeysPanel
|
||||
t={t}
|
||||
config={config}
|
||||
keysExpanded={keysExpanded}
|
||||
setKeysExpanded={setKeysExpanded}
|
||||
setShowAddKey={setShowAddKey}
|
||||
copiedKey={copiedKey}
|
||||
setCopiedKey={setCopiedKey}
|
||||
onDeleteKey={deleteKey}
|
||||
/>
|
||||
|
||||
<AccountsTable
|
||||
t={t}
|
||||
accounts={accounts}
|
||||
loadingAccounts={loadingAccounts}
|
||||
testing={testing}
|
||||
testingAll={testingAll}
|
||||
batchProgress={batchProgress}
|
||||
totalAccounts={totalAccounts}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
totalPages={totalPages}
|
||||
resolveAccountIdentifier={resolveAccountIdentifier}
|
||||
onTestAll={testAllAccounts}
|
||||
onShowAddAccount={() => setShowAddAccount(true)}
|
||||
onTestAccount={testAccount}
|
||||
onDeleteAccount={deleteAccount}
|
||||
onPrevPage={() => fetchAccounts(page - 1)}
|
||||
onNextPage={() => fetchAccounts(page + 1)}
|
||||
onPageSizeChange={changePageSize}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
/>
|
||||
|
||||
<AddKeyModal
|
||||
show={showAddKey}
|
||||
t={t}
|
||||
newKey={newKey}
|
||||
setNewKey={setNewKey}
|
||||
loading={loading}
|
||||
onClose={() => setShowAddKey(false)}
|
||||
onAdd={addKey}
|
||||
/>
|
||||
|
||||
<AddAccountModal
|
||||
show={showAddAccount}
|
||||
t={t}
|
||||
newAccount={newAccount}
|
||||
setNewAccount={setNewAccount}
|
||||
loading={loading}
|
||||
onClose={() => setShowAddAccount(false)}
|
||||
onAdd={addAccount}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,182 +1,191 @@
|
||||
import { useState } from 'react'
|
||||
import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2 } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export default function AccountsTable({
|
||||
t,
|
||||
accounts,
|
||||
loadingAccounts,
|
||||
testing,
|
||||
testingAll,
|
||||
batchProgress,
|
||||
totalAccounts,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
resolveAccountIdentifier,
|
||||
onTestAll,
|
||||
onShowAddAccount,
|
||||
onTestAccount,
|
||||
onDeleteAccount,
|
||||
onPrevPage,
|
||||
onNextPage,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const [copiedId, setCopiedId] = useState(null)
|
||||
|
||||
const copyId = (id) => {
|
||||
navigator.clipboard.writeText(id).then(() => {
|
||||
setCopiedId(id)
|
||||
setTimeout(() => setCopiedId(null), 1500)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
||||
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{t('accountManager.accountsTitle')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={onTestAll}
|
||||
disabled={testingAll || totalAccounts === 0}
|
||||
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
|
||||
>
|
||||
{testingAll ? <span className="animate-spin mr-2">⟳</span> : <Play className="w-3 h-3 mr-2" />}
|
||||
{t('accountManager.testAll')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onShowAddAccount}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('accountManager.addAccount')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{testingAll && batchProgress.total > 0 && (
|
||||
<div className="p-4 border-b border-border bg-muted/30">
|
||||
<div className="flex items-center justify-between text-sm mb-2">
|
||||
<span className="font-medium">{t('accountManager.testingAllAccounts')}</span>
|
||||
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
|
||||
<div
|
||||
className="bg-primary h-full transition-all duration-300"
|
||||
style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
{batchProgress.results.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 max-h-32 overflow-y-auto custom-scrollbar">
|
||||
{batchProgress.results.map((r, i) => (
|
||||
<div key={i} className={clsx(
|
||||
"text-xs px-2 py-1 rounded border truncate",
|
||||
r.success ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-500" : "bg-destructive/10 border-destructive/20 text-destructive"
|
||||
)}>
|
||||
{r.success ? '✓' : '✗'} {r.id}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
{loadingAccounts ? (
|
||||
<div className="p-8 text-center text-muted-foreground">{t('actions.loading')}</div>
|
||||
) : accounts.length > 0 ? (
|
||||
accounts.map((acc, i) => {
|
||||
const id = resolveAccountIdentifier(acc)
|
||||
return (
|
||||
<div key={i} className="p-4 flex flex-col md:flex-row md:items-center justify-between gap-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className={clsx(
|
||||
"w-2 h-2 rounded-full shrink-0",
|
||||
acc.test_status === 'failed' ? "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" :
|
||||
(acc.test_status === 'ok' || acc.has_token) ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
|
||||
"bg-amber-500"
|
||||
)} />
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="font-medium truncate flex items-center gap-1.5 cursor-pointer hover:text-primary transition-colors group"
|
||||
onClick={() => copyId(id)}
|
||||
>
|
||||
<span className="truncate">{id || '-'}</span>
|
||||
{copiedId === id
|
||||
? <Check className="w-3 h-3 text-emerald-500 shrink-0" />
|
||||
: <Copy className="w-3 h-3 opacity-0 group-hover:opacity-50 shrink-0 transition-opacity" />
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
||||
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
|
||||
{acc.token_preview && (
|
||||
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
|
||||
{acc.token_preview}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-start lg:self-auto ml-5 lg:ml-0">
|
||||
<button
|
||||
onClick={() => onTestAccount(id)}
|
||||
disabled={testing[id]}
|
||||
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
|
||||
>
|
||||
{testing[id] ? t('actions.testing') : t('actions.test')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteAccount(id)}
|
||||
className="p-1 lg:p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="p-8 text-center text-muted-foreground">{t('accountManager.noAccounts')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="p-4 border-t border-border flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })}
|
||||
</div>
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={e => onPageSizeChange(Number(e.target.value))}
|
||||
className="text-sm border border-border rounded-md px-2 py-1 bg-background text-foreground"
|
||||
>
|
||||
{[10, 20, 50, 100, 500, 1000, 2000, 5000].map(s => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onPrevPage}
|
||||
disabled={page <= 1 || loadingAccounts}
|
||||
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="text-sm font-medium px-2">{page} / {totalPages}</span>
|
||||
<button
|
||||
onClick={onNextPage}
|
||||
disabled={page >= totalPages || loadingAccounts}
|
||||
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useState } from 'react'
|
||||
import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2 } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export default function AccountsTable({
|
||||
t,
|
||||
accounts,
|
||||
loadingAccounts,
|
||||
testing,
|
||||
testingAll,
|
||||
batchProgress,
|
||||
totalAccounts,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
resolveAccountIdentifier,
|
||||
onTestAll,
|
||||
onShowAddAccount,
|
||||
onTestAccount,
|
||||
onDeleteAccount,
|
||||
onPrevPage,
|
||||
onNextPage,
|
||||
onPageSizeChange,
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
}) {
|
||||
const [copiedId, setCopiedId] = useState(null)
|
||||
|
||||
const copyId = (id) => {
|
||||
navigator.clipboard.writeText(id).then(() => {
|
||||
setCopiedId(id)
|
||||
setTimeout(() => setCopiedId(null), 1500)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
||||
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{t('accountManager.accountsTitle')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={e => onSearchChange(e.target.value)}
|
||||
placeholder={t('accountManager.searchPlaceholder')}
|
||||
className="px-3 py-1.5 text-sm bg-muted border border-border rounded-lg focus:outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground"
|
||||
/>
|
||||
<button
|
||||
onClick={onTestAll}
|
||||
disabled={testingAll || totalAccounts === 0}
|
||||
className="flex items-center px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-xs font-medium border border-border disabled:opacity-50"
|
||||
>
|
||||
{testingAll ? <span className="animate-spin mr-2">⟳</span> : <Play className="w-3 h-3 mr-2" />}
|
||||
{t('accountManager.testAll')}
|
||||
</button>
|
||||
<button
|
||||
onClick={onShowAddAccount}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors font-medium text-sm shadow-sm"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('accountManager.addAccount')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{testingAll && batchProgress.total > 0 && (
|
||||
<div className="p-4 border-b border-border bg-muted/30">
|
||||
<div className="flex items-center justify-between text-sm mb-2">
|
||||
<span className="font-medium">{t('accountManager.testingAllAccounts')}</span>
|
||||
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
|
||||
<div
|
||||
className="bg-primary h-full transition-all duration-300"
|
||||
style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
{batchProgress.results.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 max-h-32 overflow-y-auto custom-scrollbar">
|
||||
{batchProgress.results.map((r, i) => (
|
||||
<div key={i} className={clsx(
|
||||
"text-xs px-2 py-1 rounded border truncate",
|
||||
r.success ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-500" : "bg-destructive/10 border-destructive/20 text-destructive"
|
||||
)}>
|
||||
{r.success ? '✓' : '✗'} {r.id}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
{loadingAccounts ? (
|
||||
<div className="p-8 text-center text-muted-foreground">{t('actions.loading')}</div>
|
||||
) : accounts.length > 0 ? (
|
||||
accounts.map((acc, i) => {
|
||||
const id = resolveAccountIdentifier(acc)
|
||||
return (
|
||||
<div key={i} className="p-4 flex flex-col md:flex-row md:items-center justify-between gap-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className={clsx(
|
||||
"w-2 h-2 rounded-full shrink-0",
|
||||
acc.test_status === 'failed' ? "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" :
|
||||
(acc.test_status === 'ok' || acc.has_token) ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
|
||||
"bg-amber-500"
|
||||
)} />
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="font-medium truncate flex items-center gap-1.5 cursor-pointer hover:text-primary transition-colors group"
|
||||
onClick={() => copyId(id)}
|
||||
>
|
||||
<span className="truncate">{id || '-'}</span>
|
||||
{copiedId === id
|
||||
? <Check className="w-3 h-3 text-emerald-500 shrink-0" />
|
||||
: <Copy className="w-3 h-3 opacity-0 group-hover:opacity-50 shrink-0 transition-opacity" />
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
||||
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
|
||||
{acc.token_preview && (
|
||||
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
|
||||
{acc.token_preview}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-start lg:self-auto ml-5 lg:ml-0">
|
||||
<button
|
||||
onClick={() => onTestAccount(id)}
|
||||
disabled={testing[id]}
|
||||
className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50"
|
||||
>
|
||||
{testing[id] ? t('actions.testing') : t('actions.test')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteAccount(id)}
|
||||
className="p-1 lg:p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="p-8 text-center text-muted-foreground">{searchQuery ? t('accountManager.searchNoResults') : t('accountManager.noAccounts')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="p-4 border-t border-border flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })}
|
||||
</div>
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={e => onPageSizeChange(Number(e.target.value))}
|
||||
className="text-sm border border-border rounded-md px-2 py-1 bg-background text-foreground"
|
||||
>
|
||||
{[10, 20, 50, 100, 500, 1000, 2000, 5000].map(s => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onPrevPage}
|
||||
disabled={page <= 1 || loadingAccounts}
|
||||
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="text-sm font-medium px-2">{page} / {totalPages}</span>
|
||||
<button
|
||||
onClick={onNextPage}
|
||||
disabled={page >= totalPages || loadingAccounts}
|
||||
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,75 +1,86 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useAccountsData({ apiFetch }) {
|
||||
const [queueStatus, setQueueStatus] = useState(null)
|
||||
const [keysExpanded, setKeysExpanded] = useState(false)
|
||||
|
||||
const [accounts, setAccounts] = useState([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [totalAccounts, setTotalAccounts] = useState(0)
|
||||
const [loadingAccounts, setLoadingAccounts] = useState(false)
|
||||
|
||||
const resolveAccountIdentifier = (acc) => {
|
||||
if (!acc || typeof acc !== 'object') return ''
|
||||
return String(acc.identifier || acc.email || acc.mobile || '').trim()
|
||||
}
|
||||
|
||||
const fetchAccounts = async (targetPage = page, targetPageSize = pageSize) => {
|
||||
setLoadingAccounts(true)
|
||||
try {
|
||||
const res = await apiFetch(`/admin/accounts?page=${targetPage}&page_size=${targetPageSize}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setAccounts(data.items || [])
|
||||
setTotalPages(data.total_pages || 1)
|
||||
setTotalAccounts(data.total || 0)
|
||||
setPage(data.page || 1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch accounts:', e)
|
||||
} finally {
|
||||
setLoadingAccounts(false)
|
||||
}
|
||||
}
|
||||
|
||||
const changePageSize = (newSize) => {
|
||||
setPageSize(newSize)
|
||||
fetchAccounts(1, newSize)
|
||||
}
|
||||
|
||||
const fetchQueueStatus = async () => {
|
||||
try {
|
||||
const res = await apiFetch('/admin/queue/status')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setQueueStatus(data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch queue status:', e)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchQueueStatus()
|
||||
const interval = setInterval(fetchQueueStatus, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
queueStatus,
|
||||
keysExpanded,
|
||||
setKeysExpanded,
|
||||
accounts,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalAccounts,
|
||||
loadingAccounts,
|
||||
fetchAccounts,
|
||||
changePageSize,
|
||||
resolveAccountIdentifier,
|
||||
}
|
||||
}
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useAccountsData({ apiFetch }) {
|
||||
const [queueStatus, setQueueStatus] = useState(null)
|
||||
const [keysExpanded, setKeysExpanded] = useState(false)
|
||||
|
||||
const [accounts, setAccounts] = useState([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [totalAccounts, setTotalAccounts] = useState(0)
|
||||
const [loadingAccounts, setLoadingAccounts] = useState(false)
|
||||
|
||||
const resolveAccountIdentifier = (acc) => {
|
||||
if (!acc || typeof acc !== 'object') return ''
|
||||
return String(acc.identifier || acc.email || acc.mobile || '').trim()
|
||||
}
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
const fetchAccounts = async (targetPage = page, targetPageSize = pageSize, targetQuery = searchQuery) => {
|
||||
setLoadingAccounts(true)
|
||||
try {
|
||||
let url = `/admin/accounts?page=${targetPage}&page_size=${targetPageSize}`
|
||||
if (targetQuery.trim()) url += `&q=${encodeURIComponent(targetQuery.trim())}`
|
||||
const res = await apiFetch(url)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setAccounts(data.items || [])
|
||||
setTotalPages(data.total_pages || 1)
|
||||
setTotalAccounts(data.total || 0)
|
||||
setPage(data.page || 1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch accounts:', e)
|
||||
} finally {
|
||||
setLoadingAccounts(false)
|
||||
}
|
||||
}
|
||||
|
||||
const changePageSize = (newSize) => {
|
||||
setPageSize(newSize)
|
||||
fetchAccounts(1, newSize)
|
||||
}
|
||||
|
||||
const handleSearchChange = (query) => {
|
||||
setSearchQuery(query)
|
||||
fetchAccounts(1, pageSize, query)
|
||||
}
|
||||
|
||||
const fetchQueueStatus = async () => {
|
||||
try {
|
||||
const res = await apiFetch('/admin/queue/status')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setQueueStatus(data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch queue status:', e)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchQueueStatus()
|
||||
const interval = setInterval(fetchQueueStatus, 5000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
queueStatus,
|
||||
keysExpanded,
|
||||
setKeysExpanded,
|
||||
accounts,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
totalAccounts,
|
||||
loadingAccounts,
|
||||
fetchAccounts,
|
||||
changePageSize,
|
||||
resolveAccountIdentifier,
|
||||
searchQuery,
|
||||
handleSearchChange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,295 +1,297 @@
|
||||
{
|
||||
"language": {
|
||||
"label": "Language",
|
||||
"english": "English",
|
||||
"chinese": "中文"
|
||||
},
|
||||
"nav": {
|
||||
"accounts": {
|
||||
"label": "Account Management",
|
||||
"desc": "Manage the DeepSeek account pool"
|
||||
},
|
||||
"test": {
|
||||
"label": "API Test",
|
||||
"desc": "Test API connectivity and responses"
|
||||
},
|
||||
"import": {
|
||||
"label": "Batch Import",
|
||||
"desc": "Bulk import account configuration"
|
||||
},
|
||||
"vercel": {
|
||||
"label": "Vercel Sync",
|
||||
"desc": "Sync configuration to Vercel"
|
||||
},
|
||||
"settings": {
|
||||
"label": "Settings",
|
||||
"desc": "Edit runtime and security settings online"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"onlineAdminConsole": "Online Admin Console",
|
||||
"systemStatus": "System Status",
|
||||
"statusOnline": "Online",
|
||||
"accounts": "Accounts",
|
||||
"keys": "Keys",
|
||||
"signOut": "Sign out"
|
||||
},
|
||||
"auth": {
|
||||
"expired": "Authentication expired. Please sign in again.",
|
||||
"checking": "Checking authentication status..."
|
||||
},
|
||||
"errors": {
|
||||
"fetchConfig": "Failed to fetch configuration: {error}"
|
||||
},
|
||||
"actions": {
|
||||
"cancel": "Cancel",
|
||||
"add": "Add",
|
||||
"delete": "Delete",
|
||||
"copy": "Copy",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"testing": "Testing...",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"messages": {
|
||||
"deleted": "Deleted successfully",
|
||||
"deleteFailed": "Delete failed",
|
||||
"failedToAdd": "Failed to add",
|
||||
"networkError": "Network error.",
|
||||
"requestFailed": "Request failed.",
|
||||
"generationStopped": "Generation stopped.",
|
||||
"invalidJson": "Invalid JSON format.",
|
||||
"importFailed": "Import failed.",
|
||||
"copyFailed": "Copy failed."
|
||||
},
|
||||
"landing": {
|
||||
"adminConsole": "Admin Console",
|
||||
"apiStatus": "API Status",
|
||||
"features": {
|
||||
"compatibility": {
|
||||
"title": "Full Compatibility",
|
||||
"desc": "OpenAI & Claude format support"
|
||||
},
|
||||
"loadBalancing": {
|
||||
"title": "Load Balancing",
|
||||
"desc": "Smart rotation with stable throughput"
|
||||
},
|
||||
"reasoning": {
|
||||
"title": "Deep Reasoning",
|
||||
"desc": "Expose reasoning traces when enabled"
|
||||
},
|
||||
"search": {
|
||||
"title": "Web Search",
|
||||
"desc": "Integrated native web search"
|
||||
}
|
||||
}
|
||||
},
|
||||
"accountManager": {
|
||||
"addKeySuccess": "API key added successfully.",
|
||||
"addAccountSuccess": "Account added successfully.",
|
||||
"requiredFields": "Password and email/mobile are required.",
|
||||
"deleteKeyConfirm": "Are you sure you want to delete this API key?",
|
||||
"deleteAccountConfirm": "Are you sure you want to delete this account?",
|
||||
"invalidIdentifier": "Invalid account identifier. Operation aborted.",
|
||||
"testAllConfirm": "Test API connectivity for all accounts?",
|
||||
"testAllCompleted": "Completed: {success}/{total} available",
|
||||
"testFailed": "Test failed: {error}",
|
||||
"available": "Available",
|
||||
"inUse": "In use",
|
||||
"totalPool": "Total pool",
|
||||
"accountsUnit": "accounts",
|
||||
"threadsUnit": "threads",
|
||||
"apiKeysTitle": "API Keys",
|
||||
"apiKeysDesc": "Manage the API access key pool",
|
||||
"addKey": "Add key",
|
||||
"copied": "Copied",
|
||||
"copyKeyTitle": "Copy key",
|
||||
"deleteKeyTitle": "Delete key",
|
||||
"noApiKeys": "No API keys found.",
|
||||
"accountsTitle": "DeepSeek Accounts",
|
||||
"accountsDesc": "Manage the DeepSeek account pool",
|
||||
"testAll": "Test all",
|
||||
"addAccount": "Add account",
|
||||
"testingAllAccounts": "Testing all accounts...",
|
||||
"sessionActive": "Session active",
|
||||
"reauthRequired": "Re-auth required",
|
||||
"testStatusFailed": "Last test failed",
|
||||
"noAccounts": "No accounts found.",
|
||||
"modalAddKeyTitle": "Add API key",
|
||||
"newKeyLabel": "New key value",
|
||||
"newKeyPlaceholder": "Enter a custom API key",
|
||||
"generate": "Generate",
|
||||
"generateHint": "Click Generate to create a random key.",
|
||||
"addKeyLoading": "Adding...",
|
||||
"addKeyAction": "Add key",
|
||||
"modalAddAccountTitle": "Add DeepSeek account",
|
||||
"emailOptional": "Email (optional)",
|
||||
"mobileOptional": "Mobile (optional)",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Account password",
|
||||
"addAccountLoading": "Adding...",
|
||||
"addAccountAction": "Add account",
|
||||
"pageInfo": "Page {current}/{total}, {count} accounts total"
|
||||
},
|
||||
"apiTester": {
|
||||
"defaultMessage": "Hello, please introduce yourself in one sentence.",
|
||||
"models": {
|
||||
"chat": "Non-reasoning model",
|
||||
"reasoner": "Reasoning model",
|
||||
"chatSearch": "Non-reasoning model (with search)",
|
||||
"reasonerSearch": "Reasoning model (with search)"
|
||||
},
|
||||
"missingApiKey": "Please provide an API key.",
|
||||
"requestFailed": "Request failed.",
|
||||
"networkError": "Network error: {error}",
|
||||
"testSuccess": "{account}: Test successful ({time}ms)",
|
||||
"config": "Configuration",
|
||||
"modelLabel": "Model",
|
||||
"streamMode": "Streaming",
|
||||
"accountSelector": "Account",
|
||||
"autoRandom": "🤖 Auto / Random",
|
||||
"apiKeyOptional": "API Key (optional)",
|
||||
"apiKeyDefault": "Default: ...{suffix}",
|
||||
"apiKeyPlaceholder": "Enter a custom key",
|
||||
"modeManaged": "Managed key mode (uses account pool).",
|
||||
"modeDirect": "Direct token mode (requires a valid DeepSeek token).",
|
||||
"statusError": "Error",
|
||||
"reasoningTrace": "Reasoning Trace",
|
||||
"generating": "Generating response...",
|
||||
"enterMessage": "Enter a message...",
|
||||
"adminConsoleLabel": "DeepSeek admin console"
|
||||
},
|
||||
"batchImport": {
|
||||
"templates": {
|
||||
"full": {
|
||||
"name": "Full configuration template",
|
||||
"desc": "Includes keys, accounts, and model mapping"
|
||||
},
|
||||
"emailOnly": {
|
||||
"name": "Email-only accounts",
|
||||
"desc": "Batch import accounts using email login"
|
||||
},
|
||||
"mobileOnly": {
|
||||
"name": "Mobile-only accounts",
|
||||
"desc": "Batch import accounts using mobile login"
|
||||
},
|
||||
"keysOnly": {
|
||||
"name": "API keys only",
|
||||
"desc": "Add API access keys only"
|
||||
}
|
||||
},
|
||||
"enterJson": "Please provide JSON configuration content.",
|
||||
"importSuccess": "Import successful: {keys} keys, {accounts} accounts",
|
||||
"templateLoaded": "Template loaded: {name}",
|
||||
"currentConfigLoaded": "Current configuration loaded.",
|
||||
"fetchConfigFailed": "Failed to fetch configuration.",
|
||||
"copySuccess": "Base64 configuration copied to clipboard.",
|
||||
"quickTemplates": "Quick Templates",
|
||||
"dataExport": "Data Export",
|
||||
"dataExportDesc": "Copy the Base64-encoded configuration for Vercel environment variables.",
|
||||
"copyBase64": "Copy Base64 config",
|
||||
"copied": "Copied",
|
||||
"variableName": "Variable name",
|
||||
"jsonEditor": "JSON Editor",
|
||||
"loadCurrentConfig": "Load current config",
|
||||
"applyConfig": "Apply config",
|
||||
"importing": "Importing...",
|
||||
"importComplete": "Import complete",
|
||||
"importSummary": "Imported {keys} API keys and updated {accounts} accounts."
|
||||
},
|
||||
"settings": {
|
||||
"loadFailed": "Failed to load settings.",
|
||||
"nonJsonResponse": "Unexpected non-JSON response from server (status: {status}).",
|
||||
"save": "Save settings",
|
||||
"saving": "Saving...",
|
||||
"saveSuccess": "Settings saved and hot reloaded.",
|
||||
"saveFailed": "Failed to save settings.",
|
||||
"securityTitle": "Security",
|
||||
"jwtExpireHours": "JWT expiry (hours)",
|
||||
"newPassword": "New admin password",
|
||||
"newPasswordPlaceholder": "Enter new password (min 4 chars)",
|
||||
"updatePassword": "Update password",
|
||||
"updating": "Updating...",
|
||||
"passwordTooShort": "Password must be at least 4 characters.",
|
||||
"passwordUpdated": "Password updated. Please sign in again.",
|
||||
"passwordUpdateFailed": "Failed to update password.",
|
||||
"runtimeTitle": "Concurrency & Queue",
|
||||
"accountMaxInflight": "Per-account max inflight",
|
||||
"accountMaxQueue": "Account max queue size",
|
||||
"globalMaxInflight": "Global max inflight",
|
||||
"behaviorTitle": "Behavior",
|
||||
"toolcallMode": "Toolcall mode",
|
||||
"earlyEmitConfidence": "Early emit confidence",
|
||||
"responsesTTL": "Responses store TTL (seconds)",
|
||||
"embeddingsProvider": "Embeddings provider",
|
||||
"modelTitle": "Model mapping",
|
||||
"claudeMapping": "Claude mapping (JSON)",
|
||||
"modelAliases": "Model aliases (JSON)",
|
||||
"backupTitle": "Backup & Restore",
|
||||
"loadExport": "Load current export",
|
||||
"importModeMerge": "Merge import (default)",
|
||||
"importModeReplace": "Replace all import",
|
||||
"importNow": "Import now",
|
||||
"importing": "Importing...",
|
||||
"importPlaceholder": "Paste config JSON to import",
|
||||
"importEmpty": "Please input import JSON.",
|
||||
"importInvalidJson": "Import JSON is invalid.",
|
||||
"importFailed": "Import failed.",
|
||||
"importSuccess": "Config imported (mode: {mode}).",
|
||||
"exportFailed": "Export failed.",
|
||||
"exportLoaded": "Current export loaded.",
|
||||
"exportJson": "Export JSON",
|
||||
"invalidJsonField": "{field} is not a valid JSON object.",
|
||||
"defaultPasswordWarning": "You are using the default admin password \"admin\". Please change it.",
|
||||
"vercelSyncHint": "Configuration changed. For Vercel deployments, sync manually in Vercel Sync and redeploy.",
|
||||
"autoFetchPaused": "Auto loading paused after {count} failures: {error}",
|
||||
"retryLoad": "Retry now"
|
||||
},
|
||||
"login": {
|
||||
"welcome": "Welcome back",
|
||||
"subtitle": "Enter your admin key to continue",
|
||||
"adminKeyLabel": "Admin key",
|
||||
"adminKeyPlaceholder": "Enter your admin key...",
|
||||
"rememberSession": "Remember this session",
|
||||
"signIn": "Sign in",
|
||||
"secureConnection": "Secure connection",
|
||||
"adminPortal": "DS2API admin portal",
|
||||
"signInFailed": "Sign-in failed.",
|
||||
"networkError": "Network error: {error}"
|
||||
},
|
||||
"vercel": {
|
||||
"tokenRequired": "Vercel access token is required.",
|
||||
"projectRequired": "Project ID is required.",
|
||||
"syncFailed": "Sync failed.",
|
||||
"networkError": "Network error.",
|
||||
"title": "Vercel Deployment",
|
||||
"description": "Sync the current keys and accounts directly to Vercel environment variables.",
|
||||
"tokenLabel": "Vercel Access Token",
|
||||
"getToken": "Get token",
|
||||
"tokenPlaceholderPreconfig": "Using preconfigured token",
|
||||
"tokenPlaceholder": "Enter Vercel access token",
|
||||
"projectIdLabel": "Project ID",
|
||||
"projectIdHint": "Find it in Project Settings → General.",
|
||||
"teamIdLabel": "Team ID",
|
||||
"optional": "optional",
|
||||
"syncing": "Syncing...",
|
||||
"syncRedeploy": "Sync & redeploy",
|
||||
"redeployHint": "This triggers a Vercel redeploy and usually takes 30–60 seconds.",
|
||||
"syncSucceeded": "Sync succeeded",
|
||||
"syncFailedLabel": "Sync failed",
|
||||
"openDeployment": "Open deployment",
|
||||
"statusSynced": "Synced",
|
||||
"statusNotSynced": "Not synced",
|
||||
"statusNeverSynced": "Never synced",
|
||||
"lastSyncTime": "Last sync: {time}",
|
||||
"pollPaused": "Status polling paused after {count} failures.",
|
||||
"manualRefresh": "Refresh manually",
|
||||
"howItWorks": "How it works",
|
||||
"steps": {
|
||||
"one": "The current configuration (keys and accounts) is exported as JSON.",
|
||||
"two": "The JSON is Base64-encoded for safe formatting.",
|
||||
"three": "Update the env var in Vercel:",
|
||||
"four": "Trigger a redeploy to apply the updated environment variables."
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"language": {
|
||||
"label": "Language",
|
||||
"english": "English",
|
||||
"chinese": "中文"
|
||||
},
|
||||
"nav": {
|
||||
"accounts": {
|
||||
"label": "Account Management",
|
||||
"desc": "Manage the DeepSeek account pool"
|
||||
},
|
||||
"test": {
|
||||
"label": "API Test",
|
||||
"desc": "Test API connectivity and responses"
|
||||
},
|
||||
"import": {
|
||||
"label": "Batch Import",
|
||||
"desc": "Bulk import account configuration"
|
||||
},
|
||||
"vercel": {
|
||||
"label": "Vercel Sync",
|
||||
"desc": "Sync configuration to Vercel"
|
||||
},
|
||||
"settings": {
|
||||
"label": "Settings",
|
||||
"desc": "Edit runtime and security settings online"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"onlineAdminConsole": "Online Admin Console",
|
||||
"systemStatus": "System Status",
|
||||
"statusOnline": "Online",
|
||||
"accounts": "Accounts",
|
||||
"keys": "Keys",
|
||||
"signOut": "Sign out"
|
||||
},
|
||||
"auth": {
|
||||
"expired": "Authentication expired. Please sign in again.",
|
||||
"checking": "Checking authentication status..."
|
||||
},
|
||||
"errors": {
|
||||
"fetchConfig": "Failed to fetch configuration: {error}"
|
||||
},
|
||||
"actions": {
|
||||
"cancel": "Cancel",
|
||||
"add": "Add",
|
||||
"delete": "Delete",
|
||||
"copy": "Copy",
|
||||
"generate": "Generate",
|
||||
"test": "Test",
|
||||
"testing": "Testing...",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"messages": {
|
||||
"deleted": "Deleted successfully",
|
||||
"deleteFailed": "Delete failed",
|
||||
"failedToAdd": "Failed to add",
|
||||
"networkError": "Network error.",
|
||||
"requestFailed": "Request failed.",
|
||||
"generationStopped": "Generation stopped.",
|
||||
"invalidJson": "Invalid JSON format.",
|
||||
"importFailed": "Import failed.",
|
||||
"copyFailed": "Copy failed."
|
||||
},
|
||||
"landing": {
|
||||
"adminConsole": "Admin Console",
|
||||
"apiStatus": "API Status",
|
||||
"features": {
|
||||
"compatibility": {
|
||||
"title": "Full Compatibility",
|
||||
"desc": "OpenAI & Claude format support"
|
||||
},
|
||||
"loadBalancing": {
|
||||
"title": "Load Balancing",
|
||||
"desc": "Smart rotation with stable throughput"
|
||||
},
|
||||
"reasoning": {
|
||||
"title": "Deep Reasoning",
|
||||
"desc": "Expose reasoning traces when enabled"
|
||||
},
|
||||
"search": {
|
||||
"title": "Web Search",
|
||||
"desc": "Integrated native web search"
|
||||
}
|
||||
}
|
||||
},
|
||||
"accountManager": {
|
||||
"addKeySuccess": "API key added successfully.",
|
||||
"addAccountSuccess": "Account added successfully.",
|
||||
"requiredFields": "Password and email/mobile are required.",
|
||||
"deleteKeyConfirm": "Are you sure you want to delete this API key?",
|
||||
"deleteAccountConfirm": "Are you sure you want to delete this account?",
|
||||
"invalidIdentifier": "Invalid account identifier. Operation aborted.",
|
||||
"testAllConfirm": "Test API connectivity for all accounts?",
|
||||
"testAllCompleted": "Completed: {success}/{total} available",
|
||||
"testFailed": "Test failed: {error}",
|
||||
"available": "Available",
|
||||
"inUse": "In use",
|
||||
"totalPool": "Total pool",
|
||||
"accountsUnit": "accounts",
|
||||
"threadsUnit": "threads",
|
||||
"apiKeysTitle": "API Keys",
|
||||
"apiKeysDesc": "Manage the API access key pool",
|
||||
"addKey": "Add key",
|
||||
"copied": "Copied",
|
||||
"copyKeyTitle": "Copy key",
|
||||
"deleteKeyTitle": "Delete key",
|
||||
"noApiKeys": "No API keys found.",
|
||||
"accountsTitle": "DeepSeek Accounts",
|
||||
"accountsDesc": "Manage the DeepSeek account pool",
|
||||
"testAll": "Test all",
|
||||
"addAccount": "Add account",
|
||||
"testingAllAccounts": "Testing all accounts...",
|
||||
"sessionActive": "Session active",
|
||||
"reauthRequired": "Re-auth required",
|
||||
"testStatusFailed": "Last test failed",
|
||||
"noAccounts": "No accounts found.",
|
||||
"modalAddKeyTitle": "Add API key",
|
||||
"newKeyLabel": "New key value",
|
||||
"newKeyPlaceholder": "Enter a custom API key",
|
||||
"generate": "Generate",
|
||||
"generateHint": "Click Generate to create a random key.",
|
||||
"addKeyLoading": "Adding...",
|
||||
"addKeyAction": "Add key",
|
||||
"modalAddAccountTitle": "Add DeepSeek account",
|
||||
"emailOptional": "Email (optional)",
|
||||
"mobileOptional": "Mobile (optional)",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Account password",
|
||||
"addAccountLoading": "Adding...",
|
||||
"addAccountAction": "Add account",
|
||||
"pageInfo": "Page {current}/{total}, {count} accounts total",
|
||||
"searchPlaceholder": "Search accounts...",
|
||||
"searchNoResults": "No accounts match your search"
|
||||
},
|
||||
"apiTester": {
|
||||
"defaultMessage": "Hello, please introduce yourself in one sentence.",
|
||||
"models": {
|
||||
"chat": "Non-reasoning model",
|
||||
"reasoner": "Reasoning model",
|
||||
"chatSearch": "Non-reasoning model (with search)",
|
||||
"reasonerSearch": "Reasoning model (with search)"
|
||||
},
|
||||
"missingApiKey": "Please provide an API key.",
|
||||
"requestFailed": "Request failed.",
|
||||
"networkError": "Network error: {error}",
|
||||
"testSuccess": "{account}: Test successful ({time}ms)",
|
||||
"config": "Configuration",
|
||||
"modelLabel": "Model",
|
||||
"streamMode": "Streaming",
|
||||
"accountSelector": "Account",
|
||||
"autoRandom": "🤖 Auto / Random",
|
||||
"apiKeyOptional": "API Key (optional)",
|
||||
"apiKeyDefault": "Default: ...{suffix}",
|
||||
"apiKeyPlaceholder": "Enter a custom key",
|
||||
"modeManaged": "Managed key mode (uses account pool).",
|
||||
"modeDirect": "Direct token mode (requires a valid DeepSeek token).",
|
||||
"statusError": "Error",
|
||||
"reasoningTrace": "Reasoning Trace",
|
||||
"generating": "Generating response...",
|
||||
"enterMessage": "Enter a message...",
|
||||
"adminConsoleLabel": "DeepSeek admin console"
|
||||
},
|
||||
"batchImport": {
|
||||
"templates": {
|
||||
"full": {
|
||||
"name": "Full configuration template",
|
||||
"desc": "Includes keys, accounts, and model mapping"
|
||||
},
|
||||
"emailOnly": {
|
||||
"name": "Email-only accounts",
|
||||
"desc": "Batch import accounts using email login"
|
||||
},
|
||||
"mobileOnly": {
|
||||
"name": "Mobile-only accounts",
|
||||
"desc": "Batch import accounts using mobile login"
|
||||
},
|
||||
"keysOnly": {
|
||||
"name": "API keys only",
|
||||
"desc": "Add API access keys only"
|
||||
}
|
||||
},
|
||||
"enterJson": "Please provide JSON configuration content.",
|
||||
"importSuccess": "Import successful: {keys} keys, {accounts} accounts",
|
||||
"templateLoaded": "Template loaded: {name}",
|
||||
"currentConfigLoaded": "Current configuration loaded.",
|
||||
"fetchConfigFailed": "Failed to fetch configuration.",
|
||||
"copySuccess": "Base64 configuration copied to clipboard.",
|
||||
"quickTemplates": "Quick Templates",
|
||||
"dataExport": "Data Export",
|
||||
"dataExportDesc": "Copy the Base64-encoded configuration for Vercel environment variables.",
|
||||
"copyBase64": "Copy Base64 config",
|
||||
"copied": "Copied",
|
||||
"variableName": "Variable name",
|
||||
"jsonEditor": "JSON Editor",
|
||||
"loadCurrentConfig": "Load current config",
|
||||
"applyConfig": "Apply config",
|
||||
"importing": "Importing...",
|
||||
"importComplete": "Import complete",
|
||||
"importSummary": "Imported {keys} API keys and updated {accounts} accounts."
|
||||
},
|
||||
"settings": {
|
||||
"loadFailed": "Failed to load settings.",
|
||||
"nonJsonResponse": "Unexpected non-JSON response from server (status: {status}).",
|
||||
"save": "Save settings",
|
||||
"saving": "Saving...",
|
||||
"saveSuccess": "Settings saved and hot reloaded.",
|
||||
"saveFailed": "Failed to save settings.",
|
||||
"securityTitle": "Security",
|
||||
"jwtExpireHours": "JWT expiry (hours)",
|
||||
"newPassword": "New admin password",
|
||||
"newPasswordPlaceholder": "Enter new password (min 4 chars)",
|
||||
"updatePassword": "Update password",
|
||||
"updating": "Updating...",
|
||||
"passwordTooShort": "Password must be at least 4 characters.",
|
||||
"passwordUpdated": "Password updated. Please sign in again.",
|
||||
"passwordUpdateFailed": "Failed to update password.",
|
||||
"runtimeTitle": "Concurrency & Queue",
|
||||
"accountMaxInflight": "Per-account max inflight",
|
||||
"accountMaxQueue": "Account max queue size",
|
||||
"globalMaxInflight": "Global max inflight",
|
||||
"behaviorTitle": "Behavior",
|
||||
"toolcallMode": "Toolcall mode",
|
||||
"earlyEmitConfidence": "Early emit confidence",
|
||||
"responsesTTL": "Responses store TTL (seconds)",
|
||||
"embeddingsProvider": "Embeddings provider",
|
||||
"modelTitle": "Model mapping",
|
||||
"claudeMapping": "Claude mapping (JSON)",
|
||||
"modelAliases": "Model aliases (JSON)",
|
||||
"backupTitle": "Backup & Restore",
|
||||
"loadExport": "Load current export",
|
||||
"importModeMerge": "Merge import (default)",
|
||||
"importModeReplace": "Replace all import",
|
||||
"importNow": "Import now",
|
||||
"importing": "Importing...",
|
||||
"importPlaceholder": "Paste config JSON to import",
|
||||
"importEmpty": "Please input import JSON.",
|
||||
"importInvalidJson": "Import JSON is invalid.",
|
||||
"importFailed": "Import failed.",
|
||||
"importSuccess": "Config imported (mode: {mode}).",
|
||||
"exportFailed": "Export failed.",
|
||||
"exportLoaded": "Current export loaded.",
|
||||
"exportJson": "Export JSON",
|
||||
"invalidJsonField": "{field} is not a valid JSON object.",
|
||||
"defaultPasswordWarning": "You are using the default admin password \"admin\". Please change it.",
|
||||
"vercelSyncHint": "Configuration changed. For Vercel deployments, sync manually in Vercel Sync and redeploy.",
|
||||
"autoFetchPaused": "Auto loading paused after {count} failures: {error}",
|
||||
"retryLoad": "Retry now"
|
||||
},
|
||||
"login": {
|
||||
"welcome": "Welcome back",
|
||||
"subtitle": "Enter your admin key to continue",
|
||||
"adminKeyLabel": "Admin key",
|
||||
"adminKeyPlaceholder": "Enter your admin key...",
|
||||
"rememberSession": "Remember this session",
|
||||
"signIn": "Sign in",
|
||||
"secureConnection": "Secure connection",
|
||||
"adminPortal": "DS2API admin portal",
|
||||
"signInFailed": "Sign-in failed.",
|
||||
"networkError": "Network error: {error}"
|
||||
},
|
||||
"vercel": {
|
||||
"tokenRequired": "Vercel access token is required.",
|
||||
"projectRequired": "Project ID is required.",
|
||||
"syncFailed": "Sync failed.",
|
||||
"networkError": "Network error.",
|
||||
"title": "Vercel Deployment",
|
||||
"description": "Sync the current keys and accounts directly to Vercel environment variables.",
|
||||
"tokenLabel": "Vercel Access Token",
|
||||
"getToken": "Get token",
|
||||
"tokenPlaceholderPreconfig": "Using preconfigured token",
|
||||
"tokenPlaceholder": "Enter Vercel access token",
|
||||
"projectIdLabel": "Project ID",
|
||||
"projectIdHint": "Find it in Project Settings → General.",
|
||||
"teamIdLabel": "Team ID",
|
||||
"optional": "optional",
|
||||
"syncing": "Syncing...",
|
||||
"syncRedeploy": "Sync & redeploy",
|
||||
"redeployHint": "This triggers a Vercel redeploy and usually takes 30–60 seconds.",
|
||||
"syncSucceeded": "Sync succeeded",
|
||||
"syncFailedLabel": "Sync failed",
|
||||
"openDeployment": "Open deployment",
|
||||
"statusSynced": "Synced",
|
||||
"statusNotSynced": "Not synced",
|
||||
"statusNeverSynced": "Never synced",
|
||||
"lastSyncTime": "Last sync: {time}",
|
||||
"pollPaused": "Status polling paused after {count} failures.",
|
||||
"manualRefresh": "Refresh manually",
|
||||
"howItWorks": "How it works",
|
||||
"steps": {
|
||||
"one": "The current configuration (keys and accounts) is exported as JSON.",
|
||||
"two": "The JSON is Base64-encoded for safe formatting.",
|
||||
"three": "Update the env var in Vercel:",
|
||||
"four": "Trigger a redeploy to apply the updated environment variables."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,295 +1,297 @@
|
||||
{
|
||||
"language": {
|
||||
"label": "语言",
|
||||
"english": "English",
|
||||
"chinese": "中文"
|
||||
},
|
||||
"nav": {
|
||||
"accounts": {
|
||||
"label": "账号管理",
|
||||
"desc": "管理 DeepSeek 账号池"
|
||||
},
|
||||
"test": {
|
||||
"label": "API 测试",
|
||||
"desc": "测试 API 连接与响应"
|
||||
},
|
||||
"import": {
|
||||
"label": "批量导入",
|
||||
"desc": "批量导入账号配置"
|
||||
},
|
||||
"vercel": {
|
||||
"label": "Vercel 同步",
|
||||
"desc": "同步配置到 Vercel"
|
||||
},
|
||||
"settings": {
|
||||
"label": "设置中心",
|
||||
"desc": "在线修改系统设置与配置"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"onlineAdminConsole": "在线管理面板",
|
||||
"systemStatus": "系统状态",
|
||||
"statusOnline": "在线",
|
||||
"accounts": "账号",
|
||||
"keys": "密钥",
|
||||
"signOut": "退出登录"
|
||||
},
|
||||
"auth": {
|
||||
"expired": "认证已过期,请重新登录",
|
||||
"checking": "正在检查登录状态..."
|
||||
},
|
||||
"errors": {
|
||||
"fetchConfig": "获取配置失败: {error}"
|
||||
},
|
||||
"actions": {
|
||||
"cancel": "取消",
|
||||
"add": "添加",
|
||||
"delete": "删除",
|
||||
"copy": "复制",
|
||||
"generate": "生成",
|
||||
"test": "测试",
|
||||
"testing": "正在测试...",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"messages": {
|
||||
"deleted": "删除成功",
|
||||
"deleteFailed": "删除失败",
|
||||
"failedToAdd": "添加失败",
|
||||
"networkError": "网络错误",
|
||||
"requestFailed": "请求失败",
|
||||
"generationStopped": "已停止生成",
|
||||
"invalidJson": "无效的 JSON 格式",
|
||||
"importFailed": "导入失败",
|
||||
"copyFailed": "复制失败"
|
||||
},
|
||||
"landing": {
|
||||
"adminConsole": "管理面板",
|
||||
"apiStatus": "API 状态",
|
||||
"features": {
|
||||
"compatibility": {
|
||||
"title": "全面兼容",
|
||||
"desc": "适配 OpenAI 与 Claude 格式"
|
||||
},
|
||||
"loadBalancing": {
|
||||
"title": "负载均衡",
|
||||
"desc": "智能轮询,稳定高效"
|
||||
},
|
||||
"reasoning": {
|
||||
"title": "深度思考",
|
||||
"desc": "支持推理过程输出"
|
||||
},
|
||||
"search": {
|
||||
"title": "联网搜索",
|
||||
"desc": "集成原生网页搜索能力"
|
||||
}
|
||||
}
|
||||
},
|
||||
"accountManager": {
|
||||
"addKeySuccess": "API 密钥添加成功",
|
||||
"addAccountSuccess": "账号添加成功",
|
||||
"requiredFields": "需要填写密码以及邮箱或手机号",
|
||||
"deleteKeyConfirm": "确定要删除此 API 密钥吗?",
|
||||
"deleteAccountConfirm": "确定要删除此账号吗?",
|
||||
"invalidIdentifier": "账号标识无效,无法执行操作",
|
||||
"testAllConfirm": "测试所有账号的 API 连通性?",
|
||||
"testAllCompleted": "完成:{success}/{total} 可用",
|
||||
"testFailed": "测试失败: {error}",
|
||||
"available": "可用",
|
||||
"inUse": "正在使用",
|
||||
"totalPool": "账号池总数",
|
||||
"accountsUnit": "个账号",
|
||||
"threadsUnit": "线程",
|
||||
"apiKeysTitle": "API 密钥",
|
||||
"apiKeysDesc": "管理 API 访问密钥池",
|
||||
"addKey": "添加密钥",
|
||||
"copied": "已复制",
|
||||
"copyKeyTitle": "复制密钥",
|
||||
"deleteKeyTitle": "删除密钥",
|
||||
"noApiKeys": "未找到 API 密钥",
|
||||
"accountsTitle": "DeepSeek 账号",
|
||||
"accountsDesc": "管理 DeepSeek 账号池",
|
||||
"testAll": "测试全部",
|
||||
"addAccount": "添加账号",
|
||||
"testingAllAccounts": "正在测试所有账号...",
|
||||
"sessionActive": "已建立会话",
|
||||
"reauthRequired": "需重新登录",
|
||||
"testStatusFailed": "上次测试失败",
|
||||
"noAccounts": "未找到任何账号",
|
||||
"modalAddKeyTitle": "添加 API 密钥",
|
||||
"newKeyLabel": "新密钥值",
|
||||
"newKeyPlaceholder": "输入自定义 API 密钥",
|
||||
"generate": "生成",
|
||||
"generateHint": "点击「生成」自动创建随机密钥",
|
||||
"addKeyLoading": "添加中...",
|
||||
"addKeyAction": "添加密钥",
|
||||
"modalAddAccountTitle": "添加 DeepSeek 账号",
|
||||
"emailOptional": "邮箱 (可选)",
|
||||
"mobileOptional": "手机号 (可选)",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "账号密码",
|
||||
"addAccountLoading": "添加中...",
|
||||
"addAccountAction": "添加账号",
|
||||
"pageInfo": "第 {current}/{total} 页,共 {count} 个账号"
|
||||
},
|
||||
"apiTester": {
|
||||
"defaultMessage": "你好,请用一句话介绍你自己。",
|
||||
"models": {
|
||||
"chat": "非思考模型",
|
||||
"reasoner": "思考模型",
|
||||
"chatSearch": "非思考模型 (带搜索)",
|
||||
"reasonerSearch": "思考模型 (带搜索)"
|
||||
},
|
||||
"missingApiKey": "请提供 API 密钥",
|
||||
"requestFailed": "请求失败",
|
||||
"networkError": "网络错误: {error}",
|
||||
"testSuccess": "{account}: 测试成功 ({time}ms)",
|
||||
"config": "配置",
|
||||
"modelLabel": "模型",
|
||||
"streamMode": "流式模式",
|
||||
"accountSelector": "选择账号",
|
||||
"autoRandom": "🤖 自动 / 随机",
|
||||
"apiKeyOptional": "API 密钥 (可选)",
|
||||
"apiKeyDefault": "默认: ...{suffix}",
|
||||
"apiKeyPlaceholder": "输入自定义密钥",
|
||||
"modeManaged": "当前使用托管 key 模式(会走账号池)。",
|
||||
"modeDirect": "当前使用直通 token 模式(需填写有效 DeepSeek token)。",
|
||||
"statusError": "错误",
|
||||
"reasoningTrace": "思维链过程",
|
||||
"generating": "正在生成响应...",
|
||||
"enterMessage": "输入消息...",
|
||||
"adminConsoleLabel": "DeepSeek 管理员界面"
|
||||
},
|
||||
"batchImport": {
|
||||
"templates": {
|
||||
"full": {
|
||||
"name": "全量配置模板",
|
||||
"desc": "包含密钥、账号及模型映射"
|
||||
},
|
||||
"emailOnly": {
|
||||
"name": "仅邮箱账号",
|
||||
"desc": "批量导入邮箱格式账号"
|
||||
},
|
||||
"mobileOnly": {
|
||||
"name": "仅手机号账号",
|
||||
"desc": "批量导入手机号格式账号"
|
||||
},
|
||||
"keysOnly": {
|
||||
"name": "仅 API 密钥",
|
||||
"desc": "仅添加 API 访问密钥"
|
||||
}
|
||||
},
|
||||
"enterJson": "请输入 JSON 配置内容",
|
||||
"importSuccess": "导入成功: {keys} 个密钥, {accounts} 个账号",
|
||||
"templateLoaded": "已加载模板: {name}",
|
||||
"currentConfigLoaded": "当前配置已加载",
|
||||
"fetchConfigFailed": "获取配置失败",
|
||||
"copySuccess": "Base64 配置已复制到剪贴板",
|
||||
"quickTemplates": "快速模板",
|
||||
"dataExport": "数据导出",
|
||||
"dataExportDesc": "获取配置的 Base64 字符串,用于 Vercel 环境变量。",
|
||||
"copyBase64": "复制 Base64 配置",
|
||||
"copied": "已复制",
|
||||
"variableName": "变量名",
|
||||
"jsonEditor": "JSON 编辑器",
|
||||
"loadCurrentConfig": "加载当前配置",
|
||||
"applyConfig": "应用配置",
|
||||
"importing": "正在导入...",
|
||||
"importComplete": "导入操作已完成",
|
||||
"importSummary": "成功导入了 {keys} 个 API 密钥,并更新了 {accounts} 个账号。"
|
||||
},
|
||||
"settings": {
|
||||
"loadFailed": "加载设置失败",
|
||||
"nonJsonResponse": "服务端返回了非 JSON 响应(状态码:{status})",
|
||||
"save": "保存设置",
|
||||
"saving": "保存中...",
|
||||
"saveSuccess": "设置已保存并热更新生效",
|
||||
"saveFailed": "保存设置失败",
|
||||
"securityTitle": "安全设置",
|
||||
"jwtExpireHours": "JWT 有效期(小时)",
|
||||
"newPassword": "面板新密码",
|
||||
"newPasswordPlaceholder": "输入新密码(至少 4 位)",
|
||||
"updatePassword": "修改密码",
|
||||
"updating": "更新中...",
|
||||
"passwordTooShort": "新密码至少 4 位",
|
||||
"passwordUpdated": "密码已更新,需重新登录",
|
||||
"passwordUpdateFailed": "密码更新失败",
|
||||
"runtimeTitle": "并发与队列",
|
||||
"accountMaxInflight": "每账号并发上限",
|
||||
"accountMaxQueue": "账号等待队列上限",
|
||||
"globalMaxInflight": "全局并发上限",
|
||||
"behaviorTitle": "行为设置",
|
||||
"toolcallMode": "Toolcall 模式",
|
||||
"earlyEmitConfidence": "早发置信度",
|
||||
"responsesTTL": "Responses 缓存 TTL(秒)",
|
||||
"embeddingsProvider": "Embeddings Provider",
|
||||
"modelTitle": "模型映射",
|
||||
"claudeMapping": "Claude 映射(JSON)",
|
||||
"modelAliases": "模型别名(JSON)",
|
||||
"backupTitle": "备份与恢复",
|
||||
"loadExport": "加载当前导出",
|
||||
"importModeMerge": "合并导入(默认)",
|
||||
"importModeReplace": "全量覆盖导入",
|
||||
"importNow": "立即导入",
|
||||
"importing": "导入中...",
|
||||
"importPlaceholder": "粘贴要导入的 JSON 配置",
|
||||
"importEmpty": "请先输入导入 JSON",
|
||||
"importInvalidJson": "导入 JSON 格式无效",
|
||||
"importFailed": "导入失败",
|
||||
"importSuccess": "配置导入成功(模式:{mode})",
|
||||
"exportFailed": "导出失败",
|
||||
"exportLoaded": "已加载当前配置导出",
|
||||
"exportJson": "导出 JSON",
|
||||
"invalidJsonField": "{field} 不是有效 JSON 对象",
|
||||
"defaultPasswordWarning": "当前使用默认密码 admin,请尽快在此修改。",
|
||||
"vercelSyncHint": "当前配置已更新。Vercel 部署请到 Vercel 同步页面手动同步并重部署。",
|
||||
"autoFetchPaused": "自动加载已暂停:连续失败 {count} 次({error})",
|
||||
"retryLoad": "立即重试"
|
||||
},
|
||||
"login": {
|
||||
"welcome": "欢迎回来",
|
||||
"subtitle": "请输入管理员密钥以继续",
|
||||
"adminKeyLabel": "管理员密钥",
|
||||
"adminKeyPlaceholder": "输入您的管理员密钥...",
|
||||
"rememberSession": "记住登录状态",
|
||||
"signIn": "登录",
|
||||
"secureConnection": "安全连接",
|
||||
"adminPortal": "DS2API 管理员门户",
|
||||
"signInFailed": "登录失败",
|
||||
"networkError": "网络错误: {error}"
|
||||
},
|
||||
"vercel": {
|
||||
"tokenRequired": "需要 Vercel 访问令牌",
|
||||
"projectRequired": "需要项目 ID",
|
||||
"syncFailed": "同步失败",
|
||||
"networkError": "网络错误",
|
||||
"title": "Vercel 部署",
|
||||
"description": "将当前密钥和账号配置直接同步到 Vercel 环境变量中。",
|
||||
"tokenLabel": "Vercel 访问令牌",
|
||||
"getToken": "获取令牌",
|
||||
"tokenPlaceholderPreconfig": "正在使用预配置的令牌",
|
||||
"tokenPlaceholder": "输入 Vercel 访问令牌",
|
||||
"projectIdLabel": "项目 ID",
|
||||
"projectIdHint": "可在项目设置 (Project Settings) → 常规 (General) 中找到",
|
||||
"teamIdLabel": "团队 ID",
|
||||
"optional": "可选",
|
||||
"syncing": "正在同步...",
|
||||
"syncRedeploy": "同步并重新部署",
|
||||
"redeployHint": "这将触发 Vercel 的重新部署,大约需要 30-60 秒。",
|
||||
"syncSucceeded": "同步成功",
|
||||
"syncFailedLabel": "同步失败",
|
||||
"openDeployment": "访问部署地址",
|
||||
"statusSynced": "已同步",
|
||||
"statusNotSynced": "未同步",
|
||||
"statusNeverSynced": "从未同步",
|
||||
"lastSyncTime": "上次同步: {time}",
|
||||
"pollPaused": "状态轮询已暂停:连续失败 {count} 次。",
|
||||
"manualRefresh": "手动刷新",
|
||||
"howItWorks": "工作原理",
|
||||
"steps": {
|
||||
"one": "当前配置 (密钥和账号) 被导出为 JSON 字符串。",
|
||||
"two": "JSON 被编码为 Base64 以确保格式兼容性。",
|
||||
"three": "更新 Vercel 项目中的环境变量:",
|
||||
"four": "触发重新部署以应用新的环境变量。"
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"language": {
|
||||
"label": "语言",
|
||||
"english": "English",
|
||||
"chinese": "中文"
|
||||
},
|
||||
"nav": {
|
||||
"accounts": {
|
||||
"label": "账号管理",
|
||||
"desc": "管理 DeepSeek 账号池"
|
||||
},
|
||||
"test": {
|
||||
"label": "API 测试",
|
||||
"desc": "测试 API 连接与响应"
|
||||
},
|
||||
"import": {
|
||||
"label": "批量导入",
|
||||
"desc": "批量导入账号配置"
|
||||
},
|
||||
"vercel": {
|
||||
"label": "Vercel 同步",
|
||||
"desc": "同步配置到 Vercel"
|
||||
},
|
||||
"settings": {
|
||||
"label": "设置中心",
|
||||
"desc": "在线修改系统设置与配置"
|
||||
}
|
||||
},
|
||||
"sidebar": {
|
||||
"onlineAdminConsole": "在线管理面板",
|
||||
"systemStatus": "系统状态",
|
||||
"statusOnline": "在线",
|
||||
"accounts": "账号",
|
||||
"keys": "密钥",
|
||||
"signOut": "退出登录"
|
||||
},
|
||||
"auth": {
|
||||
"expired": "认证已过期,请重新登录",
|
||||
"checking": "正在检查登录状态..."
|
||||
},
|
||||
"errors": {
|
||||
"fetchConfig": "获取配置失败: {error}"
|
||||
},
|
||||
"actions": {
|
||||
"cancel": "取消",
|
||||
"add": "添加",
|
||||
"delete": "删除",
|
||||
"copy": "复制",
|
||||
"generate": "生成",
|
||||
"test": "测试",
|
||||
"testing": "正在测试...",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"messages": {
|
||||
"deleted": "删除成功",
|
||||
"deleteFailed": "删除失败",
|
||||
"failedToAdd": "添加失败",
|
||||
"networkError": "网络错误",
|
||||
"requestFailed": "请求失败",
|
||||
"generationStopped": "已停止生成",
|
||||
"invalidJson": "无效的 JSON 格式",
|
||||
"importFailed": "导入失败",
|
||||
"copyFailed": "复制失败"
|
||||
},
|
||||
"landing": {
|
||||
"adminConsole": "管理面板",
|
||||
"apiStatus": "API 状态",
|
||||
"features": {
|
||||
"compatibility": {
|
||||
"title": "全面兼容",
|
||||
"desc": "适配 OpenAI 与 Claude 格式"
|
||||
},
|
||||
"loadBalancing": {
|
||||
"title": "负载均衡",
|
||||
"desc": "智能轮询,稳定高效"
|
||||
},
|
||||
"reasoning": {
|
||||
"title": "深度思考",
|
||||
"desc": "支持推理过程输出"
|
||||
},
|
||||
"search": {
|
||||
"title": "联网搜索",
|
||||
"desc": "集成原生网页搜索能力"
|
||||
}
|
||||
}
|
||||
},
|
||||
"accountManager": {
|
||||
"addKeySuccess": "API 密钥添加成功",
|
||||
"addAccountSuccess": "账号添加成功",
|
||||
"requiredFields": "需要填写密码以及邮箱或手机号",
|
||||
"deleteKeyConfirm": "确定要删除此 API 密钥吗?",
|
||||
"deleteAccountConfirm": "确定要删除此账号吗?",
|
||||
"invalidIdentifier": "账号标识无效,无法执行操作",
|
||||
"testAllConfirm": "测试所有账号的 API 连通性?",
|
||||
"testAllCompleted": "完成:{success}/{total} 可用",
|
||||
"testFailed": "测试失败: {error}",
|
||||
"available": "可用",
|
||||
"inUse": "正在使用",
|
||||
"totalPool": "账号池总数",
|
||||
"accountsUnit": "个账号",
|
||||
"threadsUnit": "线程",
|
||||
"apiKeysTitle": "API 密钥",
|
||||
"apiKeysDesc": "管理 API 访问密钥池",
|
||||
"addKey": "添加密钥",
|
||||
"copied": "已复制",
|
||||
"copyKeyTitle": "复制密钥",
|
||||
"deleteKeyTitle": "删除密钥",
|
||||
"noApiKeys": "未找到 API 密钥",
|
||||
"accountsTitle": "DeepSeek 账号",
|
||||
"accountsDesc": "管理 DeepSeek 账号池",
|
||||
"testAll": "测试全部",
|
||||
"addAccount": "添加账号",
|
||||
"testingAllAccounts": "正在测试所有账号...",
|
||||
"sessionActive": "已建立会话",
|
||||
"reauthRequired": "需重新登录",
|
||||
"testStatusFailed": "上次测试失败",
|
||||
"noAccounts": "未找到任何账号",
|
||||
"modalAddKeyTitle": "添加 API 密钥",
|
||||
"newKeyLabel": "新密钥值",
|
||||
"newKeyPlaceholder": "输入自定义 API 密钥",
|
||||
"generate": "生成",
|
||||
"generateHint": "点击「生成」自动创建随机密钥",
|
||||
"addKeyLoading": "添加中...",
|
||||
"addKeyAction": "添加密钥",
|
||||
"modalAddAccountTitle": "添加 DeepSeek 账号",
|
||||
"emailOptional": "邮箱 (可选)",
|
||||
"mobileOptional": "手机号 (可选)",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "账号密码",
|
||||
"addAccountLoading": "添加中...",
|
||||
"addAccountAction": "添加账号",
|
||||
"pageInfo": "第 {current}/{total} 页,共 {count} 个账号",
|
||||
"searchPlaceholder": "搜索账号...",
|
||||
"searchNoResults": "未找到匹配的账号"
|
||||
},
|
||||
"apiTester": {
|
||||
"defaultMessage": "你好,请用一句话介绍你自己。",
|
||||
"models": {
|
||||
"chat": "非思考模型",
|
||||
"reasoner": "思考模型",
|
||||
"chatSearch": "非思考模型 (带搜索)",
|
||||
"reasonerSearch": "思考模型 (带搜索)"
|
||||
},
|
||||
"missingApiKey": "请提供 API 密钥",
|
||||
"requestFailed": "请求失败",
|
||||
"networkError": "网络错误: {error}",
|
||||
"testSuccess": "{account}: 测试成功 ({time}ms)",
|
||||
"config": "配置",
|
||||
"modelLabel": "模型",
|
||||
"streamMode": "流式模式",
|
||||
"accountSelector": "选择账号",
|
||||
"autoRandom": "🤖 自动 / 随机",
|
||||
"apiKeyOptional": "API 密钥 (可选)",
|
||||
"apiKeyDefault": "默认: ...{suffix}",
|
||||
"apiKeyPlaceholder": "输入自定义密钥",
|
||||
"modeManaged": "当前使用托管 key 模式(会走账号池)。",
|
||||
"modeDirect": "当前使用直通 token 模式(需填写有效 DeepSeek token)。",
|
||||
"statusError": "错误",
|
||||
"reasoningTrace": "思维链过程",
|
||||
"generating": "正在生成响应...",
|
||||
"enterMessage": "输入消息...",
|
||||
"adminConsoleLabel": "DeepSeek 管理员界面"
|
||||
},
|
||||
"batchImport": {
|
||||
"templates": {
|
||||
"full": {
|
||||
"name": "全量配置模板",
|
||||
"desc": "包含密钥、账号及模型映射"
|
||||
},
|
||||
"emailOnly": {
|
||||
"name": "仅邮箱账号",
|
||||
"desc": "批量导入邮箱格式账号"
|
||||
},
|
||||
"mobileOnly": {
|
||||
"name": "仅手机号账号",
|
||||
"desc": "批量导入手机号格式账号"
|
||||
},
|
||||
"keysOnly": {
|
||||
"name": "仅 API 密钥",
|
||||
"desc": "仅添加 API 访问密钥"
|
||||
}
|
||||
},
|
||||
"enterJson": "请输入 JSON 配置内容",
|
||||
"importSuccess": "导入成功: {keys} 个密钥, {accounts} 个账号",
|
||||
"templateLoaded": "已加载模板: {name}",
|
||||
"currentConfigLoaded": "当前配置已加载",
|
||||
"fetchConfigFailed": "获取配置失败",
|
||||
"copySuccess": "Base64 配置已复制到剪贴板",
|
||||
"quickTemplates": "快速模板",
|
||||
"dataExport": "数据导出",
|
||||
"dataExportDesc": "获取配置的 Base64 字符串,用于 Vercel 环境变量。",
|
||||
"copyBase64": "复制 Base64 配置",
|
||||
"copied": "已复制",
|
||||
"variableName": "变量名",
|
||||
"jsonEditor": "JSON 编辑器",
|
||||
"loadCurrentConfig": "加载当前配置",
|
||||
"applyConfig": "应用配置",
|
||||
"importing": "正在导入...",
|
||||
"importComplete": "导入操作已完成",
|
||||
"importSummary": "成功导入了 {keys} 个 API 密钥,并更新了 {accounts} 个账号。"
|
||||
},
|
||||
"settings": {
|
||||
"loadFailed": "加载设置失败",
|
||||
"nonJsonResponse": "服务端返回了非 JSON 响应(状态码:{status})",
|
||||
"save": "保存设置",
|
||||
"saving": "保存中...",
|
||||
"saveSuccess": "设置已保存并热更新生效",
|
||||
"saveFailed": "保存设置失败",
|
||||
"securityTitle": "安全设置",
|
||||
"jwtExpireHours": "JWT 有效期(小时)",
|
||||
"newPassword": "面板新密码",
|
||||
"newPasswordPlaceholder": "输入新密码(至少 4 位)",
|
||||
"updatePassword": "修改密码",
|
||||
"updating": "更新中...",
|
||||
"passwordTooShort": "新密码至少 4 位",
|
||||
"passwordUpdated": "密码已更新,需重新登录",
|
||||
"passwordUpdateFailed": "密码更新失败",
|
||||
"runtimeTitle": "并发与队列",
|
||||
"accountMaxInflight": "每账号并发上限",
|
||||
"accountMaxQueue": "账号等待队列上限",
|
||||
"globalMaxInflight": "全局并发上限",
|
||||
"behaviorTitle": "行为设置",
|
||||
"toolcallMode": "Toolcall 模式",
|
||||
"earlyEmitConfidence": "早发置信度",
|
||||
"responsesTTL": "Responses 缓存 TTL(秒)",
|
||||
"embeddingsProvider": "Embeddings Provider",
|
||||
"modelTitle": "模型映射",
|
||||
"claudeMapping": "Claude 映射(JSON)",
|
||||
"modelAliases": "模型别名(JSON)",
|
||||
"backupTitle": "备份与恢复",
|
||||
"loadExport": "加载当前导出",
|
||||
"importModeMerge": "合并导入(默认)",
|
||||
"importModeReplace": "全量覆盖导入",
|
||||
"importNow": "立即导入",
|
||||
"importing": "导入中...",
|
||||
"importPlaceholder": "粘贴要导入的 JSON 配置",
|
||||
"importEmpty": "请先输入导入 JSON",
|
||||
"importInvalidJson": "导入 JSON 格式无效",
|
||||
"importFailed": "导入失败",
|
||||
"importSuccess": "配置导入成功(模式:{mode})",
|
||||
"exportFailed": "导出失败",
|
||||
"exportLoaded": "已加载当前配置导出",
|
||||
"exportJson": "导出 JSON",
|
||||
"invalidJsonField": "{field} 不是有效 JSON 对象",
|
||||
"defaultPasswordWarning": "当前使用默认密码 admin,请尽快在此修改。",
|
||||
"vercelSyncHint": "当前配置已更新。Vercel 部署请到 Vercel 同步页面手动同步并重部署。",
|
||||
"autoFetchPaused": "自动加载已暂停:连续失败 {count} 次({error})",
|
||||
"retryLoad": "立即重试"
|
||||
},
|
||||
"login": {
|
||||
"welcome": "欢迎回来",
|
||||
"subtitle": "请输入管理员密钥以继续",
|
||||
"adminKeyLabel": "管理员密钥",
|
||||
"adminKeyPlaceholder": "输入您的管理员密钥...",
|
||||
"rememberSession": "记住登录状态",
|
||||
"signIn": "登录",
|
||||
"secureConnection": "安全连接",
|
||||
"adminPortal": "DS2API 管理员门户",
|
||||
"signInFailed": "登录失败",
|
||||
"networkError": "网络错误: {error}"
|
||||
},
|
||||
"vercel": {
|
||||
"tokenRequired": "需要 Vercel 访问令牌",
|
||||
"projectRequired": "需要项目 ID",
|
||||
"syncFailed": "同步失败",
|
||||
"networkError": "网络错误",
|
||||
"title": "Vercel 部署",
|
||||
"description": "将当前密钥和账号配置直接同步到 Vercel 环境变量中。",
|
||||
"tokenLabel": "Vercel 访问令牌",
|
||||
"getToken": "获取令牌",
|
||||
"tokenPlaceholderPreconfig": "正在使用预配置的令牌",
|
||||
"tokenPlaceholder": "输入 Vercel 访问令牌",
|
||||
"projectIdLabel": "项目 ID",
|
||||
"projectIdHint": "可在项目设置 (Project Settings) → 常规 (General) 中找到",
|
||||
"teamIdLabel": "团队 ID",
|
||||
"optional": "可选",
|
||||
"syncing": "正在同步...",
|
||||
"syncRedeploy": "同步并重新部署",
|
||||
"redeployHint": "这将触发 Vercel 的重新部署,大约需要 30-60 秒。",
|
||||
"syncSucceeded": "同步成功",
|
||||
"syncFailedLabel": "同步失败",
|
||||
"openDeployment": "访问部署地址",
|
||||
"statusSynced": "已同步",
|
||||
"statusNotSynced": "未同步",
|
||||
"statusNeverSynced": "从未同步",
|
||||
"lastSyncTime": "上次同步: {time}",
|
||||
"pollPaused": "状态轮询已暂停:连续失败 {count} 次。",
|
||||
"manualRefresh": "手动刷新",
|
||||
"howItWorks": "工作原理",
|
||||
"steps": {
|
||||
"one": "当前配置 (密钥和账号) 被导出为 JSON 字符串。",
|
||||
"two": "JSON 被编码为 Base64 以确保格式兼容性。",
|
||||
"three": "更新 Vercel 项目中的环境变量:",
|
||||
"four": "触发重新部署以应用新的环境变量。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user