Files
fund-tracer/frontend/src/pages/Settings.tsx
2026-03-10 14:25:21 +08:00

159 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { Card, Form, Input, Select, Button, Alert, Space, Divider, Descriptions, message } from "antd";
import {
api,
type RuntimeSettings,
type ProviderKey,
getApiBaseUrl,
setApiBaseUrl,
} from "../services/api";
const PROVIDER_OPTIONS = [
{ label: "OpenAI", value: "openai" as ProviderKey },
{ label: "Anthropic", value: "anthropic" as ProviderKey },
{ label: "DeepSeek", value: "deepseek" as ProviderKey },
{ label: "自定义(OpenAI兼容)", value: "custom_openai" as ProviderKey },
];
export default function Settings() {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [runtime, setRuntime] = useState<RuntimeSettings | null>(null);
const loadSettings = async () => {
setLoading(true);
try {
const data = await api.settings.get();
setRuntime(data);
form.setFieldsValue({
system_api_base_url: getApiBaseUrl(),
ocr_provider: data.ocr_provider,
ocr_model: data.ocr_model,
inference_provider: data.inference_provider,
inference_model: data.inference_model,
custom_openai_base_url: data.base_urls?.custom_openai || "",
});
} catch {
message.error("加载设置失败");
} finally {
setLoading(false);
}
};
useEffect(() => {
loadSettings();
}, []);
const onFinish = async (values: Record<string, string | undefined>) => {
setSaving(true);
try {
setApiBaseUrl(values.system_api_base_url || "");
const payload: Record<string, string | undefined> = {
ocr_provider: values.ocr_provider,
ocr_model: values.ocr_model?.trim() || undefined,
inference_provider: values.inference_provider,
inference_model: values.inference_model?.trim() || undefined,
openai_api_key: values.openai_api_key?.trim() || undefined,
anthropic_api_key: values.anthropic_api_key?.trim() || undefined,
deepseek_api_key: values.deepseek_api_key?.trim() || undefined,
custom_openai_api_key: values.custom_openai_api_key?.trim() || undefined,
custom_openai_base_url: values.custom_openai_base_url?.trim() || undefined,
custom_openai_model: values.custom_openai_model?.trim() || undefined,
};
const data = await api.settings.update(payload);
setRuntime(data);
message.success("设置已保存");
} catch {
message.error("保存失败");
} finally {
setSaving(false);
}
};
return (
<Card title="模型与接口设置" loading={loading}>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="API Key 仅在当前服务进程运行期内生效不写入磁盘。OCR 模型用于从截图中提取交易,推理模型用于生成报告等文本推理任务。"
/>
<Form form={form} layout="vertical" onFinish={onFinish}>
<Form.Item
label="系统 API BaseURL前端请求后端"
name="system_api_base_url"
extra="默认 /api前后端分离时填 http://127.0.0.1:8000/api"
>
<Input placeholder="/api" />
</Form.Item>
<Divider orientation="left">OCR </Divider>
<Form.Item label="OCR 提供商" name="ocr_provider" rules={[{ required: true }]}>
<Select options={PROVIDER_OPTIONS} />
</Form.Item>
<Form.Item label="OCR 模型名" name="ocr_model" extra="留空则使用该提供商的默认模型">
<Input placeholder="如 gpt-4o / qwen-vl-max / ..." />
</Form.Item>
<Divider orientation="left"></Divider>
<Form.Item label="推理提供商" name="inference_provider" rules={[{ required: true }]}>
<Select options={PROVIDER_OPTIONS} />
</Form.Item>
<Form.Item label="推理模型名" name="inference_model" extra="留空则使用该提供商的默认模型">
<Input placeholder="如 gpt-4o-mini / deepseek-chat / ..." />
</Form.Item>
<Divider orientation="left">API Key </Divider>
<Form.Item label="OpenAI API Key" name="openai_api_key">
<Input.Password placeholder="sk-..." />
</Form.Item>
<Form.Item label="Anthropic API Key" name="anthropic_api_key">
<Input.Password placeholder="sk-ant-..." />
</Form.Item>
<Form.Item label="DeepSeek API Key" name="deepseek_api_key">
<Input.Password placeholder="sk-..." />
</Form.Item>
<Form.Item
label="自定义厂商 BaseURLOpenAI兼容"
name="custom_openai_base_url"
extra="例如 https://api.xxx.com/v1"
>
<Input placeholder="https://api.xxx.com/v1" />
</Form.Item>
<Form.Item label="自定义厂商默认模型" name="custom_openai_model">
<Input placeholder="gpt-4o-mini / qwen-vl-plus / ..." />
</Form.Item>
<Form.Item label="自定义厂商 API Key" name="custom_openai_api_key">
<Input.Password placeholder="sk-..." />
</Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={saving}></Button>
<Button onClick={loadSettings}></Button>
</Space>
</Form>
{runtime && (
<Card title="当前生效配置" size="small" style={{ marginTop: 16 }}>
<Descriptions column={2} size="small" bordered>
<Descriptions.Item label="系统 API BaseURL">{getApiBaseUrl()}</Descriptions.Item>
<Descriptions.Item label="自定义厂商 BaseURL">{runtime.base_urls.custom_openai || "-"}</Descriptions.Item>
<Descriptions.Item label="OCR 提供商">{runtime.ocr_provider}</Descriptions.Item>
<Descriptions.Item label="OCR 模型">{runtime.ocr_model}</Descriptions.Item>
<Descriptions.Item label="推理提供商">{runtime.inference_provider}</Descriptions.Item>
<Descriptions.Item label="推理模型">{runtime.inference_model}</Descriptions.Item>
<Descriptions.Item label="OpenAI Key">{runtime.has_keys.openai ? "已配置" : "未配置"}</Descriptions.Item>
<Descriptions.Item label="Anthropic Key">{runtime.has_keys.anthropic ? "已配置" : "未配置"}</Descriptions.Item>
<Descriptions.Item label="DeepSeek Key">{runtime.has_keys.deepseek ? "已配置" : "未配置"}</Descriptions.Item>
<Descriptions.Item label="自定义厂商 Key">{runtime.has_keys.custom_openai ? "已配置" : "未配置"}</Descriptions.Item>
</Descriptions>
</Card>
)}
</Card>
);
}