"use client"; import { useEffect, useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; type Config = { llm?: { provider?: "gemini" | "openai"; gemini?: { api_key?: string; base_url?: string }; openai?: { api_key?: string; base_url?: string }; }; data_sources?: { tushare?: { api_key?: string }; finnhub?: { api_key?: string }; jp_source?: { api_key?: string }; }; database?: { url?: string }; prompts?: { info?: string; finance?: string }; }; export default function ConfigPage() { const [cfg, setCfg] = useState(null); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState(null); const [health, setHealth] = useState("unknown"); // form inputs (敏感字段不回显,留空表示保持现有值) const [provider, setProvider] = useState<"gemini" | "openai">("gemini"); const [geminiBaseUrl, setGeminiBaseUrl] = useState(""); const [geminiKey, setGeminiKey] = useState(""); // 留空则保留 const [openaiBaseUrl, setOpenaiBaseUrl] = useState(""); const [openaiKey, setOpenaiKey] = useState(""); // 留空则保留 const [tushareKey, setTushareKey] = useState(""); // 留空则保留 const [finnhubKey, setFinnhubKey] = useState(""); // 留空则保留 const [jpKey, setJpKey] = useState(""); // 留空则保留 const [dbUrl, setDbUrl] = useState(""); const [promptInfo, setPromptInfo] = useState(""); const [promptFinance, setPromptFinance] = useState(""); async function loadConfig() { try { const res = await fetch("/api/config"); const data: Config = await res.json(); setCfg(data); // 非敏感字段可回显 setProvider((data.llm?.provider as any) ?? "gemini"); setGeminiBaseUrl(data.llm?.gemini?.base_url ?? ""); setOpenaiBaseUrl(data.llm?.openai?.base_url ?? ""); setDbUrl(data.database?.url ?? ""); setPromptInfo(data.prompts?.info ?? ""); setPromptFinance(data.prompts?.finance ?? ""); } catch { setMsg("加载配置失败"); } } async function saveConfig() { if (!cfg) return; setSaving(true); setMsg(null); try { // 构造覆盖配置:敏感字段若为空则沿用现有值 const next: Config = { llm: { provider, gemini: { base_url: geminiBaseUrl, api_key: geminiKey || cfg.llm?.gemini?.api_key || undefined, }, openai: { base_url: openaiBaseUrl, api_key: openaiKey || cfg.llm?.openai?.api_key || undefined, }, }, data_sources: { tushare: { api_key: tushareKey || cfg.data_sources?.tushare?.api_key || undefined }, finnhub: { api_key: finnhubKey || cfg.data_sources?.finnhub?.api_key || undefined }, jp_source: { api_key: jpKey || cfg.data_sources?.jp_source?.api_key || undefined }, }, database: { url: dbUrl }, prompts: { info: promptInfo, finance: promptFinance }, }; const res = await fetch("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(next), }); const ok = await res.json(); if (ok?.status === "ok") { setMsg("保存成功"); await loadConfig(); // 清空敏感输入(避免页面存储) setGeminiKey(""); setOpenaiKey(""); setTushareKey(""); setFinnhubKey(""); setJpKey(""); } else { setMsg("保存失败"); } } catch { setMsg("保存失败"); } finally { setSaving(false); } } async function testHealth() { try { const res = await fetch("/health"); const h = await res.json(); setHealth(h?.status ?? "unknown"); } catch { setHealth("error"); } } useEffect(() => { loadConfig(); testHealth(); }, []); return (

配置中心

切换 LLM、配置数据源与模板;不回显敏感密钥,留空表示保持现值。

后端健康 GET /health {health} LLM 设置 Gemini / OpenAI
setGeminiBaseUrl(e.target.value)} />
setGeminiKey(e.target.value)} />
setOpenaiBaseUrl(e.target.value)} />
setOpenaiKey(e.target.value)} />
数据源密钥 TuShare / Finnhub / JP
setTushareKey(e.target.value)} />
setFinnhubKey(e.target.value)} />
setJpKey(e.target.value)} />
数据库与模板 非敏感配置可回显
setDbUrl(e.target.value)} />
setPromptInfo(e.target.value)} />
setPromptFinance(e.target.value)} />
{msg && {msg}}
); }