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,38 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, test, expect, vi } from 'vitest';
import { I18n } from '@coze-arch/i18n';
import { codeEmptyValidator } from '../code-empty-validator';
// 模拟I18n.t方法
vi.mock('@coze-arch/i18n', () => ({
I18n: { t: vi.fn(key => `translated_${key}`) },
}));
describe('codeEmptyValidator', () => {
test('当value.code存在时返回true', () => {
const result = codeEmptyValidator({ value: { code: 'some code' } });
expect(result).toBe(true);
});
test('当value.code不存在时返回翻译后的错误信息', () => {
const result = codeEmptyValidator({ value: {} });
expect(I18n.t).toHaveBeenCalledWith('workflow_running_results_error_code');
expect(result).toBe('translated_workflow_running_results_error_code');
});
});

View File

@@ -0,0 +1,194 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { type FormItemMaterialContext } from '@flowgram-adapter/free-layout-editor';
import { I18n } from '@coze-arch/i18n';
import { nodeMetaValidator } from '../node-meta-validator';
// 模拟 I18n.t 方法
vi.mock('@coze-arch/i18n', () => ({
I18n: { t: vi.fn(key => `translated_${key}`) },
}));
const mockNodesService = {
getAllNodes: vi.fn(),
getNodeTitle: vi.fn(node => node.title),
};
const baseContext = {
playgroundContext: {
nodesService: mockNodesService,
},
} as FormItemMaterialContext;
describe('nodeMetaValidator', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return true for valid metadata with a unique title', () => {
mockNodesService.getAllNodes.mockReturnValue([
{ id: 'node2', title: 'AnotherNode' },
]);
const result = nodeMetaValidator({
value: { title: 'ValidTitle' },
context: baseContext,
options: {},
});
expect(result).toBe(true);
});
it('should return an error string for an empty title', () => {
// Ensure isTitleRepeated returns false to isolate schema validation
mockNodesService.getAllNodes.mockReturnValue([]);
const result = nodeMetaValidator({
value: { title: '' },
context: baseContext,
options: {},
});
// The I18n.t mock for 'workflow_detail_node_name_error_empty' is associated with the .string({...}) definition.
// This key is used when Zod creates the error message for the .min(1) rule.
// The I18n.t call for 'workflow_node_title_duplicated' happens inside nodeMetaValidator
// when the refine schema is built.
expect(I18n.t).toHaveBeenCalledWith('workflow_node_title_duplicated');
const parsedResult = JSON.parse(result as string);
expect(parsedResult.issues[0].message).toBe(
I18n.t('workflow_detail_node_name_error_empty'),
);
expect(parsedResult.issues[0].path).toEqual(['title']);
});
it('should return an error string for a title exceeding max length', () => {
// Ensure isTitleRepeated returns false to isolate schema validation
mockNodesService.getAllNodes.mockReturnValue([]);
const longTitle = 'a'.repeat(64);
const result = nodeMetaValidator({
value: { title: longTitle },
context: baseContext,
options: {},
});
// The I18n.t mock for 'workflow_derail_node_detail_title_max' is associated with the .regex({...}) definition.
// This key is used when Zod creates the error message for the regex rule.
// The I18n.t call for 'workflow_node_title_duplicated' happens inside nodeMetaValidator
// when the refine schema is built.
expect(I18n.t).toHaveBeenCalledWith('workflow_node_title_duplicated');
const parsedResult = JSON.parse(result as string);
expect(parsedResult.issues[0].message).toBe(
I18n.t('workflow_derail_node_detail_title_max', { max: '63' }),
);
expect(parsedResult.issues[0].path).toEqual(['title']);
});
it('should return an error string for a duplicated title', () => {
mockNodesService.getAllNodes.mockReturnValue([
{ id: 'node1', title: 'ExistingTitle' },
{ id: 'node2', title: 'AnotherNode' },
{ id: 'node2', title: 'ExistingTitle' }, // 这里模拟一个重复的标题
]);
const result = nodeMetaValidator({
value: { title: 'ExistingTitle' },
context: baseContext,
options: {},
});
// The validator returns a stringified Zod error object when parsed.success is false.
// The 'workflow_node_title_duplicated' message comes from the refine function.
const parsedResult = JSON.parse(result as string);
expect(parsedResult.issues[0].message).toBe(
I18n.t('workflow_node_title_duplicated'),
);
expect(parsedResult.issues[0].path).toEqual(['title']);
// Ensure I18n.t was called for the duplication message within the refine logic.
expect(I18n.t).toHaveBeenCalledWith('workflow_node_title_duplicated');
});
it('should return true for valid metadata with optional fields', () => {
mockNodesService.getAllNodes.mockReturnValue([]);
const result = nodeMetaValidator({
value: {
title: 'ValidTitleWithExtras',
icon: 'icon.png',
subtitle: 'A subtitle',
description: 'A description',
},
context: baseContext,
options: {},
});
expect(result).toBe(true);
});
it('should return true if title is empty when checking for duplicates (isTitleRepeated returns false)', () => {
// isTitleRepeated returns false if title is empty, so it should pass this specific check
// but it will fail the .min(1) check from Zod schema
mockNodesService.getAllNodes.mockReturnValue([
{ id: 'node1', title: 'ExistingTitle' },
]);
const result = nodeMetaValidator({
value: { title: '' }, // Empty title
context: baseContext,
options: {},
});
// This will fail the zod schema's .min(1) check first
const parsedResult = JSON.parse(result as string);
expect(parsedResult.issues[0].message).toBe(
'translated_workflow_detail_node_name_error_empty',
);
});
it('should correctly handle nodesService.getNodeTitle returning different structure if applicable', () => {
// This test is to ensure getNodeTitle mock is robust or to highlight if it needs adjustment
// For example, if getNodeTitle actually expects a more complex node object
const mockComplexNode = { id: 'nodeC', data: { name: 'ComplexNodeTitle' } };
mockNodesService.getAllNodes.mockReturnValue([mockComplexNode]);
// Adjusting the mock for getNodeTitle if it's more complex than just node.title
const originalGetNodeTitle = mockNodesService.getNodeTitle;
mockNodesService.getNodeTitle = vi.fn(node => node.data.name);
const result = nodeMetaValidator({
value: { title: 'TestComplex' },
context: baseContext,
options: {},
});
expect(result).toBe(true);
// Restore original mock
mockNodesService.getNodeTitle = originalGetNodeTitle;
});
it('should return true when title is not duplicated and nodes exist', () => {
mockNodesService.getAllNodes.mockReturnValue([
{ id: 'node1', title: 'AnotherTitle1' },
{ id: 'node2', title: 'AnotherTitle2' },
]);
const result = nodeMetaValidator({
value: { title: 'UniqueTitle' },
context: baseContext,
options: {},
});
expect(result).toBe(true);
});
it('should handle when getAllNodes returns an empty array (no nodes)', () => {
mockNodesService.getAllNodes.mockReturnValue([]);
const result = nodeMetaValidator({
value: { title: 'FirstNodeTitle' },
context: baseContext,
options: {},
});
expect(result).toBe(true);
});
});

View File

@@ -0,0 +1,204 @@
/*
* 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 { ZodIssueCode } from 'zod';
import { describe, it, vi, expect } from 'vitest';
import { questionOptionValidator } from '../question-option-validator';
// 模拟 I18n.t 方法
vi.mock('@coze-arch/i18n', () => ({
I18n: { t: vi.fn(key => `translated_${key}`) },
}));
describe('questionOptionValidator', () => {
it('should return true for a valid non-empty unique array', () => {
const value = [
{ name: 'Option 1', id: '1' },
{ name: 'Option 2', id: '2' },
];
expect(
questionOptionValidator({ value, context: {} as any, options: {} }),
).toBe(true);
});
it('should return true for an empty array', () => {
const value: Array<{ name?: string; id: string }> = [];
expect(
questionOptionValidator({ value, context: {} as any, options: {} }),
).toBe(true);
});
it('should return error JSON for array with empty name', () => {
const value = [
{ name: 'Option 1', id: '1' },
{ name: '', id: '2' },
];
const result = questionOptionValidator({
value,
context: {} as any,
options: {},
});
expect(typeof result).toBe('string');
const errors = JSON.parse(result as string);
expect(errors.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: ZodIssueCode.custom,
message: 'translated_workflow_ques_option_notempty',
path: [1],
}),
]),
);
});
it('should return error JSON for array with whitespace name', () => {
const value = [
{ name: 'Option 1', id: '1' },
{ name: ' ', id: '2' },
];
const result = questionOptionValidator({
value,
context: {} as any,
options: {},
});
expect(typeof result).toBe('string');
const errors = JSON.parse(result as string);
expect(errors.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: ZodIssueCode.custom,
message: 'translated_workflow_ques_option_notempty',
path: [1],
}),
]),
);
});
it('should return error JSON for array with duplicate names', () => {
const value = [
{ name: 'Option 1', id: '1' },
{ name: 'Option 1', id: '2' },
];
const result = questionOptionValidator({
value,
context: {} as any,
options: {},
});
expect(typeof result).toBe('string');
const errors = JSON.parse(result as string);
expect(errors.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: ZodIssueCode.custom,
message: 'translated_workflow_ques_ans_testrun_dulpicate',
path: [1],
}),
]),
);
});
it('should return error JSON for array with both empty and duplicate names', () => {
const value = [
{ name: 'Option 1', id: '1' },
{ name: '', id: '2' },
{ name: 'Option 1', id: '3' },
];
const result = questionOptionValidator({
value,
context: {} as any,
options: {},
});
expect(typeof result).toBe('string');
const errors = JSON.parse(result as string);
expect(errors.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: ZodIssueCode.custom,
message: 'translated_workflow_ques_option_notempty',
path: [1],
}),
expect.objectContaining({
code: ZodIssueCode.custom,
message: 'translated_workflow_ques_ans_testrun_dulpicate',
path: [2],
}),
]),
);
});
it('should return error JSON when value is undefined', () => {
const value = undefined;
const result = questionOptionValidator({
value: value as any,
context: {} as any,
options: {},
}); // Cast to any to bypass TS check for test
expect(typeof result).toBe('string');
const errors = JSON.parse(result as string);
// Zod will throw a different type of error for undefined input on a non-optional schema
expect(errors.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: ZodIssueCode.invalid_type,
expected: 'array',
received: 'undefined',
}),
]),
);
});
it('should return error JSON when value is null', () => {
const value = null;
const result = questionOptionValidator({
value: value as any,
context: {} as any,
options: {},
}); // Cast to any to bypass TS check for test
expect(typeof result).toBe('string');
const errors = JSON.parse(result as string);
expect(errors.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: ZodIssueCode.invalid_type,
expected: 'array',
received: 'null',
}),
]),
);
});
it('should return error JSON for array with name missing (undefined)', () => {
const value = [{ id: '1' }, { name: 'Option 2', id: '2' }]; // name is undefined for the first item
const result = questionOptionValidator({
value: value as any,
context: {} as any,
options: {},
});
expect(typeof result).toBe('string');
const errors = JSON.parse(result as string);
expect(errors.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: ZodIssueCode.invalid_type, // Zod expects a string for name
expected: 'string',
received: 'undefined',
path: [0, 'name'],
}),
]),
);
});
});

View File

@@ -0,0 +1,133 @@
/*
* 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 { describe, it, expect, vi } from 'vitest';
import { settingOnErrorValidator } from '../setting-on-error-validator';
vi.mock('@flowgram-adapter/free-layout-editor', () => ({}));
vi.mock('../setting-on-error', () => ({
SettingOnErrorProcessType: {
STOP: 1,
RETURN: 2,
BACKUP: 3,
},
}));
// Mock I18n.t
vi.mock('@coze-arch/i18n', () => ({
I18n: {
t: vi.fn(key => key),
},
}));
describe('settingOnErrorValidator', () => {
it('should return true if value is undefined', () => {
expect(settingOnErrorValidator({ value: undefined } as any)).toBe(true);
});
it('should return true if value is null', () => {
expect(settingOnErrorValidator({ value: null } as any)).toBe(true);
});
it('should return true if settingOnErrorIsOpen is false', () => {
const value = {
settingOnErrorIsOpen: false,
settingOnErrorJSON: '{"key":"value"}',
processType: 1,
};
expect(settingOnErrorValidator({ value } as any)).toBe(true);
});
it('should return true if settingOnErrorIsOpen is not presented', () => {
const value = {
processType: 1,
timeoutMs: 180000,
retryTimes: 0,
};
expect(settingOnErrorValidator({ value } as any)).toBe(true);
});
it('should return true if settingOnErrorIsOpen is not presented, and selected a backup model', () => {
const value = {
processType: 1,
timeoutMs: 180000,
retryTimes: 1,
ext: {
backupLLmParam: {
temperature: '1',
maxTokens: '2200',
responseFormat: 2,
modelName: 'DeepSeek-R1/250528',
modelType: 1748588801,
generationDiversity: 'default_val',
},
},
};
expect(settingOnErrorValidator({ value } as any)).toBe(true);
});
it('should return error string if settingOnErrorIsOpen is true, processType is RETURN, and settingOnErrorJSON is invalid JSON', () => {
const value = {
settingOnErrorIsOpen: true,
settingOnErrorJSON: 'invalid-json',
processType: 2,
timeoutMs: 180000,
retryTimes: 1,
ext: {
backupLLmParam: {
temperature: '1',
maxTokens: '2200',
responseFormat: 2,
modelName: 'DeepSeek-R1/250528',
modelType: 1748588801,
generationDiversity: 'default_val',
},
},
};
const result = settingOnErrorValidator({ value } as any);
expect(result).toBeTypeOf('string');
if (typeof result === 'string') {
const parsedResult = JSON.parse(result);
expect(parsedResult.issues[0].message).toBe(
'workflow_exception_ignore_json_error',
);
}
});
it('should return error string if settingOnErrorIsOpen is true, processType is RETURN, and settingOnErrorJSON is valid JSON', () => {
const value = {
settingOnErrorIsOpen: true,
settingOnErrorJSON: '{\n "output": "hello"\n}',
processType: 2,
timeoutMs: 180000,
retryTimes: 1,
ext: {
backupLLmParam: {
temperature: '1',
maxTokens: '2200',
responseFormat: 2,
modelName: 'DeepSeek-R1/250528',
modelType: 1748588801,
generationDiversity: 'default_val',
},
},
};
const result = settingOnErrorValidator({ value } as any);
expect(result).toBe(true);
});
});

View File

@@ -0,0 +1,114 @@
/*
* 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 { describe, it, expect, vi, beforeEach } from 'vitest';
import { I18n } from '@coze-arch/i18n';
import { systemVariableValidator } from '../system-variable-validator';
// Mock I18n
vi.mock('@coze-arch/i18n', () => ({
I18n: {
t: vi.fn(key => {
if (key === 'variable_240416_01') {
return 'System variables cannot start with sys_';
}
return `translated_${key}`;
}),
},
}));
const mockContext = {} as any; // ValidatorProps context is not used by this validator
describe('systemVariableValidator', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return true for a valid variable name', () => {
const result = systemVariableValidator({
value: 'my_variable',
context: mockContext,
options: {},
});
expect(result).toBe(true);
});
it('should return true for a variable name with leading/trailing spaces that is otherwise valid', () => {
const result = systemVariableValidator({
value: ' my_variable ',
context: mockContext,
options: {},
});
expect(result).toBe(true);
});
it('should return an error message for a variable name starting with "sys_"', () => {
const result = systemVariableValidator({
value: 'sys_variable',
context: mockContext,
options: {},
});
expect(result).toBe('System variables cannot start with sys_');
expect(I18n.t).toHaveBeenCalledWith('variable_240416_01');
});
it('should return an error message for a variable name starting with "sys_" even with leading/trailing spaces', () => {
const result = systemVariableValidator({
value: ' sys_variable ',
context: mockContext,
options: {},
});
expect(result).toBe('System variables cannot start with sys_');
expect(I18n.t).toHaveBeenCalledWith('variable_240416_01');
});
it('should return true for an empty string after trimming', () => {
const result = systemVariableValidator({
value: ' ',
context: mockContext,
options: {},
});
expect(result).toBe(true);
});
it('should return true for an empty string input', () => {
const result = systemVariableValidator({
value: '',
context: mockContext,
options: {},
});
expect(result).toBe(true);
});
it('should return true for a null input value', () => {
const result = systemVariableValidator({
value: null as any,
context: mockContext,
options: {},
});
expect(result).toBe(true);
});
it('should return true for an undefined input value', () => {
const result = systemVariableValidator({
value: undefined as any,
context: mockContext,
options: {},
});
expect(result).toBe(true);
});
});

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { I18n } from '@coze-arch/i18n';
export function codeEmptyValidator({ value }) {
const code = value?.code;
if (!code) {
return I18n.t('workflow_running_results_error_code');
}
return true;
}

View File

@@ -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 { nodeMetaValidator } from './node-meta-validator';
export { outputTreeValidator } from './output-tree-validator';
export { systemVariableValidator } from './system-variable-validator';
export { codeEmptyValidator } from './code-empty-validator';
export { questionOptionValidator } from './question-option-validator';
export { settingOnErrorValidator } from './setting-on-error-validator';
export { inputTreeValidator } from './input-tree-validator';

View File

@@ -0,0 +1,177 @@
/*
* 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 { z, type ZodSchema } from 'zod';
import {
ValueExpression,
ValueExpressionType,
type InputValueVO,
} from '@coze-workflow/base/types';
import { I18n } from '@coze-arch/i18n';
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { type PlaygroundContext } from '@flowgram-adapter/free-layout-editor';
import { VARIABLE_NAME_REGEX } from '../constants';
type Path = string | number;
interface Issue {
path: Path[];
message: string;
}
/**
* 输入树校验器
*/
export class InputTreeValidator {
private node: FlowNodeEntity;
private playgroundContext: PlaygroundContext;
private issues: Issue[] = [];
constructor(node: FlowNodeEntity, playgroundContext: PlaygroundContext) {
this.node = node;
this.playgroundContext = playgroundContext;
}
/**
* 校验函数
* @param inputalues
* @reurns
*/
validate(inputValues: InputValueVO[]): Issue[] {
this.issues = [];
this.validateInputValues(inputValues);
return this.issues;
}
/**
* 校验多个输入
* @param inputValues
* @param path
* @returns
*/
private validateInputValues(inputValues: InputValueVO[], path: Path[] = []) {
if (!inputValues) {
return;
}
for (let i = 0; i < inputValues.length; i++) {
const inputValue = inputValues[i] || {};
const rules = {
name: this.validateName,
input: this.validateInput,
};
Object.keys(rules).forEach(key => {
const message = rules[key].bind(this)({
value: inputValue[key],
values: inputValues,
});
if (message) {
this.issues.push({
message,
path: path.concat(i, key),
});
}
});
const children = inputValues[i]?.children || [];
// 递归检查子节点
this.validateInputValues(children, path.concat(i, 'children'));
}
}
/**
* 输入名称校验
*/
private validateName({ value, values }) {
if (!value) {
return I18n.t('workflow_detail_node_error_name_empty');
}
const names = values.map(v => v.name).filter(Boolean);
// 名称格式校验
if (!VARIABLE_NAME_REGEX.test(value)) {
return I18n.t('workflow_detail_node_error_format');
}
// 重名校验
const foundSames = names.filter((name: string) => name === value);
return foundSames.length > 1
? I18n.t('workflow_detail_node_input_duplicated')
: undefined;
}
/**
* 输入值校验
*/
private validateInput({ value }) {
const { variableValidationService } = this.playgroundContext;
// 校验空值
if (ValueExpression.isEmpty(value)) {
return I18n.t('workflow_detail_node_error_empty');
}
if (value?.type === ValueExpressionType.REF) {
return variableValidationService.isRefVariableEligible(value, this.node);
}
}
}
export function inputTreeValidator(params: ValidatorProps<InputValueVO>) {
const {
value,
context: { playgroundContext, node },
} = params;
const validator = new InputTreeValidator(node, playgroundContext);
const InputTreeNodeSchema: ZodSchema<any> = z.lazy(() =>
z
.object({
name: z.string().optional(),
input: z.any(),
children: z.array(InputTreeNodeSchema).optional(),
})
.passthrough(),
);
const InputTreeSchema = z
.array(InputTreeNodeSchema)
.superRefine((data, ctx) => {
const issues = validator.validate(data);
issues.forEach(issue => {
ctx.addIssue({
path: issue.path,
message: issue.message,
// FIXME: 表单校验底层依赖了 validation / code去掉就跑不通了
validation: 'regex',
code: 'invalid_string',
});
});
});
const parsed = InputTreeSchema.safeParse(value);
if (!parsed.success) {
return JSON.stringify((parsed as any).error);
}
return true;
}

View File

@@ -0,0 +1,49 @@
/*
* 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 Ajv from 'ajv';
import {
variableUtils,
generateInputJsonSchema,
} from '@coze-workflow/variable';
import { type ViewVariableMeta } from '@coze-workflow/base';
let ajv;
export const jsonSchemaValidator = (
v: string,
viewVariableMeta: ViewVariableMeta,
): boolean => {
if (!viewVariableMeta || !v) {
return true;
}
const dtoMeta = variableUtils.viewMetaToDTOMeta(viewVariableMeta);
const jsonSchema = generateInputJsonSchema(dtoMeta);
if (!jsonSchema) {
return true;
}
if (!ajv) {
ajv = new Ajv();
}
try {
const validate = ajv.compile(jsonSchema);
const valid = validate(JSON.parse(v));
return valid;
// eslint-disable-next-line @coze-arch/use-error-in-catch
} catch (error) {
// parse失败说明不是合法值
return false;
}
};

View File

@@ -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 { z } from 'zod';
import { I18n } from '@coze-arch/i18n';
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
const NodeMetaSchema = z.object({
title: z
.string({
required_error: I18n.t('workflow_detail_node_name_error_empty'),
})
.min(1, I18n.t('workflow_detail_node_name_error_empty'))
// .regex(
// /^[a-zA-Z][a-zA-Z0-9_-]{0,}$/,
// I18n.t('workflow_detail_node_error_format'),
// )
.regex(
/^.{0,63}$/,
I18n.t('workflow_derail_node_detail_title_max', {
max: '63',
}),
),
icon: z.string().optional(),
subtitle: z.string().optional(),
description: z.string().optional(),
});
type NodeMeta = z.infer<typeof NodeMetaSchema>;
export const nodeMetaValidator = ({
value,
context,
}: ValidatorProps<NodeMeta>) => {
const { playgroundContext } = context;
function isTitleRepeated(title: string) {
if (!title) {
return false;
}
const { nodesService } = playgroundContext;
const nodes = nodesService
.getAllNodes()
.filter(node => nodesService.getNodeTitle(node) === title);
return nodes?.length > 1;
}
// 增加节点名重复校验
const schema = NodeMetaSchema.refine(
({ title }: NodeMeta) => !isTitleRepeated(title),
{
message: I18n.t('workflow_node_title_duplicated'),
path: ['title'],
},
);
const parsed = schema.safeParse(value);
if (!parsed.success) {
return JSON.stringify((parsed as any).error);
}
return true;
};

View File

@@ -0,0 +1,44 @@
/*
* 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 ValidatorProps } from '@flowgram-adapter/free-layout-editor';
import {
OutputTreeSchema,
OutputTreeUniqueNameSchema,
type OutputTree,
} from './schema';
export function outputTreeValidator(
params: ValidatorProps<
OutputTree,
{
uniqueName?: boolean;
}
>,
) {
const { value, options } = params;
const { uniqueName = false } = options;
const parsed = uniqueName
? OutputTreeUniqueNameSchema.safeParse(value)
: OutputTreeSchema.safeParse(value);
if (!parsed.success) {
return JSON.stringify((parsed as any).error);
}
return true;
}

View File

@@ -0,0 +1,142 @@
/*
* 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 { z, type ZodSchema } from 'zod';
import { ViewVariableType } from '@coze-workflow/base';
import { I18n } from '@coze-arch/i18n';
import { jsonSchemaValidator } from '../json-schema-validator';
// 定义节点Schema
const OutputTreeNodeSchema: ZodSchema<any> = z.lazy(() =>
z
.object({
name: z
.string({
required_error: I18n.t('workflow_detail_node_error_name_empty'),
})
.min(1, I18n.t('workflow_detail_node_error_name_empty'))
.regex(
/^(?!.*\b(true|false|and|AND|or|OR|not|NOT|null|nil|If|Switch)\b)[a-zA-Z_][a-zA-Z_$0-9]*$/,
I18n.t('workflow_detail_node_error_format'),
),
type: z.number(),
children: z.array(OutputTreeNodeSchema).optional(),
defaultValue: z.any().optional(),
})
.passthrough(),
);
export const OutputTreeSchema = z.array(OutputTreeNodeSchema);
// 定义一个辅助函数,用于查找重复名字的节点并返回错误路径
const findDuplicates = (nodes, path = []) => {
const seen = new Set();
let result: {
path: (string | number)[];
message: string;
};
for (let i = 0; i < nodes.length; i++) {
const { name } = nodes[i];
if (seen.has(name)) {
// 找到重复项时返回路径和错误信息
result = {
// @ts-expect-error -- linter-disable-autofix
path: path.concat(i, 'name'),
message: I18n.t('workflow_detail_node_error_variablename_duplicated'),
};
break;
}
seen.add(name);
if (nodes[i].children) {
// 递归检查子节点
const found = findDuplicates(
nodes[i].children,
// @ts-expect-error -- linter-disable-autofix
path.concat(i, 'children'),
);
if (found) {
result = found;
break;
}
}
}
// @ts-expect-error -- linter-disable-autofix
return result;
};
// 定义同级命名唯一的树结构Schema
export const OutputTreeUniqueNameSchema = z
.array(OutputTreeNodeSchema)
.refine(
data => {
// 使用自定义函数进行检查
const duplicate = findDuplicates(data);
return !duplicate;
},
data => {
// 使用自定义函数进行检查
const duplicate = findDuplicates(data);
return {
path: duplicate.path,
message: duplicate.message,
// FIXME: 表单校验底层依赖了 validation / code去掉就跑不通了
validation: 'regex',
code: 'invalid_string',
};
},
)
.superRefine((data, ctx) => {
// 使用自定义函数进行检查
const issues = checkObjectDefaultValue(data);
issues.forEach(issue => {
ctx.addIssue({
path: issue.path,
message: issue.message,
// FIXME: 表单校验底层依赖了 validation / code去掉就跑不通了
validation: 'regex',
code: 'invalid_string',
});
});
});
const checkObjectDefaultValue = nodes => {
const result: Array<{
path: (string | number)[];
message: string;
}> = [];
for (let i = 0; i < nodes.length; i++) {
const { defaultValue, type } = nodes[i];
if (typeof defaultValue !== 'string' || !defaultValue) {
continue;
}
if (!ViewVariableType.isJSONInputType(type)) {
continue;
}
if (!jsonSchemaValidator(defaultValue, nodes[i])) {
// 找到重复项时返回路径和错误信息
result.push({
path: [i, 'defaultValue'],
message: I18n.t('workflow_debug_wrong_json'),
});
}
// json 类型只检查第一层,不需要递归检查
}
return result;
};
// 导出类型别名
export type OutputTree = z.infer<typeof OutputTreeSchema>;

View File

@@ -0,0 +1,71 @@
/*
* 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 { z, ZodIssueCode } from 'zod';
import { I18n } from '@coze-arch/i18n';
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
// 自定义验证器,检查数组是否为空,并且没有重复的值
const nonEmptyUniqueArray = z
.array(
z.object({
name: z.string(),
}),
)
.superRefine((val, ctx) => {
const seenValues = new Set();
val.forEach((item, idx) => {
// 检查非空
if (item.name.trim() === '') {
ctx.addIssue({
code: ZodIssueCode.custom,
message: I18n.t(
'workflow_ques_option_notempty',
{},
'选项内容不可为空',
),
path: [idx],
});
}
// 检查重复
if (seenValues.has(item.name)) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: I18n.t(
'workflow_ques_ans_testrun_dulpicate',
{},
'选项内容不可重复',
),
path: [idx],
});
} else {
seenValues.add(item.name);
}
});
});
export function questionOptionValidator({
value,
}: ValidatorProps<Array<{ name?: string; id: string }>>) {
try {
nonEmptyUniqueArray.parse(value);
} catch (error) {
return JSON.stringify(error);
}
return true;
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { z } from 'zod';
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
import { I18n } from '@coze-arch/i18n';
import { SettingOnErrorProcessType } from '../setting-on-error/types';
const SettingOnErrorSchema = z.object({
settingOnErrorIsOpen: z.boolean().optional(),
settingOnErrorJSON: z.string().optional(),
processType: z.number().optional(),
});
type SettingOnError = z.infer<typeof SettingOnErrorSchema>;
export const settingOnErrorValidator = ({
value,
}: ValidatorProps<SettingOnError>) => {
if (!value) {
return true;
}
function isJSONVerified(settingOnError: SettingOnError) {
if (settingOnError?.settingOnErrorIsOpen) {
if (
settingOnError?.processType &&
settingOnError?.processType !== SettingOnErrorProcessType.RETURN
) {
return true;
}
try {
JSON.parse(settingOnError?.settingOnErrorJSON as string);
// eslint-disable-next-line @coze-arch/use-error-in-catch
} catch (e) {
return false;
}
}
return true;
}
// json 合法性校验
const schemeParesd = SettingOnErrorSchema.refine(
settingOnError => isJSONVerified(settingOnError),
{
message: I18n.t('workflow_exception_ignore_json_error'),
},
).safeParse(value);
if (!schemeParesd.success) {
return JSON.stringify((schemeParesd as any).error);
}
return true;
};

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type ValidatorProps } from '@flowgram-adapter/free-layout-editor';
import { I18n } from '@coze-arch/i18n';
export function systemVariableValidator({ value }: ValidatorProps<string>) {
const trimmed = value?.trim() || '';
if (trimmed.startsWith('sys_')) {
return I18n.t('variable_240416_01') || ' ';
}
return true;
}