Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-07-13 13:28:19 +08:00
14 changed files with 630 additions and 8 deletions
+28 -2
View File
@@ -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,
@@ -154,6 +158,7 @@ type DataKey =
| 'playgroundModels'
| 'modelCatalog'
| 'networkProxyConfig'
| 'clientCustomizationSettings'
| 'fileStorageChannels'
| 'fileStorageSettings'
| 'platforms'
@@ -202,6 +207,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[]>([]);
@@ -335,6 +341,7 @@ export function App() {
auditLogs,
apiKeys,
baseModels,
clientCustomizationSettings,
currentUser,
currentUserGroups,
fileStorageChannels,
@@ -358,7 +365,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);
@@ -438,6 +445,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;
@@ -966,6 +976,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('');
@@ -1259,6 +1284,7 @@ export function App() {
onSaveAccessRule={saveAccessRule}
onSaveFileStorageChannel={saveFileStorageChannel}
onSaveFileStorageSettings={saveFileStorageSettings}
onSaveClientCustomizationSettings={saveClientCustomizationSettings}
onSaveTenant={saveTenant}
onSaveUser={saveUser}
onRechargeUserWalletBalance={rechargeUserWallet}
@@ -1480,7 +1506,7 @@ function dataKeysForRoute(
case 'accessRules':
return ['accessRules', 'userGroups', 'platforms', 'models'];
case 'systemSettings':
return ['fileStorageSettings', 'fileStorageChannels'];
return ['clientCustomizationSettings', 'fileStorageSettings', 'fileStorageChannels'];
default:
return [];
}
+21
View File
@@ -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
View File
@@ -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;
+4
View File
@@ -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: '',