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,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { describe, it, expect } from 'vitest';
import { concatTestId } from '../../src/utils/concat-test-id';
describe('concat-test-id', () => {
it('应该正确连接多个测试 ID', () => {
const result = concatTestId('a', 'b', 'c');
expect(result).toBe('a.b.c');
});
it('应该过滤掉空字符串', () => {
const result = concatTestId('a', '', 'c');
expect(result).toBe('a.c');
});
it('应该过滤掉 undefined 和 null', () => {
const result = concatTestId('a', undefined as any, 'c', null as any);
expect(result).toBe('a.c');
});
it('应该在只有一个有效 ID 时正确返回', () => {
const result = concatTestId('a');
expect(result).toBe('a');
});
it('应该在所有 ID 都无效时返回空字符串', () => {
const result = concatTestId('', undefined as any, null as any);
expect(result).toBe('');
});
it('应该在没有参数时返回空字符串', () => {
const result = concatTestId();
expect(result).toBe('');
});
it('应该正确处理包含点号的 ID', () => {
const result = concatTestId('a.x', 'b', 'c.y');
expect(result).toBe('a.x.b.c.y');
});
it('应该正确处理数字 ID', () => {
const result = concatTestId('1', '2', '3');
expect(result).toBe('1.2.3');
});
});

View File

@@ -0,0 +1,157 @@
/*
* 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 { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import type { FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { getFormValueByPathEnds } from '../../src/utils/form-helpers';
// Mock lodash-es
vi.mock('lodash-es', () => ({
cloneDeep: vi.fn(val => {
if (val === undefined) {
return undefined;
}
return JSON.parse(JSON.stringify(val));
}),
}));
describe('form-helpers', () => {
describe('getFormValueByPathEnds', () => {
const createMockFormModel = (paths: Record<string, unknown>) => {
const formItemPathMap = new Map<string, unknown>();
Object.entries(paths).forEach(([path, value]) => {
formItemPathMap.set(path, value);
});
return {
formItemPathMap,
getFormItemValueByPath: vi.fn(path => paths[path]),
};
};
const createMockNode = (
formModel: ReturnType<typeof createMockFormModel>,
) =>
({
getData: vi.fn((dataType: symbol) => {
if (Object.is(dataType, FlowNodeFormData)) {
return { formModel };
}
return null;
}),
getNodeRegistry: () => ({}),
}) as unknown as FlowNodeEntity;
it('应该返回匹配路径结尾的表单项值', () => {
const formModel = createMockFormModel({
'/form/test/value': 'test value',
'/form/other/path': 'other value',
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'test/value');
expect(result).toBe('test value');
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
'/form/test/value',
);
});
it('应该在找不到匹配路径时返回 undefined', () => {
const formModel = createMockFormModel({
'/form/test/value': 'test value',
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'non/existent');
expect(result).toBeUndefined();
expect(formModel.getFormItemValueByPath).not.toHaveBeenCalled();
});
it('应该正确处理复杂对象值', () => {
const complexValue = {
name: 'test',
value: 42,
nested: {
field: 'nested value',
},
};
const formModel = createMockFormModel({
'/form/complex/value': complexValue,
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'complex/value');
expect(result).toEqual(complexValue);
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
'/form/complex/value',
);
});
it('应该返回深拷贝的值而不是引用', () => {
const originalValue = { name: 'test' };
const formModel = createMockFormModel({
'/form/object/value': originalValue,
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'object/value');
expect(result).toEqual(originalValue);
expect(result).not.toBe(originalValue);
});
it('应该在有多个匹配路径时返回第一个匹配的值', () => {
const formModel = createMockFormModel({
'/form/path/test/value': 'first value',
'/other/path/test/value': 'second value',
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'test/value');
expect(result).toBe('first value');
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
'/form/path/test/value',
);
});
it('应该正确处理空值', () => {
const formModel = createMockFormModel({
'/form/empty/value': null,
'/form/undefined/value': undefined,
});
const node = createMockNode(formModel);
const nullResult = getFormValueByPathEnds(node, 'empty/value');
expect(nullResult).toBeNull();
const undefinedResult = getFormValueByPathEnds(node, 'undefined/value');
expect(undefinedResult).toBeUndefined();
});
it('应该正确处理数组值', () => {
const arrayValue = [1, 2, { name: 'test' }];
const formModel = createMockFormModel({
'/form/array/value': arrayValue,
});
const node = createMockNode(formModel);
const result = getFormValueByPathEnds(node, 'array/value');
expect(result).toEqual(arrayValue);
expect(Array.isArray(result)).toBe(true);
});
});
});

View File

@@ -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 { describe, it, expect } from 'vitest';
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
import { isGeneralWorkflow } from '../../src/utils/is-general-workflow';
describe('is-general-workflow', () => {
it('应该在 flowMode 为 Workflow 时返回 true', () => {
expect(isGeneralWorkflow(WorkflowMode.Workflow)).toBe(true);
});
it('应该在 flowMode 为 ChatFlow 时返回 true', () => {
expect(isGeneralWorkflow(WorkflowMode.ChatFlow)).toBe(true);
});
it('应该在 flowMode 为其他值时返回 false', () => {
// 测试其他可能的 WorkflowMode 值
expect(isGeneralWorkflow(WorkflowMode.Imageflow)).toBe(false);
});
});

View File

@@ -0,0 +1,282 @@
/*
* 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 { defaultParser } from '../../../../src/utils/node-result-extractor/parsers';
import { StandardNodeType } from '../../../../src/types';
import type { WorkflowJSON } from '../../../../src/types';
import { TerminatePlanType } from '../../../../src/api';
import type { NodeResult } from '../../../../src/api';
// Mock @coze-arch/bot-utils
vi.mock('@coze-arch/bot-utils', () => ({
typeSafeJSONParse: (str: string) => {
try {
const result = JSON.parse(str);
// 如果是批处理数据,确保返回数组类型
if (str === 'invalid json') {
return str;
}
// 如果是批处理数据,确保返回数组类型
if (str.includes('batch')) {
return Array.isArray(result) ? result : [];
}
return result;
} catch {
// 如果是批处理数据,返回空数组
if (str.includes('batch')) {
return [];
}
return str;
}
},
}));
// Mock parseImagesFromOutputData
vi.mock('../../../../src/utils/output-image-parser', () => ({
parseImagesFromOutputData: vi.fn(({ outputData }) => {
if (typeof outputData === 'string' && outputData.includes('image')) {
return ['https://example.com/image.png'];
}
if (typeof outputData === 'object' && outputData !== null) {
const str = JSON.stringify(outputData);
if (str.includes('image')) {
return ['https://example.com/image.png'];
}
}
return [];
}),
}));
vi.mock('../../../../src/api', () => ({
TerminatePlanType: {
USESETTING: 2,
},
}));
describe('default-parser', () => {
const createMockNodeResult = (
nodeId: string,
overrides: Partial<NodeResult> = {},
): NodeResult => ({
nodeId,
isBatch: false,
input: 'test input',
output: 'test output',
raw_output: 'test raw output',
extra: '{}',
items: '[]',
...overrides,
});
const createMockWorkflowSchema = (
nodeId: string,
nodeType = StandardNodeType.LLM,
): WorkflowJSON => ({
nodes: [
{
id: nodeId,
type: nodeType,
data: {},
},
],
edges: [],
});
describe('非批处理节点', () => {
it('应该正确解析 LLM 节点结果', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.LLM,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeId).toBe('1');
expect(result.nodeType).toBe(StandardNodeType.LLM);
expect(result.isBatch).toBe(false);
expect(result.caseResult).toHaveLength(1);
expect(result.caseResult?.[0].dataList).toHaveLength(3); // 输入、原始输出、最终输出
});
it('应该正确解析 Code 节点结果', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.Code,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBe(StandardNodeType.Code);
expect(result.caseResult?.[0].dataList).toHaveLength(3); // 输入、原始输出、最终输出
});
it('应该正确解析 Start 节点结果', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.Start,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBe(StandardNodeType.Start);
expect(result.caseResult?.[0].dataList).toHaveLength(1); // 只有输入
});
it('应该正确解析 End 节点结果', () => {
const nodeResult = createMockNodeResult('1', {
extra: JSON.stringify({
response_extra: {
terminal_plan: TerminatePlanType.USESETTING,
},
}),
output: JSON.stringify({ content: 'test content' }),
raw_output: JSON.stringify({ content: 'test raw content' }),
});
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.End,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBe(StandardNodeType.End);
expect(result.caseResult?.[0].dataList).toHaveLength(2); // 输出变量、回答内容
});
it('应该正确解析 Message 节点结果', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.Output,
);
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBe(StandardNodeType.Output);
expect(result.caseResult?.[0].dataList).toHaveLength(2); // 输出变量、回答内容
});
it('应该正确解析包含图片的输出', () => {
const nodeResult = createMockNodeResult('1', {
output: JSON.stringify({ content: 'test output with image' }),
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.caseResult?.[0].imgList).toEqual([
'https://example.com/image.png',
]);
});
});
describe('批处理节点', () => {
it('应该正确解析批处理节点结果', () => {
const nodeResult = createMockNodeResult('1', {
isBatch: true,
batch: JSON.stringify([
createMockNodeResult('1'),
createMockNodeResult('1'),
]),
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.isBatch).toBe(true);
expect(result.caseResult).toHaveLength(2);
expect(result.caseResult?.[0].dataList).toBeDefined();
expect(result.caseResult?.[1].dataList).toBeDefined();
});
it('应该正确处理空的批处理结果', () => {
const nodeResult = createMockNodeResult('1', {
isBatch: true,
batch: '[]',
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.isBatch).toBe(true);
expect(result.caseResult).toEqual([]);
});
it('应该正确处理无效的批处理 JSON', () => {
const nodeResult = createMockNodeResult('1', {
isBatch: true,
batch: 'invalid batch json',
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.isBatch).toBe(true);
expect(result.caseResult).toEqual([]);
});
it('应该正确处理批处理中的 null 或 undefined 结果', () => {
const nodeResult = createMockNodeResult('1', {
isBatch: true,
batch: JSON.stringify([null, createMockNodeResult('1'), undefined]),
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(result.isBatch).toBe(true);
expect(result.caseResult).toHaveLength(1);
});
});
describe('特殊情况处理', () => {
it('应该正确处理无效的 JSON 输入', () => {
const nodeResult = createMockNodeResult('1', {
input: 'invalid json',
output: 'invalid json',
raw_output: 'invalid json',
});
const workflowSchema = createMockWorkflowSchema('1');
const result = defaultParser(nodeResult, workflowSchema);
expect(
result.caseResult?.[0].dataList?.some(
item => item.data === 'invalid json',
),
).toBe(true);
});
it('应该正确处理 Text 节点的原始输出', () => {
const nodeResult = createMockNodeResult('1', {
raw_output: '{"key": "value"}', // 即使是有效的 JSON 字符串也应该保持原样
});
const workflowSchema = createMockWorkflowSchema(
'1',
StandardNodeType.Text,
);
const result = defaultParser(nodeResult, workflowSchema);
const rawOutput = result.caseResult?.[0].dataList?.find(
item => item.title === '原始输出',
);
expect(rawOutput?.data).toBe('{"key": "value"}');
});
it('应该正确处理不存在的节点类型', () => {
const nodeResult = createMockNodeResult('1');
const workflowSchema = createMockWorkflowSchema('2'); // 不同的节点 ID
const result = defaultParser(nodeResult, workflowSchema);
expect(result.nodeType).toBeUndefined();
expect(result.caseResult).toBeDefined();
});
});
});

View File

@@ -0,0 +1,130 @@
/*
* 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 } from 'vitest';
import {
type CaseResultData,
type NodeResultExtracted,
} from '../../../src/utils/node-result-extractor/type';
import { StandardNodeType } from '../../../src/types';
describe('node-result-extractor/type', () => {
describe('CaseResultData', () => {
it('应该能够创建基本的 CaseResultData 对象', () => {
const data: CaseResultData = {
dataList: [
{
title: 'test',
data: 'test data',
},
],
imgList: ['https://example.com/image.png'],
};
expect(data.dataList).toHaveLength(1);
expect(data.dataList?.[0].title).toBe('test');
expect(data.dataList?.[0].data).toBe('test data');
expect(data.imgList).toHaveLength(1);
expect(data.imgList?.[0]).toBe('https://example.com/image.png');
});
it('应该允许所有属性为可选', () => {
const data: CaseResultData = {};
expect(data.dataList).toBeUndefined();
expect(data.imgList).toBeUndefined();
});
it('应该允许 dataList 中的 data 为任意类型', () => {
const data: CaseResultData = {
dataList: [
{ title: 'string', data: 'string data' },
{ title: 'number', data: 123 },
{ title: 'boolean', data: true },
{ title: 'object', data: { key: 'value' } },
{ title: 'array', data: [1, 2, 3] },
],
};
expect(data.dataList).toHaveLength(5);
expect(typeof data.dataList?.[0].data).toBe('string');
expect(typeof data.dataList?.[1].data).toBe('number');
expect(typeof data.dataList?.[2].data).toBe('boolean');
expect(typeof data.dataList?.[3].data).toBe('object');
expect(Array.isArray(data.dataList?.[4].data)).toBe(true);
});
});
describe('NodeResultExtracted', () => {
it('应该能够创建基本的 NodeResultExtracted 对象', () => {
const result: NodeResultExtracted = {
nodeId: '123',
nodeType: StandardNodeType.LLM,
isBatch: false,
caseResult: [
{
dataList: [{ title: 'test', data: 'test data' }],
imgList: ['https://example.com/image.png'],
},
],
};
expect(result.nodeId).toBe('123');
expect(result.nodeType).toBe(StandardNodeType.LLM);
expect(result.isBatch).toBe(false);
expect(result.caseResult).toHaveLength(1);
});
it('应该允许所有属性为可选', () => {
const result: NodeResultExtracted = {};
expect(result.nodeId).toBeUndefined();
expect(result.nodeType).toBeUndefined();
expect(result.isBatch).toBeUndefined();
expect(result.caseResult).toBeUndefined();
});
it('应该允许 caseResult 为空数组', () => {
const result: NodeResultExtracted = {
nodeId: '123',
nodeType: StandardNodeType.LLM,
isBatch: false,
caseResult: [],
};
expect(result.caseResult).toHaveLength(0);
});
it('应该允许多个 caseResult', () => {
const result: NodeResultExtracted = {
nodeId: '123',
nodeType: StandardNodeType.LLM,
isBatch: true,
caseResult: [
{
dataList: [{ title: 'case1', data: 'data1' }],
},
{
dataList: [{ title: 'case2', data: 'data2' }],
},
],
};
expect(result.caseResult).toHaveLength(2);
expect(result.caseResult?.[0].dataList?.[0].title).toBe('case1');
expect(result.caseResult?.[1].dataList?.[0].title).toBe('case2');
});
});
});

View File

@@ -0,0 +1,300 @@
/*
* 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 } from 'vitest';
import type { WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
import { parseImagesFromOutputData } from '../../src/utils/output-image-parser';
import {
StandardNodeType,
VariableTypeDTO,
AssistTypeDTO,
} from '../../src/types';
describe('output-image-parser', () => {
describe('parseImagesFromOutputData', () => {
it('应该在没有 nodeSchema 或 outputData 时返回空数组', () => {
expect(parseImagesFromOutputData({})).toEqual([]);
expect(
parseImagesFromOutputData({
outputData: 'test',
nodeSchema: undefined,
}),
).toEqual([]);
expect(
parseImagesFromOutputData({
outputData: undefined,
nodeSchema: {},
}),
).toEqual([]);
});
it('应该在节点类型被排除时返回空数组', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'image',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
],
},
};
expect(
parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
excludeNodeTypes: [StandardNodeType.LLM],
}),
).toEqual([]);
});
it('应该正确解析 End 节点的图片链接', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.End,
data: {
inputs: {
inputParameters: [
{
name: 'image',
input: {
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
},
],
},
},
};
const result = parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该正确解析 Message 节点的图片链接', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.Output,
data: {
inputs: {
inputParameters: [
{
name: 'image',
input: {
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
},
],
},
},
};
const result = parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该正确解析其他节点类型的图片链接', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'image',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
],
},
};
const result = parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该正确解析图片列表', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'images',
type: VariableTypeDTO.list,
schema: {
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
},
],
},
};
const result = parseImagesFromOutputData({
outputData: {
images: [
'https://example.com/image1.png',
'https://example.com/image2.png',
],
},
nodeSchema,
});
expect(result).toEqual([
'https://example.com/image1.png',
'https://example.com/image2.png',
]);
});
it('应该正确解析对象中的图片', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'data',
type: VariableTypeDTO.object,
schema: [
{
name: 'image',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
{
name: 'text',
type: VariableTypeDTO.string,
},
],
},
],
},
};
const result = parseImagesFromOutputData({
outputData: {
data: {
image: 'https://example.com/image.png',
text: 'Some text',
},
},
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该正确处理 SVG 类型的图片', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'svg',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.svg,
},
],
},
};
const result = parseImagesFromOutputData({
outputData: { svg: 'https://example.com/image.svg' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.svg']);
});
it('应该正确处理原生图片类型', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'image',
type: VariableTypeDTO.image,
},
],
},
};
const result = parseImagesFromOutputData({
outputData: { image: 'https://example.com/image.png' },
nodeSchema,
});
expect(result).toEqual(['https://example.com/image.png']);
});
it('应该过滤掉空的图片链接', () => {
const nodeSchema: WorkflowNodeJSON = {
id: '1',
type: StandardNodeType.LLM,
data: {
outputs: [
{
name: 'images',
type: VariableTypeDTO.list,
schema: {
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
},
},
],
},
};
const result = parseImagesFromOutputData({
outputData: {
images: [
'https://example.com/image1.png',
'',
null,
undefined,
'https://example.com/image2.png',
],
},
nodeSchema,
});
expect(result).toEqual([
'https://example.com/image1.png',
'https://example.com/image2.png',
]);
});
});
});

View File

@@ -0,0 +1,121 @@
/*
* 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 } from 'vitest';
import { expressionParser } from '../../../../src/utils/schema-extractor/parsers/expression-parser';
import type { ValueExpressionDTO } from '../../../../src/types/dto';
describe('expression-parser', () => {
it('should handle empty input', () => {
const result = expressionParser([]);
expect(result).toEqual([]);
});
it('should parse string literal expression', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'literal',
content: 'hello',
},
};
const result = expressionParser(expression);
expect(result).toEqual([
{
value: 'hello',
isImage: false,
},
]);
});
it('should parse image url expression', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/tos-cn-i-mdko3gqilj/test.png',
},
};
const result = expressionParser(expression);
expect(result).toEqual([
{
value: 'https://example.com/tos-cn-i-mdko3gqilj/test.png',
isImage: false,
},
]);
});
it('should parse block output expression', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: 'block1',
name: 'output',
},
},
};
const result = expressionParser(expression);
expect(result).toEqual([
{
value: 'output',
isImage: false,
},
]);
});
it('should parse global variable expression', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'ref',
content: {
source: 'global_variable_test',
path: ['user', 'name'],
blockID: 'global',
name: 'user.name',
},
},
};
const result = expressionParser(expression);
expect(result).toEqual([
{
value: 'user.name',
isImage: false,
},
]);
});
it('should handle invalid expressions', () => {
const expression: ValueExpressionDTO = {
type: 'string',
value: {
type: 'literal',
content: undefined,
},
};
const result = expressionParser(expression);
expect(result).toEqual([]);
});
it('should filter out invalid inputs', () => {
const result = expressionParser(undefined as any);
expect(result).toEqual([]);
});
});

View File

@@ -0,0 +1,134 @@
/*
* 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 } from 'vitest';
import { imageReferenceParser } from '../../../../src/utils/schema-extractor/parsers/image-reference';
import type { ValueExpressionDTO } from '../../../../src/types/dto';
interface ImageReferenceDTO {
url: ValueExpressionDTO;
}
describe('image-reference-parser', () => {
it('应该处理空输入', () => {
const result = imageReferenceParser([]);
expect(result).toEqual([]);
});
it('应该处理非数组输入', () => {
const result = imageReferenceParser(undefined as any);
expect(result).toEqual([]);
const result2 = imageReferenceParser({} as any);
expect(result2).toEqual([]);
const result3 = imageReferenceParser(null as any);
expect(result3).toEqual([]);
});
it('应该正确解析图片引用', () => {
const references: ImageReferenceDTO[] = [
{
url: {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test.png',
},
},
},
];
const result = imageReferenceParser(references);
expect(result).toEqual([
{
name: '-',
value: 'https://example.com/test.png',
isImage: false,
},
]);
});
it('应该正确解析多个图片引用', () => {
const references: ImageReferenceDTO[] = [
{
url: {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test1.png',
},
},
},
{
url: {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test2.png',
},
},
},
];
const result = imageReferenceParser(references);
expect(result).toEqual([
{
name: '-',
value: 'https://example.com/test1.png',
isImage: false,
},
{
name: '-',
value: 'https://example.com/test2.png',
isImage: false,
},
]);
});
it('应该过滤掉无效的图片引用', () => {
const references: ImageReferenceDTO[] = [
{
url: {
type: 'string',
value: {
type: 'literal',
content: undefined,
},
},
},
{
url: {
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test.png',
},
},
},
];
const result = imageReferenceParser(references);
expect(result).toEqual([
{
name: '-',
value: 'https://example.com/test.png',
isImage: false,
},
]);
});
});

View File

@@ -0,0 +1,278 @@
/*
* 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 { outputsParser } from '../../../../src/utils/schema-extractor/parsers/output';
import { AssistTypeDTO, VariableTypeDTO } from '../../../../src/types/dto';
// Mock isWorkflowImageTypeURL
vi.mock('../../../../src/utils/schema-extractor/utils', () => ({
isWorkflowImageTypeURL: (url: string) =>
url.startsWith('https://example.com/'),
}));
describe('output-parser', () => {
it('应该处理空输入', () => {
const result = outputsParser([]);
expect(result).toEqual([]);
});
it('应该处理非数组输入', () => {
const result = outputsParser(undefined as any);
expect(result).toEqual([]);
const result2 = outputsParser({} as any);
expect(result2).toEqual([]);
const result3 = outputsParser(null as any);
expect(result3).toEqual([]);
});
it('应该正确解析基本输出', () => {
const outputs = [
{
name: 'test',
description: 'test description',
type: VariableTypeDTO.string,
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'test',
description: 'test description',
},
]);
});
it('应该正确解析对象类型输出', () => {
const outputs = [
{
name: 'obj',
type: VariableTypeDTO.object,
schema: [
{
name: 'field1',
type: VariableTypeDTO.string,
},
{
name: 'field2',
type: VariableTypeDTO.float,
},
],
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'obj',
children: [
{
name: 'field1',
},
{
name: 'field2',
},
],
},
]);
});
it('应该正确解析列表类型输出', () => {
const outputs = [
{
name: 'list',
type: VariableTypeDTO.list,
schema: {
schema: [
{
name: 'item1',
type: VariableTypeDTO.string,
},
{
name: 'item2',
type: VariableTypeDTO.float,
},
],
},
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'list',
children: [
{
name: 'item1',
},
{
name: 'item2',
},
],
},
]);
});
it('应该正确处理带有默认值的字符串输出', () => {
const outputs = [
{
name: 'text',
type: VariableTypeDTO.string,
defaultValue: 'hello world',
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'text',
value: 'hello world',
isImage: false,
},
]);
});
it('应该正确处理图片 URL 输出', () => {
const outputs = [
{
name: 'image',
type: VariableTypeDTO.string,
defaultValue: 'https://example.com/test.png',
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'image',
value: 'https://example.com/test.png',
images: ['https://example.com/test.png'],
isImage: true,
},
]);
});
it('应该正确处理图片类型输出', () => {
const outputs = [
{
name: 'image',
type: VariableTypeDTO.string,
assistType: AssistTypeDTO.image,
defaultValue: 'https://example.com/test.png',
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'image',
value: 'https://example.com/test.png',
images: ['https://example.com/test.png'],
isImage: true,
},
]);
});
it('应该正确处理图片列表输出', () => {
const outputs = [
{
name: 'images',
type: VariableTypeDTO.list,
schema: {
assistType: AssistTypeDTO.image,
},
defaultValue: JSON.stringify([
'https://example.com/test1.png',
'https://example.com/test2.png',
]),
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'images',
value: JSON.stringify([
'https://example.com/test1.png',
'https://example.com/test2.png',
]),
images: [
'https://example.com/test1.png',
'https://example.com/test2.png',
],
isImage: true,
},
]);
});
it('应该正确处理文件列表中的图片', () => {
const outputs = [
{
name: 'files',
type: VariableTypeDTO.list,
schema: {
assistType: AssistTypeDTO.file,
},
defaultValue: JSON.stringify([
'https://example.com/test.png',
'https://example1.com/document.pdf',
]),
},
];
const result = outputsParser(outputs);
console.log(result);
expect(result).toEqual([
{
name: 'files',
value: JSON.stringify([
'https://example.com/test.png',
'https://example1.com/document.pdf',
]),
images: ['https://example.com/test.png'],
isImage: true,
},
]);
});
it('应该正确处理无效的 JSON 字符串', () => {
const consoleError = vi.spyOn(console, 'error');
const outputs = [
{
name: 'invalid',
type: VariableTypeDTO.list,
schema: {
assistType: AssistTypeDTO.image,
},
defaultValue: 'invalid json',
},
];
const result = outputsParser(outputs);
expect(result).toEqual([
{
name: 'invalid',
value: 'invalid json',
isImage: false,
},
]);
expect(consoleError).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,206 @@
/*
* 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 } from 'vitest';
import { refInputParametersParser } from '../../../../src/utils/schema-extractor/parsers/ref-input-parameters';
// Mock isWorkflowImageTypeURL
vi.mock('../../../../src/utils/schema-extractor/utils', () => ({
isWorkflowImageTypeURL: (url: string) =>
typeof url === 'string' && url.startsWith('https://example.com/'),
}));
describe('ref-input-parameters-parser', () => {
it('应该处理空输入', () => {
const result = refInputParametersParser([]);
expect(result).toEqual([]);
});
it('应该正确解析单个引用参数', () => {
const references = [
{
param1: {
type: 'string',
value: {
content: 'test value',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param1',
value: 'test value',
isImage: false,
},
]);
});
it('应该正确解析多个引用参数', () => {
const references = [
{
param1: {
type: 'string',
value: {
content: 'value1',
},
},
param2: {
type: 'string',
value: {
content: 'value2',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param1',
value: 'value1',
isImage: false,
},
{
name: 'param2',
value: 'value2',
isImage: false,
},
]);
});
it('应该正确解析多个引用对象', () => {
const references = [
{
param1: {
type: 'string',
value: {
content: 'value1',
},
},
},
{
param2: {
type: 'string',
value: {
content: 'value2',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param1',
value: 'value1',
isImage: false,
},
{
name: 'param2',
value: 'value2',
isImage: false,
},
]);
});
it('应该正确识别图片 URL', () => {
const references = [
{
image: {
type: 'string',
value: {
content: 'https://example.com/test.png',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'image',
value: 'https://example.com/test.png',
isImage: true,
},
]);
});
it('应该忽略非字符串类型的参数', () => {
const references = [
{
param1: {
type: 'number',
value: {
content: '123',
},
},
param2: {
type: 'string',
value: {
content: 'valid',
},
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param2',
value: 'valid',
isImage: false,
},
]);
});
it('应该忽略无效的参数结构', () => {
const references = [
{
param1: 'invalid',
param2: {
type: 'string',
value: {
content: 'valid',
},
},
param3: null,
param4: undefined,
param5: {
type: 'string',
value: {}, // 空的 value 对象
},
},
];
const result = refInputParametersParser(references);
expect(result).toEqual([
{
name: 'param2',
value: 'valid',
isImage: false,
},
{
name: 'param5',
value: undefined,
isImage: false,
},
]);
});
});

View File

@@ -0,0 +1,214 @@
/*
* 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 { variableMergeGroupsParser } from '../../../../src/utils/schema-extractor/parsers/variable-merge-groups-parser';
import type { ValueExpressionDTO } from '../../../../src/types/dto';
// Mock expressionParser
vi.mock(
'../../../../src/utils/schema-extractor/parsers/expression-parser',
() => ({
expressionParser: vi.fn(variables => {
if (!Array.isArray(variables)) {
return [];
}
return variables.map(variable => ({
value: variable.value?.content,
isImage:
typeof variable.value?.content === 'string' &&
variable.value?.content.startsWith('https://example.com/'),
}));
}),
}),
);
describe('variable-merge-groups-parser', () => {
it('应该处理空输入', () => {
const result = variableMergeGroupsParser([]);
expect(result).toEqual([]);
});
it('应该正确解析单个变量组', () => {
const mergeGroups = [
{
name: 'group1',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'test value',
},
} as ValueExpressionDTO,
],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'group1',
variables: [
{
value: 'test value',
isImage: false,
},
],
},
]);
});
it('应该正确解析多个变量组', () => {
const mergeGroups = [
{
name: 'group1',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'value1',
},
} as ValueExpressionDTO,
],
},
{
name: 'group2',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'value2',
},
} as ValueExpressionDTO,
],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'group1',
variables: [
{
value: 'value1',
isImage: false,
},
],
},
{
groupName: 'group2',
variables: [
{
value: 'value2',
isImage: false,
},
],
},
]);
});
it('应该正确处理包含图片 URL 的变量组', () => {
const mergeGroups = [
{
name: 'images',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test.png',
},
} as ValueExpressionDTO,
],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'images',
variables: [
{
value: 'https://example.com/test.png',
isImage: true,
},
],
},
]);
});
it('应该正确处理空变量组', () => {
const mergeGroups = [
{
name: 'emptyGroup',
variables: [],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'emptyGroup',
variables: [],
},
]);
});
it('应该正确处理包含多个变量的组', () => {
const mergeGroups = [
{
name: 'mixedGroup',
variables: [
{
type: 'string',
value: {
type: 'literal',
content: 'text value',
},
} as ValueExpressionDTO,
{
type: 'string',
value: {
type: 'literal',
content: 'https://example.com/test.png',
},
} as ValueExpressionDTO,
],
},
];
const result = variableMergeGroupsParser(mergeGroups);
expect(result).toEqual([
{
groupName: 'mixedGroup',
variables: [
{
value: 'text value',
isImage: false,
},
{
value: 'https://example.com/test.png',
isImage: true,
},
],
},
]);
});
});

View File

@@ -0,0 +1,65 @@
/*
* 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 } from 'vitest';
import {
isPresetStartParams,
isUserInputStartParams,
} from '../../src/utils/start-params';
import {
BOT_USER_INPUT,
USER_INPUT,
CONVERSATION_NAME,
} from '../../src/constants';
describe('start-params', () => {
describe('isPresetStartParams', () => {
it('应该正确识别预设的开始节点参数', () => {
expect(isPresetStartParams(BOT_USER_INPUT)).toBe(true);
expect(isPresetStartParams(USER_INPUT)).toBe(true);
expect(isPresetStartParams(CONVERSATION_NAME)).toBe(true);
});
it('应该正确识别非预设的开始节点参数', () => {
expect(isPresetStartParams('other_param')).toBe(false);
expect(isPresetStartParams('custom_input')).toBe(false);
});
it('应该正确处理 undefined 和空字符串', () => {
expect(isPresetStartParams(undefined)).toBe(false);
expect(isPresetStartParams('')).toBe(false);
});
});
describe('isUserInputStartParams', () => {
it('应该正确识别用户输入的开始节点参数', () => {
expect(isUserInputStartParams(BOT_USER_INPUT)).toBe(true);
expect(isUserInputStartParams(USER_INPUT)).toBe(true);
});
it('应该正确识别非用户输入的开始节点参数', () => {
expect(isUserInputStartParams(CONVERSATION_NAME)).toBe(false);
expect(isUserInputStartParams('other_param')).toBe(false);
expect(isUserInputStartParams('custom_input')).toBe(false);
});
it('应该正确处理 undefined 和空字符串', () => {
expect(isUserInputStartParams(undefined)).toBe(false);
expect(isUserInputStartParams('')).toBe(false);
});
});
});