Merge remote-tracking branch 'origin/main'

This commit is contained in:
wangbo 2026-07-13 13:28:19 +08:00
commit 17b1f77e1d
14 changed files with 630 additions and 8 deletions

View File

@ -2415,6 +2415,110 @@
}
}
},
"/api/admin/system/client-customization/settings": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "获取客户端自定义设置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/store.ClientCustomizationSettings"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
}
}
},
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "更新客户端名称、英文名称、平台名和图标路径。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "更新客户端自定义设置",
"parameters": [
{
"description": "客户端自定义设置",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/store.ClientCustomizationSettingsInput"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/store.ClientCustomizationSettings"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
}
}
}
},
"/api/admin/system/file-storage/channels": {
"get": {
"security": [
@ -5031,6 +5135,32 @@
}
}
},
"/api/v1/public/client-customization": {
"get": {
"description": "无需鉴权返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "获取公开客户端自定义设置",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/store.ClientCustomizationSettings"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/httpapi.ErrorEnvelope"
}
}
}
}
},
"/api/v1/reranks": {
"post": {
"security": [
@ -10872,6 +11002,40 @@
}
}
},
"store.ClientCustomizationSettings": {
"type": "object",
"properties": {
"clientEnglishName": {
"type": "string"
},
"clientName": {
"type": "string"
},
"iconPath": {
"type": "string"
},
"platformName": {
"type": "string"
}
}
},
"store.ClientCustomizationSettingsInput": {
"type": "object",
"properties": {
"clientEnglishName": {
"type": "string"
},
"clientName": {
"type": "string"
},
"iconPath": {
"type": "string"
},
"platformName": {
"type": "string"
}
}
},
"store.CreateAPIKeyInput": {
"type": "object",
"properties": {

View File

@ -1209,6 +1209,28 @@ definitions:
status:
type: string
type: object
store.ClientCustomizationSettings:
properties:
clientEnglishName:
type: string
clientName:
type: string
iconPath:
type: string
platformName:
type: string
type: object
store.ClientCustomizationSettingsInput:
properties:
clientEnglishName:
type: string
clientName:
type: string
iconPath:
type: string
platformName:
type: string
type: object
store.CreateAPIKeyInput:
properties:
expiresAt:
@ -4061,6 +4083,72 @@ paths:
summary: 更新 Runner 策略
tags:
- runtime
/api/admin/system/client-customization/settings:
get:
description: 返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/store.ClientCustomizationSettings'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"403":
description: Forbidden
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
security:
- BearerAuth: []
summary: 获取客户端自定义设置
tags:
- system
patch:
consumes:
- application/json
description: 更新客户端名称、英文名称、平台名和图标路径。
parameters:
- description: 客户端自定义设置
in: body
name: body
required: true
schema:
$ref: '#/definitions/store.ClientCustomizationSettingsInput'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/store.ClientCustomizationSettings'
"400":
description: Bad Request
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"403":
description: Forbidden
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
security:
- BearerAuth: []
summary: 更新客户端自定义设置
tags:
- system
/api/admin/system/file-storage/channels:
get:
description: 返回所有未删除的文件存储通道,用于管理上传与生成资源回传策略。
@ -5741,6 +5829,23 @@ paths:
summary: 列出目录供应商
tags:
- catalog
/api/v1/public/client-customization:
get:
description: 无需鉴权返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/store.ClientCustomizationSettings'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/httpapi.ErrorEnvelope'
summary: 获取公开客户端自定义设置
tags:
- system
/api/v1/reranks:
post:
consumes:

View File

@ -70,6 +70,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
mux.Handle("GET /api/v1/me", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.me)))
mux.Handle("GET /api/v1/public/catalog/providers", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listCatalogProviders)))
mux.Handle("GET /api/v1/public/catalog/base-models", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listBaseModels)))
mux.Handle("GET /api/v1/public/client-customization", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.getPublicClientCustomizationSettings)))
mux.Handle("GET /api/admin/catalog/providers", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders)))
mux.Handle("POST /api/admin/catalog/providers", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createCatalogProvider)))
mux.Handle("PATCH /api/admin/catalog/providers/{providerID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateCatalogProvider)))
@ -133,6 +134,8 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
mux.Handle("GET /api/admin/config/network-proxy", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getNetworkProxyConfig)))
mux.Handle("GET /api/admin/system/file-storage/settings", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getFileStorageSettings)))
mux.Handle("PATCH /api/admin/system/file-storage/settings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateFileStorageSettings)))
mux.Handle("GET /api/admin/system/client-customization/settings", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getClientCustomizationSettings)))
mux.Handle("PATCH /api/admin/system/client-customization/settings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateClientCustomizationSettings)))
mux.Handle("GET /api/admin/system/file-storage/channels", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listFileStorageChannels)))
mux.Handle("POST /api/admin/system/file-storage/channels", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createFileStorageChannel)))
mux.Handle("PATCH /api/admin/system/file-storage/channels/{channelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateFileStorageChannel)))

View File

@ -83,6 +83,76 @@ func (s *Server) updateFileStorageSettings(w http.ResponseWriter, r *http.Reques
writeJSON(w, http.StatusOK, settings)
}
// getClientCustomizationSettings godoc
// @Summary 获取客户端自定义设置
// @Description 返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。
// @Tags system
// @Produce json
// @Security BearerAuth
// @Success 200 {object} store.ClientCustomizationSettings
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/system/client-customization/settings [get]
func (s *Server) getClientCustomizationSettings(w http.ResponseWriter, r *http.Request) {
s.writeClientCustomizationSettings(w, r)
}
// getPublicClientCustomizationSettings godoc
// @Summary 获取公开客户端自定义设置
// @Description 无需鉴权返回客户端名称、英文名称、平台名和图标路径;数据库对象尚未创建时返回默认设置。
// @Tags system
// @Produce json
// @Success 200 {object} store.ClientCustomizationSettings
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/public/client-customization [get]
func (s *Server) getPublicClientCustomizationSettings(w http.ResponseWriter, r *http.Request) {
s.writeClientCustomizationSettings(w, r)
}
func (s *Server) writeClientCustomizationSettings(w http.ResponseWriter, r *http.Request) {
settings, err := s.store.GetClientCustomizationSettings(r.Context())
if err != nil {
if store.IsUndefinedDatabaseObject(err) {
writeJSON(w, http.StatusOK, store.DefaultClientCustomizationSettings())
return
}
s.logger.Error("get client customization settings failed", "error", err)
writeError(w, http.StatusInternalServerError, "get client customization settings failed")
return
}
writeJSON(w, http.StatusOK, settings)
}
// updateClientCustomizationSettings godoc
// @Summary 更新客户端自定义设置
// @Description 更新客户端名称、英文名称、平台名和图标路径。
// @Tags system
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body store.ClientCustomizationSettingsInput true "客户端自定义设置"
// @Success 200 {object} store.ClientCustomizationSettings
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/system/client-customization/settings [patch]
func (s *Server) updateClientCustomizationSettings(w http.ResponseWriter, r *http.Request) {
var input store.ClientCustomizationSettingsInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
settings, err := s.store.UpdateClientCustomizationSettings(r.Context(), input)
if err != nil {
s.logger.Error("update client customization settings failed", "error", err)
writeError(w, http.StatusInternalServerError, "update client customization settings failed")
return
}
writeJSON(w, http.StatusOK, settings)
}
// createFileStorageChannel godoc
// @Summary 创建文件存储通道
// @Description 创建文件存储通道,当前主要用于配置 server-main OpenAPI 上传通道。

View File

@ -24,6 +24,7 @@ const (
)
const SystemSettingFileStorage = "file_storage"
const SystemSettingClientCustomization = "client_customization"
const fileStorageChannelColumns = `
id::text, channel_key, name, provider, COALESCE(upload_url, ''), credentials,
@ -72,6 +73,20 @@ type FileStorageSettingsInput struct {
ResultUploadPolicy string `json:"resultUploadPolicy"`
}
type ClientCustomizationSettings struct {
ClientName string `json:"clientName"`
ClientEnglishName string `json:"clientEnglishName"`
PlatformName string `json:"platformName"`
IconPath string `json:"iconPath"`
}
type ClientCustomizationSettingsInput struct {
ClientName *string `json:"clientName"`
ClientEnglishName *string `json:"clientEnglishName"`
PlatformName *string `json:"platformName"`
IconPath *string `json:"iconPath"`
}
type fileStorageChannelScanner interface {
Scan(dest ...any) error
}
@ -348,6 +363,15 @@ func DefaultFileStorageSettings() FileStorageSettings {
return FileStorageSettings{ResultUploadPolicy: FileStorageResultUploadPolicyDefault}
}
func DefaultClientCustomizationSettings() ClientCustomizationSettings {
return ClientCustomizationSettings{
ClientName: "EasyAI AI Gateway",
ClientEnglishName: "EasyAI AI Gateway",
PlatformName: "EasyAI",
IconPath: "",
}
}
func (s *Store) GetFileStorageSettings(ctx context.Context) (FileStorageSettings, error) {
var value []byte
err := s.pool.QueryRow(ctx, `
@ -380,13 +404,53 @@ func (s *Store) UpdateFileStorageSettings(ctx context.Context, input FileStorage
return fileStorageSettingsFromValue(decodeObject(saved)), nil
}
func (s *Store) GetClientCustomizationSettings(ctx context.Context) (ClientCustomizationSettings, error) {
var value []byte
err := s.pool.QueryRow(ctx, `
SELECT value
FROM system_settings
WHERE setting_key = $1`, SystemSettingClientCustomization).Scan(&value)
if err != nil {
if IsNotFound(err) {
return DefaultClientCustomizationSettings(), nil
}
return ClientCustomizationSettings{}, err
}
return clientCustomizationSettingsFromValue(decodeObject(value)), nil
}
func (s *Store) UpdateClientCustomizationSettings(ctx context.Context, input ClientCustomizationSettingsInput) (ClientCustomizationSettings, error) {
current, err := s.GetClientCustomizationSettings(ctx)
if err != nil && !IsUndefinedDatabaseObject(err) {
return ClientCustomizationSettings{}, err
}
settings := normalizeClientCustomizationSettings(input, current)
value, _ := json.Marshal(settings)
var saved []byte
err = s.upsertSystemSetting(ctx, SystemSettingClientCustomization, value, &saved)
if err != nil && IsUndefinedDatabaseObject(err) {
if ensureErr := s.ensureSystemSettingsTable(ctx); ensureErr != nil {
return ClientCustomizationSettings{}, ensureErr
}
err = s.upsertSystemSetting(ctx, SystemSettingClientCustomization, value, &saved)
}
if err != nil {
return ClientCustomizationSettings{}, err
}
return clientCustomizationSettingsFromValue(decodeObject(saved)), nil
}
func (s *Store) upsertFileStorageSettings(ctx context.Context, value []byte, saved *[]byte) error {
return s.upsertSystemSetting(ctx, SystemSettingFileStorage, value, saved)
}
func (s *Store) upsertSystemSetting(ctx context.Context, settingKey string, value []byte, saved *[]byte) error {
return s.pool.QueryRow(ctx, `
INSERT INTO system_settings (setting_key, value)
VALUES ($1, $2)
ON CONFLICT (setting_key)
DO UPDATE SET value = EXCLUDED.value, updated_at = now()
RETURNING value`, SystemSettingFileStorage, value).Scan(saved)
RETURNING value`, settingKey, value).Scan(saved)
}
func (s *Store) ensureSystemSettingsTable(ctx context.Context) error {
@ -409,6 +473,53 @@ func fileStorageSettingsFromValue(value map[string]any) FileStorageSettings {
return settings
}
func clientCustomizationSettingsFromValue(value map[string]any) ClientCustomizationSettings {
settings := DefaultClientCustomizationSettings()
if value == nil {
return settings
}
if clientName := stringFromAny(value["clientName"]); clientName != "" {
settings.ClientName = clientName
}
if clientEnglishName := stringFromAny(value["clientEnglishName"]); clientEnglishName != "" {
settings.ClientEnglishName = clientEnglishName
}
if platformName := stringFromAny(value["platformName"]); platformName != "" {
settings.PlatformName = platformName
}
settings.IconPath = stringFromAny(value["iconPath"])
return settings
}
func normalizeClientCustomizationSettings(input ClientCustomizationSettingsInput, current ClientCustomizationSettings) ClientCustomizationSettings {
settings := current
if settings.ClientName == "" && settings.ClientEnglishName == "" && settings.PlatformName == "" {
settings = DefaultClientCustomizationSettings()
}
if input.ClientName != nil {
settings.ClientName = strings.TrimSpace(*input.ClientName)
}
if input.ClientEnglishName != nil {
settings.ClientEnglishName = strings.TrimSpace(*input.ClientEnglishName)
}
if input.PlatformName != nil {
settings.PlatformName = strings.TrimSpace(*input.PlatformName)
}
if input.IconPath != nil {
settings.IconPath = strings.TrimSpace(*input.IconPath)
}
if settings.ClientName == "" {
settings.ClientName = DefaultClientCustomizationSettings().ClientName
}
if settings.ClientEnglishName == "" {
settings.ClientEnglishName = DefaultClientCustomizationSettings().ClientEnglishName
}
if settings.PlatformName == "" {
settings.PlatformName = DefaultClientCustomizationSettings().PlatformName
}
return settings
}
func NormalizeFileStorageResultUploadPolicy(policy string) string {
normalized := strings.ToLower(strings.TrimSpace(policy))
normalized = strings.ReplaceAll(normalized, "-", "_")

View File

@ -0,0 +1,6 @@
INSERT INTO system_settings (setting_key, value)
VALUES (
'client_customization',
'{"clientName":"EasyAI AI Gateway","clientEnglishName":"EasyAI AI Gateway","platformName":"EasyAI","iconPath":""}'::jsonb
)
ON CONFLICT (setting_key) DO NOTHING;

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 [];
}

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,

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;

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>

View File

@ -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: '',

View File

@ -10,7 +10,10 @@
"lint": "nx run-many -t lint -p web contracts",
"db:create": "scripts/create-database.sh",
"migrate": "nx run api:migrate",
"openapi": "nx run api:openapi"
"openapi": "nx run api:openapi",
"docker:build": "docker compose -f docker-compose.yml build --pull api web",
"docker:push": "scripts/deploy-compose.sh push-only",
"docker:deploy": "pnpm docker:build && pnpm docker:push"
},
"devDependencies": {
"@nx/vite": "^21.0.0",

View File

@ -943,6 +943,20 @@ export interface FileStorageSettingsUpdateRequest {
resultUploadPolicy: 'default' | 'upload_all' | 'upload_none' | string;
}
export interface ClientCustomizationSettings {
clientName: string;
clientEnglishName: string;
platformName: string;
iconPath: string;
}
export interface ClientCustomizationSettingsUpdateRequest {
clientName?: string;
clientEnglishName?: string;
platformName?: string;
iconPath?: string;
}
export interface GatewayTask {
id: string;
kind: string;

View File

@ -25,6 +25,8 @@ Usage:
scripts/deploy-compose.sh Build, migrate, start, and verify
scripts/deploy-compose.sh deploy Same as default
scripts/deploy-compose.sh push Build and push API/Web images
scripts/deploy-compose.sh push-only
Push already-built API/Web images
scripts/deploy-compose.sh down Stop containers, keep volumes
scripts/deploy-compose.sh clean Stop containers and remove volumes
@ -265,6 +267,10 @@ case "$ACTION" in
build_images
push_images
;;
push-only)
cd "$PROJECT_ROOT"
push_images
;;
down)
cd "$PROJECT_ROOT"
"${compose[@]}" down --remove-orphans