feat(account): add key/account name and remark metadata

This commit is contained in:
CJACK.
2026-04-22 01:43:20 +08:00
parent d848d24a82
commit 8ff923cd77
16 changed files with 291 additions and 31 deletions

View File

@@ -271,8 +271,17 @@ go run ./cmd/ds2api
```json
{
"keys": ["your-api-key-1", "your-api-key-2"],
"api_keys": [
{
"key": "your-api-key-1",
"name": "主 Key",
"remark": "生产流量"
}
],
"accounts": [
{
"name": "账号 A",
"remark": "主账号",
"email": "user@example.com",
"password": "your-password"
},
@@ -320,7 +329,8 @@ go run ./cmd/ds2api
```
- `keys`API 访问密钥列表,客户端通过 `Authorization: Bearer <key>` 鉴权
- `accounts`DeepSeek 账号列表,支持 `email` `mobile` 登录
- `api_keys`:推荐使用的新结构化密钥列表,支持 `key` + `name` + `remark``keys` 仍兼容)
- `accounts`DeepSeek 账号列表,支持 `email` 或 `mobile` 登录;可额外填写 `name` / `remark` 便于管理
- `token`:配置文件中即使填写也会在加载时被清空(不会从 `config.json` 读取 token实际 token 仅在运行时内存中维护并自动刷新
- `model_aliases`:常见模型名(如 GPT/Codex/Claude到 DeepSeek 模型的映射
- `compat.wide_input_strict_output`:建议保持 `true`(当前实现默认宽进严出)
@@ -335,6 +345,8 @@ go run ./cmd/ds2api
### 环境变量
> 建议:长期维护请优先以 `config.json`(或其 Base64为单一配置源。环境变量仅保留部署必需项`DS2API_CONFIG_JSON` 主要用于 Vercel/无持久盘场景,后续可能进一步收敛。
| 变量 | 用途 | 默认值 |
| --- | --- | --- |
| `PORT` | 服务端口 | `5001` |
@@ -362,6 +374,15 @@ go run ./cmd/ds2api
> 提示:当检测到 `DS2API_CONFIG_JSON` 时,管理台会显示当前模式风险与自动持久化状态(含 `DS2API_CONFIG_PATH` 路径与模式切换说明)。
#### 必填 / 可选(按部署方式)
- **所有部署都必填**`DS2API_ADMIN_KEY`
- **配置来源二选一(推荐前者)**
- `config.json` 文件(推荐,持久化更直观)
- `DS2API_CONFIG_JSON`(可选,适合 Vercel支持 JSON 或 Base64
- **仅在环境变量配置模式建议开启**`DS2API_ENV_WRITEBACK=1`(避免管理台改动重启后丢失)
- 其余环境变量均为可选调优项。
## 鉴权模式
调用业务接口(`/v1/*`、`/anthropic/*`、Gemini 路由)时支持两种模式:

View File

@@ -5,14 +5,29 @@
"your-api-key-1",
"your-api-key-2"
],
"api_keys": [
{
"key": "your-api-key-1",
"name": "主 API Key",
"remark": "给 OpenAI 客户端使用"
},
{
"key": "your-api-key-2",
"name": "备用 API Key",
"remark": "压测或临时调试"
}
],
"accounts": [
{
"_comment": "邮箱登录方式",
"name": "主账号",
"remark": "优先用于生产流量",
"email": "example1@example.com",
"password": "your-password-1"
},
{
"_comment": "邮箱登录方式 - 账号2",
"name": "备用账号",
"email": "example2@example.com",
"password": "your-password-2"
},

View File

@@ -32,6 +32,8 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
for _, acc := range accounts {
id := strings.ToLower(acc.Identifier())
if strings.Contains(id, q) ||
strings.Contains(strings.ToLower(acc.Name), q) ||
strings.Contains(strings.ToLower(acc.Remark), q) ||
strings.Contains(strings.ToLower(acc.Email), q) ||
strings.Contains(strings.ToLower(acc.Mobile), q) {
filtered = append(filtered, acc)
@@ -66,6 +68,8 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
}
items = append(items, map[string]any{
"identifier": acc.Identifier(),
"name": acc.Name,
"remark": acc.Remark,
"email": acc.Email,
"mobile": acc.Mobile,
"proxy_id": acc.ProxyID,

View File

@@ -11,6 +11,7 @@ func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
snap := h.Store.Snapshot()
safe := map[string]any{
"keys": snap.Keys,
"api_keys": snap.APIKeys,
"accounts": []map[string]any{},
"proxies": []map[string]any{},
"env_backed": h.Store.IsEnvBacked(),
@@ -37,6 +38,8 @@ func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) {
}
accounts = append(accounts, map[string]any{
"identifier": acc.Identifier(),
"name": acc.Name,
"remark": acc.Remark,
"email": acc.Email,
"mobile": acc.Mobile,
"proxy_id": acc.ProxyID,

View File

@@ -19,8 +19,18 @@ func (h *Handler) updateConfig(w http.ResponseWriter, r *http.Request) {
}
old := h.Store.Snapshot()
err := h.Store.Update(func(c *config.Config) error {
if apiKeys, ok := toAPIKeys(req["api_keys"]); ok {
c.APIKeys = apiKeys
}
if keys, ok := toStringSlice(req["keys"]); ok {
c.Keys = keys
legacy := make([]config.APIKey, 0, len(keys))
for _, key := range keys {
if key == "" {
continue
}
legacy = append(legacy, config.APIKey{Key: key})
}
c.APIKeys = legacy
}
if accountsRaw, ok := req["accounts"].([]any); ok {
existing := map[string]config.Account{}
@@ -78,17 +88,19 @@ func (h *Handler) addKey(w http.ResponseWriter, r *http.Request) {
_ = json.NewDecoder(r.Body).Decode(&req)
key, _ := req["key"].(string)
key = strings.TrimSpace(key)
name := fieldString(req, "name")
remark := fieldString(req, "remark")
if key == "" {
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "Key 不能为空"})
return
}
err := h.Store.Update(func(c *config.Config) error {
for _, k := range c.Keys {
if k == key {
for _, item := range c.APIKeys {
if item.Key == key {
return fmt.Errorf("key 已存在")
}
}
c.Keys = append(c.Keys, key)
c.APIKeys = append(c.APIKeys, config.APIKey{Key: key, Name: name, Remark: remark})
return nil
})
if err != nil {
@@ -102,8 +114,8 @@ func (h *Handler) deleteKey(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
err := h.Store.Update(func(c *config.Config) error {
idx := -1
for i, k := range c.Keys {
if k == key {
for i, item := range c.APIKeys {
if item.Key == key {
idx = i
break
}
@@ -111,7 +123,7 @@ func (h *Handler) deleteKey(w http.ResponseWriter, r *http.Request) {
if idx < 0 {
return fmt.Errorf("key 不存在")
}
c.Keys = append(c.Keys[:idx], c.Keys[idx+1:]...)
c.APIKeys = append(c.APIKeys[:idx], c.APIKeys[idx+1:]...)
return nil
})
if err != nil {
@@ -129,17 +141,31 @@ func (h *Handler) batchImport(w http.ResponseWriter, r *http.Request) {
}
importedKeys, importedAccounts := 0, 0
err := h.Store.Update(func(c *config.Config) error {
if apiKeys, ok := toAPIKeys(req["api_keys"]); ok {
existing := map[string]bool{}
for _, item := range c.APIKeys {
existing[item.Key] = true
}
for _, item := range apiKeys {
if item.Key == "" || existing[item.Key] {
continue
}
c.APIKeys = append(c.APIKeys, item)
existing[item.Key] = true
importedKeys++
}
}
if keys, ok := req["keys"].([]any); ok {
existing := map[string]bool{}
for _, k := range c.Keys {
existing[k] = true
for _, item := range c.APIKeys {
existing[item.Key] = true
}
for _, k := range keys {
key := strings.TrimSpace(fmt.Sprintf("%v", k))
if key == "" || existing[key] {
continue
}
c.Keys = append(c.Keys, key)
c.APIKeys = append(c.APIKeys, config.APIKey{Key: key})
existing[key] = true
importedKeys++
}

View File

@@ -62,6 +62,8 @@ func toAccount(m map[string]any) config.Account {
email := fieldString(m, "email")
mobile := config.NormalizeMobileForStorage(fieldString(m, "mobile"))
return config.Account{
Name: fieldString(m, "name"),
Remark: fieldString(m, "remark"),
Email: email,
Mobile: mobile,
Password: fieldString(m, "password"),
@@ -69,6 +71,44 @@ func toAccount(m map[string]any) config.Account {
}
}
func toAPIKeys(v any) ([]config.APIKey, bool) {
arr, ok := v.([]any)
if !ok {
return nil, false
}
out := make([]config.APIKey, 0, len(arr))
seen := map[string]struct{}{}
for _, item := range arr {
switch x := item.(type) {
case map[string]any:
key := fieldString(x, "key")
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, config.APIKey{
Key: key,
Name: fieldString(x, "name"),
Remark: fieldString(x, "remark"),
})
default:
key := strings.TrimSpace(fmt.Sprintf("%v", item))
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
out = append(out, config.APIKey{Key: key})
}
}
return out, true
}
func fieldString(m map[string]any, key string) string {
v, ok := m[key]
if !ok || v == nil {
@@ -99,6 +139,8 @@ func accountMatchesIdentifier(acc config.Account, identifier string) bool {
}
func normalizeAccountForStorage(acc config.Account) config.Account {
acc.Name = strings.TrimSpace(acc.Name)
acc.Remark = strings.TrimSpace(acc.Remark)
acc.Email = strings.TrimSpace(acc.Email)
acc.Mobile = config.NormalizeMobileForStorage(acc.Mobile)
acc.ProxyID = strings.TrimSpace(acc.ProxyID)

View File

@@ -17,6 +17,9 @@ func (c Config) MarshalJSON() ([]byte, error) {
if len(c.Keys) > 0 {
m["keys"] = c.Keys
}
if len(c.APIKeys) > 0 {
m["api_keys"] = c.APIKeys
}
if len(c.Accounts) > 0 {
m["accounts"] = c.Accounts
}
@@ -69,6 +72,10 @@ func (c *Config) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(v, &c.Keys); err != nil {
return fmt.Errorf("invalid field %q: %w", k, err)
}
case "api_keys":
if err := json.Unmarshal(v, &c.APIKeys); err != nil {
return fmt.Errorf("invalid field %q: %w", k, err)
}
case "accounts":
if err := json.Unmarshal(v, &c.Accounts); err != nil {
return fmt.Errorf("invalid field %q: %w", k, err)
@@ -130,12 +137,14 @@ func (c *Config) UnmarshalJSON(b []byte) error {
}
}
}
c.NormalizeCredentials()
return nil
}
func (c Config) Clone() Config {
clone := Config{
Keys: slices.Clone(c.Keys),
APIKeys: slices.Clone(c.APIKeys),
Accounts: slices.Clone(c.Accounts),
Proxies: slices.Clone(c.Proxies),
ClaudeMapping: cloneStringMap(c.ClaudeMapping),

View File

@@ -9,6 +9,7 @@ import (
type Config struct {
Keys []string `json:"keys,omitempty"`
APIKeys []APIKey `json:"api_keys,omitempty"`
Accounts []Account `json:"accounts,omitempty"`
Proxies []Proxy `json:"proxies,omitempty"`
ClaudeMapping map[string]string `json:"claude_mapping,omitempty"`
@@ -26,6 +27,8 @@ type Config struct {
}
type Account struct {
Name string `json:"name,omitempty"`
Remark string `json:"remark,omitempty"`
Email string `json:"email,omitempty"`
Mobile string `json:"mobile,omitempty"`
Password string `json:"password,omitempty"`
@@ -33,6 +36,12 @@ type Account struct {
ProxyID string `json:"proxy_id,omitempty"`
}
type APIKey struct {
Key string `json:"key"`
Name string `json:"name,omitempty"`
Remark string `json:"remark,omitempty"`
}
type Proxy struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
@@ -73,6 +82,76 @@ func (c *Config) ClearAccountTokens() {
}
}
func (c *Config) NormalizeCredentials() {
if c == nil {
return
}
normalizedAPIKeys := make([]APIKey, 0, len(c.APIKeys))
metaByKey := make(map[string]APIKey, len(c.APIKeys))
for _, item := range c.APIKeys {
key := strings.TrimSpace(item.Key)
if key == "" {
continue
}
if _, ok := metaByKey[key]; ok {
continue
}
metaByKey[key] = APIKey{
Key: key,
Name: strings.TrimSpace(item.Name),
Remark: strings.TrimSpace(item.Remark),
}
}
if len(c.Keys) > 0 {
seen := make(map[string]struct{}, len(c.Keys))
for _, key := range c.Keys {
key = strings.TrimSpace(key)
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if item, ok := metaByKey[key]; ok {
normalizedAPIKeys = append(normalizedAPIKeys, item)
} else {
normalizedAPIKeys = append(normalizedAPIKeys, APIKey{Key: key})
}
}
} else {
normalizedAPIKeys = make([]APIKey, 0, len(c.APIKeys))
seen := make(map[string]struct{}, len(c.APIKeys))
for _, item := range c.APIKeys {
key := strings.TrimSpace(item.Key)
if key == "" {
continue
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
normalizedAPIKeys = append(normalizedAPIKeys, APIKey{
Key: key,
Name: strings.TrimSpace(item.Name),
Remark: strings.TrimSpace(item.Remark),
})
}
}
c.APIKeys = normalizedAPIKeys
keys := make([]string, 0, len(c.APIKeys))
for _, item := range c.APIKeys {
keys = append(keys, item.Key)
}
c.Keys = keys
for i := range c.Accounts {
c.Accounts[i].Name = strings.TrimSpace(c.Accounts[i].Name)
c.Accounts[i].Remark = strings.TrimSpace(c.Accounts[i].Remark)
}
}
// DropInvalidAccounts removes accounts that cannot be addressed by admin APIs
// (no email and no normalizable mobile). This prevents legacy token-only
// records from becoming orphaned empty entries after token stripping.

View File

@@ -43,6 +43,7 @@ func LoadStoreWithError() (*Store, error) {
func loadStore() (*Store, error) {
cfg, fromEnv, err := loadConfig()
cfg.NormalizeCredentials()
if validateErr := ValidateConfig(cfg); validateErr != nil {
err = errors.Join(err, validateErr)
}
@@ -112,6 +113,7 @@ func loadConfigFromFile(path string) (Config, error) {
if err := json.Unmarshal(content, &cfg); err != nil {
return Config{}, err
}
cfg.NormalizeCredentials()
cfg.DropInvalidAccounts()
if strings.Contains(string(content), `"test_status"`) && !IsVercel() {
if b, err := json.MarshalIndent(cfg, "", " "); err == nil {
@@ -207,6 +209,7 @@ func (s *Store) UpdateAccountToken(identifier, token string) error {
func (s *Store) Replace(cfg Config) error {
s.mu.Lock()
defer s.mu.Unlock()
cfg.NormalizeCredentials()
s.cfg = cfg.Clone()
s.rebuildIndexes()
return s.saveLocked()
@@ -219,6 +222,7 @@ func (s *Store) Update(mutator func(*Config) error) error {
if err := mutator(&cfg); err != nil {
return err
}
cfg.NormalizeCredentials()
s.cfg = cfg
s.rebuildIndexes()
return s.saveLocked()

View File

@@ -118,6 +118,7 @@ export default function AccountsTable({
runtimeUnknown ? "bg-blue-500 shadow-[0_0_8px_rgba(59,130,246,0.5)]" : "bg-amber-500"
)} />
<div className="min-w-0">
<div className="text-sm font-medium truncate">{acc.name || '-'}</div>
<div
className="font-medium truncate flex items-center gap-1.5 cursor-pointer hover:text-primary transition-colors group"
onClick={() => copyId(id)}
@@ -128,6 +129,9 @@ export default function AccountsTable({
: <Copy className="w-3 h-3 opacity-0 group-hover:opacity-50 shrink-0 transition-opacity" />
}
</div>
{acc.remark && (
<div className="text-xs text-muted-foreground truncate mt-0.5">{acc.remark}</div>
)}
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : isActive ? t('accountManager.sessionActive') : runtimeUnknown ? t('accountManager.runtimeStatusUnknown') : t('accountManager.reauthRequired')}</span>
{acc.token_preview && (

View File

@@ -23,6 +23,26 @@ export default function AddAccountModal({
</button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium mb-1.5">{t('accountManager.nameOptional')}</label>
<input
type="text"
className="input-field"
placeholder={t('accountManager.namePlaceholder')}
value={newAccount.name}
onChange={e => setNewAccount({ ...newAccount, name: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">{t('accountManager.remarkOptional')}</label>
<input
type="text"
className="input-field"
placeholder={t('accountManager.remarkPlaceholder')}
value={newAccount.remark}
onChange={e => setNewAccount({ ...newAccount, remark: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">{t('accountManager.emailOptional')}</label>
<input

View File

@@ -22,13 +22,13 @@ export default function AddKeyModal({ show, t, newKey, setNewKey, loading, onClo
type="text"
className="input-field bg-[#09090b] flex-1"
placeholder={t('accountManager.newKeyPlaceholder')}
value={newKey}
onChange={e => setNewKey(e.target.value)}
value={newKey.key}
onChange={e => setNewKey({ ...newKey, key: e.target.value })}
autoFocus
/>
<button
type="button"
onClick={() => setNewKey('sk-' + crypto.randomUUID().replace(/-/g, ''))}
onClick={() => setNewKey({ ...newKey, key: 'sk-' + crypto.randomUUID().replace(/-/g, '') })}
className="px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-sm font-medium border border-border whitespace-nowrap"
>
{t('accountManager.generate')}
@@ -36,6 +36,26 @@ export default function AddKeyModal({ show, t, newKey, setNewKey, loading, onClo
</div>
<p className="text-xs text-muted-foreground mt-1.5">{t('accountManager.generateHint')}</p>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">{t('accountManager.nameOptional')}</label>
<input
type="text"
className="input-field"
placeholder={t('accountManager.namePlaceholder')}
value={newKey.name}
onChange={e => setNewKey({ ...newKey, name: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">{t('accountManager.remarkOptional')}</label>
<input
type="text"
className="input-field"
placeholder={t('accountManager.remarkPlaceholder')}
value={newKey.remark}
onChange={e => setNewKey({ ...newKey, remark: e.target.value })}
/>
</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={onAdd} 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">

View File

@@ -37,6 +37,9 @@ export default function ApiKeysPanel({
onDeleteKey,
}) {
const [failedKey, setFailedKey] = useState(null)
const apiKeys = Array.isArray(config?.api_keys) && config.api_keys.length > 0
? config.api_keys
: (config?.keys || []).map(key => ({ key, name: '', remark: '' }))
const handleCopyKey = async (key) => {
try {
@@ -74,7 +77,7 @@ export default function ApiKeysPanel({
)} />
<div>
<h2 className="text-lg font-semibold">{t('accountManager.apiKeysTitle')}</h2>
<p className="text-sm text-muted-foreground">{t('accountManager.apiKeysDesc')} ({config.keys?.length || 0})</p>
<p className="text-sm text-muted-foreground">{t('accountManager.apiKeysDesc')} ({apiKeys.length || 0})</p>
</div>
</div>
<button
@@ -88,34 +91,36 @@ export default function ApiKeysPanel({
{keysExpanded && (
<div className="divide-y divide-border border-t border-border">
{config.keys?.length > 0 ? (
config.keys.map((key, i) => (
{apiKeys.length > 0 ? (
apiKeys.map((item, i) => (
<div key={i} className="p-4 flex items-center justify-between hover:bg-muted/50 transition-colors group">
<div className="flex items-center gap-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 flex-1">
<div className="text-sm">{item.name || '-'}</div>
<button
onClick={() => handleCopyKey(key)}
onClick={() => handleCopyKey(item.key)}
className="font-mono text-sm bg-muted/50 px-3 py-1 rounded inline-block hover:bg-muted transition-colors"
title={t('accountManager.copyKeyTitle')}
>
{key.slice(0, 16)}****
{(item.key || '').slice(0, 16)}****
</button>
{copiedKey === key && (
<div className="text-sm text-muted-foreground truncate">{item.remark || '-'}</div>
{copiedKey === item.key && (
<span className="text-xs text-green-500 animate-pulse">{t('accountManager.copied')}</span>
)}
{failedKey === key && (
{failedKey === item.key && (
<span className="text-xs text-destructive">{t('accountManager.copyFailed')}</span>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => handleCopyKey(key)}
onClick={() => handleCopyKey(item.key)}
className="p-2 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-md transition-colors"
title={t('accountManager.copyKeyTitle')}
>
{copiedKey === key ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
{copiedKey === item.key ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</button>
<button
onClick={() => onDeleteKey(key)}
onClick={() => onDeleteKey(item.key)}
className="p-2 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
title={t('accountManager.deleteKeyTitle')}
>

View File

@@ -3,9 +3,9 @@ import { useState } from 'react'
export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, fetchAccounts, resolveAccountIdentifier }) {
const [showAddKey, setShowAddKey] = useState(false)
const [showAddAccount, setShowAddAccount] = useState(false)
const [newKey, setNewKey] = useState('')
const [newKey, setNewKey] = useState({ key: '', name: '', remark: '' })
const [copiedKey, setCopiedKey] = useState(null)
const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' })
const [newAccount, setNewAccount] = useState({ name: '', remark: '', email: '', mobile: '', password: '' })
const [loading, setLoading] = useState(false)
const [testing, setTesting] = useState({})
const [testingAll, setTestingAll] = useState(false)
@@ -15,17 +15,17 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
const [updatingProxy, setUpdatingProxy] = useState({})
const addKey = async () => {
if (!newKey.trim()) return
if (!newKey.key.trim()) return
setLoading(true)
try {
const res = await apiFetch('/admin/keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: newKey.trim() }),
body: JSON.stringify({ key: newKey.key.trim(), name: newKey.name, remark: newKey.remark }),
})
if (res.ok) {
onMessage('success', t('accountManager.addKeySuccess'))
setNewKey('')
setNewKey({ key: '', name: '', remark: '' })
setShowAddKey(false)
onRefresh()
} else {
@@ -68,7 +68,7 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
})
if (res.ok) {
onMessage('success', t('accountManager.addAccountSuccess'))
setNewAccount({ email: '', mobile: '', password: '' })
setNewAccount({ name: '', remark: '', email: '', mobile: '', password: '' })
setShowAddAccount(false)
fetchAccounts(1)
onRefresh()

View File

@@ -131,6 +131,10 @@
"addKeyLoading": "Adding...",
"addKeyAction": "Add key",
"modalAddAccountTitle": "Add DeepSeek account",
"nameOptional": "Name (optional)",
"namePlaceholder": "e.g. Primary Account A",
"remarkOptional": "Remark (optional)",
"remarkPlaceholder": "e.g. Team shared / test only",
"emailOptional": "Email (optional)",
"mobileOptional": "Mobile (optional)",
"passwordLabel": "Password",

View File

@@ -131,6 +131,10 @@
"addKeyLoading": "添加中...",
"addKeyAction": "添加密钥",
"modalAddAccountTitle": "添加 DeepSeek 账号",
"nameOptional": "名称(可选)",
"namePlaceholder": "例如:主账号 A",
"remarkOptional": "备注(可选)",
"remarkPlaceholder": "例如:团队共享 / 仅测试用",
"emailOptional": "邮箱 (可选)",
"mobileOptional": "手机号 (可选)",
"passwordLabel": "密码",