feat: add configurable auto-delete modes (none, single, all) for remote chat sessions

This commit is contained in:
CJACK
2026-04-05 04:18:34 +08:00
parent f7261bec0d
commit 97e72fb174
20 changed files with 297 additions and 62 deletions

View File

@@ -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
View File

@@ -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` 策略已固定,不再作为可写入字段

View File

@@ -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`
### 环境变量

View File

@@ -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

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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()

View 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)
}
})
}
}

View File

@@ -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
}

View File

@@ -32,6 +32,7 @@ type ConfigStore interface {
RuntimeAccountMaxQueue(defaultSize int) int
RuntimeGlobalMaxInflight(defaultSize int) int
RuntimeTokenRefreshIntervalHours() int
AutoDeleteMode() string
CompatStripReferenceMarkers() bool
AutoDeleteSessions() bool
}

View File

@@ -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)
}

View File

@@ -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"],

View File

@@ -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 {

View File

@@ -77,5 +77,6 @@ type EmbeddingsConfig struct {
}
type AutoDeleteConfig struct {
Sessions bool `json:"sessions"`
Mode string `json:"mode,omitempty"`
Sessions bool `json:"sessions,omitempty"`
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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>

View File

@@ -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) },
}
}

View File

@@ -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",

View 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": "下载备份文件",