mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
Compare commits
8 Commits
v2.3.3
...
v2.3.4_bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de50fd3954 | ||
|
|
7648d5f192 | ||
|
|
d35e5eab25 | ||
|
|
90610a52ce | ||
|
|
f6296d506f | ||
|
|
dfea092583 | ||
|
|
af7dc134bb | ||
|
|
2657d37f76 |
@@ -19,6 +19,7 @@ type DeepSeekCaller interface {
|
|||||||
CreateSession(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error)
|
CreateSession(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error)
|
||||||
GetPow(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error)
|
GetPow(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error)
|
||||||
CallCompletion(ctx context.Context, a *auth.RequestAuth, payload map[string]any, powResp string, maxAttempts int) (*http.Response, error)
|
CallCompletion(ctx context.Context, a *auth.RequestAuth, payload map[string]any, powResp string, maxAttempts int) (*http.Response, error)
|
||||||
|
DeleteAllSessionsForToken(ctx context.Context, token string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigReader interface {
|
type ConfigReader interface {
|
||||||
@@ -28,6 +29,7 @@ type ConfigReader interface {
|
|||||||
ToolcallEarlyEmitConfidence() string
|
ToolcallEarlyEmitConfidence() string
|
||||||
ResponsesStoreTTLSeconds() int
|
ResponsesStoreTTLSeconds() int
|
||||||
EmbeddingsProvider() string
|
EmbeddingsProvider() string
|
||||||
|
AutoDeleteSessions() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ AuthResolver = (*auth.Resolver)(nil)
|
var _ AuthResolver = (*auth.Resolver)(nil)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func (m mockOpenAIConfig) ToolcallMode() string { return m.toolMo
|
|||||||
func (m mockOpenAIConfig) ToolcallEarlyEmitConfidence() string { return m.earlyEmit }
|
func (m mockOpenAIConfig) ToolcallEarlyEmitConfidence() string { return m.earlyEmit }
|
||||||
func (m mockOpenAIConfig) ResponsesStoreTTLSeconds() int { return m.responsesTTL }
|
func (m mockOpenAIConfig) ResponsesStoreTTLSeconds() int { return m.responsesTTL }
|
||||||
func (m mockOpenAIConfig) EmbeddingsProvider() string { return m.embedProv }
|
func (m mockOpenAIConfig) EmbeddingsProvider() string { return m.embedProv }
|
||||||
|
func (m mockOpenAIConfig) AutoDeleteSessions() bool { return false }
|
||||||
|
|
||||||
func TestNormalizeOpenAIChatRequestWithConfigInterface(t *testing.T) {
|
func TestNormalizeOpenAIChatRequestWithConfigInterface(t *testing.T) {
|
||||||
cfg := mockOpenAIConfig{
|
cfg := mockOpenAIConfig{
|
||||||
|
|||||||
@@ -35,7 +35,23 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeOpenAIError(w, status, detail)
|
writeOpenAIError(w, status, detail)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer h.Auth.Release(a)
|
defer func() {
|
||||||
|
// 自动删除会话(同步)
|
||||||
|
// 必须在 Release 之前同步删除,否则:
|
||||||
|
// 1. 异步删除时账号已被 Release
|
||||||
|
// 2. 新请求可能获取到同一账号并开始使用
|
||||||
|
// 3. 异步删除仍在进行,会截断新请求正在使用的会话
|
||||||
|
if h.Store.AutoDeleteSessions() && a.DeepSeekToken != "" {
|
||||||
|
err := h.DS.DeleteAllSessionsForToken(context.Background(), a.DeepSeekToken)
|
||||||
|
if err != nil {
|
||||||
|
config.Logger.Warn("[auto_delete_sessions] failed", "account", a.AccountID, "error", err)
|
||||||
|
} else {
|
||||||
|
config.Logger.Debug("[auto_delete_sessions] success", "account", a.AccountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.Auth.Release(a)
|
||||||
|
}()
|
||||||
|
|
||||||
r = r.WithContext(auth.WithAuth(r.Context(), a))
|
r = r.WithContext(auth.WithAuth(r.Context(), a))
|
||||||
|
|
||||||
var req map[string]any
|
var req map[string]any
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ func (m streamStatusDSStub) CallCompletion(_ context.Context, _ *auth.RequestAut
|
|||||||
return m.resp, nil
|
return m.resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m streamStatusDSStub) DeleteAllSessionsForToken(_ context.Context, _ string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func makeOpenAISSEHTTPResponse(lines ...string) *http.Response {
|
func makeOpenAISSEHTTPResponse(lines ...string) *http.Response {
|
||||||
body := strings.Join(lines, "\n")
|
body := strings.Join(lines, "\n")
|
||||||
if !strings.HasSuffix(body, "\n") {
|
if !strings.HasSuffix(body, "\n") {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type ConfigStore interface {
|
|||||||
RuntimeAccountMaxInflight() int
|
RuntimeAccountMaxInflight() int
|
||||||
RuntimeAccountMaxQueue(defaultSize int) int
|
RuntimeAccountMaxQueue(defaultSize int) int
|
||||||
RuntimeGlobalMaxInflight(defaultSize int) int
|
RuntimeGlobalMaxInflight(defaultSize int) int
|
||||||
|
AutoDeleteSessions() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type PoolController interface {
|
type PoolController interface {
|
||||||
@@ -40,6 +41,8 @@ type DeepSeekCaller interface {
|
|||||||
CreateSession(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error)
|
CreateSession(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error)
|
||||||
GetPow(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error)
|
GetPow(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (string, error)
|
||||||
CallCompletion(ctx context.Context, a *auth.RequestAuth, payload map[string]any, powResp string, maxAttempts int) (*http.Response, error)
|
CallCompletion(ctx context.Context, a *auth.RequestAuth, payload map[string]any, powResp string, maxAttempts int) (*http.Response, error)
|
||||||
|
GetSessionCountForToken(ctx context.Context, token string) (*deepseek.SessionStats, error)
|
||||||
|
DeleteAllSessionsForToken(ctx context.Context, token string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ ConfigStore = (*config.Store)(nil)
|
var _ ConfigStore = (*config.Store)(nil)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func RegisterRoutes(r chi.Router, h *Handler) {
|
|||||||
pr.Get("/queue/status", h.queueStatus)
|
pr.Get("/queue/status", h.queueStatus)
|
||||||
pr.Post("/accounts/test", h.testSingleAccount)
|
pr.Post("/accounts/test", h.testSingleAccount)
|
||||||
pr.Post("/accounts/test-all", h.testAllAccounts)
|
pr.Post("/accounts/test-all", h.testAllAccounts)
|
||||||
|
pr.Post("/accounts/sessions/delete-all", h.deleteAllSessions)
|
||||||
pr.Post("/import", h.batchImport)
|
pr.Post("/import", h.batchImport)
|
||||||
pr.Post("/test", h.testAPI)
|
pr.Post("/test", h.testAPI)
|
||||||
pr.Post("/vercel/sync", h.syncVercel)
|
pr.Post("/vercel/sync", h.syncVercel)
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ func runAccountTestsConcurrently(accounts []config.Account, maxConcurrency int,
|
|||||||
func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, message string) map[string]any {
|
func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, message string) map[string]any {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
identifier := acc.Identifier()
|
identifier := acc.Identifier()
|
||||||
result := map[string]any{"account": identifier, "success": false, "response_time": 0, "message": "", "model": model}
|
result := map[string]any{"account": identifier, "success": false, "response_time": 0, "message": "", "model": model, "session_count": 0}
|
||||||
defer func() {
|
defer func() {
|
||||||
status := "failed"
|
status := "failed"
|
||||||
if ok, _ := result["success"].(bool); ok {
|
if ok, _ := result["success"].(bool); ok {
|
||||||
@@ -124,6 +124,13 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取会话数量
|
||||||
|
sessionStats, sessionErr := h.DS.GetSessionCountForToken(ctx, token)
|
||||||
|
if sessionErr == nil && sessionStats != nil {
|
||||||
|
result["session_count"] = sessionStats.FirstPageCount
|
||||||
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(message) == "" {
|
if strings.TrimSpace(message) == "" {
|
||||||
result["success"] = true
|
result["success"] = true
|
||||||
result["message"] = "API 测试成功(仅会话创建)"
|
result["message"] = "API 测试成功(仅会话创建)"
|
||||||
@@ -210,3 +217,39 @@ func (h *Handler) testAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"success": false, "status_code": resp.StatusCode, "response": string(body)})
|
writeJSON(w, http.StatusOK, map[string]any{"success": false, "status_code": resp.StatusCode, "response": string(body)})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) deleteAllSessions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req map[string]any
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
identifier, _ := req["identifier"].(string)
|
||||||
|
if strings.TrimSpace(identifier) == "" {
|
||||||
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "需要账号标识(identifier / email / mobile)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
acc, ok := findAccountByIdentifier(h.Store, identifier)
|
||||||
|
if !ok {
|
||||||
|
writeJSON(w, http.StatusNotFound, map[string]any{"detail": "账号不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 token
|
||||||
|
token := strings.TrimSpace(acc.Token)
|
||||||
|
if token == "" {
|
||||||
|
newToken, err := h.DS.Login(r.Context(), acc)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "登录失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token = newToken
|
||||||
|
_ = h.Store.UpdateAccountToken(acc.Identifier(), token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除所有会话
|
||||||
|
err := h.DS.DeleteAllSessionsForToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "删除失败: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{"success": true, "message": "删除成功"})
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"ds2api/internal/auth"
|
"ds2api/internal/auth"
|
||||||
"ds2api/internal/config"
|
"ds2api/internal/config"
|
||||||
|
"ds2api/internal/deepseek"
|
||||||
)
|
)
|
||||||
|
|
||||||
type testingDSMock struct {
|
type testingDSMock struct {
|
||||||
@@ -38,6 +39,14 @@ func (m *testingDSMock) CallCompletion(_ context.Context, _ *auth.RequestAuth, _
|
|||||||
return nil, errors.New("should not call CallCompletion in this test")
|
return nil, errors.New("should not call CallCompletion in this test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *testingDSMock) DeleteAllSessionsForToken(_ context.Context, _ string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *testingDSMock) GetSessionCountForToken(_ context.Context, _ string) (*deepseek.SessionStats, error) {
|
||||||
|
return &deepseek.SessionStats{Success: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestTestAccount_BatchModeOnlyCreatesSession(t *testing.T) {
|
func TestTestAccount_BatchModeOnlyCreatesSession(t *testing.T) {
|
||||||
t.Setenv("DS2API_CONFIG_JSON", `{"accounts":[{"email":"batch@example.com","password":"pwd","token":""}]}`)
|
t.Setenv("DS2API_CONFIG_JSON", `{"accounts":[{"email":"batch@example.com","password":"pwd","token":""}]}`)
|
||||||
store := config.LoadStore()
|
store := config.LoadStore()
|
||||||
|
|||||||
@@ -7,15 +7,30 @@ import (
|
|||||||
"ds2api/internal/config"
|
"ds2api/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *config.RuntimeConfig, *config.ToolcallConfig, *config.ResponsesConfig, *config.EmbeddingsConfig, map[string]string, map[string]string, error) {
|
func boolFrom(v any) bool {
|
||||||
|
if v == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return x
|
||||||
|
case string:
|
||||||
|
return strings.ToLower(strings.TrimSpace(x)) == "true"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *config.RuntimeConfig, *config.ToolcallConfig, *config.ResponsesConfig, *config.EmbeddingsConfig, *config.AutoDeleteConfig, map[string]string, map[string]string, error) {
|
||||||
var (
|
var (
|
||||||
adminCfg *config.AdminConfig
|
adminCfg *config.AdminConfig
|
||||||
runtimeCfg *config.RuntimeConfig
|
runtimeCfg *config.RuntimeConfig
|
||||||
toolcallCfg *config.ToolcallConfig
|
toolcallCfg *config.ToolcallConfig
|
||||||
respCfg *config.ResponsesConfig
|
respCfg *config.ResponsesConfig
|
||||||
embCfg *config.EmbeddingsConfig
|
embCfg *config.EmbeddingsConfig
|
||||||
claudeMap map[string]string
|
autoDeleteCfg *config.AutoDeleteConfig
|
||||||
aliasMap map[string]string
|
claudeMap map[string]string
|
||||||
|
aliasMap map[string]string
|
||||||
)
|
)
|
||||||
|
|
||||||
if raw, ok := req["admin"].(map[string]any); ok {
|
if raw, ok := req["admin"].(map[string]any); ok {
|
||||||
@@ -23,7 +38,7 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
|
|||||||
if v, exists := raw["jwt_expire_hours"]; exists {
|
if v, exists := raw["jwt_expire_hours"]; exists {
|
||||||
n := intFrom(v)
|
n := intFrom(v)
|
||||||
if n < 1 || n > 720 {
|
if n < 1 || n > 720 {
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("admin.jwt_expire_hours must be between 1 and 720")
|
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("admin.jwt_expire_hours must be between 1 and 720")
|
||||||
}
|
}
|
||||||
cfg.JWTExpireHours = n
|
cfg.JWTExpireHours = n
|
||||||
}
|
}
|
||||||
@@ -35,26 +50,26 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
|
|||||||
if v, exists := raw["account_max_inflight"]; exists {
|
if v, exists := raw["account_max_inflight"]; exists {
|
||||||
n := intFrom(v)
|
n := intFrom(v)
|
||||||
if n < 1 || n > 256 {
|
if n < 1 || n > 256 {
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.account_max_inflight must be between 1 and 256")
|
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.account_max_inflight must be between 1 and 256")
|
||||||
}
|
}
|
||||||
cfg.AccountMaxInflight = n
|
cfg.AccountMaxInflight = n
|
||||||
}
|
}
|
||||||
if v, exists := raw["account_max_queue"]; exists {
|
if v, exists := raw["account_max_queue"]; exists {
|
||||||
n := intFrom(v)
|
n := intFrom(v)
|
||||||
if n < 1 || n > 200000 {
|
if n < 1 || n > 200000 {
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.account_max_queue must be between 1 and 200000")
|
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.account_max_queue must be between 1 and 200000")
|
||||||
}
|
}
|
||||||
cfg.AccountMaxQueue = n
|
cfg.AccountMaxQueue = n
|
||||||
}
|
}
|
||||||
if v, exists := raw["global_max_inflight"]; exists {
|
if v, exists := raw["global_max_inflight"]; exists {
|
||||||
n := intFrom(v)
|
n := intFrom(v)
|
||||||
if n < 1 || n > 200000 {
|
if n < 1 || n > 200000 {
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.global_max_inflight must be between 1 and 200000")
|
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.global_max_inflight must be between 1 and 200000")
|
||||||
}
|
}
|
||||||
cfg.GlobalMaxInflight = n
|
cfg.GlobalMaxInflight = n
|
||||||
}
|
}
|
||||||
if cfg.AccountMaxInflight > 0 && cfg.GlobalMaxInflight > 0 && cfg.GlobalMaxInflight < cfg.AccountMaxInflight {
|
if cfg.AccountMaxInflight > 0 && cfg.GlobalMaxInflight > 0 && cfg.GlobalMaxInflight < cfg.AccountMaxInflight {
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.global_max_inflight must be >= runtime.account_max_inflight")
|
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("runtime.global_max_inflight must be >= runtime.account_max_inflight")
|
||||||
}
|
}
|
||||||
runtimeCfg = cfg
|
runtimeCfg = cfg
|
||||||
}
|
}
|
||||||
@@ -67,7 +82,7 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
|
|||||||
case "feature_match", "off":
|
case "feature_match", "off":
|
||||||
cfg.Mode = mode
|
cfg.Mode = mode
|
||||||
default:
|
default:
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("toolcall.mode must be feature_match or off")
|
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("toolcall.mode must be feature_match or off")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v, exists := raw["early_emit_confidence"]; exists {
|
if v, exists := raw["early_emit_confidence"]; exists {
|
||||||
@@ -76,7 +91,7 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
|
|||||||
case "high", "low", "off":
|
case "high", "low", "off":
|
||||||
cfg.EarlyEmitConfidence = level
|
cfg.EarlyEmitConfidence = level
|
||||||
default:
|
default:
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("toolcall.early_emit_confidence must be high, low or off")
|
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("toolcall.early_emit_confidence must be high, low or off")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toolcallCfg = cfg
|
toolcallCfg = cfg
|
||||||
@@ -87,7 +102,7 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
|
|||||||
if v, exists := raw["store_ttl_seconds"]; exists {
|
if v, exists := raw["store_ttl_seconds"]; exists {
|
||||||
n := intFrom(v)
|
n := intFrom(v)
|
||||||
if n < 30 || n > 86400 {
|
if n < 30 || n > 86400 {
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("responses.store_ttl_seconds must be between 30 and 86400")
|
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("responses.store_ttl_seconds must be between 30 and 86400")
|
||||||
}
|
}
|
||||||
cfg.StoreTTLSeconds = n
|
cfg.StoreTTLSeconds = n
|
||||||
}
|
}
|
||||||
@@ -98,9 +113,6 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
|
|||||||
cfg := &config.EmbeddingsConfig{}
|
cfg := &config.EmbeddingsConfig{}
|
||||||
if v, exists := raw["provider"]; exists {
|
if v, exists := raw["provider"]; exists {
|
||||||
p := strings.TrimSpace(fmt.Sprintf("%v", v))
|
p := strings.TrimSpace(fmt.Sprintf("%v", v))
|
||||||
if p == "" {
|
|
||||||
return nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("embeddings.provider cannot be empty")
|
|
||||||
}
|
|
||||||
cfg.Provider = p
|
cfg.Provider = p
|
||||||
}
|
}
|
||||||
embCfg = cfg
|
embCfg = cfg
|
||||||
@@ -130,5 +142,13 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return adminCfg, runtimeCfg, toolcallCfg, respCfg, embCfg, claudeMap, aliasMap, nil
|
if raw, ok := req["auto_delete"].(map[string]any); ok {
|
||||||
|
cfg := &config.AutoDeleteConfig{}
|
||||||
|
if v, exists := raw["sessions"]; exists {
|
||||||
|
cfg.Sessions = boolFrom(v)
|
||||||
|
}
|
||||||
|
autoDeleteCfg = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
return adminCfg, runtimeCfg, toolcallCfg, respCfg, embCfg, autoDeleteCfg, claudeMap, aliasMap, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func (h *Handler) getSettings(w http.ResponseWriter, _ *http.Request) {
|
|||||||
"toolcall": snap.Toolcall,
|
"toolcall": snap.Toolcall,
|
||||||
"responses": snap.Responses,
|
"responses": snap.Responses,
|
||||||
"embeddings": snap.Embeddings,
|
"embeddings": snap.Embeddings,
|
||||||
|
"auto_delete": snap.AutoDelete,
|
||||||
"claude_mapping": settingsClaudeMapping(snap),
|
"claude_mapping": settingsClaudeMapping(snap),
|
||||||
"model_aliases": snap.ModelAliases,
|
"model_aliases": snap.ModelAliases,
|
||||||
"env_backed": h.Store.IsEnvBacked(),
|
"env_backed": h.Store.IsEnvBacked(),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
adminCfg, runtimeCfg, toolcallCfg, responsesCfg, embeddingsCfg, claudeMap, aliasMap, err := parseSettingsUpdateRequest(req)
|
adminCfg, runtimeCfg, toolcallCfg, responsesCfg, embeddingsCfg, autoDeleteCfg, claudeMap, aliasMap, err := parseSettingsUpdateRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -60,6 +60,9 @@ func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
if embeddingsCfg != nil && strings.TrimSpace(embeddingsCfg.Provider) != "" {
|
if embeddingsCfg != nil && strings.TrimSpace(embeddingsCfg.Provider) != "" {
|
||||||
c.Embeddings.Provider = strings.TrimSpace(embeddingsCfg.Provider)
|
c.Embeddings.Provider = strings.TrimSpace(embeddingsCfg.Provider)
|
||||||
}
|
}
|
||||||
|
if autoDeleteCfg != nil {
|
||||||
|
c.AutoDelete.Sessions = autoDeleteCfg.Sessions
|
||||||
|
}
|
||||||
if claudeMap != nil {
|
if claudeMap != nil {
|
||||||
c.ClaudeMapping = claudeMap
|
c.ClaudeMapping = claudeMap
|
||||||
c.ClaudeModelMap = nil
|
c.ClaudeModelMap = nil
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func (c Config) MarshalJSON() ([]byte, error) {
|
|||||||
if strings.TrimSpace(c.Embeddings.Provider) != "" {
|
if strings.TrimSpace(c.Embeddings.Provider) != "" {
|
||||||
m["embeddings"] = c.Embeddings
|
m["embeddings"] = c.Embeddings
|
||||||
}
|
}
|
||||||
|
m["auto_delete"] = c.AutoDelete
|
||||||
if c.VercelSyncHash != "" {
|
if c.VercelSyncHash != "" {
|
||||||
m["_vercel_sync_hash"] = c.VercelSyncHash
|
m["_vercel_sync_hash"] = c.VercelSyncHash
|
||||||
}
|
}
|
||||||
@@ -108,6 +109,10 @@ func (c *Config) UnmarshalJSON(b []byte) error {
|
|||||||
if err := json.Unmarshal(v, &c.Embeddings); err != nil {
|
if err := json.Unmarshal(v, &c.Embeddings); err != nil {
|
||||||
return fmt.Errorf("invalid field %q: %w", k, err)
|
return fmt.Errorf("invalid field %q: %w", k, err)
|
||||||
}
|
}
|
||||||
|
case "auto_delete":
|
||||||
|
if err := json.Unmarshal(v, &c.AutoDelete); err != nil {
|
||||||
|
return fmt.Errorf("invalid field %q: %w", k, err)
|
||||||
|
}
|
||||||
case "_vercel_sync_hash":
|
case "_vercel_sync_hash":
|
||||||
if err := json.Unmarshal(v, &c.VercelSyncHash); err != nil {
|
if err := json.Unmarshal(v, &c.VercelSyncHash); err != nil {
|
||||||
return fmt.Errorf("invalid field %q: %w", k, err)
|
return fmt.Errorf("invalid field %q: %w", k, err)
|
||||||
@@ -141,6 +146,7 @@ func (c Config) Clone() Config {
|
|||||||
Toolcall: c.Toolcall,
|
Toolcall: c.Toolcall,
|
||||||
Responses: c.Responses,
|
Responses: c.Responses,
|
||||||
Embeddings: c.Embeddings,
|
Embeddings: c.Embeddings,
|
||||||
|
AutoDelete: c.AutoDelete,
|
||||||
VercelSyncHash: c.VercelSyncHash,
|
VercelSyncHash: c.VercelSyncHash,
|
||||||
VercelSyncTime: c.VercelSyncTime,
|
VercelSyncTime: c.VercelSyncTime,
|
||||||
AdditionalFields: map[string]any{},
|
AdditionalFields: map[string]any{},
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ type Config struct {
|
|||||||
Toolcall ToolcallConfig `json:"toolcall,omitempty"`
|
Toolcall ToolcallConfig `json:"toolcall,omitempty"`
|
||||||
Responses ResponsesConfig `json:"responses,omitempty"`
|
Responses ResponsesConfig `json:"responses,omitempty"`
|
||||||
Embeddings EmbeddingsConfig `json:"embeddings,omitempty"`
|
Embeddings EmbeddingsConfig `json:"embeddings,omitempty"`
|
||||||
VercelSyncHash string `json:"_vercel_sync_hash,omitempty"`
|
AutoDelete AutoDeleteConfig `json:"auto_delete"`
|
||||||
|
VercelSyncHash string `json:"_vercel_sync_hash,omitempty"`
|
||||||
VercelSyncTime int64 `json:"_vercel_sync_time,omitempty"`
|
VercelSyncTime int64 `json:"_vercel_sync_time,omitempty"`
|
||||||
AdditionalFields map[string]any `json:"-"`
|
AdditionalFields map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
@@ -53,3 +54,7 @@ type ResponsesConfig struct {
|
|||||||
type EmbeddingsConfig struct {
|
type EmbeddingsConfig struct {
|
||||||
Provider string `json:"provider,omitempty"`
|
Provider string `json:"provider,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AutoDeleteConfig struct {
|
||||||
|
Sessions bool `json:"sessions"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -165,3 +165,9 @@ func (s *Store) RuntimeGlobalMaxInflight(defaultSize int) int {
|
|||||||
}
|
}
|
||||||
return defaultSize
|
return defaultSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) AutoDeleteSessions() bool {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.cfg.AutoDelete.Sessions
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,3 +62,51 @@ func (c *Client) postJSONWithStatus(ctx context.Context, doer trans.Doer, url st
|
|||||||
}
|
}
|
||||||
return out, resp.StatusCode, nil
|
return out, resp.StatusCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) getJSON(ctx context.Context, doer trans.Doer, url string, headers map[string]string) (map[string]any, error) {
|
||||||
|
body, status, err := c.getJSONWithStatus(ctx, doer, url, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if status == 0 {
|
||||||
|
return nil, errors.New("request failed")
|
||||||
|
}
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getJSONWithStatus(ctx context.Context, doer trans.Doer, url string, headers map[string]string) (map[string]any, int, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
for k, v := range headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
resp, err := doer.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
config.Logger.Warn("[deepseek] fingerprint GET request failed, fallback to std transport", "url", url, "error", err)
|
||||||
|
req2, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if reqErr != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
for k, v := range headers {
|
||||||
|
req2.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
resp, err = c.fallback.Do(req2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
payloadBytes, err := readResponseBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
out := map[string]any{}
|
||||||
|
if len(payloadBytes) > 0 {
|
||||||
|
if err := json.Unmarshal(payloadBytes, &out); err != nil {
|
||||||
|
config.Logger.Warn("[deepseek] json parse failed", "url", url, "status", resp.StatusCode, "content_encoding", resp.Header.Get("Content-Encoding"), "preview", preview(payloadBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|||||||
254
internal/deepseek/client_session.go
Normal file
254
internal/deepseek/client_session.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
package deepseek
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"ds2api/internal/auth"
|
||||||
|
"ds2api/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionInfo 会话信息
|
||||||
|
type SessionInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
TitleType string `json:"title_type"`
|
||||||
|
Pinned bool `json:"pinned"`
|
||||||
|
UpdatedAt float64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionStats 会话统计结果
|
||||||
|
type SessionStats struct {
|
||||||
|
AccountID string // 账号标识 (email 或 mobile)
|
||||||
|
FirstPageCount int // 第一页会话数量(当 HasMore 为 true 时,真实总数可能更大)
|
||||||
|
PinnedCount int // 置顶会话数量
|
||||||
|
HasMore bool // 是否还有更多页
|
||||||
|
Success bool // 请求是否成功
|
||||||
|
ErrorMessage string // 错误信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSessionCount 获取单个账号的会话数量
|
||||||
|
func (c *Client) GetSessionCount(ctx context.Context, a *auth.RequestAuth, maxAttempts int) (*SessionStats, error) {
|
||||||
|
if maxAttempts <= 0 {
|
||||||
|
maxAttempts = c.maxRetries
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := &SessionStats{
|
||||||
|
AccountID: a.AccountID,
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts := 0
|
||||||
|
refreshed := false
|
||||||
|
|
||||||
|
for attempts < maxAttempts {
|
||||||
|
headers := c.authHeaders(a.DeepSeekToken)
|
||||||
|
|
||||||
|
// 构建请求 URL
|
||||||
|
reqURL := DeepSeekFetchSessionURL + "?lte_cursor.pinned=false"
|
||||||
|
|
||||||
|
resp, status, err := c.getJSONWithStatus(ctx, c.regular, reqURL, headers)
|
||||||
|
if err != nil {
|
||||||
|
config.Logger.Warn("[get_session_count] request error", "error", err, "account", a.AccountID)
|
||||||
|
attempts++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
code := intFrom(resp["code"])
|
||||||
|
if status == http.StatusOK && code == 0 {
|
||||||
|
data, _ := resp["data"].(map[string]any)
|
||||||
|
bizData, _ := data["biz_data"].(map[string]any)
|
||||||
|
chatSessions, _ := bizData["chat_sessions"].([]any)
|
||||||
|
hasMore, _ := bizData["has_more"].(bool)
|
||||||
|
|
||||||
|
stats.FirstPageCount = len(chatSessions)
|
||||||
|
stats.HasMore = hasMore
|
||||||
|
stats.Success = true
|
||||||
|
|
||||||
|
// 统计置顶会话数量
|
||||||
|
for _, session := range chatSessions {
|
||||||
|
if s, ok := session.(map[string]any); ok {
|
||||||
|
if pinned, ok := s["pinned"].(bool); ok && pinned {
|
||||||
|
stats.PinnedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, _ := resp["msg"].(string)
|
||||||
|
stats.ErrorMessage = fmt.Sprintf("status=%d, code=%d, msg=%s", status, code, msg)
|
||||||
|
config.Logger.Warn("[get_session_count] failed", "status", status, "code", code, "msg", msg, "account", a.AccountID)
|
||||||
|
|
||||||
|
if a.UseConfigToken {
|
||||||
|
if isTokenInvalid(status, code, msg) && !refreshed {
|
||||||
|
if c.Auth.RefreshToken(ctx, a) {
|
||||||
|
refreshed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Auth.SwitchAccount(ctx, a) {
|
||||||
|
refreshed = false
|
||||||
|
attempts++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.Success = false
|
||||||
|
stats.ErrorMessage = "get session count failed after retries"
|
||||||
|
return stats, errors.New(stats.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSessionCountForToken 直接使用 token 获取会话数量(直通模式)
|
||||||
|
func (c *Client) GetSessionCountForToken(ctx context.Context, token string) (*SessionStats, error) {
|
||||||
|
headers := c.authHeaders(token)
|
||||||
|
reqURL := DeepSeekFetchSessionURL + "?lte_cursor.pinned=false"
|
||||||
|
|
||||||
|
resp, status, err := c.getJSONWithStatus(ctx, c.regular, reqURL, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := intFrom(resp["code"])
|
||||||
|
if status != http.StatusOK || code != 0 {
|
||||||
|
msg, _ := resp["msg"].(string)
|
||||||
|
return nil, fmt.Errorf("request failed: status=%d, code=%d, msg=%s", status, code, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := resp["data"].(map[string]any)
|
||||||
|
bizData, _ := data["biz_data"].(map[string]any)
|
||||||
|
chatSessions, _ := bizData["chat_sessions"].([]any)
|
||||||
|
hasMore, _ := bizData["has_more"].(bool)
|
||||||
|
|
||||||
|
stats := &SessionStats{
|
||||||
|
FirstPageCount: len(chatSessions),
|
||||||
|
HasMore: hasMore,
|
||||||
|
Success: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计置顶会话数量
|
||||||
|
for _, session := range chatSessions {
|
||||||
|
if s, ok := session.(map[string]any); ok {
|
||||||
|
if pinned, ok := s["pinned"].(bool); ok && pinned {
|
||||||
|
stats.PinnedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSessionCountAll 获取所有账号的会话数量统计
|
||||||
|
func (c *Client) GetSessionCountAll(ctx context.Context) []*SessionStats {
|
||||||
|
accounts := c.Store.Accounts()
|
||||||
|
results := make([]*SessionStats, 0, len(accounts))
|
||||||
|
|
||||||
|
for _, acc := range accounts {
|
||||||
|
token := acc.Token
|
||||||
|
accountID := acc.Email
|
||||||
|
if accountID == "" {
|
||||||
|
accountID = acc.Mobile
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有 token,尝试登录获取
|
||||||
|
if token == "" {
|
||||||
|
var err error
|
||||||
|
token, err = c.Login(ctx, acc)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, &SessionStats{
|
||||||
|
AccountID: accountID,
|
||||||
|
Success: false,
|
||||||
|
ErrorMessage: fmt.Sprintf("login failed: %v", err),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := c.GetSessionCountForToken(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, &SessionStats{
|
||||||
|
AccountID: accountID,
|
||||||
|
Success: false,
|
||||||
|
ErrorMessage: err.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.AccountID = accountID
|
||||||
|
results = append(results, stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchSessionPage 获取会话列表(支持分页)
|
||||||
|
func (c *Client) FetchSessionPage(ctx context.Context, a *auth.RequestAuth, cursor string) ([]SessionInfo, bool, error) {
|
||||||
|
headers := c.authHeaders(a.DeepSeekToken)
|
||||||
|
|
||||||
|
// 构建请求 URL
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("lte_cursor.pinned", "false")
|
||||||
|
if cursor != "" {
|
||||||
|
params.Set("lte_cursor", cursor)
|
||||||
|
}
|
||||||
|
reqURL := DeepSeekFetchSessionURL + "?" + params.Encode()
|
||||||
|
|
||||||
|
resp, status, err := c.getJSONWithStatus(ctx, c.regular, reqURL, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := intFrom(resp["code"])
|
||||||
|
if status != http.StatusOK || code != 0 {
|
||||||
|
msg, _ := resp["msg"].(string)
|
||||||
|
return nil, false, fmt.Errorf("request failed: status=%d, code=%d, msg=%s", status, code, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := resp["data"].(map[string]any)
|
||||||
|
bizData, _ := data["biz_data"].(map[string]any)
|
||||||
|
chatSessions, _ := bizData["chat_sessions"].([]any)
|
||||||
|
hasMore, _ := bizData["has_more"].(bool)
|
||||||
|
|
||||||
|
sessions := make([]SessionInfo, 0, len(chatSessions))
|
||||||
|
for _, s := range chatSessions {
|
||||||
|
if m, ok := s.(map[string]any); ok {
|
||||||
|
session := SessionInfo{
|
||||||
|
ID: stringFromMap(m, "id"),
|
||||||
|
Title: stringFromMap(m, "title"),
|
||||||
|
TitleType: stringFromMap(m, "title_type"),
|
||||||
|
Pinned: boolFromMap(m, "pinned"),
|
||||||
|
UpdatedAt: floatFromMap(m, "updated_at"),
|
||||||
|
}
|
||||||
|
sessions = append(sessions, session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessions, hasMore, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数
|
||||||
|
func stringFromMap(m map[string]any, key string) string {
|
||||||
|
if v, ok := m[key].(string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolFromMap(m map[string]any, key string) bool {
|
||||||
|
if v, ok := m[key].(bool); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatFromMap(m map[string]any, key string) float64 {
|
||||||
|
if v, ok := m[key].(float64); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
156
internal/deepseek/client_session_delete.go
Normal file
156
internal/deepseek/client_session_delete.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package deepseek
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"ds2api/internal/auth"
|
||||||
|
"ds2api/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeleteSessionResult 删除会话结果
|
||||||
|
type DeleteSessionResult struct {
|
||||||
|
SessionID string // 会话 ID
|
||||||
|
Success bool // 是否成功
|
||||||
|
ErrorMessage string // 错误信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSession 删除单个会话
|
||||||
|
func (c *Client) DeleteSession(ctx context.Context, a *auth.RequestAuth, sessionID string, maxAttempts int) (*DeleteSessionResult, error) {
|
||||||
|
if maxAttempts <= 0 {
|
||||||
|
maxAttempts = c.maxRetries
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &DeleteSessionResult{
|
||||||
|
SessionID: sessionID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionID == "" {
|
||||||
|
result.ErrorMessage = "session_id is required"
|
||||||
|
return result, errors.New(result.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts := 0
|
||||||
|
refreshed := false
|
||||||
|
|
||||||
|
for attempts < maxAttempts {
|
||||||
|
headers := c.authHeaders(a.DeepSeekToken)
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"chat_session_id": sessionID,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, status, err := c.postJSONWithStatus(ctx, c.regular, DeepSeekDeleteSessionURL, headers, payload)
|
||||||
|
if err != nil {
|
||||||
|
config.Logger.Warn("[delete_session] request error", "error", err, "session_id", sessionID)
|
||||||
|
attempts++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
code := intFrom(resp["code"])
|
||||||
|
if status == http.StatusOK && code == 0 {
|
||||||
|
result.Success = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, _ := resp["msg"].(string)
|
||||||
|
result.ErrorMessage = fmt.Sprintf("status=%d, code=%d, msg=%s", status, code, msg)
|
||||||
|
config.Logger.Warn("[delete_session] failed", "status", status, "code", code, "msg", msg, "session_id", sessionID)
|
||||||
|
|
||||||
|
if a.UseConfigToken {
|
||||||
|
if isTokenInvalid(status, code, msg) && !refreshed {
|
||||||
|
if c.Auth.RefreshToken(ctx, a) {
|
||||||
|
refreshed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.Auth.SwitchAccount(ctx, a) {
|
||||||
|
refreshed = false
|
||||||
|
attempts++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = false
|
||||||
|
result.ErrorMessage = "delete session failed after retries"
|
||||||
|
return result, errors.New(result.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteSessionForToken 直接使用 token 删除会话(直通模式)
|
||||||
|
func (c *Client) DeleteSessionForToken(ctx context.Context, token string, sessionID string) (*DeleteSessionResult, error) {
|
||||||
|
result := &DeleteSessionResult{
|
||||||
|
SessionID: sessionID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionID == "" {
|
||||||
|
result.ErrorMessage = "session_id is required"
|
||||||
|
return result, errors.New(result.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := c.authHeaders(token)
|
||||||
|
payload := map[string]any{
|
||||||
|
"chat_session_id": sessionID,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, status, err := c.postJSONWithStatus(ctx, c.regular, DeepSeekDeleteSessionURL, headers, payload)
|
||||||
|
if err != nil {
|
||||||
|
result.ErrorMessage = err.Error()
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := intFrom(resp["code"])
|
||||||
|
if status != http.StatusOK || code != 0 {
|
||||||
|
msg, _ := resp["msg"].(string)
|
||||||
|
result.ErrorMessage = fmt.Sprintf("request failed: status=%d, code=%d, msg=%s", status, code, msg)
|
||||||
|
return result, errors.New(result.ErrorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Success = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAllSessions 删除所有会话(谨慎使用)
|
||||||
|
func (c *Client) DeleteAllSessions(ctx context.Context, a *auth.RequestAuth) error {
|
||||||
|
headers := c.authHeaders(a.DeepSeekToken)
|
||||||
|
payload := map[string]any{}
|
||||||
|
|
||||||
|
resp, status, err := c.postJSONWithStatus(ctx, c.regular, DeepSeekDeleteAllSessionsURL, headers, payload)
|
||||||
|
if err != nil {
|
||||||
|
config.Logger.Warn("[delete_all_sessions] request error", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := intFrom(resp["code"])
|
||||||
|
if status != http.StatusOK || code != 0 {
|
||||||
|
msg, _ := resp["msg"].(string)
|
||||||
|
config.Logger.Warn("[delete_all_sessions] failed", "status", status, "code", code, "msg", msg)
|
||||||
|
return fmt.Errorf("request failed: status=%d, code=%d, msg=%s", status, code, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAllSessionsForToken 直接使用 token 删除所有会话(直通模式)
|
||||||
|
func (c *Client) DeleteAllSessionsForToken(ctx context.Context, token string) error {
|
||||||
|
headers := c.authHeaders(token)
|
||||||
|
payload := map[string]any{}
|
||||||
|
|
||||||
|
resp, status, err := c.postJSONWithStatus(ctx, c.regular, DeepSeekDeleteAllSessionsURL, headers, payload)
|
||||||
|
if err != nil {
|
||||||
|
config.Logger.Warn("[delete_all_sessions_for_token] request error", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := intFrom(resp["code"])
|
||||||
|
if status != http.StatusOK || code != 0 {
|
||||||
|
msg, _ := resp["msg"].(string)
|
||||||
|
config.Logger.Warn("[delete_all_sessions_for_token] failed", "status", status, "code", code, "msg", msg)
|
||||||
|
return fmt.Errorf("request failed: status=%d, code=%d, msg=%s", status, code, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ const (
|
|||||||
DeepSeekCreateSessionURL = "https://chat.deepseek.com/api/v0/chat_session/create"
|
DeepSeekCreateSessionURL = "https://chat.deepseek.com/api/v0/chat_session/create"
|
||||||
DeepSeekCreatePowURL = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge"
|
DeepSeekCreatePowURL = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge"
|
||||||
DeepSeekCompletionURL = "https://chat.deepseek.com/api/v0/chat/completion"
|
DeepSeekCompletionURL = "https://chat.deepseek.com/api/v0/chat/completion"
|
||||||
|
DeepSeekFetchSessionURL = "https://chat.deepseek.com/api/v0/chat_session/fetch_page"
|
||||||
|
DeepSeekDeleteSessionURL = "https://chat.deepseek.com/api/v0/chat_session/delete"
|
||||||
|
DeepSeekDeleteAllSessionsURL = "https://chat.deepseek.com/api/v0/chat_session/delete_all"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultBaseHeaders = map[string]string{
|
var defaultBaseHeaders = map[string]string{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Line gate targets for large-file decoupling refactor.
|
# Line gate targets for large-file decoupling refactor.
|
||||||
# Default limit: 300 lines
|
# Backend default limit: 300 lines
|
||||||
|
# Frontend (webui/) default limit: 500 lines
|
||||||
# Entry/facade limit: 120 lines (enforced in script)
|
# Entry/facade limit: 120 lines (enforced in script)
|
||||||
|
# Test files are ignored by the gate script.
|
||||||
|
|
||||||
internal/config/config.go
|
internal/config/config.go
|
||||||
internal/config/logger.go
|
internal/config/logger.go
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
1. Production file default upper bound: `<= 300` lines.
|
1. Backend production files upper bound: `<= 300` lines.
|
||||||
2. Entry/facade files upper bound: `<= 120` lines.
|
2. Frontend (`webui/`) production files upper bound: `<= 500` lines.
|
||||||
3. Scope is limited to target files in `plans/refactor-line-gate-targets.txt`.
|
3. Entry/facade files upper bound: `<= 120` lines.
|
||||||
4. Test files are out of scope for this gate.
|
4. Scope is limited to target files in `plans/refactor-line-gate-targets.txt`.
|
||||||
|
5. Test files are out of scope for this gate.
|
||||||
|
|
||||||
## Command
|
## Command
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
|||||||
TARGETS_FILE="$ROOT_DIR/plans/refactor-line-gate-targets.txt"
|
TARGETS_FILE="$ROOT_DIR/plans/refactor-line-gate-targets.txt"
|
||||||
|
|
||||||
DEFAULT_MAX=300
|
DEFAULT_MAX=300
|
||||||
|
FRONTEND_MAX=500
|
||||||
ENTRY_MAX=120
|
ENTRY_MAX=120
|
||||||
|
|
||||||
is_entry_file() {
|
is_entry_file() {
|
||||||
@@ -22,6 +23,27 @@ is_entry_file() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_frontend_file() {
|
||||||
|
[[ "$1" == webui/* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
is_test_file() {
|
||||||
|
local file="$1"
|
||||||
|
local base
|
||||||
|
base="$(basename "$file")"
|
||||||
|
|
||||||
|
[[ "$file" == tests/* ]] && return 0
|
||||||
|
[[ "$file" == */tests/* ]] && return 0
|
||||||
|
[[ "$file" == */__tests__/* ]] && return 0
|
||||||
|
[[ "$base" == *_test.go ]] && return 0
|
||||||
|
[[ "$base" == *.test.js ]] && return 0
|
||||||
|
[[ "$base" == *.test.jsx ]] && return 0
|
||||||
|
[[ "$base" == *.test.ts ]] && return 0
|
||||||
|
[[ "$base" == *.test.tsx ]] && return 0
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
if [[ ! -f "$TARGETS_FILE" ]]; then
|
if [[ ! -f "$TARGETS_FILE" ]]; then
|
||||||
echo "missing targets file: $TARGETS_FILE" >&2
|
echo "missing targets file: $TARGETS_FILE" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -35,6 +57,10 @@ while IFS= read -r file; do
|
|||||||
[[ -z "$file" ]] && continue
|
[[ -z "$file" ]] && continue
|
||||||
[[ "${file:0:1}" == "#" ]] && continue
|
[[ "${file:0:1}" == "#" ]] && continue
|
||||||
|
|
||||||
|
if is_test_file "$file"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
checked=$((checked + 1))
|
checked=$((checked + 1))
|
||||||
abs="$ROOT_DIR/$file"
|
abs="$ROOT_DIR/$file"
|
||||||
if [[ ! -f "$abs" ]]; then
|
if [[ ! -f "$abs" ]]; then
|
||||||
@@ -47,6 +73,8 @@ while IFS= read -r file; do
|
|||||||
limit="$DEFAULT_MAX"
|
limit="$DEFAULT_MAX"
|
||||||
if is_entry_file "$file"; then
|
if is_entry_file "$file"; then
|
||||||
limit="$ENTRY_MAX"
|
limit="$ENTRY_MAX"
|
||||||
|
elif is_frontend_file "$file"; then
|
||||||
|
limit="$FRONTEND_MAX"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( lines > limit )); then
|
if (( lines > limit )); then
|
||||||
|
|||||||
@@ -1,121 +1,127 @@
|
|||||||
import { useI18n } from '../../i18n'
|
import { useI18n } from '../../i18n'
|
||||||
import { useAccountsData } from './useAccountsData'
|
import { useAccountsData } from './useAccountsData'
|
||||||
import { useAccountActions } from './useAccountActions'
|
import { useAccountActions } from './useAccountActions'
|
||||||
import QueueCards from './QueueCards'
|
import QueueCards from './QueueCards'
|
||||||
import ApiKeysPanel from './ApiKeysPanel'
|
import ApiKeysPanel from './ApiKeysPanel'
|
||||||
import AccountsTable from './AccountsTable'
|
import AccountsTable from './AccountsTable'
|
||||||
import AddKeyModal from './AddKeyModal'
|
import AddKeyModal from './AddKeyModal'
|
||||||
import AddAccountModal from './AddAccountModal'
|
import AddAccountModal from './AddAccountModal'
|
||||||
|
|
||||||
export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
|
export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const apiFetch = authFetch || fetch
|
const apiFetch = authFetch || fetch
|
||||||
|
|
||||||
const {
|
const {
|
||||||
queueStatus,
|
queueStatus,
|
||||||
keysExpanded,
|
keysExpanded,
|
||||||
setKeysExpanded,
|
setKeysExpanded,
|
||||||
accounts,
|
accounts,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
totalPages,
|
totalPages,
|
||||||
totalAccounts,
|
totalAccounts,
|
||||||
loadingAccounts,
|
loadingAccounts,
|
||||||
fetchAccounts,
|
fetchAccounts,
|
||||||
changePageSize,
|
changePageSize,
|
||||||
resolveAccountIdentifier,
|
resolveAccountIdentifier,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
handleSearchChange,
|
handleSearchChange,
|
||||||
} = useAccountsData({ apiFetch })
|
} = useAccountsData({ apiFetch })
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showAddKey,
|
showAddKey,
|
||||||
setShowAddKey,
|
setShowAddKey,
|
||||||
showAddAccount,
|
showAddAccount,
|
||||||
setShowAddAccount,
|
setShowAddAccount,
|
||||||
newKey,
|
newKey,
|
||||||
setNewKey,
|
setNewKey,
|
||||||
copiedKey,
|
copiedKey,
|
||||||
setCopiedKey,
|
setCopiedKey,
|
||||||
newAccount,
|
newAccount,
|
||||||
setNewAccount,
|
setNewAccount,
|
||||||
loading,
|
loading,
|
||||||
testing,
|
testing,
|
||||||
testingAll,
|
testingAll,
|
||||||
batchProgress,
|
batchProgress,
|
||||||
addKey,
|
sessionCounts,
|
||||||
deleteKey,
|
deletingSessions,
|
||||||
addAccount,
|
addKey,
|
||||||
deleteAccount,
|
deleteKey,
|
||||||
testAccount,
|
addAccount,
|
||||||
testAllAccounts,
|
deleteAccount,
|
||||||
} = useAccountActions({
|
testAccount,
|
||||||
apiFetch,
|
testAllAccounts,
|
||||||
t,
|
deleteAllSessions,
|
||||||
onMessage,
|
} = useAccountActions({
|
||||||
onRefresh,
|
apiFetch,
|
||||||
config,
|
t,
|
||||||
fetchAccounts,
|
onMessage,
|
||||||
resolveAccountIdentifier,
|
onRefresh,
|
||||||
})
|
config,
|
||||||
|
fetchAccounts,
|
||||||
return (
|
resolveAccountIdentifier,
|
||||||
<div className="space-y-6">
|
})
|
||||||
<QueueCards queueStatus={queueStatus} t={t} />
|
|
||||||
|
return (
|
||||||
<ApiKeysPanel
|
<div className="space-y-6">
|
||||||
t={t}
|
<QueueCards queueStatus={queueStatus} t={t} />
|
||||||
config={config}
|
|
||||||
keysExpanded={keysExpanded}
|
<ApiKeysPanel
|
||||||
setKeysExpanded={setKeysExpanded}
|
t={t}
|
||||||
setShowAddKey={setShowAddKey}
|
config={config}
|
||||||
copiedKey={copiedKey}
|
keysExpanded={keysExpanded}
|
||||||
setCopiedKey={setCopiedKey}
|
setKeysExpanded={setKeysExpanded}
|
||||||
onDeleteKey={deleteKey}
|
setShowAddKey={setShowAddKey}
|
||||||
/>
|
copiedKey={copiedKey}
|
||||||
|
setCopiedKey={setCopiedKey}
|
||||||
<AccountsTable
|
onDeleteKey={deleteKey}
|
||||||
t={t}
|
/>
|
||||||
accounts={accounts}
|
|
||||||
loadingAccounts={loadingAccounts}
|
<AccountsTable
|
||||||
testing={testing}
|
t={t}
|
||||||
testingAll={testingAll}
|
accounts={accounts}
|
||||||
batchProgress={batchProgress}
|
loadingAccounts={loadingAccounts}
|
||||||
totalAccounts={totalAccounts}
|
testing={testing}
|
||||||
page={page}
|
testingAll={testingAll}
|
||||||
pageSize={pageSize}
|
batchProgress={batchProgress}
|
||||||
totalPages={totalPages}
|
sessionCounts={sessionCounts}
|
||||||
resolveAccountIdentifier={resolveAccountIdentifier}
|
deletingSessions={deletingSessions}
|
||||||
onTestAll={testAllAccounts}
|
totalAccounts={totalAccounts}
|
||||||
onShowAddAccount={() => setShowAddAccount(true)}
|
page={page}
|
||||||
onTestAccount={testAccount}
|
pageSize={pageSize}
|
||||||
onDeleteAccount={deleteAccount}
|
totalPages={totalPages}
|
||||||
onPrevPage={() => fetchAccounts(page - 1)}
|
resolveAccountIdentifier={resolveAccountIdentifier}
|
||||||
onNextPage={() => fetchAccounts(page + 1)}
|
onTestAll={testAllAccounts}
|
||||||
onPageSizeChange={changePageSize}
|
onShowAddAccount={() => setShowAddAccount(true)}
|
||||||
searchQuery={searchQuery}
|
onTestAccount={testAccount}
|
||||||
onSearchChange={handleSearchChange}
|
onDeleteAccount={deleteAccount}
|
||||||
/>
|
onDeleteAllSessions={deleteAllSessions}
|
||||||
|
onPrevPage={() => fetchAccounts(page - 1)}
|
||||||
<AddKeyModal
|
onNextPage={() => fetchAccounts(page + 1)}
|
||||||
show={showAddKey}
|
onPageSizeChange={changePageSize}
|
||||||
t={t}
|
searchQuery={searchQuery}
|
||||||
newKey={newKey}
|
onSearchChange={handleSearchChange}
|
||||||
setNewKey={setNewKey}
|
/>
|
||||||
loading={loading}
|
|
||||||
onClose={() => setShowAddKey(false)}
|
<AddKeyModal
|
||||||
onAdd={addKey}
|
show={showAddKey}
|
||||||
/>
|
t={t}
|
||||||
|
newKey={newKey}
|
||||||
<AddAccountModal
|
setNewKey={setNewKey}
|
||||||
show={showAddAccount}
|
loading={loading}
|
||||||
t={t}
|
onClose={() => setShowAddKey(false)}
|
||||||
newAccount={newAccount}
|
onAdd={addKey}
|
||||||
setNewAccount={setNewAccount}
|
/>
|
||||||
loading={loading}
|
|
||||||
onClose={() => setShowAddAccount(false)}
|
<AddAccountModal
|
||||||
onAdd={addAccount}
|
show={showAddAccount}
|
||||||
/>
|
t={t}
|
||||||
</div>
|
newAccount={newAccount}
|
||||||
)
|
setNewAccount={setNewAccount}
|
||||||
}
|
loading={loading}
|
||||||
|
onClose={() => setShowAddAccount(false)}
|
||||||
|
onAdd={addAccount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,191 +1,213 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2 } from 'lucide-react'
|
import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2, FolderX } from 'lucide-react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
export default function AccountsTable({
|
export default function AccountsTable({
|
||||||
t,
|
t,
|
||||||
accounts,
|
accounts,
|
||||||
loadingAccounts,
|
loadingAccounts,
|
||||||
testing,
|
testing,
|
||||||
testingAll,
|
testingAll,
|
||||||
batchProgress,
|
batchProgress,
|
||||||
totalAccounts,
|
sessionCounts,
|
||||||
page,
|
deletingSessions,
|
||||||
pageSize,
|
totalAccounts,
|
||||||
totalPages,
|
page,
|
||||||
resolveAccountIdentifier,
|
pageSize,
|
||||||
onTestAll,
|
totalPages,
|
||||||
onShowAddAccount,
|
resolveAccountIdentifier,
|
||||||
onTestAccount,
|
onTestAll,
|
||||||
onDeleteAccount,
|
onShowAddAccount,
|
||||||
onPrevPage,
|
onTestAccount,
|
||||||
onNextPage,
|
onDeleteAccount,
|
||||||
onPageSizeChange,
|
onDeleteAllSessions,
|
||||||
searchQuery,
|
onPrevPage,
|
||||||
onSearchChange,
|
onNextPage,
|
||||||
}) {
|
onPageSizeChange,
|
||||||
const [copiedId, setCopiedId] = useState(null)
|
searchQuery,
|
||||||
|
onSearchChange,
|
||||||
const copyId = (id) => {
|
}) {
|
||||||
navigator.clipboard.writeText(id).then(() => {
|
const [copiedId, setCopiedId] = useState(null)
|
||||||
setCopiedId(id)
|
|
||||||
setTimeout(() => setCopiedId(null), 1500)
|
const copyId = (id) => {
|
||||||
})
|
navigator.clipboard.writeText(id).then(() => {
|
||||||
}
|
setCopiedId(id)
|
||||||
return (
|
setTimeout(() => setCopiedId(null), 1500)
|
||||||
<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>
|
return (
|
||||||
<h2 className="text-lg font-semibold">{t('accountManager.accountsTitle')}</h2>
|
<div className="bg-card border border-border rounded-xl overflow-hidden shadow-sm">
|
||||||
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
|
<div className="p-6 border-b border-border flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
</div>
|
<div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<h2 className="text-lg font-semibold">{t('accountManager.accountsTitle')}</h2>
|
||||||
<input
|
<p className="text-sm text-muted-foreground">{t('accountManager.accountsDesc')}</p>
|
||||||
type="text"
|
</div>
|
||||||
value={searchQuery}
|
<div className="flex flex-wrap gap-2">
|
||||||
onChange={e => onSearchChange(e.target.value)}
|
<input
|
||||||
placeholder={t('accountManager.searchPlaceholder')}
|
type="text"
|
||||||
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"
|
value={searchQuery}
|
||||||
/>
|
onChange={e => onSearchChange(e.target.value)}
|
||||||
<button
|
placeholder={t('accountManager.searchPlaceholder')}
|
||||||
onClick={onTestAll}
|
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"
|
||||||
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"
|
<button
|
||||||
>
|
onClick={onTestAll}
|
||||||
{testingAll ? <span className="animate-spin mr-2">⟳</span> : <Play className="w-3 h-3 mr-2" />}
|
disabled={testingAll || totalAccounts === 0}
|
||||||
{t('accountManager.testAll')}
|
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"
|
||||||
</button>
|
>
|
||||||
<button
|
{testingAll ? <span className="animate-spin mr-2">⟳</span> : <Play className="w-3 h-3 mr-2" />}
|
||||||
onClick={onShowAddAccount}
|
{t('accountManager.testAll')}
|
||||||
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"
|
</button>
|
||||||
>
|
<button
|
||||||
<Plus className="w-4 h-4" />
|
onClick={onShowAddAccount}
|
||||||
{t('accountManager.addAccount')}
|
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"
|
||||||
</button>
|
>
|
||||||
</div>
|
<Plus className="w-4 h-4" />
|
||||||
</div>
|
{t('accountManager.addAccount')}
|
||||||
|
</button>
|
||||||
{testingAll && batchProgress.total > 0 && (
|
</div>
|
||||||
<div className="p-4 border-b border-border bg-muted/30">
|
</div>
|
||||||
<div className="flex items-center justify-between text-sm mb-2">
|
|
||||||
<span className="font-medium">{t('accountManager.testingAllAccounts')}</span>
|
{testingAll && batchProgress.total > 0 && (
|
||||||
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
|
<div className="p-4 border-b border-border bg-muted/30">
|
||||||
</div>
|
<div className="flex items-center justify-between text-sm mb-2">
|
||||||
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
|
<span className="font-medium">{t('accountManager.testingAllAccounts')}</span>
|
||||||
<div
|
<span className="text-muted-foreground">{batchProgress.current} / {batchProgress.total}</span>
|
||||||
className="bg-primary h-full transition-all duration-300"
|
</div>
|
||||||
style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%` }}
|
<div className="w-full bg-muted rounded-full h-2 overflow-hidden mb-4">
|
||||||
/>
|
<div
|
||||||
</div>
|
className="bg-primary h-full transition-all duration-300"
|
||||||
{batchProgress.results.length > 0 && (
|
style={{ width: `${(batchProgress.current / batchProgress.total) * 100}%` }}
|
||||||
<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>
|
||||||
<div key={i} className={clsx(
|
{batchProgress.results.length > 0 && (
|
||||||
"text-xs px-2 py-1 rounded border truncate",
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 max-h-32 overflow-y-auto custom-scrollbar">
|
||||||
r.success ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-500" : "bg-destructive/10 border-destructive/20 text-destructive"
|
{batchProgress.results.map((r, i) => (
|
||||||
)}>
|
<div key={i} className={clsx(
|
||||||
{r.success ? '✓' : '✗'} {r.id}
|
"text-xs px-2 py-1 rounded border truncate",
|
||||||
</div>
|
r.success ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-500" : "bg-destructive/10 border-destructive/20 text-destructive"
|
||||||
))}
|
)}>
|
||||||
</div>
|
{r.success ? '✓' : '✗'} {r.id}
|
||||||
)}
|
</div>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
<div className="divide-y divide-border">
|
</div>
|
||||||
{loadingAccounts ? (
|
)}
|
||||||
<div className="p-8 text-center text-muted-foreground">{t('actions.loading')}</div>
|
|
||||||
) : accounts.length > 0 ? (
|
<div className="divide-y divide-border">
|
||||||
accounts.map((acc, i) => {
|
{loadingAccounts ? (
|
||||||
const id = resolveAccountIdentifier(acc)
|
<div className="p-8 text-center text-muted-foreground">{t('actions.loading')}</div>
|
||||||
return (
|
) : accounts.length > 0 ? (
|
||||||
<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">
|
accounts.map((acc, i) => {
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
const id = resolveAccountIdentifier(acc)
|
||||||
<div className={clsx(
|
return (
|
||||||
"w-2 h-2 rounded-full shrink-0",
|
<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">
|
||||||
acc.test_status === 'failed' ? "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" :
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
(acc.test_status === 'ok' || acc.has_token) ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
|
<div className={clsx(
|
||||||
"bg-amber-500"
|
"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)]" :
|
||||||
<div className="min-w-0">
|
(acc.test_status === 'ok' || acc.has_token) ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" :
|
||||||
<div
|
"bg-amber-500"
|
||||||
className="font-medium truncate flex items-center gap-1.5 cursor-pointer hover:text-primary transition-colors group"
|
)} />
|
||||||
onClick={() => copyId(id)}
|
<div className="min-w-0">
|
||||||
>
|
<div
|
||||||
<span className="truncate">{id || '-'}</span>
|
className="font-medium truncate flex items-center gap-1.5 cursor-pointer hover:text-primary transition-colors group"
|
||||||
{copiedId === id
|
onClick={() => copyId(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" />
|
<span className="truncate">{id || '-'}</span>
|
||||||
}
|
{copiedId === id
|
||||||
</div>
|
? <Check className="w-3 h-3 text-emerald-500 shrink-0" />
|
||||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
: <Copy className="w-3 h-3 opacity-0 group-hover:opacity-50 shrink-0 transition-opacity" />
|
||||||
<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 && (
|
</div>
|
||||||
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
|
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
|
||||||
{acc.token_preview}
|
<span>{acc.test_status === 'failed' ? t('accountManager.testStatusFailed') : (acc.test_status === 'ok' || acc.has_token) ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')}</span>
|
||||||
</span>
|
{acc.token_preview && (
|
||||||
)}
|
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-[10px]">
|
||||||
</div>
|
{acc.token_preview}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
)}
|
||||||
<div className="flex items-center gap-2 self-start lg:self-auto ml-5 lg:ml-0">
|
{sessionCounts && sessionCounts[id] !== undefined && (
|
||||||
<button
|
<span className="font-mono bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded text-[10px]">
|
||||||
onClick={() => onTestAccount(id)}
|
{t('accountManager.sessionCount', { count: sessionCounts[id] })}
|
||||||
disabled={testing[id]}
|
</span>
|
||||||
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"
|
)}
|
||||||
>
|
{sessionCounts && sessionCounts[id] !== undefined && sessionCounts[id] > 0 && (
|
||||||
{testing[id] ? t('actions.testing') : t('actions.test')}
|
<button
|
||||||
</button>
|
onClick={() => onDeleteAllSessions(id)}
|
||||||
<button
|
disabled={deletingSessions && deletingSessions[id]}
|
||||||
onClick={() => onDeleteAccount(id)}
|
className="flex items-center gap-1 font-mono bg-red-500/10 text-red-500 hover:bg-red-500/20 px-1.5 py-0.5 rounded text-[10px] transition-colors disabled:opacity-50"
|
||||||
className="p-1 lg:p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
|
title={t('accountManager.deleteAllSessions')}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
|
{deletingSessions && deletingSessions[id] ? (
|
||||||
</button>
|
<span className="animate-spin">⟳</span>
|
||||||
</div>
|
) : (
|
||||||
</div>
|
<FolderX className="w-3 h-3" />
|
||||||
)
|
)}
|
||||||
})
|
</button>
|
||||||
) : (
|
)}
|
||||||
<div className="p-8 text-center text-muted-foreground">{searchQuery ? t('accountManager.searchNoResults') : t('accountManager.noAccounts')}</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 self-start lg:self-auto ml-5 lg:ml-0">
|
||||||
{totalPages > 1 && (
|
<button
|
||||||
<div className="p-4 border-t border-border flex items-center justify-between">
|
onClick={() => onTestAccount(id)}
|
||||||
<div className="flex items-center gap-3">
|
disabled={testing[id]}
|
||||||
<div className="text-sm text-muted-foreground">
|
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"
|
||||||
{t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })}
|
>
|
||||||
</div>
|
{testing[id] ? t('actions.testing') : t('actions.test')}
|
||||||
<select
|
</button>
|
||||||
value={pageSize}
|
<button
|
||||||
onChange={e => onPageSizeChange(Number(e.target.value))}
|
onClick={() => onDeleteAccount(id)}
|
||||||
className="text-sm border border-border rounded-md px-2 py-1 bg-background text-foreground"
|
className="p-1 lg:p-1.5 text-muted-foreground hover:text-destructive hover:bg-destructive/10 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
{[10, 20, 50, 100, 500, 1000, 2000, 5000].map(s => (
|
<Trash2 className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
|
||||||
<option key={s} value={s}>{s}</option>
|
</button>
|
||||||
))}
|
</div>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
)
|
||||||
<div className="flex items-center gap-2">
|
})
|
||||||
<button
|
) : (
|
||||||
onClick={onPrevPage}
|
<div className="p-8 text-center text-muted-foreground">{searchQuery ? t('accountManager.searchNoResults') : t('accountManager.noAccounts')}</div>
|
||||||
disabled={page <= 1 || loadingAccounts}
|
)}
|
||||||
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
</div>
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4" />
|
{totalPages > 1 && (
|
||||||
</button>
|
<div className="p-4 border-t border-border flex items-center justify-between">
|
||||||
<span className="text-sm font-medium px-2">{page} / {totalPages}</span>
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<div className="text-sm text-muted-foreground">
|
||||||
onClick={onNextPage}
|
{t('accountManager.pageInfo', { current: page, total: totalPages, count: totalAccounts })}
|
||||||
disabled={page >= totalPages || loadingAccounts}
|
</div>
|
||||||
className="p-2 border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
<select
|
||||||
>
|
value={pageSize}
|
||||||
<ChevronRight className="w-4 h-4" />
|
onChange={e => onPageSizeChange(Number(e.target.value))}
|
||||||
</button>
|
className="text-sm border border-border rounded-md px-2 py-1 bg-background text-foreground"
|
||||||
</div>
|
>
|
||||||
</div>
|
{[10, 20, 50, 100, 500, 1000, 2000, 5000].map(s => (
|
||||||
)}
|
<option key={s} value={s}>{s}</option>
|
||||||
</div>
|
))}
|
||||||
)
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
|||||||
const [testing, setTesting] = useState({})
|
const [testing, setTesting] = useState({})
|
||||||
const [testingAll, setTestingAll] = useState(false)
|
const [testingAll, setTestingAll] = useState(false)
|
||||||
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] })
|
const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] })
|
||||||
|
const [sessionCounts, setSessionCounts] = useState({})
|
||||||
|
const [deletingSessions, setDeletingSessions] = useState({})
|
||||||
|
|
||||||
const addKey = async () => {
|
const addKey = async () => {
|
||||||
if (!newKey.trim()) return
|
if (!newKey.trim()) return
|
||||||
@@ -115,6 +117,12 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
|||||||
body: JSON.stringify({ identifier: accountID }),
|
body: JSON.stringify({ identifier: accountID }),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
|
// 更新会话数
|
||||||
|
if (data.session_count !== undefined) {
|
||||||
|
setSessionCounts(prev => ({ ...prev, [accountID]: data.session_count }))
|
||||||
|
}
|
||||||
|
|
||||||
const statusMessage = data.success
|
const statusMessage = data.success
|
||||||
? t('apiTester.testSuccess', { account: accountID, time: data.response_time })
|
? t('apiTester.testSuccess', { account: accountID, time: data.response_time })
|
||||||
: `${accountID}: ${data.message}`
|
: `${accountID}: ${data.message}`
|
||||||
@@ -170,6 +178,41 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
|||||||
setTestingAll(false)
|
setTestingAll(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteAllSessions = async (identifier) => {
|
||||||
|
const accountID = String(identifier || '').trim()
|
||||||
|
if (!accountID) {
|
||||||
|
onMessage('error', t('accountManager.invalidIdentifier'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!confirm(t('accountManager.deleteAllSessionsConfirm'))) return
|
||||||
|
|
||||||
|
setDeletingSessions(prev => ({ ...prev, [accountID]: true }))
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/admin/accounts/sessions/delete-all', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ identifier: accountID }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
onMessage('success', t('accountManager.deleteAllSessionsSuccess'))
|
||||||
|
// 清除会话数显示
|
||||||
|
setSessionCounts(prev => {
|
||||||
|
const newCounts = { ...prev }
|
||||||
|
delete newCounts[accountID]
|
||||||
|
return newCounts
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
onMessage('error', data.message || t('messages.requestFailed'))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
onMessage('error', t('messages.networkError'))
|
||||||
|
} finally {
|
||||||
|
setDeletingSessions(prev => ({ ...prev, [accountID]: false }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showAddKey,
|
showAddKey,
|
||||||
setShowAddKey,
|
setShowAddKey,
|
||||||
@@ -185,11 +228,14 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
|||||||
testing,
|
testing,
|
||||||
testingAll,
|
testingAll,
|
||||||
batchProgress,
|
batchProgress,
|
||||||
|
sessionCounts,
|
||||||
|
deletingSessions,
|
||||||
addKey,
|
addKey,
|
||||||
deleteKey,
|
deleteKey,
|
||||||
addAccount,
|
addAccount,
|
||||||
deleteAccount,
|
deleteAccount,
|
||||||
testAccount,
|
testAccount,
|
||||||
testAllAccounts,
|
testAllAccounts,
|
||||||
|
deleteAllSessions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
webui/src/features/settings/AutoDeleteSection.jsx
Normal file
39
webui/src/features/settings/AutoDeleteSection.jsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Trash2 } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function AutoDeleteSection({ t, form, setForm }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card border border-border rounded-xl p-5 space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Trash2 className="w-4 h-4 text-muted-foreground" />
|
||||||
|
<h3 className="font-semibold">{t('settings.autoDeleteTitle')}</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">{t('settings.autoDeleteDesc')}</p>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm font-medium">{t('settings.autoDeleteSessions')}</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={form.auto_delete?.sessions || false}
|
||||||
|
onClick={() => setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
auto_delete: { ...prev.auto_delete, sessions: !prev.auto_delete?.sessions },
|
||||||
|
}))}
|
||||||
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||||
|
form.auto_delete?.sessions ? 'bg-primary' : 'bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||||
|
form.auto_delete?.sessions ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{form.auto_delete?.sessions && (
|
||||||
|
<p className="text-xs text-amber-500 flex items-center gap-1">
|
||||||
|
{t('settings.autoDeleteWarning')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { useSettingsForm } from './useSettingsForm'
|
|||||||
import SecuritySection from './SecuritySection'
|
import SecuritySection from './SecuritySection'
|
||||||
import RuntimeSection from './RuntimeSection'
|
import RuntimeSection from './RuntimeSection'
|
||||||
import BehaviorSection from './BehaviorSection'
|
import BehaviorSection from './BehaviorSection'
|
||||||
|
import AutoDeleteSection from './AutoDeleteSection'
|
||||||
import ModelSection from './ModelSection'
|
import ModelSection from './ModelSection'
|
||||||
import BackupSection from './BackupSection'
|
import BackupSection from './BackupSection'
|
||||||
|
|
||||||
@@ -91,6 +92,8 @@ export default function SettingsContainer({ onRefresh, onMessage, authFetch, onF
|
|||||||
|
|
||||||
<BehaviorSection t={t} form={form} setForm={setForm} />
|
<BehaviorSection t={t} form={form} setForm={setForm} />
|
||||||
|
|
||||||
|
<AutoDeleteSection t={t} form={form} setForm={setForm} />
|
||||||
|
|
||||||
<ModelSection t={t} form={form} setForm={setForm} />
|
<ModelSection t={t} form={form} setForm={setForm} />
|
||||||
|
|
||||||
<BackupSection
|
<BackupSection
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const DEFAULT_FORM = {
|
|||||||
toolcall: { mode: 'feature_match', early_emit_confidence: 'high' },
|
toolcall: { mode: 'feature_match', early_emit_confidence: 'high' },
|
||||||
responses: { store_ttl_seconds: 900 },
|
responses: { store_ttl_seconds: 900 },
|
||||||
embeddings: { provider: '' },
|
embeddings: { provider: '' },
|
||||||
|
auto_delete: { sessions: false },
|
||||||
claude_mapping_text: '{\n "fast": "deepseek-chat",\n "slow": "deepseek-reasoner"\n}',
|
claude_mapping_text: '{\n "fast": "deepseek-chat",\n "slow": "deepseek-reasoner"\n}',
|
||||||
model_aliases_text: '{}',
|
model_aliases_text: '{}',
|
||||||
}
|
}
|
||||||
@@ -55,6 +56,9 @@ function fromServerForm(data) {
|
|||||||
embeddings: {
|
embeddings: {
|
||||||
provider: data.embeddings?.provider || '',
|
provider: data.embeddings?.provider || '',
|
||||||
},
|
},
|
||||||
|
auto_delete: {
|
||||||
|
sessions: Boolean(data.auto_delete?.sessions || false),
|
||||||
|
},
|
||||||
claude_mapping_text: JSON.stringify(data.claude_mapping || {}, null, 2),
|
claude_mapping_text: JSON.stringify(data.claude_mapping || {}, null, 2),
|
||||||
model_aliases_text: JSON.stringify(data.model_aliases || {}, null, 2),
|
model_aliases_text: JSON.stringify(data.model_aliases || {}, null, 2),
|
||||||
}
|
}
|
||||||
@@ -74,6 +78,7 @@ function toServerPayload(form) {
|
|||||||
},
|
},
|
||||||
responses: { store_ttl_seconds: Number(form.responses.store_ttl_seconds) },
|
responses: { store_ttl_seconds: Number(form.responses.store_ttl_seconds) },
|
||||||
embeddings: { provider: String(form.embeddings.provider || '').trim() },
|
embeddings: { provider: String(form.embeddings.provider || '').trim() },
|
||||||
|
auto_delete: { sessions: Boolean(form.auto_delete?.sessions) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,297 +1,305 @@
|
|||||||
{
|
{
|
||||||
"language": {
|
"language": {
|
||||||
"label": "Language",
|
"label": "Language",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
"chinese": "中文"
|
"chinese": "中文"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"accounts": {
|
"accounts": {
|
||||||
"label": "Account Management",
|
"label": "Account Management",
|
||||||
"desc": "Manage the DeepSeek account pool"
|
"desc": "Manage the DeepSeek account pool"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"label": "API Test",
|
"label": "API Test",
|
||||||
"desc": "Test API connectivity and responses"
|
"desc": "Test API connectivity and responses"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"label": "Batch Import",
|
"label": "Batch Import",
|
||||||
"desc": "Bulk import account configuration"
|
"desc": "Bulk import account configuration"
|
||||||
},
|
},
|
||||||
"vercel": {
|
"vercel": {
|
||||||
"label": "Vercel Sync",
|
"label": "Vercel Sync",
|
||||||
"desc": "Sync configuration to Vercel"
|
"desc": "Sync configuration to Vercel"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"label": "Settings",
|
"label": "Settings",
|
||||||
"desc": "Edit runtime and security settings online"
|
"desc": "Edit runtime and security settings online"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"onlineAdminConsole": "Online Admin Console",
|
"onlineAdminConsole": "Online Admin Console",
|
||||||
"systemStatus": "System Status",
|
"systemStatus": "System Status",
|
||||||
"statusOnline": "Online",
|
"statusOnline": "Online",
|
||||||
"accounts": "Accounts",
|
"accounts": "Accounts",
|
||||||
"keys": "Keys",
|
"keys": "Keys",
|
||||||
"signOut": "Sign out"
|
"signOut": "Sign out"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"expired": "Authentication expired. Please sign in again.",
|
"expired": "Authentication expired. Please sign in again.",
|
||||||
"checking": "Checking authentication status..."
|
"checking": "Checking authentication status..."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"fetchConfig": "Failed to fetch configuration: {error}"
|
"fetchConfig": "Failed to fetch configuration: {error}"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"generate": "Generate",
|
"generate": "Generate",
|
||||||
"test": "Test",
|
"test": "Test",
|
||||||
"testing": "Testing...",
|
"testing": "Testing...",
|
||||||
"loading": "Loading..."
|
"loading": "Loading..."
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"deleted": "Deleted successfully",
|
"deleted": "Deleted successfully",
|
||||||
"deleteFailed": "Delete failed",
|
"deleteFailed": "Delete failed",
|
||||||
"failedToAdd": "Failed to add",
|
"failedToAdd": "Failed to add",
|
||||||
"networkError": "Network error.",
|
"networkError": "Network error.",
|
||||||
"requestFailed": "Request failed.",
|
"requestFailed": "Request failed.",
|
||||||
"generationStopped": "Generation stopped.",
|
"generationStopped": "Generation stopped.",
|
||||||
"invalidJson": "Invalid JSON format.",
|
"invalidJson": "Invalid JSON format.",
|
||||||
"importFailed": "Import failed.",
|
"importFailed": "Import failed.",
|
||||||
"copyFailed": "Copy failed."
|
"copyFailed": "Copy failed."
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"adminConsole": "Admin Console",
|
"adminConsole": "Admin Console",
|
||||||
"apiStatus": "API Status",
|
"apiStatus": "API Status",
|
||||||
"features": {
|
"features": {
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"title": "Full Compatibility",
|
"title": "Full Compatibility",
|
||||||
"desc": "OpenAI & Claude format support"
|
"desc": "OpenAI & Claude format support"
|
||||||
},
|
},
|
||||||
"loadBalancing": {
|
"loadBalancing": {
|
||||||
"title": "Load Balancing",
|
"title": "Load Balancing",
|
||||||
"desc": "Smart rotation with stable throughput"
|
"desc": "Smart rotation with stable throughput"
|
||||||
},
|
},
|
||||||
"reasoning": {
|
"reasoning": {
|
||||||
"title": "Deep Reasoning",
|
"title": "Deep Reasoning",
|
||||||
"desc": "Expose reasoning traces when enabled"
|
"desc": "Expose reasoning traces when enabled"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "Web Search",
|
"title": "Web Search",
|
||||||
"desc": "Integrated native web search"
|
"desc": "Integrated native web search"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"accountManager": {
|
"accountManager": {
|
||||||
"addKeySuccess": "API key added successfully.",
|
"addKeySuccess": "API key added successfully.",
|
||||||
"addAccountSuccess": "Account added successfully.",
|
"addAccountSuccess": "Account added successfully.",
|
||||||
"requiredFields": "Password and email/mobile are required.",
|
"requiredFields": "Password and email/mobile are required.",
|
||||||
"deleteKeyConfirm": "Are you sure you want to delete this API key?",
|
"deleteKeyConfirm": "Are you sure you want to delete this API key?",
|
||||||
"deleteAccountConfirm": "Are you sure you want to delete this account?",
|
"deleteAccountConfirm": "Are you sure you want to delete this account?",
|
||||||
"invalidIdentifier": "Invalid account identifier. Operation aborted.",
|
"invalidIdentifier": "Invalid account identifier. Operation aborted.",
|
||||||
"testAllConfirm": "Test API connectivity for all accounts?",
|
"testAllConfirm": "Test API connectivity for all accounts?",
|
||||||
"testAllCompleted": "Completed: {success}/{total} available",
|
"testAllCompleted": "Completed: {success}/{total} available",
|
||||||
"testFailed": "Test failed: {error}",
|
"testFailed": "Test failed: {error}",
|
||||||
"available": "Available",
|
"available": "Available",
|
||||||
"inUse": "In use",
|
"inUse": "In use",
|
||||||
"totalPool": "Total pool",
|
"totalPool": "Total pool",
|
||||||
"accountsUnit": "accounts",
|
"accountsUnit": "accounts",
|
||||||
"threadsUnit": "threads",
|
"threadsUnit": "threads",
|
||||||
"apiKeysTitle": "API Keys",
|
"apiKeysTitle": "API Keys",
|
||||||
"apiKeysDesc": "Manage the API access key pool",
|
"apiKeysDesc": "Manage the API access key pool",
|
||||||
"addKey": "Add key",
|
"addKey": "Add key",
|
||||||
"copied": "Copied",
|
"copied": "Copied",
|
||||||
"copyKeyTitle": "Copy key",
|
"copyKeyTitle": "Copy key",
|
||||||
"deleteKeyTitle": "Delete key",
|
"deleteKeyTitle": "Delete key",
|
||||||
"noApiKeys": "No API keys found.",
|
"noApiKeys": "No API keys found.",
|
||||||
"accountsTitle": "DeepSeek Accounts",
|
"accountsTitle": "DeepSeek Accounts",
|
||||||
"accountsDesc": "Manage the DeepSeek account pool",
|
"accountsDesc": "Manage the DeepSeek account pool",
|
||||||
"testAll": "Test all",
|
"testAll": "Test all",
|
||||||
"addAccount": "Add account",
|
"addAccount": "Add account",
|
||||||
"testingAllAccounts": "Testing all accounts...",
|
"testingAllAccounts": "Testing all accounts...",
|
||||||
"sessionActive": "Session active",
|
"sessionActive": "Session active",
|
||||||
"reauthRequired": "Re-auth required",
|
"reauthRequired": "Re-auth required",
|
||||||
"testStatusFailed": "Last test failed",
|
"testStatusFailed": "Last test failed",
|
||||||
"noAccounts": "No accounts found.",
|
"noAccounts": "No accounts found.",
|
||||||
"modalAddKeyTitle": "Add API key",
|
"modalAddKeyTitle": "Add API key",
|
||||||
"newKeyLabel": "New key value",
|
"newKeyLabel": "New key value",
|
||||||
"newKeyPlaceholder": "Enter a custom API key",
|
"newKeyPlaceholder": "Enter a custom API key",
|
||||||
"generate": "Generate",
|
"generate": "Generate",
|
||||||
"generateHint": "Click Generate to create a random key.",
|
"generateHint": "Click Generate to create a random key.",
|
||||||
"addKeyLoading": "Adding...",
|
"addKeyLoading": "Adding...",
|
||||||
"addKeyAction": "Add key",
|
"addKeyAction": "Add key",
|
||||||
"modalAddAccountTitle": "Add DeepSeek account",
|
"modalAddAccountTitle": "Add DeepSeek account",
|
||||||
"emailOptional": "Email (optional)",
|
"emailOptional": "Email (optional)",
|
||||||
"mobileOptional": "Mobile (optional)",
|
"mobileOptional": "Mobile (optional)",
|
||||||
"passwordLabel": "Password",
|
"passwordLabel": "Password",
|
||||||
"passwordPlaceholder": "Account password",
|
"passwordPlaceholder": "Account password",
|
||||||
"addAccountLoading": "Adding...",
|
"addAccountLoading": "Adding...",
|
||||||
"addAccountAction": "Add account",
|
"addAccountAction": "Add account",
|
||||||
"pageInfo": "Page {current}/{total}, {count} accounts total",
|
"pageInfo": "Page {current}/{total}, {count} accounts total",
|
||||||
"searchPlaceholder": "Search accounts...",
|
"searchPlaceholder": "Search accounts...",
|
||||||
"searchNoResults": "No accounts match your search"
|
"searchNoResults": "No accounts match your search",
|
||||||
},
|
"sessionCount": "Sessions: {count}",
|
||||||
"apiTester": {
|
"deleteAllSessions": "Delete all sessions",
|
||||||
"defaultMessage": "Hello, please introduce yourself in one sentence.",
|
"deleteAllSessionsConfirm": "Are you sure you want to delete all sessions for this account? This action cannot be undone.",
|
||||||
"models": {
|
"deleteAllSessionsSuccess": "Successfully deleted all sessions"
|
||||||
"chat": "Non-reasoning model",
|
},
|
||||||
"reasoner": "Reasoning model",
|
"apiTester": {
|
||||||
"chatSearch": "Non-reasoning model (with search)",
|
"defaultMessage": "Hello, please introduce yourself in one sentence.",
|
||||||
"reasonerSearch": "Reasoning model (with search)"
|
"models": {
|
||||||
},
|
"chat": "Non-reasoning model",
|
||||||
"missingApiKey": "Please provide an API key.",
|
"reasoner": "Reasoning model",
|
||||||
"requestFailed": "Request failed.",
|
"chatSearch": "Non-reasoning model (with search)",
|
||||||
"networkError": "Network error: {error}",
|
"reasonerSearch": "Reasoning model (with search)"
|
||||||
"testSuccess": "{account}: Test successful ({time}ms)",
|
},
|
||||||
"config": "Configuration",
|
"missingApiKey": "Please provide an API key.",
|
||||||
"modelLabel": "Model",
|
"requestFailed": "Request failed.",
|
||||||
"streamMode": "Streaming",
|
"networkError": "Network error: {error}",
|
||||||
"accountSelector": "Account",
|
"testSuccess": "{account}: Test successful ({time}ms)",
|
||||||
"autoRandom": "🤖 Auto / Random",
|
"config": "Configuration",
|
||||||
"apiKeyOptional": "API Key (optional)",
|
"modelLabel": "Model",
|
||||||
"apiKeyDefault": "Default: ...{suffix}",
|
"streamMode": "Streaming",
|
||||||
"apiKeyPlaceholder": "Enter a custom key",
|
"accountSelector": "Account",
|
||||||
"modeManaged": "Managed key mode (uses account pool).",
|
"autoRandom": "🤖 Auto / Random",
|
||||||
"modeDirect": "Direct token mode (requires a valid DeepSeek token).",
|
"apiKeyOptional": "API Key (optional)",
|
||||||
"statusError": "Error",
|
"apiKeyDefault": "Default: ...{suffix}",
|
||||||
"reasoningTrace": "Reasoning Trace",
|
"apiKeyPlaceholder": "Enter a custom key",
|
||||||
"generating": "Generating response...",
|
"modeManaged": "Managed key mode (uses account pool).",
|
||||||
"enterMessage": "Enter a message...",
|
"modeDirect": "Direct token mode (requires a valid DeepSeek token).",
|
||||||
"adminConsoleLabel": "DeepSeek admin console"
|
"statusError": "Error",
|
||||||
},
|
"reasoningTrace": "Reasoning Trace",
|
||||||
"batchImport": {
|
"generating": "Generating response...",
|
||||||
"templates": {
|
"enterMessage": "Enter a message...",
|
||||||
"full": {
|
"adminConsoleLabel": "DeepSeek admin console"
|
||||||
"name": "Full configuration template",
|
},
|
||||||
"desc": "Includes keys, accounts, and model mapping"
|
"batchImport": {
|
||||||
},
|
"templates": {
|
||||||
"emailOnly": {
|
"full": {
|
||||||
"name": "Email-only accounts",
|
"name": "Full configuration template",
|
||||||
"desc": "Batch import accounts using email login"
|
"desc": "Includes keys, accounts, and model mapping"
|
||||||
},
|
},
|
||||||
"mobileOnly": {
|
"emailOnly": {
|
||||||
"name": "Mobile-only accounts",
|
"name": "Email-only accounts",
|
||||||
"desc": "Batch import accounts using mobile login"
|
"desc": "Batch import accounts using email login"
|
||||||
},
|
},
|
||||||
"keysOnly": {
|
"mobileOnly": {
|
||||||
"name": "API keys only",
|
"name": "Mobile-only accounts",
|
||||||
"desc": "Add API access keys only"
|
"desc": "Batch import accounts using mobile login"
|
||||||
}
|
},
|
||||||
},
|
"keysOnly": {
|
||||||
"enterJson": "Please provide JSON configuration content.",
|
"name": "API keys only",
|
||||||
"importSuccess": "Import successful: {keys} keys, {accounts} accounts",
|
"desc": "Add API access keys only"
|
||||||
"templateLoaded": "Template loaded: {name}",
|
}
|
||||||
"currentConfigLoaded": "Current configuration loaded.",
|
},
|
||||||
"fetchConfigFailed": "Failed to fetch configuration.",
|
"enterJson": "Please provide JSON configuration content.",
|
||||||
"copySuccess": "Base64 configuration copied to clipboard.",
|
"importSuccess": "Import successful: {keys} keys, {accounts} accounts",
|
||||||
"quickTemplates": "Quick Templates",
|
"templateLoaded": "Template loaded: {name}",
|
||||||
"dataExport": "Data Export",
|
"currentConfigLoaded": "Current configuration loaded.",
|
||||||
"dataExportDesc": "Copy the Base64-encoded configuration for Vercel environment variables.",
|
"fetchConfigFailed": "Failed to fetch configuration.",
|
||||||
"copyBase64": "Copy Base64 config",
|
"copySuccess": "Base64 configuration copied to clipboard.",
|
||||||
"copied": "Copied",
|
"quickTemplates": "Quick Templates",
|
||||||
"variableName": "Variable name",
|
"dataExport": "Data Export",
|
||||||
"jsonEditor": "JSON Editor",
|
"dataExportDesc": "Copy the Base64-encoded configuration for Vercel environment variables.",
|
||||||
"loadCurrentConfig": "Load current config",
|
"copyBase64": "Copy Base64 config",
|
||||||
"applyConfig": "Apply config",
|
"copied": "Copied",
|
||||||
"importing": "Importing...",
|
"variableName": "Variable name",
|
||||||
"importComplete": "Import complete",
|
"jsonEditor": "JSON Editor",
|
||||||
"importSummary": "Imported {keys} API keys and updated {accounts} accounts."
|
"loadCurrentConfig": "Load current config",
|
||||||
},
|
"applyConfig": "Apply config",
|
||||||
"settings": {
|
"importing": "Importing...",
|
||||||
"loadFailed": "Failed to load settings.",
|
"importComplete": "Import complete",
|
||||||
"nonJsonResponse": "Unexpected non-JSON response from server (status: {status}).",
|
"importSummary": "Imported {keys} API keys and updated {accounts} accounts."
|
||||||
"save": "Save settings",
|
},
|
||||||
"saving": "Saving...",
|
"settings": {
|
||||||
"saveSuccess": "Settings saved and hot reloaded.",
|
"loadFailed": "Failed to load settings.",
|
||||||
"saveFailed": "Failed to save settings.",
|
"nonJsonResponse": "Unexpected non-JSON response from server (status: {status}).",
|
||||||
"securityTitle": "Security",
|
"save": "Save settings",
|
||||||
"jwtExpireHours": "JWT expiry (hours)",
|
"saving": "Saving...",
|
||||||
"newPassword": "New admin password",
|
"saveSuccess": "Settings saved and hot reloaded.",
|
||||||
"newPasswordPlaceholder": "Enter new password (min 4 chars)",
|
"saveFailed": "Failed to save settings.",
|
||||||
"updatePassword": "Update password",
|
"securityTitle": "Security",
|
||||||
"updating": "Updating...",
|
"jwtExpireHours": "JWT expiry (hours)",
|
||||||
"passwordTooShort": "Password must be at least 4 characters.",
|
"newPassword": "New admin password",
|
||||||
"passwordUpdated": "Password updated. Please sign in again.",
|
"newPasswordPlaceholder": "Enter new password (min 4 chars)",
|
||||||
"passwordUpdateFailed": "Failed to update password.",
|
"updatePassword": "Update password",
|
||||||
"runtimeTitle": "Concurrency & Queue",
|
"updating": "Updating...",
|
||||||
"accountMaxInflight": "Per-account max inflight",
|
"passwordTooShort": "Password must be at least 4 characters.",
|
||||||
"accountMaxQueue": "Account max queue size",
|
"passwordUpdated": "Password updated. Please sign in again.",
|
||||||
"globalMaxInflight": "Global max inflight",
|
"passwordUpdateFailed": "Failed to update password.",
|
||||||
"behaviorTitle": "Behavior",
|
"runtimeTitle": "Concurrency & Queue",
|
||||||
"toolcallMode": "Toolcall mode",
|
"accountMaxInflight": "Per-account max inflight",
|
||||||
"earlyEmitConfidence": "Early emit confidence",
|
"accountMaxQueue": "Account max queue size",
|
||||||
"responsesTTL": "Responses store TTL (seconds)",
|
"globalMaxInflight": "Global max inflight",
|
||||||
"embeddingsProvider": "Embeddings provider",
|
"behaviorTitle": "Behavior",
|
||||||
"modelTitle": "Model mapping",
|
"toolcallMode": "Toolcall mode",
|
||||||
"claudeMapping": "Claude mapping (JSON)",
|
"earlyEmitConfidence": "Early emit confidence",
|
||||||
"modelAliases": "Model aliases (JSON)",
|
"responsesTTL": "Responses store TTL (seconds)",
|
||||||
"backupTitle": "Backup & Restore",
|
"embeddingsProvider": "Embeddings provider",
|
||||||
"loadExport": "Load current export",
|
"modelTitle": "Model mapping",
|
||||||
"importModeMerge": "Merge import (default)",
|
"claudeMapping": "Claude mapping (JSON)",
|
||||||
"importModeReplace": "Replace all import",
|
"modelAliases": "Model aliases (JSON)",
|
||||||
"importNow": "Import now",
|
"autoDeleteTitle": "Auto Delete Sessions",
|
||||||
"importing": "Importing...",
|
"autoDeleteDesc": "When enabled, all sessions will be automatically deleted after each request completes.",
|
||||||
"importPlaceholder": "Paste config JSON to import",
|
"autoDeleteSessions": "Auto delete sessions",
|
||||||
"importEmpty": "Please input import JSON.",
|
"autoDeleteWarning": "Warning: Enabling this will delete all session history after each request. Use with caution.",
|
||||||
"importInvalidJson": "Import JSON is invalid.",
|
"backupTitle": "Backup & Restore",
|
||||||
"importFailed": "Import failed.",
|
"loadExport": "Load current export",
|
||||||
"importSuccess": "Config imported (mode: {mode}).",
|
"importModeMerge": "Merge import (default)",
|
||||||
"exportFailed": "Export failed.",
|
"importModeReplace": "Replace all import",
|
||||||
"exportLoaded": "Current export loaded.",
|
"importNow": "Import now",
|
||||||
"exportJson": "Export JSON",
|
"importing": "Importing...",
|
||||||
"invalidJsonField": "{field} is not a valid JSON object.",
|
"importPlaceholder": "Paste config JSON to import",
|
||||||
"defaultPasswordWarning": "You are using the default admin password \"admin\". Please change it.",
|
"importEmpty": "Please input import JSON.",
|
||||||
"vercelSyncHint": "Configuration changed. For Vercel deployments, sync manually in Vercel Sync and redeploy.",
|
"importInvalidJson": "Import JSON is invalid.",
|
||||||
"autoFetchPaused": "Auto loading paused after {count} failures: {error}",
|
"importFailed": "Import failed.",
|
||||||
"retryLoad": "Retry now"
|
"importSuccess": "Config imported (mode: {mode}).",
|
||||||
},
|
"exportFailed": "Export failed.",
|
||||||
"login": {
|
"exportLoaded": "Current export loaded.",
|
||||||
"welcome": "Welcome back",
|
"exportJson": "Export JSON",
|
||||||
"subtitle": "Enter your admin key to continue",
|
"invalidJsonField": "{field} is not a valid JSON object.",
|
||||||
"adminKeyLabel": "Admin key",
|
"defaultPasswordWarning": "You are using the default admin password \"admin\". Please change it.",
|
||||||
"adminKeyPlaceholder": "Enter your admin key...",
|
"vercelSyncHint": "Configuration changed. For Vercel deployments, sync manually in Vercel Sync and redeploy.",
|
||||||
"rememberSession": "Remember this session",
|
"autoFetchPaused": "Auto loading paused after {count} failures: {error}",
|
||||||
"signIn": "Sign in",
|
"retryLoad": "Retry now"
|
||||||
"secureConnection": "Secure connection",
|
},
|
||||||
"adminPortal": "DS2API admin portal",
|
"login": {
|
||||||
"signInFailed": "Sign-in failed.",
|
"welcome": "Welcome back",
|
||||||
"networkError": "Network error: {error}"
|
"subtitle": "Enter your admin key to continue",
|
||||||
},
|
"adminKeyLabel": "Admin key",
|
||||||
"vercel": {
|
"adminKeyPlaceholder": "Enter your admin key...",
|
||||||
"tokenRequired": "Vercel access token is required.",
|
"rememberSession": "Remember this session",
|
||||||
"projectRequired": "Project ID is required.",
|
"signIn": "Sign in",
|
||||||
"syncFailed": "Sync failed.",
|
"secureConnection": "Secure connection",
|
||||||
"networkError": "Network error.",
|
"adminPortal": "DS2API admin portal",
|
||||||
"title": "Vercel Deployment",
|
"signInFailed": "Sign-in failed.",
|
||||||
"description": "Sync the current keys and accounts directly to Vercel environment variables.",
|
"networkError": "Network error: {error}"
|
||||||
"tokenLabel": "Vercel Access Token",
|
},
|
||||||
"getToken": "Get token",
|
"vercel": {
|
||||||
"tokenPlaceholderPreconfig": "Using preconfigured token",
|
"tokenRequired": "Vercel access token is required.",
|
||||||
"tokenPlaceholder": "Enter Vercel access token",
|
"projectRequired": "Project ID is required.",
|
||||||
"projectIdLabel": "Project ID",
|
"syncFailed": "Sync failed.",
|
||||||
"projectIdHint": "Find it in Project Settings → General.",
|
"networkError": "Network error.",
|
||||||
"teamIdLabel": "Team ID",
|
"title": "Vercel Deployment",
|
||||||
"optional": "optional",
|
"description": "Sync the current keys and accounts directly to Vercel environment variables.",
|
||||||
"syncing": "Syncing...",
|
"tokenLabel": "Vercel Access Token",
|
||||||
"syncRedeploy": "Sync & redeploy",
|
"getToken": "Get token",
|
||||||
"redeployHint": "This triggers a Vercel redeploy and usually takes 30–60 seconds.",
|
"tokenPlaceholderPreconfig": "Using preconfigured token",
|
||||||
"syncSucceeded": "Sync succeeded",
|
"tokenPlaceholder": "Enter Vercel access token",
|
||||||
"syncFailedLabel": "Sync failed",
|
"projectIdLabel": "Project ID",
|
||||||
"openDeployment": "Open deployment",
|
"projectIdHint": "Find it in Project Settings → General.",
|
||||||
"statusSynced": "Synced",
|
"teamIdLabel": "Team ID",
|
||||||
"statusNotSynced": "Not synced",
|
"optional": "optional",
|
||||||
"statusNeverSynced": "Never synced",
|
"syncing": "Syncing...",
|
||||||
"lastSyncTime": "Last sync: {time}",
|
"syncRedeploy": "Sync & redeploy",
|
||||||
"pollPaused": "Status polling paused after {count} failures.",
|
"redeployHint": "This triggers a Vercel redeploy and usually takes 30–60 seconds.",
|
||||||
"manualRefresh": "Refresh manually",
|
"syncSucceeded": "Sync succeeded",
|
||||||
"howItWorks": "How it works",
|
"syncFailedLabel": "Sync failed",
|
||||||
"steps": {
|
"openDeployment": "Open deployment",
|
||||||
"one": "The current configuration (keys and accounts) is exported as JSON.",
|
"statusSynced": "Synced",
|
||||||
"two": "The JSON is Base64-encoded for safe formatting.",
|
"statusNotSynced": "Not synced",
|
||||||
"three": "Update the env var in Vercel:",
|
"statusNeverSynced": "Never synced",
|
||||||
"four": "Trigger a redeploy to apply the updated environment variables."
|
"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,297 +1,305 @@
|
|||||||
{
|
{
|
||||||
"language": {
|
"language": {
|
||||||
"label": "语言",
|
"label": "语言",
|
||||||
"english": "English",
|
"english": "English",
|
||||||
"chinese": "中文"
|
"chinese": "中文"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"accounts": {
|
"accounts": {
|
||||||
"label": "账号管理",
|
"label": "账号管理",
|
||||||
"desc": "管理 DeepSeek 账号池"
|
"desc": "管理 DeepSeek 账号池"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"label": "API 测试",
|
"label": "API 测试",
|
||||||
"desc": "测试 API 连接与响应"
|
"desc": "测试 API 连接与响应"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"label": "批量导入",
|
"label": "批量导入",
|
||||||
"desc": "批量导入账号配置"
|
"desc": "批量导入账号配置"
|
||||||
},
|
},
|
||||||
"vercel": {
|
"vercel": {
|
||||||
"label": "Vercel 同步",
|
"label": "Vercel 同步",
|
||||||
"desc": "同步配置到 Vercel"
|
"desc": "同步配置到 Vercel"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"label": "设置中心",
|
"label": "设置中心",
|
||||||
"desc": "在线修改系统设置与配置"
|
"desc": "在线修改系统设置与配置"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"onlineAdminConsole": "在线管理面板",
|
"onlineAdminConsole": "在线管理面板",
|
||||||
"systemStatus": "系统状态",
|
"systemStatus": "系统状态",
|
||||||
"statusOnline": "在线",
|
"statusOnline": "在线",
|
||||||
"accounts": "账号",
|
"accounts": "账号",
|
||||||
"keys": "密钥",
|
"keys": "密钥",
|
||||||
"signOut": "退出登录"
|
"signOut": "退出登录"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"expired": "认证已过期,请重新登录",
|
"expired": "认证已过期,请重新登录",
|
||||||
"checking": "正在检查登录状态..."
|
"checking": "正在检查登录状态..."
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"fetchConfig": "获取配置失败: {error}"
|
"fetchConfig": "获取配置失败: {error}"
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"generate": "生成",
|
"generate": "生成",
|
||||||
"test": "测试",
|
"test": "测试",
|
||||||
"testing": "正在测试...",
|
"testing": "正在测试...",
|
||||||
"loading": "加载中..."
|
"loading": "加载中..."
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"deleted": "删除成功",
|
"deleted": "删除成功",
|
||||||
"deleteFailed": "删除失败",
|
"deleteFailed": "删除失败",
|
||||||
"failedToAdd": "添加失败",
|
"failedToAdd": "添加失败",
|
||||||
"networkError": "网络错误",
|
"networkError": "网络错误",
|
||||||
"requestFailed": "请求失败",
|
"requestFailed": "请求失败",
|
||||||
"generationStopped": "已停止生成",
|
"generationStopped": "已停止生成",
|
||||||
"invalidJson": "无效的 JSON 格式",
|
"invalidJson": "无效的 JSON 格式",
|
||||||
"importFailed": "导入失败",
|
"importFailed": "导入失败",
|
||||||
"copyFailed": "复制失败"
|
"copyFailed": "复制失败"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"adminConsole": "管理面板",
|
"adminConsole": "管理面板",
|
||||||
"apiStatus": "API 状态",
|
"apiStatus": "API 状态",
|
||||||
"features": {
|
"features": {
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"title": "全面兼容",
|
"title": "全面兼容",
|
||||||
"desc": "适配 OpenAI 与 Claude 格式"
|
"desc": "适配 OpenAI 与 Claude 格式"
|
||||||
},
|
},
|
||||||
"loadBalancing": {
|
"loadBalancing": {
|
||||||
"title": "负载均衡",
|
"title": "负载均衡",
|
||||||
"desc": "智能轮询,稳定高效"
|
"desc": "智能轮询,稳定高效"
|
||||||
},
|
},
|
||||||
"reasoning": {
|
"reasoning": {
|
||||||
"title": "深度思考",
|
"title": "深度思考",
|
||||||
"desc": "支持推理过程输出"
|
"desc": "支持推理过程输出"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"title": "联网搜索",
|
"title": "联网搜索",
|
||||||
"desc": "集成原生网页搜索能力"
|
"desc": "集成原生网页搜索能力"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"accountManager": {
|
"accountManager": {
|
||||||
"addKeySuccess": "API 密钥添加成功",
|
"addKeySuccess": "API 密钥添加成功",
|
||||||
"addAccountSuccess": "账号添加成功",
|
"addAccountSuccess": "账号添加成功",
|
||||||
"requiredFields": "需要填写密码以及邮箱或手机号",
|
"requiredFields": "需要填写密码以及邮箱或手机号",
|
||||||
"deleteKeyConfirm": "确定要删除此 API 密钥吗?",
|
"deleteKeyConfirm": "确定要删除此 API 密钥吗?",
|
||||||
"deleteAccountConfirm": "确定要删除此账号吗?",
|
"deleteAccountConfirm": "确定要删除此账号吗?",
|
||||||
"invalidIdentifier": "账号标识无效,无法执行操作",
|
"invalidIdentifier": "账号标识无效,无法执行操作",
|
||||||
"testAllConfirm": "测试所有账号的 API 连通性?",
|
"testAllConfirm": "测试所有账号的 API 连通性?",
|
||||||
"testAllCompleted": "完成:{success}/{total} 可用",
|
"testAllCompleted": "完成:{success}/{total} 可用",
|
||||||
"testFailed": "测试失败: {error}",
|
"testFailed": "测试失败: {error}",
|
||||||
"available": "可用",
|
"available": "可用",
|
||||||
"inUse": "正在使用",
|
"inUse": "正在使用",
|
||||||
"totalPool": "账号池总数",
|
"totalPool": "账号池总数",
|
||||||
"accountsUnit": "个账号",
|
"accountsUnit": "个账号",
|
||||||
"threadsUnit": "线程",
|
"threadsUnit": "线程",
|
||||||
"apiKeysTitle": "API 密钥",
|
"apiKeysTitle": "API 密钥",
|
||||||
"apiKeysDesc": "管理 API 访问密钥池",
|
"apiKeysDesc": "管理 API 访问密钥池",
|
||||||
"addKey": "添加密钥",
|
"addKey": "添加密钥",
|
||||||
"copied": "已复制",
|
"copied": "已复制",
|
||||||
"copyKeyTitle": "复制密钥",
|
"copyKeyTitle": "复制密钥",
|
||||||
"deleteKeyTitle": "删除密钥",
|
"deleteKeyTitle": "删除密钥",
|
||||||
"noApiKeys": "未找到 API 密钥",
|
"noApiKeys": "未找到 API 密钥",
|
||||||
"accountsTitle": "DeepSeek 账号",
|
"accountsTitle": "DeepSeek 账号",
|
||||||
"accountsDesc": "管理 DeepSeek 账号池",
|
"accountsDesc": "管理 DeepSeek 账号池",
|
||||||
"testAll": "测试全部",
|
"testAll": "测试全部",
|
||||||
"addAccount": "添加账号",
|
"addAccount": "添加账号",
|
||||||
"testingAllAccounts": "正在测试所有账号...",
|
"testingAllAccounts": "正在测试所有账号...",
|
||||||
"sessionActive": "已建立会话",
|
"sessionActive": "已建立会话",
|
||||||
"reauthRequired": "需重新登录",
|
"reauthRequired": "需重新登录",
|
||||||
"testStatusFailed": "上次测试失败",
|
"testStatusFailed": "上次测试失败",
|
||||||
"noAccounts": "未找到任何账号",
|
"noAccounts": "未找到任何账号",
|
||||||
"modalAddKeyTitle": "添加 API 密钥",
|
"modalAddKeyTitle": "添加 API 密钥",
|
||||||
"newKeyLabel": "新密钥值",
|
"newKeyLabel": "新密钥值",
|
||||||
"newKeyPlaceholder": "输入自定义 API 密钥",
|
"newKeyPlaceholder": "输入自定义 API 密钥",
|
||||||
"generate": "生成",
|
"generate": "生成",
|
||||||
"generateHint": "点击「生成」自动创建随机密钥",
|
"generateHint": "点击「生成」自动创建随机密钥",
|
||||||
"addKeyLoading": "添加中...",
|
"addKeyLoading": "添加中...",
|
||||||
"addKeyAction": "添加密钥",
|
"addKeyAction": "添加密钥",
|
||||||
"modalAddAccountTitle": "添加 DeepSeek 账号",
|
"modalAddAccountTitle": "添加 DeepSeek 账号",
|
||||||
"emailOptional": "邮箱 (可选)",
|
"emailOptional": "邮箱 (可选)",
|
||||||
"mobileOptional": "手机号 (可选)",
|
"mobileOptional": "手机号 (可选)",
|
||||||
"passwordLabel": "密码",
|
"passwordLabel": "密码",
|
||||||
"passwordPlaceholder": "账号密码",
|
"passwordPlaceholder": "账号密码",
|
||||||
"addAccountLoading": "添加中...",
|
"addAccountLoading": "添加中...",
|
||||||
"addAccountAction": "添加账号",
|
"addAccountAction": "添加账号",
|
||||||
"pageInfo": "第 {current}/{total} 页,共 {count} 个账号",
|
"pageInfo": "第 {current}/{total} 页,共 {count} 个账号",
|
||||||
"searchPlaceholder": "搜索账号...",
|
"searchPlaceholder": "搜索账号...",
|
||||||
"searchNoResults": "未找到匹配的账号"
|
"searchNoResults": "未找到匹配的账号",
|
||||||
},
|
"sessionCount": "会话: {count}",
|
||||||
"apiTester": {
|
"deleteAllSessions": "删除所有会话",
|
||||||
"defaultMessage": "你好,请用一句话介绍你自己。",
|
"deleteAllSessionsConfirm": "确定要删除该账号的所有会话吗?此操作不可恢复。",
|
||||||
"models": {
|
"deleteAllSessionsSuccess": "删除成功"
|
||||||
"chat": "非思考模型",
|
},
|
||||||
"reasoner": "思考模型",
|
"apiTester": {
|
||||||
"chatSearch": "非思考模型 (带搜索)",
|
"defaultMessage": "你好,请用一句话介绍你自己。",
|
||||||
"reasonerSearch": "思考模型 (带搜索)"
|
"models": {
|
||||||
},
|
"chat": "非思考模型",
|
||||||
"missingApiKey": "请提供 API 密钥",
|
"reasoner": "思考模型",
|
||||||
"requestFailed": "请求失败",
|
"chatSearch": "非思考模型 (带搜索)",
|
||||||
"networkError": "网络错误: {error}",
|
"reasonerSearch": "思考模型 (带搜索)"
|
||||||
"testSuccess": "{account}: 测试成功 ({time}ms)",
|
},
|
||||||
"config": "配置",
|
"missingApiKey": "请提供 API 密钥",
|
||||||
"modelLabel": "模型",
|
"requestFailed": "请求失败",
|
||||||
"streamMode": "流式模式",
|
"networkError": "网络错误: {error}",
|
||||||
"accountSelector": "选择账号",
|
"testSuccess": "{account}: 测试成功 ({time}ms)",
|
||||||
"autoRandom": "🤖 自动 / 随机",
|
"config": "配置",
|
||||||
"apiKeyOptional": "API 密钥 (可选)",
|
"modelLabel": "模型",
|
||||||
"apiKeyDefault": "默认: ...{suffix}",
|
"streamMode": "流式模式",
|
||||||
"apiKeyPlaceholder": "输入自定义密钥",
|
"accountSelector": "选择账号",
|
||||||
"modeManaged": "当前使用托管 key 模式(会走账号池)。",
|
"autoRandom": "🤖 自动 / 随机",
|
||||||
"modeDirect": "当前使用直通 token 模式(需填写有效 DeepSeek token)。",
|
"apiKeyOptional": "API 密钥 (可选)",
|
||||||
"statusError": "错误",
|
"apiKeyDefault": "默认: ...{suffix}",
|
||||||
"reasoningTrace": "思维链过程",
|
"apiKeyPlaceholder": "输入自定义密钥",
|
||||||
"generating": "正在生成响应...",
|
"modeManaged": "当前使用托管 key 模式(会走账号池)。",
|
||||||
"enterMessage": "输入消息...",
|
"modeDirect": "当前使用直通 token 模式(需填写有效 DeepSeek token)。",
|
||||||
"adminConsoleLabel": "DeepSeek 管理员界面"
|
"statusError": "错误",
|
||||||
},
|
"reasoningTrace": "思维链过程",
|
||||||
"batchImport": {
|
"generating": "正在生成响应...",
|
||||||
"templates": {
|
"enterMessage": "输入消息...",
|
||||||
"full": {
|
"adminConsoleLabel": "DeepSeek 管理员界面"
|
||||||
"name": "全量配置模板",
|
},
|
||||||
"desc": "包含密钥、账号及模型映射"
|
"batchImport": {
|
||||||
},
|
"templates": {
|
||||||
"emailOnly": {
|
"full": {
|
||||||
"name": "仅邮箱账号",
|
"name": "全量配置模板",
|
||||||
"desc": "批量导入邮箱格式账号"
|
"desc": "包含密钥、账号及模型映射"
|
||||||
},
|
},
|
||||||
"mobileOnly": {
|
"emailOnly": {
|
||||||
"name": "仅手机号账号",
|
"name": "仅邮箱账号",
|
||||||
"desc": "批量导入手机号格式账号"
|
"desc": "批量导入邮箱格式账号"
|
||||||
},
|
},
|
||||||
"keysOnly": {
|
"mobileOnly": {
|
||||||
"name": "仅 API 密钥",
|
"name": "仅手机号账号",
|
||||||
"desc": "仅添加 API 访问密钥"
|
"desc": "批量导入手机号格式账号"
|
||||||
}
|
},
|
||||||
},
|
"keysOnly": {
|
||||||
"enterJson": "请输入 JSON 配置内容",
|
"name": "仅 API 密钥",
|
||||||
"importSuccess": "导入成功: {keys} 个密钥, {accounts} 个账号",
|
"desc": "仅添加 API 访问密钥"
|
||||||
"templateLoaded": "已加载模板: {name}",
|
}
|
||||||
"currentConfigLoaded": "当前配置已加载",
|
},
|
||||||
"fetchConfigFailed": "获取配置失败",
|
"enterJson": "请输入 JSON 配置内容",
|
||||||
"copySuccess": "Base64 配置已复制到剪贴板",
|
"importSuccess": "导入成功: {keys} 个密钥, {accounts} 个账号",
|
||||||
"quickTemplates": "快速模板",
|
"templateLoaded": "已加载模板: {name}",
|
||||||
"dataExport": "数据导出",
|
"currentConfigLoaded": "当前配置已加载",
|
||||||
"dataExportDesc": "获取配置的 Base64 字符串,用于 Vercel 环境变量。",
|
"fetchConfigFailed": "获取配置失败",
|
||||||
"copyBase64": "复制 Base64 配置",
|
"copySuccess": "Base64 配置已复制到剪贴板",
|
||||||
"copied": "已复制",
|
"quickTemplates": "快速模板",
|
||||||
"variableName": "变量名",
|
"dataExport": "数据导出",
|
||||||
"jsonEditor": "JSON 编辑器",
|
"dataExportDesc": "获取配置的 Base64 字符串,用于 Vercel 环境变量。",
|
||||||
"loadCurrentConfig": "加载当前配置",
|
"copyBase64": "复制 Base64 配置",
|
||||||
"applyConfig": "应用配置",
|
"copied": "已复制",
|
||||||
"importing": "正在导入...",
|
"variableName": "变量名",
|
||||||
"importComplete": "导入操作已完成",
|
"jsonEditor": "JSON 编辑器",
|
||||||
"importSummary": "成功导入了 {keys} 个 API 密钥,并更新了 {accounts} 个账号。"
|
"loadCurrentConfig": "加载当前配置",
|
||||||
},
|
"applyConfig": "应用配置",
|
||||||
"settings": {
|
"importing": "正在导入...",
|
||||||
"loadFailed": "加载设置失败",
|
"importComplete": "导入操作已完成",
|
||||||
"nonJsonResponse": "服务端返回了非 JSON 响应(状态码:{status})",
|
"importSummary": "成功导入了 {keys} 个 API 密钥,并更新了 {accounts} 个账号。"
|
||||||
"save": "保存设置",
|
},
|
||||||
"saving": "保存中...",
|
"settings": {
|
||||||
"saveSuccess": "设置已保存并热更新生效",
|
"loadFailed": "加载设置失败",
|
||||||
"saveFailed": "保存设置失败",
|
"nonJsonResponse": "服务端返回了非 JSON 响应(状态码:{status})",
|
||||||
"securityTitle": "安全设置",
|
"save": "保存设置",
|
||||||
"jwtExpireHours": "JWT 有效期(小时)",
|
"saving": "保存中...",
|
||||||
"newPassword": "面板新密码",
|
"saveSuccess": "设置已保存并热更新生效",
|
||||||
"newPasswordPlaceholder": "输入新密码(至少 4 位)",
|
"saveFailed": "保存设置失败",
|
||||||
"updatePassword": "修改密码",
|
"securityTitle": "安全设置",
|
||||||
"updating": "更新中...",
|
"jwtExpireHours": "JWT 有效期(小时)",
|
||||||
"passwordTooShort": "新密码至少 4 位",
|
"newPassword": "面板新密码",
|
||||||
"passwordUpdated": "密码已更新,需重新登录",
|
"newPasswordPlaceholder": "输入新密码(至少 4 位)",
|
||||||
"passwordUpdateFailed": "密码更新失败",
|
"updatePassword": "修改密码",
|
||||||
"runtimeTitle": "并发与队列",
|
"updating": "更新中...",
|
||||||
"accountMaxInflight": "每账号并发上限",
|
"passwordTooShort": "新密码至少 4 位",
|
||||||
"accountMaxQueue": "账号等待队列上限",
|
"passwordUpdated": "密码已更新,需重新登录",
|
||||||
"globalMaxInflight": "全局并发上限",
|
"passwordUpdateFailed": "密码更新失败",
|
||||||
"behaviorTitle": "行为设置",
|
"runtimeTitle": "并发与队列",
|
||||||
"toolcallMode": "Toolcall 模式",
|
"accountMaxInflight": "每账号并发上限",
|
||||||
"earlyEmitConfidence": "早发置信度",
|
"accountMaxQueue": "账号等待队列上限",
|
||||||
"responsesTTL": "Responses 缓存 TTL(秒)",
|
"globalMaxInflight": "全局并发上限",
|
||||||
"embeddingsProvider": "Embeddings Provider",
|
"behaviorTitle": "行为设置",
|
||||||
"modelTitle": "模型映射",
|
"toolcallMode": "Toolcall 模式",
|
||||||
"claudeMapping": "Claude 映射(JSON)",
|
"earlyEmitConfidence": "早发置信度",
|
||||||
"modelAliases": "模型别名(JSON)",
|
"responsesTTL": "Responses 缓存 TTL(秒)",
|
||||||
"backupTitle": "备份与恢复",
|
"embeddingsProvider": "Embeddings Provider",
|
||||||
"loadExport": "加载当前导出",
|
"modelTitle": "模型映射",
|
||||||
"importModeMerge": "合并导入(默认)",
|
"claudeMapping": "Claude 映射(JSON)",
|
||||||
"importModeReplace": "全量覆盖导入",
|
"modelAliases": "模型别名(JSON)",
|
||||||
"importNow": "立即导入",
|
"autoDeleteTitle": "自动删除会话",
|
||||||
"importing": "导入中...",
|
"autoDeleteDesc": "开启后,每次请求完成后会自动删除该账号的所有会话记录。",
|
||||||
"importPlaceholder": "粘贴要导入的 JSON 配置",
|
"autoDeleteSessions": "自动删除会话",
|
||||||
"importEmpty": "请先输入导入 JSON",
|
"autoDeleteWarning": "开启此功能后,每次请求完成都会删除该账号的所有历史会话,请谨慎使用。",
|
||||||
"importInvalidJson": "导入 JSON 格式无效",
|
"backupTitle": "备份与恢复",
|
||||||
"importFailed": "导入失败",
|
"loadExport": "加载当前导出",
|
||||||
"importSuccess": "配置导入成功(模式:{mode})",
|
"importModeMerge": "合并导入(默认)",
|
||||||
"exportFailed": "导出失败",
|
"importModeReplace": "全量覆盖导入",
|
||||||
"exportLoaded": "已加载当前配置导出",
|
"importNow": "立即导入",
|
||||||
"exportJson": "导出 JSON",
|
"importing": "导入中...",
|
||||||
"invalidJsonField": "{field} 不是有效 JSON 对象",
|
"importPlaceholder": "粘贴要导入的 JSON 配置",
|
||||||
"defaultPasswordWarning": "当前使用默认密码 admin,请尽快在此修改。",
|
"importEmpty": "请先输入导入 JSON",
|
||||||
"vercelSyncHint": "当前配置已更新。Vercel 部署请到 Vercel 同步页面手动同步并重部署。",
|
"importInvalidJson": "导入 JSON 格式无效",
|
||||||
"autoFetchPaused": "自动加载已暂停:连续失败 {count} 次({error})",
|
"importFailed": "导入失败",
|
||||||
"retryLoad": "立即重试"
|
"importSuccess": "配置导入成功(模式:{mode})",
|
||||||
},
|
"exportFailed": "导出失败",
|
||||||
"login": {
|
"exportLoaded": "已加载当前配置导出",
|
||||||
"welcome": "欢迎回来",
|
"exportJson": "导出 JSON",
|
||||||
"subtitle": "请输入管理员密钥以继续",
|
"invalidJsonField": "{field} 不是有效 JSON 对象",
|
||||||
"adminKeyLabel": "管理员密钥",
|
"defaultPasswordWarning": "当前使用默认密码 admin,请尽快在此修改。",
|
||||||
"adminKeyPlaceholder": "输入您的管理员密钥...",
|
"vercelSyncHint": "当前配置已更新。Vercel 部署请到 Vercel 同步页面手动同步并重部署。",
|
||||||
"rememberSession": "记住登录状态",
|
"autoFetchPaused": "自动加载已暂停:连续失败 {count} 次({error})",
|
||||||
"signIn": "登录",
|
"retryLoad": "立即重试"
|
||||||
"secureConnection": "安全连接",
|
},
|
||||||
"adminPortal": "DS2API 管理员门户",
|
"login": {
|
||||||
"signInFailed": "登录失败",
|
"welcome": "欢迎回来",
|
||||||
"networkError": "网络错误: {error}"
|
"subtitle": "请输入管理员密钥以继续",
|
||||||
},
|
"adminKeyLabel": "管理员密钥",
|
||||||
"vercel": {
|
"adminKeyPlaceholder": "输入您的管理员密钥...",
|
||||||
"tokenRequired": "需要 Vercel 访问令牌",
|
"rememberSession": "记住登录状态",
|
||||||
"projectRequired": "需要项目 ID",
|
"signIn": "登录",
|
||||||
"syncFailed": "同步失败",
|
"secureConnection": "安全连接",
|
||||||
"networkError": "网络错误",
|
"adminPortal": "DS2API 管理员门户",
|
||||||
"title": "Vercel 部署",
|
"signInFailed": "登录失败",
|
||||||
"description": "将当前密钥和账号配置直接同步到 Vercel 环境变量中。",
|
"networkError": "网络错误: {error}"
|
||||||
"tokenLabel": "Vercel 访问令牌",
|
},
|
||||||
"getToken": "获取令牌",
|
"vercel": {
|
||||||
"tokenPlaceholderPreconfig": "正在使用预配置的令牌",
|
"tokenRequired": "需要 Vercel 访问令牌",
|
||||||
"tokenPlaceholder": "输入 Vercel 访问令牌",
|
"projectRequired": "需要项目 ID",
|
||||||
"projectIdLabel": "项目 ID",
|
"syncFailed": "同步失败",
|
||||||
"projectIdHint": "可在项目设置 (Project Settings) → 常规 (General) 中找到",
|
"networkError": "网络错误",
|
||||||
"teamIdLabel": "团队 ID",
|
"title": "Vercel 部署",
|
||||||
"optional": "可选",
|
"description": "将当前密钥和账号配置直接同步到 Vercel 环境变量中。",
|
||||||
"syncing": "正在同步...",
|
"tokenLabel": "Vercel 访问令牌",
|
||||||
"syncRedeploy": "同步并重新部署",
|
"getToken": "获取令牌",
|
||||||
"redeployHint": "这将触发 Vercel 的重新部署,大约需要 30-60 秒。",
|
"tokenPlaceholderPreconfig": "正在使用预配置的令牌",
|
||||||
"syncSucceeded": "同步成功",
|
"tokenPlaceholder": "输入 Vercel 访问令牌",
|
||||||
"syncFailedLabel": "同步失败",
|
"projectIdLabel": "项目 ID",
|
||||||
"openDeployment": "访问部署地址",
|
"projectIdHint": "可在项目设置 (Project Settings) → 常规 (General) 中找到",
|
||||||
"statusSynced": "已同步",
|
"teamIdLabel": "团队 ID",
|
||||||
"statusNotSynced": "未同步",
|
"optional": "可选",
|
||||||
"statusNeverSynced": "从未同步",
|
"syncing": "正在同步...",
|
||||||
"lastSyncTime": "上次同步: {time}",
|
"syncRedeploy": "同步并重新部署",
|
||||||
"pollPaused": "状态轮询已暂停:连续失败 {count} 次。",
|
"redeployHint": "这将触发 Vercel 的重新部署,大约需要 30-60 秒。",
|
||||||
"manualRefresh": "手动刷新",
|
"syncSucceeded": "同步成功",
|
||||||
"howItWorks": "工作原理",
|
"syncFailedLabel": "同步失败",
|
||||||
"steps": {
|
"openDeployment": "访问部署地址",
|
||||||
"one": "当前配置 (密钥和账号) 被导出为 JSON 字符串。",
|
"statusSynced": "已同步",
|
||||||
"two": "JSON 被编码为 Base64 以确保格式兼容性。",
|
"statusNotSynced": "未同步",
|
||||||
"three": "更新 Vercel 项目中的环境变量:",
|
"statusNeverSynced": "从未同步",
|
||||||
"four": "触发重新部署以应用新的环境变量。"
|
"lastSyncTime": "上次同步: {time}",
|
||||||
}
|
"pollPaused": "状态轮询已暂停:连续失败 {count} 次。",
|
||||||
}
|
"manualRefresh": "手动刷新",
|
||||||
}
|
"howItWorks": "工作原理",
|
||||||
|
"steps": {
|
||||||
|
"one": "当前配置 (密钥和账号) 被导出为 JSON 字符串。",
|
||||||
|
"two": "JSON 被编码为 Base64 以确保格式兼容性。",
|
||||||
|
"three": "更新 Vercel 项目中的环境变量:",
|
||||||
|
"four": "触发重新部署以应用新的环境变量。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user