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,47 @@
/*
* 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 type { DynamicParams } from '@coze-arch/bot-typings/teamspace';
import { useParams } from 'react-router-dom';
export const useOpenWorkflowDetail = () => {
const { bot_id: botId } = useParams<DynamicParams>();
/** 打开流程详情页 */
const openWorkflowDetailPage = ({
workflowId,
spaceId,
projectId,
ideNavigate,
}: {
workflowId: string;
spaceId: string;
projectId?: string;
ideNavigate?: (uri: string) => void;
}) => {
if (projectId && ideNavigate) {
ideNavigate(`/workflow/${workflowId}?from=createSuccess`);
} else {
const query = new URLSearchParams();
botId && query.append('bot_id', botId);
query.append('space_id', spaceId ?? '');
query.append('workflow_id', workflowId);
query.append('from', 'createSuccess');
window.open(`/work_flow?${query.toString()}`, '_blank');
}
};
return openWorkflowDetailPage;
};

View File

@@ -0,0 +1,535 @@
/*
* 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.
*/
/* eslint-disable max-lines-per-function */
/* eslint-disable @coze-arch/max-line-per-function */
import {
type Dispatch,
type SetStateAction,
useCallback,
useMemo,
useState,
} from 'react';
import { omit } from 'lodash-es';
import {
useInfiniteQuery,
type UseInfiniteQueryResult,
} from '@tanstack/react-query';
import {
WorkflowMode,
type GetWorkFlowListRequest,
type GetExampleWorkFlowListRequest,
type GetWorkFlowListResponse,
type GetExampleWorkFlowListResponse,
WorkFlowType,
DeleteType,
workflowApi,
DeleteAction,
type WorkflowListByBindBizRequest,
SchemaType,
BindBizType,
CheckType,
} from '@coze-workflow/base/api';
import { I18n } from '@coze-arch/i18n';
import { CustomError } from '@coze-arch/bot-error';
import { Toast } from '@coze-arch/coze-design';
import { reporter, wait } from '../utils';
import { type WorkflowInfo, WorkflowModalFrom } from '../types';
interface FetchWorkflowListResult {
total: number;
workflow_list: WorkflowInfo[];
}
interface WorkflowListReturn {
flowType: WorkFlowType;
setFlowType: Dispatch<SetStateAction<WorkFlowType>>;
flowMode: WorkflowMode;
setFlowMode: Dispatch<SetStateAction<WorkflowMode>>;
spaceId: string;
setSpaceId: Dispatch<SetStateAction<string>>;
status: GetWorkFlowListRequest['status'] | undefined;
setStatus: Dispatch<
SetStateAction<GetWorkFlowListRequest['status'] | undefined>
>;
name: GetWorkFlowListRequest['name'] | undefined;
setName: Dispatch<SetStateAction<GetWorkFlowListRequest['name'] | undefined>>;
tags: GetWorkFlowListRequest['tags'] | undefined;
setTags: Dispatch<SetStateAction<GetWorkFlowListRequest['tags'] | undefined>>;
orderBy: GetWorkFlowListRequest['order_by'] | undefined;
setOrderBy: Dispatch<
SetStateAction<GetWorkFlowListRequest['order_by'] | undefined>
>;
loginUserCreate: boolean | undefined;
setLoginUserCreate: Dispatch<SetStateAction<boolean | undefined>>;
updatePageParam: (
newParam: Partial<GetWorkFlowListRequest & WorkflowListByBindBizRequest>,
) => void;
workflowList: WorkflowInfo[];
total: number;
queryError: UseInfiniteQueryResult['error'];
fetchNextPage: UseInfiniteQueryResult['fetchNextPage'];
hasNextPage: UseInfiniteQueryResult['hasNextPage'];
isFetching: UseInfiniteQueryResult['isFetching'];
isFetchingNextPage: UseInfiniteQueryResult['isFetchingNextPage'];
loadingStatus: UseInfiniteQueryResult['status'];
refetch: UseInfiniteQueryResult['refetch'];
handleCopy: (item: WorkflowInfo) => Promise<void>;
handleDelete: (item: WorkflowInfo) => Promise<{
canDelete: boolean;
deleteType: DeleteType;
handleDelete:
| ((params?: { needDeleteBlockwise: boolean }) => Promise<void>)
| undefined;
}>;
}
const defaultPageSize = 20;
/**
* 流程列表
*/
export function useWorkflowList({
pageSize = defaultPageSize,
enabled = false,
from,
fetchWorkflowListApi = workflowApi.GetWorkFlowList.bind(workflowApi),
}: {
pageSize?: number;
/** 是否开启数据获取 */
enabled?: boolean;
from?: WorkflowModalFrom;
fetchWorkflowListApi?: (
params: GetWorkFlowListRequest | GetExampleWorkFlowListRequest,
) => Promise<GetWorkFlowListResponse | GetExampleWorkFlowListResponse>;
} = {}): Readonly<WorkflowListReturn> {
const [flowMode, setFlowMode] = useState<WorkflowMode>(WorkflowMode.All);
const [flowType, setFlowType] = useState<WorkFlowType>(WorkFlowType.User);
const [spaceId, setSpaceId] = useState<string>('');
const [name, setName] = useState<GetWorkFlowListRequest['name']>();
const [status, setStatus] = useState<GetWorkFlowListRequest['status']>();
const [orderBy, setOrderBy] = useState<GetWorkFlowListRequest['order_by']>();
const [tags, setTags] = useState<GetWorkFlowListRequest['tags']>();
const [bindBizId, setBindBizId] =
useState<WorkflowListByBindBizRequest['bind_biz_id']>();
const [bindBizType, setBindBizType] =
useState<WorkflowListByBindBizRequest['bind_biz_type']>();
const [loginUserCreate, setLoginUserCreate] =
useState<GetWorkFlowListRequest['login_user_create']>();
const [projectId, setProjectId] =
useState<GetWorkFlowListRequest['project_id']>('');
const initialPageParam = useMemo<GetWorkFlowListRequest>(
() => ({
page: 1,
size: pageSize,
type: flowType,
name,
space_id: spaceId,
status,
tags,
order_by: orderBy,
login_user_create: loginUserCreate,
flow_mode: flowMode,
bind_biz_id: bindBizId,
bind_biz_type: bindBizType,
project_id: projectId,
}),
[
flowType,
status,
name,
flowMode,
orderBy,
spaceId,
loginUserCreate,
tags,
bindBizId,
bindBizType,
projectId,
],
);
const updatePageParam = useCallback(
(newParam: Partial<GetWorkFlowListRequest>) => {
[
{ key: 'type', func: setFlowType, defaultValue: WorkFlowType.User },
{ key: 'name', func: setName },
{ key: 'space_id', func: setSpaceId, defaultValue: '' },
{ key: 'status', func: setStatus },
{ key: 'tags', func: setTags },
{ key: 'order_by', func: setOrderBy },
{ key: 'login_user_create', func: setLoginUserCreate },
{
key: 'flow_mode',
func: setFlowMode,
defaultValue: WorkflowMode.All,
},
{ key: 'bind_biz_id', func: setBindBizId },
{ key: 'bind_biz_type', func: setBindBizType },
{ key: 'project_id', func: setProjectId },
]
.filter(({ key }) => key in newParam)
.forEach(({ key, defaultValue, func }) =>
func?.(newParam[key] ?? defaultValue),
);
},
[],
);
const fetchWorkflowList = async (
params: GetWorkFlowListRequest & WorkflowListByBindBizRequest,
): Promise<FetchWorkflowListResult> => {
try {
reporter.info({
message: 'workflow_list_get_list',
});
const result: FetchWorkflowListResult = {
total: 0,
workflow_list: [],
};
if (params.bind_biz_type === BindBizType.Scene && params.bind_biz_id) {
const resp = await workflowApi.WorkflowListByBindBiz(params);
result.total = (resp.data.total as number) ?? 0;
// 设置流程权限
result.workflow_list = (resp.data.workflow_list ?? []).map(
(item): WorkflowInfo => {
const authInfo = {
can_edit: true,
can_copy: true,
can_delete: !!item?.creator?.self,
};
return {
...item,
authInfo,
};
},
);
} else {
// 多人协作场景DEV 模式需要展示 Blockwise workflow除了流程列表引用
Object.assign(params, {
schema_type_list: [SchemaType.FDL],
checker:
from === WorkflowModalFrom.WorkflowAgent
? [CheckType.BotAgent]
: undefined,
});
const isDouyinBot = params.bind_biz_type === BindBizType.DouYinBot;
// 如果不是抖音分身模式,搜索参数不携带 bind_biz_id 参数
// 否则会导致某个工作流关联到 Agent 后0之后在该工作流添加子工作流时看不到工作流列表
const fetchParams = isDouyinBot
? params
: omit(params, ['bind_biz_id']);
const resp = await fetchWorkflowListApi(fetchParams);
result.total = (resp.data.total as number) ?? 0;
// 设置流程权限
result.workflow_list = (resp.data.workflow_list ?? []).map(
(item): WorkflowInfo => {
let authInfo = {
can_edit: true,
can_copy: true,
can_delete: !!item?.creator?.self,
};
const authItem = (resp.data.auth_list ?? []).find(
it => it.workflow_id === item.workflow_id,
);
if (authItem) {
authInfo = { ...authInfo, ...authItem.auth };
}
return {
...item,
authInfo,
};
},
);
}
reporter.info({
message: 'workflow_list_get_list_success',
meta: {
currentPage: params.page,
pageSize: params.size,
order_by: params.order_by,
name: params.name,
total: result.total,
},
});
return result;
} catch (error) {
reporter.error({
message: 'workflow_list_get_list_fail',
error,
});
throw error;
}
};
const {
data: pageData,
error: queryError,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status: loadingStatus,
refetch,
} = useInfiniteQuery({
enabled: enabled && !!spaceId,
queryKey: ['space_workflow_list', 'vcs', JSON.stringify(initialPageParam)],
queryFn: ({ pageParam }) => fetchWorkflowList(pageParam),
initialPageParam,
getNextPageParam: (lastPage, allPages, lastPageParam) => {
if ((lastPageParam.page ?? 1) * pageSize > lastPage.total) {
return null;
}
return {
...lastPageParam,
page: (lastPageParam.page ?? 1) + 1,
};
},
});
const workflowList = useMemo(() => {
const result: WorkflowInfo[] = [];
const idMap: Record<string, boolean> = {};
pageData?.pages.forEach(page => {
page.workflow_list.forEach(workflow => {
if (!workflow.workflow_id) {
return;
}
if (!idMap[workflow.workflow_id]) {
result.push(workflow);
}
idMap[workflow.workflow_id] = true;
});
});
return result;
}, [pageData]);
const total = useMemo(() => {
if (
!pageData?.pages ||
!Array.isArray(pageData.pages) ||
pageData.pages.length === 0
) {
return 0;
}
return pageData.pages[pageData.pages.length - 1].total ?? 0;
}, [pageData]);
// 复制
const handleCopy = async (item: WorkflowInfo) => {
if (!item.workflow_id || !spaceId) {
throw new CustomError('normal_error', 'miss workflowId or spaceID');
}
// 检查复制权限
if (!item.authInfo.can_copy) {
throw new CustomError('normal_error', 'no copy permission');
}
reporter.info({
message: 'workflow_list_copy_row',
meta: {
workflowId: item.workflow_id,
},
});
try {
let isError = false;
const { data } = await workflowApi.CopyWorkflow({
space_id: spaceId,
workflow_id: item.workflow_id,
});
isError = !data?.workflow_id;
if (isError) {
Toast.error(I18n.t('workflow_detail_toast_createcopy_failed'));
reporter.error({
message: 'workflow_list_copy_row_fail',
error: new CustomError('normal_error', 'result no workflow'),
});
return;
}
Toast.success({
content:
flowMode === WorkflowMode.Imageflow
? I18n.t('imageflow_detail_toast_createcopy_succeed')
: I18n.t('workflow_detail_toast_createcopy_succeed'),
showClose: false,
});
reporter.info({
message: 'workflow_list_copy_row_success',
meta: {
workflowId: item.workflow_id,
},
});
// 兜底服务主从延迟
await wait(300);
// 刷新列表
refetch();
} catch (error) {
reporter.error({
message: 'workflow_list_copy_row_fail',
error,
});
Toast.error(I18n.t('workflow_detail_toast_createcopy_failed'));
}
};
// 删除
const handleDelete = async (item: WorkflowInfo) => {
if (!item.workflow_id || !spaceId) {
throw new CustomError('normal_error', 'miss workflowId or spaceID');
}
// 先检查删除权限
if (!item.authInfo.can_delete) {
throw new CustomError('normal_error', 'no delete permission');
}
reporter.info({
message: 'workflow_list_delete_row',
meta: {
workflowId: item.workflow_id,
},
});
let deleteType = DeleteType.CanDelete;
// 从服务端查询删除模式
const resp = await workflowApi.GetDeleteStrategy({
space_id: spaceId,
workflow_id: item.workflow_id,
});
deleteType = resp.data;
const canDelete = [
DeleteType.CanDelete,
DeleteType.RejectProductDraft,
].includes(deleteType);
const deleteFuc = async (deleteParams?: {
needDeleteBlockwise: boolean;
}) => {
const needDeleteBlockwise = deleteParams?.needDeleteBlockwise;
const action = needDeleteBlockwise
? DeleteAction.BlockwiseDelete
: DeleteAction.BlockwiseUnbind;
if (!item.workflow_id || !spaceId) {
throw new CustomError('normal_error', 'miss workflowId or spaceID');
}
try {
await workflowApi.DeleteWorkflow({
space_id: spaceId,
workflow_id: item.workflow_id,
action,
});
Toast.success({
content: I18n.t('workflow_add_delete_success'),
showClose: false,
});
reporter.info({
message: 'workflow_list_delete_row_success',
});
// 兜底服务主从延迟
await wait(300);
// 刷新列表
refetch();
} catch (error) {
reporter.error({
message: 'workflow_list_delete_row_fail',
error,
});
Toast.error({
content: I18n.t('workflow_add_delete_fail'),
showClose: false,
});
}
};
return {
/** 是否可删除 */
canDelete,
/** 删除策略 */
deleteType,
/** 删除方法 */
handleDelete: canDelete ? deleteFuc : undefined,
};
};
return {
// 列表筛选状态
flowType,
setFlowType,
flowMode,
setFlowMode,
spaceId,
setSpaceId,
status,
setStatus,
name,
setName,
tags,
setTags,
orderBy,
setOrderBy,
loginUserCreate,
setLoginUserCreate,
/** 更新筛选参数 */
updatePageParam,
// 列表获取
/** 流程列表数据 */
workflowList,
/** 流程总数 */
total,
/** 获取列表请求错误 */
queryError,
/** 拉取下一页数据 */
fetchNextPage,
/** 是否有下一页 */
hasNextPage,
/** 获取数据中 */
isFetching,
/** 获取下一页数据中 */
isFetchingNextPage,
/** 加载状态 */
loadingStatus,
/** 重新加载 */
refetch,
// 列表操作
/** 复制流程 */
handleCopy,
// /** 删除流程 */
handleDelete,
} as const;
}

View File

@@ -0,0 +1,47 @@
/*
* 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 { type ReactNode } from 'react';
import { useBoolean } from 'ahooks';
import { type WorkFlowModalModeProps } from '../workflow-modal/type';
import WorkflowModal from '../workflow-modal';
interface UseWorkFlowListReturnValue {
node: ReactNode;
open: () => void;
close: () => void;
}
export const useWorkflowModal = (
props?: WorkFlowModalModeProps,
): UseWorkFlowListReturnValue => {
const { onClose, ...restProps } = props || {};
const [visible, { setTrue: showModal, setFalse: hideModal }] =
useBoolean(false);
const closeModal = () => {
onClose?.();
hideModal();
};
return {
node: visible ? (
<WorkflowModal visible onClose={closeModal} {...restProps} />
) : null,
close: closeModal,
open: showModal,
};
};

View File

@@ -0,0 +1,58 @@
/*
* 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 { useWorkflowResourceMenuActions } from './use-workflow-resource-menu-actions';
import { useWorkflowResourceClick } from './use-workflow-resource-click';
import { useCreateWorkflowModal } from './use-create-workflow-modal';
import {
type UseWorkflowResourceAction,
type WorkflowResourceActionProps,
type WorkflowResourceActionReturn,
} from './type';
export { useWorkflowPublishEntry } from './use-workflow-publish-entry';
export const useWorkflowResourceAction: UseWorkflowResourceAction = props => {
const { spaceId, userId, getCommonActions } = props;
const { handleWorkflowResourceClick, goWorkflowDetail } =
useWorkflowResourceClick(spaceId);
const {
openCreateModal,
workflowModal,
createWorkflowModal,
handleEditWorkflow,
} = useCreateWorkflowModal({ ...props, goWorkflowDetail });
const { renderWorkflowResourceActions, modals } =
useWorkflowResourceMenuActions({
...props,
userId,
onEditWorkflowInfo: handleEditWorkflow,
getCommonActions,
});
return {
workflowResourceModals: [createWorkflowModal, workflowModal, ...modals],
openCreateModal,
handleWorkflowResourceClick,
renderWorkflowResourceActions,
};
};
export {
type WorkflowResourceActionProps,
type WorkflowResourceActionReturn,
useCreateWorkflowModal,
useWorkflowResourceClick,
useWorkflowResourceMenuActions,
};

View File

@@ -0,0 +1,83 @@
/*
* 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 { type ReactNode } from 'react';
import {
type WorkflowMode,
type ProductDraftStatus,
type SchemaType,
} from '@coze-workflow/base';
import { type TableActionProps } from '@coze-arch/coze-design';
import { type ResourceInfo } from '@coze-arch/bot-api/plugin_develop';
export { type ResourceInfo };
export interface WorkflowResourceActionProps {
/* 刷新列表函数 */
refreshPage?: () => void;
spaceId?: string;
/* 当前登录用户 id */
userId?: string;
getCommonActions?: (
libraryResource: ResourceInfo,
) => NonNullable<TableActionProps['actionList']>;
}
export interface WorkflowResourceActionReturn {
/* 打开 workflow 创建弹窗 */
openCreateModal: (flowMode?: WorkflowMode) => void;
/* 创建、删除等操作的全局弹窗,直接挂载到列表父容器上 */
workflowResourceModals: ReactNode[];
/* 在 Table 组件的 columns 的 render 里调用,返回 Table.TableAction 组件 */
renderWorkflowResourceActions: (record: ResourceInfo) => ReactNode;
/* 资源 item 点击 */
handleWorkflowResourceClick: (record: ResourceInfo) => void;
}
export type UseWorkflowResourceAction = (
props: WorkflowResourceActionProps,
) => WorkflowResourceActionReturn;
export interface WorkflowResourceBizExtend {
product_draft_status: ProductDraftStatus;
external_flow_info?: string;
schema_type: SchemaType;
plugin_id?: string;
icon_uri: string;
url: string;
}
export interface DeleteModalConfig {
title: string;
desc: string;
okText: string;
okHandle: () => void;
cancelText: string;
}
export interface CommonActionProps extends WorkflowResourceActionProps {
userId?: string;
}
export interface CommonActionReturn {
actionHandler: (record: ResourceInfo) => void;
}
export interface DeleteActionReturn extends CommonActionReturn {
deleteModal?: ReactNode;
}
export interface PublishActionReturn extends CommonActionReturn {
publishModal: ReactNode;
}

View File

@@ -0,0 +1,50 @@
/*
* 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 { workflowApi } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { type ResourceInfo } from '@coze-arch/bot-api/plugin_develop';
export const useChatflowSwitch = ({
spaceId,
refreshPage,
}: {
spaceId: string;
refreshPage?: () => void;
}) => {
const changeFlowMode = async (flowMode: WorkflowMode, workflowId: string) => {
await workflowApi.UpdateWorkflowMeta({
space_id: spaceId,
workflow_id: workflowId,
flow_mode: flowMode,
});
Toast.success(
I18n.t('wf_chatflow_123', {
Chatflow: I18n.t(
flowMode === WorkflowMode.ChatFlow ? 'wf_chatflow_76' : 'Workflow',
),
}),
);
await new Promise(resolve => setTimeout(resolve, 300));
refreshPage?.();
};
const switchToWorkflow = async (record: ResourceInfo) =>
changeFlowMode(WorkflowMode.Workflow, record.res_id ?? '');
const switchToChatflow = async (record: ResourceInfo) =>
changeFlowMode(WorkflowMode.ChatFlow, record.res_id ?? '');
return { switchToWorkflow, switchToChatflow };
};

View File

@@ -0,0 +1,91 @@
/*
* 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 { workflowApi } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Toast } from '@coze-arch/coze-design';
import { CustomError } from '@coze-arch/bot-error';
import { type ResourceInfo, ResType } from '@coze-arch/bot-api/plugin_develop';
import { useNavigate } from 'react-router-dom';
import { reporter, wait } from '@/utils';
import { type CommonActionProps, type CommonActionReturn } from './type';
export const useCopyAction = (props: CommonActionProps): CommonActionReturn => {
const { spaceId } = props;
const navigate = useNavigate();
// 复制
const handleCopy = async (item: ResourceInfo) => {
if (!item.res_id || !spaceId) {
throw new CustomError('normal_error', 'miss workflowId or spaceID');
}
reporter.info({
message: 'workflow_list_copy_row',
meta: {
workflowId: item.res_id,
},
});
try {
let isError = false;
const { data } = await workflowApi.CopyWorkflow({
space_id: spaceId,
workflow_id: item.res_id,
});
isError = !data?.workflow_id;
if (isError) {
Toast.error(I18n.t('workflow_detail_toast_createcopy_failed'));
reporter.error({
message: 'workflow_list_copy_row_fail',
error: new CustomError('normal_error', 'result no workflow'),
});
return;
}
Toast.success({
content:
item.res_type === ResType.Imageflow
? I18n.t('imageflow_detail_toast_createcopy_succeed')
: I18n.t('workflow_detail_toast_createcopy_succeed'),
showClose: false,
});
reporter.info({
message: 'workflow_list_copy_row_success',
meta: {
workflowId: item.res_id,
},
});
// 兜底服务主从延迟
await wait(300);
// 复制后跳转到详情页
navigate(
`/work_flow?workflow_id=${data.workflow_id}&space_id=${spaceId}`,
);
} catch (error) {
reporter.error({
message: 'workflow_list_copy_row_fail',
error,
});
Toast.error(I18n.t('workflow_detail_toast_createcopy_failed'));
}
};
return { actionHandler: handleCopy };
};

View File

@@ -0,0 +1,174 @@
/*
* 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 { useMemo, useState } from 'react';
import { useBoolean } from 'ahooks';
import {
type FrontWorkflowInfo,
WorkflowMode,
isGeneralWorkflow,
type BindBizType,
} from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { useFlags } from '@coze-arch/bot-flags';
import { CustomError } from '@coze-arch/bot-error';
import { DataSourceType, WorkflowModalFrom } from '@/workflow-modal';
import { CreateWorkflowModal, type RuleItem } from '@/workflow-edit';
import { reporter } from '@/utils';
import { useWorkflowModal } from '@/hooks/use-workflow-modal';
import { type WorkflowResourceActionProps } from './type';
export const useCreateWorkflowModal = ({
from = WorkflowModalFrom.SpaceWorkflowList,
bindBizType,
bindBizId,
refreshPage,
spaceId,
goWorkflowDetail,
projectId,
onCreateSuccess,
hiddenTemplateEntry,
nameValidators,
}: WorkflowResourceActionProps & {
from?: WorkflowModalFrom;
/** 当前项目 id只在项目内的 workflow 有该字段 */
projectId?: string;
bindBizType?: BindBizType;
bindBizId?: string;
onCreateSuccess?: ({ workflowId }: { workflowId: string }) => void;
goWorkflowDetail?: (workflowId?: string, spaceId?: string) => void;
/** 隐藏通过模板创建入口 */
hiddenTemplateEntry?: boolean;
nameValidators?: RuleItem[];
}) => {
const [currentWorkflow, setCurrentWorkflow] = useState<FrontWorkflowInfo>();
const [formMode, setFormMode] = useState<'add' | 'update'>('add');
const [flowMode, setFlowMode] = useState<WorkflowMode>(WorkflowMode.Workflow);
const [createModalVisible, { setTrue, setFalse: closeCreateModal }] =
useBoolean(false);
const [FLAGS] = useFlags();
const openCreateModal = (mode?: WorkflowMode) => {
setFormMode('add');
setFlowMode(mode || WorkflowMode.Workflow);
reporter.info({
message: 'workflow_list_open_create_modal',
});
setTrue();
};
const openEditModal = () => {
setFormMode('update');
reporter.info({
message: 'workflow_list_open_create_modal',
});
setTrue();
};
const handleEditWorkflow = (partialWorkflowInfo: FrontWorkflowInfo) => {
setCurrentWorkflow(partialWorkflowInfo);
setFlowMode(partialWorkflowInfo.flow_mode || WorkflowMode.Workflow);
openEditModal();
};
const workflowModalInitState = useMemo(() => {
// 社区版暂不支持该功能
if (isWorkflowMode || FLAGS['bot.community.store_imageflow']) {
return {
productCategory: 'all',
isSpaceWorkflow: false,
dataSourceType: DataSourceType.Product,
};
}
return {
workflowTag: 1,
isSpaceWorkflow: false,
dataSource: DataSourceType.Workflow,
};
}, [FLAGS, flowMode]);
const { node: workflowModal } = useWorkflowModal({
from,
flowMode,
dupText: I18n.t('Copy'),
hiddenCreate: true,
hiddenSpaceList: true,
initState: workflowModalInitState,
projectId,
onAdd: () => {
refreshPage?.();
},
onDupSuccess: val => {
window.open(
`/work_flow?space_id=${spaceId}&workflow_id=${val.workflow_id}&from=dupSuccess`,
);
},
});
const isWorkflowMode = useMemo(() => isGeneralWorkflow(flowMode), [flowMode]);
return {
openCreateModal,
handleEditWorkflow,
workflowModal,
createWorkflowModal: (
<CreateWorkflowModal
initConfirmDisabled
mode={formMode}
flowMode={flowMode}
projectId={projectId}
visible={createModalVisible}
onCancel={closeCreateModal}
bindBizType={bindBizType}
bindBizId={bindBizId}
workFlow={formMode === 'update' ? currentWorkflow : undefined}
getLatestWorkflowJson={undefined}
customTitleRender={undefined}
onSuccess={({ workflowId }) => {
closeCreateModal();
if (!workflowId) {
throw new CustomError(
'[Workflow] create failed',
'create workflow failed, no workflow id',
);
}
if (onCreateSuccess && formMode === 'add') {
onCreateSuccess({ workflowId });
return;
}
// 编辑模式,不跳转,刷新当前列表
if (formMode === 'update') {
refreshPage?.();
return;
}
const navigateDelay = 500;
// 由于后端数据同步慢这里delay 500 ms
setTimeout(() => {
goWorkflowDetail?.(workflowId, spaceId);
}, navigateDelay);
}}
spaceID={spaceId}
nameValidators={nameValidators}
/>
),
};
};

View File

@@ -0,0 +1,205 @@
/*
* 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 React, { useState } from 'react';
import { DeleteAction, DeleteType, workflowApi } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { CustomError } from '@coze-arch/bot-error';
import { type ResourceInfo, ResType } from '@coze-arch/bot-api/plugin_develop';
import { Modal, Toast } from '@coze-arch/coze-design';
import { reporter, wait } from '@/utils';
import {
type CommonActionProps,
type DeleteActionReturn,
type DeleteModalConfig,
} from './type';
// eslint-disable-next-line @coze-arch/max-line-per-function
export const useDeleteAction = (
props: CommonActionProps,
): DeleteActionReturn => {
const { spaceId, userId, refreshPage } = props;
const [modalVisible, setModalVisible] = useState(false);
const [deleteModalConfig, setDeleteModalConfig] =
useState<DeleteModalConfig>();
/**
* 逻辑来自 useWorkflowList@coze-workflow/components,由于入参变了,不再复用
* @param item
*/
const handleDeleteWorkflowResource = async (item: ResourceInfo) => {
if (!item.res_id || !spaceId) {
throw new CustomError('normal_error', 'miss workflowId or spaceID');
}
reporter.info({
message: 'workflow_list_delete_row',
meta: {
workflowId: item.res_id,
},
});
let deleteType = DeleteType.CanDelete;
// 从服务端查询删除模式
const resp = await workflowApi.GetDeleteStrategy({
space_id: spaceId,
workflow_id: item.res_id,
});
deleteType = resp.data;
const canDelete = [
DeleteType.CanDelete,
DeleteType.RejectProductDraft,
].includes(deleteType);
const deleteFuc = async (deleteParams?: {
needDeleteBlockwise: boolean;
}) => {
const needDeleteBlockwise = deleteParams?.needDeleteBlockwise;
const action = needDeleteBlockwise
? DeleteAction.BlockwiseDelete
: DeleteAction.BlockwiseUnbind;
if (!item.res_id || !spaceId) {
throw new CustomError('normal_error', 'miss workflowId or spaceID');
}
try {
await workflowApi.DeleteWorkflow({
space_id: spaceId,
workflow_id: item.res_id,
action,
});
Toast.success({
content: I18n.t('workflow_add_delete_success'),
showClose: false,
});
reporter.info({
message: 'workflow_list_delete_row_success',
});
// 兜底服务主从延迟
await wait(300);
// 刷新列表
refreshPage?.();
} catch (error) {
reporter.error({
message: 'workflow_list_delete_row_fail',
error,
});
Toast.error({
content: I18n.t('workflow_add_delete_fail'),
showClose: false,
});
}
};
return {
canDelete,
deleteType,
handleDelete: canDelete ? deleteFuc : undefined,
};
};
const deleteAction = async (record: ResourceInfo) => {
const isSelfCreator = record.creator_id === userId;
const deleteConfig = await handleDeleteWorkflowResource(record);
let title = I18n.t('delete_title');
if (deleteConfig.deleteType === DeleteType.UnListProduct) {
title = I18n.t('workflowstore_unable_to_delete_workflow');
}
let desc = I18n.t('library_delete_desc');
if (deleteConfig.deleteType === DeleteType.UnListProduct) {
if (isSelfCreator) {
desc = I18n.t('workflowstore_the_workflow_has_been');
} else {
desc = I18n.t('workflowstore_delete_permission');
}
}
let okText = deleteConfig.canDelete
? I18n.t('confirm')
: I18n.t('workflowstore_remove_wf');
if (
deleteConfig.deleteType === DeleteType.UnListProduct &&
!isSelfCreator
) {
okText = '';
}
let cancelText = I18n.t('cancel');
if (
deleteConfig.deleteType === DeleteType.UnListProduct &&
!isSelfCreator
) {
cancelText = I18n.t('confirm');
}
setDeleteModalConfig({
title,
desc,
cancelText,
okText,
okHandle: () => {
if (deleteConfig.canDelete) {
deleteConfig?.handleDelete?.({
needDeleteBlockwise: false,
});
} else {
if (
deleteConfig.deleteType === DeleteType.UnListProduct &&
isSelfCreator
) {
window.open(
`/store/${
record.res_type === ResType.Workflow ? 'workflow' : 'imageflow'
}/${record.res_id}?entity_id=true`,
'_blank',
);
}
}
},
});
setModalVisible(true);
};
const deleteModal = (
<Modal
maskClosable={false}
centered={true}
visible={modalVisible}
title={deleteModalConfig?.title ?? ''}
onOk={() => {
setModalVisible(false);
deleteModalConfig?.okHandle();
}}
onCancel={() => {
setModalVisible(false);
}}
closeOnEsc={true}
cancelText={deleteModalConfig?.cancelText}
okText={deleteModalConfig?.okText}
okButtonColor={'red'}
>
<div className="coz-common-content">{deleteModalConfig?.desc ?? ''}</div>
</Modal>
);
return { deleteModal, actionHandler: deleteAction };
};

View File

@@ -0,0 +1,69 @@
/*
* 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 { useState } from 'react';
import { WorkflowMode } from '@coze-workflow/base';
import { AddWorkflowToStoreEntry } from '@coze-arch/bot-tea';
import { type ResourceInfo, ResType } from '@coze-arch/bot-api/plugin_develop';
import {
PublishWorkflowModal,
usePublishWorkflowModal,
} from '@coze-workflow/resources-adapter';
import { type CommonActionProps, type PublishActionReturn } from './type';
export const usePublishAction = ({
spaceId = '',
refreshPage,
}: CommonActionProps): PublishActionReturn => {
const [flowMode, setFlowMode] = useState<WorkflowMode>(WorkflowMode.Workflow);
const publishWorkflowModalHook = usePublishWorkflowModal({
onPublishSuccess: () => {
refreshPage?.();
},
fromSpace: true,
flowMode,
});
/**
* NOTICE: 此函数由商店侧维护, 可联系 @gaoding
* 发布/更新流程商品
*/
const onPublishStore = (item: ResourceInfo) => {
setFlowMode(
item.res_type === ResType.Imageflow
? WorkflowMode.Imageflow
: WorkflowMode.Workflow,
);
// 商店渲染流程需要 spaceId 信息, 在这个场景需要手动设置对应信息
publishWorkflowModalHook.setSpace(spaceId);
publishWorkflowModalHook.showModal({
type: PublishWorkflowModal.WORKFLOW_INFO,
product: {
meta_info: {
entity_id: item.res_id,
name: item.name,
},
},
source: AddWorkflowToStoreEntry.WORKFLOW_PERSONAL_LIST,
});
};
return {
actionHandler: onPublishStore,
publishModal: publishWorkflowModalHook.ModalComponent,
};
};

View File

@@ -0,0 +1,17 @@
/*
* 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.
*/
export { useWorkflowPublishEntry } from '@coze-workflow/resources-adapter';

View File

@@ -0,0 +1,57 @@
/*
* 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 type { ResourceInfo } from '@coze-arch/bot-api/plugin_develop';
import { useNavigate } from 'react-router-dom';
import { reporter } from '@/utils';
export const useWorkflowResourceClick = (spaceId?: string) => {
const navigate = useNavigate();
const onEditWorkFlow = (workflowId?: string) => {
reporter.info({
message: 'workflow_list_edit_row',
meta: {
workflowId,
},
});
goWorkflowDetail(workflowId, spaceId);
};
/** 打开流程编辑页 */
const goWorkflowDetail = (workflowId?: string, sId?: string) => {
if (!workflowId || !sId) {
return;
}
reporter.info({
message: 'workflow_list_navigate_to_detail',
meta: {
workflowId,
},
});
navigate(`/work_flow?workflow_id=${workflowId}&space_id=${sId}`);
};
const handleWorkflowResourceClick = (record: ResourceInfo) => {
reporter.info({
message: 'workflow_list_click_row',
});
onEditWorkFlow(record?.res_id);
};
return { handleWorkflowResourceClick, goWorkflowDetail };
};

View File

@@ -0,0 +1,161 @@
/*
* 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 type { ReactNode } from 'react';
import {
ProductDraftStatus,
type FrontWorkflowInfo,
} from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { Table, type TableActionProps } from '@coze-arch/coze-design';
import { useFlags } from '@coze-arch/bot-flags';
import {
resource_resource_common,
type ResourceInfo,
ResType,
} from '@coze-arch/bot-api/plugin_develop';
import {
parseWorkflowResourceBizExtend,
transformResourceToWorkflowEditInfo,
} from './utils';
import { useWorkflowPublishEntry } from './use-workflow-publish-entry';
import { usePublishAction } from './use-publish-action';
import { useDeleteAction } from './use-delete-action';
import { useCopyAction } from './use-copy-action';
import { useChatflowSwitch } from './use-chatflow-switch';
import {
type WorkflowResourceActionProps,
type WorkflowResourceActionReturn,
} from './type';
const { ActionKey } = resource_resource_common;
type ActionItemProps = NonNullable<TableActionProps['actionList']>[number];
export const useWorkflowResourceMenuActions = (
props: WorkflowResourceActionProps & {
userId?: string;
onEditWorkflowInfo: (partialWorkflowInfo: FrontWorkflowInfo) => void;
},
): Pick<WorkflowResourceActionReturn, 'renderWorkflowResourceActions'> & {
modals: ReactNode[];
} => {
const [FLAGS] = useFlags();
const { userId, onEditWorkflowInfo, getCommonActions } = props;
const { actionHandler: deleteAction, deleteModal } = useDeleteAction(props);
const { actionHandler: copyAction } = useCopyAction(props);
const { actionHandler: publishAction, publishModal } =
usePublishAction(props);
const { switchToChatflow, switchToWorkflow } = useChatflowSwitch({
spaceId: props.spaceId ?? '',
refreshPage: props.refreshPage,
});
const actionMap = {
[ActionKey.Copy]: copyAction,
[ActionKey.Delete]: deleteAction,
[ActionKey.Edit]: (record: ResourceInfo) => {
const workflowPartialInfo = transformResourceToWorkflowEditInfo(record);
onEditWorkflowInfo(workflowPartialInfo as FrontWorkflowInfo);
},
[ActionKey.SwitchToFuncflow]: switchToWorkflow,
[ActionKey.SwitchToChatflow]: switchToChatflow,
};
const { enablePublishEntry } = useWorkflowPublishEntry();
// eslint-disable-next-line complexity
const renderWorkflowResourceActions = (record: ResourceInfo): ReactNode => {
const bizExtend = parseWorkflowResourceBizExtend(record.biz_extend);
const productDraftStatus = bizExtend?.product_draft_status;
const isImageFlow = record.res_type === ResType.Imageflow;
const { actions } = record;
const deleteActionConfig = actions?.find(
action => action.key === ActionKey.Delete,
);
const copyActionConfig = actions?.find(
action => action.key === ActionKey.Copy,
);
const editConfig = actions?.find(action => action.key === ActionKey.Edit);
const chatflowConfig = actions?.find(
action => action.key === ActionKey.SwitchToChatflow,
);
const workflowConfig = actions?.find(
action => action.key === ActionKey.SwitchToFuncflow,
);
const isSelfCreator = record.creator_id === userId;
const extraActions: ActionItemProps[] = [
{
hide: !editConfig,
disabled: editConfig?.enable === false,
actionKey: 'edit',
actionText: I18n.t('Edit'),
handler: () => actionMap?.[ActionKey.Edit]?.(record),
},
{
hide: !chatflowConfig,
disabled: chatflowConfig?.enable === false,
actionKey: 'switchChatflow',
actionText: I18n.t('wf_chatflow_121', {
flowMode: I18n.t('wf_chatflow_76'),
}),
handler: () => actionMap?.[ActionKey.SwitchToChatflow]?.(record),
},
{
hide: !workflowConfig,
disabled: workflowConfig?.enable === false,
actionKey: 'switchWorkflow',
actionText: I18n.t('wf_chatflow_121', { flowMode: I18n.t('Workflow') }),
handler: () => actionMap?.[ActionKey.SwitchToFuncflow]?.(record),
},
...(getCommonActions?.(record) ?? []),
{
hide:
!enablePublishEntry || // 上架入口加白
// 社区版暂不支持该功能
(!FLAGS['bot.community.store_imageflow'] && isImageFlow) || // Imageflow 不支持商店
!isSelfCreator ||
bizExtend?.plugin_id === '0',
actionKey: 'publishWorkflowProduct',
actionText:
productDraftStatus === ProductDraftStatus.Default
? I18n.t('workflowstore_submit')
: I18n.t('workflowstore_submit_update'),
handler: () => {
publishAction?.(record);
},
},
];
return (
<Table.TableAction
deleteProps={{
hide: !deleteActionConfig,
disabled: deleteActionConfig?.enable === false,
disableConfirm: true,
handler: () => actionMap[ActionKey.Delete]?.(record),
}}
copyProps={{
hide: !copyActionConfig,
disabled: copyActionConfig?.enable === false,
handler: () => actionMap[ActionKey.Copy]?.(record),
}}
actionList={extraActions}
/>
);
};
return { renderWorkflowResourceActions, modals: [deleteModal, publishModal] };
};

View File

@@ -0,0 +1,75 @@
/*
* 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 {
ProductDraftStatus,
SchemaType,
type FrontWorkflowInfo,
} from '@coze-workflow/base';
import { type ResourceInfo } from '@coze-arch/bot-api/plugin_develop';
import { type WorkflowResourceBizExtend } from './type';
export const parseWorkflowResourceBizExtend = (
bizExtend?: Record<string, string>,
): WorkflowResourceBizExtend | undefined => {
if (!bizExtend) {
return undefined;
}
return {
product_draft_status:
bizExtend.product_draft_status !== undefined
? parseInt(bizExtend.product_draft_status || '0')
: ProductDraftStatus.Default,
external_flow_info: bizExtend.external_flow_info,
schema_type:
bizExtend.schema_type !== undefined
? parseInt(bizExtend.schema_type || '0')
: SchemaType.DAG,
plugin_id: bizExtend.plugin_id,
icon_uri: bizExtend.icon_uri,
url: bizExtend.url,
};
};
/**
* 转换 ResourceInfo 为编辑 workflow 所需的 WorkflowInfoLocal 结构
* @param resource
*/
export const transformResourceToWorkflowEditInfo = (
resource: ResourceInfo,
): Pick<
FrontWorkflowInfo,
| 'workflow_id'
| 'url'
| 'icon_uri'
| 'name'
| 'desc'
| 'schema_type'
| 'external_flow_info'
| 'space_id'
> => {
const bizExtend = parseWorkflowResourceBizExtend(resource.biz_extend);
return {
workflow_id: resource.res_id,
url: bizExtend?.url,
icon_uri: bizExtend?.icon_uri,
name: resource.name,
desc: resource.desc,
schema_type: bizExtend?.schema_type,
external_flow_info: bizExtend?.external_flow_info,
space_id: resource.space_id,
};
};