Parcourir la source

feat: handle soft-delete conflict on model creation with user confirmation

- Add handleSoftDeleteConflict utility (isSoftDeleteConflict, handleCreateModelWithConflict)
- Add overwrite_deleted field to FormData type
- Add skipErrorHandler option to createModel API
- Wire conflict handler into all createModel call sites:
  - table-list.tsx (handleModalOk COPY, handleCreateModel)
  - catalog.tsx (handleCreateModel)
  - model-files.tsx (handleCreateModel)
- On soft-delete conflict, show Modal.confirm asking user to overwrite or cancel
kinglee il y a 2 jours
Parent
commit
c06e5fcf5c

+ 6 - 2
src/pages/llmodels/apis/index.ts

@@ -74,10 +74,14 @@ export async function queryGPUList<T extends Record<string, any>>(
   });
 }
 
-export async function createModel(params: { data: FormData }) {
+export async function createModel(params: {
+  data: FormData;
+  skipErrorHandler?: boolean;
+}) {
   return request(`${MODELS_API}`, {
     method: 'POST',
-    data: params.data
+    data: params.data,
+    skipErrorHandler: params.skipErrorHandler
   });
 }
 

+ 5 - 8
src/pages/llmodels/catalog.tsx

@@ -16,11 +16,12 @@ import { useAtom } from 'jotai';
 import _ from 'lodash';
 import React, { useCallback, useEffect, useState } from 'react';
 import PageBox from '../_components/page-box';
-import { createModel, queryCatalogItemSpec, queryCatalogList } from './apis';
+import { queryCatalogItemSpec, queryCatalogList } from './apis';
 import CatalogList from './components/catalog/catalog-list';
 import DelopyBuiltInModal from './components/deployment/deploy-builtin-modal';
 import { modelCategories, modelSourceMap } from './config';
 import { CatalogItem as CatalogItemType, FormData } from './config/types';
+import { handleCreateModelWithConflict } from './utils/handleSoftDeleteConflict';
 
 const Catalog: React.FC = () => {
   const intl = useIntl();
@@ -76,12 +77,8 @@ const Catalog: React.FC = () => {
 
   const handleCreateModel = useCallback(
     async (data: FormData) => {
-      try {
-        const modelData = await createModel({
-          data: {
-            ..._.omit(data, ['size', 'quantization'])
-          }
-        });
+      const formData = _.omit(data, ['size', 'quantization']) as FormData;
+      await handleCreateModelWithConflict(formData, (modelData) => {
         writeState(IS_FIRST_LOGIN, false);
         setOpenDeployModal({
           ...openDeployModal,
@@ -90,7 +87,7 @@ const Catalog: React.FC = () => {
         message.success(intl.formatMessage({ id: 'common.message.success' }));
         setModelsExpandKeys([modelData.id]);
         navigate('/models/deployments');
-      } catch (error) {}
+      });
     },
     [openDeployModal]
   );

+ 20 - 18
src/pages/llmodels/components/table-list.tsx

@@ -35,7 +35,6 @@ import React, {
 } from 'react';
 import {
   MODELS_API,
-  createModel,
   deleteModel,
   deleteModelInstance,
   queryModelInstancesList,
@@ -58,6 +57,7 @@ import Filters from '../filters';
 import useEditDeployment from '../hooks/use-edit-deployment';
 import useModelsColumns from '../hooks/use-models-columns';
 import useViewInstanceLogs from '../hooks/use-view-instance-logs';
+import { handleCreateModelWithConflict } from '../utils/handleSoftDeleteConflict';
 import DeployModal from './deployment/deploy-modal';
 import UpdateModelModal from './deployment/update-modal';
 import Instances from './instance/instances';
@@ -222,15 +222,25 @@ const Models: React.FC<ModelsProps> = ({
 
   const handleModalOk = async (data: FormData) => {
     const currentData = openEditModalStatus.currentData;
+
+    const onSuccess = () => {
+      closeEditModal();
+      message.success(intl.formatMessage({ id: 'common.message.success' }));
+      setTimeout(() => {
+        handleSearch();
+      }, 150);
+      restoreScrollHeight();
+    };
+
     try {
       if (currentData.realAction === PageAction.COPY) {
-        const modelData = await createModel({
-          data
+        await handleCreateModelWithConflict(data, (modelData) => {
+          if (data.replicas > 0) {
+            updateExpandedRowKeys([modelData.id, ...expandedRowKeys]);
+          }
+          onSuccess();
         });
-
-        if (data.replicas > 0) {
-          updateExpandedRowKeys([modelData.id, ...expandedRowKeys]);
-        }
+        return;
       }
       if (currentData.realAction === PageAction.EDIT) {
         await updateModel({
@@ -242,12 +252,7 @@ const Models: React.FC<ModelsProps> = ({
           updateExpandedRowKeys([currentData.row.id, ...expandedRowKeys]);
         }
       }
-      closeEditModal();
-      message.success(intl.formatMessage({ id: 'common.message.success' }));
-      setTimeout(() => {
-        handleSearch();
-      }, 150);
-      restoreScrollHeight();
+      onSuccess();
     } catch (error) {}
   };
 
@@ -274,16 +279,13 @@ const Models: React.FC<ModelsProps> = ({
   };
 
   const handleCreateModel = async (data: FormData) => {
-    try {
-      const modelData = await createModel({
-        data
-      });
+    await handleCreateModelWithConflict(data, (modelData) => {
       setOpenDeployModal({
         ...openDeployModal,
         show: false
       });
       refreshListStatus(modelData);
-    } catch (error) {}
+    });
   };
 
   const handleLogModalCancel = () => {

+ 1 - 0
src/pages/llmodels/config/types.ts

@@ -101,6 +101,7 @@ export interface FormData {
     ngram_max_match_length: number;
   };
   max_context_len: number;
+  overwrite_deleted?: boolean;
 }
 
 interface ComputedResourceClaim {

+ 82 - 0
src/pages/llmodels/utils/handleSoftDeleteConflict.ts

@@ -0,0 +1,82 @@
+import { Modal } from 'antd';
+import { createModel } from '../apis';
+import { FormData } from '../config/types';
+
+/**
+ * 检查错误是否是软删除冲突
+ * @param error 错误对象
+ * @returns 是否是软删除冲突
+ */
+export function isSoftDeleteConflict(error: any): boolean {
+  const message = error?.response?.data?.message || error?.message || '';
+  return (
+    message.includes('previously deleted') &&
+    message.includes('overwrite_deleted')
+  );
+}
+
+/**
+ * 提取模型名称从错误消息中
+ * @param error 错误对象
+ * @returns 模型名称
+ */
+function extractModelName(error: any): string {
+  const message = error?.response?.data?.message || error?.message || '';
+  const match = message.match(/Model '([^']+)'/);
+  return match ? match[1] : '';
+}
+
+/**
+ * 处理软删除冲突,显示确认对话框
+ * @param data 表单数据
+ * @param onSuccess 成功回调
+ * @param onError 错误回调
+ */
+export async function handleCreateModelWithConflict(
+  data: FormData,
+  onSuccess: (modelData: any) => void,
+  onError?: (error: any) => void
+): Promise<void> {
+  try {
+    // 第一次尝试创建模型,跳过全局错误处理
+    const modelData = await createModel({
+      data,
+      skipErrorHandler: true
+    });
+    onSuccess(modelData);
+  } catch (error: any) {
+    // 检查是否是软删除冲突
+    if (isSoftDeleteConflict(error)) {
+      const modelName = extractModelName(error) || data.name;
+
+      // 显示确认对话框
+      Modal.confirm({
+        title: '模型已存在',
+        content: `模型 "${modelName}" 之前已被删除,是否覆盖并重新创建?`,
+        okText: '覆盖',
+        cancelText: '取消',
+        onOk: async () => {
+          try {
+            // 用户确认后,带上 overwrite_deleted=true 重新提交
+            const modelData = await createModel({
+              data: {
+                ...data,
+                overwrite_deleted: true
+              }
+            });
+            onSuccess(modelData);
+          } catch (retryError) {
+            if (onError) {
+              onError(retryError);
+            }
+          }
+        }
+      });
+    } else {
+      // 其他错误,调用错误回调
+      if (onError) {
+        onError(error);
+      }
+    }
+  }
+}

+ 3 - 8
src/pages/resources/components/model-files.tsx

@@ -4,7 +4,6 @@ import { PaginationKey, TABLE_SORT_DIRECTIONS } from '@/config/settings';
 import useBodyScroll from '@/hooks/use-body-scroll';
 import useTableFetch from '@/hooks/use-table-fetch';
 import PageBox from '@/pages/_components/page-box';
-import { createModel } from '@/pages/llmodels/apis';
 import DeployModal from '@/pages/llmodels/components/deployment/deploy-modal';
 import DownloadModal from '@/pages/llmodels/components/download';
 import { modelSourceMap } from '@/pages/llmodels/config';
@@ -17,6 +16,7 @@ import { backendOptionsMap } from '@/pages/llmodels/constants/backend-parameters
 import useCheckBackend from '@/pages/llmodels/hooks/use-check-backend';
 import { useGenerateWorkerOptions } from '@/pages/llmodels/hooks/use-form-initial-values';
 import useRecognizeAudio from '@/pages/llmodels/hooks/use-recognize-audio';
+import { handleCreateModelWithConflict } from '@/pages/llmodels/utils/handleSoftDeleteConflict';
 import {
   DeleteModal,
   FilterBar,
@@ -284,10 +284,7 @@ const ModelFiles = () => {
   };
 
   const handleCreateModel = async (data: any) => {
-    try {
-      const modelData = await createModel({
-        data
-      });
+    await handleCreateModelWithConflict(data, (modelData) => {
       setOpenDeployModal({
         ...openDeployModal,
         show: false
@@ -295,9 +292,7 @@ const ModelFiles = () => {
       message.success(intl.formatMessage({ id: 'common.message.success' }));
       setModelsExpandKeys([modelData.id]);
       navigate('/models/deployments');
-    } catch (error) {
-      // console.log('error', error);
-    }
+    });
   };
 
   const columns = useFilesColumns({