Commit be567ef7 by Seefs Committed by GitHub

fix: fix model deployment style issues, lint problems, and i18n gaps. (#2556)

* fix: fix model deployment style issues, lint problems, and i18n gaps.

* fix: adjust the key not to be displayed on the frontend, tested via the backend.

* fix: adjust the sidebar configuration logic to use the default configuration items if they are not defined.
parent 1c95a9fe
package controller
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
......@@ -23,6 +25,20 @@ func getIoAPIKey(c *gin.Context) (string, bool) {
return apiKey, true
}
func GetModelDeploymentSettings(c *gin.Context) {
common.OptionMapRWMutex.RLock()
enabled := common.OptionMap["model_deployment.ionet.enabled"] == "true"
hasAPIKey := strings.TrimSpace(common.OptionMap["model_deployment.ionet.api_key"]) != ""
common.OptionMapRWMutex.RUnlock()
common.ApiSuccess(c, gin.H{
"provider": "io.net",
"enabled": enabled,
"configured": hasAPIKey,
"can_connect": enabled && hasAPIKey,
})
}
func getIoClient(c *gin.Context) (*ionet.Client, bool) {
apiKey, ok := getIoAPIKey(c)
if !ok {
......@@ -44,15 +60,28 @@ func TestIoNetConnection(c *gin.Context) {
APIKey string `json:"api_key"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.ApiErrorMsg(c, "invalid request payload")
rawBody, err := c.GetRawData()
if err != nil {
common.ApiError(c, err)
return
}
if len(bytes.TrimSpace(rawBody)) > 0 {
if err := json.Unmarshal(rawBody, &req); err != nil {
common.ApiErrorMsg(c, "invalid request payload")
return
}
}
apiKey := strings.TrimSpace(req.APIKey)
if apiKey == "" {
common.ApiErrorMsg(c, "api_key is required")
return
common.OptionMapRWMutex.RLock()
storedKey := strings.TrimSpace(common.OptionMap["model_deployment.ionet.api_key"])
common.OptionMapRWMutex.RUnlock()
if storedKey == "" {
common.ApiErrorMsg(c, "api_key is required")
return
}
apiKey = storedKey
}
client := ionet.NewEnterpriseClient(apiKey)
......
......@@ -20,7 +20,11 @@ func GetOptions(c *gin.Context) {
var options []*model.Option
common.OptionMapRWMutex.Lock()
for k, v := range common.OptionMap {
if strings.HasSuffix(k, "Token") || strings.HasSuffix(k, "Secret") || strings.HasSuffix(k, "Key") {
if strings.HasSuffix(k, "Token") ||
strings.HasSuffix(k, "Secret") ||
strings.HasSuffix(k, "Key") ||
strings.HasSuffix(k, "secret") ||
strings.HasSuffix(k, "api_key") {
continue
}
options = append(options, &model.Option{
......
......@@ -269,24 +269,18 @@ func SetApiRouter(router *gin.Engine) {
deploymentsRoute := apiRouter.Group("/deployments")
deploymentsRoute.Use(middleware.AdminAuth())
{
// List and search deployments
deploymentsRoute.GET("/settings", controller.GetModelDeploymentSettings)
deploymentsRoute.POST("/settings/test-connection", controller.TestIoNetConnection)
deploymentsRoute.GET("/", controller.GetAllDeployments)
deploymentsRoute.GET("/search", controller.SearchDeployments)
// Connection utilities
deploymentsRoute.POST("/test-connection", controller.TestIoNetConnection)
// Resource and configuration endpoints
deploymentsRoute.GET("/hardware-types", controller.GetHardwareTypes)
deploymentsRoute.GET("/locations", controller.GetLocations)
deploymentsRoute.GET("/available-replicas", controller.GetAvailableReplicas)
deploymentsRoute.POST("/price-estimation", controller.GetPriceEstimation)
deploymentsRoute.GET("/check-name", controller.CheckClusterNameAvailability)
// Create new deployment
deploymentsRoute.POST("/", controller.CreateDeployment)
// Individual deployment operations
deploymentsRoute.GET("/:id", controller.GetDeployment)
deploymentsRoute.GET("/:id/logs", controller.GetDeploymentLogs)
deploymentsRoute.GET("/:id/containers", controller.ListDeploymentContainers)
......@@ -295,14 +289,6 @@ func SetApiRouter(router *gin.Engine) {
deploymentsRoute.PUT("/:id/name", controller.UpdateDeploymentName)
deploymentsRoute.POST("/:id/extend", controller.ExtendDeployment)
deploymentsRoute.DELETE("/:id", controller.DeleteDeployment)
// Future batch operations:
// deploymentsRoute.POST("/:id/start", controller.StartDeployment)
// deploymentsRoute.POST("/:id/stop", controller.StopDeployment)
// deploymentsRoute.POST("/:id/restart", controller.RestartDeployment)
// deploymentsRoute.POST("/batch_delete", controller.BatchDeleteDeployments)
// deploymentsRoute.POST("/batch_start", controller.BatchStartDeployments)
// deploymentsRoute.POST("/batch_stop", controller.BatchStopDeployments)
}
}
}
......@@ -25,7 +25,9 @@ export default defineConfig({
"zh",
"en",
"fr",
"ru"
"ru",
"ja",
"vi"
],
extract: {
input: [
......
......@@ -44,7 +44,10 @@ import CodeViewer from '../../../playground/CodeViewer';
import { StatusContext } from '../../../../context/Status';
import { UserContext } from '../../../../context/User';
import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
import { useSidebar } from '../../../../hooks/common/useSidebar';
import {
mergeAdminConfig,
useSidebar,
} from '../../../../hooks/common/useSidebar';
const NotificationSettings = ({
t,
......@@ -82,6 +85,7 @@ const NotificationSettings = ({
enabled: true,
channel: true,
models: true,
deployment: true,
redemption: true,
user: true,
setting: true,
......@@ -164,6 +168,7 @@ const NotificationSettings = ({
enabled: true,
channel: true,
models: true,
deployment: true,
redemption: true,
user: true,
setting: true,
......@@ -178,14 +183,27 @@ const NotificationSettings = ({
try {
// 获取管理员全局配置
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
try {
const adminConf = JSON.parse(
statusState.status.SidebarModulesAdmin,
);
setAdminConfig(mergeAdminConfig(adminConf));
} catch (error) {
setAdminConfig(mergeAdminConfig(null));
}
} else {
setAdminConfig(mergeAdminConfig(null));
}
// 获取用户个人配置
const userRes = await API.get('/api/user/self');
if (userRes.data.success && userRes.data.data.sidebar_modules) {
const userConf = JSON.parse(userRes.data.data.sidebar_modules);
let userConf;
if (typeof userRes.data.data.sidebar_modules === 'string') {
userConf = JSON.parse(userRes.data.data.sidebar_modules);
} else {
userConf = userRes.data.data.sidebar_modules;
}
setSidebarModulesUser(userConf);
}
} catch (error) {
......@@ -274,6 +292,11 @@ const NotificationSettings = ({
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
{
key: 'deployment',
title: t('模型部署'),
description: t('模型部署管理'),
},
{
key: 'redemption',
title: t('兑换码管理'),
description: t('兑换码生成管理'),
......@@ -812,7 +835,9 @@ const NotificationSettings = ({
</Typography.Text>
</div>
<Switch
checked={sidebarModulesUser[section.key]?.enabled}
checked={
sidebarModulesUser[section.key]?.enabled !== false
}
onChange={handleSectionChange(section.key)}
size='default'
/>
......@@ -835,7 +860,8 @@ const NotificationSettings = ({
>
<Card
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
sidebarModulesUser[section.key]?.enabled
sidebarModulesUser[section.key]?.enabled !==
false
? ''
: 'opacity-50'
}`}
......@@ -866,7 +892,7 @@ const NotificationSettings = ({
checked={
sidebarModulesUser[section.key]?.[
module.key
]
] !== false
}
onChange={handleModuleChange(
section.key,
......@@ -874,8 +900,8 @@ const NotificationSettings = ({
)}
size='default'
disabled={
!sidebarModulesUser[section.key]
?.enabled
sidebarModulesUser[section.key]
?.enabled === false
}
/>
</div>
......
......@@ -27,13 +27,14 @@ const DeploymentsActions = ({
setEditingDeployment,
setShowEdit,
batchDeleteDeployments,
batchOperationsEnabled = true,
compactMode,
setCompactMode,
showCreateModal,
setShowCreateModal,
t,
}) => {
const hasSelected = selectedKeys.length > 0;
const hasSelected = batchOperationsEnabled && selectedKeys.length > 0;
const handleAddDeployment = () => {
if (setShowCreateModal) {
......@@ -53,7 +54,6 @@ const DeploymentsActions = ({
setSelectedKeys([]);
};
return (
<div className='flex flex-wrap gap-2 w-full md:w-auto order-2 md:order-1'>
<Button
......
......@@ -43,7 +43,8 @@ const DeploymentsTable = (deploymentsData) => {
deploymentCount,
compactMode,
visibleColumns,
setSelectedKeys,
rowSelection,
batchOperationsEnabled = true,
handlePageChange,
handlePageSizeChange,
handleRow,
......@@ -95,7 +96,10 @@ const DeploymentsTable = (deploymentsData) => {
};
const handleConfirmAction = () => {
if (selectedDeployment && confirmOperation === 'delete') {
if (
selectedDeployment &&
(confirmOperation === 'delete' || confirmOperation === 'destroy')
) {
deleteDeployment(selectedDeployment.id);
}
setShowConfirmDialog(false);
......@@ -179,11 +183,7 @@ const DeploymentsTable = (deploymentsData) => {
hidePagination={true}
expandAllRows={false}
onRow={handleRow}
rowSelection={{
onChange: (selectedRowKeys, selectedRows) => {
setSelectedKeys(selectedRows);
},
}}
rowSelection={batchOperationsEnabled ? rowSelection : undefined}
empty={
<Empty
image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
......@@ -235,7 +235,7 @@ const DeploymentsTable = (deploymentsData) => {
onCancel={() => setShowConfirmDialog(false)}
onConfirm={handleConfirmAction}
title={t('确认操作')}
type="danger"
type='danger'
deployment={selectedDeployment}
operation={confirmOperation}
t={t}
......
......@@ -32,9 +32,10 @@ import { createCardProPagination } from '../../../helpers/utils';
const DeploymentsPage = () => {
const deploymentsData = useDeploymentsData();
const isMobile = useIsMobile();
// Create deployment modal state
const [showCreateModal, setShowCreateModal] = useState(false);
const batchOperationsEnabled = false;
const {
// Edit state
......@@ -81,7 +82,7 @@ const DeploymentsPage = () => {
visible={showEdit}
handleClose={closeEdit}
/>
<CreateDeploymentModal
visible={showCreateModal}
onCancel={() => setShowCreateModal(false)}
......@@ -109,6 +110,7 @@ const DeploymentsPage = () => {
setEditingDeployment={setEditingDeployment}
setShowEdit={setShowEdit}
batchDeleteDeployments={batchDeleteDeployments}
batchOperationsEnabled={batchOperationsEnabled}
compactMode={compactMode}
setCompactMode={setCompactMode}
showCreateModal={showCreateModal}
......@@ -138,7 +140,10 @@ const DeploymentsPage = () => {
})}
t={deploymentsData.t}
>
<DeploymentsTable {...deploymentsData} />
<DeploymentsTable
{...deploymentsData}
batchOperationsEnabled={batchOperationsEnabled}
/>
</CardPro>
</>
);
......
......@@ -25,6 +25,56 @@ import { API } from '../../helpers';
const sidebarEventTarget = new EventTarget();
const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
export const DEFAULT_ADMIN_CONFIG = {
chat: {
enabled: true,
playground: true,
chat: true,
},
console: {
enabled: true,
detail: true,
token: true,
log: true,
midjourney: true,
task: true,
},
personal: {
enabled: true,
topup: true,
personal: true,
},
admin: {
enabled: true,
channel: true,
models: true,
deployment: true,
redemption: true,
user: true,
setting: true,
},
};
const deepClone = (value) => JSON.parse(JSON.stringify(value));
export const mergeAdminConfig = (savedConfig) => {
const merged = deepClone(DEFAULT_ADMIN_CONFIG);
if (!savedConfig || typeof savedConfig !== 'object') return merged;
for (const [sectionKey, sectionConfig] of Object.entries(savedConfig)) {
if (!sectionConfig || typeof sectionConfig !== 'object') continue;
if (!merged[sectionKey]) {
merged[sectionKey] = { ...sectionConfig };
continue;
}
merged[sectionKey] = { ...merged[sectionKey], ...sectionConfig };
}
return merged;
};
export const useSidebar = () => {
const [statusState] = useContext(StatusContext);
const [userConfig, setUserConfig] = useState(null);
......@@ -37,48 +87,17 @@ export const useSidebar = () => {
instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`;
}
// 默认配置
const defaultAdminConfig = {
chat: {
enabled: true,
playground: true,
chat: true,
},
console: {
enabled: true,
detail: true,
token: true,
log: true,
midjourney: true,
task: true,
},
personal: {
enabled: true,
topup: true,
personal: true,
},
admin: {
enabled: true,
channel: true,
models: true,
deployment: true,
redemption: true,
user: true,
setting: true,
},
};
// 获取管理员配置
const adminConfig = useMemo(() => {
if (statusState?.status?.SidebarModulesAdmin) {
try {
const config = JSON.parse(statusState.status.SidebarModulesAdmin);
return config;
return mergeAdminConfig(config);
} catch (error) {
return defaultAdminConfig;
return mergeAdminConfig(null);
}
}
return defaultAdminConfig;
return mergeAdminConfig(null);
}, [statusState?.status?.SidebarModulesAdmin]);
// 加载用户配置的通用方法
......
......@@ -39,10 +39,13 @@ export const useDeploymentResources = () => {
setLoadingHardware(true);
const response = await API.get('/api/deployments/hardware-types');
if (response.data.success) {
const { hardware_types: hardwareList = [], total_available } = response.data.data || {};
const { hardware_types: hardwareList = [], total_available } =
response.data.data || {};
const normalizedHardware = hardwareList.map((hardware) => {
const availableCountValue = Number(hardware.available_count);
const availableCount = Number.isNaN(availableCountValue) ? 0 : availableCountValue;
const availableCount = Number.isNaN(availableCountValue)
? 0
: availableCountValue;
const availableBool =
typeof hardware.available === 'boolean'
? hardware.available
......@@ -57,7 +60,9 @@ export const useDeploymentResources = () => {
const providedTotal = Number(total_available);
const fallbackTotal = normalizedHardware.reduce(
(acc, item) => acc + (Number.isNaN(item.available_count) ? 0 : item.available_count),
(acc, item) =>
acc +
(Number.isNaN(item.available_count) ? 0 : item.available_count),
0,
);
const hasProvidedTotal =
......@@ -85,37 +90,64 @@ export const useDeploymentResources = () => {
}
}, []);
const fetchLocations = useCallback(async () => {
const fetchLocations = useCallback(async (hardwareId, gpuCount = 1) => {
if (!hardwareId) {
setLocations([]);
setLocationsTotalAvailable(0);
return [];
}
try {
setLoadingLocations(true);
const response = await API.get('/api/deployments/locations');
const response = await API.get(
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
);
if (response.data.success) {
const { locations: locationsList = [], total } = response.data.data || {};
const normalizedLocations = locationsList.map((location) => {
const iso2 = (location.iso2 || '').toString().toUpperCase();
const availableValue = Number(location.available);
const available = Number.isNaN(availableValue) ? 0 : availableValue;
const replicas = response.data.data?.replicas || [];
const nextLocationsMap = new Map();
replicas.forEach((replica) => {
const rawId = replica?.location_id ?? replica?.location?.id;
if (rawId === null || rawId === undefined) return;
return {
...location,
const mapKey = String(rawId);
if (nextLocationsMap.has(mapKey)) return;
const rawIso2 =
replica?.iso2 ?? replica?.location_iso2 ?? replica?.location?.iso2;
const iso2 = rawIso2 ? String(rawIso2).toUpperCase() : '';
const name =
replica?.location_name ??
replica?.location?.name ??
replica?.name ??
String(rawId);
nextLocationsMap.set(mapKey, {
id: rawId,
name: String(name),
iso2,
available,
};
region:
replica?.region ??
replica?.location_region ??
replica?.location?.region,
country:
replica?.country ??
replica?.location_country ??
replica?.location?.country,
code:
replica?.code ??
replica?.location_code ??
replica?.location?.code,
available: Number(replica?.available_count) || 0,
});
});
const providedTotal = Number(total);
const fallbackTotal = normalizedLocations.reduce(
(acc, item) => acc + (Number.isNaN(item.available) ? 0 : item.available),
0,
);
const hasProvidedTotal =
total !== undefined &&
total !== null &&
total !== '' &&
!Number.isNaN(providedTotal);
const normalizedLocations = Array.from(nextLocationsMap.values());
setLocations(normalizedLocations);
setLocationsTotalAvailable(
hasProvidedTotal ? providedTotal : fallbackTotal,
normalizedLocations.reduce(
(acc, item) => acc + (item.available || 0),
0,
),
);
return normalizedLocations;
} else {
......@@ -132,34 +164,37 @@ export const useDeploymentResources = () => {
}
}, []);
const fetchAvailableReplicas = useCallback(async (hardwareId, gpuCount = 1) => {
if (!hardwareId) {
setAvailableReplicas([]);
return [];
}
const fetchAvailableReplicas = useCallback(
async (hardwareId, gpuCount = 1) => {
if (!hardwareId) {
setAvailableReplicas([]);
return [];
}
try {
setLoadingReplicas(true);
const response = await API.get(
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`
);
if (response.data.success) {
const replicas = response.data.data.replicas || [];
setAvailableReplicas(replicas);
return replicas;
} else {
showError('获取可用资源失败: ' + response.data.message);
try {
setLoadingReplicas(true);
const response = await API.get(
`/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`,
);
if (response.data.success) {
const replicas = response.data.data.replicas || [];
setAvailableReplicas(replicas);
return replicas;
} else {
showError('获取可用资源失败: ' + response.data.message);
setAvailableReplicas([]);
return [];
}
} catch (error) {
console.error('Load available replicas error:', error);
setAvailableReplicas([]);
return [];
} finally {
setLoadingReplicas(false);
}
} catch (error) {
console.error('Load available replicas error:', error);
setAvailableReplicas([]);
return [];
} finally {
setLoadingReplicas(false);
}
}, []);
},
[],
);
const calculatePrice = useCallback(async (params) => {
const {
......@@ -167,10 +202,16 @@ export const useDeploymentResources = () => {
hardwareId,
gpusPerContainer,
durationHours,
replicaCount
replicaCount,
} = params;
if (!locationIds?.length || !hardwareId || !gpusPerContainer || !durationHours || !replicaCount) {
if (
!locationIds?.length ||
!hardwareId ||
!gpusPerContainer ||
!durationHours ||
!replicaCount
) {
setPriceEstimation(null);
return null;
}
......@@ -185,7 +226,10 @@ export const useDeploymentResources = () => {
replica_count: replicaCount,
};
const response = await API.post('/api/deployments/price-estimation', requestData);
const response = await API.post(
'/api/deployments/price-estimation',
requestData,
);
if (response.data.success) {
const estimation = response.data.data;
setPriceEstimation(estimation);
......@@ -208,7 +252,9 @@ export const useDeploymentResources = () => {
if (!name?.trim()) return false;
try {
const response = await API.get(`/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`);
const response = await API.get(
`/api/deployments/check-name?name=${encodeURIComponent(name.trim())}`,
);
if (response.data.success) {
return response.data.data.available;
} else {
......
......@@ -25,9 +25,9 @@ export const useEnhancedDeploymentActions = (t) => {
// Set loading state for specific operation
const setOperationLoading = (operation, deploymentId, isLoading) => {
setLoading(prev => ({
setLoading((prev) => ({
...prev,
[`${operation}_${deploymentId}`]: isLoading
[`${operation}_${deploymentId}`]: isLoading,
}));
};
......@@ -38,20 +38,26 @@ export const useEnhancedDeploymentActions = (t) => {
// Extend deployment duration
const extendDeployment = async (deploymentId, durationHours) => {
const operationKey = `extend_${deploymentId}`;
try {
setOperationLoading('extend', deploymentId, true);
const response = await API.post(`/api/deployments/${deploymentId}/extend`, {
duration_hours: durationHours
});
const response = await API.post(
`/api/deployments/${deploymentId}/extend`,
{
duration_hours: durationHours,
},
);
if (response.data.success) {
showSuccess(t('容器时长延长成功'));
return response.data.data;
}
} catch (error) {
showError(t('延长时长失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('延长时长失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('extend', deploymentId, false);
......@@ -62,14 +68,18 @@ export const useEnhancedDeploymentActions = (t) => {
const getDeploymentDetails = async (deploymentId) => {
try {
setOperationLoading('details', deploymentId, true);
const response = await API.get(`/api/deployments/${deploymentId}`);
if (response.data.success) {
return response.data.data;
}
} catch (error) {
showError(t('获取详情失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取详情失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('details', deploymentId, false);
......@@ -80,24 +90,31 @@ export const useEnhancedDeploymentActions = (t) => {
const getDeploymentLogs = async (deploymentId, options = {}) => {
try {
setOperationLoading('logs', deploymentId, true);
const params = new URLSearchParams();
if (options.containerId) params.append('container_id', options.containerId);
if (options.containerId)
params.append('container_id', options.containerId);
if (options.level) params.append('level', options.level);
if (options.limit) params.append('limit', options.limit.toString());
if (options.cursor) params.append('cursor', options.cursor);
if (options.follow) params.append('follow', 'true');
if (options.startTime) params.append('start_time', options.startTime);
if (options.endTime) params.append('end_time', options.endTime);
const response = await API.get(`/api/deployments/${deploymentId}/logs?${params}`);
const response = await API.get(
`/api/deployments/${deploymentId}/logs?${params}`,
);
if (response.data.success) {
return response.data.data;
}
} catch (error) {
showError(t('获取日志失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('获取日志失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('logs', deploymentId, false);
......@@ -108,15 +125,22 @@ export const useEnhancedDeploymentActions = (t) => {
const updateDeploymentConfig = async (deploymentId, config) => {
try {
setOperationLoading('config', deploymentId, true);
const response = await API.put(`/api/deployments/${deploymentId}`, config);
const response = await API.put(
`/api/deployments/${deploymentId}`,
config,
);
if (response.data.success) {
showSuccess(t('容器配置更新成功'));
return response.data.data;
}
} catch (error) {
showError(t('更新配置失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('更新配置失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('config', deploymentId, false);
......@@ -127,15 +151,19 @@ export const useEnhancedDeploymentActions = (t) => {
const deleteDeployment = async (deploymentId) => {
try {
setOperationLoading('delete', deploymentId, true);
const response = await API.delete(`/api/deployments/${deploymentId}`);
if (response.data.success) {
showSuccess(t('容器销毁请求已提交'));
return response.data.data;
}
} catch (error) {
showError(t('销毁容器失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('销毁容器失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('delete', deploymentId, false);
......@@ -146,17 +174,21 @@ export const useEnhancedDeploymentActions = (t) => {
const updateDeploymentName = async (deploymentId, newName) => {
try {
setOperationLoading('rename', deploymentId, true);
const response = await API.put(`/api/deployments/${deploymentId}/name`, {
name: newName
name: newName,
});
if (response.data.success) {
showSuccess(t('容器名称更新成功'));
return response.data.data;
}
} catch (error) {
showError(t('更新名称失败') + ': ' + (error.response?.data?.message || error.message));
showError(
t('更新名称失败') +
': ' +
(error.response?.data?.message || error.message),
);
throw error;
} finally {
setOperationLoading('rename', deploymentId, false);
......@@ -167,21 +199,23 @@ export const useEnhancedDeploymentActions = (t) => {
const batchDelete = async (deploymentIds) => {
try {
setOperationLoading('batch_delete', 'all', true);
const results = await Promise.allSettled(
deploymentIds.map(id => deleteDeployment(id))
deploymentIds.map((id) => deleteDeployment(id)),
);
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
const successful = results.filter((r) => r.status === 'fulfilled').length;
const failed = results.filter((r) => r.status === 'rejected').length;
if (successful > 0) {
showSuccess(t('批量操作完成: {{success}}个成功, {{failed}}个失败', {
success: successful,
failed: failed
}));
showSuccess(
t('批量操作完成: {{success}}个成功, {{failed}}个失败', {
success: successful,
failed: failed,
}),
);
}
return { successful, failed };
} catch (error) {
showError(t('批量操作失败') + ': ' + error.message);
......@@ -195,17 +229,20 @@ export const useEnhancedDeploymentActions = (t) => {
const exportLogs = async (deploymentId, options = {}) => {
try {
setOperationLoading('export_logs', deploymentId, true);
const logs = await getDeploymentLogs(deploymentId, {
...options,
limit: 10000 // Get more logs for export
limit: 10000, // Get more logs for export
});
if (logs && logs.logs) {
const logText = logs.logs.map(log =>
`[${new Date(log.timestamp).toISOString()}] [${log.level}] ${log.source ? `[${log.source}] ` : ''}${log.message}`
).join('\n');
const logText = logs.logs
.map(
(log) =>
`[${new Date(log.timestamp).toISOString()}] [${log.level}] ${log.source ? `[${log.source}] ` : ''}${log.message}`,
)
.join('\n');
const blob = new Blob([logText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
......@@ -215,7 +252,7 @@ export const useEnhancedDeploymentActions = (t) => {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showSuccess(t('日志导出成功'));
}
} catch (error) {
......@@ -236,14 +273,14 @@ export const useEnhancedDeploymentActions = (t) => {
updateDeploymentName,
batchDelete,
exportLogs,
// Loading states
isOperationLoading,
loading,
// Utility
setOperationLoading
setOperationLoading,
};
};
export default useEnhancedDeploymentActions;
\ No newline at end of file
export default useEnhancedDeploymentActions;
......@@ -18,13 +18,12 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useCallback, useEffect, useState } from 'react';
import { API, toBoolean } from '../../helpers';
import { API } from '../../helpers';
export const useModelDeploymentSettings = () => {
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState({
'model_deployment.ionet.enabled': false,
'model_deployment.ionet.api_key': '',
});
const [connectionState, setConnectionState] = useState({
loading: false,
......@@ -35,24 +34,13 @@ export const useModelDeploymentSettings = () => {
const getSettings = async () => {
try {
setLoading(true);
const res = await API.get('/api/option/');
const res = await API.get('/api/deployments/settings');
const { success, data } = res.data;
if (success) {
const newSettings = {
'model_deployment.ionet.enabled': false,
'model_deployment.ionet.api_key': '',
};
data.forEach((item) => {
if (item.key.endsWith('enabled')) {
newSettings[item.key] = toBoolean(item.value);
} else if (newSettings.hasOwnProperty(item.key)) {
newSettings[item.key] = item.value || '';
}
setSettings({
'model_deployment.ionet.enabled': data?.enabled === true,
});
setSettings(newSettings);
}
} catch (error) {
console.error('Failed to get model deployment settings:', error);
......@@ -65,10 +53,7 @@ export const useModelDeploymentSettings = () => {
getSettings();
}, []);
const apiKey = settings['model_deployment.ionet.api_key'];
const isIoNetEnabled = settings['model_deployment.ionet.enabled'] &&
apiKey &&
apiKey.trim() !== '';
const isIoNetEnabled = settings['model_deployment.ionet.enabled'];
const buildConnectionError = (rawMessage, fallbackMessage = 'Connection failed') => {
const message = (rawMessage || fallbackMessage).trim();
......@@ -85,18 +70,12 @@ export const useModelDeploymentSettings = () => {
return { type: 'unknown', message };
};
const testConnection = useCallback(async (apiKey) => {
const key = (apiKey || '').trim();
if (key === '') {
setConnectionState({ loading: false, ok: null, error: null });
return;
}
const testConnection = useCallback(async () => {
setConnectionState({ loading: true, ok: null, error: null });
try {
const response = await API.post(
'/api/deployments/test-connection',
{ api_key: key },
'/api/deployments/settings/test-connection',
{},
{ skipErrorHandler: true },
);
......@@ -123,16 +102,15 @@ export const useModelDeploymentSettings = () => {
useEffect(() => {
if (!loading && isIoNetEnabled) {
testConnection(apiKey);
testConnection();
return;
}
setConnectionState({ loading: false, ok: null, error: null });
}, [loading, isIoNetEnabled, apiKey, testConnection]);
}, [loading, isIoNetEnabled, testConnection]);
return {
loading,
settings,
apiKey,
isIoNetEnabled,
refresh: getSettings,
connectionLoading: connectionState.loading,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -29,7 +29,6 @@ const ModelDeploymentPage = () => {
connectionLoading,
connectionOk,
connectionError,
apiKey,
testConnection,
} = useModelDeploymentSettings();
......@@ -40,7 +39,7 @@ const ModelDeploymentPage = () => {
connectionLoading={connectionLoading}
connectionOk={connectionOk}
connectionError={connectionError}
onRetry={() => testConnection(apiKey)}
onRetry={() => testConnection()}
>
<div className='mt-[60px] px-2'>
<DeploymentsTable />
......
......@@ -48,10 +48,6 @@ export default function SettingModelDeployment(props) {
const testApiKey = async () => {
const apiKey = inputs['model_deployment.ionet.api_key'];
if (!apiKey || apiKey.trim() === '') {
showError(t('请先填写 API Key'));
return;
}
const getLocalizedMessage = (message) => {
switch (message) {
......@@ -69,10 +65,8 @@ export default function SettingModelDeployment(props) {
setTesting(true);
try {
const response = await API.post(
'/api/deployments/test-connection',
{
api_key: apiKey.trim(),
},
'/api/deployments/settings/test-connection',
apiKey && apiKey.trim() !== '' ? { api_key: apiKey.trim() } : {},
{
skipErrorHandler: true,
},
......@@ -108,12 +102,6 @@ export default function SettingModelDeployment(props) {
};
function onSubmit() {
// 前置校验:如果启用了 io.net 但没有填写 API Key
if (inputs['model_deployment.ionet.enabled'] &&
(!inputs['model_deployment.ionet.api_key'] || inputs['model_deployment.ionet.api_key'].trim() === '')) {
return showError(t('启用 io.net 部署时必须填写 API Key'));
}
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
......@@ -229,7 +217,7 @@ export default function SettingModelDeployment(props) {
<Form.Input
label={t('API Key')}
field={'model_deployment.ionet.api_key'}
placeholder={t('请输入 io.net API Key')}
placeholder={t('请输入 io.net API Key(敏感信息不显示)')}
onChange={(value) =>
setInputs({
...inputs,
......@@ -248,9 +236,7 @@ export default function SettingModelDeployment(props) {
onClick={testApiKey}
loading={testing}
disabled={
!inputs['model_deployment.ionet.enabled'] ||
!inputs['model_deployment.ionet.api_key'] ||
inputs['model_deployment.ionet.api_key'].trim() === ''
!inputs['model_deployment.ionet.enabled']
}
style={{
height: '32px',
......
......@@ -32,7 +32,7 @@ import { API, showSuccess, showError } from '../../../helpers';
import { StatusContext } from '../../../context/Status';
import { UserContext } from '../../../context/User';
import { useUserPermissions } from '../../../hooks/common/useUserPermissions';
import { useSidebar } from '../../../hooks/common/useSidebar';
import { mergeAdminConfig, useSidebar } from '../../../hooks/common/useSidebar';
import { Settings } from 'lucide-react';
const { Text } = Typography;
......@@ -198,9 +198,25 @@ export default function SettingsSidebarModulesUser() {
try {
// 获取管理员全局配置
if (statusState?.status?.SidebarModulesAdmin) {
const adminConf = JSON.parse(statusState.status.SidebarModulesAdmin);
setAdminConfig(adminConf);
console.log('加载管理员边栏配置:', adminConf);
try {
const adminConf = JSON.parse(
statusState.status.SidebarModulesAdmin,
);
const mergedAdminConf = mergeAdminConfig(adminConf);
setAdminConfig(mergedAdminConf);
console.log('加载管理员边栏配置:', mergedAdminConf);
} catch (error) {
const mergedAdminConf = mergeAdminConfig(null);
setAdminConfig(mergedAdminConf);
console.log(
'加载管理员边栏配置失败,使用默认配置:',
mergedAdminConf,
);
}
} else {
const mergedAdminConf = mergeAdminConfig(null);
setAdminConfig(mergedAdminConf);
console.log('管理员边栏配置缺失,使用默认配置:', mergedAdminConf);
}
// 获取用户个人配置
......@@ -324,6 +340,11 @@ export default function SettingsSidebarModulesUser() {
{ key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
{ key: 'models', title: t('模型管理'), description: t('AI模型配置') },
{
key: 'deployment',
title: t('模型部署'),
description: t('模型部署管理'),
},
{
key: 'redemption',
title: t('兑换码管理'),
description: t('兑换码生成管理'),
......@@ -389,7 +410,7 @@ export default function SettingsSidebarModulesUser() {
</Text>
</div>
<Switch
checked={sidebarModulesUser[section.key]?.enabled}
checked={sidebarModulesUser[section.key]?.enabled !== false}
onChange={handleSectionChange(section.key)}
size='default'
/>
......@@ -401,7 +422,9 @@ export default function SettingsSidebarModulesUser() {
<Col key={module.key} xs={24} sm={12} md={8} lg={6} xl={6}>
<Card
className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
sidebarModulesUser[section.key]?.enabled ? '' : 'opacity-50'
sidebarModulesUser[section.key]?.enabled !== false
? ''
: 'opacity-50'
}`}
bodyStyle={{ padding: '16px' }}
hoverable
......@@ -417,10 +440,15 @@ export default function SettingsSidebarModulesUser() {
</div>
<div className='ml-4'>
<Switch
checked={sidebarModulesUser[section.key]?.[module.key]}
checked={
sidebarModulesUser[section.key]?.[module.key] !==
false
}
onChange={handleModuleChange(section.key, module.key)}
size='default'
disabled={!sidebarModulesUser[section.key]?.enabled}
disabled={
sidebarModulesUser[section.key]?.enabled === false
}
/>
</div>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment