mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 08:55:28 +08:00
296 lines
10 KiB
JavaScript
296 lines
10 KiB
JavaScript
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
|
import {
|
|
fetchSettings,
|
|
getExportData,
|
|
postImportData,
|
|
postPassword,
|
|
putSettings,
|
|
} from './settingsApi'
|
|
|
|
const MAX_AUTO_FETCH_FAILURES = 3
|
|
|
|
const DEFAULT_FORM = {
|
|
admin: { jwt_expire_hours: 24 },
|
|
runtime: { account_max_inflight: 2, account_max_queue: 10, global_max_inflight: 10 },
|
|
toolcall: { mode: 'feature_match', early_emit_confidence: 'high' },
|
|
responses: { store_ttl_seconds: 900 },
|
|
embeddings: { provider: '' },
|
|
auto_delete: { sessions: false },
|
|
claude_mapping_text: '{\n "fast": "deepseek-chat",\n "slow": "deepseek-reasoner"\n}',
|
|
model_aliases_text: '{}',
|
|
}
|
|
|
|
function parseJSONMap(raw, fieldName, t) {
|
|
const text = String(raw || '').trim()
|
|
if (!text) {
|
|
return {}
|
|
}
|
|
let parsed
|
|
try {
|
|
parsed = JSON.parse(text)
|
|
} catch (_e) {
|
|
throw new Error(t('settings.invalidJsonField', { field: fieldName }))
|
|
}
|
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
throw new Error(t('settings.invalidJsonField', { field: fieldName }))
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
function fromServerForm(data) {
|
|
return {
|
|
admin: { jwt_expire_hours: Number(data.admin?.jwt_expire_hours || 24) },
|
|
runtime: {
|
|
account_max_inflight: Number(data.runtime?.account_max_inflight || 2),
|
|
account_max_queue: Number(data.runtime?.account_max_queue || 10),
|
|
global_max_inflight: Number(data.runtime?.global_max_inflight || 10),
|
|
},
|
|
toolcall: {
|
|
mode: data.toolcall?.mode || 'feature_match',
|
|
early_emit_confidence: data.toolcall?.early_emit_confidence || 'high',
|
|
},
|
|
responses: {
|
|
store_ttl_seconds: Number(data.responses?.store_ttl_seconds || 900),
|
|
},
|
|
embeddings: {
|
|
provider: data.embeddings?.provider || '',
|
|
},
|
|
auto_delete: {
|
|
sessions: Boolean(data.auto_delete?.sessions || false),
|
|
},
|
|
claude_mapping_text: JSON.stringify(data.claude_mapping || {}, null, 2),
|
|
model_aliases_text: JSON.stringify(data.model_aliases || {}, null, 2),
|
|
}
|
|
}
|
|
|
|
function toServerPayload(form) {
|
|
return {
|
|
admin: { jwt_expire_hours: Number(form.admin.jwt_expire_hours) },
|
|
runtime: {
|
|
account_max_inflight: Number(form.runtime.account_max_inflight),
|
|
account_max_queue: Number(form.runtime.account_max_queue),
|
|
global_max_inflight: Number(form.runtime.global_max_inflight),
|
|
},
|
|
toolcall: {
|
|
mode: String(form.toolcall.mode || '').trim(),
|
|
early_emit_confidence: String(form.toolcall.early_emit_confidence || '').trim(),
|
|
},
|
|
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) },
|
|
}
|
|
}
|
|
|
|
export function useSettingsForm({ apiFetch, t, onMessage, onRefresh, onForceLogout, isVercel = false }) {
|
|
const [loading, setLoading] = useState(false)
|
|
const [saving, setSaving] = useState(false)
|
|
const [changingPassword, setChangingPassword] = useState(false)
|
|
const [importing, setImporting] = useState(false)
|
|
const [exportData, setExportData] = useState(null)
|
|
const [importMode, setImportMode] = useState('merge')
|
|
const [importText, setImportText] = useState('')
|
|
const [newPassword, setNewPassword] = useState('')
|
|
const [consecutiveFailures, setConsecutiveFailures] = useState(0)
|
|
const [autoFetchPaused, setAutoFetchPaused] = useState(false)
|
|
const [lastError, setLastError] = useState('')
|
|
const [settingsMeta, setSettingsMeta] = useState({
|
|
default_password_warning: false,
|
|
env_backed: false,
|
|
needs_vercel_sync: false,
|
|
})
|
|
const [form, setForm] = useState(DEFAULT_FORM)
|
|
|
|
const trackLoadFailure = useCallback(() => {
|
|
setConsecutiveFailures((prev) => {
|
|
const next = prev + 1
|
|
if (isVercel && next >= MAX_AUTO_FETCH_FAILURES) {
|
|
setAutoFetchPaused(true)
|
|
}
|
|
return next
|
|
})
|
|
}, [isVercel])
|
|
|
|
const loadSettings = useCallback(async ({ manual = false } = {}) => {
|
|
if (isVercel && autoFetchPaused && !manual) {
|
|
return
|
|
}
|
|
setLoading(true)
|
|
try {
|
|
const { res, data } = await fetchSettings(apiFetch, t)
|
|
if (!res.ok) {
|
|
const detail = data.detail || t('settings.loadFailed')
|
|
setLastError(detail)
|
|
onMessage('error', detail)
|
|
trackLoadFailure()
|
|
return
|
|
}
|
|
setConsecutiveFailures(0)
|
|
setAutoFetchPaused(false)
|
|
setLastError('')
|
|
setSettingsMeta({
|
|
default_password_warning: Boolean(data.admin?.default_password_warning),
|
|
env_backed: Boolean(data.env_backed),
|
|
needs_vercel_sync: Boolean(data.needs_vercel_sync),
|
|
})
|
|
setForm(fromServerForm(data))
|
|
} catch (e) {
|
|
const detail = e?.message || t('settings.loadFailed')
|
|
setLastError(detail)
|
|
onMessage('error', detail)
|
|
trackLoadFailure()
|
|
// eslint-disable-next-line no-console
|
|
console.error(e)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [apiFetch, autoFetchPaused, isVercel, onMessage, t, trackLoadFailure])
|
|
|
|
useEffect(() => {
|
|
loadSettings()
|
|
}, [loadSettings])
|
|
|
|
const retryLoadSettings = useCallback(() => {
|
|
setAutoFetchPaused(false)
|
|
loadSettings({ manual: true })
|
|
}, [loadSettings])
|
|
|
|
const saveSettings = useCallback(async () => {
|
|
let claudeMapping = {}
|
|
let modelAliases = {}
|
|
try {
|
|
claudeMapping = parseJSONMap(form.claude_mapping_text, 'claude_mapping', t)
|
|
modelAliases = parseJSONMap(form.model_aliases_text, 'model_aliases', t)
|
|
} catch (e) {
|
|
onMessage('error', e.message)
|
|
return
|
|
}
|
|
|
|
const payload = {
|
|
...toServerPayload(form),
|
|
claude_mapping: claudeMapping,
|
|
model_aliases: modelAliases,
|
|
}
|
|
|
|
setSaving(true)
|
|
try {
|
|
const { res, data } = await putSettings(apiFetch, payload)
|
|
if (!res.ok) {
|
|
onMessage('error', data.detail || t('settings.saveFailed'))
|
|
return
|
|
}
|
|
onMessage('success', t('settings.saveSuccess'))
|
|
if (typeof onRefresh === 'function') {
|
|
onRefresh()
|
|
}
|
|
await loadSettings()
|
|
} catch (e) {
|
|
onMessage('error', t('settings.saveFailed'))
|
|
// eslint-disable-next-line no-console
|
|
console.error(e)
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}, [apiFetch, form, loadSettings, onMessage, onRefresh, t])
|
|
|
|
const updatePassword = useCallback(async () => {
|
|
if (String(newPassword || '').trim().length < 4) {
|
|
onMessage('error', t('settings.passwordTooShort'))
|
|
return
|
|
}
|
|
setChangingPassword(true)
|
|
try {
|
|
const { res, data } = await postPassword(apiFetch, newPassword.trim())
|
|
if (!res.ok) {
|
|
onMessage('error', data.detail || t('settings.passwordUpdateFailed'))
|
|
return
|
|
}
|
|
onMessage('success', t('settings.passwordUpdated'))
|
|
setNewPassword('')
|
|
if (typeof onForceLogout === 'function') {
|
|
onForceLogout()
|
|
}
|
|
} catch (_e) {
|
|
onMessage('error', t('settings.passwordUpdateFailed'))
|
|
} finally {
|
|
setChangingPassword(false)
|
|
}
|
|
}, [apiFetch, newPassword, onForceLogout, onMessage, t])
|
|
|
|
const loadExportData = useCallback(async () => {
|
|
try {
|
|
const { res, data } = await getExportData(apiFetch)
|
|
if (!res.ok) {
|
|
onMessage('error', data.detail || t('settings.exportFailed'))
|
|
return
|
|
}
|
|
setExportData(data)
|
|
onMessage('success', t('settings.exportLoaded'))
|
|
} catch (_e) {
|
|
onMessage('error', t('settings.exportFailed'))
|
|
}
|
|
}, [apiFetch, onMessage, t])
|
|
|
|
const doImport = useCallback(async () => {
|
|
if (!String(importText || '').trim()) {
|
|
onMessage('error', t('settings.importEmpty'))
|
|
return
|
|
}
|
|
let parsed
|
|
try {
|
|
parsed = JSON.parse(importText)
|
|
} catch (_e) {
|
|
onMessage('error', t('settings.importInvalidJson'))
|
|
return
|
|
}
|
|
setImporting(true)
|
|
try {
|
|
const { res, data } = await postImportData(apiFetch, importMode, parsed)
|
|
if (!res.ok) {
|
|
onMessage('error', data.detail || t('settings.importFailed'))
|
|
return
|
|
}
|
|
onMessage('success', t('settings.importSuccess', { mode: importMode }))
|
|
if (typeof onRefresh === 'function') {
|
|
onRefresh()
|
|
}
|
|
await loadSettings()
|
|
} catch (_e) {
|
|
onMessage('error', t('settings.importFailed'))
|
|
} finally {
|
|
setImporting(false)
|
|
}
|
|
}, [apiFetch, importMode, importText, loadSettings, onMessage, onRefresh, t])
|
|
|
|
const syncHintVisible = useMemo(
|
|
() => settingsMeta.env_backed || settingsMeta.needs_vercel_sync,
|
|
[settingsMeta.env_backed, settingsMeta.needs_vercel_sync],
|
|
)
|
|
|
|
return {
|
|
form,
|
|
setForm,
|
|
loading,
|
|
saving,
|
|
changingPassword,
|
|
importing,
|
|
exportData,
|
|
importMode,
|
|
setImportMode,
|
|
importText,
|
|
setImportText,
|
|
newPassword,
|
|
setNewPassword,
|
|
consecutiveFailures,
|
|
autoFetchPaused,
|
|
lastError,
|
|
settingsMeta,
|
|
syncHintVisible,
|
|
retryLoadSettings,
|
|
saveSettings,
|
|
updatePassword,
|
|
loadExportData,
|
|
doImport,
|
|
}
|
|
}
|