feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 WORKFLOW_CLIPBOARD_TYPE = 'coze-workflow-clipboard-data';
|
||||
export const WORKFLOW_EXPORT_TYPE = 'coze-workflow-export-data';
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
|
||||
import type {
|
||||
WorkflowNodeEntity,
|
||||
WorkflowNodeMeta,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
/** 获取可用节点 */
|
||||
export const getValidNodes = (
|
||||
nodes: WorkflowNodeEntity[],
|
||||
): WorkflowNodeEntity[] =>
|
||||
nodes.filter(n => {
|
||||
if (
|
||||
[
|
||||
StandardNodeType.Start,
|
||||
StandardNodeType.End,
|
||||
FlowNodeBaseType.SUB_CANVAS,
|
||||
].includes(n.flowNodeType as StandardNodeType)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (n.getNodeMeta<WorkflowNodeMeta>().copyDisable) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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 { TransformData } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowCommands,
|
||||
WorkflowDocument,
|
||||
type WorkflowEdgeJSON,
|
||||
type WorkflowLineEntity,
|
||||
WorkflowNodeEntity,
|
||||
WorkflowNodeLinesData,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { Rectangle } from '@flowgram-adapter/common';
|
||||
import type {
|
||||
WorkflowShortcutsContribution,
|
||||
WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
import { WorkflowNodeData } from '@coze-workflow/nodes';
|
||||
import type { StandardNodeType } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { WorkflowGlobalStateEntity } from '@/typing';
|
||||
|
||||
import { safeFn } from '../../utils';
|
||||
import type {
|
||||
WorkflowClipboardData,
|
||||
WorkflowClipboardJSON,
|
||||
WorkflowClipboardNodeTemporary,
|
||||
WorkflowClipboardNodeJSON,
|
||||
WorkflowClipboardRect,
|
||||
WorkflowClipboardSource,
|
||||
} from '../../type';
|
||||
import { WORKFLOW_CLIPBOARD_TYPE } from '../../constant';
|
||||
import { isValid } from './is-valid';
|
||||
import { hasSystemNodes } from './is-system-nodes';
|
||||
import { getValidNodes } from './get-valid-nodes';
|
||||
|
||||
/**
|
||||
* 复制快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowCopyShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
@inject(WorkflowDocument) private document: WorkflowDocument;
|
||||
@inject(WorkflowSelectService) private selection: WorkflowSelectService;
|
||||
@inject(WorkflowGlobalStateEntity)
|
||||
private globalState: WorkflowGlobalStateEntity;
|
||||
/** 注册快捷键 */
|
||||
public registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowCommands.COPY_NODES,
|
||||
shortcuts: ['meta c', 'ctrl c'],
|
||||
isEnabled: () => !this.globalState.readonly,
|
||||
execute: safeFn(this.handle.bind(this)),
|
||||
});
|
||||
}
|
||||
public async toData(): Promise<WorkflowClipboardData> {
|
||||
const validNodes = getValidNodes(this.selectedNodes);
|
||||
const source = this.toSource();
|
||||
const json = await this.toJSON(validNodes);
|
||||
const bounds = this.getEntireBounds(validNodes);
|
||||
return {
|
||||
type: WORKFLOW_CLIPBOARD_TYPE,
|
||||
source,
|
||||
json,
|
||||
bounds,
|
||||
};
|
||||
}
|
||||
/** 获取来源数据 */
|
||||
public toSource(): WorkflowClipboardSource {
|
||||
return {
|
||||
workflowId: this.globalState.workflowId,
|
||||
flowMode: this.globalState.flowMode,
|
||||
spaceId: this.globalState.spaceId,
|
||||
isDouyin: this.globalState.isBindDouyin,
|
||||
host: window.location.host,
|
||||
};
|
||||
}
|
||||
/** 获取节点的 JSON */
|
||||
public async toJSON(
|
||||
nodes: WorkflowNodeEntity[],
|
||||
): Promise<WorkflowClipboardJSON> {
|
||||
const nodeJSONs = await this.getNodeJSONs(nodes);
|
||||
const edgeJSONs = this.getEdgeJSONs(nodes);
|
||||
return {
|
||||
nodes: nodeJSONs,
|
||||
edges: edgeJSONs,
|
||||
};
|
||||
}
|
||||
/** 处理复制事件 */
|
||||
private async handle(): Promise<void> {
|
||||
if (await this.hasTextSelected()) {
|
||||
// 如果有选中的文字,优先复制文字
|
||||
return;
|
||||
}
|
||||
if (!isValid(this.selectedNodes)) {
|
||||
return;
|
||||
}
|
||||
const data = await this.toData();
|
||||
await this.write(data);
|
||||
}
|
||||
/** 写入剪贴板 */
|
||||
private async write(data: WorkflowClipboardData): Promise<void> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(data));
|
||||
if (hasSystemNodes(this.selectedNodes)) {
|
||||
Toast.warning({
|
||||
content: I18n.t('workflow_multi_choice_copy_partial_success'),
|
||||
showClose: false,
|
||||
});
|
||||
} else {
|
||||
Toast.success({
|
||||
content: I18n.t('workflow_multi_choice_copy_success'),
|
||||
showClose: false,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to write text: ', err);
|
||||
}
|
||||
}
|
||||
/** 是否有选中的文字 */
|
||||
private async hasTextSelected(): Promise<boolean> {
|
||||
if (!window.getSelection()?.toString()) {
|
||||
return false;
|
||||
}
|
||||
await navigator.clipboard.writeText(
|
||||
window.getSelection()?.toString() ?? '',
|
||||
);
|
||||
Toast.success({
|
||||
content: I18n.t('workflow_text_copy', {}, '文本已复制到剪贴板'),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/** 获取选中的节点 */
|
||||
private get selectedNodes(): WorkflowNodeEntity[] {
|
||||
return this.selection.selection.filter(
|
||||
n => n instanceof WorkflowNodeEntity,
|
||||
) as WorkflowNodeEntity[];
|
||||
}
|
||||
/** 获取节点的 JSON */
|
||||
private async getNodeJSONs(
|
||||
nodes: WorkflowNodeEntity[],
|
||||
): Promise<WorkflowClipboardNodeJSON[]> {
|
||||
const nodeJSONs = await Promise.all(
|
||||
nodes.map(node => this.tryToNodeJSON(node)),
|
||||
);
|
||||
return nodeJSONs.filter(Boolean) as WorkflowClipboardNodeJSON[];
|
||||
}
|
||||
/** 获取节点的 JSON */
|
||||
private async toNodeJSON(
|
||||
node: WorkflowNodeEntity,
|
||||
): Promise<WorkflowClipboardNodeJSON> {
|
||||
const nodeJSON = (await this.document.toNodeJSON(
|
||||
node,
|
||||
)) as WorkflowClipboardNodeJSON;
|
||||
nodeJSON._temp = this.getNodeTemporary(node);
|
||||
|
||||
// 递归处理所有嵌套层级的blocks
|
||||
if (nodeJSON.blocks?.length) {
|
||||
await Promise.all(
|
||||
nodeJSON.blocks.map(async childJSON => {
|
||||
const child = this.document.getNode(childJSON.id);
|
||||
if (!child) {
|
||||
return;
|
||||
}
|
||||
childJSON._temp = this.getNodeTemporary(child);
|
||||
|
||||
// 递归处理子节点的blocks
|
||||
if (childJSON.blocks?.length) {
|
||||
await this.processBlocksRecursively(childJSON.blocks);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
return nodeJSON;
|
||||
}
|
||||
/** 递归处理blocks */
|
||||
private async processBlocksRecursively(
|
||||
blocks: WorkflowClipboardNodeJSON[],
|
||||
): Promise<void> {
|
||||
await Promise.all(
|
||||
blocks.map(async blockJSON => {
|
||||
const node = this.document.getNode(blockJSON.id);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
blockJSON._temp = this.getNodeTemporary(node);
|
||||
|
||||
if (blockJSON.blocks?.length) {
|
||||
await this.processBlocksRecursively(blockJSON.blocks);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
private async tryToNodeJSON(
|
||||
node: WorkflowNodeEntity,
|
||||
): Promise<WorkflowClipboardNodeJSON | undefined> {
|
||||
try {
|
||||
return await this.toNodeJSON(node);
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch -- ignore error
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/** 获取节点的额外数据 */
|
||||
private getNodeTemporary(
|
||||
node: WorkflowNodeEntity,
|
||||
): WorkflowClipboardNodeTemporary {
|
||||
const bounds = this.getNodeBounds(node);
|
||||
const nodeDataEntity = node.getData<WorkflowNodeData>(WorkflowNodeData);
|
||||
const externalData = nodeDataEntity.getNodeData<StandardNodeType.Api>();
|
||||
return {
|
||||
bounds,
|
||||
externalData,
|
||||
};
|
||||
}
|
||||
/** 获取节点的矩形 */
|
||||
private getNodeBounds(node: WorkflowNodeEntity): WorkflowClipboardRect {
|
||||
const nodeData = node.getData<TransformData>(TransformData);
|
||||
return {
|
||||
x: nodeData.bounds.x,
|
||||
y: nodeData.bounds.y,
|
||||
width: nodeData.bounds.width,
|
||||
height: nodeData.bounds.height,
|
||||
};
|
||||
}
|
||||
/** 获取所有节点的边 */
|
||||
private getEdgeJSONs(nodes: WorkflowNodeEntity[]): WorkflowEdgeJSON[] {
|
||||
const lineSet = new Set<WorkflowLineEntity>();
|
||||
const nodeIdSet = new Set(nodes.map(n => n.id));
|
||||
nodes.forEach(node => {
|
||||
const linesData = node.getData(WorkflowNodeLinesData);
|
||||
const lines = [...linesData.inputLines, ...linesData.outputLines];
|
||||
lines.forEach(line => {
|
||||
if (
|
||||
nodeIdSet.has(line.from.id) &&
|
||||
line.to?.id &&
|
||||
nodeIdSet.has(line.to.id)
|
||||
) {
|
||||
lineSet.add(line);
|
||||
}
|
||||
});
|
||||
});
|
||||
return Array.from(lineSet).map(line => line.toJSON());
|
||||
}
|
||||
/** 获取所有节点的矩形 */
|
||||
private getEntireBounds(nodes: WorkflowNodeEntity[]): WorkflowClipboardRect {
|
||||
const bounds = nodes.map(
|
||||
node => node.getData<TransformData>(TransformData).bounds,
|
||||
);
|
||||
const rect = Rectangle.enlarge(bounds);
|
||||
return {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 type { WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
/** 是否有系统节点 */
|
||||
export const hasSystemNodes = (nodes: WorkflowNodeEntity[]): boolean =>
|
||||
nodes.some(n =>
|
||||
[StandardNodeType.Start, StandardNodeType.End].includes(
|
||||
n.flowNodeType as StandardNodeType,
|
||||
),
|
||||
);
|
||||
|
||||
export const isAllSystemNodes = (nodes: WorkflowNodeEntity[]): boolean =>
|
||||
nodes.every(n =>
|
||||
[StandardNodeType.Start, StandardNodeType.End].includes(
|
||||
n.flowNodeType as StandardNodeType,
|
||||
),
|
||||
);
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
import type { WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { isAllSystemNodes } from './is-system-nodes';
|
||||
import { getValidNodes } from './get-valid-nodes';
|
||||
|
||||
export const isValid = (nodes: WorkflowNodeEntity[]): boolean => {
|
||||
if (isAllSystemNodes(nodes)) {
|
||||
Toast.warning({
|
||||
content: I18n.t('workflow_multi_choice_copy_failed'),
|
||||
showClose: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const validNodes = getValidNodes(nodes);
|
||||
const nodeCount = validNodes.length;
|
||||
if (nodeCount === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 {
|
||||
WorkflowCommands,
|
||||
WorkflowDocument,
|
||||
WorkflowLineEntity,
|
||||
WorkflowNodeEntity,
|
||||
type WorkflowNodeMeta,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import type {
|
||||
WorkflowShortcutsContribution,
|
||||
WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
|
||||
import { WorkflowGlobalStateEntity } from '@/typing';
|
||||
|
||||
import { safeFn } from '../../utils';
|
||||
import { isValid } from './is-valid';
|
||||
|
||||
/**
|
||||
* 删除快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowDeleteShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
@inject(WorkflowDocument) private document: WorkflowDocument;
|
||||
@inject(WorkflowSelectService) private selection: WorkflowSelectService;
|
||||
@inject(WorkflowGlobalStateEntity)
|
||||
private globalState: WorkflowGlobalStateEntity;
|
||||
/** 注册快捷键 */
|
||||
public registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowCommands.DELETE_NODES,
|
||||
shortcuts: ['backspace', 'delete'],
|
||||
isEnabled: () => !this.globalState.readonly,
|
||||
execute: safeFn(this.handle.bind(this)),
|
||||
});
|
||||
}
|
||||
private handle(): void {
|
||||
if (!isValid(this.selectedNodes)) {
|
||||
return;
|
||||
}
|
||||
// 删除选中实体
|
||||
this.selection.selection.forEach(entity => {
|
||||
if (entity instanceof WorkflowNodeEntity) {
|
||||
this.removeNode(entity);
|
||||
} else if (entity instanceof WorkflowLineEntity) {
|
||||
this.removeLine(entity);
|
||||
} else {
|
||||
entity.dispose();
|
||||
}
|
||||
});
|
||||
// 过滤掉已删除的实体
|
||||
this.selection.selection = this.selection.selection.filter(
|
||||
s => !s.disposed,
|
||||
);
|
||||
}
|
||||
/** 获取选中的节点 */
|
||||
private get selectedNodes(): WorkflowNodeEntity[] {
|
||||
return this.selection.selection.filter(
|
||||
n => n instanceof WorkflowNodeEntity,
|
||||
) as WorkflowNodeEntity[];
|
||||
}
|
||||
/** 删除节点 */
|
||||
private removeNode(node: WorkflowNodeEntity): void {
|
||||
if (!this.document.canRemove(node)) {
|
||||
return;
|
||||
}
|
||||
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
|
||||
const subCanvas = nodeMeta.subCanvas?.(node);
|
||||
if (subCanvas?.isCanvas) {
|
||||
subCanvas.parentNode.dispose();
|
||||
return;
|
||||
}
|
||||
node.dispose();
|
||||
}
|
||||
/** 删除连线 */
|
||||
private removeLine(line: WorkflowLineEntity): void {
|
||||
if (!this.document.linesManager.canRemove(line)) {
|
||||
return;
|
||||
}
|
||||
line.dispose();
|
||||
}
|
||||
}
|
||||
@@ -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 { I18n } from '@coze-arch/i18n';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
import type { WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { hasSystemNodes, isAllSystemNodes } from '../copy/is-system-nodes';
|
||||
|
||||
export const isValid = (nodes: WorkflowNodeEntity[]): boolean => {
|
||||
if (isAllSystemNodes(nodes)) {
|
||||
Toast.warning({
|
||||
content: I18n.t('workflow_multi_choice_delete_failed'),
|
||||
showClose: false,
|
||||
});
|
||||
return false;
|
||||
} else if (hasSystemNodes(nodes)) {
|
||||
Toast.warning({
|
||||
content: I18n.t('workflow_multi_choice_delete_failed'),
|
||||
showClose: false,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowDocument,
|
||||
type WorkflowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import type {
|
||||
WorkflowShortcutsContribution,
|
||||
WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
|
||||
import { WorkflowGlobalStateEntity } from '@/typing';
|
||||
import { type WorkflowExportData } from '@/shortcuts/type';
|
||||
import { WORKFLOW_EXPORT_TYPE } from '@/shortcuts/constant';
|
||||
|
||||
import { WorkflowCopyShortcutsContribution } from '../copy';
|
||||
import { safeFn } from '../../utils';
|
||||
|
||||
/**
|
||||
* 导出快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowExportShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
public static readonly type = 'EXPORT';
|
||||
|
||||
@inject(WorkflowDocument)
|
||||
private document: WorkflowDocument;
|
||||
@inject(WorkflowGlobalStateEntity)
|
||||
private globalState: WorkflowGlobalStateEntity;
|
||||
@inject(WorkflowCopyShortcutsContribution)
|
||||
private copyShortcuts: WorkflowCopyShortcutsContribution;
|
||||
/** 注册快捷键 */
|
||||
public registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowExportShortcutsContribution.type,
|
||||
shortcuts: ['meta shift s', 'ctrl shift s'],
|
||||
execute: safeFn(this.handle.bind(this)),
|
||||
});
|
||||
}
|
||||
private async handle(): Promise<void> {
|
||||
const data = await this.toJSON();
|
||||
this.download({ data, filename: this.filename });
|
||||
}
|
||||
private async toJSON(): Promise<WorkflowExportData> {
|
||||
const source = this.copyShortcuts.toSource();
|
||||
const json = await this.copyShortcuts.toJSON(this.validNodes);
|
||||
const data: WorkflowExportData = {
|
||||
type: WORKFLOW_EXPORT_TYPE,
|
||||
source,
|
||||
json,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
private get validNodes(): WorkflowNodeEntity[] {
|
||||
return this.document.root.blocks.filter(
|
||||
b => b.flowNodeType !== FlowNodeBaseType.SUB_CANVAS,
|
||||
);
|
||||
}
|
||||
private get filename(): string {
|
||||
return `coze-workflow-${this.globalState.workflowId}.flow`;
|
||||
}
|
||||
private download(params: {
|
||||
data: WorkflowExportData;
|
||||
filename: string;
|
||||
}): void {
|
||||
const { data, filename } = params;
|
||||
// 创建 Blob 对象
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||
type: 'application/json',
|
||||
});
|
||||
|
||||
// 创建下载链接并触发下载
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 清理
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
@@ -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 { WorkflowPasteShortcutsContribution } from './paste';
|
||||
export { WorkflowLayoutShortcutsContribution } from './layout';
|
||||
export { WorkflowDeleteShortcutsContribution } from './delete';
|
||||
export { WorkflowCopyShortcutsContribution } from './copy';
|
||||
export { WorkflowZoomShortcutsContribution } from './zoom';
|
||||
export { WorkflowExportShortcutsContribution } from './export';
|
||||
export { WorkflowLoadShortcutsContribution } from './load';
|
||||
export { WorkflowSelectAllShortcutsContribution } from './select-all';
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 {
|
||||
LayoutNode,
|
||||
LayoutStore,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import { QuadTree } from './quad-tree';
|
||||
|
||||
export interface CommentContext {
|
||||
store: LayoutStore;
|
||||
quadTree: QuadTree;
|
||||
}
|
||||
|
||||
const getQuadTree = (context: CommentContext): QuadTree => {
|
||||
const nodes = context.store.nodes.filter(
|
||||
node =>
|
||||
![StandardNodeType.Comment, FlowNodeBaseType.SUB_CANVAS].includes(
|
||||
node.entity.flowNodeType as StandardNodeType | FlowNodeBaseType,
|
||||
),
|
||||
);
|
||||
context.quadTree = QuadTree.create(nodes);
|
||||
return context.quadTree;
|
||||
};
|
||||
|
||||
export const commentNodeHandler = (
|
||||
node: LayoutNode,
|
||||
context: CommentContext,
|
||||
) => {
|
||||
if (node.entity.flowNodeType !== StandardNodeType.Comment) {
|
||||
return;
|
||||
}
|
||||
const quadTree = getQuadTree(context);
|
||||
const followToNode = QuadTree.find(quadTree, node);
|
||||
if (!followToNode) {
|
||||
return;
|
||||
}
|
||||
// 加一点小偏移,防止连续触发两次后跟随节点变动
|
||||
node.offset = {
|
||||
x: 0,
|
||||
y: -5,
|
||||
};
|
||||
return {
|
||||
followTo: followToNode.id,
|
||||
};
|
||||
};
|
||||
@@ -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 type { GetFollowNode } from '@flowgram-adapter/free-layout-editor';
|
||||
import { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import { subCanvasHandler } from './sub-canvas-handler';
|
||||
import {
|
||||
type CommentContext,
|
||||
commentNodeHandler,
|
||||
} from './comment-node-handler';
|
||||
|
||||
export const getFollowNode: GetFollowNode = (node, context) => {
|
||||
if (node.entity.flowNodeType === FlowNodeBaseType.SUB_CANVAS) {
|
||||
return subCanvasHandler(node);
|
||||
}
|
||||
if (node.entity.flowNodeType === StandardNodeType.Comment) {
|
||||
return commentNodeHandler(node, context as CommentContext);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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 {
|
||||
FreeOperationType,
|
||||
HistoryService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { AutoLayoutService } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type PositionSchema,
|
||||
TransformData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowDocument,
|
||||
type WorkflowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import type {
|
||||
WorkflowShortcutsContribution,
|
||||
WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
|
||||
import { WorkflowGlobalStateEntity } from '@/typing';
|
||||
|
||||
import { safeFn } from '../../utils';
|
||||
import { getFollowNode } from './get-follow-node';
|
||||
|
||||
/**
|
||||
* 自动布局快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowLayoutShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
public static readonly type = 'LAYOUT';
|
||||
|
||||
@inject(WorkflowDocument) private document: WorkflowDocument;
|
||||
@inject(WorkflowGlobalStateEntity)
|
||||
private globalState: WorkflowGlobalStateEntity;
|
||||
@inject(AutoLayoutService) private autoLayoutService: AutoLayoutService;
|
||||
@inject(HistoryService) private historyService: HistoryService;
|
||||
/** 注册快捷键 */
|
||||
public registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowLayoutShortcutsContribution.type,
|
||||
shortcuts: ['alt shift f'],
|
||||
isEnabled: () => !this.globalState.readonly,
|
||||
execute: safeFn(this.handle.bind(this)),
|
||||
});
|
||||
}
|
||||
private async handle(): Promise<void> {
|
||||
await this.autoLayout();
|
||||
}
|
||||
public async autoLayout(): Promise<void> {
|
||||
const nodes = this.document.getAllNodes();
|
||||
const startPositions = nodes.map(this.getNodePosition);
|
||||
await this.autoLayoutService.layout({
|
||||
getFollowNode,
|
||||
});
|
||||
const endPositions = nodes.map(this.getNodePosition);
|
||||
this.updateHistory({
|
||||
nodes,
|
||||
startPositions,
|
||||
endPositions,
|
||||
});
|
||||
}
|
||||
private getNodePosition(node: WorkflowNodeEntity): PositionSchema {
|
||||
const transform = node.getData(TransformData);
|
||||
return {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y,
|
||||
};
|
||||
}
|
||||
private updateHistory(params: {
|
||||
nodes: WorkflowNodeEntity[];
|
||||
startPositions: PositionSchema[];
|
||||
endPositions: PositionSchema[];
|
||||
}): void {
|
||||
const { nodes, startPositions: oldValue, endPositions: value } = params;
|
||||
const ids = nodes.map(node => node.id);
|
||||
this.historyService.pushOperation(
|
||||
{
|
||||
type: FreeOperationType.dragNodes,
|
||||
value: {
|
||||
ids,
|
||||
value,
|
||||
oldValue,
|
||||
},
|
||||
},
|
||||
{
|
||||
noApply: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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 prefer-destructuring -- no need */
|
||||
/* eslint-disable @typescript-eslint/no-namespace -- 使用 namespace 方便管理 */
|
||||
import type { LayoutNode } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
interface QuadTreeBounds {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface QuadTree {
|
||||
bounds: QuadTreeBounds;
|
||||
nodes: LayoutNode[];
|
||||
northWest: QuadTree | null;
|
||||
northEast: QuadTree | null;
|
||||
southWest: QuadTree | null;
|
||||
southEast: QuadTree | null;
|
||||
}
|
||||
|
||||
/** 默认容量 */
|
||||
const DEFAULT_CAPACITY = 4;
|
||||
|
||||
/** 创建四叉树节点 */
|
||||
const createNode = (bounds: QuadTreeBounds): QuadTree => ({
|
||||
bounds,
|
||||
nodes: [],
|
||||
northWest: null,
|
||||
northEast: null,
|
||||
southWest: null,
|
||||
southEast: null,
|
||||
});
|
||||
|
||||
/** 检查节点是否在边界内(需要考虑节点的大小) */
|
||||
const containsNode = (bounds: QuadTreeBounds, node: LayoutNode): boolean =>
|
||||
node.position.x >= bounds.x &&
|
||||
node.position.x + node.size.width <= bounds.x + bounds.width &&
|
||||
node.position.y >= bounds.y &&
|
||||
node.position.y + node.size.height <= bounds.y + bounds.height;
|
||||
|
||||
/** 细分节点 */
|
||||
const subdivide = (quadTree: QuadTree): QuadTree => {
|
||||
const { x, y, width, height } = quadTree.bounds;
|
||||
const w = width / 2;
|
||||
const h = height / 2;
|
||||
|
||||
return {
|
||||
...quadTree,
|
||||
northWest: createNode({ x, y, width: w, height: h }),
|
||||
northEast: createNode({ x: x + w, y, width: w, height: h }),
|
||||
southWest: createNode({ x, y: y + h, width: w, height: h }),
|
||||
southEast: createNode({ x: x + w, y: y + h, width: w, height: h }),
|
||||
};
|
||||
};
|
||||
|
||||
/** 插入节点 */
|
||||
const insert = (
|
||||
quadTree: QuadTree,
|
||||
node: LayoutNode,
|
||||
capacity: number,
|
||||
): QuadTree => {
|
||||
if (!containsNode(quadTree.bounds, node)) {
|
||||
return quadTree;
|
||||
}
|
||||
|
||||
if (!quadTree.northWest) {
|
||||
const newNodes = [...quadTree.nodes, node];
|
||||
if (newNodes.length <= capacity) {
|
||||
return { ...quadTree, nodes: newNodes };
|
||||
}
|
||||
}
|
||||
|
||||
const updatedQuadTree = quadTree.northWest ? quadTree : subdivide(quadTree);
|
||||
|
||||
// 检查节点是否可以插入到任何子象限
|
||||
const canInsertIntoQuadrant = (quadrant: QuadTree | null): boolean =>
|
||||
quadrant ? containsNode(quadrant.bounds, node) : false;
|
||||
|
||||
// 如果节点不能插入到任何子象限,将其保留在当前节点中
|
||||
if (
|
||||
!canInsertIntoQuadrant(updatedQuadTree.northWest) &&
|
||||
!canInsertIntoQuadrant(updatedQuadTree.northEast) &&
|
||||
!canInsertIntoQuadrant(updatedQuadTree.southWest) &&
|
||||
!canInsertIntoQuadrant(updatedQuadTree.southEast)
|
||||
) {
|
||||
return { ...updatedQuadTree, nodes: [...updatedQuadTree.nodes, node] };
|
||||
}
|
||||
|
||||
const insertIntoQuadrant = (quadrant: QuadTree | null): QuadTree | null =>
|
||||
quadrant ? insert(quadrant, node, capacity) : null;
|
||||
|
||||
return {
|
||||
...updatedQuadTree,
|
||||
northWest: insertIntoQuadrant(updatedQuadTree.northWest),
|
||||
northEast: insertIntoQuadrant(updatedQuadTree.northEast),
|
||||
southWest: insertIntoQuadrant(updatedQuadTree.southWest),
|
||||
southEast: insertIntoQuadrant(updatedQuadTree.southEast),
|
||||
};
|
||||
};
|
||||
|
||||
/** 计算两个节点之间的距离(考虑节点中心点) */
|
||||
const distance = (n1: LayoutNode, n2: LayoutNode): number => {
|
||||
const dx =
|
||||
n1.position.x + n1.size.width / 2 - (n2.position.x + n2.size.width / 2);
|
||||
const dy =
|
||||
n1.position.y + n1.size.height / 2 - (n2.position.y + n2.size.height / 2);
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
/** 计算节点到边界的最小距离(考虑节点的大小) */
|
||||
const distanceToBounds = (node: LayoutNode, bounds: QuadTreeBounds): number => {
|
||||
if (containsNode(bounds, node)) {
|
||||
// 如果节点完全在边界内,返回0
|
||||
return 0;
|
||||
}
|
||||
const dx = Math.max(
|
||||
bounds.x - (node.position.x + node.size.width),
|
||||
0,
|
||||
node.position.x - (bounds.x + bounds.width),
|
||||
);
|
||||
const dy = Math.max(
|
||||
bounds.y - (node.position.y + node.size.height),
|
||||
0,
|
||||
node.position.y - (bounds.y + bounds.height),
|
||||
);
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
};
|
||||
|
||||
/** 计算节点集合的边界(考虑节点的大小) */
|
||||
const calculateBounds = (nodes: LayoutNode[]): QuadTreeBounds => {
|
||||
if (nodes.length === 0) {
|
||||
// 返回一个默认的边界
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
}
|
||||
|
||||
const xValues = nodes.map(node => node.position.x);
|
||||
const yValues = nodes.map(node => node.position.y);
|
||||
const widths = nodes.map(node => node.size.width);
|
||||
const heights = nodes.map(node => node.size.height);
|
||||
|
||||
const minX = Math.min(...xValues);
|
||||
const maxX = Math.max(...xValues.map((x, i) => x + widths[i]));
|
||||
const minY = Math.min(...yValues);
|
||||
const maxY = Math.max(...yValues.map((y, i) => y + heights[i]));
|
||||
|
||||
// 添加一些边距以确保所有点都在边界内
|
||||
const margin = Math.max(maxX - minX, maxY - minY) * 0.1;
|
||||
|
||||
return {
|
||||
x: minX - margin,
|
||||
y: minY - margin,
|
||||
width: maxX - minX + 2 * margin,
|
||||
height: maxY - minY + 2 * margin,
|
||||
};
|
||||
};
|
||||
|
||||
/** 创建四叉树 */
|
||||
export const createQuadTree = (nodes: LayoutNode[]): QuadTree => {
|
||||
const bounds = calculateBounds(nodes);
|
||||
let quadTree = createNode(bounds);
|
||||
|
||||
nodes.forEach(node => {
|
||||
quadTree = insert(quadTree, node, DEFAULT_CAPACITY);
|
||||
});
|
||||
|
||||
return quadTree;
|
||||
};
|
||||
|
||||
/** 查找最近邻 */
|
||||
export const findNearestNeighbor = (
|
||||
quadTree: QuadTree,
|
||||
targetNode: LayoutNode,
|
||||
): LayoutNode | null => {
|
||||
const findNearest = (
|
||||
node: QuadTree,
|
||||
nearest: LayoutNode | null,
|
||||
minDistance: number,
|
||||
): { nearest: LayoutNode | null; minDistance: number } => {
|
||||
// 检查当前节点中的所有点
|
||||
for (const currentNode of node.nodes) {
|
||||
if (currentNode === targetNode) {
|
||||
// 排除自身
|
||||
continue;
|
||||
}
|
||||
const currentDistance = distance(targetNode, currentNode);
|
||||
if (currentDistance < minDistance) {
|
||||
nearest = currentNode;
|
||||
minDistance = currentDistance;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果这是叶子节点,返回结果
|
||||
if (!node.northWest) {
|
||||
return { nearest, minDistance };
|
||||
}
|
||||
|
||||
// 检查子四叉树
|
||||
const quadrants = [
|
||||
node.northWest,
|
||||
node.northEast,
|
||||
node.southWest,
|
||||
node.southEast,
|
||||
]
|
||||
.filter((q): q is QuadTree => q !== null)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
distanceToBounds(targetNode, a.bounds) -
|
||||
distanceToBounds(targetNode, b.bounds),
|
||||
);
|
||||
|
||||
for (const quadrant of quadrants) {
|
||||
if (distanceToBounds(targetNode, quadrant.bounds) < minDistance) {
|
||||
const result = findNearest(quadrant, nearest, minDistance);
|
||||
nearest = result.nearest;
|
||||
minDistance = result.minDistance;
|
||||
}
|
||||
}
|
||||
|
||||
return { nearest, minDistance };
|
||||
};
|
||||
|
||||
const result = findNearest(quadTree, null, Infinity);
|
||||
return result.nearest;
|
||||
};
|
||||
|
||||
/** 四叉树工具类 */
|
||||
export namespace QuadTree {
|
||||
export const create = createQuadTree;
|
||||
export const find = findNearestNeighbor;
|
||||
}
|
||||
@@ -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 type { LayoutNode } from '@flowgram-adapter/free-layout-editor';
|
||||
import { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
|
||||
import type { WorkflowNodeMeta } from '@coze-workflow/nodes';
|
||||
|
||||
export const subCanvasHandler = (node: LayoutNode) => {
|
||||
if (node.entity.flowNodeType !== FlowNodeBaseType.SUB_CANVAS) {
|
||||
return;
|
||||
}
|
||||
const nodeMeta = node.entity.getNodeMeta<WorkflowNodeMeta>();
|
||||
const subCanvas = nodeMeta.subCanvas?.(node.entity);
|
||||
return {
|
||||
followTo: subCanvas?.parentNode.id,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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 {
|
||||
EntityManager,
|
||||
WorkflowDocument,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import type {
|
||||
WorkflowShortcutsContribution,
|
||||
WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { WorkflowGlobalStateEntity } from '@/typing';
|
||||
import { type WorkflowExportData } from '@/shortcuts/type';
|
||||
import { WORKFLOW_EXPORT_TYPE } from '@/shortcuts/constant';
|
||||
import { WorkflowOperationService, WorkflowSaveService } from '@/services';
|
||||
|
||||
import { WorkflowPasteShortcutsContribution } from '../paste';
|
||||
import { safeFn } from '../../utils';
|
||||
|
||||
/**
|
||||
* 加载文件快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowLoadShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
public static readonly type = 'LOAD';
|
||||
|
||||
@inject(WorkflowPasteShortcutsContribution)
|
||||
private pasteShortcuts: WorkflowPasteShortcutsContribution;
|
||||
@inject(WorkflowDocument)
|
||||
private document: WorkflowDocument;
|
||||
@inject(WorkflowGlobalStateEntity)
|
||||
private globalState: WorkflowGlobalStateEntity;
|
||||
@inject(WorkflowSaveService)
|
||||
private saveService: WorkflowSaveService;
|
||||
@inject(WorkflowOperationService) operationService: WorkflowOperationService;
|
||||
@inject(EntityManager) private entityManager: EntityManager;
|
||||
/** 注册快捷键 */
|
||||
public registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowLoadShortcutsContribution.type,
|
||||
shortcuts: ['meta shift l', 'ctrl shift l'],
|
||||
isEnabled: () => !this.globalState.readonly,
|
||||
execute: safeFn(this.handle.bind(this)),
|
||||
});
|
||||
}
|
||||
/** 处理 */
|
||||
private async handle(): Promise<void> {
|
||||
const data = await this.load();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.json.nodes.length > 200) {
|
||||
// 大型工作流,刷新页面
|
||||
await this.refresh(data);
|
||||
} else {
|
||||
// 小型工作流,重新渲染
|
||||
await this.rerender(data);
|
||||
}
|
||||
}
|
||||
/** 刷新 */
|
||||
private async refresh(data: WorkflowExportData): Promise<void> {
|
||||
await this.operationService.save(
|
||||
data.json,
|
||||
this.saveService.ignoreStatusTransfer,
|
||||
);
|
||||
await this.globalState.reload();
|
||||
window.location.reload();
|
||||
}
|
||||
/** 重新渲染 */
|
||||
private async rerender(data: WorkflowExportData): Promise<void> {
|
||||
this.document.clear();
|
||||
this.entityManager.changeEntityLocked = true;
|
||||
this.pasteShortcuts.render({
|
||||
json: data.json,
|
||||
source: data.source,
|
||||
});
|
||||
this.entityManager.changeEntityLocked = false;
|
||||
await this.saveService.fitView();
|
||||
this.saveService.save();
|
||||
}
|
||||
/** 加载 */
|
||||
private load(): Promise<WorkflowExportData | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleError = (error: Error) => {
|
||||
console.error(error);
|
||||
Toast.error(`Load failed: ${error.message}`);
|
||||
resolve(undefined);
|
||||
};
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = '.json,.flow';
|
||||
|
||||
fileInput.addEventListener('change', event => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
|
||||
if (!file) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
try {
|
||||
const content: WorkflowExportData = JSON.parse(
|
||||
e.target?.result as string,
|
||||
);
|
||||
if (!this.validate(content)) {
|
||||
handleError(new Error('Invalid file'));
|
||||
return;
|
||||
}
|
||||
resolve(content);
|
||||
} catch (error) {
|
||||
handleError(error as Error);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => handleError(new Error('Read file failed'));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
/** 验证数据 */
|
||||
private validate(data: WorkflowExportData): boolean {
|
||||
if (data.type !== WORKFLOW_EXPORT_TYPE) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* 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 {
|
||||
FlowNodeBaseType,
|
||||
FlowNodeTransformData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { EntityManager } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowCommands,
|
||||
WorkflowDocument,
|
||||
type WorkflowEdgeJSON,
|
||||
WorkflowHoverService,
|
||||
type WorkflowLineEntity,
|
||||
type WorkflowLinePortInfo,
|
||||
WorkflowLinesManager,
|
||||
WorkflowNodeEntity,
|
||||
type WorkflowNodeMeta,
|
||||
WorkflowSelectService,
|
||||
type WorkflowSubCanvas,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
delay,
|
||||
type IPoint,
|
||||
type PositionSchema,
|
||||
Rectangle,
|
||||
} from '@flowgram-adapter/common';
|
||||
import {
|
||||
type WorkflowShortcutsContribution,
|
||||
type WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
import { type WorkflowNodeJSON } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { WorkflowGlobalStateEntity } from '@/typing';
|
||||
import {
|
||||
WorkflowCustomDragService,
|
||||
WorkflowEditService,
|
||||
WorkflowSaveService,
|
||||
} from '@/services';
|
||||
|
||||
import { generateUniqueWorkflow } from '../../utils/unique-workflow';
|
||||
import { safeFn } from '../../utils';
|
||||
import type {
|
||||
WorkflowClipboardData,
|
||||
WorkflowClipboardJSON,
|
||||
WorkflowClipboardNodeJSON,
|
||||
WorkflowClipboardRect,
|
||||
WorkflowClipboardSource,
|
||||
} from '../../type';
|
||||
import { isValidNode } from './is-valid-node';
|
||||
import { isValidData } from './is-valid-data';
|
||||
|
||||
/**
|
||||
* 粘贴快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowPasteShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
@inject(EntityManager) private entityManager: EntityManager;
|
||||
@inject(WorkflowLinesManager) private linesManager: WorkflowLinesManager;
|
||||
@inject(WorkflowDocument) private document: WorkflowDocument;
|
||||
@inject(WorkflowHoverService) private hoverService: WorkflowHoverService;
|
||||
@inject(WorkflowCustomDragService)
|
||||
private dragService: WorkflowCustomDragService;
|
||||
@inject(WorkflowSelectService) private selection: WorkflowSelectService;
|
||||
@inject(WorkflowEditService) private editService: WorkflowEditService;
|
||||
@inject(WorkflowGlobalStateEntity)
|
||||
private globalState: WorkflowGlobalStateEntity;
|
||||
@inject(WorkflowSaveService) private saveService: WorkflowSaveService;
|
||||
|
||||
/** 注册快捷键 */
|
||||
public registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowCommands.PASTE_NODES,
|
||||
shortcuts: ['meta v', 'ctrl v'],
|
||||
isEnabled: () => !this.globalState.readonly,
|
||||
execute: safeFn(this.handle.bind(this)),
|
||||
});
|
||||
}
|
||||
/** 渲染 */
|
||||
public async render(params: {
|
||||
json: WorkflowClipboardJSON;
|
||||
source: WorkflowClipboardSource;
|
||||
titleCache?: string[];
|
||||
offset?: IPoint;
|
||||
parent?: WorkflowNodeEntity;
|
||||
toContainer?: WorkflowNodeEntity;
|
||||
}): Promise<WorkflowNodeEntity[]> {
|
||||
const {
|
||||
json,
|
||||
source,
|
||||
titleCache = [],
|
||||
offset = { x: 0, y: 0 },
|
||||
parent,
|
||||
toContainer,
|
||||
} = params;
|
||||
const nodes: WorkflowNodeEntity[] = await this.createNodes({
|
||||
json: json.nodes,
|
||||
source,
|
||||
titleCache,
|
||||
offset,
|
||||
parent,
|
||||
toContainer,
|
||||
});
|
||||
await this.nextTick(); // 等待节点渲染与动态端口渲染
|
||||
this.createLines({
|
||||
json: json.edges,
|
||||
parent,
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
/** 处理复制事件 */
|
||||
private async handle(
|
||||
_event: KeyboardEvent,
|
||||
): Promise<WorkflowNodeEntity[] | undefined> {
|
||||
const data = await this.tryReadClipboard();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (!isValidData({ data, globalState: this.globalState })) {
|
||||
return;
|
||||
}
|
||||
const nodes = await this.apply(data);
|
||||
if (nodes.length > 0) {
|
||||
Toast.success({
|
||||
content: I18n.t('copy_success'),
|
||||
showClose: false,
|
||||
});
|
||||
// 滚动到可视区域
|
||||
this.scrollToNodes(nodes);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
/** 尝试读取剪贴板 */
|
||||
private async tryReadClipboard(): Promise<WorkflowClipboardData | undefined> {
|
||||
try {
|
||||
// 需要用户授予网页剪贴板读取权限, 如果用户没有授予权限, 代码可能会抛出异常 NotAllowedError
|
||||
const text: string = (await navigator.clipboard.readText()) || '';
|
||||
const clipboardData: WorkflowClipboardData = JSON.parse(text);
|
||||
return clipboardData;
|
||||
// eslint-disable-next-line @coze-arch/use-error-in-catch -- no need report error
|
||||
} catch (e) {
|
||||
// 这里本身剪贴板里的数据就不固定,所以没必要报错
|
||||
return;
|
||||
}
|
||||
}
|
||||
/** 应用剪切板数据 */
|
||||
private async apply(
|
||||
data: WorkflowClipboardData,
|
||||
): Promise<WorkflowNodeEntity[]> {
|
||||
const { source, json: rawJSON } = data;
|
||||
const json = generateUniqueWorkflow({
|
||||
json: rawJSON,
|
||||
isUniqueId: (id: string) => !this.entityManager.getEntityById(id),
|
||||
});
|
||||
|
||||
// 重建节点前需要初始化节点数据
|
||||
await this.saveService.initNodeData(json.nodes as WorkflowNodeJSON[]);
|
||||
|
||||
const titleCache: string[] = [];
|
||||
const offset = this.calcPasteOffset(data.bounds);
|
||||
const container = this.getSelectedContainer();
|
||||
const nodes = await this.render({
|
||||
json,
|
||||
source,
|
||||
titleCache,
|
||||
offset,
|
||||
parent: container,
|
||||
toContainer: container,
|
||||
});
|
||||
this.selectNodes(nodes);
|
||||
return nodes;
|
||||
}
|
||||
/** 计算粘贴偏移 */
|
||||
private calcPasteOffset(boundsData: WorkflowClipboardRect): IPoint {
|
||||
const { x, y, width, height } = boundsData;
|
||||
const rect = new Rectangle(x, y, width, height);
|
||||
const { center } = rect;
|
||||
const mousePos = this.hoverService.hoveredPos;
|
||||
return {
|
||||
x: mousePos.x - center.x,
|
||||
y: mousePos.y - center.y,
|
||||
};
|
||||
}
|
||||
/** 创建节点 */
|
||||
private async createNodes(params: {
|
||||
json: WorkflowClipboardNodeJSON[];
|
||||
source: WorkflowClipboardSource;
|
||||
titleCache: string[];
|
||||
offset: IPoint;
|
||||
parent?: WorkflowNodeEntity;
|
||||
toContainer?: WorkflowNodeEntity;
|
||||
}): Promise<WorkflowNodeEntity[]> {
|
||||
const { json, source, titleCache, offset, parent, toContainer } = params;
|
||||
const nodes: WorkflowNodeEntity[] = [];
|
||||
await Promise.all(
|
||||
json
|
||||
.map(async (rawNodeJSON: WorkflowClipboardNodeJSON) => {
|
||||
const { blocks, edges, ...nodeJSON } = rawNodeJSON;
|
||||
const node = await this.createNode({
|
||||
nodeJSON,
|
||||
source,
|
||||
titleCache,
|
||||
offset,
|
||||
parent,
|
||||
toContainer,
|
||||
});
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const subCanvas = this.getNodeSubCanvas(node);
|
||||
if (subCanvas) {
|
||||
nodes.push(subCanvas.canvasNode);
|
||||
}
|
||||
if (blocks?.length) {
|
||||
const container = this.getNodeSubCanvas(node)?.canvasNode ?? node;
|
||||
this.render({
|
||||
json: {
|
||||
nodes: blocks,
|
||||
edges: edges ?? [],
|
||||
},
|
||||
source,
|
||||
titleCache,
|
||||
offset,
|
||||
parent: container,
|
||||
});
|
||||
}
|
||||
nodes.push(node);
|
||||
})
|
||||
.filter(Boolean),
|
||||
);
|
||||
return nodes;
|
||||
}
|
||||
/** 获取节点子画布信息 */
|
||||
private getNodeSubCanvas(
|
||||
node: WorkflowNodeEntity,
|
||||
): WorkflowSubCanvas | undefined {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();
|
||||
const subCanvas = nodeMeta.subCanvas?.(node);
|
||||
return subCanvas;
|
||||
}
|
||||
/** 创建节点 */
|
||||
private async createNode(params: {
|
||||
nodeJSON: WorkflowClipboardNodeJSON;
|
||||
source: WorkflowClipboardSource;
|
||||
titleCache: string[];
|
||||
offset: IPoint;
|
||||
parent?: WorkflowNodeEntity;
|
||||
toContainer?: WorkflowNodeEntity;
|
||||
}): Promise<WorkflowNodeEntity | undefined> {
|
||||
const { nodeJSON, source, titleCache, offset, parent, toContainer } =
|
||||
params;
|
||||
if (
|
||||
!isValidNode({
|
||||
node: nodeJSON,
|
||||
parent,
|
||||
source,
|
||||
globalState: this.globalState,
|
||||
dragService: this.dragService,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 生成唯一节点标题
|
||||
const processedNodeJSON = this.editService.recreateNodeJSON(
|
||||
nodeJSON,
|
||||
titleCache,
|
||||
false,
|
||||
);
|
||||
if (processedNodeJSON.meta?.canvasPosition) {
|
||||
processedNodeJSON.meta.canvasPosition = {
|
||||
x: processedNodeJSON.meta.canvasPosition.x + offset.x,
|
||||
y: processedNodeJSON.meta.canvasPosition.y + offset.y,
|
||||
};
|
||||
}
|
||||
const nodePosition = this.calcNodePosition({
|
||||
nodeJSON,
|
||||
offset,
|
||||
parent,
|
||||
toContainer,
|
||||
});
|
||||
const node = await this.document.copyNodeFromJSON(
|
||||
nodeJSON.type as string,
|
||||
processedNodeJSON,
|
||||
nodeJSON.id,
|
||||
nodePosition,
|
||||
parent?.id,
|
||||
);
|
||||
return node;
|
||||
}
|
||||
/** 创建连线 */
|
||||
private createLines(params: {
|
||||
json: WorkflowEdgeJSON[];
|
||||
parent?: WorkflowNodeEntity;
|
||||
}): WorkflowLineEntity[] {
|
||||
const { json, parent } = params;
|
||||
return json
|
||||
.map(edgeJSON => this.createLine({ edgeJSON, parent }))
|
||||
.filter(Boolean) as WorkflowLineEntity[];
|
||||
}
|
||||
/** 创建连线 */
|
||||
private createLine(params: {
|
||||
edgeJSON: WorkflowEdgeJSON;
|
||||
parent?: WorkflowNodeEntity;
|
||||
}): WorkflowLineEntity | undefined {
|
||||
const { edgeJSON, parent } = params;
|
||||
const fromNode = this.entityManager.getEntityById<WorkflowNodeEntity>(
|
||||
edgeJSON.sourceNodeID,
|
||||
);
|
||||
const toNode = this.entityManager.getEntityById<WorkflowNodeEntity>(
|
||||
edgeJSON.targetNodeID,
|
||||
);
|
||||
if (!fromNode || !toNode) {
|
||||
return;
|
||||
}
|
||||
const lineInfo: WorkflowLinePortInfo = {
|
||||
from: edgeJSON.sourceNodeID,
|
||||
fromPort: edgeJSON.sourcePortID,
|
||||
to: edgeJSON.targetNodeID,
|
||||
toPort: edgeJSON.targetPortID,
|
||||
};
|
||||
if (!parent) {
|
||||
return this.linesManager.createLine(lineInfo);
|
||||
}
|
||||
// 父子节点之间连线,需替换父节点为子画布
|
||||
const parentSubCanvas = this.getNodeSubCanvas(parent);
|
||||
if (!parentSubCanvas) {
|
||||
return this.linesManager.createLine(lineInfo);
|
||||
}
|
||||
if (lineInfo.from === parentSubCanvas.parentNode.id) {
|
||||
return this.linesManager.createLine({
|
||||
...lineInfo,
|
||||
from: parentSubCanvas.canvasNode.id,
|
||||
});
|
||||
}
|
||||
if (lineInfo.to === parentSubCanvas.parentNode.id) {
|
||||
return this.linesManager.createLine({
|
||||
...lineInfo,
|
||||
to: parentSubCanvas.canvasNode.id,
|
||||
});
|
||||
}
|
||||
return this.linesManager.createLine(lineInfo);
|
||||
}
|
||||
/** 计算节点位置 */
|
||||
private calcNodePosition(params: {
|
||||
nodeJSON: WorkflowClipboardNodeJSON;
|
||||
parent?: WorkflowNodeEntity;
|
||||
offset: IPoint;
|
||||
toContainer?: WorkflowNodeEntity;
|
||||
}): PositionSchema {
|
||||
const { nodeJSON, parent, offset, toContainer } = params;
|
||||
|
||||
if (!nodeJSON.meta?.position) {
|
||||
return this.hoverService.hoveredPos;
|
||||
}
|
||||
|
||||
const bounds = new Rectangle(
|
||||
nodeJSON._temp.bounds.x,
|
||||
nodeJSON._temp.bounds.y,
|
||||
nodeJSON._temp.bounds.width,
|
||||
nodeJSON._temp.bounds.height,
|
||||
);
|
||||
|
||||
const basePosition: PositionSchema =
|
||||
parent && !toContainer
|
||||
? nodeJSON.meta.position
|
||||
: {
|
||||
x: offset.x + bounds.center.x,
|
||||
y: offset.y + bounds.y,
|
||||
};
|
||||
|
||||
if (toContainer) {
|
||||
return this.dragService.adjustSubNodePosition(
|
||||
nodeJSON.type as string,
|
||||
toContainer,
|
||||
basePosition,
|
||||
);
|
||||
}
|
||||
|
||||
return basePosition;
|
||||
}
|
||||
/** 获取鼠标选中的容器 */
|
||||
private getSelectedContainer(): WorkflowNodeEntity | undefined {
|
||||
const { activatedNode } = this.selection;
|
||||
if (activatedNode?.flowNodeType === FlowNodeBaseType.SUB_CANVAS) {
|
||||
return activatedNode;
|
||||
}
|
||||
}
|
||||
/** 获取选中的节点 */
|
||||
private get selectedNodes(): WorkflowNodeEntity[] {
|
||||
return this.selection.selection.filter(
|
||||
n => n instanceof WorkflowNodeEntity,
|
||||
) as WorkflowNodeEntity[];
|
||||
}
|
||||
/** 选中节点 */
|
||||
private selectNodes(nodes: WorkflowNodeEntity[]): void {
|
||||
if (nodes.length === 1) {
|
||||
this.editService.focusNode(nodes[0]);
|
||||
} else {
|
||||
this.selection.selection = nodes;
|
||||
}
|
||||
}
|
||||
/** 滚动到节点 */
|
||||
private async scrollToNodes(nodes: WorkflowNodeEntity[]): Promise<void> {
|
||||
const nodeBounds = nodes.map(
|
||||
node => node.getData(FlowNodeTransformData).bounds,
|
||||
);
|
||||
await this.document.playgroundConfig.scrollToView({
|
||||
bounds: Rectangle.enlarge(nodeBounds),
|
||||
});
|
||||
}
|
||||
/** 等待下一帧 */
|
||||
private async nextTick(): Promise<void> {
|
||||
const frameTime = 16; // 16ms 为一个渲染帧
|
||||
await delay(frameTime);
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
import { type WorkflowGlobalStateEntity } from '@/typing';
|
||||
|
||||
import { type WorkflowClipboardData } from '../../type';
|
||||
import { WORKFLOW_CLIPBOARD_TYPE } from '../../constant';
|
||||
|
||||
/** 检查数据是否合法 */
|
||||
export const isValidData = (params: {
|
||||
data: WorkflowClipboardData;
|
||||
globalState: WorkflowGlobalStateEntity;
|
||||
}): boolean => {
|
||||
const { data, globalState } = params;
|
||||
if (data.type !== WORKFLOW_CLIPBOARD_TYPE) {
|
||||
return false;
|
||||
}
|
||||
// 跨域名表示不同环境,上架插件不同,不能复制
|
||||
if (data.source.host !== window.location.host) {
|
||||
return false;
|
||||
}
|
||||
// 抖音空间不兼容正常空间
|
||||
if (data.source.isDouyin !== globalState.isBindDouyin) {
|
||||
Toast.warning({
|
||||
content: I18n.t(
|
||||
'workflow_node_copy_othercanva',
|
||||
{},
|
||||
'当前画布类型不一致,无法粘贴',
|
||||
),
|
||||
showClose: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// 不同的画布类型不能复制
|
||||
if (data.source.flowMode !== globalState.flowMode) {
|
||||
Toast.warning({
|
||||
content: I18n.t(
|
||||
'workflow_node_copy_othercanva',
|
||||
{},
|
||||
'当前画布类型不一致,无法粘贴',
|
||||
),
|
||||
showClose: false,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 { WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import type { WorkflowCustomDragService } from '@/services';
|
||||
import type { WorkflowGlobalStateEntity } from '@/typing';
|
||||
|
||||
import type {
|
||||
WorkflowClipboardNodeJSON,
|
||||
WorkflowClipboardSource,
|
||||
} from '../../type';
|
||||
import {
|
||||
ApiNodeValidator,
|
||||
CrossSpaceNodeValidator,
|
||||
DropValidator,
|
||||
LoopContextValidator,
|
||||
NestedLoopBatchValidator,
|
||||
SameSpaceValidator,
|
||||
SameWorkflowValidator,
|
||||
SceneNodeValidator,
|
||||
SubWorkflowSelfRefValidator,
|
||||
} from './validators';
|
||||
import { ValidationChain } from './validators/validation-chain';
|
||||
|
||||
/** 是否合法节点 */
|
||||
export const isValidNode = (params: {
|
||||
node: WorkflowClipboardNodeJSON;
|
||||
parent?: WorkflowNodeEntity;
|
||||
source: WorkflowClipboardSource;
|
||||
globalState: WorkflowGlobalStateEntity;
|
||||
dragService: WorkflowCustomDragService;
|
||||
}): boolean => {
|
||||
const validationChain = new ValidationChain();
|
||||
validationChain
|
||||
// 1. 相同空间,相同工作流
|
||||
.setNext(new DropValidator())
|
||||
.setNext(new LoopContextValidator())
|
||||
.setNext(new NestedLoopBatchValidator())
|
||||
.setNext(new SubWorkflowSelfRefValidator())
|
||||
// 2. 相同空间,不同工作流
|
||||
.setNext(new SameWorkflowValidator())
|
||||
.setNext(new SceneNodeValidator())
|
||||
// 3. 跨空间空间
|
||||
.setNext(new SameSpaceValidator())
|
||||
.setNext(new ApiNodeValidator())
|
||||
.setNext(new CrossSpaceNodeValidator());
|
||||
|
||||
return validationChain.run(params);
|
||||
};
|
||||
@@ -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 { ApiNodeData } from '@coze-workflow/nodes';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
import { PluginProductStatus } from '@coze-arch/idl/developer_api';
|
||||
|
||||
import {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class ApiNodeValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { node } = context;
|
||||
|
||||
if (node.type !== StandardNodeType.Api) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 不允许跨空间复制未上架的插件节点
|
||||
const apiNodeData = node._temp.externalData as ApiNodeData;
|
||||
const isListed =
|
||||
apiNodeData?.pluginProductStatus === PluginProductStatus.Listed;
|
||||
return isListed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import type { WorkflowGlobalStateEntity } from '@/typing';
|
||||
import type { WorkflowCustomDragService } from '@/services';
|
||||
|
||||
import type {
|
||||
WorkflowClipboardNodeJSON,
|
||||
WorkflowClipboardSource,
|
||||
} from '../../../type';
|
||||
|
||||
export interface NodeValidationContext {
|
||||
node: WorkflowClipboardNodeJSON;
|
||||
source: WorkflowClipboardSource;
|
||||
globalState: WorkflowGlobalStateEntity;
|
||||
dragService: WorkflowCustomDragService;
|
||||
parent?: WorkflowNodeEntity;
|
||||
}
|
||||
|
||||
export interface NodeValidator {
|
||||
run: (context: NodeValidationContext) => boolean;
|
||||
setNext: (validator: NodeValidator) => NodeValidator;
|
||||
}
|
||||
|
||||
export abstract class BaseNodeValidator implements NodeValidator {
|
||||
protected next: NodeValidator | null = null;
|
||||
|
||||
setNext(validator: NodeValidator): NodeValidator {
|
||||
this.next = validator;
|
||||
return validator;
|
||||
}
|
||||
|
||||
run(context: NodeValidationContext): boolean {
|
||||
const result = this.validate(context);
|
||||
if (result !== null) {
|
||||
return result;
|
||||
}
|
||||
return this.next?.run(context) ?? true;
|
||||
}
|
||||
|
||||
protected abstract validate(context: NodeValidationContext): boolean | null;
|
||||
}
|
||||
@@ -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 { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class CrossSpaceNodeValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { node } = context;
|
||||
|
||||
// 不允许跨空间复制的节点
|
||||
if (
|
||||
[
|
||||
StandardNodeType.Dataset,
|
||||
StandardNodeType.DatasetWrite,
|
||||
StandardNodeType.Database,
|
||||
StandardNodeType.DatabaseQuery,
|
||||
StandardNodeType.DatabaseCreate,
|
||||
StandardNodeType.DatabaseUpdate,
|
||||
StandardNodeType.DatabaseDelete,
|
||||
StandardNodeType.SubWorkflow,
|
||||
StandardNodeType.Imageflow,
|
||||
].includes(node.type as StandardNodeType)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 type { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class DropValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { node, dragService, parent } = context;
|
||||
|
||||
const canDropInfo = dragService.canDropToNode({
|
||||
dragNodeType: node.type as StandardNodeType,
|
||||
dropNode: parent,
|
||||
});
|
||||
if (!canDropInfo.allowDrop) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export { ApiNodeValidator } from './api-node-validator';
|
||||
export {
|
||||
NodeValidationContext,
|
||||
NodeValidator,
|
||||
BaseNodeValidator,
|
||||
} from './base-validator';
|
||||
export { CrossSpaceNodeValidator } from './cross-space-node-validator';
|
||||
export { DropValidator } from './drop-validator';
|
||||
export { LoopContextValidator } from './loop-context-validator';
|
||||
export { NestedLoopBatchValidator } from './nested-loop-batch-validator';
|
||||
export { SameSpaceValidator } from './same-space-validator';
|
||||
export { SameWorkflowValidator } from './same-workflow-validator';
|
||||
export { SceneNodeValidator } from './scene-node-validator';
|
||||
export { ValidationChain } from './validation-chain';
|
||||
export { SubWorkflowSelfRefValidator } from './sub-workflow-self-ref-validator';
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class LoopContextValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { node, parent } = context;
|
||||
const nodeType = node.type as StandardNodeType;
|
||||
|
||||
if (!parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Break / SetVariable / Continue 只允许在 Loop SubCanvas 内
|
||||
if (
|
||||
[
|
||||
StandardNodeType.Break,
|
||||
StandardNodeType.Continue,
|
||||
StandardNodeType.SetVariable,
|
||||
].includes(nodeType)
|
||||
) {
|
||||
const parentNodeMeta = parent.getNodeMeta();
|
||||
const parentSubCanvas = parentNodeMeta.subCanvas?.(parent);
|
||||
return (
|
||||
parentSubCanvas?.isCanvas &&
|
||||
parentSubCanvas.parentNode.flowNodeType === StandardNodeType.Loop
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class NestedLoopBatchValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { node, parent } = context;
|
||||
const nodeType = node.type as StandardNodeType;
|
||||
|
||||
if (!parent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Loop / Batch 不允许嵌套
|
||||
if ([StandardNodeType.Loop, StandardNodeType.Batch].includes(nodeType)) {
|
||||
return parent.flowNodeType !== FlowNodeBaseType.SUB_CANVAS;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class SameSpaceValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { source, globalState } = context;
|
||||
|
||||
return source.spaceId === globalState.spaceId ? true : null;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class SameWorkflowValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { source, globalState } = context;
|
||||
return source.workflowId === globalState.workflowId ? true : null;
|
||||
}
|
||||
}
|
||||
@@ -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 { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class SceneNodeValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { node } = context;
|
||||
|
||||
// 不允许跨工作流复制场景工作流专属节点
|
||||
if (
|
||||
node.type === StandardNodeType.SceneChat ||
|
||||
node.type === StandardNodeType.SceneVariable
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 { StandardNodeType } from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
BaseNodeValidator,
|
||||
type NodeValidationContext,
|
||||
} from './base-validator';
|
||||
|
||||
export class SubWorkflowSelfRefValidator extends BaseNodeValidator {
|
||||
protected validate(context: NodeValidationContext): boolean | null {
|
||||
const { node, globalState } = context;
|
||||
|
||||
// 不允许工作流引用自己作为子工作流
|
||||
if (
|
||||
node.type === StandardNodeType.SubWorkflow &&
|
||||
node.data?.inputs?.workflowId === globalState.workflowId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { BaseNodeValidator } from './base-validator';
|
||||
|
||||
export class ValidationChain extends BaseNodeValidator {
|
||||
protected validate(): boolean | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 { inject, injectable } from 'inversify';
|
||||
import {
|
||||
WorkflowDocument,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import type {
|
||||
WorkflowShortcutsContribution,
|
||||
WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
|
||||
import { WorkflowGlobalStateEntity } from '@/typing';
|
||||
|
||||
import { safeFn } from '../../utils';
|
||||
|
||||
/**
|
||||
* 全选快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowSelectAllShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
public static readonly type = 'SELECT_ALL';
|
||||
|
||||
@inject(WorkflowDocument)
|
||||
private document: WorkflowDocument;
|
||||
@inject(WorkflowGlobalStateEntity)
|
||||
private globalState: WorkflowGlobalStateEntity;
|
||||
@inject(WorkflowSelectService)
|
||||
private selectService: WorkflowSelectService;
|
||||
/** 注册快捷键 */
|
||||
public registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowSelectAllShortcutsContribution.type,
|
||||
shortcuts: ['meta a', 'ctrl a'],
|
||||
isEnabled: () => !this.globalState.readonly,
|
||||
execute: safeFn(this.handle.bind(this)),
|
||||
});
|
||||
}
|
||||
private handle(): void {
|
||||
const nodes = this.document.root.blocks;
|
||||
this.selectService.selection = nodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { WorkflowCommands } from '@flowgram-adapter/free-layout-editor';
|
||||
import type {
|
||||
WorkflowShortcutsContribution,
|
||||
WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
|
||||
import { safeFn } from '../../utils';
|
||||
|
||||
/**
|
||||
* 缩放快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class WorkflowZoomShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
@inject(PlaygroundConfigEntity)
|
||||
private playgroundConfig: PlaygroundConfigEntity;
|
||||
/** 注册快捷键 */
|
||||
public registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowCommands.ZOOM_IN,
|
||||
shortcuts: ['meta =', 'ctrl ='],
|
||||
execute: safeFn(this.zoomIn.bind(this)),
|
||||
});
|
||||
registry.addHandlers({
|
||||
commandId: WorkflowCommands.ZOOM_OUT,
|
||||
shortcuts: ['meta -', 'ctrl -'],
|
||||
execute: safeFn(this.zoomOut.bind(this)),
|
||||
});
|
||||
}
|
||||
private zoomIn(): void {
|
||||
if (this.playgroundConfig.zoom > 1.9) {
|
||||
return;
|
||||
}
|
||||
this.playgroundConfig.zoomin();
|
||||
}
|
||||
private zoomOut(): void {
|
||||
if (this.playgroundConfig.zoom < 0.1) {
|
||||
return;
|
||||
}
|
||||
this.playgroundConfig.zoomout();
|
||||
}
|
||||
}
|
||||
26
frontend/packages/workflow/playground/src/shortcuts/index.ts
Normal file
26
frontend/packages/workflow/playground/src/shortcuts/index.ts
Normal 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.
|
||||
*/
|
||||
|
||||
export { WORKFLOW_CLIPBOARD_TYPE } from './constant';
|
||||
export { bindShortcuts } from './utils';
|
||||
export {
|
||||
WorkflowClipboardData,
|
||||
WorkflowClipboardJSON,
|
||||
WorkflowClipboardSource,
|
||||
WorkflowClipboardNodeJSON,
|
||||
} from './type';
|
||||
// eslint-disable-next-line @coze-arch/no-batch-import-or-export -- no duplicate declaration
|
||||
export * from './contributions';
|
||||
68
frontend/packages/workflow/playground/src/shortcuts/type.ts
Normal file
68
frontend/packages/workflow/playground/src/shortcuts/type.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 {
|
||||
WorkflowEdgeJSON,
|
||||
WorkflowNodeJSON,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import type { NodeData } from '@coze-workflow/nodes';
|
||||
import type { ValueOf, WorkflowMode } from '@coze-workflow/base';
|
||||
|
||||
import type { WORKFLOW_CLIPBOARD_TYPE, WORKFLOW_EXPORT_TYPE } from './constant';
|
||||
|
||||
export interface WorkflowClipboardData {
|
||||
type: typeof WORKFLOW_CLIPBOARD_TYPE;
|
||||
json: WorkflowClipboardJSON;
|
||||
source: WorkflowClipboardSource;
|
||||
bounds: WorkflowClipboardRect;
|
||||
}
|
||||
|
||||
export interface WorkflowExportData {
|
||||
type: typeof WORKFLOW_EXPORT_TYPE;
|
||||
json: WorkflowClipboardJSON;
|
||||
source: WorkflowClipboardSource;
|
||||
}
|
||||
|
||||
export interface WorkflowClipboardJSON {
|
||||
nodes: WorkflowClipboardNodeJSON[];
|
||||
edges: WorkflowEdgeJSON[];
|
||||
}
|
||||
|
||||
export interface WorkflowClipboardSource {
|
||||
workflowId: string;
|
||||
flowMode: WorkflowMode;
|
||||
spaceId: string;
|
||||
host: string;
|
||||
isDouyin: boolean;
|
||||
}
|
||||
|
||||
export interface WorkflowClipboardRect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface WorkflowClipboardNodeTemporary {
|
||||
externalData?: ValueOf<NodeData>;
|
||||
bounds: WorkflowClipboardRect;
|
||||
}
|
||||
|
||||
export interface WorkflowClipboardNodeJSON extends WorkflowNodeJSON {
|
||||
blocks?: WorkflowClipboardNodeJSON[];
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention -- _temp 是内部字段,不对外暴露
|
||||
_temp: WorkflowClipboardNodeTemporary;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 { bindShortcuts } from './register';
|
||||
export { safeFn } from './safe-fn';
|
||||
@@ -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 type { interfaces } from 'inversify';
|
||||
import { bindContributions } from '@flowgram-adapter/common';
|
||||
import type { WorkflowShortcutsContribution } from '@coze-workflow/render';
|
||||
|
||||
interface ShortcutFactory {
|
||||
new (): WorkflowShortcutsContribution;
|
||||
}
|
||||
|
||||
export const bindShortcuts = (
|
||||
bind: interfaces.Bind,
|
||||
to: typeof WorkflowShortcutsContribution,
|
||||
shortcuts: ShortcutFactory[],
|
||||
) => {
|
||||
shortcuts.forEach(shortcut => {
|
||||
bindContributions(bind, shortcut, [to]);
|
||||
});
|
||||
};
|
||||
@@ -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 { reporter, logger } from '@coze-arch/logger';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
|
||||
export const safeFn =
|
||||
(fn: Function) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- no need to check type
|
||||
(...args: any[]) => {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch (e) {
|
||||
Toast.error({
|
||||
content: `[Coze Workflow] Failed to run function: ${fn.name || '() => any'}`,
|
||||
});
|
||||
console.error('Failed to run function: ', e);
|
||||
reporter.errorEvent({
|
||||
namespace: 'workflow',
|
||||
eventName: 'workflow_shortcuts_error',
|
||||
error: e,
|
||||
});
|
||||
logger.error(e);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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 complexity -- no need to fix */
|
||||
/* eslint-disable @typescript-eslint/no-namespace -- no need to fix */
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { traverse, type TraverseContext } from '@coze-workflow/base';
|
||||
|
||||
import type { WorkflowClipboardJSON, WorkflowClipboardNodeJSON } from '../type';
|
||||
|
||||
namespace UniqueWorkflowUtils {
|
||||
/** 生成唯一ID */
|
||||
const generateUniqueId = customAlphabet('1234567890', 6);
|
||||
/** 获取所有节点ID */
|
||||
export const getAllNodeIds = (json: WorkflowClipboardJSON): string[] => {
|
||||
const nodeIds = new Set<string>();
|
||||
const addNodeId = (node: WorkflowClipboardNodeJSON) => {
|
||||
nodeIds.add(node.id);
|
||||
if (node.blocks?.length) {
|
||||
node.blocks.forEach(child => addNodeId(child));
|
||||
}
|
||||
};
|
||||
json.nodes.forEach(node => addNodeId(node));
|
||||
return Array.from(nodeIds);
|
||||
};
|
||||
/** 生成节点替换映射 */
|
||||
export const generateNodeReplaceMap = (
|
||||
nodeIds: string[],
|
||||
isUniqueId: (id: string) => boolean,
|
||||
): Map<string, string> => {
|
||||
const nodeReplaceMap = new Map<string, string>();
|
||||
nodeIds.forEach(id => {
|
||||
if (isUniqueId(id)) {
|
||||
nodeReplaceMap.set(id, id);
|
||||
} else {
|
||||
let newId: string;
|
||||
do {
|
||||
// 这里添加一个固定前缀,避免 ID 以 0 开头,后端会报错
|
||||
newId = `1${generateUniqueId()}`;
|
||||
} while (!isUniqueId(newId));
|
||||
nodeReplaceMap.set(id, newId);
|
||||
}
|
||||
});
|
||||
return nodeReplaceMap;
|
||||
};
|
||||
/** 是否存在 */
|
||||
const isExist = (value: unknown): boolean =>
|
||||
value !== null && value !== undefined;
|
||||
/** 是否需要处理 */
|
||||
const shouldHandle = (context: TraverseContext): boolean => {
|
||||
const { node } = context;
|
||||
// 线条数据
|
||||
if (
|
||||
node?.key &&
|
||||
['sourceNodeID', 'targetNodeID'].includes(node.key) &&
|
||||
node.parent?.parent?.key === 'edges'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// 节点数据
|
||||
if (
|
||||
node?.key === 'id' &&
|
||||
isExist(node.container?.type) &&
|
||||
isExist(node.container?.meta) &&
|
||||
isExist(node.container?.data)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// 变量数据
|
||||
if (
|
||||
node?.key === 'blockID' &&
|
||||
isExist(node.container?.name) &&
|
||||
node.container?.source === 'block-output'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* 替换节点ID
|
||||
* NOTICE: 该方法有副作用,会修改传入的json,防止深拷贝造成额外性能开销
|
||||
*/
|
||||
export const replaceNodeId = (
|
||||
json: WorkflowClipboardJSON,
|
||||
nodeReplaceMap: Map<string, string>,
|
||||
): WorkflowClipboardJSON => {
|
||||
traverse(json, context => {
|
||||
if (!shouldHandle(context)) {
|
||||
return;
|
||||
}
|
||||
const { node } = context;
|
||||
if (nodeReplaceMap.has(node.value)) {
|
||||
context.setValue(nodeReplaceMap.get(node.value));
|
||||
}
|
||||
});
|
||||
return json;
|
||||
};
|
||||
}
|
||||
|
||||
/** 生成唯一工作流JSON */
|
||||
export const generateUniqueWorkflow = (params: {
|
||||
json: WorkflowClipboardJSON;
|
||||
isUniqueId: (id: string) => boolean;
|
||||
}): WorkflowClipboardJSON => {
|
||||
const { json, isUniqueId } = params;
|
||||
const nodeIds = UniqueWorkflowUtils.getAllNodeIds(json);
|
||||
const nodeReplaceMap = UniqueWorkflowUtils.generateNodeReplaceMap(
|
||||
nodeIds,
|
||||
isUniqueId,
|
||||
);
|
||||
return UniqueWorkflowUtils.replaceNodeId(json, nodeReplaceMap);
|
||||
};
|
||||
Reference in New Issue
Block a user