feat(admin): 添加网络代理配置和钱包交易功能
- 在管理面板中集成网络代理配置显示和平台代理设置 - 添加钱包摘要和交易列表API接口及数据管理 - 实现SSE流式响应中的错误处理机制 - 添加全局HTTP代理环境变量配置支持 - 更新平台表单以支持代理模式选择和自定义代理地址 - 集成钱包交易查询过滤和分页功能 - 优化API错误详情解析和显示格式
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState, type FormEvent, type ReactNode } from 'react';
|
||||
import { Boxes, CheckCircle2, KeyRound, Pencil, Plus, RotateCcw, Search, ServerCog, ShieldCheck, SlidersHorizontal, Trash2, X } from 'lucide-react';
|
||||
import { Boxes, CheckCircle2, Globe2, KeyRound, Pencil, Plus, RotateCcw, Search, ServerCog, ShieldCheck, SlidersHorizontal, Trash2, X } from 'lucide-react';
|
||||
import type { BaseModelCatalogItem, CatalogProvider, IntegrationPlatform, PlatformModel, PricingRuleSet } from '@easyai-ai-gateway/contracts';
|
||||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, EmptyState, FormDialog, Input, Label, ScreenMessage, Select, Table, TableCell, TableHead, TableRow } from '../../components/ui';
|
||||
import type { LoadState, PlatformWithModelsInput } from '../../types';
|
||||
@@ -23,6 +23,7 @@ import { ModelCatalogCard } from './ModelCatalogCard';
|
||||
export function PlatformManagementPanel(props: {
|
||||
baseModels: BaseModelCatalogItem[];
|
||||
message: string;
|
||||
networkProxyConfig: { globalHttpProxy?: string; globalHttpProxySet: boolean; globalHttpProxySource?: string } | null;
|
||||
platforms: IntegrationPlatform[];
|
||||
platformModels: PlatformModel[];
|
||||
pricingRuleSets: PricingRuleSet[];
|
||||
@@ -37,6 +38,7 @@ export function PlatformManagementPanel(props: {
|
||||
const [modelQuery, setModelQuery] = useState('');
|
||||
const [selectedPlatformId, setSelectedPlatformId] = useState('');
|
||||
const [validationMessage, setValidationMessage] = useState('');
|
||||
const [globalProxyNoticeOpen, setGlobalProxyNoticeOpen] = useState(false);
|
||||
const [editingPlatform, setEditingPlatform] = useState<IntegrationPlatform | null>(null);
|
||||
const [pendingDeletePlatform, setPendingDeletePlatform] = useState<IntegrationPlatform | null>(null);
|
||||
const providerMap = useMemo(() => new Map(props.providers.map((item) => [item.providerKey, item])), [props.providers]);
|
||||
@@ -109,6 +111,17 @@ export function PlatformManagementPanel(props: {
|
||||
});
|
||||
}
|
||||
|
||||
function updateProxyMode(proxyMode: PlatformWizardForm['proxyMode']) {
|
||||
if (proxyMode === 'global') {
|
||||
setGlobalProxyNoticeOpen(true);
|
||||
}
|
||||
setForm({
|
||||
...form,
|
||||
proxyMode,
|
||||
httpProxy: proxyMode === 'custom' ? form.httpProxy : '',
|
||||
});
|
||||
}
|
||||
|
||||
async function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const validationMessage = validatePlatformForm(form, selectedModels.length, { allowEmptyCredentials: Boolean(editingPlatform) });
|
||||
@@ -254,6 +267,30 @@ export function PlatformManagementPanel(props: {
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
<FormSection icon={<Globe2 size={16} />} title="网络代理">
|
||||
<Label>
|
||||
代理模式
|
||||
<Select
|
||||
value={form.proxyMode}
|
||||
onChange={(event) => updateProxyMode(event.target.value as PlatformWizardForm['proxyMode'])}
|
||||
>
|
||||
<option value="none">不使用代理</option>
|
||||
<option value="global">使用全局代理</option>
|
||||
<option value="custom">自定义代理</option>
|
||||
</Select>
|
||||
</Label>
|
||||
{form.proxyMode === 'custom' && (
|
||||
<Label>
|
||||
HTTP 代理
|
||||
<Input
|
||||
value={form.httpProxy}
|
||||
placeholder="http://127.0.0.1:7890"
|
||||
onChange={(event) => setForm({ ...form, httpProxy: event.target.value })}
|
||||
/>
|
||||
</Label>
|
||||
)}
|
||||
</FormSection>
|
||||
|
||||
<FormSection icon={<SlidersHorizontal size={16} />} title="路由与计费">
|
||||
<Label>
|
||||
定价规则
|
||||
@@ -296,6 +333,19 @@ export function PlatformManagementPanel(props: {
|
||||
/>
|
||||
</FormSection>
|
||||
</FormDialog>
|
||||
<ConfirmDialog
|
||||
cancelLabel="关闭"
|
||||
confirmLabel="知道了"
|
||||
confirmVariant="default"
|
||||
description="使用全局代理前,请确认网关服务已设置 AI_GATEWAY_GLOBAL_HTTP_PROXY 或 GLOBAL_HTTP_PROXY 环境变量;也兼容 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY。修改环境变量后需要重启服务才会生效。"
|
||||
loading={props.state === 'loading'}
|
||||
open={globalProxyNoticeOpen}
|
||||
title="使用全局代理"
|
||||
onCancel={() => setGlobalProxyNoticeOpen(false)}
|
||||
onConfirm={() => setGlobalProxyNoticeOpen(false)}
|
||||
>
|
||||
<p className="platformProxyStatus">当前设置代理服务器:{globalProxyStatusText(props.networkProxyConfig)}</p>
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
confirmLabel="删除平台"
|
||||
description="已绑定的模型会一并删除,删除后不可恢复。"
|
||||
@@ -868,6 +918,7 @@ function platformToForm(
|
||||
const config = platform.config ?? {};
|
||||
const retryPolicy = platform.retryPolicy ?? {};
|
||||
const rateLimitPolicy = platform.rateLimitPolicy ?? {};
|
||||
const networkProxy = readNetworkProxyConfig(config);
|
||||
const currentModels = platformModels.filter((model) => model.platformId === platform.id);
|
||||
return {
|
||||
...createEmptyPlatformForm(platform.provider, defaults),
|
||||
@@ -892,6 +943,8 @@ function platformToForm(
|
||||
tpmLimit: readLimit(rateLimitPolicy, 'tpm_total'),
|
||||
concurrencyLimit: readLimit(rateLimitPolicy, 'concurrent'),
|
||||
testMode: readBoolean(config, 'testMode', false),
|
||||
proxyMode: networkProxy.proxyMode,
|
||||
httpProxy: networkProxy.httpProxy,
|
||||
supportBase64Input: readBoolean(config, 'supportBase64Input', true),
|
||||
supportUrlInput: readBoolean(config, 'supportUrlInput', true),
|
||||
selectedModelIds: platformModelBaseIds(platform, baseModels, currentModels),
|
||||
@@ -969,6 +1022,16 @@ function readBoolean(source: Record<string, unknown>, key: string, fallback: boo
|
||||
return typeof value === 'boolean' ? value : fallback;
|
||||
}
|
||||
|
||||
function readNetworkProxyConfig(config: Record<string, unknown>): Pick<PlatformWizardForm, 'proxyMode' | 'httpProxy'> {
|
||||
const networkProxy = readRecord(config.networkProxy);
|
||||
const mode = readString(networkProxy.mode || config.proxyMode);
|
||||
const httpProxy = readString(networkProxy.httpProxy || config.httpProxy);
|
||||
if (mode === 'global' || mode === 'custom') {
|
||||
return { proxyMode: mode, httpProxy };
|
||||
}
|
||||
return { proxyMode: 'none', httpProxy: '' };
|
||||
}
|
||||
|
||||
function readNumber(source: Record<string, unknown>, key: string) {
|
||||
const value = source[key];
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
||||
@@ -1056,7 +1119,19 @@ function platformRuntimeSummary(platform: IntegrationPlatform) {
|
||||
const retryPolicy = platform.retryPolicy ?? {};
|
||||
const retryEnabled = readBoolean(retryPolicy, 'enabled', true);
|
||||
const maxAttempts = readNumber(retryPolicy, 'maxAttempts') ?? 2;
|
||||
return `优先级 ${platform.priority} · ${retryEnabled ? `最多重试 ${maxAttempts} 次` : '不重试'}`;
|
||||
return `优先级 ${platform.priority} · ${retryEnabled ? `最多重试 ${maxAttempts} 次` : '不重试'} · ${proxyModeText(readNetworkProxyConfig(platform.config ?? {}).proxyMode)}`;
|
||||
}
|
||||
|
||||
function proxyModeText(mode: PlatformWizardForm['proxyMode']) {
|
||||
if (mode === 'global') return '全局代理';
|
||||
if (mode === 'custom') return '自定义代理';
|
||||
return '直连';
|
||||
}
|
||||
|
||||
function globalProxyStatusText(config: { globalHttpProxy?: string; globalHttpProxySet: boolean } | null) {
|
||||
if (!config) return '读取中';
|
||||
const proxy = config.globalHttpProxy?.trim() ?? '';
|
||||
return config.globalHttpProxySet && proxy ? proxy : '未设置';
|
||||
}
|
||||
|
||||
function formatLimit(value: number) {
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface PlatformWizardForm {
|
||||
tpmLimit: string;
|
||||
concurrencyLimit: string;
|
||||
testMode: boolean;
|
||||
proxyMode: 'none' | 'global' | 'custom';
|
||||
httpProxy: string;
|
||||
supportBase64Input: boolean;
|
||||
supportUrlInput: boolean;
|
||||
modelDiscountFactor: string;
|
||||
@@ -75,6 +77,8 @@ export function createEmptyPlatformForm(provider = '', defaults?: ProviderConnec
|
||||
tpmLimit: '',
|
||||
concurrencyLimit: '',
|
||||
testMode: false,
|
||||
proxyMode: 'none',
|
||||
httpProxy: '',
|
||||
supportBase64Input: true,
|
||||
supportUrlInput: true,
|
||||
modelDiscountFactor: '',
|
||||
@@ -130,6 +134,7 @@ export function platformPayload(form: PlatformWizardForm, options: { preserveEmp
|
||||
credentials,
|
||||
config: {
|
||||
testMode: form.testMode,
|
||||
networkProxy: networkProxyPayload(form),
|
||||
supportBase64Input: form.supportBase64Input,
|
||||
supportUrlInput: form.supportUrlInput,
|
||||
source: 'gateway-admin',
|
||||
@@ -250,6 +255,7 @@ export function validatePlatformForm(form: PlatformWizardForm, selectedCount: nu
|
||||
if (form.authType === 'AccessKey-SecretKey' && (!form.accessKey.trim() || !form.secretKey.trim()) && !form.testMode) {
|
||||
return '请填写 AccessKey 和 SecretKey,或开启测试模式。';
|
||||
}
|
||||
if (form.proxyMode === 'custom' && !form.httpProxy.trim()) return '请填写自定义 HTTP 代理地址。';
|
||||
if (selectedCount === 0) return '请至少添加一个模型。';
|
||||
return '';
|
||||
}
|
||||
@@ -322,6 +328,16 @@ function rateLimitPolicyPayload(form: Pick<PlatformWizardForm, 'rpmLimit' | 'rps
|
||||
return rules.length ? { rules } : {};
|
||||
}
|
||||
|
||||
function networkProxyPayload(form: PlatformWizardForm) {
|
||||
if (form.proxyMode === 'custom') {
|
||||
return {
|
||||
mode: 'custom',
|
||||
httpProxy: form.httpProxy.trim(),
|
||||
};
|
||||
}
|
||||
return { mode: form.proxyMode };
|
||||
}
|
||||
|
||||
function limitRule(metric: string, value: string, windowSeconds = 60) {
|
||||
const limit = positiveNumber(value, 0);
|
||||
if (!limit) return undefined;
|
||||
|
||||
Reference in New Issue
Block a user