2026-03-09 14:46:56 +08:00
|
|
|
|
import { useEffect, useState } from "react";
|
2026-03-10 14:25:21 +08:00
|
|
|
|
import { Card, Form, Input, Select, Button, Alert, Space, Divider, Descriptions, message } from "antd";
|
2026-03-09 14:46:56 +08:00
|
|
|
|
import {
|
|
|
|
|
|
api,
|
|
|
|
|
|
type RuntimeSettings,
|
2026-03-10 14:25:21 +08:00
|
|
|
|
type ProviderKey,
|
2026-03-09 14:46:56 +08:00
|
|
|
|
getApiBaseUrl,
|
|
|
|
|
|
setApiBaseUrl,
|
|
|
|
|
|
} from "../services/api";
|
|
|
|
|
|
|
2026-03-10 14:25:21 +08:00
|
|
|
|
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 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-09 14:46:56 +08:00
|
|
|
|
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(),
|
2026-03-10 14:25:21 +08:00
|
|
|
|
ocr_provider: data.ocr_provider,
|
|
|
|
|
|
ocr_model: data.ocr_model,
|
|
|
|
|
|
inference_provider: data.inference_provider,
|
|
|
|
|
|
inference_model: data.inference_model,
|
2026-03-09 14:46:56 +08:00
|
|
|
|
custom_openai_base_url: data.base_urls?.custom_openai || "",
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
message.error("加载设置失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadSettings();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-10 14:25:21 +08:00
|
|
|
|
const onFinish = async (values: Record<string, string | undefined>) => {
|
2026-03-09 14:46:56 +08:00
|
|
|
|
setSaving(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
setApiBaseUrl(values.system_api_base_url || "");
|
2026-03-10 14:25:21 +08:00
|
|
|
|
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,
|
2026-03-09 14:46:56 +08:00
|
|
|
|
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);
|
2026-03-10 14:25:21 +08:00
|
|
|
|
message.success("设置已保存");
|
2026-03-09 14:46:56 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
message.error("保存失败");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-10 14:25:21 +08:00
|
|
|
|
<Card title="模型与接口设置" loading={loading}>
|
2026-03-09 14:46:56 +08:00
|
|
|
|
<Alert
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ marginBottom: 16 }}
|
2026-03-10 14:25:21 +08:00
|
|
|
|
message="API Key 仅在当前服务进程运行期内生效,不写入磁盘。OCR 模型用于从截图中提取交易,推理模型用于生成报告等文本推理任务。"
|
2026-03-09 14:46:56 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<Form form={form} layout="vertical" onFinish={onFinish}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="系统 API BaseURL(前端请求后端)"
|
|
|
|
|
|
name="system_api_base_url"
|
2026-03-10 14:25:21 +08:00
|
|
|
|
extra="默认 /api;前后端分离时填 http://127.0.0.1:8000/api"
|
2026-03-09 14:46:56 +08:00
|
|
|
|
>
|
2026-03-10 14:25:21 +08:00
|
|
|
|
<Input placeholder="/api" />
|
2026-03-09 14:46:56 +08:00
|
|
|
|
</Form.Item>
|
2026-03-10 14:25:21 +08:00
|
|
|
|
|
|
|
|
|
|
<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 / ..." />
|
2026-03-09 14:46:56 +08:00
|
|
|
|
</Form.Item>
|
2026-03-10 14:25:21 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-03-09 14:46:56 +08:00
|
|
|
|
<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="自定义厂商 BaseURL(OpenAI兼容)"
|
|
|
|
|
|
name="custom_openai_base_url"
|
|
|
|
|
|
extra="例如 https://api.xxx.com/v1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="https://api.xxx.com/v1" />
|
|
|
|
|
|
</Form.Item>
|
2026-03-10 14:25:21 +08:00
|
|
|
|
<Form.Item label="自定义厂商默认模型" name="custom_openai_model">
|
2026-03-09 14:46:56 +08:00
|
|
|
|
<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>
|
2026-03-10 14:25:21 +08:00
|
|
|
|
|
2026-03-09 14:46:56 +08:00
|
|
|
|
<Space>
|
2026-03-10 14:25:21 +08:00
|
|
|
|
<Button type="primary" htmlType="submit" loading={saving}>保存设置</Button>
|
2026-03-09 14:46:56 +08:00
|
|
|
|
<Button onClick={loadSettings}>刷新</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
|
|
|
|
|
|
{runtime && (
|
2026-03-10 14:25:21 +08:00
|
|
|
|
<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>
|
2026-03-09 14:46:56 +08:00
|
|
|
|
</Card>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|