feat(proxy): add proxy IP management and account routing

Add admin CRUD and connectivity checks for SOCKS5/SOCKS5H proxy nodes.

Allow accounts to bind to a proxy, route DeepSeek requests through the selected node, and expose proxy management in the admin UI.
This commit is contained in:
Jason.li
2026-04-07 02:05:25 +08:00
parent 1c95942e5d
commit 8ae2ea10c8
30 changed files with 1675 additions and 51 deletions

View File

@@ -46,7 +46,7 @@ export default function AppRoutes() {
{!isProduction && (
<Route path="/" element={<LandingPage onEnter={() => navigate('/admin')} />} />
)}
<Route path={isProduction ? "/" : "/admin"} element={
<Route path={isProduction ? "/*" : "/admin/*"} element={
token ? (
<DashboardShell
token={token}

View File

@@ -45,6 +45,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
batchProgress,
sessionCounts,
deletingSessions,
updatingProxy,
addKey,
deleteKey,
addAccount,
@@ -52,6 +53,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
testAccount,
testAllAccounts,
deleteAllSessions,
updateAccountProxy,
} = useAccountActions({
apiFetch,
t,
@@ -107,16 +109,19 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
batchProgress={batchProgress}
sessionCounts={sessionCounts}
deletingSessions={deletingSessions}
updatingProxy={updatingProxy}
totalAccounts={totalAccounts}
page={page}
pageSize={pageSize}
totalPages={totalPages}
resolveAccountIdentifier={resolveAccountIdentifier}
proxies={config?.proxies || []}
onTestAll={testAllAccounts}
onShowAddAccount={() => setShowAddAccount(true)}
onTestAccount={testAccount}
onDeleteAccount={deleteAccount}
onDeleteAllSessions={deleteAllSessions}
onUpdateAccountProxy={updateAccountProxy}
onPrevPage={() => fetchAccounts(page - 1)}
onNextPage={() => fetchAccounts(page + 1)}
onPageSizeChange={changePageSize}

View File

@@ -11,16 +11,19 @@ export default function AccountsTable({
batchProgress,
sessionCounts,
deletingSessions,
updatingProxy,
totalAccounts,
page,
pageSize,
totalPages,
resolveAccountIdentifier,
proxies,
onTestAll,
onShowAddAccount,
onTestAccount,
onDeleteAccount,
onDeleteAllSessions,
onUpdateAccountProxy,
onPrevPage,
onNextPage,
onPageSizeChange,
@@ -102,6 +105,7 @@ export default function AccountsTable({
) : accounts.length > 0 ? (
accounts.map((acc, i) => {
const id = resolveAccountIdentifier(acc)
const assignedProxy = proxies.find(proxy => proxy.id === acc.proxy_id)
const runtimeUnknown = envBacked && !acc.test_status
const isActive = acc.test_status === 'ok' || acc.has_token
return (
@@ -150,10 +154,28 @@ export default function AccountsTable({
)}
</button>
)}
{acc.proxy_id && (
<span className="font-mono bg-amber-500/10 text-amber-500 px-1.5 py-0.5 rounded text-[10px]">
{t('accountManager.proxyBadge', { name: assignedProxy ? (assignedProxy.name || `${assignedProxy.host}:${assignedProxy.port}`) : acc.proxy_id })}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2 self-start lg:self-auto ml-5 lg:ml-0">
<select
value={acc.proxy_id || ''}
onChange={e => onUpdateAccountProxy(id, e.target.value)}
disabled={updatingProxy?.[id]}
className="max-w-[180px] px-2.5 py-1.5 text-[10px] lg:text-xs bg-secondary border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
>
<option value="">{t('accountManager.proxyNone')}</option>
{proxies.map(proxy => (
<option key={proxy.id} value={proxy.id}>
{proxy.name || `${proxy.host}:${proxy.port}`}
</option>
))}
</select>
<button
onClick={() => onTestAccount(id)}
disabled={testing[id]}

View File

@@ -12,6 +12,7 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] })
const [sessionCounts, setSessionCounts] = useState({})
const [deletingSessions, setDeletingSessions] = useState({})
const [updatingProxy, setUpdatingProxy] = useState({})
const addKey = async () => {
if (!newKey.trim()) return
@@ -213,6 +214,34 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
}
}
const updateAccountProxy = async (identifier, proxyID) => {
const accountID = String(identifier || '').trim()
if (!accountID) {
onMessage('error', t('accountManager.invalidIdentifier'))
return
}
setUpdatingProxy(prev => ({ ...prev, [accountID]: true }))
try {
const res = await apiFetch(`/admin/accounts/${encodeURIComponent(accountID)}/proxy`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proxy_id: proxyID || '' }),
})
const data = await res.json()
if (!res.ok) {
onMessage('error', data.detail || t('messages.requestFailed'))
return
}
onMessage('success', t('accountManager.proxyUpdateSuccess'))
fetchAccounts()
onRefresh()
} catch (_err) {
onMessage('error', t('messages.networkError'))
} finally {
setUpdatingProxy(prev => ({ ...prev, [accountID]: false }))
}
}
return {
showAddKey,
setShowAddKey,
@@ -230,6 +259,7 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
batchProgress,
sessionCounts,
deletingSessions,
updatingProxy,
addKey,
deleteKey,
addAccount,
@@ -237,5 +267,6 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
testAccount,
testAllAccounts,
deleteAllSessions,
updateAccountProxy,
}
}

View File

@@ -0,0 +1,464 @@
import { useState } from 'react'
import { Pencil, Play, Plus, Shield, Trash2, X } from 'lucide-react'
import clsx from 'clsx'
import { useI18n } from '../../i18n'
async function readApiResponse(res, nonJsonMessage) {
const contentType = String(res.headers.get('content-type') || '').toLowerCase()
const raw = await res.text()
const trimmed = raw.trim()
if (!trimmed) {
return {}
}
if (contentType.includes('application/json')) {
try {
return JSON.parse(trimmed)
} catch (_err) {
if (!res.ok) {
return { detail: trimmed }
}
throw new Error(nonJsonMessage)
}
}
if (!res.ok) {
return { detail: trimmed }
}
throw new Error(nonJsonMessage)
}
const EMPTY_FORM = {
name: '',
type: 'socks5h',
host: '',
port: 1080,
username: '',
password: '',
}
function createEmptyProxyForm() {
return { ...EMPTY_FORM }
}
function ProxyStatusBadge({ t, result, testing = false }) {
if (testing) {
return (
<span className="inline-flex items-center rounded-full border border-border bg-muted/40 px-2 py-1 text-[10px] font-medium text-muted-foreground">
{t('proxyManager.testing')}
</span>
)
}
if (!result) {
return (
<span className="inline-flex items-center rounded-full border border-border bg-muted/20 px-2 py-1 text-[10px] font-medium text-muted-foreground">
{t('proxyManager.untested')}
</span>
)
}
return (
<span
className={clsx(
'inline-flex items-center rounded-full border px-2 py-1 text-[10px] font-medium',
result.success
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-500'
: 'border-destructive/20 bg-destructive/10 text-destructive'
)}
>
{result.success
? t('proxyManager.testSuccessShort', { time: result.response_time ?? 0 })
: t('proxyManager.testFailedShort')}
</span>
)
}
function ProxiesTable({
t,
proxies,
testing,
testResults,
onCreate,
onTest,
onEdit,
onDelete,
}) {
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('proxyManager.title')}</h2>
<p className="text-sm text-muted-foreground">{t('proxyManager.desc')}</p>
</div>
<button
onClick={onCreate}
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('proxyManager.addProxy')}
</button>
</div>
{proxies.length === 0 ? (
<div className="p-10 text-center text-muted-foreground">{t('proxyManager.noProxies')}</div>
) : (
<div className="divide-y divide-border">
{proxies.map((proxy) => {
const result = testResults[proxy.id]
return (
<div key={proxy.id} className="p-4 md:p-5 flex flex-col lg:flex-row lg:items-center justify-between gap-4 hover:bg-muted/40 transition-colors">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<div className="font-medium text-foreground">{proxy.name || `${proxy.host}:${proxy.port}`}</div>
<span className="inline-flex items-center rounded-full border border-primary/20 bg-primary/10 px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-primary">
{proxy.type}
</span>
{proxy.username && (
<span className="inline-flex items-center gap-1 rounded-full border border-border bg-muted/20 px-2 py-1 text-[10px] font-medium text-muted-foreground">
<Shield className="w-3 h-3" />
{proxy.username}
</span>
)}
<ProxyStatusBadge t={t} result={result} testing={testing[proxy.id]} />
</div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="font-mono bg-muted/30 px-2 py-1 rounded border border-border">
{proxy.host}:{proxy.port}
</span>
{proxy.has_password && (
<span className="rounded-full border border-border bg-muted/20 px-2 py-1 text-[10px]">
{t('proxyManager.authEnabled')}
</span>
)}
{result?.message && (
<span className="truncate max-w-full">{result.message}</span>
)}
</div>
</div>
<div className="flex items-center gap-2 self-start lg:self-auto">
<button
onClick={() => onTest(proxy)}
disabled={testing[proxy.id]}
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-md border border-border hover:bg-secondary transition-colors text-xs font-medium disabled:opacity-50"
>
<Play className="w-3.5 h-3.5" />
{t('proxyManager.testAction')}
</button>
<button
onClick={() => onEdit(proxy)}
className="p-2 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-md transition-colors"
title={t('proxyManager.editProxy')}
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => onDelete(proxy)}
className="p-2 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
title={t('proxyManager.deleteProxy')}
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
)
})}
</div>
)}
</div>
)
}
function ProxyFormModal({
show,
t,
form,
setForm,
editingProxy,
loading,
onClose,
onSubmit,
}) {
if (!show) {
return null
}
const isEditing = Boolean(editingProxy?.id)
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
<div className="bg-card w-full max-w-lg rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
<div className="p-4 border-b border-border flex justify-between items-center">
<div>
<h3 className="font-semibold">
{isEditing ? t('proxyManager.modalEditTitle') : t('proxyManager.modalAddTitle')}
</h3>
<p className="text-xs text-muted-foreground mt-1">
{t('proxyManager.modalDesc')}
</p>
</div>
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
<X className="w-5 h-5" />
</button>
</div>
<div className="p-6 space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1.5">{t('proxyManager.nameLabel')}</label>
<input
type="text"
className="input-field"
placeholder={t('proxyManager.namePlaceholder')}
value={form.name}
onChange={e => setForm({ ...form, name: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">{t('proxyManager.typeLabel')}</label>
<select
className="input-field"
value={form.type}
onChange={e => setForm({ ...form, type: e.target.value })}
>
<option value="socks5">socks5</option>
<option value="socks5h">socks5h</option>
</select>
</div>
</div>
<div className="grid md:grid-cols-[1fr_128px] gap-4">
<div>
<label className="block text-sm font-medium mb-1.5">{t('proxyManager.hostLabel')}</label>
<input
type="text"
className="input-field"
placeholder={t('proxyManager.hostPlaceholder')}
value={form.host}
onChange={e => setForm({ ...form, host: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">{t('proxyManager.portLabel')}</label>
<input
type="number"
min="1"
max="65535"
className="input-field"
value={form.port}
onChange={e => setForm({ ...form, port: Number(e.target.value) || '' })}
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1.5">{t('proxyManager.usernameLabel')}</label>
<input
type="text"
className="input-field"
placeholder={t('proxyManager.usernamePlaceholder')}
value={form.username}
onChange={e => setForm({ ...form, username: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">{t('proxyManager.passwordLabel')}</label>
<input
type="password"
className="input-field bg-[#09090b]"
placeholder={t('proxyManager.passwordPlaceholder')}
value={form.password}
onChange={e => setForm({ ...form, password: e.target.value })}
/>
{isEditing && (
<p className="mt-1 text-[11px] text-muted-foreground">{t('proxyManager.passwordKeepHint')}</p>
)}
</div>
</div>
<div className="rounded-lg border border-border bg-muted/20 px-3 py-2 text-xs text-muted-foreground">
{t('proxyManager.typeHelp')}
</div>
<div className="flex justify-end gap-2 pt-2">
<button
onClick={onClose}
className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium"
>
{t('actions.cancel')}
</button>
<button
onClick={onSubmit}
disabled={loading}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50"
>
{loading
? t('proxyManager.saving')
: (isEditing ? t('proxyManager.saveEdit') : t('proxyManager.saveAdd'))}
</button>
</div>
</div>
</div>
</div>
)
}
export default function ProxyManagerContainer({ config, onRefresh, onMessage, authFetch }) {
const { t } = useI18n()
const apiFetch = authFetch || fetch
const [showModal, setShowModal] = useState(false)
const [editingProxy, setEditingProxy] = useState(null)
const [form, setForm] = useState(createEmptyProxyForm())
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState({})
const [testResults, setTestResults] = useState({})
const proxies = config?.proxies || []
const openCreate = () => {
setEditingProxy(null)
setForm(createEmptyProxyForm())
setShowModal(true)
}
const openEdit = (proxy) => {
setEditingProxy(proxy)
setForm({
name: proxy.name || '',
type: proxy.type || 'socks5h',
host: proxy.host || '',
port: proxy.port || 1080,
username: proxy.username || '',
password: '',
})
setShowModal(true)
}
const closeModal = () => {
setShowModal(false)
setEditingProxy(null)
setForm(createEmptyProxyForm())
}
const saveProxy = async () => {
if (!form.host || !form.port) {
onMessage('error', t('proxyManager.requiredFields'))
return
}
setSaving(true)
try {
const url = editingProxy?.id
? `/admin/proxies/${encodeURIComponent(editingProxy.id)}`
: '/admin/proxies'
const method = editingProxy?.id ? 'PUT' : 'POST'
const res = await apiFetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.name,
type: form.type,
host: form.host,
port: Number(form.port),
username: form.username,
password: form.password,
}),
})
const data = await readApiResponse(res, t('settings.nonJsonResponse', { status: res.status }))
if (!res.ok) {
onMessage('error', data.detail || t('messages.requestFailed'))
return
}
await onRefresh?.()
onMessage('success', editingProxy?.id ? t('proxyManager.updateSuccess') : t('proxyManager.addSuccess'))
closeModal()
} catch (err) {
onMessage('error', err?.message || t('messages.networkError'))
} finally {
setSaving(false)
}
}
const deleteProxy = async (proxy) => {
if (!confirm(t('proxyManager.deleteConfirm', { name: proxy.name || `${proxy.host}:${proxy.port}` }))) return
try {
const res = await apiFetch(`/admin/proxies/${encodeURIComponent(proxy.id)}`, { method: 'DELETE' })
const data = await readApiResponse(res, t('settings.nonJsonResponse', { status: res.status }))
if (!res.ok) {
onMessage('error', data.detail || t('messages.deleteFailed'))
return
}
await onRefresh?.()
onMessage('success', t('messages.deleted'))
setTestResults(prev => {
const next = { ...prev }
delete next[proxy.id]
return next
})
} catch (err) {
onMessage('error', err?.message || t('messages.networkError'))
}
}
const testProxy = async (proxy) => {
setTesting(prev => ({ ...prev, [proxy.id]: true }))
try {
const res = await apiFetch('/admin/proxies/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proxy_id: proxy.id }),
})
const data = await readApiResponse(res, t('settings.nonJsonResponse', { status: res.status }))
setTestResults(prev => ({ ...prev, [proxy.id]: data }))
onMessage(data.success ? 'success' : 'error', data.message || t('messages.requestFailed'))
} catch (err) {
onMessage('error', err?.message || t('messages.networkError'))
} finally {
setTesting(prev => ({ ...prev, [proxy.id]: false }))
}
}
return (
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-3">
<div className="bg-card border border-border rounded-xl p-5 shadow-sm">
<div className="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">{t('proxyManager.totalProxies')}</div>
<div className="mt-2 text-2xl font-bold">{proxies.length}</div>
</div>
<div className="bg-card border border-border rounded-xl p-5 shadow-sm">
<div className="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">{t('proxyManager.socks5hCount')}</div>
<div className="mt-2 text-2xl font-bold">{proxies.filter(proxy => proxy.type === 'socks5h').length}</div>
</div>
<div className="bg-card border border-border rounded-xl p-5 shadow-sm">
<div className="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">{t('proxyManager.authProxyCount')}</div>
<div className="mt-2 text-2xl font-bold">{proxies.filter(proxy => proxy.username || proxy.has_password).length}</div>
</div>
</div>
<ProxiesTable
t={t}
proxies={proxies}
testing={testing}
testResults={testResults}
onCreate={openCreate}
onTest={testProxy}
onEdit={openEdit}
onDelete={deleteProxy}
/>
<ProxyFormModal
show={showModal}
t={t}
form={form}
setForm={setForm}
editingProxy={editingProxy}
loading={saving}
onClose={closeModal}
onSubmit={saveProxy}
/>
</div>
)
}

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import {
LayoutDashboard,
Upload,
@@ -8,7 +9,8 @@ import {
Menu,
X,
Server,
Users
Users,
Globe
} from 'lucide-react'
import clsx from 'clsx'
@@ -17,22 +19,40 @@ import ApiTesterContainer from '../features/apiTester/ApiTesterContainer'
import BatchImport from '../components/BatchImport'
import VercelSyncContainer from '../features/vercel/VercelSyncContainer'
import SettingsContainer from '../features/settings/SettingsContainer'
import ProxyManagerContainer from '../features/proxy/ProxyManagerContainer'
import LanguageToggle from '../components/LanguageToggle'
import { useI18n } from '../i18n'
export default function DashboardShell({ token, onLogout, config, fetchConfig, showMessage, message, onForceLogout, isVercel }) {
const { t } = useI18n()
const [activeTab, setActiveTab] = useState('accounts')
const location = useLocation()
const navigate = useNavigate()
const [sidebarOpen, setSidebarOpen] = useState(false)
const navItems = [
{ id: 'accounts', label: t('nav.accounts.label'), icon: Users, description: t('nav.accounts.desc') },
{ id: 'proxies', label: t('nav.proxies.label'), icon: Globe, description: t('nav.proxies.desc') },
{ id: 'test', label: t('nav.test.label'), icon: Server, description: t('nav.test.desc') },
{ id: 'import', label: t('nav.import.label'), icon: Upload, description: t('nav.import.desc') },
{ id: 'vercel', label: t('nav.vercel.label'), icon: Cloud, description: t('nav.vercel.desc') },
{ id: 'settings', label: t('nav.settings.label'), icon: SettingsIcon, description: t('nav.settings.desc') },
]
const tabIds = new Set(navItems.map(item => item.id))
const pathSegments = location.pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean)
const routeSegments = pathSegments[0] === 'admin' ? pathSegments.slice(1) : pathSegments
const pathTab = routeSegments[0] || ''
const activeTab = tabIds.has(pathTab) ? pathTab : 'accounts'
const adminBasePath = pathSegments[0] === 'admin' ? '/admin' : ''
const navigateToTab = useCallback((tabID) => {
const nextPath = tabID === 'accounts'
? `${adminBasePath || ''}/`
: `${adminBasePath}/${tabID}`
navigate(nextPath)
setSidebarOpen(false)
}, [adminBasePath, navigate])
const authFetch = useCallback(async (url, options = {}) => {
const headers = {
...options.headers,
@@ -74,6 +94,8 @@ export default function DashboardShell({ token, onLogout, config, fetchConfig, s
switch (activeTab) {
case 'accounts':
return <AccountManagerContainer config={config} onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} />
case 'proxies':
return <ProxyManagerContainer config={config} onRefresh={fetchConfig} onMessage={showMessage} authFetch={authFetch} />
case 'test':
return <ApiTesterContainer config={config} onMessage={showMessage} authFetch={authFetch} />
case 'import':
@@ -121,8 +143,7 @@ export default function DashboardShell({ token, onLogout, config, fetchConfig, s
<button
key={item.id}
onClick={() => {
setActiveTab(item.id)
setSidebarOpen(false)
navigateToTab(item.id)
}}
className={clsx(
"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 group border",

View File

@@ -9,6 +9,10 @@
"label": "Account Management",
"desc": "Manage the DeepSeek account pool"
},
"proxies": {
"label": "Proxy IPs",
"desc": "Manage outbound proxy nodes for accounts"
},
"test": {
"label": "API Test",
"desc": "Test API connectivity and responses"
@@ -140,12 +144,55 @@
"deleteAllSessions": "Delete all sessions",
"deleteAllSessionsConfirm": "Are you sure you want to delete all sessions for this account? This action cannot be undone.",
"deleteAllSessionsSuccess": "Successfully deleted all sessions",
"accountProxyLabel": "Account proxy",
"proxyNone": "Direct connection",
"proxyBadge": "Proxy: {name}",
"proxyUpdateSuccess": "Account proxy updated.",
"envModeRiskTitle": "Environment-variable config mode detected (persistence risk)",
"envModeRiskDesc": "Detected DS2API_CONFIG_JSON. If DS2API_ENV_WRITEBACK is not enabled, Admin UI edits are in-memory only and may be lost after restart.",
"envModeWritebackPendingTitle": "Env mode + auto-persistence enabled (pending file handoff)",
"envModeWritebackActiveTitle": "Env mode + auto-persistence active",
"envModeWritebackDesc": "The app will auto-create/write the config file and transition to file-backed mode. Current persistence path: {path}"
},
"proxyManager": {
"title": "Proxy IPs",
"desc": "Manage SOCKS egress nodes for accounts and test outbound connectivity to DeepSeek.",
"addProxy": "Add proxy",
"editProxy": "Edit proxy",
"deleteProxy": "Delete proxy",
"modalAddTitle": "Add proxy node",
"modalEditTitle": "Edit proxy node",
"modalDesc": "Supports socks5 and socks5h. Accounts will use the bound node as their outbound route.",
"nameLabel": "Proxy name",
"namePlaceholder": "Example: Hong Kong Exit A",
"typeLabel": "Proxy type",
"hostLabel": "Proxy host",
"hostPlaceholder": "127.0.0.1 or proxy hostname",
"portLabel": "Port",
"usernameLabel": "Username (optional)",
"usernamePlaceholder": "Proxy auth username",
"passwordLabel": "Password (optional)",
"passwordPlaceholder": "Proxy auth password",
"passwordKeepHint": "Leave blank to keep the currently stored password.",
"typeHelp": "socks5 resolves the target hostname locally before dialing through the proxy; socks5h forwards the hostname to the proxy for remote DNS resolution.",
"requiredFields": "Host and port are required.",
"saving": "Saving...",
"testing": "Testing",
"testAction": "Check proxy",
"untested": "Untested",
"saveAdd": "Add proxy",
"saveEdit": "Save changes",
"addSuccess": "Proxy added successfully.",
"updateSuccess": "Proxy updated successfully.",
"deleteConfirm": "Delete proxy {name}? Accounts bound to it will fall back to direct connection.",
"noProxies": "No proxy nodes yet.",
"authEnabled": "Auth enabled",
"testSuccessShort": "Reachable {time}ms",
"testFailedShort": "Test failed",
"totalProxies": "Total proxies",
"socks5hCount": "socks5h nodes",
"authProxyCount": "Authenticated nodes"
},
"apiTester": {
"defaultMessage": "Hello, please introduce yourself in one sentence.",
"models": {
@@ -325,4 +372,4 @@
"four": "Trigger a redeploy to apply the updated environment variables."
}
}
}
}

View File

@@ -9,6 +9,10 @@
"label": "账号管理",
"desc": "管理 DeepSeek 账号池"
},
"proxies": {
"label": "代理 IP",
"desc": "管理账号可用的代理出口"
},
"test": {
"label": "API 测试",
"desc": "测试 API 连接与响应"
@@ -140,12 +144,55 @@
"deleteAllSessions": "删除所有会话",
"deleteAllSessionsConfirm": "确定要删除该账号的所有会话吗?此操作不可恢复。",
"deleteAllSessionsSuccess": "删除成功",
"accountProxyLabel": "账号代理",
"proxyNone": "不走代理",
"proxyBadge": "代理: {name}",
"proxyUpdateSuccess": "账号代理已更新",
"envModeRiskTitle": "当前为环境变量配置模式(有持久化风险)",
"envModeRiskDesc": "检测到 DS2API_CONFIG_JSON。若未开启 DS2API_ENV_WRITEBACK管理台改动仅在内存生效重启可能丢失。",
"envModeWritebackPendingTitle": "环境变量模式 + 自动持久化已开启(等待落盘)",
"envModeWritebackActiveTitle": "环境变量模式 + 自动持久化已生效",
"envModeWritebackDesc": "程序会自动创建/写入配置文件并在后续切换为文件模式。当前持久化路径:{path}"
},
"proxyManager": {
"title": "代理 IP",
"desc": "维护账号可选的 SOCKS 代理节点,并测试到 DeepSeek 的出站连通性。",
"addProxy": "添加代理",
"editProxy": "编辑代理",
"deleteProxy": "删除代理",
"modalAddTitle": "添加代理节点",
"modalEditTitle": "编辑代理节点",
"modalDesc": "支持 socks5 与 socks5h账号侧会按绑定结果选择出口。",
"nameLabel": "代理名称",
"namePlaceholder": "例如:香港出口 A",
"typeLabel": "代理类型",
"hostLabel": "代理主机",
"hostPlaceholder": "127.0.0.1 或代理域名",
"portLabel": "端口",
"usernameLabel": "用户名(可选)",
"usernamePlaceholder": "代理认证用户名",
"passwordLabel": "密码(可选)",
"passwordPlaceholder": "代理认证密码",
"passwordKeepHint": "留空表示保留当前已保存的密码。",
"typeHelp": "socks5 会先在本地解析目标域名再交给代理拨号socks5h 会把域名直接交给代理远端解析。",
"requiredFields": "至少需要填写主机和端口。",
"saving": "保存中...",
"testing": "测试中",
"testAction": "检查代理",
"untested": "未测试",
"saveAdd": "添加代理",
"saveEdit": "保存修改",
"addSuccess": "代理添加成功",
"updateSuccess": "代理更新成功",
"deleteConfirm": "确定要删除代理 {name} 吗?绑定到该代理的账号会自动切回直连。",
"noProxies": "还没有任何代理节点。",
"authEnabled": "已启用认证",
"testSuccessShort": "已连通 {time}ms",
"testFailedShort": "测试失败",
"totalProxies": "代理总数",
"socks5hCount": "socks5h 节点",
"authProxyCount": "带认证节点"
},
"apiTester": {
"defaultMessage": "你好,请用一句话介绍你自己。",
"models": {
@@ -325,4 +372,4 @@
"four": "触发重新部署以应用新的环境变量。"
}
}
}
}