feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import semver from 'semver';
import { type BotPluginWorkFlowItem } from '@coze-workflow/components';
import { type ApiNodeDataDTO } from '@coze-workflow/nodes';
import { BlockInput } from '@coze-workflow/base';
interface PluginApi {
name: string;
plugin_name: string;
api_id: string;
plugin_id: string;
plugin_icon: string;
desc: string;
plugin_product_status: number;
version_name?: string;
version_ts?: string;
}
export const createApiNodeInfo = (
apiParams: Partial<PluginApi> | undefined,
templateIcon?: string,
): ApiNodeDataDTO => {
const { name, plugin_name, api_id, plugin_id, desc, version_ts } =
apiParams || {};
return {
data: {
nodeMeta: {
title: name,
icon: templateIcon,
subtitle: `${plugin_name}:${name}`,
description: desc,
},
inputs: {
apiParam: [
BlockInput.create('apiID', api_id),
BlockInput.create('apiName', name),
BlockInput.create('pluginID', plugin_id),
BlockInput.create('pluginName', plugin_name),
BlockInput.create('pluginVersion', version_ts || ''),
BlockInput.create('tips', ''),
BlockInput.create('outDocLink', ''),
],
},
},
};
};
export const createSubWorkflowNodeInfo = ({
workflowItem,
spaceId,
templateIcon,
isImageflow,
}: {
workflowItem: BotPluginWorkFlowItem | undefined;
spaceId: string;
isImageflow: boolean;
templateIcon?: string;
}) => {
const { name, workflow_id, desc, version_name } = workflowItem || {};
const nodeJson = {
data: {
nodeMeta: {
title: name,
description: desc,
icon: templateIcon,
isImageflow,
},
inputs: {
workflowId: workflow_id,
spaceId,
workflowVersion: semver.valid(version_name) ? version_name : '',
},
},
};
return nodeJson;
};

View File

@@ -0,0 +1,343 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef } from 'react';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowNodesService } from '@coze-workflow/nodes';
import {
isSelectProjectCategory,
useOpenWorkflowDetail,
useWorkflowModal,
WorkflowModalFrom,
type WorkFlowModalModeProps,
} from '@coze-workflow/components';
import { type Workflow } from '@coze-workflow/base/api';
import {
StandardNodeType,
WorkflowMode,
type WorkflowNodeJSON,
} from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Button, Space, Toast, Typography } from '@coze-arch/coze-design';
import { From } from '@coze-agent-ide/plugin-shared';
import { usePluginApisModal } from '@coze-agent-ide/bot-plugin/components/plugin-apis/use-plugin-apis-modal';
import { WorkflowPlaygroundContext } from '@/workflow-playground-context';
import { WorkflowEditService } from '@/services';
import { useSpaceId } from '@/hooks/use-space-id';
import { useGlobalState } from '@/hooks/use-global-state';
import { useNodeVersionService } from '@/hooks';
import { createApiNodeInfo, createSubWorkflowNodeInfo } from './helper';
const { Text } = Typography;
/**
* 子流程、插件节点关闭时的结果
*/
export enum AddNodeModalCloseResult {
/**
* 成功添加节点
*/
NodeAdded = 'node-added',
/**
* 取消
*/
Cancel = 'cancel',
/**
* 打开 project 的新 tab
*/
OpenNewTab = 'new-tab',
}
export type AddNodeCallback = (params: {
nodeType: StandardNodeType;
nodeJSON: Partial<WorkflowNodeJSON>;
}) => void;
export const useAddNodeModal = (prevAddNodeRef: {
current: { x: number; y: number; isDrag: boolean };
}) => {
const spaceId = useSpaceId();
const globalState = useGlobalState();
const playgroundContext = useService<WorkflowPlaygroundContext>(
WorkflowPlaygroundContext,
);
const nodeVersionService = useNodeVersionService();
const projectApi = globalState.getProjectApi();
const addNodeCallbackRef = useRef<AddNodeCallback>();
const onCloseRef = useRef<(result?: AddNodeModalCloseResult) => void>();
const addNodeModalCloseResultRef = useRef<AddNodeModalCloseResult>();
const editService = useService<WorkflowEditService>(WorkflowEditService);
const nodesService = useService<WorkflowNodesService>(WorkflowNodesService);
const openWorkflowDetail = useOpenWorkflowDetail();
const createOpenWorkflowModalCallback =
(isImageflow): WorkFlowModalModeProps['onAdd'] =>
async (val, config) => {
if (!val) {
return false;
}
if (
!(await nodeVersionService.addSubWorkflowCheck(
val.workflow_id,
val.version_name,
))
) {
return false;
}
const { name } = val;
const templateIcon = playgroundContext.getTemplateList([
isImageflow ? StandardNodeType.Imageflow : StandardNodeType.SubWorkflow,
])?.[0]?.icon_url;
const nodeJSON = createSubWorkflowNodeInfo({
workflowItem: val,
spaceId,
templateIcon,
isImageflow,
});
const position = {
clientX: prevAddNodeRef.current.x,
clientY: prevAddNodeRef.current.y,
};
const { isDrag } = prevAddNodeRef.current;
if (addNodeCallbackRef.current) {
addNodeCallbackRef.current({
nodeType: StandardNodeType.SubWorkflow,
nodeJSON,
});
} else {
// 这里可能会失败,底层调用 released_workflows 接口
editService.addNode(
StandardNodeType.SubWorkflow,
nodeJSON,
position,
isDrag,
);
}
Toast.success({
content: (
<Space spacing={6}>
<Text>
{isImageflow
? I18n.t('workflow_add_imageflow_toast_success', { name })
: I18n.t('wf_node_add_wf_modal_toast_wf_added', {
workflowName: name,
})}
</Text>
{config.isDup ? (
<Button
color="primary"
onClick={() => {
window.open(
`/work_flow?space_id=${spaceId}&workflow_id=${val.workflow_id}`,
);
}}
>
{I18n.t('workflowstore_continue_editing')}
</Button>
) : null}
</Space>
),
});
};
const openWorkflowModalCallback = createOpenWorkflowModalCallback(false);
const openImageflowModalCallback = createOpenWorkflowModalCallback(true);
const onCloseModal = () => {
onCloseRef.current?.(addNodeModalCloseResultRef.current);
addNodeModalCloseResultRef.current = undefined;
};
// workflow 添加弹窗
const workflowModalFrom = globalState.projectId
? WorkflowModalFrom.ProjectWorkflowAddNode
: WorkflowModalFrom.WorkflowAddNode;
const {
node: workflowModal,
open: openWorkflow,
close: closeWorkflow,
} = useWorkflowModal({
from: workflowModalFrom,
flowMode: WorkflowMode.Workflow,
onAdd: openWorkflowModalCallback,
bindBizId: globalState.config?.bindBizID,
bindBizType: globalState.config?.bindBizType,
excludedWorkflowIds: [globalState.workflowId],
projectId: globalState.projectId,
onDupSuccess: () => null,
onClose: onCloseModal,
onCreateSuccess: val => {
addNodeModalCloseResultRef.current = AddNodeModalCloseResult.OpenNewTab;
closeWorkflow();
if (workflowModalFrom === WorkflowModalFrom.ProjectWorkflowAddNode) {
globalState.playgroundProps.refetchProjectResourceList?.();
}
openWorkflowDetail({
workflowId: val.workflowId,
spaceId: val.spaceId,
projectId: globalState.projectId,
ideNavigate: projectApi?.navigate,
});
},
onItemClick: ({ item }, modalState) => {
if (isSelectProjectCategory(modalState)) {
addNodeModalCloseResultRef.current = AddNodeModalCloseResult.OpenNewTab;
closeWorkflow();
projectApi?.navigate?.(`/workflow/${(item as Workflow).workflow_id}`);
return { handled: true };
}
return { handled: false };
},
});
// 图像流弹窗
const {
node: imageFlowModal,
open: openImageflow,
close: closeImageflow,
} = useWorkflowModal({
from: WorkflowModalFrom.WorkflowAddNode,
flowMode: WorkflowMode.Imageflow,
onAdd: openImageflowModalCallback,
excludedWorkflowIds: [globalState.workflowId],
onDupSuccess: () => null,
onClose: onCloseModal,
});
// plugin 添加弹窗
const pluginModalFrom = globalState.projectId
? From.ProjectWorkflow
: From.WorkflowAddNode;
const {
node: pluginModal,
open: openPlugin,
close: closePlugin,
} = usePluginApisModal({
from: pluginModalFrom,
projectId: globalState.projectId,
closeCallback: onCloseModal,
clickProjectPluginCallback: pluginInfo => {
addNodeModalCloseResultRef.current = AddNodeModalCloseResult.OpenNewTab;
closePlugin();
projectApi?.navigate(`/plugin/${pluginInfo?.id}`);
},
openModeCallback: async val => {
if (!val) {
return false;
}
if (
!(await nodeVersionService.addApiCheck(val.plugin_id, val.version_ts))
) {
return false;
}
const templateIcon = playgroundContext.getNodeTemplateInfoByType(
StandardNodeType.Api,
)?.icon;
const nodeJSON = createApiNodeInfo(val, templateIcon);
const position = {
clientX: prevAddNodeRef.current.x,
clientY: prevAddNodeRef.current.y,
};
const { isDrag } = prevAddNodeRef.current;
// 插件面板弹窗-点击添加插件,预先请求 api-detail 接口,获取 plugin 详情,调用 panel.tsx 的 handleSelectNode 方法
if (addNodeCallbackRef.current) {
addNodeCallbackRef.current({
nodeType: StandardNodeType.Api,
nodeJSON,
});
} else {
// 拖拽「插件」,或者「子流程」节点自身,会走这里的逻辑,此时 isDrag 为 true
editService.addNode(StandardNodeType.Api, nodeJSON, position, isDrag);
}
Toast.success(
I18n.t('bot_edit_tool_added_toast', {
api_name: val?.name,
}) as string,
);
},
onCreateSuccess:
pluginModalFrom === From.ProjectWorkflow
? val => {
addNodeModalCloseResultRef.current =
AddNodeModalCloseResult.OpenNewTab;
closePlugin();
if (val?.pluginId && pluginModalFrom === From.ProjectWorkflow) {
globalState.playgroundProps.refetchProjectResourceList?.();
projectApi?.navigate(`/plugin/${val.pluginId}`);
}
}
: undefined,
});
const wrapOpenFunc = function <T>(
openFunc: (modalProps?: T) => void,
closeFunc?: () => void,
) {
return ({
onAdd,
closeOnAdd,
onClose,
modalProps,
}: {
onAdd?: AddNodeCallback;
onClose?: (closeResult?: AddNodeModalCloseResult) => void;
closeOnAdd?: boolean;
modalProps?: T;
} = {}) => {
if (onAdd) {
addNodeCallbackRef.current = (...args) => {
const nodeJSON = args?.[0]?.nodeJSON as WorkflowNodeJSON<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Record<string, any>
>;
if (nodeJSON?.data?.nodeMeta?.title) {
nodeJSON.data.nodeMeta.title = nodesService.createUniqTitle(
nodeJSON.data.nodeMeta.title,
);
}
addNodeModalCloseResultRef.current =
AddNodeModalCloseResult.NodeAdded;
onAdd?.(...args);
closeOnAdd ? closeFunc?.() : null;
};
} else {
addNodeCallbackRef.current = undefined;
}
onCloseRef.current = onClose;
openFunc?.(modalProps);
};
};
return {
workflowModal,
openWorkflow: wrapOpenFunc(openWorkflow, closeWorkflow),
imageFlowModal,
openImageflow: wrapOpenFunc(openImageflow, closeImageflow),
pluginModal,
openPlugin: wrapOpenFunc(openPlugin, closePlugin),
};
};