feat: 初始化 AI Gateway 项目
实现支持 OpenAI 和 Anthropic 双协议的统一大模型 API 网关 MVP 版本,包含: - OpenAI 和 Anthropic 协议代理 - 供应商和模型管理 - 用量统计 - 前端配置界面
This commit is contained in:
182
frontend/src/pages/ProvidersPage.tsx
Normal file
182
frontend/src/pages/ProvidersPage.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as api from '../api/client';
|
||||
import { ProviderForm } from '../components/ProviderForm';
|
||||
import { ModelForm } from '../components/ModelForm';
|
||||
|
||||
export function ProvidersPage() {
|
||||
const [providers, setProviders] = useState<api.Provider[]>([]);
|
||||
const [models, setModels] = useState<api.Model[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 表单状态
|
||||
const [showProviderForm, setShowProviderForm] = useState(false);
|
||||
const [editingProvider, setEditingProvider] = useState<api.Provider | null>(null);
|
||||
const [showModelForm, setShowModelForm] = useState(false);
|
||||
const [editingModel, setEditingModel] = useState<api.Model | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [providersData, modelsData] = await Promise.all([
|
||||
api.listProviders(),
|
||||
api.listModels(),
|
||||
]);
|
||||
setProviders(providersData);
|
||||
setModels(modelsData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProvider(id: string) {
|
||||
if (!confirm('确定要删除这个供应商吗?关联的模型也会被删除。')) return;
|
||||
try {
|
||||
await api.deleteProvider(id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteModel(id: string) {
|
||||
if (!confirm('确定要删除这个模型吗?')) return;
|
||||
try {
|
||||
await api.deleteModel(id);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="loading">加载中...</div>;
|
||||
if (error) return <div className="error">{error}</div>;
|
||||
|
||||
return (
|
||||
<div className="providers-page">
|
||||
<h1>供应商管理</h1>
|
||||
|
||||
<div className="section">
|
||||
<h2>供应商列表</h2>
|
||||
<button onClick={() => {
|
||||
setEditingProvider(null);
|
||||
setShowProviderForm(true);
|
||||
}}>
|
||||
添加供应商
|
||||
</button>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>名称</th>
|
||||
<th>API Key</th>
|
||||
<th>Base URL</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{providers.map(p => (
|
||||
<tr key={p.id}>
|
||||
<td>{p.id}</td>
|
||||
<td>{p.name}</td>
|
||||
<td>{p.api_key}</td>
|
||||
<td>{p.base_url}</td>
|
||||
<td>{p.enabled ? '启用' : '禁用'}</td>
|
||||
<td>
|
||||
<button onClick={() => {
|
||||
setEditingProvider(p);
|
||||
setShowProviderForm(true);
|
||||
}}>编辑</button>
|
||||
<button onClick={() => handleDeleteProvider(p.id)}>删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>模型列表</h2>
|
||||
<button onClick={() => {
|
||||
setEditingModel(null);
|
||||
setShowModelForm(true);
|
||||
}}>
|
||||
添加模型
|
||||
</button>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>供应商</th>
|
||||
<th>模型名称</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{models.map(m => {
|
||||
const provider = providers.find(p => p.id === m.provider_id);
|
||||
return (
|
||||
<tr key={m.id}>
|
||||
<td>{m.id}</td>
|
||||
<td>{provider?.name || m.provider_id}</td>
|
||||
<td>{m.model_name}</td>
|
||||
<td>{m.enabled ? '启用' : '禁用'}</td>
|
||||
<td>
|
||||
<button onClick={() => {
|
||||
setEditingModel(m);
|
||||
setShowModelForm(true);
|
||||
}}>编辑</button>
|
||||
<button onClick={() => handleDeleteModel(m.id)}>删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 供应商表单 */}
|
||||
{showProviderForm && (
|
||||
<ProviderForm
|
||||
provider={editingProvider || undefined}
|
||||
onSave={() => {
|
||||
setShowProviderForm(false);
|
||||
setEditingProvider(null);
|
||||
loadData();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowProviderForm(false);
|
||||
setEditingProvider(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 模型表单 */}
|
||||
{showModelForm && (
|
||||
<ModelForm
|
||||
model={editingModel || undefined}
|
||||
providers={providers}
|
||||
onSave={() => {
|
||||
setShowModelForm(false);
|
||||
setEditingModel(null);
|
||||
loadData();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowModelForm(false);
|
||||
setEditingModel(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
110
frontend/src/pages/StatsPage.tsx
Normal file
110
frontend/src/pages/StatsPage.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as api from '../api/client';
|
||||
|
||||
export function StatsPage() {
|
||||
const [stats, setStats] = useState<api.UsageStats[]>([]);
|
||||
const [providers, setProviders] = useState<api.Provider[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 过滤条件
|
||||
const [providerId, setProviderId] = useState('');
|
||||
const [modelName, setModelName] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [statsData, providersData] = await Promise.all([
|
||||
api.getStats({
|
||||
provider_id: providerId || undefined,
|
||||
model_name: modelName || undefined,
|
||||
start_date: startDate || undefined,
|
||||
end_date: endDate || undefined,
|
||||
}),
|
||||
api.listProviders(),
|
||||
]);
|
||||
setStats(statsData);
|
||||
setProviders(providersData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFilter(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
loadData();
|
||||
}
|
||||
|
||||
if (loading) return <div className="loading">加载中...</div>;
|
||||
if (error) return <div className="error">{error}</div>;
|
||||
|
||||
return (
|
||||
<div className="stats-page">
|
||||
<h1>用量统计</h1>
|
||||
|
||||
<form onSubmit={handleFilter} className="filter-form">
|
||||
<select value={providerId} onChange={e => setProviderId(e.target.value)}>
|
||||
<option value="">所有供应商</option>
|
||||
{providers.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="模型名称"
|
||||
value={modelName}
|
||||
onChange={e => setModelName(e.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
placeholder="开始日期"
|
||||
value={startDate}
|
||||
onChange={e => setStartDate(e.target.value)}
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
placeholder="结束日期"
|
||||
value={endDate}
|
||||
onChange={e => setEndDate(e.target.value)}
|
||||
/>
|
||||
|
||||
<button type="submit">查询</button>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>供应商</th>
|
||||
<th>模型</th>
|
||||
<th>日期</th>
|
||||
<th>请求数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.map(s => {
|
||||
const provider = providers.find(p => p.id === s.provider_id);
|
||||
return (
|
||||
<tr key={s.id}>
|
||||
<td>{provider?.name || s.provider_id}</td>
|
||||
<td>{s.model_name}</td>
|
||||
<td>{s.date}</td>
|
||||
<td>{s.request_count}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user