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,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.
*/
const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(
navigator.userAgent,
);
const CTRL = isMacOS ? '⌘' : 'Ctrl';
const SHIFT = isMacOS ? '⇧' : 'Shift';
export const ENCAPSULATE_SHORTCUTS = {
encapsulate: `${CTRL} G`,
decapsulate: `${CTRL} ${SHIFT} G`,
};

View File

@@ -0,0 +1,103 @@
/*
* 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 MouseEvent, useRef, type MouseEventHandler } from 'react';
import { I18n } from '@coze-arch/i18n';
import { Button } from '@coze-arch/coze-design';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { useValidate } from '../hooks/use-validate';
import { useEncapsulate } from '../hooks/use-encapsulate';
import { EncapsulateTooltip } from '../encapsulate-tooltip';
import { EncapsulateRenderService } from '../encapsulate-render-service';
import { ENCAPSULATE_SHORTCUTS } from '../constants';
import styles from './styles.module.less';
const HOVER_DELAY = 200;
/**
* 封装按钮
*/
export function EncapsulateButton() {
const encapsulateRenderService = useService<EncapsulateRenderService>(
EncapsulateRenderService,
);
const { handleEncapsulate, loading } = useEncapsulate();
const { validating, errors } = useValidate();
const handleClick = (e: MouseEvent) => {
e.stopPropagation();
if (validating) {
return;
}
handleEncapsulate();
};
const hasError = errors && errors.length > 0;
const disabled = !!(!errors || hasError);
const timeOutRef = useRef<number>();
const ref = useRef<HTMLDivElement>(null);
const handleMouseLeave: MouseEventHandler<HTMLDivElement> = () => {
if (!timeOutRef.current) {
timeOutRef.current = window.setTimeout(() => {
encapsulateRenderService.hideTooltip();
timeOutRef.current = undefined;
}, HOVER_DELAY);
}
};
const handleMouseEnter: MouseEventHandler<HTMLDivElement> = () => {
if (timeOutRef.current) {
clearTimeout(timeOutRef.current);
timeOutRef.current = undefined;
}
encapsulateRenderService.showTooltip();
};
return (
<EncapsulateTooltip
errors={errors}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div
className="pointer-events-auto"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={ref}
>
<Button
loading={loading}
disabled={disabled}
className={styles.button}
color="highlight"
onMouseDown={handleClick}
>
<span>
{I18n.t('workflow_encapsulate_button', undefined, '封装工作流')}
</span>
<span className={styles.shortcut}>
{ENCAPSULATE_SHORTCUTS.encapsulate}
</span>
</Button>
</div>
</EncapsulateTooltip>
);
}

View File

@@ -0,0 +1,9 @@
.button {
height: 32px;
}
.shortcut {
// color: rgba(255, 255, 255, 0.6);
margin-left: 4px;
font-family: 'PICO Sans VFE SC, PingFang SC,Noto Sans SC,sans-serif';
}

View File

@@ -0,0 +1,43 @@
@keyframes encapsulate-panel-show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.encapsulate-panel {
position: absolute;
width: 100%;
top: 16px;
justify-content: center;
align-items: center;
pointer-events: none;
transition: opacity 0.35s ease;
display: flex;
opacity: 0;
&-show {
opacity: 1;
}
.encapsulate-panel-content {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
padding: 0 4px 0 8px;
background-color: var(--coz-bg-max);
border: 1px solid var(--coz-stroke-plus);
border-radius: 10px;
color: var(--coz-fg-primary);
font-weight: 500;
box-shadow: var(--coz-shadow-small);
gap: 16px;
font-size: 14px;
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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, type FC } from 'react';
import classNames from 'classnames';
import { I18n } from '@coze-arch/i18n';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { useSelectedNodes } from '../hooks/use-selected-nodes';
import { EncapsulateRenderService } from '../encapsulate-render-service';
import { EncapsulateButton } from '../encapsulate-button';
import { EncapsulateService } from '../../encapsulate';
import styles from './index.module.less';
export const EncapsulatePanel: FC = () => {
const { selectedNodes } = useSelectedNodes();
const { length } = selectedNodes || [];
const [show, setShow] = useState(false);
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
const encapsulateRenderService = useService<EncapsulateRenderService>(
EncapsulateRenderService,
);
useEffect(() => {
const display = encapsulateService.canEncapsulate() && length > 1;
if (!display) {
encapsulateRenderService.hideTooltip();
}
setShow(display);
}, [length]);
return (
<div
className={classNames(styles['encapsulate-panel'], {
[styles['encapsulate-panel-show']]: show,
})}
>
<div className={styles['encapsulate-panel-content']}>
{I18n.t(
'workflow_encapsulate_selecet',
{ length },
`已选中 ${length} 个节点`,
)}{' '}
<EncapsulateButton />
</div>
</div>
);
};

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 { ContainerModule } from 'inversify';
import { bindContributions } from '@flowgram-adapter/free-layout-editor';
import { WorkflowShortcutsContribution } from '@coze-workflow/render';
import { EncapsulateShortcutsContribution } from './encapsulate-shortcuts-contribution';
import { EncapsulateRenderService } from './encapsulate-render-service';
export const EncapsulateRenderContainerModule = new ContainerModule(bind => {
bindContributions(bind, EncapsulateShortcutsContribution, [
WorkflowShortcutsContribution,
]);
bind(EncapsulateRenderService).toSelf().inSingletonScope();
});

View File

@@ -0,0 +1,76 @@
/*
* 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 { injectable } from 'inversify';
import { Emitter } from '@flowgram-adapter/common';
@injectable()
export class EncapsulateRenderService {
private isModalVisible = false;
private onModalVisibleChangeEmitter = new Emitter<boolean>();
readonly onModalVisibleChange = this.onModalVisibleChangeEmitter.event;
private isTooltipVisible = false;
private onTooltipVisibleChangeEmitter = new Emitter<boolean>();
readonly onTooltipVisibleChange = this.onTooltipVisibleChangeEmitter.event;
private isLoading = false;
private onLoadingChangeEmitter = new Emitter<boolean>();
readonly onLoadingChange = this.onLoadingChangeEmitter.event;
get modalVisible() {
return this.isModalVisible;
}
get tooltipVisible() {
return this.isTooltipVisible;
}
get loading() {
return this.isLoading;
}
setLoading(value: boolean) {
this.isLoading = value;
this.onLoadingChangeEmitter.fire(value);
}
openModal() {
this.setModalVisible(true);
}
closeModal() {
this.setModalVisible(false);
}
showTooltip() {
this.setTooltipVisible(true);
}
hideTooltip() {
this.setTooltipVisible(false);
}
setTooltipVisible(value: boolean) {
this.isTooltipVisible = value;
this.onTooltipVisibleChangeEmitter.fire(value);
}
private setModalVisible(value: boolean) {
this.isModalVisible = value;
this.onModalVisibleChangeEmitter.fire(value);
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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 { inject, injectable } from 'inversify';
import { PlaygroundConfigEntity } from '@flowgram-adapter/free-layout-editor';
import {
type WorkflowNodeEntity,
WorkflowSelectService,
} from '@flowgram-adapter/free-layout-editor';
import {
type WorkflowShortcutsContribution,
type WorkflowShortcutsRegistry,
} from '@coze-workflow/render';
import { EncapsulateService } from '../encapsulate';
import { EncapsulateCommands } from './types';
import { EncapsulateRenderService } from './encapsulate-render-service';
/**
* 封装 快捷键
*/
@injectable()
export class EncapsulateShortcutsContribution
implements WorkflowShortcutsContribution
{
@inject(PlaygroundConfigEntity)
private playgroundConfigEntity: PlaygroundConfigEntity;
@inject(EncapsulateService)
private encapsulateService: EncapsulateService;
@inject(EncapsulateRenderService)
private encapsulateRenderService: EncapsulateRenderService;
@inject(WorkflowSelectService)
private workflowSelectService: WorkflowSelectService;
registerShortcuts(registry: WorkflowShortcutsRegistry): void {
registry.addHandlers(
/**
* 封装
*/
{
commandId: EncapsulateCommands.ENCAPSULATE,
shortcuts: ['meta g', 'ctrl g'],
isEnabled: () => !this.playgroundConfigEntity.readonly,
execute: async () => {
if (!this.encapsulateService.canEncapsulate()) {
return;
}
const res = await this.encapsulateService.validate();
if (res.hasError()) {
this.encapsulateRenderService.showTooltip();
return;
}
this.encapsulateRenderService.setLoading(true);
try {
await this.encapsulateService.encapsulate();
this.encapsulateRenderService.closeModal();
} catch (e) {
console.error(e);
}
this.encapsulateRenderService.setLoading(false);
},
},
/**
* 解封
*/
{
commandId: EncapsulateCommands.DECAPSULATE,
shortcuts: ['meta shift g', 'ctrl shift g'],
isEnabled: () => !this.playgroundConfigEntity.readonly,
execute: () => {
const { selectedNodes } = this.workflowSelectService;
if (selectedNodes.length !== 1) {
return;
}
const node = selectedNodes[0] as WorkflowNodeEntity;
if (!this.encapsulateService.canDecapsulate(node)) {
return;
}
this.encapsulateService.decapsulate(node);
},
},
);
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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 FC } from 'react';
import classNames from 'classnames';
import { Typography } from '@coze-arch/coze-design';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import {
usePlayground,
useService,
WorkflowSelectService,
} from '@flowgram-adapter/free-layout-editor';
import { type EncapsulateValidateError } from '../../validate';
import styles from './index.module.less';
interface Props {
error: EncapsulateValidateError;
}
export const ErrorTitle: FC<Props> = ({ error }) => {
const selectServices = useService<WorkflowSelectService>(
WorkflowSelectService,
);
const playground = usePlayground();
if (!error?.sourceName && !error.sourceIcon) {
return <div></div>;
}
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 (
<div
className="flex items-center gap-1 cursor-pointer max-w-[120px]"
onClick={() => {
if (error.source) {
scrollToNode(error.source);
}
}}
>
{error.sourceIcon ? (
<img
width={18}
height={18}
src={error.sourceIcon}
className="w-4.5 h-4.5 rounded-[4px]"
/>
) : null}
{error.sourceName ? (
<Typography.Paragraph
className={classNames(
'font-medium coz-fg-primary',
styles['error-name'],
)}
ellipsis={{
rows: 1,
showTooltip: {
type: 'tooltip',
opts: {
style: {
width: '100%',
wordBreak: 'break-word',
},
},
},
}}
>
{error.sourceName}
</Typography.Paragraph>
) : null}
</div>
);
};

View File

@@ -0,0 +1,20 @@
.error-name {
span {
font-weight: 500;
}
}
.tooltip {
max-width: 460px;
max-height: 260px;
overflow-y: auto;
padding: 8px 12px;
}
.errors {
display: grid;
grid-template-columns: minmax(0, max-content) 1fr;
grid-gap: 16px;
grid-row-gap: 12px;
margin-top: 12px;
}

View File

@@ -0,0 +1,137 @@
/*
* 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,
type PropsWithChildren,
type FC,
type MouseEventHandler,
useMemo,
} from 'react';
import { groupBy } from 'lodash-es';
import { I18n } from '@coze-arch/i18n';
import { Tooltip } from '@coze-arch/coze-design';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { EncapsulateRenderService } from '../encapsulate-render-service';
import { type EncapsulateValidateError } from '../../validate';
import { ErrorTitle } from './error-title';
import styles from './index.module.less';
interface Props {
errors: EncapsulateValidateError[];
onMouseEnter?: MouseEventHandler<HTMLDivElement>;
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
getPopupContainer?: () => HTMLElement;
}
const ErrorMessage: FC<{
error: EncapsulateValidateError;
}> = ({ error }) => (
<div className="flex-1 coz-fg-primary font-normal">{error.message}</div>
);
export const EncapsulateTooltip: FC<PropsWithChildren<Props>> = ({
errors = [],
onMouseEnter,
onMouseLeave,
children,
}) => {
const encapsulateRenderService = useService<EncapsulateRenderService>(
EncapsulateRenderService,
);
const [tooltipVisible, setTooltipVisible] = useState(
encapsulateRenderService.tooltipVisible,
);
useEffect(() => {
const disposable =
encapsulateRenderService.onTooltipVisibleChange(setTooltipVisible);
return () => {
disposable.dispose();
};
}, []);
const hasError = errors.length;
const groupErrors = useMemo(
() =>
groupBy(
errors.filter(e => e.message),
error =>
error?.sourceName || error?.sourceIcon
? 'withSource'
: 'withoutSource',
),
[errors],
);
return (
<Tooltip
trigger="custom"
position="bottom"
visible={tooltipVisible && errors?.length > 0}
showArrow={false}
onClickOutSide={() => {
setTooltipVisible(false);
}}
className="p-0 max-w-[460px] overflow-hidden"
content={
hasError ? (
<div
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={styles.tooltip}
>
<div className="coz-fg-plus font-medium text-[16px]">
{I18n.t(
'workflow_encapsulate_button_unable',
undefined,
'无法封装工作流',
)}
</div>
{/* 没有错误来源的 */}
{(groupErrors.withoutSource || []).map((error, index) => (
<div key={index} className="flex mt-3 gap-4 items-start">
<ErrorMessage error={error} />
</div>
))}
{/* 有错误来源的 */}
{(groupErrors.withSource || []).length ? (
<div className={styles.errors}>
{(groupErrors.withSource || []).map(error => (
<>
<ErrorTitle error={error} />
<ErrorMessage error={error} />
</>
))}
</div>
) : null}
</div>
) : null
}
>
{children}
</Tooltip>
);
};

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 { useService } from '@flowgram-adapter/free-layout-editor';
import { EncapsulateRenderService } from '../encapsulate-render-service';
import { EncapsulateService } from '../../encapsulate';
export const useEncapsulate = () => {
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
const encapsulateRenderService = useService<EncapsulateRenderService>(
EncapsulateRenderService,
);
const [loading, setLoading] = useState(encapsulateRenderService.loading);
useEffect(() => {
const disposable = encapsulateRenderService.onLoadingChange(setLoading);
return () => {
disposable.dispose();
};
}, []);
const handleEncapsulate = async () => {
encapsulateRenderService.setLoading(true);
try {
await encapsulateService.encapsulate();
encapsulateRenderService.closeModal();
} catch (e) {
console.error(e);
}
encapsulateRenderService.setLoading(false);
};
return {
handleEncapsulate,
loading,
};
};

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 { useEffect, useState } from 'react';
import { useService } from '@flowgram-adapter/free-layout-editor';
import { WorkflowSelectService } from '@flowgram-adapter/free-layout-editor';
/**
* 选中节点
*/
export function useSelectedNodes() {
const selectService = useService<WorkflowSelectService>(
WorkflowSelectService,
);
const [selectedNodes, setSelectedNodes] = useState(
selectService.selectedNodes,
);
useEffect(() => {
const disposable = selectService.onSelectionChanged(() => {
setSelectedNodes(selectService.selectedNodes);
});
return () => {
disposable.dispose();
};
});
return {
selectedNodes,
};
}

View File

@@ -0,0 +1,87 @@
/*
* 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, useRef } from 'react';
import { useDebounceEffect } from 'ahooks';
import { useService } from '@flowgram-adapter/free-layout-editor';
import {
EncapsulateValidateErrorCode,
type EncapsulateValidateError,
} from '../../validate';
import { EncapsulateService } from '../../encapsulate';
import { useVariableChange } from './use-variable-change';
import { useSelectedNodes } from './use-selected-nodes';
const DEBOUNCE_DELAY = 100;
/**
* 校验
*/
export function useValidate() {
const { selectedNodes } = useSelectedNodes();
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
const [validating, setValidating] = useState(false);
const [errors, setErrors] = useState<EncapsulateValidateError[]>([]);
const validationIdRef = useRef(0); // 新增校验ID跟踪
const handleValidate = async () => {
if (selectedNodes.length <= 1) {
return;
}
setValidating(true);
// 生成当前校验ID
const currentValidationId = ++validationIdRef.current;
try {
const validateResult = await encapsulateService.validate();
// 只处理最后一次校验结果
if (currentValidationId === validationIdRef.current) {
setErrors(validateResult.getErrors());
setValidating(false);
}
} catch (error) {
setErrors([
{
code: EncapsulateValidateErrorCode.VALIDATE_ERROR,
message: (error as Error).message,
},
]);
setValidating(false);
}
};
const { version: variableVersion } = useVariableChange(selectedNodes);
useDebounceEffect(
() => {
handleValidate();
},
[selectedNodes, variableVersion],
{
wait: DEBOUNCE_DELAY,
},
);
return {
validating,
errors,
};
}

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 { useEffect, useState } from 'react';
import { FlowNodeVariableData } from '@coze-workflow/variable';
export const useVariableChange = nodes => {
const [version, setVersion] = useState(0);
useEffect(() => {
const disposables = nodes
.filter(node => node.getData(FlowNodeVariableData)?.public?.available)
.map(node =>
node.getData(FlowNodeVariableData).public.available.onDataChange(() => {
setVersion(version + 1);
}),
);
return () => {
disposables.forEach(disposable => disposable?.dispose());
};
}, [nodes, version]);
return {
version,
};
};

View File

@@ -0,0 +1,20 @@
/*
* 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 * from './types';
export * from './encapsulate-render-container-module';
export { EncapsulatePanel } from './encapsulate-panel';
export * from './constants';

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.
*/
/**
* 封装/解封命令
*/
export enum EncapsulateCommands {
/**
* 封装
*/
ENCAPSULATE = 'ENCAPSULATE',
/**
* 解封
*/
DECAPSULATE = 'DECAPSULATE',
}