import React, { useEffect, useMemo, useState } from 'react'; import { RefreshCw, Save } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import RecursiveConfig from '../components/RecursiveConfig'; function setPath(obj: any, path: string, value: any) { const keys = path.split('.'); const next = JSON.parse(JSON.stringify(obj || {})); let cur = next; for (let i = 0; i < keys.length - 1; i++) { const k = keys[i]; if (typeof cur[k] !== 'object' || cur[k] === null) cur[k] = {}; cur = cur[k]; } cur[keys[keys.length - 1]] = value; return next; } const Config: React.FC = () => { const { t } = useTranslation(); const { cfg, setCfg, cfgRaw, setCfgRaw, loadConfig, hotReloadFieldDetails, q } = useAppContext(); const [showRaw, setShowRaw] = useState(false); const [basicMode, setBasicMode] = useState(true); const [hotOnly, setHotOnly] = useState(false); const [search, setSearch] = useState(''); const [newProxyName, setNewProxyName] = useState(''); const hotPrefixes = useMemo(() => hotReloadFieldDetails.map((x) => String(x.path || '').replace(/\.\*$/, '')).filter(Boolean), [hotReloadFieldDetails]); const allTopKeys = useMemo(() => Object.keys(cfg || {}).filter(k => typeof (cfg as any)?.[k] === 'object' && (cfg as any)?.[k] !== null), [cfg]); const basicTopKeys = useMemo(() => { const preferred = ['gateway', 'providers', 'channels', 'tools', 'cron', 'agents', 'logging']; return preferred.filter((k) => allTopKeys.includes(k)); }, [allTopKeys]); const filteredTopKeys = useMemo(() => { let keys = basicMode ? basicTopKeys : allTopKeys; if (hotOnly) { keys = keys.filter((k) => hotPrefixes.some((p) => p === k || p.startsWith(`${k}.`) || k.startsWith(`${p}.`))); } if (search.trim()) { const s = search.trim().toLowerCase(); keys = keys.filter((k) => k.toLowerCase().includes(s)); } return keys; }, [allTopKeys, basicTopKeys, basicMode, hotOnly, search, hotPrefixes]); const [selectedTop, setSelectedTop] = useState(''); const activeTop = filteredTopKeys.includes(selectedTop) ? selectedTop : (filteredTopKeys[0] || ''); const [baseline, setBaseline] = useState(null); const [showDiff, setShowDiff] = useState(false); const currentPayload = useMemo(() => { if (showRaw) { try { return JSON.parse(cfgRaw); } catch { return cfg; } } return cfg; }, [showRaw, cfgRaw, cfg]); const diffRows = useMemo(() => { const out: Array<{ path: string; before: any; after: any }> = []; const walk = (a: any, b: any, p: string) => { const keys = new Set([...(a && typeof a === 'object' ? Object.keys(a) : []), ...(b && typeof b === 'object' ? Object.keys(b) : [])]); if (keys.size === 0) { if (JSON.stringify(a) !== JSON.stringify(b)) out.push({ path: p || '(root)', before: a, after: b }); return; } keys.forEach((k) => { const pa = p ? `${p}.${k}` : k; const av = a ? a[k] : undefined; const bv = b ? b[k] : undefined; const bothObj = av && bv && typeof av === 'object' && typeof bv === 'object' && !Array.isArray(av) && !Array.isArray(bv); if (bothObj) walk(av, bv, pa); else if (JSON.stringify(av) !== JSON.stringify(bv)) out.push({ path: pa, before: av, after: bv }); }); }; walk(baseline || {}, currentPayload || {}, ''); return out; }, [baseline, currentPayload]); useEffect(() => { if (baseline == null && cfg && Object.keys(cfg).length > 0) { setBaseline(JSON.parse(JSON.stringify(cfg))); } }, [cfg, baseline]); function updateProxyField(name: string, field: string, value: any) { setCfg((v) => setPath(v, `providers.proxies.${name}.${field}`, value)); } function removeProxy(name: string) { setCfg((v) => { const next = JSON.parse(JSON.stringify(v || {})); if (next?.providers?.proxies && typeof next.providers.proxies === 'object') { delete next.providers.proxies[name]; } return next; }); } function addProxy() { const name = newProxyName.trim(); if (!name) return; setCfg((v) => { const next = JSON.parse(JSON.stringify(v || {})); if (!next.providers || typeof next.providers !== 'object') next.providers = {}; if (!next.providers.proxies || typeof next.providers.proxies !== 'object' || Array.isArray(next.providers.proxies)) { next.providers.proxies = {}; } if (!next.providers.proxies[name]) { next.providers.proxies[name] = { api_key: '', api_base: '', protocol: 'responses', models: [], supports_responses_compact: false, auth: 'bearer', timeout_sec: 120, }; } return next; }); setNewProxyName(''); } async function saveConfig() { try { const payload = showRaw ? JSON.parse(cfgRaw) : cfg; const r = await fetch(`/webui/api/config${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); alert(await r.text()); setBaseline(JSON.parse(JSON.stringify(payload))); setShowDiff(false); } catch (e) { alert('Failed to save config: ' + e); } } return (

{t('configuration')}

setSearch(e.target.value)} placeholder="搜索分类..." className="px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-lg text-sm" />
热更新字段(完整)
{hotReloadFieldDetails.map((it) => (
{it.path}
{it.name || ''}{it.description ? ` · ${it.description}` : ''}
))}
{!showRaw ? (
{activeTop === 'providers' && !showRaw && (
Proxies
setNewProxyName(e.target.value)} placeholder="new provider name" className="px-2 py-1 rounded bg-zinc-900 border border-zinc-700 text-xs" />
{Object.entries(((cfg as any)?.providers?.proxies || {}) as Record).map(([name, p]) => (
{name}
updateProxyField(name, 'api_base', e.target.value)} placeholder="api_base" className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> updateProxyField(name, 'api_key', e.target.value)} placeholder="api_key" className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> updateProxyField(name, 'protocol', e.target.value)} placeholder="protocol" className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder="models,a,b" className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
))} {Object.keys(((cfg as any)?.providers?.proxies || {}) as Record).length === 0 && (
No custom providers yet.
)}
)} {activeTop ? ( } path={activeTop} hotPaths={hotReloadFieldDetails.map((x) => x.path)} onlyHot={hotOnly} onChange={(path, val) => setCfg(v => setPath(v, path, val))} /> ) : (
No config groups found.
)}
) : (