import { useState, useEffect, useCallback } from 'react'; import { api } from '../api'; const TABS = [ { key: 'llm', label: '模型 API', icon: '✦' }, { key: 'embedding', label: 'Embedding', icon: '⊡' }, { key: 'milvus', label: 'Milvus', icon: '▥' }, { key: 'voice', label: '语音', icon: '♪' }, { key: 'ocr', label: 'OCR', icon: '☷' }, { key: 'gateway', label: 'Gateway', icon: '⇄' }, { key: 'mcp', label: 'MCP', icon: '⌘' }, { key: 'connection', label: '后端连接', icon: '⌁' }, ]; const BLANK_LLM = { name: 'openai', api_base: '', api_key: '', model: '', vision_model: '', max_tokens: 4096, temperature: 0.7, has_api_key: false, api_key_preview: '' }; const BLANK_EMB = { provider: '', model: '', dim: 1536, api_base: '', api_key: '', batch_size: 100, has_api_key: false, api_key_preview: '' }; const BLANK_MILVUS = { host: 'localhost', port: 19530, dim: 1536 }; const BLANK_API = { host: '0.0.0.0', port: 8000 }; const BLANK_VOICE = { stt_provider: 'sensevoice', stt_model: '', stt_api_base: '', stt_language: 'auto', tts_provider: 'qwen', tts_model: '', tts_api_base: '', tts_language: 'Chinese', tts_voice: '', tts_speed: 1.0 }; const BLANK_OCR = { enabled: true, api_base: '', model: '', max_pages: 50 }; const BLANK_GATEWAY = { enabled: false, response_mode: 'sync', max_reply_length: 2000, wechat_work: { enabled: false, corp_id: '', token: '', encoding_aes_key: '', agent_id: '', secret: '' }, dingtalk: { enabled: false, app_key: '', app_secret: '' }, qqbot: { enabled: false, app_id: '', app_secret: '', sandbox: true } }; function Field({ label, help, children }) { return (
{children} {help && {help}}
); } function SecretInput({ value, onChange, placeholder, hasKey, preview }) { const [show, setShow] = useState(false); return (
); } function Toast({ text, type, onDone }) { useEffect(() => { const t = setTimeout(onDone, 2800); return () => clearTimeout(t); }, [onDone]); return
{text}
; } export default function SettingsPanel() { const [tab, setTab] = useState('llm'); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [toast, setToast] = useState(null); // All settings from server const [llm, setLlm] = useState(BLANK_LLM); const [providers, setProviders] = useState([]); const [defaultProvider, setDefaultProvider] = useState(''); const [embedding, setEmbedding] = useState(BLANK_EMB); const [milvus, setMilvus] = useState(BLANK_MILVUS); const [apiServer, setApiServer] = useState(BLANK_API); const [voice, setVoice] = useState(BLANK_VOICE); const [ocr, setOcr] = useState(BLANK_OCR); const [gateway, setGateway] = useState(BLANK_GATEWAY); const [mcpServers, setMcpServers] = useState({}); const [apiBase, setApiBase] = useState(localStorage.getItem('agr_api_base') || ''); // MCP modal const [mcpModal, setMcpModal] = useState(null); const toastMsg = useCallback((type, text) => { setToast({ type, text, key: Date.now() }); }, []); useEffect(() => { api.getSettings() .then(data => { setDefaultProvider(data.default_provider || ''); const provs = data.llm_providers || []; setProviders(provs); const cur = provs.find(p => p.name === data.default_provider) || provs[0] || BLANK_LLM; setLlm({ ...cur, api_key: '' }); setEmbedding({ ...BLANK_EMB, ...data.embedding, api_key: '' }); setMilvus({ ...BLANK_MILVUS, ...data.milvus }); setApiServer({ ...BLANK_API, ...data.api }); setVoice({ ...BLANK_VOICE, ...data.voice }); setOcr({ ...BLANK_OCR, ...data.ocr }); setGateway(data.gateway || BLANK_GATEWAY); setMcpServers(data.mcp_servers || {}); }) .catch(() => toastMsg('error', '无法加载设置')) .finally(() => setLoading(false)); }, [toastMsg]); const selectProvider = (name) => { const p = providers.find(x => x.name === name); if (p) setLlm({ ...p, api_key: '' }); }; const saveSection = async (section, data) => { setSaving(true); try { await api.saveSettings([{ section, data }]); toastMsg('success', '已保存'); } catch (e) { toastMsg('error', e.message); } finally { setSaving(false); } }; const testLlm = async () => { try { const res = await api.testConnection({ api_base: llm.api_base, api_key: llm.api_key || null, model: llm.model, provider_type: llm.name === 'claude' ? 'claude' : 'openai' }); toastMsg('success', `连接成功 · ${res.detected_model || llm.model}`); } catch (e) { toastMsg('error', e.message); } }; // ── MCP helpers ── const mcpList = Object.entries(mcpServers); const openMcp = (name) => { const s = mcpServers[name] || { command: '', args: [], env: {}, disabled: false }; setMcpModal({ name, isNew: !name, command: s.command, args: Array.isArray(s.args) ? s.args.join(' ') : (s.args || ''), env: s.env ? Object.entries(s.env).map(([k, v]) => `${k}=${v}`).join('\n') : '', disabled: !!s.disabled, }); }; const saveMcpLocal = () => { if (!mcpModal.name) return; const env = {}; mcpModal.env.split('\n').filter(Boolean).forEach(line => { const i = line.indexOf('='); if (i > 0) env[line.slice(0, i).trim()] = line.slice(i + 1).trim(); }); setMcpServers(prev => ({ ...prev, [mcpModal.name]: { command: mcpModal.command, args: mcpModal.args.split(/\s+/).filter(Boolean), env, disabled: mcpModal.disabled }, })); setMcpModal(null); }; const deleteMcp = () => { if (!confirm(`Delete ${mcpModal.name}?`)) return; setMcpServers(prev => { const n = { ...prev }; delete n[mcpModal.name]; return n; }); setMcpModal(null); }; const saveMcpServer = async () => { setSaving(true); try { await api.saveMcpConfig(mcpServers); toastMsg('success', 'MCP 已保存 · 重启服务生效'); } catch (e) { toastMsg('error', e.message); } finally { setSaving(false); } }; const saveConnection = () => { localStorage.setItem('agr_api_base', apiBase.trim()); toastMsg('success', '已保存 · 刷新页面生效'); }; if (loading) return
加载设置...
; return (
{/* Tab bar */}
{/* ── LLM Provider ── */} {tab === 'llm' && (

✦ 模型 API

配置 LLM 服务商、地址与模型
{providers.map(p => ( ))}
setLlm({ ...llm, api_base: e.target.value })} placeholder="https://api.openai.com/v1" /> setLlm({ ...llm, api_key: e.target.value })} placeholder="sk-..." hasKey={llm.has_api_key} preview={llm.api_key_preview} /> setLlm({ ...llm, model: e.target.value })} placeholder="gpt-4o" /> setLlm({ ...llm, vision_model: e.target.value })} placeholder="留空使用 Chat Model" /> setLlm({ ...llm, max_tokens: Number(e.target.value) })} /> setLlm({ ...llm, temperature: e.target.value })} />
)} {/* ── Embedding ── */} {tab === 'embedding' && (

⊡ Embedding 模型

向量嵌入服务的端点与模型
setEmbedding({ ...embedding, provider: e.target.value })} placeholder="openai / local" /> setEmbedding({ ...embedding, model: e.target.value })} placeholder="text-embedding-3-small" /> setEmbedding({ ...embedding, dim: Number(e.target.value) })} /> setEmbedding({ ...embedding, batch_size: Number(e.target.value) })} /> setEmbedding({ ...embedding, api_base: e.target.value })} placeholder="留空复用 LLM Provider" /> setEmbedding({ ...embedding, api_key: e.target.value })} placeholder="留空复用 LLM Provider" hasKey={embedding.has_api_key} preview={embedding.api_key_preview} />
)} {/* ── Milvus ── */} {tab === 'milvus' && (

▥ Milvus 向量数据库

setMilvus({ ...milvus, host: e.target.value })} /> setMilvus({ ...milvus, port: Number(e.target.value) })} /> setMilvus({ ...milvus, dim: Number(e.target.value) })} />
)} {/* ── Voice ── */} {tab === 'voice' && (

♪ 语音 (STT / TTS)

语音识别 (STT)

setVoice({ ...voice, stt_model: e.target.value })} /> setVoice({ ...voice, stt_api_base: e.target.value })} /> setVoice({ ...voice, stt_language: e.target.value })} />

语音合成 (TTS)

setVoice({ ...voice, tts_model: e.target.value })} /> setVoice({ ...voice, tts_api_base: e.target.value })} /> setVoice({ ...voice, tts_language: e.target.value })} /> setVoice({ ...voice, tts_voice: e.target.value })} /> setVoice({ ...voice, tts_speed: Number(e.target.value) })} />
)} {/* ── OCR ── */} {tab === 'ocr' && (

☷ OCR

PaddleOCR-VL 配置
setOcr({ ...ocr, api_base: e.target.value })} /> setOcr({ ...ocr, model: e.target.value })} /> setOcr({ ...ocr, max_pages: Number(e.target.value) })} />
)} {/* ── Gateway ── */} {tab === 'gateway' && (

⇄ Gateway 消息网关

setGateway({ ...gateway, max_reply_length: Number(e.target.value) })} />

企业微信

setGateway({ ...gateway, wechat_work: { ...gateway.wechat_work, corp_id: e.target.value } })} /> setGateway({ ...gateway, wechat_work: { ...gateway.wechat_work, token: e.target.value } })} /> setGateway({ ...gateway, wechat_work: { ...gateway.wechat_work, encoding_aes_key: e.target.value } })} /> setGateway({ ...gateway, wechat_work: { ...gateway.wechat_work, agent_id: e.target.value } })} /> setGateway({ ...gateway, wechat_work: { ...gateway.wechat_work, secret: e.target.value } })} autoComplete="new-password" />

钉钉

setGateway({ ...gateway, dingtalk: { ...gateway.dingtalk, app_key: e.target.value } })} /> setGateway({ ...gateway, dingtalk: { ...gateway.dingtalk, app_secret: e.target.value } })} autoComplete="new-password" />

QQ Bot

setGateway({ ...gateway, qqbot: { ...gateway.qqbot, app_id: e.target.value } })} /> setGateway({ ...gateway, qqbot: { ...gateway.qqbot, app_secret: e.target.value } })} autoComplete="new-password" />
)} {/* ── MCP ── */} {tab === 'mcp' && (

⌘ MCP Servers

{mcpList.length === 0 &&
尚未配置 MCP Server
}
{mcpList.map(([name, s]) => ( ))}
)} {/* ── Connection ── */} {tab === 'connection' && (

⌁ 后端连接

前端连接 Agentic RAG 服务的地址
setApiBase(e.target.value)} placeholder="留空自动使用当前地址" />
setApiServer({ ...apiServer, host: e.target.value })} /> setApiServer({ ...apiServer, port: Number(e.target.value) })} />
)} {/* ── MCP Modal ── */} {mcpModal && (
e.target === e.currentTarget && setMcpModal(null)}>
{mcpModal.isNew ? '新建 MCP Server' : `编辑: ${mcpModal.name}`}
setMcpModal({ ...mcpModal, name: e.target.value })} disabled={!mcpModal.isNew} /> setMcpModal({ ...mcpModal, command: e.target.value })} /> setMcpModal({ ...mcpModal, args: e.target.value })} />