mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
feat: add configurable auto-delete modes (none, single, all) for remote chat sessions
This commit is contained in:
@@ -643,7 +643,7 @@ Reads runtime settings and status, including:
|
||||
- `admin` (`has_password_hash`, `jwt_expire_hours`, `jwt_valid_after_unix`, `default_password_warning`)
|
||||
- `runtime` (`account_max_inflight`, `account_max_queue`, `global_max_inflight`, `token_refresh_interval_hours`)
|
||||
- `responses` / `embeddings`
|
||||
- `auto_delete` (`sessions`)
|
||||
- `auto_delete` (`mode`: `none` / `single` / `all`; legacy `sessions=true` is still treated as `all`)
|
||||
- `claude_mapping` / `model_aliases`
|
||||
- `env_backed`, `needs_vercel_sync`
|
||||
- `toolcall` policy is fixed to `feature_match + high` and is no longer returned or editable via settings
|
||||
@@ -656,7 +656,7 @@ Hot-updates runtime settings. Supported fields:
|
||||
- `runtime.account_max_inflight` / `runtime.account_max_queue` / `runtime.global_max_inflight` / `runtime.token_refresh_interval_hours`
|
||||
- `responses.store_ttl_seconds`
|
||||
- `embeddings.provider`
|
||||
- `auto_delete.sessions`
|
||||
- `auto_delete.mode`
|
||||
- `claude_mapping`
|
||||
- `model_aliases`
|
||||
- `toolcall` policy is fixed and is no longer writable through settings
|
||||
|
||||
4
API.md
4
API.md
@@ -652,7 +652,7 @@ data: {"type":"message_stop"}
|
||||
- `admin`(`has_password_hash`、`jwt_expire_hours`、`jwt_valid_after_unix`、`default_password_warning`)
|
||||
- `runtime`(`account_max_inflight`、`account_max_queue`、`global_max_inflight`、`token_refresh_interval_hours`)
|
||||
- `responses` / `embeddings`
|
||||
- `auto_delete`(`sessions`)
|
||||
- `auto_delete`(`mode`:`none` / `single` / `all`;旧配置 `sessions=true` 仍按 `all` 处理)
|
||||
- `claude_mapping` / `model_aliases`
|
||||
- `env_backed`、`needs_vercel_sync`
|
||||
- `toolcall` 策略已固定为 `feature_match + high`,不再通过 settings 返回或修改
|
||||
@@ -665,7 +665,7 @@ data: {"type":"message_stop"}
|
||||
- `runtime.account_max_inflight` / `runtime.account_max_queue` / `runtime.global_max_inflight` / `runtime.token_refresh_interval_hours`
|
||||
- `responses.store_ttl_seconds`
|
||||
- `embeddings.provider`
|
||||
- `auto_delete.sessions`
|
||||
- `auto_delete.mode`
|
||||
- `claude_mapping`
|
||||
- `model_aliases`
|
||||
- `toolcall` 策略已固定,不再作为可写入字段
|
||||
|
||||
@@ -328,7 +328,7 @@ cp opencode.json.example opencode.json
|
||||
- `claude_mapping`:字典中 `fast`/`slow` 后缀映射到对应 DeepSeek 模型(兼容读取 `claude_model_mapping`)
|
||||
- `admin`:管理后台设置(JWT 过期时间、密码哈希等),可通过 Admin Settings API 热更新
|
||||
- `runtime`:运行时参数(并发限制、队列大小、托管账号 token 刷新间隔),可通过 Admin Settings API 热更新;`account_max_queue=0`/`global_max_inflight=0` 表示按推荐值自动计算,`token_refresh_interval_hours=6` 为默认强制重登间隔
|
||||
- `auto_delete.sessions`:是否在请求结束后自动清理 DeepSeek 会话(默认 `false`,可在 Settings 热更新)
|
||||
- `auto_delete.mode`:请求结束后如何清理 DeepSeek 远端聊天记录,支持 `none`(默认,不删除)、`single`(仅删除当前会话)、`all`(清空全部会话);旧配置里的 `auto_delete.sessions=true` 仍会被视为 `all`
|
||||
|
||||
### 环境变量
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ cp opencode.json.example opencode.json
|
||||
- `claude_mapping`: Maps `fast`/`slow` suffixes to corresponding DeepSeek models (still compatible with `claude_model_mapping`)
|
||||
- `admin`: Admin panel settings (JWT expiry, password hash, etc.), hot-reloadable via Admin Settings API
|
||||
- `runtime`: Runtime parameters (concurrency limits, queue sizes, managed token refresh interval), hot-reloadable via Admin Settings API; `account_max_queue=0`/`global_max_inflight=0` means auto-calculate from recommended values, `token_refresh_interval_hours=6` is the default forced re-login interval
|
||||
- `auto_delete.sessions`: Whether to auto-delete DeepSeek sessions after request completion (default `false`, hot-reloadable via Settings)
|
||||
- `auto_delete.mode`: How to clean up DeepSeek remote chat records after each request completes. Supported values: `none` (default, no deletion), `single` (delete only the current session), `all` (delete all sessions); legacy `auto_delete.sessions=true` is still treated as `all`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type DeepSeekCaller interface {
|
||||
CreateSession(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)
|
||||
DeleteSessionForToken(ctx context.Context, token string, sessionID string) (*deepseek.DeleteSessionResult, error)
|
||||
DeleteAllSessionsForToken(ctx context.Context, token string) error
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ type ConfigReader interface {
|
||||
ToolcallEarlyEmitConfidence() string
|
||||
ResponsesStoreTTLSeconds() int
|
||||
EmbeddingsProvider() string
|
||||
AutoDeleteMode() string
|
||||
AutoDeleteSessions() bool
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,13 @@ package openai
|
||||
import "testing"
|
||||
|
||||
type mockOpenAIConfig struct {
|
||||
aliases map[string]string
|
||||
wideInput bool
|
||||
toolMode string
|
||||
earlyEmit string
|
||||
responsesTTL int
|
||||
embedProv string
|
||||
aliases map[string]string
|
||||
wideInput bool
|
||||
autoDeleteMode string
|
||||
toolMode string
|
||||
earlyEmit string
|
||||
responsesTTL int
|
||||
embedProv string
|
||||
}
|
||||
|
||||
func (m mockOpenAIConfig) ModelAliases() map[string]string { return m.aliases }
|
||||
@@ -20,7 +21,13 @@ func (m mockOpenAIConfig) ToolcallMode() string { return m.toolMo
|
||||
func (m mockOpenAIConfig) ToolcallEarlyEmitConfidence() string { return m.earlyEmit }
|
||||
func (m mockOpenAIConfig) ResponsesStoreTTLSeconds() int { return m.responsesTTL }
|
||||
func (m mockOpenAIConfig) EmbeddingsProvider() string { return m.embedProv }
|
||||
func (m mockOpenAIConfig) AutoDeleteSessions() bool { return false }
|
||||
func (m mockOpenAIConfig) AutoDeleteMode() string {
|
||||
if m.autoDeleteMode == "" {
|
||||
return "none"
|
||||
}
|
||||
return m.autoDeleteMode
|
||||
}
|
||||
func (m mockOpenAIConfig) AutoDeleteSessions() bool { return false }
|
||||
|
||||
func TestNormalizeOpenAIChatRequestWithConfigInterface(t *testing.T) {
|
||||
cfg := mockOpenAIConfig{
|
||||
|
||||
@@ -35,22 +35,9 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) {
|
||||
writeOpenAIError(w, status, detail)
|
||||
return
|
||||
}
|
||||
var sessionID string
|
||||
defer func() {
|
||||
// 自动删除会话(同步)
|
||||
// 必须在 Release 之前同步删除,否则:
|
||||
// 1. 异步删除时账号已被 Release
|
||||
// 2. 新请求可能获取到同一账号并开始使用
|
||||
// 3. 异步删除仍在进行,会截断新请求正在使用的会话
|
||||
if h.Store.AutoDeleteSessions() && a.DeepSeekToken != "" {
|
||||
deleteCtx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
err := h.DS.DeleteAllSessionsForToken(deleteCtx, 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.autoDeleteRemoteSession(r.Context(), a, sessionID)
|
||||
h.Auth.Release(a)
|
||||
}()
|
||||
|
||||
@@ -67,7 +54,7 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, err := h.DS.CreateSession(r.Context(), a, 3)
|
||||
sessionID, err = h.DS.CreateSession(r.Context(), a, 3)
|
||||
if err != nil {
|
||||
if a.UseConfigToken {
|
||||
writeOpenAIError(w, http.StatusUnauthorized, "Account token is invalid. Please re-login the account in admin.")
|
||||
@@ -94,6 +81,38 @@ func (h *Handler) ChatCompletions(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleNonStream(w, r.Context(), resp, sessionID, stdReq.ResponseModel, stdReq.FinalPrompt, stdReq.Thinking, stdReq.ToolNames)
|
||||
}
|
||||
|
||||
func (h *Handler) autoDeleteRemoteSession(ctx context.Context, a *auth.RequestAuth, sessionID string) {
|
||||
mode := h.Store.AutoDeleteMode()
|
||||
if mode == "none" || a.DeepSeekToken == "" {
|
||||
return
|
||||
}
|
||||
|
||||
deleteCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
switch mode {
|
||||
case "single":
|
||||
if sessionID == "" {
|
||||
config.Logger.Warn("[auto_delete_sessions] skipped single-session delete because session_id is empty", "account", a.AccountID)
|
||||
return
|
||||
}
|
||||
_, err := h.DS.DeleteSessionForToken(deleteCtx, a.DeepSeekToken, sessionID)
|
||||
if err != nil {
|
||||
config.Logger.Warn("[auto_delete_sessions] failed", "account", a.AccountID, "mode", mode, "session_id", sessionID, "error", err)
|
||||
return
|
||||
}
|
||||
config.Logger.Debug("[auto_delete_sessions] success", "account", a.AccountID, "mode", mode, "session_id", sessionID)
|
||||
case "all":
|
||||
if err := h.DS.DeleteAllSessionsForToken(deleteCtx, a.DeepSeekToken); err != nil {
|
||||
config.Logger.Warn("[auto_delete_sessions] failed", "account", a.AccountID, "mode", mode, "error", err)
|
||||
return
|
||||
}
|
||||
config.Logger.Debug("[auto_delete_sessions] success", "account", a.AccountID, "mode", mode)
|
||||
default:
|
||||
config.Logger.Warn("[auto_delete_sessions] unknown mode", "account", a.AccountID, "mode", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleNonStream(w http.ResponseWriter, ctx context.Context, resp *http.Response, completionID, model, finalPrompt string, thinkingEnabled bool, toolNames []string) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
defer resp.Body.Close()
|
||||
|
||||
95
internal/adapter/openai/handler_chat_auto_delete_test.go
Normal file
95
internal/adapter/openai/handler_chat_auto_delete_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"ds2api/internal/auth"
|
||||
"ds2api/internal/deepseek"
|
||||
)
|
||||
|
||||
type autoDeleteModeDSStub struct {
|
||||
resp *http.Response
|
||||
singleCalls int
|
||||
allCalls int
|
||||
lastSessionID string
|
||||
}
|
||||
|
||||
func (m *autoDeleteModeDSStub) CreateSession(_ context.Context, _ *auth.RequestAuth, _ int) (string, error) {
|
||||
return "session-id", nil
|
||||
}
|
||||
|
||||
func (m *autoDeleteModeDSStub) GetPow(_ context.Context, _ *auth.RequestAuth, _ int) (string, error) {
|
||||
return "pow", nil
|
||||
}
|
||||
|
||||
func (m *autoDeleteModeDSStub) CallCompletion(_ context.Context, _ *auth.RequestAuth, _ map[string]any, _ string, _ int) (*http.Response, error) {
|
||||
return m.resp, nil
|
||||
}
|
||||
|
||||
func (m *autoDeleteModeDSStub) DeleteSessionForToken(_ context.Context, _ string, sessionID string) (*deepseek.DeleteSessionResult, error) {
|
||||
m.singleCalls++
|
||||
m.lastSessionID = sessionID
|
||||
return &deepseek.DeleteSessionResult{SessionID: sessionID, Success: true}, nil
|
||||
}
|
||||
|
||||
func (m *autoDeleteModeDSStub) DeleteAllSessionsForToken(_ context.Context, _ string) error {
|
||||
m.allCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestChatCompletionsAutoDeleteModes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode string
|
||||
wantSingle int
|
||||
wantAll int
|
||||
}{
|
||||
{name: "none", mode: "none"},
|
||||
{name: "single", mode: "single", wantSingle: 1},
|
||||
{name: "all", mode: "all", wantAll: 1},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ds := &autoDeleteModeDSStub{
|
||||
resp: makeOpenAISSEHTTPResponse(
|
||||
`data: {"p":"response/content","v":"hello"}`,
|
||||
"data: [DONE]",
|
||||
),
|
||||
}
|
||||
h := &Handler{
|
||||
Store: mockOpenAIConfig{
|
||||
wideInput: true,
|
||||
autoDeleteMode: tc.mode,
|
||||
},
|
||||
Auth: streamStatusAuthStub{},
|
||||
DS: ds,
|
||||
}
|
||||
|
||||
reqBody := `{"model":"deepseek-chat","messages":[{"role":"user","content":"hi"}],"stream":false}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(reqBody))
|
||||
req.Header.Set("Authorization", "Bearer direct-token")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
h.ChatCompletions(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if ds.singleCalls != tc.wantSingle {
|
||||
t.Fatalf("single delete calls=%d want=%d", ds.singleCalls, tc.wantSingle)
|
||||
}
|
||||
if ds.allCalls != tc.wantAll {
|
||||
t.Fatalf("all delete calls=%d want=%d", ds.allCalls, tc.wantAll)
|
||||
}
|
||||
if tc.wantSingle > 0 && ds.lastSessionID != "session-id" {
|
||||
t.Fatalf("expected single delete for session-id, got %q", ds.lastSessionID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
chimw "github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"ds2api/internal/auth"
|
||||
"ds2api/internal/deepseek"
|
||||
)
|
||||
|
||||
type streamStatusAuthStub struct{}
|
||||
@@ -53,6 +54,10 @@ func (m streamStatusDSStub) CallCompletion(_ context.Context, _ *auth.RequestAut
|
||||
return m.resp, nil
|
||||
}
|
||||
|
||||
func (m streamStatusDSStub) DeleteSessionForToken(_ context.Context, _ string, _ string) (*deepseek.DeleteSessionResult, error) {
|
||||
return &deepseek.DeleteSessionResult{Success: true}, nil
|
||||
}
|
||||
|
||||
func (m streamStatusDSStub) DeleteAllSessionsForToken(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type ConfigStore interface {
|
||||
RuntimeAccountMaxQueue(defaultSize int) int
|
||||
RuntimeGlobalMaxInflight(defaultSize int) int
|
||||
RuntimeTokenRefreshIntervalHours() int
|
||||
AutoDeleteMode() string
|
||||
CompatStripReferenceMarkers() bool
|
||||
AutoDeleteSessions() bool
|
||||
}
|
||||
|
||||
@@ -141,6 +141,17 @@ func parseSettingsUpdateRequest(req map[string]any) (*config.AdminConfig, *confi
|
||||
|
||||
if raw, ok := req["auto_delete"].(map[string]any); ok {
|
||||
cfg := &config.AutoDeleteConfig{}
|
||||
if v, exists := raw["mode"]; exists {
|
||||
mode := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", v)))
|
||||
switch mode {
|
||||
case "", "none":
|
||||
cfg.Mode = "none"
|
||||
case "single", "all":
|
||||
cfg.Mode = mode
|
||||
default:
|
||||
return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("auto_delete.mode must be one of none, single, all")
|
||||
}
|
||||
}
|
||||
if v, exists := raw["sessions"]; exists {
|
||||
cfg.Sessions = boolFrom(v)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,31 @@ func TestUpdateSettingsWithoutRuntimeSkipsMergedRuntimeValidation(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSettingsAutoDeleteMode(t *testing.T) {
|
||||
h := newAdminTestHandler(t, `{"keys":["k1"],"auto_delete":{"sessions":true}}`)
|
||||
|
||||
payload := map[string]any{
|
||||
"auto_delete": map[string]any{
|
||||
"mode": "single",
|
||||
},
|
||||
}
|
||||
b, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest(http.MethodPut, "/admin/settings", bytes.NewReader(b))
|
||||
rec := httptest.NewRecorder()
|
||||
h.updateSettings(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
snap := h.Store.Snapshot()
|
||||
if got := snap.AutoDelete.Mode; got != "single" {
|
||||
t.Fatalf("auto_delete.mode=%q want=single", got)
|
||||
}
|
||||
if got := h.Store.AutoDeleteMode(); got != "single" {
|
||||
t.Fatalf("AutoDeleteMode()=%q want=single", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSettingsHotReloadRuntime(t *testing.T) {
|
||||
h := newAdminTestHandler(t, `{
|
||||
"keys":["k1"],
|
||||
|
||||
@@ -64,6 +64,7 @@ func (h *Handler) updateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
c.Embeddings.Provider = strings.TrimSpace(embeddingsCfg.Provider)
|
||||
}
|
||||
if autoDeleteCfg != nil {
|
||||
c.AutoDelete.Mode = autoDeleteCfg.Mode
|
||||
c.AutoDelete.Sessions = autoDeleteCfg.Sessions
|
||||
}
|
||||
if claudeMap != nil {
|
||||
|
||||
@@ -77,5 +77,6 @@ type EmbeddingsConfig struct {
|
||||
}
|
||||
|
||||
type AutoDeleteConfig struct {
|
||||
Sessions bool `json:"sessions"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Sessions bool `json:"sessions,omitempty"`
|
||||
}
|
||||
|
||||
@@ -106,6 +106,9 @@ func TestConfigJSONRoundtrip(t *testing.T) {
|
||||
"fast": "deepseek-chat",
|
||||
"slow": "deepseek-reasoner",
|
||||
},
|
||||
AutoDelete: AutoDeleteConfig{
|
||||
Mode: "single",
|
||||
},
|
||||
Runtime: RuntimeConfig{
|
||||
TokenRefreshIntervalHours: 12,
|
||||
},
|
||||
@@ -142,6 +145,9 @@ func TestConfigJSONRoundtrip(t *testing.T) {
|
||||
if decoded.Runtime.TokenRefreshIntervalHours != 12 {
|
||||
t.Fatalf("unexpected runtime refresh interval: %#v", decoded.Runtime.TokenRefreshIntervalHours)
|
||||
}
|
||||
if decoded.AutoDelete.Mode != "single" {
|
||||
t.Fatalf("unexpected auto delete mode: %#v", decoded.AutoDelete.Mode)
|
||||
}
|
||||
if decoded.Compat.WideInputStrictOutput == nil || !*decoded.Compat.WideInputStrictOutput {
|
||||
t.Fatalf("unexpected compat wide_input_strict_output: %#v", decoded.Compat.WideInputStrictOutput)
|
||||
}
|
||||
@@ -156,6 +162,29 @@ func TestConfigJSONRoundtrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoDeleteModeResolution(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg AutoDeleteConfig
|
||||
want string
|
||||
}{
|
||||
{name: "default", cfg: AutoDeleteConfig{}, want: "none"},
|
||||
{name: "legacy all", cfg: AutoDeleteConfig{Sessions: true}, want: "all"},
|
||||
{name: "single", cfg: AutoDeleteConfig{Mode: "single"}, want: "single"},
|
||||
{name: "all", cfg: AutoDeleteConfig{Mode: "all"}, want: "all"},
|
||||
{name: "none", cfg: AutoDeleteConfig{Mode: "none"}, want: "none"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
store := &Store{cfg: Config{AutoDelete: tc.cfg}}
|
||||
if got := store.AutoDeleteMode(); got != tc.want {
|
||||
t.Fatalf("AutoDeleteMode()=%q want=%q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigUnmarshalJSONPreservesUnknownFields(t *testing.T) {
|
||||
raw := `{"keys":["k1"],"accounts":[],"my_custom_field":"hello","number_field":42}`
|
||||
var cfg Config
|
||||
|
||||
@@ -74,6 +74,20 @@ func (s *Store) EmbeddingsProvider() string {
|
||||
return strings.TrimSpace(s.cfg.Embeddings.Provider)
|
||||
}
|
||||
|
||||
func (s *Store) AutoDeleteMode() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
mode := strings.ToLower(strings.TrimSpace(s.cfg.AutoDelete.Mode))
|
||||
switch mode {
|
||||
case "none", "single", "all":
|
||||
return mode
|
||||
}
|
||||
if s.cfg.AutoDelete.Sessions {
|
||||
return "all"
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
|
||||
func (s *Store) AdminPasswordHash() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@@ -173,7 +187,5 @@ func (s *Store) RuntimeTokenRefreshIntervalHours() int {
|
||||
}
|
||||
|
||||
func (s *Store) AutoDeleteSessions() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cfg.AutoDelete.Sessions
|
||||
return s.AutoDeleteMode() != "none"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Trash2 } from 'lucide-react'
|
||||
|
||||
export default function AutoDeleteSection({ t, form, setForm }) {
|
||||
const mode = form.auto_delete?.mode || 'none'
|
||||
const descKey = mode === 'single'
|
||||
? 'settings.autoDeleteSingleDesc'
|
||||
: mode === 'all'
|
||||
? 'settings.autoDeleteAllDesc'
|
||||
: 'settings.autoDeleteNoneDesc'
|
||||
|
||||
return (
|
||||
<div className="bg-card border border-border rounded-xl p-5 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -8,28 +15,25 @@ export default function AutoDeleteSection({ t, form, setForm }) {
|
||||
<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) => ({
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">{t('settings.autoDeleteMode')}</label>
|
||||
<select
|
||||
value={mode}
|
||||
onChange={(e) => setForm((prev) => ({
|
||||
...prev,
|
||||
auto_delete: { ...prev.auto_delete, sessions: !prev.auto_delete?.sessions },
|
||||
auto_delete: { ...(prev.auto_delete || {}), mode: e.target.value },
|
||||
}))}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
form.auto_delete?.sessions ? 'bg-primary' : 'bg-muted'
|
||||
}`}
|
||||
className="w-full rounded-lg border border-border bg-muted px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<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>
|
||||
<option value="none">{t('settings.autoDeleteNone')}</option>
|
||||
<option value="single">{t('settings.autoDeleteSingle')}</option>
|
||||
<option value="all">{t('settings.autoDeleteAll')}</option>
|
||||
</select>
|
||||
</div>
|
||||
{form.auto_delete?.sessions && (
|
||||
<p className={`text-xs ${mode === 'none' ? 'text-muted-foreground' : 'text-amber-500'}`}>
|
||||
{t(descKey)}
|
||||
</p>
|
||||
{mode !== 'none' && (
|
||||
<p className="text-xs text-amber-500 flex items-center gap-1">
|
||||
{t('settings.autoDeleteWarning')}
|
||||
</p>
|
||||
|
||||
@@ -16,7 +16,7 @@ const DEFAULT_FORM = {
|
||||
compat: { strip_reference_markers: true },
|
||||
responses: { store_ttl_seconds: 900 },
|
||||
embeddings: { provider: '' },
|
||||
auto_delete: { sessions: false },
|
||||
auto_delete: { mode: 'none' },
|
||||
claude_mapping_text: '{\n "fast": "deepseek-chat",\n "slow": "deepseek-reasoner"\n}',
|
||||
model_aliases_text: '{}',
|
||||
}
|
||||
@@ -38,6 +38,17 @@ function parseJSONMap(raw, fieldName, t) {
|
||||
return parsed
|
||||
}
|
||||
|
||||
function normalizeAutoDeleteMode(raw) {
|
||||
const mode = String(raw?.mode || '').trim().toLowerCase()
|
||||
if (mode === 'none' || mode === 'single' || mode === 'all') {
|
||||
return mode
|
||||
}
|
||||
if (Boolean(raw?.sessions)) {
|
||||
return 'all'
|
||||
}
|
||||
return 'none'
|
||||
}
|
||||
|
||||
function fromServerForm(data) {
|
||||
return {
|
||||
admin: { jwt_expire_hours: Number(data.admin?.jwt_expire_hours || 24) },
|
||||
@@ -57,7 +68,7 @@ function fromServerForm(data) {
|
||||
provider: data.embeddings?.provider || '',
|
||||
},
|
||||
auto_delete: {
|
||||
sessions: Boolean(data.auto_delete?.sessions || false),
|
||||
mode: normalizeAutoDeleteMode(data.auto_delete),
|
||||
},
|
||||
claude_mapping_text: JSON.stringify(data.claude_mapping || {}, null, 2),
|
||||
model_aliases_text: JSON.stringify(data.model_aliases || {}, null, 2),
|
||||
@@ -78,7 +89,7 @@ function toServerPayload(form) {
|
||||
},
|
||||
responses: { store_ttl_seconds: Number(form.responses.store_ttl_seconds) },
|
||||
embeddings: { provider: String(form.embeddings.provider || '').trim() },
|
||||
auto_delete: { sessions: Boolean(form.auto_delete?.sessions) },
|
||||
auto_delete: { mode: normalizeAutoDeleteMode(form.auto_delete) },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,10 +242,16 @@
|
||||
"modelTitle": "Model mapping",
|
||||
"claudeMapping": "Claude mapping (JSON)",
|
||||
"modelAliases": "Model aliases (JSON)",
|
||||
"autoDeleteTitle": "Auto Delete Sessions",
|
||||
"autoDeleteDesc": "When enabled, all sessions will be automatically deleted after each request completes.",
|
||||
"autoDeleteSessions": "Auto delete sessions",
|
||||
"autoDeleteWarning": "Warning: Enabling this will delete all session history after each request. Use with caution.",
|
||||
"autoDeleteTitle": "Session Cleanup Policy",
|
||||
"autoDeleteDesc": "Choose how DeepSeek remote chat records are cleaned up after each request completes.",
|
||||
"autoDeleteMode": "Deletion mode",
|
||||
"autoDeleteNone": "Do not delete",
|
||||
"autoDeleteSingle": "Delete current session",
|
||||
"autoDeleteAll": "Delete all sessions",
|
||||
"autoDeleteNoneDesc": "Keep the remote session after the request completes.",
|
||||
"autoDeleteSingleDesc": "Delete only the remote session created by this request.",
|
||||
"autoDeleteAllDesc": "Delete every remote session for the account after the request completes.",
|
||||
"autoDeleteWarning": "This mode deletes remote chat records. Use with caution.",
|
||||
"backupTitle": "Backup & Restore",
|
||||
"loadExport": "Load current export",
|
||||
"downloadExport": "Download backup file",
|
||||
|
||||
@@ -242,10 +242,16 @@
|
||||
"modelTitle": "模型映射",
|
||||
"claudeMapping": "Claude 映射(JSON)",
|
||||
"modelAliases": "模型别名(JSON)",
|
||||
"autoDeleteTitle": "自动删除会话",
|
||||
"autoDeleteDesc": "开启后,每次请求完成后会自动删除该账号的所有会话记录。",
|
||||
"autoDeleteSessions": "自动删除会话",
|
||||
"autoDeleteWarning": "开启此功能后,每次请求完成都会删除该账号的所有历史会话,请谨慎使用。",
|
||||
"autoDeleteTitle": "会话删除策略",
|
||||
"autoDeleteDesc": "选择每次请求完成后如何清理 DeepSeek 远端聊天记录。",
|
||||
"autoDeleteMode": "删除模式",
|
||||
"autoDeleteNone": "不开启删除",
|
||||
"autoDeleteSingle": "仅删除当前会话",
|
||||
"autoDeleteAll": "删除全部会话",
|
||||
"autoDeleteNoneDesc": "请求结束后保留远端会话,不自动删除。",
|
||||
"autoDeleteSingleDesc": "请求结束后只删除本次请求创建的远端会话。",
|
||||
"autoDeleteAllDesc": "请求结束后清空该账号的全部远端会话。",
|
||||
"autoDeleteWarning": "当前模式会删除远端聊天记录,请谨慎使用。",
|
||||
"backupTitle": "备份与恢复",
|
||||
"loadExport": "加载当前导出",
|
||||
"downloadExport": "下载备份文件",
|
||||
|
||||
Reference in New Issue
Block a user