feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { WorkflowMode } from '@coze-workflow/base/api';
|
||||
|
||||
export const ICON_URIS = {
|
||||
[WorkflowMode.Workflow]: 'plugin_icon/workflow.png',
|
||||
[WorkflowMode.ChatFlow]: 'plugin_icon/chatflow-icon.png',
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 @coze-arch/use-error-in-catch */
|
||||
import { get } from 'lodash-es';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { OperateType, workflowApi } from '@coze-workflow/base/api';
|
||||
|
||||
import { EncapsulateContext } from '../encapsulate-context';
|
||||
import {
|
||||
type EncapsulateWorkflowParams,
|
||||
type EncapsulateApiService,
|
||||
} from './types';
|
||||
import { ICON_URIS } from './constants';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateApiServiceImpl implements EncapsulateApiService {
|
||||
@inject(EncapsulateContext)
|
||||
private encapsulateContext: EncapsulateContext;
|
||||
|
||||
async encapsulateWorkflow({
|
||||
name,
|
||||
desc,
|
||||
json,
|
||||
flowMode,
|
||||
}: EncapsulateWorkflowParams) {
|
||||
try {
|
||||
const res = await workflowApi.EncapsulateWorkflow({
|
||||
space_id: this.encapsulateContext.spaceId,
|
||||
name,
|
||||
flow_mode: flowMode,
|
||||
desc,
|
||||
schema: JSON.stringify(json),
|
||||
icon_uri: ICON_URIS[flowMode],
|
||||
project_id: this.encapsulateContext.projectId,
|
||||
});
|
||||
|
||||
const workflowId = res.data?.workflow_id;
|
||||
if (!workflowId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
workflowId,
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async validateWorkflow(json) {
|
||||
try {
|
||||
const res = await workflowApi.EncapsulateWorkflow({
|
||||
space_id: this.encapsulateContext.spaceId,
|
||||
name: '',
|
||||
desc: '',
|
||||
icon_uri: '',
|
||||
schema: JSON.stringify(json),
|
||||
only_validate: true,
|
||||
});
|
||||
return res?.data?.validate_data || [];
|
||||
} catch (e) {
|
||||
return [
|
||||
{
|
||||
message: 'call validate api failed',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkflow(spaceId: string, workflowId: string, version?: string) {
|
||||
let json;
|
||||
// 有历史版本的场景获取历史版本的数据
|
||||
if (version) {
|
||||
const res = await workflowApi.GetHistorySchema({
|
||||
space_id: spaceId,
|
||||
workflow_id: workflowId,
|
||||
workflow_version: version,
|
||||
commit_id: '',
|
||||
type: OperateType.DraftOperate,
|
||||
});
|
||||
json = get(res, 'data.schema');
|
||||
} else {
|
||||
const res = await workflowApi.GetCanvasInfo({
|
||||
space_id: spaceId,
|
||||
workflow_id: workflowId,
|
||||
});
|
||||
|
||||
json = get(res, 'data.workflow.schema_json');
|
||||
}
|
||||
|
||||
if (!json) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(json);
|
||||
}
|
||||
}
|
||||
@@ -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 * from './types';
|
||||
export * from './encapsulate-api-service';
|
||||
@@ -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 ValidateErrorData,
|
||||
type WorkflowMode,
|
||||
} from '@coze-workflow/base/';
|
||||
import { type WorkflowJSON } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
export interface EncapsulateWorkflowParams {
|
||||
name: string;
|
||||
desc: string;
|
||||
json: WorkflowJSON;
|
||||
flowMode: WorkflowMode;
|
||||
}
|
||||
|
||||
export interface EncapsulateApiService {
|
||||
/**
|
||||
* 封装流程
|
||||
* @param name
|
||||
*/
|
||||
encapsulateWorkflow: (
|
||||
params: EncapsulateWorkflowParams,
|
||||
) => Promise<{ workflowId: string } | null>;
|
||||
/**
|
||||
* 校验流程
|
||||
* @param schema
|
||||
* @returns
|
||||
*/
|
||||
validateWorkflow: (json: WorkflowJSON) => Promise<ValidateErrorData[]>;
|
||||
/**
|
||||
* 获取流程数据
|
||||
* @param spaceId
|
||||
* @param workflowId
|
||||
* @returns
|
||||
*/
|
||||
getWorkflow: (
|
||||
spaceId: string,
|
||||
workflowId: string,
|
||||
version?: string,
|
||||
) => Promise<WorkflowJSON | null>;
|
||||
}
|
||||
|
||||
export const EncapsulateApiService = Symbol('EncapsulateApiService');
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
definePluginCreator,
|
||||
type PluginCreator,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { WorkflowEncapsulateContainerModule } from './workflow-encapsulate-container-module';
|
||||
import { EncapsulateValidatorsContainerModule } from './validators';
|
||||
import { EncapsulateValidateManager } from './validate';
|
||||
import {
|
||||
type GetGlobalStateOption,
|
||||
type GetNodeTemplateOption,
|
||||
type OnEncapsulateOption,
|
||||
} from './types';
|
||||
import { EncapsulateRenderContainerModule } from './render';
|
||||
import { EncapsulateContext } from './encapsulate-context';
|
||||
import { EncapsulateManager, EncapsulateService } from './encapsulate';
|
||||
|
||||
interface EncapsulatePluginOptions {
|
||||
getNodeTemplate?: GetNodeTemplateOption;
|
||||
getGlobalState?: GetGlobalStateOption;
|
||||
onEncapsulate?: OnEncapsulateOption;
|
||||
}
|
||||
|
||||
export const createWorkflowEncapsulatePlugin: PluginCreator<EncapsulatePluginOptions> =
|
||||
definePluginCreator<EncapsulatePluginOptions>({
|
||||
onInit(ctx, options) {
|
||||
ctx.get<EncapsulateManager>(EncapsulateManager).init();
|
||||
|
||||
ctx.get<EncapsulateContext>(EncapsulateContext).setPluginContext(ctx);
|
||||
|
||||
if (options.getNodeTemplate) {
|
||||
ctx
|
||||
.get<EncapsulateContext>(EncapsulateContext)
|
||||
.setGetNodeTemplate(options.getNodeTemplate);
|
||||
}
|
||||
|
||||
if (options.getGlobalState) {
|
||||
ctx
|
||||
.get<EncapsulateContext>(EncapsulateContext)
|
||||
.setGetGlobalState(options.getGlobalState);
|
||||
}
|
||||
|
||||
if (options.onEncapsulate) {
|
||||
ctx.get<EncapsulateService>(EncapsulateService).onEncapsulate(res => {
|
||||
options?.onEncapsulate?.(res, ctx);
|
||||
});
|
||||
}
|
||||
},
|
||||
onDispose(ctx) {
|
||||
ctx.get<EncapsulateValidateManager>(EncapsulateValidateManager).dispose();
|
||||
ctx.get<EncapsulateService>(EncapsulateService).dispose();
|
||||
ctx.get<EncapsulateManager>(EncapsulateManager).dispose();
|
||||
},
|
||||
containerModules: [
|
||||
WorkflowEncapsulateContainerModule,
|
||||
EncapsulateRenderContainerModule,
|
||||
EncapsulateValidatorsContainerModule,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { type StandardNodeType } from '@coze-workflow/base/types';
|
||||
import { WorkflowMode } from '@coze-workflow/base/api';
|
||||
import {
|
||||
EntityManager,
|
||||
type PluginContext,
|
||||
PlaygroundConfigEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { checkEncapsulateGray } from './utils';
|
||||
import {
|
||||
type NodeMeta,
|
||||
type GetGlobalStateOption,
|
||||
type GetNodeTemplateOption,
|
||||
} from './types';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateContext {
|
||||
@inject(EntityManager)
|
||||
protected readonly entityManager: EntityManager;
|
||||
|
||||
@inject(PlaygroundConfigEntity)
|
||||
private playgroundConfigEntity: PlaygroundConfigEntity;
|
||||
|
||||
private pluginContext: PluginContext;
|
||||
|
||||
private getGlobalStateOption: GetGlobalStateOption = () => ({
|
||||
spaceId: '',
|
||||
flowMode: WorkflowMode.Workflow,
|
||||
info: {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
||||
private getNodeMetaTemplateOption: GetNodeTemplateOption = () => () =>
|
||||
undefined;
|
||||
|
||||
setGetGlobalState(getGlobalState: GetGlobalStateOption) {
|
||||
this.getGlobalStateOption = getGlobalState;
|
||||
}
|
||||
|
||||
setGetNodeTemplate(getNodeTemplate: GetNodeTemplateOption) {
|
||||
this.getNodeMetaTemplateOption = getNodeTemplate;
|
||||
}
|
||||
|
||||
getNodeTemplate(type: StandardNodeType): NodeMeta | undefined {
|
||||
return this.getNodeMetaTemplateOption(this.pluginContext)(type);
|
||||
}
|
||||
|
||||
setPluginContext(context: PluginContext) {
|
||||
this.pluginContext = context;
|
||||
}
|
||||
|
||||
private get globalState() {
|
||||
return this.getGlobalStateOption(this.pluginContext);
|
||||
}
|
||||
|
||||
get spaceId() {
|
||||
return this.globalState?.spaceId;
|
||||
}
|
||||
|
||||
get flowName() {
|
||||
return this.globalState?.info?.name;
|
||||
}
|
||||
|
||||
get flowMode() {
|
||||
return this.globalState?.flowMode;
|
||||
}
|
||||
|
||||
get isChatFlow() {
|
||||
return this.globalState?.flowMode === WorkflowMode.ChatFlow;
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return checkEncapsulateGray() && !this.playgroundConfigEntity.readonly;
|
||||
}
|
||||
|
||||
get projectId() {
|
||||
return this.globalState?.projectId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const ENCAPSULATE_START_END_PAD = 60;
|
||||
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* 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 { groupBy, uniq } from 'lodash-es';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { type WorkflowJSON } from '@coze-workflow/base';
|
||||
import {
|
||||
type WorkflowLineEntity,
|
||||
WorkflowNodeLinesData,
|
||||
type WorkflowPortEntity,
|
||||
type WorkflowNodeEntity,
|
||||
WorkflowLinesManager,
|
||||
type WorkflowEdgeJSON,
|
||||
type WorkflowLinePortInfo,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { isNodesInSubCanvas } from '../utils/subcanvas';
|
||||
import { type ConnectPortsInfo } from './types';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateLinesService {
|
||||
@inject(WorkflowLinesManager) declare linesManager: WorkflowLinesManager;
|
||||
|
||||
/**
|
||||
* 获取有效的封装连接端口
|
||||
* @param nodes 封装范围内的节点数组
|
||||
* @throws Error 输入线不符合封装规则
|
||||
* @throws Error 输出线不符合封装规则
|
||||
* @returns 封装范围内的输入和输出端口对象
|
||||
*/
|
||||
getValidEncapsulateConnectPorts(
|
||||
nodes: WorkflowNodeEntity[],
|
||||
): ConnectPortsInfo {
|
||||
const inputLines = this.getEncapsulateNodesInputLines(nodes);
|
||||
const outputLines = this.getEncapsulateNodesOutputLines(nodes);
|
||||
|
||||
if (!this.validateEncapsulateLines(inputLines)) {
|
||||
throw new Error('输入线不符合封装规则');
|
||||
}
|
||||
|
||||
if (!this.validateEncapsulateLines(outputLines)) {
|
||||
throw new Error('输出线不符合封装规则');
|
||||
}
|
||||
|
||||
return {
|
||||
inputLines,
|
||||
outputLines,
|
||||
fromPorts: uniq(inputLines.map(_line => _line.fromPort)),
|
||||
toPorts: uniq(
|
||||
outputLines
|
||||
.map(_line => _line.toPort)
|
||||
.filter(Boolean) as WorkflowPortEntity[],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the specified ports to the given node.
|
||||
*
|
||||
* @param portInfo - Information about the ports to be connected.
|
||||
* @param node - The node to which the ports will be connected.
|
||||
*
|
||||
* This method iterates over the `fromPorts` and `toPorts` arrays, creating lines
|
||||
* between each port and the specified node using the `linesManager.createLine` method.
|
||||
*/
|
||||
|
||||
connectPortsToNode(portInfo: ConnectPortsInfo, node: WorkflowNodeEntity) {
|
||||
const { fromPorts, toPorts } = portInfo;
|
||||
|
||||
fromPorts.forEach(_fromPort => {
|
||||
this.linesManager.createLine({
|
||||
from: _fromPort.node.id,
|
||||
fromPort: _fromPort.portID,
|
||||
to: node.id,
|
||||
});
|
||||
});
|
||||
|
||||
toPorts.forEach(_toPort => {
|
||||
this.linesManager.createLine({
|
||||
from: node.id,
|
||||
to: _toPort.node.id,
|
||||
toPort: _toPort.portID,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封装范围内的所有输入线
|
||||
* @param nodes 流程图节点数组
|
||||
* @returns 封装范围内的输入线数组
|
||||
*/
|
||||
getEncapsulateNodesInputLines(
|
||||
nodes: WorkflowNodeEntity[],
|
||||
): WorkflowLineEntity[] {
|
||||
return uniq(
|
||||
nodes
|
||||
.map(_node => _node.getData(WorkflowNodeLinesData).inputLines)
|
||||
.flat(),
|
||||
).filter(
|
||||
_line =>
|
||||
_line.from &&
|
||||
!nodes.includes(_line.from) &&
|
||||
(isNodesInSubCanvas(nodes) || !this.isSubCanvasLinkLine(_line)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封装范围内的所有输出线
|
||||
* @param nodes 流程图节点数组
|
||||
* @returns 封装范围内的输出线数组
|
||||
*/
|
||||
getEncapsulateNodesOutputLines(
|
||||
nodes: WorkflowNodeEntity[],
|
||||
): WorkflowLineEntity[] {
|
||||
return uniq(
|
||||
nodes
|
||||
.map(_node => _node.getData(WorkflowNodeLinesData).outputLines)
|
||||
.flat(),
|
||||
).filter(
|
||||
_line =>
|
||||
_line.to &&
|
||||
!nodes.includes(_line.to) &&
|
||||
(isNodesInSubCanvas(nodes) || !this.isSubCanvasLinkLine(_line)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验封装范围内的输入和输出线是否符合封装规则
|
||||
*/
|
||||
validateEncapsulateLines(lines: WorkflowLineEntity[]): boolean {
|
||||
const isFromPortUniq =
|
||||
uniq(lines.map(_line => _line.fromPort)).length === 1;
|
||||
const isToPortUniq = uniq(lines.map(_line => _line.toPort)).length === 1;
|
||||
return isFromPortUniq || isToPortUniq;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建封装连线
|
||||
* @param ports
|
||||
* @param subFlowNode
|
||||
* @returns
|
||||
*/
|
||||
createEncapsulateLines(
|
||||
ports: ConnectPortsInfo,
|
||||
subFlowNode: WorkflowNodeEntity,
|
||||
) {
|
||||
const inputLines: WorkflowLineEntity[] = [];
|
||||
const outputLines: WorkflowLineEntity[] = [];
|
||||
ports.inputLines.forEach(line => {
|
||||
const inputLine = this.linesManager.createLine({
|
||||
from: line.from.id,
|
||||
fromPort: line.fromPort.portID,
|
||||
to: subFlowNode.id,
|
||||
});
|
||||
|
||||
if (!inputLine) {
|
||||
throw new Error('create input line failed');
|
||||
}
|
||||
inputLines.push(inputLine);
|
||||
});
|
||||
|
||||
ports.outputLines.forEach(line => {
|
||||
if (!line.to) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outputLine = this.linesManager.createLine({
|
||||
from: subFlowNode.id,
|
||||
to: line.to.id,
|
||||
toPort: line.toPort?.portID,
|
||||
});
|
||||
|
||||
if (!outputLine) {
|
||||
throw new Error('create output line failed');
|
||||
}
|
||||
|
||||
outputLines.push(outputLine);
|
||||
});
|
||||
|
||||
return {
|
||||
inputLines,
|
||||
outputLines,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建解封线
|
||||
* @param options
|
||||
*/
|
||||
createDecapsulateLines(options: {
|
||||
node: WorkflowNodeEntity;
|
||||
workflowJSON: WorkflowJSON;
|
||||
startNodeId: string;
|
||||
endNodeId: string;
|
||||
idsMap: Map<string, string>;
|
||||
}) {
|
||||
const { node, startNodeId, endNodeId, idsMap, workflowJSON } = options;
|
||||
const edges = [
|
||||
...workflowJSON.edges,
|
||||
// 子画布中的连线
|
||||
...workflowJSON.nodes
|
||||
.map(n => n.edges)
|
||||
.filter(Boolean)
|
||||
.flat(),
|
||||
] as WorkflowEdgeJSON[];
|
||||
|
||||
const edgesGroup = groupBy(edges, edge => {
|
||||
if (edge.sourceNodeID === startNodeId) {
|
||||
return 'input';
|
||||
}
|
||||
if (edge.targetNodeID === endNodeId) {
|
||||
return 'output';
|
||||
}
|
||||
|
||||
return 'internal';
|
||||
});
|
||||
|
||||
// 内部连线
|
||||
const internalLines = this.createDecapsulateInternalLines(
|
||||
edgesGroup.internal || [],
|
||||
idsMap,
|
||||
);
|
||||
|
||||
// 输入连线
|
||||
const inputLines = this.createDecapsulateInputLines(
|
||||
edgesGroup.input || [],
|
||||
node,
|
||||
idsMap,
|
||||
);
|
||||
|
||||
// 输出连线
|
||||
const outputLines = this.createDecapsulateOutputLines(
|
||||
edgesGroup.output || [],
|
||||
node,
|
||||
idsMap,
|
||||
);
|
||||
|
||||
return {
|
||||
inputLines,
|
||||
outputLines,
|
||||
internalLines,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建解封内部连线
|
||||
* @param internalEdges
|
||||
* @param idsMap
|
||||
* @returns
|
||||
*/
|
||||
private createDecapsulateInternalLines(
|
||||
internalEdges: WorkflowEdgeJSON[],
|
||||
idsMap: Map<string, string>,
|
||||
) {
|
||||
const createdLines: WorkflowLineEntity[] = [];
|
||||
internalEdges.forEach(edge => {
|
||||
if (!idsMap.has(edge.sourceNodeID) || !idsMap.has(edge.targetNodeID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = {
|
||||
from: idsMap.get(edge.sourceNodeID) as string,
|
||||
to: idsMap.get(edge.targetNodeID) as string,
|
||||
fromPort: edge.sourcePortID,
|
||||
toPort: edge.targetPortID,
|
||||
};
|
||||
|
||||
if (line.fromPort === 'loop-function-inline-output') {
|
||||
line.from = `LoopFunction_${line.from}`;
|
||||
}
|
||||
|
||||
if (line.toPort === 'loop-function-inline-input') {
|
||||
line.to = `LoopFunction_${line.to}`;
|
||||
}
|
||||
|
||||
if (line.fromPort === 'batch-function-inline-output') {
|
||||
line.from = `BatchFunction_${line.from}`;
|
||||
}
|
||||
|
||||
if (line.toPort === 'batch-function-inline-input') {
|
||||
line.to = `BatchFunction_${line.to}`;
|
||||
}
|
||||
|
||||
this.createLine(line, createdLines);
|
||||
});
|
||||
return createdLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建解封输入连线
|
||||
* @param inputEdges
|
||||
* @param node
|
||||
* @param idsMap
|
||||
* @returns
|
||||
*/
|
||||
private createDecapsulateInputLines(
|
||||
inputEdges: WorkflowEdgeJSON[],
|
||||
node: WorkflowNodeEntity,
|
||||
idsMap: Map<string, string>,
|
||||
) {
|
||||
const createdLines: WorkflowLineEntity[] = [];
|
||||
const { inputLines } = node.getData(WorkflowNodeLinesData);
|
||||
|
||||
// 封装多开头+封装上游多个输入 不创建连线
|
||||
if (inputLines.length > 1 && inputEdges.length > 1) {
|
||||
return createdLines;
|
||||
}
|
||||
|
||||
inputLines.forEach(inputLine => {
|
||||
inputEdges.forEach(edge => {
|
||||
if (!idsMap.has(edge.targetNodeID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createLine(
|
||||
{
|
||||
from: inputLine.from.id,
|
||||
fromPort: inputLine.fromPort.portID,
|
||||
to: idsMap.get(edge.targetNodeID) as string,
|
||||
toPort: edge.targetPortID,
|
||||
},
|
||||
createdLines,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return createdLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建解封输出连线
|
||||
* @param outputEdges
|
||||
* @param node
|
||||
* @param idsMap
|
||||
* @returns
|
||||
*/
|
||||
private createDecapsulateOutputLines(
|
||||
outputEdges: WorkflowEdgeJSON[],
|
||||
node: WorkflowNodeEntity,
|
||||
idsMap: Map<string, string>,
|
||||
) {
|
||||
const createdLines: WorkflowLineEntity[] = [];
|
||||
const { outputLines } = node.getData(WorkflowNodeLinesData);
|
||||
|
||||
// 封装多输出+封装下游多个输出 不创建连线
|
||||
if (outputLines.length > 1 && outputEdges.length > 1) {
|
||||
return createdLines;
|
||||
}
|
||||
|
||||
outputLines.forEach(outputLine => {
|
||||
outputEdges.forEach(edge => {
|
||||
if (!idsMap.has(edge.sourceNodeID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createLine(
|
||||
{
|
||||
from: idsMap.get(edge.sourceNodeID) as string,
|
||||
fromPort: edge.sourcePortID,
|
||||
to: outputLine.to?.id,
|
||||
toPort: outputLine.toPort?.portID,
|
||||
},
|
||||
createdLines,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return createdLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建连线
|
||||
* @param info
|
||||
* @param createdLines
|
||||
*/
|
||||
private createLine(
|
||||
info: WorkflowLinePortInfo,
|
||||
createdLines?: WorkflowLineEntity[],
|
||||
) {
|
||||
const line = this.linesManager.createLine(info);
|
||||
|
||||
if (line && createdLines) {
|
||||
createdLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
private isSubCanvasLinkLine(line: WorkflowLineEntity): boolean {
|
||||
if (
|
||||
// loop内的最后一根线
|
||||
line.toPort?.portID === 'loop-function-inline-input' ||
|
||||
// loop内的第一根线
|
||||
line.fromPort.portID === 'loop-function-inline-output'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
line.toPort?.portID === 'batch-function-inline-input' ||
|
||||
line.fromPort.portID === 'batch-function-inline-output'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
line.fromPort.portID === 'loop-output-to-function' &&
|
||||
line.toPort?.portID === 'loop-function-input'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
line.fromPort.portID === 'batch-output-to-function' &&
|
||||
line.toPort?.portID === 'batch-function-input'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { DisposableCollection } from '@flowgram-adapter/common';
|
||||
|
||||
import { type EncapsulateManager } from './types';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateManagerImpl implements EncapsulateManager {
|
||||
private toDispose: DisposableCollection = new DisposableCollection();
|
||||
|
||||
init() {
|
||||
this.toDispose.pushAll([]);
|
||||
}
|
||||
dispose() {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-params */
|
||||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
import { get, set } from 'lodash-es';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import {
|
||||
type DragNodeOperationValue,
|
||||
FreeOperationType,
|
||||
HistoryService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
PlaygroundContext,
|
||||
TransformData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowNodeJSON,
|
||||
type WorkflowNodeEntity,
|
||||
WorkflowDocument,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { delay, type IPoint } from '@flowgram-adapter/common';
|
||||
import { WorkflowNodesService } from '@coze-workflow/nodes';
|
||||
import { StandardNodeType } from '@coze-workflow/base/types';
|
||||
|
||||
import { getNodesParentId } from '../utils/get-nodes-parent-id';
|
||||
import { getNodePoint } from '../utils';
|
||||
import { EncapsulateGenerateService } from '../generate';
|
||||
import { EncapsulateContext } from '../encapsulate-context';
|
||||
import { ENCAPSULATE_START_END_PAD } from './constants';
|
||||
|
||||
/**
|
||||
* 节点操作服务
|
||||
*/
|
||||
@injectable()
|
||||
export class EncapsulateNodesService {
|
||||
@inject(WorkflowDocument)
|
||||
private workflowDocument: WorkflowDocument;
|
||||
|
||||
@inject(WorkflowNodesService)
|
||||
private workflowNodesService: WorkflowNodesService;
|
||||
|
||||
@inject(EncapsulateContext)
|
||||
private encapsulateContext: EncapsulateContext;
|
||||
|
||||
@inject(EncapsulateGenerateService)
|
||||
private encapsulateGenerateService: EncapsulateGenerateService;
|
||||
|
||||
@inject(HistoryService)
|
||||
private historyService: HistoryService;
|
||||
|
||||
@inject(PlaygroundContext)
|
||||
private playgroundContext: PlaygroundContext;
|
||||
|
||||
/**
|
||||
* 获取一批节点的中心点
|
||||
* @param nodes 节点数组
|
||||
* @returns 中心点的坐标对象
|
||||
*/
|
||||
getNodesMiddlePoint(
|
||||
nodes: Array<WorkflowNodeEntity | WorkflowNodeJSON>,
|
||||
): IPoint {
|
||||
if (nodes.length === 0) {
|
||||
throw new Error('选中节点不能为空');
|
||||
}
|
||||
|
||||
const rect = this.getNodesRect(nodes);
|
||||
|
||||
return {
|
||||
x: rect.x + rect.width / 2,
|
||||
y: rect.y + rect.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建解封节点
|
||||
* @param sourceNode 原始被解封的节点
|
||||
* @param nodes 要生成节点的node json数据
|
||||
* @returns
|
||||
*/
|
||||
async createDecapsulateNodes(
|
||||
sourceNode: WorkflowNodeEntity,
|
||||
nodes: WorkflowNodeJSON[],
|
||||
parentId?: string,
|
||||
) {
|
||||
const { startNode, endNode, middleNodes } = this.groupNodes(nodes);
|
||||
if (!startNode || !endNode) {
|
||||
throw new Error('start or end node not found');
|
||||
}
|
||||
const nodePoint = getNodePoint(sourceNode);
|
||||
const centerPoint = this.getNodesMiddlePoint(middleNodes);
|
||||
// 平移到中心
|
||||
const translate: IPoint = {
|
||||
x: nodePoint.x - centerPoint.x,
|
||||
y: nodePoint.y - centerPoint.y,
|
||||
};
|
||||
|
||||
const idsMap = new Map<string, string>();
|
||||
for (const nodeJSON of middleNodes) {
|
||||
await this.createDecapsulateNode(nodeJSON, translate, idsMap, parentId);
|
||||
}
|
||||
|
||||
return {
|
||||
idsMap,
|
||||
startNode,
|
||||
endNode,
|
||||
middleNodes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建封装节点
|
||||
* @param name
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
async createEncapsulateNode(
|
||||
workflowId: string,
|
||||
name: string,
|
||||
nodes: WorkflowNodeEntity[],
|
||||
) {
|
||||
const pos = this.getNodesMiddlePoint(nodes);
|
||||
const type = StandardNodeType.SubWorkflow;
|
||||
const nodeJSON = this.encapsulateGenerateService.generateSubWorkflowNode({
|
||||
spaceId: this.encapsulateContext.spaceId,
|
||||
workflowId,
|
||||
name,
|
||||
desc: name,
|
||||
});
|
||||
await this.beforeCreate(type, nodeJSON);
|
||||
const subFlowNode = await this.workflowDocument.createWorkflowNodeByType(
|
||||
type,
|
||||
pos,
|
||||
nodeJSON,
|
||||
getNodesParentId(nodes),
|
||||
);
|
||||
return subFlowNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过json创建一个新节点
|
||||
* @param json
|
||||
* @returns
|
||||
*/
|
||||
async createDecapsulateNode(
|
||||
nodeJSON: WorkflowNodeJSON,
|
||||
translate: IPoint,
|
||||
idsMap?: Map<string, string>,
|
||||
parentId?: string,
|
||||
) {
|
||||
const json = this.getDecapsulateNodeJSON(nodeJSON, translate, idsMap);
|
||||
await this.beforeCreate(json.type as StandardNodeType, json);
|
||||
const newNode = await this.workflowDocument.createWorkflowNode(
|
||||
json,
|
||||
true,
|
||||
parentId || 'root',
|
||||
);
|
||||
return newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解封布局,将所有节点向外平移一半的宽高
|
||||
*/
|
||||
decapsulateLayout(sourceNode: WorkflowNodeEntity, nodes: WorkflowNodeJSON[]) {
|
||||
const point = getNodePoint(sourceNode);
|
||||
const { width, height } = this.getNodesRect(
|
||||
nodes.filter(
|
||||
n =>
|
||||
![StandardNodeType.Start, StandardNodeType.End].includes(
|
||||
n.type as StandardNodeType,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const value: DragNodeOperationValue = {
|
||||
ids: [],
|
||||
value: [],
|
||||
oldValue: [],
|
||||
};
|
||||
|
||||
this.workflowDocument.getAllNodes().reduce((previousValue, node) => {
|
||||
const nodePoint = getNodePoint(node);
|
||||
|
||||
if (nodePoint.x > point.x) {
|
||||
nodePoint.x += width / 2;
|
||||
}
|
||||
|
||||
if (nodePoint.y > point.y) {
|
||||
nodePoint.y += height / 2;
|
||||
}
|
||||
|
||||
if (nodePoint.x < point.x) {
|
||||
nodePoint.x -= width / 2;
|
||||
}
|
||||
|
||||
if (nodePoint.y < point.y) {
|
||||
nodePoint.y -= height / 2;
|
||||
}
|
||||
|
||||
const transformData = node.getData<TransformData>(TransformData);
|
||||
previousValue.ids.push(node.id);
|
||||
previousValue.value.push(nodePoint);
|
||||
previousValue.oldValue.push({
|
||||
x: transformData.position.x,
|
||||
y: transformData.position.y,
|
||||
});
|
||||
return previousValue;
|
||||
}, value);
|
||||
|
||||
this.historyService.pushOperation({
|
||||
type: FreeOperationType.dragNodes,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解封后的节点JSON
|
||||
* @param nodeJSON 原来的node json
|
||||
* @param translate 平移的距离
|
||||
* @param idsMap ids映射
|
||||
* @returns
|
||||
*/
|
||||
private getDecapsulateNodeJSON(
|
||||
nodeJSON: WorkflowNodeJSON,
|
||||
translate: IPoint,
|
||||
idsMap?: Map<string, string>,
|
||||
): WorkflowNodeJSON {
|
||||
const id = this.workflowNodesService.createUniqID();
|
||||
|
||||
if (idsMap) {
|
||||
idsMap.set(nodeJSON.id, id);
|
||||
}
|
||||
|
||||
nodeJSON.id = id;
|
||||
const title = get(nodeJSON, 'data.nodeMeta.title') || nodeJSON.type;
|
||||
const uniqueTitle = this.workflowNodesService.createUniqTitle(title);
|
||||
set(nodeJSON, 'data.nodeMeta.title', uniqueTitle);
|
||||
|
||||
if (!nodeJSON.meta) {
|
||||
nodeJSON.meta = {};
|
||||
}
|
||||
|
||||
nodeJSON.meta.position = this.getDecapsulatePosition(nodeJSON, translate);
|
||||
|
||||
if (nodeJSON.blocks) {
|
||||
nodeJSON.blocks = nodeJSON.blocks.map(block =>
|
||||
this.getDecapsulateNodeJSON(block, translate, idsMap),
|
||||
);
|
||||
}
|
||||
return nodeJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解封后的节点坐标
|
||||
* @param nodeJSON
|
||||
* @param translate
|
||||
* @returns
|
||||
*/
|
||||
private getDecapsulatePosition(
|
||||
nodeJSON: WorkflowNodeJSON,
|
||||
translate: IPoint,
|
||||
) {
|
||||
const position = nodeJSON.meta?.position || {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
return {
|
||||
x: position.x + translate.x,
|
||||
y: position.y + translate.y,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点分组
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
private groupNodes(nodes: WorkflowNodeJSON[]) {
|
||||
const middleNodes: WorkflowNodeJSON[] = [];
|
||||
let startNode: WorkflowNodeJSON | undefined;
|
||||
let endNode: WorkflowNodeJSON | undefined;
|
||||
nodes.forEach(node => {
|
||||
if (node.type === StandardNodeType.Start) {
|
||||
startNode = node;
|
||||
} else if (node.type === StandardNodeType.End) {
|
||||
endNode = node;
|
||||
} else {
|
||||
middleNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
startNode,
|
||||
endNode,
|
||||
middleNodes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个节点合起来的宽高
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
private getNodesRect(nodes: Array<WorkflowNodeEntity | WorkflowNodeJSON>) {
|
||||
const x1 = Math.min(...nodes.map(node => getNodePoint(node).x));
|
||||
const x2 = Math.max(...nodes.map(node => getNodePoint(node).x));
|
||||
const y1 = Math.min(...nodes.map(node => getNodePoint(node).y));
|
||||
const y2 = Math.max(...nodes.map(node => getNodePoint(node).y));
|
||||
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
x: x1,
|
||||
y: y1,
|
||||
};
|
||||
}
|
||||
|
||||
getEncapsulateStartEndRects(nodes: WorkflowNodeEntity[]) {
|
||||
const boundaryNodes = this.getBoundaryNodes(nodes);
|
||||
|
||||
if (!boundaryNodes.left || !boundaryNodes.right) {
|
||||
throw new Error('boundaryNodes left or right node not found');
|
||||
}
|
||||
|
||||
return {
|
||||
start: this.getEncapsulateStartEndRect(
|
||||
StandardNodeType.Start,
|
||||
boundaryNodes.left,
|
||||
),
|
||||
end: this.getEncapsulateStartEndRect(
|
||||
StandardNodeType.End,
|
||||
boundaryNodes.right,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除节点
|
||||
* @param node */
|
||||
async deleteNodes(nodes: WorkflowNodeEntity[]) {
|
||||
nodes.forEach(node => {
|
||||
node.dispose();
|
||||
});
|
||||
// 有些节点删除会有删除出相关连线,等待其执行完成
|
||||
await delay(10);
|
||||
}
|
||||
|
||||
private async beforeCreate(
|
||||
nodeType: string,
|
||||
nodeJSON: Partial<WorkflowNodeJSON>,
|
||||
) {
|
||||
if (!nodeType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeRegistry = this.workflowDocument.getNodeRegister(nodeType);
|
||||
|
||||
await nodeRegistry?.onInit?.(
|
||||
nodeJSON as WorkflowNodeJSON,
|
||||
this.playgroundContext,
|
||||
);
|
||||
}
|
||||
|
||||
private getEncapsulateStartEndRect(
|
||||
type: StandardNodeType.Start | StandardNodeType.End,
|
||||
boundaryNode: WorkflowNodeEntity,
|
||||
) {
|
||||
const node = this.workflowDocument
|
||||
.getAllNodes()
|
||||
.find(item => item.flowNodeType === type);
|
||||
|
||||
if (!node) {
|
||||
throw new Error('getEncapsulateStartEndPoint node not found');
|
||||
}
|
||||
|
||||
const nodeReact = this.getNodeRect(node);
|
||||
const boundaryNodeReact = this.getNodeRect(boundaryNode);
|
||||
|
||||
const translateX =
|
||||
type === StandardNodeType.Start
|
||||
? -ENCAPSULATE_START_END_PAD - nodeReact.width
|
||||
: ENCAPSULATE_START_END_PAD + boundaryNodeReact.width;
|
||||
const translateY = (boundaryNodeReact.height - nodeReact.height) / 2;
|
||||
|
||||
return {
|
||||
x: boundaryNodeReact.x + translateX,
|
||||
y: boundaryNodeReact.y + translateY,
|
||||
width: nodeReact.width,
|
||||
height: nodeReact.height,
|
||||
};
|
||||
}
|
||||
|
||||
private getNodeRect(node: WorkflowNodeEntity) {
|
||||
const transformData = node.getData<TransformData>(TransformData);
|
||||
return {
|
||||
x: transformData.bounds.left,
|
||||
y: transformData.bounds.top,
|
||||
width: transformData.bounds.width,
|
||||
height: transformData.bounds.height,
|
||||
};
|
||||
}
|
||||
|
||||
private getBoundaryNodes(nodes: WorkflowNodeEntity[]) {
|
||||
const boundaryNodes: {
|
||||
left?: WorkflowNodeEntity;
|
||||
right?: WorkflowNodeEntity;
|
||||
} = {};
|
||||
|
||||
nodes.reduce((previousValue, node) => {
|
||||
if (!previousValue.left) {
|
||||
previousValue.left = node;
|
||||
}
|
||||
|
||||
if (!previousValue.right) {
|
||||
previousValue.right = node;
|
||||
}
|
||||
|
||||
if (getNodePoint(node).x < getNodePoint(previousValue.left).x) {
|
||||
previousValue.left = node;
|
||||
}
|
||||
|
||||
if (getNodePoint(node).x > getNodePoint(previousValue.right).x) {
|
||||
previousValue.right = node;
|
||||
}
|
||||
|
||||
return previousValue;
|
||||
}, boundaryNodes);
|
||||
|
||||
return boundaryNodes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { WorkflowMode } from '@coze-workflow/base/api';
|
||||
import {
|
||||
reporter,
|
||||
StandardNodeType,
|
||||
type WorkflowJSON,
|
||||
} from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Toast } from '@coze-arch/coze-design';
|
||||
import { HistoryService } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowDocument,
|
||||
type WorkflowNodeEntity,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { delay, Emitter, logger } from '@flowgram-adapter/common';
|
||||
|
||||
import { EncapsulateValidateService } from '../validate';
|
||||
import { isNodeInSubCanvas } from '../utils/subcanvas';
|
||||
import { randomNameSuffix } from '../utils/random-name-suffix';
|
||||
import { hasSubCanvasNodes } from '../utils/has-sub-canvas-nodes';
|
||||
import { getSubWorkflowInfo } from '../utils';
|
||||
import { EncapsulateGenerateService } from '../generate';
|
||||
import { EncapsulateContext } from '../encapsulate-context';
|
||||
import { EncapsulateApiService } from '../api';
|
||||
import {
|
||||
type EncapsulateErrorResult,
|
||||
type EncapsulateResult,
|
||||
type EncapsulateService,
|
||||
} from './types';
|
||||
import { EncapsulateVariableService } from './encapsulate-variable-service';
|
||||
import { EncapsulateNodesService } from './encapsulate-nodes-service';
|
||||
import { EncapsulateLinesService } from './encapsulate-lines-service';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateServiceImpl implements EncapsulateService {
|
||||
private onEncapsulateEmitter = new Emitter<EncapsulateResult>();
|
||||
|
||||
public readonly onEncapsulate = this.onEncapsulateEmitter.event;
|
||||
|
||||
@inject(EncapsulateValidateService)
|
||||
private encapsulateValidateService: EncapsulateValidateService;
|
||||
|
||||
@inject(EncapsulateGenerateService)
|
||||
private encapsulateGenerateService: EncapsulateGenerateService;
|
||||
|
||||
@inject(WorkflowSelectService)
|
||||
private workflowSelectService: WorkflowSelectService;
|
||||
|
||||
@inject(EncapsulateNodesService)
|
||||
private encapsulateNodesService: EncapsulateNodesService;
|
||||
|
||||
@inject(EncapsulateApiService)
|
||||
private encapsulateApiService: EncapsulateApiService;
|
||||
|
||||
@inject(EncapsulateContext)
|
||||
private encapsulateContext: EncapsulateContext;
|
||||
|
||||
@inject(EncapsulateLinesService)
|
||||
private encapsulateLinesService: EncapsulateLinesService;
|
||||
|
||||
@inject(EncapsulateVariableService)
|
||||
private encapsulateVariableService: EncapsulateVariableService;
|
||||
|
||||
@inject(HistoryService)
|
||||
private historyService: HistoryService;
|
||||
|
||||
@inject(WorkflowDocument)
|
||||
private workflowDocument: WorkflowDocument;
|
||||
|
||||
private encapsulating = false;
|
||||
private decapsulating = false;
|
||||
|
||||
validate() {
|
||||
const { selectedNodes } = this.workflowSelectService;
|
||||
return this.encapsulateValidateService.validate(selectedNodes);
|
||||
}
|
||||
|
||||
canEncapsulate(): boolean {
|
||||
return this.encapsulateContext.enabled;
|
||||
}
|
||||
|
||||
async encapsulate() {
|
||||
if (!this.canEncapsulate()) {
|
||||
return this.encapsulateError('encapsulate is not enabled');
|
||||
}
|
||||
|
||||
if (this.encapsulating) {
|
||||
return this.encapsulateError('encapsulating');
|
||||
}
|
||||
|
||||
const name = `${this.encapsulateContext.flowName || ''}_sub_${randomNameSuffix()}`;
|
||||
const { selectedNodes } = this.workflowSelectService;
|
||||
|
||||
if (selectedNodes.length < 2) {
|
||||
return this.encapsulateError('at least 2 nodes');
|
||||
}
|
||||
|
||||
reporter.event({ eventName: 'workflow_encapsulate' });
|
||||
|
||||
this.encapsulating = true;
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await this.encapsulateNodes(name, selectedNodes);
|
||||
// 封装完成选中封装后的节点
|
||||
if (res.success) {
|
||||
this.workflowSelectService.selectNode(res.subFlowNode);
|
||||
}
|
||||
|
||||
this.onEncapsulateEmitter.fire(res);
|
||||
} finally {
|
||||
this.encapsulating = false;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
canDecapsulate(node: WorkflowNodeEntity) {
|
||||
return (
|
||||
this.encapsulateContext.enabled &&
|
||||
node.flowNodeType === StandardNodeType.SubWorkflow
|
||||
);
|
||||
}
|
||||
|
||||
async decapsulate(node: WorkflowNodeEntity) {
|
||||
if (!node || !this.canDecapsulate(node) || this.decapsulating) {
|
||||
return;
|
||||
}
|
||||
|
||||
reporter.event({ eventName: 'workflow_decapsulate' });
|
||||
this.decapsulating = true;
|
||||
try {
|
||||
await this.decapsulateNode(node);
|
||||
} finally {
|
||||
this.decapsulating = false;
|
||||
}
|
||||
}
|
||||
|
||||
async decapsulateNode(node: WorkflowNodeEntity) {
|
||||
const info = getSubWorkflowInfo(node);
|
||||
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取流程数据
|
||||
const workflow = await this.encapsulateApiService.getWorkflow(
|
||||
info.spaceId && info.spaceId !== '0'
|
||||
? info.spaceId
|
||||
: this.encapsulateContext.spaceId,
|
||||
info.workflowId,
|
||||
info.workflowVersion,
|
||||
);
|
||||
|
||||
if (!workflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workflow.nodes.length <= 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果解封子画布里面的节点,需要校验是否有子画布
|
||||
if (
|
||||
isNodeInSubCanvas(node) &&
|
||||
hasSubCanvasNodes(this.workflowDocument, workflow.nodes)
|
||||
) {
|
||||
Toast.warning(
|
||||
I18n.t(
|
||||
'workflow_encapsulate_toast_batch_or_loop',
|
||||
undefined,
|
||||
'在循环/批处理中解散的工作流不能包含循环或批处理节点',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.historyService.startTransaction();
|
||||
|
||||
// 以节点为中心向外扩张
|
||||
this.encapsulateNodesService.decapsulateLayout(node, workflow.nodes);
|
||||
|
||||
// 创建节点
|
||||
const { idsMap, startNode, endNode, middleNodes } =
|
||||
await this.encapsulateNodesService.createDecapsulateNodes(
|
||||
node,
|
||||
workflow.nodes,
|
||||
node.parent?.id,
|
||||
);
|
||||
|
||||
// 解封后更新变量引用
|
||||
this.encapsulateVariableService.updateVarsAfterDecapsulate(node, {
|
||||
idsMap,
|
||||
startNode,
|
||||
endNode,
|
||||
middleNodes,
|
||||
});
|
||||
|
||||
// 创建连线
|
||||
this.encapsulateLinesService.createDecapsulateLines({
|
||||
node,
|
||||
workflowJSON: workflow as WorkflowJSON,
|
||||
startNodeId: startNode.id,
|
||||
endNodeId: endNode.id,
|
||||
idsMap,
|
||||
});
|
||||
|
||||
await delay(30);
|
||||
|
||||
// 移除老节点
|
||||
await this.encapsulateNodesService.deleteNodes([node]);
|
||||
this.historyService.endTransaction();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.onEncapsulateEmitter.dispose();
|
||||
}
|
||||
|
||||
private async encapsulateNodes(
|
||||
name: string,
|
||||
nodes: WorkflowNodeEntity[],
|
||||
): Promise<EncapsulateResult> {
|
||||
const ports =
|
||||
this.encapsulateLinesService.getValidEncapsulateConnectPorts(nodes);
|
||||
|
||||
const encapsulateVars =
|
||||
this.encapsulateVariableService.getEncapsulateVars(nodes);
|
||||
|
||||
const startEndRects =
|
||||
this.encapsulateNodesService.getEncapsulateStartEndRects(nodes);
|
||||
|
||||
// 生成 json,并更新 JSON 内的变量引用
|
||||
const json = await this.encapsulateGenerateService.generateWorkflowJSON(
|
||||
nodes,
|
||||
{
|
||||
startEndRects,
|
||||
},
|
||||
);
|
||||
|
||||
let workflowId;
|
||||
let flowMode = WorkflowMode.Workflow;
|
||||
if (
|
||||
this.encapsulateContext.isChatFlow &&
|
||||
encapsulateVars.startVars.USER_INPUT &&
|
||||
encapsulateVars.startVars.CONVERSATION_NAME
|
||||
) {
|
||||
flowMode = WorkflowMode.ChatFlow;
|
||||
}
|
||||
|
||||
try {
|
||||
// 保存流程
|
||||
const res = await this.encapsulateApiService.encapsulateWorkflow({
|
||||
name,
|
||||
desc: name,
|
||||
json,
|
||||
flowMode,
|
||||
});
|
||||
workflowId = res?.workflowId;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return this.encapsulateError('encapsulate workflow failed');
|
||||
}
|
||||
|
||||
if (!workflowId) {
|
||||
return this.encapsulateError(
|
||||
'encapsulate workflow failed no workflowId returned',
|
||||
);
|
||||
}
|
||||
|
||||
this.historyService.startTransaction();
|
||||
// 替换成调用流程节点
|
||||
const subFlowNode =
|
||||
await this.encapsulateNodesService.createEncapsulateNode(
|
||||
workflowId,
|
||||
name,
|
||||
nodes,
|
||||
);
|
||||
|
||||
// 更新变量引用(上下游 + 封装节点本身输入)
|
||||
this.encapsulateVariableService.updateVarsAfterEncapsulate(
|
||||
subFlowNode,
|
||||
nodes,
|
||||
encapsulateVars,
|
||||
);
|
||||
|
||||
// 移除老节点
|
||||
await this.encapsulateNodesService.deleteNodes(nodes);
|
||||
|
||||
// 生成新连线
|
||||
const { inputLines, outputLines } =
|
||||
this.encapsulateLinesService.createEncapsulateLines(ports, subFlowNode);
|
||||
await delay(30);
|
||||
|
||||
this.historyService.endTransaction();
|
||||
return {
|
||||
success: true,
|
||||
subFlowNode,
|
||||
inputLines,
|
||||
outputLines,
|
||||
workflowId,
|
||||
projectId: this.encapsulateContext.projectId,
|
||||
};
|
||||
}
|
||||
|
||||
private encapsulateError(message: string): EncapsulateErrorResult {
|
||||
return {
|
||||
success: false,
|
||||
message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-len */
|
||||
import { get, set, uniq } from 'lodash-es';
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { produce } from 'immer';
|
||||
import {
|
||||
type FormModelV2,
|
||||
isFormV2,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type FlowNodeJSON,
|
||||
type FlowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
WorkflowDocument,
|
||||
type WorkflowJSON,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type UpdateRefInfo,
|
||||
WorkflowNodeRefVariablesData,
|
||||
WorkflowVariableService,
|
||||
type WorkflowVariable,
|
||||
variableUtils,
|
||||
isGlobalVariableKey,
|
||||
} from '@coze-workflow/variable';
|
||||
import {
|
||||
type InputValueDTO,
|
||||
type InputValueVO,
|
||||
StandardNodeType,
|
||||
type ValueExpression,
|
||||
ValueExpressionType,
|
||||
} from '@coze-workflow/base';
|
||||
|
||||
import {
|
||||
traverseRefsInNodeJSON,
|
||||
sortVariables,
|
||||
variableOrder,
|
||||
} from '../utils/variable';
|
||||
import { getNodesWithSubCanvas } from '../utils/get-nodes-with-sub-canvas';
|
||||
import { EncapsulateContext } from '../encapsulate-context';
|
||||
import {
|
||||
type VariableMap,
|
||||
type EncapsulateVars,
|
||||
type DecapsulateContext,
|
||||
} from './types';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateVariableService {
|
||||
@inject(WorkflowDocument) document: WorkflowDocument;
|
||||
|
||||
@inject(WorkflowVariableService) variableService: WorkflowVariableService;
|
||||
|
||||
@inject(EncapsulateContext) encapsulateContext: EncapsulateContext;
|
||||
|
||||
/**
|
||||
* 获取封装变量
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
|
||||
getEncapsulateVars(_nodes: FlowNodeEntity[]): EncapsulateVars {
|
||||
const nodes = getNodesWithSubCanvas(_nodes);
|
||||
|
||||
return {
|
||||
startVars: this.getEncapsulateStartVars(nodes),
|
||||
endVars: this.getEncapsulateEndVars(nodes),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动封装时,更新 WorkflowJSON 内部的变量引用
|
||||
* @param workflowJSON
|
||||
* @param vars
|
||||
* @returns
|
||||
*/
|
||||
updateVarsInEncapsulateJSON(
|
||||
workflowJSON: WorkflowJSON,
|
||||
vars: EncapsulateVars,
|
||||
) {
|
||||
const startId =
|
||||
workflowJSON.nodes.find(_node => _node.type === StandardNodeType.Start)
|
||||
?.id || '100001';
|
||||
|
||||
const nextWorkflowJSON = produce(workflowJSON, draft => {
|
||||
this.updateVarsInEncapsulateNodesJSON(
|
||||
draft.nodes as WorkflowJSON['nodes'],
|
||||
vars,
|
||||
startId,
|
||||
);
|
||||
});
|
||||
|
||||
return nextWorkflowJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点JSON中的变量
|
||||
* @param nodes
|
||||
* @param vars
|
||||
* @param startId
|
||||
*/
|
||||
updateVarsInEncapsulateNodesJSON(
|
||||
nodes: WorkflowJSON['nodes'],
|
||||
vars: EncapsulateVars,
|
||||
startId: string,
|
||||
) {
|
||||
const { startVars, endVars } = vars;
|
||||
|
||||
nodes.forEach(_node => {
|
||||
if (!_node.data) {
|
||||
_node.data = {};
|
||||
}
|
||||
|
||||
// 开始节点设置
|
||||
if (_node.type === StandardNodeType.Start) {
|
||||
_node.data.outputs = Object.entries(startVars).map(_entry => {
|
||||
const [name, variable] = _entry;
|
||||
return {
|
||||
...variable.dtoMeta,
|
||||
name,
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 结束节点设置
|
||||
if (_node.type === StandardNodeType.End) {
|
||||
_node.data.inputs = {
|
||||
terminatePlan: 'returnVariables',
|
||||
inputParameters: Object.entries(endVars).map(_entry => {
|
||||
const [name, variable] = _entry;
|
||||
return {
|
||||
name,
|
||||
input: variable.refExpressionDTO,
|
||||
};
|
||||
}),
|
||||
};
|
||||
return;
|
||||
}
|
||||
traverseRefsInNodeJSON(_node.data, _ref => {
|
||||
if (!_ref.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetKeyPath = [
|
||||
_ref.content?.blockID,
|
||||
...(_ref.content?.name?.split('.') || []),
|
||||
];
|
||||
|
||||
const targetEntry = Object.entries(startVars).find(
|
||||
([, _var]) =>
|
||||
_var.keyPath[0] === targetKeyPath[0] &&
|
||||
_var.keyPath[1] === targetKeyPath[1],
|
||||
);
|
||||
|
||||
// 如果命中开始节点的变量,则替换为开始节点的变量
|
||||
if (targetEntry) {
|
||||
_ref.content.blockID = startId;
|
||||
_ref.content.name = [targetEntry[0], ...targetKeyPath.slice(2)].join(
|
||||
'.',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 子画布场景需要递归
|
||||
if (_node.blocks) {
|
||||
this.updateVarsInEncapsulateNodesJSON(_node.blocks, vars, startId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装后更新上下游的变量引用
|
||||
* @param subFlowNode 封装节点
|
||||
* @param selectNodes 选中的节点
|
||||
* @param vars 封装变量
|
||||
*/
|
||||
updateVarsAfterEncapsulate(
|
||||
subFlowNode: FlowNodeEntity,
|
||||
selectNodes: FlowNodeEntity[],
|
||||
vars: EncapsulateVars,
|
||||
) {
|
||||
const { startVars, endVars } = vars;
|
||||
|
||||
// 封装节点输入变量
|
||||
this.setSubFlowNodeInputs(subFlowNode, startVars);
|
||||
|
||||
// 下游引用封装节点的变量
|
||||
const updateRefInfos: UpdateRefInfo[] = Object.entries(endVars).map(
|
||||
_entry => {
|
||||
const [name, variable] = _entry;
|
||||
|
||||
return {
|
||||
beforeKeyPath: variable.keyPath,
|
||||
afterKeyPath: [subFlowNode.id, name, ...variable.keyPath.slice(2)],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
this.getBeyondNodes([...selectNodes, subFlowNode]).forEach(_node => {
|
||||
_node
|
||||
.getData(WorkflowNodeRefVariablesData)
|
||||
.batchUpdateRefs(updateRefInfos);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解封装后更新上下游的变量引用
|
||||
*/
|
||||
updateVarsAfterDecapsulate(
|
||||
sourceNode: FlowNodeEntity,
|
||||
group: DecapsulateContext,
|
||||
) {
|
||||
const { startNode, endNode, middleNodes, idsMap } = group;
|
||||
|
||||
const decapsulateNodes = this.flatNodeJSONs(middleNodes)
|
||||
.map(_node => this.document.getNode(_node.id || ''))
|
||||
.filter(Boolean) as FlowNodeEntity[];
|
||||
|
||||
// 1, 更新封装节点内的变量
|
||||
const sourceInputParameters: Record<string, ValueExpression> = sourceNode
|
||||
.getData(FlowNodeFormData)
|
||||
.formModel.getFormItemValueByPath('/inputs/inputParameters');
|
||||
const updateInsideRefInfos: UpdateRefInfo[] = [
|
||||
// 变量引用节点 Id 更新
|
||||
...idsMap.entries().map(_entry => ({
|
||||
beforeKeyPath: [_entry[0]],
|
||||
afterKeyPath: [_entry[1]],
|
||||
})),
|
||||
// 输入到封装节点内的变量
|
||||
...Object.entries(sourceInputParameters || {}).map(_entry => {
|
||||
const [name, value] = _entry;
|
||||
|
||||
return {
|
||||
beforeKeyPath: [startNode?.id || '100001', name],
|
||||
afterKeyPath: get(value, 'content.keyPath'),
|
||||
afterExpression: value,
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
decapsulateNodes.forEach(_node => {
|
||||
_node
|
||||
.getData(WorkflowNodeRefVariablesData)
|
||||
.batchUpdateRefs(updateInsideRefInfos);
|
||||
});
|
||||
|
||||
// 2. 下游引用封装节点内的变量
|
||||
const endOutputs: InputValueDTO[] =
|
||||
get(endNode?.data || {}, 'inputs.inputParameters') || [];
|
||||
// 从封装节点内输出的变量
|
||||
const updateOutsideRefInfos: UpdateRefInfo[] = endOutputs.map(_output => {
|
||||
const { name, input } = _output || {};
|
||||
const beforeKeyPath = [sourceNode.id, name || ''];
|
||||
const expression = variableUtils.valueExpressionToVO(
|
||||
input,
|
||||
this.variableService,
|
||||
);
|
||||
|
||||
if (expression.type === ValueExpressionType.REF) {
|
||||
const [nodeId, ...paths] = get(expression, 'content.keyPath') || [];
|
||||
const actualNodeId = idsMap.get(nodeId) || nodeId;
|
||||
return {
|
||||
beforeKeyPath,
|
||||
afterKeyPath: [actualNodeId, ...paths],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
beforeKeyPath,
|
||||
afterExpression: expression,
|
||||
};
|
||||
});
|
||||
|
||||
this.getBeyondNodes([sourceNode, ...decapsulateNodes]).forEach(_node => {
|
||||
_node
|
||||
.getData(WorkflowNodeRefVariablesData)
|
||||
.batchUpdateRefs(updateOutsideRefInfos);
|
||||
});
|
||||
}
|
||||
|
||||
protected setSubFlowNodeInputs(
|
||||
subFlowNode: FlowNodeEntity,
|
||||
startVars: EncapsulateVars['startVars'],
|
||||
) {
|
||||
const formData = subFlowNode.getData(FlowNodeFormData);
|
||||
|
||||
// SubFlow 的 inputParameters 是 Object 类型
|
||||
const inputParameters: Record<string, InputValueVO> = Object.entries(
|
||||
startVars,
|
||||
)
|
||||
.sort((a, b) => variableOrder(b[0]) - variableOrder(a[0]))
|
||||
.reduce((acm, _entry) => {
|
||||
const [name, variable] = _entry;
|
||||
|
||||
return {
|
||||
...acm,
|
||||
[name]: {
|
||||
type: ValueExpressionType.REF,
|
||||
content: {
|
||||
keyPath: variable.keyPath,
|
||||
},
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
|
||||
// 新表单引擎更新数据
|
||||
if (isFormV2(subFlowNode)) {
|
||||
(formData.formModel as FormModelV2).setValueIn(
|
||||
'inputs.inputParameters',
|
||||
inputParameters,
|
||||
);
|
||||
} else {
|
||||
// 老表单引擎更新数据
|
||||
const fullData = formData.formModel.getFormItemValueByPath('/');
|
||||
set(fullData, 'inputs.inputParameters', inputParameters);
|
||||
formData.fireChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封装节点的开始变量
|
||||
* @param nodes
|
||||
*/
|
||||
protected getEncapsulateStartVars(nodes: FlowNodeEntity[]): VariableMap {
|
||||
const variablesMap = this.generateVariableMap(
|
||||
this.getEncapsulateNodesVars(nodes),
|
||||
);
|
||||
return variablesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封装节点变量
|
||||
* @param _nodes
|
||||
* @returns
|
||||
*/
|
||||
protected getEncapsulateNodesVars(
|
||||
nodes: FlowNodeEntity[],
|
||||
): WorkflowVariable[] {
|
||||
const selectNodeIds = nodes.map(_node => _node.id);
|
||||
const variables = uniq(
|
||||
nodes
|
||||
.map(
|
||||
_node =>
|
||||
Object.values(_node.getData(WorkflowNodeRefVariablesData).refs)
|
||||
.filter(
|
||||
_keyPath =>
|
||||
!selectNodeIds.includes(_keyPath[0]) &&
|
||||
!isGlobalVariableKey(_keyPath[0]),
|
||||
)
|
||||
.map(_keyPath =>
|
||||
this.variableService.getWorkflowVariableByKeyPath(
|
||||
// 产品要求,只要取第一级即可
|
||||
_keyPath.slice(0, 2),
|
||||
{ node: _node },
|
||||
),
|
||||
)
|
||||
.filter(Boolean) as WorkflowVariable[],
|
||||
)
|
||||
.flat(),
|
||||
);
|
||||
|
||||
return sortVariables(variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封装节点的结束变量
|
||||
* @param nodes
|
||||
*/
|
||||
protected getEncapsulateEndVars(nodes: FlowNodeEntity[]): VariableMap {
|
||||
const selectNodeIds = nodes.map(_node => _node.id);
|
||||
|
||||
// 获取在圈选范围外的节点的所有引用圈选范围内的变量引用
|
||||
const variables = uniq(
|
||||
this.getBeyondNodes(nodes)
|
||||
.map(
|
||||
_node =>
|
||||
Object.values(_node.getData(WorkflowNodeRefVariablesData).refs)
|
||||
.filter(_keyPath => selectNodeIds.includes(_keyPath[0]))
|
||||
.map(_keyPath =>
|
||||
this.variableService.getWorkflowVariableByKeyPath(
|
||||
// 产品要求,只要取第一级即可
|
||||
_keyPath.slice(0, 2),
|
||||
{ node: _node },
|
||||
),
|
||||
)
|
||||
.filter(Boolean) as WorkflowVariable[],
|
||||
)
|
||||
.flat(),
|
||||
);
|
||||
|
||||
return this.generateVariableMap(variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取圈选范围外的节点
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
protected getBeyondNodes(nodes: FlowNodeEntity[]) {
|
||||
const selectNodeIds = nodes.map(_node => _node.id);
|
||||
|
||||
return this.document
|
||||
.getAllNodes()
|
||||
.filter(_node => !selectNodeIds.includes(_node.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成变量映射
|
||||
* @param variables
|
||||
*/
|
||||
protected generateVariableMap(variables: WorkflowVariable[]): VariableMap {
|
||||
const variablesMap = variables.reduce((acm, _variable) => {
|
||||
const _keyPath = _variable.keyPath || [];
|
||||
let name = _keyPath[1];
|
||||
let index = 1;
|
||||
|
||||
// 如果 name 已经存在,或者和系统默认字段冲突,则需要添加后缀
|
||||
while (acm[name] || ['BOT_USER_INPUT'].includes(name)) {
|
||||
name = `${_keyPath[1]}_${index}`;
|
||||
index++;
|
||||
}
|
||||
|
||||
return {
|
||||
...acm,
|
||||
[name]: _variable,
|
||||
};
|
||||
}, {});
|
||||
|
||||
if (this.encapsulateContext.isChatFlow) {
|
||||
return this.generateChatVariableMap(variablesMap);
|
||||
}
|
||||
|
||||
return variablesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 情况1:框选范围内所有节点的入参不包含USER_INPUT和CONVERSATION_NAME,此时封装出的子流程为workflow,子流程start不带默认参数
|
||||
* 2. 情况2:框选范围内所有节点的入参同时包含了USER_INPUT和CONVERSATION_NAME,此时封装出的子流程为chatflow,子流程start默认带参数USER_INPUT和CONVERSATION_NAME,命名不变,建立和父流程引用关系
|
||||
* 3. 情况3:框选范围内所有节点的入参包含了USER_INPUT和CONVERSATION_NAME中的一种,此时封装出的子流程为workflow,子流程start不带默认参数,新建参数USER_INPUT_1或CONVERSATION_NAME_1,建立和父流程引用关系
|
||||
*/
|
||||
protected generateChatVariableMap(variablesMap: VariableMap): VariableMap {
|
||||
if (variablesMap.USER_INPUT && !variablesMap.CONVERSATION_NAME) {
|
||||
variablesMap.USER_INPUT_1 = variablesMap.USER_INPUT;
|
||||
delete variablesMap.USER_INPUT;
|
||||
}
|
||||
|
||||
if (variablesMap.CONVERSATION_NAME && !variablesMap.USER_INPUT) {
|
||||
variablesMap.CONVERSATION_NAME_1 = variablesMap.CONVERSATION_NAME;
|
||||
delete variablesMap.CONVERSATION_NAME;
|
||||
}
|
||||
|
||||
return variablesMap;
|
||||
}
|
||||
|
||||
protected flatNodeJSONs(nodes: FlowNodeJSON[]) {
|
||||
const result: FlowNodeJSON[] = [];
|
||||
const queue = [...nodes];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift();
|
||||
|
||||
if (node) {
|
||||
result.push(node);
|
||||
}
|
||||
|
||||
if (node?.blocks) {
|
||||
queue.push(...(node.blocks as FlowNodeJSON[]));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export * from './encapsulate-service';
|
||||
export * from './types';
|
||||
export * from './encapsulate-manager';
|
||||
export * from './encapsulate-lines-service';
|
||||
export * from './encapsulate-nodes-service';
|
||||
export * from './encapsulate-variable-service';
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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 WorkflowPortEntity,
|
||||
type WorkflowNodeEntity,
|
||||
type WorkflowLineEntity,
|
||||
type WorkflowNodeJSON,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { type Event } from '@flowgram-adapter/common';
|
||||
import { type WorkflowVariable } from '@coze-workflow/variable';
|
||||
|
||||
import { type EncapsulateValidateResult } from '../validate';
|
||||
|
||||
/**
|
||||
* 封装结果
|
||||
*/
|
||||
export type EncapsulateResult =
|
||||
| EncapsulateErrorResult
|
||||
| EncapsulateSuccessResult;
|
||||
|
||||
/**
|
||||
* 错误结果
|
||||
*/
|
||||
export interface EncapsulateErrorResult {
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
success: false;
|
||||
/**
|
||||
* 消息提示
|
||||
*/
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功结果
|
||||
*/
|
||||
export interface EncapsulateSuccessResult {
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
success: true;
|
||||
/**
|
||||
* 生成的节点
|
||||
*/
|
||||
subFlowNode: WorkflowNodeEntity;
|
||||
/**
|
||||
* 生成的输入线
|
||||
*/
|
||||
inputLines: WorkflowLineEntity[];
|
||||
/**
|
||||
* 生成的输出线
|
||||
*/
|
||||
outputLines: WorkflowLineEntity[];
|
||||
/**
|
||||
* 生成的workflow id
|
||||
*/
|
||||
workflowId: string;
|
||||
/**
|
||||
* projectId
|
||||
*/
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装/解封服务
|
||||
*/
|
||||
export interface EncapsulateService {
|
||||
/**
|
||||
* 是否可以封装
|
||||
*/
|
||||
canEncapsulate: () => boolean;
|
||||
/**
|
||||
* 封装
|
||||
*/
|
||||
encapsulate: () => Promise<EncapsulateResult>;
|
||||
/**
|
||||
* 是否支持解封
|
||||
*/
|
||||
canDecapsulate: (node: WorkflowNodeEntity) => boolean;
|
||||
/**
|
||||
* 解封
|
||||
*/
|
||||
decapsulate: (node: WorkflowNodeEntity) => Promise<void>;
|
||||
/**
|
||||
* 校验
|
||||
*/
|
||||
validate: () => Promise<EncapsulateValidateResult>;
|
||||
/**
|
||||
* 封装成功回调
|
||||
*/
|
||||
onEncapsulate: Event<EncapsulateResult>;
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
export const EncapsulateService = Symbol('EncapsulateService');
|
||||
|
||||
/**
|
||||
* 封装/解封管理
|
||||
*/
|
||||
export interface EncapsulateManager {
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init: () => void;
|
||||
/**
|
||||
* 销毁
|
||||
* @returns
|
||||
*/
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
export const EncapsulateManager = Symbol('EncapsulateManager');
|
||||
|
||||
/**
|
||||
* 连接端口
|
||||
*/
|
||||
export interface ConnectPortsInfo {
|
||||
inputLines: WorkflowLineEntity[];
|
||||
outputLines: WorkflowLineEntity[];
|
||||
fromPorts: WorkflowPortEntity[];
|
||||
toPorts: WorkflowPortEntity[];
|
||||
}
|
||||
|
||||
export interface DecapsulateContext {
|
||||
idsMap: Map<string, string>;
|
||||
startNode?: WorkflowNodeJSON;
|
||||
endNode?: WorkflowNodeJSON;
|
||||
middleNodes: WorkflowNodeJSON[];
|
||||
}
|
||||
|
||||
export type VariableMap = Record<string, WorkflowVariable>;
|
||||
|
||||
export interface EncapsulateVars {
|
||||
startVars: VariableMap;
|
||||
endVars: VariableMap;
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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 { uniqBy } from 'lodash-es';
|
||||
import { injectable, inject } from 'inversify';
|
||||
import { StandardNodeType } from '@coze-workflow/base/types';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowNodeEntity,
|
||||
WorkflowDocument,
|
||||
type WorkflowJSON,
|
||||
type WorkflowEdgeJSON,
|
||||
type WorkflowNodeJSON,
|
||||
type WorkflowNodeRegistry,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { getNodesParentId } from '../utils/get-nodes-parent-id';
|
||||
import { setNodePositionByRect } from '../utils';
|
||||
import { EncapsulateContext } from '../encapsulate-context';
|
||||
import { EncapsulateLinesService } from '../encapsulate/encapsulate-lines-service';
|
||||
import {
|
||||
EncapsulateVariableService,
|
||||
type ConnectPortsInfo,
|
||||
} from '../encapsulate';
|
||||
import {
|
||||
type EncapsulateGenerateService,
|
||||
type GenerateSubWorkflowNodeOptions,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* 封装生成服务,产生workflow相关的json数据
|
||||
*/
|
||||
@injectable()
|
||||
export class EncapsulateGenerateServiceImpl
|
||||
implements EncapsulateGenerateService
|
||||
{
|
||||
generate: (nodes: FlowNodeEntity[]) => Promise<WorkflowJSON>;
|
||||
@inject(EncapsulateLinesService)
|
||||
private encapsulateLinesService: EncapsulateLinesService;
|
||||
|
||||
@inject(EncapsulateVariableService)
|
||||
private encapsulateVariableService: EncapsulateVariableService;
|
||||
|
||||
@inject(WorkflowDocument)
|
||||
private workflowDocument: WorkflowDocument;
|
||||
|
||||
@inject(EncapsulateContext)
|
||||
private encapsulateContext: EncapsulateContext;
|
||||
|
||||
/**
|
||||
* 生成workflow的json
|
||||
* @param nodes
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
async generateWorkflowJSON(
|
||||
nodes: WorkflowNodeEntity[],
|
||||
options,
|
||||
): Promise<WorkflowJSON> {
|
||||
const ports =
|
||||
this.encapsulateLinesService.getValidEncapsulateConnectPorts(nodes);
|
||||
const json = await this.workflowDocument.toJSON();
|
||||
const nodeIds = nodes.map(node => node.id);
|
||||
|
||||
// step 1: 生成json
|
||||
const defaultJSON = this.defaultJSON();
|
||||
const startNode = defaultJSON.nodes.find(
|
||||
node => node.type === StandardNodeType.Start,
|
||||
);
|
||||
const endNode = defaultJSON.nodes.find(
|
||||
node => node.type === StandardNodeType.End,
|
||||
);
|
||||
|
||||
if (!startNode || !endNode) {
|
||||
throw new Error('start or end node not found');
|
||||
}
|
||||
|
||||
// step 2: 生成封装节点的json
|
||||
const parentId = getNodesParentId(nodes);
|
||||
const parentNodes =
|
||||
parentId === 'root'
|
||||
? json.nodes
|
||||
: this.findSubCanvasNodeJSON(json.nodes, parentId)?.blocks || [];
|
||||
const encapsulateNodes = parentNodes.filter(node =>
|
||||
nodeIds.includes(node.id),
|
||||
);
|
||||
const parentEdges =
|
||||
parentId === 'root'
|
||||
? json.edges
|
||||
: this.findSubCanvasNodeJSON(json.nodes, parentId)?.edges || [];
|
||||
const encapsulateEdges = parentEdges.filter(
|
||||
edge =>
|
||||
nodeIds.includes(edge.sourceNodeID) &&
|
||||
nodeIds.includes(edge.targetNodeID),
|
||||
);
|
||||
|
||||
// step 3: 将 start end 连接到封装节点
|
||||
const { startEdges, endEdges } = this.generateStartEndEdges(
|
||||
startNode,
|
||||
endNode,
|
||||
ports,
|
||||
);
|
||||
|
||||
// step 4: 更新start end 的位置
|
||||
if (options?.startEndRects) {
|
||||
setNodePositionByRect(startNode, options.startEndRects.start);
|
||||
setNodePositionByRect(endNode, options.startEndRects.end);
|
||||
}
|
||||
|
||||
let workflowJSON = {
|
||||
nodes: [...defaultJSON.nodes, ...encapsulateNodes],
|
||||
edges: [
|
||||
...defaultJSON.edges,
|
||||
...encapsulateEdges,
|
||||
...startEdges,
|
||||
...endEdges,
|
||||
],
|
||||
};
|
||||
|
||||
// step 5: 更新变量引用关系
|
||||
const vars = this.encapsulateVariableService.getEncapsulateVars(nodes);
|
||||
workflowJSON = this.encapsulateVariableService.updateVarsInEncapsulateJSON(
|
||||
workflowJSON,
|
||||
vars,
|
||||
);
|
||||
return workflowJSON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成子流程节点
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
generateSubWorkflowNode = ({
|
||||
name,
|
||||
desc,
|
||||
workflowId,
|
||||
spaceId,
|
||||
}: GenerateSubWorkflowNodeOptions) => {
|
||||
const nodeMeta = this.getTemplateNodeMeta(StandardNodeType.SubWorkflow);
|
||||
return {
|
||||
data: {
|
||||
nodeMeta: {
|
||||
title: name,
|
||||
description: desc,
|
||||
icon: nodeMeta.icon,
|
||||
isImageflow: false,
|
||||
},
|
||||
inputs: {
|
||||
workflowId,
|
||||
spaceId,
|
||||
workflowVersion: this.encapsulateContext.projectId ? '' : 'v0.0.1',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成开始结束边
|
||||
* @param startNode
|
||||
* @param endNode
|
||||
* @param ports
|
||||
* @returns
|
||||
*/
|
||||
private generateStartEndEdges(
|
||||
startNode: WorkflowNodeJSON,
|
||||
endNode: WorkflowNodeJSON,
|
||||
ports: ConnectPortsInfo,
|
||||
) {
|
||||
const startEdges: WorkflowEdgeJSON[] = uniqBy(
|
||||
ports.inputLines.map(line => {
|
||||
const edge = line.toJSON();
|
||||
edge.sourceNodeID = startNode.id;
|
||||
delete edge.sourcePortID;
|
||||
return edge;
|
||||
}),
|
||||
this.getCompareEdgeId,
|
||||
);
|
||||
|
||||
const endEdges: WorkflowEdgeJSON[] = uniqBy(
|
||||
ports.outputLines.map(line => {
|
||||
const edge = line.toJSON();
|
||||
edge.targetNodeID = endNode.id;
|
||||
delete edge.targetPortID;
|
||||
return edge;
|
||||
}),
|
||||
this.getCompareEdgeId,
|
||||
);
|
||||
|
||||
return {
|
||||
startEdges,
|
||||
endEdges,
|
||||
};
|
||||
}
|
||||
|
||||
private getCompareEdgeId(edge) {
|
||||
return `${edge.sourceNodeID || ''}:${edge.sourcePortID || ''}-${edge.targetNodeID || ''}:${edge.targetPortID || ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认的工作流json
|
||||
*/
|
||||
private defaultJSON() {
|
||||
return {
|
||||
nodes: [this.generateStartNode(), this.generateEndNode()],
|
||||
edges: [],
|
||||
versions: { loop: 'v2' },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成开始节点
|
||||
*/
|
||||
private generateStartNode() {
|
||||
return {
|
||||
id: '100001',
|
||||
type: StandardNodeType.Start,
|
||||
meta: {
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
data: {
|
||||
nodeMeta: this.getTemplateNodeMeta(StandardNodeType.Start),
|
||||
outputs: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成结束节点
|
||||
*/
|
||||
private generateEndNode() {
|
||||
return {
|
||||
id: '900001',
|
||||
type: StandardNodeType.End,
|
||||
meta: {
|
||||
position: { x: 1000, y: 0 },
|
||||
},
|
||||
data: {
|
||||
nodeMeta: this.getTemplateNodeMeta(StandardNodeType.End),
|
||||
inputs: { terminatePlan: 'returnVariables', inputParameters: [] },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的元数据
|
||||
*/
|
||||
private getTemplateNodeMeta(type: StandardNodeType) {
|
||||
const template = this.encapsulateContext.getNodeTemplate(type);
|
||||
return template
|
||||
? {
|
||||
title: template.title,
|
||||
subTitle: template.subTitle,
|
||||
description: template.description,
|
||||
icon: template.icon,
|
||||
}
|
||||
: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找子工作流对应的json
|
||||
* @param nodeJSONs
|
||||
* @param parentId
|
||||
* @returns
|
||||
*/
|
||||
private findSubCanvasNodeJSON(
|
||||
nodeJSONs: WorkflowNodeJSON[],
|
||||
parentId: string,
|
||||
) {
|
||||
const subCanvasNode = this.findSubCanvasSourceNode(parentId);
|
||||
return nodeJSONs.find(nodeJSON => nodeJSON.id === subCanvasNode?.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找子画布对应的节点
|
||||
* @param subCanvasId
|
||||
* @returns
|
||||
*/
|
||||
private findSubCanvasSourceNode(subCanvasId: string) {
|
||||
const nodes = this.workflowDocument.getAllNodes();
|
||||
return nodes.find(node => {
|
||||
const registry = node.getNodeRegistry() as WorkflowNodeRegistry;
|
||||
const subCanvas = registry?.meta?.subCanvas;
|
||||
|
||||
return subCanvas?.(node)?.canvasNode?.id === subCanvasId;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 * from './types';
|
||||
export * from './encapsulate-generate-service';
|
||||
@@ -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,
|
||||
type WorkflowJSON,
|
||||
type WorkflowNodeJSON,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { type Rect } from '../types';
|
||||
|
||||
/**
|
||||
* 生成子流程节点选项
|
||||
*/
|
||||
export interface GenerateSubWorkflowNodeOptions {
|
||||
name: string;
|
||||
workflowId: string;
|
||||
desc: string;
|
||||
spaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 封装生成服务
|
||||
*/
|
||||
export interface EncapsulateGenerateService {
|
||||
/**
|
||||
* 生成流程
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
generateWorkflowJSON: (
|
||||
nodes: WorkflowNodeEntity[],
|
||||
options?: {
|
||||
startEndRects?: {
|
||||
start: Rect;
|
||||
end: Rect;
|
||||
};
|
||||
},
|
||||
) => Promise<WorkflowJSON>;
|
||||
/**
|
||||
* 生成子流程节点
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
generateSubWorkflowNode: (
|
||||
options: GenerateSubWorkflowNodeOptions,
|
||||
) => Partial<WorkflowNodeJSON>;
|
||||
}
|
||||
|
||||
export const EncapsulateGenerateService = Symbol('EncapsulateGenerateService');
|
||||
20
frontend/packages/workflow/feature-encapsulate/src/index.ts
Normal file
20
frontend/packages/workflow/feature-encapsulate/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createWorkflowEncapsulatePlugin } from './create-workflow-encapsulate-plugin';
|
||||
export { EncapsulateService } from './encapsulate';
|
||||
export { EncapsulatePanel } from './render';
|
||||
export { ENCAPSULATE_SHORTCUTS } from './render';
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(
|
||||
navigator.userAgent,
|
||||
);
|
||||
|
||||
const CTRL = isMacOS ? '⌘' : 'Ctrl';
|
||||
const SHIFT = isMacOS ? '⇧' : 'Shift';
|
||||
|
||||
export const ENCAPSULATE_SHORTCUTS = {
|
||||
encapsulate: `${CTRL} G`,
|
||||
decapsulate: `${CTRL} ${SHIFT} G`,
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type MouseEvent, useRef, type MouseEventHandler } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Button } from '@coze-arch/coze-design';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { useValidate } from '../hooks/use-validate';
|
||||
import { useEncapsulate } from '../hooks/use-encapsulate';
|
||||
import { EncapsulateTooltip } from '../encapsulate-tooltip';
|
||||
import { EncapsulateRenderService } from '../encapsulate-render-service';
|
||||
import { ENCAPSULATE_SHORTCUTS } from '../constants';
|
||||
|
||||
import styles from './styles.module.less';
|
||||
|
||||
const HOVER_DELAY = 200;
|
||||
|
||||
/**
|
||||
* 封装按钮
|
||||
*/
|
||||
export function EncapsulateButton() {
|
||||
const encapsulateRenderService = useService<EncapsulateRenderService>(
|
||||
EncapsulateRenderService,
|
||||
);
|
||||
const { handleEncapsulate, loading } = useEncapsulate();
|
||||
|
||||
const { validating, errors } = useValidate();
|
||||
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (validating) {
|
||||
return;
|
||||
}
|
||||
handleEncapsulate();
|
||||
};
|
||||
|
||||
const hasError = errors && errors.length > 0;
|
||||
const disabled = !!(!errors || hasError);
|
||||
const timeOutRef = useRef<number>();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseLeave: MouseEventHandler<HTMLDivElement> = () => {
|
||||
if (!timeOutRef.current) {
|
||||
timeOutRef.current = window.setTimeout(() => {
|
||||
encapsulateRenderService.hideTooltip();
|
||||
timeOutRef.current = undefined;
|
||||
}, HOVER_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter: MouseEventHandler<HTMLDivElement> = () => {
|
||||
if (timeOutRef.current) {
|
||||
clearTimeout(timeOutRef.current);
|
||||
timeOutRef.current = undefined;
|
||||
}
|
||||
encapsulateRenderService.showTooltip();
|
||||
};
|
||||
|
||||
return (
|
||||
<EncapsulateTooltip
|
||||
errors={errors}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-auto"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={ref}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
className={styles.button}
|
||||
color="highlight"
|
||||
onMouseDown={handleClick}
|
||||
>
|
||||
<span>
|
||||
{I18n.t('workflow_encapsulate_button', undefined, '封装工作流')}
|
||||
</span>
|
||||
<span className={styles.shortcut}>
|
||||
{ENCAPSULATE_SHORTCUTS.encapsulate}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</EncapsulateTooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.button {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
// color: rgba(255, 255, 255, 0.6);
|
||||
margin-left: 4px;
|
||||
font-family: 'PICO Sans VFE SC, PingFang SC,Noto Sans SC,sans-serif';
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
|
||||
@keyframes encapsulate-panel-show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.encapsulate-panel {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 16px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.35s ease;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
|
||||
&-show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.encapsulate-panel-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
padding: 0 4px 0 8px;
|
||||
background-color: var(--coz-bg-max);
|
||||
border: 1px solid var(--coz-stroke-plus);
|
||||
border-radius: 10px;
|
||||
color: var(--coz-fg-primary);
|
||||
font-weight: 500;
|
||||
box-shadow: var(--coz-shadow-small);
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { useSelectedNodes } from '../hooks/use-selected-nodes';
|
||||
import { EncapsulateRenderService } from '../encapsulate-render-service';
|
||||
import { EncapsulateButton } from '../encapsulate-button';
|
||||
import { EncapsulateService } from '../../encapsulate';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
export const EncapsulatePanel: FC = () => {
|
||||
const { selectedNodes } = useSelectedNodes();
|
||||
const { length } = selectedNodes || [];
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
|
||||
const encapsulateRenderService = useService<EncapsulateRenderService>(
|
||||
EncapsulateRenderService,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const display = encapsulateService.canEncapsulate() && length > 1;
|
||||
if (!display) {
|
||||
encapsulateRenderService.hideTooltip();
|
||||
}
|
||||
setShow(display);
|
||||
}, [length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles['encapsulate-panel'], {
|
||||
[styles['encapsulate-panel-show']]: show,
|
||||
})}
|
||||
>
|
||||
<div className={styles['encapsulate-panel-content']}>
|
||||
{I18n.t(
|
||||
'workflow_encapsulate_selecet',
|
||||
{ length },
|
||||
`已选中 ${length} 个节点`,
|
||||
)}{' '}
|
||||
<EncapsulateButton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { bindContributions } from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowShortcutsContribution } from '@coze-workflow/render';
|
||||
|
||||
import { EncapsulateShortcutsContribution } from './encapsulate-shortcuts-contribution';
|
||||
import { EncapsulateRenderService } from './encapsulate-render-service';
|
||||
|
||||
export const EncapsulateRenderContainerModule = new ContainerModule(bind => {
|
||||
bindContributions(bind, EncapsulateShortcutsContribution, [
|
||||
WorkflowShortcutsContribution,
|
||||
]);
|
||||
bind(EncapsulateRenderService).toSelf().inSingletonScope();
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { Emitter } from '@flowgram-adapter/common';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateRenderService {
|
||||
private isModalVisible = false;
|
||||
private onModalVisibleChangeEmitter = new Emitter<boolean>();
|
||||
readonly onModalVisibleChange = this.onModalVisibleChangeEmitter.event;
|
||||
|
||||
private isTooltipVisible = false;
|
||||
private onTooltipVisibleChangeEmitter = new Emitter<boolean>();
|
||||
readonly onTooltipVisibleChange = this.onTooltipVisibleChangeEmitter.event;
|
||||
|
||||
private isLoading = false;
|
||||
private onLoadingChangeEmitter = new Emitter<boolean>();
|
||||
readonly onLoadingChange = this.onLoadingChangeEmitter.event;
|
||||
|
||||
get modalVisible() {
|
||||
return this.isModalVisible;
|
||||
}
|
||||
|
||||
get tooltipVisible() {
|
||||
return this.isTooltipVisible;
|
||||
}
|
||||
|
||||
get loading() {
|
||||
return this.isLoading;
|
||||
}
|
||||
|
||||
setLoading(value: boolean) {
|
||||
this.isLoading = value;
|
||||
this.onLoadingChangeEmitter.fire(value);
|
||||
}
|
||||
|
||||
openModal() {
|
||||
this.setModalVisible(true);
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.setModalVisible(false);
|
||||
}
|
||||
|
||||
showTooltip() {
|
||||
this.setTooltipVisible(true);
|
||||
}
|
||||
|
||||
hideTooltip() {
|
||||
this.setTooltipVisible(false);
|
||||
}
|
||||
|
||||
setTooltipVisible(value: boolean) {
|
||||
this.isTooltipVisible = value;
|
||||
this.onTooltipVisibleChangeEmitter.fire(value);
|
||||
}
|
||||
|
||||
private setModalVisible(value: boolean) {
|
||||
this.isModalVisible = value;
|
||||
this.onModalVisibleChangeEmitter.fire(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { PlaygroundConfigEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowNodeEntity,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowShortcutsContribution,
|
||||
type WorkflowShortcutsRegistry,
|
||||
} from '@coze-workflow/render';
|
||||
|
||||
import { EncapsulateService } from '../encapsulate';
|
||||
import { EncapsulateCommands } from './types';
|
||||
import { EncapsulateRenderService } from './encapsulate-render-service';
|
||||
|
||||
/**
|
||||
* 封装 快捷键
|
||||
*/
|
||||
@injectable()
|
||||
export class EncapsulateShortcutsContribution
|
||||
implements WorkflowShortcutsContribution
|
||||
{
|
||||
@inject(PlaygroundConfigEntity)
|
||||
private playgroundConfigEntity: PlaygroundConfigEntity;
|
||||
|
||||
@inject(EncapsulateService)
|
||||
private encapsulateService: EncapsulateService;
|
||||
|
||||
@inject(EncapsulateRenderService)
|
||||
private encapsulateRenderService: EncapsulateRenderService;
|
||||
|
||||
@inject(WorkflowSelectService)
|
||||
private workflowSelectService: WorkflowSelectService;
|
||||
|
||||
registerShortcuts(registry: WorkflowShortcutsRegistry): void {
|
||||
registry.addHandlers(
|
||||
/**
|
||||
* 封装
|
||||
*/
|
||||
{
|
||||
commandId: EncapsulateCommands.ENCAPSULATE,
|
||||
shortcuts: ['meta g', 'ctrl g'],
|
||||
|
||||
isEnabled: () => !this.playgroundConfigEntity.readonly,
|
||||
|
||||
execute: async () => {
|
||||
if (!this.encapsulateService.canEncapsulate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.encapsulateService.validate();
|
||||
|
||||
if (res.hasError()) {
|
||||
this.encapsulateRenderService.showTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
this.encapsulateRenderService.setLoading(true);
|
||||
try {
|
||||
await this.encapsulateService.encapsulate();
|
||||
this.encapsulateRenderService.closeModal();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
this.encapsulateRenderService.setLoading(false);
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 解封
|
||||
*/
|
||||
{
|
||||
commandId: EncapsulateCommands.DECAPSULATE,
|
||||
shortcuts: ['meta shift g', 'ctrl shift g'],
|
||||
isEnabled: () => !this.playgroundConfigEntity.readonly,
|
||||
execute: () => {
|
||||
const { selectedNodes } = this.workflowSelectService;
|
||||
if (selectedNodes.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = selectedNodes[0] as WorkflowNodeEntity;
|
||||
if (!this.encapsulateService.canDecapsulate(node)) {
|
||||
return;
|
||||
}
|
||||
this.encapsulateService.decapsulate(node);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Typography } from '@coze-arch/coze-design';
|
||||
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
usePlayground,
|
||||
useService,
|
||||
WorkflowSelectService,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { type EncapsulateValidateError } from '../../validate';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface Props {
|
||||
error: EncapsulateValidateError;
|
||||
}
|
||||
|
||||
export const ErrorTitle: FC<Props> = ({ error }) => {
|
||||
const selectServices = useService<WorkflowSelectService>(
|
||||
WorkflowSelectService,
|
||||
);
|
||||
|
||||
const playground = usePlayground();
|
||||
|
||||
if (!error?.sourceName && !error.sourceIcon) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
const scrollToNode = async (nodeId: string) => {
|
||||
let success = false;
|
||||
const node = playground.entityManager.getEntityById<FlowNodeEntity>(nodeId);
|
||||
|
||||
if (node) {
|
||||
await selectServices.selectNodeAndScrollToView(node, true);
|
||||
success = true;
|
||||
}
|
||||
return success;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-1 cursor-pointer max-w-[120px]"
|
||||
onClick={() => {
|
||||
if (error.source) {
|
||||
scrollToNode(error.source);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{error.sourceIcon ? (
|
||||
<img
|
||||
width={18}
|
||||
height={18}
|
||||
src={error.sourceIcon}
|
||||
className="w-4.5 h-4.5 rounded-[4px]"
|
||||
/>
|
||||
) : null}
|
||||
{error.sourceName ? (
|
||||
<Typography.Paragraph
|
||||
className={classNames(
|
||||
'font-medium coz-fg-primary',
|
||||
styles['error-name'],
|
||||
)}
|
||||
ellipsis={{
|
||||
rows: 1,
|
||||
showTooltip: {
|
||||
type: 'tooltip',
|
||||
opts: {
|
||||
style: {
|
||||
width: '100%',
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{error.sourceName}
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
.error-name {
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
max-width: 460px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.errors {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, max-content) 1fr;
|
||||
grid-gap: 16px;
|
||||
grid-row-gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
type FC,
|
||||
type MouseEventHandler,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Tooltip } from '@coze-arch/coze-design';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { EncapsulateRenderService } from '../encapsulate-render-service';
|
||||
import { type EncapsulateValidateError } from '../../validate';
|
||||
import { ErrorTitle } from './error-title';
|
||||
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface Props {
|
||||
errors: EncapsulateValidateError[];
|
||||
onMouseEnter?: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
|
||||
getPopupContainer?: () => HTMLElement;
|
||||
}
|
||||
|
||||
const ErrorMessage: FC<{
|
||||
error: EncapsulateValidateError;
|
||||
}> = ({ error }) => (
|
||||
<div className="flex-1 coz-fg-primary font-normal">{error.message}</div>
|
||||
);
|
||||
|
||||
export const EncapsulateTooltip: FC<PropsWithChildren<Props>> = ({
|
||||
errors = [],
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
children,
|
||||
}) => {
|
||||
const encapsulateRenderService = useService<EncapsulateRenderService>(
|
||||
EncapsulateRenderService,
|
||||
);
|
||||
|
||||
const [tooltipVisible, setTooltipVisible] = useState(
|
||||
encapsulateRenderService.tooltipVisible,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable =
|
||||
encapsulateRenderService.onTooltipVisibleChange(setTooltipVisible);
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const hasError = errors.length;
|
||||
|
||||
const groupErrors = useMemo(
|
||||
() =>
|
||||
groupBy(
|
||||
errors.filter(e => e.message),
|
||||
error =>
|
||||
error?.sourceName || error?.sourceIcon
|
||||
? 'withSource'
|
||||
: 'withoutSource',
|
||||
),
|
||||
[errors],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
trigger="custom"
|
||||
position="bottom"
|
||||
visible={tooltipVisible && errors?.length > 0}
|
||||
showArrow={false}
|
||||
onClickOutSide={() => {
|
||||
setTooltipVisible(false);
|
||||
}}
|
||||
className="p-0 max-w-[460px] overflow-hidden"
|
||||
content={
|
||||
hasError ? (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={styles.tooltip}
|
||||
>
|
||||
<div className="coz-fg-plus font-medium text-[16px]">
|
||||
{I18n.t(
|
||||
'workflow_encapsulate_button_unable',
|
||||
undefined,
|
||||
'无法封装工作流',
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 没有错误来源的 */}
|
||||
{(groupErrors.withoutSource || []).map((error, index) => (
|
||||
<div key={index} className="flex mt-3 gap-4 items-start">
|
||||
<ErrorMessage error={error} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 有错误来源的 */}
|
||||
{(groupErrors.withSource || []).length ? (
|
||||
<div className={styles.errors}>
|
||||
{(groupErrors.withSource || []).map(error => (
|
||||
<>
|
||||
<ErrorTitle error={error} />
|
||||
<ErrorMessage error={error} />
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { EncapsulateRenderService } from '../encapsulate-render-service';
|
||||
import { EncapsulateService } from '../../encapsulate';
|
||||
|
||||
export const useEncapsulate = () => {
|
||||
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
|
||||
const encapsulateRenderService = useService<EncapsulateRenderService>(
|
||||
EncapsulateRenderService,
|
||||
);
|
||||
const [loading, setLoading] = useState(encapsulateRenderService.loading);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = encapsulateRenderService.onLoadingChange(setLoading);
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleEncapsulate = async () => {
|
||||
encapsulateRenderService.setLoading(true);
|
||||
try {
|
||||
await encapsulateService.encapsulate();
|
||||
encapsulateRenderService.closeModal();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
encapsulateRenderService.setLoading(false);
|
||||
};
|
||||
|
||||
return {
|
||||
handleEncapsulate,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
import { WorkflowSelectService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
/**
|
||||
* 选中节点
|
||||
*/
|
||||
export function useSelectedNodes() {
|
||||
const selectService = useService<WorkflowSelectService>(
|
||||
WorkflowSelectService,
|
||||
);
|
||||
|
||||
const [selectedNodes, setSelectedNodes] = useState(
|
||||
selectService.selectedNodes,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = selectService.onSelectionChanged(() => {
|
||||
setSelectedNodes(selectService.selectedNodes);
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
selectedNodes,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import { useDebounceEffect } from 'ahooks';
|
||||
import { useService } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateValidateError,
|
||||
} from '../../validate';
|
||||
import { EncapsulateService } from '../../encapsulate';
|
||||
import { useVariableChange } from './use-variable-change';
|
||||
import { useSelectedNodes } from './use-selected-nodes';
|
||||
|
||||
const DEBOUNCE_DELAY = 100;
|
||||
|
||||
/**
|
||||
* 校验
|
||||
*/
|
||||
export function useValidate() {
|
||||
const { selectedNodes } = useSelectedNodes();
|
||||
const encapsulateService = useService<EncapsulateService>(EncapsulateService);
|
||||
|
||||
const [validating, setValidating] = useState(false);
|
||||
const [errors, setErrors] = useState<EncapsulateValidateError[]>([]);
|
||||
const validationIdRef = useRef(0); // 新增校验ID跟踪
|
||||
|
||||
const handleValidate = async () => {
|
||||
if (selectedNodes.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValidating(true);
|
||||
// 生成当前校验ID
|
||||
const currentValidationId = ++validationIdRef.current;
|
||||
|
||||
try {
|
||||
const validateResult = await encapsulateService.validate();
|
||||
|
||||
// 只处理最后一次校验结果
|
||||
if (currentValidationId === validationIdRef.current) {
|
||||
setErrors(validateResult.getErrors());
|
||||
setValidating(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrors([
|
||||
{
|
||||
code: EncapsulateValidateErrorCode.VALIDATE_ERROR,
|
||||
message: (error as Error).message,
|
||||
},
|
||||
]);
|
||||
setValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { version: variableVersion } = useVariableChange(selectedNodes);
|
||||
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
handleValidate();
|
||||
},
|
||||
[selectedNodes, variableVersion],
|
||||
{
|
||||
wait: DEBOUNCE_DELAY,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
validating,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { FlowNodeVariableData } from '@coze-workflow/variable';
|
||||
|
||||
export const useVariableChange = nodes => {
|
||||
const [version, setVersion] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const disposables = nodes
|
||||
.filter(node => node.getData(FlowNodeVariableData)?.public?.available)
|
||||
.map(node =>
|
||||
node.getData(FlowNodeVariableData).public.available.onDataChange(() => {
|
||||
setVersion(version + 1);
|
||||
}),
|
||||
);
|
||||
return () => {
|
||||
disposables.forEach(disposable => disposable?.dispose());
|
||||
};
|
||||
}, [nodes, version]);
|
||||
|
||||
return {
|
||||
version,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './encapsulate-render-container-module';
|
||||
export { EncapsulatePanel } from './encapsulate-panel';
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 封装/解封命令
|
||||
*/
|
||||
export enum EncapsulateCommands {
|
||||
/**
|
||||
* 封装
|
||||
*/
|
||||
ENCAPSULATE = 'ENCAPSULATE',
|
||||
/**
|
||||
* 解封
|
||||
*/
|
||||
DECAPSULATE = 'DECAPSULATE',
|
||||
}
|
||||
55
frontend/packages/workflow/feature-encapsulate/src/types.ts
Normal file
55
frontend/packages/workflow/feature-encapsulate/src/types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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/types';
|
||||
import { type WorkflowMode } from '@coze-workflow/base/api';
|
||||
import { type PluginContext } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { type EncapsulateResult } from './encapsulate';
|
||||
|
||||
export interface EncapsulateGlobalState {
|
||||
spaceId: string;
|
||||
flowMode: WorkflowMode;
|
||||
projectId?: string;
|
||||
info: {
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NodeMeta {
|
||||
description: string;
|
||||
icon: string;
|
||||
subTitle: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type GetGlobalStateOption = (
|
||||
context: PluginContext,
|
||||
) => EncapsulateGlobalState;
|
||||
export type GetNodeTemplateOption = (
|
||||
context: PluginContext,
|
||||
) => (type: StandardNodeType) => NodeMeta | undefined;
|
||||
export type OnEncapsulateOption = (
|
||||
result: EncapsulateResult,
|
||||
ctx: PluginContext,
|
||||
) => Promise<void>;
|
||||
|
||||
export interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
20
frontend/packages/workflow/feature-encapsulate/src/typings.d.ts
vendored
Normal file
20
frontend/packages/workflow/feature-encapsulate/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module '*.less' {
|
||||
const resource: { [key: string]: string };
|
||||
export = resource;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getFlags } from '@coze-arch/bot-flags';
|
||||
/**
|
||||
* 校验是否可以封装
|
||||
* @returns 是否可以封装
|
||||
*/
|
||||
export function checkEncapsulateGray() {
|
||||
const FLAGS = getFlags();
|
||||
return !!FLAGS['bot.automation.encapsulate'];
|
||||
}
|
||||
@@ -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 { StandardNodeType } from '@coze-workflow/base';
|
||||
import { type WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
/**
|
||||
* 排除开始和结束节点
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
export function excludeStartEnd(
|
||||
nodes: WorkflowNodeEntity[],
|
||||
): WorkflowNodeEntity[] {
|
||||
return nodes.filter(
|
||||
node =>
|
||||
![StandardNodeType.Start, StandardNodeType.End].includes(
|
||||
node.flowNodeType as StandardNodeType,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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 { PositionData } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowNodeJSON,
|
||||
WorkflowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { type IPoint } from '@flowgram-adapter/common';
|
||||
|
||||
/**
|
||||
* 获取节点坐标
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
export function getNodePoint(
|
||||
node: WorkflowNodeEntity | WorkflowNodeJSON,
|
||||
): IPoint {
|
||||
if (node instanceof WorkflowNodeEntity) {
|
||||
const positionData = node.getData<PositionData>(PositionData);
|
||||
return {
|
||||
x: positionData.x,
|
||||
y: positionData.y,
|
||||
};
|
||||
}
|
||||
|
||||
return node?.meta?.position || { x: 0, y: 0 };
|
||||
}
|
||||
@@ -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 { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
/**
|
||||
* 获取节点的父节点ID
|
||||
*/
|
||||
export const getNodesParentId = (nodes: FlowNodeEntity[]): string =>
|
||||
nodes[0]?.parent?.id || 'root';
|
||||
@@ -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 { uniq } from 'lodash-es';
|
||||
import {
|
||||
type WorkflowNodeRegistry,
|
||||
type FlowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
/**
|
||||
* 获取有子节点的节点列表
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
export const getNodesWithSubCanvas = (nodes: FlowNodeEntity[]) =>
|
||||
uniq(
|
||||
nodes
|
||||
.map(node => {
|
||||
const registry = node.getNodeRegistry() as WorkflowNodeRegistry;
|
||||
const subCanvas = registry?.meta?.subCanvas;
|
||||
|
||||
return [
|
||||
node,
|
||||
// 子画布对应的所有子节点
|
||||
...(subCanvas?.(node)?.canvasNode?.allCollapsedChildren || []),
|
||||
];
|
||||
})
|
||||
.flat(),
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
interface SubWorkflowInfo {
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
workflowVersion: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子流程信息
|
||||
* @param node 子流程节点
|
||||
* @returns spaceId 和 workflowId
|
||||
*/
|
||||
export function getSubWorkflowInfo(
|
||||
node: WorkflowNodeEntity,
|
||||
): SubWorkflowInfo | undefined {
|
||||
const formData = node.getData<FlowNodeFormData>(FlowNodeFormData);
|
||||
const formItem = formData?.formModel.getFormItemValueByPath('/inputs');
|
||||
|
||||
if (!formItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
spaceId: formItem.spaceId,
|
||||
workflowId: formItem.workflowId,
|
||||
workflowVersion: formItem.workflowVersion,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
type WorkflowNodeRegistry,
|
||||
type WorkflowNodeJSON,
|
||||
type WorkflowDocument,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
/**
|
||||
* 是否包含有子画布的节点
|
||||
*/
|
||||
export const hasSubCanvasNodes = (
|
||||
workflowDocument: WorkflowDocument,
|
||||
nodes: WorkflowNodeJSON[],
|
||||
) =>
|
||||
!!nodes.find(node => {
|
||||
const registry = workflowDocument.getNodeRegister(
|
||||
node.type,
|
||||
) as WorkflowNodeRegistry;
|
||||
return !!registry?.meta?.subCanvas;
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './check-encapsulate-gray';
|
||||
export * from './get-sub-workflow-info';
|
||||
export * from './get-node-point';
|
||||
export * from './set-node-position';
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
const RANDOM_NUM = 6;
|
||||
const RANDOM_ALPHABET = '0123456789';
|
||||
|
||||
export const randomNameSuffix = () => {
|
||||
const nanoid = customAlphabet(RANDOM_ALPHABET, RANDOM_NUM);
|
||||
return nanoid();
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
|
||||
import { type IPoint } from '@flowgram-adapter/common';
|
||||
|
||||
import { type Rect } from '../types';
|
||||
|
||||
/**
|
||||
* 设置节点坐标
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
export function setNodePosition(
|
||||
node: WorkflowNodeJSON,
|
||||
position: IPoint,
|
||||
): void {
|
||||
if (!node.meta) {
|
||||
node.meta = {};
|
||||
}
|
||||
|
||||
node.meta.position = position;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据矩形设置节点坐标
|
||||
* @param node
|
||||
* @param rect
|
||||
*/
|
||||
export function setNodePositionByRect(node: WorkflowNodeJSON, rect: Rect) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
setNodePosition(node, { x: rect.x + rect.width / 2, y: rect.y });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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,
|
||||
type FlowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
import { type WorkflowSubCanvas } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
/**
|
||||
* 多节点是否在子画布中
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
export const isNodesInSubCanvas = (nodes?: FlowNodeEntity[]) =>
|
||||
isNodeInSubCanvas(nodes?.[0]);
|
||||
|
||||
/**
|
||||
* 单节点是否在子画布中
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
export const isNodeInSubCanvas = (node?: FlowNodeEntity) =>
|
||||
node?.parent?.id !== 'root';
|
||||
|
||||
/**
|
||||
* 是不是子画布节点
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
export const isSubCanvasNode = (node?: FlowNodeEntity) =>
|
||||
node?.flowNodeType === FlowNodeBaseType.SUB_CANVAS;
|
||||
|
||||
/**
|
||||
* 获取子画布的父节点
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
export const getSubCanvasParent = (node?: FlowNodeEntity) => {
|
||||
const nodeMeta = node?.getNodeMeta();
|
||||
const subCanvas: WorkflowSubCanvas = nodeMeta?.subCanvas(node);
|
||||
return subCanvas?.parentNode;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 { isObject, get, has, isArray } from 'lodash-es';
|
||||
import { type WorkflowVariable } from '@coze-workflow/variable';
|
||||
import { type DTODefine } from '@coze-workflow/base';
|
||||
|
||||
/**
|
||||
* 遍历 DTO JSON 中的变量引用
|
||||
* @param data
|
||||
* @param cb
|
||||
* @returns
|
||||
*/
|
||||
export function traverseRefsInNodeJSON(
|
||||
data: unknown,
|
||||
cb: (_ref: DTODefine.RefExpression) => void,
|
||||
) {
|
||||
if (isObject(data)) {
|
||||
// 判断是否符合 ValueExpressionDTO 的结构
|
||||
if (
|
||||
get(data, 'type') === 'ref' &&
|
||||
get(data, 'content.source') === 'block-output' &&
|
||||
has(data, 'content.blockID') &&
|
||||
has(data, 'content.name')
|
||||
) {
|
||||
cb(data as unknown as DTODefine.RefExpression);
|
||||
}
|
||||
|
||||
Object.entries(data).forEach(([_key, _val]) => {
|
||||
traverseRefsInNodeJSON(_val, cb);
|
||||
}, {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isArray(data)) {
|
||||
data.forEach(_item => {
|
||||
traverseRefsInNodeJSON(_item, cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 变量排序
|
||||
* @param variable
|
||||
* @returns
|
||||
*/
|
||||
export const variableOrder = (name?: string) => {
|
||||
const orders = {
|
||||
USER_INPUT: 2,
|
||||
CONVERSATION_NAME: 1,
|
||||
};
|
||||
return orders[name ?? ''] || 0;
|
||||
};
|
||||
|
||||
export const sortVariables = (variables: WorkflowVariable[]) => {
|
||||
if (!variables) {
|
||||
return variables;
|
||||
}
|
||||
|
||||
return variables.sort(
|
||||
(a, b) =>
|
||||
variableOrder(b?.viewMeta?.name) - variableOrder(a?.viewMeta?.name),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable, multiInject, optional } from 'inversify';
|
||||
import { type StandardNodeType } from '@coze-workflow/base/types';
|
||||
|
||||
import {
|
||||
EncapsulateNodeValidator,
|
||||
type EncapsulateValidateManager,
|
||||
EncapsulateNodesValidator,
|
||||
EncapsulateWorkflowJSONValidator,
|
||||
} from './types';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateValidateManagerImpl
|
||||
implements EncapsulateValidateManager
|
||||
{
|
||||
@multiInject(EncapsulateNodesValidator)
|
||||
@optional()
|
||||
private nodesValidators: EncapsulateNodesValidator[] = [];
|
||||
|
||||
@multiInject(EncapsulateNodeValidator)
|
||||
@optional()
|
||||
private nodeValidators: EncapsulateNodeValidator[] = [];
|
||||
|
||||
@multiInject(EncapsulateWorkflowJSONValidator)
|
||||
@optional()
|
||||
private workflowJSONValidators: EncapsulateWorkflowJSONValidator[] = [];
|
||||
|
||||
getNodeValidators() {
|
||||
return this.nodeValidators || [];
|
||||
}
|
||||
|
||||
getNodesValidators() {
|
||||
return this.nodesValidators || [];
|
||||
}
|
||||
|
||||
getWorkflowJSONValidators() {
|
||||
return this.workflowJSONValidators || [];
|
||||
}
|
||||
|
||||
getNodeValidatorsByType(type: StandardNodeType) {
|
||||
return (this.nodeValidators || []).filter(validator =>
|
||||
validator.canHandle(type),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.nodeValidators = [];
|
||||
this.nodesValidators = [];
|
||||
this.workflowJSONValidators = [];
|
||||
}
|
||||
}
|
||||
@@ -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 { injectable } from 'inversify';
|
||||
|
||||
import {
|
||||
type EncapsulateValidateResult,
|
||||
type EncapsulateValidateErrorCode,
|
||||
type EncapsulateValidateError,
|
||||
} from './types';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateValidateResultImpl
|
||||
implements EncapsulateValidateResult
|
||||
{
|
||||
private errors: Map<string, EncapsulateValidateError[]> = new Map();
|
||||
addError(error: EncapsulateValidateError) {
|
||||
if (!this.errors.has(error.code)) {
|
||||
this.errors.set(error.code, []);
|
||||
}
|
||||
const errors = this.errors.get(error.code);
|
||||
if (errors && !errors.some(item => item.source === error.source)) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
getErrors() {
|
||||
return [...this.errors.values()].flat();
|
||||
}
|
||||
|
||||
hasError() {
|
||||
return this.errors.size > 0;
|
||||
}
|
||||
|
||||
hasErrorCode(code: EncapsulateValidateErrorCode) {
|
||||
return this.errors.has(code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 { type StandardNodeType } from '@coze-workflow/base/types';
|
||||
import {
|
||||
type WorkflowJSON,
|
||||
type WorkflowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { excludeStartEnd } from '../utils/exclude-start-end';
|
||||
import { EncapsulateGenerateService } from '../generate';
|
||||
import {
|
||||
EncapsulateValidateManager,
|
||||
type EncapsulateValidateService,
|
||||
type EncapsulateValidateResult,
|
||||
EncapsulateValidateResultFactory,
|
||||
} from './types';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateValidateServiceImpl
|
||||
implements EncapsulateValidateService
|
||||
{
|
||||
@inject(EncapsulateValidateManager)
|
||||
private encapsulateValidateManager: EncapsulateValidateManager;
|
||||
|
||||
@inject(EncapsulateValidateResultFactory)
|
||||
private encapsulateValidateResultFactory: EncapsulateValidateResultFactory;
|
||||
|
||||
@inject(EncapsulateGenerateService)
|
||||
private encapsulateGenerateService: EncapsulateGenerateService;
|
||||
|
||||
async validate(nodes: WorkflowNodeEntity[]) {
|
||||
const validateResult: EncapsulateValidateResult =
|
||||
this.encapsulateValidateResultFactory();
|
||||
this.validateNodes(nodes, validateResult);
|
||||
|
||||
for (const node of nodes) {
|
||||
await this.validateNode(node, validateResult);
|
||||
}
|
||||
|
||||
if (validateResult.hasError()) {
|
||||
return validateResult;
|
||||
}
|
||||
|
||||
const workflowJSON =
|
||||
await this.encapsulateGenerateService.generateWorkflowJSON(
|
||||
excludeStartEnd(nodes),
|
||||
);
|
||||
|
||||
await this.validateWorkflowJSON(workflowJSON, validateResult);
|
||||
return validateResult;
|
||||
}
|
||||
|
||||
private async validateWorkflowJSON(
|
||||
workflowJSON: WorkflowJSON,
|
||||
validateResult: EncapsulateValidateResult,
|
||||
) {
|
||||
const workflowJSONValidators =
|
||||
this.encapsulateValidateManager.getWorkflowJSONValidators();
|
||||
|
||||
await Promise.all(
|
||||
workflowJSONValidators.map(workflowJSONValidator =>
|
||||
workflowJSONValidator.validate(workflowJSON, validateResult),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private validateNodes(
|
||||
nodes: WorkflowNodeEntity[],
|
||||
validateResult: EncapsulateValidateResult,
|
||||
) {
|
||||
const nodesValidators =
|
||||
this.encapsulateValidateManager.getNodesValidators();
|
||||
|
||||
for (const nodesValidator of nodesValidators) {
|
||||
// 如果节点校验器需要包含起始节点和结束节点,则直接校验
|
||||
// 否则需要排除起始节点和结束节点
|
||||
nodesValidator.validate(
|
||||
nodesValidator.includeStartEnd ? nodes : excludeStartEnd(nodes),
|
||||
validateResult,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateNode(
|
||||
node: WorkflowNodeEntity,
|
||||
validateResult: EncapsulateValidateResult,
|
||||
) {
|
||||
const nodeValidators =
|
||||
this.encapsulateValidateManager.getNodeValidatorsByType(
|
||||
node.flowNodeType as StandardNodeType,
|
||||
);
|
||||
|
||||
for (const nodeValidator of nodeValidators) {
|
||||
await nodeValidator.validate(node, validateResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './encapsulate-validate-manager';
|
||||
export * from './types';
|
||||
export * from './encapsulate-validate-service';
|
||||
export * from './encapsulate-validate-result';
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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/types';
|
||||
import {
|
||||
type WorkflowJSON,
|
||||
type WorkflowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
/**
|
||||
* 封装校验结果
|
||||
*/
|
||||
export interface EncapsulateValidateError {
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
code: EncapsulateValidateErrorCode;
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* 错误来源, 如果是节点问题, 就是节点ID
|
||||
*/
|
||||
source?: string;
|
||||
/**
|
||||
* 错误来源名称
|
||||
*/
|
||||
sourceName?: string;
|
||||
/**
|
||||
* 来源图标
|
||||
*/
|
||||
sourceIcon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验错误码
|
||||
*/
|
||||
export enum EncapsulateValidateErrorCode {
|
||||
NO_START_END = '1001',
|
||||
INVALID_PORTS = '1002',
|
||||
ENCAPSULATE_LINES = '1003',
|
||||
AT_LEAST_TWO_NODES = '1005',
|
||||
INVALID_FORM = '1006',
|
||||
VALIDATE_ERROR = '1007',
|
||||
INVALID_SCHEMA = '1008',
|
||||
INVALID_LOOP_NODES = '1009',
|
||||
INVALID_SUB_CANVAS = '1010',
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验结果
|
||||
*/
|
||||
export interface EncapsulateValidateResult {
|
||||
/**
|
||||
* 是否有错误
|
||||
*/
|
||||
hasError: () => boolean;
|
||||
/**
|
||||
* 添加错误
|
||||
*/
|
||||
addError: (error: EncapsulateValidateError) => void;
|
||||
/**
|
||||
* 获取错误列表
|
||||
* @returns
|
||||
*/
|
||||
getErrors: () => EncapsulateValidateError[];
|
||||
/**
|
||||
* 是否有特定code的错误
|
||||
*/
|
||||
hasErrorCode: (code: EncapsulateValidateErrorCode) => boolean;
|
||||
}
|
||||
|
||||
export const EncapsulateValidateResult = Symbol('EncapsulateValidateResult');
|
||||
|
||||
/**
|
||||
* 校验结果工厂
|
||||
*/
|
||||
export type EncapsulateValidateResultFactory = () => EncapsulateValidateResult;
|
||||
|
||||
export const EncapsulateValidateResultFactory = Symbol(
|
||||
'EncapsulateValidateResultFactory',
|
||||
);
|
||||
|
||||
/**
|
||||
* 封装节点校验器
|
||||
*/
|
||||
export interface EncapsulateNodeValidator {
|
||||
/**
|
||||
* 节点类型
|
||||
*/
|
||||
canHandle: (type: StandardNodeType) => boolean;
|
||||
/**
|
||||
* 节点校验
|
||||
*/
|
||||
validate: (
|
||||
node: WorkflowNodeEntity,
|
||||
result: EncapsulateValidateResult,
|
||||
) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export const EncapsulateNodeValidator = Symbol('EncapsulateNodeValidator');
|
||||
|
||||
/**
|
||||
* 所有节点级别的校验器
|
||||
*/
|
||||
export interface EncapsulateNodesValidator {
|
||||
/**
|
||||
* 所有节点校验
|
||||
*/
|
||||
validate: (
|
||||
nodes: WorkflowNodeEntity[],
|
||||
result: EncapsulateValidateResult,
|
||||
) => void;
|
||||
/**
|
||||
* 是否包含开始和结束节点
|
||||
*/
|
||||
includeStartEnd?: boolean;
|
||||
}
|
||||
|
||||
export const EncapsulateNodesValidator = Symbol('EncapsulateNodesValidator');
|
||||
|
||||
/**
|
||||
* 流程JSON校验器
|
||||
*/
|
||||
export interface EncapsulateWorkflowJSONValidator {
|
||||
validate: (
|
||||
json: WorkflowJSON,
|
||||
result: EncapsulateValidateResult,
|
||||
) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export const EncapsulateWorkflowJSONValidator = Symbol(
|
||||
'EncapsulateWorkflowJSONValidator',
|
||||
);
|
||||
|
||||
/**
|
||||
* 封装校验管理
|
||||
*/
|
||||
export interface EncapsulateValidateManager {
|
||||
/**
|
||||
* 获取所有节点校验器
|
||||
*/
|
||||
getNodeValidators: () => EncapsulateNodeValidator[];
|
||||
/**
|
||||
* 根据节点类型获取对应的校验器
|
||||
* @param type
|
||||
* @returns
|
||||
*/
|
||||
getNodeValidatorsByType: (
|
||||
type: StandardNodeType,
|
||||
) => EncapsulateNodeValidator[];
|
||||
/**
|
||||
* 获取所有流程级别校验器
|
||||
*/
|
||||
getNodesValidators: () => EncapsulateNodesValidator[];
|
||||
/**
|
||||
* 获取所有流程JSON校验器
|
||||
* @returns
|
||||
*/
|
||||
getWorkflowJSONValidators: () => EncapsulateWorkflowJSONValidator[];
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
export const EncapsulateValidateManager = Symbol('EncapsulateValidateManager');
|
||||
|
||||
/**
|
||||
* 封装校验服务
|
||||
*/
|
||||
export interface EncapsulateValidateService {
|
||||
/**
|
||||
* 校验
|
||||
* @param nodes
|
||||
* @returns
|
||||
*/
|
||||
validate: (nodes: WorkflowNodeEntity[]) => Promise<EncapsulateValidateResult>;
|
||||
}
|
||||
|
||||
export const EncapsulateValidateService = Symbol('EncapsulateValidateService');
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 { WorkflowNode } from '@coze-workflow/base';
|
||||
import {
|
||||
WorkflowDocument,
|
||||
type WorkflowNodeEntity,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { getSubCanvasParent, isSubCanvasNode } from '../utils/subcanvas';
|
||||
@injectable()
|
||||
export class EncapsulateBaseValidator {
|
||||
@inject(WorkflowDocument)
|
||||
protected workflowDocument: WorkflowDocument;
|
||||
|
||||
protected getLineName(from: string, to: string) {
|
||||
return `${this.getNodeNameById(from)} -> ${this.getNodeNameById(to)}`;
|
||||
}
|
||||
|
||||
protected getLineSource(from: string, to: string) {
|
||||
return `${from}_${to}`;
|
||||
}
|
||||
|
||||
protected getNodeName(node: WorkflowNodeEntity) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubCanvasNode(node)) {
|
||||
return this.getSubCanvasName(node);
|
||||
}
|
||||
const workflowNode = new WorkflowNode(node);
|
||||
return workflowNode.title || this.defaultNodeName(node.id);
|
||||
}
|
||||
|
||||
protected getSubCanvasName(node: WorkflowNodeEntity) {
|
||||
const nodeMeta = node.getNodeMeta();
|
||||
const { title = '' } = nodeMeta?.renderSubCanvas?.() ?? {};
|
||||
return title || this.defaultNodeName(node.id);
|
||||
}
|
||||
|
||||
protected getSubCanvasIcon(node: WorkflowNodeEntity) {
|
||||
const parent = getSubCanvasParent(node);
|
||||
return this.getNodeIcon(parent);
|
||||
}
|
||||
|
||||
protected getNodeIcon(node: WorkflowNodeEntity) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubCanvasNode(node)) {
|
||||
return this.getSubCanvasIcon(node);
|
||||
}
|
||||
const workflowNode = new WorkflowNode(node);
|
||||
return workflowNode.icon;
|
||||
}
|
||||
|
||||
protected getNodeNameById(id: string) {
|
||||
const node = this.workflowDocument.getNode(id);
|
||||
|
||||
if (!node) {
|
||||
return this.defaultNodeName(id);
|
||||
}
|
||||
|
||||
return this.getNodeName(node);
|
||||
}
|
||||
|
||||
protected defaultNodeName(id: string) {
|
||||
return `Node${id}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { ValidationService } from '@coze-workflow/base/services';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
import { type WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
type EncapsulateNodeValidator,
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateValidateResult,
|
||||
} from '../validate';
|
||||
import { EncapsulateBaseValidator } from './encapsulate-base-validator';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateFormValidator
|
||||
extends EncapsulateBaseValidator
|
||||
implements EncapsulateNodeValidator
|
||||
{
|
||||
@inject(ValidationService)
|
||||
private validationService: ValidationService;
|
||||
|
||||
canHandle(_type: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async validate(node: WorkflowNodeEntity, result: EncapsulateValidateResult) {
|
||||
// 注释节点不需要校验
|
||||
if (
|
||||
[StandardNodeType.Comment].includes(node.flowNodeType as StandardNodeType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await this.validationService.validateNode(node);
|
||||
|
||||
if (!res.hasError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceName = this.getNodeName(node);
|
||||
const sourceIcon = this.getNodeIcon(node);
|
||||
const errors = res.nodeErrorMap[node.id] || [];
|
||||
|
||||
errors.forEach(error => {
|
||||
if (!error.errorInfo || error.errorLevel !== 'error') {
|
||||
return;
|
||||
}
|
||||
|
||||
result.addError({
|
||||
code: EncapsulateValidateErrorCode.INVALID_FORM,
|
||||
message: error.errorInfo,
|
||||
source: node.id,
|
||||
sourceName,
|
||||
sourceIcon,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateNodesValidator,
|
||||
} from '../validate';
|
||||
import { EncapsulateLinesService } from '../encapsulate';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateInputLinesValidator
|
||||
implements EncapsulateNodesValidator
|
||||
{
|
||||
@inject(EncapsulateLinesService)
|
||||
private encapsulateLinesService: EncapsulateLinesService;
|
||||
|
||||
validate(nodes, result) {
|
||||
const inputLines =
|
||||
this.encapsulateLinesService.getEncapsulateNodesInputLines(nodes);
|
||||
|
||||
if (inputLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const valid =
|
||||
this.encapsulateLinesService.validateEncapsulateLines(inputLines);
|
||||
|
||||
if (!valid) {
|
||||
result.addError({
|
||||
code: EncapsulateValidateErrorCode.ENCAPSULATE_LINES,
|
||||
message: I18n.t(
|
||||
'workflow_encapsulate_button_unable_connected',
|
||||
undefined,
|
||||
'框选范围内有中间节点连到框选范围外的节点',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateNodesValidator,
|
||||
} from '../validate';
|
||||
import { EncapsulateLinesService } from '../encapsulate';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateOutputLinesValidator
|
||||
implements EncapsulateNodesValidator
|
||||
{
|
||||
@inject(EncapsulateLinesService)
|
||||
private encapsulateLinesService: EncapsulateLinesService;
|
||||
|
||||
validate(nodes, result) {
|
||||
const outputLines =
|
||||
this.encapsulateLinesService.getEncapsulateNodesOutputLines(nodes);
|
||||
|
||||
if (outputLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const valid =
|
||||
this.encapsulateLinesService.validateEncapsulateLines(outputLines);
|
||||
|
||||
if (!valid) {
|
||||
result.addError({
|
||||
code: EncapsulateValidateErrorCode.ENCAPSULATE_LINES,
|
||||
message: I18n.t(
|
||||
'workflow_encapsulate_button_unable_connected',
|
||||
undefined,
|
||||
'框选范围内有中间节点连到框选范围外的节点',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { StandardNodeType } from '@coze-workflow/base';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { FlowNodeBaseType } from '@flowgram-adapter/free-layout-editor';
|
||||
import {
|
||||
type WorkflowNodeEntity,
|
||||
WorkflowNodePortsData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateNodesValidator,
|
||||
} from '../validate';
|
||||
import { getNodesWithSubCanvas } from '../utils/get-nodes-with-sub-canvas';
|
||||
import { EncapsulateBaseValidator } from './encapsulate-base-validator';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulatePortsValidator
|
||||
extends EncapsulateBaseValidator
|
||||
implements EncapsulateNodesValidator
|
||||
{
|
||||
validate(nodes: WorkflowNodeEntity[], result) {
|
||||
getNodesWithSubCanvas(nodes).forEach(node => {
|
||||
const ignoreNodes = [
|
||||
StandardNodeType.Comment,
|
||||
FlowNodeBaseType.SUB_CANVAS,
|
||||
];
|
||||
if (ignoreNodes.includes(node.flowNodeType as StandardNodeType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const portsData = node.getData<WorkflowNodePortsData>(
|
||||
WorkflowNodePortsData,
|
||||
);
|
||||
|
||||
const hasNotConnectPort = portsData.allPorts.some(
|
||||
port => port.lines.length === 0,
|
||||
);
|
||||
if (hasNotConnectPort) {
|
||||
const sourceName = this.getNodeName(node);
|
||||
const sourceIcon = this.getNodeIcon(node);
|
||||
|
||||
result.addError({
|
||||
code: EncapsulateValidateErrorCode.INVALID_PORTS,
|
||||
message: I18n.t(
|
||||
'workflow_encapsulate_button_unable_uncomplete',
|
||||
undefined,
|
||||
'封装不应该包含没有输入输出的节点',
|
||||
),
|
||||
source: node.id,
|
||||
sourceName,
|
||||
sourceIcon,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
includeStartEnd: true;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { type WorkflowJSON } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateValidateError,
|
||||
type EncapsulateValidateResult,
|
||||
type EncapsulateWorkflowJSONValidator,
|
||||
} from '../validate';
|
||||
import { EncapsulateApiService } from '../api';
|
||||
import { EncapsulateBaseValidator } from './encapsulate-base-validator';
|
||||
|
||||
@injectable()
|
||||
export class EncapsulateSchemaValidator
|
||||
extends EncapsulateBaseValidator
|
||||
implements EncapsulateWorkflowJSONValidator
|
||||
{
|
||||
@inject(EncapsulateApiService)
|
||||
private encapsulateApiService: EncapsulateApiService;
|
||||
|
||||
async validate(workflow: WorkflowJSON, result: EncapsulateValidateResult) {
|
||||
const validateResult =
|
||||
await this.encapsulateApiService.validateWorkflow(workflow);
|
||||
|
||||
if (!validateResult?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errors = validateResult || [];
|
||||
|
||||
errors.forEach(error => {
|
||||
const nodeId = error.node_error?.node_id || error.path_error?.start || '';
|
||||
const node = this.workflowDocument.getNode(nodeId);
|
||||
let sourceName: EncapsulateValidateError['sourceName'] = undefined;
|
||||
let sourceIcon: EncapsulateValidateError['sourceIcon'] = undefined;
|
||||
let source: EncapsulateValidateError['source'] = undefined;
|
||||
|
||||
if (node) {
|
||||
sourceName = this.getNodeName(node);
|
||||
sourceIcon = this.getNodeIcon(node);
|
||||
source = node.id;
|
||||
}
|
||||
|
||||
result.addError({
|
||||
code: EncapsulateValidateErrorCode.INVALID_SCHEMA,
|
||||
message: error.message || '',
|
||||
source,
|
||||
sourceName,
|
||||
sourceIcon,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { bindContributions } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import {
|
||||
EncapsulateNodesValidator,
|
||||
EncapsulateWorkflowJSONValidator,
|
||||
EncapsulateNodeValidator,
|
||||
} from '../validate';
|
||||
import { SubCanvasValidator } from './sub-canvas-validator';
|
||||
import { StartEndValidator } from './start-end-validator';
|
||||
import { LoopNodesValidator } from './loop-nodes-validator';
|
||||
import { EncapsulateSchemaValidator } from './encapsulate-schema-validator';
|
||||
import { EncapsulatePortsValidator } from './encapsulate-ports-validator';
|
||||
import { EncapsulateOutputLinesValidator } from './encapsulate-output-lines-validator';
|
||||
import { EncapsulateInputLinesValidator } from './encapsulate-input-lines-validator';
|
||||
import { EncapsulateFormValidator } from './encapsulate-form-validator';
|
||||
|
||||
export const EncapsulateValidatorsContainerModule = new ContainerModule(
|
||||
bind => {
|
||||
// json validators
|
||||
bindContributions(bind, EncapsulateSchemaValidator, [
|
||||
EncapsulateWorkflowJSONValidator,
|
||||
]);
|
||||
// nodes validators
|
||||
[
|
||||
EncapsulatePortsValidator,
|
||||
EncapsulateInputLinesValidator,
|
||||
EncapsulateOutputLinesValidator,
|
||||
StartEndValidator,
|
||||
LoopNodesValidator,
|
||||
SubCanvasValidator,
|
||||
].forEach(Validator => {
|
||||
bindContributions(bind, Validator, [EncapsulateNodesValidator]);
|
||||
});
|
||||
// node validator
|
||||
[EncapsulateFormValidator].forEach(Validator => {
|
||||
bindContributions(bind, Validator, [EncapsulateNodeValidator]);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -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 * from './encapsulate-validators-container-module';
|
||||
export { EncapsulateBaseValidator } from './encapsulate-base-validator';
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { StandardNodeType } from '@coze-workflow/base/types';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateNodesValidator,
|
||||
} from '../validate';
|
||||
|
||||
@injectable()
|
||||
export class LoopNodesValidator implements EncapsulateNodesValidator {
|
||||
validate(nodes, result) {
|
||||
const filtered = nodes.filter(node =>
|
||||
[StandardNodeType.Break, StandardNodeType.Continue].includes(
|
||||
node.flowNodeType,
|
||||
),
|
||||
);
|
||||
|
||||
if (filtered.length) {
|
||||
result.addError({
|
||||
code: EncapsulateValidateErrorCode.INVALID_LOOP_NODES,
|
||||
message: I18n.t(
|
||||
'workflow_encapsulate_button_unable_continue_or_teiminate',
|
||||
undefined,
|
||||
'框选范围内包含继续循环/终止循环',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { injectable } from 'inversify';
|
||||
import { StandardNodeType } from '@coze-workflow/base/types';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import {
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateNodesValidator,
|
||||
} from '../validate';
|
||||
|
||||
@injectable()
|
||||
export class StartEndValidator implements EncapsulateNodesValidator {
|
||||
validate(nodes, result) {
|
||||
const filtered = nodes.filter(node =>
|
||||
[StandardNodeType.Start, StandardNodeType.End].includes(
|
||||
node.flowNodeType,
|
||||
),
|
||||
);
|
||||
|
||||
if (filtered.length) {
|
||||
result.addError({
|
||||
code: EncapsulateValidateErrorCode.NO_START_END,
|
||||
message: I18n.t(
|
||||
'workflow_encapsulate_button_unable_start_or_end',
|
||||
undefined,
|
||||
'框选范围内包含开始/结束',
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
includeStartEnd = true;
|
||||
}
|
||||
@@ -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 { injectable } from 'inversify';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type WorkflowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { getSubCanvasParent, isSubCanvasNode } from '@/utils/subcanvas';
|
||||
|
||||
import {
|
||||
type EncapsulateNodesValidator,
|
||||
EncapsulateValidateErrorCode,
|
||||
type EncapsulateValidateResult,
|
||||
} from '../validate';
|
||||
import { EncapsulateBaseValidator } from './encapsulate-base-validator';
|
||||
|
||||
@injectable()
|
||||
export class SubCanvasValidator
|
||||
extends EncapsulateBaseValidator
|
||||
implements EncapsulateNodesValidator
|
||||
{
|
||||
validate(nodes: WorkflowNodeEntity[], result: EncapsulateValidateResult) {
|
||||
nodes
|
||||
.filter(node => isSubCanvasNode(node))
|
||||
.forEach(subCanvasNode => {
|
||||
const parent = getSubCanvasParent(subCanvasNode);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceName = this.getNodeName(subCanvasNode);
|
||||
const sourceIcon = this.getNodeIcon(subCanvasNode);
|
||||
if (!nodes.includes(parent)) {
|
||||
result.addError({
|
||||
code: EncapsulateValidateErrorCode.INVALID_SUB_CANVAS,
|
||||
message: I18n.t('workflow_encapsulate_button_unable_loop_or_batch'),
|
||||
source: subCanvasNode.id,
|
||||
sourceName,
|
||||
sourceIcon,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
|
||||
import {
|
||||
EncapsulateValidateService,
|
||||
EncapsulateValidateServiceImpl,
|
||||
EncapsulateValidateManager,
|
||||
EncapsulateValidateManagerImpl,
|
||||
EncapsulateValidateResult,
|
||||
EncapsulateValidateResultImpl,
|
||||
EncapsulateValidateResultFactory,
|
||||
} from './validate';
|
||||
import {
|
||||
EncapsulateGenerateService,
|
||||
EncapsulateGenerateServiceImpl,
|
||||
} from './generate';
|
||||
import { EncapsulateContext } from './encapsulate-context';
|
||||
import {
|
||||
EncapsulateNodesService,
|
||||
EncapsulateService,
|
||||
EncapsulateServiceImpl,
|
||||
EncapsulateManager,
|
||||
EncapsulateManagerImpl,
|
||||
EncapsulateLinesService,
|
||||
EncapsulateVariableService,
|
||||
} from './encapsulate';
|
||||
import { EncapsulateApiService, EncapsulateApiServiceImpl } from './api';
|
||||
|
||||
export const WorkflowEncapsulateContainerModule = new ContainerModule(bind => {
|
||||
// encapsulate
|
||||
bind(EncapsulateService).to(EncapsulateServiceImpl).inSingletonScope();
|
||||
bind(EncapsulateManager).to(EncapsulateManagerImpl).inSingletonScope();
|
||||
bind(EncapsulateNodesService).toSelf().inSingletonScope();
|
||||
bind(EncapsulateLinesService).toSelf().inSingletonScope();
|
||||
bind(EncapsulateVariableService).toSelf().inSingletonScope();
|
||||
|
||||
// validate
|
||||
bind(EncapsulateValidateService)
|
||||
.to(EncapsulateValidateServiceImpl)
|
||||
.inSingletonScope();
|
||||
bind(EncapsulateValidateManager)
|
||||
.to(EncapsulateValidateManagerImpl)
|
||||
.inSingletonScope();
|
||||
|
||||
bind(EncapsulateValidateResult)
|
||||
.to(EncapsulateValidateResultImpl)
|
||||
.inTransientScope();
|
||||
bind(EncapsulateValidateResultFactory).toFactory<EncapsulateValidateResult>(
|
||||
context => () =>
|
||||
context.container.get<EncapsulateValidateResult>(
|
||||
EncapsulateValidateResult,
|
||||
),
|
||||
);
|
||||
|
||||
// generate
|
||||
bind(EncapsulateGenerateService)
|
||||
.to(EncapsulateGenerateServiceImpl)
|
||||
.inSingletonScope();
|
||||
|
||||
// save
|
||||
bind(EncapsulateApiService).to(EncapsulateApiServiceImpl).inSingletonScope();
|
||||
|
||||
// context
|
||||
bind(EncapsulateContext).toSelf().inSingletonScope();
|
||||
});
|
||||
Reference in New Issue
Block a user