coze-studio/frontend/packages/workflow/nodes/src/workflow-json-format.ts

304 lines
8.6 KiB
TypeScript

/*
* 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, isEmpty, pick, set, isFunction } from 'lodash-es';
import { injectable } from 'inversify';
import {
PlaygroundContext,
lazyInject,
} from '@flowgram-adapter/free-layout-editor';
import {
type WorkflowNodeEntity,
type WorkflowDocument,
type WorkflowJSON,
type WorkflowJSONFormatContribution,
type WorkflowNodeJSON,
} from '@flowgram-adapter/free-layout-editor';
import {
WorkflowBatchService,
WorkflowVariableService,
variableUtils,
type ViewVariableMeta,
} from '@coze-workflow/variable';
import {
StandardNodeType,
type BatchDTOInputList,
type InputValueDTO,
type InputValueVO,
type ValueExpressionDTO,
type VariableMetaDTO,
} from '@coze-workflow/base';
import {
WorkflowNodeVariablesMeta,
type WorkflowNodeRegistry,
} from './typings';
/**
* Transform form data globally, where variable generation logic is primarily handled
*/
@injectable()
export class WorkflowJSONFormat implements WorkflowJSONFormatContribution {
@lazyInject(WorkflowVariableService)
declare variableService: WorkflowVariableService;
@lazyInject(PlaygroundContext) declare playgroundContext: PlaygroundContext;
@lazyInject(WorkflowBatchService) batchService: WorkflowBatchService;
protected getVariablesMeta(
nodeType: string | number,
document: WorkflowDocument,
): WorkflowNodeVariablesMeta {
const { variablesMeta = WorkflowNodeVariablesMeta.DEFAULT } =
document.getNodeRegister<WorkflowNodeRegistry>(nodeType);
return variablesMeta || WorkflowNodeVariablesMeta.DEFAULT;
}
/**
* Convert node variable
* @param json
* @protected
*/
protected formatOutputVariables(
json: WorkflowNodeJSON,
doc: WorkflowDocument,
): void {
const variablesMeta = this.getVariablesMeta(json.type, doc);
if (json.data) {
variablesMeta.outputsPathList!.forEach(outputsPath => {
const variableMetas = get(json.data, outputsPath) as VariableMetaDTO[];
if (variableMetas && Array.isArray(variableMetas)) {
variableMetas.forEach((meta: VariableMetaDTO, index) => {
if (!meta.type) {
return;
}
const newData = variableUtils.dtoMetaToViewMeta(meta);
set(variableMetas, index, newData);
});
}
});
}
}
/**
* Compatible with historical data with only a single batch variable
*/
protected transformBatchVariable(
json: WorkflowNodeJSON,
doc: WorkflowDocument,
): void {
if (!json.data) {
return;
}
const input: ValueExpressionDTO = get(json.data, 'inputs.batch.inputList');
if (!input || isEmpty(input)) {
// No compatibility required
return;
}
const inputList: BatchDTOInputList = {
name: 'item', // According to PRD, just write it dead.
input,
};
// Filling in new data in JSON
json.data.inputs.batch.inputLists = [inputList];
// Delete old data
delete json.data.inputs.batch.inputList;
}
/**
* Fix variable reference logic
* @param json
* @param doc
* @protected
*/
protected formatInputVariables(
json: WorkflowNodeJSON,
doc: WorkflowDocument,
): void {
const variablesMeta = this.getVariablesMeta(json.type, doc);
if (json.data) {
variablesMeta.inputsPathList!.forEach(inputsPath => {
const inputValues: InputValueDTO[] = get(
json.data,
inputsPath,
) as InputValueDTO[];
if (inputValues && Array.isArray(inputValues)) {
inputValues.map((inputValue, index) => {
set(
inputValues,
index,
variableUtils.inputValueToVO(inputValue, this.variableService),
);
});
}
});
}
}
/**
* Processing node metadata & static copy
* @param json
* @param doc
* @protected
*/
protected formatNodeMeta(
json: WorkflowNodeJSON,
doc: WorkflowDocument,
): void {
// The API plug-in node does not need to process copywriting for the time being.
if (
json?.type &&
[StandardNodeType.Api, StandardNodeType.SubWorkflow].includes(
json.type as StandardNodeType,
)
) {
return;
}
const nodeMeta = get(json, 'data.nodeMeta');
// Latest configuration data pulled
const meta = this.playgroundContext.getNodeTemplateInfoByType(json.type);
if (
nodeMeta &&
typeof nodeMeta === 'object' &&
meta &&
typeof meta === 'object'
) {
// Determined according to the backend configuration, the node metadata is not controlled by the user
const staticMeta = pick(meta, ['icon', 'subTitle']);
set(json, 'data.nodeMeta', {
...nodeMeta,
...staticMeta,
});
}
}
/**
* initialize node
* @param json
* @param doc
* @param isClone
*/
formatNodeOnInit(
json: WorkflowNodeJSON,
doc: WorkflowDocument,
isClone?: boolean,
): WorkflowNodeJSON {
// Non-cloning has been triggered in formatOnInit
if (!isClone) {
return json;
}
this.formatOutputVariables(json, doc);
this.formatInputVariables(json, doc);
return json;
}
/**
* commit node
* @param json
* @param doc
*/
formatNodeOnSubmit(
json: WorkflowNodeJSON,
doc: WorkflowDocument,
node: WorkflowNodeEntity,
): WorkflowNodeJSON {
const { nodeDTOType } = doc.getNodeRegister<WorkflowNodeRegistry>(
json.type,
).meta;
const variablesMeta = this.getVariablesMeta(json.type, doc);
if (json.data) {
// Convert output
variablesMeta.outputsPathList!.forEach(outputsPath => {
const variableMetas = get(json.data, outputsPath) as ViewVariableMeta[];
if (variableMetas && Array.isArray(variableMetas)) {
variableMetas.forEach((meta: ViewVariableMeta, index) => {
if (!meta.type) {
return;
}
const newData = variableUtils.viewMetaToDTOMeta(meta);
set(variableMetas, index, newData);
});
}
});
// Conversion input
variablesMeta.inputsPathList!.forEach(inputsPath => {
const inputValues: InputValueVO[] = get(
json.data,
inputsPath,
) as InputValueVO[];
if (inputValues && Array.isArray(inputValues)) {
inputValues.map((inputValue, index) => {
if (!inputValue) {
return;
}
set(
inputValues,
index,
variableUtils.inputValueToDTO(inputValue, this.variableService, {
node,
}),
);
});
// Filter out null values
set(json.data, inputsPath, inputValues.filter(Boolean));
}
});
}
json.type = String(nodeDTOType || json.type);
return json;
}
/**
* Convert variable data during initialization
* @param json
* @param document
*/
formatOnInit(json: WorkflowJSON, document: WorkflowDocument): WorkflowJSON {
// Step0: batch compatible processing
json.nodes.forEach(node => this.transformBatchVariable(node, document));
// Step1: Create an output variable
json.nodes.forEach(node => this.formatOutputVariables(node, document));
// Step2: Handle input median conversion
json.nodes.forEach(node => this.formatInputVariables(node, document));
// Step3: Process static copy data such as node description & subTitle (not from the drop database data, but from the latest node data)
json.nodes.forEach(node => this.formatNodeMeta(node, document));
return json;
}
/**
* Convert variable data at commit time
* @param json
*/
formatOnSubmit(json: WorkflowJSON, document: WorkflowDocument): WorkflowJSON {
json.nodes = (json.nodes || []).map(node => {
const registry = document.getNodeRegister<WorkflowNodeRegistry>(
node.type,
);
if (isFunction(registry?.beforeNodeSubmit)) {
return registry.beforeNodeSubmit(node);
}
return node;
});
console.log('------------------ save ----------------------', json);
return json;
}
}