feat(system): 完成客户端自定义与系统设置
新增管理端客户端自定义标签页、公开与管理端系统设置接口、文件存储设置持久化、数据库迁移、契约类型和 OpenAPI 文档。
This commit is contained in:
+28
-2
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useRef, useState, type FormEvent } from 'react';
|
||||
import type {
|
||||
BaseModelCatalogItem,
|
||||
CatalogProvider,
|
||||
ClientCustomizationSettings,
|
||||
ClientCustomizationSettingsUpdateRequest,
|
||||
FileStorageChannel,
|
||||
FileStorageSettings,
|
||||
FileStorageSettingsUpdateRequest,
|
||||
@@ -52,6 +54,7 @@ import {
|
||||
deleteTenant,
|
||||
deleteUserGroup,
|
||||
GatewayApiError,
|
||||
getClientCustomizationSettings,
|
||||
getHealth,
|
||||
listFileStorageChannels,
|
||||
getCurrentUser,
|
||||
@@ -92,6 +95,7 @@ import {
|
||||
type HealthResponse,
|
||||
updateAccessRule,
|
||||
updateApiKeyScopes,
|
||||
updateClientCustomizationSettings,
|
||||
updateFileStorageChannel,
|
||||
updateFileStorageSettings,
|
||||
updateGatewayUser,
|
||||
@@ -153,6 +157,7 @@ type DataKey =
|
||||
| 'playgroundModels'
|
||||
| 'modelCatalog'
|
||||
| 'networkProxyConfig'
|
||||
| 'clientCustomizationSettings'
|
||||
| 'fileStorageChannels'
|
||||
| 'fileStorageSettings'
|
||||
| 'platforms'
|
||||
@@ -201,6 +206,7 @@ export function App() {
|
||||
});
|
||||
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 [providers, setProviders] = useState<CatalogProvider[]>([]);
|
||||
@@ -321,6 +327,7 @@ export function App() {
|
||||
auditLogs,
|
||||
apiKeys,
|
||||
baseModels,
|
||||
clientCustomizationSettings,
|
||||
currentUser,
|
||||
currentUserGroups,
|
||||
fileStorageChannels,
|
||||
@@ -344,7 +351,7 @@ export function App() {
|
||||
users,
|
||||
walletAccounts,
|
||||
walletTransactions,
|
||||
}), [accessRules, apiKeys, auditLogs, baseModels, currentUser, currentUserGroups, fileStorageChannels, fileStorageSettings, modelCatalog, modelRateLimits, modelRateLimitsUpdatedAt, models, networkProxyConfig, platforms, pricingRuleSets, pricingRules, providers, rateLimitWindows, runnerPolicy, runtimePolicySets, 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, taskResult, tasks, tenants, userGroups, users, walletAccounts, walletTransactions]);
|
||||
|
||||
async function refresh(nextToken = token) {
|
||||
await ensureRouteData(nextToken, true);
|
||||
@@ -424,6 +431,9 @@ export function App() {
|
||||
case 'networkProxyConfig':
|
||||
setNetworkProxyConfig(await getNetworkProxyConfig(nextToken));
|
||||
return;
|
||||
case 'clientCustomizationSettings':
|
||||
setClientCustomizationSettings(await getClientCustomizationSettings(nextToken));
|
||||
return;
|
||||
case 'fileStorageChannels':
|
||||
setFileStorageChannels((await listFileStorageChannels(nextToken)).items);
|
||||
return;
|
||||
@@ -952,6 +962,21 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
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 removeFileStorageChannel(channelId: string) {
|
||||
setCoreState('loading');
|
||||
setCoreMessage('');
|
||||
@@ -1233,6 +1258,7 @@ export function App() {
|
||||
onSaveAccessRule={saveAccessRule}
|
||||
onSaveFileStorageChannel={saveFileStorageChannel}
|
||||
onSaveFileStorageSettings={saveFileStorageSettings}
|
||||
onSaveClientCustomizationSettings={saveClientCustomizationSettings}
|
||||
onSaveTenant={saveTenant}
|
||||
onSaveUser={saveUser}
|
||||
onRechargeUserWalletBalance={rechargeUserWallet}
|
||||
@@ -1452,7 +1478,7 @@ function dataKeysForRoute(
|
||||
case 'accessRules':
|
||||
return ['accessRules', 'userGroups', 'platforms', 'models'];
|
||||
case 'systemSettings':
|
||||
return ['fileStorageSettings', 'fileStorageChannels'];
|
||||
return ['clientCustomizationSettings', 'fileStorageSettings', 'fileStorageChannels'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import type {
|
||||
BaseModelUpsertRequest,
|
||||
CatalogProvider,
|
||||
CatalogProviderUpsertRequest,
|
||||
ClientCustomizationSettings,
|
||||
ClientCustomizationSettingsUpdateRequest,
|
||||
CreatedGatewayApiKey,
|
||||
FileStorageChannel,
|
||||
FileStorageSettings,
|
||||
@@ -974,6 +976,25 @@ export async function updateFileStorageSettings(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getClientCustomizationSettings(token: string): Promise<ClientCustomizationSettings> {
|
||||
return request<ClientCustomizationSettings>('/api/admin/system/client-customization/settings', { token });
|
||||
}
|
||||
|
||||
export async function updateClientCustomizationSettings(
|
||||
token: string,
|
||||
input: ClientCustomizationSettingsUpdateRequest,
|
||||
): Promise<ClientCustomizationSettings> {
|
||||
return request<ClientCustomizationSettings>('/api/admin/system/client-customization/settings', {
|
||||
body: input,
|
||||
method: 'PATCH',
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPublicClientCustomization(): Promise<ClientCustomizationSettings> {
|
||||
return request<ClientCustomizationSettings>('/api/v1/public/client-customization', { auth: false });
|
||||
}
|
||||
|
||||
export async function createFileStorageChannel(
|
||||
token: string,
|
||||
input: FileStorageChannelUpsertRequest,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
AuthUser,
|
||||
BaseModelCatalogItem,
|
||||
CatalogProvider,
|
||||
ClientCustomizationSettings,
|
||||
FileStorageChannel,
|
||||
FileStorageSettings,
|
||||
GatewayAccessRule,
|
||||
@@ -32,6 +33,7 @@ export interface ConsoleData {
|
||||
baseModels: BaseModelCatalogItem[];
|
||||
currentUser: AuthUser | null;
|
||||
currentUserGroups: UserGroup[];
|
||||
clientCustomizationSettings: ClientCustomizationSettings | null;
|
||||
fileStorageChannels: FileStorageChannel[];
|
||||
fileStorageSettings: FileStorageSettings | null;
|
||||
modelCatalog: ModelCatalogResponse;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Boxes, Building2, Gauge, History, KeyRound, Route, ServerCog, Settings,
|
||||
import type {
|
||||
BaseModelUpsertRequest,
|
||||
CatalogProviderUpsertRequest,
|
||||
ClientCustomizationSettingsUpdateRequest,
|
||||
FileStorageChannelUpsertRequest,
|
||||
FileStorageSettingsUpdateRequest,
|
||||
GatewayAccessRuleBatchRequest,
|
||||
@@ -78,6 +79,7 @@ export function AdminPage(props: {
|
||||
onSaveRunnerPolicy: (input: GatewayRunnerPolicyUpsertRequest) => Promise<void>;
|
||||
onSaveRuntimePolicySet: (input: RuntimePolicySetUpsertRequest, policySetId?: string) => Promise<void>;
|
||||
onSaveAccessRule: (input: GatewayAccessRuleUpsertRequest, ruleId?: string) => Promise<void>;
|
||||
onSaveClientCustomizationSettings: (input: ClientCustomizationSettingsUpdateRequest) => Promise<void>;
|
||||
onSaveFileStorageChannel: (input: FileStorageChannelUpsertRequest, channelId?: string) => Promise<void>;
|
||||
onSaveFileStorageSettings: (input: FileStorageSettingsUpdateRequest) => Promise<void>;
|
||||
onSaveTenant: (input: GatewayTenantUpsertRequest, tenantId?: string) => Promise<void>;
|
||||
@@ -185,12 +187,14 @@ export function AdminPage(props: {
|
||||
{props.section === 'systemSettings' && (
|
||||
<SystemSettingsPanel
|
||||
channels={props.data.fileStorageChannels}
|
||||
clientCustomizationSettings={props.data.clientCustomizationSettings}
|
||||
settings={props.data.fileStorageSettings}
|
||||
message={props.operationMessage}
|
||||
state={props.state}
|
||||
onDeleteFileStorageChannel={props.onDeleteFileStorageChannel}
|
||||
onSaveFileStorageChannel={props.onSaveFileStorageChannel}
|
||||
onSaveFileStorageSettings={props.onSaveFileStorageSettings}
|
||||
onSaveClientCustomizationSettings={props.onSaveClientCustomizationSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useEffect, useState, type FormEvent } from 'react';
|
||||
import { Database, Pencil, Plus, RotateCcw, Save, ServerCog, Trash2 } from 'lucide-react';
|
||||
import type { FileStorageChannel, FileStorageChannelUpsertRequest, FileStorageSettings, FileStorageSettingsUpdateRequest } from '@easyai-ai-gateway/contracts';
|
||||
import { Database, Pencil, Plus, RotateCcw, Save, ServerCog, Settings2, Trash2 } from 'lucide-react';
|
||||
import type { ClientCustomizationSettings, ClientCustomizationSettingsUpdateRequest, FileStorageChannel, FileStorageChannelUpsertRequest, FileStorageSettings, FileStorageSettingsUpdateRequest } from '@easyai-ai-gateway/contracts';
|
||||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, FormDialog, Input, Label, Select, Tabs, Textarea } from '../../components/ui';
|
||||
import type { LoadState } from '../../types';
|
||||
|
||||
type SystemSettingsTab = 'fileStorage';
|
||||
type SystemSettingsTab = 'fileStorage' | 'clientCustomization';
|
||||
|
||||
type ClientCustomizationForm = {
|
||||
clientEnglishName: string;
|
||||
clientName: string;
|
||||
iconPath: string;
|
||||
platformName: string;
|
||||
};
|
||||
|
||||
type FileStorageChannelForm = {
|
||||
apiKey: string;
|
||||
@@ -48,10 +55,12 @@ const resultUploadPolicyOptions = [
|
||||
|
||||
export function SystemSettingsPanel(props: {
|
||||
channels: FileStorageChannel[];
|
||||
clientCustomizationSettings: ClientCustomizationSettings | null;
|
||||
message: string;
|
||||
settings: FileStorageSettings | null;
|
||||
state: LoadState;
|
||||
onDeleteFileStorageChannel: (channelId: string) => Promise<void>;
|
||||
onSaveClientCustomizationSettings: (input: ClientCustomizationSettingsUpdateRequest) => Promise<void>;
|
||||
onSaveFileStorageChannel: (input: FileStorageChannelUpsertRequest, channelId?: string) => Promise<void>;
|
||||
onSaveFileStorageSettings: (input: FileStorageSettingsUpdateRequest) => Promise<void>;
|
||||
}) {
|
||||
@@ -60,6 +69,7 @@ export function SystemSettingsPanel(props: {
|
||||
const [editingChannel, setEditingChannel] = useState<FileStorageChannel | null>(null);
|
||||
const [pendingDeleteChannel, setPendingDeleteChannel] = useState<FileStorageChannel | null>(null);
|
||||
const [form, setForm] = useState<FileStorageChannelForm>(() => defaultChannelForm());
|
||||
const [clientCustomizationForm, setClientCustomizationForm] = useState<ClientCustomizationForm>(() => clientCustomizationSettingsToForm(props.clientCustomizationSettings));
|
||||
const [settingsPolicy, setSettingsPolicy] = useState(() => normalizeResultUploadPolicy(props.settings?.resultUploadPolicy));
|
||||
const [localError, setLocalError] = useState('');
|
||||
|
||||
@@ -67,6 +77,10 @@ export function SystemSettingsPanel(props: {
|
||||
setSettingsPolicy(normalizeResultUploadPolicy(props.settings?.resultUploadPolicy));
|
||||
}, [props.settings?.resultUploadPolicy]);
|
||||
|
||||
useEffect(() => {
|
||||
setClientCustomizationForm(clientCustomizationSettingsToForm(props.clientCustomizationSettings));
|
||||
}, [props.clientCustomizationSettings]);
|
||||
|
||||
function openCreateDialog() {
|
||||
setEditingChannel(null);
|
||||
setForm(defaultChannelForm(`server-main-${Date.now().toString(36)}`));
|
||||
@@ -122,6 +136,15 @@ export function SystemSettingsPanel(props: {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveClientCustomization() {
|
||||
setLocalError('');
|
||||
try {
|
||||
await props.onSaveClientCustomizationSettings(clientCustomizationFormToPayload(clientCustomizationForm));
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : '客户端自定义设置保存失败');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pageStack">
|
||||
<Card>
|
||||
@@ -136,7 +159,10 @@ export function SystemSettingsPanel(props: {
|
||||
{(props.message || localError) && <p className="formMessage">{localError || props.message}</p>}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
tabs={[{ value: 'fileStorage', label: '文件存储', icon: <Database size={15} /> }]}
|
||||
tabs={[
|
||||
{ value: 'fileStorage', label: '文件存储', icon: <Database size={15} /> },
|
||||
{ value: 'clientCustomization', label: '客户端自定义', icon: <Settings2 size={15} /> },
|
||||
]}
|
||||
onValueChange={setActiveTab}
|
||||
/>
|
||||
</CardContent>
|
||||
@@ -216,6 +242,39 @@ export function SystemSettingsPanel(props: {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{activeTab === 'clientCustomization' && (
|
||||
<section className="fileStoragePanel">
|
||||
<div className="fileStorageSettingsCard">
|
||||
<div>
|
||||
<strong>客户端自定义</strong>
|
||||
<span>配置公开展示用的客户端名称、平台名和图标路径;公开 API 无需鉴权读取。</span>
|
||||
</div>
|
||||
<div className="formGrid two spanTwo">
|
||||
<Label>
|
||||
客户端名
|
||||
<Input value={clientCustomizationForm.clientName} onChange={(event) => setClientCustomizationForm({ ...clientCustomizationForm, clientName: event.target.value })} placeholder="EasyAI AI Gateway" />
|
||||
</Label>
|
||||
<Label>
|
||||
客户端英文名
|
||||
<Input value={clientCustomizationForm.clientEnglishName} onChange={(event) => setClientCustomizationForm({ ...clientCustomizationForm, clientEnglishName: event.target.value })} placeholder="EasyAI AI Gateway" />
|
||||
</Label>
|
||||
<Label>
|
||||
平台名
|
||||
<Input value={clientCustomizationForm.platformName} onChange={(event) => setClientCustomizationForm({ ...clientCustomizationForm, platformName: event.target.value })} placeholder="EasyAI" />
|
||||
</Label>
|
||||
<Label>
|
||||
Icon 路径/URL
|
||||
<Input value={clientCustomizationForm.iconPath} onChange={(event) => setClientCustomizationForm({ ...clientCustomizationForm, iconPath: event.target.value })} placeholder="/logo.svg 或 https://..." />
|
||||
</Label>
|
||||
</div>
|
||||
<Button type="button" onClick={saveClientCustomization} disabled={props.state === 'loading'}>
|
||||
<Save size={15} />
|
||||
保存客户端自定义
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<FormDialog
|
||||
ariaLabel={editingChannel ? '编辑文件存储渠道' : '新增文件存储渠道'}
|
||||
bodyClassName="fileStorageDialogBody"
|
||||
@@ -308,6 +367,34 @@ export function SystemSettingsPanel(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function defaultClientCustomizationForm(): ClientCustomizationForm {
|
||||
return {
|
||||
clientEnglishName: 'EasyAI AI Gateway',
|
||||
clientName: 'EasyAI AI Gateway',
|
||||
iconPath: '',
|
||||
platformName: 'EasyAI',
|
||||
};
|
||||
}
|
||||
|
||||
function clientCustomizationSettingsToForm(settings: ClientCustomizationSettings | null): ClientCustomizationForm {
|
||||
const defaults = defaultClientCustomizationForm();
|
||||
return {
|
||||
clientEnglishName: settings?.clientEnglishName ?? defaults.clientEnglishName,
|
||||
clientName: settings?.clientName ?? defaults.clientName,
|
||||
iconPath: settings?.iconPath ?? defaults.iconPath,
|
||||
platformName: settings?.platformName ?? defaults.platformName,
|
||||
};
|
||||
}
|
||||
|
||||
function clientCustomizationFormToPayload(form: ClientCustomizationForm): ClientCustomizationSettingsUpdateRequest {
|
||||
return {
|
||||
clientEnglishName: form.clientEnglishName.trim(),
|
||||
clientName: form.clientName.trim(),
|
||||
iconPath: form.iconPath.trim(),
|
||||
platformName: form.platformName.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function defaultChannelForm(channelKey = ''): FileStorageChannelForm {
|
||||
return {
|
||||
apiKey: '',
|
||||
|
||||
Reference in New Issue
Block a user