feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,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}`;
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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,
});
});
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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,
'框选范围内有中间节点连到框选范围外的节点',
),
});
}
}
}

View 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 { 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,
'框选范围内有中间节点连到框选范围外的节点',
),
});
}
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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,
});
});
}
}

View File

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

View File

@@ -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';

View File

@@ -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,
'框选范围内包含继续循环/终止循环',
),
});
}
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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,
});
}
});
}
}