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,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.
*/
export { useGlobalState } from './use-global-state';
export { useExecStateEntity } from './use-exec-state-entity';
export { useSpaceId } from './use-space-id';
export { useLatestWorkflowJson } from './use-latest-workflow-json';
export { useWorkflowOperation } from './use-workflow-operation';
export { useLineService } from './use-line-service';
export { useWorkflowRunService } from './use-workflow-run-service';
export { useTestRunReporterService } from './use-test-run-reporter-service';
export { useScrollToNode } from './use-scroll-to-node';
export { useScrollToLine } from './use-scroll-to-line';
export { useHaveCollaborators } from './use-have-collaborators';
export { useNodeRenderData } from './use-node-render-data';
export { useRedoUndo } from './use-redo-undo';
export { useInputVariables } from './use-input-variables';
export { useGetWorkflowMode } from './use-get-workflow-mode';
export { useRoleService, useRoleServiceStore } from './use-role-service';
export { useUpload } from './use-upload';
export { useVariableService } from './use-variable-service';
export { useNodeRenderScene } from './use-node-render-scene';
export { useTestFormState } from './use-test-form-state';
export { useUpdateSortedPortLines } from './use-update-sorted-port-lines';
export { useAddNode } from './use-add-node';
export {
useFloatLayoutService,
useFloatLayoutSize,
} from './use-float-layout-service';
export { useOpenTraceListPanel } from './use-open-trace-list-panel';
export { useTestRun } from './use-test-run';
export { useDataSetInfos } from './use-dataset-info';
export { useNodeVersionService } from './node-version';
export { useSaveService } from './use-save-service';
export { useDatabaseNodeService } from './use-database-node-service';
export {
usePluginNodeStore,
usePluginNodeService,
} from './use-plugin-node-service';
export { useNewDatabaseQuery } from './use-new-database-query';
export { useCurrentDatabaseQuery } from './use-current-database-query';
export { useCurrentDatabaseID } from './use-current-database-id';
export { useRelatedBotService } from './use-related-bot-service';
export { useWorkflowPreset } from './use-workflow-preset';
export { useWorkflowModels } from './use-workflow-models';
export {
useDependencyService,
useDependencyEntity,
} from './use-dependency-service';

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 { useNodeVersionService } from './use-node-version-service';

View File

@@ -0,0 +1,22 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { NodeVersionService } from '@/services';
export const useNodeVersionService = () =>
useService<NodeVersionService>(NodeVersionService);

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),
};
};

View File

@@ -0,0 +1,31 @@
/*
* 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.
*/
/**
* 左侧添加节点面板的显示隐藏状态,需要被别的地方消费,所以抽象成一个全局 state
*/
import { create } from 'zustand';
interface AddNodeVisibleStore {
visible: boolean;
setVisible: (visible: boolean) => void;
}
export const useAddNodeVisibleStore = create<AddNodeVisibleStore>(set => ({
visible: true,
setVisible: visible => set({ visible }),
}));

View File

@@ -0,0 +1,162 @@
/*
* 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 { set } from 'lodash-es';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { StandardNodeType } from '@coze-workflow/base';
import { getWorkflowVersionByPluginId } from '@/utils';
import { type HandleAddNode } from '@/typing';
import { WorkflowEditService } from '@/services';
import { useNodeVersionService, useGlobalState } from '@/hooks';
import { useAddNodeModal } from './use-add-node-modal';
export interface AddNodeProps {
x: number;
y: number;
isDrag: boolean;
}
export const useAddNode = () => {
const prevAddNodeRef = useRef<{ x: number; y: number; isDrag: boolean }>({
x: 0,
y: 0,
isDrag: false,
});
const updateAddNodePosition = (props: AddNodeProps) => {
prevAddNodeRef.current = props;
};
const {
openPlugin,
openWorkflow,
openImageflow,
pluginModal,
workflowModal,
imageFlowModal,
} = useAddNodeModal(prevAddNodeRef);
const editService = useService<WorkflowEditService>(WorkflowEditService);
const nodeVersionService = useNodeVersionService();
const { spaceId } = useGlobalState();
const handleAddSubWorkflow: HandleAddNode = async (
item,
coord = { x: 0, y: 0 },
isDrag = false,
) => {
const { nodeType, nodeJson, nodeVersionInfo } = item;
if (nodeJson) {
const { workflowId, pluginId } = nodeVersionInfo;
const versionName = await getWorkflowVersionByPluginId({
spaceId,
pluginId,
});
versionName && set(nodeJson, 'data.inputs.workflowVersion', versionName);
if (
!(await nodeVersionService.addSubWorkflowCheck(workflowId, versionName))
) {
return;
}
editService.addNode(
nodeType,
nodeJson,
{ clientX: coord?.x || 0, clientY: coord?.y || 0 },
isDrag,
);
return;
}
// 记录历史位置,打开子流程弹窗
prevAddNodeRef.current = {
x: coord.x,
y: coord.y,
isDrag,
};
openWorkflow();
return;
};
const handleAddPlugin: HandleAddNode = async (
item,
coord = { x: 0, y: 0 },
isDrag = false,
) => {
const { nodeType, nodeJson, modalProps, nodeVersionInfo } = item;
if (nodeJson) {
const { pluginId, version } = nodeVersionInfo;
if (!(await nodeVersionService.addApiCheck(pluginId, version))) {
return;
}
// 节点添加面板,拖拽添加具体插件节点逻辑
editService.addNode(
nodeType,
nodeJson,
{ clientX: coord?.x || 0, clientY: coord?.y || 0 },
isDrag,
);
return;
}
// 记录历史位置,打开插件弹窗
prevAddNodeRef.current = {
x: coord.x,
y: coord.y,
isDrag,
};
// 打开插件弹窗添加节点
// eslint-disable-next-line @typescript-eslint/no-explicit-any
openPlugin({ modalProps: modalProps as any });
};
const handleAddNode: HandleAddNode = (
item,
coord = { x: 0, y: 0 },
isDrag = false,
) => {
const { nodeType } = item;
if (nodeType === StandardNodeType.Api) {
// 节点添加面板,拖拽添加具体子插件
return handleAddPlugin(item, coord, isDrag);
}
if (nodeType === StandardNodeType.SubWorkflow) {
// 节点添加面板,拖拽添加具体子流程
return handleAddSubWorkflow(item, coord, isDrag);
}
// 节点添加面板,拖拽添加普通节点
editService.addNode(
item.nodeType,
item.nodeJson,
{ clientX: coord?.x || 0, clientY: coord?.y || 0 },
isDrag,
);
};
return {
handleAddNode,
openPlugin,
openWorkflow,
openImageflow,
updateAddNodePosition,
modals: [workflowModal, pluginModal, imageFlowModal],
};
};

View File

@@ -0,0 +1,6 @@
.warningIcon {
> :global(svg) {
width: 22px;
height: 22px;
}
}

View File

@@ -0,0 +1,179 @@
/*
* 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.
*/
/**
* 全局hook管理 Biz IDE 的状态,与 React 组件交互
*/
import { create } from 'zustand';
import { I18n } from '@coze-arch/i18n';
import { Modal } from '@coze-arch/bot-semi';
import { IconWarningInfo } from '@coze-arch/bot-icons';
import { useSingletonInnerSideSheet } from '../components/workflow-inner-side-sheet';
// TODO: 改成 UIModal
import styles from './use-biz-ide-state.module.less';
interface BizIDEState {
/**
* 当前开启的 BizIDE 唯一标识
*/
uniqueId: string | null;
/**
* 当前是否有 BizIDE 开启
*/
isBizIDEOpen: boolean;
/**
* 当前开启的 BizIDE 是否在 test 运行中
*/
isBizIDETesting: boolean;
}
interface BizIDEStateStore extends BizIDEState {
setUniqueId: (uniqueId: string) => void;
setIsBizIDEOpen: (isBizIDEOpen: boolean) => void;
setIsBizIDETesting: (isBizIDETesting: boolean) => void;
setData: (data: Partial<BizIDEState>) => void;
}
const useBizIDEStateStore = create<BizIDEStateStore>(set => ({
uniqueId: null,
setUniqueId: uniqueId => set({ uniqueId }),
isBizIDEOpen: false,
setIsBizIDEOpen: isBizIDEOpen => set({ isBizIDEOpen }),
isBizIDETesting: false,
setIsBizIDETesting: isBizIDETesting => set({ isBizIDETesting }),
setData: (data: Partial<BizIDEState>) => set(data),
}));
export const useBizIDEState = () => {
const {
uniqueId,
isBizIDEOpen,
isBizIDETesting,
setUniqueId,
setIsBizIDEOpen,
setIsBizIDETesting,
setData,
} = useBizIDEStateStore(state => state);
const {
handleOpen: openSideSheet,
handleClose: closeSideSheet,
visible,
forceClose,
} = useSingletonInnerSideSheet(uniqueId || '');
const openBizIDE = async id => {
const opened = await openSideSheet(id);
if (opened) {
setUniqueId(id);
setIsBizIDEOpen(true);
}
};
const closeBizIDE = async () => {
const closed = await closeSideSheet();
if (closed) {
setData({
uniqueId: null,
isBizIDEOpen: false,
isBizIDETesting: false,
});
}
return closed;
};
const closeConfirm = async (id?: string): Promise<boolean> => {
// 当传入id时表示关闭指定id的弹窗。当id和当前nodeId不一致时说明已经关闭了
if (id && id !== uniqueId) {
return true;
}
if (!isBizIDEOpen || !isBizIDETesting) {
return true;
}
return new Promise(resolve => {
Modal.warning({
icon: (
<IconWarningInfo
className={styles.warningIcon}
style={{
width: 22,
height: 22,
}}
/>
),
title: I18n.t('workflow_detail_code_is_running'),
content: I18n.t('workflow_detail_code_is_terminate_execution'),
okType: 'warning',
okText: I18n.t('Confirm'),
cancelText: I18n.t('Cancel'),
onOk: () => {
setData({
uniqueId: null,
isBizIDEOpen: false,
isBizIDETesting: false,
});
resolve(true);
},
onCancel: () => {
resolve(false);
},
});
});
};
const forceCloseBizIDE = () => {
if (visible) {
forceClose();
}
setData({
uniqueId: null,
isBizIDEOpen: false,
isBizIDETesting: false,
});
};
return {
uniqueId,
isBizIDEOpen,
isBizIDETesting,
setBizIDEUniqueId: setUniqueId,
setIsBizIDEOpen,
setIsBizIDETesting,
/**
* 关闭 Biz IDE
* 检测是否正在运行中,包括 confirm 对话框的出现也封装在这里
* 外部只需要调用这个 hook 即可
* 在三种情况下会 resolve true并关闭 BizIDE
* 1. BizIDE 没有被打开
2. BizIDE 被打开了,但是不在运行中
3. BizIDE 在运行中,但是用户点击了 confirm 确认
*/
closeBizIDE,
/**
* 强制关闭 Biz IDE不管是否在运行中
*/
openBizIDE,
forceCloseBizIDE,
closeConfirm,
};
};

View File

@@ -0,0 +1,27 @@
/*
* 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 { useWorkflowNode } from '@coze-workflow/base';
/**
* 获取数据库节点选中的数据库ID
* @returns 返回当前数据库ID
*/
export function useCurrentDatabaseID() {
const { data } = useWorkflowNode();
const databaseList = data?.databaseInfoList ?? data?.inputs?.databaseInfoList;
return databaseList?.[0]?.databaseInfoID;
}

View File

@@ -0,0 +1,59 @@
/*
* 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 { useEffect, useRef } from 'react';
import { MessageBizType } from '@coze-arch/idl/workflow_api';
import type { Disposable } from '@flowgram-adapter/common';
import { useNewDatabaseQuery } from './use-new-database-query';
import { useDependencyService } from './use-dependency-service';
import { useDatabaseNodeService } from './use-database-node-service';
import { useCurrentDatabaseID } from './use-current-database-id';
/**
* 获取当前数据库的查询
* @returns 返回数据库查询结果
* - data: 查询成功时返回数据库对象无数据时返回undefined
* - isLoading: 加载状态
* - error: 查询失败时的错误对象
*/
export function useCurrentDatabaseQuery() {
const currentDatabaseID = useCurrentDatabaseID();
const { data, isLoading, error } = useNewDatabaseQuery(currentDatabaseID);
const disposeRef: React.MutableRefObject<Disposable | null> =
useRef<Disposable>(null);
const databaseNodeService = useDatabaseNodeService();
const dependencyService = useDependencyService();
useEffect(() => {
databaseNodeService.load(currentDatabaseID);
if (!disposeRef.current) {
disposeRef.current = dependencyService.onDependencyChange(source => {
if (source?.bizType === MessageBizType.Database) {
// 数据库资源更新时,重新请求接口
databaseNodeService.load(currentDatabaseID);
}
});
}
return () => {
disposeRef?.current?.dispose?.();
disposeRef.current = null;
};
}, [currentDatabaseID]);
return { data, isLoading, error };
}

View File

@@ -0,0 +1,35 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { type DatabseNodeStore } from '@/services/database-node-service-impl';
import { DatabaseNodeService } from '@/services/database-node-service';
export function useDatabaseNodeService() {
const databaseNodeService =
useService<DatabaseNodeService>(DatabaseNodeService);
return databaseNodeService;
}
export const useDatabaseServiceStore = <T>(
selector: (s: DatabseNodeStore) => T,
) => {
const databaseNodeService = useDatabaseNodeService();
return databaseNodeService.store(selector);
};

View File

@@ -0,0 +1,73 @@
/*
* 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, useEffect, useCallback, useRef } from 'react';
import { MessageBizType } from '@coze-arch/idl/workflow_api';
import { type Dataset } from '@coze-arch/bot-api/knowledge';
import { type Disposable } from '@flowgram-adapter/common';
import { useGlobalState } from './use-global-state';
import { useDependencyService } from './use-dependency-service';
export const useDataSetInfos = ({ ids }: { ids: string[] }) => {
const [dataSets, setDataSets] = useState<Dataset[]>([]);
const [isReady, setReady] = useState(false);
const { spaceId, sharedDataSetStore } = useGlobalState();
const dependencyService = useDependencyService();
const disposeRef: React.MutableRefObject<Disposable | null> =
useRef<Disposable>(null);
const getDataSetInfos = useCallback(
async (_ids: string[]) => {
try {
const _dataSets = await sharedDataSetStore.getDataSetInfosByIds(
_ids,
spaceId,
);
setDataSets(_dataSets);
} catch (e) {
console.error(e);
} finally {
setReady(true);
}
},
[spaceId],
);
useEffect(() => {
getDataSetInfos(ids);
if (!disposeRef.current) {
disposeRef.current = dependencyService.onDependencyChange(source => {
if (source?.bizType === MessageBizType.Dataset) {
getDataSetInfos(ids);
}
});
}
return () => {
disposeRef?.current?.dispose?.();
disposeRef.current = null;
};
}, [ids.join('')]);
return {
dataSets,
isReady,
cacheDataSetInfo: sharedDataSetStore.addDataSetInfo,
};
};

View File

@@ -0,0 +1,31 @@
/*
* 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 { useConfigEntity, useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowDependencyService } from '@/services/workflow-dependency-service';
import { WorkflowDependencyStateEntity } from '@/entities';
export const useDependencyService = () =>
useService<WorkflowDependencyService>(WorkflowDependencyService);
export const useDependencyEntity = () => {
const entity = useConfigEntity<WorkflowDependencyStateEntity>(
WorkflowDependencyStateEntity,
true,
);
return entity;
};

View File

@@ -0,0 +1,39 @@
/*
* 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 { createContext, useContext } from 'react';
export enum EditorTheme {
Light = 'light',
Dark = 'dark',
}
interface EditorThemeState {
editorTheme: EditorTheme;
setEditorTheme: (next: EditorTheme) => void;
isDarkTheme: boolean;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export const EditorThemeContext = createContext<EditorThemeState>({
editorTheme: EditorTheme.Light,
setEditorTheme: _next => {
console.log(_next);
},
isDarkTheme: false,
});
export const useEditorThemeState = () => useContext(EditorThemeContext);

View File

@@ -0,0 +1,25 @@
/*
* 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 { useEntity } from '@flowgram-adapter/free-layout-editor';
import { WorkflowExecStateEntity } from '../entities/workflow-exec-state-entity';
export const useExecStateEntity = () => {
const entity = useEntity<WorkflowExecStateEntity>(WorkflowExecStateEntity);
return entity;
};

View File

@@ -0,0 +1,38 @@
/*
* 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, useEffect } from 'react';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowFloatLayoutService } from '@/services/workflow-float-layout-service';
export const useFloatLayoutService = () => {
const floatLayoutService = useService(WorkflowFloatLayoutService);
return floatLayoutService;
};
export const useFloatLayoutSize = () => {
const floatLayoutService = useFloatLayoutService();
const [size, setSize] = useState(floatLayoutService.size);
useEffect(() => {
const disposable = floatLayoutService.onSizeChange(setSize);
return () => disposable.dispose();
}, [floatLayoutService, setSize]);
return size;
};

View File

@@ -0,0 +1,81 @@
/*
* 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 { throttle, once } from 'lodash-es';
import { useQuery } from '@tanstack/react-query';
import {
type GetMetaRoleListResponse,
RoleType,
} from '@coze-arch/idl/social_api';
import { Toast } from '@coze-arch/coze-design';
import { SocialApi } from '@coze-arch/bot-api';
import { useGlobalState } from './use-global-state';
import { useGetWorkflowMode } from './use-get-workflow-mode';
const warn = once(
throttle(() => Toast.warning('当前工作流未关联场景'), 10 * 1000),
);
const useQuerySceneFlowMetaRole = () => {
const globalState = useGlobalState();
const { isSceneFlow } = useGetWorkflowMode();
const { bindBizID } = globalState;
if (isSceneFlow && !bindBizID) {
warn();
}
return useQuery({
queryKey: ['scene_flow_role_list'],
staleTime: 10 * 1000,
queryFn: () =>
SocialApi.GetMetaRoleList({
meta_id: bindBizID as string,
}),
placeholderData: {
role_list: [],
} as unknown as GetMetaRoleListResponse,
enabled: isSceneFlow && !!bindBizID,
});
};
export const useGetSceneFlowRoleList = () => {
const { data: res, isLoading } = useQuerySceneFlowMetaRole();
return {
isLoading,
data: res?.role_list.map(item => ({
biz_role_id: item.biz_role_id as string,
role: item.name,
nickname: item.nickname,
role_type: item.role_type,
description: item.description,
})),
};
};
export const useGetSceneFlowBot = () => {
const { data: res, isLoading } = useQuerySceneFlowMetaRole();
if (isLoading) {
return null;
} else {
const host = res?.role_list?.find(item => item.role_type === RoleType.Host);
return {
name: host?.name,
participantId: host?.participant_id,
};
}
};

View File

@@ -0,0 +1,41 @@
/*
* 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.
*/
/**
* 这个 hooks 用来快速判断 workflow 的类型
*/
import { useEntity } from '@flowgram-adapter/free-layout-editor';
import { WorkflowMode } from '@coze-workflow/base/api';
import { WorkflowGlobalStateEntity } from '../typing';
export const useGetWorkflowMode = () => {
const globalState = useEntity<WorkflowGlobalStateEntity>(
WorkflowGlobalStateEntity,
);
const isImageFlow = globalState.flowMode === WorkflowMode.Imageflow;
const isSceneFlow = globalState.flowMode === WorkflowMode.SceneFlow;
const isChatflow = globalState.flowMode === WorkflowMode.ChatFlow;
// const isSceneFlow = true;
return {
isImageFlow,
isSceneFlow,
isChatflow,
};
};

View File

@@ -0,0 +1,30 @@
/*
* 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 { useConfigEntity } from '@flowgram-adapter/free-layout-editor';
import { WorkflowGlobalStateEntity } from '../entities';
/** 获取全局状态 */
export const useGlobalState = (
listenChange = true,
): WorkflowGlobalStateEntity => {
const globalState = useConfigEntity<WorkflowGlobalStateEntity>(
WorkflowGlobalStateEntity,
listenChange,
);
return globalState;
};

View File

@@ -0,0 +1,53 @@
/*
* 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 { useEffect, useState } from 'react';
import { PUBLIC_SPACE_ID } from '@coze-workflow/base/constants';
import { workflowApi } from '@coze-workflow/base';
import { useGlobalState } from './use-global-state';
// 判断当前是否有协作者
export function useHaveCollaborators() {
const { spaceId, workflowId } = useGlobalState();
const [haveCollaborators, setHaveCollaborators] = useState<
boolean | undefined
>();
useEffect(() => {
if (spaceId === PUBLIC_SPACE_ID) {
setHaveCollaborators(false);
return;
}
workflowApi
.ListCollaborators(
{
workflow_id: workflowId,
space_id: spaceId,
},
{
__disableErrorToast: true,
},
)
.then(({ data }) => {
setHaveCollaborators(data.length > 1);
});
});
return haveCollaborators;
}

View File

@@ -0,0 +1,96 @@
/*
* 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 { useThrottleEffect } from 'ahooks';
import { ExpressionEditorTreeHelper } from '@coze-workflow/components';
import {
type InputVariable,
useWorkflowNode,
type ViewVariableType,
} from '@coze-workflow/base';
import { useNodeAvailableVariablesWithNode } from '../form-extensions/hooks';
const useInputs = (): {
name: string;
id?: string;
keyPath?: string[];
}[] => {
const workflowNode = useWorkflowNode();
const inputs = (
(workflowNode?.inputParameters || []) as {
name: string;
input: {
content: {
keyPath: string[];
};
};
}[]
).map(i => ({
...i,
keyPath: [...(i.input?.content?.keyPath || [])], // 深拷贝一份
}));
return inputs;
};
export const useInputVariables = (props?: {
needNullName?: boolean;
needNullType?: boolean;
}) => {
const { needNullName = true, needNullType = false } = props ?? {};
const availableVariables = useNodeAvailableVariablesWithNode();
const inputs = useInputs();
const inputsWithVariables = ExpressionEditorTreeHelper.findAvailableVariables(
{
variables: availableVariables,
inputs,
},
);
// eslint-disable-next-line @typescript-eslint/naming-convention
const _variables = inputsWithVariables.map((v, i) => ({
name: v.name,
id: inputs[i].id,
type: v.variable?.type as ViewVariableType,
index: i,
}));
const [variables, setVariables] = useState<InputVariable[]>();
useThrottleEffect(
() => {
setVariables(
_variables.filter(
v =>
(needNullName ? true : !!v.name) &&
(needNullType ? true : !!v.type),
),
);
},
[
_variables.map(d => `${d.name}${d.type}`).join(''),
needNullName,
needNullType,
],
{
wait: 300,
},
);
return variables;
};

View File

@@ -0,0 +1,37 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import {
WorkflowDocument,
type WorkflowJSON,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowSaveService } from '../services';
export const useLatestWorkflowJson = () => {
const workflowDocument = useService<WorkflowDocument>(WorkflowDocument);
const saveService = useService<WorkflowSaveService>(WorkflowSaveService);
const getLatestWorkflowJson = async (): Promise<WorkflowJSON> => {
await saveService.waitSaving();
return workflowDocument.toJSON() as WorkflowJSON;
};
return { getLatestWorkflowJson };
};

View File

@@ -0,0 +1,25 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowLinesService } from '../services/workflow-line-service';
export const useLineService = () => {
const lineService = useService<WorkflowLinesService>(WorkflowLinesService);
return lineService;
};

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 { useShallow } from 'zustand/react/shallow';
import { type WorkflowDatabase, ViewVariableType } from '@coze-workflow/base';
import { FieldItemType, type DatabaseInfo } from '@coze-arch/bot-api/memory';
import { useDatabaseServiceStore } from './use-database-node-service';
/**
* 查询数据库信息的Hook
* @param id 数据库ID
* @returns 返回对象包含:
* - data: 查询成功时返回数据库信息无数据时返回undefined
* - isLoading: 加载状态
* - error: 查询失败时的错误对象
*/
export function useNewDatabaseQuery(id?: string) {
const { getDatabaseDetail, isLoading, getError } = useDatabaseServiceStore(
useShallow(state => ({
getDatabaseDetail: state.getData,
isLoading: state.loading,
getError: state.getError,
})),
);
const rawData = getDatabaseDetail(id);
const data = transformRawDatabaseToDatabase(rawData?.database_info);
return { data, isLoading, error: getError(id) };
}
function transformRawDatabaseToDatabase(
rawDatabase?: DatabaseInfo,
): WorkflowDatabase | undefined {
if (!rawDatabase) {
return undefined;
}
return {
id: rawDatabase.id as string,
fields: rawDatabase.field_list?.map(field => ({
id: field.alterId as number,
name: field.name,
type: fieldItemTypeToViewVariableType(field.type),
required: field.must_required,
description: field.desc,
isSystemField: field.name
? ['id', 'uuid', 'bstudio_create_time'].includes(field.name)
: false,
})),
iconUrl: rawDatabase.icon_url,
tableName: rawDatabase.table_name,
};
}
function fieldItemTypeToViewVariableType(type?: FieldItemType) {
const typeMap = {
[FieldItemType.Text]: ViewVariableType.String,
[FieldItemType.Number]: ViewVariableType.Integer,
[FieldItemType.Float]: ViewVariableType.Number,
[FieldItemType.Boolean]: ViewVariableType.Boolean,
[FieldItemType.Date]: ViewVariableType.Time,
};
if (!type) {
return undefined;
}
return typeMap[type] || undefined;
}

View File

@@ -0,0 +1,54 @@
/*
* 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 { useEffect, useState } from 'react';
import { pick } from 'lodash-es';
import { FlowNodeRenderData } from '@flowgram-adapter/free-layout-editor';
import { useCurrentEntity } from '@flowgram-adapter/free-layout-editor';
type SimpleNodeRenderData = Pick<FlowNodeRenderData, 'expanded' | 'node'>;
const pickSimpleNodeRenderData = (data: FlowNodeRenderData) =>
pick(data, 'expanded', 'node');
/**
* @deprecated
* 获取当前节点的渲染数据包括expanded等渲染相关的状态
*/
export const useNodeRenderData = () => {
const node = useCurrentEntity();
const initialRenderData =
node.getData<FlowNodeRenderData>(FlowNodeRenderData);
const [nodeRenderData, setNodeRenderData] = useState<SimpleNodeRenderData>(
pickSimpleNodeRenderData(initialRenderData),
);
useEffect(() => {
const disposable = initialRenderData.onDataChange(data => {
setNodeRenderData(pickSimpleNodeRenderData(data as FlowNodeRenderData));
});
return () => {
disposable?.dispose();
};
}, []);
return {
...nodeRenderData,
expanded: true, // Coze V2 没有节点折叠
toggleNodeExpand: initialRenderData.toggleExpand.bind(initialRenderData),
};
};

View File

@@ -0,0 +1,29 @@
/*
* 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 { useContext } from 'react';
import { NodeRenderSceneContext } from '@/contexts/node-render-context';
export function useNodeRenderScene() {
const scene = useContext(NodeRenderSceneContext);
return {
isNewNodeRender: scene === 'new-node-render',
isOldNodeRender: scene === 'old-node-render',
isNodeSideSheet: scene === 'node-side-sheet',
};
}

View File

@@ -0,0 +1,110 @@
/*
* 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 { createContext, useContext, type ReactNode } from 'react';
import { create } from 'zustand';
interface NodeSideSheetStore {
leftPanelVisible?: boolean;
leftPanelWidth?: number;
leftPanelContent?: ReactNode;
leftPanelContentId?: string;
openLeftPanel: (options: { content: ReactNode; contentId?: string }) => void;
updateLeftPanel: (options: { content: ReactNode }) => void;
closeLeftPanel: () => void;
rightPanelVisible?: boolean;
rightPanelWidth: number;
rightPanelContent?: ReactNode;
openRightPanel: (options: { content: ReactNode }) => void;
closeRightPanel: () => void;
isNodeSideSheetVisible: boolean;
openNodeSideSheet: (options?: { width?: number }) => void;
mainPanelWidth?: number;
setMainPanelWidth: (width: number) => void;
closeNodeSideSheet: () => void;
closeAllExtraSheets: () => void;
}
export const useNodeSideSheetStore = create<NodeSideSheetStore>(set => ({
isNodeSideSheetVisible: false,
leftPanelVisible: false,
mainPanelWidth: 360,
setMainPanelWidth: width => set({ mainPanelWidth: width }),
openNodeSideSheet: options =>
set(state => ({
isNodeSideSheetVisible: true,
width: options?.width || state.mainPanelWidth,
})),
closeNodeSideSheet: () => {
set({
isNodeSideSheetVisible: false,
leftPanelVisible: false,
leftPanelContent: undefined,
rightPanelVisible: false,
rightPanelContent: undefined,
});
},
openLeftPanel: options =>
set({
leftPanelVisible: true,
leftPanelContent: options.content,
leftPanelContentId: options.contentId,
}),
updateLeftPanel: options =>
set({
leftPanelContent: options.content,
}),
closeLeftPanel: () =>
set({
leftPanelVisible: false,
leftPanelContent: undefined,
leftPanelContentId: '',
}),
rightPanelWidth: 360,
openRightPanel: options =>
set({
rightPanelVisible: true,
rightPanelContent: options.content,
}),
closeRightPanel: () =>
set({
rightPanelVisible: false,
rightPanelContent: undefined,
}),
closeAllExtraSheets: () => {
set({
leftPanelVisible: false,
leftPanelContent: undefined,
rightPanelVisible: false,
rightPanelContent: undefined,
});
},
}));
interface NodeFormPanelState {
fullscreenPanel: React.ReactNode;
setFullscreenPanel: (next: React.ReactNode) => void;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export const NodeFormPanelContext = createContext(
{} as unknown as NodeFormPanelState,
);
export const useNodeFormPanelState = () => useContext(NodeFormPanelContext);

View File

@@ -0,0 +1,40 @@
/*
* 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 { useCallback } from 'react';
import { LayoutPanelKey } from '@/constants';
import { useFloatLayoutService } from './use-float-layout-service';
interface OpenOptions {
defaultTab?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
export const useOpenTraceListPanel = () => {
const floatLayoutService = useFloatLayoutService();
const open = useCallback(
(options?: OpenOptions) => {
floatLayoutService.open(LayoutPanelKey.TraceList, 'bottom', options);
},
[floatLayoutService],
);
return { open };
};

View File

@@ -0,0 +1,30 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import {
PluginNodeService,
type PluginNodeStore,
} from '@/services/plugin-node-service';
export const usePluginNodeService = () =>
useService<PluginNodeService>(PluginNodeService);
export const usePluginNodeStore = <T>(selector: (s: PluginNodeStore) => T) => {
const pluginService = usePluginNodeService();
return pluginService.store(selector);
};

View File

@@ -0,0 +1,34 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { HistoryService, WorkflowHistoryConfig } from '@coze-workflow/history';
export const useRedoUndo = () => {
const historyService = useService<HistoryService>(HistoryService);
const config = useService<WorkflowHistoryConfig>(WorkflowHistoryConfig);
return {
start: () => {
historyService.start();
config.disabled = false;
},
stop: () => {
historyService.stop();
config.disabled = true;
},
};
};

View File

@@ -0,0 +1,19 @@
/*
* 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 { useRefInput } from './use-ref-input';
export { useRefInputProps } from './use-ref-input-props';
export { useRefInputNode } from './use-ref-input-node';

View File

@@ -0,0 +1,217 @@
/*
* 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 {
useCallback,
type ReactNode,
type CSSProperties,
useState,
} from 'react';
import classNames from 'classnames';
import {
ValueExpression,
ValueExpressionType,
type RefExpression,
type ViewVariableType,
} from '@coze-workflow/base';
import { type TreeNodeData } from '@coze-arch/bot-semi/Tree';
import { IconCozApply } from '@coze-arch/coze-design/icons';
import { IconButton, type TreeSelectProps } from '@coze-arch/coze-design';
import {
RefValueDisplay,
type RefTagColor,
type RefValueDisplayProps,
} from '@/form-extensions/components/value-expression-input/ref-value-display';
import {
type CustomFilterVar,
type VariableTreeDataNode,
type RenderDisplayVarName,
} from '@/form-extensions/components/tree-variable-selector/types';
import {
VariableSelector,
type VariableSelectorProps,
} from '@/form-extensions/components/tree-variable-selector';
export const useRefInputNode = ({
value,
onChange,
onBlur,
disabled,
variablesDataSource,
validateStatus,
readonly,
invalidContent,
renderDisplayVarName,
testId,
disabledTypes,
showClear = false,
customFilterVar,
setFocused,
style,
refTagColor,
hideDeleteIcon,
variableTagStyle,
optionFilter,
renderExtraOption,
enableSelectNode,
popoverStyle,
handleDataSource,
variableTypeConstraints,
}: {
value?: ValueExpression;
onChange: (v: ValueExpression | undefined) => void;
onBlur?: () => void;
disabled?: boolean;
variablesDataSource?: VariableTreeDataNode[];
validateStatus?: TreeSelectProps['validateStatus'];
readonly?: boolean;
testId?: string;
disabledTypes?: ViewVariableType[];
showClear?: boolean;
invalidContent?: string;
renderDisplayVarName?: RenderDisplayVarName;
customFilterVar?: CustomFilterVar;
setFocused?: (focused: boolean) => void;
style?: CSSProperties;
refTagColor?: RefTagColor;
hideDeleteIcon?: boolean;
variableTagStyle?: CSSProperties;
optionFilter?: VariableSelectorProps['optionFilter'];
handleDataSource?: VariableSelectorProps['handleDataSource'];
renderExtraOption?: (
data?: TreeNodeData[],
action?: {
hiddenPopover: () => void;
},
) => ReactNode;
enableSelectNode?: boolean;
popoverStyle?: CSSProperties;
/* 类型限制,引用类型不满足限制时,显示警告信息 */
variableTypeConstraints?: RefValueDisplayProps['variableTypeConstraints'];
}) => {
const onRefChange = useCallback(
(v: string[] | undefined): void => {
if (v === undefined) {
onChange(undefined);
} else {
onChange({
type: ValueExpressionType.REF,
content: { keyPath: v as string[] },
});
}
},
[onChange],
);
const handleRefRemove = () => {
onChange?.(undefined);
onBlur?.();
};
const [focused, setStateFocused] = useState(false);
const _setFocused = useCallback(
(v: boolean) => {
setStateFocused(v);
setFocused?.(v);
},
[setFocused],
);
const renderVariableSelect = (trigger: ReactNode) =>
readonly && trigger ? (
trigger
) : (
<VariableSelector
testId={testId}
disabled={disabled}
disabledTypes={disabledTypes}
dataSource={variablesDataSource}
value={
value && ValueExpression.isRef(value)
? (value.content?.keyPath as VariableSelectorProps['value'])
: undefined
}
onChange={onRefChange}
onBlur={onBlur}
validateStatus={validateStatus}
readonly={readonly}
showClear={showClear}
customFilterVar={customFilterVar}
onPopoverVisibleChange={_setFocused}
trigger={trigger}
style={style}
invalidContent={invalidContent}
renderDisplayVarName={renderDisplayVarName}
optionFilter={optionFilter}
renderExtraOption={renderExtraOption}
enableSelectNode={enableSelectNode}
popoverStyle={popoverStyle}
handleDataSource={handleDataSource}
/>
);
const renderVariableDisplay = (props?: { needWrapper?: boolean }) =>
props?.needWrapper ? (
<div
className={classNames(
'w-full max-w-[100%] h-[24px] pr-[4px]',
'flex flex-row items-center justify-between',
'bg-transparent hover:bg-background-5 active:bg-background-6',
'rounded-lg border border-solid coz-stroke-plus hover:coz-mg-secondary-hovered active:coz-mg-secondary-pressed',
{
'semi-input-wrapper-error': validateStatus === 'error',
'coz-stroke-primary': validateStatus !== 'error',
'!coz-stroke-hglt': focused,
'pointer-events-none': readonly,
},
)}
>
{renderVariableDisplay()}
<div>
{renderVariableSelect(
<IconButton
size="mini"
color="secondary"
icon={<IconCozApply className="text-[16px]" />}
/>,
)}
</div>
</div>
) : (
renderVariableSelect(
<div
className={classNames(
'cursor-pointer w-full overflow-hidden flex items-center pl-0.5',
)}
>
<RefValueDisplay
value={value as RefExpression}
onClose={handleRefRemove}
tagColor={refTagColor}
closable={!hideDeleteIcon}
style={variableTagStyle}
variableTypeConstraints={variableTypeConstraints}
readonly={readonly}
/>
</div>,
)
);
return {
renderVariableSelect,
renderVariableDisplay,
};
};

View File

@@ -0,0 +1,110 @@
/*
* 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 { useEffect } from 'react';
import { get } from 'lodash-es';
import { type FeedbackStatus } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { useService } from '@flowgram-adapter/free-layout-editor';
import {
WorkflowVariableService,
useVariableTypeChange,
} from '@coze-workflow/variable';
import {
ValueExpressionType,
type ValueExpression,
type ViewVariableType,
} from '@coze-workflow/base';
import { useNodeAvailableVariablesWithNode } from '@/form-extensions/hooks';
import { feedbackStatus2ValidateStatus } from '@/form-extensions/components/utils';
import { formatWithNodeVariables } from '@/form-extensions/components/tree-variable-selector/utils';
export const useRefInputProps = ({
disabledTypes,
value,
onChange,
node,
feedbackStatus,
}: {
disabledTypes?: ViewVariableType[];
value?: ValueExpression;
onChange: (v: ValueExpression) => void;
node: FlowNodeEntity;
feedbackStatus?: FeedbackStatus;
}) => {
const availableVariables = useNodeAvailableVariablesWithNode();
const variableService: WorkflowVariableService = useService(
WorkflowVariableService,
);
const variablesDataSource = formatWithNodeVariables(
availableVariables,
disabledTypes || [],
);
const keyPath = get(value, 'content.keyPath') as unknown as string[];
// 监听联动变量变化,从而重新触发 effect
useEffect(() => {
const hasDisabledTypes =
Array.isArray(disabledTypes) && disabledTypes.length > 0;
if (!keyPath || !hasDisabledTypes) {
return;
}
const listener = variableService.onListenVariableTypeChange(
keyPath,
v => {
// 如果变量类型变化后,位于 disabledTypes 中,那么需要清空
if (v && (disabledTypes || []).includes(v.type)) {
onChange({
type: ValueExpressionType.REF,
});
}
},
{ node },
);
return () => {
listener?.dispose();
};
}, [keyPath, disabledTypes]);
useVariableTypeChange({
keyPath,
onTypeChange: ({ variableMeta: v }) => {
const hasDisabledTypes =
Array.isArray(disabledTypes) && disabledTypes.length > 0;
if (!hasDisabledTypes) {
return;
}
if (v && (disabledTypes || []).includes(v.type)) {
onChange({
type: ValueExpressionType.REF,
});
}
},
});
return {
variablesDataSource,
validateStatus: feedbackStatus2ValidateStatus(feedbackStatus),
};
};

View File

@@ -0,0 +1,97 @@
/*
* 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 CSSProperties } from 'react';
import { type FeedbackStatus } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import {
type ValueExpression,
type ViewVariableType,
} from '@coze-workflow/base/types';
import { type TreeSelectProps } from '@coze-arch/coze-design';
import {
type CustomFilterVar,
type RenderDisplayVarName,
} from '@/form-extensions/components/tree-variable-selector/types';
import { useRefInputProps } from './use-ref-input-props';
import { useRefInputNode } from './use-ref-input-node';
export const useRefInput = ({
node,
feedbackStatus,
value,
onChange,
onBlur,
disabled,
readonly,
testId,
disabledTypes,
showClear = false,
customFilterVar,
setFocused,
style,
invalidContent,
renderDisplayVarName,
}: {
node: FlowNodeEntity;
feedbackStatus?: FeedbackStatus;
value?: ValueExpression;
onChange: (v: ValueExpression | undefined) => void;
onBlur?: () => void;
disabled?: boolean;
readonly?: boolean;
invalidContent?: string;
renderDisplayVarName?: RenderDisplayVarName;
testId?: string;
disabledTypes?: ViewVariableType[];
showClear?: boolean;
customFilterVar?: CustomFilterVar;
style?: CSSProperties;
setFocused?: (focused: boolean) => void;
}) => {
const { variablesDataSource, validateStatus } = useRefInputProps({
disabledTypes,
value,
onChange,
node,
feedbackStatus,
});
const { renderVariableSelect, renderVariableDisplay } = useRefInputNode({
value,
onChange,
onBlur,
disabled,
variablesDataSource,
validateStatus: validateStatus as TreeSelectProps['validateStatus'],
readonly,
testId,
disabledTypes,
invalidContent,
renderDisplayVarName,
showClear,
customFilterVar,
setFocused,
style,
});
return {
renderVariableSelect,
renderVariableDisplay,
};
};

View File

@@ -0,0 +1,22 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { RelatedCaseDataService } from '@/services';
export const useRelatedBotService = () =>
useService<RelatedCaseDataService>(RelatedCaseDataService);

View File

@@ -0,0 +1,52 @@
/*
* 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 { persist, devtools } from 'zustand/middleware';
import { create } from 'zustand';
interface ResizableSidePanelStoreState {
width: number;
}
interface ResizableSidePanelStoreActions {
setWidth: (width: number) => void;
}
type ResizableSidePanelStore = ResizableSidePanelStoreState &
ResizableSidePanelStoreActions;
const NAME = 'workflow-resizable-side-panel';
/**
* 可调节宽度的侧拉窗状态,需要持久化
*/
export const useResizableSidePanelStore = create<ResizableSidePanelStore>()(
devtools(
persist(
set => ({
width: 0,
setWidth: width => set({ width }),
}),
{
name: NAME,
},
),
{
enabled: IS_DEV_MODE,
name: NAME,
},
),
);

View File

@@ -0,0 +1,29 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { RoleService, type RoleServiceState } from '@/services/role-service';
export const useRoleService = () => useService<RoleService>(RoleService);
export const useRoleServiceStore = <T>(
selector: (s: RoleServiceState) => T,
) => {
const roleService = useRoleService();
return roleService.store(selector);
};

View File

@@ -0,0 +1,22 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowSaveService } from '@/services';
export const useSaveService = () =>
useService<WorkflowSaveService>(WorkflowSaveService);

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 {
usePlayground,
SelectionService,
useService,
} from '@flowgram-adapter/free-layout-editor';
import { type PlaygroundConfigRevealOpts } from '@flowgram-adapter/free-layout-editor';
import { Rectangle, SizeSchema } from '@flowgram-adapter/common';
import { useLineService } from './use-line-service';
export const useScrollToLine = () => {
const lineService = useLineService();
const playground = usePlayground();
const selectionService = useService<SelectionService>(SelectionService);
const scrollToLine = async (fromId: string, toId: string) => {
const line = lineService.getLine(fromId, toId);
let success = false;
if (line) {
const bounds = Rectangle.enlarge([line.bounds]).pad(30, 30);
const viewport = playground.config.getViewport(false);
const zoom = SizeSchema.fixSize(bounds, viewport);
const scrollConfig: PlaygroundConfigRevealOpts = {
bounds,
zoom,
scrollToCenter: true,
easing: true,
};
selectionService.selection = [line];
await playground.config.scrollToView(scrollConfig);
success = true;
}
return success;
};
return scrollToLine;
};

View File

@@ -0,0 +1,40 @@
/*
* 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 { FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { useService, usePlayground } from '@flowgram-adapter/free-layout-editor';
import { WorkflowSelectService } from '@flowgram-adapter/free-layout-editor';
export const useScrollToNode = () => {
const selectServices = useService<WorkflowSelectService>(
WorkflowSelectService,
);
const playground = usePlayground();
const scrollToNode = async (nodeId: string) => {
let success = false;
const node = playground.entityManager.getEntityById<FlowNodeEntity>(nodeId);
if (node) {
await selectServices.selectNodeAndScrollToView(node, true);
success = true;
}
return success;
};
return scrollToNode;
};

View File

@@ -0,0 +1,22 @@
/*
* 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 { useGlobalState } from './use-global-state';
export const useSpaceId = (): string => {
const globalState = useGlobalState();
return globalState.spaceId;
};

View File

@@ -0,0 +1,28 @@
/*
* 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 { useConfigEntity } from '@flowgram-adapter/free-layout-editor';
import { WorkflowTemplateStateEntity } from '@/entities';
export const useTemplateService = () => {
const entity = useConfigEntity<WorkflowTemplateStateEntity>(
WorkflowTemplateStateEntity,
true,
);
return entity;
};

View File

@@ -0,0 +1,28 @@
/*
* 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 { useConfigEntity } from '@flowgram-adapter/free-layout-editor';
import { WorkflowTestFormStateEntity } from '../entities';
export const useTestFormState = () => {
const entity = useConfigEntity<WorkflowTestFormStateEntity>(
WorkflowTestFormStateEntity,
true,
);
return entity;
};

View File

@@ -0,0 +1,22 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { TestRunReporterService } from '@/services';
export const useTestRunReporterService = () =>
useService<TestRunReporterService>(TestRunReporterService);

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 { useEffect, useRef } from 'react';
import { type GetWorkFlowProcessData } from '@coze-arch/idl/workflow_api';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { TestRunState } from '../services/workflow-run-service';
import { WorkflowRunService } from '../services';
import { useTestRunFlow } from '../components/test-run/hooks/use-test-run-flow';
import { useExecStateEntity } from './use-exec-state-entity';
export interface TestRunInstanceAction {
/* 触发全流程 testrun */
handleTestRun: () => Promise<void>;
cancelTestRun: () => Promise<void>;
pauseTestRun: () => void;
continueTestRun: () => void;
getTestRunHistory: (config: {
/** 是否展示节点结果 */
showNodeResults?: boolean;
/** 指定执行 ID, 若不填, 则展示最近一次运行结果 */
executeId?: string;
}) => Promise<GetWorkFlowProcessData>;
}
export interface TestRunInstanceState {
/* 是否执行中 */
isExecuting: boolean;
/* 是否执行成功 */
isSucceed: boolean;
/* 是否执行失败 */
isFailed: boolean;
/* 是否取消执行 */
isCanceled: boolean;
/* 是否暂停 */
isPaused: boolean;
/* 是否运行结束 */
isEnd: boolean;
}
export interface TestRunInstanceCallback {
/** testRun 执行前回调 */
onBeforeTestRun?: () => void;
/** testRun 开始执行回调 */
onTestRunStart?: (executeId: string, isSingleMode?: boolean) => void;
/** testRun 取消回调 */
onTestRunCanceled?: (executeId: string) => void;
/** testRun 失败回调 */
onTestRunFailed?: (executeId: string) => void;
/** testRun 成功回调 */
onTestRunSucceed?: (executeId: string) => void;
/** testRun 结束回调 */
onTestRunEnd?: (testRunState: TestRunState, executeId: string) => void;
}
export type TestRunInstance = TestRunInstanceAction & TestRunInstanceState;
export const useTestRun = (props?: {
callbacks?: TestRunInstanceCallback;
}): TestRunInstance => {
const { callbacks = {} } = props || {};
const {
onBeforeTestRun,
onTestRunStart,
onTestRunEnd,
onTestRunFailed,
onTestRunCanceled,
onTestRunSucceed,
} = callbacks;
const runService = useService<WorkflowRunService>(WorkflowRunService);
const {
config: { executeId, isSingleMode },
} = useExecStateEntity();
const isSingleModeRef = useRef(isSingleMode);
isSingleModeRef.current = isSingleMode;
const { onTestRunStateChange, testRunState } = runService;
const { testRunFlow } = useTestRunFlow();
const testRunInstance: TestRunInstance = {
handleTestRun: async () => {
onBeforeTestRun?.();
await testRunFlow();
},
cancelTestRun: runService.cancelTestRun,
pauseTestRun: () => runService.pauseTestRun(),
continueTestRun: () => runService.continueTestRun(),
getTestRunHistory: config => runService.getProcessResult(config),
isExecuting: testRunState === TestRunState.Executing,
isSucceed: testRunState === TestRunState.Succeed,
isFailed: testRunState === TestRunState.Failed,
isCanceled: testRunState === TestRunState.Canceled,
isPaused: testRunState === TestRunState.Paused,
isEnd: [
TestRunState.Succeed,
TestRunState.Failed,
TestRunState.Canceled,
].includes(testRunState),
};
useEffect(() => {
// 处理testRun回调
const dispose = onTestRunStateChange(({ prevState, curState }) => {
if (
[
TestRunState.Succeed,
TestRunState.Failed,
TestRunState.Canceled,
].includes(curState)
) {
onTestRunEnd?.(curState, executeId);
switch (curState) {
case TestRunState.Failed:
onTestRunFailed?.(executeId);
break;
case TestRunState.Succeed:
onTestRunSucceed?.(executeId);
break;
case TestRunState.Canceled:
onTestRunCanceled?.(executeId);
break;
default:
break;
}
}
if (
prevState === TestRunState.Idle &&
curState === TestRunState.Executing
) {
onTestRunStart?.(executeId, isSingleModeRef.current);
}
});
return () => dispose.dispose();
}, [callbacks, executeId]);
return testRunInstance;
};

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 {
WorkflowNodePortsData,
useCurrentEntity,
} from '@flowgram-adapter/free-layout-editor';
import { useLineService } from './use-line-service';
/**
* 端口排序更新后,需要重新连线。
*
* 多端口的情况下使用了index作为portId 而不是uid。
*
* 当port排序变更后index并不会改变如果希望连线和数据保持一致只能重新连线。
*
* */
export const useUpdateSortedPortLines = (
calcPortId: (index: number) => string,
) => {
const node = useCurrentEntity();
const lineService = useLineService();
/**
* 将startIndex 到 endIndex 中间所有的连线全部重新连接
* 两两交互,例:
*
* 0 1 2
*
* 1 0 2
*
* 1 2 0
* */
const updateSortedPortLines = (startIndex: number, endIndex: number) => {
if (startIndex === endIndex) {
return;
}
const step = startIndex < endIndex ? 1 : -1;
for (let i = startIndex; i !== endIndex; i += step) {
const oldPortInfo = {
from: node.id,
fromPort: calcPortId(i),
};
const newPortInfo = {
from: node.id,
fromPort: calcPortId(i + step),
};
lineService.replaceLineByPort(oldPortInfo, newPortInfo);
}
// 拖拽结束后端口dom对应的portId变更需要更新一下
node
.getData<WorkflowNodePortsData>(WorkflowNodePortsData)
.updateDynamicPorts();
};
return updateSortedPortLines;
};

View File

@@ -0,0 +1,24 @@
/*
* 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 const PREVIEW_IMAGE_TYPE = ['jpg', 'jpeg', 'png', 'webp', 'svg'];
export const MAX_IMAGE_SIZE = 1024 * 1024 * 5;
/**
* 文件扩展至 500MB
*/
export const MAX_FILE_SIZE = 1024 * 1024 * 500;

View File

@@ -0,0 +1,27 @@
/*
* 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 { useUpload, UploadConfig } from './use-upload';
export {
getAccept,
getFileExtension,
getBase64,
getImageSize,
formatBytes,
} from './utils';
export { PREVIEW_IMAGE_TYPE } from './constant';
export { FileItem, FileItemStatus } from './types';

View File

@@ -0,0 +1,38 @@
/*
* 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 enum FileItemStatus {
Success = 'success',
UploadFail = 'uploadFail',
ValidateFail = 'validateFail',
Validating = 'validating',
Uploading = 'uploading',
Wait = 'wait',
}
export interface FileItem extends File {
// 唯一标识
uid?: string;
// 文件地址
url?: string;
// 上传进度
percent?: number;
// 校验信息
validateMessage?: string;
status?: FileItemStatus;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}

View File

@@ -0,0 +1,198 @@
/*
* 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 */
import { useState } from 'react';
import { nanoid } from 'nanoid';
import { workflowApi } from '@coze-workflow/base/api';
import { type ViewVariableType } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { upLoadFile } from '@coze-arch/bot-utils';
import { CustomError } from '@coze-arch/bot-error';
import { Toast } from '@coze-arch/coze-design';
import { validate } from './validate';
import { FileItemStatus, type FileItem } from './types';
import { MAX_IMAGE_SIZE, MAX_FILE_SIZE } from './constant';
export interface UploadConfig {
initialValue?: FileItem[];
customValidate?: (file: FileItem) => Promise<string | undefined>;
timeout?: number;
fileType?: 'object' | 'image';
multiple?: boolean;
maxSize?: number;
inputType?: ViewVariableType;
accept?: string;
maxFileCount?: number;
}
export const useUpload = (props?: UploadConfig) => {
const {
initialValue = [],
customValidate,
timeout,
fileType,
multiple = true,
maxSize,
accept,
maxFileCount = 20,
} = props || {};
const [fileList, setFileList] = useState(initialValue);
const isUploading = fileList.some(
file => file.status === FileItemStatus.Uploading,
);
const updateFileItemProps = (uid, fileItemProps) => {
setFileList(prevList => {
const newList = [...prevList];
const index = newList.findIndex(item => item.uid === uid);
if (index !== -1) {
Object.keys(fileItemProps).forEach(key => {
newList[index][key] = fileItemProps[key];
});
}
return newList;
});
};
const uploadFileWithProgress = async file => {
let progressTimer;
try {
const doUpload = async () =>
await upLoadFile({
biz: 'workflow',
fileType,
file,
getProgress: percent => {
updateFileItemProps(file.uid, {
percent,
});
},
});
if (timeout) {
progressTimer = setTimeout(() => {
throw new Error('Upload timed out');
}, timeout);
}
const uri = await doUpload();
if (!uri) {
throw new CustomError('normal_error', 'no uri');
}
// 上传完成,清空超时计时器
clearTimeout(progressTimer);
// 加签uri获得url
const { url } = await workflowApi.SignImageURL(
{
uri,
},
{
__disableErrorToast: true,
},
);
if (!url) {
throw new Error(I18n.t('imageflow_upload_error'));
}
updateFileItemProps(file.uid, {
url,
status: FileItemStatus.Success,
});
return url;
} catch (error) {
updateFileItemProps(file.uid, {
validateMessage: error.message || 'upload failed',
status: FileItemStatus.ValidateFail,
});
clearTimeout(progressTimer);
}
};
const validateFile = async (file: FileItem): Promise<string | undefined> => {
const validateMsg = await validate(file, {
customValidate,
maxSize:
(maxSize ?? fileType === 'image') ? MAX_IMAGE_SIZE : MAX_FILE_SIZE,
accept,
});
if (validateMsg) {
return validateMsg;
}
};
const upload = async (file: FileItem) => {
file.status = FileItemStatus.Uploading;
if (!file.uid) {
file.uid = nanoid();
}
const errorInfo = await validateFile(file);
if (errorInfo) {
Toast.error(errorInfo);
return;
}
if (!multiple && fileList[0]) {
setFileList([]);
}
let canUpload = true;
setFileList(prevList => {
if (prevList.length >= maxFileCount) {
Toast.warning(I18n.t('plugin_file_max'));
canUpload = false;
return prevList;
}
return [...prevList, file];
});
if (canUpload) {
await uploadFileWithProgress(file);
}
};
const deleteFile = (uid?: string) => {
const index = fileList.findIndex(item => uid === item.uid);
if (index !== -1 && uid) {
setFileList(prevList => {
const newList = [...prevList];
newList.splice(index, 1);
return newList;
});
}
};
return {
fileList,
upload,
isUploading,
deleteFile,
setFileList: _fileList => setFileList(_fileList),
};
};

View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
/**
* 格式化文件大小
* @param bytes 文件大小
* @param decimals 小数位数, 默认 2 位
* @example
* formatBytes(1024); // 1KB
* formatBytes('1024'); // 1KB
* formatBytes(1234); // 1.21KB
* formatBytes(1234, 3); // 1.205KB
*/
export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024,
dm = decimals,
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}`;
}

View File

@@ -0,0 +1,79 @@
/*
* 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 { ViewVariableType } from '@coze-workflow/base';
export const ACCEPT_MAP = {
// [ViewVariableType.File]: ['*'],
[ViewVariableType.Image]: ['image/*'],
[ViewVariableType.Doc]: ['.docx', '.doc', '.pdf'],
[ViewVariableType.Audio]: [
'.mp3',
'.wav',
'.aac',
'.flac',
'.ogg',
'.wma',
'.alac',
'.mid',
'.midi',
'.ac3',
'.dsd',
],
[ViewVariableType.Excel]: ['.xls', '.xlsx', '.csv'],
[ViewVariableType.Video]: ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'],
[ViewVariableType.Zip]: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
[ViewVariableType.Code]: ['.py', '.java', '.c', '.cpp', '.js', '.css'],
[ViewVariableType.Txt]: ['.txt'],
[ViewVariableType.Ppt]: ['.ppt', '.pptx'],
[ViewVariableType.Svg]: ['.svg'],
};
export const getAccept = (
inputType: ViewVariableType,
availableFileTypes?: ViewVariableType[],
) => {
let accept: string;
const itemType = ViewVariableType.isArrayType(inputType)
? ViewVariableType.getArraySubType(inputType)
: inputType;
if (itemType === ViewVariableType.File) {
if (availableFileTypes?.length) {
accept = availableFileTypes
.map(type => ACCEPT_MAP[type]?.join(','))
.join(',');
} else {
accept = Object.values(ACCEPT_MAP)
.map(items => items.join(','))
.join(',');
}
} else {
accept = (ACCEPT_MAP[itemType] || []).join(',');
}
return accept;
};

View File

@@ -0,0 +1,35 @@
/*
* 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 { REPORT_EVENTS } from '@coze-arch/report-events';
import { CustomError } from '@coze-arch/bot-error';
export function getBase64(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = event => {
const result = event.target?.result;
if (!result || typeof result !== 'string') {
reject(
new CustomError(REPORT_EVENTS.parmasValidation, 'file read fail'),
);
return;
}
resolve(result.replace(/^.*?,/, ''));
};
fileReader.readAsDataURL(file);
});
}

View File

@@ -0,0 +1,24 @@
/*
* 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 function getFileExtension(name?: string) {
if (!name) {
return '';
}
const index = name.lastIndexOf('.');
return name.slice(index + 1).toLowerCase();
}

View File

@@ -0,0 +1,40 @@
/*
* 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 FileItem } from '../types';
/**
* 获取图片的宽高
*/
export async function getImageSize(
file: FileItem,
): Promise<{ width: number; height: number }> {
const url = URL.createObjectURL(file);
return new Promise((resolve, reject) => {
const img = new window.Image();
img.onload = () => {
resolve({
width: img.naturalWidth,
height: img.naturalHeight,
});
};
img.onerror = e => {
reject(e);
};
img.src = url;
});
}

View File

@@ -0,0 +1,21 @@
/*
* 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 { formatBytes } from './format-bytes';
export { getImageSize } from './get-image-size';
export { getBase64 } from './get-base-64';
export { getAccept, ACCEPT_MAP } from './get-accept';
export { getFileExtension } from './get-file-extension';

View File

@@ -0,0 +1,41 @@
/*
* 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 mime from 'mime-types';
import { I18n } from '@coze-arch/i18n';
import { getFileExtension } from '../utils';
export const acceptValidate = (fileName: string, accept?: string) => {
if (!accept) {
return;
}
const acceptList = accept.split(',');
const fileExtension = getFileExtension(fileName);
const mimeType = mime.lookup(fileExtension);
// image/* 匹配所有的图片类型
if (acceptList.includes('image/*') && mimeType?.startsWith?.('image/')) {
return undefined;
}
if (!acceptList.includes(`.${fileExtension}`)) {
return I18n.t('imageflow_upload_error_type', {
type: `${acceptList.filter(Boolean).join('/')}`,
});
}
};

View File

@@ -0,0 +1,72 @@
/*
* 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 { isNil } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { getImageSize } from '../utils/get-image-size';
import { type FileItem } from '../types';
export interface ImageSizeRule {
maxWidth?: number;
minWidth?: number;
maxHeight?: number;
minHeight?: number;
aspectRatio?: number;
}
/** 图像宽高校验 */
// eslint-disable-next-line complexity
export const imageSizeValidate = async (
file: FileItem,
rule?: ImageSizeRule,
): Promise<string | undefined> => {
const { maxWidth, minWidth, maxHeight, minHeight, aspectRatio } = rule || {};
// 未定义时不校验
if (isNil(maxWidth || minWidth || maxHeight || minHeight || aspectRatio)) {
return;
}
const { width, height } = await getImageSize(file);
if (maxWidth && width > maxWidth) {
return I18n.t('imageflow_upload_error5', {
value: `${maxWidth}px`,
});
}
if (minWidth && width < minWidth) {
return I18n.t('imageflow_upload_error3', {
value: `${minWidth}px`,
});
}
if (maxHeight && height > maxHeight) {
return I18n.t('imageflow_upload_error4', {
value: `${maxHeight}px`,
});
}
if (minHeight && height < minHeight) {
return I18n.t('imageflow_upload_error2', {
value: `${minHeight}px`,
});
}
if (aspectRatio && width / height > aspectRatio) {
return I18n.t('imageflow_upload_error1');
}
};

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 FileItem } from '../types';
import { sizeValidate } from './size-validate';
import { imageSizeValidate, type ImageSizeRule } from './image-size-validate';
import { acceptValidate } from './accept-validate';
interface UploadValidateRule {
maxSize?: number;
imageSize?: ImageSizeRule;
accept?: string;
customValidate?: (file: FileItem) => Promise<string | undefined>;
}
export const validate = async (file: FileItem, rules?: UploadValidateRule) => {
const { size, name } = file;
const { maxSize, imageSize, accept, customValidate } = rules || {};
const validators = [
async () => await customValidate?.(file),
() => sizeValidate(size, maxSize),
async () => await imageSizeValidate(file, imageSize),
() => acceptValidate(name, accept),
];
for await (const validator of validators) {
const errorMsg = await validator();
if (errorMsg) {
return errorMsg;
}
}
};

View File

@@ -0,0 +1,33 @@
/*
* 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 { I18n } from '@coze-arch/i18n';
import { formatBytes } from '../utils/format-bytes';
const DEFAULT_MAX_SIZE = 1024 * 1024 * 20;
/** 文件大小校验 */
export const sizeValidate = (
size: number,
maxSize: number = DEFAULT_MAX_SIZE,
): string | undefined => {
if (maxSize && size > maxSize) {
return I18n.t('imageflow_upload_exceed', {
size: formatBytes(maxSize),
});
}
};

View File

@@ -0,0 +1,115 @@
/*
* 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 { useCallback, useEffect } from 'react';
import { debounce } from 'lodash-es';
import { useMemoizedFn } from 'ahooks';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
import { DisposableCollection } from '@flowgram-adapter/common';
import { GlobalVariableService } from '@coze-workflow/variable';
import { useValidationService } from '@coze-workflow/base/services';
import { useLineService, useGlobalState } from '@/hooks';
export const useValidateWorkflow = () => {
const lineService = useLineService();
const validationService = useValidationService();
const globalState = useGlobalState();
const feValidate = useCallback(async () => {
const { hasError, nodeErrorMap: feErrorMap } =
await validationService.validateWorkflow();
if (hasError && feErrorMap) {
validationService.setErrorsV2({
[globalState.workflowId]: {
workflowId: globalState.workflowId,
errors: feErrorMap,
},
});
}
return hasError;
}, [validationService, globalState]);
const beValidate = useCallback(async () => {
const { hasError, errors } = await validationService.validateSchemaV2();
if (hasError) {
validationService.setErrorsV2(errors);
}
return hasError;
}, [validationService]);
const validate = useCallback(async () => {
validationService.validating = true;
try {
const feHasError = await feValidate();
if (feHasError) {
return feHasError;
}
const beHasError = await beValidate();
if (!feHasError && !beHasError) {
validationService.clearErrors();
}
lineService.validateAllLine();
return beHasError;
} finally {
validationService.validating = false;
}
}, [feValidate, beValidate, validationService, lineService]);
return { validate };
};
/**
* 校验的触发频率
*/
const DEBOUNCE_TIME = 2000;
export const useWatchValidateWorkflow = () => {
const { isInIDE } = useGlobalState();
const { validate } = useValidateWorkflow();
const workflowDocument = useService<WorkflowDocument>(WorkflowDocument);
const debounceValidate = useMemoizedFn(debounce(validate, DEBOUNCE_TIME));
const globalVariableService = useService<GlobalVariableService>(
GlobalVariableService,
);
useEffect(() => {
const globalVariableDispose = new DisposableCollection();
globalVariableDispose.push(
globalVariableService.onLoaded(() => {
if (!isInIDE) {
debounceValidate();
}
}),
);
const contentChangeDispose = workflowDocument.onContentChange(() => {
debounceValidate();
});
return () => {
contentChangeDispose.dispose();
globalVariableDispose.dispose();
};
}, [workflowDocument, isInIDE]);
};

View File

@@ -0,0 +1,26 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { ValueExpressionService } from '@/services/value-expression-service';
export const useValueExpressionService = () => {
const valueExpressionService = useService<ValueExpressionService>(
ValueExpressionService,
);
return valueExpressionService;
};

View File

@@ -0,0 +1,26 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowVariableService } from '@coze-workflow/variable';
export function useVariableService() {
const variableService = useService<WorkflowVariableService>(
WorkflowVariableService,
);
return variableService;
}

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 { useState, useEffect } from 'react';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { type MessageBizType } from '@coze-workflow/base';
import type { Model } from '@coze-arch/bot-api/developer_api';
import { bizTypeToDependencyTypeMap } from '@/services/workflow-dependency-service';
import { DependencySourceType } from '@/constants';
import { WorkflowModelsService } from '../services';
import { useDependencyService } from './use-dependency-service';
/**
* 统一获取模型数据入口,监听到模型资源变化时,更新模型数据
*/
export const useWorkflowModels = () => {
const modelsService = useService<WorkflowModelsService>(
WorkflowModelsService,
);
const dependencyService = useDependencyService();
const [models, setModels] = useState<Model[]>(
modelsService?.getModels() ?? [],
);
useEffect(() => {
const disposable = dependencyService.onDependencyChange(source => {
if (
bizTypeToDependencyTypeMap[source?.bizType as MessageBizType] ===
DependencySourceType.LLM
) {
const curModels = modelsService?.getModels() ?? [];
setModels(curModels);
}
});
return () => {
disposable?.dispose?.();
};
}, []);
return { models };
};

View File

@@ -0,0 +1,27 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowOperationService } from '../services';
export const useWorkflowOperation = () => {
const operation = useService<WorkflowOperationService>(
WorkflowOperationService,
);
return operation;
};

View File

@@ -0,0 +1,131 @@
/*
* 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 { useCallback } from 'react';
import { createMinimapPlugin } from '@flowgram-adapter/free-layout-editor';
import { createHistoryNodePlugin } from '@flowgram-adapter/free-layout-editor';
import { createFreeSnapPlugin } from '@flowgram-adapter/free-layout-editor';
import { createFreeNodePanelPlugin } from '@flowgram-adapter/free-layout-editor';
import { createFreeLinesPlugin } from '@flowgram-adapter/free-layout-editor';
import { createContainerNodePlugin } from '@flowgram-adapter/free-layout-editor';
import {
EntityManager,
type PluginContext,
} from '@flowgram-adapter/free-layout-editor';
import { createWorkflowVariablePlugins } from '@coze-workflow/variable';
import { createTestRunPlugin } from '@coze-workflow/test-run';
import {
createFreeHistoryPlugin,
createOperationReportPlugin,
} from '@coze-workflow/history';
import { createWorkflowEncapsulatePlugin } from '@coze-workflow/feature-encapsulate';
import { type StandardNodeType } from '@coze-workflow/base';
import { WorkflowPlaygroundContext } from '@/workflow-playground-context';
import { type WorkflowPlaygroundProps } from '@/typing';
import { RelatedCaseDataService } from '@/services';
import { WorkflowGlobalStateEntity } from '@/entities';
import { NodePanel } from '../components/node-panel';
import { LineAddButton } from '../components/line-add-button';
const createEncapsulatePlugin = (props?: WorkflowPlaygroundProps) =>
createWorkflowEncapsulatePlugin({
getGlobalState: (ctx: PluginContext) =>
ctx
.get<EntityManager>(EntityManager)
.getEntity<WorkflowGlobalStateEntity>(
WorkflowGlobalStateEntity,
) as WorkflowGlobalStateEntity,
getNodeTemplate: (ctx: PluginContext) => (type: StandardNodeType) =>
ctx
.get<WorkflowPlaygroundContext>(WorkflowPlaygroundContext)
.getNodeTemplateInfoByType(type),
onEncapsulate: async (res, ctx) => {
if (!res.success) {
return;
}
if (res.projectId) {
// project中刷新流程列表并rename到新建的流程
await props?.refetchProjectResourceList?.();
await props?.renameProjectResource?.(res.workflowId);
} else {
// 资源库中更新生成流程绑定的Bot信息
const relatedCaseDataService = ctx.get<RelatedCaseDataService>(
RelatedCaseDataService,
);
relatedCaseDataService.updateRelatedBot(
relatedCaseDataService.getRelatedBotValue(),
res.workflowId,
);
}
},
});
export const useWorkflowPreset = (props?: WorkflowPlaygroundProps) => {
const preset = useCallback(
() => [
createFreeLinesPlugin({
renderInsideLine: LineAddButton,
}),
...createWorkflowVariablePlugins(),
createFreeHistoryPlugin({
enable: true,
limit: 50,
}),
createOperationReportPlugin({}),
createMinimapPlugin({
disableLayer: true,
canvasStyle: {
canvasWidth: 182,
canvasHeight: 102,
canvasPadding: 50,
canvasBackground: 'rgba(245, 245, 245, 1)',
canvasBorderRadius: 10,
viewportBackground: 'rgba(235, 235, 235, 1)',
viewportBorderRadius: 4,
viewportBorderColor: 'rgba(201, 201, 201, 1)',
viewportBorderWidth: 1,
viewportBorderDashLength: 2,
nodeColor: 'rgba(255, 255, 255, 1)',
nodeBorderRadius: 2,
nodeBorderWidth: 0.145,
nodeBorderColor: 'rgba(6, 7, 9, 0.10)',
overlayColor: 'rgba(255, 255, 255, 0)',
},
inactiveDebounceTime: 1,
}),
createFreeSnapPlugin({
edgeColor: '#00B2B2',
alignColor: '#00B2B2',
edgeLineWidth: 1,
alignLineWidth: 1,
alignCrossWidth: 8,
}),
createFreeNodePanelPlugin({
renderer: NodePanel,
}),
createHistoryNodePlugin({}),
createContainerNodePlugin({}),
createTestRunPlugin({}),
createEncapsulatePlugin(props),
],
[],
);
return preset;
};

View File

@@ -0,0 +1,65 @@
/*
* 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.
*/
/**
* 获取当前 workflow 被别的哪些workflow或bots 引用了
* 目前bots没有数据只有workflow的
*/
import {
useQuery,
type RefetchOptions,
type QueryObserverResult,
} from '@tanstack/react-query';
import { type Workflow } from '@coze-workflow/base/api';
import { useWorkflowOperation } from './use-workflow-operation';
import { useGlobalState } from './use-global-state';
interface WorkflowReferences {
workflowList: Workflow[];
}
export const useWorkflowReferences = (): {
references: WorkflowReferences | undefined;
refetchReferences: (options?: RefetchOptions | undefined) => Promise<
QueryObserverResult<
{
workflowList: Workflow[];
},
Error
>
>;
} => {
const { spaceId, workflowId } = useGlobalState();
const operation = useWorkflowOperation();
const getWorkflowReferences = async () => {
const workflowList = await operation.getReference();
return { workflowList };
};
const { data, refetch } = useQuery({
queryKey: ['workflow_references', spaceId, workflowId],
queryFn: getWorkflowReferences,
});
return {
references: data,
refetchReferences: refetch,
};
};

View File

@@ -0,0 +1,22 @@
/*
* 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 { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowRunService } from '../services';
export const useWorkflowRunService = () =>
useService<WorkflowRunService>(WorkflowRunService);