easyai-ai-gateway/apps/web/src/App.tsx

1063 lines
39 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, useMemo, useRef, useState, type FormEvent } from 'react';
import type {
BaseModelCatalogItem,
CatalogProvider,
GatewayAccessRuleBatchRequest,
GatewayAccessRule,
GatewayAccessRuleUpsertRequest,
GatewayApiKey,
GatewayAuditLog,
GatewayTenantUpsertRequest,
GatewayTask,
GatewayUserUpsertRequest,
GatewayTenant,
GatewayUser,
IntegrationPlatform,
ModelCatalogResponse,
PlatformModel,
PricingRule,
PricingRuleSet,
RateLimitWindow,
RuntimePolicySet,
UserGroupUpsertRequest,
UserGroup,
WalletBalanceAdjustmentRequest,
} from '@easyai-ai-gateway/contracts';
import {
batchAccessRules,
batchApiKeyAccessRules,
createAccessRule,
createApiKey,
createGatewayUser,
createPlatform,
createTenant,
createUserGroup,
deleteAccessRule,
deleteApiKey,
deleteGatewayUser,
deletePlatform,
deleteTenant,
deleteUserGroup,
getHealth,
getTask,
listAuditLogs,
listAccessRules,
listApiKeyAccessRules,
listApiKeys,
listBaseModels,
listCatalogProviders,
listModelCatalog,
listModels,
listPlayableApiKeys,
listPlayableModels,
listPlatforms,
listPricingRules,
listPricingRuleSets,
listRuntimePolicySets,
listTasks,
listPublicBaseModels,
listPublicCatalogProviders,
listRateLimitWindows,
listTenants,
listUserGroups,
listUsers,
loginLocalAccount,
registerLocalAccount,
replacePlatformModels,
setUserWalletBalance,
type HealthResponse,
updateAccessRule,
updateGatewayUser,
updatePlatform,
updateTenant,
updateUserGroup,
} from './api';
import type { ConsoleData, StatItem } from './app-state';
import { AppShell } from './components/layout/AppShell';
import { LoginRequiredPanel } from './components/LoginRequiredPanel';
import { useCatalogOperations } from './hooks/useCatalogOperations';
import { usePricingRuleSetOperations } from './hooks/usePricingRuleSetOperations';
import { useRuntimePolicySetOperations } from './hooks/useRuntimePolicySetOperations';
import { persistAccessToken, readStoredAccessToken } from './lib/auth-storage';
import { runTask } from './lib/run-task';
import { AdminPage } from './pages/AdminPage';
import { ApiDocsPage } from './pages/ApiDocsPage';
import { HomePage } from './pages/HomePage';
import { ModelsPage } from './pages/ModelsPage';
import { PlaygroundPage } from './pages/PlaygroundPage';
import { WorkspacePage } from './pages/WorkspacePage';
import {
parseAppRoute,
pathForAdminSection,
pathForApiDocSection,
pathForPage,
pathForPlaygroundMode,
pathForWorkspaceSection,
pathForWorkspaceTaskQuery,
workspaceTaskQueryKey,
type AppRouteState,
} from './routing';
import type {
AdminSection,
ApiDocSection,
ApiKeyForm,
AuthMode,
LoadState,
LoginForm,
PageKey,
PlaygroundMode,
PlatformCreateInput,
PlatformModelBindingInput,
PlatformWithModelsInput,
RegisterForm,
TaskForm,
WorkspaceTaskQuery,
WorkspaceSection,
} from './types';
type DataKey =
| 'health'
| 'publicCatalog'
| 'playgroundApiKeys'
| 'playgroundModels'
| 'modelCatalog'
| 'platforms'
| 'models'
| 'providers'
| 'baseModels'
| 'pricingRules'
| 'pricingRuleSets'
| 'runtimePolicySets'
| 'rateLimitWindows'
| 'tenants'
| 'users'
| 'userGroups'
| 'tasks'
| 'accessRules'
| 'auditLogs'
| 'apiKeys';
export function App() {
const initialRoute = parseAppRoute();
const [activePage, setActivePage] = useState<PageKey>(initialRoute.activePage);
const [adminSection, setAdminSection] = useState<AdminSection>(initialRoute.adminSection);
const [workspaceSection, setWorkspaceSection] = useState<WorkspaceSection>(initialRoute.workspaceSection);
const [workspaceTaskQuery, setWorkspaceTaskQuery] = useState<WorkspaceTaskQuery>(initialRoute.workspaceTaskQuery);
const [apiDocSection, setApiDocSection] = useState<ApiDocSection>(initialRoute.apiDocSection);
const [playgroundMode, setPlaygroundMode] = useState<PlaygroundMode>(initialRoute.playgroundMode);
const [token, setToken] = useState(readStoredAccessToken);
const [externalToken, setExternalToken] = useState('');
const [authMode, setAuthMode] = useState<AuthMode>('login');
const [loginForm, setLoginForm] = useState<LoginForm>({ account: '', password: '' });
const [registerForm, setRegisterForm] = useState<RegisterForm>({ username: '', email: '', password: '', displayName: '', invitationCode: '' });
const [health, setHealth] = useState<HealthResponse | null>(null);
const [platforms, setPlatforms] = useState<IntegrationPlatform[]>([]);
const [models, setModels] = useState<PlatformModel[]>([]);
const [modelCatalog, setModelCatalog] = useState<ModelCatalogResponse>({
items: [],
filters: { capabilities: [], providers: [] },
summary: { modelCount: 0, sourceCount: 0 },
});
const [playgroundModels, setPlaygroundModels] = useState<PlatformModel[]>([]);
const [providers, setProviders] = useState<CatalogProvider[]>([]);
const [baseModels, setBaseModels] = useState<BaseModelCatalogItem[]>([]);
const [pricingRules, setPricingRules] = useState<PricingRule[]>([]);
const [pricingRuleSets, setPricingRuleSets] = useState<PricingRuleSet[]>([]);
const [runtimePolicySets, setRuntimePolicySets] = useState<RuntimePolicySet[]>([]);
const [accessRules, setAccessRules] = useState<GatewayAccessRule[]>([]);
const [auditLogs, setAuditLogs] = useState<GatewayAuditLog[]>([]);
const [rateLimitWindows, setRateLimitWindows] = useState<RateLimitWindow[]>([]);
const [tenants, setTenants] = useState<GatewayTenant[]>([]);
const [users, setUsers] = useState<GatewayUser[]>([]);
const [userGroups, setUserGroups] = useState<UserGroup[]>([]);
const [apiKeys, setApiKeys] = useState<GatewayApiKey[]>([]);
const [apiKeyForm, setApiKeyForm] = useState<ApiKeyForm>({ name: 'Local smoke key', expiresAt: '' });
const [apiKeySecret, setApiKeySecret] = useState('');
const [apiKeySecretsById, setApiKeySecretsById] = useState<Record<string, string>>({});
const [selectedPlaygroundApiKeyId, setSelectedPlaygroundApiKeyId] = useState('');
const [taskForm, setTaskForm] = useState<TaskForm>({ kind: 'chat.completions', model: 'gpt-4o-mini', prompt: '用一句话确认 AI Gateway simulation 链路正常。' });
const [taskResult, setTaskResult] = useState<GatewayTask | null>(null);
const [tasks, setTasks] = useState<GatewayTask[]>([]);
const [taskTotal, setTaskTotal] = useState(0);
const [coreState, setCoreState] = useState<LoadState>('idle');
const [coreMessage, setCoreMessage] = useState('');
const [state, setState] = useState<LoadState>('idle');
const [error, setError] = useState('');
const loadedDataKeysRef = useRef(new Set<DataKey>());
const loadingDataKeysRef = useRef(new Set<DataKey>());
const loadedTaskQueryKeyRef = useRef('');
const currentTaskQueryKeyRef = useRef('');
const { removeBaseModel, removeProvider, resetAllBaseModelsToDefault, resetBaseModelToDefault, saveBaseModel, saveProvider } = useCatalogOperations({
setBaseModels,
setCoreMessage,
setCoreState,
setProviders,
token,
});
const { removePricingRuleSet, savePricingRuleSet } = usePricingRuleSetOperations({
setCoreMessage,
setCoreState,
setPricingRuleSets,
token,
});
const { removeRuntimePolicySet, saveRuntimePolicySet } = useRuntimePolicySetOperations({
setCoreMessage,
setCoreState,
setRuntimePolicySets,
token,
});
const taskListRequestKey = workspaceTaskQueryKey(workspaceTaskQuery);
currentTaskQueryKeyRef.current = taskListRequestKey;
useEffect(() => {
void ensureData(['health']);
}, []);
useEffect(() => {
void ensureRouteData(token);
}, [activePage, adminSection, taskListRequestKey, workspaceSection, token]);
useEffect(() => {
function handlePopState() {
applyRoute(parseAppRoute());
}
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);
const stats = useMemo<StatItem[]>(() => {
const enabledPlatforms = platforms.filter((item) => item.status === 'enabled').length;
const enabledModels = models.filter((item) => item.enabled).length;
const activeProviders = providers.filter((item) => item.status === 'active').length;
const activeRateWindows = rateLimitWindows.filter((item) => item.resetAt >= new Date().toISOString()).length;
return [
{ label: '租户', value: tenants.length, tone: 'cyan' },
{ label: '用户', value: users.length, tone: 'green' },
{ label: '用户组', value: userGroups.length, tone: 'blue' },
{ label: '平台', value: platforms.length, tone: 'blue' },
{ label: '启用平台', value: enabledPlatforms, tone: 'green' },
{ label: '平台模型', value: enabledModels, tone: 'violet' },
{ label: 'Provider', value: activeProviders || providers.length, tone: 'amber' },
{ label: '定价规则', value: pricingRules.length, tone: 'cyan' },
{ label: '运行策略', value: runtimePolicySets.length, tone: 'slate' },
{ label: '访问规则', value: accessRules.length, tone: 'amber' },
{ label: '限流窗口', value: activeRateWindows, tone: 'rose' },
];
}, [accessRules.length, models, platforms, pricingRules.length, providers, rateLimitWindows, runtimePolicySets.length, tenants.length, userGroups.length, users.length]);
const data = useMemo<ConsoleData>(() => ({
accessRules,
auditLogs,
apiKeys,
baseModels,
modelCatalog,
models,
platforms,
pricingRules,
pricingRuleSets,
providers,
rateLimitWindows,
runtimePolicySets,
taskResult,
tasks,
tenants,
userGroups,
users,
}), [accessRules, apiKeys, auditLogs, baseModels, modelCatalog, models, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runtimePolicySets, taskResult, tasks, tenants, userGroups, users]);
async function refresh(nextToken = token) {
await ensureRouteData(nextToken, true);
}
function invalidateDataKeys(...keys: DataKey[]) {
keys.forEach((key) => loadedDataKeysRef.current.delete(key));
}
async function ensureRouteData(nextToken = token, force = false) {
if (activePage === 'workspace' && workspaceSection === 'tasks' && loadedTaskQueryKeyRef.current !== taskListRequestKey) {
loadedDataKeysRef.current.delete('tasks');
loadingDataKeysRef.current.delete('tasks');
}
await ensureData(dataKeysForRoute(activePage, adminSection, workspaceSection, Boolean(nextToken)), nextToken, force);
}
async function ensureData(keys: DataKey[], nextToken = token, force = false) {
const uniqueKeys = Array.from(new Set(keys));
const requestKeys = uniqueKeys.filter((key) => {
if (!force && loadedDataKeysRef.current.has(key)) return false;
if (loadingDataKeysRef.current.has(key)) return false;
return key === 'health' || key === 'publicCatalog' || Boolean(nextToken);
});
if (requestKeys.length === 0) return;
requestKeys.forEach((key) => loadingDataKeysRef.current.add(key));
setState('loading');
setError('');
try {
await Promise.all(requestKeys.map((key) => loadDataKey(key, nextToken)));
requestKeys.forEach((key) => loadedDataKeysRef.current.add(key));
setState('ready');
} catch (err) {
setState('error');
setError(err instanceof Error ? err.message : '加载失败');
} finally {
requestKeys.forEach((key) => loadingDataKeysRef.current.delete(key));
}
}
async function loadDataKey(key: DataKey, nextToken: string) {
switch (key) {
case 'health': {
setHealth(await getHealth());
return;
}
case 'publicCatalog': {
const [providersResult, baseModelsResult] = await Promise.all([
listPublicCatalogProviders(),
listPublicBaseModels(),
]);
setProviders(providersResult.items);
setBaseModels(baseModelsResult.items);
return;
}
case 'platforms':
setPlatforms((await listPlatforms(nextToken)).items);
return;
case 'models':
setModels((await listModels(nextToken)).items);
return;
case 'modelCatalog':
setModelCatalog(await listModelCatalog(nextToken));
return;
case 'playgroundModels':
setPlaygroundModels((await listPlayableModels(nextToken)).items);
return;
case 'playgroundApiKeys': {
const response = await listPlayableApiKeys(nextToken);
setApiKeys(response.items);
setApiKeySecretsById(Object.fromEntries(response.items.map((item) => [item.id, item.secret])));
setSelectedPlaygroundApiKeyId((current) => current && response.items.some((item) => item.id === current) ? current : response.items[0]?.id ?? '');
return;
}
case 'providers':
setProviders((await listCatalogProviders(nextToken)).items);
return;
case 'baseModels':
setBaseModels((await listBaseModels(nextToken)).items);
return;
case 'pricingRules':
setPricingRules((await listPricingRules(nextToken)).items);
return;
case 'pricingRuleSets':
setPricingRuleSets((await listPricingRuleSets(nextToken)).items);
return;
case 'runtimePolicySets':
setRuntimePolicySets((await listRuntimePolicySets(nextToken)).items);
return;
case 'rateLimitWindows':
setRateLimitWindows((await listRateLimitWindows(nextToken)).items);
return;
case 'tenants':
setTenants((await listTenants(nextToken)).items);
return;
case 'users':
setUsers((await listUsers(nextToken)).items);
return;
case 'userGroups':
setUserGroups((await listUserGroups(nextToken)).items);
return;
case 'tasks':
{
const requestKey = taskListRequestKey;
const response = await listTasks(nextToken, workspaceTaskQuery);
if (requestKey !== currentTaskQueryKeyRef.current) return;
setTasks(response.items);
setTaskTotal(response.total ?? response.items.length);
loadedTaskQueryKeyRef.current = requestKey;
}
return;
case 'accessRules':
setAccessRules((await (activePage === 'workspace' && workspaceSection === 'apiKeys'
? listApiKeyAccessRules(nextToken)
: listAccessRules(nextToken))).items);
return;
case 'auditLogs':
setAuditLogs((await listAuditLogs(nextToken)).items);
return;
case 'apiKeys':
setApiKeys((await listApiKeys(nextToken)).items);
}
}
async function submitLogin(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
await authenticate(() => loginLocalAccount(loginForm), '登录失败');
}
async function submitRegister(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
await authenticate(() => registerLocalAccount(registerForm), '注册失败');
}
async function submitExternalToken(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const nextToken = externalToken.trim();
if (!nextToken) {
setError('请填写 access token');
return;
}
persistAccessToken(nextToken);
setToken(nextToken);
await ensureRouteData(nextToken, true);
}
async function authenticate(request: () => Promise<{ accessToken: string }>, fallback: string) {
setState('loading');
setError('');
try {
const response = await request();
persistAccessToken(response.accessToken);
setToken(response.accessToken);
await ensureRouteData(response.accessToken, true);
} catch (err) {
setState('error');
setError(err instanceof Error ? err.message : fallback);
}
}
async function submitAPIKey(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setCoreState('loading');
setCoreMessage('');
try {
const response = await createApiKey(token, {
name: apiKeyForm.name,
scopes: ['chat', 'image', 'video'],
expiresAt: apiKeyForm.expiresAt ? new Date(apiKeyForm.expiresAt).toISOString() : undefined,
});
setApiKeySecret(response.secret);
setApiKeySecretsById((current) => ({ ...current, [response.apiKey.id]: response.secret }));
setSelectedPlaygroundApiKeyId(response.apiKey.id);
setApiKeys((current) => [response.apiKey, ...current.filter((item) => item.id !== response.apiKey.id)]);
setApiKeyForm({ name: '', expiresAt: '' });
setCoreState('ready');
setCoreMessage('API Key 已创建secret 仅展示一次。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '创建 API Key 失败');
throw err;
}
}
async function savePlatformWithModels(input: PlatformWithModelsInput) {
setCoreState('loading');
setCoreMessage('');
try {
const platform = input.platformId
? await updatePlatform(token, input.platformId, input.platform)
: await createPlatform(token, input.platform);
const platformForState = withCredentialPreviewFallback(
platform,
input.platform,
input.platformId ? platforms.find((item) => item.id === input.platformId) : undefined,
);
const modelBindings = input.models.map((modelInput) => mergeExistingPlatformModelInput(modelInput, models, platform.id));
const modelsResponse = await replacePlatformModels(token, platform.id, modelBindings);
setPlatforms((current) => [platformForState, ...current.filter((item) => item.id !== platform.id)]);
setModels((current) => [...current.filter((model) => model.platformId !== platform.id), ...modelsResponse.items]);
invalidateDataKeys('modelCatalog');
setCoreState('ready');
setCoreMessage(input.platformId
? `平台已更新,当前绑定 ${input.models.length} 个模型。`
: `平台已创建,已绑定 ${input.models.length} 个模型。`);
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : input.platformId ? '更新平台失败' : '创建平台失败');
throw err;
}
}
async function removePlatform(platformId: string) {
setCoreState('loading');
setCoreMessage('');
try {
await deletePlatform(token, platformId);
setPlatforms((current) => current.filter((item) => item.id !== platformId));
setModels((current) => current.filter((item) => item.platformId !== platformId));
invalidateDataKeys('modelCatalog');
setCoreState('ready');
setCoreMessage('平台已删除。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '删除平台失败');
throw err;
}
}
async function saveTenant(input: GatewayTenantUpsertRequest, tenantId?: string) {
setCoreState('loading');
setCoreMessage('');
try {
const item = tenantId ? await updateTenant(token, tenantId, input) : await createTenant(token, input);
setTenants((current) => [item, ...current.filter((tenant) => tenant.id !== item.id)]);
setCoreState('ready');
setCoreMessage(tenantId ? '租户已更新。' : '租户已创建。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : tenantId ? '更新租户失败' : '创建租户失败');
throw err;
}
}
async function removeTenant(tenantId: string) {
setCoreState('loading');
setCoreMessage('');
try {
await deleteTenant(token, tenantId);
setTenants((current) => current.filter((tenant) => tenant.id !== tenantId));
setCoreState('ready');
setCoreMessage('租户已删除。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '删除租户失败');
throw err;
}
}
async function saveUser(input: GatewayUserUpsertRequest, userId?: string) {
setCoreState('loading');
setCoreMessage('');
try {
const item = userId ? await updateGatewayUser(token, userId, input) : await createGatewayUser(token, input);
setUsers((current) => [item, ...current.filter((user) => user.id !== item.id)]);
invalidateDataKeys('playgroundModels');
setCoreState('ready');
setCoreMessage(userId ? '用户已更新。' : '用户已创建。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : userId ? '更新用户失败' : '创建用户失败');
throw err;
}
}
async function saveUserWalletBalance(userId: string, input: WalletBalanceAdjustmentRequest) {
setCoreState('loading');
setCoreMessage('');
try {
const response = await setUserWalletBalance(token, userId, input);
setUsers((current) => current.map((user) => user.id === userId
? {
...user,
walletAccounts: [
response.account,
...(user.walletAccounts ?? []).filter((account) => account.id !== response.account.id),
],
}
: user));
setAuditLogs((current) => [response.auditLog, ...current.filter((item) => item.id !== response.auditLog.id)]);
invalidateDataKeys('auditLogs');
setCoreState('ready');
setCoreMessage('用户余额已更新,审计日志已记录。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '更新用户余额失败');
throw err;
}
}
async function removeUser(userId: string) {
setCoreState('loading');
setCoreMessage('');
try {
await deleteGatewayUser(token, userId);
setUsers((current) => current.filter((user) => user.id !== userId));
setCoreState('ready');
setCoreMessage('用户已删除。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '删除用户失败');
throw err;
}
}
async function saveUserGroup(input: UserGroupUpsertRequest, groupId?: string) {
setCoreState('loading');
setCoreMessage('');
try {
const item = groupId ? await updateUserGroup(token, groupId, input) : await createUserGroup(token, input);
setUserGroups((current) => [item, ...current.filter((group) => group.id !== item.id)]);
invalidateDataKeys('modelCatalog');
setCoreState('ready');
setCoreMessage(groupId ? '用户组已更新。' : '用户组已创建。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : groupId ? '更新用户组失败' : '创建用户组失败');
throw err;
}
}
async function removeUserGroup(groupId: string) {
setCoreState('loading');
setCoreMessage('');
try {
await deleteUserGroup(token, groupId);
setUserGroups((current) => current.filter((group) => group.id !== groupId));
setTenants((current) => current.map((tenant) => tenant.defaultUserGroupId === groupId ? { ...tenant, defaultUserGroupId: undefined } : tenant));
setUsers((current) => current.map((user) => user.defaultUserGroupId === groupId ? { ...user, defaultUserGroupId: undefined } : user));
invalidateDataKeys('modelCatalog');
invalidateDataKeys('playgroundModels');
setCoreState('ready');
setCoreMessage('用户组已删除。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '删除用户组失败');
throw err;
}
}
async function removeAPIKey(apiKeyId: string) {
setCoreState('loading');
setCoreMessage('');
try {
await deleteApiKey(token, apiKeyId);
setApiKeys((current) => current.filter((item) => item.id !== apiKeyId));
setAccessRules((current) => current.filter((rule) => !(rule.subjectType === 'api_key' && rule.subjectId === apiKeyId)));
setApiKeySecretsById((current) => {
const next = { ...current };
delete next[apiKeyId];
return next;
});
if (selectedPlaygroundApiKeyId === apiKeyId) setSelectedPlaygroundApiKeyId('');
setCoreState('ready');
setCoreMessage('API Key 已删除,对应权限策略已同步清理。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '删除 API Key 失败');
throw err;
}
}
async function saveAccessRule(input: GatewayAccessRuleUpsertRequest, ruleId?: string) {
setCoreState('loading');
setCoreMessage('');
try {
const item = ruleId ? await updateAccessRule(token, ruleId, input) : await createAccessRule(token, input);
setAccessRules((current) => [item, ...current.filter((rule) => rule.id !== item.id)]);
invalidateDataKeys('playgroundModels', 'modelCatalog');
setCoreState('ready');
setCoreMessage(ruleId ? '访问权限规则已更新。' : '访问权限规则已创建。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : ruleId ? '更新访问权限失败' : '创建访问权限失败');
throw err;
}
}
async function removeAccessRule(ruleId: string) {
setCoreState('loading');
setCoreMessage('');
try {
await deleteAccessRule(token, ruleId);
setAccessRules((current) => current.filter((rule) => rule.id !== ruleId));
invalidateDataKeys('playgroundModels', 'modelCatalog');
setCoreState('ready');
setCoreMessage('访问权限规则已删除。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '删除访问权限失败');
throw err;
}
}
async function batchSaveAccessRules(input: GatewayAccessRuleBatchRequest) {
setCoreState('loading');
setCoreMessage('');
try {
const response = await batchAccessRules(token, input);
setAccessRules(response.items);
invalidateDataKeys('playgroundModels', 'modelCatalog');
setCoreState('ready');
setCoreMessage('访问权限已更新。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '批量更新访问权限失败');
throw err;
}
}
async function batchSaveAPIKeyAccessRules(input: GatewayAccessRuleBatchRequest) {
setCoreState('loading');
setCoreMessage('');
try {
const response = await batchApiKeyAccessRules(token, input);
setAccessRules(response.items);
setCoreState('ready');
setCoreMessage('API Key 权限已更新。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '批量更新 API Key 权限失败');
throw err;
}
}
async function submitTask(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const credential = apiKeySecret || token;
setCoreState('loading');
setCoreMessage('');
try {
const response = await runTask(credential, taskForm);
const detail = await getTask(credential, response.task.id);
setTaskResult(detail);
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
invalidateDataKeys('tasks');
setCoreState('ready');
setCoreMessage(`${taskForm.kind} 已通过 ${apiKeySecret ? '本地 API Key' : '当前 Access Token'} 完成 simulation。`);
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '测试任务失败');
}
}
function signOut() {
persistAccessToken('');
setToken('');
loadedDataKeysRef.current = new Set(health ? ['health'] : []);
loadingDataKeysRef.current.clear();
setState('idle');
setPlatforms([]);
setModels([]);
setModelCatalog({ items: [], filters: { capabilities: [], providers: [] }, summary: { modelCount: 0, sourceCount: 0 } });
setPlaygroundModels([]);
setProviders([]);
setBaseModels([]);
setPricingRules([]);
setPricingRuleSets([]);
setRuntimePolicySets([]);
setAccessRules([]);
setAuditLogs([]);
setRateLimitWindows([]);
setTenants([]);
setUsers([]);
setUserGroups([]);
setApiKeys([]);
setApiKeySecret('');
setApiKeySecretsById({});
setSelectedPlaygroundApiKeyId('');
setTaskResult(null);
setTasks([]);
setTaskTotal(0);
setCoreMessage('');
navigatePath('/');
}
function showLogin() {
setAuthMode('login');
navigatePath(pathForWorkspaceSection('overview'));
}
function currentRouteState(): AppRouteState {
return { activePage, adminSection, apiDocSection, playgroundMode, workspaceSection, workspaceTaskQuery };
}
function applyRoute(route: AppRouteState) {
setActivePage(route.activePage);
setAdminSection(route.adminSection);
setApiDocSection(route.apiDocSection);
setPlaygroundMode(route.playgroundMode);
setWorkspaceSection(route.workspaceSection);
setWorkspaceTaskQuery(route.workspaceTaskQuery);
}
function navigatePath(path: string) {
if (`${window.location.pathname}${window.location.search}` !== path) {
window.history.pushState(null, '', path);
}
applyRoute(parseAppRoute(path));
}
function navigatePage(page: PageKey) {
navigatePath(pathForPage(page, currentRouteState()));
}
function navigateAdminSection(section: AdminSection) {
navigatePath(pathForAdminSection(section));
}
function navigateWorkspaceSection(section: WorkspaceSection) {
navigatePath(pathForWorkspaceSection(section));
}
function navigateWorkspaceTaskQuery(query: WorkspaceTaskQuery) {
navigatePath(pathForWorkspaceTaskQuery(query));
}
function navigateApiDocSection(section: ApiDocSection) {
navigatePath(pathForApiDocSection(section));
}
function navigatePlaygroundMode(mode: PlaygroundMode) {
navigatePath(pathForPlaygroundMode(mode));
}
function openApiKeyCreation() {
navigatePath(pathForWorkspaceSection('apiKeys'));
}
function useApiKeyForPlayground(apiKeyId?: string) {
if (apiKeyId) setSelectedPlaygroundApiKeyId(apiKeyId);
navigatePath(pathForPlaygroundMode('chat'));
}
const isAuthenticated = Boolean(token);
return (
<AppShell
activePage={activePage}
health={health}
isAuthenticated={isAuthenticated}
state={state}
onNavigate={navigatePage}
onLogin={showLogin}
onRefresh={() => void refresh()}
onSignOut={signOut}
>
{error && <div className="notice">{error}</div>}
{activePage === 'home' && <HomePage onNavigate={navigatePage} onPlaygroundMode={navigatePlaygroundMode} />}
{activePage === 'playground' && (
<PlaygroundPage
apiKeySecretsById={apiKeySecretsById}
apiKeys={apiKeys}
mode={playgroundMode}
models={playgroundModels}
selectedApiKeyId={selectedPlaygroundApiKeyId}
token={token}
onApiKeyChange={setSelectedPlaygroundApiKeyId}
onCreateApiKey={openApiKeyCreation}
onLogin={showLogin}
onModeChange={navigatePlaygroundMode}
/>
)}
{activePage === 'models' && <ModelsPage data={data} />}
{activePage === 'workspace' && (
isAuthenticated ? (
<WorkspacePage
apiKeyForm={apiKeyForm}
apiKeySecret={apiKeySecret}
apiKeySecretsById={apiKeySecretsById}
apiKeyPolicyModels={playgroundModels}
data={data}
message={coreMessage}
section={workspaceSection}
state={coreState}
taskQuery={workspaceTaskQuery}
taskTotal={taskTotal}
onBatchAccessRules={batchSaveAPIKeyAccessRules}
onDeleteApiKey={removeAPIKey}
onApiKeyFormChange={setApiKeyForm}
onSectionChange={navigateWorkspaceSection}
onSubmitApiKey={submitAPIKey}
onTaskQueryChange={navigateWorkspaceTaskQuery}
onUseApiKeyForPlayground={useApiKeyForPlayground}
/>
) : (
<LoginRequiredPanel
authMode={authMode}
externalToken={externalToken}
loginForm={loginForm}
registerForm={registerForm}
state={state}
onAuthModeChange={setAuthMode}
onExternalTokenChange={setExternalToken}
onLoginChange={setLoginForm}
onRegisterChange={setRegisterForm}
onSubmitExternalToken={submitExternalToken}
onSubmitLogin={submitLogin}
onSubmitRegister={submitRegister}
/>
)
)}
{activePage === 'admin' && (
isAuthenticated ? (
<AdminPage
data={data}
operationMessage={coreMessage}
section={adminSection}
stats={stats}
state={coreState}
onDeleteBaseModel={removeBaseModel}
onDeletePlatform={removePlatform}
onDeleteProvider={removeProvider}
onDeletePricingRuleSet={removePricingRuleSet}
onDeleteRuntimePolicySet={removeRuntimePolicySet}
onDeleteAccessRule={removeAccessRule}
onDeleteTenant={removeTenant}
onDeleteUser={removeUser}
onDeleteUserGroup={removeUserGroup}
onSaveBaseModel={saveBaseModel}
onResetAllBaseModels={resetAllBaseModelsToDefault}
onResetBaseModel={resetBaseModelToDefault}
onSavePlatform={savePlatformWithModels}
onSaveProvider={saveProvider}
onSavePricingRuleSet={savePricingRuleSet}
onSaveRuntimePolicySet={saveRuntimePolicySet}
onBatchAccessRules={batchSaveAccessRules}
onSaveAccessRule={saveAccessRule}
onSaveTenant={saveTenant}
onSaveUser={saveUser}
onSetUserWalletBalance={saveUserWalletBalance}
onSaveUserGroup={saveUserGroup}
onSectionChange={navigateAdminSection}
/>
) : (
<LoginRequiredPanel
authMode={authMode}
externalToken={externalToken}
loginForm={loginForm}
registerForm={registerForm}
state={state}
onAuthModeChange={setAuthMode}
onExternalTokenChange={setExternalToken}
onLoginChange={setLoginForm}
onRegisterChange={setRegisterForm}
onSubmitExternalToken={submitExternalToken}
onSubmitLogin={submitLogin}
onSubmitRegister={submitRegister}
/>
)
)}
{activePage === 'docs' && (
<ApiDocsPage
activeDocSection={apiDocSection}
apiKeySecret={apiKeySecret}
canRun={isAuthenticated}
coreMessage={coreMessage}
coreState={coreState}
taskForm={taskForm}
taskResult={taskResult}
onDocSectionChange={navigateApiDocSection}
onLogin={showLogin}
onSubmitTask={submitTask}
onTaskFormChange={setTaskForm}
/>
)}
</AppShell>
);
}
function platformModelIsSelected(model: PlatformModel, selectedModels: PlatformModelBindingInput[]) {
return selectedModels.some((selected) => {
if (selected.baseModelId && model.baseModelId) return selected.baseModelId === model.baseModelId;
return selected.modelName === model.modelName && sameModelTypes(selected.modelType, model.modelType);
});
}
function sameModelTypes(left: string[], right: string[]) {
if (left.length !== right.length) return false;
const rightSet = new Set(right);
return left.every((type) => rightSet.has(type));
}
function mergeExistingPlatformModelInput(input: PlatformModelBindingInput, currentModels: PlatformModel[], platformId: string): PlatformModelBindingInput {
const existing = currentModels.find((model) => model.platformId === platformId && platformModelIsSelected(model, [input]));
if (!existing) return input;
return {
...input,
providerModelName: input.providerModelName ?? existing.providerModelName,
discountFactor: (input.discountFactor ?? existing.discountFactor) || undefined,
pricingRuleSetId: input.pricingRuleSetId ?? existing.pricingRuleSetId,
rateLimitPolicy: input.rateLimitPolicy ?? existing.rateLimitPolicy,
retryPolicy: input.retryPolicy ?? existing.retryPolicy,
runtimePolicyOverride: input.runtimePolicyOverride ?? (existing.runtimePolicyOverride as Record<string, unknown> | undefined),
runtimePolicySetId: input.runtimePolicySetId ?? existing.runtimePolicySetId,
};
}
function withCredentialPreviewFallback(
platform: IntegrationPlatform,
input: PlatformCreateInput,
existing?: IntegrationPlatform,
): IntegrationPlatform {
const responsePreview = nonEmptyRecord(platform.credentialsPreview);
if (responsePreview) return platform;
if (input.credentials !== undefined) {
const inputPreview = maskCredentialsPreview(input.credentials);
return { ...platform, credentialsPreview: inputPreview ?? {} };
}
const existingPreview = nonEmptyRecord(existing?.credentialsPreview);
return existingPreview ? { ...platform, credentialsPreview: existingPreview } : platform;
}
function maskCredentialsPreview(credentials: Record<string, unknown> | undefined) {
if (!credentials || Object.keys(credentials).length === 0) return undefined;
return Object.fromEntries(Object.entries(credentials).map(([key, value]) => [key, maskCredentialValue(value)]));
}
function maskCredentialValue(value: unknown): unknown {
if (typeof value === 'string') return maskSecret(value);
if (Array.isArray(value)) return value.map(maskCredentialValue);
if (value && typeof value === 'object') {
return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([key, nested]) => [key, maskCredentialValue(nested)]));
}
return value;
}
function maskSecret(value: string) {
const trimmed = value.trim();
if (!trimmed) return '';
if (trimmed.length <= 6) return '*'.repeat(trimmed.length);
return `${trimmed.slice(0, 3)}${'*'.repeat(trimmed.length - 6)}${trimmed.slice(-3)}`;
}
function nonEmptyRecord(value: Record<string, unknown> | undefined) {
return value && Object.keys(value).length > 0 ? value : undefined;
}
function dataKeysForRoute(
activePage: PageKey,
adminSection: AdminSection,
workspaceSection: WorkspaceSection,
isAuthenticated: boolean,
): DataKey[] {
if (activePage === 'playground') return isAuthenticated ? ['playgroundModels', 'playgroundApiKeys'] : [];
if (activePage === 'models') {
return isAuthenticated
? ['modelCatalog']
: ['publicCatalog'];
}
if (activePage === 'home' || activePage === 'docs') return [];
if (!isAuthenticated) return [];
if (activePage === 'workspace') {
if (workspaceSection === 'overview') return ['users', 'userGroups', 'apiKeys'];
if (workspaceSection === 'apiKeys') return ['apiKeys', 'accessRules', 'playgroundModels'];
if (workspaceSection === 'tasks') return ['tasks'];
return [];
}
if (activePage !== 'admin') return [];
switch (adminSection) {
case 'overview':
return ['platforms', 'models', 'providers', 'pricingRules', 'runtimePolicySets', 'rateLimitWindows', 'tenants', 'users', 'userGroups', 'accessRules'];
case 'globalModels':
return ['providers'];
case 'pricing':
return ['pricingRuleSets'];
case 'runtime':
return ['runtimePolicySets'];
case 'baseModels':
return ['baseModels', 'providers', 'pricingRuleSets', 'runtimePolicySets'];
case 'platforms':
return ['platforms', 'models', 'providers', 'baseModels', 'pricingRuleSets'];
case 'tenants':
return ['tenants', 'userGroups'];
case 'users':
return ['users', 'tenants', 'userGroups'];
case 'userGroups':
return ['userGroups'];
case 'auditLogs':
return ['auditLogs'];
case 'accessRules':
return ['accessRules', 'userGroups', 'platforms', 'models'];
default:
return [];
}
}