mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-13 14:10:59 +00:00
fix: pipeline duplication bug
This commit is contained in:
@@ -49,6 +49,14 @@ class PipelinesRouterGroup(group.RouterGroup):
|
|||||||
|
|
||||||
return self.success()
|
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(
|
@self.route(
|
||||||
'/<pipeline_uuid>/extensions', methods=['GET', 'PUT'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
|
'/<pipeline_uuid>/extensions', methods=['GET', 'PUT'], auth_type=group.AuthType.USER_TOKEN_OR_API_KEY
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -151,6 +151,52 @@ class PipelineService:
|
|||||||
)
|
)
|
||||||
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
|
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(
|
async def update_pipeline_extensions(
|
||||||
self,
|
self,
|
||||||
pipeline_uuid: str,
|
pipeline_uuid: str,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export default function PipelineFormComponent({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [showCopyConfirm, setShowCopyConfirm] = useState(false);
|
||||||
const [isDefaultPipeline, setIsDefaultPipeline] = useState<boolean>(false);
|
const [isDefaultPipeline, setIsDefaultPipeline] = useState<boolean>(false);
|
||||||
|
|
||||||
const formSchema = isEditMode
|
const formSchema = isEditMode
|
||||||
@@ -345,25 +346,17 @@ export default function PipelineFormComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
|
setShowCopyConfirm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmCopy = () => {
|
||||||
if (pipelineId) {
|
if (pipelineId) {
|
||||||
let newPipelineName = '';
|
|
||||||
httpClient
|
httpClient
|
||||||
.getPipeline(pipelineId)
|
.copyPipeline(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);
|
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onFinish();
|
onFinish();
|
||||||
toast.success(`${t('common.copySuccess')}: ${newPipelineName}`);
|
toast.success(t('common.copySuccess'));
|
||||||
|
setShowCopyConfirm(false);
|
||||||
onCancel();
|
onCancel();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -547,6 +540,22 @@ export default function PipelineFormComponent({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.delete(`/api/v1/pipelines/${uuid}`);
|
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<{
|
public getPipelineExtensions(uuid: string): Promise<{
|
||||||
enable_all_plugins: boolean;
|
enable_all_plugins: boolean;
|
||||||
enable_all_mcp_servers: boolean;
|
enable_all_mcp_servers: boolean;
|
||||||
|
|||||||
@@ -493,6 +493,9 @@ const enUS = {
|
|||||||
defaultPipelineCannotDelete: 'Default pipeline cannot be deleted',
|
defaultPipelineCannotDelete: 'Default pipeline cannot be deleted',
|
||||||
deleteSuccess: 'Deleted successfully',
|
deleteSuccess: 'Deleted successfully',
|
||||||
deleteError: 'Delete failed: ',
|
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: {
|
extensions: {
|
||||||
title: 'Extensions',
|
title: 'Extensions',
|
||||||
loadError: 'Failed to load plugins',
|
loadError: 'Failed to load plugins',
|
||||||
|
|||||||
@@ -496,6 +496,9 @@ const jaJP = {
|
|||||||
defaultPipelineCannotDelete: 'デフォルトパイプラインは削除できません',
|
defaultPipelineCannotDelete: 'デフォルトパイプラインは削除できません',
|
||||||
deleteSuccess: '削除に成功しました',
|
deleteSuccess: '削除に成功しました',
|
||||||
deleteError: '削除に失敗しました:',
|
deleteError: '削除に失敗しました:',
|
||||||
|
copyConfirmTitle: 'コピーの確認',
|
||||||
|
copyConfirmation:
|
||||||
|
'このパイプラインをコピーしますか?すべての設定を含む新しいパイプラインが作成されます。',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: 'プラグイン統合',
|
title: 'プラグイン統合',
|
||||||
loadError: 'プラグインリストの読み込みに失敗しました',
|
loadError: 'プラグインリストの読み込みに失敗しました',
|
||||||
|
|||||||
@@ -475,6 +475,9 @@ const zhHans = {
|
|||||||
defaultPipelineCannotDelete: '默认流水线不可删除',
|
defaultPipelineCannotDelete: '默认流水线不可删除',
|
||||||
deleteSuccess: '删除成功',
|
deleteSuccess: '删除成功',
|
||||||
deleteError: '删除失败:',
|
deleteError: '删除失败:',
|
||||||
|
copyConfirmTitle: '确认复制',
|
||||||
|
copyConfirmation:
|
||||||
|
'确定要复制这个流水线吗?复制将创建一个包含完整配置的新流水线。',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: '扩展集成',
|
title: '扩展集成',
|
||||||
loadError: '加载插件列表失败',
|
loadError: '加载插件列表失败',
|
||||||
|
|||||||
@@ -473,6 +473,9 @@ const zhHant = {
|
|||||||
defaultPipelineCannotDelete: '預設流程線不可刪除',
|
defaultPipelineCannotDelete: '預設流程線不可刪除',
|
||||||
deleteSuccess: '刪除成功',
|
deleteSuccess: '刪除成功',
|
||||||
deleteError: '刪除失敗:',
|
deleteError: '刪除失敗:',
|
||||||
|
copyConfirmTitle: '確認複製',
|
||||||
|
copyConfirmation:
|
||||||
|
'確定要複製這個流程線嗎?複製將創建一個包含完整配置的新流程線。',
|
||||||
extensions: {
|
extensions: {
|
||||||
title: '擴展集成',
|
title: '擴展集成',
|
||||||
loadError: '載入插件清單失敗',
|
loadError: '載入插件清單失敗',
|
||||||
|
|||||||
Reference in New Issue
Block a user