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,35 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
import { NODE_TEST_ID_PREFIX } from '../constants';
export const concatTestId = (...testIds: string[]) =>
testIds.filter(id => !!id).join('.');
/**
* 生成节点的测试id
* @example concatNodeTestId(node, 'right-panel') => playground.node.100001.right-panel
* @param node 节点
* @param testIds 其它id
* @returns
*/
export const concatNodeTestId = (node: FlowNodeEntity, ...testIds: string[]) =>
concatTestId(
node?.id ? concatTestId(NODE_TEST_ID_PREFIX, node.id) : '',
...testIds,
);

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 { cloneDeep } from 'lodash-es';
import { isFormV2 } from '@flowgram-adapter/free-layout-editor';
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
import { type FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
/**
* 找到以 pathEnds 为结尾的 FormItem并获取它的值
* @param node
* @param pathEnds
* @returns
*/
export function getFormValueByPathEnds<T = unknown>(
node: FlowNodeEntity,
pathEnds: string,
): T | undefined {
return isFormV2(node)
? getFormValueByPathEndsV2<T>(node, pathEnds)
: getFormValueByPathEndsV1<T>(node, pathEnds);
}
function getFormValueByPathEndsV1<T = unknown>(
node: FlowNodeEntity,
pathEnds: string,
): T | undefined {
const form = node.getData(FlowNodeFormData).formModel;
const paths: string[] = [...form.formItemPathMap.keys()];
const formPath = paths.find(path => path.endsWith(pathEnds));
if (!formPath) {
return undefined;
}
const formValue = cloneDeep<T>(form.getFormItemValueByPath(formPath));
return formValue;
}
function getFormValueByPathEndsV2<T = unknown>(
node: FlowNodeEntity,
pathEnds: string,
): T | undefined {
const form = node.getData(FlowNodeFormData).formModel;
const data = form.getFormItemValueByPath('/');
if (!data || typeof data !== 'object') {
return undefined;
}
const value = findValueByPathEnds<T>(data, pathEnds);
return cloneDeep(value);
}
const findValueByPathEnds = <T = unknown>(
obj: unknown,
pathEnds: string,
currentPath = '',
): T | undefined => {
if (!obj) {
return undefined;
}
// 检查当前路径是否以 pathEnds 结尾
if (currentPath.endsWith(pathEnds)) {
return obj as T;
}
// 处理对象
if (typeof obj === 'object' && !Array.isArray(obj)) {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const newPath = currentPath ? `${currentPath}/${key}` : `/${key}`;
if (newPath.endsWith(pathEnds)) {
return obj[key] as T;
}
// 递归查找子对象
const result = findValueByPathEnds(obj[key], pathEnds, newPath);
if (result !== undefined) {
return result as T;
}
}
}
}
// 处理数组
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
const newPath = `${currentPath}/${i}`;
if (newPath.endsWith(pathEnds)) {
return obj[i] as T;
}
// 递归查找数组元素
const result = findValueByPathEnds(obj[i], pathEnds, newPath);
if (result !== undefined) {
return result as T;
}
}
}
return undefined;
};

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 { ViewVariableType } from '../types/view-variable-type';
export const ACCEPT_MAP = {
[ViewVariableType.Image]: ['image/*'],
[ViewVariableType.Doc]: ['.docx', '.doc', '.pdf'],
[ViewVariableType.Audio]: [
'.mp3',
'.wav',
'.aac',
'.flac',
'.ogg',
'.wma',
'.alac',
'.mid',
'.midi',
'.ac3',
'.dsd',
],
[ViewVariableType.Excel]: ['.xls', '.xlsx', '.csv'],
[ViewVariableType.Video]: ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv'],
[ViewVariableType.Zip]: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
[ViewVariableType.Code]: ['.py', '.java', '.c', '.cpp', '.js', '.css'],
[ViewVariableType.Txt]: ['.txt'],
[ViewVariableType.Ppt]: ['.ppt', '.pptx'],
[ViewVariableType.Svg]: ['.svg'],
};
export const getFileAccept = (
inputType: ViewVariableType,
availableFileTypes?: ViewVariableType[],
) => {
let accept: string;
const itemType = ViewVariableType.isArrayType(inputType)
? ViewVariableType.getArraySubType(inputType)
: inputType;
if (itemType === ViewVariableType.File) {
if (availableFileTypes?.length) {
accept = availableFileTypes
.map(type => ACCEPT_MAP[type]?.join(','))
.join(',');
} else {
accept = Object.values(ACCEPT_MAP)
.map(items => items.join(','))
.join(',');
}
} else {
accept = (ACCEPT_MAP[itemType] || []).join(',');
}
return accept;
};

View File

@@ -0,0 +1,50 @@
/*
* 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 {
SchemaExtractorParserName,
SchemaExtractor,
type SchemaExtractorConfig,
type SchemaExtracted,
type SchemaExtractorNodeConfig,
type ParsedVariableMergeGroups,
} from './schema-extractor';
export { concatTestId, concatNodeTestId } from './concat-test-id';
export {
type NodeResultExtracted,
type CaseResultData,
NodeResultExtractor,
} from './node-result-extractor';
export { parseImagesFromOutputData } from './output-image-parser';
export { reporter, captureException } from './slardar-reporter';
export { getFormValueByPathEnds } from './form-helpers';
export { isGeneralWorkflow } from './is-general-workflow';
export { isPresetStartParams, isUserInputStartParams } from './start-params';
export {
type TraverseValue,
type TraverseNode,
type TraverseContext,
type TraverseHandler,
traverse,
} from './traverse';
export { getFileAccept } from './get-file-accept';

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
/**
*
* @param flowMode 是否广义上的 workflow包含原来的 Workflow 和 Coze 2.0 新增的 Chatflow
* @returns
*/
export const isGeneralWorkflow = (flowMode: WorkflowMode) =>
flowMode === WorkflowMode.Workflow || flowMode === WorkflowMode.ChatFlow;

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type WorkflowJSON } from '../../types';
import { type NodeResult } from '../../api';
import {
type NodeResultExtracted,
type NodeResultExtractorParser,
} from './type';
import { defaultParser } from './parsers';
export { type NodeResultExtracted, type CaseResultData } from './type';
export class NodeResultExtractor {
private readonly parser: NodeResultExtractorParser;
public constructor(
private readonly nodeResults: NodeResult[],
private readonly workflowSchema: WorkflowJSON,
) {
this.parser = defaultParser;
}
public extract(): NodeResultExtracted[] {
return (
this.nodeResults
?.filter(Boolean)
?.map(item => this.parser(item, this.workflowSchema)) || []
);
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable complexity */
import { isString } from 'lodash-es';
import { typeSafeJSONParse } from '@coze-arch/bot-utils';
import {
type NodeResultExtractorParser,
type NodeResultExtracted,
type CaseResultData,
} from '../type';
import { parseImagesFromOutputData } from '../../output-image-parser';
import { StandardNodeType, type WorkflowNodeJSON } from '../../../types';
import { type NodeResult, TerminatePlanType } from '../../../api';
export const defaultParser: NodeResultExtractorParser = (
nodeResult,
workflowSchema,
): NodeResultExtracted => {
const { nodeId, isBatch, batch } = nodeResult;
const node = workflowSchema.nodes.find(it => it.id === nodeId);
const nodeType = node?.type as StandardNodeType;
if (!isBatch) {
return {
nodeId,
nodeType,
isBatch,
caseResult: [parseData(nodeResult, node)],
};
}
const batchNodeResult = typeSafeJSONParse(batch) as NodeResult[];
const caseResult = (batchNodeResult
?.filter(Boolean)
?.map(item => parseData(item, node, isBatch))
?.filter(Boolean) || []) as CaseResultData[];
return {
nodeId,
nodeType,
isBatch,
caseResult,
};
};
const parseData = (
nodeResult: NodeResult,
nodeSchema?: WorkflowNodeJSON,
isBatch = false,
): CaseResultData => {
const { input, output, raw_output: rawOutput, extra, items } = nodeResult;
const dataList: CaseResultData['dataList'] = [];
const inputAsOutput = [
StandardNodeType.End,
StandardNodeType.Output,
].includes(nodeSchema?.type as StandardNodeType);
if (isBatch) {
dataList.push({
title: '本次批处理变量',
data: typeSafeJSONParse(items),
});
}
if (!inputAsOutput) {
dataList.push({
title: '输入',
data: typeSafeJSONParse(input) || input?.toString?.(),
});
}
const finalOutput = inputAsOutput ? input : output;
const outputData =
typeSafeJSONParse(finalOutput) || finalOutput?.toString?.();
const textHasRawout =
nodeSchema?.type === StandardNodeType.Text &&
isString(rawOutput) &&
rawOutput?.length > 0;
// 文本节点的 raw_out 不需要反序列化,一定是 stringbadcase用户拼接的 json 字符串如 '{}'、'123',反序列化后会变成object 和 number
const rawOutputData = textHasRawout
? rawOutput?.toString?.()
: rawOutput
? typeSafeJSONParse(rawOutput) || rawOutput?.toString?.()
: undefined;
/** Code、Llm 节点需要展示 raw */
const hasRawOutput =
(Boolean(nodeSchema?.type) &&
[
StandardNodeType.Code,
StandardNodeType.LLM,
StandardNodeType.Question,
].includes(nodeSchema?.type as StandardNodeType)) ||
textHasRawout;
// Start、Input 节点只展示输入
const hasOutput =
nodeSchema?.type !== StandardNodeType.Start &&
nodeSchema?.type !== StandardNodeType.Input;
if (hasOutput) {
hasRawOutput &&
rawOutputData &&
dataList.push({
title: '原始输出',
data: rawOutputData,
});
const outputTitle = inputAsOutput
? '输出变量'
: rawOutputData
? '最终输出'
: '输出';
outputData &&
dataList.push({
title: outputTitle,
data: outputData,
});
}
if (nodeSchema?.type === StandardNodeType.End) {
const isReturnText =
(typeSafeJSONParse(extra) as any)?.response_extra?.terminal_plan ===
TerminatePlanType.USESETTING;
if (isReturnText) {
dataList.push({
title: '回答内容',
data: typeSafeJSONParse(output) || output?.toString?.(),
});
}
} else if (nodeSchema?.type === StandardNodeType.Output) {
dataList.push({
title: '回答内容',
data: typeSafeJSONParse(output) || output?.toString?.(),
});
}
const outputJsonString = finalOutput ?? '';
return {
dataList,
imgList: parseImagesFromOutputData({
// batch data 的 output 下钻了一层,需要再包一层和 output 的 schema 保持一致
outputData: isBatch
? {
outputList: [typeSafeJSONParse(outputJsonString)].filter(Boolean),
}
: typeSafeJSONParse(outputJsonString),
nodeSchema,
}),
};
};

View File

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

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.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type StandardNodeType, type WorkflowJSON } from '../../types';
import { type NodeResult } from '../../api';
export interface CaseResultData {
dataList?: Array<{ title: string; data: any }>;
imgList?: string[];
}
export interface NodeResultExtracted {
nodeId?: string;
nodeType?: StandardNodeType;
isBatch?: boolean;
caseResult?: CaseResultData[];
}
export type NodeResultExtractorParser = (
nodeResult: NodeResult,
workflowSchema: WorkflowJSON,
) => NodeResultExtracted;

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { flatten, get } from 'lodash-es';
import { type WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
import { StandardNodeType, VariableTypeDTO, AssistTypeDTO } from '../types';
interface Schema {
type: VariableTypeDTO;
name: string;
schema: Schema | Schema[];
assistType: AssistTypeDTO;
}
const isImageType = (schema: Schema) => {
if (
schema?.type === VariableTypeDTO.image ||
(schema?.type === VariableTypeDTO.string &&
[AssistTypeDTO.image, AssistTypeDTO.svg].includes(schema?.assistType))
) {
return true;
}
};
function getImgList(data: unknown, schema: Schema): string[] {
let imgList: string[] = [];
if (isImageType(schema) && typeof data === 'string') {
imgList.push(data);
}
if (schema?.type === VariableTypeDTO.list && Array.isArray(data)) {
const imgListInItems = (data as unknown[]).map(item =>
getImgList(item, schema.schema as Schema),
);
imgList = imgList.concat(...imgListInItems);
}
if (schema?.type === VariableTypeDTO.object) {
const imgListInObject = Object.entries(
(data as Record<string, unknown>) || {},
).map(([key, value]) =>
getImgList(
value,
((schema.schema as Schema[]) || []).find(
item => item.name === key,
) as Schema,
),
);
imgList = imgList.concat(...imgListInObject);
}
return imgList;
}
/**
* 从节点 output data 中解析图片链接
* @param outputData 节点输出数据 JSON 序列化后的字符串
* @param nodeSchema 节点 schema
* @param excludeNodeTypes 不解析该类型节点的图片链接
*/
export function parseImagesFromOutputData({
outputData,
nodeSchema,
excludeNodeTypes = [],
}: {
outputData?: any;
nodeSchema?: WorkflowNodeJSON;
excludeNodeTypes?: StandardNodeType[];
}): string[] {
if (!nodeSchema || !outputData) {
return [];
}
if (excludeNodeTypes.includes(nodeSchema?.type as StandardNodeType)) {
return [];
}
let outputParameters: any[] = [];
if (
nodeSchema?.type === StandardNodeType.End ||
nodeSchema?.type === StandardNodeType.Output
) {
outputParameters = (nodeSchema?.data?.inputs as any)?.inputParameters;
} else {
outputParameters = nodeSchema?.data?.outputs as any[];
}
if (!outputParameters) {
return [];
}
const imgListInOutput = flatten(
outputParameters.map(p =>
getImgList(
get(outputData, p?.name),
p?.input
? {
...p.input,
name: p.name,
}
: p,
),
),
).filter(url => !!url);
return imgListInOutput;
}

View File

@@ -0,0 +1,309 @@
/*
* 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, expect, it } from 'vitest';
import { StandardNodeType, type WorkflowJSON } from '../../../types';
import {
type SchemaExtracted,
SchemaExtractor,
type SchemaExtractorConfig,
SchemaExtractorParserName,
} from '..';
describe('SchemaExtractor', () => {
it('should create instance', () => {
const schema: WorkflowJSON = {
nodes: [],
edges: [],
};
const schemaExtractor = new SchemaExtractor(schema);
expect(schemaExtractor).toBeInstanceOf(SchemaExtractor);
});
it('should create instance with empty or undefined schema', () => {
const schemaExtractorEmpty = new SchemaExtractor(
{} as unknown as WorkflowJSON,
);
expect(schemaExtractorEmpty).toBeInstanceOf(SchemaExtractor);
const schemaExtractorUndefined = new SchemaExtractor(
undefined as unknown as WorkflowJSON,
);
expect(schemaExtractorUndefined).toBeInstanceOf(SchemaExtractor);
});
it('should extract schema with config', () => {
const schema: WorkflowJSON = {
nodes: [
{
id: '1',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName1',
},
},
},
{
id: '2',
type: StandardNodeType.LLM,
data: {
nodeMeta: {
title: 'nodeName2',
},
inputs: {
llmParam: [
{
name: 'prompt',
input: {
type: 'string',
value: {
type: 'literal',
content: 'you should test {{here}}',
},
},
},
{
name: 'systemPrompt',
input: {
type: 'string',
value: {
type: 'literal',
content: 'this is systemPrompt',
},
},
},
],
},
prompt: 'you should test here',
},
},
],
edges: [],
};
const config: SchemaExtractorConfig = {
[StandardNodeType.Api]: [
{
name: 'title',
path: 'nodeMeta.title',
},
],
[StandardNodeType.LLM]: [
{
name: 'title',
path: 'nodeMeta.title',
},
{
name: 'llmParam',
path: 'inputs.llmParam',
parser: SchemaExtractorParserName.LLM_PARAM,
},
],
};
const schemaExtractor = new SchemaExtractor(schema);
const extractedSchema: SchemaExtracted[] = schemaExtractor.extract(config);
expect(extractedSchema).toStrictEqual([
{
nodeId: '1',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName1',
},
},
{
nodeId: '2',
nodeType: StandardNodeType.LLM,
properties: {
title: 'nodeName2',
llmParam: {
prompt: 'you should test {{here}}',
systemPrompt: 'this is systemPrompt',
},
},
},
]);
});
it('should use parser in the config', () => {
const schema: WorkflowJSON = {
nodes: [
{
id: '1',
type: StandardNodeType.Api,
data: {
content: {
prefix: 'hello',
suffix: 'world',
},
},
},
],
edges: [],
};
const config: SchemaExtractorConfig = {
[StandardNodeType.Api]: [
{
name: 'content',
path: 'content',
parser: (content: { prefix: string; suffix: string }) =>
`${content.prefix},${content.suffix}!`,
},
],
};
const schemaExtractor = new SchemaExtractor(schema);
const extractedSchema: SchemaExtracted[] = schemaExtractor.extract(config);
expect(extractedSchema).toStrictEqual([
{
nodeId: '1',
nodeType: StandardNodeType.Api,
properties: {
content: 'hello,world!',
},
},
]);
});
it('should flat multi-layer schema', () => {
const schema: WorkflowJSON = {
nodes: [
{
id: '1',
type: StandardNodeType.Loop,
data: {
nodeMeta: {
title: 'nodeName1',
},
},
blocks: [
{
id: '1.1',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName1.1',
},
},
},
{
id: '1.2',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName1.2',
},
},
},
{
id: '1.3',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName1.3',
},
},
},
],
edges: [
{
sourceNodeID: '1.1',
sourcePortID: 'output',
targetNodeID: '1.2',
targetPortID: 'input',
},
{
sourceNodeID: '1.2',
sourcePortID: 'output',
targetNodeID: '1.3',
targetPortID: 'input',
},
{
sourceNodeID: '1.1',
sourcePortID: 'output',
targetNodeID: '1.3',
targetPortID: 'input',
},
],
},
{
id: '2',
type: StandardNodeType.Api,
data: {
nodeMeta: {
title: 'nodeName2',
},
},
},
],
edges: [
{
sourceNodeID: '1',
sourcePortID: 'output',
targetNodeID: '2',
targetPortID: 'input',
},
],
};
const config: SchemaExtractorConfig = {
[StandardNodeType.Loop]: [
{
name: 'title',
path: 'nodeMeta.title',
},
],
[StandardNodeType.Api]: [
{
name: 'title',
path: 'nodeMeta.title',
},
],
};
const schemaExtractor = new SchemaExtractor(schema);
const extractedSchema: SchemaExtracted[] = schemaExtractor.extract(config);
expect(extractedSchema).toStrictEqual([
{
nodeId: '1',
nodeType: StandardNodeType.Loop,
properties: {
title: 'nodeName1',
},
},
{
nodeId: '2',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName2',
},
},
{
nodeId: '1.1',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName1.1',
},
},
{
nodeId: '1.2',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName1.2',
},
},
{
nodeId: '1.3',
nodeType: StandardNodeType.Api,
properties: {
title: 'nodeName1.3',
},
},
]);
});
});

View File

@@ -0,0 +1,93 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with dataset param parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '111943',
type: '6',
data: {
inputs: {
datasetParam: [
{
name: 'datasetList',
input: {
type: 'list',
schema: { type: 'string' },
value: {
type: 'literal',
content: ['7330215302133268524', '7330215302133268524'],
},
},
},
{
name: 'topK',
input: {
type: 'integer',
value: { type: 'literal', content: 6 },
},
},
{
name: 'minScore',
input: {
type: 'number',
value: { type: 'literal', content: 0.5 },
},
},
{
name: 'strategy',
input: {
type: 'integer',
value: { type: 'literal', content: 1 },
},
},
],
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// knowledge 知识库节点 6
[StandardNodeType.Dataset]: [
{
// 对应知识库名称
name: 'datasetParam',
path: 'inputs.datasetParam',
parser: SchemaExtractorParserName.DATASET_PARAM,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '111943',
nodeType: '6',
properties: {
datasetParam: {
datasetList: ['7330215302133268524', '7330215302133268524'],
},
},
},
]);
});

View File

@@ -0,0 +1,60 @@
/*
* 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 { expect, it } from 'vitest';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with default parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '900001',
type: '2',
data: {
inputs: {
content: {
type: 'string',
value: {
type: 'literal',
content: '{{output_a}} and {{output_b}}',
},
},
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// end 结束节点 2
[StandardNodeType.End]: [
{
// 对应输出指定内容
name: 'content',
path: 'inputs.content.value.content',
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '900001',
nodeType: '2',
properties: { content: '{{output_a}} and {{output_b}}' },
},
]);
});

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with inputParameters parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '154650',
type: '3',
data: {
inputs: {
inputParameters: [
{
name: 'input_a',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '190950',
name: 'key0',
},
},
},
},
{
name: 'input_b',
input: {
type: 'list',
schema: { type: 'string' },
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '154650',
name: 'batch_a',
},
},
},
},
{
name: 'const_c',
input: {
type: 'string',
value: { type: 'literal', content: '1234' },
},
},
],
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// llm 大模型节点 3
[StandardNodeType.LLM]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '154650',
nodeType: '3',
properties: {
inputs: [
{ name: 'input_a', value: 'key0', isImage: false },
{ name: 'input_b', value: 'batch_a', isImage: false },
{ name: 'const_c', value: '1234', isImage: false },
],
},
},
]);
});

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 { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with intents param parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '159306',
type: '22',
data: {
inputs: {
inputParameters: [
{
name: 'query',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'BOT_USER_INPUT',
},
},
},
},
],
llmParam: {
modelType: 113,
generationDiversity: 'balance',
temperature: 0.5,
topP: 1,
frequencyPenalty: 0,
presencePenalty: 0,
maxTokens: 2048,
responseFormat: 2,
modelName: 'GPT-3.5',
prompt: {
type: 'string',
value: {
type: 'literal',
content: '{{query}}',
},
},
systemPrompt: {
type: 'string',
value: {
type: 'literal',
content: '你好, {{query}}',
},
},
enableChatHistory: false,
},
intents: [
{
name: '北京',
},
{
name: '上海',
},
{
name: '武汉',
},
{
name: '深圳',
},
{
name: '长沙2',
},
],
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// end 结束节点 2
[StandardNodeType.Intent]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// intents
name: 'intents',
path: 'inputs.intents',
parser: SchemaExtractorParserName.INTENTS,
},
{
// system prompt
name: 'systemPrompt',
path: 'inputs.llmParam.systemPrompt.value.content',
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '159306',
nodeType: '22',
properties: {
inputs: [{ isImage: false, name: 'query', value: 'BOT_USER_INPUT' }],
intents: { intent: '1. 北京 2. 上海 3. 武汉 4. 深圳 5. 长沙2' },
systemPrompt: '你好, {{query}}',
},
},
]);
});

View File

@@ -0,0 +1,107 @@
/*
* 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 { expect, it } from 'vitest';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor, SchemaExtractorParserName } from '..';
it('extract schema with json string parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '176450',
type: '25',
data: {
inputs: {
Messages:
'{"visibility":{"visibility":"3","user_settings":[{"biz_role_id":"7398508237160677420","role":"host","nickname":"host","role_type":1,"description":""},{"biz_role_id":"7402058670241185836","role":"juese2","nickname":"","role_type":3,"description":""},{"biz_role_id":"7405794345170763820","role":"bot1","nickname":"majiang","role_type":2,"description":"麻将高手"}]},"order":"1","contentMode":"1","messages":[{"biz_role_id":"7398508237160677420","role":"host","nickname":"host","role_type":1,"generate_mode":0,"content":"这是一条示例消息,点击可修改"},{"biz_role_id":"7402058670241185836","role":"juese2","nickname":"","role_type":3,"content":"","generate_mode":1}]}',
Roles:
'[{"biz_role_id":"7398508237160677420","role":"host","nickname":"host","role_type":1,"generate_mode":0},{"biz_role_id":"7402058670241185836","role":"juese2","nickname":"","role_type":3,"generate_mode":1}]',
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// end 结束节点 2
[StandardNodeType.SceneChat]: [
{
// 对应输出指定内容
name: 'messages',
path: 'inputs.Messages',
parser: SchemaExtractorParserName.JSON_STRING_PARSER,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '176450',
nodeType: '25',
properties: {
messages: {
visibility: {
visibility: '3',
user_settings: [
{
biz_role_id: '7398508237160677420',
role: 'host',
nickname: 'host',
role_type: 1,
description: '',
},
{
biz_role_id: '7402058670241185836',
role: 'juese2',
nickname: '',
role_type: 3,
description: '',
},
{
biz_role_id: '7405794345170763820',
role: 'bot1',
nickname: 'majiang',
role_type: 2,
description: '麻将高手',
},
],
},
order: '1',
contentMode: '1',
messages: [
{
biz_role_id: '7398508237160677420',
role: 'host',
nickname: 'host',
role_type: 1,
generate_mode: 0,
content: '这是一条示例消息,点击可修改',
},
{
biz_role_id: '7402058670241185836',
role: 'juese2',
nickname: '',
role_type: 3,
content: '',
generate_mode: 1,
},
],
},
},
},
]);
});

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with outputs parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '190950',
type: '5',
data: {
outputs: [
{ type: 'string', name: 'key0', description: 'test desc' },
{ type: 'string', name: 'key1' },
{ type: 'list', name: 'key2', schema: { type: 'string' } },
{
type: 'object',
name: 'key3',
schema: [{ type: 'string', name: 'key31' }],
},
{
type: 'list',
name: 'key4',
schema: {
type: 'object',
schema: [
{ type: 'boolean', name: 'key41' },
{ type: 'integer', name: 'key42' },
{ type: 'float', name: 'key43' },
{ type: 'list', name: 'key44', schema: { type: 'string' } },
{
type: 'object',
name: 'key45',
schema: [{ type: 'string', name: 'key451' }],
},
],
},
},
],
},
},
],
});
const extractedSchema = schemaExtractor.extract({
// code 代码节点 5
[StandardNodeType.Code]: [
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '190950',
nodeType: '5',
properties: {
outputs: [
{ name: 'key0', description: 'test desc' },
{ name: 'key1' },
{ name: 'key2' },
{ name: 'key3', children: [{ name: 'key31' }] },
{
name: 'key4',
children: [
{ name: 'key41' },
{ name: 'key42' },
{ name: 'key43' },
{ name: 'key44' },
{ name: 'key45', children: [{ name: 'key451' }] },
],
},
],
},
},
]);
});

View File

@@ -0,0 +1,289 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractorParserName } from '../constant';
import { StandardNodeType } from '../../../types';
import { SchemaExtractor } from '..';
it('extract schema with variableAssign parser', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '184010',
type: '21',
data: {
inputs: {
inputParameters: [],
variableParameters: [],
},
outputs: [],
},
blocks: [
{
id: '149710',
type: '20',
data: {
inputs: {
inputParameters: [
{
left: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_str',
},
},
},
right: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '146923',
name: 'new_str',
},
},
},
},
{
left: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_num',
},
},
},
right: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '146923',
name: 'new_num',
},
},
},
},
{
left: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_bool',
},
},
},
right: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '146923',
name: 'new_bool',
},
},
},
},
],
},
},
},
],
edges: [],
},
],
});
const extractedSchema = schemaExtractor.extract({
[StandardNodeType.SetVariable]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.VARIABLE_ASSIGN,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '149710',
nodeType: '20',
properties: {
inputs: [
{ name: 'var_str', value: 'new_str' },
{ name: 'var_num', value: 'new_num' },
{ name: 'var_bool', value: 'new_bool' },
],
},
},
]);
});
it('variableAssign parser with empty inputParameters', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '149710',
type: '20',
data: {
inputs: {
inputParameters: undefined,
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
[StandardNodeType.SetVariable]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.VARIABLE_ASSIGN,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '149710',
nodeType: '20',
properties: {
inputs: [],
},
},
]);
});
it('variableAssign parser with invalid schema', () => {
const schemaExtractor = new SchemaExtractor({
edges: [],
nodes: [
{
id: '149710',
type: '20',
data: {
inputs: {
inputParameters: [
{}, // INVALID
{
left: {
type: 'string',
value: {
type: 'ref',
content: undefined, // INVALID
},
},
right: {
type: 'string',
value: {
type: 'ref',
content: 'new_str', // INVALID
},
},
},
{
left: {
type: 'boolean',
value: {}, // INVALID
},
right: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '146923',
name: 'new_bool',
},
},
},
},
{
left: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_bool',
},
},
},
right: {
type: 'boolean',
value: {}, // INVALID
},
},
],
},
},
},
],
});
const extractedSchema = schemaExtractor.extract({
[StandardNodeType.SetVariable]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.VARIABLE_ASSIGN,
},
],
});
expect(extractedSchema).toStrictEqual([
{
nodeId: '149710',
nodeType: '20',
properties: {
inputs: [
{
name: '',
value: '',
},
{
name: '',
value: 'new_str',
},
{
name: '',
value: 'new_bool',
},
{
name: 'var_bool',
value: '',
},
],
},
},
]);
});

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @coze-arch/no-deep-relative-import */
import { type SchemaExtractorConfig } from '../../type';
import { SchemaExtractorParserName } from '../../constant';
import { StandardNodeType } from '../../../../types';
export const imageflowExtractorConfig: SchemaExtractorConfig = {
// api 节点 4
[StandardNodeType.Api]: [
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
],
};

View File

@@ -0,0 +1,503 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const imageflowSchemaJSON = {
nodes: [
{
id: '100001',
type: '1',
meta: { position: { x: 0, y: 0 } },
data: {
outputs: [
{ type: 'string', name: 'ss', required: true },
{ type: 'float', name: 'sss', required: true },
],
nodeMeta: {
title: '开始',
icon: 'icon-Start.png',
description: '工作流的起始节点,用于设定启动工作流需要的信息',
subTitle: '',
},
},
},
{
id: '900001',
type: '2',
meta: { position: { x: 305.67163461538473, y: 600.1374999999998 } },
data: {
nodeMeta: {
title: '结束',
icon: 'icon-End.png',
description: '工作流的最终节点,用于返回工作流运行后的结果信息',
subTitle: '',
},
inputs: {
terminatePlan: 'returnVariables',
inputParameters: [
{
name: 'output',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'ss',
},
},
},
},
{
name: 'sss',
input: {
type: 'string',
value: { type: 'literal', content: 'ss' },
},
},
],
streamingOutput: false,
},
},
},
{
id: '164069',
type: '4',
meta: { position: { x: -455, y: 338.06874999999997 } },
data: {
nodeMeta: {
title: '文生图',
icon: 'icon_Text-to-Image-CN.png',
isFromImageflow: true,
description: '通过文字描述生成图片',
},
inputs: {
apiParam: [
{
name: 'apiID',
input: {
type: 'string',
value: { type: 'literal', content: '7352834806217981963' },
},
},
{
name: 'apiName',
input: {
type: 'string',
value: { type: 'literal', content: 'text2image' },
},
},
{
name: 'pluginID',
input: {
type: 'string',
value: { type: 'literal', content: '7352834694330794023' },
},
},
{
name: 'pluginName',
input: {
type: 'string',
value: { type: 'literal', content: '文生图' },
},
},
{
name: 'pluginVersion',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'tips',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'outDocLink',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
],
inputParameters: [
{
name: 'prompt',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'ss',
},
},
},
},
{
name: 'ratio',
input: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'sss',
},
},
},
},
{
name: 'width',
input: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'sss',
},
},
},
},
{
name: 'height',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'ss',
},
},
},
},
],
},
outputs: [
{ type: 'image', name: 'data', required: false },
{ type: 'string', name: 'msg', required: false },
],
},
},
{
id: '164578',
type: '4',
meta: { position: { x: -543, y: 937.0687499999999 } },
data: {
nodeMeta: {
title: '智能换脸',
icon: 'icon_AI-FaceSwap.png',
isFromImageflow: true,
description: '为图片替换参考图的人脸',
},
inputs: {
apiParam: [
{
name: 'apiID',
input: {
type: 'string',
value: { type: 'literal', content: '7352888732107915305' },
},
},
{
name: 'apiName',
input: {
type: 'string',
value: { type: 'literal', content: 'smartFaceChanging' },
},
},
{
name: 'pluginID',
input: {
type: 'string',
value: { type: 'literal', content: '7352887570142969875' },
},
},
{
name: 'pluginName',
input: {
type: 'string',
value: { type: 'literal', content: '智能换脸' },
},
},
{
name: 'pluginVersion',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'tips',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'outDocLink',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
],
inputParameters: [
{
name: 'reference_picture_url',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'ss',
},
},
},
},
{
name: 'skin',
input: {
type: 'image',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '164069',
name: 'data',
},
},
},
},
{
name: 'template_picture_url',
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '164069',
name: 'msg',
},
},
},
},
],
},
outputs: [{ type: 'image', name: 'data', required: false }],
},
},
{
id: '146804',
type: '4',
meta: { position: { x: -17, y: 1182.06875 } },
data: {
nodeMeta: {
title: '提示词优化',
icon: 'icon_PromptOptimization.png',
isFromImageflow: true,
description: '智能优化图像提示词',
},
inputs: {
apiParam: [
{
name: 'apiID',
input: {
type: 'string',
value: { type: 'literal', content: '7360989981134864399' },
},
},
{
name: 'apiName',
input: {
type: 'string',
value: { type: 'literal', content: 'sd_better_prompt' },
},
},
{
name: 'pluginID',
input: {
type: 'string',
value: { type: 'literal', content: '7360989829062230050' },
},
},
{
name: 'pluginName',
input: {
type: 'string',
value: { type: 'literal', content: '提示词优化' },
},
},
{
name: 'pluginVersion',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'tips',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'outDocLink',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
],
inputParameters: [
{
name: 'prompt',
input: {
type: 'image',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '140741',
name: 'data',
},
},
},
},
],
},
outputs: [{ type: 'string', name: 'data', required: false }],
},
},
{
id: '140741',
type: '4',
meta: { position: { x: -379.44467504743835, y: 1532.3798149905122 } },
data: {
nodeMeta: {
title: '亮度',
icon: 'icon_Brightness.png',
isFromImageflow: true,
description: '改变图片亮度',
},
inputs: {
apiParam: [
{
name: 'apiID',
input: {
type: 'string',
value: { type: 'literal', content: '7355822909170073600' },
},
},
{
name: 'apiName',
input: {
type: 'string',
value: { type: 'literal', content: 'image_light' },
},
},
{
name: 'pluginID',
input: {
type: 'string',
value: { type: 'literal', content: '7355822909170057216' },
},
},
{
name: 'pluginName',
input: {
type: 'string',
value: { type: 'literal', content: '亮度' },
},
},
{
name: 'pluginVersion',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'tips',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
{
name: 'outDocLink',
input: {
type: 'string',
value: { type: 'literal', content: '' },
},
},
],
inputParameters: [
{
name: 'bright',
input: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'sss',
},
},
},
},
{
name: 'origin_url',
input: {
type: 'image',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '164069',
name: 'data',
},
},
},
},
],
},
outputs: [{ type: 'image', name: 'data', required: false }],
},
},
],
edges: [
{ sourceNodeID: '100001', targetNodeID: '164069' },
{ sourceNodeID: '164069', targetNodeID: '164578' },
{ sourceNodeID: '164578', targetNodeID: '140741' },
{ sourceNodeID: '140741', targetNodeID: '146804' },
{ sourceNodeID: '146804', targetNodeID: '900001' },
],
};

View File

@@ -0,0 +1,442 @@
/*
* 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 SchemaExtractorConfig } from '../../type';
import { SchemaExtractorParserName } from '../../constant';
import { StandardNodeType } from '../../../../types';
export const workflowExtractorConfig: SchemaExtractorConfig = {
// Start 开始节点 1
[StandardNodeType.Start]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应 input name / description
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// End 结束节点 2
[StandardNodeType.End]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应输出指定内容
name: 'content',
path: 'inputs.content.value.content',
},
],
// LLM 大模型节点 3
[StandardNodeType.LLM]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应batch value / batch description
name: 'batch',
path: 'inputs.batch.inputLists',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应提示词
name: 'llmParam',
path: 'inputs.llmParam',
parser: SchemaExtractorParserName.LLM_PARAM,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Plugin 节点 4
[StandardNodeType.Api]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应batch value / batch description
name: 'batch',
path: 'inputs.batch.inputLists',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应input value / input description
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Code 代码节点 5
[StandardNodeType.Code]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input value / input description
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应code内容
name: 'code',
path: 'inputs.code',
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Knowledge 知识库节点 6
[StandardNodeType.Dataset]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应知识库名称
name: 'datasetParam',
path: 'inputs.datasetParam',
parser: SchemaExtractorParserName.DATASET_PARAM,
},
],
// If 判断节点 8
[StandardNodeType.If]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'branches',
path: 'inputs.branches',
parser: SchemaExtractorParserName.DEFAULT,
},
],
// Sub Workflow 工作流节点 9
[StandardNodeType.SubWorkflow]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应batch value / batch description
name: 'batch',
path: 'inputs.batch.inputLists',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应input value / input description
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Variable 变量节点 11
[StandardNodeType.Variable]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Database 数据库节点 12
[StandardNodeType.Database]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// sql
name: 'sql',
path: 'inputs.sql',
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Message 消息节点 13
[StandardNodeType.Output]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// content name
name: 'content',
path: 'inputs.content.value.content',
},
],
// Sub Imageflow 图像流节点 14
[StandardNodeType.Imageflow]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应batch value / batch description
name: 'batch',
path: 'inputs.batch.inputLists',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应input value / input description
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Text 文本处理节点 15
[StandardNodeType.Text]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 拼接结果,以及拼接字符串
name: 'concatResult',
path: 'inputs.concatParams',
parser: SchemaExtractorParserName.CONCAT_RESULT,
},
{
// 自定义数组拼接符号
name: 'arrayConcatChar',
path: 'inputs.concatParams',
parser: SchemaExtractorParserName.CUSTOM_ARRAY_CONCAT_CHAR,
},
{
// 自定义分隔符
name: 'splitChar',
path: 'inputs.splitParams',
parser: SchemaExtractorParserName.CUSTOM_SPLIT_CHAR,
},
],
// Question 问题节点 18
[StandardNodeType.Question]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// question 问题
name: 'question',
path: 'inputs.question',
},
{
// answer_type 回答类型 option|text
name: 'answerType',
path: 'inputs.answer_type',
},
{
// options
name: 'options',
path: 'inputs.options',
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Break 终止循环节点 19
[StandardNodeType.Break]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
],
// Set Variable 设置变量节点 20
[StandardNodeType.SetVariable]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.VARIABLE_ASSIGN,
},
],
// Loop 循环节点 21
[StandardNodeType.Loop]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应variable name
name: 'variables',
path: 'inputs.variableParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应output name
name: 'outputs',
path: 'outputs',
parser: SchemaExtractorParserName.OUTPUTS,
},
],
// Intent 意图识别节点 22
[StandardNodeType.Intent]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// intents
name: 'intents',
path: 'inputs.intents',
parser: SchemaExtractorParserName.INTENTS,
},
{
// system prompt
name: 'systemPrompt',
path: 'inputs.llmParam.systemPrompt.value.content',
},
],
// Knowledge Write 知识库写入节点 27
[StandardNodeType.DatasetWrite]: [
{
// 节点自定义名称
name: 'title',
path: 'nodeMeta.title',
},
{
// 对应input name
name: 'inputs',
path: 'inputs.inputParameters',
parser: SchemaExtractorParserName.INPUT_PARAMETERS,
},
{
// 对应知识库名称
name: 'datasetParam',
path: 'inputs.datasetParam',
parser: SchemaExtractorParserName.DATASET_PARAM,
},
],
};

View File

@@ -0,0 +1,70 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractor } from '..';
import { imageflowSchemaJSON } from './resource/imageflow-schema';
import { imageflowExtractorConfig } from './resource/imageflow-config';
it('extract imageflow schema', () => {
const schemaExtractor = new SchemaExtractor(imageflowSchemaJSON);
const extractedImageflowSchema = schemaExtractor.extract(
imageflowExtractorConfig,
);
expect(extractedImageflowSchema).toStrictEqual([
{
nodeId: '164069',
nodeType: '4',
properties: {
inputs: [
{ name: 'prompt', value: 'ss', isImage: false },
{ name: 'ratio', value: 'sss', isImage: false },
{ name: 'width', value: 'sss', isImage: false },
{ name: 'height', value: 'ss', isImage: false },
],
},
},
{
nodeId: '164578',
nodeType: '4',
properties: {
inputs: [
{ name: 'reference_picture_url', value: 'ss', isImage: false },
{ name: 'skin', value: 'data', isImage: false },
{ name: 'template_picture_url', value: 'msg', isImage: false },
],
},
},
{
nodeId: '146804',
nodeType: '4',
properties: {
inputs: [{ name: 'prompt', value: 'data', isImage: false }],
},
},
{
nodeId: '140741',
nodeType: '4',
properties: {
inputs: [
{ name: 'bright', value: 'sss', isImage: false },
{ name: 'origin_url', value: 'data', isImage: false },
],
},
},
]);
});

View File

@@ -0,0 +1,489 @@
/*
* 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 { expect, it } from 'vitest';
import { SchemaExtractor } from '..';
import { workflowSchemaJSON } from './resource/workflow-schema';
import { workflowExtractorConfig } from './resource/workflow-config';
it('extract workflow schema', () => {
const schemaExtractor = new SchemaExtractor(workflowSchemaJSON);
const extractedWorkflowSchema = schemaExtractor.extract(
workflowExtractorConfig,
);
expect(extractedWorkflowSchema).toStrictEqual([
{
nodeId: '100001',
nodeType: '1',
properties: {
title: '开始',
outputs: [
{ name: 'start_input_a', description: 'test desc' },
{ name: 'start_input_b' },
],
},
},
{
nodeId: '900001',
nodeType: '2',
properties: {
title: '结束',
inputs: [
{ name: 'output_a', value: 'outputList.output_b', isImage: false },
],
content: '{{output_a}} and {{output_b}}',
},
},
{
nodeId: '154650',
nodeType: '3',
properties: {
title: '大模型',
batch: [
{ name: 'batch_a', value: 'key2', isImage: false },
{ name: 'batch_b', value: 'key4', isImage: false },
],
inputs: [
{ name: 'input_a', value: 'key0', isImage: false },
{ name: 'input_b', value: 'batch_a', isImage: false },
{ name: 'const_c', value: '1234', isImage: false },
],
llmParam: {
prompt: '{{input_a}} and {{input_b}}',
systemPrompt: 'this is systemPrompt',
},
outputs: [
{
name: 'outputList',
children: [
{ name: 'output_a', description: 'desc output_a' },
{ name: 'output_b' },
],
},
],
},
},
{
nodeId: '190950',
nodeType: '5',
properties: {
title: '代码',
inputs: [
{ name: 'code_input_a', value: 'start_input_a', isImage: false },
{ name: 'code_const_b', value: 'test const', isImage: false },
],
code: 'async function main({ params }: Args): Promise<Output> {\n return params; \n}',
outputs: [
{ name: 'key0' },
{ name: 'key1' },
{ name: 'key2' },
{ name: 'key3', children: [{ name: 'key31' }] },
{
name: 'key4',
children: [
{ name: 'key41' },
{ name: 'key42' },
{ name: 'key43' },
{ name: 'key44' },
{ name: 'key45', children: [{ name: 'key451' }] },
],
},
],
},
},
{
nodeId: '111943',
nodeType: '6',
properties: {
title: '知识库',
inputs: [{ name: 'Query', value: 'start_input_b', isImage: false }],
datasetParam: {
datasetList: ['7330215302133268524', '7330215302133268524'],
},
},
},
{
nodeId: '183818',
nodeType: '8',
properties: {
title: '选择器',
branches: [
{
condition: {
logic: 2,
conditions: [
{
operator: 1,
left: {
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'start_input_a',
},
},
},
},
right: {
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '190950',
name: 'key0',
},
},
},
},
},
{
operator: 2,
left: {
input: {
type: 'integer',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '100001',
name: 'start_input_b',
},
},
},
},
right: {
input: {
type: 'integer',
value: { type: 'literal', content: '2' },
},
},
},
{
operator: 1,
left: {
input: {
type: 'string',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '190950',
name: 'key1',
},
},
},
},
right: {
input: {
type: 'string',
value: { type: 'literal', content: 'constant_a' },
},
},
},
],
},
},
],
},
},
{
nodeId: '163608',
nodeType: '11',
properties: {
title: '变量设置',
inputs: [
{ name: 'variable_a', value: 'start_input_a', isImage: false },
],
outputs: [{ name: 'isSuccess' }],
},
},
{
nodeId: '150706',
nodeType: '11',
properties: {
title: '变量获取',
inputs: [{ name: 'Key', value: 'workflow_variable_a', isImage: false }],
outputs: [{ name: 'bot_variable_b' }],
},
},
{
nodeId: '193063',
nodeType: '4',
properties: {
title: 'playOrRecommendMusic',
batch: [
{ name: 'item1', value: 'key2', isImage: false },
{ name: 'item2', value: 'key4.key44', isImage: false },
],
inputs: [
{ name: 'artist', value: 'item1', isImage: false },
{ name: 'user_question', value: 'key0', isImage: false },
{ name: 'song_name', value: 'start_input_b', isImage: false },
{ name: 'description', value: 'start_input_a', isImage: false },
],
outputs: [
{
name: 'outputList',
children: [
{ name: 'response_for_model', description: 'response for model' },
{ name: 'response_type', description: 'response type' },
{ name: 'template_id', description: 'use card template 3' },
{
name: 'type_for_model',
description: 'how to treat response, 2 means directly return',
},
{
name: 'music_list',
description: 'music data list, usually single item',
children: [
{ name: 'song_name', description: 'name of the music' },
{
name: 'start',
description: 'the beginning of the material',
},
{
name: 'source_from',
description: 'id of the source, 1 is soda',
},
{ name: 'vid', description: 'vid of the video model' },
{ name: 'video_auto_play', description: 'if auto play' },
{
name: 'source_id',
description: 'the unique id from source',
},
{
name: 'album_image',
description: 'album image of the music',
},
{
name: 'ref_score',
description: 'confidence score that match user intention',
},
{
name: 'video_model',
description: 'video model to get material of the music',
},
{
name: 'artist_name',
description: 'artist name of the music',
},
{
name: 'duration',
description: 'play duration of the material',
},
{
name: 'source_app_icon',
description: 'icon of the music source',
},
{
name: 'source_app_name',
description: 'name of the music source',
},
],
},
{
name: 'recommend_reason',
description: 'if music is recommended, give reason',
},
{
name: 'rel_score',
description: 'confidence score of found music',
},
],
},
],
},
},
{
nodeId: '139426',
nodeType: '9',
properties: {
title: 'test_ref',
batch: [{ name: 'item1', value: 'key2', isImage: false }],
inputs: [{ name: 'input_a', value: 'item1', isImage: false }],
outputs: [{ name: 'outputList', children: [{ name: 'output_a' }] }],
},
},
{
nodeId: '122146',
nodeType: '11',
properties: {
title: '变量',
inputs: [{ name: 'arr_str', value: 'arr_str', isImage: false }],
outputs: [{ name: 'isSuccess' }],
},
},
{
nodeId: '124687',
nodeType: '11',
properties: {
title: '变量_1',
inputs: [{ name: 'Key', value: 'sss', isImage: false }],
outputs: [{ name: 'dddd' }],
},
},
{
nodeId: '184010',
nodeType: '21',
properties: {
title: '循环',
inputs: [{ name: 'arr_str', value: 'arr_str', isImage: false }],
variables: [
{ name: 'var_str', value: 'str', isImage: false },
{ name: 'var_num', value: 'num', isImage: false },
{ name: 'var_bool', value: 'bool', isImage: false },
],
outputs: [{ name: 'output_list' }],
},
},
{
nodeId: '149710',
nodeType: '20',
properties: {
title: '循环变量',
inputs: [
{ name: 'var_str', value: 'new_str' },
{ name: 'var_num', value: 'new_num' },
{ name: 'var_bool', value: 'new_bool' },
],
},
},
{
nodeId: '185397',
nodeType: '13',
properties: {
title: '消息',
inputs: [
{ name: 'str', value: 'var_str', isImage: false },
{ name: 'num', value: 'var_num', isImage: false },
{ name: 'bool', value: 'var_bool', isImage: false },
],
content: 'str: {{str}}\nnum: {{num}}\nbool: {{bool}}',
},
},
{
nodeId: '146923',
nodeType: '5',
properties: {
title: '代码',
inputs: [
{ name: 'var_str', value: 'var_str', isImage: false },
{ name: 'var_num', value: 'var_num', isImage: false },
{ name: 'var_bool', value: 'var_bool', isImage: false },
],
code: 'async function main({ params }: Args): Promise<Output> {\n return {\n "new_str": params.var_str + \'✅\',\n "new_num": params.var_num + 1,\n "new_bool": !params.var_bool,\n };\n}',
outputs: [
{ name: 'new_str' },
{ name: 'new_num' },
{ name: 'new_bool' },
],
},
},
{
nodeId: '123896',
nodeType: '15',
properties: {
title: '文本处理',
concatResult: 'str: {{String1}}\nnum: {{String2}}\nbool: {{String3}}',
arrayConcatChar: '',
splitChar: '',
},
},
{
nodeId: '179778',
nodeType: '8',
properties: {
title: '选择器',
branches: [
{
condition: {
logic: 2,
conditions: [
{
operator: 1,
left: {
input: {
type: 'boolean',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_bool',
},
},
},
},
right: {
input: {
type: 'boolean',
value: { type: 'literal', content: 'true' },
},
},
},
],
},
},
],
},
},
{
nodeId: '177333',
nodeType: '8',
properties: {
title: '选择器_1',
branches: [
{
condition: {
logic: 2,
conditions: [
{
operator: 13,
left: {
input: {
type: 'float',
value: {
type: 'ref',
content: {
source: 'block-output',
blockID: '184010',
name: 'var_num',
},
},
},
},
right: {
input: {
type: 'integer',
value: { type: 'literal', content: '5' },
},
},
},
],
},
},
],
},
},
{ nodeId: '194199', nodeType: '19', properties: { title: '终止循环' } },
]);
});

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum SchemaExtractorParserName {
DEFAULT = 'default',
INPUT_PARAMETERS = 'inputParameters',
OUTPUTS = 'outputs',
DATASET_PARAM = 'datasetParam',
LLM_PARAM = 'llmParam',
INTENTS = 'intents',
CONCAT_RESULT = 'concatResult',
CUSTOM_ARRAY_CONCAT_CHAR = 'customArrayConcatChar',
CUSTOM_SPLIT_CHAR = 'customSplitChar',
REF_INPUT_PARAMETER = 'refInputParameter',
VARIABLE_ASSIGN = 'variableAssign',
JSON_STRING_PARSER = 'jsonStringParser',
IMAGE_REFERENCE_PARSER = 'imageReferenceParser',
EXPRESSION_PARSER = 'expressionParser',
VARIABLE_MERGE_GROUPS_PARSER = 'variableMergeGroupsParser',
DB_FIELDS_PARSER = 'dbFieldsParser',
DB_CONDITIONS_PARSER = 'dbConditionsParser',
}
export const SYSTEM_DELIMITERS = [
'\n',
'\t',
'.',
'。',
',',
'',
';',
'',
' ',
];

View File

@@ -0,0 +1,153 @@
/*
* 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 { get, cloneDeep } from 'lodash-es';
import type { WorkflowEdgeJSON } from '@flowgram-adapter/free-layout-editor';
import type {
StandardNodeType,
WorkflowJSON,
WorkflowNodeJSON,
} from '../../types';
import type {
SchemaExtracted,
SchemaExtractorConfig,
SchemaExtractorNodeConfig,
SchemaExtractorParser,
} from './type';
import { schemaExtractorParsers } from './parsers';
import { SchemaExtractorParserName } from './constant';
export { SchemaExtractorParserName } from './constant';
export type {
SchemaExtractorConfig,
SchemaExtracted,
SchemaExtractorNodeConfig,
ParsedVariableMergeGroups,
} from './type';
export class SchemaExtractor {
private readonly schema: WorkflowJSON;
private readonly parser: Record<
SchemaExtractorParserName,
SchemaExtractorParser
>;
constructor(schema: WorkflowJSON) {
this.schema = this.flatSchema(cloneDeep(schema));
this.parser = schemaExtractorParsers;
}
public extract(config: SchemaExtractorConfig): SchemaExtracted[] {
this.bindParser(config);
// 1. 遍历schema中node数组对每个node做处理
return this.schema.nodes
.map((node: WorkflowNodeJSON): SchemaExtracted | null => {
// 2. 获取节点对应的配置
const nodeConfigs: SchemaExtractorNodeConfig[] = config[node.type];
if (!nodeConfigs) {
return null;
}
return {
nodeId: node.id,
nodeType: node.type as StandardNodeType,
properties: this.extractNode(nodeConfigs, node.data),
};
})
.filter(Boolean) as SchemaExtracted[];
}
private extractNode(
nodeConfigs: SchemaExtractorNodeConfig[],
nodeData: Record<string, unknown>,
): Record<string, unknown> {
return nodeConfigs.reduce(
(
extractedConfig: Record<string, unknown>,
nodeConfig: SchemaExtractorNodeConfig,
): Record<string, unknown> => {
// 3. 根据节点配置路径获取属性值
const rawData: unknown = this.extractProperties(
nodeData,
nodeConfig.path,
);
if (nodeConfig.parser && typeof nodeConfig.parser === 'function') {
// 4. 使用解析器对属性值进行转换
extractedConfig[nodeConfig.name] = nodeConfig.parser(rawData);
}
return extractedConfig;
},
{},
);
}
private extractProperties(properties: Record<string, unknown>, path: string) {
return get(properties, path);
}
private bindParser(config: SchemaExtractorConfig) {
Object.entries(config).forEach(([nodeType, nodeConfigs]) => {
nodeConfigs.forEach(nodeConfig => {
if (!nodeConfig.parser) {
nodeConfig.parser = SchemaExtractorParserName.DEFAULT;
}
if (typeof nodeConfig.parser === 'string') {
nodeConfig.parser = this.parser[nodeConfig.parser];
}
});
});
}
private getEdgeID(edge: WorkflowEdgeJSON): string {
const from = edge.sourceNodeID;
const to = edge.targetNodeID;
const fromPort = edge.sourcePortID;
const toPort = edge.targetPortID;
return `${from}_${fromPort || ''}-${to || ''}_${toPort || ''}`;
}
private flatSchema(
json: WorkflowJSON = { nodes: [], edges: [] },
): WorkflowJSON {
const rootNodes = json.nodes ?? [];
const rootEdges = json.edges ?? [];
const flattenNodeJSONs: WorkflowNodeJSON[] = [...rootNodes];
const flattenEdgeJSONs: WorkflowEdgeJSON[] = [...rootEdges];
// 如需支持多层结构,以下部分改为递归
rootNodes.forEach(nodeJSON => {
const { blocks, edges } = nodeJSON;
if (blocks) {
flattenNodeJSONs.push(...blocks);
const blockIDs: string[] = [];
blocks.forEach(block => {
blockIDs.push(block.id);
});
delete nodeJSON.blocks;
}
if (edges) {
flattenEdgeJSONs.push(...edges);
const edgeIDs: string[] = [];
edges.forEach(edge => {
const edgeID = this.getEdgeID(edge);
edgeIDs.push(edgeID);
});
delete nodeJSON.edges;
}
});
const flattenSchema: WorkflowJSON = {
nodes: flattenNodeJSONs,
edges: flattenEdgeJSONs,
};
return flattenSchema;
}
}

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 { get } from 'lodash-es';
import { type SchemaExtractorConcatResultParser } from '../type';
export const concatResultParser: SchemaExtractorConcatResultParser =
concatParams => {
const concatResult = (concatParams || []).find(
v => v.name === 'concatResult',
);
return get(concatResult, 'input.value.content', '') as string;
};

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import { type SchemaExtractorArrayConcatCharParser } from '../type';
import { SYSTEM_DELIMITERS } from '../constant';
export const arrayConcatCharParser: SchemaExtractorArrayConcatCharParser =
concatParams => {
const allArrayItemConcatChars = (concatParams || []).find(
v => v.name === 'allArrayItemConcatChars',
);
let customConcatChars = '';
if (allArrayItemConcatChars) {
const list = get(allArrayItemConcatChars, 'input.value.content', []) as {
value: string;
}[];
const customItems = list.filter(
v => !SYSTEM_DELIMITERS.includes(v.value),
);
customConcatChars = customItems.map(v => v.value).join(', ');
}
return customConcatChars;
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import { type SchemaExtractorSplitCharParser } from '../type';
import { SYSTEM_DELIMITERS } from '../constant';
export const splitCharParser: SchemaExtractorSplitCharParser = splitParams => {
const allDelimiters = (splitParams || []).find(
v => v.name === 'allDelimiters',
);
let customDelimiters = '';
if (allDelimiters) {
const list = get(allDelimiters, 'input.value.content', []) as {
value: string;
}[];
const customItems = list.filter(v => !SYSTEM_DELIMITERS.includes(v.value));
customDelimiters = customItems.map(v => v.value).join(', ');
}
return customDelimiters;
};

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 { get } from 'lodash-es';
import { type SchemaExtractorDatasetParamParser } from '../type';
export const datasetParamParser: SchemaExtractorDatasetParamParser =
datasetParam => {
const datasetListItem = datasetParam.find(
param => param.name === 'datasetList',
);
const datasetList = get(datasetListItem, 'input.value.content');
if (!datasetList || !Array.isArray(datasetList)) {
return {
datasetList: [],
};
}
return {
datasetList,
};
};

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.
*/
import { type SchemaExtractorDbConditionsParser } from '../type';
import { inputParametersParser } from './input-parameters';
export const dbConditionsParser: SchemaExtractorDbConditionsParser =
conditionList =>
conditionList
?.flatMap(conditions => inputParametersParser(conditions || []))
?.filter(Boolean) as ReturnType<SchemaExtractorDbConditionsParser>;

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { parseExpression } from '../utils';
import { type SchemaExtractorDbFieldsParser } from '../type';
export const dbFieldsParser: SchemaExtractorDbFieldsParser = dbFields =>
dbFields
?.map(([fieldID, fieldValue]) => {
const parsedFieldID = parseExpression(fieldID?.input);
const parsedFieldValue = parseExpression(fieldValue?.input);
if (!parsedFieldValue) {
return null;
}
return {
name: parsedFieldID?.value,
value: parsedFieldValue?.value,
isImage: parsedFieldValue?.isImage,
};
})
?.filter(Boolean) as ReturnType<SchemaExtractorDbFieldsParser>;

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 { parseExpression } from '../utils';
import { type SchemaExtractorExpressionParser } from '../type';
import type { ValueExpressionDTO } from '../../../types';
export const expressionParser: SchemaExtractorExpressionParser = expression => {
const expressions = ([] as ValueExpressionDTO[])
.concat(expression)
.filter(Boolean);
return expressions
.map(parseExpression)
.filter(Boolean) as ReturnType<SchemaExtractorExpressionParser>;
};

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { SchemaExtractorImageReferenceParser } from '../type';
import { inputParametersParser } from './input-parameters';
export const imageReferenceParser: SchemaExtractorImageReferenceParser =
references => {
if (!Array.isArray(references)) {
return [];
}
return inputParametersParser(
references.map(ref => ({
name: '-',
input: ref.url,
})),
);
};

View File

@@ -0,0 +1,58 @@
/*
* 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 SchemaExtractorParser } from '../type';
import { SchemaExtractorParserName } from '../constant';
import { variableMergeGroupsParser } from './variable-merge-groups-parser';
import { variableAssignParser } from './variable-assign';
import { refInputParametersParser } from './ref-input-parameters';
import { outputsParser } from './output';
import { llmParamParser } from './llm-param';
import { jsonStringParser } from './json-string-parser';
import { intentsParser } from './intents';
import { inputParametersParser } from './input-parameters';
import { imageReferenceParser } from './image-reference';
import { expressionParser } from './expression-parser';
import { dbFieldsParser } from './db-fields';
import { dbConditionsParser } from './db-conditions';
import { datasetParamParser } from './dataset-param';
import { splitCharParser } from './custom-split-char';
import { arrayConcatCharParser } from './custom-array-concat-char';
import { concatResultParser } from './concat-result';
export const schemaExtractorParsers: Record<
SchemaExtractorParserName,
SchemaExtractorParser
> = {
[SchemaExtractorParserName.DEFAULT]: t => t,
[SchemaExtractorParserName.INPUT_PARAMETERS]: inputParametersParser,
[SchemaExtractorParserName.OUTPUTS]: outputsParser,
[SchemaExtractorParserName.DATASET_PARAM]: datasetParamParser,
[SchemaExtractorParserName.LLM_PARAM]: llmParamParser,
[SchemaExtractorParserName.INTENTS]: intentsParser,
[SchemaExtractorParserName.CONCAT_RESULT]: concatResultParser,
[SchemaExtractorParserName.CUSTOM_ARRAY_CONCAT_CHAR]: arrayConcatCharParser,
[SchemaExtractorParserName.CUSTOM_SPLIT_CHAR]: splitCharParser,
[SchemaExtractorParserName.REF_INPUT_PARAMETER]: refInputParametersParser,
[SchemaExtractorParserName.VARIABLE_ASSIGN]: variableAssignParser,
[SchemaExtractorParserName.JSON_STRING_PARSER]: jsonStringParser,
[SchemaExtractorParserName.IMAGE_REFERENCE_PARSER]: imageReferenceParser,
[SchemaExtractorParserName.EXPRESSION_PARSER]: expressionParser,
[SchemaExtractorParserName.VARIABLE_MERGE_GROUPS_PARSER]:
variableMergeGroupsParser,
[SchemaExtractorParserName.DB_FIELDS_PARSER]: dbFieldsParser,
[SchemaExtractorParserName.DB_CONDITIONS_PARSER]: dbConditionsParser,
};

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import { parseExpression } from '../utils';
import { type SchemaExtractorInputParametersParser } from '../type';
import type { InputValueDTO } from '../../../types';
export const inputParametersParser: SchemaExtractorInputParametersParser =
inputParameters => {
let parameters: InputValueDTO[] = [];
if (!Array.isArray(inputParameters)) {
if (typeof inputParameters === 'object') {
Object.keys(inputParameters || {}).forEach(key => {
parameters.push({
name: key,
input: inputParameters[key],
});
});
}
} else {
parameters = inputParameters;
}
return parameters
.map(inputParameter => {
const expression = get(inputParameter, 'input');
const parsedExpression = parseExpression(expression);
if (!parsedExpression) {
return null;
}
return {
name: inputParameter.name,
...parsedExpression,
};
})
.filter(Boolean) as ReturnType<SchemaExtractorInputParametersParser>;
};

View File

@@ -0,0 +1,21 @@
/*
* 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 SchemaExtractorIntentsParamParser } from '../type';
export const intentsParser: SchemaExtractorIntentsParamParser = intents => ({
intent: intents.map((item, idx) => `${idx + 1}. ${item.name}`).join(' '),
});

View File

@@ -0,0 +1,21 @@
/*
* 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 SchemaExtractorJSONStringParser } from '../type';
export const jsonStringParser: SchemaExtractorJSONStringParser = (
jsonString: string,
) => JSON.parse(jsonString || '{}') as object | object[] | undefined;

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { get } from 'lodash-es';
import { type SchemaExtractorLLMParamParser } from '../type';
export const llmParamParser: SchemaExtractorLLMParamParser = llmParam => {
const promptItem = llmParam.find(param => param.name === 'prompt');
const prompt = (get(promptItem, 'input.value.content') as string) || '';
const systemPromptItem = llmParam.find(
param => param.name === 'systemPrompt',
);
const systemPrompt =
(get(systemPromptItem, 'input.value.content') as string) || '';
return {
systemPrompt,
prompt,
};
};

View File

@@ -0,0 +1,92 @@
/*
* 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 { isWorkflowImageTypeURL } from '../utils';
import { type SchemaExtractorOutputsParser } from '../type';
import { AssistTypeDTO, VariableTypeDTO } from '../../../types/dto';
export const outputsParser: SchemaExtractorOutputsParser = outputs => {
// 判断是否为数组
if (!Array.isArray(outputs)) {
return [];
}
return outputs.map(output => {
const parsed: {
name: string;
description?: string;
children?: ReturnType<SchemaExtractorOutputsParser>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value?: any;
isImage?: boolean;
// 默认值里包含图片时,图片信息单独放到这里
images?: string[];
} = {
name: output.name || '',
};
if (output.description) {
parsed.description = output.description;
}
if (output.type === 'object' && Array.isArray(output.schema)) {
parsed.children = outputsParser(output.schema);
}
if (output.type === 'list' && Array.isArray(output.schema?.schema)) {
parsed.children = outputsParser(output.schema.schema);
}
// Start 节点默认值放到 value 上
if (output.defaultValue) {
parsed.value = output.defaultValue;
// string、file、image、svg
if (
(output.type === 'string' &&
isWorkflowImageTypeURL(output.defaultValue)) ||
[AssistTypeDTO.image, AssistTypeDTO.svg].includes(
output.assistType as AssistTypeDTO,
)
) {
parsed.images = [String(output.defaultValue)];
} else if (output.type === VariableTypeDTO.list) {
// Array<Image> | Array<Svg>
if (
[AssistTypeDTO.image, AssistTypeDTO.svg].includes(
output.schema?.assistType,
)
) {
try {
const list = JSON.parse(output.defaultValue) as string[];
Array.isArray(list) &&
(parsed.images = list.map(item => String(item)));
} catch (e) {
console.error(e);
}
// Array<File>
} else if (output.schema?.assistType === AssistTypeDTO.file) {
try {
const list = JSON.parse(output.defaultValue) as string[];
Array.isArray(list) &&
(parsed.images = list
.map(item => String(item))
.filter(item => isWorkflowImageTypeURL(item)));
} catch (e) {
console.error(e);
}
}
}
parsed.isImage = (parsed.images?.length ?? 0) > 0;
}
return parsed;
});
};

View File

@@ -0,0 +1,58 @@
/*
* 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 { get, isPlainObject } from 'lodash-es';
import { isWorkflowImageTypeURL } from '../utils';
import { type SchemaExtractorReferencesParser } from '../type';
interface Item {
name: string;
value: string;
isImage: boolean;
}
interface ReferenceValue {
type: string;
value: {
content: string;
};
}
export const refInputParametersParser: SchemaExtractorReferencesParser =
references => {
const results: Item[] = [];
for (const refObject of references) {
const keys = Object.keys(refObject);
for (const itemName of keys) {
const itemValue = refObject[itemName];
if (
isPlainObject(itemValue) &&
(itemValue as ReferenceValue)?.type === 'string'
) {
const content = get(itemValue as ReferenceValue, 'value.content');
results.push({
name: itemName,
value: content,
isImage: isWorkflowImageTypeURL(content),
});
}
}
}
return results;
};

View File

@@ -0,0 +1,70 @@
/*
* 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 SchemaExtractorVariableAssignParser } from '../type';
import { type ValueExpressionDTO, type DTODefine } from '../../../types';
const getValueExpressionName = (
valueExpression: ValueExpressionDTO,
): string | undefined => {
const content = valueExpression?.value?.content as
| DTODefine.RefExpressionContent
| string;
if (!content) {
return;
}
if (typeof content === 'string') {
return content;
} else if (typeof content === 'object') {
if (content.source === 'block-output' && typeof content.name === 'string') {
return content.name;
} else if (
typeof content.source === 'string' &&
content.source.startsWith('global_variable')
) {
return (
content as {
source: `global_variable_${string}`;
path: string[];
blockID: string;
name: string;
}
)?.path?.join('.');
}
}
};
export const variableAssignParser: SchemaExtractorVariableAssignParser =
variableAssigns => {
if (!Array.isArray(variableAssigns)) {
return [];
}
return variableAssigns
.map(variableAssign => {
const leftContent = getValueExpressionName(variableAssign.left);
const rightContent = getValueExpressionName(variableAssign.right);
// 变量赋值节点的右值字段
const inputContent = variableAssign.input
? getValueExpressionName(variableAssign.input)
: null;
return {
name: leftContent ?? '',
value: rightContent ?? inputContent ?? '',
};
})
.filter(Boolean) as ReturnType<SchemaExtractorVariableAssignParser>;
};

View File

@@ -0,0 +1,24 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { type SchemaExtractorVariableMergeGroupsParser } from '../type';
import { expressionParser } from './expression-parser';
export const variableMergeGroupsParser: SchemaExtractorVariableMergeGroupsParser =
mergeGroups =>
mergeGroups.map(group => ({
groupName: group.name,
variables: expressionParser(group.variables),
}));

View File

@@ -0,0 +1,166 @@
/*
* 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 { DatasetParams } from '../../types/data-set';
import type {
InputValueDTO,
VariableMetaDTO,
StandardNodeType,
ValueExpressionDTO,
} from '../../types';
import type { SchemaExtractorParserName } from './constant';
export type SchemaExtractorConfig = Partial<
Record<StandardNodeType, SchemaExtractorNodeConfig[]>
>;
export interface SchemaExtractorNodeConfig {
name: string;
/** lodash.get 入参格式 */
path: string;
parser?: SchemaExtractorParserName | Function;
displayName?: string;
}
export interface SchemaExtracted {
nodeId: string;
nodeType: StandardNodeType;
properties: Record<string, unknown>;
}
export type SchemaExtractorParser<
T extends SchemaExtractorParserName = SchemaExtractorParserName,
> = {
[SchemaExtractorParserName.DEFAULT]: SchemaExtractorDefaultParser;
[SchemaExtractorParserName.INPUT_PARAMETERS]: SchemaExtractorInputParametersParser;
[SchemaExtractorParserName.OUTPUTS]: SchemaExtractorOutputsParser;
[SchemaExtractorParserName.DATASET_PARAM]: SchemaExtractorDatasetParamParser;
[SchemaExtractorParserName.LLM_PARAM]: SchemaExtractorLLMParamParser;
[SchemaExtractorParserName.INTENTS]: SchemaExtractorIntentsParamParser;
[SchemaExtractorParserName.CONCAT_RESULT]: SchemaExtractorConcatResultParser;
[SchemaExtractorParserName.CUSTOM_ARRAY_CONCAT_CHAR]: SchemaExtractorArrayConcatCharParser;
[SchemaExtractorParserName.CUSTOM_SPLIT_CHAR]: SchemaExtractorSplitCharParser;
[SchemaExtractorParserName.REF_INPUT_PARAMETER]: SchemaExtractorReferencesParser;
[SchemaExtractorParserName.VARIABLE_ASSIGN]: SchemaExtractorVariableAssignParser;
[SchemaExtractorParserName.JSON_STRING_PARSER]: SchemaExtractorJSONStringParser;
[SchemaExtractorParserName.IMAGE_REFERENCE_PARSER]: SchemaExtractorImageReferenceParser;
[SchemaExtractorParserName.EXPRESSION_PARSER]: SchemaExtractorExpressionParser;
[SchemaExtractorParserName.VARIABLE_MERGE_GROUPS_PARSER]: SchemaExtractorVariableMergeGroupsParser;
[SchemaExtractorParserName.DB_FIELDS_PARSER]: SchemaExtractorDbFieldsParser;
[SchemaExtractorParserName.DB_CONDITIONS_PARSER]: SchemaExtractorDbConditionsParser;
}[T];
export type SchemaExtractorDefaultParser = (arg: unknown) => unknown;
export interface ParsedExpression {
name: string;
value: string;
isImage: boolean;
}
export type SchemaExtractorInputParametersParser = (
inputParameters: InputValueDTO[] | Record<string, InputValueDTO['input']>,
) => ParsedExpression[];
export type SchemaExtractorDbFieldsParser = (
inputParameters: Array<[InputValueDTO, InputValueDTO]>,
) => ParsedExpression[];
export type SchemaExtractorDbConditionsParser = (
inputParameters: Array<InputValueDTO[]>,
) => ParsedExpression[];
export type SchemaExtractorReferencesParser = (
reference: Record<string, unknown>[],
) => ParsedExpression[];
export type SchemaExtractorOutputsParser = (outputs: VariableMetaDTO[]) => {
name: string;
description?: string;
children?: ReturnType<SchemaExtractorOutputsParser>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value?: any;
isImage?: boolean;
// 默认值里包含图片时,图片信息单独放到这里
images?: string[];
}[];
export type SchemaExtractorDatasetParamParser = (outputs: DatasetParams) => {
datasetList: string[];
};
export type SchemaExtractorLLMParamParser = (llmParam: InputValueDTO[]) => {
prompt: string;
systemPrompt: string;
};
export type SchemaExtractorIntentsParamParser = (
intents: { name: string }[],
) => {
intent: string;
};
export type SchemaExtractorConcatResultParser = (
concatParams: InputValueDTO[],
) => string;
export type SchemaExtractorArrayConcatCharParser = (
concatParams: InputValueDTO[],
) => string;
export type SchemaExtractorSplitCharParser = (
concatParams: InputValueDTO[],
) => string;
export type SchemaExtractorVariableAssignParser = (
variableAssigns: {
left: ValueExpressionDTO;
right: ValueExpressionDTO;
input?: ValueExpressionDTO;
}[],
) => {
name: string;
value: string;
}[];
export type SchemaExtractorJSONStringParser = (
jsonString: string,
) => object | object[] | undefined;
type ImageReferences = Array<{
url: ValueExpressionDTO;
}>;
export type SchemaExtractorImageReferenceParser = (
references: ImageReferences,
) => ParsedExpression[];
export type SchemaExtractorExpressionParser = (
expression: ValueExpressionDTO[] | ValueExpressionDTO,
) => ParsedExpression[];
export interface VariableMergeGroupType {
name: string;
variables: ValueExpressionDTO[];
}
export interface ParsedVariableMergeGroups {
groupName: string;
variables: ParsedExpression[];
}
export type SchemaExtractorVariableMergeGroupsParser = (
mergeGroups: VariableMergeGroupType[],
) => ParsedVariableMergeGroups[];

View File

@@ -0,0 +1,84 @@
/*
* 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 { get } from 'lodash-es';
import { type ValueExpressionDTO } from '../../types';
// 是否我们自己上传生成的url, 这是个临时方案,等修复 schema 中的 type:string -> type:image 后,删掉此逻辑
export function isWorkflowImageTypeURL(str: string): boolean {
// base64 加工
const hostWhiteList = [
'cC1ib3Qtd29ya2Zsb3ctc2lnbi5ieXRlZGFuY2UubmV0',
'cC1ib3Qtd29ya2Zsb3cuYnl0ZWQub3Jn',
'cC1ib3Qtd29ya2Zsb3cuYnl0ZWRhbmNlLm5ldA==',
'cDI2LWJvdC13b3JrZmxvdy1zaWduLmJ5dGVpbWcuY29t',
'cDMtYm90LXdvcmtmbG93LXNpZ24uYnl0ZWltZy5jb20=',
'cDktYm90LXdvcmtmbG93LXNpZ24uYnl0ZWltZy5jb20=',
];
const suffixWhiteList = ['image', 'jpg', 'jpeg', 'png'];
let urlObj;
try {
urlObj = new URL(str);
} catch (_) {
return false;
}
const suffix =
urlObj.searchParams?.get('x-wf-file_name')?.split('.')?.pop() ||
urlObj.pathname.split('.').pop();
if (!suffixWhiteList.includes(suffix)) {
return false;
}
if (!hostWhiteList.includes(btoa(urlObj.hostname ?? ''))) {
return false;
}
return true;
}
export const parseExpression = (expression?: ValueExpressionDTO) => {
if (!expression) {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const content = get(expression, 'value.content') as any;
if (!content) {
return null;
} else if (typeof content === 'string') {
return {
value: content,
isImage: isWorkflowImageTypeURL(content),
};
} else if (content.source === 'block-output' && typeof content.name) {
return {
value: content.name,
isImage: false,
};
} else if (
typeof content.source === 'string' &&
content.source.startsWith('global_variable')
) {
return {
value: content?.path?.join('.'),
isImage: false,
};
}
};

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { reporter as infraReporter } from '@coze-arch/logger';
const namespace = 'workflow';
/**
* 流程使用的 slardar 上报实例
*/
export const reporter = infraReporter.createReporterWithPreset({
namespace,
});
/**
* 异常捕获会被当js error上报
* @param exception
* @param importErrorInfo
*/
export function captureException(exception: Error) {
infraReporter.slardarInstance?.('captureException', exception, {
isErrorBoundary: 'false',
namespace,
});
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BOT_USER_INPUT, CONVERSATION_NAME, USER_INPUT } from '../constants';
/**
* 是否预设的开始节点的输入参数
*/
export const isPresetStartParams = (name?: string): boolean =>
[BOT_USER_INPUT, USER_INPUT, CONVERSATION_NAME].includes(name ?? '');
/**
* Start 节点参数是 BOT 聊天时用户的输入内容
* @param name
* @returns
*/
export const isUserInputStartParams = (name?: string): boolean =>
[BOT_USER_INPUT, USER_INPUT].includes(name ?? '');

View File

@@ -0,0 +1,183 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable security/detect-object-injection -- no-need */
/* eslint-disable @typescript-eslint/no-namespace -- no-need */
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- no-need
export type TraverseValue = any;
export interface TraverseNode {
value: TraverseValue;
container?: TraverseValue;
parent?: TraverseNode;
key?: string;
index?: number;
}
export interface TraverseContext {
node: TraverseNode;
setValue: (value: TraverseValue) => void;
getParents: () => TraverseNode[];
getPath: () => Array<string | number>;
getStringifyPath: () => string;
deleteSelf: () => void;
}
export type TraverseHandler = (context: TraverseContext) => void;
/**
* 深度遍历对象,对每个值做处理
* @param value 遍历对象
* @param handle 处理函数
*/
export const traverse = <T extends TraverseValue = TraverseValue>(
value: T,
handler: TraverseHandler | TraverseHandler[],
): T => {
const traverseHandler: TraverseHandler = Array.isArray(handler)
? (context: TraverseContext) => {
handler.forEach(handlerFn => handlerFn(context));
}
: handler;
TraverseUtils.traverseNodes({ value }, traverseHandler);
return value;
};
namespace TraverseUtils {
/**
* 深度遍历对象,对每个值做处理
* @param node 遍历节点
* @param handle 处理函数
*/
export const traverseNodes = (
node: TraverseNode,
handle: TraverseHandler,
): void => {
const { value } = node;
if (!value) {
// 异常处理
return;
}
if (Object.prototype.toString.call(value) === '[object Object]') {
// 对象,遍历对象的每个属性
Object.entries(value).forEach(([key, item]) =>
traverseNodes(
{
value: item,
container: value,
key,
parent: node,
},
handle,
),
);
} else if (Array.isArray(value)) {
// 数组,遍历数组的每个元素
// 从数组的末尾开始遍历,这样即使中途移除了某个元素,也不会影响到未处理的元素的索引
for (let index = value.length - 1; index >= 0; index--) {
const item: string = value[index];
traverseNodes(
{
value: item,
container: value,
index,
parent: node,
},
handle,
);
}
}
const context: TraverseContext = createContext({ node });
handle(context);
};
const createContext = ({
node,
}: {
node: TraverseNode;
}): TraverseContext => ({
node,
setValue: (value: unknown) => setValue(node, value),
getParents: () => getParents(node),
getPath: () => getPath(node),
getStringifyPath: () => getStringifyPath(node),
deleteSelf: () => deleteSelf(node),
});
const setValue = (node: TraverseNode, value: unknown) => {
// 设置值函数
// 引用类型,需要借助父元素修改值
// 由于是递归遍历所以需要根据node来判断是给对象的哪个属性赋值还是给数组的哪个元素赋值
if (!value || !node) {
return;
}
node.value = value;
// 从上级作用域node中取出containerkeyindex
const { container, key, index } = node;
if (key && container) {
container[key] = value;
} else if (typeof index === 'number') {
container[index] = value;
}
};
const getParents = (node: TraverseNode): TraverseNode[] => {
const parents: TraverseNode[] = [];
let currentNode: TraverseNode | undefined = node;
while (currentNode) {
parents.unshift(currentNode);
currentNode = currentNode.parent;
}
return parents;
};
const getPath = (node: TraverseNode): Array<string | number> => {
const path: Array<string | number> = [];
const parents = getParents(node);
parents.forEach(parent => {
if (parent.key) {
path.unshift(parent.key);
} else if (parent.index) {
path.unshift(parent.index);
}
});
return path;
};
const getStringifyPath = (node: TraverseNode): string => {
const path = getPath(node);
return path.reduce((stringifyPath: string, pathItem: string | number) => {
if (typeof pathItem === 'string') {
const re = /\W/g;
if (re.test(pathItem)) {
// 包含特殊字符
return `${stringifyPath}["${pathItem}"]`;
}
return `${stringifyPath}.${pathItem}`;
} else {
return `${stringifyPath}[${pathItem}]`;
}
}, '');
};
const deleteSelf = (node: TraverseNode): void => {
const { container, key, index } = node;
if (key && container) {
delete container[key];
} else if (typeof index === 'number') {
container.splice(index, 1);
}
};
}