chore: commit pending gateway changes
This commit is contained in:
+487
-93
@@ -1,9 +1,14 @@
|
||||
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
||||
import type {
|
||||
BaseModelCatalogItem,
|
||||
CatalogProvider,
|
||||
GatewayAccessRuleBatchRequest,
|
||||
GatewayAccessRule,
|
||||
GatewayAccessRuleUpsertRequest,
|
||||
GatewayApiKey,
|
||||
GatewayTenantUpsertRequest,
|
||||
GatewayTask,
|
||||
GatewayUserUpsertRequest,
|
||||
GatewayTenant,
|
||||
GatewayUser,
|
||||
IntegrationPlatform,
|
||||
@@ -11,21 +16,36 @@ import type {
|
||||
PricingRule,
|
||||
PricingRuleSet,
|
||||
RateLimitWindow,
|
||||
RuntimePolicySet,
|
||||
UserGroupUpsertRequest,
|
||||
UserGroup,
|
||||
} from '@easyai-ai-gateway/contracts';
|
||||
import {
|
||||
batchAccessRules,
|
||||
createAccessRule,
|
||||
createApiKey,
|
||||
createGatewayUser,
|
||||
createPlatform,
|
||||
createPlatformModel,
|
||||
createTenant,
|
||||
createUserGroup,
|
||||
deleteAccessRule,
|
||||
deleteGatewayUser,
|
||||
deletePlatform,
|
||||
deleteTenant,
|
||||
deleteUserGroup,
|
||||
getHealth,
|
||||
getTask,
|
||||
listAccessRules,
|
||||
listApiKeys,
|
||||
listBaseModels,
|
||||
listCatalogProviders,
|
||||
listModels,
|
||||
listPlayableApiKeys,
|
||||
listPlayableModels,
|
||||
listPlatforms,
|
||||
listPricingRules,
|
||||
listPricingRuleSets,
|
||||
listRuntimePolicySets,
|
||||
listPublicBaseModels,
|
||||
listPublicCatalogProviders,
|
||||
listRateLimitWindows,
|
||||
@@ -34,25 +54,34 @@ import {
|
||||
listUsers,
|
||||
loginLocalAccount,
|
||||
registerLocalAccount,
|
||||
replacePlatformModels,
|
||||
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,
|
||||
type AppRouteState,
|
||||
} from './routing';
|
||||
@@ -64,19 +93,41 @@ import type {
|
||||
LoadState,
|
||||
LoginForm,
|
||||
PageKey,
|
||||
PlatformForm,
|
||||
PlatformModelForm,
|
||||
PlaygroundMode,
|
||||
PlatformCreateInput,
|
||||
PlatformModelBindingInput,
|
||||
PlatformWithModelsInput,
|
||||
RegisterForm,
|
||||
TaskForm,
|
||||
WorkspaceSection,
|
||||
} from './types';
|
||||
|
||||
type DataKey =
|
||||
| 'health'
|
||||
| 'publicCatalog'
|
||||
| 'playgroundApiKeys'
|
||||
| 'playgroundModels'
|
||||
| 'platforms'
|
||||
| 'models'
|
||||
| 'providers'
|
||||
| 'baseModels'
|
||||
| 'pricingRules'
|
||||
| 'pricingRuleSets'
|
||||
| 'runtimePolicySets'
|
||||
| 'rateLimitWindows'
|
||||
| 'tenants'
|
||||
| 'users'
|
||||
| 'userGroups'
|
||||
| 'accessRules'
|
||||
| '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 [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');
|
||||
@@ -85,10 +136,13 @@ export function App() {
|
||||
const [health, setHealth] = useState<HealthResponse | null>(null);
|
||||
const [platforms, setPlatforms] = useState<IntegrationPlatform[]>([]);
|
||||
const [models, setModels] = useState<PlatformModel[]>([]);
|
||||
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 [rateLimitWindows, setRateLimitWindows] = useState<RateLimitWindow[]>([]);
|
||||
const [tenants, setTenants] = useState<GatewayTenant[]>([]);
|
||||
const [users, setUsers] = useState<GatewayUser[]>([]);
|
||||
@@ -96,15 +150,17 @@ export function App() {
|
||||
const [apiKeys, setApiKeys] = useState<GatewayApiKey[]>([]);
|
||||
const [apiKeyForm, setApiKeyForm] = useState<ApiKeyForm>({ name: 'Local smoke key' });
|
||||
const [apiKeySecret, setApiKeySecret] = useState('');
|
||||
const [platformForm, setPlatformForm] = useState<PlatformForm>({ provider: 'openai', platformKey: 'openai-simulation', name: 'OpenAI Simulation', baseUrl: 'https://api.openai.com/v1', pricingRuleSetId: '', defaultDiscountFactor: '1' });
|
||||
const [platformModelForm, setPlatformModelForm] = useState<PlatformModelForm>({ platformId: '', canonicalModelKey: '', modelName: 'gpt-4o-mini', modelAlias: 'gpt-4o-mini', modelType: 'chat', pricingRuleSetId: '', discountFactor: '' });
|
||||
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 [coreState, setCoreState] = useState<LoadState>('idle');
|
||||
const [coreMessage, setCoreMessage] = useState('');
|
||||
const [state, setState] = useState<LoadState>('idle');
|
||||
const [error, setError] = useState('');
|
||||
const { removeBaseModel, removeProvider, saveBaseModel, saveProvider } = useCatalogOperations({
|
||||
const loadedDataKeysRef = useRef(new Set<DataKey>());
|
||||
const loadingDataKeysRef = useRef(new Set<DataKey>());
|
||||
const { removeBaseModel, removeProvider, resetAllBaseModelsToDefault, resetBaseModelToDefault, saveBaseModel, saveProvider } = useCatalogOperations({
|
||||
setBaseModels,
|
||||
setCoreMessage,
|
||||
setCoreState,
|
||||
@@ -117,15 +173,19 @@ export function App() {
|
||||
setPricingRuleSets,
|
||||
token,
|
||||
});
|
||||
const { removeRuntimePolicySet, saveRuntimePolicySet } = useRuntimePolicySetOperations({
|
||||
setCoreMessage,
|
||||
setCoreState,
|
||||
setRuntimePolicySets,
|
||||
token,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void loadPublicCatalog();
|
||||
void ensureData(['health']);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
void refresh(token);
|
||||
}
|
||||
}, []);
|
||||
void ensureRouteData(token);
|
||||
}, [activePage, adminSection, workspaceSection, token]);
|
||||
useEffect(() => {
|
||||
function handlePopState() {
|
||||
applyRoute(parseAppRoute());
|
||||
@@ -147,11 +207,14 @@ export function App() {
|
||||
{ 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' },
|
||||
];
|
||||
}, [models, platforms, pricingRules.length, providers, rateLimitWindows, tenants.length, userGroups.length, users.length]);
|
||||
}, [accessRules.length, models, platforms, pricingRules.length, providers, rateLimitWindows, runtimePolicySets.length, tenants.length, userGroups.length, users.length]);
|
||||
|
||||
const data = useMemo<ConsoleData>(() => ({
|
||||
accessRules,
|
||||
apiKeys,
|
||||
baseModels,
|
||||
models,
|
||||
@@ -160,58 +223,108 @@ export function App() {
|
||||
pricingRuleSets,
|
||||
providers,
|
||||
rateLimitWindows,
|
||||
runtimePolicySets,
|
||||
taskResult,
|
||||
tenants,
|
||||
userGroups,
|
||||
users,
|
||||
}), [apiKeys, baseModels, models, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, taskResult, tenants, userGroups, users]);
|
||||
}), [accessRules, apiKeys, baseModels, models, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runtimePolicySets, taskResult, tenants, userGroups, users]);
|
||||
|
||||
async function refresh(nextToken = token) {
|
||||
await ensureRouteData(nextToken, true);
|
||||
}
|
||||
|
||||
async function ensureRouteData(nextToken = token, force = false) {
|
||||
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 {
|
||||
const responses = await Promise.all([
|
||||
listPlatforms(nextToken),
|
||||
listModels(nextToken),
|
||||
listCatalogProviders(nextToken),
|
||||
listBaseModels(nextToken),
|
||||
listPricingRules(nextToken),
|
||||
listPricingRuleSets(nextToken),
|
||||
listRateLimitWindows(nextToken),
|
||||
listTenants(nextToken),
|
||||
listUsers(nextToken),
|
||||
listUserGroups(nextToken),
|
||||
listApiKeys(nextToken),
|
||||
]);
|
||||
setPlatforms(responses[0].items);
|
||||
setModels(responses[1].items);
|
||||
setProviders(responses[2].items);
|
||||
setBaseModels(responses[3].items);
|
||||
setPricingRules(responses[4].items);
|
||||
setPricingRuleSets(responses[5].items);
|
||||
setRateLimitWindows(responses[6].items);
|
||||
setTenants(responses[7].items);
|
||||
setUsers(responses[8].items);
|
||||
setUserGroups(responses[9].items);
|
||||
setApiKeys(responses[10].items);
|
||||
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 loadPublicCatalog() {
|
||||
try {
|
||||
const [healthResult, providersResult, baseModelsResult] = await Promise.all([
|
||||
getHealth(),
|
||||
listPublicCatalogProviders(),
|
||||
listPublicBaseModels(),
|
||||
]);
|
||||
setHealth(healthResult);
|
||||
setProviders(providersResult.items);
|
||||
setBaseModels(baseModelsResult.items);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '公共模型目录加载失败');
|
||||
|
||||
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 '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 'accessRules':
|
||||
setAccessRules((await listAccessRules(nextToken)).items);
|
||||
return;
|
||||
case 'apiKeys':
|
||||
setApiKeys((await listApiKeys(nextToken)).items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +347,7 @@ export function App() {
|
||||
}
|
||||
persistAccessToken(nextToken);
|
||||
setToken(nextToken);
|
||||
await refresh(nextToken);
|
||||
await ensureRouteData(nextToken, true);
|
||||
}
|
||||
|
||||
async function authenticate(request: () => Promise<{ accessToken: string }>, fallback: string) {
|
||||
@@ -244,7 +357,7 @@ export function App() {
|
||||
const response = await request();
|
||||
persistAccessToken(response.accessToken);
|
||||
setToken(response.accessToken);
|
||||
await refresh(response.accessToken);
|
||||
await ensureRouteData(response.accessToken, true);
|
||||
} catch (err) {
|
||||
setState('error');
|
||||
setError(err instanceof Error ? err.message : fallback);
|
||||
@@ -258,6 +371,8 @@ export function App() {
|
||||
try {
|
||||
const response = await createApiKey(token, { name: apiKeyForm.name, scopes: ['chat', 'image', 'video'] });
|
||||
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)]);
|
||||
setCoreState('ready');
|
||||
setCoreMessage('API Key 已创建,secret 仅展示一次。');
|
||||
@@ -267,54 +382,183 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPlatform(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
async function savePlatformWithModels(input: PlatformWithModelsInput) {
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const platform = await createPlatform(token, {
|
||||
...platformForm,
|
||||
authType: 'bearer',
|
||||
config: { testMode: true },
|
||||
credentials: { mode: 'simulation' },
|
||||
defaultDiscountFactor: Number(platformForm.defaultDiscountFactor) || 1,
|
||||
pricingRuleSetId: platformForm.pricingRuleSetId || undefined,
|
||||
});
|
||||
setPlatforms((current) => [platform, ...current.filter((item) => item.id !== platform.id)]);
|
||||
setPlatformModelForm((current) => ({ ...current, platformId: current.platformId || platform.id }));
|
||||
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]);
|
||||
setCoreState('ready');
|
||||
setCoreMessage('平台已创建,当前阶段平台凭证仅全局管理员可配置。');
|
||||
setCoreMessage(input.platformId
|
||||
? `平台已更新,当前绑定 ${input.models.length} 个模型。`
|
||||
: `平台已创建,已绑定 ${input.models.length} 个模型。`);
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : '创建平台失败');
|
||||
setCoreMessage(err instanceof Error ? err.message : input.platformId ? '更新平台失败' : '创建平台失败');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitPlatformModel(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!platformModelForm.platformId || !platformModelForm.modelName || !platformModelForm.modelType) {
|
||||
setCoreState('error');
|
||||
setCoreMessage('请选择平台并填写模型名。');
|
||||
return;
|
||||
}
|
||||
async function removePlatform(platformId: string) {
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
try {
|
||||
const model = await createPlatformModel(token, platformModelForm.platformId, {
|
||||
canonicalModelKey: platformModelForm.canonicalModelKey || undefined,
|
||||
modelAlias: platformModelForm.modelAlias || platformModelForm.modelName,
|
||||
modelName: platformModelForm.modelName,
|
||||
modelType: platformModelForm.modelType,
|
||||
discountFactor: Number(platformModelForm.discountFactor) || undefined,
|
||||
pricingRuleSetId: platformModelForm.pricingRuleSetId || undefined,
|
||||
retryPolicy: { enabled: true, maxAttempts: 2 },
|
||||
});
|
||||
setModels((current) => [model, ...current.filter((item) => item.id !== model.id)]);
|
||||
await deletePlatform(token, platformId);
|
||||
setPlatforms((current) => current.filter((item) => item.id !== platformId));
|
||||
setModels((current) => current.filter((item) => item.platformId !== platformId));
|
||||
setCoreState('ready');
|
||||
setCoreMessage('平台模型已绑定,可参与 simulation 任务路由。');
|
||||
setCoreMessage('平台已删除。');
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : '绑定平台模型失败');
|
||||
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)]);
|
||||
setCoreState('ready');
|
||||
setCoreMessage(userId ? '用户已更新。' : '用户已创建。');
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : userId ? '更新用户失败' : '创建用户失败');
|
||||
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)]);
|
||||
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));
|
||||
setCoreState('ready');
|
||||
setCoreMessage('用户组已删除。');
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : '删除用户组失败');
|
||||
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)]);
|
||||
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));
|
||||
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);
|
||||
setCoreState('ready');
|
||||
setCoreMessage('访问权限已更新。');
|
||||
} catch (err) {
|
||||
setCoreState('error');
|
||||
setCoreMessage(err instanceof Error ? err.message : '批量更新访问权限失败');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,19 +582,26 @@ export function App() {
|
||||
function signOut() {
|
||||
persistAccessToken('');
|
||||
setToken('');
|
||||
loadedDataKeysRef.current = new Set(health ? ['health'] : []);
|
||||
loadingDataKeysRef.current.clear();
|
||||
setState('idle');
|
||||
setPlatforms([]);
|
||||
setModels([]);
|
||||
setPlaygroundModels([]);
|
||||
setProviders([]);
|
||||
setBaseModels([]);
|
||||
setPricingRules([]);
|
||||
setPricingRuleSets([]);
|
||||
setRuntimePolicySets([]);
|
||||
setAccessRules([]);
|
||||
setRateLimitWindows([]);
|
||||
setTenants([]);
|
||||
setUsers([]);
|
||||
setUserGroups([]);
|
||||
setApiKeys([]);
|
||||
setApiKeySecret('');
|
||||
setApiKeySecretsById({});
|
||||
setSelectedPlaygroundApiKeyId('');
|
||||
setTaskResult(null);
|
||||
setCoreMessage('');
|
||||
navigatePath('/');
|
||||
@@ -362,13 +613,14 @@ export function App() {
|
||||
}
|
||||
|
||||
function currentRouteState(): AppRouteState {
|
||||
return { activePage, adminSection, apiDocSection, workspaceSection };
|
||||
return { activePage, adminSection, apiDocSection, playgroundMode, workspaceSection };
|
||||
}
|
||||
|
||||
function applyRoute(route: AppRouteState) {
|
||||
setActivePage(route.activePage);
|
||||
setAdminSection(route.adminSection);
|
||||
setApiDocSection(route.apiDocSection);
|
||||
setPlaygroundMode(route.playgroundMode);
|
||||
setWorkspaceSection(route.workspaceSection);
|
||||
}
|
||||
|
||||
@@ -395,6 +647,19 @@ export function App() {
|
||||
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 (
|
||||
@@ -409,7 +674,21 @@ export function App() {
|
||||
onSignOut={signOut}
|
||||
>
|
||||
{error && <div className="notice">{error}</div>}
|
||||
{activePage === 'home' && <HomePage onNavigate={navigatePage} />}
|
||||
{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 ? (
|
||||
@@ -422,6 +701,7 @@ export function App() {
|
||||
onApiKeyFormChange={setApiKeyForm}
|
||||
onSectionChange={navigateWorkspaceSection}
|
||||
onSubmitApiKey={submitAPIKey}
|
||||
onUseApiKeyForPlayground={useApiKeyForPlayground}
|
||||
/>
|
||||
) : (
|
||||
<LoginRequiredPanel
|
||||
@@ -445,22 +725,31 @@ export function App() {
|
||||
<AdminPage
|
||||
data={data}
|
||||
operationMessage={coreMessage}
|
||||
platformForm={platformForm}
|
||||
platformModelForm={platformModelForm}
|
||||
section={adminSection}
|
||||
stats={stats}
|
||||
state={coreState}
|
||||
onDeleteBaseModel={removeBaseModel}
|
||||
onDeletePlatform={removePlatform}
|
||||
onDeleteProvider={removeProvider}
|
||||
onDeletePricingRuleSet={removePricingRuleSet}
|
||||
onPlatformFormChange={setPlatformForm}
|
||||
onPlatformModelFormChange={setPlatformModelForm}
|
||||
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}
|
||||
onSaveUserGroup={saveUserGroup}
|
||||
onSectionChange={navigateAdminSection}
|
||||
onSubmitPlatform={submitPlatform}
|
||||
onSubmitPlatformModel={submitPlatformModel}
|
||||
/>
|
||||
) : (
|
||||
<LoginRequiredPanel
|
||||
@@ -497,3 +786,108 @@ export function App() {
|
||||
</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 && selected.modelType === model.modelType;
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
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 ['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'];
|
||||
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 'accessRules':
|
||||
return ['accessRules', 'userGroups', 'platforms', 'models'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user