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' && (
)}
{/* ── Embedding ── */}
{tab === 'embedding' && (
⊡ Embedding 模型
向量嵌入服务的端点与模型
)}
{/* ── Milvus ── */}
{tab === 'milvus' && (
)}
{/* ── 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' && (
)}
{/* ── 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 服务的地址
)}
{/* ── MCP Modal ── */}
{mcpModal && (
e.target === e.currentTarget && setMcpModal(null)}>
)}
{toast &&
setToast(null)} />}
);
}