feat(admin): 添加网络代理配置和钱包交易功能

- 在管理面板中集成网络代理配置显示和平台代理设置
- 添加钱包摘要和交易列表API接口及数据管理
- 实现SSE流式响应中的错误处理机制
- 添加全局HTTP代理环境变量配置支持
- 更新平台表单以支持代理模式选择和自定义代理地址
- 集成钱包交易查询过滤和分页功能
- 优化API错误详情解析和显示格式
This commit is contained in:
2026-05-11 23:02:10 +08:00
parent c992f1de60
commit f550c0acd5
30 changed files with 1455 additions and 76 deletions
+91 -5
View File
@@ -7,11 +7,14 @@ import type {
GatewayAccessRuleUpsertRequest,
GatewayApiKey,
GatewayAuditLog,
GatewayNetworkProxyConfig,
GatewayTenantUpsertRequest,
GatewayTask,
GatewayUserUpsertRequest,
GatewayTenant,
GatewayUser,
GatewayWalletAccount,
GatewayWalletTransaction,
IntegrationPlatform,
ModelCatalogResponse,
PlatformModel,
@@ -39,9 +42,11 @@ import {
deleteTenant,
deleteUserGroup,
getHealth,
getNetworkProxyConfig,
getTask,
listAuditLogs,
getWalletSummary,
listAccessRules,
listAuditLogs,
listApiKeyAccessRules,
listApiKeys,
listBaseModels,
@@ -55,6 +60,7 @@ import {
listPricingRuleSets,
listRuntimePolicySets,
listTasks,
listWalletTransactions,
listPublicBaseModels,
listPublicCatalogProviders,
listRateLimitWindows,
@@ -112,6 +118,7 @@ import type {
RegisterForm,
TaskForm,
WorkspaceTaskQuery,
WorkspaceTransactionQuery,
WorkspaceSection,
} from './types';
@@ -121,6 +128,7 @@ type DataKey =
| 'playgroundApiKeys'
| 'playgroundModels'
| 'modelCatalog'
| 'networkProxyConfig'
| 'platforms'
| 'models'
| 'providers'
@@ -133,6 +141,8 @@ type DataKey =
| 'users'
| 'userGroups'
| 'tasks'
| 'wallet'
| 'walletTransactions'
| 'accessRules'
| 'auditLogs'
| 'apiKeys';
@@ -143,6 +153,7 @@ export function App() {
const [adminSection, setAdminSection] = useState<AdminSection>(initialRoute.adminSection);
const [workspaceSection, setWorkspaceSection] = useState<WorkspaceSection>(initialRoute.workspaceSection);
const [workspaceTaskQuery, setWorkspaceTaskQuery] = useState<WorkspaceTaskQuery>(initialRoute.workspaceTaskQuery);
const [workspaceTransactionQuery, setWorkspaceTransactionQuery] = useState<WorkspaceTransactionQuery>(() => defaultWorkspaceTransactionQuery());
const [apiDocSection, setApiDocSection] = useState<ApiDocSection>(initialRoute.apiDocSection);
const [playgroundMode, setPlaygroundMode] = useState<PlaygroundMode>(initialRoute.playgroundMode);
const [token, setToken] = useState(readStoredAccessToken);
@@ -159,6 +170,7 @@ export function App() {
summary: { modelCount: 0, sourceCount: 0 },
});
const [playgroundModels, setPlaygroundModels] = useState<PlatformModel[]>([]);
const [networkProxyConfig, setNetworkProxyConfig] = useState<GatewayNetworkProxyConfig | null>(null);
const [providers, setProviders] = useState<CatalogProvider[]>([]);
const [baseModels, setBaseModels] = useState<BaseModelCatalogItem[]>([]);
const [pricingRules, setPricingRules] = useState<PricingRule[]>([]);
@@ -179,6 +191,9 @@ export function App() {
const [taskResult, setTaskResult] = useState<GatewayTask | null>(null);
const [tasks, setTasks] = useState<GatewayTask[]>([]);
const [taskTotal, setTaskTotal] = useState(0);
const [walletAccounts, setWalletAccounts] = useState<GatewayWalletAccount[]>([]);
const [walletTransactions, setWalletTransactions] = useState<GatewayWalletTransaction[]>([]);
const [walletTransactionTotal, setWalletTransactionTotal] = useState(0);
const [coreState, setCoreState] = useState<LoadState>('idle');
const [coreMessage, setCoreMessage] = useState('');
const [state, setState] = useState<LoadState>('idle');
@@ -187,6 +202,8 @@ export function App() {
const loadingDataKeysRef = useRef(new Set<DataKey>());
const loadedTaskQueryKeyRef = useRef('');
const currentTaskQueryKeyRef = useRef('');
const loadedTransactionQueryKeyRef = useRef('');
const currentTransactionQueryKeyRef = useRef('');
const { removeBaseModel, removeProvider, resetAllBaseModelsToDefault, resetBaseModelToDefault, saveBaseModel, saveProvider } = useCatalogOperations({
setBaseModels,
setCoreMessage,
@@ -207,14 +224,16 @@ export function App() {
token,
});
const taskListRequestKey = workspaceTaskQueryKey(workspaceTaskQuery);
const transactionListRequestKey = workspaceTransactionQueryKey(workspaceTransactionQuery);
currentTaskQueryKeyRef.current = taskListRequestKey;
currentTransactionQueryKeyRef.current = transactionListRequestKey;
useEffect(() => {
void ensureData(['health']);
}, []);
useEffect(() => {
void ensureRouteData(token);
}, [activePage, adminSection, taskListRequestKey, workspaceSection, token]);
}, [activePage, adminSection, taskListRequestKey, transactionListRequestKey, workspaceSection, token]);
useEffect(() => {
function handlePopState() {
applyRoute(parseAppRoute());
@@ -249,6 +268,7 @@ export function App() {
baseModels,
modelCatalog,
models,
networkProxyConfig,
platforms,
pricingRules,
pricingRuleSets,
@@ -260,7 +280,9 @@ export function App() {
tenants,
userGroups,
users,
}), [accessRules, apiKeys, auditLogs, baseModels, modelCatalog, models, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runtimePolicySets, taskResult, tasks, tenants, userGroups, users]);
walletAccounts,
walletTransactions,
}), [accessRules, apiKeys, auditLogs, baseModels, modelCatalog, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runtimePolicySets, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions]);
async function refresh(nextToken = token) {
await ensureRouteData(nextToken, true);
@@ -275,6 +297,10 @@ export function App() {
loadedDataKeysRef.current.delete('tasks');
loadingDataKeysRef.current.delete('tasks');
}
if (activePage === 'workspace' && workspaceSection === 'transactions' && loadedTransactionQueryKeyRef.current !== transactionListRequestKey) {
loadedDataKeysRef.current.delete('walletTransactions');
loadingDataKeysRef.current.delete('walletTransactions');
}
await ensureData(dataKeysForRoute(activePage, adminSection, workspaceSection, Boolean(nextToken)), nextToken, force);
}
@@ -326,6 +352,9 @@ export function App() {
case 'modelCatalog':
setModelCatalog(await listModelCatalog(nextToken));
return;
case 'networkProxyConfig':
setNetworkProxyConfig(await getNetworkProxyConfig(nextToken));
return;
case 'playgroundModels':
setPlaygroundModels((await listPlayableModels(nextToken)).items);
return;
@@ -373,6 +402,20 @@ export function App() {
loadedTaskQueryKeyRef.current = requestKey;
}
return;
case 'wallet': {
const response = await getWalletSummary(nextToken);
setWalletAccounts(response.accounts);
return;
}
case 'walletTransactions': {
const requestKey = transactionListRequestKey;
const response = await listWalletTransactions(nextToken, { ...normalizeWorkspaceTransactionQuery(workspaceTransactionQuery), direction: 'debit' });
if (requestKey !== currentTransactionQueryKeyRef.current) return;
setWalletTransactions(response.items);
setWalletTransactionTotal(response.total ?? response.items.length);
loadedTransactionQueryKeyRef.current = requestKey;
return;
}
case 'accessRules':
setAccessRules((await (activePage === 'workspace' && workspaceSection === 'apiKeys'
? listApiKeyAccessRules(nextToken)
@@ -707,7 +750,7 @@ export function App() {
const detail = await getTask(credential, response.task.id);
setTaskResult(detail);
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
invalidateDataKeys('tasks');
invalidateDataKeys('tasks', 'wallet', 'walletTransactions');
setCoreState('ready');
setCoreMessage(`${taskForm.kind} 已通过 ${apiKeySecret ? '本地 API Key' : '当前 Access Token'} 完成 simulation。`);
} catch (err) {
@@ -726,6 +769,7 @@ export function App() {
setModels([]);
setModelCatalog({ items: [], filters: { capabilities: [], providers: [] }, summary: { modelCount: 0, sourceCount: 0 } });
setPlaygroundModels([]);
setNetworkProxyConfig(null);
setProviders([]);
setBaseModels([]);
setPricingRules([]);
@@ -744,6 +788,10 @@ export function App() {
setTaskResult(null);
setTasks([]);
setTaskTotal(0);
setWalletAccounts([]);
setWalletTransactions([]);
setWalletTransactionTotal(0);
setWorkspaceTransactionQuery(defaultWorkspaceTransactionQuery());
setCoreMessage('');
navigatePath('/');
}
@@ -849,12 +897,15 @@ export function App() {
state={coreState}
taskQuery={workspaceTaskQuery}
taskTotal={taskTotal}
transactionQuery={workspaceTransactionQuery}
transactionTotal={walletTransactionTotal}
onBatchAccessRules={batchSaveAPIKeyAccessRules}
onDeleteApiKey={removeAPIKey}
onApiKeyFormChange={setApiKeyForm}
onSectionChange={navigateWorkspaceSection}
onSubmitApiKey={submitAPIKey}
onTaskQueryChange={navigateWorkspaceTaskQuery}
onTransactionQueryChange={setWorkspaceTransactionQuery}
onUseApiKeyForPlayground={useApiKeyForPlayground}
/>
) : (
@@ -1010,6 +1061,39 @@ function nonEmptyRecord(value: Record<string, unknown> | undefined) {
return value && Object.keys(value).length > 0 ? value : undefined;
}
function defaultWorkspaceTransactionQuery(): WorkspaceTransactionQuery {
return {
query: '',
createdFrom: '',
createdTo: '',
page: 1,
pageSize: 10,
};
}
function normalizeWorkspaceTransactionQuery(query: WorkspaceTransactionQuery): WorkspaceTransactionQuery {
return {
query: query.query.trim(),
createdFrom: query.createdFrom.trim(),
createdTo: query.createdTo.trim(),
page: positiveInt(query.page, 1),
pageSize: clampTransactionPageSize(query.pageSize),
};
}
function workspaceTransactionQueryKey(query: WorkspaceTransactionQuery) {
return JSON.stringify(normalizeWorkspaceTransactionQuery(query));
}
function positiveInt(value: number, fallback: number) {
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
}
function clampTransactionPageSize(value: number) {
const normalized = positiveInt(value, 10);
return Math.min(100, Math.max(1, normalized));
}
function dataKeysForRoute(
activePage: PageKey,
adminSection: AdminSection,
@@ -1027,8 +1111,10 @@ function dataKeysForRoute(
if (activePage === 'workspace') {
if (workspaceSection === 'overview') return ['users', 'userGroups', 'apiKeys'];
if (workspaceSection === 'billing') return ['wallet'];
if (workspaceSection === 'apiKeys') return ['apiKeys', 'accessRules', 'playgroundModels'];
if (workspaceSection === 'tasks') return ['tasks'];
if (workspaceSection === 'transactions') return ['wallet', 'walletTransactions'];
return [];
}
@@ -1045,7 +1131,7 @@ function dataKeysForRoute(
case 'baseModels':
return ['baseModels', 'providers', 'pricingRuleSets', 'runtimePolicySets'];
case 'platforms':
return ['platforms', 'models', 'providers', 'baseModels', 'pricingRuleSets'];
return ['platforms', 'models', 'providers', 'baseModels', 'pricingRuleSets', 'networkProxyConfig'];
case 'tenants':
return ['tenants', 'userGroups'];
case 'users':