fix: pipeline duplication bug

This commit is contained in:
Junyan Qin
2025-12-19 23:27:08 +08:00
parent ae772074a1
commit adc65f66eb
8 changed files with 94 additions and 15 deletions

View File

@@ -49,6 +49,14 @@ class PipelinesRouterGroup(group.RouterGroup):
return self.success()
@self.route('/<pipeline_uuid>/copy', methods=['POST'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY)
async def _(pipeline_uuid: str) -> str:
try:
new_uuid = await self.ap.pipeline_service.copy_pipeline(pipeline_uuid)
return self.success(data={'uuid': new_uuid})
except ValueError as e:
return self.http_status(404, -1, str(e))
@self.route(
'/<pipeline_uuid>/extensions', methods=['GET', 'PUT'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
)

View File

@@ -151,6 +151,52 @@ class PipelineService:
)
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
async def copy_pipeline(self, pipeline_uuid: str) -> str:
"""Copy a pipeline with all its configurations"""
# Get the original pipeline
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
persistence_pipeline.LegacyPipeline.uuid == pipeline_uuid
)
)
original_pipeline = result.first()
if original_pipeline is None:
raise ValueError(f'Pipeline {pipeline_uuid} not found')
# Create new pipeline data
new_uuid = str(uuid.uuid4())
new_pipeline_data = {
'uuid': new_uuid,
'name': f'{original_pipeline.name} (Copy)',
'description': original_pipeline.description,
'for_version': self.ap.ver_mgr.get_current_version(),
'stages': original_pipeline.stages.copy() if original_pipeline.stages else default_stage_order.copy(),
'config': original_pipeline.config.copy() if original_pipeline.config else {},
'is_default': False,
'extensions_preferences': (
original_pipeline.extensions_preferences.copy()
if original_pipeline.extensions_preferences
else {
'enable_all_plugins': True,
'enable_all_mcp_servers': True,
'plugins': [],
'mcp_servers': [],
}
),
}
# Insert the new pipeline
await self.ap.persistence_mgr.execute_async(
sqlalchemy.insert(persistence_pipeline.LegacyPipeline).values(**new_pipeline_data)
)
# Load the new pipeline
pipeline = await self.get_pipeline(new_uuid)
await self.ap.pipeline_mgr.load_pipeline(pipeline)
return new_uuid
async def update_pipeline_extensions(
self,
pipeline_uuid: str,

View File

@@ -52,6 +52,7 @@ export default function PipelineFormComponent({
}) {
const { t } = useTranslation();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showCopyConfirm, setShowCopyConfirm] = useState(false);
const [isDefaultPipeline, setIsDefaultPipeline] = useState<boolean>(false);
const formSchema = isEditMode
@@ -345,25 +346,17 @@ export default function PipelineFormComponent({
};
const handleCopy = () => {
setShowCopyConfirm(true);
};
const confirmCopy = () => {
if (pipelineId) {
let newPipelineName = '';
httpClient
.getPipeline(pipelineId)
.then((resp) => {
const originalPipeline = resp.pipeline;
newPipelineName = `${originalPipeline.name}${t(
'pipelines.copySuffix',
)}`;
const newPipeline: Pipeline = {
name: newPipelineName,
description: originalPipeline.description,
config: originalPipeline.config,
};
return httpClient.createPipeline(newPipeline);
})
.copyPipeline(pipelineId)
.then(() => {
onFinish();
toast.success(`${t('common.copySuccess')}: ${newPipelineName}`);
toast.success(t('common.copySuccess'));
setShowCopyConfirm(false);
onCancel();
})
.catch((err) => {
@@ -547,6 +540,22 @@ export default function PipelineFormComponent({
</DialogFooter>
</DialogContent>
</Dialog>
{/* 复制确认对话框 */}
<Dialog open={showCopyConfirm} onOpenChange={setShowCopyConfirm}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('pipelines.copyConfirmTitle')}</DialogTitle>
</DialogHeader>
<div className="py-4">{t('pipelines.copyConfirmation')}</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCopyConfirm(false)}>
{t('common.cancel')}
</Button>
<Button onClick={confirmCopy}>{t('common.confirm')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -172,6 +172,10 @@ export class BackendClient extends BaseHttpClient {
return this.delete(`/api/v1/pipelines/${uuid}`);
}
public copyPipeline(uuid: string): Promise<{ uuid: string }> {
return this.post(`/api/v1/pipelines/${uuid}/copy`);
}
public getPipelineExtensions(uuid: string): Promise<{
enable_all_plugins: boolean;
enable_all_mcp_servers: boolean;

View File

@@ -493,6 +493,9 @@ const enUS = {
defaultPipelineCannotDelete: 'Default pipeline cannot be deleted',
deleteSuccess: 'Deleted successfully',
deleteError: 'Delete failed: ',
copyConfirmTitle: 'Confirm Copy',
copyConfirmation:
'Are you sure you want to copy this pipeline? This will create a new pipeline with all configurations.',
extensions: {
title: 'Extensions',
loadError: 'Failed to load plugins',

View File

@@ -496,6 +496,9 @@ const jaJP = {
defaultPipelineCannotDelete: 'デフォルトパイプラインは削除できません',
deleteSuccess: '削除に成功しました',
deleteError: '削除に失敗しました:',
copyConfirmTitle: 'コピーの確認',
copyConfirmation:
'このパイプラインをコピーしますか?すべての設定を含む新しいパイプラインが作成されます。',
extensions: {
title: 'プラグイン統合',
loadError: 'プラグインリストの読み込みに失敗しました',

View File

@@ -475,6 +475,9 @@ const zhHans = {
defaultPipelineCannotDelete: '默认流水线不可删除',
deleteSuccess: '删除成功',
deleteError: '删除失败:',
copyConfirmTitle: '确认复制',
copyConfirmation:
'确定要复制这个流水线吗?复制将创建一个包含完整配置的新流水线。',
extensions: {
title: '扩展集成',
loadError: '加载插件列表失败',

View File

@@ -473,6 +473,9 @@ const zhHant = {
defaultPipelineCannotDelete: '預設流程線不可刪除',
deleteSuccess: '刪除成功',
deleteError: '刪除失敗:',
copyConfirmTitle: '確認複製',
copyConfirmation:
'確定要複製這個流程線嗎?複製將創建一個包含完整配置的新流程線。',
extensions: {
title: '擴展集成',
loadError: '載入插件清單失敗',