为处理中、失败和清理中的配对展示明确状态与恢复动作,支持放弃并清理、作用域内退役冲突连接,并在 Active 状态引导管理员先安全禁用再使用新接入码。同步修复运行时切换后的浏览器会话残留与确认弹窗焦点约束。 验证:pnpm test;pnpm lint;pnpm build。
1663 lines
63 KiB
TypeScript
1663 lines
63 KiB
TypeScript
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
|
import type {
|
|
BaseModelCatalogItem,
|
|
CatalogProvider,
|
|
ClientCustomizationSettings,
|
|
ClientCustomizationSettingsUpdateRequest,
|
|
FileStorageChannel,
|
|
FileStorageSettings,
|
|
FileStorageSettingsUpdateRequest,
|
|
FileStorageChannelUpsertRequest,
|
|
GatewayAccessRuleBatchRequest,
|
|
GatewayAccessRule,
|
|
GatewayAccessRuleUpsertRequest,
|
|
GatewayApiKey,
|
|
GatewayApiKeyScopeUpdateRequest,
|
|
GatewayAuditLog,
|
|
GatewayNetworkProxyConfig,
|
|
GatewayRunnerPolicy,
|
|
GatewayTenantUpsertRequest,
|
|
GatewayTask,
|
|
GatewayUserUpsertRequest,
|
|
GatewayTenant,
|
|
GatewayUser,
|
|
GatewayWalletAccount,
|
|
GatewayWalletTransaction,
|
|
IntegrationPlatform,
|
|
ModelCatalogResponse,
|
|
ModelRateLimitStatus,
|
|
PlatformDynamicPriorityUpdateRequest,
|
|
PlatformModel,
|
|
PricingRule,
|
|
PricingRuleSet,
|
|
RateLimitWindow,
|
|
RuntimePolicySet,
|
|
SecurityEventConnectionResponse,
|
|
UserGroupUpsertRequest,
|
|
UserGroup,
|
|
WalletRechargeRequest,
|
|
} from '@easyai-ai-gateway/contracts';
|
|
import {
|
|
batchAccessRules,
|
|
batchApiKeyAccessRules,
|
|
createAccessRule,
|
|
createApiKey,
|
|
createFileStorageChannel,
|
|
createGatewayUser,
|
|
createPlatform,
|
|
createTenant,
|
|
createUserGroup,
|
|
connectSecurityEventTransmitter,
|
|
deleteAccessRule,
|
|
deleteApiKey,
|
|
deleteFileStorageChannel,
|
|
deleteGatewayUser,
|
|
deletePlatform,
|
|
deleteTenant,
|
|
deleteUserGroup,
|
|
disconnectSecurityEventConnection,
|
|
GatewayApiError,
|
|
getClientCustomizationSettings,
|
|
getHealth,
|
|
listFileStorageChannels,
|
|
getCurrentUser,
|
|
getFileStorageSettings,
|
|
getNetworkProxyConfig,
|
|
getRunnerPolicy,
|
|
getSecurityEventConnection,
|
|
getWalletSummary,
|
|
listAccessRules,
|
|
listAuditLogs,
|
|
listApiKeyAccessRules,
|
|
listApiKeys,
|
|
listBaseModels,
|
|
listCatalogProviders,
|
|
listCurrentUserGroups,
|
|
listModelCatalog,
|
|
listModelRateLimitStatuses,
|
|
listModels,
|
|
listPlayableApiKeys,
|
|
listPlayableModels,
|
|
listPlatforms,
|
|
listPricingRules,
|
|
listPricingRuleSets,
|
|
listPublicBaseModels,
|
|
listPublicCatalogProviders,
|
|
listRuntimePolicySets,
|
|
listTasks,
|
|
listWalletTransactions,
|
|
listRateLimitWindows,
|
|
listTenants,
|
|
listUserGroups,
|
|
listUsers,
|
|
loginLocalAccount,
|
|
OIDC_BROWSER_SESSION_CREDENTIAL,
|
|
pollTaskUntilSettled,
|
|
registerLocalAccount,
|
|
rechargeUserWalletBalance,
|
|
replacePlatformModels,
|
|
restoreModelRuntimeStatus,
|
|
rotateSecurityEventConnectionCredential,
|
|
type HealthResponse,
|
|
updateAccessRule,
|
|
updateApiKeyScopes,
|
|
updateClientCustomizationSettings,
|
|
updateFileStorageChannel,
|
|
updateFileStorageSettings,
|
|
updateGatewayUser,
|
|
updatePlatform,
|
|
updatePlatformDynamicPriority,
|
|
updateTenant,
|
|
updateUserGroup,
|
|
verifySecurityEventConnection,
|
|
} from './api';
|
|
import type { ConsoleData, StatItem } from './app-state';
|
|
import { AppShell } from './components/layout/AppShell';
|
|
import { LoginRequiredPanel } from './components/LoginRequiredPanel';
|
|
import { OIDCCallbackNotice } from './components/OIDCCallbackNotice';
|
|
import { useCatalogOperations } from './hooks/useCatalogOperations';
|
|
import { usePricingRuleSetOperations } from './hooks/usePricingRuleSetOperations';
|
|
import { useRuntimePolicySetOperations } from './hooks/useRuntimePolicySetOperations';
|
|
import {
|
|
persistAccessToken,
|
|
readStoredAccessToken,
|
|
} from './lib/auth-storage';
|
|
import {
|
|
reconcileOIDCBrowserSessionAfterRuntimeChange,
|
|
restoreOIDCBrowserSession,
|
|
} from './lib/oidc-browser-session';
|
|
import {
|
|
consumeOIDCCallbackError,
|
|
loadOIDCRuntimeConfiguration,
|
|
startOIDCLogin,
|
|
startOIDCLogout,
|
|
} from './lib/oidc';
|
|
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,
|
|
WorkspaceTransactionQuery,
|
|
WorkspaceSection,
|
|
} from './types';
|
|
|
|
type DataKey =
|
|
| 'health'
|
|
| 'currentUser'
|
|
| 'currentUserGroups'
|
|
| 'publicCatalog'
|
|
| 'playgroundApiKeys'
|
|
| 'playgroundModels'
|
|
| 'modelCatalog'
|
|
| 'networkProxyConfig'
|
|
| 'clientCustomizationSettings'
|
|
| 'fileStorageChannels'
|
|
| 'fileStorageSettings'
|
|
| 'securityEventConnection'
|
|
| 'platforms'
|
|
| 'models'
|
|
| 'providers'
|
|
| 'baseModels'
|
|
| 'pricingRules'
|
|
| 'pricingRuleSets'
|
|
| 'runnerPolicy'
|
|
| 'runtimePolicySets'
|
|
| 'rateLimitWindows'
|
|
| 'modelRateLimits'
|
|
| 'tenants'
|
|
| 'users'
|
|
| 'userGroups'
|
|
| 'tasks'
|
|
| 'wallet'
|
|
| 'walletTransactions'
|
|
| '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 [workspaceTransactionQuery, setWorkspaceTransactionQuery] = useState<WorkspaceTransactionQuery>(() => defaultWorkspaceTransactionQuery());
|
|
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 [currentUser, setCurrentUser] = useState<ConsoleData['currentUser']>(null);
|
|
const [currentUserGroups, setCurrentUserGroups] = useState<UserGroup[]>([]);
|
|
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 [networkProxyConfig, setNetworkProxyConfig] = useState<GatewayNetworkProxyConfig | null>(null);
|
|
const [clientCustomizationSettings, setClientCustomizationSettings] = useState<ClientCustomizationSettings | null>(null);
|
|
const [fileStorageChannels, setFileStorageChannels] = useState<FileStorageChannel[]>([]);
|
|
const [fileStorageSettings, setFileStorageSettings] = useState<FileStorageSettings | null>(null);
|
|
const [securityEventConnection, setSecurityEventConnection] = useState<SecurityEventConnectionResponse | null>(null);
|
|
const [providers, setProviders] = useState<CatalogProvider[]>([]);
|
|
const [baseModels, setBaseModels] = useState<BaseModelCatalogItem[]>([]);
|
|
const [pricingRules, setPricingRules] = useState<PricingRule[]>([]);
|
|
const [pricingRuleSets, setPricingRuleSets] = useState<PricingRuleSet[]>([]);
|
|
const [runnerPolicy, setRunnerPolicy] = useState<GatewayRunnerPolicy | null>(null);
|
|
const [runtimePolicySets, setRuntimePolicySets] = useState<RuntimePolicySet[]>([]);
|
|
const [accessRules, setAccessRules] = useState<GatewayAccessRule[]>([]);
|
|
const [auditLogs, setAuditLogs] = useState<GatewayAuditLog[]>([]);
|
|
const [rateLimitWindows, setRateLimitWindows] = useState<RateLimitWindow[]>([]);
|
|
const [modelRateLimits, setModelRateLimits] = useState<ModelRateLimitStatus[]>([]);
|
|
const [modelRateLimitsUpdatedAt, setModelRateLimitsUpdatedAt] = useState<number | null>(null);
|
|
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 [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');
|
|
const [error, setError] = useState('');
|
|
const [oidcCallbackError, setOIDCCallbackError] = useState(() => consumeOIDCCallbackError());
|
|
const [oidcEnabled, setOIDCEnabled] = useState(false);
|
|
const currentCredentialRef = useRef(token);
|
|
const resetAuthenticatedSessionRef = useRef<() => void>(() => undefined);
|
|
currentCredentialRef.current = token;
|
|
resetAuthenticatedSessionRef.current = () => {
|
|
resetAuthenticatedSession();
|
|
setAuthMode('login');
|
|
setError('统一认证配置已变更,当前登录会话已失效,请重新登录。');
|
|
};
|
|
const loadedDataKeysRef = useRef(new Set<DataKey>());
|
|
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,
|
|
setCoreState,
|
|
setProviders,
|
|
token,
|
|
});
|
|
const { removePricingRuleSet, savePricingRuleSet } = usePricingRuleSetOperations({
|
|
setCoreMessage,
|
|
setCoreState,
|
|
setPricingRuleSets,
|
|
token,
|
|
});
|
|
const { removeRuntimePolicySet, saveRunnerPolicy, saveRuntimePolicySet } = useRuntimePolicySetOperations({
|
|
setCoreMessage,
|
|
setCoreState,
|
|
setRunnerPolicy,
|
|
setRuntimePolicySets,
|
|
token,
|
|
});
|
|
const taskListRequestKey = workspaceTaskQueryKey(workspaceTaskQuery);
|
|
const transactionListRequestKey = workspaceTransactionQueryKey(workspaceTransactionQuery);
|
|
currentTaskQueryKeyRef.current = taskListRequestKey;
|
|
currentTransactionQueryKeyRef.current = transactionListRequestKey;
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const applyRestoredSession = (restored: NonNullable<Awaited<ReturnType<typeof restoreOIDCBrowserSession>>>) => {
|
|
if (cancelled) return;
|
|
setCurrentUser(restored.user);
|
|
loadedDataKeysRef.current.add('currentUser');
|
|
setToken(restored.credential);
|
|
setState('idle');
|
|
setError('');
|
|
};
|
|
const loadIdentityRuntime = async (force = false) => {
|
|
const configuration = await loadOIDCRuntimeConfiguration(force);
|
|
if (cancelled) return;
|
|
const identityEnabled = configuration.enabled && configuration.oidcLogin;
|
|
setOIDCEnabled(identityEnabled);
|
|
if (force && currentCredentialRef.current === OIDC_BROWSER_SESSION_CREDENTIAL) {
|
|
await reconcileOIDCBrowserSessionAfterRuntimeChange({
|
|
credential: currentCredentialRef.current,
|
|
oidcEnabled: identityEnabled,
|
|
onReset: () => {
|
|
if (!cancelled) resetAuthenticatedSessionRef.current();
|
|
},
|
|
onRestored: applyRestoredSession,
|
|
});
|
|
return;
|
|
}
|
|
if (oidcCallbackError || readStoredAccessToken() || !identityEnabled) return;
|
|
const restored = await restoreOIDCBrowserSession();
|
|
if (!restored || cancelled) return;
|
|
applyRestoredSession(restored);
|
|
};
|
|
const handleLoadError = (err: unknown) => {
|
|
if (cancelled) return;
|
|
setState('error');
|
|
setError(err instanceof Error ? err.message : '统一认证登录失败');
|
|
};
|
|
const runtimeChanged = () => {
|
|
void loadIdentityRuntime(true).catch(handleLoadError);
|
|
};
|
|
window.addEventListener('identity-runtime-changed', runtimeChanged);
|
|
void loadIdentityRuntime().catch(handleLoadError);
|
|
return () => {
|
|
cancelled = true;
|
|
window.removeEventListener('identity-runtime-changed', runtimeChanged);
|
|
};
|
|
}, [oidcCallbackError]);
|
|
useEffect(() => {
|
|
if (token) setOIDCCallbackError(null);
|
|
}, [token]);
|
|
useEffect(() => {
|
|
void ensureData(['health']);
|
|
}, []);
|
|
useEffect(() => {
|
|
void ensureRouteData(token);
|
|
}, [activePage, adminSection, taskListRequestKey, transactionListRequestKey, workspaceSection, token]);
|
|
useEffect(() => {
|
|
if (!token || activePage !== 'admin' || adminSection !== 'realtimeLoad') return undefined;
|
|
const timer = window.setInterval(() => {
|
|
void Promise.all([listModelRateLimitStatuses(token), listPlatforms(token)])
|
|
.then(([rateLimitResponse, platformResponse]) => {
|
|
setModelRateLimits(rateLimitResponse.items);
|
|
setModelRateLimitsUpdatedAt(Date.now());
|
|
setPlatforms(platformResponse.items);
|
|
loadedDataKeysRef.current.add('modelRateLimits');
|
|
loadedDataKeysRef.current.add('platforms');
|
|
})
|
|
.catch((err) => {
|
|
if (handleAuthExpired(err, token)) return;
|
|
loadedDataKeysRef.current.delete('modelRateLimits');
|
|
loadedDataKeysRef.current.delete('platforms');
|
|
});
|
|
}, 3000);
|
|
return () => window.clearInterval(timer);
|
|
}, [activePage, adminSection, 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,
|
|
clientCustomizationSettings,
|
|
currentUser,
|
|
currentUserGroups,
|
|
fileStorageChannels,
|
|
fileStorageSettings,
|
|
modelCatalog,
|
|
models,
|
|
networkProxyConfig,
|
|
runnerPolicy,
|
|
platforms,
|
|
pricingRules,
|
|
pricingRuleSets,
|
|
providers,
|
|
rateLimitWindows,
|
|
modelRateLimits,
|
|
modelRateLimitsUpdatedAt,
|
|
runtimePolicySets,
|
|
securityEventConnection,
|
|
taskResult,
|
|
tasks,
|
|
tenants,
|
|
userGroups,
|
|
users,
|
|
walletAccounts,
|
|
walletTransactions,
|
|
}), [accessRules, apiKeys, auditLogs, baseModels, clientCustomizationSettings, currentUser, currentUserGroups, fileStorageChannels, fileStorageSettings, modelCatalog, modelRateLimits, modelRateLimitsUpdatedAt, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runnerPolicy, runtimePolicySets, securityEventConnection, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions]);
|
|
|
|
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');
|
|
}
|
|
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);
|
|
}
|
|
|
|
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));
|
|
setError('');
|
|
setState('ready');
|
|
} catch (err) {
|
|
if (handleAuthExpired(err, nextToken)) return;
|
|
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 'currentUser':
|
|
setCurrentUser(await getCurrentUser(nextToken));
|
|
return;
|
|
case 'currentUserGroups':
|
|
setCurrentUserGroups((await listCurrentUserGroups(nextToken)).items);
|
|
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 'networkProxyConfig':
|
|
setNetworkProxyConfig(await getNetworkProxyConfig(nextToken));
|
|
return;
|
|
case 'clientCustomizationSettings':
|
|
setClientCustomizationSettings(await getClientCustomizationSettings(nextToken));
|
|
return;
|
|
case 'fileStorageChannels':
|
|
setFileStorageChannels((await listFileStorageChannels(nextToken)).items);
|
|
return;
|
|
case 'fileStorageSettings':
|
|
setFileStorageSettings(await getFileStorageSettings(nextToken));
|
|
return;
|
|
case 'securityEventConnection':
|
|
setSecurityEventConnection(await getSecurityEventConnection(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 'runnerPolicy':
|
|
setRunnerPolicy(await getRunnerPolicy(nextToken));
|
|
return;
|
|
case 'runtimePolicySets':
|
|
setRuntimePolicySets((await listRuntimePolicySets(nextToken)).items);
|
|
return;
|
|
case 'rateLimitWindows':
|
|
setRateLimitWindows((await listRateLimitWindows(nextToken)).items);
|
|
return;
|
|
case 'modelRateLimits':
|
|
{
|
|
const response = await listModelRateLimitStatuses(nextToken);
|
|
setModelRateLimits(response.items);
|
|
setModelRateLimitsUpdatedAt(Date.now());
|
|
}
|
|
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 '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)
|
|
: 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', 'embedding', 'rerank', 'image', 'video', 'music', 'audio'],
|
|
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', 'modelRateLimits');
|
|
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 savePlatformStatus(platform: IntegrationPlatform, status: 'enabled' | 'disabled') {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const input = platformStatusUpdatePayload(platform, status);
|
|
const updated = await updatePlatform(token, platform.id, input);
|
|
const platformForState = withCredentialPreviewFallback(updated, input, platform);
|
|
setPlatforms((current) => current.map((item) => item.id === platform.id ? platformForState : item));
|
|
invalidateDataKeys('modelCatalog', 'modelRateLimits', 'playgroundModels');
|
|
setCoreState('ready');
|
|
setCoreMessage(status === 'enabled' ? '平台已启用。' : '平台已禁用。');
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : '切换平台状态失败');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function savePlatformDynamicPriority(platformId: string, input: PlatformDynamicPriorityUpdateRequest) {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const state = await updatePlatformDynamicPriority(token, platformId, input);
|
|
setPlatforms((current) => current.map((platform) => platform.id === platformId
|
|
? {
|
|
...platform,
|
|
dynamicPriority: state.dynamicPriority,
|
|
effectivePriority: state.effectivePriority,
|
|
priority: state.priority,
|
|
updatedAt: state.updatedAt,
|
|
}
|
|
: platform));
|
|
setModelRateLimits((current) => current.map((status) => status.platformId === platformId
|
|
? {
|
|
...status,
|
|
platformDynamicPriority: state.dynamicPriority,
|
|
platformEffectivePriority: state.effectivePriority,
|
|
platformPriority: state.priority,
|
|
}
|
|
: status));
|
|
invalidateDataKeys('modelCatalog', 'modelRateLimits', 'platforms', 'playgroundModels');
|
|
setCoreState('ready');
|
|
setCoreMessage(input.reset ? '平台动态优先级已重置。' : '平台动态优先级已更新。');
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : '更新平台动态优先级失败');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function restoreRuntimeModel(platformModelId: string) {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const restored = await restoreModelRuntimeStatus(token, platformModelId);
|
|
setModelRateLimits((current) => current.map((status) => {
|
|
if (status.platformId !== restored.platformId) return status;
|
|
return {
|
|
...status,
|
|
platformStatus: 'enabled',
|
|
platformCooldownUntil: undefined,
|
|
platformDisabledReason: undefined,
|
|
...(status.platformModelId === platformModelId
|
|
? {
|
|
enabled: restored.enabled,
|
|
modelCooldownUntil: restored.modelCooldownUntil,
|
|
}
|
|
: {}),
|
|
};
|
|
}));
|
|
setPlatforms((current) => current.map((platform) => platform.id === restored.platformId
|
|
? {
|
|
...platform,
|
|
status: 'enabled',
|
|
cooldownUntil: undefined,
|
|
}
|
|
: platform));
|
|
setModels((current) => current.map((model) => model.id === platformModelId
|
|
? {
|
|
...model,
|
|
enabled: true,
|
|
cooldownUntil: undefined,
|
|
}
|
|
: model));
|
|
invalidateDataKeys('modelCatalog', 'modelRateLimits', 'models', 'platforms', 'playgroundModels');
|
|
setCoreState('ready');
|
|
setCoreMessage('模型运行状态已恢复。');
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : '恢复模型运行状态失败');
|
|
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', 'modelRateLimits');
|
|
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 rechargeUserWallet(userId: string, input: WalletRechargeRequest) {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const response = await rechargeUserWalletBalance(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 saveAPIKeyScopes(apiKeyId: string, input: GatewayApiKeyScopeUpdateRequest) {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const item = await updateApiKeyScopes(token, apiKeyId, input);
|
|
setApiKeys((current) => current.map((apiKey) => (apiKey.id === item.id ? { ...apiKey, ...item } : apiKey)));
|
|
invalidateDataKeys('playgroundApiKeys', 'playgroundModels', 'modelCatalog');
|
|
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 saveFileStorageChannel(input: FileStorageChannelUpsertRequest, channelId?: string) {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const item = channelId
|
|
? await updateFileStorageChannel(token, channelId, input)
|
|
: await createFileStorageChannel(token, input);
|
|
setFileStorageChannels((current) => [item, ...current.filter((channel) => channel.id !== item.id)]);
|
|
setCoreState('ready');
|
|
setCoreMessage(channelId ? '文件存储渠道已更新。' : '文件存储渠道已新增。');
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : channelId ? '更新文件存储渠道失败' : '新增文件存储渠道失败');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function saveFileStorageSettings(input: FileStorageSettingsUpdateRequest) {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const settings = await updateFileStorageSettings(token, input);
|
|
setFileStorageSettings(settings);
|
|
setCoreState('ready');
|
|
setCoreMessage('文件存储全局策略已更新。');
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : '更新文件存储全局策略失败');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function saveClientCustomizationSettings(input: ClientCustomizationSettingsUpdateRequest) {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const settings = await updateClientCustomizationSettings(token, input);
|
|
setClientCustomizationSettings(settings);
|
|
setCoreState('ready');
|
|
setCoreMessage('客户端自定义设置已更新。');
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : '更新客户端自定义设置失败');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function connectSecurityEvents(transmitterIssuer: string, managementClientId = '', managementClientSecret = '') {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const version = securityEventConnection?.connection?.version;
|
|
const connection = await connectSecurityEventTransmitter(token, transmitterIssuer, managementClientId, managementClientSecret, version);
|
|
setSecurityEventConnection(connection);
|
|
setCoreState('ready');
|
|
setCoreMessage('认证中心安全事件连接正在验证。');
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : '连接认证中心失败');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function refreshSecurityEvents() {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const connection = await getSecurityEventConnection(token);
|
|
setSecurityEventConnection(connection);
|
|
setCoreState('ready');
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : '刷新安全事件连接失败');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function verifySecurityEvents() {
|
|
const connection = securityEventConnection?.connection;
|
|
if (!connection) return;
|
|
setSecurityEventConnection(await verifySecurityEventConnection(token, connection.version));
|
|
}
|
|
|
|
async function rotateSecurityEventsCredential() {
|
|
const connection = securityEventConnection?.connection;
|
|
if (!connection) return;
|
|
setSecurityEventConnection(await rotateSecurityEventConnectionCredential(token, connection.version));
|
|
}
|
|
|
|
async function disconnectSecurityEvents() {
|
|
const connection = securityEventConnection?.connection;
|
|
if (!connection) return;
|
|
setSecurityEventConnection(await disconnectSecurityEventConnection(token, connection.version));
|
|
}
|
|
|
|
async function removeFileStorageChannel(channelId: string) {
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
await deleteFileStorageChannel(token, channelId);
|
|
setFileStorageChannels((current) => current.filter((channel) => channel.id !== channelId));
|
|
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 selectedApiKeySecret = selectedPlaygroundApiKeyId ? apiKeySecretsById[selectedPlaygroundApiKeyId] ?? '' : '';
|
|
const fallbackApiKeySecret = apiKeys.find((item) => Boolean(apiKeySecretsById[item.id]))?.id;
|
|
const credential = selectedApiKeySecret || (fallbackApiKeySecret ? apiKeySecretsById[fallbackApiKeySecret] : '') || apiKeySecret || token;
|
|
const usingLocalAPIKey = Boolean(selectedApiKeySecret || fallbackApiKeySecret || apiKeySecret);
|
|
let credentialLabel = usingLocalAPIKey ? '本地 API Key' : '当前 Access Token';
|
|
if (!usingLocalAPIKey && token === OIDC_BROWSER_SESSION_CREDENTIAL) credentialLabel = '当前登录会话';
|
|
setCoreState('loading');
|
|
setCoreMessage('');
|
|
try {
|
|
const response = await runTask(credential, taskForm);
|
|
if (response.localOnly) {
|
|
setTaskResult(response.task);
|
|
setCoreState('ready');
|
|
setCoreMessage(`${taskForm.kind} 已通过 ${credentialLabel} 完成 simulation。`);
|
|
return;
|
|
}
|
|
const syncTask = (detail: GatewayTask) => {
|
|
setTaskResult(detail);
|
|
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
|
|
};
|
|
const detail = await pollTaskUntilSettled(credential, response.task, { onUpdate: syncTask });
|
|
setTaskResult(detail);
|
|
setTasks((current) => [detail, ...current.filter((item) => item.id !== detail.id)]);
|
|
invalidateDataKeys('tasks', 'wallet', 'walletTransactions');
|
|
setCoreState('ready');
|
|
setCoreMessage(`${taskForm.kind} 已通过 ${credentialLabel} 完成 simulation。`);
|
|
} catch (err) {
|
|
setCoreState('error');
|
|
setCoreMessage(err instanceof Error ? err.message : '测试任务失败');
|
|
}
|
|
}
|
|
|
|
function resetAuthenticatedSession() {
|
|
persistAccessToken('');
|
|
setToken('');
|
|
loadedDataKeysRef.current = new Set(health ? ['health'] : []);
|
|
loadingDataKeysRef.current.clear();
|
|
setState('idle');
|
|
setCurrentUser(null);
|
|
setCurrentUserGroups([]);
|
|
setPlatforms([]);
|
|
setModels([]);
|
|
setModelCatalog({ items: [], filters: { capabilities: [], providers: [] }, summary: { modelCount: 0, sourceCount: 0 } });
|
|
setPlaygroundModels([]);
|
|
setNetworkProxyConfig(null);
|
|
setFileStorageChannels([]);
|
|
setSecurityEventConnection(null);
|
|
setProviders([]);
|
|
setBaseModels([]);
|
|
setPricingRules([]);
|
|
setPricingRuleSets([]);
|
|
setRunnerPolicy(null);
|
|
setRuntimePolicySets([]);
|
|
setAccessRules([]);
|
|
setAuditLogs([]);
|
|
setRateLimitWindows([]);
|
|
setModelRateLimits([]);
|
|
setTenants([]);
|
|
setUsers([]);
|
|
setUserGroups([]);
|
|
setApiKeys([]);
|
|
setApiKeySecret('');
|
|
setApiKeySecretsById({});
|
|
setSelectedPlaygroundApiKeyId('');
|
|
setTaskResult(null);
|
|
setTasks([]);
|
|
setTaskTotal(0);
|
|
setWalletAccounts([]);
|
|
setWalletTransactions([]);
|
|
setWalletTransactionTotal(0);
|
|
setWorkspaceTransactionQuery(defaultWorkspaceTransactionQuery());
|
|
setCoreMessage('');
|
|
}
|
|
|
|
function handleAuthExpired(err: unknown, failedToken: string) {
|
|
if (!failedToken || !(err instanceof GatewayApiError) || err.details.status !== 401) return false;
|
|
resetAuthenticatedSession();
|
|
setError('');
|
|
setAuthMode('login');
|
|
navigatePath(pathForWorkspaceSection('overview'));
|
|
return true;
|
|
}
|
|
|
|
async function signOut() {
|
|
const wasOIDCSession = token === OIDC_BROWSER_SESSION_CREDENTIAL;
|
|
const shouldEndOIDCSession = wasOIDCSession || currentUser?.source === 'oidc';
|
|
if (shouldEndOIDCSession) {
|
|
try {
|
|
if (await startOIDCLogout()) {
|
|
resetAuthenticatedSession();
|
|
return;
|
|
}
|
|
} catch {
|
|
setState('error');
|
|
setError('统一认证退出失败,请重试');
|
|
return;
|
|
}
|
|
}
|
|
resetAuthenticatedSession();
|
|
navigatePath('/');
|
|
}
|
|
|
|
function loginWithOIDC() {
|
|
setState('loading');
|
|
setError('');
|
|
setOIDCCallbackError(null);
|
|
void startOIDCLogin().catch((err) => {
|
|
setState('error');
|
|
setError(err instanceof Error ? err.message : '统一认证登录失败');
|
|
});
|
|
}
|
|
|
|
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) {
|
|
setError('');
|
|
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}
|
|
>
|
|
{oidcCallbackError ? (
|
|
<OIDCCallbackNotice error={oidcCallbackError} onRetry={loginWithOIDC} />
|
|
) : error ? (
|
|
<div className="notice">{error}</div>
|
|
) : null}
|
|
{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}
|
|
token={token}
|
|
taskQuery={workspaceTaskQuery}
|
|
taskTotal={taskTotal}
|
|
transactionQuery={workspaceTransactionQuery}
|
|
transactionTotal={walletTransactionTotal}
|
|
onBatchAccessRules={batchSaveAPIKeyAccessRules}
|
|
onDeleteApiKey={removeAPIKey}
|
|
onApiKeyFormChange={setApiKeyForm}
|
|
onSectionChange={navigateWorkspaceSection}
|
|
onSaveApiKeyScopes={saveAPIKeyScopes}
|
|
onSubmitApiKey={submitAPIKey}
|
|
onTaskQueryChange={navigateWorkspaceTaskQuery}
|
|
onTransactionQueryChange={setWorkspaceTransactionQuery}
|
|
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}
|
|
oidcEnabled={oidcEnabled}
|
|
onOIDCLogin={loginWithOIDC}
|
|
/>
|
|
)
|
|
)}
|
|
{activePage === 'admin' && (
|
|
isAuthenticated ? (
|
|
<AdminPage
|
|
token={token}
|
|
data={data}
|
|
operationMessage={coreMessage}
|
|
section={adminSection}
|
|
stats={stats}
|
|
state={coreState}
|
|
onDeleteBaseModel={removeBaseModel}
|
|
onDeletePlatform={removePlatform}
|
|
onDeleteProvider={removeProvider}
|
|
onDeletePricingRuleSet={removePricingRuleSet}
|
|
onDeleteRuntimePolicySet={removeRuntimePolicySet}
|
|
onDeleteAccessRule={removeAccessRule}
|
|
onDeleteFileStorageChannel={removeFileStorageChannel}
|
|
onDeleteTenant={removeTenant}
|
|
onDeleteUser={removeUser}
|
|
onDeleteUserGroup={removeUserGroup}
|
|
onSaveBaseModel={saveBaseModel}
|
|
onResetAllBaseModels={resetAllBaseModelsToDefault}
|
|
onResetBaseModel={resetBaseModelToDefault}
|
|
onSavePlatform={savePlatformWithModels}
|
|
onSavePlatformDynamicPriority={savePlatformDynamicPriority}
|
|
onRestoreRuntimeModel={restoreRuntimeModel}
|
|
onTogglePlatformStatus={savePlatformStatus}
|
|
onSaveProvider={saveProvider}
|
|
onSavePricingRuleSet={savePricingRuleSet}
|
|
onSaveRunnerPolicy={saveRunnerPolicy}
|
|
onSaveRuntimePolicySet={saveRuntimePolicySet}
|
|
onBatchAccessRules={batchSaveAccessRules}
|
|
onSaveAccessRule={saveAccessRule}
|
|
onSaveFileStorageChannel={saveFileStorageChannel}
|
|
onSaveFileStorageSettings={saveFileStorageSettings}
|
|
onSaveClientCustomizationSettings={saveClientCustomizationSettings}
|
|
onConnectSecurityEvents={connectSecurityEvents}
|
|
onDisconnectSecurityEvents={disconnectSecurityEvents}
|
|
onRefreshSecurityEvents={refreshSecurityEvents}
|
|
onRotateSecurityEventsCredential={rotateSecurityEventsCredential}
|
|
onVerifySecurityEvents={verifySecurityEvents}
|
|
onSaveTenant={saveTenant}
|
|
onSaveUser={saveUser}
|
|
onRechargeUserWalletBalance={rechargeUserWallet}
|
|
onSaveUserGroup={saveUserGroup}
|
|
onClearOperationMessage={() => setCoreMessage('')}
|
|
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}
|
|
oidcEnabled={oidcEnabled}
|
|
onOIDCLogin={loginWithOIDC}
|
|
/>
|
|
)
|
|
)}
|
|
{activePage === 'docs' && (
|
|
<ApiDocsPage
|
|
activeDocSection={apiDocSection}
|
|
apiKeySecretsById={apiKeySecretsById}
|
|
apiKeys={apiKeys}
|
|
canRun={isAuthenticated}
|
|
coreMessage={coreMessage}
|
|
coreState={coreState}
|
|
selectedApiKeyId={selectedPlaygroundApiKeyId}
|
|
taskForm={taskForm}
|
|
taskResult={taskResult}
|
|
onApiKeyChange={setSelectedPlaygroundApiKeyId}
|
|
onCreateApiKey={openApiKeyCreation}
|
|
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 platformStatusUpdatePayload(platform: IntegrationPlatform, status: 'enabled' | 'disabled'): PlatformCreateInput {
|
|
return {
|
|
provider: platform.provider,
|
|
platformKey: platform.platformKey,
|
|
name: platform.name,
|
|
internalName: platform.internalName,
|
|
baseUrl: platform.baseUrl,
|
|
authType: platform.authType,
|
|
config: platform.config ?? {},
|
|
retryPolicy: platform.retryPolicy ?? {},
|
|
rateLimitPolicy: platform.rateLimitPolicy ?? {},
|
|
defaultPricingMode: platform.defaultPricingMode,
|
|
defaultDiscountFactor: platform.defaultDiscountFactor,
|
|
pricingRuleSetId: platform.pricingRuleSetId,
|
|
priority: platform.priority,
|
|
status,
|
|
};
|
|
}
|
|
|
|
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 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,
|
|
workspaceSection: WorkspaceSection,
|
|
isAuthenticated: boolean,
|
|
): DataKey[] {
|
|
if (activePage === 'playground') return isAuthenticated ? ['playgroundModels', 'playgroundApiKeys'] : [];
|
|
if (activePage === 'models') {
|
|
return isAuthenticated
|
|
? ['modelCatalog']
|
|
: ['publicCatalog'];
|
|
}
|
|
if (activePage === 'docs') return isAuthenticated ? ['playgroundApiKeys'] : [];
|
|
if (activePage === 'home') return [];
|
|
if (!isAuthenticated) return [];
|
|
|
|
if (activePage === 'workspace') {
|
|
if (workspaceSection === 'overview') return ['currentUser', 'currentUserGroups', '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 [];
|
|
}
|
|
|
|
if (activePage !== 'admin') return [];
|
|
switch (adminSection) {
|
|
case 'overview':
|
|
return ['platforms', 'models', 'providers', 'pricingRules', 'runtimePolicySets', 'rateLimitWindows', 'modelRateLimits', 'tenants', 'users', 'userGroups', 'accessRules'];
|
|
case 'globalModels':
|
|
return ['providers'];
|
|
case 'pricing':
|
|
return ['pricingRuleSets'];
|
|
case 'runtime':
|
|
return ['runtimePolicySets', 'runnerPolicy'];
|
|
case 'baseModels':
|
|
return ['baseModels', 'providers', 'pricingRuleSets', 'runtimePolicySets'];
|
|
case 'platforms':
|
|
return ['platforms', 'models', 'providers', 'baseModels', 'pricingRuleSets', 'networkProxyConfig'];
|
|
case 'realtimeLoad':
|
|
return ['platforms', 'modelRateLimits'];
|
|
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'];
|
|
case 'systemSettings':
|
|
return ['clientCustomizationSettings', 'fileStorageSettings', 'fileStorageChannels', 'securityEventConnection'];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|